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()
......
This diff is collapsed.
<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