feat(chat): 收拢式AI思考/执行过程可视化 + 修复span(name)重复传参导致技能/MCP执行崩溃
This commit is contained in:
1 parent
46528eddbb
commit
c32eed39bd
4 files changed
+2298
-4
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -1557,3 +1557,110 @@ del {
|
|||||||
border-color: var(--color-accent, #5B5FED);
|
border-color: var(--color-accent, #5B5FED);
|
||||||
color: var(--color-accent, #5B5FED);
|
color: var(--color-accent, #5B5FED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== AI 思考 / 执行过程面板(收拢式) ===== */
|
||||||
|
.ai-process {
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px solid var(--color-border-light, #e6e9f0);
|
||||||
|
border-radius: var(--radius-md, 12px);
|
||||||
|
background: color-mix(in srgb, var(--color-bg-secondary, #fff) 75%, var(--color-accent, #5B5FED) 4%);
|
||||||
|
backdrop-filter: blur(8px) saturate(160%);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color var(--transition-fast, .2s), box-shadow var(--transition-fast, .2s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-process[open] {
|
||||||
|
box-shadow: 0 6px 22px rgba(91, 95, 237, 0.10);
|
||||||
|
border-color: color-mix(in srgb, var(--color-accent, #5B5FED) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-process-summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 14px;
|
||||||
|
font-size: var(--text-sm, 13px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-secondary, #475069);
|
||||||
|
user-select: none;
|
||||||
|
transition: background var(--transition-fast, .2s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-process-summary::-webkit-details-marker { display: none; }
|
||||||
|
.ai-process-summary::marker { content: ""; }
|
||||||
|
|
||||||
|
.ai-process-summary:hover {
|
||||||
|
background: color-mix(in srgb, var(--color-accent, #5B5FED) 7%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-process-badge {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
filter: drop-shadow(0 1px 2px rgba(91, 95, 237, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-process-list {
|
||||||
|
padding: 4px 14px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
animation: ai-process-in .28s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ai-process-in {
|
||||||
|
from { opacity: 0; transform: translateY(-4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-process-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--color-bg-secondary, #fff);
|
||||||
|
border: 1px solid var(--color-border-light, #e6e9f0);
|
||||||
|
border-left: 3px solid var(--color-accent, #5B5FED);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-process-thinking { border-left-color: #8b8fe6; }
|
||||||
|
.ai-process-skill { border-left-color: #5B5FED; }
|
||||||
|
.ai-process-mcp { border-left-color: #12a594; }
|
||||||
|
|
||||||
|
.ai-process-ico {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.4;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-process-body {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-process-title {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--text-sm, 13px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary, #1f2430);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-process-text {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--text-xs, 12px);
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--color-text-tertiary, #8a90a6);
|
||||||
|
max-height: 160px;
|
||||||
|
overflow-y: auto;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题微调 */
|
||||||
|
[data-theme="dark"] .ai-process,
|
||||||
|
html.dark .ai-process {
|
||||||
|
background: color-mix(in srgb, var(--color-bg-secondary, #1b1d29) 70%, var(--color-accent, #5B5FED) 8%);
|
||||||
|
border-color: color-mix(in srgb, var(--color-accent, #5B5FED) 25%, transparent);
|
||||||
|
}
|
||||||
@@ -895,6 +895,7 @@ const ChatPage: React.FC = () => {
|
|||||||
createdAt: data.assistant_message.created_at || new Date().toISOString(),
|
createdAt: data.assistant_message.created_at || new Date().toISOString(),
|
||||||
used_skills: data.assistant_message.used_skills || [],
|
used_skills: data.assistant_message.used_skills || [],
|
||||||
used_mcp: data.assistant_message.used_mcp || [],
|
used_mcp: data.assistant_message.used_mcp || [],
|
||||||
|
process_steps: data.assistant_message.process_steps || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return [...filtered, ...newMsgs];
|
return [...filtered, ...newMsgs];
|
||||||
@@ -1168,6 +1169,7 @@ const ChatPage: React.FC = () => {
|
|||||||
// Real used_skills from API response
|
// Real used_skills from API response
|
||||||
const usedSkills: string[] = (msg.role === 'assistant' && msg.used_skills) ? msg.used_skills : [];
|
const usedSkills: string[] = (msg.role === 'assistant' && msg.used_skills) ? msg.used_skills : [];
|
||||||
const usedMcp: string[] = (msg.role === 'assistant' && msg.used_mcp) ? msg.used_mcp : [];
|
const usedMcp: string[] = (msg.role === 'assistant' && msg.used_mcp) ? msg.used_mcp : [];
|
||||||
|
const steps: any[] = (msg as any).process_steps || [];
|
||||||
// 模糊创作意图确认卡片
|
// 模糊创作意图确认卡片
|
||||||
const clarify = msg.role === 'assistant' ? parseClarify(msg.content) : null;
|
const clarify = msg.role === 'assistant' ? parseClarify(msg.content) : null;
|
||||||
return (
|
return (
|
||||||
@@ -1235,6 +1237,34 @@ const ChatPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{msg.role === 'assistant' && steps.length > 0 && (
|
||||||
|
<details className="ai-process" open={false}>
|
||||||
|
<summary className="ai-process-summary">
|
||||||
|
<span className="ai-process-badge">🧠</span>
|
||||||
|
思考 / 执行过程({steps.length} 步,点击展开)
|
||||||
|
</summary>
|
||||||
|
<div className="ai-process-list">
|
||||||
|
{steps.map((s: any, i: number) => (
|
||||||
|
<div key={i} className={`ai-process-item ai-process-${s.type}`}>
|
||||||
|
<span className="ai-process-ico">
|
||||||
|
{s.type === 'thinking' ? '💡' : s.type === 'skill' ? '🛠' : '🔌'}
|
||||||
|
</span>
|
||||||
|
<div className="ai-process-body">
|
||||||
|
{s.type === 'thinking' && (
|
||||||
|
<><b className="ai-process-title">思考</b><p className="ai-process-text">{s.text}</p></>
|
||||||
|
)}
|
||||||
|
{s.type === 'skill' && (
|
||||||
|
<><b className="ai-process-title">调用技能:{s.name}</b><p className="ai-process-text">{(s.result || '').slice(0, 400)}</p></>
|
||||||
|
)}
|
||||||
|
{s.type === 'mcp' && (
|
||||||
|
<><b className="ai-process-title">MCP · {s.server}:{s.name}</b><p className="ai-process-text">{(s.result || '').slice(0, 400)}</p></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
<div className={`message-meta ${msg.role}`}>
|
<div className={`message-meta ${msg.role}`}>
|
||||||
<span className="message-time">{formatTime(msg.createdAt)}</span>
|
<span className="message-time">{formatTime(msg.createdAt)}</span>
|
||||||
<span className="message-tokens">{msg.tokenUsed} tokens</span>
|
<span className="message-tokens">{msg.tokenUsed} tokens</span>
|
||||||
|
|||||||
@@ -62,6 +62,13 @@ export interface Message {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
used_skills?: string[];
|
used_skills?: string[];
|
||||||
used_mcp?: string[];
|
used_mcp?: string[];
|
||||||
|
process_steps?: Array<{
|
||||||
|
type: 'thinking' | 'skill' | 'mcp';
|
||||||
|
text?: string;
|
||||||
|
name?: string;
|
||||||
|
server?: string;
|
||||||
|
result?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user