Skip to content

Commit a4b5ccf

Browse files
authored
Merge pull request #80 from faetalize/dev
1.1.0
2 parents 2b5fcd9 + 556af4e commit a4b5ccf

45 files changed

Lines changed: 9536 additions & 871 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎package-lock.json‎

Lines changed: 628 additions & 66 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
"@types/uuid": "^9.0.8",
66
"@vitejs/plugin-basic-ssl": "^2.1.0",
77
"supabase": "^2.39.2",
8+
"tailwindcss": "^4.1.16",
89
"typescript": "^5.8.3",
910
"vite": "^6.3.5"
1011
},
1112
"dependencies": {
1213
"@google/genai": "^0.8.0",
1314
"@supabase/supabase-js": "^2.53.0",
15+
"@tailwindcss/vite": "^4.1.16",
1416
"buffer": "^6.0.3",
1517
"dexie": "^4.0.10",
1618
"dompurify": "^3.2.3",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { EditableImage, scrollToMessage } from "../../utils/imageHistory";
2+
3+
/**
4+
* Creates a history image preview element that shows an image from chat history
5+
* that will be used for editing.
6+
*/
7+
export function historyImagePreviewElement(image: EditableImage): HTMLElement {
8+
const container = document.createElement("div");
9+
container.classList.add("history-image-preview", "attachment-container");
10+
container.dataset.messageIndex = String(image.messageIndex);
11+
container.dataset.source = image.source;
12+
13+
// Create image element
14+
const img = document.createElement("img");
15+
img.src = image.dataUri;
16+
img.alt = "Image from chat history";
17+
img.classList.add("history-image-thumbnail");
18+
19+
// Create badge to indicate source
20+
const badge = document.createElement("span");
21+
badge.classList.add("history-image-badge");
22+
badge.textContent = "Local";
23+
24+
// Create remove button
25+
const removeButton = document.createElement("button");
26+
removeButton.classList.add("btn-textual", "material-symbols-outlined", "btn-remove-attachment");
27+
removeButton.textContent = "close";
28+
removeButton.title = "Clear preview";
29+
removeButton.addEventListener("click", (e) => {
30+
e.stopPropagation();
31+
container.remove();
32+
dispatchHistoryImageRemoved();
33+
});
34+
35+
// Make the container clickable to scroll to source message
36+
container.style.cursor = "pointer";
37+
container.title = "Click to view original message";
38+
container.addEventListener("click", (e) => {
39+
// Don't scroll if clicking the remove button
40+
if (e.target === removeButton) {
41+
return;
42+
}
43+
scrollToMessage(image.messageIndex);
44+
});
45+
46+
container.appendChild(img);
47+
container.appendChild(badge);
48+
container.appendChild(removeButton);
49+
50+
return container;
51+
}
52+
53+
/**
54+
* Dispatches a custom event when history image preview is removed
55+
*/
56+
function dispatchHistoryImageRemoved(): void {
57+
window.dispatchEvent(new CustomEvent("history-image-removed"));
58+
}

‎src/components/dynamic/message.ts‎

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export const messageElement = async (
9191
<div class="generated-image-wrapper" data-index="${idx}">
9292
<img class="generated-image" src="data:${img.mimeType};base64,${img.base64}" loading="lazy" />
9393
<div class="generated-image-overlay">
94+
<button class="btn-textual btn-image-action btn-edit material-symbols-outlined" title="Edit this image">edit</button>
95+
<button class="btn-textual btn-image-action btn-attach material-symbols-outlined" title="Attach this image">attachment</button>
9496
<button class="btn-textual btn-image-action btn-download material-symbols-outlined" title="Download">download</button>
9597
<button class="btn-textual btn-image-action btn-expand material-symbols-outlined" title="Expand">open_in_full</button>
9698
</div>
@@ -447,17 +449,64 @@ async function updateMessageInDatabase(markdownContent: string, messageIndex: nu
447449
}
448450
}
449451

450-
// Adds overlay button interactions (download + expand) & a lightweight lightbox
452+
// Adds overlay button interactions (download + expand + edit + attach) & a lightweight lightbox
451453
function setupGeneratedImageInteractions(root: HTMLElement) {
452454
const wrappers = root.querySelectorAll<HTMLElement>(".generated-image-wrapper");
453455
if (!wrappers.length) return;
454456

455457
wrappers.forEach(wrap => {
456458
const img = wrap.querySelector<HTMLImageElement>(".generated-image");
459+
const editBtn = wrap.querySelector<HTMLButtonElement>(".btn-edit");
460+
const attachBtn = wrap.querySelector<HTMLButtonElement>(".btn-attach");
457461
const downloadBtn = wrap.querySelector<HTMLButtonElement>(".btn-download");
458462
const expandBtn = wrap.querySelector<HTMLButtonElement>(".btn-expand");
459463
if (!img) return;
460464

465+
// Helper to convert base64 image to File
466+
const imageToFile = async (): Promise<File> => {
467+
const base64Data = img.src.split(',')[1];
468+
const mimeType = img.src.match(/data:(.*?);/)?.[1] || 'image/png';
469+
const byteString = atob(base64Data);
470+
const arrayBuffer = new ArrayBuffer(byteString.length);
471+
const uint8Array = new Uint8Array(arrayBuffer);
472+
for (let i = 0; i < byteString.length; i++) {
473+
uint8Array[i] = byteString.charCodeAt(i);
474+
}
475+
const blob = new Blob([uint8Array], { type: mimeType });
476+
const ext = mimeType.split('/')[1];
477+
return new File([blob], `image-${Date.now()}.${ext}`, { type: mimeType });
478+
};
479+
480+
// Edit button: attach image + toggle editing mode
481+
editBtn?.addEventListener('click', async (e) => {
482+
e.stopPropagation();
483+
try {
484+
const file = await imageToFile();
485+
486+
// Dispatch custom event instead of manually modifying input
487+
window.dispatchEvent(new CustomEvent('attach-image-from-chat', {
488+
detail: { file, toggleEditing: true }
489+
}));
490+
} catch (err) {
491+
console.error('Failed to attach image for editing', err);
492+
}
493+
});
494+
495+
// Attach button: just attach image without toggling editing
496+
attachBtn?.addEventListener('click', async (e) => {
497+
e.stopPropagation();
498+
try {
499+
const file = await imageToFile();
500+
501+
// Dispatch custom event instead of manually modifying input
502+
window.dispatchEvent(new CustomEvent('attach-image-from-chat', {
503+
detail: { file, toggleEditing: false }
504+
}));
505+
} catch (err) {
506+
console.error('Failed to attach image', err);
507+
}
508+
});
509+
461510
downloadBtn?.addEventListener('click', (e) => {
462511
e.stopPropagation();
463512
try {
@@ -494,23 +543,6 @@ function setupGeneratedImageInteractions(root: HTMLElement) {
494543

495544
// Dynamically adjust sizing to ensure containment within viewport preserving aspect
496545
const lightboxImg = overlay.querySelector<HTMLImageElement>('.lightbox-image');
497-
function fit() {
498-
if (!lightboxImg || !lightboxImg.naturalWidth) return;
499-
const vw = window.innerWidth * 0.95;
500-
const vh = window.innerHeight * 0.95;
501-
const { naturalWidth: iw, naturalHeight: ih } = lightboxImg;
502-
const ratio = Math.min(vw / iw, vh / ih, 1);
503-
lightboxImg.style.width = Math.round(iw * ratio) + 'px';
504-
lightboxImg.style.height = Math.round(ih * ratio) + 'px';
505-
}
506-
if (lightboxImg?.complete) {
507-
fit();
508-
} else {
509-
lightboxImg?.addEventListener('load', fit, { once: true });
510-
}
511-
window.addEventListener('resize', fit, { passive: true });
512-
// Cleanup resize listener when closed
513-
overlay.addEventListener('remove', () => window.removeEventListener('resize', fit));
514546
}
515547

516548
expandBtn?.addEventListener('click', (e) => { e.stopPropagation(); openLightbox(); });

‎src/components/static/AttachButton.component.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ attachButton.addEventListener("click", () => {
1111
return
1212
}
1313
input.value = ""; // Clear the input value to allow re-selection of the same file
14-
input.accept = "image/*,video/*,audio/*,application/pdf,text/plain"; // Accept images, videos, audio files, and PDFs, and text files
14+
input.accept = "image/*,application/pdf,text/plain"; // Accept images, PDFs, and text files
1515
input.multiple = true; // Allow multiple files to be selected
1616
input.click();
1717
});

‎src/components/static/AttachmentPreview.component.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export const attachmentPreviewElement = (file: File) => {
1313
const container = document.createElement("div");
1414
container.classList.add("attachment-container");
1515
container.dataset.attachmentSignature = getFileSignature(file);
16+
17+
// Check if this file is from chat history
18+
const isFromChatHistory = (file as any)._fromChatHistory === true;
19+
1620
if (file.type.startsWith("image/")) {
1721
const reader = new FileReader();
1822
reader.onload = (e) => {
@@ -28,8 +32,20 @@ export const attachmentPreviewElement = (file: File) => {
2832
img.src = e.target!.result as string;
2933
img.alt = file.name;
3034
container.appendChild(img);
35+
36+
// Add "Local" badge if from chat history
37+
if (isFromChatHistory) {
38+
const badge = document.createElement("span");
39+
badge.classList.add("history-image-badge");
40+
badge.textContent = "Local";
41+
container.appendChild(badge);
42+
}
43+
3144
container.appendChild(removeButton);
3245
attachmentPreview.appendChild(container);
46+
47+
// Dispatch attachment added event
48+
dispatchAttachmentAdded();
3349
};
3450
reader.readAsDataURL(file);
3551
}
@@ -60,6 +76,9 @@ export const attachmentPreviewElement = (file: File) => {
6076
container.appendChild(fileIcon);
6177
container.appendChild(fileDetailsDiv);
6278
container.appendChild(removeButton);
79+
80+
// Dispatch attachment added event
81+
dispatchAttachmentAdded();
6382
}
6483

6584

@@ -104,4 +123,22 @@ function dispatchAttachmentRemoved(file: File): void {
104123
detail,
105124
bubbles: true,
106125
}));
126+
}
127+
128+
function dispatchAttachmentAdded(): void {
129+
const count = getAttachmentCount();
130+
window.dispatchEvent(new CustomEvent("attachment-added", {
131+
detail: { count },
132+
bubbles: true,
133+
}));
134+
}
135+
136+
/**
137+
* Get the current number of image attachments
138+
*/
139+
export function getAttachmentCount(): number {
140+
if (!input || !input.files) {
141+
return 0;
142+
}
143+
return Array.from(input.files).filter(f => f.type.startsWith('image/')).length;
107144
}

0 commit comments

Comments
 (0)