Spaces:
Paused
Paused
Alessandro commited on
Commit ·
93b7cae
1
Parent(s): bc5061f
history/context/image modals components
Browse files- webui/components/chat/attachments/attachmentsStore.js +3 -53
- webui/components/chat/attachments/inputPreview.html +2 -1
- webui/components/chat/input/bottom-actions.html +2 -2
- webui/components/modals/context-store.js +132 -0
- webui/components/modals/context.html +107 -0
- webui/components/modals/history-store.js +132 -0
- webui/components/modals/history.html +107 -0
- webui/components/modals/image-viewer-store.js +188 -0
- webui/components/{chat/attachments/imageModal.html → modals/image-viewer.html} +15 -14
- webui/css/history.css +0 -25
- webui/index.html +0 -3
- webui/js/history.js +0 -55
- webui/js/image_modal.js +0 -87
- webui/js/messages.js +2 -2
- webui/js/modal.js +0 -44
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 |
-
|
| 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 |
-
|
| 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.
|
| 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="
|
| 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="
|
| 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/
|
| 8 |
</script>
|
| 9 |
</head>
|
| 10 |
|
| 11 |
<body>
|
| 12 |
<div x-data>
|
| 13 |
-
<template x-if="$store.
|
| 14 |
-
<div id="image-
|
| 15 |
<!-- Image display area -->
|
| 16 |
<div class="image-display-wrapper">
|
| 17 |
<img
|
| 18 |
-
x-show="$store.
|
| 19 |
-
:src="$store.
|
| 20 |
-
:alt="$store.
|
| 21 |
class="modal-image"
|
| 22 |
-
@load="$store.
|
| 23 |
-
@error="$store.
|
| 24 |
/>
|
| 25 |
|
| 26 |
<!-- Loading indicator -->
|
| 27 |
-
<div x-show="!$store.
|
| 28 |
<div class="loading-spinner"></div>
|
| 29 |
<p>Loading image...</p>
|
| 30 |
</div>
|
| 31 |
|
| 32 |
<!-- Error indicator -->
|
| 33 |
-
<div x-show="$store.
|
| 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.
|
| 41 |
-
<button @click="$store.
|
| 42 |
-
<button @click="$store.
|
| 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 {
|
| 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 |
-
|
| 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;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|