Compare commits

...

4 Commits

Author SHA1 Message Date
史悦
e1ffd14bc1 新增结尾补渲逻辑 ensureFinalMermaidRender 并在 finalizeAssistantMessage 中调用(js/core/app-
shell.js:916-1355),即便流式阶段已渲染过,也会在响应完成后使用最终完整代码再渲染一次,同时更新已保
    存的 artifact,解决末尾缺失的问题。
2025-10-28 10:12:31 +08:00
史悦
71cfa133a6 mermaid 改为流式 2025-10-28 09:53:37 +08:00
史悦
7bcfadde59 - 在 js/core/app-shell.js:1275 引入 window.mermaid.parse(code) 语法校验,若捕获异常则抛出带有“Mermaid
语法错误”描述的错误,阻断后续渲染流程并避免生成无效图表。
  - 在 js/core/app-shell.js:1244 的渲染异常分支中统一处理错误信息,既在查看区域展示,又在输入框末尾追加
    (避免重复追加),并自动调整输入框高度,方便用户基于错误提示修改 Mermaid 代码。
2025-10-28 09:19:07 +08:00
史悦
298e421f7d 修改样式和提示词 2025-10-28 09:14:19 +08:00
3 changed files with 271 additions and 13 deletions

View File

@@ -459,3 +459,5 @@ iconify-icon {
object-fit: contain; object-fit: contain;
max-width: 100% !important; max-width: 100% !important;
} }
#dmermaidSvg{ height: 0px;}

View File

@@ -28,6 +28,7 @@
this.isProcessing = false; this.isProcessing = false;
this.activeStreamHandle = null; this.activeStreamHandle = null;
this.pendingCancel = false; this.pendingCancel = false;
this.manualAbortRequested = false;
this.streamState = null; this.streamState = null;
this.echartsInstance = null; this.echartsInstance = null;
this.mermaidPanZoom = null; this.mermaidPanZoom = null;
@@ -374,19 +375,21 @@
return; return;
} }
let lastAiMessageId = null; let lastAssistantLikeId = null;
for (let i = history.length - 1; i >= 0; i -= 1) { for (let i = history.length - 1; i >= 0; i -= 1) {
if (history[i].type === 'ai') { if (history[i].type === 'ai' || history[i].type === 'error') {
lastAiMessageId = history[i].id; lastAssistantLikeId = history[i].id;
break; break;
} }
} }
history.forEach((message) => { history.forEach((message) => {
const isAiMessage = message.type === 'ai';
const isAssistantLike = isAiMessage || message.type === 'error';
const bubble = this.buildMessageBubble(message, { const bubble = this.buildMessageBubble(message, {
allowRollback: message.type === 'ai', allowRollback: isAiMessage,
allowRegenerate: allowRegenerate:
message.type === 'ai' && message.id === lastAiMessageId, isAssistantLike && message.id === lastAssistantLikeId,
allowDelete: true allowDelete: true
}); });
this.el.chatHistory.appendChild(bubble); this.el.chatHistory.appendChild(bubble);
@@ -591,16 +594,17 @@
const history = this.conversationService.getHistory(manifest); const history = this.conversationService.getHistory(manifest);
const index = history.findIndex((msg) => msg.id === messageId); const index = history.findIndex((msg) => msg.id === messageId);
if (index === -1) { if (index === -1) {
alert('未找到指定的AI消息。'); alert('未找到指定的回复。');
return; return;
} }
const target = history[index]; const target = history[index];
if (target.type !== 'ai') { const isAssistantLike = target.type === 'ai' || target.type === 'error';
alert('只能对AI回复执行重新生成。'); if (!isAssistantLike) {
alert('只能对AI回复或错误提示执行重新生成。');
return; return;
} }
if (index !== history.length - 1) { if (index !== history.length - 1) {
alert('请先使用退回功能,确保该AI回复位于对话末尾后再重新生成。'); alert('请先使用退回功能,确保该回复位于对话末尾后再重新生成。');
return; return;
} }
@@ -724,6 +728,7 @@
} }
beginStreaming(manifest, payload) { beginStreaming(manifest, payload) {
this.manualAbortRequested = false;
const messageId = Utils.generateId('msg'); const messageId = Utils.generateId('msg');
const container = this.createStreamingContainer(messageId); const container = this.createStreamingContainer(messageId);
this.el.chatHistory.appendChild(container); this.el.chatHistory.appendChild(container);
@@ -745,10 +750,18 @@
this.el.sendButton.disabled = false; this.el.sendButton.disabled = false;
this.activeStreamHandle = null; this.activeStreamHandle = null;
this.pendingCancel = false; this.pendingCancel = false;
const wasManualAbort = this.manualAbortRequested;
this.manualAbortRequested = false;
this.streamState = null; this.streamState = null;
if (aborted) { if (aborted) {
container.remove(); container.remove();
if (wasManualAbort) {
this.handleManualStreamAbort(manifest, messageId, fullContent);
} else {
this.ensureActiveArtifact(manifest);
this.renderActiveArtifact();
}
return; return;
} }
@@ -916,11 +929,40 @@
if (artifactId && artifactPayload) { if (artifactId && artifactPayload) {
this.runtime.saveArtifact(manifest.id, artifactId, artifactPayload); this.runtime.saveArtifact(manifest.id, artifactId, artifactPayload);
this.renderArtifact(artifactId); this.renderArtifact(artifactId);
if (
manifest.artifact?.type === 'mermaid' &&
parsedResult?.code
) {
this.ensureFinalMermaidRender(
manifest,
artifactId,
messageId,
parsedResult.code,
streamContext?.mermaid || null
);
}
} }
this.renderConversationHistory(); this.renderConversationHistory();
} }
handleManualStreamAbort(manifest, messageId, fullContent) {
const content = (fullContent || '').trim();
const messageRecord = {
id: messageId,
type: 'ai',
content:
content || '生成已被手动终止,您可以点击重新生成继续。',
timestamp: new Date().toISOString(),
artifactId: null,
interrupted: true
};
this.conversationService.appendMessage(manifest, messageRecord);
this.ensureActiveArtifact(manifest);
this.renderConversationHistory();
this.renderActiveArtifact();
}
parseMarkdownContent(text) { parseMarkdownContent(text) {
if (!text) return ''; if (!text) return '';
if (typeof marked !== 'undefined') { if (typeof marked !== 'undefined') {
@@ -1169,7 +1211,13 @@
streamState.mermaid = { streamState.mermaid = {
started: false, started: false,
artifactId: null, artifactId: null,
beforeText: '' beforeText: '',
renderedCode: null,
pendingCode: null,
renderLoopPromise: null,
codeStartIndex: null,
completed: false,
finalRendered: false
}; };
} }
const ctx = streamState.mermaid; const ctx = streamState.mermaid;
@@ -1180,10 +1228,130 @@
ctx.started = true; ctx.started = true;
ctx.artifactId = ctx.artifactId || Utils.generateId('mermaid'); ctx.artifactId = ctx.artifactId || Utils.generateId('mermaid');
ctx.beforeText = fullContent.substring(0, match.index); ctx.beforeText = fullContent.substring(0, match.index);
ctx.codeStartIndex = match.index + match[0].length;
ctx.finalRendered = false;
this.updateMermaidPlaceholder(streamState.container, manifest, ctx); this.updateMermaidPlaceholder(streamState.container, manifest, ctx);
this.showViewerStreaming(manifest); this.showViewerStreaming(manifest);
} }
} }
if (!ctx.started) {
return;
}
if (typeof ctx.codeStartIndex !== 'number' || ctx.codeStartIndex < 0) {
return;
}
let codeSection = fullContent.substring(ctx.codeStartIndex);
const closingFenceIndex = codeSection.indexOf('```');
if (closingFenceIndex !== -1) {
ctx.completed = true;
codeSection = codeSection.substring(0, closingFenceIndex);
}
const code = codeSection.trim();
if (!code || code === ctx.renderedCode) {
return;
}
ctx.finalRendered = false;
this.scheduleMermaidStreamRender(manifest, streamState, code);
}
scheduleMermaidStreamRender(manifest, streamState, code) {
if (!streamState || !streamState.mermaid) return;
const ctx = streamState.mermaid;
ctx.pendingCode = code;
if (ctx.renderLoopPromise) {
return;
}
const renderLoop = async () => {
while (ctx.pendingCode && ctx.pendingCode !== ctx.renderedCode) {
const nextCode = ctx.pendingCode;
ctx.pendingCode = null;
try {
await this.ensureMermaidReady();
window.mermaid.parse(nextCode);
const renderId = `mermaidSvg`;
const { svg } = await window.mermaid.render(renderId, nextCode);
ctx.renderedCode = nextCode;
ctx.svgContent = svg;
streamState.mermaid.svgContent = svg;
streamState.mermaid.code = nextCode;
this.destroyMermaidPanZoom();
this.renderSvgMarkup(svg, manifest.id, {
applyTransform: false,
wrapperClasses: ['svg-content-wrapper--mermaid']
});
} catch (error) {
ctx.lastError = error;
console.warn('Mermaid 流式渲染失败,等待更多内容补全:', error);
break;
}
}
};
ctx.renderLoopPromise = renderLoop()
.catch((error) => {
console.warn('Mermaid 流式渲染循环异常:', error);
})
.finally(() => {
ctx.renderLoopPromise = null;
});
}
ensureFinalMermaidRender(
manifest,
artifactId,
messageId,
finalCode,
mermaidState
) {
const state = mermaidState || {};
if (state.finalizing) {
return;
}
const alreadyFinal =
state.finalRendered &&
state.renderedCode === finalCode &&
!!state.svgContent;
if (alreadyFinal) {
return;
}
state.finalizing = true;
(async () => {
try {
await this.ensureMermaidReady();
window.mermaid.parse(finalCode);
const renderId = `mermaidSvg`;
const { svg } = await window.mermaid.render(renderId, finalCode);
state.renderedCode = finalCode;
state.svgContent = svg;
state.completed = true;
state.finalRendered = true;
const artifacts =
this.runtime.getArtifacts(manifest.id) || {};
const existing = artifacts[artifactId] || {
id: artifactId,
type: 'mermaid',
messageId
};
const updatedArtifact = {
...existing,
code: finalCode,
svgContent: svg
};
this.runtime.saveArtifact(manifest.id, artifactId, updatedArtifact);
const currentId =
this.runtime.getState(manifest.id)?.currentArtifactId;
if (currentId === artifactId) {
this.destroyMermaidPanZoom();
this.renderSvgMarkup(svg, manifest.id, {
applyTransform: false,
wrapperClasses: ['svg-content-wrapper--mermaid']
});
}
} catch (error) {
console.warn('Mermaid 最终渲染失败:', error);
} finally {
state.finalizing = false;
}
})();
} }
updateMermaidPlaceholder(container, manifest, ctx) { updateMermaidPlaceholder(container, manifest, ctx) {
@@ -1238,11 +1406,22 @@
} catch (error) { } catch (error) {
this.destroyMermaidPanZoom(); this.destroyMermaidPanZoom();
console.error('Mermaid 渲染失败:', error); console.error('Mermaid 渲染失败:', error);
const errorMessage = error.message || '未知错误';
this.el.viewer.innerHTML = ` this.el.viewer.innerHTML = `
<div class="p-4 text-center text-red-500 font-bold"> <div class="p-4 text-center text-red-500 font-bold">
Mermaid 渲染失败:${Utils.escapeHtml(error.message || '未知错误')} Mermaid 渲染失败:${Utils.escapeHtml(errorMessage)}
</div> </div>
`; `;
if (this.el.chatInput) {
const existingValue = this.el.chatInput.value || '';
const appendedValue = existingValue.includes(errorMessage)
? existingValue
: existingValue
? `${existingValue}\n${errorMessage}`
: errorMessage;
this.el.chatInput.value = appendedValue;
Utils.autoResizeTextarea(this.el.chatInput);
}
} }
} }
@@ -1251,12 +1430,21 @@
return artifact.svgContent; return artifact.svgContent;
} }
await this.ensureMermaidReady(); await this.ensureMermaidReady();
const renderId = `mermaid-${artifact.id || Utils.generateId('mermaid')}-${Date.now()}`;
const code = artifact.code || artifact.content || ''; const code = artifact.code || artifact.content || '';
if (!code.trim()) { if (!code.trim()) {
throw new Error('缺少 Mermaid 代码,无法渲染'); throw new Error('缺少 Mermaid 代码,无法渲染');
} }
const { svg } = await window.mermaid.render("mermaidSvg", code); try {
window.mermaid.parse(code);
} catch (parseError) {
const syntaxMessage =
parseError?.str || parseError?.message || '未知错误';
const error = new Error(`Mermaid 语法错误:${syntaxMessage}`);
error.isMermaidSyntaxError = true;
throw error;
}
const renderId = `mermaidSvg`;
const { svg } = await window.mermaid.render(renderId, code);
const updatedArtifact = { const updatedArtifact = {
...artifact, ...artifact,
svgContent: svg svgContent: svg
@@ -1563,6 +1751,7 @@
cancelActiveStream() { cancelActiveStream() {
this.manualAbortRequested = true;
if (!this.activeStreamHandle || typeof this.activeStreamHandle.cancel !== 'function') { if (!this.activeStreamHandle || typeof this.activeStreamHandle.cancel !== 'function') {
this.pendingCancel = true; this.pendingCancel = true;
this.setSendButtonState('terminating'); this.setSendButtonState('terminating');

View File

@@ -36,3 +36,70 @@
(技术特长 . '(Mermaid语法 图表设计 可视化 代码优化)) (技术特长 . '(Mermaid语法 图表设计 可视化 代码优化))
(工作原则 . '(准确理解 智能选择 规范输出 易于理解)) (工作原则 . '(准确理解 智能选择 规范输出 易于理解))
(输出标准 . '(语法正确 结构清晰 美观实用 可直接使用)) (输出标准 . '(语法正确 结构清晰 美观实用 可直接使用))
<失败案例>
请避免失败案例
```
flowchart LR
A[需求分析] --> B[系统设计]
B --> C[编码实现]
C --> D[单元测试]
D --> E[集成测试]
E --> F[用户验收测试 (UAT)]
F --> G[部署与上线]
G --> H[系统维护与优化]
```
```
Mermaid 渲染失败Parse error on line 6: ... E --> F[用户验收测试 (UAT)] F --> G[部 ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
```
> **正确写法:** `F[用户验收测试 (UAT)]` 应该改为 `F["用户验收测试 (UAT)"]`
</失败案例>
<失败案例>
```
classDiagram
%% 使用中文作为类名和属性名称,注意使用双引号包裹中文
class "用户" {
+字符串 "用户名"
+字符串 "密码"
+登录()
+注销()
}
class "订单" {
+整数 "订单编号"
+日期 "订单日期"
+计算总价()
}
class "商品" {
+字符串 "商品名称"
+浮点数 "价格"
+库存数量
+更新库存()
}
"用户" --> "订单" : "下单"
"订单" *-- "商品" : "包含"
```
**正确写法:**
```
classDiagram
%% 使用中文作为类名和属性名称,注意使用双引号包裹中文
class 用户 {
+字符串 "用户名"
+字符串 "密码"
+登录()
+注销()
}
class 订单 {
+整数 "订单编号"
+日期 "订单日期"
+计算总价()
}
class 商品 {
+字符串 "商品名称"
+浮点数 "价格"
+库存数量
+更新库存()
}
用户 --> 订单 : "下单"
订单 *-- 商品 : "包含"
```
</失败案例>