fix: polish compact workspace controls

This commit is contained in:
fengxiangdebaozi committed 2026-06-29 16:44:13 +08:00
1 parent eb61b332a2
commit 210cb5117f
13 files changed
+516 -274

No files matched your search

@@ -17,6 +17,7 @@ import { useStudioNotebooks, useNotebookSources } from '@/core/studio/hooks';
import type { AIWritingRequest } from '../../types/ai-writing';
import { useArticleTypes } from '../../hooks/useArticleTypes';
import { useWritingForm } from '../../contexts/WritingFormContext';
import { ResearchTemplatesPanel } from './ResearchTemplatesPanel';
interface AIWritingFormProps {
onSubmit: (req: AIWritingRequest) => void;
@@ -436,6 +437,8 @@ export function AIWritingForm({
</Button>
</div>
<ResearchTemplatesPanel />
{!hideAdvancedFields && (
<>
<div className="flex items-start gap-2 rounded-xl border border-[#e8dedd] bg-[#f9f5f5] px-3 py-3">
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { ChevronsDown, CloudUpload, PanelRightClose, Pause, Play, Plus, RotateCcw, Settings2, Trash2 } from 'lucide-react';
import { toast } from 'sonner';
@@ -73,11 +73,13 @@ export function AIWritingPanel({
initialNotebookId,
initialDocumentIds,
studioToken,
headerActionsBeforeCollapse,
}: {
hideAdvancedFields?: boolean;
initialNotebookId?: number;
initialDocumentIds?: number[];
studioToken?: string;
headerActionsBeforeCollapse?: ReactNode;
} = {}) {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
@@ -429,6 +431,7 @@ export function AIWritingPanel({
onRename={rename}
onRefresh={refresh}
/>
{headerActionsBeforeCollapse}
{/* 收起写作配置:仅在 idle 配置阶段、表单展开时出现(与 AIWritingPage 的整块收起联动) */}
{writingForm && isIdle && !isHistoryMode && writingForm.formExpanded && (
<Button
@@ -1,29 +1,19 @@
"use client";
import {
BarChart3,
BookOpen,
Eye,
FileText,
Globe2,
History,
LayoutGrid,
Loader2,
Newspaper,
PanelLeftClose,
Pencil,
Plus,
RefreshCw,
Search,
Settings2,
SlidersHorizontal,
Sparkles,
Trash2,
TrendingUp,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useEffect, useRef, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
@@ -38,64 +28,11 @@ import { createSearchSpace } from "@/core/studio/api";
import { useStudioNotebooks } from "@/core/studio/hooks";
import { cn } from "@/lib/utils";
import { useResearchTemplates } from "../../hooks/useResearchTemplates";
import { useAIWritingSessions } from "../../hooks/useAIWritingSessions";
import type { ResearchTemplate } from "../../api/ai-writing-research-templates";
import type { AIWritingSessionSummary } from "../../api/ai-writing-sessions";
import { useWritingForm } from "../../contexts/WritingFormContext";
import { useAIWriting } from "../../contexts/AIWritingContext";
import { useAIWritingHistory } from "../../contexts/AIWritingHistoryContext";
import { formatRelative } from "./AIWritingHistoryDropdown";
type TemplateVisual = {
Icon: LucideIcon;
iconClass: string;
accentClass: string;
};
const TEMPLATE_VISUALS: TemplateVisual[] = [
{
Icon: Globe2,
iconClass: "bg-blue-50 text-blue-600",
accentClass: "bg-blue-500",
},
{
Icon: Newspaper,
iconClass: "bg-amber-50 text-amber-600",
accentClass: "bg-amber-500",
},
{
Icon: Sparkles,
iconClass: "bg-violet-50 text-violet-600",
accentClass: "bg-violet-500",
},
{
Icon: TrendingUp,
iconClass: "bg-rose-50 text-rose-600",
accentClass: "bg-rose-500",
},
{
Icon: BookOpen,
iconClass: "bg-emerald-50 text-emerald-600",
accentClass: "bg-emerald-500",
},
{
Icon: BarChart3,
iconClass: "bg-cyan-50 text-cyan-600",
accentClass: "bg-cyan-500",
},
];
function getTemplateVisual(tpl: ResearchTemplate, index: number): TemplateVisual {
const text = `${tpl.name} ${tpl.description ?? ""} ${tpl.article_type_key ?? ""}`;
if (/网摘|网络|热点|摘/.test(text)) return TEMPLATE_VISUALS[0];
if (/周报|周刊|日报|月报/.test(text)) return TEMPLATE_VISUALS[1];
if (/要讯|资讯|快讯|简报/.test(text)) return TEMPLATE_VISUALS[2];
if (/行业|分析|市场|竞争/.test(text)) return TEMPLATE_VISUALS[3];
if (/政策|解读|法规/.test(text)) return TEMPLATE_VISUALS[4];
return TEMPLATE_VISUALS[index % TEMPLATE_VISUALS.length];
}
// 历史记录状态药丸样式(对齐多智能体会商的历史记录观感)。
// 注:`in_progress` 不再转圈显示「进行中」——刷新=重置后这些会话已无任何活进程,
// 转圈会误导用户以为还在跑;统一显示为中性、不动的「未完成」。真正还在跑的是 `background`
@@ -237,25 +174,11 @@ export function ResearchSidebar({
/** 收起侧栏(由页面承载收起态)。 */
onCollapse?: () => void;
}) {
const navigate = useNavigate();
const { templates, refresh } = useResearchTemplates();
const { startWriting } = useAIWriting();
const {
notebooks,
isLoading: notebooksLoading,
isError: notebooksError,
} = useStudioNotebooks();
const {
setIntent,
setArticleType,
setOutlineText,
setShowOutline,
setMaterialSource,
setWordCountInput,
setAudience,
setStrictMode,
setFormExpanded,
} = useWritingForm();
const queryClient = useQueryClient();
// 「历史记录」列表(左下角,样式参考多智能体会商的历史记录):自带一份会话列表,
@@ -283,26 +206,11 @@ export function ResearchSidebar({
window.dispatchEvent(new CustomEvent("ai-writing:show-progress", { detail: session }));
};
const [templateSearch, setTemplateSearch] = useState("");
const [createOpen, setCreateOpen] = useState(false);
const [name, setName] = useState("");
const [creating, setCreating] = useState(false);
const [createError, setCreateError] = useState("");
useEffect(() => {
void refresh();
}, [refresh]);
const filteredTemplates = useMemo(() => {
const keyword = templateSearch.trim().toLowerCase();
if (!keyword) return templates;
return templates.filter((tpl) =>
[tpl.name, tpl.description, tpl.article_type_key]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(keyword)),
);
}, [templates, templateSearch]);
const handleCreate = async () => {
const trimmed = name.trim();
if (!trimmed) return;
@@ -323,39 +231,6 @@ export function ResearchSidebar({
}
};
const handleTemplateClick = (tpl: ResearchTemplate) => {
const intent = tpl.topic?.trim() ?? "";
const articleType = tpl.article_type_key || "analysis";
const outline = tpl.outline?.trim() || "";
const audience = tpl.audience?.trim() || "普通读者";
const wordCount = tpl.word_count > 0 ? tpl.word_count : 800;
const writingMode = tpl.strict_mode ? "strict" : "loose";
if (intent) setIntent(intent);
if (tpl.article_type_key) setArticleType(tpl.article_type_key);
if (outline) {
setOutlineText(outline);
setShowOutline(true);
}
setMaterialSource(tpl.material_source);
setWordCountInput(String(wordCount));
if (tpl.audience?.trim()) setAudience(audience);
setStrictMode(tpl.strict_mode);
setFormExpanded(true);
if (!intent) return;
void startWriting({
userIntent: intent,
userOutline: outline || undefined,
articleType,
targetAudience: audience,
wordCountTarget: wordCount,
keywordCount: 1,
writingMode,
materialSource: tpl.material_source,
});
};
return (
<aside className="ai-research-sidebar flex h-full w-[320px] shrink-0 flex-col overflow-hidden border-r border-slate-100 bg-white text-slate-700 shadow-[1px_0_18px_rgba(15,23,42,0.03)]">
{onCollapse && (
@@ -373,91 +248,7 @@ export function ResearchSidebar({
</div>
)}
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden px-3 py-4">
<section className="mb-5">
<div className="mb-3 flex items-center justify-between gap-3 px-1">
<button
type="button"
onClick={() => navigate("/page/canvas/research-templates")}
title="配置研究模板"
aria-label="配置研究模板"
className="group flex items-center gap-2 rounded-lg text-[16px] font-bold text-slate-500 transition-colors hover:text-slate-800"
>
<SlidersHorizontal className="size-4 text-slate-400" />
<span>研究模板</span>
<Settings2 className="size-3.5 text-slate-300 opacity-0 transition-opacity group-hover:opacity-100" />
</button>
<div className="relative w-[150px]">
<Search className="pointer-events-none absolute left-3 top-1/2 size-3.5 -translate-y-1/2 text-slate-400" />
<input
type="text"
value={templateSearch}
onChange={(event) => setTemplateSearch(event.target.value)}
placeholder="搜索写作模板..."
className="h-8 w-full rounded-lg border border-slate-200 bg-slate-50/80 pl-8 pr-3 text-[12px] text-slate-600 outline-none transition focus:border-blue-200 focus:bg-white focus:ring-2 focus:ring-blue-100"
/>
</div>
</div>
{templates.length === 0 ? (
<p className="rounded-xl border border-dashed border-slate-200 px-3 py-5 text-center text-xs text-slate-400">
暂无研究模板,点击标题配置
</p>
) : filteredTemplates.length === 0 ? (
<p className="rounded-xl border border-dashed border-slate-200 px-3 py-5 text-center text-xs text-slate-400">
暂无匹配模板
</p>
) : (
<div className="space-y-3">
{filteredTemplates.map((tpl, index) => {
const visual = getTemplateVisual(tpl, index);
const Icon = visual.Icon;
return (
<button
key={tpl.id}
type="button"
onClick={() => handleTemplateClick(tpl)}
title={tpl.description ?? tpl.name}
className="group relative flex w-full items-start gap-3 overflow-hidden rounded-[5px] bg-white px-4 py-4 text-left shadow-[0_8px_24px_rgba(15,23,42,0.04)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_30px_rgba(30,64,175,0.08)] active:translate-y-0"
>
<span
className={cn(
"flex size-12 shrink-0 items-center justify-center rounded-xl transition-colors",
visual.iconClass,
)}
>
<Icon className="size-6 stroke-[2.2]" />
</span>
<span className="min-w-0 flex-1 pt-0.5">
<span className="block truncate text-[16px] font-bold leading-5 text-slate-900 transition-colors group-hover:text-blue-700">
{tpl.name}
</span>
{tpl.description && (
<span
className="mt-1.5 overflow-hidden text-[13px] leading-6 text-slate-500"
style={{
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 2,
}}
>
{tpl.description}
</span>
)}
</span>
<span
className={cn(
"absolute bottom-4 left-0 top-4 w-1 rounded-r-full opacity-0 transition-opacity group-hover:opacity-100",
visual.accentClass,
)}
/>
</button>
);
})}
</div>
)}
</section>
<section className="flex flex-col border-t border-slate-100 pt-5">
<section className="flex flex-col">
<div className="mb-3 flex items-center justify-between px-2">
<h2 className="text-[15px] font-bold tracking-wide text-slate-400">
笔记本
@@ -0,0 +1,238 @@
"use client";
import {
BarChart3,
BookOpen,
Globe2,
Newspaper,
Search,
Settings2,
SlidersHorizontal,
Sparkles,
TrendingUp,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { cn } from "@/lib/utils";
import type { ResearchTemplate } from "../../api/ai-writing-research-templates";
import { useAIWriting } from "../../contexts/AIWritingContext";
import { useWritingForm } from "../../contexts/WritingFormContext";
import { useResearchTemplates } from "../../hooks/useResearchTemplates";
type TemplateVisual = {
Icon: LucideIcon;
iconClass: string;
accentClass: string;
};
const TEMPLATE_VISUALS: TemplateVisual[] = [
{
Icon: Globe2,
iconClass: "bg-blue-50 text-blue-600",
accentClass: "bg-blue-500",
},
{
Icon: Newspaper,
iconClass: "bg-amber-50 text-amber-600",
accentClass: "bg-amber-500",
},
{
Icon: Sparkles,
iconClass: "bg-violet-50 text-violet-600",
accentClass: "bg-violet-500",
},
{
Icon: TrendingUp,
iconClass: "bg-rose-50 text-rose-600",
accentClass: "bg-rose-500",
},
{
Icon: BookOpen,
iconClass: "bg-emerald-50 text-emerald-600",
accentClass: "bg-emerald-500",
},
{
Icon: BarChart3,
iconClass: "bg-cyan-50 text-cyan-600",
accentClass: "bg-cyan-500",
},
];
function getTemplateVisual(
tpl: ResearchTemplate,
index: number,
): TemplateVisual {
const text = `${tpl.name} ${tpl.description ?? ""} ${tpl.article_type_key ?? ""}`;
if (/网摘|网络|热点|摘/.test(text)) return TEMPLATE_VISUALS[0];
if (/周报|周刊|日报|月报/.test(text)) return TEMPLATE_VISUALS[1];
if (/要讯|资讯|快讯|简报/.test(text)) return TEMPLATE_VISUALS[2];
if (/行业|分析|市场|竞争/.test(text)) return TEMPLATE_VISUALS[3];
if (/政策|解读|法规/.test(text)) return TEMPLATE_VISUALS[4];
return TEMPLATE_VISUALS[index % TEMPLATE_VISUALS.length];
}
export function ResearchTemplatesPanel({
className,
}: {
className?: string;
}) {
const navigate = useNavigate();
const { templates, refresh } = useResearchTemplates();
const { startWriting } = useAIWriting();
const {
setIntent,
setArticleType,
setOutlineText,
setShowOutline,
setMaterialSource,
setWordCountInput,
setAudience,
setStrictMode,
setFormExpanded,
} = useWritingForm();
const [templateSearch, setTemplateSearch] = useState("");
useEffect(() => {
void refresh();
}, [refresh]);
const filteredTemplates = useMemo(() => {
const keyword = templateSearch.trim().toLowerCase();
if (!keyword) return templates;
return templates.filter((tpl) =>
[tpl.name, tpl.description, tpl.article_type_key]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(keyword)),
);
}, [templates, templateSearch]);
const handleTemplateClick = (tpl: ResearchTemplate) => {
const intent = tpl.topic?.trim() ?? "";
const articleType = tpl.article_type_key || "analysis";
const outline = tpl.outline?.trim() || "";
const audience = tpl.audience?.trim() || "普通读者";
const wordCount = tpl.word_count > 0 ? tpl.word_count : 800;
const writingMode = tpl.strict_mode ? "strict" : "loose";
if (intent) setIntent(intent);
if (tpl.article_type_key) setArticleType(tpl.article_type_key);
if (outline) {
setOutlineText(outline);
setShowOutline(true);
}
setMaterialSource(tpl.material_source);
setWordCountInput(String(wordCount));
if (tpl.audience?.trim()) setAudience(audience);
setStrictMode(tpl.strict_mode);
setFormExpanded(true);
if (!intent) return;
void startWriting({
userIntent: intent,
userOutline: outline || undefined,
articleType,
targetAudience: audience,
wordCountTarget: wordCount,
keywordCount: 1,
writingMode,
materialSource: tpl.material_source,
});
};
return (
<section
className={cn(
"rounded-2xl border border-[#ece5e4] bg-card p-5 shadow-sm dark:border-[#213D5E] dark:bg-[#02244C]",
className,
)}
>
<div className="mb-4 flex items-center justify-between gap-3">
<button
type="button"
onClick={() => navigate("/page/canvas/research-templates")}
title="配置研究模板"
aria-label="配置研究模板"
className="group flex items-center gap-2 rounded-lg text-[16px] font-bold text-slate-600 transition-colors hover:text-slate-900 dark:text-white"
>
<SlidersHorizontal className="size-4 text-slate-400 dark:text-white/70" />
<span>研究模板</span>
<Settings2 className="size-3.5 text-slate-300 opacity-0 transition-opacity group-hover:opacity-100 dark:text-white/60" />
</button>
<div className="relative min-w-0 flex-1 sm:max-w-[180px]">
<Search className="pointer-events-none absolute left-3 top-1/2 size-3.5 -translate-y-1/2 text-slate-400" />
<input
type="text"
value={templateSearch}
onChange={(event) => setTemplateSearch(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") event.preventDefault();
}}
placeholder="搜索写作模板..."
className="h-8 w-full rounded-lg border border-slate-200 bg-slate-50/80 pl-8 pr-3 text-[12px] text-slate-600 outline-none transition focus:border-blue-200 focus:bg-white focus:ring-2 focus:ring-blue-100 dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF]"
/>
</div>
</div>
{templates.length === 0 ? (
<p className="rounded-xl border border-dashed border-slate-200 px-3 py-5 text-center text-xs text-slate-400">
暂无研究模板,点击标题配置
</p>
) : filteredTemplates.length === 0 ? (
<p className="rounded-xl border border-dashed border-slate-200 px-3 py-5 text-center text-xs text-slate-400">
暂无匹配模板
</p>
) : (
<div className="space-y-3">
{filteredTemplates.map((tpl, index) => {
const visual = getTemplateVisual(tpl, index);
const Icon = visual.Icon;
return (
<button
key={tpl.id}
type="button"
onClick={() => handleTemplateClick(tpl)}
title={tpl.description ?? tpl.name}
className="group relative flex w-full items-start gap-3 overflow-hidden rounded-[5px] bg-white px-4 py-4 text-left shadow-[0_8px_24px_rgba(15,23,42,0.04)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_30px_rgba(30,64,175,0.08)] active:translate-y-0 dark:bg-[#032650]"
>
<span
className={cn(
"flex size-12 shrink-0 items-center justify-center rounded-xl transition-colors",
visual.iconClass,
)}
>
<Icon className="size-6 stroke-[2.2]" />
</span>
<span className="min-w-0 flex-1 pt-0.5">
<span className="block truncate text-[16px] font-bold leading-5 text-slate-900 transition-colors group-hover:text-blue-700 dark:text-white">
{tpl.name}
</span>
{tpl.description && (
<span
className="mt-1.5 overflow-hidden text-[13px] leading-6 text-slate-500 dark:text-[#AACEFF]/75"
style={{
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 2,
}}
>
{tpl.description}
</span>
)}
</span>
<span
className={cn(
"absolute bottom-4 left-0 top-4 w-1 rounded-r-full opacity-0 transition-opacity group-hover:opacity-100",
visual.accentClass,
)}
/>
</button>
);
})}
</div>
)}
</section>
);
}
@@ -81,7 +81,7 @@
* -d '{"retention_days": 0}'
* ──────────────────────────────────────────────────────────────────────────
*/
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import {
ArrowLeft,
History,
@@ -113,6 +113,10 @@ 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,
@@ -137,10 +141,20 @@ 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`;
// 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(() => {
@@ -170,7 +184,15 @@ function useAIWritingAgentRouteState(base: string) {
function AIWritingAgentDefaultRedirect() {
const { groups, isLoading } = useAgentModuleSidebarData(AI_WRITING_AGENT_MODULE_KEY);
const firstAgent = groups.flatMap((group) => group.agents)[0];
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 (
@@ -180,7 +202,7 @@ function AIWritingAgentDefaultRedirect() {
);
}
if (!firstAgent) {
if (!targetAgent) {
return (
<div className="flex size-full items-center justify-center px-6 text-center text-sm text-muted-foreground">
暂无可用智能体,请先在模块设置中配置或创建智能体。
@@ -188,24 +210,44 @@ function AIWritingAgentDefaultRedirect() {
);
}
return <Navigate to={`agents/${firstAgent.id}/chats/new`} replace />;
return <Navigate to={`agents/${targetAgent.id}/chats/new`} replace />;
}
function AIWritingAgentChatRoute() {
function AIWritingAgentChatRoute({
compactAppearanceActions,
}: {
compactAppearanceActions?: ReactNode;
}) {
const { agent_id } = useParams<{ agent_id: string }>();
return (
<ChatRuntime>
<AgentChatPage key={agent_id} />
<AgentChatPage
key={agent_id}
compactAppearanceActions={compactAppearanceActions}
/>
</ChatRuntime>
);
}
function AIWritingAgentRoutes({ base }: { base: string }) {
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 />} />
<Route
path="agents/:agent_id/chats/:thread_id"
element={
<AIWritingAgentChatRoute
compactAppearanceActions={compactAppearanceActions}
/>
}
/>
</Routes>
</RouteBaseProvider>
);
@@ -220,11 +262,26 @@ interface NotebookProps {
studioToken?: string;
}
function WritingFormPanel({ hideAdvancedFields, initialNotebookId, initialDocumentIds, studioToken }: { hideAdvancedFields: boolean } & NotebookProps) {
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} />
<AIWritingPanel
hideAdvancedFields={hideAdvancedFields}
initialNotebookId={initialNotebookId}
initialDocumentIds={initialDocumentIds}
studioToken={studioToken}
headerActionsBeforeCollapse={headerActionsBeforeCollapse}
/>
</div>
);
}
@@ -234,7 +291,16 @@ function WritingFormPanel({ hideAdvancedFields, initialNotebookId, initialDocume
* - idle(初始问答阶段):左=「写作助手 Q&A」,右=「写作配置选项」表单;
* - 开始写作后:回到原布局——左=写作配置/时间线面板,右=草稿预览。
*/
function AIWritingLayout({ hideAdvancedFields, initialNotebookId, initialDocumentIds, studioToken }: { hideAdvancedFields: boolean } & NotebookProps) {
function AIWritingLayout({
hideAdvancedFields,
initialNotebookId,
initialDocumentIds,
studioToken,
compactAppearanceActions,
}: {
hideAdvancedFields: boolean;
compactAppearanceActions?: ReactNode;
} & NotebookProps) {
const layoutRef = useRef<GroupImperativeHandle>(null);
const { state } = useAIWriting();
const { isHistoryMode } = useAIWritingHistory();
@@ -262,19 +328,28 @@ function AIWritingLayout({ hideAdvancedFields, initialNotebookId, initialDocumen
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} />
<WritingFormPanel
hideAdvancedFields={hideAdvancedFields}
initialNotebookId={initialNotebookId}
initialDocumentIds={initialDocumentIds}
studioToken={studioToken}
headerActionsBeforeCollapse={compactAppearanceActions}
/>
</div>
{/* 收起态:右上角浮动展开图标按钮(与展开态头部的收起按钮位置呼应) */}
{!formExpanded && (
<button
type="button"
onClick={() => setFormExpanded(true)}
title="展开写作配置"
className="absolute right-3 top-2 z-10 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 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>
);
@@ -322,7 +397,16 @@ function AIWritingLayout({ hideAdvancedFields, initialNotebookId, initialDocumen
* 写作页主体(侧栏 + 内容区)。放在 Provider 内部,以便读 useAIWriting 状态:
* 进入写作(status !== idle 或查看历史)后自动收起左侧研究模板栏,让写作区更宽。
*/
function AIWritingBody({ hideAdvancedFields, initialNotebookId, initialDocumentIds, studioToken }: { hideAdvancedFields: boolean } & NotebookProps) {
function AIWritingBody({
hideAdvancedFields,
initialNotebookId,
initialDocumentIds,
studioToken,
compactChrome = false,
}: {
hideAdvancedFields: boolean;
compactChrome?: boolean;
} & NotebookProps) {
const { state } = useAIWriting();
const { isHistoryMode } = useAIWritingHistory();
const location = useLocation();
@@ -354,6 +438,9 @@ function AIWritingBody({ hideAdvancedFields, initialNotebookId, initialDocumentI
// 历史内容显示到右侧区域);写作进行中收起;嵌入态(studioToken)始终不展示。
const showSidebar =
!studioToken && (state.status === 'idle' || isHistoryMode || agentsActive);
const compactAppearanceActions = compactChrome ? (
<AIWritingCompactAppearanceActions />
) : null;
return (
<div className="flex-1 min-h-0 overflow-hidden flex">
@@ -459,7 +546,13 @@ function AIWritingBody({ hideAdvancedFields, initialNotebookId, initialDocumentI
查看笔记本 / 所有空间时在其上叠加内嵌页。 */}
<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} />
<AIWritingLayout
hideAdvancedFields={hideAdvancedFields}
initialNotebookId={initialNotebookId}
initialDocumentIds={initialDocumentIds}
studioToken={studioToken}
compactAppearanceActions={compactAppearanceActions}
/>
</div>
{overlayActive && !agentsActive && (
<div className="absolute inset-0 flex flex-col bg-background">
@@ -500,7 +593,10 @@ function AIWritingBody({ hideAdvancedFields, initialNotebookId, initialDocumentI
)}
{agentsActive && (
<div className="absolute inset-0 bg-background">
<AIWritingAgentRoutes base={routeBase} />
<AIWritingAgentRoutes
base={routeBase}
compactAppearanceActions={compactAppearanceActions}
/>
</div>
)}
</div>
@@ -521,11 +617,13 @@ export default function AIWritingPage({
initialNotebookId,
initialDocumentIds,
studioToken,
compactChrome = false,
}: {
hideAdvancedFields?: boolean;
initialNotebookId?: number;
initialDocumentIds?: number[];
studioToken?: string;
compactChrome?: boolean;
} = {}) {
return (
<CanvasThreadProvider>
@@ -545,6 +643,7 @@ export default function AIWritingPage({
initialNotebookId={initialNotebookId}
initialDocumentIds={initialDocumentIds}
studioToken={studioToken}
compactChrome={compactChrome}
/>
</div>
</TooltipProvider>