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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
411 changes: 91 additions & 320 deletions app/actions.tsx

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions app/actions.tsx.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<<<<<<< SEARCH
const action = formData?.get('action') as string;
const drawnFeaturesString = formData?.get('drawnFeatures') as string;
let drawnFeatures: DrawnFeature[] = [];
try {
drawnFeatures = drawnFeaturesString ? JSON.parse(drawnFeaturesString) : [];
} catch (e) {
console.error('Failed to parse drawnFeatures:', e);
}
=======
const action = formData?.get('action') as string;
const drawnFeaturesString = formData?.get('drawnFeatures') as string;
let drawnFeatures: DrawnFeature[] = [];
try {
drawnFeatures = drawnFeaturesString ? JSON.parse(drawnFeaturesString) : [];
} catch (e) {
console.error('Failed to parse drawnFeatures:', e);
}

const chatId = aiState.get().chatId;
if (chatId && drawnFeatures.length === 0) {
const drawingContext = await getDrawingContext(chatId);
if (drawingContext) {
drawnFeatures = (drawingContext as any).drawnFeatures || [];
}
}
>>>>>>> REPLACE
100 changes: 100 additions & 0 deletions app/actions.tsx.tmp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const action = formData?.get("action") as string; const drawnFeaturesString = formData?.get("drawnFeatures") as string; let drawnFeatures: DrawnFeature[] = []; try { drawnFeatures = drawnFeaturesString ? JSON.parse(drawnFeaturesString) : []; } catch (e) { console.error("Failed to parse drawnFeatures:", e); } const chatId = aiState.get().chatId; if (chatId && drawnFeatures.length === 0) { const drawingContext = await getDrawingContext(chatId); if (drawingContext) { drawnFeatures = (drawingContext as any).drawnFeatures || []; } }
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_mapbox = formData?.get('file_mapbox') as File;
const file_google = formData?.get('file_google') as File;
const file = (formData?.get('file') as File) || file_mapbox || file_google;
const timezone = (formData?.get('timezone') as string) || 'UTC';
const lat = formData?.get('latitude') ? parseFloat(formData.get('latitude') as string) : undefined;
const lng = formData?.get('longitude') ? parseFloat(formData.get('longitude') as string) : undefined;
const location = (lat !== undefined && lng !== undefined) ? { lat, lng } : undefined;

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

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

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

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

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

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

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

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

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

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

const analysisResult = await streamResult.object;
summaryStream.done(analysisResult.summary || 'Analysis complete.');

// 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
}
}))
};
}
2 changes: 1 addition & 1 deletion components/history-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export async function HistoryList({ userId }: HistoryListProps) {
key={chat.id}
chat={{
...chat,
path: chat.path || `/search/${chat.id}`,
path: `/search/${chat.id}`,
}}
/>
))
Expand Down
2 changes: 1 addition & 1 deletion components/history-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
SheetTitle,
} from '@/components/ui/sheet'
import { History as HistoryIcon } from 'lucide-react'
import { ChatHistoryClient } from './sidebar/chat-history-client'
import ChatHistoryClient from './sidebar/chat-history-client'
import { Suspense } from 'react'
import { HistorySkeleton } from './history-skelton'
import { useHistoryToggle } from './history-toggle-context'
Expand Down
47 changes: 16 additions & 31 deletions components/sidebar/chat-history-client.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use client';
'use client'

import React, { useEffect, useState, useTransition } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { cn } from '@/lib/utils';
import React, { useState, useEffect, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import { toast } from 'sonner';
import {
AlertDialog,
AlertDialogAction,
Expand All @@ -14,22 +14,16 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { toast } from 'sonner';
import { Spinner } from '@/components/ui/spinner';
import { Zap, ChevronDown, ChevronUp } from 'lucide-react';
import { useHistoryToggle } from '../history-toggle-context';
import HistoryItem from '@/components/history-item'; // Adjust path if HistoryItem is moved or renamed
import type { Chat as DrizzleChat } from '@/lib/actions/chat-db'; // Use the Drizzle-based Chat type

interface ChatHistoryClientProps {
// userId is no longer passed as prop; API route will use authenticated user
}
import { Zap, ChevronUp, ChevronDown } from 'lucide-react';
import { useHistoryToggle } from '@/components/history-toggle-context';
import type { Chat as DrizzleChat } from '@/lib/actions/chat-db';
import HistoryItem from '@/components/history-item';

export function ChatHistoryClient({}: ChatHistoryClientProps) {
export default function ChatHistoryClient() {
const [chats, setChats] = useState<DrizzleChat[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isClearPending, startClearTransition] = useTransition();
const [isAlertDialogOpen, setIsAlertDialogOpen] = useState(false);
Expand All @@ -42,8 +36,7 @@ export function ChatHistoryClient({}: ChatHistoryClientProps) {
setIsLoading(true);
setError(null);
try {
// API route /api/chats uses getCurrentUserId internally
const response = await fetch('/api/chats?limit=50&offset=0'); // Example limit/offset
const response = await fetch('/api/chats?limit=50&offset=0');
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Failed to fetch chats: ${response.statusText}`);
Expand Down Expand Up @@ -71,10 +64,7 @@ export function ChatHistoryClient({}: ChatHistoryClientProps) {
const handleClearHistory = async () => {
startClearTransition(async () => {
try {
// We need a new API endpoint for clearing history
// Example: DELETE /api/chats (or POST /api/clear-history)
// This endpoint will call clearHistory(userId) from chat-db.ts
const response = await fetch('/api/chats/all', { // Placeholder for the actual clear endpoint
const response = await fetch('/api/chats/all', {
method: 'DELETE',
});

Expand All @@ -84,11 +74,9 @@ export function ChatHistoryClient({}: ChatHistoryClientProps) {
}

toast.success('History cleared');
setChats([]); // Clear chats from UI
setChats([]);
setIsAlertDialogOpen(false);
router.refresh(); // Refresh to reflect changes, potentially redirect if on a chat page
// Consider redirecting to '/' if current page is a chat that got deleted.
// The old clearChats action did redirect('/');
router.refresh();
} catch (err) {
if (err instanceof Error) {
toast.error(err.message);
Expand All @@ -110,7 +98,6 @@ export function ChatHistoryClient({}: ChatHistoryClientProps) {
}

if (error) {
// Optionally provide a retry button
return (
<div className="flex flex-col flex-1 space-y-3 h-full items-center justify-center text-destructive">
<p>Error loading chat history: {error}</p>
Expand Down Expand Up @@ -155,8 +142,6 @@ export function ChatHistoryClient({}: ChatHistoryClientProps) {
</div>
) : (
chats.map((chat) => (
// Assuming HistoryItem is adapted for DrizzleChat and expects chat.id and chat.title
// Also, chat.path will need to be constructed, e.g., `/search/${chat.id}`
<HistoryItem key={chat.id} chat={{...chat, path: `/search/${chat.id}`}} />
))
)}
Expand Down
42 changes: 0 additions & 42 deletions drizzle/migrations/0001_add_calendar_notes.sql

This file was deleted.

17 changes: 17 additions & 0 deletions drizzle/migrations/0001_sync_schema_full.sql
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,20 @@ ALTER TABLE "visualizations" ADD CONSTRAINT "visualizations_chat_id_chats_id_fk"
ALTER TABLE "messages" ADD CONSTRAINT "messages_location_id_locations_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."locations"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "chats" ADD CONSTRAINT "chats_shareable_link_id_unique" UNIQUE("shareable_link_id");--> statement-breakpoint
ALTER TABLE "users" ADD CONSTRAINT "users_email_unique" UNIQUE("email");

-- Add RLS for calendar_notes (extracted from removed duplicate migration)
ALTER TABLE "calendar_notes" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "user_select_own_notes" ON "calendar_notes" FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "user_insert_own_notes" ON "calendar_notes" FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "user_update_own_notes" ON "calendar_notes" FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "user_delete_own_notes" ON "calendar_notes" FOR DELETE USING (auth.uid() = user_id);

-- Add RLS for system_prompts
ALTER TABLE "system_prompts" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "user_select_own_system_prompts" ON "system_prompts" FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "user_insert_own_system_prompts" ON "system_prompts" FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "user_update_own_system_prompts" ON "system_prompts" FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "user_delete_own_system_prompts" ON "system_prompts" FOR DELETE USING (auth.uid() = user_id);

-- Add RLS for chat_contexts (newly added in 0007, but including here for consistency if needed,
-- or we can just ensure 0007 has it. Let's add it to 0007).
22 changes: 22 additions & 0 deletions drizzle/migrations/0003_consolidate_system_prompt.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Ensure system_prompts has a unique constraint on user_id
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'system_prompts_user_id_unique'
) THEN
ALTER TABLE "system_prompts" ADD CONSTRAINT "system_prompts_user_id_unique" UNIQUE("user_id");
END IF;
END $$;

-- Backfill existing users.system_prompt values into system_prompts
INSERT INTO "system_prompts" ("user_id", "prompt", "updated_at")
SELECT "id", "system_prompt", NOW()
FROM "users"
WHERE "system_prompt" IS NOT NULL
ON CONFLICT ("user_id") DO UPDATE
SET "prompt" = EXCLUDED."prompt", "updated_at" = EXCLUDED."updated_at";

-- Drop the users.system_prompt column
ALTER TABLE "users" DROP COLUMN IF EXISTS "system_prompt";
18 changes: 18 additions & 0 deletions drizzle/migrations/0004_remove_redundant_spatial.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- Convert existing geojson values into geometry for locations
UPDATE "locations"
SET "geometry" = ST_GeomFromGeoJSON("geojson"::text)
WHERE "geojson" IS NOT NULL AND "geometry" IS NULL;

-- Convert existing data values into geometry for visualizations (if they contain geometry)
UPDATE "visualizations"
SET "geometry" = ST_GeomFromGeoJSON("data"->>'geometry')
WHERE "data" IS NOT NULL AND "data"->>'geometry' IS NOT NULL AND "geometry" IS NULL;

-- Drop redundant column from locations
ALTER TABLE "locations" DROP COLUMN IF EXISTS "geojson";

-- Clean up redundant geometry overlap in visualizations.data
-- We keep the data column but remove the geometry key from the jsonb object
UPDATE "visualizations"
SET "data" = "data" - 'geometry'
WHERE "data" IS NOT NULL AND "data" ? 'geometry';
3 changes: 3 additions & 0 deletions drizzle/migrations/0005_drop_chat_paths.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Drop derived path and share_path columns from chats
ALTER TABLE "chats" DROP COLUMN IF EXISTS "path";
ALTER TABLE "chats" DROP COLUMN IF EXISTS "share_path";
4 changes: 4 additions & 0 deletions drizzle/migrations/0006_remove_synthetic_calendar_notes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Delete existing synthetic calendar-note rows from messages
DELETE FROM "messages"
WHERE "role" = 'data'
AND "content"::jsonb->>'type' = 'calendar_note';
Loading