refactor(webui): migrate welcome screen to component system architecture

- Implement welcome screen with proper component + store pattern - Embed all CSS within welcome-screen.html (remove external .css file) - Add proper component structure with Alpine guard pattern and init() lifecycle - Implement reactive visibility via store polling instead of manual toggling - Remove welcomeStore import and direct manipulation from index.js - Simplify index.html to single <x-component> tag with minimal Alpine wrappers - Add proper x-show directives to prevent welcome screen/chat overlay issue This refactor ensures zero global dependencies and self-contained component logic following Agent Zero's component system best practices.

Chadwick Jones committed Oct 27, 2025 at 23:36 UTC 8c444c0f7f9a2d66cf1a11df84e8160cd04ff9b3
4 files changed +1577 -197
webui/components/welcome/welcome-screen.html new
+263
@@ -0,0 +1,263 @@
1 +<html>
2 + <head>
3 + <script type="module">
4 + import { store } from "/components/welcome/welcome-store.js";
5 + </script>
6 + </head>
7 + <body>
8 + <div x-data x-show="$store.welcomeStore && $store.welcomeStore.isVisible">
9 + <template x-if="$store.welcomeStore">
10 + <div x-data="$store.welcomeStore" class="welcome-container">
11 + <!-- Agent Zero Logo -->
12 + <div class="welcome-logo-container">
13 + <img
14 + src="/image_get?path=webui/public/lightSymbol.svg"
15 + alt="Agent Zero Logo"
16 + class="welcome-logo"
17 + />
18 + </div>
19 +
20 + <!-- Welcome Title -->
21 + <h1 class="welcome-title">Welcome to Agent Zero</h1>
22 + <p class="welcome-subtitle">
23 + Start a new conversation or explore the features below.
24 + </p>
25 +
26 + <!-- Action Cards -->
27 + <div class="welcome-actions">
28 + <div
29 + class="welcome-action-card"
30 + @click="executeAction('new-chat')"
31 + >
32 + <span
33 + class="material-symbols-outlined welcome-action-icon"
34 + >add_circle</span
35 + >
36 + <h3 class="welcome-action-title">New Chat</h3>
37 + <p class="welcome-action-description">
38 + Start a new conversation
39 + </p>
40 + </div>
41 + <div
42 + class="welcome-action-card"
43 + @click="executeAction('settings')"
44 + >
45 + <span
46 + class="material-symbols-outlined welcome-action-icon"
47 + >settings</span
48 + >
49 + <h3 class="welcome-action-title">Settings</h3>
50 + <p class="welcome-action-description">
51 + Configure Agent Zero
52 + </p>
53 + </div>
54 + <div
55 + class="welcome-action-card"
56 + @click="executeAction('website')"
57 + >
58 + <span
59 + class="material-symbols-outlined welcome-action-icon"
60 + >language</span
61 + >
62 + <h3 class="welcome-action-title">Visit Website</h3>
63 + <p class="welcome-action-description">
64 + Learn more about Agent Zero
65 + </p>
66 + </div>
67 + <div
68 + class="welcome-action-card"
69 + @click="executeAction('github')"
70 + >
71 + <span
72 + class="material-symbols-outlined welcome-action-icon"
73 + >code</span
74 + >
75 + <h3 class="welcome-action-title">Visit GitHub</h3>
76 + <p class="welcome-action-description">
77 + View source code and documentation
78 + </p>
79 + </div>
80 + </div>
81 +
82 + <!-- Footer Info -->
83 + <div class="welcome-footer">
84 + <p>Agent Zero Framework • Open Source AI Assistant</p>
85 + </div>
86 + </div>
87 + </template>
88 + </div>
89 +
90 + <script>
91 + document.addEventListener("alpine:init", () => {
92 + const s = Alpine.store("welcomeStore");
93 + if (s && typeof s.init === "function") s.init();
94 + });
95 + </script>
96 +
97 + <style>
98 + /* Welcome Screen Styles */
99 + .welcome-container {
100 + display: flex;
101 + flex-direction: column;
102 + align-items: center;
103 + justify-content: center;
104 + padding: 2rem;
105 + text-align: center;
106 + background: var(--color-background);
107 + color: var(--color-text);
108 + min-height: 400px;
109 + }
110 +
111 + .welcome-logo-container {
112 + display: flex;
113 + justify-content: center;
114 + align-items: center;
115 + margin-bottom: 1.5rem;
116 + }
117 +
118 + .welcome-logo {
119 + width: 120px;
120 + height: 120px;
121 + filter: brightness(1);
122 + }
123 +
124 + /* Dark mode adjustments for logo */
125 + .dark-mode .welcome-logo {
126 + filter: brightness(1.2);
127 + }
128 +
129 + .welcome-title {
130 + font-size: 2rem;
131 + font-weight: 300;
132 + margin-bottom: 0.8rem;
133 + color: var(--color-text);
134 + }
135 +
136 + .welcome-subtitle {
137 + font-size: 1rem;
138 + margin-bottom: 2rem;
139 + color: var(--color-secondary);
140 + max-width: 600px;
141 + line-height: 1.5;
142 + }
143 +
144 + .welcome-actions {
145 + display: grid;
146 + grid-template-columns: repeat(2, 1fr);
147 + gap: 1.2rem;
148 + max-width: 520px;
149 + width: 100%;
150 + margin: 1.5rem 0;
151 + }
152 +
153 + .welcome-action-card {
154 + background: var(--color-panel);
155 + border: 1px solid var(--color-border);
156 + border-radius: 12px;
157 + padding: 1.5rem;
158 + transition: all 0.3s ease;
159 + cursor: pointer;
160 + text-decoration: none;
161 + color: inherit;
162 + display: flex;
163 + flex-direction: column;
164 + align-items: center;
165 + text-align: center;
166 + min-height: 120px;
167 + justify-content: center;
168 + }
169 +
170 + .welcome-action-card:hover {
171 + border-color: var(--color-accent);
172 + background: var(--color-message-bg);
173 + transform: translateY(-2px);
174 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
175 + }
176 +
177 + .dark-mode .welcome-action-card:hover {
178 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
179 + }
180 +
181 + .welcome-action-icon {
182 + font-size: 2rem;
183 + margin-bottom: 0.8rem;
184 + color: var(--color-accent);
185 + }
186 +
187 + .welcome-action-title {
188 + font-size: 1.1rem;
189 + font-weight: 500;
190 + margin-bottom: 0.4rem;
191 + color: var(--color-text);
192 + }
193 +
194 + .welcome-action-description {
195 + font-size: 0.8rem;
196 + color: var(--color-secondary);
197 + line-height: 1.3;
198 + }
199 +
200 + .welcome-footer {
201 + margin-top: 1.5rem;
202 + opacity: 0.7;
203 + font-size: 0.8rem;
204 + }
205 +
206 + /* Light mode adjustments */
207 + .light-mode .welcome-title {
208 + color: #2d2d2d;
209 + }
210 +
211 + .light-mode .welcome-subtitle {
212 + color: #666;
213 + }
214 +
215 + .light-mode .welcome-action-title {
216 + color: #2d2d2d;
217 + }
218 +
219 + .light-mode .welcome-action-description {
220 + color: #666;
221 + }
222 +
223 + .light-mode .welcome-footer {
224 + color: #666;
225 + }
226 +
227 + /* Responsive design */
228 + @media (max-width: 768px) {
229 + .welcome-container {
230 + padding: 1rem;
231 + }
232 +
233 + .welcome-title {
234 + font-size: 2rem;
235 + }
236 +
237 + .welcome-subtitle {
238 + font-size: 1rem;
239 + margin-bottom: 2rem;
240 + }
241 +
242 + .welcome-actions {
243 + grid-template-columns: 1fr;
244 + gap: 1rem;
245 + margin-bottom: 2rem;
246 + }
247 +
248 + .welcome-action-card {
249 + padding: 1.5rem;
250 + min-height: 120px;
251 + }
252 +
253 + .welcome-action-icon {
254 + font-size: 2rem;
255 + }
256 +
257 + .welcome-action-title {
258 + font-size: 1.1rem;
259 + }
260 + }
261 + </style>
262 + </body>
263 +</html>
webui/components/welcome/welcome-store.js new
+61
@@ -0,0 +1,61 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +import { store as contextStore } from "/components/chat/context/context-store.js";
3 +
4 +const model = {
5 + // State
6 + isVisible: true,
7 +
8 + init() {
9 + // Initialize visibility based on current context
10 + this.updateVisibility();
11 +
12 + // Watch for context changes with faster polling for immediate response
13 + setInterval(() => {
14 + this.updateVisibility();
15 + }, 50); // 50ms for very responsive updates
16 + },
17 +
18 + // Update visibility based on current context
19 + updateVisibility() {
20 + const hasContext = !!(globalThis.getContext && globalThis.getContext());
21 + this.isVisible = !hasContext;
22 + },
23 +
24 + // Hide welcome screen
25 + hide() {
26 + this.isVisible = false;
27 + },
28 +
29 + // Show welcome screen
30 + show() {
31 + this.isVisible = true;
32 + },
33 +
34 + // Execute an action by ID
35 + executeAction(actionId) {
36 + switch (actionId) {
37 + case "new-chat":
38 + if (globalThis.newChat) {
39 + globalThis.newChat();
40 + }
41 + break;
42 + case "settings":
43 + // Open settings modal
44 + const settingsButton = document.getElementById("settings");
45 + if (settingsButton) {
46 + settingsButton.click();
47 + }
48 + break;
49 + case "website":
50 + window.open("https://agent-zero.ai", "_blank");
51 + break;
52 + case "github":
53 + window.open("https://github.com/agent0ai/agent-zero", "_blank");
54 + break;
55 + }
56 + },
57 +};
58 +
59 +// Create and export the store
60 +const store = createStore("welcomeStore", model);
61 +export { store };
webui/index.html
+467 -31
@@ -129,23 +129,225 @@
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>
132 </head>
133
134 <body class="dark-mode device-pointer">
135 <div class="container">
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 -->
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 + <button class="config-button" id="dashboard" @click="deselectChat()">Dashboard</button>
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-->
351 <div id="time-date-container">
352 <div id="time-date"></div>
353 <div class="status-icon" x-data="{ connected: true }">
@@ -162,28 +364,187 @@
364 <!-- Notification Toggle positioned next to time-date -->
365 <x-component path="notifications/notification-icons.html"></x-component>
366 </div>
165 - <!-- Message History (actual messages) -->
166 - <div id="chat-history">
167 - </div>
367
169 - <!-- NEW: Toast Stack Component -->
170 - <div style="position: relative; height: 0;">
171 - <x-component path="notifications/notification-toast-stack.html"></x-component>
368 + <!-- Welcome Screen Component -->
369 + <div x-data x-show="$store.welcomeStore && $store.welcomeStore.isVisible">
370 + <x-component path="welcome/welcome-screen.html"></x-component>
371 </div>
372
174 - <div id="toast" class="toast">
175 - <div class="toast__content">
176 - <div class="toast__title"></div>
177 - <div class="toast__separator"></div>
178 - <div class="toast__message"></div>
373 + <!-- Chat History -->
374 + <div id="chat-history" x-data x-show="!$store.welcomeStore || !$store.welcomeStore.isVisible"></div>
375 +
376 + <!-- NEW: Toast Stack Component -->
377 + <div style="position: relative; height: 0;">
378 + <x-component path="notifications/notification-toast-stack.html"></x-component>
379 + </div>
380 +
381 + <div id="toast" class="toast">
382 + <div class="toast__content">
383 + <div class="toast__title"></div>
384 + <div class="toast__separator"></div>
385 + <div class="toast__message"></div>
386 + </div>
387 + <button class="toast__copy" style="display: none;">Copy</button>
388 + <button class="toast__close">Close</button>
389 + </div>
390 +
391 + <div id="input-section" x-data="{
392 + paused: false
393 + }">
394 +
395 + <div id="progress-bar-box">
396 + <h4 id="progress-bar-h">
397 + <span id="progress-bar-i">|></span><span id="progress-bar"></span>
398 + </h4>
399 + <h4 id="progress-bar-stop-speech" x-data x-cloak x-show="$store.speech.isSpeaking">
400 + <span id="stop-speech" @click="$store.speech.stop()" style="cursor: pointer">Stop Speech</span>
401 + </h4>
402 + </div>
403 +
404 + <!-- Attachment Preview section -->
405 + <div>
406 + <x-component path="/chat/attachments/inputPreview.html" />
407 + </div>
408 +
409 + <!-- Top row with input and buttons -->
410 + <div class="input-row">
411 + <!-- Attachment icon with tooltip -->
412 + <div class="attachment-wrapper" x-data="{ showTooltip: false }">
413 + <label for="file-input" class="attachment-icon" @mouseover="showTooltip = true"
414 + @mouseleave="showTooltip = false">
415 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
416 + fill="currentColor">
417 + <path
418 + 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" />
419 + </svg>
420 + </label>
421 + <input type="file" id="file-input" accept="*" multiple style="display: none"
422 + @change="$store.chatAttachments.handleFileUpload($event)">
423 +
424 + <div x-show="showTooltip" class="tooltip">
425 + Add attachments to the message
426 + </div>
427 + </div>
428 +
429 + <!-- Container for textarea and button -->
430 + <div id="chat-input-container" style="position: relative;">
431 + <textarea id="chat-input" placeholder="Type your message here..." rows="1"></textarea>
432 +
433 +
434 + <!-- Expand button inside the textarea container -->
435 + <button id="expand-button" @click="$store.fullScreenInputModal.openModal()"
436 + aria-label="Expand input">
437 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
438 + <path
439 + d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
440 + </svg>
441 + </button>
442 + </div>
443 +
444 + <div id="chat-buttons-wrapper">
445 +
446 + <!-- Send button -->
447 + <button class="chat-button" id="send-button" aria-label="Send message">
448 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
449 + <path d="M25 20 L75 50 L25 80" fill="none" stroke="currentColor" stroke-width="15">
450 + </path>
451 + </svg>
452 + </button>
453 +
454 + <!-- Microphone button -->
455 + <button class="chat-button mic-inactive" id="microphone-button"
456 + aria-label="Start/Stop recording" @click="$store.speech.handleMicrophoneClick()"
457 + x-effect="$store.speech.updateMicrophoneButtonUI()">
458 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 18" fill="currentColor">
459 + <path
460 + 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" />
461 + </svg>
462 +
463 + </button>
464 +
465 + </div>
466 + </div>
467 +
468 + <!-- Bottom row with text buttons -->
469 + <div class="text-buttons-row">
470 +
471 + <button class="text-button" @click="pauseAgent(!paused)">
472 + <!-- Dynamic path that switches between pause and play icons -->
473 + <template x-if="!paused">
474 + <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"
475 + stroke-width="1.5" stroke="currentColor" width="14" height="14">
476 + <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path>
477 + </svg>
478 + </template>
479 + <template x-if="paused">
480 + <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"
481 + stroke-width="1.8" stroke="currentColor" width="14" height="14">
482 + <path d="M8 5v14l11-7z"></path>
483 + </svg>
484 + </template>
485 + </svg>
486 + <span x-text="paused ? 'Resume Agent' : 'Pause Agent'"></span>
487 + </button>
488 +
489 + <button class="text-button" @click="loadKnowledge()"><svg xmlns="http://www.w3.org/2000/svg"
490 + fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
491 + <path stroke-linecap="round" stroke-linejoin="round"
492 + 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">
493 + </path>
494 + </svg>
495 + <p>Import knowledge</p>
496 + </button>
497 + <button class="text-button" id="work_dir_browser" @click="fileBrowserModalProxy.openModal()">
498 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 123.37 92.59">
499 + <path
500 + 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"
501 + fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="7"></path>
502 + <path
503 + 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"
504 + fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="8"></path>
505 + </svg>
506 + <p>Files</p>
507 + </button>
508 +
509 + <button class="text-button" id="history_inspect" @click="globalThis.openHistoryModal()">
510 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85">
511 + <path fill="currentColor"
512 + 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">
513 + </path>
514 + </svg>
515 + <p>History</p>
516 + </button>
517 +
518 + <button class="text-button" id="ctx_window" @click="globalThis.openCtxWindowModal()">
519 + <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" fill="currentColor">
520 + <path
521 + 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">
522 + </path>
523 + <path
524 + 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">
525 + </path>
526 + <path
527 + 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">
528 + </path>
529 + </svg>
530 + <p>Context</p>
531 + </button>
532 +
533 + <button class="text-button" id="nudges_window" @click="nudge()">
534 + <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49 58"
535 + fill="currentColor">
536 + <path
537 + 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"
538 + stroke-width="0.5" stroke="currentColor" />
539 + <path
540 + 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"
541 + stroke-width="0.5" stroke="currentColor" />
542 + </svg>
543 + <p>Nudge</p>
544 + </button>
545 +
546 </div>
180 - <button class="toast__copy" style="display: none;">Copy</button>
181 - <button class="toast__close">Close</button>
547 </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>
548 </div>
549 </div>
550 <div id="settingsModal" x-data="settingsModalProxy">
@@ -1330,13 +1691,77 @@
1691 </div>
1692
1693 <!-- Full Screen Input Modal -->
1333 - <x-component path="modals/full-screen-input.html"></x-component>
1694 + <div id="fullScreenInputModal" x-data="fullScreenInputModalProxy">
1695 + <template x-teleport="body">
1696 + <div x-show="isOpen" class="modal-overlay" @click.self="handleClose()"
1697 + @keydown.escape.window="handleClose()" x-transition>
1698 + <div class="modal-container full-screen-input-modal">
1699 + <div class="modal-content">
1700 + <button class="modal-close" @click="handleClose()">&times;</button>
1701 +
1702 + <!-- Add toolbar -->
1703 + <div class="editor-toolbar">
1704 + <div class="toolbar-group">
1705 + <button class="toolbar-button" @click="undo()" :disabled="!canUndo"
1706 + title="Undo (Ctrl+Z)">
1707 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1708 + stroke="currentColor" stroke-width="2">
1709 + <path d="M3 7v6h6"></path>
1710 + <path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"></path>
1711 + </svg>
1712 + </button>
1713 + <button class="toolbar-button" @click="redo()" :disabled="!canRedo"
1714 + title="Redo (Ctrl+Shift+Z)">
1715 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1716 + stroke="currentColor" stroke-width="2">
1717 + <path d="M21 7v6h-6"></path>
1718 + <path d="M3 17a9 9 0 019-9 9 9 0 016 2.3l3 2.7"></path>
1719 + </svg>
1720 + </button>
1721 + </div>
1722 + <div class="toolbar-group">
1723 + <button class="toolbar-button" @click="clearText()" title="Clear Text">
1724 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1725 + stroke="currentColor" stroke-width="2">
1726 + <path
1727 + d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2">
1728 + </path>
1729 + <line x1="10" y1="11" x2="10" y2="17"></line>
1730 + <line x1="14" y1="11" x2="14" y2="17"></line>
1731 + </svg>
1732 + </button>
1733 + <button class="toolbar-button" @click="toggleWrap()" :class="{ active: wordWrap }"
1734 + title="Toggle Word Wrap">
1735 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1736 + stroke="currentColor" stroke-width="2">
1737 + <path d="M3 6h18M3 12h15l3 3-3 3M3 18h18"></path>
1738 + </svg>
1739 + </button>
1740 + </div>
1741 + </div>
1742 +
1743 + <textarea id="full-screen-input" x-model="inputText" placeholder="Type your message here..."
1744 + @keydown.ctrl.enter="handleClose()" @keydown.ctrl.z.prevent="undo()"
1745 + @keydown.ctrl.shift.z.prevent="redo()"
1746 + :style="{ 'white-space': wordWrap ? 'pre-wrap' : 'pre' }"
1747 + @input="updateHistory()"></textarea>
1748 + </div>
1749 + <div class="modal-footer">
1750 + <div id="buttons-container">
1751 + <button class="btn btn-ok" @click="handleClose()">Done (Ctrl+Enter)</button>
1752 + </div>
1753 + </div>
1754 + </div>
1755 + </div>
1756 + </template>
1757 + </div>
1758
1759 <!-- Drag and Drop Overlay Component -->
1760 <x-component path="chat/attachments/dragDropOverlay.html"></x-component>
1761
1762 <!-- Register Service Worker for offline support and caching -->
1763 <script>
1764 +<<<<<<< HEAD
1765 if ('serviceWorker' in navigator) {
1766 window.addEventListener('load', () => {
1767 navigator.serviceWorker.register('js/sw.js').then(registration => {
@@ -1346,6 +1771,17 @@
1771 });
1772 });
1773 }
1774 +=======
1775 + if ('serviceWorker' in navigator) {
1776 + window.addEventListener('load', () => {
1777 + navigator.serviceWorker.register('js/sw.js').then(registration => {
1778 + console.log('SW registered: ', registration);
1779 + }).catch(registrationError => {
1780 + console.log('SW registration failed: ', registrationError);
1781 + });
1782 + });
1783 + }
1784 +>>>>>>> ad28629 ( Add welcome screen component with dashboard functionality)
1785 </script>
1786
1787 </body>
webui/index.js
+786 -166
@@ -5,45 +5,112 @@ 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";
8 +import { store as contextStore } from "/components/chat/context/context-store.js";
9
10 globalThis.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine
11
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;
12 +const chatInput = document.getElementById("chat-input");
13 +const chatHistory = document.getElementById("chat-history");
14 +const sendButton = document.getElementById("send-button");
15 +const inputSection = document.getElementById("input-section");
16 +const chatsSection = document.getElementById("chats-section");
17 +const tasksSection = document.getElementById("tasks-section");
18 +const progressBar = document.getElementById("progress-bar");
19 +const autoScrollSwitch = document.getElementById("auto-scroll-switch");
20 +const timeDate = document.getElementById("time-date-container");
21
22 let autoScroll = true;
19 -let context = "";
20 -globalThis.resetCounter = 0; // Used by stores and getChatBasedId
23 +let context = null;
24 +let resetCounter = 0;
25 let skipOneSpeech = false;
26 let connectionStatus = undefined; // undefined = not checked yet, true = connected, false = disconnected
27
28 +// Initialize the toggle button
29 +setupSidebarToggle();
30 +// Initialize tabs
31 +setupTabs();
32 +
33 export function getAutoScroll() {
34 return autoScroll;
35 }
36
28 -// Sidebar toggle logic is now handled by sidebar-store.js
37 +function isMobile() {
38 + return window.innerWidth <= 768;
39 +}
40
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;
41 +function toggleSidebar(show) {
42 + const overlay = document.getElementById("sidebar-overlay");
43 + if (typeof show === "boolean") {
44 + leftPanel.classList.toggle("hidden", !show);
45 + rightPanel.classList.toggle("expanded", !show);
46 + overlay.classList.toggle("visible", show);
47 + } else {
48 + leftPanel.classList.toggle("hidden");
49 + rightPanel.classList.toggle("expanded");
50 + overlay.classList.toggle(
51 + "visible",
52 + !leftPanel.classList.contains("hidden"),
53 + );
54 + }
55 +}
56 +
57 +function handleResize() {
58 + const leftPanel = document.getElementById("left-panel");
59 + const rightPanel = document.getElementById("right-panel");
60 + const overlay = document.getElementById("sidebar-overlay");
61 + if (isMobile()) {
62 + leftPanel.classList.add("hidden");
63 + rightPanel.classList.add("expanded");
64 + overlay.classList.remove("visible");
65 + } else {
66 + leftPanel.classList.remove("hidden");
67 + rightPanel.classList.remove("expanded");
68 + overlay.classList.remove("visible");
69 + }
70 +}
71 +
72 +globalThis.addEventListener("load", handleResize);
73 +globalThis.addEventListener("resize", handleResize);
74 +
75 +document.addEventListener("DOMContentLoaded", () => {
76 + const overlay = document.getElementById("sidebar-overlay");
77 + overlay.addEventListener("click", () => {
78 + if (isMobile()) {
79 + toggleSidebar(false);
80 + }
81 + });
82 +});
83 +
84 +function setupSidebarToggle() {
85 + const leftPanel = document.getElementById("left-panel");
86 + const rightPanel = document.getElementById("right-panel");
87 + const toggleSidebarButton = document.getElementById("toggle-sidebar");
88 + if (toggleSidebarButton) {
89 + toggleSidebarButton.addEventListener("click", toggleSidebar);
90 + } else {
91 + console.error("Toggle sidebar button not found");
92 + setTimeout(setupSidebarToggle, 100);
93 }
94 +}
95 +document.addEventListener("DOMContentLoaded", setupSidebarToggle);
96 +
97 +export async function sendMessage() {
98 try {
37 - const message = chatInputEl.value.trim();
99 + const message = chatInput.value.trim();
100 const attachmentsWithUrls = attachmentsStore.getAttachmentsForSending();
101 const hasAttachments = attachmentsWithUrls.length > 0;
102
103 if (message || hasAttachments) {
104 + // Create new context if none exists (e.g., sending from welcome screen)
105 + if (!context) {
106 + newContext();
107 + }
108 +
109 let response;
110 const messageId = generateGUID();
111
112 // Clear input and attachments
46 - chatInputEl.value = "";
113 + chatInput.value = "";
114 attachmentsStore.clearAttachments();
115 adjustTextareaHeight();
116
@@ -103,7 +170,6 @@ export async function sendMessage() {
170 toastFetchError("Error sending message", e); // Will use new notification system
171 }
172 }
106 -globalThis.sendMessage = sendMessage;
173
174 function toastFetchError(text, error) {
175 console.error(text, error);
@@ -113,38 +179,41 @@ function toastFetchError(text, error) {
179 if (getConnectionStatus()) {
180 // Backend is connected, just show the error
181 toastFrontendError(`${text}: ${errorMessage}`).catch((e) =>
116 - console.error("Failed to show error toast:", e)
182 + console.error("Failed to show error toast:", e),
183 );
184 } else {
185 // Backend is disconnected, show connection error
186 toastFrontendError(
187 `${text} (backend appears to be disconnected): ${errorMessage}`,
122 - "Connection Error"
188 + "Connection Error",
189 ).catch((e) => console.error("Failed to show connection error toast:", e));
190 }
191 }
192 globalThis.toastFetchError = toastFetchError;
193
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;
194 +chatInput.addEventListener("keydown", (e) => {
195 + if (
196 + e.key === "Enter" &&
197 + !e.shiftKey &&
198 + !e.isComposing &&
199 + e.key !== "Process"
200 + ) {
201 + e.preventDefault();
202 + sendMessage();
203 }
136 - console.log("updateChatInput called with:", text);
204 +});
205 +
206 +sendButton.addEventListener("click", sendMessage);
207
208 +export function updateChatInput(text) {
209 // Append text with proper spacing
139 - const currentValue = chatInputEl.value;
210 + const currentValue = chatInput.value;
211 const needsSpace = currentValue.length > 0 && !currentValue.endsWith(" ");
141 - chatInputEl.value = currentValue + (needsSpace ? " " : "") + text + " ";
212 + chatInput.value = currentValue + (needsSpace ? " " : "") + text + " ";
213
214 // Adjust height and trigger input event
215 adjustTextareaHeight();
145 - chatInputEl.dispatchEvent(new Event("input"));
146 -
147 - console.log("Updated chat input value:", chatInputEl.value);
216 + chatInput.dispatchEvent(new Event("input"));
217 }
218
219 function updateUserTime() {
@@ -174,23 +243,50 @@ setInterval(updateUserTime, 1000);
243
244 function setMessage(id, type, heading, content, temp, kvps = null) {
245 const result = msgs.setMessage(id, type, heading, content, temp, kvps);
177 - const chatHistoryEl = document.getElementById("chat-history");
178 - if (autoScroll && chatHistoryEl) {
179 - chatHistoryEl.scrollTop = chatHistoryEl.scrollHeight;
180 - }
246 + if (autoScroll) chatHistory.scrollTop = chatHistory.scrollHeight;
247 return result;
248 }
249
250 globalThis.loadKnowledge = async function () {
185 - await inputStore.loadKnowledge();
251 + const input = document.createElement("input");
252 + input.type = "file";
253 + input.accept = ".txt,.pdf,.csv,.html,.json,.md";
254 + input.multiple = true;
255 +
256 + input.onchange = async () => {
257 + try {
258 + const formData = new FormData();
259 + for (let file of input.files) {
260 + formData.append("files[]", file);
261 + }
262 +
263 + formData.append("ctxid", getContext());
264 +
265 + const response = await api.fetchApi("/import_knowledge", {
266 + method: "POST",
267 + body: formData,
268 + });
269 +
270 + if (!response.ok) {
271 + toast(await response.text(), "error");
272 + } else {
273 + const data = await response.json();
274 + toast(
275 + "Knowledge files imported: " + data.filenames.join(", "),
276 + "success",
277 + );
278 + }
279 + } catch (e) {
280 + toastFetchError("Error loading knowledge", e);
281 + }
282 + };
283 +
284 + input.click();
285 };
286
287 function adjustTextareaHeight() {
189 - const chatInputEl = document.getElementById("chat-input");
190 - if (chatInputEl) {
191 - chatInputEl.style.height = "auto";
192 - chatInputEl.style.height = chatInputEl.scrollHeight + "px";
193 - }
288 + chatInput.style.height = "auto";
289 + chatInput.style.height = chatInput.scrollHeight + "px";
290 }
291
292 export const sendJsonData = async function (url, data) {
@@ -223,21 +319,22 @@ function generateGUID() {
319 function getConnectionStatus() {
320 return connectionStatus;
321 }
226 -globalThis.getConnectionStatus = getConnectionStatus;
322
323 function setConnectionStatus(connected) {
324 connectionStatus = connected;
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
325 + if (globalThis.Alpine && timeDate) {
326 + const statusIconEl = timeDate.querySelector(".status-icon");
327 + if (statusIconEl) {
328 + const statusIcon = Alpine.$data(statusIconEl);
329 + if (statusIcon) {
330 + statusIcon.connected = connected;
331 + }
332 + }
333 }
334 }
335
336 let lastLogVersion = 0;
337 let lastLogGuid = "";
240 -let lastSpokenNo = 0;
338
339 async function poll() {
340 let updated = false;
@@ -259,13 +356,18 @@ async function poll() {
356 return false;
357 }
358
262 - if (!context) setContext(response.context);
263 - if (response.context != context) return; //skip late polls after context change
359 + // Skip late polls after context change, but allow polls when both are null or when current context is null (initial load)
360 + if (
361 + response.context != context &&
362 + !(response.context === null && context === null) &&
363 + context !== null
364 + ) {
365 + return;
366 + }
367
368 // if the chat has been reset, restart this poll as it may have been called with incorrect log_from
369 if (lastLogGuid != response.log_guid) {
267 - const chatHistoryEl = document.getElementById("chat-history");
268 - if (chatHistoryEl) chatHistoryEl.innerHTML = "";
370 + chatHistory.innerHTML = "";
371 lastLogVersion = 0;
372 lastLogGuid = response.log_guid;
373 await poll();
@@ -282,7 +384,7 @@ async function poll() {
384 log.heading,
385 log.content,
386 log.temp,
285 - log.kvps
387 + log.kvps,
388 );
389 }
390 afterMessagesUpdate(response.logs);
@@ -297,46 +399,139 @@ async function poll() {
399 notificationStore.updateFromPoll(response);
400
401 //set ui model vars from backend
300 - inputStore.paused = response.paused;
402 + if (globalThis.Alpine && inputSection) {
403 + const inputAD = Alpine.$data(inputSection);
404 + if (inputAD) {
405 + inputAD.paused = response.paused;
406 + }
407 + }
408
409 // Update status icon state
410 setConnectionStatus(true);
411
305 - // Update chats list using store
412 + // Update chats list and sort by created_at time (newer first)
413 + let chatsAD = null;
414 let contexts = response.contexts || [];
307 - chatsStore.applyContexts(contexts);
415
309 - // Update tasks list using store
310 - let tasks = response.tasks || [];
311 - tasksStore.applyTasks(tasks);
416 + // Get chatsSection fresh each time to ensure it exists
417 + const currentChatsSection = document.getElementById("chats-section");
418 +
419 + if (globalThis.Alpine && currentChatsSection) {
420 + try {
421 + chatsAD = Alpine.$data(currentChatsSection);
422 + if (chatsAD) {
423 + const sortedContexts = contexts.sort(
424 + (a, b) => (b.created_at || 0) - (a.created_at || 0),
425 + );
426 + chatsAD.contexts = sortedContexts;
427 + } else {
428 + console.warn("chatsAD is null - Alpine data not available yet");
429 + }
430 + } catch (error) {
431 + console.error("Error updating chats data:", error);
432 + }
433 + } else {
434 + console.warn(
435 + "Missing requirements - Alpine:",
436 + !!globalThis.Alpine,
437 + "chatsSection:",
438 + !!currentChatsSection,
439 + );
440 + }
441 +
442 + // Update tasks list and sort by creation time (newer first)
443 + const tasksSection = document.getElementById("tasks-section");
444 + if (globalThis.Alpine && tasksSection) {
445 + const tasksAD = Alpine.$data(tasksSection);
446 + if (tasksAD) {
447 + let tasks = response.tasks || [];
448 +
449 + // Always update tasks to ensure state changes are reflected
450 + if (tasks.length > 0) {
451 + // Sort the tasks by creation time
452 + const sortedTasks = [...tasks].sort(
453 + (a, b) => (b.created_at || 0) - (a.created_at || 0),
454 + );
455 +
456 + // Assign the sorted tasks to the Alpine data
457 + tasksAD.tasks = sortedTasks;
458 + } else {
459 + // Make sure to use a new empty array instance
460 + tasksAD.tasks = [];
461 + }
462 + }
463 + }
464
465 // Make sure the active context is properly selected in both lists
466 if (context) {
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) {
467 + // Update selection in the active tab
468 + const activeTab = localStorage.getItem("activeTab") || "chats";
469 +
470 + if (activeTab === "chats" && chatsAD) {
471 + chatsAD.selected = context;
472 + localStorage.setItem("lastSelectedChat", context);
473 +
474 + // Check if this context exists in the chats list
475 + const contextExists = contexts.some((ctx) => ctx.id === context);
476 +
477 + // If it doesn't exist in the chats list but we're in chats tab, try to select the first chat
478 + // Only do this if there was already a context set (not on initial page load)
479 + if (!contextExists && contexts.length > 0 && context !== null) {
480 + const firstChatId = contexts[0].id;
481 setContext(firstChatId);
326 - chatsStore.setSelected(firstChatId);
482 + chatsAD.selected = firstChatId;
483 + localStorage.setItem("lastSelectedChat", firstChatId);
484 + }
485 + } else if (activeTab === "tasks" && tasksSection) {
486 + const tasksAD = Alpine.$data(tasksSection);
487 + tasksAD.selected = context;
488 + localStorage.setItem("lastSelectedTask", context);
489 +
490 + // Check if this context exists in the tasks list
491 + const taskExists = response.tasks?.some((task) => task.id === context);
492 +
493 + // If it doesn't exist in the tasks list but we're in tasks tab, try to select the first task
494 + // Only do this if there was already a context set (not on initial page load)
495 + if (!taskExists && response.tasks?.length > 0 && context !== null) {
496 + const firstTaskId = response.tasks[0].id;
497 + setContext(firstTaskId);
498 + tasksAD.selected = firstTaskId;
499 + localStorage.setItem("lastSelectedTask", firstTaskId);
500 }
501 }
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);
502 + } else if (
503 + response.tasks &&
504 + response.tasks.length > 0 &&
505 + localStorage.getItem("activeTab") === "tasks" &&
506 + localStorage.getItem("lastSelectedTask")
507 + ) {
508 + // Only auto-select tasks if we have a previously selected task ID
509 + const lastSelectedTask = localStorage.getItem("lastSelectedTask");
510 + const taskExists = response.tasks.some(
511 + (task) => task.id === lastSelectedTask,
512 + );
513 + if (taskExists) {
514 + setContext(lastSelectedTask);
515 + if (tasksSection) {
516 + const tasksAD = Alpine.$data(tasksSection);
517 + tasksAD.selected = lastSelectedTask;
518 }
519 }
520 + } else if (
521 + contexts.length > 0 &&
522 + localStorage.getItem("activeTab") === "chats" &&
523 + chatsAD &&
524 + localStorage.getItem("lastSelectedChat")
525 + ) {
526 + // Only auto-select chats if we have a previously selected chat ID
527 + const lastSelectedChat = localStorage.getItem("lastSelectedChat");
528 + const chatExists = contexts.some((ctx) => ctx.id === lastSelectedChat);
529 +
530 + // Don't auto-select - let user manually select or start from welcome screen
531 + if (chatExists && !context) {
532 + setContext(lastSelectedChat);
533 + chatsAD.selected = lastSelectedChat;
534 + }
535 }
536
537 lastLogVersion = response.log_version;
@@ -348,7 +543,6 @@ async function poll() {
543
544 return updated;
545 }
351 -globalThis.poll = poll;
546
547 function afterMessagesUpdate(logs) {
548 if (localStorage.getItem("speech") == "true") {
@@ -374,7 +568,7 @@ function speakMessages(logs) {
568 speechStore.speakStream(
569 getChatBasedId(log.no),
570 log.content,
377 - log.kvps?.finished
571 + log.kvps?.finished,
572 );
573 return;
574
@@ -394,46 +588,178 @@ function speakMessages(logs) {
588 }
589
590 function updateProgress(progress, active) {
397 - const progressBarEl = document.getElementById("progress-bar");
398 - if (!progressBarEl) return;
591 if (!progress) progress = "";
592
593 if (!active) {
402 - removeClassFromElement(progressBarEl, "shiny-text");
594 + removeClassFromElement(progressBar, "shiny-text");
595 } else {
404 - addClassToElement(progressBarEl, "shiny-text");
596 + addClassToElement(progressBar, "shiny-text");
597 }
598
599 progress = msgs.convertIcons(progress);
600
409 - if (progressBarEl.innerHTML != progress) {
410 - progressBarEl.innerHTML = progress;
601 + if (progressBar.innerHTML != progress) {
602 + progressBar.innerHTML = progress;
603 }
604 }
605
606 globalThis.pauseAgent = async function (paused) {
415 - await inputStore.pauseAgent(paused);
607 + try {
608 + await sendJsonData("/pause", { paused: paused, context });
609 + } catch (e) {
610 + globalThis.toastFetchError("Error pausing agent", e);
611 + }
612 };
613
614 globalThis.resetChat = async function (ctxid = null) {
419 - await chatsStore.resetChat(ctxid);
615 + try {
616 + await sendJsonData("/chat_reset", {
617 + context: ctxid === null ? context : ctxid,
618 + });
619 + resetCounter++;
620 + if (ctxid === null) updateAfterScroll();
621 + } catch (e) {
622 + globalThis.toastFetchError("Error resetting chat", e);
623 + }
624 };
625
626 globalThis.newChat = async function () {
423 - await chatsStore.newChat();
627 + try {
628 + newContext();
629 + updateAfterScroll();
630 + } catch (e) {
631 + globalThis.toastFetchError("Error creating new chat", e);
632 + }
633 };
634
635 globalThis.killChat = async function (id) {
427 - await chatsStore.killChat(id);
636 + if (!id) {
637 + console.error("No chat ID provided for deletion");
638 + return;
639 + }
640 +
641 + try {
642 + const chatsAD = Alpine.$data(chatsSection);
643 +
644 + // switch to another context if deleting current
645 + switchFromContext(id);
646 +
647 + // Delete the chat on the server
648 + await sendJsonData("/chat_remove", { context: id });
649 +
650 + // Update the UI manually to ensure the correct chat is removed
651 + // Deep clone the contexts array to prevent reference issues
652 + const updatedContexts = chatsAD.contexts.filter((ctx) => ctx.id !== id);
653 +
654 + // Force UI update by creating a new array
655 + chatsAD.contexts = [...updatedContexts];
656 +
657 + updateAfterScroll();
658 +
659 + justToast("Chat deleted successfully", "success", 1000, "chat-removal");
660 + } catch (e) {
661 + console.error("Error deleting chat:", e);
662 + globalThis.toastFetchError("Error deleting chat", e);
663 + }
664 };
665
666 +export function switchFromContext(id) {
667 + // If we're deleting the currently selected chat, switch to another one first
668 + if (context === id) {
669 + const chatsAD = Alpine.$data(chatsSection);
670 +
671 + // Find an alternate chat to switch to if we're deleting the current one
672 + let alternateChat = null;
673 + for (let i = 0; i < chatsAD.contexts.length; i++) {
674 + if (chatsAD.contexts[i].id !== id) {
675 + alternateChat = chatsAD.contexts[i];
676 + break;
677 + }
678 + }
679 +
680 + if (alternateChat) {
681 + setContext(alternateChat.id);
682 + } else {
683 + // If no other chats, show welcome screen instead of creating new context
684 + deselectChat();
685 + }
686 + }
687 +}
688 +
689 +// Function to ensure proper UI state when switching contexts
690 +function ensureProperTabSelection(contextId) {
691 + // Get current active tab
692 + const activeTab = localStorage.getItem("activeTab") || "chats";
693 +
694 + // First attempt to determine if this is a task or chat based on the task list
695 + const tasksSection = document.getElementById("tasks-section");
696 + let isTask = false;
697 +
698 + if (tasksSection) {
699 + const tasksAD = Alpine.$data(tasksSection);
700 + if (tasksAD && tasksAD.tasks) {
701 + isTask = tasksAD.tasks.some((task) => task.id === contextId);
702 + }
703 + }
704 +
705 + // If we're selecting a task but are in the chats tab, switch to tasks tab
706 + if (isTask && activeTab === "chats") {
707 + // Store this as the last selected task before switching
708 + localStorage.setItem("lastSelectedTask", contextId);
709 + activateTab("tasks");
710 + return true;
711 + }
712 +
713 + // If we're selecting a chat but are in the tasks tab, switch to chats tab
714 + if (!isTask && activeTab === "tasks") {
715 + // Store this as the last selected chat before switching
716 + localStorage.setItem("lastSelectedChat", contextId);
717 + activateTab("chats");
718 + return true;
719 + }
720 +
721 + return false;
722 +}
723 +
724 globalThis.selectChat = async function (id) {
431 - await chatsStore.selectChat(id);
725 + if (id === context) return; //already selected
726 +
727 + // Check if we need to switch tabs based on the context type
728 + const tabSwitched = ensureProperTabSelection(id);
729 +
730 + // If we didn't switch tabs, proceed with normal selection
731 + if (!tabSwitched) {
732 + // Switch to the new context - this will clear chat history and reset tracking variables
733 + setContext(id);
734 +
735 + // Update both contexts and tasks lists to reflect the selected item
736 + const chatsAD = Alpine.$data(chatsSection);
737 + const tasksSection = document.getElementById("tasks-section");
738 + if (tasksSection) {
739 + const tasksAD = Alpine.$data(tasksSection);
740 + tasksAD.selected = id;
741 + }
742 + chatsAD.selected = id;
743 +
744 + // Store this selection in the appropriate localStorage key
745 + const activeTab = localStorage.getItem("activeTab") || "chats";
746 + if (activeTab === "chats") {
747 + localStorage.setItem("lastSelectedChat", id);
748 + } else if (activeTab === "tasks") {
749 + localStorage.setItem("lastSelectedTask", id);
750 + }
751 +
752 + // Trigger an immediate poll to fetch content
753 + poll();
754 + }
755 +
756 + updateAfterScroll();
757 };
758
759 function generateShortId() {
435 - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
436 - let result = '';
760 + const chars =
761 + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
762 + let result = "";
763 for (let i = 0; i < 8; i++) {
764 result += chars.charAt(Math.floor(Math.random() * chars.length));
765 }
@@ -443,28 +769,39 @@ function generateShortId() {
769 export const newContext = function () {
770 context = generateShortId();
771 setContext(context);
446 -}
447 -globalThis.newContext = newContext;
772 +};
773
774 export const setContext = function (id) {
775 if (id == context) return;
776 context = id;
777 + // Update the reactive stores
778 + contextStore.setContext(id);
779 + contextStore.context = id;
780 // Always reset the log tracking variables when switching contexts
781 // This ensures we get fresh data from the backend
782 lastLogGuid = "";
783 lastLogVersion = 0;
456 - lastSpokenNo = 0;
784
785 // Stop speech when switching chats
786 speechStore.stopAudio();
787
461 - // Clear the chat history immediately to avoid showing stale content
462 - const chatHistoryEl = document.getElementById("chat-history");
463 - if (chatHistoryEl) chatHistoryEl.innerHTML = "";
788 + if (id) {
789 + chatHistory.innerHTML = "";
790 + }
791
465 - // Update both selected states using stores
466 - chatsStore.setSelected(id);
467 - tasksStore.setSelected(id);
792 + // Update both selected states
793 + if (globalThis.Alpine) {
794 + if (chatsSection) {
795 + const chatsAD = Alpine.$data(chatsSection);
796 + if (chatsAD) chatsAD.selected = id || "";
797 + }
798 + if (tasksSection) {
799 + const tasksAD = Alpine.$data(tasksSection);
800 + if (tasksAD) tasksAD.selected = id || "";
801 + }
802 +
803 + // Alpine store reactivity will handle the UI updates automatically
804 + }
805
806 //skip one speech if enabled when switching context
807 if (localStorage.getItem("speech") == "true") skipOneSpeech = true;
@@ -472,12 +809,23 @@ export const setContext = function (id) {
809
810 export const getContext = function () {
811 return context;
475 -}
812 +};
813 globalThis.getContext = getContext;
477 -globalThis.setContext = setContext;
814 +
815 +export const deselectChat = function () {
816 + // Clear current context to show welcome screen
817 + setContext(null);
818 +
819 + // Clear localStorage selections so we don't auto-restore
820 + localStorage.removeItem("lastSelectedChat");
821 + localStorage.removeItem("lastSelectedTask");
822 +
823 + // Clear the chat history
824 + chatHistory.innerHTML = "";
825 +};
826
827 export const getChatBasedId = function (id) {
480 - return context + "-" + globalThis.resetCounter + "-" + id;
828 + return context + "-" + resetCounter + "-" + id;
829 };
830
831 globalThis.toggleAutoScroll = async function (_autoScroll) {
@@ -492,7 +840,7 @@ globalThis.toggleThoughts = async function (showThoughts) {
840 css.toggleCssProperty(
841 ".msg-thoughts",
842 "display",
495 - showThoughts ? undefined : "none"
843 + showThoughts ? undefined : "none",
844 );
845 };
846
@@ -500,7 +848,7 @@ globalThis.toggleUtils = async function (showUtils) {
848 css.toggleCssProperty(
849 ".message-util",
850 "display",
503 - showUtils ? undefined : "none"
851 + showUtils ? undefined : "none",
852 );
853 };
854
@@ -512,7 +860,6 @@ globalThis.toggleDarkMode = function (isDark) {
860 document.body.classList.remove("dark-mode");
861 document.body.classList.add("light-mode");
862 }
515 - console.log("Dark mode:", isDark);
863 localStorage.setItem("darkMode", isDark);
864 };
865
@@ -523,11 +870,53 @@ globalThis.toggleSpeech = function (isOn) {
870 };
871
872 globalThis.nudge = async function () {
526 - await inputStore.nudge();
873 + try {
874 + await sendJsonData("/nudge", { ctxid: getContext() });
875 + } catch (e) {
876 + toastFetchError("Error nudging agent", e);
877 + }
878 };
879
880 globalThis.restart = async function () {
530 - await chatsStore.restart();
881 + try {
882 + if (!getConnectionStatus()) {
883 + await toastFrontendError(
884 + "Backend disconnected, cannot restart.",
885 + "Restart Error",
886 + );
887 + return;
888 + }
889 + // First try to initiate restart
890 + await sendJsonData("/restart", {});
891 + } catch (e) {
892 + // Show restarting message with no timeout and restart group
893 + await toastFrontendInfo("Restarting...", "System Restart", 9999, "restart");
894 +
895 + let retries = 0;
896 + const maxRetries = 240; // Maximum number of retries (60 seconds with 250ms interval)
897 +
898 + while (retries < maxRetries) {
899 + try {
900 + await sendJsonData("/health", {});
901 + // Server is back up, show success message that replaces the restarting message
902 + await new Promise((resolve) => setTimeout(resolve, 250));
903 + await toastFrontendSuccess("Restarted", "System Restart", 5, "restart");
904 + return;
905 + } catch (e) {
906 + // Server still down, keep waiting
907 + retries++;
908 + await new Promise((resolve) => setTimeout(resolve, 250));
909 + }
910 + }
911 +
912 + // If we get here, restart failed or took too long
913 + await toastFrontendError(
914 + "Restart timed out or failed",
915 + "Restart Error",
916 + 8,
917 + "restart",
918 + );
919 + }
920 };
921
922 // Modify this part
@@ -537,13 +926,113 @@ document.addEventListener("DOMContentLoaded", () => {
926 });
927
928 globalThis.loadChats = async function () {
540 - await chatsStore.loadChats();
929 + try {
930 + const fileContents = await readJsonFiles();
931 + const response = await sendJsonData("/chat_load", { chats: fileContents });
932 +
933 + if (!response) {
934 + toast("No response returned.", "error");
935 + }
936 + // else if (!response.ok) {
937 + // if (response.message) {
938 + // toast(response.message, "error")
939 + // } else {
940 + // toast("Undefined error.", "error")
941 + // }
942 + // }
943 + else {
944 + setContext(response.ctxids[0]);
945 + toast("Chats loaded.", "success");
946 + }
947 + } catch (e) {
948 + toastFetchError("Error loading chats", e);
949 + }
950 };
951
952 globalThis.saveChat = async function () {
544 - await chatsStore.saveChat();
953 + try {
954 + const response = await sendJsonData("/chat_export", { ctxid: context });
955 +
956 + if (!response) {
957 + toast("No response returned.", "error");
958 + }
959 + // else if (!response.ok) {
960 + // if (response.message) {
961 + // toast(response.message, "error")
962 + // } else {
963 + // toast("Undefined error.", "error")
964 + // }
965 + // }
966 + else {
967 + downloadFile(response.ctxid + ".json", response.content);
968 + toast("Chat file downloaded.", "success");
969 + }
970 + } catch (e) {
971 + toastFetchError("Error saving chat", e);
972 + }
973 };
974
975 +function downloadFile(filename, content) {
976 + // Create a Blob with the content to save
977 + const blob = new Blob([content], { type: "application/json" });
978 +
979 + // Create a link element
980 + const link = document.createElement("a");
981 +
982 + // Create a URL for the Blob
983 + const url = URL.createObjectURL(blob);
984 + link.href = url;
985 +
986 + // Set the file name for download
987 + link.download = filename;
988 +
989 + // Programmatically click the link to trigger the download
990 + link.click();
991 +
992 + // Clean up by revoking the object URL
993 + setTimeout(() => {
994 + URL.revokeObjectURL(url);
995 + }, 0);
996 +}
997 +
998 +function readJsonFiles() {
999 + return new Promise((resolve, reject) => {
1000 + // Create an input element of type 'file'
1001 + const input = document.createElement("input");
1002 + input.type = "file";
1003 + input.accept = ".json"; // Only accept JSON files
1004 + input.multiple = true; // Allow multiple file selection
1005 +
1006 + // Trigger the file dialog
1007 + input.click();
1008 +
1009 + // When files are selected
1010 + input.onchange = async () => {
1011 + const files = input.files;
1012 + if (!files.length) {
1013 + resolve([]); // Return an empty array if no files are selected
1014 + return;
1015 + }
1016 +
1017 + // Read each file as a string and store in an array
1018 + const filePromises = Array.from(files).map((file) => {
1019 + return new Promise((fileResolve, fileReject) => {
1020 + const reader = new FileReader();
1021 + reader.onload = () => fileResolve(reader.result);
1022 + reader.onerror = fileReject;
1023 + reader.readAsText(file);
1024 + });
1025 + });
1026 +
1027 + try {
1028 + const fileContents = await Promise.all(filePromises);
1029 + resolve(fileContents);
1030 + } catch (error) {
1031 + reject(error); // In case of any file reading error
1032 + }
1033 + };
1034 + });
1035 +}
1036
1037 function addClassToElement(element, className) {
1038 element.classList.add(className);
@@ -554,42 +1043,38 @@ function removeClassFromElement(element, className) {
1043 }
1044
1045 function justToast(text, type = "info", timeout = 5000, group = "") {
557 - notificationStore.addFrontendToastOnly(
558 - type,
559 - text,
560 - "",
561 - timeout / 1000,
562 - group
563 - )
1046 + notificationStore.addFrontendToastOnly(type, text, "", timeout / 1000, group);
1047 }
565 -globalThis.justToast = justToast;
566 -
1048
1049 function toast(text, type = "info", timeout = 5000) {
1050 // Convert timeout from milliseconds to seconds for new notification system
1051 const display_time = Math.max(timeout / 1000, 1); // Minimum 1 second
1052
1053 // Use new frontend notification system based on type
573 - switch (type.toLowerCase()) {
574 - case "error":
575 - return notificationStore.frontendError(text, "Error", display_time);
576 - case "success":
577 - return notificationStore.frontendInfo(text, "Success", display_time);
578 - case "warning":
579 - return notificationStore.frontendWarning(text, "Warning", display_time);
580 - case "info":
581 - default:
582 - return notificationStore.frontendInfo(text, "Info", display_time);
583 - }
584 -
1054 + switch (type.toLowerCase()) {
1055 + case "error":
1056 + return notificationStore.frontendError(text, "Error", display_time);
1057 + case "success":
1058 + return notificationStore.frontendInfo(text, "Success", display_time);
1059 + case "warning":
1060 + return notificationStore.frontendWarning(text, "Warning", display_time);
1061 + case "info":
1062 + default:
1063 + return notificationStore.frontendInfo(text, "Info", display_time);
1064 + }
1065 }
1066 globalThis.toast = toast;
1067
1068 // OLD: hideToast function removed - now using new notification system
1069
1070 function scrollChanged(isAtBottom) {
591 - // Reflect scroll state into preferences store; UI is bound via x-model
592 - preferencesStore.autoScroll = isAtBottom;
1071 + if (globalThis.Alpine && autoScrollSwitch) {
1072 + const inputAS = Alpine.$data(autoScrollSwitch);
1073 + if (inputAS) {
1074 + inputAS.autoScroll = isAtBottom;
1075 + }
1076 + }
1077 + // autoScrollSwitch.checked = isAtBottom
1078 }
1079
1080 function updateAfterScroll() {
@@ -597,15 +1082,16 @@ function updateAfterScroll() {
1082 // const tolerancePx = toleranceEm * parseFloat(getComputedStyle(document.documentElement).fontSize); // Convert em to pixels
1083 const tolerancePx = 10;
1084 const chatHistory = document.getElementById("chat-history");
600 - if (!chatHistory) return;
601 -
1085 const isAtBottom =
1086 chatHistory.scrollHeight - chatHistory.scrollTop <=
1087 chatHistory.clientHeight + tolerancePx;
1088
1089 scrollChanged(isAtBottom);
1090 }
608 -globalThis.updateAfterScroll = updateAfterScroll;
1091 +
1092 +chatHistory.addEventListener("scroll", updateAfterScroll);
1093 +
1094 +chatInput.addEventListener("input", adjustTextareaHeight);
1095
1096 // setInterval(poll, 250);
1097
@@ -615,6 +1101,15 @@ async function startPolling() {
1101 const shortIntervalPeriod = 100;
1102 let shortIntervalCount = 0;
1103
1104 + // Call poll immediately to load chats on page load
1105 + try {
1106 + // Wait a bit for Alpine to be fully ready
1107 + await new Promise((resolve) => setTimeout(resolve, 50));
1108 + await poll();
1109 + } catch (error) {
1110 + console.error("Error in initial poll:", error);
1111 + }
1112 +
1113 async function _doPoll() {
1114 let nextInterval = longInterval;
1115
@@ -631,41 +1126,165 @@ async function startPolling() {
1126 setTimeout(_doPoll.bind(this), nextInterval);
1127 }
1128
634 - _doPoll();
1129 + // Start the polling loop after the initial poll
1130 + setTimeout(_doPoll, longInterval);
1131 }
1132
637 -// All initializations and event listeners are now consolidated here
1133 +document.addEventListener("DOMContentLoaded", startPolling);
1134 +
1135 +// Setup event handlers once the DOM is fully loaded
1136 document.addEventListener("DOMContentLoaded", function () {
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 -
658 - // Start polling for updates
659 - startPolling();
1137 + setupSidebarToggle();
1138 + setupTabs();
1139 + initializeActiveTab();
1140 });
1141
1142 +// Setup tabs functionality
1143 +function setupTabs() {
1144 + const chatsTab = document.getElementById("chats-tab");
1145 + const tasksTab = document.getElementById("tasks-tab");
1146 +
1147 + if (chatsTab && tasksTab) {
1148 + chatsTab.addEventListener("click", function () {
1149 + activateTab("chats");
1150 + });
1151 +
1152 + tasksTab.addEventListener("click", function () {
1153 + activateTab("tasks");
1154 + });
1155 + } else {
1156 + console.error("Tab elements not found");
1157 + setTimeout(setupTabs, 100); // Retry setup
1158 + }
1159 +}
1160 +
1161 +function activateTab(tabName) {
1162 + const chatsTab = document.getElementById("chats-tab");
1163 + const tasksTab = document.getElementById("tasks-tab");
1164 + const chatsSection = document.getElementById("chats-section");
1165 + const tasksSection = document.getElementById("tasks-section");
1166 +
1167 + // Get current context to preserve before switching
1168 + const currentContext = context;
1169 +
1170 + // Store the current selection for the active tab before switching
1171 + const previousTab = localStorage.getItem("activeTab");
1172 + if (previousTab === "chats") {
1173 + localStorage.setItem("lastSelectedChat", currentContext);
1174 + } else if (previousTab === "tasks") {
1175 + localStorage.setItem("lastSelectedTask", currentContext);
1176 + }
1177 +
1178 + // Reset all tabs and sections
1179 + chatsTab.classList.remove("active");
1180 + tasksTab.classList.remove("active");
1181 + chatsSection.style.display = "none";
1182 + tasksSection.style.display = "none";
1183 +
1184 + // Remember the last active tab in localStorage
1185 + localStorage.setItem("activeTab", tabName);
1186 +
1187 + // Activate selected tab and section
1188 + if (tabName === "chats") {
1189 + chatsTab.classList.add("active");
1190 + chatsSection.style.display = "";
1191 +
1192 + // Get the available contexts from Alpine.js data
1193 + const chatsAD = globalThis.Alpine ? Alpine.$data(chatsSection) : null;
1194 + const availableContexts = chatsAD?.contexts || [];
1195 +
1196 + // Restore previous chat selection
1197 + const lastSelectedChat = localStorage.getItem("lastSelectedChat");
1198 +
1199 + // Only switch if:
1200 + // 1. lastSelectedChat exists AND
1201 + // 2. It's different from current context AND
1202 + // 3. The context actually exists in our contexts list OR there are no contexts yet
1203 + if (
1204 + lastSelectedChat &&
1205 + lastSelectedChat !== currentContext &&
1206 + (availableContexts.some((ctx) => ctx.id === lastSelectedChat) ||
1207 + availableContexts.length === 0)
1208 + ) {
1209 + setContext(lastSelectedChat);
1210 + }
1211 + } else if (tabName === "tasks") {
1212 + tasksTab.classList.add("active");
1213 + tasksSection.style.display = "flex";
1214 + tasksSection.style.flexDirection = "column";
1215 +
1216 + // Get the available tasks from Alpine.js data
1217 + const tasksAD = globalThis.Alpine ? Alpine.$data(tasksSection) : null;
1218 + const availableTasks = tasksAD?.tasks || [];
1219 +
1220 + // Restore previous task selection
1221 + const lastSelectedTask = localStorage.getItem("lastSelectedTask");
1222 +
1223 + // Only switch if:
1224 + // 1. lastSelectedTask exists AND
1225 + // 2. It's different from current context AND
1226 + // 3. The task actually exists in our tasks list
1227 + if (
1228 + lastSelectedTask &&
1229 + lastSelectedTask !== currentContext &&
1230 + availableTasks.some((task) => task.id === lastSelectedTask)
1231 + ) {
1232 + setContext(lastSelectedTask);
1233 + }
1234 + }
1235 +
1236 + // Request a poll update
1237 + poll();
1238 +}
1239 +
1240 +// Add function to initialize active tab and selections from localStorage
1241 +function initializeActiveTab() {
1242 + // Initialize selection storage if not present
1243 + if (!localStorage.getItem("lastSelectedChat")) {
1244 + localStorage.setItem("lastSelectedChat", "");
1245 + }
1246 + if (!localStorage.getItem("lastSelectedTask")) {
1247 + localStorage.setItem("lastSelectedTask", "");
1248 + }
1249 +
1250 + const activeTab = localStorage.getItem("activeTab") || "chats";
1251 +
1252 + // Only activate tab if we have a context - otherwise show welcome screen
1253 + if (context) {
1254 + activateTab(activeTab);
1255 + } else {
1256 + // Just set the active tab UI without auto-selecting contexts
1257 + const chatsTab = document.getElementById("chats-tab");
1258 + const tasksTab = document.getElementById("tasks-tab");
1259 + const chatsSection = document.getElementById("chats-section");
1260 + const tasksSection = document.getElementById("tasks-section");
1261 +
1262 + if (activeTab === "chats") {
1263 + if (chatsTab) chatsTab.classList.add("active");
1264 + if (tasksTab) tasksTab.classList.remove("active");
1265 + if (chatsSection) chatsSection.style.display = "flex";
1266 + if (tasksSection) tasksSection.style.display = "none";
1267 + } else {
1268 + if (tasksTab) tasksTab.classList.add("active");
1269 + if (chatsTab) chatsTab.classList.remove("active");
1270 + if (tasksSection) tasksSection.style.display = "flex";
1271 + if (chatsSection) chatsSection.style.display = "none";
1272 + }
1273 + }
1274 +
1275 + // Alpine store will handle the initial state automatically
1276 +}
1277 +
1278 /*
1279 * A0 Chat UI
1280 *
665 - * Unified sidebar layout:
666 - * - Both Chats and Tasks lists are always visible in a vertical layout
1281 + * Tasks tab functionality:
1282 + * - Tasks are displayed in the Tasks tab with the same mechanics as chats
1283 * - Both lists are sorted by creation time (newest first)
1284 + * - Selection state is preserved across tab switches
1285 + * - The active tab is remembered across sessions
1286 * - Tasks use the same context system as chats for communication with the backend
1287 + * - Future support for renaming and deletion will be implemented later
1288 */
1289
1290 // Open the scheduler detail view for a specific task
@@ -697,7 +1316,7 @@ function openTaskDetail(taskId) {
1316 setTimeout(() => {
1317 // Get the scheduler component
1318 const schedulerComponent = document.querySelector(
700 - '[x-data="schedulerSettings"]'
1319 + '[x-data="schedulerSettings"]',
1320 );
1321 if (!schedulerComponent) {
1322 console.error("Scheduler component not found");
@@ -725,3 +1344,4 @@ function openTaskDetail(taskId) {
1344
1345 // Make the function available globally
1346 globalThis.openTaskDetail = openTaskDetail;
1347 +globalThis.deselectChat = deselectChat;