Skip to content

Commit 06f6273

Browse files
committed
Improve image validation
1 parent 55c6542 commit 06f6273

File tree

1 file changed

+33
-5
lines changed

1 file changed

+33
-5
lines changed

special-pages/pages/new-tab/app/omnibar/components/AiChatForm.js

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import styles from './AiChatForm.module.css';
1414
* @typedef {import('../../../types/new-tab.js').OpenTarget} OpenTarget
1515
* @typedef {import('../../../types/new-tab.js').SubmitChatAction} SubmitChatAction
1616
* @typedef {import('../../../types/new-tab.js').AIModels} AIModels
17-
* @typedef {{ dataUrl: string }} AttachedImage
17+
* @typedef {{ dataUrl: string, fileName: string, mimeType: string }} AttachedImage
1818
*/
1919

2020
// File upload constraints - matching apple-browsers implementation
@@ -85,10 +85,24 @@ export function AiChatForm({ query, autoFocus, onChange, onSubmit }) {
8585
params.modelId = selectedModelId;
8686
}
8787
if (attachedImages.length > 0) {
88+
// Format conversion logic matching apple-browsers/AIChatOmnibarController.swift:
89+
// Preserve JPEG for .jpg/.jpeg sources, use PNG for all others (including WebP)
8890
params.images = attachedImages.map((img) => {
89-
const match = img.dataUrl.match(/^data:image\/(\w+);base64,(.+)$/);
90-
if (!match) return { data: img.dataUrl, format: 'png' };
91-
return { data: match[2], format: match[1] === 'webp' ? 'png' : match[1] };
91+
// Extract file extension from filename
92+
const ext = img.fileName.split('.').pop()?.toLowerCase() || '';
93+
const isJPEG = ext === 'jpg' || ext === 'jpeg';
94+
const format = isJPEG ? 'jpeg' : 'png';
95+
96+
// Parse and validate data URL
97+
const match = img.dataUrl.match(/^data:image\/(jpeg|jpg|png|webp);base64,(.+)$/);
98+
if (!match) {
99+
console.warn(`Invalid data URL format for file "${img.fileName}". Expected data:image/(jpeg|png|webp);base64,...`);
100+
// Fallback: try to extract just the base64 portion if format is malformed
101+
const base64Match = img.dataUrl.match(/base64,(.+)$/);
102+
return { data: base64Match ? base64Match[1] : img.dataUrl, format };
103+
}
104+
105+
return { data: match[2], format };
92106
});
93107
}
94108
onSubmit(params);
@@ -192,7 +206,11 @@ export function AiChatForm({ query, autoFocus, onChange, onSubmit }) {
192206

193207
reader.onload = () => {
194208
clearTimeout(timeoutId);
195-
resolve({ dataUrl: /** @type {string} */ (reader.result) });
209+
resolve({
210+
dataUrl: /** @type {string} */ (reader.result),
211+
fileName: file.name,
212+
mimeType: file.type,
213+
});
196214
};
197215

198216
reader.onerror = () => {
@@ -231,6 +249,13 @@ export function AiChatForm({ query, autoFocus, onChange, onSubmit }) {
231249

232250
const selectedModel = aiModels.find((m) => m.id === selectedModelId) ?? aiModels[0] ?? null;
233251

252+
// Default to first model on mount if available
253+
useEffect(() => {
254+
if (aiModels.length > 0 && !selectedModelId) {
255+
setSelectedModelId(aiModels[0].id);
256+
}
257+
}, [aiModels, selectedModelId]);
258+
234259
useEffect(() => {
235260
if (!modelDropdownOpen) return;
236261
/** @param {MouseEvent} e */
@@ -240,11 +265,14 @@ export function AiChatForm({ query, autoFocus, onChange, onSubmit }) {
240265
}
241266
};
242267
const handleScroll = () => setModelDropdownOpen(false);
268+
const handleResize = () => setModelDropdownOpen(false);
243269
document.addEventListener('click', handleClickOutside, true);
244270
window.addEventListener('scroll', handleScroll, true);
271+
window.addEventListener('resize', handleResize);
245272
return () => {
246273
document.removeEventListener('click', handleClickOutside, true);
247274
window.removeEventListener('scroll', handleScroll, true);
275+
window.removeEventListener('resize', handleResize);
248276
};
249277
}, [modelDropdownOpen]);
250278

0 commit comments

Comments
 (0)