@ -160,7 +160,7 @@
v - for = "(file, fileIdx) in msg.content"
: key = "fileIdx"
style = "flex: 0 0 6rem"
class = "relative text-xs h-32 w-80 rounded-md overflow-hidden mr-1 c-black"
class = "relative text-xs h-32 rounded-md overflow-hidden mr-1 c-black"
>
<!-- @ click = "previewVideo(msg.content)" -- >
< video
@ -184,11 +184,17 @@
< / view >
< / view >
< view
v - if = "msg.role === 'assistant' && msg.type === 'text'"
v - if = "
msg . role === 'assistant' && msg . type === 'text' && messages . length - 1 === idx
"
class = "absolute bottom--3.5 flex space-x-3 ml-1"
>
< image src = "/static/aichat/copy.png" class = "w-4 h-4" @click ="copyText(msg)" / >
< image src = "/static/aichat/resect.png" class = "w-4.3 h-4" @click ="refreshText()" / >
< image
src = "/static/aichat/resect.png"
class = "w-4.3 h-4"
@ click = "refreshText(msg)"
/ >
< / view >
< / view >
< image
@ -332,6 +338,7 @@
@ focus = "onFocus"
@ confirm = "sendText"
placeholder = "想对我说点什么~"
maxlength = "5000"
class = "flex-1 h-10 px-3 border border-gray-100 bg-[#f9f9f9] rounded-1 focus:outline-none"
/ >
<!-- 将keyup替换为confirm -- >
@ -349,7 +356,6 @@
v - if = "sendTextLoading && inputText.length <= 0"
src = "/static/aichat/enter-no.png"
class = "w-7 h-7"
@ click = "sendText()"
: disabled = "loading"
: class = "[knowledgeOpen ? 'ml-2' : 'ml-0']"
/ >
@ -477,6 +483,7 @@
import { ref , reactive , nextTick , watchEffect , watch } from 'vue'
import dayjs from 'dayjs'
import { useUserStore } from '@/store'
/ / i m p o r t s t o r e f r o m ' @ / s t o r e '
import { getEnvBaseUrl } from '@/utils'
import guid from '@/utils/guid.js'
import type { IGptRequestBody } from '@/service/index/foo'
@ -489,15 +496,16 @@ import {
officeFileTypeList as fileType ,
videoFileType as videoType ,
picFileType as picType ,
isJsonObject ,
} from './utils/index'
import 'dayjs/locale/zh-cn'
import { showToastErr , showToastOk , time _format3 } from '@/utils/tools'
import { uploadFileChunk } from './utils/api.js'
/ / i m p o r t { T O K E N , A V A T A R } f r o m ' . / u t i l s / t e s t '
import { deepClone } from 'wot-design-uni/components/common/util'
import { log } from 'console'
dayjs . locale ( 'zh-cn' )
const store = useUserStore ( )
interface UploadFile {
id : string
@ -605,7 +613,7 @@ async function createChatSession() {
gptModel : chatMode . value ,
} ,
header : {
Authorization : token . value ,
/ / A u t h o r i z a t i o n : t o k e n . v a l u e ,
} ,
} )
/ / 如 果 后 台 返 回 新 的 会 话 信 息 , 可 以 在 这 里 处 理 , 比 如 拿 到 l i s t U u i d 等
@ -627,9 +635,9 @@ const state = reactive({
} )
const scrollTop = ref ( 0 )
async function fetchHistoryList ( ) {
/ / i f ( s t a t e . p a g e * s t a t e . p a g e S i z e > s t a t e . t o t a l & & s t a t e . t o t a l ! = = n u l l ) {
/ / r e t u r n
/ / }
if ( state . page >= state . total / state . pageSize + 1 && state . total !== null ) {
retur n
}
if ( state . loading ) {
return
}
@ -643,12 +651,18 @@ async function fetchHistoryList() {
pageSize : state . pageSize ,
} ,
header : {
Authorization : token . value ,
/ / A u t h o r i z a t i o n : t o k e n . v a l u e ,
} ,
} )
if ( resp . data && resp . data . data ) {
rawList . value = rawList . value . concat ( resp . data . data . data )
state . total = resp . data . data . count / / M a t h . c e i l ( r e s p . d a t a . c o u n t / s t a t e . p a g e )
if ( state . total === null ) {
rawList . value = resp . data . data . data
state . total = resp . data . data . count
} else {
rawList . value = rawList . value . concat ( resp . data . data . data )
state . total = resp . data . data . count / / M a t h . c e i l ( r e s p . d a t a . c o u n t / s t a t e . p a g e )
}
/ / s c r o l l T o p . v a l u e + = 6 0 ;
}
} catch ( err ) {
@ -662,7 +676,10 @@ const scrolltolowerLoadData = (e) => {
}
watch (
( ) => state . page ,
async ( ) => {
async ( newval ) => {
if ( newval <= 0 ) {
return
}
await fetchHistoryList ( )
} ,
{ deep : true } ,
@ -670,9 +687,12 @@ watch(
async function openPopup ( ) {
state . page ++
showPopup . value = true
rawList . value = [ ]
}
function closePopup ( ) {
showPopup . value = false
state . page = 0
state . total = null
}
function toggleFullscreen ( ) {
fullscreen . value = ! fullscreen . value
@ -689,12 +709,12 @@ async function fetchHistoryDiets(value) {
gptModel : chatMode . value ,
} ,
header : {
Authorization : token . value ,
/ / A u t h o r i z a t i o n : t o k e n . v a l u e ,
} ,
} )
if ( resp && resp . data && resp . data . data ) {
const rawList = resp . data . data . detail / / 假 设 后 端 直 接 返 回 消 息 数 组
listUuid . value = resp . data . data . listUuid
const rawList = resp ? . data ? . data ? . detail / / 假 设 后 端 直 接 返 回 消 息 数 组
listUuid . value = resp ? . data ? . data ? . listUuid
/ / c o n s t n e w M e s s a g e s = p a r s e B a c k e n d M e s s a g e s ( J S O N . p a r s e ( r a w L i s t ) )
/ / 用 解 析 后 的 消 息 替 换 当 前 消 息 列 表
messages . splice ( 0 , messages . length , ... JSON . parse ( rawList ) )
@ -971,18 +991,30 @@ onMounted(async () => {
try {
const init = async ( ) => {
const wv = plus . webview . currentWebview ( ) / / 获 取 当 前 页 面 所 属 的 W e b v i e w 对 象 。
token . value = wv . token || uni . getStorageSync ( 'token' ) || import . meta . env . VITE _DEV _TOKEN
token . value =
wv . token ||
uni . getStorageSync ( 'token' ) ||
store . userInfo . token ||
import . meta . env . VITE _DEV _TOKEN
userInfo . value = JSON . parse ( wv . userInfo ) || { }
refreshToken . value = wv . refreshToken || uni . getStorageSync ( 'refreshToken' )
statusBarHeight . value = wv . statusBarHeight || uni . getSystemInfoSync ( ) . statusBarHeight
userAvatar . value = userInfo . value . Avatar
mask . value = userInfo . value . ID
store . setUserInfo ( {
token : token . value ,
avatar : userInfo . value . Avatar ,
refreshToken : refreshToken . value ,
statusBarHeight : statusBarHeight . value ,
} )
await createChatSession ( )
}
init ( )
} catch ( e ) {
console . error ( 'onMounted e: ' , e )
} finally {
token . value = store . userInfo . token
await createChatSession ( )
}
} )
@ -1411,12 +1443,60 @@ const onPickImage = () => {
} ,
} )
}
/ / 调 用 原 生 A n d r o i d A P I 拍 摄 视 频
const onPickVideo3 = ( ) => {
var cmr = plus . camera . getCamera ( )
try {
cmr . startVideoCapture (
( ) => {
alert ( 'ok' )
} ,
( ) => {
alert ( 'err' )
} ,
{ } ,
)
} catch ( e ) {
} finally {
cmr . stopVideoCapture ( )
}
}
/ / 视 频
const onPickVideo = ( ) => {
uni . chooseVideo ( {
sourceType : [ 'album' , 'camera' ] ,
/ / s o u r c e T y p e : [ ' a l b u m ' , ' c a m e r a ' ] ,
sourceType : [ 'album' ] ,
maxDuration : 60 ,
compressed : true ,
camera : 'back' ,
albumMode : 'custom' ,
/ / e x t e n s i o n : u p l o a d C o n f i g . v i d e o . s u p p o r t T y p e ,
success : ( res : any ) => {
console . log ( res )
const tempFile = res . tempFile
tempFile . path = res . tempFilePath
/ / 开 始 上 传
addUploadQueue ( [ tempFile ] , uploadFileTypeEm . video )
} ,
fail : ( err ) => {
uni . showToast ( {
title : '选取视频失败' ,
icon : 'none' ,
} )
} ,
} )
}
/ / 视 频
const onPickVideo2 = ( ) => {
uni . chooseVideo ( {
sourceType : [ 'album' , 'camera' ] ,
maxDuration : 60 ,
compressed : true ,
camera : 'back' ,
/ / e x t e n s i o n : u p l o a d C o n f i g . v i d e o . s u p p o r t T y p e ,
success : ( res : any ) => {
console . log ( res )
@ -1561,7 +1641,6 @@ const stopMsg = () => {
stopStreamMsg = true
}
async function sendText ( ) {
console . log ( 'uploadList: ' , uploadList )
if ( uploadList . length > 0 ) {
const isUpLoading = uploadList . some ( ( file ) => {
/ / r e t u r n f i l e . s t a t u s = = = " e r r o r " | | f i l e . s t a t u s = = = " p e n d i n g "
@ -1578,18 +1657,16 @@ async function sendText() {
}
}
const msg = inputText . value . trim ( )
if ( ! msg && ! refreshSend . value ) {
if ( ! msg ) {
return showToastErr ( '不可以发送空消息!' )
}
/ / i f ( u p l o a d L i s t . l e n g t h > 0 ) {
/ / r e t u r n s h o w T o a s t E r r ( ' 请 等 待 文 件 上 传 完 成 ! ' )
/ / }
if ( ! sendTextLoading . value ) {
sendTextLoading . value = true
return showToastErr ( '正在接收消息请稍后' )
}
/ / 开 启 加 载 状 态
sendTextLoading . value = false
/ / 文 本 消 息
/ / 先 判 断 是 否 上 传 文 件 , 若 有 文 件 在 判 断 文 件 类 型 , 视 频 + 图 片 不 能 与 文 档 类 文 件 同 时 上 传
@ -1741,16 +1818,16 @@ async function sendText() {
}
}
} else {
if ( ! refreshSend . value ) {
/ / 不 重 发 时 触 发
addMessage ( {
role : 'user' ,
type : 'text' ,
content : msg ,
timestamp : new Date ( ) ,
mask : 'new' ,
} )
}
/ / i f ( ! r e f r e s h S e n d . v a l u e ) {
/ / 不 重 发 时 触 发
addMessage ( {
role : 'user' ,
type : 'text' ,
content : msg ,
timestamp : new Date ( ) ,
mask : 'new' ,
} )
/ / }
/ / 纯 文 本 时 发 送
chatMode . value = 'tongyi-app' / / ' t o n g y i - a p p ' ; q w e n - l o n g
@ -1782,7 +1859,6 @@ async function sendText() {
/ / 第 一 次 发 送 纯 文 本 消 息 , 第 二 次 发 送 图 片 + 视 频 , 第 三 次 发 送 文 档 , 此 时 因 为 历 史 消 息 都 要 一 起 发 送 给 后 端 ,
/ / 所 以 要 想 办 法 在 遇 到 这 种 情 况 时 , 截 断 历 史 记 录 , 主 动 为 用 户 建 立 一 个 新 的 回 话 , 但 是 不 需 要 清 空 历 史 记 录
console . log ( 'message: ' , messages )
uploadList . splice ( 0 , uploadList . length ) / / 清 空 上 传 的 文 件
const list = formatData ( messages )
@ -1790,9 +1866,38 @@ async function sendText() {
body . detail = JSON . stringify ( messages )
aiMsg . content = ''
addMessage ( aiMsg )
send ( body )
/ / r e t u r n
/ / r e t u r n
/ / 没 有 上 传 文 件 , 仅 文 字 消 息
}
const spliceMsg = ( list , model ) => {
let file = false
let image = false
let video = false
const length = list . length
console . log ( list )
for ( let i = length - 1 ; i >= 0 ; i -- ) {
const item = list [ i ]
if ( model === 'tongyi-long' && item . type === 'image' && item . type === 'video' ) {
const index = length - i - 1
return list . splice ( i - 1 )
} else if ( model !== 'tongyi-long' && item . type === 'file' ) {
const index = i
return list . splice ( index + 1 )
}
}
return list
}
const send = async ( body ) => {
/ / r e f r e s h S e n d . v a l u e = t r u e ; / / 正 在 重 新 发 送
/ / 开 启 加 载 状 态
sendTextLoading . value = false / / 接 收 消 息 期 间 不 可 再 次 发 送
const [ aiMsg ] = messages . slice ( - 1 )
const recordList = messages . slice ( 0 , messages . length - 1 )
body . detail = JSON . stringify ( recordList )
/ / b o d y . m e s s a g e s = s p l i c e M s g ( b o d y . m e s s a g e s , c h a t M o d e . v a l u e )
try {
/ / a i M s g . c o n t e n t = ' '
/ / 发 送 问 题 到 后 端
@ -1801,10 +1906,14 @@ async function sendText() {
const signal = controller . signal
const resp = await fetch ( baseUrl + '/chat/app/completion' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , Authorization : token . value } ,
headers : {
'Content-Type' : 'application/json' ,
Authorization : token . value ,
} ,
body : JSON . stringify ( body ) ,
signal : signal ,
} )
console . log ( resp )
const reader = resp . body ! . getReader ( )
const decoder = new TextDecoder ( )
@ -1839,13 +1948,17 @@ async function sendText() {
const json = JSON . parse ( chunk )
const delta = json . choices ? . [ 0 ] ? . delta ? . content
if ( delta ) {
msgLoading . value = false
aiMsg . content += delta
/ / m s g L o a d i n g . v a l u e = f a l s e
/ / 每 次 更 新 m e s s a g e s 消 息 , 实 现 流 式 输 出
messages [ messages . length - 1 ] = { ... aiMsg }
scrollToBottom ( )
}
} catch { }
} catch ( e ) {
console . log ( e )
} finally {
console . log ( 'over' )
}
}
/ / 更 新 上 下 文 消 息
@ -1871,6 +1984,13 @@ async function sendText() {
console . log ( 'chunk------------------: ' )
}
}
if ( isJsonObject ( buffer ) ) {
const response = JSON . parse ( buffer )
if ( response . code === 401 ) {
showToastErr ( '请重新登录' )
}
}
scrollToBottom ( )
} catch ( err ) {
/ / a i M s g . c o n t e n t = ' 请 重 新 发 送 '
@ -1880,7 +2000,7 @@ async function sendText() {
} finally {
sendTextLoading . value = true
showActions . value = false
refreshSend . value = false // 重 发 已 经 结 束 关 闭 重 发
/ / r e f r e s h S e n d . v a l u e = f a l s e // 重 发 已 经 结 束 关 闭 重 发
msgLoading . value = false
}
}
@ -1899,7 +2019,16 @@ function copyText(msg: IMessage) {
}
}
function refreshText ( ) {
const msgType = ( msg ) => {
return (
msg &&
msg . role === 'user' &&
( msg . type === 'text' || msg . type === 'image' || msg . type === 'video' )
)
}
function refreshText ( msg ) {
console . log ( 'refresh msg' , msg )
if ( ! sendTextLoading . value ) {
/ / 正 在 接 收 消 息 , 不 可 以 重 发
return
@ -1910,98 +2039,57 @@ function refreshText() {
/ / 1 . 找 到 最 后 两 条 用 户 消 息 ( 用 于 处 理 图 文 混 合 场 景 )
const userMessages = messages . filter ( ( msg ) => msg . role === 'user' )
/ / c o n s t l a s t T w o U s e r M s g s = u s e r M e s s a g e s . s l i c e ( - 2 )
const [ msg1 , msg2 ] = deepClone ( userMessages . slice ( - 2 ) )
/ / l e t t e x t = l a s t T w o U s e r M s g s [ 0 ] / / l a s t T w o U s e r M s g s . e v e r y ( ( m s g ) = > m s g . t y p e = = = " t e x t " | | m s g . t y p e = = = " i m a g e " | | m s g . t y p e = = = " v i d e o " | | m s g . t y p e = = = " f i l e " )
/ / l e t f i l e = l a s t T w o U s e r M s g s [ 1 ] / / l a s t T w o U s e r M s g s . e v e r y ( ( m s g ) = > m s g . t y p e = = = " t e x t " | | m s g . t y p e = = = " i m a g e " | | m s g . t y p e = = = " v i d e o " | | m s g . t y p e = = = " f i l e " )
/ / c o n s t [ m s g 1 , m s g 2 , m s g 3 ] = d e e p C l o n e ( u s e r M e s s a g e s . s l i c e ( - 3 ) )
const newMsgArr = deepClone ( userMessages . slice ( - 3 ) )
/ / 2 . 提 取 文 本 内 容 和 文 件 列 表
let refreshT ext = null
let t ext = null
const refreshFiles : UploadFile [ ] = [ ]
if ( msg1 && msg1 . type === 'text' && msg2 && msg2 . type !== 'text' ) {
msg1 . mask = 'new'
msg2 . mask = 'new'
refreshFiles . push ( msg1 )
refreshFiles . push ( msg2 )
} else if ( msg1 . type === 'text' && msg1 . role === 'user' && ! msg2 ) {
msg1 . mask = 'new'
refreshFiles . push ( msg1 )
} else if ( msg2 . type === 'text' && msg2 . role === 'user' && ! msg1 ) {
msg2 . mask = 'new'
refreshFiles . push ( msg2 )
} else {
msg2 . mask = 'new'
refreshFiles . push ( msg2 )
for ( let i = newMsgArr . length - 1 ; i >= 0 ; i -- ) {
const msg = newMsgArr [ i ]
if ( msg . type === 'text' ) {
refreshFiles . unshift ( msg )
break
} else {
refreshFiles . unshift ( msg )
}
}
/ / l a s t T w o U s e r M s g s . f o r E a c h ( ( m s g , i ) = > {
/ / c o n s o l e . l o g ( ' m s g : ' , m s g ) ;
/ / i f ( m s g . t y p e = = = ' t e x t ' & & m s g . r o l e = = = " u s e r " ) {
/ / r e f r e s h T e x t = m s g . c o n t e n t / / 总 是 取 最 新 的 文 本
/ / r e f r e s h F i l e s . p u s h ( {
/ / c o n t e n t : m s g . c o n t e n t ,
/ / t y p e : " t e x t " ,
/ / r o l e : " u s e r " ,
/ / t i m e s t a m p : n e w D a t e ( ) ,
/ / m a s k : " n e w "
/ / } )
/ / } e l s e i f ( m s g . t y p e = = = " v i d e o " ) {
/ / m s g . m a s k = " n e w "
/ / r e f r e s h F i l e s . p u s h ( m s g )
/ / / / m s g . c o n t e n t . f o r E a c h ( ( f i l e : a n y ) = > {
/ / / / c o n s o l e . l o g ( ' l a s t T w o U s e r M s g s f i l e : ' , f i l e ) ;
/ / / / r e f r e s h F i l e s . p u s h ( {
/ / / / i d : g u i d . g e t G u i d ( ) ,
/ / / / u r l : f i l e . v i d e o _ u r l . u r l ,
/ / / / s t a t u s : ' s u c c e s s ' ,
/ / / / n a m e : f i l e . n a m e | | ' 未 命 名 文 件 ' ,
/ / / / s i z e : f i l e . s i z e | | 0 ,
/ / / / u p l o a d F i l e T y p e : f i l e . u p l o a d F i l e T y p e | | d e t e c t F i l e T y p e ( f i l e . v i d e o _ u r l . u r l ) ,
/ / / / } )
/ / / / } )
/ / } e l s e i f ( m s g . t y p e = = = " i m a g e " ) {
/ / m s g . m a s k = " n e w "
/ / r e f r e s h F i l e s . p u s h ( m s g )
/ / / / m s g . c o n t e n t . f o r E a c h ( ( f i l e : a n y ) = > {
/ / / / c o n s o l e . l o g ( ' l a s t T w o U s e r M s g s f i l e : ' , f i l e ) ;
/ / / / r e f r e s h F i l e s . p u s h ( {
/ / / / i d : g u i d . g e t G u i d ( ) ,
/ / / / u r l : f i l e . i m a g e _ u r l . u r l ,
/ / / / s t a t u s : ' s u c c e s s ' ,
/ / / / n a m e : f i l e . n a m e | | ' 未 命 名 文 件 ' ,
/ / / / s i z e : f i l e . s i z e | | 0 ,
/ / / / u p l o a d F i l e T y p e : f i l e . u p l o a d F i l e T y p e | | d e t e c t F i l e T y p e ( f i l e . i m a g e _ u r l . u r l ) ,
/ / / / } )
/ / / / } )
/ / } e l s e {
/ / m s g . m a s k = " n e w "
/ / r e f r e s h F i l e s . p u s h ( m s g )
/ / / / m s g . c o n t e n t . f o r E a c h ( ( f i l e : a n y ) = > {
/ / / / c o n s o l e . l o g ( ' l a s t T w o U s e r M s g s f i l e : ' , f i l e ) ;
/ / / / r e f r e s h F i l e s . p u s h ( {
/ / / / i d : g u i d . g e t G u i d ( ) ,
/ / / / u r l : f i l e . c o n t e n t ,
/ / / / s t a t u s : ' s u c c e s s ' ,
/ / / / n a m e : f i l e . n a m e | | ' 未 命 名 文 件 ' ,
/ / / / s i z e : f i l e . s i z e | | 0 ,
/ / / / u p l o a d F i l e T y p e : f i l e . u p l o a d F i l e T y p e | | d e t e c t F i l e T y p e ( f i l e . c o n t e n t ) ,
/ / / / } )
/ / / / } )
/ / }
/ / } )
/ / 3 . 更 新 输 入 框 和 上 传 列 表
/ / i n p u t T e x t . v a l u e = re f r e s h T e x t
/ / i n p u t T e x t . v a l u e = t e x t
/ / u p l o a d L i s t . s p l i c e ( 0 , u p l o a d L i s t . l e n g t h , . . . r e f r e s h F i l e s )
refreshFiles . forEach ( ( ele ) => {
messages . push ( ele )
} )
refreshSend . value = true
/ / i n p u t T e x t . v a l u e = r e f r e s h T e x t
/ / r e t u r n
/ / r e f r e s h S e n d . v a l u e = t r u e
/ / i n p u t T e x t . v a l u e = t e x t
/ / 4 . 自 动 触 发 发 送 ( 模 拟 用 户 点 击 发 送 按 钮 )
setTimeout ( ( ) => {
sendText ( )
} , 100 )
/ / s e t T i m e o u t ( ( ) = > {
/ / s e n d T e x t ( )
/ / } , 1 0 0 )
let list = formatData ( messages )
const aiMsg = {
role : 'assistant' ,
type : 'text' ,
content : '' ,
timestamp : new Date ( ) ,
}
messages . push ( aiMsg )
const body = {
model : chatMode . value , / / 模 型 选 择
max _tokens : 1000 ,
top _p : 1 ,
presence _penalty : 0 ,
frequency _penalty : 0 ,
messages : list , / / t e x t ? [ a i M s g ] : h i s t o r y U s e r M s g s ,
stream : true ,
listUuid : listUuid . value ,
/ / l i s t U u i d : " e f f 1 8 a 1 0 - 1 7 1 9 - 4 5 2 8 - a d 6 3 - e e 5 c 0 1 d 0 a 4 1 2 "
}
send ( body )
}
/ / 文 件 类 型 检 测 函 数 ( 根 据 U R L 后 缀 )