Skip to content
Merged
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
61 changes: 61 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Playwright Tests

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Install Playwright Browsers
run: bunx playwright install --with-deps

- name: Build application
run: bun run build
env:
# Add any required environment variables for build
NODE_ENV: production

- name: Start application
run: |
bun run start &
sleep 10
# Wait for the server to be ready
npx wait-on http://localhost:3000 --timeout 60000

- name: Run Playwright tests
run: bun run test:e2e
env:
CI: true

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

- name: Upload test screenshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-screenshots
path: test-results/
retention-days: 7
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# Playwright
/playwright-report/
/test-results/
/dev.log
6 changes: 6 additions & 0 deletions .idx/integrations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"firebase_hosting": {},
"gemini_api": {},
"google_maps_platform": {},
"secrets_manager": {}
}
2 changes: 1 addition & 1 deletion app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ async function submit(formData?: FormData, skip?: boolean) {
messages.push({ role: 'user', content });

// Call the simplified agent, which now returns data directly.
const analysisResult = await resolutionSearch(messages);
const analysisResult = await resolutionSearch(messages) as any;

// Create a streamable value for the summary and mark it as done.
const summaryStream = createStreamableValue<string>();
Expand Down
1,716 changes: 932 additions & 784 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
variant={'secondary'}
className="rounded-full bg-secondary/80 group transition-all hover:scale-105 pointer-events-auto"
onClick={() => handleClear()}
data-testid="new-chat-button"
>
<span className="text-sm mr-2 group-hover:block hidden animate-in fade-in duration-300">
New
Expand All @@ -155,7 +156,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
<span className="text-sm text-muted-foreground truncate max-w-xs">
{selectedFile.name}
</span>
<Button variant="ghost" size="icon" onClick={clearAttachment}>
<Button variant="ghost" size="icon" onClick={clearAttachment} data-testid="clear-attachment-button">
<X size={16} />
</Button>
</div>
Expand Down Expand Up @@ -191,6 +192,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
'absolute top-1/2 transform -translate-y-1/2 left-3'
)}
onClick={handleAttachmentClick}
data-testid="attachment-button"
>
<Paperclip size={isMobile ? 18 : 20} />
</Button>
Expand All @@ -204,6 +206,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
placeholder="Explore"
spellCheck={false}
value={input}
data-testid="chat-input"
className={cn(
'resize-none w-full min-h-12 rounded-fill border border-input pl-14 pr-12 pt-3 pb-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
isMobile
Expand Down Expand Up @@ -247,6 +250,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
)}
disabled={input.length === 0 && !selectedFile}
aria-label="Send message"
data-testid="chat-submit"
>
<ArrowRight size={isMobile ? 18 : 20} />
</Button>
Expand Down
2 changes: 1 addition & 1 deletion components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const Header = () => {

<MapToggle />

<Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar">
<Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar" data-testid="calendar-toggle">
<CalendarDays className="h-[1.2rem] w-[1.2rem]" />
</Button>

Expand Down
1 change: 1 addition & 0 deletions components/history-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const HistoryItem: React.FC<HistoryItemProps> = ({ chat }) => {
'flex flex-col hover:bg-muted cursor-pointer p-2 rounded border',
isActive ? 'bg-muted/70 border-border' : 'border-transparent'
)}
data-testid={`history-item-${chat.id}`}
>
<div className="text-xs font-medium truncate select-none">
{chat.title}
Expand Down
3 changes: 2 additions & 1 deletion components/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ export function History({ location }: HistoryProps) {
className={cn({
'rounded-full text-foreground/30': location === 'sidebar'
})}
data-testid="history-button"
>
{location === 'header' ? <Menu /> : <ChevronLeft size={16} />}
</Button>
</SheetTrigger>
<SheetContent className="w-64 rounded-tl-xl rounded-bl-xl">
<SheetContent className="w-64 rounded-tl-xl rounded-bl-xl" data-testid="history-panel">
<SheetHeader>
<SheetTitle className="flex items-center gap-1 text-sm font-normal mb-2">
<HistoryIcon size={14} />
Expand Down
8 changes: 4 additions & 4 deletions components/map-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ export function MapToggle() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<Button variant="ghost" size="icon" className="relative" data-testid="map-toggle">
<Map className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
<span className="sr-only">Toggle map mode</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.RealTimeMode)}}>
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.RealTimeMode)}} data-testid="map-mode-live">
Live
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.FreeMode)}}>
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.FreeMode)}} data-testid="map-mode-mymaps">
My Maps
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.DrawingMode)}}>
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.DrawingMode)}} data-testid="map-mode-draw">
<Pencil className="h-[1rem] w-[1rem] mr-2" />
Draw & Measure
</DropdownMenuItem>
Expand Down
13 changes: 6 additions & 7 deletions components/mobile-icons-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { History } from '@/components/history'
import { MapToggle } from './map-toggle'
import { ModeToggle } from './mode-toggle'
import { ProfileToggle } from './profile-toggle'
import { useCalendarToggle } from './calendar-toggle-context'

interface MobileIconsBarProps {
Expand All @@ -35,17 +36,15 @@ export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClic

return (
<div className="mobile-icons-bar-content">
<Button variant="ghost" size="icon" onClick={handleNewChat}>
<Button variant="ghost" size="icon" onClick={handleNewChat} data-testid="mobile-new-chat-button">
<Plus className="h-[1.2rem] w-[1.2rem]" />
</Button>
<Button variant="ghost" size="icon">
<CircleUserRound className="h-[1.2rem] w-[1.2rem]" />
</Button>
<ProfileToggle />
<MapToggle />
<Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar">
<Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar" data-testid="mobile-calendar-button">
<CalendarDays className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<Button variant="ghost" size="icon">
<Button variant="ghost" size="icon" data-testid="mobile-search-button">
<Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<a href="https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00" target="_blank" rel="noopener noreferrer">
Expand All @@ -56,7 +55,7 @@ export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClic
<Button variant="ghost" size="icon" onClick={onAttachmentClick}>
<Paperclip className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<Button variant="ghost" size="icon">
<Button variant="ghost" size="icon" data-testid="mobile-submit-button" disabled aria-disabled="true">
<ArrowRight className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<History location="header" />
Expand Down
10 changes: 5 additions & 5 deletions components/mode-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function ModeToggle() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<Button variant="ghost" size="icon" className="relative" data-testid="theme-toggle">
<span className="relative block">
<Sun
className={`h-[1.2rem] w-[1.2rem] transition-all ${
Expand All @@ -47,16 +47,16 @@ export function ModeToggle() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>
<DropdownMenuItem onClick={() => setTheme('light')} data-testid="theme-light">
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
<DropdownMenuItem onClick={() => setTheme('dark')} data-testid="theme-dark">
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('earth')}>
<DropdownMenuItem onClick={() => setTheme('earth')} data-testid="theme-earth">
Earth
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
<DropdownMenuItem onClick={() => setTheme('system')} data-testid="theme-system">
System
</DropdownMenuItem>
</DropdownMenuContent>
Expand Down
10 changes: 5 additions & 5 deletions components/profile-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,25 @@ export function ProfileToggle() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<Button variant="ghost" size="icon" className="relative" data-testid="profile-toggle">
<CircleUserRound className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
<span className="sr-only">Open profile menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align={alignValue} forceMount>
<DropdownMenuItem onClick={() => handleSectionChange(ProfileToggleEnum.Account)}>
<DropdownMenuItem onClick={() => handleSectionChange(ProfileToggleEnum.Account)} data-testid="profile-account">
<User className="mr-2 h-4 w-4" />
<span>Account</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleSectionChange(ProfileToggleEnum.Settings)}>
<DropdownMenuItem onClick={() => handleSectionChange(ProfileToggleEnum.Settings)} data-testid="profile-settings">
<Settings className="mr-2 h-4 w-4" />
<span>Settings</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleSectionChange(ProfileToggleEnum.Appearance)}>
<DropdownMenuItem onClick={() => handleSectionChange(ProfileToggleEnum.Appearance)} data-testid="profile-appearance">
<Paintbrush className="mr-2 h-4 w-4" />
<span>Appearance</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleSectionChange(ProfileToggleEnum.Security)}>
<DropdownMenuItem onClick={() => handleSectionChange(ProfileToggleEnum.Security)} data-testid="profile-security">
<Shield className="mr-2 h-4 w-4" />
<span>Security</span>
</DropdownMenuItem>
Expand Down
4 changes: 2 additions & 2 deletions components/settings/components/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ const settingsFormSchema = z.object({
.max(2000, {
message: "System prompt cannot exceed 2000 characters.",
}),
selectedModel: z.string({
required_error: "Please select a model.",
selectedModel: z.string().refine(value => value.trim() !== '', {
message: "Please select a model.",
}),
users: z.array(
z.object({
Expand Down
5 changes: 3 additions & 2 deletions components/sidebar/chat-history-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export function ChatHistoryClient({}: ChatHistoryClientProps) {
<div className="mt-auto">
<AlertDialog open={isAlertDialogOpen} onOpenChange={setIsAlertDialogOpen}>
<AlertDialogTrigger asChild>
<Button variant="outline" className="w-full" disabled={!chats?.length || isClearPending}>
<Button variant="outline" className="w-full" disabled={!chats?.length || isClearPending} data-testid="clear-history-button">
{isClearPending ? <Spinner /> : 'Clear History'}
</Button>
</AlertDialogTrigger>
Expand All @@ -142,13 +142,14 @@ export function ChatHistoryClient({}: ChatHistoryClientProps) {
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isClearPending} onClick={() => setIsAlertDialogOpen(false)}>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={isClearPending} onClick={() => setIsAlertDialogOpen(false)} data-testid="clear-history-cancel">Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={isClearPending}
onClick={(event) => {
event.preventDefault();
handleClearHistory();
}}
data-testid="clear-history-confirm"
>
{isClearPending ? <Spinner /> : 'Clear'}
</AlertDialogAction>
Expand Down
6 changes: 3 additions & 3 deletions lib/agents/query-suggestor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export async function querySuggestor(
})

for await (const obj of result.partialObjectStream) {
if (obj.items) {
objectStream.update(obj)
finalRelatedQueries = obj
if (obj && typeof obj === 'object' && 'items' in obj) {
objectStream.update(obj as PartialRelated)
finalRelatedQueries = obj as PartialRelated
}
}

Expand Down
23 changes: 23 additions & 0 deletions lib/auth/use-current-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react';
import { getSupabaseBrowserClient } from '../supabase/browser-client';
import type { User } from '@supabase/supabase-js';

export function useCurrentUser() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const supabase = getSupabaseBrowserClient();

useEffect(() => {
async function fetchUser() {
const { data, error } = await supabase.auth.getUser();
if (data) {
setUser(data.user);
}
setLoading(false);
}

fetchUser();
}, [supabase.auth]);

return { user, loading };
}
1 change: 1 addition & 0 deletions lib/supabase/browser-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const getSupabaseBrowserClient = () => ({ auth: { getUser: () => ({ data: { user: null }, error: null }) } });
Loading