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

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

parent 72779ad7
......@@ -175,21 +175,21 @@ const getApiBaseUrl = () => {
const apiBaseUrl = getApiBaseUrl();
const userInfo = localStorage.getItem('wechat_user');
const { extMap = {} } = JSON.parse(userInfo || '{}');
const { extMap = {},appId = '' } = JSON.parse(userInfo || '{}');
const userToken = extMap.sessionId;
const appCode = import.meta.env.VITE_APP_CODE || 'ped.qywx';
// 基础配置对象
const baseConfig = {
apiBaseUrl,
token: userToken,
userToken,
appCode
};
const chatParams = {
appId: '83b2664019a945d0a438abe6339758d8',
stage: 'wechat-demo',
};
const time = new Date().getTime();
const chatParams = {
appId: appId, // 企业微信应用ID
stage: 'wechat-demo'+time,
};
const totalCount = ref(0);
const appName = ref('');
interface Session {
......
......@@ -36,12 +36,12 @@
// 基础配置对象
const baseConfig = {
apiBaseUrl,
token: userToken,
userToken,
appCode
};
const time = new Date().getTime();
const chatParams = {
appId: appId || '83b2664019a945d0a438abe6339758d8', // 企业微信应用ID
appId: appId, // 企业微信应用ID
stage: 'wechat-demo'+time,
};
const dialogSessionId = ref('');
......
......@@ -48,11 +48,11 @@ export default {
onMounted(() => {
// 检查是否已登录
const status = wechat.checkLoginStatus()
if (status.isLoggedIn) {
router.replace('/')
return
}
// const status = wechat.checkLoginStatus()
// if (status.isLoggedIn) {
// router.replace('/')
// return
// }
// 执行静默登录
handleLogin()
......
......@@ -6,7 +6,7 @@
<img :src="props?.logoUrl || defaultAvatar" alt="avatar" class="avatar-image" />
</div>
<div class="header-info">
<h2>{{ props?.dialogSessionId ? appData?.app_namee || '继续对话' : '新建对话' }}</h2>
<h2>{{ props?.dialogSessionId ? appData?.app_name || '继续对话' : '新建对话' }}</h2>
</div>
</div>
......@@ -37,13 +37,30 @@
<div class="message-content-wrapper">
<div class="message-content">
<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'"
:title="item.chartData.title || '图表数据'" />
</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>
<!-- 思考过程框 -->
......@@ -97,10 +114,22 @@
<button class="operation-btn copy-btn" @click="handleCopy(msg)" title="复制">
<copy-outlined />
</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 />
</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 />
</button>
</div>
......@@ -110,17 +139,63 @@
</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">
<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"
:baseConfig="baseConfig"
@text="handleVoiceText"
@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">
<send-outlined />
</button>
......@@ -133,13 +208,16 @@
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import dayjs from 'dayjs';
import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, mergeMarkdownContent } from './utils/markdownTemplate';
import { SendOutlined, ReloadOutlined, CopyOutlined, LikeOutlined, DislikeOutlined } from '@ant-design/icons-vue';
import { message as antdMessage } from 'ant-design-vue';
import defaultAvatar from '@/assets/logo.png';
import rightIcon from '@/assets/right.svg'
import thinkIcon from '@/assets/think.svg'
import { SendOutlined, ReloadOutlined, CopyOutlined, LikeOutlined, DislikeOutlined, PaperClipOutlined, EyeOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { useAttachments } from './hooks/useAttachments';
import { useFeedback } from './hooks/useFeedback';
import { useMessageActions } from './hooks/useMessageActions';
import defaultAvatar from '../../assets/logo.png';
import rightIcon from '../../assets/right.svg'
import thinkIcon from '../../assets/think.svg'
import ChartComponent from './ChartComponent.vue'; // 导入独立的图表组件
import VoiceRecognitionText from './VoiceRecognitionText.vue'; // 导入语音识别组件
import FeedbackModal from './FeedbackModal.vue'; // 导入反馈弹窗组件
import { createContentTemplateService, type Message } from './utils/contentTemplateService'; // 导入模板服务
// 定义组件属性接口
......@@ -149,7 +227,7 @@ interface Props {
// 基础配置对象
baseConfig?: {
apiBaseUrl?: string
token?: string
userToken?: string
appCode?: string
}
logoUrl?: string
......@@ -168,7 +246,7 @@ const props = withDefaults(defineProps<Props>(), {
dialogSessionId: '',
baseConfig: () => ({
apiBaseUrl: '',
token: '',
userToken: '',
appCode: ''
}),
logoUrl: '',
......@@ -198,6 +276,39 @@ const currentAIResponse = ref<Message | null>(null);
const isAIResponding = ref(false);
const dialogSessionId = ref(props.dialogSessionId || '');
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 hasStartedConversation = ref(false); // 添加对话开始状态
......@@ -221,6 +332,8 @@ const handleSSEMessage = (data: SSEData, isSimulated: boolean = false) => {
totalTokens: 0,
date: dayjs().format('HH:mm'),
contentBlocks: [],
upCount: 0,
downCount: 0,
};
messages.value.push(currentAIResponse.value);
};
......@@ -302,6 +415,13 @@ const handleSSEMessage = (data: SSEData, isSimulated: boolean = false) => {
// 创建模板服务实例
const templateService = createContentTemplateService();
// 上传配置
const uploadConfig = {
apiBaseUrl: props.baseConfig?.apiBaseUrl,
userToken: props.baseConfig?.userToken,
appCode: props.baseConfig?.appCode
};
// 语音转文字事件处理函数
const handleVoiceText = (textMessage: string) => {
// 直接使用统一的sendMessage函数发送文本消息
......@@ -322,7 +442,7 @@ const startConversation = () => {
};
// 定义消息类型
type MessageType = 'text' | 'audio';
type MessageType = 'text' | 'audio' | 'attachment';
// 定义消息参数接口
interface MessageParams {
......@@ -341,6 +461,8 @@ const validateMessageParams = (type: MessageType, params: MessageParams): boolea
return !!messageContent;
case 'audio':
return !!audioUrl;
case 'attachment':
return attachments.value.length > 0;
default:
return false;
}
......@@ -348,6 +470,13 @@ const validateMessageParams = (type: MessageType, params: MessageParams): boolea
// 统一发送消息函数
const sendMessage = async (type: MessageType = 'text', params: MessageParams = {}) => {
// 根据是否有附件自动确定消息类型
if (attachments.value.length > 0) {
type = 'attachment' as MessageType;
}
if (isUploading.value) {
return;
}
//如果消息文本为空且是文本类型,则延迟1秒后模拟折线图消息进行测试
// if (type === 'text' && !messageText.value.trim() && !params.message) {
// loading.value = true;
......@@ -395,13 +524,20 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
contentBlocks: [] as any[],
status: 1, // 发送中状态
originalContent: messageContent, // 保存原始内容用于重发
originalMessageType: type // 保存消息类型用于重发
};
switch (type) {
originalMessageType: type === 'attachment' ? 'text' : type, // 重发时使用text类型
attachments: attachments.value.map(a => ({
attachmentName: a.attachmentName || '',
attachmentType: a.attachmentType,
attachmentSize: a.attachmentSize,
attachmentPath: a.attachmentPath || '', // 后台返回的URL
})),
upCount: 0,
downCount: 0,
} as Message;
switch (type) {
case 'text':
messageData.contentBlocks.push({
content: templateService.generateTextTemplate(messageContent),
content: templateService.generateQuestionTemplate(messageContent),
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
......@@ -413,6 +549,30 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
messageText.value = '';
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':
messageData.contentBlocks.push({
audioData: {
......@@ -443,25 +603,48 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
} else {
// 默认的API调用逻辑
console.log(`默认API调用逻辑`, dialogSessionId.value);
const requestData = type === 'audio' ? {
questionLocalAudioFilePath: audioUrl,
audioDuration: durationTime,
...props.params,
dialogSessionId: dialogSessionId.value,
appId: props.params?.appId,
} : {
question: messageContent,
...props.params,
dialogSessionId: dialogSessionId.value,
let attachmentJson: any = null;
let requestData: any = {
...props.params,
dialogSessionId: dialogSessionId.value,
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`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'token': token || '',
'x-session-id': token || '',
'token': userToken || '',
'x-session-id': userToken || '',
'x-app-code': appCode || ''
} as HeadersInit,
body: JSON.stringify(requestData)
......@@ -472,10 +655,14 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
// 处理SSE流式响应
await processSSEStreamResponse(response.body);
console.log(`发送成功`)
attachmentJson = null;
// 发送成功,更新消息状态为已发送
if (messageData) {
messageData.status = 2;
}
// 发送成功后清空附件(在API调用完成后)
clearAttachments();
}else{
loading.value = false;
// 设置当前消息为失败状态
......@@ -486,9 +673,12 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
}
} catch (e) {
loading.value = false;
console.error(`发送失败:`, e);
} finally {
loading.value = false;
// 发送成功后清空附件
clearAttachments();
}
};
......@@ -519,57 +709,6 @@ const handleRecommendationClick = (message: Message, item: any) => {
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流式响应
const processSSEStreamResponse = async (stream: ReadableStream | null) => {
if (!stream) {
......@@ -668,12 +807,12 @@ const getChatRecord = async (dialogSessionId: string) => {
messages.value = [...recordList];
}
} else {
const { token, appCode } = props.baseConfig || {};
const { userToken, appCode } = props.baseConfig || {};
const response = await fetch(`${props.baseConfig?.apiBaseUrl || ''}/aiService/ask/list/chat/${dialogSessionId}`, {
method: 'GET',
headers: {
'token': token || '',
'x-session-id': token || '',
'token': userToken || '',
'x-session-id': userToken || '',
'x-app-code': appCode || ''
} as HeadersInit
});
......@@ -694,12 +833,12 @@ const getAppInfo = async () => {
if(!props.params?.appId) {
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}`, {
method: 'GET',
headers: {
'token': token || '',
'x-session-id': token || '',
'token': userToken || '',
'x-session-id': userToken || '',
'x-app-code': appCode || ''
} as HeadersInit
});
......@@ -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) => {
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 {
debug?: boolean
maxDuration?: number, // 添加最大时长参数
baseConfig?: {
token?: string,
userToken?: string,
appCode?: string,
apiBaseUrl?: string,
}
......@@ -70,7 +70,7 @@ const props = withDefaults(defineProps<Props>(), {
debug: false,
maxDuration: 30, // 默认最大时长为30秒
baseConfig: () => ({
token: '',
userToken: '',
appCode: '',
apiBaseUrl: '',
})
......@@ -412,13 +412,13 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, dura
const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav');
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`, {
method: 'POST',
headers: {
'x-app-code': appCode || '',
'token': token || '',
'x-session-id': token || '',
'token': userToken || '',
'x-session-id': userToken || '',
} as HeadersInit,
body: formData
});
......
......@@ -59,7 +59,7 @@ interface Props {
debug?: boolean
maxDuration?: number, // 添加最大时长参数
baseConfig?: {
token?: string,
userToken?: string,
appCode?: string,
apiBaseUrl?: string,
}
......@@ -70,7 +70,7 @@ const props = withDefaults(defineProps<Props>(), {
debug: false,
maxDuration: 30, // 默认最大时长为30秒
baseConfig: () => ({
token: '',
userToken: '',
appCode: '',
apiBaseUrl: '',
})
......@@ -413,13 +413,13 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<string> => {
const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav');
formData.append('fileFolder', 'AI_TEMP');
const { token, appCode } = props.baseConfig || {};
const { userToken, appCode } = props.baseConfig || {};
const response = await fetch(`/agentService/index/audio2txt`, {
method: 'POST',
headers: {
'x-app-code': appCode || '',
'token': token || '',
'x-session-id': token || '',
'token': userToken || '',
'x-session-id': userToken || '',
} as HeadersInit,
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 {
// 输入容器
.chat-input-container {
padding: 40px 0 30px;
padding: 10px 0 30px;
flex-shrink: 0;
}
}
......@@ -275,7 +275,7 @@ li {
}
}
.message-content {
padding: 10px;
border-radius: 4px;
position: relative;
white-space: pre-wrap;
......@@ -286,6 +286,13 @@ li {
:deep(.message-text) {
font-size: 16px;
}
:deep(.message-question) {
font-size: 16px;
background: #5B8AFE;
color: @white;
padding: 10px;
border-radius: 4px;
}
}
}
......@@ -298,11 +305,6 @@ li {
box-shadow:none;
}
.message.sent .message-content {
background: #5B8AFE;
color: @white;
}
// 图表块样式
.chart-block {
margin: 20px 0px 8px;
......@@ -358,6 +360,46 @@ li {
background: #fff2f0;
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 {
font-style: italic;
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 {
max-width: 100%;
height: auto;
border-radius: 4px;
margin-bottom: 8px;
}
// Markdown图片容器样式
......@@ -1278,8 +1510,8 @@ li {
text-align: left;
}
}
}
}
......@@ -1287,7 +1519,6 @@ li {
.chat-input {
display: flex;
align-items: flex-end;
gap: 8px;
textarea {
flex: 1;
......@@ -1350,6 +1581,9 @@ li {
border-top: 1px solid #e8f2f1; // 保持灰色系
background: #FCFCFC; // 保持灰色系
}
.attachments-preview-container{
padding:10px;
}
}
}
......@@ -1365,7 +1599,6 @@ li {
min-width: 60px;
}
}
.table-title {
font-size: 16px;
font-weight: bold;
......@@ -1421,7 +1654,6 @@ li {
}
}
}
.avatar-container {
display: none;
}
......@@ -1434,6 +1666,9 @@ li {
.chat-input-container{
padding:12px;
}
.attachments-preview-container{
padding:12px;
}
}
}
......
......@@ -11,6 +11,7 @@ const CHART_TYPES = {
// 内容模板类型定义
export interface ContentTemplates {
question: (content: string) => string;
text: (content: string) => string;
thinking: (content: string) => string;
error: (content: string) => string;
......@@ -31,11 +32,17 @@ export interface MessageBlock {
chartType?: number | string;
audioData?: {
src: string;
durationTime: string;
durationTime?: number | string;
};
thinkingTime?: number;
thinkingTimeText?: string;
}
attachmentData?:{
attachmentName?: string;
attachmentPath?: string;
attachmentType?: string;
attachmentSize?: number;
}
}
// 消息类型定义
export interface Message {
......@@ -57,6 +64,9 @@ export interface Message {
// 推荐会话相关属性
recommendations?: any[];
showRecommendations?: boolean;
// 赞踩相关属性
upCount: number; // 点赞数量
downCount: number; // 踩数量
}
// SSE数据类型定义
......@@ -86,7 +96,8 @@ function isLastBlockText(blocks: MessageBlock[]): boolean {
return !!lastBlock.content &&
!lastBlock.audioData &&
!lastBlock.chartData &&
!lastBlock.thinkContent;
!lastBlock.thinkContent &&
!lastBlock.attachmentData;
}
// 获取最后一个普通文本块的索引
......@@ -96,7 +107,8 @@ function getLastTextBlockIndex(blocks: MessageBlock[]): number {
if (!!block.content &&
!block.audioData &&
!block.chartData &&
!block.thinkContent) {
!block.thinkContent &&
!block.attachmentData) {
return i;
}
}
......@@ -134,6 +146,10 @@ export class ContentTemplateService {
// 创建内容模板生成器
private createTemplates(): ContentTemplates {
return {
// 普通文本
question: (content: string) => {
return `<div class="message-question">${content}</div>`;
},
// 普通文本
text: (content: string) => {
return `<div class="message-text">${content}</div>`;
......@@ -231,6 +247,9 @@ export class ContentTemplateService {
public generateTextTemplate(content: string): string {
return this.templates.text(content);
}
public generateQuestionTemplate(content: string): string {
return this.templates.question(content);
}
public generateThinkingTemplate(content: string): string {
return this.templates.thinking(content);
......@@ -567,7 +586,7 @@ export class ContentTemplateService {
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 = {
messageType: 'sent' as const,
......@@ -577,39 +596,57 @@ export class ContentTemplateService {
completionTokens: 0,
totalTokens: 0,
date,
contentBlocks: [] as MessageBlock[]
contentBlocks: [] as MessageBlock[],
// 处理赞踩状态
upCount: data.upCount || 0,
downCount: data.downCount || 0
};
// 使用switch语句处理不同类型的消息
switch (true) {
case !!data.audioPath:
// 音频消息
message.contentBlocks.push({
audioData: {
src: data.audioPath,
durationTime: data.audioTime || '0"'
},
content: '',
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
});
break;
case !!data.question:
// 文本消息
message.contentBlocks.push({
content: this.templates.text(data.question),
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
});
break;
default:
// 其他类型的消息(未来扩展)
break;
// 处理不同类型的消息(允许多种类型共存)
const contentBlocks: MessageBlock[] = [];
// 处理音频消息
if (data.audioPath) {
contentBlocks.push({
audioData: {
src: data.audioPath,
durationTime: data.audioTime || '0"'
},
content: '',
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
});
}
// 处理附件消息
if (data.attachmentPath) {
contentBlocks.push({
attachmentData: {
attachmentName: data.attachmentName || '',
attachmentPath: data.attachmentPath,
attachmentType: data.attachmentType || '',
attachmentSize: data.attachmentSize || 0,
},
content: '',
thinkContent: '',
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);
}
......@@ -625,6 +662,9 @@ export class ContentTemplateService {
totalTokens: 0,
contentBlocks: [],
date,
// 处理赞踩状态
upCount: data.upCount || 0,
downCount: data.downCount || 0
};
let currentThinkingMode = false;
......
......@@ -10,9 +10,11 @@ export interface SSEData {
// SSE服务配置
export interface SSEServiceConfig {
apiBaseUrl: string;
appCode: string;
token: string;
baseConfig:{
apiBaseUrl: string;
userToken: string;
appCode: string;
};
params: {
stage?: string;
appId?: string;
......@@ -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 || ''}`;
this.eventSource = new EventSourcePolyfill(url, {
headers: {
Token: this.config.token || '',
'x-session-id': this.config.token || '',
'x-app-code': this.config.appCode || '',
Token: this.config.baseConfig.userToken || '',
'x-session-id': this.config.baseConfig.userToken || '',
'x-app-code': this.config.baseConfig.appCode || '',
},
withCredentials: true,
connectionTimeout: 60000,
......
......@@ -53,6 +53,11 @@ export default defineConfig(({ mode }) => {
changeOrigin: true,
secure: false,
},
'/cfile': {
target: apiBaseUrl,
changeOrigin: true,
secure: false,
},
'/WeChatOauth2': {
target: apiBaseUrl,
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