Files
AIclinicalresearch/docs/01-设计文档/用户端原型图.html
AI Clinical Dev Team 9acbb0ae2b feat: complete Dify platform deployment (Day 18)
## Dify 閮ㄧ讲瀹屾垚 鉁?
### 瀹屾垚鐨勫伐浣?1. Docker 闀滃儚鍔犻€熷櫒閰嶇疆
   - 閰嶇疆 5 涓浗鍐呴暅鍍忔簮
   - 澶у箙鎻愬崌涓嬭浇閫熷害鍜屾垚鍔熺巼

2. Dify 闀滃儚鎷夊彇 (鍏?11 涓湇鍔?
   - langgenius/dify-api:1.9.1
   - langgenius/dify-web:1.9.1
   - postgres, redis, weaviate, nginx 绛?   - 鎬诲ぇ灏忕害 2GB锛岃€楁椂绾?15 鍒嗛挓

3. Dify 鏈嶅姟鍚姩
   - 鉁?nginx (80/443)
   - 鉁?api, worker, worker_beat
   - 鉁?web (3000)
   - 鉁?db (PostgreSQL), redis
   - 鉁?weaviate (鍚戦噺鏁版嵁搴?
   - 鉁?sandbox, plugin_daemon, ssrf_proxy

4. Dify 鍒濆鍖栭厤缃?   - 鍒涘缓绠$悊鍛樿处鍙?   - 鍒涘缓搴旂敤: AI Clinical Research
   - 鑾峰彇 API Key: app-VZRn0vMXdmltEJkvatHVGv5j

5. 鍚庣鐜閰嶇疆
   - DIFY_API_URL=http://localhost/v1
   - DIFY_API_KEY 宸查厤缃?
### 鏂囨。鏇存柊
- 鏂板: docs/05-姣忔棩杩涘害/Day18-Dify閮ㄧ讲瀹屾垚.md
- 鏇存柊: docs/04-寮€鍙戣鍒?寮€鍙戦噷绋嬬.md (Day 18 鏍囪涓哄畬鎴?

### 涓嬩竴姝?Day 19-24: 鐭ヨ瘑搴撶郴缁熷紑鍙?- Dify 瀹㈡埛绔皝瑁?- 鐭ヨ瘑搴撶鐞?CRUD
- 鏂囨。涓婁紶涓庡鐞?- @鐭ヨ瘑搴撻泦鎴?- RAG 闂瓟楠岃瘉

---
Progress: 閲岀▼纰?1 (MVP) 85% -> 鐭ヨ瘑搴撶郴缁熷紑鍙戜腑
2025-10-11 08:58:41 +08:00

540 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI科研助手产品原型 V6</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
body {
font-family: 'Inter', sans-serif;
}
.sidebar-icon {
stroke-width: 1.5;
}
.main-content {
transition: opacity 0.3s ease-in-out;
}
.agent-card, .kb-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.agent-card:hover, .kb-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.project-list-item.active {
background-color: #eef2ff;
color: #4f46e5;
font-weight: 600;
}
.nav-item.active {
background-color: #4f46e5;
color: white;
}
.chat-bubble-actions {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.chat-bubble:hover .chat-bubble-actions {
opacity: 1;
}
.typing-indicator span {
animation: bounce 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1.0); }
}
/* Custom dropdown */
.dropdown { position: relative; display: inline-block; }
.dropdown-content {
display: none;
position: absolute;
background-color: white;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.1);
z-index: 50;
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
padding: 0.5rem;
}
.dropdown-top .dropdown-content {
bottom: 100%;
margin-bottom: 0.5rem;
}
.dropdown-content a { color: black; padding: 8px 12px; text-decoration: none; display: block; border-radius: 0.25rem; font-size: 0.875rem; }
.dropdown-content a:hover { background-color: #f3f4f6; }
.dropdown:hover .dropdown-content { display: block; }
/* Custom scrollbar for better aesthetics */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f1f5f9; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
</style>
</head>
<body class="bg-gray-100 text-gray-800 h-screen flex overflow-hidden">
<!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col">
<div class="px-6 py-4 border-b border-gray-200">
<h1 class="text-xl font-bold text-gray-900">AI科研助手</h1>
</div>
<nav class="flex-1 px-4 py-4 space-y-2">
<a href="#" id="nav-agents" class="nav-item flex items-center px-4 py-2 text-gray-700 rounded-lg hover:bg-gray-100">
<i data-lucide="brain-circuit" class="w-5 h-5 mr-3 sidebar-icon"></i>
智能体
</a>
<a href="#" id="nav-history" class="nav-item flex items-center px-4 py-2 text-gray-700 rounded-lg hover:bg-gray-100">
<i data-lucide="history" class="w-5 h-5 mr-3 sidebar-icon"></i>
历史记录
</a>
<a href="#" id="nav-kb" class="nav-item flex items-center px-4 py-2 text-gray-700 rounded-lg hover:bg-gray-100">
<i data-lucide="book-marked" class="w-5 h-5 mr-3 sidebar-icon"></i>
知识库
</a>
<div class="pt-4 mt-4 border-t border-gray-200">
<div class="flex justify-between items-center px-4 mb-2">
<h2 class="text-sm font-semibold text-gray-500 uppercase">我的项目</h2>
<button id="add-project-btn" class="text-gray-400 hover:text-indigo-600" title="创建新项目">
<i data-lucide="plus-circle" class="w-5 h-5"></i>
</button>
</div>
<div id="projectsList" class="space-y-1">
<!-- Projects will be rendered here -->
</div>
</div>
</nav>
<div class="px-4 py-4 border-t border-gray-200 space-y-2">
<a href="#" id="nav-quickstart" class="nav-item flex items-center px-4 py-2 text-gray-700 rounded-lg hover:bg-gray-100">
<i data-lucide="rocket" class="w-5 h-5 mr-3 sidebar-icon"></i>
快速上手
</a>
<a href="#" id="nav-help" class="nav-item flex items-center px-4 py-2 text-gray-700 rounded-lg hover:bg-gray-100">
<i data-lucide="help-circle" class="w-5 h-5 mr-3 sidebar-icon"></i>
帮助中心
</a>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 p-4 md:p-8 overflow-y-auto bg-gray-50">
<div id="agents-view" class="main-content"></div>
<div id="chat-view" class="main-content hidden h-full flex flex-col"></div>
<div id="knowledge-base-view" class="main-content hidden"></div>
<div id="kb-detail-view" class="main-content hidden"></div>
<div id="history-view" class="main-content hidden"></div>
<div id="quickstart-view" class="main-content hidden"></div>
<div id="help-view" class="main-content hidden"></div>
</main>
<!-- Modal for knowledge base selection -->
<div id="kb-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">选择知识库</h3>
<div id="kb-modal-list" class="space-y-2 max-h-60 overflow-y-auto"></div>
<div class="mt-6 flex justify-end space-x-3">
<button id="kb-modal-cancel" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">取消</button>
</div>
</div>
</div>
<!-- Modal for Project Edit -->
<div id="project-edit-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl p-6">
<h3 id="project-edit-title" class="text-lg font-medium leading-6 text-gray-900 mb-4">编辑项目信息</h3>
<textarea id="project-edit-textarea" class="w-full h-48 p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"></textarea>
<div class="mt-6 flex justify-end space-x-3">
<button id="project-edit-cancel" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">取消</button>
<button id="project-edit-save" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">保存</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- DATA ---
const agentsData = [ { id: 'agent-1', name: '选题评价', description: '从创新性、价值、可行性等维度评价临床问题。', icon: 'lightbulb' }, { id: 'agent-2', name: '科学问题梳理', description: '将模糊想法提炼成清晰、可验证的科学问题。', icon: 'list-filter' }, { id: 'agent-3', name: 'PICOS构建', description: '结构化地定义临床研究的核心要素。', icon: 'construction' }, { id: 'agent-4', name: '观察指标设计', description: '推荐主要、次要及安全性观察指标。', icon: 'target' }, { id: 'agent-5', name: 'CRF制定', description: '生成符合规范的病例报告表CRF框架。', icon: 'file-text' }, { id: 'agent-6', name: '样本量计算', description: '提供科学的样本量估算。', icon: 'calculator' }, { id: 'agent-7', name: '临床研究方案撰写', description: '生成结构完整的临床研究方案初稿。', icon: 'file-plus-2' }, { id: 'agent-8', name: '论文润色', description: '专业级语言润色,修正语法、拼写和表达。', icon: 'file-signature' }, { id: 'agent-9', name: '论文翻译', description: '精准的中英互译,优化科研术语。', icon: 'languages' }, { id: 'agent-10', name: '方法学评审', description: '对研究方案或论文进行全面的方法学评审。', icon: 'shield-check' }, { id: 'agent-11', name: '期刊方法学评审', description: '模拟期刊审稿人视角,提出可能被质疑的问题。', icon: 'file-search-2' }, { id: 'agent-12', name: '期刊稿约评审', description: '检查论文格式、字数等是否符合期刊要求。', icon: 'clipboard-check' } ];
let projectsData = [ { id: 'proj-1', name: 'XX药物III期临床试验', description: '一项多中心、随机、双盲、安慰剂平行对照的临床试验旨在评估XX药物在治疗阿尔兹海默症中的有效性和安全性。' }, { id: 'proj-2', name: '骨质疏松与肠道菌群关联研究', description: '探索特定肠道菌群与老年女性骨质疏松发生发展的关系。' } ];
let knowledgeBaseData = [ { id: 'kb-1', name: '骨质疏松专题', files: [{name:'文献综述.pdf', status:'ready'}, {name:'实验数据.docx', status:'ready'}]}, { id: 'kb-2', name: '肺癌研究资料', files: [{name:'临床指南_2024.pdf', status:'ready'}, {name:'病例报告汇总.docx', status:'processing'}, {name:'损坏的文件.pdf', status:'failed'}] }, { id: 'kb-3', name: '通用方法学', files: []} ];
let historyData = [ {id: 'hist-1', title: '关于XX药物的创新性评估', agentId: 'agent-1', projectId: 'proj-1', date: '2025-10-08'}, {id: 'hist-2', title: '润色论文摘要部分', agentId: 'agent-8', projectId: null, date: '2025-10-07'}, {id: 'hist-3', title: '骨质疏松研究PICOS构建', agentId: 'agent-3', projectId: 'proj-2', date: '2025-10-06'} ];
let chatData = { 'hist-1': [ {sender: 'user', text: '请帮我评估一下关于XX药物用于治疗阿尔兹海默症的选题。'}, {sender: 'ai', text: '好的,这是一个非常有前景的方向。从创新性来看,目前针对该靶点的研究尚属少数;从临床价值来看,若成功将为患者提供全新的治疗选择...'} ], 'hist-2': [ {sender: 'user', text: '请帮我润色这段摘要...'}, {sender: 'ai', text: '当然,这是修改后的版本...'} ], 'hist-3': [ {sender: 'user', text: '我们来为骨质疏松和肠道菌群的研究构建PICOS。'}, {sender: 'ai', text: '好的,我们开始吧。\n\n**P (Patient):** 绝经后、年龄在60-75岁、被诊断为骨质疏松的女性患者。\n**I (Intervention):** 接受特定益生菌制剂XXX治疗每日一次持续6个月。\n**C (Comparison):** 接受外观、味道、包装完全相同的安慰剂每日一次持续6个月。\n**O (Outcome):** 主要观察指标为治疗6个月后腰椎L1-4骨密度的变化值。次要观察指标包括股骨颈骨密度变化、血清骨转换标志物水平等。\n**S (Study Design):** 随机、双盲、安慰剂对照临床试验。'} ] };
// --- STATE ---
let state = {
activeView: 'agents', // agents, chat, kb, kb-detail, history, quickstart, help
activeProject: null,
activeChatId: null,
activeKbId: null,
activeModel: 'Gemini Pro',
editingProjectId: null,
isAiTyping: false,
};
const availableModels = ['Gemini Pro', 'DeepSeek-V2', 'Qwen2-72B'];
// --- DOM ELEMENTS ---
const dom = {
navAgents: document.getElementById('nav-agents'),
navHistory: document.getElementById('nav-history'),
navKb: document.getElementById('nav-kb'),
navQuickstart: document.getElementById('nav-quickstart'),
navHelp: document.getElementById('nav-help'),
projectsList: document.getElementById('projectsList'),
views: {
'agents': document.getElementById('agents-view'),
'chat': document.getElementById('chat-view'),
'kb': document.getElementById('knowledge-base-view'),
'kb-detail': document.getElementById('kb-detail-view'),
'history': document.getElementById('history-view'),
'quickstart': document.getElementById('quickstart-view'),
'help': document.getElementById('help-view'),
},
kbModal: document.getElementById('kb-modal'),
kbModalList: document.getElementById('kb-modal-list'),
kbModalCancel: document.getElementById('kb-modal-cancel'),
addProjectBtn: document.getElementById('add-project-btn'),
projectEditModal: {
modal: document.getElementById('project-edit-modal'),
title: document.getElementById('project-edit-title'),
textarea: document.getElementById('project-edit-textarea'),
cancel: document.getElementById('project-edit-cancel'),
save: document.getElementById('project-edit-save'),
}
};
// --- RENDER & LOGIC ---
function setState(newState) {
Object.assign(state, newState);
render();
}
function render() {
// Nav state
document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
if (['agents', 'chat'].includes(state.activeView)) dom.navAgents.classList.add('active');
if (['kb', 'kb-detail'].includes(state.activeView)) dom.navKb.classList.add('active');
if (state.activeView === 'history') dom.navHistory.classList.add('active');
if (state.activeView === 'quickstart') dom.navQuickstart.classList.add('active');
if (state.activeView === 'help') dom.navHelp.classList.add('active');
Object.values(dom.views).forEach(view => view.classList.add('hidden'));
// Render view using a stable switch statement
switch (state.activeView) {
case 'agents':
dom.views.agents.classList.remove('hidden');
renderAgentsView();
break;
case 'chat':
dom.views.chat.classList.remove('hidden');
renderChatView();
break;
case 'kb':
dom.views.kb.classList.remove('hidden');
renderKnowledgeBaseListView();
break;
case 'kb-detail':
dom.views['kb-detail'].classList.remove('hidden');
renderKbDetailView();
break;
case 'history':
dom.views.history.classList.remove('hidden');
renderHistoryView();
break;
case 'quickstart':
dom.views.quickstart.classList.remove('hidden');
renderQuickstartView();
break;
case 'help':
dom.views.help.classList.remove('hidden');
renderHelpView();
break;
}
renderProjectsList();
lucide.createIcons();
}
window.renderProjectsList = function() {
dom.projectsList.innerHTML = `
<div onclick="handleProjectClick(null)" class="project-list-item px-4 py-2 text-sm text-gray-600 rounded-md cursor-pointer hover:bg-gray-100 ${!state.activeProject ? 'active' : ''}">
<i data-lucide="globe" class="inline-block w-4 h-4 mr-2"></i> 全局快速问答
</div>
${projectsData.map(p => `
<div onclick="handleProjectClick('${p.id}')" class="project-list-item px-4 py-2 text-sm text-gray-600 rounded-md cursor-pointer hover:bg-gray-100 ${state.activeProject === p.id ? 'active' : ''}">
<i data-lucide="folder" class="inline-block w-4 h-4 mr-2"></i> ${p.name}
</div>
`).join('')}`;
};
window.renderAgentsView = function() {
const view = dom.views.agents;
const project = state.activeProject ? projectsData.find(p => p.id === state.activeProject) : null;
let breadcrumbHtml = project
? `<span class="cursor-pointer hover:text-indigo-600" onclick="handleProjectClick(null)">全局</span> > <span>${project.name}</span>`
: `<span>全局</span>`;
view.innerHTML = `
<header class="mb-8">
<div class="text-sm text-gray-500 mb-2">${breadcrumbHtml}</div>
<div class="flex items-center space-x-3">
<h1 class="text-3xl font-bold text-gray-900">${project ? project.name : '智能体'}</h1>
${project ? `<button onclick="handleEditProject('${project.id}')" class="text-gray-400 hover:text-indigo-600 p-1 rounded-full hover:bg-gray-200" title="编辑项目信息"><i data-lucide="pencil" class="w-5 h-5"></i></button>` : ''}
</div>
<p class="text-gray-500 mt-2 whitespace-pre-wrap">${project ? project.description : '你好!我可以为你做什么?选择一个智能体或项目开始吧。'}</p>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
${agentsData.map(agent => `
<div onclick="handleAgentClick('${agent.id}')" class="agent-card bg-white p-5 rounded-xl border border-gray-200 cursor-pointer flex items-start space-x-4">
<div class="bg-indigo-100 text-indigo-600 p-3 rounded-lg"><i data-lucide="${agent.icon}" class="w-6 h-6"></i></div>
<div>
<h3 class="font-semibold text-gray-900">${agent.name}</h3>
<p class="text-sm text-gray-500 mt-1">${agent.description}</p>
</div>
</div>
`).join('')}
</div>`;
};
window.renderChatView = function() {
const view = dom.views.chat;
const historyItem = historyData.find(h => h.id === state.activeChatId);
if (!historyItem) { view.innerHTML = "错误:找不到对话记录。"; return; }
const agent = agentsData.find(a => a.id === historyItem.agentId);
const project = state.activeProject ? projectsData.find(p => p.id === state.activeProject) : null;
const messages = chatData[state.activeChatId] || [];
let breadcrumbHtml = `<span class="cursor-pointer hover:text-indigo-600" onclick="handleProjectClick(null)">全局</span>`;
if (project) {
breadcrumbHtml += ` > <span class="cursor-pointer hover:text-indigo-600" onclick="handleProjectHomeClick('${project.id}')">${project.name}</span>`;
}
breadcrumbHtml += ` > <span>${agent.name}</span>`;
view.innerHTML = `
<header class="p-4 border-b border-gray-200 bg-white flex-shrink-0">
<div class="text-sm text-gray-500 mb-2">${breadcrumbHtml}</div>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="bg-indigo-100 text-indigo-600 p-2 rounded-lg"><i data-lucide="${agent.icon}" class="w-5 h-5"></i></div>
<div>
<h2 class="text-xl font-bold text-gray-900">${agent.name}</h2>
<p class="text-sm text-gray-500">${agent.description}</p>
</div>
</div>
</div>
</header>
<div id="chatMessages" class="flex-1 overflow-y-auto p-6 space-y-6">
${messages.map((msg, index) => renderChatMessage(msg, index)).join('')}
${state.isAiTyping ? renderTypingIndicator() : ''}
</div>
<div class="p-4 bg-gray-50 border-t border-gray-200 flex-shrink-0">
<div class="flex items-center bg-white border border-gray-300 rounded-lg p-2 focus-within:ring-2 focus-within:ring-indigo-500">
<textarea id="message-input" rows="1" class="flex-1 bg-transparent border-none focus:ring-0 resize-none text-sm" placeholder="输入您的问题..."></textarea>
<button onclick="document.getElementById('file-upload').click()" class="p-2 text-gray-500 hover:text-indigo-600 rounded-full hover:bg-gray-100" title="上传附件">
<i data-lucide="paperclip" class="w-5 h-5"></i>
</button>
<input type="file" id="file-upload" class="hidden"/>
<button id="kb-button" class="p-2 text-gray-500 hover:text-indigo-600 rounded-full hover:bg-gray-100" title="引用知识库"><i data-lucide="at-sign" class="w-5 h-5"></i></button>
<button id="send-button" class="ml-2 px-4 py-2 bg-indigo-600 text-white text-sm font-semibold rounded-lg hover:bg-indigo-700 disabled:bg-indigo-300">发送</button>
</div>
<div class="mt-2 flex items-center justify-between">
<div class="dropdown dropdown-top">
<button class="flex items-center space-x-2 px-3 py-1.5 border rounded-lg text-sm text-gray-600 hover:bg-gray-100">
<i data-lucide="cpu" class="w-4 h-4"></i>
<span>模型: ${state.activeModel}</span>
<i data-lucide="chevron-up" class="w-4 h-4"></i>
</button>
<div class="dropdown-content">
${availableModels.map(m => `<a href="#" onclick="handleModelChange('${m}')">${m === state.activeModel ? '<span class="font-bold text-indigo-600">'+m+'</span>' : m}</a>`).join('')}
</div>
</div>
<span class="text-xs text-gray-400">Shift+Enter 换行</span>
</div>
</div>`;
// Event listeners
const messageInput = document.getElementById('message-input');
messageInput.addEventListener('input', () => { messageInput.style.height = 'auto'; messageInput.style.height = `${messageInput.scrollHeight}px`; });
document.getElementById('send-button').onclick = () => handleSendMessage();
messageInput.onkeydown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } };
document.getElementById('kb-button').onclick = () => dom.kbModal.classList.remove('hidden');
document.getElementById('file-upload').onchange = (e) => alert(`已选择文件: ${e.target.files[0].name}`);
document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
};
window.renderChatMessage = function(msg, index) {
const isUser = msg.sender === 'user';
return `
<div class="chat-bubble flex items-start gap-3 ${isUser ? 'justify-end' : ''}">
${!isUser ? `<div class="w-8 h-8 rounded-full bg-indigo-500 text-white flex items-center justify-center flex-shrink-0"><i data-lucide="brain" class="w-5 h-5"></i></div>` : ''}
<div class="flex flex-col ${isUser ? 'items-end' : 'items-start'}">
<div class="max-w-xl p-4 rounded-xl ${isUser ? 'bg-indigo-600 text-white' : 'bg-white border'}">
<p class="text-sm leading-relaxed whitespace-pre-wrap">${msg.text}</p>
</div>
<div class="chat-bubble-actions mt-1.5 flex items-center gap-2 px-2 h-5">
<button onclick="alert('内容已复制')" title="复制" class="text-gray-400 hover:text-gray-600"><i data-lucide="copy" class="w-3.5 h-3.5"></i></button>
${!isUser ? `
<button onclick="alert('正在重新生成...')" title="重新生成" class="text-gray-400 hover:text-gray-600"><i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i></button>
${state.activeProject ? `<button onclick="handlePinMessage('${state.activeChatId}', ${index})" title="固定到项目背景" class="text-gray-400 hover:text-gray-600"><i data-lucide="pin" class="w-3.5 h-3.5"></i></button>` : ''}
` : ''}
</div>
</div>
${isUser ? `<div class="w-8 h-8 rounded-full bg-gray-200 text-gray-600 flex items-center justify-center flex-shrink-0"><i data-lucide="user" class="w-5 h-5"></i></div>` : ''}
</div>`;
};
window.renderTypingIndicator = function() { return `<div class="flex items-start gap-3"><div class="w-8 h-8 rounded-full bg-indigo-500 text-white flex items-center justify-center flex-shrink-0"><i data-lucide="brain" class="w-5 h-5"></i></div><div class="max-w-lg p-4 rounded-xl bg-white border"><div class="typing-indicator flex items-center space-x-1.5"><span class="w-2 h-2 bg-gray-400 rounded-full"></span><span class="w-2 h-2 bg-gray-400 rounded-full"></span><span class="w-2 h-2 bg-gray-400 rounded-full"></span></div></div></div>`; }
window.renderKnowledgeBaseListView = function() {
const v = dom.views['kb'];
v.innerHTML = `<header class="mb-8 flex justify-between items-center"><div><h1 class="text-3xl font-bold text-gray-900">个人知识库</h1><p class="text-gray-500 mt-2">在这里管理您的私人研究资料AI可随时调用。</p></div><button class="bg-indigo-600 text-white px-4 py-2 rounded-lg font-semibold flex items-center space-x-2 hover:bg-indigo-700"><i data-lucide="plus" class="w-5 h-5"></i><span>创建新知识库</span></button></header><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">${knowledgeBaseData.map(kb => `<div onclick="handleKbClick('${kb.id}')" class="kb-card bg-white p-5 rounded-xl border border-gray-200 cursor-pointer flex flex-col justify-between h-32"><div><div class="flex items-center space-x-3"><i data-lucide="folder" class="w-6 h-6 text-indigo-500"></i><h3 class="font-semibold text-gray-900">${kb.name}</h3></div></div><p class="text-sm text-gray-500 mt-2">${kb.files.length}个文档</p></div>`).join('')}</div>`;
}
window.renderKbDetailView = function() {
const v = dom.views['kb-detail'];
const kb = knowledgeBaseData.find(k => k.id === state.activeKbId);
if(!kb) { v.innerHTML = '错误: 找不到知识库。'; return; }
const s={'ready':{bg:'bg-green-100',text:'text-green-800',icon:'check-circle-2'},'processing':{bg:'bg-yellow-100',text:'text-yellow-800',icon:'loader'},'failed':{bg:'bg-red-100',text:'text-red-800',icon:'alert-circle'}};
v.innerHTML = `<header class="mb-8"><div class="text-sm text-gray-500 mb-2 cursor-pointer hover:text-indigo-600 flex items-center" onclick="handleNavClick('kb')"><i data-lucide="arrow-left" class="w-4 h-4 mr-1"></i>返回知识库列表</div><div class="flex justify-between items-center mt-2"><div><h1 class="text-3xl font-bold text-gray-900">${kb.name}</h1><p class="text-gray-500 mt-2">管理"${kb.name}"知识库中的所有文档。</p></div><button class="bg-indigo-600 text-white px-4 py-2 rounded-lg font-semibold flex items-center space-x-2 hover:bg-indigo-700"><i data-lucide="upload-cloud" class="w-5 h-5"></i><span>上传文件</span></button></div></header><div class="bg-white border border-gray-200 rounded-lg"><ul class="divide-y divide-gray-200">${kb.files.length > 0 ? kb.files.map(f => `<li class="p-4 flex items-center justify-between"><div class="flex items-center space-x-3"><i data-lucide="file-text" class="w-5 h-5 text-gray-400"></i><p class="font-medium text-gray-800">${f.name}</p></div><div class="flex items-center space-x-4"><span class="px-2 py-1 text-xs font-medium rounded-full ${s[f.status].bg} ${s[f.status].text}"><i data-lucide="${s[f.status].icon}" class="inline-block w-3 h-3 mr-1 ${f.status === 'processing' ? 'animate-spin' : ''}"></i>${{ready:'已就绪',processing:'处理中',failed:'失败'}[f.status]}</span><button class="text-gray-400 hover:text-red-600" title="删除"><i data-lucide="trash-2" class="w-4 h-4"></i></button></div></li>`).join('') : `<li class="p-8 text-center text-gray-500">这个知识库还没有文件。</li>`}</ul></div>`;
}
window.renderHistoryView = function() {
const v = dom.views.history;
v.innerHTML = `<header class="mb-8"><h1 class="text-3xl font-bold text-gray-900">历史记录</h1><p class="text-gray-500 mt-2">查看您所有的对话记录。</p></header><div class="mb-6 flex space-x-4"><div class="relative flex-1"><i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"></i><input type="text" placeholder="搜索对话内容..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"></div><select class="border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"><option>所有项目</option><option>全局快速问答</option>${projectsData.map(p => `<option>${p.name}</option>`).join('')}</select><select class="border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"><option>所有智能体</option>${agentsData.map(a => `<option>${a.name}</option>`).join('')}</select></div><div class="bg-white border border-gray-200 rounded-lg"><ul class="divide-y divide-gray-200">${historyData.map(h => { const a = agentsData.find(ag => ag.id === h.agentId), p = projectsData.find(pr => pr.id === h.projectId); return `<li onclick="handleHistoryClick('${h.id}')" class="p-4 hover:bg-gray-50 cursor-pointer"><div class="flex justify-between items-center mb-1"><p class="font-medium text-gray-800">${h.title}</p><p class="text-xs text-gray-400">${h.date}</p></div><div class="flex items-center space-x-2 text-sm text-gray-500"><span class="px-2 py-0.5 rounded-full bg-gray-100 flex items-center space-x-1.5"><i data-lucide="${a.icon}" class="w-3.5 h-3.5"></i><span>${a.name}</span></span><span class="px-2 py-0.5 rounded-full ${p ? 'bg-indigo-100 text-indigo-800' : 'bg-green-100 text-green-800'} flex items-center space-x-1.5"><i data-lucide="${p ? 'folder' : 'globe'}" class="w-3.5 h-3.5"></i><span>${p ? p.name : '全局'}</span></span></div></li>`; }).join('')}</ul></div>`;
}
window.renderQuickstartView = function() { dom.views.quickstart.innerHTML = `<h1 class="text-3xl font-bold">快速上手</h1><p class="mt-2 text-gray-600">这里是产品引导和核心功能介绍...</p>`; }
window.renderHelpView = function() { dom.views.help.innerHTML = `<h1 class="text-3xl font-bold">帮助中心</h1><p class="mt-2 text-gray-600">这里是FAQ和用户手册...</p>`; }
// --- EVENT HANDLERS ---
window.handleProjectClick = (projectId) => setState({ activeProject: projectId, activeView: 'agents', activeChatId: null });
window.handleProjectHomeClick = (projectId) => setState({ activeProject: projectId, activeView: 'agents', activeChatId: null });
window.handleAgentClick = (agentId) => {
const newHistoryId = `hist-${Date.now()}`;
const agentName = agentsData.find(a => a.id === agentId).name;
historyData.unshift({ id: newHistoryId, title: `与"${agentName}"的新对话`, agentId: agentId, projectId: state.activeProject, date: new Date().toISOString().split('T')[0] });
chatData[newHistoryId] = [{sender: 'ai', text: `你好,我是${agentName},使用 ${state.activeModel} 模型为您服务。有什么可以帮助你?`}];
setState({ activeView: 'chat', activeChatId: newHistoryId });
};
window.handleNavClick = (view) => setState({ activeView: view, activeKbId: null, activeChatId: null });
window.handleKbClick = (kbId) => setState({ activeView: 'kb-detail', activeKbId: kbId });
window.handleHistoryClick = (historyId) => {
const historyItem = historyData.find(h => h.id === historyId);
setState({ activeView: 'chat', activeProject: historyItem.projectId, activeChatId: historyId });
};
window.handleSendMessage = function() {
const input = document.getElementById('message-input');
const text = input.value.trim();
if (!text || !state.activeChatId) return;
chatData[state.activeChatId].push({ sender: 'user', text });
input.value = ''; input.style.height = 'auto';
setState({ isAiTyping: true });
setTimeout(() => {
chatData[state.activeChatId].push({ sender: 'ai', text: `[${state.activeModel} 模型回复] 这是一个关于"${text}"的模拟回复。` });
setState({ isAiTyping: false });
}, 1500);
}
window.handleAddProject = function() {
const projectName = prompt("请输入新项目的名称:", `新研究项目 ${projectsData.length + 1}`);
if(projectName && projectName.trim()) {
const newProject = { id: `proj-${Date.now()}`, name: projectName.trim(), description: '新项目的简要描述。' };
projectsData.push(newProject);
setState({ activeProject: newProject.id, activeView: 'agents' });
}
}
window.handleEditProject = function(projectId) {
const project = projectsData.find(p => p.id === projectId);
if(!project) return;
state.editingProjectId = projectId;
const modal = dom.projectEditModal;
modal.title.textContent = `编辑项目: ${project.name}`;
modal.textarea.value = project.description;
modal.modal.classList.remove('hidden');
}
window.handleSaveProject = function() {
const project = projectsData.find(p => p.id === state.editingProjectId);
if(project) {
project.description = dom.projectEditModal.textarea.value;
}
dom.projectEditModal.modal.classList.add('hidden');
state.editingProjectId = null;
render();
}
window.handleCancelEditProject = function() {
dom.projectEditModal.modal.classList.add('hidden');
state.editingProjectId = null;
}
window.handlePinMessage = function(chatId, messageIndex) {
const project = projectsData.find(p => p.id === state.activeProject);
if (!project) { alert("错误:必须在项目中才能固定信息!"); return; }
const messageText = chatData[chatId][messageIndex].text;
const confirmation = confirm(`您确定要将以下内容追加到项目背景中吗?\n\n"${messageText.substring(0, 100)}..."`);
if(confirmation) {
project.description += `\n\n--- 来自对话的补充 ---\n${messageText}`;
alert("已成功追加到项目背景!");
}
}
window.handleModelChange = function(modelName) {
setState({ activeModel: modelName });
}
// --- MODAL LOGIC ---
function setupModal() {
dom.kbModalList.innerHTML = knowledgeBaseData.map(kb => `<div onclick="handleKbSelect('${kb.name}')" class="p-2 rounded-md hover:bg-gray-100 cursor-pointer">${kb.name}</div>`).join('');
dom.kbModalCancel.onclick = () => dom.kbModal.classList.add('hidden');
dom.projectEditModal.save.onclick = handleSaveProject;
dom.projectEditModal.cancel.onclick = handleCancelEditProject;
}
window.handleKbSelect = (kbName) => {
const input = document.getElementById('message-input');
input.value += ` @${kbName} `;
dom.kbModal.classList.add('hidden');
input.focus();
};
// --- INITIALIZATION ---
dom.navAgents.onclick = (e) => { e.preventDefault(); handleNavClick('agents'); };
dom.navHistory.onclick = (e) => { e.preventDefault(); handleNavClick('history'); };
dom.navKb.onclick = (e) => { e.preventDefault(); handleNavClick('kb'); };
dom.navQuickstart.onclick = (e) => { e.preventDefault(); handleNavClick('quickstart'); };
dom.navHelp.onclick = (e) => { e.preventDefault(); handleNavClick('help'); };
dom.addProjectBtn.onclick = handleAddProject;
// Make handlers globally accessible for inline onclicks
window.handleProjectClick = handleProjectClick;
window.handleProjectHomeClick = handleProjectHomeClick;
window.handleAgentClick = handleAgentClick;
window.handleNavClick = handleNavClick;
window.handleKbClick = handleKbClick;
window.handleHistoryClick = handleHistoryClick;
window.handleSendMessage = handleSendMessage;
window.handleAddProject = handleAddProject;
window.handleEditProject = handleEditProject;
window.handlePinMessage = handlePinMessage;
window.handleModelChange = handleModelChange;
window.handleKbSelect = handleKbSelect;
setupModal();
render();
});
</script>
</body>
</html>