diff --git a/index.html b/index.html index 458db28..cd54558 100644 --- a/index.html +++ b/index.html @@ -233,6 +233,7 @@ + diff --git a/js/apiclient.js b/js/apiclient.js index 1aa922b..59eda64 100644 --- a/js/apiclient.js +++ b/js/apiclient.js @@ -14,7 +14,8 @@ class APIClient { canvas: 'prompts/canvas-prompt.txt', swot: 'prompts/swot-prompt.txt', echarts: 'prompts/echarts-prompt.txt', - mermaid: 'prompts/mermaid-prompt.txt' + mermaid: 'prompts/mermaid-prompt.txt', + onepage: 'prompts/onepage-prompt.txt' }; this.promptFallbacks = { canvas: '你是一个专业的产品战略分析师,擅长创建产品画布。', @@ -23,6 +24,8 @@ class APIClient { '你是一个资深的数据可视化专家,精通将自然语言需求转化为 ECharts 配置对象,请输出结构化 JSON option。', mermaid: '你是一个资深的可视化工程师,擅长用 Mermaid 语法创建清晰的图示,请只输出一个 ```mermaid 代码块。', + onepage: + '你是一名资深的落地页架构师,请基于需求生成完整的单文件 HTML(含 Tailwind 样式与必要的原生脚本),并使用 ```html 代码块封装输出。', default: '你是一个可靠的智能助手,请直接回答用户的问题并提供结构化输出。' }; diff --git a/js/core/app-shell.js b/js/core/app-shell.js index 3f049ed..17add05 100644 --- a/js/core/app-shell.js +++ b/js/core/app-shell.js @@ -762,7 +762,8 @@ manifestId: manifest.id, messageId, container, - svg: null + svg: null, + html: null }; this.streamState = streamState; @@ -805,6 +806,8 @@ 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); } }; @@ -927,6 +930,19 @@ 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); @@ -1025,6 +1041,11 @@ 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); } } @@ -1092,6 +1113,92 @@ } } + 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 || ''); @@ -1110,14 +1217,33 @@ Utils.scrollToBottom(this.el.chatHistory); } + updateHtmlPlaceholder(container, manifest, ctx) { + if (!container) return; + const beforeHtml = this.parseMarkdownContent(ctx.beforeText || ''); + const label = manifest.label || '页面'; + container.innerHTML = ` +
正在绘制${label}...
+正在${verb}${label}...