add website downloads and deploy workflow (#1165)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user