diff --git a/CC BY-NC 4.0.docx b/CC BY-NC 4.0.docx new file mode 100644 index 00000000..0d8f7d55 Binary files /dev/null and b/CC BY-NC 4.0.docx differ diff --git a/app/actions.tsx b/app/actions.tsx index 9e0ee20a..673b921e 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -13,27 +13,25 @@ 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' 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' -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' @@ -43,16 +41,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.'); @@ -61,7 +60,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' && @@ -71,16 +69,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: [ @@ -90,12 +84,11 @@ 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 = ''; @@ -107,10 +100,17 @@ 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 => { @@ -132,8 +132,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 +145,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' }, { @@ -173,12 +174,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(
+
); @@ -243,7 +243,6 @@ async function submit(formData?: FormData, skip?: boolean) { uiStream.append(answerSection); - const groupeId = nanoid(); const relatedQueries = { items: [] }; aiState.done({ @@ -327,7 +326,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') @@ -361,7 +359,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() { @@ -410,7 +407,8 @@ async function submit(formData?: FormData, skip?: boolean) { streamText, messages, mapProvider, - useSpecificAPI + useSpecificAPI, + drawnFeatures ) answer = fullResponse toolOutputs = toolResponses @@ -643,12 +641,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 { @@ -669,8 +665,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 { @@ -682,7 +678,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, @@ -704,11 +702,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 && ( )} @@ -721,21 +721,37 @@ 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 ( 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 } } - 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} - -