TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { Check, CircleOff, FolderKanban, Plus } from "lucide-react";4import { useRouter } from "next/navigation";5import { useApp } from "@/components/app/store";6import { useApi } from "@/lib/client/api";7import { useIsMobile } from "@/lib/client/hooks";8import { ActionSheet, type ActionSheetItem } from "@/components/ui/sheet";9import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";10import { toast } from "@/components/ui/toast";11import { ProjectIcon } from "./project-icon";12import type { PublicProject } from "@/lib/client/types";1314/**15 * Active-project quick switcher. Phone: iOS-style ActionSheet; desktop: dropdown.16 * Wrap any trigger (`children`); selecting sets `activeProjectId` (new chats inherit the project).17 */18export function ProjectSwitcher({ children, onNavigate }: { children: React.ReactElement; onNavigate?: () => void }) {19 const { activeProjectId, setActiveProjectId } = useApp();20 const isMobile = useIsMobile();21 const router = useRouter();22 const [open, setOpen] = React.useState(false);23 const { data } = useApi<{ projects: PublicProject[] }>(open || !isMobile ? "/api/projects" : null);24 const projects = React.useMemo(() => (data?.projects ?? []).filter((p) => !p.archived), [data]);2526 const choose = (id: string | null) => {27 setActiveProjectId(id);28 const p = id ? projects.find((x) => x.id === id) : null;29 toast.info(p ? `Active project: ${p.name}` : "No active project", p ? "New chats will use its instructions and models." : "New chats start without project context.");30 };3132 if (isMobile) {33 const items: (ActionSheetItem | "separator")[] = [34 { key: "none", label: "No project", icon: <CircleOff />, selected: !activeProjectId, onSelect: () => choose(null) },35 ...projects.map<ActionSheetItem>((p) => ({ key: p.id, label: p.name, icon: <ProjectIcon icon={p.icon} color={p.color} size="sm" />, selected: p.id === activeProjectId, hint: p.conversationCount ? `${p.conversationCount}` : undefined, onSelect: () => choose(p.id) })),36 "separator",37 {38 key: "new",39 label: "New project",40 icon: <Plus />,41 onSelect: () => {42 onNavigate?.();43 router.push("/app/projects?new=1");44 },45 },46 ];47 return (48 <>49 {React.cloneElement(children as React.ReactElement<{ onClick?: () => void }>, { onClick: () => setOpen(true) })}50 <ActionSheet open={open} onOpenChange={setOpen} title="Active project" description="New chats inherit the active project." items={items} />51 </>52 );53 }5455 return (56 <DropdownMenu open={open} onOpenChange={setOpen}>57 <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>58 <DropdownMenuContent align="end" className="w-64">59 <DropdownMenuLabel className="flex items-center gap-1.5">60 <FolderKanban className="size-3.5" /> Active project61 </DropdownMenuLabel>62 <DropdownMenuItem onSelect={() => choose(null)}>63 <CircleOff /> <span className="flex-1">No project</span> {!activeProjectId ? <Check className="text-accent" /> : null}64 </DropdownMenuItem>65 {projects.length ? <DropdownMenuSeparator /> : null}66 {projects.map((p) => (67 <DropdownMenuItem key={p.id} onSelect={() => choose(p.id)}>68 <ProjectIcon icon={p.icon} color={p.color} size="sm" />69 <span className="min-w-0 flex-1 truncate">{p.name}</span>70 {p.id === activeProjectId ? <Check className="text-accent" /> : null}71 </DropdownMenuItem>72 ))}73 <DropdownMenuSeparator />74 <DropdownMenuItem75 onSelect={() => {76 onNavigate?.();77 router.push("/app/projects?new=1");78 }}79 >80 <Plus /> New project81 </DropdownMenuItem>82 </DropdownMenuContent>83 </DropdownMenu>84 );85}86