"use client" import { useState, useEffect, useRef, useCallback, useMemo } from "react" import { formatDistanceToNow } from "date-fns" import { enUS, zhCN, zhTW } from "date-fns/locale" import { File, Folder } from "lucide-react" import { useLocale, useTranslations } from "next-intl" import { useAuxPanelContext } from "@/contexts/aux-panel-context" import { useActiveFolder } from "@/contexts/active-folder-context" import { useAppWorkspace } from "@/contexts/app-workspace-context" import { useTabContext } from "@/contexts/tab-context" import { useWorkspaceContext } from "@/contexts/workspace-context" import { listAllConversations } from "@/lib/api" import type { AgentType, ConversationStatus, DbConversationSummary, } from "@/lib/types" import { useFileTree, type FlatFileEntry } from "@/hooks/use-file-tree" import { AGENT_LABELS, STATUS_ICON_COLORS, compareAgentType } from "@/lib/types" import { AgentIcon } from "@/components/agent-icon" import { ConversationStatusIcon } from "@/components/conversations/conversation-status-icon" import { CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, } from "@/components/ui/command" import { cn } from "@/lib/utils" type SearchTab = "conversations" | "files" interface SearchCommandDialogProps { open: boolean onOpenChange: (open: boolean) => void } export function SearchCommandDialog({ open, onOpenChange, }: SearchCommandDialogProps) { const t = useTranslations("Folder.search") const locale = useLocale() const dateFnsLocale = locale === "zh-CN" ? zhCN : locale === "zh-TW" ? zhTW : enUS const { activeFolder: folder, activeFolderId } = useActiveFolder() const { conversations: allConversations } = useAppWorkspace() const folderId = activeFolderId ?? 0 const conversations = useMemo( () => activeFolderId == null ? [] : allConversations.filter((c) => c.folder_id === activeFolderId), [allConversations, activeFolderId] ) const { openTab } = useTabContext() const { openFilePreview } = useWorkspaceContext() const { revealInFileTree } = useAuxPanelContext() const [activeTab, setActiveTab] = useState("conversations") const [query, setQuery] = useState("") const [agentFilter, setAgentFilter] = useState(null) const [results, setResults] = useState([]) const [searching, setSearching] = useState(false) const debounceRef = useRef>(undefined) const folderPath = folder?.path ?? "" // File search via shared hook (lazy-loaded when files tab is active) const { allFiles, loading: filesLoading, reset: resetFileTree, } = useFileTree({ folderPath: folderPath || undefined, enabled: activeTab === "files", }) // Compute which agent types exist in current folder const availableAgents = Array.from( new Set(conversations.map((c) => c.agent_type)) ).sort(compareAgentType) // Filter files by query using pre-computed lowercase fields const filteredFiles = useMemo(() => { const trimmed = query.trim() if (!trimmed) return allFiles.slice(0, 100) const lower = trimmed.toLowerCase() const matched: FlatFileEntry[] = [] for (const f of allFiles) { if (f.lowerName.includes(lower) || f.lowerPath.includes(lower)) { matched.push(f) if (matched.length >= 100) break } } return matched }, [allFiles, query]) const doSearch = useCallback( async (q: string, agent: AgentType | null) => { if (!q.trim() && !agent) { setResults([]) setSearching(false) return } setSearching(true) try { const data = await listAllConversations({ folder_ids: folderId > 0 ? [folderId] : null, search: q.trim() || null, agent_type: agent, }) setResults(data) } catch { setResults([]) } finally { setSearching(false) } }, [folderId] ) // Debounced search on query change (conversations tab only) useEffect(() => { if (activeTab !== "conversations") return if (debounceRef.current) clearTimeout(debounceRef.current) debounceRef.current = setTimeout(() => { doSearch(query, agentFilter) }, 300) return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, [query, agentFilter, doSearch, activeTab]) // Reset state when dialog closes useEffect(() => { if (!open) { setQuery("") setAgentFilter(null) setResults([]) setActiveTab("conversations") resetFileTree() } }, [open, resetFileTree]) const handleSelectConversation = useCallback( (conv: DbConversationSummary) => { openTab(conv.folder_id, conv.id, conv.agent_type, true) onOpenChange(false) }, [openTab, onOpenChange] ) const handleSelectFile = useCallback( (entry: FlatFileEntry) => { if (entry.kind === "dir") { revealInFileTree(entry.relativePath) } else { // Reveal parent directory in file tree, then open the file const lastSlash = entry.relativePath.lastIndexOf("/") if (lastSlash > 0) { revealInFileTree(entry.relativePath.slice(0, lastSlash)) } openFilePreview(entry.relativePath) } onOpenChange(false) }, [revealInFileTree, openFilePreview, onOpenChange] ) const placeholder = activeTab === "conversations" ? t("placeholder") : t("filePlaceholder") return ( {/* Tabs */}
{/* Agent filter (conversations tab only) */} {activeTab === "conversations" && availableAgents.length > 1 && (
{availableAgents.map((at) => ( ))}
)} {/* Conversations tab */} {activeTab === "conversations" && ( <> {searching ? t("searching") : !query.trim() && !agentFilter ? t("typeToSearch") : t("noResults")} {results.length > 0 && ( {results.map((conv) => ( handleSelectConversation(conv)} > {conv.title || t("untitledConversation")} {AGENT_LABELS[conv.agent_type]} {formatDistanceToNow(new Date(conv.created_at), { addSuffix: true, locale: dateFnsLocale, })} ))} )} )} {/* Files tab */} {activeTab === "files" && ( <> {filesLoading ? t("searching") : !query.trim() ? t("typeToSearchFiles") : t("noResults")} {filteredFiles.length > 0 && ( {filteredFiles.map((entry) => ( handleSelectFile(entry)} > {entry.kind === "dir" ? ( ) : ( )} {entry.name} {entry.relativePath} ))} )} )}
) }