Compare commits
3 Commits
bdb685b4ec
...
14d213c4a8
| Author | SHA1 | Date | |
|---|---|---|---|
| 14d213c4a8 | |||
| 34ad0ed8e3 | |||
| aa8c1737ad |
@@ -10,23 +10,32 @@
|
||||
<!-- AI avatar -->
|
||||
<ChatAvatar v-if="msg.messageRole === MessageRole.AI" :src="aiAvatar" />
|
||||
|
||||
<!-- 消息气泡 -->
|
||||
<div class="max-w-[70%]">
|
||||
<!-- 名字和时间 -->
|
||||
<ChatNameTime :showReverse="msg.messageRole === MessageRole.ME" />
|
||||
<!-- 自己 发的消息 -->
|
||||
<ChatRoleMe v-if="msg.messageRole === MessageRole.ME" :msg="msg" >
|
||||
<template #header>
|
||||
<!-- 名字和时间 -->
|
||||
<ChatNameTime :showReverse="true" />
|
||||
</template>
|
||||
</ChatRoleMe>
|
||||
|
||||
<!-- AI 发的消息 -->
|
||||
<ChatRoleAI v-if="msg.messageRole === MessageRole.AI" :msg="msg" />
|
||||
<!-- AI 发的消息 -->
|
||||
<ChatRoleAI v-if="msg.messageRole === MessageRole.AI" :msg="msg">
|
||||
<template #header>
|
||||
<!-- 名字和时间 -->
|
||||
<ChatNameTime :showReverse="false" />
|
||||
</template>
|
||||
|
||||
<!-- 自己 发的消息 -->
|
||||
<ChatRoleMe v-if="msg.messageRole === MessageRole.ME" :msg="msg" />
|
||||
<template #footer>
|
||||
<!-- 问题标签 -->
|
||||
<ChatAttach v-if="msg.question && msg.question.length > 0" :question="msg.question" @select="onTagSelect" />
|
||||
|
||||
<!-- AI 标识 -->
|
||||
<ChatAIMark v-if="msg.messageRole === MessageRole.AI && msg.finished" />
|
||||
<!-- AI 标识 -->
|
||||
<ChatAIMark v-if="msg.finished" />
|
||||
|
||||
<!-- AI 操作按钮 -->
|
||||
<ChatOperation v-if="msg.messageRole === MessageRole.AI && msg.finished" :msg="msg" />
|
||||
</div>
|
||||
<!-- AI 操作按钮 -->
|
||||
<ChatOperation v-if="msg.finished" :msg="msg" />
|
||||
</template>
|
||||
</ChatRoleAI>
|
||||
|
||||
<!-- User avatar -->
|
||||
<ChatAvatar v-if="msg.messageRole === MessageRole.ME" :src="userAvatar" />
|
||||
@@ -45,7 +54,7 @@
|
||||
shadow-[0_1px_0_rgba(0,0,0,0.03)]
|
||||
p-[18px] mt-[8px] flex flex-col justify-between">
|
||||
<textarea rows="2" placeholder="给我发布或者布置任务" class="flex-1 resize-none outline-none text-sm"
|
||||
v-model="inputMessage" @keyup.enter="sendMessageAction" />
|
||||
v-model="inputMessage" @keydown.enter="handleKeydownEnter" />
|
||||
|
||||
<div class="flex justify-between items-end">
|
||||
<button @click="addAttachmentAction()">
|
||||
@@ -76,15 +85,15 @@ import ChatRoleAI from './components/ChatRoleAI.vue';
|
||||
import ChatRoleMe from './components/ChatRoleMe.vue';
|
||||
import ChatAIMark from './components/ChatAIMark.vue';
|
||||
import ChatNameTime from './components/ChatNameTime.vue';
|
||||
import ChatAttach from './components/ChatAttach.vue';
|
||||
|
||||
import { Session } from '../../utils/storage';
|
||||
|
||||
import userAvatar from '@assets/images/login/user_icon.png';
|
||||
import aiAvatar from '@assets/images/login/blue_logo.png';
|
||||
|
||||
|
||||
///(控制滚动位置)
|
||||
const scrollTop = ref(99999);
|
||||
// 列表滚动容器引用
|
||||
const listRef = ref<HTMLElement | null>(null);
|
||||
|
||||
/// 会话列表
|
||||
const chatMsgList = ref<ChatMessage[]>([]);
|
||||
@@ -93,8 +102,8 @@ const inputMessage = ref("");
|
||||
/// 发送消息中标志
|
||||
const isSendingMessage = ref(false);
|
||||
|
||||
/// agentId 首页接口中获取
|
||||
const agentId = ref("1953462165250859010");
|
||||
/// agentId 首页接口中获取 1953462165250859010
|
||||
const agentId = ref("1");
|
||||
/// 会话ID 历史数据接口中获取
|
||||
const conversationId = ref("");
|
||||
// 会话进行中标志
|
||||
@@ -123,15 +132,39 @@ const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
// 当前会话的消息ID,用于保持发送和终止的messageId一致
|
||||
let currentSessionMessageId: string | null = null;
|
||||
|
||||
// 滚动到底部 - 优化版本,确保打字机效果始终可见
|
||||
const scrollToBottom = () => {
|
||||
// 滚动到底部 - 精确计算 last 元素位置并使用双 rAF 保证布局稳定
|
||||
const scrollToBottom = (smooth = false) => {
|
||||
nextTick(() => {
|
||||
// 使用更大的值确保滚动到真正的底部
|
||||
scrollTop.value = 99999;
|
||||
// 强制触发滚动更新,增加延迟确保DOM更新完成
|
||||
setTimeout(() => {
|
||||
scrollTop.value = scrollTop.value + Math.random();
|
||||
}, 10);
|
||||
const el = listRef.value;
|
||||
if (!el) return;
|
||||
|
||||
const doScroll = () => {
|
||||
const last = el.lastElementChild as HTMLElement | null;
|
||||
// 计算容器 style padding-bottom,保证滚动到真正可视底部
|
||||
const style = window.getComputedStyle(el);
|
||||
const paddingBottom = parseFloat(style.paddingBottom || '0') || 0;
|
||||
|
||||
if (last) {
|
||||
const lastOffset = last.offsetTop + last.offsetHeight;
|
||||
const target = lastOffset + paddingBottom - el.clientHeight;
|
||||
const top = Math.max(0, Math.ceil(target));
|
||||
if (smooth && typeof el.scrollTo === 'function') {
|
||||
el.scrollTo({ top, behavior: 'smooth' });
|
||||
} else {
|
||||
el.scrollTop = top;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 兜底:滚到底
|
||||
if (smooth && typeof el.scrollTo === 'function') {
|
||||
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
|
||||
} else {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
};
|
||||
// 使用两次 requestAnimationFrame 增强在复杂渲染/打字机更新场景的可靠性
|
||||
requestAnimationFrame(() => requestAnimationFrame(doScroll));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -149,7 +182,6 @@ const handleReplyText = (text: string) => {
|
||||
// 是发送指令消息
|
||||
const handleReplyInstruct = async (message: string, type: string) => {
|
||||
// await checkToken();
|
||||
|
||||
commonTypeMessage = type;
|
||||
// 重置消息状态,准备接收新的AI回复
|
||||
resetMessageState();
|
||||
@@ -157,6 +189,11 @@ const handleReplyInstruct = async (message: string, type: string) => {
|
||||
setTimeoutScrollToBottom();
|
||||
};
|
||||
|
||||
/// 选择标签事件
|
||||
const onTagSelect = (text: string) => {
|
||||
handleReplyText(text);
|
||||
};
|
||||
|
||||
/// 添加附件按钮事件
|
||||
const addAttachmentAction = () => {
|
||||
console.log("添加附件");
|
||||
@@ -169,14 +206,27 @@ const sendMessageAction = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("输入消息:", inputMessage.value);
|
||||
if (!inputMessage.value.trim()) return;
|
||||
const raw = inputMessage.value || '';
|
||||
// 去除尾部多余的换行,保留中间换行(例如 Shift+Enter)
|
||||
const sendText = raw.replace(/\n+$/, '');
|
||||
console.log("输入消息:", sendText);
|
||||
if (!sendText.trim()) return;
|
||||
// 重置消息状态,准备接收新的AI回复
|
||||
resetMessageState();
|
||||
sendMessage(inputMessage.value);
|
||||
sendMessage(sendText);
|
||||
setTimeoutScrollToBottom();
|
||||
};
|
||||
|
||||
// 处理在 textarea 中按 Enter:Shift+Enter 保留换行,单按 Enter 发送并阻止默认换行行为
|
||||
const handleKeydownEnter = (e: KeyboardEvent) => {
|
||||
if ((e as KeyboardEvent).shiftKey) {
|
||||
// 允许插入换行
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
sendMessageAction();
|
||||
};
|
||||
|
||||
// 停止发送消息事件
|
||||
const sendStopAction = () => {
|
||||
console.log("停止发送消息");
|
||||
@@ -369,6 +419,7 @@ const handleWebSocketMessage = (data: any) => {
|
||||
|
||||
// 处理question
|
||||
if (data.question && data.question.length > 0) {
|
||||
console.log("收到问题标签:", data.question);
|
||||
chatMsgList.value[aiMsgIndex].question = data.question;
|
||||
}
|
||||
|
||||
@@ -388,6 +439,7 @@ const handleWebSocketMessage = (data: any) => {
|
||||
isSessionActive.value = false;
|
||||
// 清理当前会话的 messageId,避免保留陈旧 id
|
||||
resetMessageState();
|
||||
setTimeoutScrollToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
34
src/renderer/views/home/components/ChatAttach.vue
Normal file
34
src/renderer/views/home/components/ChatAttach.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="tag-flex flex-wrap pt-3">
|
||||
<div class="inline-flex items-center justify-center box-border border border-[#E5E8EE] rounded-lg py-0.5 px-2.5 mr-2 mb-2"
|
||||
v-for="(item, index) in questionList" :key="index" @click="handleClick(item)">
|
||||
<span class="tag-text-[#2d91ff] text-[10px]">{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
question: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const questionList = ref<string[]>([]);
|
||||
|
||||
// 定义 emit 事件,向父组件发送选中的 tag
|
||||
const emit = defineEmits<{ (e: 'select', tag: string): void }>();
|
||||
|
||||
const handleClick = (item: string) => {
|
||||
emit('select', item);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
questionList.value = props.question.split(/[&|;]/).filter((tag) => tag.trim() !== "");
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="text-sm text-gray-700 flex flex-row">
|
||||
<div v-html="compiledMarkdown"></div>
|
||||
<ChatLoading v-if="msg.isLoading" />
|
||||
<div class="max-w-[75%] flex flex-col">
|
||||
<slot name="header"></slot>
|
||||
<div class="text-sm text-gray-700 flex flex-row">
|
||||
<div v-html="compiledMarkdown"></div>
|
||||
<ChatLoading v-if="msg.isLoading" />
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,6 +16,7 @@ import MarkdownIt from 'markdown-it'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/github.css'
|
||||
import ChatLoading from './ChatLoading.vue';
|
||||
import { sl } from 'element-plus/es/locale/index.mjs';
|
||||
|
||||
interface Props {
|
||||
msg: ChatMessage
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<div class="text-sm text-gray-700 bg-[#f7f9fc] rounded-md px-2 py-2">
|
||||
{{ msg.messageContent }}
|
||||
<div class="max-w-[75%]">
|
||||
<slot name="header"></slot>
|
||||
<div class="text-sm text-gray-700 bg-[#f7f9fc] rounded-md px-2 py-2">
|
||||
{{ msg.messageContent }}
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ChatMessage {
|
||||
// 工具调用信息
|
||||
toolCall?: any;
|
||||
// 问题信息
|
||||
question?: any;
|
||||
question?: string;
|
||||
|
||||
constructor(
|
||||
messageId: string,
|
||||
|
||||
Reference in New Issue
Block a user