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 { WorkspaceNavStudio } from "./workspace-nav-studio";
import { cn } from "@/lib/utils";
import { env } from "@/env";
const studioEnabled = env.STUDIO_ENABLED?.toLowerCase() === "true";
// 普通导航项:默认中性色,激活态与「新对话」一致——主色浅底 + 主色文字/图标
const NAV_ITEM_CLASS =
@@ -165,7 +168,7 @@ export function WorkspaceNavChatList({
)}
</SidebarMenu>
</SidebarGroup>
{!showCollapseTrigger && <WorkspaceNavStudio />}
{!showCollapseTrigger && studioEnabled && <WorkspaceNavStudio />}
</>
);
}
+22
View File
@@ -78,6 +78,28 @@ export async function fetchDocumentByChunk(
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(
notebookId: number,
token: string,
+3 -1
View File
@@ -8,7 +8,8 @@ function readEnv(
| "VITE_NOTE_API_BASE_URL"
| "VITE_NOTEBOOK_API_BASE_URL"
| "VITE_STATIC_WEBSITE_ONLY"
| "VITE_GITHUB_OAUTH_TOKEN",
| "VITE_GITHUB_OAUTH_TOKEN"
| "VITE_STUDIO_ENABLED",
): EnvValue {
const value = import.meta.env[key];
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_STATIC_WEBSITE_ONLY: readEnv("VITE_STATIC_WEBSITE_ONLY"),
GITHUB_OAUTH_TOKEN: readEnv("VITE_GITHUB_OAUTH_TOKEN"),
STUDIO_ENABLED: readEnv("VITE_STUDIO_ENABLED"),
};
@@ -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<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 (
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
<DialogContent
@@ -144,8 +172,23 @@ 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={() => 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: Globe, label: "添加网站" },
{ icon: AlignLeft, label: "复制的文字" },
@@ -158,6 +201,9 @@ function SourcesDialog({
<span className="text-left leading-tight">{label}</span>
</button>
))}
{uploadError && (
<p className="px-3 text-xs text-destructive">{uploadError}</p>
)}
</div>
<div className="flex-1 overflow-y-auto p-4">
@@ -496,10 +542,15 @@ 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>