feat: refine ai writing search flow
This commit is contained in:
1 parent
025e43f4c5
commit
d21dbeb214
15 files changed
+260
-113
No files matched your search
@@ -47,8 +47,8 @@ const MAX_INTENT_LENGTH = 500;
|
||||
const MAX_SAMPLE_TEXT_LENGTH = 120000;
|
||||
|
||||
const MATERIAL_SOURCE_OPTIONS: Array<{ value: MaterialSource; label: string; desc: string }> = [
|
||||
{ value: 'general', label: '通用检索', desc: '用素材收集专家配置的技能检索(默认:知识库检索)' },
|
||||
{ value: 'knowledge_base', label: '知识库检索', desc: '直连内网知识库检索接口,不走技能编排' },
|
||||
{ value: 'general', label: '通用检索', desc: '素材收集专家按配置技能检索' },
|
||||
{ value: 'knowledge_base', label: '知识库检索', desc: '直连内网知识库检索接口' },
|
||||
{ value: 'notebook', label: '我的空间', desc: '从空间中检索素材' },
|
||||
];
|
||||
|
||||
@@ -162,7 +162,7 @@ export function AIWritingForm({
|
||||
|
||||
function handleMaterialSourceChange(val: MaterialSource) {
|
||||
setMaterialSource(val);
|
||||
// studioToken 模式下空间和文档已锁定,切换素材来源时不清空
|
||||
// studioToken 模式下空间和文档已锁定,切换素材来源时不清空。
|
||||
if (val !== 'notebook' && !studioToken) {
|
||||
setSelectedNotebookId(null);
|
||||
setSelectedDocumentIds([]);
|
||||
@@ -222,12 +222,10 @@ export function AIWritingForm({
|
||||
writingMode: skipResearch ? 'loose' : hideAdvancedFields ? 'loose' : strictMode ? 'strict' : 'loose',
|
||||
addReferences,
|
||||
skipResearch,
|
||||
// 跳过素材收集时检索类型无意义,归一为 general
|
||||
materialSource: skipResearch ? 'general' : materialSource,
|
||||
modelName: selectedModel?.name || modelName || undefined,
|
||||
// 只有选了"我的空间"且未跳过素材收集才传笔记参数
|
||||
noteSearchSpaceId: !skipResearch && materialSource === 'notebook' ? (selectedNotebookId ?? undefined) : undefined,
|
||||
noteSourceIds: !skipResearch && materialSource === 'notebook' && selectedNotebookId != null ? selectedDocumentIds : undefined,
|
||||
noteSearchSpaceId: !skipResearch ? (selectedNotebookId ?? undefined) : undefined,
|
||||
noteSourceIds: !skipResearch && selectedNotebookId != null ? selectedDocumentIds : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -454,82 +452,80 @@ export function AIWritingForm({
|
||||
</div>
|
||||
|
||||
{!skipResearch && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<label className={FIELD_LABEL_CLASS}>检索类型</label>
|
||||
<Select
|
||||
value={materialSource}
|
||||
onValueChange={(v) => handleMaterialSourceChange(v as MaterialSource)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className={FIELD_TRIGGER_CLASS}>
|
||||
<SelectValue placeholder="选择检索类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MATERIAL_SOURCE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value} className="text-sm">
|
||||
{opt.label}
|
||||
<span className="ml-1 text-xs text-muted-foreground">— {opt.desc}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="mt-4 space-y-2">
|
||||
<label className={FIELD_LABEL_CLASS}>检索方式</label>
|
||||
<Select
|
||||
value={materialSource}
|
||||
onValueChange={(v) => handleMaterialSourceChange(v as MaterialSource)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className={FIELD_TRIGGER_CLASS}>
|
||||
<SelectValue placeholder="选择检索方式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MATERIAL_SOURCE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value} className="text-sm">
|
||||
{opt.label}
|
||||
<span className="ml-1 text-xs text-muted-foreground">— {opt.desc}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 选了"我的空间"才展示空间 / 文档选择器 */}
|
||||
{materialSource === 'notebook' && (
|
||||
<div className="mt-2 space-y-2">
|
||||
{/* studioToken 模式:空间已锁定,不展示选择器 */}
|
||||
{!studioToken && (
|
||||
<Select
|
||||
value={selectedNotebookId !== null ? String(selectedNotebookId) : ''}
|
||||
onValueChange={handleNotebookChange}
|
||||
disabled={disabled || notebooksLoading}
|
||||
>
|
||||
<SelectTrigger className={FIELD_TRIGGER_CLASS}>
|
||||
<SelectValue placeholder={notebooksLoading ? '加载中…' : '选择空间'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{notebooks.map((nb) => (
|
||||
<SelectItem key={nb.id} value={String(nb.id)} className="text-sm">
|
||||
{nb.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{materialSource === 'notebook' && (
|
||||
<div className="mt-2 space-y-2">
|
||||
{!studioToken && (
|
||||
<Select
|
||||
value={selectedNotebookId !== null ? String(selectedNotebookId) : ''}
|
||||
onValueChange={handleNotebookChange}
|
||||
disabled={disabled || notebooksLoading}
|
||||
>
|
||||
<SelectTrigger className={FIELD_TRIGGER_CLASS}>
|
||||
<SelectValue placeholder={notebooksLoading ? '加载中…' : '选择空间'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{notebooks.map((nb) => (
|
||||
<SelectItem key={nb.id} value={String(nb.id)} className="text-sm">
|
||||
{nb.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{selectedNotebookId !== null && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{sourcesLoading
|
||||
? '文档加载中…'
|
||||
: `选择文档(可多选;不选则搜整个空间${selectedDocumentIds.length > 0 ? `,已选 ${selectedDocumentIds.length} 篇` : ''})`}
|
||||
</p>
|
||||
{!sourcesLoading && (
|
||||
<div className="max-h-36 overflow-y-auto rounded-lg border border-[#e8dedd] bg-white divide-y divide-[#f0e8e7]">
|
||||
{sources.filter((s) => s.document?.id != null).map((s) => {
|
||||
const docId = s.document!.id;
|
||||
return (
|
||||
<label key={s.id} className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-[#f9f5f5] select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDocumentIds.includes(docId)}
|
||||
onChange={() => toggleDocument(docId)}
|
||||
disabled={disabled}
|
||||
className="size-3.5 shrink-0 accent-primary"
|
||||
/>
|
||||
<span className="text-xs text-foreground truncate">
|
||||
{s.document?.title ?? `文档 ${docId}`}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedNotebookId !== null && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{sourcesLoading
|
||||
? '文档加载中…'
|
||||
: `选择文档(可多选;不选则搜整个空间${selectedDocumentIds.length > 0 ? `,已选 ${selectedDocumentIds.length} 篇` : ''})`}
|
||||
</p>
|
||||
{!sourcesLoading && (
|
||||
<div className="max-h-36 overflow-y-auto rounded-lg border border-[#e8dedd] bg-white divide-y divide-[#f0e8e7]">
|
||||
{sources.filter((s) => s.document?.id != null).map((s) => {
|
||||
const docId = s.document!.id;
|
||||
return (
|
||||
<label key={s.id} className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-[#f9f5f5] select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDocumentIds.includes(docId)}
|
||||
onChange={() => toggleDocument(docId)}
|
||||
disabled={disabled}
|
||||
className="size-3.5 shrink-0 accent-primary"
|
||||
/>
|
||||
<span className="text-xs text-foreground truncate">
|
||||
{s.document?.title ?? `文档 ${docId}`}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 space-y-2">
|
||||
|
||||
@@ -605,6 +605,8 @@ export function AIWritingPanel({
|
||||
wordCountTarget={state.request?.wordCountTarget}
|
||||
revisionCount={state.revisionCount}
|
||||
skillSearchSteps={state.skillSearchSteps}
|
||||
materialSource={state.request?.materialSource ?? 'general'}
|
||||
canUseNotebookSearch={state.request?.noteSearchSpaceId != null}
|
||||
skillSearchInProgress={state.status === 'researching' && state.skillSearchSteps.length > 0}
|
||||
onSubmit={submitIntervention}
|
||||
disabled={false}
|
||||
|
||||
@@ -5,6 +5,13 @@ import {
|
||||
Trash2, BookOpen,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type {
|
||||
AIMaterial, AIOutline, ReviewResult, ReviewIssue,
|
||||
@@ -30,6 +37,8 @@ interface InterventionCardProps {
|
||||
wordCountTarget?: number;
|
||||
revisionCount: number;
|
||||
skillSearchSteps?: SkillSearchStep[];
|
||||
materialSource?: MaterialSource;
|
||||
canUseNotebookSearch?: boolean;
|
||||
/** 是否正在检索中(researching/awaiting 切换之间,前端用于流式展示步骤进行中) */
|
||||
skillSearchInProgress?: boolean;
|
||||
onSubmit: (payload: UserInterventionPayload) => void;
|
||||
@@ -38,7 +47,8 @@ interface InterventionCardProps {
|
||||
|
||||
export function InterventionCard({
|
||||
pauseEvent, materials, outline, reviewResult,
|
||||
draftTitle, draftWordCount, wordCountTarget, revisionCount, skillSearchSteps, skillSearchInProgress, onSubmit, disabled,
|
||||
draftTitle, draftWordCount, wordCountTarget, revisionCount, skillSearchSteps,
|
||||
materialSource, canUseNotebookSearch, skillSearchInProgress, onSubmit, disabled,
|
||||
}: InterventionCardProps) {
|
||||
const pausePoint = pauseEvent.pausePoint;
|
||||
|
||||
@@ -47,6 +57,8 @@ export function InterventionCard({
|
||||
<MaterialConfirmCard
|
||||
materials={materials}
|
||||
skillSearchSteps={skillSearchSteps ?? []}
|
||||
initialMaterialSource={materialSource ?? 'general'}
|
||||
canUseNotebookSearch={canUseNotebookSearch}
|
||||
skillSearchInProgress={skillSearchInProgress}
|
||||
onSubmit={onSubmit}
|
||||
disabled={disabled}
|
||||
@@ -126,17 +138,73 @@ function formatSelectedIssues(issues: ReviewIssue[], selectedIndices: Set<number
|
||||
interface MaterialConfirmCardProps {
|
||||
materials: AIMaterial[];
|
||||
skillSearchSteps: SkillSearchStep[];
|
||||
initialMaterialSource: MaterialSource;
|
||||
canUseNotebookSearch?: boolean;
|
||||
skillSearchInProgress?: boolean;
|
||||
onSubmit: (payload: UserInterventionPayload) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function MaterialConfirmCard({ materials, skillSearchSteps, skillSearchInProgress, onSubmit, disabled }: MaterialConfirmCardProps) {
|
||||
type MaterialSource = NonNullable<UserInterventionPayload['materialSource']>;
|
||||
|
||||
const MATERIAL_SOURCE_OPTIONS: Array<{ value: MaterialSource; label: string; desc: string }> = [
|
||||
{ value: 'general', label: '通用检索', desc: '素材收集专家按配置技能检索' },
|
||||
{ value: 'knowledge_base', label: '知识库检索', desc: '直连内网知识库接口' },
|
||||
{ value: 'notebook', label: '我的空间', desc: '检索当前笔记空间' },
|
||||
];
|
||||
|
||||
function MaterialSourceSwitcher({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
disabled,
|
||||
}: {
|
||||
value: MaterialSource;
|
||||
onChange: (value: MaterialSource) => void;
|
||||
options: Array<{ value: MaterialSource; label: string; desc: string }>;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const current = options.find((opt) => opt.value === value) ?? options[0];
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2 rounded-md border border-input bg-muted/30 px-2.5 py-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">检索方式</span>
|
||||
<Select value={current.value} onValueChange={(v) => onChange(v as MaterialSource)} disabled={disabled}>
|
||||
<SelectTrigger className="h-7 w-[9rem] bg-background px-2 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="min-w-0 flex-1 text-xs text-muted-foreground">
|
||||
{current.desc}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MaterialConfirmCard({
|
||||
materials,
|
||||
skillSearchSteps,
|
||||
initialMaterialSource,
|
||||
canUseNotebookSearch,
|
||||
skillSearchInProgress,
|
||||
onSubmit,
|
||||
disabled,
|
||||
}: MaterialConfirmCardProps) {
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(
|
||||
() => new Set(materials.map((m) => m.id)),
|
||||
);
|
||||
const [userQuery, setUserQuery] = useState('');
|
||||
const [materialSource, setMaterialSource] = useState<MaterialSource>(
|
||||
initialMaterialSource === 'notebook' && !canUseNotebookSearch ? 'general' : initialMaterialSource,
|
||||
);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const sourceOptions = MATERIAL_SOURCE_OPTIONS.filter((opt) => opt.value !== 'notebook' || canUseNotebookSearch);
|
||||
|
||||
async function handleConfirm() {
|
||||
setSubmitting(true);
|
||||
@@ -148,7 +216,12 @@ function MaterialConfirmCard({ materials, skillSearchSteps, skillSearchInProgres
|
||||
setSubmitting(true);
|
||||
// 带输入 = 后端在素材收集专家配置的技能中匹配检索、新素材追加到列表;
|
||||
// 无输入 = 重新生成检索词、覆盖式重检
|
||||
await onSubmit({ action: 're_search', approvedMaterialIds: [], userQuery: userQuery.trim() || undefined });
|
||||
await onSubmit({
|
||||
action: 're_search',
|
||||
approvedMaterialIds: [],
|
||||
userQuery: userQuery.trim() || undefined,
|
||||
materialSource,
|
||||
});
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -163,7 +236,7 @@ function MaterialConfirmCard({ materials, skillSearchSteps, skillSearchInProgres
|
||||
<div>
|
||||
<p className="text-xs font-medium">未检索到任何素材</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
素材收集专家本次没有检索到相关数据。你可以在输入框描述想检索的内容(自动匹配已配置的技能),或在无素材的情况下直接继续写作。
|
||||
素材收集专家本次没有检索到相关数据。你可以切换检索方式并描述想检索的内容,或在无素材的情况下直接继续写作。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,6 +245,13 @@ function MaterialConfirmCard({ materials, skillSearchSteps, skillSearchInProgres
|
||||
<SkillStepsCard steps={skillSearchSteps} inProgress={skillSearchInProgress} />
|
||||
)}
|
||||
|
||||
<MaterialSourceSwitcher
|
||||
value={materialSource}
|
||||
onChange={setMaterialSource}
|
||||
options={sourceOptions}
|
||||
disabled={disabled || submitting}
|
||||
/>
|
||||
|
||||
<MaterialsToolbar
|
||||
materials={materials}
|
||||
selectedIds={selectedIds}
|
||||
@@ -211,7 +291,7 @@ function MaterialConfirmCard({ materials, skillSearchSteps, skillSearchInProgres
|
||||
<p className="text-xs font-medium">需要您确认素材,才能继续</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
已搜集到 {materials.length} 条素材(默认全选)。您可以取消勾选不相关的素材,
|
||||
或在下方描述想补充检索的内容——系统会匹配素材收集专家配置的技能检索,并把新素材追加进列表。
|
||||
或在下方切换检索方式并描述想补充检索的内容,系统会把新素材追加进列表。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -226,6 +306,13 @@ function MaterialConfirmCard({ materials, skillSearchSteps, skillSearchInProgres
|
||||
});
|
||||
}} />
|
||||
|
||||
<MaterialSourceSwitcher
|
||||
value={materialSource}
|
||||
onChange={setMaterialSource}
|
||||
options={sourceOptions}
|
||||
disabled={disabled || submitting}
|
||||
/>
|
||||
|
||||
<MaterialsToolbar
|
||||
materials={materials}
|
||||
selectedIds={selectedIds}
|
||||
|
||||
@@ -65,7 +65,7 @@ export function MaterialsToolbar({
|
||||
{/* 单输入框 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
补充检索(可选)— 描述想检索的内容,自动匹配已配置的技能,新素材追加到列表
|
||||
补充检索(可选)— 选择检索方式后描述想检索的内容,新素材追加到列表
|
||||
</label>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
|
||||
@@ -269,6 +269,7 @@ export function WritingSetupChat({
|
||||
sampleFilename: writingModeType === 'imitate' ? formCtx?.sampleFilename || '粘贴文本' : undefined,
|
||||
imitateStrength: writingModeType === 'imitate' ? formCtx?.imitateStrength ?? 'medium' : undefined,
|
||||
preserveSampleStructure: writingModeType === 'imitate' ? formCtx?.preserveSampleStructure ?? true : undefined,
|
||||
skipIntentRecognition: true,
|
||||
articleType: payload.article_type?.trim() || formCtx?.articleType || 'analysis',
|
||||
// 新(列表外)文章类型的一句话特征,仅本次写作临时使用(命中已有类型时为空)。
|
||||
articleTypeDetail: payload.article_type_detail?.trim() || undefined,
|
||||
@@ -283,9 +284,9 @@ export function WritingSetupChat({
|
||||
materialSource,
|
||||
modelName: chatModel || formCtx?.modelName || undefined,
|
||||
noteSearchSpaceId:
|
||||
materialSource === 'notebook' ? (formCtx?.selectedNotebookId ?? undefined) : undefined,
|
||||
!skipResearch ? (formCtx?.selectedNotebookId ?? undefined) : undefined,
|
||||
noteSourceIds:
|
||||
materialSource === 'notebook' && formCtx?.selectedNotebookId != null
|
||||
!skipResearch && formCtx?.selectedNotebookId != null
|
||||
? formCtx.selectedDocumentIds
|
||||
: undefined,
|
||||
};
|
||||
|
||||
@@ -426,16 +426,13 @@ export function AIWritingProvider({ children }: { children: ReactNode }) {
|
||||
const stream = streamRef.current;
|
||||
if (!stream) return;
|
||||
try {
|
||||
// 所见即所得:用户在表单选了什么 materialSource,前端就原样传过去。
|
||||
// 两个合法值:general(通用检索,按素材收集专家配置的技能编排)/
|
||||
// notebook(我的空间);旧值 knowledge_base / skill 后端按 general 兼容。
|
||||
// 与 material_source 独立的 notebook_search_space_id / notebook_source_ids
|
||||
// 控制是否真的去笔记后端拉素材(仅当用户选了"我的空间"模式时由表单填充)。
|
||||
// 默认是 general(通用检索),但用户可以在开写前或素材确认后手动切换检索方式。
|
||||
await stream.submit(
|
||||
{
|
||||
user_intent: request.userIntent,
|
||||
// 用户自带大纲原文(留空则后端走自动规划)
|
||||
user_outline_raw: request.userOutline?.trim() || '',
|
||||
skip_intent_parser: request.skipIntentRecognition ?? false,
|
||||
writing_mode_type: request.writingModeType === 'imitate' ? 'imitate' : 'normal',
|
||||
sample_text: request.sampleText?.trim() || '',
|
||||
sample_filename: request.sampleFilename?.trim() || '',
|
||||
@@ -547,6 +544,7 @@ export function AIWritingProvider({ children }: { children: ReactNode }) {
|
||||
if (payload.approvedMaterialIds !== undefined) resumePayload.approvedMaterialIds = payload.approvedMaterialIds;
|
||||
if (payload.extraKeywords !== undefined) resumePayload.extraKeywords = payload.extraKeywords;
|
||||
if (payload.userQuery !== undefined) resumePayload.userQuery = payload.userQuery;
|
||||
if (payload.materialSource !== undefined) resumePayload.materialSource = payload.materialSource;
|
||||
if (payload.editedOutline !== undefined) resumePayload.editedOutline = payload.editedOutline;
|
||||
if (payload.outlineFeedback !== undefined) resumePayload.outlineFeedback = payload.outlineFeedback;
|
||||
if (payload.userRevisionNotes !== undefined) resumePayload.userRevisionNotes = payload.userRevisionNotes;
|
||||
@@ -562,7 +560,7 @@ export function AIWritingProvider({ children }: { children: ReactNode }) {
|
||||
seenInterruptRef.current = intr.id ?? JSON.stringify(intr.value);
|
||||
}
|
||||
|
||||
dispatch({ type: 'intervention_submitted', record, nextStatus, pendingRevisionContext });
|
||||
dispatch({ type: 'intervention_submitted', record, nextStatus, pendingRevisionContext, materialSource: payload.materialSource });
|
||||
|
||||
try {
|
||||
// resume 是新 invocation,其 config 默认不带最初的 model_name;而写作/审核节点
|
||||
|
||||
@@ -120,12 +120,6 @@ export function WritingFormProvider({
|
||||
if (payload.target_audience?.trim()) setAudience(payload.target_audience.trim());
|
||||
if (payload.writing_mode === 'strict') setStrictMode(true);
|
||||
else if (payload.writing_mode === 'loose') setStrictMode(false);
|
||||
// knowledge_base=知识库检索(直连内网 ES)保留;旧值 skill 归一为通用检索 general
|
||||
if (payload.material_source === 'knowledge_base') {
|
||||
setMaterialSource('knowledge_base');
|
||||
} else if (payload.material_source === 'skill' || payload.material_source === 'general') {
|
||||
setMaterialSource('general');
|
||||
}
|
||||
if (payload.outline?.trim()) {
|
||||
setOutlineText(payload.outline);
|
||||
setShowOutline(true);
|
||||
|
||||
@@ -85,7 +85,7 @@ export type Action =
|
||||
| { type: 'handle_batch'; events: ProgressEvent[] }
|
||||
| { type: 'start'; request: AIWritingRequest }
|
||||
| { type: 'set_session'; sessionId: string }
|
||||
| { type: 'intervention_submitted'; record: CompletedIntervention; nextStatus: WritingStatus; pendingRevisionContext: AIWritingState['pendingRevisionContext'] }
|
||||
| { type: 'intervention_submitted'; record: CompletedIntervention; nextStatus: WritingStatus; pendingRevisionContext: AIWritingState['pendingRevisionContext']; materialSource?: AIWritingRequest['materialSource'] }
|
||||
| { type: 'pause' }
|
||||
| { type: 'resume_ui' }
|
||||
// Restore history: rebuild state from the graph checkpoint values snapshot.
|
||||
@@ -132,6 +132,9 @@ export function reducer(prev: AIWritingState, action: Action): AIWritingState {
|
||||
...(ns === 'reviewing' ? { streamingReviewItems: [] } : {}),
|
||||
// 进入「修改」:右侧画布恢复初始空占位(isRevising),不再显示旧稿/流式重写
|
||||
...(ns === 'revising' ? { streamingDraftSections: [], streamingDraftTitle: '', isRevising: true } : {}),
|
||||
...(action.materialSource && prev.request
|
||||
? { request: { ...prev.request, materialSource: action.materialSource } }
|
||||
: {}),
|
||||
completedInterventions: [...prev.completedInterventions, action.record],
|
||||
...(action.pendingRevisionContext ? { pendingRevisionContext: action.pendingRevisionContext } : {}),
|
||||
};
|
||||
|
||||
@@ -534,7 +534,6 @@ export default function AIWritingPage({
|
||||
<AIWritingHistoryProvider>
|
||||
<AIWritingProvider>
|
||||
<WritingFormProvider
|
||||
initialMaterialSource={studioToken ?? initialNotebookId != null ? 'notebook' : undefined}
|
||||
initialNotebookId={initialNotebookId}
|
||||
initialDocumentIds={initialDocumentIds}
|
||||
>
|
||||
|
||||
@@ -258,6 +258,8 @@ export interface UserInterventionPayload {
|
||||
extraKeywords?: string[];
|
||||
/** PAUSE-1 素材确认:用户单输入框写的自然语言(想补充检索的内容)。后端在素材收集专家配置的技能中匹配最相关的技能检索,新素材**追加**到列表。 */
|
||||
userQuery?: string;
|
||||
/** PAUSE-1 素材确认:本次补充/重新检索使用的检索方式。 */
|
||||
materialSource?: "general" | "knowledge_base" | "notebook";
|
||||
editedOutline?: AIOutline;
|
||||
outlineFeedback?: string;
|
||||
userRevisionNotes?: string;
|
||||
@@ -281,6 +283,8 @@ export interface AIWritingRequest {
|
||||
sampleFilename?: string;
|
||||
imitateStrength?: "light" | "medium" | "strong";
|
||||
preserveSampleStructure?: boolean;
|
||||
/** 从初始化对话确认后进入正式写作时,意图字段已经结构化,可跳过后端二次识别。 */
|
||||
skipIntentRecognition?: boolean;
|
||||
articleType: string;
|
||||
/** 当 articleType 是已有类型库里没有的新类型时,配置助手识别的一句话特征说明(仅本次写作临时使用,不入库)。 */
|
||||
articleTypeDetail?: string;
|
||||
|
||||
Reference in new issue
Block a user