add aiChat

This commit is contained in:
Timi
2026-09-20 12:03:03 +08:00
parent 3d2968cb3b
commit b435bf992e
8 changed files with 1782 additions and 2 deletions
+221
View File
@@ -0,0 +1,221 @@
<template>
<section class="ai-chat-panel" :aria-label="props.ariaLabel">
<ai-chat-message-list
ref="messageList"
:messages="props.messages"
:is-generating="props.isGenerating"
:loading="props.isLoading"
:loading-text="props.loadingText"
:aria-label="props.messageListAriaLabel"
:role-names="props.roleNames"
:role-name-formatter="props.roleNameFormatter"
:thinking-title="props.thinkingTitle"
:created-at-label="props.createdAtLabel"
:duration-label="props.durationLabel"
:format-message-status="props.formatMessageStatus"
>
<template v-if="$slots.welcome" #welcome="slotProps">
<slot name="welcome" v-bind="slotProps" />
</template>
<template v-if="$slots['assistant-icon']" #assistant-icon="slotProps">
<slot name="assistant-icon" v-bind="slotProps" />
</template>
<template v-if="$slots['user-icon']" #user-icon="slotProps">
<slot name="user-icon" v-bind="slotProps" />
</template>
<template v-if="$slots.message" #message="slotProps">
<slot name="message" v-bind="slotProps" />
</template>
<template v-if="$slots['role-name']" #role-name="slotProps">
<slot name="role-name" v-bind="slotProps" />
</template>
<template v-if="$slots.thinking" #thinking="slotProps">
<slot name="thinking" v-bind="slotProps" />
</template>
<template v-if="$slots.citation" #citation="slotProps">
<slot name="citation" v-bind="slotProps" />
</template>
<template v-if="$slots['message-status']" #message-status="slotProps">
<slot name="message-status" v-bind="slotProps" />
</template>
</ai-chat-message-list>
<div class="bottom-area">
<slot name="tool-status" :status="props.activeToolStatus">
<div v-if="props.activeToolStatus" class="tool-status">
<t-icon name="loading" size="1rem" />
<span v-text="props.activeToolStatus" />
</div>
</slot>
<slot
name="composer"
:draft="draft"
:is-generating="props.isGenerating"
:is-loading="props.isLoading"
:last-error="props.lastError"
:can-retry="props.canRetry"
:on-send="submitMessage"
:on-stop="() => emit('stop')"
:on-retry="() => emit('retry')"
>
<ai-chat-composer
v-model="draft"
:is-generating="props.isGenerating"
:disabled="props.disabled || props.isLoading"
:last-error="props.lastError"
:can-retry="props.canRetry"
:placeholder="props.placeholder"
:send-text="props.sendText"
:stop-text="props.stopText"
:retry-text="props.retryText"
:max-length="props.maxLength"
@send="submitMessage"
@stop="emit('stop')"
@retry="emit('retry')"
/>
</slot>
</div>
</section>
</template>
<script setup lang="ts">
import { shallowRef, useTemplateRef } from "vue";
import type {
AiChatMessage,
AiChatMessageStatusFormatter,
AiChatRoleNameFormatter,
AiChatRoleNames
} from "./types";
import AiChatMessageList from "./AiChatMessageList.vue";
import AiChatComposer from "./AiChatComposer.vue";
defineOptions({
name: "AiChatPanel"
});
export interface Props {
messages: readonly AiChatMessage[];
isGenerating: boolean;
isLoading?: boolean;
activeToolStatus?: string;
lastError?: string;
canRetry?: boolean;
disabled?: boolean;
loadingText?: string;
ariaLabel?: string;
messageListAriaLabel?: string;
roleNames?: AiChatRoleNames;
roleNameFormatter?: AiChatRoleNameFormatter;
thinkingTitle?: string;
createdAtLabel?: string;
durationLabel?: string;
formatMessageStatus?: AiChatMessageStatusFormatter;
placeholder?: string;
sendText?: string;
stopText?: string;
retryText?: string;
maxLength?: number;
}
const props = withDefaults(defineProps<Props>(), {
isLoading: false,
activeToolStatus: "",
lastError: "",
canRetry: false,
disabled: false,
loadingText: "正在加载…",
ariaLabel: "AI 对话",
messageListAriaLabel: "聊天消息",
roleNames: () => ({ assistant: "助手", user: "我" }),
thinkingTitle: "正在思考…",
createdAtLabel: "生成时间:",
durationLabel: "耗时:",
placeholder: "输入消息…",
sendText: "发送",
stopText: "停止",
retryText: "重试",
maxLength: 2000
});
const emit = defineEmits<{
send: [message: string];
stop: [];
retry: [];
}>();
const messageListRef = useTemplateRef<{ scrollToBottom: () => Promise<void> }>("messageList");
const draft = shallowRef("");
function submitMessage(message: string): void {
emit("send", message);
}
async function scrollToBottom(): Promise<void> {
await messageListRef.value?.scrollToBottom();
}
defineExpose({
scrollToBottom
});
</script>
<style scoped lang="less">
.ai-chat-panel {
--ai-chat-background: var(--td-bg-color-page, #fff);
--ai-chat-bubble: var(--td-bg-color-container, #f5f6f8);
--ai-chat-accent: var(--td-brand-color, #0f766e);
--ai-chat-accent-soft: #ccfbf1;
--ai-chat-soft: #f0fdfa;
--ai-chat-user-accent: #2563eb;
--ai-chat-user-accent-soft: #dbeafe;
height: 100%;
display: flex;
position: relative;
overflow: hidden;
min-height: 0;
background: var(--ai-chat-background);
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: var(--ai-chat-accent);
padding: .5rem .75rem;
border-radius: .75rem;
display: flex;
font-size: .75rem;
background: var(--ai-chat-soft);
align-items: center;
.t-icon {
animation: rotate 1s linear infinite;
}
}
}
}
@keyframes rotate {
to {
transform: rotate(360deg);
}
}
:global(html.theme-dark) {
.ai-chat-panel {
--ai-chat-background: var(--td-bg-color-page, #0f172a);
--ai-chat-bubble: var(--td-bg-color-container, #1e293b);
--ai-chat-accent-soft: rgba(20, 184, 166, .12);
--ai-chat-soft: rgba(20, 184, 166, .12);
--ai-chat-user-accent-soft: rgba(37, 99, 235, .18);
}
}
</style>