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()">×</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;