From 8a5ba98cb0742931df4d797765301f0eb24ecd1a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 13:38:19 +0000 Subject: [PATCH 1/3] fix: followup panel routing and stream error handling Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/actions.tsx | 252 +++++++++++++++++---------------- components/followup-panel.tsx | 22 +-- lib/agents/query-suggestor.tsx | 62 ++++---- 3 files changed, 176 insertions(+), 160 deletions(-) diff --git a/app/actions.tsx b/app/actions.tsx index 50e985bf..5cb3e0d1 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -402,139 +402,143 @@ async function submit(formData?: FormData, skip?: boolean) { const mapProvider = formData?.get('mapProvider') as 'mapbox' | 'google' async function processEvents() { - let action: any = { object: { next: 'proceed' } } - if (!skip) { - const taskManagerResult = await taskManager(messages) - if (taskManagerResult) { - action.object = taskManagerResult.object + try { + let action: any = { object: { next: 'proceed' } } + if (!skip) { + const taskManagerResult = await taskManager(messages) + if (taskManagerResult) { + action.object = taskManagerResult.object + } } - } - if (action.object.next === 'inquire') { - const inquiry = await inquire(uiStream, messages) - uiStream.done() - isGenerating.done() - isCollapsed.done(false) - aiState.done({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { - id: nanoid(), - role: 'assistant', - content: `inquiry: ${inquiry?.question}` - } - ] - }) - return - } + if (action.object.next === 'inquire') { + const inquiry = await inquire(uiStream, messages) + uiStream.done() + isGenerating.done() + isCollapsed.done(false) + aiState.done({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: nanoid(), + role: 'assistant', + content: `inquiry: ${inquiry?.question}` + } + ] + }) + return + } - isCollapsed.done(true) - let answer = '' - let toolOutputs: ToolResultPart[] = [] - let errorOccurred = false - const streamText = createStreamableValue() - uiStream.update() - - while ( - useSpecificAPI - ? answer.length === 0 - : answer.length === 0 && !errorOccurred - ) { - const { fullResponse, hasError, toolResponses } = await researcher( - currentSystemPrompt, - uiStream, - streamText, - messages, - mapProvider, - useSpecificAPI, - drawnFeatures - ) - answer = fullResponse - toolOutputs = toolResponses - errorOccurred = hasError - - if (toolOutputs.length > 0) { - toolOutputs.map(output => { - aiState.update({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { - id: groupeId, - role: 'tool', - content: JSON.stringify(output.result), - name: output.toolName, - type: 'tool' - } - ] + isCollapsed.done(true) + let answer = '' + let toolOutputs: ToolResultPart[] = [] + let errorOccurred = false + const streamText = createStreamableValue() + uiStream.update() + + while ( + useSpecificAPI + ? answer.length === 0 + : answer.length === 0 && !errorOccurred + ) { + const { fullResponse, hasError, toolResponses } = await researcher( + currentSystemPrompt, + uiStream, + streamText, + messages, + mapProvider, + useSpecificAPI, + drawnFeatures + ) + answer = fullResponse + toolOutputs = toolResponses + errorOccurred = hasError + + if (toolOutputs.length > 0) { + toolOutputs.map(output => { + aiState.update({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: groupeId, + role: 'tool', + content: JSON.stringify(output.result), + name: output.toolName, + type: 'tool' + } + ] + }) }) - }) + } } - } - if (useSpecificAPI && answer.length === 0) { - const modifiedMessages = aiState - .get() - .messages.map(msg => - msg.role === 'tool' - ? { - ...msg, - role: 'assistant', - content: JSON.stringify(msg.content), - type: 'tool' - } - : msg - ) as CoreMessage[] - const latestMessages = modifiedMessages.slice(maxMessages * -1) - answer = await writer( - currentSystemPrompt, - uiStream, - streamText, - latestMessages - ) - } else { - streamText.done() - } + if (useSpecificAPI && answer.length === 0) { + const modifiedMessages = aiState + .get() + .messages.map(msg => + msg.role === 'tool' + ? { + ...msg, + role: 'assistant', + content: JSON.stringify(msg.content), + type: 'tool' + } + : msg + ) as CoreMessage[] + const latestMessages = modifiedMessages.slice(maxMessages * -1) + answer = await writer( + currentSystemPrompt, + uiStream, + streamText, + latestMessages + ) + } else { + streamText.done() + } - if (!errorOccurred) { - const relatedQueries = await querySuggestor(uiStream, messages) - uiStream.append( -
- -
- ) + if (!errorOccurred) { + const relatedQueries = await querySuggestor(uiStream, messages) + uiStream.append( +
+ +
+ ) - await new Promise(resolve => setTimeout(resolve, 500)) - - aiState.done({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { - id: groupeId, - role: 'assistant', - content: answer, - type: 'response' - }, - { - id: groupeId, - role: 'assistant', - content: JSON.stringify(relatedQueries), - type: 'related' - }, - { - id: groupeId, - role: 'assistant', - content: 'followup', - type: 'followup' - } - ] - }) - } + await new Promise(resolve => setTimeout(resolve, 500)) - isGenerating.done(false) - uiStream.done() + aiState.done({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: groupeId, + role: 'assistant', + content: answer, + type: 'response' + }, + { + id: groupeId, + role: 'assistant', + content: JSON.stringify(relatedQueries), + type: 'related' + }, + { + id: groupeId, + role: 'assistant', + content: 'followup', + type: 'followup' + } + ] + }) + } + } catch (error) { + console.error('Error in processEvents:', error) + } finally { + isGenerating.done(false) + uiStream.done() + } } processEvents() diff --git a/components/followup-panel.tsx b/components/followup-panel.tsx index c57f141f..7b111a9b 100644 --- a/components/followup-panel.tsx +++ b/components/followup-panel.tsx @@ -19,8 +19,7 @@ export function FollowupPanel() { const handleSubmit = async (event: React.FormEvent) => { event.preventDefault() const formData = new FormData() - formData.append("input", input) - formData.append("action", "resolution_search") + formData.append('input', input) const userMessage = { id: nanoid(), @@ -31,14 +30,17 @@ export function FollowupPanel() { // Include drawn features in the form data formData.append('drawnFeatures', JSON.stringify(mapData.drawnFeatures || [])) - const responseMessage = await submit(formData) - setMessages(currentMessages => [ - ...currentMessages, - userMessage, - responseMessage - ]) - - setInput('') + try { + const responseMessage = await submit(formData) + setMessages(currentMessages => [ + ...currentMessages, + userMessage, + responseMessage + ]) + setInput('') + } catch (error) { + console.error('Error submitting followup:', error) + } } return ( diff --git a/lib/agents/query-suggestor.tsx b/lib/agents/query-suggestor.tsx index 7cb8e50c..b4f05e9c 100644 --- a/lib/agents/query-suggestor.tsx +++ b/lib/agents/query-suggestor.tsx @@ -52,40 +52,50 @@ export async function querySuggestor( ) - let finalRelatedQueries: PartialRelated = {} + let finalRelatedQueries: PartialRelated = { items: [] } - // OPTIMIZATION: Use a more concise system prompt to reduce token usage - const result = await streamObject({ - model: (await getModel()) as LanguageModel, - system: `Generate 3 follow-up queries that explore the subject matter deeper. Format as JSON with an "items" array containing objects with "query" fields. Keep queries concise and relevant.`, - messages, - schema: relatedSchema, - temperature: 0.7, // Lower temperature for more consistent results - }) + try { + // OPTIMIZATION: Use a more concise system prompt to reduce token usage + const result = await streamObject({ + model: (await getModel()) as LanguageModel, + system: `Generate 3 follow-up queries that explore the subject matter deeper. Format as JSON with an "items" array containing objects with "query" fields. Keep queries concise and relevant.`, + messages, + schema: relatedSchema, + temperature: 0.7, // Lower temperature for more consistent results + }) - // OPTIMIZATION: Stream updates but batch them to reduce re-render frequency - let lastUpdateTime = Date.now(); - const UPDATE_THROTTLE = 200; // ms + // OPTIMIZATION: Stream updates but batch them to reduce re-render frequency + let lastUpdateTime = Date.now(); + const UPDATE_THROTTLE = 100; // Reduced from 200ms to 100ms for better responsiveness - for await (const obj of result.partialObjectStream) { - if (obj && typeof obj === 'object' && 'items' in obj) { - const now = Date.now(); - // Only update UI if enough time has passed since last update - if (now - lastUpdateTime > UPDATE_THROTTLE) { - objectStream.update(obj as PartialRelated) - lastUpdateTime = now; + for await (const obj of result.partialObjectStream) { + if (obj && typeof obj === 'object' && 'items' in obj) { + const now = Date.now(); + // Only update UI if enough time has passed since last update + if (now - lastUpdateTime > UPDATE_THROTTLE) { + objectStream.update(obj as PartialRelated) + lastUpdateTime = now; + } + finalRelatedQueries = obj as PartialRelated } - finalRelatedQueries = obj as PartialRelated } + } catch (error) { + console.error('Error in querySuggestor:', error) + } finally { + // Ensure final state is pushed even if throttled + if (finalRelatedQueries.items && finalRelatedQueries.items.length > 0) { + objectStream.update(finalRelatedQueries) + } + objectStream.done() } - - objectStream.done() // OPTIMIZATION: Cache the result - queryCache.set(cacheKey, { - data: finalRelatedQueries, - timestamp: Date.now() - }); + if (finalRelatedQueries.items && finalRelatedQueries.items.length > 0) { + queryCache.set(cacheKey, { + data: finalRelatedQueries, + timestamp: Date.now() + }); + } // OPTIMIZATION: Limit cache size to prevent memory issues if (queryCache.size > 50) { From 7ab4e939f37d875339e4717ef7d66f93768c15a4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 13:55:19 +0000 Subject: [PATCH 2/3] perf: remove artificial delays in server actions Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/actions.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/actions.tsx b/app/actions.tsx index 5cb3e0d1..82f2695b 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -154,7 +154,6 @@ async function submit(formData?: FormData, skip?: boolean) { ); - await new Promise(resolve => setTimeout(resolve, 500)); aiState.done({ ...aiState.get(), @@ -506,7 +505,6 @@ async function submit(formData?: FormData, skip?: boolean) { ) - await new Promise(resolve => setTimeout(resolve, 500)) aiState.done({ ...aiState.get(), From 64e4c2d5e6666494e151845c5bb97ee9e808706f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 14:24:06 +0000 Subject: [PATCH 3/3] fix: resolution search output and reliable streaming Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- actions_part1.tsx | 300 ++++++++++++++++++++++++++++++++++++++++++++++ actions_part2.tsx | 300 ++++++++++++++++++++++++++++++++++++++++++++++ app/actions.tsx | 22 ++-- app_actions.patch | 77 ++++++++++++ 4 files changed, 689 insertions(+), 10 deletions(-) create mode 100644 actions_part1.tsx create mode 100644 actions_part2.tsx create mode 100644 app_actions.patch diff --git a/actions_part1.tsx b/actions_part1.tsx new file mode 100644 index 00000000..45ff11e2 --- /dev/null +++ b/actions_part1.tsx @@ -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() + 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('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( + + ); + } + + 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( +
+ +
+ ); + + + 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( +
+ + +
+ ); + + 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 = ( +
+ +
+ ); + + 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) { diff --git a/actions_part2.tsx b/actions_part2.tsx new file mode 100644 index 00000000..36a57dc2 --- /dev/null +++ b/actions_part2.tsx @@ -0,0 +1,300 @@ + isGenerating.done(false) + return { + id: nanoid(), + isGenerating: isGenerating.value, + component: null, + isCollapsed: isCollapsed.value + } + } + + 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' + ).map((m: any) => { + 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' + const maxMessages = useSpecificAPI ? 5 : 10 + messages.splice(0, Math.max(messages.length - maxMessages, 0)) + + const messageParts: { + type: 'text' | 'image' + text?: string + image?: string + mimeType?: string + }[] = [] + + if (userInput) { + messageParts.push({ type: 'text', text: userInput }) + } + + if (file) { + const buffer = await file.arrayBuffer() + if (file.type.startsWith('image/')) { + const dataUrl = `data:${file.type};base64,${Buffer.from( + buffer + ).toString('base64')}` + messageParts.push({ + type: 'image', + image: dataUrl, + mimeType: file.type + }) + } else if (file.type === 'text/plain') { + const textContent = Buffer.from(buffer).toString('utf-8') + const existingTextPart = messageParts.find(p => p.type === 'text') + if (existingTextPart) { + existingTextPart.text = `${textContent}\n\n${existingTextPart.text}` + } else { + messageParts.push({ type: 'text', text: textContent }) + } + } + } + + const hasImage = messageParts.some(part => part.type === 'image') + const content: CoreMessage['content'] = hasImage + ? messageParts as CoreMessage['content'] + : messageParts.map(part => part.text).join('\n') + + const type = skip + ? undefined + : formData?.has('input') || formData?.has('file') + ? 'input' + : formData?.has('related_query') + ? 'input_related' + : 'inquiry' + + if (content) { + aiState.update({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: nanoid(), + role: 'user', + content, + type + } + ] + }) + messages.push({ + role: 'user', + content + } as CoreMessage) + } + + const userId = 'anonymous' + const currentSystemPrompt = (await getSystemPrompt(userId)) || '' + const mapProvider = formData?.get('mapProvider') as 'mapbox' | 'google' + + async function processEvents() { + try { + let action: any = { object: { next: 'proceed' } } + if (!skip) { + const taskManagerResult = await taskManager(messages) + if (taskManagerResult) { + action.object = taskManagerResult.object + } + } + + if (action.object.next === 'inquire') { + const inquiry = await inquire(uiStream, messages) + uiStream.done() + isGenerating.done() + isCollapsed.done(false) + aiState.done({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: nanoid(), + role: 'assistant', + content: `inquiry: ${inquiry?.question}` + } + ] + }) + return + } + + isCollapsed.done(true) + let answer = '' + let toolOutputs: ToolResultPart[] = [] + let errorOccurred = false + const streamText = createStreamableValue() + uiStream.update() + + while ( + useSpecificAPI + ? answer.length === 0 + : answer.length === 0 && !errorOccurred + ) { + const { fullResponse, hasError, toolResponses } = await researcher( + currentSystemPrompt, + uiStream, + streamText, + messages, + mapProvider, + useSpecificAPI, + drawnFeatures + ) + answer = fullResponse + toolOutputs = toolResponses + errorOccurred = hasError + + if (toolOutputs.length > 0) { + toolOutputs.map(output => { + aiState.update({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: groupeId, + role: 'tool', + content: JSON.stringify(output.result), + name: output.toolName, + type: 'tool' + } + ] + }) + }) + } + } + + if (useSpecificAPI && answer.length === 0) { + const modifiedMessages = aiState + .get() + .messages.map(msg => + msg.role === 'tool' + ? { + ...msg, + role: 'assistant', + content: JSON.stringify(msg.content), + type: 'tool' + } + : msg + ) as CoreMessage[] + const latestMessages = modifiedMessages.slice(maxMessages * -1) + answer = await writer( + currentSystemPrompt, + uiStream, + streamText, + latestMessages + ) + } else { + streamText.done() + } + + if (!errorOccurred) { + const relatedQueries = await querySuggestor(uiStream, messages) + uiStream.append( +
+ +
+ ) + + + aiState.done({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: groupeId, + role: 'assistant', + content: answer, + type: 'response' + }, + { + id: groupeId, + role: 'assistant', + content: JSON.stringify(relatedQueries), + type: 'related' + }, + { + id: groupeId, + role: 'assistant', + content: 'followup', + type: 'followup' + } + ] + }) + } + } catch (error) { + console.error('Error in processEvents:', error) + } finally { + isGenerating.done(false) + uiStream.done() + } + } + + processEvents() + + return { + id: nanoid(), + isGenerating: isGenerating.value, + component: uiStream.value, + isCollapsed: isCollapsed.value + } +} + +async function clearChat() { + 'use server' + + const aiState = getMutableAIState() + + aiState.done({ + chatId: nanoid(), + messages: [] + }) +} + +export type AIState = { + messages: AIMessage[] + chatId: string + isSharePage?: boolean +} + +export type UIState = { + id: string + component: React.ReactNode + isGenerating?: StreamableValue + isCollapsed?: StreamableValue +}[] + +const initialAIState: AIState = { + chatId: nanoid(), + messages: [] +} + +const initialUIState: UIState = [] + +export const AI = createAI({ + actions: { + submit, + clearChat + }, + initialUIState, + initialAIState, + onGetUIState: async () => { + 'use server' + + const aiState = getAIState() as AIState + if (aiState) { + const uiState = getUIStateFromAIState(aiState) + return uiState + } + return initialUIState + }, + onSetAIState: async ({ state }) => { diff --git a/app/actions.tsx b/app/actions.tsx index 82f2695b..2b782917 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -100,6 +100,16 @@ async function submit(formData?: FormData, skip?: boolean) { const groupeId = nanoid(); async function processResolutionSearch() { + uiStream.update( +
+ + +
+ ); try { const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location); @@ -194,6 +204,7 @@ async function submit(formData?: FormData, skip?: boolean) { console.error('Error in resolution search:', error); summaryStream.error(error); } finally { + await new Promise(resolve => setTimeout(resolve, 100)); isGenerating.done(false); uiStream.done(); } @@ -201,16 +212,6 @@ async function submit(formData?: FormData, skip?: boolean) { processResolutionSearch(); - uiStream.update( -
- - -
- ); return { id: nanoid(), @@ -534,6 +535,7 @@ async function submit(formData?: FormData, skip?: boolean) { } catch (error) { console.error('Error in processEvents:', error) } finally { + await new Promise(resolve => setTimeout(resolve, 100)); isGenerating.done(false) uiStream.done() } diff --git a/app_actions.patch b/app_actions.patch new file mode 100644 index 00000000..8ac169fe --- /dev/null +++ b/app_actions.patch @@ -0,0 +1,77 @@ +<<<<<<< SEARCH + async function processResolutionSearch() { + try { + const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location); +======= + async function processResolutionSearch() { + try { + uiStream.update( +
+ + +
+ ); + + const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location); +>>>>>>> REPLACE +<<<<<<< SEARCH + } catch (error) { + console.error('Error in resolution search:', error); + summaryStream.error(error); + } finally { + isGenerating.done(false); + uiStream.done(); + } + } + + processResolutionSearch(); + + uiStream.update( +
+ + +
+ ); + + return { +======= + } catch (error) { + console.error('Error in resolution search:', error); + summaryStream.error(error); + } finally { + await new Promise(resolve => setTimeout(resolve, 100)); + isGenerating.done(false); + uiStream.done(); + } + } + + processResolutionSearch(); + + return { +>>>>>>> REPLACE +<<<<<<< SEARCH + } catch (error) { + console.error('Error in processEvents:', error) + } finally { + isGenerating.done(false) + uiStream.done() + } + } +======= + } catch (error) { + console.error('Error in processEvents:', error) + } finally { + await new Promise(resolve => setTimeout(resolve, 100)); + isGenerating.done(false) + uiStream.done() + } + } +>>>>>>> REPLACE