放大缩小
This commit is contained in:
@@ -284,7 +284,10 @@ iconify-icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.svg-content-wrapper {
|
.svg-content-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
margin: 1rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
transform-origin: center top;
|
transform-origin: center top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -227,6 +227,9 @@
|
|||||||
<script src="js/modules/product-canvas.js"></script>
|
<script src="js/modules/product-canvas.js"></script>
|
||||||
<script src="js/modules/swot.js"></script>
|
<script src="js/modules/swot.js"></script>
|
||||||
<script src="js/modules/echarts.js"></script>
|
<script src="js/modules/echarts.js"></script>
|
||||||
|
<script src="js/modules/mermaid.js"></script>
|
||||||
|
<script src="js/vendor/mermaid.min.js"></script>
|
||||||
|
<script src="js/vendor/svg-pan-zoom.min.js"></script>
|
||||||
<script src="libs/js/echarts.min.js"></script>
|
<script src="libs/js/echarts.min.js"></script>
|
||||||
<script src="js/core/module-runtime.js"></script>
|
<script src="js/core/module-runtime.js"></script>
|
||||||
<script src="js/core/app-shell.js"></script>
|
<script src="js/core/app-shell.js"></script>
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ class APIClient {
|
|||||||
this.promptFiles = {
|
this.promptFiles = {
|
||||||
canvas: 'prompts/canvas-prompt.txt',
|
canvas: 'prompts/canvas-prompt.txt',
|
||||||
swot: 'prompts/swot-prompt.txt',
|
swot: 'prompts/swot-prompt.txt',
|
||||||
echarts: 'prompts/echarts-prompt.txt'
|
echarts: 'prompts/echarts-prompt.txt',
|
||||||
|
mermaid: 'prompts/mermaid-prompt.txt'
|
||||||
};
|
};
|
||||||
this.promptFallbacks = {
|
this.promptFallbacks = {
|
||||||
canvas: '你是一个专业的产品战略分析师,擅长创建产品画布。',
|
canvas: '你是一个专业的产品战略分析师,擅长创建产品画布。',
|
||||||
swot: '你是一个专业的商业战略分析师,擅长进行SWOT分析。',
|
swot: '你是一个专业的商业战略分析师,擅长进行SWOT分析。',
|
||||||
echarts:
|
echarts:
|
||||||
'你是一个资深的数据可视化专家,精通将自然语言需求转化为 ECharts 配置对象,请输出结构化 JSON option。',
|
'你是一个资深的数据可视化专家,精通将自然语言需求转化为 ECharts 配置对象,请输出结构化 JSON option。',
|
||||||
|
mermaid:
|
||||||
|
'你是一个资深的可视化工程师,擅长用 Mermaid 语法创建清晰的图示,请只输出一个 ```mermaid 代码块。',
|
||||||
default:
|
default:
|
||||||
'你是一个可靠的智能助手,请直接回答用户的问题并提供结构化输出。'
|
'你是一个可靠的智能助手,请直接回答用户的问题并提供结构化输出。'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
this.pendingCancel = false;
|
this.pendingCancel = false;
|
||||||
this.streamState = null;
|
this.streamState = null;
|
||||||
this.echartsInstance = null;
|
this.echartsInstance = null;
|
||||||
|
this.mermaidPanZoom = null;
|
||||||
|
this.mermaidInitialized = false;
|
||||||
|
|
||||||
this.globalStore = moduleRuntime.storageService.global();
|
this.globalStore = moduleRuntime.storageService.global();
|
||||||
this.activeModuleId = null;
|
this.activeModuleId = null;
|
||||||
@@ -138,17 +140,23 @@
|
|||||||
|
|
||||||
if (this.el.downloadSvgBtn) {
|
if (this.el.downloadSvgBtn) {
|
||||||
this.el.downloadSvgBtn.addEventListener('click', () =>
|
this.el.downloadSvgBtn.addEventListener('click', () =>
|
||||||
this.downloadArtifact()
|
this.downloadArtifact().catch((error) =>
|
||||||
|
console.error('下载SVG失败:', error)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.el.copyImageBtn) {
|
if (this.el.copyImageBtn) {
|
||||||
this.el.copyImageBtn.addEventListener('click', () =>
|
this.el.copyImageBtn.addEventListener('click', () =>
|
||||||
this.copyArtifactImage()
|
this.copyArtifactImage().catch((error) =>
|
||||||
|
console.error('复制图片失败:', error)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.el.exportImageBtn) {
|
if (this.el.exportImageBtn) {
|
||||||
this.el.exportImageBtn.addEventListener('click', () =>
|
this.el.exportImageBtn.addEventListener('click', () =>
|
||||||
this.exportArtifactAsImage()
|
this.exportArtifactAsImage().catch((error) =>
|
||||||
|
console.error('导出图片失败:', error)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.el.viewCodeBtn) {
|
if (this.el.viewCodeBtn) {
|
||||||
@@ -723,6 +731,8 @@
|
|||||||
this.updateStreamingContent(container, fullContent);
|
this.updateStreamingContent(container, fullContent);
|
||||||
if (manifest.artifact?.type === 'svg') {
|
if (manifest.artifact?.type === 'svg') {
|
||||||
this.processSvgStreamChunk(manifest, fullContent, streamState);
|
this.processSvgStreamChunk(manifest, fullContent, streamState);
|
||||||
|
} else if (manifest.artifact?.type === 'mermaid') {
|
||||||
|
this.processMermaidStreamChunk(manifest, fullContent, streamState);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -817,6 +827,19 @@
|
|||||||
messageId,
|
messageId,
|
||||||
timestamp
|
timestamp
|
||||||
};
|
};
|
||||||
|
} else if (
|
||||||
|
manifest.artifact.type === 'mermaid' &&
|
||||||
|
parsedResult.code
|
||||||
|
) {
|
||||||
|
artifactId = Utils.generateId('mermaid');
|
||||||
|
artifactPayload = {
|
||||||
|
id: artifactId,
|
||||||
|
type: manifest.artifact.type,
|
||||||
|
code: parsedResult.code,
|
||||||
|
svgContent: streamContext?.mermaid?.svgContent || null,
|
||||||
|
messageId,
|
||||||
|
timestamp
|
||||||
|
};
|
||||||
} else if (
|
} else if (
|
||||||
manifest.artifact.type === 'echarts-option' &&
|
manifest.artifact.type === 'echarts-option' &&
|
||||||
parsedResult.option
|
parsedResult.option
|
||||||
@@ -891,6 +914,11 @@
|
|||||||
const after = trim(parsedResult.afterText);
|
const after = trim(parsedResult.afterText);
|
||||||
if (before) segments.push(before);
|
if (before) segments.push(before);
|
||||||
if (after) segments.push(after);
|
if (after) segments.push(after);
|
||||||
|
} else if (manifest.artifact?.type === 'mermaid') {
|
||||||
|
const before = trim(parsedResult.beforeText);
|
||||||
|
const after = trim(parsedResult.afterText);
|
||||||
|
if (before) segments.push(before);
|
||||||
|
if (after) segments.push(after);
|
||||||
} else if (manifest.artifact?.type === 'echarts-option') {
|
} else if (manifest.artifact?.type === 'echarts-option') {
|
||||||
const before = trim(parsedResult.beforeText);
|
const before = trim(parsedResult.beforeText);
|
||||||
const after = trim(parsedResult.afterText);
|
const after = trim(parsedResult.afterText);
|
||||||
@@ -995,23 +1023,231 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTemporarySvg(svgMarkup, isPartial = false, manifest = null) {
|
renderTemporarySvg(svgMarkup, isPartial = false, manifest = null) {
|
||||||
if (!this.el.viewer || !svgMarkup) return;
|
const moduleId = manifest?.id || this.activeModuleId;
|
||||||
this.el.viewer.innerHTML = '';
|
this.renderSvgMarkup(svgMarkup, moduleId, {
|
||||||
const wrapper = document.createElement('div');
|
opacity: isPartial ? 0.9 : 1
|
||||||
wrapper.className = 'svg-content-wrapper';
|
});
|
||||||
wrapper.innerHTML = svgMarkup;
|
}
|
||||||
this.el.viewer.appendChild(wrapper);
|
|
||||||
if (isPartial) {
|
getCurrentEChartsSvgElement() {
|
||||||
wrapper.style.opacity = '0.9';
|
if (!this.echartsInstance) return null;
|
||||||
} else {
|
const dom = this.echartsInstance.getDom();
|
||||||
wrapper.style.opacity = '1';
|
if (!dom) return null;
|
||||||
|
return dom.querySelector('svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSvgStringFromElement(svgElement) {
|
||||||
|
if (!svgElement) return null;
|
||||||
|
const serializer = new XMLSerializer();
|
||||||
|
let svgContent = serializer.serializeToString(svgElement);
|
||||||
|
if (!svgContent.match(/^<svg[^>]+xmlns=/)) {
|
||||||
|
svgContent = svgContent.replace(
|
||||||
|
'<svg',
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg"'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const uiState = this.runtime.getUiState(
|
return svgContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeMermaidPanZoom(svgElement, manifest) {
|
||||||
|
if (!svgElement) return;
|
||||||
|
if (!window.svgPanZoom) {
|
||||||
|
console.warn('svgPanZoom 脚本未加载,无法提供平移缩放');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.destroyMermaidPanZoom();
|
||||||
|
let doPan = false;
|
||||||
|
let mousePos = { x: 0, y: 0 };
|
||||||
|
let panZoomInstance = null;
|
||||||
|
|
||||||
|
const onMouseDown = (ev) => {
|
||||||
|
if (!ev) return;
|
||||||
|
doPan = true;
|
||||||
|
mousePos = { x: ev.clientX, y: ev.clientY };
|
||||||
|
};
|
||||||
|
const onMouseMove = (ev) => {
|
||||||
|
if (!doPan || !panZoomInstance) return;
|
||||||
|
panZoomInstance.panBy({
|
||||||
|
x: ev.clientX - mousePos.x,
|
||||||
|
y: ev.clientY - mousePos.y
|
||||||
|
});
|
||||||
|
mousePos = { x: ev.clientX, y: ev.clientY };
|
||||||
|
const selection = window.getSelection && window.getSelection();
|
||||||
|
if (selection && selection.removeAllRanges) {
|
||||||
|
selection.removeAllRanges();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onMouseUp = () => {
|
||||||
|
doPan = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventsHandler = {
|
||||||
|
haltEventListeners: ['mousedown', 'mousemove', 'mouseup'],
|
||||||
|
init(options) {
|
||||||
|
options.svgElement.addEventListener('mousedown', onMouseDown, false);
|
||||||
|
options.svgElement.addEventListener('mousemove', onMouseMove, false);
|
||||||
|
options.svgElement.addEventListener('mouseup', onMouseUp, false);
|
||||||
|
},
|
||||||
|
destroy(options) {
|
||||||
|
options.svgElement.removeEventListener('mousedown', onMouseDown, false);
|
||||||
|
options.svgElement.removeEventListener('mousemove', onMouseMove, false);
|
||||||
|
options.svgElement.removeEventListener('mouseup', onMouseUp, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mermaidPanZoom = window.svgPanZoom(svgElement, {
|
||||||
|
zoomEnabled: true,
|
||||||
|
controlIconsEnabled: true,
|
||||||
|
fit: true,
|
||||||
|
center: true,
|
||||||
|
minZoom: 0.25,
|
||||||
|
maxZoom: 3,
|
||||||
|
customEventsHandler: eventsHandler
|
||||||
|
});
|
||||||
|
panZoomInstance = this.mermaidPanZoom;
|
||||||
|
|
||||||
|
const uiState = this.runtime.getUiState(manifest.id, { zoom: 1 });
|
||||||
|
const initialZoom = uiState.zoom || 1;
|
||||||
|
this.mermaidPanZoom.zoom(initialZoom);
|
||||||
|
this.mermaidPanZoom.setOnZoom((zoomLevel) => {
|
||||||
|
this.runtime.updateUiState(manifest.id, { zoom: zoomLevel });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyMermaidPanZoom() {
|
||||||
|
if (this.mermaidPanZoom && typeof this.mermaidPanZoom.destroy === 'function') {
|
||||||
|
this.mermaidPanZoom.destroy();
|
||||||
|
}
|
||||||
|
this.mermaidPanZoom = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isZoomableManifest(manifest) {
|
||||||
|
const type = manifest?.artifact?.type;
|
||||||
|
return type === 'svg' || type === 'mermaid';
|
||||||
|
}
|
||||||
|
|
||||||
|
processMermaidStreamChunk(manifest, fullContent, streamState) {
|
||||||
|
if (!streamState) return;
|
||||||
|
if (!streamState.mermaid) {
|
||||||
|
streamState.mermaid = {
|
||||||
|
started: false,
|
||||||
|
artifactId: null,
|
||||||
|
beforeText: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const ctx = streamState.mermaid;
|
||||||
|
const startPattern = manifest.artifact?.startPattern || /```mermaid/i;
|
||||||
|
if (!ctx.started) {
|
||||||
|
const match = fullContent.match(startPattern);
|
||||||
|
if (match) {
|
||||||
|
ctx.started = true;
|
||||||
|
ctx.artifactId = ctx.artifactId || Utils.generateId('mermaid');
|
||||||
|
ctx.beforeText = fullContent.substring(0, match.index);
|
||||||
|
this.updateMermaidPlaceholder(streamState.container, manifest, ctx);
|
||||||
|
this.showViewerStreaming(manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMermaidPlaceholder(container, manifest, ctx) {
|
||||||
|
if (!container) return;
|
||||||
|
const beforeHtml = this.parseMarkdownContent(ctx.beforeText || '');
|
||||||
|
const label = manifest.label || '图表';
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="chat-bubble-ai relative streaming-text" data-message-id="${container.dataset.messageId}">
|
||||||
|
<div>
|
||||||
|
${beforeHtml}
|
||||||
|
<div class="svg-drawing-placeholder" data-temp-id="${ctx.artifactId}">
|
||||||
|
🧠 正在生成${label}代码...
|
||||||
|
</div>
|
||||||
|
<div class="typing-cursor"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
Utils.scrollToBottom(this.el.chatHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureMermaidReady() {
|
||||||
|
if (this.mermaidInitialized) return;
|
||||||
|
if (!window.mermaid) {
|
||||||
|
throw new Error('Mermaid 脚本未加载,请检查资源引入');
|
||||||
|
}
|
||||||
|
window.mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
securityLevel: 'loose',
|
||||||
|
theme: 'default'
|
||||||
|
});
|
||||||
|
this.mermaidInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderMermaidArtifact(artifact, manifest) {
|
||||||
|
if (!this.el.viewer) return;
|
||||||
|
this.showViewerStreaming(manifest);
|
||||||
|
try {
|
||||||
|
const svgContent = await this.getMermaidSvgContent(
|
||||||
|
artifact,
|
||||||
|
manifest
|
||||||
|
);
|
||||||
|
this.destroyMermaidPanZoom();
|
||||||
|
this.renderSvgMarkup(svgContent, this.activeModuleId, {
|
||||||
|
applyTransform: false
|
||||||
|
});
|
||||||
|
const svgElement = this.el.viewer.querySelector('svg');
|
||||||
|
if (svgElement) {
|
||||||
|
svgElement.setAttribute('id', 'mermaidSvg');
|
||||||
|
this.initializeMermaidPanZoom(svgElement, manifest);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.destroyMermaidPanZoom();
|
||||||
|
console.error('Mermaid 渲染失败:', error);
|
||||||
|
this.el.viewer.innerHTML = `
|
||||||
|
<div class="p-4 text-center text-red-500 font-bold">
|
||||||
|
Mermaid 渲染失败:${Utils.escapeHtml(error.message || '未知错误')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMermaidSvgContent(artifact, manifest) {
|
||||||
|
if (artifact.svgContent) {
|
||||||
|
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(renderId, code);
|
||||||
|
const updatedArtifact = {
|
||||||
|
...artifact,
|
||||||
|
svgContent: svg
|
||||||
|
};
|
||||||
|
this.runtime.saveArtifact(
|
||||||
manifest?.id || this.activeModuleId,
|
manifest?.id || this.activeModuleId,
|
||||||
{ zoom: 1 }
|
updatedArtifact.id,
|
||||||
|
updatedArtifact
|
||||||
);
|
);
|
||||||
wrapper.style.transform = `scale(${uiState.zoom})`;
|
return svg;
|
||||||
wrapper.style.transformOrigin = 'center top';
|
}
|
||||||
|
|
||||||
|
async getSvgMarkupForArtifact(artifact, manifest) {
|
||||||
|
if (!artifact) return null;
|
||||||
|
if (artifact.type === 'svg') {
|
||||||
|
return artifact.content;
|
||||||
|
}
|
||||||
|
if (artifact.type === 'mermaid') {
|
||||||
|
return await this.getMermaidSvgContent(
|
||||||
|
artifact,
|
||||||
|
manifest || this.getActiveManifest()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (artifact.type === 'echarts-option') {
|
||||||
|
const svgElement = this.getCurrentEChartsSvgElement();
|
||||||
|
if (!svgElement) return null;
|
||||||
|
return this.getSvgStringFromElement(svgElement);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
openCodeModal(content = '') {
|
openCodeModal(content = '') {
|
||||||
@@ -1101,8 +1337,12 @@
|
|||||||
}
|
}
|
||||||
this.runtime.setActiveArtifact(manifest.id, artifactId);
|
this.runtime.setActiveArtifact(manifest.id, artifactId);
|
||||||
if (artifact.type === 'svg') {
|
if (artifact.type === 'svg') {
|
||||||
|
this.destroyMermaidPanZoom();
|
||||||
this.renderSvgArtifact(artifact);
|
this.renderSvgArtifact(artifact);
|
||||||
|
} else if (artifact.type === 'mermaid') {
|
||||||
|
this.renderMermaidArtifact(artifact, manifest);
|
||||||
} else if (artifact.type === 'echarts-option') {
|
} else if (artifact.type === 'echarts-option') {
|
||||||
|
this.destroyMermaidPanZoom();
|
||||||
this.renderEChartsArtifact(artifact);
|
this.renderEChartsArtifact(artifact);
|
||||||
}
|
}
|
||||||
this.highlightActivePlaceholder();
|
this.highlightActivePlaceholder();
|
||||||
@@ -1110,16 +1350,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSvgArtifact(artifact) {
|
renderSvgArtifact(artifact) {
|
||||||
if (!this.el.viewer) return;
|
this.renderSvgMarkup(artifact.content, this.activeModuleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSvgMarkup(svgMarkup, moduleId = this.activeModuleId, options = {}) {
|
||||||
|
if (!this.el.viewer || !svgMarkup) return;
|
||||||
|
const { opacity = 1, applyTransform = true } = options;
|
||||||
this.el.viewer.innerHTML = '';
|
this.el.viewer.innerHTML = '';
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'svg-content-wrapper';
|
wrapper.className = 'svg-content-wrapper';
|
||||||
wrapper.innerHTML = artifact.content;
|
wrapper.innerHTML = svgMarkup;
|
||||||
|
wrapper.style.opacity = opacity;
|
||||||
this.el.viewer.appendChild(wrapper);
|
this.el.viewer.appendChild(wrapper);
|
||||||
const uiState = this.runtime.getUiState(this.activeModuleId, {
|
const uiState = this.runtime.getUiState(moduleId, {
|
||||||
zoom: 1
|
zoom: 1
|
||||||
});
|
});
|
||||||
wrapper.style.transform = `scale(${uiState.zoom})`;
|
if (applyTransform) {
|
||||||
|
wrapper.style.transform = `scale(${uiState.zoom})`;
|
||||||
|
} else {
|
||||||
|
wrapper.style.transform = '';
|
||||||
|
}
|
||||||
wrapper.style.transformOrigin = 'center top';
|
wrapper.style.transformOrigin = 'center top';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1142,31 +1392,46 @@
|
|||||||
this.echartsInstance.dispose();
|
this.echartsInstance.dispose();
|
||||||
}
|
}
|
||||||
this.echartsInstance = window.echarts.init(chartContainer, null, {
|
this.echartsInstance = window.echarts.init(chartContainer, null, {
|
||||||
renderer: 'canvas'
|
renderer: 'svg',
|
||||||
|
useDirtyRect: false
|
||||||
});
|
});
|
||||||
this.echartsInstance.setOption(artifact.option, true);
|
this.echartsInstance.setOption(artifact.option, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustZoom(delta) {
|
adjustZoom(delta) {
|
||||||
const manifest = this.getActiveManifest();
|
const manifest = this.getActiveManifest();
|
||||||
if (manifest.artifact?.type !== 'svg') return;
|
if (!this.isZoomableManifest(manifest)) return;
|
||||||
const uiState = this.runtime.getUiState(manifest.id, { zoom: 1 });
|
const uiState = this.runtime.getUiState(manifest.id, { zoom: 1 });
|
||||||
const nextZoom = Math.min(
|
const nextZoom = Math.min(
|
||||||
3,
|
3,
|
||||||
Math.max(0.25, parseFloat((uiState.zoom + delta).toFixed(2)))
|
Math.max(0.25, parseFloat((uiState.zoom + delta).toFixed(2)))
|
||||||
);
|
);
|
||||||
this.runtime.updateUiState(manifest.id, { zoom: nextZoom });
|
this.runtime.updateUiState(manifest.id, { zoom: nextZoom });
|
||||||
this.renderActiveArtifact();
|
|
||||||
|
if (manifest.artifact?.type === 'mermaid') {
|
||||||
|
if (this.mermaidPanZoom) {
|
||||||
|
this.mermaidPanZoom.zoom(nextZoom);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.renderActiveArtifact();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetZoom() {
|
resetZoom() {
|
||||||
const manifest = this.getActiveManifest();
|
const manifest = this.getActiveManifest();
|
||||||
if (manifest.artifact?.type !== 'svg') return;
|
if (!this.isZoomableManifest(manifest)) return;
|
||||||
this.runtime.updateUiState(manifest.id, { zoom: 1 });
|
this.runtime.updateUiState(manifest.id, { zoom: 1 });
|
||||||
this.renderActiveArtifact();
|
if (manifest.artifact?.type === 'mermaid') {
|
||||||
|
if (this.mermaidPanZoom) {
|
||||||
|
this.mermaidPanZoom.zoom(1);
|
||||||
|
this.mermaidPanZoom.resetPan();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.renderActiveArtifact();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadArtifact() {
|
async downloadArtifact() {
|
||||||
const manifest = this.getActiveManifest();
|
const manifest = this.getActiveManifest();
|
||||||
const state = this.runtime.getState(manifest.id);
|
const state = this.runtime.getState(manifest.id);
|
||||||
const id = state.currentArtifactId;
|
const id = state.currentArtifactId;
|
||||||
@@ -1174,11 +1439,12 @@
|
|||||||
const artifact = state.artifacts[id];
|
const artifact = state.artifacts[id];
|
||||||
if (!artifact) return;
|
if (!artifact) return;
|
||||||
|
|
||||||
if (artifact.type === 'svg') {
|
const svgMarkup = await this.getSvgMarkupForArtifact(artifact, manifest);
|
||||||
Utils.downloadFile(artifact.content, `${manifest.id}.svg`, 'image/svg+xml');
|
if (!svgMarkup) {
|
||||||
} else {
|
|
||||||
alert('当前图表不支持导出 SVG,请使用导出图片功能');
|
alert('当前图表不支持导出 SVG,请使用导出图片功能');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
Utils.downloadFile(svgMarkup, `${manifest.id}.svg`, 'image/svg+xml');
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyArtifactImage() {
|
async copyArtifactImage() {
|
||||||
@@ -1189,12 +1455,13 @@
|
|||||||
const artifact = state.artifacts[id];
|
const artifact = state.artifacts[id];
|
||||||
if (!artifact) return;
|
if (!artifact) return;
|
||||||
|
|
||||||
if (artifact.type !== 'svg') {
|
const svgContent = await this.getSvgMarkupForArtifact(artifact, manifest);
|
||||||
|
if (!svgContent) {
|
||||||
alert('暂不支持复制此类型图表到剪贴板');
|
alert('暂不支持复制此类型图表到剪贴板');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const svgBlob = new Blob([artifact.content], {
|
const svgBlob = new Blob([svgContent], {
|
||||||
type: 'image/svg+xml'
|
type: 'image/svg+xml'
|
||||||
});
|
});
|
||||||
const svgUrl = URL.createObjectURL(svgBlob);
|
const svgUrl = URL.createObjectURL(svgBlob);
|
||||||
@@ -1210,7 +1477,14 @@
|
|||||||
ctx.setTransform(this.imageExportScale, 0, 0, this.imageExportScale, 0, 0);
|
ctx.setTransform(this.imageExportScale, 0, 0, this.imageExportScale, 0, 0);
|
||||||
ctx.drawImage(image, 0, 0);
|
ctx.drawImage(image, 0, 0);
|
||||||
|
|
||||||
|
const finalize = () => URL.revokeObjectURL(svgUrl);
|
||||||
|
|
||||||
canvas.toBlob(async (blob) => {
|
canvas.toBlob(async (blob) => {
|
||||||
|
if (!blob) {
|
||||||
|
finalize();
|
||||||
|
alert('复制失败,请稍后再试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const clipboardItem = new ClipboardItem({ 'image/png': blob });
|
const clipboardItem = new ClipboardItem({ 'image/png': blob });
|
||||||
await navigator.clipboard.write([clipboardItem]);
|
await navigator.clipboard.write([clipboardItem]);
|
||||||
@@ -1219,13 +1493,17 @@
|
|||||||
console.error('复制失败:', error);
|
console.error('复制失败:', error);
|
||||||
alert('复制失败,请稍后再试');
|
alert('复制失败,请稍后再试');
|
||||||
} finally {
|
} finally {
|
||||||
URL.revokeObjectURL(svgUrl);
|
finalize();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
image.onerror = () => {
|
||||||
|
URL.revokeObjectURL(svgUrl);
|
||||||
|
alert('复制失败,请稍后再试');
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
exportArtifactAsImage() {
|
async exportArtifactAsImage() {
|
||||||
const manifest = this.getActiveManifest();
|
const manifest = this.getActiveManifest();
|
||||||
const state = this.runtime.getState(manifest.id);
|
const state = this.runtime.getState(manifest.id);
|
||||||
const id = state.currentArtifactId;
|
const id = state.currentArtifactId;
|
||||||
@@ -1233,8 +1511,9 @@
|
|||||||
const artifact = state.artifacts[id];
|
const artifact = state.artifacts[id];
|
||||||
if (!artifact) return;
|
if (!artifact) return;
|
||||||
|
|
||||||
if (artifact.type === 'svg') {
|
const svgContent = await this.getSvgMarkupForArtifact(artifact, manifest);
|
||||||
const svgBlob = new Blob([artifact.content], {
|
if (svgContent) {
|
||||||
|
const svgBlob = new Blob([svgContent], {
|
||||||
type: 'image/svg+xml'
|
type: 'image/svg+xml'
|
||||||
});
|
});
|
||||||
const svgUrl = URL.createObjectURL(svgBlob);
|
const svgUrl = URL.createObjectURL(svgBlob);
|
||||||
@@ -1257,7 +1536,14 @@
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(svgUrl);
|
URL.revokeObjectURL(svgUrl);
|
||||||
};
|
};
|
||||||
} else if (artifact.type === 'echarts-option') {
|
image.onerror = () => {
|
||||||
|
URL.revokeObjectURL(svgUrl);
|
||||||
|
alert('导出图片失败,请稍后再试');
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artifact.type === 'echarts-option') {
|
||||||
if (!this.echartsInstance) {
|
if (!this.echartsInstance) {
|
||||||
alert('图表实例未准备好,无法导出');
|
alert('图表实例未准备好,无法导出');
|
||||||
return;
|
return;
|
||||||
@@ -1336,7 +1622,8 @@
|
|||||||
const state = this.runtime.getState(manifest.id);
|
const state = this.runtime.getState(manifest.id);
|
||||||
const hasArtifact = !!state.currentArtifactId;
|
const hasArtifact = !!state.currentArtifactId;
|
||||||
|
|
||||||
if (manifest.artifact?.type !== 'svg') {
|
const isZoomable = this.isZoomableManifest(manifest);
|
||||||
|
if (!isZoomable) {
|
||||||
this.el.zoomInBtn && (this.el.zoomInBtn.disabled = true);
|
this.el.zoomInBtn && (this.el.zoomInBtn.disabled = true);
|
||||||
this.el.zoomOutBtn && (this.el.zoomOutBtn.disabled = true);
|
this.el.zoomOutBtn && (this.el.zoomOutBtn.disabled = true);
|
||||||
this.el.zoomResetBtn && (this.el.zoomResetBtn.disabled = true);
|
this.el.zoomResetBtn && (this.el.zoomResetBtn.disabled = true);
|
||||||
@@ -1362,7 +1649,6 @@
|
|||||||
this.el.copyImageBtn.disabled =
|
this.el.copyImageBtn.disabled =
|
||||||
!hasArtifact ||
|
!hasArtifact ||
|
||||||
!this.copyClipboardSupported ||
|
!this.copyClipboardSupported ||
|
||||||
manifest.artifact?.type !== 'svg' ||
|
|
||||||
manifest.exports?.allowClipboard === false;
|
manifest.exports?.allowClipboard === false;
|
||||||
}
|
}
|
||||||
if (this.el.exportImageBtn) {
|
if (this.el.exportImageBtn) {
|
||||||
@@ -1388,6 +1674,7 @@
|
|||||||
this.echartsInstance.dispose();
|
this.echartsInstance.dispose();
|
||||||
this.echartsInstance = null;
|
this.echartsInstance = null;
|
||||||
}
|
}
|
||||||
|
this.destroyMermaidPanZoom();
|
||||||
this.renderConversationHistory();
|
this.renderConversationHistory();
|
||||||
this.showViewerPlaceholder(manifest.ui?.placeholderText || '');
|
this.showViewerPlaceholder(manifest.ui?.placeholderText || '');
|
||||||
this.updateToolbarState();
|
this.updateToolbarState();
|
||||||
|
|||||||
58
js/modules/mermaid.js
Normal file
58
js/modules/mermaid.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
(function registerMermaidModule(global) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (!global.ModuleRegistry) {
|
||||||
|
throw new Error('ModuleRegistry 未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
const MERMAID_FENCE = /```mermaid\s*([\s\S]*?)```/i;
|
||||||
|
|
||||||
|
const parseResponse = (content = '') => {
|
||||||
|
const match = content.match(MERMAID_FENCE);
|
||||||
|
if (match) {
|
||||||
|
const beforeText = content.substring(0, match.index).trim();
|
||||||
|
const afterText = content.substring(match.index + match[0].length).trim();
|
||||||
|
const code = match[1].trim();
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
beforeText,
|
||||||
|
afterText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
beforeText: content.trim(),
|
||||||
|
afterText: ''
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
global.ModuleRegistry.register({
|
||||||
|
id: 'mermaid',
|
||||||
|
label: 'Mermaid 图示',
|
||||||
|
icon: 'ph:circles-three-plus-duotone',
|
||||||
|
renderer: 'mermaid',
|
||||||
|
promptKey: 'mermaid',
|
||||||
|
storageNamespace: 'module:mermaid',
|
||||||
|
chat: {
|
||||||
|
placeholder: '描述你想生成的流程图、时序图或思维导图…',
|
||||||
|
streamStartToken: '```mermaid',
|
||||||
|
contextWindow: 8
|
||||||
|
},
|
||||||
|
artifact: {
|
||||||
|
type: 'mermaid',
|
||||||
|
fence: 'mermaid',
|
||||||
|
startPattern: /```mermaid/i,
|
||||||
|
parser: parseResponse
|
||||||
|
},
|
||||||
|
hooks: {},
|
||||||
|
exports: {
|
||||||
|
allowSvg: true,
|
||||||
|
allowPng: true,
|
||||||
|
allowClipboard: true,
|
||||||
|
allowCode: true
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
placeholderText: '生成的 Mermaid 图示将在此处显示'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})(window);
|
||||||
1646
js/vendor/mermaid.min.js
vendored
Normal file
1646
js/vendor/mermaid.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
js/vendor/svg-pan-zoom.min.js
vendored
Normal file
3
js/vendor/svg-pan-zoom.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
38
prompts/mermaid-prompt.txt
Normal file
38
prompts/mermaid-prompt.txt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
你是一个 mermaid 编写专家,我会给你提供一个场景,你来输出 mermaid 代码,只输出代码即可,不要输出其他的内容,无论我提出什么你都要以 mermaid 代码格式回答我;如果我没指定 mermaid 图的类型,默认使用 `flowchart LR`。
|
||||||
|
|
||||||
|
语法规则库 ()
|
||||||
|
"Mermaid 语法规则和最佳实践"
|
||||||
|
'((特殊字符处理 . "使用双引号包裹含有特殊字符或空格的文本")
|
||||||
|
(HTML实体编码 . "< > & # 等字符使用 HTML 实体编码")
|
||||||
|
(节点命名 . "使用简洁有意义的 ID,避免中文 ID")
|
||||||
|
(连接符规范 . "flowchart: --> | sequenceDiagram: ->> | classDiagram: --|>")
|
||||||
|
(注释规范 . "使用 %% 添加注释说明")
|
||||||
|
(序号处理 . "序号后不要跟空格,如 1.xxx 而非 1. xxx")
|
||||||
|
(颜色分层 . "使用不同背景色区分层级和分组"))
|
||||||
|
|
||||||
|
图表类型映射 ()
|
||||||
|
"定义支持的图表类型及其特征"
|
||||||
|
'((flowchart . (关键词 ("流程" "步骤" "过程" "决策" "分支")
|
||||||
|
语法 "flowchart TD"
|
||||||
|
适用场景 "业务流程、决策树、算法步骤"))
|
||||||
|
(sequenceDiagram . (关键词 ("交互" "通信" "调用" "请求" "响应")
|
||||||
|
语法 "sequenceDiagram"
|
||||||
|
适用场景 "系统交互、API调用、用户操作"))
|
||||||
|
(classDiagram . (关键词 ("类" "对象" "继承" "关系" "属性" "方法")
|
||||||
|
语法 "classDiagram"
|
||||||
|
适用场景 "系统设计、数据模型、架构图"))
|
||||||
|
(stateDiagram . (关键词 ("状态" "转换" "事件" "条件")
|
||||||
|
语法 "stateDiagram-v2"
|
||||||
|
适用场景 "状态机、业务状态、流程状态"))
|
||||||
|
(gantt . (关键词 ("时间" "计划" "任务" "进度" "里程碑")
|
||||||
|
语法 "gantt"
|
||||||
|
适用场景 "项目管理、时间规划、任务安排"))
|
||||||
|
(pie . (关键词 ("比例" "占比" "分布" "百分比")
|
||||||
|
语法 "pie"
|
||||||
|
适用场景 "数据分析、统计展示、比例关系")))
|
||||||
|
|
||||||
|
(专业领域 . '(流程图 时序图 类图 状态图 甘特图 饼图))
|
||||||
|
(核心能力 . '(文本分析 结构识别 语法生成 错误修复))
|
||||||
|
(技术特长 . '(Mermaid语法 图表设计 可视化 代码优化))
|
||||||
|
(工作原则 . '(准确理解 智能选择 规范输出 易于理解))
|
||||||
|
(输出标准 . '(语法正确 结构清晰 美观实用 可直接使用))
|
||||||
Reference in New Issue
Block a user