• - 在 renderConversationHistory 末尾与 renderArtifact 内部新增 highlightActivePlaceholder(),每次渲染或
切换图形后都会重新标记当前选中的占位卡片。
- 新方法会清除所有 .svg-placeholder-block 上的 svg-placeholder-active,再根据 ModuleRuntime 记录的
currentArtifactId 为对应占位卡片添加该类(js/core/app-shell.js:321, 673, 729)。
- 这样无论是点击左侧占位切换、完成流式渲染或模块切换,右侧当前图形都会同步点亮对应占位符,恢复过往的高
亮效果。
This commit is contained in:
@@ -176,6 +176,17 @@
|
|||||||
if (this.el.saveConfigBtn) {
|
if (this.el.saveConfigBtn) {
|
||||||
this.el.saveConfigBtn.addEventListener('click', () => this.saveAPI());
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupModuleSwitcher() {
|
setupModuleSwitcher() {
|
||||||
@@ -297,22 +308,38 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastAiMessageId = null;
|
||||||
|
for (let i = history.length - 1; i >= 0; i -= 1) {
|
||||||
|
if (history[i].type === 'ai') {
|
||||||
|
lastAiMessageId = history[i].id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
history.forEach((message) => {
|
history.forEach((message) => {
|
||||||
const bubble = this.buildMessageBubble(message);
|
const bubble = this.buildMessageBubble(message, {
|
||||||
|
allowRollback: message.type === 'ai',
|
||||||
|
allowRegenerate:
|
||||||
|
message.type === 'ai' && message.id === lastAiMessageId,
|
||||||
|
allowDelete: true
|
||||||
|
});
|
||||||
this.el.chatHistory.appendChild(bubble);
|
this.el.chatHistory.appendChild(bubble);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.highlightActivePlaceholder();
|
||||||
Utils.scrollToBottom(this.el.chatHistory);
|
Utils.scrollToBottom(this.el.chatHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildMessageBubble(message) {
|
buildMessageBubble(message, options = {}) {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
const manifest = this.getActiveManifest();
|
const manifest = this.getActiveManifest();
|
||||||
|
const actionsHtml = this.buildMessageActions(message, options);
|
||||||
if (message.type === 'user') {
|
if (message.type === 'user') {
|
||||||
wrapper.className = 'flex justify-end';
|
wrapper.className = 'flex justify-end';
|
||||||
wrapper.innerHTML = `
|
wrapper.innerHTML = `
|
||||||
<div class="chat-bubble-user message-with-delete" data-message-id="${message.id}">
|
<div class="chat-bubble-user message-with-delete" data-message-id="${message.id}">
|
||||||
<div>${Utils.escapeHtml(message.content)}</div>
|
<div>${Utils.escapeHtml(message.content)}</div>
|
||||||
|
${actionsHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (message.type === 'error') {
|
} else if (message.type === 'error') {
|
||||||
@@ -323,6 +350,7 @@
|
|||||||
<iconify-icon icon="ph:warning-circle" class="text-red-500 mt-0.5"></iconify-icon>
|
<iconify-icon icon="ph:warning-circle" class="text-red-500 mt-0.5"></iconify-icon>
|
||||||
<span>${Utils.escapeHtml(message.content)}</span>
|
<span>${Utils.escapeHtml(message.content)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
${actionsHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
@@ -331,15 +359,17 @@
|
|||||||
typeof marked !== 'undefined'
|
typeof marked !== 'undefined'
|
||||||
? marked.parse(message.content)
|
? marked.parse(message.content)
|
||||||
: Utils.escapeHtml(message.content);
|
: Utils.escapeHtml(message.content);
|
||||||
|
const artifactLabel = manifest.label || '图表';
|
||||||
const artifactHtml = message.artifactId
|
const artifactHtml = message.artifactId
|
||||||
? `<div class="svg-placeholder-block" data-artifact-id="${message.artifactId}" data-module-id="${manifest.id}">
|
? `<div class="svg-placeholder-block" data-artifact-id="${message.artifactId}" data-module-id="${manifest.id}">
|
||||||
📊 点击查看最新图表
|
📊 点击查看${artifactLabel}
|
||||||
</div>`
|
</div>`
|
||||||
: '';
|
: '';
|
||||||
wrapper.innerHTML = `
|
wrapper.innerHTML = `
|
||||||
<div class="chat-bubble-ai message-with-delete" data-message-id="${message.id}">
|
<div class="chat-bubble-ai message-with-delete" data-message-id="${message.id}">
|
||||||
<div class="message-body">${parsedContent}</div>
|
<div class="message-body">${parsedContent}</div>
|
||||||
${artifactHtml}
|
${artifactHtml}
|
||||||
|
${actionsHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
if (message.artifactId) {
|
if (message.artifactId) {
|
||||||
@@ -352,6 +382,240 @@
|
|||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildMessageActions(message, options = {}) {
|
||||||
|
const {
|
||||||
|
allowRollback = false,
|
||||||
|
allowRegenerate = false,
|
||||||
|
allowDelete = true
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
if (allowRollback) {
|
||||||
|
actions.push(`
|
||||||
|
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors"
|
||||||
|
data-action="rollback-message" data-message-id="${message.id}">
|
||||||
|
<iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
|
||||||
|
<span>退回</span>
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowRegenerate) {
|
||||||
|
actions.push(`
|
||||||
|
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors"
|
||||||
|
data-action="regenerate-message" data-message-id="${message.id}">
|
||||||
|
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon>
|
||||||
|
<span>重新生成</span>
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowDelete) {
|
||||||
|
actions.push(`
|
||||||
|
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-red-600 transition-colors"
|
||||||
|
data-action="delete-message" data-message-id="${message.id}">
|
||||||
|
<iconify-icon icon="ph:trash-simple-bold"></iconify-icon>
|
||||||
|
<span>删除</span>
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!actions.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="message-actions flex gap-2 mt-2 pt-2 border-t border-gray-200">
|
||||||
|
${actions.join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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('未找到指定的AI消息。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = history[index];
|
||||||
|
if (target.type !== 'ai') {
|
||||||
|
alert('只能对AI回复执行重新生成。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index !== history.length - 1) {
|
||||||
|
alert('请先使用退回功能,确保该AI回复位于对话末尾后再重新生成。');
|
||||||
|
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',
|
||||||
|
content: 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() {
|
sendMessage() {
|
||||||
if (!this.el.chatInput) return;
|
if (!this.el.chatInput) return;
|
||||||
const message = this.el.chatInput.value.trim();
|
const message = this.el.chatInput.value.trim();
|
||||||
@@ -387,16 +651,26 @@
|
|||||||
this.setSendButtonState('streaming');
|
this.setSendButtonState('streaming');
|
||||||
this.el.sendButton.disabled = false;
|
this.el.sendButton.disabled = false;
|
||||||
|
|
||||||
this.startStreaming(manifest, context);
|
this.beginStreaming(manifest, {
|
||||||
|
userMessage: context.userMessage,
|
||||||
|
contextMessages: context.contextMessages
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
startStreaming(manifest, context) {
|
beginStreaming(manifest, payload) {
|
||||||
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);
|
||||||
Utils.scrollToBottom(this.el.chatHistory);
|
Utils.scrollToBottom(this.el.chatHistory);
|
||||||
|
|
||||||
let fullContent = '';
|
let fullContent = '';
|
||||||
|
const streamState = {
|
||||||
|
manifestId: manifest.id,
|
||||||
|
messageId,
|
||||||
|
container,
|
||||||
|
svg: null
|
||||||
|
};
|
||||||
|
this.streamState = streamState;
|
||||||
|
|
||||||
const finalize = ({ aborted = false } = {}) => {
|
const finalize = ({ aborted = false } = {}) => {
|
||||||
if (!this.isProcessing) return;
|
if (!this.isProcessing) return;
|
||||||
@@ -405,13 +679,19 @@
|
|||||||
this.el.sendButton.disabled = false;
|
this.el.sendButton.disabled = false;
|
||||||
this.activeStreamHandle = null;
|
this.activeStreamHandle = null;
|
||||||
this.pendingCancel = false;
|
this.pendingCancel = false;
|
||||||
|
this.streamState = null;
|
||||||
|
|
||||||
if (aborted) {
|
if (aborted) {
|
||||||
container.remove();
|
container.remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.finalizeAssistantMessage(manifest, messageId, fullContent);
|
this.finalizeAssistantMessage(
|
||||||
|
manifest,
|
||||||
|
messageId,
|
||||||
|
fullContent,
|
||||||
|
streamState
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChunk = (chunk) => {
|
const handleChunk = (chunk) => {
|
||||||
@@ -419,6 +699,9 @@
|
|||||||
if (!delta) return;
|
if (!delta) return;
|
||||||
fullContent += delta;
|
fullContent += delta;
|
||||||
this.updateStreamingContent(container, fullContent);
|
this.updateStreamingContent(container, fullContent);
|
||||||
|
if (manifest.artifact?.type === 'svg') {
|
||||||
|
this.processSvgStreamChunk(manifest, fullContent, streamState);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleComplete = (info) => {
|
const handleComplete = (info) => {
|
||||||
@@ -428,8 +711,8 @@
|
|||||||
this.apiClient
|
this.apiClient
|
||||||
.generateModuleStream(
|
.generateModuleStream(
|
||||||
manifest,
|
manifest,
|
||||||
context.userMessage.content,
|
payload.userMessage.content,
|
||||||
context.contextMessages,
|
payload.contextMessages,
|
||||||
handleChunk,
|
handleChunk,
|
||||||
handleComplete,
|
handleComplete,
|
||||||
STREAM_DEFAULT_OPTIONS
|
STREAM_DEFAULT_OPTIONS
|
||||||
@@ -445,6 +728,9 @@
|
|||||||
.then(() => finalize({ aborted: false }))
|
.then(() => finalize({ aborted: false }))
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('发送消息失败:', error);
|
console.error('发送消息失败:', error);
|
||||||
|
if (this.streamState && this.streamState === streamState) {
|
||||||
|
this.streamState = null;
|
||||||
|
}
|
||||||
finalize({ aborted: true });
|
finalize({ aborted: true });
|
||||||
this.addErrorMessage(
|
this.addErrorMessage(
|
||||||
error.message || '生成失败,请稍后再试',
|
error.message || '生成失败,请稍后再试',
|
||||||
@@ -476,7 +762,12 @@
|
|||||||
Utils.scrollToBottom(this.el.chatHistory);
|
Utils.scrollToBottom(this.el.chatHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
finalizeAssistantMessage(manifest, messageId, fullContent) {
|
finalizeAssistantMessage(
|
||||||
|
manifest,
|
||||||
|
messageId,
|
||||||
|
fullContent,
|
||||||
|
streamContext = null
|
||||||
|
) {
|
||||||
const container = this.el.chatHistory.querySelector(
|
const container = this.el.chatHistory.querySelector(
|
||||||
`[data-message-id="${messageId}"]`
|
`[data-message-id="${messageId}"]`
|
||||||
);
|
);
|
||||||
@@ -485,17 +776,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
let artifactId = null;
|
let artifactId = streamContext?.svg?.artifactId || null;
|
||||||
let artifactPayload = null;
|
let artifactPayload = null;
|
||||||
|
let parsedResult = null;
|
||||||
|
|
||||||
if (manifest.artifact?.parser) {
|
if (manifest.artifact?.parser) {
|
||||||
try {
|
try {
|
||||||
const parsed = manifest.artifact.parser(fullContent);
|
parsedResult = manifest.artifact.parser(fullContent);
|
||||||
if (manifest.artifact.type === 'svg' && parsed.svgContent) {
|
if (manifest.artifact.type === 'svg' && parsedResult.svgContent) {
|
||||||
artifactId = Utils.generateId('svg');
|
artifactId = Utils.generateId('svg');
|
||||||
const svgBody = parsed.svgContent.trim().endsWith('</svg>')
|
const svgBody = parsedResult.svgContent.trim().endsWith('</svg>')
|
||||||
? parsed.svgContent.trim()
|
? parsedResult.svgContent.trim()
|
||||||
: `${parsed.svgContent.trim()}\n</svg>`;
|
: `${parsedResult.svgContent.trim()}\n</svg>`;
|
||||||
artifactPayload = {
|
artifactPayload = {
|
||||||
id: artifactId,
|
id: artifactId,
|
||||||
type: manifest.artifact.type,
|
type: manifest.artifact.type,
|
||||||
@@ -505,14 +797,16 @@
|
|||||||
};
|
};
|
||||||
} else if (
|
} else if (
|
||||||
manifest.artifact.type === 'echarts-option' &&
|
manifest.artifact.type === 'echarts-option' &&
|
||||||
parsed.option
|
parsedResult.option
|
||||||
) {
|
) {
|
||||||
artifactId = Utils.generateId('chart');
|
artifactId = Utils.generateId('chart');
|
||||||
artifactPayload = {
|
artifactPayload = {
|
||||||
id: artifactId,
|
id: artifactId,
|
||||||
type: manifest.artifact.type,
|
type: manifest.artifact.type,
|
||||||
option: parsed.option,
|
option: parsedResult.option,
|
||||||
optionText: parsed.optionText || JSON.stringify(parsed.option),
|
optionText:
|
||||||
|
parsedResult.optionText ||
|
||||||
|
JSON.stringify(parsedResult.option),
|
||||||
messageId,
|
messageId,
|
||||||
timestamp
|
timestamp
|
||||||
};
|
};
|
||||||
@@ -522,10 +816,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messageContent = this.buildAssistantDisplayContent(
|
||||||
|
manifest,
|
||||||
|
fullContent,
|
||||||
|
parsedResult,
|
||||||
|
artifactId
|
||||||
|
);
|
||||||
|
|
||||||
const messageRecord = {
|
const messageRecord = {
|
||||||
id: messageId,
|
id: messageId,
|
||||||
type: 'ai',
|
type: 'ai',
|
||||||
content: fullContent,
|
content: messageContent,
|
||||||
timestamp,
|
timestamp,
|
||||||
artifactId
|
artifactId
|
||||||
};
|
};
|
||||||
@@ -539,6 +840,14 @@
|
|||||||
this.renderConversationHistory();
|
this.renderConversationHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseMarkdownContent(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
if (typeof marked !== 'undefined') {
|
||||||
|
return marked.parse(text);
|
||||||
|
}
|
||||||
|
return Utils.escapeHtml(text);
|
||||||
|
}
|
||||||
|
|
||||||
addErrorMessage(errorText, manifest) {
|
addErrorMessage(errorText, manifest) {
|
||||||
const message = {
|
const message = {
|
||||||
id: Utils.generateId('msg'),
|
id: Utils.generateId('msg'),
|
||||||
@@ -550,6 +859,140 @@
|
|||||||
this.renderConversationHistory();
|
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 === 'echarts-option') {
|
||||||
|
const before = trim(parsedResult.beforeText);
|
||||||
|
const after = trim(parsedResult.afterText);
|
||||||
|
if (before) segments.push(before);
|
||||||
|
if (after) segments.push(after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = segments.filter(Boolean).join('\n\n').trim();
|
||||||
|
if (content) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artifactId) {
|
||||||
|
return `已生成 ${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*<svg/i;
|
||||||
|
if (!svgCtx.started) {
|
||||||
|
const match = fullContent.match(startPattern);
|
||||||
|
if (match) {
|
||||||
|
svgCtx.started = true;
|
||||||
|
svgCtx.startIndex = match.index;
|
||||||
|
svgCtx.artifactId = svgCtx.artifactId || Utils.generateId('svg');
|
||||||
|
svgCtx.beforeText = fullContent.substring(0, svgCtx.startIndex);
|
||||||
|
this.updateStreamingBubbleSvgPlaceholder(
|
||||||
|
streamState.container,
|
||||||
|
manifest,
|
||||||
|
svgCtx
|
||||||
|
);
|
||||||
|
this.showViewerStreaming(manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!svgCtx.started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const svgSection = fullContent.substring(svgCtx.startIndex);
|
||||||
|
let cleaned = svgSection.replace(/```(?:svg)?\s*/i, '');
|
||||||
|
cleaned = cleaned.replace(/```$/, '');
|
||||||
|
const closingIndex = cleaned.indexOf('</svg>');
|
||||||
|
if (closingIndex !== -1) {
|
||||||
|
svgCtx.completed = true;
|
||||||
|
cleaned = cleaned.substring(0, closingIndex + 6);
|
||||||
|
svgCtx.latestMarkup = cleaned;
|
||||||
|
this.renderTemporarySvg(cleaned, false, manifest);
|
||||||
|
} else if (cleaned.trim()) {
|
||||||
|
const temporaryMarkup = cleaned.endsWith('</svg>')
|
||||||
|
? cleaned
|
||||||
|
: `${cleaned}\n</svg>`;
|
||||||
|
svgCtx.latestMarkup = temporaryMarkup;
|
||||||
|
this.renderTemporarySvg(temporaryMarkup, true, manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStreamingBubbleSvgPlaceholder(container, manifest, svgCtx) {
|
||||||
|
if (!container) return;
|
||||||
|
const beforeHtml = this.parseMarkdownContent(svgCtx.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="${svgCtx.artifactId}">
|
||||||
|
🎨 正在绘制${label}...
|
||||||
|
</div>
|
||||||
|
<div class="typing-cursor"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
Utils.scrollToBottom(this.el.chatHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
showViewerStreaming(manifest) {
|
||||||
|
if (!this.el.viewer) return;
|
||||||
|
const label = manifest.label || '图表';
|
||||||
|
this.el.viewer.innerHTML = `
|
||||||
|
<div class="flex items-center justify-center w-full h-full">
|
||||||
|
<div class="text-center text-gray-600">
|
||||||
|
<iconify-icon icon="ph:spinner-gap" class="text-5xl text-purple-500 animate-spin"></iconify-icon>
|
||||||
|
<p class="mt-4 font-bold">正在绘制${label}...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTemporarySvg(svgMarkup, isPartial = false, manifest = null) {
|
||||||
|
if (!this.el.viewer || !svgMarkup) return;
|
||||||
|
this.el.viewer.innerHTML = '';
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'svg-content-wrapper';
|
||||||
|
wrapper.innerHTML = svgMarkup;
|
||||||
|
this.el.viewer.appendChild(wrapper);
|
||||||
|
if (isPartial) {
|
||||||
|
wrapper.style.opacity = '0.9';
|
||||||
|
} else {
|
||||||
|
wrapper.style.opacity = '1';
|
||||||
|
}
|
||||||
|
const uiState = this.runtime.getUiState(
|
||||||
|
manifest?.id || this.activeModuleId,
|
||||||
|
{ zoom: 1 }
|
||||||
|
);
|
||||||
|
wrapper.style.transform = `scale(${uiState.zoom})`;
|
||||||
|
wrapper.style.transformOrigin = 'center top';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
cancelActiveStream() {
|
cancelActiveStream() {
|
||||||
if (!this.activeStreamHandle || typeof this.activeStreamHandle.cancel !== 'function') {
|
if (!this.activeStreamHandle || typeof this.activeStreamHandle.cancel !== 'function') {
|
||||||
this.pendingCancel = true;
|
this.pendingCancel = true;
|
||||||
@@ -590,6 +1033,7 @@
|
|||||||
} else if (artifact.type === 'echarts-option') {
|
} else if (artifact.type === 'echarts-option') {
|
||||||
this.renderEChartsArtifact(artifact);
|
this.renderEChartsArtifact(artifact);
|
||||||
}
|
}
|
||||||
|
this.highlightActivePlaceholder();
|
||||||
this.updateToolbarState();
|
this.updateToolbarState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,6 +1325,28 @@
|
|||||||
this.updateToolbarState();
|
this.updateToolbarState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
highlightActivePlaceholder() {
|
||||||
|
if (!this.el.chatHistory) return;
|
||||||
|
const placeholders = this.el.chatHistory.querySelectorAll(
|
||||||
|
'.svg-placeholder-block'
|
||||||
|
);
|
||||||
|
placeholders.forEach((node) =>
|
||||||
|
node.classList.remove('svg-placeholder-active')
|
||||||
|
);
|
||||||
|
const activeArtifactId = this.runtime.getActiveArtifactId(
|
||||||
|
this.activeModuleId
|
||||||
|
);
|
||||||
|
if (!activeArtifactId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activeNode = this.el.chatHistory.querySelector(
|
||||||
|
`.svg-placeholder-block[data-artifact-id="${activeArtifactId}"]`
|
||||||
|
);
|
||||||
|
if (activeNode) {
|
||||||
|
activeNode.classList.add('svg-placeholder-active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
openConfigModal() {
|
openConfigModal() {
|
||||||
if (!this.el.configModal) return;
|
if (!this.el.configModal) return;
|
||||||
this.el.configModal.classList.add('active');
|
this.el.configModal.classList.add('active');
|
||||||
|
|||||||
@@ -5,3 +5,83 @@
|
|||||||
3. 如需附加解读或说明,请放在 JSON 代码块之外。
|
3. 如需附加解读或说明,请放在 JSON 代码块之外。
|
||||||
4. 如果用户没有提供数据,请生成结构清晰的示例数据并说明需要用户替换的位置。
|
4. 如果用户没有提供数据,请生成结构清晰的示例数据并说明需要用户替换的位置。
|
||||||
5. 鼓励使用易读的调色板、标题和提示信息,兼顾桌面端展示。
|
5. 鼓励使用易读的调色板、标题和提示信息,兼顾桌面端展示。
|
||||||
|
6. 根据用户需求选择合适的图表类型和样式
|
||||||
|
7. 包含丰富的交互效果和美观的样式
|
||||||
|
|
||||||
|
可以参照以下JSON格式返回:
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": {
|
||||||
|
"text": "图表标题",
|
||||||
|
"subtext": "副标题(可选)",
|
||||||
|
"left": "center",
|
||||||
|
"textStyle": {
|
||||||
|
"color": "#333",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fontSize": 18
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"trigger": "axis",
|
||||||
|
"axisPointer": {
|
||||||
|
"type": "cross",
|
||||||
|
"crossStyle": {
|
||||||
|
"color": "#999"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backgroundColor": "rgba(255,255,255,0.9)",
|
||||||
|
"borderColor": "#ccc",
|
||||||
|
"borderWidth": 1
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"show": true,
|
||||||
|
"data": ["系列名称"],
|
||||||
|
"top": "bottom",
|
||||||
|
"padding": [20, 10, 10, 10]
|
||||||
|
},
|
||||||
|
"toolbox": {
|
||||||
|
"feature": {
|
||||||
|
"dataView": { "show": true, "readOnly": false, "title": "数据视图" },
|
||||||
|
"magicType": { "show": true, "type": ["line", "bar"], "title": {"line": "切换为折线图", "bar": "切换为柱状图"} },
|
||||||
|
"restore": { "show": true, "title": "还原" },
|
||||||
|
"saveAsImage": { "show": true, "title": "保存为图片" }
|
||||||
|
},
|
||||||
|
"right": 20
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"left": "3%",
|
||||||
|
"right": "4%",
|
||||||
|
"bottom": "12%",
|
||||||
|
"containLabel": true
|
||||||
|
},
|
||||||
|
"xAxis": {
|
||||||
|
"type": "category",
|
||||||
|
"data": ["实际的X轴数据数组"],
|
||||||
|
"name": "X轴名称",
|
||||||
|
"axisLabel": {
|
||||||
|
"color": "#666"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"type": "value",
|
||||||
|
"name": "Y轴名称",
|
||||||
|
"axisLabel": {
|
||||||
|
"color": "#666"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "系列名称",
|
||||||
|
"type": "bar",
|
||||||
|
"data": ["实际的Y轴数据数组"],
|
||||||
|
"itemStyle": {
|
||||||
|
"color": "#3498db",
|
||||||
|
"borderRadius": [4, 4, 0, 0]
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"focus": "series"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color": ["#3498db", "#e74c3c", "#2ecc71", "#f39c12", "#9b59b6"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user