(function (global) {
'use strict';
const STREAM_DEFAULT_OPTIONS = {
maxTokens: 30000,
temperature: 0.7
};
class AppShell {
constructor({ apiClient, moduleRuntime }) {
if (!apiClient) throw new Error('AppShell 初始化失败:缺少 apiClient');
if (!moduleRuntime)
throw new Error('AppShell 初始化失败:缺少 moduleRuntime');
this.apiClient = apiClient;
this.runtime = moduleRuntime;
this.conversationService = moduleRuntime.getConversationService();
this.el = {};
this.moduleButtons = new Map();
this.copyClipboardSupported =
typeof ClipboardItem !== 'undefined' && !!navigator.clipboard;
const deviceScale =
typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;
this.imageExportScale = Math.min(4, Math.max(2, deviceScale));
this.isProcessing = false;
this.activeStreamHandle = null;
this.pendingCancel = false;
this.manualAbortRequested = false;
this.streamState = null;
this.echartsInstance = null;
this.mermaidPanZoom = null;
this.mermaidInitialized = false;
this.globalStore = moduleRuntime.storageService.global();
this.activeModuleId = null;
this.initElements();
this.bindGlobalEvents();
this.setupModuleSwitcher();
this.restoreLastModule();
this.setSendButtonState('idle');
}
initElements() {
this.el.pageTitle = document.getElementById('page-title');
this.el.moduleButtonGroup = document.getElementById(
'module-button-group'
);
// 对话相关
this.el.chatInput = document.getElementById('chat-input');
this.el.chatQuickActions = document.getElementById('chat-quick-actions');
this.el.sendButton = document.getElementById('send-button');
this.el.clearHistoryBtn = document.getElementById('clear-history-btn');
this.el.chatHistory = document.getElementById('chat-history');
// 视图区域
this.el.viewer = document.getElementById('svg-viewer');
this.el.placeholderText =
this.el.viewer && this.el.viewer.querySelector('#placeholder-text');
// 工具栏
this.el.zoomOutBtn = document.getElementById('zoom-out-btn');
this.el.zoomInBtn = document.getElementById('zoom-in-btn');
this.el.zoomResetBtn = document.getElementById('zoom-reset-btn');
this.el.downloadSvgBtn = document.getElementById('download-svg-btn');
this.el.copyImageBtn = document.getElementById('copy-image-btn');
this.el.exportImageBtn = document.getElementById('export-image-btn');
this.el.openPageBtn = document.getElementById('open-page-btn');
this.el.viewCodeBtn = document.getElementById('view-code-btn');
const toolbarContainer = this.el.viewCodeBtn?.parentElement;
if (toolbarContainer) {
let editBtn = toolbarContainer.querySelector('#edit-mermaid-btn');
if (!editBtn) {
editBtn = document.createElement('button');
editBtn.id = 'edit-mermaid-btn';
editBtn.type = 'button';
editBtn.className =
'p-2 bg-purple-500 text-white border-2 border-black hover:bg-purple-600 transition-all flex items-center gap-1 hidden';
editBtn.title = '在 Mermaid Live 中编辑当前图表';
editBtn.innerHTML =
'在Mermaid中编辑';
toolbarContainer.appendChild(editBtn);
}
this.el.editMermaidBtn = editBtn;
} else {
this.el.editMermaidBtn = null;
}
// 配置模态框
this.el.settingsBtn = document.getElementById('settings-btn');
this.el.configModal = document.getElementById('config-modal');
this.el.closeModalBtn = document.getElementById('close-modal-btn');
this.el.apiUrlInput = document.getElementById('api-url');
this.el.apiKeyInput = document.getElementById('api-key');
this.el.apiModelInput = document.getElementById('api-model');
this.el.testApiBtn = document.getElementById('test-api-btn');
this.el.saveConfigBtn = document.getElementById('save-config-btn');
this.el.configStatus = document.getElementById('config-status');
this.el.statusText = document.getElementById('status-text');
this.el.codeModal = document.getElementById('code-modal');
this.el.codeContent = document.getElementById('code-content');
this.el.copyCodeBtn = document.getElementById('copy-code-btn');
this.el.closeCodeModalBtn = document.getElementById('close-code-modal-btn');
this.el.pagePreviewModal = document.getElementById('page-preview-modal');
this.el.pagePreviewIframe = document.getElementById('page-preview-iframe');
this.el.closePagePreviewBtn = document.getElementById(
'close-page-preview-btn'
);
this.el.pagePreviewNewTabBtn = document.getElementById(
'page-preview-newtab-btn'
);
// 复制按钮可用性
if (this.el.copyImageBtn && !this.copyClipboardSupported) {
this.el.copyImageBtn.disabled = true;
this.el.copyImageBtn.title = '当前浏览器不支持复制图片到剪贴板';
}
}
bindGlobalEvents() {
if (this.el.sendButton) {
this.el.sendButton.addEventListener('click', () => {
if (this.isProcessing) {
this.cancelActiveStream();
} else {
this.sendMessage();
}
});
}
if (this.el.chatInput) {
this.el.chatInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (!this.isProcessing) {
this.sendMessage();
}
}
});
this.el.chatInput.addEventListener('input', () => {
Utils.autoResizeTextarea(this.el.chatInput);
});
}
if (this.el.chatQuickActions) {
this.el.chatQuickActions.addEventListener('click', (event) => {
const actionBtn = event.target.closest('[data-quick-value]');
if (!actionBtn) return;
event.preventDefault();
const quickValue = actionBtn.dataset.quickValue || '';
if (this.el.chatInput) {
this.el.chatInput.value = quickValue;
Utils.autoResizeTextarea(this.el.chatInput);
this.el.chatInput.focus();
}
});
}
if (this.el.clearHistoryBtn) {
this.el.clearHistoryBtn.addEventListener('click', () =>
this.clearCurrentConversation()
);
}
if (this.el.zoomInBtn) {
this.el.zoomInBtn.addEventListener('click', () => this.adjustZoom(0.25));
}
if (this.el.zoomOutBtn) {
this.el.zoomOutBtn.addEventListener('click', () =>
this.adjustZoom(-0.25)
);
}
if (this.el.zoomResetBtn) {
this.el.zoomResetBtn.addEventListener('click', () =>
this.resetZoom()
);
}
if (this.el.downloadSvgBtn) {
this.el.downloadSvgBtn.addEventListener('click', () =>
this.downloadArtifact().catch((error) =>
console.error('下载SVG失败:', error)
)
);
}
if (this.el.copyImageBtn) {
this.el.copyImageBtn.addEventListener('click', () =>
this.copyArtifactImage().catch((error) =>
console.error('复制图片失败:', error)
)
);
}
if (this.el.exportImageBtn) {
this.el.exportImageBtn.addEventListener('click', () =>
this.exportArtifactAsImage().catch((error) =>
console.error('导出图片失败:', error)
)
);
}
if (this.el.openPageBtn) {
this.el.openPageBtn.addEventListener('click', () =>
this.openPagePreview()
);
}
if (this.el.viewCodeBtn) {
this.el.viewCodeBtn.addEventListener('click', () =>
this.viewArtifactCode()
);
}
if (this.el.editMermaidBtn) {
this.el.editMermaidBtn.addEventListener('click', () =>
this.openMermaidLiveEditor()
);
}
if (this.el.settingsBtn) {
this.el.settingsBtn.addEventListener('click', () =>
this.openConfigModal()
);
}
if (this.el.closeModalBtn) {
this.el.closeModalBtn.addEventListener('click', () =>
this.closeConfigModal()
);
}
if (this.el.configModal) {
this.el.configModal.addEventListener('click', (event) => {
if (event.target === this.el.configModal) {
this.closeConfigModal();
}
});
}
if (this.el.testApiBtn) {
this.el.testApiBtn.addEventListener('click', () => this.testAPI());
}
if (this.el.saveConfigBtn) {
this.el.saveConfigBtn.addEventListener('click', () => this.saveAPI());
}
if (this.el.chatHistory) {
this.el.chatHistory.addEventListener('click', (event) => {
const actionBtn = event.target.closest('[data-action]');
if (!actionBtn) return;
event.preventDefault();
const action = actionBtn.dataset.action;
const messageId = actionBtn.dataset.messageId;
this.handleMessageAction(action, messageId);
});
}
if (this.el.copyCodeBtn) {
this.el.copyCodeBtn.addEventListener('click', () =>
this.copyCodeContent()
);
}
if (this.el.closeCodeModalBtn) {
this.el.closeCodeModalBtn.addEventListener('click', () =>
this.closeCodeModal()
);
}
if (this.el.closePagePreviewBtn) {
this.el.closePagePreviewBtn.addEventListener('click', () =>
this.closePagePreview()
);
}
if (this.el.pagePreviewModal) {
this.el.pagePreviewModal.addEventListener('click', (event) => {
if (event.target === this.el.pagePreviewModal) {
this.closePagePreview();
}
});
}
if (this.el.pagePreviewNewTabBtn) {
this.el.pagePreviewNewTabBtn.addEventListener('click', () =>
this.openPageInNewTab()
);
}
if (this.el.codeModal) {
this.el.codeModal.addEventListener('click', (event) => {
if (event.target === this.el.codeModal) {
this.closeCodeModal();
}
});
}
}
setupModuleSwitcher() {
const manifests = this.runtime.listManifests();
if (!manifests.length) {
throw new Error('未找到可用模块,请确认模块清单是否注册');
}
if (!this.el.moduleButtonGroup) {
console.warn('模块按钮容器缺失,无法渲染模块切换按钮');
return;
}
this.el.moduleButtonGroup.innerHTML = '';
this.moduleButtons.clear();
manifests.forEach((manifest) => {
const button = document.createElement('button');
button.className =
'module-btn bg-white text-gray-700 px-4 py-2 font-bold border-2 border-black hover:bg-gray-100 transition-all duration-200 flex items-center gap-1';
button.dataset.moduleId = manifest.id;
button.innerHTML = `
${manifest.label}
`;
button.addEventListener('click', () =>
this.activateModule(manifest.id)
);
this.el.moduleButtonGroup.appendChild(button);
this.moduleButtons.set(manifest.id, button);
});
}
restoreLastModule() {
const manifests = this.runtime.listManifests();
const preferredId =
this.globalStore.get('activeModuleId', null) ||
(manifests[0] && manifests[0].id);
const fallbackId = manifests[0].id;
this.activateModule(
this.runtime.registry.has(preferredId) ? preferredId : fallbackId
);
}
activateModule(moduleId) {
if (this.activeModuleId === moduleId) {
return;
}
const context = this.runtime.activate(moduleId);
this.activeModuleId = moduleId;
this.globalStore.set('activeModuleId', moduleId);
this.highlightActiveModuleButton();
this.updateModuleUI(context.manifest);
this.loadApiConfig();
this.renderConversationHistory();
this.renderActiveArtifact();
this.updateToolbarState();
}
highlightActiveModuleButton() {
this.moduleButtons.forEach((button, id) => {
if (id === this.activeModuleId) {
button.classList.add('mode-btn-active');
button.classList.remove('mode-btn-inactive');
} else {
button.classList.add('mode-btn-inactive');
button.classList.remove('mode-btn-active');
}
});
}
updateModuleUI(manifest) {
if (this.el.pageTitle) {
this.el.pageTitle.textContent = manifest.label;
}
if (this.el.chatInput && manifest.chat?.placeholder) {
this.el.chatInput.placeholder = manifest.chat.placeholder;
}
this.renderQuickActions(manifest.ui?.quickActions || []);
this.showViewerPlaceholder(manifest.ui?.placeholderText || '');
if (this.el.openPageBtn) {
const isHtmlModule = manifest.artifact?.type === 'html';
this.el.openPageBtn.classList.toggle('hidden', !isHtmlModule);
}
}
showViewerPlaceholder(text) {
if (!this.el.viewer) return;
this.el.viewer.innerHTML = `
`;
this.el.placeholderText =
this.el.viewer && this.el.viewer.querySelector('#placeholder-text');
}
renderQuickActions(actions = []) {
if (!this.el.chatQuickActions) return;
const container = this.el.chatQuickActions;
container.innerHTML = '';
if (!actions.length) {
container.classList.add('hidden');
return;
}
container.classList.remove('hidden');
actions.forEach((action) => {
const button = document.createElement('button');
button.type = 'button';
button.className =
'quick-action-btn bg-white text-gray-700 px-3 py-1 text-sm border-2 border-black font-semibold hover:bg-gray-100 transition-all duration-200';
button.dataset.quickValue = action.value || '';
button.textContent = action.label || action.value || '快捷选项';
container.appendChild(button);
});
}
getActiveManifest() {
return this.runtime.getManifest(this.activeModuleId);
}
getCurrentHistory() {
return this.conversationService.getHistory(this.getActiveManifest());
}
renderConversationHistory() {
if (!this.el.chatHistory) return;
const history = this.getCurrentHistory();
this.el.chatHistory.innerHTML = '';
const manifest = this.getActiveManifest();
if (!history.length) {
const welcome = document.createElement('div');
welcome.className = 'flex justify-start';
welcome.innerHTML = `
👋 欢迎使用 ${manifest.label} 助手!请输入需求,我会结合模块特性生成图表与分析。
`;
this.el.chatHistory.appendChild(welcome);
return;
}
let lastAssistantLikeId = null;
for (let i = history.length - 1; i >= 0; i -= 1) {
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: isAiMessage,
allowRegenerate:
isAssistantLike && message.id === lastAssistantLikeId,
allowDelete: true
});
this.el.chatHistory.appendChild(bubble);
});
this.highlightActivePlaceholder();
Utils.scrollToBottom(this.el.chatHistory);
}
buildMessageBubble(message, options = {}) {
const wrapper = document.createElement('div');
const manifest = this.getActiveManifest();
const actionsHtml = this.buildMessageActions(message, options);
if (message.type === 'user') {
wrapper.className = 'flex justify-end';
wrapper.innerHTML = `
${Utils.escapeHtml(message.content)}
${actionsHtml}
`;
} else if (message.type === 'error') {
wrapper.className = 'flex justify-start';
wrapper.innerHTML = `
${Utils.escapeHtml(message.content)}
${actionsHtml}
`;
} else {
wrapper.className = 'flex justify-start';
const parsedContent =
typeof marked !== 'undefined'
? marked.parse(message.content)
: Utils.escapeHtml(message.content);
const artifactLabel = manifest.label || '图表';
const artifactHtml = message.artifactId
? `
📊 点击查看${artifactLabel}
`
: '';
wrapper.innerHTML = `
${parsedContent}
${artifactHtml}
${actionsHtml}
`;
if (message.artifactId) {
const placeholder = wrapper.querySelector('.svg-placeholder-block');
placeholder?.addEventListener('click', () =>
this.renderArtifact(message.artifactId)
);
}
}
return wrapper;
}
buildMessageActions(message, options = {}) {
const {
allowRollback = false,
allowRegenerate = false,
allowDelete = true
} = options;
const actions = [];
if (allowRollback) {
actions.push(`
`);
}
if (allowRegenerate) {
actions.push(`
`);
}
if (allowDelete) {
actions.push(`
`);
}
if (!actions.length) {
return '';
}
return `
${actions.join('')}
`;
}
handleMessageAction(action, messageId) {
if (!action || !messageId) return;
switch (action) {
case 'delete-message':
this.deleteMessage(messageId);
break;
case 'rollback-message':
this.rollbackMessage(messageId);
break;
case 'regenerate-message':
this.regenerateMessage(messageId);
break;
default:
break;
}
}
deleteMessage(messageId) {
const manifest = this.getActiveManifest();
const history = this.conversationService.getHistory(manifest);
const index = history.findIndex((msg) => msg.id === messageId);
if (index === -1) {
alert('未找到要删除的消息,请重试。');
return;
}
const target = history[index];
const typeLabel =
target.type === 'user'
? '这条用户消息'
: target.type === 'ai'
? '这条AI回复'
: '这条提示';
if (
!confirm(
`${typeLabel}删除后无法恢复,确定要删除吗?`
)
) {
return;
}
const removed = history.splice(index, 1);
this.conversationService.saveHistory(manifest, history);
this.removeArtifactsForMessages(manifest.id, removed);
this.ensureActiveArtifact(manifest);
this.renderConversationHistory();
this.renderActiveArtifact();
}
rollbackMessage(messageId) {
const manifest = this.getActiveManifest();
const history = this.conversationService.getHistory(manifest);
const index = history.findIndex((msg) => msg.id === messageId);
if (index === -1) {
alert('未找到指定消息,请重试。');
return;
}
const target = history[index];
if (target.type !== 'ai') {
alert('只能退回到AI生成的消息。');
return;
}
if (index === history.length - 1) {
alert('该消息已是最新内容,无需退回。');
return;
}
if (
!confirm(
'退回将删除此消息之后的所有对话与图形,是否继续?'
)
) {
return;
}
const removed = history.splice(index + 1);
if (!removed.length) {
return;
}
this.conversationService.saveHistory(manifest, history);
this.removeArtifactsForMessages(manifest.id, removed);
this.ensureActiveArtifact(manifest);
this.renderConversationHistory();
this.renderActiveArtifact();
}
regenerateMessage(messageId) {
if (this.isProcessing) {
alert('当前仍在生成中,请稍后再试。');
return;
}
const manifest = this.getActiveManifest();
const history = this.conversationService.getHistory(manifest);
const index = history.findIndex((msg) => msg.id === messageId);
if (index === -1) {
alert('未找到指定的回复。');
return;
}
const target = history[index];
const isAssistantLike = target.type === 'ai' || target.type === 'error';
if (!isAssistantLike) {
alert('只能对AI回复或错误提示执行重新生成。');
return;
}
if (index !== history.length - 1) {
alert('请先使用退回功能,确保该回复位于对话末尾后再重新生成。');
return;
}
let userIndex = -1;
for (let i = index - 1; i >= 0; i -= 1) {
if (history[i].type === 'user') {
userIndex = i;
break;
}
}
if (userIndex === -1) {
alert('未找到对应的用户消息,无法重新生成。');
return;
}
const userMessage = history[userIndex];
const removed = history.splice(index, 1);
this.conversationService.saveHistory(manifest, history);
this.removeArtifactsForMessages(manifest.id, removed);
this.ensureActiveArtifact(manifest);
this.renderConversationHistory();
this.renderActiveArtifact();
const contextMessages = history
.slice(0, userIndex)
.filter((msg) => msg.type === 'user' || msg.type === 'ai')
.map((msg) => ({
role: msg.type === 'user' ? 'user' : 'assistant',
// 使用原始内容作为 LLM 上下文,兼容无 rawContent 的旧记录
content: msg.rawContent || msg.content || ''
}));
this.isProcessing = true;
this.pendingCancel = false;
this.setSendButtonState('streaming');
this.el.sendButton.disabled = false;
this.beginStreaming(manifest, {
userMessage,
contextMessages
});
}
removeArtifactsForMessages(moduleId, messages = []) {
if (!messages.length) return;
const artifacts = this.runtime.getArtifacts(moduleId);
const idsToRemove = new Set();
messages.forEach((msg) => {
if (msg.artifactId && artifacts[msg.artifactId]) {
idsToRemove.add(msg.artifactId);
}
});
const messageIdSet = new Set(messages.map((msg) => msg.id));
Object.entries(artifacts).forEach(([id, artifact]) => {
if (artifact.messageId && messageIdSet.has(artifact.messageId)) {
idsToRemove.add(id);
}
});
idsToRemove.forEach((id) => this.runtime.removeArtifact(moduleId, id));
}
ensureActiveArtifact(manifest) {
const state = this.runtime.getState(manifest.id);
const artifacts = state.artifacts || {};
if (state.currentArtifactId && artifacts[state.currentArtifactId]) {
return state.currentArtifactId;
}
const history = this.conversationService.getHistory(manifest);
let nextId = null;
for (let i = history.length - 1; i >= 0; i -= 1) {
const candidateId = history[i].artifactId;
if (candidateId && artifacts[candidateId]) {
nextId = candidateId;
break;
}
}
this.runtime.setActiveArtifact(manifest.id, nextId);
return nextId;
}
sendMessage() {
if (!this.el.chatInput) return;
const message = this.el.chatInput.value.trim();
if (!message || this.isProcessing) return;
if (!this.apiClient.isConfigValid()) {
alert('⚠️ 请先配置API设置!点击右上角齿轮图标进行配置。');
this.openConfigModal();
return;
}
const manifest = this.getActiveManifest();
const userMessage = {
id: Utils.generateId('msg'),
type: 'user',
content: message,
timestamp: new Date().toISOString()
};
this.conversationService.appendMessage(manifest, userMessage);
this.renderConversationHistory();
this.el.chatInput.value = '';
Utils.autoResizeTextarea(this.el.chatInput);
const context = this.conversationService.buildContext(manifest);
if (!context) {
console.warn('无法构建上下文,终止发送');
return;
}
this.isProcessing = true;
this.pendingCancel = false;
this.setSendButtonState('streaming');
this.el.sendButton.disabled = false;
this.beginStreaming(manifest, {
userMessage: context.userMessage,
contextMessages: context.contextMessages
});
}
beginStreaming(manifest, payload) {
this.manualAbortRequested = false;
const messageId = Utils.generateId('msg');
const container = this.createStreamingContainer(messageId);
this.el.chatHistory.appendChild(container);
Utils.scrollToBottom(this.el.chatHistory);
let fullContent = '';
const streamState = {
manifestId: manifest.id,
messageId,
container,
svg: null,
html: null
};
this.streamState = streamState;
const finalize = ({ aborted = false } = {}) => {
if (!this.isProcessing) return;
this.isProcessing = false;
this.setSendButtonState('idle');
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;
}
this.finalizeAssistantMessage(
manifest,
messageId,
fullContent,
streamState
);
};
const handleChunk = (chunk) => {
const delta = chunk?.choices?.[0]?.delta?.content || '';
if (!delta) return;
fullContent += delta;
this.updateStreamingContent(container, fullContent, streamState, manifest);
if (manifest.artifact?.type === 'svg') {
this.processSvgStreamChunk(manifest, fullContent, streamState);
} else if (manifest.artifact?.type === 'mermaid') {
this.processMermaidStreamChunk(manifest, fullContent, streamState);
} else if (manifest.artifact?.type === 'html') {
this.processHtmlStreamChunk(manifest, fullContent, streamState);
}
};
const handleComplete = (info) => {
finalize(info);
};
this.apiClient
.generateModuleStream(
manifest,
payload.userMessage.content,
payload.contextMessages,
handleChunk,
handleComplete,
STREAM_DEFAULT_OPTIONS
)
.then((streamHandle) => {
this.activeStreamHandle = streamHandle;
if (this.pendingCancel) {
this.pendingCancel = false;
this.cancelActiveStream();
}
return streamHandle.finished;
})
.then(() => finalize({ aborted: false }))
.catch((error) => {
console.error('发送消息失败:', error);
if (this.streamState && this.streamState === streamState) {
this.streamState = null;
}
finalize({ aborted: true });
this.addErrorMessage(
error.message || '生成失败,请稍后再试',
manifest
);
});
}
createStreamingContainer(messageId) {
const messageDiv = document.createElement('div');
messageDiv.className = 'flex justify-start';
messageDiv.dataset.messageId = messageId;
messageDiv.innerHTML = `
`;
return messageDiv;
}
updateStreamingContent(container, content, streamContext = null, manifest = null) {
const cursor = container.querySelector('.typing-cursor');
if (!cursor) return;
// 对于 HTML 模块,HTML 内容已经在右侧预览区域实时渲染,
// 为避免在对话气泡中直接注入 / 文档结构,这里不再渲染流式内容。
if (manifest?.artifact?.type === 'html') {
return;
}
const displayContent = content;
if (typeof marked !== 'undefined') {
cursor.innerHTML = marked.parse(displayContent);
} else {
cursor.textContent = displayContent;
}
Utils.scrollToBottom(this.el.chatHistory);
}
finalizeAssistantMessage(
manifest,
messageId,
fullContent,
streamContext = null
) {
const container = this.el.chatHistory.querySelector(
`[data-message-id="${messageId}"]`
);
if (container) {
container.remove();
}
const timestamp = new Date().toISOString();
let artifactId = streamContext?.svg?.artifactId || null;
let artifactPayload = null;
let parsedResult = null;
if (manifest.artifact?.parser) {
try {
parsedResult = manifest.artifact.parser(fullContent);
if (manifest.artifact.type === 'svg' && parsedResult.svgContent) {
artifactId = Utils.generateId('svg');
const svgBody = parsedResult.svgContent.trim().endsWith('')
? parsedResult.svgContent.trim()
: `${parsedResult.svgContent.trim()}\n`;
artifactPayload = {
id: artifactId,
type: manifest.artifact.type,
content: svgBody,
messageId,
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 (
manifest.artifact.type === 'echarts-option' &&
parsedResult.option
) {
artifactId = Utils.generateId('chart');
artifactPayload = {
id: artifactId,
type: manifest.artifact.type,
option: parsedResult.option,
optionText:
parsedResult.optionText ||
JSON.stringify(parsedResult.option),
messageId,
timestamp
};
} else if (
manifest.artifact.type === 'html' &&
parsedResult.htmlContent
) {
artifactId =
streamContext?.html?.artifactId || Utils.generateId('html');
artifactPayload = {
id: artifactId,
type: manifest.artifact.type,
content: parsedResult.htmlContent.trim(),
messageId,
timestamp
};
}
} catch (error) {
console.warn('解析助手内容失败:', error);
}
}
const messageContent = this.buildAssistantDisplayContent(
manifest,
fullContent,
parsedResult,
artifactId
);
const messageRecord = {
id: messageId,
type: 'ai',
// content 用于展示(可能是裁剪/清洗后的文本)
content: messageContent,
// rawContent 保留完整的 LLM 原始响应,用于上下文与重新生成
rawContent: fullContent,
timestamp,
artifactId
};
this.conversationService.appendMessage(manifest, messageRecord);
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 || '生成已被手动终止,您可以点击重新生成继续。',
rawContent: fullContent || 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') {
return marked.parse(text);
}
return Utils.escapeHtml(text);
}
addErrorMessage(errorText, manifest) {
const message = {
id: Utils.generateId('msg'),
type: 'error',
content: errorText,
timestamp: new Date().toISOString()
};
this.conversationService.appendMessage(manifest, message);
this.renderConversationHistory();
}
buildAssistantDisplayContent(manifest, rawContent, parsedResult, artifactId) {
const trim = (text) => (typeof text === 'string' ? text.trim() : '');
const segments = [];
if (parsedResult) {
if (manifest.artifact?.type === 'svg') {
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 === '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') {
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 === 'html') {
const before = trim(parsedResult.beforeText);
const after = trim(parsedResult.afterText);
if (before) segments.push(before);
if (after) segments.push(after);
}
}
let content = segments.filter(Boolean).join('\n\n').trim();
// 对于 HTML 模块,额外清理尾部可能残留的空 ```html 代码块
// 场景:模型输出两个 ```html 代码块,第二个为空;解析器只提取第一个,
// 剩余的空代码块会进入 before/after,最终在气泡中渲染出一个空的 。
if (manifest.artifact?.type === 'html' && content) {
const withoutEmptyHtmlFence = content.replace(
/\n*```(?:html|htm)?\s*```[\s]*$/i,
''
);
if (withoutEmptyHtmlFence.trim()) {
content = withoutEmptyHtmlFence.trim();
}
}
if (content) {
return content;
}
if (artifactId) {
return `已生成 ${manifest.label} 图表,请点击占位卡片查看。`;
}
// HTML 模块:避免将完整 HTML 文档渲染到对话气泡中
if (manifest.artifact?.type === 'html') {
const safeText = (rawContent || '')
// 去掉主/副 ```html 代码块,保留其余说明文字
.replace(/```(?:html|htm)?[\s\S]*?```/gi, '')
.trim();
return safeText || `已生成 ${manifest.label} 页面内容,请查看预览区域。`;
}
return rawContent.trim();
}
processSvgStreamChunk(manifest, fullContent, streamState) {
if (!streamState) {
return;
}
if (!streamState.svg) {
streamState.svg = {
started: false,
completed: false,
artifactId: null,
latestMarkup: ''
};
}
const svgCtx = streamState.svg;
const startPattern =
manifest.artifact?.startPattern || /```(?:svg)?\s*')
? cleaned
: `${cleaned}\n`;
svgCtx.latestMarkup = temporaryMarkup;
this.renderTemporarySvg(temporaryMarkup, true, manifest);
}
}
processHtmlStreamChunk(manifest, fullContent, streamState) {
if (!streamState) return;
if (!streamState.html) {
streamState.html = {
started: false,
artifactId: null,
startIndex: null,
beforeText: '',
latestHtml: '',
completed: false,
completedRendered: false,
nextRenderAt: null
};
}
const ctx = streamState.html;
const startPattern =
manifest.artifact?.startPattern || /```(?:html|htm)/i;
if (!ctx.started) {
const match = fullContent.match(startPattern);
let startIndex = null;
let beforeText = '';
if (match) {
startIndex = match.index + match[0].length;
beforeText = fullContent.substring(0, match.index);
} else {
const fallbackIndex = fullContent.search(/= 0) {
ctx.started = true;
ctx.artifactId = ctx.artifactId || Utils.generateId('html');
ctx.startIndex = startIndex;
ctx.beforeText = beforeText;
this.updateHtmlPlaceholder(streamState.container, manifest, ctx);
this.showViewerStreaming(manifest);
}
}
if (!ctx.started) {
return;
}
if (typeof ctx.startIndex !== 'number' || ctx.startIndex < 0) {
return;
}
let htmlSection = fullContent.substring(ctx.startIndex);
const closingFenceIndex = htmlSection.indexOf('```');
if (closingFenceIndex !== -1) {
ctx.completed = true;
htmlSection = htmlSection.substring(0, closingFenceIndex);
}
const cleaned = htmlSection.trim();
const hasContent = !!cleaned;
const contentChanged = hasContent && cleaned !== ctx.latestHtml;
const shouldRenderFinal = ctx.completed && !ctx.completedRendered;
if (!hasContent && !shouldRenderFinal) {
return;
}
if (!contentChanged && !shouldRenderFinal) {
return;
}
const now = Date.now();
if (!ctx.completed) {
if (ctx.nextRenderAt && now < ctx.nextRenderAt) {
return;
}
ctx.nextRenderAt = now + 250;
} else {
ctx.nextRenderAt = null;
}
if (contentChanged) {
ctx.latestHtml = cleaned;
}
const htmlForPreview = contentChanged ? cleaned : ctx.latestHtml;
streamState.html.htmlContent = htmlForPreview;
this.renderHtmlPreview(htmlForPreview, manifest, {
partial: !ctx.completed
});
if (ctx.completed) {
ctx.completedRendered = true;
}
}
updateStreamingBubbleSvgPlaceholder(container, manifest, svgCtx) {
if (!container) return;
const beforeHtml = this.parseMarkdownContent(svgCtx.beforeText || '');
const label = manifest.label || '图表';
container.innerHTML = `
${beforeHtml}
🎨 正在绘制${label}...
`;
Utils.scrollToBottom(this.el.chatHistory);
}
updateHtmlPlaceholder(container, manifest, ctx) {
if (!container) return;
const beforeHtml = this.parseMarkdownContent(ctx.beforeText || '');
const label = manifest.label || '页面';
container.innerHTML = `
${beforeHtml}
🏗️ 正在构建${label} HTML…
`;
Utils.scrollToBottom(this.el.chatHistory);
}
showViewerStreaming(manifest) {
if (!this.el.viewer) return;
const label = manifest.label || '图表';
const verb = manifest.artifact?.type === 'html' ? '构建' : '绘制';
this.el.viewer.innerHTML = `
`;
}
renderTemporarySvg(svgMarkup, isPartial = false, manifest = null) {
const moduleId = manifest?.id || this.activeModuleId;
this.renderSvgMarkup(svgMarkup, moduleId, {
opacity: isPartial ? 0.9 : 1
});
}
getCurrentEChartsSvgElement() {
if (!this.echartsInstance) return null;
const dom = this.echartsInstance.getDom();
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(/^