@@ -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 ( / ^ d a t a : i m a g e \/ ( \w + ) ; b a s e 6 4 , ( .+ ) $ / ) ;
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 ( / ^ d a t a : i m a g e \/ ( j p e g | j p g | p n g | w e b p ) ; b a s e 6 4 , ( .+ ) $ / ) ;
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 ( / b a s e 6 4 , ( .+ ) $ / ) ;
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