chore: migrate to spa
This commit is contained in:
@@ -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
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user