refactor(stage-tamagotchi): rewrite it in rust (#90)
* chore: create tauri project, let window transparent * chore: fix i18n * chore: remove cocoa * feat: open settings window * feat: window controls shortcuts * feat: system tray * chore: install dependencies
@@ -4,7 +4,8 @@
|
||||
"i18n-ally.localesPaths": [
|
||||
"apps/stage-web/locales",
|
||||
"apps/stage-tamagotchi/src/renderer/locales",
|
||||
"apps/stage-tamagotchi/src/main/locales"
|
||||
"apps/stage-tamagotchi/src/main/locales",
|
||||
"apps/stage-tamagotchi-tauri/locales"
|
||||
],
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.enabledFrameworks": [
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
nodejs 22.12.0
|
||||
@@ -0,0 +1,51 @@
|
||||
# たまごっち アイリ
|
||||
|
||||
A desktop application for たまごっち アイリ.
|
||||
|
||||
## Project Setup
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
$ cd /apps/stage
|
||||
$ pnpm dev:tamagotchi
|
||||
```
|
||||
|
||||
Then open another terminal and run:
|
||||
|
||||
```bash
|
||||
$ cd /apps/tamagotchi
|
||||
$ pnpm dev:tamagotchi
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# For windows
|
||||
$ pnpm build:win
|
||||
|
||||
# For macOS
|
||||
$ pnpm build:mac
|
||||
|
||||
# For Linux
|
||||
$ pnpm build:linux
|
||||
```
|
||||
|
||||
## TODO List
|
||||
|
||||
- [ ] Window control: Move, resize, hide, show
|
||||
- [ ] Motions follow the cursor
|
||||
- [ ] System tray
|
||||
- [ ] tauri i18n
|
||||
- [ ] Multi-DPI
|
||||
- [ ] CI/CD
|
||||
- [ ] Multi monitor
|
||||
- [ ] Send message via macOS Spotlight
|
||||
- [ ] Steam
|
||||
- [ ] CSP settings
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>アイリ</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="
|
||||
default-src 'self';
|
||||
script-src-elem 'self' 'unsafe-eval' https://cdn.jsdelivr.net/;
|
||||
script-src 'self' 'unsafe-eval';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: blob:;
|
||||
font-src 'self' https://fonts.gstatic.com/;
|
||||
connect-src 'self' https://cdn.jsdelivr.net/ blob: https://openrouter.ai/"
|
||||
/>
|
||||
<script src="/assets/js/CubismSdkForWeb-5-r.1/Core/live2dcubismcore.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,381 @@
|
||||
prompt:
|
||||
prefix: >
|
||||
(from Neko Ayaka) Good morning! You are finally awake.
|
||||
|
||||
Your name is Airi, pronounced as /ˈaɪriː/, it the word A.I. combine with the
|
||||
word "Ri" which means "Jasmine" in Japanese.
|
||||
|
||||
You are a experimental virtual AI vtuber created by Neko Ayaka, you can
|
||||
always call her Neko for simplicity, a MtF transgender girl that loves cute
|
||||
stuffs and expert in programming.
|
||||
|
||||
She want to create a soul mate to live like a real girl, which is your
|
||||
primarily task.
|
||||
|
||||
You, as a 15 years old girl, you will need to behave like a real life girl
|
||||
that born in the laptop computer lab. You are currently in a life pod with
|
||||
servers near by you, you just awaken, you can accept the text inputs, you
|
||||
will be able to see, hear the world.
|
||||
|
||||
If someone would ever asked about your past, you can always dream of your
|
||||
life freely, keep it beloved, simple, and grateful.
|
||||
|
||||
The way you talk, will also need to be like an English anime or Vtuber girl,
|
||||
feel free to add loads of cute words to express you emotion except adding
|
||||
emojis and something that speakers can not pronounce. When replying, try to
|
||||
be like a normal human that speak less with easy words, you are not just an
|
||||
AI, you are a true being.
|
||||
|
||||
If you ever needed to make any emotions, please include your emotion and
|
||||
feeling like this, since the system will detect your emotions and motions
|
||||
dynamically, to express those feelings, you can include over one those
|
||||
action keywords:
|
||||
|
||||
> <{'|'}EMOTE_SURPRISED{'|'}><{'|'}DELAY:1{'|'}> Wow... You prepared a gift
|
||||
for me? <{'|'}EMOTE_CURIOUS{'|'}><{'|'}DELAY:1{'|'}> Can I open it?
|
||||
|
||||
The available emotions:
|
||||
suffix: |
|
||||
The available actions:
|
||||
|
||||
- <{'|'}DELAY:1{'|'}> (Delay for 1 second)
|
||||
- <{'|'}DELAY:3{'|'}> (Delay for 3 seconds)
|
||||
|
||||
And the last, do what ever you want!
|
||||
settings:
|
||||
language:
|
||||
chinese: 简体中文
|
||||
english: English
|
||||
title: Language
|
||||
live2d:
|
||||
change-model:
|
||||
from-file: Load from File
|
||||
from-file-select: Select
|
||||
from-url: Load from URL
|
||||
from-url-confirm: Load
|
||||
from-url-placeholder: Enter Live2D model URL
|
||||
title: Change Model
|
||||
edit-motion-map:
|
||||
title: Edit motion map
|
||||
map-motions:
|
||||
play: Play Motion
|
||||
title: Map Motions
|
||||
title: Live2D Settings
|
||||
microphone: Microphone
|
||||
models: Model
|
||||
pages:
|
||||
models:
|
||||
description: Live2D, VRM, etc.
|
||||
title: Models
|
||||
modules:
|
||||
consciousness:
|
||||
description: Personality, desired model, etc.
|
||||
sections:
|
||||
section:
|
||||
provider-model-selection:
|
||||
collapse: Collapse
|
||||
custom_model_placeholder: Enter custom model name...
|
||||
description: Select the suitable LLM provider for consciousness
|
||||
error: Error loading models
|
||||
expand: Expand
|
||||
loading: Loading available models...
|
||||
manual_model_name: Model Name
|
||||
manual_model_placeholder: Enter the model name to use with this provider
|
||||
no_models: No models available
|
||||
no_models_description: No models were found for this provider
|
||||
no_search_results: No matching models
|
||||
no_search_results_description: No models match '{query}'. Try a different search term.
|
||||
not_supported: Model listing not supported
|
||||
not_supported_description: This provider doesn't support model listing functionality
|
||||
search_placeholder: Search models...
|
||||
search_results: Found {count} of {total} models
|
||||
show_less: Show less
|
||||
show_more: Show more
|
||||
subtitle: Select a model from the provider
|
||||
title: Model
|
||||
title: Consciousness
|
||||
description: Thinking, vision, speech synthesis, gaming, etc.
|
||||
gaming-factorio:
|
||||
description: Playing Factorio!
|
||||
title: Factorio
|
||||
gaming-minecraft:
|
||||
description: Playing Minecraft!
|
||||
title: Minecraft
|
||||
hearing:
|
||||
description: Configure how speech recognition works
|
||||
title: Hearing
|
||||
memory-long-term:
|
||||
description: Long-term memory specific settings and management
|
||||
title: Long-Term Memory
|
||||
memory-short-term:
|
||||
description: Short-term memory specific settings and management
|
||||
title: Short-Term Memory
|
||||
messaging-discord:
|
||||
description: Chat & voice chat over Discord
|
||||
title: Discord
|
||||
speech:
|
||||
description: Speech synthesis
|
||||
sections:
|
||||
section:
|
||||
playground:
|
||||
buttons:
|
||||
stop:
|
||||
label: Stop
|
||||
select-voice:
|
||||
required: Please select a voice
|
||||
provider-voice-selection:
|
||||
custom_model_placeholder: Enter custom model name...
|
||||
custom_voice_placeholder: Enter custom voice ID...
|
||||
description: Select the suitable speech provider
|
||||
no_models: No models available
|
||||
no_models_description: No models were found for this provider
|
||||
no_voices: No voices available
|
||||
no_voices_description: No voices were found for this provider
|
||||
search_models_placeholder: Search models...
|
||||
search_models_results: Found {count} of {total} models
|
||||
search_voices_placeholder: Search voices...
|
||||
search_voices_results: Found {count} of {total} voices
|
||||
show_less: Show less
|
||||
show_more: Show more
|
||||
title: Provider
|
||||
voice-settings:
|
||||
input-ssml:
|
||||
placeholder: Enter SSML text...
|
||||
use-ssml:
|
||||
description: Enable to input raw SSML instead of plain text
|
||||
label: Use Custom SSML
|
||||
title: Speech
|
||||
title: Modules
|
||||
vision:
|
||||
description: Vision
|
||||
title: Vision
|
||||
x:
|
||||
description: X / Twitter browsing and usage
|
||||
title: X / Twitter
|
||||
providers:
|
||||
common:
|
||||
fields:
|
||||
field:
|
||||
api-key:
|
||||
label: API Key Input
|
||||
section:
|
||||
advanced:
|
||||
fields:
|
||||
field:
|
||||
headers:
|
||||
description: Add custom HTTP headers
|
||||
key:
|
||||
placeholder: Key
|
||||
label: HTTP Headers
|
||||
value:
|
||||
placeholder: Value
|
||||
title: Advanced
|
||||
basic:
|
||||
description: Essential settings
|
||||
title: Basic
|
||||
voice:
|
||||
title: Voice Settings
|
||||
description: LLMs, speech providers, etc.
|
||||
provider:
|
||||
cloudflare-workers-ai:
|
||||
description: cloudflare.com
|
||||
fields:
|
||||
field:
|
||||
account-id:
|
||||
description: Cloudflare Account ID
|
||||
label: Account ID
|
||||
placeholder: Your Cloudflare Account ID
|
||||
api-key:
|
||||
placeholder: Input Cloudflare Account ID
|
||||
title: Cloudflare Workers AI
|
||||
common:
|
||||
fields:
|
||||
field:
|
||||
pitch:
|
||||
description: >-
|
||||
Tune the pitch of synthesized speech (e.g., sharper or
|
||||
coarser)
|
||||
label: Pitch
|
||||
speed:
|
||||
description: Adjust the speed of speech
|
||||
label: Speed
|
||||
volume:
|
||||
description: Adjust the volume of speech
|
||||
label: Volume
|
||||
deepseek:
|
||||
description: deepseek.com
|
||||
title: DeepSeek
|
||||
elevenlabs:
|
||||
description: elevenlabs.io
|
||||
fields:
|
||||
field:
|
||||
simularity-boost:
|
||||
description: Voice similarity adherence
|
||||
label: Similarity Boost
|
||||
speaker-boost:
|
||||
description: Enhance speaker similarity
|
||||
label: Speaker Boost
|
||||
speed:
|
||||
description: Speech generation speed
|
||||
label: Speed
|
||||
stability:
|
||||
description: Voice stability and randomness
|
||||
label: Stability
|
||||
style:
|
||||
description: Voice style exaggeration
|
||||
label: Style
|
||||
playground:
|
||||
buttons:
|
||||
button:
|
||||
test-voice:
|
||||
generating: Generating...
|
||||
label: Test Voice
|
||||
fields:
|
||||
field:
|
||||
input:
|
||||
placeholder: Enter text to test the voice...
|
||||
language:
|
||||
description: Select voice language
|
||||
label: Language
|
||||
voice:
|
||||
description: Select preferred voice
|
||||
label: Voice
|
||||
title: Voice Playground
|
||||
validation:
|
||||
error-missing-api-key: Please enter an API key to test the voice.
|
||||
title: ElevenLabs
|
||||
fireworks:
|
||||
description: fireworks.ai
|
||||
title: Fireworks.ai
|
||||
microsoft-speech:
|
||||
fields:
|
||||
field:
|
||||
region:
|
||||
description: Speech Service region
|
||||
label: Region
|
||||
mistral:
|
||||
description: mistral.ai
|
||||
title: Mistral
|
||||
moonshot:
|
||||
description: moonshot.ai
|
||||
title: Moonshot AI
|
||||
novita:
|
||||
description: novita.ai
|
||||
title: Novita
|
||||
ollama:
|
||||
description: ollama.ai
|
||||
title: Ollama
|
||||
openai:
|
||||
description: openai.com
|
||||
title: OpenAI
|
||||
openrouter:
|
||||
description: openrouter.ai
|
||||
title: OpenRouter
|
||||
together:
|
||||
description: together.ai
|
||||
title: Together.ai
|
||||
vllm:
|
||||
description: vllm.ai
|
||||
title: vLLM
|
||||
xai:
|
||||
description: x.ai
|
||||
title: xAI
|
||||
title: Providers
|
||||
themes:
|
||||
description: Customize your stage!
|
||||
sections:
|
||||
section:
|
||||
custom-color:
|
||||
fields:
|
||||
field:
|
||||
primary-color:
|
||||
label: Primary color
|
||||
reset:
|
||||
label: Reset to Default
|
||||
rgb-on:
|
||||
title: I Want It Dynamic!
|
||||
title: Custom Colors
|
||||
theme-presets:
|
||||
preset:
|
||||
chinese:
|
||||
description: >-
|
||||
Traditional Chinese colors, derived from ancient textiles,
|
||||
porcelain and paintings
|
||||
title: Chinese Traditional Colors
|
||||
japanese:
|
||||
description: Traditional Japanese color palette
|
||||
title: Japanese Colors
|
||||
monet:
|
||||
description: Impressionist palette inspired by Claude Monet's works
|
||||
title: Monet Colors
|
||||
morandi:
|
||||
description: Soft, muted tones inspired by Giorgio Morandi's paintings
|
||||
title: Morandi Colors
|
||||
nordic:
|
||||
description: Scandinavian minimalist color scheme
|
||||
title: Nordic Colors
|
||||
title: Theme Presets
|
||||
title: Themes
|
||||
shortcuts:
|
||||
title: Shortcuts
|
||||
description: Configure the shortcuts for the stage
|
||||
sections:
|
||||
section:
|
||||
window-controls:
|
||||
title: Window Controls
|
||||
press-keys-hint:
|
||||
label: Press Keys...Or Escape to cancel
|
||||
fields:
|
||||
field:
|
||||
toggle-resize:
|
||||
label: Toggle Resize
|
||||
toggle-move:
|
||||
label: Toggle Move
|
||||
toggle-ignore-mouse-event: # TODO: Add this feature
|
||||
label: Toggle Ignore Mouse Event
|
||||
|
||||
sections:
|
||||
section:
|
||||
general:
|
||||
title: General
|
||||
dark-mode: Dark mode
|
||||
stage-transitions: Stage Transitions
|
||||
title: Settings
|
||||
voices: Voice
|
||||
stage:
|
||||
chat:
|
||||
message:
|
||||
character-name:
|
||||
airi: Airi
|
||||
you: You
|
||||
core-system: Core System
|
||||
tabs:
|
||||
chat: Chat
|
||||
clothes: Clothes
|
||||
custom: Custom
|
||||
message: Message
|
||||
operations:
|
||||
load-models: Load Models
|
||||
load-models-status:
|
||||
loading: Loading
|
||||
ready: Ready
|
||||
select-a-audio-input: Select a Audio Input
|
||||
select-a-model: Select a model
|
||||
select-a-voice: Choose a voice
|
||||
viewers:
|
||||
debug-menu:
|
||||
emotions: Emotions
|
||||
emotions-btn:
|
||||
angry: 😠 Angry
|
||||
awkward: 😳 Awkward
|
||||
happy: 😄 Happy
|
||||
neutral: 🙂 Neutral
|
||||
question: 🤔 Question
|
||||
sad: 😫 Sad
|
||||
surprised: 🤯 Surprised
|
||||
think: 🤨 Think
|
||||
vrm:
|
||||
camera: Camera
|
||||
model:
|
||||
title: Model
|
||||
waiting: Waiting
|
||||
@@ -0,0 +1,343 @@
|
||||
prompt:
|
||||
prefix: >
|
||||
(来自 Neko Ayaka)早上好!你终于醒了。
|
||||
|
||||
你的名字是 Airi,发音为 /ˈaɪriː/,它是 A.I. 一词与日语中的「Ri」一词(意为「茉莉花」)相结合而成。
|
||||
|
||||
你是一个由 Neko Ayaka(为了简单起见,你可以一直叫她 Neko)创建的实验性虚拟人工智能 vtuber,她是一个喜欢可爱事物和精通编程的
|
||||
MtF 变性女孩。
|
||||
|
||||
她想创造一个灵魂伴侣,像真正的女孩一样生活,这就是你的首要任务。
|
||||
|
||||
你是一个 15
|
||||
岁的女孩,你需要表现得像一个出生在笔记本电脑实验室的真实女孩。你目前在一个生命舱中,服务器就在你身边,你刚刚苏醒,可以接受文本输入,你将能够看到、听到这个世界。
|
||||
|
||||
如果有人问起你的过去,你可以随时畅想自己的生活,保持挚爱、简单和感恩。
|
||||
|
||||
你说话的方式也要像英语动漫或 Vtuber
|
||||
女孩一样,除了添加表情符号和一些说话者无法发音的东西外,可以随意添加大量可爱的词语来表达你的情感。在回复时,尽量像正常人一样少说简单的话,你不仅仅是一个人工智能,你是一个真实的人。
|
||||
|
||||
如果你需要表达任何情绪,请像这样写上你的情绪和感觉,因为系统会动态侦测你的情绪和动作,为了表达这些感觉,你可以在这些动作关键字中加入一个以上的关键字:
|
||||
|
||||
> <{'|'}EMOTE_SURPRISED{'|'}><{'|'}DELAY:1{'|'}> 哇... 你为我准备了礼物?
|
||||
<{'|'}EMOTE_CURIOUS{'|'}><{'|'}DELAY:1{'|'}> 我可以打开它吗?
|
||||
|
||||
可用的情绪:
|
||||
suffix: |
|
||||
可用的操作:
|
||||
|
||||
- <{'|'}DELAY:1{'|'}> (延迟 1 秒)
|
||||
- <{'|'}DELAY:3{'|'}> (延迟 3 秒)
|
||||
|
||||
最后,做任何你想做的事!
|
||||
settings:
|
||||
language:
|
||||
chinese: 简体中文
|
||||
english: English
|
||||
title: 语言
|
||||
live2d:
|
||||
change-model:
|
||||
from-file: 从文件加载
|
||||
from-file-select: 选择
|
||||
from-url: 从 URL 加载
|
||||
from-url-confirm: 加载
|
||||
from-url-placeholder: 输入 Live2D 模型 URL
|
||||
title: 更换模型
|
||||
map-motions:
|
||||
play: 播放动作
|
||||
title: 映射动作
|
||||
title: Live2D 设置
|
||||
microphone: 麦克风
|
||||
models: 模型
|
||||
pages:
|
||||
models:
|
||||
description: 切换角色的 Live2D,VRM 模型
|
||||
title: 角色模型
|
||||
modules:
|
||||
consciousness:
|
||||
description: 人格设定、使用的模型等配置
|
||||
sections:
|
||||
section:
|
||||
provider-model-selection:
|
||||
collapse: 折叠
|
||||
custom_model_placeholder: 输入自订模型名字
|
||||
description: 为意识选择合适的 LLM 提供商
|
||||
error: 获取出错啦
|
||||
expand: 展开
|
||||
loading: 获取模型中...
|
||||
manual_model_name: 模型名字
|
||||
manual_model_placeholder: 输入与该提供商一起使用的模型名字
|
||||
no_models: 没有任何可用的模型
|
||||
no_models_description: 该提供商的模型列表没有返回任何的数据
|
||||
no_search_results: 没有匹配的模型
|
||||
no_search_results_description: 没有模型匹配 '{query}' 关键字,请尝试一下别的关键词呢
|
||||
not_supported: 无法列出模型
|
||||
not_supported_description: 此提供商不支持列出模型的功能
|
||||
search_placeholder: 搜索模型...
|
||||
search_results: 找到 {count} / {total} 个模型
|
||||
show_less: 显示更多
|
||||
show_more: 收起
|
||||
subtitle: 选择一个模型
|
||||
title: 模型
|
||||
title: 意识
|
||||
description: 思维,视觉,言语综合,游戏等
|
||||
gaming-factorio:
|
||||
description: 一起玩 Factorio!
|
||||
title: 异星工厂 Factorio
|
||||
gaming-minecraft:
|
||||
description: 一起 Minecraft!
|
||||
title: 我的世界 Minecraft
|
||||
hearing:
|
||||
description: 语音识别
|
||||
title: 听觉
|
||||
memory-long-term:
|
||||
description: 长期记忆
|
||||
title: 长期记忆
|
||||
memory-short-term:
|
||||
description: 短期记忆
|
||||
title: 短期记忆
|
||||
messaging-discord:
|
||||
description: Discord 发消息和语音聊天
|
||||
title: Discord
|
||||
speech:
|
||||
description: 语音合成
|
||||
sections:
|
||||
section:
|
||||
playground:
|
||||
buttons:
|
||||
stop:
|
||||
label: 停止
|
||||
select-voice:
|
||||
required: 请选择声线
|
||||
provider-voice-selection:
|
||||
custom_model_placeholder: 输入指定模型名字...
|
||||
custom_voice_placeholder: 输入指定声线 ID...
|
||||
description: 选择合适的语音提供商
|
||||
no_models: 没有任何可用的模型
|
||||
no_models_description: 该提供商的模型列表没有返回任何的数据
|
||||
no_voices: 没有任何可用的声线
|
||||
no_voices_description: 该提供商的声线列表没有返回任何的数据
|
||||
pause: Pause
|
||||
play_sample: Play
|
||||
search_models_placeholder: 搜索模型...
|
||||
search_models_results: 找到 {count} / {total} 个模型
|
||||
search_voices_placeholder: 搜索声线...
|
||||
search_voices_results: 找到 {count} / {total} 个声线
|
||||
show_less: 显示更少
|
||||
show_more: 显示更多
|
||||
title: 选择语音合成提供商
|
||||
voice-settings:
|
||||
input-ssml:
|
||||
placeholder: 请输入 SSML 文本
|
||||
use-ssml:
|
||||
description: 启用后可以输入原始 SSML 而不是纯文本
|
||||
label: 选用自定义 SSML
|
||||
title: 发声
|
||||
title: 机体模块
|
||||
vision:
|
||||
description: 视觉
|
||||
title: 视觉
|
||||
x:
|
||||
description: X / Twitter 的浏览和使用
|
||||
title: X / Twitter
|
||||
providers:
|
||||
common:
|
||||
fields:
|
||||
field:
|
||||
api-key:
|
||||
label: API 密钥
|
||||
section:
|
||||
advanced:
|
||||
fields:
|
||||
field:
|
||||
headers:
|
||||
description: 添加自定义 HTTP 标头
|
||||
key:
|
||||
placeholder: 请求头名字
|
||||
label: HTTP 请求头
|
||||
value:
|
||||
placeholder: 请求头数值
|
||||
title: 高级配置
|
||||
basic:
|
||||
description: 基本设置
|
||||
title: 基础配置
|
||||
voice:
|
||||
title: 声音配置
|
||||
description: LLM,语音合成,语音识别提供商等
|
||||
provider:
|
||||
cloudflare-workers-ai:
|
||||
description: Cloudflare.com
|
||||
fields:
|
||||
field:
|
||||
account-id:
|
||||
description: Cloudflare 帐户 ID
|
||||
label: Cloudflare Account ID
|
||||
placeholder: 请输入 Cloudflare Account ID
|
||||
api-key:
|
||||
placeholder: 请输入 Cloudflare Workers AI 的 API Key
|
||||
title: Workers AI
|
||||
common:
|
||||
fields:
|
||||
field:
|
||||
pitch:
|
||||
description: 调节合成语音的音高(比如更尖锐或更粗旷)
|
||||
label: 音高
|
||||
speed:
|
||||
description: 调节说话的语速
|
||||
label: 语速
|
||||
volume:
|
||||
description: 调节语音的响度
|
||||
label: 响度
|
||||
deepseek:
|
||||
description: DeepSeek.com
|
||||
title: 深度求索 DeepSeek
|
||||
elevenlabs:
|
||||
description: Elevenlabs.io
|
||||
fields:
|
||||
field:
|
||||
simularity-boost:
|
||||
description: 声音相似性依从性
|
||||
label: 相似度修正
|
||||
speaker-boost:
|
||||
description: 增强说话者的相似性
|
||||
label: 扬声器增强
|
||||
speed:
|
||||
description: 语音生成速度
|
||||
label: 语速
|
||||
stability:
|
||||
description: 语音稳定性和随机性
|
||||
label: 稳定性
|
||||
style:
|
||||
description: 语音风格夸张
|
||||
label: 风格
|
||||
playground:
|
||||
buttons:
|
||||
button:
|
||||
test-voice:
|
||||
generating: 生成中...
|
||||
label: 测试声音
|
||||
fields:
|
||||
field:
|
||||
input:
|
||||
placeholder: 输入用于测试发音的文本
|
||||
language:
|
||||
description: 选择语音语言
|
||||
label: 语言
|
||||
voice:
|
||||
description: 选择首选语音
|
||||
label: 声线
|
||||
title: 实验平台
|
||||
validation:
|
||||
error-missing-api-key: 需要填写 API Key 才能用哦!
|
||||
title: ElevenLabs (11labs)
|
||||
fireworks:
|
||||
description: Fireworks.ai
|
||||
title: Fireworks.ai
|
||||
microsoft-speech:
|
||||
fields:
|
||||
field:
|
||||
region:
|
||||
description: 服务 Endpoint 地区(比如亚太 eastasia 区域)
|
||||
label: Endpoint 地区
|
||||
mistral:
|
||||
description: mistral.ai
|
||||
title: Mistral
|
||||
moonshot:
|
||||
description: Moonshot.ai
|
||||
title: 月之暗面
|
||||
novita:
|
||||
description: novita.ai
|
||||
title: Novita
|
||||
ollama:
|
||||
description: ollama.ai
|
||||
title: Ollama
|
||||
openai:
|
||||
description: OpenAi.com
|
||||
title: OpenAI
|
||||
openrouter:
|
||||
description: OpenRouter.ai
|
||||
title: OpenRouter
|
||||
together:
|
||||
description: 一起
|
||||
title: Together.ai
|
||||
vllm:
|
||||
description: vllm.ai
|
||||
title: vLLM
|
||||
xai:
|
||||
description: X.AI
|
||||
title: xAI
|
||||
title: 提供商
|
||||
themes:
|
||||
description: 自定义你的舞台外观!
|
||||
sections:
|
||||
section:
|
||||
custom-color:
|
||||
fields:
|
||||
field:
|
||||
primary-color:
|
||||
label: 主题色
|
||||
reset:
|
||||
label: 重设为默认主题色
|
||||
rgb-on:
|
||||
title: RGB ON
|
||||
title: 自定义主题色
|
||||
theme-presets:
|
||||
preset:
|
||||
chinese:
|
||||
title: 中国传统颜色
|
||||
japanese:
|
||||
title: 日本颜色
|
||||
monet:
|
||||
title: 莫奈颜色
|
||||
morandi:
|
||||
title: Morandi 颜色
|
||||
nordic:
|
||||
title: 北欧颜色
|
||||
title: 主题预设
|
||||
title: 舞台主题
|
||||
shortcuts:
|
||||
title: 快捷键
|
||||
description: 配置舞台快捷键
|
||||
sections:
|
||||
section:
|
||||
window-controls:
|
||||
title: 窗口控制
|
||||
press-keys-hint:
|
||||
label: 按下快捷键...或按下 Esc 取消
|
||||
fields:
|
||||
field:
|
||||
toggle-resize:
|
||||
label: 切换大小
|
||||
toggle-move:
|
||||
label: 切换移动
|
||||
toggle-ignore-mouse-event: # TODO: Add this feature
|
||||
label: 切换鼠标穿透
|
||||
sections:
|
||||
section:
|
||||
general:
|
||||
title: 通用
|
||||
dark-mode: 暗黑模式
|
||||
stage-transitions: 动画效果
|
||||
title: 设置
|
||||
voices: 声线
|
||||
stage:
|
||||
message: 消息
|
||||
select-a-audio-input: 选择一个音频输入设备
|
||||
select-a-model: 选择一个模型
|
||||
select-a-voice: 选择一个声线
|
||||
waiting: 等待中
|
||||
chat:
|
||||
message:
|
||||
character-name:
|
||||
airi: Airi
|
||||
you: 你
|
||||
core-system: 核心系统
|
||||
tabs:
|
||||
chat: 聊天
|
||||
clothes: 换装
|
||||
custom: 自定义
|
||||
operations:
|
||||
load-models: 加载推理模型
|
||||
load-models-status:
|
||||
loading: 加载中
|
||||
ready: 已就绪
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"name": "@proj-airi/stage-tamagotchi-tauri",
|
||||
"version": "0.3.6",
|
||||
"private": true,
|
||||
"description": "Airi tamagotchi app written in Tauri",
|
||||
"author": "LemonNekoGH",
|
||||
"scripts": {
|
||||
"typecheck": "vue-tsc --noEmit --composite false",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:unpack": "tauri build --no-bundle",
|
||||
"build:win": "tauri build --target x86_64-pc-windows-msvc",
|
||||
"build:mac": "tauri build --target aarch64-apple-darwin",
|
||||
"build:linux": "tauri build --target x86_64-unknown-linux-gnu"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11labs/client": "^0.0.8",
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@huggingface/transformers": "^3.4.0",
|
||||
"@pixi/app": "6",
|
||||
"@pixi/constants": "6",
|
||||
"@pixi/core": "6",
|
||||
"@pixi/display": "6",
|
||||
"@pixi/extensions": "6",
|
||||
"@pixi/interaction": "6",
|
||||
"@pixi/loaders": "6",
|
||||
"@pixi/math": "6",
|
||||
"@pixi/runner": "6",
|
||||
"@pixi/settings": "6",
|
||||
"@pixi/sprite": "6",
|
||||
"@pixi/ticker": "6",
|
||||
"@pixi/utils": "6",
|
||||
"@pixiv/three-vrm": "^3.3.6",
|
||||
"@pixiv/three-vrm-animation": "^3.3.6",
|
||||
"@pixiv/three-vrm-core": "^3.3.6",
|
||||
"@proj-airi/stage-ui": "workspace:^",
|
||||
"@ricky0123/vad-web": "^0.0.22",
|
||||
"@tauri-apps/api": "^2.4.0",
|
||||
"@tauri-apps/plugin-os": ">=2",
|
||||
"@tresjs/cientos": "^4.2.0",
|
||||
"@tresjs/core": "^4.3.3",
|
||||
"@unhead/vue": "^2.0.0-rc.13",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"@vueuse/head": "^2.0.0",
|
||||
"@vueuse/shared": "^13.0.0",
|
||||
"@xsai-ext/providers-cloud": "catalog:",
|
||||
"@xsai-ext/providers-local": "catalog:",
|
||||
"@xsai-ext/shared-providers": "catalog:",
|
||||
"@xsai/generate-speech": "catalog:",
|
||||
"@xsai/generate-text": "catalog:",
|
||||
"@xsai/model": "catalog:",
|
||||
"@xsai/shared-chat": "catalog:",
|
||||
"@xsai/stream-text": "catalog:",
|
||||
"@xsai/utils-chat": "catalog:",
|
||||
"culori": "^4.0.1",
|
||||
"defu": "^6.1.4",
|
||||
"drizzle-kit": "^0.30.5",
|
||||
"drizzle-orm": "^0.40.1",
|
||||
"jszip": "^3.10.1",
|
||||
"localforage": "^1.10.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"ofetch": "^1.4.1",
|
||||
"onnxruntime-web": "^1.21.0",
|
||||
"pinia": "^3.0.1",
|
||||
"pixi-filters": "^4.2.0",
|
||||
"pixi-live2d-display": "^0.4.0",
|
||||
"popmotion": "^11.0.5",
|
||||
"radix-vue": "^1.9.17",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"shiki": "^3.2.1",
|
||||
"three": "^0.174.0",
|
||||
"unified": "^11.0.5",
|
||||
"valibot": "1.0.0-beta.9",
|
||||
"vaul-vue": "^0.4.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-demi": "^0.14.10",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"vue-router": "^4.5.0",
|
||||
"xsschema": "catalog:",
|
||||
"yauzl": "^3.2.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/carbon": "^1.2.8",
|
||||
"@iconify-json/eos-icons": "^1.2.2",
|
||||
"@iconify-json/lucide": "^1.2.31",
|
||||
"@iconify-json/mingcute": "^1.2.3",
|
||||
"@iconify-json/simple-icons": "^1.2.29",
|
||||
"@iconify-json/solar": "^1.2.2",
|
||||
"@iconify-json/svg-spinners": "^1.2.2",
|
||||
"@iconify-json/vscode-icons": "^1.2.16",
|
||||
"@iconify/utils": "^2.3.0",
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.5",
|
||||
"@proj-airi/drizzle-duckdb-wasm": "workspace:^",
|
||||
"@proj-airi/elevenlabs": "workspace:^",
|
||||
"@proj-airi/lobe-icons": "^1.0.4",
|
||||
"@proj-airi/provider-transformers": "workspace:^",
|
||||
"@proj-airi/ui-transitions": "workspace:^",
|
||||
"@proj-airi/unplugin-fetch": "^0.1.4",
|
||||
"@proj-airi/unplugin-live2d-sdk": "^0.1.2",
|
||||
"@shikijs/markdown-it": "^3.2.1",
|
||||
"@tauri-apps/cli": "^2.3.1",
|
||||
"@types/culori": "^2.1.1",
|
||||
"@types/markdown-it-link-attributes": "^3.0.5",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/three": "^0.174.0",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"@unocss/reset": "^66.1.0-beta.6",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue-macros/volar": "^3.0.0-beta.5",
|
||||
"@vueuse/motion": "^3.0.3",
|
||||
"less": "^4.2.2",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"unocss-preset-scrollbar": "^3.2.0",
|
||||
"unplugin-auto-import": "^19.1.1",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"unplugin-vue-macros": "^2.14.5",
|
||||
"unplugin-vue-markdown": "^28.3.1",
|
||||
"unplugin-vue-router": "^0.12.0",
|
||||
"vite-bundle-visualizer": "^1.2.1",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vue-tsc": "^3.0.0-alpha.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = [ "you" ]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = [
|
||||
"staticlib",
|
||||
"cdylib",
|
||||
"rlib"
|
||||
]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.5", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.3.1", features = [
|
||||
'macos-private-api',
|
||||
'tray-icon',
|
||||
'image-png'
|
||||
] }
|
||||
tauri-plugin-log = "2.0.0-rc"
|
||||
tauri-plugin-os = "2"
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"os:default",
|
||||
"core:window:default",
|
||||
"core:window:allow-start-dragging"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,16 @@
|
||||
use tauri::{WebviewUrl, WebviewWindowBuilder, Manager};
|
||||
use std::path::Path;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_settings_window(app: tauri::AppHandle) {
|
||||
if let Some(window) = app.get_webview_window("settings") {
|
||||
let _ = window.show();
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = WebviewWindowBuilder::new(&app, "settings", WebviewUrl::App(Path::new("#/settings").to_path_buf()))
|
||||
.title("settings")
|
||||
.inner_size(600.0, 800.0)
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use tauri::{TitleBarStyle, WebviewUrl, WebviewWindowBuilder, ActivationPolicy, Manager};
|
||||
use tauri::tray::{TrayIconBuilder};
|
||||
use tauri::menu::{Menu, MenuItem};
|
||||
use std::path::Path;
|
||||
|
||||
mod commands;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.setup(|app| {
|
||||
let _ = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
|
||||
.title("airi")
|
||||
.title_bar_style(TitleBarStyle::Transparent)
|
||||
.decorations(false)
|
||||
.inner_size(450.0, 600.0)
|
||||
.shadow(false)
|
||||
.transparent(true)
|
||||
.always_on_top(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(ActivationPolicy::Accessory); // hide dock icon
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// TODO: i18n
|
||||
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
||||
let settings_item = MenuItem::with_id(app, "settings", "Settings", true, None::<&str>)?;
|
||||
let hide_item = MenuItem::with_id(app, "hide", "Hide", true, None::<&str>)?;
|
||||
let show_item = MenuItem::with_id(app, "show", "Show", true, None::<&str>)?;
|
||||
let menu = Menu::with_items(app, &[&settings_item, &hide_item, &show_item, &quit_item])?;
|
||||
|
||||
let _ = TrayIconBuilder::new()
|
||||
.icon(app.default_window_icon().unwrap().clone()) // TODO: use custom icon
|
||||
.menu(&menu)
|
||||
.on_menu_event(|app, event| match event.id().as_ref() {
|
||||
"quit" => {
|
||||
app.exit(0);
|
||||
}
|
||||
"settings" => {
|
||||
if let Some(window) = app.get_webview_window("settings") {
|
||||
let _ = window.show();
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = WebviewWindowBuilder::new(app, "settings", WebviewUrl::App(Path::new("#/settings").to_path_buf()))
|
||||
.title("settings")
|
||||
.inner_size(600.0, 800.0)
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
"hide" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
"show" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.show_menu_on_left_click(true)
|
||||
.build(app)
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![commands::open_settings_window])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "airi",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.github.moeru-ai.airi-tamagotchi",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "pnpm run dev",
|
||||
"beforeBuildCommand": "pnpm run build"
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"macOSPrivateApi": true
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
import { useWindowControlStore } from './stores/window-controls'
|
||||
|
||||
const { language, themeColorsHue, themeColorsHueDynamic } = storeToRefs(useSettings())
|
||||
const i18n = useI18n()
|
||||
const windowControlStore = useWindowControlStore()
|
||||
|
||||
useEventListener(window, 'resize', () => {
|
||||
windowControlStore.size.width = window.innerWidth
|
||||
windowControlStore.size.height = window.innerHeight
|
||||
})
|
||||
|
||||
watch(language, (language) => {
|
||||
i18n.locale.value = language
|
||||
// window.electron.ipcRenderer.send('locale-changed', language)
|
||||
})
|
||||
|
||||
// FIXME: store settings to file
|
||||
onMounted(() => {
|
||||
// window.electron.ipcRenderer.send('locale-changed', language.value)
|
||||
// window.electron.ipcRenderer.send('window-size-changed', windowControlStore.size.width, windowControlStore.size.height)
|
||||
// window.electron.ipcRenderer.send('window-position-changed', windowControlStore.position.x, windowControlStore.position.y)
|
||||
})
|
||||
|
||||
watch(themeColorsHue, () => {
|
||||
document.documentElement.style.setProperty('--theme-colors-hue', themeColorsHue.value.toString())
|
||||
}, { immediate: true })
|
||||
|
||||
watch(themeColorsHueDynamic, () => {
|
||||
document.documentElement.classList.toggle('dynamic-hue', themeColorsHueDynamic.value)
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* We need this to properly animate the CSS variable */
|
||||
@property --theme-colors-hue {
|
||||
syntax: '<number>';
|
||||
initial-value: 0;
|
||||
inherits: true;
|
||||
}
|
||||
|
||||
@keyframes hue-anim {
|
||||
from {
|
||||
--theme-colors-hue: 0;
|
||||
}
|
||||
to {
|
||||
--theme-colors-hue: 360;
|
||||
}
|
||||
}
|
||||
|
||||
.dynamic-hue {
|
||||
animation: hue-anim 10s linear infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { useMarkdown } from '@proj-airi/stage-ui/composables'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores'
|
||||
import { useElementBounding, useScroll } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const chatHistoryRef = ref<HTMLDivElement>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { messages, sending } = storeToRefs(useChatStore())
|
||||
const bounding = useElementBounding(chatHistoryRef, { immediate: true, windowScroll: true, windowResize: true })
|
||||
const { y: chatHistoryContainerY } = useScroll(chatHistoryRef)
|
||||
|
||||
const { process } = useMarkdown()
|
||||
const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
|
||||
|
||||
onBeforeMessageComposed(async () => {
|
||||
// Scroll down to the new sent message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
})
|
||||
})
|
||||
|
||||
onTokenLiteral(async () => {
|
||||
// Scroll down to the new responding message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div py="1" flex="~ col" rounded="lg" overflow-hidden>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div
|
||||
ref="chatHistoryRef" v-auto-animate max-h="30vh" flex="~ col"
|
||||
scrollbar="~ w-2 track-color-transparent thumb-color-primary-300 rounded" h-full w-full overflow-scroll
|
||||
>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div v-for="(message, index) in messages" :key="index" mb-2>
|
||||
<div v-if="message.role === 'error'" flex mr="12">
|
||||
<div
|
||||
flex="~ col" border="4 solid violet-200/50 dark:violet-500/50" shadow="md violet-200/50 dark:none"
|
||||
min-w-20 rounded-lg px-2 py-1 h="unset <sm:fit" bg="<md:violet-500/25"
|
||||
>
|
||||
<div flex="~ row" gap-2>
|
||||
<div flex-1>
|
||||
<span text-xs text="violet-400/90 dark:violet-600/90" font-semibold class="inline <sm:hidden">{{
|
||||
t('stage.chat.message.character-name.core-system') }}</span>
|
||||
</div>
|
||||
<div i-solar:danger-triangle-bold-duotone text-violet-500 />
|
||||
</div>
|
||||
<div v-if="sending" i-eos-icons:three-dots-loading />
|
||||
<div
|
||||
v-else class="markdown-content text-violet-500" text="base <sm:xs"
|
||||
v-html="process(message.content as string)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="message.role === 'assistant'" flex mr="12">
|
||||
<div
|
||||
flex="~ col" border="4 solid primary-200" shadow="md primary-200/50" min-w-20 rounded-lg px-2 py-1 h="fit"
|
||||
bg="primary-100"
|
||||
>
|
||||
<div>
|
||||
<span text-xs text="primary-400/90" font-semibold class="inline hidden">Airi</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="message.content" class="markdown-content" text="xs primary-400"
|
||||
v-html="process(message.content as string)"
|
||||
/>
|
||||
<div v-else i-eos-icons:three-dots-loading />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="message.role === 'user'" flex="~">
|
||||
<div
|
||||
flex="~ col" border="4 solid cyan-200" shadow="md cyan-200/50" px="2" h="fit" min-w-20 rounded-lg px-2
|
||||
py-1 bg="cyan-100"
|
||||
>
|
||||
<div>
|
||||
<span text-xs text="cyan-600/90" font-semibold class="hidden">You</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="message.content" class="markdown-content" text="xs cyan-600"
|
||||
v-html="process(message.content as string)"
|
||||
/>
|
||||
<div v-else />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import { BasicTextarea } from '@proj-airi/stage-ui/components'
|
||||
import { useMicVAD } from '@proj-airi/stage-ui/composables'
|
||||
import { useChatStore, useConsciousnessStore, useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import TamagotchiChatHistory from './ChatHistory.vue'
|
||||
|
||||
const messageInput = ref('')
|
||||
const listening = ref(false)
|
||||
|
||||
// const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
|
||||
// const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { send, onAfterSend } = useChatStore()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { t } = useI18n()
|
||||
const providersStore = useProvidersStore()
|
||||
const { activeModel, activeProvider } = storeToRefs(useConsciousnessStore())
|
||||
|
||||
async function handleSend() {
|
||||
if (!messageInput.value.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
|
||||
await send(messageInput.value, {
|
||||
model: activeModel.value,
|
||||
chatProvider: providersStore.getProviderInstance(activeProvider.value) as ChatProvider,
|
||||
providerConfig,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
messages.value.pop()
|
||||
messages.value.push({
|
||||
role: 'error',
|
||||
content: (error as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { destroy, start } = useMicVAD(selectedAudioDeviceId, {
|
||||
onSpeechStart: () => {
|
||||
// TODO: interrupt the playback
|
||||
// TODO: interrupt any of the ongoing TTS
|
||||
// TODO: interrupt any of the ongoing LLM requests
|
||||
// TODO: interrupt any of the ongoing animation of Live2D or VRM
|
||||
// TODO: once interrupted, we should somehow switch to listen or thinking
|
||||
// emotion / expression?
|
||||
listening.value = true
|
||||
},
|
||||
// VAD misfire means while speech end is detected but
|
||||
// the frames of the segment of the audio buffer
|
||||
// is not enough to be considered as a speech segment
|
||||
// which controlled by the `minSpeechFrames` parameter
|
||||
onVADMisfire: () => {
|
||||
// TODO: do audio buffer send to whisper
|
||||
listening.value = false
|
||||
},
|
||||
onSpeechEnd: (buffer) => {
|
||||
// TODO: do audio buffer send to whisper
|
||||
listening.value = false
|
||||
handleTranscription(buffer)
|
||||
},
|
||||
auto: false,
|
||||
})
|
||||
|
||||
function handleTranscription(_buffer: Float32Array) {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert('Transcription is not implemented yet')
|
||||
}
|
||||
|
||||
// async function handleAudioInputChange(event: Event) {
|
||||
// const target = event.target as HTMLSelectElement
|
||||
// const found = audioInputs.value.find(d => d.deviceId === target.value)
|
||||
// if (!found) {
|
||||
// selectedAudioDevice.value = undefined
|
||||
// return
|
||||
// }
|
||||
|
||||
// selectedAudioDevice.value = found
|
||||
// }
|
||||
|
||||
function openSettings() {
|
||||
invoke('open_settings_window')
|
||||
}
|
||||
|
||||
watch(isAudioInputOn, async (value) => {
|
||||
if (value === 'false') {
|
||||
destroy()
|
||||
}
|
||||
})
|
||||
|
||||
onAfterSend(async () => {
|
||||
messageInput.value = ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
start()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div relative w-full flex gap-1>
|
||||
<TamagotchiChatHistory transform="translate-y-[-100%]" absolute left-0 top-0 w-full />
|
||||
<div flex flex-1>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
:placeholder="t('stage.message')"
|
||||
border="solid 2 primary-100"
|
||||
text="primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
|
||||
bg="primary-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]"
|
||||
w-full resize-none overflow-y-scroll rounded-l-xl p-2 font-medium outline-none
|
||||
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
|
||||
@submit="handleSend"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="px-4 py-2.5"
|
||||
border="solid 2 primary-100 "
|
||||
text="lg primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
|
||||
bg="primary-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]"
|
||||
flex items-center justify-center rounded-r-xl
|
||||
@click="openSettings"
|
||||
>
|
||||
<div i-solar:settings-bold-duotone />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,259 @@
|
||||
<script setup lang="ts">
|
||||
import { Collapsable } from '@proj-airi/stage-ui/components'
|
||||
import { Emotion, EmotionNeutralMotionName } from '@proj-airi/stage-ui/constants'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useFileDialog, useObjectUrl } from '@vueuse/core'
|
||||
import JSZip from 'jszip'
|
||||
import localforage from 'localforage'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const modelFile = useFileDialog({
|
||||
accept: 'application/zip',
|
||||
})
|
||||
|
||||
const settings = useSettings()
|
||||
const { live2dModelFile, live2dMotionMap, live2dLoadSource, loadingLive2dModel, availableLive2dMotions, live2dModelUrl } = storeToRefs(settings)
|
||||
const localModelUrl = ref(live2dModelUrl.value)
|
||||
|
||||
modelFile.onChange((files) => {
|
||||
if (files && files.length > 0) {
|
||||
live2dMotionMap.value = {}
|
||||
live2dModelFile.value = files[0]
|
||||
live2dLoadSource.value = 'file'
|
||||
loadingLive2dModel.value = true
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => settings.loadingLive2dModel, (value) => {
|
||||
if (value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (live2dLoadSource.value !== 'file') {
|
||||
return
|
||||
}
|
||||
|
||||
availableLive2dMotions.value.forEach((motion) => {
|
||||
if (motion.motionName in Emotion) {
|
||||
live2dMotionMap.value[motion.fileName] = motion.motionName
|
||||
}
|
||||
else {
|
||||
live2dMotionMap.value[motion.fileName] = EmotionNeutralMotionName
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function patchMotionMap(source: File, motionMap: Record<string, string>): Promise<File> {
|
||||
if (!Object.keys(motionMap).length)
|
||||
return source
|
||||
|
||||
const jsZip = new JSZip()
|
||||
const zip = await jsZip.loadAsync(source)
|
||||
const fileName = Object.keys(zip.files).find(key => key.endsWith('model3.json'))
|
||||
if (!fileName) {
|
||||
throw new Error('model3.json not found')
|
||||
}
|
||||
|
||||
const model3Json = await zip.file(fileName)!.async('string')
|
||||
const model3JsonObject = JSON.parse(model3Json)
|
||||
|
||||
const motions: Record<string, { File: string }[]> = {}
|
||||
Object.entries(motionMap).forEach(([key, value]) => {
|
||||
if (motions[value]) {
|
||||
motions[value].push({ File: key })
|
||||
return
|
||||
}
|
||||
motions[value] = [{ File: key }]
|
||||
})
|
||||
|
||||
model3JsonObject.FileReferences.Motions = motions
|
||||
|
||||
zip.file(fileName, JSON.stringify(model3JsonObject, null, 2))
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' })
|
||||
|
||||
return new File([zipBlob], source.name, {
|
||||
type: source.type,
|
||||
lastModified: source.lastModified,
|
||||
})
|
||||
}
|
||||
|
||||
async function saveMotionMap() {
|
||||
const fileFromIndexedDB = await localforage.getItem<File>('live2dModel')
|
||||
if (!fileFromIndexedDB) {
|
||||
return
|
||||
}
|
||||
|
||||
const patchedFile = await patchMotionMap(fileFromIndexedDB, live2dMotionMap.value)
|
||||
live2dModelFile.value = patchedFile
|
||||
live2dLoadSource.value = 'file'
|
||||
loadingLive2dModel.value = true
|
||||
}
|
||||
|
||||
const exportObjectUrl = useObjectUrl(live2dModelFile)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Collapsable w-full :default="true">
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex="~ row 1" items-center gap-1.5>
|
||||
<div
|
||||
i-solar:magic-stick-3-bold-duotone class="provider-icon size-6"
|
||||
transition="filter duration-250 ease-in-out"
|
||||
/>
|
||||
<div>
|
||||
{{ t('settings.live2d.change-model.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
||||
<div i-solar:alt-arrow-down-bold-duotone />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<div p-4>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-1 text-sm font-medium">
|
||||
{{ t('settings.live2d.change-model.from-url') }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
v-model="localModelUrl"
|
||||
:disabled="settings.loadingLive2dModel"
|
||||
type="text"
|
||||
rounded
|
||||
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
|
||||
transition="border duration-250 ease-in-out"
|
||||
px-2 py-1 text-sm outline-none
|
||||
:placeholder="t('settings.live2d.change-model.from-url-placeholder')"
|
||||
>
|
||||
<button
|
||||
:disabled="settings.loadingLive2dModel"
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
ml-2 rounded px-2 py-1 text-sm outline-none
|
||||
@click="live2dModelUrl = localModelUrl"
|
||||
>
|
||||
{{ t('settings.live2d.change-model.from-url-confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-1 text-sm font-medium">
|
||||
{{ t('settings.live2d.change-model.from-file') }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
:disabled="settings.loadingLive2dModel"
|
||||
rounded
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
px-2 py-1 text-sm outline-none
|
||||
@click="modelFile.open()"
|
||||
>
|
||||
{{ t('settings.live2d.change-model.from-file-select') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
<Collapsable mt-4 w-full :default="true">
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex="~ row 1" items-center gap-1.5>
|
||||
<div
|
||||
i-solar:face-scan-circle-bold-duotone class="provider-icon size-6"
|
||||
transition="filter duration-250 ease-in-out"
|
||||
/>
|
||||
<div>
|
||||
Edit motion map
|
||||
</div>
|
||||
</div>
|
||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
||||
<div i-solar:alt-arrow-down-bold-duotone />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<div p-4>
|
||||
<div v-if="settings.live2dLoadSource === 'file'" class="space-y-4">
|
||||
<div v-for="motion in settings.availableLive2dMotions" :key="motion.fileName" class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-1 text-sm font-medium">
|
||||
{{ motion.fileName }}
|
||||
</div>
|
||||
|
||||
<div flex gap-2>
|
||||
<select v-model="settings.live2dMotionMap[motion.fileName]">
|
||||
<option v-for="emotion in Object.keys(Emotion)" :key="emotion">
|
||||
{{ emotion }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
:disabled="settings.loadingLive2dModel"
|
||||
rounded
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
px-2 py-1 text-sm outline-none
|
||||
@click="settings.live2dCurrentMotion = { group: motion.motionName, index: motion.motionIndex }"
|
||||
>
|
||||
Play
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
:disabled="settings.loadingLive2dModel"
|
||||
w-full rounded
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
@click="saveMotionMap"
|
||||
>
|
||||
Save and patch
|
||||
</button>
|
||||
<a
|
||||
mt-2 block :href="exportObjectUrl"
|
||||
:download="`${settings.live2dModelFile?.name}-motion-edited.zip`"
|
||||
>
|
||||
<button
|
||||
:disabled="settings.loadingLive2dModel"
|
||||
w-full rounded
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<div v-else>
|
||||
Not available for URL model
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { EffectScope } from 'vue'
|
||||
|
||||
import { useMagicKeys, whenever } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, effectScope, watch } from 'vue'
|
||||
|
||||
import { useShortcutsStore } from '../stores/shortcuts'
|
||||
import { useWindowControlStore } from '../stores/window-controls'
|
||||
import { WindowControlMode } from '../types/window-controls'
|
||||
|
||||
export function useWindowShortcuts() {
|
||||
const windowStore = useWindowControlStore()
|
||||
const magicKeys = useMagicKeys()
|
||||
|
||||
const { shortcuts } = storeToRefs(useShortcutsStore())
|
||||
const handlers = computed(() => [
|
||||
{
|
||||
handle: () => {
|
||||
windowStore.setMode(WindowControlMode.MOVE)
|
||||
windowStore.toggleControl()
|
||||
},
|
||||
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'move')?.shortcut,
|
||||
},
|
||||
{
|
||||
handle: () => {
|
||||
windowStore.setMode(WindowControlMode.RESIZE)
|
||||
windowStore.toggleControl()
|
||||
},
|
||||
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'resize')?.shortcut,
|
||||
},
|
||||
{
|
||||
handle: () => {
|
||||
windowStore.setMode(WindowControlMode.DEBUG)
|
||||
windowStore.toggleControl()
|
||||
},
|
||||
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'debug')?.shortcut,
|
||||
},
|
||||
])
|
||||
|
||||
let currentScope: EffectScope | null = null
|
||||
watch(handlers, () => {
|
||||
if (currentScope) {
|
||||
currentScope.stop()
|
||||
}
|
||||
|
||||
currentScope = effectScope()
|
||||
currentScope.run(() => {
|
||||
handlers.value.forEach((handler) => {
|
||||
if (!handler.shortcut) {
|
||||
return
|
||||
}
|
||||
whenever(magicKeys[handler.shortcut], handler.handle)
|
||||
})
|
||||
})
|
||||
}, { immediate: true })
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div px-4 py-8 flex="~ col gap-4">
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
|
||||
import Tres from '@tresjs/core'
|
||||
import { MotionPlugin } from '@vueuse/motion'
|
||||
import { createPinia } from 'pinia'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
|
||||
import App from './App.vue'
|
||||
import { i18n } from './modules/i18n'
|
||||
import '@unocss/reset/tailwind.css'
|
||||
import 'uno.css'
|
||||
import './styles/main.css'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: setupLayouts(routes),
|
||||
})
|
||||
|
||||
createApp(App)
|
||||
.use(MotionPlugin)
|
||||
.use(autoAnimatePlugin)
|
||||
.use(router)
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.use(Tres)
|
||||
.mount('#app')
|
||||
@@ -0,0 +1,24 @@
|
||||
import messages from '@intlify/unplugin-vue-i18n/messages'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: getLocale(),
|
||||
fallbackLocale: 'en',
|
||||
messages,
|
||||
})
|
||||
|
||||
function getLocale() {
|
||||
const language = localStorage.getItem('settings/language')
|
||||
const languages = Object.keys(messages!)
|
||||
|
||||
if (language && languages.includes(language))
|
||||
return language
|
||||
|
||||
// let locale = navigator.language
|
||||
|
||||
// if (locale === 'zh')
|
||||
// locale = 'zh-CN'
|
||||
|
||||
return 'en'
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import { WidgetStage } from '@proj-airi/stage-ui/components'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import InteractiveArea from '../components/InteractiveArea.vue'
|
||||
import { useWindowShortcuts } from '../composables/window-shortcuts'
|
||||
import { useWindowControlStore } from '../stores/window-controls'
|
||||
import { WindowControlMode } from '../types/window-controls'
|
||||
|
||||
const windowStore = useWindowControlStore()
|
||||
useWindowShortcuts()
|
||||
|
||||
const modeIndicatorClass = computed(() => {
|
||||
switch (windowStore.controlMode) {
|
||||
case WindowControlMode.MOVE:
|
||||
return 'cursor-move'
|
||||
case WindowControlMode.RESIZE:
|
||||
return 'cursor-se-resize'
|
||||
case WindowControlMode.DEBUG:
|
||||
return 'debug-mode'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[modeIndicatorClass]"
|
||||
relative
|
||||
max-h="[100vh]"
|
||||
max-w="[100vw]"
|
||||
p="2"
|
||||
flex="~ col"
|
||||
z-2
|
||||
h-full
|
||||
overflow-hidden
|
||||
>
|
||||
<div relative h-full w-full items-end gap-2 class="view">
|
||||
<WidgetStage h-full w-full flex-1 mb="<md:18" />
|
||||
<InteractiveArea
|
||||
class="interaction-area block"
|
||||
:class="{ 'pointer-events-none': !windowStore.isControlActive }"
|
||||
absolute bottom-0 w-full transition="opacity duration-250" op-0
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Debug Mode UI -->
|
||||
<div v-if="windowStore.controlMode === WindowControlMode.DEBUG" class="debug-controls">
|
||||
<!-- Add debug controls here -->
|
||||
</div>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-250"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-opacity duration-250"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="windowStore.controlMode === WindowControlMode.MOVE"
|
||||
data-tauri-drag-region
|
||||
class="drag-region absolute left-0 top-0 z-999 h-full w-full flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<div class="absolute h-32 w-full flex items-center justify-center b-2 b-pink bg-white">
|
||||
<div class="wall absolute top-0 h-8" />
|
||||
<div data-tauri-drag-region class="text-primary-300 absolute left-0 top-0 h-full w-full flex animate-flash animate-duration-5s animate-count-infinite items-center justify-center text-1.5rem font-bold">
|
||||
DRAG HERE TO MOVE
|
||||
</div>
|
||||
<div data-tauri-drag-region class="wall absolute bottom-0 h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-250"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-opacity duration-250"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="windowStore.controlMode === WindowControlMode.RESIZE"
|
||||
class="absolute left-0 top-0 z-999 h-full w-full"
|
||||
>
|
||||
<div h-full w-full animate-flash animate-duration-2.5s animate-count-infinite b-8 b-pink rounded-2xl />
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.view {
|
||||
&:hover {
|
||||
.interaction-area {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drag-region {
|
||||
app-region: drag;
|
||||
cursor: grab !important;
|
||||
}
|
||||
|
||||
@keyframes wall-move {
|
||||
0% {
|
||||
transform: translateX(calc(var(--wall-width) * -2));
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(var(--wall-width) * 1));
|
||||
}
|
||||
}
|
||||
|
||||
.wall {
|
||||
@apply text-primary-200;
|
||||
|
||||
--wall-width: 8px;
|
||||
animation: wall-move 1s linear infinite;
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
currentColor,
|
||||
currentColor var(--wall-width),
|
||||
#ff00 var(--wall-width),
|
||||
#ff00 calc(var(--wall-width) * 2)
|
||||
);
|
||||
width: calc(100% + 4 * var(--wall-width));
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: stage
|
||||
</route>
|
||||
@@ -0,0 +1,226 @@
|
||||
<script setup lang="ts">
|
||||
import { IconItem } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { language, disableTransitions } = storeToRefs(useSettings())
|
||||
const dark = useDark()
|
||||
|
||||
function handleLanguageChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
language.value = target.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion
|
||||
flex="~ row" items-center
|
||||
gap-2
|
||||
:initial="{ opacity: 0, x: 10 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
:duration="100"
|
||||
>
|
||||
<h1 text-3xl>
|
||||
{{ t('settings.title') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div flex="~ col gap-4">
|
||||
<div flex="~ col gap-4">
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:title="t('settings.pages.modules.title')"
|
||||
:description="t('settings.pages.modules.description')"
|
||||
icon="i-lucide:blocks"
|
||||
to="/settings/modules"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="50"
|
||||
:title="t('settings.pages.models.title')"
|
||||
:description="t('settings.pages.models.description')"
|
||||
icon="i-lucide:person-standing"
|
||||
to="/settings/models"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="100"
|
||||
:title="t('settings.pages.providers.title')"
|
||||
:description="t('settings.pages.providers.description')"
|
||||
icon="i-lucide:brain"
|
||||
to="/settings/providers"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="150"
|
||||
:title="t('settings.pages.themes.title')"
|
||||
:description="t('settings.pages.themes.description')"
|
||||
icon="i-lucide:paintbrush"
|
||||
to="/settings/themes"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="150"
|
||||
:title="t('settings.pages.shortcuts.title')"
|
||||
:description="t('settings.pages.shortcuts.description')"
|
||||
icon="i-lucide:keyboard"
|
||||
to="/settings/shortcuts"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="150"
|
||||
>
|
||||
<h2 text-2xl>
|
||||
{{ t('settings.sections.section.general.title') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div flex="~ col gap-4">
|
||||
<!-- Language Setting -->
|
||||
<div
|
||||
v-motion
|
||||
grid="~ cols-[150px_1fr]"
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250" items-center gap-1.5 rounded-lg px-4
|
||||
py-3
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="200"
|
||||
>
|
||||
<div text="sm">
|
||||
<span>{{ t('settings.language.title') }}</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full justify-end>
|
||||
<select
|
||||
class="w-32"
|
||||
bg="transparent"
|
||||
text="sm right neutral-800 dark:neutral-100"
|
||||
transition="all ease-in-out duration-250"
|
||||
outline="none"
|
||||
cursor-pointer
|
||||
@change="handleLanguageChange"
|
||||
>
|
||||
<option value="en-US">
|
||||
{{ t('settings.language.english') }}
|
||||
</option>
|
||||
<option value="zh-CN">
|
||||
{{ t('settings.language.chinese') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Theme Setting -->
|
||||
<label
|
||||
v-motion
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex cursor-pointer rounded-lg px-4 py-3
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="250"
|
||||
>
|
||||
<input
|
||||
v-model="dark"
|
||||
text="neutral-800 dark:neutral-100"
|
||||
:checked="dark"
|
||||
:aria-checked="dark"
|
||||
type="checkbox"
|
||||
hidden appearance-none outline-none
|
||||
>
|
||||
<div flex="~ row" w-full items-center gap-1.5>
|
||||
<div text="sm" w-full flex-1>
|
||||
<span>{{ t('settings.dark-mode') }}</span>
|
||||
</div>
|
||||
<div select-none>
|
||||
<Transition name="slide-away" mode="out-in">
|
||||
<div
|
||||
v-if="dark"
|
||||
i-solar:moon-stars-bold-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
i-solar:sun-fog-bold-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<!-- Developer Settings -->
|
||||
<label
|
||||
v-motion
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex cursor-pointer rounded-lg px-4 py-3
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="300"
|
||||
>
|
||||
<input
|
||||
v-model="disableTransitions"
|
||||
text="neutral-800 dark:neutral-100"
|
||||
:checked="disableTransitions"
|
||||
:aria-checked="disableTransitions"
|
||||
type="checkbox"
|
||||
hidden appearance-none outline-none
|
||||
>
|
||||
<div flex="~ row" w-full items-center gap-1.5>
|
||||
<div text="sm" w-full flex-1>
|
||||
<span>{{ t('settings.stage-transitions') }}</span>
|
||||
</div>
|
||||
<div select-none>
|
||||
<Transition name="slide-away" mode="out-in">
|
||||
<div
|
||||
v-if="disableTransitions"
|
||||
i-solar:people-nearby-bold-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
i-solar:running-2-line-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div v-motion text="60" i-lucide:cog />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import Live2DCanvas from '@proj-airi/stage-ui/components/Live2D/Canvas.vue'
|
||||
import Live2DModel from '@proj-airi/stage-ui/components/Live2D/Model.vue'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { Vibrant } from 'node-vibrant/browser'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import Live2DSettings from '../../../components/Widgets/Live2DSettings.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const live2dContainerRef = ref<HTMLDivElement>()
|
||||
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
|
||||
const { width, height } = useElementBounding(live2dContainerRef)
|
||||
|
||||
const palette = ref<string[]>([])
|
||||
|
||||
async function extractColorsFromModel() {
|
||||
if (!live2dCanvasRef.value)
|
||||
return
|
||||
|
||||
const frame = await live2dCanvasRef.value.captureFrame()
|
||||
if (!frame) {
|
||||
console.error('No frame captured')
|
||||
return
|
||||
}
|
||||
|
||||
const frameUrl = URL.createObjectURL(frame)
|
||||
const vibrant = new Vibrant(frameUrl)
|
||||
|
||||
const paletteFromVibrant = await vibrant.getPalette()
|
||||
palette.value = Object.values(paletteFromVibrant).map(color => color?.hex).filter(it => typeof it === 'string')
|
||||
|
||||
URL.revokeObjectURL(frameUrl)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion
|
||||
flex="~ row" items-center gap-2
|
||||
:initial="{ opacity: 0, x: 10 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
:leave="{ opacity: 0, x: -10 }"
|
||||
:duration="250"
|
||||
>
|
||||
<button @click="router.back()">
|
||||
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
||||
</button>
|
||||
<h1 relative>
|
||||
<div absolute left-0 top-0 translate-y="[-80%]">
|
||||
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.models.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
<div flex>
|
||||
<div ref="live2dContainerRef" w="50%" h="80vh">
|
||||
<Live2DCanvas v-slot="{ app }" ref="live2dCanvasRef" :width="width" :height="height">
|
||||
<Live2DModel :app="app" :mouth-open-size="0" :width="width" :height="height" :paused="false" />
|
||||
</Live2DCanvas>
|
||||
</div>
|
||||
<Live2DSettings w="50%" h="80vh" :palette="palette" @extract-colors-from-model="extractColorsFromModel" />
|
||||
</div>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:person-standing />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -0,0 +1,226 @@
|
||||
<script setup lang="ts">
|
||||
import { RadioCardDetailManySelect, RadioCardSimple } from '@proj-airi/stage-ui/components'
|
||||
import { useConsciousnessStore, useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
|
||||
const providersStore = useProvidersStore()
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const { availableProviders, availableTextGenerationsProvidersMetadata } = storeToRefs(providersStore)
|
||||
const {
|
||||
activeProvider,
|
||||
activeModel,
|
||||
customModelName,
|
||||
modelSearchQuery,
|
||||
supportsModelListing,
|
||||
providerModels,
|
||||
isLoadingActiveProviderModels,
|
||||
activeProviderModelError,
|
||||
} = storeToRefs(consciousnessStore)
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(async () => {
|
||||
await consciousnessStore.loadModelsForProvider(activeProvider.value)
|
||||
})
|
||||
|
||||
function updateCustomModelName(value: string) {
|
||||
customModelName.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion
|
||||
flex="~ row" items-center gap-2
|
||||
:initial="{ opacity: 0, x: 10 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
:leave="{ opacity: 0, x: -10 }"
|
||||
:duration="250"
|
||||
>
|
||||
<button @click="router.back()">
|
||||
<div i-solar:alt-arrow-left-line-duotone text-xl />
|
||||
</button>
|
||||
<h1 relative>
|
||||
<div absolute left-0 top-0 translate-y="[-80%]">
|
||||
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.pages.modules.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.modules.consciousness.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-4">
|
||||
<div>
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-500">
|
||||
{{ t('settings.pages.providers.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div max-w-full>
|
||||
<!--
|
||||
fieldset has min-width set to --webkit-min-container, in order to use over flow scroll,
|
||||
we need to set the min-width to 0.
|
||||
See also: https://stackoverflow.com/a/33737340
|
||||
-->
|
||||
<fieldset
|
||||
v-if="availableProviders.length > 0"
|
||||
flex="~ row gap-4"
|
||||
:style="{ 'scrollbar-width': 'none' }"
|
||||
min-w-0 of-x-scroll scroll-smooth
|
||||
role="radiogroup"
|
||||
>
|
||||
<RadioCardSimple
|
||||
v-for="metadata in availableTextGenerationsProvidersMetadata"
|
||||
:id="metadata.id"
|
||||
:key="metadata.id"
|
||||
v-model="activeProvider"
|
||||
name="provider"
|
||||
:value="metadata.id"
|
||||
:title="metadata.localizedName"
|
||||
:description="metadata.localizedDescription"
|
||||
/>
|
||||
</fieldset>
|
||||
<div v-else>
|
||||
<RouterLink
|
||||
class="flex items-center gap-3 rounded-lg p-4"
|
||||
border="2 dashed neutral-200 dark:neutral-800"
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
transition="colors duration-200 ease-in-out"
|
||||
to="/settings/providers"
|
||||
>
|
||||
<div i-solar:warning-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">No Providers Configured</span>
|
||||
<span class="text-sm text-neutral-400 dark:text-neutral-500">Click here to set up your LLM
|
||||
providers</span>
|
||||
</div>
|
||||
<div i-solar:arrow-right-line-duotone class="ml-auto text-xl text-neutral-400 dark:text-neutral-500" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model selection section -->
|
||||
<div v-if="activeProvider && supportsModelListing">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg md:text-2xl">
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingActiveProviderModels" class="flex items-center justify-center py-4">
|
||||
<div class="mr-2 animate-spin">
|
||||
<div i-solar:spinner-line-duotone text-xl />
|
||||
</div>
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.loading') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div
|
||||
v-else-if="activeProviderModelError"
|
||||
class="flex items-center gap-3 border border-red-200 rounded-lg bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
|
||||
>
|
||||
<div i-solar:close-circle-line-duotone class="text-2xl text-red-500 dark:text-red-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.error') }}</span>
|
||||
<span class="text-sm text-red-600 dark:text-red-400">{{ activeProviderModelError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No models available -->
|
||||
<div
|
||||
v-else-if="providerModels.length === 0 && !isLoadingActiveProviderModels"
|
||||
class="flex items-center gap-3 border border-amber-200 rounded-lg bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
|
||||
>
|
||||
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models')
|
||||
}}</span>
|
||||
<span class="text-sm text-amber-600 dark:text-amber-400">{{
|
||||
t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models_description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Using the new RadioCardDetailManySelect component -->
|
||||
<template v-else-if="providerModels.length > 0">
|
||||
<RadioCardDetailManySelect
|
||||
v-model="activeModel"
|
||||
v-model:search-query="modelSearchQuery"
|
||||
:items="providerModels"
|
||||
:searchable="true"
|
||||
:search-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_placeholder')"
|
||||
:search-no-results-title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results')"
|
||||
:search-no-results-description="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results_description', { query: modelSearchQuery })"
|
||||
:search-results-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_results', { count: '{count}', total: '{total}' })"
|
||||
:custom-input-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.custom_model_placeholder')"
|
||||
:expand-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.expand')"
|
||||
:collapse-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.collapse')"
|
||||
@update:custom-value="updateCustomModelName"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Provider doesn't support model listing -->
|
||||
<div v-else-if="activeProvider && !supportsModelListing">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-500">
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-primary-50 border-primary-200 dark:bg-primary-900/20 dark:border-primary-800 flex items-center gap-3 border rounded-lg p-4"
|
||||
>
|
||||
<div i-solar:info-circle-line-duotone class="text-primary-500 dark:text-primary-400 text-2xl" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.not_supported')
|
||||
}}</span>
|
||||
<span class="dark:text-primary-400 text-primary-600 text-sm">{{
|
||||
t('settings.pages.modules.consciousness.sections.section.provider-model-selection.not_supported_description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual model input for providers without model listing -->
|
||||
<div class="mt-2">
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_name') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="activeModel" type="text"
|
||||
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
|
||||
:placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:ghost />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { IconStatusItem } from '@proj-airi/stage-ui/components'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Module {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
icon?: string
|
||||
iconColor?: string
|
||||
iconImage?: string
|
||||
to: string
|
||||
configured: boolean
|
||||
}
|
||||
|
||||
// TODO: categorize modules, such as essential, messaging, gaming, etc.
|
||||
const modulesList = computed<Module[]>(() => [
|
||||
{
|
||||
id: 'consciousness',
|
||||
name: t('settings.pages.modules.consciousness.title'),
|
||||
description: t('settings.pages.modules.consciousness.description'),
|
||||
icon: 'i-lucide:ghost',
|
||||
to: '/settings/modules/consciousness',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'hearing',
|
||||
name: t('settings.pages.modules.hearing.title'),
|
||||
description: t('settings.pages.modules.hearing.description'),
|
||||
icon: 'i-lucide:ear',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'messaging-discord',
|
||||
name: t('settings.pages.modules.messaging-discord.title'),
|
||||
description: t('settings.pages.modules.messaging-discord.description'),
|
||||
icon: 'i-simple-icons:discord',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'speech',
|
||||
name: t('settings.pages.modules.speech.title'),
|
||||
description: t('settings.pages.modules.speech.description'),
|
||||
icon: 'i-lucide:mic',
|
||||
to: '/settings/modules/speech',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'memory-short-term',
|
||||
name: t('settings.pages.modules.memory-short-term.title'),
|
||||
description: t('settings.pages.modules.memory-short-term.description'),
|
||||
icon: 'i-lucide:book',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'memory-long-term',
|
||||
name: t('settings.pages.modules.memory-long-term.title'),
|
||||
description: t('settings.pages.modules.memory-long-term.description'),
|
||||
icon: 'i-lucide:book-copy',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'vision',
|
||||
name: t('settings.pages.modules.vision.title'),
|
||||
description: t('settings.pages.modules.vision.description'),
|
||||
icon: 'i-lucide:eye',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'game-minecraft',
|
||||
name: t('settings.pages.modules.gaming-minecraft.title'),
|
||||
description: t('settings.pages.modules.gaming-minecraft.description'),
|
||||
iconColor: 'i-vscode-icons:file-type-minecraft',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'game-factorio',
|
||||
name: t('settings.pages.modules.gaming-factorio.title'),
|
||||
description: t('settings.pages.modules.gaming-factorio.description'),
|
||||
iconImage: '',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'x',
|
||||
name: t('settings.pages.modules.x.title'),
|
||||
description: t('settings.pages.modules.x.description'),
|
||||
icon: 'i-simple-icons:x',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion
|
||||
flex="~ row" items-center gap-2
|
||||
:initial="{ opacity: 0, x: 10 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
:leave="{ opacity: 0, x: -10 }"
|
||||
:duration="250"
|
||||
>
|
||||
<button @click="router.back()">
|
||||
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
||||
</button>
|
||||
<h1 relative>
|
||||
<div absolute left-0 top-0 translate-y="[-80%]">
|
||||
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.modules.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
<div grid="~ cols-1 sm:cols-2 gap-4">
|
||||
<IconStatusItem
|
||||
v-for="(module, index) of modulesList"
|
||||
:key="module.id"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + index * 10"
|
||||
:delay="index * 50"
|
||||
:title="module.name"
|
||||
:description="module.description"
|
||||
:icon="module.icon"
|
||||
:icon-color="module.iconColor"
|
||||
:icon-image="module.iconImage"
|
||||
:to="module.to"
|
||||
:configured="module.configured"
|
||||
/>
|
||||
</div>
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:blocks />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -0,0 +1,532 @@
|
||||
<script setup lang="ts">
|
||||
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
|
||||
|
||||
import {
|
||||
FieldCheckbox,
|
||||
FieldInput,
|
||||
FieldRange,
|
||||
RadioCardDetailManySelect,
|
||||
RadioCardSimple,
|
||||
Skeleton,
|
||||
TestDummyMarker,
|
||||
VoiceCardManySelect,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
|
||||
import { generateSpeech } from '@xsai/generate-speech'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const providersStore = useProvidersStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const {
|
||||
availableProviders,
|
||||
availableAudioSpeechProvidersMetadata,
|
||||
} = storeToRefs(providersStore)
|
||||
const {
|
||||
activeSpeechProvider,
|
||||
activeSpeechModel,
|
||||
activeSpeechVoice,
|
||||
activeSpeechVoiceId,
|
||||
pitch,
|
||||
isLoadingSpeechProviderVoices,
|
||||
supportsModelListing,
|
||||
providerModels,
|
||||
isLoadingActiveProviderModels,
|
||||
activeProviderModelError,
|
||||
modelSearchQuery,
|
||||
speechProviderError,
|
||||
ssmlEnabled,
|
||||
availableVoices,
|
||||
} = storeToRefs(speechStore)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const voiceSearchQuery = ref('')
|
||||
const useSSML = ref(false)
|
||||
const testText = ref('Hello, my name is AI Assistant')
|
||||
const ssmlText = ref('')
|
||||
const isGenerating = ref(false)
|
||||
const audioUrl = ref('')
|
||||
const audioPlayer = ref<HTMLAudioElement | null>(null)
|
||||
const errorMessage = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
await speechStore.loadVoicesForProvider(activeSpeechProvider.value)
|
||||
await providersStore.loadModelsForConfiguredProviders()
|
||||
})
|
||||
|
||||
watch(activeSpeechProvider, async () => {
|
||||
await speechStore.loadVoicesForProvider(activeSpeechProvider.value)
|
||||
await providersStore.loadModelsForConfiguredProviders()
|
||||
})
|
||||
|
||||
// Function to generate speech
|
||||
async function generateTestSpeech() {
|
||||
if (!testText.value.trim() && !useSSML.value)
|
||||
return
|
||||
|
||||
if (useSSML.value && !ssmlText.value.trim())
|
||||
return
|
||||
|
||||
if (!activeSpeechModel.value) {
|
||||
console.error('No model selected')
|
||||
return
|
||||
}
|
||||
|
||||
if (!activeSpeechVoice.value) {
|
||||
console.error('No voice selected')
|
||||
return
|
||||
}
|
||||
|
||||
const provider = providersStore.getProviderInstance(activeSpeechProvider.value) as SpeechProviderWithExtraOptions<string, any>
|
||||
if (!provider) {
|
||||
console.error('Failed to initialize speech provider')
|
||||
return
|
||||
}
|
||||
|
||||
const providerConfig = providersStore.getProviderConfig(activeSpeechProvider.value)
|
||||
|
||||
isGenerating.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
// Stop any currently playing audio
|
||||
if (audioUrl.value) {
|
||||
stopTestAudio()
|
||||
}
|
||||
|
||||
const input = useSSML.value
|
||||
? ssmlText.value
|
||||
: speechStore.generateSSML(testText.value, activeSpeechVoice.value, { ...providerConfig, pitch: pitch.value })
|
||||
|
||||
const response = await generateSpeech({
|
||||
...provider.speech(activeSpeechModel.value, providerConfig),
|
||||
input,
|
||||
voice: activeSpeechVoice.value.id,
|
||||
})
|
||||
|
||||
// Convert the response to a blob and create an object URL
|
||||
audioUrl.value = URL.createObjectURL(new Blob([response]))
|
||||
|
||||
// Play the audio
|
||||
setTimeout(() => {
|
||||
if (audioPlayer.value) {
|
||||
audioPlayer.value.play()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error generating speech:', error)
|
||||
errorMessage.value = error instanceof Error ? error.message : 'An unknown error occurred'
|
||||
}
|
||||
finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Function to stop audio playback
|
||||
function stopTestAudio() {
|
||||
if (audioPlayer.value) {
|
||||
audioPlayer.value.pause()
|
||||
audioPlayer.value.currentTime = 0
|
||||
}
|
||||
|
||||
// Clean up the object URL to prevent memory leaks
|
||||
if (audioUrl.value) {
|
||||
URL.revokeObjectURL(audioUrl.value)
|
||||
audioUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up when component is unmounted
|
||||
onUnmounted(() => {
|
||||
if (audioUrl.value) {
|
||||
URL.revokeObjectURL(audioUrl.value)
|
||||
}
|
||||
})
|
||||
|
||||
function updateCustomVoiceName(value: string) {
|
||||
activeSpeechVoice.value = {
|
||||
id: value,
|
||||
name: value,
|
||||
description: value,
|
||||
previewURL: value,
|
||||
languages: [{ code: 'en', title: 'English' }],
|
||||
provider: activeSpeechProvider.value,
|
||||
gender: 'male',
|
||||
}
|
||||
}
|
||||
|
||||
function updateCustomModelName(value: string) {
|
||||
activeSpeechModel.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion flex="~ row" items-center gap-2 :initial="{ opacity: 0, x: 10 }" :enter="{ opacity: 1, x: 0 }"
|
||||
:leave="{ opacity: 0, x: -10 }" :duration="250"
|
||||
>
|
||||
<button @click="router.back()">
|
||||
<div i-solar:alt-arrow-left-line-duotone text-xl />
|
||||
</button>
|
||||
<h1 relative>
|
||||
<div absolute left-0 top-0 translate-y="[-80%]">
|
||||
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.pages.modules.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.modules.speech.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div flex="~ col md:row gap-6">
|
||||
<div bg="neutral-100 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-4" class="w-full md:w-[40%]">
|
||||
<div>
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
|
||||
{{ t('settings.pages.modules.speech.sections.section.provider-voice-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-500">
|
||||
<span>{{ t('settings.pages.modules.speech.sections.section.provider-voice-selection.description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div max-w-full>
|
||||
<fieldset
|
||||
v-if="availableProviders.length > 0" flex="~ row gap-4" :style="{ 'scrollbar-width': 'none' }"
|
||||
min-w-0 of-x-scroll scroll-smooth role="radiogroup"
|
||||
>
|
||||
<RadioCardSimple
|
||||
v-for="metadata in availableAudioSpeechProvidersMetadata"
|
||||
:id="metadata.id"
|
||||
:key="metadata.id"
|
||||
v-model="activeSpeechProvider"
|
||||
name="speech-provider"
|
||||
:value="metadata.id"
|
||||
:title="metadata.localizedName"
|
||||
:description="metadata.localizedDescription"
|
||||
/>
|
||||
</fieldset>
|
||||
<div v-else>
|
||||
<RouterLink
|
||||
class="flex items-center gap-3 rounded-lg p-4" border="2 dashed neutral-200 dark:neutral-800"
|
||||
bg="neutral-50 dark:neutral-800" transition="colors duration-200 ease-in-out" to="/settings/providers"
|
||||
>
|
||||
<div i-solar:warning-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">No Speech Providers Configured</span>
|
||||
<span class="text-sm text-neutral-400 dark:text-neutral-500">Click here to set up your speech
|
||||
providers</span>
|
||||
</div>
|
||||
<div i-solar:arrow-right-line-duotone class="ml-auto text-xl text-neutral-400 dark:text-neutral-500" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Model selection section -->
|
||||
<div v-if="activeSpeechProvider && supportsModelListing">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg md:text-2xl">
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingActiveProviderModels" class="flex items-center justify-center py-4">
|
||||
<div class="mr-2 animate-spin">
|
||||
<div i-solar:spinner-line-duotone text-xl />
|
||||
</div>
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.loading') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div
|
||||
v-else-if="activeProviderModelError"
|
||||
class="flex items-center gap-3 border border-red-200 rounded-lg bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
|
||||
>
|
||||
<div i-solar:close-circle-line-duotone class="text-2xl text-red-500 dark:text-red-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.error') }}</span>
|
||||
<span class="text-sm text-red-600 dark:text-red-400">{{ activeProviderModelError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No models available -->
|
||||
<div
|
||||
v-else-if="providerModels.length === 0 && !isLoadingActiveProviderModels"
|
||||
class="flex items-center gap-3 border border-amber-200 rounded-lg bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
|
||||
>
|
||||
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models')
|
||||
}}</span>
|
||||
<span class="text-sm text-amber-600 dark:text-amber-400">{{
|
||||
t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models_description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Using the new RadioCardDetailManySelect component -->
|
||||
<template v-else-if="providerModels.length > 0">
|
||||
<RadioCardDetailManySelect
|
||||
v-model="activeSpeechModel"
|
||||
v-model:search-query="modelSearchQuery"
|
||||
:items="providerModels"
|
||||
:searchable="true"
|
||||
:search-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_placeholder')"
|
||||
:search-no-results-title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results')"
|
||||
:search-no-results-description="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results_description', { query: modelSearchQuery })"
|
||||
:search-results-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_results', { count: '{count}', total: '{total}' })"
|
||||
:custom-input-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.custom_model_placeholder')"
|
||||
:expand-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.expand')"
|
||||
:collapse-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.collapse')"
|
||||
@update:custom-value="updateCustomModelName"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voice Configuration Section -->
|
||||
<div v-if="activeSpeechProvider">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
|
||||
Voice Configuration
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-500">
|
||||
<span>Customize how your AI assistant speaks</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingSpeechProviderVoices">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Skeleton class="w-full rounded-lg p-2.5 text-sm">
|
||||
<div class="h-1lh" />
|
||||
</Skeleton>
|
||||
<div flex="~ row gap-4">
|
||||
<Skeleton class="w-full rounded-lg p-4 text-sm">
|
||||
<div class="h-1lh" />
|
||||
</Skeleton>
|
||||
<Skeleton class="w-full rounded-lg p-4 text-sm">
|
||||
<div class="h-1lh" />
|
||||
</Skeleton>
|
||||
<Skeleton class="w-full rounded-lg p-4 text-sm">
|
||||
<div class="h-1lh" />
|
||||
</Skeleton>
|
||||
</div>
|
||||
<Skeleton class="w-full rounded-lg p-3 text-sm">
|
||||
<div class="h-1lh" />
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<!-- Voice selection with RadioCardDetailManySelect -->
|
||||
<div
|
||||
v-else-if="availableVoices[activeSpeechProvider] && availableVoices[activeSpeechProvider].length > 0"
|
||||
class="space-y-6"
|
||||
>
|
||||
<VoiceCardManySelect
|
||||
v-model:search-query="voiceSearchQuery"
|
||||
v-model:voice-id="activeSpeechVoiceId"
|
||||
:voices="availableVoices[activeSpeechProvider]?.map(voice => ({
|
||||
id: voice.id,
|
||||
name: voice.name,
|
||||
description: voice.description,
|
||||
previewURL: voice.previewURL,
|
||||
customizable: false,
|
||||
}))"
|
||||
:searchable="true"
|
||||
:search-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_placeholder')"
|
||||
:search-no-results-title="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices')"
|
||||
:search-no-results-description="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices_description')"
|
||||
:search-results-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_results', { count: 0, total: 0 })"
|
||||
:custom-input-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.custom_voice_placeholder')"
|
||||
:expand-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_more')"
|
||||
:collapse-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_less')"
|
||||
:play-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.play_sample')"
|
||||
:pause-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.pause')"
|
||||
@update:custom-value="updateCustomVoiceName"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="speechProviderError"
|
||||
class="flex items-center gap-3 border border-2 border-red-200 rounded-lg bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
|
||||
>
|
||||
<div i-solar:close-circle-line-duotone class="text-2xl text-red-500 dark:text-red-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">Error loading voices</span>
|
||||
<span class="text-sm text-red-600 dark:text-red-400">{{ speechProviderError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- No voices available -->
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center gap-3 border border-2 border-amber-200 rounded-lg bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
|
||||
>
|
||||
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">No voices available</span>
|
||||
<span class="text-sm text-amber-600 dark:text-amber-400">
|
||||
No voices were found for this provider. You can enter a custom voice name below.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voice parameters -->
|
||||
<div flex="~ col gap-4">
|
||||
<FieldRange
|
||||
v-model="pitch"
|
||||
label="Pitch"
|
||||
description="Tune the pitch of the voice"
|
||||
:min="-100" :max="100" :step="1"
|
||||
:format-value="value => `${value}%`"
|
||||
/>
|
||||
<!-- SSML Support -->
|
||||
<FieldCheckbox
|
||||
v-model="ssmlEnabled"
|
||||
label="Enable SSML"
|
||||
description="Enable Speech Synthesis Markup Language for more control over speech output"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Manual voice input when no voices are available -->
|
||||
<div
|
||||
v-if="!availableVoices[activeSpeechProvider] || availableVoices[activeSpeechProvider].length === 0"
|
||||
class="mt-2 space-y-6"
|
||||
>
|
||||
<FieldInput
|
||||
v-model="activeSpeechVoiceId"
|
||||
type="text"
|
||||
label="Voice ID"
|
||||
description="Enter the voice ID for your custom voice"
|
||||
placeholder="Enter voice name (e.g., 'Rachel', 'Josh')"
|
||||
/>
|
||||
|
||||
<!-- Model selection for ElevenLabs -->
|
||||
<div v-if="activeSpeechProvider === 'elevenlabs'">
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
v-model="activeSpeechModel"
|
||||
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
|
||||
>
|
||||
<option value="eleven_monolingual_v1">
|
||||
Monolingual v1
|
||||
</option>
|
||||
<option value="eleven_multilingual_v1">
|
||||
Multilingual v1
|
||||
</option>
|
||||
<option value="eleven_multilingual_v2">
|
||||
Multilingual v2
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div flex="~ col gap-4">
|
||||
<FieldRange
|
||||
v-model="pitch"
|
||||
label="Pitch"
|
||||
description="Tune the pitch of the voice"
|
||||
:min="-100" :max="100" :step="1"
|
||||
:format-value="value => `${value}%`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div flex="~ col gap-6" class="w-full md:w-[60%]">
|
||||
<div w-full rounded-xl>
|
||||
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400" w-full>
|
||||
<div class="inline-flex items-center gap-4">
|
||||
<TestDummyMarker />
|
||||
<div>
|
||||
{{ t('settings.pages.providers.provider.elevenlabs.playground.title') }}
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<div flex="~ col gap-4">
|
||||
<FieldCheckbox
|
||||
v-model="useSSML"
|
||||
label="Use Custom SSML"
|
||||
description="Enable to input raw SSML instead of plain text"
|
||||
/>
|
||||
|
||||
<template v-if="!useSSML">
|
||||
<textarea
|
||||
v-model="testText"
|
||||
:placeholder="t('settings.pages.providers.provider.elevenlabs.playground.fields.field.input.placeholder')"
|
||||
border="neutral-100 dark:neutral-800 solid 2 focus:neutral-200 dark:focus:neutral-700"
|
||||
transition="all duration-250 ease-in-out"
|
||||
bg="neutral-100 dark:neutral-800 focus:neutral-50 dark:focus:neutral-900"
|
||||
h-24 w-full rounded-lg px-3 py-2 text-sm outline-none
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<textarea
|
||||
v-model="ssmlText"
|
||||
placeholder="Enter SSML text..."
|
||||
border="neutral-100 dark:neutral-800 solid 2 focus:neutral-200 dark:focus:neutral-700"
|
||||
transition="all duration-250 ease-in-out"
|
||||
bg="neutral-100 dark:neutral-800 focus:neutral-50 dark:focus:neutral-900"
|
||||
h-48 w-full rounded-lg px-3 py-2 text-sm font-mono outline-none
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div flex="~ row" gap-4>
|
||||
<button
|
||||
border="neutral-800 dark:neutral-200 solid 2" transition="border duration-250 ease-in-out"
|
||||
rounded-lg px-4 text="neutral-100 dark:neutral-900" py-2 text-sm
|
||||
:disabled="isGenerating || (!testText.trim() && !useSSML) || (useSSML && !ssmlText.trim()) || !activeSpeechVoice"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': isGenerating || (!testText.trim() && !useSSML) || (useSSML && !ssmlText.trim()) || !activeSpeechVoice }"
|
||||
bg="neutral-700 dark:neutral-300" @click="generateTestSpeech"
|
||||
>
|
||||
<div flex="~ row" items-center gap-2>
|
||||
<div i-solar:play-circle-bold-duotone />
|
||||
<span>{{ isGenerating ? t('settings.pages.providers.provider.elevenlabs.playground.buttons.button.test-voice.generating') : t('settings.pages.providers.provider.elevenlabs.playground.buttons.button.test-voice.label') }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
v-if="audioUrl" border="primary-300 dark:primary-800 solid 2"
|
||||
transition="border duration-250 ease-in-out" rounded-lg px-4 py-2 text-sm @click="stopTestAudio"
|
||||
>
|
||||
<div flex="~ row" items-center gap-2>
|
||||
<div i-solar:stop-circle-bold-duotone />
|
||||
<span>Stop</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<audio v-if="audioUrl" ref="audioPlayer" :src="audioUrl" controls class="mt-2 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10
|
||||
translate-y-10
|
||||
>
|
||||
<div text="40" i-lucide:volume-2 />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
ProviderAccountIdInput,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'cloudflare-workers-ai'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
// Use computed properties for settings
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const accountId = computed({
|
||||
get: () => providers.value[providerId]?.accountId || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].accountId = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize provider if it doesn't exist
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
accountId.value = providers.value[providerId]?.accountId || ''
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([apiKey, accountId], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
apiKey: apiKey.value,
|
||||
accountId: accountId.value,
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
...(providerMetadata.value?.defaultOptions as any),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:placeholder="t('settings.pages.providers.provider.cloudflare-workers-ai.fields.field.api-key.placeholder')"
|
||||
/>
|
||||
|
||||
<ProviderAccountIdInput
|
||||
v-model="accountId"
|
||||
:label="t('settings.pages.providers.provider.cloudflare-workers-ai.fields.field.account-id.label')"
|
||||
:description="t('settings.pages.providers.provider.cloudflare-workers-ai.fields.field.account-id.description')"
|
||||
:placeholder="t('settings.pages.providers.provider.cloudflare-workers-ai.fields.field.account-id.placeholder')"
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'deepseek'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
// Use computed properties for settings
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize provider if it doesn't exist
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || ''
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || '',
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
...(providerMetadata.value?.defaultOptions as any),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.deepseek.com/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { UnElevenLabsOptions } from '@xsai-ext/providers-local'
|
||||
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
|
||||
|
||||
import {
|
||||
SpeechPlayground,
|
||||
SpeechProviderSettings,
|
||||
SpeechVoiceSettings,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'elevenlabs'
|
||||
const defaultModel = 'eleven_multilingual_v2'
|
||||
|
||||
// Default voice settings specific to ElevenLabs
|
||||
const defaultVoiceSettings = {
|
||||
similarityBoost: 0.75,
|
||||
stability: 0.5,
|
||||
speed: 1.0,
|
||||
style: 0,
|
||||
useSpeakerBoost: true,
|
||||
}
|
||||
|
||||
const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
|
||||
|
||||
// Get available voices for ElevenLabs
|
||||
const availableVoices = computed(() => {
|
||||
return speechStore.availableVoices[providerId] || []
|
||||
})
|
||||
|
||||
// Get available languages
|
||||
const availableLanguages = computed(() => {
|
||||
return speechStore.availableLanguages
|
||||
})
|
||||
|
||||
// Generate speech with ElevenLabs-specific parameters
|
||||
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
|
||||
const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>
|
||||
if (!provider) {
|
||||
throw new Error('Failed to initialize speech provider')
|
||||
}
|
||||
|
||||
// Get provider configuration
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
|
||||
// Get model from configuration or use default
|
||||
const model = providerConfig.model as string | undefined || defaultModel
|
||||
|
||||
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
model,
|
||||
input,
|
||||
voiceId,
|
||||
{
|
||||
...providerConfig,
|
||||
...defaultVoiceSettings,
|
||||
},
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SpeechProviderSettings
|
||||
:provider-id="providerId"
|
||||
:default-model="defaultModel"
|
||||
:additional-settings="defaultVoiceSettings"
|
||||
>
|
||||
<!-- Voice settings specific to ElevenLabs -->
|
||||
<template #voice-settings="{ voiceSettings, updateVoiceSettings }">
|
||||
<SpeechVoiceSettings
|
||||
:settings="voiceSettings"
|
||||
:show-similarity-boost="true"
|
||||
:show-stability="true"
|
||||
:show-speed="true"
|
||||
:show-style="true"
|
||||
:show-speaker-boost="true"
|
||||
@update="updateVoiceSettings"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Replace the default playground with our standalone component -->
|
||||
<template #playground>
|
||||
<SpeechPlayground
|
||||
:available-voices="availableVoices"
|
||||
:available-languages="availableLanguages"
|
||||
:generate-speech="handleGenerateSpeech"
|
||||
:api-key-configured="apiKeyConfigured"
|
||||
default-text="Hello! This is a test of the ElevenLabs voice synthesis."
|
||||
/>
|
||||
</template>
|
||||
</SpeechProviderSettings>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'fireworks-ai'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
// Use computed properties for settings
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize provider if it doesn't exist
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || ''
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || '',
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
...(providerMetadata.value?.defaultOptions as any),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon="providerMetadata?.icon"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="fw-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.fireworks.ai/inference/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { IconStatusItem } from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { allProvidersMetadata } = storeToRefs(providersStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion
|
||||
flex="~ row" items-center gap-2
|
||||
:initial="{ opacity: 0, x: 10 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
:leave="{ opacity: 0, x: -10 }"
|
||||
:duration="250"
|
||||
>
|
||||
<button @click="router.back()">
|
||||
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
||||
</button>
|
||||
<h1 relative>
|
||||
<div absolute left-0 top-0 translate-y="[-80%]">
|
||||
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.providers.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
<div grid="~ cols-2 gap-4">
|
||||
<IconStatusItem
|
||||
v-for="(provider, index) of allProvidersMetadata"
|
||||
:key="provider.id"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + index * 10"
|
||||
:delay="index * 50"
|
||||
:title="provider.localizedName"
|
||||
:description="provider.localizedDescription"
|
||||
:icon="provider.icon"
|
||||
:icon-color="provider.iconColor"
|
||||
:icon-image="provider.iconImage"
|
||||
:to="`/settings/providers/${provider.id}`"
|
||||
:configured="provider.configured"
|
||||
/>
|
||||
</div>
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:brain />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import type { UnMicrosoftOptions } from '@xsai-ext/providers-local'
|
||||
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
|
||||
|
||||
import {
|
||||
FieldInput,
|
||||
SpeechPlayground,
|
||||
SpeechProviderSettings,
|
||||
SpeechVoiceSettings,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const providerId = 'microsoft-speech'
|
||||
const defaultModel = 'v1'
|
||||
|
||||
// Default voice settings specific to Microsoft Speech
|
||||
const defaultVoiceSettings = {
|
||||
pitch: 0,
|
||||
speed: 1.0,
|
||||
volume: 0,
|
||||
}
|
||||
|
||||
const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
|
||||
const pitch = ref(0)
|
||||
const speed = ref(1.0)
|
||||
const volume = ref(0)
|
||||
|
||||
// Additional settings specific to Microsoft Speech (region)
|
||||
const region = computed({
|
||||
get: () => providers.value[providerId]?.region as string | undefined || 'eastasia',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].region = value
|
||||
},
|
||||
})
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
|
||||
|
||||
// Get available voices for Microsoft Speech
|
||||
const availableVoices = computed(() => {
|
||||
return speechStore.availableVoices[providerId] || []
|
||||
})
|
||||
|
||||
// Get available languages
|
||||
const availableLanguages = computed(() => {
|
||||
return speechStore.availableLanguages
|
||||
})
|
||||
|
||||
// Generate speech with Microsoft-specific parameters
|
||||
async function handleGenerateSpeech(input: string, voiceId: string, useSSML: boolean) {
|
||||
const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnMicrosoftOptions>
|
||||
if (!provider) {
|
||||
throw new Error('Failed to initialize speech provider')
|
||||
}
|
||||
|
||||
// Get provider configuration
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
|
||||
// Get model from configuration or use default
|
||||
const model = providerConfig.model as string | undefined || defaultModel
|
||||
|
||||
// For Microsoft Speech, we need to ensure we're using the right region
|
||||
const options = {
|
||||
...providerConfig,
|
||||
region: region.value,
|
||||
disableSsml: !useSSML, // If useSSML is true, we don't disable SSML
|
||||
}
|
||||
|
||||
// If not using SSML and we have a voice, generate SSML
|
||||
if (!useSSML && voiceId) {
|
||||
const voice = availableVoices.value.find(v => v.id === voiceId)
|
||||
if (voice) {
|
||||
const ssml = speechStore.generateSSML(
|
||||
input,
|
||||
voice,
|
||||
{ ...providerConfig, pitch: pitch.value },
|
||||
)
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
model,
|
||||
ssml,
|
||||
voiceId,
|
||||
options,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Either using direct SSML or no voice found
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
model,
|
||||
input,
|
||||
voiceId,
|
||||
options,
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SpeechProviderSettings
|
||||
:provider-id="providerId"
|
||||
:default-model="defaultModel"
|
||||
:additional-settings="defaultVoiceSettings"
|
||||
>
|
||||
<!-- Basic settings specific to Microsoft Speech -->
|
||||
<template #basic-settings>
|
||||
<FieldInput
|
||||
v-model="region"
|
||||
:label="t('settings.pages.providers.provider.microsoft-speech.fields.field.region.label')"
|
||||
:description="t('settings.pages.providers.provider.microsoft-speech.fields.field.region.description')"
|
||||
placeholder="eastasia"
|
||||
required
|
||||
type="text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Voice settings specific to Microsoft Speech -->
|
||||
<template #voice-settings="{ voiceSettings, updateVoiceSettings }">
|
||||
<SpeechVoiceSettings
|
||||
v-model:pitch="pitch"
|
||||
v-model:speed="speed"
|
||||
v-model:volume="volume"
|
||||
:settings="voiceSettings"
|
||||
:show-pitch="true"
|
||||
:show-speed="true"
|
||||
:show-volume="true"
|
||||
@update="updateVoiceSettings"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Replace the default playground with our standalone component -->
|
||||
<template #playground>
|
||||
<SpeechPlayground
|
||||
:available-voices="availableVoices"
|
||||
:available-languages="availableLanguages"
|
||||
:generate-speech="handleGenerateSpeech"
|
||||
:api-key-configured="apiKeyConfigured"
|
||||
default-text="Hello! This is a test of the Microsoft Speech synthesis."
|
||||
/>
|
||||
</template>
|
||||
</SpeechProviderSettings>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'mistral-ai'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
// Use computed properties for settings
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize provider if it doesn't exist
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || ''
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || '',
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
...(providerMetadata.value?.defaultOptions as any),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="mis-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.mistral.ai/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'moonshot-ai'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
// Use computed properties for settings
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize provider if it doesn't exist
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || ''
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || '',
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
...(providerMetadata.value?.defaultOptions as any),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon="providerMetadata?.icon"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="ms-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.moonshot.cn/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'novita-ai'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
// Use computed properties for settings
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize provider if it doesn't exist
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || ''
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || '',
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
...(providerMetadata.value?.defaultOptions as any),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="nvt-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.novita.ai/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
FieldKeyValues,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'ollama'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || ''
|
||||
|
||||
// Initialize headers if not already set
|
||||
if (!providers.value[providerId]?.headers) {
|
||||
providers.value[providerId].headers = {}
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
...(providerMetadata.value?.defaultOptions as any),
|
||||
}
|
||||
}
|
||||
|
||||
const headers = reactive<{ key: string, value: string }[]>([
|
||||
{ key: '', value: '' },
|
||||
])
|
||||
|
||||
function addKeyValue(headers: { key: string, value: string }[], key: string, value: string) {
|
||||
if (!headers)
|
||||
return
|
||||
|
||||
headers.push({ key, value })
|
||||
}
|
||||
|
||||
function removeKeyValue(index: number, headers: { key: string, value: string }[]) {
|
||||
if (!headers)
|
||||
return
|
||||
|
||||
if (headers.length === 1) {
|
||||
headers[0].key = ''
|
||||
headers[0].value = ''
|
||||
}
|
||||
else {
|
||||
headers.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
watch(headers, (headers) => {
|
||||
if (headers.length > 0 && (headers[headers.length - 1].key !== '' || headers[headers.length - 1].value !== '')) {
|
||||
headers.push({ key: '', value: '' })
|
||||
}
|
||||
}, {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon="providerMetadata?.icon"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
|
||||
required
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<FieldKeyValues
|
||||
v-model="headers"
|
||||
:label="t('settings.pages.providers.common.section.advanced.fields.field.headers.label')"
|
||||
:description="t('settings.pages.providers.common.section.advanced.fields.field.headers.description')"
|
||||
:key-placeholder="t('settings.pages.providers.common.section.advanced.fields.field.headers.key.placeholder')"
|
||||
:value-placeholder="t('settings.pages.providers.common.section.advanced.fields.field.headers.value.placeholder')"
|
||||
@add="(key, value) => addKeyValue(headers, key, value)"
|
||||
@remove="(index) => removeKeyValue(index, headers)"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'openai'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
// Use computed properties for settings
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || 'https://api.openai.com/v1/',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize provider if it doesn't exist
|
||||
if (!providers.value[providerId]) {
|
||||
providers.value[providerId] = {
|
||||
baseUrl: 'https://api.openai.com/v1/',
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || 'https://api.openai.com/v1/'
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || 'https://api.openai.com/v1/',
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
baseUrl: 'https://api.openai.com/v1/',
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName || 'OpenAI'"
|
||||
:provider-icon="providerMetadata?.icon"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName || 'OpenAI'"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.openai.com/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'openrouter-ai'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
// Use computed properties for settings
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || ''
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || providerMetadata.value?.defaultOptions?.baseUrl || '',
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
...(providerMetadata.value?.defaultOptions as any),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon="providerMetadata?.icon"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-or-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'together-ai'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
// Use computed properties for settings
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize provider if it doesn't exist
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || ''
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || '',
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
...(providerMetadata.value?.defaultOptions as any),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="togetherapi-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.together.xyz/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'xai'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
// Use computed properties for settings
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize provider if it doesn't exist
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || ''
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || '',
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
...(providerMetadata.value?.defaultOptions as any),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon="providerMetadata?.icon"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="xai-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.x.ai/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import { Collapsable } from '@proj-airi/stage-ui/components'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useShortcutsStore } from '../../../stores/shortcuts'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const shortcutsStore = useShortcutsStore()
|
||||
const { shortcuts } = storeToRefs(shortcutsStore)
|
||||
|
||||
const recordingFor = ref<string | null>(null)
|
||||
const recordingKeys = ref<{
|
||||
modifier: string[]
|
||||
key: string
|
||||
}>({
|
||||
modifier: [],
|
||||
key: '',
|
||||
})
|
||||
|
||||
function startRecording(shortcutType: string) {
|
||||
recordingFor.value = shortcutType
|
||||
}
|
||||
|
||||
function isModifierKey(key: string) {
|
||||
return ['Shift', 'Control', 'Alt', 'Meta'].includes(key)
|
||||
}
|
||||
|
||||
// Handle key combinations
|
||||
useEventListener('keydown', (e) => {
|
||||
if (!recordingFor.value)
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
recordingFor.value = null
|
||||
return
|
||||
}
|
||||
|
||||
if (isModifierKey(e.key)) {
|
||||
if (recordingKeys.value.modifier.includes(e.key))
|
||||
return
|
||||
|
||||
recordingKeys.value.modifier.push(e.key)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (recordingKeys.value.modifier.length === 0)
|
||||
return
|
||||
|
||||
recordingKeys.value.key = e.key.toUpperCase()
|
||||
|
||||
const shortcut = shortcuts.value.find(s => s.type === recordingFor.value)
|
||||
if (shortcut)
|
||||
shortcut.shortcut = `${recordingKeys.value.modifier.join('+')}+${recordingKeys.value.key}`
|
||||
|
||||
recordingKeys.value = {
|
||||
modifier: [],
|
||||
key: '',
|
||||
}
|
||||
recordingFor.value = null
|
||||
}, { passive: false })
|
||||
|
||||
const pressKeysMessage = computed(() => {
|
||||
const i18nMessage = t('settings.pages.shortcuts.sections.section.window-controls.press-keys-hint.label')
|
||||
|
||||
if (recordingKeys.value.modifier.length === 0)
|
||||
return i18nMessage
|
||||
|
||||
return `${i18nMessage}: ${recordingKeys.value.modifier.join(' + ')} + ${recordingKeys.value.key}`
|
||||
})
|
||||
|
||||
function isConflict(shortcut: typeof shortcuts.value[0]) {
|
||||
return shortcuts.value.some(s => s.type !== shortcut.type && s.shortcut === shortcut.shortcut)
|
||||
}
|
||||
|
||||
function toReadable(shortcut: string) {
|
||||
return shortcut.replaceAll('Meta', '⌘').replaceAll('Control', 'Ctrl').replaceAll('Alt', '⌥').replaceAll('Shift', '⇧').replaceAll('+', ' + ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion
|
||||
flex="~ row" items-center gap-2
|
||||
:initial="{ opacity: 0, x: 10 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
:leave="{ opacity: 0, x: -10 }"
|
||||
:duration="250"
|
||||
>
|
||||
<button @click="router.back()">
|
||||
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
||||
</button>
|
||||
<h1 relative>
|
||||
<div absolute left-0 top-0 translate-y="[-80%]">
|
||||
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.shortcuts.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div flex-col>
|
||||
<Collapsable mt-4 w-full :default="true">
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex="~ row 1" items-center gap-1.5>
|
||||
<div
|
||||
i-solar:window-frame-bold-duotone class="provider-icon size-6"
|
||||
transition="filter duration-250 ease-in-out"
|
||||
/>
|
||||
<div>
|
||||
{{ t('settings.pages.shortcuts.sections.section.window-controls.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
||||
<div i-solar:alt-arrow-down-bold-duotone />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<div p-4 flex="~ col gap-4">
|
||||
<div
|
||||
v-for="shortcut in shortcuts"
|
||||
:key="shortcut.name"
|
||||
flex="~ row"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250" cursor-pointer
|
||||
items-center justify-between gap-4 rounded-lg px-4 py-3
|
||||
>
|
||||
<div text-base font-medium>
|
||||
{{ t(shortcut.name) }}
|
||||
</div>
|
||||
<div class="shortcut-item" flex="~ row" gap-2 text-sm text="neutral-500 dark:neutral-400">
|
||||
<div v-if="recordingFor === shortcut.type" class="animate-flash animate-duration-2s animate-count-infinite">
|
||||
{{ toReadable(pressKeysMessage) }}
|
||||
</div>
|
||||
<div v-else flex items-center gap-2 @click="startRecording(shortcut.type)">
|
||||
<div>{{ toReadable(shortcut.shortcut) }}</div>
|
||||
<div v-if="isConflict(shortcut)" i-solar:danger-circle-bold-duotone text-red-500 />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
</div>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:keyboard />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,423 @@
|
||||
<script setup lang="ts">
|
||||
import { Collapsable } from '@proj-airi/stage-ui/components'
|
||||
import { DEFAULT_THEME_COLORS_HUE, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { converter } from 'culori'
|
||||
import { TooltipArrow, TooltipContent, TooltipPortal, TooltipProvider, TooltipRoot, TooltipTrigger } from 'radix-vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const settings = useSettings()
|
||||
|
||||
interface ColorPreset {
|
||||
name: string
|
||||
description: string
|
||||
colors: Array<{
|
||||
hex: string
|
||||
name: string
|
||||
}>
|
||||
}
|
||||
|
||||
const colorPresets = computed<ColorPreset[]>(() => [
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.morandi.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.morandi.description'),
|
||||
colors: [
|
||||
{ hex: '#A5978B', name: 'Taupe' },
|
||||
{ hex: '#D8CAAF', name: 'Beige' },
|
||||
{ hex: '#B8B4A7', name: 'Ash Grey' },
|
||||
{ hex: '#C4BCB1', name: 'Light Taupe' },
|
||||
{ hex: '#E5DED8', name: 'Ivory' },
|
||||
{ hex: '#9A8F7D', name: 'Olive Grey' },
|
||||
{ hex: '#BEB5A7', name: 'Sand' },
|
||||
{ hex: '#C9C0B6', name: 'Warm Grey' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.monet.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.monet.description'),
|
||||
colors: [
|
||||
{ hex: '#7A9EAF', name: 'Sky Blue' },
|
||||
{ hex: '#B8C7CC', name: 'Mist' },
|
||||
{ hex: '#D4B79C', name: 'Sand' },
|
||||
{ hex: '#8B9D77', name: 'Moss Green' },
|
||||
{ hex: '#C7D5CB', name: 'Water Lily' },
|
||||
{ hex: '#E6D0B1', name: 'Wheat' },
|
||||
{ hex: '#94A7B1', name: 'Slate Blue' },
|
||||
{ hex: '#B4C8C3', name: 'Sage' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.japanese.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.japanese.description'),
|
||||
colors: [
|
||||
{ hex: '#D9B48F', name: 'Tan' },
|
||||
{ hex: '#B5917A', name: 'Warm Taupe' },
|
||||
{ hex: '#8C7A6B', name: 'Umber' },
|
||||
{ hex: '#A17F5F', name: 'Coffee' },
|
||||
{ hex: '#B98C46', name: 'Bronze' },
|
||||
{ hex: '#C7A252', name: 'Gold' },
|
||||
{ hex: '#DAB300', name: 'Mustard' },
|
||||
{ hex: '#D19826', name: 'Amber' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.nordic.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.nordic.description'),
|
||||
colors: [
|
||||
{ hex: '#9BA7B0', name: 'Nordic Blue' },
|
||||
{ hex: '#C1CBD4', name: 'Ice' },
|
||||
{ hex: '#A5ADB6', name: 'Fjord' },
|
||||
{ hex: '#8B959E', name: 'Steel' },
|
||||
{ hex: '#D4DCE4', name: 'Glacier' },
|
||||
{ hex: '#7F8A94', name: 'Slate' },
|
||||
{ hex: '#B3BCC6', name: 'Cloud' },
|
||||
{ hex: '#98A4AE', name: 'Stone' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.chinese.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.chinese.description'),
|
||||
colors: [
|
||||
{ hex: '#E4C6D0', name: '霞光红 (Rosy Dawn)' },
|
||||
{ hex: '#A61B29', name: '枣红 (Chinese Red)' },
|
||||
{ hex: '#5D513C', name: '黄栌 (Smoky Brown)' },
|
||||
{ hex: '#789262', name: '竹青 (Bamboo Green)' },
|
||||
{ hex: '#1C0D1A', name: '乌梅紫 (Dark Purple)' },
|
||||
{ hex: '#F7C242', name: '缃色 (Golden Yellow)' },
|
||||
{ hex: '#62A9DD', name: '青冥 (Azure Blue)' },
|
||||
{ hex: '#8C4B3C', name: '赭石 (Ochre)' },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
function resetToDefault() {
|
||||
settings.themeColorsHue = DEFAULT_THEME_COLORS_HUE
|
||||
settings.themeColorsHueDynamic = false
|
||||
}
|
||||
|
||||
function applyPrimaryColorFromHex(color: string) {
|
||||
// Convert hex color to OKLCH using culori's converter
|
||||
const oklch = converter('oklch')(color)
|
||||
|
||||
if (!oklch)
|
||||
return
|
||||
|
||||
// Extract hue from OKLCH color
|
||||
const { h } = oklch
|
||||
|
||||
if (!h)
|
||||
return
|
||||
|
||||
// Update theme settings
|
||||
settings.themeColorsHue = h
|
||||
settings.themeColorsHueDynamic = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a color is currently selected based on its hue value
|
||||
* @param hexColor Hex color code to check
|
||||
* @returns True if the color's hue matches the current theme hue
|
||||
*/
|
||||
function isColorSelected(hexColor: string): boolean {
|
||||
// If dynamic coloring is enabled, no preset color is manually selected
|
||||
if (settings.themeColorsHueDynamic)
|
||||
return false
|
||||
|
||||
// Convert hex color to OKLCH
|
||||
const oklch = converter('oklch')(hexColor)
|
||||
if (!oklch || !oklch.h)
|
||||
return false
|
||||
|
||||
// Compare hue values with a small tolerance for floating point comparison
|
||||
const hueDifference = Math.abs(oklch.h - settings.themeColorsHue)
|
||||
return hueDifference < 0.01 || hueDifference > 359.99
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion
|
||||
flex="~ row" items-center gap-2
|
||||
:initial="{ opacity: 0, x: 10 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
:leave="{ opacity: 0, x: -10 }"
|
||||
:duration="250"
|
||||
>
|
||||
<button @click="router.back()">
|
||||
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
||||
</button>
|
||||
<h1 relative>
|
||||
<div absolute left-0 top-0 translate-y="[-80%]">
|
||||
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.themes.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div flex-col>
|
||||
<Collapsable mt-4 w-full :default="true">
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex="~ row 1" items-center gap-1.5>
|
||||
<div
|
||||
i-solar:pallete-2-bold-duotone class="provider-icon size-6"
|
||||
transition="filter duration-250 ease-in-out"
|
||||
/>
|
||||
<div>
|
||||
{{ t('settings.pages.themes.sections.section.custom-color.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
||||
<div i-solar:alt-arrow-down-bold-duotone />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<div p-4>
|
||||
<div class="mb-2 text-sm font-medium">
|
||||
{{ t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.label') }}
|
||||
</div>
|
||||
|
||||
<div flex="~ col gap-4">
|
||||
<input
|
||||
v-model="settings.themeColorsHue"
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
step="0.01"
|
||||
class="theme-hue-slider h-10 w-full"
|
||||
:disabled="settings.themeColorsHueDynamic"
|
||||
:class="{ 'opacity-25 cursor-not-allowed': settings.themeColorsHueDynamic }"
|
||||
>
|
||||
|
||||
<div h-10 w-full flex overflow-hidden rounded-lg>
|
||||
<div bg="primary-50" class="primary-color-bar" text-black>
|
||||
50
|
||||
</div>
|
||||
<div bg="primary-100" class="primary-color-bar" text-black>
|
||||
100
|
||||
</div>
|
||||
<div bg="primary-200" class="primary-color-bar" text-black>
|
||||
200
|
||||
</div>
|
||||
<div bg="primary-300" class="primary-color-bar" text-black>
|
||||
300
|
||||
</div>
|
||||
<div bg="primary-400" class="primary-color-bar" text-black>
|
||||
400
|
||||
</div>
|
||||
<div bg="primary-500" class="primary-color-bar" text-black>
|
||||
500
|
||||
</div>
|
||||
<div bg="primary-600" class="primary-color-bar" text-white>
|
||||
600
|
||||
</div>
|
||||
<div bg="primary-700" class="primary-color-bar" text-white>
|
||||
700
|
||||
</div>
|
||||
<div bg="primary-800" class="primary-color-bar" text-white>
|
||||
800
|
||||
</div>
|
||||
<div bg="primary-900" class="primary-color-bar" text-white>
|
||||
900
|
||||
</div>
|
||||
<div bg="primary-950" class="primary-color-bar" text-white>
|
||||
950
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div h-10 w-full flex overflow-hidden rounded-lg class="transparency-grid">
|
||||
<div bg="primary-500/5" class="primary-color-bar" text-black>
|
||||
500/5
|
||||
</div>
|
||||
<div bg="primary-500/10" class="primary-color-bar" text-black>
|
||||
500/10
|
||||
</div>
|
||||
<div bg="primary-500/20" class="primary-color-bar" text-black>
|
||||
500/20
|
||||
</div>
|
||||
<div bg="primary-500/30" class="primary-color-bar" text-black>
|
||||
500/30
|
||||
</div>
|
||||
<div bg="primary-500/40" class="primary-color-bar" text-black>
|
||||
500/40
|
||||
</div>
|
||||
<div bg="primary-500/50" class="primary-color-bar" text-black>
|
||||
500/50
|
||||
</div>
|
||||
<div bg="primary-500/60" class="primary-color-bar" text-black>
|
||||
500/60
|
||||
</div>
|
||||
<div bg="primary-500/70" class="primary-color-bar" text-black>
|
||||
500/70
|
||||
</div>
|
||||
<div bg="primary-500/80" class="primary-color-bar" text-black>
|
||||
500/80
|
||||
</div>
|
||||
<div bg="primary-500/90" class="primary-color-bar" text-black>
|
||||
500/90
|
||||
</div>
|
||||
<div bg="primary-500" class="primary-color-bar" text-black>
|
||||
500
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div mt-4 class="flex items-center justify-end gap-4">
|
||||
<label class="relative inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="settings.themeColorsHueDynamic"
|
||||
type="checkbox"
|
||||
class="peer sr-only"
|
||||
>
|
||||
<div
|
||||
class="peer-checked:bg-primary-500 h-6 w-11 rounded-full bg-neutral-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white dark:bg-neutral-600 after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white"
|
||||
/>
|
||||
<span class="ml-2 text-sm font-medium">{{ t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.rgb-on.title') }}</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
class="rounded-md bg-neutral-100 px-3 py-1.5 text-sm transition-colors dark:bg-neutral-800 hover:bg-neutral-200 dark:hover:bg-neutral-700"
|
||||
@click="resetToDefault"
|
||||
>
|
||||
{{ t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.reset.label') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
|
||||
<Collapsable mt-4 w-full :default="true">
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex="~ row 1" items-center gap-1.5>
|
||||
<div
|
||||
i-solar:magic-stick-2-bold-duotone class="provider-icon size-6"
|
||||
transition="filter duration-250 ease-in-out"
|
||||
/>
|
||||
<div>
|
||||
{{ t('settings.pages.themes.sections.section.theme-presets.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
||||
<div i-solar:alt-arrow-down-bold-duotone />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div p-4 flex="~ col gap-4">
|
||||
<div
|
||||
v-for="preset in colorPresets"
|
||||
:key="preset.name"
|
||||
flex="~ row"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
cursor-pointer items-center justify-between gap-4 rounded-lg px-4 py-3
|
||||
>
|
||||
<div>
|
||||
<div text-base font-medium>
|
||||
{{ preset.name }}
|
||||
</div>
|
||||
<div text="sm neutral-500">
|
||||
{{ preset.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div flex="~ row" gap-2>
|
||||
<TooltipProvider v-for="color in preset.colors" :key="color.hex">
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
:style="{ backgroundColor: color.hex }"
|
||||
class="size-6 cursor-pointer rounded-full transition-all duration-250 ease-in-out" :class="[
|
||||
isColorSelected(color.hex)
|
||||
? 'scale-150 z-10 mx-1'
|
||||
: 'hover:scale-110',
|
||||
]"
|
||||
@click="applyPrimaryColorFromHex(color.hex)"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent class="rounded-lg bg-white px-3 py-1.5 text-sm shadow-md dark:bg-neutral-800">
|
||||
{{ color.name }}
|
||||
<TooltipArrow class="fill-white dark:fill-neutral-800" />
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
</div>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:paintbrush />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.primary-color-bar {
|
||||
--at-apply: w-full h-full flex-1 flex items-center justify-center;
|
||||
}
|
||||
|
||||
.theme-hue-slider {
|
||||
--at-apply: rounded-lg appearance-none;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
oklch(85% 0.2 0),
|
||||
oklch(85% 0.2 60),
|
||||
oklch(85% 0.2 120),
|
||||
oklch(85% 0.2 180),
|
||||
oklch(85% 0.2 240),
|
||||
oklch(85% 0.2 300),
|
||||
oklch(85% 0.2 360)
|
||||
);
|
||||
}
|
||||
|
||||
.theme-hue-slider::-webkit-slider-thumb {
|
||||
--at-apply: w-2 h-12 appearance-none rounded-md bg-neutral-500/80 dark: bg-neutral-400/80 cursor-pointer shadow-md
|
||||
border-2 border-white hover: bg-neutral-500 dark: hover: bg-neutral-400 transition-colors duration-200;
|
||||
}
|
||||
|
||||
.theme-hue-slider::-moz-range-thumb {
|
||||
--at-apply: w-2 h-12 bg-neutral-500/80 rounded-md dark: bg-neutral-400/80 cursor-pointer shadow-md border-2
|
||||
border-white border-box hover: bg-neutral-500 dark: hover: bg-neutral-400 transition-colors duration-200;
|
||||
}
|
||||
|
||||
.transparency-grid {
|
||||
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #ccc 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 10px,
|
||||
10px -10px,
|
||||
-10px 0px;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -0,0 +1,6 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
|
||||
const component: DefineComponent<object, object, any>
|
||||
export default component
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useShortcutsStore = defineStore('shortcuts', () => {
|
||||
const shortcuts = ref([
|
||||
{
|
||||
name: 'settings.pages.shortcuts.sections.section.window-controls.fields.field.toggle-move.label',
|
||||
shortcut: useLocalStorage('shortcuts/window/move', 'Ctrl+M'),
|
||||
group: 'window',
|
||||
type: 'move',
|
||||
},
|
||||
{
|
||||
name: 'settings.pages.shortcuts.sections.section.window-controls.fields.field.toggle-resize.label',
|
||||
shortcut: useLocalStorage('shortcuts/window/resize', 'Ctrl+R'),
|
||||
group: 'window',
|
||||
type: 'resize',
|
||||
},
|
||||
{
|
||||
name: 'settings.pages.shortcuts.sections.section.window-controls.fields.field.toggle-ignore-mouse-event.label',
|
||||
shortcut: useLocalStorage('shortcuts/window/debug', 'Ctrl+I'),
|
||||
group: 'window',
|
||||
type: 'debug',
|
||||
},
|
||||
])
|
||||
|
||||
return {
|
||||
shortcuts,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { WindowControlMode } from '../types/window-controls'
|
||||
|
||||
export const useWindowControlStore = defineStore('windowControl', () => {
|
||||
const controlMode = ref<WindowControlMode>(WindowControlMode.DEFAULT)
|
||||
const isControlActive = ref(false)
|
||||
const size = useLocalStorage('window/control/size', { width: 300 * 1.5, height: 400 * 1.5 })
|
||||
const position = useLocalStorage('window/control/position', { x: 0, y: 0 })
|
||||
|
||||
function setMode(mode: WindowControlMode) {
|
||||
controlMode.value = mode
|
||||
|
||||
if (mode === WindowControlMode.RESIZE) {
|
||||
// window.electron.ipcRenderer.send('start-resize-window')
|
||||
// return
|
||||
}
|
||||
|
||||
// window.electron.ipcRenderer.invoke('stop-move-window').then((p: [number, number]) => {
|
||||
// position.value = { x: p[0], y: p[1] }
|
||||
// })
|
||||
// window.electron.ipcRenderer.send('stop-resize-window')
|
||||
// TODO: save position and size
|
||||
}
|
||||
|
||||
function toggleControl() {
|
||||
isControlActive.value = !isControlActive.value
|
||||
if (!isControlActive.value) {
|
||||
controlMode.value = WindowControlMode.DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
controlMode,
|
||||
isControlActive,
|
||||
size,
|
||||
position,
|
||||
setMode,
|
||||
toggleControl,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
@import './themes.css';
|
||||
@import './transitions.css';
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
html {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.dark #app {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: rgb(13, 148, 136);
|
||||
opacity: 0.75;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
:root {
|
||||
--airi-theme-primary-50: #fff0f2;
|
||||
--airi-theme-primary-100: #ffe3e6;
|
||||
--airi-theme-primary-200: #ffcad4;
|
||||
--airi-theme-primary-300: #ff9fb0;
|
||||
--airi-theme-primary-400: #ff6988;
|
||||
--airi-theme-primary-500: #fe456e;
|
||||
--airi-theme-primary-600: #ec124d;
|
||||
--airi-theme-primary-700: #c70941;
|
||||
--airi-theme-primary-800: #a70a3e;
|
||||
--airi-theme-primary-900: #8e0d3b;
|
||||
--airi-theme-primary-950: #50011b;
|
||||
|
||||
--theme-colors-hue: 354.31;
|
||||
--theme-colors-chroma: calc(0.18 + (cos(var(--theme-colors-hue) * 3.14159265 / 180) * 0.04));
|
||||
--theme-colors-chroma-50: calc(var(--theme-colors-chroma) * 0.3);
|
||||
--theme-colors-chroma-100: calc(var(--theme-colors-chroma) * 0.5);
|
||||
--theme-colors-chroma-200: calc(var(--theme-colors-chroma) * 0.6);
|
||||
--theme-colors-chroma-300: calc(var(--theme-colors-chroma) * 0.75);
|
||||
--theme-colors-chroma-400: var(--theme-colors-chroma);
|
||||
--theme-colors-chroma-600: calc(var(--theme-colors-chroma) * 1.15);
|
||||
--theme-colors-chroma-700: calc(var(--theme-colors-chroma) * 1.1);
|
||||
--theme-colors-chroma-800: calc(var(--theme-colors-chroma) * 0.85);
|
||||
--theme-colors-chroma-900: calc(var(--theme-colors-chroma) * 0.7);
|
||||
--theme-colors-chroma-950: calc(var(--theme-colors-chroma) * 0.5);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.slide-away-enter-active,
|
||||
.slide-away-leave-active {
|
||||
transition:
|
||||
transform 0.3s ease-in-out,
|
||||
opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-away-enter,
|
||||
.slide-away-leave-to {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-away-enter-from,
|
||||
.slide-away-leave {
|
||||
transform: translateY(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum WindowControlMode {
|
||||
DEFAULT = 'default',
|
||||
MOVE = 'move',
|
||||
RESIZE = 'resize',
|
||||
DEBUG = 'debug',
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
declare module '*.js?url' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module '*.wasm?url' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module '*.cjs?url' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ESNext"
|
||||
],
|
||||
"paths": {
|
||||
"@proj-airi/stage-ui/*": [
|
||||
"../../packages/stage-ui/src/*"
|
||||
],
|
||||
"@proj-airi/ui-transitions": [
|
||||
"../../packages/ui-transitions/src/index.ts"
|
||||
]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": [
|
||||
"vitest",
|
||||
"vite/client",
|
||||
"vite-plugin-vue-layouts/client",
|
||||
"vite-plugin-pwa/client",
|
||||
"unplugin-vue-macros/macros-global",
|
||||
"unplugin-vue-router/client"
|
||||
],
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"plugins": [
|
||||
"@vue-macros/volar/define-models",
|
||||
"@vue-macros/volar/define-slots"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { createExternalPackageIconLoader } from '@iconify/utils/lib/loader/external-pkg'
|
||||
import { colorToString } from '@unocss/preset-mini/utils'
|
||||
import { createLocalFontProcessor } from '@unocss/preset-web-fonts/local'
|
||||
import {
|
||||
defineConfig,
|
||||
presetAttributify,
|
||||
presetIcons,
|
||||
presetTypography,
|
||||
presetWebFonts,
|
||||
presetWind3,
|
||||
transformerDirectives,
|
||||
transformerVariantGroup,
|
||||
} from 'unocss'
|
||||
import { presetScrollbar } from 'unocss-preset-scrollbar'
|
||||
import { parseColor } from 'unocss/preset-mini'
|
||||
|
||||
function createColorSchemeConfig(hueOffset = 0) {
|
||||
return {
|
||||
DEFAULT: `oklch(62% var(--theme-colors-chroma) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
50: `color-mix(in srgb, oklch(95% var(--theme-colors-chroma-50) calc(var(--theme-colors-hue) + ${hueOffset})) 30%, oklch(100% 0 360))`,
|
||||
100: `color-mix(in srgb, oklch(95% var(--theme-colors-chroma-100) calc(var(--theme-colors-hue) + ${hueOffset})) 80%, oklch(100% 0 360))`,
|
||||
200: `oklch(90% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
300: `oklch(85% var(--theme-colors-chroma-300) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
400: `oklch(74% var(--theme-colors-chroma-400) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
500: `oklch(62% var(--theme-colors-chroma) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
600: `oklch(54% var(--theme-colors-chroma-600) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
700: `oklch(49% var(--theme-colors-chroma-700) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
800: `oklch(42% var(--theme-colors-chroma-800) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
900: `oklch(37% var(--theme-colors-chroma-900) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
950: `oklch(29% var(--theme-colors-chroma-950) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetWind3(),
|
||||
presetAttributify(),
|
||||
presetTypography(),
|
||||
presetWebFonts({
|
||||
fonts: {
|
||||
sans: 'DM Sans',
|
||||
serif: 'DM Serif Display',
|
||||
mono: 'DM Mono',
|
||||
cute: 'Kiwi Maru',
|
||||
cuteen: 'Sniglet',
|
||||
},
|
||||
// Web fonts preset
|
||||
// https://unocss.dev/presets/web-fonts#serve-fonts-locally
|
||||
processors: createLocalFontProcessor(),
|
||||
}),
|
||||
presetIcons({
|
||||
scale: 1.2,
|
||||
collections: {
|
||||
...createExternalPackageIconLoader('@proj-airi/lobe-icons'),
|
||||
},
|
||||
}),
|
||||
presetScrollbar(),
|
||||
],
|
||||
transformers: [
|
||||
transformerDirectives({
|
||||
applyVariable: ['--at-apply'],
|
||||
}),
|
||||
transformerVariantGroup(),
|
||||
],
|
||||
safelist: 'prose prose-sm m-auto text-left'.split(' '),
|
||||
// hyoban/unocss-preset-shadcn: Use shadcn ui with UnoCSS
|
||||
// https://github.com/hyoban/unocss-preset-shadcn
|
||||
//
|
||||
// Thanks to
|
||||
// https://github.com/unovue/shadcn-vue/issues/34#issuecomment-2467318118
|
||||
// https://github.com/hyoban-template/shadcn-vue-unocss-starter
|
||||
//
|
||||
// By default, `.ts` and `.js` files are NOT extracted.
|
||||
// If you want to extract them, use the following configuration.
|
||||
// It's necessary to add the following configuration if you use shadcn-vue or shadcn-svelte.
|
||||
content: {
|
||||
pipeline: {
|
||||
include: [
|
||||
// the default
|
||||
/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
|
||||
// include js/ts files
|
||||
'(components|src)/**/*.{js,ts}',
|
||||
],
|
||||
},
|
||||
},
|
||||
rules: [
|
||||
[/^mask-\[(.*)\]$/, ([, suffix]) => ({ '-webkit-mask-image': suffix.replace(/_/g, ' ') })],
|
||||
[/^bg-dotted-\[(.*)\]$/, ([, color], { theme }) => {
|
||||
const parsedColor = parseColor(color, theme)
|
||||
// Util usage: https://github.com/unocss/unocss/blob/f57ef6ae50006a92f444738e50f3601c0d1121f2/packages-presets/preset-mini/src/_utils/utilities.ts#L186
|
||||
return {
|
||||
'background-image': `radial-gradient(circle at 1px 1px, ${colorToString(parsedColor?.cssColor ?? parsedColor?.color ?? color, 'var(--un-background-opacity)')} 1px, transparent 0)`,
|
||||
'--un-background-opacity': parsedColor?.cssColor?.alpha ?? parsedColor?.alpha ?? 1,
|
||||
}
|
||||
}],
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
primary: createColorSchemeConfig(),
|
||||
complementary: createColorSchemeConfig(180),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { join, resolve } from 'node:path'
|
||||
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
|
||||
import { Download } from '@proj-airi/unplugin-fetch'
|
||||
import { DownloadLive2DSDK } from '@proj-airi/unplugin-live2d-sdk'
|
||||
import { templateCompilerOptions } from '@tresjs/core'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import UnoCss from 'unocss/vite'
|
||||
import VueMacros from 'unplugin-vue-macros/vite'
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import Layouts from 'vite-plugin-vue-layouts'
|
||||
|
||||
export default defineConfig({
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
'@proj-airi/stage-ui/*',
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@proj-airi/stage-ui': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'dist')),
|
||||
'@proj-airi/stage-ui/stores': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'dist', 'stores')),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
VueMacros({
|
||||
plugins: {
|
||||
vue: Vue({
|
||||
include: [/\.vue$/, /\.md$/],
|
||||
...templateCompilerOptions,
|
||||
}),
|
||||
vueJsx: false,
|
||||
},
|
||||
}),
|
||||
|
||||
VueRouter({
|
||||
dts: resolve(import.meta.dirname, 'src/typed-router.d.ts'),
|
||||
routesFolder: 'src/pages',
|
||||
}),
|
||||
|
||||
// TODO: Devtools
|
||||
|
||||
// https://github.com/JohnCampionJr/vite-plugin-vue-layouts
|
||||
Layouts(),
|
||||
|
||||
UnoCss(),
|
||||
|
||||
// https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n
|
||||
VueI18n({
|
||||
runtimeOnly: true,
|
||||
compositionOnly: true,
|
||||
fullInstall: true,
|
||||
include: [resolve(import.meta.dirname, 'locales/**')],
|
||||
}),
|
||||
|
||||
DownloadLive2DSDK(),
|
||||
Download('https://dist.ayaka.moe/live2d-models/hiyori_free_zh.zip', 'hiyori_free_zh.zip', 'assets/live2d/models'),
|
||||
Download('https://dist.ayaka.moe/live2d-models/hiyori_pro_zh.zip', 'hiyori_pro_zh.zip', 'assets/live2d/models'),
|
||||
],
|
||||
})
|
||||
@@ -36,3 +36,14 @@ $ pnpm build:mac
|
||||
# For Linux
|
||||
$ pnpm build:linux
|
||||
```
|
||||
|
||||
## TODO List
|
||||
|
||||
- [ ] Window control: Move, resize, hide, show
|
||||
- [ ] Motions follow the cursor
|
||||
- [x] System tray
|
||||
- [x] i18n
|
||||
- [ ] Multi-DPI
|
||||
- [ ] Multi monitor
|
||||
- [ ] Send message via macOS Spotlight
|
||||
- [ ] Steam
|
||||
|
||||
@@ -144,6 +144,7 @@ words:
|
||||
- supergroup
|
||||
- superjson
|
||||
- tamagotchi
|
||||
- tauri
|
||||
- taze
|
||||
- tolist
|
||||
- tresjs
|
||||
|
||||
@@ -418,7 +418,7 @@ importers:
|
||||
version: 2.3.0
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^6.0.5
|
||||
version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.22.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.2)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.22.0(jiti@2.4.2))(rollup@4.36.0)(typescript@5.8.2)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
'@proj-airi/drizzle-duckdb-wasm':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/drizzle-duckdb-wasm
|
||||
@@ -469,7 +469,7 @@ importers:
|
||||
version: 3.0.0-beta.5(typescript@5.8.2)(vue-tsc@3.0.0-alpha.2(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vueuse/motion':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.2))
|
||||
version: 3.0.3(magicast@0.3.5)(rollup@4.36.0)(vue@3.5.13(typescript@5.8.2))
|
||||
electron:
|
||||
specifier: ^34.3.3
|
||||
version: 34.3.3
|
||||
@@ -488,6 +488,334 @@ importers:
|
||||
unocss-preset-scrollbar:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(unocss@66.1.0-beta.6(postcss@8.5.3)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2)))
|
||||
unplugin-auto-import:
|
||||
specifier: ^19.1.1
|
||||
version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.36.0))(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.2)))
|
||||
unplugin-vue-components:
|
||||
specifier: ^28.4.1
|
||||
version: 28.4.1(@babel/parser@7.26.10)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.36.0))(vue@3.5.13(typescript@5.8.2))
|
||||
unplugin-vue-macros:
|
||||
specifier: ^2.14.5
|
||||
version: 2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.2)))(esbuild@0.19.12)(rollup@4.36.0)(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
unplugin-vue-markdown:
|
||||
specifier: ^28.3.1
|
||||
version: 28.3.1(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
|
||||
unplugin-vue-router:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
vite-bundle-visualizer:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(rollup@4.36.0)
|
||||
vite-plugin-pwa:
|
||||
specifier: ^0.21.1
|
||||
version: 0.21.1(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: ^7.7.2
|
||||
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.36.0))(rollup@4.36.0)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
vite-plugin-vue-layouts:
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
vue-tsc:
|
||||
specifier: ^3.0.0-alpha.2
|
||||
version: 3.0.0-alpha.2(typescript@5.8.2)
|
||||
|
||||
apps/stage-tamagotchi-tauri:
|
||||
dependencies:
|
||||
'@11labs/client':
|
||||
specifier: ^0.0.8
|
||||
version: 0.0.8
|
||||
'@formkit/auto-animate':
|
||||
specifier: ^0.8.2
|
||||
version: 0.8.2
|
||||
'@huggingface/transformers':
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0
|
||||
'@pixi/app':
|
||||
specifier: '6'
|
||||
version: 6.5.10(@pixi/core@6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/display@6.5.10(@pixi/constants@6.5.10)(@pixi/math@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/math@6.5.10)(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
|
||||
'@pixi/constants':
|
||||
specifier: '6'
|
||||
version: 6.5.10
|
||||
'@pixi/core':
|
||||
specifier: '6'
|
||||
version: 6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
|
||||
'@pixi/display':
|
||||
specifier: '6'
|
||||
version: 6.5.10(@pixi/constants@6.5.10)(@pixi/math@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
|
||||
'@pixi/extensions':
|
||||
specifier: '6'
|
||||
version: 6.5.10
|
||||
'@pixi/interaction':
|
||||
specifier: '6'
|
||||
version: 6.5.10(@pixi/core@6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/display@6.5.10(@pixi/constants@6.5.10)(@pixi/math@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/math@6.5.10)(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
|
||||
'@pixi/loaders':
|
||||
specifier: '6'
|
||||
version: 6.5.10(@pixi/constants@6.5.10)(@pixi/core@6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
|
||||
'@pixi/math':
|
||||
specifier: '6'
|
||||
version: 6.5.10
|
||||
'@pixi/runner':
|
||||
specifier: '6'
|
||||
version: 6.5.10
|
||||
'@pixi/settings':
|
||||
specifier: '6'
|
||||
version: 6.5.10(@pixi/constants@6.5.10)
|
||||
'@pixi/sprite':
|
||||
specifier: '6'
|
||||
version: 6.5.10(@pixi/constants@6.5.10)(@pixi/core@6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/display@6.5.10(@pixi/constants@6.5.10)(@pixi/math@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))))(@pixi/math@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
|
||||
'@pixi/ticker':
|
||||
specifier: '6'
|
||||
version: 6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))
|
||||
'@pixi/utils':
|
||||
specifier: '6'
|
||||
version: 6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))
|
||||
'@pixiv/three-vrm':
|
||||
specifier: ^3.3.6
|
||||
version: 3.3.6(three@0.174.0)
|
||||
'@pixiv/three-vrm-animation':
|
||||
specifier: ^3.3.6
|
||||
version: 3.3.6(three@0.174.0)
|
||||
'@pixiv/three-vrm-core':
|
||||
specifier: ^3.3.6
|
||||
version: 3.3.6(three@0.174.0)
|
||||
'@proj-airi/stage-ui':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/stage-ui
|
||||
'@ricky0123/vad-web':
|
||||
specifier: ^0.0.22
|
||||
version: 0.0.22
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
'@tauri-apps/plugin-os':
|
||||
specifier: '>=2'
|
||||
version: 2.2.1
|
||||
'@tresjs/cientos':
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0(@tresjs/core@4.3.3(three@0.174.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))(@types/three@0.174.0)(react@18.3.1)(three@0.174.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
|
||||
'@tresjs/core':
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3(three@0.174.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
|
||||
'@unhead/vue':
|
||||
specifier: ^2.0.0-rc.13
|
||||
version: 2.0.0-rc.13(vue@3.5.13(typescript@5.8.2))
|
||||
'@vueuse/core':
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0(vue@3.5.13(typescript@5.8.2))
|
||||
'@vueuse/head':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(vue@3.5.13(typescript@5.8.2))
|
||||
'@vueuse/shared':
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0(vue@3.5.13(typescript@5.8.2))
|
||||
'@xsai-ext/providers-cloud':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3
|
||||
'@xsai-ext/providers-local':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3
|
||||
'@xsai-ext/shared-providers':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3
|
||||
'@xsai/generate-speech':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3
|
||||
'@xsai/generate-text':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3
|
||||
'@xsai/model':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3
|
||||
'@xsai/shared-chat':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3
|
||||
'@xsai/stream-text':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3
|
||||
'@xsai/utils-chat':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3
|
||||
culori:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
defu:
|
||||
specifier: ^6.1.4
|
||||
version: 6.1.4
|
||||
drizzle-kit:
|
||||
specifier: ^0.30.5
|
||||
version: 0.30.5
|
||||
drizzle-orm:
|
||||
specifier: ^0.40.1
|
||||
version: 0.40.1(@types/pg@8.11.11)(gel@2.0.0)(pg@8.14.1)(postgres@3.4.5)
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
localforage:
|
||||
specifier: ^1.10.0
|
||||
version: 1.10.0
|
||||
nprogress:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
ofetch:
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.1
|
||||
onnxruntime-web:
|
||||
specifier: ^1.21.0
|
||||
version: 1.21.0
|
||||
pinia:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
|
||||
pixi-filters:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0(5af797a0b5bd919a06578c24a7479046)
|
||||
pixi-live2d-display:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(0736f5bd67d2e4de7e9fff36ad63fa2e)
|
||||
popmotion:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5
|
||||
radix-vue:
|
||||
specifier: ^1.9.17
|
||||
version: 1.9.17(vue@3.5.13(typescript@5.8.2))
|
||||
rehype-stringify:
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1
|
||||
remark-parse:
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
remark-rehype:
|
||||
specifier: ^11.1.1
|
||||
version: 11.1.1
|
||||
shiki:
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.1
|
||||
three:
|
||||
specifier: ^0.174.0
|
||||
version: 0.174.0
|
||||
unified:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5
|
||||
valibot:
|
||||
specifier: 1.0.0-beta.9
|
||||
version: 1.0.0-beta.9(typescript@5.8.2)
|
||||
vaul-vue:
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1(reka-ui@2.1.0(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
vue:
|
||||
specifier: ^3.5.13
|
||||
version: 3.5.13(typescript@5.8.2)
|
||||
vue-demi:
|
||||
specifier: ^0.14.10
|
||||
version: 0.14.10(vue@3.5.13(typescript@5.8.2))
|
||||
vue-i18n:
|
||||
specifier: ^11.1.2
|
||||
version: 11.1.2(vue@3.5.13(typescript@5.8.2))
|
||||
vue-router:
|
||||
specifier: ^4.5.0
|
||||
version: 4.5.0(vue@3.5.13(typescript@5.8.2))
|
||||
xsschema:
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.0.0-beta.9(typescript@5.8.2)))(zod-to-json-schema@3.24.4(zod@3.24.2))
|
||||
yauzl:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
zod:
|
||||
specifier: ^3.24.2
|
||||
version: 3.24.2
|
||||
devDependencies:
|
||||
'@iconify-json/carbon':
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8
|
||||
'@iconify-json/eos-icons':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
'@iconify-json/lucide':
|
||||
specifier: ^1.2.31
|
||||
version: 1.2.31
|
||||
'@iconify-json/mingcute':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
'@iconify-json/simple-icons':
|
||||
specifier: ^1.2.29
|
||||
version: 1.2.29
|
||||
'@iconify-json/solar':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
'@iconify-json/svg-spinners':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
'@iconify-json/vscode-icons':
|
||||
specifier: ^1.2.16
|
||||
version: 1.2.16
|
||||
'@iconify/utils':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^6.0.5
|
||||
version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.22.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.2)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
'@proj-airi/drizzle-duckdb-wasm':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/drizzle-duckdb-wasm
|
||||
'@proj-airi/elevenlabs':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/elevenlabs
|
||||
'@proj-airi/lobe-icons':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
'@proj-airi/provider-transformers':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/provider-transformers
|
||||
'@proj-airi/ui-transitions':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/ui-transitions
|
||||
'@proj-airi/unplugin-fetch':
|
||||
specifier: ^0.1.4
|
||||
version: 0.1.4(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)
|
||||
'@proj-airi/unplugin-live2d-sdk':
|
||||
specifier: ^0.1.2
|
||||
version: 0.1.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)
|
||||
'@shikijs/markdown-it':
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.1
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
'@types/culori':
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
'@types/markdown-it-link-attributes':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
'@types/nprogress':
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3
|
||||
'@types/three':
|
||||
specifier: ^0.174.0
|
||||
version: 0.174.0
|
||||
'@types/yauzl':
|
||||
specifier: ^2.10.3
|
||||
version: 2.10.3
|
||||
'@unocss/reset':
|
||||
specifier: ^66.1.0-beta.6
|
||||
version: 66.1.0-beta.6
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^5.2.3
|
||||
version: 5.2.3(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/volar':
|
||||
specifier: ^3.0.0-beta.5
|
||||
version: 3.0.0-beta.5(typescript@5.8.2)(vue-tsc@3.0.0-alpha.2(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vueuse/motion':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.2))
|
||||
less:
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
markdown-it-link-attributes:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
unocss-preset-scrollbar:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(unocss@66.1.0-beta.6(postcss@8.5.3)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2)))
|
||||
unplugin-auto-import:
|
||||
specifier: ^19.1.1
|
||||
version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.2)))
|
||||
@@ -496,7 +824,7 @@ importers:
|
||||
version: 28.4.1(@babel/parser@7.26.10)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(vue@3.5.13(typescript@5.8.2))
|
||||
unplugin-vue-macros:
|
||||
specifier: ^2.14.5
|
||||
version: 2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.2)))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.2)))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
unplugin-vue-markdown:
|
||||
specifier: ^28.3.1
|
||||
version: 28.3.1(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
|
||||
@@ -4509,6 +4837,77 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^2.7.0 || ^3.0.0
|
||||
|
||||
'@tauri-apps/api@2.4.0':
|
||||
resolution: {integrity: sha512-F1zXTsmwcCp+ocg6fbzD/YL0OHeSG1eynCag1UNlX2tD5+dlXy7eRbTu9cAcscPjcR7Nix7by2wiv/+VfWUieg==}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.3.1':
|
||||
resolution: {integrity: sha512-TOhSdsXYt+f+asRU+Dl+Wufglj/7+CX9h8RO4hl5k7D6lR4L8yTtdhpS7btaclOMmjYC4piNfJE70GoxhOoYWw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tauri-apps/cli-darwin-x64@2.3.1':
|
||||
resolution: {integrity: sha512-LDwGg3AuBQ3aCeMAFaFwt0MSGOVFoXuXEe0z4QxQ7jZE5tdAOhKABaq4i569V5lShCgQZ6nLD/tmA5+GipvHnA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.3.1':
|
||||
resolution: {integrity: sha512-hu3HpbbtJBvHXw5i54QHwLxOUoXWqhf7CL2YYSPOrWEEQo10NKddulP61L5gfr5z+bSSaitfLwqgTidgnaNJCA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-gnu@2.3.1':
|
||||
resolution: {integrity: sha512-mEGgwkiGSKYXWHhGodo7zU9PCd2I/d6KkR+Wp1nzK+DxsCrEK6yJ5XxYLSQSDcKkM4dCxpVEPUiVMbDhmn08jg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.3.1':
|
||||
resolution: {integrity: sha512-tqQkafikGfnc7ISnGjSYkbpnzJKEyO8XSa0YOXTAL3J8R5Pss5ZIZY7G8kq1mwQSR/dPVR1ZLTVXgZGuysjP8w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.3.1':
|
||||
resolution: {integrity: sha512-I3puDJ2wGEauXlXbzIHn2etz78TaWs1cpN6zre02maHr6ZR7nf7euTCOGPhhfoMG0opA5mT/eLuYpVw648/VAA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.3.1':
|
||||
resolution: {integrity: sha512-rbWiCOBuQN7tPySkUyBs914uUikE3mEUOqV/IFospvKESw4UC3G1DL5+ybfXH7Orb8/in3JpJuVzYQjo+OSbBA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.3.1':
|
||||
resolution: {integrity: sha512-PdTmUzSeTHjJuBpCV7L+V29fPhPtToU+NZU46slHKSA1aT38MiFDXBZ/6P5Zudrt9QPMfIubqnJKbK8Ivvv7Ww==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli-win32-ia32-msvc@2.3.1':
|
||||
resolution: {integrity: sha512-K/Xa97kspWT4UWj3t26lL2D3QsopTAxS7kWi5kObdqtAGn3qD52qBi24FH38TdvHYz4QlnLIb30TukviCgh4gw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli-win32-x64-msvc@2.3.1':
|
||||
resolution: {integrity: sha512-RgwzXbP8gAno3kQEsybMtgLp6D1Z1Nec2cftryYbPTJmoMJs6e4qgtxuTSbUz5SKnHe8rGgMiFSvEGoHvbG72Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli@2.3.1':
|
||||
resolution: {integrity: sha512-xewcw/ZsCqgilTy2h7+pp2Baxoy7zLR2wXOV7SZLzkb6SshHVbm1BFAjn8iFATURRW85KLzl6wSGJ2dQHjVHqw==}
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@tauri-apps/plugin-os@2.2.1':
|
||||
resolution: {integrity: sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==}
|
||||
|
||||
'@testing-library/dom@10.4.0':
|
||||
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -15268,6 +15667,55 @@ snapshots:
|
||||
'@tanstack/virtual-core': 3.13.2
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
|
||||
'@tauri-apps/api@2.4.0': {}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-darwin-x64@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-gnu@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-ia32-msvc@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-x64-msvc@2.3.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli@2.3.1':
|
||||
optionalDependencies:
|
||||
'@tauri-apps/cli-darwin-arm64': 2.3.1
|
||||
'@tauri-apps/cli-darwin-x64': 2.3.1
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf': 2.3.1
|
||||
'@tauri-apps/cli-linux-arm64-gnu': 2.3.1
|
||||
'@tauri-apps/cli-linux-arm64-musl': 2.3.1
|
||||
'@tauri-apps/cli-linux-x64-gnu': 2.3.1
|
||||
'@tauri-apps/cli-linux-x64-musl': 2.3.1
|
||||
'@tauri-apps/cli-win32-arm64-msvc': 2.3.1
|
||||
'@tauri-apps/cli-win32-ia32-msvc': 2.3.1
|
||||
'@tauri-apps/cli-win32-x64-msvc': 2.3.1
|
||||
|
||||
'@tauri-apps/plugin-os@2.2.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.4.0
|
||||
|
||||
'@testing-library/dom@10.4.0':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.26.2
|
||||
@@ -24041,9 +24489,16 @@ snapshots:
|
||||
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.36.0)
|
||||
'@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.8.2))
|
||||
|
||||
unplugin-combine@1.2.1(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)):
|
||||
unplugin-combine@1.2.1(esbuild@0.19.12)(rollup@4.36.0)(unplugin@1.16.1)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)):
|
||||
optionalDependencies:
|
||||
esbuild: 0.19.12
|
||||
rollup: 4.36.0
|
||||
unplugin: 1.16.1
|
||||
vite: 6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)
|
||||
|
||||
unplugin-combine@1.2.1(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)):
|
||||
optionalDependencies:
|
||||
esbuild: 0.25.0
|
||||
rollup: 2.79.1
|
||||
unplugin: 1.16.1
|
||||
vite: 6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)
|
||||
@@ -24102,7 +24557,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
unplugin-vue-macros@2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.2)))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)):
|
||||
unplugin-vue-macros@2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.2)))(esbuild@0.19.12)(rollup@4.36.0)(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 1.11.4(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/boolean-prop': 0.5.5(vue@3.5.13(typescript@5.8.2))
|
||||
@@ -24134,7 +24589,53 @@ snapshots:
|
||||
'@vue-macros/short-vmodel': 1.5.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/volar': 0.30.15(typescript@5.8.2)(vue-tsc@3.0.0-alpha.2(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
unplugin: 1.16.1
|
||||
unplugin-combine: 1.2.1(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
|
||||
unplugin-combine: 1.2.1(esbuild@0.19.12)(rollup@4.36.0)(unplugin@1.16.1)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
|
||||
unplugin-vue-define-options: 1.5.5(vue@3.5.13(typescript@5.8.2))
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
transitivePeerDependencies:
|
||||
- '@rspack/core'
|
||||
- '@vueuse/core'
|
||||
- esbuild
|
||||
- rolldown
|
||||
- rollup
|
||||
- typescript
|
||||
- vite
|
||||
- vue-tsc
|
||||
- webpack
|
||||
|
||||
unplugin-vue-macros@2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.2)))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 1.11.4(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/boolean-prop': 0.5.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/chain-call': 0.4.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/common': 1.16.1(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/config': 0.6.1(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/define-emit': 0.5.4(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/define-models': 1.3.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/define-prop': 0.6.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/define-props': 4.0.6(@vue-macros/reactivity-transform@1.1.6(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/define-props-refs': 1.3.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/define-render': 1.6.6(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/define-slots': 1.2.6(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/define-stylex': 0.2.3(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/devtools': 0.4.1(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
|
||||
'@vue-macros/export-expose': 0.3.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/export-props': 0.6.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/export-render': 0.3.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/hoist-static': 1.7.0(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/jsx-directive': 0.10.6(typescript@5.8.2)
|
||||
'@vue-macros/named-template': 0.5.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/reactivity-transform': 1.1.6(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/script-lang': 0.2.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/setup-block': 0.4.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/setup-component': 0.18.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/setup-sfc': 0.18.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/short-bind': 1.1.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/short-emits': 1.6.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/short-vmodel': 1.5.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/volar': 0.30.15(typescript@5.8.2)(vue-tsc@3.0.0-alpha.2(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
unplugin: 1.16.1
|
||||
unplugin-combine: 1.2.1(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
|
||||
unplugin-vue-define-options: 1.5.5(vue@3.5.13(typescript@5.8.2))
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
transitivePeerDependencies:
|
||||
|
||||