@intelligo-dev/cli 1.0.0-beta.13 → 1.0.0-beta.15
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 +65 -2
- package/dist/args.d.ts +10 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +20 -0
- package/dist/args.js.map +1 -0
- package/dist/bin.js +103 -22
- package/dist/bin.js.map +1 -1
- package/dist/commands/add.d.ts +4 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +28 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/create-flow.d.ts +8 -1
- package/dist/commands/create-flow.d.ts.map +1 -1
- package/dist/commands/create-flow.js +101 -34
- package/dist/commands/create-flow.js.map +1 -1
- package/dist/commands/create.d.ts +24 -4
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +57 -14
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +123 -35
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/migrate-check.d.ts +20 -1
- package/dist/commands/migrate-check.d.ts.map +1 -1
- package/dist/commands/migrate-check.js +35 -6
- package/dist/commands/migrate-check.js.map +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +2 -8
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/sync-messages.d.ts +37 -0
- package/dist/commands/sync-messages.d.ts.map +1 -0
- package/dist/commands/sync-messages.js +88 -0
- package/dist/commands/sync-messages.js.map +1 -0
- package/dist/commands/sync-scaffold.d.ts +36 -0
- package/dist/commands/sync-scaffold.d.ts.map +1 -0
- package/dist/commands/sync-scaffold.js +53 -0
- package/dist/commands/sync-scaffold.js.map +1 -0
- package/dist/commands/sync.d.ts +109 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +392 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/upgrade-check.d.ts.map +1 -1
- package/dist/commands/upgrade-check.js +4 -1
- package/dist/commands/upgrade-check.js.map +1 -1
- package/dist/env-files.d.ts +11 -0
- package/dist/env-files.d.ts.map +1 -1
- package/dist/env-files.js +28 -9
- package/dist/env-files.js.map +1 -1
- package/dist/manifest.d.ts +27 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +30 -3
- package/dist/manifest.js.map +1 -1
- package/dist/module-exports.d.ts +63 -0
- package/dist/module-exports.d.ts.map +1 -0
- package/dist/module-exports.js +231 -0
- package/dist/module-exports.js.map +1 -0
- package/dist/registry-bundle.d.ts +43 -0
- package/dist/registry-bundle.d.ts.map +1 -0
- package/dist/registry-bundle.js +74 -0
- package/dist/registry-bundle.js.map +1 -0
- package/dist/registry-items.d.ts +41 -13
- package/dist/registry-items.d.ts.map +1 -1
- package/dist/registry-items.js +117 -33
- package/dist/registry-items.js.map +1 -1
- package/package.json +1 -1
- package/src/args.ts +25 -0
- package/src/bin.ts +124 -22
- package/src/commands/add.ts +39 -12
- package/src/commands/create-flow.ts +119 -29
- package/src/commands/create.ts +82 -14
- package/src/commands/doctor.ts +156 -47
- package/src/commands/migrate-check.ts +51 -10
- package/src/commands/migrate.ts +2 -8
- package/src/commands/sync-messages.ts +114 -0
- package/src/commands/sync-scaffold.ts +83 -0
- package/src/commands/sync.ts +574 -0
- package/src/commands/upgrade-check.ts +4 -1
- package/src/env-files.ts +28 -7
- package/src/manifest.ts +57 -3
- package/src/module-exports.ts +266 -0
- package/src/registry-bundle.ts +103 -0
- package/src/registry-items.ts +141 -36
- package/templates/admin-page/admin-page.tsx.tpl +96 -24
- package/templates/app-scaffold/gitignore.tpl +25 -0
- package/templates/app-scaffold/intelligo.ts.tpl +6 -47
- package/templates/app-scaffold/next.config.mjs.tpl +31 -2
- package/templates/app-scaffold/package.json.tpl +2 -2
- package/templates/manifest.json +40 -3
- package/templates/pnpm-standalone/npmrc.tpl +6 -0
- package/templates/pnpm-standalone/pnpm-workspace.yaml.tpl +10 -0
- package/templates/registry/ai-agent-activity.json +23 -0
- package/templates/registry/ai-agent-progress.json +21 -0
- package/templates/registry/ai-approval-card.json +27 -0
- package/templates/registry/ai-artifact.json +22 -0
- package/templates/registry/ai-branch.json +21 -0
- package/templates/registry/ai-citations.json +24 -0
- package/templates/registry/ai-code-block.json +23 -0
- package/templates/registry/ai-composer-menu.json +19 -0
- package/templates/registry/ai-file-diff.json +25 -0
- package/templates/registry/ai-image-generation.json +23 -0
- package/templates/registry/ai-markdown.json +23 -0
- package/templates/registry/ai-message-bubble.json +23 -0
- package/templates/registry/ai-message-scroller.json +22 -0
- package/templates/registry/ai-message.json +21 -0
- package/templates/registry/ai-motion.json +19 -0
- package/templates/registry/ai-prompt-input.json +28 -0
- package/templates/registry/ai-reasoning-text.json +22 -0
- package/templates/registry/ai-reasoning.json +22 -0
- package/templates/registry/ai-shimmer-text.json +19 -0
- package/templates/registry/ai-sidebar.json +25 -0
- package/templates/registry/ai-speech-input.json +21 -0
- package/templates/registry/ai-streaming-response.json +24 -0
- package/templates/registry/ai-suggestion.json +19 -0
- package/templates/registry/ai-todo-list.json +22 -0
- package/templates/registry/ai-tool-approval.json +26 -0
- package/templates/registry/ai-tool-result.json +25 -0
- package/templates/registry/alert-dialog.json +23 -0
- package/templates/registry/animated-list.json +21 -0
- package/templates/registry/app-shell.json +93 -0
- package/templates/registry/artifacts.json +82 -0
- package/templates/registry/attachment.json +22 -0
- package/templates/registry/auth-email-verification.json +39 -0
- package/templates/registry/auth-login.json +72 -0
- package/templates/registry/auth-password-reset.json +53 -0
- package/templates/registry/auth-signup.json +41 -0
- package/templates/registry/billing-settings.json +66 -0
- package/templates/registry/button.json +22 -0
- package/templates/registry/chat-eve.json +19 -0
- package/templates/registry/chat-panel.json +30 -0
- package/templates/registry/chat-share.json +42 -0
- package/templates/registry/chat-widget.json +34 -0
- package/templates/registry/chat.json +326 -0
- package/templates/registry/checkbox.json +22 -0
- package/templates/registry/checkout.json +40 -0
- package/templates/registry/collapsible.json +19 -0
- package/templates/registry/command.json +23 -0
- package/templates/registry/copy-button.json +21 -0
- package/templates/registry/dashboard.json +69 -0
- package/templates/registry/dialog.json +24 -0
- package/templates/registry/document-viewer.json +22 -0
- package/templates/registry/dropdown-menu.json +23 -0
- package/templates/registry/expandable-tabs.json +22 -0
- package/templates/registry/feature-gating.json +73 -0
- package/templates/registry/file-upload.json +24 -0
- package/templates/registry/hold-action-button.json +22 -0
- package/templates/registry/input-group.json +23 -0
- package/templates/registry/input.json +19 -0
- package/templates/registry/intelligo.json +187 -0
- package/templates/registry/invitation-accept.json +64 -0
- package/templates/registry/language-switcher.json +29 -0
- package/templates/registry/morphing-modal.json +24 -0
- package/templates/registry/notification-stack.json +24 -0
- package/templates/registry/notifications.json +87 -0
- package/templates/registry/onboarding.json +83 -0
- package/templates/registry/otp-input.json +22 -0
- package/templates/registry/page-header.json +15 -0
- package/templates/registry/payment-poll.json +58 -0
- package/templates/registry/popover-morph.json +22 -0
- package/templates/registry/popover.json +22 -0
- package/templates/registry/pricing.json +117 -0
- package/templates/registry/privacy-settings.json +65 -0
- package/templates/registry/profile-settings.json +68 -0
- package/templates/registry/progress.json +19 -0
- package/templates/registry/radio-group.json +22 -0
- package/templates/registry/registry.json +2960 -0
- package/templates/registry/route-error.json +65 -0
- package/templates/registry/select-morph.json +23 -0
- package/templates/registry/select.json +23 -0
- package/templates/registry/settings-shell.json +47 -0
- package/templates/registry/sheet.json +24 -0
- package/templates/registry/sidebar.json +28 -0
- package/templates/registry/smoke.json +34 -0
- package/templates/registry/spinner.json +16 -0
- package/templates/registry/stat-card.json +18 -0
- package/templates/registry/status-badge.json +18 -0
- package/templates/registry/switch.json +20 -0
- package/templates/registry/tabs.json +23 -0
- package/templates/registry/team-settings.json +97 -0
- package/templates/registry/textarea.json +16 -0
- package/templates/registry/tooltip.json +22 -0
- package/templates/registry/trial-banner.json +43 -0
- package/templates/registry/usage.json +84 -0
- package/templates/registry/workspace-settings.json +71 -0
- package/templates/registry-items.json +3 -3
- package/templates/registry-requires.json +30 -4
- package/templates/vitest/server-only.ts.tpl +7 -0
- package/templates/vitest/vitest.config.ts.tpl +37 -0
|
@@ -1,55 +1,127 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The operational console, mounted in your app.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
4
|
+
* The screens ship from @intelligo-dev/admin so every deployment shows
|
|
5
|
+
* the same operational truth — a fork could quietly stop showing you
|
|
6
|
+
* unsettled executions. What is generated here is the mount: the route,
|
|
7
|
+
* the authorization call and the page around the screens. The page is
|
|
8
|
+
* yours: add your product's own sections after the package's, with the
|
|
9
|
+
* data your own queries return.
|
|
8
10
|
*
|
|
9
|
-
* Access is
|
|
10
|
-
*
|
|
11
|
+
* Access is a platform-admin row (`users.role`), seeded from
|
|
12
|
+
* PLATFORM_ADMIN_EMAILS, not a workspace role: workspace `owner` is
|
|
13
|
+
* per-tenant and every signup has one. A caller who is not one gets the
|
|
14
|
+
* app's 404, because "Not authorized" — or a redirect — on a route says
|
|
15
|
+
* the route exists.
|
|
16
|
+
*
|
|
17
|
+
* Each screen reads on its own: one that fails says so, and the rest of
|
|
18
|
+
* the console still renders.
|
|
11
19
|
*/
|
|
12
20
|
|
|
21
|
+
import { notFound } from "next/navigation";
|
|
22
|
+
import type { ReactNode } from "react";
|
|
23
|
+
|
|
13
24
|
import {
|
|
25
|
+
getIntegrationHealth,
|
|
14
26
|
getPlatformOverview,
|
|
27
|
+
listFailedJobs,
|
|
15
28
|
listUnsettledExecutions,
|
|
16
29
|
listWorkspaces,
|
|
30
|
+
queryAuditEvents,
|
|
17
31
|
requireAdmin,
|
|
18
32
|
} from "@intelligo-dev/admin";
|
|
19
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
IntegrationHealthView,
|
|
35
|
+
OperationsView,
|
|
36
|
+
PlatformOverviewView,
|
|
37
|
+
} from "@intelligo-dev/admin/views";
|
|
20
38
|
|
|
21
39
|
import { composeIntelligo } from "@/lib/intelligo";
|
|
22
40
|
|
|
23
41
|
export const dynamic = "force-dynamic";
|
|
24
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The package's screens are unstyled markup; these rules give their
|
|
45
|
+
* headings, lists and tables the app's type and tokens.
|
|
46
|
+
*/
|
|
47
|
+
const SCREEN =
|
|
48
|
+
"space-y-4 rounded-xl border border-border bg-card p-6 text-sm text-card-foreground " +
|
|
49
|
+
"[&_h2]:mb-2 [&_h2]:text-base [&_h2]:font-semibold [&_section]:space-y-2 " +
|
|
50
|
+
"[&_ul]:list-disc [&_ul]:pl-5 [&_table]:w-full [&_th]:py-1 [&_th]:text-left " +
|
|
51
|
+
"[&_th]:font-medium [&_th]:text-muted-foreground [&_td]:border-t [&_td]:border-border [&_td]:py-1";
|
|
52
|
+
|
|
53
|
+
/** A screen whose read failed, in place of the screen. */
|
|
54
|
+
function Unavailable({ what }: { what: string }) {
|
|
55
|
+
return (
|
|
56
|
+
<p className="text-muted-foreground">
|
|
57
|
+
{what} could not be read — see the server log.
|
|
58
|
+
</p>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function screen<T>(
|
|
63
|
+
result: PromiseSettledResult<T>,
|
|
64
|
+
render: (value: T) => ReactNode,
|
|
65
|
+
what: string
|
|
66
|
+
) {
|
|
67
|
+
return (
|
|
68
|
+
<div className={SCREEN}>
|
|
69
|
+
{result.status === "fulfilled" ? (
|
|
70
|
+
render(result.value)
|
|
71
|
+
) : (
|
|
72
|
+
<Unavailable what={what} />
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
25
78
|
export default async function AdminPage() {
|
|
26
79
|
composeIntelligo();
|
|
27
80
|
|
|
28
81
|
try {
|
|
29
82
|
await requireAdmin("admin.overview.viewed");
|
|
30
83
|
} catch {
|
|
31
|
-
|
|
32
|
-
<main style={{ fontFamily: "system-ui", padding: "2rem" }}>
|
|
33
|
-
<h1>Admin</h1>
|
|
34
|
-
<p>Not authorized.</p>
|
|
35
|
-
</main>
|
|
36
|
-
);
|
|
84
|
+
notFound();
|
|
37
85
|
}
|
|
38
86
|
|
|
39
|
-
const [overview,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
87
|
+
const [overview, integrations, operations] = await Promise.allSettled([
|
|
88
|
+
Promise.all([
|
|
89
|
+
getPlatformOverview(),
|
|
90
|
+
listWorkspaces(20),
|
|
91
|
+
listUnsettledExecutions(),
|
|
92
|
+
]),
|
|
93
|
+
getIntegrationHealth(),
|
|
94
|
+
Promise.all([listFailedJobs(20), queryAuditEvents({ limit: 20 })]),
|
|
43
95
|
]);
|
|
44
96
|
|
|
45
97
|
return (
|
|
46
|
-
<main
|
|
47
|
-
<h1>Admin</h1>
|
|
48
|
-
|
|
49
|
-
overview
|
|
50
|
-
workspaces
|
|
51
|
-
|
|
52
|
-
|
|
98
|
+
<main className="mx-auto max-w-5xl space-y-6 bg-background px-4 py-8 text-foreground">
|
|
99
|
+
<h1 className="text-2xl font-semibold">Admin</h1>
|
|
100
|
+
{screen(
|
|
101
|
+
overview,
|
|
102
|
+
([platform, workspaces, unsettled]) => (
|
|
103
|
+
<PlatformOverviewView
|
|
104
|
+
overview={platform}
|
|
105
|
+
workspaces={workspaces}
|
|
106
|
+
unsettled={unsettled}
|
|
107
|
+
/>
|
|
108
|
+
),
|
|
109
|
+
"The platform overview"
|
|
110
|
+
)}
|
|
111
|
+
{screen(
|
|
112
|
+
integrations,
|
|
113
|
+
(value) => (
|
|
114
|
+
<IntegrationHealthView integrations={value} />
|
|
115
|
+
),
|
|
116
|
+
"Integration health"
|
|
117
|
+
)}
|
|
118
|
+
{screen(
|
|
119
|
+
operations,
|
|
120
|
+
([failedJobs, auditEvents]) => (
|
|
121
|
+
<OperationsView failedJobs={failedJobs} auditEvents={auditEvents} />
|
|
122
|
+
),
|
|
123
|
+
"Operations"
|
|
124
|
+
)}
|
|
53
125
|
</main>
|
|
54
126
|
);
|
|
55
127
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# next.js
|
|
5
|
+
.next/
|
|
6
|
+
out/
|
|
7
|
+
next-env.d.ts
|
|
8
|
+
|
|
9
|
+
# production
|
|
10
|
+
build/
|
|
11
|
+
|
|
12
|
+
# env files: .env.example is the only one committed
|
|
13
|
+
.env
|
|
14
|
+
.env*.local
|
|
15
|
+
|
|
16
|
+
# debug
|
|
17
|
+
npm-debug.log*
|
|
18
|
+
pnpm-debug.log*
|
|
19
|
+
|
|
20
|
+
# misc
|
|
21
|
+
.DS_Store
|
|
22
|
+
*.pem
|
|
23
|
+
*.tsbuildinfo
|
|
24
|
+
.vercel
|
|
25
|
+
coverage/
|
|
@@ -14,12 +14,9 @@ import "server-only";
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
+
createBillingExecutions,
|
|
17
18
|
ensureBillingSettingsRow,
|
|
18
19
|
ensurePlanRows,
|
|
19
|
-
reserveQuota,
|
|
20
|
-
recordTokenUsage,
|
|
21
|
-
releaseReservation,
|
|
22
|
-
findSettlementByRequestId,
|
|
23
20
|
} from "@intelligo-dev/billing";
|
|
24
21
|
import { DEFAULT_MARGIN_BP } from "@intelligo-dev/executions/pricing";
|
|
25
22
|
import {
|
|
@@ -33,7 +30,6 @@ import { assertEnv } from "@intelligo-dev/core/env";
|
|
|
33
30
|
import { setRequestContextSource } from "@intelligo-dev/core/request-context";
|
|
34
31
|
import {
|
|
35
32
|
DEFAULT_MODELS,
|
|
36
|
-
createExecutions,
|
|
37
33
|
registerModels,
|
|
38
34
|
} from "@intelligo-dev/executions";
|
|
39
35
|
import { nextRequestContext } from "@intelligo-dev/next";
|
|
@@ -135,45 +131,8 @@ function bind(): void {
|
|
|
135
131
|
registerModels(DEFAULT_MODELS);
|
|
136
132
|
}
|
|
137
133
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const quota = await reserveQuota(workspaceId, { modelId: model, requestId });
|
|
144
|
-
return {
|
|
145
|
-
allowed: quota.allowed,
|
|
146
|
-
code: quota.code,
|
|
147
|
-
reason: quota.reason,
|
|
148
|
-
// The hold, as an amount with its currency.
|
|
149
|
-
estimated: quota.estimated,
|
|
150
|
-
usingTrialCredits: quota.usingTrialCredits,
|
|
151
|
-
};
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
async settleUsage(s) {
|
|
155
|
-
// Returns what was charged and which pool funded it; the lifecycle
|
|
156
|
-
// records the amount and its currency on the execution row.
|
|
157
|
-
return recordTokenUsage({
|
|
158
|
-
workspaceId: s.workspaceId,
|
|
159
|
-
userId: s.userId ?? "",
|
|
160
|
-
model: s.model ?? "unknown",
|
|
161
|
-
agent: s.capability,
|
|
162
|
-
inputTokens: s.inputTokens,
|
|
163
|
-
outputTokens: s.outputTokens,
|
|
164
|
-
totalTokens: s.totalTokens,
|
|
165
|
-
usingTrialCredits: s.usingTrialCredits,
|
|
166
|
-
requestId: s.requestId,
|
|
167
|
-
metadata: s.metadata,
|
|
168
|
-
});
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
async releaseHold({ requestId }) {
|
|
172
|
-
await releaseReservation(requestId);
|
|
173
|
-
},
|
|
174
|
-
|
|
175
|
-
// Lets executions.reconcile() tell a settling row whose charge
|
|
176
|
-
// committed from one whose charge never happened.
|
|
177
|
-
findSettlement: ({ workspaceId, requestId }) =>
|
|
178
|
-
findSettlementByRequestId(workspaceId, requestId),
|
|
179
|
-
});
|
|
134
|
+
// The execution boundary, bound to the billing engine: admission
|
|
135
|
+
// reserves the worst-case cost atomically, settlement records usage and
|
|
136
|
+
// deducts credits, reconcile() can tell a committed charge from none.
|
|
137
|
+
// Pass a port to replace one.
|
|
138
|
+
export const executions = createBillingExecutions();
|
|
@@ -1,11 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parseEnv } from "node:util";
|
|
5
|
+
|
|
1
6
|
import createNextIntlPlugin from "next-intl/plugin";
|
|
2
7
|
|
|
8
|
+
// Next reads env files from this directory only. When `intelligo create`
|
|
9
|
+
// made this app as a member of a pnpm workspace, the workspace root's
|
|
10
|
+
// .env.local and .env fill in what neither they nor the shell set — the
|
|
11
|
+
// files `intelligo doctor` and `intelligo migrate` read there too. Null
|
|
12
|
+
// for an app outside any workspace.
|
|
13
|
+
const WORKSPACE_ROOT = __WORKSPACE_ROOT__;
|
|
14
|
+
|
|
15
|
+
if (WORKSPACE_ROOT) {
|
|
16
|
+
const root = join(fileURLToPath(new URL(".", import.meta.url)), WORKSPACE_ROOT);
|
|
17
|
+
for (const file of [".env.local", ".env"]) {
|
|
18
|
+
let content;
|
|
19
|
+
try {
|
|
20
|
+
content = readFileSync(join(root, file), "utf8");
|
|
21
|
+
} catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
for (const [key, value] of Object.entries(parseEnv(content))) {
|
|
25
|
+
if (process.env[key] === undefined) process.env[key] = value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
3
30
|
const withNextIntl = createNextIntlPlugin("./i18n/request.ts");
|
|
4
31
|
|
|
5
32
|
/** @type {import('next').NextConfig} */
|
|
6
33
|
const nextConfig = {
|
|
7
|
-
//
|
|
8
|
-
//
|
|
34
|
+
// Inside the framework's own workspace these packages resolve to
|
|
35
|
+
// TypeScript source; from npm they are compiled JavaScript, which this
|
|
36
|
+
// leaves as it is.
|
|
9
37
|
transpilePackages: [
|
|
10
38
|
"@intelligo-dev/admin",
|
|
11
39
|
"@intelligo-dev/audit",
|
|
@@ -14,6 +42,7 @@ const nextConfig = {
|
|
|
14
42
|
"@intelligo-dev/chat",
|
|
15
43
|
"@intelligo-dev/core",
|
|
16
44
|
"@intelligo-dev/executions",
|
|
45
|
+
"@intelligo-dev/jobs",
|
|
17
46
|
"@intelligo-dev/next",
|
|
18
47
|
],
|
|
19
48
|
};
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "next dev --turbopack
|
|
7
|
+
"dev": "next dev --turbopack",
|
|
8
8
|
"build": "next build",
|
|
9
|
-
"start": "next start
|
|
9
|
+
"start": "next start",
|
|
10
10
|
"type-check": "tsc --noEmit",
|
|
11
11
|
"db:generate": "drizzle-kit generate",
|
|
12
12
|
"db:migrate": "intelligo migrate && drizzle-kit migrate",
|
package/templates/manifest.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"deprecated": "Superseded by the `usage` registry item \u2014 install it with the shadcn CLI from the Intelligo registry instead; it ships a full page, components and thin actions as consumer-owned source."
|
|
12
12
|
},
|
|
13
13
|
"app-scaffold": {
|
|
14
|
-
"templateVersion": "1.
|
|
14
|
+
"templateVersion": "1.15.0",
|
|
15
15
|
"description": "A new application: composition root (run once per server process from instrumentation.ts), plans, an assistant route, a Stripe webhook endpoint, config, a drizzle config for the tables you own, next-intl i18n plumbing, and a registry-consuming shadcn base-nova setup with the intelligo design tokens",
|
|
16
16
|
"files": [
|
|
17
17
|
{
|
|
@@ -125,12 +125,30 @@
|
|
|
125
125
|
{
|
|
126
126
|
"template": "app-scaffold/stripe-webhook-route.ts.tpl",
|
|
127
127
|
"target": "app/api/webhooks/stripe/route.ts"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"template": "app-scaffold/gitignore.tpl",
|
|
131
|
+
"target": ".gitignore"
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"pnpm-standalone": {
|
|
136
|
+
"templateVersion": "1.0.0",
|
|
137
|
+
"description": "pnpm settings for an app that is its own workspace root: dependency build scripts declined, so pnpm 10+ installs without a prompt, and `pnpm add` allowed at the root for the shadcn CLI. `intelligo create` writes it when pnpm installs an app outside any workspace",
|
|
138
|
+
"files": [
|
|
139
|
+
{
|
|
140
|
+
"template": "pnpm-standalone/pnpm-workspace.yaml.tpl",
|
|
141
|
+
"target": "pnpm-workspace.yaml"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"template": "pnpm-standalone/npmrc.tpl",
|
|
145
|
+
"target": ".npmrc"
|
|
128
146
|
}
|
|
129
147
|
]
|
|
130
148
|
},
|
|
131
149
|
"admin-page": {
|
|
132
|
-
"templateVersion": "1.
|
|
133
|
-
"description": "Mount the Intelligo operational console at /admin",
|
|
150
|
+
"templateVersion": "1.2.0",
|
|
151
|
+
"description": "Mount the Intelligo operational console at /admin, styled with the app's tokens, for platform admins only",
|
|
134
152
|
"files": [
|
|
135
153
|
{
|
|
136
154
|
"template": "admin-page/admin-page.tsx.tpl",
|
|
@@ -151,5 +169,24 @@
|
|
|
151
169
|
"target": "app/api/cron/maintenance/route.ts"
|
|
152
170
|
}
|
|
153
171
|
]
|
|
172
|
+
},
|
|
173
|
+
"vitest": {
|
|
174
|
+
"templateVersion": "1.0.0",
|
|
175
|
+
"description": "A Vitest setup for the app's own tests: the `@` alias, a `server-only` stub, and the `@intelligo-dev/*` packages inlined so the stub reaches them",
|
|
176
|
+
"files": [
|
|
177
|
+
{
|
|
178
|
+
"template": "vitest/vitest.config.ts.tpl",
|
|
179
|
+
"target": "vitest.config.ts"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"template": "vitest/server-only.ts.tpl",
|
|
183
|
+
"target": "tests/stubs/server-only.ts"
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
"nextSteps": [
|
|
187
|
+
"Install the runner: pnpm add -D vitest",
|
|
188
|
+
"Add a script to package.json: \"test\": \"vitest run\"",
|
|
189
|
+
"Write tests beside the code as *.test.ts; mock @ai-sdk/* providers so CI makes no model calls"
|
|
190
|
+
]
|
|
154
191
|
}
|
|
155
192
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# pnpm 9 reads this setting here; later versions read it from
|
|
2
|
+
# pnpm-workspace.yaml. pnpm-workspace.yaml makes this directory a
|
|
3
|
+
# workspace root, and pnpm refuses a plain `pnpm add` in one unless told
|
|
4
|
+
# it is intended — the shadcn CLI runs exactly that when it installs a
|
|
5
|
+
# registry page.
|
|
6
|
+
ignore-workspace-root-check=true
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# This app is its own pnpm workspace root. pnpm 11 stops the install over
|
|
2
|
+
# a dependency build script nobody approved (10 warns); these ship
|
|
3
|
+
# prebuilt binaries, so their scripts are declined.
|
|
4
|
+
packages: []
|
|
5
|
+
allowBuilds:
|
|
6
|
+
'@parcel/watcher': false
|
|
7
|
+
'@swc/core': false
|
|
8
|
+
esbuild: false
|
|
9
|
+
# The shadcn CLI runs a plain `pnpm add` here when it installs a page.
|
|
10
|
+
ignoreWorkspaceRootCheck: true
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "ai-agent-activity",
|
|
4
|
+
"title": "AI Agent Activity",
|
|
5
|
+
"description": "One adaptive stream of what the agent is doing: reasoning text, plan steps, web searches with results, tool calls and structured traces, gliding while it works and folding into a summary when done.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"motion",
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"@intelligo/ai-motion",
|
|
12
|
+
"@intelligo/ai-shimmer-text"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "base/ui/ai-agent-activity/ai-agent-activity.tsx",
|
|
17
|
+
"content": "\"use client\";\n\n/*\n * One adaptive stream of what the agent is doing: reasoning text, plan\n * steps, web searches with their results, tool calls and structured\n * traces. While the run is working the newest rows glide up under a fade;\n * once complete the stream folds into a one-line summary that opens on\n * demand.\n */\n\nimport * as React from \"react\";\nimport {\n CheckIcon,\n ChevronDownIcon,\n CircleAlertIcon,\n CircleIcon,\n FileTextIcon,\n Globe2Icon,\n ImageIcon,\n MessageSquareIcon,\n PencilLineIcon,\n SearchIcon,\n SparklesIcon,\n SquareTerminalIcon,\n WrenchIcon,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nimport {\n EASE_OUT,\n Disclosure,\n SPRING_LAYOUT,\n SPRING_SWAP,\n} from \"@/components/ui/ai-motion\";\nimport { ShimmerText } from \"@/components/ui/ai-shimmer-text\";\nimport { cn } from \"@/lib/utils\";\n\n/* ----------------------------------------------------------------------------\n * Items\n * ------------------------------------------------------------------------- */\n\nexport type AgentActivityStatus = \"working\" | \"complete\";\nexport type AgentStepStatus = \"pending\" | \"active\" | \"complete\";\n\nexport interface AgentActivityStep {\n id: string;\n type: \"step\";\n label: React.ReactNode;\n status?: AgentStepStatus;\n meta?: React.ReactNode;\n}\n\nexport interface AgentActivityText {\n id: string;\n type: \"text\";\n content: React.ReactNode;\n}\n\nexport interface AgentSearchResult {\n id: string;\n title: React.ReactNode;\n domain?: React.ReactNode;\n url?: string;\n icon?: React.ReactNode;\n}\n\n/** A row's own state inside the stream; rows are complete by default. */\nexport type AgentRowStatus = \"running\" | \"complete\" | \"error\";\n\nexport interface AgentActivitySearch {\n id: string;\n type: \"search\";\n query: React.ReactNode;\n results?: AgentSearchResult[];\n moreCount?: number;\n status?: AgentRowStatus;\n /** Shown under the row when the reader opens it — the raw call, say. */\n details?: React.ReactNode;\n}\n\nexport interface AgentActivityTool {\n id: string;\n type: \"tool\";\n action: \"read\" | \"edit\" | \"run\" | (string & {});\n target?: React.ReactNode;\n additions?: number;\n deletions?: number;\n status?: AgentRowStatus;\n /** Shown under the row when the reader opens it — the raw call, say. */\n details?: React.ReactNode;\n}\n\nexport type AgentTraceKind =\n | \"thinking\"\n | \"message\"\n | \"write\"\n | \"run\"\n | \"read\"\n | (string & {});\n\nexport interface AgentActivityTrace {\n id: string;\n type: \"trace\";\n kind: AgentTraceKind;\n label: React.ReactNode;\n detail?: React.ReactNode;\n icon?: React.ReactNode;\n}\n\nexport type AgentActivityItem =\n | AgentActivityStep\n | AgentActivityText\n | AgentActivitySearch\n | AgentActivityTool\n | AgentActivityTrace;\n\nexport type AgentActivityContentType = AgentActivityItem[\"type\"] | \"mixed\";\n\n/* ----------------------------------------------------------------------------\n * Labels — every visible string, with an English default.\n * ------------------------------------------------------------------------- */\n\nexport interface AgentActivitySummaryLabels {\n /** Step- or text-only runs: `duration` is already formatted. */\n thought: (duration: string) => React.ReactNode;\n searched: string;\n tools: (count: number) => string;\n trace: (toolCalls: number, messages: number) => string;\n steps: (count: number) => string;\n}\n\nconst DEFAULT_ACTIVE_LABELS: Record<AgentActivityContentType, string> = {\n step: \"Thinking…\",\n text: \"Thinking…\",\n search: \"Searching the web…\",\n tool: \"Running tools…\",\n trace: \"Working through the run…\",\n mixed: \"Working through it…\",\n};\n\nconst DEFAULT_SUMMARY_LABELS: AgentActivitySummaryLabels = {\n thought: (duration) => (\n <>\n Thought for <span className=\"tabular-nums\">{duration}</span>\n </>\n ),\n searched: \"Searched the web\",\n tools: (count) => `Ran ${count} ${count === 1 ? \"tool\" : \"tools\"}`,\n trace: (toolCalls, messages) =>\n `${toolCalls} ${toolCalls === 1 ? \"tool call\" : \"tool calls\"}, ${messages} ${messages === 1 ? \"message\" : \"messages\"}`,\n steps: (count) => `Completed ${count} ${count === 1 ? \"step\" : \"steps\"}`,\n};\n\nfunction defaultFormatDuration(duration: number) {\n const seconds = Math.max(0, Math.round(duration));\n if (seconds < 60) return `${seconds}s`;\n\n const minutes = Math.floor(seconds / 60);\n const remainder = seconds % 60;\n return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s`;\n}\n\nfunction defaultMoreLabel(count: number) {\n return `+${count} more`;\n}\n\nfunction defaultActionLabel(action: string) {\n return action.charAt(0).toUpperCase() + action.slice(1);\n}\n\n/* ----------------------------------------------------------------------------\n * Rows\n * ------------------------------------------------------------------------- */\n\nfunction StepRow({ item }: { item: AgentActivityStep }) {\n const reduce = useReducedMotion() ?? false;\n const state = item.status ?? \"complete\";\n\n return (\n <div\n data-slot=\"agent-activity-step\"\n data-status={state}\n className=\"flex min-h-7 items-start gap-2.5 rounded-md px-1.5 py-1\"\n >\n <span\n aria-hidden=\"true\"\n className=\"mt-0.5 grid size-4 shrink-0 place-items-center text-muted-foreground/70\"\n >\n {state === \"complete\" ? (\n <CheckIcon className=\"size-4\" strokeWidth={1.8} />\n ) : state === \"active\" ? (\n <span className=\"relative grid size-3 place-items-center\">\n <motion.span\n className=\"absolute inset-0 rounded-full bg-foreground/10\"\n animate={\n reduce ? { opacity: 0.6 } : { opacity: [0.35, 0.8, 0.35] }\n }\n transition={\n reduce\n ? { duration: 0 }\n : { duration: 1.5, repeat: Number.POSITIVE_INFINITY }\n }\n />\n <span className=\"size-1.5 rounded-full bg-foreground/60\" />\n </span>\n ) : (\n <CircleIcon className=\"size-3\" strokeWidth={1.5} />\n )}\n </span>\n <span\n className={cn(\n \"min-w-0 flex-1 leading-5\",\n state === \"pending\"\n ? \"text-muted-foreground/55\"\n : \"text-foreground/90\"\n )}\n >\n {item.label}\n </span>\n {item.meta ? (\n <span className=\"shrink-0 leading-5 text-muted-foreground/55\">\n {item.meta}\n </span>\n ) : null}\n </div>\n );\n}\n\nfunction TextRow({ item }: { item: AgentActivityText }) {\n return (\n <div\n data-slot=\"agent-activity-text\"\n className=\"rounded-md px-1.5 py-1 leading-5 text-muted-foreground\"\n >\n {item.content}\n </div>\n );\n}\n\nfunction SearchResultRow({ result }: { result: AgentSearchResult }) {\n const content = (\n <>\n <span\n aria-hidden=\"true\"\n className=\"grid size-5 shrink-0 place-items-center text-muted-foreground\"\n >\n {result.icon ?? <Globe2Icon className=\"size-3\" strokeWidth={2} />}\n </span>\n <span className=\"min-w-0 truncate font-medium text-foreground/90\">\n {result.title}\n </span>\n {result.domain ? (\n <span className=\"min-w-0 truncate text-muted-foreground/55\">\n {result.domain}\n </span>\n ) : null}\n </>\n );\n const className = cn(\n \"flex min-h-7 items-center gap-2 rounded-md px-1.5 py-1 text-left outline-none transition-colors\",\n result.url && \"hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring\"\n );\n\n return result.url ? (\n <a\n data-slot=\"agent-activity-search-result\"\n href={result.url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className={className}\n >\n {content}\n </a>\n ) : (\n <div data-slot=\"agent-activity-search-result\" className={className}>\n {content}\n </div>\n );\n}\n\n/** A row's glyph, pulsing while its call runs; an alert once it failed. */\nfunction RowStatusIcon({\n status,\n children,\n}: {\n status: AgentRowStatus;\n children: React.ReactNode;\n}) {\n const reduce = useReducedMotion() ?? false;\n if (status === \"error\") {\n return <CircleAlertIcon className=\"size-4 text-destructive\" strokeWidth={1.8} />;\n }\n if (status === \"running\") {\n return (\n <motion.span\n className=\"grid size-4 place-items-center\"\n animate={reduce ? { opacity: 0.7 } : { opacity: [0.4, 1, 0.4] }}\n transition={\n reduce\n ? { duration: 0 }\n : { duration: 1.4, repeat: Number.POSITIVE_INFINITY }\n }\n >\n {children}\n </motion.span>\n );\n }\n return <>{children}</>;\n}\n\n/** A row that opens onto its details, when it has any. */\nfunction RowWithDetails({\n details,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\" | \"className\"> & {\n details?: React.ReactNode;\n className: string;\n children: React.ReactNode;\n}) {\n const reduce = useReducedMotion() ?? false;\n const [open, setOpen] = React.useState(false);\n const detailsId = React.useId();\n\n if (!details) {\n return (\n <div className={className} {...props}>\n {children}\n </div>\n );\n }\n\n return (\n <div {...props}>\n <button\n type=\"button\"\n aria-expanded={open}\n aria-controls={detailsId}\n onClick={() => setOpen(!open)}\n className={cn(\n className,\n \"group/row w-full text-left outline-none transition-colors hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring\"\n )}\n >\n {children}\n <motion.span\n aria-hidden=\"true\"\n animate={{ rotate: open ? 180 : 0 }}\n transition={reduce ? { duration: 0 } : SPRING_SWAP}\n className={cn(\n \"inline-flex shrink-0 text-muted-foreground/60 transition-opacity group-hover/row:opacity-100 group-focus-visible/row:opacity-100\",\n open ? \"opacity-100\" : \"opacity-0\"\n )}\n >\n <ChevronDownIcon className=\"size-3.5\" />\n </motion.span>\n </button>\n <Disclosure id={detailsId} open={open}>\n <div data-slot=\"agent-activity-details\" className=\"pt-1 pr-1.5 pb-2 pl-8\">\n {details}\n </div>\n </Disclosure>\n </div>\n );\n}\n\nfunction SearchRow({\n item,\n moreLabel,\n}: {\n item: AgentActivitySearch;\n moreLabel: (count: number) => string;\n}) {\n const reduce = useReducedMotion() ?? false;\n const enter = reduce ? { opacity: 1 } : { opacity: 0, y: 6 };\n const visible = { opacity: 1, y: 0 };\n const exit = reduce ? { opacity: 0 } : { opacity: 0, y: -3 };\n const transition = reduce\n ? { duration: 0 }\n : {\n opacity: { duration: 0.18, ease: EASE_OUT },\n y: SPRING_LAYOUT,\n layout: SPRING_LAYOUT,\n };\n\n return (\n <div\n data-slot=\"agent-activity-search\"\n data-status={item.status ?? \"complete\"}\n className=\"space-y-0.5\"\n >\n <RowWithDetails\n details={item.details}\n className=\"flex min-h-7 items-center gap-2.5 rounded-md px-1.5 py-1 text-muted-foreground\"\n >\n <span aria-hidden=\"true\" className=\"grid size-4 shrink-0 place-items-center\">\n <RowStatusIcon status={item.status ?? \"complete\"}>\n <SearchIcon className=\"size-4\" strokeWidth={1.7} />\n </RowStatusIcon>\n </span>\n <span className=\"min-w-0 flex-1 truncate\">{item.query}</span>\n </RowWithDetails>\n {item.results?.length ? (\n <div className=\"space-y-0.5 pl-4\">\n <AnimatePresence initial mode=\"popLayout\">\n {item.results.map((result) => (\n <motion.div\n layout=\"position\"\n key={result.id}\n initial={enter}\n animate={visible}\n exit={exit}\n transition={transition}\n >\n <SearchResultRow result={result} />\n </motion.div>\n ))}\n </AnimatePresence>\n </div>\n ) : null}\n <AnimatePresence initial>\n {item.moreCount ? (\n <motion.div\n key=\"more-results\"\n initial={enter}\n animate={visible}\n exit={exit}\n transition={transition}\n className=\"px-1.5 py-1 pl-8 text-muted-foreground/55\"\n >\n {moreLabel(item.moreCount)}\n </motion.div>\n ) : null}\n </AnimatePresence>\n </div>\n );\n}\n\nfunction ActionIcon({ action }: { action: string }) {\n if (action === \"read\") return <FileTextIcon className=\"size-4\" />;\n if (action === \"edit\" || action === \"write\") {\n return <PencilLineIcon className=\"size-4\" />;\n }\n if (action === \"run\") return <SquareTerminalIcon className=\"size-4\" />;\n return <WrenchIcon className=\"size-4\" />;\n}\n\nfunction ToolRow({\n item,\n actionLabels,\n}: {\n item: AgentActivityTool;\n actionLabels?: Record<string, string>;\n}) {\n const action = actionLabels?.[item.action] ?? defaultActionLabel(item.action);\n const status = item.status ?? \"complete\";\n\n return (\n <RowWithDetails\n data-slot=\"agent-activity-tool\"\n data-action={item.action}\n data-status={status}\n details={item.details}\n className=\"flex min-h-8 min-w-0 items-center gap-2.5 rounded-md px-1.5 py-0.5 leading-5\"\n >\n <span\n aria-hidden=\"true\"\n className=\"grid size-4 shrink-0 place-items-center text-muted-foreground/70\"\n >\n <RowStatusIcon status={status}>\n <ActionIcon action={item.action} />\n </RowStatusIcon>\n </span>\n <span className=\"shrink-0 font-medium text-foreground/90\">{action}</span>\n {item.target ? (\n <span\n className={cn(\n \"min-w-0 flex-1 truncate rounded-lg bg-muted/80 px-2.5 py-1 font-mono text-xs text-muted-foreground\",\n status === \"error\" && \"bg-transparent px-0 font-sans text-destructive\"\n )}\n >\n {item.target}\n </span>\n ) : (\n <span className=\"flex-1\" />\n )}\n {typeof item.additions === \"number\" ||\n typeof item.deletions === \"number\" ? (\n <span className=\"flex shrink-0 items-center gap-2 font-mono tabular-nums\">\n {typeof item.additions === \"number\" ? (\n <span className=\"text-success\">+{item.additions}</span>\n ) : null}\n {typeof item.deletions === \"number\" ? (\n <span className=\"text-destructive\">−{item.deletions}</span>\n ) : null}\n </span>\n ) : null}\n </RowWithDetails>\n );\n}\n\nfunction TraceIcon({ kind }: { kind: AgentTraceKind }) {\n if (kind === \"thinking\") return <SparklesIcon className=\"size-4\" />;\n if (kind === \"message\") return <MessageSquareIcon className=\"size-4\" />;\n if (kind === \"write\") return <PencilLineIcon className=\"size-4\" />;\n if (kind === \"run\") return <SquareTerminalIcon className=\"size-4\" />;\n if (kind === \"read\") return <ImageIcon className=\"size-4\" />;\n return <WrenchIcon className=\"size-4\" />;\n}\n\nfunction TraceRow({ item }: { item: AgentActivityTrace }) {\n return (\n <div\n data-slot=\"agent-activity-trace\"\n data-kind={item.kind}\n className=\"grid min-h-8 items-center gap-2.5 rounded-md px-1.5 py-0.5\"\n style={{ gridTemplateColumns: \"1rem auto minmax(0, 1fr)\" }}\n >\n <span\n aria-hidden=\"true\"\n className=\"grid size-4 place-items-center text-muted-foreground/70\"\n >\n {item.icon ?? <TraceIcon kind={item.kind} />}\n </span>\n <span className=\"font-medium text-foreground/90\">{item.label}</span>\n {item.detail ? (\n <span className=\"min-w-0 truncate rounded-lg bg-muted/80 px-2.5 py-1 font-mono text-xs text-muted-foreground/70\">\n {item.detail}\n </span>\n ) : (\n <span />\n )}\n </div>\n );\n}\n\nexport interface AgentActivityRowProps {\n item: AgentActivityItem;\n /** \"+3 more\" under a search's results. */\n moreLabel?: (count: number) => string;\n /** The word shown for a tool action (`read` → \"Read\"); capitalised by default. */\n actionLabels?: Record<string, string>;\n}\n\nfunction AgentActivityRow({\n item,\n moreLabel = defaultMoreLabel,\n actionLabels,\n}: AgentActivityRowProps) {\n if (item.type === \"text\") return <TextRow item={item} />;\n if (item.type === \"search\")\n return <SearchRow item={item} moreLabel={moreLabel} />;\n if (item.type === \"tool\")\n return <ToolRow item={item} actionLabels={actionLabels} />;\n if (item.type === \"trace\") return <TraceRow item={item} />;\n return <StepRow item={item} />;\n}\n\n/* ----------------------------------------------------------------------------\n * The stream\n * ------------------------------------------------------------------------- */\n\nexport interface AgentActivityProps extends Omit<\n React.ComponentProps<\"div\">,\n \"children\"\n> {\n /** Chronological activity entries. Append or update items as events stream. */\n items: AgentActivityItem[];\n /** Expected activity kind before the first streamed item arrives. */\n contentType?: AgentActivityContentType;\n /** Current run phase. Active runs always stay expanded. */\n status?: AgentActivityStatus;\n /** Elapsed run time, in seconds. Used by the step-only summary. */\n duration?: number;\n /** Controlled expanded state used after the run completes. */\n open?: boolean;\n /** Initial expanded state used after the run completes. */\n defaultOpen?: boolean;\n /** Called when the completed activity disclosure changes state. */\n onOpenChange?: (open: boolean) => void;\n /** Collapse the disclosure when status changes from working to complete. */\n collapseOnComplete?: boolean;\n /** The status line while the run is active; overrides `activeLabels`. */\n activeLabel?: string;\n /** The status line per content kind while the run is active. */\n activeLabels?: Partial<Record<AgentActivityContentType, string>>;\n /** The completed summary; overrides `summaryLabels`. */\n summary?: React.ReactNode;\n /** The completed summary per content kind. */\n summaryLabels?: Partial<AgentActivitySummaryLabels>;\n /** \"45s\", \"2m 5s\" — for the step-only summary. */\n formatDuration?: (seconds: number) => string;\n /** \"+3 more\" under a search's results. */\n moreLabel?: (count: number) => string;\n /** The word shown for a tool action (`read` → \"Read\"); capitalised by default. */\n actionLabels?: Record<string, string>;\n /** Optional renderer for the contents of the active status row. */\n renderWorkingStatus?: (context: {\n label: string;\n duration: number;\n }) => React.ReactNode;\n /** Optional renderer for the contents before the built-in disclosure chevron. */\n renderCompletedStatus?: (context: {\n summary: React.ReactNode;\n duration: number;\n }) => React.ReactNode;\n /** Maximum visible activity height before the stream begins gliding. */\n maxHeight?: number;\n contentClassName?: string;\n}\n\nfunction useControllableOpen({\n open,\n defaultOpen,\n onOpenChange,\n}: {\n open?: boolean;\n defaultOpen: boolean;\n onOpenChange?: (open: boolean) => void;\n}) {\n const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\n const controlled = open !== undefined;\n const currentOpen = open ?? internalOpen;\n\n const setOpen = React.useCallback(\n (next: boolean) => {\n if (!controlled) setInternalOpen(next);\n onOpenChange?.(next);\n },\n [controlled, onOpenChange]\n );\n\n return [currentOpen, setOpen] as const;\n}\n\nfunction getContentType(items: AgentActivityItem[]): AgentActivityContentType {\n const first = items[0]?.type;\n return first && items.every((item) => item.type === first) ? first : \"mixed\";\n}\n\nfunction getSummary(\n type: AgentActivityContentType,\n items: AgentActivityItem[],\n duration: string,\n labels: AgentActivitySummaryLabels\n): React.ReactNode {\n if (type === \"step\" || type === \"text\") return labels.thought(duration);\n if (type === \"search\") return labels.searched;\n if (type === \"tool\") return labels.tools(items.length);\n if (type === \"trace\") {\n const messages = items.filter(\n (item) =>\n item.type === \"trace\" &&\n (item.kind === \"thinking\" || item.kind === \"message\")\n ).length;\n return labels.trace(items.length - messages, messages);\n }\n return labels.steps(items.length);\n}\n\nfunction AgentActivity({\n items,\n contentType: initialContentType,\n status = \"working\",\n duration = 0,\n open,\n defaultOpen = false,\n onOpenChange,\n collapseOnComplete = true,\n activeLabel,\n activeLabels,\n summary,\n summaryLabels,\n formatDuration = defaultFormatDuration,\n moreLabel = defaultMoreLabel,\n actionLabels,\n renderWorkingStatus,\n renderCompletedStatus,\n maxHeight = 208,\n className,\n contentClassName,\n ...props\n}: AgentActivityProps) {\n const reduce = useReducedMotion() ?? false;\n const baseId = React.useId();\n const triggerId = `${baseId}-trigger`;\n const contentId = `${baseId}-content`;\n const contentRef = React.useRef<HTMLDivElement>(null);\n const viewportRef = React.useRef<HTMLDivElement>(null);\n const previousStatus = React.useRef(status);\n const [contentHeight, setContentHeight] = React.useState(0);\n const [currentOpen, setOpen] = useControllableOpen({\n open,\n defaultOpen,\n onOpenChange,\n });\n const working = status === \"working\";\n const expanded = working || currentOpen;\n const contentType = items.length\n ? getContentType(items)\n : (initialContentType ?? \"mixed\");\n // While working the viewport is a fixed window the content glides up\n // through; once complete it shrinks to fit (capped) and scrolls.\n const cappedHeight = Math.min(contentHeight, Math.max(0, maxHeight));\n const viewportHeight = working ? Math.max(0, maxHeight) : cappedHeight;\n const capped = contentHeight > maxHeight;\n const streamOffset = working\n ? Math.min(0, viewportHeight - contentHeight)\n : 0;\n\n React.useLayoutEffect(() => {\n const node = contentRef.current;\n if (!node) return;\n\n const measure = () => setContentHeight(node.offsetHeight);\n measure();\n\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n React.useEffect(() => {\n if (previousStatus.current === \"working\" && status === \"complete\") {\n setOpen(!collapseOnComplete);\n }\n previousStatus.current = status;\n }, [collapseOnComplete, setOpen, status]);\n\n const toggle = () => {\n const next = !currentOpen;\n setOpen(next);\n if (next) {\n requestAnimationFrame(() => viewportRef.current?.scrollTo({ top: 0 }));\n }\n };\n\n const liveLabel =\n activeLabel ??\n activeLabels?.[contentType] ??\n DEFAULT_ACTIVE_LABELS[contentType];\n const completedSummary =\n summary ??\n getSummary(contentType, items, formatDuration(duration), {\n ...DEFAULT_SUMMARY_LABELS,\n ...summaryLabels,\n });\n // Alpha-only masks: the colour words never paint.\n const maskImage = capped\n ? working\n ? \"linear-gradient(to bottom, transparent, black 12px)\"\n : \"linear-gradient(to bottom, transparent, black 12px, black calc(100% - 12px), transparent)\"\n : undefined;\n\n return (\n <div\n data-slot=\"agent-activity\"\n data-state={working ? \"working\" : expanded ? \"open\" : \"closed\"}\n data-content={contentType}\n aria-busy={working}\n className={cn(\"w-full text-sm\", className)}\n {...props}\n >\n {working ? (\n <div\n data-slot=\"agent-activity-status\"\n id={triggerId}\n role=\"status\"\n className=\"flex h-7 min-w-0 items-center text-muted-foreground\"\n >\n {renderWorkingStatus ? (\n renderWorkingStatus({ label: liveLabel, duration })\n ) : (\n // This row is the live region; the shimmer must not nest a second one.\n <ShimmerText role={undefined} className=\"font-medium\">\n {liveLabel}\n </ShimmerText>\n )}\n </div>\n ) : (\n <button\n data-slot=\"agent-activity-trigger\"\n id={triggerId}\n type=\"button\"\n aria-expanded={expanded}\n aria-controls={contentId}\n onClick={toggle}\n className=\"group flex h-7 min-w-0 items-center gap-1.5 rounded-md text-left font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n >\n <span className=\"truncate\">\n {renderCompletedStatus\n ? renderCompletedStatus({ summary: completedSummary, duration })\n : completedSummary}\n </span>\n <motion.span\n aria-hidden=\"true\"\n animate={{ rotate: expanded ? 180 : 0 }}\n transition={reduce ? { duration: 0 } : SPRING_SWAP}\n className=\"inline-flex shrink-0 text-muted-foreground/70 group-hover:text-foreground\"\n >\n <ChevronDownIcon className=\"size-3.5\" />\n </motion.span>\n </button>\n )}\n\n <Disclosure\n id={contentId}\n role=\"region\"\n aria-labelledby={triggerId}\n open={expanded}\n openHeight={viewportHeight}\n >\n <div\n data-slot=\"agent-activity-viewport\"\n ref={viewportRef}\n className={cn(\n \"pr-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n capped && expanded && !working\n ? \"overflow-y-auto\"\n : \"overflow-y-hidden\"\n )}\n style={{\n height: viewportHeight,\n maskImage,\n WebkitMaskImage: maskImage,\n }}\n >\n <motion.div\n data-slot=\"agent-activity-list\"\n ref={contentRef}\n role=\"list\"\n initial={false}\n animate={{ y: streamOffset }}\n transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n className={cn(\"space-y-0.5 py-2\", contentClassName)}\n >\n <AnimatePresence mode=\"popLayout\">\n {items.map((item) => (\n <motion.div\n data-slot=\"agent-activity-item\"\n data-type={item.type}\n layout=\"position\"\n key={item.id}\n role=\"listitem\"\n initial={reduce ? { opacity: 1 } : { opacity: 0, y: 6 }}\n animate={{ opacity: 1, y: 0 }}\n exit={reduce ? { opacity: 0 } : { opacity: 0, y: -3 }}\n transition={\n reduce\n ? { duration: 0 }\n : {\n opacity: { duration: 0.18, ease: EASE_OUT },\n y: SPRING_LAYOUT,\n layout: SPRING_LAYOUT,\n }\n }\n >\n <AgentActivityRow\n item={item}\n moreLabel={moreLabel}\n actionLabels={actionLabels}\n />\n </motion.div>\n ))}\n </AnimatePresence>\n </motion.div>\n </div>\n </Disclosure>\n </div>\n );\n}\n\nexport { AgentActivity, AgentActivityRow };\n",
|
|
18
|
+
"type": "registry:ui",
|
|
19
|
+
"target": "components/ui/ai-agent-activity.tsx"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"type": "registry:ui"
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "ai-agent-progress",
|
|
4
|
+
"title": "AI Agent Progress",
|
|
5
|
+
"description": "A long-running step: a pulsing grid beside the verb and an elapsed-time counter.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"motion"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@intelligo/ai-motion"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "base/ui/ai-agent-progress/ai-agent-progress.tsx",
|
|
15
|
+
"content": "\"use client\";\n\n/*\n * A long-running step: a 3×3 grid pulses in sequence beside the verb and\n * an elapsed-time counter.\n */\n\nimport * as React from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nimport { EASE_IN_OUT } from \"@/components/ui/ai-motion\";\nimport { cn } from \"@/lib/utils\";\n\nconst GRID_CELLS = [\n { id: \"top-left\", delay: 0 },\n { id: \"top-center\", delay: 0.14 },\n { id: \"top-right\", delay: 0.28 },\n { id: \"middle-left\", delay: 0.42 },\n { id: \"middle-center\", delay: 0.56 },\n { id: \"middle-right\", delay: 0.7 },\n { id: \"bottom-left\", delay: 0.84 },\n { id: \"bottom-center\", delay: 0.98 },\n { id: \"bottom-right\", delay: 1.12 },\n];\n\nexport interface AgentProgressProps\n extends Omit<React.ComponentProps<\"span\">, \"children\"> {\n /** Verb describing the agent's current activity. */\n label?: string;\n /** Assistive-tech name; defaults to \"{label}, in progress\". */\n statusLabel?: string;\n /** Controlled elapsed time in seconds. */\n elapsedSeconds?: number;\n /** Starting time for the internal timer, in seconds. */\n initialSeconds?: number;\n /** Whether the internal timer should advance. Ignored when elapsedSeconds is provided. */\n running?: boolean;\n /** Renders the counter; \"1m 4.2s\" by default. */\n formatElapsed?: (totalSeconds: number) => string;\n}\n\nfunction defaultFormatElapsed(totalSeconds: number) {\n const safeSeconds = Math.max(0, totalSeconds);\n const minutes = Math.floor(safeSeconds / 60);\n const seconds = (safeSeconds % 60).toFixed(1);\n return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;\n}\n\nfunction AgentProgress({\n label = \"Churning\",\n statusLabel,\n elapsedSeconds,\n initialSeconds = 0,\n running = true,\n formatElapsed = defaultFormatElapsed,\n className,\n ...props\n}: AgentProgressProps) {\n const reduced = useReducedMotion() ?? false;\n const [internalSeconds, setInternalSeconds] = React.useState(initialSeconds);\n\n React.useEffect(() => {\n if (elapsedSeconds !== undefined || !running) return;\n\n const startedAt = performance.now() - initialSeconds * 1000;\n const timer = window.setInterval(() => {\n setInternalSeconds((performance.now() - startedAt) / 1000);\n }, 100);\n\n return () => window.clearInterval(timer);\n }, [elapsedSeconds, initialSeconds, running]);\n\n const elapsed = elapsedSeconds ?? internalSeconds;\n\n return (\n <span\n data-slot=\"agent-progress\"\n role=\"status\"\n aria-label={statusLabel ?? `${label}, in progress`}\n className={cn(\n \"inline-flex items-center gap-3 font-mono text-sm text-muted-foreground\",\n className\n )}\n {...props}\n >\n <span\n data-slot=\"agent-progress-grid\"\n aria-hidden=\"true\"\n className=\"grid size-5 shrink-0 grid-cols-3 gap-0.5\"\n >\n {GRID_CELLS.map(({ id, delay }) => (\n <motion.span\n key={id}\n className=\"rounded-xs bg-current\"\n animate={\n reduced\n ? { opacity: [0.35, 0.8, 0.35] }\n : {\n opacity: [0.28, 1, 0.28],\n scale: [0.72, 1, 0.72],\n }\n }\n transition={{\n duration: 1.55,\n ease: EASE_IN_OUT,\n repeat: Infinity,\n delay,\n }}\n />\n ))}\n </span>\n <span data-slot=\"agent-progress-label\" className=\"font-sans font-medium\">\n {label}\n </span>\n <span\n data-slot=\"agent-progress-elapsed\"\n aria-hidden=\"true\"\n className=\"tabular-nums text-muted-foreground/70\"\n >\n {formatElapsed(elapsed)}\n </span>\n </span>\n );\n}\n\nexport { AgentProgress };\n",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": "components/ui/ai-agent-progress.tsx"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"type": "registry:ui"
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "ai-approval-card",
|
|
4
|
+
"title": "AI Approval Card",
|
|
5
|
+
"description": "A decision surface: approve, reject or request changes, or step through single-choice, multiple-choice and freeform questions and submit the answers.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"lucide-react",
|
|
8
|
+
"motion"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"@intelligo/ai-motion",
|
|
12
|
+
"@intelligo/button",
|
|
13
|
+
"@intelligo/checkbox",
|
|
14
|
+
"@intelligo/input",
|
|
15
|
+
"@intelligo/radio-group",
|
|
16
|
+
"@intelligo/spinner"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
{
|
|
20
|
+
"path": "base/ui/ai-approval-card/ai-approval-card.tsx",
|
|
21
|
+
"content": "\"use client\";\n\n/*\n * A decision surface the agent hands to the user: approve / reject /\n * request changes, or a short run of questions (single choice, multiple\n * choice, freeform) answered one step at a time and submitted together.\n */\n\nimport * as React from \"react\";\nimport {\n ArrowLeftIcon,\n ArrowRightIcon,\n CheckIcon,\n CircleHelpIcon,\n MessageSquareTextIcon,\n XIcon,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nimport {\n Disclosure,\n EASE_OUT,\n SPRING_SWAP,\n SwapText,\n} from \"@/components/ui/ai-motion\";\nimport { Button } from \"@/components/ui/button\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Input } from \"@/components/ui/input\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ApprovalCardStatus =\n | \"pending\"\n | \"submitting\"\n | \"approved\"\n | \"rejected\"\n | \"changes-requested\"\n | \"answered\";\n\nexport interface ApprovalCardOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface ApprovalCardQuestion {\n id: string;\n title: React.ReactNode;\n description?: React.ReactNode;\n options?: ApprovalCardOption[];\n /** Several options may be chosen; otherwise a single choice. */\n multiple?: boolean;\n /** Move on shortly after a single choice (default true). */\n autoAdvance?: boolean;\n /** Offer a freeform answer beside (or instead of) the options. */\n allowCustom?: boolean;\n customPlaceholder?: string;\n}\n\nexport interface ApprovalCardAnswer {\n selected: string[];\n custom?: string;\n}\n\nexport type ApprovalCardAnswers = Record<string, ApprovalCardAnswer>;\n\nexport interface ApprovalCardProps\n extends Omit<React.ComponentProps<\"div\">, \"title\" | \"onSubmit\"> {\n title?: React.ReactNode;\n description?: React.ReactNode;\n questions?: ApprovalCardQuestion[];\n status?: ApprovalCardStatus;\n answers?: ApprovalCardAnswers;\n defaultAnswers?: ApprovalCardAnswers;\n onAnswersChange?: (answers: ApprovalCardAnswers) => void;\n step?: number;\n defaultStep?: number;\n onStepChange?: (step: number) => void;\n onSubmit?: (answers: ApprovalCardAnswers) => void;\n onApprove?: () => void;\n onReject?: () => void;\n onRequestChanges?: () => void;\n onDismiss?: () => void;\n /** What to show once the decision has landed; defaults to the status label. */\n result?: React.ReactNode;\n approveLabel?: React.ReactNode;\n rejectLabel?: React.ReactNode;\n requestChangesLabel?: React.ReactNode;\n submitLabel?: React.ReactNode;\n dismissLabel?: string;\n previousLabel?: string;\n nextLabel?: string;\n customPlaceholder?: string;\n /** Screen-reader progress, e.g. \"Question 2 of 5\". */\n progressLabel?: (current: number, total: number) => string;\n statusLabels?: Partial<Record<ApprovalCardStatus, string>>;\n}\n\nconst EMPTY_ANSWER: ApprovalCardAnswer = { selected: [], custom: \"\" };\n\nconst DEFAULT_STATUS_LABELS: Record<ApprovalCardStatus, string> = {\n pending: \"Input required\",\n submitting: \"Submitting\",\n approved: \"Approved\",\n rejected: \"Rejected\",\n \"changes-requested\": \"Changes requested\",\n answered: \"Response submitted\",\n};\n\nconst ICON_CLASS: Partial<Record<ApprovalCardStatus, string>> = {\n approved: \"text-success\",\n answered: \"text-success\",\n rejected: \"text-destructive\",\n \"changes-requested\": \"text-warning\",\n};\n\nconst BADGE_CLASS: Record<ApprovalCardStatus, string> = {\n pending: \"border-warning/30 bg-warning/10 text-warning\",\n \"changes-requested\": \"border-warning/30 bg-warning/10 text-warning\",\n submitting: \"border-info/30 bg-info/10 text-info\",\n approved: \"border-success/30 bg-success/10 text-success\",\n answered: \"border-success/30 bg-success/10 text-success\",\n rejected: \"border-destructive/30 bg-destructive/10 text-destructive\",\n};\n\nfunction isAnswered(answer: ApprovalCardAnswer) {\n return answer.selected.length > 0 || Boolean(answer.custom?.trim());\n}\n\n/* ----------------------------------------------------------------------------\n * One question's controls: checkboxes, radios and/or a freeform input.\n * ------------------------------------------------------------------------- */\n\nconst OPTION_ROW =\n \"flex min-h-9 items-center gap-3 rounded-lg px-1.5 py-1 text-sm text-foreground select-none\";\n\nfunction OptionRow({\n disabled,\n children,\n}: {\n disabled: boolean;\n children: React.ReactNode;\n}) {\n return (\n <label\n data-slot=\"approval-card-option\"\n className={cn(\n OPTION_ROW,\n disabled ? \"cursor-not-allowed opacity-60\" : \"cursor-pointer\"\n )}\n >\n {children}\n </label>\n );\n}\n\nfunction QuestionOptions({\n question,\n answer,\n disabled,\n customPlaceholder,\n onChange,\n onSingleSelect,\n}: {\n question: ApprovalCardQuestion;\n answer: ApprovalCardAnswer;\n disabled: boolean;\n customPlaceholder: string;\n onChange: (answer: ApprovalCardAnswer) => void;\n onSingleSelect?: () => void;\n}) {\n const custom = answer.custom ?? \"\";\n\n return (\n <div data-slot=\"approval-card-question\" className=\"mt-3\">\n {question.options?.length ? (\n question.multiple ? (\n <div className=\"grid gap-0.5\">\n {question.options.map((option) => {\n const rowDisabled = disabled || Boolean(option.disabled);\n return (\n <OptionRow key={option.value} disabled={rowDisabled}>\n <Checkbox\n checked={answer.selected.includes(option.value)}\n disabled={rowDisabled}\n onCheckedChange={(checked) =>\n onChange({\n ...answer,\n selected: checked\n ? [...answer.selected, option.value]\n : answer.selected.filter(\n (value) => value !== option.value\n ),\n })\n }\n />\n <span>{option.label}</span>\n </OptionRow>\n );\n })}\n </div>\n ) : (\n <RadioGroup\n value={answer.selected[0] ?? \"\"}\n onValueChange={(value) => {\n onChange({ selected: [String(value)], custom: \"\" });\n onSingleSelect?.();\n }}\n className=\"gap-0.5\"\n >\n {question.options.map((option) => {\n const rowDisabled = disabled || Boolean(option.disabled);\n return (\n <OptionRow key={option.value} disabled={rowDisabled}>\n <RadioGroupItem value={option.value} disabled={rowDisabled} />\n <span>{option.label}</span>\n </OptionRow>\n );\n })}\n </RadioGroup>\n )\n ) : null}\n\n {question.allowCustom ? (\n <Input\n data-slot=\"approval-card-custom\"\n value={custom}\n disabled={disabled}\n placeholder={question.customPlaceholder ?? customPlaceholder}\n aria-label={question.customPlaceholder ?? customPlaceholder}\n onChange={(event) =>\n onChange({\n selected: question.multiple ? answer.selected : [],\n custom: event.target.value,\n })\n }\n className={cn(\n \"h-10 rounded-lg border-0 bg-background/70 px-3 text-sm focus-visible:bg-background\",\n question.options?.length && \"mt-1.5\"\n )}\n />\n ) : null}\n </div>\n );\n}\n\n/* ----------------------------------------------------------------------------\n * Progress dots: one per question, the current one full size.\n * ------------------------------------------------------------------------- */\n\nfunction ProgressDots({\n current,\n ids,\n label,\n}: {\n current: number;\n ids: string[];\n label: string;\n}) {\n const reduced = useReducedMotion() ?? false;\n\n return (\n <span data-slot=\"approval-card-progress\" className=\"flex gap-1.5\">\n <span className=\"sr-only\">{label}</span>\n {ids.map((id, index) => (\n <motion.span\n key={id}\n aria-hidden=\"true\"\n initial={false}\n animate={{\n scale: reduced ? 1 : index === current ? 1 : 0.75,\n opacity: index <= current ? 1 : 0.35,\n }}\n transition={reduced ? { duration: 0 } : SPRING_SWAP}\n className=\"size-1.5 rounded-full bg-foreground\"\n />\n ))}\n </span>\n );\n}\n\n/* ----------------------------------------------------------------------------\n * ApprovalCard: the card.\n * ------------------------------------------------------------------------- */\n\nfunction ApprovalCard({\n title = \"Approval required\",\n description,\n children,\n questions = [],\n status = \"pending\",\n answers,\n defaultAnswers = {},\n onAnswersChange,\n step,\n defaultStep = 0,\n onStepChange,\n onSubmit,\n onApprove,\n onReject,\n onRequestChanges,\n onDismiss,\n result,\n approveLabel = \"Approve\",\n rejectLabel = \"Reject\",\n requestChangesLabel = \"Request changes\",\n submitLabel = \"Submit response\",\n dismissLabel = \"Dismiss\",\n previousLabel = \"Previous question\",\n nextLabel = \"Next question\",\n customPlaceholder = \"Add another response…\",\n progressLabel = (current, total) => `Question ${current} of ${total}`,\n statusLabels,\n className,\n ...props\n}: ApprovalCardProps) {\n const reduced = useReducedMotion() ?? false;\n const [internalAnswers, setInternalAnswers] =\n React.useState<ApprovalCardAnswers>(defaultAnswers);\n const [internalStep, setInternalStep] = React.useState(defaultStep);\n const autoAdvanceTimer = React.useRef<number | undefined>(undefined);\n const currentAnswers = answers ?? internalAnswers;\n const currentStep = Math.min(\n Math.max(0, step ?? internalStep),\n Math.max(0, questions.length - 1)\n );\n const question = questions[currentStep];\n const questionMode = questions.length > 0;\n const lastStep = currentStep === questions.length - 1;\n const pending = status === \"pending\";\n const busy = status === \"submitting\";\n const interactive = pending || busy;\n const currentAnswer = question\n ? (currentAnswers[question.id] ?? EMPTY_ANSWER)\n : EMPTY_ANSWER;\n const displayTitle = question?.title ?? title;\n const titleKey = question?.id ?? String(status);\n const statusLabel = statusLabels?.[status] ?? DEFAULT_STATUS_LABELS[status];\n\n const clearAutoAdvance = React.useCallback(() => {\n if (autoAdvanceTimer.current === undefined) return;\n window.clearTimeout(autoAdvanceTimer.current);\n autoAdvanceTimer.current = undefined;\n }, []);\n\n React.useEffect(() => clearAutoAdvance, [clearAutoAdvance]);\n\n const setAnswers = React.useCallback(\n (next: ApprovalCardAnswers) => {\n if (answers === undefined) setInternalAnswers(next);\n onAnswersChange?.(next);\n },\n [answers, onAnswersChange]\n );\n\n const setStep = (next: number) => {\n clearAutoAdvance();\n if (step === undefined) setInternalStep(next);\n onStepChange?.(next);\n };\n\n const updateCurrentAnswer = (next: ApprovalCardAnswer) => {\n if (!question) return;\n setAnswers({ ...currentAnswers, [question.id]: next });\n };\n\n const continueQuestion = () => {\n if (currentStep < questions.length - 1) {\n setStep(currentStep + 1);\n return;\n }\n onSubmit?.(currentAnswers);\n };\n\n // A single choice reads as \"done\"; the next question follows after a beat\n // so the selection is seen before the panel slides.\n const queueAutoAdvance = () => {\n if (\n !question ||\n question.multiple ||\n question.autoAdvance === false ||\n currentStep >= questions.length - 1 ||\n busy\n ) {\n return;\n }\n\n clearAutoAdvance();\n autoAdvanceTimer.current = window.setTimeout(() => {\n setStep(currentStep + 1);\n }, 240);\n };\n\n return (\n <div\n data-slot=\"approval-card\"\n data-state={status}\n aria-busy={busy || undefined}\n className={cn(\n \"w-full overflow-hidden rounded-xl bg-muted p-4 text-sm\",\n className\n )}\n {...props}\n >\n <div className=\"flex items-start gap-3\">\n <span\n data-slot=\"approval-card-icon\"\n aria-hidden=\"true\"\n className={cn(\n \"grid size-5 shrink-0 place-items-center text-muted-foreground\",\n ICON_CLASS[status]\n )}\n >\n {busy ? (\n <Spinner className=\"size-4\" />\n ) : interactive ? (\n questionMode ? (\n <CircleHelpIcon className=\"size-4\" />\n ) : (\n <MessageSquareTextIcon className=\"size-4\" />\n )\n ) : status === \"rejected\" ? (\n <XIcon className=\"size-4\" />\n ) : (\n <CheckIcon className=\"size-4\" />\n )}\n </span>\n\n <div className=\"min-w-0 flex-1\">\n <div data-slot=\"approval-card-header\" className=\"flex min-w-0 items-start gap-3\">\n <h3\n data-slot=\"approval-card-title\"\n className=\"min-w-0 flex-1 text-base leading-5 font-medium text-foreground\"\n >\n <SwapText value={titleKey}>{displayTitle}</SwapText>\n </h3>\n {questionMode && interactive ? (\n <span\n data-slot=\"approval-card-counter\"\n className=\"shrink-0 text-xs text-muted-foreground/65 tabular-nums\"\n >\n {currentStep + 1}/{questions.length}\n </span>\n ) : (\n <span\n data-slot=\"approval-card-status\"\n className={cn(\n \"shrink-0 rounded-full border px-2 py-0.5 text-xs font-medium transition-colors\",\n BADGE_CLASS[status]\n )}\n >\n {statusLabel}\n </span>\n )}\n {onDismiss ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={dismissLabel}\n onClick={onDismiss}\n className=\"-mt-0.5 shrink-0 text-muted-foreground hover:text-foreground\"\n >\n <XIcon className=\"size-4\" />\n </Button>\n ) : null}\n </div>\n\n <Disclosure open={interactive}>\n {questionMode && question ? (\n <AnimatePresence initial={false} mode=\"wait\">\n <motion.div\n key={question.id}\n data-slot=\"approval-card-step\"\n initial={reduced ? { opacity: 1 } : { opacity: 0, x: 8 }}\n animate={{ opacity: 1, x: 0 }}\n exit={reduced ? { opacity: 0 } : { opacity: 0, x: -6 }}\n transition={{ duration: reduced ? 0 : 0.2, ease: EASE_OUT }}\n >\n {question.description ? (\n <p className=\"mt-1 leading-5 text-muted-foreground\">\n {question.description}\n </p>\n ) : null}\n <QuestionOptions\n question={question}\n answer={currentAnswer}\n disabled={busy}\n customPlaceholder={customPlaceholder}\n onChange={updateCurrentAnswer}\n onSingleSelect={queueAutoAdvance}\n />\n </motion.div>\n </AnimatePresence>\n ) : (\n <div data-slot=\"approval-card-content\">\n {description ? (\n <p className=\"mt-1 leading-5 text-muted-foreground\">\n {description}\n </p>\n ) : null}\n {children ? <div className=\"mt-3\">{children}</div> : null}\n </div>\n )}\n\n {questionMode ? (\n <div\n data-slot=\"approval-card-actions\"\n className=\"mt-4 flex items-center gap-3\"\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label={previousLabel}\n disabled={busy || currentStep === 0}\n onClick={() => setStep(currentStep - 1)}\n >\n <ArrowLeftIcon className=\"size-4\" />\n </Button>\n <ProgressDots\n current={currentStep}\n ids={questions.map((item) => item.id)}\n label={progressLabel(currentStep + 1, questions.length)}\n />\n <Button\n type=\"button\"\n size={lastStep ? \"sm\" : \"icon\"}\n aria-label={lastStep ? undefined : nextLabel}\n disabled={busy || !isAnswered(currentAnswer)}\n aria-busy={busy || undefined}\n onClick={continueQuestion}\n className=\"ml-auto\"\n >\n {busy ? (\n <Spinner className=\"size-4\" />\n ) : lastStep ? (\n <>\n {submitLabel}\n <ArrowRightIcon className=\"size-3.5\" />\n </>\n ) : (\n <ArrowRightIcon className=\"size-4\" />\n )}\n </Button>\n </div>\n ) : (\n <div\n data-slot=\"approval-card-actions\"\n className=\"mt-4 flex flex-wrap items-center gap-2\"\n >\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={busy}\n aria-busy={busy || undefined}\n onClick={onApprove}\n >\n {approveLabel}\n </Button>\n {onRequestChanges ? (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={busy}\n onClick={onRequestChanges}\n >\n {requestChangesLabel}\n </Button>\n ) : null}\n {onReject ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={busy}\n onClick={onReject}\n className=\"text-muted-foreground hover:text-destructive\"\n >\n {rejectLabel}\n </Button>\n ) : null}\n </div>\n )}\n </Disclosure>\n\n {!interactive ? (\n <p\n data-slot=\"approval-card-result\"\n className=\"mt-1 text-sm text-muted-foreground\"\n >\n {result ?? statusLabel}\n </p>\n ) : null}\n </div>\n </div>\n </div>\n );\n}\n\nexport { ApprovalCard };\n",
|
|
22
|
+
"type": "registry:ui",
|
|
23
|
+
"target": "components/ui/ai-approval-card.tsx"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"type": "registry:ui"
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "ai-artifact",
|
|
4
|
+
"title": "AI Artifact",
|
|
5
|
+
"description": "A generated document's frame: header, actions and content.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"lucide-react"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@intelligo/button",
|
|
11
|
+
"@intelligo/tooltip"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "base/ui/ai-artifact/ai-artifact.tsx",
|
|
16
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { type LucideIcon, XIcon } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nfunction Artifact({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"artifact\"\n className={cn(\n \"flex flex-col overflow-hidden rounded-lg border bg-background shadow-sm\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction ArtifactHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"artifact-header\"\n className={cn(\n \"flex items-center justify-between gap-2 border-b bg-muted/50 px-4 py-3\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction ArtifactTitle({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"artifact-title\"\n className={cn(\"text-sm font-medium text-foreground\", className)}\n {...props}\n />\n );\n}\n\nfunction ArtifactDescription({\n className,\n ...props\n}: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"artifact-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nfunction ArtifactActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"artifact-actions\"\n className={cn(\"flex items-center gap-1\", className)}\n {...props}\n />\n );\n}\n\nfunction ArtifactClose({\n label,\n children,\n variant = \"ghost\",\n size = \"icon-sm\",\n ...props\n}: React.ComponentProps<typeof Button> & {\n /** Accessible name, e.g. \"Close\". */\n label: string;\n}) {\n return (\n <Button\n data-slot=\"artifact-close\"\n aria-label={label}\n size={size}\n type=\"button\"\n variant={variant}\n {...props}\n >\n {children ?? <XIcon />}\n </Button>\n );\n}\n\nfunction ArtifactAction({\n label,\n tooltip,\n icon: Icon,\n children,\n variant = \"ghost\",\n size = \"icon-sm\",\n ...props\n}: React.ComponentProps<typeof Button> & {\n /** Accessible name. */\n label: string;\n /** Shown on hover and focus when given. */\n tooltip?: string;\n icon?: LucideIcon;\n}) {\n const button = (\n <Button\n data-slot=\"artifact-action\"\n aria-label={label}\n size={size}\n type=\"button\"\n variant={variant}\n {...props}\n >\n {Icon ? <Icon /> : children}\n </Button>\n );\n\n if (!tooltip) return button;\n\n return (\n <Tooltip>\n <TooltipTrigger render={button} />\n <TooltipContent>{tooltip}</TooltipContent>\n </Tooltip>\n );\n}\n\nfunction ArtifactContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"artifact-content\"\n className={cn(\"flex-1 overflow-auto p-4\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Artifact,\n ArtifactHeader,\n ArtifactTitle,\n ArtifactDescription,\n ArtifactActions,\n ArtifactClose,\n ArtifactAction,\n ArtifactContent,\n};\n",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/ai-artifact.tsx"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|