|
|
<!DOCTYPE html> |
|
|
<html lang="zh-CN"> |
|
|
|
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" /> |
|
|
<meta name="description" content="Fastllm Web Interface" /> |
|
|
|
|
|
<head> |
|
|
<meta charset="utf-8"> |
|
|
<title>Fastllm Web Interface</title> |
|
|
<style> |
|
|
* { |
|
|
box-sizing: border-box; |
|
|
font-family: system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto, Helvetica,Arial, sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol; |
|
|
} |
|
|
|
|
|
body { |
|
|
background-color: #edeff2; |
|
|
} |
|
|
|
|
|
.chat_window { |
|
|
position: absolute; |
|
|
width: calc(100% - 6px); |
|
|
max-width: 1100px; |
|
|
height: calc(100% - 6px); |
|
|
max-height: 888px; |
|
|
border-radius: 8px; |
|
|
background-color: #fff; |
|
|
left: 50%; |
|
|
top: 50%; |
|
|
overflow: hidden; |
|
|
transform: translate(-50%, -50%); |
|
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15); |
|
|
} |
|
|
|
|
|
.top_menu { |
|
|
background-color: #f6f6f6; |
|
|
width: 100%; |
|
|
height: 50px; |
|
|
padding: 12px 0; |
|
|
} |
|
|
|
|
|
.top_menu .buttons { |
|
|
margin: 5px 0 0 20px; |
|
|
position: absolute; |
|
|
font-size: 0; |
|
|
} |
|
|
|
|
|
.top_menu .buttons .button { |
|
|
width: 16px; |
|
|
height: 16px; |
|
|
border-radius: 50%; |
|
|
display: inline-block; |
|
|
margin-right: 10px; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.top_menu .buttons .button.close { |
|
|
background-color: #e15b64; |
|
|
} |
|
|
|
|
|
.top_menu .buttons .button.minimize { |
|
|
background-color: #f8b26a; |
|
|
} |
|
|
|
|
|
.top_menu .buttons .button.maximize { |
|
|
background-color: #99c959; |
|
|
} |
|
|
|
|
|
.top_menu .title { |
|
|
text-align: center; |
|
|
color: #909090; |
|
|
font-size: 20px; |
|
|
line-height: 26px; |
|
|
} |
|
|
|
|
|
.messages { |
|
|
position: relative; |
|
|
height: calc(100% - 117px); |
|
|
overflow-x: hidden; |
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
.messages::-webkit-scrollbar { |
|
|
width: 10px; |
|
|
height: 10px; |
|
|
} |
|
|
|
|
|
.messages::-webkit-scrollbar-track { |
|
|
background-clip: padding-box; |
|
|
background: transparent; |
|
|
border: solid transparent; |
|
|
border-width: 1px; |
|
|
} |
|
|
|
|
|
.messages::-webkit-scrollbar-corner { |
|
|
background-color: transparent; |
|
|
} |
|
|
|
|
|
.messages::-webkit-scrollbar-thumb { |
|
|
background-color: rgba(0, 0, 0, 0.1); |
|
|
background-clip: padding-box; |
|
|
border: solid transparent; |
|
|
border-radius: 10px; |
|
|
} |
|
|
|
|
|
.messages::-webkit-scrollbar-thumb:hover { |
|
|
background-color: rgba(0, 0, 0, 0.4); |
|
|
} |
|
|
|
|
|
.messages .message { |
|
|
clear: both; |
|
|
overflow: hidden; |
|
|
margin-bottom: 20px; |
|
|
transition: all 0.5s linear; |
|
|
opacity: 0; |
|
|
} |
|
|
|
|
|
.messages .message.left .avatar { |
|
|
background-color: #f5886e; |
|
|
float: left; |
|
|
} |
|
|
|
|
|
.messages .message.left .text { |
|
|
color: #c48843; |
|
|
} |
|
|
|
|
|
.messages .message.right .avatar { |
|
|
background-color: #fdbf68; |
|
|
float: right; |
|
|
} |
|
|
|
|
|
.messages .message.right .text { |
|
|
color: #45829b; |
|
|
} |
|
|
|
|
|
.messages .message.appeared { |
|
|
opacity: 1; |
|
|
} |
|
|
|
|
|
.messages .message .avatar { |
|
|
width: 60px; |
|
|
height: 60px; |
|
|
border-radius: 50%; |
|
|
display: inline-block; |
|
|
} |
|
|
|
|
|
.messages { |
|
|
font-size: 16px; |
|
|
color: #343541; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
#chatlog { |
|
|
word-wrap: break-word; |
|
|
text-align: start; |
|
|
} |
|
|
|
|
|
#chatlog>div { |
|
|
padding: 18px 25px; |
|
|
} |
|
|
|
|
|
#chatlog .request { |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
#chatlog .response { |
|
|
background: #f7f7f8; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.response .markdown-body { |
|
|
background: #f7f7f8 !important; |
|
|
} |
|
|
|
|
|
#chatlog .markdown-body>pre { |
|
|
overflow-x: auto; |
|
|
padding: 10px; |
|
|
position: relative; |
|
|
background: rgba(180, 180, 180, 0.1); |
|
|
} |
|
|
|
|
|
#chatlog .markdown-body>pre::-webkit-scrollbar { |
|
|
width: 10px; |
|
|
height: 10px; |
|
|
} |
|
|
|
|
|
#chatlog .response>pre::-webkit-scrollbar-track { |
|
|
background-clip: padding-box; |
|
|
background: transparent; |
|
|
border: solid transparent; |
|
|
border-width: 1px; |
|
|
} |
|
|
|
|
|
#chatlog .response>pre::-webkit-scrollbar-corner { |
|
|
background-color: transparent; |
|
|
} |
|
|
|
|
|
#chatlog .response>pre::-webkit-scrollbar-thumb { |
|
|
background-color: rgba(0, 0, 0, 0.1); |
|
|
background-clip: padding-box; |
|
|
border: solid transparent; |
|
|
border-radius: 10px; |
|
|
} |
|
|
|
|
|
#chatlog .response>pre::-webkit-scrollbar-thumb:hover { |
|
|
background-color: rgba(0, 0, 0, 0.4); |
|
|
} |
|
|
|
|
|
.m-mdic-copy-wrapper { |
|
|
position: absolute; |
|
|
top: 5px; |
|
|
right: 16px; |
|
|
} |
|
|
|
|
|
.m-mdic-copy-wrapper span.u-mdic-copy-code_lang { |
|
|
position: absolute; |
|
|
top: 3px; |
|
|
right: calc(100% + 4px); |
|
|
font-family: system-ui; |
|
|
font-size: 12px; |
|
|
line-height: 18px; |
|
|
color: #555; |
|
|
opacity: 0.3; |
|
|
} |
|
|
|
|
|
.m-mdic-copy-wrapper div.u-mdic-copy-notify { |
|
|
position: absolute; |
|
|
top: 0; |
|
|
right: 0; |
|
|
padding: 3px 6px; |
|
|
border: 0; |
|
|
border-radius: 3px; |
|
|
background: none; |
|
|
font-family: system-ui; |
|
|
font-size: 12px; |
|
|
line-height: 18px; |
|
|
color: #555; |
|
|
opacity: 0.3; |
|
|
outline: none; |
|
|
opacity: 1; |
|
|
right: 100%; |
|
|
padding-right: 4px; |
|
|
} |
|
|
|
|
|
.m-mdic-copy-wrapper button.u-mdic-copy-btn { |
|
|
position: relative; |
|
|
top: 0; |
|
|
right: 0; |
|
|
padding: 3px 6px; |
|
|
border: 0; |
|
|
border-radius: 3px; |
|
|
background: none; |
|
|
font-family: system-ui; |
|
|
font-size: 12px; |
|
|
line-height: 18px; |
|
|
color: #555; |
|
|
opacity: 0.3; |
|
|
outline: none; |
|
|
cursor: pointer; |
|
|
transition: opacity 0.2s; |
|
|
} |
|
|
|
|
|
.m-mdic-copy-wrapper button.u-mdic-copy-btn:hover { |
|
|
opacity: 1; |
|
|
} |
|
|
|
|
|
#stopChat { |
|
|
display: none; |
|
|
margin: 0 auto; |
|
|
margin-top: 3px; |
|
|
width: 80px; |
|
|
height: 32px; |
|
|
text-align: center; |
|
|
line-height: 32px; |
|
|
color: white; |
|
|
background: #f8b26a; |
|
|
cursor: pointer; |
|
|
border-radius: 3px; |
|
|
position: sticky; |
|
|
bottom: 2px; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
#stopChat>svg { |
|
|
margin-right: 8px; |
|
|
} |
|
|
|
|
|
#stopChat:hover { |
|
|
background: #f0aa60; |
|
|
} |
|
|
|
|
|
.bottom_wrapper { |
|
|
position: relative; |
|
|
width: 100%; |
|
|
background-color: #fff; |
|
|
padding: 10px 10px; |
|
|
position: absolute; |
|
|
bottom: 0; |
|
|
} |
|
|
|
|
|
.bottom_wrapper .message_input_wrapper { |
|
|
border: none; |
|
|
width: calc(100% - 143px); |
|
|
position: relative; |
|
|
text-align: left; |
|
|
} |
|
|
|
|
|
.bottom_wrapper .message_input_wrapper .message_input_text { |
|
|
border-radius: 4px; |
|
|
border: none; |
|
|
outline: none; |
|
|
resize: none; |
|
|
background: #eff1f1; |
|
|
color: #24292f; |
|
|
height: 47px; |
|
|
font-size: 16px; |
|
|
max-height: 200px; |
|
|
padding: 13px 0 13px 16px; |
|
|
width: 100%; |
|
|
display: block; |
|
|
transition: background 0.3s; |
|
|
} |
|
|
|
|
|
.bottom_wrapper .message_input_wrapper .message_input_text:focus { |
|
|
background: #e7e9eb; |
|
|
} |
|
|
|
|
|
.bottom_wrapper .message_input_wrapper .message_input_text::-webkit-scrollbar { |
|
|
display: none; |
|
|
width: 0; |
|
|
height: 0; |
|
|
} |
|
|
|
|
|
#sendbutton { |
|
|
width: 80px; |
|
|
height: 47px; |
|
|
font-size: 18px; |
|
|
font-weight: bold; |
|
|
border-radius: 3px; |
|
|
background-color: #b8da8b; |
|
|
border: none; |
|
|
padding: 0; |
|
|
color: #fff; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s linear; |
|
|
text-align: center; |
|
|
float: right; |
|
|
position: absolute; |
|
|
right: 65px; |
|
|
bottom: 10px; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
.activeSendBtn { |
|
|
background-color: #99c959 !important; |
|
|
cursor: pointer !important; |
|
|
} |
|
|
|
|
|
.activeSendBtn:hover { |
|
|
background-color: #90c050 !important; |
|
|
} |
|
|
|
|
|
#clearConv { |
|
|
position: absolute; |
|
|
right: 10px; |
|
|
bottom: 10px; |
|
|
width: 47px; |
|
|
height: 47px; |
|
|
font-size: 16px; |
|
|
display: inline-block; |
|
|
border-radius: 3px; |
|
|
background-color: #909090; |
|
|
border: 2px solid #909090; |
|
|
color: #fff; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s linear; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
#clearConv:hover { |
|
|
background-color: gray; |
|
|
border: 2px solid gray; |
|
|
} |
|
|
|
|
|
.loaded>span { |
|
|
display: inline-block; |
|
|
} |
|
|
|
|
|
.loaded>svg { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.loading { |
|
|
background: #e7e7e8 !important; |
|
|
} |
|
|
|
|
|
.loading>span { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.loading>svg { |
|
|
display: block; |
|
|
} |
|
|
|
|
|
.switch-slide { |
|
|
display: inline-block; |
|
|
vertical-align: middle; |
|
|
} |
|
|
|
|
|
.switch-slide-label { |
|
|
display: block; |
|
|
width: 38px; |
|
|
height: 18px; |
|
|
background: #909090; |
|
|
border-radius: 30px; |
|
|
cursor: pointer; |
|
|
position: relative; |
|
|
-webkit-transition: 0.3s ease; |
|
|
transition: 0.3s ease; |
|
|
} |
|
|
|
|
|
.switch-slide-label:after { |
|
|
content: ""; |
|
|
display: block; |
|
|
width: 16px; |
|
|
height: 16px; |
|
|
border-radius: 100%; |
|
|
background: #fff; |
|
|
box-shadow: 0 1px 1px rgba(0, 0, 0, .1); |
|
|
position: absolute; |
|
|
left: 1px; |
|
|
top: 1px; |
|
|
-webkit-transform: translateZ(0); |
|
|
transform: translateZ(0); |
|
|
-webkit-transition: 0.3s ease; |
|
|
transition: 0.3s ease; |
|
|
} |
|
|
|
|
|
.switch-slide input:checked+label { |
|
|
background: #99c959; |
|
|
transition: 0.3s ease; |
|
|
} |
|
|
|
|
|
.switch-slide input:checked+label:after { |
|
|
left: 21px; |
|
|
} |
|
|
|
|
|
#setting { |
|
|
position: absolute; |
|
|
right: 15px; |
|
|
top: 5px; |
|
|
cursor: pointer; |
|
|
padding: 5px; |
|
|
border: none; |
|
|
background-color: transparent; |
|
|
border-radius: 4px; |
|
|
} |
|
|
|
|
|
#setting:hover { |
|
|
background: #e7e7e8; |
|
|
} |
|
|
|
|
|
.showSetting { |
|
|
background: #b0b0b0 !important; |
|
|
} |
|
|
|
|
|
#setDialog { |
|
|
color: #303030; |
|
|
position: absolute; |
|
|
z-index: 2; |
|
|
background: #f4f4f4; |
|
|
width: 320px; |
|
|
right: 6px; |
|
|
top: 46px; |
|
|
-webkit-user-select: none; |
|
|
user-select: none; |
|
|
border-radius: 5px; |
|
|
padding: 8px 12px 8px 12px; |
|
|
} |
|
|
|
|
|
#setDialog>div { |
|
|
margin-bottom: 4px; |
|
|
} |
|
|
|
|
|
#setDialog input { |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
#setDialog .inlineTitle { |
|
|
display: inline-block; |
|
|
line-height: 16px; |
|
|
vertical-align: middle; |
|
|
} |
|
|
|
|
|
#speechOption { |
|
|
margin-top: 6px; |
|
|
} |
|
|
|
|
|
#speechOption>div { |
|
|
margin-bottom: 4px; |
|
|
} |
|
|
|
|
|
.inputTextClass { |
|
|
outline: none; |
|
|
border-radius: 2px; |
|
|
margin-top: 2px; |
|
|
height: 32px; |
|
|
font-size: 15px; |
|
|
padding-left: 6px; |
|
|
background: white; |
|
|
border: none; |
|
|
} |
|
|
|
|
|
.areaTextClass { |
|
|
width: 100%; |
|
|
height: 80px; |
|
|
display: block; |
|
|
resize: none; |
|
|
padding: 6px; |
|
|
} |
|
|
|
|
|
input[type="range"] { |
|
|
-webkit-appearance: none; |
|
|
display: block; |
|
|
margin: 6px 0 3px 0; |
|
|
height: 8px; |
|
|
background: #909090; |
|
|
border-radius: 5px; |
|
|
background-image: linear-gradient(#99c959, #99c959); |
|
|
background-size: 100% 100%; |
|
|
background-repeat: no-repeat; |
|
|
} |
|
|
|
|
|
input[type="range"]::-webkit-slider-thumb { |
|
|
-webkit-appearance: none; |
|
|
height: 15px; |
|
|
width: 15px; |
|
|
border-radius: 50%; |
|
|
background: #99c959; |
|
|
cursor: ew-resize; |
|
|
box-shadow: 0 0 2px 0 #555; |
|
|
} |
|
|
|
|
|
input[type=range]::-webkit-slider-runnable-track { |
|
|
-webkit-appearance: none; |
|
|
box-shadow: none; |
|
|
border: none; |
|
|
background: transparent; |
|
|
} |
|
|
|
|
|
.justSetLine { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
} |
|
|
|
|
|
.presetSelect>div { |
|
|
display: inline-block; |
|
|
} |
|
|
|
|
|
.presetSelect select { |
|
|
outline: none; |
|
|
border-radius: 3px; |
|
|
width: 100px; |
|
|
border-color: rgba(0, 0, 0, .3); |
|
|
} |
|
|
|
|
|
.selectDef { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
font-size: 13px; |
|
|
color: #707070; |
|
|
} |
|
|
|
|
|
#preSetSpeech { |
|
|
width: 100%; |
|
|
outline: none; |
|
|
margin-top: 5px; |
|
|
border-radius: 3px; |
|
|
border-color: rgba(0, 0, 0, .3); |
|
|
} |
|
|
|
|
|
.mdOption { |
|
|
position: absolute; |
|
|
right: 5px; |
|
|
bottom: 0px; |
|
|
pointer-events: none; |
|
|
} |
|
|
|
|
|
.mdOption>div { |
|
|
margin-top: 4px; |
|
|
pointer-events: auto; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.mdOption>div>svg { |
|
|
color: #e3e3e3; |
|
|
display: block; |
|
|
} |
|
|
|
|
|
.mdOption svg:hover { |
|
|
color: #989898; |
|
|
} |
|
|
|
|
|
.mdOption svg * { |
|
|
pointer-events: none; |
|
|
} |
|
|
|
|
|
.refreshReq svg:not(:first-child) { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.halfRefReq svg:not(:nth-child(2)) { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.moreOption { |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.optionItems { |
|
|
position: absolute; |
|
|
top: -8px; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
visibility: hidden; |
|
|
z-index: 1; |
|
|
color: #808080; |
|
|
} |
|
|
|
|
|
.moreOptionHidden>div { |
|
|
display: none !important; |
|
|
} |
|
|
|
|
|
.optionItems:hover { |
|
|
visibility: visible; |
|
|
} |
|
|
|
|
|
.optionItems:hover .optionItem { |
|
|
transform: scale(1); |
|
|
visibility: visible; |
|
|
} |
|
|
|
|
|
.optionTrigger:hover+.optionItems .optionItem:nth-of-type(3) { |
|
|
transform: scale(1); |
|
|
visibility: visible; |
|
|
transition-delay: 50ms; |
|
|
transition-duration: 60ms; |
|
|
} |
|
|
|
|
|
.optionTrigger:hover+.optionItems .optionItem:nth-of-type(2) { |
|
|
visibility: visible; |
|
|
transition-delay: 80ms; |
|
|
transform: scale(1); |
|
|
transition-duration: 60ms; |
|
|
} |
|
|
|
|
|
.optionTrigger:hover+.optionItems .optionItem:nth-of-type(1) { |
|
|
visibility: visible; |
|
|
transition-delay: 110ms; |
|
|
transform: scale(1); |
|
|
transition-duration: 60ms; |
|
|
} |
|
|
|
|
|
.optionItem { |
|
|
border-radius: 3px; |
|
|
height: 30px; |
|
|
width: 30px; |
|
|
background-color: #f7f7f8; |
|
|
visibility: hidden; |
|
|
transform: scale(0); |
|
|
display: flex !important; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.optionItem * { |
|
|
pointer-events: none; |
|
|
} |
|
|
|
|
|
.optionItem:hover { |
|
|
background: #e0e0e0; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
|
|
|
<body> |
|
|
<div> |
|
|
<div class="chat_window"> |
|
|
<div class="top_menu"> |
|
|
<div class="buttons"> |
|
|
<div class="button close"></div> |
|
|
<div class="button minimize"></div> |
|
|
<div class="button maximize"></div> |
|
|
</div> |
|
|
<div class="title">Fastllm</div> |
|
|
</div> |
|
|
<div class="messages"> |
|
|
<div id="chatlog"></div> |
|
|
<div id="stopChat"><svg width="24" height="24"> |
|
|
<use xlink:href="#stopResIcon" /> |
|
|
</svg>停止</div> |
|
|
</div> |
|
|
<div class="bottom_wrapper clearfix"> |
|
|
<div class="message_input_wrapper"> |
|
|
<textarea class="message_input_text" spellcheck="false" placeholder="来问点什么吧" |
|
|
id="chatinput"></textarea> |
|
|
</div> |
|
|
<button class="loaded" id="sendbutton"> |
|
|
<span>发送</span> |
|
|
<svg style="margin:0 auto;height:40px;" viewBox="0 0 200 100" preserveAspectRatio="xMidYMid"> |
|
|
<g transform="translate(50 50)"> |
|
|
<circle cx="0" cy="0" r="15" fill="#e15b64"> |
|
|
<animateTransform attributeName="transform" type="scale" begin="-0.3333333333333333s" |
|
|
calcMode="spline" keySplines="0.3 0 0.7 1;0.3 0 0.7 1" values="0;1;0" |
|
|
keyTimes="0;0.5;1" dur="1s" repeatCount="indefinite"></animateTransform> |
|
|
</circle> |
|
|
</g> |
|
|
<g transform="translate(100 50)"> |
|
|
<circle cx="0" cy="0" r="15" fill="#f8b26a"> |
|
|
<animateTransform attributeName="transform" type="scale" begin="-0.16666666666666666s" |
|
|
calcMode="spline" keySplines="0.3 0 0.7 1;0.3 0 0.7 1" values="0;1;0" |
|
|
keyTimes="0;0.5;1" dur="1s" repeatCount="indefinite"></animateTransform> |
|
|
</circle> |
|
|
</g> |
|
|
<g transform="translate(150 50)"> |
|
|
<circle cx="0" cy="0" r="15" fill="#99c959"> |
|
|
<animateTransform attributeName="transform" type="scale" begin="0s" calcMode="spline" |
|
|
keySplines="0.3 0 0.7 1;0.3 0 0.7 1" values="0;1;0" keyTimes="0;0.5;1" dur="1s" |
|
|
repeatCount="indefinite"></animateTransform> |
|
|
</circle> |
|
|
</g> |
|
|
</svg> |
|
|
</button> |
|
|
<button id="clearConv"><svg style="margin:0 auto;display:block" role="img" width="20px" height="20px" |
|
|
viewBox="0 0 24 24"> |
|
|
<title>失忆</title> |
|
|
<path fill="currentColor" |
|
|
d="M8 20v-5h2v5h9v-7H5v7h3zm-4-9h16V8h-6V4h-4v4H4v3zM3 21v-8H2V7a1 1 0 0 1 1-1h5V3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v3h5a1 1 0 0 1 1 1v6h-1v8a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"> |
|
|
</path> |
|
|
</svg></button> |
|
|
</div> |
|
|
</div> |
|
|
<div style="display: none"> |
|
|
<svg> |
|
|
<symbol viewBox="0 0 24 24" id="optionIcon"> |
|
|
<path fill="currentColor" |
|
|
d="M12 3c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2zm0 14c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2zm0-7c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2z"> |
|
|
</path> |
|
|
</symbol> |
|
|
<symbol viewBox="0 0 24 24" id="refreshIcon"> |
|
|
<path fill="currentColor" |
|
|
d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"> |
|
|
</path> |
|
|
</symbol> |
|
|
<symbol viewBox="0 0 24 24" id="halfRefIcon"> |
|
|
<path fill="currentColor" |
|
|
d="M 4.009 12.163 C 4.012 12.206 2.02 12.329 2 12.098 C 2 6.575 6.477 2 12 2 C 17.523 2 22 6.477 22 12 C 22 14.136 21.33 16.116 20.19 17.74 L 17 12 L 20 12 C 19.999 5.842 13.333 1.993 7.999 5.073 C 3.211 8.343 4.374 12.389 4.009 12.163 Z" /> |
|
|
</symbol> |
|
|
<symbol viewBox="-2 -2 20 20" id="copyIcon"> |
|
|
<path fill="currentColor" |
|
|
d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"> |
|
|
</path> |
|
|
<path fill="currentColor" |
|
|
d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"> |
|
|
</path> |
|
|
</symbol> |
|
|
<symbol viewBox="0 0 24 24" id="delIcon"> |
|
|
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" |
|
|
stroke-width="2" |
|
|
d="M9 7v0a3 3 0 0 1 3-3v0a3 3 0 0 1 3 3v0M9 7h6M9 7H6m9 0h3m2 0h-2M4 7h2m0 0v11a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7"> |
|
|
</path> |
|
|
</symbol> |
|
|
</svg> |
|
|
</div> |
|
|
</div> |
|
|
<script> |
|
|
const messagsEle = document.getElementsByClassName("messages")[0]; |
|
|
const chatlog = document.getElementById("chatlog"); |
|
|
const stopEle = document.getElementById("stopChat"); |
|
|
const sendBtnEle = document.getElementById("sendbutton"); |
|
|
const textarea = document.getElementById("chatinput"); |
|
|
const dialogEle = document.getElementById("setDialog"); |
|
|
const speechOptionEle = document.getElementById("speechOption"); |
|
|
textarea.focus(); |
|
|
textarea.oninput = (e) => { |
|
|
if (textarea.value.trim().length) { |
|
|
sendBtnEle.classList.add("activeSendBtn"); |
|
|
} else { |
|
|
sendBtnEle.classList.remove("activeSendBtn"); |
|
|
} |
|
|
textarea.style.height = "47px"; |
|
|
textarea.style.height = textarea.scrollHeight + "px"; |
|
|
}; |
|
|
</script> |
|
|
<script src="js/markdown-it.min.js"></script> |
|
|
<script src="js/highlight.min.js"></script> |
|
|
<script src="js/katex.min.js"></script> |
|
|
<script src="js/texmath.js"></script> |
|
|
<script src="js/markdown-it-link-attributes.min.js"></script> |
|
|
<script> |
|
|
const API_URL = "chat"; |
|
|
let data = []; |
|
|
let input_str; |
|
|
let uuid = ""; |
|
|
let loading = false; |
|
|
const scrollToBottom = () => { |
|
|
if (messagsEle.scrollHeight - messagsEle.scrollTop - messagsEle.clientHeight < 128) { |
|
|
messagsEle.scrollTo(0, messagsEle.scrollHeight) |
|
|
} |
|
|
} |
|
|
const scrollToBottomLoad = (ele) => { |
|
|
if (messagsEle.scrollHeight - messagsEle.scrollTop - messagsEle.clientHeight < ele.clientHeight + 128) { |
|
|
messagsEle.scrollTo(0, messagsEle.scrollHeight) |
|
|
} |
|
|
} |
|
|
const closeEvent = (ev) => { |
|
|
if (!dialogEle.contains(ev.target)) { |
|
|
dialogEle.style.display = "none"; |
|
|
document.removeEventListener("mousedown", closeEvent, true); |
|
|
} |
|
|
} |
|
|
const md = markdownit({ |
|
|
linkify: true, |
|
|
highlight: function (str, lang) { |
|
|
try { |
|
|
return hljs.highlightAuto(str).value; |
|
|
} catch (e) { } |
|
|
return ""; |
|
|
} |
|
|
}); |
|
|
md.use(texmath, {engine: katex, delimiters: "dollars", katexOptions: {macros: {"\\RR": "\\mathbb{R}"}}}) |
|
|
.use(markdownitLinkAttributes, {attrs: {target: "_blank", rel: "noopener"}}); |
|
|
const x = { |
|
|
getCodeLang(str = "") { |
|
|
const res = str.match(/ class="language-(.*?)"/); |
|
|
return (res && res[1]) || ""; |
|
|
}, |
|
|
getFragment(str = "") { |
|
|
return str ? `<span class="u-mdic-copy-code_lang">${str}</span>` : ""; |
|
|
}, |
|
|
}; |
|
|
const strEncode = (str = "") => { |
|
|
if (!str || str.length === 0) return ""; |
|
|
return str |
|
|
.replace(/</g, '<') |
|
|
.replace(/>/g, '>') |
|
|
.replace(/'/g, '\'') |
|
|
.replace(/"/g, '"'); |
|
|
}; |
|
|
const getCodeLangFragment = (oriStr = "") => { |
|
|
return x.getFragment(x.getCodeLang(oriStr)); |
|
|
}; |
|
|
const copyClickCode = (ele) => { |
|
|
const input = document.createElement("textarea"); |
|
|
input.value = ele.dataset.mdicContent; |
|
|
const nDom = ele.previousElementSibling; |
|
|
const nDelay = ele.dataset.mdicNotifyDelay; |
|
|
const cDom = nDom.previousElementSibling; |
|
|
document.body.appendChild(input); |
|
|
input.select(); |
|
|
input.setSelectionRange(0, 9999); |
|
|
document.execCommand("copy"); |
|
|
document.body.removeChild(input); |
|
|
if (nDom.style.display === "none") { |
|
|
nDom.style.display = "block"; |
|
|
cDom && (cDom.style.display = "none"); |
|
|
setTimeout(() => { |
|
|
nDom.style.display = "none"; |
|
|
cDom && (cDom.style.display = "block"); |
|
|
}, nDelay); |
|
|
} |
|
|
}; |
|
|
const copyClickMd = (idx) => { |
|
|
const input = document.createElement("textarea"); |
|
|
input.value = data[idx].content; |
|
|
document.body.appendChild(input); |
|
|
input.select(); |
|
|
input.setSelectionRange(0, 9999); |
|
|
document.execCommand("copy"); |
|
|
document.body.removeChild(input); |
|
|
} |
|
|
const enhanceCode = (render, options = {}) => (...args) => { |
|
|
|
|
|
const { |
|
|
btnText = "复制代码", |
|
|
successText = "复制成功", |
|
|
successTextDelay = 2000, |
|
|
showCodeLanguage = true, |
|
|
} = options; |
|
|
const [tokens = {}, idx = 0] = args; |
|
|
const cont = strEncode(tokens[idx].content || ""); |
|
|
const originResult = render.apply(this, args); |
|
|
const langFrag = showCodeLanguage ? getCodeLangFragment(originResult) : ""; |
|
|
const tpls = [ |
|
|
'<div class="m-mdic-copy-wrapper">', |
|
|
`${langFrag}`, |
|
|
`<div class="u-mdic-copy-notify" style="display:none;">${successText}</div>`, |
|
|
'<button ', |
|
|
'class="u-mdic-copy-btn j-mdic-copy-btn" ', |
|
|
`data-mdic-content="${cont}" `, |
|
|
`data-mdic-notify-delay="${successTextDelay}" `, |
|
|
`onclick="copyClickCode(this)">${btnText}</button>`, |
|
|
'</div>', |
|
|
]; |
|
|
const LAST_TAG = "</pre>"; |
|
|
const newResult = originResult.replace(LAST_TAG, `${tpls.join("")}${LAST_TAG}`); |
|
|
return newResult; |
|
|
}; |
|
|
const codeBlockRender = md.renderer.rules.code_block; |
|
|
const fenceRender = md.renderer.rules.fence; |
|
|
md.renderer.rules.code_block = enhanceCode(codeBlockRender); |
|
|
md.renderer.rules.fence = enhanceCode(fenceRender); |
|
|
md.renderer.rules.image = function (tokens, idx, options, env, slf) { |
|
|
var token = tokens[idx]; |
|
|
token.attrs[token.attrIndex("alt")][1] = slf.renderInlineAsText(token.children, options, env); |
|
|
token.attrSet("onload", "scrollToBottomLoad(this);this.removeAttribute('onload')"); |
|
|
return slf.renderToken(tokens, idx, options) |
|
|
} |
|
|
let delayId; |
|
|
const delay = () => { |
|
|
return new Promise((resolve) => delayId = setTimeout(resolve, 100)); |
|
|
} |
|
|
const confirmAction = (prompt) => { |
|
|
if (window.confirm(prompt)) { |
|
|
return true; |
|
|
} |
|
|
else { |
|
|
return false; |
|
|
} |
|
|
} |
|
|
const mdOptionEvent = (ev) => { |
|
|
let id = ev.target.dataset.id; |
|
|
if (id) { |
|
|
let parent = ev.target.parentElement; |
|
|
let idxEle = parent.parentElement; |
|
|
let idx = Array.prototype.indexOf.call(chatlog.children, (id === "refreshMd") ? idxEle.parentElement : idxEle.parentElement.parentElement); |
|
|
if (id === "refreshMd") { |
|
|
if (!loading && chatlog.children[idx].dataset.loading !== "true") { |
|
|
let className = parent.className; |
|
|
if (className == "refreshReq") { |
|
|
chatlog.children[idx].children[0].innerHTML = "<br />"; |
|
|
chatlog.children[idx].dataset.loading = true; |
|
|
data[idx].content = ""; |
|
|
if (idx === currentVoiceIdx) {endSpeak()}; |
|
|
loadAction(true); |
|
|
refreshIdx = idx; |
|
|
streamGen(); |
|
|
} else { |
|
|
chatlog.children[idx].dataset.loading = true; |
|
|
progressData = data[idx].content; |
|
|
loadAction(true); |
|
|
refreshIdx = idx; |
|
|
streamGen(); |
|
|
} |
|
|
} |
|
|
} else if (id === "copyMd") { |
|
|
idxEle.classList.add("moreOptionHidden"); |
|
|
copyClickMd(idx); |
|
|
} else if (id === "delMd") { |
|
|
idxEle.classList.add("moreOptionHidden"); |
|
|
if (!loading) { |
|
|
if (confirmAction("是否删除此消息?")) { |
|
|
if (currentVoiceIdx) { |
|
|
if (currentVoiceIdx === idx) {endSpeak()} |
|
|
else if (currentVoiceIdx > idx) {currentVoiceIdx -= 1} |
|
|
} |
|
|
chatlog.removeChild(chatlog.children[idx]); |
|
|
data.splice(idx, 1); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
const moreOption = (ele) => { |
|
|
ele.classList.remove("moreOptionHidden"); |
|
|
} |
|
|
const formatMdEle = (ele) => { |
|
|
let realMd = document.createElement("div"); |
|
|
realMd.className = "markdown-body"; |
|
|
ele.appendChild(realMd); |
|
|
let mdOption = document.createElement("div"); |
|
|
mdOption.className = "mdOption"; |
|
|
ele.appendChild(mdOption); |
|
|
if (ele.className !== "request") { |
|
|
mdOption.innerHTML = `<div class="refreshReq"> |
|
|
<svg data-id="refreshMd" width="16" height="16" role="img"><title>刷新</title><use xlink:href="#refreshIcon" /></svg> |
|
|
<svg data-id="refreshMd" width="16" height="16" role="img"><title>继续</title><use xlink:href="#halfRefIcon" /></svg> |
|
|
</div>` |
|
|
} |
|
|
mdOption.innerHTML += `<div class="moreOption" onmouseenter="moreOption(this)"> |
|
|
<svg class="optionTrigger" width="16" height="16" role="img"><title>选项</title><use xlink:href="#optionIcon" /></svg> |
|
|
<div class="optionItems" style="width:63px;left:-63px">` + `<div data-id="delMd" class="optionItem" title="删除"> |
|
|
<svg width="20" height="20"><use xlink:href="#delIcon" /></svg> |
|
|
</div> |
|
|
<div data-id="copyMd" class="optionItem" title="复制"> |
|
|
<svg width="20" height="20"><use xlink:href="#copyIcon" /></svg> |
|
|
</div></div></div>`; |
|
|
mdOption.onclick = mdOptionEvent; |
|
|
} |
|
|
let controller; |
|
|
let controllerId; |
|
|
let refreshIdx; |
|
|
let currentResEle; |
|
|
let progressData = ""; |
|
|
let resStr = ""; |
|
|
const reqWord = async (refresh) => { |
|
|
if (uuid == "") { |
|
|
uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { |
|
|
var r = Math.random() * 16 | 0, |
|
|
v = c == 'x' ? r : (r & 0x3 | 0x8); |
|
|
return v.toString(16); |
|
|
}); |
|
|
} |
|
|
|
|
|
let headers = {"Content-Type": "application/json", "uuid" : uuid}; |
|
|
let idx = refresh ? refreshIdx : data.length; |
|
|
let dataSlice = [data[idx - 1]]; |
|
|
const res = await fetch(API_URL, { |
|
|
method: "POST", |
|
|
headers, |
|
|
body: input_str, |
|
|
signal: controller.signal |
|
|
}); |
|
|
clearTimeout(controllerId); |
|
|
controllerId = void 0; |
|
|
if (res.status !== 200) { |
|
|
alert("请先启动服务!"); |
|
|
stopLoading(); |
|
|
return; |
|
|
} |
|
|
const decoder = new TextDecoder(); |
|
|
const reader = res.body.getReader(); |
|
|
const readChunk = async () => { |
|
|
return reader.read().then(async ({value, done}) => { |
|
|
if (!done) { |
|
|
resStr = decoder.decode(value); |
|
|
let end = resStr.indexOf("<eop>"); |
|
|
value = resStr; |
|
|
if (end > -1) { |
|
|
value = resStr.substr(0, end) |
|
|
} |
|
|
currentResEle.children[0].innerHTML = md.render(value); |
|
|
scrollToBottom(); |
|
|
return; |
|
|
} |
|
|
}); |
|
|
}; |
|
|
await readChunk(); |
|
|
}; |
|
|
const streamGen = async () => { |
|
|
controller = new AbortController(); |
|
|
controllerId = setTimeout(() => { |
|
|
alert("请求超时,请稍后重试!"); |
|
|
stopLoading(); |
|
|
}, 30000); |
|
|
let isRefresh = refreshIdx !== void 0; |
|
|
if (isRefresh) { |
|
|
currentResEle = chatlog.children[refreshIdx]; |
|
|
} else if (!currentResEle) { |
|
|
currentResEle = document.createElement("div"); |
|
|
currentResEle.className = "response"; |
|
|
chatlog.appendChild(currentResEle); |
|
|
formatMdEle(currentResEle); |
|
|
currentResEle.children[0].innerHTML = "<br />"; |
|
|
currentResEle.dataset.loading = true; |
|
|
scrollToBottom(); |
|
|
} |
|
|
resStr = ""; |
|
|
while(true) { |
|
|
await reqWord(isRefresh) |
|
|
await sleep(0) |
|
|
if (resStr.indexOf("<eop>") > -1) { |
|
|
break; |
|
|
} |
|
|
} |
|
|
stopLoading(false); |
|
|
}; |
|
|
const loadAction = (bool) => { |
|
|
loading = bool; |
|
|
sendBtnEle.disabled = bool; |
|
|
sendBtnEle.className = bool ? " loading" : "loaded"; |
|
|
stopEle.style.display = bool ? "flex" : "none"; |
|
|
} |
|
|
const stopLoading = (abort = true) => { |
|
|
stopEle.style.display = "none"; |
|
|
if (abort) { |
|
|
controller.abort(); |
|
|
if (controllerId) clearTimeout(controllerId); |
|
|
if (delayId) clearTimeout(delayId); |
|
|
if (refreshIdx !== void 0) {data[refreshIdx].content = progressData} |
|
|
else if (data[data.length - 1].role === "assistant") {data[data.length - 1].content = progressData} |
|
|
else {data.push({role: "assistant", content: progressData})} |
|
|
} |
|
|
controllerId = delayId = refreshIdx = void 0; |
|
|
currentResEle.dataset.loading = false; |
|
|
currentResEle = null; |
|
|
progressData = ""; |
|
|
loadAction(false); |
|
|
} |
|
|
function sleep(time) { |
|
|
return new Promise(resolve => { |
|
|
setTimeout(() => { |
|
|
resolve(); |
|
|
}, time); |
|
|
}); |
|
|
} |
|
|
const generateText = async (message) => { |
|
|
loadAction(true); |
|
|
let request = document.createElement("div"); |
|
|
request.className = "request"; |
|
|
chatlog.appendChild(request); |
|
|
formatMdEle(request); |
|
|
request.children[0].innerHTML = md.render(message); |
|
|
data.push({role: "user", content: message}); |
|
|
input_str = message; |
|
|
scrollToBottom(); |
|
|
await streamGen(); |
|
|
}; |
|
|
textarea.onkeydown = (e) => { |
|
|
if (e.keyCode === 13) { |
|
|
if (!e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
genFunc(); |
|
|
} |
|
|
} |
|
|
}; |
|
|
const genFunc = function () { |
|
|
let message = textarea.value.trim(); |
|
|
if (message.length !== 0) { |
|
|
if (loading === true) return; |
|
|
textarea.value = ""; |
|
|
textarea.style.height = "47px"; |
|
|
generateText(message); |
|
|
} |
|
|
}; |
|
|
sendBtnEle.onclick = genFunc; |
|
|
stopEle.onclick = stopLoading; |
|
|
document.getElementById("clearConv").onclick = () => { |
|
|
if (!loading) { |
|
|
input_str = "reset"; |
|
|
streamGen(); |
|
|
chatlog.innerHTML = "<div></div>" |
|
|
scrollToBottom(); |
|
|
} |
|
|
} |
|
|
</script> |
|
|
<link href="css/github-markdown-light.min.css" rel="stylesheet"> |
|
|
<link href="css/github.min.css" rel="stylesheet"> |
|
|
<link href="css/katex.min.css" rel="stylesheet"> |
|
|
<link href="css/texmath.css" rel="stylesheet"> |
|
|
</body> |
|
|
</html> |
|
|
|