memory dashboard editing
frdel committed
Sep 24, 2025 at 20:43 UTC
062aff7b556ed0219965d19753545412cbaf8ff1
6 files changed
+664
-378
python/api/memory_dashboard.py
+48
-24
@@ -2,6 +2,7 @@ from python.helpers.api import ApiHandler, Request, Response
2
from python.helpers.memory import Memory
3
from python.helpers import files
4
from models import ModelConfig, ModelType
5
+from langchain_core.documents import Document
6
7
8
class MemoryDashboard(ApiHandler):
@@ -12,13 +13,15 @@ class MemoryDashboard(ApiHandler):
13
if action == "get_memory_subdirs":
14
return await self._get_memory_subdirs()
15
elif action == "get_current_memory_subdir":
15
- return await self._get_current_memory_subdir(request)
16
+ return await self._get_current_memory_subdir(input)
17
elif action == "search":
18
return await self._search_memories(input)
19
elif action == "delete":
20
return await self._delete_memory(input)
21
elif action == "bulk_delete":
22
return await self._bulk_delete_memories(input)
23
+ elif action == "update":
24
+ return await self._update_memory(input)
25
else:
26
return {
27
"success": False,
@@ -101,11 +104,11 @@ class MemoryDashboard(ApiHandler):
104
"error": f"Failed to bulk delete memories: {str(e)}",
105
}
106
104
- async def _get_current_memory_subdir(self, request: Request) -> dict:
107
+ async def _get_current_memory_subdir(self, input: dict) -> dict:
108
"""Get the current memory subdirectory from the active context."""
109
try:
110
# Try to get the context from the request
108
- context_id = getattr(request, "context_id", None)
111
+ context_id = input.get("context_id", None)
112
if not context_id:
113
# Fallback to default if no context available
114
return {"success": True, "memory_subdir": "default"}
@@ -192,27 +195,7 @@ class MemoryDashboard(ApiHandler):
195
memories = memories[:limit]
196
197
# Format memories for the dashboard
195
- formatted_memories: list[dict] = []
196
- for m in memories:
197
- metadata = m.metadata
198
-
199
- # Extract key information
200
- memory_data = {
201
- "id": metadata.get("id", "unknown"),
202
- "area": metadata.get("area", "unknown"),
203
- "timestamp": metadata.get("timestamp", "unknown"),
204
- "content_preview": m.page_content[:200]
205
- + ("..." if len(m.page_content) > 200 else ""),
206
- "content_full": m.page_content,
207
- "knowledge_source": metadata.get("knowledge_source", False),
208
- "source_file": metadata.get("source_file", ""),
209
- "file_type": metadata.get("file_type", ""),
210
- "consolidation_action": metadata.get("consolidation_action", ""),
211
- "tags": metadata.get("tags", []),
212
- "metadata": metadata, # Include full metadata for advanced users
213
- }
214
-
215
- formatted_memories.append(memory_data)
198
+ formatted_memories = [self._format_memory_for_dashboard(m) for m in memories]
199
200
# Get summary statistics
201
total_memories = len(formatted_memories)
@@ -238,3 +221,44 @@ class MemoryDashboard(ApiHandler):
221
222
except Exception as e:
223
return {"success": False, "error": str(e), "memories": [], "total_count": 0}
224
+
225
+ def _format_memory_for_dashboard(self, m: Document) -> dict:
226
+ """Format a memory document for the dashboard."""
227
+ metadata = m.metadata
228
+ return {
229
+ "id": metadata.get("id", "unknown"),
230
+ "area": metadata.get("area", "unknown"),
231
+ "timestamp": metadata.get("timestamp", "unknown"),
232
+ "content_preview": m.page_content[:200]
233
+ + ("..." if len(m.page_content) > 200 else ""),
234
+ "content_full": m.page_content,
235
+ "knowledge_source": metadata.get("knowledge_source", False),
236
+ "source_file": metadata.get("source_file", ""),
237
+ "file_type": metadata.get("file_type", ""),
238
+ "consolidation_action": metadata.get("consolidation_action", ""),
239
+ "tags": metadata.get("tags", []),
240
+ "metadata": metadata, # Include full metadata for advanced users
241
+ }
242
+
243
+ async def _update_memory(self, input: dict) -> dict:
244
+ try:
245
+ memory_subdir = input.get("memory_subdir")
246
+ original = input.get("original")
247
+ edited = input.get("edited")
248
+
249
+ if not memory_subdir or not original or not edited:
250
+ return {"success": False, "error": "Missing required parameters"}
251
+
252
+ doc = Document(
253
+ page_content=edited["content_full"],
254
+ metadata=edited["metadata"],
255
+ )
256
+
257
+ memory = await Memory.get_by_subdir(memory_subdir, preload_knowledge=False)
258
+ id = (await memory.update_documents([doc]))[0]
259
+ doc = memory.get_document_by_id(id)
260
+ formatted_doc = self._format_memory_for_dashboard(doc) if doc else None
261
+
262
+ return {"success": formatted_doc is not None, "memory": formatted_doc}
263
+ except Exception as e:
264
+ return {"success": False, "error": str(e), "memory": None}
python/helpers/memory.py
+18
-4
@@ -89,9 +89,14 @@ class Memory:
89
)
90
91
@staticmethod
92
- async def get_by_subdir(memory_subdir: str, log_item: LogItem | None = None, preload_knowledge: bool = True):
92
+ async def get_by_subdir(
93
+ memory_subdir: str,
94
+ log_item: LogItem | None = None,
95
+ preload_knowledge: bool = True,
96
+ ):
97
if not Memory.index.get(memory_subdir):
98
import initialize
99
+
100
agent_config = initialize.initialize_agent()
101
model_config = agent_config.embeddings_model
102
db, _created = Memory.initialize(
@@ -312,6 +317,9 @@ class Memory:
317
318
return index
319
320
+ def get_document_by_id(self, id: str) -> Document | None:
321
+ return self.db.get_by_ids(id)[0]
322
+
323
async def search_similarity_threshold(
324
self, query: str, limit: int, threshold: float, filter: str = ""
325
):
@@ -392,15 +400,21 @@ class Memory:
400
self._save_db() # persist
401
return ids
402
403
+ async def update_documents(self, docs: list[Document]):
404
+ ids = [doc.metadata["id"] for doc in docs]
405
+ await self.db.adelete(ids=ids) # delete originals
406
+ ins = await self.db.aadd_documents(documents=docs, ids=ids) # add updated
407
+ self._save_db() # persist
408
+ return ins
409
+
410
def _save_db(self):
411
Memory._save_db_file(self.db, self.memory_subdir)
412
413
def _generate_doc_id(self):
414
while True:
400
- doc_id = guids.generate_id(10) # random ID
401
- if not self.db.get_by_ids(doc_id): # check if exists
415
+ doc_id = guids.generate_id(10) # random ID
416
+ if not self.db.get_by_ids(doc_id): # check if exists
417
return doc_id
403
-
418
419
@staticmethod
420
def _save_db_file(db: MyFaiss, memory_subdir: str):
webui/components/settings/memory/memory-dashboard-store.js
+184
-88
@@ -26,13 +26,15 @@ const memoryDashboardStore = {
26
// Memory subdirectories
27
memorySubdirs: [],
28
selectedMemorySubdir: "default",
29
- memoryInitialized: {}, // Track which subdirs have been initialized
29
+ memoryInitialized: {}, // Track which subdirs have been initialized
30
31
// Search and filters
32
searchQuery: "",
33
areaFilter: "",
34
- threshold: parseFloat(localStorage.getItem('memoryDashboard_threshold') || '0.6'),
35
- limit: parseInt(localStorage.getItem('memoryDashboard_limit') || '1000'),
34
+ threshold: parseFloat(
35
+ localStorage.getItem("memoryDashboard_threshold") || "0.6"
36
+ ),
37
+ limit: parseInt(localStorage.getItem("memoryDashboard_limit") || "1000"),
38
39
// Stats
40
totalCount: 0,
@@ -43,40 +45,55 @@ const memoryDashboardStore = {
45
46
// Memory detail modal (standard modal approach)
47
detailMemory: null,
48
+ editMode: false,
49
+ editMemoryBackup: null,
50
51
// Polling
52
pollingInterval: null,
53
pollingEnabled: false,
54
51
- init(){
55
+ async openModal() {
56
+ await openModal("settings/memory/memory-dashboard.html");
57
+ },
58
+
59
+ init() {
60
this.initialize();
61
},
62
63
+ async onOpen() {
64
+ await this.getCurrentMemorySubdir();
65
+ await this.loadMemorySubdirs();
66
+ await this.searchMemories();
67
+ // Start polling for live updates as soon as dashboard is open
68
+ this.startPolling();
69
+ },
70
+
71
async initialize() {
72
// Reset state when opening (but keep directory from context)
73
this.currentPage = 1;
74
this.searchQuery = "";
75
this.areaFilter = "";
76
61
- // Get current memory subdirectory from application context
62
- await this.getCurrentMemorySubdir();
77
+ // // Get current memory subdirectory from application context
78
+ // await this.getCurrentMemorySubdir();
79
64
- await this.loadMemorySubdirs();
80
+ // await this.loadMemorySubdirs();
81
66
- // Automatically search with selected subdirectory
67
- if (this.selectedMemorySubdir) {
68
- await this.searchMemories();
69
- }
82
+ // // Automatically search with selected subdirectory
83
+ // if (this.selectedMemorySubdir) {
84
+ // await this.searchMemories();
85
+ // }
86
71
- // Start polling for live updates as soon as dashboard is open
72
- this.startPolling();
87
+ // // Start polling for live updates as soon as dashboard is open
88
+ // this.startPolling();
89
},
90
91
async getCurrentMemorySubdir() {
92
try {
93
// Try to get current memory subdirectory from the backend
94
const response = await API.callJsonApi("memory_dashboard", {
79
- action: "get_current_memory_subdir"
95
+ action: "get_current_memory_subdir",
96
+ context_id: getContext(),
97
});
98
99
if (response.success && response.memory_subdir) {
@@ -97,14 +114,14 @@ const memoryDashboardStore = {
114
115
try {
116
const response = await API.callJsonApi("memory_dashboard", {
100
- action: "get_memory_subdirs"
117
+ action: "get_memory_subdirs",
118
});
119
103
- if (response.success) {
120
+ if (response.success) {
121
let subdirs = response.subdirs || ["default"];
122
123
// Sort alphabetically but ensure "default" is always first
107
- subdirs = subdirs.filter(dir => dir !== "default").sort();
124
+ subdirs = subdirs.filter((dir) => dir !== "default").sort();
125
if (response.subdirs && response.subdirs.includes("default")) {
126
subdirs.unshift("default");
127
} else {
@@ -137,8 +154,11 @@ const memoryDashboardStore = {
154
155
async searchMemories(silent = false) {
156
// Save limit to localStorage for persistence
140
- localStorage.setItem('memoryDashboard_limit', this.limit.toString());
141
- localStorage.setItem('memoryDashboard_threshold', this.threshold.toString());
157
+ localStorage.setItem("memoryDashboard_limit", this.limit.toString());
158
+ localStorage.setItem(
159
+ "memoryDashboard_threshold",
160
+ this.threshold.toString()
161
+ );
162
163
if (!silent) {
164
this.loading = true;
@@ -158,15 +178,15 @@ const memoryDashboardStore = {
178
area: this.areaFilter,
179
search: this.searchQuery,
180
limit: this.limit,
161
- threshold: this.threshold
181
+ threshold: this.threshold,
182
});
183
164
- if (response.success) {
184
+ if (response.success) {
185
// Preserve existing selections when updating memories during polling
186
const existingSelections = {};
187
if (silent && this.memories) {
188
// Build a map of existing selections by memory ID
169
- this.memories.forEach(memory => {
189
+ this.memories.forEach((memory) => {
190
if (memory.selected) {
191
existingSelections[memory.id] = true;
192
}
@@ -174,9 +194,9 @@ const memoryDashboardStore = {
194
}
195
196
// Add selected property to each memory item for mass selection
177
- this.memories = (response.memories || []).map(memory => ({
197
+ this.memories = (response.memories || []).map((memory) => ({
198
...memory,
179
- selected: existingSelections[memory.id] || false
199
+ selected: existingSelections[memory.id] || false,
200
}));
201
this.totalCount = response.total_count || 0;
202
this.totalDbCount = response.total_db_count || 0;
@@ -268,7 +288,7 @@ const memoryDashboardStore = {
288
289
// Mass selection
290
get selectedMemories() {
271
- return this.memories.filter(memory => memory.selected);
291
+ return this.memories.filter((memory) => memory.selected);
292
},
293
294
get selectedCount() {
@@ -276,24 +296,25 @@ const memoryDashboardStore = {
296
},
297
298
get allSelected() {
279
- return this.memories.length > 0 && this.memories.every(memory => memory.selected);
299
+ return (
300
+ this.memories.length > 0 &&
301
+ this.memories.every((memory) => memory.selected)
302
+ );
303
},
304
305
get someSelected() {
283
- return this.memories.some(memory => memory.selected);
306
+ return this.memories.some((memory) => memory.selected);
307
},
308
309
toggleSelectAll() {
310
const shouldSelectAll = !this.allSelected;
288
- this.memories.forEach(memory => {
311
+ this.memories.forEach((memory) => {
312
memory.selected = shouldSelectAll;
313
});
314
},
315
293
-
294
-
316
clearSelection() {
296
- this.memories.forEach(memory => {
317
+ this.memories.forEach((memory) => {
318
memory.selected = false;
319
});
320
},
@@ -311,17 +332,23 @@ const memoryDashboardStore = {
332
const response = await API.callJsonApi("memory_dashboard", {
333
action: "bulk_delete",
334
memory_subdir: this.selectedMemorySubdir,
314
- memory_ids: selectedMemories.map(memory => memory.id)
335
+ memory_ids: selectedMemories.map((memory) => memory.id),
336
});
337
338
if (response.success) {
318
- justToast(`Successfully deleted ${selectedMemories.length} memories`, "success");
339
+ justToast(
340
+ `Successfully deleted ${selectedMemories.length} memories`,
341
+ "success"
342
+ );
343
344
// Let polling refresh the data instead of manual manipulation
345
// Trigger an immediate refresh to get updated state from backend
346
await this.searchMemories(true); // silent refresh
347
} else {
324
- justToast(response.error || "Failed to delete selected memories", "error");
348
+ justToast(
349
+ response.error || "Failed to delete selected memories",
350
+ "error"
351
+ );
352
}
353
} catch (error) {
354
justToast(error.message || "Failed to delete selected memories", "error");
@@ -335,15 +362,22 @@ const memoryDashboardStore = {
362
let formatted = `=== Memory ID: ${memory.id} ===
363
Area: ${memory.area}
364
Timestamp: ${this.formatTimestamp(memory.timestamp)}
338
-Source: ${memory.knowledge_source ? 'Knowledge' : 'Conversation'}
339
-${memory.source_file ? `File: ${memory.source_file}` : ''}
340
-${memory.tags && memory.tags.length > 0 ? `Tags: ${memory.tags.join(', ')}` : ''}`;
365
+Source: ${memory.knowledge_source ? "Knowledge" : "Conversation"}
366
+${memory.source_file ? `File: ${memory.source_file}` : ""}
367
+${
368
+ memory.tags && memory.tags.length > 0 ? `Tags: ${memory.tags.join(", ")}` : ""
369
+}`;
370
371
// Add custom metadata if present
343
- if (memory.metadata && typeof memory.metadata === 'object' && Object.keys(memory.metadata).length > 0) {
344
- formatted += '\n\nMetadata:';
372
+ if (
373
+ memory.metadata &&
374
+ typeof memory.metadata === "object" &&
375
+ Object.keys(memory.metadata).length > 0
376
+ ) {
377
+ formatted += "\n\nMetadata:";
378
for (const [key, value] of Object.entries(memory.metadata)) {
346
- const displayValue = typeof value === 'object' ? JSON.stringify(value, null, 2) : value;
379
+ const displayValue =
380
+ typeof value === "object" ? JSON.stringify(value, null, 2) : value;
381
formatted += `\n${key}: ${displayValue}`;
382
}
383
}
@@ -359,10 +393,15 @@ ${memory.content_full}
393
const selectedMemories = this.selectedMemories;
394
if (selectedMemories.length === 0) return;
395
362
- const content = selectedMemories.map(memory => this.formatMemoryForCopy(memory)).join('\n');
396
+ const content = selectedMemories
397
+ .map((memory) => this.formatMemoryForCopy(memory))
398
+ .join("\n");
399
400
this.copyToClipboard(content);
365
- justToast(`Copied ${selectedMemories.length} memories with metadata to clipboard`, "success");
401
+ justToast(
402
+ `Copied ${selectedMemories.length} memories with metadata to clipboard`,
403
+ "success"
404
+ );
405
},
406
407
bulkExportMemories() {
@@ -373,7 +412,7 @@ ${memory.content_full}
412
export_timestamp: new Date().toISOString(),
413
memory_subdir: this.selectedMemorySubdir,
414
total_memories: selectedMemories.length,
376
- memories: selectedMemories.map(memory => ({
415
+ memories: selectedMemories.map((memory) => ({
416
id: memory.id,
417
area: memory.area,
418
timestamp: memory.timestamp,
@@ -381,18 +420,18 @@ ${memory.content_full}
420
tags: memory.tags || [],
421
knowledge_source: memory.knowledge_source,
422
source_file: memory.source_file || null,
384
- metadata: memory.metadata || {}
385
- }))
423
+ metadata: memory.metadata || {},
424
+ })),
425
};
426
427
const jsonString = JSON.stringify(exportData, null, 2);
389
- const blob = new Blob([jsonString], { type: 'application/json' });
428
+ const blob = new Blob([jsonString], { type: "application/json" });
429
const url = URL.createObjectURL(blob);
430
392
- const timestamp = new Date().toISOString().split('T')[0];
431
+ const timestamp = new Date().toISOString().split("T")[0];
432
const filename = `memories_${this.selectedMemorySubdir}_selected_${selectedMemories.length}_${timestamp}.json`;
433
395
- const a = document.createElement('a');
434
+ const a = document.createElement("a");
435
a.href = url;
436
a.download = filename;
437
document.body.appendChild(a);
@@ -400,12 +439,17 @@ ${memory.content_full}
439
document.body.removeChild(a);
440
URL.revokeObjectURL(url);
441
403
- justToast(`Exported ${selectedMemories.length} selected memories to ${filename}`, "success");
442
+ justToast(
443
+ `Exported ${selectedMemories.length} selected memories to ${filename}`,
444
+ "success"
445
+ );
446
},
447
448
// Memory detail modal (standard approach)
449
showMemoryDetails(memory) {
450
this.detailMemory = memory;
451
+ this.editMode = false;
452
+ this.editMemoryBackup = null;
453
// Use global modal system
454
openModal("settings/memory/memory-detail-modal.html");
455
},
@@ -427,25 +471,33 @@ ${memory.content_full}
471
472
if (compact) {
473
// For table display: MM/DD HH:mm
430
- return date.toLocaleDateString("en-US", {
431
- month: "2-digit",
432
- day: "2-digit",
433
- }) + " " + date.toLocaleTimeString("en-US", {
434
- hour12: false,
435
- hour: "2-digit",
436
- minute: "2-digit"
437
- });
474
+ return (
475
+ date.toLocaleDateString("en-US", {
476
+ month: "2-digit",
477
+ day: "2-digit",
478
+ }) +
479
+ " " +
480
+ date.toLocaleTimeString("en-US", {
481
+ hour12: false,
482
+ hour: "2-digit",
483
+ minute: "2-digit",
484
+ })
485
+ );
486
} else {
487
// For details: Full format
440
- return date.toLocaleDateString("en-US", {
441
- year: "numeric",
442
- month: "long",
443
- day: "numeric",
444
- }) + " at " + date.toLocaleTimeString("en-US", {
445
- hour12: true,
446
- hour: "numeric",
447
- minute: "2-digit"
448
- });
488
+ return (
489
+ date.toLocaleDateString("en-US", {
490
+ year: "numeric",
491
+ month: "long",
492
+ day: "numeric",
493
+ }) +
494
+ " at " +
495
+ date.toLocaleTimeString("en-US", {
496
+ hour12: true,
497
+ hour: "numeric",
498
+ minute: "2-digit",
499
+ })
500
+ );
501
}
502
},
503
@@ -456,22 +508,25 @@ ${memory.content_full}
508
509
getAreaColor(area) {
510
const colors = {
459
- "main": "#3b82f6",
460
- "fragments": "#10b981",
461
- "solutions": "#8b5cf6",
462
- "instruments": "#f59e0b"
511
+ main: "#3b82f6",
512
+ fragments: "#10b981",
513
+ solutions: "#8b5cf6",
514
+ instruments: "#f59e0b",
515
};
516
return colors[area] || "#6c757d";
517
},
518
519
copyToClipboard(text) {
520
if (navigator.clipboard && window.isSecureContext) {
469
- navigator.clipboard.writeText(text).then(() => {
470
- justToast("Copied to clipboard!", "success");
471
- }).catch(err => {
472
- console.error("Clipboard copy failed:", err);
473
- this.fallbackCopyToClipboard(text);
474
- });
521
+ navigator.clipboard
522
+ .writeText(text)
523
+ .then(() => {
524
+ justToast("Copied to clipboard!", "success");
525
+ })
526
+ .catch((err) => {
527
+ console.error("Clipboard copy failed:", err);
528
+ this.fallbackCopyToClipboard(text);
529
+ });
530
} else {
531
this.fallbackCopyToClipboard(text);
532
}
@@ -487,7 +542,7 @@ ${memory.content_full}
542
textArea.focus();
543
textArea.select();
544
try {
490
- document.execCommand('copy');
545
+ document.execCommand("copy");
546
justToast("Copied to clipboard!", "success");
547
} catch (err) {
548
console.error("Fallback clipboard copy failed:", err);
@@ -496,19 +551,24 @@ ${memory.content_full}
551
document.body.removeChild(textArea);
552
},
553
499
- async deleteMemory(memory) {
500
- if (!confirm(`Are you sure you want to delete this memory from ${memory.area}?`)) {
554
+ async deleteMemory(memory) {
555
+ if (
556
+ !confirm(
557
+ `Are you sure you want to delete this memory from ${memory.area}?`
558
+ )
559
+ ) {
560
return;
561
}
562
563
try {
564
// Check if this is the memory currently being viewed in detail modal
506
- const isViewingThisMemory = this.detailMemory && this.detailMemory.id === memory.id;
565
+ const isViewingThisMemory =
566
+ this.detailMemory && this.detailMemory.id === memory.id;
567
568
const response = await API.callJsonApi("memory_dashboard", {
569
action: "delete",
570
memory_subdir: this.selectedMemorySubdir,
511
- memory_id: memory.id
571
+ memory_id: memory.id,
572
});
573
574
if (response.success) {
@@ -545,22 +605,24 @@ ${memory.content_full}
605
total_memories: this.memories.length,
606
search_query: this.searchQuery,
607
area_filter: this.areaFilter,
548
- memories: this.memories.map(memory => ({
608
+ memories: this.memories.map((memory) => ({
609
id: memory.id,
610
area: memory.area,
611
timestamp: memory.timestamp,
612
content: memory.content_full,
553
- metadata: memory.metadata
554
- }))
613
+ metadata: memory.metadata,
614
+ })),
615
};
616
617
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
558
- type: 'application/json'
618
+ type: "application/json",
619
});
620
const url = URL.createObjectURL(blob);
561
- const a = document.createElement('a');
621
+ const a = document.createElement("a");
622
a.href = url;
563
- a.download = `memory-export-${this.selectedMemorySubdir}-${new Date().toISOString().split('T')[0]}.json`;
623
+ a.download = `memory-export-${this.selectedMemorySubdir}-${
624
+ new Date().toISOString().split("T")[0]
625
+ }.json`;
626
document.body.appendChild(a);
627
a.click();
628
document.body.removeChild(a);
@@ -573,7 +635,7 @@ ${memory.content_full}
635
}
636
},
637
576
- startPolling() {
638
+ startPolling() {
639
if (!this.pollingEnabled || this.pollingInterval) {
640
return; // Already polling or disabled
641
}
@@ -605,9 +667,43 @@ ${memory.content_full}
667
this.areasCount = {};
668
this.message = null;
669
this.currentPage = 1;
670
+ this.editMemoryBackup;
671
+ },
672
+
673
+ enableEditMode() {
674
+ this.editMode = true;
675
+ this.editMemoryBackup = JSON.stringify(this.detailMemory); // store backup
676
},
677
678
+ cancelEditMode() {
679
+ this.editMode = false;
680
+ this.detailMemory = JSON.parse(this.editMemoryBackup); // restore backup
681
+ },
682
+
683
+ async confirmEditMode() {
684
+ try {
685
+
686
+ const response = await API.callJsonApi("memory_dashboard", {
687
+ action: "update",
688
+ memory_subdir: this.selectedMemorySubdir,
689
+ original: JSON.parse(this.editMemoryBackup),
690
+ edited: this.detailMemory,
691
+ });
692
693
+ if(response.success){
694
+ justToast("Memory updated successfully", "success");
695
+ await this.searchMemories(true); // silent refresh
696
+ }else{
697
+ justToast(`Failed to update memory: ${response.error}`, "error");
698
+ }
699
+
700
+ this.editMode = false;
701
+ this.editMemoryBackup = null; // discard backup
702
+ } catch (error) {
703
+ console.error("Error confirming edit mode:", error);
704
+ justToast("Failed to save memory changes.", "error");
705
+ }
706
+ },
707
};
708
709
const store = createStore("memoryDashboardStore", memoryDashboardStore);
webui/components/settings/memory/memory-dashboard.html
+1
-1
@@ -10,7 +10,7 @@
10
<body>
11
<div x-data>
12
<template x-if="$store.memoryDashboardStore">
13
- <div x-create="$store.memoryDashboardStore.searchMemories()"
13
+ <div x-create="$store.memoryDashboardStore.onOpen()"
14
x-destroy="$store.memoryDashboardStore.cleanup()" class="memory-dashboard">
15
16
<!-- Search and Filters -->
webui/components/settings/memory/memory-detail-modal.html
+43
-40
@@ -265,13 +265,43 @@
265
</div>
266
</div>
267
<div class="header-actions">
268
- <button class="btn-action-header copy" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.formatMemoryForCopy($store.memoryDashboardStore.detailMemory))"
269
- title="Copy Complete Memory with Metadata">
270
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
271
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
272
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
273
- </svg>
274
- </button>
268
+ <!-- View Mode Buttons -->
269
+ <template x-if="!$store.memoryDashboardStore.editMode">
270
+ <div style="display: flex; gap: 0.5rem;">
271
+
272
+ <button class="btn-action-header copy-all" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.formatMemoryForCopy($store.memoryDashboardStore.detailMemory))"
273
+ title="Copy Complete Memory with Metadata">
274
+ <span class="material-symbols-outlined">copy_all</span> All
275
+ </button>
276
+ <button class="btn-action-header copy-content" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.detailMemory?.content_full)"
277
+ title="Copy Content Only">
278
+ <span class="material-symbols-outlined">content_copy</span> Content
279
+ </button>
280
+ <div style="width:2em"></div>
281
+ <button class="btn-action-header edit" @click="$store.memoryDashboardStore.enableEditMode()"
282
+ title="Edit Memory">
283
+ <span class="material-symbols-outlined">edit_document</span>
284
+ </button>
285
+ <button class="btn-action-header delete danger" @click="$store.memoryDashboardStore.deleteMemory($store.memoryDashboardStore.detailMemory)"
286
+ title="Delete Memory">
287
+ <span class="material-symbols-outlined">delete</span>
288
+ </button>
289
+ </div>
290
+ </template>
291
+ <!-- Edit Mode Buttons -->
292
+ <template x-if="$store.memoryDashboardStore.editMode">
293
+ <div style="display: flex; gap: 0.5rem;">
294
+ <button class="btn-action-header cancel" @click="$store.memoryDashboardStore.cancelEditMode()"
295
+ title="Cancel Edit">
296
+ <span class="material-symbols-outlined">close</span>
297
+ </button>
298
+ <button class="btn-action-header confirm" @click="$store.memoryDashboardStore.confirmEditMode()"
299
+ title="Confirm Changes">
300
+ <span class="material-symbols-outlined">check</span>
301
+ </button>
302
+
303
+ </div>
304
+ </template>
305
</div>
306
</div>
307
@@ -281,7 +311,12 @@
311
<div class="content-section-main">
312
<div class="content-block">
313
<h4>Memory Content</h4>
284
- <div class="memory-content-display" x-text="$store.memoryDashboardStore.detailMemory?.content_full"></div>
314
+ <template x-if="!$store.memoryDashboardStore.editMode">
315
+ <div class="memory-content-display" x-text="$store.memoryDashboardStore.detailMemory?.content_full"></div>
316
+ </template>
317
+ <template x-if="$store.memoryDashboardStore.editMode">
318
+ <textarea class="memory-content-display" style="width: 100%; height: 300px;" x-model="$store.memoryDashboardStore.detailMemory.content_full"></textarea>
319
+ </template>
320
</div>
321
322
<!-- Tags Section -->
@@ -343,38 +378,6 @@
378
</template>
379
</div>
380
346
- <!-- Actions -->
347
- <div class="metadata-group">
348
- <h5>Actions</h5>
349
- <div class="metadata-actions">
350
- <button class="btn slim" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.formatMemoryForCopy($store.memoryDashboardStore.detailMemory))"
351
- title="Copy Complete Memory with Metadata">
352
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
353
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
354
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
355
- </svg>
356
- Copy All
357
- </button>
358
- <button class="btn slim" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.detailMemory?.content_full)"
359
- title="Copy Content Only">
360
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
361
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
362
- <polyline points="14,2 14,8 20,8"></polyline>
363
- </svg>
364
- Copy Content
365
- </button>
366
- <button class="btn slim danger" @click="$store.memoryDashboardStore.deleteMemory($store.memoryDashboardStore.detailMemory)"
367
- title="Delete Memory">
368
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
369
- <polyline points="3,6 5,6 21,6"></polyline>
370
- <path d="M19,6V20a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2V6"></path>
371
- <line x1="10" y1="11" x2="10" y2="17"></line>
372
- <line x1="14" y1="11" x2="14" y2="17"></line>
373
- </svg>
374
- Delete
375
- </button>
376
- </div>
377
- </div>
381
</div>
382
</div>
383
</div>
webui/index.html
+370
-221
@@ -32,7 +32,7 @@
32
<!-- Pre-initialize schedulerSettings to ensure Alpine doesn't miss it -->
33
<script>
34
// Pre-define schedulerSettings skeleton to ensure it's available to Alpine
35
- globalThis.schedulerSettings = function() {
35
+ globalThis.schedulerSettings = function () {
36
return {
37
tasks: [],
38
isLoading: true,
@@ -114,8 +114,7 @@
114
115
<!-- KaTeX javascript -->
116
<script defer src="vendor/katex/katex.min.js" crossorigin="anonymous"></script>
117
- <script defer src="vendor/katex/katex.auto-render.min.js"
118
- crossorigin="anonymous"></script>
117
+ <script defer src="vendor/katex/katex.auto-render.min.js" crossorigin="anonymous"></script>
118
119
<!-- QR Code Library -->
120
<script src="vendor/qrcode.min.js"></script>
@@ -174,6 +173,9 @@
173
d="m50.301 66.5c-9 0-16.398-7.3008-16.398-16.398 0-9.1016 7.3008-16.398 16.398-16.398 9.1016 0 16.398 7.3008 16.398 16.398 0 9.0977-7.3984 16.398-16.398 16.398zm0-25.5c-5 0-9.1016 4.1016-9.1016 9.1016s4.1016 9.1016 9.1016 9.1016 9.1016-4.1016 9.1016-9.1016c-0.003906-5-4.1016-9.1016-9.1016-9.1016z">
174
</path>
175
</svg>Settings</button>
176
+ <button class="config-button" id="memory-dash"
177
+ @click="openModal('settings/memory/memory-dashboard.html');">Memory</button>
178
+
179
180
</div>
181
@@ -194,7 +196,8 @@
196
<li>
197
<div :class="{'chat-list-button': true, 'font-bold': context.id === selected}"
198
@click="selected = context.id; selectChat(context.id)">
197
- <span class="chat-name" :title="context.name ? context.name : 'Chat #' + context.no"
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>
@@ -214,8 +217,7 @@
217
openTaskDetail(taskId) {
218
globalThis.openTaskDetail(taskId);
219
}
217
- }"
218
- style="display: none;">
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">
@@ -225,32 +227,40 @@
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 -->
228
- <span class="task-name" :title="(task.task_name || `Task #${task.no}`) + ' (' + (task.name || `Chat #${task.no}`) + ')'"
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"
235
- :class="task.state ? `scheduler-status-${task.state}` : 'scheduler-status-idle'"
236
- x-text="task.state || 'idle'"></span>
238
+ :class="task.state ? `scheduler-status-${task.state}` : 'scheduler-status-idle'"
239
+ x-text="task.state || 'idle'"></span>
240
<!-- Action buttons -->
238
- <button class="edit-button"
239
- @click.stop="openTaskDetail(task.id)"
240
- title="View task details" style="margin-left: auto; margin-right: 5px;">
241
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 512 512" fill="var(--color-primary)" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
242
- <path d="M256 0c70.69 0 134.69 28.66 181.02 74.98C483.34 121.3 512 185.31 512 256c0 70.69-28.66 134.7-74.98 181.02C390.69 483.34 326.69 512 256 512c-70.69 0-134.69-28.66-181.02-74.98C28.66 390.69 0 326.69 0 256c0-70.69 28.66-134.69 74.98-181.02C121.31 28.66 185.31 0 256 0zm-9.96 161.03c0-4.28.76-8.26 2.27-11.91 1.5-3.63 3.77-6.94 6.79-9.91 3-2.95 6.29-5.2 9.84-6.7 3.57-1.5 7.41-2.28 11.52-2.28 4.12 0 7.96.78 11.49 2.27 3.54 1.51 6.78 3.76 9.75 6.73 2.95 2.97 5.16 6.26 6.64 9.91 1.49 3.63 2.22 7.61 2.22 11.89 0 4.17-.73 8.08-2.21 11.69-1.48 3.6-3.68 6.94-6.65 9.97-2.94 3.03-6.18 5.32-9.72 6.84-3.54 1.51-7.38 2.29-11.52 2.29-4.22 0-8.14-.76-11.75-2.26-3.58-1.51-6.86-3.79-9.83-6.79-2.94-3.02-5.16-6.34-6.63-9.97-1.48-3.62-2.21-7.54-2.21-11.77zm13.4 178.16c-1.11 3.97-3.35 11.76 3.3 11.76 1.44 0 3.27-.81 5.46-2.4 2.37-1.71 5.09-4.31 8.13-7.75 3.09-3.5 6.32-7.65 9.67-12.42 3.33-4.76 6.84-10.22 10.49-16.31.37-.65 1.23-.87 1.89-.48l12.36 9.18c.6.43.73 1.25.35 1.86-5.69 9.88-11.44 18.51-17.26 25.88-5.85 7.41-11.79 13.57-17.8 18.43l-.1.06c-6.02 4.88-12.19 8.55-18.51 11.01-17.58 6.81-45.36 5.7-53.32-14.83-5.02-12.96-.9-27.69 3.06-40.37l19.96-60.44c1.28-4.58 2.89-9.62 3.47-14.33.97-7.87-2.49-12.96-11.06-12.96h-17.45c-.76 0-1.38-.62-1.38-1.38l.08-.48 4.58-16.68c.16-.62.73-1.04 1.35-1.02l89.12-2.79c.76-.03 1.41.57 1.44 1.33l-.07.43-37.76 124.7zm158.3-244.93c-41.39-41.39-98.58-67-161.74-67-63.16 0-120.35 25.61-161.74 67-41.39 41.39-67 98.58-67 161.74 0 63.16 25.61 120.35 67 161.74 41.39 41.39 98.58 67 161.74 67 63.16 0 120.35-25.61 161.74-67 41.39-41.39 67-98.58 67-161.74 0-63.16-25.61-120.35-67-161.74z"/>
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>
245
- <button class="edit-button"
246
- @click.stop="resetChat(task.id)"
247
- style="margin-right: 5px;"
248
- title="Clear task chat">
249
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 122.88 121.1" fill="var(--color-primary)" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
250
- <path d="M62.89,56.03c1.11-0.35,2.34-0.25,3.72,0.37l10.4,7.87c2.26,1.71,4.24,3.78,2.73,6.9 c-0.51,1.06-1.4,2.1-2.38,3.49l-0.53,0.75c-1.97,2.8-2.61,2-5.71,1.83c0.56,13.37,1.75,27.82-2.64,40.88 c-0.87,2.7-3.32,3.44-6.95,2.71l-6.1-2.03c4.11-6.14,6.16-13.85,6.44-22.89c-3.46,8.58-6.8,16.96-10.68,20.86l-6.28-2.08 c0.61-3.05,1.05-5.43,0.35-6.9l-4.07,4.24l-9.33-5.77c6.36-3.36,11.62-7.87,15.6-13.73c-6.69,5.01-12.76,8.1-18.14,8.99 c-2.75,0.83-4.49,0.35-5.16-1.53c-0.48-1.34-0.05-1.77,0.81-2.86c1.11-1.41,2.61-2.67,4.35-3.79c-3.13,1.1-4.64,0.95-6.37,1.51 c-4.9,1.59-9.94-1.86-8.26-6.9c1.07-3.23,3.54-3.09,6.67-4.07l5.42-1.69c-5.19,0.28-10.32,0.45-15.02-0.25 c-5.4-0.8-5.31-0.99-8.24-5.38c-3.94-5.91-6.25-11.45,2.52-9.16c16.73,3.18,33.56,5.34,51.25-0.98c-0.76-1.32-0.9-2.57-0.5-3.73 C57.37,60.94,61.13,56.58,62.89,56.03L62.89,56.03z M113.8,2.42L74.45,51.53c-4.71,6.68,3.2,11.91,8.39,5.64l39.2-49.27 C125.12,1.86,119.13-3.16,113.8,2.42L113.8,2.42z"/>
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>
253
- <button title="Delete task" class="edit-button" @click.stop="deleteTaskGlobal(task.id)">X</button>
262
+ <button title="Delete task" class="edit-button"
263
+ @click.stop="deleteTaskGlobal(task.id)">X</button>
264
265
</div>
266
</div>
@@ -400,7 +410,8 @@
410
d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" />
411
</svg>
412
</label>
403
- <input type="file" id="file-input" accept="*" multiple style="display: none" @change="$store.chatAttachments.handleFileUpload($event)">
413
+ <input type="file" id="file-input" accept="*" multiple style="display: none"
414
+ @change="$store.chatAttachments.handleFileUpload($event)">
415
416
<div x-show="showTooltip" class="tooltip">
417
Add attachments to the message
@@ -412,9 +423,11 @@
423
<textarea id="chat-input" placeholder="Type your message here..." rows="1"></textarea>
424
425
<!-- Expand button inside the textarea container -->
415
- <button id="expand-button" @click="$store.fullScreenInputModal.openModal()" aria-label="Expand input">
426
+ <button id="expand-button" @click="$store.fullScreenInputModal.openModal()"
427
+ aria-label="Expand input">
428
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
417
- <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
429
+ <path
430
+ d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
431
</svg>
432
</button>
433
</div>
@@ -431,8 +444,7 @@
444
445
<!-- Microphone button -->
446
<button class="chat-button mic-inactive" id="microphone-button"
434
- aria-label="Start/Stop recording"
435
- @click="$store.speech.handleMicrophoneClick()"
447
+ aria-label="Start/Stop recording" @click="$store.speech.handleMicrophoneClick()"
448
x-effect="$store.speech.updateMicrophoneButtonUI()">
449
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 18" fill="currentColor">
450
<path
@@ -540,30 +552,18 @@
552
<!-- Tab Navigation -->
553
<div class="settings-tabs-container">
554
<div class="settings-tabs">
543
- <div class="settings-tab"
544
- :class="{'active': activeTab === 'agent'}"
545
- @click="switchTab('agent')"
546
- title="Agent Settings">Agent Settings</div>
547
- <div class="settings-tab"
548
- :class="{'active': activeTab === 'external'}"
549
- @click="switchTab('external')"
550
- title="External Services">External Services</div>
551
- <div class="settings-tab"
552
- :class="{'active': activeTab === 'mcp'}"
553
- @click="switchTab('mcp')"
554
- title="MCP">MCP/A2A</div>
555
- <div class="settings-tab"
556
- :class="{'active': activeTab === 'developer'}"
557
- @click="switchTab('developer')"
558
- title="Developer">Developer</div>
559
- <div class="settings-tab"
560
- :class="{'active': activeTab === 'scheduler'}"
561
- @click="switchTab('scheduler')"
562
- title="Task Scheduler">Task Scheduler</div>
563
- <div class="settings-tab"
564
- :class="{'active': activeTab === 'backup'}"
565
- @click="switchTab('backup')"
566
- title="Backup & Restore">Backup & Restore</div>
555
+ <div class="settings-tab" :class="{'active': activeTab === 'agent'}"
556
+ @click="switchTab('agent')" title="Agent Settings">Agent Settings</div>
557
+ <div class="settings-tab" :class="{'active': activeTab === 'external'}"
558
+ @click="switchTab('external')" title="External Services">External Services</div>
559
+ <div class="settings-tab" :class="{'active': activeTab === 'mcp'}"
560
+ @click="switchTab('mcp')" title="MCP">MCP/A2A</div>
561
+ <div class="settings-tab" :class="{'active': activeTab === 'developer'}"
562
+ @click="switchTab('developer')" title="Developer">Developer</div>
563
+ <div class="settings-tab" :class="{'active': activeTab === 'scheduler'}"
564
+ @click="switchTab('scheduler')" title="Task Scheduler">Task Scheduler</div>
565
+ <div class="settings-tab" :class="{'active': activeTab === 'backup'}"
566
+ @click="switchTab('backup')" title="Backup & Restore">Backup & Restore</div>
567
</div>
568
</div>
569
@@ -574,8 +574,7 @@
574
<template x-for="(section, index) in filteredSections" :key="section.title">
575
<li>
576
<a :href="'#section' + (index + 1)">
577
- <img :src="'/public/' + section.id +'.svg'"
578
- :alt="section.title">
577
+ <img :src="'/public/' + section.id +'.svg'" :alt="section.title">
578
<span x-text="section.title"></span>
579
</a>
580
</li>
@@ -595,11 +594,14 @@
594
<div class="section-title" x-text="section.title"></div>
595
<div class="section-description" x-html="section.description"></div>
596
598
- <template x-for="(field, fieldIndex) in section.fields.filter(f => !f.hidden)" :key="fieldIndex">
597
+ <template x-for="(field, fieldIndex) in section.fields.filter(f => !f.hidden)"
598
+ :key="fieldIndex">
599
<div :class="{'field': true, 'field-full': field.type === 'textarea'}">
600
<div class="field-label" x-show="field.title || field.description">
601
- <div class="field-title" x-text="field.title" x-show="field.title"></div>
602
- <div class="field-description" x-html="field.description || ''" x-show="field.description"></div>
601
+ <div class="field-title" x-text="field.title" x-show="field.title">
602
+ </div>
603
+ <div class="field-description" x-html="field.description || ''"
604
+ x-show="field.description"></div>
605
</div>
606
607
<div class="field-control">
@@ -614,18 +616,16 @@
616
<template x-if="field.type === 'number'">
617
<input type="number" :class="field.classes" :value="field.value"
618
:readonly="field.readonly === true"
617
- @input="field.value = $event.target.value"
618
- :min="field.min" :max="field.max" :step="field.step">
619
+ @input="field.value = $event.target.value" :min="field.min"
620
+ :max="field.max" :step="field.step">
621
</template>
622
623
624
<!-- Password field -->
625
<template x-if="field.type === 'password'">
626
<input type="password" :class="field.classes" :value="field.value"
625
- :readonly="field.readonly === true"
626
- :id="field.id"
627
- autocomplete="off"
628
- @input="field.value = $event.target.value">
627
+ :readonly="field.readonly === true" :id="field.id"
628
+ autocomplete="off" @input="field.value = $event.target.value">
629
</template>
630
631
<!-- Textarea field -->
@@ -633,8 +633,7 @@
633
<textarea :class="field.classes" :value="field.value"
634
:readonly="field.readonly === true"
635
@input="field.value = $event.target.value"
636
- :style="field.style"
637
- ></textarea>
636
+ :style="field.style"></textarea>
637
</template>
638
639
<!-- Switch field -->
@@ -707,10 +706,10 @@
706
</ul>
707
</nav>
708
710
- <div id="section-task-scheduler" class="section"
711
- x-data="schedulerSettings">
709
+ <div id="section-task-scheduler" class="section" x-data="schedulerSettings">
710
<div class="section-title">Task Scheduler</div>
713
- <div class="section-description">Manage scheduled tasks and automated processes for Agent Zero.</div>
711
+ <div class="section-description">Manage scheduled tasks and automated processes for
712
+ Agent Zero.</div>
713
714
<!-- Create Task Form -->
715
<div class="scheduler-form" x-show="isCreating">
@@ -731,7 +730,8 @@
730
<div class="scheduler-form-field">
731
<div class="label-help-wrapper">
732
<label class="scheduler-form-label">Task Name</label>
734
- <div class="scheduler-form-help">A unique name to identify this task</div>
733
+ <div class="scheduler-form-help">A unique name to identify this task
734
+ </div>
735
</div>
736
<input type="text" x-model="editingTask.name" placeholder="Enter task name">
737
</div>
@@ -753,78 +753,102 @@
753
<div class="scheduler-form-field" x-show="isCreating">
754
<div class="label-help-wrapper">
755
<label class="scheduler-form-label">State</label>
756
- <div class="scheduler-form-help">Select the initial state of the task</div>
756
+ <div class="scheduler-form-help">Select the initial state of the task
757
+ </div>
758
</div>
759
<div>
760
<div class="scheduler-state-selector">
761
<span class="scheduler-status-badge scheduler-status-idle"
761
- :class="{'scheduler-status-selected': editingTask.state === 'idle'}"
762
- @click="editingTask.state = 'idle'">idle</span>
762
+ :class="{'scheduler-status-selected': editingTask.state === 'idle'}"
763
+ @click="editingTask.state = 'idle'">idle</span>
764
<span class="scheduler-status-badge scheduler-status-running"
764
- :class="{'scheduler-status-selected': editingTask.state === 'running'}"
765
- @click="editingTask.state = 'running'">running</span>
765
+ :class="{'scheduler-status-selected': editingTask.state === 'running'}"
766
+ @click="editingTask.state = 'running'">running</span>
767
<span class="scheduler-status-badge scheduler-status-disabled"
767
- :class="{'scheduler-status-selected': editingTask.state === 'disabled'}"
768
- @click="editingTask.state = 'disabled'">disabled</span>
768
+ :class="{'scheduler-status-selected': editingTask.state === 'disabled'}"
769
+ @click="editingTask.state = 'disabled'">disabled</span>
770
<span class="scheduler-status-badge scheduler-status-error"
770
- :class="{'scheduler-status-selected': editingTask.state === 'error'}"
771
- @click="editingTask.state = 'error'">error</span>
771
+ :class="{'scheduler-status-selected': editingTask.state === 'error'}"
772
+ @click="editingTask.state = 'error'">error</span>
773
</div>
774
<div class="scheduler-state-explanation">
774
- <span x-show="editingTask.state === 'idle'"><strong>idle</strong>: ready to run</span>
775
- <span x-show="editingTask.state === 'running'"><strong>running</strong>: currently executing</span>
776
- <span x-show="editingTask.state === 'disabled'"><strong>disabled</strong>: won't execute automatically</span>
777
- <span x-show="editingTask.state === 'error'"><strong>error</strong>: task encountered an error</span>
775
+ <span x-show="editingTask.state === 'idle'"><strong>idle</strong>:
776
+ ready to run</span>
777
+ <span
778
+ x-show="editingTask.state === 'running'"><strong>running</strong>:
779
+ currently executing</span>
780
+ <span
781
+ x-show="editingTask.state === 'disabled'"><strong>disabled</strong>:
782
+ won't execute automatically</span>
783
+ <span x-show="editingTask.state === 'error'"><strong>error</strong>:
784
+ task encountered an error</span>
785
</div>
786
</div>
787
</div>
788
789
<!-- Schedule (only for scheduled tasks) -->
783
- <div class="scheduler-form-field full-width" x-show="editingTask.type === 'scheduled'">
790
+ <div class="scheduler-form-field full-width"
791
+ x-show="editingTask.type === 'scheduled'">
792
<div class="label-help-wrapper">
793
<label class="scheduler-form-label">Schedule</label>
786
- <div class="scheduler-form-help">Cron schedule for automated execution (minute hour day month weekday)</div>
794
+ <div class="scheduler-form-help">Cron schedule for automated execution
795
+ (minute hour day month weekday)</div>
796
</div>
797
<div class="scheduler-schedule-builder">
798
<div class="scheduler-schedule-field">
799
<span class="scheduler-schedule-label">Minute</span>
791
- <input type="text" x-model="editingTask.schedule.minute" placeholder="*" maxlength="9">
800
+ <input type="text" x-model="editingTask.schedule.minute"
801
+ placeholder="*" maxlength="9">
802
</div>
803
<div class="scheduler-schedule-field">
804
<span class="scheduler-schedule-label">Hour</span>
795
- <input type="text" x-model="editingTask.schedule.hour" placeholder="*" maxlength="9">
805
+ <input type="text" x-model="editingTask.schedule.hour"
806
+ placeholder="*" maxlength="9">
807
</div>
808
<div class="scheduler-schedule-field">
809
<span class="scheduler-schedule-label">Day</span>
799
- <input type="text" x-model="editingTask.schedule.day" placeholder="*" maxlength="9">
810
+ <input type="text" x-model="editingTask.schedule.day"
811
+ placeholder="*" maxlength="9">
812
</div>
813
<div class="scheduler-schedule-field">
814
<span class="scheduler-schedule-label">Month</span>
803
- <input type="text" x-model="editingTask.schedule.month" placeholder="*" maxlength="9">
815
+ <input type="text" x-model="editingTask.schedule.month"
816
+ placeholder="*" maxlength="9">
817
</div>
818
<div class="scheduler-schedule-field">
819
<span class="scheduler-schedule-label">Weekday</span>
807
- <input type="text" x-model="editingTask.schedule.weekday" placeholder="*" maxlength="9">
820
+ <input type="text" x-model="editingTask.schedule.weekday"
821
+ placeholder="*" maxlength="9">
822
</div>
823
</div>
824
</div>
825
826
<!-- Plan (for planned tasks) -->
813
- <div class="scheduler-form-field full-width" x-show="editingTask.type === 'planned'">
827
+ <div class="scheduler-form-field full-width"
828
+ x-show="editingTask.type === 'planned'">
829
<div class="label-help-wrapper">
830
<label class="scheduler-form-label">Plan</label>
816
- <div class="scheduler-form-help">Specific execution times for this task</div>
831
+ <div class="scheduler-form-help">Specific execution times for this task
832
+ </div>
833
</div>
834
<div class="scheduler-plan-builder">
835
<div class="scheduler-plan-todo">
836
<span class="scheduler-plan-label">Upcoming Executions</span>
837
<div class="scheduler-todo-list">
822
- <template x-if="editingTask.plan && Array.isArray(editingTask.plan.todo) && editingTask.plan.todo.length > 0">
823
- <template x-for="(time, index) in editingTask.plan.todo" :key="index">
838
+ <template
839
+ x-if="editingTask.plan && Array.isArray(editingTask.plan.todo) && editingTask.plan.todo.length > 0">
840
+ <template x-for="(time, index) in editingTask.plan.todo"
841
+ :key="index">
842
<div class="scheduler-todo-item">
843
<span x-text="formatDate(time)"></span>
826
- <button @click.prevent="editingTask.plan.todo.splice(index, 1)" class="scheduler-todo-remove">
827
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
844
+ <button
845
+ @click.prevent="editingTask.plan.todo.splice(index, 1)"
846
+ class="scheduler-todo-remove">
847
+ <svg xmlns="http://www.w3.org/2000/svg"
848
+ width="16" height="16" viewBox="0 0 24 24"
849
+ fill="none" stroke="currentColor"
850
+ stroke-width="2" stroke-linecap="round"
851
+ stroke-linejoin="round">
852
<line x1="18" y1="6" x2="6" y2="18"></line>
853
<line x1="6" y1="6" x2="18" y2="18"></line>
854
</svg>
@@ -832,7 +856,8 @@
856
</div>
857
</template>
858
</template>
835
- <template x-if="!editingTask.plan || !Array.isArray(editingTask.plan.todo) || editingTask.plan.todo.length === 0">
859
+ <template
860
+ x-if="!editingTask.plan || !Array.isArray(editingTask.plan.todo) || editingTask.plan.todo.length === 0">
861
<div class="scheduler-empty-plan">
862
No scheduled execution times yet. Add one below.
863
</div>
@@ -885,7 +910,11 @@
910
input.value = '';
911
}
912
" class="scheduler-add-todo-button">
888
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px;">
913
+ <svg xmlns="http://www.w3.org/2000/svg" width="16"
914
+ height="16" viewBox="0 0 24 24" fill="none"
915
+ stroke="currentColor" stroke-width="2"
916
+ stroke-linecap="round" stroke-linejoin="round"
917
+ style="margin-right: 4px;">
918
<line x1="12" y1="5" x2="12" y2="19"></line>
919
<line x1="5" y1="12" x2="19" y2="12"></line>
920
</svg>
@@ -898,14 +927,18 @@
927
</div>
928
929
<!-- Token (for ad-hoc tasks) -->
901
- <div class="scheduler-form-field full-width" x-show="editingTask.type === 'adhoc'">
930
+ <div class="scheduler-form-field full-width"
931
+ x-show="editingTask.type === 'adhoc'">
932
<div class="label-help-wrapper">
933
<label class="scheduler-form-label">Token</label>
904
- <div class="scheduler-form-help">Token used to trigger this task externally</div>
934
+ <div class="scheduler-form-help">Token used to trigger this task
935
+ externally</div>
936
</div>
937
<div class="input-group">
907
- <input type="text" x-model="editingTask.token" placeholder="Token for ad-hoc task">
908
- <button class="scheduler-task-action" @click="editingTask.token = generateRandomToken()">
938
+ <input type="text" x-model="editingTask.token"
939
+ placeholder="Token for ad-hoc task">
940
+ <button class="scheduler-task-action"
941
+ @click="editingTask.token = generateRandomToken()">
942
Generate
943
</button>
944
</div>
@@ -915,27 +948,33 @@
948
<div class="scheduler-form-field full-width">
949
<div class="label-help-wrapper">
950
<label class="scheduler-form-label">System Prompt</label>
918
- <div class="scheduler-form-help">System-level instructions for the assistant</div>
951
+ <div class="scheduler-form-help">System-level instructions for the
952
+ assistant</div>
953
</div>
920
- <textarea x-model="editingTask.system_prompt" placeholder="System instructions for the AI"></textarea>
954
+ <textarea x-model="editingTask.system_prompt"
955
+ placeholder="System instructions for the AI"></textarea>
956
</div>
957
958
<!-- User Prompt -->
959
<div class="scheduler-form-field full-width">
960
<div class="label-help-wrapper">
961
<label class="scheduler-form-label">User Prompt</label>
927
- <div class="scheduler-form-help">The main task prompt that will be executed</div>
962
+ <div class="scheduler-form-help">The main task prompt that will be
963
+ executed</div>
964
</div>
929
- <textarea x-model="editingTask.prompt" placeholder="User message for the AI"></textarea>
965
+ <textarea x-model="editingTask.prompt"
966
+ placeholder="User message for the AI"></textarea>
967
</div>
968
969
<!-- Attachments Field -->
970
<div class="scheduler-form-field full-width">
971
<div class="label-help-wrapper">
972
<label class="scheduler-form-label">Attachments</label>
936
- <div class="scheduler-form-help">Container file paths or URLs, one per line</div>
973
+ <div class="scheduler-form-help">Container file paths or URLs, one per
974
+ line</div>
975
</div>
938
- <textarea x-model="attachmentsText" placeholder="Enter file paths or URLs, one per line"></textarea>
976
+ <textarea x-model="attachmentsText"
977
+ placeholder="Enter file paths or URLs, one per line"></textarea>
978
</div>
979
</div>
980
</div>
@@ -959,7 +998,8 @@
998
<div class="scheduler-form-field">
999
<div class="label-help-wrapper">
1000
<label class="scheduler-form-label">Task Name</label>
962
- <div class="scheduler-form-help">A unique name to identify this task</div>
1001
+ <div class="scheduler-form-help">A unique name to identify this task
1002
+ </div>
1003
</div>
1004
<input type="text" x-model="editingTask.name" placeholder="Enter task name">
1005
</div>
@@ -968,7 +1008,8 @@
1008
<div class="scheduler-form-field">
1009
<div class="label-help-wrapper">
1010
<label class="scheduler-form-label">Task Type</label>
971
- <div class="scheduler-form-help">Task type cannot be changed after creation</div>
1011
+ <div class="scheduler-form-help">Task type cannot be changed after
1012
+ creation</div>
1013
</div>
1014
<select x-model="editingTask.type" disabled>
1015
<option value="scheduled">Scheduled Task</option>
@@ -986,73 +1027,96 @@
1027
<div>
1028
<div class="scheduler-state-selector">
1029
<span class="scheduler-status-badge scheduler-status-idle"
989
- :class="{'scheduler-status-selected': editingTask.state === 'idle'}"
990
- @click="editingTask.state = 'idle'">idle</span>
1030
+ :class="{'scheduler-status-selected': editingTask.state === 'idle'}"
1031
+ @click="editingTask.state = 'idle'">idle</span>
1032
<span class="scheduler-status-badge scheduler-status-running"
992
- :class="{'scheduler-status-selected': editingTask.state === 'running'}"
993
- @click="editingTask.state = 'running'">running</span>
1033
+ :class="{'scheduler-status-selected': editingTask.state === 'running'}"
1034
+ @click="editingTask.state = 'running'">running</span>
1035
<span class="scheduler-status-badge scheduler-status-disabled"
995
- :class="{'scheduler-status-selected': editingTask.state === 'disabled'}"
996
- @click="editingTask.state = 'disabled'">disabled</span>
1036
+ :class="{'scheduler-status-selected': editingTask.state === 'disabled'}"
1037
+ @click="editingTask.state = 'disabled'">disabled</span>
1038
<span class="scheduler-status-badge scheduler-status-error"
998
- :class="{'scheduler-status-selected': editingTask.state === 'error'}"
999
- @click="editingTask.state = 'error'">error</span>
1039
+ :class="{'scheduler-status-selected': editingTask.state === 'error'}"
1040
+ @click="editingTask.state = 'error'">error</span>
1041
</div>
1042
<div class="scheduler-state-explanation">
1002
- <span x-show="editingTask.state === 'idle'"><strong>idle</strong>: ready to run</span>
1003
- <span x-show="editingTask.state === 'running'"><strong>running</strong>: currently executing</span>
1004
- <span x-show="editingTask.state === 'disabled'"><strong>disabled</strong>: won't execute automatically</span>
1005
- <span x-show="editingTask.state === 'error'"><strong>error</strong>: task encountered an error</span>
1043
+ <span x-show="editingTask.state === 'idle'"><strong>idle</strong>:
1044
+ ready to run</span>
1045
+ <span
1046
+ x-show="editingTask.state === 'running'"><strong>running</strong>:
1047
+ currently executing</span>
1048
+ <span
1049
+ x-show="editingTask.state === 'disabled'"><strong>disabled</strong>:
1050
+ won't execute automatically</span>
1051
+ <span x-show="editingTask.state === 'error'"><strong>error</strong>:
1052
+ task encountered an error</span>
1053
</div>
1054
</div>
1055
</div>
1056
1057
<!-- Schedule (for scheduled tasks) -->
1011
- <div class="scheduler-form-field full-width" x-show="editingTask && editingTask.type === 'scheduled'">
1058
+ <div class="scheduler-form-field full-width"
1059
+ x-show="editingTask && editingTask.type === 'scheduled'">
1060
<div class="label-help-wrapper">
1061
<label class="scheduler-form-label">Schedule (Cron Expression)</label>
1014
- <div class="scheduler-form-help">Format: minute hour day month weekday (e.g., "* * * * *" for every minute)</div>
1062
+ <div class="scheduler-form-help">Format: minute hour day month weekday
1063
+ (e.g., "* * * * *" for every minute)</div>
1064
</div>
1065
<div class="scheduler-schedule-builder">
1066
<div class="scheduler-schedule-field">
1067
<span class="scheduler-schedule-label">Minute</span>
1019
- <input type="text" x-model="editingTask.schedule.minute" placeholder="*" maxlength="9">
1068
+ <input type="text" x-model="editingTask.schedule.minute"
1069
+ placeholder="*" maxlength="9">
1070
</div>
1071
<div class="scheduler-schedule-field">
1072
<span class="scheduler-schedule-label">Hour</span>
1023
- <input type="text" x-model="editingTask.schedule.hour" placeholder="*" maxlength="9">
1073
+ <input type="text" x-model="editingTask.schedule.hour"
1074
+ placeholder="*" maxlength="9">
1075
</div>
1076
<div class="scheduler-schedule-field">
1077
<span class="scheduler-schedule-label">Day</span>
1027
- <input type="text" x-model="editingTask.schedule.day" placeholder="*" maxlength="9">
1078
+ <input type="text" x-model="editingTask.schedule.day"
1079
+ placeholder="*" maxlength="9">
1080
</div>
1081
<div class="scheduler-schedule-field">
1082
<span class="scheduler-schedule-label">Month</span>
1031
- <input type="text" x-model="editingTask.schedule.month" placeholder="*" maxlength="9">
1083
+ <input type="text" x-model="editingTask.schedule.month"
1084
+ placeholder="*" maxlength="9">
1085
</div>
1086
<div class="scheduler-schedule-field">
1087
<span class="scheduler-schedule-label">Weekday</span>
1035
- <input type="text" x-model="editingTask.schedule.weekday" placeholder="*" maxlength="9">
1088
+ <input type="text" x-model="editingTask.schedule.weekday"
1089
+ placeholder="*" maxlength="9">
1090
</div>
1091
</div>
1092
</div>
1093
1094
<!-- Plan (for planned tasks) -->
1041
- <div class="scheduler-form-field full-width" x-show="editingTask.type === 'planned'">
1095
+ <div class="scheduler-form-field full-width"
1096
+ x-show="editingTask.type === 'planned'">
1097
<div class="label-help-wrapper">
1098
<label class="scheduler-form-label">Plan</label>
1044
- <div class="scheduler-form-help">Specific execution times for this task</div>
1099
+ <div class="scheduler-form-help">Specific execution times for this task
1100
+ </div>
1101
</div>
1102
<div class="scheduler-plan-builder">
1103
<div class="scheduler-plan-todo">
1104
<span class="scheduler-plan-label">Upcoming Executions</span>
1105
<div class="scheduler-todo-list">
1050
- <template x-if="editingTask.plan && Array.isArray(editingTask.plan.todo) && editingTask.plan.todo.length > 0">
1051
- <template x-for="(time, index) in editingTask.plan.todo" :key="index">
1106
+ <template
1107
+ x-if="editingTask.plan && Array.isArray(editingTask.plan.todo) && editingTask.plan.todo.length > 0">
1108
+ <template x-for="(time, index) in editingTask.plan.todo"
1109
+ :key="index">
1110
<div class="scheduler-todo-item">
1111
<span x-text="formatDate(time)"></span>
1054
- <button @click.prevent="editingTask.plan.todo.splice(index, 1)" class="scheduler-todo-remove">
1055
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1112
+ <button
1113
+ @click.prevent="editingTask.plan.todo.splice(index, 1)"
1114
+ class="scheduler-todo-remove">
1115
+ <svg xmlns="http://www.w3.org/2000/svg"
1116
+ width="16" height="16" viewBox="0 0 24 24"
1117
+ fill="none" stroke="currentColor"
1118
+ stroke-width="2" stroke-linecap="round"
1119
+ stroke-linejoin="round">
1120
<line x1="18" y1="6" x2="6" y2="18"></line>
1121
<line x1="6" y1="6" x2="18" y2="18"></line>
1122
</svg>
@@ -1060,7 +1124,8 @@
1124
</div>
1125
</template>
1126
</template>
1063
- <template x-if="!editingTask.plan || !Array.isArray(editingTask.plan.todo) || editingTask.plan.todo.length === 0">
1127
+ <template
1128
+ x-if="!editingTask.plan || !Array.isArray(editingTask.plan.todo) || editingTask.plan.todo.length === 0">
1129
<div class="scheduler-empty-plan">
1130
No scheduled execution times yet. Add one below.
1131
</div>
@@ -1113,7 +1178,11 @@
1178
input.value = '';
1179
}
1180
" class="scheduler-add-todo-button">
1116
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px;">
1181
+ <svg xmlns="http://www.w3.org/2000/svg" width="16"
1182
+ height="16" viewBox="0 0 24 24" fill="none"
1183
+ stroke="currentColor" stroke-width="2"
1184
+ stroke-linecap="round" stroke-linejoin="round"
1185
+ style="margin-right: 4px;">
1186
<line x1="12" y1="5" x2="12" y2="19"></line>
1187
<line x1="5" y1="12" x2="19" y2="12"></line>
1188
</svg>
@@ -1126,14 +1195,18 @@
1195
</div>
1196
1197
<!-- Token (for ad-hoc tasks) -->
1129
- <div class="scheduler-form-field full-width" x-show="editingTask.type === 'adhoc'">
1198
+ <div class="scheduler-form-field full-width"
1199
+ x-show="editingTask.type === 'adhoc'">
1200
<div class="label-help-wrapper">
1201
<label class="scheduler-form-label">Token</label>
1132
- <div class="scheduler-form-help">Token used to trigger this task externally</div>
1202
+ <div class="scheduler-form-help">Token used to trigger this task
1203
+ externally</div>
1204
</div>
1205
<div class="input-group">
1135
- <input type="text" x-model="editingTask.token" placeholder="Token for ad-hoc task">
1136
- <button class="scheduler-task-action" @click="editingTask.token = generateRandomToken()">
1206
+ <input type="text" x-model="editingTask.token"
1207
+ placeholder="Token for ad-hoc task">
1208
+ <button class="scheduler-task-action"
1209
+ @click="editingTask.token = generateRandomToken()">
1210
Generate
1211
</button>
1212
</div>
@@ -1143,33 +1216,40 @@
1216
<div class="scheduler-form-field full-width">
1217
<div class="label-help-wrapper">
1218
<label class="scheduler-form-label">System Prompt</label>
1146
- <div class="scheduler-form-help">System-level instructions for the assistant</div>
1219
+ <div class="scheduler-form-help">System-level instructions for the
1220
+ assistant</div>
1221
</div>
1148
- <textarea x-model="editingTask.system_prompt" placeholder="System instructions for the AI"></textarea>
1222
+ <textarea x-model="editingTask.system_prompt"
1223
+ placeholder="System instructions for the AI"></textarea>
1224
</div>
1225
1226
<!-- User Prompt -->
1227
<div class="scheduler-form-field full-width">
1228
<div class="label-help-wrapper">
1229
<label class="scheduler-form-label">User Prompt</label>
1155
- <div class="scheduler-form-help">The main task prompt that will be executed</div>
1230
+ <div class="scheduler-form-help">The main task prompt that will be
1231
+ executed</div>
1232
</div>
1157
- <textarea x-model="editingTask.prompt" placeholder="User message for the AI"></textarea>
1233
+ <textarea x-model="editingTask.prompt"
1234
+ placeholder="User message for the AI"></textarea>
1235
</div>
1236
1237
<!-- Attachments Field -->
1238
<div class="scheduler-form-field full-width">
1239
<div class="label-help-wrapper">
1240
<label class="scheduler-form-label">Attachments</label>
1164
- <div class="scheduler-form-help">Container file paths or URLs, one per line</div>
1241
+ <div class="scheduler-form-help">Container file paths or URLs, one per
1242
+ line</div>
1243
</div>
1166
- <textarea x-model="attachmentsText" placeholder="Enter file paths or URLs, one per line"></textarea>
1244
+ <textarea x-model="attachmentsText"
1245
+ placeholder="Enter file paths or URLs, one per line"></textarea>
1246
</div>
1247
</div>
1248
</div>
1249
1250
<!-- Task List View -->
1172
- <div class="scheduler-container" x-show="!isCreating && !isEditing && viewMode === 'list'">
1251
+ <div class="scheduler-container"
1252
+ x-show="!isCreating && !isEditing && viewMode === 'list'">
1253
<!-- Header with Actions -->
1254
<div class="scheduler-header">
1255
<h2>Task Management</h2>
@@ -1210,36 +1290,37 @@
1290
</div> -->
1291
1292
<!-- Empty State -->
1213
- <div class="scheduler-empty"
1214
- x-show="!isLoading && filteredTasks.length === 0"
1215
- x-effect="$el.style.display = (!isLoading && filteredTasks.length === 0) ? '' : 'none'">
1293
+ <div class="scheduler-empty" x-show="!isLoading && filteredTasks.length === 0"
1294
+ x-effect="$el.style.display = (!isLoading && filteredTasks.length === 0) ? '' : 'none'">
1295
<!-- <div class="scheduler-empty-icon">📋</div> -->
1296
<div class="scheduler-empty-text">No tasks found</div>
1218
- <button class="btn btn-ok" @click="startCreateTask()">Create your first task</button>
1297
+ <button class="btn btn-ok" @click="startCreateTask()">Create your first
1298
+ task</button>
1299
</div>
1300
1301
<!-- Task List Table -->
1222
- <table class="scheduler-task-list"
1223
- x-show="!isLoading && filteredTasks.length > 0"
1224
- x-effect="$el.style.display = (!isLoading && filteredTasks.length > 0) ? '' : 'none'">
1302
+ <table class="scheduler-task-list" x-show="!isLoading && filteredTasks.length > 0"
1303
+ x-effect="$el.style.display = (!isLoading && filteredTasks.length > 0) ? '' : 'none'">
1304
<thead>
1305
<tr>
1306
<th @click="changeSort('name')">
1307
Name
1308
<span class="scheduler-sort-indicator" x-show="sortField === 'name'"
1230
- :class="{'scheduler-sort-desc': sortDirection === 'desc'}">↑</span>
1309
+ :class="{'scheduler-sort-desc': sortDirection === 'desc'}">↑</span>
1310
</th>
1311
<th @click="changeSort('state')">
1312
State
1234
- <span class="scheduler-sort-indicator" x-show="sortField === 'state'"
1235
- :class="{'scheduler-sort-desc': sortDirection === 'desc'}">↑</span>
1313
+ <span class="scheduler-sort-indicator"
1314
+ x-show="sortField === 'state'"
1315
+ :class="{'scheduler-sort-desc': sortDirection === 'desc'}">↑</span>
1316
</th>
1317
<th>Type</th>
1318
<th>Schedule</th>
1319
<th @click="changeSort('last_run')">
1320
Last Run
1241
- <span class="scheduler-sort-indicator" x-show="sortField === 'last_run'"
1242
- :class="{'scheduler-sort-desc': sortDirection === 'desc'}">↑</span>
1321
+ <span class="scheduler-sort-indicator"
1322
+ x-show="sortField === 'last_run'"
1323
+ :class="{'scheduler-sort-desc': sortDirection === 'desc'}">↑</span>
1324
</th>
1325
<th>Actions</th>
1326
</tr>
@@ -1251,35 +1332,55 @@
1332
<span x-text="task.name"></span>
1333
</td>
1334
<td>
1254
- <span class="scheduler-status-badge" :class="getStateBadgeClass(task.state)" x-text="task.state"></span>
1335
+ <span class="scheduler-status-badge"
1336
+ :class="getStateBadgeClass(task.state)"
1337
+ x-text="task.state"></span>
1338
</td>
1339
<td x-text="task.type"></td>
1340
<td>
1258
- <span x-show="task.type === 'scheduled'" x-text="formatSchedule(task)"></span>
1259
- <span x-show="task.type === 'adhoc'" class="scheduler-no-schedule">—</span>
1260
- <span x-show="task.type === 'planned'" x-html="formatPlan(task).replace(/\n/g, '<br>')"></span>
1341
+ <span x-show="task.type === 'scheduled'"
1342
+ x-text="formatSchedule(task)"></span>
1343
+ <span x-show="task.type === 'adhoc'"
1344
+ class="scheduler-no-schedule">—</span>
1345
+ <span x-show="task.type === 'planned'"
1346
+ x-html="formatPlan(task).replace(/\n/g, '<br>')"></span>
1347
</td>
1348
<td x-text="formatDate(task.last_run)"></td>
1349
<td @click.stop>
1350
<div class="scheduler-task-actions">
1265
- <button class="scheduler-task-action" @click="runTask(task.uuid)" title="Run Task">
1266
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
1267
- <path d="M8 5v14l11-7z"/>
1351
+ <button class="scheduler-task-action"
1352
+ @click="runTask(task.uuid)" title="Run Task">
1353
+ <svg xmlns="http://www.w3.org/2000/svg"
1354
+ viewBox="0 0 24 24" fill="currentColor" width="16"
1355
+ height="16">
1356
+ <path d="M8 5v14l11-7z" />
1357
</svg>
1358
</button>
1270
- <button class="scheduler-task-action" @click="resetTaskState(task.uuid)" title="Reset State">
1271
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
1272
- <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
1359
+ <button class="scheduler-task-action"
1360
+ @click="resetTaskState(task.uuid)" title="Reset State">
1361
+ <svg xmlns="http://www.w3.org/2000/svg"
1362
+ viewBox="0 0 24 24" fill="currentColor" width="16"
1363
+ height="16">
1364
+ <path
1365
+ d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
1366
</svg>
1367
</button>
1275
- <button class="scheduler-task-action" @click="startEditTask(task.uuid)" title="Edit Task">
1276
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
1277
- <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
1368
+ <button class="scheduler-task-action"
1369
+ @click="startEditTask(task.uuid)" title="Edit Task">
1370
+ <svg xmlns="http://www.w3.org/2000/svg"
1371
+ viewBox="0 0 24 24" fill="currentColor" width="16"
1372
+ height="16">
1373
+ <path
1374
+ d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
1375
</svg>
1376
</button>
1280
- <button class="scheduler-task-action" @click="deleteTask(task.uuid)" title="Delete Task">
1281
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
1282
- <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
1377
+ <button class="scheduler-task-action"
1378
+ @click="deleteTask(task.uuid)" title="Delete Task">
1379
+ <svg xmlns="http://www.w3.org/2000/svg"
1380
+ viewBox="0 0 24 24" fill="currentColor" width="16"
1381
+ height="16">
1382
+ <path
1383
+ d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
1384
</svg>
1385
</button>
1386
</div>
@@ -1291,9 +1392,11 @@
1392
</div>
1393
1394
<!-- Task Detail View -->
1294
- <div class="scheduler-detail-view" x-show="!isCreating && !isEditing && viewMode === 'detail' && selectedTaskForDetail">
1395
+ <div class="scheduler-detail-view"
1396
+ x-show="!isCreating && !isEditing && viewMode === 'detail' && selectedTaskForDetail">
1397
<div class="scheduler-detail-header">
1296
- <h2 class="scheduler-detail-title" x-text="selectedTaskForDetail ? selectedTaskForDetail.name : ''"></h2>
1398
+ <h2 class="scheduler-detail-title"
1399
+ x-text="selectedTaskForDetail ? selectedTaskForDetail.name : ''"></h2>
1400
<div class="scheduler-status-badge"
1401
:class="selectedTaskForDetail ? getStateBadgeClass(selectedTaskForDetail.state) : ''"
1402
x-text="selectedTaskForDetail ? selectedTaskForDetail.state : ''">
@@ -1304,74 +1407,113 @@
1407
<div class="scheduler-detail-content">
1408
<div class="scheduler-details-grid">
1409
<div class="scheduler-details-label">Type:</div>
1307
- <div class="scheduler-details-value" x-text="selectedTaskForDetail ? selectedTaskForDetail.type : ''"></div>
1410
+ <div class="scheduler-details-value"
1411
+ x-text="selectedTaskForDetail ? selectedTaskForDetail.type : ''"></div>
1412
1413
<div class="scheduler-details-label">Created:</div>
1310
- <div class="scheduler-details-value" x-text="selectedTaskForDetail ? formatDate(selectedTaskForDetail.created_at) : ''"></div>
1414
+ <div class="scheduler-details-value"
1415
+ x-text="selectedTaskForDetail ? formatDate(selectedTaskForDetail.created_at) : ''">
1416
+ </div>
1417
1418
<div class="scheduler-details-label">Last Updated:</div>
1313
- <div class="scheduler-details-value" x-text="selectedTaskForDetail ? formatDate(selectedTaskForDetail.updated_at) : ''"></div>
1419
+ <div class="scheduler-details-value"
1420
+ x-text="selectedTaskForDetail ? formatDate(selectedTaskForDetail.updated_at) : ''">
1421
+ </div>
1422
1423
<div class="scheduler-details-label">Last Run:</div>
1316
- <div class="scheduler-details-value" x-text="selectedTaskForDetail ? formatDate(selectedTaskForDetail.last_run) : ''"></div>
1317
-
1318
- <div class="scheduler-details-label" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'scheduled'">Schedule:</div>
1319
- <div class="scheduler-details-value" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'scheduled'" x-text="selectedTaskForDetail ? formatSchedule(selectedTaskForDetail) : ''"></div>
1424
+ <div class="scheduler-details-value"
1425
+ x-text="selectedTaskForDetail ? formatDate(selectedTaskForDetail.last_run) : ''">
1426
+ </div>
1427
1321
- <div class="scheduler-details-label" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'adhoc'">Token:</div>
1322
- <div class="scheduler-details-value" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'adhoc'" x-text="selectedTaskForDetail ? selectedTaskForDetail.token : ''"></div>
1428
+ <div class="scheduler-details-label"
1429
+ x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'scheduled'">
1430
+ Schedule:</div>
1431
+ <div class="scheduler-details-value"
1432
+ x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'scheduled'"
1433
+ x-text="selectedTaskForDetail ? formatSchedule(selectedTaskForDetail) : ''">
1434
+ </div>
1435
1324
- <div class="scheduler-details-label" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'planned'">Plan:</div>
1325
- <div class="scheduler-details-value" x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'planned'">
1436
+ <div class="scheduler-details-label"
1437
+ x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'adhoc'">
1438
+ Token:</div>
1439
+ <div class="scheduler-details-value"
1440
+ x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'adhoc'"
1441
+ x-text="selectedTaskForDetail ? selectedTaskForDetail.token : ''"></div>
1442
+
1443
+ <div class="scheduler-details-label"
1444
+ x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'planned'">
1445
+ Plan:</div>
1446
+ <div class="scheduler-details-value"
1447
+ x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'planned'">
1448
<div x-show="selectedTaskForDetail && selectedTaskForDetail.plan">
1449
<div><strong>Upcoming:</strong></div>
1328
- <template x-if="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.todo && selectedTaskForDetail.plan.todo.length > 0">
1450
+ <template
1451
+ x-if="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.todo && selectedTaskForDetail.plan.todo.length > 0">
1452
<div>
1330
- <template x-for="(time, index) in selectedTaskForDetail.plan.todo" :key="index">
1453
+ <template
1454
+ x-for="(time, index) in selectedTaskForDetail.plan.todo"
1455
+ :key="index">
1456
<div x-text="formatDate(time)"></div>
1457
</template>
1458
</div>
1459
</template>
1335
- <template x-if="!selectedTaskForDetail || !selectedTaskForDetail.plan || !selectedTaskForDetail.plan.todo || selectedTaskForDetail.plan.todo.length === 0">
1460
+ <template
1461
+ x-if="!selectedTaskForDetail || !selectedTaskForDetail.plan || !selectedTaskForDetail.plan.todo || selectedTaskForDetail.plan.todo.length === 0">
1462
<div>No upcoming executions</div>
1463
</template>
1464
1465
<div><strong>In Progress:</strong></div>
1340
- <div x-text="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.in_progress ? formatDate(selectedTaskForDetail.plan.in_progress) : 'None'"></div>
1466
+ <div
1467
+ x-text="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.in_progress ? formatDate(selectedTaskForDetail.plan.in_progress) : 'None'">
1468
+ </div>
1469
1470
<div><strong>Completed:</strong></div>
1343
- <template x-if="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.done && selectedTaskForDetail.plan.done.length > 0">
1471
+ <template
1472
+ x-if="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.done && selectedTaskForDetail.plan.done.length > 0">
1473
<div>
1345
- <template x-for="(time, index) in selectedTaskForDetail.plan.done" :key="index">
1474
+ <template
1475
+ x-for="(time, index) in selectedTaskForDetail.plan.done"
1476
+ :key="index">
1477
<div x-text="formatDate(time)"></div>
1478
</template>
1479
</div>
1480
</template>
1350
- <template x-if="!selectedTaskForDetail || !selectedTaskForDetail.plan || !selectedTaskForDetail.plan.done || selectedTaskForDetail.plan.done.length === 0">
1481
+ <template
1482
+ x-if="!selectedTaskForDetail || !selectedTaskForDetail.plan || !selectedTaskForDetail.plan.done || selectedTaskForDetail.plan.done.length === 0">
1483
<div>No completed executions</div>
1484
</template>
1485
</div>
1486
</div>
1487
1488
<div class="scheduler-details-label">Last Result:</div>
1357
- <div class="scheduler-details-value" x-text="selectedTaskForDetail && selectedTaskForDetail.last_result ? selectedTaskForDetail.last_result : 'No results yet'"></div>
1489
+ <div class="scheduler-details-value"
1490
+ x-text="selectedTaskForDetail && selectedTaskForDetail.last_result ? selectedTaskForDetail.last_result : 'No results yet'">
1491
+ </div>
1492
1493
<div class="scheduler-details-label">System Prompt:</div>
1360
- <div class="scheduler-details-value" x-text="selectedTaskForDetail ? selectedTaskForDetail.system_prompt : ''"></div>
1494
+ <div class="scheduler-details-value"
1495
+ x-text="selectedTaskForDetail ? selectedTaskForDetail.system_prompt : ''">
1496
+ </div>
1497
1498
<div class="scheduler-details-label">User Prompt:</div>
1363
- <div class="scheduler-details-value" x-text="selectedTaskForDetail ? selectedTaskForDetail.prompt : ''"></div>
1499
+ <div class="scheduler-details-value"
1500
+ x-text="selectedTaskForDetail ? selectedTaskForDetail.prompt : ''">
1501
+ </div>
1502
1503
<div class="scheduler-details-label">Attachments:</div>
1504
<div class="scheduler-details-value">
1367
- <template x-if="selectedTaskForDetail && selectedTaskForDetail.attachments && selectedTaskForDetail.attachments.length > 0">
1505
+ <template
1506
+ x-if="selectedTaskForDetail && selectedTaskForDetail.attachments && selectedTaskForDetail.attachments.length > 0">
1507
<div>
1369
- <template x-for="(attachment, index) in selectedTaskForDetail.attachments" :key="index">
1508
+ <template
1509
+ x-for="(attachment, index) in selectedTaskForDetail.attachments"
1510
+ :key="index">
1511
<div x-text="attachment"></div>
1512
</template>
1513
</div>
1514
</template>
1374
- <template x-if="!selectedTaskForDetail || !selectedTaskForDetail.attachments || selectedTaskForDetail.attachments.length === 0">
1515
+ <template
1516
+ x-if="!selectedTaskForDetail || !selectedTaskForDetail.attachments || selectedTaskForDetail.attachments.length === 0">
1517
<div>No attachments</div>
1518
</template>
1519
</div>
@@ -1551,14 +1693,18 @@
1693
<!-- Add toolbar -->
1694
<div class="editor-toolbar">
1695
<div class="toolbar-group">
1554
- <button class="toolbar-button" @click="undo()" :disabled="!canUndo" title="Undo (Ctrl+Z)">
1555
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1696
+ <button class="toolbar-button" @click="undo()" :disabled="!canUndo"
1697
+ title="Undo (Ctrl+Z)">
1698
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1699
+ stroke="currentColor" stroke-width="2">
1700
<path d="M3 7v6h6"></path>
1701
<path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"></path>
1702
</svg>
1703
</button>
1560
- <button class="toolbar-button" @click="redo()" :disabled="!canRedo" title="Redo (Ctrl+Shift+Z)">
1561
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1704
+ <button class="toolbar-button" @click="redo()" :disabled="!canRedo"
1705
+ title="Redo (Ctrl+Shift+Z)">
1706
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1707
+ stroke="currentColor" stroke-width="2">
1708
<path d="M21 7v6h-6"></path>
1709
<path d="M3 17a9 9 0 019-9 9 9 0 016 2.3l3 2.7"></path>
1710
</svg>
@@ -1566,24 +1712,27 @@
1712
</div>
1713
<div class="toolbar-group">
1714
<button class="toolbar-button" @click="clearText()" title="Clear Text">
1569
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1570
- <path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path>
1715
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1716
+ stroke="currentColor" stroke-width="2">
1717
+ <path
1718
+ d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2">
1719
+ </path>
1720
<line x1="10" y1="11" x2="10" y2="17"></line>
1721
<line x1="14" y1="11" x2="14" y2="17"></line>
1722
</svg>
1723
</button>
1575
- <button class="toolbar-button" @click="toggleWrap()" :class="{ active: wordWrap }" title="Toggle Word Wrap">
1576
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1724
+ <button class="toolbar-button" @click="toggleWrap()" :class="{ active: wordWrap }"
1725
+ title="Toggle Word Wrap">
1726
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
1727
+ stroke="currentColor" stroke-width="2">
1728
<path d="M3 6h18M3 12h15l3 3-3 3M3 18h18"></path>
1729
</svg>
1730
</button>
1731
</div>
1732
</div>
1733
1583
- <textarea id="full-screen-input" x-model="inputText"
1584
- placeholder="Type your message here..."
1585
- @keydown.ctrl.enter="handleClose()"
1586
- @keydown.ctrl.z.prevent="undo()"
1734
+ <textarea id="full-screen-input" x-model="inputText" placeholder="Type your message here..."
1735
+ @keydown.ctrl.enter="handleClose()" @keydown.ctrl.z.prevent="undo()"
1736
@keydown.ctrl.shift.z.prevent="redo()"
1737
:style="{ 'white-space': wordWrap ? 'pre-wrap' : 'pre' }"
1738
@input="updateHistory()"></textarea>
@@ -1603,17 +1752,17 @@
1752
1753
<!-- Register Service Worker for offline support and caching -->
1754
<script>
1606
- if ('serviceWorker' in navigator) {
1607
- window.addEventListener('load', () => {
1608
- navigator.serviceWorker.register('js/sw.js').then(registration => {
1609
- console.log('SW registered: ', registration);
1610
- }).catch(registrationError => {
1611
- console.log('SW registration failed: ', registrationError);
1612
- });
1613
- });
1614
- }
1755
+ if ('serviceWorker' in navigator) {
1756
+ window.addEventListener('load', () => {
1757
+ navigator.serviceWorker.register('js/sw.js').then(registration => {
1758
+ console.log('SW registered: ', registration);
1759
+ }).catch(registrationError => {
1760
+ console.log('SW registration failed: ', registrationError);
1761
+ });
1762
+ });
1763
+ }
1764
</script>
1765
1766
</body>
1767
1619
-</html>
1768
+</html>
\ No newline at end of file