mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
refine den-web cloud dashboard and onboarding
This commit is contained in:
@@ -95,14 +95,14 @@ export function AuthScreen() {
|
||||
const panelTitle = verificationRequired
|
||||
? "Verify your email."
|
||||
: authMode === "sign-up"
|
||||
? "Create your Den account."
|
||||
: "Sign in to Den.";
|
||||
? "Create your Cloud account."
|
||||
: "Sign in to Cloud.";
|
||||
|
||||
const panelCopy = verificationRequired
|
||||
? "Enter the six-digit code from your inbox to finish setup."
|
||||
: authMode === "sign-up"
|
||||
? "Start with email, GitHub, or Google."
|
||||
: "Welcome back. Pick up where your workers left off.";
|
||||
: "Welcome back. Keep your team setup in sync across Cloud and desktop.";
|
||||
|
||||
return (
|
||||
<section className="den-page flex w-full items-center py-4 lg:min-h-[calc(100vh-2.5rem)]">
|
||||
@@ -110,28 +110,27 @@ export function AuthScreen() {
|
||||
<div className="grid w-full gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(360px,440px)]">
|
||||
<div className="den-frame-soft flex flex-col justify-between gap-10 p-7 md:p-10 lg:p-12">
|
||||
<div className="grid gap-6">
|
||||
<span className="den-kicker w-fit">OpenWork Den</span>
|
||||
<span className="den-kicker w-fit">OpenWork Cloud</span>
|
||||
<div className="grid gap-4">
|
||||
<h1 className="den-title-xl max-w-[10ch]">Run workers that stay on.</h1>
|
||||
<h1 className="den-title-xl max-w-[12ch]">Share your OpenWork setup with your team.</h1>
|
||||
<p className="den-copy max-w-[40rem]">
|
||||
Launch hosted workers, share the same setup with your team,
|
||||
and reconnect from desktop or web when you need it.
|
||||
Provision shared setups, invite your org, and keep background agents available when you need them.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3 lg:grid-cols-1">
|
||||
<div className="grid gap-1 border-t border-gray-200 pt-4">
|
||||
<p className="text-base font-medium text-[var(--dls-text-primary)]">Hosted workers</p>
|
||||
<p className="den-copy text-sm">Keep automations live after your laptop closes.</p>
|
||||
<p className="text-base font-medium text-[var(--dls-text-primary)]">Share setup across your team and org</p>
|
||||
<p className="den-copy text-sm">Package skills, MCPs, plugins, and config once.</p>
|
||||
</div>
|
||||
<div className="grid gap-1 border-t border-gray-200 pt-4">
|
||||
<p className="text-base font-medium text-[var(--dls-text-primary)]">Shared setup</p>
|
||||
<p className="den-copy text-sm">Bring the same skills, MCPs, and config into Den.</p>
|
||||
<p className="text-base font-medium text-[var(--dls-text-primary)]">Background agents</p>
|
||||
<p className="den-copy text-sm">Keep selected workflows running in the cloud. Alpha.</p>
|
||||
</div>
|
||||
<div className="grid gap-1 border-t border-gray-200 pt-4">
|
||||
<p className="text-base font-medium text-[var(--dls-text-primary)]">Open anywhere</p>
|
||||
<p className="den-copy text-sm">Reconnect from the browser or the OpenWork app.</p>
|
||||
<p className="text-base font-medium text-[var(--dls-text-primary)]">Custom LLM providers</p>
|
||||
<p className="den-copy text-sm">Standardize provider access for your team. Coming soon.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,8 +175,7 @@ export function AuthScreen() {
|
||||
</div>
|
||||
<p className="text-xs leading-5 opacity-80">
|
||||
If OpenWork does not open automatically, copy the sign-in
|
||||
link or one-time code and paste it into OpenWork Cloud
|
||||
settings.
|
||||
link or one-time code and paste it into the OpenWork desktop app.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -278,8 +276,8 @@ export function AuthScreen() {
|
||||
: verificationRequired
|
||||
? "Verify email"
|
||||
: authMode === "sign-in"
|
||||
? "Sign in"
|
||||
: "Create account"}
|
||||
? "Sign in to Cloud"
|
||||
: "Create Cloud account"}
|
||||
</button>
|
||||
|
||||
{verificationRequired ? (
|
||||
|
||||
@@ -135,39 +135,41 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken:
|
||||
return (
|
||||
<section className="den-page grid gap-6 py-4 lg:py-6">
|
||||
<div className="den-frame grid gap-6 p-6 md:p-8 lg:p-10">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="flex flex-col gap-4 lg:max-w-3xl">
|
||||
<div className="grid gap-3">
|
||||
<p className="den-eyebrow">{onboardingPending ? "Finish setup" : "Billing"}</p>
|
||||
<h1 className="den-title-xl max-w-[10ch]">Choose how to run Den.</h1>
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<h1 className="den-title-xl max-w-[12ch]">Provision shared setups for your team.</h1>
|
||||
<p className="den-copy max-w-2xl">
|
||||
Start with hosted workers for your team, or stay local and add
|
||||
Cloud later.
|
||||
Share your setup across your org, launch background agents in alpha, and prepare for team-wide provider provisioning.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className={`den-status-pill ${billingSummary?.hasActivePlan ? "is-positive" : "is-neutral"}`}>
|
||||
{billingSummary?.hasActivePlan ? "Active plan" : "Trial ready"}
|
||||
</span>
|
||||
<span className="den-kicker">{user?.email ?? "Signed in"}</span>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{checkoutHref ? (
|
||||
<a href={checkoutHref} rel="noreferrer" className="den-button-primary w-full sm:w-auto">
|
||||
Start free trial
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="den-button-primary w-full sm:w-auto"
|
||||
onClick={() => void refreshBilling({ includeCheckout: true, quiet: false })}
|
||||
disabled={billingBusy || billingCheckoutBusy}
|
||||
>
|
||||
Refresh trial link
|
||||
</button>
|
||||
)}
|
||||
<a href="https://openworklabs.com/download" className="den-button-secondary w-full sm:w-auto">
|
||||
Use desktop only
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="den-stat-grid">
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Cloud plan</p>
|
||||
<p className="den-stat-value">{planAmountLabel}</p>
|
||||
<p className="den-stat-copy">Hosted workers, team access, and billing in one place.</p>
|
||||
</div>
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Subscription</p>
|
||||
<p className="den-stat-value">{subscriptionStatus}</p>
|
||||
<p className="den-stat-copy">{billingSummary?.hasActivePlan ? "Your workspace is ready to keep running." : `${TRIAL_DAYS}-day free trial before billing starts.`}</p>
|
||||
</div>
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Invoices</p>
|
||||
<p className="den-stat-value">{billingSummary?.invoices.length ?? 0}</p>
|
||||
<p className="den-stat-copy">Past billing history appears here as soon as your plan is active.</p>
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm text-[var(--dls-text-secondary)]">
|
||||
<span>{TRIAL_DAYS}-day free trial</span>
|
||||
<span aria-hidden="true">•</span>
|
||||
<span>{planAmountLabel} after trial</span>
|
||||
<span aria-hidden="true">•</span>
|
||||
<span>{user?.email ?? "Signed in"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,93 +178,72 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken:
|
||||
{showLoading ? <div className="den-frame-soft px-5 py-4 text-sm text-[var(--dls-text-secondary)]">Refreshing access state...</div> : null}
|
||||
|
||||
{billingSummary ? (
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.15fr)_360px]">
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.15fr)_320px]">
|
||||
<div className="grid gap-6">
|
||||
<article className="den-frame grid gap-6 p-6 md:p-7">
|
||||
<div className="grid gap-3">
|
||||
<span className="den-kicker w-fit">Hosted workers</span>
|
||||
<h2 className="den-title-lg">Den Cloud</h2>
|
||||
<span className="den-kicker w-fit">OpenWork Cloud</span>
|
||||
<h2 className="den-title-lg">Share your setup across your team.</h2>
|
||||
<p className="den-copy">
|
||||
Run workers that stay on, share access with your team, and
|
||||
manage billing without leaving Den.
|
||||
Manage your team's setup, invite teammates, and keep everything in sync.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 text-sm text-[var(--dls-text-secondary)]">
|
||||
<div className="flex gap-3"><span className="mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" />{TRIAL_DAYS}-day free trial</div>
|
||||
<div className="flex gap-3"><span className="mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" />{planAmountLabel} after trial</div>
|
||||
<div className="flex gap-3"><span className="mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" />Hosted workers, team access, and billing controls</div>
|
||||
<div className="flex gap-3"><span className="mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" />Share setup across your team and org</div>
|
||||
<div className="flex gap-3"><span className="mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" />Background agents in alpha for selected workflows</div>
|
||||
<div className="flex gap-3"><span className="mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" />Custom LLM providers for teams, coming soon</div>
|
||||
</div>
|
||||
|
||||
{checkoutHref ? (
|
||||
<div className="mt-auto flex flex-wrap gap-3 pt-2">
|
||||
<a
|
||||
href={checkoutHref}
|
||||
rel="noreferrer"
|
||||
className="den-button-primary w-full sm:w-auto"
|
||||
>
|
||||
Start free trial
|
||||
</a>
|
||||
{billingSummary.portalUrl ? (
|
||||
<a href={billingSummary.portalUrl} rel="noreferrer" target="_blank" className="den-button-secondary w-full sm:w-auto">
|
||||
Open billing portal
|
||||
</a>
|
||||
) : null}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="den-frame-inset rounded-[1.5rem] p-4">
|
||||
<p className="den-stat-label">Background agents</p>
|
||||
<p className="mt-3 text-sm text-[var(--dls-text-secondary)]">
|
||||
Keep selected workflows running in the background. Alpha.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-auto grid gap-3 pt-2">
|
||||
<div className="den-frame-inset rounded-[1.5rem] px-4 py-3 text-sm text-[var(--dls-text-secondary)]">
|
||||
We are still preparing your trial link.
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="den-button-secondary w-full sm:w-auto"
|
||||
onClick={() => void refreshBilling({ includeCheckout: true, quiet: false })}
|
||||
disabled={billingBusy || billingCheckoutBusy}
|
||||
>
|
||||
Refresh trial link
|
||||
</button>
|
||||
<div className="den-frame-inset rounded-[1.5rem] p-4">
|
||||
<p className="den-stat-label">Custom LLM providers</p>
|
||||
<p className="mt-3 text-sm text-[var(--dls-text-secondary)]">
|
||||
Standardize provider access for your team. Coming soon.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="den-frame-soft grid gap-6 p-6 md:p-7">
|
||||
<article className="den-frame-soft grid gap-5 p-6 md:p-7">
|
||||
<div className="grid gap-3">
|
||||
<span className="den-kicker w-fit">Local-first</span>
|
||||
<h2 className="den-title-lg">Desktop App</h2>
|
||||
<span className="den-kicker w-fit">Desktop app</span>
|
||||
<h2 className="den-title-lg">Stay local when you need to.</h2>
|
||||
<p className="den-copy">
|
||||
Run locally for free, keep your data on your machine, and add
|
||||
Cloud when your team needs it.
|
||||
Run locally for free, keep your data on your machine, and add OpenWork Cloud when your team is ready.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 text-sm text-[var(--dls-text-secondary)]">
|
||||
<div className="flex gap-3"><span className="mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" />Run locally for free</div>
|
||||
<div className="flex gap-3"><span className="mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" />Keep data on your machine</div>
|
||||
<div className="flex gap-3"><span className="mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" />Add Den Cloud whenever you are ready</div>
|
||||
<div className="flex gap-3"><span className="mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" />Move into OpenWork Cloud later</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto pt-2">
|
||||
<a href="/" className="den-button-secondary w-full sm:w-auto">
|
||||
Download app
|
||||
<a href="https://openworklabs.com/download" className="den-button-secondary w-full sm:w-auto">
|
||||
Use desktop only
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<aside className="den-frame-soft grid h-fit gap-5 p-6 md:p-7">
|
||||
<aside className="den-frame-soft grid h-fit gap-4 p-5 md:p-6">
|
||||
<div className="grid gap-2">
|
||||
<p className="den-eyebrow">Billing snapshot</p>
|
||||
<h2 className="den-title-lg">Keep billing tidy.</h2>
|
||||
<p className="den-copy text-sm">
|
||||
Track plan status, open the billing portal, and review invoices
|
||||
from one place.
|
||||
</p>
|
||||
<p className="den-eyebrow">Billing status</p>
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-[var(--dls-text-primary)]">{subscriptionStatus}</h2>
|
||||
<p className="den-copy text-sm">{billingSummary.hasActivePlan ? "Your Cloud plan is active." : `${TRIAL_DAYS}-day free trial before billing starts.`}</p>
|
||||
</div>
|
||||
|
||||
<div className="den-frame-inset grid gap-3 rounded-[1.5rem] px-4 py-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm font-medium text-[var(--dls-text-primary)]">Plan status</span>
|
||||
<span className="text-sm font-medium text-[var(--dls-text-primary)]">Plan</span>
|
||||
<span className={`den-status-pill ${billingSummary.hasActivePlan ? "is-positive" : "is-neutral"}`}>
|
||||
{subscriptionStatus}
|
||||
</span>
|
||||
@@ -278,6 +259,11 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken:
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
{checkoutHref && !billingSummary.hasActivePlan ? (
|
||||
<a href={checkoutHref} rel="noreferrer" className="den-button-primary w-full">
|
||||
Start free trial
|
||||
</a>
|
||||
) : null}
|
||||
{billingSummary.portalUrl ? (
|
||||
<a href={billingSummary.portalUrl} rel="noreferrer" target="_blank" className="den-button-secondary w-full">
|
||||
Open billing portal
|
||||
@@ -293,38 +279,14 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken:
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="den-list-shell">
|
||||
<div className="px-5 py-4">
|
||||
<p className="den-eyebrow">Invoices</p>
|
||||
</div>
|
||||
{billingSummary.invoices.length === 0 ? (
|
||||
<div className="den-list-row text-sm text-[var(--dls-text-secondary)]">
|
||||
Invoices will appear here once your Cloud plan begins billing.
|
||||
</div>
|
||||
) : (
|
||||
billingSummary.invoices.slice(0, 5).map((invoice) => (
|
||||
<div key={invoice.id} className="den-list-row">
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium text-[var(--dls-text-primary)]">
|
||||
{invoice.invoiceNumber ?? "Invoice"}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--dls-text-secondary)]">
|
||||
{invoice.createdAt ? new Date(invoice.createdAt).toLocaleDateString() : "Recent"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-[var(--dls-text-secondary)]">
|
||||
{invoice.totalAmount !== null ? formatMoneyMinor(invoice.totalAmount, invoice.currency) : "Pending"}
|
||||
</span>
|
||||
{invoice.invoiceUrl ? (
|
||||
<a href={invoice.invoiceUrl} rel="noreferrer" target="_blank" className="den-button-ghost text-sm">
|
||||
View
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2 text-sm text-[var(--dls-text-secondary)]">
|
||||
{billingSummary.portalUrl ? (
|
||||
<a href={billingSummary.portalUrl} rel="noreferrer" target="_blank" className="font-medium text-[var(--dls-text-primary)] transition hover:opacity-70">
|
||||
Billing portal
|
||||
</a>
|
||||
) : null}
|
||||
<span>Invoices {billingSummary.invoices.length > 0 ? `(${billingSummary.invoices.length})` : ""}</span>
|
||||
<span>Cancel anytime</span>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -558,7 +558,7 @@ export function DashboardScreen({ showSidebar = true }: { showSidebar?: boolean
|
||||
<p className="text-[15px] leading-relaxed text-[var(--dls-text-secondary)]">
|
||||
{billingSummary?.featureGateEnabled
|
||||
? billingSummary.hasActivePlan
|
||||
? "Your account has an active Den Cloud plan."
|
||||
? "Your account has an active OpenWork Cloud plan."
|
||||
: "Your account needs billing before the next launch."
|
||||
: "Billing gates are disabled in this environment."}
|
||||
</p>
|
||||
|
||||
@@ -144,6 +144,22 @@ export function getManageMembersRoute(orgSlug: string): string {
|
||||
return `${getOrgDashboardRoute(orgSlug)}/manage-members`;
|
||||
}
|
||||
|
||||
export function getMembersRoute(orgSlug: string): string {
|
||||
return `${getOrgDashboardRoute(orgSlug)}/members`;
|
||||
}
|
||||
|
||||
export function getSharedSetupsRoute(orgSlug: string): string {
|
||||
return `${getOrgDashboardRoute(orgSlug)}/shared-setups`;
|
||||
}
|
||||
|
||||
export function getBackgroundAgentsRoute(orgSlug: string): string {
|
||||
return `${getOrgDashboardRoute(orgSlug)}/background-agents`;
|
||||
}
|
||||
|
||||
export function getCustomLlmProvidersRoute(orgSlug: string): string {
|
||||
return `${getOrgDashboardRoute(orgSlug)}/custom-llm-providers`;
|
||||
}
|
||||
|
||||
export function parseOrgListPayload(payload: unknown): {
|
||||
orgs: DenOrgSummary[];
|
||||
activeOrgId: string | null;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { getSharedSetupsRoute } from "../../../../_lib/den-org";
|
||||
import { useOrgDashboard } from "../_providers/org-dashboard-provider";
|
||||
|
||||
const EXAMPLE_AGENTS = [
|
||||
{
|
||||
name: "Sales follow-up agent",
|
||||
status: "Active",
|
||||
detail: "Source: SDR outreach setup",
|
||||
},
|
||||
{
|
||||
name: "Renewal reminder agent",
|
||||
status: "Paused",
|
||||
detail: "Source: Customer success setup",
|
||||
},
|
||||
];
|
||||
|
||||
export function BackgroundAgentsScreen() {
|
||||
const { orgSlug } = useOrgDashboard();
|
||||
|
||||
return (
|
||||
<section className="den-page flex max-w-6xl flex-col gap-6 py-4 md:py-8">
|
||||
<div className="den-frame grid gap-6 p-6 md:p-8 lg:p-10">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<span className="den-status-pill is-neutral">Alpha</span>
|
||||
</div>
|
||||
<h1 className="den-title-xl max-w-[12ch]">Background agents</h1>
|
||||
<p className="den-copy max-w-2xl">
|
||||
Keep selected workflows running in the background.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link href={getSharedSetupsRoute(orgSlug)} className="den-button-secondary">
|
||||
Open shared setups
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="den-stat-card md:col-span-2">
|
||||
<p className="den-stat-label">How this fits</p>
|
||||
<p className="den-stat-copy mt-3">
|
||||
Use shared setups as the source of truth, then keep selected workflows available without asking each teammate to run them locally.
|
||||
</p>
|
||||
</div>
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Status</p>
|
||||
<p className="den-stat-value text-[1.5rem] md:text-[1.7rem]">Alpha</p>
|
||||
<p className="den-stat-copy">Available for selected workflows while the product continues to evolve.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="den-list-shell">
|
||||
<div className="px-5 py-5">
|
||||
<p className="den-eyebrow">Example workflows</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold tracking-tight text-[var(--dls-text-primary)]">
|
||||
Background workflows
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{EXAMPLE_AGENTS.map((agent) => (
|
||||
<article key={agent.name} className="den-list-row">
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-base font-semibold text-[var(--dls-text-primary)]">{agent.name}</h3>
|
||||
<p className="text-sm text-[var(--dls-text-secondary)]">{agent.detail}</p>
|
||||
</div>
|
||||
<span className="den-status-pill is-neutral">{agent.status}</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { OPENWORK_DOCS_URL } from "./shared-setup-data";
|
||||
|
||||
const UPCOMING_BENEFITS = [
|
||||
"Standardize provider access across your team.",
|
||||
"Keep model choices consistent across shared setups.",
|
||||
"Control rollout without reconfiguring every teammate by hand.",
|
||||
];
|
||||
|
||||
export function CustomLlmProvidersScreen() {
|
||||
return (
|
||||
<section className="den-page flex max-w-6xl flex-col gap-6 py-4 md:py-8">
|
||||
<div className="den-frame grid gap-6 p-6 md:p-8 lg:p-10">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<span className="den-status-pill is-neutral">Coming soon</span>
|
||||
</div>
|
||||
<h1 className="den-title-xl max-w-[12ch]">Custom LLM providers</h1>
|
||||
<p className="den-copy max-w-2xl">
|
||||
Standardize provider access for your team.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a href={OPENWORK_DOCS_URL} target="_blank" rel="noreferrer" className="den-button-secondary">
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{UPCOMING_BENEFITS.map((benefit) => (
|
||||
<div key={benefit} className="den-stat-card">
|
||||
<p className="den-stat-label">Coming soon</p>
|
||||
<p className="den-stat-copy mt-3">{benefit}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="den-frame-soft grid gap-4 p-5 md:p-6">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-[var(--dls-text-primary)]">What to expect</h2>
|
||||
<p className="den-copy text-sm">
|
||||
This page stays intentionally light for now. The goal is to make provider access easier to manage across shared setups once the feature is ready.
|
||||
</p>
|
||||
<div>
|
||||
<Link href="/checkout" className="den-button-secondary">
|
||||
Review billing
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
getBackgroundAgentsRoute,
|
||||
getCustomLlmProvidersRoute,
|
||||
getMembersRoute,
|
||||
getSharedSetupsRoute,
|
||||
} from "../../../../_lib/den-org";
|
||||
import { useOrgDashboard } from "../_providers/org-dashboard-provider";
|
||||
import { OPENWORK_DOCS_URL, useOrgTemplates } from "./shared-setup-data";
|
||||
|
||||
export function DashboardOverviewScreen() {
|
||||
const { orgSlug, activeOrg, orgContext } = useOrgDashboard();
|
||||
const { templates, busy, error } = useOrgTemplates(orgSlug);
|
||||
const pendingInvitations = (orgContext?.invitations ?? []).filter((invitation) => invitation.status === "pending");
|
||||
|
||||
return (
|
||||
<section className="den-page flex max-w-6xl flex-col gap-6 py-4 md:py-8">
|
||||
<div className="den-frame grid gap-6 p-6 md:p-8 lg:p-10">
|
||||
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="grid gap-3">
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<h1 className="den-title-xl max-w-[12ch]">{activeOrg?.name ?? "OpenWork Cloud"}</h1>
|
||||
<p className="den-copy max-w-2xl">
|
||||
Manage your team's setup, invite teammates, and keep everything in sync.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link href={getMembersRoute(orgSlug)} className="den-button-secondary">
|
||||
Add member
|
||||
</Link>
|
||||
<a href={OPENWORK_DOCS_URL} target="_blank" rel="noreferrer" className="den-button-secondary">
|
||||
Learn how
|
||||
</a>
|
||||
<Link href="/checkout" className="den-button-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Members</p>
|
||||
<p className="den-stat-value">{orgContext?.members.length ?? 0}</p>
|
||||
<p className="den-stat-copy">People who can access this shared setup.</p>
|
||||
</div>
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Shared setups</p>
|
||||
<p className="den-stat-value">{templates.length}</p>
|
||||
<p className="den-stat-copy">Reusable templates your team can open right away.</p>
|
||||
</div>
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Pending invites</p>
|
||||
<p className="den-stat-value">{pendingInvitations.length}</p>
|
||||
<p className="den-stat-copy">Invitations waiting for teammates to join.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? <div className="den-notice is-error">{error}</div> : null}
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<div className="den-list-shell">
|
||||
<div className="flex flex-col gap-2 px-5 py-5 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<p className="den-eyebrow">Recent shared setups</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold tracking-tight text-[var(--dls-text-primary)]">
|
||||
Shared setups
|
||||
</h2>
|
||||
</div>
|
||||
<p className="max-w-sm text-sm leading-relaxed text-[var(--dls-text-secondary)] md:text-right">
|
||||
Create and update shared templates your team can use right away.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{busy ? (
|
||||
<div className="den-list-row text-sm text-[var(--dls-text-secondary)]">Loading shared setups...</div>
|
||||
) : templates.length === 0 ? (
|
||||
<div className="den-list-row text-sm text-[var(--dls-text-secondary)]">
|
||||
No shared setups yet. Create one from the OpenWork desktop app and it will appear here.
|
||||
</div>
|
||||
) : (
|
||||
templates.slice(0, 4).map((template) => (
|
||||
<article key={template.id} className="den-list-row">
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-base font-semibold text-[var(--dls-text-primary)]">{template.name}</h3>
|
||||
<p className="text-sm text-[var(--dls-text-secondary)]">Created by {template.creator.name}</p>
|
||||
<p className="text-xs text-[var(--dls-text-secondary)]">
|
||||
{template.createdAt ? `Updated ${new Date(template.createdAt).toLocaleDateString()}` : "Updated recently"}
|
||||
</p>
|
||||
</div>
|
||||
<Link href={getSharedSetupsRoute(orgSlug)} className="den-button-secondary shrink-0">
|
||||
Open
|
||||
</Link>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid h-fit gap-4">
|
||||
<article className="den-frame-soft grid gap-3 p-5 md:p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="den-eyebrow">Background agents</p>
|
||||
<span className="den-status-pill is-neutral">Alpha</span>
|
||||
</div>
|
||||
<p className="den-copy text-sm">Keep selected workflows running in the background.</p>
|
||||
<Link href={getBackgroundAgentsRoute(orgSlug)} className="den-button-secondary w-full">
|
||||
Open background agents
|
||||
</Link>
|
||||
</article>
|
||||
|
||||
<article className="den-frame-soft grid gap-3 p-5 md:p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="den-eyebrow">Custom LLM providers</p>
|
||||
<span className="den-status-pill is-neutral">Soon</span>
|
||||
</div>
|
||||
<p className="den-copy text-sm">Standardize provider access for your team.</p>
|
||||
<Link href={getCustomLlmProvidersRoute(orgSlug)} className="den-button-secondary w-full">
|
||||
Learn more
|
||||
</Link>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -180,13 +180,12 @@ export function ManageMembersScreen() {
|
||||
<div className="den-frame p-6 md:p-8 lg:p-10">
|
||||
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-end">
|
||||
<div>
|
||||
<p className="den-eyebrow">Manage members</p>
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<h1 className="mt-2 text-[2.4rem] font-semibold leading-[0.95] tracking-[-0.06em] text-[var(--dls-text-primary)]">
|
||||
{activeOrg.name}
|
||||
Members
|
||||
</h1>
|
||||
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-[var(--dls-text-secondary)]">
|
||||
Invite people, adjust roles, and keep access clean without turning
|
||||
the page into an admin maze.
|
||||
Invite teammates, adjust roles, and keep access clean without turning this into an admin maze.
|
||||
</p>
|
||||
</div>
|
||||
<div className="den-frame-inset rounded-[1.25rem] px-4 py-3 text-sm text-[var(--dls-text-secondary)]">
|
||||
|
||||
@@ -6,8 +6,11 @@ import { useMemo, useState, type ReactNode } from "react";
|
||||
import { useDenFlow } from "../../../../_providers/den-flow-provider";
|
||||
import {
|
||||
formatRoleLabel,
|
||||
getManageMembersRoute,
|
||||
getBackgroundAgentsRoute,
|
||||
getCustomLlmProvidersRoute,
|
||||
getMembersRoute,
|
||||
getOrgDashboardRoute,
|
||||
getSharedSetupsRoute,
|
||||
} from "../../../../_lib/den-org";
|
||||
import { useOrgDashboard } from "../_providers/org-dashboard-provider";
|
||||
|
||||
@@ -59,7 +62,10 @@ export function OrgDashboardShell({ children }: { children: ReactNode }) {
|
||||
|
||||
const navItems = [
|
||||
{ href: activeOrg ? getOrgDashboardRoute(activeOrg.slug) : "#", label: "Dashboard" },
|
||||
{ href: activeOrg ? getManageMembersRoute(activeOrg.slug) : "#", label: "Manage members" },
|
||||
{ href: activeOrg ? getSharedSetupsRoute(activeOrg.slug) : "#", label: "Shared setups" },
|
||||
{ href: activeOrg ? getMembersRoute(activeOrg.slug) : "#", label: "Members" },
|
||||
{ href: activeOrg ? getBackgroundAgentsRoute(activeOrg.slug) : "#", label: "Background agents", badge: "Alpha" },
|
||||
{ href: activeOrg ? getCustomLlmProvidersRoute(activeOrg.slug) : "#", label: "Custom LLM providers", badge: "Soon" },
|
||||
{ href: "/checkout", label: "Billing" },
|
||||
];
|
||||
|
||||
@@ -69,8 +75,8 @@ export function OrgDashboardShell({ children }: { children: ReactNode }) {
|
||||
<div className="flex h-full flex-col gap-5 p-4 md:p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="den-eyebrow">OpenWork Den</p>
|
||||
<p className="mt-1 text-lg font-semibold tracking-tight text-[var(--dls-text-primary)]">Workspace</p>
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<p className="mt-1 text-lg font-semibold tracking-tight text-[var(--dls-text-primary)]">Team workspace</p>
|
||||
</div>
|
||||
{orgBusy ? <span className="text-xs text-[var(--dls-text-secondary)]">Refreshing...</span> : null}
|
||||
</div>
|
||||
@@ -171,25 +177,36 @@ export function OrgDashboardShell({ children }: { children: ReactNode }) {
|
||||
</div>
|
||||
<nav className="grid gap-1.5">
|
||||
{navItems.map((item) => {
|
||||
const selected = item.href !== "#" && pathname === item.href;
|
||||
const selected = item.href !== "#" && (pathname === item.href || pathname.startsWith(`${item.href}/`));
|
||||
return (
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className={`rounded-full px-4 py-3 text-sm font-medium transition ${
|
||||
className={`flex items-center justify-between gap-3 rounded-full px-4 py-3 text-sm font-medium transition ${
|
||||
selected
|
||||
? "bg-white text-[var(--dls-text-primary)] shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_0_rgba(0,0,0,0.04)]"
|
||||
: "text-[var(--dls-text-secondary)] hover:text-[var(--dls-text-primary)]"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
<span>{item.label}</span>
|
||||
{item.badge ? <span className="den-status-pill is-neutral">{item.badge}</span> : null}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto den-frame-soft grid gap-3 p-4">
|
||||
<div className="mt-auto grid gap-3">
|
||||
<a
|
||||
href="https://openworklabs.com/docs"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="den-button-secondary w-full"
|
||||
>
|
||||
Learn how
|
||||
</a>
|
||||
|
||||
<div className="den-frame-soft grid gap-3 p-4">
|
||||
<div>
|
||||
<p className="den-eyebrow">Signed in as</p>
|
||||
<p className="mt-2 break-words text-sm font-medium text-[var(--dls-text-primary)]">
|
||||
@@ -204,6 +221,7 @@ export function OrgDashboardShell({ children }: { children: ReactNode }) {
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { getErrorMessage, requestJson } from "../../../../_lib/den-flow";
|
||||
|
||||
export const OPENWORK_DOCS_URL = "https://openworklabs.com/docs";
|
||||
|
||||
export type TemplateCard = {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string | null;
|
||||
creator: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
};
|
||||
|
||||
function asTemplateCard(value: unknown): TemplateCard | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry = value as Record<string, unknown>;
|
||||
const creator = entry.creator && typeof entry.creator === "object"
|
||||
? (entry.creator as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
if (
|
||||
typeof entry.id !== "string" ||
|
||||
typeof entry.name !== "string" ||
|
||||
!creator ||
|
||||
typeof creator.name !== "string" ||
|
||||
typeof creator.email !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
createdAt: typeof entry.createdAt === "string" ? entry.createdAt : null,
|
||||
creator: {
|
||||
name: creator.name,
|
||||
email: creator.email,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useOrgTemplates(orgSlug: string) {
|
||||
const [templates, setTemplates] = useState<TemplateCard[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function loadTemplates() {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { response, payload } = await requestJson(
|
||||
`/v1/orgs/${encodeURIComponent(orgSlug)}/templates`,
|
||||
{ method: "GET" },
|
||||
12000,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getErrorMessage(payload, `Failed to load templates (${response.status}).`));
|
||||
}
|
||||
|
||||
const list =
|
||||
payload && typeof payload === "object" && Array.isArray((payload as { templates?: unknown[] }).templates)
|
||||
? (payload as { templates: unknown[] }).templates
|
||||
: [];
|
||||
|
||||
setTemplates(list.map(asTemplateCard).filter((entry): entry is TemplateCard => entry !== null));
|
||||
} catch (loadError) {
|
||||
setError(loadError instanceof Error ? loadError.message : "Failed to load templates.");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadTemplates();
|
||||
}, [orgSlug]);
|
||||
|
||||
return {
|
||||
templates,
|
||||
busy,
|
||||
error,
|
||||
reloadTemplates: loadTemplates,
|
||||
};
|
||||
}
|
||||
@@ -1,95 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { getErrorMessage, requestJson } from "../../../../_lib/den-flow";
|
||||
import { getManageMembersRoute } from "../../../../_lib/den-org";
|
||||
import { useState } from "react";
|
||||
import { requestJson, getErrorMessage } from "../../../../_lib/den-flow";
|
||||
import { getMembersRoute } from "../../../../_lib/den-org";
|
||||
import { useOrgDashboard } from "../_providers/org-dashboard-provider";
|
||||
import { OPENWORK_DOCS_URL, useOrgTemplates } from "./shared-setup-data";
|
||||
|
||||
type TemplateCard = {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string | null;
|
||||
creator: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
};
|
||||
|
||||
function asTemplateCard(value: unknown): TemplateCard | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry = value as Record<string, unknown>;
|
||||
const creator = entry.creator && typeof entry.creator === "object"
|
||||
? (entry.creator as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
if (
|
||||
typeof entry.id !== "string" ||
|
||||
typeof entry.name !== "string" ||
|
||||
!creator ||
|
||||
typeof creator.name !== "string" ||
|
||||
typeof creator.email !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
createdAt: typeof entry.createdAt === "string" ? entry.createdAt : null,
|
||||
creator: {
|
||||
name: creator.name,
|
||||
email: creator.email,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function TemplatesDashboardScreen() {
|
||||
export function SharedSetupsScreen() {
|
||||
const { orgSlug, activeOrg, orgContext } = useOrgDashboard();
|
||||
const [templates, setTemplates] = useState<TemplateCard[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { templates, busy, error, reloadTemplates } = useOrgTemplates(orgSlug);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
|
||||
const canDelete = orgContext?.currentMember.isOwner ?? false;
|
||||
const pendingInvitations = useMemo(
|
||||
() => (orgContext?.invitations ?? []).filter((invitation) => invitation.status === "pending"),
|
||||
[orgContext?.invitations],
|
||||
);
|
||||
|
||||
async function loadTemplates() {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { response, payload } = await requestJson(
|
||||
`/v1/orgs/${encodeURIComponent(orgSlug)}/templates`,
|
||||
{ method: "GET" },
|
||||
12000,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getErrorMessage(payload, `Failed to load templates (${response.status}).`));
|
||||
}
|
||||
|
||||
const list =
|
||||
payload && typeof payload === "object" && Array.isArray((payload as { templates?: unknown[] }).templates)
|
||||
? (payload as { templates: unknown[] }).templates
|
||||
: [];
|
||||
|
||||
setTemplates(list.map(asTemplateCard).filter((entry): entry is TemplateCard => entry !== null));
|
||||
} catch (loadError) {
|
||||
setError(loadError instanceof Error ? loadError.message : "Failed to load templates.");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTemplate(templateId: string) {
|
||||
setDeletingId(templateId);
|
||||
setError(null);
|
||||
setDeleteError(null);
|
||||
try {
|
||||
const { response, payload } = await requestJson(
|
||||
`/v1/orgs/${encodeURIComponent(orgSlug)}/templates/${encodeURIComponent(templateId)}`,
|
||||
@@ -101,134 +29,106 @@ export function TemplatesDashboardScreen() {
|
||||
throw new Error(getErrorMessage(payload, `Failed to delete template (${response.status}).`));
|
||||
}
|
||||
|
||||
await loadTemplates();
|
||||
} catch (deleteError) {
|
||||
setError(deleteError instanceof Error ? deleteError.message : "Failed to delete template.");
|
||||
await reloadTemplates();
|
||||
} catch (nextError) {
|
||||
setDeleteError(nextError instanceof Error ? nextError.message : "Failed to delete template.");
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadTemplates();
|
||||
}, [orgSlug]);
|
||||
|
||||
return (
|
||||
<section className="den-page flex max-w-6xl flex-col gap-6 py-4 md:py-8">
|
||||
<div className="den-frame grid gap-6 p-6 md:p-8 lg:p-10">
|
||||
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="grid gap-3">
|
||||
<p className="den-eyebrow">Den dashboard</p>
|
||||
<h1 className="den-title-xl max-w-[11ch]">{activeOrg?.name ?? "OpenWork"}</h1>
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<h1 className="den-title-xl max-w-[12ch]">Shared setups</h1>
|
||||
<p className="den-copy max-w-2xl">
|
||||
Share setup once, keep team access tidy, and manage the links your
|
||||
organization actually uses.
|
||||
Create and update shared templates your team can use right away.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link href={getManageMembersRoute(orgSlug)} className="den-button-secondary">
|
||||
Manage members
|
||||
</Link>
|
||||
<Link href="/checkout" className="den-button-primary">
|
||||
Billing
|
||||
<a href={OPENWORK_DOCS_URL} target="_blank" rel="noreferrer" className="den-button-secondary">
|
||||
Learn how
|
||||
</a>
|
||||
<Link href={getMembersRoute(orgSlug)} className="den-button-secondary">
|
||||
Members
|
||||
</Link>
|
||||
<a href="https://openworklabs.com/download" className="den-button-primary">
|
||||
Use desktop app
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="den-stat-grid">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Organization</p>
|
||||
<p className="den-stat-value text-[1.4rem] md:text-[1.6rem]">{activeOrg?.name ?? "OpenWork"}</p>
|
||||
<p className="den-stat-copy">Shared templates stay available to this team.</p>
|
||||
</div>
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Templates</p>
|
||||
<p className="den-stat-value">{templates.length}</p>
|
||||
<p className="den-stat-copy">Current shared setups created from the desktop app.</p>
|
||||
</div>
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Members</p>
|
||||
<p className="den-stat-value">{orgContext?.members.length ?? 0}</p>
|
||||
<p className="den-stat-copy">Everyone who can access this workspace.</p>
|
||||
</div>
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Pending invites</p>
|
||||
<p className="den-stat-value">{pendingInvitations.length}</p>
|
||||
<p className="den-stat-copy">Invites waiting to be accepted.</p>
|
||||
</div>
|
||||
<div className="den-stat-card">
|
||||
<p className="den-stat-label">Shared links</p>
|
||||
<p className="den-stat-value">{templates.length}</p>
|
||||
<p className="den-stat-copy">Setup packages created from the desktop app.</p>
|
||||
<p className="den-stat-copy">Teammates who can use these shared templates.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? <div className="den-notice is-error">{error}</div> : null}
|
||||
{deleteError ? <div className="den-notice is-error">{deleteError}</div> : null}
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<div className="den-list-shell">
|
||||
<div className="flex flex-col gap-2 px-5 py-5 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<p className="den-eyebrow">Shared setup links</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold tracking-tight text-[var(--dls-text-primary)]">
|
||||
Current workspace templates
|
||||
</h2>
|
||||
</div>
|
||||
<p className="max-w-sm text-sm leading-relaxed text-[var(--dls-text-secondary)] md:text-right">
|
||||
Create new links from the OpenWork desktop app. Keep only the
|
||||
setups your team still needs.
|
||||
</p>
|
||||
<div className="den-list-shell">
|
||||
<div className="flex flex-col gap-2 px-5 py-5 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<p className="den-eyebrow">Template library</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold tracking-tight text-[var(--dls-text-primary)]">
|
||||
Team templates
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{busy ? (
|
||||
<div className="den-list-row text-sm text-[var(--dls-text-secondary)]">Loading templates...</div>
|
||||
) : templates.length === 0 ? (
|
||||
<div className="den-list-row text-sm text-[var(--dls-text-secondary)]">
|
||||
No shared links yet. Create one from the desktop app and it will
|
||||
appear here.
|
||||
</div>
|
||||
) : (
|
||||
templates.map((template) => (
|
||||
<article key={template.id} className="den-list-row">
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-base font-semibold text-[var(--dls-text-primary)]">{template.name}</h3>
|
||||
<p className="text-sm text-[var(--dls-text-secondary)]">
|
||||
Created by {template.creator.name} · {template.creator.email}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--dls-text-secondary)]">
|
||||
{template.createdAt ? `Created ${new Date(template.createdAt).toLocaleString()}` : "Created recently"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{canDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
className="den-button-danger shrink-0"
|
||||
onClick={() => void deleteTemplate(template.id)}
|
||||
disabled={deletingId === template.id}
|
||||
>
|
||||
{deletingId === template.id ? "Deleting..." : "Delete"}
|
||||
</button>
|
||||
) : null}
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
<p className="max-w-sm text-sm leading-relaxed text-[var(--dls-text-secondary)] md:text-right">
|
||||
Review what your team is sharing now, and remove templates that are no longer current.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<aside className="den-frame-soft grid h-fit gap-4 p-5 md:p-6">
|
||||
<div className="grid gap-2">
|
||||
<p className="den-eyebrow">Quick actions</p>
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-[var(--dls-text-primary)]">
|
||||
Keep the workspace moving.
|
||||
</h2>
|
||||
<p className="den-copy text-sm">
|
||||
Invite teammates, review billing, or clean up old links without
|
||||
leaving this area.
|
||||
</p>
|
||||
{busy ? (
|
||||
<div className="den-list-row text-sm text-[var(--dls-text-secondary)]">Loading shared setups...</div>
|
||||
) : templates.length === 0 ? (
|
||||
<div className="den-list-row text-sm text-[var(--dls-text-secondary)]">
|
||||
No shared setups yet. Create one from the OpenWork desktop app and it will appear here.
|
||||
</div>
|
||||
) : (
|
||||
templates.map((template) => (
|
||||
<article key={template.id} className="den-list-row">
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-base font-semibold text-[var(--dls-text-primary)]">{template.name}</h3>
|
||||
<p className="text-sm text-[var(--dls-text-secondary)]">
|
||||
Created by {template.creator.name} · {template.creator.email}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--dls-text-secondary)]">
|
||||
{template.createdAt ? `Updated ${new Date(template.createdAt).toLocaleString()}` : "Updated recently"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link href={getManageMembersRoute(orgSlug)} className="den-button-secondary w-full">
|
||||
Add or edit members
|
||||
</Link>
|
||||
<Link href="/checkout" className="den-button-secondary w-full">
|
||||
Open billing
|
||||
</Link>
|
||||
<a href="https://openworklabs.com/download" className="den-button-secondary w-full">
|
||||
Download desktop app
|
||||
</a>
|
||||
</aside>
|
||||
{canDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
className="den-button-danger shrink-0"
|
||||
onClick={() => void deleteTemplate(template.id)}
|
||||
disabled={deletingId === template.id}
|
||||
>
|
||||
{deletingId === template.id ? "Deleting..." : "Delete"}
|
||||
</button>
|
||||
) : null}
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { BackgroundAgentsScreen } from "../_components/background-agents-screen";
|
||||
|
||||
export default function BackgroundAgentsPage() {
|
||||
return <BackgroundAgentsScreen />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CustomLlmProvidersScreen } from "../_components/custom-llm-providers-screen";
|
||||
|
||||
export default function CustomLlmProvidersPage() {
|
||||
return <CustomLlmProvidersScreen />;
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { ManageMembersScreen } from "../_components/manage-members-screen";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function ManageMembersPage() {
|
||||
return <ManageMembersScreen />;
|
||||
export default function ManageMembersRedirectPage({
|
||||
params,
|
||||
}: {
|
||||
params: { orgSlug: string };
|
||||
}) {
|
||||
redirect(`/o/${encodeURIComponent(params.orgSlug)}/dashboard/members`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ManageMembersScreen } from "../_components/manage-members-screen";
|
||||
|
||||
export default function MembersPage() {
|
||||
return <ManageMembersScreen />;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
// import { DashboardScreen } from "../../../_components/dashboard-screen";
|
||||
import { TemplatesDashboardScreen } from "./_components/templates-dashboard-screen";
|
||||
import { DashboardOverviewScreen } from "./_components/dashboard-overview-screen";
|
||||
|
||||
export default function OrgDashboardPage() {
|
||||
// return <DashboardScreen showSidebar={false} />;
|
||||
return <TemplatesDashboardScreen />;
|
||||
return <DashboardOverviewScreen />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SharedSetupsScreen } from "../_components/templates-dashboard-screen";
|
||||
|
||||
export default function SharedSetupsPage() {
|
||||
return <SharedSetupsScreen />;
|
||||
}
|
||||
@@ -18,20 +18,20 @@ const ibmPlexMono = IBM_Plex_Mono({
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://app.openworklabs.com"),
|
||||
title: "Den Cloud Workers",
|
||||
title: "OpenWork Cloud",
|
||||
description:
|
||||
"Launch OpenWork cloud workers, manage Polar checkout flows, and run Den from app.openworklabs.com with the same positioning as the landing page.",
|
||||
"Share your OpenWork setup with your team, manage billing, and use OpenWork Cloud from app.openworklabs.com.",
|
||||
openGraph: {
|
||||
title: "Den Cloud Workers",
|
||||
title: "OpenWork Cloud",
|
||||
description:
|
||||
"Always-on AI workers for you and your team, launched from app.openworklabs.com.",
|
||||
"Share your OpenWork setup with your team and keep selected workflows available in OpenWork Cloud.",
|
||||
images: ["/opengraph-image"]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Den Cloud Workers",
|
||||
title: "OpenWork Cloud",
|
||||
description:
|
||||
"Launch OpenWork cloud workers, manage Polar checkout flows, and operate Den from app.openworklabs.com.",
|
||||
"Share your OpenWork setup with your team and manage OpenWork Cloud from app.openworklabs.com.",
|
||||
images: ["/opengraph-image"]
|
||||
},
|
||||
icons: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
|
||||
export const alt = "Den Cloud Workers";
|
||||
export const alt = "OpenWork Cloud";
|
||||
export const size = {
|
||||
width: 1200,
|
||||
height: 630
|
||||
@@ -74,19 +74,19 @@ export default function OpenGraphImage() {
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, textTransform: "uppercase", letterSpacing: 3, color: "#64748b" }}>
|
||||
OpenWork hosted
|
||||
OpenWork Cloud
|
||||
</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 600 }}>Den Cloud Workers</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 600 }}>OpenWork Cloud</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10, fontSize: 64, fontWeight: 600, letterSpacing: -2.8, lineHeight: 0.98 }}>
|
||||
<div>Always-on AI workers</div>
|
||||
<div>for you and your team.</div>
|
||||
<div>Share your setup</div>
|
||||
<div>with your team.</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 24, lineHeight: 1.45, color: "#475569", display: "flex", maxWidth: 520 }}>
|
||||
Launch cloud workers, manage Polar checkout, and operate Den directly from app.openworklabs.com.
|
||||
Share setups across your org, keep selected workflows available, and manage OpenWork Cloud from app.openworklabs.com.
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
|
||||
|
||||
Reference in New Issue
Block a user