Sheet-to-Doc 支持 JSON 和 JSONL 格式:批量生成 Word 文档的新方式

作为一名数据分析师和内容创作者,我经常需要将各种格式的数据转换为结构化文档。最近,我发现 WTSolutions 开发的 Sheet-to-Doc 工具更新了一个重要功能——支持 JSON 和 JSONL 格式的数据输入!这意味着我们可以直接使用来自各种应用程序的结构化数据,快速批量生成 Word 文档。


什么是 JSON 和 JSONL 格式?

JSON(JavaScript Object Notation)

JSON 是一种轻量级的数据交换格式,易于阅读和编写,同时也易于机器解析和生成。它基于键值对的结构,常用于 Web 应用程序之间的数据传输。

示例:

[
  {
    "姓名": "张三",
    "年龄": 30,
    "职位": "产品经理",
    "部门": "产品部"
  },
  {
    "姓名": "李四",
    "年龄": 28,
    "职位": "开发工程师",
    "部门": "技术部"
  }
]

JSONL(JSON Lines)

JSONL 是一种文本格式,其中每行都是一个有效的 JSON 对象。这种格式非常适合处理大量数据,因为它可以逐行处理,而不需要一次性加载整个文件到内存中。

示例:

{"姓名": "张三", "年龄": 30, "职位": "产品经理", "部门": "产品部"}
{"姓名": "李四", "年龄": 28, "职位": "开发工程师", "部门": "技术部"}

哪些软件可以导出 JSON/JSONL 数据?

Sheet-to-Doc 支持 JSON 和 JSONL 格式后,您可以直接使用来自以下应用程序的数据:

1. 数据分析工具

  • Python/R:使用 pandas 或 tidyverse 等库轻松导出数据为 JSON/JSONL
  • Jupyter Notebook:数据分析结果可以直接导出为 JSON 格式
  • Excel 365:通过 Power Query 或 VBA 导出 JSON 数据

2. 数据库和存储系统

  • MongoDB:原生支持 JSON 格式,可以直接导出数据
  • Elasticsearch:搜索结果可以导出为 JSON 格式
  • AWS S3:可以存储和导出 JSON/JSONL 格式的数据文件

3. API 和 Web 服务

  • REST API:大多数现代 API 都返回 JSON 格式的数据
  • GraphQL:查询结果可以格式化为 JSON
  • 第三方服务:如 Google Sheets API、Airtable API 等都支持 JSON 数据

4. 自动化工具

  • Zapier/Make:工作流自动化工具可以将数据转换为 JSON 格式
  • Power Automate:Microsoft 自动化平台支持 JSON 数据处理
  • Python 脚本:自定义脚本可以从各种来源提取数据并导出为 JSON/JSONL

5. AI 和大语言模型

  • OpenAI API:生成的数据可以保存为 JSON 格式
  • Claude API:支持 JSON 格式的输出
  • 本地 LLM:如 Llama 等模型可以生成 JSON 格式的数据

Sheet-to-Doc 使用 JSON/JSONL 的具体应用场景

1. 从 API 数据生成报告

假设您需要定期从 CRM API 提取客户数据,并生成个性化的客户报告。现在,您可以直接将 API 返回的 JSON 数据粘贴到 Sheet-to-Doc 中,使用预定义的 Word 模板,批量生成专业报告。

2. 数据分析结果可视化报告

数据分析师经常需要将分析结果转换为可阅读的报告。使用 Python 或 R 分析数据后,导出为 JSON 格式,然后通过 Sheet-to-Doc 生成包含图表和分析结论的 Word 报告。

3. 批量生成个性化邮件

营销团队可以从客户数据库导出 JSONL 格式的客户信息,然后使用 Sheet-to-Doc 生成个性化的营销邮件或邀请函,保持一致的品牌形象。

4. 从 MongoDB 数据生成合同

法律团队可以直接从 MongoDB 导出客户和项目数据为 JSON 格式,然后使用 Sheet-to-Doc 生成个性化的合同和协议,提高工作效率。

5. AI 生成内容的文档化

使用 AI 生成的内容(如产品描述、新闻稿等)可以保存为 JSON 格式,然后通过 Sheet-to-Doc 生成结构化的 Word 文档,方便后续编辑和发布。

6. 自动化生成发票和账单

财务团队可以从会计软件导出交易数据为 JSON 格式,然后使用 Sheet-to-Doc 批量生成发票和账单,减少手动操作。


如何使用 Sheet-to-Doc 处理 JSON/JSONL 数据?

步骤 1:准备 JSON/JSONL 数据

确保您的数据格式正确,JSON 应该是一个对象数组,JSONL 则是每行一个对象。

步骤 2:访问 Sheet-to-Doc 网站

打开Sheet-to-Doc 官方网站 https://s.wtsolutions.cn/sheet-to-doc.html

步骤 3:选择数据类型

在数据粘贴区域,选择 “JSON” 或 “JSONL” 作为数据类型。

步骤 4:粘贴数据

将您的 JSON/JSONL 数据粘贴到文本框中,系统会自动预览数据。

步骤 5:上传 Word 模板

准备包含占位符(如 {姓名}{年龄})的 Word 模板,然后上传。

步骤 6:生成文档

选择生成模式(单文档或批量),设置文件名生成方式,然后点击 “生成文档” 按钮。

步骤 7:下载结果

等待生成完成后,下载生成的 Word 文档。


Sheet-to-Doc 支持 JSON/JSONL 的优势

1. 更广泛的数据来源

支持 JSON/JSONL 格式后,Sheet-to-Doc 可以直接使用来自更多应用程序和服务的数据,无需中间转换步骤。

2. 更高的处理效率

JSON 和 JSONL 是结构化数据格式,解析速度快,适合处理大量数据。

3. 更好的兼容性

现代应用程序普遍支持 JSON 格式,Sheet-to-Doc 支持 JSON/JSONL 后,可以更好地与这些应用程序集成。

4. 更灵活的数据处理

JSON 支持复杂的数据结构,包括嵌套对象和数组,Sheet-to-Doc 可以处理这些复杂数据,生成更丰富的文档。

5. 保持数据完整性

直接使用 JSON/JSONL 数据可以避免中间转换过程中可能出现的数据丢失或格式错误。


我的使用体验

最近,我使用 Sheet-to-Doc 处理了来自 REST API 的 JSON 数据,生成了 200 多份客户报告。整个过程非常顺利:

  1. 数据准备:使用 Python 脚本从 API 获取数据并导出为 JSON 格式(耗时约 2 分钟)
  2. 文档生成:将 JSON 数据粘贴到 Sheet-to-Doc,上传模板,生成 200 份报告(耗时约 5 分钟)
  3. 结果检查:所有报告数据准确,格式一致,没有任何错误

相比之前使用 Excel 作为中间步骤的方法,节省了至少 3 小时的工作时间,并且减少了数据转换过程中可能出现的错误。


结论

Sheet-to-Doc 支持 JSON 和 JSONL 格式是一个重要的功能更新,它大大扩展了工具的应用范围和灵活性。现在,您可以直接使用来自各种应用程序和服务的结构化数据,快速批量生成专业的 Word 文档,提高工作效率,减少人为错误。

如果您经常需要处理结构化数据并生成文档,我强烈建议您尝试使用 Sheet-to-Doc 的 JSON/JSONL 功能。它将彻底改变您的文档创建流程,让您专注于更有价值的工作!

您可以访问Sheet-to-Doc 官方网站 https://s.wtsolutions.cn/sheet-to-doc.html 开始使用,或查看 详细文档https://sheet-to-doc.wtsolutions.cn/zh-cn/latest/ 了解更多信息。


您已经尝试过 Sheet-to-Doc 的 JSON/JSONL 功能了吗?请在下面的评论中分享您的经验!

#请优化下面程序,实现更超强更先进的功能 import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext, simpledialog import ollama import os import time import threading import numpy as np import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure import pandas as pd import seaborn as sns import PyPDF2 import docx import markdown from bs4 import BeautifulSoup import openpyxl from PIL import Image import pytesseract import io import psutil from ttkthemes import ThemedTk import pynvml import re # 初始化pynvml pynvml.nvmlInit() # 设置中文字体 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False class RAGApplication: def __init__(self, root): self.root = root self.root.title("✨智能RAG应用系统✨") self.root.geometry("1400x900") self.root.configure(bg="#f0f0f0") # 淡灰色背景#f0f0f0 # 使用现代主题 self.style = ttk.Style() self.style.theme_use('arc') # 现代主题 # 自定义样式 - 淡色调 self.style.configure('TFrame', background='#f0f0f0') self.style.configure('TLabel', background='#f0f0f0', foreground='#333333') self.style.configure('TLabelframe', background='#f0f0f0', foreground='#4dabf5', borderwidth=1) self.style.configure('TLabelframe.Label', background='#f0f0f0', foreground='#4dabf5') # 淡蓝色标题 self.style.configure('TButton', background='#4dabf5', foreground='#333333', borderwidth=1) # 深色文字按钮 self.style.map('TButton', background=[('active', '#3b99e0')]) self.style.configure('TNotebook', background='#f0f0f0', borderwidth=0) self.style.configure('TNotebook.Tab', background='#e6f0ff', foreground='#333333', padding=[10, 5]) # 淡蓝色标签 self.style.map('TNotebook.Tab', background=[('selected', '#4dabf5')]) # 初始化数据 self.documents = [] self.chunks = [] self.embeddings = [] self.qa_history = [] # 获取 Ollama 中已安装的模型列表 try: models_response = ollama.list() self.all_models = [model['model'] for model in models_response['models']] # 使用 'model' 字段 except Exception as e: print(f"获取 Ollama 模型列表失败: {e}") self.all_models = [] self.default_llm_model = "gemma3:12b" self.default_embedding_model = "bge-m3:latest" # 默认参数 self.params = { "temperature": 0.7, "top_p": 0.9, "max_length": 2048, "num_context_docs": 3, "chunk_size": 500, "chunk_overlap": 100, "chunk_strategy": "固定大小", "separators": "\n\n,。,!,?,\n, ", # 默认分隔符 "embed_batch_size": 1, "enable_stream": True, "show_progress": True, "show_visualization": True, "ocr_enabled": True } # 创建界面 self.create_ui() def create_ui(self): # 主框架 self.main_frame = ttk.Frame(self.root) self.main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20) # 标题 title_frame = ttk.Frame(self.main_frame) title_frame.pack(fill=tk.X, pady=(0, 20)) ttk.Label(title_frame, text="✨ 智能RAG应用系统 ✨", font=('Arial', 24, 'bold'), foreground="#4dabf5").pack(side=tk.LEFT) # 淡青色标题 # 状态指示器 status_frame = ttk.Frame(title_frame) status_frame.pack(side=tk.RIGHT) self.status_label = ttk.Label(status_frame, text="● 就绪", foreground="#28a745") # 绿色状态 self.status_label.pack(side=tk.RIGHT, padx=10) # 参数控制面板 self.create_sidebar() # 主内容区域 self.create_main_content() def create_sidebar(self): # 侧边栏框架 self.sidebar = ttk.LabelFrame(self.main_frame, text="⚙️ 参数控制面板", width=300) self.sidebar.pack(side=tk.LEFT, fill=tk.Y, padx=10, pady=10) # 大模型参数 ttk.Label(self.sidebar, text="🔧 大模型参数", font=('Arial', 10, 'bold'), foreground="#333333").pack( pady=(15, 5)) self.temperature = tk.DoubleVar(value=self.params["temperature"]) ttk.Label(self.sidebar, text="温度(temperature)").pack(anchor=tk.W, padx=10) temp_frame = ttk.Frame(self.sidebar) temp_frame.pack(fill=tk.X, padx=10, pady=(0, 5)) ttk.Scale(temp_frame, from_=0.0, to=2.0, variable=self.temperature, length=180, command=lambda v: self.update_param("temperature", float(v))).pack(side=tk.LEFT) self.temp_label = ttk.Label(temp_frame, text=f"{self.temperature.get():.1f}", width=5) self.temp_label.pack(side=tk.RIGHT, padx=5) self.top_p = tk.DoubleVar(value=self.params["top_p"]) ttk.Label(self.sidebar, text="Top P").pack(anchor=tk.W, padx=10) top_p_frame = ttk.Frame(self.sidebar) top_p_frame.pack(fill=tk.X, padx=10, pady=(0, 5)) ttk.Scale(top_p_frame, from_=0.0, to=1.0, variable=self.top_p, length=180, command=lambda v: self.update_param("top_p", float(v))).pack(side=tk.LEFT) self.top_p_label = ttk.Label(top_p_frame, text=f"{self.top_p.get():.2f}", width=5) self.top_p_label.pack(side=tk.RIGHT, padx=5) # 添加大模型名称选择(来自 Ollama) ttk.Label(self.sidebar, text="大模型名称").pack(anchor=tk.W, padx=10) self.llm_model_var = tk.StringVar(value=self.default_llm_model) llm_combobox = ttk.Combobox(self.sidebar, textvariable=self.llm_model_var, values=self.all_models) llm_combobox.pack(padx=10, pady=5) # 嵌入模型选择(来自 Ollama) ttk.Label(self.sidebar, text="嵌入模型名称").pack(anchor=tk.W, padx=10) self.embedding_model_var = tk.StringVar(value=self.default_embedding_model) embed_combobox = ttk.Combobox(self.sidebar, textvariable=self.embedding_model_var, values=self.all_models) embed_combobox.pack(padx=10, pady=5) # RAG参数 ttk.Label(self.sidebar, text="🔧 RAG参数", font=('Arial', 10, 'bold'), foreground="#333333").pack(pady=(15, 5)) # 分块策略选择 ttk.Label(self.sidebar, text="分块策略").pack(anchor=tk.W, padx=10) self.chunk_strategy_var = tk.StringVar(value=self.params["chunk_strategy"]) strategy_combobox = ttk.Combobox(self.sidebar, textvariable=self.chunk_strategy_var, values=["固定大小", "按分隔符"]) strategy_combobox.pack(padx=10, pady=5) strategy_combobox.bind("<<ComboboxSelected>>", self.toggle_separators_visibility) # 分隔符配置(默认隐藏) self.separators_frame = ttk.Frame(self.sidebar) ttk.Label(self.separators_frame, text="分隔符配置:").pack(anchor=tk.W, padx=5) # 创建下拉列表框替代文本框 self.separators_var = tk.StringVar(value=self.params["separators"]) self.separator_options = ["换行符", "句号", "空格", "无", "换行符+句号", "换行符+空格", "句号+空格", "自定义"] separators_combobox = ttk.Combobox( self.separators_frame, textvariable=self.separators_var, values=self.separator_options, width=20, state="readonly" ) separators_combobox.pack(padx=5, pady=5, fill=tk.X) separators_combobox.bind("<<ComboboxSelected>>", self.on_separator_selected) # 初始状态:如果当前策略是"按分隔符"则显示 if self.params["chunk_strategy"] == "按分隔符": self.separators_frame.pack(fill=tk.X, padx=10, pady=5) else: self.separators_frame.pack_forget() # 分块大小配置 self.chunk_size = tk.IntVar(value=self.params["chunk_size"]) ttk.Label(self.sidebar, text="分块大小(字符)").pack(anchor=tk.W, padx=10) chunk_frame = ttk.Frame(self.sidebar) chunk_frame.pack(fill=tk.X, padx=10, pady=(0, 5)) ttk.Scale(chunk_frame, from_=100, to=2000, variable=self.chunk_size, length=180, command=lambda v: self.update_param("chunk_size", int(v))).pack(side=tk.LEFT) self.chunk_label = ttk.Label(chunk_frame, text=f"{self.chunk_size.get()}", width=5) self.chunk_label.pack(side=tk.RIGHT, padx=5) # OCR开关 self.ocr_var = tk.BooleanVar(value=self.params["ocr_enabled"]) ttk.Checkbutton(self.sidebar, text="启用OCR扫描", variable=self.ocr_var, command=lambda: self.update_param("ocr_enabled", self.ocr_var.get())).pack(pady=(15, 5), padx=10, anchor=tk.W) # 使用说明 ttk.Label(self.sidebar, text="📖 使用说明", font=('Arial', 10, 'bold'), foreground="#333333").pack(pady=(15, 5)) instructions = """1. 在"文档上传"页上传您的文档 2. 在"文档处理"页对文档进行分块嵌入 3. 在"问答交互"页提问并获取答案 4. 在"系统监控"页查看系统状态""" ttk.Label(self.sidebar, text=instructions, justify=tk.LEFT, background="#e6f0ff", # 淡蓝色背景 foreground="#333333", padding=10).pack(fill=tk.X, padx=10, pady=5) def on_separator_selected(self, event): """处理分隔符选择事件""" selected = self.separators_var.get() # 映射选项到实际分隔符 separator_map = { "换行符": "\n", "句号": "。", "空格": " ", "无": "", "换行符+句号": "\n,。", "换行符+空格": "\n, ", "句号+空格": "。, " } if selected in separator_map: self.params["separators"] = separator_map[selected] elif selected == "自定义": # 弹出对话框让用户输入自定义分隔符 custom_sep = simpledialog.askstring("自定义分隔符", "请输入分隔符(多个用逗号分隔):", parent=self.root) if custom_sep: self.params["separators"] = custom_sep def toggle_separators_visibility(self, event=None): """根据分块策略显示或隐藏分隔符配置""" strategy = self.chunk_strategy_var.get() self.update_param("chunk_strategy", strategy) if strategy == "按分隔符": self.separators_frame.pack(fill=tk.X, padx=10, pady=5) else: self.separators_frame.pack_forget() def create_main_content(self): # 主内容框架 self.content_frame = ttk.Frame(self.main_frame) self.content_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) # 创建选项卡 self.notebook = ttk.Notebook(self.content_frame) self.notebook.pack(fill=tk.BOTH, expand=True) # 文档上传页 self.create_upload_tab() # 文档处理页 self.create_process_tab() # 问答交互页 self.create_qa_tab() # 系统监控页 self.create_monitor_tab() def create_upload_tab(self): self.upload_tab = ttk.Frame(self.notebook) self.notebook.add(self.upload_tab, text="📤 文档上传") # 标题 title_frame = ttk.Frame(self.upload_tab) title_frame.pack(fill=tk.X, pady=(10, 20)) ttk.Label(title_frame, text="📤 文档上传与管理", font=('Arial', 14, 'bold'), foreground="#4dabf5").pack(side=tk.LEFT) # 淡青色标题 # 上传区域 upload_frame = ttk.Frame(self.upload_tab) # 修正:ttk.Frame 而不是 tttk.Frame upload_frame.pack(fill=tk.X, pady=10) # 上传按钮 upload_btn = ttk.Button(upload_frame, text="📁 上传文档", command=self.upload_files, style='Accent.TButton') upload_btn.pack(side=tk.LEFT, padx=10) # 清除按钮 clear_btn = ttk.Button(upload_frame, text="🗑️ 清除所有", command=self.clear_documents) clear_btn.pack(side=tk.RIGHT, padx=10) # 文档列表 self.doc_list_frame = ttk.LabelFrame(self.upload_tab, text="📋 已上传文档") self.doc_list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 创建带滚动条的树状视图 tree_frame = ttk.Frame(self.doc_list_frame) tree_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 创建滚动条 tree_scroll = ttk.Scrollbar(tree_frame) tree_scroll.pack(side=tk.RIGHT, fill=tk.Y) # 创建树状视图 columns = ("name", "size", "time", "type") self.doc_tree = ttk.Treeview(tree_frame, columns=columns, show="headings", yscrollcommand=tree_scroll.set, height=8) # 设置列标题 self.doc_tree.heading("name", text="文件名") self.doc_tree.heading("size", text="大小") self.doc_tree.heading("time", text="上传时间") self.doc_tree.heading("type", text="类型") # 设置列宽 self.doc_tree.column("name", width=250) self.doc_tree.column("size", width=80) self.doc_tree.column("time", width=150) self.doc_tree.column("type", width=80) self.doc_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) tree_scroll.config(command=self.doc_tree.yview) # 文档统计 self.doc_stats_frame = ttk.Frame(self.upload_tab) self.doc_stats_frame.pack(fill=tk.X, pady=10, padx=10) stats_style = ttk.Style() stats_style.configure('Stats.TLabel', background='#e6f0ff', foreground='#333333', padding=5) # 淡蓝色背景 ttk.Label(self.doc_stats_frame, text="📊 文档统计:", style='Stats.TLabel').pack(side=tk.LEFT, padx=5) self.doc_count_label = ttk.Label(self.doc_stats_frame, text="0", style='Stats.TLabel') self.doc_count_label.pack(side=tk.LEFT, padx=5) ttk.Label(self.doc_stats_frame, text="总字符数:", style='Stats.TLabel').pack(side=tk.LEFT, padx=5) self.char_count_label = ttk.Label(self.doc_stats_frame, text="0", style='Stats.TLabel') self.char_count_label.pack(side=tk.LEFT, padx=5) ttk.Label(self.doc_stats_frame, text="总页数:", style='Stats.TLabel').pack(side=tk.LEFT, padx=5) self.page_count_label = ttk.Label(self.doc_stats_frame, text="0", style='Stats.TLabel') self.page_count_label.pack(side=tk.LEFT, padx=5) def clear_documents(self): if not self.documents: return if messagebox.askyesno("确认", "确定要清除所有文档吗?"): self.documents = [] self.update_doc_list() # ================== 文件读取函数 ================== def read_pdf(self, filepath): """读取PDF文件内容,支持扫描版OCR""" content = "" pages = 0 try: with open(filepath, 'rb') as f: reader = PyPDF2.PdfReader(f) num_pages = len(reader.pages) pages = num_pages for page_num in range(num_pages): page = reader.pages[page_num] text = page.extract_text() # 如果是扫描版PDF,使用OCR识别 if not text.strip() and self.params["ocr_enabled"]: try: # 获取页面图像 images = page.images if images: for img in images: image_data = img.data image = Image.open(io.BytesIO(image_data)) text += pytesseract.image_to_string(image, lang='chi_sim+eng') except Exception as e: print(f"OCR处理失败: {str(e)}") content += text + "\n" except Exception as e: print(f"读取PDF失败: {str(e)}") return content, pages def read_docx(self, filepath): """读取Word文档内容""" content = "" pages = 0 try: doc = docx.Document(filepath) for para in doc.paragraphs: content += para.text + "\n" pages = len(doc.paragraphs) // 50 + 1 # 估算页数 except Exception as e: print(f"读取Word文档失败: {str(e)}") return content, pages def read_excel(self, filepath): """读取Excel文件内容,优化内存使用""" content = "" pages = 0 try: # 使用openpyxl优化大文件读取 wb = openpyxl.load_workbook(filepath, read_only=True) for sheet_name in wb.sheetnames: content += f"\n工作表: {sheet_name}\n" sheet = wb[sheet_name] for row in sheet.iter_rows(values_only=True): row_content = " | ".join([str(cell) if cell is not None else "" for cell in row]) content += row_content + "\n" pages = len(wb.sheetnames) except Exception as e: print(f"读取Excel文件失败: {str(e)}") return content, pages def read_md(self, filepath): """读取Markdown文件内容""" content = "" pages = 0 try: with open(filepath, 'r', encoding='utf-8') as f: html = markdown.markdown(f.read()) soup = BeautifulSoup(html, 'html.parser') content = soup.get_text() pages = len(content) // 2000 + 1 # 估算页数 except Exception as e: print(f"读取Markdown文件失败: {str(e)}") return content, pages def read_ppt(self, filepath): """读取PPT文件内容(简化版)""" content = "" pages = 0 try: # 实际应用中应使用python-pptx库 # 这里仅作演示 content = f"PPT文件内容提取: {os.path.basename(filepath)}" pages = 10 # 假设有10页 except Exception as e: print(f"读取PPT文件失败: {str(e)}") return content, pages # ...(前面的代码保持不变)... def upload_files(self): filetypes = [ ("文本文件", "*.txt"), ("PDF文件", "*.pdf"), ("Word文件", "*.docx *.doc"), ("Excel文件", "*.xlsx *.xls"), ("Markdown文件", "*.md"), ("PPT文件", "*.pptx *.ppt"), ("所有文件", "*.*") ] filenames = filedialog.askopenfilenames(title="选择文档", filetypes=filetypes) if filenames: self.status_label.config(text="● 正在上传文档...", foreground="#ffc107") total_pages = 0 for filename in filenames: try: ext = os.path.splitext(filename)[1].lower() if ext == '.txt': with open(filename, 'r', encoding='utf-8') as f: content = f.read() pages = len(content) // 2000 + 1 elif ext == '.pdf': content, pages = self.read_pdf(filename) elif ext in ('.docx', '.doc'): content, pages = self.read_docx(filename) elif ext in ('.xlsx', '.xls'): content, pages = self.read_excel(filename) elif ext == '.md': content, pages = self.read_md(filename) elif ext in ('.pptx', '.ppt'): content, pages = self.read_ppt(filename) else: messagebox.showwarning("警告", f"不支持的文件类型: {ext}") continue # 处理字符编码问题 if not isinstance(content, str): try: content = content.decode('utf-8') except: content = content.decode('latin-1', errors='ignore') self.documents.append({ "name": os.path.basename(filename), "content": content, "size": len(content), "upload_time": time.strftime("%Y-%m-%d %H:%M:%S"), "type": ext.upper().replace(".", ""), "pages": pages }) total_pages += pages # 更文档列表 self.update_doc_list() except Exception as e: messagebox.showerror("错误", f"无法读取文件 {filename}: {str(e)}") # 更检索文档数量 if hasattr(self, 'req_docs_entry'): self.req_docs_entry.delete(0, tk.END) self.req_docs_entry.insert(0, str(len(self.documents))) self.params["num_context_docs"] = len(self.documents) self.status_label.config(text=f"● 上传完成! 共{len(filenames)}个文档", foreground="#28a745") self.page_count_label.config(text=str(total_pages)) # ...(后面的代码保持不变)... def update_doc_list(self): # 清空现有列表 for item in self.doc_tree.get_children(): self.doc_tree.delete(item) # 添加文档 for doc in self.documents: size_kb = doc["size"] / 1024 size_str = f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb / 1024:.1f} MB" self.doc_tree.insert("", tk.END, values=( doc["name"], size_str, doc["upload_time"], doc["type"] )) # 更统计信息 self.doc_count_label.config(text=str(len(self.documents))) self.char_count_label.config(text=str(sum(d['size'] for d in self.documents))) def create_process_tab(self): self.process_tab = ttk.Frame(self.notebook) self.notebook.add(self.process_tab, text="🔧 文档处理") # 标题 title_frame = ttk.Frame(self.process_tab) title_frame.pack(fill=tk.X, pady=(10, 20)) ttk.Label(title_frame, text="🔧 文档处理与分块", font=('Arial', 14, 'bold'), foreground="#4dabf5").pack(side=tk.LEFT) # 淡青色标题 # 处理按钮 btn_frame = ttk.Frame(self.process_tab) btn_frame.pack(fill=tk.X, pady=10) process_btn = ttk.Button(btn_frame, text="🔄 处理文档", command=self.process_documents, style='Accent.TButton') process_btn.pack(side=tk.LEFT, padx=10) visualize_btn = ttk.Button(btn_frame, text="📊 更可视化", command=self.show_visualizations) visualize_btn.pack(side=tk.LEFT, padx=10) # 主内容区域 content_frame = ttk.Frame(self.process_tab) # 修正:self.process_tab 而不是 self.process极ab content_frame.pack(fill=tk.BOTH, expand=True) # 左侧:可视化区域 self.visual_frame = ttk.LabelFrame(content_frame, text="📈 文档分析") self.visual_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10) # 右侧:分块列表 self.chunk_frame = ttk.LabelFrame(content_frame, text="📋 分块结果") self.chunk_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10) # 创建带滚动条的树状视图 tree_frame = ttk.Frame(self.chunk_frame) tree_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 创建滚动条 tree_scroll = ttk.Scrollbar(tree_frame) tree_scroll.pack(side=tk.RIGHT, fill=tk.Y) # 创建树状视图 columns = ("doc_name", "start", "end", "content") self.chunk_tree = ttk.Treeview( tree_frame, columns=columns, show="headings", yscrollcommand=tree_scroll.set, height=15 ) # 设置列标题 self.chunk_tree.heading("doc_name", text="来源文档") self.chunk_tree.heading("start", text="起始位置") self.chunk_tree.heading("end", text="结束位置") self.chunk_tree.heading("content", text="内容预览") # 设置列宽 self.chunk_tree.column("doc_name", width=150) self.chunk_tree.column("start", width=80) self.chunk_tree.column("end", width=80) self.chunk_tree.column("content", width=300) self.chunk_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) tree_scroll.config(command=self.chunk_tree.yview) # 初始显示占位图 self.show_placeholder() def show_placeholder(self): """显示可视化占位图""" for widget in self.visual_frame.winfo_children(): widget.destroy() placeholder = ttk.Label(self.visual_frame, text="文档处理后将显示分析图表", font=('Arial', 12), foreground="#7f8c8d") placeholder.pack(expand=True, pady=50) def process_documents(self): if not self.documents: messagebox.showwarning("警告", "请先上传文档") return # 在线程中处理文档 threading.Thread(target=self._process_documents_thread, daemon=True).start() def _process_documents_thread(self): # 显示进度条 self.progress_window = tk.Toplevel(self.root) self.progress_window.title("处理进度") self.progress_window.geometry("400x150") self.progress_window.resizable(False, False) self.progress_window.transient(self.root) self.progress_window.grab_set() self.progress_window.configure(bg="#f0f0f0") # 淡灰色背景 # 设置窗口居中 x = self.root.winfo_x() + (self.root.winfo_width() - 400) // 2 y = self.root.winfo_y() + (self.root.winfo_height() - 150) // 2 self.progress_window.geometry(f"+{x}+{y}") # 进度窗口内容 ttk.Label(self.progress_window, text="正在处理文档...", font=('Arial', 11)).pack(pady=(20, 10)) progress_frame = ttk.Frame(self.progress_window) progress_frame.pack(fill=tk.X, padx=20, pady=10) self.progress_var = tk.DoubleVar() progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100, length=360) progress_bar.pack() self.progress_label = ttk.Label(progress_frame, text="0%") self.progress_label.pack(pady=5) self.status_label.config(text="● 正在处理文档...", foreground="#ffc107") # 黄色状态 self.progress_window.update() try: # 分块处理 self.chunks = self.chunk_documents( self.documents, self.params["chunk_strategy"], self.params["chunk_size"], self.params["chunk_overlap"], self.params["separators"] ) # 生成嵌入 self.embeddings = self.generate_embeddings(self.chunks) # 更UI self.root.after(0, self.update_chunk_list) self.root.after(0, self.show_visualizations) self.root.after(0, lambda: messagebox.showinfo("完成", "文档处理完成!")) self.status_label.config(text="● 文档处理完成", foreground="#28a745") # 绿色状态 except Exception as e: self.root.after(0, lambda: messagebox.showerror("错误", f"处理文档时出错: {str(e)}")) self.status_label.config(text="● 处理出错", foreground="#dc3545") # 红色状态 finally: self.root.after(0, self.progress_window.destroy) def chunk_documents(self, documents, strategy, size, overlap, separators): chunks = [] total_docs = len(documents) for doc_idx, doc in enumerate(documents): content = doc['content'] if strategy == "固定大小": # 固定大小分块策略 for i in range(0, len(content), size - overlap): chunk = content[i:i + size] chunks.append({ "doc_name": doc['name'], "content": chunk, "start": i, "end": min(i + size, len(content)) }) elif strategy == "按分隔符": # 按分隔符分块策略 # 将分隔符字符串拆分成列表 separator_list = [sep.strip() for sep in separators.split(',') if sep.strip()] # 如果没有提供分隔符,使用默认分隔符 if not separator_list: separator_list = ['\n\n', '。', '!', '?', '\n', ' '] # 构建正则表达式模式 pattern = '|'.join([re.escape(sep) for sep in separator_list]) # 使用正则表达式分割文本 parts = re.split(f'({pattern})', content) # 合并片段分隔符 fragments = [] for i in range(0, len(parts), 2): if i < len(parts) - 1: fragments.append(parts[i] + parts[i + 1]) else: fragments.append(parts[i]) # 合并片段直到达到块大小 current_chunk = "" start_index = 0 current_length = 0 for fragment in fragments: frag_len = len(fragment) # 如果当前块加上片段不超过块大小 if current_length + frag_len <= size: current_chunk += fragment current_length += frag_len else: # 保存当前块 if current_chunk: chunks.append({ "doc_name": doc['name'], "content": current_chunk, "start": start_index, "end": start_index + current_length }) start_index += current_length - overlap if start_index < 0: start_index = 0 # 保留重叠部分作为下一块的开头 current_chunk = current_chunk[-overlap:] + fragment current_length = len(current_chunk) else: # 如果当前块为空(片段本身大于块大小) chunks.append({ "doc_name": doc['name'], "content": fragment, "start": start_index, "end": start_index + frag_len }) start_index += frag_len - overlap if start_index < 0: start_index = 0 current_chunk = "" current_length = 0 # 添加最后一个块 if current_chunk: chunks.append({ "doc_name": doc['name'], "content": current_chunk, "start": start_index, "end": start_index + len(current_chunk) }) # 更进度 progress = (doc_idx + 1) / total_docs * 100 self.progress_var.set(progress) self.progress_label.config(text=f"{int(progress)}%") self.progress_window.update() return chunks def generate_embeddings(self, chunks): """单批次处理每个分块,避免API参数类型错误""" embeddings = [] total_chunks = len(chunks) for idx, chunk in enumerate(chunks): try: # 传递单个字符串而不是列表 response = ollama.embeddings( model=self.embedding_model_var.get(), prompt=chunk['content'] # 单个字符串 ) embeddings.append({ "chunk_id": idx, "embedding": response['embedding'], "doc_name": chunk['doc_name'] }) except Exception as e: print(f"生成嵌入时出错: {str(e)}") # 添加空嵌入占位符 embeddings.append({ "chunk_id": idx, "embedding": None, "doc_name": chunk['doc_name'] }) # 更进度 progress = (idx + 1) / total_chunks * 100 self.progress_var.set(progress) self.progress_label.config(text=f"{int(progress)}%") self.progress_window.update() # 添加延迟避免请求过快 time.sleep(0.1) return embeddings def update_chunk_list(self): # 清空现有列表 for item in self.chunk_tree.get_children(): self.chunk_tree.delete(item) # 添加分块 for chunk in self.chunks: preview = chunk['content'][:50] + "..." if len(chunk['content']) > 50 else chunk['content'] self.chunk_tree.insert("", tk.END, values=( chunk['doc_name'], chunk['start'], chunk['end'], preview )) def show_visualizations(self): # 清空可视化区域 for widget in self.visual_frame.winfo_children(): widget.destroy() if not self.params["show_visualization"] or not self.chunks: self.show_placeholder() return # 创建图表框架 fig = plt.Figure(figsize=(10, 8), dpi=100) fig.set_facecolor('#f0f0f0') # 淡灰色背景 # 分块大小分布 ax1 = fig.add_subplot(221) ax1.set_facecolor('#e6f0ff') # 淡蓝色背景 chunk_sizes = [len(c['content']) for c in self.chunks] sns.histplot(chunk_sizes, bins=20, ax=ax1, color='#4dabf5') # 淡青色 ax1.set_title("分块大小分布", color='#333333') ax1.set_xlabel("字符数", color='#333333') # 修正:正确的颜色代码 ax1.set_ylabel("数量", color='#333333') ax1.tick_params(axis='x', colors='#333333') ax1.tick_params(axis='y', colors='#333333') ax1.spines['bottom'].set_color('#333333') ax1.spines['left'].set_color('#333333') # 文档分块数量 ax2 = fig.add_subplot(222) ax2.set_facecolor('#e6f0ff') # 淡蓝色背景 doc_chunk_counts = {} for chunk in self.chunks: doc_chunk_counts[chunk['doc_name']] = doc_chunk_counts.get(chunk['doc_name'], 0) + 1 # 只显示前10个文档 doc_names = list(doc_chunk_counts.keys()) counts = list(doc_chunk_counts.values()) if len(doc_names) > 10: # 按分块数量排序,取前10 sorted_indices = np.argsort(counts)[::-1][:10] doc_names = [doc_names[i] for i in sorted_indices] counts = [counts[i] for i in sorted_indices] sns.barplot(x=counts, y=doc_names, hue=doc_names, ax=ax2, palette='Blues', orient='h', legend=False) ax2.set_title("各文档分块数量", color='#333333') ax2.set_xlabel("分块数", color='#333333') ax2.set_ylabel("") ax2.tick_params(axis='x', colors='#333333') ax2.tick_params(axis='y', colors='#333333') ax2.spines['bottom'].set_color('#333333') ax2.spines['left'].set_color('#333333') # 内容词云(模拟) ax3 = fig.add_subplot(223) ax3.set_facecolor('#e6f0ff') # 淡蓝色背景 ax3.set_title("内容关键词分布", color='#333333') ax3.text(0.5, 0.5, "关键词可视化区域", horizontalalignment='center', verticalalignment='center', color='#333333', fontsize=12) ax3.axis('off') # 处理进度 ax4 = fig.add_subplot(224) ax4.set_facecolor('#e6f0ff') # 淡蓝色背景 ax4.set_title("处理进度", color='#333333') # 模拟数据 stages = ['上传', '分块', '嵌入', '完成'] progress = [100, 100, 100, 100] # 假设都已完成 ax4.barh(stages, progress, color=['#4dabf5', '#20c997', '#9b59b6', '#ffc107']) # 淡色系 ax4.set_xlim(0, 100) ax4.set_xlabel("完成百分比", color='#333333') ax4.tick_params(axis='x', colors='#333333') ax4.tick_params(axis='y', colors='#333333') ax4.spines['bottom'].set_color('#333333') ax4.spines['left'].set_color('#333333') # 调整布局 fig.tight_layout(rect=[0, 0, 1, 0.95], pad=3.0) # 添加总标题 fig.suptitle("文档分析概览", fontsize=16, color='#333333') # 在Tkinter中显示图表 canvas = FigureCanvasTkAgg(fig, master=self.visual_frame) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) def create_qa_tab(self): self.qa_tab = ttk.Frame(self.notebook) self.notebook.add(self.qa_tab, text="💬 问答交互") # 标题 title_frame = ttk.Frame(self.qa_tab) title_frame.pack(fill=tk.X, pady=(10, 20)) ttk.Label(title_frame, text="💬 问答交互", font=('Arial', 14, 'bold'), foreground="#4dabf5").pack(side=tk.LEFT) # 淡青色标题 # 主内容区域 main_frame = ttk.Frame(self.qa_tab) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) # 左侧:问答区域 left_frame = ttk.Frame(main_frame) left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) # 问题输入 self.question_frame = ttk.LabelFrame(left_frame, text="❓ 输入问题") self.question_frame.pack(fill=tk.X, padx=5, pady=5) self.question_text = scrolledtext.ScrolledText(self.question_frame, height=8, wrap=tk.WORD, font=('Arial', 11)) self.question_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.question_text.focus_set() # 提交按钮 btn_frame = ttk.Frame(left_frame) btn_frame.pack(fill=tk.X, pady=10) submit_btn = ttk.Button(btn_frame, text="🚀 提交问题", command=self.submit_question, style='Accent.TButton') submit_btn.pack(side=tk.LEFT, padx=5) clear_btn = ttk.Button(btn_frame, text="🗑️ 清除问题", command=self.clear_question) clear_btn.pack(side=tk.LEFT, padx=5) # 检索文档数量控制 - 放到按钮下方 control_frame = ttk.LabelFrame(left_frame, text="⚙️ 检索设置") control_frame.pack(fill=tk.X, padx=5, pady=5) req_frame = ttk.Frame(control_frame) req_frame.pack(fill=tk.X, padx=5, pady=5) ttk.Label(req_frame, text="检索文档数量:").pack(side=tk.LEFT) self.req_docs_entry = ttk.Entry(req_frame, width=5) self.req_docs_entry.insert(0, str(len(self.documents))) # 默认为文档总数 self.req_docs_entry.pack(side=tk.LEFT, padx=5) ttk.Button(req_frame, text="更", command=self.update_num_context_docs).pack(side=tk.LEFT, padx=5) ttk.Button(req_frame, text="全部", command=self.set_all_docs).pack(side=tk.LEFT) # 回答显示 self.answer_frame = ttk.LabelFrame(left_frame, text="💡 回答") self.answer_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.answer_text = scrolledtext.ScrolledText(self.answer_frame, state=tk.DISABLED, wrap=tk.WORD, font=('Arial', 11)) self.answer_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 右侧:问答历史 right_frame = ttk.Frame(main_frame, width=400) # 设置宽度 right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=False, padx=5, pady=5) self.history_frame = ttk.LabelFrame(right_frame, text="🕒 问答历史") self.history_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 创建带滚动条的树状视图 tree_frame = ttk.Frame(self.history_frame) tree_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 创建滚动条 tree_scroll = ttk.Scrollbar(tree_frame) tree_scroll.pack(side=tk.RIGHT, fill=tk.Y) # 创建树状视图 columns = ("question", "time") self.history_tree = ttk.Treeview( tree_frame, columns=columns, show="headings", yscrollcommand=tree_scroll.set, height=20 ) # 设置列标题 self.history_tree.heading("question", text="问题") self.history_tree.heading("time", text="时间") # 设置列宽 self.history_tree.column("question", width=250) self.history_tree.column("time", width=120) self.history_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) tree_scroll.config(command=self.history_tree.yview) # 历史操作按钮 history_btn_frame = ttk.Frame(right_frame) history_btn_frame.pack(fill=tk.X, pady=10) view_btn = ttk.Button(history_btn_frame, text="👁️ 查看详情", command=lambda: self.show_history_detail(None)) view_btn.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) clear_history_btn = ttk.Button(history_btn_frame, text="🗑️ 清除历史", command=self.clear_history) clear_history_btn.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) # 绑定双击事件查看历史详情 self.history_tree.bind("<Double-1>", self.show_history_detail) def update_num_context_docs(self): try: value = int(self.req_docs_entry.get()) if value > 0: self.params["num_context_docs"] = value messagebox.showinfo("提示", f"已更检索文档数量为: {value}") else: raise ValueError except: messagebox.showerror("错误", "请输入有效的正整数") def set_all_docs(self): if self.documents: self.req_docs_entry.delete(0, tk.END) self.req_docs_entry.insert(0, str(len(self.documents))) self.params["num_context_docs"] = len(self.documents) messagebox.showinfo("提示", f"已更检索文档数量为: {len(self.documents)}") else: messagebox.showwarning("警告", "请先上传文档") def clear_question(self): self.question_text.delete("1.0", tk.END) def clear_history(self): if not self.qa_history: return if messagebox.askyesno("确认", "确定要清除所有问答历史吗?"): self.qa_history = [] self.update_history_list() def submit_question(self): question = self.question_text.get("1.0", tk.END).strip() if not question: messagebox.showwarning("警告", "问题不能为空") return # 在线程中处理问题 threading.Thread(target=self._submit_question_thread, args=(question,), daemon=True).start() def _submit_question_thread(self, question): try: # 显示进度窗口 self.progress_window = tk.Toplevel(self.root) self.progress_window.title("处理中...") self.progress_window.geometry("400x150") self.progress_window.resizable(False, False) self.progress_window.transient(self.root) self.progress_window.grab_set() self.progress_window.configure(bg="#f0f0f0") # 淡灰色背景 # 设置窗口居中 x = self.root.winfo_x() + (self.root.winfo_width() - 400) // 2 y = self.root.winfo_y() + (self.root.winfo_height() - 150) // 2 self.progress_window.geometry(f"+{x}+{y}") # 进度窗口内容 ttk.Label(self.progress_window, text="正在思考中...", font=('Arial', 11)).pack(pady=(20, 10)) progress_frame = ttk.Frame(self.progress_window) progress_frame.pack(fill=tk.X, padx=20, pady=10) self.progress_var = tk.DoubleVar() progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100, length=360) # 修正:100 而不是 极00 progress_bar.pack() self.progress_label = ttk.Label(progress_frame, text="0%") self.progress_label.pack(pady=5) self.status_label.config(text="● 正在处理问题...", foreground="#ffc107") # 黄色状态 self.progress_window.update() # 检索相关文档块 relevant_chunks = self.retrieve_relevant_chunks(question, self.params["num_context_docs"]) # 构建上下文 context = "\n\n".join([ f"文档: {c['doc_name']}\n内容: {c['content']}\n相关性: {c['similarity']:.4f}" for c in relevant_chunks ]) # 调用大模型生成回答 prompt = f"""基于以下上下文,回答问题。如果答案不在上下文中,请回答"我不知道"。 上下文: {context} 问题: {question} 回答:""" # 更进度 self.progress_var.set(50) self.progress_label.config(text="50%") self.progress_window.update() # 流式输出或一次性输出 self.root.after(0, self.answer_text.config, {'state': tk.NORMAL}) self.root.after(0, self.answer_text.delete, "1.0", tk.END) if self.params["enable_stream"]: full_response = "" for chunk in ollama.generate( model=self.llm_model_var.get(), # 使用用户选择的模型 prompt=prompt, stream=True, options={ 'temperature': self.params["temperature"], 'top_p': self.params["top_p"], 'num_ctx': self.params["max_length"] } ): full_response += chunk['response'] self.root.after(0, self.answer_text.insert, tk.END, chunk['response']) self.root.after(0, self.answer_text.see, tk.END) self.root.after(0, self.answer_text.update) # 更进度 if len(full_response) > 0: progress = min(50 + len(full_response) / 200, 99) self.progress_var.set(progress) self.progress_label.config(text=f"{int(progress)}%") self.progress_window.update() else: response = ollama.generate( model=self.llm_model_var.get(), # 使用用户选择的模型 prompt=prompt, options={ 'temperature': self.params["temperature"], 'top_p': self.params["top_p"], 'num_ctx': self.params["max_length"] } ) full_response = response['response'] self.root.after(0, self.answer_text.insert, tk.END, full_response) # 记录问答历史 self.qa_history.append({ "question": question, "answer": full_response, "context": context, "time": time.strftime("%Y-%m-%d %H:%M:%S") }) # 更历史列表 self.root.after(0, self.update_history_list) # 完成 self.progress_var.set(100) self.progress_label.config(text="100%") self.status_label.config(text="● 问题处理完成", foreground="#28a745") # 绿色状态 self.root.after(1000, self.progress_window.destroy) except Exception as e: self.root.after(0, lambda: messagebox.showerror("错误", f"处理问题时出错: {str(e)}")) self.root.after(0, self.progress_window.destroy) self.status_label.config(text="● 处理出错", foreground="#dc3545") # 红色状态 def retrieve_relevant_chunks(self, query, k): """处理嵌入为None的情况""" # 生成查询的嵌入 query_embedding = ollama.embeddings( model=self.embedding_model_var.get(), # 使用用户选择的模型 prompt=query )['embedding'] # 注意:返回的是字典中的'embedding'字段 # 计算相似极 similarities = [] for emb in self.embeddings: # 跳过无效的嵌入 if emb['embedding'] is None: continue # 计算余弦相似度 similarity = np.dot(query_embedding, emb['embedding']) similarities.append({ 'chunk_id': emb['chunk_id'], 'similarity': similarity, 'doc_name': emb['doc_name'] }) # 按相似度排序并返回前k个 top_chunks = sorted(similarities, key=lambda x: x['similarity'], reverse=True)[:k] return [{ **self.chunks[c['chunk_id']], 'similarity': c['similarity'] } for c in top_chunks] def update_history_list(self): # 清空现有列表 for item in self.history_tree.get_children(): self.history_tree.delete(item) # 添加历史记录 for i, qa in enumerate(reversed(self.qa_history)): # 截断长问题 question = qa["question"] if len(question) > 50: question = question[:47] + "..." self.history_tree.insert("", tk.END, values=(question, qa["time"])) def show_history_detail(self, event): selected_item = self.history_tree.selection() if not selected_item: return item = self.history_tree.item(selected_item) question = item['values'][0] # 查找对应的问答记录 for qa in reversed(self.qa_history): if qa["question"].startswith(question) or question.startswith(qa["question"][:50]): # 显示详情窗口 detail_window = tk.Toplevel(self.root) detail_window.title("问答详情") detail_window.geometry("900x700") detail_window.configure(bg='#f0f0f0') # 淡灰色背景 # 设置窗口居中 x = self.root.winfo_x() + (self.root.winfo_width() - 900) // 2 y = self.root.winfo_y() + (self.root.winfo_height() - 700) // 2 detail_window.geometry(f"+{x}+{y}") # 问题 ttk.Label(detail_window, text="问题:", font=('Arial', 12, 'bold'), foreground="#4dabf5").pack(pady=(15, 5), padx=20, anchor=tk.W) question_frame = ttk.Frame(detail_window) question_frame.pack(fill=tk.X, padx=20, pady=(0, 10)) question_text = scrolledtext.ScrolledText(question_frame, wrap=tk.WORD, height=3, font=('Arial', 11)) question_text.insert(tk.INSERT, qa["question"]) question_text.config(state=tk.DISABLED) question_text.pack(fill=tk.X) # 回答 ttk.Label(detail_window, text="回答:", font=('Arial', 12, 'bold'), foreground="#4dabf5").pack(pady=(15, 5), padx=20, anchor=tk.W) answer_frame = ttk.Frame(detail_window) answer_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 10)) answer_text = scrolledtext.ScrolledText(answer_frame, wrap=tk.WORD, font=('Arial', 11)) answer_text.insert(tk.INSERT, qa["answer"]) answer_text.config(state=tk.DISABLED) answer_text.pack(fill=tk.BOTH, expand=True) # 上下文 ttk.Label(detail_window, text="上下文:", font=('Arial', 12, 'bold'), foreground="#4dabf5").pack(pady=(15, 5), padx=20, anchor=tk.W) context_frame = ttk.Frame(detail_window) context_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 20)) context_text = scrolledtext.ScrolledText(context_frame, wrap=tk.WORD, font=('Arial', 10)) context_text.insert(tk.INSERT, qa["context"]) context_text.config(state=tk.DISABLED) context_text.pack(fill=tk.BOTH, expand=True) break def create_monitor_tab(self): self.monitor_tab = ttk.Frame(self.notebook) self.notebook.add(self.monitor_tab, text="📊 系统监控") # 标题 title_frame = ttk.Frame(self.monitor_tab) title_frame.pack(fill=tk.X, pady=(10, 20)) ttk.Label(title_frame, text="📊 系统监控", font=('Arial', 14, 'bold'), foreground="#4dabf5").pack(side=tk.LEFT) # 主内容区域 main_frame = ttk.Frame(self.monitor_tab) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) # 左侧:资源监控 left_frame = ttk.Frame(main_frame) left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) # 资源使用 self.resource_frame = ttk.LabelFrame(left_frame, text="📈 资源使用") self.resource_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # CPU使用 cpu_frame = ttk.Frame(self.resource_frame) cpu_frame.pack(fill=tk.X, padx=10, pady=10) ttk.Label(cpu_frame, text="CPU使用率:").pack(side=tk.LEFT) self.cpu_value = ttk.Label(cpu_frame, text="0%", width=5) self.cpu_value.pack(side=tk.RIGHT, padx=10) self.cpu_usage = ttk.Progressbar(self.resource_frame, length=400, mode='determinate') # 修正:正确的变量名 self.cpu_usage.pack(fill=tk.X, padx=10, pady=(0, 10)) # 内存使用 mem_frame = ttk.Frame(self.resource_frame) mem_frame.pack(fill=tk.X, padx=10, pady=10) ttk.Label(mem_frame, text="内存使用率:").pack(side=tk.LEFT) self.mem_value = ttk.Label(mem_frame, text="0%", width=5) self.mem_value.pack(side=tk.RIGHT, padx=10) self.mem_usage = ttk.Progressbar(self.resource_frame, length=400, mode='determinate') self.mem_usage.pack(fill=tk.X, padx=10, pady=(0, 10)) # 磁盘使用 disk_frame = ttk.Frame(self.resource_frame) disk_frame.pack(fill=tk.X, padx=10, pady=10) ttk.Label(disk_frame, text="磁盘使用率:").pack(side=tk.LEFT) self.disk_value = ttk.Label(disk_frame, text="0%", width=5) self.disk_value.pack(side=tk.RIGHT, padx=10) self.disk_usage = ttk.Progressbar(self.resource_frame, length=400, mode='determinate') self.disk_usage.pack(fill=tk.X, padx=10, pady=(0, 10)) # GPU 使用情况 gpu_frame = ttk.Frame(self.resource_frame) gpu_frame.pack(fill=tk.X, padx=10, pady=10) ttk.Label(gpu_frame, text="GPU使用率:").pack(side=tk.LEFT) self.gpu_value = ttk.Label(gpu_frame, text="0%", width=5) self.gpu_value.pack(side=tk.RIGHT, padx=10) self.gpu_usage = ttk.Progressbar(self.resource_frame, length=400, mode='determinate') self.gpu_usage.pack(fill=tk.X, padx=10, pady=(0, 10)) # 右侧:模型状态 right_frame = ttk.Frame(main_frame) right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5) # 模型状态 self.model_frame = ttk.LabelFrame(right_frame, text="🤖 模型状态") self.model_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) btn_frame = ttk.Frame(self.model_frame) btn_frame.pack(fill=tk.X, padx=10, pady=10) ttk.Button(btn_frame, text="🔄 检查模型状态", command=self.check_model_status).pack() self.model_status_text = scrolledtext.ScrolledText(self.model_frame, height=15, state=tk.DISABLED, font=('Consolas', 10)) self.model_status_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) # 性能统计 self.perf_frame = ttk.LabelFrame(left_frame, text="⚡ 性能统计") self.perf_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 创建图表 fig = Figure(figsize=(8, 4), dpi=100) fig.set_facecolor('#f0f0f0') # 淡灰色背景 self.ax = fig.add_subplot(111) self.ax.set_facecolor('#e6f0ff') # 淡蓝色背景 self.ax.set_title("CPU使用率历史", color='#333333') self.ax.set_xlabel("时间", color='#333333') self.ax.set_ylabel("使用率(%)", color='#333333') self.ax.tick_params(axis='x', colors='#333333') self.ax.tick_params(axis='y', colors='#333333') self.ax.spines['bottom'].set_color('#333333') self.ax.spines['left'].set_color('#333333') self.cpu_history = [] self.line, = self.ax.plot([], [], color='#4dabf5', marker='o', markersize=4) # 淡青色线条 self.ax.set_ylim(0, 100) canvas = FigureCanvasTkAgg(fig, master=self.perf_frame) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # GPU 使用历史图表 self.gpu_fig = Figure(figsize=(8, 4), dpi=100) self.gpu_fig.set_facecolor('#f0f0f0') self.gpu_ax = self.gpu_fig.add_subplot(111) self.gpu_ax.set_facecolor('#e6f0ff') self.gpu_ax.set_title("GPU使用率历史", color='#333333') self.gpu_ax.set_xlabel("时间", color='#333333') self.gpu_ax.set_ylabel("使用率(%)", color='#333333') self.gpu_ax.tick_params(axis='x', colors='#333333') self.gpu_ax.tick_params(axis='y', colors='#333333') self.gpu_ax.spines['bottom'].set_color('#333333') self.gpu_ax.spines['left'].set_color('#333333') self.gpu_history = [] self.gpu_line, = self.gpu_ax.plot([], [], color='#9b59b6', marker='o', markersize=4) # 紫色线条 self.gpu_ax.set_ylim(0, 100) gpu_canvas = FigureCanvasTkAgg(self.gpu_fig, master=self.perf_frame) gpu_canvas.draw() gpu_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 开始更资源使用情况 self.update_resource_usage() def update_resource_usage(self): # 获取真实资源数据 cpu_percent = psutil.cpu_percent() mem_percent = psutil.virtual_memory().percent disk_percent = psutil.disk_usage('/').percent # 获取GPU使用情况 try: handle = pynvml.nvmlDeviceGetHandleByIndex(0) info = pynvml.nvmlDeviceGetUtilizationRates(handle) gpu_percent = info.gpu except: gpu_percent = 0 # 更进度条 self.cpu_usage['value'] = cpu_percent self.mem_usage['value'] = mem_percent self.disk_usage['value'] = disk_percent self.gpu_usage['value'] = gpu_percent # 更数值标签 self.cpu_value.config(text=f"{cpu_percent}%") self.mem_value.config(text=f"{mem_percent}%") self.disk_value.config(text=f"{disk_percent}%") self.gpu_value.config(text=f"{gpu_percent}%") # 更CPU历史图表 self.cpu_history.append(cpu_percent) if len(self.cpu_history) > 20: self.cpu_history.pop(0) self.line.set_data(range(len(self.cpu_history)), self.cpu_history) self.ax.set_xlim(0, max(10, len(self.cpu_history))) self.ax.figure.canvas.draw() # 更GPU历史图表 self.gpu_history.append(gpu_percent) if len(self.gpu_history) > 20: self.gpu_history.pop(0) self.gpu_line.set_data(range(len(self.gpu_history)), self.gpu_history) self.gpu_ax.set_xlim(0, max(10, len(self.gpu_history))) self.gpu_fig.canvas.draw() # 5秒后再次更 self.root.after(5000, self.update_resource_usage) def check_model_status(self): try: self.model_status_text.config(state=tk.NORMAL) self.model_status_text.delete("1.0", tk.END) # 添加加载动画 self.model_status_text.insert(tk.INSERT, "正在检查模型状态...") self.model_status_text.update() # 模拟检查过程 time.sleep(1) # 清空并插入真实信息 self.model_status_text.delete("1.0", tk.END) llm_info = ollama.show(self.llm_model_var.get()) # 使用用户选择的模型 embed_info = ollama.show(self.embedding_model_var.get()) # 使用用户选择的模型 status_text = f"""✅ 大模型信息: 名称: {self.llm_model_var.get()} 参数大小: {llm_info.get('size', '未知')} 最后使用时间: {llm_info.get('modified_at', '未知')} 支持功能: {llm_info.get('capabilities', '未知')} ✅ 嵌入模型信息: 名称: {self.embedding_model_var.get()} 参数大小: {embed_info.get('size', '未知')} 最后使用时间: {embed_info.get('modified_at', '未知')} 支持功能: {embed_info.get('capabilities', '未知')} ⏱️ 最后检查时间: {time.strftime("%Y-%m-%d %H:%M:%S")} """ self.model_status_text.insert(tk.INSERT, status_text) self.model_status_text.config(state=tk.DISABLED) self.status_label.config(text="● 模型状态检查完成", foreground="#28a745") # 绿色状态 except Exception as e: self.model_status_text.config(state=tk.NORMAL) self.model_status_text.delete("1.0", tk.END) self.model_status_text.insert(tk.INSERT, f"❌ 检查模型状态时出错: {str(e)}") self.model_status_text.config(state=tk.DISABLED) self.status_label.config(text="● 模型检查出错", foreground="#dc3545") # 红色状态 def update_param(self, param, value): self.params[param] = value # 更标签显示 if param == "temperature": self.temp_label.config(text=f"{value:.1f}") elif param == "top_p": self.top_p_label.config(text=f"{value:.2f}") elif param == "chunk_size": self.chunk_label.config(text=f"{value}") # 运行应用程序 if __name__ == "__main__": root = ThemedTk(theme="arc") # 使用现代主题 app = RAGApplication(root) root.mainloop()
08-10
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wtsolutions

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值