Squashed commit of the following:

commit 55698f662e0154de991490891d7119b7b0d1b4e8 Author: Alessandro <real.eclypso@gmail.com> Date: Mon Oct 6 17:55:37 2025 +0200 fix Alpine init anti-patterns and clean globals - Remove manual init via 'alpine:init' from components - Drop store imports from index.html (components load their stores) - Replace Alpine.store() calls with direct ES module imports in index.js - Remove Alpine global accesses; broadcast connection status via CustomEvent - Bind scroll state to preferences store; UI sync via x-model commit 709afbaf22493ea5e827442bb0c879c453d5d0ec Author: Alessandro <real.eclypso@gmail.com> Date: Fri Oct 3 03:26:46 2025 +0200 sidebar top/bottom component; remove tabs (chats/tasks) - remove tabs, unify chats/tasks - sidebar bottom section component - guards for speechStore and sidebarStore for double initialization - sidebar path order - logo fix commit c8639d3312d609af2416f5f56861985452594157 Author: Alessandro <real.eclypso@gmail.com> Date: Thu Sep 25 01:39:56 2025 +0200 CSS lift-and-shift and js cleanup - sidebar CSS lift-and-shift - chat input area CSS lift-and-shift - cleanup, docstrings, and finetuning CSS - quick fix for version info and newChat - pref panel header css fix - late-mount safe polling for pauseAgent button - task actions incapsulation - task actions extraction within component - fix enter handler for chat input - quick actions css lift - memory dashboard import commit f99640c6b62cdfba456d4276be48f533b0819801 Author: Alessandro <real.eclypso@gmail.com> Date: Sun Sep 21 12:54:03 2025 +0200 input area, progress bar and bottom actions componentize - componentize chat input area + action buttons - componentize sidebar top icons and cleanup - full screen input modal componentization - progress bar box component creation - legacy markup cleanup for input section - simpler updateProgress (index.js) commit 356abe5853e6b99fa3e964dce8baf49e90737870 Author: Alessandro <real.eclypso@gmail.com> Date: Mon Sep 8 15:20:23 2025 +0200 sidebar componentize - left sidebar wrapper and css fine-tuning - chat list extraction, cleanup and path handling (sidebar) - updated and reordered paths for components extraction - uniform style blocks for pref component - unified context reference for killChat (chat lists fix) - tasks list initial extraction as component and tasksStore creation - quick actions component extraction - sidebar preferences component extraction commit 30d0b5ae8dc9d00c7700abda5da96fed94b47d88 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Thu Oct 2 09:18:06 2025 +0200 Update README.md commit e7027ca1f4bc245b80bb70be0b3133627f1633a7 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Wed Oct 1 16:53:53 2025 +0200 browser use allow http:// commit 0c837997f751693de654a71fe93d1d9f6a1255db Author: frdel <38891707+frdel@users.noreply.github.com> Date: Wed Oct 1 09:12:14 2025 +0200 bugfixing in memory detail modal commit 74aba3eb8c6124c0a8117343dea6f041772735db Author: frdel <38891707+frdel@users.noreply.github.com> Date: Wed Oct 1 08:42:31 2025 +0200 fix memory detail sizing commit 02d5d1e4cb9a834b705da80806f6f3db844a6aa3 Merge: f09ea5b 57bf5e7 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Wed Oct 1 08:23:54 2025 +0200 Merge branch 'pr/740' into testing commit 57bf5e7b16bef7a50c548c5a7d4a7ff597c8a022 Author: Alessandro <real.eclypso@gmail.com> Date: Tue Sep 30 15:08:39 2025 +0200 modal body fix, mem-detail CSS polishing commit f09ea5b21eb8cc539d19276e2e823428f4e4f418 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Tue Sep 30 11:26:20 2025 +0200 memory dashboard - double toast fix commit bd09a89beff531e63521452a63bd998d13424b94 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Mon Sep 29 21:39:51 2025 +0200 memory dashboard polishing commit 06c0dc97dc51ccd59f6f453939ec777546596307 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Mon Sep 29 17:59:06 2025 +0200 memory dashboard design polishing commit 24d5756c7caa3892617dad9c2d4581188eaf8e0e Author: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Mon Sep 29 15:35:59 2025 +0200 css finetuning for memory dashboard commit b81ed66743774d7f12a8b77bbe1d6ed05dd0ba6d Author: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Fri Sep 26 15:18:07 2025 +0200 further css centralization and footer injection - mobile breakpoint change for mem dashboard (800=>960px) - css cleanup and globalization - final css cleanup commit a6ce9c440489ddbef0052aa267b216af20636114 Author: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Thu Sep 25 21:17:01 2025 +0200 memory dashboard design commit cd3bbf7062a30ca9f9b81d526cb9afe3da99ac5d Author: frdel <38891707+frdel@users.noreply.github.com> Date: Thu Sep 25 13:06:02 2025 +0200 exclude embeddings from memory

frdel committed Oct 8, 2025 at 11:04 UTC 4283f5a937f1ff676994787edab436de26dfe9e5
36 files changed +2905 -3685
README.md
+11
@@ -169,6 +169,17 @@ docker run -p 50001:80 agent0ai/agent-zero
169
170 ## 🎯 Changelog
171
172 +### v0.9.6 - Memory Dashboard
173 +[Release video](https://youtu.be/sizjAq2-d9s)
174 +- Memory Management Dashboard
175 +- Kali update
176 +- Python update + dual installation
177 +- Browser Use update
178 +- New login screen
179 +- LiteLLM retry on temporary errors
180 +- Github Copilot provider support
181 +
182 +
183 ### v0.9.5 - Secrets
184 [Release video](https://www.youtube.com/watch?v=VqxUdt7pjd8)
185 - Secrets management - agent can use credentials without seeing them
python/api/memory_dashboard.py
+3 -3
@@ -138,7 +138,7 @@ class MemoryDashboard(ApiHandler):
138 """Get available memory subdirectories."""
139 try:
140 # Get subdirectories from memory folder
141 - subdirs = files.get_subdirectories("memory")
141 + subdirs = files.get_subdirectories("memory", exclude="embeddings")
142
143 # Ensure 'default' is always available
144 if "default" not in subdirs:
@@ -229,8 +229,8 @@ class MemoryDashboard(ApiHandler):
229 "id": metadata.get("id", "unknown"),
230 "area": metadata.get("area", "unknown"),
231 "timestamp": metadata.get("timestamp", "unknown"),
232 - "content_preview": m.page_content[:200]
233 - + ("..." if len(m.page_content) > 200 else ""),
232 + # "content_preview": m.page_content[:200]
233 + # + ("..." if len(m.page_content) > 200 else ""),
234 "content_full": m.page_content,
235 "knowledge_source": metadata.get("knowledge_source", False),
236 "source_file": metadata.get("source_file", ""),
python/tools/browser_agent.py
+1 -1
@@ -57,7 +57,7 @@ class State:
57 chromium_sandbox=False,
58 accept_downloads=True,
59 downloads_path=files.get_abs_path("tmp/downloads"),
60 - allowed_domains=["*"],
60 + allowed_domains=["*", "http://*", "https://*"],
61 executable_path=pw_binary,
62 keep_alive=True,
63 minimum_wait_page_load_time=1.0,
webui/components/chat/input/bottom-actions.html new
+80
@@ -0,0 +1,80 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/chat/input/input-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <div x-data>
9 + <template x-if="$store.chatInput">
10 + <div class="text-buttons-row">
11 + <button class="text-button" @click="$store.chatInput.pauseAgent(!$store.chatInput.paused)">
12 + <template x-if="!$store.chatInput.paused">
13 + <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14">
14 + <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path>
15 + </svg>
16 + </template>
17 + <template x-if="$store.chatInput.paused">
18 + <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" width="14" height="14">
19 + <path d="M8 5v14l11-7z"></path>
20 + </svg>
21 + </template>
22 + <span x-text="$store.chatInput.paused ? 'Resume Agent' : 'Pause Agent'"></span>
23 + </button>
24 +
25 + <button class="text-button" @click="$store.chatInput.loadKnowledge()">
26 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
27 + <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"></path>
28 + </svg>
29 + <p>Import knowledge</p>
30 + </button>
31 +
32 + <button class="text-button" id="work_dir_browser" @click="fileBrowserModalProxy.openModal()">
33 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 123.37 92.59">
34 + <path d="m5.72,11.5l-3.93,8.73h119.77s-3.96-8.73-3.96-8.73h-60.03c-1.59,0-2.88-1.29-2.88-2.88V1.75H13.72v6.87c0,1.59-1.29,2.88-2.88,2.88h-5.12Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="7"></path>
35 + <path d="m6.38,20.23H1.75l7.03,67.03c.11,1.07.55,2.02,1.2,2.69.55.55,1.28.89,2.11.89h97.1c.82,0,1.51-.33,2.05-.87.68-.68,1.13-1.67,1.28-2.79l9.1-66.94H6.38Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="8"></path>
36 + </svg>
37 + <p>Files</p>
38 + </button>
39 +
40 + <button class="text-button" id="history_inspect" @click="globalThis.openHistoryModal()">
41 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85">
42 + <path fill="currentColor" d="m59.572,57.949c-.41,0-.826-.105-1.207-.325l-9.574-5.528c-.749-.432-1.21-1.231-1.21-2.095v-14.923c0-1.336,1.083-2.419,2.419-2.419s2.419,1.083,2.419,2.419v13.526l8.364,4.829c1.157.668,1.554,2.148.886,3.305-.448.776-1.261,1.21-2.097,1.21Zm30.427-7.947c0,10.684-4.161,20.728-11.716,28.283-6.593,6.59-15.325,10.69-24.59,11.544-1.223.113-2.448.169-3.669.169-7.492,0-14.878-2.102-21.22-6.068l-15.356,5.733c-.888.331-1.887.114-2.557-.556s-.887-1.669-.556-2.557l5.733-15.351c-4.613-7.377-6.704-16.165-5.899-24.891.854-9.266,4.954-17.998,11.544-24.588,7.555-7.555,17.6-11.716,28.285-11.716s20.73,4.161,28.285,11.716c7.555,7.555,11.716,17.599,11.716,28.283Zm-15.137-24.861c-13.71-13.71-36.018-13.71-49.728,0-11.846,11.846-13.682,30.526-4.365,44.417.434.647.53,1.464.257,2.194l-4.303,11.523,11.528-4.304c.274-.102.561-.153.846-.153.474,0,.944.139,1.348.41,13.888,9.315,32.568,7.479,44.417-4.365,13.707-13.708,13.706-36.014,0-49.723Zm-24.861-4.13c-15.989,0-28.996,13.006-28.996,28.992s13.008,28.992,28.996,28.992c1.336,0,2.419-1.083,2.419-2.419s-1.083-2.419-2.419-2.419c-13.32,0-24.157-10.835-24.157-24.153s10.837-24.153,24.157-24.153,24.153,10.835,24.153,24.153c0,1.336,1.083,2.419,2.419,2.419c0-15.986-13.006-28.992-28.992-28.992Zm25.041,33.531c-1.294.347-2.057,1.673-1.71,2.963.343,1.289,1.669,2.057,2.963,1.71,1.289-.343,2.053-1.669,1.71-2.963-.347-1.289-1.673-2.057-2.963-1.71Zm-2.03,6.328c-1.335,0-2.419,1.084-2.419,2.419s1.084,2.419,2.419,2.419,2.419-1.084,2.419-2.419-1.084-2.419-2.419-2.419Zm-3.598,5.587c-1.289-.347-2.615.416-2.963,1.71-.343,1.289.421,2.615,1.71,2.963,1.294.347,2.62-.421,2.963-1.71.347-1.294-.416-2.62-1.71-2.963Zm-4.919,4.462c-1.157-.667-2.638-.27-3.306.887-.667,1.157-.27,2.638.887,3.305,1.157.668,2.638.27,3.306-.887.667-1.157.27-2.638-.887-3.306Zm-9.327,3.04c-.946.946-.946,2.478,0,3.42.942.946,2.473.946,3.42,0,.946-.942.946-2.473,0-3.42-.946-.942-2.478-.946-3.42,0Z"></path>
43 + </svg>
44 + <p>History</p>
45 + </button>
46 +
47 + <button class="text-button" id="ctx_window" @click="globalThis.openCtxWindowModal()">
48 + <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" fill="currentColor">
49 + <path d="m63 25c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z"></path>
50 + <path d="m63 79c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z"></path>
51 + <path d="m68 39h-36c-6.0703 0-11 4.9297-11 11s4.9297 11 11 11h36c6.0703 0 11-4.9297 11-11s-4.9297-11-11-11zm0 18h-36c-3.8594 0-7-3.1406-7-7s3.1406-7 7-7h36c3.8594 0 7 3.1406 7 7s-3.1406 7-7 7z"></path>
52 + </svg>
53 + <p>Context</p>
54 + </button>
55 +
56 + <button class="text-button" id="nudges_window" @click="$store.chatInput.nudge()">
57 + <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49 58" fill="currentColor">
58 + <path d="m11.97,16.32c-.46,0-.91-.25-1.15-.68-.9-1.63-1.36-3.34-1.36-5.1C9.45,4.73,14.18,0,20,0s10.55,4.73,10.55,10.55c0,.87-.13,1.75-.41,2.76-.19.7-.9,1.13-1.62.93-.7-.19-1.12-.92-.93-1.62.21-.79.31-1.44.31-2.07,0-4.36-3.55-7.91-7.91-7.91s-7.91,3.55-7.91,7.91c0,1.3.35,2.59,1.03,3.82.36.64.13,1.44-.51,1.79-.21.11-.42.17-.64.17Z" stroke-width="0.5" stroke="currentColor"/>
59 + <path d="M34.5,58h-6.18c-3.17,0-6.15-1.23-8.39-3.47L1.16,35.75c-1.54-1.54-1.54-4.05,0-5.59,2.4-2.4,6.27-2.68,8.99-.64l4.58,3.44V10.55c0-2.91,2.36-5.27,5.27-5.27s5.27,2.36,5.27,5.27v8.62c.78-.45,1.68-.71,2.64-.71,2.3,0,4.26,1.48,4.98,3.53.84-.56,1.85-.89,2.93-.89,2.3,0,4.26,1.48,4.98,3.53.84-.56,1.85-.89,2.93-.89,2.91,0,5.27,2.36,5.27,5.27v14.5c0,8-6.51,14.5-14.5,14.5ZM6.03,30.79c-1.1,0-2.19.42-3.01,1.23-.51.51-.51,1.35,0,1.86l18.77,18.78c1.74,1.74,4.06,2.7,6.53,2.7h6.18c6.54,0,11.86-5.32,11.86-11.86v-14.5c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v1.32c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32v-3.95c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v3.95c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32v-6.59c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v6.59c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32V10.55c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v25.05c0,.5-.28.95-.73,1.18s-.98.18-1.38-.12l-6.69-5.02c-.75-.56-1.65-.84-2.54-.84Z" stroke-width="0.5" stroke="currentColor"/>
60 + </svg>
61 + <p>Nudge</p>
62 + </button>
63 + </div>
64 + </template>
65 + </div>
66 +
67 + <style>
68 + .text-buttons-row { width: 100%; display: flex; padding-top: var(--spacing-xs); margin-left: var(--spacing-xs); }
69 + .text-button { background-color: transparent; border: none; border-radius: 5px; color: var(--color-text); font-family: "Rubik", Arial, Helvetica, sans-serif; font-size: 0.6rem; padding: 6px var(--spacing-sm); cursor: pointer; opacity: 0.8; -webkit-transition: all 0.3s; transition: all 0.3s; display: flex; align-items: center; gap: var(--spacing-xs); }
70 + .text-button:hover { opacity: 1; background-color: var(--color-secondary); border-radius: 4px; }
71 + .text-button:active { opacity: 0.5; }
72 + .text-button svg { width: 14px; height: 14px; flex-shrink: 0; }
73 + .text-button p { margin-block: 0; }
74 +
75 + @media (max-width: 768px) { .text-buttons-row { width: 90%; display: flex; padding-top: var(--spacing-xs); gap: var(--spacing-xs); white-space: pre-wrap; } .text-button { font-size: 0.6rem; } .text-button svg { width: 18px; height: 18px; flex-shrink: 0; } }
76 + @media (max-width: 640px) { .text-buttons-row { display: flex; width: 90%; padding-top: var(--spacing-xs); gap: var(--spacing-xs); white-space: pre-wrap; } .text-button { max-height: 25px; } .text-button p { display: none; } }
77 + </style>
78 +</body>
79 +</html>
80 +
webui/components/chat/input/chat-bar-input.html new
+128
@@ -0,0 +1,128 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store as speechStore } from "/components/chat/speech/speech-store.js";
5 + import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
6 + import { store as fullScreenStore } from "/components/modals/full-screen-store.js";
7 + </script>
8 +</head>
9 +<body>
10 + <div class="input-row">
11 + <!-- Attachment icon with tooltip -->
12 + <div class="attachment-wrapper" x-data="{ showTooltip: false }">
13 + <label for="file-input" class="attachment-icon" @mouseover="showTooltip = true" @mouseleave="showTooltip = false">
14 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
15 + <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" />
16 + </svg>
17 + </label>
18 + <input type="file" id="file-input" accept="*" multiple style="display: none" @change="$store.chatAttachments.handleFileUpload($event)">
19 +
20 + <div x-show="showTooltip" class="tooltip">Add attachments to the message</div>
21 + </div>
22 +
23 + <!-- Container for textarea and expand button -->
24 + <div id="chat-input-container" style="position: relative;">
25 + <textarea id="chat-input" placeholder="Type your message here..." rows="1"
26 ++ @keydown.enter="if (!$event.shiftKey && !$event.isComposing && $event.keyCode !== 229) { $event.preventDefault(); $store.chatInput.sendMessage(); }"
27 + @input="$store.chatInput.adjustTextareaHeight()"></textarea>
28 + <button id="expand-button" @click="$store.fullScreenInputModal.openModal()" aria-label="Expand input">
29 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
30 + <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
31 + </svg>
32 + </button>
33 + </div>
34 +
35 + <div id="chat-buttons-wrapper">
36 + <!-- Send button -->
37 + <button class="chat-button" id="send-button" aria-label="Send message"
38 + @click="$store.chatInput.sendMessage()">
39 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
40 + <path d="M25 20 L75 50 L25 80" fill="none" stroke="currentColor" stroke-width="15"></path>
41 + </svg>
42 + </button>
43 +
44 + <!-- Microphone button -->
45 + <button class="chat-button mic-inactive" id="microphone-button" aria-label="Start/Stop recording"
46 + @click="$store.speech.handleMicrophoneClick()" x-effect="$store.speech.updateMicrophoneButtonUI()">
47 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 18" fill="currentColor">
48 + <path d="m8,12c1.66,0,3-1.34,3-3V3c0-1.66-1.34-3-3-3s-3,1.34-3,3v6c0,1.66,1.34,3,3,3Zm-1,1.9c-2.7-.4-4.8-2.6-5-5.4H0c.2,3.8,3.1,6.9,7,7.5v2h2v-2c3.9-.6,6.8-3.7,7-7.5h-2c-.2,2.8-2.3,5-5,5.4h-2Z" />
49 + </svg>
50 + </button>
51 + </div>
52 + </div>
53 +
54 +
55 + <style>
56 + /* Layout rows */
57 + .input-row { display: flex; align-items: center; gap: var(--spacing-xs); width: 100%; white-space: nowrap; }
58 +
59 + /* Attachments icon + tooltip */
60 + .attachment-wrapper { position: relative; flex-shrink: 0; }
61 + .attachment-icon { cursor: pointer; color: var(--color-text); opacity: 0.7; transition: opacity 0.2s ease; display: flex; align-items: center; }
62 + .attachment-icon:hover { opacity: 1; }
63 + .attachment-icon:active { opacity: 0.5; }
64 + .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(0%); padding: 8px; background-color: var(--color-secondary); color: var(--color-text); border-radius: 4px; font-size: 12px; white-space: nowrap; z-index: 1002; }
65 +
66 +/* Text input */
67 + #chat-input-container {
68 + position: relative;
69 + width: 100%;
70 + }
71 + #chat-input {
72 + background-color: var(--color-input);
73 + border: 1px solid var(--color-border);
74 + border-radius: 8px;
75 + color: var(--color-text);
76 + flex-grow: 1;
77 + font-family: "Roboto Mono", monospace;
78 + font-optical-sizing: auto;
79 + -webkit-font-optical-sizing: auto;
80 + font-size: 0.9rem;
81 + max-height: 7rem;
82 + min-height: 3.05rem;
83 + width: 100%;
84 + padding: 0.48rem 40px var(--spacing-sm) var(--spacing-sm);
85 + margin-right: var(--spacing-xs);
86 + overflow-y: auto;
87 + scroll-behavior: smooth;
88 + resize: none;
89 + align-content: start;
90 + background-clip: border-box;
91 + border: 6px solid transparent;
92 + outline: 1px solid var(--color-border);
93 + transition: all 0.3s ease;
94 + -webkit-transition: all 0.3s ease;
95 + }
96 + #chat-input::-webkit-scrollbar { width: 6px; height: 6px; }
97 + #chat-input::-webkit-scrollbar-track { background: transparent; margin: 4px 0; border-radius: 6px; }
98 + #chat-input::-webkit-scrollbar-thumb { background-color: rgba(155,155,155,0.5); border-radius: 6px; -webkit-transition: background-color 0.2s ease; transition: background-color 0.2s ease; }
99 + #chat-input::-webkit-scrollbar-thumb:hover { background-color: rgba(155,155,155,0.7); }
100 + #chat-input:focus { outline: 0.05rem solid rgba(155,155,155,0.5); font-size: 0.955rem; padding-top: 0.45rem; background-color: var(--color-input-focus); }
101 + #chat-input::placeholder { color: var(--color-text-muted); opacity: 0.7; }
102 +
103 + #expand-button { position: absolute; top: 12px; right: 10px; background: transparent; border: none; cursor: pointer; font-size: 1.2rem; color: var(--color-text); opacity: 0.4; transition: opacity 0.2s; }
104 + #expand-button:hover { opacity: 0.7; }
105 + #expand-button:active { opacity: 1; }
106 + #expand-button svg { width: 1.3rem; height: 1.3rem; }
107 +
108 + /* Chat buttons (Send/Mic) */
109 + #chat-buttons-wrapper { gap: var(--spacing-xs); padding-left: var(--spacing-xs); line-height: 0.5rem; display: -webkit-flex; display: flex; }
110 + .chat-button { border: none; border-radius: 50%; color: var(--color-background); cursor: pointer; font-size: var(--font-size-normal); height: 2.525rem; width: 2.525rem; margin: 0 0.18rem 0 0 var(--spacing-xs); display: -webkit-flex; display: flex; align-items: center; justify-content: center; flex-shrink: 0; flex-grow: 0; min-width: 2.525rem; -webkit-transition: all var(--transition-speed), transform 0.1s ease-in-out; transition: all var(--transition-speed), transform 0.1s ease-in-out; }
111 + #send-button { background-color: #4248f1; }
112 + #send-button:hover { -webkit-transform: scale(1.05); transform: scale(1.05); transform-origin: center; background-color: #353bc5; }
113 + #send-button:active { -webkit-transform: scale(1); transform: scale(1); transform-origin: center; background-color: #2b309c; }
114 + .chat-button svg { width: 1.5rem; height: 1.5rem; }
115 + /* Microphone button */
116 + .chat-button.mic-inactive svg { /* Add specific styles if needed */ }
117 + /* Tooltip */
118 + .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(0%); padding: 8px; background-color: var(--color-secondary); color: var(--color-text); border-radius: 4px; font-size: 12px; white-space: nowrap; z-index: 1002; }
119 + /* Responsive tweaks */
120 + @media (max-width: 640px) {
121 + #chat-input { min-height: 5.3rem; align-content: start; }
122 + #chat-buttons-wrapper { display: flex; gap: var(--spacing-xs); padding: 0 0 0 var(--spacing-sm) !important; width: 3.5rem; flex-wrap: wrap; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; }
123 + }
124 + @media (min-width: 768px) { #chat-buttons-wrapper { flex-wrap: nowrap; -webkit-flex-wrap: nowrap; flex-wrap: nowrap; } }
125 + </style>
126 +</body>
127 +</html>
128 +
webui/components/chat/input/chat-bar.html new
+45
@@ -0,0 +1,45 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/chat/input/input-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <div id="input-section" x-data>
9 + <template x-if="$store.chatInput">
10 + <div style="width: 100%; display: contents;">
11 + <!-- Attachment Preview section -->
12 + <div>
13 + <x-component path="/chat/attachments/inputPreview.html" />
14 + </div>
15 +
16 + <x-component path="chat/input/chat-bar-input.html"></x-component>
17 + <x-component path="chat/input/bottom-actions.html"></x-component>
18 + </div>
19 + </template>
20 + </div>
21 +
22 +
23 + <style>
24 + /* Input section wrapper */
25 + #input-section {
26 + position: relative;
27 + background-color: var(--color-panel);
28 + display: -webkit-flex;
29 + display: flex;
30 + flex-direction: column;
31 + gap: var(--spacing-xs);
32 + padding: var(--spacing-sm) var(--spacing-md) var(--spacing-sm) 0.8rem;
33 + align-items: start;
34 + place-items: normal;
35 + flex-shrink: 0;
36 + z-index: 1001;
37 + }
38 +
39 + @media (max-width: 768px) {
40 + #input-section { align-items: normal !important; }
41 + }
42 + </style>
43 +</body>
44 +</html>
45 +
webui/components/chat/input/input-store.js new
+97
@@ -0,0 +1,97 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +
3 +const model = {
4 + paused: false,
5 +
6 + init() {
7 + console.log("Input store initialized");
8 + // Event listeners are now handled via Alpine directives in the component
9 + },
10 +
11 + async sendMessage() {
12 + // Delegate to the global function
13 + if (globalThis.sendMessage) {
14 + await globalThis.sendMessage();
15 + }
16 + },
17 +
18 + adjustTextareaHeight() {
19 + const chatInput = document.getElementById("chat-input");
20 + if (chatInput) {
21 + chatInput.style.height = "auto";
22 + chatInput.style.height = chatInput.scrollHeight + "px";
23 + }
24 + },
25 +
26 + async pauseAgent(paused) {
27 + const prev = this.paused;
28 + this.paused = paused;
29 + try {
30 + const context = globalThis.getContext?.();
31 + if (!globalThis.sendJsonData) throw new Error("sendJsonData not available");
32 + await globalThis.sendJsonData("/pause", { paused, context });
33 + } catch (e) {
34 + this.paused = prev;
35 + if (globalThis.toastFetchError) {
36 + globalThis.toastFetchError("Error pausing agent", e);
37 + }
38 + }
39 + },
40 +
41 + async nudge() {
42 + try {
43 + const context = globalThis.getContext();
44 + await globalThis.sendJsonData("/nudge", { ctxid: context });
45 + } catch (e) {
46 + if (globalThis.toastFetchError) {
47 + globalThis.toastFetchError("Error nudging agent", e);
48 + }
49 + }
50 + },
51 +
52 + async loadKnowledge() {
53 + const input = document.createElement("input");
54 + input.type = "file";
55 + input.accept = ".txt,.pdf,.csv,.html,.json,.md";
56 + input.multiple = true;
57 +
58 + input.onchange = async () => {
59 + try {
60 + const formData = new FormData();
61 + for (let file of input.files) {
62 + formData.append("files[]", file);
63 + }
64 +
65 + formData.append("ctxid", globalThis.getContext());
66 +
67 + const response = await globalThis.fetchApi("/import_knowledge", {
68 + method: "POST",
69 + body: formData,
70 + });
71 +
72 + if (!response.ok) {
73 + if (globalThis.toast) globalThis.toast(await response.text(), "error");
74 + } else {
75 + const data = await response.json();
76 + if (globalThis.toast) {
77 + globalThis.toast(
78 + "Knowledge files imported: " + data.filenames.join(", "),
79 + "success"
80 + );
81 + }
82 + }
83 + } catch (e) {
84 + if (globalThis.toastFetchError) {
85 + globalThis.toastFetchError("Error loading knowledge", e);
86 + }
87 + }
88 + };
89 +
90 + input.click();
91 + },
92 +};
93 +
94 +const store = createStore("chatInput", model);
95 +
96 +export { store };
97 +
webui/components/chat/input/progress.html new
+29
@@ -0,0 +1,29 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store as speechStore } from "/components/chat/speech/speech-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <div id="progress-bar-box">
9 + <h4 id="progress-bar-h">
10 + <span id="progress-bar-i">|></span><span id="progress-bar"></span>
11 + </h4>
12 + <h4 id="progress-bar-stop-speech" x-data x-cloak x-show="$store.speech.isSpeaking">
13 + <span id="stop-speech" @click="$store.speech.stop()" style="cursor: pointer">Stop Speech</span>
14 + </h4>
15 + </div>
16 +
17 +
18 + <style>
19 + #progress-bar-box { background-color: var(--color-panel); padding: var(--spacing-sm) var(--spacing-md); padding-bottom: 0; display: flex; justify-content: space-between; z-index: 1001; }
20 + #progress-bar-box h4 { margin: 0 1.2em 0 0; }
21 + #progress-bar-h { color: var(--color-primary); display: flex; align-items: left; justify-content: flex-start; height: 1.2em; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; font-weight: normal; }
22 + #progress-bar-i { font-weight: bold; padding-right: 0.5em; color: var(--color-secondary); }
23 + .shiny-text { background: linear-gradient(to right, var(--color-primary-dark) 20%, var(--color-text) 40%, var(--color-text) 60%, var(--color-primary-dark) 60%); background-size: 200% auto; color: transparent; -webkit-background-clip: text; background-clip: text; animation: shine 1s linear infinite; }
24 + @keyframes shine { to { background-position: -200% center; } }
25 + .light-mode #progress-bar-i { color: var(--color-border-dark); opacity: 0.5; }
26 + </style>
27 +</body>
28 +</html>
29 +
webui/components/chat/speech/speech-store.js
+10
@@ -14,6 +14,9 @@ const Status = {
14
15 // Create the speech store
16 const model = {
17 + // Initialization guard
18 + _initialized: false,
19 +
20 // STT Settings
21 stt_model_size: "tiny",
22 stt_language: "en",
@@ -89,6 +92,13 @@ const model = {
92
93 // Initialize speech functionality
94 async init() {
95 + // Guard against multiple initializations
96 + if (this._initialized) {
97 + console.log('[Speech Store] Already initialized, skipping duplicate init()');
98 + return;
99 + }
100 +
101 + this._initialized = true;
102 await this.loadSettings();
103 this.setupBrowserTTS();
104 this.setupUserInteractionHandling();
webui/components/modals/full-screen-input.html new
+78
@@ -0,0 +1,78 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/modals/full-screen-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <div x-data>
9 + <template x-if="$store.fullScreenInputModal">
10 + <div id="fullScreenInputModal" x-data>
11 + <template x-teleport="body">
12 + <div x-show="$store.fullScreenInputModal.isOpen" class="modal-overlay"
13 + @click.self="$store.fullScreenInputModal.handleClose()"
14 + @keydown.escape.window="$store.fullScreenInputModal.isOpen && $store.fullScreenInputModal.handleClose()"
15 + x-transition>
16 + <div class="modal-container full-screen-input-modal">
17 + <div class="modal-content">
18 + <button class="modal-close" @click="$store.fullScreenInputModal.handleClose()">&times;</button>
19 +
20 + <div class="editor-toolbar">
21 + <div class="toolbar-group">
22 + <button class="toolbar-button" @click="$store.fullScreenInputModal.undo()" :disabled="!$store.fullScreenInputModal.canUndo" title="Undo (Ctrl+Z)">
23 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
24 + <path d="M3 7v6h6"></path>
25 + <path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"></path>
26 + </svg>
27 + </button>
28 + <button class="toolbar-button" @click="$store.fullScreenInputModal.redo()" :disabled="!$store.fullScreenInputModal.canRedo" title="Redo (Ctrl+Shift+Z)">
29 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
30 + <path d="M21 7v6h-6"></path>
31 + <path d="M3 17a9 9 0 019-9 9 9 0 016 2.3l3 2.7"></path>
32 + </svg>
33 + </button>
34 + </div>
35 + <div class="toolbar-group">
36 + <button class="toolbar-button" @click="$store.fullScreenInputModal.clearText()" title="Clear Text">
37 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
38 + <path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path>
39 + <line x1="10" y1="11" x2="10" y2="17"></line>
40 + <line x1="14" y1="11" x2="14" y2="17"></line>
41 + </svg>
42 + </button>
43 + <button class="toolbar-button" @click="$store.fullScreenInputModal.toggleWrap()" :class="{ active: $store.fullScreenInputModal.wordWrap }" title="Toggle Word Wrap">
44 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
45 + <path d="M3 6h18M3 12h15l3 3-3 3M3 18h18"></path>
46 + </svg>
47 + </button>
48 + </div>
49 + </div>
50 +
51 + <textarea id="full-screen-input" x-model="$store.fullScreenInputModal.inputText"
52 + placeholder="Type your message here..."
53 + @keydown.ctrl.enter="$store.fullScreenInputModal.handleClose()"
54 + @keydown.ctrl.z.prevent="$store.fullScreenInputModal.undo()"
55 + @keydown.ctrl.shift.z.prevent="$store.fullScreenInputModal.redo()"
56 + :style="{ 'white-space': $store.fullScreenInputModal.wordWrap ? 'pre-wrap' : 'pre' }"
57 + @input="$store.fullScreenInputModal.updateHistory()"></textarea>
58 + </div>
59 + <div class="modal-footer">
60 + <div id="buttons-container">
61 + <button class="btn btn-ok" @click="$store.fullScreenInputModal.handleClose()">Done (Ctrl+Enter)</button>
62 + </div>
63 + </div>
64 + </div>
65 + </div>
66 + </template>
67 + </div>
68 + </template>
69 + </div>
70 +
71 +
72 + <style>
73 + /* No component-specific styles needed; styles are provided by css/modals.css and css/modals2.css */
74 + </style>
75 +</body>
76 +</html>
77 +
78 +
webui/components/modals/full-screen-store.js new
+91
@@ -0,0 +1,91 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +
3 +// Store model for the Full-Screen Input Modal
4 +const model = {
5 + // State
6 + isOpen: false,
7 + inputText: "",
8 + wordWrap: true,
9 + undoStack: [],
10 + redoStack: [],
11 + maxStackSize: 100,
12 + lastSavedState: "",
13 +
14 + // Lifecycle
15 + init() {
16 + // No-op for now; kept for parity and future side-effects
17 + },
18 +
19 + // Open modal with current chat input content
20 + openModal() {
21 + const chatInput = document.getElementById("chat-input");
22 + this.inputText = chatInput ? chatInput.value : this.inputText;
23 + this.lastSavedState = this.inputText;
24 + this.isOpen = true;
25 + this.undoStack = [];
26 + this.redoStack = [];
27 +
28 + // Focus the full screen input after rendering
29 + setTimeout(() => {
30 + const fullScreenInput = document.getElementById("full-screen-input");
31 + if (fullScreenInput) fullScreenInput.focus();
32 + }, 50);
33 + },
34 +
35 + // Close modal and write value back into main chat input
36 + handleClose() {
37 + const chatInput = document.getElementById("chat-input");
38 + if (chatInput) {
39 + chatInput.value = this.inputText;
40 + chatInput.dispatchEvent(new Event("input")); // trigger auto-resize
41 + }
42 + this.isOpen = false;
43 + },
44 +
45 + // History management
46 + updateHistory() {
47 + if (this.lastSavedState === this.inputText) return; // no change
48 + this.undoStack.push(this.lastSavedState);
49 + if (this.undoStack.length > this.maxStackSize) this.undoStack.shift();
50 + this.redoStack = [];
51 + this.lastSavedState = this.inputText;
52 + },
53 +
54 + undo() {
55 + if (!this.canUndo) return;
56 + this.redoStack.push(this.inputText);
57 + this.inputText = this.undoStack.pop();
58 + this.lastSavedState = this.inputText;
59 + },
60 +
61 + redo() {
62 + if (!this.canRedo) return;
63 + this.undoStack.push(this.inputText);
64 + this.inputText = this.redoStack.pop();
65 + this.lastSavedState = this.inputText;
66 + },
67 +
68 + clearText() {
69 + if (!this.inputText) return;
70 + this.updateHistory();
71 + this.inputText = "";
72 + this.lastSavedState = "";
73 + },
74 +
75 + toggleWrap() {
76 + this.wordWrap = !this.wordWrap;
77 + },
78 +
79 + // Computed
80 + get canUndo() {
81 + return this.undoStack.length > 0;
82 + },
83 +
84 + get canRedo() {
85 + return this.redoStack.length > 0;
86 + },
87 +};
88 +
89 +export const store = createStore("fullScreenInputModal", model);
90 +
91 +
webui/components/settings/memory/memory-dashboard-store.js
+9 -7
@@ -397,7 +397,7 @@ ${memory.content_full}
397 .map((memory) => this.formatMemoryForCopy(memory))
398 .join("\n");
399
400 - this.copyToClipboard(content);
400 + this.copyToClipboard(content, false);
401 justToast(
402 `Copied ${selectedMemories.length} memories with metadata to clipboard`,
403 "success"
@@ -516,23 +516,24 @@ ${memory.content_full}
516 return colors[area] || "#6c757d";
517 },
518
519 - copyToClipboard(text) {
519 + copyToClipboard(text, toastSuccess = true) {
520 if (navigator.clipboard && window.isSecureContext) {
521 navigator.clipboard
522 .writeText(text)
523 .then(() => {
524 - justToast("Copied to clipboard!", "success");
524 + if(toastSuccess)
525 + justToast("Copied to clipboard!", "success");
526 })
527 .catch((err) => {
528 console.error("Clipboard copy failed:", err);
528 - this.fallbackCopyToClipboard(text);
529 + this.fallbackCopyToClipboard(text, toastSuccess);
530 });
531 } else {
531 - this.fallbackCopyToClipboard(text);
532 + this.fallbackCopyToClipboard(text, toastSuccess);
533 }
534 },
535
535 - fallbackCopyToClipboard(text) {
536 + fallbackCopyToClipboard(text, toastSuccess = true) {
537 const textArea = document.createElement("textarea");
538 textArea.value = text;
539 textArea.style.position = "fixed";
@@ -543,7 +544,8 @@ ${memory.content_full}
544 textArea.select();
545 try {
546 document.execCommand("copy");
546 - justToast("Copied to clipboard!", "success");
547 + if(toastSuccess)
548 + justToast("Copied to clipboard!", "success");
549 } catch (err) {
550 console.error("Fallback clipboard copy failed:", err);
551 justToast("Failed to copy to clipboard", "error");
webui/components/settings/memory/memory-dashboard.html
+538 -1091
@@ -10,69 +10,61 @@
10 <body>
11 <div x-data>
12 <template x-if="$store.memoryDashboardStore">
13 - <div x-create="$store.memoryDashboardStore.onOpen()"
14 - x-destroy="$store.memoryDashboardStore.cleanup()" class="memory-dashboard">
15 -
16 - <!-- Search and Filters -->
17 - <div class="filters-section">
18 - <div class="filter-row">
19 - <div class="filter-group">
20 - <label for="memory-subdir-select">Memory Directory:</label>
21 - <select id="memory-subdir-select" x-model="$store.memoryDashboardStore.selectedMemorySubdir"
22 - @change="$store.memoryDashboardStore.onMemorySubdirChange()"
23 - :disabled="$store.memoryDashboardStore.loadingSubdirs">
24 - <template x-for="subdir in $store.memoryDashboardStore.memorySubdirs" :key="subdir">
25 - <option :value="subdir" x-text="subdir"
26 - :selected="subdir === $store.memoryDashboardStore.selectedMemorySubdir">
27 - </option>
28 - </template>
29 - </select>
30 - <span x-show="$store.memoryDashboardStore.loadingSubdirs"
31 - class="loading-text">Loading...</span>
32 - </div>
13 + <div x-create="$store.memoryDashboardStore.onOpen()" x-destroy="$store.memoryDashboardStore.cleanup()"
14 + class="memory-dashboard">
15 +
16 + <!-- Search and Filters Section -->
17 + <div class="memory-filters-header">
18 + <div class="filter-group-inline filter-memory-dir">
19 + <label for="memory-subdir-select">Memory Directory</label>
20 + <select id="memory-subdir-select" x-model="$store.memoryDashboardStore.selectedMemorySubdir"
21 + @change="$store.memoryDashboardStore.onMemorySubdirChange()"
22 + :disabled="$store.memoryDashboardStore.loadingSubdirs">
23 + <template x-for="subdir in $store.memoryDashboardStore.memorySubdirs" :key="subdir">
24 + <option :value="subdir" x-text="subdir"
25 + :selected="subdir === $store.memoryDashboardStore.selectedMemorySubdir">
26 + </option>
27 + </template>
28 + </select>
29 + </div>
30
34 - <div class="filter-group">
35 - <label for="area-filter">Area:</label>
36 - <select id="area-filter" x-model="$store.memoryDashboardStore.areaFilter">
37 - <option value="">All Areas</option>
38 - <option value="main">Main</option>
39 - <option value="fragments">Fragments</option>
40 - <option value="solutions">Solutions</option>
41 - <option value="instruments">Instruments</option>
42 - </select>
43 - </div>
31 + <div class="filter-group-inline filter-area">
32 + <label for="area-filter">Area</label>
33 + <select id="area-filter" x-model="$store.memoryDashboardStore.areaFilter">
34 + <option value="">All Areas</option>
35 + <option value="main">Main</option>
36 + <option value="fragments">Fragments</option>
37 + <option value="solutions">Solutions</option>
38 + <option value="instruments">Instruments</option>
39 + </select>
40 + </div>
41
45 - <div class="filter-group">
46 - <label for="search-input">Search:</label>
47 - <input type="text" id="search-input" x-model="$store.memoryDashboardStore.searchQuery"
48 - placeholder="Search memory content..."
49 - @keyup.enter="$store.memoryDashboardStore.searchMemories()" />
50 - </div>
42 + <div class="filter-group-inline filter-limit">
43 + <label for="limit-input">Limit:</label>
44 + <input type="number" id="limit-input" x-model.number="$store.memoryDashboardStore.limit"
45 + min="10" max="1000" step="10" />
46 + </div>
47
52 - <div class="filter-group">
53 - <label for="limit-input">Limit:</label>
54 - <input type="number" id="limit-input" x-model.number="$store.memoryDashboardStore.limit"
55 - min="10" max="1000" step="10" style="min-width: 100px;" />
56 - </div>
48 + <div class="filter-threshold">
49 + <label for="threshold-input">Threshold: <strong
50 + x-text="$store.memoryDashboardStore.threshold.toFixed(2)"></strong></label>
51 + <input type="range" id="threshold-input" x-model.number="$store.memoryDashboardStore.threshold"
52 + min="0" max="1" step="0.01" />
53 + </div>
54
58 - <div class="filter-group">
59 - <label for="threshold-input">Threshold: <span x-text="$store.memoryDashboardStore.threshold.toFixed(2)"></span></label>
60 - <input type="range" id="threshold-input" x-model.number="$store.memoryDashboardStore.threshold"
61 - min="0" max="1" step="0.01" style="min-width: 10em;" />
62 - </div>
55 + <input type="text" id="search-input" x-model="$store.memoryDashboardStore.searchQuery"
56 + placeholder="Search memory content..."
57 + @keyup.enter="$store.memoryDashboardStore.searchMemories()" />
58
64 - <div class="filter-actions">
65 - <button class="btn primary slim" @click="$store.memoryDashboardStore.searchMemories()"
66 - :disabled="$store.memoryDashboardStore.loading || $store.memoryDashboardStore.loadingSubdirs">
67 - <span x-show="!$store.memoryDashboardStore.initializingMemory">Search</span>
68 - <span x-show="$store.memoryDashboardStore.initializingMemory">Initializing
69 - Memory...</span>
70 - </button>
71 - <button class="btn slim" @click="$store.memoryDashboardStore.clearSearch()">
72 - Clear
73 - </button>
74 - </div>
75 - </div>
59 + <button class="btn btn-ok btn-search" @click="$store.memoryDashboardStore.searchMemories()"
60 + :disabled="$store.memoryDashboardStore.loading || $store.memoryDashboardStore.loadingSubdirs">
61 + <span x-show="!$store.memoryDashboardStore.initializingMemory">Search</span>
62 + <span x-show="$store.memoryDashboardStore.initializingMemory">Init...</span>
63 + </button>
64 +
65 + <button class="btn btn-cancel btn-clear" @click="$store.memoryDashboardStore.clearSearch()">
66 + Clear
67 + </button>
68 </div>
69
70 <!-- Loading state -->
@@ -91,56 +83,48 @@
83 <div x-show="!$store.memoryDashboardStore.loading && !$store.memoryDashboardStore.error"
84 class="memory-table-container">
85
94 - <!-- Combined stats and pagination header -->
95 - <div class="stats-pagination-header" x-show="!$store.memoryDashboardStore.loading">
96 - <!-- Statistics -->
97 - <div class="memory-stats-compact">
98 - <div class="stat-item">
99 - <span class="stat-label">DB Total:</span>
100 - <span class="stat-value" x-text="$store.memoryDashboardStore.totalDbCount"></span>
101 - </div>
102 - <div class="stat-item">
103 - <span class="stat-label">Filtered:</span>
104 - <span class="stat-value" x-text="$store.memoryDashboardStore.totalCount"></span>
105 - </div>
106 - <div class="stat-item">
107 - <span class="stat-label">Knowledge:</span>
108 - <span class="stat-value" x-text="$store.memoryDashboardStore.knowledgeCount"></span>
109 - </div>
110 - <div class="stat-item">
111 - <span class="stat-label">Conversation:</span>
112 - <span class="stat-value" x-text="$store.memoryDashboardStore.conversationCount"></span>
113 - </div>
86 + <!-- Status Bar -->
87 + <div class="memory-status-bar" x-show="!$store.memoryDashboardStore.loading">
88 + <div class="status-info">
89 + <span class="status-item">
90 + <span class="material-symbols-outlined">database</span>
91 + Total: <strong x-text="$store.memoryDashboardStore.totalDbCount"></strong>
92 + </span>
93 + <span class="status-separator">•</span>
94 + <span class="status-item">
95 + Filtered: <strong x-text="$store.memoryDashboardStore.totalCount"></strong>
96 + </span>
97 + <span class="status-separator">•</span>
98 + <span class="status-item">
99 + Knowledge: <strong x-text="$store.memoryDashboardStore.knowledgeCount"></strong>
100 + </span>
101 + <span class="status-separator">•</span>
102 + <span class="status-item">
103 + Conversation: <strong x-text="$store.memoryDashboardStore.conversationCount"></strong>
104 + </span>
105 </div>
106
116 - <!-- Pagination controls -->
117 - <div class="pagination-controls-compact" x-show="$store.memoryDashboardStore.totalPages > 1">
118 - <div class="pagination-info-inline">
119 - <span>Page <span x-text="$store.memoryDashboardStore.currentPage"></span>
120 - of <span x-text="$store.memoryDashboardStore.totalPages"></span>
121 - </span>
122 - </div>
123 -
124 - <div class="pagination-controls">
125 - <button class="btn slim" @click="$store.memoryDashboardStore.prevPage()"
126 - :disabled="$store.memoryDashboardStore.currentPage === 1">
127 - Previous
128 - </button>
129 -
130 - <select class="page-select" x-model.number="$store.memoryDashboardStore.currentPage"
131 - @change="$store.memoryDashboardStore.goToPage($store.memoryDashboardStore.currentPage)">
132 - <template
133 - x-for="page in Array.from({length: $store.memoryDashboardStore.totalPages}, (_, i) => i + 1)"
134 - :key="page">
135 - <option :value="page" x-text="'Page ' + page"></option>
136 - </template>
137 - </select>
138 -
139 - <button class="btn slim" @click="$store.memoryDashboardStore.nextPage()"
140 - :disabled="$store.memoryDashboardStore.currentPage === $store.memoryDashboardStore.totalPages">
141 - Next
142 - </button>
107 + <!-- Pagination -->
108 + <div class="status-pagination" x-show="$store.memoryDashboardStore.totalPages > 1">
109 + <button class="btn btn-icon" @click="$store.memoryDashboardStore.prevPage()"
110 + :disabled="$store.memoryDashboardStore.currentPage === 1" title="Previous Page">
111 + <span class="material-symbols-outlined">chevron_left</span>
112 + </button>
113 + <div class="page-input-group">
114 + <label>Page</label>
115 + <input type="number" class="page-input"
116 + x-model.number="$store.memoryDashboardStore.currentPage"
117 + @change="$store.memoryDashboardStore.goToPage($store.memoryDashboardStore.currentPage)"
118 + @keyup.enter="$store.memoryDashboardStore.goToPage($store.memoryDashboardStore.currentPage)"
119 + :min="1" :max="$store.memoryDashboardStore.totalPages" />
120 + <span class="page-total">of <strong
121 + x-text="$store.memoryDashboardStore.totalPages"></strong></span>
122 </div>
123 + <button class="btn btn-icon" @click="$store.memoryDashboardStore.nextPage()"
124 + :disabled="$store.memoryDashboardStore.currentPage === $store.memoryDashboardStore.totalPages"
125 + title="Next Page">
126 + <span class="material-symbols-outlined">chevron_right</span>
127 + </button>
128 </div>
129 </div>
130
@@ -151,48 +135,29 @@
135 </div>
136
137 <div class="mass-actions">
154 - <button class="btn-mass copy" @click="$store.memoryDashboardStore.bulkCopyMemories()"
138 + <button class="btn btn-mass copy" @click="$store.memoryDashboardStore.bulkCopyMemories()"
139 title="Copy Selected Content">
156 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
157 - stroke-width="2">
158 - <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
159 - <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
160 - </svg>
140 + <span class="material-symbols-outlined">content_copy</span>
141 Copy
142 </button>
143
164 - <button class="btn-mass export" @click="$store.memoryDashboardStore.bulkExportMemories()"
144 + <button class="btn btn-mass export"
145 + @click="$store.memoryDashboardStore.bulkExportMemories()"
146 title="Export Selected Memories">
166 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
167 - stroke-width="2">
168 - <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
169 - <polyline points="7,10 12,15 17,10"></polyline>
170 - <line x1="12" y1="15" x2="12" y2="3"></line>
171 - </svg>
147 + <span class="material-symbols-outlined">download</span>
148 Export
149 </button>
150
175 - <button class="btn-mass delete" @click="$store.memoryDashboardStore.bulkDeleteMemories()"
151 + <button class="btn btn-mass delete"
152 + @click="$store.memoryDashboardStore.bulkDeleteMemories()"
153 title="Delete Selected Memories">
177 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
178 - stroke-width="2">
179 - <polyline points="3,6 5,6 21,6"></polyline>
180 - <path
181 - d="M19,6V20a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2V6">
182 - </path>
183 - <line x1="10" y1="11" x2="10" y2="17"></line>
184 - <line x1="14" y1="11" x2="14" y2="17"></line>
185 - </svg>
154 + <span class="material-symbols-outlined">delete</span>
155 Delete
156 </button>
157
189 - <button class="btn-mass clear" @click="$store.memoryDashboardStore.clearSelection()"
158 + <button class="btn btn-mass clear" @click="$store.memoryDashboardStore.clearSelection()"
159 title="Clear Selection">
191 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
192 - stroke-width="2">
193 - <line x1="18" y1="6" x2="6" y2="18"></line>
194 - <line x1="6" y1="6" x2="18" y2="18"></line>
195 - </svg>
160 + <span class="material-symbols-outlined">close</span>
161 Clear
162 </button>
163 </div>
@@ -220,7 +185,7 @@
185 @click="$store.memoryDashboardStore.showMemoryDetails(memory)"
186 style="cursor: pointer;">
187 <!-- Selection checkbox -->
223 - <td class="select-cell" @click.stop>
188 + <td class="select-cell" @click.stop="memory.selected = !memory.selected">
189 <input type="checkbox" x-model="memory.selected" />
190 </td>
191
@@ -232,11 +197,6 @@
197 :style="`background-color: ${$store.memoryDashboardStore.getAreaColor(memory.area)}`"
198 x-text="(memory.area || 'UNKNOWN').toUpperCase()"></span>
199 </div>
235 - <div class="metadata-row metadata-timestamp">
236 - <span
237 - x-text="$store.memoryDashboardStore.formatTimestamp(memory.timestamp, true)"
238 - :title="$store.memoryDashboardStore.formatTimestamp(memory.timestamp, false)"></span>
239 - </div>
200 <div class="metadata-row">
201 <template x-if="memory.knowledge_source">
202 <span class="source-type knowledge">Knowledge</span>
@@ -245,12 +205,20 @@
205 <span class="source-type conversation">Conversation</span>
206 </template>
207 </div>
208 + <div class="metadata-row metadata-timestamp">
209 + <span
210 + x-text="$store.memoryDashboardStore.formatTimestamp(memory.timestamp, true)"
211 + :title="$store.memoryDashboardStore.formatTimestamp(memory.timestamp, false)"></span>
212 + </div>
213 +
214 </div>
215 </td>
216
217 <!-- Content preview -->
218 <td class="preview-cell">
253 - <div class="content-preview" x-text="memory.content_preview"></div>
219 + <div class="content-preview"
220 + x-text="memory.content_full.substring(0, 200) + (memory.content_full.length > 200 ? '...' : '')">
221 + </div>
222 <div class="tags" x-show="memory.tags && memory.tags.length > 0">
223 <template x-for="tag in memory.tags" :key="tag">
224 <span class="tag" x-text="tag"></span>
@@ -261,30 +229,16 @@
229 <!-- Actions -->
230 <td class="actions-cell" @click.stop>
231 <div class="actions-wrapper">
264 - <button class="btn-action copy"
232 + <button class="btn btn-action copy"
233 @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.formatMemoryForCopy(memory))"
234 title="Copy Memory">
267 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none"
268 - stroke="currentColor" stroke-width="2">
269 - <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
270 - <path
271 - d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">
272 - </path>
273 - </svg>
235 + <span class="material-symbols-outlined">content_copy</span>
236 </button>
237
276 - <button class="btn-action delete"
238 + <button class="btn btn-action delete"
239 @click="$store.memoryDashboardStore.deleteMemory(memory)"
240 title="Delete Memory">
279 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none"
280 - stroke="currentColor" stroke-width="2">
281 - <polyline points="3,6 5,6 21,6"></polyline>
282 - <path
283 - d="M19,6V20a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6M8,6V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2V6">
284 - </path>
285 - <line x1="10" y1="11" x2="10" y2="17"></line>
286 - <line x1="14" y1="11" x2="14" y2="17"></line>
287 - </svg>
241 + <span class="material-symbols-outlined">delete</span>
242 </button>
243 </div>
244 </td>
@@ -297,6 +251,88 @@
251
252 </div>
253
254 + <!-- Mass action toolbar -->
255 + <div x-show="$store.memoryDashboardStore.selectedCount > 0" class="mass-action-toolbar">
256 + <div class="selection-info">
257 + <span x-text="$store.memoryDashboardStore.selectedCount"></span> memories selected
258 + </div>
259 +
260 + <div class="mass-actions">
261 + <button class="btn btn-mass copy" @click="$store.memoryDashboardStore.bulkCopyMemories()"
262 + title="Copy Selected Content">
263 + <span class="material-symbols-outlined">content_copy</span>
264 + Copy
265 + </button>
266 +
267 + <button class="btn btn-mass export"
268 + @click="$store.memoryDashboardStore.bulkExportMemories()"
269 + title="Export Selected Memories">
270 + <span class="material-symbols-outlined">download</span>
271 + Export
272 + </button>
273 +
274 + <button class="btn btn-mass delete"
275 + @click="$store.memoryDashboardStore.bulkDeleteMemories()"
276 + title="Delete Selected Memories">
277 + <span class="material-symbols-outlined">delete</span>
278 + Delete
279 + </button>
280 +
281 + <button class="btn btn-mass clear" @click="$store.memoryDashboardStore.clearSelection()"
282 + title="Clear Selection">
283 + <span class="material-symbols-outlined">close</span>
284 + Clear
285 + </button>
286 + </div>
287 + </div>
288 +
289 +
290 + <!-- Status Bar -->
291 + <div class="memory-status-bar" x-show="!$store.memoryDashboardStore.loading">
292 + <div class="status-info">
293 + <span class="status-item">
294 + <span class="material-symbols-outlined">database</span>
295 + Total: <strong x-text="$store.memoryDashboardStore.totalDbCount"></strong>
296 + </span>
297 + <span class="status-separator">•</span>
298 + <span class="status-item">
299 + Filtered: <strong x-text="$store.memoryDashboardStore.totalCount"></strong>
300 + </span>
301 + <span class="status-separator">•</span>
302 + <span class="status-item">
303 + Knowledge: <strong x-text="$store.memoryDashboardStore.knowledgeCount"></strong>
304 + </span>
305 + <span class="status-separator">•</span>
306 + <span class="status-item">
307 + Conversation: <strong x-text="$store.memoryDashboardStore.conversationCount"></strong>
308 + </span>
309 + </div>
310 +
311 + <!-- Pagination -->
312 + <div class="status-pagination" x-show="$store.memoryDashboardStore.totalPages > 1">
313 + <button class="btn btn-icon" @click="$store.memoryDashboardStore.prevPage()"
314 + :disabled="$store.memoryDashboardStore.currentPage === 1" title="Previous Page">
315 + <span class="material-symbols-outlined">chevron_left</span>
316 + </button>
317 + <div class="page-input-group">
318 + <label>Page</label>
319 + <input type="number" class="page-input"
320 + x-model.number="$store.memoryDashboardStore.currentPage"
321 + @change="$store.memoryDashboardStore.goToPage($store.memoryDashboardStore.currentPage)"
322 + @keyup.enter="$store.memoryDashboardStore.goToPage($store.memoryDashboardStore.currentPage)"
323 + :min="1" :max="$store.memoryDashboardStore.totalPages" />
324 + <span class="page-total">of <strong
325 + x-text="$store.memoryDashboardStore.totalPages"></strong></span>
326 + </div>
327 + <button class="btn btn-icon" @click="$store.memoryDashboardStore.nextPage()"
328 + :disabled="$store.memoryDashboardStore.currentPage === $store.memoryDashboardStore.totalPages"
329 + title="Next Page">
330 + <span class="material-symbols-outlined">chevron_right</span>
331 + </button>
332 + </div>
333 + </div>
334 +
335 +
336 <!-- No memories message -->
337 <div x-show="$store.memoryDashboardStore.memories.length === 0 && !$store.memoryDashboardStore.message"
338 class="no-memories">
@@ -310,12 +346,6 @@
346
347
348
313 - <!-- Export button -->
314 - <div class="export-section">
315 - <button class="btn slim" @click="$store.memoryDashboardStore.exportMemories()">
316 - Export Memories (JSON)
317 - </button>
318 - </div>
349 </div>
350
351
@@ -325,150 +355,248 @@
355 </div>
356
357 <style>
328 - [x-cloak] {
329 - display: none !important;
358 + .modal-inner.modal-with-footer {
359 + display: flex;
360 + flex-direction: column;
361 }
362
332 - .memory-dashboard {
333 - padding: 1rem;
334 - max-width: 100%;
335 - background: var(--color-background);
336 - color: var(--color-text);
363 + .modal-inner.modal-with-footer .modal-scroll {
364 + flex: 1;
365 + min-height: 0;
366 }
367
339 - .memory-dashboard h3 {
340 - color: var(--color-text);
341 - margin: 0;
342 - font-size: 1.5rem;
343 - font-weight: 600;
368 + .modal-inner.modal-with-footer .modal-footer {
369 + flex-shrink: 0;
370 }
371
346 -
347 -
348 - .stats-pagination-header {
349 - display: flex;
350 - justify-content: space-between;
372 + .memory-filters-header {
373 + display: grid;
374 + grid-template-columns: 35% 25% 10% auto;
375 + grid-template-rows: auto auto;
376 + gap: 0.75rem;
377 align-items: center;
378 + margin: 1rem 0;
379 padding: 1rem;
380 background: var(--color-panel);
381 border: 1px solid var(--color-border);
382 border-radius: 8px;
356 - border-bottom: none;
357 - border-bottom-left-radius: 0;
358 - border-bottom-right-radius: 0;
383 }
384
361 - .memory-stats-compact {
362 - display: flex;
363 - gap: 1rem;
364 - flex-wrap: wrap;
385 + /* Grid positioning for desktop - 2 rows */
386 + .filter-memory-dir {
387 + grid-column: 1;
388 + grid-row: 1;
389 }
390
367 - .pagination-controls-compact {
368 - display: flex;
369 - align-items: center;
370 - gap: 1rem;
391 + .filter-area {
392 + grid-column: 2;
393 + grid-row: 1;
394 + }
395 +
396 + .filter-limit {
397 + grid-column: 3;
398 + grid-row: 1;
399 }
400
373 - .stat-item {
401 + .filter-threshold {
402 + grid-column: 4;
403 + grid-row: 2;
404 + }
405 +
406 + #search-input {
407 + grid-column: 1 / 3;
408 + grid-row: 2;
409 + }
410 +
411 + .btn-search {
412 + grid-column: 3;
413 + grid-row: 2;
414 + justify-content: center;
415 + }
416 +
417 + .btn-clear {
418 + grid-column: 4;
419 + grid-row: 1;
420 + justify-self: end;
421 + }
422 +
423 + .filter-group-inline {
424 display: flex;
375 - flex-direction: column;
425 + flex: 1;
426 align-items: center;
377 - padding: 0.5rem;
378 - background: var(--color-panel);
379 - border: 1px solid var(--color-border);
380 - border-radius: 8px;
381 - min-width: 80px;
427 + gap: 0.5rem;
428 }
429
384 - .stat-label {
385 - font-size: 0.8rem;
430 + .filter-group-inline label {
431 + font-size: 0.85rem;
432 + font-weight: 600;
433 color: var(--color-text);
387 - opacity: 0.7;
388 - margin-bottom: 0.25rem;
434 + opacity: 0.8;
435 + white-space: nowrap;
436 }
437
391 - .light-mode .stat-label {
392 - opacity: 0.8;
438 + .filter-group-inline select,
439 + .filter-group-inline input {
440 + border: 1px solid var(--color-border);
441 + flex: 0 auto;
442 + min-width: 0;
443 }
444
395 - .stat-value {
396 - font-size: 1.1rem;
397 - font-weight: bold;
398 - color: var(--color-primary);
445 + .filter-group-inline select:focus,
446 + .filter-group-inline input:focus {
447 + outline: none;
448 + border-color: var(--color-primary);
449 + background: var(--color-input-focus);
450 }
451
401 - .filters-section {
402 - margin-bottom: 1.5rem;
403 - padding: 1rem;
404 - background: var(--color-panel);
452 + .filter-group-inline.filter-limit {
453 + flex: 0 0 auto;
454 + }
455 +
456 + .filter-limit input {
457 + flex: 0 0 150%;
458 + }
459 +
460 +
461 + #search-input {
462 + flex: 1;
463 border: 1px solid var(--color-border);
464 + font-size: 0.9rem;
465 + height: 38px;
466 + }
467 +
468 + #search-input:focus {
469 + outline: none;
470 + border-color: var(--color-primary);
471 + background: var(--color-input-focus);
472 + }
473 +
474 + .filter-threshold label {
475 + font-size: 0.85rem;
476 + opacity: 0.8;
477 + }
478 +
479 + .memory-status-bar {
480 + display: flex;
481 + justify-content: space-between;
482 + align-items: center;
483 + padding: 0.75rem 1rem;
484 + background: var(--color-panel);
485 + border-bottom: 1px solid var(--color-border);
486 border-radius: 8px;
487 + /* margin-bottom: 1rem; */
488 + font-size: 0.9rem;
489 }
490
409 - .filter-row {
491 + .status-info {
492 display: flex;
411 - gap: 1rem;
412 - align-items: end;
493 + align-items: center;
494 + gap: 0.5rem;
495 flex-wrap: wrap;
496 }
497
416 - .filter-group {
498 + .status-item {
499 display: flex;
418 - flex-direction: column;
500 + align-items: center;
501 gap: 0.25rem;
502 }
503
422 - .filter-group label {
423 - font-size: 0.9rem;
424 - font-weight: 500;
425 - color: var(--color-text);
504 + .status-item strong {
505 + color: var(--color-primary);
506 + font-weight: 600;
507 + }
508 +
509 + .status-item .material-symbols-outlined {
510 + font-size: 18px;
511 + opacity: 0.6;
512 }
513
428 - .filter-group input,
429 - .filter-group select {
430 - padding: 0.5rem;
514 + .status-separator {
515 + color: var(--color-border);
516 + margin: 0 0.25rem;
517 + }
518 +
519 + .status-pagination {
520 + display: flex;
521 + align-items: center;
522 + gap: 0.5rem;
523 + }
524 +
525 + .btn-icon {
526 + background: transparent;
527 border: 1px solid var(--color-border);
432 - background: var(--color-background);
528 color: var(--color-text);
434 - border-radius: 6px;
435 - font-size: 0.9rem;
436 - min-width: 200px;
437 - height: 40px;
438 - box-sizing: border-box;
529 + padding: 0.25rem;
530 + display: flex;
531 + align-items: center;
532 + font-size: 0;
533 }
534
441 - .filter-group input:focus,
442 - .filter-group select:focus {
443 - outline: none;
535 + .btn-icon:hover:not(:disabled) {
536 + background: var(--color-panel);
537 border-color: var(--color-primary);
445 - box-shadow: 0 0 0 2px rgba(115, 122, 129, 0.2);
446 - background: var(--color-input-focus);
538 + color: var(--color-text);
539 }
540
449 - .light-mode .filter-group input:focus,
450 - .light-mode .filter-group select:focus {
451 - box-shadow: 0 0 0 2px rgba(56, 70, 83, 0.1);
541 + .btn-icon:disabled {
542 + opacity: 0.4;
543 + cursor: not-allowed;
544 }
545
454 - .filter-actions {
455 - display: flex;
456 - gap: 0.5rem;
457 - align-items: stretch;
546 + .btn-icon .material-symbols-outlined {
547 + font-size: 20px;
548 }
549
460 - .filter-actions .btn {
461 - height: 40px;
550 + .page-input-group {
551 display: flex;
552 align-items: center;
464 - justify-content: center;
553 + gap: 0.5rem;
554 + }
555 +
556 + .page-input-group label {
557 + opacity: 0.8;
558 }
559
467 - .loading-text {
560 + .page-total {
561 + color: var(--color-text);
562 + opacity: 0.8;
563 font-size: 0.85rem;
564 + white-space: nowrap;
565 + }
566 +
567 + .page-total strong {
568 + color: var(--color-primary);
569 + font-weight: 600;
570 + }
571 +
572 + .page-input {
573 + width: 60px;
574 + padding: 0.25rem 0.5rem;
575 + text-align: center;
576 + border: 1px solid var(--color-border);
577 + background: var(--color-input);
578 color: var(--color-text);
470 - opacity: 0.7;
471 - font-style: italic;
579 + border-radius: 4px;
580 + font-size: 0.85rem;
581 + height: 28px;
582 + }
583 +
584 + .page-input:focus {
585 + outline: none;
586 + border-color: var(--color-primary);
587 + background: var(--color-input-focus);
588 + }
589 +
590 + /* Remove spinner arrows */
591 + .page-input::-webkit-inner-spin-button,
592 + .page-input::-webkit-outer-spin-button {
593 + -webkit-appearance: none;
594 + margin: 0;
595 + }
596 +
597 + .page-input[type=number] {
598 + -moz-appearance: textfield;
599 + appearance: textfield;
600 }
601
602 .loading-state,
@@ -485,26 +613,21 @@
613 margin: 1rem 0;
614 }
615
616 + .loading-state {
617 + display: grid;
618 + align-items: center;
619 + }
620 +
621 .init-message {
622 color: var(--color-primary);
490 - background: rgba(115, 122, 129, 0.2);
623 border-color: var(--color-primary);
624 }
625
494 - .light-mode .init-message {
495 - background: rgba(56, 70, 83, 0.1);
496 - }
497 -
626 .error-state {
627 color: var(--color-accent);
500 - background: rgba(207, 102, 121, 0.2);
628 border-color: var(--color-accent);
629 }
630
504 - .light-mode .error-state {
505 - background: rgba(176, 0, 32, 0.1);
506 - }
507 -
631 .loading-spinner {
632 width: 24px;
633 height: 24px;
@@ -531,16 +654,8 @@
654 }
655
656 .memory-table-container {
534 - background: var(--color-background);
657 border: 1px solid var(--color-border);
658 border-radius: 8px;
537 - overflow: hidden;
538 - margin-bottom: 1rem;
539 - }
540 -
541 - .table-wrapper {
542 - width: 100%;
543 - overflow-x: auto;
659 }
660
661 .memory-table {
@@ -549,21 +664,18 @@
664 table-layout: fixed;
665 }
666
552 - /* Fixed column widths for proper fit */
667 .col-select {
668 width: 5%;
669 }
670
671 .col-metadata {
558 - width: 25%;
672 + width: 10em;
673 }
674
561 - .col-preview {
562 - width: 60%;
563 - }
675 + .col-preview {}
676
677 .col-actions {
566 - width: 10%;
678 + width: 4em;
679 }
680
681 .memory-table th,
@@ -575,7 +687,6 @@
687 overflow: hidden;
688 }
689
578 - /* Metadata cell styling */
690 .metadata-cell {
691 overflow: visible;
692 white-space: normal;
@@ -600,17 +711,19 @@
711 font-family: monospace;
712 }
713
603 - .source-type {
604 - font-size: 0.8rem;
605 - color: var(--color-text);
606 - font-weight: 500;
714 + .area-badge {
715 + color: white;
716 + padding: 0.35rem 0.75rem;
717 + border-radius: 16px;
718 + font-size: 0.75rem;
719 + font-weight: bold;
720 + text-transform: uppercase;
721 }
722
609 - /* Selection column styling */
723 .select-cell,
724 .col-select {
725 text-align: center;
613 - padding: 0.5rem !important;
726 + padding: 0.65rem 0.5rem !important;
727 }
728
729 .select-cell input[type="checkbox"],
@@ -619,71 +732,59 @@
732 transform: scale(1.2);
733 }
734
622 - /* Selected row styling */
735 .memory-row.selected {
624 - background: rgba(115, 122, 129, 0.2);
736 + background: var(--color-panel);
737 border-left: 3px solid var(--color-primary);
738 }
739
628 - .light-mode .memory-row.selected {
629 - background: rgba(56, 70, 83, 0.1);
630 - }
631 -
632 - /* Mass action toolbar */
740 .mass-action-toolbar {
741 display: flex;
742 justify-content: space-between;
743 align-items: center;
744 padding: 1rem;
638 - background: rgba(115, 122, 129, 0.2);
639 - border: 1px solid var(--color-primary);
640 - border-radius: 8px;
641 - margin: 1rem 0;
745 + background: var(--color-panel);
746 + /* border: 1px solid var(--color-primary); */
747 + /* border-radius: 8px; */
748 + /* margin: 1rem 0; */
749 + margin: 0;
750 animation: slideDown 0.3s ease;
751 }
752
645 - .light-mode .mass-action-toolbar {
646 - background: rgba(56, 70, 83, 0.1);
647 - }
648 -
649 - .selection-info {
650 - font-weight: 600;
651 - color: var(--color-primary);
652 - }
653 -
654 - .mass-actions {
655 - display: flex;
656 - gap: 0.5rem;
657 - }
658 -
753 .btn-mass {
660 - display: flex;
754 align-items: center;
662 - gap: 0.5rem;
663 - padding: 0.5rem 1rem;
755 border: 1px solid var(--color-border);
756 + gap: 0.25rem;
757 + display: flex;
758 background: var(--color-background);
759 color: var(--color-text);
667 - border-radius: 6px;
668 - cursor: pointer;
669 - font-size: 0.9rem;
670 - font-weight: 500;
671 - transition: all 0.2s ease;
760 }
761
762 .btn-mass:hover {
763 border-color: var(--color-primary);
676 - background: var(--color-panel);
764 + color: var(--color-primary);
765 }
766
679 - .btn-mass.copy:hover {
680 - border-color: var(--color-primary);
681 - color: var(--color-primary);
767 + .btn-mass.delete:hover {
768 + border-color: var(--color-accent);
769 + color: var(--color-accent);
770 + }
771 +
772 + .selection-info {
773 + font-weight: 600;
774 + }
775 +
776 + .mass-actions {
777 + display: flex;
778 + gap: 0.5rem;
779 }
780
684 - .btn-mass.export:hover {
781 + .btn-mass .material-symbols-outlined {
782 + font-size: 18px;
783 + }
784 +
785 + .btn-mass:hover {
786 border-color: var(--color-primary);
686 - color: var(--color-primary);
787 + background: var(--color-panel);
788 }
789
790 .btn-mass.delete:hover {
@@ -691,11 +792,6 @@
792 color: var(--color-accent);
793 }
794
694 - .btn-mass.clear:hover {
695 - border-color: var(--color-primary);
696 - color: var(--color-primary);
697 - }
698 -
795 @keyframes slideDown {
796 from {
797 opacity: 0;
@@ -720,56 +816,19 @@
816
817 .memory-table tbody tr {
818 border-bottom: 1px solid var(--color-border);
723 - transition: background-color 0.2s ease;
724 - }
725 -
726 - .memory-table tbody tr:nth-child(even) {
727 - background: var(--color-panel);
728 - }
729 -
730 - .memory-table tbody tr:hover {
731 - background: var(--color-panel);
819 }
820
821 .memory-table tbody tr:last-child {
822 border-bottom: none;
823 }
824
738 - .area-badge {
739 - padding: 0.25rem 0.5rem;
740 - border-radius: 12px;
741 - color: white;
742 - font-size: 0.75rem;
743 - font-weight: bold;
744 - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
745 - border: 1px solid rgba(255, 255, 255, 0.1);
746 - }
747 -
748 - .light-mode .area-badge {
749 - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
750 - border: 1px solid rgba(0, 0, 0, 0.1);
751 - }
752 -
753 - .timestamp-cell {
754 - font-size: 0.85rem;
755 - color: var(--color-text);
756 - opacity: 0.7;
757 - white-space: normal;
758 - word-break: break-word;
759 - font-family: monospace;
760 - line-height: 1.2;
761 - }
762 -
763 - .source-info {
764 - display: flex;
765 - flex-direction: column;
766 - gap: 0.25rem;
767 - align-items: flex-start;
825 + .memory-table tbody tr.memory-row>td {
826 + height: 5em;
827 }
828
829 .source-type {
830 padding: 0.25rem 0.5rem;
772 - border-radius: 6px;
831 + border-radius: var(--border-radius);
832 font-size: 0.75rem;
833 font-weight: 500;
834 text-align: center;
@@ -778,54 +837,12 @@
837 flex-shrink: 0;
838 }
839
781 - .source-type.knowledge {
782 - background: rgba(115, 122, 129, 0.2);
783 - color: var(--color-primary);
784 - border-color: var(--color-primary);
785 - }
786 -
787 - .light-mode .source-type.knowledge {
788 - background: rgba(56, 70, 83, 0.1);
789 - color: var(--color-primary);
790 - }
791 -
792 - .source-type.conversation {
793 - background: rgba(101, 101, 101, 0.2);
794 - color: var(--color-text);
795 - opacity: 0.8;
796 - border-color: var(--color-border);
797 - }
798 -
799 - .light-mode .source-type.conversation {
800 - background: rgba(232, 234, 246, 0.5);
801 - color: var(--color-text);
802 - opacity: 0.9;
803 - }
804 -
805 - .source-file {
806 - font-size: 0.7rem;
807 - color: var(--color-text);
808 - opacity: 0.7;
809 - font-family: monospace;
810 - background: var(--color-panel);
811 - padding: 0.2rem 0.4rem;
812 - border-radius: 4px;
813 - border: 1px solid var(--color-border);
814 - word-break: break-all;
815 - white-space: normal;
816 - max-width: 100%;
817 - overflow-wrap: break-word;
818 - }
819 -
820 - .content-preview {
821 - line-height: 1.4;
822 - font-size: 0.9rem;
823 - color: var(--color-text);
840 + .content-preview,
841 + .metadata-info {
842 + height: 5.5em;
843 + overflow: hidden;
844 word-wrap: break-word;
845 overflow-wrap: break-word;
826 - max-width: 100%;
827 - overflow: hidden;
828 - text-overflow: ellipsis;
846 }
847
848 .tags {
@@ -846,10 +863,7 @@
863 }
864
865 .actions-cell {
849 - min-width: 60px;
850 - width: 60px;
866 padding: 0 !important;
852 - height: 60px;
867 position: relative;
868 }
869
@@ -865,28 +879,19 @@
879 justify-content: center;
880 }
881
868 - .actions-cell .btn-action {
869 - display: block;
870 - margin: 0.1rem 0;
871 - width: 28px;
872 - height: 28px;
873 - }
874 -
882 + /* Table action buttons - extends .btn */
883 .btn-action {
884 background: none;
885 border: 1px solid var(--color-border);
878 - padding: 0.3rem;
886 + padding: 0.25rem;
887 margin: 0 0.1rem;
880 - cursor: pointer;
881 - border-radius: 6px;
888 color: var(--color-text);
889 opacity: 0.7;
884 - transition: all 0.2s ease;
890 display: inline-flex;
886 - align-items: center;
887 - justify-content: center;
888 - min-width: 32px;
889 - height: 32px;
891 + }
892 +
893 + .btn-action .material-symbols-outlined {
894 + font-size: 18px;
895 }
896
897 .btn-action:hover {
@@ -915,408 +920,142 @@
920 color: var(--color-accent);
921 }
922
918 -
919 -
920 - .pagination-info-inline {
921 - color: var(--color-text);
922 - opacity: 0.7;
923 - font-size: 0.9rem;
924 - }
925 -
926 - .pagination-controls {
927 - display: flex;
928 - align-items: center;
929 - gap: 0.5rem;
930 - }
931 -
932 - .page-select {
933 - background: var(--color-background);
934 - border: 1px solid var(--color-border);
935 - color: var(--color-text);
936 - padding: 0.5rem 0.75rem;
937 - border-radius: 6px;
938 - font-size: 0.9rem;
939 - cursor: pointer;
940 - min-width: 120px;
941 - }
942 -
943 - .page-select:focus {
944 - outline: none;
945 - border-color: var(--color-primary);
946 - box-shadow: 0 0 0 2px rgba(115, 122, 129, 0.2);
947 - background: var(--color-input-focus);
948 - }
949 -
950 - .light-mode .page-select:focus {
951 - box-shadow: 0 0 0 2px rgba(56, 70, 83, 0.1);
952 - }
953 -
954 -
955 -
956 - .export-section {
957 - text-align: center;
958 - margin-top: 1rem;
959 - padding: 1rem;
960 - background: var(--color-panel);
961 - border: 1px solid var(--color-border);
962 - border-radius: 8px;
963 - border-top: 1px solid var(--color-border);
964 - }
965 -
966 - /* Enhanced Modal Styles */
967 - .modal-overlay {
968 - position: fixed;
969 - top: 0;
970 - left: 0;
971 - right: 0;
972 - bottom: 0;
973 - background: rgba(0, 0, 0, 0.6);
974 - backdrop-filter: blur(4px);
975 - display: flex;
976 - align-items: flex-start;
977 - justify-content: center;
978 - z-index: 1000;
979 - overflow-y: auto;
980 - padding: 2rem 0;
981 - }
982 -
983 - .modal-enter {
984 - transition: all 0.3s ease;
985 - }
986 -
987 - .modal-enter-start {
988 - opacity: 0;
989 - }
990 -
991 - .modal-enter-end {
992 - opacity: 1;
993 - }
994 -
995 - .modal-leave {
996 - transition: all 0.2s ease;
997 - }
998 -
999 - .modal-leave-start {
1000 - opacity: 1;
1001 - }
1002 -
1003 - .modal-leave-end {
1004 - opacity: 0;
1005 - }
1006 -
1007 - .memory-detail-modal {
1008 - background: var(--color-background);
1009 - border: 1px solid var(--color-border);
1010 - border-radius: 16px;
1011 - width: 95%;
1012 - max-width: 1200px;
1013 - max-height: 90vh;
1014 - min-height: 60vh;
1015 - overflow: hidden;
1016 - display: flex;
1017 - flex-direction: column;
1018 - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
1019 - animation: modalSlideIn 0.3s ease-out;
1020 - margin: 0 auto;
1021 - }
1022 -
1023 - @keyframes modalSlideIn {
1024 - from {
1025 - transform: translateY(20px) scale(0.95);
1026 - opacity: 0;
923 + /* Responsive design */
924 + @media (max-width: 870px) {
925 + .memory-dashboard {
926 + padding: 0.5rem;
927 }
928
1029 - to {
1030 - transform: translateY(0) scale(1);
1031 - opacity: 1;
929 + .memory-filters-header {
930 + padding: 0.75rem;
931 }
1033 - }
1034 -
1035 - .modal-header-enhanced {
1036 - background: var(--color-panel);
1037 - padding: 1.5rem 2rem;
1038 - border-bottom: 2px solid var(--color-border);
1039 - display: flex;
1040 - justify-content: space-between;
1041 - align-items: flex-start;
1042 - }
1043 -
1044 - .header-left {
1045 - flex: 1;
1046 - }
1047 -
1048 - .memory-title {
1049 - display: flex;
1050 - align-items: center;
1051 - gap: 1rem;
1052 - margin-bottom: 0.75rem;
1053 - }
1054 -
1055 - .memory-title h3 {
1056 - margin: 0;
1057 - font-size: 1.5rem;
1058 - font-weight: 600;
1059 - color: var(--color-text);
1060 - }
1061 -
1062 - .area-badge-large {
1063 - padding: 0.5rem 1rem;
1064 - border-radius: 20px;
1065 - color: white;
1066 - font-size: 0.75rem;
1067 - font-weight: 700;
1068 - text-transform: uppercase;
1069 - letter-spacing: 0.5px;
1070 - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1071 - }
1072 -
1073 - .memory-meta-quick {
1074 - display: flex;
1075 - gap: 0.75rem;
1076 - align-items: center;
1077 - flex-wrap: wrap;
1078 - }
1079 -
1080 - .timestamp-badge {
1081 - background: var(--color-panel);
1082 - border: 1px solid var(--color-border);
1083 - color: var(--color-text);
1084 - opacity: 0.8;
1085 - padding: 0.25rem 0.75rem;
1086 - border-radius: 12px;
1087 - font-size: 0.8rem;
1088 - font-weight: 500;
1089 - }
1090 -
1091 - .source-badge {
1092 - padding: 0.25rem 0.75rem;
1093 - border-radius: 12px;
1094 - font-size: 0.8rem;
1095 - font-weight: 500;
1096 - border: 1px solid;
1097 - }
1098 -
1099 - .source-badge.knowledge {
1100 - background: rgba(115, 122, 129, 0.1);
1101 - color: var(--color-primary);
1102 - border-color: var(--color-primary);
1103 - }
1104 -
1105 - .source-badge.conversation {
1106 - background: rgba(101, 101, 101, 0.1);
1107 - color: var(--color-text);
1108 - opacity: 0.8;
1109 - border-color: var(--color-border);
1110 - }
1111 -
1112 - .header-actions {
1113 - display: flex;
1114 - gap: 0.5rem;
1115 - align-items: flex-start;
1116 - }
1117 -
1118 - .btn-action-header {
1119 - background: var(--color-background);
1120 - border: 1px solid var(--color-border);
1121 - padding: 0.75rem;
1122 - border-radius: 12px;
1123 - cursor: pointer;
1124 - color: var(--color-secondary);
1125 - transition: all 0.2s ease;
1126 - display: flex;
1127 - align-items: center;
1128 - justify-content: center;
1129 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1130 - }
1131 -
1132 - .btn-action-header:hover {
1133 - background: rgba(255, 255, 255, 0.1);
1134 - border-color: var(--color-primary);
1135 - transform: translateY(-1px);
1136 - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1137 - }
1138 -
1139 - .btn-action-header.copy:hover {
1140 - background: rgba(76, 175, 80, 0.05);
1141 - border-color: #4CAF50;
1142 - color: #4CAF50;
1143 - }
1144 -
1145 - .btn-action-header.delete:hover {
1146 - background: rgba(244, 67, 54, 0.05);
1147 - border-color: #f44336;
1148 - color: #f44336;
1149 - }
1150 -
1151 - .btn-close-enhanced {
1152 - background: rgba(207, 102, 121, 0.1);
1153 - border: 1px solid var(--color-accent);
1154 - padding: 0.75rem;
1155 - border-radius: 12px;
1156 - cursor: pointer;
1157 - color: var(--color-accent);
1158 - transition: all 0.2s ease;
1159 - display: flex;
1160 - align-items: center;
1161 - justify-content: center;
1162 - }
1163 -
1164 - .btn-close-enhanced:hover {
1165 - background: rgba(207, 102, 121, 0.15);
1166 - border-color: var(--color-accent);
1167 - transform: translateY(-1px);
1168 - }
1169 -
1170 - .modal-body-enhanced {
1171 - display: flex;
1172 - flex: 1;
1173 - overflow: hidden;
1174 - min-height: 0;
1175 - }
1176 -
1177 - .content-section-main {
1178 - flex: 2;
1179 - padding: 2rem;
1180 - overflow-y: auto;
1181 - background: var(--color-background);
1182 - }
1183 -
1184 - .metadata-sidebar {
1185 - flex: 1;
1186 - min-width: 320px;
1187 - background: var(--color-panel);
1188 - border-left: 1px solid var(--color-border);
1189 - padding: 2rem;
1190 - overflow-y: auto;
1191 - }
932
1193 - .section-header {
1194 - margin-bottom: 1.5rem;
1195 - }
933 + .memory-filters-header {
934 + display: flex;
935 + flex-wrap: wrap;
936 + gap: 0.75rem;
937 + }
938
1197 - .section-header h4 {
1198 - margin: 0;
1199 - font-size: 1.1rem;
1200 - font-weight: 600;
1201 - color: var(--color-text);
1202 - display: flex;
1203 - align-items: center;
1204 - gap: 0.5rem;
1205 - }
939 + /* Reset grid positioning */
940 + .filter-memory-dir,
941 + .filter-area,
942 + .filter-limit,
943 + .filter-threshold,
944 + #search-input,
945 + .btn-search,
946 + .btn-clear {
947 + grid-column: auto;
948 + grid-row: auto;
949 + }
950
1207 - .content-display {
1208 - background: var(--color-background);
1209 - border: 1px solid var(--color-border);
1210 - border-radius: 12px;
1211 - padding: 1.5rem;
1212 - font-size: 0.95rem;
1213 - line-height: 1.6;
1214 - color: var(--color-text);
1215 - white-space: pre-wrap;
1216 - max-height: 60vh;
1217 - overflow-y: auto;
1218 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
1219 - }
951 + .filter-group-inline {
952 + width: 100%;
953 + flex-direction: row;
954 + align-items: center;
955 + gap: 0.5rem;
956 + }
957
1221 - .metadata-grid {
1222 - display: flex;
1223 - flex-direction: column;
1224 - gap: 1.25rem;
1225 - }
958 + .filter-group-inline label {
959 + font-size: 0.9rem;
960 + margin-bottom: 0.25rem;
961 + }
962
1227 - .metadata-item {
1228 - display: flex;
1229 - flex-direction: column;
1230 - gap: 0.5rem;
1231 - }
963 + .filter-group-inline select,
964 + .filter-group-inline input {
965 + height: 42px;
966 + font-size: 1rem;
967 + }
968
1233 - .metadata-item label {
1234 - font-size: 0.8rem;
1235 - font-weight: 600;
1236 - color: var(--color-secondary);
1237 - text-transform: uppercase;
1238 - letter-spacing: 0.5px;
1239 - }
969 + .filter-group-inline.filter-limit {
970 + flex: 33%;
971 + }
972
1241 - .metadata-item value {
1242 - font-size: 0.9rem;
1243 - color: var(--color-text);
1244 - background: var(--color-background);
1245 - padding: 0.75rem;
1246 - border-radius: 8px;
1247 - border: 1px solid var(--color-border);
1248 - word-break: break-all;
1249 - }
973 + .filter-limit input {
974 + flex: 1;
975 + }
976
1251 - .metadata-item value.monospace {
1252 - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
1253 - font-size: 0.85rem;
1254 - }
977 + /* Mobile order and layout - force items to specific widths */
978 + .filter-memory-dir {
979 + order: 1;
980 + flex: 1 1 100%;
981 + }
982
1256 - .consolidation-badge {
1257 - background: var(--color-accent) !important;
1258 - color: white !important;
1259 - padding: 0.5rem 1rem !important;
1260 - border-radius: 20px !important;
1261 - font-weight: 600 !important;
1262 - text-transform: capitalize !important;
1263 - border: none !important;
1264 - }
983 + .filter-area {
984 + order: 2;
985 + flex: 1 1 100%;
986 + }
987
1266 - .tags-display {
1267 - display: flex;
1268 - flex-wrap: wrap;
1269 - gap: 0.5rem;
1270 - background: var(--color-background);
1271 - padding: 0.75rem;
1272 - border-radius: 8px;
1273 - border: 1px solid var(--color-border);
1274 - }
988 + .filter-limit {
989 + order: 3;
990 + flex: 1 1 100%;
991 + }
992
1276 - .tag-pill {
1277 - background: var(--color-primary);
1278 - color: white;
1279 - padding: 0.25rem 0.75rem;
1280 - border-radius: 12px;
1281 - font-size: 0.75rem;
1282 - font-weight: 500;
1283 - }
993 + .btn-clear {
994 + order: 6;
995 + flex: 40%;
996 + min-width: 80px;
997 + padding: 0;
998 + height: 42px;
999 + justify-content: center;
1000 + }
1001
1285 - /* Responsive design */
1286 - @media (max-width: 768px) {
1287 - .modal-overlay {
1288 - padding: 1rem 0;
1002 + #search-input {
1003 + order: 5;
1004 + flex: auto;
1005 + width: 100%;
1006 + height: 42px;
1007 }
1008
1291 - .memory-dashboard {
1292 - padding: 0.5rem;
1009 + /* Search button and threshold on same row */
1010 + .btn-search {
1011 + order: 7;
1012 + flex: 40%;
1013 + height: 42px;
1014 + padding: 0 1rem;
1015 + min-width: 80px;
1016 }
1017
1295 - .stats-pagination-header {
1018 + .filter-threshold {
1019 + order: 4;
1020 + flex: 40%;
1021 flex-direction: column;
1297 - align-items: flex-start;
1298 - gap: 1rem;
1022 + align-items: stretch;
1023 + gap: 0.5rem;
1024 + padding: 0;
1025 }
1026
1301 - .memory-stats-compact {
1027 + .filter-threshold label {
1028 + font-size: 0.9rem;
1029 width: 100%;
1030 + display: flex;
1031 justify-content: space-between;
1032 }
1033
1306 - .pagination-controls-compact {
1307 - align-self: stretch;
1308 - justify-content: center;
1034 + .filter-threshold input[type="range"] {
1035 + width: 100%;
1036 }
1037
1311 - .filter-row {
1038 + .memory-status-bar {
1039 flex-direction: column;
1313 - align-items: stretch;
1040 + gap: 0.75rem;
1041 + }
1042 +
1043 + .status-info {
1044 + width: 100%;
1045 + justify-content: center;
1046 + flex-wrap: wrap;
1047 + gap: 0.5rem 1rem;
1048 + }
1049 +
1050 + .status-info .material-symbols-outlined {
1051 + font-size: 18px;
1052 }
1053
1316 - .filter-group input,
1317 - .filter-group select {
1318 - min-width: unset;
1054 + .status-pagination {
1055 width: 100%;
1056 + justify-content: center;
1057 + padding-top: 0.5rem;
1058 + border-top: 1px solid var(--color-border);
1059 }
1060
1061 .memory-table th,
@@ -1345,298 +1084,6 @@
1084 .col-actions {
1085 width: 10%;
1086 }
1348 -
1349 - .memory-detail-modal {
1350 - width: 98%;
1351 - max-height: 95vh;
1352 - min-height: 50vh;
1353 - margin: 0;
1354 - }
1355 -
1356 - .modal-body-enhanced {
1357 - flex-direction: column;
1358 - }
1359 -
1360 - .metadata-sidebar {
1361 - min-width: unset;
1362 - border-left: none;
1363 - border-top: 1px solid var(--color-border);
1364 - }
1365 -
1366 - .modal-header-enhanced {
1367 - padding: 1rem;
1368 - }
1369 -
1370 - .memory-title {
1371 - flex-direction: column;
1372 - align-items: flex-start;
1373 - gap: 0.5rem;
1374 - }
1375 -
1376 - .content-section-main,
1377 - .metadata-sidebar {
1378 - padding: 1rem;
1379 - }
1380 -
1381 -
1382 -
1383 - .pagination-controls {
1384 - flex-wrap: wrap;
1385 - }
1386 - }
1387 -
1388 - /* Memory Detail Modal Styles */
1389 - .modal-header-enhanced {
1390 - display: flex;
1391 - justify-content: space-between;
1392 - align-items: flex-start;
1393 - padding: 1.5rem;
1394 - border-bottom: 2px solid var(--color-border);
1395 - background: linear-gradient(135deg, var(--color-panel) 0%, var(--color-background) 100%);
1396 - }
1397 -
1398 - .memory-title {
1399 - display: flex;
1400 - align-items: center;
1401 - gap: 1rem;
1402 - margin-bottom: 0.5rem;
1403 - }
1404 -
1405 - .area-badge-large {
1406 - color: white;
1407 - padding: 0.5rem 1rem;
1408 - border-radius: 20px;
1409 - font-size: 0.8rem;
1410 - font-weight: bold;
1411 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
1412 - }
1413 -
1414 - .memory-meta-quick {
1415 - display: flex;
1416 - gap: 1rem;
1417 - align-items: center;
1418 - }
1419 -
1420 - .timestamp-badge,
1421 - .source-badge {
1422 - padding: 0.25rem 0.75rem;
1423 - border-radius: 12px;
1424 - font-size: 0.75rem;
1425 - font-weight: 500;
1426 - background: var(--color-panel);
1427 - border: 1px solid var(--color-border);
1428 - }
1429 -
1430 - .source-badge.knowledge {
1431 - background: rgba(115, 122, 129, 0.1);
1432 - color: var(--color-primary);
1433 - border-color: var(--color-primary);
1434 - }
1435 -
1436 - .source-badge.conversation {
1437 - background: rgba(101, 101, 101, 0.1);
1438 - color: var(--color-text);
1439 - opacity: 0.8;
1440 - border-color: var(--color-border);
1441 - }
1442 -
1443 - .header-actions {
1444 - display: flex;
1445 - gap: 0.5rem;
1446 - }
1447 -
1448 - .btn-action-header {
1449 - background: var(--color-background);
1450 - border: 1px solid var(--color-border);
1451 - color: var(--color-text);
1452 - padding: 0.5rem;
1453 - border-radius: 8px;
1454 - cursor: pointer;
1455 - transition: all 0.2s ease;
1456 - }
1457 -
1458 - .btn-action-header:hover {
1459 - border-color: var(--color-primary);
1460 - background: rgba(115, 122, 129, 0.1);
1461 - }
1462 -
1463 - .modal-body-enhanced {
1464 - display: flex;
1465 - min-height: 60vh;
1466 - max-height: 70vh;
1467 - }
1468 -
1469 - .content-section-main {
1470 - flex: 1;
1471 - padding: 1.5rem;
1472 - overflow-y: auto;
1473 - }
1474 -
1475 - .content-block {
1476 - margin-bottom: 2rem;
1477 - }
1478 -
1479 - .content-block h4 {
1480 - margin: 0 0 1rem 0;
1481 - color: var(--color-primary);
1482 - font-size: 1.1rem;
1483 - font-weight: 600;
1484 - border-bottom: 1px solid var(--color-border);
1485 - padding-bottom: 0.5rem;
1486 - }
1487 -
1488 - .memory-content-display {
1489 - background: var(--color-panel);
1490 - border: 1px solid var(--color-border);
1491 - border-radius: 8px;
1492 - padding: 1.5rem;
1493 - line-height: 1.6;
1494 - color: var(--color-text);
1495 - white-space: pre-wrap;
1496 - word-break: break-word;
1497 - max-height: 300px;
1498 - overflow-y: auto;
1499 - }
1500 -
1501 - .tags-display {
1502 - display: flex;
1503 - flex-wrap: wrap;
1504 - gap: 0.5rem;
1505 - }
1506 -
1507 - .tag-display {
1508 - background: var(--color-primary);
1509 - color: white;
1510 - padding: 0.25rem 0.75rem;
1511 - border-radius: 12px;
1512 - font-size: 0.75rem;
1513 - font-weight: 500;
1514 - }
1515 -
1516 - .metadata-sidebar {
1517 - min-width: 300px;
1518 - max-width: 350px;
1519 - background: var(--color-panel);
1520 - border-left: 1px solid var(--color-border);
1521 - padding: 1.5rem;
1522 - overflow-y: auto;
1523 - }
1524 -
1525 - .metadata-sidebar h4 {
1526 - margin: 0 0 1rem 0;
1527 - color: var(--color-primary);
1528 - font-size: 1.1rem;
1529 - font-weight: 600;
1530 - }
1531 -
1532 - .metadata-group {
1533 - margin-bottom: 1.5rem;
1534 - }
1535 -
1536 - .metadata-group h5 {
1537 - margin: 0 0 0.75rem 0;
1538 - color: var(--color-text);
1539 - opacity: 0.7;
1540 - font-size: 0.9rem;
1541 - font-weight: 600;
1542 - text-transform: uppercase;
1543 - letter-spacing: 0.5px;
1544 - }
1545 -
1546 - .metadata-item {
1547 - display: flex;
1548 - flex-direction: column;
1549 - gap: 0.25rem;
1550 - margin-bottom: 0.75rem;
1551 - padding: 0.5rem;
1552 - background: var(--color-background);
1553 - border: 1px solid var(--color-border);
1554 - border-radius: 6px;
1555 - }
1556 -
1557 - .metadata-label {
1558 - font-size: 0.8rem;
1559 - font-weight: 600;
1560 - color: var(--color-text);
1561 - opacity: 0.7;
1562 - text-transform: uppercase;
1563 - letter-spacing: 0.3px;
1564 - }
1565 -
1566 - .metadata-value {
1567 - font-size: 0.9rem;
1568 - color: var(--color-text);
1569 - word-break: break-all;
1570 - line-height: 1.4;
1571 - }
1572 -
1573 - .source-file-display {
1574 - font-family: monospace;
1575 - background: var(--color-panel);
1576 - padding: 0.5rem;
1577 - border-radius: 4px;
1578 - border: 1px solid var(--color-border);
1579 - }
1580 -
1581 - .metadata-actions {
1582 - display: flex;
1583 - flex-direction: column;
1584 - gap: 0.5rem;
1585 - }
1586 -
1587 - .metadata-actions .btn {
1588 - display: flex;
1589 - align-items: center;
1590 - gap: 0.5rem;
1591 - justify-content: flex-start;
1592 - font-size: 0.9rem;
1593 - }
1594 -
1595 - /* Light mode adjustments for modal */
1596 - .light-mode .modal-header-enhanced {
1597 - background: linear-gradient(135deg, rgba(240, 240, 240, 0.8) 0%, rgba(255, 255, 255, 0.9) 100%);
1598 - }
1599 -
1600 - .light-mode .memory-content-display {
1601 - background: rgba(248, 248, 248, 0.8);
1602 - }
1603 -
1604 - .light-mode .metadata-sidebar {
1605 - background: rgba(248, 248, 248, 0.8);
1606 - }
1607 -
1608 - .light-mode .metadata-item {
1609 - background: rgba(255, 255, 255, 0.9);
1610 - }
1611 -
1612 - /* Responsive design for modal */
1613 - @media (max-width: 768px) {
1614 - .modal-body-enhanced {
1615 - flex-direction: column;
1616 - max-height: 80vh;
1617 - }
1618 -
1619 - .metadata-sidebar {
1620 - min-width: unset;
1621 - max-width: unset;
1622 - border-left: none;
1623 - border-top: 1px solid var(--color-border);
1624 - }
1625 -
1626 - .modal-header-enhanced {
1627 - padding: 1rem;
1628 - }
1629 -
1630 - .memory-title {
1631 - flex-direction: column;
1632 - align-items: flex-start;
1633 - gap: 0.5rem;
1634 - }
1635 -
1636 - .content-section-main,
1637 - .metadata-sidebar {
1638 - padding: 1rem;
1639 - }
1087 }
1088 </style>
1089
webui/components/settings/memory/memory-detail-modal.html
+287 -266
@@ -4,285 +4,38 @@
4 <title>Memory Details</title>
5 </head>
6
7 -<style>
8 - /* Memory Detail Modal Styles */
9 - .modal-header-enhanced {
10 - display: flex;
11 - justify-content: space-between;
12 - align-items: flex-start;
13 - padding: 1.5rem;
14 - border-bottom: 2px solid var(--color-border);
15 - background: var(--color-panel);
16 - }
17 -
18 - .memory-title {
19 - display: flex;
20 - align-items: center;
21 - gap: 1rem;
22 - margin-bottom: 0.5rem;
23 - }
24 -
25 - .area-badge-large {
26 - color: white;
27 - padding: 0.5rem 1rem;
28 - border-radius: 20px;
29 - font-size: 0.8rem;
30 - font-weight: bold;
31 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
32 - }
33 -
34 - .memory-meta-quick {
35 - display: flex;
36 - gap: 1rem;
37 - align-items: center;
38 - }
39 -
40 - .timestamp-badge, .source-badge {
41 - padding: 0.25rem 0.75rem;
42 - border-radius: 12px;
43 - font-size: 0.75rem;
44 - font-weight: 500;
45 - background: var(--color-panel);
46 - border: 1px solid var(--color-border);
47 - color: var(--color-text);
48 - opacity: 0.8;
49 - }
50 -
51 - .source-badge.knowledge {
52 - background: rgba(115, 122, 129, 0.1);
53 - color: var(--color-primary);
54 - border-color: var(--color-primary);
55 - opacity: 1;
56 - }
57 -
58 - .source-badge.conversation {
59 - background: rgba(101, 101, 101, 0.1);
60 - color: var(--color-text);
61 - opacity: 0.8;
62 - border-color: var(--color-border);
63 - }
64 -
65 - .header-actions {
66 - display: flex;
67 - gap: 0.5rem;
68 - }
69 -
70 - .btn-action-header {
71 - background: var(--color-background);
72 - border: 1px solid var(--color-border);
73 - color: var(--color-text);
74 - padding: 0.5rem;
75 - border-radius: 8px;
76 - cursor: pointer;
77 - transition: all 0.2s ease;
78 - }
79 -
80 - .btn-action-header:hover {
81 - border-color: var(--color-primary);
82 - background: var(--color-panel);
83 - }
84 -
85 - .modal-body-enhanced {
86 - display: flex;
87 - min-height: 60vh;
88 - max-height: 70vh;
89 - }
90 -
91 - .content-section-main {
92 - flex: 1;
93 - padding: 1.5rem;
94 - overflow-y: auto;
95 - }
96 -
97 - .content-block {
98 - margin-bottom: 2rem;
99 - }
100 -
101 - .content-block h4 {
102 - margin: 0 0 1rem 0;
103 - color: var(--color-text);
104 - font-size: 1.1rem;
105 - font-weight: 600;
106 - border-bottom: 1px solid var(--color-border);
107 - padding-bottom: 0.5rem;
108 - }
109 -
110 - .memory-content-display {
111 - background: var(--color-panel);
112 - border: 1px solid var(--color-border);
113 - border-radius: 8px;
114 - padding: 1.5rem;
115 - line-height: 1.6;
116 - color: var(--color-text);
117 - white-space: pre-wrap;
118 - word-break: break-word;
119 - max-height: 300px;
120 - overflow-y: auto;
121 - }
122 -
123 - .tags-display {
124 - display: flex;
125 - flex-wrap: wrap;
126 - gap: 0.5rem;
127 - }
128 -
129 - .tag-display {
130 - background: var(--color-primary);
131 - color: white;
132 - padding: 0.25rem 0.75rem;
133 - border-radius: 12px;
134 - font-size: 0.75rem;
135 - font-weight: 500;
136 - }
137 -
138 - .metadata-sidebar {
139 - min-width: 300px;
140 - max-width: 350px;
141 - background: var(--color-panel);
142 - border-left: 1px solid var(--color-border);
143 - padding: 1.5rem;
144 - overflow-y: auto;
145 - }
146 -
147 - .metadata-sidebar h4 {
148 - margin: 0 0 1rem 0;
149 - color: var(--color-text);
150 - font-size: 1.1rem;
151 - font-weight: 600;
152 - }
153 -
154 - .metadata-group {
155 - margin-bottom: 1.5rem;
156 - }
157 -
158 - .metadata-group h5 {
159 - margin: 0 0 0.75rem 0;
160 - color: var(--color-text);
161 - opacity: 0.7;
162 - font-size: 0.9rem;
163 - font-weight: 600;
164 - text-transform: uppercase;
165 - letter-spacing: 0.5px;
166 - }
167 -
168 - .metadata-item {
169 - display: flex;
170 - flex-direction: column;
171 - gap: 0.25rem;
172 - margin-bottom: 0.75rem;
173 - padding: 0.5rem;
174 - background: var(--color-background);
175 - border: 1px solid var(--color-border);
176 - border-radius: 6px;
177 - }
178 -
179 - .metadata-label {
180 - font-size: 0.8rem;
181 - font-weight: 600;
182 - color: var(--color-text);
183 - opacity: 0.7;
184 - text-transform: uppercase;
185 - letter-spacing: 0.3px;
186 - }
187 -
188 - .metadata-value {
189 - font-size: 0.9rem;
190 - color: var(--color-text);
191 - word-break: break-all;
192 - line-height: 1.4;
193 - }
194 -
195 - .source-file-display {
196 - font-family: monospace;
197 - background: var(--color-panel);
198 - padding: 0.5rem;
199 - border-radius: 4px;
200 - border: 1px solid var(--color-border);
201 - }
202 -
203 - .metadata-actions {
204 - display: flex;
205 - flex-direction: column;
206 - gap: 0.5rem;
207 - }
208 -
209 - .metadata-actions .btn {
210 - display: flex;
211 - align-items: center;
212 - gap: 0.5rem;
213 - justify-content: flex-start;
214 - font-size: 0.9rem;
215 - }
216 -
217 - /* Responsive design for modal */
218 - @media (max-width: 768px) {
219 - .modal-body-enhanced {
220 - flex-direction: column;
221 - max-height: 80vh;
222 - }
223 -
224 - .metadata-sidebar {
225 - min-width: unset;
226 - max-width: unset;
227 - border-left: none;
228 - border-top: 1px solid var(--color-border);
229 - }
230 -
231 - .modal-header-enhanced {
232 - padding: 1rem;
233 - }
234 -
235 - .memory-title {
236 - flex-direction: column;
237 - align-items: flex-start;
238 - gap: 0.5rem;
239 - }
240 -
241 - .content-section-main,
242 - .metadata-sidebar {
243 - padding: 1rem;
244 - }
245 - }
246 -</style>
247 -
7 <body>
8 <div x-data>
9 <template x-if="$store.memoryDashboardStore && $store.memoryDashboardStore.detailMemory">
251 - <div>
252 - <!-- Modal Header with Memory Info -->
253 - <div class="modal-header-enhanced">
254 - <div class="header-left">
255 - <div class="memory-title">
256 - <span class="area-badge-large"
257 - :style="`background-color: ${$store.memoryDashboardStore.getAreaColor($store.memoryDashboardStore.detailMemory?.area)}`"
258 - x-text="$store.memoryDashboardStore.detailMemory?.area?.toUpperCase() || 'UNKNOWN'"></span>
259 - </div>
260 - <div class="memory-meta-quick">
261 - <span class="timestamp-badge" x-text="$store.memoryDashboardStore.formatTimestamp($store.memoryDashboardStore.detailMemory?.timestamp, true)"></span>
262 - <span class="source-badge"
263 - :class="$store.memoryDashboardStore.detailMemory?.knowledge_source ? 'knowledge' : 'conversation'"
264 - x-text="$store.memoryDashboardStore.detailMemory?.knowledge_source ? 'Knowledge' : 'Conversation'"></span>
265 - </div>
10 + <div class="memory-detail-container">
11 + <!-- Compact Modal Header -->
12 + <div class="modal-header modal-header-compact">
13 + <div class="header-info">
14 + <span class="area-badge"
15 + :style="`background-color: ${$store.memoryDashboardStore.getAreaColor($store.memoryDashboardStore.detailMemory?.area)}`"
16 + x-text="$store.memoryDashboardStore.detailMemory?.area?.toUpperCase() || 'UNKNOWN'"></span>
17 + <span class="timestamp-text" x-text="$store.memoryDashboardStore.formatTimestamp($store.memoryDashboardStore.detailMemory?.timestamp, true)"></span>
18 + <span class="source-text" x-text="$store.memoryDashboardStore.detailMemory?.knowledge_source ? 'Knowledge' : 'Conversation'"></span>
19 </div>
20 <div class="header-actions">
21 <!-- View Mode Buttons -->
22 <template x-if="!$store.memoryDashboardStore.editMode">
23 <div style="display: flex; gap: 0.5rem;">
24
272 - <button class="btn-action-header copy-all" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.formatMemoryForCopy($store.memoryDashboardStore.detailMemory))"
25 + <button class="btn btn-action-header copy-all" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.formatMemoryForCopy($store.memoryDashboardStore.detailMemory))"
26 title="Copy Complete Memory with Metadata">
27 <span class="material-symbols-outlined">copy_all</span> All
28 </button>
276 - <button class="btn-action-header copy-content" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.detailMemory?.content_full)"
29 + <button class="btn btn-action-header copy-content" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.detailMemory?.content_full)"
30 title="Copy Content Only">
31 <span class="material-symbols-outlined">content_copy</span> Content
32 </button>
33 <div style="width:2em"></div>
281 - <button class="btn-action-header edit" @click="$store.memoryDashboardStore.enableEditMode()"
34 + <button class="btn btn-action-header edit" @click="$store.memoryDashboardStore.enableEditMode()"
35 title="Edit Memory">
36 <span class="material-symbols-outlined">edit_document</span>
37 </button>
285 - <button class="btn-action-header delete danger" @click="$store.memoryDashboardStore.deleteMemory($store.memoryDashboardStore.detailMemory)"
38 + <button class="btn btn-action-header delete danger" @click="$store.memoryDashboardStore.deleteMemory($store.memoryDashboardStore.detailMemory)"
39 title="Delete Memory">
40 <span class="material-symbols-outlined">delete</span>
41 </button>
@@ -291,11 +44,11 @@
44 <!-- Edit Mode Buttons -->
45 <template x-if="$store.memoryDashboardStore.editMode">
46 <div style="display: flex; gap: 0.5rem;">
294 - <button class="btn-action-header cancel" @click="$store.memoryDashboardStore.cancelEditMode()"
47 + <button class="btn btn-action-header cancel" @click="$store.memoryDashboardStore.cancelEditMode()"
48 title="Cancel Edit">
49 <span class="material-symbols-outlined">close</span>
50 </button>
298 - <button class="btn-action-header confirm" @click="$store.memoryDashboardStore.confirmEditMode()"
51 + <button class="btn btn-action-header confirm" @click="$store.memoryDashboardStore.confirmEditMode()"
52 title="Confirm Changes">
53 <span class="material-symbols-outlined">check</span>
54 </button>
@@ -306,16 +59,18 @@
59 </div>
60
61 <!-- Modal Body -->
309 - <div class="modal-body-enhanced">
62 + <div class="modal-body">
63 <!-- Main Content Section -->
64 <div class="content-section-main">
65 <div class="content-block">
66 <h4>Memory Content</h4>
67 <template x-if="!$store.memoryDashboardStore.editMode">
315 - <div class="memory-content-display" x-text="$store.memoryDashboardStore.detailMemory?.content_full"></div>
68 + <div class="memory-content-display">
69 + <p x-text="$store.memoryDashboardStore.detailMemory?.content_full"></p>
70 + </div>
71 </template>
72 <template x-if="$store.memoryDashboardStore.editMode">
318 - <textarea class="memory-content-display" style="width: 100%; height: 300px;" x-model="$store.memoryDashboardStore.detailMemory.content_full"></textarea>
73 + <textarea class="memory-content-display" x-model="$store.memoryDashboardStore.detailMemory.content_full"></textarea>
74 </template>
75 </div>
76
@@ -369,7 +124,7 @@
124
125 <!-- All Custom Metadata -->
126 <div class="metadata-group" x-show="$store.memoryDashboardStore.detailMemory?.metadata && Object.keys($store.memoryDashboardStore.detailMemory.metadata).length > 0">
372 - <h5>Additional Metadata</h5>
127 + <h5>Raw Metadata</h5>
128 <template x-for="[key, value] in Object.entries($store.memoryDashboardStore.detailMemory?.metadata || {})" :key="key">
129 <div class="metadata-item">
130 <span class="metadata-label" x-text="key + ':'"></span>
@@ -382,6 +137,272 @@
137 </div>
138 </div>
139 </template>
140 + <style>
141 + .modal-header-compact {
142 + background: linear-gradient(135deg, var(--color-panel) 0%, var(--color-background) 100%);
143 + position: sticky;
144 + padding-left: 0.8rem;
145 + border-radius: 6px;
146 + top: 0;
147 + z-index: 100;
148 + }
149 +
150 + .header-info {
151 + display: flex;
152 + align-items: center;
153 + gap: 0.5rem;
154 + font-size: 0.9rem;
155 + }
156 +
157 + .timestamp-text,
158 + .source-text {
159 + color: var(--color-text);
160 + opacity: 0.8;
161 + padding: 0.25rem 0.5rem;
162 + border-radius: var(--border-radius);
163 + font-size: 0.75rem;
164 + font-weight: 500;
165 + border: 1px solid;
166 + }
167 +
168 + .btn-action-header {
169 + padding: 0.4rem 0.6rem;
170 + display: flex;
171 + align-items: center;
172 + gap: 0.25rem;
173 + background: var(--color-message-bg);
174 + color: var(--color-text);
175 + }
176 +
177 + .btn-action-header .material-symbols-outlined {
178 + font-size: 18px;
179 + }
180 +
181 + .btn-action-header.delete:hover {
182 + border-color: var(--color-accent);
183 + color: var(--color-accent);
184 + }
185 +
186 + .btn-action-header.copy-all:hover,
187 + .btn-action-header.copy-content:hover,
188 + .btn-action-header.edit:hover {
189 + border-color: var(--color-primary);
190 + color: var(--color-primary);
191 + }
192 +
193 + .btn-action-header.confirm:hover {
194 + border-color: #4CAF50;
195 + color: #4CAF50;
196 + background: var(--color-background);
197 + }
198 +
199 + .btn-action-header.cancel:hover {
200 + border-color: var(--color-accent);
201 + color: var(--color-accent);
202 + background: var(--color-background);
203 + }
204 +
205 + .modal-body {
206 + display: flex;
207 + min-height: 60vh;
208 + max-height: 100%;
209 + }
210 +
211 + .content-section-main {
212 + flex: 1;
213 + padding: 1.5rem;
214 + padding-bottom: 0;
215 + overflow-y: hidden;
216 + display: flex;
217 + flex-direction: column;
218 + }
219 +
220 + .content-block {
221 + /* margin-bottom: 3rem; */
222 + display: flex;
223 + flex-direction: column;
224 + min-height: 0;
225 + }
226 +
227 + /* Make the first content block (Memory Content) take all available space */
228 + .content-block:first-child {
229 + flex: 1;
230 + /* margin-bottom: 1.5rem; */
231 + min-height: 0;
232 + }
233 +
234 + .content-block h4 {
235 + margin: 0 0 1rem 0;
236 + color: var(--color-text);
237 + font-size: 1.1rem;
238 + font-weight: 600;
239 + /* border-bottom: 1px solid var(--color-border); */
240 + /* padding-bottom: 0.5rem; */
241 + }
242 +
243 + .memory-content-display {
244 + border: 1px solid var(--color-border);
245 + border-radius: 8px;
246 + /* padding: 1.5rem; */
247 + line-height: 1.6;
248 + color: var(--color-text);
249 + /* Fixed height: parent container minus 1rem */
250 + flex: 1;
251 + min-height: 25em;
252 + /* Ensure textarea uses same styling */
253 + font-family: inherit;
254 + font-size: inherit;
255 + resize: none;
256 + width: 100%;
257 + box-sizing: border-box;
258 + overflow: hidden;
259 + display: flex;
260 + flex-direction: column;
261 + }
262 +
263 + .memory-content-display p {
264 + background: var(--color-panel);
265 + margin: 0;
266 + padding: 1.5rem;
267 + white-space: pre-wrap;
268 + word-break: break-word;
269 + overflow-y: auto;
270 + overflow-x: hidden;
271 + height: 0;
272 + flex-grow: 1;
273 + }
274 +
275 + textarea.memory-content-display {
276 + padding: 1.5rem;
277 + overflow-y: auto;
278 + display: block;
279 + }
280 +
281 + .tags-display {
282 + display: flex;
283 + flex-wrap: wrap;
284 + gap: 0.5rem;
285 + background: var(--color-background);
286 + padding: 0.75rem;
287 + border-radius: 8px;
288 + border: 1px solid var(--color-border);
289 + }
290 +
291 + .tag-display {
292 + background: var(--color-primary);
293 + color: white;
294 + padding: 0.25rem 0.75rem;
295 + border-radius: 12px;
296 + font-size: 0.75rem;
297 + font-weight: 500;
298 + }
299 +
300 + .metadata-sidebar {
301 + min-width: 300px;
302 + max-width: 350px;
303 + background: var(--color-panel);
304 + border-left: 1px solid var(--color-border);
305 + padding: 1.5rem;
306 + overflow-y: auto;
307 + max-height: 50em;
308 + }
309 +
310 + .metadata-sidebar h4 {
311 + margin: 0 0 1rem 0;
312 + color: var(--color-text);
313 + font-size: 1.1rem;
314 + font-weight: 600;
315 + }
316 +
317 + .metadata-group {
318 + margin-bottom: 1.5rem;
319 + }
320 +
321 + .metadata-group h5 {
322 + margin: 0 0 0.75rem 0;
323 + color: var(--color-text);
324 + opacity: 0.7;
325 + font-size: 0.9rem;
326 + font-weight: 600;
327 + text-transform: uppercase;
328 + letter-spacing: 0.5px;
329 + }
330 +
331 + .metadata-item {
332 + display: flex;
333 + flex-direction: column;
334 + gap: 0.25rem;
335 + margin-bottom: 0.75rem;
336 + padding: 0.5rem;
337 + background: var(--color-background);
338 + border: 1px solid var(--color-border);
339 + border-radius: 6px;
340 + }
341 +
342 + .metadata-label {
343 + font-size: 0.8rem;
344 + font-weight: 600;
345 + color: var(--color-text);
346 + opacity: 0.7;
347 + text-transform: uppercase;
348 + letter-spacing: 0.3px;
349 + }
350 +
351 + .metadata-value {
352 + font-size: 0.9rem;
353 + color: var(--color-text);
354 + word-break: break-all;
355 + line-height: 1.4;
356 + }
357 +
358 + .source-file-display {
359 + font-family: monospace;
360 + background: var(--color-panel);
361 + padding: 0.5rem;
362 + border-radius: 4px;
363 + border: 1px solid var(--color-border);
364 + }
365 +
366 + .memory-detail-container {
367 + display: flex;
368 + flex-direction: column;
369 + height: 100%;
370 + }
371 +
372 + /* Responsive design for modal */
373 + @media (max-width: 870px) {
374 + .modal-body {
375 + flex-direction: column;
376 + }
377 +
378 + .metadata-sidebar {
379 + min-width: unset;
380 + max-width: unset;
381 + min-height: unset;
382 + border-left: none;
383 + border-top: 1px solid var(--color-border);
384 + }
385 +
386 + .metadata-item {
387 + display: block;
388 + }
389 +
390 + .modal-header-compact {
391 + padding: 0.75rem 1rem;
392 + flex-wrap: wrap;
393 + gap: 0.75rem;
394 + }
395 +
396 + .header-info {
397 + flex-wrap: wrap;
398 + }
399 +
400 + .content-section-main,
401 + .metadata-sidebar {
402 + padding: 1rem;
403 + }
404 + }
405 + </style>
406 </div>
407 </body>
408
webui/components/sidebar/bottom/preferences/preferences-panel.html new
+119
@@ -0,0 +1,119 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/sidebar/bottom/preferences/preferences-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <div x-data>
9 + <template x-if="$store.preferences">
10 + <div class="pref-section" x-data="{ prefOpen: true }">
11 + <span>
12 + <h3 class="pref-header" @click="prefOpen = !prefOpen">
13 + Preferences
14 + <svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" :class="{'rotated': !prefOpen}">
15 + <path d="M8 4l8 8-8 8" />
16 + </svg>
17 + </h3>
18 + <ul class="config-list" id="pref-list" x-show="prefOpen" x-collapse x-transition>
19 + <li x-data x-init="$watch('$store.preferences.autoScroll', v => $store.preferences.applyAutoScroll(v))">
20 + <span>Autoscroll</span>
21 + <label class="switch">
22 + <input id="auto-scroll-switch" type="checkbox" x-model="$store.preferences.autoScroll">
23 + <span class="slider"></span>
24 + </label>
25 + </li>
26 + <li x-data x-init="$watch('$store.preferences.darkMode', v => $store.preferences.applyDarkMode(v))">
27 + <span class="switch-label">Dark mode</span>
28 + <label class="switch">
29 + <input type="checkbox" x-model="$store.preferences.darkMode">
30 + <span class="slider"></span>
31 + </label>
32 + </li>
33 + <li x-data x-init="$watch('$store.preferences.speech', v => $store.preferences.applySpeech(v))">
34 + <span class="switch-label">Speech</span>
35 + <label class="switch">
36 + <input type="checkbox" x-model="$store.preferences.speech">
37 + <span class="slider"></span>
38 + </label>
39 + </li>
40 + <li x-data x-init="$watch('$store.preferences.showThoughts', v => $store.preferences.applyShowThoughts(v))">
41 + <span>Show thoughts</span>
42 + <label class="switch">
43 + <input type="checkbox" x-model="$store.preferences.showThoughts">
44 + <span class="slider"></span>
45 + </label>
46 + </li>
47 + <li x-data x-init="$watch('$store.preferences.showJson', v => $store.preferences.applyShowJson(v))">
48 + <span>Show JSON</span>
49 + <label class="switch">
50 + <input type="checkbox" x-model="$store.preferences.showJson">
51 + <span class="slider"></span>
52 + </label>
53 + </li>
54 + <li x-data x-init="$watch('$store.preferences.showUtils', v => $store.preferences.applyShowUtils(v))">
55 + <span>Show utility messages</span>
56 + <label class="switch">
57 + <input type="checkbox" x-model="$store.preferences.showUtils">
58 + <span class="slider"></span>
59 + </label>
60 + </li>
61 + </ul>
62 + </span>
63 + </div>
64 + </template>
65 + </div>
66 +
67 + <style>
68 + .pref-section {
69 + font-size: var(--font-size-small);
70 + padding: 0.6rem var(--spacing-md) 0.05rem var(--spacing-md);
71 + }
72 + /* Collapse transition */
73 + .pref-section [x-cloak] {
74 + display: none;
75 + }
76 + .pref-section .pref-header {
77 + display: flex;
78 + align-items: center;
79 + justify-content: space-between;
80 + gap: 6px;
81 + cursor: pointer;
82 + user-select: none;
83 + }
84 + .pref-section .arrow-icon {
85 + margin-left: 0.5rem;
86 + transition: transform .15s ease;
87 + transform: rotate(90deg);
88 + }
89 + .pref-section .arrow-icon.rotated {
90 + transform: rotate(0deg);
91 + }
92 + .pref-section .config-list {
93 + list-style: none;
94 + padding: 0.05rem 0;
95 + margin: 0;
96 + display: flex;
97 + flex-direction: column;
98 + gap: 1px;
99 + }
100 + #pref-list li {
101 + opacity: 0.8;
102 + }
103 + .pref-section .switch-label,
104 + .pref-section span {
105 + font-size: 0.8rem;
106 + }
107 + .pref-section .switch {
108 + position: relative;
109 + display: inline-block;
110 + width: 35px;
111 + height: 19px;
112 + margin-left: auto;
113 + }
114 + .pref-section li { display: flex; align-items: center; }
115 + </style>
116 +</body>
117 +</html>
118 +
119 +
webui/components/sidebar/bottom/preferences/preferences-store.js new
+83
@@ -0,0 +1,83 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +
3 +// Preferences store centralizes user preference toggles and side-effects
4 +const model = {
5 + // UI toggles (initialized with safe defaults, loaded from localStorage in init)
6 + autoScroll: true,
7 + darkMode: true,
8 + speech: false,
9 + showThoughts: true,
10 + showJson: false,
11 + showUtils: false,
12 +
13 + // Initialize preferences and apply current state
14 + init() {
15 + try {
16 + // Load persisted preferences with safe fallbacks
17 + try {
18 + const storedDarkMode = localStorage.getItem("darkMode");
19 + this.darkMode = storedDarkMode !== "false";
20 + } catch {
21 + this.darkMode = true; // Default to dark mode if localStorage is unavailable
22 + }
23 +
24 + try {
25 + const storedSpeech = localStorage.getItem("speech");
26 + this.speech = storedSpeech === "true";
27 + } catch {
28 + this.speech = false; // Default to speech off if localStorage is unavailable
29 + }
30 +
31 + // Apply all preferences
32 + this.applyDarkMode(this.darkMode);
33 + this.applyAutoScroll(this.autoScroll);
34 + this.applySpeech(this.speech);
35 + this.applyShowThoughts(this.showThoughts);
36 + this.applyShowJson(this.showJson);
37 + this.applyShowUtils(this.showUtils);
38 + } catch (e) {
39 + console.error("Failed to initialize preferences store", e);
40 + }
41 + },
42 +
43 + // Side-effect appliers delegate to existing global handlers for parity
44 + applyAutoScroll(value = this.autoScroll) {
45 + if (typeof globalThis.toggleAutoScroll === "function") {
46 + globalThis.toggleAutoScroll(value);
47 + }
48 + },
49 +
50 + applyDarkMode(value = this.darkMode) {
51 + if (typeof globalThis.toggleDarkMode === "function") {
52 + globalThis.toggleDarkMode(value);
53 + }
54 + },
55 +
56 + applySpeech(value = this.speech) {
57 + if (typeof globalThis.toggleSpeech === "function") {
58 + globalThis.toggleSpeech(value);
59 + }
60 + },
61 +
62 + applyShowThoughts(value = this.showThoughts) {
63 + if (typeof globalThis.toggleThoughts === "function") {
64 + globalThis.toggleThoughts(value);
65 + }
66 + },
67 +
68 + applyShowJson(value = this.showJson) {
69 + if (typeof globalThis.toggleJson === "function") {
70 + globalThis.toggleJson(value);
71 + }
72 + },
73 +
74 + applyShowUtils(value = this.showUtils) {
75 + if (typeof globalThis.toggleUtils === "function") {
76 + globalThis.toggleUtils(value);
77 + }
78 + },
79 +};
80 +
81 +export const store = createStore("preferences", model);
82 +
83 +
webui/components/sidebar/bottom/sidebar-bottom-store.js new
+25
@@ -0,0 +1,25 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +
3 +// Sidebar Bottom store manages version info display
4 +const model = {
5 + versionNo: "",
6 + commitTime: "",
7 +
8 + get versionLabel() {
9 + return this.versionNo && this.commitTime
10 + ? `Version ${this.versionNo} ${this.commitTime}`
11 + : "";
12 + },
13 +
14 + init() {
15 + // Load version info from global scope (exposed in index.html)
16 + const gi = globalThis.gitinfo;
17 + if (gi && gi.version && gi.commit_time) {
18 + this.versionNo = gi.version;
19 + this.commitTime = gi.commit_time;
20 + }
21 + },
22 +};
23 +
24 +export const store = createStore("sidebarBottom", model);
25 +
webui/components/sidebar/bottom/sidebar-bottom.html new
+49
@@ -0,0 +1,49 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/sidebar/bottom/sidebar-bottom-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <div x-data>
9 + <template x-if="$store.sidebarBottom">
10 + <div class="sidebar-bottom-wrapper">
11 + <!-- Preferences Panel -->
12 + <x-component path="sidebar/bottom/preferences/preferences-panel.html"></x-component>
13 +
14 + <!-- Version Info -->
15 + <div class="version-info">
16 + <span id="a0version" x-text="$store.sidebarBottom.versionLabel"></span>
17 + </div>
18 + </div>
19 + </template>
20 + </div>
21 +
22 +
23 + <style>
24 + /* Sidebar bottom wrapper */
25 + .sidebar-bottom-wrapper {
26 + display: flex;
27 + flex-direction: column;
28 + flex-shrink: 0;
29 + }
30 +
31 + /* Version label area */
32 + .version-info {
33 + line-height: 0.8rem;
34 + position: relative;
35 + margin: 0 var(--spacing-md) 1rem var(--spacing-md);
36 + padding-top: 10px;
37 + border-top: 1px solid var(--color-border);
38 + }
39 +
40 + #a0version {
41 + color: var(--color-text);
42 + opacity: 0.7;
43 + font-size: 0.7rem;
44 + user-select: all;
45 + }
46 + </style>
47 +</body>
48 +</html>
49 +
webui/components/sidebar/chats/chats-list.html new
+94
@@ -0,0 +1,94 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/sidebar/chats/chats-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <div x-data>
9 + <template x-if="$store.chats">
10 + <div class="chats-list-container" x-data>
11 + <h3 class="section-header">Chats</h3>
12 + <ul class="config-list chats-config-list" x-show="$store.chats.contexts.length > 0">
13 + <template x-for="context in $store.chats.contexts" :key="context.id">
14 + <li>
15 + <div :class="{'chat-list-button': true, 'font-bold': context.id === $store.chats.selected}"
16 + @click="$store.chats.selectChat(context.id)">
17 + <span class="chat-name" :title="context.name ? context.name : 'Chat #' + context.no"
18 + x-text="context.name ? context.name : 'Chat #' + context.no"></span>
19 + </div>
20 + <button class="edit-button" @click.stop="$store.chats.killChat(context.id)">X</button>
21 + </li>
22 + </template>
23 + </ul>
24 + <div class="empty-list-message" x-show="$store.chats.contexts.length === 0">
25 + <p><i>No chats to list.</i></p>
26 + </div>
27 + </div>
28 + </template>
29 + </div>
30 +
31 +</body>
32 +</html>
33 +
34 +<style>
35 + /* Chats list container and items */
36 + .chats-list-container {
37 + display: flex;
38 + flex-direction: column;
39 + flex: 1;
40 + min-height: 0;
41 + overflow: hidden;
42 + }
43 +
44 + /* Sticky header for chats list */
45 + .chats-list-container .section-header {
46 + flex-shrink: 0;
47 + z-index: 10;
48 + }
49 +
50 + /* Scrollable config list */
51 + .chats-config-list {
52 + flex: 1;
53 + min-height: 0;
54 + overflow-y: auto;
55 + scroll-behavior: smooth;
56 + max-height: 23vh;
57 + /* Hide scrollbar but keep functionality */
58 + scrollbar-width: none; /* Firefox */
59 + -ms-overflow-style: none; /* IE/Edge */
60 + }
61 + /* Hide scrollbar for Chrome/Safari/Webkit */
62 + .chats-config-list::-webkit-scrollbar {
63 + display: none;
64 + }
65 +
66 + .chat-list-button { display: block; width: 100%; padding: 8px 5px; cursor: pointer; overflow: hidden; position: relative; border-radius: 4px; transition: background-color 0.2s ease-in-out; }
67 + .chat-list-button.has-task-container { padding-top: 6px; padding-bottom: 6px; }
68 + .chat-list-button:hover { background-color: rgba(255, 255, 255, 0.03); }
69 + .light-mode .chat-list-button:hover { background-color: rgba(0, 0, 0, 0.02); }
70 +
71 + .chat-name { display: inline-block; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; padding: 3px 8px; border-radius: 4px; transition: background-color 0.2s; margin-right: 60px; font-size: var(--font-size-small); }
72 + .chat-name:hover { background-color: rgba(255, 255, 255, 0.1); text-decoration: none; }
73 + .light-mode .chat-name:hover { background-color: rgba(0, 0, 0, 0.05); }
74 +
75 + .chat-container { display: flex; align-items: center; position: relative; width: 100%; min-height: 30px; gap: 0; }
76 +
77 + .edit-button { background-color: transparent; border: 1px solid var(--color-border); border-radius: 0.1875rem; color: var(--color-primary); cursor: pointer; padding: 0.125rem 0.5rem; -webkit-transition: all var(--transition-speed) ease-in-out; transition: all var(--transition-speed) ease-in-out; width: 2rem; height: 2rem; }
78 + .edit-button:hover { border-color: var(--color-primary); background-color: #32455690; }
79 + .edit-button:active { background-color: #131a2090; color: rgba(253, 253, 253, 0.35); }
80 + .light-mode .edit-button { border-color: var(--color-primary-light); color: var(--color-primary-light); }
81 + .light-mode .edit-button:hover { background-color: #e4e7f0; }
82 + .light-mode .edit-button:active { background-color: #979fb9; color: rgba(0,0,0,0.35); }
83 +
84 + .empty-list-message { display: flex; justify-content: center; align-items: center; height: 100px; color: var(--color-secondary); text-align: center; opacity: 0.7; font-style: italic; }
85 + .light-mode .empty-list-message { color: var(--color-secondary-light); }
86 +
87 + /* Selected chat accent */
88 + .chat-list-button.font-bold { position: relative; background-color: var(--color-border) 0.05; }
89 + .chat-list-button.font-bold::before { content: ""; position: absolute; left: 0; top: 0; height: 100%; width: 3px; background-color: var(--color-border); border-top-left-radius: 3px; border-bottom-left-radius: 3px; }
90 + .light-mode .chat-list-button.font-bold { background-color: var(--color-border) 0.05; }
91 + .light-mode .chat-list-button.font-bold::before { background-color: var(--color-border); }
92 +</style>
93 +
94 +
webui/components/sidebar/chats/chats-store.js new
+305
@@ -0,0 +1,305 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +import { sendJsonData } from "/index.js";
3 +import { store as notificationStore } from "/components/notifications/notification-store.js";
4 +
5 +const model = {
6 + contexts: [],
7 + selected: "",
8 +
9 + init() {
10 + // Initialize from localStorage
11 + const lastSelectedChat = localStorage.getItem("lastSelectedChat");
12 + if (lastSelectedChat) {
13 + this.selected = lastSelectedChat;
14 + }
15 + },
16 +
17 + // Update contexts from polling
18 + applyContexts(contextsList) {
19 + // Sort by created_at time (newer first)
20 + this.contexts = contextsList.sort(
21 + (a, b) => (b.created_at || 0) - (a.created_at || 0)
22 + );
23 + },
24 +
25 + // Select a chat
26 + async selectChat(id) {
27 + const currentContext = globalThis.getContext?.();
28 + if (id === currentContext) return; // already selected
29 +
30 + // Proceed with context selection
31 + if (globalThis.setContext) {
32 + globalThis.setContext(id);
33 + }
34 +
35 + // Update selection state (will also persist to localStorage)
36 + this.setSelected(id);
37 +
38 + // Trigger immediate poll
39 + if (globalThis.poll) {
40 + globalThis.poll();
41 + }
42 +
43 + // Update scroll
44 + if (globalThis.updateAfterScroll) {
45 + globalThis.updateAfterScroll();
46 + }
47 + },
48 +
49 + // Delete a chat
50 + async killChat(id) {
51 + if (!id) {
52 + console.error("No chat ID provided for deletion");
53 + return;
54 + }
55 +
56 + console.log("Deleting chat with ID:", id);
57 +
58 + try {
59 + // Switch to another context if deleting current
60 + if (this.selected === id) {
61 + await this.switchFromContext(id);
62 + }
63 +
64 + // Delete the chat on the server
65 + await sendJsonData("/chat_remove", { context: id });
66 +
67 + // Update the UI - remove from contexts
68 + const updatedContexts = this.contexts.filter((ctx) => ctx.id !== id);
69 + console.log(
70 + "Updated contexts after deletion:",
71 + JSON.stringify(updatedContexts.map((c) => ({ id: c.id, name: c.name })))
72 + );
73 +
74 + // Force UI update by creating a new array
75 + this.contexts = [...updatedContexts];
76 +
77 + // Show success notification
78 + if (globalThis.justToast) {
79 + globalThis.justToast("Chat deleted successfully", "success", 1000, "chat-removal");
80 + }
81 + } catch (e) {
82 + console.error("Error deleting chat:", e);
83 + if (globalThis.toastFetchError) {
84 + globalThis.toastFetchError("Error deleting chat", e);
85 + }
86 + }
87 + },
88 +
89 + // Switch from a context that's being deleted
90 + async switchFromContext(id) {
91 + // Find an alternate chat to switch to
92 + let alternateChat = null;
93 + for (let i = 0; i < this.contexts.length; i++) {
94 + if (this.contexts[i].id !== id) {
95 + alternateChat = this.contexts[i];
96 + break;
97 + }
98 + }
99 +
100 + if (alternateChat) {
101 + await this.selectChat(alternateChat.id);
102 + } else {
103 + // If no other chats, create a new empty context
104 + if (globalThis.newChat) {
105 + await globalThis.newChat();
106 + }
107 + }
108 + },
109 +
110 + // Reset current chat
111 + async resetChat(ctxid = null) {
112 + try {
113 + const context = ctxid || this.selected || globalThis.getContext?.();
114 + await sendJsonData("/chat_reset", {
115 + context
116 + });
117 +
118 + // Increment reset counter
119 + if (typeof globalThis.resetCounter === 'number') {
120 + globalThis.resetCounter = globalThis.resetCounter + 1;
121 + }
122 +
123 + if (globalThis.updateAfterScroll) {
124 + globalThis.updateAfterScroll();
125 + }
126 + } catch (e) {
127 + if (globalThis.toastFetchError) {
128 + globalThis.toastFetchError("Error resetting chat", e);
129 + }
130 + }
131 + },
132 +
133 + // Create new chat
134 + async newChat() {
135 + try {
136 + if (globalThis.newContext) {
137 + globalThis.newContext();
138 + }
139 + if (globalThis.updateAfterScroll) {
140 + globalThis.updateAfterScroll();
141 + }
142 + } catch (e) {
143 + if (globalThis.toastFetchError) {
144 + globalThis.toastFetchError("Error creating new chat", e);
145 + }
146 + }
147 + },
148 +
149 + // Load chats from files
150 + async loadChats() {
151 + try {
152 + const fileContents = await this.readJsonFiles();
153 + const response = await sendJsonData("/chat_load", { chats: fileContents });
154 +
155 + if (!response) {
156 + if (globalThis.toast) globalThis.toast("No response returned.", "error");
157 + } else {
158 + // Set context to first loaded chat
159 + if (globalThis.setContext && response.ctxids?.[0]) {
160 + globalThis.setContext(response.ctxids[0]);
161 + }
162 + if (globalThis.toast) globalThis.toast("Chats loaded.", "success");
163 + }
164 + } catch (e) {
165 + if (globalThis.toastFetchError) {
166 + globalThis.toastFetchError("Error loading chats", e);
167 + }
168 + }
169 + },
170 +
171 + // Save current chat
172 + async saveChat() {
173 + try {
174 + const context = this.selected || globalThis.getContext?.();
175 + const response = await sendJsonData("/chat_export", { ctxid: context });
176 +
177 + if (!response) {
178 + if (globalThis.toast) globalThis.toast("No response returned.", "error");
179 + } else {
180 + this.downloadFile(response.ctxid + ".json", response.content);
181 + if (globalThis.toast) globalThis.toast("Chat file downloaded.", "success");
182 + }
183 + } catch (e) {
184 + if (globalThis.toastFetchError) {
185 + globalThis.toastFetchError("Error saving chat", e);
186 + }
187 + }
188 + },
189 +
190 + // Helper: read JSON files
191 + readJsonFiles() {
192 + return new Promise((resolve, reject) => {
193 + const input = document.createElement("input");
194 + input.type = "file";
195 + input.accept = ".json";
196 + input.multiple = true;
197 +
198 + input.click();
199 +
200 + input.onchange = async () => {
201 + const files = input.files;
202 + if (!files.length) {
203 + resolve([]);
204 + return;
205 + }
206 +
207 + const filePromises = Array.from(files).map((file) => {
208 + return new Promise((fileResolve, fileReject) => {
209 + const reader = new FileReader();
210 + reader.onload = () => fileResolve(reader.result);
211 + reader.onerror = fileReject;
212 + reader.readAsText(file);
213 + });
214 + });
215 +
216 + try {
217 + const fileContents = await Promise.all(filePromises);
218 + resolve(fileContents);
219 + } catch (error) {
220 + reject(error);
221 + }
222 + };
223 + });
224 + },
225 +
226 + // Helper: download file
227 + downloadFile(filename, content) {
228 + const blob = new Blob([content], { type: "application/json" });
229 + const link = document.createElement("a");
230 + const url = URL.createObjectURL(blob);
231 + link.href = url;
232 + link.download = filename;
233 + link.click();
234 +
235 + setTimeout(() => {
236 + URL.revokeObjectURL(url);
237 + }, 0);
238 + },
239 +
240 + // Check if context exists
241 + contains(contextId) {
242 + return this.contexts.some((ctx) => ctx.id === contextId);
243 + },
244 +
245 + // Get first context ID
246 + firstId() {
247 + return this.contexts.length > 0 ? this.contexts[0].id : null;
248 + },
249 +
250 + // Set selected context
251 + setSelected(contextId) {
252 + this.selected = contextId;
253 + localStorage.setItem("lastSelectedChat", contextId);
254 + },
255 +
256 + // Restart the backend
257 + async restart() {
258 + try {
259 + // Check connection status
260 + const connectionStatus = globalThis.getConnectionStatus?.();
261 + if (connectionStatus === false) {
262 + await notificationStore.frontendError(
263 + "Backend disconnected, cannot restart.",
264 + "Restart Error"
265 + );
266 + return;
267 + }
268 +
269 + // Try to initiate restart
270 + const resp = await sendJsonData("/restart", {});
271 + } catch (e) {
272 + // Show restarting message
273 + await notificationStore.frontendInfo("Restarting...", "System Restart", 9999, "restart");
274 +
275 + let retries = 0;
276 + const maxRetries = 240; // 60 seconds with 250ms interval
277 +
278 + while (retries < maxRetries) {
279 + try {
280 + const resp = await sendJsonData("/health", {});
281 + // Server is back up
282 + await new Promise((resolve) => setTimeout(resolve, 250));
283 + await notificationStore.frontendSuccess("Restarted", "System Restart", 5, "restart");
284 + return;
285 + } catch (e) {
286 + // Server still down, keep waiting
287 + retries++;
288 + await new Promise((resolve) => setTimeout(resolve, 250));
289 + }
290 + }
291 +
292 + // Restart failed or timed out
293 + await notificationStore.frontendError(
294 + "Restart timed out or failed",
295 + "Restart Error",
296 + 8,
297 + "restart"
298 + );
299 + }
300 + }
301 +};
302 +
303 +const store = createStore("chats", model);
304 +
305 +export { store };
webui/components/sidebar/left-sidebar.html new
+177
@@ -0,0 +1,177 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store as sidebarStore } from "/components/sidebar/sidebar-store.js";
5 + </script>
6 + <!-- Wrapper composes existing sidebar components -->
7 +</head>
8 +<body>
9 + <div id="left-panel" class="panel" x-data :class="{'hidden': !$store.sidebar.isOpen}">
10 + <!--Sidebar upper elements-->
11 + <div class="left-panel-top">
12 + <!-- Top Section: Header Icons + Quick Actions -->
13 + <x-component path="sidebar/top-section/sidebar-top.html"></x-component>
14 +
15 + <!-- Chats and Tasks Wrapper -->
16 + <div class="lists-wrapper">
17 + <!-- Chats List -->
18 + <div class="config-section" id="chats-section">
19 + <x-component path="sidebar/chats/chats-list.html"></x-component>
20 + </div>
21 +
22 + <!-- Tasks List -->
23 + <div class="config-section" id="tasks-section">
24 + <x-component path="sidebar/tasks/tasks-list.html"></x-component>
25 + </div>
26 + </div>
27 + </div>
28 + <!--Sidebar lower elements-->
29 + <div class="left-panel-bottom">
30 + <x-component path="sidebar/bottom/sidebar-bottom.html"></x-component>
31 + </div>
32 + </div>
33 +
34 +
35 + <style>
36 + /* Left Panel styles from index.css */
37 + #left-panel {
38 + background-color: var(--color-panel);
39 + border-right: 1px solid var(--color-border);
40 + box-sizing: border-box;
41 + display: flex;
42 + flex-direction: column;
43 + justify-content: space-between;
44 + transition: all var(--transition-speed) ease-in-out;
45 + width: 250px;
46 + min-width: 250px;
47 + color: var(--color-text);
48 + box-shadow: 1px 0 5px rgba(0, 0, 0, 0.3);
49 + user-select: none;
50 + }
51 +
52 + /* Ensure component host behaves like the original direct children for flex layout */
53 + #left-panel > .left-panel-top,
54 + #left-panel > .left-panel-bottom {
55 + display: flex;
56 + flex-direction: column;
57 + }
58 +
59 + #left-panel > .left-panel-top {
60 + flex: 1;
61 + min-height: 0; /* allow inner flex children to shrink properly */
62 + }
63 +
64 + #left-panel.hidden {
65 + margin-left: -250px;
66 + }
67 +
68 + .left-panel-top {
69 + flex: 1;
70 + display: -webkit-flex;
71 + display: flex;
72 + flex-direction: column;
73 + min-height: 0;
74 + overflow: hidden;
75 + margin-top: 3.5rem;
76 + padding: var(--spacing-md) var(--spacing-md) 0 var(--spacing-md);
77 + }
78 +
79 + /* Wrapper for Chats and Tasks lists */
80 + .lists-wrapper {
81 + display: flex;
82 + flex-direction: column;
83 + flex: 1;
84 + min-height: 0;
85 + overflow: hidden;
86 + }
87 + .left-panel-top::-webkit-scrollbar {
88 + width: 0px;
89 + }
90 + .left-panel-top {
91 + scrollbar-width: none;
92 + -ms-overflow-style: none;
93 + }
94 + .left-panel-bottom {
95 + position: relative;
96 + flex-shrink: 0;
97 + }
98 + /* Ensure non-chat sections don't shrink */
99 + .config-section:not(#chats-section) {
100 + flex-shrink: 0;
101 + }
102 + /* Chats section container inside sidebar */
103 + #chats-section {
104 + display: -webkit-flex;
105 + display: flex;
106 + flex-direction: column;
107 + min-height: 0;
108 + flex: 0 auto;
109 + margin-top: var(--spacing-md);
110 + overflow: hidden;
111 + }
112 + /* Tasks section container inside sidebar */
113 + #tasks-section {
114 + display: -webkit-flex;
115 + display: flex;
116 + flex-direction: column;
117 + min-height: 0;
118 + flex: 0 auto;
119 + margin-top: 0;
120 + padding-top: var(--spacing-md);
121 + border-top: 1px solid var(--color-border);
122 + }
123 +
124 + /* Common section header styling (Chats, Tasks) - matching Preferences style */
125 + .section-header {
126 + display: flex;
127 + align-items: center;
128 + justify-content: space-between;
129 + gap: 6px;
130 + margin: 0 0 0.5rem 0;
131 + padding: 0;
132 + user-select: none;
133 + font-size: var(--font-size-normal);
134 + }
135 +
136 + /* Collapsible section headers (Tasks) */
137 + .section-header-collapsible {
138 + cursor: pointer;
139 + }
140 +
141 + /* Arrow icon for collapsible sections */
142 + .section-header .arrow-icon {
143 + margin-left: 0.5rem;
144 + transition: transform 0.15s ease;
145 + transform: rotate(90deg);
146 + }
147 +
148 + .section-header .arrow-icon.rotated {
149 + transform: rotate(0deg);
150 + }
151 +
152 + @media (max-width: 768px) {
153 + #left-panel {
154 + position: fixed;
155 + left: 0;
156 + top: 0;
157 + bottom: 0;
158 + width: 250px !important; /* Force width */
159 + min-width: 250px;
160 + z-index: 1003;
161 + transition: all var(--transition-speed) ease-in-out;
162 + }
163 +
164 + #left-panel.hidden {
165 + margin-left: -250px;
166 + }
167 + }
168 +
169 + @media (max-height: 600px) {
170 + #chats-section { min-height: 100%; }
171 + .left-panel-top { overflow-y: auto; -webkit-scroll-behavior: smooth; scroll-behavior: smooth; }
172 + }
173 + </style>
174 +</body>
175 +</html>
176 +
177 +
webui/components/sidebar/sidebar-store.js new
+51
@@ -0,0 +1,51 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +
3 +// This store manages the visibility and state of the main sidebar panel.
4 +const model = {
5 + isOpen: true,
6 + _initialized: false,
7 +
8 + // Initialize the store by setting up a resize listener
9 + // Guard ensures this runs only once, even if called from multiple components
10 + init() {
11 + if (this._initialized) return;
12 + this._initialized = true;
13 +
14 + this.handleResize();
15 + this.resizeHandler = () => this.handleResize();
16 + window.addEventListener("resize", this.resizeHandler);
17 + },
18 +
19 + // Cleanup method for lifecycle management
20 + destroy() {
21 + if (this.resizeHandler) {
22 + window.removeEventListener("resize", this.resizeHandler);
23 + this.resizeHandler = null;
24 + }
25 + this._initialized = false;
26 + },
27 +
28 + // Toggle the sidebar's visibility
29 + toggle() {
30 + this.isOpen = !this.isOpen;
31 + },
32 +
33 + // Close the sidebar, e.g., on overlay click on mobile
34 + close() {
35 + if (this.isMobile()) {
36 + this.isOpen = false;
37 + }
38 + },
39 +
40 + // Handle browser resize to show/hide sidebar based on viewport width
41 + handleResize() {
42 + this.isOpen = !this.isMobile();
43 + },
44 +
45 + // Check if the current viewport is mobile
46 + isMobile() {
47 + return window.innerWidth <= 768;
48 + },
49 +};
50 +
51 +export const store = createStore("sidebar", model);
webui/components/sidebar/tasks/tasks-list.html new
+121
@@ -0,0 +1,121 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/sidebar/tasks/tasks-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <div x-data="{ tasksOpen: true }">
9 + <template x-if="$store.tasks">
10 + <div x-data>
11 + <h3 class="section-header section-header-collapsible" @click="tasksOpen = !tasksOpen">
12 + Tasks
13 + <svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" :class="{'rotated': !tasksOpen}">
14 + <path d="M8 4l8 8-8 8" />
15 + </svg>
16 + </h3>
17 + <div x-show="tasksOpen" x-collapse x-transition>
18 + <div class="tasks-list-container" x-data>
19 + <ul class="config-list tasks-config-list" x-show="$store.tasks.tasks.length > 0">
20 + <template x-for="task in $store.tasks.tasks" :key="task.id">
21 + <li>
22 + <div :class="{'chat-list-button': true, 'font-bold': task.id === $store.tasks.selected, 'has-task-container': true}"
23 + @click="$store.tasks.selectTask(task.id)">
24 + <div class="task-container task-container-vertical">
25 + <span class="task-name"
26 + :title="(task.task_name || `Task #${task.no}`) + ' (' + (task.name || `Chat #${task.no}`) + ')'"
27 + x-text="(task.task_name || `Task #${task.no}`) + ' (' + (task.name || `Chat #${task.no}`) + ')'"
28 + :data-task-id="task.id"></span>
29 + <div class="task-info-line">
30 + <span class="scheduler-status-badge scheduler-status-badge-small"
31 + :class="task.state ? `scheduler-status-${task.state}` : 'scheduler-status-idle'"
32 + x-text="task.state || 'idle'"></span>
33 + <button class="edit-button"
34 + @click.stop="$store.tasks.openDetail(task.id)"
35 + title="View task details" style="margin-left: auto; margin-right: 5px;">
36 + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 512 512" fill="var(--color-primary)" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
37 + <path d="M256 0c70.69 0 134.69 28.66 181.02 74.98C483.34 121.3 512 185.31 512 256c0 70.69-28.66 134.7-74.98 181.02C390.69 483.34 326.69 512 256 512c-70.69 0-134.69-28.66-181.02-74.98C28.66 390.69 0 326.69 0 256c0-70.69 28.66-134.69 74.98-181.02C121.31 28.66 185.31 0 256 0zm-9.96 161.03c0-4.28.76-8.26 2.27-11.91 1.5-3.63 3.77-6.94 6.79-9.91 3-2.95 6.29-5.2 9.84-6.7 3.57-1.5 7.41-2.28 11.52-2.28 4.12 0 7.96.78 11.49 2.27 3.54 1.51 6.78 3.76 9.75 6.73 2.95 2.97 5.16 6.26 6.64 9.91 1.49 3.63 2.22 7.61 2.22 11.89 0 4.17-.73 8.08-2.21 11.69-1.48 3.6-3.68 6.94-6.65 9.97-2.94 3.03-6.18 5.32-9.72 6.84-3.54 1.51-7.38 2.29-11.52 2.29-4.22 0-8.14-.76-11.75-2.26-3.58-1.51-6.86-3.79-9.83-6.79-2.94-3.02-5.16-6.34-6.63-9.97-1.48-3.62-2.21-7.54-2.21-11.77zm13.4 178.16c-1.11 3.97-3.35 11.76 3.3 11.76 1.44 0 3.27-.81 5.46-2.4 2.37-1.71 5.09-4.31 8.13-7.75 3.09-3.5 6.32-7.65 9.67-12.42 3.33-4.76 6.84-10.22 10.49-16.31.37-.65 1.23-.87 1.89-.48l12.36 9.18c.6.43.73 1.25.35 1.86-5.69 9.88-11.44 18.51-17.26 25.88-5.85 7.41-11.79 13.57-17.8 18.43l-.1.06c-6.02 4.88-12.19 8.55-18.51 11.01-17.58 6.81-45.36 5.7-53.32-14.83-5.02-12.96-.9-27.69 3.06-40.37l19.96-60.44c1.28-4.58 2.89-9.62 3.47-14.33.97-7.87-2.49-12.96-11.06-12.96h-17.45c-.76 0-1.38-.62-1.38-1.38l.08-.48 4.58-16.68c.16-.62.73-1.04 1.35-1.02l89.12-2.79c.76-.03 1.41.57 1.44 1.33l-.07.43-37.76 124.7zm158.3-244.93c-41.39-41.39-98.58-67-161.74-67-63.16 0-120.35 25.61-161.74 67-41.39 41.39-67 98.58-67 161.74 0 63.16 25.61 120.35 67 161.74 41.39 41.39 98.58 67 161.74 67 63.16 0 120.35-25.61 161.74-67 41.39-41.39 67-98.58 67-161.74 0-63.16-25.61-120.35-67-161.74z"/>
38 + </svg>
39 + </button>
40 + <button class="edit-button" title="Clear task chat"
41 + @click.stop="$store.tasks.reset(task.id)" style="margin-right: 5px;">
42 + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 122.88 121.1" fill="var(--color-primary)" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
43 + <path d="M62.89,56.03c1.11-0.35,2.34-0.25,3.72,0.37l10.4,7.87c2.26,1.71,4.24,3.78,2.73,6.9 c-0.51,1.06-1.4,2.1-2.38,3.49l-0.53,0.75c-1.97,2.8-2.61,2-5.71,1.83c0.56,13.37,1.75,27.82-2.64,40.88 c-0.87,2.7-3.32,3.44-6.95,2.71l-6.1-2.03c4.11-6.14,6.16-13.85,6.44-22.89c-3.46,8.58-6.8,16.96-10.68,20.86l-6.28-2.08 c0.61-3.05,1.05-5.43,0.35-6.9l-4.07,4.24l-9.33-5.77c6.36-3.36,11.62-7.87,15.6-13.73c-6.69,5.01-12.76,8.1-18.14,8.99 c-2.75,0.83-4.49,0.35-5.16-1.53c-0.48-1.34-0.05-1.77,0.81-2.86c1.11-1.41,2.61-2.67,4.35-3.79c-3.13,1.1-4.64,0.95-6.37,1.51 c-4.9,1.59-9.94-1.86-8.26-6.9c1.07-3.23,3.54-3.09,6.67-4.07l5.42-1.69c-5.19,0.28-10.32,0.45-15.02-0.25 c-5.4-0.8-5.31-0.99-8.24-5.38c-3.94-5.91-6.25-11.45,2.52-9.16c16.73,3.18,33.56,5.34,51.25-0.98c-0.76-1.32-0.9-2.57-0.5-3.73 C57.37,60.94,61.13,56.58,62.89,56.03L62.89,56.03z M113.8,2.42L74.45,51.53c-4.71,6.68,3.2,11.91,8.39,5.64l39.2-49.27 C125.12,1.86,119.13-3.16,113.8,2.42L113.8,2.42z"/>
44 + </svg>
45 + </button>
46 + <button class="edit-button" title="Delete task" @click.stop="$store.tasks.deleteTask(task.id)">X</button>
47 + </div>
48 + </div>
49 + </div>
50 + </li>
51 + </template>
52 + </ul>
53 + <div class="empty-list-message" x-show="$store.tasks.tasks.length === 0">
54 + <p><i>No tasks to list.</i></p>
55 + </div>
56 + </div>
57 + </div>
58 + </div>
59 + </template>
60 + </div>
61 +
62 +</body>
63 +</html>
64 +
65 +<style>
66 + /* Tasks list styling (migrated) */
67 + .tasks-list-container {
68 + display: flex;
69 + flex-direction: column;
70 + max-height: 30vh;
71 + overflow: hidden;
72 + position: relative;
73 + }
74 +
75 + /* Scrollable tasks config list */
76 + .tasks-config-list {
77 + flex: 1;
78 + min-height: 0;
79 + overflow-y: auto;
80 + scroll-behavior: smooth;
81 + max-height: 23vh;
82 + /* Hide scrollbar but keep functionality */
83 + scrollbar-width: none; /* Firefox */
84 + -ms-overflow-style: none; /* IE/Edge */
85 + }
86 + /* Hide scrollbar for Chrome/Safari/Webkit */
87 + .tasks-config-list::-webkit-scrollbar {
88 + display: none;
89 + }
90 +
91 +
92 + .tasks-list-container .section-header {
93 + flex-shrink: 0;
94 + z-index: 10;
95 + }
96 +
97 + .task-name { display: block; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 3px 0; margin-left: 10px; cursor: pointer; border-radius: 4px; transition: background-color 0.2s; font-size: var(--font-size-small); margin-bottom: 2px; }
98 + .task-name:hover { background-color: rgba(255,255,255,0.1); text-decoration: none; }
99 + .light-mode .task-name:hover { background-color: rgba(0,0,0,0.05); }
100 +
101 + .task-info-line { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-top: 2px; margin-left: 5px; }
102 +
103 + .task-container-vertical { display: flex; flex-direction: column; width: 100%; gap: 6px; }
104 +
105 + /* Scheduler badges (subset used in sidebar) */
106 + .scheduler-status-badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; text-transform: capitalize; white-space: nowrap; }
107 + .scheduler-status-badge-small { font-size: 10px; padding: 2px 6px; margin-right: 5px; min-width: 40px; text-align: center; }
108 + .scheduler-status-idle { background-color: rgba(0, 100, 0, 0.2); color: #2a9d8f; border: 1px solid rgba(42, 157, 143, 0.3); }
109 + .scheduler-status-running { background-color: rgba(0, 60, 120, 0.2); color: #4361ee; border: 1px solid rgba(67, 97, 238, 0.3); }
110 + .scheduler-status-disabled { background-color: rgba(70, 70, 70, 0.2); color: #6c757d; border: 1px solid rgba(108, 117, 125, 0.3); }
111 + .scheduler-status-error { background-color: rgba(120, 0, 0, 0.2); color: #e63946; border: 1px solid rgba(230, 57, 70, 0.3); }
112 + .light-mode .scheduler-status-idle { background-color: rgba(42, 157, 143, 0.1); color: #1a6f65; }
113 + .light-mode .scheduler-status-running { background-color: rgba(67, 97, 238, 0.1); color: #2540b3; }
114 + .light-mode .scheduler-status-disabled { background-color: rgba(108, 117, 125, 0.1); color: #495057; }
115 + .light-mode .scheduler-status-error { background-color: rgba(230, 57, 70, 0.1); color: #c5283d; }
116 +
117 + .empty-list-message { display: flex; justify-content: center; align-items: center; height: 100px; color: var(--color-secondary); text-align: center; opacity: 0.7; font-style: italic; }
118 + .light-mode .empty-list-message { color: var(--color-secondary-light); }
119 +</style>
120 +
121 +
webui/components/sidebar/tasks/tasks-store.js new
+74
@@ -0,0 +1,74 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +
3 +// Tasks sidebar store: tasks list and selected task id
4 +const model = {
5 + tasks: [],
6 + selected: "",
7 +
8 + init() {
9 + // No-op: data is driven by poll() in index.js; this store provides a stable target
10 + },
11 +
12 + // Apply tasks coming from poll() and keep them sorted (newest first)
13 + applyTasks(tasksList) {
14 + try {
15 + const tasks = Array.isArray(tasksList) ? tasksList : [];
16 + const sorted = [...tasks].sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0));
17 + this.tasks = sorted;
18 +
19 + // After updating tasks, ensure selection is still valid
20 + if (this.selected && !this.contains(this.selected)) {
21 + this.setSelected("");
22 + }
23 + } catch (e) {
24 + console.error("tasks-store.applyTasks failed", e);
25 + this.tasks = [];
26 + }
27 + },
28 +
29 + // Update selected task and persist for tab restore
30 + setSelected(taskId) {
31 + this.selected = taskId || "";
32 + try { localStorage.setItem("lastSelectedTask", this.selected); } catch {}
33 + },
34 +
35 + // Returns true if a task with the given id exists in the current list
36 + contains(taskId) {
37 + return Array.isArray(this.tasks) && this.tasks.some((t) => t?.id === taskId);
38 + },
39 +
40 + // Convenience: id of the first task in the current list (or empty string)
41 + firstId() {
42 + return (Array.isArray(this.tasks) && this.tasks[0]?.id) || "";
43 + },
44 +
45 + // Action methods for task management
46 + selectTask(taskId) {
47 + this.setSelected(taskId);
48 + if (globalThis.selectChat) {
49 + globalThis.selectChat(taskId);
50 + }
51 + },
52 +
53 + openDetail(taskId) {
54 + if (globalThis.openTaskDetail) {
55 + globalThis.openTaskDetail(taskId);
56 + }
57 + },
58 +
59 + reset(taskId) {
60 + if (globalThis.resetChat) {
61 + globalThis.resetChat(taskId);
62 + }
63 + },
64 +
65 + deleteTask(taskId) {
66 + if (globalThis.deleteTaskGlobal) {
67 + globalThis.deleteTaskGlobal(taskId);
68 + }
69 + },
70 +};
71 +
72 +export const store = createStore("tasks", model);
73 +
74 +
webui/components/sidebar/top-section/header-icons.html new
+78
@@ -0,0 +1,78 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store as sidebarStore } from "/components/sidebar/sidebar-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <div class="icons-section" id="hide-button" x-data>
9 + <!-- Sidebar Toggle Button -->
10 + <button id="toggle-sidebar" class="toggle-sidebar-button" @click="$store.sidebar.toggle()" aria-label="Toggle Sidebar" aria-expanded="false">
11 + <span aria-hidden="true">
12 + <!-- Hamburger Icon -->
13 + <svg id="sidebar-hamburger-svg" xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="CurrentColor">
14 + <path d="M3 13h18v-2H3v2zm0 4h18v-2H3v2zm0-8h18V7H3v2z"></path>
15 + </svg>
16 + </span>
17 + </button>
18 +
19 + <div id="logo-container">
20 + <a href="https://github.com/agent0ai/agent-zero" target="_blank" rel="noopener noreferrer">
21 + <img src="./public/splash.jpg" alt="a0" width="22" height="22">
22 + </a>
23 + </div>
24 + </div>
25 +
26 +
27 + <style>
28 + /* Sidebar toggle button + logo container (migrated) */
29 + .toggle-sidebar-button {
30 + height: 2.6rem;
31 + width: 2.6rem;
32 + background-color: var(--color-background);
33 + border: 0.1rem solid var(--color-border);
34 + border-radius: var(--spacing-xs);
35 + color: var(--color-text);
36 + opacity: 0.8;
37 + cursor: pointer;
38 + left: var(--spacing-md);
39 + padding: 0.47rem 0.56rem;
40 + position: absolute;
41 + top: var(--spacing-md);
42 + z-index: 999;
43 + -webkit-transition: all var(--transition-speed) ease-in-out;
44 + transition: all var(--transition-speed) ease-in-out;
45 + }
46 + .toggle-sidebar-button:hover { background-color: var(--color-secondary); opacity: 1; }
47 + .toggle-sidebar-button:active { opacity: 0.5; }
48 + #sidebar-hamburger-svg { -webkit-transition: all var(--transition-speed) ease; transition: all var(--transition-speed) ease; }
49 + .toggle-sidebar-button:active #sidebar-hamburger-svg { -webkit-transform: scaleY(0.8); transform: scaleY(0.8); }
50 +
51 + #logo-container {
52 + display: -webkit-flex;
53 + display: flex;
54 + align-items: center;
55 + justify-content: space-between;
56 + position: fixed;
57 + left: 4.6rem;
58 + top: var(--spacing-md);
59 + z-index: 1004;
60 + -webkit-transition: margin-left var(--transition-speed) ease-in-out;
61 + transition: margin-left var(--transition-speed) ease-in-out;
62 + }
63 + #logo-container a { color: inherit; text-decoration: none; }
64 + #logo-container img { border-radius: var(--spacing-xs); width: auto; height: 2.6rem; -webkit-transition: filter 0.3s ease; transition: filter 0.3s ease; }
65 + #right-panel.expanded #logo-container { margin-left: 4.6rem; }
66 +
67 + .light-mode #logo-container img { -webkit-filter: invert(100%) grayscale(100%); filter: invert(100%) grayscale(100%); }
68 +
69 + @media (max-width: 768px) {
70 + .toggle-sidebar-button { position: fixed; left: var(--spacing-md); z-index: 1004; }
71 + #logo-container { margin-left: 4.6rem; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; z-index: 1004; }
72 + #right-panel.expanded #logo-container { margin-left: 4.6rem; }
73 + }
74 + </style>
75 +</body>
76 +</html>
77 +
78 +
webui/components/sidebar/top-section/quick-actions.html new
+67
@@ -0,0 +1,67 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/sidebar/chats/chats-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <div class="config-section" x-data>
9 + <button class="config-button" id="resetChat" @click="$store.chats.resetChat()">Reset Chat</button>
10 + <button class="config-button" id="newChat" @click="$store.chats.newChat()">New Chat</button>
11 + <button class="config-button" id="loadChats" @click="$store.chats.loadChats()">Load Chat</button>
12 + <button class="config-button" id="loadChat" @click="$store.chats.saveChat()">Save Chat</button>
13 + <button class="config-button" id="restart" @click="$store.chats.restart()">Restart</button>
14 + <button class="config-button" id="settings" @click="settingsModalProxy.openModal()"><svg
15 + xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-5.0 -17.0 110.0 135.0" fill="currentColor"
16 + width="24" height="24">
17 + <path
18 + d="m52.301 90.102h-4.1016c-3 0-5 0-6.8984-1.3984-1.8984-1.3984-2.5-3.3008-3.5-6.1016l-1.1016-3.6016c-0.19922-0.60156-0.69922-1.1992-1.3984-1.6016l-1.1016-0.60156c-0.60156-0.30078-1.5-0.39844-2.3008-0.19922l-4.3008 1.1992c-3.1016 0.89844-5.1016 1.5-7.3984 0.5-2.3008-0.89844-3.3984-2.8008-5-5.6016l-1.8008-3.1016c-1.5-2.5-2.5-4.3008-2.3008-6.6992 0.19922-2.3984 1.6016-3.8008 3.6016-6.1016l3.8008-4.1992c0.19922-0.19922 0.60156-1.3984 0.60156-2.6016 0-1.1016-0.5-2.3008-0.80078-2.6992l-3.6016-4c-2-2.1992-3.3008-3.6992-3.6016-6.1016-0.19922-2.3984 0.80078-4.1992 2.3008-6.6992l1.8008-3.1016c1.6016-2.8008 2.6992-4.6016 5-5.6016 2.3008-0.89844 4.3008-0.39844 7.3984 0.5l4.3984 1.1992c0.69922 0.10156 1.5 0 2.3008-0.30078l1.1016-0.60156c0.5-0.30078 1-0.89844 1.3008-1.6992l1.1992-3.5c0.89844-2.8008 1.6016-4.6992 3.5-6.1016 1.8984-1.3984 3.8984-1.3984 6.8984-1.3984h4.1016c3 0 5 0 6.8984 1.3984 1.8984 1.3984 2.5 3.1992 3.5 6.1016l1.1992 3.6016c0.19922 0.60156 0.69922 1.1992 1.3984 1.6016l1.1016 0.60156c0.60156 0.30078 1.5 0.39844 2.3008 0.19922l4.3008-1.1992c3.1016-0.89844 5.1016-1.5 7.3984-0.5 2.3008 0.89844 3.3984 2.8008 5 5.5l1.8008 3.1016c1.3984 2.5 2.5 4.3008 2.3008 6.6992-0.19922 2.3984-1.6016 3.8008-3.6016 6.1016l-3.9961 4.4062c-0.19922 0.19922-0.60156 1.3984-0.60156 2.6016 0 1.1016 0.5 2.3008 0.80078 2.6992l3.6016 4c2 2.1992 3.3008 3.6992 3.6016 6.1016 0.19922 2.3984-0.80078 4.1992-2.3008 6.6992l-1.8008 3.1016c-1.6016 2.8008-2.6992 4.6016-5 5.6016-2.3008 0.89844-4.3984 0.39844-7.3984-0.5l-4.3984-1.1992c-0.69922-0.10156-1.5 0-2.3008 0.30078l-1.1016 0.60156c-0.5 0.30078-1 0.89844-1.3008 1.6992l-1.1992 3.5c-0.89844 2.8008-1.6016 4.6992-3.5 6.1016-1.8008 1.293-3.8008 1.293-6.8008 1.293zm-6.6016-7.3008c0.5 0.10156 1.6016 0.10156 2.6016 0.10156h4.1016c1 0 2 0 2.6016-0.10156 0.19922-0.5 0.60156-1.5 0.89844-2.3984l1.1992-3.6016c0.89844-2.3008 2.3984-4.1992 4.3984-5.3984l1.3984-0.80078c2.3984-1.3008 5.1016-1.6016 7.6016-1l4.6016 1.3008c1 0.30078 2.1016 0.60156 2.6992 0.69922 0.30078-0.5 0.89844-1.5 1.3984-2.3984l1.8008-3.1016c0.5-0.80078 1-1.8008 1.1992-2.3008-0.30078-0.39844-1-1.1992-1.6992-2l-3.8008-4.1992c-1.6016-2-2.5-4.8008-2.5-7.3984 0-2.6016 0.89844-5.3984 2.3008-7.3008l3.8984-4.3984c0.69922-0.69922 1.3984-1.5 1.6992-2-0.19922-0.5-0.80078-1.3984-1.1992-2.3008l-1.8984-3.1016c-0.5-0.89844-1.1016-1.8984-1.3984-2.3984-0.60156 0.10156-1.6992 0.39844-2.6992 0.69922l-4.3984 1.3008c-2.6992 0.60156-5.3008 0.30078-7.6016-0.89844l-1.4023-0.80469c-2.1016-1.3984-3.6016-3.1992-4.3984-5.3984l-1.3008-3.8008c-0.30078-0.89844-0.60156-1.8984-0.89844-2.3984-0.5-0.10156-1.6016-0.10156-2.6016-0.10156h-4.1016c-1 0-2 0-2.6016 0.10156-0.19922 0.5-0.60156 1.5-0.89844 2.3984l-1.1992 3.6016c-0.89844 2.3008-2.3984 4.1992-4.3984 5.3984l-1.3984 0.80078c-2.3984 1.3008-5.1016 1.6016-7.6016 1l-4.6016-1.3008c-1-0.30078-2.1016-0.60156-2.6992-0.69922-0.30078 0.5-0.89844 1.5-1.3984 2.3984l-1.8008 3.1016c-0.5 0.80078-1 1.8008-1.1992 2.3008 0.30078 0.39844 1 1.1992 1.6992 2l3.8008 4.1992c1.6016 2 2.5 4.8008 2.5 7.3984 0 2.6016-0.89844 5.3984-2.3008 7.3008l-3.8984 4.3984c-0.69922 0.69922-1.3984 1.5-1.6992 2 0.19922 0.5 0.80078 1.3984 1.1992 2.3008l1.8008 3.1016c0.5 0.89844 1.1016 1.8984 1.3984 2.3984 0.60156-0.10156 1.6992-0.39844 2.6992-0.69922l4.3984-1.1992c2.6992-0.60156 5.3008-0.30078 7.6016 0.89844l1.3984 0.80078c2.1016 1.3008 3.6016 3.1992 4.3984 5.3984l1.3008 3.8008c0.5 0.80078 0.80078 1.8008 1 2.3008z"></path>
19 + <path
20 + d="m50.301 66.5c-9 0-16.398-7.3008-16.398-16.398 0-9.1016 7.3008-16.398 16.398-16.398 9.1016 0 16.398 7.3008 16.398 16.398 0 9.0977-7.3984 16.398-16.398 16.398zm0-25.5c-5 0-9.1016 4.1016-9.1016 9.1016s4.1016 9.1016 9.1016 9.1016 9.1016-4.1016 9.1016-9.1016c-0.003906-5-4.1016-9.1016-9.1016-9.1016z"></path>
21 + </svg>Settings</button>
22 + <button class="config-button" id="memory-dash"
23 + @click="openModal('settings/memory/memory-dashboard.html');">Memory</button>
24 + </div>
25 +</body>
26 + <style>
27 + .config-button {
28 + background-color: var(--color-background);
29 + border: 0.1rem solid var(--color-border);
30 + border-radius: var(--spacing-xs);
31 + cursor: pointer;
32 + display: inline;
33 + font-family: "Rubik", Arial, Helvetica, sans-serif;
34 + font-size: var(--font-size-small);
35 + opacity: 0.8;
36 + text-wrap: nowrap;
37 + width: calc(50% - var(--spacing-xs));
38 + float: left;
39 + margin: 0 var(--spacing-xs) var(--spacing-xs) 0;
40 + padding: var(--spacing-sm) 0.75rem;
41 + max-height: 2.3rem;
42 + -webkit-transition: all var(--transition-speed), transform 0.1s ease-in-out;
43 + transition: all var(--transition-speed), transform 0.1s ease-in-out;
44 + }
45 + .config-button:hover {
46 + background-color: var(--color-secondary);
47 + opacity: 1;
48 + }
49 + .config-button:active {
50 + opacity: 0.5;
51 + }
52 + #settings {
53 + display: flex;
54 + align-items: center;
55 + }
56 + .light-mode .config-button {
57 + background-color: var(--color-background);
58 + color: #333333;
59 + }
60 + .light-mode .config-button:hover {
61 + background-color: #d6dae8;
62 + }
63 + .light-mode .config-button:active {
64 + background-color: #bdc0cb;
65 + }
66 + </style>
67 +</html>
\ No newline at end of file
webui/components/sidebar/top-section/sidebar-top.html new
+31
@@ -0,0 +1,31 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/sidebar/sidebar-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <div x-data>
9 + <template x-if="$store.sidebar">
10 + <div class="sidebar-top-wrapper">
11 + <!-- Header Icons (Toggle + Logo) -->
12 + <x-component path="sidebar/top-section/header-icons.html"></x-component>
13 +
14 + <!-- Quick Actions Buttons -->
15 + <x-component path="sidebar/top-section/quick-actions.html"></x-component>
16 + </div>
17 + </template>
18 + </div>
19 +
20 +
21 + <style>
22 + /* Sidebar top wrapper - ensures proper spacing and layout */
23 + .sidebar-top-wrapper {
24 + display: flex;
25 + flex-direction: column;
26 + flex-shrink: 0;
27 + }
28 + </style>
29 +</body>
30 +</html>
31 +
webui/css/modals.css
+38 -1
@@ -276,7 +276,7 @@
276 }
277
278 .btn:disabled {
279 - background: #ccc;
279 + /* background: #ccc; */
280 cursor: not-allowed;
281 }
282
@@ -475,3 +475,40 @@
475 .toolbar-button:disabled:hover {
476 background-color: transparent;
477 }
478 +
479 +/* Range Input Styles */
480 +
481 +input[type="range"] {
482 + width: 100%;
483 + cursor: pointer;
484 + height: 4px;
485 + -webkit-appearance: none;
486 + appearance: none;
487 + background: var(--color-border);
488 + border-radius: 2px;
489 + outline: none;
490 +}
491 +
492 +input[type="range"]::-webkit-slider-thumb {
493 + -webkit-appearance: none;
494 + appearance: none;
495 + width: 16px;
496 + height: 16px;
497 + background: var(--color-text);
498 + border-radius: 50%;
499 + cursor: pointer;
500 +}
501 +
502 +input[type="range"]::-moz-range-thumb {
503 + width: 16px;
504 + height: 16px;
505 + background: var(--color-text);
506 + border-radius: 50%;
507 + cursor: pointer;
508 + border: none;
509 +}
510 +
511 +.range-value {
512 + min-width: 3em;
513 + text-align: right;
514 +}
\ No newline at end of file
webui/css/modals2.css
+1 -1
@@ -34,7 +34,7 @@ display: flex;
34 transform: translate(-50%, -50%);
35 background-color: var(--color-background-light);
36 width: 90%;
37 - max-width: 800px;
37 + max-width: 960px;
38 max-height: 90vh;
39 border-radius: 8px;
40 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
webui/css/settings.css
-11
@@ -128,17 +128,6 @@ input:checked + .toggler:before {
128 transform: translateX(26px);
129 }
130
131 -/* Range Input Styles */
132 -input[type="range"] {
133 - width: 100%;
134 - cursor: grab;
135 -}
136 -
137 -.range-value {
138 - min-width: 3em;
139 - text-align: right;
140 -}
141 -
131 /* Select Styles */
132 select {
133 cursor: pointer;
webui/index.css
+26 -1098
@@ -115,7 +115,6 @@ p:last-child {
115 }
116
117 body,
118 -#left-panel,
118 #chat-input,
119 .message,
120 .config-button,
@@ -151,260 +150,6 @@ img {
150 scroll-behavior: smooth;
151 }
152
154 -/* Left Panel */
155 -#left-panel {
156 - background-color: var(--color-panel);
157 - border-right: 1px solid var(--color-border);
158 - box-sizing: border-box;
159 - display: -webkit-flex;
160 - display: flex;
161 - flex-direction: column;
162 - justify-content: space-between;
163 - -webkit-transition: all var(--transition-speed) ease-in-out;
164 - transition: all var(--transition-speed) ease-in-out;
165 - width: 250px;
166 - min-width: 250px;
167 - color: var(--color-text);
168 - box-shadow: 1px 0 5px rgba(0, 0, 0, 0.3);
169 - user-select: none;
170 -}
171 -
172 -#left-panel.hidden {
173 - margin-left: -250px;
174 -}
175 -
176 -.left-panel-top {
177 - flex: 1;
178 - display: -webkit-flex;
179 - display: flex;
180 - flex-direction: column;
181 - min-height: 0;
182 - overflow: hidden;
183 - margin-top: 3.5rem;
184 - padding: var(--spacing-md) var(--spacing-md) 0 var(--spacing-md);
185 -}
186 -
187 -.left-panel-top::-webkit-scrollbar {
188 - width: 0px;
189 -}
190 -
191 -.left-panel-top {
192 - scrollbar-width: none;
193 - -ms-overflow-style: none;
194 -}
195 -
196 -#status-section,
197 -.config-section:not(#chats-section) {
198 - flex-shrink: 0;
199 -}
200 -
201 -.left-panel-bottom {
202 - position: relative;
203 - flex-shrink: 0;
204 -}
205 -
206 -/* Sidebar Toggle Button */
207 -.toggle-sidebar-button {
208 - height: 2.6rem;
209 - width: 2.6rem;
210 - background-color: var(--color-background);
211 - border: 0.1rem solid var(--color-border);
212 - border-radius: var(--spacing-xs);
213 - color: var(--color-text);
214 - opacity: 0.8;
215 - cursor: pointer;
216 - left: var(--spacing-md);
217 - padding: 0.47rem 0.56rem;
218 - position: absolute;
219 - top: var(--spacing-md);
220 - z-index: 999;
221 - -webkit-transition: all var(--transition-speed) ease-in-out;
222 - transition: all var(--transition-speed) ease-in-out;
223 -}
224 -
225 -.toggle-sidebar-button:hover {
226 - background-color: var(--color-secondary);
227 - opacity: 1;
228 -}
229 -
230 -.toggle-sidebar-button:active {
231 - opacity: 0.5;
232 -}
233 -
234 -#sidebar-hamburger-svg {
235 - -webkit-transition: all var(--transition-speed) ease;
236 - transition: all var(--transition-speed) ease;
237 -}
238 -
239 -.toggle-sidebar-button:active #sidebar-hamburger-svg {
240 - -webkit-transform: scaleY(0.8);
241 - transform: scaleY(0.8);
242 -}
243 -
244 -.switch-label {
245 - margin-right: 0.5rem;
246 -}
247 -
248 -/* Chats container */
249 -.chat-container {
250 - display: flex;
251 - align-items: center;
252 - position: relative;
253 - width: 100%;
254 - min-height: 30px;
255 - gap: 0;
256 -}
257 -
258 -/* Update the chat-list-button padding to accommodate the vertical layout */
259 -.chat-list-button {
260 - display: block;
261 - width: 100%;
262 - padding: 8px 5px;
263 - cursor: pointer;
264 - overflow: hidden;
265 - position: relative;
266 - border-radius: 4px;
267 - transition: background-color 0.2s ease-in-out;
268 -}
269 -
270 -/* Add some more padding to the list items to accommodate the vertical layout */
271 -.chat-list-button.has-task-container {
272 - padding-top: 6px;
273 - padding-bottom: 6px;
274 -}
275 -
276 -/* Subtle background on hover for the entire row */
277 -.chat-list-button:hover {
278 - background-color: rgba(255, 255, 255, 0.03);
279 -}
280 -
281 -.light-mode .chat-list-button:hover {
282 - background-color: rgba(0, 0, 0, 0.02);
283 -}
284 -
285 -.chat-name {
286 - display: inline-block;
287 - max-width: 160px;
288 - overflow: hidden;
289 - text-overflow: ellipsis;
290 - white-space: nowrap;
291 - cursor: pointer;
292 - padding: 3px 8px;
293 - border-radius: 4px;
294 - transition: background-color 0.2s;
295 - margin-right: 60px; /* Make space for buttons */
296 - font-size: var(--font-size-small); /* Match config button font size */
297 -}
298 -
299 -/* Add a nice hover effect to just the chat name */
300 -.chat-name:hover {
301 - background-color: rgba(255, 255, 255, 0.1);
302 - text-decoration: none;
303 -}
304 -
305 -.light-mode .chat-name:hover {
306 - background-color: rgba(0, 0, 0, 0.05);
307 -}
308 -
309 -.chats-list-container {
310 - flex: 1;
311 - min-height: 0;
312 - overflow-y: auto;
313 - scroll-behavior: smooth;
314 - /* Mask */
315 - mask-image: linear-gradient(
316 - to bottom,
317 - black calc(100% - 20px),
318 - transparent 100%
319 - );
320 - -webkit-mask-image: linear-gradient(
321 - to bottom,
322 - black calc(100% - 20px),
323 - transparent 100%
324 - );
325 - /* Fallback for browsers that do not support mask-image */
326 - background: linear-gradient(to bottom, calc(100% - 20px), transparent 100%);
327 - /* Add padding to account for fade */
328 - padding-bottom: 20px;
329 - scrollbar-width: thin;
330 - -ms-overflow-style: auto;
331 -}
332 -
333 -.chats-list-container::-webkit-scrollbar {
334 - width: 5px;
335 -}
336 -
337 -.chats-list-container::-webkit-scrollbar-track {
338 - background: rgba(0, 0, 0, 0.2);
339 - border-radius: 6px;
340 -}
341 -
342 -.chats-list-container::-webkit-scrollbar-thumb {
343 - background-color: var(--color-border);
344 - border-radius: 6px;
345 -}
346 -
347 -.chats-list-container::-webkit-scrollbar-thumb:hover {
348 - background-color: var(--color-border);
349 -}
350 -
351 -/* Chats Section */
352 -#chats-section {
353 - display: -webkit-flex;
354 - display: flex;
355 - flex-direction: column;
356 - min-height: 0;
357 - flex: 1;
358 - margin-top: 0.5rem;
359 -}
360 -
361 -/* Preferences */
362 -.pref-header {
363 - display: -webkit-flex;
364 - display: flex;
365 - justify-content: space-between;
366 - align-items: center;
367 - cursor: pointer;
368 - user-select: none;
369 - font-size: var(--font-size-normal);
370 - margin: 0.28rem 0 0.6rem 0;
371 -}
372 -
373 -/* Arrow icon */
374 -.arrow-icon {
375 - flex-shrink: 0;
376 - -webkit-transition: transform var(--transition-speed) ease-in-out;
377 - transition: transform var(--transition-speed) ease-in-out;
378 - margin-left: 0.5rem;
379 - width: 16px;
380 - height: 16px;
381 - transform: rotate(90deg);
382 -}
383 -
384 -.arrow-icon.rotated {
385 - -webkit-transform: rotate(-90deg);
386 - transform: rotate(-90deg);
387 -}
388 -
389 -.pref-section {
390 - font-size: var(--font-size-small);
391 - padding: 0.6rem var(--spacing-md) 0.05rem var(--spacing-md);
392 -}
393 -
394 -/* Collapse transition */
395 -.pref-section [x-cloak] {
396 - display: none;
397 -}
398 -
399 -/* Version */
400 -.version-info {
401 - line-height: 0.8rem;
402 - position: relative;
403 - margin: 0 var(--spacing-md) 1rem var(--spacing-md);
404 - padding-top: 10px;
405 - border-top: 1px solid var(--color-border);
406 -}
407 -
153 /* Right Panel */
154 #right-panel {
155 display: -webkit-flex;
@@ -461,110 +206,6 @@ h4 {
206 margin: auto 0;
207 } */
208
464 -#a0version {
465 - color: var(--color-text);
466 - opacity: 0.7;
467 - font-size: 0.7rem;
468 - user-select: all;
469 -}
470 -
471 -
472 -/* Logo Container */
473 -#logo-container {
474 - display: -webkit-flex;
475 - display: flex;
476 - align-items: center;
477 - justify-content: space-between;
478 - position: fixed;
479 - margin-left: 4.6rem;
480 - margin-top: var(--spacing-md);
481 - z-index: 999;
482 - -webkit-transition: margin-left var(--transition-speed) ease-in-out;
483 - transition: margin-left var(--transition-speed) ease-in-out;
484 -}
485 -
486 -#logo-container a {
487 - color: inherit;
488 - text-decoration: none;
489 -}
490 -
491 -#logo-container img {
492 - border-radius: var(--spacing-xs);
493 - width: auto;
494 - height: 2.6rem;
495 - -webkit-transition: filter 0.3s ease;
496 - transition: filter 0.3s ease;
497 -}
498 -
499 -#progress-bar-box {
500 - background-color: var(--color-panel);
501 - padding: var(--spacing-sm) var(--spacing-md);
502 - padding-bottom: 0;
503 - display: flex;
504 - justify-content: space-between;
505 - z-index: 1001;
506 -}
507 -
508 -#progress-bar-box h4 {
509 - margin: 0;
510 -}
511 -
512 -#progress-bar-h {
513 - color: var(--color-primary);
514 - display: flex;
515 - align-items: left;
516 - justify-content: flex-start;
517 - height: 1.2em;
518 - text-overflow: ellipsis;
519 - white-space: nowrap; /* Added for text overflow */
520 - overflow: hidden;
521 - font-weight: normal;
522 -}
523 -
524 -#progress-bar-i {
525 - font-weight: bold;
526 - padding-right: 0.5em;
527 - color: var(--color-secondary);
528 -}
529 -
530 -.progress-bar h4 {
531 - margin-left: 1em;
532 - margin-right: 1.2em;
533 -}
534 -
535 -.shiny-text {
536 - background: linear-gradient(
537 - to right,
538 - var(--color-primary-dark) 20%,
539 - var(--color-text) 40%,
540 - var(--color-text) 60%,
541 - var(--color-primary-dark) 60%
542 - );
543 - background-size: 200% auto;
544 - color: transparent;
545 - -webkit-background-clip: text;
546 - background-clip: text;
547 - animation: shine 1s linear infinite;
548 -}
549 -
550 -#right-panel.expanded #logo-container {
551 - margin-left: 4.6rem;
552 -}
553 -
554 -
555 -
556 -/* Input Section */
557 -#input-section {
558 - position: relative;
559 - background-color: var(--color-panel);
560 - display: -webkit-flex;
561 - display: flex;
562 - flex-direction: column;
563 - gap: var(--spacing-xs);
564 - padding: 0.8rem var(--spacing-md) var(--spacing-sm) var(--spacing-sm);
565 - align-items: start;
566 - flex-shrink: 0;
567 -}
209
210 /* Preview section with unified tile system */
211 .preview-section {
@@ -721,101 +362,6 @@ h4 {
362 content: "⚠️";
363 }
364
724 -/* Text input */
725 -#chat-input-container {
726 - position: relative;
727 - width: 100%;
728 -}
729 -
730 -#chat-input {
731 - background-color: var(--color-input);
732 - border: 1px solid var(--color-border);
733 - border-radius: 8px;
734 - color: var(--color-text);
735 - flex-grow: 1;
736 - font-family: "Roboto Mono", monospace;
737 - font-optical-sizing: auto;
738 - -webkit-font-optical-sizing: auto;
739 - font-size: 0.9rem;
740 - max-height: 7rem;
741 - min-height: 3.05rem;
742 - width: 100%;
743 - padding: 0.48rem 40px var(--spacing-sm) var(--spacing-sm);
744 - margin-right: var(--spacing-xs);
745 - overflow-y: auto;
746 - scroll-behavior: smooth;
747 - resize: none;
748 - align-content: start;
749 - background-clip: border-box;
750 - border: 6px solid transparent;
751 - transition: all 0.3s ease;
752 - -webkit-transition: all 0.3s ease;
753 -}
754 -
755 -#chat-input {
756 - outline: 1px solid var(--color-border);
757 -}
758 -
759 -#expand-button {
760 - position: absolute;
761 - top: 12px;
762 - right: 10px;
763 - background: transparent;
764 - border: none;
765 - cursor: pointer;
766 - font-size: 1.2rem;
767 - color: var(--color-text);
768 - opacity: 0.4;
769 - transition: opacity 0.2s;
770 -}
771 -
772 -#expand-button:hover {
773 - opacity: 0.7;
774 -}
775 -
776 -#expand-button:active {
777 - opacity: 1;
778 -}
779 -
780 -#expand-button svg {
781 - width: 1.3rem;
782 - height: 1.3rem;
783 -}
784 -
785 -#chat-input::-webkit-scrollbar {
786 - width: 6px;
787 - height: 6px;
788 -}
789 -
790 -#chat-input::-webkit-scrollbar-track {
791 - background: transparent;
792 - margin: 4px 0;
793 - border-radius: 6px;
794 -}
795 -
796 -#chat-input::-webkit-scrollbar-thumb {
797 - background-color: rgba(155, 155, 155, 0.5);
798 - border-radius: 6px;
799 - -webkit-transition: background-color 0.2s ease;
800 - transition: background-color 0.2s ease;
801 -}
802 -
803 -#chat-input::-webkit-scrollbar-thumb:hover {
804 - background-color: rgba(155, 155, 155, 0.7);
805 -}
806 -
807 -#chat-input:focus {
808 - outline: 0.05rem solid rgba(155, 155, 155, 0.5);
809 - font-size: 0.955rem;
810 - padding-top: 0.45rem;
811 - background-color: var(--color-input-focus);
812 -}
813 -
814 -#chat-input::placeholder {
815 - color: var(--color-text-muted);
816 - opacity: 0.7;
817 -}
818 -
365 /* Config Section */
366 .config-section > h4 {
367 margin-top: 0;
@@ -833,89 +379,13 @@ h4 {
379 display: -webkit-flex;
380 display: flex;
381 justify-content: space-between;
836 - padding: 0.35rem 0;
382 + padding: 0.28rem 0;
383 }
384
385 .config-list > *:first-child {
386 border-top: 0px;
387 }
388
843 -#pref-list li {
844 - opacity: 0.8;
845 -}
846 -
847 -.config-button {
848 - background-color: var(--color-background);
849 - border: 0.1rem solid var(--color-border);
850 - border-radius: var(--spacing-xs);
851 - cursor: pointer;
852 - display: inline;
853 - font-family: "Rubik", Arial, Helvetica, sans-serif;
854 - font-size: var(--font-size-small);
855 - opacity: 0.8;
856 - text-wrap: nowrap;
857 - width: calc(50% - var(--spacing-xs));
858 - float: left;
859 - margin: 0 var(--spacing-xs) var(--spacing-xs) 0;
860 - padding: var(--spacing-sm) 0.75rem;
861 - max-height: 2.3rem;
862 - -webkit-transition: all var(--transition-speed), transform 0.1s ease-in-out;
863 - transition: all var(--transition-speed), transform 0.1s ease-in-out;
864 -}
865 -
866 -.config-button:hover {
867 - background-color: var(--color-secondary);
868 - opacity: 1;
869 -}
870 -
871 -.config-button:active {
872 - opacity: 0.5;
873 -}
874 -
875 -#settings {
876 - display: flex;
877 - align-items: center;
878 -}
879 -
880 -.edit-button {
881 - background-color: transparent;
882 - border: 1px solid var(--color-border);
883 - border-radius: 0.1875rem;
884 - color: var(--color-primary);
885 - cursor: pointer;
886 - padding: 0.125rem 0.5rem;
887 - -webkit-transition: all var(--transition-speed) ease-in-out;
888 - transition: all var(--transition-speed) ease-in-out;
889 - width: 2rem;
890 - height: 2rem;
891 -}
892 -
893 -.edit-button:hover {
894 - border-color: var(--color-primary);
895 - background-color: #32455690;
896 -}
897 -
898 -.edit-button:active {
899 - background-color: #131a2090;
900 - color: rgba(253, 253, 253, 0.35);
901 -}
902 -
903 -/* Input section layout */
904 -#input-section {
905 - display: flex;
906 - flex-direction: column;
907 - gap: var(--spacing-xs);
908 - padding: var(--spacing-sm) var(--spacing-md) var(--spacing-sm) 0.8rem;
909 - background-color: var(--color-panel);
910 - z-index: 1001;
911 -}
912 -
913 -/* Top row styling */
914 -.input-row {
915 - display: flex;
916 - align-items: center;
917 - gap: var(--spacing-xs);
918 -}
389 /* Attachment icon */
390 .attachment-wrapper {
391 position: relative;
@@ -1123,162 +593,41 @@ h4 {
593 width: 100%;
594 height: 100%;
595 padding: 12px;
1126 - text-align: center;
1127 -}
1128 -
1129 -.extension {
1130 - background: var(--color-primary);
1131 - color: var(--color-text);
1132 - padding: 2px 6px;
1133 - border-radius: 4px;
1134 - font-size: 0.7rem;
1135 - text-transform: uppercase;
1136 -}
1137 -
1138 -.remove-attachment:hover {
1139 - background-color: var(--color-accent);
1140 - transform: scale(1.1);
1141 -}
1142 -
1143 -.remove-attachment:active {
1144 - transform: scale(0.9);
1145 -}
1146 -
1147 -/* Error handling */
1148 -.image-error {
1149 - border: 1px solid var(--color-error);
1150 - padding: 10px;
1151 - color: var(--color-error);
1152 - border-radius: 4px;
1153 - font-size: 0.9em;
1154 - display: flex;
1155 - align-items: center;
1156 - gap: 8px;
1157 -}
1158 -
1159 -.image-error::before {
1160 - content: "⚠️";
1161 -}
1162 -
1163 -/* Text input */
1164 -.input-row {
1165 - width: 100%;
1166 - white-space: nowrap;
1167 -}
1168 -
1169 -/* with text buttons */
1170 -.text-buttons-row {
1171 - width: 100%;
1172 - display: flex;
1173 - padding-top: var(--spacing-xs);
1174 - margin-left: var(--spacing-xs);
1175 -}
1176 -
1177 -.text-button {
1178 - background-color: transparent;
1179 - border: none;
1180 - border-radius: 5px;
1181 - color: var(--color-text);
1182 - font-family: "Rubik", Arial, Helvetica, sans-serif;
1183 - font-size: 0.6rem;
1184 - padding: 6px var(--spacing-sm);
1185 - cursor: pointer;
1186 - opacity: 0.8;
1187 - -webkit-transition: all 0.3s;
1188 - transition: all 0.3s;
1189 - display: flex;
1190 - align-items: center;
1191 - gap: var(--spacing-xs); /* space between icon and text */
1192 -}
1193 -
1194 -.text-button:hover {
1195 - opacity: 1;
1196 - background-color: var(--color-secondary);
1197 - border-radius: 4px;
1198 -}
1199 -
1200 -.text-button:active {
1201 - opacity: 0.5;
1202 -}
1203 -
1204 -.text-button svg {
1205 - width: 14px;
1206 - height: 14px;
1207 - flex-shrink: 0; /* prevents SVG from shrinking */
1208 -}
1209 -
1210 -.text-button p {
1211 - margin-block: 0;
1212 -}
1213 -
1214 -/* Chat buttons (Send and Mic) */
1215 -
1216 -#chat-buttons-wrapper {
1217 - gap: var(--spacing-xs);
1218 - padding-left: var(--spacing-xs);
1219 -}
1220 -
1221 -.chat-button {
1222 - border: none;
1223 - border-radius: 50%;
1224 - color: var(--color-background);
1225 - cursor: pointer;
1226 - font-size: var(--font-size-normal);
1227 - height: 2.525rem;
1228 - width: 2.525rem;
1229 - margin: 0 0.18rem 0 0 var(--spacing-xs);
1230 - display: -webkit-flex;
1231 - display: flex;
1232 - align-items: center;
1233 - justify-content: center;
1234 - flex-shrink: 0;
1235 - flex-grow: 0;
1236 - min-width: 2.525rem;
1237 - -webkit-transition: all var(--transition-speed), transform 0.1s ease-in-out;
1238 - transition: all var(--transition-speed), transform 0.1s ease-in-out;
1239 -}
1240 -
1241 -#send-button {
1242 - background-color: #4248f1;
596 + text-align: center;
597 }
598
1245 -#send-button:hover {
1246 - -webkit-transform: scale(1.05);
1247 - transform: scale(1.05);
1248 - transform-origin: center;
1249 - background-color: #353bc5;
599 +.extension {
600 + background: var(--color-primary);
601 + color: var(--color-text);
602 + padding: 2px 6px;
603 + border-radius: 4px;
604 + font-size: 0.7rem;
605 + text-transform: uppercase;
606 }
607
1252 -#send-button:active {
1253 - -webkit-transform: scale(1);
1254 - transform: scale(1);
1255 - transform-origin: center;
1256 - background-color: #2b309c;
608 +.remove-attachment:hover {
609 + background-color: var(--color-accent);
610 + transform: scale(1.1);
611 }
612
1259 -.chat-button svg {
1260 - width: 1.5rem;
1261 - height: 1.5rem;
613 +.remove-attachment:active {
614 + transform: scale(0.9);
615 }
616
1264 -/* Microphone button */
1265 -.chat-button.mic-inactive svg {
1266 - /* Add specific styles if needed */
617 +/* Error handling */
618 +.image-error {
619 + border: 1px solid var(--color-error);
620 + padding: 10px;
621 + color: var(--color-error);
622 + border-radius: 4px;
623 + font-size: 0.9em;
624 + display: flex;
625 + align-items: center;
626 + gap: 8px;
627 }
628
1269 -/* Tooltip */
1270 -.tooltip {
1271 - position: absolute;
1272 - bottom: 100%;
1273 - left: 50%;
1274 - transform: translateX(0%);
1275 - padding: 8px;
1276 - background-color: var(--color-secondary);
1277 - color: var(--color-text);
1278 - border-radius: 4px;
1279 - font-size: 12px;
1280 - white-space: nowrap;
1281 - z-index: 1002;
629 +.image-error::before {
630 + content: "⚠️";
631 }
632
633 /* Image preview section */
@@ -1381,21 +730,6 @@ input:checked + .slider:before {
730 transform: translateX(1rem);
731 }
732
1384 -#chat-buttons-wrapper {
1385 - line-height: 0.5rem;
1386 - display: -webkit-flex;
1387 - display: flex;
1388 -}
1389 -
1390 -/* Tooltip */
1391 -.tooltip {
1392 - /* Already defined above */
1393 -}
1394 -
1395 -
1396 -
1397 -
1398 -
733 .status-icon {
734 display: flex;
735 justify-content: center;
@@ -1431,24 +765,8 @@ input:checked + .slider:before {
765 }
766
767
1434 -
1435 -
768 /* Media Queries */
769 @media (max-width: 640px) {
1438 - .text-buttons-row {
1439 - display: table;
1440 - gap: 0.1rem !important;
1441 - }
1442 -
1443 - .text-button {
1444 - max-height: 25px;
1445 - }
1446 -
1447 - .text-button p {
1448 - display: none;
1449 - }
1450 -
1451 -
770 /* Responsive tiles for mobile */
771 .preview-section {
772 grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
@@ -1515,21 +833,6 @@ input:checked + .slider:before {
833 }
834
835 @media (max-width: 640px) {
1518 - #chat-input {
1519 - min-height: 5.3rem;
1520 - align-content: start;
1521 - }
1522 -
1523 - #chat-buttons-wrapper {
1524 - display: flex;
1525 - gap: var(--spacing-xs);
1526 - padding: 0;
1527 - width: 3.5rem;
1528 - flex-wrap: wrap;
1529 - -webkit-transition: all 0.3s ease;
1530 - transition: all 0.3s ease;
1531 - }
1532 -
836 .sidebar-overlay {
837 display: none;
838 position: fixed;
@@ -1548,60 +851,6 @@ input:checked + .slider:before {
851 }
852
853 @media (max-width: 768px) {
1551 - #left-panel {
1552 - position: fixed;
1553 - left: 0;
1554 - top: 0;
1555 - bottom: 0;
1556 - width: 250px !important; /* Force width */
1557 - min-width: 250px;
1558 - z-index: 1003;
1559 - -webkit-transition: all var(--transition-speed) ease-in-out;
1560 - transition: all var(--transition-speed) ease-in-out;
1561 - }
1562 -
1563 - #left-panel.hidden {
1564 - margin-left: -250px;
1565 - }
1566 -
1567 - .toggle-sidebar-button {
1568 - position: fixed;
1569 - left: var(--spacing-md);
1570 - z-index: 1004;
1571 - }
1572 -
1573 - #logo-container {
1574 - margin-left: 4.6rem;
1575 - -webkit-transition: all 0.3s ease;
1576 - transition: all 0.3s ease;
1577 - }
1578 -
1579 - #right-panel.expanded #logo-container {
1580 - margin-left: 4.6rem;
1581 - }
1582 -
1583 - #input-section {
1584 - align-items: start;
1585 - }
1586 -
1587 - .text-buttons-row {
1588 - width: 90%;
1589 - display: flex;
1590 - padding-top: var(--spacing-xs);
1591 - gap: var(--spacing-xs);
1592 - white-space: pre-wrap;
1593 - }
1594 -
1595 - .text-button {
1596 - font-size: 0.6rem;
1597 - }
1598 -
1599 - .text-button svg {
1600 - width: 18px;
1601 - height: 18px;
1602 - flex-shrink: 0; /* prevents SVG from shrinking */
1603 - }
1604 -
854 /* .copy-button {
855 display: none !important;
856 } */
@@ -1641,14 +890,6 @@ input:checked + .slider:before {
890 } */
891 }
892
1644 -@media (min-width: 768px) {
1645 - #chat-buttons-wrapper {
1646 - flex-wrap: nowrap;
1647 - -webkit-flex-wrap: nowrap;
1648 - flex-wrap: nowrap;
1649 - }
1650 -}
1651 -
893 @media (max-height: 600px) {
894 /* consistent font sizing */
895 html {
@@ -1662,16 +903,6 @@ input:checked + .slider:before {
903 -webkit-font-smoothing: antialiased;
904 -moz-osx-font-smoothing: grayscale;
905 }
1665 -
1666 - #chats-section {
1667 - min-height: 100%;
1668 - }
1669 -
1670 - .left-panel-top {
1671 - overflow-y: auto;
1672 - -webkit-scroll-behavior: smooth;
1673 - scroll-behavior: smooth;
1674 - }
906 }
907
908 @media screen and (orientation: landscape) {
@@ -1709,42 +940,6 @@ a:active {
940 color: #f44336;
941 }
942
1712 -.light-mode #left-panel {
1713 - box-shadow: 1px 0 25px rgba(0, 0, 0, 0.05);
1714 -}
1715 -
1716 -.light-mode .config-button {
1717 - background-color: var(--color-background);
1718 - color: #333333;
1719 -}
1720 -
1721 -.light-mode .config-button:hover {
1722 - background-color: #d6dae8;
1723 -}
1724 -
1725 -.light-mode .config-button:active {
1726 - background-color: #bdc0cb;
1727 -}
1728 -
1729 -.light-mode .edit-button {
1730 - border-color: var(--color-primary-light);
1731 - color: var(--color-primary-light);
1732 -}
1733 -
1734 -.light-mode .edit-button:hover {
1735 - background-color: #e4e7f0;
1736 -}
1737 -
1738 -.light-mode .edit-button:active {
1739 - background-color: #979fb9;
1740 - color: rgba(0, 0, 0, 0.35);
1741 -}
1742 -
1743 -.light-mode #progress-bar-i {
1744 - color: var(--color-border-dark);
1745 - opacity: 0.5;
1746 -}
1747 -
943 .light-mode .slider {
944 background-color: #f1f1f1;
945 border: 1px solid #dddddd;
@@ -1758,18 +953,12 @@ a:active {
953 background-color: #e6e6e6;
954 }
955
1761 -.light-mode #logo-container img {
1762 - -webkit-filter: invert(100%) grayscale(100%);
1763 - filter: invert(100%) grayscale(100%);
1764 -}
1765 -
956 .light-mode .extension {
957 background: var(--color-primary);
958 color: var(--color-background);
959 opacity: 0.7;
960 }
961
1772 -
962 .dragdrop-overlay {
963 position: fixed;
964 top: 0;
@@ -1852,255 +1041,6 @@ a:active {
1041 }
1042 }
1043
1855 -/* Add to the existing .chat-actions class or create it */
1856 -.chat-actions {
1857 - display: flex;
1858 - gap: 5px;
1859 - position: absolute;
1860 - right: 5px;
1861 - top: 50%;
1862 - transform: translateY(-50%);
1863 - z-index: 2; /* Ensure buttons are above the edit field */
1864 - min-width: 70px; /* Ensure minimum width for the buttons */
1865 - justify-content: flex-end;
1866 -}
1867 -
1868 -/* Tasks list container - similar to chats list */
1869 -.tasks-list-container {
1870 - max-height: 300px;
1871 - overflow-y: auto;
1872 - margin-top: 10px;
1873 - padding-right: 5px;
1874 - border-radius: 5px;
1875 - position: relative;
1876 - scrollbar-width: thin;
1877 - -ms-overflow-style: auto;
1878 -}
1879 -
1880 -.tasks-list-container::-webkit-scrollbar {
1881 - width: 5px;
1882 -}
1883 -
1884 -.tasks-list-container::-webkit-scrollbar-track {
1885 - background: rgba(0, 0, 0, 0.2);
1886 - border-radius: 6px;
1887 -}
1888 -
1889 -.tasks-list-container::-webkit-scrollbar-thumb {
1890 - background-color: var(--color-border);
1891 - border-radius: 6px;
1892 -}
1893 -
1894 -.tasks-list-container::-webkit-scrollbar-thumb:hover {
1895 - background-color: var(--color-border);
1896 -}
1897 -
1898 -.task-name {
1899 - display: block;
1900 - width: 100%;
1901 - overflow: hidden;
1902 - text-overflow: ellipsis;
1903 - white-space: nowrap;
1904 - padding: 3px 0;
1905 - margin-left: 10px;
1906 - cursor: pointer;
1907 - border-radius: 4px;
1908 - transition: background-color 0.2s;
1909 - font-size: var(--font-size-small);
1910 - margin-bottom: 2px;
1911 -}
1912 -
1913 -.task-info-line {
1914 - display: flex;
1915 - justify-content: space-between;
1916 - align-items: center;
1917 - width: 100%;
1918 - margin-top: 2px;
1919 - margin-left: 5px;
1920 -}
1921 -
1922 -.task-name:hover {
1923 - background-color: rgba(255, 255, 255, 0.1);
1924 - text-decoration: none;
1925 -}
1926 -
1927 -.light-mode .task-name:hover {
1928 - background-color: rgba(0, 0, 0, 0.05);
1929 -}
1930 -
1931 -/* Dark mode overrides */
1932 -.light-mode .tab.active {
1933 - color: var(--highlight-pink);
1934 -}
1935 -
1936 -.light-mode .tab.active::after {
1937 - background-color: var(--highlight-pink);
1938 - /* box-shadow: 0 0 8px var(--highlight-pink); */
1939 -}
1940 -/* Tabs styling */
1941 -.tabs-container {
1942 - width: 100%;
1943 - margin-bottom: 8px; /* Reduced spacing between tabs and list */
1944 - padding: 0;
1945 - margin-top: 20px; /* Increased spacing from elements above */
1946 -}
1947 -
1948 -.tabs {
1949 - display: flex;
1950 - width: 100%;
1951 - position: relative;
1952 - gap: 5px;
1953 - border-bottom: 3px solid var(--color-border); /* Thicker bottom line */
1954 - justify-content: center; /* Center the tabs */
1955 -}
1956 -
1957 -.tab {
1958 - padding: 8px 16px;
1959 - cursor: pointer;
1960 - position: relative;
1961 - color: var(--color-text);
1962 - border: 2px solid var(--color-border);
1963 - border-bottom: none;
1964 - border-radius: 8px 8px 0 0;
1965 - transition: all 0.3s ease;
1966 - background-color: var(--color-panel);
1967 - margin-bottom: -3px; /* Match the thicker border */
1968 - z-index: 1;
1969 -}
1970 -
1971 -.tab:not(.active) {
1972 - opacity: 0.8;
1973 - border-bottom: 3px solid var(--color-border);
1974 - background-color: rgba(255, 255, 255, 0.03);
1975 -}
1976 -
1977 -.tab.active {
1978 - border-color: var(--color-border);
1979 - /* box-shadow:
1980 - 0 -4px 8px -2px var(--color-border),
1981 - 4px 0 8px -2px var(--color-border),
1982 - -4px 0 8px -2px var(--color-border); */
1983 - font-weight: bold;
1984 - background-color: var(--color-panel);
1985 -}
1986 -
1987 -.light-mode .tab.active {
1988 - box-shadow: 0 -4px 8px -2px var(--color-border),
1989 - 4px 0 8px -2px var(--color-border), -4px 0 8px -2px var(--color-border);
1990 -}
1991 -
1992 -.light-mode .tab:not(.active) {
1993 - background-color: rgba(0, 0, 0, 0.03);
1994 -}
1995 -
1996 -/* Remove previous tab styling that conflicts */
1997 -.tab.active::after {
1998 - display: none;
1999 -}
2000 -
2001 -/* Empty list message styling enhancement */
2002 -.empty-list-message {
2003 - display: flex;
2004 - justify-content: center;
2005 - align-items: center;
2006 - height: 100px;
2007 - color: var(--color-secondary);
2008 - text-align: center;
2009 - opacity: 0.7;
2010 - font-style: italic;
2011 -}
2012 -
2013 -.light-mode .empty-list-message {
2014 - color: var(--color-secondary-light);
2015 -}
2016 -
2017 -/* Common scrollbar styling */
2018 -::-webkit-scrollbar {
2019 - width: 5px;
2020 - height: 5px;
2021 -}
2022 -
2023 -::-webkit-scrollbar-track {
2024 - background: rgba(0, 0, 0, 0.2);
2025 - border-radius: 6px;
2026 -}
2027 -
2028 -::-webkit-scrollbar-thumb {
2029 - background-color: var(--color-border);
2030 - border-radius: 6px;
2031 - transition: background-color 0.2s ease;
2032 -}
2033 -
2034 -::-webkit-scrollbar-thumb:hover {
2035 - background-color: var(--color-border);
2036 -}
2037 -
2038 -::-webkit-scrollbar-thumb:active {
2039 - background-color: var(--color-border);
2040 -}
2041 -
2042 -/* Firefox scrollbar */
2043 -* {
2044 - scrollbar-width: thin;
2045 - scrollbar-color: var(--color-border) rgba(0, 0, 0, 0.2);
2046 -}
2047 -
2048 -/* Light mode scrollbar */
2049 -.light-mode ::-webkit-scrollbar-track {
2050 - background: rgba(200, 200, 200, 0.3);
2051 -}
2052 -
2053 -.light-mode ::-webkit-scrollbar-thumb {
2054 - background-color: var(--color-border);
2055 -}
2056 -
2057 -.light-mode ::-webkit-scrollbar-thumb:hover {
2058 - background-color: var(--color-border);
2059 -}
2060 -
2061 -.light-mode ::-webkit-scrollbar-thumb:active {
2062 - background-color: var(--color-border);
2063 -}
2064 -
2065 -.light-mode * {
2066 - scrollbar-color: var(--color-border) rgba(200, 200, 200, 0.3);
2067 -}
2068 -
2069 -/* Add specific styling for selected chat items */
2070 -.chat-list-button.font-bold {
2071 - position: relative;
2072 - background-color: var(--color-border) 0.05;
2073 -}
2074 -
2075 -.chat-list-button.font-bold::before {
2076 - content: "";
2077 - position: absolute;
2078 - left: 0;
2079 - top: 0;
2080 - height: 100%;
2081 - width: 3px;
2082 - background-color: var(--color-border);
2083 - border-top-left-radius: 3px;
2084 - border-bottom-left-radius: 3px;
2085 -}
2086 -
2087 -.light-mode .chat-list-button.font-bold {
2088 - background-color: var(--color-border) 0.05;
2089 -}
2090 -
2091 -.light-mode .chat-list-button.font-bold::before {
2092 - background-color: var(--color-border);
2093 -}
2094 -
2095 -/* Make sure the chat container has proper spacing */
2096 -.chat-container,
2097 -.task-container {
2098 - display: flex;
2099 - align-items: center;
2100 - width: 100%;
2101 - justify-content: space-between;
2102 -}
2103 -
1044 /* Settings Modal Styles */
1045 .settings-modal {
1046 position: fixed;
@@ -2641,16 +1581,10 @@ nav ul li a img {
1581 min-width: 40px;
1582 text-align: center;
1583 }
2644 -
2645 -
2646 -
1584 .icon {
1585 vertical-align: middle;
1586 }
1587
2651 -
2652 -
2653 -
1588 /* Animations */
1589 @keyframes fadeIn {
1590 from {
@@ -2677,12 +1611,6 @@ nav ul li a img {
1611 }
1612 }
1613
2680 -@keyframes shine {
2681 - to {
2682 - background-position: -200% center;
2683 - }
2684 -}
2685 -
1614 @keyframes heartbeat {
1615 0% {
1616 transform: scale(1);
webui/index.html
+19 -434
@@ -129,225 +129,23 @@
129 <script type="text/javascript" src="js/settings.js"></script>
130 <script type="text/javascript" src="js/file_browser.js"></script>
131 <script type="text/javascript" src="js/modal.js"></script>
132 + <script type="module" src="js/tunnel.js"></script>
133 + <script>
134 + // Expose git info for sidebar component
135 + globalThis.gitinfo = { version: "{{version_no}}", commit_time: "{{version_time}}" };
136 + </script>
137 </head>
138
139 <body class="dark-mode device-pointer">
140 <div class="container">
136 - <div id="sidebar-overlay" class="sidebar-overlay hidden"></div>
137 - <div class="icons-section" id="hide-button" x-data="{ connected: true }">
138 - <!--Sidebar-->
139 - <!-- Sidebar Toggle Button -->
140 - <button id="toggle-sidebar" class="toggle-sidebar-button" aria-label="Toggle Sidebar" aria-expanded="false">
141 - <span aria-hidden="true">
142 - <!-- Hamburger Icon -->
143 - <svg id="sidebar-hamburger-svg" xmlns="http://www.w3.org/2000/svg" width="22" height="22"
144 - viewBox="0 0 24 24" fill="CurrentColor">
145 - <path d="M3 13h18v-2H3v2zm0 4h18v-2H3v2zm0-8h18V7H3v2z"></path>
146 - </svg>
147 - </span>
148 - </button>
149 -
150 - <div id="logo-container">
151 - <a href="https://github.com/agent0ai/agent-zero" target="_blank" rel="noopener noreferrer">
152 - <img src="./public/splash.jpg" alt="a0" width="22" height="22">
153 - </a>
154 - </div>
155 - </div>
156 -
157 - <div id="left-panel" class="panel">
158 - <!--Sidebar upper elements-->
159 - <div class="left-panel-top">
160 - <div class="config-section" x-data="{ showQuickActions: true }">
161 - <button class="config-button" id="resetChat" @click="resetChat()">Reset Chat</button>
162 - <button class="config-button" id="newChat" @click="newChat()">New Chat</button>
163 - <button class="config-button" id="loadChats" @click="loadChats()">Load Chat</button>
164 - <button class="config-button" id="loadChat" @click="saveChat()">Save Chat</button>
165 - <button class="config-button" id="restart" @click="restart()">Restart</button>
166 - <button class="config-button" id="settings" @click="settingsModalProxy.openModal()"><svg
167 - xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-5.0 -17.0 110.0 135.0"
168 - fill="currentColor" width="24" height="24">
169 - <path
170 - d="m52.301 90.102h-4.1016c-3 0-5 0-6.8984-1.3984-1.8984-1.3984-2.5-3.3008-3.5-6.1016l-1.1016-3.6016c-0.19922-0.60156-0.69922-1.1992-1.3984-1.6016l-1.1016-0.60156c-0.60156-0.30078-1.5-0.39844-2.3008-0.19922l-4.3008 1.1992c-3.1016 0.89844-5.1016 1.5-7.3984 0.5-2.3008-0.89844-3.3984-2.8008-5-5.6016l-1.8008-3.1016c-1.5-2.5-2.5-4.3008-2.3008-6.6992 0.19922-2.3984 1.6016-3.8008 3.6016-6.1016l3.8008-4.1992c0.19922-0.19922 0.60156-1.3984 0.60156-2.6016 0-1.1016-0.5-2.3008-0.80078-2.6992l-3.6016-4c-2-2.1992-3.3008-3.6992-3.6016-6.1016-0.19922-2.3984 0.80078-4.1992 2.3008-6.6992l1.8008-3.1016c1.6016-2.8008 2.6992-4.6016 5-5.6016 2.3008-0.89844 4.3008-0.39844 7.3984 0.5l4.3984 1.1992c0.69922 0.10156 1.5 0 2.3008-0.30078l1.1016-0.60156c0.5-0.30078 1-0.89844 1.3008-1.6992l1.1992-3.5c0.89844-2.8008 1.6016-4.6992 3.5-6.1016 1.8984-1.3984 3.8984-1.3984 6.8984-1.3984h4.1016c3 0 5 0 6.8984 1.3984 1.8984 1.3984 2.5 3.1992 3.5 6.1016l1.1992 3.6016c0.19922 0.60156 0.69922 1.1992 1.3984 1.6016l1.1016 0.60156c0.60156 0.30078 1.5 0.39844 2.3008 0.19922l4.3008-1.1992c3.1016-0.89844 5.1016-1.5 7.3984-0.5 2.3008 0.89844 3.3984 2.8008 5 5.5l1.8008 3.1016c1.3984 2.5 2.5 4.3008 2.3008 6.6992-0.19922 2.3984-1.6016 3.8008-3.6016 6.1016l-3.9961 4.4062c-0.19922 0.19922-0.60156 1.3984-0.60156 2.6016 0 1.1016 0.5 2.3008 0.80078 2.6992l3.6016 4c2 2.1992 3.3008 3.6992 3.6016 6.1016 0.19922 2.3984-0.80078 4.1992-2.3008 6.6992l-1.8008 3.1016c-1.6016 2.8008-2.6992 4.6016-5 5.6016-2.3008 0.89844-4.3984 0.39844-7.3984-0.5l-4.3984-1.1992c-0.69922-0.10156-1.5 0-2.3008 0.30078l-1.1016 0.60156c-0.5 0.30078-1 0.89844-1.3008 1.6992l-1.1992 3.5c-0.89844 2.8008-1.6016 4.6992-3.5 6.1016-1.8008 1.293-3.8008 1.293-6.8008 1.293zm-6.6016-7.3008c0.5 0.10156 1.6016 0.10156 2.6016 0.10156h4.1016c1 0 2 0 2.6016-0.10156 0.19922-0.5 0.60156-1.5 0.89844-2.3984l1.1992-3.6016c0.89844-2.3008 2.3984-4.1992 4.3984-5.3984l1.3984-0.80078c2.3984-1.3008 5.1016-1.6016 7.6016-1l4.6016 1.3008c1 0.30078 2.1016 0.60156 2.6992 0.69922 0.30078-0.5 0.89844-1.5 1.3984-2.3984l1.8008-3.1016c0.5-0.80078 1-1.8008 1.1992-2.3008-0.30078-0.39844-1-1.1992-1.6992-2l-3.8008-4.1992c-1.6016-2-2.5-4.8008-2.5-7.3984 0-2.6016 0.89844-5.3984 2.3008-7.3008l3.8984-4.3984c0.69922-0.69922 1.3984-1.5 1.6992-2-0.19922-0.5-0.80078-1.3984-1.1992-2.3008l-1.8984-3.1016c-0.5-0.89844-1.1016-1.8984-1.3984-2.3984-0.60156 0.10156-1.6992 0.39844-2.6992 0.69922l-4.3984 1.3008c-2.6992 0.60156-5.3008 0.30078-7.6016-0.89844l-1.4023-0.80469c-2.1016-1.3984-3.6016-3.1992-4.3984-5.3984l-1.3008-3.8008c-0.30078-0.89844-0.60156-1.8984-0.89844-2.3984-0.5-0.10156-1.6016-0.10156-2.6016-0.10156h-4.1016c-1 0-2 0-2.6016 0.10156-0.19922 0.5-0.60156 1.5-0.89844 2.3984l-1.1992 3.6016c-0.89844 2.3008-2.3984 4.1992-4.3984 5.3984l-1.3984 0.80078c-2.3984 1.3008-5.1016 1.6016-7.6016 1l-4.6016-1.3008c-1-0.30078-2.1016-0.60156-2.6992-0.69922-0.30078 0.5-0.89844 1.5-1.3984 2.3984l-1.8008 3.1016c-0.5 0.80078-1 1.8008-1.1992 2.3008 0.30078 0.39844 1 1.1992 1.6992 2l3.8008 4.1992c1.6016 2 2.5 4.8008 2.5 7.3984 0 2.6016-0.89844 5.3984-2.3008 7.3008l-3.8984 4.3984c-0.69922 0.69922-1.3984 1.5-1.6992 2 0.19922 0.5 0.80078 1.3984 1.1992 2.3008l1.8008 3.1016c0.5 0.89844 1.1016 1.8984 1.3984 2.3984 0.60156-0.10156 1.6992-0.39844 2.6992-0.69922l4.3984-1.1992c2.6992-0.60156 5.3008-0.30078 7.6016 0.89844l1.3984 0.80078c2.1016 1.3008 3.6016 3.1992 4.3984 5.3984l1.3008 3.8008c0.5 0.80078 0.80078 1.8008 1 2.3008z">
171 - </path>
172 - <path
173 - d="m50.301 66.5c-9 0-16.398-7.3008-16.398-16.398 0-9.1016 7.3008-16.398 16.398-16.398 9.1016 0 16.398 7.3008 16.398 16.398 0 9.0977-7.3984 16.398-16.398 16.398zm0-25.5c-5 0-9.1016 4.1016-9.1016 9.1016s4.1016 9.1016 9.1016 9.1016 9.1016-4.1016 9.1016-9.1016c-0.003906-5-4.1016-9.1016-9.1016-9.1016z">
174 - </path>
175 - </svg>Settings</button>
176 - <button class="config-button" id="memory-dash"
177 - @click="openModal('settings/memory/memory-dashboard.html');">Memory</button>
178 -
179 -
180 - </div>
181 -
182 - <!-- Tabs container -->
183 - <div class="tabs-container">
184 - <div class="tabs">
185 - <div class="tab active" id="chats-tab">Chats</div>
186 - <div class="tab" id="tasks-tab">Tasks</div>
187 - </div>
188 - </div>
189 -
190 - <!-- Chats List -->
191 - <div class="config-section" id="chats-section" x-data="{ contexts: [], selected: '' }"
192 - x-show="contexts.length > 0 || true">
193 - <div class="chats-list-container">
194 - <ul class="config-list" x-show="contexts.length > 0">
195 - <template x-for="context in contexts">
196 - <li>
197 - <div :class="{'chat-list-button': true, 'font-bold': context.id === selected}"
198 - @click="selected = context.id; selectChat(context.id)">
199 - <span class="chat-name"
200 - :title="context.name ? context.name : 'Chat #' + context.no"
201 - x-text="context.name ? context.name : 'Chat #' + context.no"></span>
202 - </div>
203 - <button class="edit-button" @click="killChat(context.id)">X</button>
204 - </li>
205 - </template>
206 - </ul>
207 - <div class="empty-list-message" x-show="contexts.length === 0">
208 - <p><i>No chats to list.</i></p>
209 - </div>
210 - </div>
211 - </div>
212 -
213 - <!-- Tasks List -->
214 - <div class="config-section" id="tasks-section" x-data="{
215 - tasks: [],
216 - selected: '',
217 - openTaskDetail(taskId) {
218 - globalThis.openTaskDetail(taskId);
219 - }
220 - }" style="display: none;">
221 - <div class="tasks-list-container">
222 - <ul class="config-list" x-show="tasks.length > 0">
223 - <template x-for="task in tasks">
224 - <li>
225 - <div :class="{'chat-list-button': true, 'font-bold': task.id === selected, 'has-task-container': true}"
226 - @click="selected = task.id; selectChat(task.id)">
227 - <!-- Task container with a vertical layout -->
228 - <div class="task-container task-container-vertical">
229 - <!-- Task name on its own line with full width -->
230 - <span class="task-name"
231 - :title="(task.task_name || `Task #${task.no}`) + ' (' + (task.name || `Chat #${task.no}`) + ')'"
232 - x-text="(task.task_name || `Task #${task.no}`) + ' (' + (task.name || `Chat #${task.no}`) + ')'"
233 - :data-task-id="task.id"></span>
234 - <!-- Second line with status badge and action button -->
235 - <div class="task-info-line">
236 - <!-- Status badge (reusing scheduler styling) -->
237 - <span class="scheduler-status-badge scheduler-status-badge-small"
238 - :class="task.state ? `scheduler-status-${task.state}` : 'scheduler-status-idle'"
239 - x-text="task.state || 'idle'"></span>
240 - <!-- Action buttons -->
241 - <button class="edit-button" @click.stop="openTaskDetail(task.id)"
242 - title="View task details"
243 - style="margin-left: auto; margin-right: 5px;">
244 - <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
245 - viewBox="0 0 512 512" fill="var(--color-primary)"
246 - stroke="currentColor" stroke-width="2" stroke-linecap="round"
247 - stroke-linejoin="round">
248 - <path
249 - d="M256 0c70.69 0 134.69 28.66 181.02 74.98C483.34 121.3 512 185.31 512 256c0 70.69-28.66 134.7-74.98 181.02C390.69 483.34 326.69 512 256 512c-70.69 0-134.69-28.66-181.02-74.98C28.66 390.69 0 326.69 0 256c0-70.69 28.66-134.69 74.98-181.02C121.31 28.66 185.31 0 256 0zm-9.96 161.03c0-4.28.76-8.26 2.27-11.91 1.5-3.63 3.77-6.94 6.79-9.91 3-2.95 6.29-5.2 9.84-6.7 3.57-1.5 7.41-2.28 11.52-2.28 4.12 0 7.96.78 11.49 2.27 3.54 1.51 6.78 3.76 9.75 6.73 2.95 2.97 5.16 6.26 6.64 9.91 1.49 3.63 2.22 7.61 2.22 11.89 0 4.17-.73 8.08-2.21 11.69-1.48 3.6-3.68 6.94-6.65 9.97-2.94 3.03-6.18 5.32-9.72 6.84-3.54 1.51-7.38 2.29-11.52 2.29-4.22 0-8.14-.76-11.75-2.26-3.58-1.51-6.86-3.79-9.83-6.79-2.94-3.02-5.16-6.34-6.63-9.97-1.48-3.62-2.21-7.54-2.21-11.77zm13.4 178.16c-1.11 3.97-3.35 11.76 3.3 11.76 1.44 0 3.27-.81 5.46-2.4 2.37-1.71 5.09-4.31 8.13-7.75 3.09-3.5 6.32-7.65 9.67-12.42 3.33-4.76 6.84-10.22 10.49-16.31.37-.65 1.23-.87 1.89-.48l12.36 9.18c.6.43.73 1.25.35 1.86-5.69 9.88-11.44 18.51-17.26 25.88-5.85 7.41-11.79 13.57-17.8 18.43l-.1.06c-6.02 4.88-12.19 8.55-18.51 11.01-17.58 6.81-45.36 5.7-53.32-14.83-5.02-12.96-.9-27.69 3.06-40.37l19.96-60.44c1.28-4.58 2.89-9.62 3.47-14.33.97-7.87-2.49-12.96-11.06-12.96h-17.45c-.76 0-1.38-.62-1.38-1.38l.08-.48 4.58-16.68c.16-.62.73-1.04 1.35-1.02l89.12-2.79c.76-.03 1.41.57 1.44 1.33l-.07.43-37.76 124.7zm158.3-244.93c-41.39-41.39-98.58-67-161.74-67-63.16 0-120.35 25.61-161.74 67-41.39 41.39-67 98.58-67 161.74 0 63.16 25.61 120.35 67 161.74 41.39 41.39 98.58 67 161.74 67 63.16 0 120.35-25.61 161.74-67 41.39-41.39 67-98.58 67-161.74 0-63.16-25.61-120.35-67-161.74z" />
250 - </svg>
251 - </button>
252 - <button class="edit-button" @click.stop="resetChat(task.id)"
253 - style="margin-right: 5px;" title="Clear task chat">
254 - <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
255 - viewBox="0 0 122.88 121.1" fill="var(--color-primary)"
256 - stroke="currentColor" stroke-width="2" stroke-linecap="round"
257 - stroke-linejoin="round">
258 - <path
259 - d="M62.89,56.03c1.11-0.35,2.34-0.25,3.72,0.37l10.4,7.87c2.26,1.71,4.24,3.78,2.73,6.9 c-0.51,1.06-1.4,2.1-2.38,3.49l-0.53,0.75c-1.97,2.8-2.61,2-5.71,1.83c0.56,13.37,1.75,27.82-2.64,40.88 c-0.87,2.7-3.32,3.44-6.95,2.71l-6.1-2.03c4.11-6.14,6.16-13.85,6.44-22.89c-3.46,8.58-6.8,16.96-10.68,20.86l-6.28-2.08 c0.61-3.05,1.05-5.43,0.35-6.9l-4.07,4.24l-9.33-5.77c6.36-3.36,11.62-7.87,15.6-13.73c-6.69,5.01-12.76,8.1-18.14,8.99 c-2.75,0.83-4.49,0.35-5.16-1.53c-0.48-1.34-0.05-1.77,0.81-2.86c1.11-1.41,2.61-2.67,4.35-3.79c-3.13,1.1-4.64,0.95-6.37,1.51 c-4.9,1.59-9.94-1.86-8.26-6.9c1.07-3.23,3.54-3.09,6.67-4.07l5.42-1.69c-5.19,0.28-10.32,0.45-15.02-0.25 c-5.4-0.8-5.31-0.99-8.24-5.38c-3.94-5.91-6.25-11.45,2.52-9.16c16.73,3.18,33.56,5.34,51.25-0.98c-0.76-1.32-0.9-2.57-0.5-3.73 C57.37,60.94,61.13,56.58,62.89,56.03L62.89,56.03z M113.8,2.42L74.45,51.53c-4.71,6.68,3.2,11.91,8.39,5.64l39.2-49.27 C125.12,1.86,119.13-3.16,113.8,2.42L113.8,2.42z" />
260 - </svg>
261 - </button>
262 - <button title="Delete task" class="edit-button"
263 - @click.stop="deleteTaskGlobal(task.id)">X</button>
264 -
265 - </div>
266 - </div>
267 - </div>
268 - </li>
269 - </template>
270 - </ul>
271 - <div class="empty-list-message" x-show="tasks.length === 0">
272 - <p><i>No tasks to list.</i></p>
273 - </div>
274 - </div>
275 - </div>
276 - </div>
277 - <!--Sidebar lower elements-->
278 - <div class="left-panel-bottom">
279 - <!-- Preferences -->
280 - <div class="pref-section" x-data="{ prefOpen: true }">
281 - <span>
282 - <h3 class="pref-header" @click="prefOpen = !prefOpen">
283 - Preferences
284 - <svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
285 - stroke="currentColor" stroke-width="2" width="16" height="16"
286 - :class="{'rotated': !prefOpen}">
287 - <path d="M8 4l8 8-8 8" />
288 - </svg>
289 - </h3>
290 - <ul class="config-list" id="pref-list" x-show="prefOpen" x-collapse x-transition>
291 - <!-- Preferences Items -->
292 - <li x-data="{ autoScroll: true }">
293 - <span>Autoscroll</span>
294 - <label class="switch">
295 - <input id="auto-scroll-switch" type="checkbox" x-model="autoScroll"
296 - x-effect="globalThis.safeCall('toggleAutoScroll',autoScroll)">
297 - <span class="slider"></span>
298 - </label>
299 - </li>
300 - <li x-data="{ darkMode: localStorage.getItem('darkMode') != 'false' }"
301 - x-init="$watch('darkMode', val => toggleDarkMode(val))">
302 - <span class="switch-label">Dark mode</span>
303 - <label class="switch">
304 - <input type="checkbox" x-model="darkMode">
305 - <span class="slider"></span>
306 - </label>
307 - </li>
308 - <li x-data="{ speech: localStorage.getItem('speech') == 'true' }"
309 - x-init="$watch('speech', val => toggleSpeech(val))">
310 - <span class="switch-label">Speech</span>
311 - <label class="switch">
312 - <input type="checkbox" x-model="speech">
313 - <span class="slider"></span>
314 - </label>
315 - </li>
316 - <li x-data="{ showThoughts: true }">
317 - <span>Show thoughts</span>
318 - <label class="switch">
319 - <input type="checkbox" x-model="showThoughts"
320 - x-effect="globalThis.safeCall('toggleThoughts',showThoughts)">
321 - <span class="slider"></span>
322 - </label>
323 - </li>
324 - <li x-data="{ showJson: false }">
325 - <span>Show JSON</span>
326 - <label class="switch">
327 - <input type="checkbox" x-model="showJson"
328 - x-effect="globalThis.safeCall('toggleJson',showJson)">
329 - <span class="slider"></span>
330 - </label>
331 - </li>
332 - <li x-data="{ showUtils: false }">
333 - <span>Show utility messages</span>
334 - <label class="switch">
335 - <input type="checkbox" x-model="showUtils"
336 - x-effect="globalThis.safeCall('toggleUtils',showUtils)">
337 - <span class="slider"></span>
338 - </label>
339 - </li>
340 - </ul>
341 - </span>
342 - </div>
343 - <!-- Version Info -->
344 - <div class="version-info">
345 - <span id="a0version">Version {{version_no}} {{version_time}}</span>
346 - </div>
347 - </div>
348 - </div>
349 - <div id="right-panel" class="panel">
350 - <!--Chat-->
141 + <!-- Sidebar Overlay -->
142 + <div id="sidebar-overlay" class="sidebar-overlay" x-data :class="{'visible': $store.sidebar.isOpen && $store.sidebar.isMobile()}" @click="$store.sidebar.close()"></div>
143 + <!-- Left Sidebar (Header Icons, Quick Actions, Tabs, Chats, Tasks) -->
144 + <x-component path="sidebar/left-sidebar.html"></x-component>
145 +
146 + <!-- Right Panel (Message History and Input Section) -->
147 + <div id="right-panel" class="panel" :class="{'expanded': !$store.sidebar.isOpen}">
148 + <!-- Time and Date -->
149 <div id="time-date-container">
150 <div id="time-date"></div>
151 <div class="status-icon" x-data="{ connected: true }">
@@ -364,6 +162,7 @@
162 <!-- Notification Toggle positioned next to time-date -->
163 <x-component path="notifications/notification-icons.html"></x-component>
164 </div>
165 + <!-- Message History (actual messages) -->
166 <div id="chat-history">
167 </div>
168
@@ -381,161 +180,10 @@
180 <button class="toast__copy" style="display: none;">Copy</button>
181 <button class="toast__close">Close</button>
182 </div>
384 - <div id="progress-bar-box">
385 - <h4 id="progress-bar-h">
386 - <span id="progress-bar-i">|></span><span id="progress-bar"></span>
387 - </h4>
388 - <h4 id="progress-bar-stop-speech" x-data x-cloak x-show="$store.speech.isSpeaking">
389 - <span id="stop-speech" @click="$store.speech.stop()" style="cursor: pointer">Stop Speech</span>
390 - </h4>
391 - </div>
392 - <div id="input-section" x-data="{
393 - paused: false
394 - }">
395 -
396 - <!-- Attachment Preview section -->
397 - <div>
398 - <x-component path="/chat/attachments/inputPreview.html" />
399 - </div>
400 -
401 - <!-- Top row with input and buttons -->
402 - <div class="input-row">
403 - <!-- Attachment icon with tooltip -->
404 - <div class="attachment-wrapper" x-data="{ showTooltip: false }">
405 - <label for="file-input" class="attachment-icon" @mouseover="showTooltip = true"
406 - @mouseleave="showTooltip = false">
407 - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
408 - fill="currentColor">
409 - <path
410 - d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" />
411 - </svg>
412 - </label>
413 - <input type="file" id="file-input" accept="*" multiple style="display: none"
414 - @change="$store.chatAttachments.handleFileUpload($event)">
415 -
416 - <div x-show="showTooltip" class="tooltip">
417 - Add attachments to the message
418 - </div>
419 - </div>
420 -
421 - <!-- Container for textarea and button -->
422 - <div id="chat-input-container" style="position: relative;">
423 - <textarea id="chat-input" placeholder="Type your message here..." rows="1"></textarea>
424 -
425 - <!-- Expand button inside the textarea container -->
426 - <button id="expand-button" @click="$store.fullScreenInputModal.openModal()"
427 - aria-label="Expand input">
428 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
429 - <path
430 - d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
431 - </svg>
432 - </button>
433 - </div>
434 -
435 - <div id="chat-buttons-wrapper">
436 -
437 - <!-- Send button -->
438 - <button class="chat-button" id="send-button" aria-label="Send message">
439 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
440 - <path d="M25 20 L75 50 L25 80" fill="none" stroke="currentColor" stroke-width="15">
441 - </path>
442 - </svg>
443 - </button>
444 -
445 - <!-- Microphone button -->
446 - <button class="chat-button mic-inactive" id="microphone-button"
447 - aria-label="Start/Stop recording" @click="$store.speech.handleMicrophoneClick()"
448 - x-effect="$store.speech.updateMicrophoneButtonUI()">
449 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 18" fill="currentColor">
450 - <path
451 - d="m8,12c1.66,0,3-1.34,3-3V3c0-1.66-1.34-3-3-3s-3,1.34-3,3v6c0,1.66,1.34,3,3,3Zm-1,1.9c-2.7-.4-4.8-2.6-5-5.4H0c.2,3.8,3.1,6.9,7,7.5v2h2v-2c3.9-.6,6.8-3.7,7-7.5h-2c-.2,2.8-2.3,5-5,5.4h-2Z" />
452 - </svg>
453 -
454 - </button>
455 -
456 - </div>
457 - </div>
458 -
459 - <!-- Bottom row with text buttons -->
460 - <div class="text-buttons-row">
461 -
462 - <button class="text-button" @click="pauseAgent(!paused)">
463 - <!-- Dynamic path that switches between pause and play icons -->
464 - <template x-if="!paused">
465 - <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"
466 - stroke-width="1.5" stroke="currentColor" width="14" height="14">
467 - <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path>
468 - </svg>
469 - </template>
470 - <template x-if="paused">
471 - <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"
472 - stroke-width="1.8" stroke="currentColor" width="14" height="14">
473 - <path d="M8 5v14l11-7z"></path>
474 - </svg>
475 - </template>
476 - </svg>
477 - <span x-text="paused ? 'Resume Agent' : 'Pause Agent'"></span>
478 - </button>
479 -
480 - <button class="text-button" @click="loadKnowledge()"><svg xmlns="http://www.w3.org/2000/svg"
481 - fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
482 - <path stroke-linecap="round" stroke-linejoin="round"
483 - d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5">
484 - </path>
485 - </svg>
486 - <p>Import knowledge</p>
487 - </button>
488 - <button class="text-button" id="work_dir_browser" @click="fileBrowserModalProxy.openModal()">
489 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 123.37 92.59">
490 - <path
491 - d="m5.72,11.5l-3.93,8.73h119.77s-3.96-8.73-3.96-8.73h-60.03c-1.59,0-2.88-1.29-2.88-2.88V1.75H13.72v6.87c0,1.59-1.29,2.88-2.88,2.88h-5.12Z"
492 - fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="7"></path>
493 - <path
494 - d="m6.38,20.23H1.75l7.03,67.03c.11,1.07.55,2.02,1.2,2.69.55.55,1.28.89,2.11.89h97.1c.82,0,1.51-.33,2.05-.87.68-.68,1.13-1.67,1.28-2.79l9.1-66.94H6.38Z"
495 - fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="8"></path>
496 - </svg>
497 - <p>Files</p>
498 - </button>
499 -
500 - <button class="text-button" id="history_inspect" @click="globalThis.openHistoryModal()">
501 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85">
502 - <path fill="currentColor"
503 - d="m59.572,57.949c-.41,0-.826-.105-1.207-.325l-9.574-5.528c-.749-.432-1.21-1.231-1.21-2.095v-14.923c0-1.336,1.083-2.419,2.419-2.419s2.419,1.083,2.419,2.419v13.526l8.364,4.829c1.157.668,1.554,2.148.886,3.305-.448.776-1.261,1.21-2.097,1.21Zm30.427-7.947c0,10.684-4.161,20.728-11.716,28.283-6.593,6.59-15.325,10.69-24.59,11.544-1.223.113-2.448.169-3.669.169-7.492,0-14.878-2.102-21.22-6.068l-15.356,5.733c-.888.331-1.887.114-2.557-.556s-.887-1.669-.556-2.557l5.733-15.351c-4.613-7.377-6.704-16.165-5.899-24.891.854-9.266,4.954-17.998,11.544-24.588,7.555-7.555,17.6-11.716,28.285-11.716s20.73,4.161,28.285,11.716c7.555,7.555,11.716,17.599,11.716,28.283Zm-15.137-24.861c-13.71-13.71-36.018-13.71-49.728,0-11.846,11.846-13.682,30.526-4.365,44.417.434.647.53,1.464.257,2.194l-4.303,11.523,11.528-4.304c.274-.102.561-.153.846-.153.474,0,.944.139,1.348.41,13.888,9.315,32.568,7.479,44.417-4.365,13.707-13.708,13.706-36.014,0-49.723Zm-24.861-4.13c-15.989,0-28.996,13.006-28.996,28.992s13.008,28.992,28.996,28.992c1.336,0,2.419-1.083,2.419-2.419s-1.083-2.419-2.419-2.419c-13.32,0-24.157-10.835-24.157-24.153s10.837-24.153,24.157-24.153,24.153,10.835,24.153,24.153c0,1.336,1.083,2.419,2.419,2.419s2.419-1.083,2.419-2.419c0-15.986-13.006-28.992-28.992-28.992Zm25.041,33.531c-1.294.347-2.057,1.673-1.71,2.963.343,1.289,1.669,2.057,2.963,1.71,1.289-.343,2.053-1.669,1.71-2.963-.347-1.289-1.673-2.057-2.963-1.71Zm-2.03,6.328c-1.335,0-2.419,1.084-2.419,2.419s1.084,2.419,2.419,2.419,2.419-1.084,2.419-2.419-1.084-2.419-2.419-2.419Zm-3.598,5.587c-1.289-.347-2.615.416-2.963,1.71-.343,1.289.421,2.615,1.71,2.963,1.294.347,2.62-.421,2.963-1.71.347-1.294-.416-2.62-1.71-2.963Zm-4.919,4.462c-1.157-.667-2.638-.27-3.306.887-.667,1.157-.27,2.638.887,3.305,1.157.668,2.638.27,3.306-.887.667-1.157.27-2.638-.887-3.306Zm-9.327,3.04c-.946.946-.946,2.478,0,3.42.942.946,2.473.946,3.42,0,.946-.942.946-2.473,0-3.42-.946-.946-2.478-.946-3.42,0Z">
504 - </path>
505 - </svg>
506 - <p>History</p>
507 - </button>
508 -
509 - <button class="text-button" id="ctx_window" @click="globalThis.openCtxWindowModal()">
510 - <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" fill="currentColor">
511 - <path
512 - d="m63 25c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z">
513 - </path>
514 - <path
515 - d="m63 79c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z">
516 - </path>
517 - <path
518 - d="m68 39h-36c-6.0703 0-11 4.9297-11 11s4.9297 11 11 11h36c6.0703 0 11-4.9297 11-11s-4.9297-11-11-11zm0 18h-36c-3.8594 0-7-3.1406-7-7s3.1406-7 7-7h36c3.8594 0 7 3.1406 7 7s-3.1406 7-7 7z">
519 - </path>
520 - </svg>
521 - <p>Context</p>
522 - </button>
523 -
524 - <button class="text-button" id="nudges_window" @click="nudge()">
525 - <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49 58"
526 - fill="currentColor">
527 - <path
528 - d="m11.97,16.32c-.46,0-.91-.25-1.15-.68-.9-1.63-1.36-3.34-1.36-5.1C9.45,4.73,14.18,0,20,0s10.55,4.73,10.55,10.55c0,.87-.13,1.75-.41,2.76-.19.7-.9,1.13-1.62.93-.7-.19-1.12-.92-.93-1.62.21-.79.31-1.44.31-2.07,0-4.36-3.55-7.91-7.91-7.91s-7.91,3.55-7.91,7.91c0,1.3.35,2.59,1.03,3.82.36.64.13,1.44-.51,1.79-.21.11-.42.17-.64.17Z"
529 - stroke-width="0.5" stroke="currentColor" />
530 - <path
531 - d="m34.5,58h-6.18c-3.17,0-6.15-1.23-8.39-3.47L1.16,35.75c-1.54-1.54-1.54-4.05,0-5.59,2.4-2.4,6.27-2.68,8.99-.64l4.58,3.44V10.55c0-2.91,2.36-5.27,5.27-5.27s5.27,2.36,5.27,5.27v8.62c.78-.45,1.68-.71,2.64-.71,2.3,0,4.26,1.48,4.98,3.53.84-.56,1.85-.89,2.93-.89,2.3,0,4.26,1.48,4.98,3.53.84-.56,1.85-.89,2.93-.89,2.91,0,5.27,2.36,5.27,5.27v14.5c0,8-6.51,14.5-14.5,14.5ZM6.03,30.79c-1.1,0-2.19.42-3.01,1.23-.51.51-.51,1.35,0,1.86l18.77,18.78c1.74,1.74,4.06,2.7,6.53,2.7h6.18c6.54,0,11.86-5.32,11.86-11.86v-14.5c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v1.32c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32v-3.95c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v3.95c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32v-6.59c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v6.59c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32V10.55c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v25.05c0,.5-.28.95-.73,1.18s-.98.18-1.38-.12l-6.69-5.02c-.75-.56-1.65-.84-2.54-.84Z"
532 - stroke-width="0.5" stroke="currentColor" />
533 - </svg>
534 - <p>Nudge</p>
535 - </button>
536 -
537 - </div>
538 - </div>
183 + <!-- Progress Bar -->
184 + <x-component path="chat/input/progress.html"></x-component>
185 + <!-- Input Section -->
186 + <x-component path="chat/input/chat-bar.html"></x-component>
187 </div>
188 </div>
189 <div id="settingsModal" x-data="settingsModalProxy">
@@ -1682,70 +1330,7 @@
1330 </div>
1331
1332 <!-- Full Screen Input Modal -->
1685 - <div id="fullScreenInputModal" x-data="fullScreenInputModalProxy">
1686 - <template x-teleport="body">
1687 - <div x-show="isOpen" class="modal-overlay" @click.self="handleClose()"
1688 - @keydown.escape.window="handleClose()" x-transition>
1689 - <div class="modal-container full-screen-input-modal">
1690 - <div class="modal-content">
1691 - <button class="modal-close" @click="handleClose()">&times;</button>
1692 -
1693 - <!-- Add toolbar -->
1694 - <div class="editor-toolbar">
1695 - <div class="toolbar-group">
1696 - <button class="toolbar-button" @click="undo()" :disabled="!canUndo"
1697 - title="Undo (Ctrl+Z)">
1698 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1699 - stroke="currentColor" stroke-width="2">
1700 - <path d="M3 7v6h6"></path>
1701 - <path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"></path>
1702 - </svg>
1703 - </button>
1704 - <button class="toolbar-button" @click="redo()" :disabled="!canRedo"
1705 - title="Redo (Ctrl+Shift+Z)">
1706 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1707 - stroke="currentColor" stroke-width="2">
1708 - <path d="M21 7v6h-6"></path>
1709 - <path d="M3 17a9 9 0 019-9 9 9 0 016 2.3l3 2.7"></path>
1710 - </svg>
1711 - </button>
1712 - </div>
1713 - <div class="toolbar-group">
1714 - <button class="toolbar-button" @click="clearText()" title="Clear Text">
1715 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1716 - stroke="currentColor" stroke-width="2">
1717 - <path
1718 - d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2">
1719 - </path>
1720 - <line x1="10" y1="11" x2="10" y2="17"></line>
1721 - <line x1="14" y1="11" x2="14" y2="17"></line>
1722 - </svg>
1723 - </button>
1724 - <button class="toolbar-button" @click="toggleWrap()" :class="{ active: wordWrap }"
1725 - title="Toggle Word Wrap">
1726 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1727 - stroke="currentColor" stroke-width="2">
1728 - <path d="M3 6h18M3 12h15l3 3-3 3M3 18h18"></path>
1729 - </svg>
1730 - </button>
1731 - </div>
1732 - </div>
1733 -
1734 - <textarea id="full-screen-input" x-model="inputText" placeholder="Type your message here..."
1735 - @keydown.ctrl.enter="handleClose()" @keydown.ctrl.z.prevent="undo()"
1736 - @keydown.ctrl.shift.z.prevent="redo()"
1737 - :style="{ 'white-space': wordWrap ? 'pre-wrap' : 'pre' }"
1738 - @input="updateHistory()"></textarea>
1739 - </div>
1740 - <div class="modal-footer">
1741 - <div id="buttons-container">
1742 - <button class="btn btn-ok" @click="handleClose()">Done (Ctrl+Enter)</button>
1743 - </div>
1744 - </div>
1745 - </div>
1746 - </div>
1747 - </template>
1748 - </div>
1333 + <x-component path="modals/full-screen-input.html"></x-component>
1334
1335 <!-- Drag and Drop Overlay Component -->
1336 <x-component path="chat/attachments/dragDropOverlay.html"></x-component>
webui/index.js
+126 -681
@@ -5,99 +5,36 @@ import { sleep } from "/js/sleep.js";
5 import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
6 import { store as speechStore } from "/components/chat/speech/speech-store.js";
7 import { store as notificationStore } from "/components/notifications/notification-store.js";
8 +import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
9 +import { store as inputStore } from "/components/chat/input/input-store.js";
10 +import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
11 +import { store as tasksStore } from "/components/sidebar/tasks/tasks-store.js";
12
13 globalThis.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine
14
11 -const leftPanel = document.getElementById("left-panel");
12 -const rightPanel = document.getElementById("right-panel");
13 -const container = document.querySelector(".container");
14 -const chatInput = document.getElementById("chat-input");
15 -const chatHistory = document.getElementById("chat-history");
16 -const sendButton = document.getElementById("send-button");
17 -const inputSection = document.getElementById("input-section");
18 -const statusSection = document.getElementById("status-section");
19 -const chatsSection = document.getElementById("chats-section");
20 -const tasksSection = document.getElementById("tasks-section");
21 -const progressBar = document.getElementById("progress-bar");
22 -const autoScrollSwitch = document.getElementById("auto-scroll-switch");
23 -const timeDate = document.getElementById("time-date-container");
15 +// Declare variables for DOM elements, they will be assigned on DOMContentLoaded
16 +let leftPanel, rightPanel, container, chatInput, chatHistory, sendButton, inputSection, statusSection, progressBar, autoScrollSwitch, timeDate;
17
18 let autoScroll = true;
19 let context = "";
27 -let resetCounter = 0;
20 +globalThis.resetCounter = 0; // Used by stores and getChatBasedId
21 let skipOneSpeech = false;
22 let connectionStatus = undefined; // undefined = not checked yet, true = connected, false = disconnected
23
31 -// Initialize the toggle button
32 -setupSidebarToggle();
33 -// Initialize tabs
34 -setupTabs();
35 -
24 export function getAutoScroll() {
25 return autoScroll;
26 }
27
40 -function isMobile() {
41 - return window.innerWidth <= 768;
42 -}
43 -
44 -function toggleSidebar(show) {
45 - const overlay = document.getElementById("sidebar-overlay");
46 - if (typeof show === "boolean") {
47 - leftPanel.classList.toggle("hidden", !show);
48 - rightPanel.classList.toggle("expanded", !show);
49 - overlay.classList.toggle("visible", show);
50 - } else {
51 - leftPanel.classList.toggle("hidden");
52 - rightPanel.classList.toggle("expanded");
53 - overlay.classList.toggle(
54 - "visible",
55 - !leftPanel.classList.contains("hidden")
56 - );
57 - }
58 -}
59 -
60 -function handleResize() {
61 - const overlay = document.getElementById("sidebar-overlay");
62 - if (isMobile()) {
63 - leftPanel.classList.add("hidden");
64 - rightPanel.classList.add("expanded");
65 - overlay.classList.remove("visible");
66 - } else {
67 - leftPanel.classList.remove("hidden");
68 - rightPanel.classList.remove("expanded");
69 - overlay.classList.remove("visible");
70 - }
71 -}
72 -
73 -globalThis.addEventListener("load", handleResize);
74 -globalThis.addEventListener("resize", handleResize);
75 -
76 -document.addEventListener("DOMContentLoaded", () => {
77 - const overlay = document.getElementById("sidebar-overlay");
78 - overlay.addEventListener("click", () => {
79 - if (isMobile()) {
80 - toggleSidebar(false);
81 - }
82 - });
83 -});
84 -
85 -function setupSidebarToggle() {
86 - const leftPanel = document.getElementById("left-panel");
87 - const rightPanel = document.getElementById("right-panel");
88 - const toggleSidebarButton = document.getElementById("toggle-sidebar");
89 - if (toggleSidebarButton) {
90 - toggleSidebarButton.addEventListener("click", toggleSidebar);
91 - } else {
92 - console.error("Toggle sidebar button not found");
93 - setTimeout(setupSidebarToggle, 100);
94 - }
95 -}
96 -document.addEventListener("DOMContentLoaded", setupSidebarToggle);
28 +// Sidebar toggle logic is now handled by sidebar-store.js
29
30 export async function sendMessage() {
31 + const chatInputEl = document.getElementById("chat-input");
32 + if (!chatInputEl) {
33 + console.warn("chatInput not available, cannot send message");
34 + return;
35 + }
36 try {
100 - const message = chatInput.value.trim();
37 + const message = chatInputEl.value.trim();
38 const attachmentsWithUrls = attachmentsStore.getAttachmentsForSending();
39 const hasAttachments = attachmentsWithUrls.length > 0;
40
@@ -106,7 +43,7 @@ export async function sendMessage() {
43 const messageId = generateGUID();
44
45 // Clear input and attachments
109 - chatInput.value = "";
46 + chatInputEl.value = "";
47 attachmentsStore.clearAttachments();
48 adjustTextareaHeight();
49
@@ -166,6 +103,7 @@ export async function sendMessage() {
103 toastFetchError("Error sending message", e); // Will use new notification system
104 }
105 }
106 +globalThis.sendMessage = sendMessage;
107
108 function toastFetchError(text, error) {
109 console.error(text, error);
@@ -187,28 +125,26 @@ function toastFetchError(text, error) {
125 }
126 globalThis.toastFetchError = toastFetchError;
127
190 -chatInput.addEventListener("keydown", (e) => {
191 - if (e.key === "Enter" && !e.shiftKey && !e.isComposing && e.keyCode !== 229) {
192 - e.preventDefault();
193 - sendMessage();
194 - }
195 -});
196 -
197 -sendButton.addEventListener("click", sendMessage);
128 +// Event listeners will be set up in DOMContentLoaded
129
130 export function updateChatInput(text) {
131 + const chatInputEl = document.getElementById("chat-input");
132 + if (!chatInputEl) {
133 + console.warn("`chatInput` element not found, cannot update.");
134 + return;
135 + }
136 console.log("updateChatInput called with:", text);
137
138 // Append text with proper spacing
203 - const currentValue = chatInput.value;
139 + const currentValue = chatInputEl.value;
140 const needsSpace = currentValue.length > 0 && !currentValue.endsWith(" ");
205 - chatInput.value = currentValue + (needsSpace ? " " : "") + text + " ";
141 + chatInputEl.value = currentValue + (needsSpace ? " " : "") + text + " ";
142
143 // Adjust height and trigger input event
144 adjustTextareaHeight();
209 - chatInput.dispatchEvent(new Event("input"));
145 + chatInputEl.dispatchEvent(new Event("input"));
146
211 - console.log("Updated chat input value:", chatInput.value);
147 + console.log("Updated chat input value:", chatInputEl.value);
148 }
149
150 function updateUserTime() {
@@ -238,50 +174,23 @@ setInterval(updateUserTime, 1000);
174
175 function setMessage(id, type, heading, content, temp, kvps = null) {
176 const result = msgs.setMessage(id, type, heading, content, temp, kvps);
241 - if (autoScroll) chatHistory.scrollTop = chatHistory.scrollHeight;
177 + const chatHistoryEl = document.getElementById("chat-history");
178 + if (autoScroll && chatHistoryEl) {
179 + chatHistoryEl.scrollTop = chatHistoryEl.scrollHeight;
180 + }
181 return result;
182 }
183
184 globalThis.loadKnowledge = async function () {
246 - const input = document.createElement("input");
247 - input.type = "file";
248 - input.accept = ".txt,.pdf,.csv,.html,.json,.md";
249 - input.multiple = true;
250 -
251 - input.onchange = async () => {
252 - try {
253 - const formData = new FormData();
254 - for (let file of input.files) {
255 - formData.append("files[]", file);
256 - }
257 -
258 - formData.append("ctxid", getContext());
259 -
260 - const response = await api.fetchApi("/import_knowledge", {
261 - method: "POST",
262 - body: formData,
263 - });
264 -
265 - if (!response.ok) {
266 - toast(await response.text(), "error");
267 - } else {
268 - const data = await response.json();
269 - toast(
270 - "Knowledge files imported: " + data.filenames.join(", "),
271 - "success"
272 - );
273 - }
274 - } catch (e) {
275 - toastFetchError("Error loading knowledge", e);
276 - }
277 - };
278 -
279 - input.click();
185 + await inputStore.loadKnowledge();
186 };
187
188 function adjustTextareaHeight() {
283 - chatInput.style.height = "auto";
284 - chatInput.style.height = chatInput.scrollHeight + "px";
189 + const chatInputEl = document.getElementById("chat-input");
190 + if (chatInputEl) {
191 + chatInputEl.style.height = "auto";
192 + chatInputEl.style.height = chatInputEl.scrollHeight + "px";
193 + }
194 }
195
196 export const sendJsonData = async function (url, data) {
@@ -314,17 +223,15 @@ function generateGUID() {
223 function getConnectionStatus() {
224 return connectionStatus;
225 }
226 +globalThis.getConnectionStatus = getConnectionStatus;
227
228 function setConnectionStatus(connected) {
229 connectionStatus = connected;
320 - if (globalThis.Alpine && timeDate) {
321 - const statusIconEl = timeDate.querySelector(".status-icon");
322 - if (statusIconEl) {
323 - const statusIcon = Alpine.$data(statusIconEl);
324 - if (statusIcon) {
325 - statusIcon.connected = connected;
326 - }
327 - }
230 + // Broadcast connection status without touching Alpine directly
231 + try {
232 + window.dispatchEvent(new CustomEvent("connection-status", { detail: { connected } }));
233 + } catch (_e) {
234 + // no-op
235 }
236 }
237
@@ -357,7 +264,8 @@ async function poll() {
264
265 // if the chat has been reset, restart this poll as it may have been called with incorrect log_from
266 if (lastLogGuid != response.log_guid) {
360 - chatHistory.innerHTML = "";
267 + const chatHistoryEl = document.getElementById("chat-history");
268 + if (chatHistoryEl) chatHistoryEl.innerHTML = "";
269 lastLogVersion = 0;
270 lastLogGuid = response.log_guid;
271 await poll();
@@ -389,118 +297,45 @@ async function poll() {
297 notificationStore.updateFromPoll(response);
298
299 //set ui model vars from backend
392 - if (globalThis.Alpine && inputSection) {
393 - const inputAD = Alpine.$data(inputSection);
394 - if (inputAD) {
395 - inputAD.paused = response.paused;
396 - }
397 - }
300 + inputStore.paused = response.paused;
301
302 // Update status icon state
303 setConnectionStatus(true);
304
402 - // Update chats list and sort by created_at time (newer first)
403 - let chatsAD = null;
305 + // Update chats list using store
306 let contexts = response.contexts || [];
405 - if (globalThis.Alpine && chatsSection) {
406 - chatsAD = Alpine.$data(chatsSection);
407 - if (chatsAD) {
408 - chatsAD.contexts = contexts.sort(
409 - (a, b) => (b.created_at || 0) - (a.created_at || 0)
410 - );
411 - }
412 - }
307 + chatsStore.applyContexts(contexts);
308
414 - // Update tasks list and sort by creation time (newer first)
415 - const tasksSection = document.getElementById("tasks-section");
416 - if (globalThis.Alpine && tasksSection) {
417 - const tasksAD = Alpine.$data(tasksSection);
418 - if (tasksAD) {
419 - let tasks = response.tasks || [];
420 -
421 - // Always update tasks to ensure state changes are reflected
422 - if (tasks.length > 0) {
423 - // Sort the tasks by creation time
424 - const sortedTasks = [...tasks].sort(
425 - (a, b) => (b.created_at || 0) - (a.created_at || 0)
426 - );
427 -
428 - // Assign the sorted tasks to the Alpine data
429 - tasksAD.tasks = sortedTasks;
430 - } else {
431 - // Make sure to use a new empty array instance
432 - tasksAD.tasks = [];
433 - }
434 - }
435 - }
309 + // Update tasks list using store
310 + let tasks = response.tasks || [];
311 + tasksStore.applyTasks(tasks);
312
313 // Make sure the active context is properly selected in both lists
314 if (context) {
439 - // Update selection in the active tab
440 - const activeTab = localStorage.getItem("activeTab") || "chats";
441 -
442 - if (activeTab === "chats" && chatsAD) {
443 - chatsAD.selected = context;
444 - localStorage.setItem("lastSelectedChat", context);
445 -
446 - // Check if this context exists in the chats list
447 - const contextExists = contexts.some((ctx) => ctx.id === context);
448 -
449 - // If it doesn't exist in the chats list but we're in chats tab, try to select the first chat
450 - if (!contextExists && contexts.length > 0) {
451 - // Check if the current context is empty before creating a new one
452 - // If there's already a current context and we're just updating UI, don't automatically
453 - // create a new context by calling setContext
454 - const firstChatId = contexts[0].id;
455 -
456 - // Only create a new context if we're not currently in an existing context
457 - // This helps prevent duplicate contexts when switching tabs
315 + // Update selection in both stores
316 + chatsStore.setSelected(context);
317 +
318 + // Check if this context exists in the chats list
319 + const contextExists = chatsStore.contains(context);
320 +
321 + // If it doesn't exist in the chats list, try to select the first chat
322 + if (!contextExists && chatsStore.contexts.length > 0) {
323 + const firstChatId = chatsStore.firstId();
324 + if (firstChatId) {
325 setContext(firstChatId);
459 - chatsAD.selected = firstChatId;
460 - localStorage.setItem("lastSelectedChat", firstChatId);
461 - }
462 - } else if (activeTab === "tasks" && tasksSection) {
463 - const tasksAD = Alpine.$data(tasksSection);
464 - tasksAD.selected = context;
465 - localStorage.setItem("lastSelectedTask", context);
466 -
467 - // Check if this context exists in the tasks list
468 - const taskExists = response.tasks?.some((task) => task.id === context);
469 -
470 - // If it doesn't exist in the tasks list but we're in tasks tab, try to select the first task
471 - if (!taskExists && response.tasks?.length > 0) {
472 - const firstTaskId = response.tasks[0].id;
473 - setContext(firstTaskId);
474 - tasksAD.selected = firstTaskId;
475 - localStorage.setItem("lastSelectedTask", firstTaskId);
326 + chatsStore.setSelected(firstChatId);
327 }
328 }
478 - } else if (
479 - response.tasks &&
480 - response.tasks.length > 0 &&
481 - localStorage.getItem("activeTab") === "tasks"
482 - ) {
483 - // If we're in tasks tab with no selection but have tasks, select the first one
484 - const firstTaskId = response.tasks[0].id;
485 - setContext(firstTaskId);
486 - if (tasksSection) {
487 - const tasksAD = Alpine.$data(tasksSection);
488 - tasksAD.selected = firstTaskId;
489 - localStorage.setItem("lastSelectedTask", firstTaskId);
490 - }
491 - } else if (
492 - contexts.length > 0 &&
493 - localStorage.getItem("activeTab") === "chats" &&
494 - chatsAD
495 - ) {
496 - // If we're in chats tab with no selection but have chats, select the first one
497 - const firstChatId = contexts[0].id;
498 -
499 - // Only set context if we don't already have one to avoid duplicates
500 - if (!context) {
501 - setContext(firstChatId);
502 - chatsAD.selected = firstChatId;
503 - localStorage.setItem("lastSelectedChat", firstChatId);
329 +
330 + tasksStore.setSelected(context);
331 + } else {
332 + // No context selected, try to select the first available item
333 + if (contexts.length > 0) {
334 + const firstChatId = chatsStore.firstId();
335 + if (firstChatId) {
336 + setContext(firstChatId);
337 + chatsStore.setSelected(firstChatId);
338 + }
339 }
340 }
341
@@ -513,6 +348,7 @@ async function poll() {
348
349 return updated;
350 }
351 +globalThis.poll = poll;
352
353 function afterMessagesUpdate(logs) {
354 if (localStorage.getItem("speech") == "true") {
@@ -558,182 +394,41 @@ function speakMessages(logs) {
394 }
395
396 function updateProgress(progress, active) {
397 + const progressBarEl = document.getElementById("progress-bar");
398 + if (!progressBarEl) return;
399 if (!progress) progress = "";
400
401 if (!active) {
564 - removeClassFromElement(progressBar, "shiny-text");
402 + removeClassFromElement(progressBarEl, "shiny-text");
403 } else {
566 - addClassToElement(progressBar, "shiny-text");
404 + addClassToElement(progressBarEl, "shiny-text");
405 }
406
407 progress = msgs.convertIcons(progress);
408
571 - if (progressBar.innerHTML != progress) {
572 - progressBar.innerHTML = progress;
409 + if (progressBarEl.innerHTML != progress) {
410 + progressBarEl.innerHTML = progress;
411 }
412 }
413
414 globalThis.pauseAgent = async function (paused) {
577 - try {
578 - const resp = await sendJsonData("/pause", { paused: paused, context });
579 - } catch (e) {
580 - globalThis.toastFetchError("Error pausing agent", e);
581 - }
415 + await inputStore.pauseAgent(paused);
416 };
417
418 globalThis.resetChat = async function (ctxid = null) {
585 - try {
586 - const resp = await sendJsonData("/chat_reset", {
587 - context: ctxid === null ? context : ctxid,
588 - });
589 - resetCounter++;
590 - if (ctxid === null) updateAfterScroll();
591 - } catch (e) {
592 - globalThis.toastFetchError("Error resetting chat", e);
593 - }
419 + await chatsStore.resetChat(ctxid);
420 };
421
422 globalThis.newChat = async function () {
597 - try {
598 - newContext();
599 - updateAfterScroll();
600 - } catch (e) {
601 - globalThis.toastFetchError("Error creating new chat", e);
602 - }
423 + await chatsStore.newChat();
424 };
425
426 globalThis.killChat = async function (id) {
606 - if (!id) {
607 - console.error("No chat ID provided for deletion");
608 - return;
609 - }
610 -
611 - console.log("Deleting chat with ID:", id);
612 -
613 - try {
614 - const chatsAD = Alpine.$data(chatsSection);
615 - console.log(
616 - "Current contexts before deletion:",
617 - JSON.stringify(chatsAD.contexts.map((c) => ({ id: c.id, name: c.name })))
618 - );
619 -
620 - // switch to another context if deleting current
621 - switchFromContext(id);
622 -
623 - // Delete the chat on the server
624 - await sendJsonData("/chat_remove", { context: id });
625 -
626 - // Update the UI manually to ensure the correct chat is removed
627 - // Deep clone the contexts array to prevent reference issues
628 - const updatedContexts = chatsAD.contexts.filter((ctx) => ctx.id !== id);
629 - console.log(
630 - "Updated contexts after deletion:",
631 - JSON.stringify(updatedContexts.map((c) => ({ id: c.id, name: c.name })))
632 - );
633 -
634 - // Force UI update by creating a new array
635 - chatsAD.contexts = [...updatedContexts];
636 -
637 - updateAfterScroll();
638 -
639 - justToast("Chat deleted successfully", "success", 1000, "chat-removal");
640 - } catch (e) {
641 - console.error("Error deleting chat:", e);
642 - globalThis.toastFetchError("Error deleting chat", e);
643 - }
427 + await chatsStore.killChat(id);
428 };
429
646 -export function switchFromContext(id) {
647 - // If we're deleting the currently selected chat, switch to another one first
648 - if (context === id) {
649 - const chatsAD = Alpine.$data(chatsSection);
650 -
651 - // Find an alternate chat to switch to if we're deleting the current one
652 - let alternateChat = null;
653 - for (let i = 0; i < chatsAD.contexts.length; i++) {
654 - if (chatsAD.contexts[i].id !== id) {
655 - alternateChat = chatsAD.contexts[i];
656 - break;
657 - }
658 - }
659 -
660 - if (alternateChat) {
661 - setContext(alternateChat.id);
662 - } else {
663 - // If no other chats, create a new empty context
664 - newContext();
665 - }
666 - }
667 -}
668 -
669 -// Function to ensure proper UI state when switching contexts
670 -function ensureProperTabSelection(contextId) {
671 - // Get current active tab
672 - const activeTab = localStorage.getItem("activeTab") || "chats";
673 -
674 - // First attempt to determine if this is a task or chat based on the task list
675 - const tasksSection = document.getElementById("tasks-section");
676 - let isTask = false;
677 -
678 - if (tasksSection) {
679 - const tasksAD = Alpine.$data(tasksSection);
680 - if (tasksAD && tasksAD.tasks) {
681 - isTask = tasksAD.tasks.some((task) => task.id === contextId);
682 - }
683 - }
684 -
685 - // If we're selecting a task but are in the chats tab, switch to tasks tab
686 - if (isTask && activeTab === "chats") {
687 - // Store this as the last selected task before switching
688 - localStorage.setItem("lastSelectedTask", contextId);
689 - activateTab("tasks");
690 - return true;
691 - }
692 -
693 - // If we're selecting a chat but are in the tasks tab, switch to chats tab
694 - if (!isTask && activeTab === "tasks") {
695 - // Store this as the last selected chat before switching
696 - localStorage.setItem("lastSelectedChat", contextId);
697 - activateTab("chats");
698 - return true;
699 - }
700 -
701 - return false;
702 -}
703 -
430 globalThis.selectChat = async function (id) {
705 - if (id === context) return; //already selected
706 -
707 - // Check if we need to switch tabs based on the context type
708 - const tabSwitched = ensureProperTabSelection(id);
709 -
710 - // If we didn't switch tabs, proceed with normal selection
711 - if (!tabSwitched) {
712 - // Switch to the new context - this will clear chat history and reset tracking variables
713 - setContext(id);
714 -
715 - // Update both contexts and tasks lists to reflect the selected item
716 - const chatsAD = Alpine.$data(chatsSection);
717 - const tasksSection = document.getElementById("tasks-section");
718 - if (tasksSection) {
719 - const tasksAD = Alpine.$data(tasksSection);
720 - tasksAD.selected = id;
721 - }
722 - chatsAD.selected = id;
723 -
724 - // Store this selection in the appropriate localStorage key
725 - const activeTab = localStorage.getItem("activeTab") || "chats";
726 - if (activeTab === "chats") {
727 - localStorage.setItem("lastSelectedChat", id);
728 - } else if (activeTab === "tasks") {
729 - localStorage.setItem("lastSelectedTask", id);
730 - }
731 -
732 - // Trigger an immediate poll to fetch content
733 - poll();
734 - }
735 -
736 - updateAfterScroll();
431 + await chatsStore.selectChat(id);
432 };
433
434 function generateShortId() {
@@ -749,6 +444,7 @@ export const newContext = function () {
444 context = generateShortId();
445 setContext(context);
446 }
447 +globalThis.newContext = newContext;
448
449 export const setContext = function (id) {
450 if (id == context) return;
@@ -763,19 +459,12 @@ export const setContext = function (id) {
459 speechStore.stopAudio();
460
461 // Clear the chat history immediately to avoid showing stale content
766 - chatHistory.innerHTML = "";
462 + const chatHistoryEl = document.getElementById("chat-history");
463 + if (chatHistoryEl) chatHistoryEl.innerHTML = "";
464
768 - // Update both selected states
769 - if (globalThis.Alpine) {
770 - if (chatsSection) {
771 - const chatsAD = Alpine.$data(chatsSection);
772 - if (chatsAD) chatsAD.selected = id;
773 - }
774 - if (tasksSection) {
775 - const tasksAD = Alpine.$data(tasksSection);
776 - if (tasksAD) tasksAD.selected = id;
777 - }
778 - }
465 + // Update both selected states using stores
466 + chatsStore.setSelected(id);
467 + tasksStore.setSelected(id);
468
469 //skip one speech if enabled when switching context
470 if (localStorage.getItem("speech") == "true") skipOneSpeech = true;
@@ -783,10 +472,12 @@ export const setContext = function (id) {
472
473 export const getContext = function () {
474 return context;
786 -};
475 +}
476 +globalThis.getContext = getContext;
477 +globalThis.setContext = setContext;
478
479 export const getChatBasedId = function (id) {
789 - return context + "-" + resetCounter + "-" + id;
480 + return context + "-" + globalThis.resetCounter + "-" + id;
481 };
482
483 globalThis.toggleAutoScroll = async function (_autoScroll) {
@@ -832,53 +523,11 @@ globalThis.toggleSpeech = function (isOn) {
523 };
524
525 globalThis.nudge = async function () {
835 - try {
836 - const resp = await sendJsonData("/nudge", { ctxid: getContext() });
837 - } catch (e) {
838 - toastFetchError("Error nudging agent", e);
839 - }
526 + await inputStore.nudge();
527 };
528
529 globalThis.restart = async function () {
843 - try {
844 - if (!getConnectionStatus()) {
845 - await toastFrontendError(
846 - "Backend disconnected, cannot restart.",
847 - "Restart Error"
848 - );
849 - return;
850 - }
851 - // First try to initiate restart
852 - const resp = await sendJsonData("/restart", {});
853 - } catch (e) {
854 - // Show restarting message with no timeout and restart group
855 - await toastFrontendInfo("Restarting...", "System Restart", 9999, "restart");
856 -
857 - let retries = 0;
858 - const maxRetries = 240; // Maximum number of retries (60 seconds with 250ms interval)
859 -
860 - while (retries < maxRetries) {
861 - try {
862 - const resp = await sendJsonData("/health", {});
863 - // Server is back up, show success message that replaces the restarting message
864 - await new Promise((resolve) => setTimeout(resolve, 250));
865 - await toastFrontendSuccess("Restarted", "System Restart", 5, "restart");
866 - return;
867 - } catch (e) {
868 - // Server still down, keep waiting
869 - retries++;
870 - await new Promise((resolve) => setTimeout(resolve, 250));
871 - }
872 - }
873 -
874 - // If we get here, restart failed or took too long
875 - await toastFrontendError(
876 - "Restart timed out or failed",
877 - "Restart Error",
878 - 8,
879 - "restart"
880 - );
881 - }
530 + await chatsStore.restart();
531 };
532
533 // Modify this part
@@ -888,113 +537,13 @@ document.addEventListener("DOMContentLoaded", () => {
537 });
538
539 globalThis.loadChats = async function () {
891 - try {
892 - const fileContents = await readJsonFiles();
893 - const response = await sendJsonData("/chat_load", { chats: fileContents });
894 -
895 - if (!response) {
896 - toast("No response returned.", "error");
897 - }
898 - // else if (!response.ok) {
899 - // if (response.message) {
900 - // toast(response.message, "error")
901 - // } else {
902 - // toast("Undefined error.", "error")
903 - // }
904 - // }
905 - else {
906 - setContext(response.ctxids[0]);
907 - toast("Chats loaded.", "success");
908 - }
909 - } catch (e) {
910 - toastFetchError("Error loading chats", e);
911 - }
540 + await chatsStore.loadChats();
541 };
542
543 globalThis.saveChat = async function () {
915 - try {
916 - const response = await sendJsonData("/chat_export", { ctxid: context });
917 -
918 - if (!response) {
919 - toast("No response returned.", "error");
920 - }
921 - // else if (!response.ok) {
922 - // if (response.message) {
923 - // toast(response.message, "error")
924 - // } else {
925 - // toast("Undefined error.", "error")
926 - // }
927 - // }
928 - else {
929 - downloadFile(response.ctxid + ".json", response.content);
930 - toast("Chat file downloaded.", "success");
931 - }
932 - } catch (e) {
933 - toastFetchError("Error saving chat", e);
934 - }
544 + await chatsStore.saveChat();
545 };
546
937 -function downloadFile(filename, content) {
938 - // Create a Blob with the content to save
939 - const blob = new Blob([content], { type: "application/json" });
940 -
941 - // Create a link element
942 - const link = document.createElement("a");
943 -
944 - // Create a URL for the Blob
945 - const url = URL.createObjectURL(blob);
946 - link.href = url;
947 -
948 - // Set the file name for download
949 - link.download = filename;
950 -
951 - // Programmatically click the link to trigger the download
952 - link.click();
953 -
954 - // Clean up by revoking the object URL
955 - setTimeout(() => {
956 - URL.revokeObjectURL(url);
957 - }, 0);
958 -}
959 -
960 -function readJsonFiles() {
961 - return new Promise((resolve, reject) => {
962 - // Create an input element of type 'file'
963 - const input = document.createElement("input");
964 - input.type = "file";
965 - input.accept = ".json"; // Only accept JSON files
966 - input.multiple = true; // Allow multiple file selection
967 -
968 - // Trigger the file dialog
969 - input.click();
970 -
971 - // When files are selected
972 - input.onchange = async () => {
973 - const files = input.files;
974 - if (!files.length) {
975 - resolve([]); // Return an empty array if no files are selected
976 - return;
977 - }
978 -
979 - // Read each file as a string and store in an array
980 - const filePromises = Array.from(files).map((file) => {
981 - return new Promise((fileResolve, fileReject) => {
982 - const reader = new FileReader();
983 - reader.onload = () => fileResolve(reader.result);
984 - reader.onerror = fileReject;
985 - reader.readAsText(file);
986 - });
987 - });
988 -
989 - try {
990 - const fileContents = await Promise.all(filePromises);
991 - resolve(fileContents);
992 - } catch (error) {
993 - reject(error); // In case of any file reading error
994 - }
995 - };
996 - });
997 -}
547
548 function addClassToElement(element, className) {
549 element.classList.add(className);
@@ -1013,6 +562,7 @@ function justToast(text, type = "info", timeout = 5000, group = "") {
562 group
563 )
564 }
565 +globalThis.justToast = justToast;
566
567
568 function toast(text, type = "info", timeout = 5000) {
@@ -1038,13 +588,8 @@ globalThis.toast = toast;
588 // OLD: hideToast function removed - now using new notification system
589
590 function scrollChanged(isAtBottom) {
1041 - if (globalThis.Alpine && autoScrollSwitch) {
1042 - const inputAS = Alpine.$data(autoScrollSwitch);
1043 - if (inputAS) {
1044 - inputAS.autoScroll = isAtBottom;
1045 - }
1046 - }
1047 - // autoScrollSwitch.checked = isAtBottom
591 + // Reflect scroll state into preferences store; UI is bound via x-model
592 + preferencesStore.autoScroll = isAtBottom;
593 }
594
595 function updateAfterScroll() {
@@ -1052,16 +597,15 @@ function updateAfterScroll() {
597 // const tolerancePx = toleranceEm * parseFloat(getComputedStyle(document.documentElement).fontSize); // Convert em to pixels
598 const tolerancePx = 10;
599 const chatHistory = document.getElementById("chat-history");
600 + if (!chatHistory) return;
601 +
602 const isAtBottom =
603 chatHistory.scrollHeight - chatHistory.scrollTop <=
604 chatHistory.clientHeight + tolerancePx;
605
606 scrollChanged(isAtBottom);
607 }
1061 -
1062 -chatHistory.addEventListener("scroll", updateAfterScroll);
1063 -
1064 -chatInput.addEventListener("input", adjustTextareaHeight);
608 +globalThis.updateAfterScroll = updateAfterScroll;
609
610 // setInterval(poll, 250);
611
@@ -1090,137 +634,38 @@ async function startPolling() {
634 _doPoll();
635 }
636
1093 -document.addEventListener("DOMContentLoaded", startPolling);
1094 -
1095 -// Setup event handlers once the DOM is fully loaded
637 +// All initializations and event listeners are now consolidated here
638 document.addEventListener("DOMContentLoaded", function () {
1097 - setupSidebarToggle();
1098 - setupTabs();
1099 - initializeActiveTab();
1100 -});
1101 -
1102 -// Setup tabs functionality
1103 -function setupTabs() {
1104 - const chatsTab = document.getElementById("chats-tab");
1105 - const tasksTab = document.getElementById("tasks-tab");
1106 -
1107 - if (chatsTab && tasksTab) {
1108 - chatsTab.addEventListener("click", function () {
1109 - activateTab("chats");
1110 - });
1111 -
1112 - tasksTab.addEventListener("click", function () {
1113 - activateTab("tasks");
1114 - });
1115 - } else {
1116 - console.error("Tab elements not found");
1117 - setTimeout(setupTabs, 100); // Retry setup
1118 - }
1119 -}
1120 -
1121 -function activateTab(tabName) {
1122 - const chatsTab = document.getElementById("chats-tab");
1123 - const tasksTab = document.getElementById("tasks-tab");
1124 - const chatsSection = document.getElementById("chats-section");
1125 - const tasksSection = document.getElementById("tasks-section");
1126 -
1127 - // Get current context to preserve before switching
1128 - const currentContext = context;
1129 -
1130 - // Store the current selection for the active tab before switching
1131 - const previousTab = localStorage.getItem("activeTab");
1132 - if (previousTab === "chats") {
1133 - localStorage.setItem("lastSelectedChat", currentContext);
1134 - } else if (previousTab === "tasks") {
1135 - localStorage.setItem("lastSelectedTask", currentContext);
1136 - }
1137 -
1138 - // Reset all tabs and sections
1139 - chatsTab.classList.remove("active");
1140 - tasksTab.classList.remove("active");
1141 - chatsSection.style.display = "none";
1142 - tasksSection.style.display = "none";
1143 -
1144 - // Remember the last active tab in localStorage
1145 - localStorage.setItem("activeTab", tabName);
1146 -
1147 - // Activate selected tab and section
1148 - if (tabName === "chats") {
1149 - chatsTab.classList.add("active");
1150 - chatsSection.style.display = "";
1151 -
1152 - // Get the available contexts from Alpine.js data
1153 - const chatsAD = globalThis.Alpine ? Alpine.$data(chatsSection) : null;
1154 - const availableContexts = chatsAD?.contexts || [];
1155 -
1156 - // Restore previous chat selection
1157 - const lastSelectedChat = localStorage.getItem("lastSelectedChat");
1158 -
1159 - // Only switch if:
1160 - // 1. lastSelectedChat exists AND
1161 - // 2. It's different from current context AND
1162 - // 3. The context actually exists in our contexts list OR there are no contexts yet
1163 - if (
1164 - lastSelectedChat &&
1165 - lastSelectedChat !== currentContext &&
1166 - (availableContexts.some((ctx) => ctx.id === lastSelectedChat) ||
1167 - availableContexts.length === 0)
1168 - ) {
1169 - setContext(lastSelectedChat);
1170 - }
1171 - } else if (tabName === "tasks") {
1172 - tasksTab.classList.add("active");
1173 - tasksSection.style.display = "flex";
1174 - tasksSection.style.flexDirection = "column";
1175 -
1176 - // Get the available tasks from Alpine.js data
1177 - const tasksAD = globalThis.Alpine ? Alpine.$data(tasksSection) : null;
1178 - const availableTasks = tasksAD?.tasks || [];
1179 -
1180 - // Restore previous task selection
1181 - const lastSelectedTask = localStorage.getItem("lastSelectedTask");
1182 -
1183 - // Only switch if:
1184 - // 1. lastSelectedTask exists AND
1185 - // 2. It's different from current context AND
1186 - // 3. The task actually exists in our tasks list
1187 - if (
1188 - lastSelectedTask &&
1189 - lastSelectedTask !== currentContext &&
1190 - availableTasks.some((task) => task.id === lastSelectedTask)
1191 - ) {
1192 - setContext(lastSelectedTask);
1193 - }
1194 - }
1195 -
1196 - // Request a poll update
1197 - poll();
1198 -}
1199 -
1200 -// Add function to initialize active tab and selections from localStorage
1201 -function initializeActiveTab() {
1202 - // Initialize selection storage if not present
1203 - if (!localStorage.getItem("lastSelectedChat")) {
1204 - localStorage.setItem("lastSelectedChat", "");
1205 - }
1206 - if (!localStorage.getItem("lastSelectedTask")) {
1207 - localStorage.setItem("lastSelectedTask", "");
639 + // Assign DOM elements to variables now that the DOM is ready
640 + leftPanel = document.getElementById("left-panel");
641 + rightPanel = document.getElementById("right-panel");
642 + container = document.querySelector(".container");
643 + chatInput = document.getElementById("chat-input");
644 + chatHistory = document.getElementById("chat-history");
645 + sendButton = document.getElementById("send-button");
646 + inputSection = document.getElementById("input-section");
647 + statusSection = document.getElementById("status-section");
648 + progressBar = document.getElementById("progress-bar");
649 + autoScrollSwitch = document.getElementById("auto-scroll-switch");
650 + timeDate = document.getElementById("time-date-container");
651 +
652 + // Sidebar and input event listeners are now handled by their respective stores
653 +
654 + if (chatHistory) {
655 + chatHistory.addEventListener("scroll", updateAfterScroll);
656 }
657
1210 - const activeTab = localStorage.getItem("activeTab") || "chats";
1211 - activateTab(activeTab);
1212 -}
658 + // Start polling for updates
659 + startPolling();
660 +});
661
662 /*
663 * A0 Chat UI
664 *
1217 - * Tasks tab functionality:
1218 - * - Tasks are displayed in the Tasks tab with the same mechanics as chats
665 + * Unified sidebar layout:
666 + * - Both Chats and Tasks lists are always visible in a vertical layout
667 * - Both lists are sorted by creation time (newest first)
1220 - * - Selection state is preserved across tab switches
1221 - * - The active tab is remembered across sessions
668 * - Tasks use the same context system as chats for communication with the backend
1223 - * - Future support for renaming and deletion will be implemented later
669 */
670
671 // Open the scheduler detail view for a specific task
webui/js/modal.js
+2 -89
@@ -1,92 +1,5 @@
1 -const fullScreenInputModalProxy = {
2 - isOpen: false,
3 - inputText: '',
4 - wordWrap: true,
5 - undoStack: [],
6 - redoStack: [],
7 - maxStackSize: 100,
8 - lastSavedState: '',
9 -
10 - openModal() {
11 - const chatInput = document.getElementById('chat-input');
12 - this.inputText = chatInput.value;
13 - this.lastSavedState = this.inputText;
14 - this.isOpen = true;
15 - this.undoStack = [];
16 - this.redoStack = [];
17 -
18 - // Focus the full screen input after a short delay to ensure the modal is rendered
19 - setTimeout(() => {
20 - const fullScreenInput = document.getElementById('full-screen-input');
21 - fullScreenInput.focus();
22 - }, 100);
23 - },
24 -
25 - handleClose() {
26 - const chatInput = document.getElementById('chat-input');
27 - chatInput.value = this.inputText;
28 - chatInput.dispatchEvent(new Event('input')); // Trigger input event for textarea auto-resize
29 - this.isOpen = false;
30 - },
31 -
32 - updateHistory() {
33 - // Don't save if the text hasn't changed
34 - if (this.lastSavedState === this.inputText) return;
35 -
36 - this.undoStack.push(this.lastSavedState);
37 - if (this.undoStack.length > this.maxStackSize) {
38 - this.undoStack.shift();
39 - }
40 - this.redoStack = [];
41 - this.lastSavedState = this.inputText;
42 - },
43 -
44 - undo() {
45 - if (!this.canUndo) return;
46 -
47 - this.redoStack.push(this.inputText);
48 - this.inputText = this.undoStack.pop();
49 - this.lastSavedState = this.inputText;
50 - },
51 -
52 - redo() {
53 - if (!this.canRedo) return;
54 -
55 - this.undoStack.push(this.inputText);
56 - this.inputText = this.redoStack.pop();
57 - this.lastSavedState = this.inputText;
58 - },
59 -
60 - clearText() {
61 - if (this.inputText) {
62 - this.updateHistory(); // Save current state before clearing
63 - this.inputText = '';
64 - this.lastSavedState = '';
65 - }
66 - },
67 -
68 - toggleWrap() {
69 - this.wordWrap = !this.wordWrap;
70 - },
71 -
72 - get canUndo() {
73 - return this.undoStack.length > 0;
74 - },
75 -
76 - get canRedo() {
77 - return this.redoStack.length > 0;
78 - }
79 -};
80 -
81 -// Register the full screen input modal with Alpine as a store
82 -document.addEventListener('alpine:init', () => {
83 - Alpine.store('fullScreenInputModal', fullScreenInputModalProxy);
84 -});
85 -
86 -// Also register as a component for x-data usage
87 -document.addEventListener('alpine:init', () => {
88 - Alpine.data('fullScreenInputModalProxy', () => fullScreenInputModalProxy);
89 -});
1 +// Full-Screen Input Modal logic moved to /components/modals/full-screen-store.js
2 +// Keep only other modals here.
3
4 const genericModalProxy = {
5 isOpen: false,
webui/js/modals.js
+7
@@ -128,6 +128,13 @@ export function openModal(modalPath) {
128 .then((doc) => {
129 // Set the title from the document
130 modal.title.innerHTML = doc.title || modalPath;
131 + if (doc.html && doc.html.classList) {
132 + const inner = modal.element.querySelector(".modal-inner");
133 + if (inner) inner.classList.add(...doc.html.classList);
134 + }
135 + if (doc.body && doc.body.classList) {
136 + modal.body.classList.add(...doc.body.classList);
137 + }
138 })
139 .catch((error) => {
140 console.error("Error loading modal content:", error);
webui/js/scheduler.js
+5 -2
@@ -4,7 +4,7 @@
4 */
5
6 import { formatDateTime, getUserTimezone } from './time-utils.js';
7 -import { switchFromContext } from '../index.js';
7 +// switchFromContext is now in chats-store, accessed via globalThis
8
9 // Ensure the showToast function is available
10 // if (typeof window.showToast !== 'function') {
@@ -973,7 +973,10 @@ const fullComponentImplementation = function() {
973 try {
974
975 // if we delete selected context, switch to another first
976 - switchFromContext(taskId);
976 + const chatsStore = globalThis.Alpine?.store('chats');
977 + if (chatsStore && typeof chatsStore.switchFromContext === 'function') {
978 + await chatsStore.switchFromContext(taskId);
979 + }
980
981 const response = await fetchApi('/scheduler_task_delete', {
982 method: 'POST',