@agent-native/core 0.114.13 → 0.114.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/docs/content/deployment.mdx +1 -1
- package/corpus/core/docs/content/frames.mdx +1 -1
- package/corpus/core/docs/content/integrations.mdx +27 -27
- package/corpus/core/docs/content/pr-visual-recap.mdx +17 -13
- package/corpus/core/docs/content/workspace-management.mdx +1 -1
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/agent/engine/builder-engine.ts +4 -0
- package/corpus/core/src/client/AgentPanel.tsx +10 -4
- package/corpus/core/src/client/ConnectBuilderCard.tsx +5 -1
- package/corpus/core/src/client/chat/markdown-renderer.tsx +3 -5
- package/corpus/core/src/client/error-format.ts +2 -1
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +2 -18
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +43 -32
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +4 -2
- package/corpus/core/src/client/settings/BackgroundAgentSection.tsx +5 -1
- package/corpus/core/src/client/settings/SettingsPanel.tsx +4 -4
- package/corpus/core/src/client/settings/useBuilderStatus.ts +3 -0
- package/corpus/core/src/localization/default-messages.ts +1 -7
- package/corpus/core/src/server/builder-browser.ts +4 -0
- package/corpus/core/src/server/builder-design-systems.ts +6 -1
- package/corpus/core/src/server/fusion-app.ts +5 -1
- package/corpus/core/src/shared/builder-link-tracking.ts +41 -0
- package/corpus/core/src/shared/index.ts +1 -0
- package/corpus/templates/analytics/AGENTS.md +10 -1
- package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +10 -2
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +5 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +20 -32
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +62 -0
- package/corpus/templates/analytics/changelog/2026-07-21-chart-series-controls-stay-open-while-moving-from-the-legend.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-21-dashboard-email-captures-complete-large-dashboards-in-chunks.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +4 -4
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -8
- package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +39 -7
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +341 -251
- package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +114 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +84 -6
- package/corpus/templates/analytics/server/plugins/db.ts +13 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +10 -4
- package/corpus/templates/clips/changelog/2026-07-21-choose-which-whisper-model-to-use-in-settings.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-21-fixed-organization-recording-visibility-default.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-21-meeting-notes-stop-when-calls-end.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +66 -75
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +2 -0
- package/corpus/templates/clips/desktop/src/hooks/useWhisperSettings.ts +172 -0
- package/corpus/templates/clips/desktop/src/lib/meeting-call-app.ts +41 -0
- package/corpus/templates/clips/desktop/src/lib/silence-events.ts +1 -0
- package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
- package/corpus/templates/clips/desktop/src/styles.css +80 -7
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +32 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +268 -20
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_model.rs +285 -164
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +18 -7
- package/corpus/templates/clips/learnings.defaults.md +1 -1
- package/corpus/templates/clips/server/plugins/db.ts +11 -0
- package/corpus/templates/clips/shared/builder-credits.ts +1 -1
- package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +9 -1
- package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +5 -1
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +9 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +9 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +4 -0
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +10 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +5 -1
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
- package/dist/client/chat/markdown-renderer.js +3 -2
- package/dist/client/chat/markdown-renderer.js.map +1 -1
- package/dist/client/error-format.d.ts +1 -1
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +1 -1
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.js +3 -4
- package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +21 -7
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +2 -2
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/settings/BackgroundAgentSection.d.ts.map +1 -1
- package/dist/client/settings/BackgroundAgentSection.js +5 -1
- package/dist/client/settings/BackgroundAgentSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +2 -0
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +0 -6
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +1 -7
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +4 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/builder-design-systems.d.ts.map +1 -1
- package/dist/server/builder-design-systems.js +6 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/fusion-app.d.ts.map +1 -1
- package/dist/server/fusion-app.js +2 -1
- package/dist/server/fusion-app.js.map +1 -1
- package/dist/shared/builder-link-tracking.d.ts +14 -0
- package/dist/shared/builder-link-tracking.d.ts.map +1 -0
- package/dist/shared/builder-link-tracking.js +26 -0
- package/dist/shared/builder-link-tracking.js.map +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +1 -0
- package/dist/shared/index.js.map +1 -1
- package/docs/content/deployment.mdx +1 -1
- package/docs/content/frames.mdx +1 -1
- package/docs/content/integrations.mdx +27 -27
- package/docs/content/pr-visual-recap.mdx +17 -13
- package/docs/content/workspace-management.mdx +1 -1
- package/package.json +3 -2
- package/src/agent/engine/builder-engine.ts +4 -0
- package/src/client/AgentPanel.tsx +10 -4
- package/src/client/ConnectBuilderCard.tsx +5 -1
- package/src/client/chat/markdown-renderer.tsx +3 -5
- package/src/client/error-format.ts +2 -1
- package/src/client/index.ts +1 -0
- package/src/client/integrations/IntegrationsPanel.tsx +2 -18
- package/src/client/resources/McpIntegrationDialog.tsx +43 -32
- package/src/client/resources/mcp-integration-catalog.ts +4 -2
- package/src/client/settings/BackgroundAgentSection.tsx +5 -1
- package/src/client/settings/SettingsPanel.tsx +4 -4
- package/src/client/settings/useBuilderStatus.ts +3 -0
- package/src/localization/default-messages.ts +1 -7
- package/src/server/builder-browser.ts +4 -0
- package/src/server/builder-design-systems.ts +6 -1
- package/src/server/fusion-app.ts +5 -1
- package/src/shared/builder-link-tracking.ts +41 -0
- package/src/shared/index.ts +1 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { recordChange } from "@agent-native/core/server";
|
|
4
|
+
import { and, desc, eq } from "drizzle-orm";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
FIRST_PARTY_DASHBOARD_ID,
|
|
9
|
+
repairFirstPartyRecurringUserPanels,
|
|
10
|
+
} from "./first-party-metric-catalog";
|
|
11
|
+
|
|
12
|
+
export async function repairPersistedFirstPartyDashboardQueries(): Promise<boolean> {
|
|
13
|
+
// guard:allow-unscoped — startup repair targets one fixed canonical dashboard
|
|
14
|
+
// and only replaces the exact shipped legacy SQL under an optimistic fence.
|
|
15
|
+
const db = getDb() as any;
|
|
16
|
+
const [row] = await db
|
|
17
|
+
.select({
|
|
18
|
+
id: schema.dashboards.id,
|
|
19
|
+
config: schema.dashboards.config,
|
|
20
|
+
kind: schema.dashboards.kind,
|
|
21
|
+
title: schema.dashboards.title,
|
|
22
|
+
updatedAt: schema.dashboards.updatedAt,
|
|
23
|
+
ownerEmail: schema.dashboards.ownerEmail,
|
|
24
|
+
orgId: schema.dashboards.orgId,
|
|
25
|
+
visibility: schema.dashboards.visibility,
|
|
26
|
+
})
|
|
27
|
+
.from(schema.dashboards)
|
|
28
|
+
.where(eq(schema.dashboards.id, FIRST_PARTY_DASHBOARD_ID));
|
|
29
|
+
if (!row || row.kind !== "sql" || typeof row.config !== "string") {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let config: Record<string, unknown>;
|
|
34
|
+
try {
|
|
35
|
+
config = JSON.parse(row.config) as Record<string, unknown>;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const repaired = repairFirstPartyRecurringUserPanels(config);
|
|
40
|
+
if (!repaired.changed) return false;
|
|
41
|
+
|
|
42
|
+
const repairedAt = new Date().toISOString();
|
|
43
|
+
const revisionId = `dashrev-${Date.now()}-${randomUUID()}`;
|
|
44
|
+
const changed = await db.transaction(async (tx: any) => {
|
|
45
|
+
const updated = await tx
|
|
46
|
+
.update(schema.dashboards)
|
|
47
|
+
.set({
|
|
48
|
+
config: JSON.stringify(repaired.config),
|
|
49
|
+
updatedAt: repairedAt,
|
|
50
|
+
updatedBy: null,
|
|
51
|
+
})
|
|
52
|
+
.where(
|
|
53
|
+
and(
|
|
54
|
+
eq(schema.dashboards.id, FIRST_PARTY_DASHBOARD_ID),
|
|
55
|
+
eq(schema.dashboards.config, row.config),
|
|
56
|
+
eq(schema.dashboards.updatedAt, row.updatedAt),
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
.returning({ id: schema.dashboards.id });
|
|
60
|
+
if (updated.length !== 1) return false;
|
|
61
|
+
|
|
62
|
+
await tx.insert(schema.dashboardRevisions).values({
|
|
63
|
+
id: revisionId,
|
|
64
|
+
dashboardId: row.id,
|
|
65
|
+
kind: row.kind,
|
|
66
|
+
title: row.title,
|
|
67
|
+
config: row.config,
|
|
68
|
+
createdAt: repairedAt,
|
|
69
|
+
createdBy: null,
|
|
70
|
+
ownerEmail: row.ownerEmail,
|
|
71
|
+
orgId: row.orgId,
|
|
72
|
+
});
|
|
73
|
+
const revisions = await tx
|
|
74
|
+
.select({ id: schema.dashboardRevisions.id })
|
|
75
|
+
.from(schema.dashboardRevisions)
|
|
76
|
+
.where(eq(schema.dashboardRevisions.dashboardId, row.id))
|
|
77
|
+
.orderBy(
|
|
78
|
+
desc(schema.dashboardRevisions.createdAt),
|
|
79
|
+
desc(schema.dashboardRevisions.id),
|
|
80
|
+
);
|
|
81
|
+
const retainedRevisionIds = new Set([revisionId]);
|
|
82
|
+
for (const revision of revisions) {
|
|
83
|
+
if (retainedRevisionIds.size >= 50) break;
|
|
84
|
+
retainedRevisionIds.add(revision.id);
|
|
85
|
+
}
|
|
86
|
+
for (const revision of revisions) {
|
|
87
|
+
if (retainedRevisionIds.has(revision.id)) continue;
|
|
88
|
+
await tx
|
|
89
|
+
.delete(schema.dashboardRevisions)
|
|
90
|
+
.where(eq(schema.dashboardRevisions.id, revision.id));
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
});
|
|
94
|
+
if (!changed) return false;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
recordChange({
|
|
98
|
+
source: "dashboards",
|
|
99
|
+
type: "change",
|
|
100
|
+
key: row.id,
|
|
101
|
+
...(row.visibility === "public"
|
|
102
|
+
? {}
|
|
103
|
+
: row.visibility === "org" && row.orgId
|
|
104
|
+
? { orgId: row.orgId }
|
|
105
|
+
: { owner: row.ownerEmail }),
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.warn(
|
|
109
|
+
"[db] Canonical dashboard repair committed without a live change event:",
|
|
110
|
+
err instanceof Error ? err.message : err,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
export type MetricWindow = "30d" | "90d" | "all";
|
|
28
28
|
|
|
29
|
+
export const FIRST_PARTY_DASHBOARD_ID = "agent-native-templates-first-party";
|
|
30
|
+
|
|
29
31
|
export interface FirstPartyMetric {
|
|
30
32
|
/** Stable catalog key, also used as the default panel id. */
|
|
31
33
|
key: string;
|
|
@@ -122,6 +124,7 @@ const USER_KEY_SQL = "NULLIF(user_key, '')";
|
|
|
122
124
|
const RETENTION_ROLLING_DAYS = 7;
|
|
123
125
|
const RETENTION_MIN_COHORT_SIZE = 5;
|
|
124
126
|
const PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE = 20;
|
|
127
|
+
const RECURRING_ACTIVITY_LOOKBACK_DAYS = 365;
|
|
125
128
|
|
|
126
129
|
function daysAgoSql(days: number): string {
|
|
127
130
|
const unit = days === 1 ? "day" : "days";
|
|
@@ -223,8 +226,85 @@ const RETENTION_OVER_TIME_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_S
|
|
|
223
226
|
const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(7)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date > c.cohort_date AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
|
|
224
227
|
const SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date >= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
|
|
225
228
|
const SIGNUPS_BY_TEMPLATE_SQL = `SELECT ${TEMPLATE_EXPR} AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${TEMPLATE_EXPR} ORDER BY count DESC`;
|
|
226
|
-
const
|
|
227
|
-
const
|
|
229
|
+
export const LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
|
|
230
|
+
export const LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
|
|
231
|
+
const RECURRING_ACTIVITY_LOOKBACK_FILTER = `${EVENT_DATE_SQL} >= ${daysAgoSql(RECURRING_ACTIVITY_LOOKBACK_DAYS)}`;
|
|
232
|
+
const RECURRING_USERS_BY_TEMPLATE_SQL =
|
|
233
|
+
LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL.replace(
|
|
234
|
+
`${DASHBOARD_EMAIL_FILTER}), first_seen`,
|
|
235
|
+
`${DASHBOARD_EMAIL_FILTER} AND ${RECURRING_ACTIVITY_LOOKBACK_FILTER}), first_seen`,
|
|
236
|
+
);
|
|
237
|
+
const RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL =
|
|
238
|
+
LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL.replace(
|
|
239
|
+
`${DASHBOARD_EMAIL_FILTER}), first_seen`,
|
|
240
|
+
`${DASHBOARD_EMAIL_FILTER} AND ${RECURRING_ACTIVITY_LOOKBACK_FILTER}), first_seen`,
|
|
241
|
+
);
|
|
242
|
+
const LEGACY_RECURRING_USERS_DESCRIPTION =
|
|
243
|
+
"Daily signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that day. Docs traffic and unknown template are excluded.";
|
|
244
|
+
const LEGACY_RECURRING_USERS_WEEKLY_DESCRIPTION =
|
|
245
|
+
"Weekly distinct signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that week. Weeks start Monday; docs traffic and unknown template are excluded.";
|
|
246
|
+
const RECURRING_USERS_DESCRIPTION =
|
|
247
|
+
"Daily signed-in visitors who are not on their first active day observed in the previous 365 days, stacked by inferred template/app used that day. Docs traffic and unknown template are excluded.";
|
|
248
|
+
const RECURRING_USERS_WEEKLY_DESCRIPTION =
|
|
249
|
+
"Weekly distinct signed-in visitors who are not on their first active day observed in the previous 365 days, stacked by inferred template/app used that week. Weeks start Monday; docs traffic and unknown template are excluded.";
|
|
250
|
+
|
|
251
|
+
export function repairFirstPartyRecurringUserPanels(
|
|
252
|
+
config: Record<string, unknown>,
|
|
253
|
+
): { config: Record<string, unknown>; changed: boolean } {
|
|
254
|
+
if (!Array.isArray(config.panels)) return { config, changed: false };
|
|
255
|
+
|
|
256
|
+
const replacements = new Map([
|
|
257
|
+
[
|
|
258
|
+
"recurring-users-by-template",
|
|
259
|
+
{
|
|
260
|
+
legacySql: LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL,
|
|
261
|
+
sql: RECURRING_USERS_BY_TEMPLATE_SQL,
|
|
262
|
+
legacyDescription: LEGACY_RECURRING_USERS_DESCRIPTION,
|
|
263
|
+
description: RECURRING_USERS_DESCRIPTION,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
[
|
|
267
|
+
"recurring-users-by-template-bar",
|
|
268
|
+
{
|
|
269
|
+
legacySql: LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
|
|
270
|
+
sql: RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
|
|
271
|
+
legacyDescription: LEGACY_RECURRING_USERS_WEEKLY_DESCRIPTION,
|
|
272
|
+
description: RECURRING_USERS_WEEKLY_DESCRIPTION,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
]);
|
|
276
|
+
let changed = false;
|
|
277
|
+
const panels = config.panels.map((rawPanel) => {
|
|
278
|
+
if (!rawPanel || typeof rawPanel !== "object") return rawPanel;
|
|
279
|
+
const panel = rawPanel as Record<string, unknown>;
|
|
280
|
+
const replacement =
|
|
281
|
+
typeof panel.id === "string" ? replacements.get(panel.id) : undefined;
|
|
282
|
+
if (!replacement || panel.sql !== replacement.legacySql) return rawPanel;
|
|
283
|
+
|
|
284
|
+
changed = true;
|
|
285
|
+
const panelConfig =
|
|
286
|
+
panel.config && typeof panel.config === "object"
|
|
287
|
+
? (panel.config as Record<string, unknown>)
|
|
288
|
+
: null;
|
|
289
|
+
return {
|
|
290
|
+
...panel,
|
|
291
|
+
sql: replacement.sql,
|
|
292
|
+
...(panelConfig
|
|
293
|
+
? {
|
|
294
|
+
config: {
|
|
295
|
+
...panelConfig,
|
|
296
|
+
...(panelConfig.description === replacement.legacyDescription
|
|
297
|
+
? { description: replacement.description }
|
|
298
|
+
: {}),
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
: {}),
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
return changed
|
|
305
|
+
? { config: { ...config, panels }, changed }
|
|
306
|
+
: { config, changed };
|
|
307
|
+
}
|
|
228
308
|
const DAU_BY_TEMPLATE_SQL = `SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template, COUNT(DISTINCT ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS visitors FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, ${TEMPLATE_EXPR} ORDER BY date, template`;
|
|
229
309
|
const WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS visitor_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_EVENT_DATE_RANGE_FILTER}) SELECT d.date, b.template, COUNT(DISTINCT b.visitor_key) AS visitors FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
|
|
230
310
|
|
|
@@ -684,8 +764,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
684
764
|
valueKey: "users",
|
|
685
765
|
},
|
|
686
766
|
stacked: true,
|
|
687
|
-
description:
|
|
688
|
-
"Daily signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that day. Docs traffic and unknown template are excluded.",
|
|
767
|
+
description: RECURRING_USERS_DESCRIPTION,
|
|
689
768
|
},
|
|
690
769
|
},
|
|
691
770
|
{
|
|
@@ -706,8 +785,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
706
785
|
valueKey: "users",
|
|
707
786
|
},
|
|
708
787
|
stacked: true,
|
|
709
|
-
description:
|
|
710
|
-
"Weekly distinct signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that week. Weeks start Monday; docs traffic and unknown template are excluded.",
|
|
788
|
+
description: RECURRING_USERS_WEEKLY_DESCRIPTION,
|
|
711
789
|
},
|
|
712
790
|
},
|
|
713
791
|
{
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
// startup so the dashboard / analysis share actions know where to dispatch.
|
|
9
9
|
import "../db/index.js";
|
|
10
10
|
import * as schema from "../db/schema.js";
|
|
11
|
+
import { repairPersistedFirstPartyDashboardQueries } from "../lib/first-party-dashboard-repair.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Every Drizzle table exported from schema.ts. Filters out type-only and
|
|
@@ -1280,6 +1281,18 @@ const runAnalyticsMigrations = runMigrations(
|
|
|
1280
1281
|
*/
|
|
1281
1282
|
export default async (nitroApp: any): Promise<void> => {
|
|
1282
1283
|
await runAnalyticsMigrations(nitroApp);
|
|
1284
|
+
try {
|
|
1285
|
+
if (await repairPersistedFirstPartyDashboardQueries()) {
|
|
1286
|
+
console.info(
|
|
1287
|
+
"[db] Repaired bounded recurring-user queries on the canonical first-party dashboard.",
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
console.warn(
|
|
1292
|
+
"[db] Failed to repair canonical first-party dashboard queries (non-fatal):",
|
|
1293
|
+
err instanceof Error ? err.message : err,
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1283
1296
|
try {
|
|
1284
1297
|
const summary = await ensureAdditiveColumns({
|
|
1285
1298
|
db: getDbExec(),
|
|
@@ -646,10 +646,10 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
646
646
|
const togglePlayback = useCallback(() => {
|
|
647
647
|
const v = videoRef.current;
|
|
648
648
|
if (!v) return;
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
649
|
+
// A finished clip must always replay from the start, even when the browser
|
|
650
|
+
// left `paused` false at end of stream (MSE end-of-stream / DB-duration
|
|
651
|
+
// mismatch) or `isPlaying` is stale — otherwise the toggle below would
|
|
652
|
+
// pause an already-ended element and the play button appears to do nothing.
|
|
653
653
|
if (v.ended) {
|
|
654
654
|
try {
|
|
655
655
|
v.currentTime = 0;
|
|
@@ -657,6 +657,12 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
657
657
|
} catch {
|
|
658
658
|
// Let the normal play attempt report a media error if the seek fails.
|
|
659
659
|
}
|
|
660
|
+
requestPlay();
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (!v.paused || isPlaying) {
|
|
664
|
+
pauseVideo();
|
|
665
|
+
return;
|
|
660
666
|
}
|
|
661
667
|
requestPlay();
|
|
662
668
|
}, [isPlaying, pauseVideo, requestPlay]);
|
|
@@ -51,6 +51,7 @@ import { UpdateBanner } from "./components/UpdateBanner";
|
|
|
51
51
|
import { useMediaDevices } from "./hooks/useMediaDevices";
|
|
52
52
|
import { useMeetingTranscription } from "./hooks/useMeetingTranscription";
|
|
53
53
|
import { stopAllMicMeters } from "./hooks/useMicMeter";
|
|
54
|
+
import { useWhisperSettings } from "./hooks/useWhisperSettings";
|
|
54
55
|
import { startBubbleFramePump } from "./lib/bubble-pump";
|
|
55
56
|
import {
|
|
56
57
|
startBubbleWebrtc,
|
|
@@ -4940,17 +4941,20 @@ function Setup({
|
|
|
4940
4941
|
featureConfig?.meetingTranscriptionMode ?? "ask";
|
|
4941
4942
|
const showMeetingWidgetEnabled =
|
|
4942
4943
|
featureConfig?.showMeetingWidgetEnabled !== false;
|
|
4943
|
-
const
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
downloadedMb: number;
|
|
4949
|
-
totalMb: number;
|
|
4950
|
-
}
|
|
4951
|
-
const [whisperStatus, setWhisperStatus] = useState<WhisperModelStatus | null>(
|
|
4952
|
-
null,
|
|
4944
|
+
const whisper = useWhisperSettings(
|
|
4945
|
+
featureConfig,
|
|
4946
|
+
voiceProvider,
|
|
4947
|
+
onVoiceProviderChange,
|
|
4948
|
+
nativeVoiceProvider,
|
|
4953
4949
|
);
|
|
4950
|
+
const {
|
|
4951
|
+
catalog: whisperModels,
|
|
4952
|
+
status: whisperStatus,
|
|
4953
|
+
enabled: whisperModelEnabled,
|
|
4954
|
+
modelId: whisperModelId,
|
|
4955
|
+
selectedModel: selectedWhisperModel,
|
|
4956
|
+
deletableModels,
|
|
4957
|
+
} = whisper;
|
|
4954
4958
|
const [screenMemoryStatus, setScreenMemoryStatus] =
|
|
4955
4959
|
useState<ScreenMemoryStatus | null>(null);
|
|
4956
4960
|
const screenMemoryStatusRefreshVersionRef = useRef(0);
|
|
@@ -5059,24 +5063,6 @@ function Setup({
|
|
|
5059
5063
|
);
|
|
5060
5064
|
}
|
|
5061
5065
|
|
|
5062
|
-
function triggerWhisperDownload() {
|
|
5063
|
-
invoke("whisper_model_download").catch(() => {});
|
|
5064
|
-
}
|
|
5065
|
-
|
|
5066
|
-
function setWhisperModelEnabled(enabled: boolean) {
|
|
5067
|
-
if (!featureConfig) return;
|
|
5068
|
-
invoke("set_feature_config", {
|
|
5069
|
-
config: { ...featureConfig, whisperModelEnabled: enabled },
|
|
5070
|
-
}).catch((err) =>
|
|
5071
|
-
console.error("[settings] set_feature_config failed", err),
|
|
5072
|
-
);
|
|
5073
|
-
if (enabled) {
|
|
5074
|
-
triggerWhisperDownload();
|
|
5075
|
-
} else if (voiceProvider === "whisper") {
|
|
5076
|
-
onVoiceProviderChange(nativeVoiceProvider());
|
|
5077
|
-
}
|
|
5078
|
-
}
|
|
5079
|
-
|
|
5080
5066
|
function setLaunchAtLoginEnabled(enabled: boolean) {
|
|
5081
5067
|
if (!featureConfig) return;
|
|
5082
5068
|
invoke("set_feature_config", {
|
|
@@ -5480,47 +5466,6 @@ function Setup({
|
|
|
5480
5466
|
};
|
|
5481
5467
|
}, [refreshScreenMemoryStatus]);
|
|
5482
5468
|
|
|
5483
|
-
// Load model status on mount and keep it current via events.
|
|
5484
|
-
useEffect(() => {
|
|
5485
|
-
let cancelled = false;
|
|
5486
|
-
const refresh = () => {
|
|
5487
|
-
invoke<WhisperModelStatus>("whisper_model_status")
|
|
5488
|
-
.then((s) => {
|
|
5489
|
-
if (!cancelled) setWhisperStatus(s);
|
|
5490
|
-
})
|
|
5491
|
-
.catch(() => {});
|
|
5492
|
-
};
|
|
5493
|
-
refresh();
|
|
5494
|
-
const unlistens: Array<() => void> = [];
|
|
5495
|
-
const track = (p: Promise<() => void>) => {
|
|
5496
|
-
p.then((u) => {
|
|
5497
|
-
if (cancelled) {
|
|
5498
|
-
try {
|
|
5499
|
-
u();
|
|
5500
|
-
} catch {
|
|
5501
|
-
/* ignore */
|
|
5502
|
-
}
|
|
5503
|
-
return;
|
|
5504
|
-
}
|
|
5505
|
-
unlistens.push(u);
|
|
5506
|
-
}).catch(() => {});
|
|
5507
|
-
};
|
|
5508
|
-
track(listen("whisper:model-progress", () => refresh()));
|
|
5509
|
-
track(listen("whisper:model-ready", () => refresh()));
|
|
5510
|
-
track(listen("whisper:model-error", () => refresh()));
|
|
5511
|
-
track(listen("whisper:model-enabled-changed", () => refresh()));
|
|
5512
|
-
return () => {
|
|
5513
|
-
cancelled = true;
|
|
5514
|
-
unlistens.forEach((u) => {
|
|
5515
|
-
try {
|
|
5516
|
-
u();
|
|
5517
|
-
} catch {
|
|
5518
|
-
/* ignore */
|
|
5519
|
-
}
|
|
5520
|
-
});
|
|
5521
|
-
};
|
|
5522
|
-
}, []);
|
|
5523
|
-
|
|
5524
5469
|
useEffect(() => {
|
|
5525
5470
|
const base = (serverUrl ?? initial ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
5526
5471
|
let cancelled = false;
|
|
@@ -5606,7 +5551,7 @@ function Setup({
|
|
|
5606
5551
|
onVoiceProviderChange(nativeVoiceProvider());
|
|
5607
5552
|
} else if (mode === "whisper") {
|
|
5608
5553
|
onVoiceProviderChange("whisper");
|
|
5609
|
-
if (!whisperModelEnabled)
|
|
5554
|
+
if (!whisperModelEnabled) whisper.setEnabled(true);
|
|
5610
5555
|
} else if (mode === "builder") {
|
|
5611
5556
|
onVoiceProviderChange("builder-gemini");
|
|
5612
5557
|
} else {
|
|
@@ -7192,15 +7137,58 @@ function Setup({
|
|
|
7192
7137
|
/>
|
|
7193
7138
|
<Switch
|
|
7194
7139
|
on={whisperModelEnabled}
|
|
7195
|
-
onChange={
|
|
7140
|
+
onChange={whisper.setEnabled}
|
|
7196
7141
|
label="Enable Whisper model"
|
|
7197
7142
|
/>
|
|
7198
7143
|
</div>
|
|
7144
|
+
<SettingLabel
|
|
7145
|
+
label="Model"
|
|
7146
|
+
hint="Larger models can improve transcription accuracy but use more storage and may run more slowly."
|
|
7147
|
+
htmlFor="whisper-model"
|
|
7148
|
+
/>
|
|
7149
|
+
<select
|
|
7150
|
+
id="whisper-model"
|
|
7151
|
+
className="setup-select"
|
|
7152
|
+
value={whisperModelId}
|
|
7153
|
+
onChange={(event) => whisper.setModelId(event.target.value)}
|
|
7154
|
+
disabled={
|
|
7155
|
+
whisperModels.length === 0 || whisperStatus?.state === "downloading"
|
|
7156
|
+
}
|
|
7157
|
+
>
|
|
7158
|
+
{whisperModels.map((model) => (
|
|
7159
|
+
<option key={model.id} value={model.id}>
|
|
7160
|
+
{model.title} · {model.sizeMb} MB — {model.description}
|
|
7161
|
+
</option>
|
|
7162
|
+
))}
|
|
7163
|
+
</select>
|
|
7164
|
+
{selectedWhisperModel ? (
|
|
7165
|
+
<p className="setup-hint">{selectedWhisperModel.description}</p>
|
|
7166
|
+
) : null}
|
|
7199
7167
|
<WhisperModelStatusRow
|
|
7200
7168
|
status={whisperStatus}
|
|
7201
7169
|
enabled={whisperModelEnabled}
|
|
7202
|
-
onDownload={
|
|
7170
|
+
onDownload={whisper.triggerDownload}
|
|
7203
7171
|
/>
|
|
7172
|
+
{deletableModels.length > 0 ? (
|
|
7173
|
+
<div className="whisper-other-models">
|
|
7174
|
+
<p className="setup-hint">Other downloaded models</p>
|
|
7175
|
+
{deletableModels.map((model) => (
|
|
7176
|
+
<div key={model.id} className="whisper-other-model-row">
|
|
7177
|
+
<span className="whisper-other-model-name">
|
|
7178
|
+
{model.title} · {model.sizeMb} MB
|
|
7179
|
+
</span>
|
|
7180
|
+
<button
|
|
7181
|
+
type="button"
|
|
7182
|
+
className="whisper-delete-btn"
|
|
7183
|
+
onClick={() => whisper.deleteModel(model.id)}
|
|
7184
|
+
>
|
|
7185
|
+
<IconTrash size={13} />
|
|
7186
|
+
Delete
|
|
7187
|
+
</button>
|
|
7188
|
+
</div>
|
|
7189
|
+
))}
|
|
7190
|
+
</div>
|
|
7191
|
+
) : null}
|
|
7204
7192
|
</div>
|
|
7205
7193
|
|
|
7206
7194
|
<div className="setup-section-heading">Dictation</div>
|
|
@@ -7528,9 +7516,12 @@ function WhisperModelStatusRow({
|
|
|
7528
7516
|
<span className="whisper-progress-label">
|
|
7529
7517
|
Downloading… {status.downloadedMb} / {status.totalMb} MB ({pct}%)
|
|
7530
7518
|
</span>
|
|
7531
|
-
<
|
|
7532
|
-
|
|
7533
|
-
|
|
7519
|
+
<progress
|
|
7520
|
+
className="whisper-progress-bar"
|
|
7521
|
+
value={pct}
|
|
7522
|
+
max={100}
|
|
7523
|
+
aria-label={`Whisper model download ${pct}% complete`}
|
|
7524
|
+
/>
|
|
7534
7525
|
</div>
|
|
7535
7526
|
);
|
|
7536
7527
|
}
|
|
@@ -3,6 +3,7 @@ import { emit, listen } from "@tauri-apps/api/event";
|
|
|
3
3
|
import { open as openExternal } from "@tauri-apps/plugin-shell";
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
5
5
|
|
|
6
|
+
import { callAppBundleIdsForJoinUrl } from "../lib/meeting-call-app";
|
|
6
7
|
import {
|
|
7
8
|
appendFinalTranscript,
|
|
8
9
|
onFinalTranscript,
|
|
@@ -414,6 +415,7 @@ export function useMeetingTranscription({
|
|
|
414
415
|
silenceThreshold: 0.05,
|
|
415
416
|
silenceMs: 15 * 60 * 1000,
|
|
416
417
|
callEndedMs: 2 * 60 * 1000,
|
|
418
|
+
callAppBundleIds: callAppBundleIdsForJoinUrl(payload.joinUrl),
|
|
417
419
|
scheduledEndMs,
|
|
418
420
|
watchSleep: true,
|
|
419
421
|
watchCallEnded: true,
|