|
<script setup lang="ts"> |
|
import { IMessage } from '../../interface'; |
|
import { computed, ref, watch } from 'vue'; |
|
import ChatMessage from './message.vue'; |
|
import { useAppStore } from '../../store'; |
|
import { chat } from '../../api'; |
|
import { SystemPrompts, UserPrompts } from '../../contants'; |
|
import { useI18n } from 'vue-i18n'; |
|
|
|
interface IProps { |
|
ask: string |
|
contexts?: Record<string, any>[] |
|
query: string |
|
answer: string |
|
|
|
clear?: boolean |
|
} |
|
|
|
interface IEmit { |
|
(e: 'message', val: string): void |
|
} |
|
|
|
const { t } = useI18n(); |
|
|
|
const appStore = useAppStore(); |
|
const loading = ref(false); |
|
const props = defineProps<IProps>(); |
|
const emits = defineEmits<IEmit>(); |
|
|
|
const messages = ref<IMessage[]>([]); |
|
|
|
let abortCtrl: AbortController | null = null; |
|
|
|
const queryMessages = computed(() => { |
|
const contexts = props.contexts || []; |
|
const strs = contexts.map((context) => { |
|
return `[${context.id}]: ${context.snippet}`; |
|
}); |
|
const ctx = UserPrompts.replace('%s', strs.join('\n')); |
|
|
|
const current: IMessage = { |
|
role: 'user', |
|
content: props.ask |
|
}; |
|
|
|
const firstMessage: IMessage = { |
|
role: 'user', |
|
content: `${ctx}\n${props.query}` |
|
}; |
|
|
|
const secondMessage: IMessage = { |
|
role: 'assistant', |
|
content: props.answer |
|
}; |
|
|
|
const chatMessages = messages.value.filter((message) => { |
|
return !!message.content; |
|
}); |
|
|
|
return [ |
|
firstMessage, |
|
secondMessage, |
|
...chatMessages, |
|
current |
|
]; |
|
}); |
|
|
|
watch(() => props.ask, (ask) => { |
|
if (ask && !loading.value) { |
|
messages.value.push({ |
|
role: 'user', |
|
content: ask |
|
}); |
|
startChat(); |
|
} |
|
}); |
|
|
|
watch(() => props.clear, (clear) => { |
|
if (clear) { |
|
messages.value = []; |
|
loading.value = false; |
|
} |
|
}); |
|
|
|
const enableReload = computed(() => { |
|
const length = messages.value.length; |
|
return length > 1 && messages.value[length - 1].role === 'user'; |
|
}); |
|
|
|
const onClearMessage = () => { |
|
messages.value = []; |
|
}; |
|
|
|
async function startChat() { |
|
if (abortCtrl) { |
|
abortCtrl.abort(); |
|
abortCtrl = null; |
|
} |
|
const ctrl = new AbortController(); |
|
abortCtrl = ctrl; |
|
loading.value = true; |
|
const { model, enableLocal, localModel } = appStore; |
|
const modelName = enableLocal ? localModel : model?.split(':')[1]; |
|
messages.value.push({ |
|
role: 'assistant', |
|
content: '' |
|
}); |
|
const answer = messages.value[messages.value.length - 1]; |
|
await chat(queryMessages.value, { |
|
ctrl, |
|
model: modelName, |
|
locally: enableLocal, |
|
system: SystemPrompts, |
|
onMessage: (message) => { |
|
answer.content += message.text || ''; |
|
emits('message', answer.content); |
|
} |
|
}); |
|
|
|
abortCtrl = null; |
|
loading.value = false; |
|
if (!answer.content) { |
|
messages.value.pop(); |
|
} |
|
} |
|
</script> |
|
|
|
<script lang="ts"> |
|
export default { |
|
name: 'ChatPage', |
|
}; |
|
</script> |
|
|
|
<template> |
|
<div class="w-full"> |
|
<div v-if="!messages.length" class="w-full text-center text-zinc-500"> |
|
{{ t('tips.continue') }} |
|
</div> |
|
<div class="mt-4 w-full"> |
|
<div v-for="(message, index) in messages" :key="index" class="mt-4"> |
|
<t-loading v-if="message.role === 'assistant' && !message.content" size="small" /> |
|
<ChatMessage v-else :enable-reload="enableReload" :message="message" /> |
|
</div> |
|
</div> |
|
<div v-if="messages.length && !loading" class="mt-4 flex items-center justify-center"> |
|
<t-button |
|
shape="round" |
|
:disabled="loading" |
|
:loading="loading" |
|
@click="onClearMessage" |
|
> |
|
{{ t('btn.clear') }} |
|
</t-button> |
|
</div> |
|
</div> |
|
</template> |