Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/app/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default defineConfig({
testIsolation: false,
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.ts',
supportFile: false,
supportFile: 'cypress/support/e2e.ts',
setupNodeEvents(on, config) {
on(
'file:preprocessor',
Expand Down
16 changes: 16 additions & 0 deletions packages/app/cypress/support/e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Global e2e setup. Loaded before every `cy.visit` via `supportFile` in
* `cypress.config.ts`.
*
* Snoozes the feedback-modal nudge so it doesn't render its centered modal
* + backdrop on top of the UI under test. Specs that want to exercise the
* feedback-modal flow can clear `inferencex-feedback-modal-snoozed` in their
* own `onBeforeLoad`.
*/
Cypress.on('window:before:load', (win) => {
try {
win.localStorage.setItem('inferencex-feedback-modal-snoozed', String(Date.now()));
} catch {
// localStorage unavailable — fine, the test will just see the modal.
}
});
2 changes: 1 addition & 1 deletion packages/app/src/components/feedback-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function FeedbackForm({ onDismiss }: FeedbackFormProps) {
Help us improve InferenceX
</h2>
<p id={descId} className="text-sm text-muted-foreground">
You're a regular! We'd love to hear what's working and what isn't.
We'd love to hear what's working and what isn't.
</p>
</div>

Expand Down
27 changes: 24 additions & 3 deletions packages/app/src/components/nudge-engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,15 +355,20 @@ function ModalRenderer({
const { content } = def;
const Icon = content.icon;
const idPrefix = def.id;
const centered = content.centered;

return (
const dialog = (
<aside
data-testid={content.testId}
role="dialog"
aria-modal="false"
aria-modal={centered ? 'true' : 'false'}
aria-labelledby={`${idPrefix}-title`}
aria-describedby={`${idPrefix}-description`}
className={`fixed bottom-4 right-4 z-50 w-[calc(100vw-2rem)] max-w-md rounded-lg border bg-background p-6 shadow-lg ${content.containerClassName ?? ''}`}
className={
centered
? `relative z-50 w-[calc(100vw-2rem)] max-w-md rounded-lg border bg-background p-6 shadow-lg ${content.containerClassName ?? ''}`
: `fixed bottom-4 right-4 z-50 w-[calc(100vw-2rem)] max-w-md rounded-lg border bg-background p-6 shadow-lg ${content.containerClassName ?? ''}`
}
>
<button
type="button"
Expand Down Expand Up @@ -414,6 +419,22 @@ function ModalRenderer({
)}
</aside>
);

if (centered) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<button
type="button"
aria-label="Close"
onClick={onDismiss}
className="absolute inset-0 cursor-default bg-black/50"
/>
{dialog}
</div>
);
}

return dialog;
}

function BannerRenderer({
Expand Down
11 changes: 5 additions & 6 deletions packages/app/src/lib/nudges/registry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ import { GITHUB_OWNER, GITHUB_REPO } from '@semianalysisai/inferencex-constants'

import { FEEDBACK_SUBMITTED_EVENT } from '@/components/feedback-modal';

// Keep the ~210-line FeedbackForm out of the landing/dashboard initial JS;
// it only renders after the eligibility event fires (see feedback-modal nudge below).
// Keep the ~210-line FeedbackForm out of the landing/dashboard initial JS.
const FeedbackForm = dynamic(
() => import('@/components/feedback-modal').then((m) => m.FeedbackForm),
{ ssr: false },
);
import { GitHubIcon } from '@/components/ui/github-icon';
import { STARRED_EVENT, STARRED_KEY, saveStarred } from '@/lib/star-storage';
import { FEEDBACK_ELIGIBLE_EVENT } from '@/lib/visit-tracking';
import type { NudgeDefinition } from './types';

const GITHUB_REPO_URL = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}`;
Expand Down Expand Up @@ -196,10 +194,10 @@ export const NUDGE_REGISTRY: NudgeDefinition[] = [
{
id: 'feedback-modal',
type: 'modal',
trigger: { type: 'event', event: FEEDBACK_ELIGIBLE_EVENT, delayMs: 2000 },
trigger: { type: 'immediate' },
dismissal: {
type: 'timed',
durationMs: 90 * 24 * 60 * 60 * 1000,
durationMs: 24 * 60 * 60 * 1000,
cooldownStartsOnShow: true,
},
storageKey: 'inferencex-feedback-modal-snoozed',
Expand All @@ -211,8 +209,9 @@ export const NUDGE_REGISTRY: NudgeDefinition[] = [
icon: MessageSquareText,
iconClassName: 'text-brand',
title: 'Help us improve InferenceX',
description: "You're a regular! We'd love to hear what's working and what isn't.",
description: "We'd love to hear what's working and what isn't.",
testId: 'feedback-modal',
centered: true,
renderContent: ({ dismiss }) => <FeedbackForm onDismiss={dismiss} />,
},
analytics: {
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/lib/nudges/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export interface NudgeContent {
primaryAction?: NudgeAction;
/** Extra CSS class on the modal container (e.g. branded border). */
containerClassName?: string;
/** Render as a true center-of-page modal with a backdrop instead of a bottom-right card. */
centered?: boolean;
/** Extra CSS class on the primary action button (e.g. glow effect). */
actionClassName?: string;
/** Badge text rendered next to the title (e.g. "New"). */
Expand Down
3 changes: 0 additions & 3 deletions packages/app/src/lib/visit-tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ const FIRST_SEEN_KEY = 'inferencex-first-seen';
const LAST_SEEN_KEY = 'inferencex-last-seen';
const SESSION_KEY = 'inferencex-visit-counted';

export const FEEDBACK_TARGET_VISIT = 2;
export const FEEDBACK_ELIGIBLE_EVENT = 'inferencex:feedback-eligible';

function todayISO(): string {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
Expand Down
14 changes: 2 additions & 12 deletions packages/app/src/providers/visit-tracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,11 @@

import { useEffect } from 'react';

import {
FEEDBACK_ELIGIBLE_EVENT,
FEEDBACK_TARGET_VISIT,
recordVisitIfNew,
} from '@/lib/visit-tracking';
import { recordVisitIfNew } from '@/lib/visit-tracking';

export function VisitTracker() {
useEffect(() => {
const count = recordVisitIfNew();
if (count !== FEEDBACK_TARGET_VISIT) return undefined;
// setTimeout(0) so the engine's listener (deeper in the tree) is attached first.
const t = window.setTimeout(() => {
window.dispatchEvent(new Event(FEEDBACK_ELIGIBLE_EVENT));
}, 0);
return () => window.clearTimeout(t);
recordVisitIfNew();
}, []);
return null;
}
Loading