Files
mattlution/www/js/index.js
2025-08-11 22:55:39 +08:00

649 lines
18 KiB
JavaScript

// 接口地址
const api = 'https://api.pandorastudio.cn/'
let messages = []
let timer = null
// 超时时间
const timeout = 60
// 默认加载文字
const defaultLoadingText = '正在思考中...'
// 思考时间过长加载文字
const longLoadingText = '问题有点难,正在思考中...'
// 加载文字
let loadingText = defaultLoadingText
// 历史记录文件夹入口
let historyDirEntry = null
// APP目录入口
let appDirEntry = null
const md = markdownit()
md.set({ linkify: true, typographer: true, breaks: true })
// 默认设置
let settings = {
// 是否开启自动保存
autoSave: true,
// gpt模型
model: 'gpt-3.5-turbo',
// 是否使用流式对话
useStream: true,
// 创造性程度
temperature: 1,
}
// 用户数据
let userData = {
// 历史文件
history: 'history.json',
// 设置文件
settings: 'settings.json',
}
// websocket
let socket = null
// 超时断开websocket 30s
const socketTimeOut = 30
// socket超时计时器
let socketTimer = null
// 服务端是否开始处理
let isStreamStart = false
// 用户重试次数
let retryCount = 0
// 重试次数上限
const retryLimit = 3
// 流式对话
function streamChat() {
const decoder = new TextDecoder('utf-8')
const div = document.createElement('div')
// 给div添加class(left和pending)
div.className = 'left pending'
// 时间戳id
div.id = Date.now()
let streamOutput = ''
// 插入div
$('article').append(div)
// 禁用提交按钮
$('#submit').attr('disabled', true)
if (!socket) {
// 连接websocket
socket = io(api)
// 创建socket超时计时器
let socketTime = 0
socketTimer = setInterval(() => {
// 如果超时
if (socketTime > socketTimeOut && !isStreamStart) {
clearInterval(socketTimer)
// 断开连接
socket.close()
// 清空socket
socket = null
// 移除最新创建的div
$('article').get.lastElementChild.remove()
confirm({ content: `连接超时,是否重新发问?`, confirmText: '重新发问' }).then(questionSubmit)
} else {
socketTime++
}
}, 1000)
// 监听连接
socket.on('connect', () => {
// 监听消息
socket.on('chatResult', res => {
if (res.code == 200) {
// 获取最新创建的div
const newDiv = $('article').get.lastElementChild
try {
const chunk = decoder.decode(res.chunk)
const lines = chunk.split('\n')
const parsedLines = lines
.map(line => line.replace(/^data: /, '').trim()) // Remove the "data: " prefix
.filter(line => line !== '' && line !== '[DONE]') // Remove empty lines and "[DONE]"
.map(line => JSON.parse(line)) // Parse the JSON string
// 移除pending
newDiv.classList.remove('pending')
isStreamStart = true
for (const parsedLine of parsedLines) {
const { choices } = parsedLine
const { delta, finish_reason } = choices[0]
const { content } = delta
// Update the UI with the new content
if (content) {
streamOutput += content
newDiv.innerText += content
//滚动到底部
$('article').get.scrollTo({ top: $('article').get.scrollHeight, behavior: 'smooth' })
}
// 对话结束
if (finish_reason === 'stop') {
clearInterval(socketTimer)
newDiv.innerHTML = md.render(newDiv.innerText)
// 高亮html
newDiv.querySelectorAll('code').forEach(el => {
hljs.highlightElement(el)
})
isStreamStart = false
navigator.vibrate(300)
// 添加复制按钮
addCopyBtn(newDiv)
//滚动到底部
$('article').get.scrollTo({ top: $('article').get.scrollHeight, behavior: 'smooth' })
// 启用reload按钮
$('#reload').removeAttr('disabled')
// 清空输入框所有内容
$('textarea').val('')
messages.push({ role: 'assistant', content: streamOutput })
// 是否开启自动保存
settings.autoSave && autoSave()
}
}
} catch (err) {
isStreamStart = false
clearInterval(socketTimer)
confirm({ content: `网络发生错误`, confirmText: '重新发送' }).then(questionSubmit)
// 移除最新创建的div
newDiv.remove()
}
} else {
isStreamStart = false
clearInterval(socketTimer)
confirm({ content: `连接发生错误`, confirmText: '重新发送' }).then(questionSubmit)
// 移除最新创建的div
$('article').get.lastElementChild.remove()
}
})
// 监听错误
socket.on('error', () => {
isStreamStart = false
clearInterval(socketTimer)
confirm({ content: `连接发生错误`, confirmText: '重新发送' }).then(questionSubmit)
// 移除最新创建的div
$('article').get.lastElementChild.remove()
})
})
}
// 发送消息
const { model, temperature } = settings
socket.emit('chat', { messages, model, temperature })
}
// 标准对话
function standardChat() {
let loadTime = 0
// 恢复默认加载文字
loadingText = defaultLoadingText
showLoading(`${loadingText} ${loadTime++}s`)
timer = setInterval(() => {
// 如果超时
if (loadTime > timeout) {
clearInterval(timer)
hideLoading()
confirm({ content: `连接超时,是否重新发问?`, confirmText: '重新发问' }).then(questionSubmit)
// 取消请求
cancelAjax()
} else if (loadTime > 10) {
loadingText = longLoadingText
showLoading(`${loadingText} ${loadTime++}s`)
} else {
showLoading(`${loadingText} ${loadTime++}s`)
}
}, 1000)
// 发送消息
const { model, temperature } = settings
$()
.ajax({
url: `${api}common/chat`,
type: 'POST',
data: { messages: JSON.stringify(messages), model, temperature },
async: true,
})
.then(res => {
const { code, data } = res
if (code == 200) {
// 获取data最后一条消息
const lastMessage = data[data.length - 1]
const div = document.createElement('div')
div.innerHTML = md.render(lastMessage.message.content)
div.className = 'left'
// 高亮html
div.querySelectorAll('code').forEach(el => {
hljs.highlightElement(el)
})
// 添加复制按钮
addCopyBtn(div)
$('article').append(div)
messages.push({ role: 'assistant', content: lastMessage.message.content })
// 清空输入框所有内容
$('textarea').val('')
navigator.vibrate(300)
// 是否开启自动保存
settings.autoSave && autoSave()
} else {
const { message } = JSON.parse(res.err).error
alert(message)
}
})
.catch(err => {
confirm({ content: `发生错误:${JSON.stringify(err)}`, confirmText: '重新发送' }).then(questionSubmit)
})
.finally(() => {
clearInterval(timer)
hideLoading()
//滚动到底部
$('article').get.scrollTo({ top: $('article').get.scrollHeight, behavior: 'smooth' })
// 启用reload按钮
$('#reload').removeAttr('disabled')
// 禁用提交按钮
$('#submit').attr('disabled', true)
})
}
// 清空所有记录
function clearChat() {
return new Promise((resolve, reject) => {
$('article').empty()
messages = []
$('#reload').attr('disabled', true)
// 清空本地历史记录文件
createFile(historyDirEntry, userData.history).then(fileEntry => {
// 写入历史记录文件
writeFile(fileEntry, '').then(resolve).catch(reject)
})
})
}
// 提交问题
function questionSubmit() {
const question = $('textarea').val().trim()
if (question == '') {
alert('请输入内容')
return
}
$('textarea').blur()
$('article').append(`<p class="right">${question.toString()}</p>`)
//延时滚动到底部
$('article').get.scrollTo({ top: $('article').get.scrollHeight, behavior: 'smooth' })
messages.push({ role: 'user', content: question.toString() })
// 判断是否使用流式对话
if (settings.useStream) {
streamChat()
} else {
standardChat()
}
}
// 创建本地目录
function createDirectory(rootDirEntry, folderName) {
return new Promise((resolve, reject) => {
rootDirEntry.getDirectory(
folderName,
{ create: true },
function (dirEntry) {
resolve(dirEntry)
},
reject
)
})
}
// 创建本地提问历史记录
function createFile(dirEntry, fileName) {
return new Promise((resolve, reject) => {
dirEntry.getFile(
fileName,
{ create: true, exclusive: false },
function (fileEntry) {
resolve(fileEntry)
},
reject
)
})
}
// 写入本地提问历史记录
function writeFile(fileEntry, dataObj) {
return new Promise((resolve, reject) => {
fileEntry.createWriter(function (fileWriter) {
fileWriter.onwriteend = function () {
resolve(fileEntry)
}
fileWriter.onerror = function (e) {
reject(e)
}
fileWriter.write(dataObj)
})
})
}
// 读取本地提问历史记录
function readFile(fileEntry) {
return new Promise((resolve, reject) => {
fileEntry.file(function (file) {
const reader = new FileReader()
reader.onloadend = function () {
resolve(this.result)
}
reader.readAsText(file)
}, reject)
})
}
// 自动保存提问历史记录
function autoSave() {
// 创建历史记录文件
createFile(historyDirEntry, userData.history).then(fileEntry => {
// 写入历史记录文件
writeFile(fileEntry, JSON.stringify(messages))
})
}
// 添加复制功能
function addCopyBtn(div) {
// 创建复制按钮
const copyBtn = document.createElement('button')
copyBtn.className = 'btn-copy'
// 绑定复制事件
copyBtn.addEventListener('click', () => {
// 复制到剪切板
const text = div.innerText
cordova.plugins.clipboard.copy(text)
alert('复制成功')
})
div.appendChild(copyBtn)
}
// 更新UI显示
function updateUI() {
return new Promise((resolve, reject) => {
settings.autoSave ? $('#autoSave').addClass('active') : $('#autoSave').removeClass('active')
settings.useStream ? $('#useStream').addClass('active') : $('#useStream').removeClass('active')
resolve()
})
}
// 添加事件
function addEvent() {
// 点击提交按钮
$('#submit').click(questionSubmit)
// 监听回车键
$('textarea').bind('keydown', e => {
if (e.keyCode == 13) {
e.preventDefault()
questionSubmit()
}
})
// 监听输入框变化
$('textarea').bind('input', e => {
const question = $('textarea').val().trim()
if (question == '') {
$('#submit').attr('disabled', true)
} else {
$('#submit').removeAttr('disabled')
}
})
// 点击清空
$('#reload').click(() => {
// 提醒用户是否清空
confirm({
content: `确认清空?`,
}).then(() => {
// 判断是否开启流式对话
if (socket) {
// 关闭socket
socket.close()
// 重置socket
socket = null
// 清空socket超时计时器
clearInterval(socketTimeOut)
isStreamStart = false
}
// 清空所有记录
clearChat()
})
})
// 切换自动保存
$('#autoSave').bind('click', e => {
e.preventDefault()
// 判断是否开启了自动保存
if (settings.autoSave) {
// 提示用户关闭自动保存将会清空历史记录
confirm(`确定不再记住?\n关闭后将清空历史记录`).then(() => {
settings.autoSave = false
// 清空所有记录
clearChat().then(() => {
// 保存设置
saveSettings('autoSave', settings.autoSave).then(updateUI)
})
})
} else {
settings.autoSave = true
// 保存设置
saveSettings('autoSave', settings.autoSave).then(updateUI)
}
})
// 切换对话模式
$('#useStream').bind('click', e => {
e.preventDefault()
// 判断是否开启流式对话
if (settings.useStream) {
settings.useStream = false
if (socket) {
// 关闭socket
socket.close()
// 清空socket
socket = null
// 清空socket超时计时器
clearInterval(socketTimeout)
socketTimeout = null
}
} else {
settings.useStream = true
}
// 保存设置
saveSettings('useStream', settings.useStream).then(updateUI)
})
// 调整创造性
$('.creativity input').bind('change', e => {
settings.temperature = e.target.value
// 保存设置
saveSettings('temperature', settings.temperature)
// 更新UI显示
$('.creativity dfn').text(`创意度:${settings.temperature}`)
})
// 点击展开侧边栏
$('.btn-menu').click(() => {
$('menu').attr('open', 'open')
// 监听侧边栏过渡动画结束事件
$('menu').bind(
'transitionend',
() => {
// 添加点击侧边栏以外区域关闭侧边栏事件
$('body').bind('click', e => {
if (e.target.tagName !== 'MENU') {
$('menu').removeAttr('open')
$('body').unbind('click')
}
})
},
{ once: true }
)
})
}
// 保存设置
function saveSettings(key = 'default', value) {
return new Promise((resolve, reject) => {
if (key !== 'default') {
settings[key] = value
}
// 写入设置文件
createFile(appDirEntry, userData.settings).then(fileEntry => {
// 写入设置文件
writeFile(fileEntry, JSON.stringify(settings)).then(resolve).catch(reject)
})
})
}
// 后台自动更新应用
function backgroundUpdateApp() {
return confirm(11111)
// 获取服务器配置文件
$()
.ajax({
url: `https://upload.pandorajs.com/apk/package.json?${Date.now()}`,
})
.then(res => {
// 获取服务器版本号
const { version, name: appName } = res
// 获取本地版本号
const { version: localVersion } = AppVersion
// 判断是否有新版本
if (version !== localVersion) {
// 提示用户是否更新
confirm(`有新版本,是否下载?`).then(() => {
// 下载apk
const fileTransfer = new FileTransfer()
const uri = encodeURI(`https://upload.pandorajs.com/apk/${appName}.apk`)
// 创建apk下载目录
fileTransfer.download(
uri,
`${cordova.file.externalApplicationStorageDirectory}${appName}.apk`,
() => {
confirm(`下载完成,是否安装?`).then(() => {
cordova.plugins.fileOpener2.open(`${cordova.file.externalApplicationStorageDirectory}${appName}.apk`, 'application/vnd.android.package-archive')
})
},
() => {
alert('更新失败,请稍后重试')
},
false
)
})
} else {
// 无须更新删除已下载的apk
resolveLocalFileSystemURL(`${cordova.file.externalApplicationStorageDirectory}`, function (fileDir) {
fileDir.getFile(`${appName}.apk`, { create: false, exclusive: true }, fileEntry => {
fileEntry.remove()
})
})
}
})
}
// 初始化
function Init() {
// 开始后台自动更新应用
backgroundUpdateApp()
resolveLocalFileSystemURL(cordova.file.externalApplicationStorageDirectory, function (dirEntry) {
appDirEntry = dirEntry
// 创建历史记录文件夹
createDirectory(dirEntry, userData.history).then(dirEntry => {
historyDirEntry = dirEntry
// 读取历史记录文件
historyDirEntry.getFile(userData.history, { create: false, exclusive: true }, fileEntry => {
// 读取历史记录文件内容
readFile(fileEntry).then(data => {
messages = JSON.parse(data)
messages.forEach(message => {
const div = document.createElement('div')
div.innerHTML = md.render(message.content)
// 高亮html
div.querySelectorAll('code').forEach(el => {
hljs.highlightElement(el)
})
if (message.role == 'user') {
div.className = 'right'
$('article').append(div)
} else {
div.className = 'left'
// 添加复制按钮
addCopyBtn(div)
$('article').append(div)
}
})
//滚动到底部
$('article').get.scrollTo({ top: $('article').get.scrollHeight })
// 启用reload按钮
$('#reload').removeAttr('disabled')
})
})
})
// 读取设置文件
appDirEntry.getFile(
userData.settings,
{ create: false, exclusive: true },
fileEntry => {
// 读取设置文件内容
readFile(fileEntry).then(data => {
const userSettings = JSON.parse(data)
// 合并设置
settings = Object.assign(settings, userSettings)
updateUI()
addEvent()
})
},
() => {
// 创建默认设置文件
appDirEntry.getFile(userData.settings, { create: true, exclusive: false }, () => {
saveSettings().then(() => {
// 首次使用
confirm({
content: `欢迎使用问·想`,
showCancel: false,
confirmText: '进入',
})
updateUI()
addEvent()
})
})
}
)
})
}
switch (env) {
default:
document.addEventListener('deviceready', Init, false)
break
case 'web':
Init()
break
}
// TODO 收藏夹功能
// TODO 整合Midjourney
// TODO 角色扮演
// TODO 文本转语音
// TODO 分享功能