You've already forked mattlution
701 lines
18 KiB
JavaScript
701 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 分享功能
|