From b779a4c5a21f67aa6334a7a23702ba34285be4f3 Mon Sep 17 00:00:00 2001 From: Himanshu Gupta Date: Mon, 28 Sep 2026 21:16:56 +0530 Subject: [PATCH 1/5] feat: reorder dashboard navigation --- platform/test/e2e/dashboard-api.spec.ts | 5 ++++- web/app/dashboard/DashboardSidebar.tsx | 2 +- web/app/dashboard/ResearchHost.tsx | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/platform/test/e2e/dashboard-api.spec.ts b/platform/test/e2e/dashboard-api.spec.ts index 4fbe2b2..c84d7fc 100644 --- a/platform/test/e2e/dashboard-api.spec.ts +++ b/platform/test/e2e/dashboard-api.spec.ts @@ -564,9 +564,12 @@ test('settings renders while navigation access checks are pending', async ({page await page.goto('/dashboard/settings',{waitUntil:'commit'}); await expect(page.getByRole('switch',{name:/In-app alerts/})).toBeEnabled(); await expect(page.getByRole('button',{name:'Upgrade to Builder'})).toBeEnabled(); + const navigation = page.getByRole('navigation', { name: 'Dashboard navigation' }); await expect(page.getByRole('link',{name:'Agent',exact:true})).toHaveCount(0); + await expect.poll(async () => (await navigation.locator('a').allTextContents()).slice(0, 2).map(label => label.trim())).toEqual(['Sources', 'Trends']); await scenario.release(); await expect(page.getByRole('link',{name:'Agent',exact:true})).toBeVisible(); + await expect.poll(async () => (await navigation.locator('a').allTextContents()).slice(0, 3).map(label => label.trim())).toEqual(['Sources', 'Agent', 'Trends']); }finally{await scenario.clear();} }); @@ -599,7 +602,7 @@ test('an active trend request survives projects navigation without restarting',a await page.getByRole('link',{name:'Projects',exact:true}).click(); await expect(page.getByRole('heading',{name:'Your projects'})).toBeVisible(); release(); - await page.getByRole('link',{name:'Trend Lab',exact:true}).click(); + await page.getByRole('link',{name:'Trends',exact:true}).click(); await expect(page.getByRole('alert').filter({hasText:'Retained scan completed'})).toBeVisible(); expect(reads).toBe(1); }); diff --git a/web/app/dashboard/DashboardSidebar.tsx b/web/app/dashboard/DashboardSidebar.tsx index b770ded..ae8321c 100644 --- a/web/app/dashboard/DashboardSidebar.tsx +++ b/web/app/dashboard/DashboardSidebar.tsx @@ -87,9 +87,9 @@ export function DashboardSidebar({ activeSection
Recent projects
- {projects.slice(0, 5).map(project => )} - {!projectsResource.ready && !projectsResource.error &&
{[0, 1, 2].map(index => )}
} + {projects.slice(0, 5).map(project => { + const expanded = expandedProjectId === project.id; + return
+ + {expanded &&
+ {!projectDetail && !projectError &&
} + {projectError && } + {projectDetail?.id === project.id && (projectDetail.items.length + ? projectDetail.items.map(item => ) + : No saved sources)} +
} +
; + })} + {!projectsResource.ready && !projectsResource.error &&
{[0, 1, 2].map(index => )}
} {projectsResource.ready && !projects.length &&

Save a source to start a project.

}
diff --git a/web/app/dashboard/NewProjectDialog.tsx b/web/app/dashboard/NewProjectDialog.tsx index b992b79..08b54f8 100644 --- a/web/app/dashboard/NewProjectDialog.tsx +++ b/web/app/dashboard/NewProjectDialog.tsx @@ -1,9 +1,54 @@ 'use client'; -import { useRef, useEffect, useState } from 'react'; -export function NewProjectDialog({ onClose,onCreate }: { onClose:()=>void;onCreate:(name:string)=>void }) { const dialogRef=useDialogFocus(onClose);const [name,setName]=useState('');return
event.stopPropagation()} onSubmit={(event)=>{event.preventDefault();if(name.trim())onCreate(name.trim());}}>

New project

Name this line of inquiry

setName(event.target.value)} placeholder='e.g. AI video research'/>
; } + +import { useEffect, useRef, useState } from 'react'; + +export function NewProjectDialog({ onClose, onCreate, error }: { + onClose: () => void; + onCreate: (name: string) => Promise; + error: string; +}) { + const [name, setName] = useState(''); + const [creating, setCreating] = useState(false); + const close = () => { if (!creating) onClose(); }; + const dialogRef = useDialogFocus(close); + + const create = async () => { + const trimmed = name.trim(); + if (!trimmed || creating) return; + setCreating(true); + const succeeded = await onCreate(trimmed); + if (!succeeded) setCreating(false); + }; + + return
+
event.stopPropagation()} + onSubmit={event => { event.preventDefault(); void create(); }} + > + +

New project

+

Keep related sources in one place.

+ + setName(event.target.value)} placeholder='e.g. AI video research' /> + {error &&

{error}

} + +
+
; +} function useDialogFocus(onClose: () => void) { const dialogRef = useRef(null); + const closeRef = useRef(onClose); + closeRef.current = onClose; useEffect(() => { const previouslyFocused = document.activeElement as HTMLElement | null; const dialog = dialogRef.current; @@ -12,9 +57,9 @@ function useDialogFocus(onClose: () => void) { preferred?.focus(); }); const onKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') { event.preventDefault(); onClose(); return; } + if (event.key === 'Escape') { event.preventDefault(); closeRef.current(); return; } if (event.key !== 'Tab' || !dialog) return; - const focusable = Array.from(dialog.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [href], [tabindex]:not([tabindex="-1"])')).filter((element) => !element.hidden); + const focusable = Array.from(dialog.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [href], [tabindex]:not([tabindex="-1"])')).filter(element => !element.hidden); if (!focusable.length) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } @@ -22,6 +67,6 @@ function useDialogFocus(onClose: () => void) { }; document.addEventListener('keydown', onKeyDown); return () => { document.removeEventListener('keydown', onKeyDown); previouslyFocused?.focus(); }; - }, [onClose]); + }, []); return dialogRef; } diff --git a/web/app/dashboard/WorkspaceShell.tsx b/web/app/dashboard/WorkspaceShell.tsx index 5e6c061..5c1d8f2 100644 --- a/web/app/dashboard/WorkspaceShell.tsx +++ b/web/app/dashboard/WorkspaceShell.tsx @@ -30,6 +30,7 @@ export function WorkspaceShell({ onNavigate={(s) => router.push(dashboardPath(s))} onNewProject={() => router.push('/dashboard/projects?newProject=1')} onOpenProject={(p) => router.push(`/dashboard/projects?project=${encodeURIComponent(p.id)}`)} + onOpenProjectItem={(item) => router.push(`/dashboard/sources?type=${item.entity_type}&id=${encodeURIComponent(item.entity_id)}`)} onSignIn={() => router.push('/login')} accountName={user?.name ?? user?.email ?? (demoEnabled ? 'Local demo' : undefined)} credits={usage?.creditBalance} diff --git a/web/app/dashboard/admin/AdminAccessClient.tsx b/web/app/dashboard/admin/AdminAccessClient.tsx index 3055e8d..4855ff3 100644 --- a/web/app/dashboard/admin/AdminAccessClient.tsx +++ b/web/app/dashboard/admin/AdminAccessClient.tsx @@ -68,6 +68,7 @@ export default function AdminAccessClient() { onNavigate={section => router.push(`/dashboard/${section === 'discover' ? 'sources' : section}`)} onNewProject={() => router.push('/dashboard?section=projects')} onOpenProject={project => router.push(`/dashboard/projects?project=${encodeURIComponent(project.id)}`)} + onOpenProjectItem={item => router.push(`/dashboard/sources?type=${item.entity_type}&id=${encodeURIComponent(item.entity_id)}`)} onSignIn={() => router.push('/dashboard')} accountName={user?.name ?? user?.email} credits={credits} onSignOut={() => void signOut()} />
diff --git a/web/app/dashboard/developer/DeveloperSettingsClient.tsx b/web/app/dashboard/developer/DeveloperSettingsClient.tsx index 516c815..edeb536 100644 --- a/web/app/dashboard/developer/DeveloperSettingsClient.tsx +++ b/web/app/dashboard/developer/DeveloperSettingsClient.tsx @@ -87,6 +87,7 @@ export default function DeveloperSettingsClient({promise}:{promise:Promise router.push('/dashboard/projects?newProject=1')} onOpenProject={project => router.push(`/dashboard/projects?project=${encodeURIComponent(project.id)}`)} + onOpenProjectItem={item => router.push(`/dashboard/sources?type=${item.entity_type}&id=${encodeURIComponent(item.entity_id)}`)} onSignIn={() => router.push('/login?returnTo=%2Fdashboard%2Fdeveloper')} accountName={displayUser.name ?? displayUser.email} credits={credits} diff --git a/web/app/dashboard/projects/ProjectsClient.tsx b/web/app/dashboard/projects/ProjectsClient.tsx index 7c345b5..261958e 100644 --- a/web/app/dashboard/projects/ProjectsClient.tsx +++ b/web/app/dashboard/projects/ProjectsClient.tsx @@ -14,6 +14,7 @@ export function ProjectsClient({ promise }: { promise: Promise('selected-project', null); const [loading, setLoading] = useState(false), [error, setError] = useState(''), + [createError, setCreateError] = useState(''), [create, setCreate] = useState(false); const controller = useRef(null); const open = useCallback( @@ -41,16 +42,19 @@ export function ProjectsClient({ promise }: { promise: Promise { + const add = async (name: string): Promise => { + setCreateError(''); try { - await api('/v1/projects', { method: 'POST', body: JSON.stringify({ name }) }); - await resource.refresh(); + const project = await api('/v1/projects', { method: 'POST', body: JSON.stringify({ name }) }); + resource.setData(current => [project, ...current]); setCreate(false); + return true; } catch (cause) { - setError(cause instanceof Error ? cause.message : 'Could not create project.'); + setCreateError(cause instanceof Error ? cause.message : 'Could not create project.'); + return false; } }; return ( @@ -66,7 +70,7 @@ export function ProjectsClient({ promise }: { promise: Promise setCreate(true)} + onCreate={() => { setCreateError(''); setCreate(true); }} onOpen={(p) => void open(p)} onBack={() => { setSelected(null); @@ -78,12 +82,7 @@ export function ProjectsClient({ promise }: { promise: Promise )} - {create && ( - <> - setCreate(false)} onCreate={(name) => void add(name)} /> - {error &&

{error}

} - - )} + {create && { setCreate(false); setCreateError(''); }} onCreate={add} error={createError} />} ); } diff --git a/web/app/dashboard/sessions/SessionsClient.tsx b/web/app/dashboard/sessions/SessionsClient.tsx index e372b19..9c9f29b 100644 --- a/web/app/dashboard/sessions/SessionsClient.tsx +++ b/web/app/dashboard/sessions/SessionsClient.tsx @@ -36,8 +36,9 @@ export function AgentShell({ children }: { children: ReactNode }) { return
router.push(`/dashboard/${section === 'discover' ? 'sources' : section}`)} - onNewProject={() => router.push('/dashboard?section=projects')} - onOpenProject={() => router.push('/dashboard?section=projects')} + onNewProject={() => router.push('/dashboard/projects?newProject=1')} + onOpenProject={project => router.push(`/dashboard/projects?project=${encodeURIComponent(project.id)}`)} + onOpenProjectItem={item => router.push(`/dashboard/sources?type=${item.entity_type}&id=${encodeURIComponent(item.entity_id)}`)} onSignIn={() => router.push('/login?returnTo=%2Fdashboard%2Fsessions')} accountName={user?.name ?? user?.email} onSignOut={() => void signOut()} />
diff --git a/web/app/dashboard/settings/SettingsShell.tsx b/web/app/dashboard/settings/SettingsShell.tsx index 16a85c6..c6dcc06 100644 --- a/web/app/dashboard/settings/SettingsShell.tsx +++ b/web/app/dashboard/settings/SettingsShell.tsx @@ -18,6 +18,7 @@ export function SettingsShell({ children }: { children: ReactNode }) { router.push('/dashboard/projects?newProject=1')} onOpenProject={project => router.push(`/dashboard/projects?project=${encodeURIComponent(project.id)}`)} + onOpenProjectItem={item => router.push(`/dashboard/sources?type=${item.entity_type}&id=${encodeURIComponent(item.entity_id)}`)} onSignIn={() => router.push('/login')} accountName={user?.name ?? user?.email ?? (demoEnabled ? 'Local demo' : undefined)} credits={usage?.creditBalance} onSignOut={() => void signOut()} />
diff --git a/web/app/globals.css b/web/app/globals.css index 8758ecd..b0381f3 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -1039,6 +1039,13 @@ html:has(.developer-page) body { backdrop-filter: blur(.45rem); } +.new-project-dialog h2 { margin: 0 0 .375rem; } +.new-project-dialog .new-project-description { margin: 0 0 1.5rem; color: var(--color-dashboard-muted); font-size: .8125rem; } +.new-project-dialog .new-project-error { margin: .25rem 0 0; color: var(--color-dashboard-danger); font-size: .75rem; } +.new-project-dialog .button.primary { display: inline-flex; align-items: center; justify-content: center; gap: .5rem; min-width: 6.5rem; margin-top: .75rem; } +.new-project-dialog .button.primary:disabled { cursor: default; opacity: .65; } +.new-project-dialog .status-spinner { width: .875rem; height: .875rem; border-width: 2px; border-color: #ffffff66; border-top-color: #fff; } + .dialog-close, .back, .project-grid footer button,