Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
import { useSettingsStore } from '@/stores/hermes/settings'
import { useTheme, type ThemeMode } from '@/composables/useTheme'
import SettingRow from './SettingRow.vue'
import ThinkingAnimationPicker from './ThinkingAnimationPicker.vue'

const settingsStore = useSettingsStore()
const message = useMessage()
Expand Down Expand Up @@ -58,13 +59,17 @@ function handleThemeChange(val: string) {
<SettingRow :label="t('settings.display.busyInputMode')" :hint="t('settings.display.busyInputModeHint')">
<NSwitch :value="settingsStore.display.busy_input_mode === 'interrupt'" @update:value="v => save({ busy_input_mode: v ? 'interrupt' : 'off' })" />
</SettingRow>
<ThinkingAnimationPicker />
</section>
</template>

<style scoped lang="scss">
@use '@/styles/variables' as *;

.settings-section {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 2px;
}
.input-sm {
min-width: 120px;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { NButton, NUpload, useMessage } from 'naive-ui'
import type { UploadCustomRequestOptions } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import SettingRow from './SettingRow.vue'

const { t } = useI18n()
const message = useMessage()

const hasCustom = ref(false)
const customUrl = ref('')
const customType = ref<'gif' | 'video'>('gif')
const uploading = ref(false)

async function checkStatus() {
try {
const res = await fetch('/api/thinking-animation/status')
const data = await res.json()
hasCustom.value = data.hasCustom
if (data.hasCustom) {
customUrl.value = data.url
customType.value = data.type
} else {
customUrl.value = ''
}
} catch {
hasCustom.value = false
customUrl.value = ''
}
}

async function handleUpload({ file }: UploadCustomRequestOptions) {
if (!file.file) return
const ext = file.name.split('.').pop()?.toLowerCase() || ''
if (!['gif', 'mp4', 'webm', 'mov', 'avi', 'mkv'].includes(ext)) {
message.error(t('settings.display.thinkingAnimationUnsupported'))
return
}
if (file.file.size > 100 * 1024 * 1024) {
message.error(t('settings.display.thinkingAnimationTooLarge'))
return
}
uploading.value = true
try {
const formData = new FormData()
formData.append('file', file.file, file.name)
const res = await fetch('/api/thinking-animation/upload', {
method: 'POST',
body: formData,
})
const data = await res.json()
if (data.success) {
hasCustom.value = true
customUrl.value = data.url
customType.value = data.type
message.success(t('settings.display.thinkingAnimationUploaded'))
} else {
message.error(data.error || t('settings.display.thinkingAnimationFailed'))
}
} catch (err) {
message.error(t('settings.display.thinkingAnimationFailed'))
} finally {
uploading.value = false
}
}

async function handleReset() {
try {
const res = await fetch('/api/thinking-animation', { method: 'DELETE' })
const data = await res.json()
if (data.success) {
hasCustom.value = false
customUrl.value = ''
message.success(t('settings.display.thinkingAnimationReset'))
}
} catch {
message.error(t('settings.display.thinkingAnimationFailed'))
}
}

onMounted(checkStatus)
</script>

<template>
<SettingRow
:label="t('settings.display.thinkingAnimation')"
:hint="t('settings.display.thinkingAnimationHint')"
>
<div class="thinking-animation-picker">
<div v-if="hasCustom && customUrl" class="thinking-animation-preview">
<img
v-if="customType === 'gif'"
:src="customUrl"
class="thinking-animation-thumb"
alt="Custom thinking animation"
/>
<video
v-else
:src="customUrl"
class="thinking-animation-thumb"
autoplay
loop
muted
/>
<NButton size="small" type="error" quaternary @click="handleReset">
{{ t('settings.display.thinkingAnimationReset') }}
</NButton>
</div>
<NUpload
v-else
:custom-request="handleUpload"
accept=".gif,.mp4,.webm,.mov,.avi,.mkv"
:max="1"
:disabled="uploading"
:show-file-list="false"
>
<NButton size="small" :loading="uploading" secondary>
{{ uploading ? t('settings.display.thinkingAnimationUploading') : t('settings.display.thinkingAnimationUpload') }}
</NButton>
</NUpload>
</div>
</SettingRow>
</template>

<style scoped>
.thinking-animation-picker {
display: flex;
align-items: center;
gap: 8px;
}
.thinking-animation-preview {
display: flex;
align-items: center;
gap: 8px;
}
.thinking-animation-thumb {
width: 48px;
height: 48px;
object-fit: contain;
border-radius: 8px;
border: 1px solid var(--border-color);
}
</style>
10 changes: 10 additions & 0 deletions packages/client/src/i18n/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,16 @@ export default {
themeLight: 'Hell',
themeDark: 'Dunkel',
themeSystem: 'System',
thinkingAnimation: 'Denk-Animation',
thinkingAnimationHint: 'Laden Sie eine benutzerdefinierte Animation (GIF, MP4, WebM, MOV, AVI, MKV) hoch, um die Standard-AI-Denk-Animation zu ersetzen',
thinkingAnimationUpload: 'Animation hochladen',
thinkingAnimationUploading: 'Hochladen...',
thinkingAnimationUploaded: 'Animation erfolgreich hochgeladen',
thinkingAnimationReset: 'Auf Standard zurücksetzen',
thinkingAnimationResetDone: 'Animation auf Standard zurückgesetzt',
thinkingAnimationFailed: 'Upload fehlgeschlagen',
thinkingAnimationUnsupported: 'Nicht unterstützter Dateityp (verwenden Sie GIF, MP4, WebM, MOV, AVI oder MKV)',
thinkingAnimationTooLarge: 'Datei zu groß (max. 100MB)',
},
agent: {
maxTurns: 'Maximale Runden',
Expand Down
10 changes: 10 additions & 0 deletions packages/client/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,16 @@ export default {
themeLight: 'Light',
themeDark: 'Dark',
themeSystem: 'System',
thinkingAnimation: 'Thinking Animation',
thinkingAnimationHint: 'Upload a custom animation (GIF, MP4, WebM, MOV, AVI, MKV) to replace the default AI thinking animation',
thinkingAnimationUpload: 'Upload Animation',
thinkingAnimationUploading: 'Uploading...',
thinkingAnimationUploaded: 'Animation uploaded successfully',
thinkingAnimationReset: 'Reset to Default',
thinkingAnimationResetDone: 'Animation reset to default',
thinkingAnimationFailed: 'Upload failed',
thinkingAnimationUnsupported: 'Unsupported file type (use GIF, MP4, WebM, MOV, AVI, or MKV)',
thinkingAnimationTooLarge: 'File too large (max 100MB)',
},
agent: {
maxTurns: 'Max Turns',
Expand Down
10 changes: 10 additions & 0 deletions packages/client/src/i18n/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,16 @@ export default {
themeLight: 'Claro',
themeDark: 'Oscuro',
themeSystem: 'Sistema',
thinkingAnimation: 'Animación de pensamiento',
thinkingAnimationHint: 'Sube una animación personalizada (GIF, MP4, WebM, MOV, AVI, MKV) para reemplazar la animación de pensamiento de IA predeterminada',
thinkingAnimationUpload: 'Subir animación',
thinkingAnimationUploading: 'Subiendo...',
thinkingAnimationUploaded: 'Animación subida exitosamente',
thinkingAnimationReset: 'Restablecer predeterminado',
thinkingAnimationResetDone: 'Animación restablecida a predeterminada',
thinkingAnimationFailed: 'Error al subir',
thinkingAnimationUnsupported: 'Tipo de archivo no compatible (usa GIF, MP4, WebM, MOV, AVI o MKV)',
thinkingAnimationTooLarge: 'Archivo demasiado grande (máx 100MB)',
},
agent: {
maxTurns: 'Turnos maximos',
Expand Down
10 changes: 10 additions & 0 deletions packages/client/src/i18n/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,16 @@ export default {
themeLight: 'Clair',
themeDark: 'Sombre',
themeSystem: 'Systeme',
thinkingAnimation: 'Animation de réflexion',
thinkingAnimationHint: 'Téléchargez une animation personnalisée (GIF, MP4, WebM, MOV, AVI, MKV) pour remplacer l\'animation de réflexion IA par défaut',
thinkingAnimationUpload: 'Télécharger l\'animation',
thinkingAnimationUploading: 'Téléchargement...',
thinkingAnimationUploaded: 'Animation téléchargée avec succès',
thinkingAnimationReset: 'Rétablir par défaut',
thinkingAnimationResetDone: 'Animation rétablie par défaut',
thinkingAnimationFailed: 'Échec du téléchargement',
thinkingAnimationUnsupported: 'Type de fichier non pris en charge (utilisez GIF, MP4, WebM, MOV, AVI ou MKV)',
thinkingAnimationTooLarge: 'Fichier trop volumineux (max 100 Mo)',
},
agent: {
maxTurns: 'Tours maximum',
Expand Down
10 changes: 10 additions & 0 deletions packages/client/src/i18n/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,16 @@ export default {
themeLight: 'ライト',
themeDark: 'ダーク',
themeSystem: 'システム',
thinkingAnimation: '思考アニメーション',
thinkingAnimationHint: 'カスタムアニメーション(GIF、MP4、WebM、MOV、AVI、MKV)をアップロードして、デフォルトのAI思考アニメーションを置き換えます',
thinkingAnimationUpload: 'アニメーションをアップロード',
thinkingAnimationUploading: 'アップロード中...',
thinkingAnimationUploaded: 'アニメーションのアップロードが成功しました',
thinkingAnimationReset: 'デフォルトに戻す',
thinkingAnimationResetDone: 'デフォルトアニメーションに戻しました',
thinkingAnimationFailed: 'アップロードに失敗しました',
thinkingAnimationUnsupported: 'サポートされていないファイル形式(GIF、MP4、WebM、MOV、AVI、MKVを使用)',
thinkingAnimationTooLarge: 'ファイルが大きすぎます(最大100MB)',
},
agent: {
maxTurns: '最大ターン数',
Expand Down
10 changes: 10 additions & 0 deletions packages/client/src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,16 @@ export default {
themeLight: '라이트',
themeDark: '다크',
themeSystem: '시스템',
thinkingAnimation: '사고 애니메이션',
thinkingAnimationHint: '커스텀 애니메이션(GIF, MP4, WebM, MOV, AVI, MKV)을 업로드하여 기본 AI 사고 애니메이션을 대체합니다',
thinkingAnimationUpload: '애니메이션 업로드',
thinkingAnimationUploading: '업로드 중...',
thinkingAnimationUploaded: '애니메이션이 성공적으로 업로드되었습니다',
thinkingAnimationReset: '기본값으로 복원',
thinkingAnimationResetDone: '기본 애니메이션으로 복원되었습니다',
thinkingAnimationFailed: '업로드에 실패했습니다',
thinkingAnimationUnsupported: '지원되지 않는 파일 형식(GIF, MP4, WebM, MOV, AVI, MKV 사용)',
thinkingAnimationTooLarge: '파일이 너무 큽니다(최대 100MB)',
},
agent: {
maxTurns: '최대 턴 수',
Expand Down
10 changes: 10 additions & 0 deletions packages/client/src/i18n/locales/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,16 @@ export default {
themeLight: 'Claro',
themeDark: 'Escuro',
themeSystem: 'Sistema',
thinkingAnimation: 'Animação de pensamento',
thinkingAnimationHint: 'Envie uma animação personalizada (GIF, MP4, WebM, MOV, AVI, MKV) para substituir a animação de pensamento de IA padrão',
thinkingAnimationUpload: 'Enviar animação',
thinkingAnimationUploading: 'Enviando...',
thinkingAnimationUploaded: 'Animação enviada com sucesso',
thinkingAnimationReset: 'Redefinir para padrão',
thinkingAnimationResetDone: 'Animação redefinida para o padrão',
thinkingAnimationFailed: 'Falha no envio',
thinkingAnimationUnsupported: 'Tipo de arquivo não suportado (use GIF, MP4, WebM, MOV, AVI ou MKV)',
thinkingAnimationTooLarge: 'Arquivo muito grande (máx 100MB)',
},
agent: {
maxTurns: 'Maximo de turnos',
Expand Down
10 changes: 10 additions & 0 deletions packages/client/src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,16 @@ export default {
themeLight: '浅色',
themeDark: '暗色',
themeSystem: '跟随系统',
thinkingAnimation: '思考动画',
thinkingAnimationHint: '上传自定义动画(GIF、MP4、WebM、MOV、AVI、MKV)替换默认 AI 思考动画',
thinkingAnimationUpload: '上传动画',
thinkingAnimationUploading: '上传中...',
thinkingAnimationUploaded: '动画上传成功',
thinkingAnimationReset: '恢复默认',
thinkingAnimationResetDone: '已恢复默认动画',
thinkingAnimationFailed: '上传失败',
thinkingAnimationUnsupported: '不支持的文件类型(请使用 GIF、MP4、WebM、MOV、AVI 或 MKV)',
thinkingAnimationTooLarge: '文件过大(最大 100MB)',
},
agent: {
maxTurns: '最大轮次',
Expand Down
Loading
Loading