feat(ai-writing): 大纲先行确认 + 新文章类型识别 + 参考文献 + 编辑器修复
- 写作配置 Q&A 改为「先流式生成大纲并多轮确认 → 再确认意图」,放宽轮数上限 - 意图识别出列表外的新文章类型时带上类型名+简介(仅本次写作临时使用) - 成稿支持参考文献:正文 [n] 角标 + 文末按出现顺序的「参考文献」列表(表单开关,默认开) - 流式接取未闭合 <think> 思考内容,首 token 前加「正在整理…」占位 - 修复右侧编辑器输入空格只出一次、光标跳字(避免自激回灌 setContent 重置光标) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
cd33aaeded
commit
b8c78ba06e
26 files changed
+1273
-352
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||
import { CheckIcon, SendIcon, XIcon } from "lucide-react";
|
||||
import { CheckIcon, PenLineIcon, SendIcon, XIcon } from "lucide-react";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useStickToBottomContext } from "use-stick-to-bottom";
|
||||
|
||||
@@ -20,10 +20,10 @@ import {
|
||||
groupMessages,
|
||||
hasContent,
|
||||
hasPresentFiles,
|
||||
hasReasoning,
|
||||
hasToolCalls,
|
||||
isHiddenFromUIMessage,
|
||||
} from "@/core/messages/utils";
|
||||
hasReasoning,
|
||||
hasToolCalls,
|
||||
isHiddenFromUIMessage,
|
||||
} from "@/core/messages/utils";
|
||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||
import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
@@ -103,6 +103,7 @@ export function MessageList({
|
||||
onSetupAgentReject,
|
||||
onSetupWritingApprove,
|
||||
onSetupWritingReject,
|
||||
onSetupWritingStart,
|
||||
}: {
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
@@ -120,6 +121,8 @@ export function MessageList({
|
||||
onSetupAgentReject?: (payload: SetupAgentApprovalPayload) => void | Promise<void>;
|
||||
onSetupWritingApprove?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
|
||||
onSetupWritingReject?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
|
||||
/** 审批卡「直接开始写作」回调:应用配置(含已确认大纲)并立即启动写作流程(仅写作页提供,模板编辑等场景缺省隐藏该按钮)。 */
|
||||
onSetupWritingStart?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
|
||||
}) {
|
||||
const baseRehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
|
||||
const rehypePlugins = useMemo(
|
||||
@@ -152,10 +155,10 @@ export function MessageList({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const allMessages = useMemo(
|
||||
() => thread.messages.filter((m) => !isHiddenFromUIMessage(m)),
|
||||
[thread.messages],
|
||||
);
|
||||
const allMessages = useMemo(
|
||||
() => thread.messages.filter((m) => !isHiddenFromUIMessage(m)),
|
||||
[thread.messages],
|
||||
);
|
||||
const messages = useMemo(
|
||||
() =>
|
||||
hiddenMessageIds.size === 0
|
||||
@@ -430,6 +433,7 @@ export function MessageList({
|
||||
message={message}
|
||||
onApprove={onSetupWritingApprove}
|
||||
onReject={onSetupWritingReject}
|
||||
onStart={onSetupWritingStart}
|
||||
/>
|
||||
<MessageTokenUsageList
|
||||
enabled={tokenUsageEnabled}
|
||||
@@ -629,6 +633,8 @@ export type SetupAgentApprovalPayload = {
|
||||
export type SetupWritingApprovalPayload = {
|
||||
user_intent?: string;
|
||||
article_type?: string;
|
||||
/** 当 article_type 是已有类型库里没有的新类型时,模型识别出的一句话特征说明(命中已有类型时为空)。 */
|
||||
article_type_detail?: string;
|
||||
word_count_target?: number | null;
|
||||
target_audience?: string;
|
||||
writing_mode?: string;
|
||||
@@ -820,14 +826,16 @@ function SetupWritingApprovalCard({
|
||||
message,
|
||||
onApprove,
|
||||
onReject,
|
||||
onStart,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
message: unknown;
|
||||
onApprove?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
|
||||
onReject?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
|
||||
onStart?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
|
||||
}) {
|
||||
const payload = getSetupWritingApprovalPayload(message);
|
||||
const [submitting, setSubmitting] = useState<"approve" | "reject" | null>(
|
||||
const [submitting, setSubmitting] = useState<"approve" | "reject" | "start" | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -838,7 +846,7 @@ function SetupWritingApprovalCard({
|
||||
const approvalPayload = payload;
|
||||
const busy = disabled || submitting !== null;
|
||||
|
||||
async function run(action: "approve" | "reject") {
|
||||
async function run(action: "approve" | "reject" | "start") {
|
||||
if (busy) {
|
||||
return;
|
||||
}
|
||||
@@ -846,6 +854,8 @@ function SetupWritingApprovalCard({
|
||||
try {
|
||||
if (action === "approve") {
|
||||
await onApprove?.(approvalPayload);
|
||||
} else if (action === "start") {
|
||||
await onStart?.(approvalPayload);
|
||||
} else {
|
||||
await onReject?.(approvalPayload);
|
||||
}
|
||||
@@ -854,9 +864,14 @@ function SetupWritingApprovalCard({
|
||||
}
|
||||
}
|
||||
|
||||
const hasNewType = !!approvalPayload.article_type_detail?.trim();
|
||||
const rows: Array<{ label: string; value: string }> = [];
|
||||
if (approvalPayload.article_type) {
|
||||
rows.push({ label: "文章类型", value: approvalPayload.article_type });
|
||||
rows.push({
|
||||
label: "文章类型",
|
||||
// 新类型(列表外)标注「· 新」,让用户知道这是模型新识别的类型。
|
||||
value: hasNewType ? `${approvalPayload.article_type} · 新` : approvalPayload.article_type,
|
||||
});
|
||||
}
|
||||
if (typeof approvalPayload.word_count_target === "number") {
|
||||
rows.push({ label: "目标字数", value: `${approvalPayload.word_count_target} 字` });
|
||||
@@ -903,6 +918,13 @@ function SetupWritingApprovalCard({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{hasNewType ? (
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground text-xs">新类型说明</div>
|
||||
<p className="text-sm leading-6">{approvalPayload.article_type_detail}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{approvalPayload.outline ? (
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground text-xs">已有大纲</div>
|
||||
@@ -926,6 +948,7 @@ function SetupWritingApprovalCard({
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={onStart ? "outline" : "default"}
|
||||
size="sm"
|
||||
disabled={busy || !onApprove}
|
||||
onClick={() => void run("approve")}
|
||||
@@ -933,6 +956,17 @@ function SetupWritingApprovalCard({
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
应用到表单
|
||||
</Button>
|
||||
{onStart ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => void run("start")}
|
||||
>
|
||||
<PenLineIcon className="h-4 w-4" />
|
||||
直接开始写作
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user