187 lines
4.4 KiB
Vue
187 lines
4.4 KiB
Vue
<template>
|
|
<section class="ai-chat-panel" :aria-label="`${props.storeName || '当前门店'}经营助手`">
|
|
<ai-chat-message-list
|
|
ref="messageList"
|
|
:messages="messages"
|
|
:is-generating="isGenerating"
|
|
:quick-question-list="aiQuickQuestionList"
|
|
@select-question="selectQuestion"
|
|
/>
|
|
<div class="bottom-area">
|
|
<div v-if="activeToolStatus" class="tool-status">
|
|
<t-icon name="loading" size="1rem" />
|
|
<span v-text="activeToolStatus" />
|
|
</div>
|
|
<ai-chat-composer
|
|
v-model="draft"
|
|
:is-generating="isGenerating"
|
|
:disabled="isLoadingConversation"
|
|
:last-error="lastError"
|
|
:can-retry="canRetry"
|
|
@send="submitMessage"
|
|
@stop="stopGeneration"
|
|
@retry="retryLastMessage"
|
|
/>
|
|
</div>
|
|
</section>
|
|
<ai-conversation-list
|
|
v-model:visible="conversationVisible"
|
|
:active-conversation-id="conversationId"
|
|
:disabled="isBusy"
|
|
:session-key="props.sessionKey"
|
|
@select="selectConversation"
|
|
@delete="handleConversationDelete"
|
|
/>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import { useAiChat } from "@/composables/useAiChat";
|
|
import type { GaoAiConversation } from "@/api/GaoAiAPI";
|
|
import AiChatComposer from "@/pages/ai/components/AiChatComposer.vue";
|
|
import AiConversationList from "@/pages/ai/components/AiConversationList.vue";
|
|
import AiChatMessageList from "@/pages/ai/components/AiChatMessageList.vue";
|
|
import { aiQuickQuestionList } from "@/pages/ai/aiChatQuestions";
|
|
import { useNavBarStore } from "@/store/navBarStore";
|
|
import { getErrorMessage } from "@/utils/error";
|
|
import { Message } from "@/utils/message";
|
|
|
|
defineOptions({
|
|
name: "AiChatPanel"
|
|
});
|
|
|
|
interface Props {
|
|
storeName: string;
|
|
sessionKey: string;
|
|
storeId?: string;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
const navBarStore = useNavBarStore();
|
|
type MessageListExpose = {
|
|
scrollToBottom: () => Promise<void>;
|
|
};
|
|
|
|
const messageListRef = useTemplateRef<MessageListExpose>("messageList");
|
|
const draft = shallowRef("");
|
|
const conversationVisible = ref(false);
|
|
const {
|
|
messages,
|
|
conversationId,
|
|
isGenerating,
|
|
isLoadingConversation,
|
|
activeToolStatus,
|
|
lastError,
|
|
canRetry,
|
|
sendMessage,
|
|
loadConversation,
|
|
stopGeneration,
|
|
retryLastMessage,
|
|
resetConversation
|
|
} = useAiChat({
|
|
sessionKey: () => props.sessionKey,
|
|
storeId: () => props.storeId,
|
|
scrollToBottom
|
|
});
|
|
const isBusy = computed(() => isGenerating.value || isLoadingConversation.value);
|
|
|
|
async function submitMessage(message: string): Promise<void> {
|
|
await sendMessage(message);
|
|
}
|
|
|
|
function openConversationList(): void {
|
|
if (!isBusy.value) {
|
|
conversationVisible.value = true;
|
|
}
|
|
}
|
|
|
|
async function selectConversation(conversation: GaoAiConversation): Promise<void> {
|
|
if (isBusy.value || !conversation.id) {
|
|
return;
|
|
}
|
|
try {
|
|
await loadConversation(conversation.id);
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "历史会话读取失败"));
|
|
}
|
|
}
|
|
|
|
function handleConversationDelete(deletedConversationId: string): void {
|
|
if (deletedConversationId === conversationId.value) {
|
|
resetConversation();
|
|
}
|
|
}
|
|
|
|
function selectQuestion(question: string): void {
|
|
draft.value = question;
|
|
}
|
|
|
|
async function scrollToBottom(): Promise<void> {
|
|
await messageListRef.value?.scrollToBottom();
|
|
}
|
|
|
|
onMounted(() => {
|
|
navBarStore.clearRight();
|
|
navBarStore.setRightText("历史", "var(--td-brand-color)", openConversationList);
|
|
});
|
|
|
|
watch(isBusy, value => navBarStore.setRightDisabled(value));
|
|
|
|
onUnmounted(() => navBarStore.clearRight());
|
|
</script>
|
|
<style scoped lang="less">
|
|
.ai-chat-panel {
|
|
height: 100%;
|
|
display: flex;
|
|
position: relative;
|
|
overflow: hidden;
|
|
min-height: 0;
|
|
background: var(--ai-background);
|
|
padding-top: var(--app-nav-height);
|
|
flex-direction: column;
|
|
|
|
.bottom-area {
|
|
inset: auto 1rem calc(1rem + env(safe-area-inset-bottom, 0px));
|
|
width: auto;
|
|
margin: 0 auto;
|
|
z-index: 2;
|
|
position: absolute;
|
|
max-width: 40rem;
|
|
|
|
.tool-status {
|
|
gap: .375rem;
|
|
margin-bottom: .5rem;
|
|
color: #0f766e;
|
|
padding: .5rem .75rem;
|
|
border-radius: .75rem;
|
|
display: flex;
|
|
font-size: .75rem;
|
|
background: var(--ai-soft);
|
|
align-items: center;
|
|
|
|
.t-icon {
|
|
animation: rotate 1s linear infinite;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes rotate {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
:global(:root) {
|
|
--ai-background: rgba(255, 255, 255, .98);
|
|
--ai-bubble: #f5f6f8;
|
|
--ai-input: white;
|
|
--ai-soft: #f0fdfa;
|
|
}
|
|
|
|
:global(html.theme-dark) {
|
|
--ai-background: rgba(15, 23, 42, .98);
|
|
--ai-bubble: #1e293b;
|
|
--ai-input: #111827;
|
|
--ai-soft: rgba(20, 184, 166, .12);
|
|
}
|
|
</style>
|