diff --git a/frontend-web/src/core/studio/api.ts b/frontend-web/src/core/studio/api.ts index 16e8d90a..29dff88e 100644 --- a/frontend-web/src/core/studio/api.ts +++ b/frontend-web/src/core/studio/api.ts @@ -78,6 +78,28 @@ export async function fetchDocumentByChunk( return res.json() as Promise; } +export async function uploadSourceFiles( + notebookId: number, + files: File[], + token: string, +): Promise { + const apiBase = base(); + if (!apiBase) throw new Error("VITE_NOTEBOOK_API_BASE_URL 未配置"); + + const formData = new FormData(); + formData.append("search_space_id", String(notebookId)); + for (const file of files) { + formData.append("files", file); + } + + const res = await fetch(`${apiBase}/api/v1/documents/fileupload`, { + method: "POST", + headers: { Authorization: `Bearer ${token}` }, + body: formData, + }); + if (!res.ok) throw new Error(`上传失败:${res.status}`); +} + export async function fetchNotebookSources( notebookId: number, token: string, diff --git a/frontend-web/src/pages/StudioNotebookDetailPage.tsx b/frontend-web/src/pages/StudioNotebookDetailPage.tsx index 71af9c3f..67a4fd2c 100644 --- a/frontend-web/src/pages/StudioNotebookDetailPage.tsx +++ b/frontend-web/src/pages/StudioNotebookDetailPage.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useParams } from "react-router-dom"; import { AlignLeft, @@ -23,7 +23,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { useNotebookChat, useNotebookSources, useNotebookThreads, useStudioNotebooks } from "@/core/studio/hooks"; -import { fetchDocumentByChunk } from "@/core/studio/api"; +import { fetchDocumentByChunk, uploadSourceFiles } from "@/core/studio/api"; import type { DocumentWithChunks } from "@/core/studio/api"; import type { SourceItem } from "@/core/studio/types"; import { getStoredAuth } from "@/core/auth"; @@ -103,24 +103,52 @@ 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(null); + const [uploading, setUploading] = useState(false); + const [uploadError, setUploadError] = useState(""); + + const handleFileChange = async (e: React.ChangeEvent) => { + 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 = ""; + } + }; + return ( !v && onClose()}>
+ { void handleFileChange(e); }} + /> + {[ - { icon: Plus, label: "上传文件" }, { icon: CloudUpload, label: "从云端硬盘添加" }, { icon: Globe, label: "添加网站" }, { icon: AlignLeft, label: "复制的文字" }, @@ -158,6 +201,9 @@ function SourcesDialog({ {label} ))} + {uploadError && ( +

{uploadError}

+ )}
@@ -496,10 +542,15 @@ export default function StudioNotebookDetailPage() { open={sourcesOpen} onClose={() => setSourcesOpen(false)} notebookName={notebook.name} + notebookId={notebook.id} + token={queryClient.getQueryData(["studio", "token", getStoredAuth()?.email ?? ""]) ?? ""} sources={sources} isLoadingSources={isLoadingSources} selectedSourceIds={selectedSourceIds} onToggle={handleToggleSource} + onUploadSuccess={() => { + void queryClient.invalidateQueries({ queryKey: ["studio", "sources", notebook.id] }); + }} />