Commit 796092f1 authored by 水玉婷's avatar 水玉婷
Browse files

feat:文件上传及踩添加反馈弹窗

parent 72779ad7
...@@ -175,21 +175,21 @@ const getApiBaseUrl = () => { ...@@ -175,21 +175,21 @@ const getApiBaseUrl = () => {
const apiBaseUrl = getApiBaseUrl(); const apiBaseUrl = getApiBaseUrl();
const userInfo = localStorage.getItem('wechat_user'); const userInfo = localStorage.getItem('wechat_user');
const { extMap = {} } = JSON.parse(userInfo || '{}'); const { extMap = {},appId = '' } = JSON.parse(userInfo || '{}');
const userToken = extMap.sessionId; const userToken = extMap.sessionId;
const appCode = import.meta.env.VITE_APP_CODE || 'ped.qywx'; const appCode = import.meta.env.VITE_APP_CODE || 'ped.qywx';
// 基础配置对象 // 基础配置对象
const baseConfig = { const baseConfig = {
apiBaseUrl, apiBaseUrl,
token: userToken, userToken,
appCode appCode
}; };
const time = new Date().getTime();
const chatParams = { const chatParams = {
appId: '83b2664019a945d0a438abe6339758d8', appId: appId, // 企业微信应用ID
stage: 'wechat-demo', stage: 'wechat-demo'+time,
}; };
const totalCount = ref(0); const totalCount = ref(0);
const appName = ref(''); const appName = ref('');
interface Session { interface Session {
......
...@@ -36,12 +36,12 @@ ...@@ -36,12 +36,12 @@
// 基础配置对象 // 基础配置对象
const baseConfig = { const baseConfig = {
apiBaseUrl, apiBaseUrl,
token: userToken, userToken,
appCode appCode
}; };
const time = new Date().getTime(); const time = new Date().getTime();
const chatParams = { const chatParams = {
appId: appId || '83b2664019a945d0a438abe6339758d8', // 企业微信应用ID appId: appId, // 企业微信应用ID
stage: 'wechat-demo'+time, stage: 'wechat-demo'+time,
}; };
const dialogSessionId = ref(''); const dialogSessionId = ref('');
......
...@@ -48,11 +48,11 @@ export default { ...@@ -48,11 +48,11 @@ export default {
onMounted(() => { onMounted(() => {
// 检查是否已登录 // 检查是否已登录
const status = wechat.checkLoginStatus() // const status = wechat.checkLoginStatus()
if (status.isLoggedIn) { // if (status.isLoggedIn) {
router.replace('/') // router.replace('/')
return // return
} // }
// 执行静默登录 // 执行静默登录
handleLogin() handleLogin()
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<img :src="props?.logoUrl || defaultAvatar" alt="avatar" class="avatar-image" /> <img :src="props?.logoUrl || defaultAvatar" alt="avatar" class="avatar-image" />
</div> </div>
<div class="header-info"> <div class="header-info">
<h2>{{ props?.dialogSessionId ? appData?.app_namee || '继续对话' : '新建对话' }}</h2> <h2>{{ props?.dialogSessionId ? appData?.app_name || '继续对话' : '新建对话' }}</h2>
</div> </div>
</div> </div>
...@@ -37,13 +37,30 @@ ...@@ -37,13 +37,30 @@
<div class="message-content-wrapper"> <div class="message-content-wrapper">
<div class="message-content"> <div class="message-content">
<template v-for="(item, i) in msg.contentBlocks" :key="i"> <template v-for="(item, i) in msg.contentBlocks" :key="i">
<!-- 附件内容块 -->
<div v-if="item.attachmentData" class="attachment-block">
<div class="attachment-display" @click="previewAttachment(item.attachmentData as any)">
<img v-if="item.attachmentData.attachmentType?.startsWith?.('image/')" :src="item.attachmentData.attachmentPath || item.content" :alt="item.attachmentData.attachmentName" class="message-attachment-image" />
<div v-else class="file-display">
<span class="file-icon">{{ getFileTypeIcon(item.attachmentData.attachmentType || '') }}</span>
<div class="file-info">
<span class="file-name">{{ item.attachmentData.attachmentName }}</span>
<span class="file-size">{{ formatFileSize(item.attachmentData.attachmentSize || 0) }}</span>
</div>
</div>
</div>
<!-- 普通内容块 -->
<div class="message-inner-box" @click="msg.messageType === 'sent' ? handleMessageClick(msg, item, textarea) : null">
<div v-html="item.content"></div>
</div>
</div>
<!-- 图表内容块 --> <!-- 图表内容块 -->
<div v-if="item.chartData" class="chart-block"> <div v-else-if="item.chartData" class="chart-block">
<ChartComponent :chart-data="item.chartData" :chart-type="item.chartType || 'column'" <ChartComponent :chart-data="item.chartData" :chart-type="item.chartType || 'column'"
:title="item.chartData.title || '图表数据'" /> :title="item.chartData.title || '图表数据'" />
</div> </div>
<!-- 普通内容块 --> <!-- 普通内容块 -->
<div v-else class="message-inner-box" @click="msg.messageType === 'sent' ? handleMessageClick(msg, item) : null"> <div v-else class="message-inner-box" @click="msg.messageType === 'sent' ? handleMessageClick(msg, item, textarea) : null">
<div v-html="item.content"></div> <div v-html="item.content"></div>
</div> </div>
<!-- 思考过程框 --> <!-- 思考过程框 -->
...@@ -97,10 +114,22 @@ ...@@ -97,10 +114,22 @@
<button class="operation-btn copy-btn" @click="handleCopy(msg)" title="复制"> <button class="operation-btn copy-btn" @click="handleCopy(msg)" title="复制">
<copy-outlined /> <copy-outlined />
</button> </button>
<button class="operation-btn like-btn" @click="handleLike(msg)" title="赞"> <button
class="operation-btn like-btn"
@click="likeMessage(msg)"
:class="{ disabled: msg.upCount > 0 }"
:disabled="msg.upCount > 0"
:title="msg.upCount > 0 ? '已点赞' : '赞'"
>
<like-outlined /> <like-outlined />
</button> </button>
<button class="operation-btn dislike-btn" @click="handleDislike(msg)" title="踩"> <button
class="operation-btn dislike-btn"
@click="dislikeMessage(msg)"
:class="{ disabled: msg.downCount > 0 }"
:disabled="msg.downCount > 0"
:title="msg.downCount > 0 ? '已踩' : '踩'"
>
<dislike-outlined /> <dislike-outlined />
</button> </button>
</div> </div>
...@@ -110,17 +139,63 @@ ...@@ -110,17 +139,63 @@
</div> </div>
</div> </div>
<!-- 附件预览区域(只显示一个附件) -->
<div v-if="hasAttachment" class="attachments-preview-container">
<div class="attachment-item">
<div class="attachment-preview">
<img v-if="currentAttachment.attachmentType.startsWith('image/')" :src="currentAttachment.previewUrl" :alt="currentAttachment.attachmentName" class="preview-image" />
<div v-else class="file-icon">
<span class="file-type">{{ getFileTypeIcon(currentAttachment.attachmentType) }}</span>
</div>
<div class="attachment-info">
<span class="attachment-name">{{ currentAttachment.attachmentName }}</span>
<span class="attachment-size">{{ formatFileSize(currentAttachment.attachmentSize) }}</span>
</div>
</div>
<div class="attachment-actions">
<button @click="previewAttachment(currentAttachment)" class="action-btn preview-btn" title="预览">
<eye-outlined />
</button>
<button @click="removeAttachment(currentAttachment.id)" class="action-btn remove-btn" title="删除">
<delete-outlined />
</button>
</div>
</div>
</div>
<!-- 反馈弹窗 -->
<FeedbackModal
:visible="feedbackModalData.visible"
@update:visible="(value) => feedbackModalData.visible = value"
@submit="submitFeedback"
/>
<!-- 输入区域 - 始终显示 --> <!-- 输入区域 - 始终显示 -->
<div class="chat-input-container"> <div class="chat-input-container">
<div class="chat-input"> <div class="chat-input">
<textarea ref="textarea" v-model="messageText" placeholder="输入消息..." @keypress="handleKeyPress"
@input="adjustTextareaHeight" @paste="(event) => handlePaste(event, uploadConfig)"></textarea>
<!-- 上传附件按钮(放在右边) -->
<div class="upload-wrapper">
<input
ref="fileInput"
type="file"
:accept="getAcceptTypes()"
@change="(event) => handleFileSelect(event, uploadConfig)"
style="display: none"
/>
<button @click="triggerFileInput(fileInput)" class="upload-button" title="上传附件" :disabled="loading">
<paper-clip-outlined />
</button>
</div>
<!-- 语音识别按钮 --> <!-- 语音识别按钮 -->
<VoiceRecognitionText ref="voiceRecognitionRef" :disabled="loading" :debug="true" <VoiceRecognitionText ref="voiceRecognitionRef" :disabled="loading" :debug="true"
:baseConfig="baseConfig" :baseConfig="baseConfig"
@text="handleVoiceText" @text="handleVoiceText"
@error="handleVoiceError" class="voice-recognition-wrapper" /> @error="handleVoiceError" class="voice-recognition-wrapper" />
<textarea ref="textarea" v-model="messageText" placeholder="输入消息..." @keypress="handleKeyPress"
@input="adjustTextareaHeight"></textarea>
<button @click="sendMessage()" :disabled="loading" class="send-button"> <button @click="sendMessage()" :disabled="loading" class="send-button">
<send-outlined /> <send-outlined />
</button> </button>
...@@ -133,13 +208,16 @@ ...@@ -133,13 +208,16 @@
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'; import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, mergeMarkdownContent } from './utils/markdownTemplate'; import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, mergeMarkdownContent } from './utils/markdownTemplate';
import { SendOutlined, ReloadOutlined, CopyOutlined, LikeOutlined, DislikeOutlined } from '@ant-design/icons-vue'; import { SendOutlined, ReloadOutlined, CopyOutlined, LikeOutlined, DislikeOutlined, PaperClipOutlined, EyeOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { message as antdMessage } from 'ant-design-vue'; import { useAttachments } from './hooks/useAttachments';
import defaultAvatar from '@/assets/logo.png'; import { useFeedback } from './hooks/useFeedback';
import rightIcon from '@/assets/right.svg' import { useMessageActions } from './hooks/useMessageActions';
import thinkIcon from '@/assets/think.svg' import defaultAvatar from '../../assets/logo.png';
import rightIcon from '../../assets/right.svg'
import thinkIcon from '../../assets/think.svg'
import ChartComponent from './ChartComponent.vue'; // 导入独立的图表组件 import ChartComponent from './ChartComponent.vue'; // 导入独立的图表组件
import VoiceRecognitionText from './VoiceRecognitionText.vue'; // 导入语音识别组件 import VoiceRecognitionText from './VoiceRecognitionText.vue'; // 导入语音识别组件
import FeedbackModal from './FeedbackModal.vue'; // 导入反馈弹窗组件
import { createContentTemplateService, type Message } from './utils/contentTemplateService'; // 导入模板服务 import { createContentTemplateService, type Message } from './utils/contentTemplateService'; // 导入模板服务
// 定义组件属性接口 // 定义组件属性接口
...@@ -149,7 +227,7 @@ interface Props { ...@@ -149,7 +227,7 @@ interface Props {
// 基础配置对象 // 基础配置对象
baseConfig?: { baseConfig?: {
apiBaseUrl?: string apiBaseUrl?: string
token?: string userToken?: string
appCode?: string appCode?: string
} }
logoUrl?: string logoUrl?: string
...@@ -168,7 +246,7 @@ const props = withDefaults(defineProps<Props>(), { ...@@ -168,7 +246,7 @@ const props = withDefaults(defineProps<Props>(), {
dialogSessionId: '', dialogSessionId: '',
baseConfig: () => ({ baseConfig: () => ({
apiBaseUrl: '', apiBaseUrl: '',
token: '', userToken: '',
appCode: '' appCode: ''
}), }),
logoUrl: '', logoUrl: '',
...@@ -198,6 +276,39 @@ const currentAIResponse = ref<Message | null>(null); ...@@ -198,6 +276,39 @@ const currentAIResponse = ref<Message | null>(null);
const isAIResponding = ref(false); const isAIResponding = ref(false);
const dialogSessionId = ref(props.dialogSessionId || ''); const dialogSessionId = ref(props.dialogSessionId || '');
const isInThinkingMode = ref(false); const isInThinkingMode = ref(false);
// 赞踩功能hooks
const {
feedbackModalData,
likeMessage,
dislikeMessage,
submitFeedback
} = useFeedback(props.baseConfig);
// 消息操作hooks
const {
handleMessageClick,
handleCopy
} = useMessageActions(messageText);
// 附件上传相关 - 使用hooks
const {
attachments,
hasAttachment,
currentAttachment,
getFileTypeIcon,
formatFileSize,
getAcceptTypes,
previewAttachment,
removeAttachment,
clearAttachments,
isUploading,
handlePaste,
handleFileSelect,
triggerFileInput
} = useAttachments();
const fileInput = ref<HTMLInputElement>();
const currentBlockIndex = ref(-1); const currentBlockIndex = ref(-1);
const hasStartedConversation = ref(false); // 添加对话开始状态 const hasStartedConversation = ref(false); // 添加对话开始状态
...@@ -221,6 +332,8 @@ const handleSSEMessage = (data: SSEData, isSimulated: boolean = false) => { ...@@ -221,6 +332,8 @@ const handleSSEMessage = (data: SSEData, isSimulated: boolean = false) => {
totalTokens: 0, totalTokens: 0,
date: dayjs().format('HH:mm'), date: dayjs().format('HH:mm'),
contentBlocks: [], contentBlocks: [],
upCount: 0,
downCount: 0,
}; };
messages.value.push(currentAIResponse.value); messages.value.push(currentAIResponse.value);
}; };
...@@ -302,6 +415,13 @@ const handleSSEMessage = (data: SSEData, isSimulated: boolean = false) => { ...@@ -302,6 +415,13 @@ const handleSSEMessage = (data: SSEData, isSimulated: boolean = false) => {
// 创建模板服务实例 // 创建模板服务实例
const templateService = createContentTemplateService(); const templateService = createContentTemplateService();
// 上传配置
const uploadConfig = {
apiBaseUrl: props.baseConfig?.apiBaseUrl,
userToken: props.baseConfig?.userToken,
appCode: props.baseConfig?.appCode
};
// 语音转文字事件处理函数 // 语音转文字事件处理函数
const handleVoiceText = (textMessage: string) => { const handleVoiceText = (textMessage: string) => {
// 直接使用统一的sendMessage函数发送文本消息 // 直接使用统一的sendMessage函数发送文本消息
...@@ -322,7 +442,7 @@ const startConversation = () => { ...@@ -322,7 +442,7 @@ const startConversation = () => {
}; };
// 定义消息类型 // 定义消息类型
type MessageType = 'text' | 'audio'; type MessageType = 'text' | 'audio' | 'attachment';
// 定义消息参数接口 // 定义消息参数接口
interface MessageParams { interface MessageParams {
...@@ -341,6 +461,8 @@ const validateMessageParams = (type: MessageType, params: MessageParams): boolea ...@@ -341,6 +461,8 @@ const validateMessageParams = (type: MessageType, params: MessageParams): boolea
return !!messageContent; return !!messageContent;
case 'audio': case 'audio':
return !!audioUrl; return !!audioUrl;
case 'attachment':
return attachments.value.length > 0;
default: default:
return false; return false;
} }
...@@ -348,6 +470,13 @@ const validateMessageParams = (type: MessageType, params: MessageParams): boolea ...@@ -348,6 +470,13 @@ const validateMessageParams = (type: MessageType, params: MessageParams): boolea
// 统一发送消息函数 // 统一发送消息函数
const sendMessage = async (type: MessageType = 'text', params: MessageParams = {}) => { const sendMessage = async (type: MessageType = 'text', params: MessageParams = {}) => {
// 根据是否有附件自动确定消息类型
if (attachments.value.length > 0) {
type = 'attachment' as MessageType;
}
if (isUploading.value) {
return;
}
//如果消息文本为空且是文本类型,则延迟1秒后模拟折线图消息进行测试 //如果消息文本为空且是文本类型,则延迟1秒后模拟折线图消息进行测试
// if (type === 'text' && !messageText.value.trim() && !params.message) { // if (type === 'text' && !messageText.value.trim() && !params.message) {
// loading.value = true; // loading.value = true;
...@@ -395,13 +524,20 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = { ...@@ -395,13 +524,20 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
contentBlocks: [] as any[], contentBlocks: [] as any[],
status: 1, // 发送中状态 status: 1, // 发送中状态
originalContent: messageContent, // 保存原始内容用于重发 originalContent: messageContent, // 保存原始内容用于重发
originalMessageType: type // 保存消息类型用于重发 originalMessageType: type === 'attachment' ? 'text' : type, // 重发时使用text类型
}; attachments: attachments.value.map(a => ({
attachmentName: a.attachmentName || '',
switch (type) { attachmentType: a.attachmentType,
attachmentSize: a.attachmentSize,
attachmentPath: a.attachmentPath || '', // 后台返回的URL
})),
upCount: 0,
downCount: 0,
} as Message;
switch (type) {
case 'text': case 'text':
messageData.contentBlocks.push({ messageData.contentBlocks.push({
content: templateService.generateTextTemplate(messageContent), content: templateService.generateQuestionTemplate(messageContent),
thinkContent: '', thinkContent: '',
hasThinkBox: false, hasThinkBox: false,
thinkBoxExpanded: false, thinkBoxExpanded: false,
...@@ -413,6 +549,30 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = { ...@@ -413,6 +549,30 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
messageText.value = ''; messageText.value = '';
break; break;
case 'attachment':
// 处理附件消息
if (attachments.value.length > 0) {
const attachment = attachments.value[0]; // 只取第一个附件(单文件限制)
messageData.contentBlocks.push({
attachmentData: {
attachmentName: attachment.attachmentName,
attachmentType: attachment.attachmentType,
attachmentSize: attachment.attachmentSize,
attachmentPath: attachment.attachmentPath || '', // 后台返回的URL
},
content: templateService.generateQuestionTemplate(messageContent),
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
});
}
// 重置文本输入框
if (textarea.value) {
textarea.value.style.height = '52px';
}
messageText.value = '';
break;
case 'audio': case 'audio':
messageData.contentBlocks.push({ messageData.contentBlocks.push({
audioData: { audioData: {
...@@ -443,25 +603,48 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = { ...@@ -443,25 +603,48 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
} else { } else {
// 默认的API调用逻辑 // 默认的API调用逻辑
console.log(`默认API调用逻辑`, dialogSessionId.value); console.log(`默认API调用逻辑`, dialogSessionId.value);
const requestData = type === 'audio' ? { let attachmentJson: any = null;
questionLocalAudioFilePath: audioUrl, let requestData: any = {
audioDuration: durationTime, ...props.params,
...props.params, dialogSessionId: dialogSessionId.value,
dialogSessionId: dialogSessionId.value,
appId: props.params?.appId,
} : {
question: messageContent,
...props.params,
dialogSessionId: dialogSessionId.value,
appId: props.params?.appId, appId: props.params?.appId,
}; };
const { token, appCode } = props.baseConfig || {}; switch (type) {
case 'text':
requestData = {
...requestData,
question: messageContent,
};
break;
case 'attachment':
attachmentJson = JSON.parse(JSON.stringify(attachments.value[0]));
requestData = {
...requestData,
attachmentPath: attachmentJson?.attachmentPath || '',
attachmentType: attachmentJson?.attachmentType || '',
attachmentSize: attachmentJson?.attachmentSize || 0,
attachmentName: attachmentJson?.attachmentName || '',
question: messageContent,
}
clearAttachments();
break;
case 'audio':
requestData = {
...requestData,
questionLocalAudioFilePath: audioUrl,
audioDuration: durationTime,
}
break;
default:
break;
}
const { userToken, appCode } = props.baseConfig || {};
const response = await fetch(`${import.meta.env.VITE_SSE_PATH}/sse/ask`, { const response = await fetch(`${import.meta.env.VITE_SSE_PATH}/sse/ask`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'token': token || '', 'token': userToken || '',
'x-session-id': token || '', 'x-session-id': userToken || '',
'x-app-code': appCode || '' 'x-app-code': appCode || ''
} as HeadersInit, } as HeadersInit,
body: JSON.stringify(requestData) body: JSON.stringify(requestData)
...@@ -472,10 +655,14 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = { ...@@ -472,10 +655,14 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
// 处理SSE流式响应 // 处理SSE流式响应
await processSSEStreamResponse(response.body); await processSSEStreamResponse(response.body);
console.log(`发送成功`) console.log(`发送成功`)
attachmentJson = null;
// 发送成功,更新消息状态为已发送 // 发送成功,更新消息状态为已发送
if (messageData) { if (messageData) {
messageData.status = 2; messageData.status = 2;
} }
// 发送成功后清空附件(在API调用完成后)
clearAttachments();
}else{ }else{
loading.value = false; loading.value = false;
// 设置当前消息为失败状态 // 设置当前消息为失败状态
...@@ -486,9 +673,12 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = { ...@@ -486,9 +673,12 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
} }
} catch (e) { } catch (e) {
loading.value = false; loading.value = false;
console.error(`发送失败:`, e); console.error(`发送失败:`, e);
} finally { } finally {
loading.value = false; loading.value = false;
// 发送成功后清空附件
clearAttachments();
} }
}; };
...@@ -519,57 +709,6 @@ const handleRecommendationClick = (message: Message, item: any) => { ...@@ -519,57 +709,6 @@ const handleRecommendationClick = (message: Message, item: any) => {
sendMessage('text', { message: item.title }); sendMessage('text', { message: item.title });
}; };
// 处理消息点击 - 将消息内容放到输入框
const handleMessageClick = (message: Message, block: any) => {
// 只处理文字消息,不处理音频、图表等特殊消息
if (block.audioData || block.chartData) {
return;
}
try {
// 提取消息中的文本内容
let textToInput = '';
if (message.contentBlocks && message.contentBlocks.length > 0) {
message.contentBlocks.forEach(block => {
// 跳过非文字内容块
if (block.audioData || block.chartData) {
return;
}
if (block.content) {
// 移除HTML标签,只保留纯文本
const textContent = block.content.replace(/<[^>]*>/g, '').trim();
if (textContent) {
textToInput += textContent + '\n';
}
}
});
}
// 如果没有内容,使用原始内容
if (!textToInput.trim() && message.originalContent) {
textToInput = message.originalContent;
}
if (textToInput.trim()) {
// 将内容设置到输入框
messageText.value = textToInput.trim();
// 自动调整输入框高度
adjustTextareaHeight();
// 聚焦到输入框
if (textarea.value) {
textarea.value.focus();
}
// 提示用户
antdMessage.success('消息内容已放入输入框');
}
} catch (error) {
console.error('处理消息点击失败:', error);
}
};
// 处理SSE流式响应 // 处理SSE流式响应
const processSSEStreamResponse = async (stream: ReadableStream | null) => { const processSSEStreamResponse = async (stream: ReadableStream | null) => {
if (!stream) { if (!stream) {
...@@ -668,12 +807,12 @@ const getChatRecord = async (dialogSessionId: string) => { ...@@ -668,12 +807,12 @@ const getChatRecord = async (dialogSessionId: string) => {
messages.value = [...recordList]; messages.value = [...recordList];
} }
} else { } else {
const { token, appCode } = props.baseConfig || {}; const { userToken, appCode } = props.baseConfig || {};
const response = await fetch(`${props.baseConfig?.apiBaseUrl || ''}/aiService/ask/list/chat/${dialogSessionId}`, { const response = await fetch(`${props.baseConfig?.apiBaseUrl || ''}/aiService/ask/list/chat/${dialogSessionId}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'token': token || '', 'token': userToken || '',
'x-session-id': token || '', 'x-session-id': userToken || '',
'x-app-code': appCode || '' 'x-app-code': appCode || ''
} as HeadersInit } as HeadersInit
}); });
...@@ -694,12 +833,12 @@ const getAppInfo = async () => { ...@@ -694,12 +833,12 @@ const getAppInfo = async () => {
if(!props.params?.appId) { if(!props.params?.appId) {
return; return;
} }
const { token, appCode } = props.baseConfig || {}; const { userToken, appCode } = props.baseConfig || {};
const response = await fetch(`${import.meta.env.VITE_SSE_PATH}/apps/getAppInfoById/${props.params?.appId}`, { const response = await fetch(`${import.meta.env.VITE_SSE_PATH}/apps/getAppInfoById/${props.params?.appId}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'token': token || '', 'token': userToken || '',
'x-session-id': token || '', 'x-session-id': userToken || '',
'x-app-code': appCode || '' 'x-app-code': appCode || ''
} as HeadersInit } as HeadersInit
}); });
...@@ -719,68 +858,8 @@ const toggleThinkBox = (messageIndex: number, blockIndex: number) => { ...@@ -719,68 +858,8 @@ const toggleThinkBox = (messageIndex: number, blockIndex: number) => {
} }
}; };
// 复制消息内容
const handleCopy = async (msg: Message) => {
try {
let textToCopy = '';
// 遍历所有内容块
msg.contentBlocks.forEach(block => {
if (block.chartData) {
// 如果是表格内容,复制description
textToCopy += block.chartData.description || '';
} else if (block.content) {
// 检查是否是iframe内容
if (block.content.includes('message-iframe')) {
// 提取iframe的src属性
const srcMatch = block.content.match(/src="([^"]+)"/);
if (srcMatch && srcMatch[1]) {
// 添加API基础URL到src前面
const fullSrc = import.meta.env.VITE_API_BASE_URL + srcMatch[1];
textToCopy += fullSrc + '\n';
}
} else {
// 其他内容复制content
// 移除HTML标签,保留纯文本
const textContent = block.content.replace(/<[^>]*>/g, '').trim();
if (textContent) {
textToCopy += textContent + '\n';
}
}
}
});
// 如果没有内容,使用原始内容
if (!textToCopy.trim() && msg.originalContent) {
textToCopy = msg.originalContent;
}
if (textToCopy.trim()) {
// 使用Clipboard API复制到剪贴板
await navigator.clipboard.writeText(textToCopy.trim());
antdMessage.success('内容已复制到剪贴板');
} else {
antdMessage.warning('没有可复制的内容');
}
} catch (error) {
console.error('复制失败:', error);
antdMessage.error('复制失败,请手动复制');
}
};
// 点赞消息
const handleLike = (msg: Message) => {
console.log('点赞消息:', msg.recordId);
antdMessage.success('已点赞');
// 这里可以添加实际的点赞API调用
};
// 踩消息
const handleDislike = (msg: Message) => {
console.log('踩消息:', msg.recordId);
antdMessage.success('已踩');
// 这里可以添加实际的踩API调用
};
// 处理按键事件 // 处理按键事件
const handleKeyPress = (e: KeyboardEvent) => { const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
......
<template>
<a-modal :visible="visible" title="反馈" :width="500" centered class="feedback-modal"
:maskClosable="false" @cancel="handleCancel" @update:visible="(value) => emit('update:visible', value)">
<div class="feedback-content">
<div class="feedback-input">
<a-textarea v-model:value="feedbackContent" placeholder="我们想知道您对此回答不满意的原因,您认为更好的回答是什么?" :rows="8"
:maxlength="500" show-count />
</div>
</div>
<template #footer>
<div class="feedback-actions">
<a-button @click="handleCancel" class="cancel-btn">取消</a-button>
<a-button type="primary" @click="handleSubmit" :disabled="!feedbackContent.trim()" class="submit-btn">
提交
</a-button>
</div>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
// 定义组件属性
interface Props {
visible: boolean;
}
interface Emits {
(e: 'update:visible', value: boolean): void;
(e: 'submit', comment: string): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
// 响应式数据
const feedbackContent = ref<string>('');
// 监听visible变化
watch(() => props.visible, (newVal) => {
if (!newVal) {
// 关闭时重置表单
resetForm();
}
});
// 重置表单
const resetForm = () => {
feedbackContent.value = '';
};
// 处理取消
const handleCancel = () => {
emit('update:visible', false);
resetForm();
};
// 处理提交
const handleSubmit = async () => {
if (!feedbackContent.value.trim()) {
message.warning('请输入反馈内容');
return;
}
try {
// 只emit comment数据,其他逻辑在hooks中处理
emit('submit', feedbackContent.value.trim());
} catch (error) {
console.error('提交反馈失败:', error);
}
};
</script>
<style lang="less">
.feedback-modal {
max-width: calc(100vw - 50px) !important;
.ant-modal-body {
padding: 16px 16px 0;
}
.feedback-content {
.feedback-input {
margin-bottom: 30px;
}
.feedback-actions {
display: flex;
justify-content: flex-end;
gap: 16px;
.cancel-btn {
min-width: 60px;
}
.submit-btn {
min-width: 60px;
}
}
}
}
</style>
\ No newline at end of file
...@@ -59,7 +59,7 @@ interface Props { ...@@ -59,7 +59,7 @@ interface Props {
debug?: boolean debug?: boolean
maxDuration?: number, // 添加最大时长参数 maxDuration?: number, // 添加最大时长参数
baseConfig?: { baseConfig?: {
token?: string, userToken?: string,
appCode?: string, appCode?: string,
apiBaseUrl?: string, apiBaseUrl?: string,
} }
...@@ -70,7 +70,7 @@ const props = withDefaults(defineProps<Props>(), { ...@@ -70,7 +70,7 @@ const props = withDefaults(defineProps<Props>(), {
debug: false, debug: false,
maxDuration: 30, // 默认最大时长为30秒 maxDuration: 30, // 默认最大时长为30秒
baseConfig: () => ({ baseConfig: () => ({
token: '', userToken: '',
appCode: '', appCode: '',
apiBaseUrl: '', apiBaseUrl: '',
}) })
...@@ -412,13 +412,13 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, dura ...@@ -412,13 +412,13 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, dura
const formData = new FormData(); const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav'); formData.append('file', audioBlob, 'recording.wav');
formData.append('fileFolder', 'AI_TEMP'); formData.append('fileFolder', 'AI_TEMP');
const { token, appCode } = props.baseConfig || {}; const { userToken, appCode } = props.baseConfig || {};
const response = await fetch(`${props.baseConfig?.apiBaseUrl || ''}/platformService/upload/v2`, { const response = await fetch(`${props.baseConfig?.apiBaseUrl || ''}/platformService/upload/v2`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-app-code': appCode || '', 'x-app-code': appCode || '',
'token': token || '', 'token': userToken || '',
'x-session-id': token || '', 'x-session-id': userToken || '',
} as HeadersInit, } as HeadersInit,
body: formData body: formData
}); });
......
...@@ -59,7 +59,7 @@ interface Props { ...@@ -59,7 +59,7 @@ interface Props {
debug?: boolean debug?: boolean
maxDuration?: number, // 添加最大时长参数 maxDuration?: number, // 添加最大时长参数
baseConfig?: { baseConfig?: {
token?: string, userToken?: string,
appCode?: string, appCode?: string,
apiBaseUrl?: string, apiBaseUrl?: string,
} }
...@@ -70,7 +70,7 @@ const props = withDefaults(defineProps<Props>(), { ...@@ -70,7 +70,7 @@ const props = withDefaults(defineProps<Props>(), {
debug: false, debug: false,
maxDuration: 30, // 默认最大时长为30秒 maxDuration: 30, // 默认最大时长为30秒
baseConfig: () => ({ baseConfig: () => ({
token: '', userToken: '',
appCode: '', appCode: '',
apiBaseUrl: '', apiBaseUrl: '',
}) })
...@@ -413,13 +413,13 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<string> => { ...@@ -413,13 +413,13 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<string> => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav'); formData.append('file', audioBlob, 'recording.wav');
formData.append('fileFolder', 'AI_TEMP'); formData.append('fileFolder', 'AI_TEMP');
const { token, appCode } = props.baseConfig || {}; const { userToken, appCode } = props.baseConfig || {};
const response = await fetch(`/agentService/index/audio2txt`, { const response = await fetch(`/agentService/index/audio2txt`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-app-code': appCode || '', 'x-app-code': appCode || '',
'token': token || '', 'token': userToken || '',
'x-session-id': token || '', 'x-session-id': userToken || '',
} as HeadersInit, } as HeadersInit,
body: formData body: formData
}); });
......
import { ref, computed } from 'vue';
import { message as antdMessage } from 'ant-design-vue';
export interface Attachment {
id: string;
file: File;
previewUrl: string; // 本地预览URL
attachmentPath: string; // 后台返回的URL
attachmentName: string;
attachmentType: string;
attachmentSize: number;
}
// 支持的文件扩展名
const supportedFileExtensions = {
image: ['.png', '.jpg', '.jpeg'],
// document: ['.pdf', '.pptx', '.docx', '.doc', '.docx'],
// text: ['.txt', '.md']
};
// 上传配置接口
interface UploadConfig {
apiBaseUrl?: string;
userToken?: string;
appCode?: string;
}
export function useAttachments() {
// 附件列表(只允许一个文件)
const attachments = ref<Attachment[]>([]);
// 是否有附件的计算属性
const hasAttachment = computed(() => attachments.value.length > 0);
// 当前附件(只允许一个)
const currentAttachment = computed(() => attachments.value[0] || null);
// 上传状态
const isUploading = ref(false);
// 上传文件到后台
const uploadFileToServer = async (file: File, config: UploadConfig): Promise<string> => {
try {
isUploading.value = true;
const formData = new FormData();
formData.append('file', file);
formData.append('fileFolder', 'AI_TEMP');
const { userToken, appCode } = config;
const response = await fetch(`${config.apiBaseUrl || ''}/platformService/upload/v2`, {
method: 'POST',
headers: {
'x-app-code': appCode || '',
'token': userToken || '',
'x-session-id': userToken || '',
} as HeadersInit,
body: formData
});
const result = await response.json();
if (result.code === 0) {
return result.data.filePath;
} else {
throw new Error(result.message || '上传失败');
}
} catch (error) {
console.error('文件上传失败:', error);
throw error;
} finally {
isUploading.value = false;
}
};
// 获取文件类型图标
const getFileTypeIcon = (fileType: string): string => {
if (fileType.startsWith('image/')) return '🖼️';
if (fileType.includes('pdf')) return '📄';
if (fileType.includes('word') || fileType.includes('document')) return '📝';
if (fileType.startsWith('text/')) return '📄';
return '📎';
};
// 格式化文件大小
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 获取可接受的文件类型(用于文件选择器)
const getAcceptTypes = (): string => {
return Object.values(supportedFileExtensions).flat().map(ext => ext).join(',');
};
// 检查文件是否支持
const isFileSupported = (file: File): boolean => {
// 检查文件扩展名
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
const isExtensionSupported = Object.values(supportedFileExtensions).flat().some(ext => ext === fileExtension);
if (!isExtensionSupported) {
antdMessage.warning(`不支持的文件类型: ${file.name}`);
return false;
}
// 检查文件大小(限制为10MB)
if (file.size > 10 * 1024 * 1024) {
antdMessage.warning('文件大小不能超过10MB');
return false;
}
return true;
};
// 添加附件(替换现有附件)
const addAttachment = async (file: File, config?: UploadConfig): Promise<boolean> => {
if (!isFileSupported(file)) return false;
// 清空现有附件
clearAttachments();
// 创建附件对象(先创建本地预览)
const attachment: Attachment = {
id: Math.random().toString(36).substr(2, 9),
file: file,
previewUrl: file.type.startsWith('image/') ? URL.createObjectURL(file) : '',
attachmentPath: '', // 初始为空,稍后上传
attachmentName: file.name,
attachmentType: file.type,
attachmentSize: file.size
};
attachments.value.push(attachment);
// 如果有上传配置,立即上传到后台
if (config) {
try {
const remoteUrl = await uploadFileToServer(file, config);
// 更新附件的远程URL
const current = attachments.value.find(a => a.id === attachment.id);
if (current) {
current.attachmentPath = remoteUrl;
}
} catch (error) {
antdMessage.error('文件上传失败');
// 上传失败时移除附件
removeAttachment(attachment.id);
return false;
}
}
return true;
};
// 从粘贴事件添加附件
const addAttachmentFromPaste = async (event: ClipboardEvent, config?: UploadConfig): Promise<boolean> => {
const clipboardData = event.clipboardData;
if (!clipboardData) return false;
// 检查是否有支持的文件数据
const items = Array.from(clipboardData.items);
// 查找所有支持的文件类型
const supportedItems = items.filter(item => {
// 检查是否是文件类型
if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
return isFileSupported(file); // 使用现有的文件支持检查函数
}
}
return false;
});
if (supportedItems.length > 0) {
event.preventDefault(); // 阻止默认粘贴行为
// 只取第一个支持的文件(单文件限制)
const file = supportedItems[0].getAsFile();
if (file) {
return await addAttachment(file, config);
}
}
return false;
};
// 预览附件
const previewAttachment = (attachment: Attachment) => {
// 优先使用后台返回的URL,如果没有则使用本地预览URL
const url = attachment.attachmentPath || attachment.previewUrl;
if (attachment.attachmentType.startsWith('image/')) {
// 图片预览 - 新窗口打开URL
window.open(url, '_blank');
} else {
// 其他文件类型,可以下载预览
if (attachment.attachmentPath) {
// 如果有后台URL,直接打开
window.open(url, '_blank');
} else {
// 如果没有后台URL,使用本地文件下载
const localUrl = URL.createObjectURL(attachment.file);
const a = document.createElement('a');
a.href = localUrl;
a.download = attachment.attachmentName;
a.click();
URL.revokeObjectURL(localUrl);
}
}
};
// 删除附件
const removeAttachment = (attachmentId: string) => {
const attachment = attachments.value.find(a => a.id === attachmentId);
if (attachment && attachment.previewUrl) {
URL.revokeObjectURL(attachment.previewUrl);
}
attachments.value = attachments.value.filter(a => a.id !== attachmentId);
};
// 清空所有附件
const clearAttachments = () => {
attachments.value.forEach(attachment => {
if (attachment.previewUrl) {
URL.revokeObjectURL(attachment.previewUrl);
}
});
attachments.value = [];
};
/**
* 处理粘贴事件,自动识别并上传附件
*/
const handlePaste = async (event: ClipboardEvent, config?: UploadConfig): Promise<boolean> => {
return await addAttachmentFromPaste(event, config);
};
/**
* 处理文件选择事件
*/
const handleFileSelect = async (event: Event, config?: UploadConfig): Promise<boolean> => {
const input = event.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return false;
const file = input.files[0];
return await addAttachment(file, config);
};
/**
* 触发文件选择器
*/
const triggerFileInput = (fileInput: HTMLInputElement | null) => {
if (fileInput) {
fileInput.click();
}
};
return {
// 响应式数据
attachments,
hasAttachment,
currentAttachment,
isUploading,
// 核心方法
getFileTypeIcon,
formatFileSize,
getAcceptTypes,
addAttachment,
addAttachmentFromPaste,
previewAttachment,
removeAttachment,
clearAttachments,
// 事件处理方法
handlePaste,
handleFileSelect,
triggerFileInput
};
}
\ No newline at end of file
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import type { Message } from '../utils/contentTemplateService';
// 反馈弹窗相关数据接口
export interface FeedbackModalData {
visible: boolean;
recordId: string;
type: 'up' | 'down'; // 反馈类型:up=赞,down=踩
msg?: any; // 保存对应的消息对象,避免重复查找
}
// 反馈提交数据接口
export interface FeedbackSubmitData {
recordId: string;
comment?: string; // 可选,踩的时候需要
type: 'up' | 'down'; // 反馈类型:up=赞,down=踩
}
// 基础配置接口
interface BaseConfig {
apiBaseUrl?: string;
userToken?: string;
appCode?: string;
}
/**
* 赞踩功能hooks
* 使用同一个接口,只是类型不同,踩多一个理由
*/
export function useFeedback(baseConfig?: BaseConfig) {
// 反馈弹窗相关数据
const feedbackModalData = ref<FeedbackModalData>({
visible: false,
recordId: '',
type: 'up'
});
/**
* 统一的反馈接口
*/
const submitFeedbackApi = async (feedbackData: FeedbackSubmitData) => {
// 使用传入的配置,如果没有则使用默认值
const config = baseConfig || {};
const {userToken, appCode} = config || {};
const response = await fetch(`${import.meta.env.VITE_SSE_PATH}/ask/comment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-app-code': appCode || '',
'token': userToken || '',
'x-session-id': userToken || '',
} as HeadersInit,
body: JSON.stringify({
...feedbackData
})
});
const result = await response.json();
if (result.code === 0) {
return result;
} else {
throw new Error(result.message || '反馈提交失败');
}
};
/**
* 点赞消息
*/
const likeMessage = async (msg: Message) => {
if (msg.upCount > 0) {
message.warning('您已经点过赞了');
return;
}
try {
// 调用统一的反馈接口,type='up'
let result = await submitFeedbackApi({
recordId: msg.recordId,
type: 'up'
});
// 更新前端状态
if(result.code === 0) {
msg.upCount = result.data.up_count;
message.success('感谢您的反馈!');
// 提交成功后关闭弹窗
closeFeedbackModal();
}
message.success('点赞成功');
} catch (error) {
console.error('点赞失败:', error);
message.error('点赞失败,请稍后重试');
}
};
/**
* 踩消息
*/
const dislikeMessage = async (msg: Message) => {
if (msg.downCount > 0) {
message.warning('您已经踩过了');
return;
}
// 直接显示反馈弹窗,让用户先输入理由,并保存msg对象
feedbackModalData.value = {
visible: true,
recordId: msg.recordId,
type: 'down',
msg: msg // 保存消息对象,避免重复查找
};
};
/**
* 处理反馈提交(踩的时候输入理由)
*/
const submitFeedback = async (comment: string) => {
const { recordId, type, msg } = feedbackModalData.value;
try {
// 调用统一的反馈接口,包含用户输入的理由
let result = await submitFeedbackApi({
recordId,
comment,
type
});
// 更新前端状态
if(result.code === 0) {
msg.downCount = result.data.down_count;
message.success('感谢您的反馈!');
// 提交成功后关闭弹窗
closeFeedbackModal();
}
} catch (error) {
message.error('反馈提交失败,请稍后重试');
}
};
/**
* 关闭反馈弹窗
*/
const closeFeedbackModal = () => {
feedbackModalData.value.visible = false;
};
/**
* 打开反馈弹窗
*/
const openFeedbackModal = (recordId: string, type: 'up' | 'down') => {
feedbackModalData.value = {
visible: true,
recordId,
type
};
};
return {
// 响应式数据
feedbackModalData,
// 操作方法
likeMessage,
dislikeMessage,
submitFeedback,
closeFeedbackModal,
openFeedbackModal
};
}
\ No newline at end of file
import { type Ref } from 'vue';
import { message as antdMessage } from 'ant-design-vue';
import type { Message, MessageBlock } from '../utils/contentTemplateService';
/**
* 消息操作hooks
* 封装了复制、消息点击等相关逻辑
*/
export function useMessageActions(messageText: Ref<string>) {
/**
* 处理消息点击 - 将消息内容放到输入框
*/
const handleMessageClick = (message: Message, block: MessageBlock, textarea: HTMLTextAreaElement | null) => {
// 只处理文字消息,不处理音频、图表等特殊消息
if (block.audioData || block.chartData) {
return;
}
try {
// 提取消息中的文本内容
let textToInput = '';
if (message.contentBlocks && message.contentBlocks.length > 0) {
message.contentBlocks.forEach(block => {
// 跳过非文字内容块
if (block.audioData || block.chartData) {
return;
}
if (block.content) {
// 移除HTML标签,只保留纯文本
const textContent = block.content.replace(/<[^>]*>/g, '').trim();
if (textContent) {
textToInput += textContent + '\n';
}
}
});
}
// 如果没有内容,使用原始内容
if (!textToInput.trim() && message.originalContent) {
textToInput = message.originalContent;
}
if (textToInput.trim()) {
// 将内容设置到输入框
messageText.value = textToInput.trim();
// 自动调整输入框高度
adjustTextareaHeight(textarea);
// 聚焦到输入框
if (textarea) {
textarea.focus();
}
// 提示用户
antdMessage.success('消息内容已放入输入框');
}
} catch (error) {
console.error('处理消息点击失败:', error);
}
};
/**
* 复制消息内容
*/
const handleCopy = async (msg: Message) => {
try {
let textToCopy = '';
// 遍历所有内容块
msg.contentBlocks.forEach(block => {
if (block.chartData) {
// 如果是表格内容,复制description
textToCopy += block.chartData.description || '';
} else if (block.content) {
// 检查是否是iframe内容
if (block.content.includes('message-iframe')) {
// 提取iframe的src属性
const srcMatch = block.content.match(/src="([^"]+)"/);
if (srcMatch && srcMatch[1]) {
// 添加API基础URL到src前面
const fullSrc = import.meta.env.VITE_API_BASE_URL + srcMatch[1];
textToCopy += fullSrc + '\n';
}
} else {
// 其他内容复制content
// 移除HTML标签,保留纯文本
const textContent = block.content.replace(/<[^>]*>/g, '').trim();
if (textContent) {
textToCopy += textContent + '\n';
}
}
}
});
// 如果没有内容,使用原始内容
if (!textToCopy.trim() && msg.originalContent) {
textToCopy = msg.originalContent;
}
if (textToCopy.trim()) {
// 使用Clipboard API复制到剪贴板
await navigator.clipboard.writeText(textToCopy.trim());
antdMessage.success('内容已复制到剪贴板');
} else {
antdMessage.warning('没有可复制的内容');
}
} catch (error) {
console.error('复制失败:', error);
antdMessage.error('复制失败,请手动复制');
}
};
/**
* 自动调整文本区域高度
*/
const adjustTextareaHeight = (textarea: HTMLTextAreaElement | null) => {
if (textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${Math.min(textarea.scrollHeight, 52)}px`;
}
};
/**
* 提取消息中的纯文本内容
*/
const extractMessageText = (message: Message): string => {
let text = '';
if (message.contentBlocks && message.contentBlocks.length > 0) {
message.contentBlocks.forEach(block => {
// 跳过非文字内容块
if (block.audioData || block.chartData) {
return;
}
if (block.content) {
// 移除HTML标签,只保留纯文本
const textContent = block.content.replace(/<[^>]*>/g, '').trim();
if (textContent) {
text += textContent + '\n';
}
}
});
}
// 如果没有内容,使用原始内容
if (!text.trim() && message.originalContent) {
text = message.originalContent;
}
return text.trim();
};
/**
* 检查消息是否可点击(包含可复制的文本内容)
*/
const isMessageClickable = (message: Message, block: MessageBlock): boolean => {
// 如果有音频、图表等特殊内容,不可点击
if (block.audioData || block.chartData) {
return false;
}
// 检查是否有可复制的文本内容
return extractMessageText(message).length > 0;
};
return {
// 操作方法
handleMessageClick,
handleCopy,
adjustTextareaHeight,
// 工具方法
extractMessageText,
isMessageClickable
};
}
\ No newline at end of file
...@@ -191,7 +191,7 @@ li { ...@@ -191,7 +191,7 @@ li {
// 输入容器 // 输入容器
.chat-input-container { .chat-input-container {
padding: 40px 0 30px; padding: 10px 0 30px;
flex-shrink: 0; flex-shrink: 0;
} }
} }
...@@ -275,7 +275,7 @@ li { ...@@ -275,7 +275,7 @@ li {
} }
} }
.message-content { .message-content {
padding: 10px;
border-radius: 4px; border-radius: 4px;
position: relative; position: relative;
white-space: pre-wrap; white-space: pre-wrap;
...@@ -286,6 +286,13 @@ li { ...@@ -286,6 +286,13 @@ li {
:deep(.message-text) { :deep(.message-text) {
font-size: 16px; font-size: 16px;
} }
:deep(.message-question) {
font-size: 16px;
background: #5B8AFE;
color: @white;
padding: 10px;
border-radius: 4px;
}
} }
} }
...@@ -298,11 +305,6 @@ li { ...@@ -298,11 +305,6 @@ li {
box-shadow:none; box-shadow:none;
} }
.message.sent .message-content {
background: #5B8AFE;
color: @white;
}
// 图表块样式 // 图表块样式
.chart-block { .chart-block {
margin: 20px 0px 8px; margin: 20px 0px 8px;
...@@ -358,6 +360,46 @@ li { ...@@ -358,6 +360,46 @@ li {
background: #fff2f0; background: #fff2f0;
color: @error-color; color: @error-color;
} }
// 禁用状态
&.disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
background: @gray-2;
color: @gray-7;
transform: none;
}
}
// 计数徽章
.count-badge {
position: absolute;
top: -4px;
right: -4px;
background: @primary-color;
color: white;
border-radius: 8px;
font-size: 10px;
min-width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
font-weight: 600;
}
// 赞按钮的计数徽章
&.like-btn .count-badge {
background: @success-color;
}
// 踩按钮的计数徽章
&.dislike-btn .count-badge {
background: @error-color;
}
} }
} }
} }
...@@ -1096,13 +1138,203 @@ li { ...@@ -1096,13 +1138,203 @@ li {
font-style: italic; font-style: italic;
color: @gray-6; color: @gray-6;
} }
}
}
// =============================================
// 附件上传样式
// =============================================
// 附件预览容器
.attachments-preview-container {
width: fit-content;
}
.attachments-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.attachment-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background-color: @gray-1;
border-radius: 8px;
border: 1px solid @gray-3;
}
.attachment-preview {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.preview-image {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
}
.file-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: @blue-light-1;
border-radius: 4px;
font-size: 18px;
}
.attachment-info {
display: flex;
flex-direction: column;
margin-right: 12px;
}
.attachment-name {
font-size: 14px;
font-weight: 500;
color: @gray-7;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attachment-size {
font-size: 12px;
color: @gray-5;
}
.attachment-actions {
display: flex;
gap: 8px;
}
.action-btn {
border:none;
border-radius: 4px;
background-color: @white;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.action-btn:hover {
background-color: @gray-2;
}
.preview-btn {
color: @primary-color;
border-color: @primary-color;
}
.preview-btn:hover {
background-color: @blue-light-1;
}
.remove-btn {
color: @error-color;
border-color: @error-color;
}
.remove-btn:hover {
background-color: @error-bg;
}
// 上传按钮
.upload-wrapper {
display: flex;
align-items: center;
}
.upload-button {
padding: 8px;
border: none;
background: transparent;
cursor: pointer;
color: @gray-5;
transition: color 0.2s;
position: absolute;
right: 92px; // 放在语音识别按钮左边
top: 50%;
transform: translateY(-50%);
z-index: 10;
font-size: 18px;
color: @primary-color;
}
.upload-button:disabled {
color: @gray-4;
cursor: not-allowed;
}
// 消息中的附件
.attachment-block {
margin: 8px 0;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.attachment-display {
display: inline-block;
max-width: 100%;
margin-bottom:8px;
}
.message-attachment-image {
max-width: 200px;
max-height: 200px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.file-display {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background-color: @gray-1;
border-radius: 8px;
border: 1px solid @gray-3;
}
.file-icon {
font-size: 24px;
}
.file-info {
display: flex;
flex-direction: column;
}
.file-name {
font-size: 14px;
font-weight: 500;
color: @gray-7;
}
.file-size {
font-size: 12px;
color: @gray-5;
}
// 图片样式 // 图片样式
img { img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
border-radius: 4px; border-radius: 4px;
margin-bottom: 8px;
} }
// Markdown图片容器样式 // Markdown图片容器样式
...@@ -1278,8 +1510,8 @@ li { ...@@ -1278,8 +1510,8 @@ li {
text-align: left; text-align: left;
} }
} }
}
}
...@@ -1287,7 +1519,6 @@ li { ...@@ -1287,7 +1519,6 @@ li {
.chat-input { .chat-input {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
gap: 8px;
textarea { textarea {
flex: 1; flex: 1;
...@@ -1350,6 +1581,9 @@ li { ...@@ -1350,6 +1581,9 @@ li {
border-top: 1px solid #e8f2f1; // 保持灰色系 border-top: 1px solid #e8f2f1; // 保持灰色系
background: #FCFCFC; // 保持灰色系 background: #FCFCFC; // 保持灰色系
} }
.attachments-preview-container{
padding:10px;
}
} }
} }
...@@ -1365,7 +1599,6 @@ li { ...@@ -1365,7 +1599,6 @@ li {
min-width: 60px; min-width: 60px;
} }
} }
.table-title { .table-title {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
...@@ -1421,7 +1654,6 @@ li { ...@@ -1421,7 +1654,6 @@ li {
} }
} }
} }
.avatar-container { .avatar-container {
display: none; display: none;
} }
...@@ -1434,6 +1666,9 @@ li { ...@@ -1434,6 +1666,9 @@ li {
.chat-input-container{ .chat-input-container{
padding:12px; padding:12px;
} }
.attachments-preview-container{
padding:12px;
}
} }
} }
......
...@@ -11,6 +11,7 @@ const CHART_TYPES = { ...@@ -11,6 +11,7 @@ const CHART_TYPES = {
// 内容模板类型定义 // 内容模板类型定义
export interface ContentTemplates { export interface ContentTemplates {
question: (content: string) => string;
text: (content: string) => string; text: (content: string) => string;
thinking: (content: string) => string; thinking: (content: string) => string;
error: (content: string) => string; error: (content: string) => string;
...@@ -31,11 +32,17 @@ export interface MessageBlock { ...@@ -31,11 +32,17 @@ export interface MessageBlock {
chartType?: number | string; chartType?: number | string;
audioData?: { audioData?: {
src: string; src: string;
durationTime: string; durationTime?: number | string;
}; };
thinkingTime?: number; thinkingTime?: number;
thinkingTimeText?: string; thinkingTimeText?: string;
} attachmentData?:{
attachmentName?: string;
attachmentPath?: string;
attachmentType?: string;
attachmentSize?: number;
}
}
// 消息类型定义 // 消息类型定义
export interface Message { export interface Message {
...@@ -57,6 +64,9 @@ export interface Message { ...@@ -57,6 +64,9 @@ export interface Message {
// 推荐会话相关属性 // 推荐会话相关属性
recommendations?: any[]; recommendations?: any[];
showRecommendations?: boolean; showRecommendations?: boolean;
// 赞踩相关属性
upCount: number; // 点赞数量
downCount: number; // 踩数量
} }
// SSE数据类型定义 // SSE数据类型定义
...@@ -86,7 +96,8 @@ function isLastBlockText(blocks: MessageBlock[]): boolean { ...@@ -86,7 +96,8 @@ function isLastBlockText(blocks: MessageBlock[]): boolean {
return !!lastBlock.content && return !!lastBlock.content &&
!lastBlock.audioData && !lastBlock.audioData &&
!lastBlock.chartData && !lastBlock.chartData &&
!lastBlock.thinkContent; !lastBlock.thinkContent &&
!lastBlock.attachmentData;
} }
// 获取最后一个普通文本块的索引 // 获取最后一个普通文本块的索引
...@@ -96,7 +107,8 @@ function getLastTextBlockIndex(blocks: MessageBlock[]): number { ...@@ -96,7 +107,8 @@ function getLastTextBlockIndex(blocks: MessageBlock[]): number {
if (!!block.content && if (!!block.content &&
!block.audioData && !block.audioData &&
!block.chartData && !block.chartData &&
!block.thinkContent) { !block.thinkContent &&
!block.attachmentData) {
return i; return i;
} }
} }
...@@ -134,6 +146,10 @@ export class ContentTemplateService { ...@@ -134,6 +146,10 @@ export class ContentTemplateService {
// 创建内容模板生成器 // 创建内容模板生成器
private createTemplates(): ContentTemplates { private createTemplates(): ContentTemplates {
return { return {
// 普通文本
question: (content: string) => {
return `<div class="message-question">${content}</div>`;
},
// 普通文本 // 普通文本
text: (content: string) => { text: (content: string) => {
return `<div class="message-text">${content}</div>`; return `<div class="message-text">${content}</div>`;
...@@ -231,6 +247,9 @@ export class ContentTemplateService { ...@@ -231,6 +247,9 @@ export class ContentTemplateService {
public generateTextTemplate(content: string): string { public generateTextTemplate(content: string): string {
return this.templates.text(content); return this.templates.text(content);
} }
public generateQuestionTemplate(content: string): string {
return this.templates.question(content);
}
public generateThinkingTemplate(content: string): string { public generateThinkingTemplate(content: string): string {
return this.templates.thinking(content); return this.templates.thinking(content);
...@@ -567,7 +586,7 @@ export class ContentTemplateService { ...@@ -567,7 +586,7 @@ export class ContentTemplateService {
let date = dayjs(data.startTime).format('YYYY-MM-DD HH:mm:ss'); let date = dayjs(data.startTime).format('YYYY-MM-DD HH:mm:ss');
// 处理问题消息 // 处理问题消息
if (data.question || data.audioPath) { if (data.question || data.audioPath || data.attachmentPath) {
// 创建基础消息结构 // 创建基础消息结构
const message = { const message = {
messageType: 'sent' as const, messageType: 'sent' as const,
...@@ -577,39 +596,57 @@ export class ContentTemplateService { ...@@ -577,39 +596,57 @@ export class ContentTemplateService {
completionTokens: 0, completionTokens: 0,
totalTokens: 0, totalTokens: 0,
date, date,
contentBlocks: [] as MessageBlock[] contentBlocks: [] as MessageBlock[],
// 处理赞踩状态
upCount: data.upCount || 0,
downCount: data.downCount || 0
}; };
// 使用switch语句处理不同类型的消息 // 处理不同类型的消息(允许多种类型共存)
switch (true) { const contentBlocks: MessageBlock[] = [];
case !!data.audioPath:
// 音频消息 // 处理音频消息
message.contentBlocks.push({ if (data.audioPath) {
audioData: { contentBlocks.push({
src: data.audioPath, audioData: {
durationTime: data.audioTime || '0"' src: data.audioPath,
}, durationTime: data.audioTime || '0"'
content: '', },
thinkContent: '', content: '',
hasThinkBox: false, thinkContent: '',
thinkBoxExpanded: false, hasThinkBox: false,
}); thinkBoxExpanded: false,
break; });
}
case !!data.question:
// 文本消息 // 处理附件消息
message.contentBlocks.push({ if (data.attachmentPath) {
content: this.templates.text(data.question), contentBlocks.push({
thinkContent: '', attachmentData: {
hasThinkBox: false, attachmentName: data.attachmentName || '',
thinkBoxExpanded: false, attachmentPath: data.attachmentPath,
}); attachmentType: data.attachmentType || '',
break; attachmentSize: data.attachmentSize || 0,
},
default: content: '',
// 其他类型的消息(未来扩展) thinkContent: '',
break; hasThinkBox: false,
thinkBoxExpanded: false,
});
} }
// 处理文本消息(如果有文本内容)
if (data.question) {
contentBlocks.push({
content: this.templates.question(data.question),
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
});
}
// 将所有内容块添加到消息中
message.contentBlocks = contentBlocks;
result.push(message); result.push(message);
} }
...@@ -625,6 +662,9 @@ export class ContentTemplateService { ...@@ -625,6 +662,9 @@ export class ContentTemplateService {
totalTokens: 0, totalTokens: 0,
contentBlocks: [], contentBlocks: [],
date, date,
// 处理赞踩状态
upCount: data.upCount || 0,
downCount: data.downCount || 0
}; };
let currentThinkingMode = false; let currentThinkingMode = false;
......
...@@ -10,9 +10,11 @@ export interface SSEData { ...@@ -10,9 +10,11 @@ export interface SSEData {
// SSE服务配置 // SSE服务配置
export interface SSEServiceConfig { export interface SSEServiceConfig {
apiBaseUrl: string; baseConfig:{
appCode: string; apiBaseUrl: string;
token: string; userToken: string;
appCode: string;
};
params: { params: {
stage?: string; stage?: string;
appId?: string; appId?: string;
...@@ -54,9 +56,9 @@ export class SSEService { ...@@ -54,9 +56,9 @@ export class SSEService {
const url = `${import.meta.env.VITE_SSE_PATH}/sse/join/${this.config.params?.stage || ''}?app-id=${this.config.params?.appId || ''}&dialog-session-id=${dialogSessionId || ''}`; const url = `${import.meta.env.VITE_SSE_PATH}/sse/join/${this.config.params?.stage || ''}?app-id=${this.config.params?.appId || ''}&dialog-session-id=${dialogSessionId || ''}`;
this.eventSource = new EventSourcePolyfill(url, { this.eventSource = new EventSourcePolyfill(url, {
headers: { headers: {
Token: this.config.token || '', Token: this.config.baseConfig.userToken || '',
'x-session-id': this.config.token || '', 'x-session-id': this.config.baseConfig.userToken || '',
'x-app-code': this.config.appCode || '', 'x-app-code': this.config.baseConfig.appCode || '',
}, },
withCredentials: true, withCredentials: true,
connectionTimeout: 60000, connectionTimeout: 60000,
......
...@@ -53,6 +53,11 @@ export default defineConfig(({ mode }) => { ...@@ -53,6 +53,11 @@ export default defineConfig(({ mode }) => {
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },
'/cfile': {
target: apiBaseUrl,
changeOrigin: true,
secure: false,
},
'/WeChatOauth2': { '/WeChatOauth2': {
target: apiBaseUrl, target: apiBaseUrl,
changeOrigin: true, changeOrigin: true,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment