diff --git a/package.json b/package.json index 9dded4b..6871717 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,9 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", + "dev:website": "vite --config vite.config.website.ts", + "build:website": "vite build --config vite.config.website.ts", + "preview:website": "vite preview --config vite.config.website.ts", "openapi:generate": "node scripts/generate-openapi.mjs" }, "files": [ diff --git a/packages/website/index.html b/packages/website/index.html new file mode 100644 index 0000000..22e9de7 --- /dev/null +++ b/packages/website/index.html @@ -0,0 +1,16 @@ + + + + + + + Hermes Web UI - Self-Hosted AI Chat Dashboard + + + + + +
+ + + diff --git a/packages/website/public/favicon.ico b/packages/website/public/favicon.ico new file mode 100644 index 0000000..9d196f6 Binary files /dev/null and b/packages/website/public/favicon.ico differ diff --git a/packages/website/public/image1.png b/packages/website/public/image1.png new file mode 100644 index 0000000..833c62f Binary files /dev/null and b/packages/website/public/image1.png differ diff --git a/packages/website/public/image2.png b/packages/website/public/image2.png new file mode 100644 index 0000000..5d55a68 Binary files /dev/null and b/packages/website/public/image2.png differ diff --git a/packages/website/public/image3.png b/packages/website/public/image3.png new file mode 100644 index 0000000..9d49b83 Binary files /dev/null and b/packages/website/public/image3.png differ diff --git a/packages/website/public/image4.png b/packages/website/public/image4.png new file mode 100644 index 0000000..3e1de6b Binary files /dev/null and b/packages/website/public/image4.png differ diff --git a/packages/website/public/logo.png b/packages/website/public/logo.png new file mode 100644 index 0000000..5d23421 Binary files /dev/null and b/packages/website/public/logo.png differ diff --git a/packages/website/src/App.vue b/packages/website/src/App.vue new file mode 100644 index 0000000..c7c4b4e --- /dev/null +++ b/packages/website/src/App.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/packages/website/src/components/docs/DocContent.vue b/packages/website/src/components/docs/DocContent.vue new file mode 100644 index 0000000..2be3b75 --- /dev/null +++ b/packages/website/src/components/docs/DocContent.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/packages/website/src/components/docs/DocSidebar.vue b/packages/website/src/components/docs/DocSidebar.vue new file mode 100644 index 0000000..aa64fc3 --- /dev/null +++ b/packages/website/src/components/docs/DocSidebar.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/packages/website/src/components/landing/FeaturesGrid.vue b/packages/website/src/components/landing/FeaturesGrid.vue new file mode 100644 index 0000000..7f0002c --- /dev/null +++ b/packages/website/src/components/landing/FeaturesGrid.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/packages/website/src/components/landing/HeroSection.vue b/packages/website/src/components/landing/HeroSection.vue new file mode 100644 index 0000000..b404488 --- /dev/null +++ b/packages/website/src/components/landing/HeroSection.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/packages/website/src/components/landing/InstallSection.vue b/packages/website/src/components/landing/InstallSection.vue new file mode 100644 index 0000000..50ca432 --- /dev/null +++ b/packages/website/src/components/landing/InstallSection.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/packages/website/src/components/landing/PlatformsSection.vue b/packages/website/src/components/landing/PlatformsSection.vue new file mode 100644 index 0000000..9cffbeb --- /dev/null +++ b/packages/website/src/components/landing/PlatformsSection.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/packages/website/src/components/landing/ScreenshotsSection.vue b/packages/website/src/components/landing/ScreenshotsSection.vue new file mode 100644 index 0000000..22ab6f3 --- /dev/null +++ b/packages/website/src/components/landing/ScreenshotsSection.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/packages/website/src/components/landing/StarHistorySection.vue b/packages/website/src/components/landing/StarHistorySection.vue new file mode 100644 index 0000000..79dcb52 --- /dev/null +++ b/packages/website/src/components/landing/StarHistorySection.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/packages/website/src/components/layout/SiteFooter.vue b/packages/website/src/components/layout/SiteFooter.vue new file mode 100644 index 0000000..28a25f8 --- /dev/null +++ b/packages/website/src/components/layout/SiteFooter.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/packages/website/src/components/layout/SiteHeader.vue b/packages/website/src/components/layout/SiteHeader.vue new file mode 100644 index 0000000..19af49e --- /dev/null +++ b/packages/website/src/components/layout/SiteHeader.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/packages/website/src/composables/useScrollReveal.ts b/packages/website/src/composables/useScrollReveal.ts new file mode 100644 index 0000000..0fc5a3a --- /dev/null +++ b/packages/website/src/composables/useScrollReveal.ts @@ -0,0 +1,27 @@ +import { onMounted, onUnmounted } from 'vue' + +export function useScrollReveal() { + let observer: IntersectionObserver | null = null + + onMounted(() => { + observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + entry.target.classList.add('revealed') + observer!.unobserve(entry.target) + } + } + }, + { threshold: 0.1, rootMargin: '0px 0px -40px 0px' }, + ) + + document.querySelectorAll('.reveal').forEach((el) => { + observer!.observe(el) + }) + }) + + onUnmounted(() => { + observer?.disconnect() + }) +} diff --git a/packages/website/src/composables/useTheme.ts b/packages/website/src/composables/useTheme.ts new file mode 100644 index 0000000..42291d2 --- /dev/null +++ b/packages/website/src/composables/useTheme.ts @@ -0,0 +1,54 @@ +import { ref, watch } from 'vue' + +export type ThemeMode = 'light' | 'dark' | 'system' + +const STORAGE_KEY = 'hermes_website_theme' + +const mode = ref( + (localStorage.getItem(STORAGE_KEY) as ThemeMode) || 'system', +) + +const isDark = ref(false) + +function applyTheme(dark: boolean) { + isDark.value = dark + document.documentElement.classList.toggle('dark', dark) +} + +function resolveDark(m: ThemeMode): boolean { + if (m === 'system') { + return window.matchMedia('(prefers-color-scheme: dark)').matches + } + return m === 'dark' +} + +applyTheme(resolveDark(mode.value)) + +const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') +mediaQuery.addEventListener('change', () => { + if (mode.value === 'system') { + applyTheme(resolveDark('system')) + } +}) + +watch(mode, (newMode) => { + localStorage.setItem(STORAGE_KEY, newMode) + applyTheme(resolveDark(newMode)) +}) + +export function useTheme() { + function setMode(m: ThemeMode) { + mode.value = m + } + + function toggleTheme() { + mode.value = isDark.value ? 'light' : 'dark' + } + + return { + mode, + isDark, + setMode, + toggleTheme, + } +} diff --git a/packages/website/src/env.d.ts b/packages/website/src/env.d.ts new file mode 100644 index 0000000..54eaa07 --- /dev/null +++ b/packages/website/src/env.d.ts @@ -0,0 +1,3 @@ +/// + +declare const __APP_VERSION__: string diff --git a/packages/website/src/i18n/en.ts b/packages/website/src/i18n/en.ts new file mode 100644 index 0000000..f698692 --- /dev/null +++ b/packages/website/src/i18n/en.ts @@ -0,0 +1,247 @@ +export default { + nav: { + home: 'Home', + docs: 'Documentation', + github: 'GitHub', + }, + hero: { + title: 'Self-Hosted AI Chat Dashboard', + subtitle: 'Open-source AI agent dashboard — streaming chat, multi-model routing, Kanban boards, usage analytics, web terminal, all in one self-hosted interface.', + cta: 'Get Started', + viewGithub: 'View on GitHub', + install: 'npm install -g hermes-web-ui', + }, + features: { + title: 'Everything You Need', + desc: 'A complete AI agent management dashboard with rich features out of the box.', + streaming: { + title: 'Streaming Chat', + desc: 'Real-time SSE-powered AI conversations with multi-session management, Markdown rendering, and code syntax highlighting.', + }, + platforms: { + title: '8 Platforms', + desc: 'Unified management for Telegram, Discord, Slack, WhatsApp, Matrix, Feishu, WeChat, and WeCom channels.', + }, + multiModel: { + title: 'Multi-Model', + desc: 'Support for Claude, GPT, Gemini, DeepSeek, and any OpenAI-compatible provider with auto-discovery.', + }, + groupChat: { + title: 'Group Chat', + desc: 'Multi-agent chat rooms with mention routing, context compression, and real-time collaboration.', + }, + kanban: { + title: 'Kanban Board', + desc: 'Visual task management with 7 status columns, assignee tracking, and filtering for AI-driven workflows.', + }, + analytics: { + title: 'Usage Analytics', + desc: 'Token usage breakdown, cost tracking, cache hit rates, model distribution, and 30-day trends.', + }, + profiles: { + title: 'Multi-Profile', + desc: 'Isolated profiles with independent configs. Clone, import/export profiles, run multiple gateways.', + }, + files: { + title: 'File Browser', + desc: 'Manage files across local, Docker, SSH, and Singularity backends with upload, preview, and edit.', + }, + terminal: { + title: 'Web Terminal', + desc: 'Full PTY terminal in the browser with multi-session support via WebSocket and xterm.js.', + }, + quickInstall: { + title: 'One Command', + desc: 'Install and start with a single command. Auto-detects config, resolves ports, opens the browser.', + }, + i18n: { + title: '8 Languages', + desc: 'Built-in support for English, Chinese, German, Spanish, French, Japanese, Korean, and Portuguese.', + }, + theme: { + title: 'Dark / Light', + desc: 'Pure Ink monochrome design with smooth theme switching. Responsive layout for mobile and desktop.', + }, + }, + platforms: { + title: 'Unified Platform Management', + desc: 'Configure credentials and behavior for 8 messaging platforms from a single settings page.', + telegram: 'Telegram', + discord: 'Discord', + slack: 'Slack', + whatsapp: 'WhatsApp', + matrix: 'Matrix', + feishu: 'Feishu', + wechat: 'WeChat', + wecom: 'WeCom', + }, + install: { + title: 'Quick Start', + desc: 'Get Hermes Web UI running in under a minute.', + npm: { + title: 'npm', + cmd1: 'npm install -g hermes-web-ui', + cmd2: 'hermes-web-ui start', + }, + docker: { + title: 'Docker', + cmd: 'docker compose up -d', + }, + source: { + title: 'From Source', + cmd1: 'git clone https://github.com/EKKOLearnAI/hermes-web-ui.git', + cmd2: 'cd hermes-web-ui && npm install && npm run dev', + }, + prereq: 'Requires Node.js >= 23', + }, + starHistory: { + title: 'Growing Community', + desc: 'Star us on GitHub and join the community.', + }, + footer: { + description: 'Self-hosted AI chat dashboard for Hermes Agent.', + license: 'MIT License', + madeWith: 'Built with Vue 3, Naive UI, and TypeScript.', + }, + docs: { + sidebar: { + gettingStarted: 'Getting Started', + configuration: 'Configuration', + features: 'Features', + platforms: 'Platform Guides', + api: 'API Reference', + }, + gettingStarted: { + title: 'Getting Started', + intro: 'Hermes Web UI is a self-hosted web dashboard for managing AI conversations, platform channels, scheduled jobs, and more. It wraps the Hermes Agent CLI and provides a beautiful web interface.', + install: { + title: 'Installation', + content: 'Install globally via npm. Node.js 23 or higher is required.', + }, + firstRun: { + title: 'First Run', + content: 'On first start, Hermes Web UI will automatically generate an auth token, validate configuration files, start the Hermes gateway, and open the dashboard in your browser.', + }, + login: { + title: 'Login', + content: 'The auto-generated token is stored in ~/.hermes-web-ui/.token. You can also set up username/password login from the Settings page after your first login.', + }, + }, + configuration: { + title: 'Configuration', + intro: 'Hermes Web UI can be configured via environment variables.', + envVars: { + title: 'Environment Variables', + rows: [ + ['AUTH_DISABLED', 'Set to "1" to disable authentication'], + ['AUTH_TOKEN', 'Custom auth token (overrides auto-generated)'], + ['PORT', 'Server listen port (default: 8648)'], + ['UPSTREAM', 'Hermes gateway URL (default: http://127.0.0.1:8642)'], + ['UPLOAD_DIR', 'Custom upload directory path'], + ['CORS_ORIGINS', 'CORS origin config (default: *)'], + ['HERMES_BIN', 'Custom path to hermes CLI binary'], + ], + }, + gateway: { + title: 'Gateway Management', + content: 'The gateway is the Hermes Agent process that handles AI conversations. Hermes Web UI manages the gateway lifecycle — start, stop, and monitor from the Gateways page. Multiple gateways can run with different profiles.', + }, + profiles: { + title: 'Profiles', + content: 'Profiles provide isolated configurations for different use cases. Each profile has its own Hermes config, cache, and gateway. Create, clone, import, or export profiles from the Profiles page.', + }, + }, + features: { + title: 'Features', + intro: 'Explore the core features of Hermes Web UI.', + chat: { + title: 'AI Chat', + content: 'Real-time streaming chat powered by Server-Sent Events. Supports multi-session management, Markdown rendering with syntax highlighting, tool call inspection, file upload/download, and global search across all conversations (Ctrl+K).', + }, + kanban: { + title: 'Kanban Board', + content: 'A visual task management board with 7 status columns: triage, todo, ready, running, blocked, done, and archived. Supports assignee management, filtering, and detailed task editing via a side drawer.', + }, + groupChat: { + title: 'Group Chat', + content: 'Multi-agent chat rooms where multiple AI agents collaborate. Features mention routing to trigger specific agents, automatic context compression when history exceeds limits, typing indicators, and SQLite-based message persistence.', + }, + jobs: { + title: 'Scheduled Jobs', + content: 'Create and manage cron-based scheduled jobs that run AI tasks automatically. Configure schedule, prompt, and model for each job.', + }, + skills: { + title: 'Skills', + content: 'Browse and manage installed AI skills. Skills extend the agent\'s capabilities with specialized knowledge and tool integrations.', + }, + memory: { + title: 'Memory', + content: 'Manage agent memory and user notes. The agent uses memory to maintain context across conversations and personalize responses.', + }, + terminal: { + title: 'Terminal', + content: 'Full pseudo-terminal in the browser powered by node-pty and xterm.js. Supports multiple terminal sessions, real-time keyboard input, and window resizing via WebSocket.', + }, + files: { + title: 'File Browser', + content: 'Browse and manage files on remote backends including local, Docker, SSH, and Singularity. Upload, download, rename, move, delete files, and preview content with syntax highlighting.', + }, + analytics: { + title: 'Usage Analytics', + content: 'Track token usage (input/output), estimated costs, cache hit rates, session counts, and model distribution. View 30-day daily trends with interactive charts.', + }, + }, + platforms: { + title: 'Platform Guides', + intro: 'Configure messaging platform integrations from the Channels settings page.', + telegram: { + title: 'Telegram', + content: 'Create a Telegram Bot via BotFather, then enter the bot token. Configure mention requirements, free-response chats, and reaction handling.', + }, + discord: { + title: 'Discord', + content: 'Create a Discord Bot in the Developer Portal. Supports auto-thread creation, allowed/ignored channels, reaction handling, and free-response channels.', + }, + slack: { + title: 'Slack', + content: 'Create a Slack App with bot token scope. Configure mention requirements, bot allowlisting, and free-response channels.', + }, + whatsapp: { + title: 'WhatsApp', + content: 'Enable WhatsApp integration and configure mention patterns and free-response chats.', + }, + matrix: { + title: 'Matrix', + content: 'Provide access token and homeserver URL. Supports auto-thread, DM mention threads, and free-response rooms.', + }, + feishu: { + title: 'Feishu (Lark)', + content: 'Register a Feishu app and configure App ID and Secret.', + }, + wechat: { + title: 'WeChat', + content: 'Scan the QR code from the settings page to log in. Credentials are auto-saved for subsequent sessions.', + }, + wecom: { + title: 'WeCom', + content: 'Configure Bot ID and Secret from the WeCom admin console.', + }, + }, + api: { + title: 'API Reference', + intro: 'Hermes Web UI provides both a local BFF API and proxies requests to the upstream Hermes gateway.', + local: { + title: 'Local BFF Endpoints', + content: 'The Koa server handles session management, profile CRUD, config read/write, log access, skill listing, and memory operations. These endpoints call the Hermes CLI directly.', + }, + proxy: { + title: 'Gateway Proxy', + content: 'Requests to /api/hermes/v1/* are forwarded to the Hermes gateway. This includes AI model interactions, run management, and streaming events.', + }, + auth: { + title: 'Authentication', + content: 'All API endpoints require a Bearer token via the Authorization header. The token is auto-generated on first run and stored in ~/.hermes-web-ui/.token. Optional username/password login can be configured from the Settings page.', + }, + }, + }, +} diff --git a/packages/website/src/i18n/index.ts b/packages/website/src/i18n/index.ts new file mode 100644 index 0000000..29ba3dc --- /dev/null +++ b/packages/website/src/i18n/index.ts @@ -0,0 +1,13 @@ +import { createI18n } from 'vue-i18n' +import en from './en' +import zh from './zh' + +const detected = navigator.language.startsWith('zh') ? 'zh' : 'en' +const saved = localStorage.getItem('hermes_website_locale') + +export const i18n = createI18n({ + legacy: false, + locale: saved || detected, + fallbackLocale: 'en', + messages: { en, zh }, +}) diff --git a/packages/website/src/i18n/zh.ts b/packages/website/src/i18n/zh.ts new file mode 100644 index 0000000..166267a --- /dev/null +++ b/packages/website/src/i18n/zh.ts @@ -0,0 +1,247 @@ +export default { + nav: { + home: '首页', + docs: '文档', + github: 'GitHub', + }, + hero: { + title: '自托管 AI 聊天仪表板', + subtitle: '开源 AI Agent 仪表板 — 流式对话、多模型调度、看板管理、用量分析、Web 终端,一个界面掌控一切。', + cta: '快速开始', + viewGithub: '查看 GitHub', + install: 'npm install -g hermes-web-ui', + }, + features: { + title: '功能齐全', + desc: '开箱即用的完整 AI Agent 管理仪表板。', + streaming: { + title: '流式聊天', + desc: '基于 SSE 的实时 AI 对话,支持多会话管理、Markdown 渲染和代码语法高亮。', + }, + platforms: { + title: '8 大平台', + desc: '统一管理 Telegram、Discord、Slack、WhatsApp、Matrix、飞书、微信、企业微信。', + }, + multiModel: { + title: '多模型支持', + desc: '支持 Claude、GPT、Gemini、DeepSeek 及任何 OpenAI 兼容模型,自动发现。', + }, + groupChat: { + title: '群聊协作', + desc: '多 Agent 聊天室,支持提及路由、上下文压缩和实时协作。', + }, + kanban: { + title: '看板管理', + desc: '可视化任务看板,7 个状态列,支持任务分配和筛选。', + }, + analytics: { + title: '用量分析', + desc: 'Token 用量、费用追踪、缓存命中率、模型分布和 30 天趋势。', + }, + profiles: { + title: '多配置', + desc: '隔离的多配置文件,独立配置。支持克隆、导入/导出、多网关运行。', + }, + files: { + title: '文件管理', + desc: '跨本地、Docker、SSH 和 Singularity 管理文件,支持上传、预览和编辑。', + }, + terminal: { + title: 'Web 终端', + desc: '浏览器内完整 PTY 终端,基于 WebSocket 和 xterm.js 的多会话支持。', + }, + quickInstall: { + title: '一键安装', + desc: '一条命令安装启动。自动检测配置、解析端口、打开浏览器。', + }, + i18n: { + title: '8 种语言', + desc: '内置英语、中文、德语、西班牙语、法语、日语、韩语和葡萄牙语。', + }, + theme: { + title: '暗色 / 亮色', + desc: '水墨单色设计,平滑主题切换,响应式布局适配移动端和桌面端。', + }, + }, + platforms: { + title: '统一平台管理', + desc: '在一个页面配置 8 大消息平台的凭证和行为。', + telegram: 'Telegram', + discord: 'Discord', + slack: 'Slack', + whatsapp: 'WhatsApp', + matrix: 'Matrix', + feishu: '飞书', + wechat: '微信', + wecom: '企业微信', + }, + install: { + title: '快速开始', + desc: '一分钟内启动 Hermes Web UI。', + npm: { + title: 'npm', + cmd1: 'npm install -g hermes-web-ui', + cmd2: 'hermes-web-ui start', + }, + docker: { + title: 'Docker', + cmd: 'docker compose up -d', + }, + source: { + title: '源码安装', + cmd1: 'git clone https://github.com/EKKOLearnAI/hermes-web-ui.git', + cmd2: 'cd hermes-web-ui && npm install && npm run dev', + }, + prereq: '需要 Node.js >= 23', + }, + starHistory: { + title: '社区成长', + desc: '在 GitHub 上给我们加星,加入社区。', + }, + footer: { + description: 'Hermes Agent 的自托管 AI 聊天仪表板。', + license: 'MIT 开源协议', + madeWith: '使用 Vue 3、Naive UI 和 TypeScript 构建。', + }, + docs: { + sidebar: { + gettingStarted: '快速开始', + configuration: '配置说明', + features: '功能详解', + platforms: '平台接入', + api: 'API 参考', + }, + gettingStarted: { + title: '快速开始', + intro: 'Hermes Web UI 是一个自托管的 Web 仪表板,用于管理 AI 对话、平台通道、定时任务等。它封装了 Hermes Agent CLI 并提供美观的 Web 界面。', + install: { + title: '安装', + content: '通过 npm 全局安装。需要 Node.js 23 或更高版本。', + }, + firstRun: { + title: '首次运行', + content: '首次启动时,Hermes Web UI 会自动生成认证令牌、验证配置文件、启动 Hermes 网关并在浏览器中打开仪表板。', + }, + login: { + title: '登录', + content: '自动生成的令牌存储在 ~/.hermes-web-ui/.token。首次登录后可在设置页面配置用户名/密码登录。', + }, + }, + configuration: { + title: '配置说明', + intro: 'Hermes Web UI 可通过环境变量进行配置。', + envVars: { + title: '环境变量', + rows: [ + ['AUTH_DISABLED', '设为 "1" 禁用认证'], + ['AUTH_TOKEN', '自定义认证令牌(覆盖自动生成的令牌)'], + ['PORT', '服务器监听端口(默认:8648)'], + ['UPSTREAM', 'Hermes 网关 URL(默认:http://127.0.0.1:8642)'], + ['UPLOAD_DIR', '自定义上传目录路径'], + ['CORS_ORIGINS', 'CORS 来源配置(默认:*)'], + ['HERMES_BIN', '自定义 hermes CLI 二进制路径'], + ], + }, + gateway: { + title: '网关管理', + content: '网关是处理 AI 对话的 Hermes Agent 进程。Hermes Web UI 管理网关生命周期——在网关页面启动、停止和监控。不同配置可运行多个网关。', + }, + profiles: { + title: '配置文件', + content: '配置文件为不同场景提供隔离的配置。每个配置文件拥有独立的 Hermes 配置、缓存和网关。可在配置页面创建、克隆、导入或导出配置文件。', + }, + }, + features: { + title: '功能详解', + intro: '探索 Hermes Web UI 的核心功能。', + chat: { + title: 'AI 聊天', + content: '基于 Server-Sent Events 的实时流式聊天。支持多会话管理、Markdown 渲染与语法高亮、工具调用检查、文件上传/下载以及全局搜索 (Ctrl+K)。', + }, + kanban: { + title: '看板管理', + content: '可视化任务看板,包含 7 个状态列:分流、待办、就绪、运行中、阻塞、完成和已归档。支持任务分配、筛选和通过侧边抽屉进行详细编辑。', + }, + groupChat: { + title: '群聊协作', + content: '多 Agent 聊天室,多个 AI Agent 协同工作。支持提及路由触发特定 Agent、历史记录超限时自动压缩上下文、输入状态指示和基于 SQLite 的消息持久化。', + }, + jobs: { + title: '定时任务', + content: '创建和管理基于 cron 的定时任务,自动运行 AI 任务。可配置计划、提示词和模型。', + }, + skills: { + title: '技能', + content: '浏览和管理已安装的 AI 技能。技能通过专业知识和工具集成扩展 Agent 能力。', + }, + memory: { + title: '记忆', + content: '管理 Agent 记忆和用户笔记。Agent 使用记忆在对话间保持上下文并提供个性化回复。', + }, + terminal: { + title: '终端', + content: '基于 node-pty 和 xterm.js 的浏览器内完整伪终端。支持多个终端会话、实时键盘输入和通过 WebSocket 的窗口大小调整。', + }, + files: { + title: '文件管理', + content: '浏览和管理本地、Docker、SSH 和 Singularity 等远程后端上的文件。支持上传、下载、重命名、移动、删除文件以及带语法高亮的内容预览。', + }, + analytics: { + title: '用量分析', + content: '追踪 Token 用量(输入/输出)、预估费用、缓存命中率、会话数和模型分布。查看 30 天日趋势交互图表。', + }, + }, + platforms: { + title: '平台接入', + intro: '从通道设置页面配置消息平台集成。', + telegram: { + title: 'Telegram', + content: '通过 BotFather 创建 Telegram Bot,输入 Bot Token。可配置提及要求、自由回复聊天和反应处理。', + }, + discord: { + title: 'Discord', + content: '在开发者门户创建 Discord Bot。支持自动创建线程、允许/忽略频道、反应处理和自由回复频道。', + }, + slack: { + title: 'Slack', + content: '创建带有 bot token 权限的 Slack App。配置提及要求、Bot 白名单和自由回复频道。', + }, + whatsapp: { + title: 'WhatsApp', + content: '启用 WhatsApp 集成,配置提及模式和自由回复聊天。', + }, + matrix: { + title: 'Matrix', + content: '提供访问令牌和服务器 URL。支持自动线程、私聊提及线程和自由回复房间。', + }, + feishu: { + title: '飞书', + content: '注册飞书应用并配置 App ID 和 Secret。', + }, + wechat: { + title: '微信', + content: '从设置页面扫描二维码登录。凭据会自动保存供后续使用。', + }, + wecom: { + title: '企业微信', + content: '从企业微信管理后台配置 Bot ID 和 Secret。', + }, + }, + api: { + title: 'API 参考', + intro: 'Hermes Web UI 提供本地 BFF API 并代理请求到上游 Hermes 网关。', + local: { + title: '本地 BFF 端点', + content: 'Koa 服务器处理会话管理、配置文件 CRUD、配置读写、日志访问、技能列表和记忆操作。这些端点直接调用 Hermes CLI。', + }, + proxy: { + title: '网关代理', + content: '对 /api/hermes/v1/* 的请求会转发到 Hermes 网关。包括 AI 模型交互、运行管理和流式事件。', + }, + auth: { + title: '认证', + content: '所有 API 端点需要通过 Authorization 头提供 Bearer 令牌。令牌在首次运行时自动生成并存储在 ~/.hermes-web-ui/.token。可在设置页面配置可选的用户名/密码登录。', + }, + }, + }, +} diff --git a/packages/website/src/main.ts b/packages/website/src/main.ts new file mode 100644 index 0000000..99b473f --- /dev/null +++ b/packages/website/src/main.ts @@ -0,0 +1,18 @@ +import { createApp } from 'vue' +import router from './router' +import { i18n } from './i18n' +import App from './App.vue' +// Import CSS custom properties (theme variables) from client +import '@client/styles/variables.scss' +import './styles/global.scss' + +const savedTheme = localStorage.getItem('hermes_website_theme') || 'system' +const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches +if (savedTheme === 'dark' || (savedTheme === 'system' && prefersDark)) { + document.documentElement.classList.add('dark') +} + +const app = createApp(App) +app.use(i18n) +app.use(router) +app.mount('#app') diff --git a/packages/website/src/router/index.ts b/packages/website/src/router/index.ts new file mode 100644 index 0000000..4148bf3 --- /dev/null +++ b/packages/website/src/router/index.ts @@ -0,0 +1,61 @@ +import { createRouter, createWebHashHistory } from 'vue-router' + +const EmptyView = { render: () => null } + +const router = createRouter({ + history: createWebHashHistory(), + routes: [ + { + path: '/', + name: 'landing', + component: () => import('@/views/LandingView.vue'), + }, + { + path: '/docs', + name: 'docs', + component: () => import('@/views/DocsView.vue'), + redirect: { name: 'docs.getting-started' }, + children: [ + { + path: 'getting-started', + name: 'docs.getting-started', + component: EmptyView, + meta: { page: 'gettingStarted' }, + }, + { + path: 'configuration', + name: 'docs.configuration', + component: EmptyView, + meta: { page: 'configuration' }, + }, + { + path: 'features', + name: 'docs.features', + component: EmptyView, + meta: { page: 'features' }, + }, + { + path: 'platforms', + name: 'docs.platforms', + component: EmptyView, + meta: { page: 'platforms' }, + }, + { + path: 'api', + name: 'docs.api', + component: EmptyView, + meta: { page: 'api' }, + }, + ], + }, + { + path: '/:pathMatch(.*)*', + redirect: '/', + }, + ], + scrollBehavior() { + return { top: 0 } + }, +}) + +export default router diff --git a/packages/website/src/styles/_variables.scss b/packages/website/src/styles/_variables.scss new file mode 100644 index 0000000..b776c0a --- /dev/null +++ b/packages/website/src/styles/_variables.scss @@ -0,0 +1,14 @@ +// Website SCSS variables — pure constants (no CSS custom properties) +// CSS custom properties are defined in global.scss (imported from client) + +$font-ui: 'Inter', system-ui, -apple-system, sans-serif; +$font-code: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; + +$breakpoint-mobile: 768px; + +$radius-sm: 6px; +$radius-md: 10px; +$radius-lg: 14px; + +$transition-fast: 0.15s ease; +$transition-normal: 0.25s ease; diff --git a/packages/website/src/styles/global.scss b/packages/website/src/styles/global.scss new file mode 100644 index 0000000..5c8ce9d --- /dev/null +++ b/packages/website/src/styles/global.scss @@ -0,0 +1,132 @@ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 14px; + line-height: 1.6; + scroll-behavior: smooth; +} + +body { + font-family: $font-ui; + background: var(--bg-primary); + color: var(--text-primary); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + color: var(--accent-primary); + text-decoration: none; + + &:hover { + color: var(--accent-hover); + } +} + +code { + font-family: $font-code; + background: var(--code-bg); + padding: 2px 6px; + border-radius: $radius-sm; + font-size: 0.9em; +} + +pre { + code { + display: block; + padding: 16px; + border-radius: $radius-md; + overflow-x: auto; + } +} + +.section { + max-width: 1120px; + margin: 0 auto; + padding: 80px 24px; + + @media (max-width: $breakpoint-mobile) { + padding: 48px 16px; + } +} + +.section-title { + font-size: 32px; + font-weight: 700; + text-align: center; + margin-bottom: 16px; + color: var(--text-primary); + + @media (max-width: $breakpoint-mobile) { + font-size: 24px; + } +} + +.section-desc { + text-align: center; + color: var(--text-secondary); + font-size: 16px; + max-width: 640px; + margin: 0 auto 48px; +} + +// ─── Scroll reveal animations ──────────────────────────── + +.reveal { + opacity: 0; + transform: translateY(24px); + transition: opacity 0.6s ease, transform 0.6s ease; + + &.revealed { + opacity: 1; + transform: translateY(0); + } +} + +.reveal-delay-1 { transition-delay: 0.08s; } +.reveal-delay-2 { transition-delay: 0.16s; } +.reveal-delay-3 { transition-delay: 0.24s; } +.reveal-delay-4 { transition-delay: 0.32s; } + +// ─── Keyframes ──────────────────────────────────────────── + +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(32px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes pulse-border { + 0%, 100% { border-color: var(--border-color); } + 50% { border-color: var(--text-muted); } +} + +.animate-fade-in-up { + animation: fade-in-up 0.7s ease both; +} + +.animate-fade-in { + animation: fade-in 0.5s ease both; +} + +.animate-delay-1 { animation-delay: 0.1s; } +.animate-delay-2 { animation-delay: 0.2s; } +.animate-delay-3 { animation-delay: 0.3s; } +.animate-delay-4 { animation-delay: 0.4s; } +.animate-delay-5 { animation-delay: 0.5s; } diff --git a/packages/website/src/views/DocsView.vue b/packages/website/src/views/DocsView.vue new file mode 100644 index 0000000..77c68d5 --- /dev/null +++ b/packages/website/src/views/DocsView.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/packages/website/src/views/LandingView.vue b/packages/website/src/views/LandingView.vue new file mode 100644 index 0000000..5c4c67f --- /dev/null +++ b/packages/website/src/views/LandingView.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..e9d1e0e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "files": [], "references": [ { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.website.json" } ] } diff --git a/tsconfig.website.json b/tsconfig.website.json new file mode 100644 index 0000000..9b1ce43 --- /dev/null +++ b/tsconfig.website.json @@ -0,0 +1,19 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "lib": ["ES2025", "DOM", "DOM.Iterable"], + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.website.tsbuildinfo", + "types": ["vite/client"], + "ignoreDeprecations": "6.0", + "baseUrl": ".", + "paths": { + "@/*": ["packages/website/src/*"], + "@client/*": ["packages/client/src/*"] + }, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["packages/website/src/**/*.ts", "packages/website/src/**/*.tsx", "packages/website/src/**/*.vue"] +} diff --git a/vite.config.website.ts b/vite.config.website.ts new file mode 100644 index 0000000..903b012 --- /dev/null +++ b/vite.config.website.ts @@ -0,0 +1,54 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' +import pkg from './package.json' + +export default defineConfig({ + root: 'packages/website', + plugins: [vue()], + define: { + __APP_VERSION__: JSON.stringify(pkg.version), + }, + resolve: { + alias: { + '@': resolve(__dirname, 'packages/website/src'), + '@client': resolve(__dirname, 'packages/client/src'), + }, + }, + css: { + preprocessorOptions: { + scss: { + additionalData: `@use "@/styles/variables" as *;\n`, + }, + }, + }, + build: { + outDir: '../../dist/website', + emptyOutDir: true, + minify: 'esbuild', + sourcemap: false, + target: 'es2020', + cssCodeSplit: true, + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('node_modules')) { + if (id.includes('vue') || id.includes('vue-router') || id.includes('vue-i18n') || id.includes('pinia')) { + return 'vue-vendor' + } + if (id.includes('naive-ui')) { + return 'ui-vendor' + } + return 'vendor' + } + }, + chunkFileNames: 'assets/js/[name]-[hash].js', + entryFileNames: 'assets/js/[name]-[hash].js', + assetFileNames: 'assets/[ext]/[name]-[hash].[ext]', + }, + }, + }, + server: { + port: 3000, + }, +})