feat: light app center and iframe refactor

This commit is contained in:
fengxiangdebaozi committed 2026-06-12 15:40:09 +08:00
1 parent 728a4b1170
commit 9d73f27146
35 files changed
+1878 -246

No files matched your search

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { ChevronsDown, Play, Plus, RotateCcw, Settings2, Square, Trash2 } from 'lucide-react';
import { ChevronsDown, PanelRightClose, Play, Plus, RotateCcw, Settings2, Square, Trash2 } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
@@ -15,6 +15,7 @@ import { useArticleTypes } from '../../hooks/useArticleTypes';
import { useAIWriting } from '../../contexts/AIWritingContext';
import { useAIWritingHistory } from '../../contexts/AIWritingHistoryContext';
import { useWritingFormOptional } from '../../contexts/WritingFormContext';
import { useAIWritingSessions } from '../../hooks/useAIWritingSessions';
import { adminCleanupAIWritingSessions, getAIWritingSession } from '../../api/ai-writing-sessions';
@@ -83,6 +84,7 @@ export function AIWritingPanel({
const [searchParams, setSearchParams] = useSearchParams();
const { state, startWriting, submitIntervention, pauseWriting, resumeWriting, reset } = useAIWriting();
const { historySession, isHistoryMode, openHistory, closeHistory } = useAIWritingHistory();
const writingForm = useWritingFormOptional();
const { sessions, loading: sessionsLoading, refresh, rename, remove } = useAIWritingSessions();
const { articleTypes, refresh: refreshArticleTypes } = useArticleTypes();
@@ -342,6 +344,18 @@ export function AIWritingPanel({
onRename={rename}
onRefresh={refresh}
/>
{/* 收起写作配置:仅在 idle 配置阶段、表单展开时出现(与 AIWritingPage 的整块收起联动) */}
{writingForm && isIdle && !isHistoryMode && writingForm.formExpanded && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => writingForm.setFormExpanded(false)}
title="收起写作配置"
>
<PanelRightClose className="size-3.5" />
</Button>
)}
</div>
</div>
@@ -19,6 +19,9 @@ import type { SetupWritingApprovalPayload } from '@/components/workspace/message
export interface WritingFormValue {
intent: string;
setIntent: (v: string) => void;
/** 整个写作配置表单主体是否展开;初始收起,用户点表头或问答侧「应用到表单」时展开。 */
formExpanded: boolean;
setFormExpanded: (v: boolean) => void;
showOutline: boolean;
setShowOutline: (v: boolean) => void;
outlineText: string;
@@ -55,6 +58,7 @@ export function useWritingFormOptional(): WritingFormValue | null {
export function WritingFormProvider({ children, initialMaterialSource }: { children: ReactNode; initialMaterialSource?: 'knowledge_base' | 'skill' | 'notebook' }) {
const [intent, setIntent] = useState('');
const [formExpanded, setFormExpanded] = useState(false);
const [showOutline, setShowOutline] = useState(false);
const [outlineText, setOutlineText] = useState('');
const [articleType, setArticleType] = useState('analysis');
@@ -65,6 +69,8 @@ export function WritingFormProvider({ children, initialMaterialSource }: { child
const [modelName, setModelName] = useState('');
const applyProposal = useCallback((payload: SetupWritingApprovalPayload) => {
// 问答侧识别出意图并点「应用到表单」时,自动展开表单让用户看到回填结果。
setFormExpanded(true);
if (payload.user_intent?.trim()) setIntent(payload.user_intent.trim());
if (payload.article_type?.trim()) setArticleType(payload.article_type.trim());
if (typeof payload.word_count_target === 'number' && payload.word_count_target > 0) {
@@ -85,6 +91,7 @@ export function WritingFormProvider({ children, initialMaterialSource }: { child
const value = useMemo<WritingFormValue>(
() => ({
intent, setIntent,
formExpanded, setFormExpanded,
showOutline, setShowOutline,
outlineText, setOutlineText,
articleType, setArticleType,
@@ -95,7 +102,7 @@ export function WritingFormProvider({ children, initialMaterialSource }: { child
modelName, setModelName,
applyProposal,
}),
[intent, showOutline, outlineText, articleType, wordCountInput, audience, strictMode, materialSource, modelName, applyProposal],
[intent, formExpanded, showOutline, outlineText, articleType, wordCountInput, audience, strictMode, materialSource, modelName, applyProposal],
);
return <WritingFormContext.Provider value={value}>{children}</WritingFormContext.Provider>;
@@ -82,7 +82,9 @@
* ──────────────────────────────────────────────────────────────────────────
*/
import { useRef } from 'react';
import { ChevronLeft } from 'lucide-react';
import { TooltipProvider } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import type { GroupImperativeHandle } from 'react-resizable-panels';
import { CanvasThreadProvider } from '../contexts/CanvasThreadContext';
@@ -93,7 +95,7 @@ import { AIWritingDraftPanel } from '../components/ai-writing/AIWritingDraftPane
import { WritingSetupChat } from '../components/ai-writing/WritingSetupChat';
import { AIWritingProvider, useAIWriting } from '../contexts/AIWritingContext';
import { AIWritingHistoryProvider, useAIWritingHistory } from '../contexts/AIWritingHistoryContext';
import { WritingFormProvider } from '../contexts/WritingFormContext';
import { WritingFormProvider, useWritingForm } from '../contexts/WritingFormContext';
import {
ResizableHandle,
ResizablePanel,
@@ -103,14 +105,9 @@ import {
// Panel IDs used as keys in the v4 Layout map
const WRITING_ID = 'aw-writing';
const DRAFT_ID = 'aw-draft';
// 初始问答阶段(idle)专用的两栏 ID:左=问答助手,右=写作配置表单
const SETUP_CHAT_ID = 'aw-setup-chat';
const SETUP_FORM_ID = 'aw-setup-form';
// Match the original canvas page proportions (chat=35, artifact=65)
const DEFAULT_LAYOUT = { [WRITING_ID]: 35, [DRAFT_ID]: 65 };
// idle 阶段镜像原比例:问答框在左占大头(65),配置表单在右占小头(35)
const SETUP_LAYOUT = { [SETUP_CHAT_ID]: 65, [SETUP_FORM_ID]: 35 };
/**
* 写作配置表单面板(写作助手回填的目标),抽出复用:idle 时在右栏,开始写作后在左栏。
@@ -139,38 +136,45 @@ function AIWritingLayout({ hideAdvancedFields, initialNotebookId, initialDocumen
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 (
<ResizablePanelGroup
orientation="horizontal"
groupRef={layoutRef}
defaultLayout={SETUP_LAYOUT}
className="h-full"
>
{/* 左侧:写作助手 Q&A */}
<ResizablePanel
id={SETUP_CHAT_ID}
defaultSize={65}
minSize={40}
className="overflow-hidden"
>
<div className="flex h-full">
{/* 左侧:写作助手 Q&A —— 恒为第一个子节点,位置稳定不重挂 */}
<div className="min-h-0 flex-1 overflow-hidden">
<WritingSetupChat />
</ResizablePanel>
</div>
<ResizableHandle withHandle />
{/* 右侧:写作配置选项表单 */}
<ResizablePanel
id={SETUP_FORM_ID}
defaultSize={35}
minSize={20}
className="overflow-hidden flex flex-col"
{/* 右侧:写作配置表单整块。容器持久存在,宽度随 formExpanded 平滑过渡(展开/收起
都有动画);收起时只剩一条竖向「展开」栏,收起按钮则在表单头部右上角。 */}
<div
className={cn(
'relative flex min-h-0 shrink-0 flex-col overflow-hidden border-l transition-[width,min-width] duration-300 ease-in-out',
formExpanded ? 'w-[36%] min-w-[340px] max-w-[600px]' : 'w-10 min-w-10',
)}
>
<WritingFormPanel hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
</ResizablePanel>
</ResizablePanelGroup>
{formExpanded ? (
<WritingFormPanel hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
) : (
<button
type="button"
onClick={() => setFormExpanded(true)}
title="展开写作配置"
className="flex h-full w-full flex-col items-center justify-center gap-3 bg-muted/30 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<ChevronLeft className="size-4" />
<span className="[writing-mode:vertical-rl] text-xs tracking-widest">写作配置</span>
</button>
)}
</div>
</div>
);
}