Files
deerflow-code/frontend-web/src/pages/NotebookAIWritingPage.tsx
T
xuhdandClaude Sonnet 4.6 0e7149eec3 feat(notebook): load notebook space via token and pre-select docs/sources
- NotebookAIWritingPage reads `token` from URL, fetches space data from
  notebook-token API (search_space_id, doc_ids, user_token) and space
  name from searchspaces API; keep-alive ping every 50s resets Redis TTL
- Pass user_token as studioToken through AIWritingPage → AIWritingLayout
  → WritingFormPanel → AIWritingPanel → AIWritingForm to skip
  getTokenByEmail when studio token is already available
- useStudioNotebooks accepts enabled flag; useNotebookSources accepts
  overrideToken to bypass the internal token fetch
- AIWritingForm hides notebook selector when studioToken is provided
  (notebook is pre-locked) and pre-selects initialDocumentIds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:38:08 +08:00

111 lines
3.8 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import AIWritingPage from '@/open-canvas/pages/AIWritingPage';
import { getNotebookApiBaseURL } from '@/core/config';
interface NotebookTokenData {
doc_ids: number[];
search_space_id: number;
user_id: string;
user_token: string;
}
export default function NotebookAIWritingPage() {
const [searchParams] = useSearchParams();
const token = searchParams.get('token');
const [data, setData] = useState<NotebookTokenData | null>(null);
const [spaceName, setSpaceName] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!token) {
setError('缺少 token 参数');
return;
}
const base = getNotebookApiBaseURL();
fetch(`${base}/api/v1/ai-writing/notebook-token/${token}`)
.then((res) => {
if (!res.ok) throw new Error(`请求失败 ${res.status}`);
return res.json() as Promise<NotebookTokenData>;
})
.then((d) => {
setData(d);
return fetch(`${base}/api/v1/searchspaces/${d.search_space_id}`, {
headers: { Authorization: `Bearer ${d.user_token}` },
});
})
.then((res) => {
if (!res.ok) return null;
return res.json() as Promise<{ name: string }>;
})
.then((space) => {
if (space?.name) setSpaceName(space.name);
})
.catch((e: Error) => setError(e.message));
}, [token]);
// Keep-alive:每 50 秒 ping 一次 token 接口,防止 Redis TTL 过期
useEffect(() => {
if (!token) return;
const base = getNotebookApiBaseURL();
const id = setInterval(() => {
fetch(`${base}/api/v1/ai-writing/notebook-token/${token}`).catch(() => {});
}, 50_000);
return () => clearInterval(id);
}, [token]);
return (
<div className="h-full flex flex-col overflow-hidden">
{/* 索财笔记 Header */}
<div className="flex items-center gap-3 px-5 h-11 shrink-0 bg-gradient-to-r from-[#1e3a5f] to-[#1a3255] border-b border-[#2a4a70]">
{/* 品牌 */}
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-md bg-blue-500 flex items-center justify-center text-white text-xs font-bold shrink-0">
索
</div>
<span className="text-[#e0f0ff] text-sm font-semibold tracking-wide">索财笔记</span>
</div>
{/* 分隔线 */}
<div className="w-px h-4 bg-[#2d4f7a]" />
{/* 空间名称 */}
{data?.search_space_id && (
<div className="flex items-center gap-1.5">
<span className="text-[#7fb3e8] text-xs">空间</span>
<span className="text-[#b3d4f5] text-xs font-semibold bg-[#1a3a60] border border-[#2d5080] rounded px-2 py-0.5">
{spaceName ?? data.search_space_id}
</span>
</div>
)}
{/* 弹性占位 */}
<div className="flex-1" />
{/* 笔记标签 */}
<div className="flex items-center gap-1.5 bg-[#0f2840] border border-[#2a4a70] rounded-md px-2.5 py-1">
<span className="text-xs">📒</span>
<span className="text-[#bfdbfe] text-xs">这是我的笔记</span>
</div>
</div>
{/* 主体 */}
<div className="flex-1 min-h-0 overflow-hidden">
{error ? (
<div className="flex items-center justify-center h-full text-red-400 text-sm">{error}</div>
) : !data ? (
<div className="flex items-center justify-center h-full text-[#7fb3e8] text-sm">加载中…</div>
) : (
<AIWritingPage
hideAdvancedFields
initialNotebookId={data.search_space_id}
initialDocumentIds={data.doc_ids}
studioToken={data.user_token}
/>
)}
</div>
</div>
);
}