Merge remote-tracking branch 'origin/feature/v1.3_主agent_rxd' into test
This commit is contained in:
@ -1,15 +1,14 @@
|
|||||||
<script lang="tsx">
|
<script lang="tsx">
|
||||||
import { message as antdMessage, Tooltip } from 'ant-design-vue';
|
import { message as antdMessage } from 'ant-design-vue';
|
||||||
import { BubbleList } from '@/components/xt-chat/xt-bubble';
|
import { BubbleList } from '@/components/xt-chat/xt-bubble';
|
||||||
import SenderInput from './components/sender-input/index.vue';
|
import SenderInput from './components/sender-input/index.vue';
|
||||||
import RightView from './components/right-view/index.vue';
|
import RightView from './components/right-view/index.vue';
|
||||||
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useChatStore } from '@/stores/modules/chat';
|
import { useChatStore } from '@/stores/modules/chat';
|
||||||
import { getConversationList } from '@/api/all/chat';
|
import { getConversationList } from '@/api/all/chat';
|
||||||
import querySSE from '@/utils/querySSE';
|
import querySSE from '@/utils/querySSE';
|
||||||
import useChatHandler from './useChatHandler';
|
import useChatHandler from './useChatHandler';
|
||||||
import { QUESTION_ROLE, LOADING_ROLE, REMOTE_ROLE } from './constants';
|
import { QUESTION_ROLE, LOADING_ROLE } from './constants';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@ -85,6 +84,7 @@ export default {
|
|||||||
agent_id: chatStore.agentInfo.agent_id,
|
agent_id: chatStore.agentInfo.agent_id,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize SSE:', error);
|
console.error('Failed to initialize SSE:', error);
|
||||||
antdMessage.error('初始化连接失败');
|
antdMessage.error('初始化连接失败');
|
||||||
@ -128,6 +128,7 @@ export default {
|
|||||||
role: QUESTION_ROLE,
|
role: QUESTION_ROLE,
|
||||||
content: message,
|
content: message,
|
||||||
});
|
});
|
||||||
|
|
||||||
initSse(newVal);
|
initSse(newVal);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -226,15 +226,13 @@ export default function useChatHandler(options: UseChatHandlerOptions): UseChatH
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isCollapse && (
|
<div class="relative thought-chain-item" style={{ display: isCollapse ? 'block' : 'none' }}>
|
||||||
<div class="relative thought-chain-item">
|
|
||||||
<div class="flex items-center mb-4px">
|
<div class="flex items-center mb-4px">
|
||||||
<img src={isRulCompleted ? icon1 : icon2} width={13} height={13} class="mr-4px" />
|
<img src={isRulCompleted ? icon1 : icon2} width={13} height={13} class="mr-4px" />
|
||||||
<div>{node}</div>
|
<div>{node}</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-html={md.render(output)} class={outputEleClass} />
|
<div v-html={md.render(output)} class={outputEleClass} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{customRender?.()}
|
{customRender?.()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -118,7 +118,7 @@ export interface BubbleProps<ContentType extends BubbleContentType = string>
|
|||||||
*/
|
*/
|
||||||
export interface BubbleRef {
|
export interface BubbleRef {
|
||||||
/** 气泡组件的原生DOM元素 */
|
/** 气泡组件的原生DOM元素 */
|
||||||
nativeElement: HTMLElement;
|
bubbleElement: HTMLElement;
|
||||||
/** 中止当前打字效果并立即展示完整内容 */
|
/** 中止当前打字效果并立即展示完整内容 */
|
||||||
abortTyping: VoidFunction;
|
abortTyping: VoidFunction;
|
||||||
}
|
}
|
||||||
@ -138,7 +138,7 @@ export interface BubbleContextProps {
|
|||||||
*/
|
*/
|
||||||
export interface BubbleListRef {
|
export interface BubbleListRef {
|
||||||
/** 气泡列表的原生DOM元素 */
|
/** 气泡列表的原生DOM元素 */
|
||||||
nativeElement: HTMLDivElement;
|
bubbleElement: HTMLDivElement;
|
||||||
/** 滚动到指定位置的方法 */
|
/** 滚动到指定位置的方法 */
|
||||||
scrollTo: (info: {
|
scrollTo: (info: {
|
||||||
/** 滚动偏移量 */
|
/** 滚动偏移量 */
|
||||||
@ -180,8 +180,6 @@ export type RolesType = Record<string, RoleType> | ((bubbleDataP: BubbleDataType
|
|||||||
* 定义气泡列表组件的所有可配置属性
|
* 定义气泡列表组件的所有可配置属性
|
||||||
*/
|
*/
|
||||||
export interface BubbleListProps extends /* @vue-ignore */ HTMLAttributes {
|
export interface BubbleListProps extends /* @vue-ignore */ HTMLAttributes {
|
||||||
/** 组件前缀类名 */
|
|
||||||
prefixCls?: string;
|
|
||||||
/** 根元素的自定义类名 */
|
/** 根元素的自定义类名 */
|
||||||
rootClassName?: string;
|
rootClassName?: string;
|
||||||
/** 气泡数据数组 */
|
/** 气泡数据数组 */
|
||||||
|
|||||||
@ -15,6 +15,8 @@ export default defineComponent({
|
|||||||
const props = attrs as unknown as BubbleProps<BubbleContentType> & { style?: any; class?: any };
|
const props = attrs as unknown as BubbleProps<BubbleContentType> & { style?: any; class?: any };
|
||||||
|
|
||||||
const content = ref<BubbleContentType>(props.content ?? '');
|
const content = ref<BubbleContentType>(props.content ?? '');
|
||||||
|
const bubbleElement = ref<HTMLDivElement>(null);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.content,
|
() => props.content,
|
||||||
(val) => {
|
(val) => {
|
||||||
@ -103,10 +105,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
expose({
|
expose({
|
||||||
abortTyping,
|
abortTyping,
|
||||||
|
bubbleElement,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<div class={mergedCls.value} style={{ ...(props.style || {}) }}>
|
<div class={mergedCls.value} style={{ ...(props.style || {}) }} ref={bubbleElement}>
|
||||||
{(slots.avatar || props.avatar) && (
|
{(slots.avatar || props.avatar) && (
|
||||||
<div class={[`${prefixCls}-avatar`, props.classNames?.avatar]} style={props.styles?.avatar}>
|
<div class={[`${prefixCls}-avatar`, props.classNames?.avatar]} style={props.styles?.avatar}>
|
||||||
{avatarNode.value}
|
{avatarNode.value}
|
||||||
|
|||||||
@ -23,9 +23,34 @@ import {
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'BubbleList',
|
name: 'BubbleList',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: {},
|
// 正确声明 props,提供默认值,确保 TSX 下默认值生效
|
||||||
setup(_, { attrs, slots, expose }) {
|
props: {
|
||||||
const props = attrs as unknown as BubbleListProps & { class?: any; style?: any };
|
autoScroll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array as () => BubbleListProps['items'],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
type: Object as () => RolesType,
|
||||||
|
default: () => ({} as RolesType),
|
||||||
|
},
|
||||||
|
rootClassName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
type: Object as () => Record<string, any>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
type: [String, Array, Object] as unknown as () => any,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { attrs, slots, expose }) {
|
||||||
const passThroughAttrs = useAttrs();
|
const passThroughAttrs = useAttrs();
|
||||||
|
|
||||||
const TOLERANCE = 1;
|
const TOLERANCE = 1;
|
||||||
@ -63,14 +88,16 @@ export default defineComponent({
|
|||||||
// scroll
|
// scroll
|
||||||
const [scrollReachEnd, setScrollReachEnd] = useState(true);
|
const [scrollReachEnd, setScrollReachEnd] = useState(true);
|
||||||
const [updateCount, setUpdateCount] = useState(0);
|
const [updateCount, setUpdateCount] = useState(0);
|
||||||
|
// 首次挂载后仅自动滚动一次
|
||||||
|
const didInitialAutoScroll = ref(false);
|
||||||
|
|
||||||
const onInternalScroll = (e: Event) => {
|
const onInternalScroll = (e: Event) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
setScrollReachEnd(target.scrollHeight - Math.abs(target.scrollTop) - target.clientHeight <= TOLERANCE);
|
setScrollReachEnd(target.scrollHeight - Math.abs(target.scrollTop) - target.clientHeight <= TOLERANCE);
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(updateCount, () => {
|
watch([updateCount, scrollReachEnd, listRef], () => {
|
||||||
if ((props.autoScroll ?? true) && unref(listRef) && unref(scrollReachEnd)) {
|
if (props.autoScroll && unref(listRef) && unref(scrollReachEnd)) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
unref(listRef)!.scrollTo({ top: unref(listRef)!.scrollHeight });
|
unref(listRef)!.scrollTo({ top: unref(listRef)!.scrollHeight });
|
||||||
});
|
});
|
||||||
@ -79,26 +106,24 @@ export default defineComponent({
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => unref(displayData).length,
|
() => unref(displayData).length,
|
||||||
() => {
|
(newLen, oldLen) => {
|
||||||
if (props.autoScroll ?? true) {
|
if (!props.autoScroll) return;
|
||||||
const lastItemKey = unref(displayData)[unref(displayData).length - 2]?.key;
|
// 首次渲染:当有内容时滚到底部一次
|
||||||
const bubbleInst = unref(bubbleRefs)[lastItemKey!];
|
if (!didInitialAutoScroll.value && newLen > 0) {
|
||||||
if (bubbleInst) {
|
scrollToBottom('auto');
|
||||||
const { nativeElement } = bubbleInst;
|
didInitialAutoScroll.value = true;
|
||||||
const { top = 0, bottom = 0 } = nativeElement?.getBoundingClientRect() ?? {};
|
return;
|
||||||
const { top: listTop, bottom: listBottom } = unref(listRef)!.getBoundingClientRect();
|
|
||||||
const isVisible = top < listBottom && bottom > listTop;
|
|
||||||
if (isVisible) {
|
|
||||||
setUpdateCount(unref(updateCount) + 1);
|
|
||||||
setScrollReachEnd(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 新增内容且当前在底部:继续粘底
|
||||||
|
if (oldLen !== undefined && newLen > (oldLen ?? 0) && unref(scrollReachEnd)) {
|
||||||
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const onBubbleUpdate = useEventCallback<void>(() => {
|
const onBubbleUpdate = useEventCallback<void>(() => {
|
||||||
if (props.autoScroll ?? true) setUpdateCount(unref(updateCount) + 1);
|
if (props.autoScroll) setUpdateCount(unref(updateCount) + 1);
|
||||||
});
|
});
|
||||||
const context = computed(() => ({ onUpdate: onBubbleUpdate }));
|
const context = computed(() => ({ onUpdate: onBubbleUpdate }));
|
||||||
|
|
||||||
@ -106,6 +131,18 @@ export default defineComponent({
|
|||||||
const abortTypingByKey = (key: string | number) => {
|
const abortTypingByKey = (key: string | number) => {
|
||||||
bubbleRefs.value[key]?.abortTyping?.();
|
bubbleRefs.value[key]?.abortTyping?.();
|
||||||
};
|
};
|
||||||
|
// 通用:滚动到底部
|
||||||
|
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
|
||||||
|
nextTick(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const el = unref(listRef);
|
||||||
|
if (el) {
|
||||||
|
el.scrollTo({ top: el.scrollHeight, behavior });
|
||||||
|
setScrollReachEnd(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
// 对外暴露能力
|
// 对外暴露能力
|
||||||
expose({
|
expose({
|
||||||
nativeElement: listRef,
|
nativeElement: listRef,
|
||||||
@ -113,6 +150,7 @@ export default defineComponent({
|
|||||||
scrollTo: (info: any) => {
|
scrollTo: (info: any) => {
|
||||||
unref(listRef)?.scrollTo?.(info);
|
unref(listRef)?.scrollTo?.(info);
|
||||||
},
|
},
|
||||||
|
scrollToBottom,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
|
|||||||
Reference in New Issue
Block a user