Commit a4d5ca91 authored by 水玉婷's avatar 水玉婷
Browse files

feat:添加语音输入功能

parent b1f619b0
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCmrZ80Y5XKfRZG
utsLbDeQhBFhpAqvoE9FnN614BsF4lVVNPYR6Oxowa/XYne56AVEleGpFxXVoRw5
j0s3mT8MIpUpK20WKczO3f88Q9WVQFi68pkBVLUigp78fYH8u3ZSv37tG0SMwlle
NlYDQGo66T/a7fybulWNdtmWmr4pz+BO7cLbodCRglVkrg2aRZTIbjVbWWBoyKXO
yRwzgGgzDpbzesgnlrtvAX1cV+oCtt/ixI+qrrkXtpVkKSmqzwrGzDx8h1QP2qU+
ZhfpNqig+ANtNd7a7Gnhsy0+q3pYSY/JHGikJLSSW5hZFgWqXBhSkDG25O2BSwyz
dy2uGFmPAgMBAAECggEAMfMmGtUdNql124xzyGCN5kktzE0Uxr0MBJiWRXr7ni/N
0tMkSwm6j0o8IBfqOVRG/97K2ZmJeZPmmXlP2UGbm09h1AynjFTKg9QTgUPy5d96
t8ur/rIb9lOewZv7MHodY37v0q6xRF2Z2pn9/Mt5Cl6MPFfFtAWLTfGoE3IcOvsN
Br4/XGYKi0zkd3biqLAEJDL1wHL5J+5VqtL8ZmV6qGBQXw0PBI3ZZO8pV5lsIL1Z
qB/2LMeKCv96KFCfpanK3XdBep9KVUdWIxsjJhgi1ETtLdD/8OmBu+YJp78/Xdqv
io7ux2+Bj3gqEqTIdMySRrRg3oFe/zX0/bkHzScJgQKBgQDaO5lXIjg0OGwdCn81
YeS8z9uqjkFlxvqQe8Xkub57Wrvcva4+f2VRVvXM/xOmFhfMIFMqjTQ6OVwol3Nd
pEvDeiy67bozGle++A/RvPEgt1HpWha79CiJ8V4KCc8sonu5GgLZGCRTmwZ3m0ru
aSR9OwQb7y4ttNsk0UoSLmnBhwKBgQDDhf3lRtiTQic4Zlb2rLdvU4OAOYhDsT6h
JQSVGSwrcGyuv3pkg2yQ8/VlZ8M54rIg7m0+/gRCxZ6owvnj7V0AgUFbk5aG/JSV
Atpqcf12pkoBo1xFmcozf5y0E3rrn5D5jGU/lnOXhDAxmP+bJP80O2+ffhCrJOda
OjY2N6RJuQKBgQChbYCqIZftmObwPHmIpVcsC51z9jKN9LgX9FaYMIWkfaOFT5H6
jQYHOworj2ubabBEwIyEZ1sAzrlLFWyzEfsxJ8i6pWscrhnGG3yoKtk62B/xO0Ch
26O5Fh/30PW9EJvwejstF1yXs47/HpI49PGW6PbLKwu/p46LF31xIX/9NQKBgGVK
IdjIFeRbrfPC2KRbn3+1tPcVVukyhi52/eO7sa0jRbpVibNOfkythWAuG+396aez
vLaYY16v/9yPfWM9kSN00oX9dEqjyNlVLA9e1B7GUKp+lYuc+yoonuaO/OvZswIE
YGNLrsA8g7b9+tTFmsvVSqNGbJ4stQmCBJmbw6lJAoGBAL0/LehG2sKvsNDoXT72
2HOYH2jz5w5/20ptEjV4I6XvdRw4tfQDe1casNS1RuE7pNTcSKnN8J0vQ4BImHyR
hX2SAd8gTg6UDVCEpjwRveIhnvR+dbIgoIF8b8EdMIeepzP7Bs5fHcIXS3ikXl9t
eTWeKLyu4ce/Lz7caIlkp6tk
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQDzfHp3ypuCvDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjUxMTI1MDgzNDUyWhcNMjYxMTI1MDgzNDUyWjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCm
rZ80Y5XKfRZGutsLbDeQhBFhpAqvoE9FnN614BsF4lVVNPYR6Oxowa/XYne56AVE
leGpFxXVoRw5j0s3mT8MIpUpK20WKczO3f88Q9WVQFi68pkBVLUigp78fYH8u3ZS
v37tG0SMwlleNlYDQGo66T/a7fybulWNdtmWmr4pz+BO7cLbodCRglVkrg2aRZTI
bjVbWWBoyKXOyRwzgGgzDpbzesgnlrtvAX1cV+oCtt/ixI+qrrkXtpVkKSmqzwrG
zDx8h1QP2qU+ZhfpNqig+ANtNd7a7Gnhsy0+q3pYSY/JHGikJLSSW5hZFgWqXBhS
kDG25O2BSwyzdy2uGFmPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADmQg8xLPZ2R
+sZcSEK6jxxvq6lImC0o+vd664jcZb3wG5YMI7gskpiz7uDluH7Tqt+8DjvA5anr
Yvyt6VGYLsCvFq150j4/nRPajlLWM4y3Ulz9vuVSAqY3HXlA55n8ab19HbmpgR0g
QmWDKI4uJ6gLXpQYvp0UAyMwuD1JDf/SPm1kZOQl1gWO2s8rF7fxZD66a4UbjloB
IwVk7oASGcYk+IN9SS3iF1PKHyaoOMA5BnyZvkWS3h0Q++AgYWXzFTvN/lq4g088
lTvsOwA0mMGIRek0zMkrlVB7exhJpRHEhqS576q2O7H9JsZRwlRxc1G7kWimlT0L
pscjZR5TtQo=
-----END CERTIFICATE-----
...@@ -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 ? props?.detailData?.title || '继续对话' : '新建对333' }}</h2> <h2>{{ props.dialogSessionId ? props?.detailData?.title || '继续对话' : '新建对话' }}</h2>
</div> </div>
</div> </div>
...@@ -69,6 +69,16 @@ ...@@ -69,6 +69,16 @@
<!-- 输入区域 - 始终显示 --> <!-- 输入区域 - 始终显示 -->
<div class="chat-input-container"> <div class="chat-input-container">
<div class="chat-input"> <div class="chat-input">
<!-- 语音识别按钮 -->
<VoiceRecognition
ref="voiceRecognitionRef"
:disabled="loading"
:debug="true"
@audio="handleVoiceAudio"
@error="handleVoiceError"
class="voice-recognition-wrapper"
/>
<textarea ref="textarea" v-model="messageText" placeholder="输入消息..." @keypress="handleKeyPress" <textarea ref="textarea" v-model="messageText" placeholder="输入消息..." @keypress="handleKeyPress"
@input="adjustTextareaHeight" :disabled="loading"></textarea> @input="adjustTextareaHeight" :disabled="loading"></textarea>
<button @click="sendMessage" :disabled="loading"> <button @click="sendMessage" :disabled="loading">
...@@ -88,6 +98,7 @@ import tableTemplate from './tableTemplate'; ...@@ -88,6 +98,7 @@ import tableTemplate from './tableTemplate';
import { SendOutlined, UserOutlined } from '@ant-design/icons-vue'; import { SendOutlined, UserOutlined } from '@ant-design/icons-vue';
import defaultAvatar from '@/assets/logo.png'; import defaultAvatar from '@/assets/logo.png';
import ChartComponent from './ChartComponent.vue'; // 导入独立的图表组件 import ChartComponent from './ChartComponent.vue'; // 导入独立的图表组件
import VoiceRecognition from './VoiceRecognition.vue'; // 导入语音识别组件
// 组件属性 // 组件属性
const props = withDefaults( const props = withDefaults(
...@@ -192,13 +203,28 @@ const contentTemplates = { ...@@ -192,13 +203,28 @@ const contentTemplates = {
onerror="console.error('iframe加载失败:', this.src)" onerror="console.error('iframe加载失败:', this.src)"
></iframe> ></iframe>
</div>`; </div>`;
},
// 音频消息模板
audio: (audioData: any) => {
const { audioUrl, audioBlob } = audioData;
let src = audioUrl;
// 如果提供了Blob对象,创建对象URL
if (audioBlob && !audioUrl) {
src = URL.createObjectURL(audioBlob);
}
return `<div class="audio-message">
<audio controls src="${src}">
您的浏览器不支持音频播放
</audio>
</div>`;
} }
}; };
// 定义消息类型 - 更新接口添加图表相关字段 // 定义消息类型 - 更新接口添加图表相关字段
interface Message { interface Message {
messageType: 'received' | 'sent'; messageType: 'received' | 'sent';
type?: number | string;
avatar: string; avatar: string;
recordId: string; recordId: string;
promptTokens: number; promptTokens: number;
...@@ -216,6 +242,13 @@ interface Message { ...@@ -216,6 +242,13 @@ interface Message {
}[]; }[];
} }
// 检查是否为音频消息的辅助函数
const isAudioMessage = (messageData: any): boolean => {
return messageData.questionType === 'audio' ||
(messageData.question && typeof messageData.question === 'object' &&
(messageData.question.audioUrl || messageData.question.audioData));
};
interface SSEData { interface SSEData {
message: any; message: any;
status: number | string; status: number | string;
...@@ -243,6 +276,86 @@ const isReconnecting = ref(false); ...@@ -243,6 +276,86 @@ const isReconnecting = ref(false);
const timeArr = ref([]); const timeArr = ref([]);
const hasStartedConversation = ref(false); // 添加对话开始状态 const hasStartedConversation = ref(false); // 添加对话开始状态
// 语音事件处理函数
const handleVoiceAudio = (audioBlob: Blob) => {
console.log('收到音频数据:', audioBlob);
// 开始对话
startConversation();
// 添加音频消息到聊天记录
messages.value.push({
messageType: 'sent',
avatar: '',
recordId: '',
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
date: dayjs().format('HH:mm'),
contentBlocks: [
{
content: contentTemplates.audio({ audioBlob }),
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
}
],
});
// 滚动到底部
nextTick(() => {
scrollToBottom();
});
// 发送音频到AI
sendAudioMessage(audioBlob);
};
const handleVoiceError = (error: string) => {
console.error('语音识别错误:', error);
// 可以添加错误提示
};
// 发送音频消息
const sendAudioMessage = async (audioBlob: Blob) => {
loading.value = true;
try {
// 创建FormData来发送音频文件
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.wav');
formData.append('dialogSessionId', dialogSessionId.value);
formData.append('appId', props.params?.appId || '');
formData.append('stage', props.params?.stage || '');
// 调用外部传入的消息发送函数
if (props.onMessageSend) {
console.log('调用外部音频发送函数');
// 这里需要根据实际情况调整,可能需要将音频转换为base64或其他格式
await props.onMessageSend(audioBlob);
} else {
// 默认的API调用逻辑 - 发送音频
console.log('默认音频API调用逻辑');
const response = await post(`${props.apiBaseUrl}/aiService/ask/audio/app/${props.params?.appId}`, formData, {
headers: {
Token: props.token || '',
'x-session-id': props.token || '',
'x-app-code': props.appCode || '',
'Content-Type': 'multipart/form-data',
}
});
const data = response.data;
if (data.code === 0) {
console.log('音频发送成功');
}
}
} catch (e) {
console.error('发送音频消息失败:', e);
} finally {
loading.value = false;
}
};
// 开始对话函数 - 修改为在发送消息时调用 // 开始对话函数 - 修改为在发送消息时调用
const startConversation = () => { const startConversation = () => {
hasStartedConversation.value = true; hasStartedConversation.value = true;
...@@ -264,7 +377,7 @@ const simulateOptionData = () => { ...@@ -264,7 +377,7 @@ const simulateOptionData = () => {
} }
}; };
// 第二个消息:展示一个options(会走iframe逻辑) // 第二个消息:展示一个options(会走iframe逻辑)
const secondOptionData = { const secondOptionData = {
status: 3, status: 3,
type: 'option', type: 'option',
...@@ -314,7 +427,7 @@ const simulateOptionData = () => { ...@@ -314,7 +427,7 @@ const simulateOptionData = () => {
messages.value.push(secondResult.updatedResponse); messages.value.push(secondResult.updatedResponse);
nextTick(() => { nextTick(() => {
scrollToBottom(); scrollToBottom();
}); });
} }
}; };
...@@ -765,81 +878,92 @@ onBeforeUnmount(() => { ...@@ -765,81 +878,92 @@ onBeforeUnmount(() => {
}); });
// 处理历史记录数据 // 处理历史记录数据
const processHistoryData = (dataArray: any[]) => { const processHistoryData = (data: any): Message[] => {
const result: Message[] = []; const result: Message[] = [];
dataArray.forEach((data) => { const date = dayjs(data.createTime).format('HH:mm');
let date = dayjs(data.startTime).format('YYYY-MM-DD HH:mm:ss');
if (data.question) { // 处理问题消息
result.push({ if (data.question) {
messageType: 'sent', let questionContent = '';
avatar: '',
recordId: '', // 检查是否为音频消息
promptTokens: 0, if (isAudioMessage(data)) {
completionTokens: 0, // 处理音频消息
totalTokens: 0, const audioData = data.question;
date, questionContent = contentTemplates.audio({
contentBlocks: [ audioUrl: audioData.audioUrl,
{ audioBlob: audioData.audioBlob
content: contentTemplates.text(data.question),
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
},
],
}); });
} else {
// 处理文本消息
questionContent = contentTemplates.text(
typeof data.question === 'string' ? data.question : data.question.content || ''
);
} }
if (data.answerInfoList && Array.isArray(data.answerInfoList)) { result.push({
const aiMessage: Message = { messageType: 'sent',
messageType: 'received', avatar: '',
avatar: 'AI', recordId: '',
recordId: '', promptTokens: 0,
promptTokens: 0, completionTokens: 0,
completionTokens: 0, totalTokens: 0,
totalTokens: 0, date,
contentBlocks: [], contentBlocks: [
date, {
}; content: questionContent,
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
},
],
});
}
let currentThinkingMode = false; // 处理AI回答消息
let currentBlockIdx = -1; if (data.answerInfoList && Array.isArray(data.answerInfoList)) {
const aiMessage: Message = {
// 历史数据处理,isHistoryData设为true,思考框折叠 messageType: 'received',
data.answerInfoList.forEach((answer) => { avatar: 'AI',
const sseData: SSEData = { recordId: '',
message: answer.message || '', promptTokens: 0,
status: answer.status || 0, completionTokens: 0,
type: answer.type || 0, totalTokens: 0,
}; contentBlocks: [],
date,
const processResult = processSSEMessage( };
sseData,
aiMessage,
currentThinkingMode,
currentBlockIdx,
true,
);
currentThinkingMode = processResult.updatedIsThinking; let currentThinkingMode = false;
currentBlockIdx = processResult.updatedBlockIndex; let currentBlockIdx = -1;
aiMessage.recordId = processResult.recordId;
aiMessage.promptTokens = processResult.promptTokens;
aiMessage.completionTokens = processResult.completionTokens;
aiMessage.totalTokens = processResult.totalTokens;
});
// 确保历史记录中的思考框默认折叠 // 历史数据处理,isHistoryData设为true,思考框折叠
aiMessage.contentBlocks.forEach((block) => { data.answerInfoList.forEach((answer) => {
if (block.hasThinkBox) { const sseData: SSEData = {
block.thinkBoxExpanded = false; message: answer.message || '',
} status: answer.status || 0,
}); type: answer.type || '',
};
if (aiMessage.contentBlocks.length > 0) { const processResult = processSSEMessage(
result.push(aiMessage); sseData,
} aiMessage,
currentThinkingMode,
currentBlockIdx,
true,
);
currentThinkingMode = processResult.updatedIsThinking;
currentBlockIdx = processResult.updatedBlockIndex;
aiMessage.recordId = processResult.recordId;
aiMessage.promptTokens = processResult.promptTokens;
aiMessage.completionTokens = processResult.completionTokens;
aiMessage.totalTokens = processResult.totalTokens;
});
if (aiMessage.contentBlocks.length > 0) {
result.push(aiMessage);
} }
}); }
return result; return result;
}; };
......
<template>
<div class="voice-recognition">
<!-- 语音按钮 -->
<button
class="voice-btn"
:class="{ 'recording': isRecording, 'disabled': disabled }"
@click="toggleRecording"
:disabled="disabled"
:title="isRecording ? '停止录音' : '开始录音'"
>
<!-- 语音图标始终显示 -->
<span class="voice-icon">
<AudioOutlined />
</span>
<!-- 录音时显示指示器 -->
<span v-if="isRecording" class="recording-indicator">
<span class="pulse"></span>
<span class="pulse"></span>
<span class="pulse"></span>
</span>
</button>
<!-- 语音识别状态提示 -->
<div v-if="showStatus" class="voice-status" :class="statusClass">
{{ statusText }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { AudioOutlined } from '@ant-design/icons-vue'
// 组件属性
interface Props {
disabled?: boolean
debug?: boolean
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
debug: false
})
// 组件事件
const emit = defineEmits<{
audio: [audioBlob: Blob]
error: [error: string]
}>()
// 响应式数据
const isRecording = ref(false)
const showStatus = ref(false)
const statusText = ref('')
// MediaRecorder相关
const mediaRecorder = ref<MediaRecorder | null>(null)
const audioChunks = ref<Blob[]>([])
const audioStream = ref<MediaStream | null>(null)
// 计算属性
const statusClass = computed(() => {
return isRecording.value ? 'recording' : 'idle'
})
// 检查浏览器是否支持MediaRecorder
const isMediaRecorderSupported = () => {
const supported = 'MediaRecorder' in window;
if (props.debug) {
console.log('MediaRecorder支持检查:', supported);
}
return supported;
}
// 显示状态消息
const showStatusMessage = (message: string) => {
statusText.value = message
showStatus.value = true
setTimeout(() => {
showStatus.value = false
}, 3000)
}
// 检查麦克风权限
const checkMicrophonePermission = async () => {
try {
const permissionStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName });
if (props.debug) {
console.log('麦克风权限状态:', permissionStatus.state);
}
if (permissionStatus.state === 'denied') {
showStatusMessage('麦克风权限被拒绝,请在浏览器设置中允许访问');
return false;
}
return true;
} catch (error) {
if (props.debug) {
console.warn('无法检查麦克风权限:', error);
}
return true; // 如果无法检查权限,继续尝试
}
}
// 显示权限引导提示
const showPermissionGuide = () => {
const guideMessage = `
麦克风权限被拒绝,请按以下步骤操作:
1. 点击浏览器地址栏左侧的"锁形图标"或"不安全"标识
2. 选择"网站设置"
3. 找到"麦克风"权限,选择"允许"
4. 刷新页面后重试
`;
showStatusMessage('麦克风权限被拒绝,请检查浏览器设置');
// 在调试模式下显示详细引导
if (props.debug) {
console.warn('麦克风权限被拒绝,用户需要手动授权');
console.log(guideMessage);
}
}
// 开始录音
const startRecording = async () => {
if (props.disabled) return
// 检查权限
const hasPermission = await checkMicrophonePermission();
if (!hasPermission) {
showPermissionGuide();
return;
}
// 检查浏览器支持
if (!isMediaRecorderSupported()) {
const errorMsg = '您的浏览器不支持音频录制功能';
showStatusMessage(errorMsg);
emit('error', errorMsg);
return;
}
try {
// 获取麦克风权限
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
});
audioStream.value = stream;
audioChunks.value = [];
// 创建MediaRecorder实例
mediaRecorder.value = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
});
// 设置数据可用事件处理
mediaRecorder.value.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.value.push(event.data);
}
};
// 设置停止事件处理 - 录制完成后直接发送
mediaRecorder.value.onstop = () => {
sendRecordedAudio();
};
// 开始录制
mediaRecorder.value.start(100); // 每100ms收集一次数据
isRecording.value = true;
showStatusMessage('正在录音...');
if (props.debug) {
console.log('开始录音,MediaRecorder状态:', mediaRecorder.value.state);
}
} catch (error) {
console.error('启动录音失败:', error);
let errorMessage = '无法启动录音';
if (error && error.name === 'NotAllowedError') {
errorMessage = '麦克风权限被拒绝';
showPermissionGuide();
} else if (error && error.name === 'NotFoundError') {
errorMessage = '未找到麦克风设备';
}
showStatusMessage(errorMessage);
emit('error', errorMessage);
}
}
// 停止录音
const stopRecording = () => {
if (mediaRecorder.value && mediaRecorder.value.state === 'recording') {
mediaRecorder.value.stop();
isRecording.value = false;
// 停止所有音频轨道
if (audioStream.value) {
audioStream.value.getTracks().forEach(track => track.stop());
audioStream.value = null;
}
if (props.debug) {
console.log('停止录音,MediaRecorder状态:', mediaRecorder.value.state);
}
}
}
// 发送录制的音频
const sendRecordedAudio = () => {
if (audioChunks.value.length === 0) {
showStatusMessage('录音数据为空');
return;
}
const audioBlob = new Blob(audioChunks.value, { type: 'audio/webm;codecs=opus' });
// 发送音频数据
emit('audio', audioBlob);
showStatusMessage('音频已发送');
// 清理录音数据
audioChunks.value = [];
if (props.debug) {
console.log('音频发送完成,大小:', Math.round(audioBlob.size / 1024), 'KB');
}
}
// 切换录音状态
const toggleRecording = () => {
if (isRecording.value) {
stopRecording();
} else {
startRecording();
}
}
// 组件卸载时清理资源
onUnmounted(() => {
if (isRecording.value) {
stopRecording();
}
if (audioStream.value) {
audioStream.value.getTracks().forEach(track => track.stop());
}
})
// 暴露方法给父组件
defineExpose({
startRecording,
stopRecording,
isRecording: () => isRecording.value
})
</script>
<style scoped lang="less">
@import './style.less';
.voice-recognition {
position: relative;
display: inline-block;
}
.voice-btn {
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: @primary-color;
color: @white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
position: relative;
z-index: 100; /* 大幅提高按钮的z-index,确保始终在最上层 */
&:hover:not(.disabled) {
background: @primary-hover;
transform: scale(1.05);
}
&:active:not(.disabled) {
transform: scale(0.95);
}
&.recording {
background: @error-color;
animation: pulse 1.5s infinite;
}
&.disabled {
background: @gray-4;
cursor: not-allowed;
opacity: 0.6;
}
}
.voice-icon {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.recording-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
gap: 2px;
.pulse {
width: 4px;
height: 4px;
background: @white;
border-radius: 50%;
animation: pulse 1.5s infinite;
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
.voice-status {
position: absolute;
top: -45px; /* 进一步增加距离,确保完全不会遮挡按钮 */
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: @white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 1; /* 保持较低的z-index */
pointer-events: none; /* 禁止状态提示框接收点击事件 */
&.recording {
background: rgba(255, 0, 0, 0.8);
}
}
@keyframes pulse {
0% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.8);
}
100% {
opacity: 1;
transform: scale(1);
}
}
</style>
\ No newline at end of file
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
@gray-6: #666666; @gray-6: #666666;
@gray-7: #333333; @gray-7: #333333;
@success-color: #52c41a; @success-color: #52c41a;
@success-hover: #46a51a; // 添加缺失的变量定义
@error-color: #f5222d; @error-color: #f5222d;
@warning-color: #faad14; @warning-color: #faad14;
...@@ -333,9 +334,18 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -333,9 +334,18 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
align-items: flex-end; align-items: flex-end;
position: relative; position: relative;
// 语音识别按钮容器 - 移动到右边
.voice-recognition-wrapper {
position: absolute;
right: 40px; // 放在发送按钮左边
top: 50%;
transform: translateY(-50%);
z-index: 10;
}
textarea { textarea {
flex: 1; flex: 1;
padding: 14px 70px 14px 18px; padding: 14px 110px 14px 16px; // 调整内边距:右侧为两个按钮留空间,左侧恢复正常
border-radius: 12px; border-radius: 12px;
outline: none; outline: none;
resize: none; resize: none;
...@@ -378,13 +388,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -378,13 +388,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
transition: color 0.2s, border-color 0.2s, background-color 0.2s; transition: color 0.2s, border-color 0.2s, background-color 0.2s;
z-index: 10; z-index: 10;
transform: translateY(-50%); transform: translateY(-50%);
&:hover {
color: @primary-hover;
border-color: @primary-hover;
background-color: rgba(91, 138, 254, 0.05);
}
&:active { &:active {
background-color: rgba(91, 138, 254, 0.1); background-color: rgba(91, 138, 254, 0.1);
transform: translateY(-50%) scale(0.95); transform: translateY(-50%) scale(0.95);
...@@ -842,4 +846,93 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -842,4 +846,93 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
} }
} }
} }
}
// 音频消息样式
.message-audio {
background: linear-gradient(135deg, #f0f9ff, #e6f7ff);
border: 1px solid #91d5ff;
border-radius: 12px;
padding: 12px;
margin: 8px 0;
.audio-indicator {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.audio-icon {
font-size: 16px;
}
.audio-text {
font-size: 14px;
font-weight: 500;
color: #1890ff;
}
}
.audio-transcript {
font-size: 14px;
line-height: 1.4;
color: #595959;
background: rgba(255, 255, 255, 0.7);
padding: 8px;
border-radius: 6px;
border-left: 3px solid #1890ff;
}
}
// 语音识别组件样式调整
.chat-input {
display: flex;
align-items: flex-end;
gap: 8px;
textarea {
flex: 1;
padding: 14px 110px 14px 16px; // 调整内边距:右侧为两个按钮留空间,左侧恢复正常
border-radius: 12px;
outline: none;
resize: none;
height: 52px;
font-size: 15px;
transition: border-color 0.3s, box-shadow 0.3s;
background-color: @blue-light-2;
border: 1px solid @gray-3;
overflow: hidden;
position: relative;
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12);
&:focus {
border-color: @primary-color;
box-shadow: 0 0 0 2px rgba(91, 138, 254, 0.1);
}
&:disabled {
background-color: @gray-2;
border-color: @gray-3;
color: @gray-5;
cursor: not-allowed;
}
}
// 语音识别按钮样式
.voice-recognition {
margin: 0;
}
}
.operation-box {
margin-top: 6px;
p {
color: @gray-5;
font-size: 12px;
span {
margin-right: 15px;
}
}
} }
\ No newline at end of file
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'; import { resolve } from 'path';
import fs from 'fs';
export default defineConfig({ export default defineConfig({
base: '/ai/', // 添加基础路径前缀 base: '/ai/', // 添加基础路径前缀
plugins: [vue()], plugins: [vue()],
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
port: 3000, port: 3000,
https: {
key: fs.readFileSync('./localhost-key.pem'),
cert: fs.readFileSync('./localhost.pem')
},
// 添加history fallback配置 // 添加history fallback配置
historyApiFallback: { historyApiFallback: {
rewrites: [ rewrites: [
...@@ -40,24 +44,6 @@ export default defineConfig({ ...@@ -40,24 +44,6 @@ export default defineConfig({
console.log('发送请求到:', options.target); console.log('发送请求到:', options.target);
}); });
} }
},
// 修复pedService代理配置
'/pedService': {
target: 'http://10.17.86.37:8630',
changeOrigin: true, // 解决跨域问题
secure: false, // 允许不安全的SSL连接
rewrite: (path) => path.replace(/^\/pedService/, '/pedService'), // 保留前缀
configure: (proxy, options) => {
proxy.on('error', (err, req, res) => {
console.log('pedService代理错误:', err);
});
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('pedService发送请求到本地服务:', options.target + req.url);
});
proxy.on('proxyRes', (proxyRes, req, res) => {
console.log('pedService收到响应,状态码:', proxyRes.statusCode);
});
}
} }
}, },
}, },
......
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