Compare commits

..

4 Commits

Author SHA1 Message Date
huangjinysf 483d92cee7 合并 branch 'main' of http://111.79.108.213:3000/AI/AI-app
# Conflicts:
#	pages/index/index.vue
#	unpackage/dist/dev/app-plus/app-service.js
4 months ago
huangjinysf fcf352d25a minor fix 4 months ago
xushilin cc29d195ce 修改 4 months ago
xushilin 89f81667aa 修改 4 months ago

12
.gitignore vendored

@ -0,0 +1,12 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
unpackage/
.idea
.hbuilderx

@ -2,7 +2,10 @@ import request from '@/request';
// 获取汇总
export const getEnamellingSummaryData = () => request({
url: '/mes/mesEnamellingWorkorder/getEnamellingSummaryData',
export const getAIResponse = (data) => request({
method : 'POST',
url: '/chat',
data
});

@ -5,11 +5,7 @@ if (process.env.NODE_ENV === 'production') {
baseUrl = 'http://106.227.91.181:9081/api';
} else {
// 非生产环境代码
// baseUrl = 'http://192.168.9.237:8182';
// baseUrl = 'http://192.168.50.200:8080';
// baseUrl = 'http://huaerda-api.24yt.com';
// baseUrl = 'http://192.168.20.80:8080';
baseUrl = 'http://106.227.91.181:9081/api';
baseUrl = 'http://192.168.1.18:9020/api';
}
const config = {
baseUrl

@ -1,4 +1,4 @@
<template>
<template>
<view class="ai-page">
<uni-nav-bar left-icon="left" @clickLeft="openDrawer" title="AI对话">
<template v-slot:left>
@ -10,14 +10,12 @@
</template>
<template v-slot:right>
<view class="nav-right">
<!-- <view class="gear" @tap="onSettingTap"></view> -->
<image src="../../static/set.png" mode="widthFix" @tap="onSettingTap" style="width: 18px;"></image>
</view>
</template>
</uni-nav-bar>
<!-- scrollable content -->
<scroll-view class="content" :scroll-y="true" show-scrollbar="false"
scroll-with-animation ref="scrollView">
<scroll-view class="content" :scroll-y="true" show-scrollbar="false" scroll-with-animation ref="scrollView">
<!-- greeting card -->
<view class="greet-card">
<image src="../../static/ai.webp" mode="widthFix" style="width: 60px;margin-right: 10px;"></image>
@ -36,16 +34,8 @@
<view class="guess-panel">
<view class="guess-title">猜你想问</view>
<view class="guess-list">
<view class="guess-item" @tap="onSuggestionTap('今日出入库数据')">
<text>今日出入库数据</text>
<text class="arrow"></text>
</view>
<view class="guess-item" @tap="onSuggestionTap('今日销售数据')">
<text>今日销售数据</text>
<text class="arrow"></text>
</view>
<view class="guess-item" @tap="onSuggestionTap('今日生产数据')">
<text>今日生产数据</text>
<view class="guess-item" @tap="onSuggestionTap(item.label)" v-for="item in guessData" :key="item.id">
<text>{{item.label}}</text>
<text class="arrow"></text>
</view>
</view>
@ -56,7 +46,7 @@
<view v-for="m in messages" :key="m.id" :id="'msg-' + m.id" :class="['msg', m.role]">
<view v-if="m.role === 'user'" class="bubble user-bubble">
<text v-if="m.inputType === 'text'">{{ m.content }}</text>
<view class="text-voice" v-else @tap="playVoice(m.inputContent,m.id)">
<view class="text-voice" v-if="m.inputType === 'voice'" @tap="playVoice(m.inputContent,m.id)">
<text>{{m.duration }}</text>
<image class="voice-play" src="../../static/voice-play.png" mode="widthFix"></image>
</view>
@ -74,7 +64,6 @@
</view>
<view v-else>
<text>{{ m.displayText !== undefined ? m.displayText : m.content }}</text>
<!-- <text class="listen-btn" @tap="onListen(m.content)">🔊</text> -->
</view>
</view>
</view>
@ -87,10 +76,7 @@
<view class="dock">
<scroll-view class="quick-actions horizontal" scroll-x show-scrollbar="false">
<view class="qa-btn minor" @tap="onSwitchModel"></view>
<view class="qa-btn" @tap="onQuickAsk('你是谁?')"></view>
<view class="qa-btn" @tap="onQuickAsk('今日任务有哪些?')"></view>
<view class="qa-btn" @tap="onQuickAsk('展示一份报表示例')"></view>
<view class="qa-btn" @tap="onQuickAsk('生成日报模版')"></view>
<view class="qa-btn" @tap="onQuickAsk(item.quickAskText)" v-for="item in quickAskList" :key="item.id">{{item.label}}</view>
</scroll-view>
<view class="input-bar">
<input class="input" confirm-type="send" :value="inputText" @input="onInput" @confirm="onSend()"
@ -102,13 +88,15 @@
</view>
<!-- left drawer -->
<uni-popup ref="popup" background-color="#fff" type="left" :z-index="10090" @change="onPopupChange" style="z-index: 99999;width: 100vw" >
<uni-popup ref="popup" background-color="#fff" type="left" :z-index="10090" @change="onPopupChange"
style="z-index: 99999;width: 100vw">
<view class="drawer-mask">
<view class="drawer">
<scroll-view class="drawer-scroll" scroll-y show-scrollbar="false">
<view v-for="g in historyGroups" :key="g.date" class="drawer-group">
<view class="drawer-date">{{ g.date }}</view>
<view v-for="(t, idx) in g.items" :key="idx" class="drawer-item" @tap="onHistoryItemTap(t)" @longpress="onLongPressHistory(t)">
<view v-for="(t, idx) in g.items" :key="idx" class="drawer-item" @tap="onHistoryItemTap(t)"
@longpress="onLongPressHistory(t)">
{{ t }}
</view>
<view class="drawer-divider" />
@ -138,43 +126,63 @@
</view>
</template>
<script>
<script>
const HISTORY_KEY = 'chat_history_groups'
import {getAIResponse} from '@/api/index.js'
export default {
data() {
return {
inputText: '',
messages: [
// {
// id: 1,
// role: 'user',
// type: 'text',
// content: '',
// inputType: 'text'
// },
// {
// id: 2,
// role: 'assistant',
// type: 'card',
// title: '',
// content: '........................'
// }
],
messages: [],
scrollInto: '',
drawerOpen: false,
historyGroups: [
],
historyGroups: [],
isRecording: false,
isLoading:false,
isLoading: false,
willCancel: false,
recorder: null,
recordStartY: 0,
recordStartTs: 0,
recordSimTimer: null,
// show: false,
innerAudioContext: null,
popupVisible: false,
typewriterTimers: {},
guessData : [
{
id : 1,
label : '今日出入库数据'
},
{
id : 2,
label : '今日销售数据'
},
{
id : 3,
label : '今日生产数据'
}
],
quickAskList : [
{
id : 1,
label : '自我介绍',
quickAskText : '你是谁?'
},
{
id : 2,
label : '快捷提问',
quickAskText : '今日任务有哪些?'
},
{
id : 3,
label : '快捷提问',
quickAskText : '展示一份报表示例'
},
{
id : 4,
label : '快捷提问',
quickAskText : '生成日报模版'
}
]
}
},
computed: {
@ -187,7 +195,7 @@
}
},
mounted() {
this.loadChatHistory()
this.loadChatHistory();
this.scrollToBottom();
},
beforeDestroy() {
@ -202,73 +210,6 @@
}
},
methods: {
//
async recognizeAudio(tempFilePath) {
try {
console.log('开始语音识别,文件路径:', tempFilePath)
//
const fileInfo = await new Promise((resolve, reject) => {
uni.getFileInfo({
filePath: tempFilePath,
success: resolve,
fail: reject
})
})
console.log('文件大小:', fileInfo.size)
// 使 UniApp API
const uploadRes = await new Promise((resolve, reject) => {
uni.uploadFile({
// url: 'http://192.168.133.83:8000/recognize_speech',
url: 'http://192.168.10.44:8000/recognize_speech',
filePath: tempFilePath,
name: 'speech', // UploadFile
formData: {
'format': 'amr',
'rate': 16000,
'channel': 1,
'cuid': 'uniapp_user',
'audio_len': fileInfo.size
},
success: (res) => {
console.log('上传响应:', res)
if (res.statusCode === 200) {
try {
// JSON
const data = JSON.parse(res.data)
resolve({ statusCode: 200, data })
} catch (e) {
reject(new Error('响应解析失败: ' + e.message))
}
} else {
reject(new Error(`上传失败: ${res.statusCode}`))
}
},
fail: (err) => {
reject(new Error('上传请求失败: ' + err.errMsg))
}
})
})
console.log('语音识别响应:', uploadRes)
const result = uploadRes.data
if (result.status === 'success') {
return result.result
} else {
throw new Error(result.error || '识别失败')
}
} catch (error) {
console.error('语音识别错误:', error)
uni.showToast({
title: '识别失败: ' + (error.message || '网络错误'),
icon: 'none'
})
return null
}
},
// ==================== ====================
formatDate(date) {
const y = date.getFullYear()
@ -294,7 +235,10 @@
let todayGroup = groups.find(g => g.date === today)
if (!todayGroup) {
todayGroup = { date: today, items: [] }
todayGroup = {
date: today,
items: []
}
groups.unshift(todayGroup)
}
@ -307,7 +251,10 @@
if (groups.length > 30) groups = groups.slice(0, 30)
this.historyGroups = groups
uni.setStorageSync(HISTORY_KEY, { groups, updatedAt: Date.now() })
uni.setStorageSync(HISTORY_KEY, {
groups,
updatedAt: Date.now()
})
},
removeFromHistory(text) {
@ -317,7 +264,10 @@
})
groups = groups.filter(g => g.items.length > 0)
this.historyGroups = groups
uni.setStorageSync(HISTORY_KEY, { groups, updatedAt: Date.now() })
uni.setStorageSync(HISTORY_KEY, {
groups,
updatedAt: Date.now()
})
},
clearAllHistory() {
@ -328,7 +278,10 @@
if (res.confirm) {
uni.removeStorageSync(HISTORY_KEY)
this.historyGroups = []
uni.showToast({ title: '已清除', icon: 'success' })
uni.showToast({
title: '已清除',
icon: 'success'
})
}
}
})
@ -350,66 +303,36 @@
if (idx > -1) this.messages.splice(idx, 1)
},
addAssistantMessage(id, content) {
this.messages.push({ id, role: 'assistant', type: 'text', content, displayText: '' })
},
async getAIResponse(message){
try {
// const url = 'http://192.168.133.83:9020/api/chat'
const url = 'http://192.168.10.44:9020/api/chat'
// const url = 'http://106.227.91.181:9020/api/chat' // 线
const headers = { 'Content-Type': 'application/json' }
const data = { message }
// console.log(data)
// const [error, res] = await uni.request({
// url,
// method: 'POST',
// header: headers,
// data
// })
// console.log(res)
// 使 Promise
const res = await new Promise((resolve, reject) => {
uni.request({
url,
method: 'POST',
header: headers,
data,
success: (res) => resolve(res),
fail: (err) => reject(err)
})
this.messages.push({
id,
role: 'assistant',
type: 'text',
content,
displayText: ''
})
console.log('请求响应:', res)
if (res.statusCode !== 200) {
throw new Error(`HTTP错误! 状态码: ${res.statusCode}`)
}
return res.data?.result?.data || '未获取到有效回复'
} catch (error) {
console.error('AI请求错误:', error)
return `抱歉,出了点问题: ${error.errMsg || error.message}`
}
},
playVoice(voicePath) {
if (!voicePath) {
uni.showToast({ title: '无可播放的语音', icon: 'none' })
uni.showToast({
title: '无可播放的语音',
icon: 'none'
})
return
}
if (!this.innerAudioContext) {
this.innerAudioContext = uni.createInnerAudioContext()
this.innerAudioContext.autoplay = false
this.innerAudioContext.onError(() => {
uni.showToast({ title: '播放失败', icon: 'none' })
uni.showToast({
title: '播放失败',
icon: 'none'
})
});
}
try { this.innerAudioContext.stop() } catch(e) {}
try {
this.innerAudioContext.stop()
} catch (e) {}
console.log(voicePath)
this.innerAudioContext.src = voicePath
this.innerAudioContext.play()
@ -439,8 +362,7 @@
openDrawer() {
this.$refs.popup.open()
},
onPopupChange(e){
// e.show: true when opened, false when closed
onPopupChange(e) {
this.popupVisible = !!(e && (e.show === true))
},
// ===== Voice input (WeChat-like) =====
@ -453,9 +375,8 @@
}
if (this.recorder) {
this.recorder.onStart()
this.recorder.onStop( async(res) => {
this.recorder.onStop((res) => {
const duration = Date.now() - this.recordStartTs;
const tempFilePath = res.tempFilePath; // res
if (this.willCancel || duration < 700) {
uni.showToast({
title: duration < 700 ? '说话时间太短' : '已取消',
@ -463,40 +384,24 @@
})
return
}
//
uni.showLoading({ title: '识别中...' });
// TODO: res.tempFilePath mock
// this.handleRecognizedText(this.mockSpeechToText(duration), res.tempFilePath, Math.ceil(
// duration / 100) / 10)
//
const recognizedText = await this.recognizeAudio(tempFilePath);
uni.hideLoading();
if (recognizedText) {
//
this.inputText = recognizedText;
this.$nextTick(() => {
//
// this.onSend('voice', tempFilePath, Math.ceil(duration / 100) / 10);
});
}
this.handleRecognizedText(this.mockSpeechToText(duration), res.tempFilePath, Math.ceil(
duration / 100) / 10)
})
}
},
onPressMic(e) {
if(this.isLoading) return
this.ensureRecorder()
this.isRecording = true
// this.show = true
this.willCancel = false
this.recordStartTs = Date.now()
this.recordStartY = (e.changedTouches && e.changedTouches[0]) ? e.changedTouches[0].clientY : 0
if (this.recorder) {
try {
this.recorder.start({
format: 'amr',
format: 'mp3',
sampleRate: 16000,
encodeBitRate: 16000, //
frameSize: 4, //
numberOfChannels: 1,
duration: 60000
})
@ -512,9 +417,7 @@
this.willCancel = (this.recordStartY - y) > 60
},
onReleaseMic() {
console.log('onReleaseMic');
if (!this.isRecording) return
const cancel = this.willCancel
this.isRecording = false;
this.show = false
if (this.recorder) {
@ -545,6 +448,8 @@
this.$refs.popup.close()
},
async onSend(inputType = 'text', inputContent = '', duration = undefined) {
console.log('inputType',inputType);
const text = (this.inputText || '').trim()
if (!text || this.isLoading) return
@ -556,7 +461,7 @@
role: 'user',
type: 'text',
content: text,
inputType,
inputType : typeof inputType === 'string' ? inputType : 'text',
inputContent,
duration
})
@ -568,17 +473,13 @@
role: 'assistant',
loading: true
})
this.scrollToBottom()
this.inputText = ''
this.isLoading = true
this.addToHistory(text)
try {
// 3. AI
const reply = await this.getAIResponse(text)
const reply = await getAIResponse({message : text})
// 4. loading
const loadingIdx = this.messages.findIndex(m => m.id === loadingId)
if (loadingIdx > -1) this.messages.splice(loadingIdx, 1)
@ -592,7 +493,6 @@
content: reply,
displayText: ''
})
this.typewriter(replyId, reply)
} catch (e) {
//
@ -604,6 +504,7 @@
content: `请求出错:${e.message || e}`
})
} finally {
console.log('finally');
this.isLoading = false
this.$nextTick(() => this.scrollToBottom())
}
@ -623,10 +524,7 @@
if (index < fullText.length) {
msg.displayText = fullText.substring(0, index + 1)
index++
//
this.$nextTick(() => {
this.scrollToBottom()
})
} else {
clearInterval(timer)
delete this.typewriterTimers[messageId]
@ -637,16 +535,19 @@
this.typewriterTimers[messageId] = timer
},
scrollToBottom() {
let self = this;
this.$nextTick(() => {
uni.createSelectorQuery().select('.content').boundingClientRect((rect) => {
if(self.height !== rect.height){
self.height = rect.height;
uni.pageScrollTo({
scrollTop: rect.height,
duration: 300,
class: '.content'
});
}
}).exec();
})
},
mockReply(text) {
const candidates = [
@ -678,20 +579,19 @@
}
}
}
</script>
</script>
<style scoped>
::v-deep .uni-nav-bar-text{
<style scoped>
::v-deep .uni-nav-bar-text {
font-size: 18px !important;
}
::v-deeo .uni-navbar--border{
::v-deeo .uni-navbar--border {
/* border-bottom: 0px !important; */
border-bottom: 1px solid #fff !important;
}
.ai-page {
height: 100vh;
display: flex;
flex-direction: column;
background: #f7f8fc;
@ -879,7 +779,7 @@
height: 8px;
border-radius: 50%;
background: #9ca3af;
animation: loading-bounce 1.4s ease-in-out infinite both;
animation: loading-bounce 1.5s ease-in-out infinite both;
}
.loading-dot:nth-child(1) {
@ -891,10 +791,14 @@
}
@keyframes loading-bounce {
0%, 80%, 100% {
0%,
80%,
100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1.2);
opacity: 1;
@ -1115,7 +1019,7 @@
margin-left: 5px;
}
.mask-layer{
.mask-layer {
position: fixed;
left: 0;
right: 0;
@ -1123,6 +1027,4 @@
bottom: 0;
background-color: rgba(0, 0, 0, .1);
}
</style>
</style>

@ -1,70 +1,67 @@
import config from '@/config'
import errorCode from '@/utils/errorCode'
import {
toast,
showConfirm,
tansParams
} from '@/utils/common'
import config from "@/config";
import errorCode from "@/utils/errorCode";
import { toast, showConfirm, tansParams } from "@/utils/common";
let timeout = 120000
const baseUrl = config.baseUrl
let timeout = 120000;
const baseUrl = config.baseUrl;
const request = config => {
const request = (config) => {
// 是否需要设置 token
// const isToken = (config.headers || {}).isToken === false
config.header = config.header || {}
config.header = config.header || {};
// if (getToken() && !isToken) {
// config.header['Authorization'] = 'Bearer ' + getToken()
// }
// get请求映射params参数
if (config.params) {
let url = config.url + '?' + tansParams(config.params)
url = url.slice(0, -1)
config.url = url
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.url = url;
}
return new Promise((resolve, reject) => {
uni.request({
method: config.method || 'get',
uni
.request({
method: config.method || "get",
timeout: config.timeout || timeout,
url: config.baseUrl || baseUrl + config.url,
data: config.data,
header: {
...config.header,
"x-tenant-id": '0'
},
dataType: 'json'
}).then(response => {
let res = response
const code = res.data.code || 200
const msg = errorCode[code] || res.data.msg || errorCode['default']
if (code === 500) {
// toast(msg)
reject('500')
} else if (code !== 200) {
// console.log(2);
// toast(msg)
reject(code)
dataType: "json",
})
.then((response) => {
let res = response.data.result;
// const code = res.data.code || 200
// if (code === 500) {
// reject('500')
// } else if (code !== 200) {
// reject(code)
// }
if (res.success) {
resolve(res.data);
} else {
resolve(res.error);
}
resolve(res.data)
})
.catch(error => {
let {
message
} = error
.catch((error) => {
let { message } = error;
if (message === 'Network Error') {
message = '网络不稳定,请检查网络 '
}else if(error.errMsg === 'request:fail' ) {
message = '后端接口连接异常'
}else if ( error.errMsg === 'request:fail timeout') {
message = '系统接口请求超时'
} else if (message.includes('Request failed with status code')) {
message = '系统接口' + message.substr(message.length - 3) + '异常'
if (message === "Network Error") {
message = "网络不稳定,请检查网络 ";
} else if (error.errMsg === "request:fail") {
message = "后端接口连接异常";
} else if (error.errMsg === "request:fail timeout") {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
} else {
message = `抱歉,出了点问题: ${error.errMsg || error.message}`;
}
// toast(message)
reject(error)
})
})
}
reject(error);
});
});
};
export default request
export default request;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save