diff --git a/frontend-web/src/pages/StudioNotebookDetailPage.tsx b/frontend-web/src/pages/StudioNotebookDetailPage.tsx index 83d367f4..bfb7ea8b 100644 --- a/frontend-web/src/pages/StudioNotebookDetailPage.tsx +++ b/frontend-web/src/pages/StudioNotebookDetailPage.tsx @@ -6,11 +6,13 @@ import { AlertCircle, AlignLeft, ArrowLeft, + ArrowUp, BookOpen, Files, Globe, Loader2, Plus, + Square, X, } from "lucide-react"; @@ -119,6 +121,20 @@ function SourceCard({ ); } +// ── 上传中占位卡片 ────────────────────────────────────── +function PendingSourceCard({ title }: { title: string }) { + return ( +
+
+ +
+
+

{title}

+
+
+ ); +} + // ── 来源骨架屏 ────────────────────────────────────────── function SourceCardSkeleton() { return ( @@ -137,6 +153,7 @@ function SourcesDialog({ sources, isLoadingSources, selectedSourceIds, + pendingSources, onToggle, onDelete, }: { @@ -146,6 +163,7 @@ function SourcesDialog({ sources: SourceItem[]; isLoadingSources: boolean; selectedSourceIds: number[]; + pendingSources: Map; onToggle: (id: number) => void; onDelete: (id: number) => void; }) { @@ -189,15 +207,26 @@ function SourcesDialog({
{isLoadingSources ? Array.from({ length: 8 }).map((_, i) => ) - : sources.map((src) => ( - - ))} + : <> + {Array.from(pendingSources.entries()) + .filter(([, s]) => !sources.some((src) => { + const srcTitle = src.title ?? (src.document as { title?: string } | undefined)?.title ?? ""; + return srcTitle === s.title; + })) + .map(([taskId, s]) => ( + + ))} + {sources.map((src) => ( + + ))} + + }
{!isLoadingSources && sources.length === 0 && (

暂无来源,请添加文件。

@@ -231,6 +260,8 @@ export default function StudioNotebookDetailPage({ // null = 显示历史线程列表;string = 显示该线程的聊天 const [activeThreadId, setActiveThreadId] = useState(null); const [citationChunkId, setCitationChunkId] = useState(null); + // 轮询成功后待选中的来源标题集合(后端上传时不返回 id,等 sources 刷新后按标题匹配) + const pendingSelectTitlesRef = useRef>(new Set()); const [threadsExpanded, setThreadsExpanded] = useState(false); const THREADS_LIMIT = 5; const [citationDoc, setCitationDoc] = useState(null); @@ -255,6 +286,22 @@ export default function StudioNotebookDetailPage({ setSelectedSourceIds(sources.map((s) => s.id)); } }, [sources, notebook?.id, initializedForNotebook]); + + // sources 刷新后,将轮询成功的来源(按标题匹配)自动选中 + useEffect(() => { + if (pendingSelectTitlesRef.current.size === 0) return; + const matched = sources.filter((s) => { + const title = s.title ?? (s.document as { title?: string } | undefined)?.title ?? ""; + return pendingSelectTitlesRef.current.has(title); + }); + if (matched.length > 0) { + setSelectedSourceIds((prev) => [...new Set([...prev, ...matched.map((s) => s.id)])]); + matched.forEach((s) => { + const title = s.title ?? (s.document as { title?: string } | undefined)?.title ?? ""; + pendingSelectTitlesRef.current.delete(title); + }); + } + }, [sources]); const citationRehypePlugins = useMemo(() => { const mapped = sources.map((s) => ({ id: s.id, @@ -299,6 +346,10 @@ export default function StudioNotebookDetailPage({ } }; + const handleStop = useCallback(() => { + void thread.stop(); + }, [thread]); + const handleSend = async (e: React.SyntheticEvent) => { e.preventDefault(); const text = input.trim(); @@ -360,20 +411,17 @@ export default function StudioNotebookDetailPage({ const data = await response.json() as { status: 'processing' | 'success' | 'failed' | 'error' }; const taskStatus = data.status === 'failed' ? 'error' : data.status; if (taskStatus !== 'processing') { - // 任务完成,从轮询队列移除并刷新来源列表 + // 先取出 source 信息,再从轮询队列移除 + const source = pendingSources.get(taskId); setPendingSources((prev) => { const newMap = new Map(prev); - const source = newMap.get(taskId); newMap.delete(taskId); - if (source) { - if (taskStatus === 'success') { - // toast.success(`来源 "${source.title}" 处理完成`); - } else { - // toast.error(`来源 "${source.title}" 处理失败`); - } - } return newMap; }); + // 仅成功时记录标题,等 sources 刷新后按标题匹配 id 并选中 + if (taskStatus === 'success' && source?.title) { + pendingSelectTitlesRef.current.add(source.title); + } // 刷新来源列表 void queryClient.invalidateQueries({ queryKey: ["studio", "sources"] }); // 刷新 notebooks 数据以更新 source_count @@ -383,7 +431,7 @@ export default function StudioNotebookDetailPage({ } catch (error) { console.error(`轮询任务 ${taskId} 状态失败:`, error); } - }, [notebook?.id, queryClient]); + }, [notebook?.id, pendingSources, queryClient]); // 控制轮询 - 只在对话框打开时轮询 useEffect(() => { @@ -585,7 +633,6 @@ export default function StudioNotebookDetailPage({ disabled={isStreaming} className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50" /> - {isStreaming && } {models.length > 0 && ( )} + {isStreaming ? ( + + ) : input.trim() ? ( + + ) : null} {/* 空状态提示 */} @@ -713,7 +778,6 @@ export default function StudioNotebookDetailPage({ disabled={isStreaming} className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50" /> - {isStreaming && } {models.length > 0 && ( )} + {isStreaming ? ( + + ) : input.trim() ? ( + + ) : null} @@ -771,6 +853,7 @@ export default function StudioNotebookDetailPage({ sources={sources} isLoadingSources={isLoadingSources} selectedSourceIds={selectedSourceIds} + pendingSources={pendingSources} onToggle={handleToggleSource} onDelete={handleDeleteSource} />