feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能 - 增加智能体聊天,并对接持久化
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
ChatTimelineItem as ChatTimelineItemType,
|
||||
ChatTimelineMessageItem,
|
||||
ChatTimelineToolApprovalPayload,
|
||||
} from './types';
|
||||
|
||||
import {nextTick, onBeforeUnmount, ref, watch} from 'vue';
|
||||
|
||||
import ChatTimelineItem from './ChatTimelineItem.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
approvalLoading?: boolean;
|
||||
copyable?: (item: ChatTimelineMessageItem) => boolean;
|
||||
emptyText?: string;
|
||||
items: ChatTimelineItemType[];
|
||||
regenerable?: (item: ChatTimelineMessageItem) => boolean;
|
||||
regenerateDisabled?: boolean;
|
||||
variantLoading?: (item: ChatTimelineMessageItem) => boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
approve: [payload: ChatTimelineToolApprovalPayload];
|
||||
copyMessage: [item: ChatTimelineMessageItem];
|
||||
regenerateMessage: [item: ChatTimelineMessageItem];
|
||||
reject: [payload: ChatTimelineToolApprovalPayload];
|
||||
selectNextVariant: [item: ChatTimelineMessageItem];
|
||||
selectPreviousVariant: [item: ChatTimelineMessageItem];
|
||||
}>();
|
||||
|
||||
const containerRef = ref<HTMLElement>();
|
||||
const isPinnedToBottom = ref(true);
|
||||
const suppressNextAutoScroll = ref(false);
|
||||
let preservedScrollTop: number | undefined;
|
||||
|
||||
const bottomThreshold = 24;
|
||||
let scrollFrame = 0;
|
||||
|
||||
function isNearBottom(container: HTMLElement) {
|
||||
return (
|
||||
container.scrollHeight - container.scrollTop - container.clientHeight <=
|
||||
bottomThreshold
|
||||
);
|
||||
}
|
||||
|
||||
function updatePinnedState() {
|
||||
const container = containerRef.value;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
isPinnedToBottom.value = isNearBottom(container);
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (scrollFrame) {
|
||||
cancelAnimationFrame(scrollFrame);
|
||||
}
|
||||
scrollFrame = requestAnimationFrame(() => {
|
||||
const container = containerRef.value;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
container.scrollTop = container.scrollHeight;
|
||||
updatePinnedState();
|
||||
});
|
||||
}
|
||||
|
||||
function handleTimelineScroll() {
|
||||
updatePinnedState();
|
||||
}
|
||||
|
||||
function handleThinkingToggle() {
|
||||
preservedScrollTop = containerRef.value?.scrollTop;
|
||||
suppressNextAutoScroll.value = true;
|
||||
}
|
||||
|
||||
function canCopyMessage(item: ChatTimelineItemType) {
|
||||
return item.type === 'message' && (props.copyable?.(item) ?? false);
|
||||
}
|
||||
|
||||
function canRegenerateMessage(item: ChatTimelineItemType) {
|
||||
return item.type === 'message' && (props.regenerable?.(item) ?? false);
|
||||
}
|
||||
|
||||
function isAssistantActionAnchor(
|
||||
item: ChatTimelineItemType,
|
||||
index: number,
|
||||
items: ChatTimelineItemType[],
|
||||
) {
|
||||
if (item.type !== 'message' || item.role !== 'assistant' || !item.roundId) {
|
||||
return false;
|
||||
}
|
||||
for (let cursor = items.length - 1; cursor >= 0; cursor -= 1) {
|
||||
const current = items[cursor];
|
||||
if (
|
||||
current &&
|
||||
current.type === 'message' &&
|
||||
current.role === 'assistant' &&
|
||||
current.roundId === item.roundId
|
||||
) {
|
||||
return cursor === index;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isVariantLoading(item: ChatTimelineItemType) {
|
||||
return item.type === 'message' && (props.variantLoading?.(item) ?? false);
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (scrollFrame) {
|
||||
cancelAnimationFrame(scrollFrame);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.items,
|
||||
async () => {
|
||||
if (suppressNextAutoScroll.value) {
|
||||
suppressNextAutoScroll.value = false;
|
||||
await nextTick();
|
||||
if (preservedScrollTop !== undefined && containerRef.value) {
|
||||
containerRef.value.scrollTop = preservedScrollTop;
|
||||
}
|
||||
preservedScrollTop = undefined;
|
||||
updatePinnedState();
|
||||
return;
|
||||
}
|
||||
if (isPinnedToBottom.value) {
|
||||
scrollToBottom();
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="chat-timeline"
|
||||
@scroll.passive="handleTimelineScroll"
|
||||
>
|
||||
<div v-if="items.length === 0" class="chat-timeline__empty">
|
||||
<div class="chat-timeline__empty-icon" aria-hidden="true"></div>
|
||||
<div class="chat-timeline__empty-text">
|
||||
{{ emptyText || '开始对话' }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<ChatTimelineItem
|
||||
v-for="(item, index) in items"
|
||||
:key="item.id"
|
||||
:assistant-actions-visible="isAssistantActionAnchor(item, index, items)"
|
||||
:item="item"
|
||||
:approval-loading="approvalLoading"
|
||||
:copyable="canCopyMessage(item)"
|
||||
:regenerable="canRegenerateMessage(item)"
|
||||
:regenerate-disabled="regenerateDisabled"
|
||||
:variant-loading="isVariantLoading(item)"
|
||||
@approve="emit('approve', $event)"
|
||||
@copy-message="emit('copyMessage', $event)"
|
||||
@regenerate-message="emit('regenerateMessage', $event)"
|
||||
@reject="emit('reject', $event)"
|
||||
@select-next-variant="emit('selectNextVariant', $event)"
|
||||
@select-previous-variant="emit('selectPreviousVariant', $event)"
|
||||
@thinking-toggle="handleThinkingToggle"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-timeline {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chat-timeline__empty-icon {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: 0 auto;
|
||||
background: var(--el-color-primary-light-9);
|
||||
border: 1px solid var(--el-color-primary-light-7);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.chat-timeline__empty {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 180px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.chat-timeline__empty-text {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user