Step3 总结报告支持随时插话、暂停与附件上传
生成中可打断并续问,输入框支持上传参考文件并随消息注入总结线程。
This commit is contained in:
1 parent
2d74f8883e
commit
b682ff5d08
2 files changed
+192
-46
No files matched your search
@@ -21,6 +21,8 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
PanelLeftOpen,
|
PanelLeftOpen,
|
||||||
|
Paperclip,
|
||||||
|
Pause,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
SendHorizontal,
|
SendHorizontal,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
@@ -113,11 +115,12 @@ export function Step3SummaryMode({
|
|||||||
dialogues,
|
dialogues,
|
||||||
seatStubMessages,
|
seatStubMessages,
|
||||||
activeArtifactThreadId,
|
activeArtifactThreadId,
|
||||||
canAsk,
|
busy,
|
||||||
start,
|
start,
|
||||||
hydrateSaved,
|
hydrateSaved,
|
||||||
redraw,
|
redraw,
|
||||||
ask,
|
ask,
|
||||||
|
pause,
|
||||||
abort,
|
abort,
|
||||||
openArtifactFromStep,
|
openArtifactFromStep,
|
||||||
openArtifactPreview,
|
openArtifactPreview,
|
||||||
@@ -303,9 +306,10 @@ export function Step3SummaryMode({
|
|||||||
phase={phase}
|
phase={phase}
|
||||||
dialogues={dialogues}
|
dialogues={dialogues}
|
||||||
error={error}
|
error={error}
|
||||||
canAsk={canAsk}
|
busy={busy}
|
||||||
onRetry={redraw}
|
onRetry={redraw}
|
||||||
onAsk={ask}
|
onAsk={ask}
|
||||||
|
onPause={pause}
|
||||||
onArtifactStepClick={openArtifactFromStep}
|
onArtifactStepClick={openArtifactFromStep}
|
||||||
onArtifactPreview={openArtifactPreview}
|
onArtifactPreview={openArtifactPreview}
|
||||||
selectedModel={selectedModel}
|
selectedModel={selectedModel}
|
||||||
@@ -422,9 +426,10 @@ function AgentProgressPanel({
|
|||||||
phase,
|
phase,
|
||||||
dialogues,
|
dialogues,
|
||||||
error,
|
error,
|
||||||
canAsk,
|
busy,
|
||||||
onRetry,
|
onRetry,
|
||||||
onAsk,
|
onAsk,
|
||||||
|
onPause,
|
||||||
onArtifactStepClick,
|
onArtifactStepClick,
|
||||||
onArtifactPreview,
|
onArtifactPreview,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
@@ -435,9 +440,10 @@ function AgentProgressPanel({
|
|||||||
phase: ReturnType<typeof useStep3Summary>["phase"];
|
phase: ReturnType<typeof useStep3Summary>["phase"];
|
||||||
dialogues: ReturnType<typeof useStep3Summary>["dialogues"];
|
dialogues: ReturnType<typeof useStep3Summary>["dialogues"];
|
||||||
error: string | null;
|
error: string | null;
|
||||||
canAsk: boolean;
|
busy: boolean;
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
onAsk: (message: string) => void;
|
onAsk: (message: string, files?: File[]) => void;
|
||||||
|
onPause: () => void;
|
||||||
onArtifactStepClick: ReturnType<typeof useStep3Summary>["openArtifactFromStep"];
|
onArtifactStepClick: ReturnType<typeof useStep3Summary>["openArtifactFromStep"];
|
||||||
onArtifactPreview: ReturnType<typeof useStep3Summary>["openArtifactPreview"];
|
onArtifactPreview: ReturnType<typeof useStep3Summary>["openArtifactPreview"];
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
@@ -445,17 +451,33 @@ function AgentProgressPanel({
|
|||||||
modelsLoading: boolean;
|
modelsLoading: boolean;
|
||||||
setSelectedModel: (model: string) => void;
|
setSelectedModel: (model: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const busy = phase === "creating" || phase === "streaming";
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const { atBottom, scrollToBottom } = useAutoScroll(scrollRef, [dialogues]);
|
const { atBottom, scrollToBottom } = useAutoScroll(scrollRef, [dialogues]);
|
||||||
const [askText, setAskText] = useState("");
|
const [askText, setAskText] = useState("");
|
||||||
|
// 输入框携带的本地附件(点发送时连同指令一起上传到总结线程)。
|
||||||
|
const [askFiles, setAskFiles] = useState<File[]>([]);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const pickFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const picked = Array.from(e.target.files ?? []);
|
||||||
|
if (picked.length > 0) {
|
||||||
|
setAskFiles((prev) => {
|
||||||
|
const seen = new Set(prev.map((f) => `${f.name}:${f.size}`));
|
||||||
|
return [...prev, ...picked.filter((f) => !seen.has(`${f.name}:${f.size}`))];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
e.target.value = "";
|
||||||
|
};
|
||||||
|
const removeFile = (idx: number) => setAskFiles((prev) => prev.filter((_, i) => i !== idx));
|
||||||
|
|
||||||
const handleAskSubmit = (e: FormEvent<HTMLFormElement>) => {
|
const handleAskSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const text = askText.trim();
|
const text = askText.trim();
|
||||||
if (!text || !canAsk) return;
|
const hasFiles = askFiles.length > 0;
|
||||||
|
if (!text && !hasFiles) return; // 允许「随时」发送(含生成中);空内容不发
|
||||||
setAskText("");
|
setAskText("");
|
||||||
onAsk(text);
|
setAskFiles([]);
|
||||||
|
onAsk(text, hasFiles ? askFiles : undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -551,6 +573,31 @@ function AgentProgressPanel({
|
|||||||
</div>
|
</div>
|
||||||
{!atBottom && <ScrollToBottomButton onClick={scrollToBottom} />}
|
{!atBottom && <ScrollToBottomButton onClick={scrollToBottom} />}
|
||||||
<form onSubmit={handleAskSubmit} className="shrink-0 border-t border-slate-100 bg-white px-3 py-3">
|
<form onSubmit={handleAskSubmit} className="shrink-0 border-t border-slate-100 bg-white px-3 py-3">
|
||||||
|
{/* 已选附件预览(发送时随指令上传到总结线程,供智能体读取) */}
|
||||||
|
{askFiles.length > 0 && (
|
||||||
|
<div className="mb-2 flex flex-wrap gap-1.5">
|
||||||
|
{askFiles.map((file, idx) => (
|
||||||
|
<span
|
||||||
|
key={`${file.name}-${file.size}-${idx}`}
|
||||||
|
className="inline-flex items-center gap-1.5 bg-white border border-slate-200 rounded-lg pl-2 pr-1 py-1 text-[11px] text-slate-600 max-w-[200px]"
|
||||||
|
>
|
||||||
|
<FileText className="w-3 h-3 shrink-0 text-primary" />
|
||||||
|
<span className="truncate font-medium" title={file.name}>
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-slate-400">{formatBytes(file.size)}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeFile(idx)}
|
||||||
|
className="shrink-0 p-0.5 rounded hover:bg-slate-200 text-slate-400 hover:text-slate-600 transition"
|
||||||
|
title="移除"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-end gap-2 rounded-2xl border border-slate-200 bg-slate-50/70 px-3 py-2 focus-within:border-primary/60 focus-within:bg-white focus-within:ring-2 focus-within:ring-[var(--brand-200)]/40">
|
<div className="flex items-end gap-2 rounded-2xl border border-slate-200 bg-slate-50/70 px-3 py-2 focus-within:border-primary/60 focus-within:bg-white focus-within:ring-2 focus-within:ring-[var(--brand-200)]/40">
|
||||||
<textarea
|
<textarea
|
||||||
value={askText}
|
value={askText}
|
||||||
@@ -561,22 +608,46 @@ function AgentProgressPanel({
|
|||||||
e.currentTarget.form?.requestSubmit();
|
e.currentTarget.form?.requestSubmit();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!canAsk}
|
|
||||||
rows={1}
|
rows={1}
|
||||||
placeholder="继续对话:提修改意见(如「执行路线写细些」)或就报告提问"
|
placeholder={
|
||||||
className="min-h-8 max-h-28 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
busy
|
||||||
|
? "生成中也可随时插话干预:发送将打断当前轮并据此继续…"
|
||||||
|
: "继续对话:提修改意见(如「执行路线写细些」)/ 提问 / 上传参考文件"
|
||||||
|
}
|
||||||
|
className="min-h-8 max-h-28 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 上传文件(参考第一/二步:本地选取,发送时上传到总结线程) */}
|
||||||
|
<input ref={fileInputRef} type="file" multiple className="hidden" onChange={pickFiles} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
title="上传参考文件(智能体将读取文件内容辅助总结/修改)"
|
||||||
|
className="mb-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-white border border-slate-200 text-slate-500 hover:text-primary hover:bg-slate-50 transition active:scale-95"
|
||||||
|
>
|
||||||
|
<Paperclip className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 暂停(仅生成中显示) */}
|
||||||
|
{busy && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onPause}
|
||||||
|
title="暂停当前生成"
|
||||||
|
className="mb-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-amber-100 border border-amber-200/60 text-amber-700 hover:bg-amber-200 transition active:scale-95"
|
||||||
|
>
|
||||||
|
<Pause className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 发送(随时可发:生成中发送=打断并续问) */}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!canAsk || askText.trim().length === 0}
|
disabled={askText.trim().length === 0 && askFiles.length === 0}
|
||||||
title={busy ? "正在撰写,请稍后发送" : "发送(智能体自行判断改报告 or 回答)"}
|
title={busy ? "打断当前轮并发送" : "发送(智能体自行判断改报告 or 回答)"}
|
||||||
className="mb-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-primary text-white transition hover:bg-[var(--brand-900)] disabled:cursor-not-allowed disabled:opacity-50"
|
className="mb-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-primary text-white transition hover:bg-[var(--brand-900)] disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{busy ? (
|
<SendHorizontal className="h-4 w-4" />
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<SendHorizontal className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||||||
|
|
||||||
import { useArtifacts } from "@/components/workspace/artifacts";
|
import { useArtifacts } from "@/components/workspace/artifacts";
|
||||||
import { apiFetch } from "@/core/api";
|
import { apiFetch } from "@/core/api";
|
||||||
|
import { uploadFiles } from "@/core/uploads/api";
|
||||||
import type {
|
import type {
|
||||||
DraftStep3SummarySnapshot,
|
DraftStep3SummarySnapshot,
|
||||||
} from "@/roundtable-planning/api/drafts";
|
} from "@/roundtable-planning/api/drafts";
|
||||||
@@ -25,8 +26,12 @@ import {
|
|||||||
initSummaryThread,
|
initSummaryThread,
|
||||||
listSessionArtifacts,
|
listSessionArtifacts,
|
||||||
streamMultiAgent,
|
streamMultiAgent,
|
||||||
|
type StreamMultiAgentRequest,
|
||||||
type StreamPhase,
|
type StreamPhase,
|
||||||
} from "@/roundtable-planning/api/multi-agent";
|
} from "@/roundtable-planning/api/multi-agent";
|
||||||
|
|
||||||
|
/** 一个上传文件的 run 引用(与 streamMultiAgent 的 files 入参对齐)。 */
|
||||||
|
type RunFileRef = NonNullable<StreamMultiAgentRequest["files"]>[number];
|
||||||
import type {
|
import type {
|
||||||
ReportIntent,
|
ReportIntent,
|
||||||
ReportSeatDelivery,
|
ReportSeatDelivery,
|
||||||
@@ -64,16 +69,21 @@ export interface UseStep3SummaryReturn {
|
|||||||
dialogues: Step2Dialogue[];
|
dialogues: Step2Dialogue[];
|
||||||
seatStubMessages: Record<string, StubAiMessage[]>;
|
seatStubMessages: Record<string, StubAiMessage[]>;
|
||||||
activeArtifactThreadId: string | null;
|
activeArtifactThreadId: string | null;
|
||||||
/** 是否可发送追问/修改(非流式中、且已有线程或可新建)。 */
|
/** 是否正在生成中(撰写/初始化/上传)—— 驱动暂停按钮与发送态。 */
|
||||||
canAsk: boolean;
|
busy: boolean;
|
||||||
/** 启动总结(幂等:进行中重复调用会被忽略)。 */
|
/** 启动总结(幂等:进行中重复调用会被忽略)。 */
|
||||||
start: () => void;
|
start: () => void;
|
||||||
/** 从草稿快照恢复展示(不重新生成;保留 threadId 以便继续问答)。 */
|
/** 从草稿快照恢复展示(不重新生成;保留 threadId 以便继续问答)。 */
|
||||||
hydrateSaved: (snapshot: DraftStep3SummarySnapshot) => void;
|
hydrateSaved: (snapshot: DraftStep3SummarySnapshot) => void;
|
||||||
/** 重新生成(先中断在途请求再重跑)。 */
|
/** 重新生成(先中断在途请求再重跑)。 */
|
||||||
redraw: () => void;
|
redraw: () => void;
|
||||||
/** 用户在输入框里继续对话:续同一线程,智能体自判改报告 or 问答。 */
|
/**
|
||||||
ask: (message: string) => void;
|
* 用户在输入框里继续对话 —— **随时可发**:生成中发送会先打断当前轮再续同一线程,
|
||||||
|
* 智能体自判改报告 or 问答;可携带上传文件(先传到总结线程再随消息注入)。
|
||||||
|
*/
|
||||||
|
ask: (message: string, files?: File[]) => void;
|
||||||
|
/** 暂停当前生成(中断在途请求并把当前气泡收尾为「已暂停」)。 */
|
||||||
|
pause: () => void;
|
||||||
/** 中断在途请求(卸载 / 切回普通视图时调用)。 */
|
/** 中断在途请求(卸载 / 切回普通视图时调用)。 */
|
||||||
abort: () => void;
|
abort: () => void;
|
||||||
openArtifactFromStep: (
|
openArtifactFromStep: (
|
||||||
@@ -622,6 +632,13 @@ export function useStep3Summary(
|
|||||||
runningRef.current = false;
|
runningRef.current = false;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/** 把仍在流式的气泡冻结(停转圈),并打上状态标签(打断 / 暂停时用)。 */
|
||||||
|
const freezeStreaming = useCallback((note: string) => {
|
||||||
|
setDialogues((prev) =>
|
||||||
|
prev.map((d) => (d.streaming ? { ...d, streaming: false, confidence: note } : d)),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
/** 把当前报告 md 种进某线程 stub + 开沙箱(续问时给 reconcile/预览一个锚点)。 */
|
/** 把当前报告 md 种进某线程 stub + 开沙箱(续问时给 reconcile/预览一个锚点)。 */
|
||||||
const seedStubWithCurrentMd = useCallback(
|
const seedStubWithCurrentMd = useCallback(
|
||||||
(threadId: string) => {
|
(threadId: string) => {
|
||||||
@@ -648,8 +665,9 @@ export function useStep3Summary(
|
|||||||
aborter: AbortController;
|
aborter: AbortController;
|
||||||
seedThreadId?: string | null;
|
seedThreadId?: string | null;
|
||||||
forceReconcile?: boolean;
|
forceReconcile?: boolean;
|
||||||
|
files?: RunFileRef[];
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
const { bubbleId, prompt, aborter, seedThreadId, forceReconcile } = args;
|
const { bubbleId, prompt, aborter, seedThreadId, forceReconcile, files } = args;
|
||||||
|
|
||||||
let threadId = seedThreadId ?? null;
|
let threadId = seedThreadId ?? null;
|
||||||
let agentName = SUMMARY_AGENT_NAME;
|
let agentName = SUMMARY_AGENT_NAME;
|
||||||
@@ -676,6 +694,7 @@ export function useStep3Summary(
|
|||||||
displayName: "方案总结报告",
|
displayName: "方案总结报告",
|
||||||
newMessage: prompt,
|
newMessage: prompt,
|
||||||
model: selectedModel || undefined,
|
model: selectedModel || undefined,
|
||||||
|
files: files && files.length > 0 ? files : undefined,
|
||||||
signal: aborter.signal,
|
signal: aborter.signal,
|
||||||
onTextDelta: (text) => appendToDialogue(bubbleId, text),
|
onTextDelta: (text) => appendToDialogue(bubbleId, text),
|
||||||
onPhaseChange: (p, toolName, pArgs) => {
|
onPhaseChange: (p, toolName, pArgs) => {
|
||||||
@@ -763,7 +782,8 @@ export function useStep3Summary(
|
|||||||
finalizeDialogue(bubbleId, `(总结失败) ${msg}`, "总结失败");
|
finalizeDialogue(bubbleId, `(总结失败) ${msg}`, "总结失败");
|
||||||
setPhase("error");
|
setPhase("error");
|
||||||
} finally {
|
} finally {
|
||||||
runningRef.current = false;
|
// 只有「本轮仍是当前轮」才复位 running —— 被新一轮(干预/重新生成)抢占时不要误清。
|
||||||
|
if (aborterRef.current === aborter) runningRef.current = false;
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
intentReady,
|
intentReady,
|
||||||
@@ -776,53 +796,99 @@ export function useStep3Summary(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const ask = useCallback(
|
const ask = useCallback(
|
||||||
async (message: string) => {
|
async (message: string, files?: File[]) => {
|
||||||
const text = message.trim();
|
const text = message.trim();
|
||||||
if (!text || runningRef.current) return;
|
const hasFiles = !!(files && files.length > 0);
|
||||||
|
if (!text && !hasFiles) return;
|
||||||
|
|
||||||
|
// 「随时干预」:生成中也能发——先打断当前轮(后端 multitask=rollback 抢占旧 run),
|
||||||
|
// 并把当前流式气泡冻结成「已打断」,再续上同一线程跑新一轮。
|
||||||
|
const wasBusy = runningRef.current;
|
||||||
|
abort();
|
||||||
|
if (wasBusy) freezeStreaming("已打断");
|
||||||
|
|
||||||
runningRef.current = true;
|
runningRef.current = true;
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const aborter = new AbortController();
|
const aborter = new AbortController();
|
||||||
aborterRef.current = aborter;
|
aborterRef.current = aborter;
|
||||||
|
// 续问前是否已有真线程:决定 prompt 形态——有则发原始消息(上下文在线程里),
|
||||||
appendUserDialogue(text);
|
// 没有(恢复后 / 首轮还没建好就被打断)则用带研讨原料 + 旧报告的完整 prompt。
|
||||||
const bubbleId = appendStreamingDialogue({
|
const hadLiveThread = !!liveThreadRef.current;
|
||||||
sender: "方案总结报告智能体",
|
|
||||||
avatarType: "blue",
|
|
||||||
});
|
|
||||||
setPhase("streaming");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const live = liveThreadRef.current;
|
// 有附件:先确保有线程(首轮还没建好则新建),再把文件上传到该线程。
|
||||||
if (live) {
|
let threadId = liveThreadRef.current;
|
||||||
|
if (hasFiles && !threadId) {
|
||||||
|
setPhase("creating");
|
||||||
|
const init = await initSummaryThread({ signal: aborter.signal });
|
||||||
|
if (aborter.signal.aborted) return;
|
||||||
|
threadId = init.threadId;
|
||||||
|
liveThreadRef.current = threadId;
|
||||||
|
setSummaryThreadId(threadId);
|
||||||
|
setActiveArtifactThreadId(threadId);
|
||||||
|
}
|
||||||
|
let fileRefs: RunFileRef[] | undefined;
|
||||||
|
if (hasFiles && threadId) {
|
||||||
|
setPhase("creating");
|
||||||
|
const up = await uploadFiles(threadId, files!);
|
||||||
|
if (aborter.signal.aborted) return;
|
||||||
|
fileRefs = up.files.map((f) => ({
|
||||||
|
filename: f.filename,
|
||||||
|
size: f.size,
|
||||||
|
path: f.virtual_path,
|
||||||
|
status: "uploaded" as const,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileNote = hasFiles ? `(附 ${files!.length} 个文件)` : "";
|
||||||
|
appendUserDialogue((text || "(上传了参考文件)") + fileNote);
|
||||||
|
const bubbleId = appendStreamingDialogue({
|
||||||
|
sender: "方案总结报告智能体",
|
||||||
|
avatarType: "blue",
|
||||||
|
});
|
||||||
|
setPhase("streaming");
|
||||||
|
|
||||||
|
if (hadLiveThread) {
|
||||||
// 真线程仍在 → 续问,发原始消息,靠线程上下文 + SOUL 自判改/答。
|
// 真线程仍在 → 续问,发原始消息,靠线程上下文 + SOUL 自判改/答。
|
||||||
|
const prompt =
|
||||||
|
text || "我上传了参考文件,请先阅读文件内容,再据此更新报告或回答。";
|
||||||
await streamOnce({
|
await streamOnce({
|
||||||
bubbleId,
|
bubbleId,
|
||||||
prompt: text,
|
prompt,
|
||||||
aborter,
|
aborter,
|
||||||
seedThreadId: live,
|
seedThreadId: liveThreadRef.current,
|
||||||
forceReconcile: true,
|
forceReconcile: true,
|
||||||
|
files: fileRefs,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 恢复草稿后无 live thread → 新建线程,prompt 带上旧报告做底。
|
// 无 live thread(恢复草稿 / 首轮没建好就被打断)→ prompt 带研讨原料 + 旧报告做底。
|
||||||
|
// 若上面为上传文件已建好线程(threadId)就复用它,否则交给 streamOnce 新建。
|
||||||
const prompt = buildResumeAskPrompt({
|
const prompt = buildResumeAskPrompt({
|
||||||
intentReady,
|
intentReady,
|
||||||
seatDeliveries,
|
seatDeliveries,
|
||||||
consensusText,
|
consensusText,
|
||||||
coordinatorPrompt,
|
coordinatorPrompt,
|
||||||
previousMd: currentMdRef.current,
|
previousMd: currentMdRef.current,
|
||||||
userMessage: text,
|
userMessage:
|
||||||
|
text || "请阅读我上传的参考文件后,据此更新报告或回答。",
|
||||||
|
});
|
||||||
|
await streamOnce({
|
||||||
|
bubbleId,
|
||||||
|
prompt,
|
||||||
|
aborter,
|
||||||
|
seedThreadId: threadId,
|
||||||
|
forceReconcile: true,
|
||||||
|
files: fileRefs,
|
||||||
});
|
});
|
||||||
await streamOnce({ bubbleId, prompt, aborter, forceReconcile: true });
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (aborter.signal.aborted) return;
|
if (aborter.signal.aborted) return;
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
setError(msg);
|
setError(msg);
|
||||||
finalizeDialogue(bubbleId, `(处理失败) ${msg}`, "处理失败");
|
freezeStreaming("处理失败");
|
||||||
setPhase("error");
|
setPhase("error");
|
||||||
} finally {
|
} finally {
|
||||||
runningRef.current = false;
|
if (aborterRef.current === aborter) runningRef.current = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -830,13 +896,22 @@ export function useStep3Summary(
|
|||||||
seatDeliveries,
|
seatDeliveries,
|
||||||
consensusText,
|
consensusText,
|
||||||
coordinatorPrompt,
|
coordinatorPrompt,
|
||||||
|
abort,
|
||||||
|
freezeStreaming,
|
||||||
appendUserDialogue,
|
appendUserDialogue,
|
||||||
appendStreamingDialogue,
|
appendStreamingDialogue,
|
||||||
finalizeDialogue,
|
|
||||||
streamOnce,
|
streamOnce,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** 暂停当前生成:中断在途请求 + 把流式气泡收尾为「已暂停」+ 状态回 done。 */
|
||||||
|
const pause = useCallback(() => {
|
||||||
|
if (!runningRef.current) return;
|
||||||
|
abort();
|
||||||
|
freezeStreaming("已暂停");
|
||||||
|
setPhase((p) => (p === "creating" || p === "streaming" ? "done" : p));
|
||||||
|
}, [abort, freezeStreaming]);
|
||||||
|
|
||||||
const start = useCallback(() => {
|
const start = useCallback(() => {
|
||||||
void run();
|
void run();
|
||||||
}, [run]);
|
}, [run]);
|
||||||
@@ -912,7 +987,6 @@ export function useStep3Summary(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const busy = phase === "creating" || phase === "streaming";
|
const busy = phase === "creating" || phase === "streaming";
|
||||||
const canAsk = !busy && (phase === "done" || phase === "error");
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
phase,
|
phase,
|
||||||
@@ -921,11 +995,12 @@ export function useStep3Summary(
|
|||||||
dialogues,
|
dialogues,
|
||||||
seatStubMessages,
|
seatStubMessages,
|
||||||
activeArtifactThreadId,
|
activeArtifactThreadId,
|
||||||
canAsk,
|
busy,
|
||||||
start,
|
start,
|
||||||
hydrateSaved,
|
hydrateSaved,
|
||||||
redraw,
|
redraw,
|
||||||
ask,
|
ask,
|
||||||
|
pause,
|
||||||
abort,
|
abort,
|
||||||
openArtifactFromStep,
|
openArtifactFromStep,
|
||||||
openArtifactPreview,
|
openArtifactPreview,
|
||||||
|
|||||||
Reference in new issue
Block a user