This commit is contained in:
liuq_1233 committed 2026-05-29 15:16:20 +08:00
commit 57289251a0
4 files changed
+83 -5

No files matched your search

@@ -19,6 +19,9 @@ import { BotIcon, BrainIcon, ListChecks, MessageSquarePlus, MessagesSquare, PenL
import { SidebarCollapseButton } from "./workspace-header"; import { SidebarCollapseButton } from "./workspace-header";
import { WorkspaceNavStudio } from "./workspace-nav-studio"; import { WorkspaceNavStudio } from "./workspace-nav-studio";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { env } from "@/env";
const studioEnabled = env.STUDIO_ENABLED?.toLowerCase() === "true";
// 普通导航项:默认中性色,激活态与「新对话」一致——主色浅底 + 主色文字/图标 // 普通导航项:默认中性色,激活态与「新对话」一致——主色浅底 + 主色文字/图标
const NAV_ITEM_CLASS = const NAV_ITEM_CLASS =
@@ -165,7 +168,7 @@ export function WorkspaceNavChatList({
)} )}
</SidebarMenu> </SidebarMenu>
</SidebarGroup> </SidebarGroup>
{!showCollapseTrigger && <WorkspaceNavStudio />} {!showCollapseTrigger && studioEnabled && <WorkspaceNavStudio />}
</> </>
); );
} }
+22
View File
@@ -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,
+3 -1
View File
@@ -8,7 +8,8 @@ function readEnv(
| "VITE_NOTE_API_BASE_URL" | "VITE_NOTE_API_BASE_URL"
| "VITE_NOTEBOOK_API_BASE_URL" | "VITE_NOTEBOOK_API_BASE_URL"
| "VITE_STATIC_WEBSITE_ONLY" | "VITE_STATIC_WEBSITE_ONLY"
| "VITE_GITHUB_OAUTH_TOKEN", | "VITE_GITHUB_OAUTH_TOKEN"
| "VITE_STUDIO_ENABLED",
): EnvValue { ): EnvValue {
const value = import.meta.env[key]; const value = import.meta.env[key];
return typeof value === "string" && value.trim() ? value : undefined; return typeof value === "string" && value.trim() ? value : undefined;
@@ -24,4 +25,5 @@ export const env = {
NEXT_PUBLIC_NOTEBOOK_API_BASE_URL: readEnv("VITE_NOTEBOOK_API_BASE_URL"), NEXT_PUBLIC_NOTEBOOK_API_BASE_URL: readEnv("VITE_NOTEBOOK_API_BASE_URL"),
NEXT_PUBLIC_STATIC_WEBSITE_ONLY: readEnv("VITE_STATIC_WEBSITE_ONLY"), NEXT_PUBLIC_STATIC_WEBSITE_ONLY: readEnv("VITE_STATIC_WEBSITE_ONLY"),
GITHUB_OAUTH_TOKEN: readEnv("VITE_GITHUB_OAUTH_TOKEN"), GITHUB_OAUTH_TOKEN: readEnv("VITE_GITHUB_OAUTH_TOKEN"),
STUDIO_ENABLED: readEnv("VITE_STUDIO_ENABLED"),
}; };
@@ -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>