From 74a8ad502dfbd3baf3988432e23a4bff09eb95fb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 14:13:53 +0000 Subject: [PATCH 1/6] feat: enhance resolution search with high-res imagery and image display - Added `scale=2` to Google Static Maps capture for improved resolution. - Created `ResolutionImage` component for thumbnail and full-screen display of analyzed satellite imagery. - Updated the core response logic to include images in the initial stream and persistent AI state. - Updated `getUIStateFromAIState` to support image restoration from saved messages. - Fixed a bug where GeoJson layers were not immediately appended to the UI stream during analysis. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/actions.tsx | 21 ++++++++-- components/header-search-button.tsx | 2 +- components/resolution-image.tsx | 59 +++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 components/resolution-image.tsx diff --git a/app/actions.tsx b/app/actions.tsx index 9e0ee20a..9a988025 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -23,6 +23,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 { ResolutionImage } from '@/components/resolution-image' import { CopilotDisplay } from '@/components/copilot-display' import RetrieveSection from '@/components/retrieve-section' import { VideoSearchSection } from '@/components/video-search-section' @@ -92,6 +93,7 @@ async function submit(formData?: FormData, skip?: boolean) { // Create a streamable value for the summary. const summaryStream = createStreamableValue(''); + const groupeId = nanoid(); async function processResolutionSearch() { try { @@ -111,6 +113,15 @@ async function submit(formData?: FormData, skip?: boolean) { // Mark the summary stream as done with the result. 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 => { @@ -132,8 +143,6 @@ async function submit(formData?: FormData, skip?: boolean) { await new Promise(resolve => setTimeout(resolve, 500)); - const groupeId = nanoid(); - aiState.done({ ...aiState.get(), messages: [ @@ -147,7 +156,10 @@ async function submit(formData?: FormData, skip?: boolean) { { id: groupeId, role: 'assistant', - content: JSON.stringify(analysisResult), + content: JSON.stringify({ + ...analysisResult, + image: dataUrl + }), type: 'resolution_search_result' }, { @@ -179,6 +191,7 @@ async function submit(formData?: FormData, skip?: boolean) { // Immediately update the UI stream with the BotMessage component. uiStream.update(
+
); @@ -704,11 +717,13 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { case 'resolution_search_result': { const analysisResult = JSON.parse(content as string); const geoJson = analysisResult.geoJson as FeatureCollection; + const image = analysisResult.image as string; return { id, component: ( <> + {image && } {geoJson && ( )} diff --git a/components/header-search-button.tsx b/components/header-search-button.tsx index 92cb1c65..d5d2cd69 100644 --- a/components/header-search-button.tsx +++ b/components/header-search-button.tsx @@ -73,7 +73,7 @@ export function HeaderSearchButton() { const { center, range } = mapData.cameraState const zoom = Math.round(Math.log2(40000000 / (range || 1))); - let staticMapUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${center.lat},${center.lng}&zoom=${zoom}&size=640x480&maptype=satellite&key=${apiKey}`; + let staticMapUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${center.lat},${center.lng}&zoom=${zoom}&size=640x480&scale=2&maptype=satellite&key=${apiKey}`; const response = await fetch(staticMapUrl); if (!response.ok) { diff --git a/components/resolution-image.tsx b/components/resolution-image.tsx new file mode 100644 index 00000000..0c5cda9f --- /dev/null +++ b/components/resolution-image.tsx @@ -0,0 +1,59 @@ +/* eslint-disable @next/next/no-img-element */ +'use client' + +import React from 'react' +import { Card, CardContent } from '@/components/ui/card' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger +} from '@/components/ui/dialog' +import { motion } from 'framer-motion' +import { cn } from '@/lib/utils' + +interface ResolutionImageProps { + src: string + className?: string +} + +export function ResolutionImage({ src, className }: ResolutionImageProps) { + if (!src) return null + + return ( +
+ + + + + + Satellite Analysis Map + + + + + + + Satellite Imagery + +
+ Satellite Analysis Map Full +
+
+
+
+ ) +} From 76ace98f8907d89a0059872be4a64b889b04eb49 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:49:20 +0000 Subject: [PATCH 2/6] feat: add google maps image render to resolution search and geospatial tools - Increased Google Static Maps resolution (scale=2). - Added image display to Resolution Search results. - Extended image display to general geospatial query tool results (geocode, search, map). - Added generic `alt` prop to `ResolutionImage` component. - Implemented Google Static Maps URL generation in `geospatialTool` for the Google provider. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/actions.tsx | 16 +++++++++++++++- components/resolution-image.tsx | 9 +++++---- lib/agents/tools/geospatial.tsx | 21 ++++++++++++++++++++- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/app/actions.tsx b/app/actions.tsx index 9a988025..113deb6d 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -743,9 +743,23 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { toolOutput.type === 'MAP_QUERY_TRIGGER' && name === 'geospatialQueryTool' ) { + const mapUrl = toolOutput.mcp_response?.mapUrl; + const placeName = toolOutput.mcp_response?.location?.place_name; + return { id, - component: , + component: ( + <> + {mapUrl && ( + + )} + + + ), isCollapsed: false } } diff --git a/components/resolution-image.tsx b/components/resolution-image.tsx index 0c5cda9f..69def4b8 100644 --- a/components/resolution-image.tsx +++ b/components/resolution-image.tsx @@ -16,9 +16,10 @@ import { cn } from '@/lib/utils' interface ResolutionImageProps { src: string className?: string + alt?: string } -export function ResolutionImage({ src, className }: ResolutionImageProps) { +export function ResolutionImage({ src, className, alt = 'Map Imagery' }: ResolutionImageProps) { if (!src) return null return ( @@ -34,7 +35,7 @@ export function ResolutionImage({ src, className }: ResolutionImageProps) { Satellite Analysis Map @@ -43,12 +44,12 @@ export function ResolutionImage({ src, className }: ResolutionImageProps) { - Satellite Imagery + {alt}
Satellite Analysis Map Full
diff --git a/lib/agents/tools/geospatial.tsx b/lib/agents/tools/geospatial.tsx index ccff0d02..ca5f9f49 100644 --- a/lib/agents/tools/geospatial.tsx +++ b/lib/agents/tools/geospatial.tsx @@ -152,6 +152,15 @@ async function closeClient(client: McpClient | null) { } } +/** + * Helper to generate a Google Static Map URL + */ +function getGoogleStaticMapUrl(latitude: number, longitude: number): string { + const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || process.env.GOOGLE_MAPS_API_KEY; + if (!apiKey) return ''; + return `https://maps.googleapis.com/maps/api/staticmap?center=${latitude},${longitude}&zoom=15&size=640x480&scale=2&markers=color:red%7C${latitude},${longitude}&key=${apiKey}`; +} + /** * Main geospatial tool executor. */ @@ -269,13 +278,18 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g const { latitude, longitude } = place.coordinates; const place_name = place.displayName; - const mcpData = { + const mcpData: McpResponse = { location: { latitude, longitude, place_name, }, }; + + if (mapProvider === 'google') { + mcpData.mapUrl = getGoogleStaticMapUrl(latitude, longitude); + } + feedbackMessage = `Found location: ${place_name}`; uiFeedbackStream.update(feedbackMessage); uiFeedbackStream.done(); @@ -392,6 +406,11 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g feedbackMessage = `Successfully processed ${queryType} query for: ${mcpData.location.place_name || JSON.stringify(params)}`; uiFeedbackStream.update(feedbackMessage); + // Enhance with Google Static Map URL if provider is google and we have coordinates + if (mapProvider === 'google' && mcpData.location.latitude && mcpData.location.longitude && !mcpData.mapUrl) { + mcpData.mapUrl = getGoogleStaticMapUrl(mcpData.location.latitude, mcpData.location.longitude); + } + } catch (error: any) { toolError = `Mapping service error: ${error.message}`; uiFeedbackStream.update(toolError); From 830d53a582f7be73ff7c3ab804a075a48f007e53 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 17:10:42 +0000 Subject: [PATCH 3/6] feat: enhance resolution search with multi-image previews and fix follow-ups - Capture and display Mapbox and Google Satellite previews side-by-side in resolution search. - Update `ResolutionImage` component for multi-image comparison and session persistence. - Fix `getModel` utility to correctly return vision-capable models (e.g., `grok-vision-beta`) when history contains images. - Make Task Manager, Inquirer, and Query Suggestor agents vision-aware. - Include `mapProvider` context in follow-up submissions to maintain tool and model selection accuracy. - Enhance `resolutionSearch` system prompt for comparative analysis of dual previews. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/actions.tsx | 74 +++++++++++++++++++----- components/followup-panel.tsx | 3 + components/header-search-button.tsx | 60 ++++++++++++-------- components/resolution-image.tsx | 88 ++++++++++++++++++++++------- lib/agents/inquire.tsx | 8 ++- lib/agents/query-suggestor.tsx | 8 ++- lib/agents/resolution-search.tsx | 14 +++-- lib/agents/task-manager.tsx | 7 ++- lib/utils/index.ts | 8 +-- 9 files changed, 199 insertions(+), 71 deletions(-) diff --git a/app/actions.tsx b/app/actions.tsx index 113deb6d..2068b24b 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -45,7 +45,9 @@ async function submit(formData?: FormData, skip?: boolean) { const action = formData?.get('action') as string; if (action === 'resolution_search') { - const file = formData?.get('file') as File; + const mapboxFile = formData?.get('mapboxFile') as File; + const googleFile = formData?.get('googleFile') as File; + const legacyFile = formData?.get('file') as File; const timezone = (formData?.get('timezone') as string) || 'UTC'; const drawnFeaturesString = formData?.get('drawnFeatures') as string; let drawnFeatures: DrawnFeature[] = []; @@ -55,12 +57,27 @@ async function submit(formData?: FormData, skip?: boolean) { console.error('Failed to parse drawnFeatures:', e); } - if (!file) { - throw new Error('No file provided for resolution search.'); + let mapboxDataUrl = ''; + let googleDataUrl = ''; + + if (mapboxFile) { + const buffer = await mapboxFile.arrayBuffer(); + mapboxDataUrl = `data:${mapboxFile.type};base64,${Buffer.from(buffer).toString('base64')}`; + } + if (googleFile) { + const buffer = await googleFile.arrayBuffer(); + googleDataUrl = `data:${googleFile.type};base64,${Buffer.from(buffer).toString('base64')}`; } - const buffer = await file.arrayBuffer(); - const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`; + // Fallback if only 'file' was provided (backward compatibility) + if (!mapboxDataUrl && !googleDataUrl && legacyFile) { + const buffer = await legacyFile.arrayBuffer(); + mapboxDataUrl = `data:${legacyFile.type};base64,${Buffer.from(buffer).toString('base64')}`; + } + + if (!mapboxDataUrl && !googleDataUrl) { + throw new Error('No files provided for resolution search.'); + } // Get the current messages, excluding tool-related ones. const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter( @@ -76,10 +93,26 @@ async function submit(formData?: FormData, skip?: boolean) { const userInput = 'Analyze this map view.'; // Construct the multimodal content for the user message. - const content: CoreMessage['content'] = [ - { type: 'text', text: userInput }, - { type: 'image', image: dataUrl, mimeType: file.type } - ]; + const contentParts: any[] = [ + { type: 'text', text: userInput } + ] + + if (mapboxDataUrl) { + contentParts.push({ + type: 'image', + image: mapboxDataUrl, + mimeType: 'image/png' + }) + } + if (googleDataUrl) { + contentParts.push({ + type: 'image', + image: googleDataUrl, + mimeType: 'image/png' + }) + } + + const content = contentParts as any // Add the new user message to the AI state. aiState.update({ @@ -158,7 +191,7 @@ async function submit(formData?: FormData, skip?: boolean) { role: 'assistant', content: JSON.stringify({ ...analysisResult, - image: dataUrl + image: JSON.stringify({ mapbox: mapboxDataUrl, google: googleDataUrl }) }), type: 'resolution_search_result' }, @@ -191,7 +224,7 @@ async function submit(formData?: FormData, skip?: boolean) { // Immediately update the UI stream with the BotMessage component. uiStream.update(
- +
); @@ -717,13 +750,28 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { case 'resolution_search_result': { const analysisResult = JSON.parse(content as string); const geoJson = analysisResult.geoJson as FeatureCollection; - const image = analysisResult.image as string; + const imageData = analysisResult.image as string; + let mapboxSrc = ''; + let googleSrc = ''; + + if (imageData) { + try { + const parsed = JSON.parse(imageData); + mapboxSrc = parsed.mapbox || ''; + googleSrc = parsed.google || ''; + } catch (e) { + // Fallback for older image format which was just a single string + mapboxSrc = imageData; + } + } return { id, component: ( <> - {image && } + {(mapboxSrc || googleSrc) && ( + + )} {geoJson && ( )} diff --git a/components/followup-panel.tsx b/components/followup-panel.tsx index 08642530..04328c88 100644 --- a/components/followup-panel.tsx +++ b/components/followup-panel.tsx @@ -8,16 +8,19 @@ import { useActions, useUIState } from 'ai/rsc' import type { AI } from '@/app/actions' import { UserMessage } from './user-message' import { ArrowRight } from 'lucide-react' +import { useSettingsStore } from '@/lib/store/settings' export function FollowupPanel() { const [input, setInput] = useState('') const { submit } = useActions() + const { mapProvider } = useSettingsStore() // Removed mcp instance as it's no longer passed to submit const [, setMessages] = useUIState() const handleSubmit = async (event: React.FormEvent) => { event.preventDefault() const formData = new FormData(event.currentTarget as HTMLFormElement) + formData.append('mapProvider', mapProvider) const userMessage = { id: Date.now(), diff --git a/components/header-search-button.tsx b/components/header-search-button.tsx index d5d2cd69..7b2283aa 100644 --- a/components/header-search-button.tsx +++ b/components/header-search-button.tsx @@ -56,41 +56,51 @@ export function HeaderSearchButton() { } ]) - let blob: Blob | null = null; - - if (mapProvider === 'mapbox') { - const canvas = map!.getCanvas() - blob = await new Promise(resolve => { - canvas.toBlob(resolve, 'image/png') - }) - } else if (mapProvider === 'google') { - const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY - if (!apiKey || !mapData.cameraState) { - toast.error('Google Maps API key or camera state is not available.') - setIsAnalyzing(false) - return - } - const { center, range } = mapData.cameraState - const zoom = Math.round(Math.log2(40000000 / (range || 1))); - - let staticMapUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${center.lat},${center.lng}&zoom=${zoom}&size=640x480&scale=2&maptype=satellite&key=${apiKey}`; + let mapboxBlob: Blob | null = null; + let googleBlob: Blob | null = null; + + const { center, range, zoom: cameraZoom } = mapData.cameraState || {}; + const zoom = cameraZoom ?? (range ? Math.round(Math.log2(40000000 / range)) : 2); + + // Capture Mapbox Preview + if (map) { + const canvas = map.getCanvas(); + mapboxBlob = await new Promise(resolve => { + canvas.toBlob(resolve, 'image/png'); + }); + } - const response = await fetch(staticMapUrl); - if (!response.ok) { - throw new Error('Failed to fetch static map image.'); + // Capture Google Preview + if (center) { + const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY; + if (apiKey) { + let staticMapUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${center.lat},${center.lng}&zoom=${Math.round(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:', e); + } } - blob = await response.blob(); } - if (!blob) { - throw new Error('Failed to capture map image.') + if (!mapboxBlob && !googleBlob) { + throw new Error('Failed to capture any map images.') } const formData = new FormData() - formData.append('file', blob, 'map_capture.png') + if (mapboxBlob) formData.append('mapboxFile', mapboxBlob, 'mapbox_capture.png') + if (googleBlob) formData.append('googleFile', googleBlob, 'google_capture.png') + + // Keep 'file' for backward compatibility in case it's used elsewhere + 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 || [])) + formData.append('mapProvider', mapProvider) const responseMessage = await actions.submit(formData) setMessages(currentMessages => [...currentMessages, responseMessage as any]) diff --git a/components/resolution-image.tsx b/components/resolution-image.tsx index 69def4b8..35cc8aaa 100644 --- a/components/resolution-image.tsx +++ b/components/resolution-image.tsx @@ -14,44 +14,90 @@ import { motion } from 'framer-motion' import { cn } from '@/lib/utils' interface ResolutionImageProps { - src: string + src?: string + mapboxSrc?: string + googleSrc?: string className?: string alt?: string } -export function ResolutionImage({ src, className, alt = 'Map Imagery' }: ResolutionImageProps) { - if (!src) return null +export function ResolutionImage({ + src, + mapboxSrc, + googleSrc, + className, + alt = 'Map Imagery' +}: ResolutionImageProps) { + const mSrc = mapboxSrc || (src && !googleSrc ? src : undefined) + const gSrc = googleSrc || (src && !mapboxSrc ? src : undefined) + + if (!mSrc && !gSrc) return null + + const hasBoth = mSrc && gSrc return (
- - {alt} + + {mSrc && ( +
+ {`${alt} +

Mapbox

+
+ )} + {gSrc && ( +
+ {`${alt} +

Google Satellite

+
+ )}
- - - {alt} + + + {alt} Comparison -
- {`${alt} +
+ {mSrc && ( +
+

Mapbox Preview

+ {`${alt} +
+ )} + {gSrc && ( +
+

Google Satellite Full

+ {`${alt} +
+ )}
diff --git a/lib/agents/inquire.tsx b/lib/agents/inquire.tsx index e15926b7..a19eec3e 100644 --- a/lib/agents/inquire.tsx +++ b/lib/agents/inquire.tsx @@ -22,8 +22,14 @@ export async function inquire( ); let finalInquiry: PartialInquiry = {}; + + const hasImage = messages.some(message => + Array.isArray(message.content) && + message.content.some(part => part.type === 'image') + ) + const result = await streamObject({ - model: (await getModel()) as LanguageModel, + model: (await getModel(hasImage)) as LanguageModel, system: `...`, // Your system prompt remains unchanged messages, schema: inquirySchema, diff --git a/lib/agents/query-suggestor.tsx b/lib/agents/query-suggestor.tsx index de2b3749..026a2de0 100644 --- a/lib/agents/query-suggestor.tsx +++ b/lib/agents/query-suggestor.tsx @@ -17,8 +17,14 @@ export async function querySuggestor( ) let finalRelatedQueries: PartialRelated = {} + + const hasImage = messages.some(message => + Array.isArray(message.content) && + message.content.some(part => part.type === 'image') + ) + const result = await streamObject({ - model: (await getModel()) as LanguageModel, + model: (await getModel(hasImage)) as LanguageModel, system: `As a professional web researcher, your task is to generate a set of three queries that explore the subject matter more deeply, building upon the initial query and the information uncovered in its search results. For instance, if the original query was "Starship's third test flight key milestones", your output should follow this format: diff --git a/lib/agents/resolution-search.tsx b/lib/agents/resolution-search.tsx index 1acd0e01..17ec3733 100644 --- a/lib/agents/resolution-search.tsx +++ b/lib/agents/resolution-search.tsx @@ -43,7 +43,11 @@ export async function resolutionSearch(messages: CoreMessage[], timezone: string }); const systemPrompt = ` -As a geospatial analyst, your task is to analyze the provided satellite image of a geographic location. +As a geospatial analyst, your task is to analyze the provided satellite imagery of a geographic location. +You have been provided with two distinct previews for comparison and depth: +1. **Mapbox Preview**: The current live view of the map. +2. **Google Satellite Preview**: High-resolution satellite imagery from Google. + The current local time at this location is ${localTime}. ${drawnFeatures && drawnFeatures.length > 0 ? `The user has drawn the following features on the map for your reference: @@ -52,13 +56,13 @@ Use these user-drawn areas/lines as primary areas of interest for your analysis. 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). +1. **Comparative Land Feature Classification:** Identify and describe the different types of land cover visible in both images (e.g., urban areas, forests, water bodies, agricultural fields). Note any discrepancies or additional details visible in the Google Satellite view compared to the Mapbox preview. 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. **Structured Output:** Return your findings in a structured JSON format. The output must include a 'summary' (a detailed text description of your analysis that explicitly mentions findings from both Mapbox and Google previews) and a 'geoJson' object. The GeoJSON should contain features (Points or Polygons) for the identified POIs and land classifications, with appropriate properties. -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. +Your analysis should be based solely on the visual information in the images and your general knowledge. Do not attempt to access external websites or perform web searches. -Analyze the user's prompt and the image to provide a holistic understanding of the location. +Analyze the user's prompt and both images to provide a holistic understanding of the location. `; const filteredMessages = messages.filter(msg => msg.role !== 'system'); diff --git a/lib/agents/task-manager.tsx b/lib/agents/task-manager.tsx index 90a72b67..fa708554 100644 --- a/lib/agents/task-manager.tsx +++ b/lib/agents/task-manager.tsx @@ -15,8 +15,13 @@ export async function taskManager(messages: CoreMessage[]) { } } + const hasImageAnywhere = messages.some(message => + Array.isArray(message.content) && + message.content.some(part => part.type === 'image') + ) + const result = await generateObject({ - model: (await getModel()) as LanguageModel, + model: (await getModel(hasImageAnywhere)) as LanguageModel, system: `As a planet computer, your primary objective is to act as an efficient **Task Manager** for the user's query. Your goal is to minimize unnecessary steps and maximize the efficiency of the subsequent exploration phase (researcher agent). You must first analyze the user's input and determine the optimal course of action. You have two options at your disposal: diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 64e8a305..83629a92 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -37,7 +37,7 @@ export async function getModel(requireVision: boolean = false) { baseURL: 'https://api.x.ai/v1', }); try { - return xai('grok-4-fast-non-reasoning'); + return xai(requireVision ? 'grok-vision-beta' : 'grok-beta'); } catch (error) { console.error('Selected model "Grok 4.2" is configured but failed to initialize.', error); throw new Error('Failed to initialize selected model.'); @@ -52,7 +52,7 @@ export async function getModel(requireVision: boolean = false) { apiKey: gemini3ProApiKey, }); try { - return google('gemini-3-pro-preview'); + return google('gemini-1.5-pro'); } catch (error) { console.error('Selected model "Gemini 3" is configured but failed to initialize.', error); throw new Error('Failed to initialize selected model.'); @@ -81,7 +81,7 @@ export async function getModel(requireVision: boolean = false) { baseURL: 'https://api.x.ai/v1', }); try { - return xai('grok-4-fast-non-reasoning'); + return xai(requireVision ? 'grok-vision-beta' : 'grok-beta'); } catch (error) { console.warn('xAI API unavailable, falling back to next provider:'); } @@ -92,7 +92,7 @@ export async function getModel(requireVision: boolean = false) { apiKey: gemini3ProApiKey, }); try { - return google('gemini-3-pro-preview'); + return google('gemini-1.5-pro'); } catch (error) { console.warn('Gemini 3 Pro API unavailable, falling back to next provider:', error); } From 0732290c634a71470191f69a9c3d4ddb400b0c07 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:13:21 +0000 Subject: [PATCH 4/6] feat: enhance resolution search with multi-image previews and fix follow-ups - Capture and display Mapbox and Google Satellite previews side-by-side in resolution search. - Update `ResolutionImage` component for multi-image comparison and session persistence. - Fix `getModel` utility to correctly return vision-capable models (e.g., `grok-vision-beta`) when history contains images. - Make Task Manager, Inquirer, and Query Suggestor agents vision-aware. - Include `mapProvider` context in follow-up submissions to maintain tool and model selection accuracy. - Enhance `resolutionSearch` system prompt for comparative analysis of dual previews. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> From cd272904d5362911de3e1b633e47511c934504e8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:58:03 +0000 Subject: [PATCH 5/6] feat: reset branch to 76ace98 and synchronize with main - Hard reset to commit 76ace98. - Merged origin/main. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- CC BY-NC 4.0.docx | Bin 0 -> 11099 bytes app/actions.tsx | 88 +++++-------------- app/layout.tsx | 52 ++++++----- components/chat-panel.tsx | 2 +- components/chat.tsx | 7 +- components/conditional-lottie.tsx | 4 +- components/followup-panel.tsx | 3 - components/header-search-button.tsx | 61 ++++++------- components/header.tsx | 55 ++++++++---- components/history-sidebar.tsx | 35 ++++++++ components/history-toggle-context.tsx | 30 +++++++ components/history.tsx | 52 +++-------- components/map/mapbox-map.tsx | 38 ++++++-- components/mobile-icons-bar.tsx | 2 +- components/profile-toggle-context.tsx | 7 +- components/profile-toggle.tsx | 18 ++-- components/purchase-credits-popup.tsx | 73 ++++++++++++++++ components/resolution-image.tsx | 88 +++++-------------- components/sidebar/chat-history-client.tsx | 39 ++++++++- components/usage-toggle-context.tsx | 30 +++++++ components/usage-view.tsx | 96 +++++++++++++++++++++ lib/agents/inquire.tsx | 8 +- lib/agents/query-suggestor.tsx | 8 +- lib/agents/resolution-search.tsx | 14 ++- lib/agents/task-manager.tsx | 7 +- lib/utils/index.ts | 8 +- tests/map.spec.ts | 5 ++ 27 files changed, 527 insertions(+), 303 deletions(-) create mode 100644 CC BY-NC 4.0.docx create mode 100644 components/history-sidebar.tsx create mode 100644 components/history-toggle-context.tsx create mode 100644 components/purchase-credits-popup.tsx create mode 100644 components/usage-toggle-context.tsx create mode 100644 components/usage-view.tsx diff --git a/CC BY-NC 4.0.docx b/CC BY-NC 4.0.docx new file mode 100644 index 0000000000000000000000000000000000000000..0d8f7d559328446c99bded42597b2e03fbd0f6c4 GIT binary patch literal 11099 zcmaKS1yCK$*6qOw?oP1a?(XjH9^5@>aCbYnI~*K>ySuvthXBEY+vEGC?!Bq^-b~GO zjjUa>SMR;MdU`3zLO@~w003A3BfGqI+I&DiDHs5N3;_V3f85m-b+C6evv)OA^K>+G z(P!|mvn@^>SLkO#5x@5dIj{qAnFXN8%t6JGyDAIx)Kf+*K9I!eu60`^CNn5e<=K;R zyIk<^;iiIu&Q?E@;$6^mO~6m}P}juR(7ttfj-GS(f5}eibXU3}BQ{**JUGMw;HDF5 zbjVWk!3I=7&@$hH5vb!4Rmr(VSHY^-nZfTiQJn{0GX%(L;-}A@uwxrtfi3+?mVcm z2#rXylb75{Rs(tl7|dqjnH^g*ROH~;|u z(Hc6N*}5Lgilbtx)5iUij%t%-pg8FcipC)R=A(kHBOX(JsoBP~|z_!Hf@GxTZJ4($7#ozRO! z=!Si<{o4pzk~b&ZBN135Fgr%8)hgAu+IoZ%x5<)Nh#VQicnnh1?3joiYnCppvs?28 z79_GFmR}EV1afhYf|uS~3(SV%CcP)H&NLs^p|Le*z61V~kDw{%t%(mFDnG)E;G=bS za5iQ9Z&FMhOx)}~qV1nB`)`8cy2vcKuYU@XmhrBKbU2_Fd zbDh#aTDNpcwz@=E#f=Gc$UAUi5vx{Zsz8rhEH=lH@`?3PGXsZH-#zGn6C#wgWEBR+ zjL3^AO!ct4){Fpq`hGVqA<2L%;7vOfA{}PU)KenGsSnyFko(6N*&`n z?r;*mHDmggu@%8E zUEnHR9oGj&wWZjg^}+tMRKPFW;>t5~aoNY#`T#n9g7rND-(()h9^s5sZSVwh>h5#( zLOT`fpxnJ{(=@pc2iL6I#XFYlfjw?->XC@| z$9jXda~~lt!PeYqc%rtlR;;P#eOLdx47*I{Re7l;&6Xp#mZM}iG||5LuKR=tatmt#+=2dF4HVgw*)+$glQHTMvEjG zS&DFvU)8~8!qpC)h4&9DDhH;sbd$Vg(15B1Y#w3oYBx0F$+RI}RLc2P2p*Zl)uDP* z(+64iIASZyoJ5_yE|^91*laCXH&M{`t|+GAm}?jvkhL~P1xp-ywbscrBYwDYz9yYR zh=(->bUSLYNdZ>@hvyvD2+heUF;h(bK@2^3>p0J=By}Sddf;l?n2j?PF&vtcZ{>+- zPBIhLvQ*B!krV7@vgJ_>sI5SmKYB@{q0qxfHj(LL6XsOsIO(JmL1cT+V6;5(G6G!? z2$q=X_VnvmW!CE2wHizr%BJgJ{Ga&T7>vrpiYp1m?!fBwtf>?N01?2(FMT_*ONm{a zzhZx9fjeO9O9)e3339>iaqdFNa87G%FRQgu^dQ@BIoVP)kOSQomQf+_dojP$eWiRf4E zt`74molMC7?EY<)g4f?EC1Xr*bgCk;BJTV4DE$eAy>O-WuyI-RJtTO(m-iZ9WKBk3 zA?u(DtbdF(39yVw#G$Kri+y2*nM#qN5hEWjVAiLiON#%7k75F)B2Q@Nt;n^vxsqa& zFaq9Ed(AckSD=$CO}PN!VUX8_6khg4b+Wisdt{+sMC>AZfL-UQVV_3{_ioe+rX@L@E+yuc{uxcXMW_Wd zQM%gDo*(kr-&0dEM;l6u0~M@yr@~G?g?{ZTd5SW!uEaNa-(Pb@r-8*^{%Vk_BV`eJ zbHmLVQ~lYrYit4FQz=F&YrG3{DfBLyw7gaKtqlu6TV!o>^g{3pbQz07ie))gA$~CK zMuoRpU*kYyz^2{~`qD=YoZl73HeKo+!KcmQDHkOzO?lD{)Wk1XJxB>$g*{Z-K#!~M zD<71jpA3ue(mbJqm|a9NDiotM)i&P0#nw{#88_@+L(UVppJs#z)D6W9Byq=L%p+btF(qtZzm?yXKG1&Tp3%T zmonm3AKzmkoCr)M-YYN#i}zP%*2UDO&+FXJ3bQ$MT_wAZ?rce6)*cK>Aq9R6!w1e& z?z;J(;|H>vhtz+cOAS`tTPdirFGH>6YMJB6uLgb-hRmF5; zJ6AH=&^)#rZYn*$AD@+S-~AI@Y{buvAIWPdh}w1DE45rfjf^-WIQWqx%!$O30U{@$*BFHo6Qdg@^0l{@Y< zOBEP@=}j^s0D<*aIZ0z7M=(Tr(mnE; zyWdcdbcJ_YKz1u&kI8f6f-etk%SMw_im^`^^r!){BnG}dL&CyDh8Ks^Pb~*yqE+sY zG;w(R)1gyKhV+%QZw7ESU&AQYR&hYKu6RV1fwL`z-2pDJD3M@c8-b!}cjTxw)`sbM z-~~*Kqlt_Z#ced**c_7I6i5gbBmhGzFXQv8tH)6dH}k0$P9^$8 zIfy>PL$#>jS{8h{Wb&237S+*u|7IPfU%Nqx)q$l;Q=wO}Vcp@VzlAMzcObCwCRs~G zj0eUtFk`pE`JEst>V*|Ds1qzR)gE<}6C&c+%$@sXuO~pZ)eX@@pZuu|g3}>B$0X3Z zB~oAQiWA7+T3ug-malt-9r)^!sEU53hW#ZAEc1cp>huvnVeEQ7zcpH3TO#08^ZYz8 zrmVLxB&p`YU5Wyyrtu&gLupyn?M z4Ky}#{vNJ&bzH8-&=nfH=bx31C;hv_o(d#b{he3=5(dFidYg(uGG-R_n{TC2BuhD`^T*IE^5Q~O(t^&8_g-SazK*2PM9~za zRV;t;$JIOoHm{dt@23gUJjv!xXj!)7vVVy~J-F=d`M0fjk;RMzf0Z+h^bI5(T0lmh zghwp{^AYfie-bay#xRzNShbDD745w5`NYS}w#?_&34gD2?U2q}_tY3$|7u}ECYBa8 zLfeG~%84Mz7YwJevagP9$_<+QdCH2~pB(+5G;(Lc`FHZy5Eb;RF-uUcs<@^0q&Mhj zWQuy=Xr~wL(cYeBzqsT1lSZ)yR}cqL1Y^}`G9bG z}#sOBV(T8yq`wMD5lT{BbGIc>TQ5{L|DPYD3t14y-4Nr29iIxhH zT9Kt}k;H|kP4BrFx7fI0J7Sz${1h6GtEADVa-&oMd9JS~?k8LNZVI`by?>9)nzPWL z6R#Vk+oooMUH1`NZq_*WdN33I&}?u}Xb?0#)>`J5p{kLpR{mbf&OtxZBLu<6@lu|q z@7Z+h8r3lhdnySFY7Hz1F=o)%8#$JaTc>|;YwgO4bfa;LBp!F&z(?Q>|HP??YQkfN z)f|KqEpcD>M>b6mI5v6rdVg5!zRwNYMe?zk(^=-F8wSRU@n@Qu2NgnGq+u@nXsjp?OMNZzX+&lF zg)JiaQrm}g=95}G<6N8=+h;pq@jxWMDQu%LC5#5YhCgM884(6jUw_8A!GxpbuG56d z`Gl3ly7A~pW`%E5Mp6J~@pA*pzbI%KxX`@4KF9@)&4c*b6U=&c=0|f96$OQygfCdj z^KzRkM8t-9^rG}cbG$HdWlK?f9dvSOxl)taK7mYMN)3xzG#9&xi&$N&3U_I3&N9yG zYn0u^0}-nmG>HLzYR{PU)7w_^+tlT}A1g+x5qCRU^Z`LC;1}?`wC(fGN5@fw?LuRe z-=^r6J{`Qg^Z*o?p;voAdR?uHd{+;Ah)Pt@d@5=a6K!xjOy%2xU`k;THp6JkVWh5QWe{5v(k_m0c$TSG<5nT|?aM;g1E!hQD};eFh3Q;|~)< zPRSj6WhEhNG?t(lYmU{6Yz{Ih&wNrE;Kx@e8aoz8y&nzT4vjbC*F{)KBGS68oL<5k z7tEE>Gx6l%;a&W0yj+)^v~i73(@@vJ;o{6~VRVKqdey6?1l`1Lj~0{Y)eA?XN1)Ml z8O6~*ren_3OCJS2T<9WFOSyz5u*cM!Q0xM-7Zds>|C`=*jbf!I{tUyGT7?wR=f znbXDu%d4l}a3#*3k=eYKWgAk-!EKnwvp^_@NUtiAFHz07Y*EE5zoIbHy&&Dnw-Z&c z5?tFYvZCzaX*AHpmuoPPulDkL1{nF0#Jy)};YZN-8X^r!Y7a3d6$8u1ieL2 zkFC8@BwdDrlZTem()_}OC2jfd0dc>x3Sa(^-VA4AgNmOYyRGP0f6*Ff^&?e(A2tQ$7H{wS)ojCPLbmGfpMR8o zVcbIxro+vKmL$jy<+L>j{M_8g5HQw@)wa#8WIjMp19hZ?<~}RnMYxIJ+)tS)8xa;f z{ZOg?B0YgyrC`)wLivT~T$#U;AnxUzb$=eX@W#a|4mBh3M?QRfJeR`|a9 z@#Ws#Jg>jshVKwXG$xhXRg&#%*qTej$as%(aI^%)k!6b3$kP|Nr;v@R@Hywc5~XQW zA4PmT{pldj=j6wm57xR4tWReUkc{PO431O{+(U5*-fdoc1SGAEK~nMskj>qUCuBFg zj!%a+5^;kW^sEpK`icTx>W&S4I26wurD4;?NJX$jgnu>nM=h$$4;2vs+^xo zp5(@c#kNl2^n^vCwZ?aI^kqx6N5C5}1b@Id>5B!IJ=Q9nTm6fQ za4=E*KXc7K2^{^SW@LZG5IJtxlBK??#o%oG(F^}z=f~8)fVG) z-XzfYN)aMW-iqvxOyT;TY4W;(yw|&~^1W!eu0#AnU(x$VM1C6y%e}0apw)1V0t%f4 zY6-Ew4~SIxQwcU^gj}t(pQU2YA3cB%3yjR<-f&If+J$Sb9B@=MJ}6@fJu2vpseFwl zb``jV^ff(t3FcKMS|vXfom5nUXa(uTU$3FjoiZZfo=0tbFtz}OR@yZ3W44?Q?derj zb#hGDLY#Og)hg0iYn4FK^coD_o{h6wY|934XBu)$3$n&1fEfR&Q5ry?A@w`gjXZ^b z89%X>rD^_4z|LLDLAFFdJEu%RPGt`qgR1Fz*TCV()G@qG2Ru<#tZ1Ndsy3<1W$&jb zs)FDi0&_B=ZG)t0ov~Zv_83eRr!t&)nOV#deyuova)mt)Re6k*lOGDS+Z|GF7Fq=j z_(rGj&9RqRYSrqchCaVQR=>Y@bdbV1*IzvNLp9eZYsam+f7d%0nRnVE%j`Q@2cEhku0#gZxhiis27;R8Uqsb8WXH{QNg);d$Q)sj?8S& z9cs>{n6FEV6^yl)bl(FRZr6=poo)Vd*t5Ah#|E25(M*cEy8UzbjriRD5}c@Po8#?y z^WjOsrz2f~@}hbu2#Iv}@b&rXtkKQRhgtv-+nh8D#F3w-RMjwwsb;)dD9n{i0-@%I zXbJ$BA;#SezqH_d})4@x5}dth@P$@8-R;cl0*g~yAtHDd$J=YrCGnd*HDB9;s+ z@!j6p$&bCw3~F)sSr_F5Fnv3D`?&w~O3$?Y00B#hqH`f6r5}yj2SSZ_mvY>7e)08j z8jgmN#KyjFE`*=daeWqggl#!-fl}#%hS&R<>7)sjjvc2*}CQi@Lwp)P%)*iI}t89`u61{?%uQ zLd5*^fGVfPB;|x9^3ki#P~> z!r3nZQ$dI&N%qea0L)RQWbeH-%o}oK`1y2nbKhdS|7vU4C_ZjYqooC$fsnN!g2BGT z2)aNv^ygagObfwxoL8we9~NTVzi>-jh$e)CKnV35F^L3KDd45qU`f@+r z#h(-lsl#ut*lDvQQUw)JGW~&?e)~UmijQ9h6%$sGP>E^Fz#GKZy%tm)_FLow+i59O z9eB>uGb^`GGM4Boz2-A|xyw-w?A>`xY{Wn|}XR|_h7nVuoqIiaJyLo#oFwD#haPH+M3>geY(V6w*(E#YTeDL`XLU@SBq zWM!7cj8}wnYD=YB&n|EI)3{Oa1}09u+4bDis4t?}uLB7te3Y2T8-!)#NsVp8i?90R zDxy@=zcDnfgyHNrN$bsN$gqAQ4l0@#3K1cEt-H`WnQ%a;9uL;ecgvGt{zIswTzE?? zpff!J48jHU*-5rN)Jd;An8MIi5vFTEY_DH#;^jN03}$^2)B5eEcsTHe`aI9FvlPmhynol36LOqq-cpMSD6y-a${ zTG~DOIZjs6oQ+~s0^zrWbk@s(hVk>$ObT>WrWF;gsaNUuA&pj- z0uRJ)Gm_%MqE5z!T><2I8tekJXB^uaTLl4L8gOr#$;_J?OJgl(9rwz$Z&|}GQ|$z3vpayqr}`{YbH zA@R~yuE8a091FA}_dEQMlLHo$CS`g8%{s1QS@oJTX(}|B)u}abW?Ybv1e|3zJ*kNE`&&Gi0|~!2Sz^TIQ7a@ddprMfOKrgb8>Zxajg-5Pj@Lmcu(M zCxfFC_fd#?p3xH5G0C)5rnY4~Dy@|)T4~EChrx<976flc(}uhYA`l()S7gRURkhGb zF#u3gxTHr7fL)7s9WKg}71t875ytS^Z;xw-(fE#FmoKJ=F3=?78aBJINFA~~gYtD5!o zdzDJW+?d+IoG%t-J8?D&C?M`claFIMcsyOZ_7fR|iuWJ%8)Z{(?U)Oj1l}kdefvH_ zTZ(o`xr{tq=1$TUeYMv{ibdC3PPH=X?+*##~P4}AK% z$ZS9=S?-_2<0n`Cv-Np>q70V&+J+J0ckr~D;}WOFonV95Y4Vh7I}HLuUZzKUI0V;k zzSK#HbYGo%)cE9Jkrq-c^&Inm z7tmcT&Fst=|Le^BkBHzvOV(kJ1EuR;6Q$!~%!}I@#iH&T-JcmO@!bLoodR+@DjDK* zuE%>qWyZ03w2PrAtTJQ{>F+m_Q3SC*HwWYvztZ8-F?}M=rdgw@;v%{BM$z7XAPPd^ zNQ-;OY4skXb*n^lZ@f8+R;$anL<(h%+td$~s~r3=8--1Yc+pEv?r-=`;#^=vgGP(B z4`}3W$Xd0vKlrkT1dU4-;e6(34*neh~oMum|GX)S1Xqj}TnI-z;e;$=}DTCfxG9a`do4}`2 z9~*uX{r-iXzs}67;&rNY^QT2Lf0?F)U(y0Vyzw z9t|tI-4&;{H`X5ZxUw_9K9?iU!>6PSSAHHYXG8s3)`{rp(7oraPQb9&6JhO|gK2N{ z9;Kl*TweUlAG0tZBWRa{sTdg<#wL(Q=#6#{`TOo<6VpOPIDGw+D|gqjZO6{2gF^3B z)p{0E-&ismJ2jKDVgGGQgYY!MP35O`zZuN?!0Ly0L(Ph8S#CFzO<^G-V%UniUuf7O z#ZYSbERzLFLPvRS0s1r4ps_Cl6{+E48c!R`RT+g0&e9d<4UiECG}$sx#eNk-)9xd- z!ySgY1tn9ihhz0`o3j>UO^e6bnr|k_O^ZdJf`Uy)7=ogQ(jgkT?x+(JbZAA z;;rRo$^w=7+T$;l5~q#alV>WDt(=S1ZfV@__1-BCtoiXSEIowl3}r^=#JtH-ecKXp zzwshFT`t8YA8Gpz5BV8{-T4ahpA})^_ois@q$JV~IT*njmu(ZV$8*17cunTtyqkp!jpF*BC;BJW?WXN% z6p~kcXFc>U@7MuD_N0s6j>DNhtQJ)-ViM^KQ7|=psZIsD>vGpkZt8w+^m= zf~0K0aFF-eG}AA+UtmYAMosWYtocq7w!5l{dJ-%P#rxH@h8D}ZfY6#|FLLvwOt5ST zWQ+*%i#?3A$pl+^T8V;14bBm+=&zqNpC-OR9eGwCP6i%6<9qmd{-m!-SuzBPsEES6 z!{K%PrLEFRv>Z2qHj2#{SVIX^oAHI(XBV=dO5VgL!9Er5p3ft*h**Qt; zE5OA;s=D~~UIKEH5SfuLt3>xzln!NHuI!+zvIx+)Z%*)|<2H5oi}=@|DLz=?am2*m zYx*uR7{u2I(xzZZDd;X`K$&mg4l3!ahb&5eLT{oA3yjm~vqlB@i9w?3gqW2Gzj`ot z;K$;$7+)AqM!MdMy}Ltx|A=|ov@`tAj+BZOL5utN`tpzCKRVLR%+>fGH^j&5;QE+Q z{C74>VVg4>F{7jsk)SAxqY6$FKHKF>%Q%O3buJA)zWZ)=ywK$&UtiZN%((@qV)LO;;!M<0NJV*vi&BFYap03R{=KimIB zQu()mza@kJAphHd>PPc$0>Zz6e{1Xh1vL3kAO8Q8c7I#?Tc7qXOI9C)?tfYOceUEz z@V`grf59tA{|El}SpBz&ztj0&CORnoFUkEk{O?rw7rdMD-{Aj~7=NSx4#9t+L#X}@ Z{l5XJBnt)o&nvJWJNO6FqSXIf{U0 - + ); @@ -715,8 +682,8 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { } break case 'assistant': - const answer = createStreamableValue() - answer.done(content) + const answer = createStreamableValue(content as string) + answer.done(content as string) switch (type) { case 'response': return { @@ -728,7 +695,9 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { ) } case 'related': - const relatedQueries = createStreamableValue() + const relatedQueries = createStreamableValue({ + items: [] + }) relatedQueries.done(JSON.parse(content as string)) return { id, @@ -750,28 +719,13 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { case 'resolution_search_result': { const analysisResult = JSON.parse(content as string); const geoJson = analysisResult.geoJson as FeatureCollection; - const imageData = analysisResult.image as string; - let mapboxSrc = ''; - let googleSrc = ''; - - if (imageData) { - try { - const parsed = JSON.parse(imageData); - mapboxSrc = parsed.mapbox || ''; - googleSrc = parsed.google || ''; - } catch (e) { - // Fallback for older image format which was just a single string - mapboxSrc = imageData; - } - } + const image = analysisResult.image as string; return { id, component: ( <> - {(mapboxSrc || googleSrc) && ( - - )} + {image && } {geoJson && ( )} @@ -784,7 +738,7 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { case 'tool': try { const toolOutput = JSON.parse(content as string) - const isCollapsed = createStreamableValue() + const isCollapsed = createStreamableValue(true) isCollapsed.done(true) if ( @@ -812,7 +766,9 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { } } - const searchResults = createStreamableValue() + const searchResults = createStreamableValue( + JSON.stringify(toolOutput) + ) searchResults.done(JSON.stringify(toolOutput)) switch (name) { case 'search': diff --git a/app/layout.tsx b/app/layout.tsx index a092d4fe..bddadc19 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,7 +12,10 @@ import { SpeedInsights } from "@vercel/speed-insights/next" import { Toaster } from '@/components/ui/sonner' import { MapToggleProvider } from '@/components/map-toggle-context' import { ProfileToggleProvider } from '@/components/profile-toggle-context' +import { UsageToggleProvider } from '@/components/usage-toggle-context' import { CalendarToggleProvider } from '@/components/calendar-toggle-context' +import { HistoryToggleProvider } from '@/components/history-toggle-context' +import { HistorySidebar } from '@/components/history-sidebar' import { MapLoadingProvider } from '@/components/map-loading-context'; import ConditionalLottie from '@/components/conditional-lottie'; import { MapProvider as MapContextProvider } from '@/components/map/map-context' @@ -70,28 +73,33 @@ export default function RootLayout({ )} > - - - - - -
- - {children} - -