180 lines
7.2 KiB
TypeScript
180 lines
7.2 KiB
TypeScript
import { Shield, UploadCloud, FileCheck, Loader2, RefreshCw } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { useAppStore, useCurrentProject } from '../../stores/useAppStore';
|
|
import { useUIStore } from '../../stores/useUIStore';
|
|
import { useChatStore } from '../../stores/useChatStore';
|
|
|
|
import { ParseDeliveryStandard } from '../../../bindings/engimind/internal/parser/parseservice.js';
|
|
import { StreamTemplateDirectory } from '../../../bindings/engimind/internal/chat/chatservice.js';
|
|
import { Events } from '@wailsio/runtime';
|
|
|
|
export function TemplateCard() {
|
|
const currentProject = useCurrentProject();
|
|
const { isParsingTemplate, hasNewTemplatePending, setParsingTemplate, setNewTemplatePending, setEditingOutline } = useUIStore();
|
|
const setProjects = useAppStore(s => s.setProjects);
|
|
const currentProjectId = useAppStore(s => s.currentProjectId);
|
|
const [pendingMarkdown, setPendingMarkdown] = useState('');
|
|
|
|
const startTemplateParse = async () => {
|
|
if (!pendingMarkdown) return;
|
|
|
|
try {
|
|
setParsingTemplate(true);
|
|
setNewTemplatePending(false);
|
|
|
|
const activeModelId = useAppStore.getState().activeModelId;
|
|
if (!activeModelId) {
|
|
alert("提示:请先在配置中心连接一个有效的大语言模型提供商,否则无法进行文件解析!");
|
|
setParsingTemplate(false);
|
|
setNewTemplatePending(true);
|
|
return;
|
|
}
|
|
|
|
const cs = useChatStore.getState();
|
|
const userMsgId = Date.now();
|
|
const assistantMsgId = userMsgId + 1;
|
|
const msgIdStr = assistantMsgId.toString();
|
|
|
|
cs.addMessage({ id: userMsgId, role: 'user', content: '我上传了一份工程交付文档。请帮我深度解析并归纳出标准大纲结构。' });
|
|
cs.addMessage({ id: assistantMsgId, role: 'assistant', content: '', status: 'processing' });
|
|
|
|
let unSub = Events.On("chat_stream_" + msgIdStr, (e: any) => {
|
|
const fullText = Array.isArray(e.data) ? e.data[0] : e.data;
|
|
if (typeof fullText === 'string') {
|
|
const msg = cs.messages.find(m => m.id === assistantMsgId);
|
|
if (msg && fullText.length > msg.content.length) {
|
|
cs.updateMessage(assistantMsgId, { content: fullText });
|
|
}
|
|
}
|
|
});
|
|
|
|
// Invoke LLM to extract standard chapters
|
|
let jsonStr = await StreamTemplateDirectory(pendingMarkdown, activeModelId, msgIdStr);
|
|
unSub();
|
|
|
|
cs.updateMessage(assistantMsgId, { status: 'success' });
|
|
|
|
let jsonStrClean = jsonStr.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
jsonStrClean = jsonStrClean.replace(/```json/g, '').replace(/```/g, '').trim();
|
|
|
|
const startIdx = jsonStrClean.indexOf('[');
|
|
const endIdx = jsonStrClean.lastIndexOf(']');
|
|
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
jsonStrClean = jsonStrClean.substring(startIdx, endIdx + 1);
|
|
}
|
|
|
|
let parsedChapters;
|
|
try {
|
|
parsedChapters = JSON.parse(jsonStrClean);
|
|
} catch (err) {
|
|
console.error("LLM json decode failed:", jsonStrClean);
|
|
alert("模型解析出的结果不符合要求格式,请重试!\n模型原始输出片段:" + jsonStrClean.substring(0, 100) + "...");
|
|
setParsingTemplate(false);
|
|
setNewTemplatePending(true);
|
|
return;
|
|
}
|
|
|
|
if (!Array.isArray(parsedChapters)) {
|
|
parsedChapters = parsedChapters.chapters || parsedChapters.data || [];
|
|
if (!Array.isArray(parsedChapters)) {
|
|
parsedChapters = [parsedChapters];
|
|
}
|
|
}
|
|
|
|
const chapters = parsedChapters.map((c: any, i: number) => ({
|
|
id: c.id || `generated-${Date.now()}-${i}`,
|
|
title: c.title || `第${i+1}节 内容生成`,
|
|
status: 'idle',
|
|
progress: 0,
|
|
content: c.content || ''
|
|
}));
|
|
|
|
// Update project directory globally
|
|
setProjects(prev => prev.map(p => {
|
|
if (p.id !== currentProjectId) return p;
|
|
return {
|
|
...p,
|
|
activeTemplate: {
|
|
name: 'AI 深层解析大纲', version: 'v1.0 (Auto)',
|
|
chapters,
|
|
},
|
|
};
|
|
}));
|
|
|
|
setParsingTemplate(false);
|
|
setPendingMarkdown('');
|
|
setEditingOutline(true);
|
|
|
|
} catch (err: any) {
|
|
console.error(err);
|
|
alert("模型解析遇到错误:" + (err.message || err));
|
|
setParsingTemplate(false);
|
|
setNewTemplatePending(true);
|
|
}
|
|
};
|
|
|
|
const handleUploadClick = async () => {
|
|
if (currentProject.activeTemplate.chapters && currentProject.activeTemplate.chapters.length > 0) {
|
|
if (!window.confirm("当前已经存在解析好的交付标准目录,确定要重新上传并替换吗?已有的内容和结构将会被覆盖。")) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const markdownContent = await ParseDeliveryStandard();
|
|
if (markdownContent) {
|
|
setPendingMarkdown(markdownContent);
|
|
setNewTemplatePending(true);
|
|
}
|
|
} catch (error: any) {
|
|
alert("读取或转换文件内容失败: " + (error.message || error));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="p-3 mx-2 my-4 mt-auto rounded-[14px] bg-[#F1EFEA]">
|
|
<div className="flex items-center justify-between mb-3 text-[10px] font-medium text-[#8E8B83] uppercase tracking-[0.15em] px-1">
|
|
<div className="flex items-center gap-2"><Shield size={12} className="text-[#2D2D2D]" /> 交付标准</div>
|
|
<button
|
|
onClick={handleUploadClick}
|
|
className="p-1 hover:bg-[#EBE9E4] rounded-md text-[#8E8B83] hover:text-[#2D2D2D] transition-colors"
|
|
title="上传交付标准"
|
|
>
|
|
<UploadCloud size={14} />
|
|
</button>
|
|
</div>
|
|
<div className={`relative p-3 rounded-xl border transition-all duration-300 bg-[#FFFFFF] border-[#E5E4E0] shadow-[0_2px_10px_-4px_rgba(0,0,0,0.05)] ${
|
|
isParsingTemplate ? 'border-[#E5E4E0] animate-pulse ring-1 ring-[#E5E4E0]' : ''
|
|
}`}>
|
|
{isParsingTemplate ? (
|
|
<div className="flex flex-col items-center py-1 gap-2">
|
|
<Loader2 size={16} className="text-[#D97757] animate-spin" />
|
|
<span className="text-[9px] font-medium text-[#D97757] uppercase">AI 解析目录中...</span>
|
|
</div>
|
|
) : hasNewTemplatePending ? (
|
|
<button
|
|
onClick={startTemplateParse}
|
|
className="w-full py-1.5 bg-[#D97757] text-white rounded-lg text-[11px] font-medium transition-all hover:bg-[#C86444] shadow-sm transform hover:scale-[1.01]"
|
|
>
|
|
开始大模型解析目录
|
|
</button>
|
|
) : (
|
|
<div className="flex items-start gap-3">
|
|
<FileCheck size={16} className="text-[#8E8B83] mt-0.5" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-[12px] font-medium text-[#2D2D2D] truncate text-left">{currentProject.activeTemplate.name}</p>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<span className="inline-flex items-center gap-1.5 text-[9px] font-mono text-[#8E8B83]">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-[#D97757]" />
|
|
{currentProject.activeTemplate.version} ACTIVE
|
|
</span>
|
|
<RefreshCw size={10} className="text-[#C2C0B8]" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|