增加onepage页面
This commit is contained in:
@@ -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:
|
||||
'你是一个可靠的智能助手,请直接回答用户的问题并提供结构化输出。'
|
||||
};
|
||||
|
||||
@@ -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(/<!DOCTYPE|<html|<body/i);
|
||||
if (fallbackIndex !== -1) {
|
||||
startIndex = fallbackIndex;
|
||||
beforeText = fullContent.substring(0, fallbackIndex);
|
||||
}
|
||||
}
|
||||
if (startIndex !== null && startIndex >= 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 = `
|
||||
<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} HTML…
|
||||
</div>
|
||||
<div class="typing-cursor"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<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>
|
||||
<p class="mt-4 font-bold">正在${verb}${label}...</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1875,6 +2001,9 @@
|
||||
} else if (artifact.type === 'echarts-option') {
|
||||
this.destroyMermaidPanZoom();
|
||||
this.renderEChartsArtifact(artifact);
|
||||
} else if (artifact.type === 'html') {
|
||||
this.destroyMermaidPanZoom();
|
||||
this.renderHtmlArtifact(artifact, manifest);
|
||||
}
|
||||
this.highlightActivePlaceholder();
|
||||
this.updateToolbarState();
|
||||
@@ -1940,6 +2069,64 @@
|
||||
this.echartsInstance.setOption(artifact.option, true);
|
||||
}
|
||||
|
||||
renderHtmlArtifact(artifact, manifest) {
|
||||
if (!manifest) {
|
||||
manifest = this.getActiveManifest();
|
||||
}
|
||||
if (!artifact || !artifact.content) {
|
||||
this.showViewerPlaceholder(manifest?.ui?.placeholderText || '');
|
||||
return;
|
||||
}
|
||||
this.renderHtmlPreview(artifact.content, manifest, { partial: false });
|
||||
}
|
||||
|
||||
renderHtmlPreview(htmlContent, manifest, options = {}) {
|
||||
if (!this.el.viewer) return;
|
||||
const { partial = false } = options;
|
||||
const preparedHtml = this.prepareHtmlDocument(htmlContent);
|
||||
this.el.viewer.innerHTML = '';
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className =
|
||||
'w-full h-full relative rounded-xl overflow-hidden shadow-lg bg-white border border-purple-200';
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'w-full h-full border-0 bg-white';
|
||||
iframe.setAttribute(
|
||||
'title',
|
||||
`${manifest?.label || '页面'}预览`
|
||||
);
|
||||
iframe.setAttribute(
|
||||
'sandbox',
|
||||
'allow-forms allow-pointer-lock allow-same-origin allow-scripts'
|
||||
);
|
||||
iframe.srcdoc = preparedHtml;
|
||||
wrapper.appendChild(iframe);
|
||||
|
||||
if (partial) {
|
||||
const banner = document.createElement('div');
|
||||
banner.className =
|
||||
'absolute inset-x-0 top-0 flex justify-end pointer-events-none';
|
||||
banner.innerHTML =
|
||||
'<span class="m-3 px-3 py-1 text-xs font-semibold bg-yellow-300/90 text-gray-900 border border-yellow-600 rounded-full shadow-sm">内容生成中…</span>';
|
||||
wrapper.appendChild(banner);
|
||||
}
|
||||
|
||||
this.el.viewer.appendChild(wrapper);
|
||||
}
|
||||
|
||||
prepareHtmlDocument(htmlContent) {
|
||||
const raw = typeof htmlContent === 'string' ? htmlContent.trim() : '';
|
||||
if (!raw) {
|
||||
return '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><title>空白页面</title></head><body><section style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;color:#6b7280;background:#f9fafb;">暂无可展示内容</section></body></html>';
|
||||
}
|
||||
const hasDocumentTag = /<!DOCTYPE|<html[\s>]/i.test(raw);
|
||||
if (hasDocumentTag) {
|
||||
return raw;
|
||||
}
|
||||
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Generated Page</title></head><body>${raw}</body></html>`;
|
||||
}
|
||||
|
||||
adjustZoom(delta) {
|
||||
const manifest = this.getActiveManifest();
|
||||
if (!this.isZoomableManifest(manifest)) return;
|
||||
@@ -2168,6 +2355,8 @@
|
||||
content = artifact.code || artifact.content || '';
|
||||
} else if (artifact.type === 'echarts-option') {
|
||||
content = artifact.optionText || JSON.stringify(artifact.option, null, 2);
|
||||
} else if (artifact.type === 'html') {
|
||||
content = artifact.content || '';
|
||||
}
|
||||
|
||||
this.openCodeModal(content);
|
||||
|
||||
85
js/modules/onepage.js
Normal file
85
js/modules/onepage.js
Normal file
@@ -0,0 +1,85 @@
|
||||
(function registerOnepageModule(global) {
|
||||
'use strict';
|
||||
|
||||
if (!global.ModuleRegistry) {
|
||||
throw new Error('ModuleRegistry 未初始化');
|
||||
}
|
||||
|
||||
const HTML_FENCE = /```(?:html|htm)?\s*([\s\S]*?)```/i;
|
||||
const START_PATTERN = /```(?:html|htm)/i;
|
||||
|
||||
const parseResponse = (content = '') => {
|
||||
const text =
|
||||
typeof content === 'string' ? content : String(content || '');
|
||||
const match = text.match(HTML_FENCE);
|
||||
if (match) {
|
||||
const beforeText = text.substring(0, match.index).trim();
|
||||
const afterText = text
|
||||
.substring(match.index + match[0].length)
|
||||
.trim();
|
||||
return {
|
||||
htmlContent: match[1].trim(),
|
||||
beforeText,
|
||||
afterText
|
||||
};
|
||||
}
|
||||
const fallback = text.trim();
|
||||
if (!fallback) {
|
||||
return {
|
||||
htmlContent: '',
|
||||
beforeText: '',
|
||||
afterText: ''
|
||||
};
|
||||
}
|
||||
const htmlIndex = text.search(/<!DOCTYPE|<html|<body/i);
|
||||
if (htmlIndex !== -1) {
|
||||
return {
|
||||
htmlContent: text.substring(htmlIndex).trim(),
|
||||
beforeText: text.substring(0, htmlIndex).trim(),
|
||||
afterText: ''
|
||||
};
|
||||
}
|
||||
return {
|
||||
htmlContent: fallback,
|
||||
beforeText: '',
|
||||
afterText: ''
|
||||
};
|
||||
};
|
||||
|
||||
global.ModuleRegistry.register({
|
||||
id: 'onepage',
|
||||
label: '落地页生成',
|
||||
icon: 'ph:browser-duotone',
|
||||
renderer: 'html',
|
||||
promptKey: 'onepage',
|
||||
storageNamespace: 'module:onepage',
|
||||
chat: {
|
||||
placeholder:
|
||||
'请输入品牌定位、目标客群、核心卖点等信息,我会生成完整的落地页 HTML…',
|
||||
streamStartToken: '```html',
|
||||
contextWindow: 6
|
||||
},
|
||||
artifact: {
|
||||
type: 'html',
|
||||
fence: 'html',
|
||||
startPattern: START_PATTERN,
|
||||
parser: parseResponse
|
||||
},
|
||||
hooks: {},
|
||||
exports: {
|
||||
allowSvg: false,
|
||||
allowPng: false,
|
||||
allowClipboard: false,
|
||||
allowCode: true
|
||||
},
|
||||
ui: {
|
||||
placeholderText: '生成的落地页预览将在此处显示',
|
||||
quickActions: [
|
||||
{ label: 'SaaS 产品页', value: '为 B2B SaaS 产品生成官方落地页;' },
|
||||
{ label: '新品发布', value: '为科技新品发布会设计预告落地页;' },
|
||||
{ label: '在线课程', value: '为在线课程制作报名落地页;' },
|
||||
{ label: '活动报名', value: '为线下品牌活动创建报名落地页;' }
|
||||
]
|
||||
}
|
||||
});
|
||||
})(window);
|
||||
Reference in New Issue
Block a user