You've already forked mattlution
'初始化提交'
This commit is contained in:
94
www/css/atom-one-light.css
Normal file
94
www/css/atom-one-light.css
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
|
||||
Atom One Light by Daniel Gamage
|
||||
Original One Light Syntax theme from https://github.com/atom/one-light-syntax
|
||||
|
||||
base: #fafafa
|
||||
mono-1: #383a42
|
||||
mono-2: #686b77
|
||||
mono-3: #a0a1a7
|
||||
hue-1: #0184bb
|
||||
hue-2: #4078f2
|
||||
hue-3: #a626a4
|
||||
hue-4: #50a14f
|
||||
hue-5: #e45649
|
||||
hue-5-2: #c91243
|
||||
hue-6: #986801
|
||||
hue-6-2: #c18401
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
color: #383a42;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #a0a1a7;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-formula {
|
||||
color: #a626a4;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name,
|
||||
.hljs-selector-tag,
|
||||
.hljs-deletion,
|
||||
.hljs-subst {
|
||||
color: #e45649;
|
||||
}
|
||||
|
||||
.hljs-literal {
|
||||
color: #0184bb;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-addition,
|
||||
.hljs-attribute,
|
||||
.hljs-meta .hljs-string {
|
||||
color: #50a14f;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-number {
|
||||
color: #986801;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-link,
|
||||
.hljs-meta,
|
||||
.hljs-selector-id,
|
||||
.hljs-title {
|
||||
color: #4078f2;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-title.class_,
|
||||
.hljs-class .hljs-title {
|
||||
color: #c18401;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
361
www/css/style.css
Normal file
361
www/css/style.css
Normal file
@@ -0,0 +1,361 @@
|
||||
:root {
|
||||
--alertFontSize: 0.15rem;
|
||||
--alertTheme: #fff6dc;
|
||||
--alertBg: transparent;
|
||||
--alertColor: #9b6e44;
|
||||
--confirmFontSize: 0.15rem;
|
||||
--confirmBg: rgba(0, 0, 0, 0);
|
||||
}
|
||||
#Pd_loader {
|
||||
font-size: 0.15rem !important;
|
||||
}
|
||||
a,
|
||||
button,
|
||||
input,
|
||||
label {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
outline: none;
|
||||
background: none;
|
||||
border: none;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
html {
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background: #fff;
|
||||
font-size: 0.18rem;
|
||||
}
|
||||
/* 自定义滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #fff;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #aaa;
|
||||
}
|
||||
/* 复制按钮 */
|
||||
.btn-copy {
|
||||
background-image: url(../icon/icon-copy.svg);
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translate(150%, 0);
|
||||
}
|
||||
/* 菜单按钮 */
|
||||
.btn-menu {
|
||||
background-image: url(../icon/icon-menu.svg);
|
||||
background-size: 1.5em;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right bottom;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
/* 侧边栏 */
|
||||
menu {
|
||||
width: 40vw;
|
||||
height: 100vh;
|
||||
background: #f6f6f6;
|
||||
position: absolute;
|
||||
box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.15);
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
z-index: 3;
|
||||
padding: 5%;
|
||||
font-size: 0.85em;
|
||||
color: #828282;
|
||||
transform: translateX(calc(-1 * 100% - 20px));
|
||||
transition: transform 0.2s ease-out;
|
||||
font-weight: bold;
|
||||
}
|
||||
menu[open] {
|
||||
transform: translateX(0);
|
||||
}
|
||||
menu li {
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
}
|
||||
menu li a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1em;
|
||||
}
|
||||
menu li a::before {
|
||||
content: "";
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
display: block;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
.menu_favo::before {
|
||||
background-image: url(../icon/icon-favo.svg);
|
||||
}
|
||||
section {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: 0.18rem;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
section .content {
|
||||
width: calc(100% + 20px);
|
||||
height: calc(100% - 10em - 5vh - 20px);
|
||||
flex-shrink: 0;
|
||||
font-size: 0.85em;
|
||||
position: relative;
|
||||
box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.15) inset;
|
||||
}
|
||||
section article {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 10px 10px 40px 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
section article .left,
|
||||
section article .right {
|
||||
margin: 10px 0;
|
||||
line-height: 1.5;
|
||||
max-width: 90%;
|
||||
padding: 10px 20px;
|
||||
box-sizing: border-box;
|
||||
clear: both;
|
||||
border-radius: 10px;
|
||||
font-family: "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.35);
|
||||
position: relative;
|
||||
}
|
||||
section article .left {
|
||||
float: left;
|
||||
background: linear-gradient(to bottom, #fdfdfd, #f9f9f9);
|
||||
border-top-left-radius: 0;
|
||||
color: #656565;
|
||||
}
|
||||
section article .left.pending::before {
|
||||
content: "...";
|
||||
animation: pending 0.6s infinite linear;
|
||||
}
|
||||
@keyframes pending {
|
||||
0% {
|
||||
content: ".";
|
||||
}
|
||||
33% {
|
||||
content: "..";
|
||||
}
|
||||
66% {
|
||||
content: "...";
|
||||
}
|
||||
100% {
|
||||
content: ".";
|
||||
}
|
||||
}
|
||||
section article .right {
|
||||
float: right;
|
||||
background: linear-gradient(to bottom, #ebf1ff, #e8efff);
|
||||
color: #455d8a;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
section textarea {
|
||||
font-size: 0.8em;
|
||||
width: 95%;
|
||||
height: 6em;
|
||||
resize: none;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
margin: 0 auto 2vh auto;
|
||||
font-family: "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background: #f6f6f6;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ededed;
|
||||
}
|
||||
section textarea::placeholder {
|
||||
color: #d1d1d1;
|
||||
}
|
||||
section textarea:focus {
|
||||
outline: #40a9ff solid 1px;
|
||||
}
|
||||
section .buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
section .buttons button {
|
||||
font-size: 0.8em;
|
||||
border-radius: 10px;
|
||||
margin: 0 0.5em;
|
||||
padding: 10px 30px;
|
||||
outline: none;
|
||||
background: #fff;
|
||||
color: #828282;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.5s ease-in-out;
|
||||
border: 1px solid #e6e6e6;
|
||||
}
|
||||
section .buttons button:active {
|
||||
box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.08);
|
||||
transform: scale(1.3);
|
||||
}
|
||||
section .buttons #reload {
|
||||
background: #d06968;
|
||||
color: white;
|
||||
}
|
||||
section .buttons #reload:disabled {
|
||||
background: #f6d1d0;
|
||||
color: white;
|
||||
pointer-events: none;
|
||||
}
|
||||
section .buttons #submit {
|
||||
background: #eff4e6;
|
||||
color: #7bb234;
|
||||
font-weight: bold;
|
||||
}
|
||||
section .buttons #submit:disabled {
|
||||
background: #fafcf7;
|
||||
color: #dae9c0;
|
||||
pointer-events: none;
|
||||
}
|
||||
section article > pre,
|
||||
section article p {
|
||||
margin: 0;
|
||||
white-space: break-spaces;
|
||||
word-break: break-all;
|
||||
}
|
||||
section article p > * {
|
||||
margin: inherit;
|
||||
white-space: inherit;
|
||||
word-break: inherit;
|
||||
}
|
||||
section article > pre {
|
||||
background: #fafafa;
|
||||
padding: 1em;
|
||||
border-radius: 15px;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
section article a {
|
||||
color: #f6bd60;
|
||||
}
|
||||
section .toolbar {
|
||||
width: 90%;
|
||||
font-size: 0.6em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 1.5vh 0;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
section .toolbar span {
|
||||
color: #828282;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
section .toolbar span:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.toolbar .checkbox {
|
||||
font-size: 0.5em;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 5.5em;
|
||||
height: 3em;
|
||||
border-radius: 1.5em;
|
||||
transition: 350ms;
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.07), rgba(255, 255, 255, 0)), #ddd;
|
||||
box-shadow: 0 0.07em 0.1em -0.1em rgba(0, 0, 0, 0.4) inset, 0 0.05em 0.08em -0.01em rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.toolbar .checkbox::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
border-radius: 50%;
|
||||
transition: 250ms ease-in-out;
|
||||
background: linear-gradient(#f5f5f5 10%, #eeeeee);
|
||||
box-shadow: 0 0.1em 0.15em -0.05em rgba(255, 255, 255, 0.9) inset, 0 0.5em 0.3em -0.1em rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.toolbar .checkbox::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
top: 1em;
|
||||
left: 6em;
|
||||
border-radius: 50%;
|
||||
transition: 250ms ease-in;
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.07), rgba(255, 255, 255, 0.1)), #ddd;
|
||||
box-shadow: 0 0.08em 0.15em -0.1em rgba(0, 0, 0, 0.5) inset, 0 0.05em 0.08em -0.01em rgba(255, 255, 255, 0.7), -7.25em 0 0 -0.25em rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.toolbar input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
.toolbar input[type="checkbox"].active + .checkbox::after {
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.07), rgba(255, 255, 255, 0.1)), #4c6;
|
||||
box-shadow: 0 0.08em 0.15em -0.1em rgba(0, 0, 0, 0.5) inset, 0 0.05em 0.08em -0.01em rgba(255, 255, 255, 0.7), -7.25em 0 0 -0.25em rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.toolbar input[type="checkbox"].active + .checkbox::before {
|
||||
left: 3em;
|
||||
}
|
||||
section .toolbar dfn {
|
||||
margin-left: 1em;
|
||||
font-style: normal;
|
||||
}
|
||||
.toolbar .creativity {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.toolbar .creativity input {
|
||||
width: 4em;
|
||||
margin: 0 0.5em;
|
||||
background: #ededed;
|
||||
height: 0.3em;
|
||||
}
|
||||
/* 修改滑块样式 */
|
||||
.toolbar .creativity input::-webkit-slider-thumb {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(#f5f5f5 10%, #eeeeee);
|
||||
box-shadow: 0 0.1em 0.15em -0.05em rgba(255, 255, 255, 0.9) inset, 0 0.5em 0.3em -0.1em rgba(0, 0, 0, 0.25);
|
||||
color: #fff;
|
||||
}
|
||||
.toolbar .creativity dfn {
|
||||
margin-left: 0;
|
||||
}
|
||||
1
www/icon/icon-copy.svg
Normal file
1
www/icon/icon-copy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1688674648755" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2279" width="200" height="200"><path d="M377 432h349a8 8 0 0 1 8 8v48a8 8 0 0 1-8 8H377a8 8 0 0 1-8-8v-48a8 8 0 0 1 8-8z m0 160h258a8 8 0 0 1 8 8v48a8 8 0 0 1-8 8H377a8 8 0 0 1-8-8v-48a8 8 0 0 1 8-8z m-65-280v576h480V312H312z m-40-72h560c17.673 0 32 14.327 32 32v656c0 17.673-14.327 32-32 32H272c-17.673 0-32-14.327-32-32V272c0-17.673 14.327-32 32-32z m-88-56v664a8 8 0 0 1-8 8h-56a8 8 0 0 1-8-8V144c0-17.673 14.327-32 32-32h632a8 8 0 0 1 8 8v56a8 8 0 0 1-8 8H184z" fill="#707070" p-id="2280"></path></svg>
|
||||
|
After Width: | Height: | Size: 621 B |
1
www/icon/icon-favo.svg
Normal file
1
www/icon/icon-favo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1689094885756" class="icon" viewBox="0 0 1065 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5304" width="200" height="200"><path d="M964.340451 303.311757l-209.677811-30.424833a32.072845 32.072845 0 0 1-24.086326-17.62105l-93.683133-190.155209a116.755298 116.755298 0 0 0-209.424271 0l-93.683133 190.155209a32.199615 32.199615 0 0 1-24.213096 17.494279L100.02164 303.311757a116.755298 116.755298 0 0 0-64.652771 199.155889l152.124167 147.813983a31.946075 31.946075 0 0 1 9.25422 28.396511L160.364226 887.7221A116.501758 116.501758 0 0 0 329.602363 1010.435595l187.619806-98.500398a32.072845 32.072845 0 0 1 29.917753 0l187.366266 98.500398a115.994678 115.994678 0 0 0 122.967036-8.87391 115.994678 115.994678 0 0 0 46.397871-114.093125l-35.74918-208.79042a31.946075 31.946075 0 0 1 9.25422-28.396511l152.124168-147.813983a116.755298 116.755298 0 0 0-64.652771-199.155889z" fill="#f7a01b" p-id="5305"></path></svg>
|
||||
|
After Width: | Height: | Size: 936 B |
1
www/icon/icon-menu.svg
Normal file
1
www/icon/icon-menu.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1689091803588" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2312" width="200" height="200"><path d="M79.954 686.724h864.092v85.945h-864.092zM79.954 469.027h864.092v85.945h-864.092zM79.954 251.331h864.092v85.943h-864.092z" fill="#2c2c2c" p-id="2313"></path></svg>
|
||||
|
After Width: | Height: | Size: 317 B |
54
www/index.html
Normal file
54
www/index.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
<link rel="stylesheet" href="css/atom-one-light.css" />
|
||||
<script src="js/Pandora.min.js"></script>
|
||||
<script>
|
||||
$("html").AutoSize({ Resize: false });
|
||||
// 运行环境
|
||||
const env = "";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 侧边栏 -->
|
||||
<label class="btn-menu"></label>
|
||||
<menu>
|
||||
<li><a href="javascript:alert('暂未开放');" class="menu_favo">收藏夹</a></li>
|
||||
</menu>
|
||||
<!-- 主体 -->
|
||||
<section>
|
||||
<div class="content">
|
||||
<article></article>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<!-- 保存记录开关 -->
|
||||
<span>
|
||||
<input type="checkbox" id="autoSave" class="active" />
|
||||
<label class="checkbox" for="autoSave"></label>
|
||||
<dfn>记住</dfn>
|
||||
</span>
|
||||
<!-- 流式对话开关 -->
|
||||
<span>
|
||||
<input type="checkbox" id="useStream" class="active" />
|
||||
<label class="checkbox" for="useStream"></label>
|
||||
<dfn>逐字</dfn>
|
||||
</span>
|
||||
</div>
|
||||
<textarea placeholder="请在这里输入你的发问..."></textarea>
|
||||
<div class="buttons">
|
||||
<button id="reload" disabled>清空</button>
|
||||
<button id="submit" disabled>发问</button>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
<script src="js/socket.io.min.js"></script>
|
||||
<script src="js/markdown-it.min.js"></script>
|
||||
<script src="js/highlight.min.js"></script>
|
||||
<script src="cordova.js"></script>
|
||||
<script src="js/index.js"></script>
|
||||
</html>
|
||||
59
www/js/Pandora.min.js
vendored
Normal file
59
www/js/Pandora.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1207
www/js/highlight.min.js
vendored
Normal file
1207
www/js/highlight.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
701
www/js/index.build.js
Normal file
701
www/js/index.build.js
Normal file
@@ -0,0 +1,701 @@
|
||||
// 接口地址
|
||||
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 分享功能
|
||||
648
www/js/index.js
Normal file
648
www/js/index.js
Normal file
@@ -0,0 +1,648 @@
|
||||
// 接口地址
|
||||
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 分享功能
|
||||
3
www/js/markdown-it.min.js
vendored
Normal file
3
www/js/markdown-it.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
www/js/socket.io.min.js
vendored
Normal file
7
www/js/socket.io.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user