@agent-native/core 0.130.0 → 0.130.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +481 -122
- package/corpus/core/src/integrations/a2a-continuations-store.ts +345 -14
- package/corpus/core/src/integrations/adapters/slack.ts +243 -53
- package/corpus/core/src/integrations/integration-campaign-recovery.ts +4 -1
- package/corpus/core/src/integrations/plugin.ts +39 -21
- package/corpus/core/src/integrations/task-queue-stats.ts +146 -0
- package/corpus/core/src/integrations/types.ts +25 -4
- package/corpus/core/src/integrations/webhook-handler.ts +18 -5
- package/corpus/core/src/server/email-template.ts +43 -13
- package/corpus/core/src/sharing/actions/share-resource.ts +26 -4
- package/corpus/core/src/sharing/index.ts +1 -0
- package/corpus/core/src/sharing/registry.ts +23 -0
- package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +3 -1
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/components/meetings/day-header.tsx +1 -1
- package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +1 -22
- package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +26 -1
- package/corpus/templates/clips/app/components/workspace/organization-identity-card.tsx +107 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -4
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +0 -10
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +23 -51
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +1 -8
- package/corpus/templates/clips/changelog/2026-07-29-clearing-all-recording-edits-no-longer-leaves-the-editor-unr.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-clip-share-emails-now-open-with-who-shared-the-clip-add-a-su.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-finished-meeting-pills-now-have-a-clear-close-button.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-meeting-cards-no-longer-flicker-or-disappear-when-hovered-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-organization-name-logo-brand-color-and-default-visibility-no.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-transcript-lines-on-the-meeting-page-no-longer-jump-to-the-r.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-when-a-meeting-wraps-up-the-recording-pill-now-shows-a-green.md +6 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +6 -3
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +112 -32
- package/corpus/templates/clips/desktop/src/styles.css +42 -2
- package/corpus/templates/clips/server/db/index.ts +6 -0
- package/corpus/templates/clips/server/lib/share-email-hero.ts +33 -0
- package/corpus/templates/clips/shared/clips-ai-prefs.ts +0 -1
- package/corpus/templates/dispatch/changelog/2026-07-26-slack-replies-recover-after-connected-app-updates.md +6 -0
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +15 -0
- package/corpus/templates/slides/actions/delete-design-system.ts +145 -0
- package/corpus/templates/slides/actions/list-design-systems.ts +94 -2
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +43 -1
- package/corpus/templates/slides/app/hooks/use-design-systems.ts +2 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +7 -0
- package/corpus/templates/slides/app/pages/DesignSystems.tsx +56 -1
- package/corpus/templates/slides/changelog/2026-07-28-delete-a-design-system-you-own-from-the-design-systems-page-.md +6 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts +5 -0
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +293 -79
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts +23 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +249 -15
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +155 -31
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/integration-campaign-recovery.d.ts.map +1 -1
- package/dist/integrations/integration-campaign-recovery.js +3 -1
- package/dist/integrations/integration-campaign-recovery.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +29 -12
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/task-queue-stats.d.ts +12 -0
- package/dist/integrations/task-queue-stats.d.ts.map +1 -1
- package/dist/integrations/task-queue-stats.js +106 -0
- package/dist/integrations/task-queue-stats.js.map +1 -1
- package/dist/integrations/types.d.ts +24 -6
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +12 -3
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/email-template.d.ts +4 -0
- package/dist/server/email-template.d.ts.map +1 -1
- package/dist/server/email-template.js +25 -8
- package/dist/server/email-template.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/sharing/actions/share-resource.d.ts.map +1 -1
- package/dist/sharing/actions/share-resource.js +23 -4
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/sharing/index.d.ts +1 -1
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js.map +1 -1
- package/dist/sharing/registry.d.ts +23 -0
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/package.json +1 -1
- package/src/integrations/a2a-continuation-processor.ts +481 -122
- package/src/integrations/a2a-continuations-store.ts +345 -14
- package/src/integrations/adapters/slack.ts +243 -53
- package/src/integrations/integration-campaign-recovery.ts +4 -1
- package/src/integrations/plugin.ts +39 -21
- package/src/integrations/task-queue-stats.ts +146 -0
- package/src/integrations/types.ts +25 -4
- package/src/integrations/webhook-handler.ts +18 -5
- package/src/server/email-template.ts +43 -13
- package/src/sharing/actions/share-resource.ts +26 -4
- package/src/sharing/index.ts +1 -0
- package/src/sharing/registry.ts +23 -0
- package/corpus/templates/clips/app/routes/_app.settings.organization.tsx +0 -232
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useActionQuery, useSession } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
6
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
7
|
+
import {
|
|
8
|
+
BrandingEditor,
|
|
9
|
+
type RecordingVisibility,
|
|
10
|
+
} from "@/components/workspace/branding-editor";
|
|
11
|
+
import type { MemberRole } from "@/components/workspace/members-list";
|
|
12
|
+
|
|
13
|
+
interface OrganizationStateResponse {
|
|
14
|
+
organization: {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
brandColor: string;
|
|
18
|
+
brandLogoUrl: string | null;
|
|
19
|
+
defaultVisibility: RecordingVisibility;
|
|
20
|
+
ownerEmail?: string;
|
|
21
|
+
} | null;
|
|
22
|
+
members: { email: string; role: MemberRole }[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Organization identity — name, logo, brand color, default recording
|
|
27
|
+
* visibility. It sits directly above membership in the Organization tab
|
|
28
|
+
* because the name and logo are what recipients see in share emails.
|
|
29
|
+
*/
|
|
30
|
+
export function OrganizationIdentityCard() {
|
|
31
|
+
const t = useT();
|
|
32
|
+
const { session } = useSession();
|
|
33
|
+
const email = session?.email ?? "";
|
|
34
|
+
|
|
35
|
+
const { data, isPending, isError } =
|
|
36
|
+
useActionQuery<OrganizationStateResponse>(
|
|
37
|
+
"list-organization-state",
|
|
38
|
+
undefined,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const organization = data?.organization ?? null;
|
|
42
|
+
const members = useMemo(() => data?.members ?? [], [data?.members]);
|
|
43
|
+
const isAdmin = useMemo(() => {
|
|
44
|
+
if (organization?.ownerEmail && organization.ownerEmail === email) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const role = members.find((m) => m.email === email)?.role;
|
|
48
|
+
return role === "admin" || role === "owner";
|
|
49
|
+
}, [members, email, organization?.ownerEmail]);
|
|
50
|
+
|
|
51
|
+
if (isPending) return <Skeleton className="h-64 w-full" />;
|
|
52
|
+
// A failed load must not look like "this org has no branding".
|
|
53
|
+
if (isError) {
|
|
54
|
+
return (
|
|
55
|
+
<Card>
|
|
56
|
+
<CardContent className="py-6 text-center text-sm text-muted-foreground">
|
|
57
|
+
{t("organizationSettings.brandingLoadFailed")}
|
|
58
|
+
</CardContent>
|
|
59
|
+
</Card>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (!organization) return null;
|
|
63
|
+
|
|
64
|
+
if (!isAdmin) {
|
|
65
|
+
return (
|
|
66
|
+
<Card>
|
|
67
|
+
<CardHeader>
|
|
68
|
+
<CardTitle className="text-base">
|
|
69
|
+
{t("brandingEditor.title")}
|
|
70
|
+
</CardTitle>
|
|
71
|
+
</CardHeader>
|
|
72
|
+
<CardContent>
|
|
73
|
+
<div className="flex items-center gap-3">
|
|
74
|
+
{organization.brandLogoUrl ? (
|
|
75
|
+
<img
|
|
76
|
+
src={organization.brandLogoUrl}
|
|
77
|
+
alt=""
|
|
78
|
+
className="h-10 w-10 rounded object-contain"
|
|
79
|
+
/>
|
|
80
|
+
) : (
|
|
81
|
+
<div
|
|
82
|
+
className="h-10 w-10 rounded"
|
|
83
|
+
style={{ background: organization.brandColor }}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
<div>
|
|
87
|
+
<div className="font-medium">{organization.name}</div>
|
|
88
|
+
<div className="text-xs text-muted-foreground">
|
|
89
|
+
{t("organizationSettings.adminsOnlyBranding")}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</CardContent>
|
|
94
|
+
</Card>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<BrandingEditor
|
|
100
|
+
organizationId={organization.id}
|
|
101
|
+
initialName={organization.name}
|
|
102
|
+
initialBrandColor={organization.brandColor}
|
|
103
|
+
initialBrandLogoUrl={organization.brandLogoUrl}
|
|
104
|
+
initialDefaultVisibility={organization.defaultVisibility}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -547,6 +547,8 @@ const messages = {
|
|
|
547
547
|
useColor: "Use {{color}}",
|
|
548
548
|
logo: "Logo",
|
|
549
549
|
logoPreview: "Logo preview",
|
|
550
|
+
logoUsage: "Shown to recipients in share emails and on public clip pages.",
|
|
551
|
+
emailHeaderPreview: "Share email header",
|
|
550
552
|
dropReplace: "Drop a new image to replace",
|
|
551
553
|
dropHere: "Drop a PNG / JPG / SVG here",
|
|
552
554
|
uploading: "Uploading…",
|
|
@@ -643,10 +645,6 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
643
645
|
### Added
|
|
644
646
|
|
|
645
647
|
- See what's new right inside Clips. A changelog now lives in the command menu (Cmd+K) and in Settings.`,
|
|
646
|
-
profile: "Profile",
|
|
647
|
-
email: "Email",
|
|
648
|
-
displayName: "Display name",
|
|
649
|
-
displayNamePlaceholder: "Your name",
|
|
650
648
|
playback: "Playback",
|
|
651
649
|
defaultPlaybackSpeed: "Default playback speed",
|
|
652
650
|
playbackDescription: "Applied automatically when you open a recording.",
|
|
@@ -786,6 +784,7 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
786
784
|
"No organization yet. Create one from the organization switcher to get started.",
|
|
787
785
|
description: "Organization admin: branding, members, invites.",
|
|
788
786
|
adminsOnlyBranding: "Only admins can edit branding.",
|
|
787
|
+
brandingLoadFailed: "Couldn't load organization branding.",
|
|
789
788
|
members: "Members",
|
|
790
789
|
pendingInvites: "Pending invites",
|
|
791
790
|
noPendingInvites: "No pending invites.",
|
|
@@ -495,14 +495,6 @@ export default function MeetingDetailRoute() {
|
|
|
495
495
|
});
|
|
496
496
|
};
|
|
497
497
|
|
|
498
|
-
const handleSeek = (ms: number) => {
|
|
499
|
-
if (!meeting?.recordingId) return;
|
|
500
|
-
// /r/:recordingId's `t` param is seconds (see parseTimeParam in
|
|
501
|
-
// r.$recordingId.tsx), while transcript segments use ms timestamps.
|
|
502
|
-
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
503
|
-
navigate(`/r/${meeting.recordingId}?t=${seconds}`);
|
|
504
|
-
};
|
|
505
|
-
|
|
506
498
|
const handleJumpToSegment = (segmentIndex: number) => {
|
|
507
499
|
transcriptScrollToRef.current?.(segmentIndex);
|
|
508
500
|
};
|
|
@@ -976,8 +968,6 @@ export default function MeetingDetailRoute() {
|
|
|
976
968
|
<TranscriptBubbles
|
|
977
969
|
segments={segments}
|
|
978
970
|
isLive={isLive}
|
|
979
|
-
recordingId={meeting.recordingId}
|
|
980
|
-
onSeek={handleSeek}
|
|
981
971
|
registerScrollTo={(fn) => {
|
|
982
972
|
transcriptScrollToRef.current = fn;
|
|
983
973
|
}}
|
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
appApiPath,
|
|
4
4
|
} from "@agent-native/core/client/api-path";
|
|
5
5
|
import { ChangelogSettingsCard } from "@agent-native/core/client/changelog";
|
|
6
|
-
import {
|
|
6
|
+
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
7
7
|
import { LanguagePicker, useT } from "@agent-native/core/client/i18n";
|
|
8
|
-
import {
|
|
8
|
+
import { useOrg, useSwitchOrg } from "@agent-native/core/client/org";
|
|
9
9
|
import {
|
|
10
10
|
AccountSettingsCard,
|
|
11
11
|
useBuilderConnectFlow,
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
IconLoader2,
|
|
31
31
|
IconServer,
|
|
32
32
|
IconTrash,
|
|
33
|
-
IconUser,
|
|
34
33
|
IconUsersGroup,
|
|
35
34
|
} from "@tabler/icons-react";
|
|
36
35
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
@@ -71,6 +70,7 @@ import {
|
|
|
71
70
|
SelectValue,
|
|
72
71
|
} from "@/components/ui/select";
|
|
73
72
|
import { Switch } from "@/components/ui/switch";
|
|
73
|
+
import { OrganizationIdentityCard } from "@/components/workspace/organization-identity-card";
|
|
74
74
|
import { useVideoStorageStatus } from "@/hooks/use-video-storage-status";
|
|
75
75
|
import enMessages from "@/i18n/en-US";
|
|
76
76
|
import { cn } from "@/lib/utils";
|
|
@@ -166,7 +166,6 @@ const AI_PROVIDER_FIELDS = [
|
|
|
166
166
|
interface ClipsUserSettings {
|
|
167
167
|
defaultPlaybackSpeed?: string;
|
|
168
168
|
emailNotifications?: boolean;
|
|
169
|
-
displayName?: string;
|
|
170
169
|
transcriptCleanupEnabled?: boolean;
|
|
171
170
|
includeFullVideoInAi?: boolean;
|
|
172
171
|
}
|
|
@@ -393,12 +392,29 @@ async function saveRegisteredSecret(key: string, value: string): Promise<void> {
|
|
|
393
392
|
}
|
|
394
393
|
|
|
395
394
|
export default function SettingsIndexRoute() {
|
|
396
|
-
const { session } = useSession();
|
|
397
395
|
const t = useT();
|
|
398
396
|
const agentSettingsTabs = useAgentSettingsTabs();
|
|
397
|
+
// Organization identity (name, logo, brand color) belongs with membership,
|
|
398
|
+
// so it rides on the framework's Organization tab rather than a second one.
|
|
399
|
+
const settingsTabs = useMemo(
|
|
400
|
+
() =>
|
|
401
|
+
agentSettingsTabs.map((tab) =>
|
|
402
|
+
tab.id === "organization"
|
|
403
|
+
? {
|
|
404
|
+
...tab,
|
|
405
|
+
content: (
|
|
406
|
+
<div className="mx-auto w-full max-w-2xl space-y-6">
|
|
407
|
+
<OrganizationIdentityCard />
|
|
408
|
+
{tab.content}
|
|
409
|
+
</div>
|
|
410
|
+
),
|
|
411
|
+
}
|
|
412
|
+
: tab,
|
|
413
|
+
),
|
|
414
|
+
[agentSettingsTabs],
|
|
415
|
+
);
|
|
399
416
|
const { data: org } = useOrg();
|
|
400
417
|
const switchOrg = useSwitchOrg();
|
|
401
|
-
const email = session?.email ?? "";
|
|
402
418
|
const storageStatus = useVideoStorageStatus();
|
|
403
419
|
const builderStatus = useBuilderStatus();
|
|
404
420
|
const builderConnect = useBuilderConnectFlow({
|
|
@@ -430,7 +446,6 @@ export default function SettingsIndexRoute() {
|
|
|
430
446
|
useState<SlackInstallation | null>(null);
|
|
431
447
|
const [defaultSpeed, setDefaultSpeed] = useState("1.2");
|
|
432
448
|
const [emailNotifications, setEmailNotifications] = useState(true);
|
|
433
|
-
const [displayName, setDisplayName] = useState("");
|
|
434
449
|
const [transcriptCleanupEnabled, setTranscriptCleanupEnabled] =
|
|
435
450
|
useState(true);
|
|
436
451
|
const [s3Values, setS3Values] = useState<Record<string, string>>({});
|
|
@@ -496,7 +511,6 @@ export default function SettingsIndexRoute() {
|
|
|
496
511
|
if (cancelled) return;
|
|
497
512
|
setDefaultSpeed(v.defaultPlaybackSpeed ?? "1.2");
|
|
498
513
|
setEmailNotifications(v.emailNotifications ?? true);
|
|
499
|
-
setDisplayName(v.displayName ?? "");
|
|
500
514
|
setTranscriptCleanupEnabled(v.transcriptCleanupEnabled !== false);
|
|
501
515
|
setLoading(false);
|
|
502
516
|
});
|
|
@@ -531,7 +545,6 @@ export default function SettingsIndexRoute() {
|
|
|
531
545
|
await saveSettings({
|
|
532
546
|
defaultPlaybackSpeed: defaultSpeed,
|
|
533
547
|
emailNotifications,
|
|
534
|
-
displayName: displayName.trim() || undefined,
|
|
535
548
|
transcriptCleanupEnabled,
|
|
536
549
|
});
|
|
537
550
|
toast.success(t("settings.saved"));
|
|
@@ -801,12 +814,6 @@ export default function SettingsIndexRoute() {
|
|
|
801
814
|
"ai provider api key anthropic openai gemini groq openrouter builder",
|
|
802
815
|
hash: "ai-providers",
|
|
803
816
|
},
|
|
804
|
-
{
|
|
805
|
-
id: "clips-profile",
|
|
806
|
-
label: t("settings.profile"),
|
|
807
|
-
keywords: "profile email display name",
|
|
808
|
-
hash: "profile",
|
|
809
|
-
},
|
|
810
817
|
{
|
|
811
818
|
id: "clips-playback",
|
|
812
819
|
label: t("settings.playback"),
|
|
@@ -839,7 +846,7 @@ export default function SettingsIndexRoute() {
|
|
|
839
846
|
<SettingsTabsPage
|
|
840
847
|
account={<AccountSettingsCard />}
|
|
841
848
|
whatsNewLabel={t("settings.whatsNew")}
|
|
842
|
-
extraTabs={
|
|
849
|
+
extraTabs={settingsTabs}
|
|
843
850
|
generalSearchEntries={generalSearchEntries}
|
|
844
851
|
general={
|
|
845
852
|
<div className="mx-auto w-full max-w-4xl space-y-6">
|
|
@@ -1477,33 +1484,6 @@ export default function SettingsIndexRoute() {
|
|
|
1477
1484
|
</CardContent>
|
|
1478
1485
|
</Card>
|
|
1479
1486
|
|
|
1480
|
-
<Card id="profile" className="scroll-mt-16">
|
|
1481
|
-
<CardHeader>
|
|
1482
|
-
<CardTitle className="text-base flex items-center gap-2">
|
|
1483
|
-
<IconUser className="size-4 text-primary" />
|
|
1484
|
-
{t("settings.profile")}
|
|
1485
|
-
</CardTitle>
|
|
1486
|
-
</CardHeader>
|
|
1487
|
-
<CardContent className="space-y-4">
|
|
1488
|
-
<div className="space-y-1.5">
|
|
1489
|
-
<Label htmlFor="email">{t("settings.email")}</Label>
|
|
1490
|
-
<Input id="email" value={email} readOnly disabled />
|
|
1491
|
-
</div>
|
|
1492
|
-
<div className="space-y-1.5">
|
|
1493
|
-
<Label htmlFor="display-name">
|
|
1494
|
-
{t("settings.displayName")}
|
|
1495
|
-
</Label>
|
|
1496
|
-
<Input
|
|
1497
|
-
id="display-name"
|
|
1498
|
-
value={displayName}
|
|
1499
|
-
onChange={(e) => setDisplayName(e.target.value)}
|
|
1500
|
-
placeholder={t("settings.displayNamePlaceholder")}
|
|
1501
|
-
disabled={loading}
|
|
1502
|
-
/>
|
|
1503
|
-
</div>
|
|
1504
|
-
</CardContent>
|
|
1505
|
-
</Card>
|
|
1506
|
-
|
|
1507
1487
|
<Card id="playback" className="scroll-mt-16">
|
|
1508
1488
|
<CardHeader>
|
|
1509
1489
|
<CardTitle className="text-base">
|
|
@@ -1605,14 +1585,6 @@ export default function SettingsIndexRoute() {
|
|
|
1605
1585
|
</div>
|
|
1606
1586
|
</div>
|
|
1607
1587
|
}
|
|
1608
|
-
team={
|
|
1609
|
-
<div className="mx-auto w-full max-w-3xl">
|
|
1610
|
-
<TeamPage
|
|
1611
|
-
showTitle={false}
|
|
1612
|
-
createOrgDescription={t("organizationSettings.description")}
|
|
1613
|
-
/>
|
|
1614
|
-
</div>
|
|
1615
|
-
}
|
|
1616
1588
|
whatsNew={
|
|
1617
1589
|
<div className="mx-auto w-full max-w-3xl">
|
|
1618
1590
|
<ChangelogSettingsCard
|
|
@@ -427,8 +427,6 @@ export default function ShareMeetingRoute() {
|
|
|
427
427
|
<TranscriptBubbles
|
|
428
428
|
segments={transcript.segments}
|
|
429
429
|
isLive={false}
|
|
430
|
-
recordingId={null}
|
|
431
|
-
onSeek={() => {}}
|
|
432
430
|
/>
|
|
433
431
|
</div>
|
|
434
432
|
) : transcript.fullText ? (
|
|
@@ -437,12 +435,7 @@ export default function ShareMeetingRoute() {
|
|
|
437
435
|
</div>
|
|
438
436
|
) : (
|
|
439
437
|
<div className="flex min-h-40 flex-col overflow-hidden rounded-lg border border-border">
|
|
440
|
-
<TranscriptBubbles
|
|
441
|
-
segments={[]}
|
|
442
|
-
isLive={false}
|
|
443
|
-
recordingId={null}
|
|
444
|
-
onSeek={() => {}}
|
|
445
|
-
/>
|
|
438
|
+
<TranscriptBubbles segments={[]} isLive={false} />
|
|
446
439
|
</div>
|
|
447
440
|
)}
|
|
448
441
|
</section>
|
|
@@ -207,14 +207,17 @@ export function useMeetingTranscription({
|
|
|
207
207
|
if (reason !== "app-quit") await finalizePromise;
|
|
208
208
|
}
|
|
209
209
|
// Keep completed notes in Clips instead of interrupting the user by
|
|
210
|
-
// opening a browser tab.
|
|
211
|
-
//
|
|
210
|
+
// opening a browser tab. On a normal stop the pill stays up and
|
|
211
|
+
// switches to its finished banner off `meetings:transcription-stopped`;
|
|
212
|
+
// it hides itself once the user opens the meeting or dismisses it.
|
|
212
213
|
// Guard the shared Rust-side state writes and sessionRef null-out by
|
|
213
214
|
// identity. App quit and other callers can still race a stop against a
|
|
214
215
|
// new start that slips in between awaits, and stale teardown must not
|
|
215
216
|
// clobber the session that has since taken over.
|
|
216
217
|
if (sessionRef.current === session) {
|
|
217
|
-
|
|
218
|
+
if (reason === "app-quit" || reason === "replaced") {
|
|
219
|
+
await invoke("recording_pill_hide").catch(() => {});
|
|
220
|
+
}
|
|
218
221
|
await invoke("set_recording_state", { active: false }).catch(
|
|
219
222
|
() => {},
|
|
220
223
|
);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
IconPlayerPauseFilled,
|
|
10
10
|
IconPlayerPlayFilled,
|
|
11
11
|
IconPlayerStopFilled,
|
|
12
|
+
IconX,
|
|
12
13
|
} from "@tabler/icons-react";
|
|
13
14
|
import { invoke } from "@tauri-apps/api/core";
|
|
14
15
|
import { emit, listen } from "@tauri-apps/api/event";
|
|
@@ -46,6 +47,10 @@ export function RecordingPill() {
|
|
|
46
47
|
const [ctx, setCtx] = useState<PillContext>({ mode: "clip" });
|
|
47
48
|
const ctxRef = useRef<PillContext>({ mode: "clip" });
|
|
48
49
|
const [stopping, setStopping] = useState(false);
|
|
50
|
+
const [finishedMeetingId, setFinishedMeetingId] = useState<string | null>(
|
|
51
|
+
null,
|
|
52
|
+
);
|
|
53
|
+
const finished = finishedMeetingId !== null;
|
|
49
54
|
const [error, setError] = useState<string | null>(null);
|
|
50
55
|
const transcriptLinesRef = useRef<FinalLine[]>([]);
|
|
51
56
|
const [hasTranscriptLines, setHasTranscriptLines] = useState(false);
|
|
@@ -113,6 +118,7 @@ export function RecordingPill() {
|
|
|
113
118
|
// new recording session begins, otherwise the Stop button stays
|
|
114
119
|
// disabled and a stale fallback timer can fire mid-session.
|
|
115
120
|
setStopping(false);
|
|
121
|
+
setFinishedMeetingId(null);
|
|
116
122
|
setError(null);
|
|
117
123
|
setExpanded(false);
|
|
118
124
|
// Reset transcript state for the new session.
|
|
@@ -146,6 +152,21 @@ export function RecordingPill() {
|
|
|
146
152
|
if (lines?.length) setPreloadedLines(lines);
|
|
147
153
|
}),
|
|
148
154
|
);
|
|
155
|
+
trackListen(
|
|
156
|
+
listen<{ meetingId?: string | null; reason?: string }>(
|
|
157
|
+
"meetings:transcription-stopped",
|
|
158
|
+
(ev) => {
|
|
159
|
+
const reason = ev.payload?.reason;
|
|
160
|
+
// "replaced" hands straight over to the next session and "app-quit"
|
|
161
|
+
// is tearing the window down — neither has a user left to read this.
|
|
162
|
+
if (reason === "replaced" || reason === "app-quit") return;
|
|
163
|
+
if (ctxRef.current.mode !== "meeting") return;
|
|
164
|
+
const meetingId = ev.payload?.meetingId ?? activeMeetingIdRef.current;
|
|
165
|
+
if (!meetingId) return;
|
|
166
|
+
showFinished(meetingId);
|
|
167
|
+
},
|
|
168
|
+
),
|
|
169
|
+
);
|
|
149
170
|
trackListen(
|
|
150
171
|
listen<{ error: string }>("pill:error", (ev) => {
|
|
151
172
|
setError(ev.payload?.error ?? "An error occurred.");
|
|
@@ -187,7 +208,7 @@ export function RecordingPill() {
|
|
|
187
208
|
useEffect(() => {
|
|
188
209
|
// Clip recordings already broadcast their pause-aware elapsed time every
|
|
189
210
|
// 500ms. Keep the local wall clock only for meeting mode.
|
|
190
|
-
if (paused || ctx.mode === "clip") return;
|
|
211
|
+
if (paused || finished || ctx.mode === "clip") return;
|
|
191
212
|
tickRef.current = setInterval(() => {
|
|
192
213
|
setElapsed(Math.floor((Date.now() - startedAtRef.current) / 1000));
|
|
193
214
|
}, 500);
|
|
@@ -195,7 +216,7 @@ export function RecordingPill() {
|
|
|
195
216
|
if (tickRef.current) clearInterval(tickRef.current);
|
|
196
217
|
tickRef.current = null;
|
|
197
218
|
};
|
|
198
|
-
}, [ctx.mode, paused]);
|
|
219
|
+
}, [ctx.mode, finished, paused]);
|
|
199
220
|
|
|
200
221
|
async function toggleExpanded() {
|
|
201
222
|
const next = !expanded;
|
|
@@ -215,12 +236,32 @@ export function RecordingPill() {
|
|
|
215
236
|
);
|
|
216
237
|
}
|
|
217
238
|
|
|
239
|
+
function showFinished(meetingId: string) {
|
|
240
|
+
if (stopFallbackRef.current) {
|
|
241
|
+
clearTimeout(stopFallbackRef.current);
|
|
242
|
+
stopFallbackRef.current = null;
|
|
243
|
+
}
|
|
244
|
+
setStopping(false);
|
|
245
|
+
setFinishedMeetingId(meetingId);
|
|
246
|
+
setExpanded(true);
|
|
247
|
+
invoke("recording_pill_expand", { expanded: true }).catch(() => {});
|
|
248
|
+
}
|
|
249
|
+
|
|
218
250
|
async function onStopClick() {
|
|
219
251
|
if (stopping) return;
|
|
220
|
-
|
|
252
|
+
const meetingId = ctx.meetingId ?? activeMeetingIdRef.current;
|
|
221
253
|
emit("clips:pill-stop", { meetingId: ctx.meetingId ?? null }).catch(
|
|
222
254
|
() => {},
|
|
223
255
|
);
|
|
256
|
+
// Meetings keep the pill up and switch it to the finished banner right
|
|
257
|
+
// away. Teardown (final flush, finalize) runs for seconds afterwards, so
|
|
258
|
+
// waiting on `meetings:transcription-stopped` would leave the pill looking
|
|
259
|
+
// stuck; the banner is what the user acts on, not the save.
|
|
260
|
+
if (ctxRef.current.mode === "meeting" && meetingId) {
|
|
261
|
+
showFinished(meetingId);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
setStopping(true);
|
|
224
265
|
stopFallbackRef.current = setTimeout(() => {
|
|
225
266
|
invoke("recording_pill_hide").catch(() => {});
|
|
226
267
|
}, 3_000);
|
|
@@ -288,6 +329,15 @@ export function RecordingPill() {
|
|
|
288
329
|
});
|
|
289
330
|
};
|
|
290
331
|
|
|
332
|
+
const closeFinished = (openMeeting: boolean) => {
|
|
333
|
+
if (openMeeting && finishedMeetingId) {
|
|
334
|
+
emit("clips:open-meeting", { meetingId: finishedMeetingId }).catch(
|
|
335
|
+
() => {},
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
invoke("recording_pill_hide").catch(() => {});
|
|
339
|
+
};
|
|
340
|
+
|
|
291
341
|
const mm = String(Math.floor(elapsed / 60)).padStart(2, "0");
|
|
292
342
|
const ss = String(elapsed % 60).padStart(2, "0");
|
|
293
343
|
const stopLabel =
|
|
@@ -323,7 +373,7 @@ export function RecordingPill() {
|
|
|
323
373
|
<span className="pill-timer">
|
|
324
374
|
{mm}:{ss}
|
|
325
375
|
</span>
|
|
326
|
-
{expanded ? (
|
|
376
|
+
{expanded && !finished ? (
|
|
327
377
|
<button
|
|
328
378
|
type="button"
|
|
329
379
|
onClick={onPauseClick}
|
|
@@ -339,34 +389,49 @@ export function RecordingPill() {
|
|
|
339
389
|
)}
|
|
340
390
|
</button>
|
|
341
391
|
) : null}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
392
|
+
{!finished ? (
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
onClick={onStopClick}
|
|
396
|
+
disabled={stopping}
|
|
397
|
+
data-no-drag
|
|
398
|
+
className="pill-stop-btn"
|
|
399
|
+
aria-label={stopping ? "Stopping" : stopLabel}
|
|
400
|
+
title={stopping ? "Stopping..." : stopLabel}
|
|
401
|
+
>
|
|
402
|
+
{stopping ? (
|
|
403
|
+
<IconLoader2 className="pill-spinner" size={14} />
|
|
404
|
+
) : (
|
|
405
|
+
<IconPlayerStopFilled size={14} />
|
|
406
|
+
)}
|
|
407
|
+
</button>
|
|
408
|
+
) : null}
|
|
409
|
+
{finished ? (
|
|
410
|
+
<button
|
|
411
|
+
type="button"
|
|
412
|
+
onClick={() => closeFinished(false)}
|
|
413
|
+
data-no-drag
|
|
414
|
+
className="pill-close-btn"
|
|
415
|
+
aria-label="Dismiss"
|
|
416
|
+
title="Dismiss"
|
|
417
|
+
>
|
|
418
|
+
<IconX size={16} />
|
|
419
|
+
</button>
|
|
420
|
+
) : (
|
|
421
|
+
<button
|
|
422
|
+
type="button"
|
|
423
|
+
onClick={toggleExpanded}
|
|
424
|
+
data-no-drag
|
|
425
|
+
className="pill-expand-btn"
|
|
426
|
+
aria-label={expanded ? "Collapse" : "Expand"}
|
|
427
|
+
>
|
|
428
|
+
{expanded ? (
|
|
429
|
+
<IconChevronUp size={16} />
|
|
430
|
+
) : (
|
|
431
|
+
<IconChevronDown size={16} />
|
|
432
|
+
)}
|
|
433
|
+
</button>
|
|
434
|
+
)}
|
|
370
435
|
</div>
|
|
371
436
|
</div>
|
|
372
437
|
|
|
@@ -389,6 +454,21 @@ export function RecordingPill() {
|
|
|
389
454
|
}
|
|
390
455
|
>
|
|
391
456
|
<div className="pill-divider" />
|
|
457
|
+
{finished ? (
|
|
458
|
+
<div className="pill-finished-banner" role="status">
|
|
459
|
+
<span className="pill-finished-text">
|
|
460
|
+
Meeting finished — notes are ready
|
|
461
|
+
</span>
|
|
462
|
+
<button
|
|
463
|
+
type="button"
|
|
464
|
+
data-no-drag
|
|
465
|
+
className="pill-finished-open"
|
|
466
|
+
onClick={() => closeFinished(true)}
|
|
467
|
+
>
|
|
468
|
+
Open meeting
|
|
469
|
+
</button>
|
|
470
|
+
</div>
|
|
471
|
+
) : null}
|
|
392
472
|
<div className="pill-transcript-area">
|
|
393
473
|
<div className="pill-pane-label pill-pane-label-row">
|
|
394
474
|
<span>Transcript</span>
|