test(api): 测试流式接口返回

This commit is contained in:
何嘉悦 2025-06-02 11:31:50 +08:00
parent 165a4748bb
commit 1d5e519fb4
7 changed files with 473 additions and 134 deletions

View File

@ -28,6 +28,10 @@ const popoverStyle = ref({
width: '200px', width: '200px',
padding: '4px', padding: '4px',
height: 'fit-content', height: 'fit-content',
background: 'var(--el-bg-color, #fff)',
border: '1px solid var(--el-border-color-light)',
borderRadius: '8px',
boxShadow: '0 2px 12px 0 rgba(0, 0, 0, 0.1)',
}); });
const popoverRef = ref(); const popoverRef = ref();
@ -45,25 +49,25 @@ function handleClick(item: GetSessionListVO) {
</script> </script>
<template> <template>
<div class="model-switch"> <div class="model-select">
<Popover <Popover
ref="popoverRef" ref="popoverRef"
position="top-start" placement="top-start"
:offset="[-14, 10]" :offset="[4, 0]"
:trigger-style="{ cursor: 'pointer' }"
popover-class="popover-content" popover-class="popover-content"
:popover-style="popoverStyle" :popover-style="popoverStyle"
trigger="clickTarget"
@show="showPopover" @show="showPopover"
> >
<!-- 触发元素插槽 --> <!-- 触发元素插槽 -->
<template #trigger> <template #trigger>
<div <div
class="model-switch-box select-none flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-12px border-[rgba()]" class="model-select-box select-none flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-12px border-[rgba()]"
> >
<div class="model-switch-box-icon"> <div class="model-select-box-icon">
<SvgIcon name="models" size="12" /> <SvgIcon name="models" size="12" />
</div> </div>
<div class="model-switch-box-text font-size-12px"> <div class="model-select-box-text font-size-12px">
{{ currentModelName }} {{ currentModelName }}
</div> </div>
</div> </div>
@ -73,30 +77,30 @@ function handleClick(item: GetSessionListVO) {
<div <div
v-for="item in popoverList" v-for="item in popoverList"
:key="item.id" :key="item.id"
class="popover-content-box-items flex rounded-8px select-none transition-all transition-duration-300 flex items-center gap-8px p-4px hover:cursor-pointer hover:bg-[rgba(0,0,0,.04)]" class="popover-content-box-items w-full rounded-8px select-none transition-all transition-duration-300 flex items-center hover:cursor-pointer hover:bg-[rgba(0,0,0,.04)]"
> >
<el-tooltip <Popover
popper-class="rounded-tooltip" trigger-class="popover-trigger-item-text"
effect="dark" popover-class="rounded-tooltip"
placement="right" placement="right"
trigger="hover" trigger="hover"
:offset="10" :offset="[12, 0]"
:show-arrow="false"
transition="zoom-fade"
> >
<template #content> <template #trigger>
<div class="popover-content-box-item-text text-wrap max-w-200px rounded-lg">
{{ item.remark }}
</div>
</template>
<div <div
class="popover-content-box-item font-size-12px text-overflow w-full line-height-16px" class="popover-content-box-item p-4px font-size-12px text-overflow line-height-16px"
:class="{ 'bg-[rgba(0,0,0,.04)] is-select': item.modelName === currentModelName }" :class="{ 'bg-[rgba(0,0,0,.04)] is-select': item.modelName === currentModelName }"
@click="handleClick(item)" @click="handleClick(item)"
> >
{{ item.modelName }} {{ item.modelName }}
</div> </div>
</el-tooltip> </template>
<div
class="popover-content-box-item-text text-wrap max-w-200px rounded-lg p-8px font-size-12px line-height-tight"
>
{{ item.remark }}
</div>
</Popover>
</div> </div>
</div> </div>
</Popover> </Popover>
@ -104,7 +108,7 @@ function handleClick(item: GetSessionListVO) {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.model-switch-box { .model-select-box {
color: var(--el-color-primary, #409eff); color: var(--el-color-primary, #409eff);
border: 1px solid var(--el-color-primary, #409eff); border: 1px solid var(--el-color-primary, #409eff);
border-radius: 10px; border-radius: 10px;
@ -116,20 +120,35 @@ function handleClick(item: GetSessionListVO) {
} }
.popover-content-box { .popover-content-box {
// background-color: red;
height: 200px; height: 200px;
gap: 4px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
.popover-content-box-items {
:deep() {
.popover-trigger-item-text {
width: 100%;
}
}
}
.popover-content-box-item-text {
background-color: black;
color: white;
}
// //
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 4px; width: 4px;
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: #f5f5f5; background: #f5f5f5;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: #ccc; background: #ccc;
border-radius: 4px; border-radius: 4px;

View File

@ -1,31 +1,9 @@
<!-- Popover 弹框 -->
<script setup lang="ts"> <script setup lang="ts">
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { flip, offset, shift, useFloating } from '@floating-ui/vue'; import { arrow, autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/vue';
import { onClickOutside } from '@vueuse/core';
const props = withDefaults(defineProps<PopoverProps>(), { export type PopoverPlacement =
position: 'bottom',
offset: () => [8, 8],
boundary: 'viewport',
closeOnContentClick: false,
closeOnTriggerClick: false,
triggerStyle: () => ({}),
popoverStyle: () => ({}),
popoverClass: '',
});
const emits = defineEmits<{
(e: 'show'): void;
(e: 'hide'): void;
(e: 'positionChange', pos: PopoverPosition): void;
}>();
const reference = ref();
const floating = ref(null);
const { floatingStyles } = useFloating(reference, floating, {
placement: props.position,
middleware: [offset(props.offset[0]), flip(), shift()],
});
type PopoverPosition =
| 'top' | 'top'
| 'top-start' | 'top-start'
| 'top-end' | 'top-end'
@ -39,54 +17,189 @@ type PopoverPosition =
| 'right-start' | 'right-start'
| 'right-end'; | 'right-end';
type Offset = [number, number]; export type Offset = [number, number];
interface PopoverProps {
position?: PopoverPosition; export interface PopoverProps {
placement?: PopoverPlacement;
offset?: Offset; offset?: Offset;
triggerStyle?: CSSProperties;
popoverStyle?: CSSProperties; popoverStyle?: CSSProperties;
popoverClass?: string; popoverClass?: string;
boundary?: 'viewport' | HTMLElement; trigger?: 'hover' | 'click' | 'clickTarget';
closeOnContentClick?: boolean; triggerStyle?: CSSProperties;
closeOnTriggerClick?: boolean; triggerClass?: string;
hoverDelay?: number; // ms
} }
const showPoperContent = ref(false); const props = withDefaults(defineProps<PopoverProps>(), {
placement: 'bottom',
offset: () => [0, 0],
trigger: 'hover',
hoverDelay: 0, // 300ms
});
function handleTriggerClick() { const emits = defineEmits<{
showPoperContent.value = !showPoperContent.value; (e: 'show'): void;
if (showPoperContent.value) { (e: 'hide'): void;
}>();
const triggerRef = ref<HTMLElement | null>(null);
const popoverRef = ref<HTMLElement | null>(null);
const floatingArrow = ref<HTMLElement | null>(null);
const showPopover = ref(false);
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
//
const isHovering = ref(false);
const { floatingStyles } = useFloating(triggerRef, popoverRef, {
placement: props.placement,
transform: false,
whileElementsMounted: autoUpdate,
middleware: [
shift(),
flip(),
arrow({ element: floatingArrow }),
offset({
mainAxis: props.offset[0],
crossAxis: props.offset[1],
}),
],
});
function show() {
if (!showPopover.value) {
showPopover.value = true;
emits('show'); emits('show');
} }
else { //
if (hideTimeout)
clearTimeout(hideTimeout);
hideTimeout = null;
}
function hide() {
if (showPopover.value) {
showPopover.value = false;
emits('hide'); emits('hide');
} }
hideTimeout = null;
}
defineExpose({ show, hide });
watch(showPopover, (newValue) => {
if (newValue && props.trigger !== 'hover') {
onClickOutside(popoverRef, () => hide(), {
ignore: [triggerRef] as any[],
});
}
});
// isHovering
function handleTriggerMouseEnter() {
if (props.trigger === 'hover') {
isHovering.value = true; //
show();
}
}
function handleTriggerMouseLeave() {
if (props.trigger === 'hover') {
isHovering.value = false; //
//
scheduleHideIfNeeded();
}
}
// isHovering
function handlePopoverMouseEnter() {
if (props.trigger === 'hover') {
isHovering.value = true; //
if (hideTimeout)
clearTimeout(hideTimeout); //
}
}
function handlePopoverMouseLeave() {
if (props.trigger === 'hover') {
isHovering.value = false; //
//
scheduleHideIfNeeded();
}
}
//
function scheduleHideIfNeeded() {
// isHoveringtrue
if (isHovering.value)
return;
//
hideTimeout = setTimeout(() => {
if (!isHovering.value) {
//
hide();
}
}, props.hoverDelay);
}
function handleClick() {
if (props.trigger === 'click') {
showPopover.value ? hide() : show();
}
else if (props.trigger === 'clickTarget' && !showPopover.value) {
show();
}
} }
</script> </script>
<template> <template>
<div> <div
<div ref="reference" :style="props.triggerStyle" @click.stop="handleTriggerClick"> ref="triggerRef"
class="popover-trigger"
:class="[props.triggerClass]"
:style="[props.triggerStyle]"
@mouseenter="handleTriggerMouseEnter"
@mouseleave="handleTriggerMouseLeave"
@click="handleClick"
>
<slot name="trigger" /> <slot name="trigger" />
</div> </div>
<Teleport to="body">
<Transition name="popover-fade"> <Transition name="popover-fade">
<div <div
v-if="showPoperContent" v-if="showPopover"
ref="floating" ref="popoverRef"
class="popover-content" :style="[floatingStyles, props.popoverStyle]"
:style="{ class="popover-content-box"
...floatingStyles,
...props.popoverStyle,
}"
:class="[props.popoverClass]" :class="[props.popoverClass]"
@mouseenter="handlePopoverMouseEnter"
@mouseleave="handlePopoverMouseLeave"
> >
<slot name="header" />
<slot /> <slot />
<slot name="footer" />
</div> </div>
</Transition> </Transition>
</div> </Teleport>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss">
.popover-fade-enter-active,
.popover-fade-leave-active {
transition:
opacity 0.2s ease,
transform 0.2s ease;
will-change: transform, opacity;
}
.popover-fade-enter-from,
.popover-fade-leave-to {
opacity: 0;
transform: scale(0.95);
}
.popover-fade-enter-to,
.popover-fade-leave-from {
opacity: 1;
transform: scale(1);
}
</style>

View File

@ -28,10 +28,16 @@ onMounted(async () => {
const currentSessionRes = await get_session(`${sessionId.value}`); const currentSessionRes = await get_session(`${sessionId.value}`);
// ID () // ID ()
sessionStore.setCurrentSession(currentSessionRes.data); sessionStore.setCurrentSession(currentSessionRes.data);
active.value = `${sessionId.value}`;
} }
}); });
watch(
() => sessionStore.currentSession,
(newValue) => {
active.value = newValue ? `${newValue.id}` : undefined;
},
);
// //
function handleCreatChat() { function handleCreatChat() {
// , // ,

View File

@ -94,8 +94,8 @@ function handleClick(item: any) {
<div class="avatar-container"> <div class="avatar-container">
<Popover <Popover
ref="popoverRef" ref="popoverRef"
position="bottom-end" placement="bottom-end"
:offset="[0, 8]" trigger="clickTarget"
:trigger-style="{ cursor: 'pointer' }" :trigger-style="{ cursor: 'pointer' }"
popover-class="popover-content" popover-class="popover-content"
:popover-style="popoverStyle" :popover-style="popoverStyle"
@ -105,7 +105,7 @@ function handleClick(item: any) {
<el-avatar :src="src" :size="28" fit="fit" shape="circle" /> <el-avatar :src="src" :size="28" fit="fit" shape="circle" />
</template> </template>
<div class="popover-content-box"> <div class="popover-content-box shadow-lg">
<div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full"> <div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full">
<div <div
v-if="!item.divider" v-if="!item.divider"
@ -130,4 +130,12 @@ function handleClick(item: any) {
width: 520px; width: 520px;
height: 520px; height: 520px;
} }
.popover-content-box {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
padding: 8px;
}
</style> </style>

View File

@ -1,13 +1,19 @@
<!-- 默认消息列表页 --> <!-- 默认消息列表页 -->
<script setup lang="ts"> <script setup lang="ts">
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
import FilesSelect from '@/components/FilesSelect/index.vue';
import ModelSelect from '@/components/ModelSelect/index.vue'; import ModelSelect from '@/components/ModelSelect/index.vue';
import WelecomeText from '@/components/WelecomeText/index.vue'; import WelecomeText from '@/components/WelecomeText/index.vue';
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
import { useFilesStore } from '@/stores/modules/files';
import { useSessionStore } from '@/stores/modules/session'; import { useSessionStore } from '@/stores/modules/session';
const userStore = useUserStore(); const userStore = useUserStore();
const sessionStore = useSessionStore(); const sessionStore = useSessionStore();
const filesStore = useFilesStore();
const senderValue = ref(''); const senderValue = ref('');
const senderRef = ref();
async function handleSend() { async function handleSend() {
localStorage.setItem('chatContent', senderValue.value); localStorage.setItem('chatContent', senderValue.value);
@ -18,12 +24,33 @@ async function handleSend() {
remark: senderValue.value.slice(0, 10), remark: senderValue.value.slice(0, 10),
}); });
} }
function handleDeleteCard(_item: FilesCardProps, index: number) {
filesStore.deleteFileByIndex(index);
}
watch(
() => filesStore.filesList.length,
(val) => {
if (val > 0) {
nextTick(() => {
senderRef.value.openHeader();
});
}
else {
nextTick(() => {
senderRef.value.closeHeader();
});
}
},
);
</script> </script>
<template> <template>
<div class="chat-defaul-wrap"> <div class="chat-defaul-wrap">
<WelecomeText /> <WelecomeText />
<Sender <Sender
ref="senderRef"
v-model="senderValue" v-model="senderValue"
class="chat-defaul-sender" class="chat-defaul-sender"
:auto-size="{ :auto-size="{
@ -35,16 +62,43 @@ async function handleSend() {
allow-speech allow-speech
@submit="handleSend" @submit="handleSend"
> >
<template #prefix> <template #header>
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden"> <div class="sender-header p-12px pt-6px pb-0px">
<ModelSelect /> <Attachments
:items="filesStore.filesList"
:hide-upload="true"
@delete-card="handleDeleteCard"
>
<template #prev-button="{ show, onScrollLeft }">
<div <div
class="flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px border-1px border-[rgba(0,0,0,0.08)] border-solid hover:bg-[rgba(0,0,0,.04)]" v-if="show"
class="prev-next-btn left-8px flex-center w-22px h-22px rounded-8px border-1px border-solid border-[rgba(0,0,0,0.08)] c-[rgba(0,0,0,.4)] hover:bg-#f3f4f6 bg-#fff font-size-10px"
@click="onScrollLeft"
> >
<el-icon> <el-icon>
<Paperclip /> <ArrowLeftBold />
</el-icon> </el-icon>
</div> </div>
</template>
<template #next-button="{ show, onScrollRight }">
<div
v-if="show"
class="prev-next-btn right-8px flex-center w-22px h-22px rounded-8px border-1px border-solid border-[rgba(0,0,0,0.08)] c-[rgba(0,0,0,.4)] hover:bg-#f3f4f6 bg-#fff font-size-10px"
@click="onScrollRight"
>
<el-icon>
<ArrowRightBold />
</el-icon>
</div>
</template>
</Attachments>
</div>
</template>
<template #prefix>
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
<FilesSelect />
<ModelSelect />
</div> </div>
</template> </template>
</Sender> </Sender>

View File

@ -2,12 +2,15 @@
<script setup lang="ts"> <script setup lang="ts">
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble'; import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList'; import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking'; import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
import { Loading, Position } from '@element-plus/icons-vue';
import { useXStream } from 'vue-element-plus-x'; import { useXStream } from 'vue-element-plus-x';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { send } from '@/api/chat/index'; import { send } from '@/api/chat/index';
import FilesSelect from '@/components/FilesSelect/index.vue';
import ModelSelect from '@/components/ModelSelect/index.vue';
import { useChatStore } from '@/stores/modules/chat'; import { useChatStore } from '@/stores/modules/chat';
import { useFilesStore } from '@/stores/modules/files';
import { useModelStore } from '@/stores/modules/model'; import { useModelStore } from '@/stores/modules/model';
type MessageItem = BubbleProps & { type MessageItem = BubbleProps & {
@ -18,7 +21,7 @@ type MessageItem = BubbleProps & {
expanded?: boolean; expanded?: boolean;
}; };
const { startStream: _, cancel, data, error, isLoading } = useXStream(); const { cancel, error, isLoading } = useXStream();
// const BASE_URL = 'https://api.siliconflow.cn/v1/chat/completions'; // const BASE_URL = 'https://api.siliconflow.cn/v1/chat/completions';
// //
@ -28,18 +31,22 @@ const { startStream: _, cancel, data, error, isLoading } = useXStream();
const route = useRoute(); const route = useRoute();
const chatStore = useChatStore(); const chatStore = useChatStore();
const modelStore = useModelStore(); const modelStore = useModelStore();
const inputValue = ref('帮我写一篇小米手机介绍'); const filesStore = useFilesStore();
const inputValue = ref('');
const senderRef = ref<any>(null); const senderRef = ref<any>(null);
const bubbleItems = ref<MessageItem[]>([]); const bubbleItems = ref<MessageItem[]>([]);
const bubbleListRef = ref<BubbleListInstance | null>(null); const bubbleListRef = ref<BubbleListInstance | null>(null);
const processedIndex = ref(0); // const processedIndex = ref(0);
watch( watch(
() => route.params?.id, () => route.params?.id,
async (_id_) => { async (_id_) => {
if (_id_) { if (_id_) {
await chatStore.requestChatList(`${_id_}`); //
// id inputValue.value = '';
// id
if (chatStore.chatMap[`${_id_}`] && chatStore.chatMap[`${_id_}`].length) { if (chatStore.chatMap[`${_id_}`] && chatStore.chatMap[`${_id_}`].length) {
bubbleItems.value = chatStore.chatMap[`${_id_}`].map((item: any) => ({ bubbleItems.value = chatStore.chatMap[`${_id_}`].map((item: any) => ({
key: item.id, key: item.id,
@ -57,17 +64,39 @@ watch(
setTimeout(() => { setTimeout(() => {
bubbleListRef.value!.scrollToBottom(); bubbleListRef.value!.scrollToBottom();
}, 350); }, 350);
return;
} }
//
await chatStore.requestChatList(`${_id_}`);
//
bubbleItems.value = chatStore.chatMap[`${_id_}`].map((item: any) => ({
key: item.id,
avatar:
item.role === 'user'
? 'https://avatars.githubusercontent.com/u/76239030?v=4'
: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
content: item.content,
avatarSize: '32px',
role: item.role,
typing: false,
}));
//
setTimeout(() => {
bubbleListRef.value!.scrollToBottom();
}, 350);
//
const v = localStorage.getItem('chatContent'); const v = localStorage.getItem('chatContent');
if (v) { if (v) {
inputValue.value = v;
localStorage.removeItem('chatContent'); localStorage.removeItem('chatContent');
// //
console.log('发送消息 v', v); console.log('发送消息 v', v);
setTimeout(() => { // setTimeout(() => {
startSSE(); // startSSE();
}, 350); // }, 350);
} }
} }
}, },
@ -113,28 +142,88 @@ function handleDataChunk(chunk: string) {
} }
} }
watch( // watch(
data, // data,
() => { // () => {
for (let i = processedIndex.value; i < data.value.length; i++) { // for (let i = processedIndex.value; i < data.value.length; i++) {
const chunk = data.value[i].data; // const chunk = data.value[i].data;
handleDataChunk(chunk); // handleDataChunk(chunk);
processedIndex.value++; // processedIndex.value++;
} // }
}, // },
{ deep: true }, // { deep: true },
); // );
// //
function handleError(err: any) { function handleError(err: any) {
console.error('Fetch error:', err); console.error('Fetch error:', err);
} }
async function startSSE() { function checkJsonSerializable(data: any) {
const visited = new WeakSet();
let error: any = null;
function check(value: any, currentPath = 'root') {
if (error)
return;
const type = typeof value;
// JSON
if (type === 'string' || type === 'number' || type === 'boolean' || value === null) {
return;
}
// undefinedJSON
if (value === undefined) {
error = { path: currentPath, value, type: 'undefined' };
return;
}
// SymbolJSON
if (type === 'function' || type === 'symbol') {
error = { path: currentPath, value, type };
return;
}
//
if (Array.isArray(value)) {
value.forEach((item, index) => {
check(item, `${currentPath}[${index}]`);
});
return;
}
//
if (type === 'object' && value !== null) {
//
if (visited.has(value)) {
error = { path: currentPath, value, type: 'circular_reference' };
return;
}
visited.add(value);
//
for (const [key, val] of Object.entries(value)) {
check(val, `${currentPath}.${key}`);
}
visited.delete(value);
return;
}
// DateRegExp
error = { path: currentPath, value, type };
}
check(data);
return { valid: !error, error };
}
async function startSSE(chatContent: string) {
try { try {
// //
console.log('inputValue.value', inputValue.value); console.log('chatContent', chatContent);
addMessage(inputValue.value, true); addMessage(chatContent, true);
addMessage('', false); addMessage('', false);
// BubbleList // BubbleList
@ -154,6 +243,22 @@ async function startSSE() {
console.log('res', res); console.log('res', res);
for await (const chunk of res) {
console.log('chunk', chunk);
const resError = checkJsonSerializable(chunk.result);
console.log('resError', resError);
// json
if (chunk.result && typeof chunk.result !== 'object') {
console.log('json 序列化失败');
handleDataChunk(chunk.result as string);
}
else if (chunk.result) {
const strChunk = JSON.stringify(chunk.result);
handleDataChunk(strChunk);
}
}
// const response = await fetch(BASE_URL, { // const response = await fetch(BASE_URL, {
// method: 'POST', // method: 'POST',
// headers: { // headers: {
@ -190,7 +295,7 @@ function addMessage(message: string, isUser: boolean) {
avatar: isUser avatar: isUser
? 'https://avatars.githubusercontent.com/u/76239030?v=4' ? 'https://avatars.githubusercontent.com/u/76239030?v=4'
: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png', : 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
avatarSize: '48px', avatarSize: '32px',
role: isUser ? 'user' : 'system', role: isUser ? 'user' : 'system',
placement: isUser ? 'end' : 'start', placement: isUser ? 'end' : 'start',
isMarkdown: !isUser, isMarkdown: !isUser,
@ -211,6 +316,26 @@ function addMessage(message: string, isUser: boolean) {
function handleChange(payload: { value: boolean; status: ThinkingStatus }) { function handleChange(payload: { value: boolean; status: ThinkingStatus }) {
console.log('value', payload.value, 'status', payload.status); console.log('value', payload.value, 'status', payload.status);
} }
function handleDeleteCard(_item: FilesCardProps, index: number) {
filesStore.deleteFileByIndex(index);
}
watch(
() => filesStore.filesList.length,
(val) => {
if (val > 0) {
nextTick(() => {
senderRef.value.openHeader();
});
}
else {
nextTick(() => {
senderRef.value.closeHeader();
});
}
},
);
</script> </script>
<template> <template>
@ -243,32 +368,46 @@ function handleChange(payload: { value: boolean; status: ThinkingStatus }) {
variant="updown" variant="updown"
clearable clearable
allow-speech allow-speech
:loading="isLoading"
@submit="startSSE" @submit="startSSE"
> >
<template #prefix> <template #header>
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden"> <div class="sender-header p-12px pt-6px pb-0px">
<Attachments
:items="filesStore.filesList"
:hide-upload="true"
@delete-card="handleDeleteCard"
>
<template #prev-button="{ show, onScrollLeft }">
<div <div
class="flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px border-1px border-[rgba(0,0,0,0.08)] border-solid hover:bg-[rgba(0,0,0,.04)]" v-if="show"
class="prev-next-btn left-8px flex-center w-22px h-22px rounded-8px border-1px border-solid border-[rgba(0,0,0,0.08)] c-[rgba(0,0,0,.4)] hover:bg-#f3f4f6 bg-#fff font-size-10px"
@click="onScrollLeft"
> >
<el-icon> <el-icon>
<Paperclip /> <ArrowLeftBold />
</el-icon> </el-icon>
</div> </div>
</div>
</template> </template>
<template #action-list> <template #next-button="{ show, onScrollRight }">
<div class="footer-container"> <div
<el-button v-if="!isLoading" type="danger" circle @click="senderRef.submit()"> v-if="show"
class="prev-next-btn right-8px flex-center w-22px h-22px rounded-8px border-1px border-solid border-[rgba(0,0,0,0.08)] c-[rgba(0,0,0,.4)] hover:bg-#f3f4f6 bg-#fff font-size-10px"
@click="onScrollRight"
>
<el-icon> <el-icon>
<Position /> <ArrowRightBold />
</el-icon> </el-icon>
</el-button> </div>
<el-button v-if="isLoading" type="primary" @click="cancel"> </template>
<el-icon class="is-loading"> </Attachments>
<Loading /> </div>
</el-icon> </template>
</el-button> <template #prefix>
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
<FilesSelect />
<ModelSelect />
</div> </div>
</template> </template>
</Sender> </Sender>

View File

@ -36,7 +36,7 @@ declare module 'vue' {
VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default'] VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default']
WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default'] WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default']
} }
export interface ComponentCustomProperties { export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective'] vLoading: typeof import('element-plus/es')['ElLoadingDirective']
} }
} }