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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ aef_index.csv
*.log
deno.json
edge_function/

# clerk configuration (can include secrets)
/.clerk/
13 changes: 12 additions & 1 deletion app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ async function submit(formData?: FormData, skip?: boolean) {
aiState.done({
...aiState.get(),
messages: [
...aiState.get().messages,
...sanitizedHistory,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
id: groupeId,
role: 'assistant',
Expand Down Expand Up @@ -695,6 +695,14 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
} catch (e) {
messageContent = content
}

// Filter out image parts that were processed/sanitized
if (Array.isArray(messageContent)) {
messageContent = messageContent.filter(part =>
part.type !== 'image' || (part.image && part.image !== 'IMAGE_PROCESSED')
);
}

return {
id,
component: (
Expand All @@ -705,6 +713,9 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
/>
)
}
case 'drawing_context':
return null // Drawing context is handled by the map, not rendered in chat

case 'inquiry':
return {
id,
Expand Down
29 changes: 19 additions & 10 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,17 +118,22 @@

.mobile-map-section {
height: 40vh;
width: 100%;
width: calc(100% - 16px);
margin: 8px 8px 4px 8px;
background-color: hsl(var(--secondary));
z-index: 10;
border-radius: 16px;
border: 1px solid hsl(var(--border));
overflow: hidden;
}

.mobile-icons-bar {
height: 60px;
width: 100%;
width: calc(100% - 16px);
margin: 4px 8px;
background-color: hsl(var(--background));
border-top: 1px solid hsl(var(--border));
border-bottom: 1px solid hsl(var(--border));
border: 1px solid hsl(var(--border));
border-radius: 12px;
display: flex;
align-items: center;
padding: 0 5px;
Expand All @@ -144,10 +149,10 @@

.mobile-icons-bar-content {
display: flex;
gap: 8px;
padding: 0 5px;
width: 100%;
justify-content: center;
gap: 16px;
padding: 0 10px;
min-width: max-content;
justify-content: flex-start;
}

.mobile-icons-bar-content > * {
Expand Down Expand Up @@ -175,14 +180,18 @@
color: hsl(var(--card-foreground));
box-sizing: border-box;
min-height: 0;
border-radius: 16px;
border: 1px solid hsl(var(--border));
margin: 4px 8px 8px 8px;
}

.mobile-chat-input-area {
height: auto;
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 */
border: 1px solid hsl(var(--border));
border-radius: 12px;
margin: 4px 8px;
box-sizing: border-box;
/* z-index: 30; */ /* No longer needed as it's in flow */
display: flex;
Expand Down
31 changes: 24 additions & 7 deletions app/search/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,33 @@ export default async function SearchPage({ params }: SearchPageProps) {

// Transform DrizzleMessages to AIMessages
const initialMessages: AIMessage[] = dbMessages.map((dbMsg): AIMessage => {
let content = dbMsg.content;
let type = undefined;
let name = undefined;

// Attempt to extract type and name from the content if it's a JSON string
// This handles the metadata we've started embedding in the content field.
try {
if (content.startsWith('{') && content.endsWith('}')) {
const parsed = JSON.parse(content);
if (parsed && typeof parsed === 'object' && '__metadata' in parsed) {
const { __metadata, ...rest } = parsed;
content = JSON.stringify(rest);
type = __metadata.type;
name = __metadata.name;
}
}
} catch (e) {
// Fallback to original content if parsing fails
}

return {
id: dbMsg.id,
role: dbMsg.role as AIMessage['role'], // Cast role, ensure AIMessage['role'] includes all dbMsg.role possibilities
content: dbMsg.content,
role: dbMsg.role as AIMessage['role'],
content,
type: type as any,
name,
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.
};
});

Expand Down
3 changes: 3 additions & 0 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
setIsUploading(true)
try {
const supabase = getSupabaseBrowserClient()
if (!supabase) {
throw new Error('Supabase client is not initialized.')
}
Comment on lines 130 to +133

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Rollback the optimistic message when upload initialization fails.

The optimistic message is added before this guard. When the guard throws, the catch returns while the fake attachment remains in chat history and the selected file remains attached, so retrying can duplicate the message. Either defer setMessages until the upload succeeds or remove the optimistic entry in the catch path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/chat-panel.tsx` around lines 130 - 133, Update the upload flow
around the optimistic message creation and Supabase initialization guard to roll
back the optimistic message when initialization fails. In the catch path, remove
the fake attachment message and clear the selected file state before returning,
while preserving the existing optimistic behavior when upload initialization
succeeds.

const fileExt = selectedFile.name.split('.').pop()
const storagePath = `${crypto.randomUUID()}.${fileExt}`

Expand Down
1 change: 1 addition & 0 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export function Chat({ id }: ChatProps) {
if (!id) return

const supabase = getSupabaseBrowserClient()
if (!supabase) return

// Subscribe to messages changes
const channel = supabase
Expand Down
21 changes: 10 additions & 11 deletions components/empty-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Button } from '@/components/ui/button';
import { Globe, Thermometer, Laptop, HelpCircle } from 'lucide-react';

const exampleMessages = [
Expand Down Expand Up @@ -33,23 +32,23 @@ export function EmptyScreen({
}) {
return (
<div className={`mx-auto w-full transition-all overflow-hidden ${className}`}>
<div className="bg-background p-2">
<div className="mt-4 flex flex-col items-start space-y-2 mb-4">
<div className="p-2">
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
{exampleMessages.map((item) => {
const Icon = item.icon;
return (
<Button
key={item.message} // Use a unique property as the key.
variant="link"
className="h-auto p-0 text-base flex items-center gap-1.5 whitespace-normal text-left break-words max-w-[calc(100vw-2rem)] sm:max-w-full"
name={item.message}
<button
key={item.message}
onClick={async () => {
submitMessage(item.message);
}}
Comment on lines +40 to 44

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add an explicit button type to prevent unintended form submissions.

A plain <button> element defaults to type="submit". If this component is placed within a <form> context in the future, clicking the example message will trigger a form submission. Explicitly set type="button".

Also, the onClick handler is currently marked as async but doesn't await anything, which can be safely simplified.

💡 Proposed fix
-              <button
-                key={item.message}
-                onClick={async () => {
-                  submitMessage(item.message);
-                }}
+              <button
+                key={item.message}
+                type="button"
+                onClick={() => {
+                  submitMessage(item.message);
+                }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
key={item.message}
onClick={async () => {
submitMessage(item.message);
}}
<button
key={item.message}
type="button"
onClick={() => {
submitMessage(item.message);
}}
🧰 Tools
🪛 React Doctor (0.7.6)

[warning] 40-40: Your users can submit the form by accident because a <button> with no type defaults to submit.

Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".

(button-has-type)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/empty-screen.tsx` around lines 40 - 44, Update the button rendered
in the example-message loop by adding an explicit type="button" and simplify its
onClick handler to a non-async callback since submitMessage is not awaited;
preserve the existing submitMessage(item.message) behavior.

Source: Linters/SAST tools

className="flex items-center gap-3 p-3 text-sm text-left border border-border bg-card hover:bg-accent/50 rounded-xl transition-all w-full text-foreground/80 hover:text-foreground font-medium shadow-sm"
>
<Icon size={16} className="text-muted-foreground flex-shrink-0" />
{item.heading}
</Button>
<div className="p-2 bg-secondary rounded-lg text-muted-foreground flex-shrink-0">
<Icon size={16} />
</div>
<span className="line-clamp-2">{item.heading}</span>
</button>
);
})}
</div>
Expand Down
11 changes: 10 additions & 1 deletion components/map-loading-context.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use client';
import { createContext, useContext, useState, ReactNode } from 'react';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';

interface MapLoadingContextType {
isMapLoaded: boolean;
Expand All @@ -10,6 +10,15 @@ const MapLoadingContext = createContext<MapLoadingContextType | undefined>(undef

export const MapLoadingProvider = ({ children }: { children: ReactNode }) => {
const [isMapLoaded, setIsMapLoaded] = useState(false);

useEffect(() => {
// Automatic fallback to map loaded after 1 second to prevent blocking tests or local setups without keys
const timer = setTimeout(() => {
setIsMapLoaded(true);
}, 1000);
return () => clearTimeout(timer);
}, []);

return (
<MapLoadingContext.Provider value={{ isMapLoaded, setIsMapLoaded }}>
{children}
Expand Down
131 changes: 71 additions & 60 deletions components/map/mapbox-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,75 +372,86 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number
// Initialize map (only once)
useEffect(() => {
if (mapContainer.current && !map.current) {
let initialCenter: [number, number] = [position?.longitude ?? 0, position?.latitude ?? 0];
let initialZoom = 2;
let initialPitch = 0;
let initialBearing = 0;

if (mapData.cameraState) {
const { center, range, tilt, heading, zoom, pitch, bearing } = mapData.cameraState;
initialCenter = [center.lng, center.lat];
if (zoom !== undefined) {
initialZoom = zoom;
} else if (range !== undefined) {
initialZoom = Math.log2(40000000 / range);
try {
if (!process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN) {
throw new Error('An API access token is required to use Mapbox GL. Please configure NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN.');
}
initialPitch = pitch ?? tilt ?? 0;
initialBearing = bearing ?? heading ?? 0;
} else if (typeof window !== 'undefined' && window.innerWidth < 768) {
initialZoom = 1.3;
}

currentMapCenterRef.current = { center: initialCenter, zoom: initialZoom, pitch: initialPitch };

map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/satellite-streets-v12',
center: initialCenter,
zoom: initialZoom,
pitch: initialPitch,
bearing: initialBearing,
maxZoom: 22,
attributionControl: true,
preserveDrawingBuffer: true
})
let initialCenter: [number, number] = [position?.longitude ?? 0, position?.latitude ?? 0];
let initialZoom = 2;
let initialPitch = 0;
let initialBearing = 0;

if (mapData.cameraState) {
const { center, range, tilt, heading, zoom, pitch, bearing } = mapData.cameraState;
initialCenter = [center.lng, center.lat];
if (zoom !== undefined) {
initialZoom = zoom;
} else if (range !== undefined) {
initialZoom = Math.log2(40000000 / range);
}
initialPitch = pitch ?? tilt ?? 0;
initialBearing = bearing ?? heading ?? 0;
} else if (typeof window !== 'undefined' && window.innerWidth < 768) {
initialZoom = 1.3;
}

// Register event listeners
map.current.on('moveend', captureMapCenter)
map.current.on('mousedown', handleUserInteraction)
map.current.on('touchstart', handleUserInteraction)
map.current.on('wheel', handleUserInteraction)
map.current.on('drag', handleUserInteraction)
map.current.on('zoom', handleUserInteraction)

map.current.on('load', () => {
if (!map.current) return
setMap(map.current) // Set map instance in context

// Add terrain and sky
map.current.addSource('mapbox-dem', {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
maxzoom: 14,
currentMapCenterRef.current = { center: initialCenter, zoom: initialZoom, pitch: initialPitch };

map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/satellite-streets-v12',
center: initialCenter,
zoom: initialZoom,
pitch: initialPitch,
bearing: initialBearing,
maxZoom: 22,
attributionControl: true,
preserveDrawingBuffer: true
})

map.current.setTerrain({ source: 'mapbox-dem', exaggeration: 1.5 })
// Register event listeners
map.current.on('moveend', captureMapCenter)
map.current.on('mousedown', handleUserInteraction)
map.current.on('touchstart', handleUserInteraction)
map.current.on('wheel', handleUserInteraction)
map.current.on('drag', handleUserInteraction)
map.current.on('zoom', handleUserInteraction)

map.current.on('load', () => {
if (!map.current) return
setMap(map.current) // Set map instance in context

// Add terrain and sky
map.current.addSource('mapbox-dem', {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
maxzoom: 14,
})

map.current.addLayer({
id: 'sky',
type: 'sky',
paint: {
'sky-type': 'atmosphere',
'sky-atmosphere-sun': [0.0, 0.0],
'sky-atmosphere-sun-intensity': 15,
},
})
map.current.setTerrain({ source: 'mapbox-dem', exaggeration: 1.5 })

map.current.addLayer({
id: 'sky',
type: 'sky',
paint: {
'sky-type': 'atmosphere',
'sky-atmosphere-sun': [0.0, 0.0],
'sky-atmosphere-sun-intensity': 15,
},
})

initializedRef.current = true
setIsMapReady(true)
setIsMapLoaded(true) // Set map loaded state to true
})
} catch (error) {
console.error('Failed to initialize Mapbox Map:', error)
initializedRef.current = true
setIsMapReady(true)
setIsMapLoaded(true) // Set map loaded state to true
})
setIsMapLoaded(true) // Set loaded/ready so overlay disappears and app remains functional
Comment on lines +445 to +453

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused initializedRef.

The initializedRef is assigned here but never read, making it dead code.

While it might seem tempting to add this ref to the initialization guard (if (!map.current && !initializedRef.current)) to prevent multiple failure logs, doing so would actually break React 18 Strict Mode. In Strict Mode, refs persist across the simulated unmount and remount cycle, so checking the ref would permanently prevent the map from re-initializing after the simulated unmount. The current !map.current guard works correctly on its own, so it is best to remove this ref entirely.

🧹 Proposed cleanup
-          initializedRef.current = true
           setIsMapReady(true)
           setIsMapLoaded(true) // Set map loaded state to true
         })
       } catch (error) {
         console.error('Failed to initialize Mapbox Map:', error)
-        initializedRef.current = true
         setIsMapReady(true)
         setIsMapLoaded(true) // Set loaded/ready so overlay disappears and app remains functional
       }

(You can also safely remove its declaration const initializedRef = useRef<boolean>(false) around line 33).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/map/mapbox-map.tsx` around lines 445 - 453, Remove the unused
initializedRef declaration and all assignments to initializedRef.current in the
Mapbox initialization success and failure paths. Keep the existing !map.current
initialization guard unchanged; do not add initializedRef to it, preserving
React 18 Strict Mode re-initialization behavior.

}
}

return () => {
Expand Down
9 changes: 5 additions & 4 deletions components/settings/components/settings-skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ export function SettingsSkeleton() {
return (
<div className="space-y-6">
<Tabs.Root defaultValue="system-prompt" className="w-full">
<Tabs.List className="grid w-full grid-cols-3">
<Tabs.Trigger value="system-prompt">System Prompt</Tabs.Trigger>
<Tabs.Trigger value="users">User Management</Tabs.Trigger>
<Tabs.Trigger value="model">Model Selection</Tabs.Trigger>
<Tabs.List className="grid w-full grid-cols-2 md:grid-cols-4 gap-2">
<Tabs.Trigger value="system-prompt">System</Tabs.Trigger>
<Tabs.Trigger value="model">Models</Tabs.Trigger>
<Tabs.Trigger value="user-management">Users</Tabs.Trigger>
<Tabs.Trigger value="map">Map</Tabs.Trigger>
</Tabs.List>

<Tabs.Content value="system-prompt">
Expand Down
Loading