roxqtang commited on
Commit
8fe50ee
·
0 Parent(s):

Initial commit

Browse files
.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>