Skip to content
Draft
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
27 changes: 27 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,29 @@
# The Graph (optional — falls back to hardcoded values)
THEGRAPH_API_KEY=

# Discovery Service — Explore capability catalog (legacy pipelines by default).
DISCOVERY_SERVICE_URL=https://discovery-service-production-8955.up.railway.app

# PymtHouse OIDC (device login + signer JWT exchange facade)
PYMTHOUSE_ISSUER_URL=http://localhost:3001/api/v1/oidc
# Public app client id (OAuth client_id) — used for usage/balance Builder API calls
PYMTHOUSE_PUBLIC_CLIENT_ID=
PYMTHOUSE_M2M_CLIENT_ID=
PYMTHOUSE_M2M_CLIENT_SECRET=
# Initiate login URI for device flow (register on pymthouse app):
# http://localhost:3002/api/auth/initiate-login
# Public signer API for clients (pymthouse proxy — records usage). Not raw DMZ :8080.
PYMTHOUSE_SIGNER_URL=http://localhost:3001/api/signer
# Set to 1 for local dev when issuer uses http://127.0.0.1
PYMTHOUSE_ALLOW_INSECURE_HTTP=

# Browser gateway relay (orchestrator LV2V via same-origin HTTP segments)
GATEWAY_ENABLED=0
NEXT_PUBLIC_GATEWAY_ENABLED=0
# Gateway server calls signer on the same host:port as the dashboard (proxied to PYMTHOUSE_SIGNER_URL upstream)
GATEWAY_SIGNER_FROM_REQUEST_ORIGIN=1
# Optional overrides (defaults to PYMTHOUSE_SIGNER_URL + LIVEPEER_DISCOVERY_SERVICE_URL)
# GATEWAY_DISCOVERY_URL=
# GATEWAY_DISCOVERY_TIMEOUT_MS=60000
# GATEWAY_USE_TOFU=1
# GATEWAY_PAYMENT_INTERVAL_MS=2000
6 changes: 3 additions & 3 deletions app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Metadata } from "next";
import type { CSSProperties } from "react";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { AuthProvider } from "@/components/dashboard/AuthContext";
import { DashboardProviders } from "@/components/dashboard/DashboardProviders";
import { ThemeProvider } from "@/components/dashboard/ThemeContext";
import DashboardSidebar from "@/components/dashboard/DashboardSidebar";
import KeyboardShortcuts from "@/components/dashboard/KeyboardShortcuts";
Expand Down Expand Up @@ -58,7 +58,7 @@ export default function DashboardLayout({
paints. ThemeProvider below takes over post-hydration. */}
<script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
<ThemeProvider>
<AuthProvider>
<DashboardProviders>
<div
className={`flex min-h-screen flex-col bg-dark font-sans md:h-screen md:min-h-0 md:flex-row md:overflow-hidden ${GeistSans.variable} ${GeistMono.variable}`}
style={dashboardOverrides}
Expand All @@ -69,7 +69,7 @@ export default function DashboardLayout({
</div>
<KeyboardShortcuts />
</div>
</AuthProvider>
</DashboardProviders>
</ThemeProvider>
</>
);
Expand Down
54 changes: 45 additions & 9 deletions app/(app)/models/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,22 @@ import JobsTable from "@/components/dashboard/JobsTable";
import StatusDot from "@/components/dashboard/StatusDot";
import Tooltip from "@/components/design-system/Tooltip";
import { useStarredModels } from "@/lib/dashboard/useStarredModels";
import {
getModelById,
SETTINGS_API_KEYS,
MOCK_RECENT_REQUESTS,
} from "@/lib/dashboard/mock-data";
import { SETTINGS_API_KEYS, MOCK_RECENT_REQUESTS } from "@/lib/dashboard/mock-data";
import { useDiscoveryModel } from "@/lib/dashboard/useDiscoveryModel";
import DashboardPageSkeleton from "@/components/dashboard/DashboardPageSkeleton";
import { getModelIcon, formatRuns, formatPrice } from "@/lib/dashboard/utils";
import PlaygroundForm from "@/components/dashboard/playground/PlaygroundForm";
import JsonInput from "@/components/dashboard/playground/JsonInput";
import PlaygroundOutput from "@/components/dashboard/playground/PlaygroundOutput";
import TranscodingOutput from "@/components/dashboard/playground/TranscodingOutput";
import CodeSnippets from "@/components/dashboard/playground/CodeSnippets";
import WebcamPlayground from "@/components/dashboard/playground/WebcamPlayground";
import LiveStreamPlayground from "@/components/dashboard/playground/LiveStreamPlayground";
import { isGatewayEnabledPublic } from "@/lib/dashboard/gateway-public";
import {
getStreamingFacadeEndpointLabel,
isStreamingCapabilityModel,
} from "@/lib/dashboard/sdk-streaming-example";
import ModelAnalytics from "@/components/dashboard/stats/ModelAnalytics";
import type { Model } from "@/lib/dashboard/types";

Expand Down Expand Up @@ -156,6 +160,9 @@ function PlaygroundTab({ model }: { model: Model }) {
}

if (model.playgroundConfig.playgroundVariant === "webcam") {
if (isGatewayEnabledPublic()) {
return <LiveStreamPlayground model={model} />;
}
return <WebcamPlayground model={model} />;
}

Expand Down Expand Up @@ -289,8 +296,11 @@ function PlaygroundTab({ model }: { model: Model }) {

function ApiTab({ model }: { model: Model }) {
const baseUrl = model.apiEndpoint ?? "https://gateway.livepeer.org/v1";
const endpoint =
model.category === "Language"
const gatewayStreaming =
isGatewayEnabledPublic() && isStreamingCapabilityModel(model);
const endpoint = gatewayStreaming
? getStreamingFacadeEndpointLabel(model)
: model.category === "Language"
? `${baseUrl}/chat/completions`
: `${baseUrl}/${model.id}`;
const defaultKey =
Expand Down Expand Up @@ -684,7 +694,8 @@ function ModelIdChip({ modelId }: { modelId: string }) {
export default function ModelDetailPage() {
const { id } = useParams<{ id: string }>();
const [activeTab, setActiveTab] = useState<Tab>("playground");
const model = getModelById(id);
const discovery = useDiscoveryModel(id);
const model = discovery.status === "ready" ? discovery.model : undefined;

// Jobs filtered to this model — drives both the Jobs panel and the count
// chip on the Jobs tab. `model` may be undefined here (404 path below); we
Expand All @@ -705,11 +716,36 @@ export default function ModelDetailPage() {
[filteredRuns.length],
);

if (discovery.status === "loading") {
return (
<main id="main-content" className="flex flex-1 flex-col bg-dark">
<DashboardPageSkeleton maxWidth="5xl" withTabs kpiCount={0} withChart={false} />
</main>
);
}

if (discovery.status === "error") {
return (
<main id="main-content" className="flex flex-1 flex-col bg-dark">
<div className="flex flex-1 flex-col items-center justify-center px-5 text-center">
<p className="text-sm text-fg-muted">Could not load capability from Discovery Service.</p>
<p className="mt-2 max-w-md font-mono text-xs text-fg-faint">{discovery.message}</p>
<Link
href="/"
className="mt-6 text-xs text-green-bright hover:underline focus:outline-none rounded"
>
Back to Explore
</Link>
</div>
</main>
);
}

if (!model) {
return (
<main id="main-content" className="flex flex-1 flex-col bg-dark">
<div className="flex flex-1 flex-col items-center justify-center text-center">
<p className="text-sm text-fg-label">Model not found</p>
<p className="text-sm text-fg-label">Capability not found</p>
<Link
href="/"
className="mt-3 text-xs text-green-bright hover:underline focus:outline-none rounded"
Expand Down
53 changes: 47 additions & 6 deletions app/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
Star,
Search,
} from "lucide-react";
import { MODELS } from "@/lib/dashboard/mock-data";
import Button from "@/components/design-system/Button";
import { useExploreModels } from "@/lib/dashboard/useExploreModels";
import Drawer from "@/components/design-system/Drawer";
import { getModelIcon, formatRuns } from "@/lib/dashboard/utils";
import { useStarredModels } from "@/lib/dashboard/useStarredModels";
Expand Down Expand Up @@ -391,7 +391,27 @@ export default function ExplorePage() {
);
}

function ExploreLoadError({
message,
onRetry,
}: {
message: string;
onRetry: () => void;
}) {
return (
<div className="flex flex-1 flex-col items-center justify-center px-5 py-24 text-center">
<p className="text-sm text-fg-muted">Could not load capabilities from Discovery Service.</p>
<p className="mt-2 max-w-md font-mono text-xs text-fg-faint">{message}</p>
<Button className="mt-6" variant="secondary" size="sm" onClick={onRetry}>
Retry
</Button>
</div>
);
}

function ExplorePageInner() {
const exploreState = useExploreModels();
const { status, models, reload } = exploreState;
const searchParams = useSearchParams();
const initialCategory = (() => {
const qp = searchParams.get("category");
Expand All @@ -412,12 +432,12 @@ function ExplorePageInner() {
const [priceMin, setPriceMin] = useState(0);
const [priceMax, setPriceMax] = useState(100);
const dataMaxPrice = useMemo(
() => Math.max(...MODELS.map((m) => m.pricing.amount), 0.01),
[],
() => Math.max(...models.map((m) => m.pricing.amount), 0.01),
[models],
);

const filtered = useMemo(() => {
const result = MODELS.filter((m) => {
const result = models.filter((m) => {
if (availabilityFilter === "warm" && m.status !== "hot") return false;
if (availabilityFilter === "cold" && m.status !== "cold") return false;
if (favoritesOnly && !isStarred(m.id)) return false;
Expand All @@ -443,7 +463,28 @@ function ExplorePageInner() {
});

return result;
}, [search, category, availabilityFilter, favoritesOnly, isStarred, priceMin, priceMax, dataMaxPrice]);
}, [models, search, category, availabilityFilter, favoritesOnly, isStarred, priceMin, priceMax, dataMaxPrice]);

if (status === "loading" && models.length === 0) {
return (
<main id="main-content" className="flex flex-1 flex-col bg-dark">
<DashboardPageHeader title="Explore" icon={LayoutGrid} />
<DashboardPageSkeleton maxWidth="7xl" withTabs kpiCount={0} withChart={false} />
</main>
);
}

if (status === "error") {
return (
<main id="main-content" className="flex flex-1 flex-col bg-dark">
<DashboardPageHeader title="Explore" icon={LayoutGrid} />
<ExploreLoadError
message={exploreState.status === "error" ? exploreState.error : "Unknown error"}
onRetry={reload}
/>
</main>
);
}

const activeFilters = [
...(category
Expand Down Expand Up @@ -717,7 +758,7 @@ function ExplorePageInner() {
min={priceMin}
max={priceMax}
onChange={(min, max) => { setPriceMin(min); setPriceMax(max); }}
models={MODELS}
models={models}
/>
</div>

Expand Down
2 changes: 1 addition & 1 deletion app/(app)/usage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function UsageContent() {
<DashboardPageHeader
title="Usage"
icon={BarChart3}
description="Requests, latency, errors, and spend across your API tokens."
description="Signed requests, network cost, and Starter allowance from PymtHouse OpenMeter."
actions={
<>
<button
Expand Down
29 changes: 29 additions & 0 deletions app/(auth)/device-approved/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Link from "next/link";
import { LivepeerSymbol } from "@/components/design-system/LivepeerLogo";

export default function DeviceApprovedPage() {
return (
<main className="flex min-h-screen flex-col items-center justify-center bg-dark px-6">
<div className="w-full max-w-md rounded-xl border border-hairline bg-dark-card p-8 text-center">
<div className="mb-5 flex justify-center">
<LivepeerSymbol className="h-9 w-9" />
</div>
<h1 className="text-2xl font-semibold tracking-tight text-fg">
Device login approved
</h1>
<p className="mt-3 text-sm leading-relaxed text-fg-muted">
You can return to your terminal. The python-gateway device flow should
finish automatically in a few seconds.
</p>
<div className="mt-6">
<Link
href="/home"
className="inline-flex rounded-full bg-green-bright px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-green-light"
>
Open dashboard
</Link>
</div>
</div>
</main>
);
}
6 changes: 3 additions & 3 deletions app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Metadata } from "next";
import type { CSSProperties } from "react";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { AuthProvider } from "@/components/dashboard/AuthContext";
import { DashboardProviders } from "@/components/dashboard/DashboardProviders";

export const metadata: Metadata = {
title: "Sign in — Livepeer Developer Dashboard",
Expand All @@ -23,13 +23,13 @@ export default function DashboardAuthLayout({
children: React.ReactNode;
}) {
return (
<AuthProvider>
<DashboardProviders>
<div
className={`min-h-screen bg-dark font-sans ${GeistSans.variable} ${GeistMono.variable}`}
style={geistOverride}
>
{children}
</div>
</AuthProvider>
</DashboardProviders>
);
}
22 changes: 16 additions & 6 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { Suspense, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useAuth } from "@/components/dashboard/AuthContext";
import LoginPage from "@/components/dashboard/LoginPage";

export default function LoginRoute() {
function LoginRouteInner() {
const { isConnected } = useAuth();
const router = useRouter();
const searchParams = useSearchParams();
const deviceFlow = searchParams.get("flow") === "device";

useEffect(() => {
if (isConnected) {
if (isConnected && !deviceFlow) {
router.replace("/home");
}
}, [isConnected, router]);
}, [isConnected, deviceFlow, router]);

if (isConnected) return null;
if (isConnected && !deviceFlow) return null;

return <LoginPage />;
}

export default function LoginRoute() {
return (
<Suspense fallback={null}>
<LoginRouteInner />
</Suspense>
);
}
18 changes: 10 additions & 8 deletions app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
"use client";

import { useEffect } from "react";
import { Suspense, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/components/dashboard/AuthContext";
import LoginPage from "@/components/dashboard/LoginPage";

/**
* Signup route — sibling of `/login`. Renders the same
* `LoginPage` component but seeds it with `initialMode="signup"`. The
* footer toggle inside the page is a `<Link>` to `/login`, so
* URL and visible mode stay in sync without query-param trickery.
*/
export default function SignupRoute() {
function SignupRouteInner() {
const { isConnected } = useAuth();
const router = useRouter();

Expand All @@ -25,3 +19,11 @@ export default function SignupRoute() {

return <LoginPage initialMode="signup" />;
}

export default function SignupRoute() {
return (
<Suspense fallback={null}>
<SignupRouteInner />
</Suspense>
);
}
Loading