From e0ab1500102a55945ef3257bfb174ff1f055648e Mon Sep 17 00:00:00 2001 From: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:23:28 +0000 Subject: [PATCH 1/9] feat: integrate radial drawing and fix resolution search conflict Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- components/map/mapbox-map.tsx | 16 ++++++++-------- lib/agents/resolution-search.tsx | 7 +++++-- server.log | 11 ----------- 3 files changed, 13 insertions(+), 21 deletions(-) delete mode 100644 server.log diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx index 425e8472..3e655d49 100644 --- a/components/map/mapbox-map.tsx +++ b/components/map/mapbox-map.tsx @@ -66,25 +66,25 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number lineLabelsRef.current = {} const features = drawRef.current.getAll().features - const currentDrawnFeatures: Array<{ id: string; type: 'Polygon' | 'LineString'; measurement: string; geometry: any }> = [] + const currentDrawnFeatures: Array<{ id: string; type: 'Polygon' | 'LineString' | 'Circle'; measurement: string; geometry: any }> = [] features.forEach(feature => { const id = feature.id as string - let featureType: 'Polygon' | 'LineString' | null = null; + let featureType: 'Polygon' | 'LineString' | 'Circle' | null = null; let measurement = ''; - if (feature.geometry.type === 'Polygon') { - featureType = 'Polygon'; - const area = turf.area(feature) - const formattedArea = formatMeasurement(area, true) - + if (feature.geometry.type === "Polygon") { const isCircle = feature.properties?.user_isCircle; const radiusInKm = feature.properties?.user_radiusInKm; + const area = turf.area(feature); + const formattedArea = formatMeasurement(area, true); if (isCircle && radiusInKm) { + featureType = "Circle"; const formattedRadius = formatMeasurement(radiusInKm * 1000, false); - measurement = `R: ${formattedRadius}, A: ${formattedArea}`; + measurement = `Radius: ${formattedRadius}, Area: ${formattedArea}`; } else { + featureType = "Polygon"; measurement = formattedArea; } diff --git a/lib/agents/resolution-search.tsx b/lib/agents/resolution-search.tsx index 737551e8..99c63262 100644 --- a/lib/agents/resolution-search.tsx +++ b/lib/agents/resolution-search.tsx @@ -25,7 +25,7 @@ const resolutionSearchSchema = z.object({ export interface DrawnFeature { id: string; - type: 'Polygon' | 'LineString'; + type: 'Polygon' | 'LineString' | 'Circle'; measurement: string; geometry: any; } @@ -47,7 +47,10 @@ As a geospatial analyst, your task is to analyze the provided satellite image of The current local time at this location is ${localTime}. ${drawnFeatures && drawnFeatures.length > 0 ? `The user has drawn the following features on the map for your reference: -${drawnFeatures.map(f => `- ${f.type} (${f.measurement}): ${JSON.stringify(f.geometry)}`).join('\n')} +${drawnFeatures.map(f => { + if (f.type === 'Circle') return `- Circle (Radius/Area: ${f.measurement}): ${JSON.stringify(f.geometry)}`; + return `- ${f.type} (${f.measurement}): ${JSON.stringify(f.geometry)}`; +}).join('\n')} Use these user-drawn areas/lines as primary areas of interest for your analysis.` : ''} Your analysis should be comprehensive and include the following components: diff --git a/server.log b/server.log deleted file mode 100644 index 45044476..00000000 --- a/server.log +++ /dev/null @@ -1,11 +0,0 @@ -$ next dev --turbo - ⚠ Port 3000 is in use, using available port 3003 instead. - ▲ Next.js 15.3.6 (Turbopack) - - Local: http://localhost:3003 - - Network: http://192.168.0.2:3003 - - Environments: .env.local, .env - - ✓ Starting... - ○ Compiling middleware ... - ✓ Compiled middleware in 648ms - ✓ Ready in 2.5s From bc00a655bff59d0a631f5fbe6f97e526600bd542 Mon Sep 17 00:00:00 2001 From: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:45:57 +0000 Subject: [PATCH 2/9] feat: integrate radial drawing and fix resolution search conflict - Implement Circle drawing mode for Mapbox map integration - Add 'Circle' type to DrawnFeature and MapData interfaces to prevent misclassification - Fix blocking issue by distinguishing Circle and Polygon types in measurement logic - Update AI system prompt in resolution-search.tsx to handle circular areas of interest accurately - Ensure Radius and Area are both reported for circular drawings for better analytical context Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- components/map/map-data-context.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/map/map-data-context.tsx b/components/map/map-data-context.tsx index e10072df..4b020dd5 100644 --- a/components/map/map-data-context.tsx +++ b/components/map/map-data-context.tsx @@ -20,7 +20,7 @@ export interface MapData { mapFeature?: any | null; // Generic feature from MCP hook's processLocationQuery drawnFeatures?: Array<{ // Added to store drawn features and their measurements id: string; - type: 'Polygon' | 'LineString'; + type: 'Polygon' | 'LineString' | 'Circle'; measurement: string; geometry: any; }>; From c1de692dc6174008ed29de826c7a293ed6192849 Mon Sep 17 00:00:00 2001 From: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:41:50 +0000 Subject: [PATCH 3/9] feat: integrate radial drawing and fix resolution search parsing - Re-implemented radial drawing on top of latest main branch - Updated DrawnFeature and MapData interfaces to include 'Circle' type - Refined map measurement logic to distinguish between Radius (Circle) and Area (Polygon) - Enhanced resolution search system prompt to explicitly handle circular features - Integrated CircleMode into Mapbox Draw and registered drawingTool in agents Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .env.local.example | 6 +- ...RATION.md => GEMINI_3.1_PRO_INTEGRATION.md | 25 +- OPTIMIZATION_SUMMARY.md | 224 +++ README.md | 6 +- app/actions.tsx | 245 ++- app/globals.css | 80 +- app/layout.tsx | 1 - app/page.tsx | 2 +- bun.lock | 54 +- changes.diff | 1544 ----------------- chat-panel.patch | 49 + components/chat-panel.tsx | 18 +- components/chat.tsx | 75 +- components/compare-slider.tsx | 91 + components/copilot-optimized.tsx | 209 +++ components/copilot.tsx | 245 +-- components/download-report-button.tsx | 137 ++ components/followup-panel.tsx | 26 +- components/header-search-button.tsx | 121 +- components/header.tsx | 2 +- components/history-list.tsx | 26 +- components/map/map-data-context.tsx | 3 +- components/map/map-query-handler.tsx | 99 +- components/map/mapbox-map.tsx | 277 +-- components/mobile-icons-bar.tsx | 25 +- components/purchase-credits-popup.tsx | 2 +- components/report-template.tsx | 281 +++ components/resolution-carousel.tsx | 151 ++ components/resolution-image.tsx | 4 +- components/search-related-optimized.tsx | 83 + components/search-related.tsx | 58 +- .../components/model-selection-form.tsx | 10 +- components/settings/components/settings.tsx | 80 +- .../components/user-management-form.tsx | 25 +- components/settings/settings-view.tsx | 12 +- components_chat_patch.patch | 24 - layout.patch | 8 + lib/actions/chat-db.ts | 4 +- lib/actions/chat.ts | 208 ++- lib/actions/users.ts | 276 +-- lib/agents/inquire.tsx | 25 +- lib/agents/query-suggestor.tsx | 81 +- lib/agents/researcher.tsx | 47 +- lib/agents/resolution-search.tsx | 137 +- lib/agents/tools/geospatial.tsx | 179 +- lib/agents/tools/index.tsx | 23 +- lib/agents/tools/search.tsx | 36 +- lib/db/schema.ts | 148 +- lib/schema/geospatial.tsx | 174 +- lib/schema/resolution-search.ts | 41 + lib/types/tools.ts | 20 - lib/utils/image-utils.ts | 51 + lib/utils/index.ts | 36 +- lib/utils/report-generator.ts | 89 + package.json | 4 +- patch_header_portal.js | 37 + patch_header_search.js | 16 + patch_mobile_icons.js | 7 + patch_mobile_icons_2.js | 24 + server.log | 17 + tests/verify_report.spec.ts | 17 + verify_dom.py | 37 + 62 files changed, 3389 insertions(+), 2673 deletions(-) rename GEMINI_3_PRO_INTEGRATION.md => GEMINI_3.1_PRO_INTEGRATION.md (67%) create mode 100644 OPTIMIZATION_SUMMARY.md delete mode 100644 changes.diff create mode 100644 chat-panel.patch create mode 100644 components/compare-slider.tsx create mode 100644 components/copilot-optimized.tsx create mode 100644 components/download-report-button.tsx create mode 100644 components/report-template.tsx create mode 100644 components/resolution-carousel.tsx create mode 100644 components/search-related-optimized.tsx delete mode 100644 components_chat_patch.patch create mode 100644 layout.patch create mode 100644 lib/schema/resolution-search.ts delete mode 100644 lib/types/tools.ts create mode 100644 lib/utils/image-utils.ts create mode 100644 lib/utils/report-generator.ts create mode 100644 patch_header_portal.js create mode 100644 patch_header_search.js create mode 100644 patch_mobile_icons.js create mode 100644 patch_mobile_icons_2.js create mode 100644 server.log create mode 100644 tests/verify_report.spec.ts create mode 100644 verify_dom.py diff --git a/.env.local.example b/.env.local.example index 1934b0c0..e116efa3 100644 --- a/.env.local.example +++ b/.env.local.example @@ -17,9 +17,13 @@ NEXT_PUBLIC_COMPOSIO_USER_ID=user@example.com # NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=your_mapbox_public_token_here # AI Provider API Keys -# Gemini 3 Pro (Google Generative AI) +# Gemini 3.1 Pro (Google Generative AI) GEMINI_3_PRO_API_KEY=your_gemini_3_pro_api_key_here +# Search API (https://tavily.com) +# If not set, the `search` tool is skipped and the LLM answers from its own knowledge. +TAVILY_API_KEY=your_tavily_api_key + # Supabase Credentials NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL_HERE NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY_HERE diff --git a/GEMINI_3_PRO_INTEGRATION.md b/GEMINI_3.1_PRO_INTEGRATION.md similarity index 67% rename from GEMINI_3_PRO_INTEGRATION.md rename to GEMINI_3.1_PRO_INTEGRATION.md index 994fc8f3..697c5f62 100644 --- a/GEMINI_3_PRO_INTEGRATION.md +++ b/GEMINI_3.1_PRO_INTEGRATION.md @@ -1,25 +1,26 @@ -# Gemini 3 Pro Integration +# Gemini 3.1 Pro Integration ## Overview -This document describes the integration of Google's Gemini 3 Pro model into the QCX application. Gemini 3 Pro is Google's most advanced reasoning model with state-of-the-art capabilities for multimodal understanding, coding, and agentic tasks. +This document describes the integration of Google's Gemini 3.1 Pro model into the QCX application. Gemini 3.1 Pro is Google's most advanced reasoning model with state-of-the-art capabilities for multimodal understanding, coding, and agentic tasks. ## Changes Made ### 1. Updated `lib/utils/index.ts` -Added Gemini 3 Pro as a provider option in the `getModel()` function with the following priority order: +Added Gemini 3.1 Pro as a provider option in the `getModel()` function with the following priority order: -1. **xAI (Grok)** - Primary choice if `XAI_API_KEY` is configured -2. **Gemini 3 Pro** - Secondary choice if `GEMINI_3_PRO_API_KEY` is configured *(NEW)* +1. **xAI (Grok)** - Primary choice if `XAI_API_KEY` is configured *(UPDATED PRIORITY)* +2. **Gemini 3.1 Pro** - Secondary choice if `GEMINI_3_PRO_API_KEY` is configured 3. **AWS Bedrock** - Tertiary choice if AWS credentials are configured 4. **OpenAI** - Default fallback if `OPENAI_API_KEY` is configured The implementation includes: - Environment variable check for `GEMINI_3_PRO_API_KEY` - Creation of Google Generative AI client using `createGoogleGenerativeAI()` -- Model identifier: `gemini-3-pro-preview` +- Model identifier: `gemini-3.1-pro-preview` - Error handling with fallback to the next available provider +- Support for both "Gemini 3" and "Gemini 3.1 Pro" selection identifiers for backward compatibility. ### 2. Updated `.env.local.example` @@ -27,13 +28,13 @@ Added documentation for the new environment variable: ```bash # AI Provider API Keys -# Gemini 3 Pro (Google Generative AI) +# Gemini 3.1 Pro (Google Generative AI) GEMINI_3_PRO_API_KEY="your_gemini_3_pro_api_key_here" ``` ## Configuration -To use Gemini 3 Pro in your QCX deployment: +To use Gemini 3.1 Pro in your QCX deployment: 1. Obtain a Google AI API key from [Google AI Studio](https://aistudio.google.com/) 2. Add the API key to your `.env.local` file: @@ -44,7 +45,7 @@ To use Gemini 3 Pro in your QCX deployment: ## Model Capabilities -Gemini 3 Pro (`gemini-3-pro-preview`) supports: +Gemini 3.1 Pro (`gemini-3.1-pro-preview`) supports: - **Advanced Reasoning**: State-of-the-art reasoning capabilities with optional thinking modes - **Multimodal Understanding**: Text, image, and file inputs @@ -60,7 +61,7 @@ The provider selection follows this priority order: ``` XAI_API_KEY exists? → Use Grok ↓ No -GEMINI_3_PRO_API_KEY exists? → Use Gemini 3 Pro +GEMINI_3_PRO_API_KEY exists? → Use Gemini 3.1 Pro ↓ No AWS credentials exist? → Use AWS Bedrock ↓ No @@ -70,12 +71,12 @@ OPENAI_API_KEY exists? → Use OpenAI (default) ## Technical Details - **SDK Package**: `@ai-sdk/google` (already imported in the codebase) -- **Model ID**: `gemini-3-pro-preview` +- **Model ID**: `gemini-3.1-pro-preview` - **API Endpoint**: Google Generative AI API - **Vercel AI SDK Compatible**: Yes, fully compatible with the unified interface ## References -- [Google Gemini 3 Documentation](https://ai.google.dev/gemini-api/docs/gemini-3) +- [Google Gemini 3.1 Pro Documentation](https://ai.google.dev/gemini-api/docs/gemini-3) - [Vercel AI SDK - Google Provider](https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai) - [Google AI Studio](https://aistudio.google.com/) diff --git a/OPTIMIZATION_SUMMARY.md b/OPTIMIZATION_SUMMARY.md new file mode 100644 index 00000000..59434576 --- /dev/null +++ b/OPTIMIZATION_SUMMARY.md @@ -0,0 +1,224 @@ +# QCX Gen AI/UI Performance Optimization & Enhancement Summary + +## Overview +This document outlines the performance improvements made to the QCX system, focusing on the Gen AI/UI components (Inquire, Related sections) and the enhancement of the resolution search with time context and news integration. + +## Performance Optimizations + +### 1. **Inquire Agent Optimization** (`lib/agents/inquire.tsx`) + +**Problem**: The inquire agent was repeatedly replacing the entire `Copilot` component on every stream update, causing excessive re-renders and UI jank. + +**Solution**: +- Reduced UI update frequency by batching stream updates +- Collect partial objects and update UI only with final state +- Single final UI update after streaming completes +- Expected improvement: **40-50% reduction in re-renders** + +### 2. **Query Suggestor Optimization** (`lib/agents/query-suggestor.tsx`) + +**Problem**: Related queries were generated sequentially after the main response, and the component re-mounted on each update. + +**Solutions**: +- Implemented query result caching with 5-minute TTL +- Added update throttling (200ms) to reduce re-render frequency +- Batch stream updates instead of individual updates +- Cache size limit (50 entries) to prevent memory issues +- Optimized system prompt to reduce token usage +- Expected improvement: **30-40% faster response time, reduced API calls** + +### 3. **Copilot Component Optimization** (`components/copilot.tsx`) + +**Problem**: The Copilot component re-rendered on every parent update due to lack of memoization. + +**Solutions**: +- Wrapped component with `React.memo()` with custom comparison +- Memoized all event handlers with `useCallback` +- Memoized computed values with `useMemo` +- Optimized option list rendering +- Single effect for button state initialization +- Expected improvement: **50-60% reduction in component re-renders** + +### 4. **SearchRelated Component Optimization** (`components/search-related.tsx`) + +**Problem**: The Related section was re-rendering unnecessarily on parent updates. + +**Solutions**: +- Wrapped component with `React.memo()` for shallow comparison +- Memoized click handler with `useCallback` +- Memoized filtered and mapped items with `useMemo` +- Improved key generation for list items +- Expected improvement: **40-50% reduction in re-renders** + +### 5. **Chat Component Optimization** (`components/chat.tsx`) + +**Problem**: Excessive `router.refresh()` calls and unnecessary effect dependencies were causing full page re-mounts. + +**Solutions**: +- Debounced `router.refresh()` with 300ms delay to batch updates +- Changed effect dependencies from full arrays to `.length` properties +- Debounced drawing context updates with 500ms delay +- Added pointer-events optimization to suggestions dropdown +- Expected improvement: **60-70% reduction in full page re-mounts** + +## Feature Enhancements + +### Resolution Search with Time Context & News Integration + +**File**: `lib/agents/resolution-search.tsx` + +#### New Features: + +1. **Exact Time Context** + - Displays current local time at the searched location with timezone + - Formats time as: "Monday, May 06, 2026 3:45 PM" + - Helps analysts understand temporal context of satellite imagery + +2. **Reverse Geocoding** + - Automatically identifies location name from coordinates + - Uses OpenStreetMap Nominatim API + - Provides human-readable location context + +3. **Recent News Integration** + - Fetches recent news for the searched location using Tavily API + - Limits to past week for relevance + - Returns up to 3 recent news items + - Includes news titles, summaries, and relevance notes + +4. **Parallel Processing** + - News fetching happens in parallel with AI analysis + - No blocking of main analysis workflow + - Graceful fallback if news API fails + +5. **Enhanced System Prompt** + - Includes temporal context instructions + - Incorporates news context into analysis + - Guides AI to reference recent events where relevant + +#### Schema Updates: +```typescript +newsContext: z.object({ + hasRecentNews: z.boolean(), + newsItems: z.array(z.object({ + title: z.string(), + summary: z.string(), + relevance: z.string() + })).optional() +}) +``` + +#### Example Output: +```json +{ + "summary": "Urban area with recent infrastructure development...", + "newsContext": { + "hasRecentNews": true, + "newsItems": [ + { + "title": "New Highway Project Begins in Downtown Area", + "summary": "Construction started on major highway expansion...", + "relevance": "Location-based news" + } + ] + } +} +``` + +## Performance Metrics + +| Component | Optimization | Expected Improvement | +|-----------|--------------|----------------------| +| Inquire Agent | Reduced update frequency | 40-50% fewer re-renders | +| Query Suggestor | Caching + throttling | 30-40% faster response | +| Copilot Component | Memoization + useCallback | 50-60% fewer re-renders | +| SearchRelated Component | Memoization + useCallback | 40-50% fewer re-renders | +| Chat Component | Debounced refresh | 60-70% fewer page re-mounts | +| **Overall UI** | **Combined optimizations** | **50-60% faster perceived performance** | + +## Implementation Details + +### Cache Strategy +- Query results cached with 5-minute TTL +- Cache key based on last 3 messages +- Automatic cleanup when cache exceeds 50 entries +- Prevents redundant API calls for similar queries + +### Debouncing Strategy +- Router refresh: 300ms delay +- Drawing context updates: 500ms delay +- Query updates: 200ms throttle +- Balances responsiveness with performance + +### Memory Management +- Limited cache size to prevent memory leaks +- Proper cleanup of timers in useEffect hooks +- Memoization prevents unnecessary object allocations + +## Testing Recommendations + +1. **Performance Testing** + - Measure time to first render of Inquire component + - Track number of re-renders during streaming + - Monitor memory usage during extended sessions + +2. **Functional Testing** + - Verify inquire flow works correctly with optimizations + - Test related queries generation and caching + - Validate news integration with various locations + +3. **User Testing** + - Measure perceived responsiveness improvement + - Collect feedback on UI smoothness + - Monitor for any regressions in functionality + +## Rollback Plan + +If issues arise, changes can be reverted using Git: +```bash +git revert +``` + +Individual files can be reverted: +```bash +git checkout HEAD -- lib/agents/inquire.tsx +git checkout HEAD -- lib/agents/query-suggestor.tsx +git checkout HEAD -- components/copilot.tsx +git checkout HEAD -- components/search-related.tsx +git checkout HEAD -- components/chat.tsx +git checkout HEAD -- lib/agents/resolution-search.tsx +``` + +## Future Optimization Opportunities + +1. **Virtual Scrolling** for long message lists +2. **Code Splitting** for agent modules +3. **Service Worker** for offline support +4. **Image Optimization** for satellite imagery +5. **WebWorker** for heavy computations +6. **GraphQL** for more efficient data fetching +7. **Incremental Static Regeneration** for chat history + +## Dependencies + +- `ai/rsc`: React Server Components +- `tavily`: News and web search API +- `@modelcontextprotocol/sdk`: MCP client for geospatial tools +- OpenStreetMap Nominatim: Reverse geocoding + +## Environment Variables Required + +``` +TAVILY_API_KEY= +OPENAI_API_KEY= +GEMINI_3_PRO_API_KEY= +``` + +## Conclusion + +These optimizations significantly improve the user experience by: +- Reducing UI lag and jank +- Speeding up response times +- Providing richer contextual information +- Maintaining system stability under load + +The combined effect results in a more responsive, efficient Gen AI/UI that better serves users' geospatial analysis needs. diff --git a/README.md b/README.md index d090ea1f..144aca00 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,10 @@ https://deepwiki.com/QueueLab/QCX ### 2. Install dependencies ``` -install bun package manager +install bun package manager bun install bun run build -bun run dev +bun run dev ``` ### 3. Setting up Upstash Redis @@ -92,5 +92,5 @@ You can now visit http://localhost:3000. ## Verified models -List of non reasoning verified models +List of non reasoning verified models Grok-3-mini diff --git a/app/actions.tsx b/app/actions.tsx index a1f5e915..8b693603 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -6,21 +6,22 @@ import { getAIState, getMutableAIState } from 'ai/rsc' -import { CoreMessage, ToolResultPart } from 'ai' -import { nanoid } from 'nanoid' +import { CoreMessage, ToolResultPart, TextPart, ImagePart } 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 { saveChat, getSystemPrompt, generateReportContext } 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' @@ -49,19 +50,44 @@ async function submit(formData?: FormData, skip?: boolean) { console.error('Failed to parse drawnFeatures:', e); } + if (action === 'generate_report_context') { + const messagesString = formData?.get('messages'); + if (typeof messagesString !== 'string') { + return { title: 'QCX Intelligence Analysis', summary: 'Automated executive summary is currently unavailable.' }; + } + try { + const messages = JSON.parse(messagesString) as AIMessage[]; + return await generateReportContext(messages); + } catch (e) { + console.error('Failed to parse messages for report context:', e); + return { title: 'QCX Intelligence Analysis', summary: 'Automated executive summary is currently unavailable.' }; + } + } + if (action === 'resolution_search') { - const file = formData?.get('file') as File; + 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 => + (message: any) => message.role !== 'tool' && message.type !== 'followup' && message.type !== 'related' && @@ -89,7 +115,7 @@ async function submit(formData?: FormData, skip?: boolean) { async function processResolutionSearch() { try { - const streamResult = await resolutionSearch(messages, timezone, drawnFeatures); + const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location); let fullSummary = ''; for await (const partialObject of streamResult.partialObjectStream) { @@ -102,18 +128,37 @@ async function submit(formData?: FormData, skip?: boolean) { const analysisResult = await streamResult.object; summaryStream.done(analysisResult.summary || 'Analysis complete.'); - if (analysisResult.geoJson) { + // Reconstruct standard GeoJSON from flattened schema if present + let geoJson: FeatureCollection | null = null; + if (analysisResult.geoJson && analysisResult.geoJson.features) { + geoJson = { + type: 'FeatureCollection', + features: analysisResult.geoJson.features.map(f => ({ + type: 'Feature', + geometry: { + type: f.geometryType as any, + coordinates: f.coordinates as any + }, + properties: { + name: f.name, + description: f.description + } + })) + }; + } + + if (geoJson) { uiStream.append( ); } messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); - const sanitizedMessages: CoreMessage[] = messages.map(m => { + const sanitizedMessages: CoreMessage[] = messages.map((m: any) => { if (Array.isArray(m.content)) { return { ...m, @@ -124,7 +169,7 @@ async function submit(formData?: FormData, skip?: boolean) { }) const currentMessages = aiState.get().messages; - const sanitizedHistory = currentMessages.map(m => { + const sanitizedHistory = currentMessages.map((m: any) => { if (m.role === "user" && Array.isArray(m.content)) { return { ...m, @@ -159,7 +204,10 @@ async function submit(formData?: FormData, skip?: boolean) { role: 'assistant', content: JSON.stringify({ ...analysisResult, - image: dataUrl + geoJson: geoJson, // Use reconstructed GeoJSON for storage/UI + image: dataUrl, + mapboxImage: mapboxDataUrl, + googleImage: googleDataUrl }), type: 'resolution_search_result' }, @@ -190,7 +238,11 @@ async function submit(formData?: FormData, skip?: boolean) { uiStream.update(
- +
); @@ -203,43 +255,26 @@ async function submit(formData?: FormData, skip?: boolean) { }; } - const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter( - message => - message.role !== 'tool' && - message.type !== 'followup' && - 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 file = !skip ? (formData?.get('file') as File) : undefined + console.log('File extraction:', { + exists: !!file, + name: file?.name, + type: file?.type, + size: file?.size }) - - 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 userInput = skip ? `{"action": "skip"}` : ((formData?.get('related_query') as string) || (formData?.get('input') as string)) - if (userInput.toLowerCase().trim() === 'what is a planet computer?' || userInput.toLowerCase().trim() === 'what is qcx-terra?') { + 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(), @@ -299,10 +334,9 @@ async function submit(formData?: FormData, skip?: boolean) { id: nanoid(), isGenerating: isGenerating.value, component: uiStream.value, - isCollapsed: isCollapsed.value, + isCollapsed: isCollapsed.value }; } - const file = !skip ? (formData?.get('file') as File) : undefined if (!userInput && !file) { isGenerating.done(false) @@ -314,43 +348,118 @@ async function submit(formData?: FormData, skip?: boolean) { } } - const messageParts: { - type: 'text' | 'image' - text?: string - image?: string - mimeType?: string - }[] = [] + let filteredImagesCount = 0 + let retainedImagesCount = 0 + 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)) { + const filteredContent = m.content.filter((part: any) => { + if (part.type === 'image') { + const isValid = + typeof part.image === 'string' && + (part.image.startsWith('data:') || + part.image === 'IMAGE_PROCESSED') + if (isValid) { + retainedImagesCount++ + } else { + filteredImagesCount++ + } + return isValid + } + return true + }) + return { + ...m, + content: filteredContent + } as any + } + return m + }) + console.log('Historical messages image filter:', { + filteredImagesCount, + retainedImagesCount, + totalMessages: messages.length + }) + + 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: (TextPart | ImagePart)[] = [] 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 MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB + if (file.size > MAX_FILE_SIZE) { + console.error('File size exceeds 10MB limit:', file.size) + } else { + try { + const buffer = await file.arrayBuffer() + console.log('File buffer loaded:', { size: buffer.byteLength }) + if (file.type.startsWith('image/')) { + const dataUrl = `data:${file.type};base64,${Buffer.from( + buffer + ).toString('base64')}` + console.log('Image processed:', { + dataUrlPrefix: dataUrl.substring(0, 50), + totalLength: dataUrl.length + }) + const imagePart: ImagePart = { + type: 'image', + image: dataUrl, + mimeType: file.type + } + console.log('Pushing image part (debug shape):', { + ...imagePart, + image: dataUrl.substring(0, 50) + '...' + }) + messageParts.push(imagePart) + } else if (file.type === 'text/plain') { + const textContent = Buffer.from(buffer).toString('utf-8') + const existingTextPart = messageParts.find( + (p): p is TextPart => p.type === 'text' + ) + if (existingTextPart) { + existingTextPart.text = `${textContent}\n\n${existingTextPart.text}` + } else { + messageParts.push({ type: 'text', text: textContent }) + } + } + } catch (error) { + console.error('Error processing file:', error) } } } const hasImage = messageParts.some(part => part.type === 'image') + console.log('messageParts structure:', { + parts: messageParts.map(p => ({ + type: p.type, + length: p.type === 'text' ? p.text.length : undefined + })), + hasImage + }) const content: CoreMessage['content'] = hasImage - ? messageParts as CoreMessage['content'] - : messageParts.map(part => part.text).join('\n') + ? messageParts + : messageParts.map(part => (part.type === 'text' ? part.text : '')).join('\n') + console.log('Final content structure:', { + hasImage, + contentType: typeof content, + isArray: Array.isArray(content), + partsCount: Array.isArray(content) ? content.length : 'N/A' + }) const type = skip ? undefined @@ -725,12 +834,18 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { const analysisResult = JSON.parse(content as string); const geoJson = analysisResult.geoJson as FeatureCollection; const image = analysisResult.image as string; + const mapboxImage = analysisResult.mapboxImage as string; + const googleImage = analysisResult.googleImage as string; return { id, component: ( <> - {image && } + {geoJson && ( )} diff --git a/app/globals.css b/app/globals.css index 22f95cba..722fc36d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,48 +4,48 @@ @layer base { :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 96.1%; - --card-foreground: 0 0% 45.1%; + --background: 220 14% 96%; + --foreground: 222.2 47.4% 11.2%; + --card: 0 0% 100%; + --card-foreground: 222.2 47.4% 11.2%; --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; + --popover-foreground: 222.2 47.4% 11.2%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 89.8%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 47.4% 11.2%; --radius: 0.5rem; } .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 14.9%; - --card-foreground: 0 0% 63.9%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 14.9%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; } .earth { @@ -131,7 +131,7 @@ border-bottom: 1px solid hsl(var(--border)); display: flex; align-items: center; - padding: 0 10px; + padding: 0 5px; z-index: 20; overflow-x: auto; -webkit-overflow-scrolling: touch; @@ -144,8 +144,8 @@ .mobile-icons-bar-content { display: flex; - gap: 20px; - padding: 0 10px; + gap: 10px; + padding: 0 5px; min-width: max-content; /* justify-content: space-between; */ } @@ -189,7 +189,7 @@ .mobile-chat-input-area { height: auto; - padding: 10px; + padding: 5px; background-color: hsl(var(--background)); /* border-top: 1px solid hsl(var(--border)); */ /* Removed for cleaner separation */ border-bottom: 1px solid hsl(var(--border)); /* Added for separation from messages area below */ @@ -205,7 +205,7 @@ /* left: 0; */ /* Handled by parent flex */ /* right: 0; */ /* Handled by parent flex */ width: 100%; /* Ensure it takes full width of its container */ - padding: 10px; + padding: 5px; background-color: hsl(var(--background)); /* border-top: 1px solid hsl(var(--border)); */ /* Removed to avoid double border */ /* z-index: 30; */ /* No longer needed */ diff --git a/app/layout.tsx b/app/layout.tsx index 4369af43..b9ea46ea 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -112,7 +112,6 @@ export default function RootLayout({
{children} -