diff --git a/apps/web/src/components/chat/DraftHeroHeadline.tsx b/apps/web/src/components/chat/DraftHeroHeadline.tsx index 3f50d3818b5..448e8bc2334 100644 --- a/apps/web/src/components/chat/DraftHeroHeadline.tsx +++ b/apps/web/src/components/chat/DraftHeroHeadline.tsx @@ -1,21 +1,28 @@ import type { ScopedProjectRef } from "@t3tools/contracts"; import { scopedProjectKey, scopeProjectRef } from "@t3tools/client-runtime/environment"; -import { FolderPlusIcon } from "lucide-react"; -import { useCallback, useMemo } from "react"; +import { FolderPlusIcon, SearchIcon } from "lucide-react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { openCommandPalette } from "~/commandPaletteBus"; import { useNewThreadHandler } from "~/hooks/useHandleNewThread"; import { useProjects, useThreadShells } from "~/state/entities"; import { sortScopedProjectsForSidebar } from "../Sidebar.logic"; import { - Menu, - MenuItem, - MenuPopup, - MenuRadioGroup, - MenuRadioItem, - MenuSeparator, - MenuTrigger, -} from "../ui/menu"; + Combobox, + ComboboxInput, + ComboboxItem, + ComboboxListVirtualized, + ComboboxPopup, + ComboboxSeparator, + ComboboxTrigger, +} from "../ui/combobox"; +import { + DRAFT_HERO_NEW_PROJECT_ITEM, + filterDraftHeroProjects, + getDraftHeroFilteredPickerItems, + getDraftHeroProjectPickerItemValue, + resolveDraftHeroPickerKeyAction, +} from "./draftHeroProjectSearch"; interface DraftHeroHeadlineProps { readonly activeProjectRef: ScopedProjectRef | null; @@ -30,66 +37,173 @@ export function DraftHeroHeadline({ const threads = useThreadShells(); const handleNewThread = useNewThreadHandler(); const openAddProject = useCallback(() => openCommandPalette({ open: "add-project" }), []); + const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false); + const [projectQuery, setProjectQuery] = useState(""); + const hasNewProjectKeyboardIntentRef = useRef(false); const orderedProjects = useMemo( () => sortScopedProjectsForSidebar(projects, threads, "updated_at"), [projects, threads], ); + const filteredProjects = useMemo( + () => filterDraftHeroProjects(orderedProjects, projectQuery), + [orderedProjects, projectQuery], + ); + const filteredProjectItems = useMemo( + () => + getDraftHeroFilteredPickerItems( + filteredProjects.map((project) => + getDraftHeroProjectPickerItemValue( + scopedProjectKey(scopeProjectRef(project.environmentId, project.id)), + ), + ), + ), + [filteredProjects], + ); const projectByKey = useMemo( () => new Map( orderedProjects.map( (project) => [ - scopedProjectKey(scopeProjectRef(project.environmentId, project.id)), + getDraftHeroProjectPickerItemValue( + scopedProjectKey(scopeProjectRef(project.environmentId, project.id)), + ), project, ] as const, ), ), [orderedProjects], ); - const activeProjectKey = activeProjectRef === null ? "" : scopedProjectKey(activeProjectRef); + const activeProjectKey = + activeProjectRef === null + ? "" + : getDraftHeroProjectPickerItemValue(scopedProjectKey(activeProjectRef)); const hasResolvedProject = activeProjectTitle !== null; const canChooseProject = orderedProjects.length > 0; const shouldShowProjectMenu = canChooseProject; + const hasFailedProjectSearch = projectQuery.trim().length > 0 && filteredProjects.length === 0; const projectSelector = shouldShowProjectMenu ? ( - - + getDraftHeroProjectPickerItemValue( + scopedProjectKey(scopeProjectRef(project.environmentId, project.id)), + ), + ), + DRAFT_HERO_NEW_PROJECT_ITEM, + ]} + filteredItems={filteredProjectItems} + open={isProjectPickerOpen} + value={activeProjectKey} + onOpenChange={(open) => { + setIsProjectPickerOpen(open); + if (!open) { + hasNewProjectKeyboardIntentRef.current = false; + setProjectQuery(""); + } + }} + onValueChange={(value) => { + if (value === DRAFT_HERO_NEW_PROJECT_ITEM) { + setIsProjectPickerOpen(false); + openAddProject(); + return; + } + if (!value || value === activeProjectKey) { + setIsProjectPickerOpen(false); + return; + } + const project = projectByKey.get(value); + if (!project) { + return; + } + setIsProjectPickerOpen(false); + void handleNewThread(scopeProjectRef(project.environmentId, project.id), { + replace: true, + }); + }} + > + {activeProjectTitle ?? "Choose a project"} - - - { - const project = projectByKey.get(value as string); - if (!project || value === activeProjectKey) { - return; - } - void handleNewThread(scopeProjectRef(project.environmentId, project.id), { - replace: true, - }); - }} - > - {orderedProjects.map((project) => { - const key = scopedProjectKey(scopeProjectRef(project.environmentId, project.id)); - return ( - - {project.title} - - ); - })} - - - - - New project - - - + + +
+
+
+
+ {filteredProjects.length === 0 ? ( +
+ No matching projects. +
+ ) : null} + +
+ {filteredProjects.map((project) => { + const key = getDraftHeroProjectPickerItemValue( + scopedProjectKey(scopeProjectRef(project.environmentId, project.id)), + ); + return ( + + {project.title} + + ); + })} +
+
+ + + + New project + +
+
+
+ ) : (