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
7 changes: 0 additions & 7 deletions echo/api.http

This file was deleted.

8 changes: 0 additions & 8 deletions echo/check-later.md

This file was deleted.

7 changes: 7 additions & 0 deletions echo/frontend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ Cross-cutting rules (brand, UI, Directus, BFF, architecture, translations) live
- **2FA flow**: Directus surfaces it by returning `INVALID_OTP` — toggle a Mantine `PinInput` field and retry the same mutation. See `src/routes/auth/Login.tsx`
- **Transitions**: login/logout flows call `useTransitionCurtain().runTransition()` before navigation — animations expect the Directus mutation promise to be awaited

## Sidebar Navigation

- The sketch is canonical for the first-layer sidebar: Search and Inbox live at the top; Home and Organisations are the primary body; user-level settings sits directly below organisations; Help is an expanded footer utility list, not a pushed view.
- Bottom footer actions must not change the top sidebar context. Scope changes should come from body rows like organisations, workspaces, projects, inbox, or user settings.
- User settings are global. Organisation, workspace, and project settings stay inside their scope views.
- Do not link to `status.dembrane.com` until a status surface exists. The future status page should cover queue depth and backend health.

## Analytics (PostHog)

- `posthog-js` + `@posthog/react` are initialized in `src/main.tsx`; the app is wrapped in `PostHogProvider`
Expand Down
150 changes: 103 additions & 47 deletions echo/frontend/src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,23 @@ import {
createLazyRoute,
} from "./components/common/LazyRoute";
import { Protected } from "./components/common/Protected";
import { WorkspaceRedirect } from "./components/common/WorkspaceRedirect";
import { ErrorPage } from "./components/error/ErrorPage";
import { AuthLayout } from "./components/layout/AuthLayout";
// Layout components - keep as regular imports since they're used frequently
import { BaseLayout } from "./components/layout/BaseLayout";
import { WorkspaceLayout } from "./components/layout/WorkspaceLayout";
import { LanguageLayout } from "./components/layout/LanguageLayout";
import { ParticipantLayout } from "./components/layout/ParticipantLayout";
import { ProjectConversationLayout } from "./components/layout/ProjectConversationLayout";
import { ProjectLayout } from "./components/layout/ProjectLayout";
import { ProjectAccessGuard } from "./components/project/ProjectAccessGuard";
import { ProjectLibraryLayout } from "./components/layout/ProjectLibraryLayout";
import { ProjectOverviewLayout } from "./components/layout/ProjectOverviewLayout";
import { WorkspaceLayout } from "./components/layout/WorkspaceLayout";
import { ParticipantConversationAudioContent } from "./components/participant/ParticipantConversationAudioContent";
import { RefineSelection } from "./components/participant/refine/RefineSelection";
import { Verify } from "./components/participant/verify/Verify";
import { VerifyArtefact } from "./components/participant/verify/VerifyArtefact";
import { VerifySelection } from "./components/participant/verify/VerifySelection";
import { ProjectAccessGuard } from "./components/project/ProjectAccessGuard";
import {
ParticipantConversationAudioRoute,
ParticipantConversationTextRoute,
Expand All @@ -30,12 +29,18 @@ import { ParticipantPostConversation } from "./routes/participant/ParticipantPos
import { ParticipantStartRoute } from "./routes/participant/ParticipantStart";
import { ProjectConversationOverviewRoute } from "./routes/project/conversation/ProjectConversationOverview";
import { ProjectConversationTranscript } from "./routes/project/conversation/ProjectConversationTranscript";
import { ProjectHomeRoute } from "./routes/project/ProjectHomeRoute";
// Tab-based routes - import directly for now to debug
import {
ProjectAccessRoute,
ProjectConversationsRoute,
ProjectIntegrationsRoute,
ProjectPortalSettingsRoute,
ProjectSettingsRoute,
ProjectUploadRoute,
} from "./routes/project/ProjectRoutes";
import { SidebarPreviewLayout } from "./routes/sidebar-preview/SidebarPreviewLayout";
import { SidebarPreviewRoute } from "./routes/sidebar-preview/SidebarPreviewRoute";

// Lazy-loaded route components
const ProjectsHomeRoute = createLazyNamedRoute(
Expand Down Expand Up @@ -150,7 +155,7 @@ const AdminSettingsRoute = createLazyNamedRoute(
// Project route children — shared between /projects and /w/:workspaceId/projects
const projectRouteChildren = [
{
element: <ProjectsHomeRoute />,
element: <Navigate to="../home" replace />,
index: true,
},
{
Expand All @@ -161,10 +166,14 @@ const projectRouteChildren = [
children: [
{
children: [
{
element: <ProjectHomeRoute />,
path: "home",
},
{
children: [
{
element: <Navigate to="portal-editor" replace />,
element: <Navigate to="home" replace />,
index: true,
},
{
Expand Down Expand Up @@ -226,7 +235,6 @@ const projectRouteChildren = [
element: <ProjectConversationLayout />,
path: "conversation/:conversationId",
},

{
children: [
{
Expand All @@ -249,6 +257,22 @@ const projectRouteChildren = [
element: <ProjectReportRoute />,
path: "report",
},
{
element: <ProjectConversationsRoute />,
path: "conversations",
},
{
element: <ProjectUploadRoute />,
path: "upload",
},
{
element: <Navigate to="../integrations" replace />,
path: "export",
},
{
element: <ProjectIntegrationsRoute />,
path: "integrations",
},
{
element: <DebugPage />,
path: "debug",
Expand All @@ -269,7 +293,9 @@ export const mainRouter = createBrowserRouter([
{
children: [
{
element: <Navigate to="projects" />,
// Root → workspace selector. Legacy /projects routes are gone;
// the canonical "home" for an authed user is /w.
element: <Navigate to="w" replace />,
path: "",
},
{
Expand Down Expand Up @@ -355,17 +381,6 @@ export const mainRouter = createBrowserRouter([
element: <CreateWorkspaceRoute />,
path: "new",
},
{
element: <Navigate to="settings" replace />,
path: ":workspaceId",
},
{
// Splat so the tab lives in the path
// (/w/:workspaceId/settings/:tab). The component parses
// the trailing segment.
element: <WorkspaceSettingsRoute />,
path: ":workspaceId/settings/*",
},
],
element: (
<Protected>
Expand Down Expand Up @@ -393,23 +408,37 @@ export const mainRouter = createBrowserRouter([
),
path: "o",
},
{
// Host Guide - standalone page, protected but no header/layout
element: (
<Protected>
<HostGuidePage />
</Protected>
),
path: "projects/:projectId/host-guide",
},
{
// Workspace-scoped projects: /w/:workspaceId/projects/...
// This is the PRIMARY route — workspace ID in URL makes it shareable
// SINGLE canonical shape — legacy /projects/:projectId is gone.
children: [
{
children: projectRouteChildren,
element: <Navigate to="home" replace />,
index: true,
},
{
element: <HostGuidePage />,
path: "projects/:projectId/host-guide",
},
{
children: [
{
element: <ProjectsHomeRoute />,
path: "home",
},
{
// Splat so the tab lives in the path
// (/w/:workspaceId/settings/:tab). The component parses
// the trailing segment.
element: <WorkspaceSettingsRoute />,
path: "settings/*",
},
{
children: projectRouteChildren,
path: "projects",
},
],
element: <BaseLayout />,
path: "projects",
},
],
element: (
Expand All @@ -420,28 +449,14 @@ export const mainRouter = createBrowserRouter([
path: "w/:workspaceId",
},
{
// Legacy /projects — redirects to /w/:workspaceId/projects
// Kept for backward compat (bookmarks, existing links)
children: [
{
element: <WorkspaceRedirect />,
element: <UserSettingsRoute />,
index: true,
},
// Direct project access still works (falls through to v1)
...projectRouteChildren.slice(1),
],
element: (
<Protected>
<BaseLayout />
</Protected>
),
path: "projects",
},
{
children: [
{
element: <UserSettingsRoute />,
index: true,
path: ":section",
},
],
element: (
Expand All @@ -456,7 +471,7 @@ export const mainRouter = createBrowserRouter([
// Client-side guard lives inside AdminSettingsRoute (reads
// meV2.is_staff); backend /v2/admin/* also gates on is_admin.
children: [
{ element: <AdminSettingsRoute />, index: true },
{ element: <Navigate to="usage-and-billing" replace />, index: true },
{ element: <AdminSettingsRoute />, path: ":tab" },
],
element: (
Expand All @@ -466,6 +481,47 @@ export const mainRouter = createBrowserRouter([
),
path: "admin",
},
{
// Sidebar preview — feature/sidebar work-in-progress. No auth gate
// so the design can be poked at without sign-in friction. Remove
// or fold into real layouts once the sidebar replaces production
// chrome.
children: [
{ element: <SidebarPreviewRoute />, index: true },
{ element: <SidebarPreviewRoute />, path: "settings/:section" },
{ element: <SidebarPreviewRoute />, path: "o/:orgId" },
{
element: <SidebarPreviewRoute />,
path: "o/:orgId/settings/:section",
},
{
element: <SidebarPreviewRoute />,
path: "w/:workspaceId/home",
},
{
element: <SidebarPreviewRoute />,
path: "w/:workspaceId/projects",
},
{
element: <SidebarPreviewRoute />,
path: "w/:workspaceId/settings/:section",
},
{
element: <SidebarPreviewRoute />,
path: "w/:workspaceId/projects/:projectId/home",
},
{
element: <SidebarPreviewRoute />,
path: "w/:workspaceId/projects/:projectId/conversations",
},
{
element: <SidebarPreviewRoute />,
path: "w/:workspaceId/projects/:projectId/settings/:section",
},
],
element: <SidebarPreviewLayout />,
path: "sidebar-preview",
},
{
element: <ErrorPage />,
path: "*",
Expand Down
4 changes: 2 additions & 2 deletions echo/frontend/src/components/aspect/AspectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const AspectCard = ({
data: Aspect;
className?: string;
}) => {
const { projectId } = useParams();
const { projectId, workspaceId } = useParams();

const project = useProjectById({
projectId: projectId ?? "",
Expand All @@ -26,7 +26,7 @@ export const AspectCard = ({
<Box className="relative mb-2 place-self-stretch">
<LoadingOverlay visible={project.isLoading} />
<I18nLink
to={`/projects/${projectId}/library/views/${data.view_id}/aspects/${data.id}`}
to={`/w/${workspaceId}/projects/${projectId}/library/views/${data.view_id}/aspects/${data.id}`}
>
<Paper
bg="var(--app-background)"
Expand Down
15 changes: 15 additions & 0 deletions echo/frontend/src/components/auth/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useLocation, useSearchParams } from "react-router";
import { toast } from "@/components/common/Toaster";
import { ADMIN_BASE_URL, API_BASE_URL } from "@/config";
import { useI18nNavigate } from "@/hooks/useI18nNavigate";
import { emitAuthCacheBoundary } from "@/lib/authCacheBoundary";
import { directus } from "@/lib/directus";
import { isAuthPath } from "../utils/authPaths";
import { throwWithMessage } from "../utils/errorUtils";
Expand Down Expand Up @@ -193,9 +194,22 @@ export const useLoginMutation = () => {
);
},
onSuccess: async () => {
queryClient.removeQueries({ queryKey: ["users", "me"] });
queryClient.removeQueries({ queryKey: ["v2", "workspaces"] });
queryClient.removeQueries({ queryKey: ["v2", "workspaces-context"] });
if (typeof window !== "undefined") {
try {
sessionStorage.removeItem("dembrane_ws_selected");
} catch {}
}
emitAuthCacheBoundary();
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["auth", "session"] }),
queryClient.invalidateQueries({ queryKey: ["users", "me"] }),
queryClient.invalidateQueries({ queryKey: ["v2", "workspaces"] }),
queryClient.invalidateQueries({
queryKey: ["v2", "workspaces-context"],
}),
]);
},
});
Expand Down Expand Up @@ -241,6 +255,7 @@ export const useLogoutMutation = () => {
sessionStorage.removeItem("dembrane_ws_selected");
} catch {}
}
emitAuthCacheBoundary();
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["auth", "session"] });
Expand Down
7 changes: 4 additions & 3 deletions echo/frontend/src/components/chat/ChatAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const ChatAccordionItemMenu = ({
const deleteChatMutation = useDeleteChatMutation();
const updateChatMutation = useUpdateChatMutation();
const navigate = useI18nNavigate();
const { workspaceId } = useParams();
const [
deleteConfirmOpened,
{ open: openDeleteConfirm, close: closeDeleteConfirm },
Expand Down Expand Up @@ -183,7 +184,7 @@ export const ChatAccordionItemMenu = ({
chatId: chat.id ?? "",
projectId: (chat.project_id as string) ?? "",
});
navigate(`/projects/${chat.project_id}/overview`);
navigate(`/w/${workspaceId}/projects/${chat.project_id}/overview`);
closeDeleteConfirm();
}}
data-testid="chat-delete-modal"
Expand All @@ -194,7 +195,7 @@ export const ChatAccordionItemMenu = ({

// Chat Accordion
export const ChatAccordionMain = ({ projectId }: { projectId: string }) => {
const { chatId: activeChatId } = useParams();
const { chatId: activeChatId, workspaceId } = useParams();
const { ref: loadMoreRef, inView } = useInView();

const chatsQuery = useInfiniteProjectChats(
Expand Down Expand Up @@ -309,7 +310,7 @@ export const ChatAccordionMain = ({ projectId }: { projectId: string }) => {
return (
<NavigationButton
key={item.id}
to={`/projects/${projectId}/chats/${item.id}`}
to={`/w/${workspaceId}/projects/${projectId}/chats/${item.id}`}
active={isActive}
rightSection={
<Group gap="xs" wrap="nowrap">
Expand Down
Loading
Loading