{imagePart && (
diff --git a/lib/actions/chat.ts b/lib/actions/chat.ts
index c257d6e8..4c7847fc 100644
--- a/lib/actions/chat.ts
+++ b/lib/actions/chat.ts
@@ -162,7 +162,7 @@ export async function saveChat(chat: OldChatType, userId: string): Promise
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');
From 9f799299cb64e62717e0dc95f27a7f4ab5bc0cfe Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sun, 1 Feb 2026 18:17:23 +0000
Subject: [PATCH 2/4] feat: implement GeoJSON upload and tool ingestion
pipeline
- Extend MapData context to support uploaded GeoJSON layers
- Update ChatPanel to support GeoJSON file selection and add test IDs
- Implement MapDataUpdater component for automatic context sync and map framing
- Update Mapbox and Google Maps components to render uploaded GeoJSON
- Enable AI tools to ingest GeoJSON into the map pipeline via MapQueryHandler
- Ensure persistence of GeoJSON data across chat sessions via database sync
- Update AIMessage type to support 'geojson_upload'
- Fix ESLint warnings to ensure clean build performance
Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
---
components/chat-panel.tsx | 2 +-
components/map/mapbox-map.tsx | 2 +-
lib/types/index.ts | 1 +
3 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx
index 3a00cde2..33820160 100644
--- a/components/chat-panel.tsx
+++ b/components/chat-panel.tsx
@@ -153,7 +153,7 @@ export const ChatPanel = forwardRef(({ messages, i
}
}, 500) // 500ms debounce delay
},
- [mapData]
+ [mapData, setSuggestions]
)
useEffect(() => {
diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx
index ac7c5eba..99f879f0 100644
--- a/components/map/mapbox-map.tsx
+++ b/components/map/mapbox-map.tsx
@@ -455,7 +455,7 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number
geolocationWatchIdRef.current = null
}
}
- }, [setMap, setIsMapLoaded, captureMapCenter, handleUserInteraction, stopRotation])
+ }, [setMap, setIsMapLoaded, captureMapCenter, handleUserInteraction, stopRotation, mapData.cameraState, position?.latitude, position?.longitude])
// Handle map mode changes
useEffect(() => {
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 = {
From bfe62f9d7c9f131a2f4c94a70d28bf6d1b233e53 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sun, 1 Feb 2026 18:33:21 +0000
Subject: [PATCH 3/4] feat: support pasted GeoJSON and fix refresh loop
- Add support for pasting GeoJSON directly into the chat input
- Implement synchronization guard in Chat.tsx to prevent redundant state saves
- Ensure 'geojson_upload' message type is correctly handled in AI state
- Improve GeoJSON extraction from geospatial tool responses
Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
---
app/actions.tsx | 29 +++++++++++++++++++++++++++++
components/chat.tsx | 16 +++++++++++++---
2 files changed, 42 insertions(+), 3 deletions(-)
diff --git a/app/actions.tsx b/app/actions.tsx
index a1ab3b1f..90c3dd88 100644
--- a/app/actions.tsx
+++ b/app/actions.tsx
@@ -211,6 +211,35 @@ async function submit(formData?: FormData, skip?: boolean) {
: ((formData?.get('related_query') as string) ||
(formData?.get('input') as string))
+ 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') {
+ 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)`
diff --git a/components/chat.tsx b/components/chat.tsx
index 311be019..a8b94a76 100644
--- a/components/chat.tsx
+++ b/components/chat.tsx
@@ -81,6 +81,7 @@ export function Chat({ id }: ChatProps) {
// Get mapData to access drawnFeatures
const { mapData } = useMapData();
+ const lastSyncedDataRef = useRef('');
useEffect(() => {
if (isSubmitting) {
@@ -92,12 +93,21 @@ export function Chat({ id }: ChatProps) {
// useEffect to call the server action when drawnFeatures or uploadedGeoJson changes
useEffect(() => {
if (id && (mapData.drawnFeatures || mapData.uploadedGeoJson) && mapData.cameraState) {
- console.log('Chat.tsx: map data changed, calling updateDrawingContext');
- updateDrawingContext(id, {
+ const currentData = JSON.stringify({
drawnFeatures: mapData.drawnFeatures || [],
cameraState: mapData.cameraState,
- uploadedGeoJson: mapData.uploadedGeoJson || []
+ uploadedGeoJson: (mapData.uploadedGeoJson || []).map(item => ({ id: item.id, visible: item.visible }))
});
+
+ if (currentData !== lastSyncedDataRef.current) {
+ console.log('Chat.tsx: map data changed, calling updateDrawingContext');
+ lastSyncedDataRef.current = currentData;
+ updateDrawingContext(id, {
+ drawnFeatures: mapData.drawnFeatures || [],
+ cameraState: mapData.cameraState,
+ uploadedGeoJson: mapData.uploadedGeoJson || []
+ });
+ }
}
}, [id, mapData.drawnFeatures, mapData.cameraState, mapData.uploadedGeoJson]);
From 9450b1596fc978aab4ae722244ae4d178b490cf8 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sun, 1 Feb 2026 19:17:29 +0000
Subject: [PATCH 4/4] feat: enable GeoJSON uploads, paste support, and fix
refresh loop
This commit implements a comprehensive pipeline for GeoJSON data overlays:
- Updated `ChatPanel` to accept `.geojson` files and handle pasted GeoJSON text.
- Added `MapDataUpdater` headless component to sync GeoJSON to map context and automatically fit viewport bounds using Turf.js.
- Fixed an infinite refresh loop in `Chat.tsx` by introducing `lastRefreshedMessageIdRef` to guard `router.refresh()`.
- Prevented redundant camera movement loops in `MapDataUpdater` using `hasZoomedRef`.
- Updated database schema and message mapping to include `type`, `attachments`, and tool metadata, ensuring map overlays persist across page refreshes.
- Implemented `onConflictDoUpdate` (upsert) for saving messages to prevent primary key collisions.
- Supported GeoJSON ingestion from both file uploads, manual text input, and agent tools.
Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
---
app/actions.tsx | 13 +
app/search/[id]/page.tsx | 9 +-
bun.lock | 4 +-
components/chat.tsx | 13 +-
components/map/map-data-updater.tsx | 6 +-
.../migrations/0001_aromatic_ultimatum.sql | 19 ++
drizzle/migrations/meta/0000_snapshot.json | 57 ++--
drizzle/migrations/meta/0001_snapshot.json | 305 ++++++++++++++++++
drizzle/migrations/meta/_journal.json | 7 +
lib/actions/chat-db.ts | 16 +-
lib/actions/chat.ts | 21 +-
lib/db/schema.ts | 8 +-
package.json | 2 +-
13 files changed, 435 insertions(+), 45 deletions(-)
create mode 100644 drizzle/migrations/0001_aromatic_ultimatum.sql
create mode 100644 drizzle/migrations/meta/0001_snapshot.json
diff --git a/app/actions.tsx b/app/actions.tsx
index 90c3dd88..c773e503 100644
--- a/app/actions.tsx
+++ b/app/actions.tsx
@@ -211,12 +211,14 @@ 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(),
@@ -331,6 +333,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 })
}
@@ -685,10 +689,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' ||
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..907bd01f 100644
--- a/bun.lock
+++ b/bun.lock
@@ -50,7 +50,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",
@@ -1313,7 +1313,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=="],
diff --git a/components/chat.tsx b/components/chat.tsx
index a8b94a76..f0dfc8bc 100644
--- a/components/chat.tsx
+++ b/components/chat.tsx
@@ -73,8 +73,10 @@ export function Chat({ id }: ChatProps) {
}, [id, path, messages])
useEffect(() => {
- if (aiState.messages[aiState.messages.length - 1]?.type === 'response') {
+ const lastMessage = aiState.messages[aiState.messages.length - 1];
+ if (lastMessage?.type === 'response' && lastMessage.id !== lastRefreshedMessageIdRef.current) {
// Refresh the page to chat history updates
+ lastRefreshedMessageIdRef.current = lastMessage.id;
router.refresh()
}
}, [aiState, router])
@@ -82,6 +84,7 @@ export function Chat({ id }: ChatProps) {
// Get mapData to access drawnFeatures
const { mapData } = useMapData();
const lastSyncedDataRef = useRef('');
+ const lastRefreshedMessageIdRef = useRef(null);
useEffect(() => {
if (isSubmitting) {
@@ -114,7 +117,7 @@ export function Chat({ id }: ChatProps) {
// Mobile layout
if (isMobile) {
return (
- {/* Add Provider */}
+ <>
@@ -168,13 +171,13 @@ export function Chat({ id }: ChatProps) {
)}
-
+ >
);
}
// Desktop layout
return (
- {/* Add Provider */}
+ <>
{/* This is the new div for scrolling */}
@@ -232,6 +235,6 @@ export function Chat({ id }: ChatProps) {
{activeView ? : }
-
+ >
);
}
diff --git a/components/map/map-data-updater.tsx b/components/map/map-data-updater.tsx
index 3ceaec69..747becd0 100644
--- a/components/map/map-data-updater.tsx
+++ b/components/map/map-data-updater.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect } from 'react';
+import { useEffect, useRef } from 'react';
import { useMapData } from './map-data-context';
import { useMap } from './map-context';
import type { FeatureCollection } from 'geojson';
@@ -15,6 +15,7 @@ interface MapDataUpdaterProps {
export function MapDataUpdater({ id, data, filename }: MapDataUpdaterProps) {
const { setMapData } = useMapData();
const { map } = useMap();
+ const hasZoomedRef = useRef(false);
useEffect(() => {
if (!data) return;
@@ -45,7 +46,8 @@ export function MapDataUpdater({ id, data, filename }: MapDataUpdaterProps) {
});
// Fly to the extent of the GeoJSON
- if (map && featureCollection.features.length > 0) {
+ if (map && featureCollection.features.length > 0 && !hasZoomedRef.current) {
+ hasZoomedRef.current = true;
try {
const bbox = turf.bbox(featureCollection);
map.fitBounds(bbox as [number, number, number, number], {
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/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/package.json b/package.json
index cdf8a7e3..77546007 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",