修改名称 笔记本改为 空间

This commit is contained in:
xuhd committed 2026-06-11 11:19:26 +08:00
1 parent 06508cfb7c
commit 82d14351fc
12 files changed
+446 -393

No files matched your search

@@ -230,7 +230,7 @@ export default function LightAppNoteListPage() {
isDark ? "text-slate-100" : "text-slate-950",
)}
>
我的笔记本
我的空间
</h1>
<div className="flex items-center gap-2">
@@ -77,11 +77,10 @@ function SourceCard({
<div
title={title}
onClick={() => onToggle(item.id)}
className={`group relative flex h-32 w-full cursor-pointer flex-col justify-between rounded-2xl p-4 ring-1 transition-colors ${
selected
className={`group relative flex h-32 w-full cursor-pointer flex-col justify-between rounded-2xl p-4 ring-1 transition-colors ${selected
? "bg-primary/10 ring-primary"
: "bg-muted ring-border/50 hover:bg-muted/70"
}`}
}`}
>
{/* 删除按钮 - 右上角始终显示,悬停时高亮 */}
<button
@@ -181,14 +180,14 @@ function SourcesDialog({
{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}
onDelete={onDelete}
/>
))}
<SourceCard
key={src.id}
item={src}
selected={selectedSourceIds.includes(src.id)}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</div>
{!isLoadingSources && sources.length === 0 && (
<p className="text-sm text-muted-foreground">暂无来源,请添加文件。</p>
@@ -432,7 +431,7 @@ export default function StudioNotebookDetailPage() {
if (!notebook) {
return (
<div className="flex h-full flex-col items-center justify-center bg-background">
<p className="text-muted-foreground">笔记本不存在</p>
<p className="text-muted-foreground">空间不存在</p>
</div>
);
}
@@ -453,308 +452,308 @@ export default function StudioNotebookDetailPage() {
}}
onUploadComplete={handleUploadComplete}
>
<ThreadContext.Provider value={{ thread }}>
<ChatBox threadId={chatThreadId ?? ""}>
<div className="relative flex h-full flex-col overflow-hidden bg-background">
{/* 背景渐变(列表视图时显示) */}
{!isChatView && (
<div
className="pointer-events-none absolute inset-0"
style={{
background:
"linear-gradient(145deg, hsl(var(--primary) / 0.22) 0%, hsl(var(--primary) / 0.10) 45%, transparent 80%)",
}}
/>
)}
{/* 聊天视图:顶部栏 */}
{isChatView && (
<div className="flex shrink-0 items-center justify-between border-b border-border px-6 py-3">
<div className="flex items-center gap-3">
<button
onClick={handleBackToList}
className="rounded-lg p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
>
<ArrowLeft className="size-4" />
</button>
<div className="flex size-8 items-center justify-center rounded-xl bg-muted">
{notebook.avatar_url ? (
<img src={notebook.avatar_url} alt="" className="size-5 rounded-lg object-contain" />
) : (
<Files className="size-4 text-muted-foreground" />
)}
</div>
<span className="text-sm font-medium text-foreground">{notebook.name}</span>
</div>
<div className="flex items-center gap-2">
<a
href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat`}
target="_blank"
rel="noreferrer"
className="flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/70"
>
<BookOpen className="size-3" />
<span>我的笔记</span>
</a>
<button
onClick={() => setSourcesOpen(true)}
className="flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/70"
>
<Files className="size-3" />
<span>{notebook.source_count} 个来源</span>
</button>
</div>
</div>
)}
{/* 列表视图:笔记本标题 + 历史线程列表 */}
{!isChatView && (
<div className="relative z-10 flex flex-1 flex-col overflow-hidden">
{/* 右上角跳转按钮 */}
<a
href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat`}
target="_blank"
rel="noreferrer"
className="absolute right-6 top-4 z-10 flex items-center gap-1.5 rounded-full bg-muted/70 px-3 py-1.5 text-xs text-muted-foreground backdrop-blur transition-colors hover:bg-muted"
>
<BookOpen className="size-3.5" />
<span>我的笔记</span>
</a>
{/* 垂直居中主区域:图标 → 标题 → 来源 → 输入框 → 空状态/历史 */}
<div className="flex flex-1 flex-col items-center justify-center overflow-y-auto px-8 pb-12 pt-16">
<div className="flex size-14 items-center justify-center leading-none select-none">
{notebook.avatar_url ? (
<img src={notebook.avatar_url} alt="" className="size-12 rounded-xl object-contain" />
) : (
<Files className="size-10 text-foreground/50" />
)}
</div>
<h1 className="mt-5 max-w-xl text-center text-3xl font-normal leading-snug text-foreground">
{notebook.name}
</h1>
<button
onClick={() => setSourcesOpen(true)}
className="mt-4 flex cursor-pointer items-center gap-2 rounded-full bg-muted/70 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur transition-colors hover:bg-muted"
>
<Files className="size-3.5" />
<span>{notebook.source_count} 个来源</span>
</button>
{/* 输入框(列表视图内联) */}
<form
onSubmit={(e) => { void handleSend(e); }}
className="relative z-10 mt-8 flex w-full max-w-[680px] items-center gap-3 rounded-full bg-card px-5 py-3.5 shadow-md ring-1 ring-border"
>
<button
type="button"
onClick={() => {
void queryClient.invalidateQueries({ queryKey: ["studio", "sources"] });
setSourcesOpen(true);
<ThreadContext.Provider value={{ thread }}>
<ChatBox threadId={chatThreadId ?? ""}>
<div className="relative flex h-full flex-col overflow-hidden bg-background">
{/* 背景渐变(列表视图时显示) */}
{!isChatView && (
<div
className="pointer-events-none absolute inset-0"
style={{
background:
"linear-gradient(145deg, hsl(var(--primary) / 0.22) 0%, hsl(var(--primary) / 0.10) 45%, transparent 80%)",
}}
className="shrink-0 text-muted-foreground hover:text-foreground"
>
<Plus className="size-5" />
</button>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="问问 AI..."
disabled={isStreaming}
className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50"
/>
{isStreaming && <Loader2 className="size-4 shrink-0 animate-spin text-muted-foreground" />}
{models.length > 0 && (
<Select value={selectedModel} onValueChange={setSelectedModel}>
<SelectTrigger className="h-7 w-auto shrink-0 gap-1 rounded-full border-0 bg-transparent px-2 text-sm text-muted-foreground shadow-none focus:ring-0 hover:bg-muted/60">
<SelectValue />
</SelectTrigger>
<SelectContent>
{models.map((m) => (
<SelectItem key={m.name} value={m.name} className="text-xs">
{m.display_name || m.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</form>
{/* 空状态提示 */}
{(!notebookThreads || notebookThreads.length === 0) && (
<div className="mt-8 flex items-center gap-2 text-sm text-foreground/40">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
笔记本对话会显示在这里
</div>
)}
{/* 历史线程列表 */}
{notebookThreads && notebookThreads.length > 0 && (
<div className="mt-8 w-full max-w-[680px] flex flex-col gap-2">
<p className="mb-1 text-xs font-medium text-foreground/40">历史对话</p>
<div className="flex flex-col gap-2 overflow-y-auto" style={{ maxHeight: "260px" }}>
{(threadsExpanded ? notebookThreads : notebookThreads.slice(0, THREADS_LIMIT)).map((t) => {
const values = t.values as Record<string, unknown> | undefined;
const msgs = values?.messages as Array<{ type: string; content: unknown }> | undefined;
const firstHuman = msgs?.find((m) => m.type === "human");
const rawContent = firstHuman?.content;
const preview =
typeof rawContent === "string"
? rawContent.slice(0, 60)
: Array.isArray(rawContent)
? String((rawContent[0] as { text?: string })?.text ?? "").slice(0, 60)
: "对话";
const title = (values?.title as string | undefined) || preview || "对话";
const time = new Date(t.updated_at ?? "").toLocaleString("zh-CN", {
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
return (
<button
key={t.thread_id}
onClick={() => {
setActiveThreadId(t.thread_id);
setThreadId(t.thread_id);
}}
className="flex items-center justify-between rounded-xl bg-muted/50 px-4 py-3 text-left transition-colors hover:bg-muted"
>
<span className="truncate text-sm font-medium text-foreground">{title}</span>
<span className="ml-4 shrink-0 text-xs text-foreground/50">{time}</span>
</button>
);
})}
</div>
{notebookThreads.length > THREADS_LIMIT && (
{/* 聊天视图:顶部栏 */}
{isChatView && (
<div className="flex shrink-0 items-center justify-between border-b border-border px-6 py-3">
<div className="flex items-center gap-3">
<button
onClick={() => setThreadsExpanded((v) => !v)}
className="mt-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={handleBackToList}
className="rounded-lg p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
>
{threadsExpanded ? "收起" : `查看全部 ${notebookThreads.length} 条对话`}
<ArrowLeft className="size-4" />
</button>
)}
<div className="flex size-8 items-center justify-center rounded-xl bg-muted">
{notebook.avatar_url ? (
<img src={notebook.avatar_url} alt="" className="size-5 rounded-lg object-contain" />
) : (
<Files className="size-4 text-muted-foreground" />
)}
</div>
<span className="text-sm font-medium text-foreground">{notebook.name}</span>
</div>
<div className="flex items-center gap-2">
<a
href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat`}
target="_blank"
rel="noreferrer"
className="flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/70"
>
<BookOpen className="size-3" />
<span>我的笔记</span>
</a>
<button
onClick={() => setSourcesOpen(true)}
className="flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/70"
>
<Files className="size-3" />
<span>{notebook.source_count} 个来源</span>
</button>
</div>
</div>
)}
</div>
</div>
)}
{/* 聊天视图:消息列表 */}
{isChatView && (
<div
className="min-h-0 flex-1 overflow-hidden"
onClick={(e) => {
const sup = (e.target as HTMLElement).closest<HTMLElement>("[data-chunk-id]");
if (!sup) return;
const chunkId = Number(sup.dataset.chunkId);
if (!chunkId) return;
setCitationChunkId(chunkId);
setCitationDoc(null);
setCitationLoading(true);
const email = getStoredAuth()?.email ?? "";
const tok = queryClient.getQueryData<string>(["studio", "token", email]) ?? "";
void fetchDocumentByChunk(chunkId, tok)
.then((doc) => setCitationDoc(doc))
.finally(() => setCitationLoading(false));
}}
>
<MessageList
threadId={chatThreadId ?? ""}
thread={thread}
paddingBottom={120}
extraRehypePlugins={citationRehypePlugins}
onClarificationSubmit={handleClarificationSubmit}
/>
</div>
)}
{/* 列表视图:空间标题 + 历史线程列表 */}
{!isChatView && (
<div className="relative z-10 flex flex-1 flex-col overflow-hidden">
{/* 右上角跳转按钮 */}
<a
href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat`}
target="_blank"
rel="noreferrer"
className="absolute right-6 top-4 z-10 flex items-center gap-1.5 rounded-full bg-muted/70 px-3 py-1.5 text-xs text-muted-foreground backdrop-blur transition-colors hover:bg-muted"
>
<BookOpen className="size-3.5" />
<span>我的笔记</span>
</a>
{/* 聊天视图的底部输入框 */}
<div className={`relative z-10 shrink-0 px-6 ${isChatView ? "border-t border-border py-4" : "hidden"}`}>
<form
onSubmit={(e) => { void handleSend(e); }}
className="mx-auto flex max-w-[720px] items-center gap-3 rounded-full bg-card px-5 py-3.5 shadow-md ring-1 ring-border"
>
<button
type="button"
onClick={() => setSourcesOpen(true)}
className="shrink-0 text-muted-foreground hover:text-foreground"
>
<Plus className="size-5" />
</button>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="问问 AI..."
disabled={isStreaming}
className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50"
/>
{isStreaming && <Loader2 className="size-4 shrink-0 animate-spin text-muted-foreground" />}
{models.length > 0 && (
<Select value={selectedModel} onValueChange={setSelectedModel}>
<SelectTrigger className="h-7 w-auto shrink-0 gap-1 rounded-full border-0 bg-transparent px-2 text-sm text-muted-foreground shadow-none focus:ring-0 hover:bg-muted/60">
<SelectValue />
</SelectTrigger>
<SelectContent>
{models.map((m) => (
<SelectItem key={m.name} value={m.name} className="text-xs">
{m.display_name || m.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</form>
</div>
{/* 垂直居中主区域:图标 → 标题 → 来源 → 输入框 → 空状态/历史 */}
<div className="flex flex-1 flex-col items-center justify-center overflow-y-auto px-8 pb-12 pt-16">
<div className="flex size-14 items-center justify-center leading-none select-none">
{notebook.avatar_url ? (
<img src={notebook.avatar_url} alt="" className="size-12 rounded-xl object-contain" />
) : (
<Files className="size-10 text-foreground/50" />
)}
</div>
<h1 className="mt-5 max-w-xl text-center text-3xl font-normal leading-snug text-foreground">
{notebook.name}
</h1>
<button
onClick={() => setSourcesOpen(true)}
className="mt-4 flex cursor-pointer items-center gap-2 rounded-full bg-muted/70 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur transition-colors hover:bg-muted"
>
<Files className="size-3.5" />
<span>{notebook.source_count} 个来源</span>
</button>
{/* 引用来源弹窗 */}
<Dialog open={citationChunkId !== null} onOpenChange={(v) => { if (!v) setCitationChunkId(null); }}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-base">引用来源</DialogTitle>
</DialogHeader>
{citationLoading && (
<div className="flex justify-center py-8">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
)}
{!citationLoading && citationDoc && (
<div className="flex flex-col gap-4 pt-1">
<div className="flex items-start gap-3">
<span className="mt-0.5 shrink-0 rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground uppercase">
{citationDoc.file_type || citationDoc.document_type}
</span>
<span className="text-sm font-medium text-foreground leading-snug">{citationDoc.title}</span>
</div>
{citationDoc.chunks.find((c) => c.id === citationChunkId) && (
<div className="max-h-60 overflow-y-auto rounded-xl bg-muted p-4 text-sm text-foreground leading-relaxed">
{citationDoc.chunks.find((c) => c.id === citationChunkId)!.content}
{/* 输入框(列表视图内联) */}
<form
onSubmit={(e) => { void handleSend(e); }}
className="relative z-10 mt-8 flex w-full max-w-[680px] items-center gap-3 rounded-full bg-card px-5 py-3.5 shadow-md ring-1 ring-border"
>
<button
type="button"
onClick={() => {
void queryClient.invalidateQueries({ queryKey: ["studio", "sources"] });
setSourcesOpen(true);
}}
className="shrink-0 text-muted-foreground hover:text-foreground"
>
<Plus className="size-5" />
</button>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="问问 AI..."
disabled={isStreaming}
className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50"
/>
{isStreaming && <Loader2 className="size-4 shrink-0 animate-spin text-muted-foreground" />}
{models.length > 0 && (
<Select value={selectedModel} onValueChange={setSelectedModel}>
<SelectTrigger className="h-7 w-auto shrink-0 gap-1 rounded-full border-0 bg-transparent px-2 text-sm text-muted-foreground shadow-none focus:ring-0 hover:bg-muted/60">
<SelectValue />
</SelectTrigger>
<SelectContent>
{models.map((m) => (
<SelectItem key={m.name} value={m.name} className="text-xs">
{m.display_name || m.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</form>
{/* 空状态提示 */}
{(!notebookThreads || notebookThreads.length === 0) && (
<div className="mt-8 flex items-center gap-2 text-sm text-foreground/40">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
空间对话会显示在这里
</div>
)}
{/* 历史线程列表 */}
{notebookThreads && notebookThreads.length > 0 && (
<div className="mt-8 w-full max-w-[680px] flex flex-col gap-2">
<p className="mb-1 text-xs font-medium text-foreground/40">历史对话</p>
<div className="flex flex-col gap-2 overflow-y-auto" style={{ maxHeight: "260px" }}>
{(threadsExpanded ? notebookThreads : notebookThreads.slice(0, THREADS_LIMIT)).map((t) => {
const values = t.values as Record<string, unknown> | undefined;
const msgs = values?.messages as Array<{ type: string; content: unknown }> | undefined;
const firstHuman = msgs?.find((m) => m.type === "human");
const rawContent = firstHuman?.content;
const preview =
typeof rawContent === "string"
? rawContent.slice(0, 60)
: Array.isArray(rawContent)
? String((rawContent[0] as { text?: string })?.text ?? "").slice(0, 60)
: "对话";
const title = (values?.title as string | undefined) || preview || "对话";
const time = new Date(t.updated_at ?? "").toLocaleString("zh-CN", {
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
return (
<button
key={t.thread_id}
onClick={() => {
setActiveThreadId(t.thread_id);
setThreadId(t.thread_id);
}}
className="flex items-center justify-between rounded-xl bg-muted/50 px-4 py-3 text-left transition-colors hover:bg-muted"
>
<span className="truncate text-sm font-medium text-foreground">{title}</span>
<span className="ml-4 shrink-0 text-xs text-foreground/50">{time}</span>
</button>
);
})}
</div>
{notebookThreads.length > THREADS_LIMIT && (
<button
onClick={() => setThreadsExpanded((v) => !v)}
className="mt-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
{threadsExpanded ? "收起" : `查看全部 ${notebookThreads.length} 条对话`}
</button>
)}
</div>
)}
</div>
)}
</div>
)}
{!citationLoading && !citationDoc && (
<p className="py-4 text-sm text-muted-foreground">加载失败,请重试。</p>
)}
</DialogContent>
</Dialog>
</div>
)}
{/* 来源 Dialog */}
<SourcesDialog
open={sourcesOpen}
onClose={() => setSourcesOpen(false)}
notebookName={notebook.name}
sources={sources}
isLoadingSources={isLoadingSources}
selectedSourceIds={selectedSourceIds}
onToggle={handleToggleSource}
onDelete={handleDeleteSource}
/>
</div>
</ChatBox>
</ThreadContext.Provider>
{/* 聊天视图:消息列表 */}
{isChatView && (
<div
className="min-h-0 flex-1 overflow-hidden"
onClick={(e) => {
const sup = (e.target as HTMLElement).closest<HTMLElement>("[data-chunk-id]");
if (!sup) return;
const chunkId = Number(sup.dataset.chunkId);
if (!chunkId) return;
setCitationChunkId(chunkId);
setCitationDoc(null);
setCitationLoading(true);
const email = getStoredAuth()?.email ?? "";
const tok = queryClient.getQueryData<string>(["studio", "token", email]) ?? "";
void fetchDocumentByChunk(chunkId, tok)
.then((doc) => setCitationDoc(doc))
.finally(() => setCitationLoading(false));
}}
>
<MessageList
threadId={chatThreadId ?? ""}
thread={thread}
paddingBottom={120}
extraRehypePlugins={citationRehypePlugins}
onClarificationSubmit={handleClarificationSubmit}
/>
</div>
)}
{/* 聊天视图的底部输入框 */}
<div className={`relative z-10 shrink-0 px-6 ${isChatView ? "border-t border-border py-4" : "hidden"}`}>
<form
onSubmit={(e) => { void handleSend(e); }}
className="mx-auto flex max-w-[720px] items-center gap-3 rounded-full bg-card px-5 py-3.5 shadow-md ring-1 ring-border"
>
<button
type="button"
onClick={() => setSourcesOpen(true)}
className="shrink-0 text-muted-foreground hover:text-foreground"
>
<Plus className="size-5" />
</button>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="问问 AI..."
disabled={isStreaming}
className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50"
/>
{isStreaming && <Loader2 className="size-4 shrink-0 animate-spin text-muted-foreground" />}
{models.length > 0 && (
<Select value={selectedModel} onValueChange={setSelectedModel}>
<SelectTrigger className="h-7 w-auto shrink-0 gap-1 rounded-full border-0 bg-transparent px-2 text-sm text-muted-foreground shadow-none focus:ring-0 hover:bg-muted/60">
<SelectValue />
</SelectTrigger>
<SelectContent>
{models.map((m) => (
<SelectItem key={m.name} value={m.name} className="text-xs">
{m.display_name || m.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</form>
</div>
{/* 引用来源弹窗 */}
<Dialog open={citationChunkId !== null} onOpenChange={(v) => { if (!v) setCitationChunkId(null); }}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-base">引用来源</DialogTitle>
</DialogHeader>
{citationLoading && (
<div className="flex justify-center py-8">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
)}
{!citationLoading && citationDoc && (
<div className="flex flex-col gap-4 pt-1">
<div className="flex items-start gap-3">
<span className="mt-0.5 shrink-0 rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground uppercase">
{citationDoc.file_type || citationDoc.document_type}
</span>
<span className="text-sm font-medium text-foreground leading-snug">{citationDoc.title}</span>
</div>
{citationDoc.chunks.find((c) => c.id === citationChunkId) && (
<div className="max-h-60 overflow-y-auto rounded-xl bg-muted p-4 text-sm text-foreground leading-relaxed">
{citationDoc.chunks.find((c) => c.id === citationChunkId)!.content}
</div>
)}
</div>
)}
{!citationLoading && !citationDoc && (
<p className="py-4 text-sm text-muted-foreground">加载失败,请重试。</p>
)}
</DialogContent>
</Dialog>
{/* 来源 Dialog */}
<SourcesDialog
open={sourcesOpen}
onClose={() => setSourcesOpen(false)}
notebookName={notebook.name}
sources={sources}
isLoadingSources={isLoadingSources}
selectedSourceIds={selectedSourceIds}
onToggle={handleToggleSource}
onDelete={handleDeleteSource}
/>
</div>
</ChatBox>
</ThreadContext.Provider>
</DocumentUploadDialogProvider>
);
}
@@ -180,7 +180,7 @@ export default function StudioNotebooksPage() {
<div className="min-h-full bg-background p-8">
{/* Header */}
<div className="mb-8 flex items-center justify-between">
<h1 className="text-2xl font-semibold text-foreground">笔记本</h1>
<h1 className="text-2xl font-semibold text-foreground">空间</h1>
<button
onClick={() => { setCreateOpen(true); setCreateError(""); setCreateName(""); }}
className="flex items-center gap-1.5 rounded-full bg-primary px-5 py-2.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
@@ -191,7 +191,7 @@ export default function StudioNotebooksPage() {
</div>
{isError && (
<p className="text-sm text-muted-foreground">加载笔记本失败,请稍后重试。</p>
<p className="text-sm text-muted-foreground">加载空间失败,请稍后重试。</p>
)}
{/* Grid */}
@@ -220,7 +220,7 @@ export default function StudioNotebooksPage() {
value={createName}
onChange={(e) => setCreateName(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") void handleCreate(); }}
placeholder="笔记本名称"
placeholder="空间名称"
disabled={creating}
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
/>
@@ -239,7 +239,7 @@ export default function StudioNotebooksPage() {
<Dialog open={!!renameTarget} onOpenChange={(v) => !renaming && !v && setRenameTarget(null)}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>重命名笔记本</DialogTitle>
<DialogTitle>重命名空间</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 pt-2">
<div className="flex items-center gap-3">
@@ -272,12 +272,12 @@ export default function StudioNotebooksPage() {
<Dialog open={!!deleteTarget} onOpenChange={(v) => !deleting && !v && setDeleteTarget(null)}>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>删除笔记本</DialogTitle>
<DialogTitle>删除空间</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 pt-1">
<p className="text-sm text-muted-foreground">
确定要删除{deleteTarget ? <span className="font-medium text-foreground">「{deleteTarget.name}」</span> : ""}吗?
删除后笔记本内所有来源数据将一并移除,且无法恢复。
删除后空间内所有来源数据将一并移除,且无法恢复。
</p>
{deleteError && <p className="text-xs text-destructive">{deleteError}</p>}
<div className="flex justify-end gap-2">