From c7783fa7253c7c6f0dd6d497896123a75b219b3f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:10:42 +0000 Subject: [PATCH 1/7] feat: implement dual image resolution search with Mapbox-Google compare slider and QCX-TERRA analysis button Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> From 33b6dca1d8aa488804eebac211a7363731100c4c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:26:07 +0000 Subject: [PATCH 2/7] feat: implement dual image resolution search with Mapbox-Google compare slider and QCX-TERRA analysis button Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/actions.tsx | 175 +++++++++++++++++++++++++++- components/compare-slider.tsx | 91 +++++++++++++++ components/header-search-button.tsx | 50 ++++++-- components/map/mapbox-map.tsx | 4 +- components/resolution-carousel.tsx | 146 +++++++++++++++++++++++ lib/agents/resolution-search.tsx | 20 +++- resolution_search_block.txt | 170 +++++++++++++++++++++++++++ 7 files changed, 638 insertions(+), 18 deletions(-) create mode 100644 components/compare-slider.tsx create mode 100644 components/resolution-carousel.tsx create mode 100644 resolution_search_block.txt diff --git a/app/actions.tsx b/app/actions.tsx index a1f5e915..d5450b16 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -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,13 +51,177 @@ 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; 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: any) => + message.role !== 'tool' && + message.type !== 'followup' && + message.type !== 'related' && + message.type !== 'end' && + message.type !== 'resolution_search_result' + ); + + const userInput = 'Analyze this map view.'; + const content: CoreMessage['content'] = [ + { type: 'text', text: userInput }, + { type: 'image', image: dataUrl, mimeType: file.type } + ]; + + aiState.update({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { id: nanoid(), role: 'user', content, type: 'input' } + ] + }); + messages.push({ role: 'user', content }); + + const summaryStream = createStreamableValue('Analyzing map view...'); + const groupeId = nanoid(); + + async function processResolutionSearch() { + try { + const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location); + + let fullSummary = ''; + for await (const partialObject of streamResult.partialObjectStream) { + if (partialObject.summary) { + fullSummary = partialObject.summary; + summaryStream.update(fullSummary); + } + } + + const analysisResult = await streamResult.object; + summaryStream.done(analysisResult.summary || 'Analysis complete.'); + + if (analysisResult.geoJson) { + uiStream.append( + + ); + } + + messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); + + const sanitizedMessages: CoreMessage[] = messages.map((m: any) => { + if (Array.isArray(m.content)) { + return { + ...m, + content: m.content.filter((part: any) => part.type !== 'image') + } as CoreMessage + } + return m + }) + + const currentMessages = aiState.get().messages; + const sanitizedHistory = currentMessages.map((m: any) => { + if (m.role === "user" && Array.isArray(m.content)) { + return { + ...m, + content: m.content.map((part: any) => + part.type === "image" ? { ...part, image: "IMAGE_PROCESSED" } : part + ) + } + } + return m + }); + const relatedQueries = await querySuggestor(uiStream, sanitizedMessages); + uiStream.append( +
+ +
+ ); + + await new Promise(resolve => setTimeout(resolve, 500)); + + aiState.done({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: groupeId, + role: 'assistant', + content: analysisResult.summary || 'Analysis complete.', + type: 'response' + }, + { + id: groupeId, + role: 'assistant', + content: JSON.stringify({ + ...analysisResult, + image: dataUrl, + mapboxImage: mapboxDataUrl, + googleImage: googleDataUrl + }), + type: 'resolution_search_result' + }, + { + id: groupeId, + role: 'assistant', + content: JSON.stringify(relatedQueries), + type: 'related' + }, + { + id: groupeId, + role: 'assistant', + content: 'followup', + type: 'followup' + } + ] + }); + } catch (error) { + console.error('Error in resolution search:', error); + summaryStream.error(error); + } finally { + isGenerating.done(false); + uiStream.done(); + } + } + + processResolutionSearch(); + + uiStream.update( +
+ + +
+ ); + + return { + id: nanoid(), + isGenerating: isGenerating.value, + component: uiStream.value, + isCollapsed: isCollapsed.value + }; + } + + const buffer = await file.arrayBuffer(); const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`; @@ -725,12 +890,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 && } + {geoJson && ( )} diff --git a/components/compare-slider.tsx b/components/compare-slider.tsx new file mode 100644 index 00000000..34877148 --- /dev/null +++ b/components/compare-slider.tsx @@ -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(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 ( +
+ {/* Right Image (Google Satellite) */} + Google Satellite + + {/* Left Image (Mapbox) */} +
+
+ Mapbox +
+
+ + {/* Slider Handle */} +
+
+
+
+
+
+
+
+ + {/* Labels */} +
+ MAPBOX +
+
+ GOOGLE SATELLITE +
+
+ ) +} diff --git a/components/header-search-button.tsx b/components/header-search-button.tsx index 69fda09a..f53862a6 100644 --- a/components/header-search-button.tsx +++ b/components/header-search-button.tsx @@ -9,7 +9,7 @@ import { useActions, useUIState } from 'ai/rsc' import { AI } from '@/app/actions' import { nanoid } from 'nanoid' import { UserMessage } from './user-message' -import { toast } from 'react-toastify' +import { toast } from 'sonner' import { useSettingsStore } from '@/lib/store/settings' import { useMapData } from './map/map-data-context' @@ -48,7 +48,7 @@ export function HeaderSearchButton() { setIsAnalyzing(true) try { - setMessages(currentMessages => [ + setMessages((currentMessages: any[]) => [ ...currentMessages, { id: nanoid(), @@ -56,13 +56,32 @@ export function HeaderSearchButton() { } ]) - let blob: Blob | null = null; + let mapboxBlob: Blob | null = null; + let googleBlob: Blob | null = null; - if (mapProvider === 'mapbox') { - const canvas = map!.getCanvas() - blob = await new Promise(resolve => { + if (mapProvider === 'mapbox' && map) { + // Capture Mapbox + const canvas = map.getCanvas() + mapboxBlob = await new Promise(resolve => { canvas.toBlob(resolve, 'image/png') }) + + // Also fetch Google Static Map for the same view + const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY; + if (apiKey) { + const center = map.getCenter(); + const zoom = Math.round(map.getZoom()); + const staticMapUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${center.lat},${center.lng}&zoom=${zoom}&size=640x480&scale=2&maptype=satellite&key=${apiKey}`; + + try { + const response = await fetch(staticMapUrl); + if (response.ok) { + googleBlob = await response.blob(); + } + } catch (e) { + console.error('Failed to fetch Google static map during Mapbox session:', e); + } + } } else if (mapProvider === 'google') { const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY if (!apiKey || !mapData.cameraState) { @@ -79,21 +98,32 @@ export function HeaderSearchButton() { if (!response.ok) { throw new Error('Failed to fetch static map image.'); } - blob = await response.blob(); + googleBlob = await response.blob(); } - if (!blob) { + if (!mapboxBlob && !googleBlob) { throw new Error('Failed to capture map image.') } const formData = new FormData() - formData.append('file', blob, 'map_capture.png') + if (mapboxBlob) formData.append('file_mapbox', mapboxBlob, 'mapbox_capture.png') + if (googleBlob) formData.append('file_google', googleBlob, 'google_capture.png') + + // Keep 'file' for backward compatibility if needed, or just use the first available + formData.append('file', (mapboxBlob || googleBlob)!, 'map_capture.png') + formData.append('action', 'resolution_search') formData.append('timezone', mapData.currentTimezone || 'UTC') formData.append('drawnFeatures', JSON.stringify(mapData.drawnFeatures || [])) + const center = mapProvider === 'mapbox' && map ? map.getCenter() : mapData.cameraState?.center; + if (center) { + formData.append('latitude', center.lat.toString()) + formData.append('longitude', center.lng.toString()) + } + const responseMessage = await actions.submit(formData) - setMessages(currentMessages => [...currentMessages, responseMessage as any]) + setMessages((currentMessages: any[]) => [...currentMessages, responseMessage as any]) } catch (error) { console.error('Failed to perform resolution search:', error) toast.error('An error occurred while analyzing the map.') diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx index eecd7f54..55552b5d 100644 --- a/components/map/mapbox-map.tsx +++ b/components/map/mapbox-map.tsx @@ -5,8 +5,8 @@ import mapboxgl from 'mapbox-gl' import MapboxDraw from '@mapbox/mapbox-gl-draw' import * as turf from '@turf/turf' import tzlookup from 'tz-lookup' -import { toast } from 'react-toastify' -import 'react-toastify/dist/ReactToastify.css' +import { toast } from 'sonner' + import 'mapbox-gl/dist/mapbox-gl.css' import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css' import { useMapToggle, MapToggleEnum } from '../map-toggle-context' diff --git a/components/resolution-carousel.tsx b/components/resolution-carousel.tsx new file mode 100644 index 00000000..ddf99fa2 --- /dev/null +++ b/components/resolution-carousel.tsx @@ -0,0 +1,146 @@ +'use client' + +import React from 'react' +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from "@/components/ui/carousel" +import { ResolutionImage } from './resolution-image' +import { Button } from './ui/button' +import { useActions, useUIState } from 'ai/rsc' +import { AI } from '@/app/actions' +import { nanoid } from 'nanoid' +import { UserMessage } from './user-message' +import { toast } from 'sonner' +import { CompareSlider } from './compare-slider' + +interface ResolutionCarouselProps { + mapboxImage?: string | null + googleImage?: string | null + initialImage?: string | null +} + +export function ResolutionCarousel({ mapboxImage, googleImage, initialImage }: ResolutionCarouselProps) { + const actions = useActions() as any + const [, setMessages] = useUIState() + const [isAnalyzing, setIsAnalyzing] = React.useState(false) + + const handleQCXAnalysis = async () => { + if (!googleImage) return + setIsAnalyzing(true) + + try { + const response = await fetch(googleImage) + const blob = await response.blob() + + setMessages((currentMessages: any[]) => [ + ...currentMessages, + { + id: nanoid(), + component: + } + ]) + + const formData = new FormData() + formData.append('file', blob, 'google_analysis.png') + formData.append('action', 'resolution_search') + + const responseMessage = await actions.submit(formData) + setMessages((currentMessages: any[]) => [...currentMessages, responseMessage as any]) + } catch (error) { + console.error('Failed to perform QCX-TERRA ANALYSIS:', error) + toast.error('An error occurred during analysis.') + } finally { + setIsAnalyzing(false) + } + } + + const slides = [] + + // Slide 1: Comparison (if both exist) + if (mapboxImage && googleImage) { + slides.push({ + type: 'compare', + left: mapboxImage, + right: googleImage + }) + } + + // Individual slides + if (mapboxImage) slides.push({ type: 'image', src: mapboxImage, showAnalysis: false, label: 'MAPBOX' }) + if (googleImage) slides.push({ type: 'image', src: googleImage, showAnalysis: true, label: 'GOOGLE SATELLITE' }) + + // Fallback + if (slides.length === 0 && initialImage) { + slides.push({ type: 'image', src: initialImage, showAnalysis: false, label: 'MAP CAPTURE' }) + } + + if (slides.length === 0) return null + + if (slides.length === 1) { + const item = slides[0] + if (item.type === 'image') { + return ( +
+ + {item.showAnalysis && ( + + )} +
+ ) + } + } + + return ( +
+ + + {slides.map((slide, index) => ( + +
+ {slide.type === 'compare' ? ( + + ) : ( + <> + + {slide.showAnalysis && ( + + )} +
+ {slide.label} +
+ + )} +
+
+ ))} +
+ {slides.length > 1 && ( +
+ + +
+ )} +
+
+ ) +} diff --git a/lib/agents/resolution-search.tsx b/lib/agents/resolution-search.tsx index 737551e8..1bcc3290 100644 --- a/lib/agents/resolution-search.tsx +++ b/lib/agents/resolution-search.tsx @@ -21,6 +21,14 @@ const resolutionSearchSchema = z.object({ }), })), }).describe('A GeoJSON object containing points of interest and classified land features to be overlaid on the map.'), + extractedCoordinates: z.object({ + latitude: z.number(), + longitude: z.number() + }).optional().describe('The extracted geocoordinates of the center of the image.'), + cogInfo: z.object({ + applicable: z.boolean(), + description: z.string().optional() + }).optional().describe('Information about whether Cloud Optimized GeoTIFF (COG) data is applicable or available for this area.') }) export interface DrawnFeature { @@ -30,7 +38,7 @@ export interface DrawnFeature { geometry: any; } -export async function resolutionSearch(messages: CoreMessage[], timezone: string = 'UTC', drawnFeatures?: DrawnFeature[]) { +export async function resolutionSearch(messages: CoreMessage[], timezone: string = 'UTC', drawnFeatures?: DrawnFeature[], location?: { lat: number, lng: number }) { const localTime = new Date().toLocaleString('en-US', { timeZone: timezone, hour: '2-digit', @@ -46,6 +54,8 @@ export async function resolutionSearch(messages: CoreMessage[], timezone: string As a geospatial analyst, your task is to analyze the provided satellite image of a geographic location. The current local time at this location is ${localTime}. +${location ? `The coordinates provided for this image are: Latitude ${location.lat}, Longitude ${location.lng}.` : ''} + ${drawnFeatures && drawnFeatures.length > 0 ? `The user has drawn the following features on the map for your reference: ${drawnFeatures.map(f => `- ${f.type} (${f.measurement}): ${JSON.stringify(f.geometry)}`).join('\n')} Use these user-drawn areas/lines as primary areas of interest for your analysis.` : ''} @@ -54,7 +64,9 @@ Your analysis should be comprehensive and include the following components: 1. **Land Feature Classification:** Identify and describe the different types of land cover visible in the image (e.g., urban areas, forests, water bodies, agricultural fields). 2. **Points of Interest (POI):** Detect and name any significant landmarks, infrastructure (e.g., bridges, major roads), or notable buildings. -3. **Structured Output:** Return your findings in a structured JSON format. The output must include a 'summary' (a detailed text description of your analysis) and a 'geoJson' object. The GeoJSON should contain features (Points or Polygons) for the identified POIs and land classifications, with appropriate properties. +3. **Coordinate Extraction:** If possible, confirm or refine the geocoordinates (latitude/longitude) of the center of the image. +4. **COG Applicability:** Determine if this location would benefit from Cloud Optimized GeoTIFF (COG) analysis for high-precision temporal or spectral data. +5. **Structured Output:** Return your findings in a structured JSON format. The output must include a 'summary' (a detailed text description of your analysis) and a 'geoJson' object. Your analysis should be based solely on the visual information in the image and your general knowledge. Do not attempt to access external websites or perform web searches. @@ -64,9 +76,9 @@ Analyze the user's prompt and the image to provide a holistic understanding of t const filteredMessages = messages.filter(msg => msg.role !== 'system'); // Check if any message contains an image (resolution search is specifically for image analysis) - const hasImage = messages.some(message => + const hasImage = messages.some((message: any) => Array.isArray(message.content) && - message.content.some(part => part.type === 'image') + message.content.some((part: any) => part.type === 'image') ) // Use streamObject to get partial results. diff --git a/resolution_search_block.txt b/resolution_search_block.txt new file mode 100644 index 00000000..e53b6151 --- /dev/null +++ b/resolution_search_block.txt @@ -0,0 +1,170 @@ + if (action === 'resolution_search') { + 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; + + 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: any) => + message.role !== 'tool' && + message.type !== 'followup' && + message.type !== 'related' && + message.type !== 'end' && + message.type !== 'resolution_search_result' + ); + + const userInput = 'Analyze this map view.'; + const content: CoreMessage['content'] = [ + { type: 'text', text: userInput }, + { type: 'image', image: dataUrl, mimeType: file.type } + ]; + + aiState.update({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { id: nanoid(), role: 'user', content, type: 'input' } + ] + }); + messages.push({ role: 'user', content }); + + const summaryStream = createStreamableValue('Analyzing map view...'); + const groupeId = nanoid(); + + async function processResolutionSearch() { + try { + const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location); + + let fullSummary = ''; + for await (const partialObject of streamResult.partialObjectStream) { + if (partialObject.summary) { + fullSummary = partialObject.summary; + summaryStream.update(fullSummary); + } + } + + const analysisResult = await streamResult.object; + summaryStream.done(analysisResult.summary || 'Analysis complete.'); + + if (analysisResult.geoJson) { + uiStream.append( + + ); + } + + messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); + + const sanitizedMessages: CoreMessage[] = messages.map((m: any) => { + if (Array.isArray(m.content)) { + return { + ...m, + content: m.content.filter((part: any) => part.type !== 'image') + } as CoreMessage + } + return m + }) + + const currentMessages = aiState.get().messages; + const sanitizedHistory = currentMessages.map((m: any) => { + if (m.role === "user" && Array.isArray(m.content)) { + return { + ...m, + content: m.content.map((part: any) => + part.type === "image" ? { ...part, image: "IMAGE_PROCESSED" } : part + ) + } + } + return m + }); + const relatedQueries = await querySuggestor(uiStream, sanitizedMessages); + uiStream.append( +
+ +
+ ); + + await new Promise(resolve => setTimeout(resolve, 500)); + + aiState.done({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: groupeId, + role: 'assistant', + content: analysisResult.summary || 'Analysis complete.', + type: 'response' + }, + { + id: groupeId, + role: 'assistant', + content: JSON.stringify({ + ...analysisResult, + image: dataUrl, + mapboxImage: mapboxDataUrl, + googleImage: googleDataUrl + }), + type: 'resolution_search_result' + }, + { + id: groupeId, + role: 'assistant', + content: JSON.stringify(relatedQueries), + type: 'related' + }, + { + id: groupeId, + role: 'assistant', + content: 'followup', + type: 'followup' + } + ] + }); + } catch (error) { + console.error('Error in resolution search:', error); + summaryStream.error(error); + } finally { + isGenerating.done(false); + uiStream.done(); + } + } + + processResolutionSearch(); + + uiStream.update( +
+ + +
+ ); + + return { + id: nanoid(), + isGenerating: isGenerating.value, + component: uiStream.value, + isCollapsed: isCollapsed.value + }; + } From 5c1c0becc17a4b7de197cc871ab12f6459adb348 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:41:44 +0000 Subject: [PATCH 3/7] feat: implement dual image resolution search with Mapbox-Google compare slider and QCX-TERRA analysis button Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/actions.tsx | 203 +++++------------------------------- components_chat_patch.patch | 24 ----- resolution_search_block.txt | 170 ------------------------------ 3 files changed, 28 insertions(+), 369 deletions(-) delete mode 100644 components_chat_patch.patch delete mode 100644 resolution_search_block.txt diff --git a/app/actions.tsx b/app/actions.tsx index d5450b16..7871933a 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -221,190 +221,20 @@ async function submit(formData?: FormData, skip?: boolean) { }; } - - 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.role !== 'tool' && - message.type !== 'followup' && - message.type !== 'related' && - message.type !== 'end' && - message.type !== 'resolution_search_result' - ); - - const userInput = 'Analyze this map view.'; - const content: CoreMessage['content'] = [ - { type: 'text', text: userInput }, - { type: 'image', image: dataUrl, mimeType: file.type } - ]; - - aiState.update({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { id: nanoid(), role: 'user', content, type: 'input' } - ] - }); - messages.push({ role: 'user', content }); - - const summaryStream = createStreamableValue('Analyzing map view...'); - const groupeId = nanoid(); - - async function processResolutionSearch() { - try { - const streamResult = await resolutionSearch(messages, timezone, drawnFeatures); - - let fullSummary = ''; - for await (const partialObject of streamResult.partialObjectStream) { - if (partialObject.summary) { - fullSummary = partialObject.summary; - summaryStream.update(fullSummary); - } - } - - const analysisResult = await streamResult.object; - summaryStream.done(analysisResult.summary || 'Analysis complete.'); - - if (analysisResult.geoJson) { - uiStream.append( - - ); - } - - messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); - - const sanitizedMessages: CoreMessage[] = messages.map(m => { - if (Array.isArray(m.content)) { - return { - ...m, - content: m.content.filter((part: any) => part.type !== 'image') - } as CoreMessage - } - return m - }) - - const currentMessages = aiState.get().messages; - const sanitizedHistory = currentMessages.map(m => { - if (m.role === "user" && Array.isArray(m.content)) { - return { - ...m, - content: m.content.map((part: any) => - part.type === "image" ? { ...part, image: "IMAGE_PROCESSED" } : part - ) - } - } - return m - }); - const relatedQueries = await querySuggestor(uiStream, sanitizedMessages); - uiStream.append( -
- -
- ); - - await new Promise(resolve => setTimeout(resolve, 500)); - - aiState.done({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { - id: groupeId, - role: 'assistant', - content: analysisResult.summary || 'Analysis complete.', - type: 'response' - }, - { - id: groupeId, - role: 'assistant', - content: JSON.stringify({ - ...analysisResult, - image: dataUrl - }), - type: 'resolution_search_result' - }, - { - id: groupeId, - role: 'assistant', - content: JSON.stringify(relatedQueries), - type: 'related' - }, - { - id: groupeId, - role: 'assistant', - content: 'followup', - type: 'followup' - } - ] - }); - } catch (error) { - console.error('Error in resolution search:', error); - summaryStream.error(error); - } finally { - isGenerating.done(false); - uiStream.done(); - } - } - - processResolutionSearch(); - - uiStream.update( -
- - -
- ); - - return { - id: nanoid(), - isGenerating: isGenerating.value, - component: uiStream.value, - isCollapsed: isCollapsed.value - }; - } - - 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)`; const content = JSON.stringify(Object.fromEntries(formData!)); const type = 'input'; + const groupeId = nanoid(); aiState.update({ ...aiState.get(), @@ -464,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) @@ -479,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 diff --git a/components_chat_patch.patch b/components_chat_patch.patch deleted file mode 100644 index 17df116b..00000000 --- a/components_chat_patch.patch +++ /dev/null @@ -1,24 +0,0 @@ ---- components/chat.tsx -+++ components/chat.tsx -@@ -156,10 +156,7 @@ - { - setInput(message) -- setTimeout(() => { -- setIsSubmitting(true) -- }, 0) -+ setIsSubmitting(true) - }} - /> - ) : ( -@@ -198,10 +195,7 @@ - { - setInput(message) -- setTimeout(() => { -- setIsSubmitting(true) -- }, 0) -+ setIsSubmitting(true) - }} - /> - ) : ( diff --git a/resolution_search_block.txt b/resolution_search_block.txt deleted file mode 100644 index e53b6151..00000000 --- a/resolution_search_block.txt +++ /dev/null @@ -1,170 +0,0 @@ - if (action === 'resolution_search') { - 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; - - 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: any) => - message.role !== 'tool' && - message.type !== 'followup' && - message.type !== 'related' && - message.type !== 'end' && - message.type !== 'resolution_search_result' - ); - - const userInput = 'Analyze this map view.'; - const content: CoreMessage['content'] = [ - { type: 'text', text: userInput }, - { type: 'image', image: dataUrl, mimeType: file.type } - ]; - - aiState.update({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { id: nanoid(), role: 'user', content, type: 'input' } - ] - }); - messages.push({ role: 'user', content }); - - const summaryStream = createStreamableValue('Analyzing map view...'); - const groupeId = nanoid(); - - async function processResolutionSearch() { - try { - const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location); - - let fullSummary = ''; - for await (const partialObject of streamResult.partialObjectStream) { - if (partialObject.summary) { - fullSummary = partialObject.summary; - summaryStream.update(fullSummary); - } - } - - const analysisResult = await streamResult.object; - summaryStream.done(analysisResult.summary || 'Analysis complete.'); - - if (analysisResult.geoJson) { - uiStream.append( - - ); - } - - messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); - - const sanitizedMessages: CoreMessage[] = messages.map((m: any) => { - if (Array.isArray(m.content)) { - return { - ...m, - content: m.content.filter((part: any) => part.type !== 'image') - } as CoreMessage - } - return m - }) - - const currentMessages = aiState.get().messages; - const sanitizedHistory = currentMessages.map((m: any) => { - if (m.role === "user" && Array.isArray(m.content)) { - return { - ...m, - content: m.content.map((part: any) => - part.type === "image" ? { ...part, image: "IMAGE_PROCESSED" } : part - ) - } - } - return m - }); - const relatedQueries = await querySuggestor(uiStream, sanitizedMessages); - uiStream.append( -
- -
- ); - - await new Promise(resolve => setTimeout(resolve, 500)); - - aiState.done({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { - id: groupeId, - role: 'assistant', - content: analysisResult.summary || 'Analysis complete.', - type: 'response' - }, - { - id: groupeId, - role: 'assistant', - content: JSON.stringify({ - ...analysisResult, - image: dataUrl, - mapboxImage: mapboxDataUrl, - googleImage: googleDataUrl - }), - type: 'resolution_search_result' - }, - { - id: groupeId, - role: 'assistant', - content: JSON.stringify(relatedQueries), - type: 'related' - }, - { - id: groupeId, - role: 'assistant', - content: 'followup', - type: 'followup' - } - ] - }); - } catch (error) { - console.error('Error in resolution search:', error); - summaryStream.error(error); - } finally { - isGenerating.done(false); - uiStream.done(); - } - } - - processResolutionSearch(); - - uiStream.update( -
- - -
- ); - - return { - id: nanoid(), - isGenerating: isGenerating.value, - component: uiStream.value, - isCollapsed: isCollapsed.value - }; - } From e4839bd8c00028dd072c7a901380c90fdf4bf967 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:07:14 +0000 Subject: [PATCH 4/7] feat: implement dual image resolution search with Mapbox-Google compare slider and QCX-TERRA analysis button Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- components/resolution-carousel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/resolution-carousel.tsx b/components/resolution-carousel.tsx index ddf99fa2..0f7b1105 100644 --- a/components/resolution-carousel.tsx +++ b/components/resolution-carousel.tsx @@ -58,7 +58,7 @@ export function ResolutionCarousel({ mapboxImage, googleImage, initialImage }: R } } - const slides = [] + const slides: Array<{ type: 'compare', left: string, right: string } | { type: 'image', src: string, showAnalysis: boolean, label: string }> = [] // Slide 1: Comparison (if both exist) if (mapboxImage && googleImage) { @@ -110,10 +110,10 @@ export function ResolutionCarousel({ mapboxImage, googleImage, initialImage }: R
{slide.type === 'compare' ? ( - + ) : ( <> - + {slide.showAnalysis && (