1.6.0前端

AI:完善AI问答,增加语音识别记录信息查询功能
master
xs 2 months ago
parent 9ba36e6ed3
commit 725f68c979

@ -0,0 +1,77 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { AsrRecordVO, AsrRecordForm, AsrRecordQuery } from '@/api/ai/asrRecord/types';
/**
* AI
* @param query
* @returns {*}
*/
export const listAsrRecord = (query?: AsrRecordQuery): AxiosPromise<AsrRecordVO[]> => {
return request({
url: '/ai/asrRecord/list',
method: 'get',
params: query
});
};
/**
* AI
* @param asrRecordId
*/
export const getAsrRecord = (asrRecordId: string | number): AxiosPromise<AsrRecordVO> => {
return request({
url: '/ai/asrRecord/' + asrRecordId,
method: 'get'
});
};
/**
* AI
* @param data
*/
export const addAsrRecord = (data: AsrRecordForm) => {
return request({
url: '/ai/asrRecord',
method: 'post',
data: data
});
};
/**
* AI
* @param data
*/
export const updateAsrRecord = (data: AsrRecordForm) => {
return request({
url: '/ai/asrRecord',
method: 'put',
data: data
});
};
/**
* AI
* @param asrRecordId
*/
export const delAsrRecord = (asrRecordId: string | number | Array<string | number>) => {
return request({
url: '/ai/asrRecord/' + asrRecordId,
method: 'delete'
});
};
/**
* AI
* @param query
* @returns {*}
*/
export function getAiAsrRecordList (query) {
return request({
url: '/ai/asrRecord/getAiAsrRecordList',
method: 'get',
params: query
});
};

@ -0,0 +1,106 @@
export interface AsrRecordVO {
/**
* ID
*/
asrRecordId: string | number;
/**
* LLMIDai_model
*/
modelId: string | number;
/**
*
*/
asrFileUrl: string;
/**
*
*/
asrContent: string;
/**
* (10)
*/
asrResult: string;
/**
*
*/
asrFailedReason: string;
}
export interface AsrRecordForm extends BaseEntity {
/**
* ID
*/
asrRecordId?: string | number;
/**
* LLMIDai_model
*/
modelId?: string | number;
/**
*
*/
asrFileUrl?: string;
/**
*
*/
asrContent?: string;
/**
* (10)
*/
asrResult?: string;
/**
*
*/
asrFailedReason?: string;
}
export interface AsrRecordQuery extends PageQuery {
/**
* ID
*/
asrRecordId?: string | number;
/**
* LLMIDai_model
*/
modelId?: string | number;
/**
*
*/
asrFileUrl?: string;
/**
*
*/
asrContent?: string;
/**
* (10)
*/
asrResult?: string;
/**
*
*/
asrFailedReason?: string;
/**
*
*/
params?: any;
}

@ -0,0 +1,395 @@
<template>
<div class="p-2">
<!-- <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">-->
<!-- <div v-show="showSearch" class="mb-[10px]">-->
<!-- <el-card shadow="hover">-->
<!-- <el-form ref="queryFormRef" :model="queryParams" :inline="true" >-->
<!-- <el-form-item label="记录ID主键" prop="asrRecordId">-->
<!-- <el-input v-model="queryParams.asrRecordId" placeholder="请输入记录ID主键" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="LLM对话模型ID关联ai_model" prop="modelId">-->
<!-- <el-input v-model="queryParams.modelId" placeholder="请输入LLM对话模型ID关联ai_model" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="语音识别文件地址" prop="asrFileUrl">-->
<!-- <el-input v-model="queryParams.asrFileUrl" placeholder="请输入语音识别文件地址" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="识别结果(1成功0失败)" prop="asrResult">-->
<!-- <el-input v-model="queryParams.asrResult" placeholder="请输入识别结果(1成功0失败)" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="识别失败原因" prop="asrFailedReason">-->
<!-- <el-input v-model="queryParams.asrFailedReason" placeholder="请输入识别失败原因" clearable @keyup.enter="handleQuery" />-->
<!-- </el-form-item>-->
<!-- <el-form-item>-->
<!-- <el-button type="primary" icon="Search" @click="handleQuery"></el-button>-->
<!-- <el-button icon="Refresh" @click="resetQuery"></el-button>-->
<!-- </el-form-item>-->
<!-- </el-form>-->
<!-- </el-card>-->
<!-- </div>-->
<!-- </transition>-->
<el-card shadow="never">
<el-table v-loading="loading" :data="asrRecordList" @selection-change="handleSelectionChange">
<!-- <el-table-column type="selection" width="55" align="center" />-->
<el-table-column label="记录ID主键" align="center" prop="asrRecordId" v-if="columns[0].visible"/>
<el-table-column label="模型名称" align="center" prop="modelName" v-if="columns[2].visible"/>
<el-table-column label="语音识别文件地址" align="center" prop="asrFileUrl" v-if="columns[3].visible"/>
<el-table-column label="语音识别内容" align="center" prop="asrContent" v-if="columns[4].visible"/>
<el-table-column label="识别结果" align="center" prop="asrResult" v-if="columns[5].visible">
<template #default="scope">
<dict-tag :options='ai_asr_result' :value='scope.row.asrResult'/>
</template>
</el-table-column>
<el-table-column label="识别失败原因" align="center" prop="asrFailedReason" v-if="columns[6].visible"/>
<el-table-column label="音频播放" align="center" width="300" v-if="columns[7].visible">
<template #default="scope">
<div class="audio-player">
<el-button
:type="currentPlayingId === scope.row.asrRecordId ? 'danger' : 'primary'"
:icon="currentPlayingId === scope.row.asrRecordId ? 'VideoPause' : 'VideoPlay'"
size="small"
@click="togglePlay(scope.row)"
:loading="audioLoading"
>
{{ currentPlayingId === scope.row.asrRecordId ? '停止' : '播放' }}
</el-button>
<div v-if="currentPlayingId === scope.row.asrRecordId" class="audio-controls">
<el-slider
v-model="currentTime"
:max="duration"
:format-tooltip="formatTime"
@change="seekTo"
:disabled="!audioElement"
class="audio-slider"
/>
<div class="time-display">
<span>{{ formatTime(currentTime) }}</span>
<span>/</span>
<span>{{ formatTime(duration) }}</span>
</div>
</div>
</div>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</template>
<script setup name="AsrRecord" lang="ts">
import { listAsrRecord, getAsrRecord, delAsrRecord, addAsrRecord, updateAsrRecord } from '@/api/ai/record/asrRecord';
import { AsrRecordVO, AsrRecordQuery, AsrRecordForm } from '@/api/ai/record/asrRecord/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const {ai_asr_result} = toRefs<any>(proxy?.useDict('ai_asr_result'));
const asrRecordList = ref<AsrRecordVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
//
const audioElement = ref<HTMLAudioElement | null>(null);
const currentPlayingId = ref<string | number | null>(null);
const currentTime = ref(0);
const duration = ref(0);
const audioLoading = ref(false);
const queryFormRef = ref<ElFormInstance>();
const asrRecordFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
//
const columns = ref<FieldOption[]>([
{ key: 0, label: `记录ID主键`, visible: false },
{ key: 1, label: `租户ID`, visible: true },
{ key: 2, label: `模型名称`, visible: true },
{ key: 3, label: `语音识别文件地址`, visible: true },
{ key: 4, label: `语音识别内容`, visible: true },
{ key: 5, label: `识别结果`, visible: true },
{ key: 6, label: `识别失败原因`, visible: true },
{ key: 7, label: `音频播放`, visible: true },
{ key: 8, label: `创建部门`, visible: true },
{ key: 9, label: `创建人`, visible: true },
{ key: 10, label: `创建时间`, visible: true },
]);
const initFormData: AsrRecordForm = {
asrRecordId: undefined,
modelId: undefined,
asrFileUrl: undefined,
asrContent: undefined,
asrResult: undefined,
asrFailedReason: undefined,
}
const data = reactive<PageData<AsrRecordForm, AsrRecordQuery>>({
form: {...initFormData},
queryParams: {
pageNum: 1,
pageSize: 10,
asrRecordId: undefined,
modelId: undefined,
asrFileUrl: undefined,
asrContent: undefined,
asrResult: undefined,
asrFailedReason: undefined,
params: {
}
},
rules: {
asrRecordId: [
{ required: true, message: "记录ID主键不能为空", trigger: "blur" }
],
modelId: [
{ required: true, message: "LLM对话模型ID关联ai_model不能为空", trigger: "blur" }
],
asrFileUrl: [
{ required: true, message: "语音识别文件地址不能为空", trigger: "blur" }
],
asrContent: [
{ required: true, message: "语音识别内容不能为空", trigger: "blur" }
],
asrResult: [
{ required: true, message: "识别结果(1成功0失败)不能为空", trigger: "blur" }
],
asrFailedReason: [
{ required: true, message: "识别失败原因不能为空", trigger: "blur" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询AI语音识别记录列表 */
const getList = async () => {
loading.value = true;
const res = await listAsrRecord(queryParams.value);
asrRecordList.value = res.rows;
total.value = res.total;
loading.value = false;
}
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
/** 表单重置 */
const reset = () => {
form.value = {...initFormData};
asrRecordFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: AsrRecordVO[]) => {
ids.value = selection.map(item => item.asrRecordId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = "添加AI语音识别记录";
}
/** 修改按钮操作 */
const handleUpdate = async (row?: AsrRecordVO) => {
reset();
const _asrRecordId = row?.asrRecordId || ids.value[0]
const res = await getAsrRecord(_asrRecordId);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改AI语音识别记录";
}
/** 提交按钮 */
const submitForm = () => {
asrRecordFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.asrRecordId) {
await updateAsrRecord(form.value).finally(() => buttonLoading.value = false);
} else {
await addAsrRecord(form.value).finally(() => buttonLoading.value = false);
}
proxy?.$modal.msgSuccess("操作成功");
dialog.visible = false;
await getList();
}
});
}
/** 删除按钮操作 */
const handleDelete = async (row?: AsrRecordVO) => {
const _asrRecordIds = row?.asrRecordId || ids.value;
await proxy?.$modal.confirm('是否确认删除AI语音识别记录编号为"' + _asrRecordIds + '"的数据项?').finally(() => loading.value = false);
await delAsrRecord(_asrRecordIds);
proxy?.$modal.msgSuccess("删除成功");
await getList();
}
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('ai/asrRecord/export', {
...queryParams.value
}, `asrRecord_${new Date().getTime()}.xlsx`)
}
//
const togglePlay = async (row: AsrRecordVO) => {
if (currentPlayingId.value === row.asrRecordId) {
//
stopAudio();
} else {
//
await playAudio(row);
}
}
const playAudio = async (row: AsrRecordVO) => {
try {
audioLoading.value = true;
//
if (audioElement.value) {
audioElement.value.pause();
audioElement.value = null;
}
//
audioElement.value = new Audio(row.asrFileUrl);
//
audioElement.value.addEventListener('loadedmetadata', () => {
duration.value = audioElement.value?.duration || 0;
});
audioElement.value.addEventListener('timeupdate', () => {
currentTime.value = audioElement.value?.currentTime || 0;
});
audioElement.value.addEventListener('ended', () => {
stopAudio();
});
audioElement.value.addEventListener('error', (e) => {
console.error('音频播放错误:', e);
proxy?.$modal.msgError('音频播放失败');
stopAudio();
});
//
await audioElement.value.play();
currentPlayingId.value = row.asrRecordId;
} catch (error) {
console.error('播放音频失败:', error);
proxy?.$modal.msgError('播放音频失败');
stopAudio();
} finally {
audioLoading.value = false;
}
}
const stopAudio = () => {
if (audioElement.value) {
audioElement.value.pause();
audioElement.value.currentTime = 0;
audioElement.value = null;
}
currentPlayingId.value = null;
currentTime.value = 0;
duration.value = 0;
}
const seekTo = (value: number) => {
if (audioElement.value) {
audioElement.value.currentTime = value;
}
}
const formatTime = (seconds: number) => {
if (!seconds || isNaN(seconds)) return '00:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
onMounted(() => {
getList();
});
onBeforeUnmount(() => {
//
stopAudio();
});
</script>
<style scoped>
.audio-player {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
min-width: 250px;
}
.audio-controls {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
width: 100%;
}
.audio-slider {
width: 100%;
margin: 0 8px;
}
.time-display {
display: flex;
align-items: center;
gap: 2px;
font-size: 12px;
color: #666;
font-family: monospace;
}
.time-display span {
min-width: 35px;
text-align: center;
}
</style>

@ -175,7 +175,7 @@
<div class="message-avatar">
<img
v-if="message.role === 'assistant'"
:src="platformIcon"
:src="message.platformIcon"
:alt="provider"
/>
<img
@ -247,7 +247,8 @@
<!-- 输入区域 -->
<footer class="chat-input">
<div class="input-container">
<div class="suggestion-bar align-to-textarea" v-if="!selectedKnowledgeBaseId || selectedKnowledgeBaseId === ''">
<div class="suggestion-bar align-to-textarea"
v-if="!selectedKnowledgeBaseId || selectedKnowledgeBaseId === ''">
<span
v-for="(sug, i) in suggestions"
:key="i"
@ -650,12 +651,19 @@ const selectChat = async (chatSessionId: string) => {
// res.data
currentChat.value.messages = res.data.map(item => {
const model = aiModelList.value.find(m => m.modelId === item.modelId)
let platformIcon = '';
if (model) {
platformIcon = model.platformIcon;
}
return {
role: item.role,
content: typeof item.content === 'string'
? item.content.replace(/^["']|["']$/g, '')
: item.content,
timestamp: item.timestamp
timestamp: item.timestamp,
platformIcon:platformIcon
//
};
});
@ -814,7 +822,7 @@ function onModelChange() {
const model = aiModelList.value.find(m => m.modelId === selectedModel.value)
if (model) {
provider.value = model.platformCode
platformIcon.value = model.platformIcon;
// platformIcon.value = model.platformIcon;
}
}
@ -886,11 +894,18 @@ const sendMessage = async () => {
const userMsg = inputMessage.value.trim()
const model = aiModelList.value.find(m => m.modelId === selectedModel.value)
if (model) {
platformIcon.value = model.platformIcon;
}
//
const userMessage: ChatMessage = {
role: 'user',
content: userMsg,
timestamp: Date.now()
timestamp: Date.now(),
}
//
@ -945,13 +960,15 @@ const sendMessage = async () => {
currentChat.value.messages.push({
role: 'assistant',
content: streamingContent.value,
timestamp: Date.now()
timestamp: Date.now(),
platformIcon: platformIcon.value,
})
} catch (error) {
currentChat.value.messages.push({
role: 'assistant',
content: '出错: ' + (error as Error).message,
timestamp: Date.now()
timestamp: Date.now(),
platformIcon: platformIcon.value,
})
} finally {
isStreaming.value = false;
@ -967,9 +984,15 @@ const processStream = (raw) => {
chunks.forEach(chunk => {
const content = chunk.substring(5)
if (content && content !== '[DONE]') {
if (content.startsWith(fullText)) {
fullText = content
} else {
fullText += content
}
}
})
fullText = fullText.replaceAll("data:", "");

@ -116,10 +116,8 @@ watch(
() => ({ ...props }),
(newProps) => {
Object.keys(formData).forEach(key => {
console.log("---12323-");
if (newProps[key] !== undefined) {
formData[key] = newProps[key]
console.log(newProps[key]);
}
})
},

@ -677,6 +677,7 @@ const resetForm = () => {
form.value.status = '1';
};
//onMountedgetSourceInfoAI
onMounted(() => {
getTreeSelect(); //
getList(); //
@ -684,12 +685,11 @@ onMounted(() => {
initPassword.value = response.data;
});
getSourceInfo();
});
//AI
const getSourceInfo = () =>{
const from = proxy.$route.query.from;
if(form && from==="ai"){
@ -716,6 +716,7 @@ watch(
//
// handleDataUpdate(newValue)
copyNonEmptyValues(newValue.message,form.value);
//onchange
if(form.value.deptId && form.value.deptId!==''){
handleDeptChange(form.value.deptId)
}

Loading…
Cancel
Save