add website downloads and deploy workflow (#1165)
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
name: Website
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- base
|
||||||
|
paths:
|
||||||
|
- packages/website/**
|
||||||
|
- packages/client/src/styles/variables.scss
|
||||||
|
- package.json
|
||||||
|
- package-lock.json
|
||||||
|
- tsconfig.website.json
|
||||||
|
- vite.config.website.ts
|
||||||
|
- .github/workflows/website-deploy.yml
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: website-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build website
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --ignore-scripts
|
||||||
|
|
||||||
|
- name: Type-check website
|
||||||
|
run: npx vue-tsc -p tsconfig.website.json --noEmit
|
||||||
|
|
||||||
|
- name: Build website
|
||||||
|
run: npm run build:website
|
||||||
|
|
||||||
|
- name: Prepare SSH
|
||||||
|
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||||
|
env:
|
||||||
|
WEBSITE_SSH_KEY: ${{ secrets.WEBSITE_SSH_KEY }}
|
||||||
|
WEBSITE_SSH_KNOWN_HOSTS: ${{ secrets.WEBSITE_SSH_KNOWN_HOSTS }}
|
||||||
|
run: |
|
||||||
|
test -n "$WEBSITE_SSH_KEY"
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
chmod 700 ~/.ssh
|
||||||
|
printf '%s\n' "$WEBSITE_SSH_KEY" > ~/.ssh/website_deploy_key
|
||||||
|
chmod 600 ~/.ssh/website_deploy_key
|
||||||
|
if [ -n "$WEBSITE_SSH_KNOWN_HOSTS" ]; then
|
||||||
|
printf '%s\n' "$WEBSITE_SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Deploy website
|
||||||
|
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||||
|
env:
|
||||||
|
WEBSITE_SSH_USER: ${{ secrets.WEBSITE_SSH_USER }}
|
||||||
|
WEBSITE_SSH_PORT: ${{ secrets.WEBSITE_SSH_PORT }}
|
||||||
|
run: |
|
||||||
|
SSH_USER="${WEBSITE_SSH_USER:-root}"
|
||||||
|
SSH_PORT="${WEBSITE_SSH_PORT:-22}"
|
||||||
|
command -v rsync >/dev/null || {
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y rsync
|
||||||
|
}
|
||||||
|
rsync -az --delete \
|
||||||
|
-e "ssh -i ~/.ssh/website_deploy_key -p ${SSH_PORT} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \
|
||||||
|
dist/website/ \
|
||||||
|
"$SSH_USER@154.3.33.232:/var/www/ekkolearnai.com/current/"
|
||||||
@@ -4,12 +4,14 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
A full-featured web dashboard for <a href="https://github.com/NousResearch/hermes-agent">Hermes Agent</a>.<br/>
|
A full-featured desktop app and web dashboard for <a href="https://github.com/NousResearch/hermes-agent">Hermes Agent</a>.<br/>
|
||||||
Manage AI chat sessions, monitor usage & costs, configure platform channels,<br/>
|
Manage AI chat sessions, monitor usage & costs, configure platform channels,<br/>
|
||||||
schedule cron jobs, browse skills — all from a clean, responsive web interface.
|
schedule cron jobs, browse skills — all from a clean, responsive web interface.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest">Download Hermes Studio Desktop</a>
|
||||||
|
·
|
||||||
<code>npm install -g hermes-web-ui && hermes-web-ui start</code>
|
<code>npm install -g hermes-web-ui && hermes-web-ui start</code>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -171,7 +173,22 @@ hermes-web-ui reset-default-login
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### npm (Recommended)
|
### Desktop App (Recommended)
|
||||||
|
|
||||||
|
Download the latest **Hermes Studio** desktop installer from
|
||||||
|
[GitHub Releases](https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest).
|
||||||
|
|
||||||
|
Desktop builds are published for macOS, Windows, and Linux, with separate
|
||||||
|
architecture assets where applicable. The desktop app bundles the Web UI
|
||||||
|
runtime and stores Hermes Agent data in the native Hermes location:
|
||||||
|
|
||||||
|
- Windows: `%LOCALAPPDATA%\hermes` (falls back to `%APPDATA%\hermes`)
|
||||||
|
- macOS/Linux: `~/.hermes`
|
||||||
|
|
||||||
|
The desktop wrapper stores its own Web UI state separately in
|
||||||
|
`~/.hermes-web-ui` unless `HERMES_WEB_UI_HOME` is set.
|
||||||
|
|
||||||
|
### npm
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g hermes-web-ui
|
npm install -g hermes-web-ui
|
||||||
|
|||||||
+18
-2
@@ -4,12 +4,14 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/NousResearch/hermes-agent">Hermes Agent</a> 的全功能 Web 管理面板。<br/>
|
<a href="https://github.com/NousResearch/hermes-agent">Hermes Agent</a> 的全功能桌面应用和 Web 管理面板。<br/>
|
||||||
管理 AI 聊天会话、监控用量与成本、配置平台渠道、<br/>
|
管理 AI 聊天会话、监控用量与成本、配置平台渠道、<br/>
|
||||||
管理定时任务、浏览技能 —— 全部在一个简洁响应式的 Web 界面中完成。
|
管理定时任务、浏览技能 —— 全部在一个简洁响应式的 Web 界面中完成。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest">下载 Hermes Studio 桌面版</a>
|
||||||
|
·
|
||||||
<code>npm install -g hermes-web-ui && hermes-web-ui start</code>
|
<code>npm install -g hermes-web-ui && hermes-web-ui start</code>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -179,7 +181,21 @@ hermes-web-ui reset-default-login
|
|||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### npm 安装(推荐)
|
### 桌面应用(推荐)
|
||||||
|
|
||||||
|
从 [GitHub Releases](https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest)
|
||||||
|
下载最新的 **Hermes Studio** 桌面安装包。
|
||||||
|
|
||||||
|
桌面版会发布 macOS、Windows 和 Linux 构建;适用时会区分不同 CPU 架构。
|
||||||
|
桌面应用内置 Web UI 运行时,Hermes Agent 数据会保存到原生 Hermes 目录:
|
||||||
|
|
||||||
|
- Windows:`%LOCALAPPDATA%\hermes`(找不到时回退到 `%APPDATA%\hermes`)
|
||||||
|
- macOS/Linux:`~/.hermes`
|
||||||
|
|
||||||
|
桌面壳自身的 Web UI 状态会单独保存到 `~/.hermes-web-ui`,除非设置了
|
||||||
|
`HERMES_WEB_UI_HOME`。
|
||||||
|
|
||||||
|
### npm 安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g hermes-web-ui
|
npm install -g hermes-web-ui
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
Electron desktop distribution for Hermes Studio.
|
Electron desktop distribution for Hermes Studio.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Download the latest macOS, Windows, or Linux installer for your CPU
|
||||||
|
architecture from the project
|
||||||
|
[GitHub Releases](https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest).
|
||||||
|
|
||||||
|
The desktop app bundles the Web UI runtime and launches it locally from the
|
||||||
|
native shell app.
|
||||||
|
|
||||||
## Data directories
|
## Data directories
|
||||||
|
|
||||||
Hermes Agent data is stored in the same platform-specific location as native
|
Hermes Agent data is stored in the same platform-specific location as native
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ onMounted(() => {
|
|||||||
<div class="install-box animate-fade-in animate-delay-3">
|
<div class="install-box animate-fade-in animate-delay-3">
|
||||||
<code>{{ installCmd }}</code>
|
<code>{{ installCmd }}</code>
|
||||||
<button class="copy-btn" @click="copyCmd">
|
<button class="copy-btn" @click="copyCmd">
|
||||||
{{ copied ? 'Copied!' : 'Copy' }}
|
{{ copied ? t('ui.copied') : t('ui.copy') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useScrollReveal } from '@/composables/useScrollReveal'
|
import { useScrollReveal } from '@/composables/useScrollReveal'
|
||||||
|
|
||||||
const { t } = useI18n()
|
interface DesktopDownload {
|
||||||
|
title: string
|
||||||
|
desc: string
|
||||||
|
assetSuffix: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t, tm } = useI18n()
|
||||||
useScrollReveal()
|
useScrollReveal()
|
||||||
const activeTab = ref<'npm' | 'docker' | 'source'>('npm')
|
const activeTab = ref<'desktop' | 'npm' | 'docker' | 'source'>('desktop')
|
||||||
|
|
||||||
|
const releaseVersion = __APP_VERSION__.replace(/^v/, '')
|
||||||
|
const releaseTag = `v${releaseVersion}`
|
||||||
|
const releaseBaseUrl = 'https://github.com/EKKOLearnAI/hermes-web-ui/releases'
|
||||||
|
const releaseUrl = `${releaseBaseUrl}/tag/${releaseTag}`
|
||||||
|
const releaseDownloadUrl = `${releaseBaseUrl}/download/${releaseTag}`
|
||||||
|
const desktopDownloads = computed(() =>
|
||||||
|
(tm('install.desktop.downloads') as DesktopDownload[]).map((item) => ({
|
||||||
|
...item,
|
||||||
|
href: `${releaseDownloadUrl}/Hermes.Studio-${releaseVersion}-${item.assetSuffix}`,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
function copyText(text: string) {
|
function copyText(text: string) {
|
||||||
navigator.clipboard.writeText(text).catch(() => {})
|
navigator.clipboard.writeText(text).catch(() => {})
|
||||||
@@ -19,7 +37,7 @@ function copyText(text: string) {
|
|||||||
|
|
||||||
<div class="install-tabs reveal">
|
<div class="install-tabs reveal">
|
||||||
<button
|
<button
|
||||||
v-for="tab in (['npm', 'docker', 'source'] as const)"
|
v-for="tab in (['desktop', 'npm', 'docker', 'source'] as const)"
|
||||||
:key="tab"
|
:key="tab"
|
||||||
class="tab-btn"
|
class="tab-btn"
|
||||||
:class="{ active: activeTab === tab }"
|
:class="{ active: activeTab === tab }"
|
||||||
@@ -30,7 +48,33 @@ function copyText(text: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="install-content reveal reveal-delay-1">
|
<div class="install-content reveal reveal-delay-1">
|
||||||
<template v-if="activeTab === 'npm'">
|
<template v-if="activeTab === 'desktop'">
|
||||||
|
<div class="download-list">
|
||||||
|
<a
|
||||||
|
v-for="item in desktopDownloads"
|
||||||
|
:key="item.href"
|
||||||
|
class="download-row"
|
||||||
|
:href="item.href"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<strong>{{ item.title }}</strong>
|
||||||
|
<small>{{ item.desc }}</small>
|
||||||
|
</span>
|
||||||
|
<span class="download-action">{{ t('install.desktop.download') }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
class="all-downloads"
|
||||||
|
:href="releaseUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
{{ t('install.desktop.allDownloads') }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="activeTab === 'npm'">
|
||||||
<div class="code-block" @click="copyText(t('install.npm.cmd1'))">
|
<div class="code-block" @click="copyText(t('install.npm.cmd1'))">
|
||||||
<code>{{ t('install.npm.cmd1') }}</code>
|
<code>{{ t('install.npm.cmd1') }}</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +95,7 @@ function copyText(text: string) {
|
|||||||
<code>{{ t('install.source.cmd2') }}</code>
|
<code>{{ t('install.source.cmd2') }}</code>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<p class="prereq">{{ t('install.prereq') }}</p>
|
<p class="prereq">{{ activeTab === 'desktop' ? t('install.desktop.prereq') : t('install.prereq') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,6 +132,8 @@ function copyText(text: string) {
|
|||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: $radius-md;
|
border-radius: $radius-md;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn {
|
.tab-btn {
|
||||||
@@ -114,6 +160,65 @@ function copyText(text: string) {
|
|||||||
// full width within panel
|
// full width within panel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 14px 0;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .download-action {
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
strong,
|
||||||
|
small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-action {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
padding: 7px 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: border-color $transition-fast;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-downloads {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-top: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.code-block {
|
.code-block {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useScrollReveal } from '@/composables/useScrollReveal'
|
import { useScrollReveal } from '@/composables/useScrollReveal'
|
||||||
|
|
||||||
|
interface ScreenshotItem {
|
||||||
|
src: string
|
||||||
|
alt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t, tm } = useI18n()
|
||||||
useScrollReveal()
|
useScrollReveal()
|
||||||
|
|
||||||
const images = [
|
const images = computed(() => tm('screenshots.items') as ScreenshotItem[])
|
||||||
{ src: '/image1.png', alt: 'AI Chat with Image Generation' },
|
|
||||||
{ src: '/image2.png', alt: 'Chat and File Browser' },
|
|
||||||
{ src: '/image3.png', alt: 'Multi-panel Workspace' },
|
|
||||||
{ src: '/image4.png', alt: 'Kanban Board' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const activeIndex = ref(0)
|
const activeIndex = ref(0)
|
||||||
let timer: ReturnType<typeof setInterval>
|
let timer: ReturnType<typeof setInterval>
|
||||||
|
|
||||||
function next() {
|
function next() {
|
||||||
activeIndex.value = (activeIndex.value + 1) % images.length
|
activeIndex.value = (activeIndex.value + 1) % images.value.length
|
||||||
}
|
}
|
||||||
|
|
||||||
function prev() {
|
function prev() {
|
||||||
activeIndex.value = (activeIndex.value - 1 + images.length) % images.length
|
activeIndex.value = (activeIndex.value - 1 + images.value.length) % images.value.length
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActive(i: number) {
|
function setActive(i: number) {
|
||||||
@@ -53,7 +54,7 @@ onUnmounted(() => {
|
|||||||
<span class="dot green" />
|
<span class="dot green" />
|
||||||
</div>
|
</div>
|
||||||
<div class="browser-url">
|
<div class="browser-url">
|
||||||
<span>http://localhost:8648</span>
|
<span>{{ t('screenshots.localUrl') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="browser-spacer" />
|
<div class="browser-spacer" />
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +72,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<div class="screenshot-nav">
|
<div class="screenshot-nav">
|
||||||
<button class="nav-arrow" @click="prev(); resetTimer()">
|
<button class="nav-arrow" :aria-label="t('screenshots.previous')" @click="prev(); resetTimer()">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6" /></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6" /></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -80,12 +81,13 @@ onUnmounted(() => {
|
|||||||
v-for="(_img, i) in images"
|
v-for="(_img, i) in images"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="dot-btn"
|
class="dot-btn"
|
||||||
|
:aria-label="t('screenshots.goTo', { number: i + 1 })"
|
||||||
:class="{ active: activeIndex === i }"
|
:class="{ active: activeIndex === i }"
|
||||||
@click="setActive(i)"
|
@click="setActive(i)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="nav-arrow" @click="next(); resetTimer()">
|
<button class="nav-arrow" :aria-label="t('screenshots.next')" @click="next(); resetTimer()">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6" /></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6" /></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,19 +39,19 @@ onMounted(async () => {
|
|||||||
<svg viewBox="0 0 24 24" fill="currentColor" class="star-icon">
|
<svg viewBox="0 0 24 24" fill="currentColor" class="star-icon">
|
||||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Star</span>
|
<span>{{ t('starHistory.star') }}</span>
|
||||||
<span v-if="stars !== null" class="star-count">{{ stars.toLocaleString() }}</span>
|
<span v-if="stars !== null" class="star-count">{{ stars.toLocaleString() }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
class="github-badge"
|
class="github-badge"
|
||||||
src="https://img.shields.io/github/license/EKKOLearnAI/hermes-web-ui?style=flat-square"
|
src="https://img.shields.io/github/license/EKKOLearnAI/hermes-web-ui?style=flat-square"
|
||||||
alt="License"
|
:alt="t('starHistory.licenseAlt')"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
class="github-badge"
|
class="github-badge"
|
||||||
src="https://img.shields.io/github/v/release/EKKOLearnAI/hermes-web-ui?style=flat-square"
|
src="https://img.shields.io/github/v/release/EKKOLearnAI/hermes-web-ui?style=flat-square"
|
||||||
alt="Version"
|
:alt="t('starHistory.versionAlt')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="chartSrc"
|
:src="chartSrc"
|
||||||
alt="Star History"
|
:alt="t('starHistory.chartAlt')"
|
||||||
class="chart-img"
|
class="chart-img"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ const { t } = useI18n()
|
|||||||
<div class="footer-inner">
|
<div class="footer-inner">
|
||||||
<div class="footer-left">
|
<div class="footer-left">
|
||||||
<div class="footer-brand">
|
<div class="footer-brand">
|
||||||
<img src="/logo.png" alt="Hermes" class="footer-logo" />
|
<img src="/logo.png" :alt="t('brand.logoAlt')" class="footer-logo" />
|
||||||
<span>Hermes Web UI</span>
|
<span>{{ t('brand.name') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="footer-desc">{{ t('footer.description') }}</p>
|
<p class="footer-desc">{{ t('footer.description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ function goHome() {
|
|||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div class="header-inner">
|
<div class="header-inner">
|
||||||
<div class="header-left" @click="goHome">
|
<div class="header-left" @click="goHome">
|
||||||
<img src="/logo.png" alt="Hermes" class="logo-icon" />
|
<img src="/logo.png" :alt="t('brand.logoAlt')" class="logo-icon" />
|
||||||
<span class="logo-text">Hermes Web UI</span>
|
<span class="logo-text">{{ t('brand.name') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="header-nav">
|
<nav class="header-nav">
|
||||||
@@ -50,10 +50,10 @@ function goHome() {
|
|||||||
<line x1="10" y1="14" x2="21" y2="3" />
|
<line x1="10" y1="14" x2="21" y2="3" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<button class="icon-btn" @click="switchLocale" :title="locale === 'en' ? '中文' : 'English'">
|
<button class="icon-btn" @click="switchLocale" :title="locale === 'en' ? t('ui.switchToChinese') : t('ui.switchToEnglish')">
|
||||||
{{ locale === 'en' ? '中' : 'EN' }}
|
{{ locale === 'en' ? '中' : 'EN' }}
|
||||||
</button>
|
</button>
|
||||||
<button class="icon-btn" @click="toggleTheme" :title="isDark ? 'Light' : 'Dark'">
|
<button class="icon-btn" @click="toggleTheme" :title="isDark ? t('ui.lightTheme') : t('ui.darkTheme')">
|
||||||
<svg v-if="isDark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg v-if="isDark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="5" />
|
<circle cx="12" cy="12" r="5" />
|
||||||
<line x1="12" y1="1" x2="12" y2="3" />
|
<line x1="12" y1="1" x2="12" y2="3" />
|
||||||
@@ -71,7 +71,7 @@ function goHome() {
|
|||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<button class="mobile-toggle" @click="mobileMenuOpen = !mobileMenuOpen">
|
<button class="mobile-toggle" @click="mobileMenuOpen = !mobileMenuOpen" :title="t('ui.menu')">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<line x1="3" y1="6" x2="21" y2="6" />
|
<line x1="3" y1="6" x2="21" y2="6" />
|
||||||
<line x1="3" y1="12" x2="21" y2="12" />
|
<line x1="3" y1="12" x2="21" y2="12" />
|
||||||
@@ -92,7 +92,7 @@ function goHome() {
|
|||||||
<line x1="2" y1="12" x2="22" y2="12" />
|
<line x1="2" y1="12" x2="22" y2="12" />
|
||||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||||
</svg>
|
</svg>
|
||||||
{{ locale === 'en' ? '中文' : 'English' }}
|
{{ locale === 'en' ? t('ui.switchToChinese') : t('ui.switchToEnglish') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="mobile-action-btn" @click="toggleTheme">
|
<button class="mobile-action-btn" @click="toggleTheme">
|
||||||
<svg v-if="isDark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="action-icon">
|
<svg v-if="isDark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="action-icon">
|
||||||
@@ -109,7 +109,7 @@ function goHome() {
|
|||||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="action-icon">
|
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="action-icon">
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||||
</svg>
|
</svg>
|
||||||
{{ isDark ? 'Light Mode' : 'Dark Mode' }}
|
{{ isDark ? t('ui.lightMode') : t('ui.darkMode') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,19 @@
|
|||||||
export default {
|
export default {
|
||||||
|
brand: {
|
||||||
|
name: 'Hermes Web UI',
|
||||||
|
logoAlt: 'Hermes',
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
copy: 'Copy',
|
||||||
|
copied: 'Copied!',
|
||||||
|
darkTheme: 'Dark',
|
||||||
|
lightTheme: 'Light',
|
||||||
|
darkMode: 'Dark Mode',
|
||||||
|
lightMode: 'Light Mode',
|
||||||
|
menu: 'Menu',
|
||||||
|
switchToChinese: 'Chinese',
|
||||||
|
switchToEnglish: 'English',
|
||||||
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
docs: 'Documentation',
|
docs: 'Documentation',
|
||||||
@@ -75,9 +90,59 @@ export default {
|
|||||||
wechat: 'WeChat',
|
wechat: 'WeChat',
|
||||||
wecom: 'WeCom',
|
wecom: 'WeCom',
|
||||||
},
|
},
|
||||||
|
screenshots: {
|
||||||
|
localUrl: 'http://localhost:8648',
|
||||||
|
previous: 'Previous screenshot',
|
||||||
|
next: 'Next screenshot',
|
||||||
|
goTo: 'View screenshot {number}',
|
||||||
|
items: [
|
||||||
|
{ src: '/image1.png', alt: 'AI chat with image generation' },
|
||||||
|
{ src: '/image2.png', alt: 'Chat and file browser' },
|
||||||
|
{ src: '/image3.png', alt: 'Multi-panel workspace' },
|
||||||
|
{ src: '/image4.png', alt: 'Kanban board' },
|
||||||
|
],
|
||||||
|
},
|
||||||
install: {
|
install: {
|
||||||
title: 'Quick Start',
|
title: 'Quick Start',
|
||||||
desc: 'Get Hermes Web UI running in under a minute.',
|
desc: 'Download the desktop app or run Hermes Web UI yourself.',
|
||||||
|
desktop: {
|
||||||
|
title: 'Desktop',
|
||||||
|
download: 'Download',
|
||||||
|
allDownloads: 'View all release assets',
|
||||||
|
prereq: 'Desktop builds bundle the Web UI runtime.',
|
||||||
|
downloads: [
|
||||||
|
{
|
||||||
|
title: 'macOS Apple Silicon',
|
||||||
|
desc: 'Apple Silicon DMG',
|
||||||
|
assetSuffix: 'arm64.dmg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'macOS Intel',
|
||||||
|
desc: 'x64 DMG',
|
||||||
|
assetSuffix: 'x64.dmg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Windows',
|
||||||
|
desc: 'x64 installer',
|
||||||
|
assetSuffix: 'x64.exe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Linux x64 AppImage',
|
||||||
|
desc: 'x64 AppImage',
|
||||||
|
assetSuffix: 'x86_64.AppImage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Linux x64 Debian',
|
||||||
|
desc: 'amd64 .deb package',
|
||||||
|
assetSuffix: 'amd64.deb',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Linux arm64',
|
||||||
|
desc: 'arm64 AppImage',
|
||||||
|
assetSuffix: 'arm64.AppImage',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
npm: {
|
npm: {
|
||||||
title: 'npm',
|
title: 'npm',
|
||||||
cmd1: 'npm install -g hermes-web-ui',
|
cmd1: 'npm install -g hermes-web-ui',
|
||||||
@@ -97,6 +162,10 @@ export default {
|
|||||||
starHistory: {
|
starHistory: {
|
||||||
title: 'Growing Community',
|
title: 'Growing Community',
|
||||||
desc: 'Star us on GitHub and join the community.',
|
desc: 'Star us on GitHub and join the community.',
|
||||||
|
star: 'Star',
|
||||||
|
licenseAlt: 'License',
|
||||||
|
versionAlt: 'Version',
|
||||||
|
chartAlt: 'Star History',
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
description: 'Self-hosted AI chat dashboard for Hermes Agent.',
|
description: 'Self-hosted AI chat dashboard for Hermes Agent.',
|
||||||
@@ -104,6 +173,7 @@ export default {
|
|||||||
madeWith: 'Built with Vue 3, Naive UI, and TypeScript.',
|
madeWith: 'Built with Vue 3, Naive UI, and TypeScript.',
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
|
placeholder: 'Select a section from the sidebar to get started.',
|
||||||
sidebar: {
|
sidebar: {
|
||||||
gettingStarted: 'Getting Started',
|
gettingStarted: 'Getting Started',
|
||||||
configuration: 'Configuration',
|
configuration: 'Configuration',
|
||||||
|
|||||||
@@ -1,4 +1,19 @@
|
|||||||
export default {
|
export default {
|
||||||
|
brand: {
|
||||||
|
name: 'Hermes Web UI',
|
||||||
|
logoAlt: 'Hermes',
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
copy: '复制',
|
||||||
|
copied: '已复制',
|
||||||
|
darkTheme: '深色',
|
||||||
|
lightTheme: '浅色',
|
||||||
|
darkMode: '深色模式',
|
||||||
|
lightMode: '浅色模式',
|
||||||
|
menu: '菜单',
|
||||||
|
switchToChinese: '中文',
|
||||||
|
switchToEnglish: 'English',
|
||||||
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: '首页',
|
home: '首页',
|
||||||
docs: '文档',
|
docs: '文档',
|
||||||
@@ -75,9 +90,59 @@ export default {
|
|||||||
wechat: '微信',
|
wechat: '微信',
|
||||||
wecom: '企业微信',
|
wecom: '企业微信',
|
||||||
},
|
},
|
||||||
|
screenshots: {
|
||||||
|
localUrl: 'http://localhost:8648',
|
||||||
|
previous: '上一张截图',
|
||||||
|
next: '下一张截图',
|
||||||
|
goTo: '查看第 {number} 张截图',
|
||||||
|
items: [
|
||||||
|
{ src: '/image1.png', alt: '带图片生成的 AI 聊天界面' },
|
||||||
|
{ src: '/image2.png', alt: '聊天和文件浏览器界面' },
|
||||||
|
{ src: '/image3.png', alt: '多面板工作区界面' },
|
||||||
|
{ src: '/image4.png', alt: '看板管理界面' },
|
||||||
|
],
|
||||||
|
},
|
||||||
install: {
|
install: {
|
||||||
title: '快速开始',
|
title: '快速开始',
|
||||||
desc: '一分钟内启动 Hermes Web UI。',
|
desc: '下载桌面应用,或自行运行 Hermes Web UI。',
|
||||||
|
desktop: {
|
||||||
|
title: '桌面版',
|
||||||
|
download: '下载',
|
||||||
|
allDownloads: '查看全部发布文件',
|
||||||
|
prereq: '桌面版已内置 Web UI 运行时。',
|
||||||
|
downloads: [
|
||||||
|
{
|
||||||
|
title: 'macOS Apple Silicon',
|
||||||
|
desc: 'Apple Silicon DMG',
|
||||||
|
assetSuffix: 'arm64.dmg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'macOS Intel',
|
||||||
|
desc: 'x64 DMG',
|
||||||
|
assetSuffix: 'x64.dmg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Windows',
|
||||||
|
desc: 'x64 安装包',
|
||||||
|
assetSuffix: 'x64.exe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Linux x64 AppImage',
|
||||||
|
desc: 'x64 AppImage',
|
||||||
|
assetSuffix: 'x86_64.AppImage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Linux x64 Debian',
|
||||||
|
desc: 'amd64 .deb 安装包',
|
||||||
|
assetSuffix: 'amd64.deb',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Linux arm64',
|
||||||
|
desc: 'arm64 AppImage',
|
||||||
|
assetSuffix: 'arm64.AppImage',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
npm: {
|
npm: {
|
||||||
title: 'npm',
|
title: 'npm',
|
||||||
cmd1: 'npm install -g hermes-web-ui',
|
cmd1: 'npm install -g hermes-web-ui',
|
||||||
@@ -97,6 +162,10 @@ export default {
|
|||||||
starHistory: {
|
starHistory: {
|
||||||
title: '社区成长',
|
title: '社区成长',
|
||||||
desc: '在 GitHub 上给我们加星,加入社区。',
|
desc: '在 GitHub 上给我们加星,加入社区。',
|
||||||
|
star: '加星',
|
||||||
|
licenseAlt: '许可证',
|
||||||
|
versionAlt: '版本',
|
||||||
|
chartAlt: 'Star 历史',
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
description: 'Hermes Agent 的自托管 AI 聊天仪表板。',
|
description: 'Hermes Agent 的自托管 AI 聊天仪表板。',
|
||||||
@@ -104,6 +173,7 @@ export default {
|
|||||||
madeWith: '使用 Vue 3、Naive UI 和 TypeScript 构建。',
|
madeWith: '使用 Vue 3、Naive UI 和 TypeScript 构建。',
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
|
placeholder: '从侧边栏选择一个章节开始阅读。',
|
||||||
sidebar: {
|
sidebar: {
|
||||||
gettingStarted: '快速开始',
|
gettingStarted: '快速开始',
|
||||||
configuration: '配置说明',
|
configuration: '配置说明',
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function navigate(name: string) {
|
|||||||
<router-view />
|
<router-view />
|
||||||
<DocContent v-if="route.meta.page" />
|
<DocContent v-if="route.meta.page" />
|
||||||
<div v-else class="docs-placeholder">
|
<div v-else class="docs-placeholder">
|
||||||
<p>Select a section from the sidebar to get started.</p>
|
<p>{{ t('docs.placeholder') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,16 +9,28 @@ import StarHistorySection from '@/components/landing/StarHistorySection.vue'
|
|||||||
<template>
|
<template>
|
||||||
<div class="landing">
|
<div class="landing">
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
|
<section class="download-section">
|
||||||
|
<InstallSection />
|
||||||
|
</section>
|
||||||
<ScreenshotsSection />
|
<ScreenshotsSection />
|
||||||
<FeaturesGrid />
|
<FeaturesGrid />
|
||||||
<div class="cta-row">
|
<div class="cta-row">
|
||||||
<InstallSection class="cta-col" />
|
|
||||||
<StarHistorySection class="cta-col" />
|
<StarHistorySection class="cta-col" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.download-section {
|
||||||
|
max-width: 1120px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 56px 24px 16px;
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-mobile) {
|
||||||
|
padding: 32px 16px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.cta-row {
|
.cta-row {
|
||||||
max-width: 1120px;
|
max-width: 1120px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user