Skip to content
Merged
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/
2 changes: 1 addition & 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,
{
id: groupeId,
role: 'assistant',
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
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.')
}
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);
}}
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
}
}

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
18 changes: 9 additions & 9 deletions components/settings/components/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,13 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
data-testid={`theme-select-${t.value}`}
onClick={() => setTheme(t.value)}
className={cn(
"flex flex-col items-center justify-center p-6 rounded-xl border-2 transition-all gap-3 text-center",
"flex flex-col items-center justify-center p-3 rounded-xl border-2 transition-all gap-2 text-center",
isActive
? "bg-accent/40 border-primary text-primary shadow-sm"
: "bg-card border-muted hover:border-muted-foreground/50 text-muted-foreground hover:text-foreground"
)}
>
<Icon className="h-8 w-8" />
<Icon className="h-6 w-6" />
<span className="text-sm font-medium">{t.name}</span>
</button>
)
Expand All @@ -211,9 +211,9 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {

<Tabs.Root value={currentTab} onValueChange={setCurrentTab} className="w-full">
<Tabs.List className="grid w-full grid-cols-2 md:grid-cols-4 gap-2">
<Tabs.Trigger value="system-prompt" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 data-[state=active]:bg-primary/80">System Prompt</Tabs.Trigger>
<Tabs.Trigger value="model" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 data-[state=active]:bg-primary/80">Model Selection</Tabs.Trigger>
<Tabs.Trigger value="user-management" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 data-[state=active]:bg-primary/80">User Management</Tabs.Trigger>
<Tabs.Trigger value="system-prompt" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 data-[state=active]:bg-primary/80">System</Tabs.Trigger>
<Tabs.Trigger value="model" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 data-[state=active]:bg-primary/80">Models</Tabs.Trigger>
<Tabs.Trigger value="user-management" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 data-[state=active]:bg-primary/80">Users</Tabs.Trigger>
<Tabs.Trigger value="map" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 data-[state=active]:bg-primary/80">Map</Tabs.Trigger>
</Tabs.List>
<AnimatePresence mode="wait">
Expand All @@ -227,7 +227,7 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
<Tabs.Content value="system-prompt" className="mt-6">
<Card>
<CardHeader>
<CardTitle>System Prompt</CardTitle>
<CardTitle>System</CardTitle>
<CardDescription>Customize the behavior and persona of your planetary copilot</CardDescription>
</CardHeader>
<CardContent>
Expand All @@ -239,7 +239,7 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
<Tabs.Content value="model" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Model Selection</CardTitle>
<CardTitle>Models</CardTitle>
<CardDescription>Choose the AI model that powers your planetary copilot</CardDescription>
</CardHeader>
<CardContent>
Expand Down Expand Up @@ -285,7 +285,7 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
<CardFooter className="flex justify-between pt-6">
<Button type="button" variant="outline" onClick={onReset} disabled={isSaving}>
<RotateCcw className="mr-2 h-4 w-4" />
Reset to Defaults
Reset
</Button>
<Button type="submit" disabled={isSaving}>
{isSaving ? (
Expand All @@ -296,7 +296,7 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
) : (
<>
<Save className="mr-2 h-4 w-4" />
Save Changes
Save
</>
)}
</Button>
Expand Down
2 changes: 1 addition & 1 deletion components/settings/components/system-prompt-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export function SystemPromptForm({ form }: SystemPromptFormProps) {
name="systemPrompt"
render={({ field }) => (
<FormItem>
<FormLabel>System Prompt</FormLabel>
<FormLabel>System</FormLabel>
<FormControl>
<Textarea
placeholder="Enter the system prompt for your planetary copilot..."
Expand Down
Loading