add website downloads and deploy workflow (#1165)

This commit is contained in:
ekko
2026-05-30 21:35:38 +08:00
committed by GitHub
parent 4c3f025b8f
commit e45b3a881e
14 changed files with 425 additions and 41 deletions
@@ -150,7 +150,7 @@ onMounted(() => {
<div class="install-box animate-fade-in animate-delay-3">
<code>{{ installCmd }}</code>
<button class="copy-btn" @click="copyCmd">
{{ copied ? 'Copied!' : 'Copy' }}
{{ copied ? t('ui.copied') : t('ui.copy') }}
</button>
</div>
</div>
@@ -1,11 +1,29 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { useScrollReveal } from '@/composables/useScrollReveal'
const { t } = useI18n()
interface DesktopDownload {
title: string
desc: string
assetSuffix: string
}
const { t, tm } = useI18n()
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) {
navigator.clipboard.writeText(text).catch(() => {})
@@ -19,7 +37,7 @@ function copyText(text: string) {
<div class="install-tabs reveal">
<button
v-for="tab in (['npm', 'docker', 'source'] as const)"
v-for="tab in (['desktop', 'npm', 'docker', 'source'] as const)"
:key="tab"
class="tab-btn"
:class="{ active: activeTab === tab }"
@@ -30,7 +48,33 @@ function copyText(text: string) {
</div>
<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'))">
<code>{{ t('install.npm.cmd1') }}</code>
</div>
@@ -51,7 +95,7 @@ function copyText(text: string) {
<code>{{ t('install.source.cmd2') }}</code>
</div>
</template>
<p class="prereq">{{ t('install.prereq') }}</p>
<p class="prereq">{{ activeTab === 'desktop' ? t('install.desktop.prereq') : t('install.prereq') }}</p>
</div>
</div>
</template>
@@ -88,6 +132,8 @@ function copyText(text: string) {
background: var(--bg-secondary);
border-radius: $radius-md;
padding: 4px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.tab-btn {
@@ -114,6 +160,65 @@ function copyText(text: string) {
// 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 {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
@@ -1,25 +1,26 @@
<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'
interface ScreenshotItem {
src: string
alt: string
}
const { t, tm } = useI18n()
useScrollReveal()
const images = [
{ 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 images = computed(() => tm('screenshots.items') as ScreenshotItem[])
const activeIndex = ref(0)
let timer: ReturnType<typeof setInterval>
function next() {
activeIndex.value = (activeIndex.value + 1) % images.length
activeIndex.value = (activeIndex.value + 1) % images.value.length
}
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) {
@@ -53,7 +54,7 @@ onUnmounted(() => {
<span class="dot green" />
</div>
<div class="browser-url">
<span>http://localhost:8648</span>
<span>{{ t('screenshots.localUrl') }}</span>
</div>
<div class="browser-spacer" />
</div>
@@ -71,7 +72,7 @@ onUnmounted(() => {
<!-- Navigation -->
<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>
</button>
@@ -80,12 +81,13 @@ onUnmounted(() => {
v-for="(_img, i) in images"
:key="i"
class="dot-btn"
:aria-label="t('screenshots.goTo', { number: i + 1 })"
:class="{ active: activeIndex === i }"
@click="setActive(i)"
/>
</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>
</button>
</div>
@@ -39,19 +39,19 @@ onMounted(async () => {
<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"/>
</svg>
<span>Star</span>
<span>{{ t('starHistory.star') }}</span>
<span v-if="stars !== null" class="star-count">{{ stars.toLocaleString() }}</span>
</a>
<img
class="github-badge"
src="https://img.shields.io/github/license/EKKOLearnAI/hermes-web-ui?style=flat-square"
alt="License"
:alt="t('starHistory.licenseAlt')"
/>
<img
class="github-badge"
src="https://img.shields.io/github/v/release/EKKOLearnAI/hermes-web-ui?style=flat-square"
alt="Version"
:alt="t('starHistory.versionAlt')"
/>
</div>
@@ -64,7 +64,7 @@ onMounted(async () => {
>
<img
:src="chartSrc"
alt="Star History"
:alt="t('starHistory.chartAlt')"
class="chart-img"
/>
</a>