"use client"; import * as React from "react"; import Link from "next/link"; import { Check, FolderMinus, MessageSquare, MessageSquarePlus, Plus, Search } from "lucide-react"; import { useApp, invalidateConversations, invalidateProjects } from "@/components/app/store"; import { api, useApi } from "@/lib/client/api"; import { errorMessage } from "@/lib/client/humanize"; import { useIsMobile, useLongPress } from "@/lib/client/hooks"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ResponsiveDialog, type ActionSheetItem } from "@/components/ui/sheet"; import { EmptyState, Skeleton } from "@/components/ui/misc"; import { toast } from "@/components/ui/toast"; import { ProviderIcon } from "@/components/brand/provider-icon"; import type { PublicConversation } from "@/lib/client/types"; import { formatRelative, cn } from "@/lib/utils"; import { RowMenu } from "./row-menu"; interface Unassigned { id: string; title: string; modelKey: string | null; updatedAt: string; messageCount: number; } /** Conversations tab: rows linking to chats, remove-from-project, and an "Add existing chats" picker. */ export function ProjectConversations({ projectId, conversations, onChanged, onNewChat }: { projectId: string; conversations: PublicConversation[]; onChanged: () => Promise | void; onNewChat: () => void }) { const [adding, setAdding] = React.useState(false); const remove = async (c: PublicConversation) => { try { await api(`/api/projects/${projectId}`, { method: "POST", json: { action: "remove-conversations", conversationIds: [c.id] } }); await Promise.all([onChanged(), invalidateConversations(), invalidateProjects()]); toast.success("Removed from project", "The chat is still in your history."); } catch (e) { toast.error("Could not remove chat", errorMessage(e)); } }; return (

{conversations.length} chat{conversations.length === 1 ? "" : "s"}

{conversations.length === 0 ? ( } title="No chats in this project yet" description="Start a chat here — it will use the project's instructions and preferred models — or move existing chats in." action={
} /> ) : (
    {conversations.map((c) => ( , onSelect: () => void remove(c) }]} /> ))}
)}
); } function ConversationRow({ c, items }: { c: PublicConversation; items: (ActionSheetItem | "separator")[] }) { const { modelsByKey } = useApp(); const isMobile = useIsMobile(); const [menu, setMenu] = React.useState(false); const press = useLongPress({ onLongPress: () => setMenu(true), disabled: !isMobile }); const model = c.modelKey ? modelsByKey.get(c.modelKey) : undefined; return (
  • {c.provider ? : } {c.title} {model?.displayName ?? c.modelKey ?? "No model"} · {c.messageCount} message{c.messageCount === 1 ? "" : "s"} · {formatRelative(c.updatedAt)}
  • ); } function AddConversationsSheet({ projectId, open, onOpenChange, onAdded }: { projectId: string; open: boolean; onOpenChange: (o: boolean) => void; onAdded: () => Promise | void }) { const { modelsByKey } = useApp(); const { data, isLoading } = useApi<{ conversations: Unassigned[] }>(open ? `/api/projects/${projectId}?unassigned=1` : null); const [selected, setSelected] = React.useState>(new Set()); const [q, setQ] = React.useState(""); const [busy, setBusy] = React.useState(false); React.useEffect(() => { if (!open) return; // eslint-disable-next-line react-hooks/set-state-in-effect setSelected(new Set()); setQ(""); }, [open]); const list = React.useMemo(() => { const s = q.trim().toLowerCase(); return (data?.conversations ?? []).filter((c) => (s ? c.title.toLowerCase().includes(s) : true)); }, [data, q]); const toggle = (id: string) => setSelected((prev) => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n; }); const add = async () => { setBusy(true); try { const res = await api<{ moved: number }>(`/api/projects/${projectId}`, { method: "POST", json: { action: "add-conversations", conversationIds: [...selected] } }); await Promise.all([onAdded(), invalidateConversations(), invalidateProjects()]); toast.success(`${res.moved} chat${res.moved === 1 ? "" : "s"} added to the project`); onOpenChange(false); } catch (e) { toast.error("Could not add chats", errorMessage(e)); } finally { setBusy(false); } }; return (
    setQ(e.target.value)} placeholder="Search chats…" className="h-10 pl-8 sm:h-9" aria-label="Search chats" />
    } footer={
    {selected.size} selected
    } >
    {isLoading && !data ? (
    {Array.from({ length: 5 }).map((_, i) => ( ))}
    ) : list.length === 0 ? ( ) : (
      {list.map((c) => { const on = selected.has(c.id); const model = c.modelKey ? modelsByKey.get(c.modelKey) : undefined; return (
    • ); })}
    )}
    ); }