feat: assistant templates startup

This commit is contained in:
2024-04-08 16:10:48 +08:00
parent 73dd6a21a7
commit 5442381e40
5 changed files with 290 additions and 108 deletions

View File

@@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type {PropType} from "vue"; import type {PropType} from 'vue'
import type {ChatMessage} from "~/typings/llm"; import type {ChatMessage} from '~/typings/llm'
import MessageResponding from "~/components/Icon/MessageResponding.vue"; import MessageResponding from '~/components/Icon/MessageResponding.vue'
const props = defineProps({ const props = defineProps({
message: { message: {
type: Object as PropType<ChatMessage>, type: Object as PropType<ChatMessage>,
required: true, required: true,
} },
}) })
const dayjs = useDayjs() const dayjs = useDayjs()
@@ -59,7 +59,10 @@ const message_background = computed(() => {
</span> </span>
</div> </div>
</Transition> </Transition>
<div v-if="message.create_at" class="chat-inside-extra"> <div v-if="message.preset" class="chat-inside-extra">
预设消息
</div>
<div v-else-if="message.create_at" class="chat-inside-extra">
{{ dayjs(message.create_at * 1000).format('YYYY-MM-DD HH:mm:ss') }} {{ dayjs(message.create_at * 1000).format('YYYY-MM-DD HH:mm:ss') }}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,139 @@
<script setup lang="ts">
import type {Assistant} from '~/typings/llm'
import {useLazyAsyncData} from '#app'
const loginState = useLoginState()
const props = defineProps()
const emit = defineEmits({
select: (assistant: Assistant | null) => true,
cancel: () => true,
})
const {
data: assistantTemplates,
pending: assistantTemplatesPending,
} = await useLazyAsyncData(
'App.Assistant_Template.GetList',
() => useFetchWrapped<
req.AssistantTemplateList & AuthedRequest, BaseResponse<PagedData<Assistant>>
>('App.Assistant_Template.GetList', {
user_id: loginState.user.id,
token: loginState.token as string,
page: 1,
perpage: 20,
}), {
server: false,
},
)
</script>
<template>
<div class="w-full h-full flex flex-col items-center gap-4 relative">
<Transition name="loading-screen">
<div v-if="assistantTemplatesPending"
class="absolute inset-0 bg-white dark:bg-neutral-900 flex justify-center items-center z-[1] text-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24">
<defs>
<filter id="svgSpinnersGooeyBalls20">
<feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/>
<feColorMatrix in="y" result="z" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7"/>
<feBlend in="SourceGraphic" in2="z"/>
</filter>
</defs>
<g filter="url(#svgSpinnersGooeyBalls20)">
<circle cx="5" cy="12" r="4" fill="currentColor">
<animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57"
repeatCount="indefinite" values="5;8;5"/>
</circle>
<circle cx="19" cy="12" r="4" fill="currentColor">
<animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57"
repeatCount="indefinite" values="19;16;19"/>
</circle>
<animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate"
values="0 12 12;360 12 12"/>
</g>
</svg>
</div>
</Transition>
<div class="w-full p-2">
<UButton
variant="ghost"
size="xs"
@click="emit('cancel')"
>
<template #leading>
<UIcon name="i-tabler-chevron-left"/>
</template>
<span>返回</span>
</UButton>
</div>
<div class="flex flex-col items-center gap-8">
<h1 class="text-lg font-medium flex flex-col items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path
d="M13.192 9h6.616a2 2 0 0 1 1.992 2.183l-.567 6.182A4 4 0 0 1 17.25 21h-1.5a4 4 0 0 1-3.983-3.635l-.567-6.182A2 2 0 0 1 13.192 9M15 13h.01M18 13h.01"/>
<path
d="M15 16.5c1 .667 2 .667 3 0m-9.368-.518A4.037 4.037 0 0 1 8.25 16h-1.5a4 4 0 0 1-3.983-3.635L2.2 6.183A2 2 0 0 1 4.192 4h6.616a2 2 0 0 1 2 2M6 8h.01M9 8h.01"/>
<path d="M6 12c.764-.51 1.528-.63 2.291-.36"/>
</g>
</svg>
<span>选择智能助手</span>
</h1>
<UButton
class="group ring-primary hover:ring-2 transition duration-300"
variant="soft"
size="lg"
:ui="{ rounded: 'rounded-full' }"
@click="emit('select', null)"
>
<span class="-mt-0.5">直接开始</span>
<template #trailing>
<span class="group-hover:translate-x-1 transition duration-300 ease-out relative w-3 h-full -mt-0.5">
<UIcon
name="i-tabler-arrow-right"
class="w-5 h-5 absolute top-auto bottom-auto right-0 opacity-0 group-hover:opacity-100 transition duration-300"
/>
<UIcon
name="i-tabler-chevron-right"
class="w-5 h-5 absolute top-auto bottom-auto right-0 -mr-[3.5px] group-hover:opacity-0 transition duration-300"
/>
</span>
</template>
</UButton>
</div>
<div
class="w-full md:w-3/4 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 overflow-y-auto p-4 md:p-8"
>
<div
v-for="assistant in assistantTemplates?.data.items || []"
:key="assistant.id"
class="assistant-item select-none"
@click="emit('select', assistant)"
>
<div class="flex flex-col gap-1">
<div class="text-base font-medium">{{ assistant.tpl_name }}</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400">{{ assistant.des }}</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.loading-screen-leave-active {
@apply transition duration-300;
}
.loading-screen-leave-to {
@apply opacity-0;
}
.assistant-item {
@apply w-full bg-white dark:bg-neutral-800 rounded-lg shadow-sm ring-primary ring-offset-2 dark:ring-offset-0 hover:ring-2 transition;
@apply flex items-center gap-4 px-4 py-2 cursor-pointer border dark:border-neutral-700 hover:border-transparent;
}
</style>

View File

@@ -15,7 +15,7 @@ import {uuidv4} from '@uniiem/uuid'
import {useLLM} from '~/composables/useLLM' import {useLLM} from '~/composables/useLLM'
import {trimObject} from '@uniiem/object-trim' import {trimObject} from '@uniiem/object-trim'
import ModalAuthentication from '~/components/ModalAuthentication.vue' import ModalAuthentication from '~/components/ModalAuthentication.vue'
import {useAsyncData} from '#app' import NewSessionScreen from '~/components/aigc/chat/NewSessionScreen.vue'
useHead({ useHead({
title: '聊天 | XSH AI', title: '聊天 | XSH AI',
@@ -25,7 +25,6 @@ const dayjs = useDayjs()
const toast = useToast() const toast = useToast()
const modal = useModal() const modal = useModal()
const loginState = useLoginState() const loginState = useLoginState()
const {token, user, is_logged_in} = storeToRefs(loginState)
const historyStore = useHistory() const historyStore = useHistory()
const {chatSessions} = storeToRefs(historyStore) const {chatSessions} = storeToRefs(historyStore)
const {setChatSessions} = historyStore const {setChatSessions} = historyStore
@@ -37,32 +36,13 @@ const showSidebar = ref(false)
const user_input = ref('') const user_input = ref('')
const responding = ref(false) const responding = ref(false)
const currentModel = ref<ModelTag>('spark3_5') const currentModel = ref<ModelTag>('spark3_5')
const currentAssistant = computed<Assistant | null>(() => getSessionCopyById(currentSessionId.value || '')?.assistant || null)
const modals = reactive({ const modals = reactive({
modelSelect: false, modelSelect: false,
assistantSelect: false,
newSessionScreen: false,
}) })
loginState.$subscribe((mutation, state) => {
console.log(mutation, state)
})
const {
data: assistantTemplates
} = await useAsyncData(
'App.Assistant_Template.GetList',
() => useFetchWrapped<
req.AssistantTemplateList & AuthedRequest, PagedData<Assistant>
>('App.Assistant_Template.GetList', {
user_id: loginState.user.id,
token: loginState.token as string,
page: 1,
perpage: 20,
}),
{
immediate: true,
watch: [is_logged_in],
},
)
/** /**
* 获取指定 ID 的会话数据 * 获取指定 ID 的会话数据
* @param chatSessionId * @param chatSessionId
@@ -98,39 +78,79 @@ const selectCurrentSessionId = (chatSessionId?: ChatSessionId) => {
} }
nextTick(() => { nextTick(() => {
showSidebar.value = false showSidebar.value = false
modals.newSessionScreen = false
scrollToMessageListBottom() scrollToMessageListBottom()
}) })
} }
/** /**
* 处理新建会话操作 * 创建新会话处理函数
* @param assistant 指定助手,不传或空值则不指定助手
*/ */
const handleClickCreateSession = () => { const createSession = (assistant: Assistant | null) => {
// 生成一个新的会话 ID // 生成一个新的会话 ID
const sessionId = uuidv4() const sessionId = uuidv4()
// 插入新会话数据 // 新会话数据
setChatSessions([ const newChat = !!assistant ? {
{
id: sessionId, id: sessionId,
subject: '新对话', subject: '新对话',
messages: [], messages: [],
create_at: dayjs().unix(), create_at: dayjs().unix(),
}, assistant,
} : {
id: sessionId,
subject: '新对话',
messages: [],
create_at: dayjs().unix(),
}
// 插入新会话数据
setChatSessions([
newChat,
...chatSessions.value, ...chatSessions.value,
]) ])
// 切换到新的会话 // 切换到新的会话
selectCurrentSessionId(sessionId) selectCurrentSessionId(sessionId)
// TODO: Model or Assistant Selection // 关闭新建会话屏幕
// modals.modelSelect = true modals.newSessionScreen = false
nextTick(() => { nextTick(() => {
if (!!currentAssistant.value) {
insetMessage({
id: uuidv4(),
role: 'system',
content: currentAssistant.value?.role || '',
preset: true,
})
insetMessage({
id: uuidv4(),
role: 'user',
content: `${currentAssistant.value?.target}${currentAssistant.value?.demand}`,
preset: true,
})
insetMessage({
id: uuidv4(),
role: 'assistant',
content: currentAssistant.value?.input_tpl || '',
preset: true,
})
} else {
insetMessage({ insetMessage({
id: uuidv4(), id: uuidv4(),
role: 'assistant', role: 'assistant',
content: '你好,有什么可以帮助你的吗?', content: '你好,有什么可以帮助你的吗?',
preset: true,
}) })
}
}) })
} }
/**
* 处理点击新建会话按钮事件
*/
const handleClickCreateSession = () => {
showSidebar.value = false
modals.newSessionScreen = true
}
/** /**
* 处理发送消息操作 * 处理发送消息操作
* @param event * @param event
@@ -273,7 +293,18 @@ onMounted(() => {
</div> </div>
</div> </div>
</div> </div>
<div class="h-[calc(100vh-4rem)] bg-white dark:bg-neutral-900 flex-1 flex flex-col"> <div class="h-[calc(100vh-4rem)] flex-1 bg-white dark:bg-neutral-900">
<Transition name="message" mode="out-in">
<NewSessionScreen
v-if="modals.newSessionScreen"
@select="createSession"
@cancel="modals.newSessionScreen = false"
/>
<div
v-else
class="w-full h-full flex flex-col"
>
<div <div
class="w-full p-4 bg-neutral-50 dark:bg-neutral-800/50 border-b dark:border-neutral-700/50 flex items-center gap-2"> class="w-full p-4 bg-neutral-50 dark:bg-neutral-800/50 border-b dark:border-neutral-700/50 flex items-center gap-2">
<UButton <UButton
@@ -296,7 +327,6 @@ onMounted(() => {
/> />
</TransitionGroup> </TransitionGroup>
</div> </div>
<pre>{{assistantTemplates}}</pre>
</div> </div>
<ClientOnly> <ClientOnly>
<div <div
@@ -308,10 +338,13 @@ onMounted(() => {
{{ llmModels.find(m => m.tag === currentModel)?.name.toUpperCase() || '模型' }} {{ llmModels.find(m => m.tag === currentModel)?.name.toUpperCase() || '模型' }}
</span> </span>
</button> </button>
<button class="chat-option-btn" @click="modals.modelSelect = true"> <button
v-if="currentAssistant?.tpl_name"
class="chat-option-btn"
>
<Icon name="tabler:robot-face"/> <Icon name="tabler:robot-face"/>
<span class="text-xs"> <span class="text-xs">
助手 {{ currentAssistant.tpl_name }}
</span> </span>
</button> </button>
</div> </div>
@@ -340,16 +373,18 @@ onMounted(() => {
</div> </div>
</ClientOnly> </ClientOnly>
</div> </div>
</Transition>
</div>
<!-- Modals --> <!-- Modals -->
<UModal prevent-close v-model="modals.modelSelect"> <UModal v-model="modals.modelSelect">
<UCard> <UCard>
<template #header> <template #header>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
选择大语言模型 选择大语言模型
</h3> </h3>
</template> </template>
<div class="grid grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div <div
v-for="(llm, index) in llmModels" v-for="(llm, index) in llmModels"
@@ -358,14 +393,13 @@ onMounted(() => {
class="flex flex-col gap-2 justify-center items-center w-full aspect-[1/1] border-2 rounded-xl cursor-pointer transition duration-150 select-none" class="flex flex-col gap-2 justify-center items-center w-full aspect-[1/1] border-2 rounded-xl cursor-pointer transition duration-150 select-none"
:class="llm.tag === currentModel ? 'border-primary shadow-xl bg-primary text-white' : 'dark:border-neutral-800 bg-white dark:bg-black shadow-card'" :class="llm.tag === currentModel ? 'border-primary shadow-xl bg-primary text-white' : 'dark:border-neutral-800 bg-white dark:bg-black shadow-card'"
> >
<Icon :name="llm?.icon" class="text-4xl opacity-80"/> <Icon v-if="llm?.icon" :name="llm.icon" class="text-4xl opacity-80"/>
<div class="flex flex-col gap-0.5 items-center"> <div class="flex flex-col gap-0.5 items-center">
<h1 class="font-bold drop-shadow opacity-90">{{ llm.name || 'unknown' }}</h1> <h1 class="font-bold drop-shadow opacity-90">{{ llm.name || 'unknown' }}</h1>
<p class="text-xs opacity-60">{{ llm.description }}</p> <p class="text-xs opacity-60">{{ llm.description }}</p>
</div> </div>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<div class="flex justify-end items-center" @click="modals.modelSelect = false"> <div class="flex justify-end items-center" @click="modals.modelSelect = false">
<UButton> <UButton>
@@ -375,6 +409,7 @@ onMounted(() => {
</template> </template>
</UCard> </UCard>
</UModal> </UModal>
</div> </div>
</template> </template>

View File

@@ -76,6 +76,7 @@ export interface ChatSession {
create_at: number create_at: number
messages: ChatMessage[] messages: ChatMessage[]
last_input?: string last_input?: string
assistant?: Assistant
} }
export type MessageRole = 'user' | 'assistant' | 'system' export type MessageRole = 'user' | 'assistant' | 'system'
@@ -84,6 +85,7 @@ export interface ChatMessage {
id: ChatMessageId id: ChatMessageId
role: MessageRole role: MessageRole
content: string content: string
preset?: boolean
create_at?: number create_at?: number
interrupted?: boolean interrupted?: boolean
} }

5
typings/types.d.ts vendored
View File

@@ -11,7 +11,10 @@ interface BaseResponse<T> {
// TODO: PagedData schema // TODO: PagedData schema
interface PagedData<T> { interface PagedData<T> {
total: number
page: number
perpage: number
items: T[]
} }
interface UserSchema { interface UserSchema {