add aiChat
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user