@e-llm-studio/feedback-flow 0.0.0-stage → 0.1.31
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/README.md +63 -2
- package/dist/FeedbackErrorBoundary.d.ts +17 -0
- package/dist/FeedbackErrorBoundary.js +26 -0
- package/dist/FeedbackExperience.d.ts +27 -0
- package/dist/FeedbackExperience.js +54 -0
- package/dist/FeedbackModal.d.ts +27 -0
- package/dist/FeedbackModal.js +659 -0
- package/dist/FeedbackPrompt.d.ts +20 -0
- package/dist/FeedbackPrompt.js +43 -0
- package/dist/FeedbackScreenshotThumbnail.d.ts +9 -0
- package/dist/FeedbackScreenshotThumbnail.js +33 -0
- package/dist/StarRating.d.ts +17 -0
- package/dist/StarRating.js +16 -0
- package/dist/featureAvailability.d.ts +21 -0
- package/dist/featureAvailability.js +104 -0
- package/dist/feedback.css +1078 -0
- package/dist/feedbackApi.d.ts +70 -0
- package/dist/feedbackApi.js +164 -0
- package/dist/feedbackTheme.d.ts +4 -0
- package/dist/feedbackTheme.js +22 -0
- package/dist/feedbackUpload.d.ts +50 -0
- package/dist/feedbackUpload.js +190 -0
- package/dist/httpFeedbackApi.d.ts +15 -0
- package/dist/httpFeedbackApi.js +390 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +31 -0
- package/dist/tracking.d.ts +3 -0
- package/dist/tracking.js +22 -0
- package/dist/types.d.ts +276 -0
- package/dist/types.js +152 -0
- package/dist/useFeedbackFlow.d.ts +32 -0
- package/dist/useFeedbackFlow.js +137 -0
- package/dist/useFeedbackPersistence.d.ts +12 -0
- package/dist/useFeedbackPersistence.js +130 -0
- package/package.json +34 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import type { FeedbackFlowController } from './useFeedbackFlow';
|
|
3
|
+
import { type FeedbackCopy, type FeedbackPromptClassNames, type FeedbackPromptStyles, type FeedbackTheme } from './types';
|
|
4
|
+
export interface FeedbackPromptProps {
|
|
5
|
+
flow: FeedbackFlowController;
|
|
6
|
+
copy?: Partial<FeedbackCopy>;
|
|
7
|
+
theme?: FeedbackTheme;
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
promptRef?: Ref<HTMLDivElement>;
|
|
10
|
+
extraContent?: ReactNode;
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
onBeforeOpen?: () => void;
|
|
14
|
+
onRatingSelected?: (rating: number) => void;
|
|
15
|
+
className?: string;
|
|
16
|
+
style?: CSSProperties;
|
|
17
|
+
styles?: FeedbackPromptStyles;
|
|
18
|
+
classNames?: FeedbackPromptClassNames;
|
|
19
|
+
}
|
|
20
|
+
export declare function FeedbackPrompt({ flow, copy: copyOverrides, theme, compact, promptRef, extraContent, loading, disabled, onBeforeOpen, onRatingSelected, className, style, styles, classNames, }: FeedbackPromptProps): JSX.Element | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FeedbackPrompt = FeedbackPrompt;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
const feedbackTheme_1 = require("./feedbackTheme");
|
|
8
|
+
const StarRating_1 = require("./StarRating");
|
|
9
|
+
function FeedbackPrompt({ flow, copy: copyOverrides, theme, compact = false, promptRef, extraContent, loading = false, disabled = false, onBeforeOpen, onRatingSelected, className, style, styles, classNames, }) {
|
|
10
|
+
const copy = (0, types_1.createFeedbackCopy)(copyOverrides);
|
|
11
|
+
const [hovered, setHovered] = (0, react_1.useState)(null);
|
|
12
|
+
const [selectedTooltipStar, setSelectedTooltipStar] = (0, react_1.useState)(null);
|
|
13
|
+
if (!flow)
|
|
14
|
+
return null;
|
|
15
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: promptRef, className: [
|
|
16
|
+
'cw-feedback-prompt',
|
|
17
|
+
loading ? 'cw-feedback-prompt--loading' : '',
|
|
18
|
+
compact ? 'cw-feedback-prompt--compact' : '',
|
|
19
|
+
className,
|
|
20
|
+
classNames?.root,
|
|
21
|
+
]
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.join(' '), style: {
|
|
24
|
+
...(0, feedbackTheme_1.feedbackThemeStyle)(theme),
|
|
25
|
+
...style,
|
|
26
|
+
...styles?.root,
|
|
27
|
+
}, "aria-busy": loading, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", disabled: disabled, className: ['cw-feedback-prompt__label', classNames?.labelButton].filter(Boolean).join(' '), style: styles?.labelButton, onClick: () => {
|
|
28
|
+
if (flow.rating === null || flow.step !== 'closed')
|
|
29
|
+
return;
|
|
30
|
+
onBeforeOpen?.();
|
|
31
|
+
flow.setStep(flow.completed ? (flow.ratingBase >= 3 && flow.ratingBase < 5 ? 'rating' : flow.ratingBase === 5 ? 'time' : 'gap') : flow.resumeStep);
|
|
32
|
+
}, children: flow.completed ? copy.savedLabel : copy.prompt }), (0, jsx_runtime_1.jsx)(StarRating_1.StarRating, { value: flow.rating, disabled: disabled || (flow.ratingLocked && flow.step === 'closed'), groupLabel: copy.starGroupLabel, starLabel: copy.starLabel, theme: theme, className: classNames?.stars, style: styles?.stars, styles: styles?.starRating, classNames: classNames?.starRating, onHoverChange: setHovered, onChange: (star) => {
|
|
33
|
+
onBeforeOpen?.();
|
|
34
|
+
setSelectedTooltipStar(star);
|
|
35
|
+
flow.selectRating(star);
|
|
36
|
+
onRatingSelected?.(star);
|
|
37
|
+
} }), flow.rating !== null && flow.step === 'closed' && ((0, jsx_runtime_1.jsx)("button", { type: "button", disabled: disabled, className: ['cw-feedback-review', classNames?.reviewButton].filter(Boolean).join(' '), style: styles?.reviewButton, onClick: () => {
|
|
38
|
+
onBeforeOpen?.();
|
|
39
|
+
flow.setStep(flow.completed ? (flow.ratingBase >= 3 && flow.ratingBase < 5 ? 'rating' : flow.ratingBase === 5 ? 'time' : 'gap') : flow.resumeStep);
|
|
40
|
+
}, children: flow.completed ? copy.reviewLabel : copy.continueLabel })), !flow.completed && (hovered !== null || selectedTooltipStar !== null) && ((0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-tooltip', classNames?.tooltip].filter(Boolean).join(' '), style: styles?.tooltip, children: hovered === 5 && !copy.starTooltipLabel
|
|
41
|
+
? copy.fiveStarTooltip
|
|
42
|
+
: copy.starTooltipLabel?.(hovered ?? selectedTooltipStar ?? 5) ?? copy.starLabel(hovered ?? selectedTooltipStar ?? 5) })), extraContent && ((0, jsx_runtime_1.jsx)("div", { className: classNames?.extraContent, style: styles?.extraContent, children: extraContent })), loading && (0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-prompt__loading", role: "status", children: [(0, jsx_runtime_1.jsx)("span", { className: "cw-feedback-prompt__spinner" }), " Loading feedback\u2026"] })] }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FeedbackAttachmentRef } from './types';
|
|
2
|
+
interface FeedbackScreenshotThumbnailProps {
|
|
3
|
+
screenshot: File | FeedbackAttachmentRef;
|
|
4
|
+
resolveUrl?: (gcsUrl: string, forceRefresh?: boolean) => Promise<string>;
|
|
5
|
+
onOpen: (url: string, name: string) => void;
|
|
6
|
+
onRemove: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function FeedbackScreenshotThumbnail({ screenshot, resolveUrl, onOpen, onRemove }: FeedbackScreenshotThumbnailProps): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FeedbackScreenshotThumbnail = FeedbackScreenshotThumbnail;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const lucide_react_1 = require("lucide-react");
|
|
7
|
+
const feedbackUpload_1 = require("./feedbackUpload");
|
|
8
|
+
function FeedbackScreenshotThumbnail({ screenshot, resolveUrl, onOpen, onRemove }) {
|
|
9
|
+
const [url, setUrl] = (0, react_1.useState)(null);
|
|
10
|
+
const [failed, setFailed] = (0, react_1.useState)(false);
|
|
11
|
+
const [retry, setRetry] = (0, react_1.useState)(0);
|
|
12
|
+
(0, react_1.useEffect)(() => {
|
|
13
|
+
setFailed(false);
|
|
14
|
+
if ('attachmentId' in screenshot) {
|
|
15
|
+
if (resolveUrl && screenshot.gcsUrl.startsWith('gs://')) {
|
|
16
|
+
let active = true;
|
|
17
|
+
setUrl(null);
|
|
18
|
+
resolveUrl(screenshot.gcsUrl, retry > 0)
|
|
19
|
+
.then((signedUrl) => { if (active)
|
|
20
|
+
setUrl(signedUrl); })
|
|
21
|
+
.catch(() => { if (active)
|
|
22
|
+
setFailed(true); });
|
|
23
|
+
return () => { active = false; };
|
|
24
|
+
}
|
|
25
|
+
setUrl(screenshot.publicUrl || (0, feedbackUpload_1.feedbackScreenshotPublicUrl)(screenshot.gcsUrl));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const objectUrl = URL.createObjectURL(screenshot);
|
|
29
|
+
setUrl(objectUrl);
|
|
30
|
+
return () => URL.revokeObjectURL(objectUrl);
|
|
31
|
+
}, [screenshot, resolveUrl, retry]);
|
|
32
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-file-item", children: [url ? ((0, jsx_runtime_1.jsxs)("button", { className: "cw-feedback-file-preview", type: "button", "aria-label": `Preview screenshot ${screenshot.name}`, onClick: () => onOpen(url, screenshot.name), children: [(0, jsx_runtime_1.jsx)("img", { src: url, alt: "", loading: "lazy", onError: () => { setUrl(null); setFailed(true); } }), (0, jsx_runtime_1.jsx)("span", { children: screenshot.name })] })) : failed ? ((0, jsx_runtime_1.jsxs)("button", { className: "cw-feedback-file-retry", type: "button", onClick: () => setRetry((value) => value + 1), children: ["Retry preview: ", screenshot.name] })) : ((0, jsx_runtime_1.jsx)("span", { className: "cw-feedback-file-name", children: screenshot.name })), (0, jsx_runtime_1.jsx)("button", { className: "cw-feedback-file-remove", type: "button", "aria-label": `Remove screenshot ${screenshot.name}`, onClick: onRemove, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, "aria-hidden": "true" }) })] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
import { type FeedbackTheme, type StarRatingClassNames, type StarRatingStyles } from './types';
|
|
3
|
+
export interface StarRatingProps {
|
|
4
|
+
value: number | null;
|
|
5
|
+
onChange: (value: number) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
groupLabel?: string;
|
|
8
|
+
starLabel?: (star: number) => string;
|
|
9
|
+
theme?: FeedbackTheme;
|
|
10
|
+
onHoverChange?: (star: number | null) => void;
|
|
11
|
+
className?: string;
|
|
12
|
+
style?: CSSProperties;
|
|
13
|
+
styles?: StarRatingStyles;
|
|
14
|
+
classNames?: StarRatingClassNames;
|
|
15
|
+
}
|
|
16
|
+
/** Configurable accessible five-star control, also used by the progressive feedback prompt. */
|
|
17
|
+
export declare function StarRating({ value, onChange, disabled, groupLabel, starLabel, theme, onHoverChange, className, style, styles, classNames, }: StarRatingProps): JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StarRating = StarRating;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
/** Configurable accessible five-star control, also used by the progressive feedback prompt. */
|
|
8
|
+
function StarRating({ value, onChange, disabled = false, groupLabel = 'Rate this response', starLabel = (star) => `${star} star${star === 1 ? '' : 's'}`, theme, onHoverChange, className, style, styles, classNames, }) {
|
|
9
|
+
const mergedTheme = (0, types_1.createFeedbackTheme)(theme);
|
|
10
|
+
const [hovered, setHovered] = (0, react_1.useState)(null);
|
|
11
|
+
const displayedRating = hovered ?? (value === null ? 0 : Math.floor(value) + (value % 1 ? 0.5 : 0));
|
|
12
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-stars', className, classNames?.root].filter(Boolean).join(' '), style: { ...style, ...styles?.root }, role: "radiogroup", "aria-label": groupLabel, children: [1, 2, 3, 4, 5].map((star) => {
|
|
13
|
+
const fill = Math.max(0, Math.min(1, displayedRating - star + 1)) * 100;
|
|
14
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "radio", "aria-checked": Math.ceil(value ?? 0) === star, "aria-label": starLabel(star), disabled: disabled, className: classNames?.starButton, style: styles?.starButton, onMouseEnter: () => { setHovered(star); onHoverChange?.(star); }, onMouseLeave: () => { setHovered(null); onHoverChange?.(null); }, onFocus: () => { setHovered(star); onHoverChange?.(star); }, onBlur: () => { setHovered(null); onHoverChange?.(null); }, onClick: () => onChange(star), children: [(0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "M10.0003 2.5L12.2503 7.16667L17.3337 7.91667L13.667 11.5L14.5003 16.5833L10.0003 14.1667L5.50033 16.5833L6.33366 11.5L2.66699 7.91667L7.75033 7.16667L10.0003 2.5Z", stroke: mergedTheme.starEmpty ?? '#cbd5e1', strokeWidth: "1.33333", strokeLinejoin: "round" }) }), fill > 0 && ((0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", className: "cw-feedback-stars__filled", style: { clipPath: `inset(0 ${100 - fill}% 0 0)` }, children: (0, jsx_runtime_1.jsx)("path", { d: "M10.0003 2.5L12.2503 7.16667L17.3337 7.91667L13.667 11.5L14.5003 16.5833L10.0003 14.1667L5.50033 16.5833L6.33366 11.5L2.66699 7.91667L7.75033 7.16667L10.0003 2.5Z", fill: mergedTheme.accent ?? '#5b21e6', stroke: mergedTheme.accent ?? '#5b21e6', strokeWidth: "1.33333", strokeLinejoin: "round" }) }))] }, star));
|
|
15
|
+
}) }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface FeatureAvailabilityWindow {
|
|
2
|
+
enabled?: boolean;
|
|
3
|
+
startsAt?: string | null;
|
|
4
|
+
endsAt?: string | null;
|
|
5
|
+
uiNewTtl?: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface FeatureAvailabilityApi {
|
|
8
|
+
getFeatureAvailability(modeId: string, featureId: string): Promise<FeatureAvailabilityWindow | null>;
|
|
9
|
+
}
|
|
10
|
+
/** Replace this adapter with the feature-availability endpoint when its contract is available. */
|
|
11
|
+
export declare function createMockFeatureAvailabilityApi(windows: Record<string, FeatureAvailabilityWindow>): FeatureAvailabilityApi;
|
|
12
|
+
export interface HttpFeatureAvailabilityOptions {
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
getAuthToken: () => string | undefined;
|
|
15
|
+
fetcher?: typeof fetch;
|
|
16
|
+
}
|
|
17
|
+
/** Reads one mode's feature list, reusing the request across cards in that mode. */
|
|
18
|
+
export declare function createHttpFeatureAvailabilityApi(options: HttpFeatureAvailabilityOptions): FeatureAvailabilityApi;
|
|
19
|
+
export declare function isFeatureAvailable(window: FeatureAvailabilityWindow | null, now?: number): boolean;
|
|
20
|
+
/** Feature-only gate; chat responses with request IDs never call this API. */
|
|
21
|
+
export declare function useFeatureAvailability(modeId: string | undefined, featureId: string, api: FeatureAvailabilityApi | undefined, enabled: boolean): boolean;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMockFeatureAvailabilityApi = createMockFeatureAvailabilityApi;
|
|
4
|
+
exports.createHttpFeatureAvailabilityApi = createHttpFeatureAvailabilityApi;
|
|
5
|
+
exports.isFeatureAvailable = isFeatureAvailable;
|
|
6
|
+
exports.useFeatureAvailability = useFeatureAvailability;
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
/** Replace this adapter with the feature-availability endpoint when its contract is available. */
|
|
9
|
+
function createMockFeatureAvailabilityApi(windows) {
|
|
10
|
+
return {
|
|
11
|
+
async getFeatureAvailability(_modeId, featureId) {
|
|
12
|
+
return windows[featureId] ?? null;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const MODE_FEATURES_PATH = "/segment_click_analytics/v1/api/mode-features";
|
|
17
|
+
const MODE_FEATURES_CACHE_MS = 60000;
|
|
18
|
+
/** Reads one mode's feature list, reusing the request across cards in that mode. */
|
|
19
|
+
function createHttpFeatureAvailabilityApi(options) {
|
|
20
|
+
const cache = new Map();
|
|
21
|
+
return {
|
|
22
|
+
async getFeatureAvailability(modeId, featureId) {
|
|
23
|
+
const token = options.getAuthToken();
|
|
24
|
+
if (!modeId || !token)
|
|
25
|
+
throw new Error("Feature availability requires a mode ID and auth token");
|
|
26
|
+
const url = `${options.baseUrl.replace(/\/$/, "")}${MODE_FEATURES_PATH}/${encodeURIComponent(modeId)}`;
|
|
27
|
+
const cacheKey = `${url}:${token}`;
|
|
28
|
+
let cached = cache.get(cacheKey);
|
|
29
|
+
if (!cached || cached.expiresAt <= Date.now()) {
|
|
30
|
+
const promise = (options.fetcher ?? fetch)(url, {
|
|
31
|
+
headers: { accept: "application/json", Authorization: `Bearer ${token}` },
|
|
32
|
+
}).then(async (response) => {
|
|
33
|
+
if (!response.ok)
|
|
34
|
+
throw new Error(`Feature availability lookup failed (${response.status})`);
|
|
35
|
+
const body = await response.json();
|
|
36
|
+
if (body.mode_id !== modeId || !Array.isArray(body.features))
|
|
37
|
+
throw new Error("Feature availability response did not match the requested mode");
|
|
38
|
+
return body.features;
|
|
39
|
+
});
|
|
40
|
+
cached = { expiresAt: Date.now() + MODE_FEATURES_CACHE_MS, promise };
|
|
41
|
+
cache.set(cacheKey, cached);
|
|
42
|
+
void promise.catch(() => { if (cache.get(cacheKey)?.promise === promise)
|
|
43
|
+
cache.delete(cacheKey); });
|
|
44
|
+
}
|
|
45
|
+
const feature = (await cached.promise).find((row) => row.feature_id === featureId);
|
|
46
|
+
if (!feature)
|
|
47
|
+
return null;
|
|
48
|
+
return {
|
|
49
|
+
enabled: true,
|
|
50
|
+
startsAt: feature.start_date,
|
|
51
|
+
endsAt: feature.end_date,
|
|
52
|
+
uiNewTtl: feature.ui_new_ttl,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function isFeatureAvailable(window, now = Date.now()) {
|
|
58
|
+
if (!window || window.enabled === false)
|
|
59
|
+
return false;
|
|
60
|
+
const start = window.startsAt ? Date.parse(window.startsAt) : null;
|
|
61
|
+
const end = window.endsAt ? Date.parse(window.endsAt) : null;
|
|
62
|
+
const ttl = window.uiNewTtl ? Date.parse(window.uiNewTtl) : null;
|
|
63
|
+
return (start === null || (Number.isFinite(start) && start <= now)) &&
|
|
64
|
+
(end === null || (Number.isFinite(end) && now < end)) &&
|
|
65
|
+
(ttl === null || (Number.isFinite(ttl) && now < ttl));
|
|
66
|
+
}
|
|
67
|
+
/** Feature-only gate; chat responses with request IDs never call this API. */
|
|
68
|
+
function useFeatureAvailability(modeId, featureId, api, enabled) {
|
|
69
|
+
const [result, setResult] = (0, react_1.useState)(null);
|
|
70
|
+
const [now, setNow] = (0, react_1.useState)(() => Date.now());
|
|
71
|
+
const window = result && result.modeId === modeId && result.featureId === featureId ? result.window : null;
|
|
72
|
+
(0, react_1.useEffect)(() => {
|
|
73
|
+
if (!enabled || !modeId || !api)
|
|
74
|
+
return;
|
|
75
|
+
let cancelled = false;
|
|
76
|
+
setResult(null);
|
|
77
|
+
void api.getFeatureAvailability(modeId, featureId)
|
|
78
|
+
.then((window) => { if (!cancelled)
|
|
79
|
+
setResult({ modeId, featureId, window }); })
|
|
80
|
+
.catch(() => { if (!cancelled)
|
|
81
|
+
setResult({ modeId, featureId, window: null }); });
|
|
82
|
+
return () => { cancelled = true; };
|
|
83
|
+
}, [api, enabled, featureId, modeId]);
|
|
84
|
+
(0, react_1.useEffect)(() => {
|
|
85
|
+
if (!enabled || !window)
|
|
86
|
+
return;
|
|
87
|
+
const update = () => setNow(Date.now());
|
|
88
|
+
const boundaries = [window.startsAt, window.endsAt, window.uiNewTtl]
|
|
89
|
+
.filter((value) => Boolean(value))
|
|
90
|
+
.map((value) => Date.parse(value))
|
|
91
|
+
.filter((time) => Number.isFinite(time) && time > now);
|
|
92
|
+
const next = Math.min(...boundaries);
|
|
93
|
+
const timer = Number.isFinite(next)
|
|
94
|
+
? setTimeout(update, Math.min(next - now + 1, 2147483647))
|
|
95
|
+
: undefined;
|
|
96
|
+
document.addEventListener("visibilitychange", update);
|
|
97
|
+
return () => {
|
|
98
|
+
if (timer !== undefined)
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
document.removeEventListener("visibilitychange", update);
|
|
101
|
+
};
|
|
102
|
+
}, [enabled, now, window]);
|
|
103
|
+
return !enabled || (Boolean(modeId) && isFeatureAvailable(window, now));
|
|
104
|
+
}
|