feat: TaskScheduler - frontend tabbed display chats tasks
Rafael Uzarowski committed
Mar 22, 2025 at 14:30 UTC
525c1c13f8516f3b210330d5f68fe472e3bd8c95
8 files changed
+966
-117
python/api/poll.py
+35
-12
@@ -3,6 +3,9 @@ from flask import Request, Response
3
4
from agent import AgentContext
5
6
+from python.helpers import persist_chat
7
+
8
+
9
class Poll(ApiHandler):
10
async def process(self, input: dict, request: Request) -> dict | Response:
11
ctxid = input.get("context", None)
@@ -15,27 +18,47 @@ class Poll(ApiHandler):
18
19
# loop AgentContext._contexts
20
ctxs = []
21
+ tasks = []
22
+ processed_contexts = set() # Track processed context IDs
23
+
24
+ # First, identify all tasks
25
for ctx in AgentContext._contexts.values():
19
- ctxs.append(
20
- {
21
- "id": ctx.id,
22
- "name": ctx.name,
23
- "no": ctx.no,
24
- "log_guid": ctx.log.guid,
25
- "log_version": len(ctx.log.updates),
26
- "log_length": len(ctx.log.logs),
27
- "paused": ctx.paused,
28
- }
29
- )
26
+ # Skip if already processed
27
+ if ctx.id in processed_contexts:
28
+ continue
29
+
30
+ context_data = {
31
+ "id": ctx.id,
32
+ "name": ctx.name,
33
+ "no": ctx.no,
34
+ "log_guid": ctx.log.guid,
35
+ "log_version": len(ctx.log.updates),
36
+ "log_length": len(ctx.log.logs),
37
+ "paused": ctx.paused,
38
+ }
39
+
40
+ # Determine if this is a task using multiple methods
41
+ ctx_path = persist_chat.get_chat_folder_path(ctx.id)
42
+ is_task = (ctx_path and persist_chat.TASKS_FOLDER in ctx_path)
43
+
44
+ # Add to the appropriate list
45
+ if is_task:
46
+ tasks.append(context_data)
47
+ else:
48
+ ctxs.append(context_data)
49
+
50
+ # Mark as processed
51
+ processed_contexts.add(ctx.id)
52
53
# data from this server
54
return {
55
"context": context.id,
56
"contexts": ctxs,
57
+ "tasks": tasks,
58
"logs": logs,
59
"log_guid": context.log.guid,
60
"log_version": len(context.log.updates),
61
"log_progress": context.log.progress,
62
"log_progress_active": context.log.progress_active,
63
"paused": context.paused,
41
- }
\ No newline at end of file
64
+ }
python/helpers/chat_names.py
new
+137
@@ -0,0 +1,137 @@
1
+import json
2
+import os
3
+import time
4
+from typing import Dict, Any
5
+
6
+from python.helpers import files
7
+
8
+
9
+class ChatNames:
10
+ _instance = None
11
+ _names_file = "chat_names.json"
12
+ _metadata_file = "chat_metadata.json"
13
+
14
+ @classmethod
15
+ def get_instance(cls):
16
+ if cls._instance is None:
17
+ cls._instance = ChatNames()
18
+ cls._instance._names_file = files.get_abs_path("tmp") + "/" + cls._names_file
19
+ cls._instance._metadata_file = files.get_abs_path("tmp") + "/" + cls._metadata_file
20
+ return cls._instance
21
+
22
+ def _read_names(self) -> dict:
23
+ """Read current names from file"""
24
+ try:
25
+ if os.path.exists(self._names_file):
26
+ with open(self._names_file, 'r') as f:
27
+ return json.load(f)
28
+ except Exception as e:
29
+ print(f"Error reading chat names: {e}")
30
+ return {}
31
+
32
+ def _save_names(self, names: dict):
33
+ """Save names to file"""
34
+ try:
35
+ with open(self._names_file, 'w') as f:
36
+ json.dump(names, f, indent=2)
37
+ except Exception as e:
38
+ print(f"Error saving chat names: {e}")
39
+
40
+ def _read_metadata(self) -> dict:
41
+ """Read context metadata from file"""
42
+ try:
43
+ if os.path.exists(self._metadata_file):
44
+ with open(self._metadata_file, 'r') as f:
45
+ return json.load(f)
46
+ except Exception as e:
47
+ print(f"Error reading context metadata: {e}")
48
+ return {}
49
+
50
+ def _save_metadata(self, metadata: dict):
51
+ """Save metadata to file"""
52
+ try:
53
+ with open(self._metadata_file, 'w') as f:
54
+ json.dump(metadata, f, indent=2)
55
+ except Exception as e:
56
+ print(f"Error saving context metadata: {e}")
57
+
58
+ def set_name(self, chat_id: str, name: str):
59
+ """Set name for a chat/task"""
60
+ names = self._read_names()
61
+ names[chat_id] = name
62
+ self._save_names(names)
63
+
64
+ # Also update the name in metadata if it exists
65
+ self.update_metadata(chat_id, {"name": name})
66
+
67
+ def get_name(self, chat_id: str) -> str:
68
+ """Get name for a chat/task"""
69
+ names = self._read_names()
70
+ return names.get(chat_id, f"Chat #{chat_id[:8]}")
71
+
72
+ def set_metadata(self, chat_id: str, metadata_dict: Dict[str, Any]):
73
+ """Set complete metadata for a chat/task"""
74
+ all_metadata = self._read_metadata()
75
+
76
+ # Ensure we have created_at timestamp if not provided
77
+ if "created_at" not in metadata_dict:
78
+ metadata_dict["created_at"] = int(time.time())
79
+
80
+ all_metadata[chat_id] = metadata_dict
81
+ self._save_metadata(all_metadata)
82
+
83
+ # Also update the name in the names file for backward compatibility
84
+ if "name" in metadata_dict:
85
+ self.set_name(chat_id, metadata_dict["name"])
86
+
87
+ def update_metadata(self, chat_id: str, metadata_update: Dict[str, Any]):
88
+ """Update specific metadata fields for a chat/task"""
89
+ all_metadata = self._read_metadata()
90
+
91
+ if chat_id in all_metadata:
92
+ all_metadata[chat_id].update(metadata_update)
93
+ else:
94
+ # Initialize with current timestamp if creating new metadata
95
+ metadata_update["created_at"] = metadata_update.get("created_at", int(time.time()))
96
+ all_metadata[chat_id] = metadata_update
97
+
98
+ self._save_metadata(all_metadata)
99
+
100
+ def get_metadata(self, chat_id: str) -> Dict[str, Any]:
101
+ """Get metadata for a chat/task"""
102
+ all_metadata = self._read_metadata()
103
+
104
+ # Return metadata if it exists
105
+ if chat_id in all_metadata:
106
+ return all_metadata[chat_id]
107
+
108
+ # Otherwise create default metadata with just the name
109
+ name = self.get_name(chat_id)
110
+ default_metadata = {
111
+ "name": name,
112
+ "created_at": int(time.time()) # Current time as fallback
113
+ }
114
+ return default_metadata
115
+
116
+ def get_created_at(self, chat_id: str) -> int:
117
+ """Get creation timestamp for a chat/task"""
118
+ metadata = self.get_metadata(chat_id)
119
+ return metadata.get("created_at", 0)
120
+
121
+ def remove_chat(self, chat_id: str):
122
+ """Remove chat/task and its metadata"""
123
+ print("Removing chat name and metadata: ", chat_id)
124
+
125
+ # Remove from names file
126
+ names = self._read_names()
127
+ if chat_id in names:
128
+ del names[chat_id]
129
+ print("Chat name removed: ", chat_id)
130
+ self._save_names(names)
131
+
132
+ # Remove from metadata file
133
+ metadata = self._read_metadata()
134
+ if chat_id in metadata:
135
+ del metadata[chat_id]
136
+ print("Chat metadata removed: ", chat_id)
137
+ self._save_metadata(metadata)
python/helpers/persist_chat.py
+33
-15
@@ -9,27 +9,30 @@ from initialize import initialize
9
from python.helpers.log import Log, LogItem
10
11
CHATS_FOLDER = "tmp/chats"
12
+TASKS_FOLDER = "tmp/task_chats"
13
LOG_SIZE = 1000
14
CHAT_FILE_NAME = "chat.json"
15
16
16
-def get_chat_folder_path(ctxid: str):
17
- return files.get_abs_path(CHATS_FOLDER, ctxid)
17
+def get_chat_folder_path(ctxid: str, folder: str = CHATS_FOLDER):
18
+ return files.get_abs_path(folder, ctxid)
19
19
-def save_tmp_chat(context: AgentContext):
20
- path = _get_chat_file_path(context.id)
20
+
21
+def save_tmp_chat(context: AgentContext, folder: str = CHATS_FOLDER):
22
+ path = _get_chat_file_path(context.id, folder)
23
files.make_dirs(path)
24
data = _serialize_context(context)
25
js = _safe_json_serialize(data, ensure_ascii=False)
26
files.write_file(path, js)
27
28
27
-def load_tmp_chats():
28
- _convert_v080_chats()
29
- folders = files.list_files(CHATS_FOLDER, "*")
29
+def load_tmp_chats(folder: str = CHATS_FOLDER):
30
+ if folder == CHATS_FOLDER:
31
+ _convert_v080_chats()
32
+ folders = files.list_files(folder, "*")
33
json_files = []
31
- for folder in folders:
32
- json_files.append(_get_chat_file_path(folder))
34
+ for folder_name in folders:
35
+ json_files.append(_get_chat_file_path(folder_name, folder))
36
37
ctxids = []
38
for file in json_files:
@@ -43,8 +46,8 @@ def load_tmp_chats():
46
return ctxids
47
48
46
-def _get_chat_file_path(ctxid: str):
47
- return files.get_abs_path(CHATS_FOLDER, ctxid, CHAT_FILE_NAME)
49
+def _get_chat_file_path(ctxid: str, folder: str = CHATS_FOLDER):
50
+ return files.get_abs_path(folder, ctxid, CHAT_FILE_NAME)
51
52
53
def _convert_v080_chats():
@@ -52,12 +55,11 @@ def _convert_v080_chats():
55
for file in json_files:
56
path = files.get_abs_path(CHATS_FOLDER, file)
57
name = file.rstrip(".json")
55
- fold = files.get_abs_path(CHATS_FOLDER, name)
58
new = _get_chat_file_path(name)
59
files.move_file(path, new)
60
61
60
-def load_json_chats(jsons: list[str]):
62
+def load_json_chats(jsons: list[str], folder: str = CHATS_FOLDER):
63
ctxids = []
64
for js in jsons:
65
data = json.loads(js)
@@ -74,9 +76,25 @@ def export_json_chat(context: AgentContext):
76
return js
77
78
77
-def remove_chat(ctxid):
78
- files.delete_dir(get_chat_folder_path(ctxid))
79
+def remove_chat(ctxid, folder: str = CHATS_FOLDER):
80
+ files.delete_dir(get_chat_folder_path(ctxid, folder))
81
+
82
+
83
+# Task-specific functions for convenience
84
+def save_tmp_task(context: AgentContext):
85
+ save_tmp_chat(context, TASKS_FOLDER)
86
+
87
+
88
+def load_tmp_tasks():
89
+ return load_tmp_chats(TASKS_FOLDER)
90
+
91
+
92
+def load_json_tasks(jsons: list[str]):
93
+ return load_json_chats(jsons, TASKS_FOLDER)
94
+
95
96
+def remove_task(ctxid):
97
+ remove_chat(ctxid, TASKS_FOLDER)
98
99
100
def _serialize_context(context: AgentContext):
python/helpers/task_scheduler.py
+34
-24
@@ -14,14 +14,13 @@ from pydantic import BaseModel, Field, PrivateAttr
14
from python.helpers.files import get_abs_path, exists, write_file, read_file, make_dirs
15
from agent import Agent, AgentContext, UserMessage
16
from initialize import initialize
17
-from python.helpers.persist_chat import export_json_chat, load_json_chats
17
+from python.helpers.persist_chat import export_json_chat, load_json_chats, load_tmp_chats, save_tmp_chat
18
from python.helpers.print_style import PrintStyle
19
from python.helpers.defer import DeferredTask
20
-from python.helpers.persist_chat import CHATS_FOLDER
20
+from python.helpers.persist_chat import CHATS_FOLDER, TASKS_FOLDER
21
from python.helpers import errors
22
23
SCHEDULER_FOLDER = "memory/scheduler"
24
-TASKS_FOLDER = CHATS_FOLDER
24
25
26
class TaskSchedule(BaseModel):
@@ -37,7 +36,7 @@ class TaskSchedule(BaseModel):
36
37
class AdHocTask(BaseModel):
38
uuid: str = Field(default_factory=lambda: str(uuid.uuid4()))
40
- state: Literal["idle", "running"] = Field(default="idle")
39
+ state: Literal["idle", "running", "disabled"] = Field(default="idle")
40
name: str = Field()
41
system_prompt: str
42
prompt: str
@@ -71,7 +70,7 @@ class AdHocTask(BaseModel):
70
71
def update(self,
72
name: str | None = None,
74
- state: Literal["idle", "running"] | None = None,
73
+ state: Literal["idle", "running", "disabled"] | None = None,
74
system_prompt: str | None = None,
75
prompt: str | None = None,
76
attachments: list[str] | None = None,
@@ -107,7 +106,7 @@ class AdHocTask(BaseModel):
106
107
class ScheduledTask(BaseModel):
108
uuid: str = Field(default_factory=lambda: str(uuid.uuid4()))
110
- state: Literal["idle", "running"] = Field(default="idle")
109
+ state: Literal["idle", "running", "disabled"] = Field(default="idle")
110
name: str
111
schedule: TaskSchedule
112
system_prompt: str
@@ -141,7 +140,7 @@ class ScheduledTask(BaseModel):
140
141
def update(self,
142
name: str | None = None,
144
- state: Literal["idle", "running"] | None = None,
143
+ state: Literal["idle", "running", "disabled"] | None = None,
144
system_prompt: str | None = None,
145
prompt: str | None = None,
146
attachments: list[str] | None = None,
@@ -277,44 +276,55 @@ class TaskScheduler:
276
config = initialize()
277
context: AgentContext = AgentContext(config)
278
context.id = task.uuid
280
- chat_json = export_json_chat(context)
281
- chat_file = get_abs_path(TASKS_FOLDER, task.uuid, "chat.json")
282
- make_dirs(chat_file)
283
- write_file(chat_file, chat_json)
279
+ # chat_json = export_json_chat(context)
280
+ # chat_file = get_abs_path(TASKS_FOLDER, task.uuid, "chat.json")
281
+ # make_dirs(chat_file)
282
+ # write_file(chat_file, chat_json)
283
+ save_tmp_chat(context, TASKS_FOLDER)
284
return context
285
286
async def _get_chat_context(self, task: Union[ScheduledTask, AdHocTask]) -> AgentContext:
287
- chat_file = get_abs_path(TASKS_FOLDER, task.uuid, "chat.json")
288
- if exists(chat_file):
289
- chat = read_file(chat_file)
290
- context = AgentContext.get(load_json_chats([chat])[0])
287
+ ctxids = load_tmp_chats(TASKS_FOLDER)
288
+ # chat_file = get_abs_path(TASKS_FOLDER, task.uuid, "chat.json")
289
+ # if exists(chat_file):
290
+ if task.uuid in ctxids:
291
+ # chat = read_file(chat_file)
292
+ context = AgentContext.get(task.uuid)
293
if isinstance(context, AgentContext):
294
self._printer.print(
293
- f"Scheduler Task {task.name} loaded from chat {task.uuid}"
295
+ f"Scheduler Task {task.name} loaded from task {task.uuid}, context ok"
296
)
297
+ context.id = task.uuid
298
+ save_tmp_chat(context, TASKS_FOLDER)
299
return context
300
else:
301
self._printer.print(
298
- f"Scheduler Task {task.name} loaded from chat {task.uuid} but failed to load context"
302
+ f"Scheduler Task {task.name} loaded from task {task.uuid} but failed to load context"
303
)
304
return await self.__new_context(task)
305
else:
306
self._printer.print(
303
- f"Scheduler Task {task.name} loaded from chat {task.uuid} but chat file not found"
307
+ f"Scheduler Task {task.name} loaded from task {task.uuid} but context not found"
308
)
309
return await self.__new_context(task)
310
311
async def _persist_chat(self, task: Union[ScheduledTask, AdHocTask], context: AgentContext):
308
- chat_json = export_json_chat(context)
309
- chat_file = get_abs_path(TASKS_FOLDER, task.uuid, "chat.json")
310
- make_dirs(chat_file)
311
- write_file(chat_file, chat_json)
312
+ # chat_json = export_json_chat(context)
313
+ # chat_file = get_abs_path(TASKS_FOLDER, task.uuid, "chat.json")
314
+ # make_dirs(chat_file)
315
+ # write_file(chat_file, chat_json)
316
+ context.id = task.uuid
317
+ save_tmp_chat(context, TASKS_FOLDER)
318
319
async def _run_task(self, task: Union[ScheduledTask, AdHocTask]):
320
321
async def _run_task_wrapper(task: Union[ScheduledTask, AdHocTask]):
322
+ if task.state != "idle":
323
+ self._printer.print(f"Scheduler Task {task.name} state is '{task.state}', skipping")
324
+ return
325
+
326
if task.state == "running":
317
- self._printer.print(f"Scheduler Task {task.name} already running")
327
+ self._printer.print(f"Scheduler Task {task.name} already running, skipping")
328
return
329
330
try:
@@ -359,7 +369,7 @@ class TaskScheduler:
369
result = await agent.monologue()
370
task.update(last_result="SUCCESS: " + result)
371
362
- self._printer.print(f"Scheduler Task {task.name} completed: {result}")
372
+ self._printer.print(f"Scheduler Task '{task.name}' completed: {result}")
373
374
await self._persist_chat(task, context)
375
run_ui.py
+3
-1
@@ -11,7 +11,6 @@ from python.helpers.extract_tools import load_classes_from_folder
11
from python.helpers.api import ApiHandler
12
from python.helpers.print_style import PrintStyle
13
import sys
14
-import asyncio
14
import socket
15
import struct
16
@@ -168,6 +167,9 @@ def run():
167
# initialize contexts from persisted chats
168
persist_chat.load_tmp_chats()
169
170
+ # initialize contexts from persisted tasks
171
+ persist_chat.load_tmp_tasks()
172
+
173
except Exception as e:
174
PrintStyle().error(errors.format_error(e))
175
webui/index.css
+337
-13
@@ -208,8 +208,57 @@ body,
208
}
209
210
/* Chats container */
211
+.chat-container {
212
+ display: flex;
213
+ align-items: center;
214
+ position: relative;
215
+ width: 100%;
216
+ min-height: 30px;
217
+ gap: 0;
218
+}
219
+
220
.chat-list-button {
221
+ display: block;
222
+ width: 100%;
223
+ padding: 1px 5px;
224
+ cursor: pointer;
225
+ overflow: hidden;
226
+ position: relative;
227
+ transition: background-color 0.2s;
228
+ border-radius: 4px;
229
+}
230
+
231
+/* Subtle background on hover for the entire row */
232
+.chat-list-button:hover {
233
+ background-color: rgba(255, 255, 255, 0.03);
234
+}
235
+
236
+.light-mode .chat-list-button:hover {
237
+ background-color: rgba(0, 0, 0, 0.02);
238
+}
239
+
240
+.chat-name {
241
+ display: inline-block;
242
+ max-width: 160px;
243
+ overflow: hidden;
244
+ text-overflow: ellipsis;
245
+ white-space: nowrap;
246
cursor: pointer;
247
+ padding: 3px 8px;
248
+ border-radius: 4px;
249
+ transition: background-color 0.2s;
250
+ margin-right: 60px; /* Make space for buttons */
251
+ font-size: var(--font-size-small); /* Match config button font size */
252
+}
253
+
254
+/* Add a nice hover effect to just the chat name */
255
+.chat-name:hover {
256
+ background-color: rgba(255, 255, 255, 0.1);
257
+ text-decoration: none;
258
+}
259
+
260
+.light-mode .chat-name:hover {
261
+ background-color: rgba(0, 0, 0, 0.05);
262
}
263
264
.chats-list-container {
@@ -232,12 +281,26 @@ body,
281
background: linear-gradient(to bottom, calc(100% - 20px), transparent 100%);
282
/* Add padding to account for fade */
283
padding-bottom: 20px;
235
- scrollbar-width: none;
236
- -ms-overflow-style: none;
284
+ scrollbar-width: thin;
285
+ -ms-overflow-style: auto;
286
}
287
288
.chats-list-container::-webkit-scrollbar {
240
- width: 0px;
289
+ width: 5px;
290
+}
291
+
292
+.chats-list-container::-webkit-scrollbar-track {
293
+ background: rgba(0, 0, 0, 0.2);
294
+ border-radius: 6px;
295
+}
296
+
297
+.chats-list-container::-webkit-scrollbar-thumb {
298
+ background-color: var(--color-border);
299
+ border-radius: 6px;
300
+}
301
+
302
+.chats-list-container::-webkit-scrollbar-thumb:hover {
303
+ background-color: var(--color-border);
304
}
305
306
/* Chats Section */
@@ -247,7 +310,7 @@ body,
310
flex-direction: column;
311
min-height: 0;
312
flex: 1;
250
- margin-top: 1.5rem;
313
+ margin-top: 0.5rem;
314
}
315
316
/* Preferences */
@@ -1178,7 +1241,7 @@ pre {
1241
}
1242
1243
.text-button:active {
1181
- opacity: 0.5;
1244
+ opacity: 0.5;
1245
}
1246
1247
.text-button svg {
@@ -1588,7 +1651,7 @@ input:checked + .slider:before {
1651
display: table;
1652
gap: 0.1rem !important;
1653
}
1591
-
1654
+
1655
.text-button {
1656
max-height: 25px;
1657
}
@@ -1603,7 +1666,7 @@ input:checked + .slider:before {
1666
margin-left: var(--spacing-md);
1667
margin-bottom: var(--spacing-md);
1668
}
1606
-
1669
+
1670
.msg-kvps {
1671
display: flex;
1672
flex-direction: column;
@@ -1669,7 +1732,7 @@ input:checked + .slider:before {
1732
.sidebar-overlay.visible {
1733
display: block;
1734
}
1672
-}
1735
+}
1736
1737
@media (max-width: 768px) {
1738
#left-panel {
@@ -1699,15 +1762,15 @@ input:checked + .slider:before {
1762
-webkit-transition: all 0.3s ease;
1763
transition: all 0.3s ease;
1764
}
1702
-
1765
+
1766
#right-panel.expanded #logo-container {
1767
margin-left: 4.6rem;
1768
}
1706
-
1769
+
1770
#input-section {
1771
align-items: start;
1772
}
1710
-
1773
+
1774
.text-buttons-row {
1775
width: 90%;
1776
display: flex;
@@ -1786,7 +1849,7 @@ input:checked + .slider:before {
1849
-webkit-font-smoothing: antialiased;
1850
-moz-osx-font-smoothing: grayscale;
1851
}
1789
-
1852
+
1853
#chats-section {
1854
min-height: 100%;
1855
}
@@ -2095,4 +2158,265 @@ a:active {
2158
/* Alpine cloak to prevent FOUC */
2159
[x-cloak] {
2160
display: none !important;
2098
-}
\ No newline at end of file
2161
+}
2162
+
2163
+/* Add new styles for reasoning and deepsearchbutton */
2164
+.ml-auto {
2165
+ margin-left: auto;
2166
+}
2167
+
2168
+/* Remove unnecessary specific media query that was causing issues */
2169
+@media (max-width: 480px) {
2170
+ .text-button svg {
2171
+ width: 16px;
2172
+ height: 16px;
2173
+ }
2174
+}
2175
+
2176
+/* Add to the existing .chat-actions class or create it */
2177
+.chat-actions {
2178
+ display: flex;
2179
+ gap: 5px;
2180
+ position: absolute;
2181
+ right: 5px;
2182
+ top: 50%;
2183
+ transform: translateY(-50%);
2184
+ z-index: 2; /* Ensure buttons are above the edit field */
2185
+ min-width: 70px; /* Ensure minimum width for the buttons */
2186
+ justify-content: flex-end;
2187
+}
2188
+
2189
+/* Tasks list container - similar to chats list */
2190
+.tasks-list-container {
2191
+ max-height: 300px;
2192
+ overflow-y: auto;
2193
+ margin-top: 10px;
2194
+ padding-right: 5px;
2195
+ border-radius: 5px;
2196
+ position: relative;
2197
+ scrollbar-width: thin;
2198
+ -ms-overflow-style: auto;
2199
+}
2200
+
2201
+.tasks-list-container::-webkit-scrollbar {
2202
+ width: 5px;
2203
+}
2204
+
2205
+.tasks-list-container::-webkit-scrollbar-track {
2206
+ background: rgba(0, 0, 0, 0.2);
2207
+ border-radius: 6px;
2208
+}
2209
+
2210
+.tasks-list-container::-webkit-scrollbar-thumb {
2211
+ background-color: var(--color-border);
2212
+ border-radius: 6px;
2213
+}
2214
+
2215
+.tasks-list-container::-webkit-scrollbar-thumb:hover {
2216
+ background-color: var(--color-border);
2217
+}
2218
+
2219
+.task-name {
2220
+ display: inline-block;
2221
+ max-width: 160px;
2222
+ overflow: hidden;
2223
+ text-overflow: ellipsis;
2224
+ white-space: nowrap;
2225
+ cursor: pointer;
2226
+ padding: 3px 5px;
2227
+ border-radius: 4px;
2228
+ transition: background-color 0.2s;
2229
+ margin-right: 60px; /* Make space for future buttons */
2230
+ font-size: var(--font-size-small); /* Match config button font size */
2231
+}
2232
+
2233
+.task-name:hover {
2234
+ background-color: rgba(255, 255, 255, 0.1);
2235
+ text-decoration: none;
2236
+}
2237
+
2238
+.light-mode .task-name:hover {
2239
+ background-color: rgba(0, 0, 0, 0.05);
2240
+}
2241
+
2242
+/* Dark mode overrides */
2243
+.light-mode .tab.active {
2244
+ color: var(--highlight-pink);
2245
+}
2246
+
2247
+.light-mode .tab.active::after {
2248
+ background-color: var(--highlight-pink);
2249
+ box-shadow: 0 0 8px var(--highlight-pink);
2250
+}
2251
+/* Tabs styling */
2252
+.tabs-container {
2253
+ width: 100%;
2254
+ margin-bottom: 8px; /* Reduced spacing between tabs and list */
2255
+ padding: 0;
2256
+ margin-top: 20px; /* Increased spacing from elements above */
2257
+}
2258
+
2259
+.tabs {
2260
+ display: flex;
2261
+ width: 100%;
2262
+ position: relative;
2263
+ gap: 5px;
2264
+ border-bottom: 3px solid var(--color-border); /* Thicker bottom line */
2265
+ justify-content: center; /* Center the tabs */
2266
+}
2267
+
2268
+.tab {
2269
+ padding: 8px 16px;
2270
+ cursor: pointer;
2271
+ position: relative;
2272
+ color: var(--color-text);
2273
+ border: 2px solid var(--color-border);
2274
+ border-bottom: none;
2275
+ border-radius: 8px 8px 0 0;
2276
+ transition: all 0.3s ease;
2277
+ background-color: var(--color-panel);
2278
+ margin-bottom: -3px; /* Match the thicker border */
2279
+ z-index: 1;
2280
+}
2281
+
2282
+.tab:not(.active) {
2283
+ opacity: 0.8;
2284
+ border-bottom: 3px solid var(--color-border);
2285
+ background-color: rgba(255, 255, 255, 0.03);
2286
+}
2287
+
2288
+.tab.active {
2289
+ color: var(--color-primary);
2290
+ border-color: var(--color-border);
2291
+ box-shadow:
2292
+ 0 -4px 8px -2px var(--color-border),
2293
+ 4px 0 8px -2px var(--color-border),
2294
+ -4px 0 8px -2px var(--color-border);
2295
+ font-weight: bold;
2296
+ background-color: var(--color-panel);
2297
+}
2298
+
2299
+.light-mode .tab.active {
2300
+ color: var(--color-primary);
2301
+ box-shadow:
2302
+ 0 -4px 8px -2px var(--color-border),
2303
+ 4px 0 8px -2px var(--color-border),
2304
+ -4px 0 8px -2px var(--color-border);
2305
+}
2306
+
2307
+.light-mode .tab:not(.active) {
2308
+ background-color: rgba(0, 0, 0, 0.03);
2309
+}
2310
+
2311
+/* Remove previous tab styling that conflicts */
2312
+.tab.active::after {
2313
+ display: none;
2314
+}
2315
+
2316
+/* Empty list message styling enhancement */
2317
+.empty-list-message {
2318
+ display: flex;
2319
+ justify-content: center;
2320
+ align-items: center;
2321
+ height: 100px;
2322
+ color: var(--color-secondary);
2323
+ text-align: center;
2324
+ opacity: 0.7;
2325
+ font-style: italic;
2326
+}
2327
+
2328
+.light-mode .empty-list-message {
2329
+ color: var(--color-secondary-light);
2330
+}
2331
+
2332
+/* Common scrollbar styling */
2333
+::-webkit-scrollbar {
2334
+ width: 5px;
2335
+ height: 5px;
2336
+}
2337
+
2338
+::-webkit-scrollbar-track {
2339
+ background: rgba(0, 0, 0, 0.2);
2340
+ border-radius: 6px;
2341
+}
2342
+
2343
+::-webkit-scrollbar-thumb {
2344
+ background-color: var(--color-border);
2345
+ border-radius: 6px;
2346
+ transition: background-color 0.2s ease;
2347
+}
2348
+
2349
+::-webkit-scrollbar-thumb:hover {
2350
+ background-color: var(--color-border);
2351
+}
2352
+
2353
+::-webkit-scrollbar-thumb:active {
2354
+ background-color: var(--color-border);
2355
+}
2356
+
2357
+/* Firefox scrollbar */
2358
+* {
2359
+ scrollbar-width: thin;
2360
+ scrollbar-color: var(--color-border) rgba(0, 0, 0, 0.2);
2361
+}
2362
+
2363
+/* Light mode scrollbar */
2364
+.light-mode ::-webkit-scrollbar-track {
2365
+ background: rgba(200, 200, 200, 0.3);
2366
+}
2367
+
2368
+.light-mode ::-webkit-scrollbar-thumb {
2369
+ background-color: var(--color-border);
2370
+}
2371
+
2372
+.light-mode ::-webkit-scrollbar-thumb:hover {
2373
+ background-color: var(--color-border);
2374
+}
2375
+
2376
+.light-mode ::-webkit-scrollbar-thumb:active {
2377
+ background-color: var(--color-border);
2378
+}
2379
+
2380
+.light-mode * {
2381
+ scrollbar-color: var(--color-border) rgba(200, 200, 200, 0.3);
2382
+}
2383
+
2384
+/* Add specific styling for selected chat items */
2385
+.chat-list-button.font-bold {
2386
+ position: relative;
2387
+ background-color: var(--color-border) 0.05;
2388
+}
2389
+
2390
+.chat-list-button.font-bold::before {
2391
+ content: '';
2392
+ position: absolute;
2393
+ left: 0;
2394
+ top: 0;
2395
+ height: 100%;
2396
+ width: 3px;
2397
+ background-color: var(--color-border);
2398
+ border-top-left-radius: 3px;
2399
+ border-bottom-left-radius: 3px;
2400
+}
2401
+
2402
+.light-mode .chat-list-button.font-bold {
2403
+ background-color: var(--color-border) 0.05;
2404
+}
2405
+
2406
+.light-mode .chat-list-button.font-bold::before {
2407
+ background-color: var(--color-border);
2408
+}
2409
+
2410
+/* Add some padding to the list items to accommodate the stripe */
2411
+.chat-list-button {
2412
+ padding-left: 5px;
2413
+ border-radius: 4px;
2414
+ transition: background-color 0.2s ease-in-out;
2415
+}
2416
+
2417
+/* Make sure the chat container has proper spacing */
2418
+.chat-container, .task-container {
2419
+ display: flex;
2420
+ align-items: center;
2421
+ width: 100%;
2422
+}
webui/index.html
+55
-20
@@ -87,22 +87,57 @@
87
</svg>Settings</button>
88
89
</div>
90
+
91
+ <!-- Tabs container -->
92
+ <div class="tabs-container">
93
+ <div class="tabs">
94
+ <div class="tab active" id="chats-tab">Chats</div>
95
+ <div class="tab" id="tasks-tab">Tasks</div>
96
+ </div>
97
+ </div>
98
+
99
<!-- Chats List -->
100
<div class="config-section" id="chats-section" x-data="{ contexts: [], selected: '' }"
92
- x-show="contexts.length > 0">
93
- <h3>Chats</h3>
101
+ x-show="contexts.length > 0 || true">
102
<div class="chats-list-container">
95
- <ul class="config-list">
103
+ <ul class="config-list" x-show="contexts.length > 0">
104
<template x-for="context in contexts">
105
<li>
98
- <span :class="{'chat-list-button': true, 'font-bold': context.id === selected}"
106
+ <div :class="{'chat-list-button': true, 'font-bold': context.id === selected}"
107
@click="selected = context.id; selectChat(context.id)">
100
- <span x-text="context.name ? context.name : 'Chat #' + context.no"></span>
101
- </span>
108
+ <span class="chat-name" x-text="context.name ? context.name : 'Chat #' + context.no"></span>
109
+ </div>
110
<button class="edit-button" @click="killChat(context.id)">X</button>
111
</li>
112
</template>
113
</ul>
114
+ <div class="empty-list-message" x-show="contexts.length === 0">
115
+ <p><i>No chats to list.</i></p>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Tasks List -->
121
+ <div class="config-section" id="tasks-section" x-data="{ tasks: [], selected: '' }"
122
+ style="display: none;">
123
+ <div class="tasks-list-container">
124
+ <ul class="config-list" x-show="tasks.length > 0">
125
+ <template x-for="task in tasks">
126
+ <li>
127
+ <div :class="{'chat-list-button': true, 'font-bold': task.id === selected}"
128
+ @click="selected = task.id; selectChat(task.id)">
129
+ <div class="chat-container">
130
+ <span class="task-name"
131
+ x-text="task.name || `Task #${task.id.substring(0,8)}`"
132
+ :data-task-id="task.id"></span>
133
+ </div>
134
+ </div>
135
+ </li>
136
+ </template>
137
+ </ul>
138
+ <div class="empty-list-message" x-show="tasks.length === 0">
139
+ <p><i>No tasks to list.</i></p>
140
+ </div>
141
</div>
142
</div>
143
</div>
@@ -216,19 +251,19 @@
251
<span id="stop-speech" @click="window.speech.stop()" style="cursor: pointer">Stop Speech</span>
252
</h4>
253
</div>
219
- <div id="input-section" x-data="{
254
+ <div id="input-section" x-data="{
255
paused: false,
256
attachments: [],
257
hasAttachments: false,
223
-
258
+
259
handleFileUpload(event) {
260
const files = event.target.files;
226
-
261
+
262
Array.from(files).forEach(file => {
263
const ext = file.name.split('.').pop().toLowerCase();
229
-
264
+
265
const isImage = ['jpg', 'jpeg', 'png', 'bmp'].includes(ext);
231
-
266
+
267
if (isImage) {
268
// Handle image preview
269
const reader = new FileReader();
@@ -298,7 +333,7 @@
333
<!-- Container for textarea and button -->
334
<div id="chat-input-container" style="position: relative;">
335
<textarea id="chat-input" placeholder="Type your message here..." rows="1"></textarea>
301
-
336
+
337
<!-- Expand button inside the textarea container -->
338
<button id="expand-button" @click="$store.fullScreenInputModal.openModal()" aria-label="Expand input">
339
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
@@ -306,7 +341,7 @@
341
</svg>
342
</button>
343
</div>
309
-
344
+
345
<div id="chat-buttons-wrapper">
346
347
<!-- Send button -->
@@ -687,12 +722,12 @@
722
<!-- Full Screen Input Modal -->
723
<div id="fullScreenInputModal" x-data="fullScreenInputModalProxy">
724
<template x-teleport="body">
690
- <div x-show="isOpen" class="modal-overlay" @click.self="handleClose()"
725
+ <div x-show="isOpen" class="modal-overlay" @click.self="handleClose()"
726
@keydown.escape.window="handleClose()" x-transition>
727
<div class="modal-container full-screen-input-modal">
728
<div class="modal-content">
729
<button class="modal-close" @click="handleClose()">×</button>
695
-
730
+
731
<!-- Add toolbar -->
732
<div class="editor-toolbar">
733
<div class="toolbar-group">
@@ -725,8 +760,8 @@
760
</div>
761
</div>
762
728
- <textarea id="full-screen-input" x-model="inputText"
729
- placeholder="Type your message here..."
763
+ <textarea id="full-screen-input" x-model="inputText"
764
+ placeholder="Type your message here..."
765
@keydown.ctrl.enter="handleClose()"
766
@keydown.ctrl.z.prevent="undo()"
767
@keydown.ctrl.shift.z.prevent="redo()"
@@ -744,9 +779,9 @@
779
</div>
780
781
<!-- Drag and Drop Overlay -->
747
- <div id="dragdrop-overlay"
782
+ <div id="dragdrop-overlay"
783
x-cloak
749
- x-data="{ isVisible: false }"
784
+ x-data="{ isVisible: false }"
785
x-show="isVisible"
786
x-transition:enter="transition ease-out duration-300"
787
x-transition:enter-start="opacity-0"
@@ -762,4 +797,4 @@
797
798
</body>
799
765
-</html>
\ No newline at end of file
800
+</html>
webui/index.js
+332
-32
@@ -10,6 +10,7 @@ const sendButton = document.getElementById('send-button');
10
const inputSection = document.getElementById('input-section');
11
const statusSection = document.getElementById('status-section');
12
const chatsSection = document.getElementById('chats-section');
13
+const tasksSection = document.getElementById('tasks-section');
14
const progressBar = document.getElementById('progress-bar');
15
const autoScrollSwitch = document.getElementById('auto-scroll-switch');
16
const timeDate = document.getElementById('time-date-container');
@@ -20,8 +21,10 @@ let context = "";
21
let connectionStatus = false
22
23
23
-// Initialize the toggle button
24
+// Initialize the toggle button
25
setupSidebarToggle();
26
+// Initialize tabs
27
+setupTabs();
28
29
function isMobile() {
30
return window.innerWidth <= 768;
@@ -149,7 +152,7 @@ export async function sendMessage() {
152
// } else {
153
// toast("Undefined error.", "error");
154
// }
152
- // }
155
+ // }
156
else {
157
setContext(jsonResponse.context);
158
}
@@ -340,7 +343,12 @@ async function poll() {
343
let updated = false
344
try {
345
const response = await sendJsonData("/poll", { log_from: lastLogVersion, context });
343
- //console.log(response)
346
+
347
+ // Check if the response is valid
348
+ if (!response) {
349
+ console.error("Invalid response from poll endpoint");
350
+ return false;
351
+ }
352
353
if (!context) setContext(response.context)
354
if (response.context != context) return //skip late polls after context change
@@ -359,6 +367,9 @@ async function poll() {
367
afterMessagesUpdate(response.logs)
368
}
369
370
+ lastLogVersion = response.log_version;
371
+ lastLogGuid = response.log_guid;
372
+
373
updateProgress(response.log_progress, response.log_progress_active)
374
375
//set ui model vars from backend
@@ -368,11 +379,95 @@ async function poll() {
379
// Update status icon state
380
setConnectionStatus(true)
381
382
+ // Update chats list and sort by created_at time (newer first)
383
const chatsAD = Alpine.$data(chatsSection);
372
- chatsAD.contexts = response.contexts;
384
+ const contexts = response.contexts || [];
385
+ chatsAD.contexts = contexts.sort((a, b) =>
386
+ (b.created_at || 0) - (a.created_at || 0)
387
+ );
388
+
389
+ // Update tasks list and sort by creation time (newer first)
390
+ const tasksSection = document.getElementById('tasks-section');
391
+ if (tasksSection) {
392
+ const tasksAD = Alpine.$data(tasksSection);
393
+ let tasks = response.tasks || [];
394
+
395
+ // Only update the tasks array if it's actually different
396
+ // This prevents unnecessary reactivity triggers
397
+ const currentTaskIds = new Set((tasksAD.tasks || []).map(t => t.id));
398
+ const newTaskIds = new Set(tasks.map(t => t.id));
399
+
400
+ // Check if the sets are different sizes or have different contents
401
+ const needsUpdate = currentTaskIds.size !== newTaskIds.size ||
402
+ tasks.some(task => !currentTaskIds.has(task.id));
403
+
404
+ if (needsUpdate) {
405
+ if (tasks.length > 0) {
406
+ // Sort the tasks by creation time
407
+ const sortedTasks = [...tasks].sort((a, b) =>
408
+ (b.created_at || 0) - (a.created_at || 0)
409
+ );
410
+
411
+ // Use a clean array assignment to avoid duplicating elements
412
+ tasksAD.tasks = sortedTasks;
413
+ } else {
414
+ // Make sure to use a new empty array instance
415
+ tasksAD.tasks = [];
416
+ }
417
+ }
418
+ }
419
374
- lastLogVersion = response.log_version;
375
- lastLogGuid = response.log_guid;
420
+ // Make sure the active context is properly selected in both lists
421
+ if (context) {
422
+ // Update selection in the active tab
423
+ const activeTab = localStorage.getItem('activeTab') || 'chats';
424
+
425
+ if (activeTab === 'chats') {
426
+ chatsAD.selected = context;
427
+ localStorage.setItem('lastSelectedChat', context);
428
+
429
+ // Check if this context exists in the chats list
430
+ const contextExists = contexts.some(ctx => ctx.id === context);
431
+
432
+ // If it doesn't exist in the chats list but we're in chats tab, try to select the first chat
433
+ if (!contextExists && contexts.length > 0) {
434
+ const firstChatId = contexts[0].id;
435
+ setContext(firstChatId);
436
+ chatsAD.selected = firstChatId;
437
+ localStorage.setItem('lastSelectedChat', firstChatId);
438
+ }
439
+ } else if (activeTab === 'tasks' && tasksSection) {
440
+ const tasksAD = Alpine.$data(tasksSection);
441
+ tasksAD.selected = context;
442
+ localStorage.setItem('lastSelectedTask', context);
443
+
444
+ // Check if this context exists in the tasks list
445
+ const taskExists = response.tasks?.some(task => task.id === context);
446
+
447
+ // If it doesn't exist in the tasks list but we're in tasks tab, try to select the first task
448
+ if (!taskExists && response.tasks?.length > 0) {
449
+ const firstTaskId = response.tasks[0].id;
450
+ setContext(firstTaskId);
451
+ tasksAD.selected = firstTaskId;
452
+ localStorage.setItem('lastSelectedTask', firstTaskId);
453
+ }
454
+ }
455
+ } else if (response.tasks && response.tasks.length > 0 && localStorage.getItem('activeTab') === 'tasks') {
456
+ // If we're in tasks tab with no selection but have tasks, select the first one
457
+ const firstTaskId = response.tasks[0].id;
458
+ setContext(firstTaskId);
459
+ if (tasksSection) {
460
+ const tasksAD = Alpine.$data(tasksSection);
461
+ tasksAD.selected = firstTaskId;
462
+ localStorage.setItem('lastSelectedTask', firstTaskId);
463
+ }
464
+ } else if (contexts.length > 0 && localStorage.getItem('activeTab') === 'chats') {
465
+ // If we're in chats tab with no selection but have chats, select the first one
466
+ const firstChatId = contexts[0].id;
467
+ setContext(firstChatId);
468
+ chatsAD.selected = firstChatId;
469
+ localStorage.setItem('lastSelectedChat', firstChatId);
470
+ }
471
472
} catch (error) {
473
console.error('Error:', error);
@@ -443,45 +538,144 @@ window.newChat = async function () {
538
}
539
540
window.killChat = async function (id) {
541
+ if (!id) {
542
+ console.error("No chat ID provided for deletion");
543
+ return;
544
+ }
545
+
546
+ console.log("Deleting chat with ID:", id);
547
+
548
try {
549
const chatsAD = Alpine.$data(chatsSection);
448
- let found, other
550
+ console.log("Current contexts before deletion:", JSON.stringify(chatsAD.contexts.map(c => ({ id: c.id, name: c.name }))));
551
+
552
+ // Find an alternate chat to switch to if we're deleting the current one
553
+ let alternateChat = null;
554
for (let i = 0; i < chatsAD.contexts.length; i++) {
450
- if (chatsAD.contexts[i].id == id) {
451
- found = true
452
- } else {
453
- other = chatsAD.contexts[i]
555
+ if (chatsAD.contexts[i].id !== id) {
556
+ alternateChat = chatsAD.contexts[i];
557
+ break;
558
}
455
- if (found && other) break
559
}
560
458
- if (context == id && found) {
459
- if (other) setContext(other.id)
460
- else setContext(generateGUID())
561
+ // If we're deleting the currently selected chat, switch to another one first
562
+ if (context === id) {
563
+ if (alternateChat) {
564
+ setContext(alternateChat.id);
565
+ } else {
566
+ // If no other chats, create a new empty context
567
+ setContext(generateGUID());
568
+ }
569
}
570
463
- if (found) sendJsonData("/chat_remove", { context: id });
571
+ // Delete the chat on the server
572
+ await sendJsonData("/chat_remove", { context: id });
573
465
- updateAfterScroll()
574
+ // Update the UI manually to ensure the correct chat is removed
575
+ // Deep clone the contexts array to prevent reference issues
576
+ const updatedContexts = chatsAD.contexts.filter(ctx => ctx.id !== id);
577
+ console.log("Updated contexts after deletion:", JSON.stringify(updatedContexts.map(c => ({ id: c.id, name: c.name }))));
578
+
579
+ // Force UI update by creating a new array
580
+ chatsAD.contexts = [...updatedContexts];
581
582
+ updateAfterScroll();
583
+
584
+ toast("Chat deleted successfully", "success");
585
} catch (e) {
468
- window.toastFetchError("Error creating new chat", e)
586
+ console.error("Error deleting chat:", e);
587
+ window.toastFetchError("Error deleting chat", e);
588
+ }
589
+}
590
+
591
+// Function to ensure proper UI state when switching contexts
592
+function ensureProperTabSelection(contextId) {
593
+ // Get current active tab
594
+ const activeTab = localStorage.getItem('activeTab') || 'chats';
595
+
596
+ // First attempt to determine if this is a task or chat based on the task list
597
+ const tasksSection = document.getElementById('tasks-section');
598
+ let isTask = false;
599
+
600
+ if (tasksSection) {
601
+ const tasksAD = Alpine.$data(tasksSection);
602
+ if (tasksAD && tasksAD.tasks) {
603
+ isTask = tasksAD.tasks.some(task => task.id === contextId);
604
+ }
605
+ }
606
+
607
+ // If we're selecting a task but are in the chats tab, switch to tasks tab
608
+ if (isTask && activeTab === 'chats') {
609
+ // Store this as the last selected task before switching
610
+ localStorage.setItem('lastSelectedTask', contextId);
611
+ activateTab('tasks');
612
+ return true;
613
+ }
614
+
615
+ // If we're selecting a chat but are in the tasks tab, switch to chats tab
616
+ if (!isTask && activeTab === 'tasks') {
617
+ // Store this as the last selected chat before switching
618
+ localStorage.setItem('lastSelectedChat', contextId);
619
+ activateTab('chats');
620
+ return true;
621
}
622
+
623
+ return false;
624
}
625
626
window.selectChat = async function (id) {
473
- setContext(id)
474
- updateAfterScroll()
627
+ if (id === context) return //already selected
628
+
629
+ // Check if we need to switch tabs based on the context type
630
+ const tabSwitched = ensureProperTabSelection(id);
631
+
632
+ // If we didn't switch tabs, proceed with normal selection
633
+ if (!tabSwitched) {
634
+ // Switch to the new context - this will clear chat history and reset tracking variables
635
+ setContext(id);
636
+
637
+ // Update both contexts and tasks lists to reflect the selected item
638
+ const chatsAD = Alpine.$data(chatsSection);
639
+ const tasksSection = document.getElementById('tasks-section');
640
+ if (tasksSection) {
641
+ const tasksAD = Alpine.$data(tasksSection);
642
+ tasksAD.selected = id;
643
+ }
644
+ chatsAD.selected = id;
645
+
646
+ // Store this selection in the appropriate localStorage key
647
+ const activeTab = localStorage.getItem('activeTab') || 'chats';
648
+ if (activeTab === 'chats') {
649
+ localStorage.setItem('lastSelectedChat', id);
650
+ } else if (activeTab === 'tasks') {
651
+ localStorage.setItem('lastSelectedTask', id);
652
+ }
653
+
654
+ // Trigger an immediate poll to fetch content
655
+ poll();
656
+ }
657
+
658
+ updateAfterScroll();
659
}
660
661
export const setContext = function (id) {
478
- if (id == context) return
479
- context = id
480
- lastLogGuid = ""
481
- lastLogVersion = 0
482
- lastSpokenNo = 0
662
+ if (id == context) return;
663
+ context = id;
664
+ // Always reset the log tracking variables when switching contexts
665
+ // This ensures we get fresh data from the backend
666
+ lastLogGuid = "";
667
+ lastLogVersion = 0;
668
+ lastSpokenNo = 0;
669
+
670
+ // Clear the chat history immediately to avoid showing stale content
671
+ chatHistory.innerHTML = "";
672
+
673
+ // Update both selected states
674
const chatsAD = Alpine.$data(chatsSection);
484
- chatsAD.selected = id
675
+ const tasksAD = Alpine.$data(tasksSection);
676
+
677
+ chatsAD.selected = id;
678
+ tasksAD.selected = id;
679
}
680
681
export const getContext = function () {
@@ -625,7 +819,7 @@ window.loadChats = async function () {
819
// } else {
820
// toast("Undefined error.", "error")
821
// }
628
- // }
822
+ // }
823
else {
824
setContext(response.ctxids[0])
825
toast("Chats loaded.", "success")
@@ -905,7 +1099,7 @@ document.addEventListener('DOMContentLoaded', () => {
1099
dragDropOverlay.addEventListener('drop', (e) => {
1100
dragCounter = 0;
1101
Alpine.$data(dragDropOverlay).isVisible = false;
908
-
1102
+
1103
const inputAD = Alpine.$data(inputSection);
1104
const files = e.dataTransfer.files;
1105
handleFiles(files, inputAD);
@@ -916,9 +1110,9 @@ document.addEventListener('DOMContentLoaded', () => {
1110
function handleFiles(files, inputAD) {
1111
Array.from(files).forEach(file => {
1112
const ext = file.name.split('.').pop().toLowerCase();
919
-
1113
+
1114
const isImage = ['jpg', 'jpeg', 'png', 'bmp'].includes(ext);
921
-
1115
+
1116
if (isImage) {
1117
const reader = new FileReader();
1118
reader.onload = e => {
@@ -941,7 +1135,7 @@ function handleFiles(files, inputAD) {
1135
});
1136
inputAD.hasAttachments = true;
1137
}
944
-
1138
+
1139
});
1140
}
1141
@@ -950,4 +1144,110 @@ window.handleFileUpload = function(event) {
1144
const files = event.target.files;
1145
const inputAD = Alpine.$data(inputSection);
1146
handleFiles(files, inputAD);
953
-}
\ No newline at end of file
1147
+}
1148
+
1149
+// Setup event handlers once the DOM is fully loaded
1150
+document.addEventListener('DOMContentLoaded', function() {
1151
+ setupSidebarToggle();
1152
+ setupTabs();
1153
+ initializeActiveTab();
1154
+});
1155
+
1156
+// Setup tabs functionality
1157
+function setupTabs() {
1158
+ const chatsTab = document.getElementById('chats-tab');
1159
+ const tasksTab = document.getElementById('tasks-tab');
1160
+
1161
+ if (chatsTab && tasksTab) {
1162
+ chatsTab.addEventListener('click', function() {
1163
+ activateTab('chats');
1164
+ });
1165
+
1166
+ tasksTab.addEventListener('click', function() {
1167
+ activateTab('tasks');
1168
+ });
1169
+ } else {
1170
+ console.error('Tab elements not found');
1171
+ setTimeout(setupTabs, 100); // Retry setup
1172
+ }
1173
+}
1174
+
1175
+function activateTab(tabName) {
1176
+ const chatsTab = document.getElementById('chats-tab');
1177
+ const tasksTab = document.getElementById('tasks-tab');
1178
+ const chatsSection = document.getElementById('chats-section');
1179
+ const tasksSection = document.getElementById('tasks-section');
1180
+
1181
+ // Get current context to preserve before switching
1182
+ const currentContext = context;
1183
+
1184
+ // Store the current selection for the active tab before switching
1185
+ const previousTab = localStorage.getItem('activeTab');
1186
+ if (previousTab === 'chats') {
1187
+ localStorage.setItem('lastSelectedChat', currentContext);
1188
+ } else if (previousTab === 'tasks') {
1189
+ localStorage.setItem('lastSelectedTask', currentContext);
1190
+ }
1191
+
1192
+ // Reset all tabs and sections
1193
+ chatsTab.classList.remove('active');
1194
+ tasksTab.classList.remove('active');
1195
+ chatsSection.style.display = 'none';
1196
+ tasksSection.style.display = 'none';
1197
+
1198
+ // Remember the last active tab in localStorage
1199
+ localStorage.setItem('activeTab', tabName);
1200
+
1201
+ // Activate selected tab and section
1202
+ if (tabName === 'chats') {
1203
+ chatsTab.classList.add('active');
1204
+ chatsSection.style.display = '';
1205
+
1206
+ // Restore previous chat selection
1207
+ const lastSelectedChat = localStorage.getItem('lastSelectedChat');
1208
+ if (lastSelectedChat && lastSelectedChat !== currentContext) {
1209
+ // Only switch if there's a stored selection and it's different from current
1210
+ setContext(lastSelectedChat);
1211
+ }
1212
+ } else if (tabName === 'tasks') {
1213
+ tasksTab.classList.add('active');
1214
+ tasksSection.style.display = 'flex';
1215
+ tasksSection.style.flexDirection = 'column';
1216
+
1217
+ // Restore previous task selection
1218
+ const lastSelectedTask = localStorage.getItem('lastSelectedTask');
1219
+ if (lastSelectedTask && lastSelectedTask !== currentContext) {
1220
+ // Only switch if there's a stored selection and it's different from current
1221
+ setContext(lastSelectedTask);
1222
+ }
1223
+ }
1224
+
1225
+ // Request a poll update
1226
+ poll();
1227
+}
1228
+
1229
+// Add function to initialize active tab and selections from localStorage
1230
+function initializeActiveTab() {
1231
+ // Initialize selection storage if not present
1232
+ if (!localStorage.getItem('lastSelectedChat')) {
1233
+ localStorage.setItem('lastSelectedChat', '');
1234
+ }
1235
+ if (!localStorage.getItem('lastSelectedTask')) {
1236
+ localStorage.setItem('lastSelectedTask', '');
1237
+ }
1238
+
1239
+ const activeTab = localStorage.getItem('activeTab') || 'chats';
1240
+ activateTab(activeTab);
1241
+}
1242
+
1243
+/*
1244
+ * A0 Chat UI
1245
+ *
1246
+ * Tasks tab functionality:
1247
+ * - Tasks are displayed in the Tasks tab with the same mechanics as chats
1248
+ * - Both lists are sorted by creation time (newest first)
1249
+ * - Selection state is preserved across tab switches
1250
+ * - The active tab is remembered across sessions
1251
+ * - Tasks use the same context system as chats for communication with the backend
1252
+ * - Future support for renaming and deletion will be implemented later
1253
+ */