feat(studio): replace notes source upload with multi-tab popup
- Add document-upload-popup.tsx with tabs for B系统/A系统/Y系统/XWT/upload - Add network-actor.tsx for XWT search tab - Add empty.tsx for empty state - Refactor SourcesDialog: left panel becomes single "上传来源" button that opens new popup - Wire DocumentUploadDialogProvider around StudioNotebookDetailPage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
2c2650ff07
commit
e6d3226c47
4 files changed
+1565
-131
No files matched your search
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
AlignLeft,
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useNotebookChat, useNotebookSources, useNotebookThreads, useStudioNotebooks } from "@/core/studio/hooks";
|
||||
import { fetchDocumentByChunk, uploadSourceFiles, uploadSourceText } from "@/core/studio/api";
|
||||
import { fetchDocumentByChunk } from "@/core/studio/api";
|
||||
import type { DocumentWithChunks } from "@/core/studio/api";
|
||||
import type { SourceItem } from "@/core/studio/types";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
@@ -38,6 +38,10 @@ import { MessageList } from "@/components/workspace/messages/message-list";
|
||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
import { ChatBox } from "@/components/workspace/chats/chat-box";
|
||||
import { rehypeCitations } from "@/core/rehype";
|
||||
import {
|
||||
DocumentUploadDialogProvider,
|
||||
useDocumentUploadDialog,
|
||||
} from "@/components/assistant-ui/document-upload-popup";
|
||||
|
||||
// ── 来源卡片图标 ────────────────────────────────────────
|
||||
function SourceIcon({ item }: { item: SourceItem }) {
|
||||
@@ -102,79 +106,31 @@ function SourceCardSkeleton() {
|
||||
);
|
||||
}
|
||||
|
||||
const UPLOAD_ACCEPT = ".pdf,.docx,.xlsx,.pptx,.html,.csv,.md,.txt,.jpg,.jpeg,.png,.bmp,.webp,.tiff,.mp3,.mp4,.wav,.webm";
|
||||
|
||||
// ── 来源 Dialog ─────────────────────────────────────────
|
||||
function SourcesDialog({
|
||||
open,
|
||||
onClose,
|
||||
notebookName,
|
||||
notebookId,
|
||||
token,
|
||||
sources,
|
||||
isLoadingSources,
|
||||
selectedSourceIds,
|
||||
onToggle,
|
||||
onUploadSuccess,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
notebookName: string;
|
||||
notebookId: number;
|
||||
token: string;
|
||||
sources: SourceItem[];
|
||||
isLoadingSources: boolean;
|
||||
selectedSourceIds: number[];
|
||||
onToggle: (id: number) => void;
|
||||
onUploadSuccess: () => void;
|
||||
}) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadError, setUploadError] = useState("");
|
||||
const [panel, setPanel] = useState<"sources" | "text">("sources");
|
||||
const [textContent, setTextContent] = useState("");
|
||||
const [textTitle, setTextTitle] = useState("");
|
||||
const [submittingText, setSubmittingText] = useState(false);
|
||||
const [textError, setTextError] = useState("");
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files ?? []);
|
||||
if (!files.length) return;
|
||||
setUploading(true);
|
||||
setUploadError("");
|
||||
try {
|
||||
await uploadSourceFiles(notebookId, files, token);
|
||||
onUploadSuccess();
|
||||
} catch (err) {
|
||||
setUploadError(err instanceof Error ? err.message : "上传失败");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextSubmit = async () => {
|
||||
if (!textContent.trim()) return;
|
||||
setSubmittingText(true);
|
||||
setTextError("");
|
||||
try {
|
||||
await uploadSourceText(notebookId, textContent.trim(), textTitle.trim(), token);
|
||||
setTextContent("");
|
||||
setTextTitle("");
|
||||
setPanel("sources");
|
||||
onUploadSuccess();
|
||||
} catch (err) {
|
||||
setTextError(err instanceof Error ? err.message : "上传失败");
|
||||
} finally {
|
||||
setSubmittingText(false);
|
||||
}
|
||||
};
|
||||
const { openDialog: openUploadPopup } = useDocumentUploadDialog();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className="flex h-[75vh] w-[900px] max-w-[calc(100vw-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-[900px]"
|
||||
className="flex h-[90vh] w-[75vw] max-w-[75vw] sm:max-w-[75vw] flex-col gap-0 overflow-hidden p-0"
|
||||
>
|
||||
<DialogHeader className="flex flex-row items-start justify-between border-b border-border px-6 py-5">
|
||||
<div>
|
||||
@@ -194,87 +150,33 @@ function SourcesDialog({
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
{/* 左侧导航 */}
|
||||
<div className="flex w-44 shrink-0 flex-col gap-1 border-r border-border p-3">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept={UPLOAD_ACCEPT}
|
||||
className="hidden"
|
||||
onChange={(e) => { void handleFileChange(e); }}
|
||||
/>
|
||||
<button
|
||||
disabled={uploading}
|
||||
onClick={() => { setPanel("sources"); fileInputRef.current?.click(); }}
|
||||
className="flex items-center gap-2.5 rounded-xl px-3 py-2.5 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
|
||||
onClick={() => openUploadPopup()}
|
||||
className="flex w-full flex-col items-center justify-center gap-3 rounded-2xl border-2 border-dashed border-border bg-muted/40 px-4 py-8 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-primary"
|
||||
>
|
||||
{uploading ? <Loader2 className="size-4 shrink-0 animate-spin" /> : <Plus className="size-4 shrink-0" />}
|
||||
<span className="text-left leading-tight">上传文件</span>
|
||||
<Plus className="size-8 shrink-0" />
|
||||
<span className="text-sm font-medium leading-tight text-center">上传来源</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setPanel("text"); setTextError(""); }}
|
||||
className={`flex items-center gap-2.5 rounded-xl px-3 py-2.5 text-sm transition-colors hover:bg-muted hover:text-foreground ${panel === "text" ? "bg-muted text-foreground" : "text-muted-foreground"}`}
|
||||
>
|
||||
<AlignLeft className="size-4 shrink-0" />
|
||||
<span className="text-left leading-tight">复制的文字</span>
|
||||
</button>
|
||||
{uploadError && (
|
||||
<p className="px-3 text-xs text-destructive">{uploadError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧内容 */}
|
||||
{panel === "sources" ? (
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{isLoadingSources
|
||||
? Array.from({ length: 8 }).map((_, i) => <SourceCardSkeleton key={i} />)
|
||||
: sources.map((src) => (
|
||||
<SourceCard
|
||||
key={src.id}
|
||||
item={src}
|
||||
selected={selectedSourceIds.includes(src.id)}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!isLoadingSources && sources.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">暂无来源,请添加文件。</p>
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{isLoadingSources
|
||||
? Array.from({ length: 8 }).map((_, i) => <SourceCardSkeleton key={i} />)
|
||||
: sources.map((src) => (
|
||||
<SourceCard
|
||||
key={src.id}
|
||||
item={src}
|
||||
selected={selectedSourceIds.includes(src.id)}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col gap-3 p-5">
|
||||
<input
|
||||
autoFocus
|
||||
value={textTitle}
|
||||
onChange={(e) => setTextTitle(e.target.value)}
|
||||
placeholder="标题(可选)"
|
||||
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
<textarea
|
||||
value={textContent}
|
||||
onChange={(e) => setTextContent(e.target.value)}
|
||||
placeholder="在此粘贴或输入文字内容…"
|
||||
className="flex-1 resize-none rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
{textError && <p className="text-xs text-destructive">{textError}</p>}
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setPanel("sources")}
|
||||
className="rounded-lg border border-border px-4 py-2 text-sm text-muted-foreground hover:bg-muted"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
disabled={!textContent.trim() || submittingText}
|
||||
onClick={() => void handleTextSubmit()}
|
||||
className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{submittingText && <Loader2 className="size-3.5 animate-spin" />}
|
||||
添加来源
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingSources && sources.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">暂无来源,请添加文件。</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -378,6 +280,12 @@ export default function StudioNotebookDetailPage() {
|
||||
const isChatView = activeThreadId !== null || hasMessages;
|
||||
|
||||
return (
|
||||
<DocumentUploadDialogProvider
|
||||
searchSpace={{ id: notebook.id }}
|
||||
onUploadSuccess={() => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["studio", "sources", notebook.id] });
|
||||
}}
|
||||
>
|
||||
<ThreadContext.Provider value={{ thread }}>
|
||||
<ChatBox threadId={chatThreadId ?? ""}>
|
||||
<div className="relative flex h-full flex-col overflow-hidden bg-background">
|
||||
@@ -667,18 +575,14 @@ export default function StudioNotebookDetailPage() {
|
||||
open={sourcesOpen}
|
||||
onClose={() => setSourcesOpen(false)}
|
||||
notebookName={notebook.name}
|
||||
notebookId={notebook.id}
|
||||
token={queryClient.getQueryData<string>(["studio", "token", getStoredAuth()?.email ?? ""]) ?? ""}
|
||||
sources={sources}
|
||||
isLoadingSources={isLoadingSources}
|
||||
selectedSourceIds={selectedSourceIds}
|
||||
onToggle={handleToggleSource}
|
||||
onUploadSuccess={() => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["studio", "sources", notebook.id] });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ChatBox>
|
||||
</ThreadContext.Provider>
|
||||
</DocumentUploadDialogProvider>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user