feat(ui): added core system error message
Signed-off-by: Neko Ayaka <neko@ayaka.moe>
This commit is contained in:
@@ -79,6 +79,7 @@ stage:
|
||||
character-name:
|
||||
airi: Airi
|
||||
you: You
|
||||
core-system: Core System
|
||||
tabs:
|
||||
chat: Chat
|
||||
clothes: Clothes
|
||||
|
||||
@@ -66,3 +66,18 @@ stage:
|
||||
select-a-model: 选择一个模型
|
||||
select-a-voice: 选择一个声线
|
||||
waiting: 等待中
|
||||
chat:
|
||||
message:
|
||||
character-name:
|
||||
airi: Airi
|
||||
you: 你
|
||||
core-system: 核心系统
|
||||
tabs:
|
||||
chat: 聊天
|
||||
clothes: 换装
|
||||
custom: 自定义
|
||||
operations:
|
||||
load-models: 加载推理模型
|
||||
load-models-status:
|
||||
loading: 加载中
|
||||
ready: 已就绪
|
||||
|
||||
@@ -7,7 +7,7 @@ import { nextTick, ref } from 'vue'
|
||||
|
||||
const chatHistoryRef = ref<HTMLDivElement>()
|
||||
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { messages, sending } = storeToRefs(useChatStore())
|
||||
const bounding = useElementBounding(chatHistoryRef, { immediate: true, windowScroll: true, windowResize: true })
|
||||
const { y: chatHistoryContainerY } = useScroll(chatHistoryRef)
|
||||
|
||||
@@ -34,38 +34,58 @@ onTokenLiteral(async () => {
|
||||
<template>
|
||||
<div py="1" flex="~ col" rounded="lg" overflow-hidden>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div ref="chatHistoryRef" v-auto-animate max-h="30vh" flex="~ col" scrollbar="~ w-2 track-color-transparent thumb-color-pink-300 rounded" h-full w-full overflow-scroll>
|
||||
<div
|
||||
ref="chatHistoryRef" v-auto-animate max-h="30vh" flex="~ col"
|
||||
scrollbar="~ w-2 track-color-transparent thumb-color-pink-300 rounded" h-full w-full overflow-scroll
|
||||
>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div v-for="(message, index) in messages" :key="index" mb-2>
|
||||
<div v-if="message.role === 'error'" flex mr="12">
|
||||
<div
|
||||
flex="~ col" border="4 solid violet-200/50 dark:violet-500/50" shadow="md violet-200/50 dark:none"
|
||||
min-w-20 rounded-lg px-2 py-1 h="unset <sm:fit" bg="<md:violet-500/25"
|
||||
>
|
||||
<div flex="~ row" gap-2>
|
||||
<div flex-1>
|
||||
<span text-xs text="violet-400/90 dark:violet-600/90" font-semibold class="inline <sm:hidden">{{
|
||||
$t('stage.chat.message.character-name.core-system') }}</span>
|
||||
</div>
|
||||
<div i-solar:danger-triangle-bold-duotone text-violet-500 />
|
||||
</div>
|
||||
<div v-if="sending" i-eos-icons:three-dots-loading />
|
||||
<div
|
||||
v-else class="markdown-content text-violet-500" text="base <sm:xs"
|
||||
v-html="process(message.content as string)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="message.role === 'assistant'" flex mr="12">
|
||||
<div
|
||||
flex="~ col"
|
||||
border="4 solid pink-200"
|
||||
shadow="md pink-200/50"
|
||||
min-w-20 rounded-lg px-2 py-1
|
||||
h="fit"
|
||||
flex="~ col" border="4 solid pink-200" shadow="md pink-200/50" min-w-20 rounded-lg px-2 py-1 h="fit"
|
||||
bg="pink-100"
|
||||
>
|
||||
<div>
|
||||
<span text-xs text="pink-400/90" font-semibold class="inline hidden">Airi</span>
|
||||
</div>
|
||||
<div v-if="message.content" class="markdown-content" text="xs pink-400" v-html="process(message.content as string)" />
|
||||
<div
|
||||
v-if="message.content" class="markdown-content" text="xs pink-400"
|
||||
v-html="process(message.content as string)"
|
||||
/>
|
||||
<div v-else i-eos-icons:three-dots-loading />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="message.role === 'user'" flex="~">
|
||||
<div
|
||||
flex="~ col"
|
||||
border="4 solid cyan-200"
|
||||
shadow="md cyan-200/50"
|
||||
px="2"
|
||||
h="fit" min-w-20 rounded-lg px-2 py-1
|
||||
bg="cyan-100"
|
||||
flex="~ col" border="4 solid cyan-200" shadow="md cyan-200/50" px="2" h="fit" min-w-20 rounded-lg px-2
|
||||
py-1 bg="cyan-100"
|
||||
>
|
||||
<div>
|
||||
<span text-xs text="cyan-600/90" font-semibold class="hidden">You</span>
|
||||
</div>
|
||||
<div v-if="message.content" class="markdown-content" text="xs cyan-600" v-html="process(message.content as string)" />
|
||||
<div
|
||||
v-if="message.content" class="markdown-content" text="xs cyan-600"
|
||||
v-html="process(message.content as string)"
|
||||
/>
|
||||
<div v-else />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ const listening = ref(false)
|
||||
// const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { send, onAfterSend } = useChatStore()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { t } = useI18n()
|
||||
|
||||
async function handleSend() {
|
||||
@@ -22,7 +23,16 @@ async function handleSend() {
|
||||
return
|
||||
}
|
||||
|
||||
await send(messageInput.value)
|
||||
try {
|
||||
await send(messageInput.value)
|
||||
}
|
||||
catch (error) {
|
||||
messages.value.pop()
|
||||
messages.value.push({
|
||||
role: 'error',
|
||||
content: (error as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { destroy, start } = useMicVAD(selectedAudioDeviceId, {
|
||||
|
||||
@@ -51,21 +51,21 @@ settings:
|
||||
chinese: 简体中文
|
||||
english: English
|
||||
title: Language
|
||||
live2d:
|
||||
change-model:
|
||||
from-file: Load from File
|
||||
from-file-select: Select
|
||||
from-url: Load from URL
|
||||
from-url-confirm: Load
|
||||
from-url-placeholder: Enter Live2D model URL
|
||||
title: Change Model
|
||||
map-motions:
|
||||
play: Play Motion
|
||||
title: Map Motions
|
||||
title: Live2D Settings
|
||||
microphone: Microphone
|
||||
model-provider:
|
||||
title: Model Providers
|
||||
live2d:
|
||||
title: Live2D Settings
|
||||
change-model:
|
||||
title: Change Model
|
||||
from-url: Load from URL
|
||||
from-url-placeholder: Enter Live2D model URL
|
||||
from-url-confirm: Load
|
||||
from-file: Load from File
|
||||
from-file-select: Select
|
||||
map-motions:
|
||||
title: Map Motions
|
||||
play: Play Motion
|
||||
models: Model
|
||||
openai-api-key:
|
||||
label: OpenAI API Key
|
||||
@@ -83,6 +83,7 @@ stage:
|
||||
message:
|
||||
character-name:
|
||||
airi: Airi
|
||||
core-system: Core System
|
||||
you: You
|
||||
tabs:
|
||||
chat: Chat
|
||||
|
||||
@@ -39,20 +39,21 @@ settings:
|
||||
chinese: 简体中文
|
||||
english: English
|
||||
title: 语言
|
||||
model-provider:
|
||||
title: 模型提供商
|
||||
live2d:
|
||||
title: Live2D 设置
|
||||
change-model:
|
||||
title: 更换模型
|
||||
from-url: 从 URL 加载
|
||||
from-url-placeholder: 输入 Live2D 模型 URL
|
||||
from-url-confirm: 加载
|
||||
from-file: 从文件加载
|
||||
from-file-select: 选择
|
||||
from-url: 从 URL 加载
|
||||
from-url-confirm: 加载
|
||||
from-url-placeholder: 输入 Live2D 模型 URL
|
||||
title: 更换模型
|
||||
map-motions:
|
||||
title: 映射动作
|
||||
play: 播放动作
|
||||
title: 映射动作
|
||||
title: Live2D 设置
|
||||
microphone: 麦克风
|
||||
model-provider:
|
||||
title: 模型提供商
|
||||
models: 模型
|
||||
openai-api-key:
|
||||
label: OpenAI API 密钥
|
||||
@@ -70,9 +71,22 @@ stage:
|
||||
message:
|
||||
character-name:
|
||||
airi: Airi
|
||||
core-system: 核心系统
|
||||
you: 你
|
||||
tabs:
|
||||
chat: 聊天
|
||||
clothes: 换装
|
||||
custom: 自定义
|
||||
message: 消息
|
||||
operations:
|
||||
load-models: 加载推理模型
|
||||
load-models-status:
|
||||
loading: 加载中
|
||||
ready: 已就绪
|
||||
select-a-audio-input: 选择一个音频输入设备
|
||||
select-a-model: 选择一个模型
|
||||
select-a-voice: 选择一个声线
|
||||
viewers:
|
||||
debug-menu:
|
||||
emotions: 表情
|
||||
waiting: 等待中
|
||||
|
||||
@@ -19,17 +19,35 @@ const showMicrophoneSelect = ref(false)
|
||||
const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
|
||||
const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { send, onAfterSend } = useChatStore()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { audioContext } = useAudioContext()
|
||||
const { t } = useI18n()
|
||||
|
||||
const { transcribe: generate, load: loadWhisper, status: whisperStatus, terminate } = useWhisper(WhisperWorker, {
|
||||
onComplete: async (res) => {
|
||||
if (!res || !res.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
await send(res)
|
||||
},
|
||||
})
|
||||
|
||||
async function handleSend() {
|
||||
await send(messageInput.value)
|
||||
if (!messageInput.value.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await send(messageInput.value)
|
||||
}
|
||||
catch (error) {
|
||||
messages.value.pop()
|
||||
messages.value.push({
|
||||
role: 'error',
|
||||
content: (error as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { destroy, start } = useMicVAD(selectedAudioDeviceId, {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { nextTick, ref } from 'vue'
|
||||
|
||||
const chatHistoryRef = ref<HTMLDivElement>()
|
||||
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { messages, sending } = storeToRefs(useChatStore())
|
||||
const bounding = useElementBounding(chatHistoryRef, { immediate: true, windowScroll: true, windowResize: true })
|
||||
const { y: chatHistoryContainerY } = useScroll(chatHistoryRef)
|
||||
|
||||
@@ -32,43 +32,54 @@ onTokenLiteral(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
relative px="<sm:2" py="<sm:2" flex="~ col" rounded="lg" overflow-hidden
|
||||
>
|
||||
<div relative px="<sm:2" py="<sm:2" flex="~ col" rounded="lg" overflow-hidden>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div ref="chatHistoryRef" v-auto-animate h-full w-full flex="~ col" overflow-scroll>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div v-for="(message, index) in messages" :key="index" mb-2>
|
||||
<div v-if="message.role === 'error'" flex mr="12">
|
||||
<div
|
||||
flex="~ col" border="4 solid violet-200/50 dark:violet-500/50" shadow="md violet-200/50 dark:none"
|
||||
min-w-20 rounded-lg px-2 py-1 h="unset <sm:fit" bg="<md:violet-500/25"
|
||||
>
|
||||
<div flex="~ row" gap-2>
|
||||
<div flex-1>
|
||||
<span text-xs text="violet-400/90 dark:violet-600/90" font-semibold class="inline <sm:hidden">{{
|
||||
$t('stage.chat.message.character-name.core-system') }}</span>
|
||||
</div>
|
||||
<div i-solar:danger-triangle-bold-duotone text-violet-500 />
|
||||
</div>
|
||||
<div v-if="sending" i-eos-icons:three-dots-loading />
|
||||
<div
|
||||
v-else class="markdown-content text-violet-500" text="base <sm:xs"
|
||||
v-html="process(message.content as string)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="message.role === 'assistant'" flex mr="12">
|
||||
<div
|
||||
flex="~ col"
|
||||
border="4 solid pink-200/50 dark:pink-500/50"
|
||||
shadow="md pink-200/50 dark:none"
|
||||
min-w-20 rounded-lg px-2 py-1
|
||||
h="unset <sm:fit"
|
||||
bg="<md:pink-500/25"
|
||||
flex="~ col" border="4 solid pink-200/50 dark:pink-500/50" shadow="md pink-200/50 dark:none" min-w-20
|
||||
rounded-lg px-2 py-1 h="unset <sm:fit" bg="<md:pink-500/25"
|
||||
>
|
||||
<div>
|
||||
<span text-xs text="pink-400/90 dark:pink-600/90" font-semibold class="inline <sm:hidden">{{ $t('stage.chat.message.character-name.airi') }}</span>
|
||||
<span text-xs text="pink-400/90 dark:pink-600/90" font-semibold class="inline <sm:hidden">{{
|
||||
$t('stage.chat.message.character-name.airi') }}</span>
|
||||
</div>
|
||||
<div v-if="message.content" class="markdown-content" text="base <sm:xs" v-html="process(message.content as string)" />
|
||||
<div v-else i-eos-icons:three-dots-loading />
|
||||
<div v-if="sending" i-eos-icons:three-dots-loading />
|
||||
<div v-else class="markdown-content" text="base <sm:xs" v-html="process(message.content as string)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="message.role === 'user'" flex="~ row-reverse" ml="12">
|
||||
<div
|
||||
flex="~ col"
|
||||
border="4 solid cyan-200/50 dark:cyan-500/50"
|
||||
shadow="md cyan-200/50 dark:none"
|
||||
px="2"
|
||||
h="unset <sm:fit" min-w-20 rounded-lg px-2 py-1
|
||||
bg="<md:cyan-500/25"
|
||||
flex="~ col" border="4 solid cyan-200/50 dark:cyan-500/50" shadow="md cyan-200/50 dark:none" px="2"
|
||||
h="unset <sm:fit" min-w-20 rounded-lg px-2 py-1 bg="<md:cyan-500/25"
|
||||
>
|
||||
<div>
|
||||
<span text-xs text="cyan-400/90 dark:cyan-600/90" font-semibold class="inline <sm:hidden">{{ $t('stage.chat.message.character-name.you') }}</span>
|
||||
<span text-xs text="cyan-400/90 dark:cyan-600/90" font-semibold class="inline <sm:hidden">{{
|
||||
$t('stage.chat.message.character-name.you') }}</span>
|
||||
</div>
|
||||
<div v-if="message.content" class="markdown-content" text="base <sm:xs" v-html="process(message.content as string)" />
|
||||
<div v-else />
|
||||
<div v-if="sending" i-eos-icons:three-dots-loading />
|
||||
<div v-else class="markdown-content" text="base <sm:xs" v-html="process(message.content as string)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -203,6 +203,58 @@ function addRecordEntry(entries: Array<[string, string]>) {
|
||||
function removeRecordEntry(entries: Array<[string, string]>, index: number) {
|
||||
entries.splice(index, 1)
|
||||
}
|
||||
|
||||
// Helper function to get default value from field schema
|
||||
function getDefaultValue(field: any, _fieldName: string) {
|
||||
// Check if field has a default value defined
|
||||
if (field.default !== undefined) {
|
||||
return field.default
|
||||
}
|
||||
|
||||
// For record types, return an empty array (for the key-value pairs)
|
||||
if (field.type === 'object' && field.additionalProperties) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Return appropriate empty values based on field type
|
||||
switch (field.type) {
|
||||
case 'string':
|
||||
return ''
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return 0
|
||||
case 'boolean':
|
||||
return false
|
||||
case 'array':
|
||||
return []
|
||||
case 'object':
|
||||
return {}
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize field with default value if not already set
|
||||
function initFieldWithDefault(providerId: string, fieldName: string, field: any) {
|
||||
const currentValue = providerValues.value[providerId]?.[fieldName]
|
||||
|
||||
// Only set default if field doesn't already have a value
|
||||
if (currentValue === undefined) {
|
||||
const defaultValue = getDefaultValue(field, fieldName)
|
||||
setFieldValue(providerId, fieldName, defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize all fields with defaults when provider is expanded
|
||||
function initProviderDefaults(providerId: string) {
|
||||
const provider = providers.value.find(p => p.id === providerId)
|
||||
if (!provider || !provider.fields.properties)
|
||||
return
|
||||
|
||||
Object.entries(provider.fields.properties).forEach(([fieldName, field]) => {
|
||||
initFieldWithDefault(providerId, fieldName, field)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -216,7 +268,7 @@ function removeRecordEntry(entries: Array<[string, string]>, index: number) {
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
@click="slotProps.setVisible(!slotProps.visible) && initProviderDefaults(provider.id)"
|
||||
>
|
||||
<div flex="~ row 1" items-center gap-1.5>
|
||||
<div
|
||||
|
||||
@@ -16,6 +16,7 @@ watchEffect(() => {
|
||||
|
||||
function setVisible(value: boolean) {
|
||||
visible.value = value
|
||||
return value
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,11 +10,18 @@ import { useLLM } from '../stores/llm'
|
||||
import { useProvidersStore } from '../stores/providers'
|
||||
import { asyncIteratorFromReadableStream } from '../utils/iterator'
|
||||
|
||||
export interface ErrorMessage {
|
||||
role: 'error'
|
||||
content: string
|
||||
}
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const { stream } = useLLM()
|
||||
const { t } = useI18n()
|
||||
const { providers: providerValues } = storeToRefs(useProvidersStore())
|
||||
|
||||
const sending = ref(false)
|
||||
|
||||
const onBeforeMessageComposedHooks = ref<Array<(message: string) => Promise<void>>>([])
|
||||
const onAfterMessageComposedHooks = ref<Array<(message: string) => Promise<void>>>([])
|
||||
const onBeforeSendHooks = ref<Array<(message: string) => Promise<void>>>([])
|
||||
@@ -56,7 +63,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
onAssistantResponseEndHooks.value.push(cb)
|
||||
}
|
||||
|
||||
const messages = ref<Array<Message>>([
|
||||
const messages = ref<Array<Message | ErrorMessage>>([
|
||||
SystemPromptV2(
|
||||
t('prompt.prefix'),
|
||||
t('prompt.suffix'),
|
||||
@@ -70,75 +77,83 @@ export const useChatStore = defineStore('chat', () => {
|
||||
apiKey?: string
|
||||
model?: { id: string }
|
||||
}) {
|
||||
if (!sendingMessage)
|
||||
return
|
||||
try {
|
||||
sending.value = true
|
||||
|
||||
for (const hook of onBeforeMessageComposedHooks.value) {
|
||||
await hook(sendingMessage)
|
||||
if (!sendingMessage)
|
||||
return
|
||||
|
||||
for (const hook of onBeforeMessageComposedHooks.value) {
|
||||
await hook(sendingMessage)
|
||||
}
|
||||
|
||||
const {
|
||||
baseUrl = providerValues.value['openrouter-ai']?.baseUrl as string | undefined || '',
|
||||
apiKey = providerValues.value['openrouter-ai']?.apiKey as string | undefined || '',
|
||||
model = providerValues.value['openrouter-ai']?.model as { id: string } | undefined || { id: 'openai/gpt-4o-mini' },
|
||||
} = options ?? { }
|
||||
|
||||
streamingMessage.value = { role: 'assistant', content: '' }
|
||||
messages.value.push({ role: 'user', content: sendingMessage })
|
||||
messages.value.push(streamingMessage.value)
|
||||
const newMessages = messages.value.slice(0, messages.value.length - 1).map(msg => toRaw(msg))
|
||||
|
||||
for (const hook of onAfterMessageComposedHooks.value) {
|
||||
await hook(sendingMessage)
|
||||
}
|
||||
|
||||
for (const hook of onBeforeSendHooks.value) {
|
||||
await hook(sendingMessage)
|
||||
}
|
||||
|
||||
const res = await stream(baseUrl, apiKey, model.id, newMessages as Message[])
|
||||
|
||||
for (const hook of onAfterSendHooks.value) {
|
||||
await hook(sendingMessage)
|
||||
}
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const parser = useLlmmarkerParser({
|
||||
onLiteral: async (literal) => {
|
||||
for (const hook of onTokenLiteralHooks.value) {
|
||||
await hook(literal)
|
||||
}
|
||||
|
||||
streamingMessage.value.content += literal
|
||||
},
|
||||
onSpecial: async (special) => {
|
||||
for (const hook of onTokenSpecialHooks.value) {
|
||||
await hook(special)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
for await (const textPart of asyncIteratorFromReadableStream(res.textStream, async v => v)) {
|
||||
fullText += textPart
|
||||
await parser.consume(textPart)
|
||||
}
|
||||
|
||||
await parser.end()
|
||||
|
||||
for (const hook of onStreamEndHooks.value) {
|
||||
await hook()
|
||||
}
|
||||
|
||||
for (const hook of onAssistantResponseEndHooks.value) {
|
||||
await hook(fullText)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('LLM output:', fullText)
|
||||
}
|
||||
|
||||
const {
|
||||
baseUrl = providerValues.value['openrouter-ai']?.baseUrl as string | undefined || '',
|
||||
apiKey = providerValues.value['openrouter-ai']?.apiKey as string | undefined || '',
|
||||
model = providerValues.value['openrouter-ai']?.model as { id: string } | undefined || { id: 'openai/gpt-4o-mini' },
|
||||
} = options ?? { }
|
||||
|
||||
streamingMessage.value = { role: 'assistant', content: '' }
|
||||
messages.value.push({ role: 'user', content: sendingMessage })
|
||||
messages.value.push(streamingMessage.value)
|
||||
const newMessages = messages.value.slice(0, messages.value.length - 1).map(msg => toRaw(msg))
|
||||
|
||||
for (const hook of onAfterMessageComposedHooks.value) {
|
||||
await hook(sendingMessage)
|
||||
finally {
|
||||
sending.value = false
|
||||
}
|
||||
|
||||
for (const hook of onBeforeSendHooks.value) {
|
||||
await hook(sendingMessage)
|
||||
}
|
||||
|
||||
const res = await stream(baseUrl, apiKey, model.id, newMessages)
|
||||
|
||||
for (const hook of onAfterSendHooks.value) {
|
||||
await hook(sendingMessage)
|
||||
}
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const parser = useLlmmarkerParser({
|
||||
onLiteral: async (literal) => {
|
||||
for (const hook of onTokenLiteralHooks.value) {
|
||||
await hook(literal)
|
||||
}
|
||||
|
||||
streamingMessage.value.content += literal
|
||||
},
|
||||
onSpecial: async (special) => {
|
||||
for (const hook of onTokenSpecialHooks.value) {
|
||||
await hook(special)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
for await (const textPart of asyncIteratorFromReadableStream(res.textStream, async v => v)) {
|
||||
fullText += textPart
|
||||
await parser.consume(textPart)
|
||||
}
|
||||
|
||||
await parser.end()
|
||||
|
||||
for (const hook of onStreamEndHooks.value) {
|
||||
await hook()
|
||||
}
|
||||
|
||||
for (const hook of onAssistantResponseEndHooks.value) {
|
||||
await hook(fullText)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('LLM output:', fullText)
|
||||
}
|
||||
|
||||
return {
|
||||
sending,
|
||||
messages,
|
||||
streamingMessage,
|
||||
send,
|
||||
|
||||
@@ -4,7 +4,12 @@ import { defineStore } from 'pinia'
|
||||
export const useProvidersStore = defineStore('providers', () => {
|
||||
const providers = useLocalStorage<Record<string, Record<string, unknown>>>('settings/credentials/providers', {})
|
||||
|
||||
const coreControllerProvider = useLocalStorage<string>('settings/credentials/coreControllerProvider', 'openai')
|
||||
const audioSynthesisProvider = useLocalStorage<string>('settings/credentials/audioSynthesisProvider', 'elevenlabs')
|
||||
|
||||
return {
|
||||
providers,
|
||||
coreControllerProvider,
|
||||
audioSynthesisProvider,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user