// 接口地址 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(`
${question.toString()}
`) //延时滚动到底部 $('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 分享功能