diff options
| author | sigoden <sigoden@gmail.com> | 2024-12-05 18:19:13 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-12-05 18:19:13 +0800 |
| commit | 8dfcbe1643063dcd4911fd8f61aafa7adfb84a04 (patch) | |
| tree | 562e3ad946ac81c0bda91ce3cf10c470d524a966 /assets | |
| parent | 3a3388375be05758d5f5574cce1d0f8eb7bdd604 (diff) | |
| download | aichat-8dfcbe1643063dcd4911fd8f61aafa7adfb84a04.tar.gz | |
feat: webui playground support multiple sessions in memory (#1035)
Diffstat (limited to 'assets')
| -rw-r--r-- | assets/arena.html | 6 | ||||
| -rw-r--r-- | assets/playground.html | 269 |
2 files changed, 248 insertions, 27 deletions
diff --git a/assets/arena.html b/assets/arena.html index e1eec1e..5cfe8f4 100644 --- a/assets/arena.html +++ b/assets/arena.html @@ -847,9 +847,9 @@ const chat = this.chats[index]; const messages = []; for ([userMessage, assistantMessage] of chunkArray(chat.messages, 2)) { - if (assistantMessage.state == "failed") { + if (assistantMessage.state === "failed") { continue; - } else if (assistantMessage.state == "loading") { + } else if (assistantMessage.state === "loading") { messages.push({ role: userMessage.role, content: userMessage.content, @@ -929,7 +929,7 @@ const parsed = JSON.parse(message); yield parsed; } catch { - if (i == lines.length - 1) { + if (i === lines.length - 1) { reamingChunkValue += line; break; } diff --git a/assets/playground.html b/assets/playground.html index 6b541e6..d454ec0 100644 --- a/assets/playground.html +++ b/assets/playground.html @@ -72,6 +72,8 @@ } .sidebar-header { + display: flex; + align-items: center; padding: 1.25rem; } @@ -85,13 +87,21 @@ padding-top: 0.3rem; } + .sidebar-right { + display: flex; + flex-direction: row; + margin-left: auto; + gap: 6px; + } + + .sidebar-btn { + cursor: pointer; + width: 1.2rem; + height: 1.2rem; + } + .hide-sidebar-btn { display: none; - width: 1.5rem; - height: 1.5rem; - position: absolute; - right: 1.5rem; - top: 1.5rem; } .settings { @@ -391,6 +401,78 @@ opacity: 0.3; } + .session-list { + padding-top: 0.4rem; + max-height: 80vh; + font-size: 0.8rem; + overflow-y: auto; + overflow-x: hidden; + } + + .session-item { + padding: 5px; + border-bottom: 1px solid #e0e0e0; + cursor: pointer; + } + + .session-item:hover { + background-color: #f0f0f0; + } + + .session-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 50px; + } + + .modal-content { + position: relative; + padding: 0.8rem; + border-radius: 8px; + max-width: 1000px; + width: calc(100% - 100px); + background-color: white; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + } + + .modal-header { + display: flex; + flex-direction: row; + align-items: center; + } + + .modal-header .title { + font-weight: 500; + font-size: 1.5rem; + } + + .modal-header .close-btn { + margin-left: auto; + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #999; + } + + .modal-close-btn:hover { + color: #333; + } + .spinner { width: 1.1rem; height: 1.1rem; @@ -491,13 +573,35 @@ <div class="container" x-data="app"> <div class="sidebar" x-ref="sidebar"> <div class="sidebar-header"> - <div class="title">AIChat</div> - <div class="subtitle">All-in-one AI-Powered Chat & Copilot</div> - <div class="hide-sidebar-btn" @click="handleHideSidebarBtnClick"> - <svg fill="currentColor" viewBox="0 0 16 16"> - <path - d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708" /> - </svg> + <div class="sidebar-left"> + <div class="title">AIChat</div> + <div class="subtitle">All-in-one AI-Powered Chat</div> + </div> + <div class="sidebar-right"> + <div class="sidebar-btn new-chat-btn" title="New Chat (Ctrl/Cmd+Shift+O)" @click="handleNewChat"> + <svg fill="currentColor" viewBox="0 0 16 16"> + <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" /> + <path + d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4" /> + </svg> + </div> + <div class="sidebar-btn list-sessions-btn" title="List Sessions (Ctrl/Cmd+Shift+L)" + @click="showModal = 'list-sessions'"> + <svg fill="currentColor" viewBox="0 0 16 16"> + <path fill-rule="evenodd" + d="M2 2.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5V3a.5.5 0 0 0-.5-.5zM3 3H2v1h1z" /> + <path + d="M5 3.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5M5.5 7a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1zm0 4a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1z" /> + <path fill-rule="evenodd" + d="M1.5 7a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5H2a.5.5 0 0 1-.5-.5zM2 7h1v1H2zm0 3.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm1 .5H2v1h1z" /> + </svg> + </div> + <div class="sidebar-btn hide-sidebar-btn" @click="handleHideSidebarBtnClick"> + <svg fill="currentColor" viewBox="0 0 16 16"> + <path + d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708" /> + </svg> + </div> </div> </div> <div class="settings"> @@ -652,7 +756,7 @@ </div> <div class="input-panel"> <div class="input-panel-inner"> - <textarea id="chat-input" x-model="input" x-ref="input" @keydown.enter="handleEnterKeydown" + <textarea id="chat-input" x-model="input" x-ref="input" @keydown.enter="handleEnterKeyDown" placeholder="Ask Anything" autofocus></textarea> <div class="input-image-bar" x-show="images.length > 0"> <template x-for="(image, index) in images"> @@ -702,6 +806,22 @@ </div> </div> </div> + <div class="modal" x-cloak x-show="showModal == 'list-sessions'" + @click="if ($event.target == $el) { showModal = ''}"> + <div class="modal-content"> + <div class="modal-header"> + <div class="title">Sessions</div> + <button class="close-btn" @click="showModal = ''">×</button> + </div> + <div class="session-list"> + <template x-for="session in sessions" :key="session.id"> + <div class="session-item" @click="handleSelectSession(session.id)"> + <div class="session-title" x-text="session.sessionTitle"></div> + </div> + </template> + </div> + </div> + </div> <div id="toast" class="toast"></div> </div> <script> @@ -735,18 +855,21 @@ models: [], rags: [""], roles: [{ name: "", prompt: "" }], + settings: defaultSettings, modelData: {}, messages: [], - hoveredMessageIndex: null, input: "", images: [], asking: false, - sessionMode: false, askAbortController: null, + hoveredMessageIndex: null, shouldScrollChatBodyToBottom: true, isShowScrollToBottomBtn: false, - summary: "", - settings: defaultSettings, + showModal: "", + sessionMode: false, + sessionTitle: "", + selectSessionId: null, + sessions: [], async init() { await Promise.all([ @@ -781,6 +904,7 @@ } else { this.settings.role = ""; } + document.addEventListener("keydown", (event) => this.handleKeyDown(event)) }, handleAsk() { @@ -789,6 +913,16 @@ if (this.asking || (isEmptyImage && isEmptyInput)) { return; } + if (this.messages.length === 0) { + let sessionTitle = "" + if (this.images.length > 0) { + sessionTitle = `🖼️x${this.images.length} ` + } + if (this.input) { + sessionTitle += this.input.trim().replace(/\n/g, "↵").slice(0, 200); + } + this.sessionTitle = sessionTitle; + } if (isEmptyImage) { this.messages.push({ id: msgIdx++, @@ -895,7 +1029,7 @@ this.$refs["main-panel"].style.display = this.$refs["main-panel"]._display; }, - handleEnterKeydown(event) { + handleEnterKeyDown(event) { if (event.shiftKey) { return; } @@ -948,6 +1082,81 @@ this.images.push(...urls); }, + handleKeyDown(event) { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') > -1; + const controlKey = isMac ? event.metaKey : event.ctrlKey; + if (controlKey && event.shiftKey && event.key.toLowerCase() === 'o') { + event.preventDefault(); + this.handleNewChat(); + } else if (controlKey && event.shiftKey && event.key.toLowerCase() === 'l') { + event.preventDefault(); + this.showModal = 'list-sessions' + } else if (event.shiftKey && event.key === "Escape") { + event.preventDefault(); + this.focusInput(); + } else if (this.showModal && event.key === "Escape") { + event.preventDefault(); + this.showModal = ""; + } + }, + + handleNewChat() { + if (this.asking) { + this.askAbortController?.abort(); + } + if (this.sessionTitle) { + const lastMessage = this.messages[this.messages.length - 1]; + if (lastMessage.state === "loading") { + lastMessage.state = "failed"; + lastMessage.error = "Error: Aborted"; + lastMessage.html = renderMarkdown(lastMessage.content, lastMessage.error); + } + const sessionData = JSON.parse(JSON.stringify({ + settings: this.settings, + messages: this.messages, + sessionMode: this.sessionMode, + sessionTitle: this.sessionTitle, + })); + let session = this.sessions.find(v => v.id === this.selectSessionId); + if (session) { + Object.assign(session, sessionData); + } else { + this.sessions.unshift({ + id: randomUUID(), + createdAt: Date.now(), + ...sessionData, + }); + } + } + this.messages = []; + this.asking = false; + this.askAbortController = null; + this.hoveredMessageIndex = null; + this.shouldScrollChatBodyToBottom = true; + this.isShowScrollToBottomBtn = false; + this.showModal = ""; + this.sessionMode = false; + this.sessionTitle = ""; + this.selectSessionId = null; + + this.focusInput(); + }, + + handleSelectSession(id) { + const session = this.sessions.find(v => v.id === id); + if (!session || id === this.selectSessionId) { + this.showModal = ""; + this.focusInput(); + return; + } + this.handleNewChat(); + this.settings = session.settings; + this.messages = session.messages; + this.sessionMode = session.sessionMode; + this.sessionTitle = session.sessionTitle; + this.selectSessionId = session.id; + }, + updateUrl() { const newUrl = new URL(location.href); ["model", "rag", "role", "max_output_tokens", "temperature", "top_p"].forEach(key => { @@ -975,6 +1184,10 @@ $input.style.height = $input.scrollHeight + 'px'; }, + focusInput() { + this.$refs?.input?.focus(); + }, + async ask() { this.askAbortController = new AbortController(); this.shouldScrollChatBodyToBottom = true; @@ -1005,7 +1218,7 @@ } catch (err) { lastMessage.state = "failed"; if (this.askAbortController?.signal?.aborted) { - lastMessage.error = ""; + lastMessage.error = "Error: Aborted"; } else { lastMessage.error = err?.message || err; } @@ -1034,9 +1247,9 @@ buildBody() { let messages = []; for ([userMessage, assistantMessage] of chunkArray(this.messages, 2)) { - if (assistantMessage.state == "failed") { + if (assistantMessage.state === "failed") { continue; - } else if (assistantMessage.state == "loading") { + } else if (assistantMessage.state === "loading") { messages.push({ role: userMessage.role, content: userMessage.content, @@ -1052,7 +1265,7 @@ }); } } - const systemPrompt = (this.summary || this.settings.prompt).trim(); + const systemPrompt = this.settings.prompt.trim(); if (systemPrompt) { if (messages[0]?.content?.indexOf("__INPUT__") > -1) { messages[0].content = systemPrompt.replace("__INPUT__", messages[0].content); @@ -1148,7 +1361,7 @@ const parsed = JSON.parse(message); yield parsed; } catch { - if (i == lines.length - 1) { + if (i === lines.length - 1) { reamingChunkValue += line; break; } @@ -1302,10 +1515,18 @@ return chunks; } + function randomUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + function renderMarkdown(text, error = '') { return marked.marked(text) + (error ? `<pre class="error">${error}</pre>` : ''); } </script> </body> -</html> +</html>
\ No newline at end of file |
