Skip to content
Binary file added CC BY-NC 4.0.docx
Binary file not shown.
92 changes: 54 additions & 38 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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.');
Expand All @@ -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' &&
Expand All @@ -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: [
Expand All @@ -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<string>('');
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 = '';
Expand All @@ -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(
<GeoJsonLayer
id={groupeId}
data={analysisResult.geoJson as FeatureCollection}
/>
);
}

messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' });

const sanitizedMessages: CoreMessage[] = messages.map(m => {
Expand All @@ -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: [
Expand All @@ -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'
},
{
Expand All @@ -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(
<Section title="response">
<ResolutionImage src={dataUrl} />
<BotMessage content={summaryStream.value} />
</Section>
);
Expand Down Expand Up @@ -243,7 +243,6 @@ async function submit(formData?: FormData, skip?: boolean) {

uiStream.append(answerSection);

const groupeId = nanoid();
const relatedQueries = { items: [] };

aiState.done({
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -410,7 +407,8 @@ async function submit(formData?: FormData, skip?: boolean) {
streamText,
messages,
mapProvider,
useSpecificAPI
useSpecificAPI,
drawnFeatures
)
answer = fullResponse
toolOutputs = toolResponses
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -682,7 +678,9 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
)
}
case 'related':
const relatedQueries = createStreamableValue<RelatedQueries>()
const relatedQueries = createStreamableValue<RelatedQueries>({
items: []
})
relatedQueries.done(JSON.parse(content as string))
return {
id,
Expand All @@ -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 && <ResolutionImage src={image} />}
{geoJson && (
<GeoJsonLayer id={id} data={geoJson} />
)}
Expand All @@ -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: <MapQueryHandler toolOutput={toolOutput} />,
component: (
<>
{mapUrl && (
<ResolutionImage
src={mapUrl}
className="mb-0"
alt={placeName ? `Map of ${placeName}` : 'Map Preview'}
/>
)}
<MapQueryHandler toolOutput={toolOutput} />
</>
),
isCollapsed: false
}
}

const searchResults = createStreamableValue()
const searchResults = createStreamableValue(
JSON.stringify(toolOutput)
)
searchResults.done(JSON.stringify(toolOutput))
switch (name) {
case 'search':
Expand Down
52 changes: 30 additions & 22 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -70,28 +73,33 @@ export default function RootLayout({
)}
>
<CalendarToggleProvider>
<MapToggleProvider>
<ProfileToggleProvider>
<ThemeProvider
attribute="class"
defaultTheme="earth"
enableSystem
disableTransitionOnChange
themes={['light', 'dark', 'earth']}
>
<MapContextProvider>
<MapLoadingProvider>
<Header />
<ConditionalLottie />
{children}
<Sidebar />
<Footer />
<Toaster />
</MapLoadingProvider>
</MapContextProvider>
</ThemeProvider>
</ProfileToggleProvider>
</MapToggleProvider>
<HistoryToggleProvider>
<MapToggleProvider>
<ProfileToggleProvider>
<UsageToggleProvider>
<ThemeProvider
attribute="class"
defaultTheme="earth"
enableSystem
disableTransitionOnChange
themes={['light', 'dark', 'earth']}
>
<MapContextProvider>
<MapLoadingProvider>
<Header />
<ConditionalLottie />
{children}
<Sidebar />
<HistorySidebar />
<Footer />
<Toaster />
</MapLoadingProvider>
</MapContextProvider>
</ThemeProvider>
</UsageToggleProvider>
</ProfileToggleProvider>
</MapToggleProvider>
</HistoryToggleProvider>
</CalendarToggleProvider>
<Analytics />
<SpeedInsights />
Expand Down
8 changes: 4 additions & 4 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { useEffect, useState, useRef, ChangeEvent, forwardRef, useImperativeHandle, useCallback } from 'react'
import type { AI, UIState } from '@/app/actions'
import { useUIState, useActions, readStreamableValue } from 'ai/rsc'
// Removed import of useGeospatialToolMcp as it's no longer used/available
import { cn } from '@/lib/utils'
import { UserMessage } from './user-message'
import { Button } from './ui/button'
Expand Down Expand Up @@ -31,7 +30,6 @@ export interface ChatPanelRef {
export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, input, setInput, onSuggestionsChange }, ref) => {
const [, setMessages] = useUIState<typeof AI>()
const { submit, clearChat } = useActions()
// Removed mcp instance as it's no longer passed to submit
const { mapProvider } = useSettingsStore()
const [isMobile, setIsMobile] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
Expand Down Expand Up @@ -117,6 +115,9 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
formData.append('file', selectedFile)
}

// Include drawn features in the form data
formData.append('drawnFeatures', JSON.stringify(mapData.drawnFeatures || []))

setInput('')
clearAttachment()

Expand Down Expand Up @@ -153,7 +154,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
}
}, 500) // 500ms debounce delay
},
[mapData]
[mapData, setSuggestions]
)

useEffect(() => {
Expand Down Expand Up @@ -288,7 +289,6 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
>
<ArrowRight size={isMobile ? 18 : 20} />
</Button>
{/* Suggestions are now handled by the parent component (chat.tsx) as an overlay */}
</div>
</form>
{selectedFile && (
Expand Down
Loading