Alessandro commited on
Commit
93b7cae
·
1 Parent(s): bc5061f

history/context/image modals components

Browse files
webui/components/chat/attachments/attachmentsStore.js CHANGED
@@ -1,5 +1,6 @@
1
  import { createStore } from "/js/AlpineStore.js";
2
  import { fetchApi } from "/js/api.js";
 
3
 
4
  const model = {
5
  // State properties
@@ -7,13 +8,6 @@ const model = {
7
  hasAttachments: false,
8
  dragDropOverlayVisible: false,
9
 
10
- // Image modal properties
11
- currentImageUrl: null,
12
- currentImageName: null,
13
- imageLoaded: false,
14
- imageError: false,
15
- zoomLevel: 1,
16
-
17
  async init() {
18
  await this.initialize();
19
  },
@@ -358,7 +352,7 @@ const model = {
358
  previewUrl: previewUrl,
359
  clickHandler: () => {
360
  if (this.isImageFile(filename)) {
361
- this.openImageModal(this.getServerImgUrl(filename), filename);
362
  } else {
363
  this.downloadAttachment(filename);
364
  }
@@ -380,7 +374,7 @@ const model = {
380
  clickHandler: () => {
381
  if (attachment.type === "image") {
382
  const imageUrl = this.getServerImgUrl(attachment.name);
383
- this.openImageModal(imageUrl, attachment.name);
384
  } else {
385
  this.downloadAttachment(attachment.name);
386
  }
@@ -425,50 +419,6 @@ const model = {
425
  );
426
  },
427
 
428
- // Image modal methods
429
- openImageModal(imageUrl, imageName) {
430
- this.currentImageUrl = imageUrl;
431
- this.currentImageName = imageName;
432
- this.imageLoaded = false;
433
- this.imageError = false;
434
- this.zoomLevel = 1;
435
-
436
- // Open the modal using the modals system
437
- if (window.openModal) {
438
- window.openModal("chat/attachments/imageModal.html");
439
- }
440
- },
441
-
442
- closeImageModal() {
443
- this.currentImageUrl = null;
444
- this.currentImageName = null;
445
- this.imageLoaded = false;
446
- this.imageError = false;
447
- this.zoomLevel = 1;
448
- },
449
-
450
- // Zoom controls
451
- zoomIn() {
452
- this.zoomLevel = Math.min(this.zoomLevel * 1.2, 5); // Max 5x zoom
453
- this.updateImageZoom();
454
- },
455
-
456
- zoomOut() {
457
- this.zoomLevel = Math.max(this.zoomLevel / 1.2, 0.1); // Min 0.1x zoom
458
- this.updateImageZoom();
459
- },
460
-
461
- resetZoom() {
462
- this.zoomLevel = 1;
463
- this.updateImageZoom();
464
- },
465
-
466
- updateImageZoom() {
467
- const img = document.querySelector(".modal-image");
468
- if (img) {
469
- img.style.transform = `scale(${this.zoomLevel})`;
470
- }
471
- },
472
  };
473
 
474
  const store = createStore("chatAttachments", model);
 
1
  import { createStore } from "/js/AlpineStore.js";
2
  import { fetchApi } from "/js/api.js";
3
+ import { store as imageViewerStore } from "/components/modals/image-viewer-store.js";
4
 
5
  const model = {
6
  // State properties
 
8
  hasAttachments: false,
9
  dragDropOverlayVisible: false,
10
 
 
 
 
 
 
 
 
11
  async init() {
12
  await this.initialize();
13
  },
 
352
  previewUrl: previewUrl,
353
  clickHandler: () => {
354
  if (this.isImageFile(filename)) {
355
+ imageViewerStore.open(this.getServerImgUrl(filename), { name: filename });
356
  } else {
357
  this.downloadAttachment(filename);
358
  }
 
374
  clickHandler: () => {
375
  if (attachment.type === "image") {
376
  const imageUrl = this.getServerImgUrl(attachment.name);
377
+ imageViewerStore.open(imageUrl, { name: attachment.name });
378
  } else {
379
  this.downloadAttachment(attachment.name);
380
  }
 
419
  );
420
  },
421
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  };
423
 
424
  const store = createStore("chatAttachments", model);
webui/components/chat/attachments/inputPreview.html CHANGED
@@ -1,5 +1,6 @@
1
  <script type="module">
2
  import { store } from "/components/chat/attachments/attachmentsStore.js";
 
3
  </script>
4
 
5
  <div x-data>
@@ -11,7 +12,7 @@
11
  :class="{'image-type': attachment.type === 'image', 'file-type': attachment.type === 'file'}">
12
  <template x-if="attachment.type === 'image'">
13
  <img :src="attachment.url" class="attachment-preview" :alt="attachment.name" style="cursor: pointer;"
14
- @click="$store.chatAttachments.openImageModal(attachment.url, attachment.name)">
15
  </template>
16
  <template x-if="attachment.type === 'file'">
17
  <div>
 
1
  <script type="module">
2
  import { store } from "/components/chat/attachments/attachmentsStore.js";
3
+ import { store as imageViewerStore } from "/components/modals/image-viewer-store.js";
4
  </script>
5
 
6
  <div x-data>
 
12
  :class="{'image-type': attachment.type === 'image', 'file-type': attachment.type === 'file'}">
13
  <template x-if="attachment.type === 'image'">
14
  <img :src="attachment.url" class="attachment-preview" :alt="attachment.name" style="cursor: pointer;"
15
+ @click="$store.imageViewer.open(attachment.url, { name: attachment.name })">
16
  </template>
17
  <template x-if="attachment.type === 'file'">
18
  <div>
webui/components/chat/input/bottom-actions.html CHANGED
@@ -40,14 +40,14 @@
40
  <p>Files</p>
41
  </button>
42
 
43
- <button class="text-button" id="history_inspect" @click="globalThis.openHistoryModal()">
44
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85">
45
  <path fill="currentColor" d="m59.572,57.949c-.41,0-.826-.105-1.207-.325l-9.574-5.528c-.749-.432-1.21-1.231-1.21-2.095v-14.923c0-1.336,1.083-2.419,2.419-2.419s2.419,1.083,2.419,2.419v13.526l8.364,4.829c1.157.668,1.554,2.148.886,3.305-.448.776-1.261,1.21-2.097,1.21Zm30.427-7.947c0,10.684-4.161,20.728-11.716,28.283-6.593,6.59-15.325,10.69-24.59,11.544-1.223.113-2.448.169-3.669.169-7.492,0-14.878-2.102-21.22-6.068l-15.356,5.733c-.888.331-1.887.114-2.557-.556s-.887-1.669-.556-2.557l5.733-15.351c-4.613-7.377-6.704-16.165-5.899-24.891.854-9.266,4.954-17.998,11.544-24.588,7.555-7.555,17.6-11.716,28.285-11.716s20.73,4.161,28.285,11.716c7.555,7.555,11.716,17.599,11.716,28.283Zm-15.137-24.861c-13.71-13.71-36.018-13.71-49.728,0-11.846,11.846-13.682,30.526-4.365,44.417.434.647.53,1.464.257,2.194l-4.303,11.523,11.528-4.304c.274-.102.561-.153.846-.153.474,0,.944.139,1.348.41,13.888,9.315,32.568,7.479,44.417-4.365,13.707-13.708,13.706-36.014,0-49.723Zm-24.861-4.13c-15.989,0-28.996,13.006-28.996,28.992s13.008,28.992,28.996,28.992c1.336,0,2.419-1.083,2.419-2.419s-1.083-2.419-2.419-2.419c-13.32,0-24.157-10.835-24.157-24.153s10.837-24.153,24.157-24.153,24.153,10.835,24.153,24.153c0,1.336,1.083,2.419,2.419,2.419c0-15.986-13.006-28.992-28.992-28.992Zm25.041,33.531c-1.294.347-2.057,1.673-1.71,2.963.343,1.289,1.669,2.057,2.963,1.71,1.289-.343,2.053-1.669,1.71-2.963-.347-1.289-1.673-2.057-2.963-1.71Zm-2.03,6.328c-1.335,0-2.419,1.084-2.419,2.419s1.084,2.419,2.419,2.419,2.419-1.084,2.419-2.419-1.084-2.419-2.419-2.419Zm-3.598,5.587c-1.289-.347-2.615.416-2.963,1.71-.343,1.289.421,2.615,1.71,2.963,1.294.347,2.62-.421,2.963-1.71.347-1.294-.416-2.62-1.71-2.963Zm-4.919,4.462c-1.157-.667-2.638-.27-3.306.887-.667,1.157-.27,2.638.887,3.305,1.157.668,2.638.27,3.306-.887.667-1.157.27-2.638-.887-3.306Zm-9.327,3.04c-.946.946-.946,2.478,0,3.42.942.946,2.473.946,3.42,0,.946-.942.946-2.473,0-3.42-.946-.942-2.478-.946-3.42,0Z"></path>
46
  </svg>
47
  <p>History</p>
48
  </button>
49
 
50
- <button class="text-button" id="ctx_window" @click="globalThis.openCtxWindowModal()">
51
  <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" fill="currentColor">
52
  <path d="m63 25c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z"></path>
53
  <path d="m63 79c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z"></path>
 
40
  <p>Files</p>
41
  </button>
42
 
43
+ <button class="text-button" id="history_inspect" @click="$store.history.open()">
44
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85">
45
  <path fill="currentColor" d="m59.572,57.949c-.41,0-.826-.105-1.207-.325l-9.574-5.528c-.749-.432-1.21-1.231-1.21-2.095v-14.923c0-1.336,1.083-2.419,2.419-2.419s2.419,1.083,2.419,2.419v13.526l8.364,4.829c1.157.668,1.554,2.148.886,3.305-.448.776-1.261,1.21-2.097,1.21Zm30.427-7.947c0,10.684-4.161,20.728-11.716,28.283-6.593,6.59-15.325,10.69-24.59,11.544-1.223.113-2.448.169-3.669.169-7.492,0-14.878-2.102-21.22-6.068l-15.356,5.733c-.888.331-1.887.114-2.557-.556s-.887-1.669-.556-2.557l5.733-15.351c-4.613-7.377-6.704-16.165-5.899-24.891.854-9.266,4.954-17.998,11.544-24.588,7.555-7.555,17.6-11.716,28.285-11.716s20.73,4.161,28.285,11.716c7.555,7.555,11.716,17.599,11.716,28.283Zm-15.137-24.861c-13.71-13.71-36.018-13.71-49.728,0-11.846,11.846-13.682,30.526-4.365,44.417.434.647.53,1.464.257,2.194l-4.303,11.523,11.528-4.304c.274-.102.561-.153.846-.153.474,0,.944.139,1.348.41,13.888,9.315,32.568,7.479,44.417-4.365,13.707-13.708,13.706-36.014,0-49.723Zm-24.861-4.13c-15.989,0-28.996,13.006-28.996,28.992s13.008,28.992,28.996,28.992c1.336,0,2.419-1.083,2.419-2.419s-1.083-2.419-2.419-2.419c-13.32,0-24.157-10.835-24.157-24.153s10.837-24.153,24.157-24.153,24.153,10.835,24.153,24.153c0,1.336,1.083,2.419,2.419,2.419c0-15.986-13.006-28.992-28.992-28.992Zm25.041,33.531c-1.294.347-2.057,1.673-1.71,2.963.343,1.289,1.669,2.057,2.963,1.71,1.289-.343,2.053-1.669,1.71-2.963-.347-1.289-1.673-2.057-2.963-1.71Zm-2.03,6.328c-1.335,0-2.419,1.084-2.419,2.419s1.084,2.419,2.419,2.419,2.419-1.084,2.419-2.419-1.084-2.419-2.419-2.419Zm-3.598,5.587c-1.289-.347-2.615.416-2.963,1.71-.343,1.289.421,2.615,1.71,2.963,1.294.347,2.62-.421,2.963-1.71.347-1.294-.416-2.62-1.71-2.963Zm-4.919,4.462c-1.157-.667-2.638-.27-3.306.887-.667,1.157-.27,2.638.887,3.305,1.157.668,2.638.27,3.306-.887.667-1.157.27-2.638-.887-3.306Zm-9.327,3.04c-.946.946-.946,2.478,0,3.42.942.946,2.473.946,3.42,0,.946-.942.946-2.473,0-3.42-.946-.942-2.478-.946-3.42,0Z"></path>
46
  </svg>
47
  <p>History</p>
48
  </button>
49
 
50
+ <button class="text-button" id="ctx_window" @click="$store.context.open()">
51
  <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" fill="currentColor">
52
  <path d="m63 25c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z"></path>
53
  <path d="m63 79c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z"></path>
webui/components/modals/context-store.js ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createStore } from "/js/AlpineStore.js";
2
+
3
+ const model = {
4
+ // State
5
+ isLoading: false,
6
+ contextData: null,
7
+ tokenCount: 0,
8
+ error: null,
9
+ editor: null,
10
+ closePromise: null,
11
+
12
+ // Open Context Window modal
13
+ async open() {
14
+ if (this.isLoading) return; // Prevent double-open
15
+
16
+ this.isLoading = true;
17
+ this.error = null;
18
+ this.contextData = null;
19
+ this.tokenCount = 0;
20
+
21
+ try {
22
+ // Open modal FIRST (immediate UI feedback, but DON'T await)
23
+ this.closePromise = window.openModal('modals/context.html');
24
+
25
+ // Setup cleanup on modal close
26
+ if (this.closePromise && typeof this.closePromise.then === 'function') {
27
+ this.closePromise.then(() => {
28
+ this.destroy();
29
+ });
30
+ }
31
+
32
+ this.updateModalTitle(); // Set initial "loading" title
33
+
34
+ // Fetch data from backend
35
+ const contextId = window.getContext();
36
+ const response = await window.sendJsonData('/ctx_window_get', {
37
+ context: contextId,
38
+ });
39
+
40
+ // Update state with data
41
+ this.contextData = response.content;
42
+ this.tokenCount = response.tokens || 0;
43
+ this.isLoading = false;
44
+ this.updateModalTitle(); // Update with token count
45
+
46
+ // Initialize ACE editor
47
+ this.scheduleEditorInit();
48
+
49
+ } catch (error) {
50
+ console.error("Context fetch error:", error);
51
+ this.error = error?.message || "Failed to load context window";
52
+ this.isLoading = false;
53
+ this.updateModalTitle(); // Show error in title
54
+ }
55
+ },
56
+
57
+ scheduleEditorInit() {
58
+ // Use double requestAnimationFrame to ensure DOM is ready
59
+ window.requestAnimationFrame(() => {
60
+ if (this.isLoading || this.error) return;
61
+ window.requestAnimationFrame(() => this.initEditor());
62
+ });
63
+ },
64
+
65
+ initEditor() {
66
+ const container = document.getElementById("context-viewer-container");
67
+ if (!container) {
68
+ console.warn("Context container not found, deferring editor init");
69
+ return;
70
+ }
71
+
72
+ // Destroy old instance if exists
73
+ if (this.editor?.destroy) {
74
+ this.editor.destroy();
75
+ }
76
+
77
+ // Check if ACE is available
78
+ if (!window.ace?.edit) {
79
+ console.error("ACE editor not available");
80
+ this.error = "Editor library not loaded";
81
+ return;
82
+ }
83
+
84
+ const editorInstance = window.ace.edit("context-viewer-container");
85
+ if (!editorInstance) {
86
+ console.error("Failed to create ACE editor instance");
87
+ return;
88
+ }
89
+
90
+ this.editor = editorInstance;
91
+
92
+ // Configure theme based on dark mode (legacy parity: != "false")
93
+ const darkMode = window.localStorage?.getItem("darkMode");
94
+ const theme = darkMode !== "false" ? "ace/theme/github_dark" : "ace/theme/tomorrow";
95
+
96
+ this.editor.setTheme(theme);
97
+ this.editor.session.setMode("ace/mode/markdown");
98
+ this.editor.setValue(this.contextData, -1); // -1 moves cursor to start
99
+ this.editor.setReadOnly(true);
100
+ this.editor.clearSelection();
101
+ },
102
+
103
+ updateModalTitle() {
104
+ window.requestAnimationFrame(() => {
105
+ const modalTitles = document.querySelectorAll(".modal.show .modal-title");
106
+ if (!modalTitles.length) return;
107
+
108
+ // Get the last (topmost) modal title
109
+ const title = modalTitles[modalTitles.length - 1];
110
+ if (!title) return;
111
+
112
+ if (this.error) {
113
+ title.textContent = "Context Window – Error";
114
+ } else if (this.isLoading) {
115
+ title.textContent = "Context Window (loading…)";
116
+ } else {
117
+ title.textContent = `Context Window ~${this.tokenCount} tokens`;
118
+ }
119
+ });
120
+ },
121
+
122
+ // Optional: cleanup method for lifecycle management
123
+ destroy() {
124
+ if (this.editor?.destroy) {
125
+ this.editor.destroy();
126
+ }
127
+ this.editor = null;
128
+ },
129
+ };
130
+
131
+ export const store = createStore("context", model);
132
+
webui/components/modals/context.html ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <html>
2
+ <head>
3
+ <title>Context Window</title>
4
+ <script type="module">
5
+ import { store } from "/components/modals/context-store.js";
6
+ </script>
7
+ </head>
8
+ <body>
9
+ <div x-data>
10
+ <template x-if="$store.context">
11
+ <div class="context-modal-root">
12
+
13
+ <!-- Loading State -->
14
+ <template x-if="$store.context.isLoading">
15
+ <div class="loading-state">
16
+ <div class="loading-spinner"></div>
17
+ <p>Loading context window…</p>
18
+ </div>
19
+ </template>
20
+
21
+ <!-- Error State -->
22
+ <template x-if="$store.context.error">
23
+ <div class="error-state">
24
+ <p class="error-message" x-text="$store.context.error"></p>
25
+ </div>
26
+ </template>
27
+
28
+ <!-- Content (ACE Editor) -->
29
+ <template x-if="!$store.context.isLoading && !$store.context.error">
30
+ <div id="context-viewer-container"></div>
31
+ </template>
32
+
33
+ </div>
34
+ </template>
35
+ </div>
36
+
37
+ <style>
38
+ .context-modal-root {
39
+ display: flex;
40
+ flex-direction: column;
41
+ width: 100%;
42
+ height: 100%;
43
+ min-height: 400px;
44
+ }
45
+
46
+ .loading-state,
47
+ .error-state {
48
+ display: flex;
49
+ flex-direction: column;
50
+ align-items: center;
51
+ justify-content: center;
52
+ padding: var(--spacing-lg);
53
+ min-height: 200px;
54
+ }
55
+
56
+ .loading-spinner {
57
+ width: 40px;
58
+ height: 40px;
59
+ border: 3px solid var(--color-border);
60
+ border-top: 3px solid var(--color-primary);
61
+ border-radius: 50%;
62
+ animation: spin 1s linear infinite;
63
+ margin-bottom: var(--spacing-md);
64
+ }
65
+
66
+ @keyframes spin {
67
+ 0% { transform: rotate(0deg); }
68
+ 100% { transform: rotate(360deg); }
69
+ }
70
+
71
+ .loading-state p,
72
+ .error-state p {
73
+ color: var(--color-text-secondary);
74
+ margin: 0;
75
+ }
76
+
77
+ .error-message {
78
+ color: var(--color-error);
79
+ font-weight: 500;
80
+ }
81
+
82
+ /* ACE Editor Container */
83
+ #context-viewer-container {
84
+ width: 100%;
85
+ height: 71vh;
86
+ border-radius: 0.4rem;
87
+ overflow: auto;
88
+ }
89
+
90
+ #context-viewer-container::-webkit-scrollbar {
91
+ width: 0;
92
+ }
93
+
94
+ /* ACE Editor Scrollbar */
95
+ .ace_scrollbar-v {
96
+ overflow-y: auto;
97
+ }
98
+
99
+ /* Viewer Styles */
100
+ .context-modal-root {
101
+ overflow: hidden;
102
+ }
103
+ </style>
104
+
105
+ </body>
106
+ </html>
107
+
webui/components/modals/history-store.js ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createStore } from "/js/AlpineStore.js";
2
+
3
+ const model = {
4
+ // State
5
+ isLoading: false,
6
+ historyData: null,
7
+ tokenCount: 0,
8
+ error: null,
9
+ editor: null,
10
+ closePromise: null,
11
+
12
+ // Open History modal
13
+ async open() {
14
+ if (this.isLoading) return; // Prevent double-open
15
+
16
+ this.isLoading = true;
17
+ this.error = null;
18
+ this.historyData = null;
19
+ this.tokenCount = 0;
20
+
21
+ try {
22
+ // Open modal FIRST (immediate UI feedback, but DON'T await)
23
+ this.closePromise = window.openModal('modals/history.html');
24
+
25
+ // Setup cleanup on modal close
26
+ if (this.closePromise && typeof this.closePromise.then === 'function') {
27
+ this.closePromise.then(() => {
28
+ this.destroy();
29
+ });
30
+ }
31
+
32
+ this.updateModalTitle(); // Set initial "loading" title
33
+
34
+ // Fetch data from backend
35
+ const contextId = window.getContext();
36
+ const response = await window.sendJsonData('/history_get', {
37
+ context: contextId,
38
+ });
39
+
40
+ // Update state with data
41
+ this.historyData = response.history;
42
+ this.tokenCount = response.tokens || 0;
43
+ this.isLoading = false;
44
+ this.updateModalTitle(); // Update with token count
45
+
46
+ // Initialize ACE editor
47
+ this.scheduleEditorInit();
48
+
49
+ } catch (error) {
50
+ console.error("History fetch error:", error);
51
+ this.error = error?.message || "Failed to load history";
52
+ this.isLoading = false;
53
+ this.updateModalTitle(); // Show error in title
54
+ }
55
+ },
56
+
57
+ scheduleEditorInit() {
58
+ // Use double requestAnimationFrame to ensure DOM is ready
59
+ window.requestAnimationFrame(() => {
60
+ if (this.isLoading || this.error) return;
61
+ window.requestAnimationFrame(() => this.initEditor());
62
+ });
63
+ },
64
+
65
+ initEditor() {
66
+ const container = document.getElementById("history-viewer-container");
67
+ if (!container) {
68
+ console.warn("History container not found, deferring editor init");
69
+ return;
70
+ }
71
+
72
+ // Destroy old instance if exists
73
+ if (this.editor?.destroy) {
74
+ this.editor.destroy();
75
+ }
76
+
77
+ // Check if ACE is available
78
+ if (!window.ace?.edit) {
79
+ console.error("ACE editor not available");
80
+ this.error = "Editor library not loaded";
81
+ return;
82
+ }
83
+
84
+ const editorInstance = window.ace.edit("history-viewer-container");
85
+ if (!editorInstance) {
86
+ console.error("Failed to create ACE editor instance");
87
+ return;
88
+ }
89
+
90
+ this.editor = editorInstance;
91
+
92
+ // Configure theme based on dark mode (legacy parity: != "false")
93
+ const darkMode = window.localStorage?.getItem("darkMode");
94
+ const theme = darkMode !== "false" ? "ace/theme/github_dark" : "ace/theme/tomorrow";
95
+
96
+ this.editor.setTheme(theme);
97
+ this.editor.session.setMode("ace/mode/markdown");
98
+ this.editor.setValue(this.historyData, -1); // -1 moves cursor to start
99
+ this.editor.setReadOnly(true);
100
+ this.editor.clearSelection();
101
+ },
102
+
103
+ updateModalTitle() {
104
+ window.requestAnimationFrame(() => {
105
+ const modalTitles = document.querySelectorAll(".modal.show .modal-title");
106
+ if (!modalTitles.length) return;
107
+
108
+ // Get the last (topmost) modal title
109
+ const title = modalTitles[modalTitles.length - 1];
110
+ if (!title) return;
111
+
112
+ if (this.error) {
113
+ title.textContent = "History – Error";
114
+ } else if (this.isLoading) {
115
+ title.textContent = "History (loading…)";
116
+ } else {
117
+ title.textContent = `History ~${this.tokenCount} tokens`;
118
+ }
119
+ });
120
+ },
121
+
122
+ // Optional: cleanup method for lifecycle management
123
+ destroy() {
124
+ if (this.editor?.destroy) {
125
+ this.editor.destroy();
126
+ }
127
+ this.editor = null;
128
+ },
129
+ };
130
+
131
+ export const store = createStore("history", model);
132
+
webui/components/modals/history.html ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <html>
2
+ <head>
3
+ <title>History</title>
4
+ <script type="module">
5
+ import { store } from "/components/modals/history-store.js";
6
+ </script>
7
+ </head>
8
+ <body>
9
+ <div x-data>
10
+ <template x-if="$store.history">
11
+ <div class="history-modal-root">
12
+
13
+ <!-- Loading State -->
14
+ <template x-if="$store.history.isLoading">
15
+ <div class="loading-state">
16
+ <div class="loading-spinner"></div>
17
+ <p>Loading history…</p>
18
+ </div>
19
+ </template>
20
+
21
+ <!-- Error State -->
22
+ <template x-if="$store.history.error">
23
+ <div class="error-state">
24
+ <p class="error-message" x-text="$store.history.error"></p>
25
+ </div>
26
+ </template>
27
+
28
+ <!-- Content (ACE Editor) -->
29
+ <template x-if="!$store.history.isLoading && !$store.history.error">
30
+ <div id="history-viewer-container"></div>
31
+ </template>
32
+
33
+ </div>
34
+ </template>
35
+ </div>
36
+
37
+ <style>
38
+ .history-modal-root {
39
+ display: flex;
40
+ flex-direction: column;
41
+ width: 100%;
42
+ height: 100%;
43
+ min-height: 400px;
44
+ }
45
+
46
+ .loading-state,
47
+ .error-state {
48
+ display: flex;
49
+ flex-direction: column;
50
+ align-items: center;
51
+ justify-content: center;
52
+ padding: var(--spacing-lg);
53
+ min-height: 200px;
54
+ }
55
+
56
+ .loading-spinner {
57
+ width: 40px;
58
+ height: 40px;
59
+ border: 3px solid var(--color-border);
60
+ border-top: 3px solid var(--color-primary);
61
+ border-radius: 50%;
62
+ animation: spin 1s linear infinite;
63
+ margin-bottom: var(--spacing-md);
64
+ }
65
+
66
+ @keyframes spin {
67
+ 0% { transform: rotate(0deg); }
68
+ 100% { transform: rotate(360deg); }
69
+ }
70
+
71
+ .loading-state p,
72
+ .error-state p {
73
+ color: var(--color-text-secondary);
74
+ margin: 0;
75
+ }
76
+
77
+ .error-message {
78
+ color: var(--color-error);
79
+ font-weight: 500;
80
+ }
81
+
82
+ /* ACE Editor Container */
83
+ #history-viewer-container {
84
+ width: 100%;
85
+ height: 71vh;
86
+ border-radius: 0.4rem;
87
+ overflow: auto;
88
+ }
89
+
90
+ #history-viewer-container::-webkit-scrollbar {
91
+ width: 0;
92
+ }
93
+
94
+ /* ACE Editor Scrollbar */
95
+ .ace_scrollbar-v {
96
+ overflow-y: auto;
97
+ }
98
+
99
+ /* Viewer Styles (legacy parity) */
100
+ .history-modal-root {
101
+ overflow: hidden;
102
+ }
103
+ </style>
104
+
105
+ </body>
106
+ </html>
107
+
webui/components/modals/image-viewer-store.js ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createStore } from "/js/AlpineStore.js";
2
+
3
+ const model = {
4
+ // State
5
+ currentImageUrl: null,
6
+ currentImageName: null,
7
+ baseImageUrl: null,
8
+ imageLoaded: false,
9
+ imageError: false,
10
+ zoomLevel: 1,
11
+ refreshInterval: 0,
12
+ activeIntervalId: null,
13
+ closePromise: null,
14
+
15
+ /**
16
+ * Open image viewer modal
17
+ * @param {string} imageUrl - URL of the image to display
18
+ * @param {number|object} refreshOrOptions - Either:
19
+ * - number: refresh interval in ms (legacy compat)
20
+ * - object: { refreshInterval?: number, name?: string }
21
+ */
22
+ async open(imageUrl, refreshOrOptions) {
23
+ // Parse options (backward compatibility)
24
+ const options = typeof refreshOrOptions === 'number'
25
+ ? { refreshInterval: refreshOrOptions, name: null }
26
+ : refreshOrOptions || {};
27
+
28
+ // Reset state
29
+ this.baseImageUrl = imageUrl;
30
+ this.refreshInterval = options.refreshInterval || 0;
31
+ this.currentImageName = options.name || this.extractImageName(imageUrl);
32
+ this.imageLoaded = false;
33
+ this.imageError = false;
34
+ this.zoomLevel = 1;
35
+
36
+ // Add timestamp for cache-busting if refreshing
37
+ this.currentImageUrl = this.refreshInterval > 0
38
+ ? this.addTimestamp(imageUrl)
39
+ : imageUrl;
40
+
41
+ try {
42
+ // Open modal and track close promise for cleanup
43
+ this.closePromise = window.openModal('modals/image-viewer.html');
44
+
45
+ // Setup cleanup on modal close
46
+ if (this.closePromise && typeof this.closePromise.finally === 'function') {
47
+ this.closePromise.finally(() => {
48
+ this.stopRefresh();
49
+ this.resetState();
50
+ });
51
+ }
52
+
53
+ // Start refresh loop if needed
54
+ if (this.refreshInterval > 0) {
55
+ this.setupAutoRefresh();
56
+ }
57
+ } catch (error) {
58
+ console.error("Image viewer error:", error);
59
+ this.imageError = true;
60
+ }
61
+ },
62
+
63
+ setupAutoRefresh() {
64
+ // Clear any existing interval
65
+ this.stopRefresh();
66
+
67
+ this.activeIntervalId = setInterval(() => {
68
+ if (!this.isModalVisible()) {
69
+ this.stopRefresh();
70
+ return;
71
+ }
72
+ this.preloadNextImage();
73
+ }, this.refreshInterval);
74
+ },
75
+
76
+ async preloadNextImage() {
77
+ const nextSrc = this.addTimestamp(this.baseImageUrl);
78
+
79
+ // Create a promise that resolves when the image is loaded
80
+ const preloadPromise = new Promise((resolve, reject) => {
81
+ const tempImg = new Image();
82
+ tempImg.onload = () => resolve(nextSrc);
83
+ tempImg.onerror = reject;
84
+ tempImg.src = nextSrc;
85
+ });
86
+
87
+ try {
88
+ // Wait for preload to complete
89
+ const loadedSrc = await preloadPromise;
90
+
91
+ // Check if modal is still visible before updating
92
+ if (this.isModalVisible()) {
93
+ this.currentImageUrl = loadedSrc;
94
+ this.imageLoaded = false; // Trigger reload animation
95
+ }
96
+ } catch (err) {
97
+ console.error('Failed to preload image:', err);
98
+ }
99
+ },
100
+
101
+ isModalVisible() {
102
+ const container = document.querySelector('#image-viewer-wrapper');
103
+ if (!container) return false;
104
+
105
+ // Check if element or any parent is hidden
106
+ let element = container;
107
+ while (element) {
108
+ const styles = window.getComputedStyle(element);
109
+ if (styles.display === 'none' || styles.visibility === 'hidden') {
110
+ return false;
111
+ }
112
+ element = element.parentElement;
113
+ }
114
+ return true;
115
+ },
116
+
117
+ stopRefresh() {
118
+ if (this.activeIntervalId !== null) {
119
+ clearInterval(this.activeIntervalId);
120
+ this.activeIntervalId = null;
121
+ }
122
+ },
123
+
124
+ resetState() {
125
+ this.currentImageUrl = null;
126
+ this.currentImageName = null;
127
+ this.baseImageUrl = null;
128
+ this.imageLoaded = false;
129
+ this.imageError = false;
130
+ this.zoomLevel = 1;
131
+ this.refreshInterval = 0;
132
+ },
133
+
134
+ // Zoom controls
135
+ zoomIn() {
136
+ this.zoomLevel = Math.min(this.zoomLevel * 1.2, 5); // Max 5x zoom
137
+ this.updateImageZoom();
138
+ },
139
+
140
+ zoomOut() {
141
+ this.zoomLevel = Math.max(this.zoomLevel / 1.2, 0.1); // Min 0.1x zoom
142
+ this.updateImageZoom();
143
+ },
144
+
145
+ resetZoom() {
146
+ this.zoomLevel = 1;
147
+ this.updateImageZoom();
148
+ },
149
+
150
+ updateImageZoom() {
151
+ const img = document.querySelector(".modal-image");
152
+ if (img) {
153
+ img.style.transform = `scale(${this.zoomLevel})`;
154
+ }
155
+ },
156
+
157
+ // Utility methods
158
+ addTimestamp(url) {
159
+ try {
160
+ const urlObj = new URL(url, window.location.origin);
161
+ urlObj.searchParams.set("t", Date.now().toString());
162
+ return urlObj.toString();
163
+ } catch (e) {
164
+ // Fallback for invalid URLs
165
+ const separator = url.includes('?') ? '&' : '?';
166
+ return `${url}${separator}t=${Date.now()}`;
167
+ }
168
+ },
169
+
170
+ extractImageName(url) {
171
+ try {
172
+ const urlObj = new URL(url, window.location.origin);
173
+ const pathname = urlObj.pathname;
174
+ return pathname.split("/").pop() || "Image";
175
+ } catch (e) {
176
+ return url.split("/").pop() || "Image";
177
+ }
178
+ },
179
+
180
+ // Optional: cleanup on store destruction
181
+ destroy() {
182
+ this.stopRefresh();
183
+ this.resetState();
184
+ },
185
+ };
186
+
187
+ export const store = createStore("imageViewer", model);
188
+
webui/components/{chat/attachments/imageModal.html → modals/image-viewer.html} RENAMED
@@ -4,42 +4,42 @@
4
  <title>Image Viewer</title>
5
 
6
  <script type="module">
7
- import { store } from "/components/chat/attachments/attachmentsStore.js";
8
  </script>
9
  </head>
10
 
11
  <body>
12
  <div x-data>
13
- <template x-if="$store.chatAttachments">
14
- <div id="image-modal-content" class="image-modal-container">
15
  <!-- Image display area -->
16
  <div class="image-display-wrapper">
17
  <img
18
- x-show="$store.chatAttachments.currentImageUrl"
19
- :src="$store.chatAttachments.currentImageUrl"
20
- :alt="$store.chatAttachments.currentImageName || 'Image'"
21
  class="modal-image"
22
- @load="$store.chatAttachments.imageLoaded = true"
23
- @error="$store.chatAttachments.imageError = true"
24
  />
25
 
26
  <!-- Loading indicator -->
27
- <div x-show="!$store.chatAttachments.imageLoaded && !$store.chatAttachments.imageError" class="loading-indicator">
28
  <div class="loading-spinner"></div>
29
  <p>Loading image...</p>
30
  </div>
31
 
32
  <!-- Error indicator -->
33
- <div x-show="$store.chatAttachments.imageError" class="error-indicator">
34
  <p>Failed to load image</p>
35
  </div>
36
  </div>
37
 
38
  <!-- Simple zoom controls -->
39
  <div class="zoom-controls">
40
- <button @click="$store.chatAttachments.zoomOut()" class="zoom-btn" title="Zoom Out">−</button>
41
- <button @click="$store.chatAttachments.resetZoom()" class="zoom-btn" title="Reset">⌂</button>
42
- <button @click="$store.chatAttachments.zoomIn()" class="zoom-btn" title="Zoom In">+</button>
43
  </div>
44
  </div>
45
  </template>
@@ -145,4 +145,5 @@
145
 
146
  </body>
147
 
148
- </html>
 
 
4
  <title>Image Viewer</title>
5
 
6
  <script type="module">
7
+ import { store } from "/components/modals/image-viewer-store.js";
8
  </script>
9
  </head>
10
 
11
  <body>
12
  <div x-data>
13
+ <template x-if="$store.imageViewer">
14
+ <div id="image-viewer-wrapper" class="image-modal-container">
15
  <!-- Image display area -->
16
  <div class="image-display-wrapper">
17
  <img
18
+ x-show="$store.imageViewer.currentImageUrl"
19
+ :src="$store.imageViewer.currentImageUrl"
20
+ :alt="$store.imageViewer.currentImageName || 'Image'"
21
  class="modal-image"
22
+ @load="$store.imageViewer.imageLoaded = true"
23
+ @error="$store.imageViewer.imageError = true"
24
  />
25
 
26
  <!-- Loading indicator -->
27
+ <div x-show="!$store.imageViewer.imageLoaded && !$store.imageViewer.imageError" class="loading-indicator">
28
  <div class="loading-spinner"></div>
29
  <p>Loading image...</p>
30
  </div>
31
 
32
  <!-- Error indicator -->
33
+ <div x-show="$store.imageViewer.imageError" class="error-indicator">
34
  <p>Failed to load image</p>
35
  </div>
36
  </div>
37
 
38
  <!-- Simple zoom controls -->
39
  <div class="zoom-controls">
40
+ <button @click="$store.imageViewer.zoomOut()" class="zoom-btn" title="Zoom Out">−</button>
41
+ <button @click="$store.imageViewer.resetZoom()" class="zoom-btn" title="Reset">⌂</button>
42
+ <button @click="$store.imageViewer.zoomIn()" class="zoom-btn" title="Zoom In">+</button>
43
  </div>
44
  </div>
45
  </template>
 
145
 
146
  </body>
147
 
148
+ </html>
149
+
webui/css/history.css DELETED
@@ -1,25 +0,0 @@
1
- /* History Styles */
2
-
3
- /* ACE Editor Scrollbar */
4
- .ace_scrollbar-v {
5
- overflow-y: auto;
6
- }
7
-
8
- /* JSON Viewer Container */
9
- #json-viewer-container {
10
- width: 100%;
11
- height: 71vh;
12
- border-radius: 0.4rem;
13
- overflow: auto;
14
- }
15
-
16
- #json-viewer-container::-webkit-scrollbar {
17
- width: 0;
18
- }
19
-
20
- /* Viewer Styles */
21
- .history-viewer {
22
- overflow: hidden;
23
- margin-bottom: 0.5rem;
24
- }
25
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/index.html CHANGED
@@ -14,7 +14,6 @@
14
  <link rel="stylesheet" href="css/modals.css">
15
  <link rel="stylesheet" href="css/modals2.css">
16
  <link rel="stylesheet" href="css/speech.css">
17
- <link rel="stylesheet" href="css/history.css">
18
  <link rel="stylesheet" href="css/scheduler-datepicker.css">
19
  <link rel="stylesheet" href="css/notification.css">
20
 
@@ -97,7 +96,6 @@
97
 
98
  <!-- Load module scripts first -->
99
  <script type="module" src="js/scheduler.js"></script>
100
- <script type="module" src="js/history.js"></script>
101
 
102
  <script type="module" src="index.js"></script>
103
 
@@ -127,7 +125,6 @@
127
  <!-- Non-module scripts after Alpine.js -->
128
  <script type="text/javascript" src="js/settings.js"></script>
129
  <script type="text/javascript" src="js/file_browser.js"></script>
130
- <script type="text/javascript" src="js/modal.js"></script>
131
  <script type="module" src="js/tunnel.js"></script>
132
  <script>
133
  // Expose git info for sidebar component
 
14
  <link rel="stylesheet" href="css/modals.css">
15
  <link rel="stylesheet" href="css/modals2.css">
16
  <link rel="stylesheet" href="css/speech.css">
 
17
  <link rel="stylesheet" href="css/scheduler-datepicker.css">
18
  <link rel="stylesheet" href="css/notification.css">
19
 
 
96
 
97
  <!-- Load module scripts first -->
98
  <script type="module" src="js/scheduler.js"></script>
 
99
 
100
  <script type="module" src="index.js"></script>
101
 
 
125
  <!-- Non-module scripts after Alpine.js -->
126
  <script type="text/javascript" src="js/settings.js"></script>
127
  <script type="text/javascript" src="js/file_browser.js"></script>
 
128
  <script type="module" src="js/tunnel.js"></script>
129
  <script>
130
  // Expose git info for sidebar component
webui/js/history.js DELETED
@@ -1,55 +0,0 @@
1
- import { getContext } from "../index.js";
2
-
3
- export async function openHistoryModal() {
4
- try {
5
- const hist = await window.sendJsonData("/history_get", { context: getContext() });
6
- // const data = JSON.stringify(hist.history, null, 4);
7
- const data = hist.history
8
- const size = hist.tokens
9
- await showEditorModal(data, "markdown", `History ~${size} tokens`, "Conversation history visible to the LLM. History is compressed to fit into the context window over time.");
10
- } catch (e) {
11
- window.toastFrontendError("Error fetching history: " + e.message, "Chat History Error");
12
- return
13
- }
14
- }
15
-
16
- export async function openCtxWindowModal() {
17
- try {
18
- const win = await window.sendJsonData("/ctx_window_get", { context: getContext() });
19
- const data = win.content
20
- const size = win.tokens
21
- await showEditorModal(data, "markdown", `Context window ~${size} tokens`, "Data passed to the LLM during last interaction. Contains system message, conversation history and RAG.");
22
- } catch (e) {
23
- window.toastFrontendError("Error fetching context: " + e.message, "Context Error");
24
- return
25
- }
26
- }
27
-
28
- async function showEditorModal(data, type = "json", title, description = "") {
29
- // Generate the HTML with JSON Viewer container
30
- const html = `<div id="json-viewer-container"></div>`;
31
-
32
- // Open the modal with the generated HTML
33
- await window.genericModalProxy.openModal(title, description, html, ["history-viewer"]);
34
-
35
- // Initialize the JSON Viewer after the modal is rendered
36
- const container = document.getElementById("json-viewer-container");
37
- if (container) {
38
- const editor = ace.edit("json-viewer-container");
39
-
40
- const dark = localStorage.getItem('darkMode')
41
- if (dark != "false") {
42
- editor.setTheme("ace/theme/github_dark");
43
- } else {
44
- editor.setTheme("ace/theme/tomorrow");
45
- }
46
-
47
- editor.session.setMode("ace/mode/" + type);
48
- editor.setValue(data);
49
- editor.clearSelection();
50
- // editor.session.$toggleFoldWidget(5, {})
51
- }
52
- }
53
-
54
- window.openHistoryModal = openHistoryModal;
55
- window.openCtxWindowModal = openCtxWindowModal;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/js/image_modal.js DELETED
@@ -1,87 +0,0 @@
1
- // Singleton interval ID for image refresh
2
- let activeIntervalId = null;
3
-
4
- export async function openImageModal(src, refreshInterval = 0) {
5
- try {
6
- let imgSrc = src;
7
-
8
- // Clear any existing refresh interval
9
- if (activeIntervalId !== null) {
10
- clearInterval(activeIntervalId);
11
- activeIntervalId = null;
12
- }
13
-
14
- if (refreshInterval > 0) {
15
- // Add or update timestamp to bypass cache
16
- const addTimestamp = (url) => {
17
- const urlObj = new URL(url, window.location.origin);
18
- urlObj.searchParams.set('t', Date.now());
19
- return urlObj.toString();
20
- };
21
-
22
- // Check if image viewer is still active
23
- const isImageViewerActive = () => {
24
- const container = document.querySelector('#image-viewer-container');
25
- if (!container) return false;
26
-
27
- // Check if element or any parent is hidden
28
- let element = container;
29
- while (element) {
30
- const style = window.getComputedStyle(element);
31
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
32
- return false;
33
- }
34
- element = element.parentElement;
35
- }
36
- return true;
37
- };
38
-
39
- // Preload next image before displaying
40
- const preloadAndUpdate = async (currentImg) => {
41
- const nextSrc = addTimestamp(src);
42
- // Create a promise that resolves when the image is loaded
43
- const preloadPromise = new Promise((resolve, reject) => {
44
- const tempImg = new Image();
45
- tempImg.onload = () => resolve(nextSrc);
46
- tempImg.onerror = reject;
47
- tempImg.src = nextSrc;
48
- });
49
-
50
- try {
51
- // Wait for preload to complete
52
- const loadedSrc = await preloadPromise;
53
- // Check if this interval is still the active one
54
- if (currentImg && isImageViewerActive()) {
55
- currentImg.src = loadedSrc;
56
- }
57
- } catch (err) {
58
- console.error('Failed to preload image:', err);
59
- }
60
- };
61
-
62
- imgSrc = addTimestamp(src);
63
-
64
- // Set up periodic refresh with preloading
65
- activeIntervalId = setInterval(() => {
66
- if (!isImageViewerActive()) {
67
- clearInterval(activeIntervalId);
68
- activeIntervalId = null;
69
- return;
70
- }
71
- const img = document.querySelector('.image-viewer-img');
72
- if (img) {
73
- preloadAndUpdate(img);
74
- }
75
- }, refreshInterval);
76
- }
77
-
78
- const html = `<div id="image-viewer-container"><img class="image-viewer-img" src="${imgSrc}" /></div>`;
79
- const fileName = src.split("/").pop();
80
-
81
- // Open the modal with the generated HTML
82
- await window.genericModalProxy.openModal(fileName, "", html);
83
- } catch (e) {
84
- window.toastFrontendError("Error fetching history: " + e.message, "Image History Error");
85
- return;
86
- }
87
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
webui/js/messages.js CHANGED
@@ -1,5 +1,5 @@
1
  // message actions and components
2
- import { openImageModal } from "./image_modal.js";
3
  import { marked } from "../vendor/marked/marked.esm.js";
4
  import { store as _messageResizeStore } from "/components/messages/resize/message-resize-store.js"; // keep here, required in html
5
  import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
@@ -852,7 +852,7 @@ function drawKvpsIncremental(container, kvps, latex) {
852
  // Add click handler and cursor change
853
  imgElement.style.cursor = "pointer";
854
  imgElement.addEventListener("click", () => {
855
- openImageModal(imgElement.src, 1000);
856
  });
857
  } else {
858
  const pre = document.createElement("pre");
 
1
  // message actions and components
2
+ import { store as imageViewerStore } from "/components/modals/image-viewer-store.js";
3
  import { marked } from "../vendor/marked/marked.esm.js";
4
  import { store as _messageResizeStore } from "/components/messages/resize/message-resize-store.js"; // keep here, required in html
5
  import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
 
852
  // Add click handler and cursor change
853
  imgElement.style.cursor = "pointer";
854
  imgElement.addEventListener("click", () => {
855
+ imageViewerStore.open(imgElement.src, { refreshInterval: 1000 });
856
  });
857
  } else {
858
  const pre = document.createElement("pre");
webui/js/modal.js DELETED
@@ -1,44 +0,0 @@
1
- // Full-Screen Input Modal logic moved to /components/modals/full-screen-store.js
2
- // Keep only other modals here.
3
-
4
- const genericModalProxy = {
5
- isOpen: false,
6
- isLoading: false,
7
- title: '',
8
- description: '',
9
- html: '',
10
-
11
- async openModal(title, description, html, contentClasses = []) {
12
- const modalEl = document.getElementById('genericModal');
13
- const modalContent = document.getElementById('viewer');
14
- const modalAD = Alpine.$data(modalEl);
15
-
16
- modalAD.isOpen = true;
17
- modalAD.title = title
18
- modalAD.description = description
19
- modalAD.html = html
20
-
21
- modalContent.className = 'modal-content';
22
- modalContent.classList.add(...contentClasses);
23
- },
24
-
25
- handleClose() {
26
- this.isOpen = false;
27
- }
28
- }
29
-
30
- // Wait for Alpine to be ready
31
- document.addEventListener('alpine:init', () => {
32
- Alpine.data('genericModalProxy', () => ({
33
- init() {
34
- Object.assign(this, genericModalProxy);
35
- // Ensure immediate file fetch when modal opens
36
- this.$watch('isOpen', async (value) => {
37
- // what now?
38
- });
39
- }
40
- }));
41
- });
42
-
43
- // Keep the global assignment for backward compatibility
44
- window.genericModalProxy = genericModalProxy;