diff --git a/app/actions.tsx b/app/actions.tsx index 31bba0bb..a1fb117f 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -13,10 +13,8 @@ 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' // Added getSystemPrompt +import { saveChat, getSystemPrompt } from '@/lib/actions/chat' import { Chat, AIMessage } from '@/lib/types' import { UserMessage } from '@/components/user-message' import { BotMessage } from '@/components/message' @@ -24,17 +22,17 @@ import { SearchSection } from '@/components/search-section' import SearchRelated from '@/components/search-related' import { GeoJsonLayer } from '@/components/map/geojson-layer' import { MapDataUpdater } from '@/components/map/map-data-updater' +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' // Add this import +import { MapQueryHandler } from '@/components/map/map-query-handler' // 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' @@ -44,16 +42,17 @@ 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 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); - } if (!file) { throw new Error('No file provided for resolution search.'); @@ -62,7 +61,6 @@ async function submit(formData?: FormData, skip?: boolean) { const buffer = await file.arrayBuffer(); const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`; - // Get the current messages, excluding tool-related ones. const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter( message => message.role !== 'tool' && @@ -72,16 +70,12 @@ 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.'; - - // Construct the multimodal content for the user message. const content: CoreMessage['content'] = [ { type: 'text', text: userInput }, { type: 'image', image: dataUrl, mimeType: file.type } ]; - // Add the new user message to the AI state. aiState.update({ ...aiState.get(), messages: [ @@ -91,12 +85,11 @@ async function submit(formData?: FormData, skip?: boolean) { }); messages.push({ role: 'user', content }); - // Create a streamable value for the summary. - const summaryStream = createStreamableValue(''); + const summaryStream = createStreamableValue('Analyzing map view...'); + 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 = ''; @@ -108,22 +101,41 @@ 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) { + 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 => part.type !== 'image') + 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(
@@ -133,8 +145,6 @@ async function submit(formData?: FormData, skip?: boolean) { await new Promise(resolve => setTimeout(resolve, 500)); - const groupeId = nanoid(); - aiState.done({ ...aiState.get(), messages: [ @@ -148,7 +158,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' }, { @@ -174,12 +187,11 @@ 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(
+
); @@ -199,7 +211,17 @@ async function submit(formData?: FormData, skip?: boolean) { 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' @@ -273,9 +295,8 @@ async function submit(formData?: FormData, skip?: boolean) {
); - uiStream.append(answerSection); + uiStream.update(answerSection); - const groupeId = nanoid(); const relatedQueries = { items: [] }; aiState.done({ @@ -392,7 +413,6 @@ 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') @@ -426,7 +446,6 @@ 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() { @@ -475,7 +494,8 @@ async function submit(formData?: FormData, skip?: boolean) { streamText, messages, mapProvider, - useSpecificAPI + useSpecificAPI, + drawnFeatures ) answer = fullResponse toolOutputs = toolResponses @@ -717,12 +737,10 @@ 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 { @@ -780,11 +798,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 && ( )} @@ -811,9 +831,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/app/layout.tsx b/app/layout.tsx index bddadc19..4369af43 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -65,6 +65,29 @@ export default function RootLayout({ }>) { return ( + +