diff --git a/lightrag_webui/src/components/AppSettings.tsx b/lightrag_webui/src/components/AppSettings.tsx new file mode 100644 index 00000000..bc2161d6 --- /dev/null +++ b/lightrag_webui/src/components/AppSettings.tsx @@ -0,0 +1,66 @@ +import { useState, useCallback } from 'react' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover' +import Button from '@/components/ui/Button' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select' +import { useSettingsStore } from '@/stores/settings' +import { PaletteIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +export default function AppSettings() { + const [opened, setOpened] = useState(false) + const { t } = useTranslation() + + const language = useSettingsStore.use.language() + const setLanguage = useSettingsStore.use.setLanguage() + + const theme = useSettingsStore.use.theme() + const setTheme = useSettingsStore.use.setTheme() + + const handleLanguageChange = useCallback((value: string) => { + setLanguage(value as 'en' | 'zh') + }, [setLanguage]) + + const handleThemeChange = useCallback((value: string) => { + setTheme(value as 'light' | 'dark' | 'system') + }, [setTheme]) + + return ( + + + + + +
+
+ + +
+ +
+ + +
+
+
+
+ ) +} diff --git a/lightrag_webui/src/components/ThemeProvider.tsx b/lightrag_webui/src/components/ThemeProvider.tsx index 873b92a4..dfccb795 100644 --- a/lightrag_webui/src/components/ThemeProvider.tsx +++ b/lightrag_webui/src/components/ThemeProvider.tsx @@ -1,4 +1,4 @@ -import { createContext, useEffect, useState } from 'react' +import { createContext, useEffect } from 'react' import { Theme, useSettingsStore } from '@/stores/settings' type ThemeProviderProps = { @@ -21,30 +21,32 @@ const ThemeProviderContext = createContext(initialState) * Component that provides the theme state and setter function to its children. */ export default function ThemeProvider({ children, ...props }: ThemeProviderProps) { - const [theme, setTheme] = useState(useSettingsStore.getState().theme) + const theme = useSettingsStore.use.theme() + const setTheme = useSettingsStore.use.setTheme() useEffect(() => { const root = window.document.documentElement root.classList.remove('light', 'dark') if (theme === 'system') { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light' - root.classList.add(systemTheme) - setTheme(systemTheme) - return + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const handleChange = (e: MediaQueryListEvent) => { + root.classList.remove('light', 'dark') + root.classList.add(e.matches ? 'dark' : 'light') + } + + root.classList.add(mediaQuery.matches ? 'dark' : 'light') + mediaQuery.addEventListener('change', handleChange) + + return () => mediaQuery.removeEventListener('change', handleChange) + } else { + root.classList.add(theme) } - - root.classList.add(theme) }, [theme]) const value = { theme, - setTheme: (theme: Theme) => { - useSettingsStore.getState().setTheme(theme) - setTheme(theme) - } + setTheme } return ( diff --git a/lightrag_webui/src/features/SiteHeader.tsx b/lightrag_webui/src/features/SiteHeader.tsx index ac3bdd70..90330d1a 100644 --- a/lightrag_webui/src/features/SiteHeader.tsx +++ b/lightrag_webui/src/features/SiteHeader.tsx @@ -1,6 +1,6 @@ import Button from '@/components/ui/Button' import { SiteInfo } from '@/lib/constants' -import ThemeToggle from '@/components/ThemeToggle' +import AppSettings from '@/components/AppSettings' import { TabsList, TabsTrigger } from '@/components/ui/Tabs' import { useSettingsStore } from '@/stores/settings' import { cn } from '@/lib/utils' @@ -67,12 +67,14 @@ export default function SiteHeader() { ) diff --git a/lightrag_webui/src/i18n.js b/lightrag_webui/src/i18n.js index 41140bb2..7f1b6718 100644 --- a/lightrag_webui/src/i18n.js +++ b/lightrag_webui/src/i18n.js @@ -1,8 +1,8 @@ -import i18n from "i18next"; -import { initReactI18next } from "react-i18next"; +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; -import en from "./locales/en.json"; -import zh from "./locales/zh.json"; +import en from './locales/en.json'; +import zh from './locales/zh.json'; i18n .use(initReactI18next) @@ -11,8 +11,8 @@ i18n en: { translation: en }, zh: { translation: zh } }, - lng: "en", // default - fallbackLng: "en", + lng: 'zh', // default + fallbackLng: 'zh', interpolation: { escapeValue: false } diff --git a/lightrag_webui/src/locales/en.json b/lightrag_webui/src/locales/en.json index dcfaea9f..679b8620 100644 --- a/lightrag_webui/src/locales/en.json +++ b/lightrag_webui/src/locales/en.json @@ -1,4 +1,11 @@ { + "settings": { + "language": "Language", + "theme": "Theme", + "light": "Light", + "dark": "Dark", + "system": "System" + }, "header": { "documents": "Documents", "knowledgeGraph": "Knowledge Graph", diff --git a/lightrag_webui/src/locales/zh.json b/lightrag_webui/src/locales/zh.json index 5843e0c1..a04eb461 100644 --- a/lightrag_webui/src/locales/zh.json +++ b/lightrag_webui/src/locales/zh.json @@ -1,4 +1,11 @@ { + "settings": { + "language": "语言", + "theme": "主题", + "light": "浅色", + "dark": "深色", + "system": "系统" + }, "header": { "documents": "文档", "knowledgeGraph": "知识图谱", @@ -6,41 +13,41 @@ "api": "API", "projectRepository": "项目仓库", "themeToggle": { - "switchToLight": "切换到亮色主题", - "switchToDark": "切换到暗色主题" + "switchToLight": "切换到浅色主题", + "switchToDark": "切换到深色主题" } }, "documentPanel": { "clearDocuments": { - "button": "清除", - "tooltip": "清除文档", - "title": "清除文档", - "confirm": "您确定要清除所有文档吗?", + "button": "清空", + "tooltip": "清空文档", + "title": "清空文档", + "confirm": "确定要清空所有文档吗?", "confirmButton": "确定", - "success": "文档已成功清除", - "failed": "清除文档失败:\n{{message}}", - "error": "清除文档失败:\n{{error}}" + "success": "文档清空成功", + "failed": "清空文档失败:\n{{message}}", + "error": "清空文档失败:\n{{error}}" }, "uploadDocuments": { "button": "上传", "tooltip": "上传文档", "title": "上传文档", - "description": "拖放文档到此处或点击浏览。", - "uploading": "正在上传 {{name}}: {{percent}}%", - "success": "上传成功:\n{{name}} 上传成功", - "failed": "上传失败:\n{{name}}\n{{message}}", - "error": "上传失败:\n{{name}}\n{{error}}", + "description": "拖拽文件到此处或点击浏览", + "uploading": "正在上传 {{name}}:{{percent}}%", + "success": "上传成功:\n{{name}} 上传完成", + "failed": "上传失败:\n{{name}}\n{{message}}", + "error": "上传失败:\n{{name}}\n{{error}}", "generalError": "上传失败\n{{error}}", - "fileTypes": "支持的文件类型: TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS" + "fileTypes": "支持的文件类型:TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS" }, "documentManager": { "title": "文档管理", "scanButton": "扫描", "scanTooltip": "扫描文档", "uploadedTitle": "已上传文档", - "uploadedDescription": "已上传文档及其状态列表。", - "emptyTitle": "暂无文档", - "emptyDescription": "尚未上传任何文档。", + "uploadedDescription": "已上传文档列表及其状态", + "emptyTitle": "无文档", + "emptyDescription": "还没有上传任何文档", "columns": { "id": "ID", "summary": "摘要", @@ -54,7 +61,7 @@ "status": { "completed": "已完成", "processing": "处理中", - "pending": "待处理", + "pending": "等待中", "failed": "失败" }, "errors": { @@ -74,40 +81,37 @@ "showNodeLabel": "显示节点标签", "nodeDraggable": "节点可拖动", "showEdgeLabel": "显示边标签", - "hideUnselectedEdges": "隐藏未选中边", + "hideUnselectedEdges": "隐藏未选中的边", "edgeEvents": "边事件", "maxQueryDepth": "最大查询深度", "minDegree": "最小度数", "maxLayoutIterations": "最大布局迭代次数", - "apiKey": "API 密钥", - "enterYourAPIkey": "输入您的 API 密钥", + "apiKey": "API密钥", + "enterYourAPIkey": "输入您的API密钥", "save": "保存", "refreshLayout": "刷新布局" }, - "zoomControl": { "zoomIn": "放大", "zoomOut": "缩小", "resetZoom": "重置缩放" }, - "layoutsControl": { "startAnimation": "开始布局动画", "stopAnimation": "停止布局动画", - "layoutGraph": "布局图", + "layoutGraph": "图布局", "layouts": { - "Circular": "环形布局", - "Circlepack": "圆形打包布局", - "Random": "随机布局", - "Noverlaps": "无重叠布局", - "Force Directed": "力导向布局", - "Force Atlas": "力导向图谱布局" + "Circular": "环形", + "Circlepack": "圆形打包", + "Random": "随机", + "Noverlaps": "无重叠", + "Force Directed": "力导向", + "Force Atlas": "力图" } }, - "fullScreenControl": { "fullScreen": "全屏", - "windowed": "窗口模式" + "windowed": "窗口" } }, "statusIndicator": { @@ -119,17 +123,17 @@ "storageInfo": "存储信息", "workingDirectory": "工作目录", "inputDirectory": "输入目录", - "llmConfig": "LLM 配置", - "llmBinding": "LLM 绑定", - "llmBindingHost": "LLM 绑定主机", - "llmModel": "LLM 模型", - "maxTokens": "最大 Token 数", + "llmConfig": "LLM配置", + "llmBinding": "LLM绑定", + "llmBindingHost": "LLM绑定主机", + "llmModel": "LLM模型", + "maxTokens": "最大令牌数", "embeddingConfig": "嵌入配置", "embeddingBinding": "嵌入绑定", "embeddingBindingHost": "嵌入绑定主机", "embeddingModel": "嵌入模型", "storageConfig": "存储配置", - "kvStorage": "KV 存储", + "kvStorage": "KV存储", "docStatusStorage": "文档状态存储", "graphStorage": "图存储", "vectorStorage": "向量存储" @@ -147,90 +151,77 @@ "title": "关系", "id": "ID", "type": "类型", - "source": "源", - "target": "目标", + "source": "源节点", + "target": "目标节点", "properties": "属性" } }, "search": { "placeholder": "搜索节点...", - "message": "以及其它 {count} 项" + "message": "还有 {count} 个" }, "graphLabels": { "selectTooltip": "选择查询标签", "noLabels": "未找到标签", "label": "标签", "placeholder": "搜索标签...", - "andOthers": "以及其它 {count} 个" + "andOthers": "还有 {count} 个" } }, "retrievePanel": { "chatMessage": { "copyTooltip": "复制到剪贴板", - "copyError": "无法复制文本到剪贴板" + "copyError": "复制文本到剪贴板失败" }, - "retrieval": { - "startPrompt": "在下面输入您的查询以开始检索", - "clear": "清除", + "startPrompt": "输入查询开始检索", + "clear": "清空", "send": "发送", - "placeholder": "输入您的查询...", - "error": "错误:无法获取响应" + "placeholder": "输入查询...", + "error": "错误:获取响应失败" }, "querySettings": { - "parametersTitle": "参数设置", + "parametersTitle": "参数", "parametersDescription": "配置查询参数", - "queryMode": "查询模式", - "queryModeTooltip": "选择检索策略:\n• 朴素:不使用高级技术的基本搜索\n• 本地:基于上下文的信息检索\n• 全局:利用全局知识库\n• 混合:结合本地和全局检索\n• 综合:集成知识图谱与向量检索", + "queryModeTooltip": "选择检索策略:\n• Naive:基础搜索,无高级技术\n• Local:上下文相关信息检索\n• Global:利用全局知识库\n• Hybrid:结合本地和全局检索\n• Mix:整合知识图谱和向量检索", "queryModeOptions": { "naive": "朴素", "local": "本地", "global": "全局", "hybrid": "混合", - "mix": "综合" + "mix": "混合" }, - "responseFormat": "响应格式", - "responseFormatTooltip": "定义响应格式。例如:\n• 多个段落\n• 单个段落\n• 项目符号", + "responseFormatTooltip": "定义响应格式。例如:\n• 多段落\n• 单段落\n• 要点", "responseFormatOptions": { - "multipleParagraphs": "多个段落", - "singleParagraph": "单个段落", - "bulletPoints": "项目符号" + "multipleParagraphs": "多段落", + "singleParagraph": "单段落", + "bulletPoints": "要点" }, - - "topK": "Top K 结果数", - "topKTooltip": "要检索的前 K 个项目数量。在“本地”模式下表示实体,在“全局”模式下表示关系", - "topKPlaceholder": "结果数", - - "maxTokensTextUnit": "文本单元最大 Token 数", - "maxTokensTextUnitTooltip": "每个检索到的文本块允许的最大 Token 数", - - "maxTokensGlobalContext": "全局上下文最大 Token 数", - "maxTokensGlobalContextTooltip": "在全局检索中为关系描述分配的最大 Token 数", - - "maxTokensLocalContext": "本地上下文最大 Token 数", - "maxTokensLocalContextTooltip": "在本地检索中为实体描述分配的最大 Token 数", - + "topK": "Top K结果", + "topKTooltip": "检索的顶部项目数。在'local'模式下表示实体,在'global'模式下表示关系", + "topKPlaceholder": "结果数量", + "maxTokensTextUnit": "文本单元最大令牌数", + "maxTokensTextUnitTooltip": "每个检索文本块允许的最大令牌数", + "maxTokensGlobalContext": "全局上下文最大令牌数", + "maxTokensGlobalContextTooltip": "全局检索中关系描述的最大令牌数", + "maxTokensLocalContext": "本地上下文最大令牌数", + "maxTokensLocalContextTooltip": "本地检索中实体描述的最大令牌数", "historyTurns": "历史轮次", - "historyTurnsTooltip": "在响应上下文中考虑的完整对话轮次(用户-助手对)", - "historyTurnsPlaceholder": "历史轮次的数量", - + "historyTurnsTooltip": "响应上下文中考虑的完整对话轮次(用户-助手对)数量", + "historyTurnsPlaceholder": "历史轮次数", "hlKeywords": "高级关键词", - "hlKeywordsTooltip": "检索时优先考虑的高级关键词。请用逗号分隔", + "hlKeywordsTooltip": "检索中优先考虑的高级关键词列表。用逗号分隔", "hlkeywordsPlaceHolder": "输入关键词", - "llKeywords": "低级关键词", - "llKeywordsTooltip": "用于优化检索焦点的低级关键词。请用逗号分隔", - - "onlyNeedContext": "仅需要上下文", - "onlyNeedContextTooltip": "如果为 True,则仅返回检索到的上下文,而不会生成回复", - - "onlyNeedPrompt": "仅需要提示", - "onlyNeedPromptTooltip": "如果为 True,则仅返回生成的提示,而不会生成回复", - + "llKeywordsTooltip": "用于细化检索重点的低级关键词列表。用逗号分隔", + "onlyNeedContext": "仅需上下文", + "onlyNeedContextTooltip": "如果为True,仅返回检索到的上下文而不生成响应", + "onlyNeedPrompt": "仅需提示", + "onlyNeedPromptTooltip": "如果为True,仅返回生成的提示而不产生响应", "streamResponse": "流式响应", - "streamResponseTooltip": "如果为 True,则启用流式输出以获得实时响应" + "streamResponseTooltip": "如果为True,启用实时流式输出响应" } } } diff --git a/lightrag_webui/src/stores/settings.ts b/lightrag_webui/src/stores/settings.ts index d57473e0..6ff61f1a 100644 --- a/lightrag_webui/src/stores/settings.ts +++ b/lightrag_webui/src/stores/settings.ts @@ -6,6 +6,7 @@ import { Message, QueryRequest } from '@/api/lightrag' import { useGraphStore } from '@/stores/graph' type Theme = 'dark' | 'light' | 'system' +type Language = 'en' | 'zh' type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api' interface SettingsState { @@ -48,6 +49,9 @@ interface SettingsState { theme: Theme setTheme: (theme: Theme) => void + language: Language + setLanguage: (lang: Language) => void + enableHealthCheck: boolean setEnableHealthCheck: (enable: boolean) => void @@ -59,6 +63,7 @@ const useSettingsStoreBase = create()( persist( (set) => ({ theme: 'system', + language: 'zh', refreshLayout: () => { const graphState = useGraphStore.getState(); const currentGraph = graphState.sigmaGraph; @@ -110,6 +115,13 @@ const useSettingsStoreBase = create()( setTheme: (theme: Theme) => set({ theme }), + setLanguage: (language: Language) => { + import('i18next').then(({ default: i18n }) => { + i18n.changeLanguage(language); + }); + set({ language }); + }, + setGraphLayoutMaxIterations: (iterations: number) => set({ graphLayoutMaxIterations: iterations