diff --git a/deepsearcher/backend/templates/index.html b/deepsearcher/backend/templates/index.html
deleted file mode 100644
index b81719f..0000000
--- a/deepsearcher/backend/templates/index.html
+++ /dev/null
@@ -1,948 +0,0 @@
-
-
-
-
-
- DeepSearcher - 智能搜索系统
-
-
-
-
-
-
-
-
-
-
-
文件加载
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
网站内容加载
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
智能查询
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/deepsearcher/templates/html/index.html b/deepsearcher/templates/html/index.html
new file mode 100644
index 0000000..f9591d0
--- /dev/null
+++ b/deepsearcher/templates/html/index.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+ DeepSearcher - 智能搜索系统
+
+
+
+
+
+
+
+
+
文件加载
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
网站内容加载
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
智能查询
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/deepsearcher/templates/index.html b/deepsearcher/templates/index.html
new file mode 100644
index 0000000..20af32f
--- /dev/null
+++ b/deepsearcher/templates/index.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+ DeepSearcher - 智能搜索系统
+
+
+
+
+
+
+
+
+
文件加载
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
网站加载
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
智能查询
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/deepsearcher/templates/static/css/styles.css b/deepsearcher/templates/static/css/styles.css
new file mode 100644
index 0000000..29175ab
--- /dev/null
+++ b/deepsearcher/templates/static/css/styles.css
@@ -0,0 +1,280 @@
+:root {
+ --primary-color: #4f46e5;
+ --secondary-color: #f9fafb;
+ --border-color: #e5e7eb;
+ --text-primary: #1f2937;
+ --text-secondary: #6b7280;
+ --success-color: #10b981;
+ --error-color: #ef4444;
+ --warning-color: #f59e0b;
+ --info-color: #3b82f6;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ line-height: 1.6;
+ color: var(--text-primary);
+ background-color: #f3f4f6;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+header {
+ text-align: center;
+ margin-bottom: 30px;
+ padding: 20px;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+h1 {
+ color: var(--primary-color);
+ margin-bottom: 10px;
+}
+
+.app-description {
+ color: var(--text-secondary);
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.card {
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ padding: 24px;
+ margin-bottom: 24px;
+}
+
+.card-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin-bottom: 16px;
+ color: var(--text-primary);
+}
+
+.form-group {
+ margin-bottom: 16px;
+}
+
+label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 500;
+}
+
+input, textarea, select {
+ width: 100%;
+ padding: 10px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ font-size: 16px;
+ transition: border-color 0.15s;
+}
+
+input:focus, textarea:focus, select:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
+}
+
+button {
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 12px 20px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.15s;
+}
+
+button:hover {
+ background-color: #4338ca;
+}
+
+button:disabled {
+ background-color: var(--border-color);
+ cursor: not-allowed;
+}
+
+.btn-secondary {
+ background-color: var(--text-secondary);
+}
+
+.btn-secondary:hover {
+ background-color: var(--text-primary);
+}
+
+.result-container {
+ margin-top: 20px;
+ padding: 16px;
+ border-radius: 6px;
+ background-color: var(--secondary-color);
+ display: none;
+}
+
+.result-container.visible {
+ display: block;
+}
+
+.status {
+ padding: 12px;
+ border-radius: 6px;
+ margin: 10px 0;
+ display: none;
+}
+
+.status.visible {
+ display: block;
+}
+
+.status-success {
+ background-color: #d1fae5;
+ color: var(--success-color);
+ border: 1px solid var(--success-color);
+}
+
+.status-error {
+ background-color: #fee2e2;
+ color: var(--error-color);
+ border: 1px solid var(--error-color);
+}
+
+.status-loading {
+ background-color: #fffbeb;
+ color: var(--warning-color);
+ border: 1px solid var(--warning-color);
+ display: flex;
+ align-items: center;
+}
+
+.loading-spinner {
+ display: none;
+ width: 12px;
+ height: 12px;
+ border: 2px solid #f3f3f3;
+ border-top: 2px solid var(--warning-color);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ flex-shrink: 0;
+ align-items: center;
+}
+
+.status-loading .loading-spinner {
+ display: inline-block;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.loading .loading-spinner {
+ display: inline-block;
+}
+
+.query-result {
+ white-space: pre-wrap;
+ line-height: 1.6;
+ background-color: white;
+ padding: 16px;
+ border-radius: 6px;
+ border: 1px solid var(--border-color);
+ margin-top: 8px;
+}
+
+.message-stream {
+ margin-top: 16px;
+}
+
+#processResult {
+ margin-top: 16px;
+}
+
+#processResult h3 {
+ color: var(--text-secondary);
+ font-size: 1rem;
+}
+
+#queryResult h3 {
+ color: var(--text-secondary);
+ font-size: 1rem;
+}
+
+.message-container {
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ background-color: white;
+ padding: 12px;
+}
+
+.message {
+ margin-bottom: 12px;
+ padding: 8px 12px;
+ border-radius: 4px;
+ border-left: 4px solid;
+ font-size: 14px;
+ line-height: 1.4;
+}
+
+.message-start {
+ background-color: #f0f9ff;
+ border-left-color: var(--info-color);
+}
+
+.message-info {
+ background-color: #fef3c7;
+ border-left-color: var(--warning-color);
+}
+
+.message-answer {
+ background-color: #d1fae5;
+ border-left-color: var(--success-color);
+}
+
+.message-complete {
+ background-color: #dbeafe;
+ border-left-color: var(--primary-color);
+}
+
+.message-error {
+ background-color: #fee2e2;
+ border-left-color: var(--error-color);
+}
+
+.message-timestamp {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-top: 4px;
+}
+
+footer {
+ text-align: center;
+ margin-top: 30px;
+ padding: 20px;
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+}
+
+@media (max-width: 768px) {
+ .container {
+ padding: 10px;
+ }
+
+ .card {
+ padding: 16px;
+ }
+}
diff --git a/deepsearcher/templates/static/js/app.js b/deepsearcher/templates/static/js/app.js
new file mode 100644
index 0000000..d13cbd2
--- /dev/null
+++ b/deepsearcher/templates/static/js/app.js
@@ -0,0 +1,446 @@
+// 全局变量
+let eventSource = null;
+let isStreaming = false;
+
+// 工具函数:显示状态信息
+function showStatus(elementId, message, type) {
+ const statusElement = document.getElementById(elementId);
+
+ // 清除之前的类型类
+ statusElement.classList.remove('status-success', 'status-error', 'status-loading');
+
+ // 添加新的类型类
+ if (type === 'success') {
+ statusElement.classList.add('status-success');
+ statusElement.innerHTML = message;
+ } else if (type === 'error') {
+ statusElement.classList.add('status-error');
+ statusElement.innerHTML = message;
+ } else if (type === 'loading') {
+ statusElement.classList.add('status-loading');
+ statusElement.innerHTML = `${message}`;
+ }
+
+ statusElement.classList.add('visible');
+}
+
+// 工具函数:显示消息流
+function displayMessages(messages) {
+ const container = document.getElementById('messageContainer');
+ container.innerHTML = '';
+
+ messages.forEach(message => {
+ addMessageToContainer(message);
+ });
+
+ // 滚动到底部
+ container.scrollTop = container.scrollHeight;
+}
+
+// 工具函数:添加单个消息到容器
+function addMessageToContainer(message) {
+ console.log('Adding message to container:', message);
+
+ const container = document.getElementById('messageContainer');
+ if (!container) {
+ console.error('Message container not found!');
+ return;
+ }
+
+ const messageElement = document.createElement('div');
+ messageElement.className = `message message-${message.type}`;
+
+ const contentElement = document.createElement('div');
+ contentElement.textContent = message.content;
+
+ messageElement.appendChild(contentElement);
+
+ // 只有在有有效时间戳时才显示时间
+ if (message.timestamp && !isNaN(message.timestamp)) {
+ const date = new Date(message.timestamp * 1000);
+ if (!isNaN(date.getTime())) {
+ const timestampElement = document.createElement('div');
+ timestampElement.className = 'message-timestamp';
+ timestampElement.textContent = date.toLocaleTimeString();
+ messageElement.appendChild(timestampElement);
+ }
+ }
+ container.appendChild(messageElement);
+
+ // 确保处理过程容器是可见的
+ const processContainer = document.getElementById('processResult');
+ if (processContainer && !processContainer.classList.contains('visible')) {
+ processContainer.classList.add('visible');
+ }
+
+ // 滚动到底部
+ container.scrollTop = container.scrollHeight;
+
+ console.log('Message added successfully, container now has', container.children.length, 'messages');
+}
+
+// 工具函数:隐藏状态信息
+function hideStatus(elementId) {
+ const statusElement = document.getElementById(elementId);
+ statusElement.classList.remove('visible');
+}
+
+// 工具函数:显示结果
+function showResult() {
+ const resultElement = document.getElementById('queryResult');
+ resultElement.classList.add('visible');
+}
+
+// 工具函数:隐藏结果
+function hideResult() {
+ const resultElement = document.getElementById('queryResult');
+ resultElement.classList.remove('visible');
+}
+
+// 工具函数:显示处理过程
+function showProcessResult() {
+ const processElement = document.getElementById('processResult');
+ processElement.classList.add('visible');
+}
+
+// 工具函数:隐藏处理过程
+function hideProcessResult() {
+ const processElement = document.getElementById('processResult');
+ processElement.classList.remove('visible');
+}
+
+// 工具函数:转义HTML特殊字符
+function escapeHtml(text) {
+ const map = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": '''
+ };
+ return text.replace(/[&<>"']/g, function(m) { return map[m]; });
+}
+
+// 工具函数:设置按钮加载状态
+function setButtonLoading(button, loading) {
+ if (loading) {
+ button.classList.add('loading');
+ button.disabled = true;
+ } else {
+ button.classList.remove('loading');
+ button.disabled = false;
+ }
+}
+
+// 工具函数:关闭EventSource连接
+function closeEventSource() {
+ if (eventSource) {
+ console.log('Closing eventSource in closeEventSource function');
+ eventSource.close();
+ eventSource = null;
+ }
+ if (window.currentEventSource) {
+ console.log('Closing currentEventSource in closeEventSource function');
+ window.currentEventSource.close();
+ window.currentEventSource = null;
+ }
+ isStreaming = false;
+}
+
+// 工具函数:处理实时消息流
+function handleStreamMessage(data) {
+ try {
+ const message = JSON.parse(data);
+
+ switch (message.type) {
+ case 'connection':
+ console.log('Connected to message stream:', message.message);
+ break;
+ case 'heartbeat':
+ // 心跳消息,不需要处理
+ break;
+ case 'start':
+ console.log('Query started:', message.content);
+ showStatus('queryStatus', ' 正在处理...', 'loading');
+ addMessageToContainer(message);
+ break;
+ case 'complete':
+ console.log('Query completed - closing connection');
+ showStatus('queryStatus', '查询完成', 'success');
+ addMessageToContainer(message);
+ // 关闭EventSource连接
+ if (window.currentEventSource) {
+ console.log('Closing currentEventSource');
+ window.currentEventSource.close();
+ window.currentEventSource = null;
+ }
+ isStreaming = false;
+ setButtonLoading(document.getElementById('queryBtn'), false);
+ console.log('Query completed - connection closed, isStreaming set to false');
+ break;
+ case 'error':
+ console.error('Error:', message.content);
+ showStatus('queryStatus', message.content, 'error');
+ addMessageToContainer(message);
+ // 关闭EventSource连接
+ if (window.currentEventSource) {
+ window.currentEventSource.close();
+ window.currentEventSource = null;
+ }
+ isStreaming = false;
+ setButtonLoading(document.getElementById('queryBtn'), false);
+ break;
+ case 'info':
+ // 处理信息消息
+ console.log('Processing info message:', message.content.substring(0, 100) + '...');
+ addMessageToContainer(message);
+ break;
+ case 'answer':
+ // 处理answer类型,显示查询结果
+ console.log('Processing answer message:', message.content.substring(0, 100) + '...');
+ // 将结果内容显示在结果区域
+ if (message.content && message.content !== "==== FINAL ANSWER====") {
+ document.getElementById('resultText').textContent = message.content;
+ showResult();
+ }
+ // 不将answer消息添加到处理过程容器中,只显示在查询结果框中
+ break;
+ default:
+ console.log('Unknown message type:', message.type);
+ }
+ } catch (error) {
+ console.error('Error parsing message:', error);
+ }
+}
+
+// 工具函数:开始实时消息流
+function startMessageStream() {
+ closeEventSource(); // 关闭之前的连接
+
+ eventSource = new EventSource('/stream-messages/');
+
+ eventSource.onopen = function(event) {
+ console.log('EventSource connection opened');
+ };
+
+ eventSource.onmessage = function(event) {
+ handleStreamMessage(event.data);
+ };
+
+ eventSource.onerror = function(event) {
+ console.error('EventSource error:', event);
+ if (eventSource.readyState === EventSource.CLOSED) {
+ console.log('EventSource connection closed');
+ }
+ };
+}
+
+// 加载文件功能
+document.getElementById('loadFilesBtn').addEventListener('click', async function() {
+ const button = this;
+ const filePathsInput = document.getElementById('filePaths').value;
+ const collectionName = document.getElementById('collectionName').value;
+ const collectionDesc = document.getElementById('collectionDesc').value;
+
+ if (!filePathsInput) {
+ showStatus('loadStatus', '请提供至少一个文件路径', 'error');
+ return;
+ }
+
+ const filePaths = filePathsInput.split(',').map(path => path.trim()).filter(path => path);
+
+ setButtonLoading(button, true);
+ showStatus('loadStatus', '正在加载文件...', 'loading');
+ hideResult();
+ hideProcessResult();
+
+ try {
+ const response = await fetch('/load-files/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ paths: filePaths,
+ collection_name: collectionName || undefined,
+ collection_description: collectionDesc || undefined
+ })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ showStatus('loadStatus', data.message, 'success');
+ } else {
+ showStatus('loadStatus', `加载失败: ${data.detail}`, 'error');
+ }
+ } catch (error) {
+ showStatus('loadStatus', `请求失败: ${error.message}`, 'error');
+ } finally {
+ setButtonLoading(button, false);
+ }
+});
+
+// 清空消息功能
+document.getElementById('clearMessagesBtn').addEventListener('click', async function() {
+ try {
+ const response = await fetch('/clear-messages/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ // 清空消息容器
+ const container = document.getElementById('messageContainer');
+ container.innerHTML = '';
+
+ // 清空查询结果
+ const resultText = document.getElementById('resultText');
+ resultText.textContent = '';
+
+ // 隐藏处理过程容器
+ hideProcessResult();
+
+ // 隐藏查询结果容器
+ hideResult();
+
+ showStatus('queryStatus', '消息已清空', 'success');
+ } else {
+ showStatus('queryStatus', '清空消息失败', 'error');
+ }
+ } catch (error) {
+ showStatus('queryStatus', `请求失败: ${error.message}`, 'error');
+ }
+});
+
+// 加载网站内容功能
+document.getElementById('loadWebsiteBtn').addEventListener('click', async function() {
+ const button = this;
+ const urlsInput = document.getElementById('websiteUrls').value;
+ const collectionName = document.getElementById('webCollectionName').value;
+ const collectionDesc = document.getElementById('webCollectionDesc').value;
+
+ if (!urlsInput) {
+ showStatus('webLoadStatus', '请提供至少一个网站URL', 'error');
+ return;
+ }
+
+ const urls = urlsInput.split(',').map(url => url.trim()).filter(url => url);
+
+ setButtonLoading(button, true);
+ showStatus('webLoadStatus', '正在加载网站内容...', 'loading');
+ hideResult();
+ hideProcessResult();
+
+ try {
+ const response = await fetch('/load-website/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ urls: urls,
+ collection_name: collectionName || undefined,
+ collection_description: collectionDesc || undefined
+ })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ showStatus('webLoadStatus', data.message, 'success');
+ } else {
+ showStatus('webLoadStatus', `加载失败: ${data.detail}`, 'error');
+ }
+ } catch (error) {
+ showStatus('webLoadStatus', `请求失败: ${error.message}`, 'error');
+ } finally {
+ setButtonLoading(button, false);
+ }
+});
+
+// 查询功能 - 使用实时流
+document.getElementById('queryBtn').addEventListener('click', async function() {
+ const button = this;
+ const queryText = document.getElementById('queryText').value;
+ const maxIter = parseInt(document.getElementById('maxIter').value);
+
+ if (!queryText) {
+ showStatus('queryStatus', '请输入查询问题', 'error');
+ return;
+ }
+
+ if (isNaN(maxIter) || maxIter < 1 || maxIter > 10) {
+ showStatus('queryStatus', '迭代次数必须是1到10之间的整数', 'error');
+ return;
+ }
+
+ if (isStreaming) {
+ console.log('Query already in progress, isStreaming:', isStreaming);
+ showStatus('queryStatus', '查询正在进行中,请等待完成', 'error');
+ return;
+ }
+
+ setButtonLoading(button, true);
+ showStatus('queryStatus', '正在启动查询...', 'loading');
+ hideResult();
+ hideProcessResult();
+
+ // 清空消息容器
+ const container = document.getElementById('messageContainer');
+ container.innerHTML = '';
+
+ try {
+ console.log('Starting new query, setting isStreaming to true');
+ isStreaming = true;
+
+ // 确保没有其他连接存在
+ if (window.currentEventSource) {
+ console.log('Closing existing EventSource connection');
+ window.currentEventSource.close();
+ window.currentEventSource = null;
+ }
+
+ // 使用EventSource直接连接到查询流
+ const eventSource = new EventSource(`/query-stream/?original_query=${encodeURIComponent(queryText)}&max_iter=${maxIter}`);
+
+ // 保存EventSource引用以便后续关闭
+ window.currentEventSource = eventSource;
+
+ eventSource.onopen = function(event) {
+ console.log('EventSource connection opened for query');
+ showStatus('queryStatus', ' 正在处理...', 'loading');
+ };
+
+ eventSource.onmessage = function(event) {
+ console.log('Received message:', event.data);
+ handleStreamMessage(event.data);
+ };
+
+ eventSource.onerror = function(event) {
+ console.error('EventSource error:', event);
+ if (eventSource.readyState === EventSource.CLOSED) {
+ console.log('EventSource connection closed due to error');
+ isStreaming = false;
+ setButtonLoading(button, false);
+ window.currentEventSource = null;
+ }
+ };
+
+ } catch (error) {
+ console.error('Query error:', error);
+ showStatus('queryStatus', `请求失败: ${error.message}`, 'error');
+ isStreaming = false;
+ setButtonLoading(button, false);
+ }
+});
+
+// 页面卸载时清理连接
+window.addEventListener('beforeunload', function() {
+ if (window.currentEventSource) {
+ window.currentEventSource.close();
+ window.currentEventSource = null;
+ }
+});
diff --git a/main.py b/main.py
index 7e268e2..e9e5eda 100644
--- a/main.py
+++ b/main.py
@@ -4,6 +4,7 @@ import uvicorn
from fastapi import Body, FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, StreamingResponse
+from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
import os
import asyncio
@@ -18,6 +19,11 @@ from deepsearcher.utils.message_stream import get_message_stream
app = FastAPI()
+# 配置静态文件服务
+current_dir = os.path.dirname(os.path.abspath(__file__))
+static_dir = os.path.join(current_dir, "deepsearcher", "templates", "static")
+app.mount("/static", StaticFiles(directory=static_dir), name="static")
+
config = Configuration()
init_config(config)
@@ -47,7 +53,7 @@ async def read_root():
Serve the main HTML page.
"""
current_dir = os.path.dirname(os.path.abspath(__file__))
- template_path = os.path.join(current_dir, "deepsearcher", "backend", "templates", "index.html")
+ template_path = os.path.join(current_dir, "deepsearcher", "templates", "html", "index.html")
try:
with open(template_path, encoding="utf-8") as file:
diff --git a/test_fix.py b/test_fix.py
deleted file mode 100644
index 5fb3044..0000000
--- a/test_fix.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env python3
-"""
-测试修复后的查询功能
-"""
-
-import sys
-import os
-sys.path.append(os.path.dirname(os.path.abspath(__file__)))
-
-from deepsearcher.configuration import Configuration, init_config
-from deepsearcher.online_query import query
-
-def test_query_fix():
- """测试修复后的查询功能"""
- print("=== 测试修复后的查询功能 ===")
-
- # 初始化配置
- config = Configuration()
- init_config(config)
-
- try:
- print("开始查询...")
- result_text, retrieval_results = query("什么是Milvus?", max_iter=1)
-
- print(f"查询完成!")
- print(f"结果长度: {len(result_text) if result_text else 0}")
- print(f"检索结果数量: {len(retrieval_results) if retrieval_results else 0}")
-
- if result_text:
- print(f"结果预览: {result_text[:200]}...")
-
- except Exception as e:
- import traceback
- print(f"查询失败: {e}")
- print(f"错误详情: {traceback.format_exc()}")
-
-if __name__ == "__main__":
- test_query_fix()