fix: improve assistant thinking and image progress
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user