Compare commits
4 Commits
dd930805e0
...
e1ffd14bc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1ffd14bc1 | ||
|
|
71cfa133a6 | ||
|
|
7bcfadde59 | ||
|
|
298e421f7d |
@@ -459,3 +459,5 @@ iconify-icon {
|
||||
object-fit: contain;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
#dmermaidSvg{ height: 0px;}
|
||||
@@ -28,6 +28,7 @@
|
||||
this.isProcessing = false;
|
||||
this.activeStreamHandle = null;
|
||||
this.pendingCancel = false;
|
||||
this.manualAbortRequested = false;
|
||||
this.streamState = null;
|
||||
this.echartsInstance = null;
|
||||
this.mermaidPanZoom = null;
|
||||
@@ -374,19 +375,21 @@
|
||||
return;
|
||||
}
|
||||
|
||||
let lastAiMessageId = null;
|
||||
let lastAssistantLikeId = null;
|
||||
for (let i = history.length - 1; i >= 0; i -= 1) {
|
||||
if (history[i].type === 'ai') {
|
||||
lastAiMessageId = history[i].id;
|
||||
if (history[i].type === 'ai' || history[i].type === 'error') {
|
||||
lastAssistantLikeId = history[i].id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
history.forEach((message) => {
|
||||
const isAiMessage = message.type === 'ai';
|
||||
const isAssistantLike = isAiMessage || message.type === 'error';
|
||||
const bubble = this.buildMessageBubble(message, {
|
||||
allowRollback: message.type === 'ai',
|
||||
allowRollback: isAiMessage,
|
||||
allowRegenerate:
|
||||
message.type === 'ai' && message.id === lastAiMessageId,
|
||||
isAssistantLike && message.id === lastAssistantLikeId,
|
||||
allowDelete: true
|
||||
});
|
||||
this.el.chatHistory.appendChild(bubble);
|
||||
@@ -591,16 +594,17 @@
|
||||
const history = this.conversationService.getHistory(manifest);
|
||||
const index = history.findIndex((msg) => msg.id === messageId);
|
||||
if (index === -1) {
|
||||
alert('未找到指定的AI消息。');
|
||||
alert('未找到指定的回复。');
|
||||
return;
|
||||
}
|
||||
const target = history[index];
|
||||
if (target.type !== 'ai') {
|
||||
alert('只能对AI回复执行重新生成。');
|
||||
const isAssistantLike = target.type === 'ai' || target.type === 'error';
|
||||
if (!isAssistantLike) {
|
||||
alert('只能对AI回复或错误提示执行重新生成。');
|
||||
return;
|
||||
}
|
||||
if (index !== history.length - 1) {
|
||||
alert('请先使用退回功能,确保该AI回复位于对话末尾后再重新生成。');
|
||||
alert('请先使用退回功能,确保该回复位于对话末尾后再重新生成。');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -724,6 +728,7 @@
|
||||
}
|
||||
|
||||
beginStreaming(manifest, payload) {
|
||||
this.manualAbortRequested = false;
|
||||
const messageId = Utils.generateId('msg');
|
||||
const container = this.createStreamingContainer(messageId);
|
||||
this.el.chatHistory.appendChild(container);
|
||||
@@ -745,10 +750,18 @@
|
||||
this.el.sendButton.disabled = false;
|
||||
this.activeStreamHandle = null;
|
||||
this.pendingCancel = false;
|
||||
const wasManualAbort = this.manualAbortRequested;
|
||||
this.manualAbortRequested = false;
|
||||
this.streamState = null;
|
||||
|
||||
if (aborted) {
|
||||
container.remove();
|
||||
if (wasManualAbort) {
|
||||
this.handleManualStreamAbort(manifest, messageId, fullContent);
|
||||
} else {
|
||||
this.ensureActiveArtifact(manifest);
|
||||
this.renderActiveArtifact();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -916,11 +929,40 @@
|
||||
if (artifactId && artifactPayload) {
|
||||
this.runtime.saveArtifact(manifest.id, artifactId, artifactPayload);
|
||||
this.renderArtifact(artifactId);
|
||||
if (
|
||||
manifest.artifact?.type === 'mermaid' &&
|
||||
parsedResult?.code
|
||||
) {
|
||||
this.ensureFinalMermaidRender(
|
||||
manifest,
|
||||
artifactId,
|
||||
messageId,
|
||||
parsedResult.code,
|
||||
streamContext?.mermaid || null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!text) return '';
|
||||
if (typeof marked !== 'undefined') {
|
||||
@@ -1169,7 +1211,13 @@
|
||||
streamState.mermaid = {
|
||||
started: false,
|
||||
artifactId: null,
|
||||
beforeText: ''
|
||||
beforeText: '',
|
||||
renderedCode: null,
|
||||
pendingCode: null,
|
||||
renderLoopPromise: null,
|
||||
codeStartIndex: null,
|
||||
completed: false,
|
||||
finalRendered: false
|
||||
};
|
||||
}
|
||||
const ctx = streamState.mermaid;
|
||||
@@ -1180,10 +1228,130 @@
|
||||
ctx.started = true;
|
||||
ctx.artifactId = ctx.artifactId || Utils.generateId('mermaid');
|
||||
ctx.beforeText = fullContent.substring(0, match.index);
|
||||
ctx.codeStartIndex = match.index + match[0].length;
|
||||
ctx.finalRendered = false;
|
||||
this.updateMermaidPlaceholder(streamState.container, manifest, ctx);
|
||||
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) {
|
||||
@@ -1238,11 +1406,22 @@
|
||||
} catch (error) {
|
||||
this.destroyMermaidPanZoom();
|
||||
console.error('Mermaid 渲染失败:', error);
|
||||
const errorMessage = error.message || '未知错误';
|
||||
this.el.viewer.innerHTML = `
|
||||
<div class="p-4 text-center text-red-500 font-bold">
|
||||
Mermaid 渲染失败:${Utils.escapeHtml(error.message || '未知错误')}
|
||||
Mermaid 渲染失败:${Utils.escapeHtml(errorMessage)}
|
||||
</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;
|
||||
}
|
||||
await this.ensureMermaidReady();
|
||||
const renderId = `mermaid-${artifact.id || Utils.generateId('mermaid')}-${Date.now()}`;
|
||||
const code = artifact.code || artifact.content || '';
|
||||
if (!code.trim()) {
|
||||
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 = {
|
||||
...artifact,
|
||||
svgContent: svg
|
||||
@@ -1563,6 +1751,7 @@
|
||||
|
||||
|
||||
cancelActiveStream() {
|
||||
this.manualAbortRequested = true;
|
||||
if (!this.activeStreamHandle || typeof this.activeStreamHandle.cancel !== 'function') {
|
||||
this.pendingCancel = true;
|
||||
this.setSendButtonState('terminating');
|
||||
|
||||
@@ -36,3 +36,70 @@
|
||||
(技术特长 . '(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 商品 {
|
||||
+字符串 "商品名称"
|
||||
+浮点数 "价格"
|
||||
+库存数量
|
||||
+更新库存()
|
||||
}
|
||||
用户 --> 订单 : "下单"
|
||||
订单 *-- 商品 : "包含"
|
||||
```
|
||||
</失败案例>
|
||||
Reference in New Issue
Block a user