"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import { useApp } from "@/lib/app-context"; import { getImageUrl, fetchLlmModels } from "@/lib/api"; import type { LlmModelInfo } from "@/lib/types"; function timeAgo(ts: number): string { const diff = Date.now() - ts; const mins = Math.floor(diff / 60000); if (mins < 1) return "刚刚"; if (mins < 60) return `${mins}分钟前`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}小时前`; const days = Math.floor(hours / 24); if (days < 30) return `${days}天前`; return new Date(ts).toLocaleDateString("zh-CN"); } export function SessionList() { const { sessions, activeSessionId, tags, selectedTagIds, createSession, switchSession, deleteSession, renameSession, updateSessionLlmModel, } = useApp(); const [menuSessionId, setMenuSessionId] = useState(null); const [showModelSub, setShowModelSub] = useState(false); const [llmModels, setLlmModels] = useState([]); const [defaultLlmModel, setDefaultLlmModel] = useState(""); const [editingId, setEditingId] = useState(null); const [editTitle, setEditTitle] = useState(""); const editRef = useRef(null); const menuRef = useRef(null); const filtered = selectedTagIds.length === 0 ? sessions : sessions.filter((s) => selectedTagIds.some((tid) => s.tags.includes(tid))); const tagMap = new Map(tags.map((t) => [t.id, t])); useEffect(() => { fetchLlmModels() .then((data) => { setLlmModels(data.models); setDefaultLlmModel(data.default); }) .catch(() => {}); }, []); useEffect(() => { const handler = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { closeMenu(); } }; if (menuSessionId) { window.addEventListener("mousedown", handler); return () => window.removeEventListener("mousedown", handler); } }, [menuSessionId]); useEffect(() => { if (editingId) editRef.current?.focus(); }, [editingId]); const closeMenu = useCallback(() => { setMenuSessionId(null); setShowModelSub(false); }, []); const handleDots = (e: React.MouseEvent, sessionId: string) => { e.stopPropagation(); if (menuSessionId === sessionId) { closeMenu(); return; } setMenuSessionId(sessionId); setShowModelSub(false); }; const startRename = (id: string, currentTitle: string) => { setEditingId(id); setEditTitle(currentTitle); closeMenu(); }; const commitRename = () => { if (editingId && editTitle.trim()) { renameSession(editingId, editTitle.trim()); } setEditingId(null); }; const handleSelectModel = (sessionId: string, modelId: string) => { updateSessionLlmModel(sessionId, modelId); closeMenu(); }; const menuSession = menuSessionId ? sessions.find((s) => s.id === menuSessionId) : null; return (
{/* 新建按钮 */}
{/* 会话列表 */}
{filtered.map((session) => (
switchSession(session.id)} className={`group relative flex items-start gap-3 px-3 py-2.5 rounded-xl cursor-pointer transition-all ${ session.id === activeSessionId ? "glow-border !bg-[var(--bg-secondary)]" : "hover:bg-[var(--bg-tertiary)]/50 border border-transparent" }`} > {/* 缩略图 */} {session.thumbnail ? ( ) : (
)} {/* 会话信息 */}
{editingId === session.id ? ( setEditTitle(e.target.value)} onBlur={commitRename} onKeyDown={(e) => { if (e.key === "Enter") commitRename(); if (e.key === "Escape") setEditingId(null); }} className="w-full text-sm bg-transparent border-b border-[var(--accent)] text-[var(--text-primary)] focus:outline-none" /> ) : (
{session.title}
)} {/* 标签 + 时间 */}
{session.tags.slice(0, 3).map((tid) => { const tag = tagMap.get(tid); if (!tag) return null; return ( {tag.name} ); })} {session.llmModel && ( {llmModels.find((m) => m.id === session.llmModel)?.name || session.llmModel} )} {timeAgo(session.updatedAt)}
{/* 三点按钮 */}
{/* 下拉菜单:在会话项正下方展开 */} {menuSessionId === session.id && menuSession && (
e.stopPropagation()} > {/* 模型选择(折叠/展开式) */} {showModelSub && (
{llmModels.map((model) => { const isActive = menuSession.llmModel ? menuSession.llmModel === model.id : model.id === defaultLlmModel; return ( ); })}
)}
)}
))} {filtered.length === 0 && (
没有匹配的对话
)}
); }