SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
3.9 KB · 86 lines tsx
Raw Blame History
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