// 接口地址 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 分享功能