import { Plus, MessageSquare, Trash2, Search } from "lucide-react"; import { useState, useRef } from "react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; export type ThreadItem = { thread_id: string; created_at: string; metadata?: Record; }; type Props = { threads: ThreadItem[]; currentThreadId: string | null; onNewThread: () => void; onSelectThread: (threadId: string) => void; onDeleteThread: (threadId: string) => void; }; // Get lastActive timestamp from localStorage, fallback to created_at function getLastActive(threadId: string, createdAt: string): number { try { const stored = localStorage.getItem(`lastActive_${threadId}`); if (stored) return parseInt(stored, 10); } catch { // ignore } return new Date(createdAt).getTime(); } // Get thread title from localStorage (saved by main.tsx on first message) function getLocalTitle(threadId: string): string | null { try { return localStorage.getItem(`title_${threadId}`); } catch { return null; } } // Reject titles that look like UUIDs, short hashes, or empty strings const BAD_TITLE = /^[0-9a-f-]{8,}$/i; function sanitizeTitle(s: unknown): string | null { if (typeof s !== "string" || !s.trim() || BAD_TITLE.test(s.trim())) return null; return s.trim(); } function groupByDate(threads: ThreadItem[]): { label: string; threads: ThreadItem[] }[] { const now = new Date(); const groups: Record = {}; // Sort by lastActive descending before grouping const sorted = [...threads].sort( (a, b) => getLastActive(b.thread_id, b.created_at) - getLastActive(a.thread_id, a.created_at), ); sorted.forEach((t) => { const lastActive = getLastActive(t.thread_id, t.created_at); const diffDays = (now.getTime() - lastActive) / 86400000; let label: string; if (diffDays < 1) label = "今天"; else if (diffDays < 2) label = "昨天"; else if (diffDays < 7) label = "本周"; else label = "更早"; if (!groups[label]) groups[label] = []; groups[label].push(t); }); return ["今天", "昨天", "本周", "更早"] .filter((label) => groups[label]?.length) .map((label) => ({ label, threads: groups[label] })); } export function updateThreadLastActive(threadId: string) { try { localStorage.setItem(`lastActive_${threadId}`, String(Date.now())); } catch { // ignore } } function formatTime(iso: string) { try { const d = new Date(iso); const now = new Date(); const diffMs = now.getTime() - d.getTime(); const diffHrs = diffMs / (1000 * 60 * 60); if (diffHrs < 24) { return d.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }); } return d.toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit" }); } catch { return ""; } } export function ThreadSidebar({ threads, currentThreadId, onNewThread, onSelectThread, onDeleteThread, }: Props) { const [searchQuery, setSearchQuery] = useState(""); const [searchExpanded, setSearchExpanded] = useState(false); const searchInputRef = useRef(null); const filtered = threads.filter((t) => { const label = sanitizeTitle(getLocalTitle(t.thread_id)) ?? sanitizeTitle(t.metadata?.title) ?? sanitizeTitle(t.metadata?.firstMessage) ?? "新对话"; return label.toLowerCase().includes(searchQuery.toLowerCase()); }); const groups = groupByDate(filtered); return (
{/* New chat button */}
{/* Search toggle icon */}
{/* Collapsible search input */}
setSearchQuery(e.target.value)} onBlur={() => { if (!searchQuery) { setSearchExpanded(false); } }} placeholder="搜索对话..." className="w-full text-xs border border-border rounded-md pl-8 pr-3 py-1.5 bg-background/50 text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring" />
{/* Thread list */}
{filtered.length === 0 && (

{searchQuery ? "未找到匹配对话" : "暂无历史对话"}

)} {groups.map(({ label, threads: groupThreads }) => (
{/* Group heading */}

{label}

{groupThreads.map((t) => { const isActive = t.thread_id === currentThreadId; const itemLabel = sanitizeTitle(getLocalTitle(t.thread_id)) ?? sanitizeTitle(t.metadata?.title) ?? sanitizeTitle(t.metadata?.firstMessage) ?? "新对话"; const displayLabel = itemLabel.length > 16 ? itemLabel.slice(0, 16) + "…" : itemLabel; return (
onSelectThread(t.thread_id)} >

16 ? itemLabel : undefined} > {displayLabel}

{formatTime(t.created_at)}

); })}
))}
); }