Skip to content
3 changes: 3 additions & 0 deletions apps/apollo-vertex/app/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export default {
"data-querying": "Data Querying",
localization: "Localization",
mcp: "MCP Server",
"guided-buying": {
display: "hidden",
},
auth_callback: {
display: "hidden",
},
Expand Down
4 changes: 4 additions & 0 deletions apps/apollo-vertex/app/experiment/_meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
index: { display: "hidden" },
"guided-buying": "Guided Buying",
};
18 changes: 18 additions & 0 deletions apps/apollo-vertex/app/experiment/guided-buying/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Guided Buying

An experimental prototype exploring a guided buying experience built on the
Apollo Vertex shell. The prototype opens in its own full-screen window with a
minimal shell and the following sections: **Dashboard**, **Buy**, **Catalog**,
and **Workbench**.

<a
href="/guided-buying"
target="_blank"
rel="noreferrer"
className="not-prose inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground no-underline transition-colors hover:bg-primary/90"
>
Open the prototype ↗
</a>

The prototype launches in a new tab so it can take over the full viewport
without the documentation chrome.
37 changes: 37 additions & 0 deletions apps/apollo-vertex/app/guided-buying/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { useEffect } from "react";
import { createRoot } from "react-dom/client";
import { ThemeProvider } from "@/registry/shell/shell-theme-provider";
import { GuidedBuyingShell } from "@/templates/guided-buying/GuidedBuyingShell";

export default function GuidedBuyingPage() {
// The shell is a full-screen client app. Rendering it through Next's tree
// puts it inside Nextra's docs layout, whose interrupted hydration leaves the
// subtree stuck `display:none`. So we mount it in a *separate* React root
// attached to <body> — built by a fresh client render (no hydration), fully
// outside Nextra's tree and Suspense, so nothing can hide it. It carries its
// own ThemeProvider (normally supplied by the app's root ThemeWrapper).
useEffect(() => {
const container = document.createElement("div");
document.body.append(container);
const root = createRoot(container);
root.render(
<ThemeProvider storageKey="theme">
<div className="fixed inset-0 z-50 bg-background">
<GuidedBuyingShell />
</div>
</ThemeProvider>,
);
return () => {
root.unmount();
container.remove();
};
}, []);

return (
<div className="fixed inset-0 z-50 flex h-screen items-center justify-center bg-background text-sm text-muted-foreground">
Loading prototype…
</div>
);
}
5 changes: 5 additions & 0 deletions apps/apollo-vertex/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
"autopilot_empty_description": "Ask me anything about your automation data.",
"bad_response": "Bad response",
"business_user": "Business user",
"buy": "Buy",
"cancel": "Cancel",
"card_component": "Card Component",
"catalog": "Catalog",
"changes_apply_instantly": "Changes apply instantly",
"chart_render_failed_title": "Couldn't render this chart",
"chat_messages": "Chat messages",
Expand Down Expand Up @@ -108,6 +110,7 @@
"projects": "Projects",
"remove_file": "Remove {{name}}",
"remove_quoted_text": "Remove quoted text",
"requests": "Requests",
"reset_to_default": "Reset to Default",
"retry": "Retry",
"romanian": "Romanian",
Expand Down Expand Up @@ -139,6 +142,7 @@
"start_conversation_with": "Start a conversation with {{name}}",
"stop": "Stop",
"success": "Success",
"switch_user": "Switch user",
"system": "System",
"table_no_valid_fields": "None of the requested fields exist on {{entity}}: {{fields}}.",
"team": "Team",
Expand Down Expand Up @@ -166,6 +170,7 @@
"view_customer": "View customer",
"view_payment_details": "View payment details",
"warning": "Warning",
"workbench": "Workbench",
"wrong_dimension_type_in_entity": "Field \"{{field}}\" on {{entity}} is {{actual}}; this chart needs a {{expected}} field.",
"wrong_dimension_type_in_joined": "Field \"{{field}}\" is {{actual}}; this chart needs a {{expected}} field.",
"wrong_metric_type_in_entity": "Field \"{{field}}\" on {{entity}} is {{actual}}; {{aggregation}} requires a numeric field.",
Expand Down
2 changes: 1 addition & 1 deletion apps/apollo-vertex/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ export function AiChatMessageActions({
<div
className={cn(
"flex items-center gap-0.5 -ml-[7px] transition-opacity",
isLatest
? "opacity-100"
: "opacity-0 group-hover/message:opacity-100 group-focus-within/message:opacity-100 has-[[data-state=delayed-open]]:opacity-100 has-[[data-state=instant-open]]:opacity-100",
// Hover-only (also revealed on keyboard focus / open tooltip) to cut
// persistent noise — including on the latest message.
"opacity-0 group-hover/message:opacity-100 group-focus-within/message:opacity-100 has-[[data-state=delayed-open]]:opacity-100 has-[[data-state=instant-open]]:opacity-100",
)}
>
{content.trim() && <CopyToClipboardButton value={content} />}
Expand Down
14 changes: 4 additions & 10 deletions apps/apollo-vertex/registry/ai-chat/components/ai-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export function AiChat<
{messages.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center min-h-0">
<div className="w-full">
<div className="px-4 text-center mb-7">
<div className="px-4 text-center mb-5">
{emptyState ?? defaultEmptyState}
</div>
<AiChatInput {...sharedInputProps} hasMessages={false} />
Expand All @@ -262,22 +262,16 @@ export function AiChat<
</div>
) : (
<div className="relative flex-1 min-h-0">
<div
className="absolute top-0 left-0 right-0 h-6 z-10 pointer-events-none"
style={{
background:
"linear-gradient(to bottom, var(--background) 0%, transparent 100%)",
}}
aria-hidden="true"
/>
<div
ref={attachScrollListeners}
role="log"
aria-label={t("chat_messages")}
aria-live="polite"
aria-atomic="false"
aria-busy={isInFlight}
className="relative h-full overflow-y-auto py-4 pl-10 pr-10"
// Fade content out at the top edge via an alpha mask (works over any
// background, unlike a solid-color gradient overlay).
className="relative h-full overflow-y-auto py-4 pl-10 pr-10 [scrollbar-gutter:stable] [mask-image:linear-gradient(to_bottom,transparent_0,#000_24px)] [-webkit-mask-image:linear-gradient(to_bottom,transparent_0,#000_24px)]"
>
{enableTextSelection && (
<AiChatSelectionMenu
Expand Down
2 changes: 1 addition & 1 deletion apps/apollo-vertex/registry/card/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function Card({
className={cn(
"absolute inset-0 rounded-2xl pointer-events-none blur-xl",
"transition-opacity duration-150",
selected ? "opacity-100" : "opacity-0",
selected ? "opacity-50" : "opacity-0",
)}
style={{ background: "var(--ai-gradient)" }}
/>
Expand Down
4 changes: 4 additions & 0 deletions apps/apollo-vertex/registry/shell/shell-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface ShellLayoutProps {
variant?: "minimal";
companyLogo?: CompanyLogo;
navItems: ShellNavItem[];
onUserClick?: () => void;
}

function DarkGradientBackground() {
Expand Down Expand Up @@ -177,6 +178,7 @@ export function ShellLayout({
variant,
companyLogo,
navItems,
onUserClick,
}: PropsWithChildren<ShellLayoutProps>) {
if (variant === "minimal") {
return (
Expand All @@ -189,6 +191,7 @@ export function ShellLayout({
productName={productName}
companyLogo={companyLogo}
navItems={navItems}
onUserClick={onUserClick}
/>
<div className="flex-1 overflow-y-auto custom-scrollbar">
{children}
Expand All @@ -210,6 +213,7 @@ export function ShellLayout({
productName={productName}
companyLogo={companyLogo}
navItems={navItems}
onUserClick={onUserClick}
/>
<SidebarInset className="relative flex-1 flex flex-col overflow-hidden rounded-none m-0 ml-0 shadow-none bg-transparent">
<header className="flex items-center h-12 px-4 md:hidden">
Expand Down
8 changes: 7 additions & 1 deletion apps/apollo-vertex/registry/shell/shell-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface ShellSidebarProps {
variant?: "minimal";
companyLogo?: CompanyLogo;
navItems: ShellNavItem[];
onUserClick?: () => void;
}

export const ShellSidebar = ({
Expand All @@ -55,6 +56,7 @@ export const ShellSidebar = ({
variant,
companyLogo,
navItems,
onUserClick,
}: ShellSidebarProps) => {
if (variant === "minimal") {
return (
Expand Down Expand Up @@ -82,6 +84,7 @@ export const ShellSidebar = ({
isCollapsed
collapsedMenuSide="bottom"
collapsedMenuAlign="end"
onUserClick={onUserClick}
/>
</div>
</header>
Expand All @@ -94,6 +97,7 @@ export const ShellSidebar = ({
productName={productName}
companyLogo={companyLogo}
navItems={navItems}
onUserClick={onUserClick}
/>
);
};
Expand All @@ -103,13 +107,15 @@ interface SidebarNavProps {
productName: string;
companyLogo?: CompanyLogo;
navItems: ShellNavItem[];
onUserClick?: () => void;
}

function SidebarNav({
companyName,
productName,
companyLogo,
navItems,
onUserClick,
}: SidebarNavProps) {
const { t } = useTranslation();
const { state, toggleSidebar } = useSidebar();
Expand Down Expand Up @@ -338,7 +344,7 @@ function SidebarNav({
</SidebarContent>

<SidebarFooter className="p-4 pt-0">
<UserProfile isCollapsed={isCollapsed} />
<UserProfile isCollapsed={isCollapsed} onUserClick={onUserClick} />
</SidebarFooter>

<div
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Globe, LogOut, Monitor, Moon, Sun } from "lucide-react";
import {
ArrowLeftRight,
Globe,
LogOut,
Monitor,
Moon,
Sun,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import {
DropdownMenuItem,
Expand All @@ -15,7 +22,14 @@ import type { LanguageChangedEvent } from "./shell-constants";
import { Text } from "./shell-text";
import { useTheme } from "./shell-theme-provider";

export const UserProfileMenuItems = () => {
interface UserProfileMenuItemsProps {
/** When set, a "Switch user" item appears at the top (e.g. demo seats). */
onSwitchUser?: () => void;
}

export const UserProfileMenuItems = ({
onSwitchUser,
}: UserProfileMenuItemsProps) => {
const { t, i18n } = useTranslation();
const { logout } = useAuth();
const { setTheme } = useTheme();
Expand All @@ -31,6 +45,15 @@ export const UserProfileMenuItems = () => {

return (
<>
{onSwitchUser && (
<>
<DropdownMenuItem onClick={onSwitchUser}>
<ArrowLeftRight className="w-4 h-4" />
<span>{t("switch_user")}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Sun className="w-4 h-4 dark:hidden" />
Expand Down
7 changes: 5 additions & 2 deletions apps/apollo-vertex/registry/shell/shell-user-profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ interface UserProfileProps {
isCollapsed: boolean;
collapsedMenuSide?: "top" | "right" | "bottom" | "left";
collapsedMenuAlign?: "start" | "center" | "end";
/** When set, a "Switch user" item appears in the menu (e.g. demo seats). */
onUserClick?: () => void;
}

export const UserProfile = ({
isCollapsed,
collapsedMenuSide = "top",
collapsedMenuAlign = "start",
onUserClick,
}: UserProfileProps) => {
const { t } = useTranslation();
const { user } = useUser();
Expand Down Expand Up @@ -73,7 +76,7 @@ export const UserProfile = ({
</div>
</div>
<DropdownMenuSeparator />
<UserProfileMenuItems />
<UserProfileMenuItems onSwitchUser={onUserClick} />
</DropdownMenuContent>
</DropdownMenu>
) : (
Expand Down Expand Up @@ -110,7 +113,7 @@ export const UserProfile = ({
side="top"
sideOffset={8}
>
<UserProfileMenuItems />
<UserProfileMenuItems onSwitchUser={onUserClick} />
</DropdownMenuContent>
</DropdownMenu>
)}
Expand Down
15 changes: 13 additions & 2 deletions apps/apollo-vertex/registry/shell/shell-user-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export const useUser = () => {

export const UserContext = createContext<UserContextValue | null>(null);

export const ShellUserProvider: FC<PropsWithChildren> = ({ children }) => {
export const ShellUserProvider: FC<
PropsWithChildren<{ userOverride?: User | null }>
> = ({ children, userOverride }) => {
const { user: authUser, isAuthenticated, isLoading } = useAuth();
const [user, setUser] = useState<User | null>(null);

Expand All @@ -52,8 +54,17 @@ export const ShellUserProvider: FC<PropsWithChildren> = ({ children }) => {
}
}, [authUser]);

// A caller-supplied identity (e.g. a demo "seat") wins over the auth user.
const effectiveUser = userOverride ?? user;

return (
<UserContext.Provider value={{ user, isAuthenticated, isLoading }}>
<UserContext.Provider
value={{
user: effectiveUser,
isAuthenticated: userOverride != null || isAuthenticated,
isLoading,
}}
>
{children}
</UserContext.Provider>
);
Expand Down
Loading
Loading