Files
deerflow-code/frontend-web/src/open-canvas/pages/AIWritingPage.tsx
T
董宏凯 27e6b0b23e Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into dhk-dev
# Conflicts:
#	frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx
2026-07-11 19:01:10 +08:00

660 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AI 写作入口页(左右双栏:聊天面板 + 草稿预览)。
*
* ──────────────────────────────────────────────────────────────────────────
* Docker 内网部署后「会话丢失 / transcript 报 500 / resume 报 400」排查指南
* ──────────────────────────────────────────────────────────────────────────
*
* 后端涉及接口(前缀 /api/ai-writing):
* POST /start 启动写作流程
* GET /{session_id}/stream SSE 进度流
* POST /{session_id}/resume 提交用户干预 → 继续流程
* PUT /sessions/{session_id}/transcript 保存对话时间线
*
* ── 1) 确认 alembic 迁移已跑过(修 transcript 500)─────────────────────
* transcript 列在 MySQL 上原是 TEXT(64 KB 上限),长时间线会溢出抛 500。
* 迁移 20260519_02 把它改成 LONGTEXT。容器启动会自动跑,验证方法:
*
* docker exec <gateway> sh -c "cd /app/backend \
* && PYTHONPATH=. uv run alembic current"
* docker exec <mysql> mysql -uroot -p<pwd> deerflow -e \
* "SHOW COLUMNS FROM ai_writing_sessions LIKE 'transcript';"
*
* 列类型应是 longtext。若仍是 text,手动执行:
* docker exec <gateway> sh -c "cd /app/backend \
* && PYTHONPATH=. uv run alembic upgrade head"
*
* ── 2) 确认 .deer-flow 数据卷已挂出(修「容器没重启也丢会话」)──────
* AI 写作 graph 的 checkpointer 已从 MemorySaver 换成 AsyncSqliteSaver,
* 文件落到 .deer-flow/data/checkpoints.db。docker-compose 必须把该目录
* 挂出来,否则容器重启数据照样丢:
*
* services:
* gateway:
* volumes:
* - ./data:/app/.deer-flow # 关键:挂出 .deer-flow
*
* ── 3) 确认 worker 数 = 1(关键嫌疑:多 worker 内存隔离)────────────
* _event_queues / _session_pending_pause 都是模块级 dict,每个 worker
* 一份。多 worker 下,/start 命中 worker A、/resume 命中 worker B,B
* 的 dict 没有这个 session → 404/「会话丢失」。
*
* docker exec <gateway> ps -ef | grep -E 'uvicorn|gunicorn'
*
* 若 > 1:要么把启动参数改成 --workers 1,要么在 nginx 加 sticky
* session(ip_hash / cookie),要么把队列搬 Redis。
*
* ── 4) 排查节点异常(嫌疑:graph 节点抛错触发清理分支)─────────────
* _stream_graph_segment 的 except 分支会清掉 _event_queues[session_id],
* 用户下一次 resume 就 404。日志里搜:
*
* docker logs <gateway> 2>&1 | grep -E \
* 'ai_writing graph segment failed|ai_writing.*error' | tail -50
*
* ── 5) 排查反向代理掐 SSE 长连接(嫌疑:proxy_read_timeout 太短)───
* SSE 默认连接挂半个小时以上,nginx 默认 60s 会断。检查内网反代:
*
* grep -rE 'proxy_read_timeout|sse|keepalive' /etc/nginx/
*
* 需在 SSE 路由上加:proxy_read_timeout 3600s; proxy_buffering off;
*
* ── 定期清理(每天 03:00 跑)────────────────────────────────────────
* config.yaml -> ai_writing.cleanup 段(retention_days: 7)。联动清理
* MySQL 业务表 + SQLite checkpointer 里的 thread 状态。手动验证:
*
* docker exec <mysql> mysql -uroot -p<pwd> deerflow -e \
* "SELECT COUNT(*) FROM ai_writing_sessions \
* WHERE updated_at < NOW() - INTERVAL 7 DAY;"
*
* ── 不想等凌晨 3 点?手动触发一次清理(admin only)──────────────────
* POST /api/ai-writing/admin/cleanup
* Body(全部可选,留空走 config 默认):
* { "retention_days": 7, "only_finished": false, "batch_size": 500 }
* 返回:
* { "deleted_db": N, "deleted_checkpoints": N,
* "elapsed_seconds": X, "params": {...} }
*
* 例:清掉所有 1 小时前的会话(测试用):
* curl -X POST http://<gateway>/api/ai-writing/admin/cleanup \
* -H 'Content-Type: application/json' \
* -H 'Cookie: <admin session cookie>' \
* -d '{"retention_days": 0}'
* ──────────────────────────────────────────────────────────────────────────
*/
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import {
ArrowLeft,
History,
Loader2,
PanelLeftClose,
PanelLeftOpen,
PanelRightOpen,
} from 'lucide-react';
import { TooltipProvider } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import type { GroupImperativeHandle } from 'react-resizable-panels';
import {
Navigate,
Route,
Routes,
useLocation,
useNavigate,
useParams,
} from 'react-router-dom';
import { CanvasThreadProvider } from '../contexts/CanvasThreadContext';
import { CanvasGraphProvider } from '../contexts/CanvasGraphContext';
import { CanvasUIProvider } from '../contexts/CanvasUIContext';
import { AIWritingPanel } from '../components/ai-writing/AIWritingPanel';
import { AIWritingDraftPanel } from '../components/ai-writing/AIWritingDraftPanel';
import { WritingSetupChat } from '../components/ai-writing/WritingSetupChat';
import { ResearchSidebar } from '../components/ai-writing/ResearchSidebar';
import StudioNotebookDetailPage from '@/pages/StudioNotebookDetailPage';
import StudioNotebooksPage from '@/pages/StudioNotebooksPage';
import AgentChatPage from '@/pages/AgentChatPage';
import { ChatRuntime } from '@/pages/WorkspaceRoutes';
import {
LayoutModeSwitcher,
ThemeSwitcher,
} from '@/components/workspace/appearance-controls';
import {
AgentModuleSidebar,
useAgentModuleSidebarData,
} from '@/components/workspace/agents/agent-module-sidebar';
import { AgentModuleSettingsDialog } from '@/components/workspace/agents/agent-module-settings-dialog';
import { getStoredAuth } from '@/core/auth';
import { RouteBaseProvider } from '@/core/route-base';
/** 右侧内嵌视图:笔记本详情 / 所有空间列表 / null(写作) */
type RightView = { kind: 'notebook'; id: number } | { kind: 'allSpaces' } | null;
import { AIWritingProvider, useAIWriting } from '../contexts/AIWritingContext';
import { AIWritingHistoryProvider, useAIWritingHistory } from '../contexts/AIWritingHistoryContext';
import { WritingFormProvider, useWritingForm } from '../contexts/WritingFormContext';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/ui/resizable';
// Panel IDs used as keys in the v4 Layout map
const WRITING_ID = 'aw-writing';
const DRAFT_ID = 'aw-draft';
const AI_WRITING_AGENT_MODULE_KEY = 'ai-writing';
const AI_WRITING_AGENT_MODULE_TITLE = 'AI 写作智能体';
const AI_WRITING_LAST_AGENT_KEY = `agent-module:${AI_WRITING_AGENT_MODULE_KEY}:last-agent`;
const SHOW_NOTEBOOK_SPACE_ENTRIES = false;
// Match the original canvas page proportions (chat=35, artifact=65)
const DEFAULT_LAYOUT = { [WRITING_ID]: 35, [DRAFT_ID]: 65 };
function AIWritingCompactAppearanceActions() {
return (
<div className="flex items-center gap-1">
<ThemeSwitcher tone="content" />
<LayoutModeSwitcher tone="content" />
</div>
);
}
function useAIWritingRouteBase() {
const { pathname } = useLocation();
return useMemo(() => {
const marker = '/ai-writing';
const index = pathname.indexOf(marker);
return index >= 0
? pathname.slice(0, index + marker.length)
: '/page/workspace/ai-writing';
}, [pathname]);
}
function useAIWritingAgentRouteState(base: string) {
const { pathname } = useLocation();
return useMemo(() => {
const prefix = `${base}/agents/`;
if (!pathname.startsWith(prefix)) {
return { active: false, agentId: undefined, threadId: undefined };
}
const parts = pathname.slice(prefix.length).split('/');
return {
active: true,
agentId: parts[0] ? decodeURIComponent(parts[0]) : undefined,
threadId: parts[2] ? decodeURIComponent(parts[2]) : undefined,
};
}, [base, pathname]);
}
function AIWritingAgentDefaultRedirect() {
const { groups, isLoading } = useAgentModuleSidebarData(AI_WRITING_AGENT_MODULE_KEY);
const agents = groups.flatMap((group) => group.agents);
const lastAgentId = (() => {
try {
return sessionStorage.getItem(AI_WRITING_LAST_AGENT_KEY);
} catch {
return null;
}
})();
const targetAgent = agents.find((agent) => agent.id === lastAgentId) ?? agents[0];
if (isLoading) {
return (
<div className="flex size-full items-center justify-center">
<Loader2 className="size-5 animate-spin text-muted-foreground" />
</div>
);
}
if (!targetAgent) {
return (
<div className="flex size-full items-center justify-center px-6 text-center text-sm text-muted-foreground">
暂无可用智能体,请先在模块设置中配置或创建智能体。
</div>
);
}
return <Navigate to={`agents/${targetAgent.id}/chats/new`} replace />;
}
function AIWritingAgentChatRoute({
compactAppearanceActions,
}: {
compactAppearanceActions?: ReactNode;
}) {
const { agent_id } = useParams<{ agent_id: string }>();
return (
<ChatRuntime>
<AgentChatPage
key={agent_id}
compactAppearanceActions={compactAppearanceActions}
/>
</ChatRuntime>
);
}
function AIWritingAgentRoutes({
base,
compactAppearanceActions,
}: {
base: string;
compactAppearanceActions?: ReactNode;
}) {
return (
<RouteBaseProvider value={base}>
<Routes>
<Route index element={<AIWritingAgentDefaultRedirect />} />
<Route
path="agents/:agent_id/chats/:thread_id"
element={
<AIWritingAgentChatRoute
compactAppearanceActions={compactAppearanceActions}
/>
}
/>
</Routes>
</RouteBaseProvider>
);
}
/**
* 写作配置表单面板(写作助手回填的目标),抽出复用:idle 时在右栏,开始写作后在左栏。
*/
interface NotebookProps {
initialNotebookId?: number;
initialDocumentIds?: number[];
studioToken?: string;
}
function WritingFormPanel({
hideAdvancedFields,
initialNotebookId,
initialDocumentIds,
studioToken,
headerActionsBeforeCollapse,
}: {
hideAdvancedFields: boolean;
headerActionsBeforeCollapse?: ReactNode;
} & NotebookProps) {
return (
// 与原布局保持相同的 flex-1 min-h-0 包裹,确保内部 h-full 正确解析
<div className="flex-1 min-h-0 overflow-hidden">
<AIWritingPanel
hideAdvancedFields={hideAdvancedFields}
initialNotebookId={initialNotebookId}
initialDocumentIds={initialDocumentIds}
studioToken={studioToken}
headerActionsBeforeCollapse={headerActionsBeforeCollapse}
/>
</div>
);
}
/**
* 根据写作状态切换两套左右布局:
* - idle(初始问答阶段):左=「写作助手 Q&A」,右=「写作配置选项」表单;
* - 开始写作后:回到原布局——左=写作配置/时间线面板,右=草稿预览。
*/
function AIWritingLayout({
hideAdvancedFields,
initialNotebookId,
initialDocumentIds,
studioToken,
compactAppearanceActions,
}: {
hideAdvancedFields: boolean;
compactAppearanceActions?: ReactNode;
} & NotebookProps) {
const layoutRef = useRef<GroupImperativeHandle>(null);
const { state } = useAIWriting();
const { isHistoryMode } = useAIWritingHistory();
const { formExpanded, setFormExpanded } = useWritingForm();
// 初始问答阶段:无历史查看;查看历史时与「正在写作」相同左右分栏(左时间线、右草稿)。
//
// 用稳定的 flex 结构(而非按 formExpanded 在两套容器间切换):左侧问答区永远是第一个
// 子节点、位置不变,避免收起/展开时 WritingSetupChat 被重新挂载(它在内部 useMemo 生成
// 独立 threadId,一旦重挂会换新线程、丢失对话)。右侧整块「写作配置表单」可收起为一条
// 竖向展开栏;问答侧点「应用到表单」(applyProposal) 会自动把 formExpanded 置 true 展开。
if (state.status === 'idle' && !isHistoryMode) {
return (
<div className="relative flex h-full">
{/* 左侧:写作助手 Q&A —— 恒为第一个子节点,位置稳定不重挂 */}
<div className="min-h-0 flex-1 overflow-hidden">
<WritingSetupChat />
</div>
{/* 右侧:写作配置表单整块。容器持久存在(表单始终挂载、保留状态),宽度随 formExpanded
平滑过渡——收起时宽度归零、整块滑出,右侧完全让给问答区。 */}
<div
className={cn(
'flex min-h-0 shrink-0 flex-col overflow-hidden transition-[width,min-width] duration-300 ease-in-out',
formExpanded ? 'w-[36%] min-w-[340px] max-w-[600px] border-l' : 'w-0 min-w-0',
)}
>
<WritingFormPanel
hideAdvancedFields={hideAdvancedFields}
initialNotebookId={initialNotebookId}
initialDocumentIds={initialDocumentIds}
studioToken={studioToken}
headerActionsBeforeCollapse={compactAppearanceActions}
/>
</div>
{/* 收起态:右上角浮动展开图标按钮(与展开态头部的收起按钮位置呼应) */}
{!formExpanded && (
<div className="absolute right-3 top-2 z-10 flex items-center gap-1">
{compactAppearanceActions}
<button
type="button"
onClick={() => setFormExpanded(true)}
title="展开写作配置"
className="flex h-7 w-7 items-center justify-center rounded-md border bg-background text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground"
>
<PanelRightOpen className="size-4" />
</button>
</div>
)}
</div>
);
}
return (
<ResizablePanelGroup
orientation="horizontal"
groupRef={layoutRef}
defaultLayout={DEFAULT_LAYOUT}
className="h-full"
>
{/* 左侧:AI 写作面板,与原画布页左侧等宽 */}
<ResizablePanel
id={WRITING_ID}
defaultSize={35}
minSize={20}
className="overflow-hidden flex flex-col"
>
<WritingFormPanel hideAdvancedFields={hideAdvancedFields} />
</ResizablePanel>
{/* 隐形拖拽热区只向右(草稿区)延伸、可见抓手也微微右移,二者都不再盖住左侧时间线的
滚动条——否则鼠标想抓滚动条会误触左右 resize、导致滑不动。右侧是草稿区留白(px-10),
盖住无副作用。 */}
<ResizableHandle
withHandle
className="after:left-0 after:w-3 after:translate-x-0 [&>div]:translate-x-1.5"
/>
{/* 右侧:草稿预览面板 */}
<ResizablePanel
id={DRAFT_ID}
defaultSize={65}
minSize={40}
className="overflow-hidden"
>
<AIWritingDraftPanel />
</ResizablePanel>
</ResizablePanelGroup>
);
}
/**
* 写作页主体(侧栏 + 内容区)。放在 Provider 内部,以便读 useAIWriting 状态:
* 进入写作(status !== idle 或查看历史)后自动收起左侧研究模板栏,让写作区更宽。
*/
function AIWritingBody({
hideAdvancedFields,
initialNotebookId,
initialDocumentIds,
studioToken,
compactChrome = false,
}: {
hideAdvancedFields: boolean;
compactChrome?: boolean;
} & NotebookProps) {
const { state } = useAIWriting();
const { isHistoryMode } = useAIWritingHistory();
const location = useLocation();
const navigate = useNavigate();
const routeBase = useAIWritingRouteBase();
const agentRoute = useAIWritingAgentRouteState(routeBase);
const isAdmin = getStoredAuth()?.system_role === 'admin';
const externalNotebookMode =
studioToken != null || initialNotebookId != null || initialDocumentIds != null;
// 需求 0616 #5:左栏点「笔记本 / 所有空间」→ 在右侧内嵌打开对应页,替换右侧内容
// (写作布局不卸载,仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程)。
const [rightView, setRightView] = useState<RightView>(null);
const overlayActive = rightView != null;
const viewingNotebookId = rightView?.kind === 'notebook' ? rightView.id : null;
// 侧栏「收起」态:由页面承载,ResearchSidebar 顶部的收起按钮触发,收起后显示一条细窄展开条。
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [sidebarTab, setSidebarTab] = useState<'research' | 'agents'>('agents');
const [settingsOpen, setSettingsOpen] = useState(false);
useEffect(() => {
if (agentRoute.active) {
setSidebarTab('agents');
}
}, [agentRoute.active]);
const agentsActive = !externalNotebookMode && sidebarTab === 'agents';
// 研究模板侧栏在「写作配置(idle)」与「历史回看」两态都展示(点历史记录后保留侧栏、
// 历史内容显示到右侧区域);写作进行中收起;嵌入态(studioToken)始终不展示。
const showSidebar =
!externalNotebookMode && (state.status === 'idle' || isHistoryMode || agentsActive);
const compactAppearanceActions = compactChrome ? (
<AIWritingCompactAppearanceActions />
) : null;
return (
<div className="flex-1 min-h-0 overflow-hidden flex">
{/* 左栏:课题研究 / 智能体。课题研究支持收起为细窄展开条。 */}
{showSidebar && (
<div
className={cn(
'relative h-full shrink-0 overflow-hidden border-r border-slate-100 bg-white text-slate-700 shadow-[1px_0_18px_rgba(15,23,42,0.03)] transition-[width] duration-300 ease-in-out dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:text-[#c5cfe0] dark:shadow-none dark:backdrop-blur-md',
sidebarCollapsed ? 'w-11' : 'w-[320px]',
)}
>
<div
className={cn(
'absolute inset-0 flex flex-col items-center gap-2 py-3 transition-opacity duration-200 ease-out',
sidebarCollapsed ? 'opacity-100' : 'pointer-events-none opacity-0',
)}
>
<button
type="button"
onClick={() => setSidebarCollapsed(false)}
title="展开侧栏"
aria-label="展开侧栏"
className="rounded-md p-1.5 text-slate-400 transition-colors hover:bg-slate-50 hover:text-slate-700 dark:text-[#8aa0c4] dark:hover:bg-white/10 dark:hover:text-[#e8edf5]"
>
<PanelLeftOpen className="size-4" />
</button>
<History className="size-4 text-slate-300 dark:text-[#6e88b0]" />
</div>
<aside
className={cn(
'absolute inset-y-0 left-0 flex h-full w-[320px] flex-col overflow-hidden transition-all duration-300 ease-in-out',
sidebarCollapsed ? 'pointer-events-none -translate-x-4 opacity-0' : 'translate-x-0 opacity-100',
)}
>
<div className="flex shrink-0 items-center gap-2 border-b border-slate-100 px-3 py-2 dark:border-[#2b8ffe]/20">
<div className="grid flex-1 grid-cols-2 rounded-lg bg-slate-100 p-1 dark:bg-white/10">
<button
type="button"
onClick={() => {
setSidebarTab('agents');
setRightView(null);
if (!agentRoute.active) navigate(routeBase);
}}
className={cn(
'rounded-md px-2 py-1.5 text-xs font-semibold transition-colors',
sidebarTab === 'agents'
? 'bg-white text-slate-900 shadow-sm dark:bg-[#122347]/90 dark:text-[#e8edf5] dark:shadow-none'
: 'text-slate-500 hover:text-slate-800 dark:text-[#8aa0c4] dark:hover:bg-white/10 dark:hover:text-[#e8edf5]',
)}
>
课题研究
</button>
<button
type="button"
onClick={() => {
setSidebarTab('research');
setRightView(null);
if (location.pathname !== routeBase) navigate(routeBase);
}}
className={cn(
'rounded-md px-2 py-1.5 text-xs font-semibold transition-colors',
sidebarTab === 'research'
? 'bg-white text-slate-900 shadow-sm dark:bg-[#122347]/90 dark:text-[#e8edf5] dark:shadow-none'
: 'text-slate-500 hover:text-slate-800 dark:text-[#8aa0c4] dark:hover:bg-white/10 dark:hover:text-[#e8edf5]',
)}
>
ai写作
</button>
</div>
<button
type="button"
onClick={() => setSidebarCollapsed(true)}
title="收起侧栏"
aria-label="收起侧栏"
className="rounded-md p-1 text-slate-400 transition-colors hover:bg-slate-50 hover:text-slate-700 dark:text-[#8aa0c4] dark:hover:bg-white/10 dark:hover:text-[#e8edf5]"
>
<PanelLeftClose className="size-4" />
</button>
</div>
<div className="min-h-0 flex-1 overflow-hidden">
{sidebarTab === 'research' ? (
<ResearchSidebar
viewingNotebookId={viewingNotebookId}
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
onOpenAllSpaces={() => setRightView({ kind: 'allSpaces' })}
/>
) : (
<AgentModuleSidebar
moduleKey={AI_WRITING_AGENT_MODULE_KEY}
activeAgentId={agentRoute.agentId}
activeThreadId={agentRoute.threadId}
routeBase={routeBase}
headerTitle={AI_WRITING_AGENT_MODULE_TITLE}
className="w-full border-r-0"
onOpenSettings={isAdmin ? () => setSettingsOpen(true) : undefined}
/>
)}
</div>
</aside>
</div>
)}
{/* 右侧:写作布局常驻(仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程),
查看笔记本 / 所有空间时在其上叠加内嵌页。 */}
<div className="relative flex-1 min-h-0 overflow-hidden">
<div className={cn('absolute inset-0', (overlayActive || agentsActive) && 'hidden')}>
<AIWritingLayout
hideAdvancedFields={hideAdvancedFields}
initialNotebookId={initialNotebookId}
initialDocumentIds={initialDocumentIds}
studioToken={studioToken}
compactAppearanceActions={compactAppearanceActions}
/>
</div>
{overlayActive && !agentsActive && (
<div className="absolute inset-0 flex flex-col bg-background">
<div className="flex shrink-0 items-center gap-1 border-b border-border px-4 py-2">
<button
type="button"
onClick={() => setRightView(null)}
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<ArrowLeft className="size-4" />
返回写作
</button>
{SHOW_NOTEBOOK_SPACE_ENTRIES && rightView.kind === 'notebook' && (
<button
type="button"
onClick={() => setRightView({ kind: 'allSpaces' })}
className="ml-1 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
所有空间
</button>
)}
</div>
<div className="min-h-0 flex-1 overflow-hidden">
{rightView.kind === 'notebook' ? (
// 笔记本详情页内部用 ChatBox(useArtifacts) → 需 ChatRuntime 提供 Provider。
<ChatRuntime>
<StudioNotebookDetailPage key={rightView.id} notebookId={rightView.id} />
</ChatRuntime>
) : (
<div className="h-full overflow-y-auto">
<StudioNotebooksPage
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
/>
</div>
)}
</div>
</div>
)}
{agentsActive && (
<div className="workspace-chat-bg absolute inset-0">
<AIWritingAgentRoutes
base={routeBase}
compactAppearanceActions={compactAppearanceActions}
/>
</div>
)}
</div>
{isAdmin && (
<AgentModuleSettingsDialog
open={settingsOpen}
onOpenChange={setSettingsOpen}
moduleKey={AI_WRITING_AGENT_MODULE_KEY}
moduleTitle={AI_WRITING_AGENT_MODULE_TITLE}
/>
)}
</div>
);
}
export default function AIWritingPage({
hideAdvancedFields = false,
initialNotebookId,
initialDocumentIds,
studioToken,
compactChrome = false,
}: {
hideAdvancedFields?: boolean;
initialNotebookId?: number;
initialDocumentIds?: number[];
studioToken?: string;
compactChrome?: boolean;
} = {}) {
return (
<CanvasThreadProvider>
<CanvasGraphProvider>
<CanvasUIProvider>
<AIWritingHistoryProvider>
<AIWritingProvider>
<WritingFormProvider
initialNotebookId={initialNotebookId}
initialDocumentIds={initialDocumentIds}
>
<TooltipProvider delayDuration={300}>
<div className="h-full flex flex-col overflow-hidden bg-background canvas-writing-bg">
<AIWritingBody
hideAdvancedFields={hideAdvancedFields}
initialNotebookId={initialNotebookId}
initialDocumentIds={initialDocumentIds}
studioToken={studioToken}
compactChrome={compactChrome}
/>
</div>
</TooltipProvider>
</WritingFormProvider>
</AIWritingProvider>
</AIWritingHistoryProvider>
</CanvasUIProvider>
</CanvasGraphProvider>
</CanvasThreadProvider>
);
}