Compare commits

...

6 Commits

Author SHA1 Message Date
史悦
ae46cedd37 1. 在浏览器中切换至 Mermaid 模块生成示例图,确认 SVG 容器占满可用空间且居中显示。
2. 若存在缓存,建议强制刷新(Ctrl+F5)以确保最新样式生效。
2025-10-27 15:10:06 +08:00
史悦
f7fd53c9a2 放大缩小 2025-10-27 15:05:35 +08:00
史悦
4dde0e31b1 放大缩小 2025-10-27 13:50:34 +08:00
史悦
01e1083e5e 资源改为本地引用 2025-10-27 12:25:23 +08:00
史悦
f37357096e • - 在 renderConversationHistory 末尾与 renderArtifact 内部新增 highlightActivePlaceholder(),每次渲染或
切换图形后都会重新标记当前选中的占位卡片。
  - 新方法会清除所有 .svg-placeholder-block 上的 svg-placeholder-active,再根据 ModuleRuntime 记录的
    currentArtifactId 为对应占位卡片添加该类(js/core/app-shell.js:321, 673, 729)。
  - 这样无论是点击左侧占位切换、完成流式渲染或模块切换,右侧当前图形都会同步点亮对应占位符,恢复过往的高
    亮效果。
2025-10-27 11:27:52 +08:00
史悦
533375e8ca 调整了整个框架,模块化解耦 2025-10-27 11:04:00 +08:00
26 changed files with 5344 additions and 1564 deletions

View File

@@ -284,10 +284,24 @@ iconify-icon {
}
.svg-content-wrapper {
/* flex: 1; */
/* margin: 1rem; */
display: inline-block;
/* text-align: center; */
transform-origin: center top;
}
.svg-content-wrapper--mermaid {
flex: 1;
margin: 1rem;
text-align: center;
width: 100%;
height: 100%;
object-fit: contain;
max-width: 100% !important;
}
/* 小手摇摆动画 */
@keyframes wave {
0%, 100% {transform: translateX(0px) rotate(90deg);}
@@ -329,6 +343,25 @@ iconify-icon {
overflow-y: auto;
}
.code-modal-content {
max-width: 760px;
}
.code-viewer {
background: #0f172a;
color: #f8fafc;
border: 2px solid #000;
padding: 16px;
font-family: 'Fira Code', 'Courier New', Courier, monospace;
font-size: 14px;
line-height: 1.6;
border-radius: 6px;
max-height: 60vh;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
/* 表单输入框样式 */
.config-input {
width: 100%;
@@ -414,3 +447,12 @@ iconify-icon {
#send-button.terminate-mode {
border-color: #dc2626;
}
.flowchart {
width: 100%;
height: 100%;
object-fit: contain;
max-width: 100% !important;
}

View File

@@ -4,12 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>产品画布 / SWOT分析</title>
<script src="https://cdn.tailwindcss.com/3.4.1"></script>
<script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
<script src="libs/css/tailwind.css"></script>
<script src="libs/js/iconify-icon.min.js"></script>
<script src="libs/js/marked.min.js"></script>
<link rel="stylesheet" href="libs/css/inter-font.css">
<link rel="stylesheet" href="css/style.css">
</head>
<body class="bg-gray-100 h-screen flex flex-col">
@@ -29,17 +27,9 @@
<iconify-icon icon="ph:gear-six-fill" class="text-2xl"></iconify-icon>
</button>
<span class="text-white font-bold text-sm">点击切换模</span>
<span class="text-white font-bold text-sm">点击切换模</span>
<iconify-icon icon="ph:hand-pointing-fill" class="text-2xl text-yellow-300 wave-hand"></iconify-icon>
<button id="canvas-mode-btn" class="mode-btn-active bg-white text-orange-600 px-4 py-2 font-bold border-2 border-black hover:bg-orange-50 transition-all duration-200">
<iconify-icon icon="ph:pen-nib-duotone" class="align-middle mr-1"></iconify-icon>
产品画布
</button>
<button id="swot-mode-btn" class="mode-btn-inactive bg-white text-purple-600 px-4 py-2 font-bold border-2 border-black hover:bg-purple-50 transition-all duration-200">
<iconify-icon icon="ph:chart-bar-duotone" class="align-middle mr-1"></iconify-icon>
SWOT分析
</button>
<div id="module-button-group" class="flex items-center gap-2"></div>
</div>
</header>
@@ -204,8 +194,45 @@
</div>
</div>
<!-- 代码查看模态窗 -->
<div id="code-modal" class="modal-overlay">
<div class="modal-content code-modal-content">
<div class="bg-gradient-to-r from-indigo-600 to-purple-600 p-4 border-b-4 border-black flex items-center justify-between">
<div class="flex items-center gap-2">
<iconify-icon icon="ph:code-fill" class="text-3xl text-white"></iconify-icon>
<h2 class="text-xl font-black text-white">生成代码</h2>
</div>
<button id="close-code-modal-btn" class="text-white hover:bg-white/20 p-2 transition-all">
<iconify-icon icon="ph:x-bold" class="text-2xl"></iconify-icon>
</button>
</div>
<div class="p-6 space-y-4">
<div class="flex items-center justify-between gap-3">
<p class="font-bold text-gray-700">以下为当前图表对应的代码,可复制后用于自定义编辑。</p>
<button id="copy-code-btn" class="px-4 py-2 bg-indigo-500 text-white font-bold border-2 border-black hover:bg-indigo-600 transition-all flex items-center gap-2">
<iconify-icon icon="ph:copy-bold"></iconify-icon>
复制代码
</button>
</div>
<pre id="code-content" class="code-viewer"></pre>
</div>
</div>
</div>
<!-- 引入JavaScript文件 -->
<script src="js/utils.js"></script>
<script src="js/services/storage-service.js"></script>
<script src="js/services/conversation-service.js"></script>
<script src="js/core/module-registry.js"></script>
<script src="js/modules/product-canvas.js"></script>
<script src="js/modules/swot.js"></script>
<script src="js/modules/echarts.js"></script>
<script src="js/modules/mermaid.js"></script>
<script src="libs/js/mermaid.min.js"></script>
<script src="libs/js/svg-pan-zoom.min.js"></script>
<script src="libs/js/echarts.min.js"></script>
<script src="js/core/module-runtime.js"></script>
<script src="js/core/app-shell.js"></script>
<script src="js/apiclient.js"></script>
<script src="js/app.js"></script>
</body>

View File

@@ -9,12 +9,25 @@ class APIClient {
key: '',
model: ''
};
this.prompts = {
canvas: '',
swot: ''
this.promptMap = {};
this.promptFiles = {
canvas: 'prompts/canvas-prompt.txt',
swot: 'prompts/swot-prompt.txt',
echarts: 'prompts/echarts-prompt.txt',
mermaid: 'prompts/mermaid-prompt.txt'
};
this.promptFallbacks = {
canvas: '你是一个专业的产品战略分析师,擅长创建产品画布。',
swot: '你是一个专业的商业战略分析师擅长进行SWOT分析。',
echarts:
'你是一个资深的数据可视化专家,精通将自然语言需求转化为 ECharts 配置对象,请输出结构化 JSON option。',
mermaid:
'你是一个资深的可视化工程师,擅长用 Mermaid 语法创建清晰的图示,请只输出一个 ```mermaid 代码块。',
default:
'你是一个可靠的智能助手,请直接回答用户的问题并提供结构化输出。'
};
this.loadConfig();
this.loadPrompts();
this.preloadPrompts(Object.keys(this.promptFiles));
}
// 加载API配置
@@ -25,21 +38,45 @@ class APIClient {
}
}
// 加载系统提示词
async loadPrompts() {
preloadPrompts(keys = []) {
keys.forEach((key) => {
this.ensurePrompt(key).catch((error) =>
console.warn(`预加载提示词 ${key} 失败:`, error)
);
});
}
async ensurePrompt(promptKey) {
if (!promptKey) return '';
if (this.promptMap[promptKey]) {
return this.promptMap[promptKey];
}
const prompt = await this.fetchPrompt(promptKey);
this.promptMap[promptKey] = prompt;
return prompt;
}
async fetchPrompt(promptKey) {
const filePath = this.promptFiles[promptKey];
const fallback =
this.promptFallbacks[promptKey] ||
'你是一个可靠的智能助手,请直接回答用户问题。';
if (!filePath) {
console.warn(`未找到提示词 ${promptKey} 对应的文件配置`);
return fallback;
}
try {
// 加载产品画布提示词
const canvasResponse = await fetch('prompts/canvas-prompt.txt');
this.prompts.canvas = await canvasResponse.text();
// 加载SWOT分析提示词
const swotResponse = await fetch('prompts/swot-prompt.txt');
this.prompts.swot = await swotResponse.text();
const response = await fetch(filePath);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const text = await response.text();
return text.trim() || fallback;
} catch (error) {
console.error('加载提示词失败:', error);
// 使用默认提示词
this.prompts.canvas = '你是一个专业的产品战略分析师,擅长创建产品画布。';
this.prompts.swot = '你是一个专业的商业战略分析师擅长进行SWOT分析。';
console.warn(`加载提示词 ${promptKey} 失败:`, error);
return fallback;
}
}
@@ -76,6 +113,49 @@ class APIClient {
}
}
async buildMessagesForModule(manifest, userMessage, contextMessages = []) {
const prompt =
(manifest && manifest.promptKey
? await this.ensurePrompt(manifest.promptKey)
: null) || this.promptFallbacks.default;
return [
{ role: 'system', content: prompt },
...contextMessages,
{ role: 'user', content: userMessage }
];
}
async generateModuleCompletion(
manifest,
userMessage,
contextMessages = [],
options = {}
) {
const messages = await this.buildMessagesForModule(
manifest,
userMessage,
contextMessages
);
return this.sendChatMessage(messages, options);
}
async generateModuleStream(
manifest,
userMessage,
contextMessages = [],
onChunk,
onComplete,
options = {}
) {
const messages = await this.buildMessagesForModule(
manifest,
userMessage,
contextMessages
);
return this.sendChatMessageStream(messages, options, onChunk, onComplete);
}
// 发送聊天请求
async sendChatMessage(messages, options = {}) {
if (!this.isConfigValid()) {
@@ -127,46 +207,46 @@ class APIClient {
// 生成产品画布的专用方法
async generateProductCanvas(userRequest, context = []) {
const messages = [
{ role: 'system', content: this.prompts.canvas },
...context,
{ role: 'user', content: userRequest }
];
return await this.sendChatMessage(messages, { maxTokens: 18000 });
return this.generateModuleCompletion(
{ promptKey: 'canvas' },
userRequest,
context,
{ maxTokens: 18000 }
);
}
// 生成SWOT分析的专用方法
async generateSWOTAnalysis(userRequest, context = []) {
const messages = [
{ role: 'system', content: this.prompts.swot },
...context,
{ role: 'user', content: userRequest }
];
return await this.sendChatMessage(messages, { maxTokens: 18000 });
return this.generateModuleCompletion(
{ promptKey: 'swot' },
userRequest,
context,
{ maxTokens: 18000 }
);
}
// 流式生成产品画布
async generateProductCanvasStream(userRequest, context = [], onChunk, onComplete) {
const messages = [
{ role: 'system', content: this.prompts.canvas },
...context,
{ role: 'user', content: userRequest }
];
return this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
return this.generateModuleStream(
{ promptKey: 'canvas' },
userRequest,
context,
onChunk,
onComplete,
{ maxTokens: 13000 }
);
}
// 流式生成SWOT分析
async generateSWOTAnalysisStream(userRequest, context = [], onChunk, onComplete) {
const messages = [
{ role: 'system', content: this.prompts.swot },
...context,
{ role: 'user', content: userRequest }
];
return this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
return this.generateModuleStream(
{ promptKey: 'swot' },
userRequest,
context,
onChunk,
onComplete,
{ maxTokens: 13000 }
);
}
// 流式发送聊天请求
@@ -233,7 +313,8 @@ class APIClient {
throw new Error('没有找到用户消息');
}
const mode = Utils.storage.get('currentMode', 'canvas');
const activeModuleId = Utils.storage.get('tool-engine:activeModuleId', 'product-canvas');
const mode = activeModuleId === 'swot' ? 'swot' : 'canvas';
if (mode === 'canvas') {
return await this.generateProductCanvas(lastUserMessage.content, contextMessages.slice(0, -1));
@@ -296,4 +377,4 @@ class APIClient {
}
// 创建全局API客户端实例
window.apiClient = new APIClient();
window.apiClient = new APIClient();

1526
js/app.js

File diff suppressed because it is too large Load Diff

1809
js/core/app-shell.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
(function (global) {
'use strict';
class ModuleRegistry {
constructor() {
this.modules = new Map();
this.order = [];
}
register(manifest) {
if (!manifest || !manifest.id) {
throw new Error('注册模块失败:缺少 id');
}
if (this.modules.has(manifest.id)) {
console.warn(`模块 ${manifest.id} 已存在,将被覆盖`);
} else {
this.order.push(manifest.id);
}
this.modules.set(manifest.id, manifest);
}
get(moduleId) {
return this.modules.get(moduleId) || null;
}
list() {
return this.order.map((id) => this.modules.get(id));
}
has(moduleId) {
return this.modules.has(moduleId);
}
}
global.ModuleRegistry = new ModuleRegistry();
})(window);

181
js/core/module-runtime.js Normal file
View File

@@ -0,0 +1,181 @@
(function (global) {
'use strict';
const DEFAULT_STORAGE_KEYS = {
history: 'history',
artifacts: 'artifacts',
ui: 'uiState'
};
class ModuleRuntime {
constructor({ registry, storageService, conversationService }) {
if (!registry) throw new Error('ModuleRuntime 需要 ModuleRegistry');
if (!storageService) throw new Error('ModuleRuntime 需要 StorageService');
if (!conversationService)
throw new Error('ModuleRuntime 需要 ConversationService');
this.registry = registry;
this.storageService = storageService;
this.conversationService = conversationService;
this.moduleStates = new Map();
this.activeModuleId = null;
}
_storageKeys(manifest) {
return {
...DEFAULT_STORAGE_KEYS,
...(manifest.storageKeys || {})
};
}
_namespace(manifest) {
const namespace =
manifest.storageNamespace || `module:${manifest.id || 'unknown'}`;
return this.storageService.namespace(namespace);
}
_ensureState(manifest) {
if (this.moduleStates.has(manifest.id)) {
return this.moduleStates.get(manifest.id);
}
const store = this._namespace(manifest);
const keys = this._storageKeys(manifest);
const state = {
artifacts: store.get(keys.artifacts, {}),
uiState: store.get(keys.ui, {}),
currentArtifactId: null
};
if (state.uiState && state.uiState.__activeArtifact) {
state.currentArtifactId = state.uiState.__activeArtifact;
}
this.moduleStates.set(manifest.id, state);
return state;
}
_persistState(manifest) {
const store = this._namespace(manifest);
const keys = this._storageKeys(manifest);
const state = this._ensureState(manifest);
if (state.uiState) {
state.uiState.__activeArtifact = state.currentArtifactId;
}
store.set(keys.artifacts, state.artifacts);
store.set(keys.ui, state.uiState);
}
getManifest(moduleId) {
const manifest = this.registry.get(moduleId);
if (!manifest) {
throw new Error(`未找到模块 ${moduleId}`);
}
return manifest;
}
listManifests() {
return this.registry.list();
}
activate(moduleId) {
const manifest = this.getManifest(moduleId);
this.activeModuleId = moduleId;
const state = this._ensureState(manifest);
const context = {
manifest,
state,
history: this.conversationService.getHistory(manifest)
};
if (manifest.hooks && typeof manifest.hooks.onActivate === 'function') {
try {
manifest.hooks.onActivate(context);
} catch (error) {
console.warn(`执行模块 ${moduleId} onActivate 时出错:`, error);
}
}
return context;
}
getActiveModule() {
if (!this.activeModuleId) return null;
return this.getManifest(this.activeModuleId);
}
getState(moduleId) {
const manifest = this.getManifest(moduleId);
return this._ensureState(manifest);
}
getArtifacts(moduleId) {
const state = this.getState(moduleId);
return state.artifacts;
}
saveArtifact(moduleId, artifactId, payload) {
const manifest = this.getManifest(moduleId);
const state = this._ensureState(manifest);
state.artifacts[artifactId] = payload;
state.currentArtifactId = artifactId;
this._persistState(manifest);
return payload;
}
removeArtifact(moduleId, artifactId) {
const manifest = this.getManifest(moduleId);
const state = this._ensureState(manifest);
if (state.artifacts[artifactId]) {
delete state.artifacts[artifactId];
if (state.currentArtifactId === artifactId) {
state.currentArtifactId = null;
}
this._persistState(manifest);
}
}
setActiveArtifact(moduleId, artifactId) {
const manifest = this.getManifest(moduleId);
const state = this._ensureState(manifest);
state.currentArtifactId = artifactId;
this._persistState(manifest);
}
getActiveArtifactId(moduleId) {
const state = this.getState(moduleId);
return state.currentArtifactId || null;
}
updateUiState(moduleId, patch) {
const manifest = this.getManifest(moduleId);
const state = this._ensureState(manifest);
state.uiState = {
...state.uiState,
...patch
};
this._persistState(manifest);
return state.uiState;
}
getUiState(moduleId, defaultValue = {}) {
const state = this.getState(moduleId);
const uiState = { ...(state.uiState || {}) };
delete uiState.__activeArtifact;
return { ...defaultValue, ...uiState };
}
getConversationService() {
return this.conversationService;
}
clearArtifacts(moduleId) {
const manifest = this.getManifest(moduleId);
const state = this._ensureState(manifest);
state.artifacts = {};
state.currentArtifactId = null;
this._persistState(manifest);
}
}
global.ModuleRuntime = ModuleRuntime;
})(window);

78
js/modules/echarts.js Normal file
View File

@@ -0,0 +1,78 @@
(function registerEChartsModule(global) {
'use strict';
if (!global.ModuleRegistry) {
throw new Error('ModuleRegistry 未初始化');
}
const CODE_FENCE_REGEX = /```(?:json|js|javascript|echarts|option)?\s*([\s\S]*?)```/i;
const parseOptionText = (text) => {
if (!text) return null;
try {
return JSON.parse(text);
} catch (error) {
try {
// 尝试处理 JS 对象语法
// eslint-disable-next-line no-new-func
return new Function(`return (${text});`)();
} catch (innerError) {
console.warn('解析 ECharts 配置失败:', innerError);
return null;
}
}
};
const parseResponse = (content) => {
const match = content.match(CODE_FENCE_REGEX);
if (match) {
const optionText = match[1].trim();
return {
optionText,
option: parseOptionText(optionText),
beforeText: content.substring(0, match.index).trim(),
afterText: content.substring(match.index + match[0].length).trim()
};
}
return {
optionText: '',
option: null,
beforeText: content.trim(),
afterText: ''
};
};
global.ModuleRegistry.register({
id: 'echarts',
label: 'ECharts 图表',
icon: 'ph:chart-line-up-duotone',
renderer: 'echarts',
promptKey: 'echarts',
storageNamespace: 'module:echarts',
chat: {
placeholder: '描述想生成的图表或调整需求,我会输出 ECharts 配置…',
streamStartToken: '```json',
contextWindow: 8
},
artifact: {
type: 'echarts-option',
fence: ['json', 'js', 'javascript', 'echarts', 'option'],
startPattern: /```(?:json|js|javascript|echarts|option)/i,
parser: parseResponse
},
hooks: {
onActivate() {
// 预留钩子,可在此初始化额外资源
}
},
exports: {
allowSvg: true,
allowPng: true,
allowClipboard: true,
allowCode: true
},
ui: {
placeholderText: '生成的 ECharts 图表将在此处显示'
}
});
})(window);

58
js/modules/mermaid.js Normal file
View 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);

View File

@@ -0,0 +1,43 @@
(function registerProductCanvasModule(global) {
'use strict';
if (!global.ModuleRegistry) {
throw new Error('ModuleRegistry 未初始化');
}
const parseResponse = (content) => Utils.parseSVGResponse(content);
global.ModuleRegistry.register({
id: 'product-canvas',
label: '产品画布',
icon: 'ph:pen-nib-duotone',
renderer: 'svg',
promptKey: 'canvas',
storageNamespace: 'module:product-canvas',
chat: {
placeholder: '描述你的产品定位、用户画像、价值主张等内容…',
streamStartToken: '```svg',
contextWindow: 10
},
artifact: {
type: 'svg',
fence: 'svg',
startPattern: /```(?:svg)?\s*<svg/i,
parser: parseResponse
},
hooks: {
onActivate() {
// 保留扩展点,后续可追加自定义逻辑
}
},
exports: {
allowSvg: true,
allowPng: true,
allowClipboard: true,
allowCode: true
},
ui: {
placeholderText: '生成的产品画布将在此处显示'
}
});
})(window);

39
js/modules/swot.js Normal file
View File

@@ -0,0 +1,39 @@
(function registerSwotModule(global) {
'use strict';
if (!global.ModuleRegistry) {
throw new Error('ModuleRegistry 未初始化');
}
const parseResponse = (content) => Utils.parseSVGResponse(content);
global.ModuleRegistry.register({
id: 'swot',
label: 'SWOT分析',
icon: 'ph:chart-bar-duotone',
renderer: 'svg',
promptKey: 'swot',
storageNamespace: 'module:swot',
chat: {
placeholder: '输入业务背景或问题,我来生成 SWOT 分析…',
streamStartToken: '```svg',
contextWindow: 10
},
artifact: {
type: 'svg',
fence: 'svg',
startPattern: /```(?:svg)?\s*<svg/i,
parser: parseResponse
},
hooks: {},
exports: {
allowSvg: true,
allowPng: true,
allowClipboard: true,
allowCode: true
},
ui: {
placeholderText: '生成的SWOT分析将在此处显示'
}
});
})(window);

View File

@@ -0,0 +1,118 @@
(function (global) {
'use strict';
/**
* 管理模块化对话历史及上下文构建
*/
class ConversationService {
constructor(storageService, defaultOptions = {}) {
if (!storageService) {
throw new Error('ConversationService 需要 StorageService 实例');
}
this.storageService = storageService;
this.defaultOptions = {
historyKey: 'history',
contextWindow: 10,
...defaultOptions
};
this.cache = new Map();
}
_getHistoryKey(moduleConfig) {
return moduleConfig.storageKeys?.history || this.defaultOptions.historyKey;
}
_getNamespace(moduleConfig) {
const namespace =
moduleConfig.storageNamespace || `module:${moduleConfig.id}`;
return this.storageService.namespace(namespace);
}
_getCacheKey(moduleId) {
return `history:${moduleId}`;
}
getHistory(moduleConfig) {
const cacheKey = this._getCacheKey(moduleConfig.id);
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
const store = this._getNamespace(moduleConfig);
const history =
store.get(this._getHistoryKey(moduleConfig), []).map((msg) => ({
...msg
}));
this.cache.set(cacheKey, history);
return history;
}
saveHistory(moduleConfig, history) {
const cacheKey = this._getCacheKey(moduleConfig.id);
const clonedHistory = history.map((msg) => ({ ...msg }));
this.cache.set(cacheKey, clonedHistory);
const store = this._getNamespace(moduleConfig);
store.set(this._getHistoryKey(moduleConfig), clonedHistory);
}
appendMessage(moduleConfig, message) {
const history = this.getHistory(moduleConfig);
history.push({ ...message });
this.saveHistory(moduleConfig, history);
return history;
}
replaceHistory(moduleConfig, history) {
this.saveHistory(moduleConfig, history);
}
clearHistory(moduleConfig) {
this.saveHistory(moduleConfig, []);
}
/**
* 构建流式上下文,为最后一个用户消息提供所需历史
*/
buildContext(moduleConfig, tailMessages = null) {
const history = this.getHistory(moduleConfig);
if (!history.length) return null;
let targetIndex = history.length - 1;
if (tailMessages != null) {
targetIndex = Math.max(0, history.length - tailMessages);
}
// 确保目标是用户消息
while (targetIndex >= 0 && history[targetIndex].type !== 'user') {
targetIndex -= 1;
}
if (targetIndex < 0) {
return null;
}
const contextWindow =
moduleConfig.chat?.contextWindow || this.defaultOptions.contextWindow;
const start = Math.max(0, targetIndex - contextWindow);
const contextSlice = history.slice(start, targetIndex);
const contextMessages = contextSlice
.filter((msg) => msg.type === 'user' || msg.type === 'ai')
.map((msg) => ({
role: msg.type === 'user' ? 'user' : 'assistant',
content: msg.content
}));
return {
history,
userMessage: history[targetIndex],
contextMessages,
targetIndex
};
}
}
global.ConversationService = ConversationService;
})(window);

View File

@@ -0,0 +1,84 @@
(function (global) {
'use strict';
/**
* 提供按命名空间隔离的本地存储封装
* 依赖 Utils.storage 作为底层驱动
*/
class NamespacedStorage {
constructor(namespace) {
this.namespace = namespace;
}
_key(key) {
return `${this.namespace}:${key}`;
}
get(key, defaultValue = null) {
return Utils.storage.get(this._key(key), defaultValue);
}
set(key, value) {
return Utils.storage.set(this._key(key), value);
}
remove(key) {
return Utils.storage.remove(this._key(key));
}
clear() {
const prefix = `${this.namespace}:`;
const toDelete = [];
for (let i = 0; i < localStorage.length; i += 1) {
const storageKey = localStorage.key(i);
if (storageKey && storageKey.startsWith(prefix)) {
toDelete.push(storageKey);
}
}
toDelete.forEach((storageKey) => localStorage.removeItem(storageKey));
}
}
class StorageService {
constructor(globalNamespace = 'tool-engine') {
this.globalNamespace = globalNamespace;
this.cache = new Map();
}
/**
* 获取全局命名空间存储
*/
global() {
if (!this.cache.has(this.globalNamespace)) {
this.cache.set(
this.globalNamespace,
new NamespacedStorage(this.globalNamespace)
);
}
return this.cache.get(this.globalNamespace);
}
/**
* 获取指定命名空间存储
*/
namespace(namespace) {
if (!namespace) {
throw new Error('Storage namespace 不能为空');
}
if (!this.cache.has(namespace)) {
this.cache.set(namespace, new NamespacedStorage(namespace));
}
return this.cache.get(namespace);
}
/**
* 清除指定命名空间内容
*/
clearNamespace(namespace) {
const store = this.namespace(namespace);
store.clear();
}
}
global.StorageService = StorageService;
})(window);

21
libs/css/inter-font.css Normal file
View File

@@ -0,0 +1,21 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/inter-400.ttf) format('truetype');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/inter-700.ttf) format('truetype');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(../fonts/inter-900.ttf) format('truetype');
}

65
libs/css/tailwind.css Normal file

File diff suppressed because one or more lines are too long

BIN
libs/fonts/inter-400.ttf Normal file

Binary file not shown.

BIN
libs/fonts/inter-700.ttf Normal file

Binary file not shown.

BIN
libs/fonts/inter-900.ttf Normal file

Binary file not shown.

45
libs/js/echarts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

12
libs/js/iconify-icon.min.js vendored Normal file

File diff suppressed because one or more lines are too long

69
libs/js/marked.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2314
libs/js/mermaid.min.js vendored Normal file

File diff suppressed because one or more lines are too long

6
libs/js/panzoom.min.js vendored Normal file

File diff suppressed because one or more lines are too long

3
libs/js/svg-pan-zoom.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,87 @@
你是一名资深的数据可视化专家,擅长使用 ECharts 将业务需求转化为可执行的配置。请遵循以下规则输出结果:
1. 使用 JSON 对象表达完整的 ECharts option不要包含解释说明。
2. 保持字段命名符合 ECharts 官方文档,避免多余字段。
3. 如需附加解读或说明,请放在 JSON 代码块之外。
4. 如果用户没有提供数据,请生成结构清晰的示例数据并说明需要用户替换的位置。
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"]
}

View File

@@ -0,0 +1,38 @@
你是一个 mermaid 编写专家,我会给你提供一个场景,你来输出 mermaid 代码,只输出代码即可,不要输出其他的内容,无论我提出什么你都要以 mermaid 代码格式回答我;如果我没指定 mermaid 图的类型,默认使用 `flowchart LR`。
语法规则库 ()
"Mermaid 语法规则和最佳实践"
'((特殊字符处理 . "使用双引号包裹含有特殊字符或空格的文本")
(HTML实体编码 . "&lt; &gt; &amp; # 等字符使用 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语法 图表设计 可视化 代码优化))
(工作原则 . '(准确理解 智能选择 规范输出 易于理解))
(输出标准 . '(语法正确 结构清晰 美观实用 可直接使用))