chore: migrate to spa

This commit is contained in:
Neko Ayaka
2024-12-03 13:11:43 +08:00
parent 83fb9b5748
commit cd0f371595
81 changed files with 4436 additions and 2827 deletions
-4
View File
@@ -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()
+18
View File
@@ -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()
},
}
}
+110
View File
@@ -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,
}
}
+236
View File
@@ -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
}
},
],
})
}