diff --git a/dashboard/eslint.config.js b/dashboard/eslint.config.js index f53da2d8..26a7be20 100644 --- a/dashboard/eslint.config.js +++ b/dashboard/eslint.config.js @@ -6,7 +6,7 @@ import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ['dist', 'out'] }, jsxA11y.flatConfigs.recommended, { extends: [js.configs.recommended, ...tseslint.configs.recommended], @@ -25,10 +25,7 @@ export default tseslint.config( acc[key] = 'warn' return acc }, {}), - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], // 关闭或降级其他规则 '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-unused-vars': 'warn', @@ -37,4 +34,11 @@ export default tseslint.config( 'jsx-a11y/no-autofocus': 'warn', }, }, + { + files: ['**/*.d.ts'], + rules: { + // Ambient global declarations use `var` in TypeScript declaration files. + 'no-var': 'off', + }, + } ) diff --git a/dashboard/package.json b/dashboard/package.json index 042d95a3..e88f0731 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -121,6 +121,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-tooltip": "^1.2.8", + "@react-spring/web": "10.0.3", "@tanstack/react-router": "^1.140.0", "@tanstack/react-virtual": "^3.13.13", "@tanstack/router-devtools": "^1.140.0", @@ -130,6 +131,7 @@ "@uppy/dashboard": "^5.1.0", "@uppy/react": "^5.1.1", "@uppy/xhr-upload": "^5.1.1", + "@use-gesture/react": "^10.3.1", "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -142,6 +144,7 @@ "idb": "^8.0.3", "katex": "^0.16.27", "lucide-react": "^0.556.0", + "motion": "^12.38.0", "react": "^19.2.1", "react-day-picker": "^9.12.0", "react-dom": "^19.2.1", @@ -154,9 +157,7 @@ "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "smol-toml": "^1.5.2", - "tailwind-merge": "^3.4.0", - "@react-spring/web": "10.0.3", - "@use-gesture/react": "^10.3.1" + "tailwind-merge": "^3.4.0" }, "devDependencies": { "@tailwindcss/vite": "^4.2.1", diff --git a/dashboard/src/components/CodeEditor.tsx b/dashboard/src/components/CodeEditor.tsx index ae928f90..58a5875d 100644 --- a/dashboard/src/components/CodeEditor.tsx +++ b/dashboard/src/components/CodeEditor.tsx @@ -1,19 +1,8 @@ -import { useEffect, useState } from 'react' -import CodeMirror from '@uiw/react-codemirror' -import { css } from '@codemirror/lang-css' -import { json, jsonParseLinter } from '@codemirror/lang-json' -import { linter } from '@codemirror/lint' -import { python } from '@codemirror/lang-python' -import { oneDark } from '@codemirror/theme-one-dark' -import { EditorView } from '@codemirror/view' -import { StreamLanguage } from '@codemirror/language' -import { toml as tomlMode } from '@codemirror/legacy-modes/mode/toml' - -import { useTheme } from '@/components/use-theme' +import { lazy, Suspense } from 'react' export type Language = 'python' | 'json' | 'toml' | 'css' | 'text' -interface CodeEditorProps { +export interface CodeEditorProps { value: string onChange?: (value: string) => void @@ -27,109 +16,38 @@ interface CodeEditorProps { className?: string } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const languageExtensions: Record = { - python: [python()], - json: [json(), linter(jsonParseLinter())], - toml: [StreamLanguage.define(tomlMode)], - css: [css()], - text: [], -} +const CodeEditorImpl = lazy(() => import('./CodeEditorImpl')) -export function CodeEditor({ - value, - onChange, - language = 'text', - readOnly = false, - height = '400px', +function CodeEditorFallback({ + height, minHeight, maxHeight, - placeholder, - theme, className = '', -}: CodeEditorProps) { - const [mounted, setMounted] = useState(false) - const { resolvedTheme } = useTheme() +}: Pick) { + return ( +
+ ) +} - useEffect(() => { - setMounted(true) - }, []) - - if (!mounted) { - return ( -
- ) - } - - const extensions = [ - ...(languageExtensions[language] || []), - EditorView.lineWrapping, - // 应用 JetBrains Mono 字体 - EditorView.theme({ - '&': { - fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', - }, - '.cm-content': { - fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', - }, - '.cm-gutters': { - fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', - }, - '.cm-scroller': { - fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', - }, - }), - ] - - if (readOnly) { - extensions.push(EditorView.editable.of(false)) - } - - // 如果外部传了 theme prop 则使用,否则从 context 自动获取 - const effectiveTheme = theme ?? resolvedTheme +export function CodeEditor(props: CodeEditorProps) { + const { height = '400px', minHeight, maxHeight, className = '' } = props return ( -
- -
+ + } + > + + ) } diff --git a/dashboard/src/components/CodeEditorImpl.tsx b/dashboard/src/components/CodeEditorImpl.tsx new file mode 100644 index 00000000..5477a449 --- /dev/null +++ b/dashboard/src/components/CodeEditorImpl.tsx @@ -0,0 +1,105 @@ +import { css } from '@codemirror/lang-css' +import { json, jsonParseLinter } from '@codemirror/lang-json' +import { python } from '@codemirror/lang-python' +import { StreamLanguage } from '@codemirror/language' +import { toml as tomlMode } from '@codemirror/legacy-modes/mode/toml' +import { linter } from '@codemirror/lint' +import { oneDark } from '@codemirror/theme-one-dark' +import { EditorView } from '@codemirror/view' +import CodeMirror from '@uiw/react-codemirror' + +import { useTheme } from '@/components/use-theme' + +import type { CodeEditorProps, Language } from './CodeEditor' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const languageExtensions: Record = { + python: [python()], + json: [json(), linter(jsonParseLinter())], + toml: [StreamLanguage.define(tomlMode)], + css: [css()], + text: [], +} + +export default function CodeEditorImpl({ + value, + onChange, + language = 'text', + readOnly = false, + height = '400px', + minHeight, + maxHeight, + placeholder, + theme, + className = '', +}: CodeEditorProps) { + const { resolvedTheme } = useTheme() + + const extensions = [ + ...(languageExtensions[language] || []), + EditorView.lineWrapping, + // 应用 JetBrains Mono 字体 + EditorView.theme({ + '&': { + fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', + }, + '.cm-content': { + fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', + }, + '.cm-gutters': { + fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', + }, + '.cm-scroller': { + fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', + }, + }), + ] + + if (readOnly) { + extensions.push(EditorView.editable.of(false)) + } + + // 如果外部传了 theme prop 则使用,否则从 context 自动获取 + const effectiveTheme = theme ?? resolvedTheme + + return ( +
+ +
+ ) +} diff --git a/dashboard/src/components/expression-reviewer.tsx b/dashboard/src/components/expression-reviewer.tsx index 9f3e8b27..e03d9f06 100644 --- a/dashboard/src/components/expression-reviewer.tsx +++ b/dashboard/src/components/expression-reviewer.tsx @@ -1443,6 +1443,7 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
@@ -1561,14 +1562,14 @@ if (isCurrent) {
{/* 情景 */}
- +
情景

{expr.situation}

{/* 风格 */}
- +
风格
{expr.style.split(/[,,]/).map((s, i) => ( @@ -1614,14 +1615,14 @@ if (isCurrent) {
{/* 情景 */}
- +
情景

{expr.situation}

{/* 风格 */}
- +
风格
{expr.style.split(/[,,]/).map((s, i) => ( diff --git a/dashboard/src/components/layout/Header.tsx b/dashboard/src/components/layout/Header.tsx index 7a3465b7..c9f085f5 100644 --- a/dashboard/src/components/layout/Header.tsx +++ b/dashboard/src/components/layout/Header.tsx @@ -1,4 +1,18 @@ -import { BookOpen, ChevronLeft, Globe, LogOut, Menu, Moon, Search, Server, Sun } from 'lucide-react' +import { Link } from '@tanstack/react-router' +import { + BookOpen, + ChevronLeft, + Globe, + LogOut, + Menu, + MessageSquare, + Moon, + Search, + Server, + SlidersHorizontal, + Sun, +} from 'lucide-react' +import { LayoutGroup, motion } from 'motion/react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -13,14 +27,21 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { ShortcutKbd } from '@/components/ui/kbd' +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { toggleThemeWithTransition } from '@/components/use-theme' import { useBackground } from '@/hooks/use-background' import { logout } from '@/lib/fetch-with-auth' import { isElectron } from '@/lib/runtime' import { cn } from '@/lib/utils' +import type { WorkspaceMode } from './types' const LANGUAGE_CODES = ['zh', 'en', 'ja', 'ko'] as const -const LANGUAGE_NAMES: Record = { "zh": "中文", "en": "English", "ja": "日本語", "ko": "한국어" } +const LANGUAGE_NAMES: Record<(typeof LANGUAGE_CODES)[number], string> = { + zh: '中文', + en: 'English', + ja: '日本語', + ko: '한국어', +} interface HeaderProps { sidebarOpen: boolean @@ -31,6 +52,7 @@ interface HeaderProps { onMobileMenuToggle: () => void onSearchOpenChange: (open: boolean) => void onThemeChange: (theme: 'light' | 'dark' | 'system') => void + workspaceMode: WorkspaceMode } export function Header({ @@ -42,6 +64,7 @@ export function Header({ onMobileMenuToggle, onSearchOpenChange, onThemeChange, + workspaceMode, }: HeaderProps) { const { t, i18n: i18nInstance } = useTranslation() const currentLang = i18nInstance.language || 'zh' @@ -62,10 +85,12 @@ export function Header({ } return ( -
+
{!inheritsPageBackground && }
{/* 移动端菜单按钮 */} @@ -73,17 +98,23 @@ export function Header({ onClick={onMobileMenuToggle} aria-label={t('a11y.closeMenu')} aria-expanded={mobileMenuOpen} - className="rounded-lg p-2 hover:bg-accent lg:hidden" + className={cn( + 'hover:bg-accent rounded-lg p-2 lg:hidden', + workspaceMode === 'chat' && 'hidden' + )} > - + {/* 桌面端侧边栏收起/展开按钮 */} -
+
)} {/* 搜索框 */} {/* 搜索对话框 */} @@ -142,26 +223,23 @@ export function Header({ - { - - LANGUAGE_CODES.map((code) => ( + {LANGUAGE_CODES.map((code) => ( i18nInstance.changeLanguage(code)} className={cn( 'cursor-pointer', - currentLang.split('-')[0] === code && 'font-semibold text-primary' + currentLang.split('-')[0] === code && 'text-primary font-semibold' )} > - {currentLang.split('-')[0] === code && ( - - )} + {currentLang.split('-')[0] === code && } {LANGUAGE_NAMES[code]} ))} @@ -175,13 +253,13 @@ export function Header({ toggleThemeWithTransition(newTheme, onThemeChange, e) }} aria-label={actualTheme === 'dark' ? t('header.switchToLight') : t('header.switchToDark')} - className="rounded-lg p-2 hover:bg-accent" + className="hover:bg-accent rounded-lg p-2" > {actualTheme === 'dark' ? : } {/* 分隔线 */} -
+
{/* 登出按钮 */}