feat(ai写作): 大纲整理可见化 + 检索词结合大纲 + 素材/大纲阶段关思考

- 草稿确认卡:「给出修改意见」「让编辑继续审核」移到「确认定稿」上方。
- 大纲整理阶段文案:用户自带大纲时显示「整理你的大纲」而非「规划大纲」
  (OutlineStreamingView / 骨架屏 / 状态栏,按 request.userOutline 切换)。
- 素材收集专家:用户自带大纲时把大纲原文带入检索词生成,让关键词结合写作
  意图覆盖各章节主题(_keyword_outline_section,agent + legacy 两路)。
- 素材收集(检索词/排序)与大纲整理/生成阶段强制关闭思考
  (create_chat_model force_disable_thinking:GLM thinking.disabled +
  Qwen/vLLM enable_thinking=false),关不掉的模型仍由 ThinkTagParser 把
  <think> 分流到思考框正常展示。disable_thinking 串入
  _utils.get_model/llm_json 与 _agent_runner.stream/run_writing_agent。
- 含草稿「局部修改」WIP(writer_draft 只重写命中章节)及外观控件改动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
fengxiangdebaoziandClaude Opus 4.8 committed 2026-06-24 18:32:10 +08:00
1 parent 8678ceb422
commit 49173d9c28
13 files changed
+485 -36

No files matched your search

@@ -2,6 +2,7 @@
import { Check, LayoutIcon, PanelLeftIcon, Shirt } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import {
@@ -140,6 +141,7 @@ export function LayoutModeSwitcher({
fallback?: "full" | "compact";
}) {
const [mode, setLayoutMode] = useLayoutMode();
const navigate = useNavigate();
const current = mode ?? fallback;
const TriggerIcon = current === "full" ? PanelLeftIcon : LayoutIcon;
@@ -166,7 +168,10 @@ export function LayoutModeSwitcher({
return (
<DropdownMenuItem
key={option.id}
onClick={() => setLayoutMode(option.id)}
onClick={() => {
setLayoutMode(option.id);
navigate("/page/workspace/chats/new");
}}
className="gap-2"
>
<Icon className="size-4 shrink-0" />
@@ -496,9 +496,13 @@ export function AIWritingPanel({
/>
)}
{/* 大纲流式思考框 */}
{/* 大纲流式思考框(用户自带大纲时文案显示「整理」) */}
{state.status === 'writing_outline' && (state.outlineStreamingText || state.outlineThinkingText) && (
<OutlineStreamingView text={state.outlineStreamingText} thinkingText={state.outlineThinkingText} />
<OutlineStreamingView
text={state.outlineStreamingText}
thinkingText={state.outlineThinkingText}
organizing={!!state.request?.userOutline}
/>
)}
{/* 编辑审核流式结果 */}
@@ -560,7 +564,11 @@ export function AIWritingPanel({
/* 意图解析流式过程中(LLM 还在吐 JSON)不要显示 skeleton */
!state.intentStreamingText && !state.intentThinkingText && (
<div className="px-4 pb-4">
<WritingSkeletonCard status={state.status} researchProgress={state.researchProgress} />
<WritingSkeletonCard
status={state.status}
researchProgress={state.researchProgress}
organizing={!!state.request?.userOutline}
/>
</div>
)}
@@ -605,7 +613,9 @@ export function AIWritingPanel({
)}
{state.status === 'researching' && state.researchProgress
? state.researchProgress
: STATUS_LABEL[state.status] ?? state.status}
: state.status === 'writing_outline' && state.request?.userOutline
? '作家整理你的大纲中…'
: STATUS_LABEL[state.status] ?? state.status}
</>
)}
</div>
@@ -455,13 +455,6 @@ function DraftConfirmCard({ draftTitle, draftWordCount, wordCountTarget, revisio
)}
<div className="flex flex-col gap-1.5 pt-1">
<Button
type="button" size="sm" className="w-full h-8 text-xs gap-1.5"
onClick={() => submit('finalize')} disabled={disabled || submitting}
>
<CheckCircle2 className="size-3.5" />确认定稿
</Button>
{/* 按编辑审核意见修改(有审核结果时始终显示) */}
{hasReview && (
<Button
@@ -504,6 +497,13 @@ function DraftConfirmCard({ draftTitle, draftWordCount, wordCountTarget, revisio
<ArrowRight className="size-3.5" />让编辑继续审核
</Button>
</div>
<Button
type="button" size="sm" className="w-full h-8 text-xs gap-1.5"
onClick={() => submit('finalize')} disabled={disabled || submitting}
>
<CheckCircle2 className="size-3.5" />确认定稿
</Button>
</div>
</div>
);
@@ -4,9 +4,11 @@ import { FileText, ChevronDown, ChevronRight, Brain } from 'lucide-react';
interface OutlineStreamingViewProps {
text: string;
thinkingText?: string;
/** 用户自带大纲时为 true:文案显示「整理你的大纲」而非「规划大纲」。 */
organizing?: boolean;
}
export function OutlineStreamingView({ text, thinkingText }: OutlineStreamingViewProps) {
export function OutlineStreamingView({ text, thinkingText, organizing }: OutlineStreamingViewProps) {
const preRef = useRef<HTMLPreElement>(null);
const thinkRef = useRef<HTMLPreElement>(null);
const [thinkOpen, setThinkOpen] = useState(true);
@@ -66,7 +68,7 @@ export function OutlineStreamingView({ text, thinkingText }: OutlineStreamingVie
<div className="flex items-center gap-2 px-3 py-2 border-b border-purple-200/60 dark:border-purple-900/60 bg-purple-100/40 dark:bg-purple-900/30">
<FileText className="size-3.5 text-purple-500 shrink-0" />
<span className="text-xs font-medium text-purple-600 dark:text-purple-400">
作家正在规划大纲…
{organizing ? '作家正在整理你的大纲…' : '作家正在规划大纲…'}
</span>
<span className="ml-auto inline-flex gap-1">
{[0, 1, 2].map((i) => (
@@ -5,6 +5,8 @@ interface WritingSkeletonCardProps {
status: WritingStatus;
/** 素材收集阶段的实时进度文案(仅 researching 阶段使用) */
researchProgress?: string;
/** 用户自带大纲时为 true:大纲阶段文案显示「整理你的大纲」而非「规划大纲」。 */
organizing?: boolean;
}
// ── 基础骨架元素 ──────────────────────────────────────────────────────────────
@@ -80,12 +82,12 @@ function MaterialsSkeleton({ researchProgress }: { researchProgress?: string })
// ── 大纲规划骨架屏 ────────────────────────────────────────────────────────────
function OutlineSkeleton() {
function OutlineSkeleton({ organizing }: { organizing?: boolean } = {}) {
return (
<div className="space-y-3 p-4">
<div className="flex items-center gap-2 text-xs text-purple-500 font-medium">
<ThinkingDots color="bg-purple-400" />
<span>作家规划大纲中…</span>
<span>{organizing ? '作家整理你的大纲中…' : '作家规划大纲中…'}</span>
</div>
{/* 文章标题占位 */}
@@ -213,7 +215,7 @@ const SKELETON_MAP: Partial<Record<WritingStatus, React.FC>> = {
awaiting_review_confirm: RevisingSkeleton, // 采纳编辑意见后
};
export function WritingSkeletonCard({ status, researchProgress }: WritingSkeletonCardProps) {
export function WritingSkeletonCard({ status, researchProgress, organizing }: WritingSkeletonCardProps) {
// 素材收集阶段单独渲染,以便透传实时进度文案
if (status === 'researching' || status === 'awaiting_material_confirm') {
return (
@@ -222,6 +224,14 @@ export function WritingSkeletonCard({ status, researchProgress }: WritingSkeleto
</div>
);
}
// 大纲阶段单独渲染,以便透传「整理 / 规划」文案(用户自带大纲走「整理」)
if (status === 'writing_outline' || status === 'awaiting_outline_confirm') {
return (
<div className="rounded-xl border bg-card shadow-sm overflow-hidden">
<OutlineSkeleton organizing={organizing} />
</div>
);
}
const Skeleton = SKELETON_MAP[status];
if (!Skeleton) return null;
return (