import { useState, useEffect, useRef, useMemo } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import { toast } from 'sonner'; import { ArrowLeft, Pencil, Plus, Check, X, Trash2, Search } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { useClient } from 'hooks/useClient'; import { useVisibleClaudeModels } from '@/state/useModels'; import { Card } from '@/components/Card'; import type { ChatMessage } from 'widgets/Chat'; import { useClaude } from '@/Screens/Dashboard/Chat/useClaude'; import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat'; type ResourceSummary = { dirName: string; name: string; description: string; scope: 'global' | 'user'; }; type ResourceDetail = ResourceSummary & { body: string; filePath: string; chatSessionId: string | null; }; type ResourcePageProps = { kind: string; endpoint: string; queryKey: string; }; type ResourceChatProps = { kind: string; endpoint: string; dirName: string; filePath: string; resourceDir: string; chatSessionId: string | null; isNew?: boolean; onResponseEnd?: () => void; }; const ResourceChat = ({ kind, endpoint, dirName, filePath, resourceDir, chatSessionId, isNew, onResponseEnd, }: ResourceChatProps) => { const client = useClient(); const claudeModels = useVisibleClaudeModels(); const defaultInput = chatSessionId ? undefined : isNew ? `Help me create the content for this new ${kind} file: ${filePath}` : `Help me understand and improve this ${kind} file: ${filePath}`; const storage = useMemo( () => ({ load: async () => { const data = await client.get<{ sessionId: string | null; messages: ChatMessage[] }>( `${endpoint}/${dirName}/chat`, ); return { sessionId: data.sessionId, messages: data.messages ?? [] }; }, save: async (sessionId: string, messages: ChatMessage[]) => { await client.put(`${endpoint}/${dirName}/chat`, { sessionId, messages }); }, }), [endpoint, dirName], ); const claude = useClaude(chatSessionId ?? undefined, undefined, { replaceUrl: false, storage, resourceChatDir: resourceDir, }); const onResponseEndRef = useRef(onResponseEnd); onResponseEndRef.current = onResponseEnd; const wasGenerating = useRef(false); useEffect(() => { if (wasGenerating.current && !claude.isGenerating) { onResponseEndRef.current?.(); } wasGenerating.current = claude.isGenerating; }, [claude.isGenerating]); return ( ); }; export const ResourcePage = ({ kind, endpoint, queryKey }: ResourcePageProps) => { const client = useClient(); const qc = useQueryClient(); const [selected, setSelected] = useState(null); const [editing, setEditing] = useState(false); const [isNew, setIsNew] = useState(false); const [creating, setCreating] = useState(false); const [newName, setNewName] = useState(''); const [deleteConfirm, setDeleteConfirm] = useState(false); const [search, setSearch] = useState(''); const [showDetail, setShowDetail] = useState(false); const newNameRef = useRef(null); const { data: items = [] } = useQuery({ queryKey: [queryKey], queryFn: () => client.get(endpoint), }); useEffect(() => { if (items.length > 0 && !selected) { setSelected(items[0]!.dirName); } }, [items, selected]); const { data: detail } = useQuery({ queryKey: [queryKey, selected], queryFn: () => client.get(`${endpoint}/${selected}`), enabled: !!selected, }); const selectItem = (dirName: string) => { setSelected(dirName); setShowDetail(true); setIsNew(false); setEditing(false); }; const handleCreate = async () => { const name = newName.trim(); if (!name) return; try { const res = await client.post<{ name: string; dirName: string }>(endpoint, { name }); await qc.invalidateQueries({ queryKey: [queryKey] }); setCreating(false); setNewName(''); setSelected(res.dirName); setShowDetail(true); setIsNew(true); setEditing(true); } catch { toast.error(`Failed to create ${kind}`); } }; const handleDelete = async () => { if (!selected) return; try { await client.delete(`${endpoint}/${selected}`); setDeleteConfirm(false); setEditing(false); setSelected(null); setShowDetail(false); await qc.invalidateQueries({ queryKey: [queryKey] }); } catch { toast.error(`Failed to delete ${kind}`); } }; const filtered = items.filter( (item) => !search || item.name.toLowerCase().includes(search.toLowerCase()) || item.description?.toLowerCase().includes(search.toLowerCase()), ); return ( <>
{/* Left panel — list */}
{kind}s {!creating && ( )}
{creating && (
setNewName(ev.target.value)} onKeyDown={(ev) => { if (ev.key === 'Enter') { ev.preventDefault(); handleCreate(); } if (ev.key === 'Escape') { setCreating(false); setNewName(''); } }} placeholder={`${kind} name...`} className="flex-1 min-w-0 rounded border border-duck-dark/20 bg-white px-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30" />
)}
setSearch(ev.target.value)} placeholder={`Search ${kind.toLowerCase()}s...`} className="w-full rounded border border-duck-dark/15 bg-white/80 pl-7 pr-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30" />
{filtered.map((item) => ( ))} {items.length === 0 && (

No {kind.toLowerCase()}s found

)} {items.length > 0 && filtered.length === 0 && (

No matches

)}
{/* Right panel — detail + chat */}
{detail?.name ?? `Select a ${kind.toLowerCase()}`} {detail && ( <> )}
{detail?.body ? (
{detail.body}
) : detail ? (

Empty file

) : (

Select a {kind.toLowerCase()} to view its contents

)}
{editing && detail?.filePath && selected && (
{detail?.name ?? 'Chat'}
qc.invalidateQueries({ queryKey: [queryKey, selected] })} />
)}
Delete {kind} Are you sure you want to delete "{detail?.name}"? This action cannot be undone.
); };