修改名称 笔记本改为 空间
This commit is contained in:
1 parent
06508cfb7c
commit
82d14351fc
12 files changed
+446
-393
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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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(/\/+$/, "");
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -41,8 +47,8 @@ export interface SkillSearchStep {
|
|||||||
// guard 兼容,防止前端流式接收到旧大纲时崩溃。详见 normalizeKeyPoint() helper。
|
// guard 兼容,防止前端流式接收到旧大纲时崩溃。详见 normalizeKeyPoint() helper。
|
||||||
|
|
||||||
export interface KeyPoint {
|
export interface KeyPoint {
|
||||||
pointTitle: string; // 小节标题(渲染为 ### 三级标题)
|
pointTitle: string; // 小节标题(渲染为 ### 三级标题)
|
||||||
subPoints: string[]; // 子要点(可空;非空时在小节正文起始处以 bullet 列表呈现)
|
subPoints: string[]; // 子要点(可空;非空时在小节正文起始处以 bullet 列表呈现)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OutlineSection {
|
export interface OutlineSection {
|
||||||
@@ -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;
|
||||||
// 流式过程文本(正文 / 思考分开)
|
// 流式过程文本(正文 / 思考分开)
|
||||||
@@ -266,7 +282,7 @@ export interface RevisionSummary {
|
|||||||
export interface StreamingDraftSection {
|
export interface StreamingDraftSection {
|
||||||
sectionIndex: number;
|
sectionIndex: number;
|
||||||
sectionTitle: string;
|
sectionTitle: string;
|
||||||
text: string; // 正文内容(非思考)
|
text: string; // 正文内容(非思考)
|
||||||
thinkingText: string; // 思考内容(<think>...</think> 内)
|
thinkingText: string; // 思考内容(<think>...</think> 内)
|
||||||
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,11 +77,10 @@ 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"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* 删除按钮 - 右上角始终显示,悬停时高亮 */}
|
{/* 删除按钮 - 右上角始终显示,悬停时高亮 */}
|
||||||
<button
|
<button
|
||||||
@@ -181,14 +180,14 @@ function SourcesDialog({
|
|||||||
{isLoadingSources
|
{isLoadingSources
|
||||||
? Array.from({ length: 8 }).map((_, i) => <SourceCardSkeleton key={i} />)
|
? Array.from({ length: 8 }).map((_, i) => <SourceCardSkeleton key={i} />)
|
||||||
: sources.map((src) => (
|
: sources.map((src) => (
|
||||||
<SourceCard
|
<SourceCard
|
||||||
key={src.id}
|
key={src.id}
|
||||||
item={src}
|
item={src}
|
||||||
selected={selectedSourceIds.includes(src.id)}
|
selected={selectedSourceIds.includes(src.id)}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{!isLoadingSources && sources.length === 0 && (
|
{!isLoadingSources && sources.length === 0 && (
|
||||||
<p className="text-sm text-muted-foreground">暂无来源,请添加文件。</p>
|
<p className="text-sm text-muted-foreground">暂无来源,请添加文件。</p>
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -453,308 +452,308 @@ export default function StudioNotebookDetailPage() {
|
|||||||
}}
|
}}
|
||||||
onUploadComplete={handleUploadComplete}
|
onUploadComplete={handleUploadComplete}
|
||||||
>
|
>
|
||||||
<ThreadContext.Provider value={{ thread }}>
|
<ThreadContext.Provider value={{ thread }}>
|
||||||
<ChatBox threadId={chatThreadId ?? ""}>
|
<ChatBox threadId={chatThreadId ?? ""}>
|
||||||
<div className="relative flex h-full flex-col overflow-hidden bg-background">
|
<div className="relative flex h-full flex-col overflow-hidden bg-background">
|
||||||
{/* 背景渐变(列表视图时显示) */}
|
{/* 背景渐变(列表视图时显示) */}
|
||||||
{!isChatView && (
|
{!isChatView && (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute inset-0"
|
className="pointer-events-none absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
"linear-gradient(145deg, hsl(var(--primary) / 0.22) 0%, hsl(var(--primary) / 0.10) 45%, transparent 80%)",
|
"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);
|
|
||||||
}}
|
}}
|
||||||
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 && (
|
{isChatView && (
|
||||||
<div className="mt-8 w-full max-w-[680px] flex flex-col gap-2">
|
<div className="flex shrink-0 items-center justify-between border-b border-border px-6 py-3">
|
||||||
<p className="mb-1 text-xs font-medium text-foreground/40">历史对话</p>
|
<div className="flex items-center gap-3">
|
||||||
<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
|
<button
|
||||||
onClick={() => setThreadsExpanded((v) => !v)}
|
onClick={handleBackToList}
|
||||||
className="mt-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
className="rounded-lg p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
>
|
>
|
||||||
{threadsExpanded ? "收起" : `查看全部 ${notebookThreads.length} 条对话`}
|
<ArrowLeft className="size-4" />
|
||||||
</button>
|
</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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 聊天视图:消息列表 */}
|
{/* 列表视图:空间标题 + 历史线程列表 */}
|
||||||
{isChatView && (
|
{!isChatView && (
|
||||||
<div
|
<div className="relative z-10 flex flex-1 flex-col overflow-hidden">
|
||||||
className="min-h-0 flex-1 overflow-hidden"
|
{/* 右上角跳转按钮 */}
|
||||||
onClick={(e) => {
|
<a
|
||||||
const sup = (e.target as HTMLElement).closest<HTMLElement>("[data-chunk-id]");
|
href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat`}
|
||||||
if (!sup) return;
|
target="_blank"
|
||||||
const chunkId = Number(sup.dataset.chunkId);
|
rel="noreferrer"
|
||||||
if (!chunkId) return;
|
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"
|
||||||
setCitationChunkId(chunkId);
|
>
|
||||||
setCitationDoc(null);
|
<BookOpen className="size-3.5" />
|
||||||
setCitationLoading(true);
|
<span>我的笔记</span>
|
||||||
const email = getStoredAuth()?.email ?? "";
|
</a>
|
||||||
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"}`}>
|
<div className="flex flex-1 flex-col items-center justify-center overflow-y-auto px-8 pb-12 pt-16">
|
||||||
<form
|
<div className="flex size-14 items-center justify-center leading-none select-none">
|
||||||
onSubmit={(e) => { void handleSend(e); }}
|
{notebook.avatar_url ? (
|
||||||
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"
|
<img src={notebook.avatar_url} alt="" className="size-12 rounded-xl object-contain" />
|
||||||
>
|
) : (
|
||||||
<button
|
<Files className="size-10 text-foreground/50" />
|
||||||
type="button"
|
)}
|
||||||
onClick={() => setSourcesOpen(true)}
|
</div>
|
||||||
className="shrink-0 text-muted-foreground hover:text-foreground"
|
<h1 className="mt-5 max-w-xl text-center text-3xl font-normal leading-snug text-foreground">
|
||||||
>
|
{notebook.name}
|
||||||
<Plus className="size-5" />
|
</h1>
|
||||||
</button>
|
<button
|
||||||
<input
|
onClick={() => setSourcesOpen(true)}
|
||||||
value={input}
|
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"
|
||||||
onChange={(e) => setInput(e.target.value)}
|
>
|
||||||
placeholder="问问 AI..."
|
<Files className="size-3.5" />
|
||||||
disabled={isStreaming}
|
<span>{notebook.source_count} 个来源</span>
|
||||||
className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50"
|
</button>
|
||||||
/>
|
|
||||||
{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); }}>
|
<form
|
||||||
<DialogContent className="sm:max-w-lg">
|
onSubmit={(e) => { void handleSend(e); }}
|
||||||
<DialogHeader>
|
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"
|
||||||
<DialogTitle className="text-base">引用来源</DialogTitle>
|
>
|
||||||
</DialogHeader>
|
<button
|
||||||
{citationLoading && (
|
type="button"
|
||||||
<div className="flex justify-center py-8">
|
onClick={() => {
|
||||||
<Loader2 className="size-6 animate-spin text-muted-foreground" />
|
void queryClient.invalidateQueries({ queryKey: ["studio", "sources"] });
|
||||||
</div>
|
setSourcesOpen(true);
|
||||||
)}
|
}}
|
||||||
{!citationLoading && citationDoc && (
|
className="shrink-0 text-muted-foreground hover:text-foreground"
|
||||||
<div className="flex flex-col gap-4 pt-1">
|
>
|
||||||
<div className="flex items-start gap-3">
|
<Plus className="size-5" />
|
||||||
<span className="mt-0.5 shrink-0 rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground uppercase">
|
</button>
|
||||||
{citationDoc.file_type || citationDoc.document_type}
|
<input
|
||||||
</span>
|
value={input}
|
||||||
<span className="text-sm font-medium text-foreground leading-snug">{citationDoc.title}</span>
|
onChange={(e) => setInput(e.target.value)}
|
||||||
</div>
|
placeholder="问问 AI..."
|
||||||
{citationDoc.chunks.find((c) => c.id === citationChunkId) && (
|
disabled={isStreaming}
|
||||||
<div className="max-h-60 overflow-y-auto rounded-xl bg-muted p-4 text-sm text-foreground leading-relaxed">
|
className="flex-1 bg-transparent text-[15px] text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50"
|
||||||
{citationDoc.chunks.find((c) => c.id === citationChunkId)!.content}
|
/>
|
||||||
|
{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>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
{!citationLoading && !citationDoc && (
|
|
||||||
<p className="py-4 text-sm text-muted-foreground">加载失败,请重试。</p>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* 来源 Dialog */}
|
{/* 聊天视图:消息列表 */}
|
||||||
<SourcesDialog
|
{isChatView && (
|
||||||
open={sourcesOpen}
|
<div
|
||||||
onClose={() => setSourcesOpen(false)}
|
className="min-h-0 flex-1 overflow-hidden"
|
||||||
notebookName={notebook.name}
|
onClick={(e) => {
|
||||||
sources={sources}
|
const sup = (e.target as HTMLElement).closest<HTMLElement>("[data-chunk-id]");
|
||||||
isLoadingSources={isLoadingSources}
|
if (!sup) return;
|
||||||
selectedSourceIds={selectedSourceIds}
|
const chunkId = Number(sup.dataset.chunkId);
|
||||||
onToggle={handleToggleSource}
|
if (!chunkId) return;
|
||||||
onDelete={handleDeleteSource}
|
setCitationChunkId(chunkId);
|
||||||
/>
|
setCitationDoc(null);
|
||||||
</div>
|
setCitationLoading(true);
|
||||||
</ChatBox>
|
const email = getStoredAuth()?.email ?? "";
|
||||||
</ThreadContext.Provider>
|
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>
|
</DocumentUploadDialogProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
+1
-1
@@ -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:
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
+2
-2
@@ -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 配置。
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in new issue
Block a user