@agent-native/core-corpus 0.205.0 → 0.205.1-nightly-20261008213122
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/templates/analytics/changelog/2026-10-08-onboarding-journeys-use-the-current-builder-setup-label.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +1 -1
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +10 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +1 -1
- package/corpus/templates/analytics/server/lib/journey-steps.ts +1 -1
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +24 -4
- package/corpus/templates/clips/changelog/2026-10-08-clips-respects-skip-after-storage-connects-and-gives-accurat.md +6 -0
- package/corpus/templates/slides/.agents/skills/apply-slide-comments/SKILL.md +55 -0
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +34 -10
- package/corpus/templates/slides/.agents/skills/slide-design/SKILL.md +9 -5
- package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +40 -15
- package/corpus/templates/slides/AGENTS.md +3 -2
- package/corpus/templates/slides/agent-native.app-skill.json +5 -0
- package/corpus/templates/slides/app/components/editor/AlignmentGuides.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1129 -863
- package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +2 -4
- package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +14 -73
- package/corpus/templates/slides/app/components/editor/fit-text-object.ts +31 -0
- package/corpus/templates/slides/app/components/editor/in-place-text-session.ts +45 -0
- package/corpus/templates/slides/app/components/editor/selection-overlay-measurement.ts +56 -0
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +371 -44
- package/corpus/templates/slides/app/components/editor/slide-pointer-target.ts +400 -0
- package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +61 -12
- package/corpus/templates/slides/app/context/DeckContext.tsx +12 -4
- package/corpus/templates/slides/app/global.css +95 -24
- package/corpus/templates/slides/changelog/2026-10-07-clicking-dragging-and-selecting-in-the-slide-editor-now-work.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-07-text-boxes-grow-with-their-text-and-the-selection-outline-fo.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-07-the-ai-can-now-plan-slide-height-before-writing-and-apply-co.md +6 -0
- package/corpus/templates/slides/interaction-oracle.md +252 -0
- package/corpus/templates/slides/interaction-parity.md +47 -22
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -859,7 +859,7 @@
|
|
|
859
859
|
"chartType": "table",
|
|
860
860
|
"source": "first-party",
|
|
861
861
|
"width": 3,
|
|
862
|
-
"sql": "WITH auth_identity_bridge AS (\n SELECT linked_email, MIN(auth_user_id) AS auth_user_id\n FROM (\n SELECT lower(COALESCE(\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END\n )) AS linked_email,\n NULLIF(e.properties::jsonb ->> 'auth_user_id', '') AS auth_user_id\n FROM analytics_events e\n WHERE (event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND event_date >= '{{timeRangeStart}}' AND event_date <= '{{timeRangeEnd}}')))\n AND ('{{appFilter}}' IN ('', 'all') OR lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) = lower('{{appFilter}}'))\n AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) IN ('analytics', 'assets', 'brain', 'calendar', 'chat', 'clips', 'content', 'design', 'dispatch', 'forms', 'mail', 'plan', 'slides')\n ) AS identities\n WHERE linked_email IS NOT NULL\n AND auth_user_id IS NOT NULL\n GROUP BY linked_email\n HAVING COUNT(DISTINCT auth_user_id) = 1\n), scoped_onboarding_events AS (\n SELECT e.*,\n COALESCE(\n NULLIF(e.properties::jsonb ->> 'auth_user_id', ''),\n auth_identity_bridge.auth_user_id,\n NULLIF(e.user_key, ''),\n NULLIF(e.user_id, ''),\n NULLIF(e.anonymous_id, '')\n ) AS funnel_user_key,\n COALESCE(\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END,\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.properties::jsonb ->> 'auth_user_id', '') LIKE '%@%.%' THEN e.properties::jsonb ->> 'auth_user_id' END\n ) AS funnel_user_email\n FROM analytics_events e\n LEFT JOIN auth_identity_bridge ON auth_identity_bridge.linked_email = lower(COALESCE(\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END\n ))\n WHERE (event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND event_date >= '{{timeRangeStart}}' AND event_date <= '{{timeRangeEnd}}')))\n AND ('{{appFilter}}' IN ('', 'all') OR lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) = lower('{{appFilter}}'))\n AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) IN ('analytics', 'assets', 'brain', 'calendar', 'chat', 'clips', 'content', 'design', 'dispatch', 'forms', 'mail', 'plan', 'slides')\n), onboarding_events AS (\n SELECT * FROM scoped_onboarding_events\n WHERE ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(funnel_user_email, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(funnel_user_email, '')) LIKE '%@builder.io'))\n AND NOT (strpos(lower(trim(coalesce(funnel_user_email, ''))), '@') > 1 AND (strpos(lower(trim(coalesce(funnel_user_email, ''))), '+autoz') > 0 OR strpos(lower(trim(coalesce(funnel_user_email, ''))), '+qa-test-bot-') > 0 OR left(lower(trim(coalesce(funnel_user_email, ''))), 12) = 'qa-test-bot-' OR left(lower(trim(coalesce(funnel_user_email, ''))), 13) = 'an-e2e-probe-' OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.com') OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.net') OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.org') OR right(lower(trim(coalesce(funnel_user_email, ''))), 5) = '@test' OR right(lower(trim(coalesce(funnel_user_email, ''))), 5) = '.test' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '@invalid' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '.invalid' OR right(lower(trim(coalesce(funnel_user_email, ''))), 10) = '@localhost' OR right(lower(trim(coalesce(funnel_user_email, ''))), 10) = '.localhost' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '@example' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '.example'))\n), choice_viewers AS (\n SELECT DISTINCT funnel_user_key\n FROM onboarding_events\n WHERE event_name = 'onboarding_step_viewed'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), choices AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id,\n timestamp::timestamptz AS clicked_at,\n id\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_clicked'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), ranked_choices AS (\n SELECT choices.*,\n ROW_NUMBER() OVER (PARTITION BY funnel_user_key ORDER BY clicked_at, id) AS choice_number\n FROM choices\n JOIN choice_viewers USING (funnel_user_key)\n), first_choice_summary AS (\n SELECT method_id, COUNT(DISTINCT funnel_user_key) AS first_choice_users\n FROM ranked_choices\n WHERE choice_number = 1\n GROUP BY method_id\n), selection_summary AS (\n SELECT method_id,\n COUNT(DISTINCT funnel_user_key) AS selected_users,\n COUNT(*) AS method_clicks,\n COUNT(DISTINCT attempt_id) AS selection_attempts\n FROM choices\n GROUP BY method_id\n), starts AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_started'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), method_outcomes AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id,\n COALESCE(NULLIF(properties::jsonb ->> 'outcome', ''), 'unknown') AS outcome\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_outcome'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), attempts AS (\n SELECT starts.funnel_user_key, starts.method_id, starts.attempt_id,\n MAX(CASE WHEN method_outcomes.outcome IN ('connected', 'already_connected') THEN 1 ELSE 0 END) AS connected,\n MAX(CASE WHEN method_outcomes.outcome = 'failed' THEN 1 ELSE 0 END) AS failed,\n MAX(CASE WHEN method_outcomes.outcome = 'handoff_failed' THEN 1 ELSE 0 END) AS handoff_failed,\n MAX(CASE WHEN method_outcomes.outcome = 'settings_opened' THEN 1 ELSE 0 END) AS settings_opened,\n MAX(CASE WHEN method_outcomes.outcome IS NOT NULL THEN 1 ELSE 0 END) AS has_outcome\n FROM starts\n LEFT JOIN method_outcomes\n ON method_outcomes.funnel_user_key = starts.funnel_user_key\n AND method_outcomes.method_id = starts.method_id\n AND method_outcomes.attempt_id = starts.attempt_id\n GROUP BY starts.funnel_user_key, starts.method_id, starts.attempt_id\n), attempt_summary AS (\n SELECT method_id,\n COUNT(DISTINCT CASE WHEN connected = 1 THEN attempt_id END) AS connection_success_attempts,\n COUNT(DISTINCT CASE WHEN failed = 1 THEN attempt_id END) AS connection_failure_attempts,\n COUNT(DISTINCT CASE WHEN handoff_failed = 1 THEN attempt_id END) AS handoff_failure_attempts,\n COUNT(DISTINCT CASE WHEN connected = 1 THEN funnel_user_key END) AS connection_success_users,\n COUNT(DISTINCT CASE WHEN failed = 1 THEN funnel_user_key END) AS connection_failure_users,\n COUNT(DISTINCT CASE WHEN connected = 0 AND failed = 0 AND has_outcome = 0 THEN attempt_id END) AS connection_no_outcome_attempts,\n COUNT(DISTINCT CASE WHEN settings_opened = 1 THEN attempt_id END) AS settings_handoff_attempts\n FROM attempts\n GROUP BY method_id\n), method_list AS (\n SELECT 'builder_create_account' AS method_id, 'Create Builder.io account' AS method_label\n UNION ALL SELECT 'builder_sign_in', 'Sign in with Builder.io account'\n UNION ALL SELECT 'custom_keys', 'Configure custom keys'\n)\nSELECT method_list.method_id, method_list.method_label,\n (SELECT COUNT(DISTINCT funnel_user_key) FROM choice_viewers) AS choice_screen_viewers,\n COALESCE(first_choice_summary.first_choice_users, 0) AS first_choice_users,\n COALESCE(first_choice_summary.first_choice_users::float / NULLIF((SELECT COUNT(DISTINCT funnel_user_key) FROM choice_viewers), 0), 0) AS first_choice_rate,\n COALESCE(selection_summary.selected_users, 0) AS selected_users,\n COALESCE(selection_summary.method_clicks, 0) AS method_clicks,\n COALESCE(selection_summary.selection_attempts, 0) AS selection_attempts,\n COALESCE(attempt_summary.connection_success_attempts, 0) AS connection_success_attempts,\n COALESCE(attempt_summary.connection_failure_attempts, 0) AS connection_failure_attempts,\n COALESCE(attempt_summary.handoff_failure_attempts, 0) AS handoff_failure_attempts,\n COALESCE(attempt_summary.connection_success_users, 0) AS connection_success_users,\n COALESCE(attempt_summary.connection_failure_users, 0) AS connection_failure_users,\n COALESCE(attempt_summary.connection_no_outcome_attempts, 0) AS connection_no_outcome_attempts,\n COALESCE(attempt_summary.settings_handoff_attempts, 0) AS settings_handoff_attempts,\n attempt_summary.connection_success_attempts::float / NULLIF(attempt_summary.connection_success_attempts + attempt_summary.connection_failure_attempts, 0) AS connection_success_rate\nFROM method_list\nLEFT JOIN first_choice_summary ON first_choice_summary.method_id = method_list.method_id\nLEFT JOIN selection_summary ON selection_summary.method_id = method_list.method_id\nLEFT JOIN attempt_summary ON attempt_summary.method_id = method_list.method_id\nORDER BY method_list.method_id",
|
|
862
|
+
"sql": "WITH auth_identity_bridge AS (\n SELECT linked_email, MIN(auth_user_id) AS auth_user_id\n FROM (\n SELECT lower(COALESCE(\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END\n )) AS linked_email,\n NULLIF(e.properties::jsonb ->> 'auth_user_id', '') AS auth_user_id\n FROM analytics_events e\n WHERE (event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND event_date >= '{{timeRangeStart}}' AND event_date <= '{{timeRangeEnd}}')))\n AND ('{{appFilter}}' IN ('', 'all') OR lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) = lower('{{appFilter}}'))\n AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) IN ('analytics', 'assets', 'brain', 'calendar', 'chat', 'clips', 'content', 'design', 'dispatch', 'forms', 'mail', 'plan', 'slides')\n ) AS identities\n WHERE linked_email IS NOT NULL\n AND auth_user_id IS NOT NULL\n GROUP BY linked_email\n HAVING COUNT(DISTINCT auth_user_id) = 1\n), scoped_onboarding_events AS (\n SELECT e.*,\n COALESCE(\n NULLIF(e.properties::jsonb ->> 'auth_user_id', ''),\n auth_identity_bridge.auth_user_id,\n NULLIF(e.user_key, ''),\n NULLIF(e.user_id, ''),\n NULLIF(e.anonymous_id, '')\n ) AS funnel_user_key,\n COALESCE(\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END,\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.properties::jsonb ->> 'auth_user_id', '') LIKE '%@%.%' THEN e.properties::jsonb ->> 'auth_user_id' END\n ) AS funnel_user_email\n FROM analytics_events e\n LEFT JOIN auth_identity_bridge ON auth_identity_bridge.linked_email = lower(COALESCE(\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END\n ))\n WHERE (event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND event_date >= '{{timeRangeStart}}' AND event_date <= '{{timeRangeEnd}}')))\n AND ('{{appFilter}}' IN ('', 'all') OR lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) = lower('{{appFilter}}'))\n AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) IN ('analytics', 'assets', 'brain', 'calendar', 'chat', 'clips', 'content', 'design', 'dispatch', 'forms', 'mail', 'plan', 'slides')\n), onboarding_events AS (\n SELECT * FROM scoped_onboarding_events\n WHERE ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(funnel_user_email, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(funnel_user_email, '')) LIKE '%@builder.io'))\n AND NOT (strpos(lower(trim(coalesce(funnel_user_email, ''))), '@') > 1 AND (strpos(lower(trim(coalesce(funnel_user_email, ''))), '+autoz') > 0 OR strpos(lower(trim(coalesce(funnel_user_email, ''))), '+qa-test-bot-') > 0 OR left(lower(trim(coalesce(funnel_user_email, ''))), 12) = 'qa-test-bot-' OR left(lower(trim(coalesce(funnel_user_email, ''))), 13) = 'an-e2e-probe-' OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.com') OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.net') OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.org') OR right(lower(trim(coalesce(funnel_user_email, ''))), 5) = '@test' OR right(lower(trim(coalesce(funnel_user_email, ''))), 5) = '.test' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '@invalid' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '.invalid' OR right(lower(trim(coalesce(funnel_user_email, ''))), 10) = '@localhost' OR right(lower(trim(coalesce(funnel_user_email, ''))), 10) = '.localhost' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '@example' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '.example'))\n), choice_viewers AS (\n SELECT DISTINCT funnel_user_key\n FROM onboarding_events\n WHERE event_name = 'onboarding_step_viewed'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), choices AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id,\n timestamp::timestamptz AS clicked_at,\n id\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_clicked'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), ranked_choices AS (\n SELECT choices.*,\n ROW_NUMBER() OVER (PARTITION BY funnel_user_key ORDER BY clicked_at, id) AS choice_number\n FROM choices\n JOIN choice_viewers USING (funnel_user_key)\n), first_choice_summary AS (\n SELECT method_id, COUNT(DISTINCT funnel_user_key) AS first_choice_users\n FROM ranked_choices\n WHERE choice_number = 1\n GROUP BY method_id\n), selection_summary AS (\n SELECT method_id,\n COUNT(DISTINCT funnel_user_key) AS selected_users,\n COUNT(*) AS method_clicks,\n COUNT(DISTINCT attempt_id) AS selection_attempts\n FROM choices\n GROUP BY method_id\n), starts AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_started'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), method_outcomes AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id,\n COALESCE(NULLIF(properties::jsonb ->> 'outcome', ''), 'unknown') AS outcome\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_outcome'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), attempts AS (\n SELECT starts.funnel_user_key, starts.method_id, starts.attempt_id,\n MAX(CASE WHEN method_outcomes.outcome IN ('connected', 'already_connected') THEN 1 ELSE 0 END) AS connected,\n MAX(CASE WHEN method_outcomes.outcome = 'failed' THEN 1 ELSE 0 END) AS failed,\n MAX(CASE WHEN method_outcomes.outcome = 'handoff_failed' THEN 1 ELSE 0 END) AS handoff_failed,\n MAX(CASE WHEN method_outcomes.outcome = 'settings_opened' THEN 1 ELSE 0 END) AS settings_opened,\n MAX(CASE WHEN method_outcomes.outcome IS NOT NULL THEN 1 ELSE 0 END) AS has_outcome\n FROM starts\n LEFT JOIN method_outcomes\n ON method_outcomes.funnel_user_key = starts.funnel_user_key\n AND method_outcomes.method_id = starts.method_id\n AND method_outcomes.attempt_id = starts.attempt_id\n GROUP BY starts.funnel_user_key, starts.method_id, starts.attempt_id\n), attempt_summary AS (\n SELECT method_id,\n COUNT(DISTINCT CASE WHEN connected = 1 THEN attempt_id END) AS connection_success_attempts,\n COUNT(DISTINCT CASE WHEN failed = 1 THEN attempt_id END) AS connection_failure_attempts,\n COUNT(DISTINCT CASE WHEN handoff_failed = 1 THEN attempt_id END) AS handoff_failure_attempts,\n COUNT(DISTINCT CASE WHEN connected = 1 THEN funnel_user_key END) AS connection_success_users,\n COUNT(DISTINCT CASE WHEN failed = 1 THEN funnel_user_key END) AS connection_failure_users,\n COUNT(DISTINCT CASE WHEN connected = 0 AND failed = 0 AND has_outcome = 0 THEN attempt_id END) AS connection_no_outcome_attempts,\n COUNT(DISTINCT CASE WHEN settings_opened = 1 THEN attempt_id END) AS settings_handoff_attempts\n FROM attempts\n GROUP BY method_id\n), method_list AS (\n SELECT 'builder_create_account' AS method_id, 'Use Builder.io' AS method_label\n UNION ALL SELECT 'builder_sign_in', 'Sign in with Builder.io account'\n UNION ALL SELECT 'custom_keys', 'Configure custom keys'\n)\nSELECT method_list.method_id, method_list.method_label,\n (SELECT COUNT(DISTINCT funnel_user_key) FROM choice_viewers) AS choice_screen_viewers,\n COALESCE(first_choice_summary.first_choice_users, 0) AS first_choice_users,\n COALESCE(first_choice_summary.first_choice_users::float / NULLIF((SELECT COUNT(DISTINCT funnel_user_key) FROM choice_viewers), 0), 0) AS first_choice_rate,\n COALESCE(selection_summary.selected_users, 0) AS selected_users,\n COALESCE(selection_summary.method_clicks, 0) AS method_clicks,\n COALESCE(selection_summary.selection_attempts, 0) AS selection_attempts,\n COALESCE(attempt_summary.connection_success_attempts, 0) AS connection_success_attempts,\n COALESCE(attempt_summary.connection_failure_attempts, 0) AS connection_failure_attempts,\n COALESCE(attempt_summary.handoff_failure_attempts, 0) AS handoff_failure_attempts,\n COALESCE(attempt_summary.connection_success_users, 0) AS connection_success_users,\n COALESCE(attempt_summary.connection_failure_users, 0) AS connection_failure_users,\n COALESCE(attempt_summary.connection_no_outcome_attempts, 0) AS connection_no_outcome_attempts,\n COALESCE(attempt_summary.settings_handoff_attempts, 0) AS settings_handoff_attempts,\n attempt_summary.connection_success_attempts::float / NULLIF(attempt_summary.connection_success_attempts + attempt_summary.connection_failure_attempts, 0) AS connection_success_rate\nFROM method_list\nLEFT JOIN first_choice_summary ON first_choice_summary.method_id = method_list.method_id\nLEFT JOIN selection_summary ON selection_summary.method_id = method_list.method_id\nLEFT JOIN attempt_summary ON attempt_summary.method_id = method_list.method_id\nORDER BY method_list.method_id",
|
|
863
863
|
"config": {
|
|
864
864
|
"description": "Choice-screen viewers, first selected setup path, repeat selections, and linked Builder connection outcomes. Rates use choice-screen viewers or resolved Builder outcomes as their denominator; started attempts with no outcome are unknown or abandoned, not assumed failures. Handoff failures are reported separately from Builder connection failures. A successful Builder outcome confirms credentials connected, not that a new external account was created. Account-exists is counted as a failed create-account attempt. Test identities are excluded.",
|
|
865
865
|
"columns": [
|
|
@@ -634,6 +634,16 @@ function repairFingerprintedPanelQueries(
|
|
|
634
634
|
|
|
635
635
|
const CANONICAL_CUSTOM_PANEL_REPLACEMENTS: readonly ExactFirstPartyPanelReplacement[] =
|
|
636
636
|
[
|
|
637
|
+
{
|
|
638
|
+
id: "onboarding-setup-choice",
|
|
639
|
+
legacySql: [
|
|
640
|
+
buildPanel("onboarding-setup-choice")!.sql.replace(
|
|
641
|
+
"'Use Builder.io' AS method_label",
|
|
642
|
+
"'Create Builder.io account' AS method_label",
|
|
643
|
+
),
|
|
644
|
+
],
|
|
645
|
+
sql: buildPanel("onboarding-setup-choice")!.sql,
|
|
646
|
+
},
|
|
637
647
|
{
|
|
638
648
|
id: "signups-over-time",
|
|
639
649
|
legacySql: [
|
|
@@ -1309,7 +1309,7 @@ const ONBOARDING_SETUP_CHOICE_SQL = `${ONBOARDING_EVENTS_CTE}, choice_viewers AS
|
|
|
1309
1309
|
FROM attempts
|
|
1310
1310
|
GROUP BY method_id
|
|
1311
1311
|
), method_list AS (
|
|
1312
|
-
SELECT 'builder_create_account' AS method_id, '
|
|
1312
|
+
SELECT 'builder_create_account' AS method_id, 'Use Builder.io' AS method_label
|
|
1313
1313
|
UNION ALL SELECT 'builder_sign_in', 'Sign in with Builder.io account'
|
|
1314
1314
|
UNION ALL SELECT 'custom_keys', 'Configure custom keys'
|
|
1315
1315
|
)
|
|
@@ -29,7 +29,7 @@ export interface JourneyStep {
|
|
|
29
29
|
|
|
30
30
|
/** Display names match the `onboarding-setup-choice` metric's method list. */
|
|
31
31
|
const METHOD_LABELS: Record<string, string> = {
|
|
32
|
-
builder_create_account: "
|
|
32
|
+
builder_create_account: "Use Builder.io",
|
|
33
33
|
builder_sign_in: "Sign in with Builder.io account",
|
|
34
34
|
custom_keys: "Configure custom keys",
|
|
35
35
|
};
|
|
@@ -62,12 +62,22 @@ export function StorageSetupCard({
|
|
|
62
62
|
const [retryingBuilderStatus, setRetryingBuilderStatus] = useState(false);
|
|
63
63
|
const retryingBuilderStatusAtCountRef = useRef<number | null>(null);
|
|
64
64
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
65
|
+
const configuredTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
66
|
+
null,
|
|
67
|
+
);
|
|
65
68
|
const mountedRef = useRef(true);
|
|
66
69
|
const inFlightRef = useRef(false);
|
|
67
70
|
const visibilityHandlerRef = useRef<(() => void) | null>(null);
|
|
68
71
|
const connectRequestedRef = useRef(false);
|
|
69
72
|
const connectIntentExpiresAtRef = useRef<number | null>(null);
|
|
70
73
|
|
|
74
|
+
const clearConfiguredTimeout = useCallback(() => {
|
|
75
|
+
if (configuredTimeoutRef.current !== null) {
|
|
76
|
+
clearTimeout(configuredTimeoutRef.current);
|
|
77
|
+
configuredTimeoutRef.current = null;
|
|
78
|
+
}
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
71
81
|
const stopVisibilityHandler = useCallback(() => {
|
|
72
82
|
if (visibilityHandlerRef.current) {
|
|
73
83
|
document.removeEventListener(
|
|
@@ -79,6 +89,7 @@ export function StorageSetupCard({
|
|
|
79
89
|
}, []);
|
|
80
90
|
|
|
81
91
|
const startFileUploadPoll = useCallback(() => {
|
|
92
|
+
clearConfiguredTimeout();
|
|
82
93
|
if (pollRef.current) {
|
|
83
94
|
clearInterval(pollRef.current);
|
|
84
95
|
pollRef.current = null;
|
|
@@ -135,7 +146,11 @@ export function StorageSetupCard({
|
|
|
135
146
|
stop();
|
|
136
147
|
setConnecting(false);
|
|
137
148
|
setConnected(true);
|
|
138
|
-
|
|
149
|
+
clearConfiguredTimeout();
|
|
150
|
+
configuredTimeoutRef.current = setTimeout(() => {
|
|
151
|
+
configuredTimeoutRef.current = null;
|
|
152
|
+
if (mountedRef.current) void onConfigured();
|
|
153
|
+
}, 800);
|
|
139
154
|
} else if (status.state === "unavailable") {
|
|
140
155
|
stop();
|
|
141
156
|
setConnecting(false);
|
|
@@ -162,7 +177,7 @@ export function StorageSetupCard({
|
|
|
162
177
|
if (!document.hidden) void tick();
|
|
163
178
|
};
|
|
164
179
|
document.addEventListener("visibilitychange", visibilityHandlerRef.current);
|
|
165
|
-
}, [onConfigured, stopVisibilityHandler, t]);
|
|
180
|
+
}, [clearConfiguredTimeout, onConfigured, stopVisibilityHandler, t]);
|
|
166
181
|
|
|
167
182
|
const handleBuilderConnected = useCallback(() => {
|
|
168
183
|
if (!connectRequestedRef.current) return;
|
|
@@ -232,6 +247,10 @@ export function StorageSetupCard({
|
|
|
232
247
|
Date.now() + CANCELLED_SETUP_RECOVERY_MS;
|
|
233
248
|
builderConnect.cancel();
|
|
234
249
|
}, [builderConnect.cancel]);
|
|
250
|
+
const handleSkip = useCallback(() => {
|
|
251
|
+
clearConfiguredTimeout();
|
|
252
|
+
onSkip?.();
|
|
253
|
+
}, [clearConfiguredTimeout, onSkip]);
|
|
235
254
|
const builderConnectErrorMessage = builderConnect.error
|
|
236
255
|
? builderConnect.errorKind === "launch"
|
|
237
256
|
? t("storageSetup.builderConnectPopupError")
|
|
@@ -250,13 +269,14 @@ export function StorageSetupCard({
|
|
|
250
269
|
mountedRef.current = true;
|
|
251
270
|
return () => {
|
|
252
271
|
mountedRef.current = false;
|
|
272
|
+
clearConfiguredTimeout();
|
|
253
273
|
if (pollRef.current) {
|
|
254
274
|
clearInterval(pollRef.current);
|
|
255
275
|
pollRef.current = null;
|
|
256
276
|
}
|
|
257
277
|
stopVisibilityHandler();
|
|
258
278
|
};
|
|
259
|
-
}, [stopVisibilityHandler]);
|
|
279
|
+
}, [clearConfiguredTimeout, stopVisibilityHandler]);
|
|
260
280
|
|
|
261
281
|
return (
|
|
262
282
|
<div className="mx-auto flex w-full max-w-md flex-col gap-5 rounded-2xl border border-border bg-card p-6 shadow-lg">
|
|
@@ -379,7 +399,7 @@ export function StorageSetupCard({
|
|
|
379
399
|
variant="ghost"
|
|
380
400
|
size="sm"
|
|
381
401
|
className="self-center text-muted-foreground"
|
|
382
|
-
onClick={
|
|
402
|
+
onClick={handleSkip}
|
|
383
403
|
>
|
|
384
404
|
{t("agentChat.onboarding.skipForNow")}
|
|
385
405
|
</Button>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apply-slide-comments
|
|
3
|
+
description: >-
|
|
4
|
+
Turn open slide comments into edits and resolve them. Use when the user asks
|
|
5
|
+
to apply, address, resolve, or work through comments, review notes, or
|
|
6
|
+
feedback left on a deck or slide.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Apply Slide Comments
|
|
10
|
+
|
|
11
|
+
Comments are instructions anchored to a slide. Edits follow `slide-editing`;
|
|
12
|
+
this skill is the loop around them.
|
|
13
|
+
|
|
14
|
+
## Procedure
|
|
15
|
+
|
|
16
|
+
1. **Collect.** `list-slide-comments` with `deckId` (add `slideId` only when the
|
|
17
|
+
user scoped the request; page with `next_cursor` while `has_more`). Keep
|
|
18
|
+
threads with `resolved: false`. A thread is the root comment plus replies
|
|
19
|
+
(`thread_id`, `parent_id`); read the whole thread, and treat the latest
|
|
20
|
+
instruction as current.
|
|
21
|
+
2. **Group by slide** and note each comment's anchor: `anchor.objectId` is the
|
|
22
|
+
`data-slide-object-id` element, `quoted_text` / `anchor.targetText` and the
|
|
23
|
+
text offsets locate the words, and an anchor with only `x` / `y` is a
|
|
24
|
+
position on the slide.
|
|
25
|
+
3. **Read each affected slide** once: `view-screen` for the open slide,
|
|
26
|
+
`get-deck` with `slideIds` and `compact=false` for the rest. Interpret the
|
|
27
|
+
comment against its anchor object, not the whole slide.
|
|
28
|
+
4. **Apply every edit for one slide in a single `patch-deck` `patch-slide`
|
|
29
|
+
operation** with that slide's `baseContentHash`. Preserve ids and
|
|
30
|
+
`left`/`top`/`width`, and change only what the comment asks for.
|
|
31
|
+
5. **Verify.** When text length changed, call `get-layout-overflows` once after
|
|
32
|
+
all slides are written, and `audit-contrast` last (see `slide-editing`). Read
|
|
33
|
+
the persisted slide back before resolving anything.
|
|
34
|
+
6. **Resolve** each applied thread: `update-slide-comment` with `id`, `deckId`,
|
|
35
|
+
`resolved: true` (resolving applies to the full thread). Never delete a
|
|
36
|
+
user's comment.
|
|
37
|
+
7. **Leave open** any comment you skipped, that is ambiguous, or that asks for
|
|
38
|
+
something outside the deck (new data, an image only the user has). Say why
|
|
39
|
+
in the summary; reply with `add-slide-comment` (`threadId` + `parentId`)
|
|
40
|
+
only if the user asked you to.
|
|
41
|
+
|
|
42
|
+
## Judgment
|
|
43
|
+
|
|
44
|
+
- Ambiguous but low-risk: apply the smallest reasonable reading and state the
|
|
45
|
+
assumption. Ambiguous and costly to undo (cuts content, changes numbers,
|
|
46
|
+
conflicts with another comment): skip it.
|
|
47
|
+
- Conflicting comments on one element: apply neither, name both.
|
|
48
|
+
- A comment about a fact needs the source; follow `creative-context`, do not
|
|
49
|
+
guess.
|
|
50
|
+
- Leave no TODO or "per comment" text in slide HTML.
|
|
51
|
+
|
|
52
|
+
## Summary
|
|
53
|
+
|
|
54
|
+
Finish with one line, `N applied, M skipped`, then one line per change
|
|
55
|
+
(slide number, what changed) and one per skipped comment with its reason.
|
|
@@ -36,6 +36,8 @@ Context, reference deck, or source material that the app already provides.
|
|
|
36
36
|
|
|
37
37
|
When speaker notes are requested, put presenter-only text in each slide's
|
|
38
38
|
`notes` field on `create-deck` or `add-slide`; keep it out of the slide HTML.
|
|
39
|
+
Write notes as speech, not a summary of the slide: the opener, the transition
|
|
40
|
+
to the next slide, numbers to cite, a timing cue.
|
|
39
41
|
Preserve existing notes when editing or importing a source deck.
|
|
40
42
|
|
|
41
43
|
When the UI has already created the empty deck, keep its id and rename it before
|
|
@@ -62,7 +64,12 @@ and slide chrome. Read `slide-design` for visual craft; it works inside the
|
|
|
62
64
|
active system and reference deck, never as a competing theme. If the
|
|
63
65
|
request is open-ended and no approved direction exists, ask one targeted guided
|
|
64
66
|
question or present a bounded choice before writing; do not silently pick a new
|
|
65
|
-
brand language.
|
|
67
|
+
brand language. Write a direction choice for this topic: three options, each a
|
|
68
|
+
vibe word plus one concrete visual cue (palette, type, motif; for example "rust
|
|
69
|
+
technical editorial: warm rust on charcoal, mono headings, code-grid layout"),
|
|
70
|
+
clearly different from each other, the best fit marked recommended. Never offer
|
|
71
|
+
generic labels such as "minimal" or "corporate" alone. Take everything else
|
|
72
|
+
(density, slide count, structure) from the request and do the work.
|
|
66
73
|
|
|
67
74
|
Before the first slide, lock a deck-level visual contract: background family,
|
|
68
75
|
text and surface roles, accent treatment, heading/body type pairing, spacing
|
|
@@ -232,13 +239,24 @@ component language as a substitute for choosing.
|
|
|
232
239
|
|
|
233
240
|
The canvas is fixed at its aspect-ratio dimensions. With the standard 16:9
|
|
234
241
|
canvas (960x540) and `padding: 64px 80px`, the usable content area is only
|
|
235
|
-
800x412px
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
800x412px (usable height is canvas height minus both vertical paddings). Treat
|
|
243
|
+
that as a hard budget for the main flow: use at most two title lines, three
|
|
244
|
+
short bullets or cards, and two or three short items per column. A content slide
|
|
245
|
+
carries about 40 words besides its title. A bullet fits on one line (about 70
|
|
246
|
+
characters at 18px); only a card may wrap, to two lines. Split dense source
|
|
247
|
+
material across slides instead of shrinking it into a dense stack. Keep body
|
|
248
|
+
text at or above 16px. Never hide overflow with zoom, `transform: scale()`,
|
|
249
|
+
clipping, or scroll overflow. A later structural repair may reduce the slide's
|
|
250
|
+
explicit padding, and that padding must remain intact when the saved HTML is
|
|
251
|
+
rendered.
|
|
252
|
+
|
|
253
|
+
Before writing a slide, total its flow height: per text block, font-size x
|
|
254
|
+
line-height x wrapped lines (average glyph width is about 0.5 x font-size), plus
|
|
255
|
+
padding and gaps. Count every wrapped line and keep about 10% slack for the
|
|
256
|
+
measured check. Example: a two-line 34px/1.12 heading (76) + 18 gap + three
|
|
257
|
+
cards of two 18px/1.4 lines plus 24 padding (3 x 74 = 223) + two 14px gaps (28)
|
|
258
|
+
= 345, inside 412; a fourth card adds 88 and does not fit. When the total
|
|
259
|
+
exceeds the budget, split the slide; do not shrink type, padding, or gaps.
|
|
242
260
|
|
|
243
261
|
Build an intentional composition beyond a text dump: use a title block,
|
|
244
262
|
two-column split, metric treatment, rule, callout, visual placeholder, or
|
|
@@ -249,6 +267,9 @@ with nothing real to show is complete with type, spacing, and a rule. A
|
|
|
249
267
|
built-in template's signature art belongs to that template; do not carry it
|
|
250
268
|
into other decks.
|
|
251
269
|
|
|
270
|
+
Author in normal flex/grid flow; the card, wrapper, height, and `contain` rules
|
|
271
|
+
are in `slide-editing` (Flow Layout and the Editor).
|
|
272
|
+
|
|
252
273
|
## Bounded visual QA
|
|
253
274
|
|
|
254
275
|
Before calling the deck complete, render every changed slide at its canonical
|
|
@@ -306,8 +327,11 @@ short rule, or callout should support the message, not fill empty space.
|
|
|
306
327
|
|
|
307
328
|
## Image Placeholders
|
|
308
329
|
|
|
309
|
-
When a slide needs a visual, use this div. It renders as a styled
|
|
310
|
-
and can later be replaced with a generated image
|
|
330
|
+
When a slide needs a specific visual, use this div. It renders as a styled
|
|
331
|
+
placeholder and can later be replaced with a generated image. Describe the
|
|
332
|
+
content the image must show ("Q3 revenue by region, bar chart"), not its role
|
|
333
|
+
("hero image"). If type, layout, and color can carry the slide, use no
|
|
334
|
+
placeholder:
|
|
311
335
|
|
|
312
336
|
```html
|
|
313
337
|
<div class="fmd-img-placeholder" style="width: 100%; min-height: 220px; border-radius: var(--deck-radius);">[Description of what image should show]</div>
|
|
@@ -90,8 +90,11 @@ Type does most of the work.
|
|
|
90
90
|
for related, far for unrelated; equal gaps everywhere read as amateur.
|
|
91
91
|
- **Optical alignment:** nudge large type to look flush with small text.
|
|
92
92
|
- **One focal point.** Squint; one thing should land first.
|
|
93
|
-
-
|
|
94
|
-
|
|
93
|
+
- **One idea per slide; split, do not shrink.** Total the flow height and word
|
|
94
|
+
count before writing (Fit budget in `create-deck`); a slide that does not fit
|
|
95
|
+
becomes two slides, not smaller type, tighter padding, or clipped overflow.
|
|
96
|
+
- Flow, card, and fixed-height constraints: `slide-editing` (Flow Layout and the
|
|
97
|
+
Editor).
|
|
95
98
|
|
|
96
99
|
## Color
|
|
97
100
|
|
|
@@ -117,7 +120,7 @@ With a system or reference, use only its colors; these rules govern how.
|
|
|
117
120
|
- **Screenshots as hero shots:** crop to the region the headline is about,
|
|
118
121
|
scale it up, set it on a surface that fits the direction.
|
|
119
122
|
- **One graphic vocabulary:** 1-2px rules, large index numbers ("01"), one
|
|
120
|
-
repeated shape motif. Every shape needs a semantic role
|
|
123
|
+
repeated shape motif. Every shape needs a semantic role.
|
|
121
124
|
- **Charts in the deck's type and palette:** thin strokes, direct labels, the
|
|
122
125
|
key series in the accent, the rest in a tinted neutral, no gridline or
|
|
123
126
|
legend clutter.
|
|
@@ -128,8 +131,9 @@ With a system or reference, use only its colors; these rules govern how.
|
|
|
128
131
|
headline + image, split, grid of 3, quote, closing. Alternate dense and
|
|
129
132
|
sparse; avoid three identical layouts in a row.
|
|
130
133
|
- Fixed chrome in the exact same place on every slide: a small running head,
|
|
131
|
-
a
|
|
132
|
-
|
|
134
|
+
maybe a hairline. Quiet repetition is what makes a deck feel designed. Do not
|
|
135
|
+
type page numbers such as `04 / 12`: inserting or reordering slides makes
|
|
136
|
+
them wrong everywhere, and the renderer provides no page-number token.
|
|
133
137
|
- Title and section slides carry the strongest expression of the direction.
|
|
134
138
|
|
|
135
139
|
## What reads as generic
|
|
@@ -85,14 +85,10 @@ panels, fake logos, and filler bullets.
|
|
|
85
85
|
|
|
86
86
|
## Fit and Density
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
column. If the source is denser, split it across slides. Never use zoom,
|
|
93
|
-
`transform: scale()`, clipping, or scroll overflow to hide a fit issue; body
|
|
94
|
-
text must remain at least 16px. Explicitly reduced slide padding is allowed when
|
|
95
|
-
the content still needs the space.
|
|
88
|
+
Edits obey the Fit budget in `create-deck`, measured against the slide's own
|
|
89
|
+
padding and aspect ratio. When an edit adds or lengthens text, redo the height
|
|
90
|
+
arithmetic for that slide and split it instead of shrinking. Explicitly reduced
|
|
91
|
+
slide padding is allowed when the content still needs the space.
|
|
96
92
|
|
|
97
93
|
After all deck edits, call `get-layout-overflows` once. If you repair a
|
|
98
94
|
measured overflow, call it once more; do not check between writes. If status is
|
|
@@ -139,6 +135,12 @@ To edit a slide's content:
|
|
|
139
135
|
For a targeted persisted read, pass that stable `slideId` to `get-deck` so
|
|
140
136
|
only the target slide is returned; use `compact=false` when you need its
|
|
141
137
|
full HTML.
|
|
138
|
+
The user navigates and reselects between turns. On any turn that says
|
|
139
|
+
this, here, that slide, these, or the selected one (including a follow-up
|
|
140
|
+
like "now make it bigger"), call `view-screen` again before acting; never
|
|
141
|
+
reuse the previous turn's slide ID or selection. If it shows nothing
|
|
142
|
+
selected or the target is unclear, ask which slide rather than guessing from
|
|
143
|
+
recent edits.
|
|
142
144
|
2. **Retrieve before generating**: when the edit changes facts, brand language,
|
|
143
145
|
or layout, follow the `creative-context` skill and query those roles
|
|
144
146
|
separately. Respect opt-out, pinned packs, and the exact reuse ladder.
|
|
@@ -259,6 +261,10 @@ same operations.
|
|
|
259
261
|
## Click-to-reveal animations
|
|
260
262
|
|
|
261
263
|
Animations are metadata over the final slide HTML, not alternate slide markup.
|
|
264
|
+
Reveal only where the order of ideas carries the argument (a list whose payoff
|
|
265
|
+
is the last item, a build to a conclusion, before/after); a title, single
|
|
266
|
+
quote, or diagram the audience should take in at a glance is stronger shown
|
|
267
|
+
whole. Do not add reveals by default.
|
|
262
268
|
Read the full target slide, keep its existing visual structure, and patch the
|
|
263
269
|
complete ordered `animations` list with `elementPath` values from that exact
|
|
264
270
|
HTML. Elements omitted from the list remain visible immediately, so labels and
|
|
@@ -300,23 +306,42 @@ children of `.fmd-slide`. Give each one a stable `data-slide-object-id`:
|
|
|
300
306
|
</div>
|
|
301
307
|
```
|
|
302
308
|
|
|
303
|
-
- Preserve `data-slide-object-id`
|
|
304
|
-
|
|
309
|
+
- Preserve `data-slide-object-id` and the `left`/`top`/`width` of hand-placed
|
|
310
|
+
objects when updating, moving, resizing, or styling them.
|
|
305
311
|
- Mint a new unique object ID when duplicating an object.
|
|
306
312
|
- Do not use runtime-only `data-builder-id` values in saved slide HTML.
|
|
307
|
-
-
|
|
308
|
-
|
|
309
|
-
deliberate freeform object instead.
|
|
313
|
+
- A text box (`.fmd-text-box`) with no inline `height` auto-grows with its text
|
|
314
|
+
from the top edge.
|
|
310
315
|
- Build editable shapes with styled HTML elements such as `div`. Do not use
|
|
311
316
|
inline SVG, which the slide sanitizer removes.
|
|
312
317
|
|
|
318
|
+
## Flow Layout and the Editor
|
|
319
|
+
|
|
320
|
+
Keep generated flex and grid content in normal flow. Do not silently
|
|
321
|
+
absolute-position a nested layout child just to make it draggable; create a
|
|
322
|
+
deliberate freeform object instead. The editor presents flow content as flat
|
|
323
|
+
objects the way Google Slides does, so write markup that maps cleanly:
|
|
324
|
+
|
|
325
|
+
- A card is one painted box (background, border, or shadow on a single element)
|
|
326
|
+
that owns its text. Never stack separately positioned text over a card
|
|
327
|
+
background.
|
|
328
|
+
- Text containers, including `.fmd-text-box`, have no fixed `height`
|
|
329
|
+
(`min-height` only for a deliberate minimum), so text grows instead of
|
|
330
|
+
overflowing. Never write `contain` or `contain-intrinsic-size`.
|
|
331
|
+
- Keep nesting shallow. Unpainted wrappers with no direct text (grid rows,
|
|
332
|
+
columns) are fine; the pointer skips them.
|
|
333
|
+
- Ids belong to freeform objects only. Never stamp `data-slide-object-id` on a
|
|
334
|
+
flow region: any id marks an object freeform and `export-pptx` rejects it.
|
|
335
|
+
Preserve existing ids when rewriting a slide.
|
|
336
|
+
|
|
313
337
|
## Image Placeholders
|
|
314
338
|
|
|
315
|
-
For visual elements (diagrams, charts, photos), use placeholder divs
|
|
339
|
+
For visual elements (diagrams, charts, photos), use placeholder divs whose text
|
|
340
|
+
names the content to show, not its role (see `create-deck` Image Placeholders):
|
|
316
341
|
|
|
317
342
|
```html
|
|
318
343
|
<div class="fmd-img-placeholder" style="width: 100%; height: 300px; border-radius: 12px;">
|
|
319
|
-
|
|
344
|
+
Q3 revenue by region, bar chart
|
|
320
345
|
</div>
|
|
321
346
|
```
|
|
322
347
|
|
|
@@ -13,6 +13,7 @@ Read relevant guides before deeper work:
|
|
|
13
13
|
- `.agents/skills/image-generation-via-a2a/SKILL.md` — for A2A image generation.
|
|
14
14
|
- `.agents/skills/design-systems/SKILL.md` — for deck design systems.
|
|
15
15
|
- `.agents/skills/slide-design/SKILL.md` — for visual-craft requests.
|
|
16
|
+
- `.agents/skills/apply-slide-comments/SKILL.md` — for applying/resolving comments.
|
|
16
17
|
- `.agents/skills/creative-context/SKILL.md` — for source reuse and context.
|
|
17
18
|
- `.agents/skills/analytics-data-for-decks/SKILL.md` — for delegated data requests.
|
|
18
19
|
|
|
@@ -45,8 +46,8 @@ Use local framework docs, not web research: `pnpm action docs-search --query "<t
|
|
|
45
46
|
- UI feedback: target 100 ms, never exceed 400 ms; acknowledge before network work.
|
|
46
47
|
- Use actions for deck/slide writes; keep large files in configured file storage, not SQL/settings/resources. Never hardcode secrets or private/customer data; use obvious placeholders.
|
|
47
48
|
- For external integrations, inspect the workspace/provider connection catalog first; reuse its scoped resolver.
|
|
48
|
-
- Use `view-screen` when the active deck/slide/layout is unclear. Preserve requested structure; do not restyle imports marked partial or with `imagesSkipped` without reporting the warning.
|
|
49
|
-
-
|
|
49
|
+
- Use `view-screen` when the active deck/slide/layout is unclear, and on every turn that says "this"/"here"/"selected". Preserve requested structure; do not restyle imports marked partial or with `imagesSkipped` without reporting the warning.
|
|
50
|
+
- Slide HTML layout, card, no-SVG, and freeform-id rules: `slide-editing`.
|
|
50
51
|
- Import attachments only on request or via Import. `sourceImport` records provenance; structural edits clear it. Read preloaded new-deck attachments before deciding whether to import.
|
|
51
52
|
- Provider API actions are shortcuts, not limits. For exact Drive API needs use `provider-api-catalog` / `-docs` / `-request` with the user's Google Docs OAuth. Preserve imported PPTX timing metadata.
|
|
52
53
|
- For data requests, follow `analytics-data-for-decks` and delegate via Analytics/A2A; never query SQL or providers directly. Without a reference deck or design system, read `get-workspace-defaults` before generation; use `creative-context` for source order and context submission.
|
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
"visibility": "both",
|
|
43
43
|
"exportAs": "slide-editing"
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
"path": ".agents/skills/apply-slide-comments",
|
|
47
|
+
"visibility": "both",
|
|
48
|
+
"exportAs": "apply-slide-comments"
|
|
49
|
+
},
|
|
45
50
|
{
|
|
46
51
|
"path": ".agents/skills/deck-management",
|
|
47
52
|
"visibility": "internal"
|
|
@@ -32,8 +32,8 @@ export function AlignmentGuides({
|
|
|
32
32
|
position: "fixed",
|
|
33
33
|
pointerEvents: "none",
|
|
34
34
|
zIndex: 70,
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// guard:allow-raw-color — Google Slides guides are literal #ff0000, theme-independent (interaction-oracle.md 9.2)
|
|
36
|
+
backgroundColor: "#ff0000",
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
return createPortal(
|