diff --git a/CC BY-NC 4.0.docx b/CC BY-NC 4.0.docx deleted file mode 100644 index 0d8f7d55..00000000 Binary files a/CC BY-NC 4.0.docx and /dev/null differ diff --git a/app/actions.tsx b/app/actions.tsx index 673b921e..2068b24b 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -13,8 +13,10 @@ import { Spinner } from '@/components/ui/spinner' import { Section } from '@/components/section' import { FollowupPanel } from '@/components/followup-panel' import { inquire, researcher, taskManager, querySuggestor, resolutionSearch, type DrawnFeature } from '@/lib/agents' +// Removed import of useGeospatialToolMcp as it no longer exists and was incorrectly used here. +// The geospatialTool (if used by agents like researcher) now manages its own MCP client. import { writer } from '@/lib/agents/writer' -import { saveChat, getSystemPrompt } from '@/lib/actions/chat' +import { saveChat, getSystemPrompt } from '@/lib/actions/chat' // Added getSystemPrompt import { Chat, AIMessage } from '@/lib/types' import { UserMessage } from '@/components/user-message' import { BotMessage } from '@/components/message' @@ -25,13 +27,14 @@ 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' -import { MapQueryHandler } from '@/components/map/map-query-handler' +import { MapQueryHandler } from '@/components/map/map-query-handler' // Add this import // Define the type for related queries type RelatedQueries = { items: { query: string }[] } +// Removed mcp parameter from submit, as geospatialTool now handles its client. async function submit(formData?: FormData, skip?: boolean) { 'use server' @@ -41,25 +44,42 @@ async function submit(formData?: FormData, skip?: boolean) { const isCollapsed = createStreamableValue(false) const action = formData?.get('action') as string; - const drawnFeaturesString = formData?.get('drawnFeatures') as string; - let drawnFeatures: DrawnFeature[] = []; - try { - drawnFeatures = drawnFeaturesString ? JSON.parse(drawnFeaturesString) : []; - } catch (e) { - console.error('Failed to parse drawnFeatures:', e); - } - 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[] = []; + try { + drawnFeatures = drawnFeaturesString ? JSON.parse(drawnFeaturesString) : []; + } catch (e) { + console.error('Failed to parse drawnFeatures:', e); + } + + let mapboxDataUrl = ''; + let googleDataUrl = ''; - if (!file) { - throw new Error('No file provided for resolution search.'); + 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( message => message.role !== 'tool' && @@ -69,12 +89,32 @@ async function submit(formData?: FormData, skip?: boolean) { message.type !== 'resolution_search_result' ); + // The user's prompt for this action is static. const userInput = 'Analyze this map view.'; - const content: CoreMessage['content'] = [ - { type: 'text', text: userInput }, - { type: 'image', image: dataUrl, mimeType: file.type } - ]; + // Construct the multimodal content for the user message. + 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({ ...aiState.get(), messages: [ @@ -84,11 +124,13 @@ async function submit(formData?: FormData, skip?: boolean) { }); messages.push({ role: 'user', content }); + // Create a streamable value for the summary. const summaryStream = createStreamableValue(''); const groupeId = nanoid(); async function processResolutionSearch() { try { + // Call the simplified agent, which now returns a stream. const streamResult = await resolutionSearch(messages, timezone, drawnFeatures); let fullSummary = ''; @@ -100,6 +142,8 @@ async function submit(formData?: FormData, skip?: boolean) { } const analysisResult = await streamResult.object; + + // Mark the summary stream as done with the result. summaryStream.done(analysisResult.summary || 'Analysis complete.'); if (analysisResult.geoJson) { @@ -147,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' }, @@ -174,11 +218,13 @@ async function submit(formData?: FormData, skip?: boolean) { } } + // Start the background process without awaiting it. processResolutionSearch(); + // Immediately update the UI stream with the BotMessage component. uiStream.update(
- +
); @@ -243,6 +289,7 @@ async function submit(formData?: FormData, skip?: boolean) { uiStream.append(answerSection); + const groupeId = nanoid(); const relatedQueries = { items: [] }; aiState.done({ @@ -326,6 +373,7 @@ async function submit(formData?: FormData, skip?: boolean) { } const hasImage = messageParts.some(part => part.type === 'image') + // Properly type the content based on whether it contains images const content: CoreMessage['content'] = hasImage ? messageParts as CoreMessage['content'] : messageParts.map(part => part.text).join('\n') @@ -359,6 +407,7 @@ async function submit(formData?: FormData, skip?: boolean) { const userId = 'anonymous' const currentSystemPrompt = (await getSystemPrompt(userId)) || '' + const mapProvider = formData?.get('mapProvider') as 'mapbox' | 'google' async function processEvents() { @@ -407,8 +456,7 @@ async function submit(formData?: FormData, skip?: boolean) { streamText, messages, mapProvider, - useSpecificAPI, - drawnFeatures + useSpecificAPI ) answer = fullResponse toolOutputs = toolResponses @@ -641,10 +689,12 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { case 'input_related': let messageContent: string | any[] try { + // For backward compatibility with old messages that stored a JSON string const json = JSON.parse(content as string) messageContent = type === 'input' ? json.input : json.related_query } catch (e) { + // New messages will store the content array or string directly messageContent = content } return { @@ -665,8 +715,8 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { } break case 'assistant': - const answer = createStreamableValue(content as string) - answer.done(content as string) + const answer = createStreamableValue() + answer.done(content) switch (type) { case 'response': return { @@ -678,9 +728,7 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { ) } case 'related': - const relatedQueries = createStreamableValue({ - items: [] - }) + const relatedQueries = createStreamableValue() relatedQueries.done(JSON.parse(content as string)) return { id, @@ -702,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 && ( )} @@ -721,7 +784,7 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { case 'tool': try { const toolOutput = JSON.parse(content as string) - const isCollapsed = createStreamableValue(true) + const isCollapsed = createStreamableValue() isCollapsed.done(true) if ( @@ -749,9 +812,7 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { } } - const searchResults = createStreamableValue( - JSON.stringify(toolOutput) - ) + const searchResults = createStreamableValue() searchResults.done(JSON.stringify(toolOutput)) switch (name) { case 'search': diff --git a/app/layout.tsx b/app/layout.tsx index bddadc19..a092d4fe 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,10 +12,7 @@ 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' @@ -73,33 +70,28 @@ export default function RootLayout({ )} > - - - - - - - -
- - {children} - - -