diff --git a/src/engines/hermes/lib/svg-icons.js b/src/engines/hermes/lib/svg-icons.js new file mode 100644 index 0000000..e018460 --- /dev/null +++ b/src/engines/hermes/lib/svg-icons.js @@ -0,0 +1,72 @@ +/** + * Hermes 页面共用 SVG 图标集 — 替换分散的 emoji,统一视觉语言。 + * + * 命名约定: + * - 名字与 lucide / feather 一致,方便后续替换 + * - 所有 path 默认使用 currentColor + fill:none + stroke-width:1.6 + * - viewBox 全部 0 0 24 24,渲染时由调用方提供 width/height + 容器颜色 + * + * 用法: + * import { svgIcon } from '../lib/svg-icons.js' + * svgIcon('folder', { size: 18, className: 'hm-files-icon' }) + */ + +/* eslint-disable max-len */ +const PATHS = { + // === 通用 / 状态 === + 'alert-triangle': '', + 'check': '', + 'x': '', + 'check-circle': '', + 'x-circle': '', + 'info': '', + + // === 文件系统 === + 'folder': '', + 'folder-up': '', + 'file': '', + 'file-text': '', + 'image': '', + 'link-2': '', + 'settings': '', + + // === 列表 / 看板 === + 'clipboard-list': '', + 'message-square': '', + 'inbox': '', + + // === 安全 / 认证 === + 'lock': '', + 'shield': '', + 'key': '', + + // === 媒体 === + 'volume': '', + 'mic': '', + 'search': '', +} + +/** + * 渲染指定图标为 inline SVG 字符串(可直接放进 innerHTML)。 + * + * @param {string} name - PATHS 里的 key + * @param {object} [opts] + * @param {number} [opts.size=14] - 边长 + * @param {number} [opts.stroke=1.6] - stroke-width + * @param {string} [opts.className] - 额外 class + * @param {string} [opts.color] - 强制颜色(不传则跟随 currentColor) + */ +export function svgIcon(name, opts = {}) { + const path = PATHS[name] + if (!path) return '' + const size = opts.size ?? 14 + const stroke = opts.stroke ?? 1.6 + const cls = opts.className ? ` class="${opts.className}"` : '' + const color = opts.color ? ` stroke="${opts.color}"` : '' + return `${path}` +} + +/** 直接拿原 path 字符串(用于场景需要自定义 svg 包装的情况) */ +export function svgPath(name) { + return PATHS[name] || '' +} diff --git a/src/engines/hermes/pages/files.js b/src/engines/hermes/pages/files.js index 91117fe..7e84393 100644 --- a/src/engines/hermes/pages/files.js +++ b/src/engines/hermes/pages/files.js @@ -15,6 +15,7 @@ import { api } from '../../../lib/tauri-api.js' import { toast } from '../../../components/toast.js' import { showConfirm } from '../../../components/modal.js' import { humanizeError } from '../../../lib/humanize-error.js' +import { svgIcon } from '../lib/svg-icons.js' function escHtml(s) { return String(s ?? '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') @@ -35,14 +36,13 @@ function formatTime(secs) { } function iconForKind(kind, name) { - if (kind === 'dir') return '📁' - if (kind === 'symlink') return '🔗' + if (kind === 'dir') return svgIcon('folder', { size: 14 }) + if (kind === 'symlink') return svgIcon('link-2', { size: 14 }) const ext = (name.split('.').pop() || '').toLowerCase() - if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) return '🖼️' - if (['md', 'txt'].includes(ext)) return '📝' - if (['json', 'yaml', 'yml', 'toml'].includes(ext)) return '⚙️' - if (['py', 'js', 'ts', 'rs', 'go'].includes(ext)) return '📄' - return '📄' + if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) return svgIcon('image', { size: 14 }) + if (['md', 'txt'].includes(ext)) return svgIcon('file-text', { size: 14 }) + if (['json', 'yaml', 'yml', 'toml'].includes(ext)) return svgIcon('settings', { size: 14 }) + return svgIcon('file', { size: 14 }) } export function render() { @@ -118,7 +118,7 @@ export function render() { } return `
- ${currentDir ? `
📁..
` : ''} + ${currentDir ? `
${svgIcon('folder-up', { size: 14 })}..
` : ''} ${entries.map(e => `
/g, '>').replace(/"/g, '"') @@ -131,7 +132,7 @@ export function render() { if (m.error) { return `
-
${fromTag} ⚠️ ${escHtml(t('engine.hermesGroupChatRunFailed'))}
+
${fromTag} ${svgIcon('alert-triangle', { size: 12 })} ${escHtml(t('engine.hermesGroupChatRunFailed'))}
${escHtml(m.error)}
` diff --git a/src/engines/hermes/pages/kanban.js b/src/engines/hermes/pages/kanban.js index 91bb752..21d4bf9 100644 --- a/src/engines/hermes/pages/kanban.js +++ b/src/engines/hermes/pages/kanban.js @@ -18,6 +18,7 @@ import { api } from '../../../lib/tauri-api.js' import { toast } from '../../../components/toast.js' import { showModal, showContentModal } from '../../../components/modal.js' import { humanizeError } from '../../../lib/humanize-error.js' +import { svgIcon } from '../lib/svg-icons.js' const KANBAN_BASE = '/api/plugins/kanban' @@ -30,7 +31,7 @@ function renderInlineError(err) { const h = humanizeError(err, t('engine.hermesKanbanTaskLoadFailed')) return `
-
⚠️
+
${svgIcon('alert-triangle', { size: 20 })}
${escHtml(h.message)}
${h.hint ? `
${escHtml(h.hint)}
` : ''} @@ -77,7 +78,7 @@ export function render() { function renderBoard() { if (!board?.columns?.length) { - return `
📋
${escHtml(t('engine.hermesKanbanEmpty'))}
` + return `
${svgIcon('clipboard-list', { size: 32 })}
${escHtml(t('engine.hermesKanbanEmpty'))}
` } return `
@@ -107,7 +108,7 @@ export function render() {
${priorityBadge} ${assignee} - ${task.comment_count ? `💬 ${task.comment_count}` : ''} + ${task.comment_count ? `${svgIcon('message-square', { size: 12 })} ${task.comment_count}` : ''}
` diff --git a/src/engines/hermes/pages/lazy-deps.js b/src/engines/hermes/pages/lazy-deps.js index 4681021..75a48f6 100644 --- a/src/engines/hermes/pages/lazy-deps.js +++ b/src/engines/hermes/pages/lazy-deps.js @@ -11,16 +11,17 @@ import { t } from '../../../lib/i18n.js' import { api } from '../../../lib/tauri-api.js' import { toast } from '../../../components/toast.js' import { humanizeError } from '../../../lib/humanize-error.js' +import { svgIcon } from '../lib/svg-icons.js' // feature 分类配置(决定分组顺序 + 图标 + 文案) const CATEGORIES = [ - { prefix: 'platform.', emoji: '💬', titleKey: 'hermesLazyDeps.catPlatform' }, - { prefix: 'tts.', emoji: '🔊', titleKey: 'hermesLazyDeps.catTts' }, - { prefix: 'stt.', emoji: '🎙️', titleKey: 'hermesLazyDeps.catStt' }, - { prefix: 'search.', emoji: '🔍', titleKey: 'hermesLazyDeps.catSearch' }, - { prefix: 'provider.', emoji: '🧠', titleKey: 'hermesLazyDeps.catProvider' }, - { prefix: 'memory.', emoji: '🗂️', titleKey: 'hermesLazyDeps.catMemory' }, - { prefix: 'image.', emoji: '🎨', titleKey: 'hermesLazyDeps.catImage' }, + { prefix: 'platform.', icon: 'message-square', titleKey: 'hermesLazyDeps.catPlatform' }, + { prefix: 'tts.', icon: 'volume', titleKey: 'hermesLazyDeps.catTts' }, + { prefix: 'stt.', icon: 'mic', titleKey: 'hermesLazyDeps.catStt' }, + { prefix: 'search.', icon: 'search', titleKey: 'hermesLazyDeps.catSearch' }, + { prefix: 'provider.', icon: 'shield', titleKey: 'hermesLazyDeps.catProvider' }, + { prefix: 'memory.', icon: 'inbox', titleKey: 'hermesLazyDeps.catMemory' }, + { prefix: 'image.', icon: 'image', titleKey: 'hermesLazyDeps.catImage' }, ] const DESC_OVERRIDE_KEY = 'hermesLazyDeps.descOverride' // i18n.key 下的 feature → 描述 @@ -28,7 +29,7 @@ const DESC_OVERRIDE_KEY = 'hermesLazyDeps.descOverride' // i18n.key 下的 feat // 把 feature 按分类分组 function groupByCategory(features) { const groups = CATEGORIES.map(c => ({ ...c, items: [] })) - const other = { prefix: '', emoji: '🧩', titleKey: 'hermesLazyDeps.catOther', items: [] } + const other = { prefix: '', icon: 'file', titleKey: 'hermesLazyDeps.catOther', items: [] } for (const f of features) { const cat = groups.find(g => f.feature.startsWith(g.prefix)) if (cat) cat.items.push(f) @@ -83,7 +84,7 @@ async function loadAndRender(page) { const features = featuresResp.features || [] if (!features.length) { content.innerHTML = `
-
📦
+
${svgIcon('inbox', { size: 32 })}
${escapeHtml(t('hermesLazyDeps.emptyTitle'))}
` return @@ -113,7 +114,7 @@ function renderGroup(group, status) { return `
- ${group.emoji} + ${svgIcon(group.icon, { size: 18 })} ${escapeHtml(t(group.titleKey))}
@@ -130,7 +131,7 @@ function renderItem(f, st) { const specsTitle = (f.specs || []).join('\n') const featureLabel = featureDisplayName(f.feature) const stateBadge = satisfied - ? `✓ ${escapeHtml(t('hermesLazyDeps.installed'))}` + ? `${svgIcon('check', { size: 11 })} ${escapeHtml(t('hermesLazyDeps.installed'))}` : (known ? `${escapeHtml(t('hermesLazyDeps.notInstalled'))}` : `?`) diff --git a/src/engines/hermes/pages/logs.js b/src/engines/hermes/pages/logs.js index ff1bce3..1929dc8 100644 --- a/src/engines/hermes/pages/logs.js +++ b/src/engines/hermes/pages/logs.js @@ -117,7 +117,7 @@ export function render() { levelFilter !== 'ALL' ? levelFilter : null, ) } catch (e) { - entries = [{ raw: `⚠️ ${t('engine.logsLoadFailed')}: ${e.message || e}` }] + entries = [{ raw: `[ERROR] ${t('engine.logsLoadFailed')}: ${e.message || e}`, level: 'ERROR' }] } loading = false draw() diff --git a/src/engines/hermes/pages/oauth.js b/src/engines/hermes/pages/oauth.js index baaece4..968b4de 100644 --- a/src/engines/hermes/pages/oauth.js +++ b/src/engines/hermes/pages/oauth.js @@ -15,6 +15,7 @@ import { api } from '../../../lib/tauri-api.js' import { toast } from '../../../components/toast.js' import { showModal, showContentModal } from '../../../components/modal.js' import { humanizeError } from '../../../lib/humanize-error.js' +import { svgIcon } from '../lib/svg-icons.js' const OAUTH_BASE = '/api/providers/oauth' @@ -27,7 +28,7 @@ function renderInlineError(err) { const h = humanizeError(err, t('engine.hermesOAuthTitle')) return `
-
⚠️
+
${svgIcon('alert-triangle', { size: 20 })}
${escHtml(h.message)}
${h.hint ? `
${escHtml(h.hint)}
` : ''} @@ -62,7 +63,7 @@ export function render() { ${error ? renderInlineError(error) : ''} ${(!loading && !error && !providers.length) ? `
-
🔐
+
${svgIcon('lock', { size: 32 })}
${escHtml(t('engine.hermesOAuthEmpty'))}
` : ''} ${(!loading && providers.length) ? ` diff --git a/src/engines/hermes/pages/profiles.js b/src/engines/hermes/pages/profiles.js index 153b183..e67ef41 100644 --- a/src/engines/hermes/pages/profiles.js +++ b/src/engines/hermes/pages/profiles.js @@ -16,6 +16,7 @@ import { toast } from '../../../components/toast.js' import { showConfirm, showModal } from '../../../components/modal.js' import { humanizeError } from '../../../lib/humanize-error.js' import { getChatStore } from '../lib/chat-store.js' +import { svgIcon } from '../lib/svg-icons.js' const chatStore = getChatStore() @@ -28,7 +29,7 @@ function renderInlineError(err) { const h = humanizeError(err, t('engine.hermesProfilesTitle')) return `
-
⚠️
+
${svgIcon('alert-triangle', { size: 20 })}
${escHtml(h.message)}
${h.hint ? `
${escHtml(h.hint)}
` : ''} @@ -64,7 +65,7 @@ export function render() { ${error ? renderInlineError(error) : ''} ${(!loading && !error && !profiles.length) ? `
-
📁
+
${svgIcon('folder', { size: 32 })}
${escHtml(t('engine.hermesProfilesEmpty'))}
` : ''} ${(!loading && profiles.length) ? ` diff --git a/src/style/components.css b/src/style/components.css index f8bd2d5..04fe822 100644 --- a/src/style/components.css +++ b/src/style/components.css @@ -438,6 +438,14 @@ mark { margin-bottom: var(--space-md); opacity: 0.85; user-select: none; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-tertiary); +} +.empty-state .empty-icon svg { + width: 1em; + height: 1em; } .empty-state .empty-title { diff --git a/src/style/pages.css b/src/style/pages.css index f12dd26..6e4ce33 100644 --- a/src/style/pages.css +++ b/src/style/pages.css @@ -2675,6 +2675,14 @@ line-height: 1; margin-top: 2px; flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--error, #ef4444); +} +.page-inline-error-icon svg { + width: 20px; + height: 20px; } .page-inline-error-body { flex: 1;