fix(den): tighten workspace pages to match the new mock

This commit is contained in:
Benjamin Shafii
2026-03-26 22:49:37 -07:00
parent a6fb2e07d8
commit 851424b70b
5 changed files with 423 additions and 369 deletions

View File

@@ -2,14 +2,21 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { useState } from "react";
import {
ArrowRight,
Box,
Check,
ChevronDown,
ChevronUp,
Copy,
ExternalLink,
KeyRound,
Monitor,
MoreHorizontal,
Plus,
Search,
X,
RefreshCw,
} from "lucide-react";
import { Dithering, MeshGradient } from "@paper-design/shaders-react";
import {
OPENWORK_APP_CONNECT_BASE_URL,
buildOpenworkAppConnectUrl,
@@ -18,15 +25,11 @@ import {
getWorkerStatusMeta,
getWorkerTokens,
requestJson,
type WorkerStatusBucket,
type WorkerListItem,
} from "../../../../_lib/den-flow";
import { useDenFlow } from "../../../../_providers/den-flow-provider";
import { getSharedSetupsRoute } from "../../../../_lib/den-org";
import { useOrgDashboard } from "../_providers/org-dashboard-provider";
import {
formatTemplateTimestamp,
useOrgTemplates,
} from "./shared-setup-data";
type ConnectionDetails = {
openworkUrl: string | null;
@@ -36,43 +39,267 @@ type ConnectionDetails = {
openworkDeepLink: string | null;
};
const statusOptions: Array<{ label: string; value: WorkerStatusBucket | "all" }> = [
{ label: "All", value: "all" },
{ label: "Ready", value: "ready" },
{ label: "Starting", value: "starting" },
{ label: "Attention", value: "attention" },
];
function getStatusBadgeClass(bucket: ReturnType<typeof getWorkerStatusMeta>["bucket"]) {
switch (bucket) {
case "ready":
return "border-emerald-100 bg-emerald-50 text-emerald-600";
case "starting":
return "border-amber-100 bg-amber-50 text-amber-600";
case "attention":
return "border-rose-100 bg-rose-50 text-rose-600";
default:
return "border-gray-100 bg-gray-50 text-gray-500";
}
}
function getTemplateAccent(seed: string) {
let hash = 0;
for (let index = 0; index < seed.length; index += 1) {
hash = (hash * 33 + seed.charCodeAt(index)) % 360;
function CredentialField({
id,
label,
value,
onCopy,
copied,
}: {
id: string;
label: string;
value: string;
onCopy: (field: string, text: string) => void;
copied: boolean;
}) {
return (
<div>
<label className="mb-1.5 block text-[12px] text-gray-500">{label}</label>
<div className="flex items-center gap-2">
<input
readOnly
value={value}
className="flex-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-[12px] font-mono text-gray-600 outline-none shadow-sm transition-colors focus:border-gray-300"
onClick={(event) => event.currentTarget.select()}
/>
<button
type="button"
onClick={() => onCopy(id, value)}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-400 shadow-sm transition-colors hover:border-gray-300 hover:text-gray-700"
aria-label={`Copy ${label}`}
>
{copied ? <Check size={14} className="text-emerald-500" /> : <Copy size={14} />}
</button>
</div>
</div>
);
}
function SandboxCard({
sandbox,
expanded,
details,
connectBusy,
renameBusy,
onToggle,
onRefresh,
onRename,
}: {
sandbox: WorkerListItem;
expanded: boolean;
details: ConnectionDetails | null;
connectBusy: boolean;
renameBusy: boolean;
onToggle: () => void;
onRefresh: () => void;
onRename: () => void;
}) {
const [showTokens, setShowTokens] = useState(false);
const [copiedField, setCopiedField] = useState<string | null>(null);
const meta = getWorkerStatusMeta(sandbox.status);
const canConnect = meta.bucket === "ready";
const connectionUrl = details?.openworkUrl ?? sandbox.instanceUrl ?? null;
const ownerToken = details?.ownerToken ?? null;
const clientToken = details?.clientToken ?? null;
const openWebUrl = details?.openworkAppConnectUrl ?? null;
const openDesktopUrl = details?.openworkDeepLink ?? null;
async function handleCopy(field: string, text: string) {
await navigator.clipboard.writeText(text);
setCopiedField(field);
window.setTimeout(() => {
setCopiedField((current) => (current === field ? null : current));
}, 2000);
}
return {
background: `hsl(${hash} 92% 96%)`,
gradient: `radial-gradient(circle at 30% 30%, hsl(${(hash + 60) % 360} 92% 68%), hsl(${hash} 82% 48%), hsl(${(hash + 140) % 360} 88% 28%))`,
};
const credentialFields = [
connectionUrl ? { id: "url", label: "Connection URL", value: connectionUrl } : null,
ownerToken ? { id: "owner", label: "Owner token", value: ownerToken } : null,
clientToken ? { id: "client", label: "Client token", value: clientToken } : null,
].filter((field): field is { id: string; label: string; value: string } => Boolean(field));
return (
<div className="rounded-2xl border border-gray-100 bg-white p-5 transition-all hover:border-gray-200 hover:shadow-[0_2px_8px_-4px_rgba(0,0,0,0.04)]">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-gray-100 bg-gray-50">
<Box size={18} className="text-gray-400" />
</div>
<div>
<h3 className="mb-0.5 flex items-center gap-2 text-[14px] font-medium text-gray-900">
{sandbox.workerName}
<span
className={`flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.5px] ${getStatusBadgeClass(meta.bucket)}`}
>
<span className="h-1.5 w-1.5 rounded-full bg-current" />
{meta.label}
</span>
</h3>
<p className="text-[12px] text-gray-400">
Source: {sandbox.provider ? `${sandbox.provider} sandbox` : "cloud sandbox"}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
if (expanded) {
setShowTokens(false);
}
onToggle();
}}
disabled={!canConnect}
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[13px] font-medium transition-colors ${
expanded
? "bg-gray-100 text-gray-900 hover:bg-gray-200"
: "bg-gray-50 text-gray-700 hover:bg-gray-100 hover:text-gray-900"
} disabled:cursor-not-allowed disabled:opacity-60`}
>
{expanded ? "Hide details" : "Connect"}
{expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
<button
type="button"
onClick={onRename}
disabled={renameBusy}
className="flex h-8 w-8 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-gray-50 hover:text-gray-900 disabled:cursor-not-allowed disabled:opacity-60"
aria-label={`Rename ${sandbox.workerName}`}
>
<MoreHorizontal size={16} />
</button>
</div>
</div>
{expanded ? (
<div className="mt-5 border-t border-gray-100 pt-5">
<div className="flex flex-col gap-3 sm:flex-row">
<button
type="button"
onClick={() => {
if (openDesktopUrl) {
window.location.href = openDesktopUrl;
}
}}
disabled={!openDesktopUrl}
className="flex flex-1 items-center justify-center gap-2 rounded-xl bg-gray-900 py-2.5 text-[13px] font-medium text-white shadow-sm transition-colors hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60"
>
<Monitor size={15} /> Open in desktop
</button>
{openWebUrl ? (
<a
href={openWebUrl}
target="_blank"
rel="noreferrer"
className="flex flex-1 items-center justify-center gap-2 rounded-xl border border-gray-200 bg-white py-2.5 text-[13px] font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50"
>
<ExternalLink size={15} /> Open in web
</a>
) : (
<button
type="button"
disabled
className="flex flex-1 items-center justify-center gap-2 rounded-xl border border-gray-200 bg-white py-2.5 text-[13px] font-medium text-gray-700 shadow-sm opacity-60"
>
<ExternalLink size={15} /> Open in web
</button>
)}
</div>
{canConnect ? (
<div className="mt-4">
<button
type="button"
onClick={() => setShowTokens((current) => !current)}
className="flex w-full items-center justify-between rounded-xl border border-gray-100 bg-gray-50/50 px-4 py-2.5 text-[12px] font-medium text-gray-600 transition-colors hover:bg-gray-50"
>
<span className="flex items-center gap-2">
<KeyRound size={14} className="text-gray-400" />
Connection credentials
</span>
{showTokens ? (
<ChevronUp size={14} className="text-gray-400" />
) : (
<ChevronDown size={14} className="text-gray-400" />
)}
</button>
{showTokens ? (
<div className="mt-2 space-y-4 rounded-xl border border-gray-100 bg-gray-50/50 p-5">
<div className="mb-2 flex items-center justify-between">
<span className="text-[10px] font-semibold uppercase tracking-[1px] text-gray-500">
Access Tokens
</span>
<button
type="button"
onClick={onRefresh}
disabled={connectBusy}
className="flex items-center gap-1.5 text-[12px] font-medium text-gray-500 transition-colors hover:text-gray-900 disabled:cursor-not-allowed disabled:opacity-60"
>
<RefreshCw size={13} className={connectBusy ? "animate-spin" : ""} />
{connectBusy ? "Refreshing..." : "Refresh tokens"}
</button>
</div>
{credentialFields.length > 0 ? (
credentialFields.map((field) => (
<CredentialField
key={field.id}
id={field.id}
label={field.label}
value={field.value}
onCopy={handleCopy}
copied={copiedField === field.id}
/>
))
) : (
<p className="text-[12px] text-gray-500">
{connectBusy
? "Loading connection credentials..."
: "Connection credentials will appear here once the workspace is ready."}
</p>
)}
</div>
) : null}
</div>
) : (
<p className="mt-4 text-[12px] text-gray-500">
Connection details will appear once this workspace is ready.
</p>
)}
</div>
) : null}
</div>
);
}
export function BackgroundAgentsScreen() {
const router = useRouter();
const { orgSlug } = useOrgDashboard();
const { templates } = useOrgTemplates(orgSlug);
const [expandedWorkerId, setExpandedWorkerId] = useState<string | null>(null);
const [connectBusyWorkerId, setConnectBusyWorkerId] = useState<string | null>(null);
const [connectError, setConnectError] = useState<string | null>(null);
const [copiedField, setCopiedField] = useState<string | null>(null);
const [connectionDetailsByWorkerId, setConnectionDetailsByWorkerId] = useState<
Record<string, ConnectionDetails>
>({});
const [showTemplatesBanner, setShowTemplatesBanner] = useState(true);
const {
filteredWorkers,
workerQuery,
setWorkerQuery,
workerStatusFilter,
setWorkerStatusFilter,
workersBusy,
workersLoadedOnce,
workersError,
@@ -82,25 +309,13 @@ export function BackgroundAgentsScreen() {
renameBusyWorkerId,
} = useDenFlow();
async function handleAddSandbox() {
async function handleAddWorkspace() {
const result = await launchWorker({ source: "manual" });
if (result === "checkout") {
router.push("/checkout");
}
}
async function copyValue(field: string, value: string | null) {
if (!value) {
return;
}
await navigator.clipboard.writeText(value);
setCopiedField(field);
window.setTimeout(() => {
setCopiedField((current) => (current === field ? null : current));
}, 1500);
}
async function loadConnectionDetails(workerId: string, workerName: string) {
setConnectBusyWorkerId(workerId);
setConnectError(null);
@@ -150,158 +365,90 @@ export function BackgroundAgentsScreen() {
...current,
[workerId]: nextDetails,
}));
return nextDetails;
} catch (error) {
setConnectError(
error instanceof Error ? error.message : "Failed to load connection details.",
);
return null;
} finally {
setConnectBusyWorkerId(null);
}
}
async function toggleConnect(workerId: string, workerName: string) {
if (expandedWorkerId === workerId) {
async function toggleSandbox(worker: WorkerListItem) {
const meta = getWorkerStatusMeta(worker.status);
if (meta.bucket !== "ready") {
return;
}
if (expandedWorkerId === worker.workerId) {
setExpandedWorkerId(null);
return;
}
setExpandedWorkerId(workerId);
if (!connectionDetailsByWorkerId[workerId]) {
await loadConnectionDetails(workerId, workerName);
setExpandedWorkerId(worker.workerId);
if (!connectionDetailsByWorkerId[worker.workerId]) {
await loadConnectionDetails(worker.workerId, worker.workerName);
}
}
const bannerTemplates = useMemo(() => templates.slice(0, 3), [templates]);
return (
<div className="mx-auto w-full max-w-[1200px] px-6 py-8 md:px-8">
<div className="mb-8 flex items-center justify-between gap-4">
<div>
<h1 className="text-[28px] font-semibold tracking-[-0.5px] text-gray-900">
Agents
</h1>
<p className="mt-2 text-[14px] text-gray-500">
Launch cloud sandboxes, connect them to OpenWork, and keep background workflows available for the team.
</p>
<div className="mx-auto max-w-[860px] p-8">
<div className="relative mb-8 flex min-h-[180px] items-center overflow-hidden rounded-3xl border border-gray-100 px-10">
<div className="absolute inset-0 z-0">
<Dithering
speed={0}
shape="warp"
type="4x4"
size={2.5}
scale={1}
frame={5213.4}
colorBack="#00000000"
colorFront="#FEFEFE"
style={{ backgroundColor: "#23301C", width: "100%", height: "100%" }}
>
<MeshGradient
speed={0}
distortion={0.8}
swirl={0.1}
grainMixer={0}
grainOverlay={0}
frame={176868.9}
colors={["#E9FFE0", "#3E9A1D", "#B3F750", "#51F0A3"]}
style={{ width: "100%", height: "100%" }}
/>
</Dithering>
</div>
<div className="flex items-center gap-3">
<Link
href={getSharedSetupsRoute(orgSlug)}
className="rounded-full border border-gray-200 bg-white px-4 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-50"
>
Browse templates
</Link>
<button
type="button"
onClick={() => void handleAddSandbox()}
disabled={launchBusy}
className="inline-flex items-center gap-1.5 rounded-full bg-gray-900 px-4 py-2 text-[13px] font-medium text-white transition-colors hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60"
>
<Plus className="h-3.5 w-3.5" />
{launchBusy ? "Adding..." : "New agent"}
</button>
<div className="relative z-10 flex flex-col items-start gap-3">
<div>
<span className="mb-2 inline-block rounded-full border border-white/20 bg-white/20 px-2.5 py-1 text-[10px] font-medium uppercase tracking-[1px] text-white backdrop-blur-md">
Alpha
</span>
<h1 className="mb-1.5 text-[26px] font-medium tracking-[-0.5px] text-white">
Shared Workspaces
</h1>
<p className="max-w-[500px] text-[14px] text-white/80">
Keep selected workflows running in the background without asking each teammate to run them locally. Available for selected workflows while the product continues to evolve.
</p>
</div>
</div>
</div>
{showTemplatesBanner ? (
<div className="relative mb-8 rounded-[20px] border border-gray-100 bg-white p-6 shadow-[0_2px_8px_-4px_rgba(0,0,0,0.02)]">
<button
type="button"
onClick={() => setShowTemplatesBanner(false)}
className="absolute right-4 top-4 text-gray-400 transition-colors hover:text-gray-600"
aria-label="Dismiss template suggestions"
>
<X className="h-4 w-4" />
</button>
<h2 className="mb-1 text-[15px] font-semibold text-gray-900">
Get started with a template
</h2>
<p className="mb-5 text-[13px] text-gray-500">
Build faster with shared setups your team has already created.
</p>
{bannerTemplates.length > 0 ? (
<div className="mb-5 grid gap-4 md:grid-cols-3">
{bannerTemplates.map((template) => {
const accent = getTemplateAccent(template.name);
return (
<div
key={template.id}
className="rounded-xl border border-gray-100 bg-white p-4 transition-all hover:border-gray-200 hover:shadow-[0_2px_12px_-4px_rgba(0,0,0,0.06)]"
>
<div className="mb-2 flex items-center gap-2">
<div
className="relative flex h-6 w-6 items-center justify-center overflow-hidden rounded-full"
style={{ backgroundColor: accent.background }}
>
<div
className="absolute inset-0"
style={{ backgroundImage: accent.gradient }}
/>
</div>
<span className="truncate text-[13px] font-semibold text-gray-900">
{template.name}
</span>
</div>
<p className="text-[13px] leading-relaxed text-gray-500">
Created by {template.creator.name}
</p>
<p className="mt-2 text-[12px] text-gray-400">
Updated {formatTemplateTimestamp(template.createdAt, { includeTime: true })}
</p>
</div>
);
})}
</div>
) : (
<div className="mb-5 rounded-xl border border-dashed border-gray-200 bg-gray-50 px-4 py-5 text-[13px] text-gray-500">
No shared templates yet. Create one first, then launch an agent from it.
</div>
)}
<Link
href={getSharedSetupsRoute(orgSlug)}
className="inline-flex items-center gap-1 text-[13px] font-semibold text-gray-900 transition-colors hover:text-gray-700"
>
Browse all templates <ArrowRight className="h-3.5 w-3.5" />
</Link>
</div>
) : null}
<div className="mb-6">
<div className="relative mb-4">
<div className="pointer-events-none absolute inset-y-0 left-3 flex items-center">
<Search className="h-4 w-4 text-gray-400" />
</div>
<input
type="text"
value={workerQuery}
onChange={(event) => setWorkerQuery(event.target.value)}
placeholder="Search agents..."
className="w-full rounded-xl border border-gray-200 bg-white py-2.5 pl-9 pr-4 text-[14px] text-gray-900 outline-none transition-all placeholder:text-gray-400 focus:border-gray-300 focus:ring-2 focus:ring-gray-900/5"
/>
</div>
<div className="flex flex-wrap gap-2">
{statusOptions.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setWorkerStatusFilter(option.value)}
className={`rounded-full px-3 py-1.5 text-[12px] font-medium transition-colors ${
workerStatusFilter === option.value
? "bg-gray-900 text-white"
: "border border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
}`}
>
{option.label}
</button>
))}
</div>
<div className="mb-10 flex items-center gap-3">
<button
type="button"
onClick={() => void handleAddWorkspace()}
disabled={launchBusy}
className="flex items-center gap-2 rounded-full bg-gray-900 px-5 py-2.5 text-[13px] font-medium text-white shadow-sm transition-colors hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60"
>
<Plus size={15} />
{launchBusy ? "Adding workspace..." : "Add workspace"}
</button>
<Link
href={getSharedSetupsRoute(orgSlug)}
className="flex items-center gap-2 rounded-full border border-gray-200 bg-white px-5 py-2.5 text-[13px] font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50"
>
Open shared setups
</Link>
</div>
{workersError ? (
@@ -315,174 +462,76 @@ export function BackgroundAgentsScreen() {
</div>
) : null}
<div className="mt-6 overflow-hidden border-t border-gray-100">
<div className="grid grid-cols-[2fr_1.2fr_1.2fr_auto] gap-4 border-b border-gray-100 py-3 text-[12px] font-medium text-gray-500">
<div>Name</div>
<div>Provider</div>
<div>Created at</div>
<div className="w-8" />
<div>
<div className="mb-4 flex items-center justify-between gap-4">
<h2 className="text-[15px] font-medium tracking-[-0.2px] text-gray-900">
Current workspaces
</h2>
<div className="relative w-full max-w-[240px]">
<div className="pointer-events-none absolute inset-y-0 left-3 flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-gray-400"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
</div>
<input
type="text"
value={workerQuery}
onChange={(event) => setWorkerQuery(event.target.value)}
placeholder="Search workspaces..."
className="w-full rounded-lg border border-gray-200 bg-white py-2 pl-9 pr-4 text-[13px] text-gray-900 outline-none transition-all placeholder:text-gray-400 focus:border-gray-300 focus:ring-2 focus:ring-gray-900/5"
/>
</div>
</div>
{!workersLoadedOnce ? (
<div className="py-8 text-[13px] text-gray-500">Loading agents</div>
) : filteredWorkers.length === 0 ? (
<div className="py-10 text-[13px] text-gray-400">
{workerQuery.trim()
? "No agents match that search yet."
: "No agents launched yet. Start a new agent to see it here."}
</div>
) : (
<div className="divide-y divide-gray-50/50">
{filteredWorkers.map((worker) => {
const meta = getWorkerStatusMeta(worker.status);
const canConnect = meta.bucket === "ready";
const isExpanded = expandedWorkerId === worker.workerId;
const details = connectionDetailsByWorkerId[worker.workerId] ?? null;
return (
<div key={worker.workerId} className="group">
<div className="grid grid-cols-[2fr_1.2fr_1.2fr_auto] gap-4 items-center rounded-lg px-2 py-4 transition-colors hover:bg-gray-50/50">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="truncate text-[13px] font-medium text-gray-900">
{worker.workerName}
</span>
<span className="rounded-full bg-gray-100 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-gray-500">
{meta.label}
</span>
</div>
<p className="truncate text-[12px] text-gray-400">
{worker.workerId}
</p>
</div>
<div className="text-[13px] text-gray-500">
{worker.provider ? `${worker.provider} sandbox` : "Cloud sandbox"}
</div>
<div className="text-[13px] text-gray-500">
{worker.createdAt
? formatTemplateTimestamp(worker.createdAt, { includeTime: true })
: "Recently"}
</div>
<div className="flex items-center justify-end gap-2">
{canConnect ? (
<button
type="button"
onClick={() => void toggleConnect(worker.workerId, worker.workerName)}
className="rounded-full border border-gray-200 px-3 py-1.5 text-[12px] font-medium text-gray-600 transition-colors hover:bg-gray-50"
>
{isExpanded ? "Hide" : "Connect"}
</button>
) : null}
<button
type="button"
onClick={() => {
const nextName = window.prompt("Rename sandbox", worker.workerName)?.trim();
if (!nextName || nextName === worker.workerName) {
return;
}
void renameWorker(worker.workerId, nextName);
}}
disabled={renameBusyWorkerId === worker.workerId}
className="rounded-full p-1.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-60"
aria-label={`Rename ${worker.workerName}`}
>
<MoreHorizontal className="h-4 w-4" />
</button>
</div>
</div>
{isExpanded && canConnect ? (
<div className="mb-4 rounded-[20px] border border-gray-100 bg-white p-4">
<div className="mb-4 flex flex-wrap gap-3">
<a
href={details?.openworkAppConnectUrl ?? "#"}
target="_blank"
rel="noreferrer"
className={`rounded-full bg-gray-900 px-4 py-2 text-[13px] font-medium text-white transition-colors hover:bg-gray-800 ${
details?.openworkAppConnectUrl ? "" : "pointer-events-none opacity-60"
}`}
>
Open in web
</a>
<button
type="button"
onClick={() => {
if (details?.openworkDeepLink) {
window.location.href = details.openworkDeepLink;
}
}}
disabled={!details?.openworkDeepLink}
className="rounded-full border border-gray-200 bg-white px-4 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
>
Open in desktop
</button>
<button
type="button"
onClick={() => void loadConnectionDetails(worker.workerId, worker.workerName)}
disabled={connectBusyWorkerId === worker.workerId}
className="rounded-full border border-gray-200 bg-white px-4 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
>
{connectBusyWorkerId === worker.workerId ? "Refreshing..." : "Refresh tokens"}
</button>
</div>
<div className="grid gap-4 lg:grid-cols-3">
{[
{
label: "Connection URL",
value: details?.openworkUrl ?? worker.instanceUrl ?? "Preparing...",
key: `url-${worker.workerId}`,
},
{
label: "Owner token",
value: details?.ownerToken ?? "Preparing...",
key: `owner-${worker.workerId}`,
},
{
label: "Client token",
value: details?.clientToken ?? "Preparing...",
key: `client-${worker.workerId}`,
},
].map((field) => (
<div key={field.key} className="grid gap-2">
<span className="text-[11px] font-bold uppercase tracking-[0.14em] text-gray-400">
{field.label}
</span>
<div className="flex items-center gap-2 rounded-2xl border border-gray-200 bg-white px-3 py-2.5">
<input
readOnly
value={field.value}
className="min-w-0 flex-1 border-none bg-transparent font-mono text-xs text-gray-900 outline-none"
onClick={(event) => event.currentTarget.select()}
/>
<button
type="button"
onClick={() =>
void copyValue(
field.key,
field.value === "Preparing..." ? null : field.value,
)
}
disabled={field.value === "Preparing..."}
className="rounded-full border border-gray-200 px-3 py-1.5 text-[12px] font-medium text-gray-600 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
>
{copiedField === field.key ? "Copied" : "Copy"}
</button>
</div>
</div>
))}
</div>
</div>
) : null}
</div>
);
})}
</div>
)}
<div className="space-y-3">
{!workersLoadedOnce ? (
<div className="rounded-2xl border border-gray-100 bg-white p-5 text-[13px] text-gray-500">
Loading workspaces...
</div>
) : filteredWorkers.length === 0 ? (
<div className="rounded-2xl border border-gray-100 bg-white p-5 text-[13px] text-gray-500">
{workerQuery.trim()
? "No workspaces match that search yet."
: "No workspaces launched yet. Add one to start connecting cloud workflows."}
</div>
) : (
filteredWorkers.map((sandbox) => (
<SandboxCard
key={sandbox.workerId}
sandbox={sandbox}
expanded={expandedWorkerId === sandbox.workerId}
details={connectionDetailsByWorkerId[sandbox.workerId] ?? null}
connectBusy={connectBusyWorkerId === sandbox.workerId}
renameBusy={renameBusyWorkerId === sandbox.workerId}
onToggle={() => void toggleSandbox(sandbox)}
onRefresh={() => void loadConnectionDetails(sandbox.workerId, sandbox.workerName)}
onRename={() => {
const nextName = window.prompt("Rename workspace", sandbox.workerName)?.trim();
if (!nextName || nextName === sandbox.workerName) {
return;
}
void renameWorker(sandbox.workerId, nextName);
}}
/>
))
)}
</div>
</div>
{workersLoadedOnce && workersBusy ? (
<p className="mt-4 text-[12px] text-gray-400">Refreshing agents</p>
<p className="mt-4 text-[12px] text-gray-400">Refreshing workspaces</p>
) : null}
</div>
);

View File

@@ -1,27 +1,45 @@
"use client";
import Link from "next/link";
import { Cpu } from "lucide-react";
import { getBillingRoute } from "../../../../_lib/den-org";
import { useOrgDashboard } from "../_providers/org-dashboard-provider";
import { OPENWORK_DOCS_URL } from "./shared-setup-data";
import { Dithering, MeshGradient } from "@paper-design/shaders-react";
const UPCOMING_BENEFITS = [
const comingSoonItems = [
"Standardize provider access across your team.",
"Keep model choices consistent across shared setups.",
"Control rollout without reconfiguring every teammate by hand.",
];
export function CustomLlmProvidersScreen() {
const { orgSlug } = useOrgDashboard();
return (
<div className="mx-auto max-w-[980px] px-6 py-8 md:px-8">
<div className="relative mb-8 flex h-[220px] items-center overflow-hidden rounded-[28px] border border-gray-100 px-10">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_20%_20%,rgba(224,252,255,0.95),transparent_28%),radial-gradient(circle_at_80%_10%,rgba(80,247,212,0.55),transparent_22%),radial-gradient(circle_at_70%_80%,rgba(81,142,240,0.6),transparent_28%),linear-gradient(135deg,#1c2a30_0%,#1d7b9a_45%,#223140_100%)]" />
<div className="mx-auto max-w-[860px] p-8">
<div className="relative mb-8 flex h-[200px] items-center overflow-hidden rounded-3xl border border-gray-100 px-10">
<div className="absolute inset-0 z-0">
<Dithering
speed={0}
shape="warp"
type="4x4"
size={2.5}
scale={1}
frame={41112.4}
colorBack="#00000000"
colorFront="#FEFEFE"
style={{ backgroundColor: "#1C2A30", width: "100%", height: "100%" }}
>
<MeshGradient
speed={1}
distortion={0.8}
swirl={0.1}
grainMixer={0}
grainOverlay={0}
frame={176868.9}
colors={["#E0FCFF", "#1D7B9A", "#50F7D4", "#518EF0"]}
style={{ width: "100%", height: "100%" }}
/>
</Dithering>
</div>
<div className="relative z-10 flex flex-col items-start gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl border border-white/30 bg-white/20 backdrop-blur-md">
<Cpu className="h-6 w-6 text-white" strokeWidth={1.5} />
<Cpu size={24} className="text-white" strokeWidth={1.5} />
</div>
<div>
<span className="mb-2 inline-block rounded-full border border-white/20 bg-white/20 px-2.5 py-1 text-[10px] uppercase tracking-[1px] text-white backdrop-blur-md">
@@ -38,36 +56,19 @@ export function CustomLlmProvidersScreen() {
Standardize provider access for your team.
</p>
<div className="mb-6 grid gap-4 md:grid-cols-3">
{UPCOMING_BENEFITS.map((benefit) => (
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
{comingSoonItems.map((text) => (
<div
key={benefit}
key={text}
className="flex flex-col gap-3 rounded-2xl border border-gray-100 bg-white p-6"
>
<span className="inline-block self-start rounded-full border border-gray-100 bg-gray-50 px-2 py-0.5 text-[10px] uppercase tracking-[1px] text-gray-500">
Coming soon
</span>
<p className="text-[13px] leading-[1.6] text-gray-600">{benefit}</p>
<p className="text-[13px] leading-[1.6] text-gray-600">{text}</p>
</div>
))}
</div>
<div className="flex flex-wrap gap-3">
<a
href={OPENWORK_DOCS_URL}
target="_blank"
rel="noreferrer"
className="rounded-full border border-gray-200 bg-white px-4 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-50"
>
Learn more
</a>
<Link
href={getBillingRoute(orgSlug)}
className="rounded-full border border-gray-200 bg-white px-4 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-50"
>
Review billing
</Link>
</div>
</div>
);
}

View File

@@ -58,10 +58,10 @@ function getDashboardPageTitle(pathname: string, orgSlug: string | null) {
return "Members";
}
if (pathname.startsWith(getBackgroundAgentsRoute(orgSlug))) {
return "Agents";
return "Shared Workspaces";
}
if (pathname.startsWith(getCustomLlmProvidersRoute(orgSlug))) {
return "Custom LLM providers";
return "Custom LLMs";
}
if (pathname.startsWith(getBillingRoute(orgSlug)) || pathname === "/checkout") {
return "Billing";

View File

@@ -10,6 +10,7 @@
"lint": "next lint"
},
"dependencies": {
"@paper-design/shaders-react": "0.0.71",
"lucide-react": "^0.577.0",
"next": "14.2.5",
"react": "18.2.0",

3
pnpm-lock.yaml generated
View File

@@ -410,6 +410,9 @@ importers:
ee/apps/den-web:
dependencies:
'@paper-design/shaders-react':
specifier: 0.0.71
version: 0.0.71(@types/react@18.2.79)(react@18.2.0)
lucide-react:
specifier: ^0.577.0
version: 0.577.0(react@18.2.0)