@cosmoops/connect-sdk 0.6.0 → 0.9.0
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/CHANGELOG.md +165 -0
- package/README.md +273 -91
- package/dist/cjs/cli/doctor.js +3 -3
- package/dist/cjs/cli/init.js +3 -3
- package/dist/cjs/client/components/access-gate.js +42 -7
- package/dist/cjs/client/components/account-sessions.js +3 -3
- package/dist/cjs/client/components/ai-format.js +135 -0
- package/dist/cjs/client/components/ai-widgets.js +232 -0
- package/dist/cjs/client/components/app-shell.js +9 -8
- package/dist/cjs/client/components/apps-grid.js +3 -3
- package/dist/cjs/client/components/ask-ai.js +334 -0
- package/dist/cjs/client/components/feature-gate.js +2 -2
- package/dist/cjs/client/components/integrations-modal.js +1 -1
- package/dist/cjs/client/components/logo.js +3 -3
- package/dist/cjs/client/components/nav.js +1 -1
- package/dist/cjs/client/components/plan-pricing.js +1 -1
- package/dist/cjs/client/components/plans.js +6 -6
- package/dist/cjs/client/components/readiness.js +3 -3
- package/dist/cjs/client/components/side-nav.js +1 -1
- package/dist/cjs/client/components/subscribe-dialog.js +2 -2
- package/dist/cjs/client/components/subscription.js +1 -1
- package/dist/cjs/client/components/usage-meter.js +1 -1
- package/dist/cjs/client/components/user-menu.js +31 -8
- package/dist/cjs/client/components/workspace-type-gate.js +48 -0
- package/dist/cjs/client/index.js +32 -4
- package/dist/cjs/client/mark.js +1 -1
- package/dist/cjs/client/provider.js +50 -7
- package/dist/cjs/client/use-checkout.js +3 -3
- package/dist/cjs/errors.js +4 -4
- package/dist/cjs/helpers.js +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/next/index.js +2 -2
- package/dist/cjs/server/client.js +15 -15
- package/dist/cjs/server/handler.js +56 -13
- package/dist/cjs/server/http.js +47 -25
- package/dist/cjs/server/resources/access.js +3 -3
- package/dist/cjs/server/resources/ai.js +1 -1
- package/dist/cjs/server/resources/analytics.js +2 -2
- package/dist/cjs/server/resources/apps.js +4 -4
- package/dist/cjs/server/resources/billing.js +5 -5
- package/dist/cjs/server/resources/drive.js +4 -4
- package/dist/cjs/server/resources/entitlements.js +48 -7
- package/dist/cjs/server/resources/identity.js +6 -4
- package/dist/cjs/server/resources/integrations.js +13 -13
- package/dist/cjs/server/resources/logs.js +3 -3
- package/dist/cjs/server/resources/sessions.js +6 -6
- package/dist/cjs/server/webhooks.js +4 -4
- package/dist/cjs/types.js +1 -1
- package/dist/esm/cli/doctor.js +3 -3
- package/dist/esm/cli/init.js +3 -3
- package/dist/esm/client/components/access-gate.js +43 -8
- package/dist/esm/client/components/account-sessions.js +3 -3
- package/dist/esm/client/components/ai-format.js +129 -0
- package/dist/esm/client/components/ai-widgets.js +223 -0
- package/dist/esm/client/components/app-shell.js +8 -7
- package/dist/esm/client/components/apps-grid.js +3 -3
- package/dist/esm/client/components/ask-ai.js +331 -0
- package/dist/esm/client/components/feature-gate.js +2 -2
- package/dist/esm/client/components/integrations-modal.js +1 -1
- package/dist/esm/client/components/logo.js +3 -3
- package/dist/esm/client/components/nav.js +1 -1
- package/dist/esm/client/components/plan-pricing.js +1 -1
- package/dist/esm/client/components/plans.js +6 -6
- package/dist/esm/client/components/readiness.js +3 -3
- package/dist/esm/client/components/side-nav.js +1 -1
- package/dist/esm/client/components/subscribe-dialog.js +2 -2
- package/dist/esm/client/components/subscription.js +1 -1
- package/dist/esm/client/components/usage-meter.js +1 -1
- package/dist/esm/client/components/user-menu.js +31 -8
- package/dist/esm/client/components/workspace-type-gate.js +45 -0
- package/dist/esm/client/index.js +14 -3
- package/dist/esm/client/mark.js +1 -1
- package/dist/esm/client/provider.js +48 -7
- package/dist/esm/client/use-checkout.js +3 -3
- package/dist/esm/errors.js +4 -4
- package/dist/esm/helpers.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/next/index.js +2 -2
- package/dist/esm/server/client.js +15 -15
- package/dist/esm/server/handler.js +56 -13
- package/dist/esm/server/http.js +47 -25
- package/dist/esm/server/resources/access.js +3 -3
- package/dist/esm/server/resources/ai.js +1 -1
- package/dist/esm/server/resources/analytics.js +2 -2
- package/dist/esm/server/resources/apps.js +4 -4
- package/dist/esm/server/resources/billing.js +5 -5
- package/dist/esm/server/resources/drive.js +4 -4
- package/dist/esm/server/resources/entitlements.js +48 -7
- package/dist/esm/server/resources/identity.js +6 -4
- package/dist/esm/server/resources/integrations.js +13 -13
- package/dist/esm/server/resources/logs.js +3 -3
- package/dist/esm/server/resources/sessions.js +6 -6
- package/dist/esm/server/webhooks.js +4 -4
- package/dist/esm/types.js +1 -1
- package/dist/theme.css +87 -0
- package/dist/types/client/components/access-gate.d.ts +18 -4
- package/dist/types/client/components/account-sessions.d.ts +1 -1
- package/dist/types/client/components/ai-format.d.ts +24 -0
- package/dist/types/client/components/ai-widgets.d.ts +42 -0
- package/dist/types/client/components/app-shell.d.ts +25 -9
- package/dist/types/client/components/apps-grid.d.ts +1 -1
- package/dist/types/client/components/ask-ai.d.ts +73 -0
- package/dist/types/client/components/feature-gate.d.ts +1 -1
- package/dist/types/client/components/integrations-modal.d.ts +1 -1
- package/dist/types/client/components/logo.d.ts +5 -5
- package/dist/types/client/components/nav.d.ts +3 -3
- package/dist/types/client/components/plan-pricing.d.ts +2 -2
- package/dist/types/client/components/readiness.d.ts +2 -2
- package/dist/types/client/components/side-nav.d.ts +1 -1
- package/dist/types/client/components/subscribe-dialog.d.ts +3 -3
- package/dist/types/client/components/usage-meter.d.ts +1 -1
- package/dist/types/client/components/user-menu.d.ts +4 -3
- package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
- package/dist/types/client/index.d.ts +11 -4
- package/dist/types/client/mark.d.ts +1 -1
- package/dist/types/client/provider.d.ts +52 -9
- package/dist/types/client/use-checkout.d.ts +3 -3
- package/dist/types/errors.d.ts +4 -4
- package/dist/types/helpers.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/next/index.d.ts +1 -1
- package/dist/types/server/client.d.ts +13 -13
- package/dist/types/server/handler.d.ts +2 -2
- package/dist/types/server/http.d.ts +6 -1
- package/dist/types/server/resources/access.d.ts +3 -3
- package/dist/types/server/resources/ai.d.ts +2 -2
- package/dist/types/server/resources/analytics.d.ts +2 -2
- package/dist/types/server/resources/apps.d.ts +4 -4
- package/dist/types/server/resources/billing.d.ts +4 -4
- package/dist/types/server/resources/drive.d.ts +4 -4
- package/dist/types/server/resources/entitlements.d.ts +37 -5
- package/dist/types/server/resources/identity.d.ts +2 -2
- package/dist/types/server/resources/integrations.d.ts +9 -9
- package/dist/types/server/resources/logs.d.ts +3 -3
- package/dist/types/server/resources/sessions.d.ts +5 -5
- package/dist/types/server/webhooks.d.ts +4 -4
- package/dist/types/types.d.ts +235 -61
- package/package.json +8 -2
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ConnectAskAi = ConnectAskAi;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const react_2 = require("@heroui/react");
|
|
8
|
+
const lucide_react_1 = require("lucide-react");
|
|
9
|
+
const streamdown_1 = require("streamdown");
|
|
10
|
+
const provider_js_1 = require("../provider.js");
|
|
11
|
+
const ai_widgets_js_1 = require("./ai-widgets.js");
|
|
12
|
+
const PANEL_WIDTH = "w-full lg:w-[400px]";
|
|
13
|
+
/**
|
|
14
|
+
* `blurIn` over the default `fadeIn`: Gemini arrives in bursts rather than an
|
|
15
|
+
* even token drip, and a blur masks a batch landing at once where a plain
|
|
16
|
+
* opacity fade just flickers. Hoisted to module scope because Streamdown
|
|
17
|
+
* compares `animated` by reference.
|
|
18
|
+
*/
|
|
19
|
+
const ANIMATED = {
|
|
20
|
+
animation: "blurIn",
|
|
21
|
+
duration: 220,
|
|
22
|
+
easing: "ease-out",
|
|
23
|
+
sep: "word",
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The "working on it" state - a highlight sweeping through the word rather
|
|
27
|
+
* than a spinner beside it. `connect-ai-shimmer` is defined in the SDK's
|
|
28
|
+
* `theme.css`, which every application platform already imports.
|
|
29
|
+
*/
|
|
30
|
+
function ThinkingShimmer({ label = "Thinking" }) {
|
|
31
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: "connect-ai-shimmer text-xs font-medium", role: "status", children: label }));
|
|
32
|
+
}
|
|
33
|
+
/** Open while it streams, collapsed once the answer lands. */
|
|
34
|
+
function ThinkingDisclosure({ text, isStreaming }) {
|
|
35
|
+
if (!text)
|
|
36
|
+
return null;
|
|
37
|
+
return ((0, jsx_runtime_1.jsxs)("details", { className: "group mb-2", open: isStreaming, children: [(0, jsx_runtime_1.jsxs)("summary", { className: "flex cursor-pointer list-none items-center gap-1 text-[11px] font-medium text-muted select-none hover:text-foreground", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Brain, { className: "size-3" }), isStreaming ? (0, jsx_runtime_1.jsx)(ThinkingShimmer, {}) : "Thinking", (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "size-3 transition-transform group-open:rotate-180" })] }), (0, jsx_runtime_1.jsx)("div", { className: "mt-1.5 border-s-2 border-border ps-2 text-xs whitespace-pre-wrap text-muted italic", children: text })] }));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Structural guards, one per widget.
|
|
41
|
+
*
|
|
42
|
+
* The published package carries no schema validator, and should not gain one
|
|
43
|
+
* for this - so each tool input is checked for the fields its renderer would
|
|
44
|
+
* otherwise crash on. Anything that fails renders nothing: the model's prose
|
|
45
|
+
* sits around it either way, so a missing widget reads as terse rather than
|
|
46
|
+
* broken, which is the better failure.
|
|
47
|
+
*/
|
|
48
|
+
const isArray = (value) => Array.isArray(value) && value.length > 0;
|
|
49
|
+
const field = (input, key) => typeof input === "object" && input !== null ? input[key] : undefined;
|
|
50
|
+
function renderTool(name, input, renderMarkdown) {
|
|
51
|
+
switch (name) {
|
|
52
|
+
case "showTable":
|
|
53
|
+
return isArray(field(input, "columns")) && Array.isArray(field(input, "rows")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiTable, { ...input })) : null;
|
|
54
|
+
case "showChart":
|
|
55
|
+
return isArray(field(input, "series")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiChart, { ...input }) : null;
|
|
56
|
+
case "showMetrics":
|
|
57
|
+
return isArray(field(input, "metrics")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiMetrics, { ...input }) : null;
|
|
58
|
+
case "showTimeline":
|
|
59
|
+
return isArray(field(input, "events")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiTimeline, { ...input }) : null;
|
|
60
|
+
case "showDetails":
|
|
61
|
+
return isArray(field(input, "fields")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiDetails, { ...input }) : null;
|
|
62
|
+
case "showSteps":
|
|
63
|
+
return isArray(field(input, "steps")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSteps, { ...input, renderDetail: renderMarkdown })) : null;
|
|
64
|
+
case "showSources":
|
|
65
|
+
return isArray(field(input, "sources")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSources, { ...input }) : null;
|
|
66
|
+
default:
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Placeholder while a tool's arguments are still arriving, sized like the real thing. */
|
|
71
|
+
function ToolSkeleton() {
|
|
72
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "my-3 flex h-24 animate-pulse items-center justify-center rounded-lg border border-border bg-surface-secondary", role: "status", "aria-label": "Preparing result", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 text-accent/60", "aria-hidden": true }) }));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* One server-sent event stream, folded into the turn being built.
|
|
76
|
+
*
|
|
77
|
+
* Hand-rolled rather than pulled from `@ai-sdk/react`, and that is a deliberate
|
|
78
|
+
* trade: this package ships to partners, and `ai` + `@ai-sdk/react` is a large
|
|
79
|
+
* dependency to add to every application platform for one panel. The wire format
|
|
80
|
+
* is small, both ends of it are ours, and the chunk kinds below are all this
|
|
81
|
+
* panel can render.
|
|
82
|
+
*
|
|
83
|
+
* Tool arguments are read from `tool-input-available` - the complete object -
|
|
84
|
+
* rather than reassembled from `tool-input-delta` fragments. `tool-input-start`
|
|
85
|
+
* is enough to put a skeleton on screen in the meantime, and partial-JSON
|
|
86
|
+
* parsing buys nothing a placeholder does not.
|
|
87
|
+
*
|
|
88
|
+
* Chunks are newline-delimited `data:` frames; a frame can split across network
|
|
89
|
+
* reads, so the tail of each read is carried forward rather than parsed.
|
|
90
|
+
*/
|
|
91
|
+
async function readStream(response, onChunk) {
|
|
92
|
+
const body = response.body;
|
|
93
|
+
if (!body)
|
|
94
|
+
throw new Error("Ask AI returned no stream");
|
|
95
|
+
const reader = body.pipeThrough(new TextDecoderStream()).getReader();
|
|
96
|
+
let buffer = "";
|
|
97
|
+
for (;;) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done)
|
|
100
|
+
break;
|
|
101
|
+
buffer += value;
|
|
102
|
+
// Everything up to the last newline is whole; whatever follows it is a
|
|
103
|
+
// partial frame and waits for the next read.
|
|
104
|
+
const lines = buffer.split("\n");
|
|
105
|
+
buffer = lines.pop() ?? "";
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
if (!line.startsWith("data:"))
|
|
108
|
+
continue;
|
|
109
|
+
const payload = line.slice(5).trim();
|
|
110
|
+
if (!payload || payload === "[DONE]")
|
|
111
|
+
continue;
|
|
112
|
+
let chunk;
|
|
113
|
+
try {
|
|
114
|
+
chunk = JSON.parse(payload);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// A frame we cannot parse is a frame we cannot render. Dropping it
|
|
118
|
+
// keeps the rest of the answer flowing, which is strictly better than
|
|
119
|
+
// failing the whole turn over one malformed chunk.
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
switch (chunk.type) {
|
|
123
|
+
case "text-delta":
|
|
124
|
+
if (chunk.delta)
|
|
125
|
+
onChunk({ text: chunk.delta });
|
|
126
|
+
break;
|
|
127
|
+
case "reasoning-delta":
|
|
128
|
+
if (chunk.delta)
|
|
129
|
+
onChunk({ reasoning: chunk.delta });
|
|
130
|
+
break;
|
|
131
|
+
case "tool-input-start":
|
|
132
|
+
if (chunk.toolCallId && chunk.toolName) {
|
|
133
|
+
onChunk({ toolStart: { id: chunk.toolCallId, name: chunk.toolName } });
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
case "tool-input-available":
|
|
137
|
+
if (chunk.toolCallId)
|
|
138
|
+
onChunk({ toolInput: { id: chunk.toolCallId, input: chunk.input } });
|
|
139
|
+
break;
|
|
140
|
+
case "error":
|
|
141
|
+
throw new Error(chunk.errorText || "Ask AI failed");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Index of the final text block, or `-1`. A reverse loop rather than
|
|
148
|
+
* `findLastIndex`, which is ES2023 - this package targets ES2022 so its
|
|
149
|
+
* published output stays usable on the runtimes its `engines` field claims.
|
|
150
|
+
*/
|
|
151
|
+
function lastTextBlockIndex(blocks) {
|
|
152
|
+
for (let index = blocks.length - 1; index >= 0; index--) {
|
|
153
|
+
if (blocks[index]?.kind === "text")
|
|
154
|
+
return index;
|
|
155
|
+
}
|
|
156
|
+
return -1;
|
|
157
|
+
}
|
|
158
|
+
/** Appends a text delta to the open text block, or opens one after a widget. */
|
|
159
|
+
function appendText(blocks, text) {
|
|
160
|
+
const last = blocks.at(-1);
|
|
161
|
+
if (last?.kind === "text") {
|
|
162
|
+
return [...blocks.slice(0, -1), { kind: "text", text: last.text + text }];
|
|
163
|
+
}
|
|
164
|
+
return [...blocks, { kind: "text", text }];
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Ask AI - a chat panel grounded in the signed-in workspace's Context Index,
|
|
168
|
+
* streamed through your own bridge.
|
|
169
|
+
*
|
|
170
|
+
* Answers are not only prose. Connect's model can call a small set of
|
|
171
|
+
* presentation tools - `showTable`, `showChart`, `showMetrics`, `showTimeline`,
|
|
172
|
+
* `showDetails`, `showSteps`, `showSources` - and this panel renders each one
|
|
173
|
+
* natively: a sortable table with a CSV download, a chart, a row of metric
|
|
174
|
+
* tiles. Values arrive raw with a format tag, so they are formatted here rather
|
|
175
|
+
* than by the model, which is what lets a column sort numerically.
|
|
176
|
+
*
|
|
177
|
+
* Nothing here holds a credential: it POSTs to `${basePath}/ai/chat` on your
|
|
178
|
+
* own origin, and `createConnectHandler` is what reaches Connect. The workspace
|
|
179
|
+
* is resolved from the caller's session there, never sent from the browser - so
|
|
180
|
+
* a user can only ever ask about the workspace they are signed into.
|
|
181
|
+
*
|
|
182
|
+
* ```tsx
|
|
183
|
+
* <AppShell name="Dispatch" nav={nav}>
|
|
184
|
+
* {children}
|
|
185
|
+
* </AppShell>
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* `AppShell` mounts it by default. Mount it directly only when you are not
|
|
189
|
+
* using the shell: it renders a fixed corner trigger plus a panel that is an
|
|
190
|
+
* *in-flow flex sibling* at `lg` and up, so opening it pushes your content
|
|
191
|
+
* aside rather than covering it. That requires it to be the last child of a
|
|
192
|
+
* `flex` row owning the viewport height - which is exactly what `AppShell` is.
|
|
193
|
+
* Below `lg` it is a full-screen sheet with a backdrop instead; there is no
|
|
194
|
+
* room to push anything on a phone.
|
|
195
|
+
*
|
|
196
|
+
* **Host setup:** markdown rendering needs `streamdown` installed, one Tailwind
|
|
197
|
+
* `@source "…/node_modules/streamdown/dist/*.js"` line so its utility classes
|
|
198
|
+
* are generated, and `import "streamdown/styles.css"` for the streaming
|
|
199
|
+
* animation. The colour tokens Streamdown expects are already mapped in this
|
|
200
|
+
* SDK's `theme.css`.
|
|
201
|
+
*
|
|
202
|
+
* Answers require the workspace's Context Index to be turned on (a workspace
|
|
203
|
+
* admin toggle in Connect) and your API key to carry the `ai:chat` scope.
|
|
204
|
+
* Neither is something the browser can discover, so both surface as a message
|
|
205
|
+
* in the panel rather than as a hidden button.
|
|
206
|
+
*/
|
|
207
|
+
function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placeholder = "Ask a question...", hint = "Ask a question about your workspace - answers are grounded in what your team has indexed.", defaultOpen = false, markdownPlugins, }) {
|
|
208
|
+
const { session, isLoading, streamResource } = (0, provider_js_1.useConnect)();
|
|
209
|
+
const [isOpen, setIsOpen] = (0, react_1.useState)(defaultOpen);
|
|
210
|
+
const [turns, setTurns] = (0, react_1.useState)([]);
|
|
211
|
+
const [input, setInput] = (0, react_1.useState)("");
|
|
212
|
+
const [isBusy, setIsBusy] = (0, react_1.useState)(false);
|
|
213
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
214
|
+
const abortRef = (0, react_1.useRef)(null);
|
|
215
|
+
const bottomRef = (0, react_1.useRef)(null);
|
|
216
|
+
// Ids only have to be unique within one mounted conversation, so a counter is
|
|
217
|
+
// enough - and unlike `crypto.randomUUID()` it cannot differ between the
|
|
218
|
+
// server's render and the browser's.
|
|
219
|
+
const nextId = (0, react_1.useRef)(0);
|
|
220
|
+
(0, react_1.useEffect)(() => {
|
|
221
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
|
|
222
|
+
}, [turns]);
|
|
223
|
+
// A conversation left mid-stream should stop costing tokens the moment the
|
|
224
|
+
// panel goes away.
|
|
225
|
+
(0, react_1.useEffect)(() => () => abortRef.current?.abort(), []);
|
|
226
|
+
const renderMarkdown = (0, react_1.useCallback)((text, isStreaming = false) => ((0, jsx_runtime_1.jsx)(streamdown_1.Streamdown, { className: "connect-ai-body", mode: isStreaming ? "streaming" : "static", isAnimating: isStreaming, animated: ANIMATED, caret: isStreaming ? "block" : undefined, plugins: markdownPlugins,
|
|
227
|
+
// Retrieved documents cite real URLs; an interstitial on every one of
|
|
228
|
+
// them turns a citation into a chore. The renderer still sanitises the
|
|
229
|
+
// markup itself, which is the part that matters.
|
|
230
|
+
linkSafety: LINK_SAFETY, children: text })), [markdownPlugins]);
|
|
231
|
+
const stop = (0, react_1.useCallback)(() => {
|
|
232
|
+
abortRef.current?.abort();
|
|
233
|
+
abortRef.current = null;
|
|
234
|
+
setIsBusy(false);
|
|
235
|
+
}, []);
|
|
236
|
+
const send = (0, react_1.useCallback)(async (text) => {
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
abortRef.current = controller;
|
|
239
|
+
setError(null);
|
|
240
|
+
setIsBusy(true);
|
|
241
|
+
const userTurn = {
|
|
242
|
+
id: `u${nextId.current++}`,
|
|
243
|
+
role: "user",
|
|
244
|
+
blocks: [{ kind: "text", text }],
|
|
245
|
+
reasoning: "",
|
|
246
|
+
};
|
|
247
|
+
const answerId = `a${nextId.current++}`;
|
|
248
|
+
// The wire format Connect reads - part-based `UIMessage`s, not flat
|
|
249
|
+
// strings. Built from the turns *plus* this one rather than from state,
|
|
250
|
+
// which has not been committed yet at this point. Only text is sent back:
|
|
251
|
+
// a widget is a rendering of something the model already said.
|
|
252
|
+
const history = [...turns, userTurn].map((turn) => ({
|
|
253
|
+
id: turn.id,
|
|
254
|
+
role: turn.role,
|
|
255
|
+
parts: [
|
|
256
|
+
{
|
|
257
|
+
type: "text",
|
|
258
|
+
text: turn.blocks
|
|
259
|
+
.filter((block) => block.kind === "text")
|
|
260
|
+
.map((block) => block.text)
|
|
261
|
+
.join(""),
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
}));
|
|
265
|
+
setTurns((current) => [...current, userTurn]);
|
|
266
|
+
const patchAnswer = (patch) => setTurns((current) => current.map((turn) => (turn.id === answerId ? patch(turn) : turn)));
|
|
267
|
+
try {
|
|
268
|
+
// Relative to the provider's own `basePath` - `streamResource` prefixes
|
|
269
|
+
// it, the same way every other bridge call is addressed.
|
|
270
|
+
const response = await streamResource("/ai/chat", { messages: history }, { signal: controller.signal });
|
|
271
|
+
// The assistant's turn is appended only once the stream is live, so a
|
|
272
|
+
// failed request leaves an error under the question rather than an
|
|
273
|
+
// empty bubble that never fills.
|
|
274
|
+
setTurns((current) => [...current, { id: answerId, role: "assistant", blocks: [], reasoning: "" }]);
|
|
275
|
+
await readStream(response, (chunk) => {
|
|
276
|
+
if (chunk.text) {
|
|
277
|
+
patchAnswer((turn) => ({ ...turn, blocks: appendText(turn.blocks, chunk.text) }));
|
|
278
|
+
}
|
|
279
|
+
if (chunk.reasoning) {
|
|
280
|
+
patchAnswer((turn) => ({ ...turn, reasoning: turn.reasoning + chunk.reasoning }));
|
|
281
|
+
}
|
|
282
|
+
if (chunk.toolStart) {
|
|
283
|
+
const { id, name } = chunk.toolStart;
|
|
284
|
+
patchAnswer((turn) => ({ ...turn, blocks: [...turn.blocks, { kind: "tool", id, name, input: null }] }));
|
|
285
|
+
}
|
|
286
|
+
if (chunk.toolInput) {
|
|
287
|
+
const { id, input: toolInput } = chunk.toolInput;
|
|
288
|
+
patchAnswer((turn) => ({
|
|
289
|
+
...turn,
|
|
290
|
+
blocks: turn.blocks.map((block) => block.kind === "tool" && block.id === id ? { ...block, input: toolInput } : block),
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
catch (cause) {
|
|
296
|
+
// Pressing Stop aborts the fetch, which lands here - a decision, not a
|
|
297
|
+
// failure, and nothing to report.
|
|
298
|
+
if (!controller.signal.aborted) {
|
|
299
|
+
setError(cause instanceof Error ? cause.message : "Ask AI failed");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
if (abortRef.current === controller)
|
|
304
|
+
abortRef.current = null;
|
|
305
|
+
setIsBusy(false);
|
|
306
|
+
}
|
|
307
|
+
}, [streamResource, turns]);
|
|
308
|
+
const lastTurn = turns.at(-1);
|
|
309
|
+
const isAnswering = isBusy && lastTurn?.role === "assistant";
|
|
310
|
+
const rendered = (0, react_1.useMemo)(() => turns.map((turn) => {
|
|
311
|
+
const isLive = isAnswering && turn.id === lastTurn?.id;
|
|
312
|
+
const lastTextIndex = lastTextBlockIndex(turn.blocks);
|
|
313
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: `connect-ai-host rounded-md border border-border p-2.5 text-sm ${turn.role === "user" ? "bg-surface-secondary" : "bg-accent/5"}`, children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-1 text-[11px] font-semibold tracking-wide text-muted uppercase", children: turn.role === "user" ? "You" : assistantName }), turn.role === "assistant" && ((0, jsx_runtime_1.jsx)(ThinkingDisclosure, { text: turn.reasoning, isStreaming: isLive && lastTextIndex === -1 })), turn.blocks.map((block, index) => block.kind === "text" ? (
|
|
314
|
+
// The user's own turn is shown as they typed it - running a
|
|
315
|
+
// question that happens to start with `#` through the markdown
|
|
316
|
+
// renderer would turn it into a heading.
|
|
317
|
+
(0, jsx_runtime_1.jsx)("div", { children: turn.role === "user" ? ((0, jsx_runtime_1.jsx)("p", { className: "whitespace-pre-wrap", children: block.text })) : (renderMarkdown(block.text, isLive && index === lastTextIndex)) }, index)) : block.input === null ? ((0, jsx_runtime_1.jsx)(ToolSkeleton, {}, block.id)) : ((0, jsx_runtime_1.jsx)("div", { children: renderTool(block.name, block.input, renderMarkdown) }, block.id))), isLive && lastTextIndex === -1 && turn.blocks.length === 0 && (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] }, turn.id));
|
|
318
|
+
}), [turns, isAnswering, lastTurn?.id, assistantName, renderMarkdown]);
|
|
319
|
+
// Signed out there is no workspace to ground an answer in, and the bridge
|
|
320
|
+
// would answer 401 - so there is nothing to offer, not even a button.
|
|
321
|
+
if (isLoading || !session?.user)
|
|
322
|
+
return null;
|
|
323
|
+
const handleSubmit = (event) => {
|
|
324
|
+
event.preventDefault();
|
|
325
|
+
const text = input.trim();
|
|
326
|
+
if (!text || isBusy)
|
|
327
|
+
return;
|
|
328
|
+
setInput("");
|
|
329
|
+
void send(text);
|
|
330
|
+
};
|
|
331
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isOpen && ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "primary", "aria-label": title, onPress: () => setIsOpen(true), className: "fixed bottom-4 end-4 z-30 size-12 rounded-full shadow-lg sm:bottom-6 sm:end-6", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)("div", { className: `fixed inset-0 z-40 bg-backdrop transition-opacity lg:hidden ${isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`, onClick: () => setIsOpen(false), "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("aside", { "aria-hidden": !isOpen, className: `fixed inset-y-0 end-0 z-50 flex flex-col border-s border-border bg-surface shadow-xl transition-transform duration-300 ease-in-out ${PANEL_WIDTH} ${isOpen ? "translate-x-0" : "translate-x-full rtl:-translate-x-full"} lg:static lg:z-auto lg:translate-x-0 lg:shadow-none lg:transition-[width] ${isOpen ? "" : "lg:w-0 lg:overflow-hidden lg:border-s-0"}`, children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-full shrink-0 flex-col ${PANEL_WIDTH}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 border-b border-border px-4 py-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 shrink-0 text-accent" }), (0, jsx_runtime_1.jsx)("span", { className: "flex-1 text-sm font-semibold", children: title }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Close", onPress: () => setIsOpen(false), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "size-4" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [turns.length === 0 && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: hint }), rendered, isBusy && lastTurn?.role === "user" && ((0, jsx_runtime_1.jsxs)("div", { className: "rounded-md border border-border bg-accent/5 p-2.5 text-sm", children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-1 text-[11px] font-semibold tracking-wide text-muted uppercase", children: assistantName }), (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] })), error && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-danger", children: error }), (0, jsx_runtime_1.jsx)("div", { ref: bottomRef })] }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex gap-2 border-t border-border p-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", className: "flex-1", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { className: "sr-only", children: placeholder }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: placeholder, value: input, onChange: (event) => setInput(event.currentTarget.value), disabled: isBusy })] }), isBusy ? ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", isIconOnly: true, variant: "secondary", "aria-label": "Stop generating", onPress: stop, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Square, { className: "size-3.5" }) })) : ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", isIconOnly: true, variant: "primary", "aria-label": "Send", isDisabled: !input.trim(), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Send, { className: "size-3.5" }) }))] })] }) })] }));
|
|
332
|
+
}
|
|
333
|
+
/** Hoisted for the same reference-equality reason as `ANIMATED` - Streamdown compares it by identity. */
|
|
334
|
+
const LINK_SAFETY = { enabled: false };
|
|
@@ -42,7 +42,7 @@ function useConnectFeature(feature) {
|
|
|
42
42
|
*
|
|
43
43
|
* Like every gate in this package, it is a **UX gate, not a security
|
|
44
44
|
* boundary**: the entitlement is read through your own origin and the browser
|
|
45
|
-
* can ignore it. Check `client.entitlements.hasFeature(
|
|
45
|
+
* can ignore it. Check `client.entitlements.hasFeature(workspaceId, feature)` on the
|
|
46
46
|
* server before doing the thing the feature unlocks.
|
|
47
47
|
*
|
|
48
48
|
* Renders `loading` - not `fallback` - while the answer is unknown, so a paying
|
|
@@ -57,5 +57,5 @@ function ConnectFeatureGate({ feature, children, fallback, loading = null, plans
|
|
|
57
57
|
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children });
|
|
58
58
|
if (fallback !== undefined)
|
|
59
59
|
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: fallback });
|
|
60
|
-
return ((0, jsx_runtime_1.jsx)(subscription_js_1.ConnectNoSubscriptionCard, { plansHref: plansHref, title: title ?? "Not included in your plan", description: description ?? "This part of the
|
|
60
|
+
return ((0, jsx_runtime_1.jsx)(subscription_js_1.ConnectNoSubscriptionCard, { plansHref: plansHref, title: title ?? "Not included in your plan", description: description ?? "This part of the application is on a higher plan. Compare plans to see what unlocks it.", className: className }));
|
|
61
61
|
}
|
|
@@ -45,7 +45,7 @@ function ToolkitTile({ toolkit, isPending, onConnect, onDisconnect }) {
|
|
|
45
45
|
(0, jsx_runtime_1.jsx)("img", { src: toolkit.logoUrl, alt: "", className: "size-full object-cover" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Puzzle, { className: "size-6 text-muted", "aria-hidden": "true" })), toolkit.status === "failed" && ((0, jsx_runtime_1.jsx)("span", { className: "absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full bg-danger text-danger-foreground", children: (0, jsx_runtime_1.jsx)(lucide_react_1.AlertCircle, { className: "size-3", "aria-hidden": "true" }) }))] }), (0, jsx_runtime_1.jsx)("span", { className: "line-clamp-2 text-center text-xs font-medium text-foreground", children: toolkit.name }), (0, jsx_runtime_1.jsx)(react_2.Chip, { size: "sm", variant: "soft", color: STATUS_COLOR[toolkit.status], children: STATUS_LABEL[toolkit.status] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex w-full flex-col gap-1.5 pt-1", children: [CAN_CONNECT.has(toolkit.status) && ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "secondary", className: "w-full text-xs", isPending: isPending, isDisabled: isPending, onPress: onConnect, children: "Connect" })), toolkit.status === "active" && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "secondary", className: "w-full text-xs", isPending: isPending, isDisabled: isPending, onPress: onConnect, children: "Reconnect" }), (0, jsx_runtime_1.jsxs)(react_2.AlertDialog, { isOpen: confirmingDisconnect, onOpenChange: setConfirmingDisconnect, children: [(0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "tertiary", className: "w-full text-xs text-danger", isDisabled: isPending, children: "Disconnect" }), (0, jsx_runtime_1.jsx)(react_2.AlertDialog.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_2.AlertDialog.Container, { children: (0, jsx_runtime_1.jsxs)(react_2.AlertDialog.Dialog, { className: "sm:max-w-100", children: [(0, jsx_runtime_1.jsx)(react_2.AlertDialog.CloseTrigger, {}), (0, jsx_runtime_1.jsxs)(react_2.AlertDialog.Header, { children: [(0, jsx_runtime_1.jsx)(react_2.AlertDialog.Icon, { status: "danger" }), (0, jsx_runtime_1.jsxs)(react_2.AlertDialog.Heading, { children: ["Disconnect ", toolkit.name, "?"] })] }), (0, jsx_runtime_1.jsx)(react_2.AlertDialog.Body, { children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "Every member loses this connection immediately - it can be reconnected later." }) }), (0, jsx_runtime_1.jsxs)(react_2.AlertDialog.Footer, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { slot: "close", variant: "secondary", className: "text-xs", children: "Cancel" }), (0, jsx_runtime_1.jsx)(react_2.Button, { slot: "close", variant: "danger", className: "text-xs", onPress: onDisconnect, children: "Disconnect" })] })] }) }) })] })] })), toolkit.status === "pending" && ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "tertiary", className: "w-full text-xs text-danger", isDisabled: isPending, onPress: onDisconnect, children: "Cancel" }))] })] }));
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* A grid of every Composio-hosted toolkit this
|
|
48
|
+
* A grid of every Composio-hosted toolkit this workspace can connect -
|
|
49
49
|
* Google Calendar, Slack, Twitter/X, whatever a platform admin enabled in
|
|
50
50
|
* Connect's own Console. Each tile carries its own connect/reconnect/
|
|
51
51
|
* disconnect action and its own pending state, independent of every other
|
|
@@ -5,10 +5,10 @@ exports.ConnectLogo = ConnectLogo;
|
|
|
5
5
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
6
|
const mark_js_1 = require("../mark.js");
|
|
7
7
|
/**
|
|
8
|
-
* The shared platform lockup:
|
|
9
|
-
* `cosmo·ops` beside it, and the
|
|
8
|
+
* The shared platform lockup: an application's own glyph on the CosmoOps badge,
|
|
9
|
+
* `cosmo·ops` beside it, and the application name underneath.
|
|
10
10
|
*
|
|
11
|
-
* This is what makes a family of
|
|
11
|
+
* This is what makes a family of application platforms look like one product. Each
|
|
12
12
|
* platform passes only what differs - its `name` and its `icon` - and inherits
|
|
13
13
|
* the mark geometry, the wordmark treatment, the separator, and the accent from
|
|
14
14
|
* one place. Change the lockup here and every platform follows.
|
|
@@ -89,7 +89,7 @@ function isNavItemActive(item, activeHref) {
|
|
|
89
89
|
return false;
|
|
90
90
|
return normalizePath(item.href) === activeHref;
|
|
91
91
|
}
|
|
92
|
-
/** Hrefs declared `gateExempt` - the routes `
|
|
92
|
+
/** Hrefs declared `gateExempt` - the routes `AppShell` renders outside its `ConnectAccessGate`. */
|
|
93
93
|
function gateExemptPaths(nav) {
|
|
94
94
|
return flattenNav(nav)
|
|
95
95
|
.filter((item) => item.gateExempt)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* an upgrade while the card behind it said downgrade would be worse than
|
|
6
6
|
* either label being wrong on its own.
|
|
7
7
|
*
|
|
8
|
-
* Mirrors `packages/data/src/
|
|
8
|
+
* Mirrors `packages/data/src/applications/catalog.ts` and
|
|
9
9
|
* `packages/data/src/payments/plan-change.ts`, which are the copies Connect's
|
|
10
10
|
* own UI uses and which decide which use-case actually runs. The duplication
|
|
11
11
|
* is deliberate and load-bearing: this package carries no dependency on
|
|
@@ -47,7 +47,7 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
|
|
|
47
47
|
/** The plan under review in the confirmation dialog, or `null` when it is closed. */
|
|
48
48
|
const [pending, setPending] = (0, react_2.useState)(null);
|
|
49
49
|
const subscription = entitlements?.subscription ?? null;
|
|
50
|
-
const
|
|
50
|
+
const applicationId = subscription?.applicationId ?? null;
|
|
51
51
|
const allPlans = (0, react_2.useMemo)(() => {
|
|
52
52
|
const plans = catalog?.plans ?? [];
|
|
53
53
|
if (!planIds)
|
|
@@ -85,12 +85,12 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
|
|
|
85
85
|
* then would misreport a completed checkout as an abandoned one.
|
|
86
86
|
*/
|
|
87
87
|
function startCheckout(plan) {
|
|
88
|
-
if (!
|
|
88
|
+
if (!applicationId)
|
|
89
89
|
return;
|
|
90
90
|
checkout.start({
|
|
91
91
|
url: (0, use_checkout_js_1.buildCheckoutUrl)({
|
|
92
92
|
appUrl,
|
|
93
|
-
|
|
93
|
+
applicationId,
|
|
94
94
|
planId: plan.id,
|
|
95
95
|
subscriptionId: subscription?.subscription?.id ?? null,
|
|
96
96
|
coupon,
|
|
@@ -119,7 +119,7 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
|
|
|
119
119
|
return null;
|
|
120
120
|
const action = (0, plan_pricing_js_1.resolveAction)(plan, subscription);
|
|
121
121
|
// "Current" belongs to the tier, not just the exact price point: an
|
|
122
|
-
//
|
|
122
|
+
// workspace on Growth annual looking at monthly pricing is still on the
|
|
123
123
|
// Growth tier, and marking only the row it literally subscribes to
|
|
124
124
|
// would leave its own card unmarked half the time.
|
|
125
125
|
const isCurrentTier = subscription?.plan != null &&
|
|
@@ -137,8 +137,8 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
|
|
|
137
137
|
? "primary"
|
|
138
138
|
: "secondary",
|
|
139
139
|
// Disabled while a hand-off is in flight, not just on the
|
|
140
|
-
// card that started it: the
|
|
141
|
-
//
|
|
140
|
+
// card that started it: the workspace has one subscription per
|
|
141
|
+
// application, so a second checkout opened against a different
|
|
142
142
|
// plan would race the first and one of them would be
|
|
143
143
|
// rejected on arrival.
|
|
144
144
|
isDisabled: isCurrent || checkout.status === "waiting", onPress: () => setPending({ plan, action }), children: [plan_pricing_js_1.ACTION_LABEL[action], !isCurrent && (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }) })] }) }, tier.planGroup));
|
|
@@ -33,7 +33,7 @@ function actionLabel(blocker) {
|
|
|
33
33
|
return ACTION_LABELS[blocker.code] ?? "Resolve on Connect";
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* Every gate between this workspace and your
|
|
36
|
+
* Every gate between this workspace and your application, with the way out of each.
|
|
37
37
|
*
|
|
38
38
|
* This is the component that keeps a blocked customer from staring at a broken
|
|
39
39
|
* app. `getReadiness` resolves subscription state, KYC/KYB, a chargeable
|
|
@@ -41,7 +41,7 @@ function actionLabel(blocker) {
|
|
|
41
41
|
* `resolveUrl` into the exact Connect screen that fixes it** - so being blocked
|
|
42
42
|
* comes with somewhere to go rather than a refusal.
|
|
43
43
|
*
|
|
44
|
-
* Render it high on any page your
|
|
44
|
+
* Render it high on any page your application gates. It draws nothing at all when
|
|
45
45
|
* the workspace is ready, so it is safe to leave mounted permanently.
|
|
46
46
|
*
|
|
47
47
|
* Two deliberate refusals:
|
|
@@ -76,7 +76,7 @@ function BlockerRow({ blocker }) {
|
|
|
76
76
|
return ((0, jsx_runtime_1.jsxs)("div", { className: `flex flex-wrap items-center justify-between gap-3 rounded-lg px-3 py-2 text-sm ${inProgress || !isBlocking ? "bg-surface text-muted" : "bg-danger-soft text-danger"}`, children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [inProgress || !isBlocking ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Info, { className: "size-4 shrink-0", "aria-hidden": "true" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.AlertTriangle, { className: "size-4 shrink-0", "aria-hidden": "true" })), blocker.message] }), blocker.resolveUrl && !inProgress && ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(blocker.resolveUrl, "_blank", "noopener,noreferrer"), children: [actionLabel(blocker), (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }))] }));
|
|
77
77
|
}
|
|
78
78
|
function ReadyRow({ className }) {
|
|
79
|
-
return ((0, jsx_runtime_1.jsx)("p", { className: `text-sm text-muted ${className ?? ""}`, children: "This workspace is cleared to use every part of the
|
|
79
|
+
return ((0, jsx_runtime_1.jsx)("p", { className: `text-sm text-muted ${className ?? ""}`, children: "This workspace is cleared to use every part of the application." }));
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
82
|
* Renders `children` only once the workspace clears every blocking gate.
|
|
@@ -39,7 +39,7 @@ function NavRow({ item, isActive, hasActiveChild, collapsed, depth, renderLink,
|
|
|
39
39
|
* The sidebar's nav column on its own - sections, items, one level of
|
|
40
40
|
* children, and the active highlight.
|
|
41
41
|
*
|
|
42
|
-
* `
|
|
42
|
+
* `AppShell` renders this inside its rail and its mobile drawer, and it
|
|
43
43
|
* is exported for a host that wants the shell's navigation without its
|
|
44
44
|
* chrome: the same items, the same active resolution, dropped into a layout
|
|
45
45
|
* you already have.
|
|
@@ -37,7 +37,7 @@ const HEADING = {
|
|
|
37
37
|
* The authoritative price is computed by Connect against the real cart:
|
|
38
38
|
* proration is priced against the actual cycle, a coupon is validated
|
|
39
39
|
* against the plan it is being applied to, trial eligibility depends on
|
|
40
|
-
* whether this
|
|
40
|
+
* whether this workspace has ever subscribed to this application before, and
|
|
41
41
|
* an accumulated credit is deducted. None of that is knowable from the
|
|
42
42
|
* partner's origin, so this component states the list price and says where
|
|
43
43
|
* the final figure comes from rather than quietly guessing at one.
|
|
@@ -50,7 +50,7 @@ function ConnectSubscribeDialog({ plan, action, current, coupon, onClose, onConf
|
|
|
50
50
|
// member and the one most in need of it: the two prices differ by an order
|
|
51
51
|
// of magnitude, and showing only the new one reads as a price rise.
|
|
52
52
|
const isChange = action === "upgrade" || action === "downgrade" || isIntervalSwitch;
|
|
53
|
-
return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { isOpen: plan !== null, onOpenChange: (open) => !open && onClose(), children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "sm", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, { onPress: onClose }), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: HEADING[action] }) }), plan && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-4", children: [isChange && currentPlan ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-muted", children: currentPlan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(currentPlan.price), "/", currentPlan.billingInterval === "annual" ? "yr" : "mo"] })] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate font-medium text-foreground", children: plan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(plan.price), "/", plan.billingInterval === "annual" ? "yr" : "mo"] })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-medium text-foreground", children: plan.name }), plan.description && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: plan.description })] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1 border-t border-separator pt-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsx)("span", { children: plan.name }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["GST (", plan.gstRate, "%)"] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate) - plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between border-t border-separator pt-1 font-semibold text-foreground", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["Per ", cadenceOf(plan)] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate)) })] }), plan.billingInterval === "annual" && ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Works out to ", (0, plan_pricing_js_1.formatPrice)((0, plan_pricing_js_1.monthlyOf)(withGst(plan.price, plan.gstRate), "annual")), " a month, billed yearly."] }))] }), plan.trialDays ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-accent", children: [plan.trialDays, "-day free trial, if this workspace has not subscribed to this
|
|
53
|
+
return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { isOpen: plan !== null, onOpenChange: (open) => !open && onClose(), children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "sm", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, { onPress: onClose }), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: HEADING[action] }) }), plan && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-4", children: [isChange && currentPlan ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-muted", children: currentPlan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(currentPlan.price), "/", currentPlan.billingInterval === "annual" ? "yr" : "mo"] })] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate font-medium text-foreground", children: plan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(plan.price), "/", plan.billingInterval === "annual" ? "yr" : "mo"] })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-medium text-foreground", children: plan.name }), plan.description && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: plan.description })] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1 border-t border-separator pt-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsx)("span", { children: plan.name }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["GST (", plan.gstRate, "%)"] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate) - plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between border-t border-separator pt-1 font-semibold text-foreground", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["Per ", cadenceOf(plan)] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate)) })] }), plan.billingInterval === "annual" && ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Works out to ", (0, plan_pricing_js_1.formatPrice)((0, plan_pricing_js_1.monthlyOf)(withGst(plan.price, plan.gstRate), "annual")), " a month, billed yearly."] }))] }), plan.trialDays ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-accent", children: [plan.trialDays, "-day free trial, if this workspace has not subscribed to this application before."] })) : null, coupon?.trim() ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Coupon ", (0, jsx_runtime_1.jsx)("span", { className: "font-mono text-foreground", children: coupon.trim() }), " is carried over and validated on Connect."] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2 rounded-lg bg-surface p-3 text-xs text-muted", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Info, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1", children: [action === "subscribe" && ((0, jsx_runtime_1.jsx)("p", { children: "Payment is completed on Connect, where your card or UPI mandate is set up and the tax invoice is issued. This page updates by itself once it is done." })), action === "upgrade" && ((0, jsx_runtime_1.jsxs)("p", { children: ["Connect prices the difference for the days left in your current cycle and collects just that amount now. From the next cycle you are billed the ", plan.name, " rate."] })), action === "downgrade" && ((0, jsx_runtime_1.jsx)("p", { children: "You keep what you have paid for. Connect works out the unused part of this cycle and applies it as credit or a refund, depending on how this application is configured." })), isIntervalSwitch && (
|
|
54
54
|
// Not a proration. The cycle you are on ends, so this is
|
|
55
55
|
// the one action where the amount collected can exceed a
|
|
56
56
|
// single cycle's list price - or be nothing at all, with
|
|
@@ -63,7 +63,7 @@ function useSubscriptionState() {
|
|
|
63
63
|
* Rendered on its own it is just a card; `ConnectSubscriptionGate` is the usual
|
|
64
64
|
* way in, since it picks this only when there is genuinely nothing to show.
|
|
65
65
|
*/
|
|
66
|
-
function ConnectNoSubscriptionCard({ plansHref = DEFAULT_PLANS_HREF, title = "You're not on a plan yet", description = "Pick a plan to unlock the full
|
|
66
|
+
function ConnectNoSubscriptionCard({ plansHref = DEFAULT_PLANS_HREF, title = "You're not on a plan yet", description = "Pick a plan to unlock the full application. You can change or cancel it whenever you like.", className, }) {
|
|
67
67
|
return ((0, jsx_runtime_1.jsxs)(react_1.Card, { className: className, children: [(0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: title }), (0, jsx_runtime_1.jsx)(react_1.Card.Description, { children: description })] }), (0, jsx_runtime_1.jsx)(react_1.Card.Footer, { children: (0, jsx_runtime_1.jsxs)(link_button_js_1.ConnectLinkButton, { href: plansHref, variant: "primary", children: ["See plans", (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { className: "size-3.5" })] }) })] }));
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
@@ -19,7 +19,7 @@ function formatDate(iso) {
|
|
|
19
19
|
*
|
|
20
20
|
* Reads the balance Connect actually debits, so the number here and the number
|
|
21
21
|
* `consumeAddon` enforces cannot disagree. Before the `addons:read` endpoint
|
|
22
|
-
* existed this component was impossible to write honestly:
|
|
22
|
+
* existed this component was impossible to write honestly: an application could
|
|
23
23
|
* spend an allowance down on every request and the only thing the API exposed
|
|
24
24
|
* was the list of add-on *ids* on the subscription.
|
|
25
25
|
*
|