上传文件添加
This commit is contained in:
1 parent
4a63399363
commit
423abf4fde
2 files changed
+76
-3
No files matched your search
@@ -78,6 +78,28 @@ export async function fetchDocumentByChunk(
|
|||||||
return res.json() as Promise<DocumentWithChunks>;
|
return res.json() as Promise<DocumentWithChunks>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadSourceFiles(
|
||||||
|
notebookId: number,
|
||||||
|
files: File[],
|
||||||
|
token: string,
|
||||||
|
): Promise<void> {
|
||||||
|
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(
|
export async function fetchNotebookSources(
|
||||||
notebookId: number,
|
notebookId: number,
|
||||||
token: string,
|
token: string,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
AlignLeft,
|
AlignLeft,
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useNotebookChat, useNotebookSources, useNotebookThreads, useStudioNotebooks } from "@/core/studio/hooks";
|
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 { DocumentWithChunks } from "@/core/studio/api";
|
||||||
import type { SourceItem } from "@/core/studio/types";
|
import type { SourceItem } from "@/core/studio/types";
|
||||||
import { getStoredAuth } from "@/core/auth";
|
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 ─────────────────────────────────────────
|
// ── 来源 Dialog ─────────────────────────────────────────
|
||||||
function SourcesDialog({
|
function SourcesDialog({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
notebookName,
|
notebookName,
|
||||||
|
notebookId,
|
||||||
|
token,
|
||||||
sources,
|
sources,
|
||||||
isLoadingSources,
|
isLoadingSources,
|
||||||
selectedSourceIds,
|
selectedSourceIds,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
onUploadSuccess,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
notebookName: string;
|
notebookName: string;
|
||||||
|
notebookId: number;
|
||||||
|
token: string;
|
||||||
sources: SourceItem[];
|
sources: SourceItem[];
|
||||||
isLoadingSources: boolean;
|
isLoadingSources: boolean;
|
||||||
selectedSourceIds: number[];
|
selectedSourceIds: number[];
|
||||||
onToggle: (id: number) => void;
|
onToggle: (id: number) => void;
|
||||||
|
onUploadSuccess: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [uploadError, setUploadError] = 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 = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
@@ -144,8 +172,23 @@ function SourcesDialog({
|
|||||||
|
|
||||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
<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">
|
<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={() => 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"
|
||||||
|
>
|
||||||
|
{uploading ? <Loader2 className="size-4 shrink-0 animate-spin" /> : <Plus className="size-4 shrink-0" />}
|
||||||
|
<span className="text-left leading-tight">上传文件</span>
|
||||||
|
</button>
|
||||||
{[
|
{[
|
||||||
{ icon: Plus, label: "上传文件" },
|
|
||||||
{ icon: CloudUpload, label: "从云端硬盘添加" },
|
{ icon: CloudUpload, label: "从云端硬盘添加" },
|
||||||
{ icon: Globe, label: "添加网站" },
|
{ icon: Globe, label: "添加网站" },
|
||||||
{ icon: AlignLeft, label: "复制的文字" },
|
{ icon: AlignLeft, label: "复制的文字" },
|
||||||
@@ -158,6 +201,9 @@ function SourcesDialog({
|
|||||||
<span className="text-left leading-tight">{label}</span>
|
<span className="text-left leading-tight">{label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
{uploadError && (
|
||||||
|
<p className="px-3 text-xs text-destructive">{uploadError}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
@@ -496,10 +542,15 @@ export default function StudioNotebookDetailPage() {
|
|||||||
open={sourcesOpen}
|
open={sourcesOpen}
|
||||||
onClose={() => setSourcesOpen(false)}
|
onClose={() => setSourcesOpen(false)}
|
||||||
notebookName={notebook.name}
|
notebookName={notebook.name}
|
||||||
|
notebookId={notebook.id}
|
||||||
|
token={queryClient.getQueryData<string>(["studio", "token", getStoredAuth()?.email ?? ""]) ?? ""}
|
||||||
sources={sources}
|
sources={sources}
|
||||||
isLoadingSources={isLoadingSources}
|
isLoadingSources={isLoadingSources}
|
||||||
selectedSourceIds={selectedSourceIds}
|
selectedSourceIds={selectedSourceIds}
|
||||||
onToggle={handleToggleSource}
|
onToggle={handleToggleSource}
|
||||||
|
onUploadSuccess={() => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["studio", "sources", notebook.id] });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ChatBox>
|
</ChatBox>
|
||||||
|
|||||||
Reference in new issue
Block a user