diff --git a/src/frontend/index.html b/src/frontend/index.html index 7046e12..162b27d 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -5,6 +5,28 @@ HugAgentOS +
diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 54d306f..74d0f83 100755 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -707,7 +707,7 @@ export default function App() { onSelectSearchResult={handleSelectSearchResult} /> - +
{!showChatHeader && (
diff --git a/src/frontend/src/AppThemeProvider.tsx b/src/frontend/src/AppThemeProvider.tsx new file mode 100644 index 0000000..0368fde --- /dev/null +++ b/src/frontend/src/AppThemeProvider.tsx @@ -0,0 +1,36 @@ +import { useEffect, useSyncExternalStore, type ReactNode } from 'react'; +import { ConfigProvider } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; +import enUS from 'antd/locale/en_US'; +import { getLang } from './i18n'; +import { getAppTheme } from './appTheme'; +import { useUIStore } from './stores/uiStore'; +import { applyThemeToDom, systemPrefersDark, watchSystemTheme } from './theme'; + +interface AppThemeProviderProps { + children: ReactNode; + /** 分享预览等对外页面锁定浅色:观看者常不是本机账号,内容外观应与作者所见一致 */ + forceLight?: boolean; +} + +/** + * 主题根:统一包掉 antd ConfigProvider(含 locale),按 uiStore 的档位 + 系统外观 + * 解析当前深浅,切换 light/dark algorithm 并把 data-theme 落 DOM。 + * 「跟随系统」经 useSyncExternalStore 订阅 matchMedia,系统翻转即时生效。 + * 主入口与 CE overlay 入口共用,入口文件各自只包一层。 + */ +export function AppThemeProvider({ children, forceLight = false }: AppThemeProviderProps) { + const themeMode = useUIStore((s) => s.themeMode); + const sysDark = useSyncExternalStore(watchSystemTheme, systemPrefersDark); + const isDark = !forceLight && (themeMode === 'dark' || (themeMode === 'system' && sysDark)); + + useEffect(() => { + applyThemeToDom(isDark); + }, [isDark]); + + return ( + + {children} + + ); +} diff --git a/src/frontend/src/appTheme.ts b/src/frontend/src/appTheme.ts index feea09f..1e16ddf 100644 --- a/src/frontend/src/appTheme.ts +++ b/src/frontend/src/appTheme.ts @@ -1,25 +1,63 @@ -/** Ant Design theme tokens aligned with UI design spec — a single shared point for main.tsx (including the CE overlay version). */ -export const appTheme = { - token: { - colorPrimary: '#126DFF', - colorSuccess: '#02B589', - colorWarning: '#F8AB42', - colorError: '#FC5D5D', - colorInfo: '#126DFF', - colorText: '#101828', - colorTextSecondary: '#475467', - colorTextTertiary: '#667085', - colorTextQuaternary: '#98A2B3', - colorBorder: '#DDE3EC', - colorBorderSecondary: '#E9EDF3', - colorBgContainer: '#FFFFFF', - colorBgLayout: '#F5F7FB', - borderRadius: 10, - borderRadiusLG: 16, - borderRadiusSM: 6, - fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif', - fontSize: 14, - fontSizeSM: 12, - fontSizeLG: 16, - }, +/** + * Ant Design theme tokens aligned with UI design spec — a single shared point for main.tsx (including the CE overlay version). + * 深浅两套 token 的取值与 styles/variables.css 的 :root / :root[data-theme="dark"] 覆盖块一一对应,两边同步改。 + */ +import { theme as antdTheme } from 'antd'; +import type { ThemeConfig } from 'antd'; + +const sharedToken = { + colorWarning: '#F8AB42', + borderRadius: 10, + borderRadiusLG: 16, + borderRadiusSM: 6, + fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif', + fontSize: 14, + fontSizeSM: 12, + fontSizeLG: 16, }; + +const lightToken = { + colorPrimary: '#126DFF', + colorSuccess: '#02B589', + colorError: '#FC5D5D', + colorInfo: '#126DFF', + colorText: '#101828', + colorTextSecondary: '#475467', + colorTextTertiary: '#667085', + colorTextQuaternary: '#98A2B3', + colorBorder: '#DDE3EC', + colorBorderSecondary: '#E9EDF3', + colorBgContainer: '#FFFFFF', + colorBgLayout: '#F5F7FB', +}; + +const darkToken = { + colorPrimary: '#3E8BFF', + colorSuccess: '#22C79D', + colorError: '#FF6B6B', + colorInfo: '#3E8BFF', + colorText: '#E8ECF4', + colorTextSecondary: '#B3BDCD', + colorTextTertiary: '#8792A4', + colorTextQuaternary: '#5F6B7D', + colorBorder: '#2B3442', + colorBorderSecondary: '#242C38', + colorBgContainer: '#161C25', + colorBgElevated: '#1C2330', + colorBgLayout: '#12171F', + colorBgSpotlight: '#1C2330', +}; + +// 模块级预建常量:同主题下引用稳定,ConfigProvider 无需重算派生 token +const LIGHT_THEME: ThemeConfig = { + algorithm: antdTheme.defaultAlgorithm, + token: { ...sharedToken, ...lightToken }, +}; +const DARK_THEME: ThemeConfig = { + algorithm: antdTheme.darkAlgorithm, + token: { ...sharedToken, ...darkToken }, +}; + +export function getAppTheme(isDark: boolean): ThemeConfig { + return isDark ? DARK_THEME : LIGHT_THEME; +} diff --git a/src/frontend/src/components/loop/loop.css b/src/frontend/src/components/loop/loop.css index 535106d..0324233 100644 --- a/src/frontend/src/components/loop/loop.css +++ b/src/frontend/src/components/loop/loop.css @@ -18,28 +18,30 @@ min-height: 0; height: calc(100vh - 150px); font-size: 14px; - color: var(--text-primary, #1f2328); - background: var(--bg-primary, #fff); - border: 1px solid var(--border-color, #e1e4e8); + color: var(--color-text); + background: var(--color-bg-container); + border: 1px solid var(--color-border); border-radius: 10px; overflow: hidden; } .loop-sidebar { width: 340px; - border-right: 1px solid var(--border-color, #e1e4e8); + border-right: 1px solid var(--color-border); padding: 16px; overflow-y: auto; flex-shrink: 0; } .loop-sidebar h3 { margin: 8px 0; font-size: 15px; } -.loop-sidebar label { display: block; margin: 8px 0 3px; font-size: 12px; color: #666; } +.loop-sidebar label { display: block; margin: 8px 0 3px; font-size: 12px; color: var(--color-text-secondary); } .loop-sidebar textarea, .loop-sidebar input { width: 100%; box-sizing: border-box; - border: 1px solid var(--border-color, #d0d7de); + border: 1px solid var(--color-fill); border-radius: 6px; padding: 6px 8px; + background: var(--color-bg-container); + color: var(--color-text); font-family: inherit; font-size: 13px; } @@ -49,28 +51,28 @@ border: none; border-radius: 6px; padding: 7px 14px; cursor: pointer; font-size: 13px; margin-top: 10px; } -.loop-btn.primary { background: #2563eb; color: #fff; } -.loop-btn.danger { background: #dc2626; color: #fff; } +.loop-btn.primary { background: var(--color-primary); color: var(--color-text-white); } +.loop-btn.danger { background: var(--color-error); color: var(--color-text-white); } .loop-list { margin-top: 8px; } .loop-list-item { - border: 1px solid var(--border-color, #e1e4e8); border-radius: 8px; + border: 1px solid var(--color-border); border-radius: 8px; padding: 8px 10px; margin-bottom: 6px; cursor: pointer; } -.loop-list-item.active { border-color: #2563eb; background: rgba(37,99,235,0.06); } +.loop-list-item.active { border-color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 6%, transparent); } .loop-title { font-weight: 600; font-size: 13px; } -.loop-status { display: inline-block; font-size: 11px; padding: 1px 7px; border-radius: 10px; margin: 3px 0; background: #eee; } +.loop-status { display: inline-block; font-size: 11px; padding: 1px 7px; border-radius: 10px; margin: 3px 0; background: var(--color-fill-hover); } .loop-status.s-completed { background: #dcfce7; color: #166534; } .loop-status.s-running { background: #dbeafe; color: #1e40af; } .loop-status.s-budget_exhausted { background: #fef9c3; color: #854d0e; } .loop-status.s-failed { background: #fee2e2; color: #991b1b; } .loop-status.s-awaiting_human { background: #fae8ff; color: #86198f; } -.loop-meta { font-size: 11px; color: #888; } +.loop-meta { font-size: 11px; color: var(--color-text-tertiary); } .loop-main { flex: 1; padding: 20px; overflow-y: auto; } -.loop-empty, .loop-dim { color: #999; } +.loop-empty, .loop-dim { color: var(--color-text-placeholder); } .loop-header { display: flex; justify-content: space-between; align-items: center; } -.loop-goal { background: #f6f8fa; border-radius: 8px; padding: 10px 12px; margin: 10px 0; } -.loop-budget { font-size: 12px; color: #666; margin-top: 4px; } +.loop-goal { background: var(--color-bg-layout); border-radius: 8px; padding: 10px 12px; margin: 10px 0; } +.loop-budget { font-size: 12px; color: var(--color-text-secondary); margin-top: 4px; } .loop-stream { background: #0d1117; color: #c9d1d9; border-radius: 8px; padding: 12px; font-family: ui-monospace, monospace; font-size: 12.5px; max-height: 300px; overflow-y: auto; @@ -81,9 +83,9 @@ .loop-done { color: #3fb950; font-weight: 600; } .loop-err { color: #f85149; } .loop-table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 12.5px; } -.loop-table th, .loop-table td { border: 1px solid var(--border-color, #e1e4e8); padding: 5px 8px; text-align: left; } -.loop-table th { background: #f6f8fa; } -.loop-reason-cell { max-width: 320px; white-space: pre-wrap; color: #555; } +.loop-table th, .loop-table td { border: 1px solid var(--color-border); padding: 5px 8px; text-align: left; } +.loop-table th { background: var(--color-bg-layout); } +.loop-reason-cell { max-width: 320px; white-space: pre-wrap; color: var(--color-text-secondary); } .loop-tableWrap { max-width: 100%; overflow-x: auto; } @media (max-width: 768px) { @@ -120,7 +122,7 @@ width: 100%; padding: 14px; border-right: 0; - border-bottom: 1px solid var(--border-color, #e1e4e8); + border-bottom: 1px solid var(--color-border); overflow: visible; box-sizing: border-box; } @@ -166,24 +168,18 @@ } } -@media (prefers-color-scheme: dark) { - .loop-panel { background: #0d1117; color: #c9d1d9; } - .loop-sidebar, .loop-table th, .loop-goal { background: #161b22; } - .loop-sidebar textarea, .loop-sidebar input { background: #0d1117; color: #c9d1d9; border-color: #30363d; } -} - /* ── 对话内「计划条」(输入框上方,取代第 N 轮表格)───────────────────── */ .loop-planbar { - border: 1px solid var(--border-color, #e5e7eb); + border: 1px solid var(--color-border); border-radius: 12px; - background: var(--card-bg, #fafbfc); + background: var(--color-bg-layout); margin: 0 0 8px; overflow: hidden; font-size: 13px; } .loop-planbar-running { - border-color: var(--primary-color, #4f46e5); - box-shadow: 0 0 0 1px var(--primary-color, #4f46e5) inset; + border-color: var(--color-primary); + box-shadow: 0 0 0 1px var(--color-primary) inset; } .loop-planbar-head { display: flex; align-items: center; gap: 8px; @@ -193,34 +189,34 @@ .loop-planbar-title { flex: 1; min-width: 0; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - color: var(--text-color, #1f2328); + color: var(--color-text); } .loop-planbar-status { flex-shrink: 0; font-size: 12px; font-weight: 600; padding: 1px 8px; border-radius: 20px; - background: var(--tag-bg, #eef2ff); color: var(--primary-color, #4f46e5); + background: var(--color-primary-light); color: var(--color-primary); } .loop-status-completed { background: #dcfce7; color: #16a34a; } .loop-status-budget_exhausted { background: #fef3c7; color: #b45309; } .loop-status-cancelled, .loop-status-failed { background: #fee2e2; color: #dc2626; } -.loop-planbar-caret { flex-shrink: 0; color: var(--text-muted, #8a94a6); font-size: 11px; } +.loop-planbar-caret { flex-shrink: 0; color: var(--color-text-placeholder); font-size: 11px; } .loop-planbar-continue { flex-shrink: 0; border: none; cursor: pointer; white-space: nowrap; font-size: 12px; font-weight: 600; line-height: 1; padding: 3px 10px; border-radius: 20px; - background: var(--primary-color, #4f46e5); color: #fff; + background: var(--color-primary); color: var(--color-text-white); } .loop-planbar-continue:hover { filter: brightness(1.08); } .loop-planbar-close { flex-shrink: 0; border: none; background: transparent; cursor: pointer; - color: var(--text-muted, #8a94a6); font-size: 16px; line-height: 1; padding: 0 2px; + color: var(--color-text-placeholder); font-size: 16px; line-height: 1; padding: 0 2px; } -.loop-planbar-close:hover { color: var(--text-color, #1f2328); } +.loop-planbar-close:hover { color: var(--color-text); } .loop-planbar-progress { - height: 3px; background: var(--border-color, #e5e7eb); overflow: hidden; + height: 3px; background: var(--color-border); overflow: hidden; } .loop-planbar-progress-fill { - height: 100%; background: var(--primary-color, #4f46e5); + height: 100%; background: var(--color-primary); transition: width .4s ease; } .loop-planbar-list { list-style: none; margin: 0; padding: 6px 12px 10px; } @@ -228,19 +224,19 @@ display: flex; align-items: flex-start; gap: 8px; padding: 3px 0; line-height: 1.5; } .loop-planbar-mark { flex-shrink: 0; width: 14px; text-align: center; font-size: 12px; } -.loop-planbar-desc { color: var(--text-color, #1f2328); } +.loop-planbar-desc { color: var(--color-text); } .loop-item-done .loop-planbar-mark { color: #16a34a; } -.loop-item-done .loop-planbar-desc { color: var(--text-muted, #8a94a6); text-decoration: line-through; } -.loop-item-active .loop-planbar-mark { color: var(--primary-color, #4f46e5); } +.loop-item-done .loop-planbar-desc { color: var(--color-text-placeholder); text-decoration: line-through; } +.loop-item-active .loop-planbar-mark { color: var(--color-primary); } .loop-item-active .loop-planbar-desc { font-weight: 600; } -.loop-item-todo .loop-planbar-mark { color: var(--text-muted, #b6bdc9); } +.loop-item-todo .loop-planbar-mark { color: var(--color-text-placeholder); } .loop-planbar-reviewing { flex-shrink: 0; margin-left: 6px; font-size: 11px; font-weight: 600; - color: var(--primary-color, #4f46e5); white-space: nowrap; + color: var(--color-primary); white-space: nowrap; animation: loop-review-pulse 1.2s ease-in-out infinite; } @keyframes loop-review-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.45; } } -.loop-planbar-hint { padding: 4px 12px 10px; color: var(--text-muted, #8a94a6); } +.loop-planbar-hint { padding: 4px 12px 10px; color: var(--color-text-placeholder); } .loop-status-interrupted { background: #fef3c7; color: #b45309; } .loop-planbar-steer { display: flex; align-items: center; gap: 6px; padding: 4px 12px 10px; @@ -248,16 +244,16 @@ .loop-planbar-steer-input { flex: 1; min-width: 0; font-size: 12px; line-height: 1.4; padding: 5px 10px; border-radius: 16px; - border: 1px solid var(--border-color, #e5e7eb); - background: var(--bg-color, #fff); color: var(--text-color, #1f2328); + border: 1px solid var(--color-border); + background: var(--color-bg-container); color: var(--color-text); outline: none; } -.loop-planbar-steer-input:focus { border-color: var(--primary-color, #4f46e5); } +.loop-planbar-steer-input:focus { border-color: var(--color-primary); } .loop-planbar-steer-send { flex-shrink: 0; border: none; cursor: pointer; white-space: nowrap; font-size: 12px; font-weight: 600; line-height: 1; padding: 6px 12px; border-radius: 16px; - background: var(--primary-color, #4f46e5); color: #fff; + background: var(--color-primary); color: var(--color-text-white); } .loop-planbar-steer-send:disabled { opacity: .45; cursor: default; } @@ -271,9 +267,9 @@ .loop-sidebar, .loop-main { - border: 1px solid rgba(15,23,42,.065); + border: 1px solid color-mix(in srgb, var(--color-text) 6.5%, transparent); border-radius: 20px; - background: rgba(255,255,255,.94); + background: color-mix(in srgb, var(--color-bg-container) 94%, transparent); box-shadow: 0 12px 34px rgba(42,61,94,.075), 0 1px 2px rgba(15,23,42,.035); } @@ -287,7 +283,7 @@ .loop-sidebar h3, .loop-header h2 { - color: #101828; + color: var(--color-text); font-weight: 700; letter-spacing: -.025em; } @@ -296,16 +292,16 @@ .loop-budget, .loop-meta, .loop-dim { - color: #667085; + color: var(--color-text-tertiary); } .loop-sidebar textarea, .loop-sidebar input { min-height: 44px; - border-color: rgba(15,23,42,.1); + border-color: color-mix(in srgb, var(--color-text) 10%, transparent); border-radius: 12px; - background: #F7F8FA; - color: #101828; + background: var(--color-bg-layout); + color: var(--color-text); font-size: 14px; } @@ -320,14 +316,14 @@ } .loop-btn.primary { - background: #126DFF; + background: var(--color-primary); } .loop-list { overflow: hidden; - border: 1px solid rgba(15,23,42,.065); + border: 1px solid color-mix(in srgb, var(--color-text) 6.5%, transparent); border-radius: 14px; - background: #F7F8FA; + background: var(--color-bg-layout); } .loop-list-item { @@ -338,7 +334,7 @@ } .loop-list-item + .loop-list-item { - border-top: 1px solid rgba(15,23,42,.075); + border-top: 1px solid color-mix(in srgb, var(--color-text) 7.5%, transparent); } .loop-list-item.active { @@ -348,8 +344,8 @@ .loop-goal { border-radius: 14px; - background: #F7F8FA; - color: #344054; + background: var(--color-bg-layout); + color: var(--color-text-secondary); } .loop-stream { diff --git a/src/frontend/src/components/settings/SettingsModal.tsx b/src/frontend/src/components/settings/SettingsModal.tsx index 0beb5a2..ed0aab4 100644 --- a/src/frontend/src/components/settings/SettingsModal.tsx +++ b/src/frontend/src/components/settings/SettingsModal.tsx @@ -43,6 +43,7 @@ import { ChatModesPanel } from './ChatModesPanel'; import { FactsList } from '../memory/FactsList'; import { OntologyManager } from '../ontology'; import { getLang, setLang, t, type Lang } from '../../i18n'; +import type { ThemeMode } from '../../theme'; interface SectionDef { id: string; @@ -99,7 +100,7 @@ function getCropBounds(imageWidth: number, imageHeight: number, zoom: number) { } export default function SettingsPage() { - const { dispatchProcessVisible, setDispatchProcessVisible } = useUIStore(); + const { dispatchProcessVisible, setDispatchProcessVisible, themeMode, setThemeMode } = useUIStore(); const { memoryEnabled, memoryWriteEnabled, @@ -672,6 +673,27 @@ export default function SettingsPage() { onChange={(v) => setLang(v as Lang)} />
+ +
+ +
+
+ {t('外观')} + + {t('切换界面深浅配色,选择跟随系统时自动匹配系统外观')} + +
+