| <!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> |
|
|