refactor(stage-tamagotchi): remove stage electron (#115)
* refactor(stage-tamagotchi): remove stage electron * fix(stage-tamagotchi): path
@@ -59,13 +59,13 @@ jobs:
|
||||
run: pnpm run packages:build
|
||||
|
||||
- name: Build Application
|
||||
run: cd apps/stage-tamagotchi-tauri && pnpm tauri build --target ${{ matrix.target }}
|
||||
run: cd apps/stage-tamagotchi && pnpm tauri build --target ${{ matrix.target }}
|
||||
|
||||
- name: Rename Artifacts
|
||||
run: node apps/stage-tamagotchi-tauri/scripts/rename-artifacts.js ${{ matrix.target }}
|
||||
run: node apps/stage-tamagotchi/scripts/rename-artifacts.js ${{ matrix.target }}
|
||||
|
||||
- name: Upload To GitHub Releases
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: apps/stage-tamagotchi-tauri/bundle/airi_*
|
||||
files: apps/stage-tamagotchi/bundle/airi_*
|
||||
append_body: true
|
||||
|
||||
@@ -61,13 +61,13 @@ jobs:
|
||||
run: pnpm run packages:build
|
||||
|
||||
- name: Build Application
|
||||
run: cd apps/stage-tamagotchi-tauri && pnpm tauri build --target ${{ matrix.target }}
|
||||
run: cd apps/stage-tamagotchi && pnpm tauri build --target ${{ matrix.target }}
|
||||
|
||||
- name: Rename Artifacts
|
||||
run: node apps/stage-tamagotchi-tauri/scripts/rename-artifacts.js ${{ matrix.target }}
|
||||
run: node apps/stage-tamagotchi/scripts/rename-artifacts.js ${{ matrix.target }}
|
||||
|
||||
- name: Upload To GitHub Releases
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: apps/stage-tamagotchi-tauri/bundle/airi_*
|
||||
files: apps/stage-tamagotchi/bundle/airi_*
|
||||
append_body: true
|
||||
|
||||
@@ -32,7 +32,6 @@ coverage/
|
||||
**/public/assets/vrm/models/*
|
||||
packages/stage-ui/public/assets/fonts/
|
||||
apps/stage-tamagotchi/public/assets/fonts/
|
||||
apps/stage-tamagotchi-tauri/public/assets/fonts/
|
||||
apps/stage-web/public/assets/fonts/
|
||||
|
||||
*.pcm
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": [
|
||||
"apps/stage-web/locales",
|
||||
"apps/stage-tamagotchi/src/renderer/locales",
|
||||
"apps/stage-tamagotchi/src/main/locales",
|
||||
"apps/stage-tamagotchi-tauri/locales"
|
||||
"apps/stage-tamagotchi/locales"
|
||||
],
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.enabledFrameworks": [
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# たまごっち アイリ
|
||||
|
||||
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
|
||||
@@ -1,12 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,126 +0,0 @@
|
||||
{
|
||||
"name": "@proj-airi/stage-tamagotchi-tauri",
|
||||
"type": "module",
|
||||
"version": "0.4.19",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11labs/client": "^0.1.0",
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@huggingface/transformers": "^3.4.2",
|
||||
"@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.1",
|
||||
"@tauri-apps/plugin-os": ">=2.2.1",
|
||||
"@tresjs/cientos": "^4.3.0",
|
||||
"@tresjs/core": "^4.3.3",
|
||||
"@unhead/vue": "^2.0.3",
|
||||
"@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.6",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"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.2",
|
||||
"shiki": "^3.2.1",
|
||||
"three": "^0.175.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.34",
|
||||
"@iconify-json/mingcute": "^1.2.3",
|
||||
"@iconify-json/simple-icons": "^1.2.30",
|
||||
"@iconify-json/solar": "^1.2.2",
|
||||
"@iconify-json/svg-spinners": "^1.2.2",
|
||||
"@iconify-json/vscode-icons": "^1.2.18",
|
||||
"@iconify/utils": "^2.3.0",
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.5",
|
||||
"@proj-airi/drizzle-duckdb-wasm": "workspace:^",
|
||||
"@proj-airi/lobe-icons": "^1.0.5",
|
||||
"@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.4.1",
|
||||
"@types/culori": "^2.1.1",
|
||||
"@types/markdown-it-link-attributes": "^3.0.5",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/three": "^0.175.0",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"@unocss/reset": "^66.1.0-beta.9",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue-macros/volar": "^3.0.0-beta.7",
|
||||
"@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.2",
|
||||
"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": "^1.0.0",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vue-tsc": "^3.0.0-alpha.2"
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
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}',
|
||||
'**/stage-ui/**/*.{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),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -41,9 +41,11 @@ $ pnpm build:linux
|
||||
|
||||
- [ ] Window control: Move, resize, hide, show
|
||||
- [ ] Motions follow the cursor
|
||||
- [x] System tray
|
||||
- [x] i18n
|
||||
- [ ] System tray
|
||||
- [ ] tauri i18n
|
||||
- [ ] Multi-DPI
|
||||
- [ ] CI/CD
|
||||
- [ ] Multi monitor
|
||||
- [ ] Send message via macOS Spotlight
|
||||
- [ ] Steam
|
||||
- [ ] CSP settings
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
appId: com.github.moeru-ai.airi-tamagotchi
|
||||
productName: airi
|
||||
directories:
|
||||
buildResources: build
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
win:
|
||||
executableName: tamagotchi
|
||||
nsis:
|
||||
artifactName: ${productName}-${os}-${arch}-v${version}.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
# - NSCameraUseContinuityCameraDeviceType: Application requests access to the device's camera.
|
||||
# - AVCaptureDeviceTypeContinuityCamera: Application requests access to the device's camera.
|
||||
notarize: false
|
||||
dmg:
|
||||
artifactName: ${productName}-${os}-${arch}-v${version}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
maintainer: github.com/moeru-ai Contributors
|
||||
category: Entertainment
|
||||
appImage:
|
||||
artifactName: ${productName}-${os}-${arch}-v${version}.${ext}
|
||||
npmRebuild: false
|
||||
# publish:
|
||||
# provider: generic
|
||||
# url: https://example.com/auto-updates
|
||||
@@ -1,13 +0,0 @@
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
|
||||
import rendererConfig from './renderer.vite.config'
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
},
|
||||
renderer: rendererConfig,
|
||||
})
|
||||
@@ -1,28 +1,19 @@
|
||||
{
|
||||
"name": "@proj-airi/stage-tamagotchi",
|
||||
"type": "module",
|
||||
"version": "0.4.19",
|
||||
"private": true,
|
||||
"description": "An Electron application with Vue and TypeScript",
|
||||
"description": "Airi tamagotchi app written in Tauri",
|
||||
"author": "LemonNekoGH",
|
||||
"homepage": "https://electron-vite.org",
|
||||
"main": "./out/main/index.js",
|
||||
"scripts": {
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
||||
"typecheck": "vue-tsc --noEmit --composite false",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev",
|
||||
"build": "pnpm run typecheck && electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:unpack": "pnpm run build && electron-builder --dir",
|
||||
"build:win": "pnpm run build && electron-builder --win",
|
||||
"build:mac": "pnpm run build && electron-builder --mac",
|
||||
"build:linux": "pnpm run build && electron-builder --linux"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:unpack": "tauri build --no-bundle"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11labs/client": "^0.1.0",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@huggingface/transformers": "^3.4.2",
|
||||
"@pixi/app": "6",
|
||||
@@ -43,6 +34,8 @@
|
||||
"@pixiv/three-vrm-core": "^3.3.6",
|
||||
"@proj-airi/stage-ui": "workspace:^",
|
||||
"@ricky0123/vad-web": "^0.0.22",
|
||||
"@tauri-apps/api": "^2.4.1",
|
||||
"@tauri-apps/plugin-os": ">=2.2.1",
|
||||
"@tresjs/cientos": "^4.3.0",
|
||||
"@tresjs/core": "^4.3.3",
|
||||
"@unhead/vue": "^2.0.3",
|
||||
@@ -89,7 +82,6 @@
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@iconify-json/carbon": "^1.2.8",
|
||||
"@iconify-json/eos-icons": "^1.2.2",
|
||||
"@iconify-json/lucide": "^1.2.34",
|
||||
@@ -107,6 +99,7 @@
|
||||
"@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.4.1",
|
||||
"@types/culori": "^2.1.1",
|
||||
"@types/markdown-it-link-attributes": "^3.0.5",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
@@ -116,9 +109,6 @@
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue-macros/volar": "^3.0.0-beta.7",
|
||||
"@vueuse/motion": "^3.0.3",
|
||||
"electron": "^34.4.1",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-vite": "^2.3.0",
|
||||
"less": "^4.2.2",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"unocss-preset-scrollbar": "^3.2.0",
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
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: {
|
||||
'@renderer': resolve(join('src', 'renderer', 'src')),
|
||||
'@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/renderer/src/pages',
|
||||
}),
|
||||
|
||||
// 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, 'src', 'renderer', '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'),
|
||||
],
|
||||
})
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@@ -1,210 +0,0 @@
|
||||
import { join } from 'node:path'
|
||||
import { env, platform } from 'node:process'
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
import { nativeImage, shell } from 'electron/common'
|
||||
import { app, BrowserWindow, dialog, ipcMain, Tray } from 'electron/main'
|
||||
|
||||
import trayIconMacos from '../../build/icon-tray-macos.png?asset'
|
||||
import icon from '../../build/icon.png?asset'
|
||||
import { createI18n } from './locales'
|
||||
import { createApplicationMenu, createTrayMenu } from './menu'
|
||||
|
||||
let mainWindow: BrowserWindow
|
||||
let tray: Tray
|
||||
|
||||
const i18n = createI18n()
|
||||
|
||||
function showQuitDialog() {
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: i18n.t('menu.quit'),
|
||||
message: i18n.t('quitDialog.message'),
|
||||
buttons: [i18n.t('quitDialog.buttons.quit'), i18n.t('quitDialog.buttons.cancel')],
|
||||
}).then((result) => {
|
||||
if (result.response === 0) {
|
||||
mainWindow.webContents.send('before-quit')
|
||||
setTimeout(() => {
|
||||
app.quit()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function rebuildTrayMenu() {
|
||||
if (mainWindow.isVisible()) {
|
||||
tray.setContextMenu(createTrayMenu(i18n, mainWindow.isVisible(), () => {
|
||||
mainWindow.webContents.send('before-hide')
|
||||
setTimeout(() => {
|
||||
mainWindow.hide()
|
||||
rebuildTrayMenu()
|
||||
}, 2000)
|
||||
}, createSettingsWindow, showQuitDialog))
|
||||
return
|
||||
}
|
||||
tray.setContextMenu(createTrayMenu(i18n, mainWindow.isVisible(), () => {
|
||||
mainWindow.show()
|
||||
mainWindow.webContents.send('after-show')
|
||||
rebuildTrayMenu()
|
||||
}, createSettingsWindow, showQuitDialog))
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 300 * 1.5,
|
||||
height: 400 * 1.5,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
resizable: false,
|
||||
hasShadow: false,
|
||||
alwaysOnTop: true,
|
||||
...(platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(import.meta.dirname, '..', 'preload', 'index.js'),
|
||||
sandbox: false,
|
||||
},
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
mainWindow.show()
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
|
||||
if (is.dev && env.ELECTRON_RENDERER_URL) {
|
||||
mainWindow.loadURL(env.ELECTRON_RENDERER_URL)
|
||||
}
|
||||
else {
|
||||
mainWindow.loadFile(join(import.meta.dirname, '..', '..', 'out', 'renderer', 'index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
let settingsWindow: BrowserWindow | null = null
|
||||
|
||||
function createSettingsWindow() {
|
||||
if (settingsWindow) {
|
||||
settingsWindow.show()
|
||||
return
|
||||
}
|
||||
|
||||
settingsWindow = new BrowserWindow({
|
||||
width: 300 * 2,
|
||||
height: 400 * 2,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
preload: join(import.meta.dirname, '..', 'preload', 'index.js'),
|
||||
sandbox: false,
|
||||
},
|
||||
})
|
||||
|
||||
settingsWindow.on('ready-to-show', () => {
|
||||
settingsWindow?.show()
|
||||
})
|
||||
|
||||
settingsWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
settingsWindow.on('closed', () => {
|
||||
settingsWindow = null
|
||||
})
|
||||
|
||||
settingsWindow.show()
|
||||
|
||||
if (is.dev && env.ELECTRON_RENDERER_URL) {
|
||||
settingsWindow.loadURL(join(env.ELECTRON_RENDERER_URL, '#/settings'))
|
||||
}
|
||||
else {
|
||||
settingsWindow.loadFile(join(import.meta.dirname, '..', '..', 'out', 'renderer', 'index.html'), {
|
||||
hash: '/settings',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
createApplicationMenu(i18n, showQuitDialog, createSettingsWindow)
|
||||
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('com.github.moeru-ai.airi-tamagotchi')
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
// IPC test
|
||||
ipcMain.on('quit', showQuitDialog)
|
||||
|
||||
ipcMain.on('open-settings', () => createSettingsWindow())
|
||||
|
||||
ipcMain.on('close-settings', () => {
|
||||
if (settingsWindow) {
|
||||
settingsWindow.close()
|
||||
}
|
||||
})
|
||||
|
||||
createWindow()
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
const trayIcon = platform === 'darwin'
|
||||
? nativeImage.createFromPath(trayIconMacos).resize({ width: 16, height: 16 })
|
||||
: nativeImage.createFromPath(icon).resize({ width: 16, height: 16 })
|
||||
tray = new Tray(trayIcon)
|
||||
tray.setToolTip('Airi')
|
||||
rebuildTrayMenu()
|
||||
|
||||
app.dock.hide()
|
||||
|
||||
ipcMain.on('locale-changed', (_, language: string) => {
|
||||
i18n.setLocale(language)
|
||||
rebuildTrayMenu()
|
||||
createApplicationMenu(i18n, showQuitDialog, createSettingsWindow)
|
||||
})
|
||||
|
||||
ipcMain.on('start-resize-window', () => {
|
||||
mainWindow.setResizable(true)
|
||||
})
|
||||
|
||||
ipcMain.on('stop-resize-window', () => {
|
||||
mainWindow.setResizable(false)
|
||||
})
|
||||
|
||||
ipcMain.handle('stop-move-window', () => {
|
||||
return mainWindow.getPosition()
|
||||
})
|
||||
|
||||
ipcMain.on('window-size-changed', (_, width: number, height: number) => {
|
||||
if (width === 0 || height === 0) {
|
||||
mainWindow.setSize(300 * 1.5, 400 * 1.5) // back to default size
|
||||
return
|
||||
}
|
||||
mainWindow.setSize(width, height)
|
||||
})
|
||||
|
||||
ipcMain.on('window-position-changed', (_, x: number, y: number) => {
|
||||
mainWindow.setPosition(x, y)
|
||||
})
|
||||
})
|
||||
@@ -1,18 +0,0 @@
|
||||
export default {
|
||||
menu: {
|
||||
settings: 'Settings',
|
||||
quit: 'Quit',
|
||||
about: 'About',
|
||||
toggleDevTools: 'Toggle Developer Tools',
|
||||
show: 'Show airi',
|
||||
hide: 'Hide airi',
|
||||
},
|
||||
quitDialog: {
|
||||
title: 'Quit',
|
||||
message: 'Are you sure you want to quit?',
|
||||
buttons: {
|
||||
quit: 'Quit',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createI18n } from '.'
|
||||
|
||||
describe('createI18n', () => {
|
||||
it('should return the correct locale', () => {
|
||||
const { t } = createI18n()
|
||||
expect(t('menu.settings')).toBe('Settings')
|
||||
})
|
||||
|
||||
it('should return key if the key is not found', () => {
|
||||
const { t } = createI18n()
|
||||
// @ts-expect-error for test
|
||||
expect(t('menu.not.found')).toBe('menu.not.found')
|
||||
})
|
||||
|
||||
it('should set the correct locale', () => {
|
||||
const { t, setLocale } = createI18n()
|
||||
setLocale('zh-CN')
|
||||
expect(t('menu.settings')).toBe('设置')
|
||||
})
|
||||
|
||||
it('should return the correct locale in array', () => {
|
||||
const { t } = createI18n()
|
||||
expect(t('quitDialog.buttons.quit')).toBe('Quit')
|
||||
expect(t('quitDialog.buttons.cancel')).toBe('Cancel')
|
||||
})
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
import enUS from './en-US'
|
||||
import zhCN from './zh-CN'
|
||||
|
||||
// TODO: compact locales, such as 'en' can be 'en-US'
|
||||
const locales = {
|
||||
'en-US': enUS,
|
||||
'zh-CN': zhCN,
|
||||
}
|
||||
|
||||
type Message = typeof locales['en-US']
|
||||
type KeyOfExcludeSymbol<T> = Exclude<keyof T, symbol>
|
||||
type ValueOf<T> = T[KeyOfExcludeSymbol<T>]
|
||||
type PathOf<T, Root extends boolean = true> = T extends Array<any> ? number : T extends string ? '' : Root extends true ? `${KeyOfExcludeSymbol<T>}${PathOf<ValueOf<T>, false>}` : `.${KeyOfExcludeSymbol<T>}${PathOf<ValueOf<T>, false>}`
|
||||
type LocalePath = PathOf<Message>
|
||||
|
||||
export function createI18n() {
|
||||
let locale = 'en-US'
|
||||
let messages = locales['en-US']
|
||||
|
||||
function t(key: LocalePath) {
|
||||
const path = key.split('.')
|
||||
let current = messages
|
||||
let result = ''
|
||||
|
||||
while (path.length > 0) {
|
||||
const k = path.shift()
|
||||
if (k && current && k in current) {
|
||||
current = current[k]
|
||||
}
|
||||
else {
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof current === 'string') {
|
||||
result = current
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function setLocale(l: string) {
|
||||
locale = l
|
||||
messages = locales[l]
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
setLocale,
|
||||
locale,
|
||||
}
|
||||
}
|
||||
|
||||
export type I18n = ReturnType<typeof createI18n>
|
||||
@@ -1,18 +0,0 @@
|
||||
export default {
|
||||
menu: {
|
||||
settings: '设置',
|
||||
quit: '退出',
|
||||
about: '关于',
|
||||
toggleDevTools: '切换开发者工具',
|
||||
show: '显示 airi',
|
||||
hide: '隐藏 airi',
|
||||
},
|
||||
quitDialog: {
|
||||
title: '退出',
|
||||
message: '确定要退出吗?',
|
||||
buttons: {
|
||||
quit: '退出',
|
||||
cancel: '取消',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { I18n } from './locales'
|
||||
|
||||
import { Menu } from 'electron/main'
|
||||
|
||||
export function createBasicMenu(
|
||||
{ t }: I18n,
|
||||
createSettingsWindow: () => void,
|
||||
onQuitClick: () => void,
|
||||
): Array<(Electron.MenuItemConstructorOptions)> {
|
||||
return [
|
||||
{
|
||||
role: 'about',
|
||||
label: t('menu.about'),
|
||||
},
|
||||
{
|
||||
label: t('menu.settings'),
|
||||
click: createSettingsWindow,
|
||||
},
|
||||
{
|
||||
label: t('menu.quit'),
|
||||
click: onQuitClick,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function createTrayMenu(
|
||||
i18n: I18n,
|
||||
isVisible: boolean,
|
||||
onVisibleChange: () => void,
|
||||
createSettingsWindow: () => void,
|
||||
onQuitClick: () => void,
|
||||
) {
|
||||
const menu = createBasicMenu(i18n, createSettingsWindow, onQuitClick)
|
||||
if (isVisible) {
|
||||
menu.push({
|
||||
label: i18n.t('menu.hide'),
|
||||
click: onVisibleChange,
|
||||
})
|
||||
}
|
||||
else {
|
||||
menu.push({
|
||||
label: i18n.t('menu.show'),
|
||||
click: onVisibleChange,
|
||||
})
|
||||
}
|
||||
return Menu.buildFromTemplate(menu)
|
||||
}
|
||||
|
||||
export function createApplicationMenu(
|
||||
i18n: I18n,
|
||||
onQuitClick: () => void,
|
||||
createSettingsWindow: () => void,
|
||||
) {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'airi',
|
||||
role: 'appMenu',
|
||||
submenu: createBasicMenu(i18n, createSettingsWindow, onQuitClick),
|
||||
},
|
||||
{
|
||||
role: 'editMenu',
|
||||
},
|
||||
])
|
||||
Menu.setApplicationMenu(menu)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { ElectronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: unknown
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { contextIsolated } from 'node:process'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { contextBridge } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
// renderer only if context isolation is enabled, otherwise
|
||||
// just add to the DOM global.
|
||||
if (contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// @ts-expect-error (define in dts)
|
||||
window.electron = electronAPI
|
||||
// @ts-expect-error (define in dts)
|
||||
window.api = api
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,372 +0,0 @@
|
||||
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
|
||||
memory:
|
||||
description: Where memories got stored, and organized
|
||||
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:
|
||||
anthropic:
|
||||
description: anthropic.com
|
||||
title: Anthropic | Claude
|
||||
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:
|
||||
description: speech.microsoft.com
|
||||
fields:
|
||||
field:
|
||||
region:
|
||||
description: Speech Service region
|
||||
label: Region
|
||||
title: Microsoft / Azure Speech
|
||||
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
|
||||
perplexity:
|
||||
description: perplexity.ai
|
||||
title: Perplexity
|
||||
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
|
||||
sections:
|
||||
section:
|
||||
general:
|
||||
title: General
|
||||
theme: Theme
|
||||
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
|
||||
@@ -1,336 +0,0 @@
|
||||
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:
|
||||
memory:
|
||||
description: 存放记忆的地方,以及策略
|
||||
title: 记忆体
|
||||
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:
|
||||
anthropic:
|
||||
description: anthropic.com
|
||||
title: Anthropic | Claude
|
||||
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:
|
||||
description: speech.microsoft.com
|
||||
fields:
|
||||
field:
|
||||
region:
|
||||
description: 服务 Endpoint 地区(比如亚太 eastasia 区域)
|
||||
label: Endpoint 地区
|
||||
title: Microsoft / Azure 语音服务
|
||||
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
|
||||
perplexity:
|
||||
description: perplexity.ai
|
||||
title: Perplexity
|
||||
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: 舞台主题
|
||||
sections:
|
||||
section:
|
||||
general:
|
||||
title: 通用
|
||||
theme: 主题颜色
|
||||
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: 已就绪
|
||||
@@ -1,65 +0,0 @@
|
||||
<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>
|
||||
@@ -1,97 +0,0 @@
|
||||
<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>
|
||||
@@ -1,136 +0,0 @@
|
||||
<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 { 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() {
|
||||
window.electron.ipcRenderer.send('open-settings')
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -1,259 +0,0 @@
|
||||
<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>
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { EffectScope } from 'vue'
|
||||
|
||||
import { useShortcutsStore } from '@renderer/stores/shortcuts'
|
||||
import { useMagicKeys, whenever } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, effectScope, watch } from 'vue'
|
||||
|
||||
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 })
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div px-4 py-8 flex="~ col gap-4">
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||