## 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% -> 鐭ヨ瘑搴撶郴缁熷紑鍙戜腑
540 lines
36 KiB
HTML
540 lines
36 KiB
HTML
<!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>
|
||
|
||
|
||
|