fix: improve assistant thinking and image progress

This commit is contained in:
boos
2026-06-06 22:02:45 +08:00
parent d7d14e077e
commit 3c6f061709
3 changed files with 217 additions and 14 deletions
+89
View File
@@ -3639,6 +3639,35 @@ textarea {
font-size: 12px; font-size: 12px;
} }
.picture-agent-active-stack {
display: grid;
gap: 8px;
padding: 10px;
border: 1px solid color-mix(in srgb, var(--color-primary) 18%, var(--color-border-light));
border-radius: var(--radius-sm);
background:
linear-gradient(180deg, color-mix(in srgb, var(--color-primary) 6%, var(--color-bg-card)), color-mix(in srgb, var(--color-bg) 56%, var(--color-bg-card)));
min-width: 0;
}
.picture-agent-active-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.picture-agent-active-head strong {
color: var(--color-text-primary);
font-size: 13px;
}
.picture-agent-active-head span {
color: var(--color-primary);
font-size: 12px;
font-weight: 700;
}
.picture-agent-job-panel { .picture-agent-job-panel {
display: grid; display: grid;
gap: 10px; gap: 10px;
@@ -4091,6 +4120,66 @@ textarea {
background-size: 28px 28px; background-size: 28px 28px;
} }
.picture-agent-board:has(.picture-agent-board-progress) {
grid-template-rows: auto minmax(0, 1fr) auto;
}
.picture-agent-board-progress {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px 12px;
padding: 10px 12px;
border: 1px solid color-mix(in srgb, var(--color-primary) 20%, var(--color-border-light));
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--color-bg-card) 84%, var(--color-primary) 4%);
box-shadow: var(--shadow-sm);
}
.picture-agent-board-progress div:first-child {
display: grid;
gap: 2px;
min-width: 0;
}
.picture-agent-board-progress strong {
color: var(--color-text-primary);
font-size: 13px;
}
.picture-agent-board-progress span {
min-width: 0;
overflow: hidden;
color: var(--color-text-muted);
font-size: 12px;
line-height: 1.45;
text-overflow: ellipsis;
white-space: nowrap;
}
.picture-agent-board-progress em {
align-self: start;
color: var(--color-primary);
font-size: 12px;
font-style: normal;
font-weight: 800;
}
.picture-agent-board-progress-bar {
grid-column: 1 / -1;
height: 7px;
overflow: hidden;
border-radius: 999px;
background: color-mix(in srgb, var(--color-border) 48%, transparent);
}
.picture-agent-board-progress-bar span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--color-primary), var(--color-info));
transition: width 0.22s ease;
}
.picture-agent-image-frame { .picture-agent-image-frame {
min-height: 0; min-height: 0;
display: grid; display: grid;
+95 -10
View File
@@ -112,6 +112,9 @@ const QUICK_PROMPTS = [
'根据已有角色和场景,继续生成 ComfyUI 工作流 JSON', '根据已有角色和场景,继续生成 ComfyUI 工作流 JSON',
]; ];
const STREAMING_REASONING_PLACEHOLDER = '正在等待模型返回真实思考过程,收到后会实时替换这里。';
const STREAMING_BODY_PLACEHOLDER = '模型正在组织正文和素材步骤...';
const SEEDANCE_PROMPT_TEMPLATES = [ const SEEDANCE_PROMPT_TEMPLATES = [
{ {
title: '文生视频', title: '文生视频',
@@ -651,7 +654,7 @@ function renderMarkdown(content: string): string {
} }
function processThinkTags(content: string, reasoningContent = '') { function processThinkTags(content: string, reasoningContent = '') {
if (!content.includes('<think>')) { if (!content || !/<think>/i.test(content)) {
return { content, reasoningContent }; return { content, reasoningContent };
} }
const thoughts: string[] = []; const thoughts: string[] = [];
@@ -665,7 +668,18 @@ function processThinkTags(content: string, reasoningContent = '') {
lastIndex = match.index + match[0].length; lastIndex = match.index + match[0].length;
} }
replyParts.push(content.substring(lastIndex)); replyParts.push(content.substring(lastIndex));
const cleanContent = replyParts.join('').replace(/<\/?think>/gi, '').trim();
let cleanContent = replyParts.join('');
const lastOpenThinkIndex = cleanContent.toLowerCase().lastIndexOf('<think>');
if (lastOpenThinkIndex !== -1) {
const fragmentAfterLastOpen = cleanContent.substring(lastOpenThinkIndex + '<think>'.length);
if (!/<\/think>/i.test(fragmentAfterLastOpen)) {
thoughts.push(fragmentAfterLastOpen.trim());
cleanContent = cleanContent.substring(0, lastOpenThinkIndex);
}
}
cleanContent = cleanContent.replace(/<\/?think>/gi, '').trim();
const thoughtsText = thoughts.filter(Boolean).join('\n\n---\n\n'); const thoughtsText = thoughts.filter(Boolean).join('\n\n---\n\n');
return { return {
content: cleanContent, content: cleanContent,
@@ -821,7 +835,7 @@ export default function ComfyUIAgent() {
useEffect(() => { useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); chatEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' });
}, [messages, streaming?.content]); }, [messages, streaming?.content, streaming?.reasoningContent, streaming?.images.length, streaming?.scenes.length, streaming?.tts.length]);
useEffect(() => { useEffect(() => {
activeConversationIdRef.current = activeConversationId; activeConversationIdRef.current = activeConversationId;
@@ -1349,16 +1363,30 @@ export default function ComfyUIAgent() {
const renderReasoning = (message: AgentMessage | StreamAccumulator, isStreaming = false) => { const renderReasoning = (message: AgentMessage | StreamAccumulator, isStreaming = false) => {
const processed = processThinkTags(message.content || '', message.reasoningContent || ''); const processed = processThinkTags(message.content || '', message.reasoningContent || '');
const hasReasoning = processed.reasoningContent.trim().length > 0; const hasReasoning = processed.reasoningContent.trim().length > 0;
if (!hasReasoning) return null; if (!hasReasoning && !isStreaming) return null;
return ( return (
<details open={isStreaming} style={reasoningBoxStyle}> <details
open={isStreaming}
style={{
...reasoningBoxStyle,
...(!hasReasoning && isStreaming ? reasoningPendingBoxStyle : {}),
}}
>
<summary style={reasoningSummaryStyle}> <summary style={reasoningSummaryStyle}>
思考过程 <span>思考过程</span>
{isStreaming ? <em style={reasoningLiveBadgeStyle}>实时</em> : null}
</summary> </summary>
<div {hasReasoning ? (
style={reasoningContentStyle} <div
dangerouslySetInnerHTML={{ __html: renderMarkdown(processed.reasoningContent) }} style={reasoningContentStyle}
/> dangerouslySetInnerHTML={{ __html: renderMarkdown(processed.reasoningContent) }}
/>
) : (
<div style={reasoningPendingStyle}>
<span style={reasoningPulseDotStyle} />
<span>{STREAMING_REASONING_PLACEHOLDER}</span>
</div>
)}
</details> </details>
); );
}; };
@@ -1711,6 +1739,9 @@ export default function ComfyUIAgent() {
{renderReasoning(streaming, true)} {renderReasoning(streaming, true)}
{streaming.content ? renderMessageContent(streaming) : null} {streaming.content ? renderMessageContent(streaming) : null}
{streaming.content ? <span style={cursorStyle}>|</span> : null} {streaming.content ? <span style={cursorStyle}>|</span> : null}
{!streaming.content && !streaming.images.length && !streaming.scenes.length && !streaming.tts.length ? (
<div style={streamingBodyStatusStyle}>{STREAMING_BODY_PLACEHOLDER}</div>
) : null}
{renderAssets(streaming)} {renderAssets(streaming)}
<div style={messageActionsStyle}> <div style={messageActionsStyle}>
<button <button
@@ -2121,6 +2152,9 @@ const reasoningSummaryStyle: CSSProperties = {
fontSize: 12, fontSize: 12,
fontWeight: 700, fontWeight: 700,
color: 'var(--color-primary)', color: 'var(--color-primary)',
display: 'flex',
alignItems: 'center',
gap: 6,
}; };
const reasoningContentStyle: CSSProperties = { const reasoningContentStyle: CSSProperties = {
@@ -2130,6 +2164,57 @@ const reasoningContentStyle: CSSProperties = {
color: 'var(--color-text-secondary)', color: 'var(--color-text-secondary)',
}; };
const reasoningPendingBoxStyle: CSSProperties = {
borderStyle: 'dashed',
background:
'linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 7%, var(--color-bg)), color-mix(in srgb, var(--color-info) 5%, var(--color-bg)))',
};
const reasoningLiveBadgeStyle: CSSProperties = {
marginLeft: 8,
padding: '1px 6px',
border: '1px solid color-mix(in srgb, var(--color-primary) 22%, var(--color-border-light))',
borderRadius: 999,
color: 'var(--color-primary)',
background: 'color-mix(in srgb, var(--color-primary) 8%, transparent)',
fontSize: 11,
fontStyle: 'normal',
fontWeight: 700,
};
const reasoningPendingStyle: CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '0 10px 10px',
color: 'var(--color-text-muted)',
fontSize: 12,
lineHeight: 1.6,
};
const reasoningPulseDotStyle: CSSProperties = {
width: 7,
height: 7,
flexShrink: 0,
borderRadius: 999,
background: 'var(--color-primary)',
boxShadow: '0 0 0 5px color-mix(in srgb, var(--color-primary) 12%, transparent)',
};
const streamingBodyStatusStyle: CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
width: 'fit-content',
maxWidth: '100%',
marginTop: 4,
padding: '7px 10px',
border: '1px solid var(--color-border-light)',
borderRadius: 999,
background: 'color-mix(in srgb, var(--color-bg) 74%, var(--color-surface))',
color: 'var(--color-text-muted)',
fontSize: 12,
};
const inputBarStyle: CSSProperties = { const inputBarStyle: CSSProperties = {
padding: 16, padding: 16,
borderTop: '1px solid var(--color-border)', borderTop: '1px solid var(--color-border)',
+33 -4
View File
@@ -199,6 +199,12 @@ function buildSize(aspect: string, width: number, height: number) {
return `${option.width}x${option.height}`; return `${option.width}x${option.height}`;
} }
function jobProgressPercent(job: PictureJob) {
const total = Math.max(1, Number(job.progressTotal || 4));
const progress = Math.min(total, Math.max(0, Number(job.progress || 0)));
return Math.round((progress / total) * 100);
}
function templateTitle(prompt: string, postNumber?: number) { function templateTitle(prompt: string, postNumber?: number) {
const clean = prompt.split(/\s+/).join(' ').trim(); const clean = prompt.split(/\s+/).join(' ').trim();
let title = Array.from(clean).slice(0, 18).join(''); let title = Array.from(clean).slice(0, 18).join('');
@@ -311,6 +317,7 @@ export default function PictureAgent() {
const selectedImage = images.find((image) => image.id === selectedId) ?? images[0] ?? null; const selectedImage = images.find((image) => image.id === selectedId) ?? images[0] ?? null;
const activeJobs = jobs.filter((job) => job.status === 'pending' || job.status === 'running'); const activeJobs = jobs.filter((job) => job.status === 'pending' || job.status === 'running');
const primaryActiveJob = activeJobs[0] ?? null;
const modeLabel = mode === 'edit' ? '图片编辑' : '文生图'; const modeLabel = mode === 'edit' ? '图片编辑' : '文生图';
const resultLabel = selectedImage?.mode === 'edit' ? '编辑结果' : '生成结果'; const resultLabel = selectedImage?.mode === 'edit' ? '编辑结果' : '生成结果';
const resolvedSize = buildSize(aspect, customWidth, customHeight); const resolvedSize = buildSize(aspect, customWidth, customHeight);
@@ -654,9 +661,7 @@ export default function PictureAgent() {
}; };
const renderJobCard = (job: PictureJob) => { const renderJobCard = (job: PictureJob) => {
const total = Math.max(1, Number(job.progressTotal || 4)); const percent = jobProgressPercent(job);
const progress = Math.min(total, Math.max(0, Number(job.progress || 0)));
const percent = Math.round((progress / total) * 100);
const hint = staleRunningHint(job); const hint = staleRunningHint(job);
const isActive = job.status === 'pending' || job.status === 'running'; const isActive = job.status === 'pending' || job.status === 'running';
const isFinished = !isActive; const isFinished = !isActive;
@@ -864,12 +869,24 @@ export default function PictureAgent() {
<div className="picture-agent-job-summary"> <div className="picture-agent-job-summary">
<div> <div>
<strong>生图进度</strong> <strong>生图进度</strong>
<span>{latestJobLabel} · {jobs.length} 条记录</span> <span>{latestJobLabel} · {activeJobs.length} 个进行中 · {jobs.length} 条记录</span>
</div> </div>
<button type="button" className="btn btn-ghost" onClick={() => setJobDialogOpen(true)}> <button type="button" className="btn btn-ghost" onClick={() => setJobDialogOpen(true)}>
查看进度 查看进度
</button> </button>
</div> </div>
{activeJobs.length > 0 ? (
<div className="picture-agent-active-stack">
<div className="picture-agent-active-head">
<strong>后台任务</strong>
<span>{activeJobs.length} 个生成中</span>
</div>
<div className="picture-agent-job-list">
{activeJobs.slice(0, 3).map(renderJobCard)}
</div>
</div>
) : null}
</aside> </aside>
<section className="picture-agent-canvas"> <section className="picture-agent-canvas">
@@ -917,6 +934,18 @@ export default function PictureAgent() {
</div> </div>
<div className="picture-agent-board"> <div className="picture-agent-board">
{primaryActiveJob ? (
<div className="picture-agent-board-progress">
<div>
<strong>{primaryActiveJob.status === 'pending' ? '任务已进入队列' : '正在生成图片'}</strong>
<span>{primaryActiveJob.message || primaryActiveJob.result?.prompt || prompt}</span>
</div>
<em>{jobProgressPercent(primaryActiveJob)}%</em>
<div className="picture-agent-board-progress-bar">
<span style={{ width: `${jobProgressPercent(primaryActiveJob)}%` }} />
</div>
</div>
) : null}
{selectedImage ? ( {selectedImage ? (
<> <>
<div className="picture-agent-image-frame"> <div className="picture-agent-image-frame">