Spaces:
Sleeping
Sleeping
Commit ·
8fe50ee
0
Parent(s):
Initial commit
Browse files- .dockerignore +47 -0
- .gitattributes +35 -0
- .gitignore +75 -0
- Dockerfile +19 -0
- README.md +51 -0
- app.py +181 -0
- folder_chat_flow_google_hf_FIXED_v2_副本.json +283 -0
- gemini_chatbot.py +758 -0
- interface +151 -0
- static/css/style.css +1436 -0
- static/js/chat.js +512 -0
- static/js/knowledge-map.js +1140 -0
- templates/index.html +230 -0
- templates/knowledge_map.html +80 -0
- templates/settings.html +53 -0
- templates/view_document.html +197 -0
.dockerignore
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 排除特定文件夹
|
| 2 |
+
hf_ai_teaching_assistant/
|
| 3 |
+
VectorRAG/
|
| 4 |
+
|
| 5 |
+
# 版本控制
|
| 6 |
+
.git
|
| 7 |
+
.gitignore
|
| 8 |
+
.gitattributes
|
| 9 |
+
|
| 10 |
+
# 环境变量和敏感信息
|
| 11 |
+
.env
|
| 12 |
+
*.env
|
| 13 |
+
|
| 14 |
+
# 缓存和临时文件
|
| 15 |
+
__pycache__/
|
| 16 |
+
*.py[cod]
|
| 17 |
+
*$py.class
|
| 18 |
+
*.so
|
| 19 |
+
.Python
|
| 20 |
+
.pytest_cache/
|
| 21 |
+
.coverage
|
| 22 |
+
htmlcov/
|
| 23 |
+
|
| 24 |
+
# 系统文件
|
| 25 |
+
.DS_Store
|
| 26 |
+
Thumbs.db
|
| 27 |
+
*.swp
|
| 28 |
+
*.swo
|
| 29 |
+
*~
|
| 30 |
+
|
| 31 |
+
# 日志文件
|
| 32 |
+
*.log
|
| 33 |
+
logs/
|
| 34 |
+
log/
|
| 35 |
+
|
| 36 |
+
# 数据库文件
|
| 37 |
+
*.sqlite3
|
| 38 |
+
*.db
|
| 39 |
+
|
| 40 |
+
# 上传的示例文件,应该在运行时创建
|
| 41 |
+
uploads/*.pdf
|
| 42 |
+
uploads/*.txt
|
| 43 |
+
|
| 44 |
+
# 编辑器配置
|
| 45 |
+
.vscode/
|
| 46 |
+
.idea/
|
| 47 |
+
*.sublime-*
|
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 环境变量文件(包含API密钥)
|
| 2 |
+
.env
|
| 3 |
+
*.env
|
| 4 |
+
|
| 5 |
+
# 排除特定文件夹
|
| 6 |
+
/hf_ai_teaching_assistant/
|
| 7 |
+
hf_ai_teaching_assistant/
|
| 8 |
+
|
| 9 |
+
# 上传的文件目录
|
| 10 |
+
/uploads/
|
| 11 |
+
uploads/
|
| 12 |
+
*.pdf
|
| 13 |
+
*.txt
|
| 14 |
+
*.doc
|
| 15 |
+
*.docx
|
| 16 |
+
|
| 17 |
+
# 虚拟环境
|
| 18 |
+
.venv/
|
| 19 |
+
venv/
|
| 20 |
+
ENV/
|
| 21 |
+
env/
|
| 22 |
+
.virtualenv/
|
| 23 |
+
virtualenv/
|
| 24 |
+
.python-version
|
| 25 |
+
|
| 26 |
+
# Python缓存文件
|
| 27 |
+
__pycache__/
|
| 28 |
+
*.py[cod]
|
| 29 |
+
*$py.class
|
| 30 |
+
*.so
|
| 31 |
+
.Python
|
| 32 |
+
.pytest_cache/
|
| 33 |
+
.coverage
|
| 34 |
+
htmlcov/
|
| 35 |
+
|
| 36 |
+
# 系统文件
|
| 37 |
+
.DS_Store
|
| 38 |
+
Thumbs.db
|
| 39 |
+
*.swp
|
| 40 |
+
*.swo
|
| 41 |
+
*~
|
| 42 |
+
|
| 43 |
+
# 日志文件
|
| 44 |
+
*.log
|
| 45 |
+
logs/
|
| 46 |
+
log/
|
| 47 |
+
|
| 48 |
+
# 数据库文件
|
| 49 |
+
*.sqlite3
|
| 50 |
+
*.db
|
| 51 |
+
VectorRAG/chroma.sqlite3
|
| 52 |
+
|
| 53 |
+
# 个人配置文件
|
| 54 |
+
.vscode/
|
| 55 |
+
.idea/
|
| 56 |
+
*.sublime-*
|
| 57 |
+
|
| 58 |
+
# 临时文件
|
| 59 |
+
temp/
|
| 60 |
+
tmp/
|
| 61 |
+
|
| 62 |
+
# 编译生成的文件
|
| 63 |
+
dist/
|
| 64 |
+
build/
|
| 65 |
+
*.egg-info/
|
| 66 |
+
|
| 67 |
+
# Jupyter Notebook
|
| 68 |
+
.ipynb_checkpoints
|
| 69 |
+
|
| 70 |
+
# 其他可能包含敏感信息的文件
|
| 71 |
+
config.json
|
| 72 |
+
secrets.json
|
| 73 |
+
credentials.json
|
| 74 |
+
token.json
|
| 75 |
+
*_key.json
|
Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY . .
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# 确保上传目录存在
|
| 10 |
+
RUN mkdir -p uploads
|
| 11 |
+
|
| 12 |
+
# 设置环境变量
|
| 13 |
+
ENV PYTHONUNBUFFERED=1
|
| 14 |
+
|
| 15 |
+
# 暴露端口
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
|
| 18 |
+
# 启动命令
|
| 19 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:7860", "gemini_chatbot:app"]
|
README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Gemini AI Teaching Assistant
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Gemini AI Teaching Assistant
|
| 13 |
+
|
| 14 |
+
An intelligent teaching assistant powered by Google's Gemini API, designed to help educators organize teaching materials and interact with educational content.
|
| 15 |
+
|
| 16 |
+
## Features
|
| 17 |
+
|
| 18 |
+
- Upload and process PDF and TXT documents
|
| 19 |
+
- Automatic summarization and tagging of documents
|
| 20 |
+
- Interactive knowledge map visualization
|
| 21 |
+
- Dynamic knowledge folder organization
|
| 22 |
+
- Generate word clouds from document content
|
| 23 |
+
- Chat with AI about uploaded documents
|
| 24 |
+
|
| 25 |
+
## Usage
|
| 26 |
+
|
| 27 |
+
1. Upload your teaching materials (PDF/TXT)
|
| 28 |
+
2. View automatically generated summaries and tags
|
| 29 |
+
3. Browse your knowledge folders organized by tags
|
| 30 |
+
4. Use the knowledge map to visualize connections
|
| 31 |
+
5. Chat with the AI assistant about your documents
|
| 32 |
+
|
| 33 |
+
## Setup for Hugging Face
|
| 34 |
+
|
| 35 |
+
1. Fork this repository to your Hugging Face account
|
| 36 |
+
2. Add your Google Gemini API key as a secret in your Hugging Face space:
|
| 37 |
+
- Go to Settings > Repository Secrets
|
| 38 |
+
- Add a new secret with name `GOOGLE_API_KEY` and your API key as the value
|
| 39 |
+
|
| 40 |
+
## Technology
|
| 41 |
+
|
| 42 |
+
- Flask web framework
|
| 43 |
+
- Google Gemini API for AI capabilities
|
| 44 |
+
- PDF text extraction
|
| 45 |
+
- Natural language processing
|
| 46 |
+
- Interactive visualizations
|
| 47 |
+
|
| 48 |
+
## Requirements
|
| 49 |
+
|
| 50 |
+
- Google API key for Gemini API (required for functionality)
|
| 51 |
+
- Minimum 2GB RAM Space recommended
|
app.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import chainlit as cl
|
| 2 |
+
import requests
|
| 3 |
+
import google.generativeai as genai
|
| 4 |
+
import os
|
| 5 |
+
import json
|
| 6 |
+
from typing import List
|
| 7 |
+
import tempfile
|
| 8 |
+
|
| 9 |
+
# Langflow API configuration
|
| 10 |
+
LANGFLOW_URL = "http://127.0.0.1:7861/api/v1/run/YOUR_NEW_FLOW_ID" # 替换为新的 Flow ID
|
| 11 |
+
HEADERS = {
|
| 12 |
+
"Content-Type": "application/json"
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
# Gemini API configuration
|
| 16 |
+
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "your_api_key_here")
|
| 17 |
+
genai.configure(api_key=GOOGLE_API_KEY)
|
| 18 |
+
model = genai.GenerativeModel('gemini-pro')
|
| 19 |
+
|
| 20 |
+
# 初始化文件存储
|
| 21 |
+
@cl.on_chat_start
|
| 22 |
+
async def start():
|
| 23 |
+
# 初始化空的文件列表
|
| 24 |
+
cl.user_session.set("uploaded_files", [])
|
| 25 |
+
|
| 26 |
+
# 创建文件上传组件
|
| 27 |
+
await cl.Message(
|
| 28 |
+
content="欢迎!您可以上传文件或直接提问。我会同时使用 Langflow 和 Gemini 来回答。",
|
| 29 |
+
actions=[
|
| 30 |
+
cl.Action(name="upload_file", label="上传文件", description="上传PDF或TXT文件进行分析")
|
| 31 |
+
]
|
| 32 |
+
).send()
|
| 33 |
+
|
| 34 |
+
# 处理文件上传动作
|
| 35 |
+
@cl.action_callback("upload_file")
|
| 36 |
+
async def handle_upload_action():
|
| 37 |
+
# 在此处要求用户上传文件
|
| 38 |
+
files = await cl.AskFileMessage(
|
| 39 |
+
content="请选择要上传的文件(PDF或TXT)",
|
| 40 |
+
accept=["application/pdf", "text/plain"],
|
| 41 |
+
max_size_mb=20,
|
| 42 |
+
timeout=180,
|
| 43 |
+
).send()
|
| 44 |
+
|
| 45 |
+
if not files:
|
| 46 |
+
await cl.Message("没有收到文件或上传已取消。").send()
|
| 47 |
+
return
|
| 48 |
+
|
| 49 |
+
# 处理上传的文件
|
| 50 |
+
await process_files(files)
|
| 51 |
+
|
| 52 |
+
# 处理文件的函数
|
| 53 |
+
async def process_files(files: List):
|
| 54 |
+
uploaded_files = cl.user_session.get("uploaded_files", [])
|
| 55 |
+
|
| 56 |
+
for file in files:
|
| 57 |
+
# 创建临时文件
|
| 58 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=f".{file.name.split('.')[-1]}") as temp_file:
|
| 59 |
+
temp_file.write(file.content)
|
| 60 |
+
temp_path = temp_file.name
|
| 61 |
+
|
| 62 |
+
# 提取文件内容
|
| 63 |
+
content = ""
|
| 64 |
+
|
| 65 |
+
if file.name.lower().endswith('.pdf'):
|
| 66 |
+
try:
|
| 67 |
+
import PyPDF2
|
| 68 |
+
with open(temp_path, 'rb') as f:
|
| 69 |
+
reader = PyPDF2.PdfReader(f)
|
| 70 |
+
for page in reader.pages:
|
| 71 |
+
content += page.extract_text() or ""
|
| 72 |
+
except Exception as e:
|
| 73 |
+
await cl.Message(f"读取PDF文件时出错: {str(e)}").send()
|
| 74 |
+
content = f"[无法读取PDF内容: {str(e)}]"
|
| 75 |
+
|
| 76 |
+
elif file.name.lower().endswith('.txt'):
|
| 77 |
+
try:
|
| 78 |
+
with open(temp_path, 'r', encoding='utf-8') as f:
|
| 79 |
+
content = f.read()
|
| 80 |
+
except Exception as e:
|
| 81 |
+
await cl.Message(f"读取TXT文件时出错: {str(e)}").send()
|
| 82 |
+
content = f"[无法读取TXT内容: {str(e)}]"
|
| 83 |
+
|
| 84 |
+
# 清理临时文件
|
| 85 |
+
os.unlink(temp_path)
|
| 86 |
+
|
| 87 |
+
# 将内容限制在合理范围内(对于大文件)
|
| 88 |
+
if len(content) > 10000:
|
| 89 |
+
content_to_save = content[:10000] + "... [内容被截断]"
|
| 90 |
+
else:
|
| 91 |
+
content_to_save = content
|
| 92 |
+
|
| 93 |
+
# 获取文件摘要
|
| 94 |
+
try:
|
| 95 |
+
summary = await get_summary_from_gemini(content[:10000])
|
| 96 |
+
except Exception as e:
|
| 97 |
+
summary = f"生成摘要失败: {str(e)}"
|
| 98 |
+
|
| 99 |
+
# 将文件添加到已上传列表
|
| 100 |
+
uploaded_files.append({
|
| 101 |
+
"name": file.name,
|
| 102 |
+
"content": content_to_save,
|
| 103 |
+
"summary": summary
|
| 104 |
+
})
|
| 105 |
+
|
| 106 |
+
# 更新session中的文件列表
|
| 107 |
+
cl.user_session.set("uploaded_files", uploaded_files)
|
| 108 |
+
|
| 109 |
+
# 显示当前所有上传的文件
|
| 110 |
+
files_list = "\n".join([f"- {f['name']}:\n 摘要: {f['summary'][:100]}..." for f in uploaded_files])
|
| 111 |
+
await cl.Message(f"已上传的文件:\n{files_list}").send()
|
| 112 |
+
|
| 113 |
+
# 提示用户可以开始提问
|
| 114 |
+
await cl.Message("您现在可以问我关于这些文件的问题了!").send()
|
| 115 |
+
|
| 116 |
+
# 从Gemini获取摘要
|
| 117 |
+
async def get_summary_from_gemini(text):
|
| 118 |
+
try:
|
| 119 |
+
prompt = f"请提供以下文档的简短摘要(不超过100字):\n\n{text[:10000]}"
|
| 120 |
+
response = model.generate_content(prompt)
|
| 121 |
+
return response.text
|
| 122 |
+
except Exception as e:
|
| 123 |
+
return f"无法生成摘要: {str(e)}"
|
| 124 |
+
|
| 125 |
+
@cl.on_message
|
| 126 |
+
async def main(message: str):
|
| 127 |
+
try:
|
| 128 |
+
# 获取所有已上传的文件
|
| 129 |
+
uploaded_files = cl.user_session.get("uploaded_files", [])
|
| 130 |
+
|
| 131 |
+
# 准备发送给 Langflow 的数据
|
| 132 |
+
payload = {
|
| 133 |
+
"input_value": message,
|
| 134 |
+
"output_type": "chat",
|
| 135 |
+
"input_type": "chat",
|
| 136 |
+
"files": uploaded_files # 添加文件信息到请求中
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
# Langflow 处理
|
| 140 |
+
try:
|
| 141 |
+
response = requests.post(LANGFLOW_URL, json=payload, headers=HEADERS, timeout=30)
|
| 142 |
+
response.raise_for_status()
|
| 143 |
+
langflow_response = response.json().get('output', 'No response')
|
| 144 |
+
await cl.Message(content=f"Langflow 回复:\n{langflow_response}").send()
|
| 145 |
+
except Exception as langflow_err:
|
| 146 |
+
# 如果Langflow处理失败,记录错误但继续Gemini处理
|
| 147 |
+
await cl.Message(content=f"Langflow API 错误: {str(langflow_err)}", type="error").send()
|
| 148 |
+
|
| 149 |
+
# 如果有文件,将文件内容添加到 Gemini 的上下文中
|
| 150 |
+
if uploaded_files:
|
| 151 |
+
context = f"Context from uploaded files:\n"
|
| 152 |
+
for file in uploaded_files:
|
| 153 |
+
# 使用摘要而不是全文,避免超出提示长度限制
|
| 154 |
+
context += f"\nFile: {file['name']}\nSummary: {file.get('summary', '无摘要')}\n"
|
| 155 |
+
prompt = f"{context}\n\nUser question: {message}"
|
| 156 |
+
else:
|
| 157 |
+
prompt = message
|
| 158 |
+
|
| 159 |
+
# Gemini 处理
|
| 160 |
+
gemini_response = model.generate_content(prompt).text
|
| 161 |
+
await cl.Message(content=f"Gemini 回复:\n{gemini_response}").send()
|
| 162 |
+
|
| 163 |
+
except Exception as e:
|
| 164 |
+
await cl.Message(content=f"处理错误: {str(e)}", type="error").send()
|
| 165 |
+
|
| 166 |
+
# 清除文件的功能
|
| 167 |
+
@cl.action_callback("clear_files")
|
| 168 |
+
async def clear_files():
|
| 169 |
+
cl.user_session.set("uploaded_files", [])
|
| 170 |
+
await cl.Message("已清除所有上传的文件记录。").send()
|
| 171 |
+
|
| 172 |
+
# 显示文件列表的功能
|
| 173 |
+
@cl.action_callback("list_files")
|
| 174 |
+
async def list_files():
|
| 175 |
+
uploaded_files = cl.user_session.get("uploaded_files", [])
|
| 176 |
+
if uploaded_files:
|
| 177 |
+
files_list = "\n".join([f"- {f['name']}" for f in uploaded_files])
|
| 178 |
+
await cl.Message(f"当前上传的文件:\n{files_list}").send()
|
| 179 |
+
else:
|
| 180 |
+
await cl.Message("当前没有上传的文件。").send()
|
| 181 |
+
|
folder_chat_flow_google_hf_FIXED_v2_副本.json
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "ca3e4a9c-11c0-42ea-8557-581b877445df",
|
| 3 |
+
"name": "文档自动分类与提问",
|
| 4 |
+
"description": "文档上传后自动摘要、关键词分类,并支持基于 folder 的问答。",
|
| 5 |
+
"tags": [
|
| 6 |
+
"document",
|
| 7 |
+
"folder",
|
| 8 |
+
"chat",
|
| 9 |
+
"langflow"
|
| 10 |
+
],
|
| 11 |
+
"endpoint_name": "Langflow API",
|
| 12 |
+
"is_component": false,
|
| 13 |
+
"last_tested_version": "1.3.3",
|
| 14 |
+
"data": {
|
| 15 |
+
"nodes": [
|
| 16 |
+
{
|
| 17 |
+
"id": "file",
|
| 18 |
+
"type": "File",
|
| 19 |
+
"position": {
|
| 20 |
+
"x": 0,
|
| 21 |
+
"y": 0
|
| 22 |
+
},
|
| 23 |
+
"data": {
|
| 24 |
+
"node": {
|
| 25 |
+
"id": "file",
|
| 26 |
+
"type": "File",
|
| 27 |
+
"data": {
|
| 28 |
+
"inputs": {},
|
| 29 |
+
"template": ""
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "split",
|
| 36 |
+
"type": "Split Text",
|
| 37 |
+
"position": {
|
| 38 |
+
"x": 200,
|
| 39 |
+
"y": 0
|
| 40 |
+
},
|
| 41 |
+
"data": {
|
| 42 |
+
"node": {
|
| 43 |
+
"id": "split",
|
| 44 |
+
"type": "Split Text",
|
| 45 |
+
"data": {
|
| 46 |
+
"inputs": {},
|
| 47 |
+
"template": ""
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"id": "prompt_summary",
|
| 54 |
+
"type": "Prompt",
|
| 55 |
+
"position": {
|
| 56 |
+
"x": 400,
|
| 57 |
+
"y": -100
|
| 58 |
+
},
|
| 59 |
+
"data": {
|
| 60 |
+
"node": {
|
| 61 |
+
"id": "prompt_summary",
|
| 62 |
+
"type": "Prompt",
|
| 63 |
+
"data": {
|
| 64 |
+
"inputs": {},
|
| 65 |
+
"template": "请总结以下文档内容:\n\n{{text}}"
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"id": "llm_summary",
|
| 72 |
+
"type": "Google Generative AI",
|
| 73 |
+
"position": {
|
| 74 |
+
"x": 600,
|
| 75 |
+
"y": -100
|
| 76 |
+
},
|
| 77 |
+
"data": {
|
| 78 |
+
"node": {
|
| 79 |
+
"id": "llm_summary",
|
| 80 |
+
"type": "Google Generative AI",
|
| 81 |
+
"data": {
|
| 82 |
+
"inputs": {},
|
| 83 |
+
"template": ""
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"id": "prompt_keywords",
|
| 90 |
+
"type": "Prompt",
|
| 91 |
+
"position": {
|
| 92 |
+
"x": 400,
|
| 93 |
+
"y": 100
|
| 94 |
+
},
|
| 95 |
+
"data": {
|
| 96 |
+
"node": {
|
| 97 |
+
"id": "prompt_keywords",
|
| 98 |
+
"type": "Prompt",
|
| 99 |
+
"data": {
|
| 100 |
+
"inputs": {},
|
| 101 |
+
"template": "请从以下文本中提取 3-5 个关键词:\n\n{{text}}"
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"id": "llm_keywords",
|
| 108 |
+
"type": "Google Generative AI",
|
| 109 |
+
"position": {
|
| 110 |
+
"x": 600,
|
| 111 |
+
"y": 100
|
| 112 |
+
},
|
| 113 |
+
"data": {
|
| 114 |
+
"node": {
|
| 115 |
+
"id": "llm_keywords",
|
| 116 |
+
"type": "Google Generative AI",
|
| 117 |
+
"data": {
|
| 118 |
+
"inputs": {},
|
| 119 |
+
"template": ""
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"id": "hf_embeddings",
|
| 126 |
+
"type": "HuggingFace Embeddings",
|
| 127 |
+
"position": {
|
| 128 |
+
"x": 800,
|
| 129 |
+
"y": 0
|
| 130 |
+
},
|
| 131 |
+
"data": {
|
| 132 |
+
"node": {
|
| 133 |
+
"id": "hf_embeddings",
|
| 134 |
+
"type": "HuggingFace Embeddings",
|
| 135 |
+
"data": {
|
| 136 |
+
"inputs": {},
|
| 137 |
+
"template": ""
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"id": "chroma",
|
| 144 |
+
"type": "Chroma DB",
|
| 145 |
+
"position": {
|
| 146 |
+
"x": 1000,
|
| 147 |
+
"y": 0
|
| 148 |
+
},
|
| 149 |
+
"data": {
|
| 150 |
+
"node": {
|
| 151 |
+
"id": "chroma",
|
| 152 |
+
"type": "Chroma DB",
|
| 153 |
+
"data": {
|
| 154 |
+
"inputs": {},
|
| 155 |
+
"template": ""
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"id": "agent",
|
| 162 |
+
"type": "Agent",
|
| 163 |
+
"position": {
|
| 164 |
+
"x": 1200,
|
| 165 |
+
"y": 0
|
| 166 |
+
},
|
| 167 |
+
"data": {
|
| 168 |
+
"node": {
|
| 169 |
+
"id": "agent",
|
| 170 |
+
"type": "Agent",
|
| 171 |
+
"data": {
|
| 172 |
+
"inputs": {},
|
| 173 |
+
"template": ""
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
"id": "chat_input",
|
| 180 |
+
"type": "Chat Input",
|
| 181 |
+
"position": {
|
| 182 |
+
"x": 1000,
|
| 183 |
+
"y": -200
|
| 184 |
+
},
|
| 185 |
+
"data": {
|
| 186 |
+
"node": {
|
| 187 |
+
"id": "chat_input",
|
| 188 |
+
"type": "Chat Input",
|
| 189 |
+
"data": {
|
| 190 |
+
"inputs": {},
|
| 191 |
+
"template": ""
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"id": "chat_output",
|
| 198 |
+
"type": "Chat Output",
|
| 199 |
+
"position": {
|
| 200 |
+
"x": 1400,
|
| 201 |
+
"y": 0
|
| 202 |
+
},
|
| 203 |
+
"data": {
|
| 204 |
+
"node": {
|
| 205 |
+
"id": "chat_output",
|
| 206 |
+
"type": "Chat Output",
|
| 207 |
+
"data": {
|
| 208 |
+
"inputs": {},
|
| 209 |
+
"template": ""
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
],
|
| 215 |
+
"edges": [
|
| 216 |
+
{
|
| 217 |
+
"source": "file",
|
| 218 |
+
"target": "split",
|
| 219 |
+
"sourceHandle": "output",
|
| 220 |
+
"targetHandle": "input"
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"source": "split",
|
| 224 |
+
"target": "prompt_summary",
|
| 225 |
+
"sourceHandle": "output",
|
| 226 |
+
"targetHandle": "input"
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
"source": "prompt_summary",
|
| 230 |
+
"target": "llm_summary",
|
| 231 |
+
"sourceHandle": "output",
|
| 232 |
+
"targetHandle": "input"
|
| 233 |
+
},
|
| 234 |
+
{
|
| 235 |
+
"source": "split",
|
| 236 |
+
"target": "prompt_keywords",
|
| 237 |
+
"sourceHandle": "output",
|
| 238 |
+
"targetHandle": "input"
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"source": "prompt_keywords",
|
| 242 |
+
"target": "llm_keywords",
|
| 243 |
+
"sourceHandle": "output",
|
| 244 |
+
"targetHandle": "input"
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"source": "split",
|
| 248 |
+
"target": "hf_embeddings",
|
| 249 |
+
"sourceHandle": "output",
|
| 250 |
+
"targetHandle": "input"
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"source": "hf_embeddings",
|
| 254 |
+
"target": "chroma",
|
| 255 |
+
"sourceHandle": "output",
|
| 256 |
+
"targetHandle": "input"
|
| 257 |
+
},
|
| 258 |
+
{
|
| 259 |
+
"source": "chat_input",
|
| 260 |
+
"target": "agent",
|
| 261 |
+
"sourceHandle": "output",
|
| 262 |
+
"targetHandle": "input"
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"source": "chroma",
|
| 266 |
+
"target": "agent",
|
| 267 |
+
"sourceHandle": "output",
|
| 268 |
+
"targetHandle": "input"
|
| 269 |
+
},
|
| 270 |
+
{
|
| 271 |
+
"source": "agent",
|
| 272 |
+
"target": "chat_output",
|
| 273 |
+
"sourceHandle": "output",
|
| 274 |
+
"targetHandle": "input"
|
| 275 |
+
}
|
| 276 |
+
],
|
| 277 |
+
"viewport": {
|
| 278 |
+
"x": 0,
|
| 279 |
+
"y": 0,
|
| 280 |
+
"zoom": 1
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
}
|
gemini_chatbot.py
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, request, jsonify, render_template, redirect, url_for, send_from_directory, session
|
| 2 |
+
import requests
|
| 3 |
+
import os
|
| 4 |
+
import uuid
|
| 5 |
+
from werkzeug.utils import secure_filename
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
import PyPDF2 # Import PyPDF2
|
| 8 |
+
import logging # Add logging
|
| 9 |
+
import json # Add json for JSON handling
|
| 10 |
+
import sys
|
| 11 |
+
import traceback
|
| 12 |
+
import jieba # 中文分词
|
| 13 |
+
import re
|
| 14 |
+
from collections import Counter
|
| 15 |
+
from wordcloud import WordCloud
|
| 16 |
+
import matplotlib
|
| 17 |
+
matplotlib.use('Agg') # 设置Matplotlib为非交互模式
|
| 18 |
+
import matplotlib.pyplot as plt
|
| 19 |
+
from io import BytesIO
|
| 20 |
+
import base64
|
| 21 |
+
|
| 22 |
+
# 加载环境变量
|
| 23 |
+
load_dotenv()
|
| 24 |
+
|
| 25 |
+
app = Flask(__name__)
|
| 26 |
+
app.config['UPLOAD_FOLDER'] = 'uploads'
|
| 27 |
+
app.config['ALLOWED_EXTENSIONS'] = {'pdf', 'txt'} # 仅限PDF和TXT文件
|
| 28 |
+
app.secret_key = os.getenv('FLASK_SECRET_KEY', os.urandom(24)) # Needed for session management
|
| 29 |
+
app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # 10MB 大小限制
|
| 30 |
+
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
| 31 |
+
|
| 32 |
+
# Configure logging
|
| 33 |
+
logging.basicConfig(
|
| 34 |
+
level=logging.INFO, # 使用INFO级别减少日志量
|
| 35 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 36 |
+
handlers=[
|
| 37 |
+
logging.StreamHandler(sys.stdout)
|
| 38 |
+
# 移除文件处理器,避免权限问题
|
| 39 |
+
]
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
API_KEY = os.getenv("GOOGLE_API_KEY", "")
|
| 43 |
+
API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent" # Using latest Flash model which is good for RAG
|
| 44 |
+
|
| 45 |
+
# 创建文件夹存储结构
|
| 46 |
+
knowledge_folders = {
|
| 47 |
+
"Instructional Theories": ["instructional_theory1.pdf", "instructional_theory2.pdf", "instructional_theory3.pdf", "instructional_theory4.pdf"],
|
| 48 |
+
"Learner Characteristics": ["learner_characteristics1.pdf", "learner_characteristics2.pdf", "learner_characteristics3.pdf"],
|
| 49 |
+
"Design Models": ["design_model1.pdf", "design_model2.pdf", "design_model3.pdf", "design_model4.pdf", "design_model5.pdf"],
|
| 50 |
+
"Assessment Methods": ["assessment_method1.pdf", "assessment_method2.pdf"]
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
def get_dynamic_knowledge_folders(session_files):
|
| 54 |
+
"""Generate dynamic knowledge folder structure based on uploaded files and their tags"""
|
| 55 |
+
dynamic_folders = {}
|
| 56 |
+
|
| 57 |
+
# 如果没有上传文件,返回空结构
|
| 58 |
+
if not session_files:
|
| 59 |
+
return {}
|
| 60 |
+
|
| 61 |
+
# 遍历所有上传的文件
|
| 62 |
+
for file_info in session_files:
|
| 63 |
+
filename = file_info.get('filename', '')
|
| 64 |
+
tags = file_info.get('tags', [])
|
| 65 |
+
|
| 66 |
+
# 如果没有标签,添加到"未分类"文件夹
|
| 67 |
+
if not tags:
|
| 68 |
+
if "Uncategorized" not in dynamic_folders:
|
| 69 |
+
dynamic_folders["Uncategorized"] = []
|
| 70 |
+
dynamic_folders["Uncategorized"].append(filename)
|
| 71 |
+
continue
|
| 72 |
+
|
| 73 |
+
# 对每个标签创建或更新文件夹
|
| 74 |
+
for tag in tags:
|
| 75 |
+
if tag not in dynamic_folders:
|
| 76 |
+
dynamic_folders[tag] = []
|
| 77 |
+
if filename not in dynamic_folders[tag]:
|
| 78 |
+
dynamic_folders[tag].append(filename)
|
| 79 |
+
|
| 80 |
+
# 记录日志
|
| 81 |
+
logging.debug(f"Generated dynamic knowledge folders: {dynamic_folders}")
|
| 82 |
+
|
| 83 |
+
return dynamic_folders
|
| 84 |
+
|
| 85 |
+
def allowed_file(filename):
|
| 86 |
+
"""检查文件类型是否允许上传"""
|
| 87 |
+
if '.' not in filename:
|
| 88 |
+
return False
|
| 89 |
+
ext = filename.rsplit('.', 1)[1].lower()
|
| 90 |
+
return ext in app.config['ALLOWED_EXTENSIONS']
|
| 91 |
+
|
| 92 |
+
def extract_text_from_pdf(file_path):
|
| 93 |
+
"""Extract text from a PDF file."""
|
| 94 |
+
text = ""
|
| 95 |
+
try:
|
| 96 |
+
# 检查文件是否存在
|
| 97 |
+
if not os.path.exists(file_path):
|
| 98 |
+
logging.error(f"PDF file does not exist: {file_path}")
|
| 99 |
+
return None
|
| 100 |
+
|
| 101 |
+
# 检查文件大小
|
| 102 |
+
file_size = os.path.getsize(file_path)
|
| 103 |
+
logging.debug(f"PDF file size: {file_size} bytes")
|
| 104 |
+
|
| 105 |
+
# 检查文件是否为空
|
| 106 |
+
if file_size == 0:
|
| 107 |
+
logging.error(f"PDF file is empty: {file_path}")
|
| 108 |
+
return None
|
| 109 |
+
|
| 110 |
+
logging.debug(f"Opening PDF file: {file_path}")
|
| 111 |
+
with open(file_path, 'rb') as file:
|
| 112 |
+
try:
|
| 113 |
+
reader = PyPDF2.PdfReader(file)
|
| 114 |
+
logging.debug(f"PDF has {len(reader.pages)} pages")
|
| 115 |
+
|
| 116 |
+
if len(reader.pages) == 0:
|
| 117 |
+
logging.warning(f"PDF has no pages: {file_path}")
|
| 118 |
+
return None
|
| 119 |
+
|
| 120 |
+
# 检查文件是否加密
|
| 121 |
+
if reader.is_encrypted:
|
| 122 |
+
logging.error(f"PDF is encrypted: {file_path}")
|
| 123 |
+
return None
|
| 124 |
+
|
| 125 |
+
# 提取文本
|
| 126 |
+
for i, page in enumerate(reader.pages):
|
| 127 |
+
page_text = page.extract_text() or ""
|
| 128 |
+
text += page_text
|
| 129 |
+
logging.debug(f"Extracted {len(page_text)} characters from page {i+1}")
|
| 130 |
+
|
| 131 |
+
if not text.strip():
|
| 132 |
+
logging.warning(f"Extracted text is empty from PDF: {file_path}")
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
logging.info(f"Successfully extracted {len(text)} characters from {file_path}")
|
| 136 |
+
return text
|
| 137 |
+
except PyPDF2.errors.PdfReadError as pdf_err:
|
| 138 |
+
logging.error(f"PyPDF2 read error: {pdf_err}")
|
| 139 |
+
return None
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logging.error(f"Error extracting text from PDF {file_path}: {e}")
|
| 142 |
+
logging.error(traceback.format_exc())
|
| 143 |
+
return None
|
| 144 |
+
|
| 145 |
+
def extract_text_from_txt(file_path):
|
| 146 |
+
"""Extract text from a TXT file."""
|
| 147 |
+
try:
|
| 148 |
+
# 检查文件是否存在
|
| 149 |
+
if not os.path.exists(file_path):
|
| 150 |
+
logging.error(f"TXT file does not exist: {file_path}")
|
| 151 |
+
return None
|
| 152 |
+
|
| 153 |
+
# 检查文件大小
|
| 154 |
+
file_size = os.path.getsize(file_path)
|
| 155 |
+
logging.debug(f"TXT file size: {file_size} bytes")
|
| 156 |
+
|
| 157 |
+
# 检查文件是否为空
|
| 158 |
+
if file_size == 0:
|
| 159 |
+
logging.error(f"TXT file is empty: {file_path}")
|
| 160 |
+
return None
|
| 161 |
+
|
| 162 |
+
logging.debug(f"Opening TXT file: {file_path}")
|
| 163 |
+
|
| 164 |
+
# 尝试不同的编码
|
| 165 |
+
encodings = ['utf-8', 'gbk', 'latin-1']
|
| 166 |
+
for encoding in encodings:
|
| 167 |
+
try:
|
| 168 |
+
with open(file_path, 'r', encoding=encoding) as file:
|
| 169 |
+
text = file.read()
|
| 170 |
+
|
| 171 |
+
# 如果读取成功且文本不为空
|
| 172 |
+
if text and len(text.strip()) > 0:
|
| 173 |
+
logging.info(f"Successfully extracted {len(text)} characters from {file_path} using {encoding} encoding")
|
| 174 |
+
return text
|
| 175 |
+
except UnicodeDecodeError:
|
| 176 |
+
logging.debug(f"Failed to decode {file_path} with {encoding} encoding, trying next...")
|
| 177 |
+
continue
|
| 178 |
+
|
| 179 |
+
logging.warning(f"Failed to extract text with all encodings from {file_path}")
|
| 180 |
+
return None
|
| 181 |
+
except Exception as e:
|
| 182 |
+
logging.error(f"Error extracting text from TXT {file_path}: {e}")
|
| 183 |
+
logging.error(traceback.format_exc())
|
| 184 |
+
return None
|
| 185 |
+
|
| 186 |
+
def get_summary_and_tags_from_gemini(text_content):
|
| 187 |
+
"""Use Gemini API to generate summary and tags from text."""
|
| 188 |
+
if not API_KEY or not text_content:
|
| 189 |
+
return "Could not generate summary.", []
|
| 190 |
+
|
| 191 |
+
# Use triple quotes for the f-string to handle internal quotes correctly
|
| 192 |
+
# Double the curly braces for the example JSON to output literal braces
|
| 193 |
+
prompt = f"""Please summarize the following document and extract up to 5 relevant keywords (tags) as a JSON list. Document content:
|
| 194 |
+
|
| 195 |
+
{text_content[:10000]}
|
| 196 |
+
|
| 197 |
+
Respond ONLY with a JSON object containing 'summary' and 'tags' keys, like this: {{ "summary": "Your summary here", "tags": ["tag1", "tag2"] }}. Do not include any other text or markdown formatting."""
|
| 198 |
+
|
| 199 |
+
try:
|
| 200 |
+
response = requests.post(
|
| 201 |
+
f"{API_URL}?key={API_KEY}",
|
| 202 |
+
json={
|
| 203 |
+
"contents": [{"parts": [{"text": prompt}]}],
|
| 204 |
+
"generationConfig": {
|
| 205 |
+
"responseMimeType": "application/json", # Request JSON output
|
| 206 |
+
}
|
| 207 |
+
},
|
| 208 |
+
timeout=45 # Increased timeout for potentially longer processing
|
| 209 |
+
)
|
| 210 |
+
response.raise_for_status()
|
| 211 |
+
data = response.json()
|
| 212 |
+
|
| 213 |
+
if 'candidates' in data and data['candidates']:
|
| 214 |
+
candidate = data['candidates'][0]
|
| 215 |
+
if 'content' in candidate and 'parts' in candidate['content']:
|
| 216 |
+
part_text = candidate['content']['parts'][0].get('text', '{}')
|
| 217 |
+
try:
|
| 218 |
+
result_json = json.loads(part_text)
|
| 219 |
+
summary = result_json.get("summary", "Summary not generated.")
|
| 220 |
+
tags = result_json.get("tags", [])
|
| 221 |
+
logging.info(f"Gemini generated summary and tags.")
|
| 222 |
+
return summary, tags
|
| 223 |
+
except json.JSONDecodeError as json_e:
|
| 224 |
+
logging.error(f"Error decoding Gemini JSON response: {json_e}\nResponse text: {part_text}")
|
| 225 |
+
return "Failed to parse summary response.", []
|
| 226 |
+
return "Could not get summary from API response.", []
|
| 227 |
+
except requests.exceptions.RequestException as e:
|
| 228 |
+
logging.error(f"Gemini API request failed for summary/tags: {e}")
|
| 229 |
+
return f"API request failed: {e}", []
|
| 230 |
+
except Exception as e:
|
| 231 |
+
logging.error(f"Error getting summary/tags: {e}")
|
| 232 |
+
return f"An error occurred: {e}", []
|
| 233 |
+
|
| 234 |
+
@app.route('/')
|
| 235 |
+
def home():
|
| 236 |
+
"""Render home page"""
|
| 237 |
+
# 获取用户上传的文件信息
|
| 238 |
+
uploaded_files = session.get('uploaded_files', [])
|
| 239 |
+
|
| 240 |
+
# 使用动态文件夹结构替代静态结构
|
| 241 |
+
dynamic_folders = get_dynamic_knowledge_folders(uploaded_files)
|
| 242 |
+
|
| 243 |
+
# 如果没有上传文件或动态文件夹为空,则使用预设的静态结构作为示例
|
| 244 |
+
if not dynamic_folders:
|
| 245 |
+
return render_template('index.html',
|
| 246 |
+
knowledge_folders=knowledge_folders,
|
| 247 |
+
use_static_folders=True,
|
| 248 |
+
uploaded_files=[])
|
| 249 |
+
|
| 250 |
+
# 使用动态生成的文件夹结构
|
| 251 |
+
return render_template('index.html',
|
| 252 |
+
knowledge_folders=dynamic_folders,
|
| 253 |
+
use_static_folders=False,
|
| 254 |
+
uploaded_files=uploaded_files)
|
| 255 |
+
|
| 256 |
+
@app.route('/chat', methods=['POST'])
|
| 257 |
+
def chat():
|
| 258 |
+
"""Handle chat requests"""
|
| 259 |
+
if not API_KEY:
|
| 260 |
+
return jsonify({"error": "Please configure Google API key in settings page first"}), 400
|
| 261 |
+
|
| 262 |
+
# Get user input
|
| 263 |
+
user_message = request.json.get('message', '')
|
| 264 |
+
if not user_message:
|
| 265 |
+
return jsonify({"error": "Message cannot be empty"}), 400
|
| 266 |
+
|
| 267 |
+
# Check session for uploaded file context
|
| 268 |
+
uploaded_files_context = session.get('uploaded_files_context', [])
|
| 269 |
+
context_text = ""
|
| 270 |
+
if uploaded_files_context:
|
| 271 |
+
# Use context from the most recently uploaded file for simplicity
|
| 272 |
+
# A more complex app might allow selecting which file context to use
|
| 273 |
+
last_file_context = uploaded_files_context[-1]
|
| 274 |
+
context_text = f"Use the following document context to answer the question:\n--- DOCUMENT START ---\n{last_file_context['summary']}\n--- DOCUMENT END ---\n\n"
|
| 275 |
+
logging.info(f"Using context from file: {last_file_context['filename']}")
|
| 276 |
+
|
| 277 |
+
# If the user message is just a test connection message, return a simple response
|
| 278 |
+
if user_message.lower() == 'test connection':
|
| 279 |
+
return jsonify({"reply": "Connection successful. API is working properly."}), 200
|
| 280 |
+
|
| 281 |
+
# Combine context and user message
|
| 282 |
+
prompt_to_gemini = f"{context_text}User Question: {user_message}"
|
| 283 |
+
|
| 284 |
+
# Call Google Gemini API
|
| 285 |
+
try:
|
| 286 |
+
response = requests.post(
|
| 287 |
+
f"{API_URL}?key={API_KEY}",
|
| 288 |
+
json={
|
| 289 |
+
"contents": [{"parts": [{"text": prompt_to_gemini}]}]
|
| 290 |
+
},
|
| 291 |
+
timeout=30 # Adjusted timeout
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# Check response
|
| 295 |
+
response.raise_for_status()
|
| 296 |
+
data = response.json()
|
| 297 |
+
|
| 298 |
+
# Handle API response
|
| 299 |
+
if 'candidates' in data and len(data['candidates']) > 0:
|
| 300 |
+
candidate = data['candidates'][0]
|
| 301 |
+
if 'content' in candidate and 'parts' in candidate['content']:
|
| 302 |
+
parts = candidate['content']['parts']
|
| 303 |
+
ai_message = "".join([part.get('text', '') for part in parts])
|
| 304 |
+
logging.info(f"Gemini chat response generated.")
|
| 305 |
+
# Clear context after use? Or keep for follow-up?
|
| 306 |
+
# session.pop('uploaded_files_context', None) # Option: Clear context after one use
|
| 307 |
+
return jsonify({"reply": ai_message})
|
| 308 |
+
|
| 309 |
+
logging.warning("Could not parse Gemini chat response structure.")
|
| 310 |
+
return jsonify({"error": "Unable to parse API response"}), 500
|
| 311 |
+
|
| 312 |
+
except requests.exceptions.RequestException as e:
|
| 313 |
+
logging.error(f"Gemini API request failed for chat: {e}")
|
| 314 |
+
return jsonify({"error": f"API request failed: {str(e)}"}), 500
|
| 315 |
+
except Exception as e:
|
| 316 |
+
logging.error(f"Error during chat processing: {e}")
|
| 317 |
+
return jsonify({"error": f"Error during chat processing: {str(e)}"}), 500
|
| 318 |
+
|
| 319 |
+
@app.route('/upload', methods=['POST'])
|
| 320 |
+
def upload_file():
|
| 321 |
+
"""Handle file uploads"""
|
| 322 |
+
try:
|
| 323 |
+
logging.debug(f"Upload request received. Files: {request.files}")
|
| 324 |
+
logging.debug(f"Request form data: {request.form}")
|
| 325 |
+
|
| 326 |
+
if 'file' not in request.files:
|
| 327 |
+
logging.warning("No file part in the request")
|
| 328 |
+
return jsonify({"error": "No file part found", "success": False}), 400
|
| 329 |
+
|
| 330 |
+
file = request.files['file']
|
| 331 |
+
logging.debug(f"Received file: {file.filename}, mimetype: {file.mimetype}")
|
| 332 |
+
|
| 333 |
+
if file.filename == '':
|
| 334 |
+
logging.warning("Empty filename submitted")
|
| 335 |
+
return jsonify({"error": "No file selected", "success": False}), 400
|
| 336 |
+
|
| 337 |
+
if not allowed_file(file.filename):
|
| 338 |
+
logging.warning(f"File type not allowed: {file.filename}")
|
| 339 |
+
return jsonify({"error": f"Unsupported file type. Only supporting: {', '.join(app.config['ALLOWED_EXTENSIONS'])}", "success": False}), 400
|
| 340 |
+
|
| 341 |
+
# Properly handle filename and extension
|
| 342 |
+
original_filename = file.filename
|
| 343 |
+
file_ext = os.path.splitext(original_filename)[1].lower() # Get original file extension with dot(.)
|
| 344 |
+
|
| 345 |
+
# Use secure_filename for base name, then manually add extension
|
| 346 |
+
safe_filename = secure_filename(os.path.splitext(original_filename)[0]) + file_ext
|
| 347 |
+
unique_id = str(uuid.uuid4())
|
| 348 |
+
unique_filename = f"{unique_id}_{safe_filename}"
|
| 349 |
+
|
| 350 |
+
file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
|
| 351 |
+
|
| 352 |
+
# Ensure upload directory exists
|
| 353 |
+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
| 354 |
+
|
| 355 |
+
logging.debug(f"Saving file to: {file_path}")
|
| 356 |
+
file.save(file_path)
|
| 357 |
+
logging.info(f"File saved: {file_path}")
|
| 358 |
+
|
| 359 |
+
# Extract text
|
| 360 |
+
text_content = None
|
| 361 |
+
if file_ext.lower() == '.pdf':
|
| 362 |
+
logging.debug(f"Attempting to extract text from PDF: {file_path}")
|
| 363 |
+
text_content = extract_text_from_pdf(file_path)
|
| 364 |
+
elif file_ext.lower() == '.txt':
|
| 365 |
+
logging.debug(f"Attempting to extract text from TXT: {file_path}")
|
| 366 |
+
text_content = extract_text_from_txt(file_path)
|
| 367 |
+
|
| 368 |
+
if not text_content:
|
| 369 |
+
logging.warning(f"Could not extract text from {safe_filename}")
|
| 370 |
+
return jsonify({
|
| 371 |
+
"error": "Could not extract text from file. Please ensure the file is valid and not encrypted",
|
| 372 |
+
"success": False
|
| 373 |
+
}), 400
|
| 374 |
+
|
| 375 |
+
logging.info(f"Extracted {len(text_content)} characters from {safe_filename}")
|
| 376 |
+
|
| 377 |
+
# Get summary and tags
|
| 378 |
+
try:
|
| 379 |
+
logging.debug("Calling Gemini API for summary and tags")
|
| 380 |
+
summary, tags = get_summary_and_tags_from_gemini(text_content)
|
| 381 |
+
logging.debug(f"Received summary: {summary[:100]}... and tags: {tags}")
|
| 382 |
+
except Exception as summary_error:
|
| 383 |
+
logging.error(f"Error getting summary: {summary_error}")
|
| 384 |
+
logging.error(traceback.format_exc())
|
| 385 |
+
summary = "Could not generate summary, but you can still ask questions about the document."
|
| 386 |
+
tags = []
|
| 387 |
+
|
| 388 |
+
# Store session data - uploaded file context
|
| 389 |
+
if 'uploaded_files_context' not in session:
|
| 390 |
+
session['uploaded_files_context'] = []
|
| 391 |
+
|
| 392 |
+
# Add new file context
|
| 393 |
+
file_context = {
|
| 394 |
+
'filename': safe_filename,
|
| 395 |
+
'unique_filename': unique_filename,
|
| 396 |
+
'summary': summary,
|
| 397 |
+
'tags': tags # Add tags to context
|
| 398 |
+
}
|
| 399 |
+
session['uploaded_files_context'].append(file_context)
|
| 400 |
+
|
| 401 |
+
# Initialize uploaded_files (for knowledge map)
|
| 402 |
+
if 'uploaded_files' not in session:
|
| 403 |
+
session['uploaded_files'] = []
|
| 404 |
+
|
| 405 |
+
# Add file info to uploaded files list
|
| 406 |
+
session['uploaded_files'].append({
|
| 407 |
+
'filename': safe_filename,
|
| 408 |
+
'summary': summary,
|
| 409 |
+
'tags': tags
|
| 410 |
+
})
|
| 411 |
+
|
| 412 |
+
# Ensure session changes are saved
|
| 413 |
+
session.modified = True
|
| 414 |
+
logging.info(f"Stored context for {safe_filename} in session with {len(tags)} tags")
|
| 415 |
+
|
| 416 |
+
return jsonify({
|
| 417 |
+
"success": True,
|
| 418 |
+
"filename": safe_filename,
|
| 419 |
+
"summary": summary,
|
| 420 |
+
"tags": tags
|
| 421 |
+
})
|
| 422 |
+
|
| 423 |
+
except Exception as e:
|
| 424 |
+
# Catch and log all exceptions
|
| 425 |
+
logging.error(f"Unexpected error during file upload: {str(e)}")
|
| 426 |
+
logging.error(traceback.format_exc())
|
| 427 |
+
return jsonify({
|
| 428 |
+
"error": f"Error during file upload: {str(e)}",
|
| 429 |
+
"success": False
|
| 430 |
+
}), 500
|
| 431 |
+
|
| 432 |
+
@app.route('/settings', methods=['GET', 'POST'])
|
| 433 |
+
def settings():
|
| 434 |
+
"""API密钥设置页面"""
|
| 435 |
+
if request.method == 'POST':
|
| 436 |
+
# In a real app, store this securely, not globally or just in session.
|
| 437 |
+
session['api_key'] = request.form.get('api_key', '')
|
| 438 |
+
# Update the global variable too for immediate effect if needed by current requests
|
| 439 |
+
global API_KEY
|
| 440 |
+
API_KEY = session['api_key']
|
| 441 |
+
return redirect(url_for('home'))
|
| 442 |
+
|
| 443 |
+
# Get key from session if available, else from global/env
|
| 444 |
+
current_api_key = session.get('api_key', API_KEY)
|
| 445 |
+
return render_template('settings.html', api_key=current_api_key)
|
| 446 |
+
|
| 447 |
+
@app.route('/uploads/<filename>')
|
| 448 |
+
def uploaded_file(filename):
|
| 449 |
+
"""Serve uploaded files."""
|
| 450 |
+
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
|
| 451 |
+
|
| 452 |
+
@app.route('/knowledge-map')
|
| 453 |
+
def knowledge_map():
|
| 454 |
+
"""Render the knowledge map page."""
|
| 455 |
+
return render_template('knowledge_map.html')
|
| 456 |
+
|
| 457 |
+
@app.route('/api/knowledge-map')
|
| 458 |
+
def get_knowledge_map():
|
| 459 |
+
"""API endpoint to provide knowledge map data."""
|
| 460 |
+
# 优先使用会话中的uploaded_files数据
|
| 461 |
+
uploaded_files = session.get('uploaded_files', [])
|
| 462 |
+
|
| 463 |
+
logging.debug(f"Knowledge map API - Session data keys: {session.keys()}")
|
| 464 |
+
logging.debug(f"Knowledge map API - Files found: {len(uploaded_files)}")
|
| 465 |
+
|
| 466 |
+
# 如果没有上传文件,检查context数据作为备选
|
| 467 |
+
if not uploaded_files and 'uploaded_files_context' in session:
|
| 468 |
+
# 从context数据中提取文件信息
|
| 469 |
+
context_files = session.get('uploaded_files_context', [])
|
| 470 |
+
for file_info in context_files:
|
| 471 |
+
uploaded_files.append({
|
| 472 |
+
'filename': file_info.get('filename', ''),
|
| 473 |
+
'summary': file_info.get('summary', ''),
|
| 474 |
+
'tags': file_info.get('tags', [])
|
| 475 |
+
})
|
| 476 |
+
logging.debug(f"Used context data as fallback, found {len(uploaded_files)} files")
|
| 477 |
+
|
| 478 |
+
# 如果还是没有上传文件,返回空数据
|
| 479 |
+
if not uploaded_files:
|
| 480 |
+
logging.warning("No uploaded files found in session for knowledge map")
|
| 481 |
+
return jsonify({
|
| 482 |
+
"centralTopic": "Knowledge Center",
|
| 483 |
+
"documents": []
|
| 484 |
+
})
|
| 485 |
+
|
| 486 |
+
# 构建文档数据列表
|
| 487 |
+
documents = []
|
| 488 |
+
|
| 489 |
+
for file_info in uploaded_files:
|
| 490 |
+
try:
|
| 491 |
+
filename = file_info.get('filename', '')
|
| 492 |
+
summary = file_info.get('summary', '')
|
| 493 |
+
tags = file_info.get('tags', [])
|
| 494 |
+
|
| 495 |
+
# 确保每个文档至少有一个标签用于分类,如果没有则添加一个默认标签
|
| 496 |
+
if not tags:
|
| 497 |
+
tags = ["Uncategorized"]
|
| 498 |
+
|
| 499 |
+
# 添加文档查看链接
|
| 500 |
+
document_url = url_for('view_document', filename=filename)
|
| 501 |
+
|
| 502 |
+
documents.append({
|
| 503 |
+
"filename": filename,
|
| 504 |
+
"summary": summary,
|
| 505 |
+
"tags": tags,
|
| 506 |
+
"url": document_url
|
| 507 |
+
})
|
| 508 |
+
|
| 509 |
+
logging.debug(f"Added document to knowledge map: {filename} with tags: {tags}")
|
| 510 |
+
except Exception as e:
|
| 511 |
+
logging.error(f"Error processing file for knowledge map: {e}")
|
| 512 |
+
logging.error(traceback.format_exc())
|
| 513 |
+
|
| 514 |
+
# 创建与知识文件夹相同的分类结构
|
| 515 |
+
dynamic_folders = get_dynamic_knowledge_folders(uploaded_files)
|
| 516 |
+
|
| 517 |
+
# 构建知识图谱数据结构
|
| 518 |
+
knowledge_map_data = {
|
| 519 |
+
"centralTopic": "Knowledge Center",
|
| 520 |
+
"documents": documents,
|
| 521 |
+
"folders": dynamic_folders
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
return jsonify(knowledge_map_data)
|
| 525 |
+
|
| 526 |
+
@app.route('/view-document')
|
| 527 |
+
def view_document():
|
| 528 |
+
"""查看文档内容"""
|
| 529 |
+
filename = request.args.get('filename')
|
| 530 |
+
|
| 531 |
+
if not filename:
|
| 532 |
+
return jsonify({"error": "未提供文件名"}), 400
|
| 533 |
+
|
| 534 |
+
# 检查文件是否存在于上传文件夹中
|
| 535 |
+
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 536 |
+
|
| 537 |
+
# 同时检查原始文件名和通过UUID生成的唯一文件名
|
| 538 |
+
if not os.path.exists(file_path):
|
| 539 |
+
# 如果按原始文件名找不到,尝试在会话中查找匹配的唯一文件名
|
| 540 |
+
uploaded_files_context = session.get('uploaded_files_context', [])
|
| 541 |
+
unique_filename = None
|
| 542 |
+
|
| 543 |
+
for file_info in uploaded_files_context:
|
| 544 |
+
if file_info.get('filename') == filename:
|
| 545 |
+
unique_filename = file_info.get('unique_filename')
|
| 546 |
+
break
|
| 547 |
+
|
| 548 |
+
if unique_filename:
|
| 549 |
+
file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
|
| 550 |
+
if not os.path.exists(file_path):
|
| 551 |
+
return jsonify({"error": f"找不到文件: {filename}"}), 404
|
| 552 |
+
else:
|
| 553 |
+
return jsonify({"error": f"找不到文件: {filename}"}), 404
|
| 554 |
+
|
| 555 |
+
# 根据文件类型返回适当的内容
|
| 556 |
+
if filename.lower().endswith('.pdf'):
|
| 557 |
+
# 对于PDF文件,重定向到文件查看器
|
| 558 |
+
return redirect(url_for('uploaded_file', filename=filename))
|
| 559 |
+
elif filename.lower().endswith('.txt'):
|
| 560 |
+
# 对于文本文件,读取内容并显示
|
| 561 |
+
try:
|
| 562 |
+
with open(file_path, 'r', encoding='utf-8') as file:
|
| 563 |
+
content = file.read()
|
| 564 |
+
return render_template('view_document.html', filename=filename, content=content)
|
| 565 |
+
except UnicodeDecodeError:
|
| 566 |
+
# 如果UTF-8解码失败,尝试其他编码
|
| 567 |
+
try:
|
| 568 |
+
with open(file_path, 'r', encoding='gbk') as file:
|
| 569 |
+
content = file.read()
|
| 570 |
+
return render_template('view_document.html', filename=filename, content=content)
|
| 571 |
+
except Exception as e:
|
| 572 |
+
logging.error(f"读取文件时出错: {e}")
|
| 573 |
+
return jsonify({"error": f"读取文件时出错: {str(e)}"}), 500
|
| 574 |
+
except Exception as e:
|
| 575 |
+
logging.error(f"读取文件时出错: {e}")
|
| 576 |
+
return jsonify({"error": f"读取文件时出错: {str(e)}"}), 500
|
| 577 |
+
else:
|
| 578 |
+
return jsonify({"error": "不支持的文件类型"}), 400
|
| 579 |
+
|
| 580 |
+
# 添加停用词处理功能
|
| 581 |
+
def load_stopwords():
|
| 582 |
+
"""加载停用词列表"""
|
| 583 |
+
stopwords = set()
|
| 584 |
+
stopwords_path = os.path.join(os.path.dirname(__file__), 'stopwords.txt')
|
| 585 |
+
|
| 586 |
+
# 如果停用词文件不存在,创建一个基本的停用词列表
|
| 587 |
+
if not os.path.exists(stopwords_path):
|
| 588 |
+
basic_stopwords = [
|
| 589 |
+
'the', 'a', 'an', 'of', 'in', 'on', 'at', 'for', 'to', 'with',
|
| 590 |
+
'by', 'about', 'as', 'and', 'or', 'but', 'if', 'because', 'as',
|
| 591 |
+
'until', 'while', 'that', 'which', 'when', 'where', 'how', 'why',
|
| 592 |
+
'what', 'who', 'whom'
|
| 593 |
+
]
|
| 594 |
+
with open(stopwords_path, 'w', encoding='utf-8') as f:
|
| 595 |
+
f.write('\n'.join(basic_stopwords))
|
| 596 |
+
stopwords = set(basic_stopwords)
|
| 597 |
+
else:
|
| 598 |
+
with open(stopwords_path, 'r', encoding='utf-8') as f:
|
| 599 |
+
for line in f:
|
| 600 |
+
stopwords.add(line.strip())
|
| 601 |
+
|
| 602 |
+
return stopwords
|
| 603 |
+
|
| 604 |
+
# 词频统计功能
|
| 605 |
+
def get_word_frequency(text, top_n=100, additional_stopwords=None):
|
| 606 |
+
"""对文本进行分词并统计词频,返回前N个高频词"""
|
| 607 |
+
stopwords = load_stopwords()
|
| 608 |
+
|
| 609 |
+
# 添加额外的停用词
|
| 610 |
+
if additional_stopwords:
|
| 611 |
+
for word in additional_stopwords:
|
| 612 |
+
stopwords.add(word)
|
| 613 |
+
|
| 614 |
+
# 使用正则表达式清理文本,只保留中英文和数字
|
| 615 |
+
text = re.sub(r'[^\w\s\u4e00-\u9fff]', ' ', text)
|
| 616 |
+
|
| 617 |
+
# 分词,针对中文使用jieba分词
|
| 618 |
+
words = []
|
| 619 |
+
for word in jieba.cut(text):
|
| 620 |
+
word = word.strip()
|
| 621 |
+
# 过滤停用词、空格和长度为1的词
|
| 622 |
+
if word and word not in stopwords and len(word) > 1:
|
| 623 |
+
words.append(word)
|
| 624 |
+
|
| 625 |
+
# 统计词频
|
| 626 |
+
word_counts = Counter(words)
|
| 627 |
+
|
| 628 |
+
# 返回前N个高频词及其频率
|
| 629 |
+
return word_counts.most_common(top_n)
|
| 630 |
+
|
| 631 |
+
# 生成词云图
|
| 632 |
+
def generate_wordcloud(word_freq, width=800, height=400):
|
| 633 |
+
"""根据词频生成词云图,返回base64编码的图像数据"""
|
| 634 |
+
# 将词频转换为WordCloud需要的格式
|
| 635 |
+
word_freq_dict = dict(word_freq)
|
| 636 |
+
|
| 637 |
+
# 设置词云图参数
|
| 638 |
+
wordcloud = WordCloud(
|
| 639 |
+
width=width,
|
| 640 |
+
height=height,
|
| 641 |
+
background_color='white',
|
| 642 |
+
font_path='/System/Library/Fonts/PingFang.ttc', # 使用系统字体支持中文
|
| 643 |
+
max_words=100,
|
| 644 |
+
prefer_horizontal=0.9,
|
| 645 |
+
contour_width=3,
|
| 646 |
+
contour_color='steelblue'
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
# 生成词云
|
| 650 |
+
wordcloud.generate_from_frequencies(word_freq_dict)
|
| 651 |
+
|
| 652 |
+
# 将词云图保存到内存
|
| 653 |
+
img_buffer = BytesIO()
|
| 654 |
+
plt.figure(figsize=(10, 5))
|
| 655 |
+
plt.imshow(wordcloud, interpolation='bilinear')
|
| 656 |
+
plt.axis('off')
|
| 657 |
+
plt.tight_layout(pad=0)
|
| 658 |
+
plt.savefig(img_buffer, format='png')
|
| 659 |
+
plt.close()
|
| 660 |
+
|
| 661 |
+
# 将图像数据转换为base64编码
|
| 662 |
+
img_buffer.seek(0)
|
| 663 |
+
img_data = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
|
| 664 |
+
|
| 665 |
+
return img_data
|
| 666 |
+
|
| 667 |
+
@app.route('/api/wordcloud/<filename>')
|
| 668 |
+
def get_wordcloud(filename):
|
| 669 |
+
"""Generate wordcloud for document and return word frequency and image data"""
|
| 670 |
+
try:
|
| 671 |
+
# Set adjustable word frequency count parameter
|
| 672 |
+
top_n = request.args.get('top_n', default=100, type=int)
|
| 673 |
+
|
| 674 |
+
# Check if file exists
|
| 675 |
+
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 676 |
+
if not os.path.exists(file_path):
|
| 677 |
+
return jsonify({"error": "File not found"}), 404
|
| 678 |
+
|
| 679 |
+
# Extract text content
|
| 680 |
+
text_content = None
|
| 681 |
+
if filename.lower().endswith('.pdf'):
|
| 682 |
+
text_content = extract_text_from_pdf(file_path)
|
| 683 |
+
elif filename.lower().endswith('.txt'):
|
| 684 |
+
text_content = extract_text_from_txt(file_path)
|
| 685 |
+
else:
|
| 686 |
+
return jsonify({"error": "Unsupported file type"}), 400
|
| 687 |
+
|
| 688 |
+
if not text_content:
|
| 689 |
+
return jsonify({"error": "Could not extract text from file"}), 400
|
| 690 |
+
|
| 691 |
+
# Extract keywords to exclude from filename
|
| 692 |
+
# 1. Remove extension
|
| 693 |
+
basename = os.path.splitext(filename)[0]
|
| 694 |
+
# 2. Process filename, remove uuid and other special characters
|
| 695 |
+
clean_name = re.sub(r'^[a-f0-9\-_]+_', '', basename)
|
| 696 |
+
|
| 697 |
+
# 3. Prepare words to exclude
|
| 698 |
+
filename_words = set()
|
| 699 |
+
|
| 700 |
+
# Add original and cleaned filename
|
| 701 |
+
filename_words.add(basename)
|
| 702 |
+
filename_words.add(clean_name)
|
| 703 |
+
|
| 704 |
+
# If filename contains underscores, hyphens or spaces, split into words
|
| 705 |
+
for word in re.split(r'[_\-\s]+', clean_name):
|
| 706 |
+
if word and len(word) > 1:
|
| 707 |
+
filename_words.add(word)
|
| 708 |
+
|
| 709 |
+
# For Chinese filenames, do jieba segmentation
|
| 710 |
+
if re.search(r'[\u4e00-\u9fff]', word):
|
| 711 |
+
for chunk in jieba.cut(word):
|
| 712 |
+
if chunk and len(chunk) > 1:
|
| 713 |
+
filename_words.add(chunk)
|
| 714 |
+
|
| 715 |
+
# Add uppercase/lowercase/capitalized versions of English words
|
| 716 |
+
english_words = set()
|
| 717 |
+
for word in filename_words:
|
| 718 |
+
if not re.search(r'[\u4e00-\u9fff]', word): # Non-Chinese words
|
| 719 |
+
english_words.add(word.lower())
|
| 720 |
+
english_words.add(word.upper())
|
| 721 |
+
english_words.add(word.capitalize())
|
| 722 |
+
filename_words.update(english_words)
|
| 723 |
+
|
| 724 |
+
# Log excluded words
|
| 725 |
+
logging.info(f"Excluding filename-related words: {filename_words}")
|
| 726 |
+
|
| 727 |
+
# Get word frequency statistics, passing filename-related words as additional stopwords
|
| 728 |
+
word_freq = get_word_frequency(text_content, top_n=top_n, additional_stopwords=filename_words)
|
| 729 |
+
|
| 730 |
+
# Generate wordcloud
|
| 731 |
+
wordcloud_img = generate_wordcloud(word_freq)
|
| 732 |
+
|
| 733 |
+
# Return word frequency and wordcloud image data
|
| 734 |
+
return jsonify({
|
| 735 |
+
"filename": filename,
|
| 736 |
+
"word_frequency": word_freq,
|
| 737 |
+
"wordcloud_image": wordcloud_img
|
| 738 |
+
})
|
| 739 |
+
|
| 740 |
+
except Exception as e:
|
| 741 |
+
logging.error(f"Error generating wordcloud: {str(e)}")
|
| 742 |
+
logging.error(traceback.format_exc())
|
| 743 |
+
return jsonify({"error": f"Error generating wordcloud: {str(e)}"}), 500
|
| 744 |
+
|
| 745 |
+
# 确保上传文件夹存在且有正确权限,在创建app后
|
| 746 |
+
uploads_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), app.config['UPLOAD_FOLDER'])
|
| 747 |
+
if not os.path.exists(uploads_dir):
|
| 748 |
+
try:
|
| 749 |
+
os.makedirs(uploads_dir, exist_ok=True)
|
| 750 |
+
logging.info(f"Created upload directory: {uploads_dir}")
|
| 751 |
+
except Exception as e:
|
| 752 |
+
logging.error(f"Failed to create upload directory: {str(e)}")
|
| 753 |
+
|
| 754 |
+
if __name__ == "__main__":
|
| 755 |
+
# 获取环境变量中的端口,如果没有则使用默认值5000
|
| 756 |
+
port = int(os.environ.get("PORT", 7860))
|
| 757 |
+
# 启动应用,允许来自任何IP的访问,指定端口,并关闭调试模式
|
| 758 |
+
app.run(host='0.0.0.0', port=port)
|
interface
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Card, CardContent } from "@/components/ui/card";
|
| 2 |
+
import { Button } from "@/components/ui/button";
|
| 3 |
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
| 4 |
+
import { Input } from "@/components/ui/input";
|
| 5 |
+
import { Textarea } from "@/components/ui/textarea";
|
| 6 |
+
import { Badge } from "@/components/ui/badge";
|
| 7 |
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
| 8 |
+
import { MessageSquareIcon, UploadIcon, BrainIcon, FolderIcon, BookOpenIcon, ClockIcon, SendIcon } from "lucide-react";
|
| 9 |
+
import { useState } from "react";
|
| 10 |
+
|
| 11 |
+
const GoogleAPI = 'AIzaSyAMqFn0dNAoS75Zo4GUdcD99reEIM2IvzU'
|
| 12 |
+
export default function TeachingAssistantDashboard() {
|
| 13 |
+
const [message, setMessage] = useState("");
|
| 14 |
+
const [chatHistory, setChatHistory] = useState([]);
|
| 15 |
+
|
| 16 |
+
const handleSend = async () => {
|
| 17 |
+
if (message.trim() === "") return;
|
| 18 |
+
const newEntry = { sender: "user", content: message };
|
| 19 |
+
setChatHistory([...chatHistory, newEntry]);
|
| 20 |
+
setMessage("");
|
| 21 |
+
|
| 22 |
+
try {
|
| 23 |
+
const response = await fetch(
|
| 24 |
+
`https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${GoogleAPI}`,
|
| 25 |
+
{
|
| 26 |
+
method: "POST",
|
| 27 |
+
headers: {
|
| 28 |
+
"Content-Type": "application/json"
|
| 29 |
+
},
|
| 30 |
+
body: JSON.stringify({
|
| 31 |
+
contents: [{ parts: [{ text: message }] }]
|
| 32 |
+
})
|
| 33 |
+
}
|
| 34 |
+
);
|
| 35 |
+
const data = await response.json();
|
| 36 |
+
console.log("Gemini raw response:", data);
|
| 37 |
+
|
| 38 |
+
// More robust extraction of AI message
|
| 39 |
+
let aiMessage = "";
|
| 40 |
+
if (data.candidates && data.candidates.length > 0) {
|
| 41 |
+
const candidate = data.candidates[0];
|
| 42 |
+
// Try nested parts
|
| 43 |
+
if (candidate.content?.parts) {
|
| 44 |
+
aiMessage = candidate.content.parts.map(p => p.text).join("\n");
|
| 45 |
+
}
|
| 46 |
+
// Fallback to simple text
|
| 47 |
+
else if (candidate.content?.text) {
|
| 48 |
+
aiMessage = candidate.content.text;
|
| 49 |
+
}
|
| 50 |
+
// Fallback to candidate text
|
| 51 |
+
else if (candidate.text) {
|
| 52 |
+
aiMessage = candidate.text;
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
// Last resort: stringify whole response
|
| 56 |
+
if (!aiMessage) {
|
| 57 |
+
aiMessage = JSON.stringify(data);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
setChatHistory(prev => [...prev, { sender: "ai", content: aiMessage }] );
|
| 61 |
+
} catch (error) {
|
| 62 |
+
console.error("Error connecting to Gemini API:", error);
|
| 63 |
+
setChatHistory(prev => [...prev, { sender: "ai", content: "[Error connecting to Gemini API]" }]);
|
| 64 |
+
}
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
return (
|
| 68 |
+
<div className="grid grid-cols-6 gap-4 p-4 bg-background text-foreground">
|
| 69 |
+
{/* Sidebar */}
|
| 70 |
+
<div className="col-span-1 bg-muted rounded-2xl p-4 space-y-4 shadow">
|
| 71 |
+
<h2 className="text-xl font-semibold">AI Teaching Assistant</h2>
|
| 72 |
+
<Button variant="ghost" className="w-full justify-start"><UploadIcon className="mr-2" />Upload</Button>
|
| 73 |
+
<Button variant="ghost" className="w-full justify-start"><FolderIcon className="mr-2" />Folders</Button>
|
| 74 |
+
<Button variant="ghost" className="w-full justify-start"><BookOpenIcon className="mr-2" />Knowledge Map</Button>
|
| 75 |
+
<Button variant="ghost" className="w-full justify-start"><MessageSquareIcon className="mr-2" />Chat Assistant</Button>
|
| 76 |
+
<Button variant="ghost" className="w-full justify-start"><ClockIcon className="mr-2" />Review</Button>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
{/* Main Content */}
|
| 80 |
+
<div className="col-span-5 space-y-4">
|
| 81 |
+
{/* Upload Section */}
|
| 82 |
+
<Card>
|
| 83 |
+
<CardContent className="p-4">
|
| 84 |
+
<h3 className="text-lg font-semibold mb-2">📂 Upload Documents</h3>
|
| 85 |
+
<Input type="file" multiple />
|
| 86 |
+
<p className="text-sm text-muted-foreground mt-1">Supported: PDF, PPT, TXT</p>
|
| 87 |
+
</CardContent>
|
| 88 |
+
</Card>
|
| 89 |
+
|
| 90 |
+
{/* Auto-Summarization */}
|
| 91 |
+
<Card>
|
| 92 |
+
<CardContent className="p-4">
|
| 93 |
+
<h3 className="text-lg font-semibold mb-2">🧠 Auto-Summarization & Tags</h3>
|
| 94 |
+
<div className="flex gap-2 flex-wrap">
|
| 95 |
+
<Badge variant="secondary">Constructivism</Badge>
|
| 96 |
+
<Badge variant="secondary">Cognitive Load</Badge>
|
| 97 |
+
<Badge variant="secondary">Assessment Design</Badge>
|
| 98 |
+
<Badge variant="secondary">Multimedia Principles</Badge>
|
| 99 |
+
</div>
|
| 100 |
+
<Textarea className="mt-2" placeholder="Generated summary will appear here..." rows={3} />
|
| 101 |
+
</CardContent>
|
| 102 |
+
</Card>
|
| 103 |
+
|
| 104 |
+
{/* Folder View */}
|
| 105 |
+
<Card>
|
| 106 |
+
<CardContent className="p-4">
|
| 107 |
+
<h3 className="text-lg font-semibold mb-2">📁 Knowledge Folders</h3>
|
| 108 |
+
<ul className="list-disc list-inside space-y-1">
|
| 109 |
+
<li>Instructional Theories (4 docs)</li>
|
| 110 |
+
<li>Learner Characteristics (3 docs)</li>
|
| 111 |
+
<li>Design Models (5 docs)</li>
|
| 112 |
+
<li>Assessment Methods (2 docs)</li>
|
| 113 |
+
</ul>
|
| 114 |
+
</CardContent>
|
| 115 |
+
</Card>
|
| 116 |
+
|
| 117 |
+
{/* Chat Assistant */}
|
| 118 |
+
<Card>
|
| 119 |
+
<CardContent className="p-4 space-y-3">
|
| 120 |
+
<h3 className="text-lg font-semibold">🤖 Chat with AI Assistant</h3>
|
| 121 |
+
<div className="space-y-2 max-h-40 overflow-y-auto bg-muted p-2 rounded">
|
| 122 |
+
{chatHistory.map((entry, index) => (
|
| 123 |
+
<div key={index} className={`text-sm ${entry.sender === "user" ? "text-right" : "text-left"}`}>
|
| 124 |
+
<span className="inline-block px-3 py-1 bg-background rounded shadow">{entry.content}</span>
|
| 125 |
+
</div>
|
| 126 |
+
))}
|
| 127 |
+
</div>
|
| 128 |
+
<div className="flex items-center gap-2">
|
| 129 |
+
<Input
|
| 130 |
+
className="flex-grow"
|
| 131 |
+
placeholder="Ask about any concept..."
|
| 132 |
+
value={message}
|
| 133 |
+
onChange={(e) => setMessage(e.target.value)}
|
| 134 |
+
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
| 135 |
+
/>
|
| 136 |
+
<Button onClick={handleSend} variant="default"><SendIcon size={16} /></Button>
|
| 137 |
+
</div>
|
| 138 |
+
</CardContent>
|
| 139 |
+
</Card>
|
| 140 |
+
|
| 141 |
+
{/* Spaced Repetition */}
|
| 142 |
+
<Card>
|
| 143 |
+
<CardContent className="p-4">
|
| 144 |
+
<h3 className="text-lg font-semibold mb-2">⏳ Review Reminders</h3>
|
| 145 |
+
<p className="text-sm">Next review cycle: <strong>Tomorrow (2 docs)</strong> based on Ebbinghaus curve</p>
|
| 146 |
+
</CardContent>
|
| 147 |
+
</Card>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
);
|
| 151 |
+
}
|
static/css/style.css
ADDED
|
@@ -0,0 +1,1436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Global Reset */
|
| 2 |
+
* {
|
| 3 |
+
margin: 0;
|
| 4 |
+
padding: 0;
|
| 5 |
+
box-sizing: border-box;
|
| 6 |
+
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; /* Updated Font */
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
body {
|
| 10 |
+
background-color: #f0f2f5; /* Lighter grey background */
|
| 11 |
+
color: #1c1e21; /* Darker text color */
|
| 12 |
+
line-height: 1.5;
|
| 13 |
+
display: flex;
|
| 14 |
+
height: 100vh;
|
| 15 |
+
overflow: hidden;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
/* Container */
|
| 19 |
+
.container {
|
| 20 |
+
display: flex;
|
| 21 |
+
width: 100%;
|
| 22 |
+
height: 100%;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/* Sidebar */
|
| 26 |
+
.sidebar {
|
| 27 |
+
width: 260px; /* Slightly wider */
|
| 28 |
+
background-color: #ffffff; /* White background */
|
| 29 |
+
color: #333; /* Dark text for sidebar */
|
| 30 |
+
display: flex;
|
| 31 |
+
flex-direction: column;
|
| 32 |
+
height: 100%;
|
| 33 |
+
border-right: 1px solid #e0e0e0; /* Subtle border */
|
| 34 |
+
flex-shrink: 0;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.sidebar-header {
|
| 38 |
+
padding: 25px 20px; /* Increased padding */
|
| 39 |
+
text-align: left; /* Align left */
|
| 40 |
+
border-bottom: 1px solid #e0e0e0;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.sidebar-header h1 {
|
| 44 |
+
font-size: 1.6rem; /* Slightly larger */
|
| 45 |
+
font-weight: 600;
|
| 46 |
+
color: #0d6efd; /* Accent color */
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.sidebar-menu {
|
| 50 |
+
flex-grow: 1;
|
| 51 |
+
padding: 15px 0; /* Reduced top/bottom padding */
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.sidebar-menu ul {
|
| 55 |
+
list-style: none;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.sidebar-menu li {
|
| 59 |
+
margin-bottom: 5px; /* Reduced margin */
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.sidebar-menu a {
|
| 63 |
+
display: flex;
|
| 64 |
+
align-items: center;
|
| 65 |
+
padding: 12px 20px; /* Adjusted padding */
|
| 66 |
+
color: #555; /* Grey text color */
|
| 67 |
+
text-decoration: none;
|
| 68 |
+
transition: all 0.2s ease;
|
| 69 |
+
border-radius: 5px; /* Slight rounding */
|
| 70 |
+
margin: 0 10px; /* Horizontal margin */
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.sidebar-menu a i {
|
| 74 |
+
margin-right: 12px; /* Increased icon margin */
|
| 75 |
+
width: 20px;
|
| 76 |
+
text-align: center;
|
| 77 |
+
font-size: 1.1em; /* Slightly larger icons */
|
| 78 |
+
color: #888; /* Lighter icon color */
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.sidebar-menu a:hover,
|
| 82 |
+
.sidebar-menu li.active a {
|
| 83 |
+
background-color: #e7f3ff; /* Light blue background for hover/active */
|
| 84 |
+
color: #0d6efd; /* Blue text for hover/active */
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.sidebar-menu li.active a i,
|
| 88 |
+
.sidebar-menu a:hover i {
|
| 89 |
+
color: #0d6efd; /* Blue icon for hover/active */
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.sidebar-footer {
|
| 93 |
+
padding: 15px;
|
| 94 |
+
text-align: center;
|
| 95 |
+
font-size: 0.8rem;
|
| 96 |
+
color: #aaa; /* Lighter grey */
|
| 97 |
+
border-top: 1px solid #e0e0e0;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* Main Content Area */
|
| 101 |
+
.main-content {
|
| 102 |
+
flex-grow: 1;
|
| 103 |
+
padding: 20px;
|
| 104 |
+
overflow-y: auto; /* Enable vertical scroll */
|
| 105 |
+
background-color: #f0f2f5;
|
| 106 |
+
display: flex;
|
| 107 |
+
flex-direction: column;
|
| 108 |
+
gap: 20px; /* Space between sections */
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/* Card Style */
|
| 112 |
+
.card {
|
| 113 |
+
background-color: #ffffff;
|
| 114 |
+
border-radius: 8px;
|
| 115 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
| 116 |
+
padding: 20px;
|
| 117 |
+
border: 1px solid #e0e0e0; /* Subtle border for cards */
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.card h2 {
|
| 121 |
+
font-size: 1.2rem;
|
| 122 |
+
font-weight: 600;
|
| 123 |
+
margin-bottom: 15px;
|
| 124 |
+
color: #333;
|
| 125 |
+
display: flex;
|
| 126 |
+
align-items: center;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.card h2 i {
|
| 130 |
+
margin-right: 10px;
|
| 131 |
+
color: #0d6efd;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/* Sections Layout */
|
| 135 |
+
.top-section,
|
| 136 |
+
.middle-section {
|
| 137 |
+
display: flex;
|
| 138 |
+
gap: 20px;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.top-section > div,
|
| 142 |
+
.middle-section > div {
|
| 143 |
+
flex: 1;
|
| 144 |
+
min-width: 0; /* Prevent flex overflow */
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/* Upload Panel */
|
| 148 |
+
.upload-panel .file-upload-container {
|
| 149 |
+
display: flex;
|
| 150 |
+
flex-direction: column;
|
| 151 |
+
align-items: flex-start;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.upload-btn {
|
| 155 |
+
background-color: #0d6efd;
|
| 156 |
+
color: white;
|
| 157 |
+
padding: 10px 20px;
|
| 158 |
+
border: none;
|
| 159 |
+
border-radius: 5px;
|
| 160 |
+
cursor: pointer;
|
| 161 |
+
font-size: 1rem;
|
| 162 |
+
transition: background-color 0.2s ease;
|
| 163 |
+
margin-bottom: 10px;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.upload-btn:hover {
|
| 167 |
+
background-color: #0b5ed7;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.supported-files {
|
| 171 |
+
font-size: 0.85rem;
|
| 172 |
+
color: #888;
|
| 173 |
+
margin-bottom: 10px;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/* 文件上传列表容器 */
|
| 177 |
+
.file-list-container {
|
| 178 |
+
margin-top: 15px;
|
| 179 |
+
width: 100%;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/* 文件列表表格样式 */
|
| 183 |
+
.file-list-table {
|
| 184 |
+
list-style: none;
|
| 185 |
+
padding: 0;
|
| 186 |
+
width: 100%;
|
| 187 |
+
border: 1px solid #e0e0e0;
|
| 188 |
+
border-radius: 5px;
|
| 189 |
+
background-color: #f8f9fa;
|
| 190 |
+
overflow: hidden;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
#file-list {
|
| 194 |
+
list-style: none;
|
| 195 |
+
padding: 0;
|
| 196 |
+
margin: 0;
|
| 197 |
+
max-height: 250px;
|
| 198 |
+
overflow-y: auto;
|
| 199 |
+
border: 1px solid #e0e0e0;
|
| 200 |
+
border-radius: 5px;
|
| 201 |
+
background-color: #f8f9fa;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
#file-list:empty {
|
| 205 |
+
border: none;
|
| 206 |
+
padding: 0;
|
| 207 |
+
margin: 0;
|
| 208 |
+
height: 0;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
#file-list li {
|
| 212 |
+
padding: 10px 15px;
|
| 213 |
+
border-bottom: 1px solid #e0e0e0;
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
font-size: 0.9rem;
|
| 217 |
+
position: relative;
|
| 218 |
+
background-color: white;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
#file-list li:nth-child(odd) {
|
| 222 |
+
background-color: #f8f9fa;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
#file-list li:last-child {
|
| 226 |
+
border-bottom: none;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
#file-list li i {
|
| 230 |
+
margin-right: 10px;
|
| 231 |
+
color: #6c757d;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
/* 文件信息部分 */
|
| 235 |
+
.file-name {
|
| 236 |
+
flex-grow: 1;
|
| 237 |
+
font-weight: 500;
|
| 238 |
+
color: #333;
|
| 239 |
+
white-space: nowrap;
|
| 240 |
+
overflow: hidden;
|
| 241 |
+
text-overflow: ellipsis;
|
| 242 |
+
max-width: 70%;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/* 文件状态样式 */
|
| 246 |
+
.file-status {
|
| 247 |
+
padding: 3px 8px;
|
| 248 |
+
border-radius: 12px;
|
| 249 |
+
font-size: 0.8rem;
|
| 250 |
+
font-weight: normal;
|
| 251 |
+
white-space: nowrap;
|
| 252 |
+
margin-left: auto;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
/* 不同状态的样式 */
|
| 256 |
+
.file-status-uploading {
|
| 257 |
+
color: #0d6efd;
|
| 258 |
+
background-color: rgba(13, 110, 253, 0.1);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.file-status-success {
|
| 262 |
+
color: #198754;
|
| 263 |
+
background-color: rgba(25, 135, 84, 0.1);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.file-status-error {
|
| 267 |
+
color: #dc3545;
|
| 268 |
+
background-color: rgba(220, 53, 69, 0.1);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.file-status-retrying {
|
| 272 |
+
color: #fd7e14;
|
| 273 |
+
background-color: rgba(253, 126, 20, 0.1);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
/* Summary & Tags Panel */
|
| 277 |
+
.summary-tags-panel {
|
| 278 |
+
display: flex;
|
| 279 |
+
flex-direction: column;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
/* 文件信息部分样式 */
|
| 283 |
+
.file-info-section {
|
| 284 |
+
margin-bottom: 15px;
|
| 285 |
+
border-bottom: 1px solid #e0e0e0;
|
| 286 |
+
padding-bottom: 10px;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.file-info-section h4 {
|
| 290 |
+
font-size: 1rem;
|
| 291 |
+
font-weight: 600;
|
| 292 |
+
color: #444;
|
| 293 |
+
margin-bottom: 8px;
|
| 294 |
+
padding-left: 5px;
|
| 295 |
+
border-left: 3px solid #0d6efd;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
#uploaded-filename {
|
| 299 |
+
font-size: 1.25rem;
|
| 300 |
+
font-weight: 700;
|
| 301 |
+
color: #0d6efd;
|
| 302 |
+
margin-bottom: 15px;
|
| 303 |
+
padding-left: 10px;
|
| 304 |
+
border-left: 4px solid #0d6efd;
|
| 305 |
+
display: block;
|
| 306 |
+
width: 100%;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.summary-section h4 {
|
| 310 |
+
font-size: 1rem;
|
| 311 |
+
font-weight: 600;
|
| 312 |
+
color: #444;
|
| 313 |
+
margin-bottom: 8px;
|
| 314 |
+
padding-left: 5px;
|
| 315 |
+
border-left: 3px solid #0d6efd;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
/* 文档标签区域 */
|
| 319 |
+
.document-tags-container {
|
| 320 |
+
margin-top: 10px;
|
| 321 |
+
margin-bottom: 15px;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.document-tag-group {
|
| 325 |
+
margin-bottom: 12px;
|
| 326 |
+
border-left: 2px solid #e7f3ff;
|
| 327 |
+
padding-left: 8px;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.document-tag-header {
|
| 331 |
+
display: flex;
|
| 332 |
+
align-items: center;
|
| 333 |
+
cursor: pointer;
|
| 334 |
+
padding: 5px 8px;
|
| 335 |
+
background-color: #f8f9fa;
|
| 336 |
+
border-radius: 4px;
|
| 337 |
+
margin-bottom: 5px;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.document-tag-header:hover {
|
| 341 |
+
background-color: #e7f3ff;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.document-tag-header i {
|
| 345 |
+
margin-right: 8px;
|
| 346 |
+
color: #0d6efd;
|
| 347 |
+
transition: transform 0.2s ease;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.document-tag-header.active i.fa-chevron-right {
|
| 351 |
+
transform: rotate(90deg);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.document-tag-name {
|
| 355 |
+
font-weight: 500;
|
| 356 |
+
color: #333;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.document-tag-content {
|
| 360 |
+
display: none;
|
| 361 |
+
padding: 5px 0 5px 20px;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.document-tag-content.active {
|
| 365 |
+
display: block;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
/* 标签容器放在文件名下面 */
|
| 369 |
+
#tags-container {
|
| 370 |
+
margin-top: 8px;
|
| 371 |
+
margin-bottom: 12px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.tag {
|
| 375 |
+
display: inline-block;
|
| 376 |
+
background-color: #e7f3ff; /* Light blue background */
|
| 377 |
+
color: #0d6efd; /* Blue text */
|
| 378 |
+
padding: 5px 12px; /* Adjusted padding */
|
| 379 |
+
border-radius: 15px;
|
| 380 |
+
font-size: 0.85rem;
|
| 381 |
+
font-weight: 500;
|
| 382 |
+
margin-right: 6px;
|
| 383 |
+
margin-bottom: 6px;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
#summary-area {
|
| 387 |
+
flex-grow: 1;
|
| 388 |
+
width: 100%;
|
| 389 |
+
min-height: 150px; /* 增加高度 */
|
| 390 |
+
padding: 12px 15px;
|
| 391 |
+
border: 1px solid #ccc;
|
| 392 |
+
border-radius: 5px;
|
| 393 |
+
background-color: #f8f9fa; /* Light background for textarea */
|
| 394 |
+
resize: vertical; /* Allow vertical resize */
|
| 395 |
+
outline: none;
|
| 396 |
+
font-size: 0.95rem;
|
| 397 |
+
line-height: 1.5;
|
| 398 |
+
color: #333;
|
| 399 |
+
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
| 400 |
+
white-space: pre-wrap; /* 保留换行 */
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
/* Knowledge Folders Panel */
|
| 404 |
+
.knowledge-folders-panel .folder-list {
|
| 405 |
+
list-style: none;
|
| 406 |
+
padding: 0;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.knowledge-folders-panel .folder-list li {
|
| 410 |
+
padding: 8px 0;
|
| 411 |
+
font-size: 0.95rem;
|
| 412 |
+
color: #333;
|
| 413 |
+
display: flex;
|
| 414 |
+
align-items: center;
|
| 415 |
+
border-bottom: 1px dashed #eee; /* Dashed separator */
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.knowledge-folders-panel .folder-list li:last-child {
|
| 419 |
+
border-bottom: none;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.knowledge-folders-panel .folder-list i {
|
| 423 |
+
margin-right: 10px;
|
| 424 |
+
color: #ffc107; /* Folder icon color */
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
/* Chat Panel */
|
| 428 |
+
.chat-panel {
|
| 429 |
+
display: flex;
|
| 430 |
+
flex-direction: column;
|
| 431 |
+
overflow: hidden; /* Prevent content overflow */
|
| 432 |
+
height: 450px; /* Fixed height for chat panel */
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.chat-history {
|
| 436 |
+
flex-grow: 1;
|
| 437 |
+
padding: 15px;
|
| 438 |
+
overflow-y: auto;
|
| 439 |
+
display: flex;
|
| 440 |
+
flex-direction: column;
|
| 441 |
+
gap: 15px;
|
| 442 |
+
background-color: #f8f9fa; /* Light background for chat history */
|
| 443 |
+
border-radius: 5px;
|
| 444 |
+
margin-bottom: 15px;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
.message {
|
| 448 |
+
max-width: 85%; /* Slightly wider messages */
|
| 449 |
+
padding: 10px 15px; /* Adjusted padding */
|
| 450 |
+
border-radius: 18px;
|
| 451 |
+
line-height: 1.5;
|
| 452 |
+
word-wrap: break-word;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
.user-message {
|
| 456 |
+
align-self: flex-end;
|
| 457 |
+
background-color: #0d6efd; /* Blue user messages */
|
| 458 |
+
color: white;
|
| 459 |
+
border-bottom-right-radius: 5px;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
.bot-message {
|
| 463 |
+
align-self: flex-start;
|
| 464 |
+
background-color: #e9ecef; /* Light grey bot messages */
|
| 465 |
+
color: #333;
|
| 466 |
+
border-bottom-left-radius: 5px;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
.input-container {
|
| 470 |
+
padding-top: 15px;
|
| 471 |
+
display: flex;
|
| 472 |
+
align-items: center; /* Align items vertically */
|
| 473 |
+
gap: 10px;
|
| 474 |
+
border-top: 1px solid #e0e0e0;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
#user-input {
|
| 478 |
+
flex-grow: 1;
|
| 479 |
+
padding: 10px 15px;
|
| 480 |
+
border: 1px solid #ccc;
|
| 481 |
+
border-radius: 20px;
|
| 482 |
+
background-color: #fff;
|
| 483 |
+
resize: none;
|
| 484 |
+
outline: none;
|
| 485 |
+
min-height: 40px; /* Match button height */
|
| 486 |
+
max-height: 100px; /* Limit expansion */
|
| 487 |
+
font-size: 0.95rem;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
#send-btn {
|
| 491 |
+
flex-shrink: 0;
|
| 492 |
+
width: 40px;
|
| 493 |
+
height: 40px;
|
| 494 |
+
border-radius: 50%;
|
| 495 |
+
background-color: #0d6efd;
|
| 496 |
+
color: white;
|
| 497 |
+
border: none;
|
| 498 |
+
cursor: pointer;
|
| 499 |
+
display: flex;
|
| 500 |
+
align-items: center;
|
| 501 |
+
justify-content: center;
|
| 502 |
+
transition: background-color 0.2s ease;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
#send-btn:hover {
|
| 506 |
+
background-color: #0b5ed7;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.status-indicator {
|
| 510 |
+
margin-top: 8px;
|
| 511 |
+
font-size: 0.8rem;
|
| 512 |
+
color: #dc3545; /* Default to error color */
|
| 513 |
+
min-height: 1em;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.status-indicator.success {
|
| 517 |
+
color: #198754; /* Green for success */
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
/* Review Panel */
|
| 521 |
+
.review-panel p {
|
| 522 |
+
font-size: 0.95rem;
|
| 523 |
+
color: #555;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
.review-panel strong {
|
| 527 |
+
color: #0d6efd;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
/* Remove old status indicator style from chat header */
|
| 531 |
+
/* #status-indicator { ... } */
|
| 532 |
+
|
| 533 |
+
/* Remove old typing indicator styles if they exist */
|
| 534 |
+
/* .typing-indicator { ... } */
|
| 535 |
+
/* .typing-dot { ... } */
|
| 536 |
+
/* @keyframes typingAnimation { ... } */
|
| 537 |
+
|
| 538 |
+
/* Adjustments for responsiveness if needed */
|
| 539 |
+
@media (max-width: 1200px) {
|
| 540 |
+
.top-section,
|
| 541 |
+
.middle-section {
|
| 542 |
+
flex-direction: column;
|
| 543 |
+
}
|
| 544 |
+
.chat-panel {
|
| 545 |
+
height: auto; /* Allow chat panel height to adjust */
|
| 546 |
+
min-height: 400px;
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
@media (max-width: 768px) {
|
| 551 |
+
.sidebar {
|
| 552 |
+
width: 100%;
|
| 553 |
+
height: auto;
|
| 554 |
+
position: static; /* Or fixed/sticky as needed */
|
| 555 |
+
border-right: none;
|
| 556 |
+
border-bottom: 1px solid #e0e0e0;
|
| 557 |
+
}
|
| 558 |
+
.main-content {
|
| 559 |
+
padding: 15px;
|
| 560 |
+
}
|
| 561 |
+
body {
|
| 562 |
+
flex-direction: column;
|
| 563 |
+
height: auto;
|
| 564 |
+
overflow: visible;
|
| 565 |
+
}
|
| 566 |
+
.container {
|
| 567 |
+
flex-direction: column;
|
| 568 |
+
height: auto;
|
| 569 |
+
}
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
/* 打字指示器样式 */
|
| 573 |
+
.typing-indicator span {
|
| 574 |
+
display: inline-block;
|
| 575 |
+
width: 6px;
|
| 576 |
+
height: 6px;
|
| 577 |
+
background-color: #999;
|
| 578 |
+
border-radius: 50%;
|
| 579 |
+
margin: 0 2px;
|
| 580 |
+
animation: typingAnimation 1.4s infinite ease-in-out;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
.typing-indicator span:nth-child(1) {
|
| 584 |
+
animation-delay: 0s;
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.typing-indicator span:nth-child(2) {
|
| 588 |
+
animation-delay: 0.2s;
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
.typing-indicator span:nth-child(3) {
|
| 592 |
+
animation-delay: 0.4s;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
@keyframes typingAnimation {
|
| 596 |
+
0%, 60%, 100% {
|
| 597 |
+
transform: translateY(0);
|
| 598 |
+
opacity: 0.6;
|
| 599 |
+
}
|
| 600 |
+
30% {
|
| 601 |
+
transform: translateY(-4px);
|
| 602 |
+
opacity: 1;
|
| 603 |
+
}
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
/* 文件状态样式 */
|
| 607 |
+
.file-status {
|
| 608 |
+
color: #666;
|
| 609 |
+
font-size: 0.85rem;
|
| 610 |
+
font-style: italic;
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
/* 状态指示器类型 */
|
| 614 |
+
.status-indicator.info {
|
| 615 |
+
color: #0d6efd;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
.status-indicator.success {
|
| 619 |
+
color: #198754;
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
.status-indicator.error {
|
| 623 |
+
color: #dc3545;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
/* 添加错误消息样式 */
|
| 627 |
+
.error-message {
|
| 628 |
+
background-color: #ffe6e6 !important; /* 浅红色背景 */
|
| 629 |
+
border-left: 3px solid #dc3545; /* 红色左边框 */
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
.error-message .message-content {
|
| 633 |
+
color: #721c24; /* 深红色文本 */
|
| 634 |
+
font-size: 0.9rem;
|
| 635 |
+
line-height: 1.6;
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
/* 空标签样式 */
|
| 639 |
+
.tag.empty-tag {
|
| 640 |
+
background-color: #f0f0f0; /* 灰色背景 */
|
| 641 |
+
color: #666; /* 灰色文本 */
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
/* 摘要内容区域样式 */
|
| 645 |
+
.summary-content {
|
| 646 |
+
width: 100%;
|
| 647 |
+
min-height: 150px;
|
| 648 |
+
max-height: 400px;
|
| 649 |
+
padding: 12px 15px;
|
| 650 |
+
border: 1px solid #ccc;
|
| 651 |
+
border-radius: 5px;
|
| 652 |
+
background-color: #f8f9fa;
|
| 653 |
+
overflow-y: auto;
|
| 654 |
+
font-size: 0.95rem;
|
| 655 |
+
line-height: 1.5;
|
| 656 |
+
color: #333;
|
| 657 |
+
white-space: pre-wrap;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
/* 文件标题样式 - 对应Markdown中的## */
|
| 661 |
+
.summary-content h2 {
|
| 662 |
+
font-size: 1.1rem;
|
| 663 |
+
font-weight: 700;
|
| 664 |
+
color: #0d6efd;
|
| 665 |
+
margin-top: 15px;
|
| 666 |
+
margin-bottom: 10px;
|
| 667 |
+
padding-left: 8px;
|
| 668 |
+
border-left: 3px solid #0d6efd;
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
/* 摘要标题样式 - 对应Markdown中的### */
|
| 672 |
+
.summary-content h3 {
|
| 673 |
+
font-size: 0.95rem;
|
| 674 |
+
font-weight: 600;
|
| 675 |
+
color: #444;
|
| 676 |
+
margin-top: 8px;
|
| 677 |
+
margin-bottom: 8px;
|
| 678 |
+
padding-left: 5px;
|
| 679 |
+
border-left: 2px solid #28a745;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
/* 摘要中的段落样式 */
|
| 683 |
+
.summary-content p {
|
| 684 |
+
margin-bottom: 10px;
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
/* 占位文本样式 */
|
| 688 |
+
.placeholder-text {
|
| 689 |
+
color: #999;
|
| 690 |
+
font-style: italic;
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
/* 文件分隔线样式 */
|
| 694 |
+
.file-divider {
|
| 695 |
+
height: 1px;
|
| 696 |
+
background-color: #e0e0e0;
|
| 697 |
+
margin: 15px 0;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
/* 知识图谱页面样式 */
|
| 701 |
+
.knowledge-map-section {
|
| 702 |
+
height: calc(100vh - 60px);
|
| 703 |
+
display: flex;
|
| 704 |
+
flex-direction: column;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.knowledge-map-panel {
|
| 708 |
+
display: flex;
|
| 709 |
+
flex-direction: column;
|
| 710 |
+
flex-grow: 1;
|
| 711 |
+
overflow: hidden;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
.map-controls {
|
| 715 |
+
display: flex;
|
| 716 |
+
align-items: center;
|
| 717 |
+
padding: 10px 0;
|
| 718 |
+
margin-bottom: 15px;
|
| 719 |
+
border-bottom: 1px solid #e0e0e0;
|
| 720 |
+
flex-wrap: wrap; /* 允许控制项在窄屏幕上换行 */
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
.control-group {
|
| 724 |
+
display: flex;
|
| 725 |
+
align-items: center;
|
| 726 |
+
margin-right: 20px;
|
| 727 |
+
margin-bottom: 5px; /* 在换行时提供一些垂直间距 */
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
.control-group label {
|
| 731 |
+
margin-right: 8px;
|
| 732 |
+
font-size: 0.9rem;
|
| 733 |
+
color: #555;
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
.map-control-select {
|
| 737 |
+
padding: 5px 10px;
|
| 738 |
+
border: 1px solid #ccc;
|
| 739 |
+
border-radius: 4px;
|
| 740 |
+
font-size: 0.9rem;
|
| 741 |
+
background-color: #fff;
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
.map-control-btn {
|
| 745 |
+
background-color: #0d6efd;
|
| 746 |
+
color: white;
|
| 747 |
+
padding: 5px 12px;
|
| 748 |
+
border: none;
|
| 749 |
+
border-radius: 4px;
|
| 750 |
+
cursor: pointer;
|
| 751 |
+
font-size: 0.9rem;
|
| 752 |
+
transition: background-color 0.2s ease;
|
| 753 |
+
margin-right: 10px; /* 按钮之间的间距 */
|
| 754 |
+
white-space: nowrap; /* 防止文本换行 */
|
| 755 |
+
display: flex;
|
| 756 |
+
align-items: center;
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.map-control-btn i {
|
| 760 |
+
margin-right: 5px;
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
.map-control-btn:hover {
|
| 764 |
+
background-color: #0b5ed7;
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
#view-toggle-btn {
|
| 768 |
+
background-color: #6c757d; /* 不同的颜色以区分功能 */
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
#view-toggle-btn:hover {
|
| 772 |
+
background-color: #5a6268;
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
/* 添加视图模式指示器 */
|
| 776 |
+
.view-mode-indicator {
|
| 777 |
+
margin-left: auto; /* 推到右边 */
|
| 778 |
+
font-size: 0.85rem;
|
| 779 |
+
color: #6c757d;
|
| 780 |
+
font-style: italic;
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
/* 节点类型样式 */
|
| 784 |
+
.tag-node {
|
| 785 |
+
color: #0d6efd;
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
.document-node {
|
| 789 |
+
color: #e15759;
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
.category-node {
|
| 793 |
+
color: #f28e2c;
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
.knowledge-map-container {
|
| 797 |
+
flex-grow: 1;
|
| 798 |
+
width: 100%;
|
| 799 |
+
height: 70%;
|
| 800 |
+
min-height: 400px;
|
| 801 |
+
border: 1px solid #e0e0e0;
|
| 802 |
+
border-radius: 5px;
|
| 803 |
+
background-color: #f8f9fa;
|
| 804 |
+
overflow: hidden;
|
| 805 |
+
position: relative;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
.map-info {
|
| 809 |
+
margin-top: 15px;
|
| 810 |
+
padding-top: 15px;
|
| 811 |
+
border-top: 1px solid #e0e0e0;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
.map-info p {
|
| 815 |
+
font-size: 0.9rem;
|
| 816 |
+
color: #666;
|
| 817 |
+
margin-bottom: 10px;
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
.node-info {
|
| 821 |
+
background-color: #f8f9fa;
|
| 822 |
+
border: 1px solid #e0e0e0;
|
| 823 |
+
border-radius: 5px;
|
| 824 |
+
padding: 15px;
|
| 825 |
+
margin-top: 10px;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.node-info h3 {
|
| 829 |
+
font-size: 1.1rem;
|
| 830 |
+
font-weight: 600;
|
| 831 |
+
color: #333;
|
| 832 |
+
margin-bottom: 10px;
|
| 833 |
+
padding-left: 8px;
|
| 834 |
+
border-left: 3px solid #0d6efd;
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
.node-details {
|
| 838 |
+
font-size: 0.95rem;
|
| 839 |
+
line-height: 1.5;
|
| 840 |
+
color: #444;
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
.node-details .tag-list {
|
| 844 |
+
display: flex;
|
| 845 |
+
flex-wrap: wrap;
|
| 846 |
+
gap: 5px;
|
| 847 |
+
margin-top: 8px;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.node-details .document-link {
|
| 851 |
+
display: inline-flex;
|
| 852 |
+
align-items: center;
|
| 853 |
+
margin-top: 10px;
|
| 854 |
+
background-color: #e7f3ff;
|
| 855 |
+
color: #0d6efd;
|
| 856 |
+
padding: 6px 12px;
|
| 857 |
+
border-radius: 4px;
|
| 858 |
+
text-decoration: none;
|
| 859 |
+
font-size: 0.9rem;
|
| 860 |
+
transition: all 0.2s ease;
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
.node-details .document-link:hover {
|
| 864 |
+
background-color: #0d6efd;
|
| 865 |
+
color: white;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
/* 词云链接样式 */
|
| 869 |
+
.node-details .wordcloud-link {
|
| 870 |
+
display: inline-flex;
|
| 871 |
+
align-items: center;
|
| 872 |
+
margin-top: 10px;
|
| 873 |
+
margin-left: 10px;
|
| 874 |
+
background-color: #e8f5e9;
|
| 875 |
+
color: #2e7d32;
|
| 876 |
+
padding: 6px 12px;
|
| 877 |
+
border-radius: 4px;
|
| 878 |
+
text-decoration: none;
|
| 879 |
+
font-size: 0.9rem;
|
| 880 |
+
transition: all 0.2s ease;
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
.node-details .wordcloud-link:hover {
|
| 884 |
+
background-color: #2e7d32;
|
| 885 |
+
color: white;
|
| 886 |
+
}
|
| 887 |
+
|
| 888 |
+
/* 文档操作按钮容器 */
|
| 889 |
+
.document-action-buttons {
|
| 890 |
+
display: flex;
|
| 891 |
+
flex-wrap: wrap;
|
| 892 |
+
margin-top: 15px;
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
.document-action-buttons a i {
|
| 896 |
+
margin-right: 5px;
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
/* 知识图谱缩放控制器 */
|
| 900 |
+
.zoom-controllers {
|
| 901 |
+
position: absolute;
|
| 902 |
+
top: 10px;
|
| 903 |
+
right: 10px;
|
| 904 |
+
z-index: 100;
|
| 905 |
+
display: flex;
|
| 906 |
+
flex-direction: column;
|
| 907 |
+
gap: 5px;
|
| 908 |
+
background-color: rgba(255, 255, 255, 0.8);
|
| 909 |
+
border-radius: 4px;
|
| 910 |
+
padding: 5px;
|
| 911 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
.zoom-btn {
|
| 915 |
+
width: 36px;
|
| 916 |
+
height: 36px;
|
| 917 |
+
border-radius: 4px;
|
| 918 |
+
background-color: white;
|
| 919 |
+
border: 1px solid #ddd;
|
| 920 |
+
cursor: pointer;
|
| 921 |
+
display: flex;
|
| 922 |
+
align-items: center;
|
| 923 |
+
justify-content: center;
|
| 924 |
+
transition: all 0.2s ease;
|
| 925 |
+
}
|
| 926 |
+
|
| 927 |
+
.zoom-btn:hover {
|
| 928 |
+
background-color: #f0f0f0;
|
| 929 |
+
border-color: #ccc;
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
.zoom-btn i {
|
| 933 |
+
color: #555;
|
| 934 |
+
font-size: 16px;
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
.zoom-btn:hover i {
|
| 938 |
+
color: #0d6efd;
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
/* 强调当前活动的节点 */
|
| 942 |
+
.active-node-path {
|
| 943 |
+
stroke-width: 2;
|
| 944 |
+
stroke: #0d6efd;
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
/* 在知识图谱容器上添加相对定位以支持缩放控制器定位 */
|
| 948 |
+
.knowledge-map-container.draggable {
|
| 949 |
+
cursor: grab;
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
.knowledge-map-container.dragging {
|
| 953 |
+
cursor: grabbing;
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
/* 词云图布局样式 */
|
| 957 |
+
.wordcloud-mode .node {
|
| 958 |
+
font-size: 1.2em;
|
| 959 |
+
font-weight: bold;
|
| 960 |
+
transition: transform 0.3s ease, font-size 0.3s ease;
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
.wordcloud-mode .node:hover {
|
| 964 |
+
transform: scale(1.1);
|
| 965 |
+
z-index: 10;
|
| 966 |
+
}
|
| 967 |
+
|
| 968 |
+
.wordcloud-mode .document-node {
|
| 969 |
+
color: #0d6efd;
|
| 970 |
+
font-size: 1.3em;
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
/* 词云图模态框样式 */
|
| 974 |
+
.wordcloud-modal {
|
| 975 |
+
position: fixed;
|
| 976 |
+
top: 0;
|
| 977 |
+
left: 0;
|
| 978 |
+
width: 100%;
|
| 979 |
+
height: 100%;
|
| 980 |
+
background-color: rgba(0, 0, 0, 0.7);
|
| 981 |
+
display: flex;
|
| 982 |
+
align-items: center;
|
| 983 |
+
justify-content: center;
|
| 984 |
+
z-index: 1000;
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
.wordcloud-modal-content {
|
| 988 |
+
width: 80%;
|
| 989 |
+
max-width: 900px;
|
| 990 |
+
max-height: 80vh;
|
| 991 |
+
background-color: white;
|
| 992 |
+
border-radius: 8px;
|
| 993 |
+
overflow: auto;
|
| 994 |
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
| 995 |
+
display: flex;
|
| 996 |
+
flex-direction: column;
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
.wordcloud-modal-header {
|
| 1000 |
+
padding: 15px 20px;
|
| 1001 |
+
border-bottom: 1px solid #e0e0e0;
|
| 1002 |
+
display: flex;
|
| 1003 |
+
justify-content: space-between;
|
| 1004 |
+
align-items: center;
|
| 1005 |
+
}
|
| 1006 |
+
|
| 1007 |
+
.wordcloud-modal-header h3 {
|
| 1008 |
+
margin: 0;
|
| 1009 |
+
color: #333;
|
| 1010 |
+
font-size: 1.2rem;
|
| 1011 |
+
}
|
| 1012 |
+
|
| 1013 |
+
.close-modal {
|
| 1014 |
+
background: none;
|
| 1015 |
+
border: none;
|
| 1016 |
+
font-size: 1.2rem;
|
| 1017 |
+
color: #666;
|
| 1018 |
+
cursor: pointer;
|
| 1019 |
+
padding: 5px;
|
| 1020 |
+
border-radius: 50%;
|
| 1021 |
+
width: 30px;
|
| 1022 |
+
height: 30px;
|
| 1023 |
+
display: flex;
|
| 1024 |
+
align-items: center;
|
| 1025 |
+
justify-content: center;
|
| 1026 |
+
transition: background-color 0.2s ease;
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
.close-modal:hover {
|
| 1030 |
+
background-color: #f0f0f0;
|
| 1031 |
+
color: #333;
|
| 1032 |
+
}
|
| 1033 |
+
|
| 1034 |
+
.wordcloud-modal-body {
|
| 1035 |
+
padding: 20px;
|
| 1036 |
+
overflow-y: auto;
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
.loading-indicator {
|
| 1040 |
+
text-align: center;
|
| 1041 |
+
padding: 30px;
|
| 1042 |
+
color: #666;
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
.loading-indicator i {
|
| 1046 |
+
font-size: 2rem;
|
| 1047 |
+
margin-bottom: 10px;
|
| 1048 |
+
color: #0d6efd;
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
.wordcloud-image-container {
|
| 1052 |
+
text-align: center;
|
| 1053 |
+
margin-bottom: 20px;
|
| 1054 |
+
}
|
| 1055 |
+
|
| 1056 |
+
.wordcloud-image {
|
| 1057 |
+
max-width: 100%;
|
| 1058 |
+
max-height: 400px;
|
| 1059 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
| 1060 |
+
border-radius: 4px;
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
.wordcloud-freq-container {
|
| 1064 |
+
border-top: 1px solid #e0e0e0;
|
| 1065 |
+
padding-top: 15px;
|
| 1066 |
+
margin-top: 15px;
|
| 1067 |
+
}
|
| 1068 |
+
|
| 1069 |
+
.wordcloud-freq-container h4 {
|
| 1070 |
+
margin-bottom: 10px;
|
| 1071 |
+
color: #333;
|
| 1072 |
+
font-size: 1rem;
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
.word-freq-list {
|
| 1076 |
+
display: flex;
|
| 1077 |
+
flex-wrap: wrap;
|
| 1078 |
+
gap: 8px;
|
| 1079 |
+
max-height: 150px;
|
| 1080 |
+
overflow-y: auto;
|
| 1081 |
+
padding: 10px;
|
| 1082 |
+
background-color: #f8f9fa;
|
| 1083 |
+
border-radius: 5px;
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
/* 在词云图布局中强调文档节点 */
|
| 1087 |
+
.map-control-select option[value="wordcloud"] {
|
| 1088 |
+
font-weight: bold;
|
| 1089 |
+
color: #0d6efd;
|
| 1090 |
+
}
|
| 1091 |
+
|
| 1092 |
+
/* 直接生成词云图按钮样式 */
|
| 1093 |
+
.generate-cloud-link {
|
| 1094 |
+
display: inline-flex;
|
| 1095 |
+
align-items: center;
|
| 1096 |
+
margin-top: 10px;
|
| 1097 |
+
margin-left: 10px;
|
| 1098 |
+
background-color: #fff0e5;
|
| 1099 |
+
color: #ff6d00;
|
| 1100 |
+
padding: 6px 12px;
|
| 1101 |
+
border-radius: 4px;
|
| 1102 |
+
text-decoration: none;
|
| 1103 |
+
font-size: 0.9rem;
|
| 1104 |
+
transition: all 0.2s ease;
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
.generate-cloud-link:hover {
|
| 1108 |
+
background-color: #ff6d00;
|
| 1109 |
+
color: white;
|
| 1110 |
+
}
|
| 1111 |
+
|
| 1112 |
+
/* 相关文档样式 */
|
| 1113 |
+
.related-docs {
|
| 1114 |
+
margin-top: 10px;
|
| 1115 |
+
max-height: 300px;
|
| 1116 |
+
overflow-y: auto;
|
| 1117 |
+
border: 1px solid #e0e0e0;
|
| 1118 |
+
border-radius: 5px;
|
| 1119 |
+
padding: 10px;
|
| 1120 |
+
background-color: #f8f9fa;
|
| 1121 |
+
}
|
| 1122 |
+
|
| 1123 |
+
.related-doc-item {
|
| 1124 |
+
padding: 10px;
|
| 1125 |
+
border-bottom: 1px solid #eee;
|
| 1126 |
+
transition: background-color 0.2s ease;
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
.related-doc-item:last-child {
|
| 1130 |
+
border-bottom: none;
|
| 1131 |
+
}
|
| 1132 |
+
|
| 1133 |
+
.related-doc-item:hover {
|
| 1134 |
+
background-color: #f0f0f0;
|
| 1135 |
+
}
|
| 1136 |
+
|
| 1137 |
+
.doc-title {
|
| 1138 |
+
display: block;
|
| 1139 |
+
font-weight: 500;
|
| 1140 |
+
color: #333;
|
| 1141 |
+
margin-bottom: 5px;
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
/* 词云图专用样式增强 */
|
| 1145 |
+
#knowledge-map-container.wordcloud-mode {
|
| 1146 |
+
background-color: #ffffff !important;
|
| 1147 |
+
border: 1px solid #ddd !important;
|
| 1148 |
+
padding: 10px;
|
| 1149 |
+
overflow: visible !important;
|
| 1150 |
+
min-height: 400px;
|
| 1151 |
+
}
|
| 1152 |
+
|
| 1153 |
+
/* 词云图样式增强 */
|
| 1154 |
+
.wordcloud-mode div {
|
| 1155 |
+
overflow: visible !important;
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
+
/* 确保词云图文字可见 */
|
| 1159 |
+
.wordcloud-mode text,
|
| 1160 |
+
.wordcloud-mode .ec-wordCloud text,
|
| 1161 |
+
.wordcloud-mode svg text {
|
| 1162 |
+
fill: #333 !important;
|
| 1163 |
+
fill-opacity: 1 !important;
|
| 1164 |
+
font-weight: bold !important;
|
| 1165 |
+
stroke: none !important;
|
| 1166 |
+
font-family: 'Arial', '微软雅黑', sans-serif !important;
|
| 1167 |
+
visibility: visible !important;
|
| 1168 |
+
opacity: 1 !important;
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
/* 强制显示SVG元素 */
|
| 1172 |
+
.wordcloud-mode svg,
|
| 1173 |
+
.wordcloud-mode canvas {
|
| 1174 |
+
display: block !important;
|
| 1175 |
+
visibility: visible !important;
|
| 1176 |
+
opacity: 1 !important;
|
| 1177 |
+
}
|
| 1178 |
+
|
| 1179 |
+
/* 去除任何可能引起问题的遮挡元素 */
|
| 1180 |
+
.wordcloud-mode * {
|
| 1181 |
+
pointer-events: auto !important;
|
| 1182 |
+
}
|
| 1183 |
+
|
| 1184 |
+
/* 确保词语悬停时的效果 */
|
| 1185 |
+
.wordcloud-mode text:hover,
|
| 1186 |
+
.wordcloud-mode .ec-wordCloud text:hover {
|
| 1187 |
+
fill: #f00 !important;
|
| 1188 |
+
cursor: pointer;
|
| 1189 |
+
transform: scale(1.1);
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
/* 词云图词语样式 */
|
| 1193 |
+
.ec-wordCloud text {
|
| 1194 |
+
cursor: pointer;
|
| 1195 |
+
transition: all 0.3s ease;
|
| 1196 |
+
text-shadow: 0 2px 1px rgba(0,0,0,0.1);
|
| 1197 |
+
fill-opacity: 1 !important; /* 确保文字不透明 */
|
| 1198 |
+
font-weight: bold;
|
| 1199 |
+
}
|
| 1200 |
+
|
| 1201 |
+
.ec-wordCloud text:hover {
|
| 1202 |
+
fill-opacity: 0.8;
|
| 1203 |
+
transform: scale(1.1);
|
| 1204 |
+
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
| 1205 |
+
}
|
| 1206 |
+
|
| 1207 |
+
/* 当选择词云图模式时应用特殊样式 */
|
| 1208 |
+
.wordcloud-mode .ec-wordCloud {
|
| 1209 |
+
transform-origin: center center;
|
| 1210 |
+
animation: wordcloud-appear 1s ease-out;
|
| 1211 |
+
}
|
| 1212 |
+
|
| 1213 |
+
@keyframes wordcloud-appear {
|
| 1214 |
+
0% {
|
| 1215 |
+
opacity: 0;
|
| 1216 |
+
transform: scale(0.95);
|
| 1217 |
+
}
|
| 1218 |
+
100% {
|
| 1219 |
+
opacity: 1;
|
| 1220 |
+
transform: scale(1);
|
| 1221 |
+
}
|
| 1222 |
+
}
|
| 1223 |
+
|
| 1224 |
+
/* 根据字体大小设置不同样式 */
|
| 1225 |
+
.ec-wordCloud text[font-size="24"],
|
| 1226 |
+
.ec-wordCloud text[font-size="25"],
|
| 1227 |
+
.ec-wordCloud text[font-size="26"],
|
| 1228 |
+
.ec-wordCloud text[font-size="27"],
|
| 1229 |
+
.ec-wordCloud text[font-size="28"],
|
| 1230 |
+
.ec-wordCloud text[font-size="29"],
|
| 1231 |
+
.ec-wordCloud text[font-size="30"] {
|
| 1232 |
+
opacity: 0.9;
|
| 1233 |
+
}
|
| 1234 |
+
|
| 1235 |
+
.ec-wordCloud text[font-size="31"],
|
| 1236 |
+
.ec-wordCloud text[font-size="32"],
|
| 1237 |
+
.ec-wordCloud text[font-size="33"],
|
| 1238 |
+
.ec-wordCloud text[font-size="34"],
|
| 1239 |
+
.ec-wordCloud text[font-size="35"],
|
| 1240 |
+
.ec-wordCloud text[font-size="40"] {
|
| 1241 |
+
opacity: 0.95;
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
+
.ec-wordCloud text[font-size="41"],
|
| 1245 |
+
.ec-wordCloud text[font-size="42"],
|
| 1246 |
+
.ec-wordCloud text[font-size="43"],
|
| 1247 |
+
.ec-wordCloud text[font-size="44"],
|
| 1248 |
+
.ec-wordCloud text[font-size="45"],
|
| 1249 |
+
.ec-wordCloud text[font-size="50"] {
|
| 1250 |
+
opacity: 1;
|
| 1251 |
+
font-weight: bold;
|
| 1252 |
+
}
|
| 1253 |
+
|
| 1254 |
+
.ec-wordCloud text[font-size="60"],
|
| 1255 |
+
.ec-wordCloud text[font-size="70"],
|
| 1256 |
+
.ec-wordCloud text[font-size="80"],
|
| 1257 |
+
.ec-wordCloud text[font-size="90"],
|
| 1258 |
+
.ec-wordCloud text[font-size="100"] {
|
| 1259 |
+
opacity: 1;
|
| 1260 |
+
font-weight: bold;
|
| 1261 |
+
text-shadow: 0 2px 3px rgba(0,0,0,0.2);
|
| 1262 |
+
}
|
| 1263 |
+
|
| 1264 |
+
/* 知识图谱节点样式由JS控制 */
|
| 1265 |
+
|
| 1266 |
+
/* 词云图容器样式 */
|
| 1267 |
+
.wordcloud-container {
|
| 1268 |
+
background-color: #ffffff !important;
|
| 1269 |
+
border: 1px solid #e0e0e0 !important;
|
| 1270 |
+
border-radius: 8px !important;
|
| 1271 |
+
padding: 15px !important;
|
| 1272 |
+
min-height: 400px !important;
|
| 1273 |
+
position: relative !important;
|
| 1274 |
+
margin-top: 20px !important;
|
| 1275 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
/* 确保词云图内的文字清晰可见 */
|
| 1279 |
+
.wordcloud-container .echarts-for-react text,
|
| 1280 |
+
.wordcloud-container text {
|
| 1281 |
+
fill: #333333 !important;
|
| 1282 |
+
font-weight: bold !important;
|
| 1283 |
+
font-family: 'Arial', sans-serif !important;
|
| 1284 |
+
pointer-events: auto !important;
|
| 1285 |
+
cursor: pointer !important;
|
| 1286 |
+
text-shadow: 0px 0px 2px rgba(255, 255, 255, 0.8) !important;
|
| 1287 |
+
transition: all 0.2s ease !important;
|
| 1288 |
+
}
|
| 1289 |
+
|
| 1290 |
+
/* 词云图高亮状态 */
|
| 1291 |
+
.wordcloud-container text:hover {
|
| 1292 |
+
fill: #2c85ff !important;
|
| 1293 |
+
transform: scale(1.1) !important;
|
| 1294 |
+
}
|
| 1295 |
+
|
| 1296 |
+
/* 词云图加载状态 */
|
| 1297 |
+
.wordcloud-loading {
|
| 1298 |
+
position: absolute !important;
|
| 1299 |
+
top: 50% !important;
|
| 1300 |
+
left: 50% !important;
|
| 1301 |
+
transform: translate(-50%, -50%) !important;
|
| 1302 |
+
text-align: center !important;
|
| 1303 |
+
color: #666 !important;
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
/* 相关文档列表样式 */
|
| 1307 |
+
.related-docs {
|
| 1308 |
+
max-height: 300px !important;
|
| 1309 |
+
overflow-y: auto !important;
|
| 1310 |
+
margin-top: 10px !important;
|
| 1311 |
+
border: 1px solid #eee !important;
|
| 1312 |
+
border-radius: 4px !important;
|
| 1313 |
+
padding: 10px !important;
|
| 1314 |
+
}
|
| 1315 |
+
|
| 1316 |
+
.related-doc-item {
|
| 1317 |
+
padding: 8px !important;
|
| 1318 |
+
border-bottom: 1px solid #eee !important;
|
| 1319 |
+
display: flex !important;
|
| 1320 |
+
flex-direction: column !important;
|
| 1321 |
+
}
|
| 1322 |
+
|
| 1323 |
+
.related-doc-item:last-child {
|
| 1324 |
+
border-bottom: none !important;
|
| 1325 |
+
}
|
| 1326 |
+
|
| 1327 |
+
.doc-title {
|
| 1328 |
+
font-weight: bold !important;
|
| 1329 |
+
margin-bottom: 5px !important;
|
| 1330 |
+
}
|
| 1331 |
+
|
| 1332 |
+
.document-action-buttons {
|
| 1333 |
+
display: flex !important;
|
| 1334 |
+
gap: 10px !important;
|
| 1335 |
+
margin-top: 5px !important;
|
| 1336 |
+
}
|
| 1337 |
+
|
| 1338 |
+
.document-link, .wordcloud-link {
|
| 1339 |
+
color: #2c85ff !important;
|
| 1340 |
+
text-decoration: none !important;
|
| 1341 |
+
font-size: 0.9em !important;
|
| 1342 |
+
display: inline-flex !important;
|
| 1343 |
+
align-items: center !important;
|
| 1344 |
+
gap: 4px !important;
|
| 1345 |
+
}
|
| 1346 |
+
|
| 1347 |
+
.document-link:hover, .wordcloud-link:hover {
|
| 1348 |
+
text-decoration: underline !important;
|
| 1349 |
+
}
|
| 1350 |
+
|
| 1351 |
+
/* 树形图节点悬停样式 */
|
| 1352 |
+
.node:hover {
|
| 1353 |
+
cursor: pointer !important;
|
| 1354 |
+
filter: brightness(1.1) !important;
|
| 1355 |
+
}
|
| 1356 |
+
|
| 1357 |
+
/* 添加知识文件夹相关的样式 */
|
| 1358 |
+
.folder-list {
|
| 1359 |
+
list-style: none;
|
| 1360 |
+
padding: 0;
|
| 1361 |
+
margin: 0;
|
| 1362 |
+
}
|
| 1363 |
+
|
| 1364 |
+
.folder-item {
|
| 1365 |
+
margin-bottom: 10px;
|
| 1366 |
+
border-radius: 6px;
|
| 1367 |
+
overflow: hidden;
|
| 1368 |
+
border: 1px solid #e5e7eb;
|
| 1369 |
+
background-color: #f9fafb;
|
| 1370 |
+
}
|
| 1371 |
+
|
| 1372 |
+
.folder-header {
|
| 1373 |
+
padding: 10px 15px;
|
| 1374 |
+
display: flex;
|
| 1375 |
+
justify-content: space-between;
|
| 1376 |
+
align-items: center;
|
| 1377 |
+
cursor: pointer;
|
| 1378 |
+
background-color: #f3f4f6;
|
| 1379 |
+
transition: background-color 0.3s;
|
| 1380 |
+
}
|
| 1381 |
+
|
| 1382 |
+
.folder-header:hover {
|
| 1383 |
+
background-color: #e5e7eb;
|
| 1384 |
+
}
|
| 1385 |
+
|
| 1386 |
+
.folder-toggle {
|
| 1387 |
+
transition: transform 0.3s;
|
| 1388 |
+
}
|
| 1389 |
+
|
| 1390 |
+
.file-list {
|
| 1391 |
+
list-style: none;
|
| 1392 |
+
padding: 0;
|
| 1393 |
+
margin: 0;
|
| 1394 |
+
background-color: #ffffff;
|
| 1395 |
+
}
|
| 1396 |
+
|
| 1397 |
+
.file-item {
|
| 1398 |
+
padding: 8px 15px 8px 35px;
|
| 1399 |
+
border-top: 1px solid #e5e7eb;
|
| 1400 |
+
display: flex;
|
| 1401 |
+
align-items: center;
|
| 1402 |
+
font-size: 0.9em;
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
.file-item i {
|
| 1406 |
+
margin-right: 8px;
|
| 1407 |
+
color: #6b7280;
|
| 1408 |
+
}
|
| 1409 |
+
|
| 1410 |
+
.file-tag {
|
| 1411 |
+
margin-left: 5px;
|
| 1412 |
+
font-size: 0.85em;
|
| 1413 |
+
color: #9ca3af;
|
| 1414 |
+
font-style: italic;
|
| 1415 |
+
}
|
| 1416 |
+
|
| 1417 |
+
.file-link {
|
| 1418 |
+
color: #3b82f6;
|
| 1419 |
+
text-decoration: none;
|
| 1420 |
+
}
|
| 1421 |
+
|
| 1422 |
+
.file-link:hover {
|
| 1423 |
+
text-decoration: underline;
|
| 1424 |
+
}
|
| 1425 |
+
|
| 1426 |
+
.demo-notice {
|
| 1427 |
+
margin-top: 15px;
|
| 1428 |
+
padding: 10px;
|
| 1429 |
+
border-radius: 6px;
|
| 1430 |
+
background-color: #fff7ed;
|
| 1431 |
+
border: 1px solid #ffedd5;
|
| 1432 |
+
color: #c2410c;
|
| 1433 |
+
font-size: 0.9em;
|
| 1434 |
+
}
|
| 1435 |
+
|
| 1436 |
+
/* 确保已存在的样式不受影响 */
|
static/js/chat.js
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
// Get element references
|
| 3 |
+
const chatHistory = document.getElementById('chat-history');
|
| 4 |
+
const userInput = document.getElementById('user-input');
|
| 5 |
+
const sendBtn = document.getElementById('send-btn');
|
| 6 |
+
const statusIndicator = document.getElementById('status-indicator');
|
| 7 |
+
const fileUpload = document.getElementById('file-upload');
|
| 8 |
+
const uploadButton = document.getElementById('upload-button');
|
| 9 |
+
const fileList = document.getElementById('file-list');
|
| 10 |
+
const summaryArea = document.getElementById('summary-area');
|
| 11 |
+
const documentTagsContainer = document.getElementById('document-tags-container');
|
| 12 |
+
const serverData = document.getElementById('server-data');
|
| 13 |
+
|
| 14 |
+
// Array to store uploaded file information
|
| 15 |
+
let uploadedFiles = [];
|
| 16 |
+
|
| 17 |
+
// 从服务器数据加载已上传的文件信息
|
| 18 |
+
if (serverData) {
|
| 19 |
+
try {
|
| 20 |
+
const uploadedFilesData = serverData.getAttribute('data-uploaded-files');
|
| 21 |
+
if (uploadedFilesData) {
|
| 22 |
+
uploadedFiles = JSON.parse(uploadedFilesData);
|
| 23 |
+
console.log('Loaded uploaded files from server:', uploadedFiles);
|
| 24 |
+
|
| 25 |
+
// 更新摘要和标签区域
|
| 26 |
+
if (uploadedFiles.length > 0) {
|
| 27 |
+
updateSummaryArea();
|
| 28 |
+
updateDocumentTags();
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
} catch (error) {
|
| 32 |
+
console.error('Error parsing uploaded files data:', error);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Bind event listeners
|
| 37 |
+
sendBtn.addEventListener('click', sendMessage);
|
| 38 |
+
userInput.addEventListener('keydown', (e) => {
|
| 39 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 40 |
+
e.preventDefault();
|
| 41 |
+
sendMessage();
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
// Add delegated event listener to document tags container to handle click events
|
| 46 |
+
if (documentTagsContainer) {
|
| 47 |
+
documentTagsContainer.addEventListener('click', (e) => {
|
| 48 |
+
// Find the closest tag header
|
| 49 |
+
const header = e.target.closest('.document-tag-header');
|
| 50 |
+
if (header) {
|
| 51 |
+
// Toggle active class
|
| 52 |
+
header.classList.toggle('active');
|
| 53 |
+
|
| 54 |
+
// Find corresponding content area
|
| 55 |
+
const content = header.nextElementSibling;
|
| 56 |
+
if (content && content.classList.contains('document-tag-content')) {
|
| 57 |
+
content.classList.toggle('active');
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Connect upload button and file input
|
| 64 |
+
if (uploadButton && fileUpload) {
|
| 65 |
+
uploadButton.addEventListener('click', () => {
|
| 66 |
+
fileUpload.click();
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
fileUpload.addEventListener('change', (e) => {
|
| 70 |
+
if (fileUpload.files && fileUpload.files.length > 0) {
|
| 71 |
+
const file = fileUpload.files[0]; // 只处理第一个文件
|
| 72 |
+
console.log(`Selected file for upload: ${file.name}, type: ${file.type}`);
|
| 73 |
+
|
| 74 |
+
// 验证文件类型
|
| 75 |
+
const validTypes = [
|
| 76 |
+
'application/pdf',
|
| 77 |
+
'text/plain'
|
| 78 |
+
];
|
| 79 |
+
|
| 80 |
+
const fileExt = file.name.split('.').pop().toLowerCase();
|
| 81 |
+
|
| 82 |
+
if (!validTypes.includes(file.type) && !(fileExt === 'pdf' || fileExt === 'txt')) {
|
| 83 |
+
alert('Please select a PDF or TXT file only');
|
| 84 |
+
fileUpload.value = ''; // 清除选择
|
| 85 |
+
return;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Clear selected files to allow selecting the same file again
|
| 89 |
+
const selectedFile = file;
|
| 90 |
+
fileUpload.value = '';
|
| 91 |
+
|
| 92 |
+
// Process the file
|
| 93 |
+
console.log(`Starting to process file: ${selectedFile.name}`);
|
| 94 |
+
handleFileUpload(selectedFile);
|
| 95 |
+
}
|
| 96 |
+
});
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Check API key and enable input
|
| 100 |
+
checkApiConnection();
|
| 101 |
+
|
| 102 |
+
// Check API connection
|
| 103 |
+
async function checkApiConnection() {
|
| 104 |
+
try {
|
| 105 |
+
setStatusMessage('Checking API connection...', 'info');
|
| 106 |
+
|
| 107 |
+
const response = await fetch('/chat', {
|
| 108 |
+
method: 'POST',
|
| 109 |
+
headers: { 'Content-Type': 'application/json' },
|
| 110 |
+
body: JSON.stringify({ message: 'Test connection' })
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
const data = await response.json();
|
| 114 |
+
|
| 115 |
+
if (response.status !== 200) {
|
| 116 |
+
if (data.error && data.error.includes('API key')) {
|
| 117 |
+
setStatusMessage('Please configure Google API key in settings page', 'error');
|
| 118 |
+
} else {
|
| 119 |
+
setStatusMessage(data.error || 'Unable to connect to API', 'error');
|
| 120 |
+
}
|
| 121 |
+
} else {
|
| 122 |
+
setStatusMessage('', '');
|
| 123 |
+
// Enable input
|
| 124 |
+
userInput.disabled = false;
|
| 125 |
+
sendBtn.disabled = false;
|
| 126 |
+
}
|
| 127 |
+
} catch (error) {
|
| 128 |
+
console.error('API check failed:', error);
|
| 129 |
+
setStatusMessage('Server connection error, please try again later', 'error');
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// Send chat message
|
| 134 |
+
async function sendMessage() {
|
| 135 |
+
const message = userInput.value.trim();
|
| 136 |
+
if (!message) return;
|
| 137 |
+
|
| 138 |
+
// Add user message to chat area
|
| 139 |
+
addMessageToChat(message, 'user');
|
| 140 |
+
userInput.value = '';
|
| 141 |
+
|
| 142 |
+
// Disable input and show typing indicator
|
| 143 |
+
toggleInputState(false);
|
| 144 |
+
const typingIndicator = showTypingIndicator();
|
| 145 |
+
|
| 146 |
+
try {
|
| 147 |
+
const response = await fetch('/chat', {
|
| 148 |
+
method: 'POST',
|
| 149 |
+
headers: { 'Content-Type': 'application/json' },
|
| 150 |
+
body: JSON.stringify({ message })
|
| 151 |
+
});
|
| 152 |
+
|
| 153 |
+
// Remove typing indicator
|
| 154 |
+
hideTypingIndicator(typingIndicator);
|
| 155 |
+
|
| 156 |
+
const data = await response.json();
|
| 157 |
+
|
| 158 |
+
if (response.ok && data.reply) {
|
| 159 |
+
addMessageToChat(data.reply, 'bot');
|
| 160 |
+
setStatusMessage('', '');
|
| 161 |
+
} else {
|
| 162 |
+
const errorMessage = data.error || 'Unknown error, please try again';
|
| 163 |
+
setStatusMessage(errorMessage, 'error');
|
| 164 |
+
console.error('API error:', data);
|
| 165 |
+
}
|
| 166 |
+
} catch (error) {
|
| 167 |
+
hideTypingIndicator(typingIndicator);
|
| 168 |
+
setStatusMessage('Unable to connect to server, please check your network', 'error');
|
| 169 |
+
console.error('Request error:', error);
|
| 170 |
+
} finally {
|
| 171 |
+
toggleInputState(true);
|
| 172 |
+
scrollToBottom();
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// Update summary area to display summaries for all files
|
| 177 |
+
function updateSummaryArea() {
|
| 178 |
+
if (!summaryArea) return;
|
| 179 |
+
|
| 180 |
+
// Clear current content
|
| 181 |
+
summaryArea.innerHTML = '';
|
| 182 |
+
|
| 183 |
+
if (uploadedFiles.length === 0) {
|
| 184 |
+
// Show placeholder text
|
| 185 |
+
const placeholder = document.createElement('p');
|
| 186 |
+
placeholder.className = 'placeholder-text';
|
| 187 |
+
placeholder.textContent = 'Summary will appear after uploading files...';
|
| 188 |
+
summaryArea.appendChild(placeholder);
|
| 189 |
+
return;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// Add summary for each uploaded file
|
| 193 |
+
uploadedFiles.forEach((file, index) => {
|
| 194 |
+
// Add file separator (except for first file)
|
| 195 |
+
if (index > 0) {
|
| 196 |
+
const divider = document.createElement('div');
|
| 197 |
+
divider.className = 'file-divider';
|
| 198 |
+
summaryArea.appendChild(divider);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// Add file name title (##)
|
| 202 |
+
const fileTitle = document.createElement('h2');
|
| 203 |
+
fileTitle.textContent = file.filename;
|
| 204 |
+
summaryArea.appendChild(fileTitle);
|
| 205 |
+
|
| 206 |
+
// Add file summary title (###)
|
| 207 |
+
const summaryTitle = document.createElement('h3');
|
| 208 |
+
summaryTitle.textContent = 'File Summary';
|
| 209 |
+
summaryArea.appendChild(summaryTitle);
|
| 210 |
+
|
| 211 |
+
// Add summary content
|
| 212 |
+
const summaryContent = document.createElement('p');
|
| 213 |
+
summaryContent.textContent = file.summary || 'Could not generate summary';
|
| 214 |
+
summaryArea.appendChild(summaryContent);
|
| 215 |
+
});
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// Update document tags area
|
| 219 |
+
function updateDocumentTags() {
|
| 220 |
+
if (!documentTagsContainer) return;
|
| 221 |
+
|
| 222 |
+
// Clear current content
|
| 223 |
+
documentTagsContainer.innerHTML = '';
|
| 224 |
+
|
| 225 |
+
if (uploadedFiles.length === 0) {
|
| 226 |
+
// Show placeholder text
|
| 227 |
+
const placeholder = document.createElement('p');
|
| 228 |
+
placeholder.className = 'placeholder-text';
|
| 229 |
+
placeholder.textContent = 'Tags will appear after uploading files...';
|
| 230 |
+
documentTagsContainer.appendChild(placeholder);
|
| 231 |
+
return;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Create tag group for each uploaded file
|
| 235 |
+
uploadedFiles.forEach(file => {
|
| 236 |
+
// Create document tag group
|
| 237 |
+
const tagGroup = document.createElement('div');
|
| 238 |
+
tagGroup.className = 'document-tag-group';
|
| 239 |
+
|
| 240 |
+
// Create collapsible tag header
|
| 241 |
+
const tagHeader = document.createElement('div');
|
| 242 |
+
tagHeader.className = 'document-tag-header';
|
| 243 |
+
tagHeader.innerHTML = `
|
| 244 |
+
<i class="fas fa-chevron-right"></i>
|
| 245 |
+
<span class="document-tag-name">${file.filename}</span>
|
| 246 |
+
`;
|
| 247 |
+
|
| 248 |
+
// Create tag content area
|
| 249 |
+
const tagContent = document.createElement('div');
|
| 250 |
+
tagContent.className = 'document-tag-content';
|
| 251 |
+
|
| 252 |
+
// Add tags to content area
|
| 253 |
+
if (file.tags && file.tags.length > 0) {
|
| 254 |
+
file.tags.forEach(tag => {
|
| 255 |
+
const tagEl = document.createElement('span');
|
| 256 |
+
tagEl.className = 'tag';
|
| 257 |
+
tagEl.textContent = tag;
|
| 258 |
+
tagContent.appendChild(tagEl);
|
| 259 |
+
});
|
| 260 |
+
} else {
|
| 261 |
+
const noTagsEl = document.createElement('span');
|
| 262 |
+
noTagsEl.className = 'tag empty-tag';
|
| 263 |
+
noTagsEl.textContent = 'No tags generated';
|
| 264 |
+
tagContent.appendChild(noTagsEl);
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
// Add header and content to tag group
|
| 268 |
+
tagGroup.appendChild(tagHeader);
|
| 269 |
+
tagGroup.appendChild(tagContent);
|
| 270 |
+
|
| 271 |
+
// Add tag group to container
|
| 272 |
+
documentTagsContainer.appendChild(tagGroup);
|
| 273 |
+
});
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
// Update file list
|
| 277 |
+
function updateFileList(fileName, status) {
|
| 278 |
+
if (!fileList) return;
|
| 279 |
+
|
| 280 |
+
// Generate unique file ID for DOM element identification
|
| 281 |
+
const fileId = fileName.replace(/[^a-zA-Z0-9]/g, '_');
|
| 282 |
+
|
| 283 |
+
// Check if file already exists in list
|
| 284 |
+
let existingItem = document.querySelector(`#file-list li[data-file-id="${fileId}"]`);
|
| 285 |
+
|
| 286 |
+
// Determine status class
|
| 287 |
+
let statusClass = '';
|
| 288 |
+
if (status.includes('Uploading')) {
|
| 289 |
+
statusClass = 'file-status-uploading';
|
| 290 |
+
} else if (status.includes('Uploaded')) {
|
| 291 |
+
statusClass = 'file-status-success';
|
| 292 |
+
} else if (status.includes('Failed')) {
|
| 293 |
+
statusClass = 'file-status-error';
|
| 294 |
+
} else if (status.includes('Retrying')) {
|
| 295 |
+
statusClass = 'file-status-retrying';
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
// Create or update file item
|
| 299 |
+
if (!existingItem) {
|
| 300 |
+
// Create new file item
|
| 301 |
+
const fileItem = document.createElement('li');
|
| 302 |
+
fileItem.dataset.fileId = fileId;
|
| 303 |
+
fileItem.dataset.filename = fileName;
|
| 304 |
+
fileItem.innerHTML = `
|
| 305 |
+
<i class="fas fa-file-alt"></i>
|
| 306 |
+
<span class="file-name">${fileName}</span>
|
| 307 |
+
<span class="file-status ${statusClass}">${status}</span>
|
| 308 |
+
`;
|
| 309 |
+
fileList.appendChild(fileItem);
|
| 310 |
+
|
| 311 |
+
// Ensure newly added item scrolls into view
|
| 312 |
+
fileList.scrollTop = fileList.scrollHeight;
|
| 313 |
+
} else {
|
| 314 |
+
// Just update status of existing item
|
| 315 |
+
const statusSpan = existingItem.querySelector('.file-status');
|
| 316 |
+
if (statusSpan) {
|
| 317 |
+
statusSpan.textContent = status;
|
| 318 |
+
statusSpan.className = `file-status ${statusClass}`;
|
| 319 |
+
}
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
// Print debug info
|
| 323 |
+
console.log(`File list updated: ${fileName}, Status: ${status}, Item count: ${fileList.children.length}`);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// Add message to chat history
|
| 327 |
+
function addMessageToChat(content, sender) {
|
| 328 |
+
const messageDiv = document.createElement('div');
|
| 329 |
+
messageDiv.className = `message ${sender}-message`;
|
| 330 |
+
|
| 331 |
+
const contentDiv = document.createElement('div');
|
| 332 |
+
contentDiv.className = 'message-content';
|
| 333 |
+
contentDiv.textContent = content;
|
| 334 |
+
|
| 335 |
+
messageDiv.appendChild(contentDiv);
|
| 336 |
+
chatHistory.appendChild(messageDiv);
|
| 337 |
+
|
| 338 |
+
scrollToBottom();
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
// Show typing indicator
|
| 342 |
+
function showTypingIndicator() {
|
| 343 |
+
const indicator = document.createElement('div');
|
| 344 |
+
indicator.className = 'message bot-message typing-indicator';
|
| 345 |
+
indicator.innerHTML = '<div class="message-content"><span>•</span><span>•</span><span>•</span></div>';
|
| 346 |
+
chatHistory.appendChild(indicator);
|
| 347 |
+
scrollToBottom();
|
| 348 |
+
return indicator;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
// Hide typing indicator
|
| 352 |
+
function hideTypingIndicator(indicator) {
|
| 353 |
+
if (indicator && indicator.parentNode) {
|
| 354 |
+
indicator.parentNode.removeChild(indicator);
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
// Set status message
|
| 359 |
+
function setStatusMessage(message, type = '') {
|
| 360 |
+
if (!statusIndicator) return;
|
| 361 |
+
|
| 362 |
+
statusIndicator.textContent = message;
|
| 363 |
+
statusIndicator.className = 'status-indicator';
|
| 364 |
+
if (type) {
|
| 365 |
+
statusIndicator.classList.add(type);
|
| 366 |
+
}
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
// Toggle input state (enable/disable)
|
| 370 |
+
function toggleInputState(enabled) {
|
| 371 |
+
if (userInput) userInput.disabled = !enabled;
|
| 372 |
+
if (sendBtn) sendBtn.disabled = !enabled;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// Scroll to bottom of chat history
|
| 376 |
+
function scrollToBottom() {
|
| 377 |
+
if (chatHistory) {
|
| 378 |
+
chatHistory.scrollTop = chatHistory.scrollHeight;
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
// Handle file upload
|
| 383 |
+
async function handleFileUpload(file) {
|
| 384 |
+
if (!file) return;
|
| 385 |
+
|
| 386 |
+
const fileName = file.name;
|
| 387 |
+
console.log(`Starting to upload file: ${fileName}`);
|
| 388 |
+
|
| 389 |
+
// Show file name and add to list
|
| 390 |
+
updateFileList(fileName, 'Uploading...');
|
| 391 |
+
setStatusMessage('Uploading file...', 'info');
|
| 392 |
+
|
| 393 |
+
// Create FormData object
|
| 394 |
+
const formData = new FormData();
|
| 395 |
+
formData.append('file', file);
|
| 396 |
+
|
| 397 |
+
let retryCount = 0;
|
| 398 |
+
const maxRetries = 2;
|
| 399 |
+
|
| 400 |
+
async function attemptUpload() {
|
| 401 |
+
try {
|
| 402 |
+
// Record upload start time
|
| 403 |
+
const startTime = new Date().getTime();
|
| 404 |
+
|
| 405 |
+
const response = await fetch('/upload', {
|
| 406 |
+
method: 'POST',
|
| 407 |
+
body: formData
|
| 408 |
+
});
|
| 409 |
+
|
| 410 |
+
// Calculate upload time
|
| 411 |
+
const uploadTime = (new Date().getTime() - startTime) / 1000;
|
| 412 |
+
console.log(`File ${fileName} upload request took: ${uploadTime} seconds`);
|
| 413 |
+
|
| 414 |
+
const data = await response.json();
|
| 415 |
+
|
| 416 |
+
if (!response.ok) {
|
| 417 |
+
throw new Error(`Upload failed: ${response.status} - ${data.error || 'Unknown error'}`);
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
if (data.success) {
|
| 421 |
+
console.log(`File upload successful: ${fileName}`);
|
| 422 |
+
console.log(`Server response:`, data);
|
| 423 |
+
|
| 424 |
+
// Show success message
|
| 425 |
+
updateFileList(fileName, 'Uploaded');
|
| 426 |
+
|
| 427 |
+
// Add to uploaded files list, ensure no duplicates
|
| 428 |
+
const fileIndex = uploadedFiles.findIndex(f => f.filename === data.filename);
|
| 429 |
+
if (fileIndex >= 0) {
|
| 430 |
+
// If already exists, update information
|
| 431 |
+
uploadedFiles[fileIndex] = {
|
| 432 |
+
filename: data.filename,
|
| 433 |
+
summary: data.summary,
|
| 434 |
+
tags: data.tags || [],
|
| 435 |
+
uploadTime: new Date().toISOString()
|
| 436 |
+
};
|
| 437 |
+
} else {
|
| 438 |
+
// Add new file
|
| 439 |
+
uploadedFiles.push({
|
| 440 |
+
filename: data.filename,
|
| 441 |
+
summary: data.summary,
|
| 442 |
+
tags: data.tags || [],
|
| 443 |
+
uploadTime: new Date().toISOString()
|
| 444 |
+
});
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// Update summary area and document tags area
|
| 448 |
+
updateSummaryArea();
|
| 449 |
+
updateDocumentTags();
|
| 450 |
+
setStatusMessage('File uploaded successfully', 'success');
|
| 451 |
+
|
| 452 |
+
// Add a prompt to the chat area
|
| 453 |
+
const promptMsg = document.createElement('div');
|
| 454 |
+
promptMsg.className = 'message bot-message';
|
| 455 |
+
promptMsg.innerHTML = `<div class="message-content">I've received your uploaded file "${fileName}". The file summary and tags have been updated. You can continue uploading more files, or ask questions about these files.</div>`;
|
| 456 |
+
chatHistory.appendChild(promptMsg);
|
| 457 |
+
scrollToBottom();
|
| 458 |
+
|
| 459 |
+
// 获取最新的知识文件夹结构
|
| 460 |
+
setTimeout(() => {
|
| 461 |
+
// 通过API请求获取更新的文件夹结构并更新DOM
|
| 462 |
+
fetch('/api/knowledge-map')
|
| 463 |
+
.then(response => response.json())
|
| 464 |
+
.then(data => {
|
| 465 |
+
console.log('Knowledge map refreshed without page reload');
|
| 466 |
+
// 通过自定义事件通知知识文件夹结构已更新
|
| 467 |
+
const event = new CustomEvent('knowledge-folders-updated', { detail: data });
|
| 468 |
+
document.dispatchEvent(event);
|
| 469 |
+
})
|
| 470 |
+
.catch(error => {
|
| 471 |
+
console.error('Error refreshing knowledge map:', error);
|
| 472 |
+
});
|
| 473 |
+
}, 500);
|
| 474 |
+
} else {
|
| 475 |
+
throw new Error(data.error || 'Upload failed, but server did not provide a specific reason');
|
| 476 |
+
}
|
| 477 |
+
} catch (error) {
|
| 478 |
+
console.error(`Upload error (${fileName}):`, error);
|
| 479 |
+
|
| 480 |
+
if (retryCount < maxRetries) {
|
| 481 |
+
retryCount++;
|
| 482 |
+
console.log(`File ${fileName} upload failed, attempting retry ${retryCount}`);
|
| 483 |
+
setStatusMessage(`Upload failed, retrying (${retryCount}/${maxRetries})...`, 'error');
|
| 484 |
+
updateFileList(fileName, `Retrying (${retryCount}/${maxRetries})...`);
|
| 485 |
+
// Wait one second before retrying
|
| 486 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
| 487 |
+
return attemptUpload();
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
// Exceeded retry count, show detailed error
|
| 491 |
+
console.log(`File ${fileName} upload failed, maximum retry count reached`);
|
| 492 |
+
setStatusMessage(`Upload error: ${error.message}`, 'error');
|
| 493 |
+
updateFileList(fileName, 'Upload failed');
|
| 494 |
+
|
| 495 |
+
// Add an error message to the chat
|
| 496 |
+
const errorMsg = document.createElement('div');
|
| 497 |
+
errorMsg.className = 'message bot-message error-message';
|
| 498 |
+
errorMsg.innerHTML = `<div class="message-content">File "${fileName}" upload failed: ${error.message}<br>
|
| 499 |
+
Please ensure:<br>
|
| 500 |
+
- The file type is PDF or TXT<br>
|
| 501 |
+
- The file is not encrypted<br>
|
| 502 |
+
- The file size is appropriate<br>
|
| 503 |
+
You can try uploading again or use a different file.</div>`;
|
| 504 |
+
chatHistory.appendChild(errorMsg);
|
| 505 |
+
scrollToBottom();
|
| 506 |
+
}
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
// Start upload attempt
|
| 510 |
+
await attemptUpload();
|
| 511 |
+
}
|
| 512 |
+
});
|
static/js/knowledge-map.js
ADDED
|
@@ -0,0 +1,1140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
// Get DOM elements
|
| 3 |
+
const mapContainer = document.getElementById('knowledge-map-container');
|
| 4 |
+
const mapLayout = document.getElementById('map-layout');
|
| 5 |
+
const mapDepth = document.getElementById('map-depth');
|
| 6 |
+
const refreshBtn = document.getElementById('refresh-map');
|
| 7 |
+
const nodeTitle = document.getElementById('node-title');
|
| 8 |
+
const nodeDetails = document.getElementById('node-details');
|
| 9 |
+
// Add view toggle button reference
|
| 10 |
+
const viewToggleBtn = document.createElement('button');
|
| 11 |
+
viewToggleBtn.id = 'view-toggle-btn';
|
| 12 |
+
viewToggleBtn.className = 'map-control-btn';
|
| 13 |
+
viewToggleBtn.innerHTML = '<i class="fas fa-exchange-alt"></i> Switch View';
|
| 14 |
+
|
| 15 |
+
// Add to control group
|
| 16 |
+
const controlsContainer = document.querySelector('.map-controls');
|
| 17 |
+
if (controlsContainer) {
|
| 18 |
+
const viewToggleGroup = document.createElement('div');
|
| 19 |
+
viewToggleGroup.className = 'control-group';
|
| 20 |
+
viewToggleGroup.appendChild(viewToggleBtn);
|
| 21 |
+
controlsContainer.appendChild(viewToggleGroup);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// View mode: tag->doc (default) or doc->tag
|
| 25 |
+
let viewMode = 'tag-doc';
|
| 26 |
+
|
| 27 |
+
// Initialize ECharts instance
|
| 28 |
+
const myChart = echarts.init(mapContainer, null, {
|
| 29 |
+
renderer: 'canvas'
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
// Reset chart size on window resize
|
| 33 |
+
window.addEventListener('resize', () => {
|
| 34 |
+
myChart.resize();
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
// Chart configuration options
|
| 38 |
+
let option = {
|
| 39 |
+
tooltip: {
|
| 40 |
+
trigger: 'item',
|
| 41 |
+
formatter: '{b}: {c}'
|
| 42 |
+
},
|
| 43 |
+
series: [{
|
| 44 |
+
type: 'tree',
|
| 45 |
+
data: [],
|
| 46 |
+
top: '10%',
|
| 47 |
+
left: '5%',
|
| 48 |
+
bottom: '10%',
|
| 49 |
+
right: '15%',
|
| 50 |
+
symbolSize: 10,
|
| 51 |
+
label: {
|
| 52 |
+
position: 'left',
|
| 53 |
+
verticalAlign: 'middle',
|
| 54 |
+
align: 'right',
|
| 55 |
+
fontSize: 14
|
| 56 |
+
},
|
| 57 |
+
leaves: {
|
| 58 |
+
label: {
|
| 59 |
+
position: 'right',
|
| 60 |
+
verticalAlign: 'middle',
|
| 61 |
+
align: 'left'
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
emphasis: {
|
| 65 |
+
focus: 'descendant'
|
| 66 |
+
},
|
| 67 |
+
expandAndCollapse: true,
|
| 68 |
+
animationDuration: 550,
|
| 69 |
+
animationDurationUpdate: 750,
|
| 70 |
+
// Add drag functionality
|
| 71 |
+
roam: true, // Allow zooming and panning
|
| 72 |
+
// Set styles for different level nodes
|
| 73 |
+
itemStyle: {
|
| 74 |
+
color: function(params) {
|
| 75 |
+
// Set different colors based on node depth
|
| 76 |
+
const depth = params.treePathInfo ? params.treePathInfo.length - 1 : 0;
|
| 77 |
+
// Define different level colors
|
| 78 |
+
const colors = ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f'];
|
| 79 |
+
return colors[Math.min(depth, colors.length - 1)];
|
| 80 |
+
}
|
| 81 |
+
},
|
| 82 |
+
// Set styles for different level labels
|
| 83 |
+
levels: [
|
| 84 |
+
{ // Root node
|
| 85 |
+
itemStyle: {
|
| 86 |
+
color: '#4e79a7',
|
| 87 |
+
borderWidth: 0
|
| 88 |
+
},
|
| 89 |
+
label: {
|
| 90 |
+
fontSize: 18,
|
| 91 |
+
fontWeight: 'bold'
|
| 92 |
+
}
|
| 93 |
+
},
|
| 94 |
+
{ // First level node
|
| 95 |
+
itemStyle: {
|
| 96 |
+
color: '#f28e2c',
|
| 97 |
+
borderWidth: 1
|
| 98 |
+
},
|
| 99 |
+
label: {
|
| 100 |
+
fontSize: 16,
|
| 101 |
+
fontWeight: 'bold'
|
| 102 |
+
}
|
| 103 |
+
},
|
| 104 |
+
{ // Second level node
|
| 105 |
+
itemStyle: {
|
| 106 |
+
color: '#e15759'
|
| 107 |
+
},
|
| 108 |
+
label: {
|
| 109 |
+
fontSize: 14
|
| 110 |
+
}
|
| 111 |
+
},
|
| 112 |
+
{ // Leaf node (document)
|
| 113 |
+
itemStyle: {
|
| 114 |
+
color: '#76b7b2'
|
| 115 |
+
},
|
| 116 |
+
label: {
|
| 117 |
+
fontSize: 12
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
]
|
| 121 |
+
}]
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
// Set layout type
|
| 125 |
+
function setLayout(layout) {
|
| 126 |
+
if (layout === 'tree') {
|
| 127 |
+
option.series[0].type = 'tree';
|
| 128 |
+
option.series[0].layout = 'orthogonal';
|
| 129 |
+
option.series[0].orient = 'LR';
|
| 130 |
+
// Restore default series settings
|
| 131 |
+
option.series = [{
|
| 132 |
+
type: 'tree',
|
| 133 |
+
data: [],
|
| 134 |
+
top: '10%',
|
| 135 |
+
left: '5%',
|
| 136 |
+
bottom: '10%',
|
| 137 |
+
right: '15%',
|
| 138 |
+
symbolSize: 10,
|
| 139 |
+
label: {
|
| 140 |
+
position: 'left',
|
| 141 |
+
verticalAlign: 'middle',
|
| 142 |
+
align: 'right',
|
| 143 |
+
fontSize: 14,
|
| 144 |
+
color: '#000' // Default text color
|
| 145 |
+
},
|
| 146 |
+
leaves: {
|
| 147 |
+
label: {
|
| 148 |
+
position: 'right',
|
| 149 |
+
verticalAlign: 'middle',
|
| 150 |
+
align: 'left',
|
| 151 |
+
color: '#000' // Leaf node text color
|
| 152 |
+
}
|
| 153 |
+
},
|
| 154 |
+
emphasis: {
|
| 155 |
+
focus: 'descendant'
|
| 156 |
+
},
|
| 157 |
+
expandAndCollapse: true,
|
| 158 |
+
animationDuration: 550,
|
| 159 |
+
animationDurationUpdate: 750,
|
| 160 |
+
roam: true,
|
| 161 |
+
// Explicitly set node styles
|
| 162 |
+
itemStyle: {
|
| 163 |
+
color: function(params) {
|
| 164 |
+
// Set different colors based on node depth
|
| 165 |
+
const depth = params.treePathInfo ? params.treePathInfo.length - 1 : 0;
|
| 166 |
+
const colors = ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f'];
|
| 167 |
+
return colors[Math.min(depth, colors.length - 1)];
|
| 168 |
+
},
|
| 169 |
+
borderWidth: 1,
|
| 170 |
+
borderColor: '#999'
|
| 171 |
+
},
|
| 172 |
+
lineStyle: {
|
| 173 |
+
color: '#999',
|
| 174 |
+
width: 1
|
| 175 |
+
}
|
| 176 |
+
}];
|
| 177 |
+
} else if (layout === 'force') {
|
| 178 |
+
option.series[0].type = 'graph';
|
| 179 |
+
option.series[0].layout = 'force';
|
| 180 |
+
option.series[0].force = {
|
| 181 |
+
repulsion: 100,
|
| 182 |
+
edgeLength: 50
|
| 183 |
+
};
|
| 184 |
+
} else if (layout === 'radial') {
|
| 185 |
+
option.series[0].type = 'tree';
|
| 186 |
+
option.series[0].layout = 'radial';
|
| 187 |
+
option.series[0].orient = undefined;
|
| 188 |
+
} else if (layout === 'wordcloud') {
|
| 189 |
+
// Real word cloud mode - Use wordcloud chart type
|
| 190 |
+
console.log("Switch to word cloud mode");
|
| 191 |
+
|
| 192 |
+
// Completely replace series configuration with word cloud chart
|
| 193 |
+
option.series = [{
|
| 194 |
+
type: 'wordCloud',
|
| 195 |
+
// Word cloud shape can be rectangle or circle
|
| 196 |
+
shape: 'circle',
|
| 197 |
+
// Word cloud size
|
| 198 |
+
left: 'center',
|
| 199 |
+
top: 'center',
|
| 200 |
+
width: '70%',
|
| 201 |
+
height: '70%',
|
| 202 |
+
// Word cloud background color, set to obvious white
|
| 203 |
+
backgroundColor: '#ffffff',
|
| 204 |
+
// Font size range, ensure words visible
|
| 205 |
+
sizeRange: [24, 80],
|
| 206 |
+
// Word rotation angle range - Reduce rotation for easier reading
|
| 207 |
+
rotationRange: [0, 0], // Set to 0, no rotation
|
| 208 |
+
// Grid size, for layout optimization, reduce this value for denser words
|
| 209 |
+
gridSize: 6,
|
| 210 |
+
// Word cloud text style
|
| 211 |
+
textStyle: {
|
| 212 |
+
fontFamily: 'Arial, sans-serif',
|
| 213 |
+
fontWeight: 'bold',
|
| 214 |
+
// Fixed text color, no random color
|
| 215 |
+
color: '#333'
|
| 216 |
+
},
|
| 217 |
+
// Global zoom scale
|
| 218 |
+
layoutAnimation: false,
|
| 219 |
+
// Mouse hover effect
|
| 220 |
+
emphasis: {
|
| 221 |
+
focus: 'self',
|
| 222 |
+
textStyle: {
|
| 223 |
+
shadowBlur: 10,
|
| 224 |
+
shadowColor: '#333'
|
| 225 |
+
}
|
| 226 |
+
},
|
| 227 |
+
// Must attribute, set drawOutOfBound to false to prevent text from being clipped
|
| 228 |
+
drawOutOfBound: false,
|
| 229 |
+
// Whether to enable fuzzy antialiasing processing
|
| 230 |
+
textShadowBlur: 2,
|
| 231 |
+
textShadowColor: '#fff',
|
| 232 |
+
// Data will be filled in buildWordcloudData function
|
| 233 |
+
data: []
|
| 234 |
+
}];
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
// Set level depth
|
| 239 |
+
function setDepth(depth) {
|
| 240 |
+
if (depth === 'all') {
|
| 241 |
+
option.series[0].expandAndCollapse = true;
|
| 242 |
+
option.series[0].initialTreeDepth = -1;
|
| 243 |
+
} else {
|
| 244 |
+
option.series[0].expandAndCollapse = true;
|
| 245 |
+
option.series[0].initialTreeDepth = parseInt(depth);
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
// Get knowledge map data
|
| 250 |
+
async function fetchKnowledgeMap() {
|
| 251 |
+
try {
|
| 252 |
+
const response = await fetch('/api/knowledge-map');
|
| 253 |
+
|
| 254 |
+
if (!response.ok) {
|
| 255 |
+
throw new Error('Failed to get knowledge map data');
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
const data = await response.json();
|
| 259 |
+
return data;
|
| 260 |
+
} catch (error) {
|
| 261 |
+
console.error('Failed to get knowledge map data:', error);
|
| 262 |
+
return {
|
| 263 |
+
nodes: [],
|
| 264 |
+
links: [],
|
| 265 |
+
error: error.message
|
| 266 |
+
};
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
// Truncate long filename
|
| 271 |
+
function truncateFilename(filename, maxLength = 25) {
|
| 272 |
+
if (!filename || filename.length <= maxLength) {
|
| 273 |
+
return filename;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
const ext = filename.lastIndexOf('.') > 0 ? filename.slice(filename.lastIndexOf('.')) : '';
|
| 277 |
+
const nameWithoutExt = filename.slice(0, filename.lastIndexOf('.') > 0 ? filename.lastIndexOf('.') : filename.length);
|
| 278 |
+
|
| 279 |
+
// Keep filename prefix, add ellipsis, then keep extension
|
| 280 |
+
return nameWithoutExt.slice(0, maxLength - 3 - ext.length) + '...' + ext;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
// Build knowledge map tree data - Tag to document structure (default view)
|
| 284 |
+
function buildTagToDocTree(data) {
|
| 285 |
+
// Process backend returned data, build into tree structure
|
| 286 |
+
if (data.error) {
|
| 287 |
+
showError(data.error);
|
| 288 |
+
return [];
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
// If empty data, show prompt information
|
| 292 |
+
if (!data.documents || data.documents.length === 0) {
|
| 293 |
+
showError('No usable knowledge map data, please upload documents first');
|
| 294 |
+
return [];
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
// Build tree structure
|
| 298 |
+
const rootNode = {
|
| 299 |
+
name: data.centralTopic || 'Knowledge Center',
|
| 300 |
+
value: data.documents.length,
|
| 301 |
+
children: []
|
| 302 |
+
};
|
| 303 |
+
|
| 304 |
+
// Group processing
|
| 305 |
+
const tagGroups = {};
|
| 306 |
+
|
| 307 |
+
// First find all tags common in all documents as the first layer
|
| 308 |
+
const allTags = new Set();
|
| 309 |
+
data.documents.forEach(doc => {
|
| 310 |
+
if (doc.tags && doc.tags.length > 0) {
|
| 311 |
+
doc.tags.forEach(tag => allTags.add(tag));
|
| 312 |
+
}
|
| 313 |
+
});
|
| 314 |
+
|
| 315 |
+
// Generate tag hierarchy structure (simulate mind map hierarchy)
|
| 316 |
+
// Use tag co-occurrence frequency as hierarchy building basis
|
| 317 |
+
const tagRelations = {};
|
| 318 |
+
|
| 319 |
+
// Calculate tag co-occurrence frequency
|
| 320 |
+
data.documents.forEach(doc => {
|
| 321 |
+
if (doc.tags && doc.tags.length > 0) {
|
| 322 |
+
for (let i = 0; i < doc.tags.length; i++) {
|
| 323 |
+
for (let j = i + 1; j < doc.tags.length; j++) {
|
| 324 |
+
const tag1 = doc.tags[i];
|
| 325 |
+
const tag2 = doc.tags[j];
|
| 326 |
+
const key = `${tag1}:${tag2}`;
|
| 327 |
+
const reverseKey = `${tag2}:${tag1}`;
|
| 328 |
+
|
| 329 |
+
if (tagRelations[key]) {
|
| 330 |
+
tagRelations[key]++;
|
| 331 |
+
} else if (tagRelations[reverseKey]) {
|
| 332 |
+
tagRelations[reverseKey]++;
|
| 333 |
+
} else {
|
| 334 |
+
tagRelations[key] = 1;
|
| 335 |
+
}
|
| 336 |
+
}
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
});
|
| 340 |
+
|
| 341 |
+
// Find main tags based on co-occurrence frequency
|
| 342 |
+
let maxCoOccurrence = 0;
|
| 343 |
+
let primaryTags = [];
|
| 344 |
+
|
| 345 |
+
Object.keys(tagRelations).forEach(key => {
|
| 346 |
+
if (tagRelations[key] > maxCoOccurrence) {
|
| 347 |
+
maxCoOccurrence = tagRelations[key];
|
| 348 |
+
const [tag1, tag2] = key.split(':');
|
| 349 |
+
primaryTags = [tag1, tag2];
|
| 350 |
+
}
|
| 351 |
+
});
|
| 352 |
+
|
| 353 |
+
// If no related tags are found, use the most common tag
|
| 354 |
+
if (primaryTags.length === 0) {
|
| 355 |
+
const tagFrequency = {};
|
| 356 |
+
data.documents.forEach(doc => {
|
| 357 |
+
if (doc.tags && doc.tags.length > 0) {
|
| 358 |
+
doc.tags.forEach(tag => {
|
| 359 |
+
tagFrequency[tag] = (tagFrequency[tag] || 0) + 1;
|
| 360 |
+
});
|
| 361 |
+
}
|
| 362 |
+
});
|
| 363 |
+
|
| 364 |
+
// Find the tag with the highest frequency
|
| 365 |
+
let maxFreq = 0;
|
| 366 |
+
let mostFrequentTag = '';
|
| 367 |
+
|
| 368 |
+
Object.keys(tagFrequency).forEach(tag => {
|
| 369 |
+
if (tagFrequency[tag] > maxFreq) {
|
| 370 |
+
maxFreq = tagFrequency[tag];
|
| 371 |
+
mostFrequentTag = tag;
|
| 372 |
+
}
|
| 373 |
+
});
|
| 374 |
+
|
| 375 |
+
primaryTags = [mostFrequentTag];
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
// Use main tags as first level nodes
|
| 379 |
+
primaryTags.forEach(tag => {
|
| 380 |
+
const tagNode = {
|
| 381 |
+
name: tag,
|
| 382 |
+
value: 0,
|
| 383 |
+
children: []
|
| 384 |
+
};
|
| 385 |
+
|
| 386 |
+
// Find documents containing the tag
|
| 387 |
+
const docsWithTag = data.documents.filter(doc =>
|
| 388 |
+
doc.tags && doc.tags.includes(tag)
|
| 389 |
+
);
|
| 390 |
+
|
| 391 |
+
tagNode.value = docsWithTag.length;
|
| 392 |
+
|
| 393 |
+
// Add child tag nodes to the tag node
|
| 394 |
+
const secondaryTags = new Set();
|
| 395 |
+
docsWithTag.forEach(doc => {
|
| 396 |
+
if (doc.tags) {
|
| 397 |
+
doc.tags.forEach(t => {
|
| 398 |
+
if (t !== tag && !primaryTags.includes(t)) {
|
| 399 |
+
secondaryTags.add(t);
|
| 400 |
+
}
|
| 401 |
+
});
|
| 402 |
+
}
|
| 403 |
+
});
|
| 404 |
+
|
| 405 |
+
// Add secondary tag nodes
|
| 406 |
+
secondaryTags.forEach(secondaryTag => {
|
| 407 |
+
const secondaryNode = {
|
| 408 |
+
name: secondaryTag,
|
| 409 |
+
value: 0,
|
| 410 |
+
children: []
|
| 411 |
+
};
|
| 412 |
+
|
| 413 |
+
// Find documents containing both first and second level tags
|
| 414 |
+
const docsWithBothTags = docsWithTag.filter(doc =>
|
| 415 |
+
doc.tags && doc.tags.includes(secondaryTag)
|
| 416 |
+
);
|
| 417 |
+
|
| 418 |
+
secondaryNode.value = docsWithBothTags.length;
|
| 419 |
+
|
| 420 |
+
// Add document nodes to secondary tag
|
| 421 |
+
docsWithBothTags.forEach(doc => {
|
| 422 |
+
secondaryNode.children.push({
|
| 423 |
+
name: truncateFilename(doc.filename), // Truncate long filename
|
| 424 |
+
fullName: doc.filename, // Save full filename
|
| 425 |
+
value: 1,
|
| 426 |
+
document: doc,
|
| 427 |
+
nodeType: 'document'
|
| 428 |
+
});
|
| 429 |
+
});
|
| 430 |
+
|
| 431 |
+
if (secondaryNode.children.length > 0) {
|
| 432 |
+
tagNode.children.push(secondaryNode);
|
| 433 |
+
}
|
| 434 |
+
});
|
| 435 |
+
|
| 436 |
+
// Directly add documents without secondary tags to first level tag
|
| 437 |
+
const docsWithOnlyPrimaryTag = docsWithTag.filter(doc =>
|
| 438 |
+
doc.tags && doc.tags.filter(t => !primaryTags.includes(t) && secondaryTags.has(t)).length === 0
|
| 439 |
+
);
|
| 440 |
+
|
| 441 |
+
docsWithOnlyPrimaryTag.forEach(doc => {
|
| 442 |
+
tagNode.children.push({
|
| 443 |
+
name: truncateFilename(doc.filename), // Truncate long filename
|
| 444 |
+
fullName: doc.filename, // Save full filename
|
| 445 |
+
value: 1,
|
| 446 |
+
document: doc,
|
| 447 |
+
nodeType: 'document'
|
| 448 |
+
});
|
| 449 |
+
});
|
| 450 |
+
|
| 451 |
+
if (tagNode.children.length > 0) {
|
| 452 |
+
rootNode.children.push(tagNode);
|
| 453 |
+
}
|
| 454 |
+
});
|
| 455 |
+
|
| 456 |
+
// Add documents without main tags
|
| 457 |
+
const docsWithoutPrimaryTags = data.documents.filter(doc =>
|
| 458 |
+
!doc.tags || !doc.tags.some(tag => primaryTags.includes(tag))
|
| 459 |
+
);
|
| 460 |
+
|
| 461 |
+
if (docsWithoutPrimaryTags.length > 0) {
|
| 462 |
+
const otherNode = {
|
| 463 |
+
name: 'Other Documents',
|
| 464 |
+
value: docsWithoutPrimaryTags.length,
|
| 465 |
+
children: []
|
| 466 |
+
};
|
| 467 |
+
|
| 468 |
+
docsWithoutPrimaryTags.forEach(doc => {
|
| 469 |
+
otherNode.children.push({
|
| 470 |
+
name: truncateFilename(doc.filename), // Truncate long filename
|
| 471 |
+
fullName: doc.filename, // Save full filename
|
| 472 |
+
value: 1,
|
| 473 |
+
document: doc,
|
| 474 |
+
nodeType: 'document'
|
| 475 |
+
});
|
| 476 |
+
});
|
| 477 |
+
|
| 478 |
+
rootNode.children.push(otherNode);
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
return [rootNode];
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
// Build document to tag tree structure (reverse view)
|
| 485 |
+
function buildDocToTagTree(data) {
|
| 486 |
+
// Process backend returned data, build into tree structure
|
| 487 |
+
if (data.error) {
|
| 488 |
+
showError(data.error);
|
| 489 |
+
return [];
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
// If empty data, show prompt information
|
| 493 |
+
if (!data.documents || data.documents.length === 0) {
|
| 494 |
+
showError('No usable knowledge map data, please upload documents first');
|
| 495 |
+
return [];
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
// Build tree structure
|
| 499 |
+
const rootNode = {
|
| 500 |
+
name: data.centralTopic || 'Knowledge Center',
|
| 501 |
+
value: data.documents.length,
|
| 502 |
+
children: []
|
| 503 |
+
};
|
| 504 |
+
|
| 505 |
+
// Group documents by document type or topic
|
| 506 |
+
const docCategories = {};
|
| 507 |
+
|
| 508 |
+
// Find all documents, extract possible classification information
|
| 509 |
+
data.documents.forEach(doc => {
|
| 510 |
+
// Try to infer category from filename
|
| 511 |
+
let category = 'Document Set';
|
| 512 |
+
|
| 513 |
+
// Check file extension
|
| 514 |
+
if (doc.filename) {
|
| 515 |
+
const fileExt = doc.filename.split('.').pop().toLowerCase();
|
| 516 |
+
if (['pdf', 'docx', 'doc'].includes(fileExt)) {
|
| 517 |
+
category = 'Text Document';
|
| 518 |
+
} else if (['ppt', 'pptx'].includes(fileExt)) {
|
| 519 |
+
category = 'Presentation';
|
| 520 |
+
} else if (['xlsx', 'xls', 'csv'].includes(fileExt)) {
|
| 521 |
+
category = 'Data File';
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
// Also try to infer better classification from tags
|
| 525 |
+
if (doc.tags && doc.tags.length > 0) {
|
| 526 |
+
// Find some possible tags that might represent topic
|
| 527 |
+
const possibleCategories = ['Education', 'Cognition', 'Learning', 'Media', 'Design', 'Evaluation'];
|
| 528 |
+
for (const pc of possibleCategories) {
|
| 529 |
+
const matchingTag = doc.tags.find(tag => tag.includes(pc));
|
| 530 |
+
if (matchingTag) {
|
| 531 |
+
category = matchingTag;
|
| 532 |
+
break;
|
| 533 |
+
}
|
| 534 |
+
}
|
| 535 |
+
}
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
// Ensure category exists
|
| 539 |
+
if (!docCategories[category]) {
|
| 540 |
+
docCategories[category] = [];
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
// Add document to category
|
| 544 |
+
docCategories[category].push(doc);
|
| 545 |
+
});
|
| 546 |
+
|
| 547 |
+
// Create a tree node for each category
|
| 548 |
+
Object.keys(docCategories).forEach(category => {
|
| 549 |
+
const categoryNode = {
|
| 550 |
+
name: category,
|
| 551 |
+
value: docCategories[category].length,
|
| 552 |
+
children: []
|
| 553 |
+
};
|
| 554 |
+
|
| 555 |
+
// Create a node for each document in the category
|
| 556 |
+
docCategories[category].forEach(doc => {
|
| 557 |
+
const docNode = {
|
| 558 |
+
name: truncateFilename(doc.filename),
|
| 559 |
+
fullName: doc.filename,
|
| 560 |
+
value: doc.tags ? doc.tags.length : 0,
|
| 561 |
+
document: doc,
|
| 562 |
+
nodeType: 'document',
|
| 563 |
+
children: []
|
| 564 |
+
};
|
| 565 |
+
|
| 566 |
+
// Add tags as child nodes for each document
|
| 567 |
+
if (doc.tags && doc.tags.length > 0) {
|
| 568 |
+
doc.tags.forEach(tag => {
|
| 569 |
+
docNode.children.push({
|
| 570 |
+
name: tag,
|
| 571 |
+
value: 1,
|
| 572 |
+
nodeType: 'tag',
|
| 573 |
+
tagName: tag
|
| 574 |
+
});
|
| 575 |
+
});
|
| 576 |
+
} else {
|
| 577 |
+
// If no tags, add a "No Tags" node
|
| 578 |
+
docNode.children.push({
|
| 579 |
+
name: 'No Tags',
|
| 580 |
+
value: 1,
|
| 581 |
+
nodeType: 'tag',
|
| 582 |
+
tagName: 'No Tags'
|
| 583 |
+
});
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
categoryNode.children.push(docNode);
|
| 587 |
+
});
|
| 588 |
+
|
| 589 |
+
rootNode.children.push(categoryNode);
|
| 590 |
+
});
|
| 591 |
+
|
| 592 |
+
return [rootNode];
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
// Choose suitable tree building function
|
| 596 |
+
function buildKnowledgeTree(data) {
|
| 597 |
+
// Return tree corresponding to current view mode
|
| 598 |
+
if (viewMode === 'tag-doc') {
|
| 599 |
+
return buildTagToDocTree(data);
|
| 600 |
+
} else {
|
| 601 |
+
return buildDocToTagTree(data);
|
| 602 |
+
}
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
// Show error message
|
| 606 |
+
function showError(message) {
|
| 607 |
+
nodeTitle.textContent = 'Error';
|
| 608 |
+
nodeDetails.innerHTML = `<p class="error-message">${message}</p>`;
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
// Show node details
|
| 612 |
+
function showNodeDetails(params) {
|
| 613 |
+
const data = params.data;
|
| 614 |
+
const isWordcloudMode = mapLayout.value === 'wordcloud';
|
| 615 |
+
|
| 616 |
+
if (data) {
|
| 617 |
+
// Use saved full filename or display name
|
| 618 |
+
nodeTitle.textContent = data.fullName || data.name;
|
| 619 |
+
|
| 620 |
+
if (data.nodeType === 'document' || data.document) {
|
| 621 |
+
// Show document information
|
| 622 |
+
const doc = data.document;
|
| 623 |
+
let html = `<p><strong>Filename:</strong> ${doc.filename}</p>`;
|
| 624 |
+
|
| 625 |
+
if (doc.summary) {
|
| 626 |
+
html += `<p><strong>Summary:</strong> ${doc.summary}</p>`;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
if (doc.tags && doc.tags.length > 0) {
|
| 630 |
+
html += `<p><strong>Tags:</strong></p><div class="tag-list">`;
|
| 631 |
+
doc.tags.forEach(tag => {
|
| 632 |
+
html += `<span class="tag">${tag}</span>`;
|
| 633 |
+
});
|
| 634 |
+
html += `</div>`;
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
html += `<div class="document-action-buttons">
|
| 638 |
+
<a href="#" class="document-link" data-file="${doc.filename}">
|
| 639 |
+
<i class="fas fa-external-link-alt"></i> View Document
|
| 640 |
+
</a>
|
| 641 |
+
<a href="#" class="wordcloud-link" data-file="${doc.filename}">
|
| 642 |
+
<i class="fas fa-cloud"></i> View Word Cloud
|
| 643 |
+
</a>`;
|
| 644 |
+
|
| 645 |
+
// If in wordcloud mode, add direct wordcloud generation button
|
| 646 |
+
if (isWordcloudMode) {
|
| 647 |
+
html += `<a href="#" class="generate-cloud-link" data-file="${doc.filename}">
|
| 648 |
+
<i class="fas fa-magic"></i> Generate Word Cloud
|
| 649 |
+
</a>`;
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
html += `</div>`;
|
| 653 |
+
|
| 654 |
+
nodeDetails.innerHTML = html;
|
| 655 |
+
} else if (data.nodeType === 'tag') {
|
| 656 |
+
// Show tag information
|
| 657 |
+
nodeDetails.innerHTML = `
|
| 658 |
+
<p>This is a tag node</p>
|
| 659 |
+
<p><strong>Tag name:</strong> ${data.tagName || data.name}</p>
|
| 660 |
+
<p>Click on parent node to view document details</p>
|
| 661 |
+
`;
|
| 662 |
+
} else {
|
| 663 |
+
// Show tag node or category node information
|
| 664 |
+
nodeDetails.innerHTML = `
|
| 665 |
+
<p>This is a ${data.children ? 'category node' : 'node'}</p>
|
| 666 |
+
<p>Contains ${data.value} items</p>
|
| 667 |
+
`;
|
| 668 |
+
}
|
| 669 |
+
} else {
|
| 670 |
+
nodeTitle.textContent = 'No node selected';
|
| 671 |
+
nodeDetails.innerHTML = `<p class="placeholder-text">Click on a node in the map to view details</p>`;
|
| 672 |
+
}
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
// Add chart controls
|
| 676 |
+
function addChartControls() {
|
| 677 |
+
// Add zoom controller
|
| 678 |
+
const zoomController = document.createElement('div');
|
| 679 |
+
zoomController.className = 'zoom-controllers';
|
| 680 |
+
zoomController.innerHTML = `
|
| 681 |
+
<button class="zoom-btn zoom-in" title="Zoom In"><i class="fas fa-search-plus"></i></button>
|
| 682 |
+
<button class="zoom-btn zoom-out" title="Zoom Out"><i class="fas fa-search-minus"></i></button>
|
| 683 |
+
<button class="zoom-btn zoom-reset" title="Reset View"><i class="fas fa-redo-alt"></i></button>
|
| 684 |
+
`;
|
| 685 |
+
mapContainer.parentNode.insertBefore(zoomController, mapContainer);
|
| 686 |
+
|
| 687 |
+
// Bind zoom events
|
| 688 |
+
document.querySelector('.zoom-in').addEventListener('click', () => {
|
| 689 |
+
myChart.dispatchAction({
|
| 690 |
+
type: 'dataZoom',
|
| 691 |
+
start: 0,
|
| 692 |
+
end: 50
|
| 693 |
+
});
|
| 694 |
+
});
|
| 695 |
+
|
| 696 |
+
document.querySelector('.zoom-out').addEventListener('click', () => {
|
| 697 |
+
myChart.dispatchAction({
|
| 698 |
+
type: 'dataZoom',
|
| 699 |
+
start: 0,
|
| 700 |
+
end: 100
|
| 701 |
+
});
|
| 702 |
+
});
|
| 703 |
+
|
| 704 |
+
document.querySelector('.zoom-reset').addEventListener('click', () => {
|
| 705 |
+
myChart.dispatchAction({
|
| 706 |
+
type: 'restore'
|
| 707 |
+
});
|
| 708 |
+
});
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
// Refresh knowledge map
|
| 712 |
+
async function refreshKnowledgeMap() {
|
| 713 |
+
const layout = mapLayout.value;
|
| 714 |
+
const depth = mapDepth.value;
|
| 715 |
+
|
| 716 |
+
console.log("Refreshing knowledge map, layout:", layout, "depth:", depth);
|
| 717 |
+
|
| 718 |
+
// Set layout and depth
|
| 719 |
+
setLayout(layout);
|
| 720 |
+
setDepth(depth);
|
| 721 |
+
|
| 722 |
+
// Show loading state
|
| 723 |
+
myChart.showLoading({
|
| 724 |
+
text: 'Loading...',
|
| 725 |
+
color: '#4e79a7',
|
| 726 |
+
textColor: '#000',
|
| 727 |
+
maskColor: 'rgba(255, 255, 255, 0.8)',
|
| 728 |
+
zlevel: 0
|
| 729 |
+
});
|
| 730 |
+
|
| 731 |
+
try {
|
| 732 |
+
// Get data
|
| 733 |
+
const mapData = await fetchKnowledgeMap();
|
| 734 |
+
|
| 735 |
+
// Add or remove wordcloud mode CSS class
|
| 736 |
+
const container = document.querySelector('.knowledge-map-container');
|
| 737 |
+
if (layout === 'wordcloud') {
|
| 738 |
+
container.classList.add('wordcloud-mode');
|
| 739 |
+
// Clear node details, show wordcloud explanation
|
| 740 |
+
nodeTitle.textContent = 'Word Cloud View';
|
| 741 |
+
nodeDetails.innerHTML = `<p>Word cloud shows high-frequency words and tags from documents. Word size represents its importance in the document.</p>
|
| 742 |
+
<p>Click on any word to see related documents containing that word.</p>`;
|
| 743 |
+
} else {
|
| 744 |
+
container.classList.remove('wordcloud-mode');
|
| 745 |
+
nodeTitle.textContent = 'No node selected';
|
| 746 |
+
nodeDetails.innerHTML = '<p class="placeholder-text">Click on a node in the map to view details</p>';
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
// First clear previous chart instance, completely re-render
|
| 750 |
+
myChart.clear();
|
| 751 |
+
|
| 752 |
+
// Choose data building method based on different layout types
|
| 753 |
+
let chartData;
|
| 754 |
+
if (layout === 'wordcloud') {
|
| 755 |
+
console.log("Building word cloud data...");
|
| 756 |
+
chartData = buildWordcloudData(mapData);
|
| 757 |
+
console.log("Word cloud data preparation completed, data item count:", chartData.length);
|
| 758 |
+
|
| 759 |
+
// For word cloud, set a simple configuration, focus on data
|
| 760 |
+
myChart.setOption({
|
| 761 |
+
series: [{
|
| 762 |
+
type: 'wordCloud',
|
| 763 |
+
shape: 'circle',
|
| 764 |
+
left: 'center',
|
| 765 |
+
top: 'center',
|
| 766 |
+
width: '80%',
|
| 767 |
+
height: '80%',
|
| 768 |
+
right: null,
|
| 769 |
+
bottom: null,
|
| 770 |
+
sizeRange: [24, 80],
|
| 771 |
+
rotationRange: [0, 0],
|
| 772 |
+
rotationStep: 0,
|
| 773 |
+
gridSize: 8,
|
| 774 |
+
drawOutOfBound: false,
|
| 775 |
+
layoutAnimation: false,
|
| 776 |
+
textStyle: {
|
| 777 |
+
fontFamily: 'Arial, 微软雅黑, sans-serif',
|
| 778 |
+
fontWeight: 'bold',
|
| 779 |
+
color: function(params) {
|
| 780 |
+
// Use fixed color set, keep simple
|
| 781 |
+
const colors = ['#000', '#333', '#666'];
|
| 782 |
+
return colors[params.dataIndex % colors.length];
|
| 783 |
+
}
|
| 784 |
+
},
|
| 785 |
+
emphasis: {
|
| 786 |
+
textStyle: {
|
| 787 |
+
color: '#f00'
|
| 788 |
+
}
|
| 789 |
+
},
|
| 790 |
+
data: chartData
|
| 791 |
+
}]
|
| 792 |
+
}, true);
|
| 793 |
+
} else {
|
| 794 |
+
// For tree chart and other chart types, use original data building method
|
| 795 |
+
chartData = buildKnowledgeTree(mapData);
|
| 796 |
+
option.series[0].data = chartData;
|
| 797 |
+
|
| 798 |
+
// Apply configuration
|
| 799 |
+
myChart.setOption(option, true);
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
console.log("Chart updated");
|
| 803 |
+
|
| 804 |
+
// For word cloud, ensure view is fully updated
|
| 805 |
+
if (layout === 'wordcloud') {
|
| 806 |
+
setTimeout(() => {
|
| 807 |
+
console.log("Force redraw word cloud...");
|
| 808 |
+
myChart.resize();
|
| 809 |
+
}, 200);
|
| 810 |
+
}
|
| 811 |
+
} catch (error) {
|
| 812 |
+
console.error('Failed to refresh knowledge map:', error);
|
| 813 |
+
showError('Failed to refresh knowledge map: ' + error.message);
|
| 814 |
+
} finally {
|
| 815 |
+
// Hide loading state
|
| 816 |
+
myChart.hideLoading();
|
| 817 |
+
}
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
// Toggle view mode
|
| 821 |
+
function toggleViewMode() {
|
| 822 |
+
// Toggle view mode
|
| 823 |
+
viewMode = viewMode === 'tag-doc' ? 'doc-tag' : 'tag-doc';
|
| 824 |
+
|
| 825 |
+
// Update button text
|
| 826 |
+
viewToggleBtn.innerHTML = viewMode === 'tag-doc'
|
| 827 |
+
? '<i class="fas fa-exchange-alt"></i> Switch to Document-Tag View'
|
| 828 |
+
: '<i class="fas fa-exchange-alt"></i> Switch to Tag-Document View';
|
| 829 |
+
|
| 830 |
+
// Refresh knowledge map
|
| 831 |
+
refreshKnowledgeMap();
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
// Bind events
|
| 835 |
+
mapLayout.addEventListener('change', refreshKnowledgeMap);
|
| 836 |
+
mapDepth.addEventListener('change', refreshKnowledgeMap);
|
| 837 |
+
refreshBtn.addEventListener('click', refreshKnowledgeMap);
|
| 838 |
+
viewToggleBtn.addEventListener('click', toggleViewMode);
|
| 839 |
+
|
| 840 |
+
// Click node event
|
| 841 |
+
myChart.on('click', 'series', (params) => {
|
| 842 |
+
if (mapLayout.value === 'wordcloud') {
|
| 843 |
+
// Word cloud mode click processing
|
| 844 |
+
handleWordCloudClick(params);
|
| 845 |
+
} else {
|
| 846 |
+
// Other chart mode
|
| 847 |
+
showNodeDetails(params);
|
| 848 |
+
}
|
| 849 |
+
});
|
| 850 |
+
|
| 851 |
+
// Handle wordcloud click event
|
| 852 |
+
function handleWordCloudClick(params) {
|
| 853 |
+
console.log("Word cloud click event:", params);
|
| 854 |
+
|
| 855 |
+
// Ensure there is data
|
| 856 |
+
if (!params || !params.data) {
|
| 857 |
+
console.error("Click event has no valid data");
|
| 858 |
+
return;
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
const word = params.data.name;
|
| 862 |
+
const value = params.data.value;
|
| 863 |
+
|
| 864 |
+
// Set node title to clicked word
|
| 865 |
+
nodeTitle.textContent = word;
|
| 866 |
+
|
| 867 |
+
// First show a temporary message indicating loading
|
| 868 |
+
nodeDetails.innerHTML = `
|
| 869 |
+
<p><strong>Word:</strong> ${word}</p>
|
| 870 |
+
<p><strong>Frequency:</strong> ${value}</p>
|
| 871 |
+
<p><i class="fas fa-spinner fa-spin"></i> Searching for related documents...</p>
|
| 872 |
+
`;
|
| 873 |
+
|
| 874 |
+
// Find all documents containing the word
|
| 875 |
+
fetchKnowledgeMap().then(mapData => {
|
| 876 |
+
if (!mapData.documents || mapData.documents.length === 0) {
|
| 877 |
+
nodeDetails.innerHTML = `
|
| 878 |
+
<p><strong>Word:</strong> ${word}</p>
|
| 879 |
+
<p><strong>Frequency:</strong> ${value}</p>
|
| 880 |
+
<p class="placeholder-text">No documents found</p>
|
| 881 |
+
`;
|
| 882 |
+
return;
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
// Find all documents containing the word (in tags or summary)
|
| 886 |
+
const relatedDocs = mapData.documents.filter(doc => {
|
| 887 |
+
// Check tags
|
| 888 |
+
if (doc.tags && doc.tags.includes(word)) {
|
| 889 |
+
return true;
|
| 890 |
+
}
|
| 891 |
+
|
| 892 |
+
// Check filename
|
| 893 |
+
if (doc.filename && doc.filename.toLowerCase().includes(word.toLowerCase())) {
|
| 894 |
+
return true;
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
// Check summary
|
| 898 |
+
if (doc.summary && doc.summary.toLowerCase().includes(word.toLowerCase())) {
|
| 899 |
+
return true;
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
return false;
|
| 903 |
+
});
|
| 904 |
+
|
| 905 |
+
// Show related document list
|
| 906 |
+
if (relatedDocs.length > 0) {
|
| 907 |
+
let html = `
|
| 908 |
+
<p><strong>Word:</strong> ${word}</p>
|
| 909 |
+
<p><strong>Frequency:</strong> ${value}</p>
|
| 910 |
+
<p><strong>Related documents:</strong> ${relatedDocs.length}</p>
|
| 911 |
+
<div class="related-docs">
|
| 912 |
+
`;
|
| 913 |
+
|
| 914 |
+
relatedDocs.forEach(doc => {
|
| 915 |
+
html += `
|
| 916 |
+
<div class="related-doc-item">
|
| 917 |
+
<span class="doc-title">${doc.filename || 'Unnamed Document'}</span>
|
| 918 |
+
<div class="document-action-buttons">
|
| 919 |
+
<a href="/view-document?filename=${encodeURIComponent(doc.filename)}" class="document-link">
|
| 920 |
+
<i class="fas fa-external-link-alt"></i> View Document
|
| 921 |
+
</a>
|
| 922 |
+
<a href="/view-document?filename=${encodeURIComponent(doc.filename)}&show_wordcloud=true" class="wordcloud-link">
|
| 923 |
+
<i class="fas fa-cloud"></i> View Word Cloud
|
| 924 |
+
</a>
|
| 925 |
+
</div>
|
| 926 |
+
</div>
|
| 927 |
+
`;
|
| 928 |
+
});
|
| 929 |
+
|
| 930 |
+
html += `</div>`;
|
| 931 |
+
nodeDetails.innerHTML = html;
|
| 932 |
+
} else {
|
| 933 |
+
nodeDetails.innerHTML = `
|
| 934 |
+
<p><strong>Word:</strong> ${word}</p>
|
| 935 |
+
<p><strong>Frequency:</strong> ${value}</p>
|
| 936 |
+
<p class="placeholder-text">No documents containing "${word}" found</p>
|
| 937 |
+
<p>This word may have been added by default, or comes from a deleted document.</p>
|
| 938 |
+
`;
|
| 939 |
+
}
|
| 940 |
+
}).catch(error => {
|
| 941 |
+
console.error('Failed to get document data:', error);
|
| 942 |
+
nodeDetails.innerHTML = `
|
| 943 |
+
<p><strong>Word:</strong> ${word}</p>
|
| 944 |
+
<p><strong>Frequency:</strong> ${value}</p>
|
| 945 |
+
<p class="error-message">Failed to get data: ${error.message}</p>
|
| 946 |
+
`;
|
| 947 |
+
});
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
// Document link click event
|
| 951 |
+
nodeDetails.addEventListener('click', (e) => {
|
| 952 |
+
if (e.target.closest('.document-link')) {
|
| 953 |
+
e.preventDefault();
|
| 954 |
+
const filename = e.target.closest('.document-link').dataset.file;
|
| 955 |
+
if (filename) {
|
| 956 |
+
// Handle view document logic here, can send request or redirect
|
| 957 |
+
console.log('View document:', filename);
|
| 958 |
+
window.location.href = `/view-document?filename=${encodeURIComponent(filename)}`;
|
| 959 |
+
}
|
| 960 |
+
}
|
| 961 |
+
else if (e.target.closest('.wordcloud-link')) {
|
| 962 |
+
e.preventDefault();
|
| 963 |
+
const filename = e.target.closest('.wordcloud-link').dataset.file;
|
| 964 |
+
if (filename) {
|
| 965 |
+
// Directly redirect to view document page, adding show wordcloud parameter
|
| 966 |
+
console.log('View word cloud:', filename);
|
| 967 |
+
window.location.href = `/view-document?filename=${encodeURIComponent(filename)}&show_wordcloud=true`;
|
| 968 |
+
}
|
| 969 |
+
}
|
| 970 |
+
else if (e.target.closest('.generate-cloud-link')) {
|
| 971 |
+
e.preventDefault();
|
| 972 |
+
const filename = e.target.closest('.generate-cloud-link').dataset.file;
|
| 973 |
+
if (filename) {
|
| 974 |
+
// Generate word cloud directly on current page
|
| 975 |
+
console.log('Directly generate word cloud:', filename);
|
| 976 |
+
|
| 977 |
+
// Create a modal to display the word cloud
|
| 978 |
+
const modal = document.createElement('div');
|
| 979 |
+
modal.className = 'wordcloud-modal';
|
| 980 |
+
modal.innerHTML = `
|
| 981 |
+
<div class="wordcloud-modal-content">
|
| 982 |
+
<div class="wordcloud-modal-header">
|
| 983 |
+
<h3>Word Cloud for ${filename}</h3>
|
| 984 |
+
<button class="close-modal"><i class="fas fa-times"></i></button>
|
| 985 |
+
</div>
|
| 986 |
+
<div class="wordcloud-modal-body">
|
| 987 |
+
<div class="loading-indicator">
|
| 988 |
+
<i class="fas fa-spinner fa-spin"></i> Generating word cloud...
|
| 989 |
+
</div>
|
| 990 |
+
<div class="wordcloud-image-container" style="display: none;">
|
| 991 |
+
<img class="wordcloud-image" alt="Word Cloud">
|
| 992 |
+
</div>
|
| 993 |
+
<div class="wordcloud-freq-container">
|
| 994 |
+
<h4>Word Frequency Statistics</h4>
|
| 995 |
+
<div class="word-freq-list">
|
| 996 |
+
<p class="placeholder-text">Loading...</p>
|
| 997 |
+
</div>
|
| 998 |
+
</div>
|
| 999 |
+
</div>
|
| 1000 |
+
</div>
|
| 1001 |
+
`;
|
| 1002 |
+
|
| 1003 |
+
document.body.appendChild(modal);
|
| 1004 |
+
|
| 1005 |
+
// Add close modal event
|
| 1006 |
+
modal.querySelector('.close-modal').addEventListener('click', () => {
|
| 1007 |
+
document.body.removeChild(modal);
|
| 1008 |
+
});
|
| 1009 |
+
|
| 1010 |
+
// Call API to get word cloud data
|
| 1011 |
+
fetch(`/api/wordcloud/${encodeURIComponent(filename)}?top_n=50`)
|
| 1012 |
+
.then(response => response.json())
|
| 1013 |
+
.then(data => {
|
| 1014 |
+
if (data.wordcloud_image) {
|
| 1015 |
+
const imageContainer = modal.querySelector('.wordcloud-image-container');
|
| 1016 |
+
const loadingIndicator = modal.querySelector('.loading-indicator');
|
| 1017 |
+
const wordFreqList = modal.querySelector('.word-freq-list');
|
| 1018 |
+
|
| 1019 |
+
// Show word cloud
|
| 1020 |
+
imageContainer.style.display = 'block';
|
| 1021 |
+
loadingIndicator.style.display = 'none';
|
| 1022 |
+
|
| 1023 |
+
const img = modal.querySelector('.wordcloud-image');
|
| 1024 |
+
img.src = `data:image/png;base64,${data.wordcloud_image}`;
|
| 1025 |
+
|
| 1026 |
+
// Show word frequency statistics
|
| 1027 |
+
wordFreqList.innerHTML = '';
|
| 1028 |
+
data.word_frequency.forEach(([word, freq]) => {
|
| 1029 |
+
const wordItem = document.createElement('div');
|
| 1030 |
+
wordItem.className = 'word-freq-item';
|
| 1031 |
+
wordItem.textContent = `${word} (${freq})`;
|
| 1032 |
+
wordFreqList.appendChild(wordItem);
|
| 1033 |
+
});
|
| 1034 |
+
} else {
|
| 1035 |
+
modal.querySelector('.loading-indicator').innerHTML =
|
| 1036 |
+
`<p class="error-message">Failed to generate word cloud: ${data.error || 'Unknown error'}</p>`;
|
| 1037 |
+
}
|
| 1038 |
+
})
|
| 1039 |
+
.catch(error => {
|
| 1040 |
+
console.error('Failed to generate word cloud:', error);
|
| 1041 |
+
modal.querySelector('.loading-indicator').innerHTML =
|
| 1042 |
+
`<p class="error-message">Failed to generate word cloud: ${error.message}</p>`;
|
| 1043 |
+
});
|
| 1044 |
+
}
|
| 1045 |
+
}
|
| 1046 |
+
});
|
| 1047 |
+
|
| 1048 |
+
// Initialize chart
|
| 1049 |
+
myChart.setOption(option);
|
| 1050 |
+
|
| 1051 |
+
// Add chart controls
|
| 1052 |
+
addChartControls();
|
| 1053 |
+
|
| 1054 |
+
// Set initial view mode button text
|
| 1055 |
+
viewToggleBtn.innerHTML = '<i class="fas fa-exchange-alt"></i> Switch to Document-Tag View';
|
| 1056 |
+
|
| 1057 |
+
// First load knowledge map
|
| 1058 |
+
refreshKnowledgeMap();
|
| 1059 |
+
|
| 1060 |
+
// Build word cloud data - Extract tags and high-frequency words from documents
|
| 1061 |
+
function buildWordcloudData(data) {
|
| 1062 |
+
if (data.error) {
|
| 1063 |
+
showError(data.error);
|
| 1064 |
+
return [{name: 'Data Error', value: 30}]; // Return a simple word, ensure at least some content is displayed
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
// If empty data, show prompt information
|
| 1068 |
+
if (!data.documents || data.documents.length === 0) {
|
| 1069 |
+
showError('No usable knowledge map data, please upload documents first');
|
| 1070 |
+
return [
|
| 1071 |
+
{name: 'No Data', value: 50},
|
| 1072 |
+
{name: 'Please Upload Documents', value: 40},
|
| 1073 |
+
{name: 'Word Cloud', value: 30}
|
| 1074 |
+
];
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
console.log("Building word cloud data, document count:", data.documents.length);
|
| 1078 |
+
|
| 1079 |
+
// Collect all tags and their frequencies
|
| 1080 |
+
const tagCount = {};
|
| 1081 |
+
|
| 1082 |
+
// Some common stop words
|
| 1083 |
+
const stopwords = ['the', 'and', 'for', 'with', 'this', 'that', 'in', 'on', 'at', 'to', 'of', 'a', 'an'];
|
| 1084 |
+
|
| 1085 |
+
// Ensure some basic words
|
| 1086 |
+
const defaultWords = ['Document', 'Learning', 'Cognition', 'Knowledge', 'Material', 'Content'];
|
| 1087 |
+
let wordIndex = 0;
|
| 1088 |
+
|
| 1089 |
+
// Loop through all documents, extract tags
|
| 1090 |
+
data.documents.forEach(doc => {
|
| 1091 |
+
// Process tags - Tag weight highest
|
| 1092 |
+
if (doc.tags && doc.tags.length > 0) {
|
| 1093 |
+
doc.tags.forEach(tag => {
|
| 1094 |
+
if (tag && tag.length > 1 && !stopwords.includes(tag.toLowerCase())) {
|
| 1095 |
+
tagCount[tag] = (tagCount[tag] || 0) + 15;
|
| 1096 |
+
}
|
| 1097 |
+
});
|
| 1098 |
+
} else {
|
| 1099 |
+
// If no tags, use default word
|
| 1100 |
+
const word = defaultWords[wordIndex % defaultWords.length];
|
| 1101 |
+
tagCount[word] = (tagCount[word] || 0) + 10;
|
| 1102 |
+
wordIndex++;
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
// Extract keywords from filename
|
| 1106 |
+
if (doc.filename) {
|
| 1107 |
+
const baseName = doc.filename.split('.')[0]; // Remove extension
|
| 1108 |
+
if (baseName && baseName.length > 1) {
|
| 1109 |
+
tagCount[baseName] = (tagCount[baseName] || 0) + 10;
|
| 1110 |
+
}
|
| 1111 |
+
}
|
| 1112 |
+
});
|
| 1113 |
+
|
| 1114 |
+
// Always add some default words, ensure enough content is displayed
|
| 1115 |
+
if (Object.keys(tagCount).length < 5) {
|
| 1116 |
+
defaultWords.forEach((word, index) => {
|
| 1117 |
+
tagCount[word] = 30 - index * 3; // Decreasing weight
|
| 1118 |
+
});
|
| 1119 |
+
|
| 1120 |
+
// Add a "new" word for testing
|
| 1121 |
+
tagCount["new"] = 10;
|
| 1122 |
+
}
|
| 1123 |
+
|
| 1124 |
+
// Convert to word cloud data format
|
| 1125 |
+
const cloudData = Object.keys(tagCount).map(tag => ({
|
| 1126 |
+
name: tag,
|
| 1127 |
+
value: tagCount[tag]
|
| 1128 |
+
}));
|
| 1129 |
+
|
| 1130 |
+
// Sort by frequency
|
| 1131 |
+
cloudData.sort((a, b) => b.value - a.value);
|
| 1132 |
+
|
| 1133 |
+
// Show some debugging information
|
| 1134 |
+
console.log("Word cloud data building completed, vocabulary count:", cloudData.length);
|
| 1135 |
+
console.log("Word cloud data:", JSON.stringify(cloudData.slice(0, 10)));
|
| 1136 |
+
|
| 1137 |
+
// Return sorted word cloud data
|
| 1138 |
+
return cloudData.slice(0, 50); // Limit word count
|
| 1139 |
+
}
|
| 1140 |
+
});
|
templates/index.html
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>AI Teaching Assistant</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="container">
|
| 12 |
+
<aside class="sidebar">
|
| 13 |
+
<div class="sidebar-header">
|
| 14 |
+
<h1>AI Teaching Assistant</h1>
|
| 15 |
+
</div>
|
| 16 |
+
<nav class="sidebar-menu">
|
| 17 |
+
<ul>
|
| 18 |
+
<li><a href="#"><i class="fas fa-upload"></i>Upload</a></li>
|
| 19 |
+
<li><a href="#"><i class="fas fa-folder"></i>Folders</a></li>
|
| 20 |
+
<li><a href="{{ url_for('knowledge_map') }}"><i class="fas fa-project-diagram"></i>Knowledge Map</a></li>
|
| 21 |
+
<li class="active"><a href="#"><i class="fas fa-comment-dots"></i>Chat Assistant</a></li>
|
| 22 |
+
<li><a href="#"><i class="fas fa-history"></i>Review</a></li>
|
| 23 |
+
<li><a href="{{ url_for('settings') }}"><i class="fas fa-cog"></i>Settings</a></li>
|
| 24 |
+
</ul>
|
| 25 |
+
</nav>
|
| 26 |
+
<div class="sidebar-footer">
|
| 27 |
+
<p>Version 1.0.0</p>
|
| 28 |
+
</div>
|
| 29 |
+
</aside>
|
| 30 |
+
|
| 31 |
+
<!-- 在main-content前添加一个隐藏的数据区域,用于存储服务器端传递的文件信息 -->
|
| 32 |
+
<div id="server-data" data-uploaded-files="{{ uploaded_files|tojson if uploaded_files else '[]' }}" style="display: none;"></div>
|
| 33 |
+
|
| 34 |
+
<main class="main-content">
|
| 35 |
+
<section class="top-section">
|
| 36 |
+
<div class="card upload-panel">
|
| 37 |
+
<h2><i class="fas fa-upload"></i> Upload Documents</h2>
|
| 38 |
+
<div class="file-upload-container">
|
| 39 |
+
<button id="upload-button" class="upload-btn">Choose File</button>
|
| 40 |
+
<input type="file" id="file-upload" accept=".pdf,.txt" style="display: none">
|
| 41 |
+
<div class="supported-files">Supported: PDF, TXT</div>
|
| 42 |
+
|
| 43 |
+
<!-- File list structure using table style -->
|
| 44 |
+
<div class="file-list-container">
|
| 45 |
+
<ul id="file-list" class="file-list-table">
|
| 46 |
+
<!-- Files will be added dynamically through JavaScript -->
|
| 47 |
+
</ul>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
<div class="summary-tags-panel card">
|
| 52 |
+
<h2><i class="fas fa-tags"></i> Auto-Summarization & Tags</h2>
|
| 53 |
+
<div class="file-info-section">
|
| 54 |
+
<h4><i class="fas fa-tag"></i> Document Tags</h4>
|
| 55 |
+
<div id="document-tags-container" class="document-tags-container">
|
| 56 |
+
<!-- Document tags will be generated here dynamically -->
|
| 57 |
+
<p class="placeholder-text">Tags will appear after uploading files...</p>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
<div class="summary-section">
|
| 61 |
+
<h4><i class="fas fa-file-alt"></i> File Summary</h4>
|
| 62 |
+
<div id="summary-area" class="summary-content">
|
| 63 |
+
<p class="placeholder-text">Summary will appear after uploading files...</p>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</section>
|
| 68 |
+
|
| 69 |
+
<section class="middle-section">
|
| 70 |
+
<div class="knowledge-folders-panel card">
|
| 71 |
+
<h2><i class="fas fa-folder-open"></i> Knowledge Folders</h2>
|
| 72 |
+
<ul class="folder-list">
|
| 73 |
+
{% if knowledge_folders %}
|
| 74 |
+
{% for folder, files in knowledge_folders.items() %}
|
| 75 |
+
<li class="folder-item" data-folder="{{ folder }}">
|
| 76 |
+
<div class="folder-header">
|
| 77 |
+
<i class="fas fa-folder"></i> {{ folder }} ({{ files|length }} docs)
|
| 78 |
+
<i class="fas fa-chevron-down folder-toggle"></i>
|
| 79 |
+
</div>
|
| 80 |
+
<ul class="file-list" style="display: none;">
|
| 81 |
+
{% for file in files %}
|
| 82 |
+
<li class="file-item">
|
| 83 |
+
<i class="fas fa-file-alt"></i>
|
| 84 |
+
{% if use_static_folders is defined and use_static_folders %}
|
| 85 |
+
{{ file }} <span class="file-tag">(Example)</span>
|
| 86 |
+
{% else %}
|
| 87 |
+
<a href="{{ url_for('view_document', filename=file) }}" class="file-link">{{ file }}</a>
|
| 88 |
+
{% endif %}
|
| 89 |
+
</li>
|
| 90 |
+
{% endfor %}
|
| 91 |
+
</ul>
|
| 92 |
+
</li>
|
| 93 |
+
{% endfor %}
|
| 94 |
+
{% else %}
|
| 95 |
+
<li>No knowledge folders. Upload files to start building your knowledge base.</li>
|
| 96 |
+
{% endif %}
|
| 97 |
+
</ul>
|
| 98 |
+
{% if use_static_folders is defined and use_static_folders %}
|
| 99 |
+
<div class="demo-notice">
|
| 100 |
+
<i class="fas fa-info-circle"></i> These are example folders. Your personal knowledge base will appear after uploading files.
|
| 101 |
+
</div>
|
| 102 |
+
{% endif %}
|
| 103 |
+
</div>
|
| 104 |
+
<div class="chat-panel card">
|
| 105 |
+
<h2><i class="fas fa-comments"></i> Chat with AI Assistant</h2>
|
| 106 |
+
<div id="chat-history" class="chat-history">
|
| 107 |
+
<div class="message bot-message">
|
| 108 |
+
<div class="message-content">
|
| 109 |
+
Hello, I'm your AI teaching assistant. You can ask me any teaching-related questions, or upload files for analysis.
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
<div class="input-container">
|
| 114 |
+
<textarea id="user-input" placeholder="Ask about any concept..."></textarea>
|
| 115 |
+
<button id="send-btn"><i class="fas fa-paper-plane"></i></button>
|
| 116 |
+
</div>
|
| 117 |
+
<div id="status-indicator" class="status-indicator"></div>
|
| 118 |
+
</div>
|
| 119 |
+
</section>
|
| 120 |
+
|
| 121 |
+
<section class="bottom-section">
|
| 122 |
+
<div class="review-panel card">
|
| 123 |
+
<h2><i class="fas fa-bell"></i> Review Reminders</h2>
|
| 124 |
+
<p>Next review cycle: <strong>Tomorrow (2 docs)</strong> based on Ebbinghaus curve</p>
|
| 125 |
+
<!-- Placeholder content -->
|
| 126 |
+
</div>
|
| 127 |
+
</section>
|
| 128 |
+
</main>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
<script src="{{ url_for('static', filename='js/chat.js') }}"></script>
|
| 132 |
+
<script>
|
| 133 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 134 |
+
// 为所有文件夹添加点击事件
|
| 135 |
+
setupFolderEvents();
|
| 136 |
+
|
| 137 |
+
// 监听知识文件夹更新事件
|
| 138 |
+
document.addEventListener('knowledge-folders-updated', function(e) {
|
| 139 |
+
// 获取更新后的文件夹数据
|
| 140 |
+
const data = e.detail;
|
| 141 |
+
// 检查是否有有效的文件夹数据
|
| 142 |
+
if (data && data.folders) {
|
| 143 |
+
// 更新文件夹结构
|
| 144 |
+
updateKnowledgeFolders(data.folders);
|
| 145 |
+
}
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
// 设置文件夹点击事件
|
| 149 |
+
function setupFolderEvents() {
|
| 150 |
+
const folderHeaders = document.querySelectorAll('.folder-header');
|
| 151 |
+
folderHeaders.forEach(header => {
|
| 152 |
+
header.addEventListener('click', function() {
|
| 153 |
+
// 获取对应的文件列表
|
| 154 |
+
const fileList = this.nextElementSibling;
|
| 155 |
+
const folderIcon = this.querySelector('.folder-toggle');
|
| 156 |
+
|
| 157 |
+
// 切换文件列表的显示状态
|
| 158 |
+
if (fileList.style.display === 'none') {
|
| 159 |
+
fileList.style.display = 'block';
|
| 160 |
+
folderIcon.classList.remove('fa-chevron-down');
|
| 161 |
+
folderIcon.classList.add('fa-chevron-up');
|
| 162 |
+
} else {
|
| 163 |
+
fileList.style.display = 'none';
|
| 164 |
+
folderIcon.classList.remove('fa-chevron-up');
|
| 165 |
+
folderIcon.classList.add('fa-chevron-down');
|
| 166 |
+
}
|
| 167 |
+
});
|
| 168 |
+
});
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// 更新知识文件夹结构
|
| 172 |
+
function updateKnowledgeFolders(folders) {
|
| 173 |
+
const folderList = document.querySelector('.folder-list');
|
| 174 |
+
if (!folderList) return;
|
| 175 |
+
|
| 176 |
+
// 检查是否有文件夹数据
|
| 177 |
+
if (!folders || Object.keys(folders).length === 0) {
|
| 178 |
+
folderList.innerHTML = '<li>No knowledge folders. Upload files to start building your knowledge base.</li>';
|
| 179 |
+
return;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// 清空当前文件夹列表
|
| 183 |
+
folderList.innerHTML = '';
|
| 184 |
+
|
| 185 |
+
// 为每个文件夹创建HTML结构
|
| 186 |
+
Object.entries(folders).forEach(([folder, files]) => {
|
| 187 |
+
const folderItem = document.createElement('li');
|
| 188 |
+
folderItem.className = 'folder-item';
|
| 189 |
+
folderItem.setAttribute('data-folder', folder);
|
| 190 |
+
|
| 191 |
+
const folderHeader = document.createElement('div');
|
| 192 |
+
folderHeader.className = 'folder-header';
|
| 193 |
+
folderHeader.innerHTML = `
|
| 194 |
+
<i class="fas fa-folder"></i> ${folder} (${files.length} docs)
|
| 195 |
+
<i class="fas fa-chevron-down folder-toggle"></i>
|
| 196 |
+
`;
|
| 197 |
+
|
| 198 |
+
const fileListEl = document.createElement('ul');
|
| 199 |
+
fileListEl.className = 'file-list';
|
| 200 |
+
fileListEl.style.display = 'none';
|
| 201 |
+
|
| 202 |
+
// 添加文件到文件列表
|
| 203 |
+
files.forEach(file => {
|
| 204 |
+
const fileItem = document.createElement('li');
|
| 205 |
+
fileItem.className = 'file-item';
|
| 206 |
+
fileItem.innerHTML = `
|
| 207 |
+
<i class="fas fa-file-alt"></i>
|
| 208 |
+
<a href="/view-document?filename=${encodeURIComponent(file)}" class="file-link">${file}</a>
|
| 209 |
+
`;
|
| 210 |
+
fileListEl.appendChild(fileItem);
|
| 211 |
+
});
|
| 212 |
+
|
| 213 |
+
folderItem.appendChild(folderHeader);
|
| 214 |
+
folderItem.appendChild(fileListEl);
|
| 215 |
+
folderList.appendChild(folderItem);
|
| 216 |
+
});
|
| 217 |
+
|
| 218 |
+
// 移除示例提示信息
|
| 219 |
+
const demoNotice = document.querySelector('.demo-notice');
|
| 220 |
+
if (demoNotice) {
|
| 221 |
+
demoNotice.remove();
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// 重新设置文件夹点击事件
|
| 225 |
+
setupFolderEvents();
|
| 226 |
+
}
|
| 227 |
+
});
|
| 228 |
+
</script>
|
| 229 |
+
</body>
|
| 230 |
+
</html>
|
templates/knowledge_map.html
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Knowledge Map - AI Teaching Assistant</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 9 |
+
<!-- Import ECharts -->
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
| 11 |
+
<!-- Import ECharts WordCloud extension -->
|
| 12 |
+
<script src="https://cdn.jsdelivr.net/npm/echarts-wordcloud@2.1.0/dist/echarts-wordcloud.min.js"></script>
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<div class="container">
|
| 16 |
+
<aside class="sidebar">
|
| 17 |
+
<div class="sidebar-header">
|
| 18 |
+
<h1>AI Teaching Assistant</h1>
|
| 19 |
+
</div>
|
| 20 |
+
<nav class="sidebar-menu">
|
| 21 |
+
<ul>
|
| 22 |
+
<li><a href="{{ url_for('home') }}"><i class="fas fa-upload"></i>Upload</a></li>
|
| 23 |
+
<li><a href="#"><i class="fas fa-folder"></i>Folders</a></li>
|
| 24 |
+
<li class="active"><a href="{{ url_for('knowledge_map') }}"><i class="fas fa-project-diagram"></i>Knowledge Map</a></li>
|
| 25 |
+
<li><a href="{{ url_for('home') }}"><i class="fas fa-comment-dots"></i>Chat Assistant</a></li>
|
| 26 |
+
<li><a href="#"><i class="fas fa-history"></i>Review</a></li>
|
| 27 |
+
<li><a href="{{ url_for('settings') }}"><i class="fas fa-cog"></i>Settings</a></li>
|
| 28 |
+
</ul>
|
| 29 |
+
</nav>
|
| 30 |
+
<div class="sidebar-footer">
|
| 31 |
+
<p>Version 1.0.0</p>
|
| 32 |
+
</div>
|
| 33 |
+
</aside>
|
| 34 |
+
|
| 35 |
+
<main class="main-content">
|
| 36 |
+
<section class="knowledge-map-section">
|
| 37 |
+
<div class="knowledge-map-panel card">
|
| 38 |
+
<h2><i class="fas fa-project-diagram"></i> Knowledge Map</h2>
|
| 39 |
+
<div class="map-controls">
|
| 40 |
+
<div class="control-group">
|
| 41 |
+
<label for="map-layout">Layout:</label>
|
| 42 |
+
<select id="map-layout" class="map-control-select">
|
| 43 |
+
<option value="tree">Tree</option>
|
| 44 |
+
<option value="force">Force Directed</option>
|
| 45 |
+
<option value="radial">Radial</option>
|
| 46 |
+
<option value="wordcloud">Word Cloud</option>
|
| 47 |
+
</select>
|
| 48 |
+
</div>
|
| 49 |
+
<div class="control-group">
|
| 50 |
+
<label for="map-depth">Depth Level:</label>
|
| 51 |
+
<select id="map-depth" class="map-control-select">
|
| 52 |
+
<option value="all">All</option>
|
| 53 |
+
<option value="1">Level 1</option>
|
| 54 |
+
<option value="2">Level 2</option>
|
| 55 |
+
<option value="3">Level 3</option>
|
| 56 |
+
</select>
|
| 57 |
+
</div>
|
| 58 |
+
<button id="refresh-map" class="map-control-btn"><i class="fas fa-sync-alt"></i> Refresh</button>
|
| 59 |
+
<!-- View switching buttons will be added dynamically in JavaScript -->
|
| 60 |
+
</div>
|
| 61 |
+
<div id="knowledge-map-container" class="knowledge-map-container">
|
| 62 |
+
<!-- Knowledge map will be rendered here -->
|
| 63 |
+
</div>
|
| 64 |
+
<div class="map-info">
|
| 65 |
+
<p>Select a node to view related information</p>
|
| 66 |
+
<div id="node-info" class="node-info">
|
| 67 |
+
<h3 id="node-title">No node selected</h3>
|
| 68 |
+
<div id="node-details" class="node-details">
|
| 69 |
+
<p class="placeholder-text">Click on a node in the map to view details</p>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</section>
|
| 75 |
+
</main>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<script src="{{ url_for('static', filename='js/knowledge-map.js') }}"></script>
|
| 79 |
+
</body>
|
| 80 |
+
</html>
|
templates/settings.html
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Settings - Gemini Chatbot</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="container">
|
| 11 |
+
<header>
|
| 12 |
+
<h1>Settings</h1>
|
| 13 |
+
<a href="{{ url_for('home') }}" class="back-btn">Back</a>
|
| 14 |
+
</header>
|
| 15 |
+
|
| 16 |
+
<main>
|
| 17 |
+
<div class="settings-container">
|
| 18 |
+
<h2>Google Gemini API Settings</h2>
|
| 19 |
+
|
| 20 |
+
<form action="{{ url_for('settings') }}" method="post">
|
| 21 |
+
<div class="form-group">
|
| 22 |
+
<label for="api_key">API Key</label>
|
| 23 |
+
<input type="password" id="api_key" name="api_key"
|
| 24 |
+
value="{{ api_key }}" required>
|
| 25 |
+
<small class="info-text">
|
| 26 |
+
You can get an API key from <a href="https://makersuite.google.com/app/apikey" target="_blank">Google AI Studio</a>
|
| 27 |
+
</small>
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
<div class="form-actions">
|
| 31 |
+
<button type="submit" class="save-btn">Save Settings</button>
|
| 32 |
+
</div>
|
| 33 |
+
</form>
|
| 34 |
+
|
| 35 |
+
<div class="instructions">
|
| 36 |
+
<h3>How to get an API key:</h3>
|
| 37 |
+
<ol>
|
| 38 |
+
<li>Visit <a href="https://makersuite.google.com/app/apikey" target="_blank">Google AI Studio</a></li>
|
| 39 |
+
<li>Log in to your Google account</li>
|
| 40 |
+
<li>Click "Get API key" or "Create API key"</li>
|
| 41 |
+
<li>Create a new API key or use an existing one</li>
|
| 42 |
+
<li>Copy the key and paste it into the form above</li>
|
| 43 |
+
</ol>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
</main>
|
| 47 |
+
|
| 48 |
+
<footer>
|
| 49 |
+
<p>Powered by Google Gemini API | Developed with Python + Flask</p>
|
| 50 |
+
</footer>
|
| 51 |
+
</div>
|
| 52 |
+
</body>
|
| 53 |
+
</html>
|
templates/view_document.html
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>View Document - {{ filename }}</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 9 |
+
<style>
|
| 10 |
+
.document-container {
|
| 11 |
+
max-width: 800px;
|
| 12 |
+
margin: 0 auto;
|
| 13 |
+
padding: 20px;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.document-header {
|
| 17 |
+
display: flex;
|
| 18 |
+
justify-content: space-between;
|
| 19 |
+
align-items: center;
|
| 20 |
+
margin-bottom: 20px;
|
| 21 |
+
padding-bottom: 10px;
|
| 22 |
+
border-bottom: 1px solid #e5e7eb;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.back-button {
|
| 26 |
+
text-decoration: none;
|
| 27 |
+
color: #3b82f6;
|
| 28 |
+
display: flex;
|
| 29 |
+
align-items: center;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.back-button i {
|
| 33 |
+
margin-right: 5px;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.document-title {
|
| 37 |
+
margin: 0;
|
| 38 |
+
font-size: 1.5rem;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.document-actions {
|
| 42 |
+
display: flex;
|
| 43 |
+
gap: 10px;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.action-button {
|
| 47 |
+
background-color: #f3f4f6;
|
| 48 |
+
border: 1px solid #e5e7eb;
|
| 49 |
+
border-radius: 4px;
|
| 50 |
+
padding: 5px 10px;
|
| 51 |
+
cursor: pointer;
|
| 52 |
+
display: flex;
|
| 53 |
+
align-items: center;
|
| 54 |
+
transition: background-color 0.3s;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.action-button i {
|
| 58 |
+
margin-right: 5px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.action-button:hover {
|
| 62 |
+
background-color: #e5e7eb;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.document-content {
|
| 66 |
+
background-color: #ffffff;
|
| 67 |
+
border: 1px solid #e5e7eb;
|
| 68 |
+
border-radius: 6px;
|
| 69 |
+
padding: 20px;
|
| 70 |
+
white-space: pre-wrap;
|
| 71 |
+
font-family: Arial, sans-serif;
|
| 72 |
+
line-height: 1.6;
|
| 73 |
+
max-height: 70vh;
|
| 74 |
+
overflow-y: auto;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.document-footer {
|
| 78 |
+
margin-top: 20px;
|
| 79 |
+
text-align: center;
|
| 80 |
+
font-size: 0.9rem;
|
| 81 |
+
color: #6b7280;
|
| 82 |
+
}
|
| 83 |
+
</style>
|
| 84 |
+
</head>
|
| 85 |
+
<body>
|
| 86 |
+
<div class="document-container">
|
| 87 |
+
<div class="document-header">
|
| 88 |
+
<a href="{{ url_for('home') }}" class="back-button">
|
| 89 |
+
<i class="fas fa-arrow-left"></i> Back to Home
|
| 90 |
+
</a>
|
| 91 |
+
<h1 class="document-title">{{ filename }}</h1>
|
| 92 |
+
<div class="document-actions">
|
| 93 |
+
<button class="action-button" id="generate-wordcloud-btn">
|
| 94 |
+
<i class="fas fa-cloud"></i> Generate Word Cloud
|
| 95 |
+
</button>
|
| 96 |
+
<button class="action-button" onclick="window.print()">
|
| 97 |
+
<i class="fas fa-print"></i> Print
|
| 98 |
+
</button>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<div class="document-content">
|
| 103 |
+
{{ content }}
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<div id="wordcloud-container" style="display: none; margin-top: 20px;">
|
| 107 |
+
<h2><i class="fas fa-cloud"></i> Document Word Cloud</h2>
|
| 108 |
+
<div id="wordcloud-image-container" style="text-align: center; padding: 20px; background-color: #f9fafb; border-radius: 6px;">
|
| 109 |
+
<div id="loading-indicator" style="display: none;">
|
| 110 |
+
<i class="fas fa-spinner fa-spin"></i> Generating word cloud...
|
| 111 |
+
</div>
|
| 112 |
+
<img id="wordcloud-image" style="max-width: 100%; display: none;" />
|
| 113 |
+
</div>
|
| 114 |
+
<div id="word-frequency" style="margin-top: 15px; display: none;">
|
| 115 |
+
<h3>Word Frequency Statistics</h3>
|
| 116 |
+
<div id="word-frequency-list" style="display: flex; flex-wrap: wrap; gap: 10px;">
|
| 117 |
+
<!-- Word frequency data will be added here dynamically -->
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
<div class="document-footer">
|
| 123 |
+
<p>AI Teaching Assistant - Document Viewer</p>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
<script>
|
| 128 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 129 |
+
const generateWordcloudBtn = document.getElementById('generate-wordcloud-btn');
|
| 130 |
+
const wordcloudContainer = document.getElementById('wordcloud-container');
|
| 131 |
+
const loadingIndicator = document.getElementById('loading-indicator');
|
| 132 |
+
const wordcloudImage = document.getElementById('wordcloud-image');
|
| 133 |
+
const wordFrequency = document.getElementById('word-frequency');
|
| 134 |
+
const wordFrequencyList = document.getElementById('word-frequency-list');
|
| 135 |
+
|
| 136 |
+
// If URL parameter generate_wordcloud=true, automatically generate word cloud
|
| 137 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 138 |
+
if (urlParams.get('generate_wordcloud') === 'true') {
|
| 139 |
+
generateWordcloud();
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
generateWordcloudBtn.addEventListener('click', function() {
|
| 143 |
+
generateWordcloud();
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
function generateWordcloud() {
|
| 147 |
+
// Show word cloud container and loading indicator
|
| 148 |
+
wordcloudContainer.style.display = 'block';
|
| 149 |
+
loadingIndicator.style.display = 'block';
|
| 150 |
+
wordcloudImage.style.display = 'none';
|
| 151 |
+
wordFrequency.style.display = 'none';
|
| 152 |
+
|
| 153 |
+
// Get top_n parameter, default is 100
|
| 154 |
+
const topN = urlParams.get('top_n') || 100;
|
| 155 |
+
|
| 156 |
+
// Call word cloud API
|
| 157 |
+
fetch(`/api/wordcloud/{{ filename }}?top_n=${topN}`)
|
| 158 |
+
.then(response => {
|
| 159 |
+
if (!response.ok) {
|
| 160 |
+
throw new Error('Failed to generate word cloud');
|
| 161 |
+
}
|
| 162 |
+
return response.json();
|
| 163 |
+
})
|
| 164 |
+
.then(data => {
|
| 165 |
+
// Hide loading indicator
|
| 166 |
+
loadingIndicator.style.display = 'none';
|
| 167 |
+
|
| 168 |
+
// Show word cloud image - Fix field name to match backend
|
| 169 |
+
wordcloudImage.src = `data:image/png;base64,${data.wordcloud_image}`;
|
| 170 |
+
wordcloudImage.style.display = 'block';
|
| 171 |
+
|
| 172 |
+
// Show word frequency statistics
|
| 173 |
+
wordFrequency.style.display = 'block';
|
| 174 |
+
|
| 175 |
+
// Clear previous word frequency list
|
| 176 |
+
wordFrequencyList.innerHTML = '';
|
| 177 |
+
|
| 178 |
+
// Add word frequency data
|
| 179 |
+
data.word_frequency.forEach(([word, freq]) => {
|
| 180 |
+
const wordItem = document.createElement('div');
|
| 181 |
+
wordItem.className = 'word-item';
|
| 182 |
+
wordItem.style.padding = '5px 10px';
|
| 183 |
+
wordItem.style.backgroundColor = '#e5e7eb';
|
| 184 |
+
wordItem.style.borderRadius = '4px';
|
| 185 |
+
wordItem.innerHTML = `<strong>${word}</strong>: ${freq}`;
|
| 186 |
+
wordFrequencyList.appendChild(wordItem);
|
| 187 |
+
});
|
| 188 |
+
})
|
| 189 |
+
.catch(error => {
|
| 190 |
+
loadingIndicator.style.display = 'none';
|
| 191 |
+
wordFrequencyList.innerHTML = `<div style="color: red;">Error: ${error.message}</div>`;
|
| 192 |
+
});
|
| 193 |
+
}
|
| 194 |
+
});
|
| 195 |
+
</script>
|
| 196 |
+
</body>
|
| 197 |
+
</html>
|