diff --git a/app/actions.tsx b/app/actions.tsx index a1f5e915..77769a5e 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -21,11 +21,13 @@ 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 { MapDataUpdater } from '@/components/map/map-data-updater' 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' +import { geospatialTool } from '@/lib/agents/tools/geospatial' // Define the type for related queries type RelatedQueries = { @@ -232,6 +234,37 @@ async function submit(formData?: FormData, skip?: boolean) { : ((formData?.get('related_query') as string) || (formData?.get('input') as string)) + let isGeoJsonInput = false + if (userInput) { + try { + const trimmedInput = userInput.trim() + if ((trimmedInput.startsWith('{') && trimmedInput.endsWith('}')) || (trimmedInput.startsWith('[') && trimmedInput.endsWith(']'))) { + const geoJson = JSON.parse(trimmedInput) + if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') { + isGeoJsonInput = true + const geoJsonId = nanoid() + aiState.update({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: geoJsonId, + role: 'assistant', + content: JSON.stringify({ data: geoJson, filename: 'Pasted GeoJSON' }), + type: 'geojson_upload' + } + ] + }) + uiStream.append( + + ) + } + } + } catch (e) { + // Not a valid JSON, ignore + } + } + if (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)` @@ -322,6 +355,8 @@ async function submit(formData?: FormData, skip?: boolean) { }[] = [] if (userInput) { + // If it's a GeoJSON input, we still want to keep it in the message history for the AI to see, + // but we might want to truncate it if it's huge. For now, just pass it. messageParts.push({ type: 'text', text: userInput }) } @@ -336,8 +371,39 @@ async function submit(formData?: FormData, skip?: boolean) { image: dataUrl, mimeType: file.type }) - } else if (file.type === 'text/plain') { + } else if (file.type === 'text/plain' || file.name.endsWith('.geojson') || file.type === 'application/geo+json') { const textContent = Buffer.from(buffer).toString('utf-8') + const isGeoJson = file.name.endsWith('.geojson') || file.type === 'application/geo+json' + + if (isGeoJson) { + try { + const geoJson = JSON.parse(textContent) + if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') { + const geoJsonId = nanoid() + // Add a special message to track the GeoJSON upload + aiState.update({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: geoJsonId, + role: 'assistant', + content: JSON.stringify({ data: geoJson, filename: file.name }), + type: 'geojson_upload' + } + ] + }) + + // Immediately append the updater to the UI stream + uiStream.append( + + ) + } + } catch (e) { + console.error('Failed to parse GeoJSON:', e) + } + } + const existingTextPart = messageParts.find(p => p.type === 'text') if (existingTextPart) { existingTextPart.text = `${textContent}\n\n${existingTextPart.text}` @@ -383,6 +449,47 @@ async function submit(formData?: FormData, skip?: boolean) { const currentSystemPrompt = (await getSystemPrompt(userId)) || '' const mapProvider = formData?.get('mapProvider') as 'mapbox' | 'google' + // Autonomous Map Navigation: Check if input looks like an address + const isPotentialAddress = (text: string) => { + // Simple heuristic: contains numbers and multiple words, or specific keywords + const addressKeywords = ['palace', 'street', 'st', 'avenue', 'ave', 'road', 'rd', 'boulevard', 'blvd', 'drive', 'dr', 'lane', 'ln', 'court', 'ct', 'square', 'sq', 'parkway', 'pkwy']; + const words = text.toLowerCase().split(/\s+/); + const hasNumber = /\d+/.test(text); + const hasKeyword = words.some(word => addressKeywords.includes(word)); + const isCoordinate = /^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/.test(text.trim()); + + return (hasNumber && words.length >= 2) || hasKeyword || isCoordinate; + }; + + if (userInput && isPotentialAddress(userInput)) { + console.log('[AutonomousMap] Detected potential address:', userInput); + // Trigger geospatial tool directly for immediate map update + const geoTool = geospatialTool({ uiStream, mapProvider }); + + // Run geocoding in the background or wait briefly + // We don't await it here to avoid blocking the main AI response, + // but we want it to start immediately. + (async () => { + try { + const isCoordinate = /^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/.test(userInput.trim()); + const result = await geoTool.execute( + isCoordinate + ? { queryType: 'reverse', coordinates: { + latitude: parseFloat(userInput.split(',')[0]), + longitude: parseFloat(userInput.split(',')[1]) + } } + : { queryType: 'geocode', location: userInput } + ); + + if (result && result.type === 'MAP_QUERY_TRIGGER') { + uiStream.append(); + } + } catch (error) { + console.error('[AutonomousMap] Quick geocode failed:', error); + } + })(); + } + async function processEvents() { let action: any = { object: { next: 'proceed' } } if (!skip) { @@ -644,10 +751,19 @@ export const AI = createAI({ export const getUIStateFromAIState = (aiState: AIState): UIState => { const chatId = aiState.chatId const isSharePage = aiState.isSharePage + + // Filter messages to only include the last 'data' message if multiple exist + const lastDataMessageIndex = [...aiState.messages].reverse().findIndex(m => m.role === 'data') + const actualLastDataIndex = lastDataMessageIndex === -1 ? -1 : aiState.messages.length - 1 - lastDataMessageIndex + return aiState.messages .map((message, index) => { const { role, content, id, type, name } = message + if (role === 'data' && index !== actualLastDataIndex) { + return null + } + if ( !type || type === 'end' || @@ -738,6 +854,13 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { ) } } + case 'geojson_upload': { + const { data, filename } = JSON.parse(content as string) + return { + id, + component: + } + } } break case 'tool': @@ -813,6 +936,26 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { } } break + case 'data': + try { + const contextData = JSON.parse(content as string) + if (contextData.uploadedGeoJson && Array.isArray(contextData.uploadedGeoJson)) { + return { + id, + component: ( + <> + {contextData.uploadedGeoJson.map((item: any) => ( + + ))} + + ) + } + } + return { id, component: null } + } catch (e) { + console.error('Error parsing data message:', e) + return { id, component: null } + } default: return { id, diff --git a/app/search/[id]/page.tsx b/app/search/[id]/page.tsx index 8db74186..5523632b 100644 --- a/app/search/[id]/page.tsx +++ b/app/search/[id]/page.tsx @@ -48,14 +48,11 @@ export default async function SearchPage({ params }: SearchPageProps) { const initialMessages: AIMessage[] = dbMessages.map((dbMsg): AIMessage => { return { id: dbMsg.id, - role: dbMsg.role as AIMessage['role'], // Cast role, ensure AIMessage['role'] includes all dbMsg.role possibilities + role: dbMsg.role as AIMessage['role'], content: dbMsg.content, createdAt: dbMsg.createdAt ? new Date(dbMsg.createdAt) : undefined, - // 'type' and 'name' are not in the basic Drizzle 'messages' schema. - // These would be undefined unless specific logic is added to derive them. - // For instance, if a message with role 'tool' should have a 'name', - // or if some messages have a specific 'type' based on content or other flags. - // This mapping assumes standard user/assistant messages primarily. + type: dbMsg.type as AIMessage['type'], + name: dbMsg.toolName as string, }; }); diff --git a/bun.lock b/bun.lock index a3de9819..12913e2a 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "QCX", @@ -50,7 +51,7 @@ "csv-parse": "^6.1.0", "dotenv": "^16.5.0", "drizzle-kit": "^0.31.1", - "drizzle-orm": "^0.29.0", + "drizzle-orm": "^0.45.1", "embla-carousel-react": "^8.6.0", "exa-js": "^1.6.13", "framer-motion": "^12.23.24", @@ -61,7 +62,7 @@ "lottie-react": "^2.4.1", "lucide-react": "^0.507.0", "mapbox-gl": "^3.11.0", - "next": "15.3.6", + "next": "15.3.8", "next-themes": "^0.3.0", "open-codex": "^0.1.30", "pg": "^8.16.2", @@ -401,7 +402,7 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - "@next/env": ["@next/env@15.3.6", "", {}, "sha512-/cK+QPcfRbDZxmI/uckT4lu9pHCfRIPBLqy88MhE+7Vg5hKrEYc333Ae76dn/cw2FBP2bR/GoK/4DU+U7by/Nw=="], + "@next/env": ["@next/env@15.3.8", "", {}, "sha512-SAfHg0g91MQVMPioeFeDjE+8UPF3j3BvHjs8ZKJAUz1BG7eMPvfCKOAgNWJ6s1MLNeP6O2InKQRTNblxPWuq+Q=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@14.2.35", "", { "dependencies": { "glob": "10.3.10" } }, "sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ=="], @@ -1313,7 +1314,7 @@ "drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="], - "drizzle-orm": ["drizzle-orm@0.29.5", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=3", "@libsql/client": "*", "@neondatabase/serverless": ">=0.1", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=13.2.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@libsql/client", "@neondatabase/serverless", "@opentelemetry/api", "@planetscale/database", "@types/better-sqlite3", "@types/pg", "@types/react", "@types/sql.js", "@vercel/postgres", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "react", "sql.js", "sqlite3"] }, "sha512-jS3+uyzTz4P0Y2CICx8FmRQ1eplURPaIMWDn/yq6k4ShRFj9V7vlJk67lSf2kyYPzQ60GkkNGXcJcwrxZ6QCRw=="], + "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -1939,7 +1940,7 @@ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "next": ["next@15.3.6", "", { "dependencies": { "@next/env": "15.3.6", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.5", "@next/swc-darwin-x64": "15.3.5", "@next/swc-linux-arm64-gnu": "15.3.5", "@next/swc-linux-arm64-musl": "15.3.5", "@next/swc-linux-x64-gnu": "15.3.5", "@next/swc-linux-x64-musl": "15.3.5", "@next/swc-win32-arm64-msvc": "15.3.5", "@next/swc-win32-x64-msvc": "15.3.5", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-oI6D1zbbsh6JzzZFDCSHnnx6Qpvd1fSkVJu/5d8uluqnxzuoqtodVZjYvNovooznUq8udSAiKp7MbwlfZ8Gm6w=="], + "next": ["next@15.3.8", "", { "dependencies": { "@next/env": "15.3.8", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.5", "@next/swc-darwin-x64": "15.3.5", "@next/swc-linux-arm64-gnu": "15.3.5", "@next/swc-linux-arm64-musl": "15.3.5", "@next/swc-linux-x64-gnu": "15.3.5", "@next/swc-linux-x64-musl": "15.3.5", "@next/swc-win32-arm64-msvc": "15.3.5", "@next/swc-win32-x64-msvc": "15.3.5", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-L+4c5Hlr84fuaNADZbB9+ceRX9/CzwxJ+obXIGHupboB/Q1OLbSUapFs4bO8hnS/E6zV/JDX7sG1QpKVR2bguA=="], "next-themes": ["next-themes@0.3.0", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18", "react-dom": "^16.8 || ^17 || ^18" } }, "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w=="], diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 7d877ccd..39488b65 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -215,7 +215,8 @@ export const ChatPanel = forwardRef(({ messages, i ref={fileInputRef} onChange={handleFileChange} className="hidden" - accept="text/plain,image/png,image/jpeg,image/webp" + accept="text/plain,image/png,image/jpeg,image/webp,.geojson,application/geo+json" + data-testid="file-upload-input" /> {!isMobile && ( - + {isCreditsVisible && (
diff --git a/components/user-message.tsx b/components/user-message.tsx index 03b8ea8d..3b7ad8da 100644 --- a/components/user-message.tsx +++ b/components/user-message.tsx @@ -32,7 +32,7 @@ export const UserMessage: React.FC = ({ )?.image return ( -
+
{imagePart && (
diff --git a/drizzle/migrations/0001_aromatic_ultimatum.sql b/drizzle/migrations/0001_aromatic_ultimatum.sql new file mode 100644 index 00000000..dca6de0b --- /dev/null +++ b/drizzle/migrations/0001_aromatic_ultimatum.sql @@ -0,0 +1,19 @@ +CREATE TABLE "calendar_notes" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "chat_id" uuid, + "date" timestamp with time zone NOT NULL, + "content" text NOT NULL, + "location_tags" jsonb, + "user_tags" text[], + "map_feature_id" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "messages" ADD COLUMN "attachments" jsonb;--> statement-breakpoint +ALTER TABLE "messages" ADD COLUMN "tool_name" varchar(100);--> statement-breakpoint +ALTER TABLE "messages" ADD COLUMN "tool_call_id" varchar(100);--> statement-breakpoint +ALTER TABLE "messages" ADD COLUMN "type" varchar(50);--> statement-breakpoint +ALTER TABLE "calendar_notes" ADD CONSTRAINT "calendar_notes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "calendar_notes" ADD CONSTRAINT "calendar_notes_chat_id_chats_id_fk" FOREIGN KEY ("chat_id") REFERENCES "public"."chats"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/migrations/meta/0000_snapshot.json b/drizzle/migrations/meta/0000_snapshot.json index eb62145d..8f31b8c5 100644 --- a/drizzle/migrations/meta/0000_snapshot.json +++ b/drizzle/migrations/meta/0000_snapshot.json @@ -1,10 +1,8 @@ { - "id": "0d46923a-5423-4b73-91cb-5f46741e7ff9", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "5", - "dialect": "pg", + "version": "7", + "dialect": "postgresql", "tables": { - "chats": { + "public.chats": { "name": "chats", "schema": "", "columns": { @@ -48,21 +46,24 @@ "chats_user_id_users_id_fk": { "name": "chats_user_id_users_id_fk", "tableFrom": "chats", - "tableTo": "users", "columnsFrom": [ "user_id" ], + "tableTo": "users", "columnsTo": [ "id" ], - "onDelete": "cascade", - "onUpdate": "no action" + "onUpdate": "no action", + "onDelete": "cascade" } }, "compositePrimaryKeys": {}, - "uniqueConstraints": {} + "uniqueConstraints": {}, + "policies": {}, + "isRLSEnabled": false, + "checkConstraints": {} }, - "messages": { + "public.messages": { "name": "messages", "schema": "", "columns": { @@ -110,34 +111,37 @@ "messages_chat_id_chats_id_fk": { "name": "messages_chat_id_chats_id_fk", "tableFrom": "messages", - "tableTo": "chats", "columnsFrom": [ "chat_id" ], + "tableTo": "chats", "columnsTo": [ "id" ], - "onDelete": "cascade", - "onUpdate": "no action" + "onUpdate": "no action", + "onDelete": "cascade" }, "messages_user_id_users_id_fk": { "name": "messages_user_id_users_id_fk", "tableFrom": "messages", - "tableTo": "users", "columnsFrom": [ "user_id" ], + "tableTo": "users", "columnsTo": [ "id" ], - "onDelete": "cascade", - "onUpdate": "no action" + "onUpdate": "no action", + "onDelete": "cascade" } }, "compositePrimaryKeys": {}, - "uniqueConstraints": {} + "uniqueConstraints": {}, + "policies": {}, + "isRLSEnabled": false, + "checkConstraints": {} }, - "users": { + "public.users": { "name": "users", "schema": "", "columns": { @@ -152,14 +156,23 @@ "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, - "uniqueConstraints": {} + "uniqueConstraints": {}, + "policies": {}, + "isRLSEnabled": false, + "checkConstraints": {} } }, "enums": {}, "schemas": {}, "_meta": { - "columns": {}, "schemas": {}, - "tables": {} - } + "tables": {}, + "columns": {} + }, + "id": "0d46923a-5423-4b73-91cb-5f46741e7ff9", + "prevId": "00000000-0000-0000-0000-000000000000", + "sequences": {}, + "policies": {}, + "views": {}, + "roles": {} } \ No newline at end of file diff --git a/drizzle/migrations/meta/0001_snapshot.json b/drizzle/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..49d7c82b --- /dev/null +++ b/drizzle/migrations/meta/0001_snapshot.json @@ -0,0 +1,305 @@ +{ + "id": "63b6e4b5-8ef8-4789-a16c-d109a5fa233a", + "prevId": "0d46923a-5423-4b73-91cb-5f46741e7ff9", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.calendar_notes": { + "name": "calendar_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_tags": { + "name": "location_tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "user_tags": { + "name": "user_tags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "map_feature_id": { + "name": "map_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "calendar_notes_user_id_users_id_fk": { + "name": "calendar_notes_user_id_users_id_fk", + "tableFrom": "calendar_notes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendar_notes_chat_id_chats_id_fk": { + "name": "calendar_notes_chat_id_chats_id_fk", + "tableFrom": "calendar_notes", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chats": { + "name": "chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "default": "'Untitled Chat'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "visibility": { + "name": "visibility", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'private'" + } + }, + "indexes": {}, + "foreignKeys": { + "chats_user_id_users_id_fk": { + "name": "chats_user_id_users_id_fk", + "tableFrom": "chats", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attachments": { + "name": "attachments", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "messages_chat_id_chats_id_fk": { + "name": "messages_chat_id_chats_id_fk", + "tableFrom": "messages", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_user_id_users_id_fk": { + "name": "messages_user_id_users_id_fk", + "tableFrom": "messages", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 34cd1203..d224f68c 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1750358514791, "tag": "0000_sweet_metal_master", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1769972170699, + "tag": "0001_aromatic_ultimatum", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/actions/chat-db.ts b/lib/actions/chat-db.ts index 4f0559ec..5169be9e 100644 --- a/lib/actions/chat-db.ts +++ b/lib/actions/chat-db.ts @@ -119,7 +119,21 @@ export async function saveChat(chatData: NewChat, messagesData: Omit m.role === 'data'); + + if (dataMessage) { + // Update existing message content using direct db call since chat-db.ts doesn't have an updateMessage + const { db } = await import('@/lib/db'); + const { messages } = await import('@/lib/db/schema'); + const { eq } = await import('drizzle-orm'); + + await db.update(messages) + .set({ content: JSON.stringify(contextData) }) + .where(eq(messages.id, dataMessage.id)); + + console.log('Drawing context updated for chat:', chatId, 'messageId:', dataMessage.id); + return { success: true, messageId: dataMessage.id }; + } + + // Otherwise create a new one const messageToSave: DbNewMessage = { ...newDrawingMessage, chatId: chatId, diff --git a/lib/agents/tools/geospatial.tsx b/lib/agents/tools/geospatial.tsx index ca5f9f49..178bde56 100644 --- a/lib/agents/tools/geospatial.tsx +++ b/lib/agents/tools/geospatial.tsx @@ -25,6 +25,7 @@ interface Location { interface McpResponse { location: Location; mapUrl?: string; + geoJson?: any; } interface MapboxConfig { @@ -395,11 +396,35 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g const parsedData = content as any; if (parsedData.results?.length > 0) { const firstResult = parsedData.results[0]; - mcpData = { location: { latitude: firstResult.coordinates?.latitude, longitude: firstResult.coordinates?.longitude, place_name: firstResult.name || firstResult.place_name, address: firstResult.full_address || firstResult.address }, mapUrl: parsedData.mapUrl }; + mcpData = { + location: { + latitude: firstResult.coordinates?.latitude, + longitude: firstResult.coordinates?.longitude, + place_name: firstResult.name || firstResult.place_name, + address: firstResult.full_address || firstResult.address + }, + mapUrl: parsedData.mapUrl, + geoJson: parsedData.geoJson || parsedData.geojson || firstResult.geoJson || firstResult.geojson + }; } else if (parsedData.location) { - mcpData = { location: { latitude: parsedData.location.latitude, longitude: parsedData.location.longitude, place_name: parsedData.location.place_name || parsedData.location.name, address: parsedData.location.address || parsedData.location.formatted_address }, mapUrl: parsedData.mapUrl || parsedData.map_url }; + mcpData = { + location: { + latitude: parsedData.location.latitude, + longitude: parsedData.location.longitude, + place_name: parsedData.location.place_name || parsedData.location.name, + address: parsedData.location.address || parsedData.location.formatted_address + }, + mapUrl: parsedData.mapUrl || parsedData.map_url, + geoJson: parsedData.geoJson || parsedData.geojson || parsedData.location.geoJson || parsedData.location.geojson + }; + } else if (parsedData.type === 'FeatureCollection' || parsedData.type === 'Feature') { + // Direct GeoJSON response + mcpData = { + location: {}, // Will be derived from bbox if needed, or left empty + geoJson: parsedData + }; } else { - throw new Error("Response missing required 'location' or 'results' field"); + throw new Error("Response missing required 'location', 'results', or 'geoJson' field"); } } else throw new Error('Unexpected response format from mapping service'); diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 4b7ef891..3242c083 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -30,10 +30,10 @@ export const messages = pgTable('messages', { role: varchar('role', { length: 50 }).notNull(), // e.g., 'user', 'assistant', 'system', 'tool' content: text('content').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - // attachments: jsonb('attachments'), // As per PR commit: "feat: remove updatedAt and add attachments field to messages" - // toolName: varchar('tool_name', { length: 100 }), // If messages can be from tools - // toolCallId: varchar('tool_call_id', {length: 100}), // if tracking specific tool calls - // type: varchar('type', { length: 50 }) // As per app/actions.tsx AIMessage type + attachments: jsonb('attachments'), + toolName: varchar('tool_name', { length: 100 }), + toolCallId: varchar('tool_call_id', {length: 100}), + type: varchar('type', { length: 50 }) }); // Calendar Notes Table diff --git a/lib/types/index.ts b/lib/types/index.ts index c4ea616c..d20b9803 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -74,6 +74,7 @@ export type AIMessage = { | 'end' | 'drawing_context' // Added custom type for drawing context messages | 'resolution_search_result' + | 'geojson_upload' // Added custom type for GeoJSON upload messages } export type CalendarNote = { diff --git a/package.json b/package.json index 87066925..ca1aed72 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "csv-parse": "^6.1.0", "dotenv": "^16.5.0", "drizzle-kit": "^0.31.1", - "drizzle-orm": "^0.29.0", + "drizzle-orm": "^0.45.1", "embla-carousel-react": "^8.6.0", "exa-js": "^1.6.13", "framer-motion": "^12.23.24",