@agent-native/core 0.73.0 → 0.75.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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +67 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +51 -5
- package/corpus/core/src/agent/production-agent.ts +92 -12
- package/corpus/core/src/client/ConnectBuilderCard.tsx +7 -7
- package/corpus/core/src/client/NewWorkspaceAppFlow.tsx +1 -1
- package/corpus/core/src/client/blocks/library/AnnotatedCodeBlock.tsx +33 -18
- package/corpus/core/src/client/blocks/library/annotation-rail.tsx +3 -4
- package/corpus/core/src/client/components/CodeRequiredDialog.tsx +5 -8
- package/corpus/core/src/deploy/build.ts +21 -9
- package/corpus/core/src/deploy/workspace-deploy.ts +12 -6
- package/corpus/core/src/server/agent-chat-plugin.ts +7 -7
- package/corpus/core/src/server/self-dispatch.ts +8 -1
- package/corpus/templates/analytics/actions/compose-dashboard.ts +34 -0
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +51 -3
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +31 -8
- package/corpus/templates/analytics/changelog/2026-06-24-dashboard-filters-now-apply-consistently-to-first-party-traf.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +97 -28
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +57 -10
- package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -0
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +786 -323
- package/corpus/templates/clips/changelog/2026-06-24-settings-now-puts-what-s-new-in-a-compact-sidebar-prioritize.md +6 -0
- package/corpus/templates/plan/changelog/2026-06-24-annotated-code-callouts-now-use-numbered-gutter-markers-that.md +6 -0
- package/dist/agent/durable-background.d.ts +21 -0
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +48 -5
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +88 -13
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +6 -6
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/NewWorkspaceAppFlow.js +1 -1
- package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js +14 -10
- package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
- package/dist/client/blocks/library/annotation-rail.d.ts +3 -4
- package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
- package/dist/client/blocks/library/annotation-rail.js +3 -4
- package/dist/client/blocks/library/annotation-rail.js.map +1 -1
- package/dist/client/components/CodeRequiredDialog.d.ts.map +1 -1
- package/dist/client/components/CodeRequiredDialog.js +2 -1
- package/dist/client/components/CodeRequiredDialog.js.map +1 -1
- package/dist/deploy/build.d.ts +13 -2
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +21 -9
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.js +12 -6
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/server/agent-chat-plugin.js +7 -7
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/self-dispatch.d.ts.map +1 -1
- package/dist/server/self-dispatch.js +5 -1
- package/dist/server/self-dispatch.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
IconBrain,
|
|
4
4
|
IconBrandSlack,
|
|
5
|
+
IconCheck,
|
|
6
|
+
IconChevronDown,
|
|
5
7
|
IconCloud,
|
|
6
8
|
IconExternalLink,
|
|
7
9
|
IconKey,
|
|
@@ -14,12 +16,16 @@ import {
|
|
|
14
16
|
useSession,
|
|
15
17
|
agentNativePath,
|
|
16
18
|
appApiPath,
|
|
17
|
-
|
|
19
|
+
parseChangelog,
|
|
18
20
|
useActionQuery,
|
|
21
|
+
useBuilderConnectFlow,
|
|
22
|
+
useBuilderStatus,
|
|
19
23
|
ChangelogSettingsCard,
|
|
20
24
|
} from "@agent-native/core/client";
|
|
21
25
|
import changelog from "../../CHANGELOG.md?raw";
|
|
22
26
|
import { toast } from "sonner";
|
|
27
|
+
import { cn } from "@/lib/utils";
|
|
28
|
+
import { Badge } from "@/components/ui/badge";
|
|
23
29
|
import { Button } from "@/components/ui/button";
|
|
24
30
|
import {
|
|
25
31
|
Card,
|
|
@@ -28,6 +34,11 @@ import {
|
|
|
28
34
|
CardHeader,
|
|
29
35
|
CardTitle,
|
|
30
36
|
} from "@/components/ui/card";
|
|
37
|
+
import {
|
|
38
|
+
Collapsible,
|
|
39
|
+
CollapsibleContent,
|
|
40
|
+
CollapsibleTrigger,
|
|
41
|
+
} from "@/components/ui/collapsible";
|
|
31
42
|
import { Input } from "@/components/ui/input";
|
|
32
43
|
import { Label } from "@/components/ui/label";
|
|
33
44
|
import { PageHeader } from "@/components/library/page-header";
|
|
@@ -95,6 +106,39 @@ const S3_STORAGE_FIELDS = [
|
|
|
95
106
|
},
|
|
96
107
|
] as const;
|
|
97
108
|
|
|
109
|
+
const AI_PROVIDER_FIELDS = [
|
|
110
|
+
{
|
|
111
|
+
key: "ANTHROPIC_API_KEY",
|
|
112
|
+
label: "Anthropic",
|
|
113
|
+
placeholder: "sk-ant-...",
|
|
114
|
+
storage: "agent-engine",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
key: "OPENAI_API_KEY",
|
|
118
|
+
label: "OpenAI",
|
|
119
|
+
placeholder: "sk-...",
|
|
120
|
+
storage: "agent-engine",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
key: "GEMINI_API_KEY",
|
|
124
|
+
label: "Gemini",
|
|
125
|
+
placeholder: "AI...",
|
|
126
|
+
storage: "secret",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
key: "GROQ_API_KEY",
|
|
130
|
+
label: "Groq",
|
|
131
|
+
placeholder: "gsk_...",
|
|
132
|
+
storage: "secret",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
key: "OPENROUTER_API_KEY",
|
|
136
|
+
label: "OpenRouter",
|
|
137
|
+
placeholder: "sk-or-...",
|
|
138
|
+
storage: "agent-engine",
|
|
139
|
+
},
|
|
140
|
+
] as const;
|
|
141
|
+
|
|
98
142
|
interface ClipsUserSettings {
|
|
99
143
|
defaultPlaybackSpeed?: string;
|
|
100
144
|
emailNotifications?: boolean;
|
|
@@ -253,10 +297,105 @@ async function saveS3StorageSettings(
|
|
|
253
297
|
}
|
|
254
298
|
}
|
|
255
299
|
|
|
300
|
+
async function saveAgentEngineApiKey(
|
|
301
|
+
key: string,
|
|
302
|
+
value: string,
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
const res = await fetch(
|
|
305
|
+
agentNativePath("/_agent-native/agent-engine/api-key"),
|
|
306
|
+
{
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: { "Content-Type": "application/json" },
|
|
309
|
+
body: JSON.stringify({ key, value, scope: "user" }),
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (!res.ok) {
|
|
314
|
+
const body = (await res.json().catch(() => null)) as {
|
|
315
|
+
error?: string;
|
|
316
|
+
} | null;
|
|
317
|
+
throw new Error(body?.error ?? `Save failed (${res.status})`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function saveRegisteredSecret(key: string, value: string): Promise<void> {
|
|
322
|
+
const res = await fetch(
|
|
323
|
+
agentNativePath(`/_agent-native/secrets/${encodeURIComponent(key)}`),
|
|
324
|
+
{
|
|
325
|
+
method: "POST",
|
|
326
|
+
headers: { "Content-Type": "application/json" },
|
|
327
|
+
body: JSON.stringify({ value }),
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (!res.ok) {
|
|
332
|
+
const body = (await res.json().catch(() => null)) as {
|
|
333
|
+
error?: string;
|
|
334
|
+
} | null;
|
|
335
|
+
throw new Error(body?.error ?? `Save failed (${res.status})`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function CompactChangelogAside() {
|
|
340
|
+
const entries = useMemo(() => parseChangelog(changelog), []);
|
|
341
|
+
const [expanded, setExpanded] = useState(false);
|
|
342
|
+
|
|
343
|
+
if (entries.length === 0) return null;
|
|
344
|
+
|
|
345
|
+
const latestBody = entries[0]?.body ?? "";
|
|
346
|
+
const canExpand = latestBody.length > 260 || entries.length > 1;
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<aside className="2xl:sticky 2xl:top-6 2xl:self-start">
|
|
350
|
+
<div className="relative">
|
|
351
|
+
<div
|
|
352
|
+
className={cn(
|
|
353
|
+
"overflow-hidden transition-[max-height] duration-200 ease-out",
|
|
354
|
+
expanded ? "max-h-[70vh]" : "max-h-72",
|
|
355
|
+
)}
|
|
356
|
+
>
|
|
357
|
+
<ChangelogSettingsCard
|
|
358
|
+
markdown={changelog}
|
|
359
|
+
limit={1}
|
|
360
|
+
className="rounded-md"
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
{canExpand && !expanded ? (
|
|
364
|
+
<div className="pointer-events-none absolute inset-x-px bottom-10 h-16 bg-gradient-to-t from-card to-transparent" />
|
|
365
|
+
) : null}
|
|
366
|
+
{canExpand ? (
|
|
367
|
+
<div className="mt-2 flex justify-end">
|
|
368
|
+
<Button
|
|
369
|
+
type="button"
|
|
370
|
+
variant="ghost"
|
|
371
|
+
size="sm"
|
|
372
|
+
className="h-7 px-2 text-xs text-muted-foreground"
|
|
373
|
+
onClick={() => setExpanded((value) => !value)}
|
|
374
|
+
>
|
|
375
|
+
{expanded ? "Collapse" : "Expand"}
|
|
376
|
+
</Button>
|
|
377
|
+
</div>
|
|
378
|
+
) : null}
|
|
379
|
+
</div>
|
|
380
|
+
</aside>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
256
384
|
export default function SettingsIndexRoute() {
|
|
257
385
|
const { session } = useSession();
|
|
258
386
|
const email = session?.email ?? "";
|
|
259
387
|
const storageStatus = useVideoStorageStatus();
|
|
388
|
+
const builderStatus = useBuilderStatus();
|
|
389
|
+
const builderConnect = useBuilderConnectFlow({
|
|
390
|
+
popupUrl:
|
|
391
|
+
builderStatus.status?.cliAuthUrl ?? builderStatus.status?.connectUrl,
|
|
392
|
+
trackingSource: "clips_settings",
|
|
393
|
+
trackingFlow: "clips_setup",
|
|
394
|
+
onConnected: async () => {
|
|
395
|
+
await Promise.all([storageStatus.refetch(), builderStatus.refetch()]);
|
|
396
|
+
toast.success("Builder.io connected");
|
|
397
|
+
},
|
|
398
|
+
});
|
|
260
399
|
const slackStatus = useActionQuery<SlackInstallationsResponse>(
|
|
261
400
|
"list-slack-installations",
|
|
262
401
|
undefined,
|
|
@@ -275,6 +414,45 @@ export default function SettingsIndexRoute() {
|
|
|
275
414
|
const [transcriptCleanupEnabled, setTranscriptCleanupEnabled] =
|
|
276
415
|
useState(true);
|
|
277
416
|
const [s3Values, setS3Values] = useState<Record<string, string>>({});
|
|
417
|
+
const [s3Expanded, setS3Expanded] = useState(false);
|
|
418
|
+
const [apiKeysExpanded, setApiKeysExpanded] = useState(false);
|
|
419
|
+
const [apiKeyValues, setApiKeyValues] = useState<Record<string, string>>({});
|
|
420
|
+
const [apiKeyStatus, setApiKeyStatus] = useState<Record<string, boolean>>({});
|
|
421
|
+
const [apiKeyStatusLoading, setApiKeyStatusLoading] = useState(true);
|
|
422
|
+
const [savingApiKey, setSavingApiKey] = useState<string | null>(null);
|
|
423
|
+
|
|
424
|
+
const refreshApiKeyStatus = useCallback(async () => {
|
|
425
|
+
setApiKeyStatusLoading(true);
|
|
426
|
+
try {
|
|
427
|
+
const [envRes, secretsRes] = await Promise.all([
|
|
428
|
+
fetch(agentNativePath("/_agent-native/env-status")),
|
|
429
|
+
fetch(agentNativePath("/_agent-native/secrets")),
|
|
430
|
+
]);
|
|
431
|
+
const envData = envRes.ok
|
|
432
|
+
? ((await envRes.json()) as Array<{
|
|
433
|
+
key: string;
|
|
434
|
+
configured?: boolean;
|
|
435
|
+
}>)
|
|
436
|
+
: [];
|
|
437
|
+
const secretsData = secretsRes.ok
|
|
438
|
+
? ((await secretsRes.json()) as Array<{
|
|
439
|
+
key: string;
|
|
440
|
+
status?: string;
|
|
441
|
+
}>)
|
|
442
|
+
: [];
|
|
443
|
+
const next = Object.fromEntries(
|
|
444
|
+
envData.map((entry) => [entry.key, Boolean(entry.configured)]),
|
|
445
|
+
);
|
|
446
|
+
for (const entry of secretsData) {
|
|
447
|
+
next[entry.key] = entry.status === "set";
|
|
448
|
+
}
|
|
449
|
+
setApiKeyStatus(next);
|
|
450
|
+
} catch {
|
|
451
|
+
setApiKeyStatus({});
|
|
452
|
+
} finally {
|
|
453
|
+
setApiKeyStatusLoading(false);
|
|
454
|
+
}
|
|
455
|
+
}, []);
|
|
278
456
|
|
|
279
457
|
useEffect(() => {
|
|
280
458
|
let cancelled = false;
|
|
@@ -291,6 +469,10 @@ export default function SettingsIndexRoute() {
|
|
|
291
469
|
};
|
|
292
470
|
}, []);
|
|
293
471
|
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
void refreshApiKeyStatus();
|
|
474
|
+
}, [refreshApiKeyStatus]);
|
|
475
|
+
|
|
294
476
|
async function handleSave() {
|
|
295
477
|
setSaving(true);
|
|
296
478
|
try {
|
|
@@ -339,6 +521,33 @@ export default function SettingsIndexRoute() {
|
|
|
339
521
|
}
|
|
340
522
|
}
|
|
341
523
|
|
|
524
|
+
async function handleSaveApiKey(key: string) {
|
|
525
|
+
const value = (apiKeyValues[key] ?? "").trim();
|
|
526
|
+
if (!value) {
|
|
527
|
+
toast.error("Paste a provider key first.");
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
setSavingApiKey(key);
|
|
532
|
+
try {
|
|
533
|
+
const field = AI_PROVIDER_FIELDS.find((item) => item.key === key);
|
|
534
|
+
if (field?.storage === "secret") {
|
|
535
|
+
await saveRegisteredSecret(key, value);
|
|
536
|
+
} else {
|
|
537
|
+
await saveAgentEngineApiKey(key, value);
|
|
538
|
+
}
|
|
539
|
+
setApiKeyValues((current) => ({ ...current, [key]: "" }));
|
|
540
|
+
setApiKeyStatus((current) => ({ ...current, [key]: true }));
|
|
541
|
+
window.dispatchEvent(new CustomEvent("agent-engine:configured-changed"));
|
|
542
|
+
await refreshApiKeyStatus();
|
|
543
|
+
toast.success("API key saved");
|
|
544
|
+
} catch (err) {
|
|
545
|
+
toast.error(err instanceof Error ? err.message : "Failed to save key");
|
|
546
|
+
} finally {
|
|
547
|
+
setSavingApiKey(null);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
342
551
|
async function handleConnectSlack() {
|
|
343
552
|
const beforeCount = slackInstallations.length;
|
|
344
553
|
setConnectingSlack(true);
|
|
@@ -380,6 +589,24 @@ export default function SettingsIndexRoute() {
|
|
|
380
589
|
|
|
381
590
|
const storageConfigured = !!storageStatus.data?.configured;
|
|
382
591
|
const activeProviderName = storageStatus.data?.activeProvider?.name ?? null;
|
|
592
|
+
const activeProviderId = storageStatus.data?.activeProvider?.id ?? null;
|
|
593
|
+
const builderConnected = Boolean(
|
|
594
|
+
builderConnect.configured ||
|
|
595
|
+
builderStatus.status?.configured ||
|
|
596
|
+
storageStatus.data?.builderConfigured ||
|
|
597
|
+
activeProviderId === "builder",
|
|
598
|
+
);
|
|
599
|
+
const builderOrgName =
|
|
600
|
+
builderConnect.orgName ?? builderStatus.status?.orgName ?? null;
|
|
601
|
+
const builderStatusLoading =
|
|
602
|
+
storageStatus.isLoading ||
|
|
603
|
+
builderStatus.loading ||
|
|
604
|
+
!builderConnect.hasFetchedStatus;
|
|
605
|
+
const s3Configured = activeProviderId === "s3";
|
|
606
|
+
const s3Collapsed = builderConnected && !s3Expanded;
|
|
607
|
+
const configuredApiKeyCount = AI_PROVIDER_FIELDS.filter(
|
|
608
|
+
(field) => apiKeyStatus[field.key],
|
|
609
|
+
).length;
|
|
383
610
|
const slackInstallations = slackStatus.data?.installations ?? [];
|
|
384
611
|
const slackOauthConfigured = slackStatus.data?.oauthConfigured ?? false;
|
|
385
612
|
const slackSigningConfigured = slackStatus.data?.signingConfigured ?? false;
|
|
@@ -392,346 +619,582 @@ export default function SettingsIndexRoute() {
|
|
|
392
619
|
Settings
|
|
393
620
|
</h1>
|
|
394
621
|
</PageHeader>
|
|
395
|
-
<div className="
|
|
396
|
-
<
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
:
|
|
417
|
-
|
|
418
|
-
|
|
622
|
+
<div className="mx-auto w-full max-w-6xl p-6">
|
|
623
|
+
<div className="grid gap-6 2xl:grid-cols-[minmax(0,1fr)_360px]">
|
|
624
|
+
<div className="min-w-0 space-y-6">
|
|
625
|
+
<p className="text-sm text-muted-foreground">
|
|
626
|
+
Preferences and connected services for this Clips workspace.
|
|
627
|
+
</p>
|
|
628
|
+
|
|
629
|
+
<Card id="video-storage" className="scroll-mt-16">
|
|
630
|
+
<CardHeader>
|
|
631
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
632
|
+
<IconCloud className="size-4 text-primary" />
|
|
633
|
+
Video storage
|
|
634
|
+
</CardTitle>
|
|
635
|
+
<CardDescription>
|
|
636
|
+
Builder.io is the primary storage path for Clips uploads. S3
|
|
637
|
+
is available when you need to bring your own bucket.
|
|
638
|
+
</CardDescription>
|
|
639
|
+
</CardHeader>
|
|
640
|
+
<CardContent className="space-y-4">
|
|
641
|
+
<div
|
|
642
|
+
className={cn(
|
|
643
|
+
"flex flex-col gap-3 rounded-md border px-3 py-3 sm:flex-row sm:items-center sm:justify-between",
|
|
644
|
+
builderConnected
|
|
645
|
+
? "border-primary/35 bg-primary/5"
|
|
646
|
+
: "border-border bg-accent/30",
|
|
647
|
+
)}
|
|
648
|
+
>
|
|
649
|
+
<div className="min-w-0">
|
|
650
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
651
|
+
{builderConnected ? (
|
|
652
|
+
<IconCheck className="h-4 w-4 text-primary" />
|
|
653
|
+
) : (
|
|
654
|
+
<IconKey className="h-4 w-4 text-muted-foreground" />
|
|
655
|
+
)}
|
|
656
|
+
{builderStatusLoading
|
|
657
|
+
? "Checking Builder.io"
|
|
658
|
+
: builderConnected
|
|
659
|
+
? "Builder.io connected"
|
|
660
|
+
: "Connect Builder.io"}
|
|
661
|
+
</div>
|
|
662
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
663
|
+
{builderConnected
|
|
664
|
+
? builderOrgName
|
|
665
|
+
? `Using Builder.io for ${builderOrgName}.`
|
|
666
|
+
: "New clips use the connected Builder.io provider."
|
|
667
|
+
: "Includes object storage, uploads, and managed transcription for new clips."}
|
|
668
|
+
</p>
|
|
669
|
+
</div>
|
|
670
|
+
{builderConnected ? (
|
|
671
|
+
<Badge variant="secondary" className="shrink-0">
|
|
672
|
+
Connected
|
|
673
|
+
</Badge>
|
|
674
|
+
) : (
|
|
675
|
+
<Button
|
|
676
|
+
type="button"
|
|
677
|
+
variant="outline"
|
|
678
|
+
size="sm"
|
|
679
|
+
className="shrink-0"
|
|
680
|
+
onClick={() =>
|
|
681
|
+
builderConnect.start({
|
|
682
|
+
trackingSource: "clips_settings_video_storage",
|
|
683
|
+
trackingFlow: "video_storage",
|
|
684
|
+
})
|
|
685
|
+
}
|
|
686
|
+
disabled={
|
|
687
|
+
builderConnect.connecting || builderStatusLoading
|
|
688
|
+
}
|
|
689
|
+
>
|
|
690
|
+
{builderConnect.connecting ? (
|
|
691
|
+
<IconLoader2 className="h-4 w-4 animate-spin" />
|
|
692
|
+
) : (
|
|
693
|
+
<IconExternalLink className="h-4 w-4" />
|
|
694
|
+
)}
|
|
695
|
+
Connect Builder.io
|
|
696
|
+
</Button>
|
|
697
|
+
)}
|
|
419
698
|
</div>
|
|
420
|
-
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
421
|
-
{storageConfigured
|
|
422
|
-
? "New clips will upload to the connected provider."
|
|
423
|
-
: "Save S3-compatible credentials or connect Builder.io from the recorder."}
|
|
424
|
-
</p>
|
|
425
|
-
</div>
|
|
426
|
-
<span className="shrink-0 rounded-full bg-background px-2 py-1 text-[10px] font-medium text-muted-foreground">
|
|
427
|
-
{storageConfigured ? "Connected" : "Pending"}
|
|
428
|
-
</span>
|
|
429
|
-
</div>
|
|
430
699
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
<
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
700
|
+
<Collapsible
|
|
701
|
+
open={builderConnected ? !s3Collapsed : true}
|
|
702
|
+
onOpenChange={(open) => setS3Expanded(open)}
|
|
703
|
+
>
|
|
704
|
+
<div className="rounded-md border border-border">
|
|
705
|
+
<div className="flex flex-col gap-3 px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
706
|
+
<div className="min-w-0">
|
|
707
|
+
<div className="flex flex-wrap items-center gap-2 text-sm font-medium">
|
|
708
|
+
<IconServer className="h-4 w-4 text-muted-foreground" />
|
|
709
|
+
S3-compatible storage
|
|
710
|
+
<Badge variant="outline" className="text-[10px]">
|
|
711
|
+
Secondary
|
|
712
|
+
</Badge>
|
|
713
|
+
{s3Configured ? (
|
|
714
|
+
<Badge variant="secondary" className="text-[10px]">
|
|
715
|
+
Active
|
|
716
|
+
</Badge>
|
|
717
|
+
) : null}
|
|
718
|
+
</div>
|
|
719
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
720
|
+
{builderConnected
|
|
721
|
+
? "Use this only if this workspace should upload to your own bucket instead of Builder.io."
|
|
722
|
+
: storageConfigured && activeProviderName
|
|
723
|
+
? `Currently using ${activeProviderName}.`
|
|
724
|
+
: "Use your own bucket if you do not want Builder.io storage."}
|
|
725
|
+
</p>
|
|
726
|
+
</div>
|
|
727
|
+
{builderConnected ? (
|
|
728
|
+
<CollapsibleTrigger asChild>
|
|
729
|
+
<Button
|
|
730
|
+
type="button"
|
|
731
|
+
variant="ghost"
|
|
732
|
+
size="sm"
|
|
733
|
+
className="shrink-0"
|
|
734
|
+
>
|
|
735
|
+
{s3Collapsed ? "Configure S3" : "Hide S3"}
|
|
736
|
+
<IconChevronDown
|
|
737
|
+
className={cn(
|
|
738
|
+
"h-4 w-4 transition-transform",
|
|
739
|
+
!s3Collapsed && "rotate-180",
|
|
740
|
+
)}
|
|
741
|
+
/>
|
|
742
|
+
</Button>
|
|
743
|
+
</CollapsibleTrigger>
|
|
744
|
+
) : null}
|
|
745
|
+
</div>
|
|
746
|
+
|
|
747
|
+
<CollapsibleContent>
|
|
748
|
+
<div className="space-y-4 border-t border-border px-3 py-4">
|
|
749
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
750
|
+
{S3_STORAGE_FIELDS.map((field) => (
|
|
751
|
+
<div key={field.key} className="space-y-1.5">
|
|
752
|
+
<Label htmlFor={field.key}>{field.label}</Label>
|
|
753
|
+
<Input
|
|
754
|
+
id={field.key}
|
|
755
|
+
type={
|
|
756
|
+
"secret" in field && field.secret
|
|
757
|
+
? "password"
|
|
758
|
+
: "text"
|
|
759
|
+
}
|
|
760
|
+
value={s3Values[field.key] ?? ""}
|
|
761
|
+
onChange={(event) =>
|
|
762
|
+
setS3Values((current) => ({
|
|
763
|
+
...current,
|
|
764
|
+
[field.key]: event.target.value,
|
|
765
|
+
}))
|
|
766
|
+
}
|
|
767
|
+
placeholder={field.placeholder}
|
|
768
|
+
autoComplete="off"
|
|
769
|
+
disabled={savingStorage}
|
|
770
|
+
/>
|
|
771
|
+
</div>
|
|
772
|
+
))}
|
|
773
|
+
</div>
|
|
774
|
+
|
|
775
|
+
<div className="flex justify-end">
|
|
776
|
+
<Button
|
|
777
|
+
onClick={handleSaveS3Storage}
|
|
778
|
+
disabled={savingStorage || storageStatus.isLoading}
|
|
779
|
+
>
|
|
780
|
+
{savingStorage && (
|
|
781
|
+
<IconLoader2 className="h-4 w-4 animate-spin" />
|
|
782
|
+
)}
|
|
783
|
+
Save storage
|
|
784
|
+
</Button>
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
787
|
+
</CollapsibleContent>
|
|
788
|
+
</div>
|
|
789
|
+
</Collapsible>
|
|
790
|
+
</CardContent>
|
|
791
|
+
</Card>
|
|
792
|
+
|
|
793
|
+
<Card id="slack" className="scroll-mt-16">
|
|
794
|
+
<CardHeader>
|
|
795
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
796
|
+
<IconBrandSlack className="size-4 text-primary" />
|
|
797
|
+
Agent-Native Clips for Slack
|
|
798
|
+
</CardTitle>
|
|
799
|
+
<CardDescription>
|
|
800
|
+
Share a public clip, paste the link in Slack, and it plays
|
|
801
|
+
inline — no extra steps for viewers. Connect each workspace
|
|
802
|
+
once.
|
|
803
|
+
</CardDescription>
|
|
804
|
+
</CardHeader>
|
|
805
|
+
<CardContent className="space-y-4">
|
|
806
|
+
<div className="flex flex-col gap-3 rounded-md border border-border bg-accent/30 px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
807
|
+
<div className="min-w-0">
|
|
808
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
809
|
+
<IconBrandSlack className="h-4 w-4 text-muted-foreground" />
|
|
810
|
+
{slackStatus.isLoading
|
|
811
|
+
? "Checking Slack"
|
|
812
|
+
: slackConnected
|
|
813
|
+
? `${slackInstallations.length} workspace${
|
|
814
|
+
slackInstallations.length === 1 ? "" : "s"
|
|
815
|
+
} connected`
|
|
816
|
+
: slackOauthConfigured
|
|
817
|
+
? "Not connected"
|
|
818
|
+
: "OAuth credentials needed"}
|
|
819
|
+
</div>
|
|
820
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
821
|
+
Public Clips links can render as playable Slack previews.
|
|
822
|
+
</p>
|
|
823
|
+
</div>
|
|
824
|
+
<Button
|
|
825
|
+
type="button"
|
|
826
|
+
variant="outline"
|
|
827
|
+
size="sm"
|
|
828
|
+
className="shrink-0"
|
|
829
|
+
onClick={handleConnectSlack}
|
|
830
|
+
disabled={
|
|
831
|
+
connectingSlack ||
|
|
832
|
+
slackStatus.isLoading ||
|
|
833
|
+
!slackOauthConfigured
|
|
446
834
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
835
|
+
>
|
|
836
|
+
{connectingSlack ? (
|
|
837
|
+
<IconLoader2 className="h-4 w-4 animate-spin" />
|
|
838
|
+
) : (
|
|
839
|
+
<IconExternalLink className="h-4 w-4" />
|
|
840
|
+
)}
|
|
841
|
+
Connect Slack
|
|
842
|
+
</Button>
|
|
451
843
|
</div>
|
|
452
|
-
))}
|
|
453
|
-
</div>
|
|
454
844
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
<IconLoader2 className="h-4 w-4 animate-spin" />
|
|
462
|
-
)}
|
|
463
|
-
Save storage
|
|
464
|
-
</Button>
|
|
465
|
-
</div>
|
|
466
|
-
</CardContent>
|
|
467
|
-
</Card>
|
|
468
|
-
|
|
469
|
-
<Card id="slack" className="scroll-mt-16">
|
|
470
|
-
<CardHeader>
|
|
471
|
-
<CardTitle className="text-base flex items-center gap-2">
|
|
472
|
-
<IconBrandSlack className="size-4 text-primary" />
|
|
473
|
-
Agent-Native Clips for Slack
|
|
474
|
-
</CardTitle>
|
|
475
|
-
<CardDescription>
|
|
476
|
-
Share a public clip, paste the link in Slack, and it plays inline
|
|
477
|
-
— no extra steps for viewers. Connect each workspace once.
|
|
478
|
-
</CardDescription>
|
|
479
|
-
</CardHeader>
|
|
480
|
-
<CardContent className="space-y-4">
|
|
481
|
-
<div className="flex flex-col gap-3 rounded-md border border-border bg-accent/30 px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
482
|
-
<div className="min-w-0">
|
|
483
|
-
<div className="flex items-center gap-2 text-sm font-medium">
|
|
484
|
-
<IconBrandSlack className="h-4 w-4 text-muted-foreground" />
|
|
485
|
-
{slackStatus.isLoading
|
|
486
|
-
? "Checking Slack"
|
|
487
|
-
: slackConnected
|
|
488
|
-
? `${slackInstallations.length} workspace${
|
|
489
|
-
slackInstallations.length === 1 ? "" : "s"
|
|
490
|
-
} connected`
|
|
491
|
-
: slackOauthConfigured
|
|
492
|
-
? "Not connected"
|
|
493
|
-
: "OAuth credentials needed"}
|
|
494
|
-
</div>
|
|
495
|
-
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
496
|
-
Public Clips links can render as playable Slack previews.
|
|
497
|
-
</p>
|
|
498
|
-
</div>
|
|
499
|
-
<Button
|
|
500
|
-
type="button"
|
|
501
|
-
variant="outline"
|
|
502
|
-
size="sm"
|
|
503
|
-
className="shrink-0"
|
|
504
|
-
onClick={handleConnectSlack}
|
|
505
|
-
disabled={
|
|
506
|
-
connectingSlack ||
|
|
507
|
-
slackStatus.isLoading ||
|
|
508
|
-
!slackOauthConfigured
|
|
509
|
-
}
|
|
510
|
-
>
|
|
511
|
-
{connectingSlack ? (
|
|
512
|
-
<IconLoader2 className="h-4 w-4 animate-spin" />
|
|
513
|
-
) : (
|
|
514
|
-
<IconExternalLink className="h-4 w-4" />
|
|
515
|
-
)}
|
|
516
|
-
Connect Slack
|
|
517
|
-
</Button>
|
|
518
|
-
</div>
|
|
845
|
+
{!slackOauthConfigured ? (
|
|
846
|
+
<div className="rounded-md border border-border p-3 text-xs text-muted-foreground">
|
|
847
|
+
Set SLACK_CLIENT_ID and SLACK_CLIENT_SECRET for this
|
|
848
|
+
deployment before connecting workspaces.
|
|
849
|
+
</div>
|
|
850
|
+
) : null}
|
|
519
851
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
<
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
852
|
+
{!slackSigningConfigured ? (
|
|
853
|
+
<div className="rounded-md border border-border p-3 text-xs text-muted-foreground">
|
|
854
|
+
Set SLACK_SIGNING_SECRET so Slack event callbacks can be
|
|
855
|
+
verified.
|
|
856
|
+
</div>
|
|
857
|
+
) : null}
|
|
858
|
+
|
|
859
|
+
{slackInstallations.length > 0 ? (
|
|
860
|
+
<div className="space-y-2">
|
|
861
|
+
{slackInstallations.map((installation) => (
|
|
862
|
+
<div
|
|
863
|
+
key={installation.id}
|
|
864
|
+
className="flex items-center justify-between gap-3 rounded-md border border-border px-3 py-2"
|
|
865
|
+
>
|
|
866
|
+
<div className="min-w-0">
|
|
867
|
+
<div className="truncate text-sm font-medium">
|
|
868
|
+
{installation.teamName || installation.teamId}
|
|
869
|
+
</div>
|
|
870
|
+
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
|
|
871
|
+
<span>{installation.status}</span>
|
|
872
|
+
{installation.enterpriseName ? (
|
|
873
|
+
<span>{installation.enterpriseName}</span>
|
|
874
|
+
) : null}
|
|
875
|
+
<span>Connected by {installation.ownerEmail}</span>
|
|
876
|
+
</div>
|
|
877
|
+
</div>
|
|
878
|
+
<Button
|
|
879
|
+
type="button"
|
|
880
|
+
variant="ghost"
|
|
881
|
+
size="icon"
|
|
882
|
+
className="shrink-0"
|
|
883
|
+
aria-label={`Disconnect ${
|
|
884
|
+
installation.teamName || installation.teamId
|
|
885
|
+
}`}
|
|
886
|
+
onClick={() => setDisconnectSlackTarget(installation)}
|
|
887
|
+
>
|
|
888
|
+
<IconTrash className="h-4 w-4" />
|
|
889
|
+
</Button>
|
|
551
890
|
</div>
|
|
891
|
+
))}
|
|
892
|
+
</div>
|
|
893
|
+
) : null}
|
|
894
|
+
</CardContent>
|
|
895
|
+
</Card>
|
|
896
|
+
|
|
897
|
+
<Card id="ai-providers" className="scroll-mt-16">
|
|
898
|
+
<CardHeader>
|
|
899
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
900
|
+
<IconBrain className="size-4 text-primary" />
|
|
901
|
+
AI setup
|
|
902
|
+
</CardTitle>
|
|
903
|
+
<CardDescription>
|
|
904
|
+
Builder.io is the default path for managed AI credits.
|
|
905
|
+
Provider keys are optional and can be added here.
|
|
906
|
+
</CardDescription>
|
|
907
|
+
</CardHeader>
|
|
908
|
+
<CardContent className="space-y-4">
|
|
909
|
+
<div
|
|
910
|
+
className={cn(
|
|
911
|
+
"flex flex-col gap-3 rounded-md border px-3 py-3 sm:flex-row sm:items-center sm:justify-between",
|
|
912
|
+
builderConnected
|
|
913
|
+
? "border-primary/35 bg-primary/5"
|
|
914
|
+
: "border-border bg-accent/30",
|
|
915
|
+
)}
|
|
916
|
+
>
|
|
917
|
+
<div className="min-w-0">
|
|
918
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
919
|
+
{builderConnected ? (
|
|
920
|
+
<IconCheck className="h-4 w-4 text-primary" />
|
|
921
|
+
) : (
|
|
922
|
+
<IconKey className="h-4 w-4 text-muted-foreground" />
|
|
923
|
+
)}
|
|
924
|
+
{builderStatusLoading
|
|
925
|
+
? "Checking Builder.io"
|
|
926
|
+
: builderConnected
|
|
927
|
+
? "Builder.io connected"
|
|
928
|
+
: "Builder.io is the easiest setup"}
|
|
552
929
|
</div>
|
|
930
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
931
|
+
{builderConnected
|
|
932
|
+
? "Included AI credits and managed transcription are available for Clips."
|
|
933
|
+
: "Connect Builder first for included AI credits, object storage, uploads, and managed transcription."}
|
|
934
|
+
</p>
|
|
935
|
+
</div>
|
|
936
|
+
{builderConnected ? (
|
|
937
|
+
<Badge variant="secondary" className="shrink-0">
|
|
938
|
+
Connected
|
|
939
|
+
</Badge>
|
|
940
|
+
) : (
|
|
553
941
|
<Button
|
|
554
942
|
type="button"
|
|
555
|
-
variant="
|
|
556
|
-
size="
|
|
943
|
+
variant="outline"
|
|
944
|
+
size="sm"
|
|
557
945
|
className="shrink-0"
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
946
|
+
onClick={() =>
|
|
947
|
+
builderConnect.start({
|
|
948
|
+
trackingSource: "clips_settings_ai_setup",
|
|
949
|
+
trackingFlow: "connect_llm",
|
|
950
|
+
})
|
|
951
|
+
}
|
|
952
|
+
disabled={
|
|
953
|
+
builderConnect.connecting || builderStatusLoading
|
|
954
|
+
}
|
|
562
955
|
>
|
|
563
|
-
|
|
956
|
+
{builderConnect.connecting ? (
|
|
957
|
+
<IconLoader2 className="h-4 w-4 animate-spin" />
|
|
958
|
+
) : (
|
|
959
|
+
<IconExternalLink className="h-4 w-4" />
|
|
960
|
+
)}
|
|
961
|
+
Connect Builder.io
|
|
564
962
|
</Button>
|
|
963
|
+
)}
|
|
964
|
+
</div>
|
|
965
|
+
|
|
966
|
+
<Collapsible
|
|
967
|
+
open={apiKeysExpanded}
|
|
968
|
+
onOpenChange={setApiKeysExpanded}
|
|
969
|
+
>
|
|
970
|
+
<div className="rounded-md border border-border">
|
|
971
|
+
<CollapsibleTrigger asChild>
|
|
972
|
+
<button
|
|
973
|
+
type="button"
|
|
974
|
+
className="flex w-full items-center justify-between gap-3 px-3 py-3 text-left"
|
|
975
|
+
>
|
|
976
|
+
<div className="min-w-0">
|
|
977
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
978
|
+
<IconKey className="h-4 w-4 text-muted-foreground" />
|
|
979
|
+
Bring your own provider key
|
|
980
|
+
{configuredApiKeyCount > 0 ? (
|
|
981
|
+
<Badge
|
|
982
|
+
variant="secondary"
|
|
983
|
+
className="text-[10px]"
|
|
984
|
+
>
|
|
985
|
+
{configuredApiKeyCount} set
|
|
986
|
+
</Badge>
|
|
987
|
+
) : null}
|
|
988
|
+
</div>
|
|
989
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
990
|
+
Add Anthropic, OpenAI, Gemini, Groq, or OpenRouter
|
|
991
|
+
keys for provider-billed usage.
|
|
992
|
+
</p>
|
|
993
|
+
</div>
|
|
994
|
+
<IconChevronDown
|
|
995
|
+
className={cn(
|
|
996
|
+
"h-4 w-4 shrink-0 text-muted-foreground transition-transform",
|
|
997
|
+
apiKeysExpanded && "rotate-180",
|
|
998
|
+
)}
|
|
999
|
+
/>
|
|
1000
|
+
</button>
|
|
1001
|
+
</CollapsibleTrigger>
|
|
1002
|
+
<CollapsibleContent>
|
|
1003
|
+
<div className="space-y-3 border-t border-border px-3 py-4">
|
|
1004
|
+
{apiKeyStatusLoading ? (
|
|
1005
|
+
<div className="text-xs text-muted-foreground">
|
|
1006
|
+
Checking provider keys…
|
|
1007
|
+
</div>
|
|
1008
|
+
) : null}
|
|
1009
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
1010
|
+
{AI_PROVIDER_FIELDS.map((field) => {
|
|
1011
|
+
const configured = Boolean(apiKeyStatus[field.key]);
|
|
1012
|
+
const savingThisKey = savingApiKey === field.key;
|
|
1013
|
+
return (
|
|
1014
|
+
<div key={field.key} className="space-y-1.5">
|
|
1015
|
+
<div className="flex items-center justify-between gap-2">
|
|
1016
|
+
<Label htmlFor={field.key}>
|
|
1017
|
+
{field.label}
|
|
1018
|
+
</Label>
|
|
1019
|
+
{configured ? (
|
|
1020
|
+
<span className="flex items-center gap-1 text-[10px] font-medium text-primary">
|
|
1021
|
+
<IconCheck className="h-3 w-3" />
|
|
1022
|
+
Set
|
|
1023
|
+
</span>
|
|
1024
|
+
) : null}
|
|
1025
|
+
</div>
|
|
1026
|
+
<div className="flex gap-2">
|
|
1027
|
+
<Input
|
|
1028
|
+
id={field.key}
|
|
1029
|
+
type="password"
|
|
1030
|
+
value={apiKeyValues[field.key] ?? ""}
|
|
1031
|
+
onChange={(event) =>
|
|
1032
|
+
setApiKeyValues((current) => ({
|
|
1033
|
+
...current,
|
|
1034
|
+
[field.key]: event.target.value,
|
|
1035
|
+
}))
|
|
1036
|
+
}
|
|
1037
|
+
onKeyDown={(event) => {
|
|
1038
|
+
if (event.key === "Enter") {
|
|
1039
|
+
void handleSaveApiKey(field.key);
|
|
1040
|
+
}
|
|
1041
|
+
}}
|
|
1042
|
+
placeholder={
|
|
1043
|
+
configured
|
|
1044
|
+
? "Replace key…"
|
|
1045
|
+
: field.placeholder
|
|
1046
|
+
}
|
|
1047
|
+
autoComplete="off"
|
|
1048
|
+
disabled={savingThisKey}
|
|
1049
|
+
/>
|
|
1050
|
+
<Button
|
|
1051
|
+
type="button"
|
|
1052
|
+
variant="outline"
|
|
1053
|
+
size="sm"
|
|
1054
|
+
className="shrink-0"
|
|
1055
|
+
onClick={() => handleSaveApiKey(field.key)}
|
|
1056
|
+
disabled={
|
|
1057
|
+
savingThisKey ||
|
|
1058
|
+
!(apiKeyValues[field.key] ?? "").trim()
|
|
1059
|
+
}
|
|
1060
|
+
>
|
|
1061
|
+
{savingThisKey ? (
|
|
1062
|
+
<IconLoader2 className="h-4 w-4 animate-spin" />
|
|
1063
|
+
) : (
|
|
1064
|
+
"Save"
|
|
1065
|
+
)}
|
|
1066
|
+
</Button>
|
|
1067
|
+
</div>
|
|
1068
|
+
</div>
|
|
1069
|
+
);
|
|
1070
|
+
})}
|
|
1071
|
+
</div>
|
|
1072
|
+
</div>
|
|
1073
|
+
</CollapsibleContent>
|
|
565
1074
|
</div>
|
|
566
|
-
|
|
567
|
-
</
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
<div className="
|
|
583
|
-
<
|
|
584
|
-
|
|
1075
|
+
</Collapsible>
|
|
1076
|
+
</CardContent>
|
|
1077
|
+
</Card>
|
|
1078
|
+
|
|
1079
|
+
<Card>
|
|
1080
|
+
<CardHeader>
|
|
1081
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
1082
|
+
<IconUser className="size-4 text-primary" />
|
|
1083
|
+
Profile
|
|
1084
|
+
</CardTitle>
|
|
1085
|
+
</CardHeader>
|
|
1086
|
+
<CardContent className="space-y-4">
|
|
1087
|
+
<div className="space-y-1.5">
|
|
1088
|
+
<Label htmlFor="email">Email</Label>
|
|
1089
|
+
<Input id="email" value={email} readOnly disabled />
|
|
1090
|
+
</div>
|
|
1091
|
+
<div className="space-y-1.5">
|
|
1092
|
+
<Label htmlFor="display-name">Display name</Label>
|
|
1093
|
+
<Input
|
|
1094
|
+
id="display-name"
|
|
1095
|
+
value={displayName}
|
|
1096
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
1097
|
+
placeholder="Your name"
|
|
1098
|
+
disabled={loading}
|
|
1099
|
+
/>
|
|
585
1100
|
</div>
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1101
|
+
</CardContent>
|
|
1102
|
+
</Card>
|
|
1103
|
+
|
|
1104
|
+
<Card>
|
|
1105
|
+
<CardHeader>
|
|
1106
|
+
<CardTitle className="text-base">Playback</CardTitle>
|
|
1107
|
+
</CardHeader>
|
|
1108
|
+
<CardContent className="space-y-4">
|
|
1109
|
+
<div className="space-y-1.5">
|
|
1110
|
+
<Label htmlFor="speed">Default playback speed</Label>
|
|
1111
|
+
<Select
|
|
1112
|
+
value={defaultSpeed}
|
|
1113
|
+
onValueChange={setDefaultSpeed}
|
|
1114
|
+
disabled={loading}
|
|
1115
|
+
>
|
|
1116
|
+
<SelectTrigger id="speed" className="w-40">
|
|
1117
|
+
<SelectValue />
|
|
1118
|
+
</SelectTrigger>
|
|
1119
|
+
<SelectContent>
|
|
1120
|
+
{SPEEDS.map((s) => (
|
|
1121
|
+
<SelectItem key={s} value={s}>
|
|
1122
|
+
{s}×
|
|
1123
|
+
</SelectItem>
|
|
1124
|
+
))}
|
|
1125
|
+
</SelectContent>
|
|
1126
|
+
</Select>
|
|
1127
|
+
<p className="text-xs text-muted-foreground">
|
|
1128
|
+
Applied automatically when you open a recording.
|
|
1129
|
+
</p>
|
|
1130
|
+
</div>
|
|
1131
|
+
</CardContent>
|
|
1132
|
+
</Card>
|
|
1133
|
+
|
|
1134
|
+
<Card>
|
|
1135
|
+
<CardHeader>
|
|
1136
|
+
<CardTitle className="text-base">Transcript</CardTitle>
|
|
1137
|
+
</CardHeader>
|
|
1138
|
+
<CardContent>
|
|
1139
|
+
<div className="flex items-center justify-between gap-4">
|
|
1140
|
+
<div>
|
|
1141
|
+
<Label
|
|
1142
|
+
htmlFor="transcript-cleanup"
|
|
1143
|
+
className="cursor-pointer"
|
|
1144
|
+
>
|
|
1145
|
+
Background cleanup
|
|
1146
|
+
</Label>
|
|
1147
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
1148
|
+
Show the native transcript immediately, then clean it up
|
|
1149
|
+
in the background when available.
|
|
1150
|
+
</p>
|
|
1151
|
+
</div>
|
|
1152
|
+
<Switch
|
|
1153
|
+
id="transcript-cleanup"
|
|
1154
|
+
checked={transcriptCleanupEnabled}
|
|
1155
|
+
onCheckedChange={setTranscriptCleanupEnabled}
|
|
1156
|
+
disabled={loading}
|
|
1157
|
+
/>
|
|
1158
|
+
</div>
|
|
1159
|
+
</CardContent>
|
|
1160
|
+
</Card>
|
|
1161
|
+
|
|
1162
|
+
<Card>
|
|
1163
|
+
<CardHeader>
|
|
1164
|
+
<CardTitle className="text-base">Notifications</CardTitle>
|
|
1165
|
+
</CardHeader>
|
|
1166
|
+
<CardContent>
|
|
1167
|
+
<div className="flex items-center justify-between">
|
|
1168
|
+
<div>
|
|
1169
|
+
<Label htmlFor="email-notif" className="cursor-pointer">
|
|
1170
|
+
Email notifications
|
|
1171
|
+
</Label>
|
|
1172
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
1173
|
+
Get an email when someone comments, reacts, or shares a
|
|
1174
|
+
recording with you.
|
|
1175
|
+
</p>
|
|
1176
|
+
</div>
|
|
1177
|
+
<Switch
|
|
1178
|
+
id="email-notif"
|
|
1179
|
+
checked={emailNotifications}
|
|
1180
|
+
onCheckedChange={setEmailNotifications}
|
|
1181
|
+
disabled={loading}
|
|
1182
|
+
/>
|
|
1183
|
+
</div>
|
|
1184
|
+
</CardContent>
|
|
1185
|
+
</Card>
|
|
1186
|
+
|
|
1187
|
+
<div className="flex justify-end">
|
|
592
1188
|
<Button
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
className="shrink-0"
|
|
597
|
-
onClick={() => {
|
|
598
|
-
openBuilderConnectPopup({
|
|
599
|
-
source: "clips_settings_ai_providers",
|
|
600
|
-
});
|
|
601
|
-
toast.message("Finish connecting Builder.io in the popup.");
|
|
602
|
-
}}
|
|
1189
|
+
onClick={handleSave}
|
|
1190
|
+
disabled={loading || saving}
|
|
1191
|
+
className="bg-primary hover:bg-primary/90"
|
|
603
1192
|
>
|
|
604
|
-
|
|
605
|
-
Connect Builder.io
|
|
1193
|
+
{saving ? "Saving…" : "Save changes"}
|
|
606
1194
|
</Button>
|
|
607
1195
|
</div>
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
<div className="text-sm font-medium">
|
|
611
|
-
Bring your own provider key
|
|
612
|
-
</div>
|
|
613
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
614
|
-
Open the agent sidebar menu, then API Keys & Connections, to add
|
|
615
|
-
Anthropic, OpenAI, Gemini, Groq, or other supported keys. Usage
|
|
616
|
-
bills to the provider account you connect.
|
|
617
|
-
</p>
|
|
618
|
-
</div>
|
|
619
|
-
</CardContent>
|
|
620
|
-
</Card>
|
|
621
|
-
|
|
622
|
-
<Card>
|
|
623
|
-
<CardHeader>
|
|
624
|
-
<CardTitle className="text-base flex items-center gap-2">
|
|
625
|
-
<IconUser className="size-4 text-primary" />
|
|
626
|
-
Profile
|
|
627
|
-
</CardTitle>
|
|
628
|
-
</CardHeader>
|
|
629
|
-
<CardContent className="space-y-4">
|
|
630
|
-
<div className="space-y-1.5">
|
|
631
|
-
<Label htmlFor="email">Email</Label>
|
|
632
|
-
<Input id="email" value={email} readOnly disabled />
|
|
633
|
-
</div>
|
|
634
|
-
<div className="space-y-1.5">
|
|
635
|
-
<Label htmlFor="display-name">Display name</Label>
|
|
636
|
-
<Input
|
|
637
|
-
id="display-name"
|
|
638
|
-
value={displayName}
|
|
639
|
-
onChange={(e) => setDisplayName(e.target.value)}
|
|
640
|
-
placeholder="Your name"
|
|
641
|
-
disabled={loading}
|
|
642
|
-
/>
|
|
643
|
-
</div>
|
|
644
|
-
</CardContent>
|
|
645
|
-
</Card>
|
|
646
|
-
|
|
647
|
-
<Card>
|
|
648
|
-
<CardHeader>
|
|
649
|
-
<CardTitle className="text-base">Playback</CardTitle>
|
|
650
|
-
</CardHeader>
|
|
651
|
-
<CardContent className="space-y-4">
|
|
652
|
-
<div className="space-y-1.5">
|
|
653
|
-
<Label htmlFor="speed">Default playback speed</Label>
|
|
654
|
-
<Select
|
|
655
|
-
value={defaultSpeed}
|
|
656
|
-
onValueChange={setDefaultSpeed}
|
|
657
|
-
disabled={loading}
|
|
658
|
-
>
|
|
659
|
-
<SelectTrigger id="speed" className="w-40">
|
|
660
|
-
<SelectValue />
|
|
661
|
-
</SelectTrigger>
|
|
662
|
-
<SelectContent>
|
|
663
|
-
{SPEEDS.map((s) => (
|
|
664
|
-
<SelectItem key={s} value={s}>
|
|
665
|
-
{s}×
|
|
666
|
-
</SelectItem>
|
|
667
|
-
))}
|
|
668
|
-
</SelectContent>
|
|
669
|
-
</Select>
|
|
670
|
-
<p className="text-xs text-muted-foreground">
|
|
671
|
-
Applied automatically when you open a recording.
|
|
672
|
-
</p>
|
|
673
|
-
</div>
|
|
674
|
-
</CardContent>
|
|
675
|
-
</Card>
|
|
676
|
-
|
|
677
|
-
<Card>
|
|
678
|
-
<CardHeader>
|
|
679
|
-
<CardTitle className="text-base">Transcript</CardTitle>
|
|
680
|
-
</CardHeader>
|
|
681
|
-
<CardContent>
|
|
682
|
-
<div className="flex items-center justify-between gap-4">
|
|
683
|
-
<div>
|
|
684
|
-
<Label htmlFor="transcript-cleanup" className="cursor-pointer">
|
|
685
|
-
Background cleanup
|
|
686
|
-
</Label>
|
|
687
|
-
<p className="text-xs text-muted-foreground mt-0.5">
|
|
688
|
-
Show the native transcript immediately, then clean it up in
|
|
689
|
-
the background when available.
|
|
690
|
-
</p>
|
|
691
|
-
</div>
|
|
692
|
-
<Switch
|
|
693
|
-
id="transcript-cleanup"
|
|
694
|
-
checked={transcriptCleanupEnabled}
|
|
695
|
-
onCheckedChange={setTranscriptCleanupEnabled}
|
|
696
|
-
disabled={loading}
|
|
697
|
-
/>
|
|
698
|
-
</div>
|
|
699
|
-
</CardContent>
|
|
700
|
-
</Card>
|
|
701
|
-
|
|
702
|
-
<Card>
|
|
703
|
-
<CardHeader>
|
|
704
|
-
<CardTitle className="text-base">Notifications</CardTitle>
|
|
705
|
-
</CardHeader>
|
|
706
|
-
<CardContent>
|
|
707
|
-
<div className="flex items-center justify-between">
|
|
708
|
-
<div>
|
|
709
|
-
<Label htmlFor="email-notif" className="cursor-pointer">
|
|
710
|
-
Email notifications
|
|
711
|
-
</Label>
|
|
712
|
-
<p className="text-xs text-muted-foreground mt-0.5">
|
|
713
|
-
Get an email when someone comments, reacts, or shares a
|
|
714
|
-
recording with you.
|
|
715
|
-
</p>
|
|
716
|
-
</div>
|
|
717
|
-
<Switch
|
|
718
|
-
id="email-notif"
|
|
719
|
-
checked={emailNotifications}
|
|
720
|
-
onCheckedChange={setEmailNotifications}
|
|
721
|
-
disabled={loading}
|
|
722
|
-
/>
|
|
723
|
-
</div>
|
|
724
|
-
</CardContent>
|
|
725
|
-
</Card>
|
|
726
|
-
|
|
727
|
-
<div className="flex justify-end">
|
|
728
|
-
<Button
|
|
729
|
-
onClick={handleSave}
|
|
730
|
-
disabled={loading || saving}
|
|
731
|
-
className="bg-primary hover:bg-primary/90"
|
|
732
|
-
>
|
|
733
|
-
{saving ? "Saving…" : "Save changes"}
|
|
734
|
-
</Button>
|
|
1196
|
+
</div>
|
|
1197
|
+
<CompactChangelogAside />
|
|
735
1198
|
</div>
|
|
736
1199
|
</div>
|
|
737
1200
|
<AlertDialog
|