-
-
Notifications
You must be signed in to change notification settings - Fork 7
Dual Image Resolution Search and QCX-TERRA Analysis #499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c7783fa
33b6dca
5c1c0be
e4839bd
3879252
66ae964
3c3f44b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ import { BotMessage } from '@/components/message' | |
| import { SearchSection } from '@/components/search-section' | ||
| import SearchRelated from '@/components/search-related' | ||
| import { GeoJsonLayer } from '@/components/map/geojson-layer' | ||
| import { ResolutionCarousel } from '@/components/resolution-carousel' | ||
| import { ResolutionImage } from '@/components/resolution-image' | ||
| import { CopilotDisplay } from '@/components/copilot-display' | ||
| import RetrieveSection from '@/components/retrieve-section' | ||
|
|
@@ -50,18 +51,29 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| } | ||
|
|
||
| if (action === 'resolution_search') { | ||
| const file = formData?.get('file') as File; | ||
| const file_mapbox = formData?.get('file_mapbox') as File; | ||
| const file_google = formData?.get('file_google') as File; | ||
| const file = (formData?.get('file') as File) || file_mapbox || file_google; | ||
| const timezone = (formData?.get('timezone') as string) || 'UTC'; | ||
| const lat = formData?.get('latitude') ? parseFloat(formData.get('latitude') as string) : undefined; | ||
| const lng = formData?.get('longitude') ? parseFloat(formData.get('longitude') as string) : undefined; | ||
| const location = (lat !== undefined && lng !== undefined) ? { lat, lng } : undefined; | ||
|
Comment on lines
53
to
+60
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate latitude/longitude before forming Line 58–60 can pass NaN (or out-of-range) values into ✅ Suggested fix- const lat = formData?.get('latitude') ? parseFloat(formData.get('latitude') as string) : undefined;
- const lng = formData?.get('longitude') ? parseFloat(formData.get('longitude') as string) : undefined;
- const location = (lat !== undefined && lng !== undefined) ? { lat, lng } : undefined;
+ const lat = formData?.get('latitude') ? parseFloat(formData.get('latitude') as string) : undefined;
+ const lng = formData?.get('longitude') ? parseFloat(formData.get('longitude') as string) : undefined;
+ const location =
+ Number.isFinite(lat) &&
+ Number.isFinite(lng) &&
+ Math.abs(lat as number) <= 90 &&
+ Math.abs(lng as number) <= 180
+ ? { lat: lat as number, lng: lng as number }
+ : undefined;🤖 Prompt for AI Agents |
||
|
|
||
| if (!file) { | ||
| throw new Error('No file provided for resolution search.'); | ||
| } | ||
|
|
||
| const mapboxBuffer = file_mapbox ? await file_mapbox.arrayBuffer() : null; | ||
| const mapboxDataUrl = mapboxBuffer ? `data:${file_mapbox.type};base64,${Buffer.from(mapboxBuffer).toString('base64')}` : null; | ||
|
|
||
| const googleBuffer = file_google ? await file_google.arrayBuffer() : null; | ||
| const googleDataUrl = googleBuffer ? `data:${file_google.type};base64,${Buffer.from(googleBuffer).toString('base64')}` : null; | ||
|
|
||
| const buffer = await file.arrayBuffer(); | ||
| const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`; | ||
|
|
||
| const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter( | ||
| message => | ||
| (message: any) => | ||
| message.role !== 'tool' && | ||
| message.type !== 'followup' && | ||
| message.type !== 'related' && | ||
|
|
@@ -89,7 +101,7 @@ async function submit(formData?: FormData, skip?: boolean) { | |
|
|
||
| async function processResolutionSearch() { | ||
| try { | ||
| const streamResult = await resolutionSearch(messages, timezone, drawnFeatures); | ||
| const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location); | ||
|
|
||
| let fullSummary = ''; | ||
| for await (const partialObject of streamResult.partialObjectStream) { | ||
|
|
@@ -113,7 +125,7 @@ async function submit(formData?: FormData, skip?: boolean) { | |
|
|
||
| messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); | ||
|
|
||
| const sanitizedMessages: CoreMessage[] = messages.map(m => { | ||
| const sanitizedMessages: CoreMessage[] = messages.map((m: any) => { | ||
| if (Array.isArray(m.content)) { | ||
| return { | ||
| ...m, | ||
|
|
@@ -124,7 +136,7 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| }) | ||
|
|
||
| const currentMessages = aiState.get().messages; | ||
| const sanitizedHistory = currentMessages.map(m => { | ||
| const sanitizedHistory = currentMessages.map((m: any) => { | ||
| if (m.role === "user" && Array.isArray(m.content)) { | ||
| return { | ||
| ...m, | ||
|
|
@@ -159,7 +171,9 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| role: 'assistant', | ||
| content: JSON.stringify({ | ||
| ...analysisResult, | ||
| image: dataUrl | ||
| image: dataUrl, | ||
| mapboxImage: mapboxDataUrl, | ||
| googleImage: googleDataUrl | ||
| }), | ||
| type: 'resolution_search_result' | ||
| }, | ||
|
|
@@ -190,7 +204,11 @@ async function submit(formData?: FormData, skip?: boolean) { | |
|
|
||
| uiStream.update( | ||
| <Section title="response"> | ||
| <ResolutionImage src={dataUrl} /> | ||
| <ResolutionCarousel | ||
| mapboxImage={mapboxDataUrl || undefined} | ||
| googleImage={googleDataUrl || undefined} | ||
| initialImage={dataUrl} | ||
| /> | ||
| <BotMessage content={summaryStream.value} /> | ||
| </Section> | ||
| ); | ||
|
|
@@ -203,43 +221,20 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| }; | ||
| } | ||
|
|
||
| const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter( | ||
| message => | ||
| message.role !== 'tool' && | ||
| message.type !== 'followup' && | ||
| message.type !== 'related' && | ||
| message.type !== 'end' && | ||
| message.type !== 'resolution_search_result' | ||
| ).map(m => { | ||
| if (Array.isArray(m.content)) { | ||
| return { | ||
| ...m, | ||
| content: m.content.filter((part: any) => | ||
| part.type !== "image" || (typeof part.image === "string" && part.image.startsWith("data:")) | ||
| ) | ||
| } as any | ||
| } | ||
| return m | ||
| }) | ||
|
|
||
| const groupeId = nanoid() | ||
| const useSpecificAPI = process.env.USE_SPECIFIC_API_FOR_WRITER === 'true' | ||
| const maxMessages = useSpecificAPI ? 5 : 10 | ||
| messages.splice(0, Math.max(messages.length - maxMessages, 0)) | ||
|
|
||
| const file = !skip ? (formData?.get('file') as File) : undefined | ||
| const userInput = skip | ||
| ? `{"action": "skip"}` | ||
| : ((formData?.get('related_query') as string) || | ||
| (formData?.get('input') as string)) | ||
|
|
||
| if (userInput.toLowerCase().trim() === 'what is a planet computer?' || userInput.toLowerCase().trim() === 'what is qcx-terra?') { | ||
| if (userInput && (userInput.toLowerCase().trim() === 'what is a planet computer?' || userInput.toLowerCase().trim() === 'what is qcx-terra?')) { | ||
| const definition = userInput.toLowerCase().trim() === 'what is a planet computer?' | ||
| ? `A planet computer is a proprietary environment aware system that interoperates weather forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet. Available for our Pro and Enterprise customers. [QCX Pricing](https://www.queue.cx/#pricing)` | ||
|
|
||
| : `QCX-Terra is a model garden of pixel level precision geospatial foundational models for efficient land feature predictions from satellite imagery. Available for our Pro and Enterprise customers. [QCX Pricing] (https://www.queue.cx/#pricing)`; | ||
|
Comment on lines
+230
to
233
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Hardcoded product definition strings with inline marketing copy are fragile and difficult to maintain. These definitions (lines 231–234) are hardcoded with pricing links and product descriptions. Consider moving them to a configuration file or CMS to keep marketing content out of server action logic. 🤖 Prompt for AI Agents |
||
|
|
||
| const content = JSON.stringify(Object.fromEntries(formData!)); | ||
| const type = 'input'; | ||
| const groupeId = nanoid(); | ||
|
|
||
| aiState.update({ | ||
| ...aiState.get(), | ||
|
|
@@ -299,10 +294,9 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| id: nanoid(), | ||
| isGenerating: isGenerating.value, | ||
| component: uiStream.value, | ||
| isCollapsed: isCollapsed.value, | ||
| isCollapsed: isCollapsed.value | ||
| }; | ||
| } | ||
| const file = !skip ? (formData?.get('file') as File) : undefined | ||
|
|
||
| if (!userInput && !file) { | ||
| isGenerating.done(false) | ||
|
|
@@ -314,6 +308,30 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| } | ||
| } | ||
|
|
||
| const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter( | ||
| (message: any) => | ||
| message.role !== 'tool' && | ||
| message.type !== 'followup' && | ||
| message.type !== 'related' && | ||
| message.type !== 'end' && | ||
| message.type !== 'resolution_search_result' | ||
| ).map((m: any) => { | ||
| if (Array.isArray(m.content)) { | ||
| return { | ||
| ...m, | ||
| content: m.content.filter((part: any) => | ||
| part.type !== "image" || (typeof part.image === "string" && part.image.startsWith("data:")) | ||
| ) | ||
| } as any | ||
| } | ||
| return m | ||
| }) | ||
|
|
||
| const groupeId = nanoid() | ||
| const useSpecificAPI = process.env.USE_SPECIFIC_API_FOR_WRITER === 'true' | ||
| const maxMessages = useSpecificAPI ? 5 : 10 | ||
| messages.splice(0, Math.max(messages.length - maxMessages, 0)) | ||
|
|
||
| const messageParts: { | ||
| type: 'text' | 'image' | ||
| text?: string | ||
|
|
@@ -725,12 +743,18 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { | |
| const analysisResult = JSON.parse(content as string); | ||
| const geoJson = analysisResult.geoJson as FeatureCollection; | ||
| const image = analysisResult.image as string; | ||
| const mapboxImage = analysisResult.mapboxImage as string; | ||
| const googleImage = analysisResult.googleImage as string; | ||
|
|
||
| return { | ||
| id, | ||
| component: ( | ||
| <> | ||
| {image && <ResolutionImage src={image} />} | ||
| <ResolutionCarousel | ||
| mapboxImage={mapboxImage} | ||
| googleImage={googleImage} | ||
| initialImage={image} | ||
| /> | ||
| {geoJson && ( | ||
| <GeoJsonLayer id={id} data={geoJson} /> | ||
| )} | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,91 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'use client' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React, { useState, useRef, useEffect } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from '@/lib/utils' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface CompareSliderProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| leftImage: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rightImage: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className?: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function CompareSlider({ leftImage, rightImage, className }: CompareSliderProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [sliderPosition, setSliderPosition] = useState(50) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const containerRef = useRef<HTMLDivElement>(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [containerWidth, setContainerWidth] = useState(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!containerRef.current) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const observer = new ResizeObserver((entries) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let entry of entries) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setContainerWidth(entry.contentRect.width) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| observer.observe(containerRef.current) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => observer.disconnect() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleMove = (event: React.MouseEvent | React.TouchEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!containerRef.current) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const containerRect = containerRef.current.getBoundingClientRect() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const x = 'touches' in event ? event.touches[0].clientX : (event as React.MouseEvent).clientX | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const relativeX = x - containerRect.left | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const position = Math.max(0, Math.min(100, (relativeX / containerRect.width) * 100)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSliderPosition(position) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={containerRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn("relative w-full aspect-square sm:aspect-video overflow-hidden cursor-ew-resize rounded-lg border bg-muted", className)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onMouseMove={handleMove} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onTouchMove={handleMove} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+47
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add ARIA + keyboard support for the interactive slider container. The root div is interactive but lacks semantic role/keyboard handling (Biome a11y rule). This blocks keyboard-only users. ♿ Suggested fix+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
+ const step = event.shiftKey ? 10 : 2
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
+ setSliderPosition(pos => Math.max(0, pos - step))
+ } else if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {
+ setSliderPosition(pos => Math.min(100, pos + step))
+ }
+ }
+
return (
<div
ref={containerRef}
className={cn("relative w-full aspect-square sm:aspect-video overflow-hidden cursor-ew-resize rounded-lg border bg-muted", className)}
onMouseMove={handleMove}
onTouchMove={handleMove}
+ role="slider"
+ tabIndex={0}
+ aria-label="Compare Mapbox and Google Satellite imagery"
+ aria-valuemin={0}
+ aria-valuemax={100}
+ aria-valuenow={Math.round(sliderPosition)}
+ onKeyDown={handleKeyDown}
>📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (2.3.13)[error] 42-47: Static Elements should not be interactive. To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value. (lint/a11y/noStaticElementInteractions) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Right Image (Google Satellite) */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src={rightImage} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alt="Google Satellite" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="absolute inset-0 w-full h-full object-cover select-none pointer-events-none" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Left Image (Mapbox) */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="absolute inset-0 overflow-hidden" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ width: `${sliderPosition}%` }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div style={{ width: containerWidth }} className="h-full relative"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src={leftImage} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alt="Mapbox" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="absolute inset-0 w-full h-full object-cover select-none pointer-events-none" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Slider Handle */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="absolute inset-y-0 w-1 bg-white shadow-[0_0_10px_rgba(0,0,0,0.5)] pointer-events-none" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ left: `${sliderPosition}%` }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-10 h-10 bg-white rounded-full border-2 border-primary flex items-center justify-center shadow-xl"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex gap-1"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-1 h-4 bg-primary/40 rounded-full" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-1 h-4 bg-primary/40 rounded-full" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Labels */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="absolute bottom-2 left-2 px-2 py-1 bg-black/50 text-white text-[10px] rounded pointer-events-none"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MAPBOX | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="absolute bottom-2 right-2 px-2 py-1 bg-black/50 text-white text-[10px] rounded pointer-events-none"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GOOGLE SATELLITE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
File fallback creates redundant data: when
filefalls back tofile_mapbox,dataUrlduplicatesmapboxDataUrl.Line 56 falls back:
const file = (formData?.get('file') as File) || file_mapbox || file_google. If no explicitfileis provided,filebecomesfile_mapbox, and line 74 converts it todataUrl— which is identical tomapboxDataUrl(line 68). The result JSON then stores the same base64 string twice underimageandmapboxImage, doubling the storage cost for that image.♻️ Suggested approach
const buffer = await file.arrayBuffer(); const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`; + // Avoid duplicate: if file fell back to mapbox/google, reuse that data URL + const primaryDataUrl = (file === file_mapbox && mapboxDataUrl) + ? mapboxDataUrl + : (file === file_google && googleDataUrl) + ? googleDataUrl + : dataUrl;Then use
primaryDataUrlas theimagefield in the stored result instead ofdataUrl.Also applies to: 67-74
🤖 Prompt for AI Agents