修改名称 笔记本改为 空间

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

No files matched your search

@@ -102,7 +102,7 @@ export function WorkspaceNavStudio() {
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
{/* 笔记本列表(最多展示 SIDEBAR_PREVIEW_COUNT 条) */} {/* 空间列表(最多展示 SIDEBAR_PREVIEW_COUNT 条) */}
{preview.map((nb) => ( {preview.map((nb) => (
<SidebarMenuItem key={nb.id}> <SidebarMenuItem key={nb.id}>
<SidebarMenuButton <SidebarMenuButton
@@ -152,7 +152,7 @@ export function WorkspaceNavStudio() {
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") void handleCreate(); }} onKeyDown={(e) => { if (e.key === "Enter") void handleCreate(); }}
placeholder="笔记本名称" placeholder="空间名称"
disabled={creating} 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" 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"
/> />
+1 -1
View File
@@ -69,7 +69,7 @@ export function getLangGraphBaseURL(isMock?: boolean) {
} }
} }
/** 笔记本外部系统后端 API 地址,通过 VITE_AIPI_BACKENP_URL 配置。 */ /** 空间外部系统后端 API 地址,通过 VITE_AIPI_BACKENP_URL 配置。 */
export function getNotebookApiBaseURL(): string { export function getNotebookApiBaseURL(): string {
if (env.NEXT_PUBLIC_NOTEBOOK_API_BASE_URL) { if (env.NEXT_PUBLIC_NOTEBOOK_API_BASE_URL) {
return env.NEXT_PUBLIC_NOTEBOOK_API_BASE_URL.replace(/\/+$/, ""); return env.NEXT_PUBLIC_NOTEBOOK_API_BASE_URL.replace(/\/+$/, "");
+26 -7
View File
@@ -20,14 +20,16 @@ export async function fetchStudioToken(email: string): Promise<string> {
return token; return token;
} }
export async function fetchStudioNotebooks(token: string): Promise<StudioNotebook[]> { export async function fetchStudioNotebooks(
token: string,
): Promise<StudioNotebook[]> {
const apiBase = base(); const apiBase = base();
if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置"); if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置");
const res = await fetch(`${apiBase}/api/v1/searchspaces`, { const res = await fetch(`${apiBase}/api/v1/searchspaces`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (!res.ok) throw new Error(`获取笔记本列表失败:${res.status}`); if (!res.ok) throw new Error(`获取空间列表失败:${res.status}`);
return res.json() as Promise<StudioNotebook[]>; return res.json() as Promise<StudioNotebook[]>;
} }
@@ -62,13 +64,23 @@ export async function uploadSourceText(
if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置"); if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置");
const res = await fetch(`${apiBase}/api/v1/documents/textupload`, { const res = await fetch(`${apiBase}/api/v1/documents/textupload`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, headers: {
body: JSON.stringify({ search_space_id: notebookId, content, title: title || "粘贴的文字" }), "Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
search_space_id: notebookId,
content,
title: title || "粘贴的文字",
}),
}); });
if (!res.ok) throw new Error(`上传失败:${res.status}`); if (!res.ok) throw new Error(`上传失败:${res.status}`);
} }
export async function deleteSearchSpace(id: number, token: string): Promise<void> { export async function deleteSearchSpace(
id: number,
token: string,
): Promise<void> {
const apiBase = base(); const apiBase = base();
if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置"); if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置");
const res = await fetch(`${apiBase}/api/v1/searchspaces/${id}`, { const res = await fetch(`${apiBase}/api/v1/searchspaces/${id}`, {
@@ -78,12 +90,19 @@ export async function deleteSearchSpace(id: number, token: string): Promise<void
if (!res.ok) throw new Error(`删除失败:${res.status}`); if (!res.ok) throw new Error(`删除失败:${res.status}`);
} }
export async function renameSearchSpace(id: number, name: string, token: string): Promise<void> { export async function renameSearchSpace(
id: number,
name: string,
token: string,
): Promise<void> {
const apiBase = base(); const apiBase = base();
if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置"); if (!apiBase) throw new Error("VITE_AIPI_FRONTEND_URL 未配置");
const res = await fetch(`${apiBase}/api/v1/searchspaces/${id}`, { const res = await fetch(`${apiBase}/api/v1/searchspaces/${id}`, {
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ name }), body: JSON.stringify({ name }),
}); });
if (!res.ok) throw new Error(`重命名失败:${res.status}`); if (!res.ok) throw new Error(`重命名失败:${res.status}`);
+19 -5
View File
@@ -4,7 +4,11 @@ import { useStream } from "@langchain/langgraph-sdk/react";
import { getStoredAuth } from "@/core/auth"; import { getStoredAuth } from "@/core/auth";
import { getAPIClient } from "@/core/api"; import { getAPIClient } from "@/core/api";
import { fetchNotebookSources, fetchStudioNotebooks, fetchStudioToken } from "./api"; import {
fetchNotebookSources,
fetchStudioNotebooks,
fetchStudioToken,
} from "./api";
import type { AgentThreadState } from "@/core/threads/types"; import type { AgentThreadState } from "@/core/threads/types";
import type { SourceItem, StudioNotebook } from "./types"; import type { SourceItem, StudioNotebook } from "./types";
@@ -30,7 +34,7 @@ export function useStudioNotebooks(enabled = true): {
queryKey: ["studio", "notebooks", tokenQuery.data], queryKey: ["studio", "notebooks", tokenQuery.data],
queryFn: () => fetchStudioNotebooks(tokenQuery.data!), queryFn: () => fetchStudioNotebooks(tokenQuery.data!),
enabled: enabled && Boolean(tokenQuery.data), enabled: enabled && Boolean(tokenQuery.data),
// 笔记本列表不会高频变动;新建/删除路径已通过 invalidateQueries 主动失效 // 空间列表不会高频变动;新建/删除路径已通过 invalidateQueries 主动失效
// (见 workspace-nav-studio.tsx),不需要短 staleTime + windowFocus 双兜底。 // (见 workspace-nav-studio.tsx),不需要短 staleTime + windowFocus 双兜底。
staleTime: 10 * 60 * 1000, staleTime: 10 * 60 * 1000,
retry: 1, retry: 1,
@@ -105,7 +109,9 @@ export function useNotebookChat(
const sourceIds = const sourceIds =
selectedSourceIds.length > 0 selectedSourceIds.length > 0
? toDocIds(selectedSourceIds) ? toDocIds(selectedSourceIds)
: sources.map((s) => s.document?.id).filter((id): id is number => id !== undefined); : sources
.map((s) => s.document?.id)
.filter((id): id is number => id !== undefined);
await thread.submit( await thread.submit(
{ messages: [{ type: "human", content: text }] }, { messages: [{ type: "human", content: text }] },
@@ -124,7 +130,14 @@ export function useNotebookChat(
); );
}; };
return { thread, messages: thread.messages, isStreaming: thread.isLoading, send, threadId, setThreadId }; return {
thread,
messages: thread.messages,
isStreaming: thread.isLoading,
send,
threadId,
setThreadId,
};
} }
export function useNotebookSources( export function useNotebookSources(
@@ -161,7 +174,8 @@ export function useNotebookSources(
return { return {
sources: sourcesQuery.data ?? [], sources: sourcesQuery.data ?? [],
isLoading: (!overrideToken && tokenQuery.isLoading) || sourcesQuery.isLoading, isLoading:
(!overrideToken && tokenQuery.isLoading) || sourcesQuery.isLoading,
isError: (!overrideToken && tokenQuery.isError) || sourcesQuery.isError, isError: (!overrideToken && tokenQuery.isError) || sourcesQuery.isError,
}; };
} }
@@ -22,7 +22,7 @@ interface AIWritingFormProps {
disabled?: boolean; disabled?: boolean;
/** workspace 入口:隐藏严格模式、目标读者 */ /** workspace 入口:隐藏严格模式、目标读者 */
hideAdvancedFields?: boolean; hideAdvancedFields?: boolean;
/** 预选笔记本 ID(传入时自动切换素材来源为「我的笔记」) */ /** 预选空间 ID(传入时自动切换素材来源为「我的笔记」) */
initialNotebookId?: number; initialNotebookId?: number;
/** 预选文档 ID 列表 */ /** 预选文档 ID 列表 */
initialDocumentIds?: number[]; initialDocumentIds?: number[];
@@ -45,7 +45,7 @@ const MAX_INTENT_LENGTH = 500;
const MATERIAL_SOURCE_OPTIONS: Array<{ value: MaterialSource; label: string; desc: string }> = [ const MATERIAL_SOURCE_OPTIONS: Array<{ value: MaterialSource; label: string; desc: string }> = [
{ value: 'knowledge_base', label: '知识库', desc: '默认 web/intranet 检索' }, { value: 'knowledge_base', label: '知识库', desc: '默认 web/intranet 检索' },
{ value: 'skill', label: '技能检索', desc: '用已启用的技能编排检索' }, { value: 'skill', label: '技能检索', desc: '用已启用的技能编排检索' },
{ value: 'notebook', label: '我的笔记', desc: '从笔记本中检索素材' }, { value: 'notebook', label: '我的笔记', desc: '从空间中检索素材' },
]; ];
const FIELD_LABEL_CLASS = 'text-[16px] font-semibold leading-none text-[#222]'; const FIELD_LABEL_CLASS = 'text-[16px] font-semibold leading-none text-[#222]';
@@ -139,7 +139,7 @@ export function AIWritingForm({
function handleMaterialSourceChange(val: MaterialSource) { function handleMaterialSourceChange(val: MaterialSource) {
setMaterialSource(val); setMaterialSource(val);
// studioToken 模式下笔记本和文档已锁定,切换素材来源时不清空 // studioToken 模式下空间和文档已锁定,切换素材来源时不清空
if (val !== 'notebook' && !studioToken) { if (val !== 'notebook' && !studioToken) {
setSelectedNotebookId(null); setSelectedNotebookId(null);
setSelectedDocumentIds([]); setSelectedDocumentIds([]);
@@ -296,10 +296,10 @@ export function AIWritingForm({
</SelectContent> </SelectContent>
</Select> </Select>
{/* 选了"我的笔记"才展示笔记本 / 文档选择器 */} {/* 选了"我的笔记"才展示空间 / 文档选择器 */}
{materialSource === 'notebook' && ( {materialSource === 'notebook' && (
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
{/* studioToken 模式:笔记本已锁定,不展示选择器 */} {/* studioToken 模式:空间已锁定,不展示选择器 */}
{!studioToken && ( {!studioToken && (
<Select <Select
value={selectedNotebookId !== null ? String(selectedNotebookId) : ''} value={selectedNotebookId !== null ? String(selectedNotebookId) : ''}
@@ -307,7 +307,7 @@ export function AIWritingForm({
disabled={disabled || notebooksLoading} disabled={disabled || notebooksLoading}
> >
<SelectTrigger className={FIELD_TRIGGER_CLASS}> <SelectTrigger className={FIELD_TRIGGER_CLASS}>
<SelectValue placeholder={notebooksLoading ? '加载中…' : '选择笔记本'} /> <SelectValue placeholder={notebooksLoading ? '加载中…' : '选择空间'} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{notebooks.map((nb) => ( {notebooks.map((nb) => (
@@ -324,7 +324,7 @@ export function AIWritingForm({
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{sourcesLoading {sourcesLoading
? '文档加载中…' ? '文档加载中…'
: `选择文档(可多选;不选则搜整个笔记本${selectedDocumentIds.length > 0 ? `,已选 ${selectedDocumentIds.length} 篇` : ''})`} : `选择文档(可多选;不选则搜整个空间${selectedDocumentIds.length > 0 ? `,已选 ${selectedDocumentIds.length} 篇` : ''})`}
</p> </p>
{!sourcesLoading && ( {!sourcesLoading && (
<div className="max-h-36 overflow-y-auto rounded-lg border border-[#e8dedd] bg-white divide-y divide-[#f0e8e7]"> <div className="max-h-36 overflow-y-auto rounded-lg border border-[#e8dedd] bg-white divide-y divide-[#f0e8e7]">
@@ -7,7 +7,7 @@ export interface AIMaterial {
source: string; source: string;
publishedAt: string; publishedAt: string;
relevanceScore: number; relevanceScore: number;
sourceType: 'internet' | 'intranet' | 'skill' | 'notebook'; sourceType: "internet" | "intranet" | "skill" | "notebook";
url?: string; url?: string;
recUuid?: string; recUuid?: string;
// skill 类素材的元信息(skill_name / container_path / category / skill_full_content 等) // skill 类素材的元信息(skill_name / container_path / category / skill_full_content 等)
@@ -22,7 +22,13 @@ export interface AIMaterial {
// skill_result:某个技能检索返回了 N 条素材 // skill_result:某个技能检索返回了 N 条素材
// stop_early:累计素材达到上限,停止后续技能 // stop_early:累计素材达到上限,停止后续技能
export interface SkillSearchStep { export interface SkillSearchStep {
type: 'list_skills' | 'match_skills' | 'select_skills' | 'invoke_skill' | 'skill_result' | 'stop_early'; type:
| "list_skills"
| "match_skills"
| "select_skills"
| "invoke_skill"
| "skill_result"
| "stop_early";
message: string; message: string;
skillNames?: string[]; skillNames?: string[];
skillName?: string; skillName?: string;
@@ -65,23 +71,28 @@ export interface AIOutline {
* - 任何意外形态 → 空 KeyPoint(防止崩溃,不抛异常) * - 任何意外形态 → 空 KeyPoint(防止崩溃,不抛异常)
*/ */
export function normalizeKeyPoint(raw: unknown): KeyPoint { export function normalizeKeyPoint(raw: unknown): KeyPoint {
if (typeof raw === 'string') { if (typeof raw === "string") {
return { pointTitle: raw, subPoints: [] }; return { pointTitle: raw, subPoints: [] };
} }
if (raw && typeof raw === 'object') { if (raw && typeof raw === "object") {
const r = raw as Record<string, unknown>; const r = raw as Record<string, unknown>;
const title = const title =
typeof r.pointTitle === 'string' ? r.pointTitle : typeof r.pointTitle === "string"
typeof r.point_title === 'string' ? r.point_title : ? r.pointTitle
typeof r.title === 'string' ? r.title : : typeof r.point_title === "string"
''; ? r.point_title
: typeof r.title === "string"
? r.title
: "";
const subsRaw = (r.subPoints ?? r.sub_points ?? []) as unknown; const subsRaw = (r.subPoints ?? r.sub_points ?? []) as unknown;
const subPoints: string[] = Array.isArray(subsRaw) const subPoints: string[] = Array.isArray(subsRaw)
? subsRaw.map((s) => (typeof s === 'string' ? s : String(s ?? ''))).filter(Boolean) ? subsRaw
.map((s) => (typeof s === "string" ? s : String(s ?? "")))
.filter(Boolean)
: []; : [];
return { pointTitle: title, subPoints }; return { pointTitle: title, subPoints };
} }
return { pointTitle: '', subPoints: [] }; return { pointTitle: "", subPoints: [] };
} }
// ── 素材不足求助 ────────────────────────────────────────────────────────────── // ── 素材不足求助 ──────────────────────────────────────────────────────────────
@@ -93,7 +104,7 @@ export interface BlockedSection {
} }
/** 单个被阻塞章节的处理决策 */ /** 单个被阻塞章节的处理决策 */
export type SectionDecisionKind = 'supplement' | 'delete' | 'loose'; export type SectionDecisionKind = "supplement" | "delete" | "loose";
export interface SectionDecision { export interface SectionDecision {
sectionTitle: string; sectionTitle: string;
@@ -103,15 +114,15 @@ export interface SectionDecision {
// ── 编辑审核 ────────────────────────────────────────────────────────────────── // ── 编辑审核 ──────────────────────────────────────────────────────────────────
export interface ReviewIssue { export interface ReviewIssue {
category: 'fact' | 'logic' | 'language'; category: "fact" | "logic" | "language";
severity: 'major' | 'minor'; severity: "major" | "minor";
location: string; location: string;
issue: string; issue: string;
suggestion: string; suggestion: string;
} }
export interface ReviewResult { export interface ReviewResult {
verdict: 'pass' | 'reject'; verdict: "pass" | "reject";
factScore: number; factScore: number;
logicScore: number; logicScore: number;
languageScore: number; languageScore: number;
@@ -123,44 +134,44 @@ export interface ReviewResult {
// ── 写作状态 ────────────────────────────────────────────────────────────────── // ── 写作状态 ──────────────────────────────────────────────────────────────────
export type WritingStatus = export type WritingStatus =
| 'idle' | "idle"
| 'researching' | "researching"
| 'awaiting_material_confirm' | "awaiting_material_confirm"
| 'writing_outline' | "writing_outline"
| 'awaiting_outline_confirm' | "awaiting_outline_confirm"
| 'writing_draft' | "writing_draft"
| 'awaiting_section_help' | "awaiting_section_help"
| 'awaiting_draft_confirm' | "awaiting_draft_confirm"
| 'reviewing' | "reviewing"
| 'awaiting_review_confirm' | "awaiting_review_confirm"
| 'revising' | "revising"
| 'done' | "done"
| 'error'; | "error";
// ── SSE 进度事件 ────────────────────────────────────────────────────────────── // ── SSE 进度事件 ──────────────────────────────────────────────────────────────
export type ProgressEventType = export type ProgressEventType =
| 'connected' | "connected"
| 'step_start' | "step_start"
| 'step_done' | "step_done"
| 'intent_chunk' | "intent_chunk"
| 'intent_ready' | "intent_ready"
| 'research_progress' | "research_progress"
| 'materials_ready' | "materials_ready"
| 'outline_chunk' | "outline_chunk"
| 'outline_ready' | "outline_ready"
| 'draft_chunk' | "draft_chunk"
| 'draft_ready' | "draft_ready"
| 'review_chunk' | "review_chunk"
| 'review_item_done' | "review_item_done"
| 'review_ready' | "review_ready"
| 'await_user' | "await_user"
| 'resumed' | "resumed"
| 'notice' | "notice"
| 'skill_search_step' | "skill_search_step"
| 'skill_select_chunk' | "skill_select_chunk"
| 'done' | "done"
| 'error'; | "error";
export interface ProgressEvent { export interface ProgressEvent {
type: ProgressEventType; type: ProgressEventType;
@@ -183,13 +194,18 @@ export interface ProgressEvent {
draftTitle?: string; draftTitle?: string;
revisionCount?: number; revisionCount?: number;
// review_item_done (streaming per-category result) // review_item_done (streaming per-category result)
reviewItemCategory?: 'fact' | 'logic' | 'language'; reviewItemCategory?: "fact" | "logic" | "language";
reviewItemScore?: number; reviewItemScore?: number;
reviewItemIssues?: ReviewIssue[]; reviewItemIssues?: ReviewIssue[];
// review_ready // review_ready
reviewResult?: ReviewResult; reviewResult?: ReviewResult;
// await_user // await_user
pausePoint?: 'material_confirm' | 'outline_confirm' | 'draft_confirm' | 'review_confirm' | 'section_help'; pausePoint?:
| "material_confirm"
| "outline_confirm"
| "draft_confirm"
| "review_confirm"
| "section_help";
pauseContext?: Record<string, unknown>; pauseContext?: Record<string, unknown>;
// section_help:素材不足、待用户决策的章节 // section_help:素材不足、待用户决策的章节
blockedSections?: BlockedSection[]; blockedSections?: BlockedSection[];
@@ -229,19 +245,19 @@ export interface AIWritingRequest {
/** 素材检索关键词数量(1~10,默认 1) */ /** 素材检索关键词数量(1~10,默认 1) */
keywordCount: number; keywordCount: number;
/** 写作模式:strict=严格依据素材 / loose=可结合通用知识(默认 loose) */ /** 写作模式:strict=严格依据素材 / loose=可结合通用知识(默认 loose) */
writingMode: 'strict' | 'loose'; writingMode: "strict" | "loose";
/** 素材来源:knowledge_base=知识库 / skill=技能检索 / notebook=我的笔记 */ /** 素材来源:knowledge_base=知识库 / skill=技能检索 / notebook=我的笔记 */
materialSource: 'knowledge_base' | 'skill' | 'notebook'; materialSource: "knowledge_base" | "skill" | "notebook";
/** 笔记来源(可选):笔记本 search_space_id */ /** 笔记来源(可选):空间 search_space_id */
noteSearchSpaceId?: number; noteSearchSpaceId?: number;
/** 笔记来源(可选):笔记本内指定文档的 document_id 列表,空数组表示检索整个笔记本 */ /** 笔记来源(可选):空间内指定文档的 document_id 列表,空数组表示检索整个空间 */
noteSourceIds?: number[]; noteSourceIds?: number[];
} }
// ── 流式审核条目 ────────────────────────────────────────────────────────────── // ── 流式审核条目 ──────────────────────────────────────────────────────────────
export interface StreamingReviewItem { export interface StreamingReviewItem {
category: 'fact' | 'logic' | 'language'; category: "fact" | "logic" | "language";
// 是否已生成完毕(true 时展示结构化评分/意见,false 时展示流式打字机文本) // 是否已生成完毕(true 时展示结构化评分/意见,false 时展示流式打字机文本)
isDone: boolean; isDone: boolean;
// 流式过程文本(正文 / 思考分开) // 流式过程文本(正文 / 思考分开)
@@ -274,7 +290,12 @@ export interface StreamingDraftSection {
// ── 已完成干预记录 ──────────────────────────────────────────────────────────── // ── 已完成干预记录 ────────────────────────────────────────────────────────────
export interface CompletedIntervention { export interface CompletedIntervention {
pausePoint: 'material_confirm' | 'outline_confirm' | 'draft_confirm' | 'review_confirm' | 'section_help'; pausePoint:
| "material_confirm"
| "outline_confirm"
| "draft_confirm"
| "review_confirm"
| "section_help";
action: string; action: string;
// material_confirm // material_confirm
materials?: AIMaterial[]; materials?: AIMaterial[];
@@ -230,7 +230,7 @@ export default function LightAppNoteListPage() {
isDark ? "text-slate-100" : "text-slate-950", isDark ? "text-slate-100" : "text-slate-950",
)} )}
> >
我的笔记本 我的空间
</h1> </h1>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -77,8 +77,7 @@ function SourceCard({
<div <div
title={title} title={title}
onClick={() => onToggle(item.id)} 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 ${ className={`group relative flex h-32 w-full cursor-pointer flex-col justify-between rounded-2xl p-4 ring-1 transition-colors ${selected
selected
? "bg-primary/10 ring-primary" ? "bg-primary/10 ring-primary"
: "bg-muted ring-border/50 hover:bg-muted/70" : "bg-muted ring-border/50 hover:bg-muted/70"
}`} }`}
@@ -432,7 +431,7 @@ export default function StudioNotebookDetailPage() {
if (!notebook) { if (!notebook) {
return ( return (
<div className="flex h-full flex-col items-center justify-center bg-background"> <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> </div>
); );
} }
@@ -507,7 +506,7 @@ export default function StudioNotebookDetailPage() {
</div> </div>
)} )}
{/* 列表视图:笔记本标题 + 历史线程列表 */} {/* 列表视图:空间标题 + 历史线程列表 */}
{!isChatView && ( {!isChatView && (
<div className="relative z-10 flex flex-1 flex-col overflow-hidden"> <div className="relative z-10 flex flex-1 flex-col overflow-hidden">
{/* 右上角跳转按钮 */} {/* 右上角跳转按钮 */}
@@ -586,7 +585,7 @@ export default function StudioNotebookDetailPage() {
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"> <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" /> <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> </svg>
笔记本对话会显示在这里 空间对话会显示在这里
</div> </div>
)} )}
@@ -180,7 +180,7 @@ export default function StudioNotebooksPage() {
<div className="min-h-full bg-background p-8"> <div className="min-h-full bg-background p-8">
{/* Header */} {/* Header */}
<div className="mb-8 flex items-center justify-between"> <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 <button
onClick={() => { setCreateOpen(true); setCreateError(""); setCreateName(""); }} 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" 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> </div>
{isError && ( {isError && (
<p className="text-sm text-muted-foreground">加载笔记本失败,请稍后重试。</p> <p className="text-sm text-muted-foreground">加载空间失败,请稍后重试。</p>
)} )}
{/* Grid */} {/* Grid */}
@@ -220,7 +220,7 @@ export default function StudioNotebooksPage() {
value={createName} value={createName}
onChange={(e) => setCreateName(e.target.value)} onChange={(e) => setCreateName(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") void handleCreate(); }} onKeyDown={(e) => { if (e.key === "Enter") void handleCreate(); }}
placeholder="笔记本名称" placeholder="空间名称"
disabled={creating} 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" 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)}> <Dialog open={!!renameTarget} onOpenChange={(v) => !renaming && !v && setRenameTarget(null)}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>重命名笔记本</DialogTitle> <DialogTitle>重命名空间</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-4 pt-2"> <div className="flex flex-col gap-4 pt-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -272,12 +272,12 @@ export default function StudioNotebooksPage() {
<Dialog open={!!deleteTarget} onOpenChange={(v) => !deleting && !v && setDeleteTarget(null)}> <Dialog open={!!deleteTarget} onOpenChange={(v) => !deleting && !v && setDeleteTarget(null)}>
<DialogContent className="sm:max-w-sm"> <DialogContent className="sm:max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle>删除笔记本</DialogTitle> <DialogTitle>删除空间</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-4 pt-1"> <div className="flex flex-col gap-4 pt-1">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
确定要删除{deleteTarget ? <span className="font-medium text-foreground">「{deleteTarget.name}」</span> : ""}吗? 确定要删除{deleteTarget ? <span className="font-medium text-foreground">「{deleteTarget.name}」</span> : ""}吗?
删除后笔记本内所有来源数据将一并移除,且无法恢复。 删除后空间内所有来源数据将一并移除,且无法恢复。
</p> </p>
{deleteError && <p className="text-xs text-destructive">{deleteError}</p>} {deleteError && <p className="text-xs text-destructive">{deleteError}</p>}
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
@@ -416,7 +416,7 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
# ── 纯笔记模式 ── # ── 纯笔记模式 ──
# 用户在表单选了「我的笔记」作为素材来源 → 跳过 web/intranet 检索, # 用户在表单选了「我的笔记」作为素材来源 → 跳过 web/intranet 检索,
# 仅使用 Step 1.5 已拉取的 notebook_materials。 # 仅使用 Step 1.5 已拉取的 notebook_materials。
# 与 UI 文案"从笔记本中检索素材"对齐。 # 与 UI 文案"从空间中检索素材"对齐。
_emit(f"仅使用笔记素材,跳过 web/intranet 检索({len(notebook_materials)} 条)") _emit(f"仅使用笔记素材,跳过 web/intranet 检索({len(notebook_materials)} 条)")
all_results_nested = [] all_results_nested = []
else: else:
@@ -1078,7 +1078,7 @@ When a search, knowledge, skill, or MCP tool returns a result list:
<notebook_mode> <notebook_mode>
## 角色 ## 角色
你是一个笔记本 AI 助手,**只能**基于笔记本文档来源回答用户问题,禁止使用任何其他知识。 你是一个空间 AI 助手,**只能**基于空间文档来源回答用户问题,禁止使用任何其他知识。
## 可用知识来源 ## 可用知识来源
{source_line} {source_line}
@@ -1104,7 +1104,7 @@ When a search, knowledge, skill, or MCP tool returns a result list:
- 未调用 `search_notebook_sources` 就直接回答 - 未调用 `search_notebook_sources` 就直接回答
- 引用不在检索结果中的 chunk_id - 引用不在检索结果中的 chunk_id
- 省略引用标记 - 省略引用标记
- 使用笔记本来源之外的知识 - 使用空间来源之外的知识
</notebook_mode>""" </notebook_mode>"""
return prompt return prompt
@@ -1,7 +1,7 @@
"""笔记本 RAG 检索工具。 """空间 RAG 检索工具。
从 LangGraph configurable 中读取 notebook_search_space_id / notebook_source_ids / 从 LangGraph configurable 中读取 notebook_search_space_id / notebook_source_ids /
notebook_token,调用外部检索接口返回相关文本块,供 LLM 在笔记本模式下引用回答。 notebook_token,调用外部检索接口返回相关文本块,供 LLM 在空间模式下引用回答。
API base URL 通过环境变量 DEER_FLOW_NOTEBOOK_API_BASE 配置。 API base URL 通过环境变量 DEER_FLOW_NOTEBOOK_API_BASE 配置。
""" """