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>
This commit is contained in:
xuhdandClaude Sonnet 4.6 committed 2026-06-05 13:38:08 +08:00
1 parent e53c1a0f7b
commit 0e7149eec3
6 files changed
+140 -85

No files matched your search

@@ -1,9 +1,59 @@
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 notebookId = searchParams.get('notebook_id');
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">
@@ -20,12 +70,12 @@ export default function NotebookAIWritingPage() {
{/* 分隔线 */}
<div className="w-px h-4 bg-[#2d4f7a]" />
{/* 空间 ID */}
{notebookId && (
{/* 空间名称 */}
{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">
{notebookId}
{spaceName ?? data.search_space_id}
</span>
</div>
)}
@@ -40,12 +90,20 @@ export default function NotebookAIWritingPage() {
</div>
</div>
{/* 主体:AIWritingPage 完整组件,含 context providers */}
{/* 主体 */}
<div className="flex-1 min-h-0 overflow-hidden">
<AIWritingPage
hideAdvancedFields
initialNotebookId={notebookId ? Number(notebookId) : undefined}
/>
{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>
);