@talgent/workbench-ui 0.0.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/assets/brand-icons/LICENSE.md +30 -0
- package/assets/brand-icons/README.md +5 -0
- package/assets/brand-icons/figma.svg +1 -0
- package/assets/brand-icons/github.svg +1 -0
- package/assets/brand-icons/linear.svg +1 -0
- package/assets/brand-icons/notion.svg +1 -0
- package/assets/brand-icons/sentry.svg +1 -0
- package/assets/brand-icons/slack.svg +1 -0
- package/assets/fonts/Inter-OFL.txt +93 -0
- package/assets/fonts/inter-latin.woff2 +0 -0
- package/components/agent/agent-icon.css +14 -0
- package/components/agent/agent-icon.tsx +14 -0
- package/components/analytics/chart-geometry.ts +95 -0
- package/components/analytics/dashboard-charts.tsx +136 -0
- package/components/analytics/model-usage-chart.tsx +84 -0
- package/components/analytics/token-breakdown.css +13 -0
- package/components/analytics/token-breakdown.tsx +23 -0
- package/components/analytics/token-report.tsx +57 -0
- package/components/analytics/work-usage/turn-timeline.tsx +98 -0
- package/components/analytics/work-usage/turns.ts +170 -0
- package/components/analytics/work-usage/work-status.ts +36 -0
- package/components/analytics/work-usage/work-usage-reader.tsx +122 -0
- package/components/analytics/work-usage/work-usage.css +48 -0
- package/components/auth/auth-gate.tsx +44 -0
- package/components/chat/message-input.tsx +105 -0
- package/components/composer/permission-mode-list.tsx +15 -0
- package/components/composer/prompt-bar-controls.tsx +77 -0
- package/components/composer/prompt-bar.tsx +45 -0
- package/components/composer/skill-completion.css +27 -0
- package/components/composer/skill-completion.tsx +49 -0
- package/components/composer/use-slash-completion.tsx +69 -0
- package/components/composer/work-start.css +133 -0
- package/components/feedback/toast-region.css +2 -0
- package/components/feedback/toast-region.tsx +15 -0
- package/components/files/file-facts.tsx +16 -0
- package/components/files/file-section.tsx +181 -0
- package/components/markdown.tsx +144 -0
- package/components/marketplace/connector-pages.tsx +166 -0
- package/components/marketplace/marketplace-icons.tsx +66 -0
- package/components/marketplace/marketplace-shared.tsx +43 -0
- package/components/marketplace/marketplace-shell.tsx +32 -0
- package/components/marketplace/marketplace.css +97 -0
- package/components/marketplace/parameter-fields.tsx +46 -0
- package/components/marketplace/plugin-list.tsx +84 -0
- package/components/marketplace/plugin-page.tsx +133 -0
- package/components/marketplace/provider-pages.tsx +120 -0
- package/components/preview/file-format.ts +75 -0
- package/components/preview/file-preview.tsx +153 -0
- package/components/preview/preview-verdict.ts +44 -0
- package/components/preview/preview.css +19 -0
- package/components/preview/text-preview.tsx +174 -0
- package/components/profile/provider-callback.tsx +43 -0
- package/components/profile/work-agent-appearance.tsx +109 -0
- package/components/profile/work-agent-soul.tsx +76 -0
- package/components/rail/rail-section.tsx +20 -0
- package/components/rail/rail.css +19 -0
- package/components/rail/use-expand-on-change.ts +18 -0
- package/components/session/startup-progress.css +77 -0
- package/components/session/startup-progress.tsx +103 -0
- package/components/session/work-permission-control.tsx +45 -0
- package/components/session/work-thinking-control.tsx +52 -0
- package/components/shell/account-menu.tsx +134 -0
- package/components/shell/app-shell.tsx +148 -0
- package/components/shell/core-reading-views.tsx +26 -0
- package/components/shell/core-reading.tsx +17 -0
- package/components/shell/document-title.tsx +11 -0
- package/components/shell/entry-shell.ts +39 -0
- package/components/shell/file-reading.tsx +22 -0
- package/components/shell/page-layout.tsx +7 -0
- package/components/shell/reading-context.ts +41 -0
- package/components/shell/reading-panel.css +67 -0
- package/components/shell/reading-panel.tsx +74 -0
- package/components/shell/sidebar-parts.tsx +38 -0
- package/components/work/context-rail.css +61 -0
- package/components/work/conversation-unavailable.css +6 -0
- package/components/work/conversation-unavailable.tsx +11 -0
- package/components/work/subagent-item.tsx +42 -0
- package/components/work/subagents-section.css +31 -0
- package/components/work/subagents-section.tsx +40 -0
- package/components/work/token-usage.tsx +28 -0
- package/components/work/tool-message-detail.tsx +39 -0
- package/components/work/tool-output-preview.tsx +52 -0
- package/components/work/tool-preview-reading.tsx +27 -0
- package/components/work/tool-reading-content.tsx +31 -0
- package/components/work/work-context-rail.tsx +23 -0
- package/components/work/work-conversation.tsx +207 -0
- package/components/work/work-history.tsx +115 -0
- package/components/work/work-interaction.css +38 -0
- package/components/work/work-interaction.tsx +105 -0
- package/components/work/work-message-flow.css +86 -0
- package/components/work/work-message-flow.tsx +352 -0
- package/components/work/work-progress-section.tsx +20 -0
- package/components/work/work-prompt-bar.tsx +82 -0
- package/components/work/work-question-record.tsx +72 -0
- package/components/work/work-snapshot-state.tsx +24 -0
- package/components/work/work-status-control.tsx +54 -0
- package/components/work-start/start-work-launch-state.ts +33 -0
- package/components/work-start/work-start-catalog.tsx +116 -0
- package/components/work-start/work-start-form.tsx +173 -0
- package/components/work-start/work-start-tray.css +40 -0
- package/components/work-start/work-start-tray.tsx +128 -0
- package/hooks/use-debounced.ts +11 -0
- package/hooks/use-work-conversation.ts +125 -0
- package/hooks/use-work-lifecycle-controls.ts +40 -0
- package/hooks/use-work-stream.ts +91 -0
- package/lib/account-service.ts +77 -0
- package/lib/agent-appearance-service.ts +59 -0
- package/lib/agent-icon.ts +26 -0
- package/lib/agent-soul-service.ts +48 -0
- package/lib/agent-work-service.ts +23 -0
- package/lib/client.ts +20 -0
- package/lib/computer-release.ts +30 -0
- package/lib/computer-service.ts +241 -0
- package/lib/dashboard-report.ts +38 -0
- package/lib/dashboard.ts +21 -0
- package/lib/marketplace-catalog.ts +138 -0
- package/lib/marketplace-connections.ts +98 -0
- package/lib/marketplace-plugins.ts +53 -0
- package/lib/marketplace-routes.ts +12 -0
- package/lib/model-vendors.ts +13 -0
- package/lib/name-taken-error.ts +7 -0
- package/lib/notifications.ts +37 -0
- package/lib/permission-modes.ts +25 -0
- package/lib/personal-usage-service.ts +19 -0
- package/lib/personal-work-service.ts +159 -0
- package/lib/plugin-parameters.ts +64 -0
- package/lib/preview-verdict.ts +26 -0
- package/lib/relative-time.ts +11 -0
- package/lib/session.ts +108 -0
- package/lib/share-link.ts +4 -0
- package/lib/stale-build-reload.ts +41 -0
- package/lib/start-work-request.ts +34 -0
- package/lib/startup-progress.ts +9 -0
- package/lib/subagents.ts +12 -0
- package/lib/thinking-levels.ts +2 -0
- package/lib/token-analytics.ts +168 -0
- package/lib/token-usage.ts +64 -0
- package/lib/trace-view-model.ts +292 -0
- package/lib/transport.ts +29 -0
- package/lib/use-now.ts +11 -0
- package/lib/use-work-trace.ts +175 -0
- package/lib/user-display.ts +49 -0
- package/lib/work-conversation.ts +118 -0
- package/lib/work-file-service.ts +118 -0
- package/lib/work-model.ts +38 -0
- package/lib/work-notices.ts +23 -0
- package/lib/work-progress.ts +41 -0
- package/lib/work-projection.ts +122 -0
- package/lib/work-questions.ts +18 -0
- package/lib/work-start-draft.ts +61 -0
- package/lib/work-start-loadout.ts +100 -0
- package/lib/work-start-source.ts +16 -0
- package/lib/work-state-presentation.ts +80 -0
- package/lib/work-stream-source.ts +32 -0
- package/package.json +81 -5
- package/pages/computers/computers-page.tsx +56 -0
- package/pages/computers/connect-computer-page.tsx +92 -0
- package/pages/computers/install-commands.tsx +42 -0
- package/pages/computers/remove-computer-dialog.tsx +33 -0
- package/pages/computers/rename-computer-dialog.tsx +37 -0
- package/pages/computers/room-rules-dialog.tsx +55 -0
- package/pages/login-page.css +33 -0
- package/pages/login-page.tsx +88 -0
- package/pages/login-threads.tsx +51 -0
- package/pages/marketplace-page.tsx +59 -0
- package/pages/profile-page.tsx +68 -0
- package/pages/workbench/library-page.tsx +88 -0
- package/pages/workbench/new-work-page.tsx +24 -0
- package/pages/workbench/person.tsx +13 -0
- package/pages/workbench/personal-work-context.tsx +93 -0
- package/pages/workbench/personal-work-page.tsx +100 -0
- package/pages/workbench/personal-works-page.tsx +36 -0
- package/pages/workbench/share-dialog.tsx +57 -0
- package/pages/workbench/title-dialog.tsx +35 -0
- package/pages/workbench/usage-page.tsx +52 -0
- package/public/assets/brand/talgent-lettering.png +0 -0
- package/public/assets/brand/talgent-logo-page-bg.png +0 -0
- package/public/assets/brand/talgent-logo-white-bg.png +0 -0
- package/public/assets/brand/talgent-mark-page-bg.png +0 -0
- package/public/assets/brand/talgent-mark-white-bg.png +0 -0
- package/public/assets/brand/talgent-mark.png +0 -0
- package/public/assets/product-icons/LICENSE +21 -0
- package/public/assets/product-icons/README.md +13 -0
- package/public/assets/product-icons/anthropic.svg +1 -0
- package/public/assets/product-icons/deepseek.svg +1 -0
- package/public/assets/product-icons/glm.svg +1 -0
- package/public/assets/product-icons/kimi.svg +1 -0
- package/public/assets/product-icons/linear.svg +1 -0
- package/public/assets/product-icons/minimax.svg +1 -0
- package/public/assets/product-icons/notion.svg +1 -0
- package/public/assets/product-icons/openai.svg +1 -0
- package/public/assets/product-icons/qwen.svg +1 -0
- package/styles/base.css +95 -0
- package/styles/fonts.css +14 -0
- package/styles/markdown.css +79 -0
- package/styles/uber-theme.css +96 -0
- package/styles/work-composer.css +222 -0
- package/styles/work-stream.css +343 -0
- package/styles/workbench.css +144 -0
- package/styles/workspace.css +260 -0
- package/test-support/backend.ts +67 -0
- package/test-support/plugin-summary.ts +8 -0
- package/test-support/setup.ts +59 -0
- package/vite-preset.ts +85 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useState, type FormEvent } from "react";
|
|
2
|
+
import { Alert, Button, Card, Input, Label, Skeleton, TextField } from "@heroui/react";
|
|
3
|
+
import { CircleCheck, Monitor } from "lucide-react";
|
|
4
|
+
import { Link, useSearchParams } from "react-router-dom";
|
|
5
|
+
import { PageLayout } from "../../components/shell/page-layout";
|
|
6
|
+
import { expiryLabel, normalizeUserCode, registrationRefusal, registrationRefusalText, useApproveRegistration, useRegistration } from "../../lib/computer-service";
|
|
7
|
+
import { useNow } from "../../lib/use-now";
|
|
8
|
+
import "../../components/marketplace/marketplace.css";
|
|
9
|
+
|
|
10
|
+
/** Approving a machine's registration: `talgent-computer register` shows a
|
|
11
|
+
* code and a link here; the signed-in person who approves owns the Computer. */
|
|
12
|
+
export function ConnectComputerPage() {
|
|
13
|
+
const [search, setSearch] = useSearchParams();
|
|
14
|
+
const code = normalizeUserCode(search.get("code"));
|
|
15
|
+
return <PageLayout workspaceTitle="Connect a computer">
|
|
16
|
+
<div className="mx-auto grid w-[min(480px,100%)] gap-6 py-6">
|
|
17
|
+
{code
|
|
18
|
+
? <Registration key={code} code={code} onChangeCode={() => setSearch({}, { replace: true })} />
|
|
19
|
+
: <CodeForm onSubmit={value => setSearch({ code: value })} />}
|
|
20
|
+
</div>
|
|
21
|
+
</PageLayout>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function CodeForm({ onSubmit }: { onSubmit: (code: string) => void }) {
|
|
25
|
+
const [value, setValue] = useState("");
|
|
26
|
+
const code = normalizeUserCode(value);
|
|
27
|
+
const submit = (event: FormEvent) => {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
if (code) onSubmit(code);
|
|
30
|
+
};
|
|
31
|
+
return <Card className="mk-group">
|
|
32
|
+
<form onSubmit={submit} aria-label="Enter code">
|
|
33
|
+
<Card.Content className="grid gap-4 p-5">
|
|
34
|
+
<p className="text-[14px] text-muted">Enter the code <code>talgent-computer register</code> shows.</p>
|
|
35
|
+
<TextField value={value} onChange={setValue} autoFocus>
|
|
36
|
+
<Label>Code</Label>
|
|
37
|
+
<Input placeholder="XXXX-XXXX" autoComplete="off" spellCheck={false} />
|
|
38
|
+
</TextField>
|
|
39
|
+
<Button type="submit" className="justify-self-start" isDisabled={!code}>Continue</Button>
|
|
40
|
+
</Card.Content>
|
|
41
|
+
</form>
|
|
42
|
+
</Card>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Registration({ code, onChangeCode }: { code: string; onChangeCode: () => void }) {
|
|
46
|
+
const registration = useRegistration(code);
|
|
47
|
+
const approve = useApproveRegistration();
|
|
48
|
+
const now = useNow(30_000);
|
|
49
|
+
if (registration.isPending) return <Card className="mk-group" aria-busy="true"><Card.Content className="grid gap-3 p-5">
|
|
50
|
+
<p className="sr-only" role="status">Checking code…</p>
|
|
51
|
+
<Skeleton className="h-5 w-40 rounded" /><Skeleton className="h-4 w-24 rounded" />
|
|
52
|
+
</Card.Content></Card>;
|
|
53
|
+
if (registration.isError) {
|
|
54
|
+
const refusal = registrationRefusal(registration.error);
|
|
55
|
+
return <Refused text={registrationRefusalText[refusal]} onChangeCode={onChangeCode}
|
|
56
|
+
onRetry={refusal === "failed" ? () => void registration.refetch() : undefined} isRetrying={registration.isFetching} />;
|
|
57
|
+
}
|
|
58
|
+
if (approve.isError && registrationRefusal(approve.error) !== "failed") {
|
|
59
|
+
return <Refused text={registrationRefusalText[registrationRefusal(approve.error)]} onChangeCode={onChangeCode} />;
|
|
60
|
+
}
|
|
61
|
+
const { machineName, expiresAt, approved } = registration.data;
|
|
62
|
+
if (approved || approve.isSuccess) return <Card className="mk-group" role="status"><Card.Content className="grid gap-3 p-5">
|
|
63
|
+
<p className="flex items-center gap-2 text-[15px] font-medium"><CircleCheck size={18} className="text-success" aria-hidden="true" />Approved — return to your terminal</p>
|
|
64
|
+
<p className="text-[14px] text-muted">{machineName} is now one of your computers.</p>
|
|
65
|
+
<Link to="/computers" className="justify-self-start text-[14px] underline underline-offset-4">View your computers</Link>
|
|
66
|
+
</Card.Content></Card>;
|
|
67
|
+
return <Card className="mk-group"><Card.Content className="grid gap-4 p-5">
|
|
68
|
+
<div className="flex items-center gap-3">
|
|
69
|
+
<Monitor size={20} aria-hidden="true" />
|
|
70
|
+
<span className="mk-row-text min-w-0">
|
|
71
|
+
<strong className="truncate">{machineName}</strong>
|
|
72
|
+
<span>Code {code}{expiresAt && <> · {expiryLabel(expiresAt, now)}</>}</span>
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
<p className="text-[14px] text-muted">Approve only a machine you are setting up.</p>
|
|
76
|
+
{approve.isError && <Alert status="danger" role="alert"><Alert.Indicator /><Alert.Content><Alert.Description>Approval didn’t go through. Try again.</Alert.Description></Alert.Content></Alert>}
|
|
77
|
+
<div className="flex gap-2">
|
|
78
|
+
<Button isPending={approve.isPending} onPress={() => approve.mutate(code)}>Approve</Button>
|
|
79
|
+
<Button variant="ghost" isDisabled={approve.isPending} onPress={onChangeCode}>Use another code</Button>
|
|
80
|
+
</div>
|
|
81
|
+
</Card.Content></Card>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function Refused({ text, onChangeCode, onRetry, isRetrying = false }: { text: string; onChangeCode: () => void; onRetry?: () => void; isRetrying?: boolean }) {
|
|
85
|
+
return <Alert status="danger" role="alert"><Alert.Indicator /><Alert.Content>
|
|
86
|
+
<Alert.Title>{text}</Alert.Title>
|
|
87
|
+
<div className="mt-2 flex gap-2">
|
|
88
|
+
{onRetry && <Button size="sm" variant="secondary" isPending={isRetrying} onPress={onRetry}>Try again</Button>}
|
|
89
|
+
<Button size="sm" variant="ghost" onPress={onChangeCode}>Use another code</Button>
|
|
90
|
+
</div>
|
|
91
|
+
</Alert.Content></Alert>;
|
|
92
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Button, Card, Tooltip } from "@heroui/react";
|
|
3
|
+
import { Check, Copy } from "lucide-react";
|
|
4
|
+
import { LoadState } from "../../components/marketplace/marketplace-shared";
|
|
5
|
+
import { installCommands, useComputerRelease } from "../../lib/computer-release";
|
|
6
|
+
|
|
7
|
+
export function InstallCommands() {
|
|
8
|
+
const release = useComputerRelease();
|
|
9
|
+
return <section className="mk-section" aria-labelledby="cp-install">
|
|
10
|
+
<h3 id="cp-install" className="mk-section-title">Add a computer</h3>
|
|
11
|
+
<p className="text-[13px] text-muted">Run these on the machine: macOS, Linux or Windows. Needs Node.js 24 or newer.</p>
|
|
12
|
+
<LoadState noun="the install command" isPending={release.isPending} isError={release.isError} hasData={release.data !== undefined} isFetching={release.isFetching} onRetry={() => void release.refetch()} />
|
|
13
|
+
{release.data !== undefined && <Card className="mk-group">
|
|
14
|
+
<ol className="mk-group-list">
|
|
15
|
+
{installCommands(window.location.origin, release.data).map(command => <li key={command} className="!items-center"><code className="min-w-0 flex-1 break-all">{command}</code><CopyButton text={command} /></li>)}
|
|
16
|
+
</ol>
|
|
17
|
+
</Card>}
|
|
18
|
+
{release.data !== undefined && <p className="text-[13px] text-muted">The last command keeps it running in the background from each login. To run it in a terminal instead, use <code>talgent-computer run</code>.</p>}
|
|
19
|
+
{release.data !== undefined && <p className="text-[13px] text-muted">On Windows, use Command Prompt, or allow npm’s commands in PowerShell first with <code>Set-ExecutionPolicy -Scope CurrentUser RemoteSigned</code>.</p>}
|
|
20
|
+
</section>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function CopyButton({ text }: { text: string }) {
|
|
24
|
+
const [state, setState] = useState<"idle" | "copied" | "failed">("idle");
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (state === "idle") return;
|
|
27
|
+
const timer = window.setTimeout(() => setState("idle"), 2000);
|
|
28
|
+
return () => window.clearTimeout(timer);
|
|
29
|
+
}, [state]);
|
|
30
|
+
const label = state === "copied" ? "Copied" : state === "failed" ? "Couldn’t copy" : "Copy command";
|
|
31
|
+
const copy = () => {
|
|
32
|
+
const clipboard = navigator.clipboard as Clipboard | undefined;
|
|
33
|
+
if (!clipboard) { setState("failed"); return; }
|
|
34
|
+
clipboard.writeText(text).then(() => setState("copied"), () => setState("failed"));
|
|
35
|
+
};
|
|
36
|
+
return <><Tooltip delay={300}>
|
|
37
|
+
<Button size="sm" variant="ghost" isIconOnly aria-label={label} onPress={copy}>
|
|
38
|
+
{state === "copied" ? <Check size={15} aria-hidden="true" /> : <Copy size={15} aria-hidden="true" />}
|
|
39
|
+
</Button>
|
|
40
|
+
<Tooltip.Content>{label}</Tooltip.Content>
|
|
41
|
+
</Tooltip><span className="sr-only" role="status">{state === "idle" ? "" : label}</span></>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Alert, AlertDialog, Button, Tooltip } from "@heroui/react";
|
|
3
|
+
import { Trash2 } from "lucide-react";
|
|
4
|
+
import { removeComputerErrorMessage, useRemoveComputer, type ManagedComputer } from "../../lib/computer-service";
|
|
5
|
+
|
|
6
|
+
/** Removes one of the person's Managed Computers for good, after saying what
|
|
7
|
+
* happens to its Works. Cancel has focus; a failure keeps the dialog open. */
|
|
8
|
+
export function RemoveComputerButton({ computer }: { computer: ManagedComputer }) {
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
return <>
|
|
11
|
+
<Tooltip delay={300}><Button size="sm" variant="ghost" isIconOnly aria-label={`Remove ${computer.name}`} onPress={() => setOpen(true)}><Trash2 size={15} aria-hidden="true" /></Button><Tooltip.Content>Remove</Tooltip.Content></Tooltip>
|
|
12
|
+
{open && <RemoveComputerDialog computer={computer} onClose={() => setOpen(false)} />}
|
|
13
|
+
</>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function RemoveComputerDialog({ computer, onClose }: { computer: ManagedComputer; onClose: () => void }) {
|
|
17
|
+
const remove = useRemoveComputer(computer.id);
|
|
18
|
+
return <AlertDialog><AlertDialog.Backdrop isOpen onOpenChange={open => { if (!open && !remove.isPending) onClose(); }}><AlertDialog.Container><AlertDialog.Dialog className="sm:max-w-[440px]">
|
|
19
|
+
<AlertDialog.Header><AlertDialog.Icon status="danger" /><AlertDialog.Heading>Remove {computer.name}?</AlertDialog.Heading></AlertDialog.Header>
|
|
20
|
+
<AlertDialog.Body>
|
|
21
|
+
<div className="flex flex-col gap-3 text-sm">
|
|
22
|
+
<p>It can no longer connect or run anything, and Talgent Computer ends every Work process on it.</p>
|
|
23
|
+
<p className="text-muted">Its running Works fail and closing ones close. Its stopped and failed Works can only be closed. Their files stay on the machine.</p>
|
|
24
|
+
<p className="text-xs text-muted">To use this machine again, register it as a new computer.</p>
|
|
25
|
+
{remove.isError && <Alert status="danger" role="alert"><Alert.Indicator /><Alert.Content><Alert.Description>{removeComputerErrorMessage(remove.error)}</Alert.Description></Alert.Content></Alert>}
|
|
26
|
+
</div>
|
|
27
|
+
</AlertDialog.Body>
|
|
28
|
+
<AlertDialog.Footer>
|
|
29
|
+
<Button slot="close" variant="tertiary" autoFocus isDisabled={remove.isPending}>Cancel</Button>
|
|
30
|
+
<Button variant="danger" isPending={remove.isPending} onPress={() => remove.mutate(undefined, { onSuccess: onClose })}>Remove</Button>
|
|
31
|
+
</AlertDialog.Footer>
|
|
32
|
+
</AlertDialog.Dialog></AlertDialog.Container></AlertDialog.Backdrop></AlertDialog>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Alert, Button, FieldError, Input, Label, Modal, TextField, Tooltip } from "@heroui/react";
|
|
3
|
+
import { Pencil } from "lucide-react";
|
|
4
|
+
import { computerNameProblem, useRenameComputer, type ManagedComputer } from "../../lib/computer-service";
|
|
5
|
+
|
|
6
|
+
/** Renames one of the person's Managed Computers. */
|
|
7
|
+
export function RenameComputerButton({ computer }: { computer: ManagedComputer }) {
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
return <>
|
|
10
|
+
<Tooltip delay={300}><Button size="sm" variant="ghost" isIconOnly aria-label={`Rename ${computer.name}`} onPress={() => setOpen(true)}><Pencil size={15} aria-hidden="true" /></Button><Tooltip.Content>Rename</Tooltip.Content></Tooltip>
|
|
11
|
+
{open && <RenameComputerDialog computer={computer} onClose={() => setOpen(false)} />}
|
|
12
|
+
</>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function RenameComputerDialog({ computer, onClose }: { computer: ManagedComputer; onClose: () => void }) {
|
|
16
|
+
const [value, setValue] = useState(computer.name);
|
|
17
|
+
const rename = useRenameComputer(computer.id);
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
const problem = computerNameProblem(value);
|
|
20
|
+
const unchanged = trimmed === computer.name;
|
|
21
|
+
const submit = () => {
|
|
22
|
+
if (problem || unchanged || rename.isPending) return;
|
|
23
|
+
rename.mutate(trimmed, { onSuccess: onClose });
|
|
24
|
+
};
|
|
25
|
+
return <Modal><Modal.Backdrop isOpen onOpenChange={open => { if (!open && !rename.isPending) onClose(); }} isDismissable={!rename.isPending}><Modal.Container placement="center" size="sm"><Modal.Dialog>
|
|
26
|
+
<Modal.Header><Modal.Heading>Rename computer</Modal.Heading></Modal.Header>
|
|
27
|
+
<Modal.Body>
|
|
28
|
+
<TextField value={value} onChange={setValue} isInvalid={Boolean(problem)} isDisabled={rename.isPending} autoFocus>
|
|
29
|
+
<Label>Name</Label>
|
|
30
|
+
<Input onKeyDown={event => { if (event.key === "Enter") submit(); }} />
|
|
31
|
+
<FieldError>{problem}</FieldError>
|
|
32
|
+
</TextField>
|
|
33
|
+
{rename.isError && <Alert status="danger" role="alert"><Alert.Indicator /><Alert.Content><Alert.Description>The computer couldn’t be renamed. Try again.</Alert.Description></Alert.Content></Alert>}
|
|
34
|
+
</Modal.Body>
|
|
35
|
+
<Modal.Footer><Button variant="secondary" isDisabled={rename.isPending} onPress={onClose}>Cancel</Button><Button isPending={rename.isPending} isDisabled={Boolean(problem) || unchanged} onPress={submit}>Rename</Button></Modal.Footer>
|
|
36
|
+
</Modal.Dialog></Modal.Container></Modal.Backdrop></Modal>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Alert, Button, Description, FieldError, Label, Modal, NumberField, Radio, RadioGroup, Tooltip } from "@heroui/react";
|
|
3
|
+
import { SlidersHorizontal } from "lucide-react";
|
|
4
|
+
import { capacityProblem, maxCapacity, roomRulesErrorMessage, useSetRoomRules, type AdmissionPolicy, type ManagedComputer } from "../../lib/computer-service";
|
|
5
|
+
|
|
6
|
+
/** Sets how many Works one of the person's Managed Computers runs at once and
|
|
7
|
+
* what it does with a run while full. */
|
|
8
|
+
export function RoomRulesButton({ computer }: { computer: ManagedComputer }) {
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
return <>
|
|
11
|
+
<Tooltip delay={300}><Button size="sm" variant="ghost" isIconOnly aria-label={`Capacity of ${computer.name}`} onPress={() => setOpen(true)}><SlidersHorizontal size={15} aria-hidden="true" /></Button><Tooltip.Content>Capacity</Tooltip.Content></Tooltip>
|
|
12
|
+
{open && <RoomRulesDialog computer={computer} onClose={() => setOpen(false)} />}
|
|
13
|
+
</>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const policies: { value: AdmissionPolicy; title: string; description: string }[] = [
|
|
17
|
+
{ value: "reject", title: "Reject", description: "A new run is refused until a running Work ends." },
|
|
18
|
+
{ value: "queue", title: "Queue", description: "A new run waits as Starting and begins once a running Work ends." },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function RoomRulesDialog({ computer, onClose }: { computer: ManagedComputer; onClose: () => void }) {
|
|
22
|
+
const [capacity, setCapacity] = useState(computer.capacity);
|
|
23
|
+
const [policy, setPolicy] = useState<AdmissionPolicy>(computer.policy);
|
|
24
|
+
const rules = useSetRoomRules(computer.id);
|
|
25
|
+
const problem = capacityProblem(capacity, computer.running);
|
|
26
|
+
const unchanged = capacity === computer.capacity && policy === computer.policy;
|
|
27
|
+
const submit = () => {
|
|
28
|
+
if (problem || unchanged || rules.isPending) return;
|
|
29
|
+
rules.mutate({ capacity, policy }, { onSuccess: onClose });
|
|
30
|
+
};
|
|
31
|
+
return <Modal><Modal.Backdrop isOpen onOpenChange={open => { if (!open && !rules.isPending) onClose(); }} isDismissable={!rules.isPending}><Modal.Container placement="center" size="sm"><Modal.Dialog>
|
|
32
|
+
<Modal.Header><Modal.Heading>Capacity of {computer.name}</Modal.Heading></Modal.Header>
|
|
33
|
+
<Modal.Body className="grid gap-5">
|
|
34
|
+
<NumberField value={capacity} onChange={setCapacity} minValue={1} maxValue={maxCapacity} step={1} isInvalid={Boolean(problem)} isDisabled={rules.isPending} autoFocus>
|
|
35
|
+
<Label>Works at once</Label>
|
|
36
|
+
<NumberField.Group>
|
|
37
|
+
<NumberField.DecrementButton />
|
|
38
|
+
<NumberField.Input className="w-16" />
|
|
39
|
+
<NumberField.IncrementButton />
|
|
40
|
+
</NumberField.Group>
|
|
41
|
+
<Description>{computer.running} running now</Description>
|
|
42
|
+
<FieldError>{problem}</FieldError>
|
|
43
|
+
</NumberField>
|
|
44
|
+
<RadioGroup value={policy} onChange={value => setPolicy(value as AdmissionPolicy)} isDisabled={rules.isPending}>
|
|
45
|
+
<Label>When it’s full</Label>
|
|
46
|
+
{policies.map(option => <Radio key={option.value} value={option.value}>
|
|
47
|
+
<Radio.Control><Radio.Indicator /></Radio.Control>
|
|
48
|
+
<Radio.Content><Label>{option.title}</Label><Description>{option.description}</Description></Radio.Content>
|
|
49
|
+
</Radio>)}
|
|
50
|
+
</RadioGroup>
|
|
51
|
+
{rules.isError && <Alert status="danger" role="alert"><Alert.Indicator /><Alert.Content><Alert.Description>{roomRulesErrorMessage(rules.error)}</Alert.Description></Alert.Content></Alert>}
|
|
52
|
+
</Modal.Body>
|
|
53
|
+
<Modal.Footer><Button variant="secondary" isDisabled={rules.isPending} onPress={onClose}>Cancel</Button><Button isPending={rules.isPending} isDisabled={Boolean(problem) || unchanged} onPress={submit}>Save</Button></Modal.Footer>
|
|
54
|
+
</Modal.Dialog></Modal.Container></Modal.Backdrop></Modal>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* Sign-in page. The product site (talgent.work) tells the story; this page
|
|
2
|
+
only signs people in, in the site's visual language: a porcelain canvas lit
|
|
3
|
+
from above in the product's violet with a warm edge. */
|
|
4
|
+
.si-page { position: relative; isolation: isolate; overflow: hidden; background: #fbfbfd; }
|
|
5
|
+
.si-page::before { content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
|
|
6
|
+
background:
|
|
7
|
+
radial-gradient(75% 60% at 50% -10%, rgba(106,76,245,.16), rgba(106,76,245,.05) 45%, transparent 75%),
|
|
8
|
+
radial-gradient(40% 40% at 74% 0%, rgba(240,164,58,.12), transparent 75%),
|
|
9
|
+
radial-gradient(40% 40% at 24% 0%, rgba(56,120,255,.09), transparent 75%); }
|
|
10
|
+
/* Threads converge on the centre and fade before they reach the content. */
|
|
11
|
+
.si-threads { position: absolute; inset: 0; z-index: -1; width: 100%; height: 100%; pointer-events: none;
|
|
12
|
+
mask-image: radial-gradient(closest-side at 50% 47%, transparent 22%, #000 62%); -webkit-mask-image: radial-gradient(closest-side at 50% 47%, transparent 22%, #000 62%); }
|
|
13
|
+
.si-thread { fill: none; stroke: url(#si-thread); stroke-width: 1.2; vector-effect: non-scaling-stroke; opacity: .7; }
|
|
14
|
+
.si-thread-pulse { fill: none; stroke: url(#si-pulse); stroke-width: 2; stroke-linecap: round;
|
|
15
|
+
stroke-dasharray: .05 .95; stroke-dashoffset: 1; filter: drop-shadow(0 0 4px rgba(143,123,255,.9));
|
|
16
|
+
animation: si-travel linear infinite; }
|
|
17
|
+
@keyframes si-travel { to { stroke-dashoffset: 0; } }
|
|
18
|
+
|
|
19
|
+
@media (max-width: 640px) {
|
|
20
|
+
.si-threads { opacity: .55; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@media (prefers-reduced-motion: reduce) {
|
|
24
|
+
.si-thread-pulse { animation: none; opacity: 0; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.si-brand { --brand-foreground: #0f0d1d; transform: scale(1.5); transform-origin: center; }
|
|
28
|
+
.si-github-button { justify-self: center; width: min(100%, 17.5rem); height: 52px; font-size: 15px; box-shadow: 0 12px 28px -12px rgba(40,24,140,.55); }
|
|
29
|
+
.si-github-mark { display: inline-block; width: 1rem; height: 1rem; background: currentColor; mask-size: contain; mask-repeat: no-repeat; mask-position: center;
|
|
30
|
+
-webkit-mask-size: contain; -webkit-mask-repeat: no-repeat; -webkit-mask-position: center; }
|
|
31
|
+
.si-site-link { color: #6b6980; font-size: 13px; text-decoration: none; }
|
|
32
|
+
.si-site-link a { color: #0f0d1d; font-weight: 500; text-underline-offset: 3px; }
|
|
33
|
+
.si-site-link a:hover { text-decoration: underline; }
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Alert, Button, Spinner } from "@heroui/react";
|
|
3
|
+
import { useSearchParams } from "react-router-dom";
|
|
4
|
+
import github from "../assets/brand-icons/github.svg";
|
|
5
|
+
import { safeInternalPath, signInHref } from "../lib/session";
|
|
6
|
+
import { LoginThreads } from "./login-threads";
|
|
7
|
+
import "./login-page.css";
|
|
8
|
+
|
|
9
|
+
const PRODUCT_SITE = "https://talgent.work";
|
|
10
|
+
|
|
11
|
+
// Why Account sent the person back here instead of into the workspace: one
|
|
12
|
+
// notice per failure code Account's /auth handler redirects with
|
|
13
|
+
// (internal/business/account/transport/http/handler.go); change both.
|
|
14
|
+
const FAILURES: Record<string, { title: string; body: string }> = {
|
|
15
|
+
denied: { title: "GitHub sign-in was cancelled", body: "You left GitHub without authorizing Talgent. Nothing was changed. Try again when you are ready." },
|
|
16
|
+
expired: { title: "This sign-in attempt expired", body: "The link back from GitHub was older than ten minutes or opened in another browser. Start again from here." },
|
|
17
|
+
email: { title: "GitHub did not share an email address", body: "Talgent needs a verified email on your GitHub account. Add one at github.com/settings/emails, then try again." },
|
|
18
|
+
unavailable: { title: "GitHub sign-in didn’t finish", body: "Talgent couldn’t complete the sign-in with GitHub. Try again in a moment." },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// The approved brand lockup from the sidebar.
|
|
22
|
+
function Brand() {
|
|
23
|
+
return (
|
|
24
|
+
<div className="p-brand-lockup" role="img" aria-label="Talgent">
|
|
25
|
+
<span className="p-brand-icon" aria-hidden="true" />
|
|
26
|
+
<span className="p-brand-name" aria-hidden="true" />
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The Marketplace GitHub mark, tinted through a mask so it reads white on the
|
|
32
|
+
// black primary button.
|
|
33
|
+
function GitHubMark() {
|
|
34
|
+
return <span aria-hidden="true" className="si-github-mark" style={{ maskImage: `url("${github}")`, WebkitMaskImage: `url("${github}")` }} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function FailureNotice({ code }: { code: string | null }) {
|
|
38
|
+
const failure = code ? FAILURES[code] : undefined;
|
|
39
|
+
if (!failure) return null;
|
|
40
|
+
return (
|
|
41
|
+
<Alert status="danger" role="alert">
|
|
42
|
+
<Alert.Indicator />
|
|
43
|
+
<Alert.Content>
|
|
44
|
+
<Alert.Title>{failure.title}</Alert.Title>
|
|
45
|
+
<Alert.Description>{failure.body}</Alert.Description>
|
|
46
|
+
</Alert.Content>
|
|
47
|
+
</Alert>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// Signing in happens on GitHub; this page says where the person is before
|
|
53
|
+
// they leave, and why they came back if it did not work. Telling people what
|
|
54
|
+
// Talgent is belongs to the product site, which it links to.
|
|
55
|
+
export function LoginPage() {
|
|
56
|
+
const [searchParams] = useSearchParams();
|
|
57
|
+
const [leaving, setLeaving] = useState(false);
|
|
58
|
+
const next = safeInternalPath(searchParams.get("next"));
|
|
59
|
+
// Coming back from GitHub with the browser's Back button restores this page
|
|
60
|
+
// from cache; the button must be usable again.
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const restore = (event: PageTransitionEvent) => { if (event.persisted) setLeaving(false); };
|
|
63
|
+
window.addEventListener("pageshow", restore);
|
|
64
|
+
return () => window.removeEventListener("pageshow", restore);
|
|
65
|
+
}, []);
|
|
66
|
+
const continueWithGitHub = () => {
|
|
67
|
+
setLeaving(true);
|
|
68
|
+
window.location.assign(signInHref(next));
|
|
69
|
+
};
|
|
70
|
+
return (
|
|
71
|
+
<main className="si-page grid min-h-dvh place-items-center p-6">
|
|
72
|
+
<LoginThreads />
|
|
73
|
+
<div className="grid w-full max-w-[22rem] justify-items-center gap-10">
|
|
74
|
+
<div className="si-brand my-3"><Brand /></div>
|
|
75
|
+
{/* The button says it all on screen; the heading stays for assistive tech. */}
|
|
76
|
+
<h1 className="sr-only">Sign in to Talgent</h1>
|
|
77
|
+
<div className="grid w-full gap-4">
|
|
78
|
+
<FailureNotice code={searchParams.get("error")} />
|
|
79
|
+
<Button variant="primary" className="si-github-button" isPending={leaving} onPress={continueWithGitHub}>
|
|
80
|
+
{leaving ? <Spinner size="sm" color="current" aria-hidden="true" /> : <GitHubMark />}
|
|
81
|
+
{leaving ? "Opening GitHub" : "Continue with GitHub"}
|
|
82
|
+
</Button>
|
|
83
|
+
</div>
|
|
84
|
+
<p className="si-site-link">New to Talgent? <a href={PRODUCT_SITE}>Visit talgent.work</a></p>
|
|
85
|
+
</div>
|
|
86
|
+
</main>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Abstract threads behind the sign-in: streams of work converging on the
|
|
2
|
+
// centre and fanning out again, with a few lights travelling along them. They
|
|
3
|
+
// echo the product's collaboration chain; purely decorative.
|
|
4
|
+
const centre = { x: 800, y: 470 };
|
|
5
|
+
|
|
6
|
+
const streams = [
|
|
7
|
+
{ from: 90, to: 160, duration: 11, delay: -1 },
|
|
8
|
+
{ from: 250, to: 330, duration: 13, delay: -6 },
|
|
9
|
+
{ from: 420, to: 560, duration: 9, delay: -3 },
|
|
10
|
+
{ from: 640, to: 760, duration: 12, delay: -8 },
|
|
11
|
+
{ from: 840, to: 900, duration: 10, delay: -4 },
|
|
12
|
+
{ from: 980, to: 60, duration: 14, delay: -10 },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function streamPath(from: number, to: number) {
|
|
16
|
+
return `M -60 ${from} C 330 ${from}, 520 ${centre.y}, ${centre.x} ${centre.y} S 1270 ${to}, 1660 ${to}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function LoginThreads() {
|
|
20
|
+
return (
|
|
21
|
+
<svg className="si-threads" viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false">
|
|
22
|
+
<defs>
|
|
23
|
+
<linearGradient id="si-thread" x1="0" y1="0" x2="1600" y2="0" gradientUnits="userSpaceOnUse">
|
|
24
|
+
<stop offset="0" stopColor="#6a4cf5" stopOpacity="0" />
|
|
25
|
+
<stop offset=".3" stopColor="#6a4cf5" stopOpacity=".55" />
|
|
26
|
+
<stop offset=".7" stopColor="#8f7bff" stopOpacity=".55" />
|
|
27
|
+
<stop offset="1" stopColor="#f0a43a" stopOpacity="0" />
|
|
28
|
+
</linearGradient>
|
|
29
|
+
<linearGradient id="si-pulse" x1="0" y1="0" x2="1600" y2="0" gradientUnits="userSpaceOnUse">
|
|
30
|
+
<stop offset="0" stopColor="#8f7bff" />
|
|
31
|
+
<stop offset=".6" stopColor="#c59bff" />
|
|
32
|
+
<stop offset="1" stopColor="#f0a43a" />
|
|
33
|
+
</linearGradient>
|
|
34
|
+
</defs>
|
|
35
|
+
{streams.map((stream) => {
|
|
36
|
+
const d = streamPath(stream.from, stream.to);
|
|
37
|
+
return (
|
|
38
|
+
<g key={stream.from}>
|
|
39
|
+
<path className="si-thread" d={d} />
|
|
40
|
+
<path
|
|
41
|
+
className="si-thread-pulse"
|
|
42
|
+
d={d}
|
|
43
|
+
pathLength={1}
|
|
44
|
+
style={{ animationDuration: `${stream.duration}s`, animationDelay: `${stream.delay}s` }}
|
|
45
|
+
/>
|
|
46
|
+
</g>
|
|
47
|
+
);
|
|
48
|
+
})}
|
|
49
|
+
</svg>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Navigate, Route, Routes, useParams } from "react-router-dom";
|
|
2
|
+
import { ConnectorList, ConnectorPage } from "../components/marketplace/connector-pages";
|
|
3
|
+
import { LoadState } from "../components/marketplace/marketplace-shared";
|
|
4
|
+
import { MarketplaceShell } from "../components/marketplace/marketplace-shell";
|
|
5
|
+
import { PluginList } from "../components/marketplace/plugin-list";
|
|
6
|
+
import { PluginPage } from "../components/marketplace/plugin-page";
|
|
7
|
+
import { ProviderPage } from "../components/marketplace/provider-pages";
|
|
8
|
+
import { usePlugin } from "../lib/marketplace-catalog";
|
|
9
|
+
import { providerName, serviceLabel } from "../lib/marketplace-connections";
|
|
10
|
+
import { pluginDisplayName, pluginPresentation } from "../lib/marketplace-plugins";
|
|
11
|
+
import { marketplacePaths } from "../lib/marketplace-routes";
|
|
12
|
+
|
|
13
|
+
// Marketplace › Section › Item. Every object is a page; the topbar trail
|
|
14
|
+
// carries the levels above the current page, and the topbar switches the
|
|
15
|
+
// catalog between Plugins and Connectors.
|
|
16
|
+
const plugins = { label: "Plugins", to: marketplacePaths.plugins };
|
|
17
|
+
const connectors = { label: "Connectors", to: marketplacePaths.connectors };
|
|
18
|
+
// Provider Authorizations are about the person, so their pages sit under Profile.
|
|
19
|
+
const profile = { label: "Profile", to: "/profile" };
|
|
20
|
+
|
|
21
|
+
export function MarketplacePage() {
|
|
22
|
+
return <Routes>
|
|
23
|
+
<Route index element={<MarketplaceShell title="Plugins"><PluginList /></MarketplaceShell>} />
|
|
24
|
+
<Route path="plugins" element={<Navigate to={marketplacePaths.plugins} replace />} />
|
|
25
|
+
<Route path="plugins/categories/:category" element={<CategoryRoute kind="plugins" />} />
|
|
26
|
+
<Route path="plugins/:slug" element={<PluginRoute />} />
|
|
27
|
+
<Route path="connectors" element={<MarketplaceShell title="Connectors"><ConnectorList /></MarketplaceShell>} />
|
|
28
|
+
<Route path="connectors/categories/:category" element={<CategoryRoute kind="connectors" />} />
|
|
29
|
+
<Route path="connectors/:name" element={<ConnectorRoute />} />
|
|
30
|
+
<Route path="providers" element={<Navigate to="/profile" replace />} />
|
|
31
|
+
<Route path="providers/:provider" element={<ProviderRoute />} />
|
|
32
|
+
<Route path="*" element={<Navigate to={marketplacePaths.plugins} replace />} />
|
|
33
|
+
</Routes>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CategoryRoute({ kind }: { kind: "plugins" | "connectors" }) {
|
|
37
|
+
const category = useParams().category ?? "";
|
|
38
|
+
return <MarketplaceShell title={category} trail={[kind === "plugins" ? plugins : connectors]}>
|
|
39
|
+
{kind === "plugins" ? <PluginList category={category} /> : <ConnectorList category={category} />}
|
|
40
|
+
</MarketplaceShell>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function PluginRoute() {
|
|
44
|
+
const slug = useParams().slug ?? "";
|
|
45
|
+
const plugin = usePlugin(slug);
|
|
46
|
+
return <MarketplaceShell title={plugin.data ? pluginPresentation(plugin.data).name : pluginDisplayName(slug)} trail={[plugins]}>
|
|
47
|
+
{plugin.data ? <PluginPage key={slug} plugin={plugin.data} /> : <LoadState noun="plugin" isPending={plugin.isPending} isError={plugin.isError} hasData={false} isFetching={plugin.isFetching} onRetry={() => void plugin.refetch()} />}
|
|
48
|
+
</MarketplaceShell>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ConnectorRoute() {
|
|
52
|
+
const name = useParams().name ?? "";
|
|
53
|
+
return <MarketplaceShell title={serviceLabel(name)} trail={[connectors]}><ConnectorPage key={name} name={name} /></MarketplaceShell>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ProviderRoute() {
|
|
57
|
+
const provider = useParams().provider ?? "";
|
|
58
|
+
return <MarketplaceShell title={providerName(provider)} root={profile}><ProviderPage key={provider} provider={provider} /></MarketplaceShell>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Avatar, Button, Tooltip } from "@heroui/react";
|
|
2
|
+
import { Info, LogOut } from "lucide-react";
|
|
3
|
+
import { PageLayout } from "../components/shell/page-layout";
|
|
4
|
+
import { ProviderRows } from "../components/marketplace/provider-pages";
|
|
5
|
+
import { useProviderAuthorizationCallback } from "../components/profile/provider-callback";
|
|
6
|
+
import { WorkAgentAppearance } from "../components/profile/work-agent-appearance";
|
|
7
|
+
import { WorkAgentSoul } from "../components/profile/work-agent-soul";
|
|
8
|
+
import { useCurrentUser, useLogout } from "../lib/account-service";
|
|
9
|
+
import { avatarInitial, safeAvatarUrl, safeDisplayName, safeEmail } from "../lib/user-display";
|
|
10
|
+
import "../components/marketplace/marketplace.css";
|
|
11
|
+
|
|
12
|
+
// Profile holds everything about the person: who they are, as GitHub
|
|
13
|
+
// reports it; the Work Agent that represents them and its Soul; the Provider Authorizations
|
|
14
|
+
// their agents act with; and signing out of this device, beside who they are so
|
|
15
|
+
// it stays in reach however many Providers follow.
|
|
16
|
+
export function ProfilePage() {
|
|
17
|
+
const connecting = useProviderAuthorizationCallback();
|
|
18
|
+
const current = useCurrentUser();
|
|
19
|
+
const logout = useLogout();
|
|
20
|
+
const user = current.data?.user;
|
|
21
|
+
const email = safeEmail(user?.primaryEmail);
|
|
22
|
+
const displayName = safeDisplayName(user?.displayName, user?.id, email || "Profile");
|
|
23
|
+
const username = safeDisplayName(user?.username, user?.id, "");
|
|
24
|
+
const avatarUrl = safeAvatarUrl(user?.avatarUrl);
|
|
25
|
+
if (connecting) return <PageLayout workspaceTitle="Profile"><p className="sr-only" role="status">Connecting your account…</p></PageLayout>;
|
|
26
|
+
return (
|
|
27
|
+
<PageLayout workspaceTitle="Profile">
|
|
28
|
+
<div className="mx-auto grid w-[min(640px,100%)] gap-8 py-6">
|
|
29
|
+
<header className="mk-detail-head !items-center">
|
|
30
|
+
<Avatar aria-label={`${displayName} avatar`} className="size-16 shrink-0 rounded-full">
|
|
31
|
+
{avatarUrl && <Avatar.Image src={avatarUrl} alt="" />}
|
|
32
|
+
<Avatar.Fallback className="text-[26px] font-semibold">{avatarInitial(displayName, email, "P")}</Avatar.Fallback>
|
|
33
|
+
</Avatar>
|
|
34
|
+
<div className="mk-detail-title">
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
<h2>{displayName}</h2>
|
|
37
|
+
<Tooltip delay={200}>
|
|
38
|
+
<Tooltip.Trigger aria-label="Where these details come from" className="rounded-full text-muted"><Info size={15} aria-hidden="true" /></Tooltip.Trigger>
|
|
39
|
+
<Tooltip.Content>From your GitHub profile; updated each time you sign in.</Tooltip.Content>
|
|
40
|
+
</Tooltip>
|
|
41
|
+
</div>
|
|
42
|
+
<p className="flex flex-wrap gap-x-3 text-[14px] text-muted">{username && <span>@{username}</span>}{email && <span>{email}</span>}</p>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="mk-detail-actions">
|
|
45
|
+
<Tooltip delay={300}>
|
|
46
|
+
<Button size="sm" variant="secondary" isPending={logout.isPending} onPress={() => logout.mutate()}>
|
|
47
|
+
<LogOut size={15} aria-hidden="true" />{logout.isPending ? "Signing out" : "Sign out"}
|
|
48
|
+
</Button>
|
|
49
|
+
<Tooltip.Content>Signs out this device only; other devices stay signed in.</Tooltip.Content>
|
|
50
|
+
</Tooltip>
|
|
51
|
+
</div>
|
|
52
|
+
</header>
|
|
53
|
+
<WorkAgentAppearance />
|
|
54
|
+
<WorkAgentSoul />
|
|
55
|
+
<section className="mk-section" aria-labelledby="pf-providers">
|
|
56
|
+
<div className="flex items-center gap-2">
|
|
57
|
+
<h3 id="pf-providers" className="mk-section-title">Providers</h3>
|
|
58
|
+
<Tooltip delay={200}>
|
|
59
|
+
<Tooltip.Trigger aria-label="About Providers" className="rounded-full text-muted"><Info size={15} aria-hidden="true" /></Tooltip.Trigger>
|
|
60
|
+
<Tooltip.Content>Accounts your agents act with in Works you start. Connecting installs nothing.</Tooltip.Content>
|
|
61
|
+
</Tooltip>
|
|
62
|
+
</div>
|
|
63
|
+
<ProviderRows />
|
|
64
|
+
</section>
|
|
65
|
+
</div>
|
|
66
|
+
</PageLayout>
|
|
67
|
+
);
|
|
68
|
+
}
|