Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 300 additions & 0 deletions actions_part1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
import {
StreamableValue,
createAI,
createStreamableUI,
createStreamableValue,
getAIState,
getMutableAIState
} from 'ai/rsc'
import { CoreMessage, ToolResultPart } from 'ai'
import { nanoid } from '@/lib/utils'
import type { FeatureCollection } from 'geojson'
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'
import { writer } from '@/lib/agents/writer'
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 { ResolutionCarousel } from '@/components/resolution-carousel'
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'

// Define the type for related queries
type RelatedQueries = {
items: { query: string }[]
}

async function submit(formData?: FormData, skip?: boolean) {
'use server'

const aiState = getMutableAIState<typeof AI>()
const uiStream = createStreamableUI()
const isGenerating = createStreamableValue(true)
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_mapbox = formData?.get('file_mapbox') as File;
const file_google = formData?.get('file_google') as File;
const file = (formData?.get('file') as File) || file_mapbox || file_google;
const timezone = (formData?.get('timezone') as string) || 'UTC';
const lat = formData?.get('latitude') ? parseFloat(formData.get('latitude') as string) : undefined;
const lng = formData?.get('longitude') ? parseFloat(formData.get('longitude') as string) : undefined;
const location = (lat !== undefined && lng !== undefined) ? { lat, lng } : undefined;

if (!file) {
throw new Error('No file provided for resolution search.');
}

const mapboxBuffer = file_mapbox ? await file_mapbox.arrayBuffer() : null;
const mapboxDataUrl = mapboxBuffer ? `data:${file_mapbox.type};base64,${Buffer.from(mapboxBuffer).toString('base64')}` : null;

const googleBuffer = file_google ? await file_google.arrayBuffer() : null;
const googleDataUrl = googleBuffer ? `data:${file_google.type};base64,${Buffer.from(googleBuffer).toString('base64')}` : null;

const buffer = await file.arrayBuffer();
const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`;

const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter(
(message: any) =>
message.role !== 'tool' &&
message.type !== 'followup' &&
message.type !== 'related' &&
message.type !== 'end' &&
message.type !== 'resolution_search_result'
);

const userInput = 'Analyze this map view.';
const content: CoreMessage['content'] = [
{ type: 'text', text: userInput },
{ type: 'image', image: dataUrl, mimeType: file.type }
];

aiState.update({
...aiState.get(),
messages: [
...aiState.get().messages,
{ id: nanoid(), role: 'user', content, type: 'input' }
]
});
messages.push({ role: 'user', content });

const summaryStream = createStreamableValue<string>('Analyzing map view...');
const groupeId = nanoid();

async function processResolutionSearch() {
try {
const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location);

let fullSummary = '';
for await (const partialObject of streamResult.partialObjectStream) {
if (partialObject.summary) {
fullSummary = partialObject.summary;
summaryStream.update(fullSummary);
}
}

const analysisResult = await streamResult.object;
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: any) => {
if (Array.isArray(m.content)) {
return {
...m,
content: m.content.filter((part: any) => part.type !== 'image')
} as CoreMessage
}
return m
})

const currentMessages = aiState.get().messages;
const sanitizedHistory = currentMessages.map((m: any) => {
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(
<Section title="Follow-up">
<FollowupPanel />
</Section>
);


aiState.done({
...aiState.get(),
messages: [
...aiState.get().messages,
{
id: groupeId,
role: 'assistant',
content: analysisResult.summary || 'Analysis complete.',
type: 'response'
},
{
id: groupeId,
role: 'assistant',
content: JSON.stringify({
...analysisResult,
image: dataUrl,
mapboxImage: mapboxDataUrl,
googleImage: googleDataUrl
}),
type: 'resolution_search_result'
},
{
id: groupeId,
role: 'assistant',
content: JSON.stringify(relatedQueries),
type: 'related'
},
{
id: groupeId,
role: 'assistant',
content: 'followup',
type: 'followup'
}
]
});
} catch (error) {
console.error('Error in resolution search:', error);
summaryStream.error(error);
} finally {
isGenerating.done(false);
uiStream.done();
}
}

processResolutionSearch();

uiStream.update(
<Section title="response">
<ResolutionCarousel
mapboxImage={mapboxDataUrl || undefined}
googleImage={googleDataUrl || undefined}
initialImage={dataUrl}
/>
<BotMessage content={summaryStream.value} />
</Section>
);

return {
id: nanoid(),
isGenerating: isGenerating.value,
component: uiStream.value,
isCollapsed: isCollapsed.value
};
}

const file = !skip ? (formData?.get('file') as File) : undefined
const userInput = skip
? `{"action": "skip"}`
: ((formData?.get('related_query') as string) ||
(formData?.get('input') as string))

if (userInput && (userInput.toLowerCase().trim() === 'what is a planet computer?' || userInput.toLowerCase().trim() === 'what is qcx-terra?')) {
const definition = userInput.toLowerCase().trim() === 'what is a planet computer?'
? `A planet computer is a proprietary environment aware system that interoperates weather forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet. Available for our Pro and Enterprise customers. [QCX Pricing](https://www.queue.cx/#pricing)`
: `QCX-Terra is a model garden of pixel level precision geospatial foundational models for efficient land feature predictions from satellite imagery. Available for our Pro and Enterprise customers. [QCX Pricing] (https://www.queue.cx/#pricing)`;

const content = JSON.stringify(Object.fromEntries(formData!));
const type = 'input';
const groupeId = nanoid();

aiState.update({
...aiState.get(),
messages: [
...aiState.get().messages,
{
id: nanoid(),
role: 'user',
content,
type,
},
],
});

const definitionStream = createStreamableValue();
definitionStream.done(definition);

const answerSection = (
<Section title="response">
<BotMessage content={definitionStream.value} />
</Section>
);

uiStream.update(answerSection);

const relatedQueries = { items: [] };

aiState.done({
...aiState.get(),
messages: [
...aiState.get().messages,
{
id: groupeId,
role: 'assistant',
content: definition,
type: 'response',
},
{
id: groupeId,
role: 'assistant',
content: JSON.stringify(relatedQueries),
type: 'related',
},
{
id: groupeId,
role: 'assistant',
content: 'followup',
type: 'followup',
},
],
});

isGenerating.done(false);
uiStream.done();

return {
id: nanoid(),
isGenerating: isGenerating.value,
component: uiStream.value,
isCollapsed: isCollapsed.value
};
}

if (!userInput && !file) {
Loading