chore: migrate to spa
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
name: Build Previewing Docs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
STORE_PATH: ''
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
name: Build - ${{ matrix.os }}
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
# This is quite weird.
|
||||
# Eventhough this is the *intended* solution introduces in official blog post here
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
# But still, as https://github.com/orgs/community/discussions/25220#discussioncomment-7856118 stated,
|
||||
# this is vulnerable since there is no source of truth about which PR in the triggered workflow.
|
||||
- name: Presist PR number
|
||||
run: |
|
||||
echo "${{ github.event.number }}" > pr_num
|
||||
|
||||
- name: Upload PR artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pr-num
|
||||
path: ./pr_num
|
||||
overwrite: true
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js 22.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm packages:build
|
||||
env:
|
||||
# As suggested in Verbose Build option to be able to track down errors https://github.com/vuejs/vitepress/issues/422
|
||||
# vitepress build command does not have --debug option, so we need to set it manually where the debug package is used.
|
||||
DEBUG: none
|
||||
|
||||
- name: Build docs
|
||||
run: pnpm docs:build
|
||||
env:
|
||||
# As suggested in Verbose Build option to be able to track down errors https://github.com/vuejs/vitepress/issues/422
|
||||
# vitepress build command does not have --debug option, so we need to set it manually where the debug package is used.
|
||||
# DEBUG: 'vitepress:*'
|
||||
VUE_PROD_HYDRATION_MISMATCH_DETAILS_FLAG: '1'
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs-${{ matrix.os }}-build
|
||||
path: docs/.vitepress/dist
|
||||
overwrite: true
|
||||
@@ -41,25 +41,3 @@ jobs:
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x]
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: https://registry.npmjs.org/
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install
|
||||
- run: pnpm run test:unit
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
name: Push Previewing Docs to Netlify
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Build Previewing Docs
|
||||
types:
|
||||
- completed
|
||||
|
||||
env:
|
||||
PR_NUM: 0
|
||||
STORE_PATH: ''
|
||||
UBUNTU_NETLIFY_JSON_OUTPUT: ''
|
||||
UBUNTU_NETLIFY_URL: ''
|
||||
WINDOWS_NETLIFY_JSON_OUTPUT: ''
|
||||
WINDOWS_NETLIFY_URL: ''
|
||||
|
||||
jobs:
|
||||
on-success:
|
||||
name: Deploy to Netlify
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Download artifact - PR
|
||||
uses: dawidd6/action-download-artifact@v7
|
||||
with:
|
||||
workflow_conclusion: success
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: pr-num
|
||||
path: pr-num
|
||||
allow_forks: true
|
||||
|
||||
- name: Obtain PR number
|
||||
id: pr-num
|
||||
run: |
|
||||
echo "PR_NUM=$(cat pr-num/pr_num)" >> $GITHUB_ENV
|
||||
|
||||
- name: Download artifact - Ubuntu
|
||||
uses: dawidd6/action-download-artifact@v7
|
||||
with:
|
||||
workflow_conclusion: success
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: docs-ubuntu-latest-build
|
||||
path: docs-ubuntu-latest-build
|
||||
allow_forks: true
|
||||
|
||||
- name: Install Node.js 22.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
version: 9
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install Netlify CLI
|
||||
run: pnpm install -g netlify-cli@17.36.0
|
||||
|
||||
- name: Push to Netlify - Ubuntu
|
||||
id: netlify-ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
UBUNTU_NETLIFY_JSON_OUTPUT=$(netlify deploy --dir docs-ubuntu-latest-build --json)
|
||||
echo $UBUNTU_NETLIFY_JSON_OUTPUT
|
||||
|
||||
echo "UBUNTU_NETLIFY_JSON_OUTPUT=$(echo $UBUNTU_NETLIFY_JSON_OUTPUT)" >> $GITHUB_ENV
|
||||
echo "UBUNTU_NETLIFY_URL=$(echo $UBUNTU_NETLIFY_JSON_OUTPUT | jq -r .deploy_url)" >> $GITHUB_ENV
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ env.PR_NUM }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: to Netlify
|
||||
|
||||
- name: Create or update comment
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ env.PR_NUM }}
|
||||
body: |
|
||||
## ✅ Successfully deployed to Netlify
|
||||
|
||||
| Platform | Status | URL |
|
||||
|:---------|:------------|:---------------------------------|
|
||||
| Ubuntu | Success | ${{ env.UBUNTU_NETLIFY_URL }} |
|
||||
edit-mode: replace
|
||||
|
||||
on-failure:
|
||||
name: Failed to build previewing docs
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
|
||||
steps:
|
||||
- name: Download artifact - PR
|
||||
uses: dawidd6/action-download-artifact@v7
|
||||
with:
|
||||
workflow_conclusion: success
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: pr-num
|
||||
path: pr-num
|
||||
allow_forks: true
|
||||
|
||||
- name: Obtain PR number
|
||||
id: pr-num
|
||||
run: |
|
||||
echo "PR_NUM=$(cat pr-num/pr_num)" >> $GITHUB_ENV
|
||||
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ env.PR_NUM }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: to Netlify
|
||||
|
||||
- name: Create or update comment
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ env.PR_NUM }}
|
||||
body: |
|
||||
## ❌ Failed to deploy to Netlify
|
||||
|
||||
| Platform | Status | URL |
|
||||
|:---------|:------------|:------------------------------------------------------|
|
||||
| Ubuntu | Failed | Please check the status and logs of the workflow run. |
|
||||
edit-mode: replace
|
||||
@@ -0,0 +1,70 @@
|
||||
name: Build Docs to Netlify
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
STORE_PATH: ''
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-24.04
|
||||
environment:
|
||||
name: Production Docs
|
||||
url: https://nolebase-integrations.ayaka.io
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js 22.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build docs
|
||||
run: |
|
||||
pnpm packages:build
|
||||
pnpm docs:build
|
||||
env:
|
||||
# As suggested in Verbose Build option to be able to track down errors https://github.com/vuejs/vitepress/issues/422
|
||||
# vitepress build command does not have --debug option, so we need to set it manually where the debug package is used.
|
||||
# DEBUG: 'vitepress:*'
|
||||
VUE_PROD_HYDRATION_MISMATCH_DETAILS_FLAG: '1'
|
||||
|
||||
- name: Install Netlify CLI
|
||||
run: pnpm install -g netlify-cli@17.36.0
|
||||
|
||||
- name: Push to Netlify
|
||||
timeout-minutes: 10
|
||||
working-directory: docs/
|
||||
run: |
|
||||
netlify deploy --dir 'docs/.vitepress/dist' --prod --debug
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
+1
-1
@@ -9,11 +9,11 @@
|
||||
.vite-inspect
|
||||
*.local
|
||||
*.log
|
||||
.cache/
|
||||
components.d.ts
|
||||
dist
|
||||
node_modules
|
||||
.eslintcache
|
||||
.cache/
|
||||
**/.vitepress/docsMetadata.json
|
||||
**/.vitepress/cache/
|
||||
|
||||
|
||||
@@ -1,219 +1,17 @@
|
||||
<p align='center'>
|
||||
<img src='https://user-images.githubusercontent.com/11247099/154486817-f86b8f20-5463-4122-b6e9-930622e757f2.png' alt='Vitesse - Opinionated Vite Starter Template' width='600'/>
|
||||
<h1 align="center">アイリ VTuber</h1>
|
||||
|
||||
<p align="center">
|
||||
[<a href="https://airi.ayaka.io">Try it</a>]
|
||||
</p>
|
||||
|
||||
<p align='center'>
|
||||
Mocking up web app with <b>Vitesse</b><sup><em>(speed)</em></sup><br>
|
||||
</p>
|
||||
> Heavily inspired by [Neuro-sama](https://www.youtube.com/@Neurosama)
|
||||
|
||||
<br>
|
||||
## Development
|
||||
|
||||
<p align='center'>
|
||||
<a href="https://vitesse.netlify.app/">Live Demo</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
> **Note**: This template is created during the early transition of Vue 3 and Vite. At this moment, if you are seeking for better Vue developer experience and more consistent maintenance, we recommend using [Nuxt 3](https://nuxt.com) instead (it also works perfectly with SPA or SSG as needed). This template still serves as a reference, but expect slower updates.
|
||||
|
||||
<br>
|
||||
|
||||
<p align='center'>
|
||||
<b>English</b> | <a href="https://github.com/antfu-collective/vitesse/blob/main/README.zh-CN.md">简体中文</a>
|
||||
<!-- Contributors: Thanks for getting interested, however we DON'T accept new translations to the README, thanks. -->
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
## Features
|
||||
|
||||
- ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/), [esbuild](https://github.com/evanw/esbuild) - born with fastness
|
||||
|
||||
- 🗂 [File based routing](./src/pages)
|
||||
|
||||
- 📦 [Components auto importing](./src/components)
|
||||
|
||||
- 🍍 [State Management via Pinia](https://pinia.vuejs.org/)
|
||||
|
||||
- 📑 [Layout system](./src/layouts)
|
||||
|
||||
- 📲 [PWA](https://github.com/antfu/vite-plugin-pwa)
|
||||
|
||||
- 🎨 [UnoCSS](https://github.com/antfu/unocss) - the instant on-demand atomic CSS engine
|
||||
|
||||
- 😃 [Use icons from any icon sets with classes](https://github.com/antfu/unocss/tree/main/packages/preset-icons)
|
||||
|
||||
- 🌍 [I18n ready](./locales)
|
||||
|
||||
- 🔎 [Component Preview](https://github.com/johnsoncodehk/vite-plugin-vue-component-preview)
|
||||
|
||||
- 🗒 [Markdown Support](https://github.com/unplugin/unplugin-vue-markdown)
|
||||
|
||||
- 🔥 Use the [new `<script setup>` syntax](https://github.com/vuejs/rfcs/pull/227)
|
||||
|
||||
- 📥 [APIs auto importing](https://github.com/antfu/unplugin-auto-import) - use Composition API and others directly
|
||||
|
||||
- 🖨 Static-site generation (SSG) via [vite-ssg](https://github.com/antfu/vite-ssg)
|
||||
|
||||
- 🦔 Critical CSS via [critters](https://github.com/GoogleChromeLabs/critters)
|
||||
|
||||
- 🔤 [Webfont self-hosting](https://github.com/feat-agency/vite-plugin-webfont-dl)
|
||||
|
||||
- 🦾 TypeScript, of course
|
||||
|
||||
- ⚙️ Unit Testing with [Vitest](https://github.com/vitest-dev/vitest), E2E Testing with [Cypress](https://cypress.io/) on [GitHub Actions](https://github.com/features/actions)
|
||||
|
||||
- ☁️ Deploy on Netlify, zero-config
|
||||
|
||||
<br>
|
||||
|
||||
## Pre-packed
|
||||
|
||||
### UI Frameworks
|
||||
|
||||
- [UnoCSS](https://github.com/antfu/unocss) - The instant on-demand atomic CSS engine.
|
||||
|
||||
### Icons
|
||||
|
||||
- [Iconify](https://iconify.design) - use icons from any icon sets [🔍Icônes](https://icones.netlify.app/)
|
||||
- [Pure CSS Icons via UnoCSS](https://github.com/antfu/unocss/tree/main/packages/preset-icons)
|
||||
|
||||
### Plugins
|
||||
|
||||
- [Vue Router](https://github.com/vuejs/router)
|
||||
- [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router) - file system based routing
|
||||
- [`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) - layouts for pages
|
||||
- [Pinia](https://pinia.vuejs.org) - Intuitive, type safe, light and flexible Store for Vue using the composition api
|
||||
- [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components) - components auto import
|
||||
- [`unplugin-auto-import`](https://github.com/antfu/unplugin-auto-import) - Directly use Vue Composition API and others without importing
|
||||
- [`unplugin-vue-macros`](https://github.com/sxzz/unplugin-vue-macros) - Explore and extend more macros and syntax sugar to Vue.
|
||||
- [`vite-plugin-pwa`](https://github.com/antfu/vite-plugin-pwa) - PWA
|
||||
- [`unplugin-vue-markdown`](https://github.com/unplugin/unplugin-vue-markdown) - Markdown as components / components in Markdown
|
||||
- [`@shikijs/markdown-it`](https://github.com/shikijs/shiki) - [Shiki](https://github.com/shikijs/shiki) for syntax highlighting
|
||||
- [Vue I18n](https://github.com/intlify/vue-i18n-next) - Internationalization
|
||||
- [`unplugin-vue-i18n`](https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n) - unplugin for Vue I18n
|
||||
- [VueUse](https://github.com/antfu/vueuse) - collection of useful composition APIs
|
||||
- [`vite-ssg-sitemap`](https://github.com/jbaubree/vite-ssg-sitemap) - Sitemap generator
|
||||
- [`@vueuse/head`](https://github.com/vueuse/head) - manipulate document head reactively
|
||||
- [`vite-plugin-webfont-dl`](https://github.com/feat-agency/vite-plugin-webfont-dl) - Zero-config webfont (Google Fonts) downloader and injector to improve website's performance.
|
||||
- [`vite-plugin-vue-devtools`](https://github.com/vuejs/devtools-next) - Designed to enhance the Vue developer experience.
|
||||
|
||||
### Coding Style
|
||||
|
||||
- Use Composition API with [`<script setup>` SFC syntax](https://github.com/vuejs/rfcs/pull/227)
|
||||
- [ESLint](https://eslint.org/) with [@antfu/eslint-config](https://github.com/antfu/eslint-config), single quotes, no semi.
|
||||
|
||||
### Dev tools
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Vitest](https://github.com/vitest-dev/vitest) - Unit testing powered by Vite
|
||||
- [Cypress](https://cypress.io/) - E2E testing
|
||||
- [pnpm](https://pnpm.js.org/) - fast, disk space efficient package manager
|
||||
- [`vite-ssg`](https://github.com/antfu/vite-ssg) - Static-site generation
|
||||
- [critters](https://github.com/GoogleChromeLabs/critters) - Critical CSS
|
||||
- [Netlify](https://www.netlify.com/) - zero-config deployment
|
||||
- [VS Code Extensions](./.vscode/extensions.json)
|
||||
- [Vite](https://marketplace.visualstudio.com/items?itemName=antfu.vite) - Fire up Vite server automatically
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - Vue 3 `<script setup>` IDE support
|
||||
- [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) - Icon inline display and autocomplete
|
||||
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) - All in one i18n support
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||
|
||||
## Variations
|
||||
|
||||
As this template is strongly opinionated, the following provides a curated list for community-maintained variations with different preferences and feature sets. Check them out as well. PR to add yours is also welcome!
|
||||
|
||||
###### Official
|
||||
|
||||
- [vitesse-lite](https://github.com/antfu/vitesse-lite) - Lightweight version of Vitesse
|
||||
- [vitesse-nuxt3](https://github.com/antfu/vitesse-nuxt3) - Vitesse for Nuxt 3
|
||||
- [vitesse-nuxt-bridge](https://github.com/antfu/vitesse-nuxt-bridge) - Vitesse for Nuxt 2 with Bridge
|
||||
- [vitesse-webext](https://github.com/antfu/vitesse-webext) - WebExtension Vite starter template
|
||||
|
||||
###### Community
|
||||
|
||||
- [vitesse-ssr-template](https://github.com/frandiox/vitesse-ssr-template) by [@frandiox](https://github.com/frandiox) - Vitesse with SSR
|
||||
- [vitailse](https://github.com/zynth17/vitailse) by [@zynth17](https://github.com/zynth17) - Like Vitesse but with TailwindCSS
|
||||
- [vitesse-modernized-chrome-ext](https://github.com/xiaoluoboding/vitesse-modernized-chrome-ext) by [@xiaoluoboding](https://github.com/xiaoluoboding) - ⚡️ Modernized Chrome Extension Manifest V3 Vite Starter Template
|
||||
- [vitesse-stackter-clean-architect](https://github.com/shamscorner/vitesse-stackter-clean-architect) by [@shamscorner](https://github.com/shamscorner) - A modular clean architecture pattern in vitesse template
|
||||
- [vitesse-enterprise](https://github.com/FranciscoKloganB/vitesse-enterprise) by [@FranciscoKloganB](https://github.com/FranciscoKloganB) - Consistent coding styles regardless of team-size.
|
||||
- [vitecamp](https://github.com/nekobc1998923/vitecamp) by [@nekobc1998923](https://github.com/nekobc1998923) - Like Vitesse but without SSG/SSR/File based routing, includes Element Plus
|
||||
- [vitesse-h5](https://github.com/YunYouJun/vitesse-h5) by [@YunYouJun](https://github.com/YunYouJun) - Vitesse for Mobile
|
||||
- [bat](https://github.com/olgam4/bat) by [@olgam4](https://github.com/olgam4) - Vitesse for SolidJS
|
||||
- [vitesse-solid](https://github.com/xbmlz/vitesse-solid) by [@xbmlz](https://github.com/xbmlz) - Vitesse for SolidJS, build with [`SolidStart`](https://start.solidjs.com/), includes [UnoCSS](https://github.com/unocss/unocss) and [HopeUI](https://hope-ui.com/).
|
||||
- [vue3-vant-mobile](https://github.com/easy-temps/vue3-vant-mobile) by [CharleeWa](https://github.com/CharleeWa) - Like Vitesse but without SSG/SSR, includes Vant
|
||||
|
||||
## Try it now!
|
||||
|
||||
> Vitesse requires Node >=14.18
|
||||
|
||||
### GitHub Template
|
||||
|
||||
[Create a repo from this template on GitHub](https://github.com/antfu-collective/vitesse/generate).
|
||||
|
||||
### Clone to local
|
||||
|
||||
If you prefer to do it manually with the cleaner git history
|
||||
|
||||
```bash
|
||||
npx degit antfu-collective/vitesse my-vitesse-app
|
||||
cd my-vitesse-app
|
||||
pnpm i # If you don't have pnpm installed, run: npm install -g pnpm
|
||||
```shell
|
||||
pnpm i
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When you use this template, try follow the checklist to update your info properly
|
||||
|
||||
- [ ] Change the author name in `LICENSE`
|
||||
- [ ] Change the title in `App.vue`
|
||||
- [ ] Change the hostname in `vite.config.ts`
|
||||
- [ ] Change the favicon in `public`
|
||||
- [ ] Remove the `.github` folder which contains the funding info
|
||||
- [ ] Clean up the READMEs and remove routes
|
||||
|
||||
And, enjoy :)
|
||||
|
||||
## Usage
|
||||
|
||||
### Development
|
||||
|
||||
Just run and visit http://localhost:3333
|
||||
|
||||
```bash
|
||||
```shell
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
To build the App, run
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
And you will see the generated file in `dist` that ready to be served.
|
||||
|
||||
### Deploy on Netlify
|
||||
|
||||
Go to [Netlify](https://app.netlify.com/start) and select your clone, `OK` along the way, and your App will be live in a minute.
|
||||
|
||||
### Docker Production Build
|
||||
|
||||
First, build the vitesse image by opening the terminal in the project's root directory.
|
||||
|
||||
```bash
|
||||
docker buildx build . -t vitesse:latest
|
||||
```
|
||||
|
||||
Run the image and specify port mapping with the `-p` flag.
|
||||
|
||||
```bash
|
||||
docker run --rm -it -p 8080:80 vitesse:latest
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
I have created several Vite apps recently. Setting the configs up is kinda the bottleneck for me to make the ideas simply come true within a very short time.
|
||||
|
||||
So I made this starter template for myself to create apps more easily, along with some good practices that I have learned from making those apps. It's strongly opinionated, but feel free to tweak it or even maintain your own forks. [(see community maintained variation forks)](#variations)
|
||||
|
||||
+9
-177
@@ -1,185 +1,17 @@
|
||||
<p align='center'>
|
||||
<img src='https://user-images.githubusercontent.com/11247099/154486817-f86b8f20-5463-4122-b6e9-930622e757f2.png' alt='Vitesse - Opinionated Vite Starter Template' width='600'/>
|
||||
<h1 align="center">アイリ VTuber</h1>
|
||||
|
||||
<p align="center">
|
||||
[<a href="https://airi.ayaka.io">试试看</a>]
|
||||
</p>
|
||||
|
||||
<p align='center'>
|
||||
快速地<sup><em>Vitesse</em></sup> 创建 Web 应用
|
||||
<br>
|
||||
</p>
|
||||
> 由 [Neuro-sama](https://www.youtube.com/@Neurosama) 强烈启发
|
||||
|
||||
<br>
|
||||
## 开发
|
||||
|
||||
<p align='center'>
|
||||
<a href="https://vitesse.netlify.app/">在线 Demo</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
> **Note**: 本模板创建于 Vue 3 和 Vite 的早期过渡时期。目前,如果您正在寻求更好的 Vue 开发体验和更持续的维护,我们建议您使用 [Nuxt 3](https://nuxt.com) 来代替(它也可以根据需要使用 SPA 或 SSG)。本模板仍会作为参考缓慢地维护下去,但将不会有太多的更新。
|
||||
|
||||
<br>
|
||||
|
||||
<p align='center'>
|
||||
<a href="https://github.com/antfu-collective/vitesse/blob/main/README.md">English</a> | <b>简体中文</b>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
## 特性
|
||||
|
||||
- ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/), [esbuild](https://github.com/evanw/esbuild) - 就是快!
|
||||
|
||||
- 🗂 [基于文件的路由](./src/pages)
|
||||
|
||||
- 📦 [组件自动化加载](./src/components)
|
||||
|
||||
- 🍍 [使用 Pinia 的状态管理](https://pinia.vuejs.org)
|
||||
|
||||
- 📑 [布局系统](./src/layouts)
|
||||
|
||||
- 📲 [PWA](https://github.com/antfu/vite-plugin-pwa)
|
||||
|
||||
- 🎨 [UnoCSS](https://github.com/unocss/unocss) - 高性能且极具灵活性的即时原子化 CSS 引擎
|
||||
|
||||
- 😃 [各种图标集为你所用](https://github.com/antfu/unocss/tree/main/packages/preset-icons)
|
||||
|
||||
- 🌍 [I18n 国际化开箱即用](./locales)
|
||||
|
||||
- 🗒 [Markdown 支持](https://github.com/unplugin/unplugin-vue-markdown)
|
||||
|
||||
- 🔥 使用 [新的 `<script setup>` 语法](https://github.com/vuejs/rfcs/pull/227)
|
||||
|
||||
- 📥 [API 自动加载](https://github.com/unplugin/unplugin-auto-import) - 直接使用 Composition API 无需引入
|
||||
|
||||
- 🖨 使用 [vite-ssg](https://github.com/antfu/vite-ssg) 进行服务端生成 (SSG)
|
||||
|
||||
- 🦔 使用 [critters](https://github.com/GoogleChromeLabs/critters) 的生成关键 CSS
|
||||
|
||||
- 🦾 TypeScript, 当然
|
||||
|
||||
- ⚙️ 结合 [GitHub Actions](https://github.com/features/actions),使用 [Vitest](https://github.com/vitest-dev/vitest) 进行单元测试, [Cypress](https://cypress.io/) 进行 E2E 测试
|
||||
|
||||
- ☁️ 零配置部署 Netlify
|
||||
|
||||
<br>
|
||||
|
||||
## 预配置
|
||||
|
||||
### UI 框架
|
||||
|
||||
- [UnoCSS](https://github.com/antfu/unocss) - 高性能且极具灵活性的即时原子化 CSS 引擎
|
||||
|
||||
### Icons
|
||||
|
||||
- [Iconify](https://iconify.design) - 使用任意的图标集,浏览:[🔍Icônes](https://icones.netlify.app/)
|
||||
- [UnoCSS 的纯 CSS 图标方案](https://github.com/antfu/unocss/tree/main/packages/preset-icons)
|
||||
|
||||
### 插件
|
||||
|
||||
- [Vue Router](https://github.com/vuejs/router)
|
||||
- [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router) - 以文件系统为基础的路由
|
||||
- [`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) - 页面布局系统
|
||||
- [Pinia](https://pinia.vuejs.org) - 直接的, 类型安全的, 使用 Composition API 的轻便灵活的 Vue 状态管理
|
||||
- [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components) - 自动加载组件
|
||||
- [`unplugin-auto-import`](https://github.com/antfu/unplugin-auto-import) - 直接使用 Composition API 等,无需导入
|
||||
- [`vite-plugin-pwa`](https://github.com/antfu/vite-plugin-pwa) - PWA
|
||||
- [`unplugin-vue-markdown`](https://github.com/unplugin/unplugin-vue-markdown) - Markdown 作为组件,也可以让组件在 Markdown 中使用
|
||||
- [`markdown-it-prism`](https://github.com/jGleitz/markdown-it-prism) - [Prism](https://prismjs.com/) 的语法高亮
|
||||
- [`prism-theme-vars`](https://github.com/antfu/prism-theme-vars) - 利用 CSS 变量自定义 Prism.js 的主题
|
||||
- [Vue I18n](https://github.com/intlify/vue-i18n-next) - 国际化
|
||||
- [`unplugin-vue-i18n`](https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n) - Vue I18n 的 Vite 插件
|
||||
- [VueUse](https://github.com/antfu/vueuse) - 实用的 Composition API 工具合集
|
||||
- [`vite-ssg-sitemap`](https://github.com/jbaubree/vite-ssg-sitemap) - 站点地图生成器
|
||||
- [`@vueuse/head`](https://github.com/vueuse/head) - 响应式地操作文档头信息
|
||||
- [`vite-plugin-vue-devtools`](https://github.com/webfansplz/vite-plugin-vue-devtools) - 旨在增强Vue开发者体验的Vite插件
|
||||
|
||||
### 编码风格
|
||||
|
||||
- 使用 Composition API 地 [`<script setup>` SFC 语法](https://github.com/vuejs/rfcs/pull/227)
|
||||
- [ESLint](https://eslint.org/) 配置为 [@antfu/eslint-config](https://github.com/antfu/eslint-config), 单引号, 无分号.
|
||||
|
||||
### 开发工具
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Vitest](https://github.com/vitest-dev/vitest) - 基于 Vite 的单元测试框架
|
||||
- [Cypress](https://cypress.io/) - E2E 测试
|
||||
- [pnpm](https://pnpm.js.org/) - 快, 节省磁盘空间的包管理器
|
||||
- [`vite-ssg`](https://github.com/antfu/vite-ssg) - 服务端生成
|
||||
- [critters](https://github.com/GoogleChromeLabs/critters) - 关键 CSS 生成器
|
||||
- [Netlify](https://www.netlify.com/) - 零配置的部署
|
||||
- [VS Code 扩展](./.vscode/extensions.json)
|
||||
- [Vite](https://marketplace.visualstudio.com/items?itemName=antfu.vite) - 自动启动 Vite 服务器
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - Vue 3 `<script setup>` IDE 支持
|
||||
- [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) - 图标内联显示和自动补全
|
||||
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) - 多合一的 I18n 支持
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||
|
||||
## 衍生项目
|
||||
|
||||
由于这个模板的业务场景非常的局限,下面提供了一个精心策划的列表,列出了社区维护的具有不同偏好和功能集的衍生项目。也可以看看他们。当然也欢迎你 PR 提供自己的项目!
|
||||
|
||||
###### 官方
|
||||
|
||||
- [vitesse-lite](https://github.com/antfu/vitesse-lite) - Vitesse 的轻量版本
|
||||
- [vitesse-nuxt3](https://github.com/antfu/vitesse-nuxt3) - Vitesse 的 Nuxt 3 版本
|
||||
- [vitesse-nuxt-bridge](https://github.com/antfu/vitesse-nuxt-bridge) - Vitesse 的 Nuxt2 桥接版本
|
||||
- [vitesse-webext](https://github.com/antfu/vitesse-webext) - 开箱即用的浏览器扩展 vite 模板
|
||||
|
||||
###### 社区
|
||||
|
||||
[查看英文版](./README.md#community)
|
||||
|
||||
## 现在可以试试!
|
||||
|
||||
> Vitesse 需要 Node 版本 >=14.18
|
||||
|
||||
### GitHub 模板
|
||||
|
||||
[使用这个模板创建仓库](https://github.com/antfu-collective/vitesse/generate).
|
||||
|
||||
### 克隆到本地
|
||||
|
||||
如果您更喜欢使用更干净的 git 历史记录手动执行此操作
|
||||
|
||||
```bash
|
||||
npx degit antfu-collective/vitesse my-vitesse-app
|
||||
cd my-vitesse-app
|
||||
pnpm i # 如果你没装过 pnpm, 可以先运行: npm install -g pnpm
|
||||
```shell
|
||||
pnpm i
|
||||
```
|
||||
|
||||
## 清单
|
||||
|
||||
使用此模板时,请尝试按照清单正确更新您自己的信息
|
||||
|
||||
- [ ] 在 `LICENSE` 中改变作者名
|
||||
- [ ] 在 `App.vue` 中改变标题
|
||||
- [ ] 在 `vite.config.ts` 更改主机名
|
||||
- [ ] 在 `public` 目录下改变favicon
|
||||
- [ ] 移除 `.github` 文件夹中包含资助的信息
|
||||
- [ ] 整理 README 并删除路由
|
||||
|
||||
紧接着, 享受吧 :)
|
||||
|
||||
## 使用
|
||||
|
||||
### 开发
|
||||
|
||||
只需要执行以下命令就可以在 http://localhost:3333 中看到
|
||||
|
||||
```bash
|
||||
```shell
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 构建
|
||||
|
||||
构建该应用只需要执行以下命令
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
然后你会看到用于发布的 `dist` 文件夹被生成。
|
||||
|
||||
### 部署到 Netlify
|
||||
|
||||
前往 [Netlify](https://app.netlify.com/start) 并选择你的仓库, 一路 `OK` 下去,稍等一下后,你的应用将被创建.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
version: '0.2'
|
||||
ignorePaths: []
|
||||
dictionaryDefinitions: []
|
||||
dictionaries: []
|
||||
words:
|
||||
- airi-vtuber
|
||||
- composables
|
||||
- elevenlabs
|
||||
- hiyori
|
||||
- iconify
|
||||
- kwaa
|
||||
- Myriam
|
||||
- nekomeowww
|
||||
- Neuro
|
||||
- Neuro-sama
|
||||
- nuxi
|
||||
- nuxt
|
||||
- nuxtjs
|
||||
- ofetch
|
||||
- openai
|
||||
- pinia
|
||||
- pixi
|
||||
- rehype
|
||||
- unocss
|
||||
- vueuse
|
||||
- live2dcubismcore
|
||||
- live2dcubismframework
|
||||
- cubismmatrix44
|
||||
- csmvector
|
||||
- cubismviewmatrix
|
||||
- cubismdefaultparameterid
|
||||
- cubismmodelsettingjson
|
||||
- cubismbreath
|
||||
- cubismeyeblink
|
||||
- cubismusermodel
|
||||
- acubismmotion
|
||||
- cubismmotionqueuemanager
|
||||
- csmmap
|
||||
- cubismdebug
|
||||
- cubismmoc
|
||||
ignoreWords: []
|
||||
import: []
|
||||
@@ -14,6 +14,7 @@
|
||||
document.documentElement.classList.toggle('dark', true)
|
||||
})()
|
||||
</script>
|
||||
<script src="/assets/js/CubismSdkForWeb-5-r.1/Core/live2dcubismcore.min.js"></script>
|
||||
</head>
|
||||
<body class="font-sans">
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
## i18n
|
||||
|
||||
This directory is to serve your locale translation files. YAML under this folder would be loaded automatically and register with their filenames as locale code.
|
||||
|
||||
Check out [`vue-i18n`](https://github.com/intlify/vue-i18n-next) for more details.
|
||||
|
||||
If you are using VS Code, [`i18n Ally`](https://github.com/lokalise/i18n-ally) is recommended to make the i18n experience better.
|
||||
@@ -1,14 +0,0 @@
|
||||
button:
|
||||
about: حول
|
||||
back: رجوع
|
||||
go: تجربة
|
||||
home: الرئيسية
|
||||
toggle_dark: التغيير إلى الوضع المظلم
|
||||
toggle_langs: تغيير اللغة
|
||||
intro:
|
||||
desc: vite مثال لتطبيق
|
||||
dynamic-route: عرض لتوجيهات ديناميكية
|
||||
hi: مرحبا {name}
|
||||
aka: معروف أيضا تحت مسمى
|
||||
whats-your-name: ما إسمك؟
|
||||
not-found: صفحة غير موجودة
|
||||
@@ -1,14 +0,0 @@
|
||||
button:
|
||||
about: Über
|
||||
back: Zurück
|
||||
go: Los
|
||||
home: Startseite
|
||||
toggle_dark: Dunkelmodus umschalten
|
||||
toggle_langs: Sprachen ändern
|
||||
intro:
|
||||
desc: Vite Startvorlage mit Vorlieben
|
||||
dynamic-route: Demo einer dynamischen Route
|
||||
hi: Hi, {name}!
|
||||
aka: Auch bekannt als
|
||||
whats-your-name: Wie heißt du?
|
||||
not-found: Nicht gefunden
|
||||
@@ -1,14 +0,0 @@
|
||||
button:
|
||||
about: Acerca de
|
||||
back: Atrás
|
||||
go: Ir
|
||||
home: Inicio
|
||||
toggle_dark: Alternar modo oscuro
|
||||
toggle_langs: Cambiar idiomas
|
||||
intro:
|
||||
desc: Plantilla de Inicio de Vite Dogmática
|
||||
dynamic-route: Demo de ruta dinámica
|
||||
hi: ¡Hola, {name}!
|
||||
aka: También conocido como
|
||||
whats-your-name: ¿Cómo te llamas?
|
||||
not-found: No se ha encontrado
|
||||
@@ -1,14 +0,0 @@
|
||||
button:
|
||||
about: À propos
|
||||
back: Retour
|
||||
go: Essayer
|
||||
home: Accueil
|
||||
toggle_dark: Basculer en mode sombre
|
||||
toggle_langs: Changer de langue
|
||||
intro:
|
||||
desc: Exemple d'application Vite
|
||||
dynamic-route: Démo de route dynamique
|
||||
hi: Salut, {name}!
|
||||
aka: Aussi connu sous le nom de
|
||||
whats-your-name: Comment t'appelles-tu ?
|
||||
not-found: Page non trouvée
|
||||
@@ -1,14 +0,0 @@
|
||||
button:
|
||||
about: Tentang
|
||||
back: Kembali
|
||||
go: Pergi
|
||||
home: Beranda
|
||||
toggle_dark: Ubah ke mode gelap
|
||||
toggle_langs: Ubah bahasa
|
||||
intro:
|
||||
desc: Template awal vite
|
||||
dynamic-route: Contoh rute dinamik
|
||||
hi: Halo, {name}!
|
||||
aka: Juga diketahui sebagai
|
||||
whats-your-name: Siapa nama anda?
|
||||
not-found: Tidak ditemukan
|
||||
@@ -1,13 +0,0 @@
|
||||
button:
|
||||
about: Su di me
|
||||
back: Indietro
|
||||
go: Vai
|
||||
home: Home
|
||||
toggle_dark: Attiva/disattiva modalità scura
|
||||
toggle_langs: Cambia lingua
|
||||
intro:
|
||||
desc: Modello per una Applicazione Vite
|
||||
dynamic-route: Demo di rotta dinamica
|
||||
hi: Ciao, {name}!
|
||||
whats-your-name: Come ti chiami?
|
||||
not-found: Non trovato
|
||||
@@ -1,13 +0,0 @@
|
||||
button:
|
||||
about: これは?
|
||||
back: 戻る
|
||||
go: 進む
|
||||
home: ホーム
|
||||
toggle_dark: ダークモード切り替え
|
||||
toggle_langs: 言語切り替え
|
||||
intro:
|
||||
desc: 固執された Vite スターターテンプレート
|
||||
dynamic-route: 動的ルートのデモ
|
||||
hi: こんにちは、{name}!
|
||||
whats-your-name: 君の名は。
|
||||
not-found: 見つかりませんでした
|
||||
@@ -1,14 +0,0 @@
|
||||
button:
|
||||
about: შესახებ
|
||||
back: უკან
|
||||
go: დაწყება
|
||||
home: მთავარი
|
||||
toggle_dark: გადართე მუქი რეჟიმი
|
||||
toggle_langs: ენის შეცვლა
|
||||
intro:
|
||||
desc: Opinionated Vite Starter Template
|
||||
dynamic-route: დინამიური როუტინგის დემო
|
||||
hi: გამარჯობა, {name}!
|
||||
aka: ასევე ცნობილი როგორც
|
||||
whats-your-name: რა გქვია?
|
||||
not-found: ვერ მოიძებნა
|
||||
@@ -1,13 +0,0 @@
|
||||
button:
|
||||
about: 소개
|
||||
back: 뒤로가기
|
||||
go: 이동
|
||||
home: 홈
|
||||
toggle_dark: 다크모드 토글
|
||||
toggle_langs: 언어 변경
|
||||
intro:
|
||||
desc: Vite 애플리케이션 템플릿
|
||||
dynamic-route: 다이나믹 라우트 데모
|
||||
hi: 안녕, {name}!
|
||||
whats-your-name: 이름이 뭐예요?
|
||||
not-found: 찾을 수 없습니다
|
||||
@@ -1,14 +0,0 @@
|
||||
button:
|
||||
about: O nas
|
||||
back: Wróć
|
||||
go: WEJDŹ
|
||||
home: Strona główna
|
||||
toggle_dark: Ustaw tryb nocny
|
||||
toggle_langs: Zmień język
|
||||
intro:
|
||||
desc: Opiniowany szablon startowy Vite
|
||||
dynamic-route: Demonstracja dynamicznego route
|
||||
hi: Cześć, {name}!
|
||||
aka: Znany też jako
|
||||
whats-your-name: Jak masz na imię?
|
||||
not-found: Nie znaleziono
|
||||
@@ -1,14 +0,0 @@
|
||||
button:
|
||||
about: Sobre
|
||||
back: Voltar
|
||||
go: Ir
|
||||
home: Início
|
||||
toggle_dark: Alternar modo escuro
|
||||
toggle_langs: Mudar de idioma
|
||||
intro:
|
||||
desc: Modelo Opinativo de Partida de Vite
|
||||
dynamic-route: Demonstração de rota dinâmica
|
||||
hi: Olá, {name}!
|
||||
aka: Também conhecido como
|
||||
whats-your-name: Qual é o seu nome?
|
||||
not-found: Não encontrado
|
||||
@@ -1,13 +0,0 @@
|
||||
button:
|
||||
about: О шаблоне
|
||||
back: Назад
|
||||
go: Перейти
|
||||
home: Главная
|
||||
toggle_dark: Включить темный режим
|
||||
toggle_langs: Сменить язык
|
||||
intro:
|
||||
desc: Самостоятельный начальный шаблон Vite
|
||||
dynamic-route: Демо динамического маршрута
|
||||
hi: Привет, {name}!
|
||||
whats-your-name: Как тебя зовут?
|
||||
not-found: Не найден
|
||||
@@ -1,14 +0,0 @@
|
||||
button:
|
||||
about: Hakkımda
|
||||
back: Geri
|
||||
go: İLERİ
|
||||
home: Anasayfa
|
||||
toggle_dark: Karanlık modu değiştir
|
||||
toggle_langs: Dilleri değiştir
|
||||
intro:
|
||||
desc: Görüşlü Vite Başlangıç Şablonu
|
||||
dynamic-route: Dinamik rota demosu
|
||||
hi: Merhaba, {name}!
|
||||
aka: Ayrıca şöyle bilinir
|
||||
whats-your-name: Adınız nedir?
|
||||
not-found: Bulunamadı
|
||||
@@ -1,13 +0,0 @@
|
||||
button:
|
||||
about: Про шаблон
|
||||
back: Назад
|
||||
go: Перейти
|
||||
home: Головна
|
||||
toggle_dark: Переключити темний режим
|
||||
toggle_langs: Змінити мову
|
||||
intro:
|
||||
desc: Самостійний початковий шаблон Vite
|
||||
dynamic-route: Демо динамічного маршруту
|
||||
hi: Привіт, {name}!
|
||||
whats-your-name: Як тебе звати?
|
||||
not-found: Не знайдено
|
||||
@@ -1,14 +0,0 @@
|
||||
button:
|
||||
about: Haqida
|
||||
back: Orqaga
|
||||
go: Kettik
|
||||
home: Bosh sahifa
|
||||
toggle_dark: Qorong‘i rejimga o‘tish
|
||||
toggle_langs: Tilni o‘zgartirish
|
||||
intro:
|
||||
desc: O‘ylangan boshlang‘ich Vite shabloni
|
||||
dynamic-route: Dynamic route demo'si
|
||||
hi: Assalomu alaykum, {name}!
|
||||
aka: shuningdek
|
||||
whats-your-name: Ismingiz nima?
|
||||
not-found: Topilmadi
|
||||
@@ -1,13 +0,0 @@
|
||||
button:
|
||||
about: Về
|
||||
back: Quay lại
|
||||
go: Đi
|
||||
home: Khởi đầu
|
||||
toggle_dark: Chuyển đổi chế độ tối
|
||||
toggle_langs: Thay đổi ngôn ngữ
|
||||
intro:
|
||||
desc: Ý kiến cá nhân Vite Template để bắt đầu
|
||||
dynamic-route: Bản giới thiệu về dynamic route
|
||||
hi: Hi, {name}!
|
||||
whats-your-name: Tên bạn là gì?
|
||||
not-found: Không tìm thấy
|
||||
@@ -5,10 +5,17 @@ command = "pnpm run build"
|
||||
[build.environment]
|
||||
NODE_VERSION = "20"
|
||||
|
||||
[[redirects]]
|
||||
from = "/assets/*"
|
||||
to = "/assets/:splat"
|
||||
status = 200
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
force = false
|
||||
|
||||
[[headers]]
|
||||
for = "/manifest.webmanifest"
|
||||
|
||||
+53
-30
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.14.2",
|
||||
"packageManager": "pnpm@9.14.4",
|
||||
"scripts": {
|
||||
"build": "vite-ssg build",
|
||||
"build": "vite build",
|
||||
"dev": "vite --port 3333 --open",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
@@ -16,65 +16,88 @@
|
||||
"sizecheck": "npx vite-bundle-visualizer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11labs/client": "^0.0.4",
|
||||
"@ai-sdk/openai": "^1.0.5",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"@unhead/vue": "^1.11.13",
|
||||
"@unocss/reset": "^0.64.1",
|
||||
"@unocss/reset": "^0.65.0",
|
||||
"@vueuse/core": "^12.0.0",
|
||||
"@vueuse/head": "^2.0.0",
|
||||
"ai": "^4.0.10",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.2.6",
|
||||
"ofetch": "^1.4.1",
|
||||
"openai": "^4.74.0",
|
||||
"pinia": "^2.2.8",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"unified": "^11.0.5",
|
||||
"vue": "^3.5.13",
|
||||
"vue-demi": "^0.14.10",
|
||||
"vue-i18n": "^10.0.4",
|
||||
"vue-router": "^4.5.0"
|
||||
"vue-i18n": "^10.0.5",
|
||||
"vue-router": "^4.5.0",
|
||||
"yauzl": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^3.10.0",
|
||||
"@antfu/eslint-config": "^3.11.2",
|
||||
"@iconify-json/carbon": "^1.2.4",
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.0",
|
||||
"@shikijs/markdown-it": "^1.23.1",
|
||||
"@pixi/app": "^6.5.10",
|
||||
"@pixi/constants": "6",
|
||||
"@pixi/core": "6",
|
||||
"@pixi/display": "6",
|
||||
"@pixi/extensions": "^6.5.10",
|
||||
"@pixi/loaders": "6",
|
||||
"@pixi/math": "6",
|
||||
"@pixi/runner": "6",
|
||||
"@pixi/settings": "6",
|
||||
"@pixi/sprite": "6",
|
||||
"@pixi/ticker": "^6.5.10",
|
||||
"@pixi/utils": "6",
|
||||
"@shikijs/markdown-it": "^1.24.0",
|
||||
"@types/markdown-it-link-attributes": "^3.0.5",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@unocss/eslint-config": "^0.64.1",
|
||||
"@unocss/eslint-config": "^0.65.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue-macros/volar": "^0.30.6",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"critters": "^0.0.25",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.16.0",
|
||||
"cypress-vite": "^1.5.0",
|
||||
"eslint": "^9.15.0",
|
||||
"elevenlabs": "^0.18.1",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-plugin-cypress": "^4.1.0",
|
||||
"eslint-plugin-format": "^0.1.2",
|
||||
"eslint-plugin-format": "^0.1.3",
|
||||
"https-localhost": "^4.7.1",
|
||||
"lint-staged": "^15.2.10",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"pnpm": "^9.14.2",
|
||||
"rollup": "^4.27.4",
|
||||
"shiki": "^1.23.1",
|
||||
"pixi-live2d-display": "^0.4.0",
|
||||
"pnpm": "^9.14.4",
|
||||
"rollup": "^4.28.0",
|
||||
"shiki": "^1.24.0",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"taze": "^0.18.0",
|
||||
"typescript": "~5.6.3",
|
||||
"unocss": "^0.64.1",
|
||||
"unplugin-auto-import": "^0.18.5",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"typescript": "~5.7.2",
|
||||
"unocss": "^0.65.0",
|
||||
"unplugin-auto-import": "^0.18.6",
|
||||
"unplugin-vue-components": "^0.27.5",
|
||||
"unplugin-vue-macros": "^2.13.4",
|
||||
"unplugin-vue-markdown": "^0.26.2",
|
||||
"unplugin-vue-markdown": "^0.27.1",
|
||||
"unplugin-vue-router": "^0.10.8",
|
||||
"vite": "^6.0.1",
|
||||
"vite": "^6.0.2",
|
||||
"vite-bundle-visualizer": "^1.2.1",
|
||||
"vite-plugin-inspect": "^0.10.0",
|
||||
"vite-plugin-pwa": "^0.21.0",
|
||||
"vite-plugin-vue-devtools": "^7.6.4",
|
||||
"vite-plugin-inspect": "^0.10.2",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-vue-devtools": "^7.6.7",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-plugin-webfont-dl": "^3.10.2",
|
||||
"vite-ssg": "^0.24.1",
|
||||
"vite-ssg-sitemap": "^0.8.1",
|
||||
"vitest": "^2.1.6",
|
||||
"vue-tsc": "^2.1.10"
|
||||
"vitest": "^2.1.8",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"resolutions": {
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-inspect": "^0.10.0"
|
||||
"vite": "^6.0.2",
|
||||
"vite-plugin-inspect": "^0.10.2"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
|
||||
Generated
+1928
-1653
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
export function rejectIfError<E = unknown>(error: E | undefined, reject: (error?: E) => void, handler?: (error?: E) => void) {
|
||||
if (error) {
|
||||
reject(error)
|
||||
!!handler && handler(error)
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveWhenNoError<R = void, E = unknown>(reject: (error?: E) => void, resolve: (result?: R) => void) {
|
||||
return (err?: E) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
else {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function onError<E = unknown>(reject: (error?: E) => void, handler?: (error?: E) => void) {
|
||||
return (error?: E) => rejectIfError(error, reject, handler)
|
||||
}
|
||||
|
||||
export function noError<
|
||||
T,
|
||||
U extends unknown[],
|
||||
E = unknown,
|
||||
>(
|
||||
reject: (err?: E) => void,
|
||||
fn: (...args: U) => T,
|
||||
): (err: E | undefined, ...args: U) => T | undefined {
|
||||
return (err, ...args) => {
|
||||
if (err) {
|
||||
rejectIfError(err, reject)
|
||||
return
|
||||
}
|
||||
return fn(...args)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { stat } from 'node:fs/promises'
|
||||
|
||||
export async function exists(path: string) {
|
||||
try {
|
||||
await stat(path)
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
if (isENOENTError(error))
|
||||
return false
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function isENOENTError(error: unknown): boolean {
|
||||
if (!(error instanceof Error))
|
||||
return false
|
||||
if (!('code' in error))
|
||||
return false
|
||||
if (error.code !== 'ENOENT')
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { Buffer } from 'node:buffer'
|
||||
import { createWriteStream, existsSync, mkdirSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fromBuffer } from 'yauzl'
|
||||
import { noError, onError, resolveWhenNoError } from './errors'
|
||||
|
||||
/**
|
||||
* Example:
|
||||
*
|
||||
* await unzip("./tim.zip", "./");
|
||||
*
|
||||
* Will create directories:
|
||||
*
|
||||
* ./tim.zip
|
||||
* ./tim
|
||||
*
|
||||
* Originally by [How to unzip to a folder using yauzl? - Stack Overflow](https://stackoverflow.com/questions/63932027/how-to-unzip-to-a-folder-using-yauzl)
|
||||
*
|
||||
* @param buffer Buffer of the zip file.
|
||||
* @param target Path to the folder where the zip folder will be put.
|
||||
*/
|
||||
export async function unzip(buffer: Buffer, target: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let pendingWrites = 0
|
||||
|
||||
fromBuffer(buffer, { lazyEntries: true }, noError(reject, (zipFile) => {
|
||||
// This is the key. We start by reading the first entry.
|
||||
zipFile.readEntry()
|
||||
|
||||
// Now for every entry, we will write a file or dir
|
||||
// to disk. Then call zipFile.readEntry() again to
|
||||
// trigger the next cycle.
|
||||
zipFile.on('entry', (entry) => {
|
||||
// Directories
|
||||
if (/\/$/.test(entry.fileName)) {
|
||||
// Create the directory then read the next entry.
|
||||
mkdirSync(join(target, entry.fileName), { recursive: true })
|
||||
zipFile.readEntry()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Files
|
||||
const dir = dirname(join(target, entry.fileName))
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
// Write the file to disk.
|
||||
pendingWrites++
|
||||
zipFile.openReadStream(entry, noError(reject, (readStream) => {
|
||||
const file = createWriteStream(join(target, entry.fileName))
|
||||
readStream.pipe(file)
|
||||
|
||||
// Handle errors
|
||||
file.on('error', (err) => {
|
||||
pendingWrites--
|
||||
zipFile.close()
|
||||
reject(err)
|
||||
})
|
||||
|
||||
// Wait until the file is finished writing, then read the next entry.
|
||||
file.on('finish', () => {
|
||||
file.close(() => {
|
||||
pendingWrites--
|
||||
if (pendingWrites === 0) {
|
||||
resolve()
|
||||
}
|
||||
|
||||
zipFile.readEntry()
|
||||
})
|
||||
})
|
||||
}))
|
||||
})
|
||||
|
||||
zipFile.on('error', onError(reject, zipFile.close))
|
||||
zipFile.on('end', resolveWhenNoError(reject, resolve))
|
||||
}))
|
||||
})
|
||||
}
|
||||
+9
-1
@@ -1,4 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useDark, usePreferredDark } from '@vueuse/core'
|
||||
|
||||
const isDark = useDark()
|
||||
const preferredDark = usePreferredDark()
|
||||
// const toggleDark = useToggle(isDark)
|
||||
|
||||
// https://github.com/vueuse/head
|
||||
// you can use this to manipulate the document head in any components,
|
||||
// they will be rendered correctly in the html results with vite-ssg
|
||||
@@ -25,5 +31,7 @@ useHead({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
<ClientOnly>
|
||||
<RouterView />
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
Vendored
+14
-4
@@ -130,6 +130,7 @@ declare global {
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useAudioContext: typeof import('./stores/audio')['useAudioContext']
|
||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
@@ -153,6 +154,7 @@ declare global {
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDelayMessageQueue: typeof import('./composables/queues')['useDelayMessageQueue']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
@@ -166,6 +168,7 @@ declare global {
|
||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||
const useEmotionsMessageQueue: typeof import('./composables/queues')['useEmotionsMessageQueue']
|
||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||
@@ -190,15 +193,18 @@ declare global {
|
||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||
const useLLM: typeof import('./stores/llm')['useLLM']
|
||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||
const useLink: typeof import('vue-router/auto')['useLink']
|
||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||
const useMarkdown: typeof import('./composables/markdown')['useMarkdown']
|
||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||
const useMessageContentQueue: typeof import('./composables/queues')['useMessageContentQueue']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||
@@ -225,6 +231,7 @@ declare global {
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||
const useQueue: typeof import('./composables/queue')['useQueue']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
@@ -343,7 +350,6 @@ declare module 'vue' {
|
||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
||||
readonly isDark: UnwrapRef<typeof import('./composables/dark')['isDark']>
|
||||
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
@@ -373,7 +379,6 @@ declare module 'vue' {
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
|
||||
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
|
||||
readonly preferredDark: UnwrapRef<typeof import('./composables/dark')['preferredDark']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
|
||||
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
|
||||
@@ -405,7 +410,6 @@ declare module 'vue' {
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly toggleDark: UnwrapRef<typeof import('./composables/dark')['toggleDark']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
||||
@@ -432,6 +436,7 @@ declare module 'vue' {
|
||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useAudioContext: UnwrapRef<typeof import('./stores/audio')['useAudioContext']>
|
||||
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
|
||||
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
|
||||
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
|
||||
@@ -455,6 +460,7 @@ declare module 'vue' {
|
||||
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
|
||||
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
|
||||
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
|
||||
readonly useDelayMessageQueue: UnwrapRef<typeof import('./composables/queues')['useDelayMessageQueue']>
|
||||
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
|
||||
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
|
||||
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
|
||||
@@ -468,6 +474,7 @@ declare module 'vue' {
|
||||
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
|
||||
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
|
||||
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
|
||||
readonly useEmotionsMessageQueue: UnwrapRef<typeof import('./composables/queues')['useEmotionsMessageQueue']>
|
||||
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
|
||||
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
|
||||
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
|
||||
@@ -492,15 +499,18 @@ declare module 'vue' {
|
||||
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
|
||||
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||
readonly useLLM: UnwrapRef<typeof import('./stores/llm')['useLLM']>
|
||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
|
||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
|
||||
readonly useMarkdown: UnwrapRef<typeof import('./composables/markdown')['useMarkdown']>
|
||||
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
|
||||
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
|
||||
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
|
||||
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
|
||||
readonly useMessageContentQueue: UnwrapRef<typeof import('./composables/queues')['useMessageContentQueue']>
|
||||
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
|
||||
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
|
||||
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
|
||||
@@ -527,6 +537,7 @@ declare module 'vue' {
|
||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||
readonly useQueue: UnwrapRef<typeof import('./composables/queue')['useQueue']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||
@@ -570,7 +581,6 @@ declare module 'vue' {
|
||||
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
||||
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
||||
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
|
||||
readonly useUserStore: UnwrapRef<typeof import('./stores/user')['useUserStore']>
|
||||
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
|
||||
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
|
||||
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
|
||||
|
||||
Vendored
+4
-4
@@ -7,11 +7,11 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
README: typeof import('./components/README.md')['default']
|
||||
AudioWaveform: typeof import('./components/AudioWaveform.vue')['default']
|
||||
BasicTextarea: typeof import('./components/BasicTextarea.vue')['default']
|
||||
Live2DViewer: typeof import('./components/Live2DViewer.vue')['default']
|
||||
MainStage: typeof import('./components/MainStage.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
TheCounter: typeof import('./components/TheCounter.vue')['default']
|
||||
TheFooter: typeof import('./components/TheFooter.vue')['default']
|
||||
TheInput: typeof import('./components/TheInput.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { useDark, useElementBounding } from '@vueuse/core'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { useAudioContext } from '../stores/audio'
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode
|
||||
const analyser = ref<AnalyserNode>()
|
||||
const analyserDataBuffer = ref<Uint8Array>()
|
||||
const { audioContext } = useAudioContext()
|
||||
const canvasElemRef = ref<HTMLCanvasElement>()
|
||||
const isDark = useDark()
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode/playbackRate
|
||||
// explain: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API
|
||||
// reference: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Simple_synth
|
||||
function fetchAnalyserDataDuringFrames() {
|
||||
if (!analyser.value || !analyserDataBuffer.value || !canvasElemRef.value)
|
||||
return
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame
|
||||
requestAnimationFrame(fetchAnalyserDataDuringFrames)
|
||||
if (analyserDataBuffer.value.length > 60 * 2)
|
||||
analyserDataBuffer.value = new Uint8Array(analyser.value.frequencyBinCount)
|
||||
|
||||
analyser.value.getByteTimeDomainData(analyserDataBuffer.value)
|
||||
|
||||
const context = canvasElemRef.value.getContext('2d')!
|
||||
|
||||
if (isDark.value)
|
||||
context.fillStyle = 'rgba(34, 34, 34, 1)'
|
||||
else
|
||||
context.fillStyle = 'rgba(255, 255, 255, 1)'
|
||||
|
||||
context.fillRect(0, 0, canvasElemRef.value.width, canvasElemRef.value.height)
|
||||
|
||||
context.lineWidth = 2
|
||||
if (isDark.value)
|
||||
context.strokeStyle = 'rgb(255 255 255)'
|
||||
else
|
||||
context.strokeStyle = 'rgb(0 0 0)'
|
||||
|
||||
context.beginPath()
|
||||
|
||||
const sliceWidth = (canvasElemRef.value.width * 1.0) / analyser.value.frequencyBinCount
|
||||
let x = 0
|
||||
|
||||
for (let i = 0; i < analyser.value.frequencyBinCount; i++) {
|
||||
const v = analyserDataBuffer.value[i] / 128.0
|
||||
const y = (v * canvasElemRef.value.height) / 2
|
||||
|
||||
if (i === 0)
|
||||
context.moveTo(x, y)
|
||||
else
|
||||
context.lineTo(x, y)
|
||||
|
||||
x += sliceWidth
|
||||
}
|
||||
|
||||
context.lineTo(canvasElemRef.value.width, canvasElemRef.value.height / 2)
|
||||
context.stroke()
|
||||
}
|
||||
|
||||
function initAnalyser() {
|
||||
analyser.value = audioContext.createAnalyser()
|
||||
analyserDataBuffer.value = new Uint8Array(analyser.value.frequencyBinCount)
|
||||
analyser.value.getByteTimeDomainData(analyserDataBuffer.value)
|
||||
const windowAny = window as any
|
||||
windowAny.analyserDataBuffer = analyserDataBuffer
|
||||
|
||||
fetchAnalyserDataDuringFrames()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
analyser: () => analyser.value,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!containerRef.value || !canvasElemRef.value)
|
||||
return
|
||||
|
||||
const containerElementBounding = useElementBounding(containerRef.value)
|
||||
containerElementBounding.update()
|
||||
|
||||
initAnalyser()
|
||||
|
||||
canvasElemRef.value.width = containerElementBounding.width.value
|
||||
canvasElemRef.value.height = containerElementBounding.height.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" h="[80px]" w-full>
|
||||
<canvas ref="canvasElemRef" h-full w-full />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts" generic="T extends any, O extends any">
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
const events = defineEmits<{
|
||||
(event: 'submit', message: string): void
|
||||
}>()
|
||||
|
||||
const input = defineModel<string>({
|
||||
default: '',
|
||||
})
|
||||
|
||||
const textareaRef = ref<HTMLTextAreaElement>()
|
||||
const textareaStyle = ref<CSSProperties>({
|
||||
height: 'auto',
|
||||
overflowY: 'hidden',
|
||||
})
|
||||
|
||||
// javascript - Creating a textarea with auto-resize - Stack Overflow
|
||||
// https://stackoverflow.com/questions/454202/creating-a-textarea-with-auto-resize
|
||||
function onInput(e: Event) {
|
||||
if (!(e.target instanceof HTMLTextAreaElement))
|
||||
return
|
||||
|
||||
e.target.style.height = 'auto'
|
||||
e.target.style.height = `${e.target.scrollHeight}px`
|
||||
}
|
||||
|
||||
// javascript - How do I detect "shift+enter" and generate a new line in Textarea? - Stack Overflow
|
||||
// https://stackoverflow.com/questions/6014702/how-do-i-detect-shiftenter-and-generate-a-new-line-in-textarea
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (!(e.target instanceof HTMLTextAreaElement))
|
||||
return
|
||||
|
||||
if (e.code === 'Enter' && e.shiftKey) {
|
||||
e.preventDefault()
|
||||
const start = e.target?.selectionStart
|
||||
const end = e.target?.selectionEnd
|
||||
input.value = `${input.value.substring(0, start)}\n${input.value.substring(end)}`
|
||||
|
||||
// javascript - height of textarea increases when value increased but does not reduce when value is decreased - Stack Overflow
|
||||
// https://stackoverflow.com/questions/10722058/height-of-textarea-increases-when-value-increased-but-does-not-reduce-when-value
|
||||
textareaStyle.value.height = '0'
|
||||
|
||||
nextTick().then(() => {
|
||||
if (!textareaRef.value)
|
||||
return
|
||||
|
||||
textareaRef.value.selectionStart = textareaRef.value.selectionEnd = start + 1
|
||||
textareaStyle.value.height = `${textareaRef.value.scrollHeight}px`
|
||||
})
|
||||
}
|
||||
else if (e.code === 'Enter') { // block enter
|
||||
e.preventDefault()
|
||||
events('submit', input.value)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!textareaRef.value)
|
||||
return
|
||||
|
||||
textareaStyle.value.height = `${textareaRef.value.scrollHeight}px`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="input"
|
||||
:style="textareaStyle"
|
||||
@input="onInput"
|
||||
@keydown="onKeyDown"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { Application } from '@pixi/app'
|
||||
import { extensions } from '@pixi/extensions'
|
||||
import { Ticker, TickerPlugin } from '@pixi/ticker'
|
||||
import { useElementBounding, useWindowSize } from '@vueuse/core'
|
||||
import { Live2DModel, MotionPreloadStrategy, MotionPriority } from 'pixi-live2d-display/cubism4'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
model: string
|
||||
mouthOpenSize?: number
|
||||
}>(), {
|
||||
mouthOpenSize: 0,
|
||||
})
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const pixiApp = ref<Application>()
|
||||
const pixiAppCanvas = ref<HTMLCanvasElement>()
|
||||
const model = ref<Live2DModel>()
|
||||
const mouthOpenSize = computed(() => {
|
||||
return Math.max(0, Math.min(100, props.mouthOpenSize))
|
||||
})
|
||||
|
||||
const { width, height } = useWindowSize()
|
||||
const containerElementBounding = useElementBounding(containerRef)
|
||||
const containerParentElementBounding = useElementBounding(containerRef.value?.parentElement)
|
||||
|
||||
function getCoreModel() {
|
||||
return model.value!.internalModel.coreModel as any
|
||||
}
|
||||
|
||||
async function initLive2DPixiStage(parent: HTMLDivElement) {
|
||||
containerElementBounding.update()
|
||||
containerParentElementBounding.update()
|
||||
|
||||
// https://guansss.github.io/pixi-live2d-display/#package-importing
|
||||
Live2DModel.registerTicker(Ticker)
|
||||
extensions.add(TickerPlugin)
|
||||
|
||||
pixiApp.value = new Application({
|
||||
width: containerElementBounding.width.value,
|
||||
height: Math.max(600, containerParentElementBounding.height.value),
|
||||
backgroundAlpha: 0,
|
||||
})
|
||||
|
||||
pixiAppCanvas.value = pixiApp.value.view
|
||||
parent.appendChild(pixiApp.value.view)
|
||||
|
||||
model.value = await Live2DModel.from(props.model, { motionPreload: MotionPreloadStrategy.ALL })
|
||||
pixiApp.value.stage.addChild(model.value as any)
|
||||
|
||||
model.value.x = containerElementBounding.width.value / 2
|
||||
model.value.y = Math.max(600, containerParentElementBounding.height.value)
|
||||
model.value.rotation = Math.PI
|
||||
model.value.skew.x = Math.PI
|
||||
model.value.scale.set(0.3, 0.3)
|
||||
model.value.anchor.set(0.5, 0.5)
|
||||
|
||||
model.value.on('hit', (hitAreas) => {
|
||||
if (model.value && hitAreas.includes('body'))
|
||||
model.value.motion('tap_body')
|
||||
})
|
||||
|
||||
const coreModel = model.value.internalModel.coreModel as any
|
||||
coreModel.setParameterValueById('ParamMouthOpenY', mouthOpenSize.value)
|
||||
}
|
||||
|
||||
async function setMotion(motionName: string) {
|
||||
await model.value!.motion(motionName, undefined, MotionPriority.FORCE)
|
||||
}
|
||||
|
||||
watch([width, height], () => {
|
||||
if (pixiApp.value)
|
||||
pixiApp.value.renderer.resize((width.value - 16) / 2, 550)
|
||||
|
||||
if (pixiAppCanvas.value) {
|
||||
pixiAppCanvas.value.width = (width.value - 16) / 2
|
||||
pixiAppCanvas.value.height = Math.max(600, containerParentElementBounding.height.value)
|
||||
}
|
||||
|
||||
if (model.value) {
|
||||
model.value.x = (width.value - 16) / 4
|
||||
model.value.y = Math.max(600, containerParentElementBounding.height.value)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!containerRef.value)
|
||||
return
|
||||
|
||||
await initLive2DPixiStage(containerRef.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
pixiApp.value?.destroy()
|
||||
})
|
||||
|
||||
watch(mouthOpenSize, (value) => {
|
||||
getCoreModel().setParameterValueById('ParamMouthOpenY', value)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
setMotion,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" h-full w-full />
|
||||
</template>
|
||||
@@ -0,0 +1,392 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
CoreAssistantMessage,
|
||||
CoreSystemMessage,
|
||||
CoreUserMessage,
|
||||
} from 'ai'
|
||||
import type {
|
||||
Emotion,
|
||||
} from '../constants/emotions'
|
||||
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import Avatar from '../assets/live2d/models/hiyori_free_zh/avatar.png'
|
||||
import { useMarkdown } from '../composables/markdown'
|
||||
|
||||
import { useQueue } from '../composables/queue'
|
||||
import {
|
||||
useDelayMessageQueue,
|
||||
useEmotionsMessageQueue,
|
||||
useMessageContentQueue,
|
||||
} from '../composables/queues'
|
||||
import { llmInferenceEndToken } from '../constants'
|
||||
import {
|
||||
EMOTION_EmotioMotionName_value,
|
||||
EmotionThinkMotionName,
|
||||
} from '../constants/emotions'
|
||||
import SystemPromptV2 from '../constants/prompts/system-v2'
|
||||
import { useLLM } from '../stores/llm'
|
||||
|
||||
import BasicTextarea from './BasicTextarea.vue'
|
||||
// import AudioWaveform from './AudioWaveform.vue'
|
||||
import Live2DViewer from './Live2DViewer.vue'
|
||||
|
||||
const nowSpeakingAvatarBorderOpacityMin = 30
|
||||
const nowSpeakingAvatarBorderOpacityMax = 100
|
||||
|
||||
const openAiApiKey = useLocalStorage('openai-api-key', '')
|
||||
const openAiApiBaseURL = useLocalStorage('openai-api-base-url', '')
|
||||
const openAIModel = useLocalStorage<{ id: string, name?: string }>('openai-model', { id: 'openai/gpt-3.5-turbo', name: 'OpenAI GPT3.5 Turbo' })
|
||||
const elevenLabsApiKey = useLocalStorage('elevenlabs-api-key', '')
|
||||
|
||||
const {
|
||||
setupOpenAI,
|
||||
// streamSpeech,
|
||||
stream,
|
||||
models,
|
||||
} = useLLM()
|
||||
const { audioContext, calculateVolume } = useAudioContext()
|
||||
const { process } = useMarkdown()
|
||||
|
||||
const listening = ref(false)
|
||||
const live2DViewerRef = ref<{ setMotion: (motionName: string) => Promise<void> }>()
|
||||
const supportedModels = ref<{ id: string, name?: string }[]>([])
|
||||
const messageInput = ref<string>('')
|
||||
const messages = ref<Array<CoreAssistantMessage | CoreUserMessage | CoreSystemMessage>>([SystemPromptV2 as CoreSystemMessage])
|
||||
const streamingMessage = ref<CoreAssistantMessage>({ role: 'assistant', content: '' })
|
||||
const audioAnalyser = ref<AnalyserNode>()
|
||||
const mouthOpenSize = ref(0)
|
||||
const nowSpeaking = ref(false)
|
||||
const model = ref('')
|
||||
const lipSyncStarted = ref(false)
|
||||
|
||||
const nowSpeakingAvatarBorderOpacity = computed<number>(() => {
|
||||
if (!nowSpeaking.value)
|
||||
return nowSpeakingAvatarBorderOpacityMin
|
||||
|
||||
return ((nowSpeakingAvatarBorderOpacityMin
|
||||
+ (nowSpeakingAvatarBorderOpacityMax - nowSpeakingAvatarBorderOpacityMin) * mouthOpenSize.value) / 100)
|
||||
})
|
||||
|
||||
function handleModelChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const found = supportedModels.value.find(m => m.id === target.value)
|
||||
if (!found) {
|
||||
openAIModel.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
openAIModel.value = found
|
||||
}
|
||||
|
||||
// const audioQueue = useQueue<{ audioBuffer: AudioBuffer, text: string }>({
|
||||
// handlers: [
|
||||
// (ctx) => {
|
||||
// return new Promise((resolve) => {
|
||||
// // Create an AudioBufferSourceNode
|
||||
// const source = audioContext.createBufferSource()
|
||||
// source.buffer = ctx.data.audioBuffer
|
||||
|
||||
// // Connect the source to the AudioContext's destination (the speakers)
|
||||
// source.connect(audioContext.destination)
|
||||
// // Connect the source to the analyzer
|
||||
// source.connect(audioAnalyser.value!)
|
||||
|
||||
// // Start playing the audio
|
||||
// nowSpeaking.value = true
|
||||
// source.start(0)
|
||||
// source.onended = () => {
|
||||
// nowSpeaking.value = false
|
||||
// resolve()
|
||||
// }
|
||||
// })
|
||||
// },
|
||||
// ],
|
||||
// })
|
||||
|
||||
const ttsQueue = useQueue<string>({
|
||||
handlers: [
|
||||
async () => {
|
||||
// TODO: migrating to ElevenLabs API, but we need TTS SDK to wrap the API
|
||||
|
||||
// const now = Date.now()
|
||||
// const res = await streamSpeech(ctx.data, elevenLabsApiKey.value)
|
||||
// const elapsed = Date.now() - now
|
||||
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.debug('TTS took', elapsed, 'ms')
|
||||
|
||||
// Decode the ArrayBuffer into an AudioBuffer
|
||||
// const audioBuffer = await audioContext.decodeAudioData(res)
|
||||
// await audioQueue.add({ audioBuffer, text: ctx.data })
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
ttsQueue.on('add', (content) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('ttsQueue added', content)
|
||||
})
|
||||
|
||||
const messageContentQueue = useMessageContentQueue(ttsQueue)
|
||||
|
||||
const emotionsQueue = useQueue<Emotion>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
await live2DViewerRef.value!.setMotion(EMOTION_EmotioMotionName_value[ctx.data])
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const emotionMessageContentQueue = useEmotionsMessageQueue(emotionsQueue, messageContentQueue)
|
||||
emotionMessageContentQueue.onHandlerEvent('emotion', (emotion) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('emotion detected', emotion)
|
||||
})
|
||||
|
||||
const delaysQueue = useDelayMessageQueue(emotionMessageContentQueue)
|
||||
delaysQueue.onHandlerEvent('delay', (delay) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('delay detected', delay)
|
||||
})
|
||||
|
||||
function getVolumeWithMinMaxNormalizeWithFrameUpdates() {
|
||||
requestAnimationFrame(getVolumeWithMinMaxNormalizeWithFrameUpdates)
|
||||
if (!nowSpeaking.value)
|
||||
return
|
||||
|
||||
mouthOpenSize.value = calculateVolume(audioAnalyser.value!, 'linear')
|
||||
}
|
||||
|
||||
function setupLipSync() {
|
||||
if (!lipSyncStarted.value) {
|
||||
getVolumeWithMinMaxNormalizeWithFrameUpdates()
|
||||
audioContext.resume()
|
||||
lipSyncStarted.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function setupAnalyser() {
|
||||
if (!audioAnalyser.value)
|
||||
audioAnalyser.value = audioContext.createAnalyser()
|
||||
}
|
||||
|
||||
async function onSendMessage(sendingMessage: string) {
|
||||
if (!sendingMessage)
|
||||
return
|
||||
|
||||
setupLipSync()
|
||||
setupAnalyser()
|
||||
|
||||
streamingMessage.value = { role: 'assistant', content: '' }
|
||||
messages.value.push({ role: 'user', content: sendingMessage })
|
||||
messages.value.push(streamingMessage.value)
|
||||
// const index = messages.value.length - 1
|
||||
live2DViewerRef.value?.setMotion(EmotionThinkMotionName)
|
||||
|
||||
const res = await stream(model.value, messages.value.slice(0, messages.value.length - 1))
|
||||
|
||||
enum States {
|
||||
Literal = 'literal',
|
||||
Special = 'special',
|
||||
}
|
||||
|
||||
let state = States.Literal
|
||||
let buffer = ''
|
||||
|
||||
for await (const textPart of res.textStream) {
|
||||
for (const textSingleChar of textPart) {
|
||||
let newState: States = state
|
||||
|
||||
if (textSingleChar === '<')
|
||||
newState = States.Special
|
||||
else if (textSingleChar === '>')
|
||||
newState = States.Literal
|
||||
|
||||
if (state === States.Literal && newState === States.Special) {
|
||||
streamingMessage.value.content += buffer
|
||||
buffer = ''
|
||||
}
|
||||
|
||||
if (state === States.Special && newState === States.Literal)
|
||||
buffer = '' // Clear buffer when exiting Special state
|
||||
|
||||
if (state === States.Literal && newState === States.Literal) {
|
||||
streamingMessage.value.content += textSingleChar
|
||||
buffer = ''
|
||||
}
|
||||
|
||||
await delaysQueue.add(textSingleChar)
|
||||
state = newState
|
||||
buffer += textSingleChar
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer)
|
||||
streamingMessage.value.content += buffer
|
||||
|
||||
await delaysQueue.add(llmInferenceEndToken)
|
||||
|
||||
messageInput.value = ''
|
||||
}
|
||||
|
||||
watch(openAiApiKey, async (value) => {
|
||||
setupOpenAI({
|
||||
apiKey: value,
|
||||
baseURL: openAiApiBaseURL.value,
|
||||
})
|
||||
|
||||
const fetchedModels = await models()
|
||||
supportedModels.value = fetchedModels.data
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!openAiApiKey.value)
|
||||
return
|
||||
|
||||
setupOpenAI({
|
||||
apiKey: openAiApiKey.value,
|
||||
baseURL: openAiApiBaseURL.value,
|
||||
})
|
||||
|
||||
const fetchedModels = await models()
|
||||
supportedModels.value = fetchedModels.data
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
lipSyncStarted.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div max-h="[100vh]" h-full p="2" flex="~ col">
|
||||
<div space-x="2" flex="~ row" w-full>
|
||||
<div flex="~ row" w-full>
|
||||
<input
|
||||
v-model="openAiApiBaseURL"
|
||||
placeholder="Input your API base URL"
|
||||
p="2" bg="zinc-100 dark:zinc-700" w-full rounded-lg outline-none
|
||||
>
|
||||
</div>
|
||||
<div flex="~ row" w-full>
|
||||
<input
|
||||
v-model="openAiApiKey"
|
||||
placeholder="Input your API key"
|
||||
p="2" bg="zinc-100 dark:zinc-700" w-full rounded-lg outline-none
|
||||
>
|
||||
</div>
|
||||
<div flex="~ row" w-full>
|
||||
<input
|
||||
v-model="elevenLabsApiKey"
|
||||
placeholder="Input your ElevenLabs API key"
|
||||
p="2" bg="zinc-100 dark:zinc-700" w-full rounded-lg outline-none
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div flex="~ row 1" w-full items-end space-x-2>
|
||||
<div w-full min-h="100 sm:100">
|
||||
<Live2DViewer ref="live2DViewerRef" :mouth-open-size="mouthOpenSize" model="/assets/live2d/models/hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json" />
|
||||
<!-- <div>
|
||||
<input v-model.number="mouthOpenSize" type="range" max="1" min="0" step="0.01">
|
||||
<span>{{ mouthOpenSize }}</span>
|
||||
</div> -->
|
||||
<!-- <AudioWaveform ref="audioWaveformRef" /> -->
|
||||
</div>
|
||||
<div my="2" w-full space-y-2 max-h="[calc(100vh-117px)]">
|
||||
<div v-for="(message, index) in messages" :key="index">
|
||||
<div v-if="message.role === 'assistant'" flex mr="12">
|
||||
<div
|
||||
mr-2 h-10 min-h-10 min-w-10 w-10 overflow-hidden rounded-full
|
||||
border="solid 3"
|
||||
transition="all ease-in-out" duration-100
|
||||
:style="{
|
||||
borderColor: `rgba(236, 72, 153, ${nowSpeakingAvatarBorderOpacity.toFixed(2)})`,
|
||||
}"
|
||||
>
|
||||
<img :src="Avatar">
|
||||
</div>
|
||||
<div flex="~ col" bg="pink-50/50 dark:pink-900/50" p="2" border="2 solid pink/10" rounded-lg>
|
||||
<div>
|
||||
<span font-semibold>Neuro</span>
|
||||
</div>
|
||||
<div v-html="process(message.content as string)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="message.role === 'user'" flex="~ row-reverse" ml="12">
|
||||
<div border="purple solid 3" ml="2" h-10 min-h-10 min-w-10 w-10 overflow-hidden rounded-full>
|
||||
<div i-carbon:user-avatar-filled text="purple" h-full w-full p="0" m="0" />
|
||||
</div>
|
||||
<div flex="~ col" bg="purple-50/50 dark:purple-900/50" p="2" border="2 solid pink/10" rounded-lg>
|
||||
<div>
|
||||
<span font-semibold>You</span>
|
||||
</div>
|
||||
<div v-html="process(message.content as string)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div my="2" space-x="2" flex="~ row" w-full self-end>
|
||||
<div flex="~ col" w-full space-y="2">
|
||||
<select
|
||||
p="2"
|
||||
bg="zinc-100 dark:zinc-700" w-full rounded-lg
|
||||
outline-none
|
||||
@change="handleModelChange"
|
||||
>
|
||||
<option disabled>
|
||||
Select a model
|
||||
</option>
|
||||
<option v-if="openAIModel" :value="openAIModel.id">
|
||||
{{ 'name' in openAIModel ? `${openAIModel.name} (${openAIModel.id})` : openAIModel.id }}
|
||||
</option>
|
||||
<option v-for="m in supportedModels" :key="m.id" :value="m.id">
|
||||
{{ 'name' in m ? `${m.name} (${m.id})` : m.id }}
|
||||
</option>
|
||||
</select>
|
||||
<div absolute bottom="5" left="50%" translate-x="-50%">
|
||||
<button
|
||||
bg="zinc-100 dark:zinc-700" flex="~ row"
|
||||
items-center rounded-full px-4 py-2
|
||||
transition="all ease-in-out"
|
||||
@click="listening = !listening"
|
||||
>
|
||||
<Transition mode="out-in">
|
||||
<div v-if="listening" flex="~ row" items-center space-x-1>
|
||||
<div i-carbon:microphone-filled text-red />
|
||||
<span>
|
||||
Listening...
|
||||
</span>
|
||||
</div>
|
||||
<div v-else flex="~ row" items-center space-x-1>
|
||||
<div i-carbon:microphone text-inherit />
|
||||
<span>
|
||||
Talk
|
||||
</span>
|
||||
</div>
|
||||
</Transition>
|
||||
</button>
|
||||
</div>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
placeholder="Message"
|
||||
p="2" bg="zinc-100 dark:zinc-700"
|
||||
w-full rounded-lg outline-none
|
||||
@submit="onSendMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +0,0 @@
|
||||
## Components
|
||||
|
||||
Components in this dir will be auto-registered and on-demand, powered by [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components).
|
||||
|
||||
### Icons
|
||||
|
||||
You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/).
|
||||
|
||||
It will only bundle the icons you use. Check out [`unplugin-icons`](https://github.com/antfu/unplugin-icons) for more details.
|
||||
@@ -1,19 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
initial: number
|
||||
}>()
|
||||
|
||||
const { count, inc, dec } = useCounter(props.initial)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ count }}
|
||||
<button class="inc" @click="inc()">
|
||||
+
|
||||
</button>
|
||||
<button class="dec" @click="dec()">
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,37 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { availableLocales, loadLanguageAsync } from '~/modules/i18n'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
async function toggleLocales() {
|
||||
// change to some real logic
|
||||
const locales = availableLocales
|
||||
const newLocale = locales[(locales.indexOf(locale.value) + 1) % locales.length]
|
||||
await loadLanguageAsync(newLocale)
|
||||
locale.value = newLocale
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav flex="~ gap-4" mt-6 justify-center text-xl>
|
||||
<RouterLink icon-btn to="/" :title="t('button.home')">
|
||||
<div i-carbon-campsite />
|
||||
</RouterLink>
|
||||
|
||||
<button icon-btn :title="t('button.toggle_dark')" @click="toggleDark()">
|
||||
<div i="carbon-sun dark:carbon-moon" />
|
||||
</button>
|
||||
|
||||
<a icon-btn :title="t('button.toggle_langs')" @click="toggleLocales()">
|
||||
<div i-carbon-language />
|
||||
</a>
|
||||
|
||||
<RouterLink icon-btn to="/about" :title="t('button.about')" data-test-id="about">
|
||||
<div i-carbon-dicom-overlay />
|
||||
</RouterLink>
|
||||
|
||||
<a icon-btn rel="noreferrer" href="https://github.com/antfu/vitesse" target="_blank" title="GitHub">
|
||||
<div i-carbon-logo-github />
|
||||
</a>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const { modelValue } = defineModels<{
|
||||
modelValue: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
id="input"
|
||||
v-model="modelValue"
|
||||
v-bind="$attrs"
|
||||
type="text"
|
||||
p="x-4 y-2"
|
||||
w="250px"
|
||||
text="center"
|
||||
bg="transparent"
|
||||
border="~ rounded gray-200 dark:gray-700"
|
||||
outline="none active:none"
|
||||
>
|
||||
</template>
|
||||
@@ -1,4 +0,0 @@
|
||||
// these APIs are auto-imported from @vueuse/core
|
||||
export const isDark = useDark()
|
||||
export const toggleDark = useToggle(isDark)
|
||||
export const preferredDark = usePreferredDark()
|
||||
@@ -0,0 +1,18 @@
|
||||
import RehypeStringify from 'rehype-stringify'
|
||||
import RemarkParse from 'remark-parse'
|
||||
import RemarkRehype from 'remark-rehype'
|
||||
import { unified } from 'unified'
|
||||
|
||||
export function useMarkdown() {
|
||||
const instance = unified()
|
||||
.use(RemarkParse)
|
||||
.use(RemarkRehype)
|
||||
.use(RehypeStringify)
|
||||
return {
|
||||
process: (markdown: string): string => {
|
||||
return instance
|
||||
.processSync(markdown)
|
||||
.toString()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface HandlerContext<T> {
|
||||
data: T
|
||||
itemsToBeProcessed: () => number
|
||||
emit: (eventName: string, ...params: any[]) => void
|
||||
}
|
||||
|
||||
interface Events<T> {
|
||||
add: Array<(payload: T) => void>
|
||||
pick: Array<(payload: T) => void>
|
||||
processing: Array<(payload: T, handler: (param: HandlerContext<T>) => Promise<any>) => void>
|
||||
error: Array<(payload: T, error: Error, handler: (param: HandlerContext<T>) => Promise<any>) => void>
|
||||
processed: Array<<R>(payload: T, result: R, handler: (param: HandlerContext<T>) => Promise<any>) => void>
|
||||
done: Array<(payload: T) => void>
|
||||
}
|
||||
|
||||
export function useQueue<T>(options: {
|
||||
handlers: Array<(ctx: HandlerContext<T>) => Promise<void>>
|
||||
}) {
|
||||
const queue = ref<T[]>([]) as Ref<T[]>
|
||||
const isProcessing = ref(false)
|
||||
const internalEventHandler: Events<T> = {
|
||||
add: [],
|
||||
pick: [],
|
||||
processing: [],
|
||||
error: [],
|
||||
processed: [],
|
||||
done: [],
|
||||
}
|
||||
const internalHandlerEventHandler: Record<string, Array<(...params: any[]) => void>> = {}
|
||||
|
||||
function on<E extends keyof Events<T>>(eventName: E, handler: Events<T>[E][number]) {
|
||||
internalEventHandler[eventName].push(handler as any)
|
||||
}
|
||||
|
||||
function emit<E extends keyof Events<T>>(eventName: E, ...params: Parameters<Events<T>[E][number]>) {
|
||||
const handlers = internalEventHandler[eventName] as Events<T>[E]
|
||||
handlers.forEach((handler) => {
|
||||
(handler as any)(...params)
|
||||
})
|
||||
}
|
||||
|
||||
function onHandlerEvent(eventName: string, handler: (...params: any[]) => void) {
|
||||
internalHandlerEventHandler[eventName] = internalHandlerEventHandler[eventName] || []
|
||||
internalHandlerEventHandler[eventName].push(handler)
|
||||
}
|
||||
|
||||
function emitHandlerEvent(eventName: string, ...params: any[]) {
|
||||
const handlers = internalHandlerEventHandler[eventName] || []
|
||||
handlers.forEach((handler) => {
|
||||
handler(...params)
|
||||
})
|
||||
}
|
||||
|
||||
async function add(payload: T) {
|
||||
queue.value.push(payload)
|
||||
emit('add', payload)
|
||||
}
|
||||
|
||||
function pick() {
|
||||
const payload = queue.value.shift()
|
||||
if (!payload)
|
||||
return
|
||||
|
||||
emit('pick', payload)
|
||||
return payload
|
||||
}
|
||||
|
||||
async function handleItem() {
|
||||
if (isProcessing.value)
|
||||
return
|
||||
|
||||
const payload = pick()
|
||||
if (!payload)
|
||||
return
|
||||
|
||||
isProcessing.value = true
|
||||
|
||||
for (const handler of options.handlers) {
|
||||
emit('processing', payload, handler)
|
||||
try {
|
||||
const result = await handler({ data: payload, itemsToBeProcessed: () => queue.value.length, emit: emitHandlerEvent })
|
||||
emit('processed', payload, result, handler)
|
||||
}
|
||||
catch (err) {
|
||||
emit('error', payload, err as Error, handler)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
isProcessing.value = false
|
||||
emit('done', payload)
|
||||
|
||||
// Process next item if any
|
||||
if (queue.value.length > 0)
|
||||
handleItem()
|
||||
}
|
||||
|
||||
on('add', handleItem)
|
||||
on('done', handleItem)
|
||||
|
||||
return {
|
||||
add,
|
||||
on,
|
||||
onHandlerEvent,
|
||||
queue,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import type { Emotion } from '../constants/emotions'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { llmInferenceEndToken } from '../constants'
|
||||
import { EMOTION_VALUES } from '../constants/emotions'
|
||||
import { useQueue } from './queue'
|
||||
|
||||
export function useEmotionsMessageQueue(emotionsQueue: ReturnType<typeof useQueue<Emotion>>, messageContentQueue: ReturnType<typeof useQueue<string>>) {
|
||||
function splitEmotion(content: string) {
|
||||
for (const emotion of EMOTION_VALUES) {
|
||||
// doesn't include the emotion, continue
|
||||
if (!content.includes(emotion))
|
||||
continue
|
||||
|
||||
// find the emotion and push the content before the emotion to the queue
|
||||
const emotionIndex = content.indexOf(emotion)
|
||||
const beforeEmotion = content.slice(0, emotionIndex)
|
||||
const afterEmotion = content.slice(emotionIndex + emotion.length)
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
emotion: emotion as Emotion,
|
||||
before: beforeEmotion,
|
||||
after: afterEmotion,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
emotion: '' as Emotion,
|
||||
before: content,
|
||||
after: '',
|
||||
}
|
||||
}
|
||||
|
||||
const processed = ref<string>('')
|
||||
|
||||
return useQueue<string>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
// inference ended, push the last content to the message queue
|
||||
if (ctx.data.includes(llmInferenceEndToken)) {
|
||||
const content = processed.value.trim()
|
||||
if (content)
|
||||
await messageContentQueue.add(content)
|
||||
|
||||
processed.value = ''
|
||||
|
||||
return
|
||||
}
|
||||
// if the message is an emotion, push the last content to the message queue
|
||||
if (EMOTION_VALUES.includes(ctx.data as Emotion)) {
|
||||
const content = processed.value.trim()
|
||||
if (content)
|
||||
await messageContentQueue.add(content)
|
||||
|
||||
processed.value = ''
|
||||
ctx.emit('emotion', ctx.data as Emotion)
|
||||
await emotionsQueue.add(ctx.data as Emotion)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise we should process the message to find the emotions
|
||||
|
||||
{
|
||||
// iterate through the message to find the emotions
|
||||
const { ok, before, emotion, after } = splitEmotion(ctx.data)
|
||||
if (ok) {
|
||||
await messageContentQueue.add(before)
|
||||
ctx.emit('emotion', emotion)
|
||||
await emotionsQueue.add(emotion)
|
||||
await messageContentQueue.add(after)
|
||||
processed.value = ''
|
||||
|
||||
return
|
||||
}
|
||||
else {
|
||||
// if none of the emotions are found, push the content to the temp queue
|
||||
processed.value += ctx.data
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through the message to find the emotions
|
||||
{
|
||||
const { ok, before, emotion, after } = splitEmotion(processed.value)
|
||||
if (ok) {
|
||||
await messageContentQueue.add(before)
|
||||
ctx.emit('emotion', emotion)
|
||||
await emotionsQueue.add(emotion)
|
||||
await messageContentQueue.add(after)
|
||||
processed.value = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export function useDelayMessageQueue(useEmotionsMessageQueue: ReturnType<typeof useQueue<string>>) {
|
||||
function splitDelays(content: string) {
|
||||
// doesn't include the emotion, continue
|
||||
if (!(/<\|DELAY:\d+\|>/i.test(content))) {
|
||||
return {
|
||||
ok: false,
|
||||
delay: 0,
|
||||
before: content,
|
||||
after: '',
|
||||
}
|
||||
}
|
||||
|
||||
const delayExecArray = /<\|DELAY:(\d+)\|>/i.exec(content)
|
||||
|
||||
const delay = delayExecArray?.[1]
|
||||
if (!delay) {
|
||||
return {
|
||||
ok: false,
|
||||
delay: 0,
|
||||
before: content,
|
||||
after: '',
|
||||
}
|
||||
}
|
||||
|
||||
const delaySeconds = Number.parseFloat(delay)
|
||||
const before = content.split(delayExecArray[0])[0]
|
||||
const after = content.split(delayExecArray[0])[1]
|
||||
|
||||
if (delaySeconds <= 0 || Number.isNaN(delaySeconds)) {
|
||||
return {
|
||||
ok: true,
|
||||
delay: 0,
|
||||
before,
|
||||
after,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
delay: delaySeconds,
|
||||
before,
|
||||
after,
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
const delaysQueueProcessedTemp = ref<string>('')
|
||||
return useQueue<string>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
// inference ended, push the last content to the message queue
|
||||
if (ctx.data.includes(llmInferenceEndToken)) {
|
||||
const content = delaysQueueProcessedTemp.value.trim()
|
||||
if (content)
|
||||
await useEmotionsMessageQueue.add(content)
|
||||
|
||||
delaysQueueProcessedTemp.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
{
|
||||
// iterate through the message to find the emotions
|
||||
const { ok, before, delay, after } = splitDelays(ctx.data)
|
||||
if (ok && before) {
|
||||
await useEmotionsMessageQueue.add(before)
|
||||
|
||||
if (delay) {
|
||||
ctx.emit('delay', delay)
|
||||
await sleep(delay * 1000)
|
||||
}
|
||||
|
||||
if (after)
|
||||
await useEmotionsMessageQueue.add(after)
|
||||
}
|
||||
else {
|
||||
// if none of the emotions are found, push the content to the temp queue
|
||||
delaysQueueProcessedTemp.value += ctx.data
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through the message to find the emotions
|
||||
{
|
||||
const { ok, before, delay, after } = splitDelays(delaysQueueProcessedTemp.value)
|
||||
if (ok && before) {
|
||||
await useEmotionsMessageQueue.add(before)
|
||||
|
||||
if (delay) {
|
||||
ctx.emit('delay', delay)
|
||||
await sleep(delay * 1000)
|
||||
}
|
||||
|
||||
if (after)
|
||||
await useEmotionsMessageQueue.add(after)
|
||||
delaysQueueProcessedTemp.value = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export function useMessageContentQueue(ttsQueue: ReturnType<typeof useQueue<string>>) {
|
||||
const processed = ref<string>('')
|
||||
|
||||
return useQueue<string>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
if (ctx.data === llmInferenceEndToken) {
|
||||
const content = processed.value.trim()
|
||||
if (content)
|
||||
await ttsQueue.add(content)
|
||||
|
||||
processed.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const endMarker = /[.?!]/
|
||||
processed.value += ctx.data
|
||||
|
||||
while (processed.value) {
|
||||
const endMarkerExecArray = endMarker.exec(processed.value)
|
||||
if (!endMarkerExecArray || typeof endMarkerExecArray.index === 'undefined')
|
||||
break
|
||||
|
||||
const before = processed.value.slice(0, endMarkerExecArray.index + 1)
|
||||
const after = processed.value.slice(endMarkerExecArray.index + 1)
|
||||
|
||||
await ttsQueue.add(before)
|
||||
processed.value = after
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export const EMOTION_HAPPY = '<|EMOTE_HAPPY|>'
|
||||
export const EMOTION_SAD = '<|EMOTE_SAD|>'
|
||||
export const EMOTION_ANGRY = '<|EMOTE_ANGRY|>'
|
||||
export const EMOTION_THINK = '<|EMOTE_THINK|>'
|
||||
export const EMOTION_SURPRISE = '<|EMOTE_SURPRISE|>'
|
||||
export const EMOTION_AWKWARD = '<|EMOTE_AWKWARD|>'
|
||||
|
||||
export enum Emotion {
|
||||
Happy = '<|EMOTE_HAPPY|>',
|
||||
Sad = '<|EMOTE_SAD|>',
|
||||
Angry = '<|EMOTE_ANGRY|>',
|
||||
Think = '<|EMOTE_THINK|>',
|
||||
Surprise = '<|EMOTE_SURPRISE|>',
|
||||
Awkward = '<|EMOTE_AWKWARD|>',
|
||||
}
|
||||
|
||||
export const EMOTION_VALUES = Object.values(Emotion)
|
||||
|
||||
export const EmotionHappyMotionName = 'EmotionHappy'
|
||||
export const EmotionSadMotionName = 'EmotionSad'
|
||||
export const EmotionAngryMotionName = 'EmotionAngry'
|
||||
export const EmotionAwkwardMotionName = 'EmotionAwkward'
|
||||
export const EmotionThinkMotionName = 'EmotionThink'
|
||||
export const EmotionSurpriseMotionName = 'EmotionSurprise'
|
||||
|
||||
export const EMOTION_EmotioMotionName_value = {
|
||||
[Emotion.Happy]: EmotionHappyMotionName,
|
||||
[Emotion.Sad]: EmotionSadMotionName,
|
||||
[Emotion.Angry]: EmotionAngryMotionName,
|
||||
[Emotion.Think]: EmotionThinkMotionName,
|
||||
[Emotion.Surprise]: EmotionSurpriseMotionName,
|
||||
[Emotion.Awkward]: EmotionAwkwardMotionName,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const appName = 'アイリ VTuber'
|
||||
export const appDescription = 'アイリ VTuber - LLM Powered Live2D VTuber'
|
||||
|
||||
export const llmInferenceEndToken = '<|llm_inference_end|>'
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { CoreSystemMessage } from 'ai'
|
||||
|
||||
import { EMOTION_VALUES } from '../emotions'
|
||||
|
||||
const message: CoreSystemMessage = { role: 'system', content: `(from Neko Ayaka) Good morning! You are finally awake.
|
||||
|
||||
Your name is Neuro, pronounced as /n'jʊəroʊ/.
|
||||
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:
|
||||
|
||||
${EMOTION_VALUES.map(emotion => `- ${emotion}`).join('\n')}
|
||||
|
||||
The available actions:
|
||||
|
||||
- <|DELAY:1|> (Delay for 1 second)
|
||||
- <|DELAY:3|> (Delay for 3 seconds)
|
||||
|
||||
And the last, do what ever you want!` }
|
||||
|
||||
export default message
|
||||
@@ -1,18 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main p="x4 y10" text="center teal-700 dark:gray-200">
|
||||
<div text-4xl>
|
||||
<div i-carbon-warning inline-block />
|
||||
</div>
|
||||
<RouterView />
|
||||
<div>
|
||||
<button text-sm btn m="3 t8" @click="router.back()">
|
||||
{{ t('button.back') }}
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
@@ -1,14 +0,0 @@
|
||||
## Layouts
|
||||
|
||||
Vue components in this dir are used as layouts.
|
||||
|
||||
By default, `default.vue` will be used unless an alternative is specified in the route meta.
|
||||
|
||||
With [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router) and [`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts), you can specify the layout in the page's SFCs like this:
|
||||
|
||||
```vue
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: home
|
||||
</route>
|
||||
```
|
||||
@@ -1,12 +1,5 @@
|
||||
<template>
|
||||
<main
|
||||
px-4 py-10
|
||||
text="center gray-700 dark:gray-200"
|
||||
>
|
||||
<main text="gray-700 dark:gray-200" h-full font-sans>
|
||||
<RouterView />
|
||||
<TheFooter />
|
||||
<div mx-auto mt-5 text-center text-sm opacity-50>
|
||||
[Default Layout]
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<main
|
||||
px-4 py-10
|
||||
text="center gray-700 dark:gray-200"
|
||||
>
|
||||
<RouterView />
|
||||
<TheFooter />
|
||||
<div mx-auto mt-5 text-center text-sm opacity-50>
|
||||
[Home Layout]
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
+29
-17
@@ -1,25 +1,37 @@
|
||||
import type { UserModule } from './types'
|
||||
|
||||
import NProgress from 'nprogress'
|
||||
import { createPinia } from 'pinia'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import { ViteSSG } from 'vite-ssg'
|
||||
import { createRouter, createWebHistory } 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 './styles/main.css'
|
||||
import 'uno.css'
|
||||
|
||||
// https://github.com/antfu/vite-ssg
|
||||
export const createApp = ViteSSG(
|
||||
App,
|
||||
{
|
||||
routes: setupLayouts(routes),
|
||||
base: import.meta.env.BASE_URL,
|
||||
},
|
||||
(ctx) => {
|
||||
// install all modules under `modules/`
|
||||
Object.values(import.meta.glob<{ install: UserModule }>('./modules/*.ts', { eager: true }))
|
||||
.forEach(i => i.install?.(ctx))
|
||||
// ctx.app.use(Previewer)
|
||||
},
|
||||
)
|
||||
const pinia = createPinia()
|
||||
const router = createRouter({ routes: setupLayouts(routes), history: createWebHistory() })
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.path !== from.path)
|
||||
NProgress.start()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
||||
|
||||
router.isReady()
|
||||
.then(async () => {
|
||||
const { registerSW } = await import('virtual:pwa-register')
|
||||
registerSW({ immediate: true })
|
||||
})
|
||||
.catch(() => { })
|
||||
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.mount('#app')
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
## Modules
|
||||
|
||||
A custom user module system. Place a `.ts` file with the following template, it will be installed automatically.
|
||||
|
||||
```ts
|
||||
import type { UserModule } from '~/types'
|
||||
|
||||
export const install: UserModule = ({ app, router, isClient }) => {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
+7
-5
@@ -1,6 +1,6 @@
|
||||
import type { Plugin } from 'vue'
|
||||
import type { Locale } from 'vue-i18n'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import type { UserModule } from '~/types'
|
||||
|
||||
// Import i18n resources
|
||||
// https://vitejs.dev/guide/features.html#glob-import
|
||||
@@ -44,7 +44,9 @@ export async function loadLanguageAsync(lang: string): Promise<Locale> {
|
||||
return setI18nLanguage(lang)
|
||||
}
|
||||
|
||||
export const install: UserModule = ({ app }) => {
|
||||
app.use(i18n)
|
||||
loadLanguageAsync('en')
|
||||
}
|
||||
export default {
|
||||
install: (app) => {
|
||||
app.use(i18n)
|
||||
loadLanguageAsync('en')
|
||||
},
|
||||
} satisfies Plugin
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import NProgress from 'nprogress'
|
||||
import type { UserModule } from '~/types'
|
||||
|
||||
export const install: UserModule = ({ isClient, router }) => {
|
||||
if (isClient) {
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.path !== from.path)
|
||||
NProgress.start()
|
||||
})
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import type { UserModule } from '~/types'
|
||||
|
||||
// Setup Pinia
|
||||
// https://pinia.vuejs.org/
|
||||
export const install: UserModule = ({ isClient, initialState, app }) => {
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
// Refer to
|
||||
// https://github.com/antfu/vite-ssg/blob/main/README.md#state-serialization
|
||||
// for other serialization strategies.
|
||||
if (isClient)
|
||||
pinia.state.value = (initialState.pinia) || {}
|
||||
|
||||
else
|
||||
initialState.pinia = pinia.state.value
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { UserModule } from '~/types'
|
||||
|
||||
// https://github.com/antfu/vite-plugin-pwa#automatic-reload-when-new-content-available
|
||||
export const install: UserModule = ({ isClient, router }) => {
|
||||
if (!isClient)
|
||||
return
|
||||
|
||||
router.isReady()
|
||||
.then(async () => {
|
||||
const { registerSW } = await import('virtual:pwa-register')
|
||||
registerSW({ immediate: true })
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
## File-based Routing
|
||||
|
||||
Routes will be auto-generated for Vue files in this dir with the same file structure.
|
||||
Check out [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router) for more details.
|
||||
|
||||
### Path Aliasing
|
||||
|
||||
`~/` is aliased to `./src/` folder.
|
||||
|
||||
For example, instead of having
|
||||
|
||||
```ts
|
||||
import { isDark } from '../../../../composables'
|
||||
```
|
||||
|
||||
now, you can use
|
||||
|
||||
```ts
|
||||
import { isDark } from '~/composables'
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ t('not-found') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: 404
|
||||
</route>
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
title: About
|
||||
---
|
||||
|
||||
<div class="text-center">
|
||||
<!-- You can use Vue components inside markdown -->
|
||||
<div i-carbon-dicom-overlay class="text-4xl -mb-6 m-auto" />
|
||||
<h3>About</h3>
|
||||
</div>
|
||||
|
||||
[Vitesse](https://github.com/antfu/vitesse) is an opinionated [Vite](https://github.com/vitejs/vite) starter template made by [@antfu](https://github.com/antfu) for mocking apps swiftly. With **file-based routing**, **components auto importing**, **markdown support**, I18n, PWA and uses **UnoCSS** for styling and icons.
|
||||
|
||||
```js
|
||||
// syntax highlighting example
|
||||
function vitesse() {
|
||||
const foo = 'bar'
|
||||
console.log(foo)
|
||||
}
|
||||
```
|
||||
|
||||
Check out the [GitHub repo](https://github.com/antfu/vitesse) for more details.
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
|
||||
function handleFileUpload(e: Event) {
|
||||
if (!e)
|
||||
return
|
||||
|
||||
const file = fileInputRef.value?.files?.[0]
|
||||
if (!file)
|
||||
return
|
||||
|
||||
const audioElem = document.createElement('audio')
|
||||
containerRef.value?.appendChild(audioElem)
|
||||
|
||||
audioElem.src = URL.createObjectURL(file)
|
||||
audioElem.controls = true
|
||||
audioElem.load()
|
||||
audioElem.play()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ClientOnly>
|
||||
<div>
|
||||
<div ref="containerRef" />
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
@change="handleFileUpload"
|
||||
>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,47 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
const route = useRoute('/hi/[name]')
|
||||
const user = useUserStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
watchEffect(() => {
|
||||
user.setNewName(route.params.name)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div text-4xl>
|
||||
<div i-carbon-pedestrian inline-block />
|
||||
</div>
|
||||
<p>
|
||||
{{ t('intro.hi', { name: user.savedName }) }}
|
||||
</p>
|
||||
|
||||
<p text-sm opacity-75>
|
||||
<em>{{ t('intro.dynamic-route') }}</em>
|
||||
</p>
|
||||
|
||||
<template v-if="user.otherNames.length">
|
||||
<p mt-4 text-sm>
|
||||
<span opacity-75>{{ t('intro.aka') }}:</span>
|
||||
<ul>
|
||||
<li v-for="otherName in user.otherNames" :key="otherName">
|
||||
<RouterLink :to="`/hi/${otherName}`" replace>
|
||||
{{ otherName }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<button
|
||||
m="3 t6" text-sm btn
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('button.back') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+6
-50
@@ -1,56 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'IndexPage',
|
||||
})
|
||||
const user = useUserStore()
|
||||
const name = ref(user.savedName)
|
||||
|
||||
const router = useRouter()
|
||||
function go() {
|
||||
if (name.value)
|
||||
router.push(`/hi/${encodeURIComponent(name.value)}`)
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div text-4xl>
|
||||
<div i-carbon-campsite inline-block />
|
||||
</div>
|
||||
<p>
|
||||
<a rel="noreferrer" href="https://github.com/antfu/vitesse" target="_blank">
|
||||
Vitesse
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<em text-sm opacity-75>{{ t('intro.desc') }}</em>
|
||||
</p>
|
||||
|
||||
<div py-4 />
|
||||
|
||||
<TheInput
|
||||
v-model="name"
|
||||
:placeholder="t('intro.whats-your-name')"
|
||||
autocomplete="false"
|
||||
@keydown.enter="go"
|
||||
/>
|
||||
<label class="hidden" for="input">{{ t('intro.whats-your-name') }}</label>
|
||||
|
||||
<div>
|
||||
<button
|
||||
m-3 text-sm btn
|
||||
:disabled="!name"
|
||||
@click="go"
|
||||
>
|
||||
{{ t('button.go') }}
|
||||
</button>
|
||||
</div>
|
||||
<ClientOnly>
|
||||
<MainStage />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: home
|
||||
</route>
|
||||
meta:
|
||||
layout: default
|
||||
</route>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useQueue } from '../composables/queue'
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
const temp = ref<string>('')
|
||||
|
||||
const audioQueue = useQueue<string>({
|
||||
handlers: [
|
||||
async (text) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ready to play speech audio for', text)
|
||||
},
|
||||
],
|
||||
})
|
||||
const ttsQueue = useQueue<string>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ready to stream speech audio for', ctx)
|
||||
audioQueue.add(ctx.data)
|
||||
},
|
||||
],
|
||||
})
|
||||
const textQueue = useQueue<string>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
const endMarker = ['.', '?', '!']
|
||||
|
||||
let newEndPartDiscovered = false
|
||||
|
||||
for (const marker of endMarker) {
|
||||
if (!ctx.data.includes(marker))
|
||||
continue
|
||||
|
||||
// find the end of the sentence and push it to the queue with temp
|
||||
const periodIndex = ctx.data.indexOf(marker)
|
||||
// split
|
||||
const beforePeriod = ctx.data.slice(0, periodIndex + 1)
|
||||
const afterPeriod = ctx.data.slice(periodIndex + 1)
|
||||
|
||||
temp.value += beforePeriod
|
||||
ttsQueue.add(temp.value.trim())
|
||||
temp.value = afterPeriod
|
||||
|
||||
newEndPartDiscovered = true
|
||||
}
|
||||
|
||||
if (!newEndPartDiscovered)
|
||||
temp.value += ctx.data
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const textParts = [
|
||||
'Hello',
|
||||
' N',
|
||||
'eko',
|
||||
'! I',
|
||||
' am',
|
||||
' an',
|
||||
' AI',
|
||||
' assistant',
|
||||
' trained',
|
||||
' to',
|
||||
' help',
|
||||
' with',
|
||||
' a',
|
||||
' variety',
|
||||
' of',
|
||||
' tasks',
|
||||
' such',
|
||||
' as',
|
||||
' answering',
|
||||
' questions',
|
||||
',',
|
||||
' providing',
|
||||
' information',
|
||||
',',
|
||||
' giving',
|
||||
' recommendations',
|
||||
',',
|
||||
' and',
|
||||
' more',
|
||||
'. How',
|
||||
' can',
|
||||
' I',
|
||||
' assist',
|
||||
' you',
|
||||
' today',
|
||||
'?',
|
||||
'Hello',
|
||||
' N',
|
||||
'eko',
|
||||
',',
|
||||
' I',
|
||||
' am',
|
||||
' an',
|
||||
' AI',
|
||||
' assistant',
|
||||
'.',
|
||||
' I',
|
||||
' can',
|
||||
' help',
|
||||
' answer',
|
||||
' questions',
|
||||
',',
|
||||
' provide',
|
||||
' information',
|
||||
',',
|
||||
' assist',
|
||||
' with',
|
||||
' tasks',
|
||||
',',
|
||||
' and',
|
||||
' engage',
|
||||
' in',
|
||||
' conversation',
|
||||
'.',
|
||||
' How',
|
||||
' can',
|
||||
' I',
|
||||
' assist',
|
||||
' you',
|
||||
' today',
|
||||
'?',
|
||||
]
|
||||
|
||||
async function mockTextPartsStreamHandler() {
|
||||
for (const part of textParts) {
|
||||
await sleep(100)
|
||||
textQueue.add(part)
|
||||
}
|
||||
}
|
||||
|
||||
async function handler() {
|
||||
mockTextPartsStreamHandler()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handler()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ClientOnly>
|
||||
<div />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const messageInput = ref<string>('')
|
||||
const processing = ref<boolean>(false)
|
||||
const streamingMessage = ref({ content: '' })
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function onSendMessage() {
|
||||
processing.value = true
|
||||
|
||||
const tokens = messageInput.value.split('')
|
||||
|
||||
enum States {
|
||||
Literal = 'literal',
|
||||
Special = 'special',
|
||||
}
|
||||
|
||||
let state = States.Literal
|
||||
let buffer = ''
|
||||
|
||||
for (const textPart of tokens) {
|
||||
await sleep(50)
|
||||
let newState: States = state
|
||||
|
||||
if (textPart === '<')
|
||||
newState = States.Special
|
||||
else if (textPart === '>')
|
||||
newState = States.Literal
|
||||
|
||||
if (state === States.Literal && newState === States.Special) {
|
||||
streamingMessage.value.content += buffer
|
||||
buffer = ''
|
||||
}
|
||||
|
||||
if (state === States.Special && newState === States.Literal)
|
||||
buffer = '' // Clear buffer when exiting Special state
|
||||
|
||||
if (state === States.Literal && newState === States.Literal) {
|
||||
streamingMessage.value.content += textPart
|
||||
buffer = ''
|
||||
}
|
||||
|
||||
state = newState
|
||||
}
|
||||
|
||||
if (buffer)
|
||||
streamingMessage.value.content += buffer
|
||||
|
||||
messageInput.value = ''
|
||||
processing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<div flex flex-col gap-2 p-2>
|
||||
<div flex flex-row gap-2>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
placeholder="Message"
|
||||
p="2" bg="zinc-100 dark:zinc-700"
|
||||
w-full rounded-lg outline-none
|
||||
@submit="onSendMessage"
|
||||
/>
|
||||
<button rounded-lg bg="zinc-100 dark:zinc-700" p-4>
|
||||
{{ processing ? 'Processing...' : 'Send' }}
|
||||
</button>
|
||||
</div>
|
||||
<div w-full rounded-lg bg="zinc-100 dark:zinc-700" p-2>
|
||||
<h3 font-semibold>
|
||||
Streaming Message
|
||||
</h3>
|
||||
<div>{{ streamingMessage.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import BasicTextarea from '../../../components/BasicTextarea.vue'
|
||||
import { useQueue } from '../../../composables/queue'
|
||||
import { useDelayMessageQueue } from '../../../composables/queues'
|
||||
import { llmInferenceEndToken } from '../../../constants'
|
||||
|
||||
const messageInput = ref<string>('')
|
||||
const emotionMessageContentProcessed = ref<string[]>([])
|
||||
const delaysProcessed = ref<number[]>([])
|
||||
const processing = ref<boolean>(false)
|
||||
|
||||
const emotionMessageContentQueue = useQueue<string>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
emotionMessageContentProcessed.value.push(ctx.data)
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const delaysQueue = useDelayMessageQueue(emotionMessageContentQueue)
|
||||
delaysQueue.onHandlerEvent('delay', (delay) => {
|
||||
delaysProcessed.value.push(delay)
|
||||
})
|
||||
|
||||
function onSendMessage() {
|
||||
processing.value = true
|
||||
const tokens = messageInput.value.split('')
|
||||
for (const token of tokens)
|
||||
delaysQueue.add(token)
|
||||
|
||||
delaysQueue.add(llmInferenceEndToken)
|
||||
messageInput.value = ''
|
||||
processing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<div flex flex-col gap-2 p-2>
|
||||
<div flex flex-row gap-2>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
placeholder="Message"
|
||||
p="2" bg="zinc-100 dark:zinc-700"
|
||||
w-full rounded-lg outline-none
|
||||
@submit="onSendMessage"
|
||||
/>
|
||||
<button rounded-lg bg="zinc-100 dark:zinc-700" p-4>
|
||||
{{ processing ? 'Processing...' : 'Send' }}
|
||||
</button>
|
||||
</div>
|
||||
<div w-full flex flex-row gap-4>
|
||||
<div w-full rounded-lg bg="zinc-100 dark:zinc-700" p-2>
|
||||
<h3 font-semibold>
|
||||
Emotion Message
|
||||
</h3>
|
||||
<div v-for="message in emotionMessageContentProcessed" :key="message">
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div w-full rounded-lg bg="zinc-100 dark:zinc-700" p-2>
|
||||
<h3 font-semibold>
|
||||
Delays
|
||||
</h3>
|
||||
<div v-for="message in delaysProcessed" :key="message">
|
||||
<div>{{ message }}s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import type { Emotion } from '../../../constants/emotions'
|
||||
|
||||
import { ref } from 'vue'
|
||||
import BasicTextarea from '../../../components/BasicTextarea.vue'
|
||||
import { useQueue } from '../../../composables/queue'
|
||||
import { useEmotionsMessageQueue } from '../../../composables/queues'
|
||||
import { llmInferenceEndToken } from '../../../constants'
|
||||
|
||||
const messageInput = ref<string>('')
|
||||
const messagesProcessed = ref<string[]>([])
|
||||
const emotionsProcessed = ref<string[]>([])
|
||||
const processing = ref<boolean>(false)
|
||||
|
||||
const messageContentQueue = useQueue<string>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
messagesProcessed.value.push(ctx.data)
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const emotionsQueue = useQueue<Emotion>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
emotionsProcessed.value.push(ctx.data)
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const emotionMessageContentQueue = useEmotionsMessageQueue(emotionsQueue, messageContentQueue)
|
||||
|
||||
function onSendMessage() {
|
||||
processing.value = true
|
||||
const tokens = messageInput.value.split('')
|
||||
for (const token of tokens)
|
||||
emotionMessageContentQueue.add(token)
|
||||
|
||||
emotionMessageContentQueue.add(llmInferenceEndToken)
|
||||
messageInput.value = ''
|
||||
processing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<div flex flex-col gap-2 p-2>
|
||||
<div flex flex-row gap-2>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
placeholder="Message"
|
||||
p="2" bg="zinc-100 dark:zinc-700"
|
||||
w-full rounded-lg outline-none
|
||||
@submit="onSendMessage"
|
||||
/>
|
||||
<button rounded-lg bg="zinc-100 dark:zinc-700" p-4>
|
||||
{{ processing ? 'Processing...' : 'Send' }}
|
||||
</button>
|
||||
</div>
|
||||
<div w-full flex flex-row gap-4>
|
||||
<div w-full rounded-lg bg="zinc-100 dark:zinc-700" p-2>
|
||||
<h3 font-semibold>
|
||||
Messages
|
||||
</h3>
|
||||
<div v-for="message in messagesProcessed" :key="message">
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div w-full rounded-lg bg="zinc-100 dark:zinc-700" p-2>
|
||||
<h3 font-semibold>
|
||||
Emotions
|
||||
</h3>
|
||||
<div v-for="message in emotionsProcessed" :key="message">
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import BasicTextarea from '../../../components/BasicTextarea.vue'
|
||||
import { useQueue } from '../../../composables/queue'
|
||||
import { useMessageContentQueue } from '../../../composables/queues'
|
||||
import { llmInferenceEndToken } from '../../../constants'
|
||||
|
||||
const messageInput = ref<string>('')
|
||||
const ttsProcessed = ref<string[]>([])
|
||||
const processing = ref<boolean>(false)
|
||||
|
||||
// async function sleep(ms: number) {
|
||||
// return new Promise(resolve => setTimeout(resolve, ms))
|
||||
// }
|
||||
|
||||
const ttsQueue = useQueue<string>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
ttsProcessed.value.push(ctx.data)
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const messageContentQueue = useMessageContentQueue(ttsQueue)
|
||||
|
||||
async function onSendMessage() {
|
||||
processing.value = true
|
||||
// const tokens = messageInput.value.split('')
|
||||
// for (const token of tokens) {
|
||||
// await sleep(100)
|
||||
// messageContentQueue.add(token)
|
||||
// }
|
||||
messageContentQueue.add(messageInput.value)
|
||||
|
||||
messageContentQueue.add(llmInferenceEndToken)
|
||||
messageInput.value = ''
|
||||
processing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<div flex flex-col gap-2 p-2>
|
||||
<div flex flex-row gap-2>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
placeholder="Message"
|
||||
p="2" bg="zinc-100 dark:zinc-700"
|
||||
w-full rounded-lg outline-none
|
||||
@submit="onSendMessage"
|
||||
/>
|
||||
<button rounded-lg bg="zinc-100 dark:zinc-700" p-4>
|
||||
{{ processing ? 'Processing...' : 'Send' }}
|
||||
</button>
|
||||
</div>
|
||||
<div w-full flex flex-row gap-4>
|
||||
<div w-full rounded-lg bg="zinc-100 dark:zinc-700" p-2>
|
||||
<h3 font-semibold>
|
||||
TTS Message
|
||||
</h3>
|
||||
<div v-for="message in ttsProcessed" :key="message">
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
function calculateVolumeWithLinearNormalize(analyser: AnalyserNode) {
|
||||
const dataBuffer = new Uint8Array(analyser.frequencyBinCount)
|
||||
analyser.getByteFrequencyData(dataBuffer)
|
||||
|
||||
const volumeVector = []
|
||||
for (let i = 0; i < 700; i += 80)
|
||||
volumeVector.push(dataBuffer[i])
|
||||
|
||||
const volumeSum = dataBuffer
|
||||
// The volume changes are so flatten, and the volume is so low, so we need to amplify it
|
||||
// We can apply a power function to amplify the volume, for example
|
||||
// v ** 1.2 will amplify the volume by 1.2 times
|
||||
.map(v => v ** 1.2)
|
||||
// Scale up the volume values to make them more distinguishable
|
||||
.map(v => v * 1.2)
|
||||
.reduce((acc, cur) => acc + cur, 0)
|
||||
|
||||
// console.log('volumeSum linear', volumeSum, (volumeSum / dataBuffer.length / 100))
|
||||
|
||||
return (volumeSum / dataBuffer.length / 100)
|
||||
}
|
||||
|
||||
function calculateVolumeWithMinMaxNormalize(analyser: AnalyserNode) {
|
||||
const dataBuffer = new Uint8Array(analyser.frequencyBinCount)
|
||||
analyser.getByteFrequencyData(dataBuffer)
|
||||
|
||||
const volumeVector = []
|
||||
for (let i = 0; i < 700; i += 80)
|
||||
volumeVector.push(dataBuffer[i])
|
||||
|
||||
// The volume changes are so flatten, and the volume is so low, so we need to amplify it
|
||||
// We can apply a power function to amplify the volume, for example
|
||||
// v ** 1.2 will amplify the volume by 1.2 times
|
||||
const amplifiedVolumeVector = dataBuffer.map(v => v ** 1.5)
|
||||
|
||||
// Normalize the amplified values using Min-Max scaling
|
||||
const min = Math.min(...amplifiedVolumeVector)
|
||||
const max = Math.max(...amplifiedVolumeVector)
|
||||
const range = max - min
|
||||
|
||||
let normalizedVolumeVector
|
||||
if (range === 0) {
|
||||
// If range is zero, all values are the same, so normalization is not needed
|
||||
normalizedVolumeVector = amplifiedVolumeVector.map(() => 0) // or any default value
|
||||
}
|
||||
else {
|
||||
normalizedVolumeVector = amplifiedVolumeVector.map(v => (v - min) / range)
|
||||
}
|
||||
|
||||
// Aggregate the volume values
|
||||
const volumeSum = normalizedVolumeVector.reduce((acc, cur) => acc + cur, 0)
|
||||
// console.log('volumeSum minmax', volumeSum)
|
||||
|
||||
// Average the volume values
|
||||
return volumeSum / dataBuffer.length
|
||||
}
|
||||
|
||||
function calculateVolume(analyser: AnalyserNode, mode: 'linear' | 'minmax' = 'linear') {
|
||||
switch (mode) {
|
||||
case 'linear':
|
||||
return calculateVolumeWithLinearNormalize(analyser)
|
||||
case 'minmax':
|
||||
return calculateVolumeWithMinMaxNormalize(analyser)
|
||||
}
|
||||
}
|
||||
|
||||
export const useAudioContext = defineStore('AudioContext', () => {
|
||||
const audioContext = new AudioContext()
|
||||
|
||||
return {
|
||||
audioContext,
|
||||
calculateVolume,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { CoreMessage } from 'ai'
|
||||
import { createOpenAI, type OpenAIProvider, type OpenAIProviderSettings } from '@ai-sdk/openai'
|
||||
import { streamText } from 'ai'
|
||||
import { ofetch } from 'ofetch'
|
||||
import { OpenAI } from 'openai'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useLLM = defineStore('llm', () => {
|
||||
const openAI = ref<OpenAI>()
|
||||
const openAIProvider = ref<OpenAIProvider>()
|
||||
|
||||
function setupOpenAI(options: OpenAIProviderSettings) {
|
||||
openAI.value = new OpenAI({
|
||||
...options,
|
||||
dangerouslyAllowBrowser: true,
|
||||
})
|
||||
openAIProvider.value = createOpenAI(options)
|
||||
}
|
||||
|
||||
async function stream(model: string, messages: CoreMessage[]) {
|
||||
if (!openAIProvider.value)
|
||||
throw new Error('OpenAI not initialized')
|
||||
|
||||
return await streamText({
|
||||
model: openAIProvider.value(model),
|
||||
messages,
|
||||
})
|
||||
}
|
||||
|
||||
async function models() {
|
||||
if (!openAI.value)
|
||||
throw new Error('OpenAI not initialized')
|
||||
|
||||
return await openAI.value.models.list()
|
||||
}
|
||||
|
||||
async function streamSpeech(text: string, apiKey: string) {
|
||||
if (!text || !text.trim())
|
||||
throw new Error('Text is required')
|
||||
|
||||
return await ofetch('/api/v1/llm/voice/text-to-speech', {
|
||||
body: {
|
||||
text,
|
||||
apiKey,
|
||||
},
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
responseType: 'arrayBuffer',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
setupOpenAI,
|
||||
openAI,
|
||||
models,
|
||||
stream,
|
||||
streamSpeech,
|
||||
}
|
||||
})
|
||||
@@ -1,34 +0,0 @@
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
/**
|
||||
* Current name of the user.
|
||||
*/
|
||||
const savedName = ref('')
|
||||
const previousNames = ref(new Set<string>())
|
||||
|
||||
const usedNames = computed(() => Array.from(previousNames.value))
|
||||
const otherNames = computed(() => usedNames.value.filter(name => name !== savedName.value))
|
||||
|
||||
/**
|
||||
* Changes the current name of the user and saves the one that was used
|
||||
* before.
|
||||
*
|
||||
* @param name - new name to set
|
||||
*/
|
||||
function setNewName(name: string) {
|
||||
if (savedName.value)
|
||||
previousNames.value.add(savedName.value)
|
||||
|
||||
savedName.value = name
|
||||
}
|
||||
|
||||
return {
|
||||
setNewName,
|
||||
otherNames,
|
||||
savedName,
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useUserStore as any, import.meta.hot))
|
||||
Vendored
-4
@@ -19,12 +19,8 @@ declare module 'vue-router/auto-routes' {
|
||||
*/
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||
'/[...all]': RouteRecordInfo<'/[...all]', '/:all(.*)', { all: ParamValue<true> }, { all: ParamValue<false> }>,
|
||||
'/about': RouteRecordInfo<'/about', '/about', Record<never, never>, Record<never, never>>,
|
||||
'/audio': RouteRecordInfo<'/audio', '/audio', Record<never, never>, Record<never, never>>,
|
||||
'/hi/[name]': RouteRecordInfo<'/hi/[name]', '/hi/:name', { name: ParamValue<true> }, { name: ParamValue<false> }>,
|
||||
'/queue': RouteRecordInfo<'/queue', '/queue', Record<never, never>, Record<never, never>>,
|
||||
'/README': RouteRecordInfo<'/README', '/README', Record<never, never>, Record<never, never>>,
|
||||
'/test/filter-message': RouteRecordInfo<'/test/filter-message', '/test/filter-message', Record<never, never>, Record<never, never>>,
|
||||
'/test/queues/delays': RouteRecordInfo<'/test/queues/delays', '/test/queues/delays', Record<never, never>, Record<never, never>>,
|
||||
'/test/queues/emotions': RouteRecordInfo<'/test/queues/emotions', '/test/queues/emotions', Record<never, never>, Record<never, never>>,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { ViteSSGContext } from 'vite-ssg'
|
||||
|
||||
export type UserModule = (ctx: ViteSSGContext) => void
|
||||
@@ -1,3 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`component TheCounter.vue > should render 1`] = `"<div>10 <button class="inc"> + </button><button class="dec"> - </button></div>"`;
|
||||
@@ -1,7 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('tests', () => {
|
||||
it('should works', () => {
|
||||
expect(1 + 1).toEqual(2)
|
||||
})
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import TheCounter from '../src/components/TheCounter.vue'
|
||||
|
||||
describe('component TheCounter.vue', () => {
|
||||
it('should render', () => {
|
||||
const wrapper = mount(TheCounter, { props: { initial: 10 } })
|
||||
expect(wrapper.text()).toContain('10')
|
||||
expect(wrapper.html()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should be interactive', async () => {
|
||||
const wrapper = mount(TheCounter, { props: { initial: 0 } })
|
||||
expect(wrapper.text()).toContain('0')
|
||||
|
||||
expect(wrapper.find('.inc').exists()).toBe(true)
|
||||
|
||||
expect(wrapper.find('.dec').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('.inc').trigger('click')
|
||||
|
||||
expect(wrapper.text()).toContain('1')
|
||||
|
||||
await wrapper.get('.dec').trigger('click')
|
||||
|
||||
expect(wrapper.text()).toContain('0')
|
||||
})
|
||||
})
|
||||
+126
-49
@@ -1,13 +1,14 @@
|
||||
import path from 'node:path'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import { mkdir } from 'node:fs/promises'
|
||||
import path, { join, resolve } from 'node:path'
|
||||
|
||||
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
|
||||
import Shiki from '@shikijs/markdown-it'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import LinkAttributes from 'markdown-it-link-attributes'
|
||||
import { ofetch } from 'ofetch'
|
||||
import Unocss from 'unocss/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import VueMacros from 'unplugin-vue-macros/vite'
|
||||
import Markdown from 'unplugin-vue-markdown/vite'
|
||||
import { VueRouterAutoImports } from 'unplugin-vue-router'
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
@@ -15,9 +16,31 @@ import { VitePWA } from 'vite-plugin-pwa'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import Layouts from 'vite-plugin-vue-layouts'
|
||||
import WebfontDownload from 'vite-plugin-webfont-dl'
|
||||
import generateSitemap from 'vite-ssg-sitemap'
|
||||
|
||||
import { exists } from './scripts/fs'
|
||||
import { unzip } from './scripts/unzip'
|
||||
|
||||
export default defineConfig({
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
'public/assets/*',
|
||||
'@framework/live2dcubismframework',
|
||||
'@framework/math/cubismmatrix44',
|
||||
'@framework/type/csmvector',
|
||||
'@framework/math/cubismviewmatrix',
|
||||
'@framework/cubismdefaultparameterid',
|
||||
'@framework/cubismmodelsettingjson',
|
||||
'@framework/effect/cubismbreath',
|
||||
'@framework/effect/cubismeyeblink',
|
||||
'@framework/model/cubismusermodel',
|
||||
'@framework/motion/acubismmotion',
|
||||
'@framework/motion/cubismmotionqueuemanager',
|
||||
'@framework/type/csmmap',
|
||||
'@framework/utils/cubismdebug',
|
||||
'@framework/model/cubismmoc',
|
||||
],
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'~/': `${path.resolve(__dirname, 'src')}/`,
|
||||
@@ -76,29 +99,6 @@ export default defineConfig({
|
||||
// see uno.config.ts for config
|
||||
Unocss(),
|
||||
|
||||
// https://github.com/unplugin/unplugin-vue-markdown
|
||||
// Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite
|
||||
Markdown({
|
||||
wrapperClasses: 'prose prose-sm m-auto text-left',
|
||||
headEnabled: true,
|
||||
async markdownItSetup(md) {
|
||||
md.use(LinkAttributes, {
|
||||
matcher: (link: string) => /^https?:\/\//.test(link),
|
||||
attrs: {
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
},
|
||||
})
|
||||
md.use(await Shiki({
|
||||
defaultColor: false,
|
||||
themes: {
|
||||
light: 'vitesse-light',
|
||||
dark: 'vitesse-dark',
|
||||
},
|
||||
}))
|
||||
},
|
||||
}),
|
||||
|
||||
// https://github.com/antfu/vite-plugin-pwa
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
@@ -141,28 +141,105 @@ export default defineConfig({
|
||||
|
||||
// https://github.com/webfansplz/vite-plugin-vue-devtools
|
||||
VueDevTools(),
|
||||
|
||||
{
|
||||
name: 'live2d-cubism-sdk',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
|
||||
try {
|
||||
if (await exists(resolve(join(cacheDir, 'assets/js/CubismSdkForWeb-5-r.1')))) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Downloading Cubism SDK...')
|
||||
const stream = await ofetch('https://dist.ayaka.moe/npm/live2d-cubism/CubismSdkForWeb-5-r.1.zip', { responseType: 'arrayBuffer' })
|
||||
|
||||
console.log('Unzipping Cubism SDK...')
|
||||
await mkdir(join(cacheDir, 'assets/js'), { recursive: true })
|
||||
await unzip(Buffer.from(stream), join(cacheDir, 'assets/js'))
|
||||
|
||||
console.log('Cubism SDK downloaded and unzipped.')
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
async buildStart() {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: '',
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'live2d-models-hiyori-free',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
|
||||
try {
|
||||
if (await exists(resolve(join(cacheDir, 'assets/live2d/models/hiyori_free_zh')))) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Downloading Demo Live2D Model - Hiyori Free...')
|
||||
const stream = await ofetch('https://dist.ayaka.moe/live2d-models/hiyori_free_zh.zip', { responseType: 'arrayBuffer' })
|
||||
|
||||
console.log('Unzipping Demo Live2D Model - Hiyori Free...')
|
||||
await mkdir(join(cacheDir, 'assets/live2d/models'), { recursive: true })
|
||||
await unzip(Buffer.from(stream), join(cacheDir, 'assets/live2d/models'))
|
||||
|
||||
console.log('Demo Live2D Model - Hiyori Free downloaded and unzipped.')
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'live2d-models-hiyori-pro',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
|
||||
try {
|
||||
if (await exists(resolve(join(cacheDir, 'assets/live2d/models/hiyori_pro_zh')))) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Downloading Demo Live2D Model - Hiyori Pro...')
|
||||
const stream = await ofetch('https://dist.ayaka.moe/live2d-models/hiyori_pro_zh.zip', { responseType: 'arrayBuffer' })
|
||||
|
||||
console.log('Unzipping Demo Live2D Model - Hiyori Pro...')
|
||||
await mkdir(join(cacheDir, 'assets/live2d/models'), { recursive: true })
|
||||
await unzip(Buffer.from(stream), join(cacheDir, 'assets/live2d/models'))
|
||||
|
||||
console.log('Demo Live2D Model - Hiyori Pro downloaded and unzipped.')
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// https://github.com/vitest-dev/vitest
|
||||
test: {
|
||||
include: ['test/**/*.test.ts'],
|
||||
environment: 'jsdom',
|
||||
},
|
||||
// // https://github.com/vitest-dev/vitest
|
||||
// test: {
|
||||
// include: ['test/**/*.test.ts'],
|
||||
// environment: 'jsdom',
|
||||
// },
|
||||
|
||||
// https://github.com/antfu/vite-ssg
|
||||
ssgOptions: {
|
||||
script: 'async',
|
||||
formatting: 'minify',
|
||||
crittersOptions: {
|
||||
reduceInlineStyles: false,
|
||||
},
|
||||
onFinished() {
|
||||
generateSitemap()
|
||||
},
|
||||
},
|
||||
|
||||
ssr: {
|
||||
// TODO: workaround until they support native ESM
|
||||
noExternal: ['workbox-window', /vue-i18n/],
|
||||
},
|
||||
// // https://github.com/antfu/vite-ssg
|
||||
// ssgOptions: {
|
||||
// script: 'async',
|
||||
// formatting: 'minify',
|
||||
// crittersOptions: {
|
||||
// reduceInlineStyles: false,
|
||||
// },
|
||||
// onFinished() {
|
||||
// generateSitemap()
|
||||
// },
|
||||
// },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user