@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 CHANGED
@@ -1,3 +1,64 @@
1
- # Temporary Holding Version
1
+ # Response feedback components
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `useFeedbackFlow(requestId, featureId)` owns the rating and draft for one response. Render `FeedbackPrompt` and `FeedbackModal` with the same controller. The caller decides where to place the prompt and supplies the modal's `anchorRef` so the popup appears above it.
4
+
5
+ For chat timelines, `FeedbackResponseControls` owns that controller per response. Give it the response's `requestId`, `featureId`, message index, chat shell and sticky host refs, and viewport state. It renders one prompt at the heading for a newly completed response, beside the response's actions at its end, or in the sticky host while that response is active in the viewport. Each response keeps a separate draft.
6
+
7
+ ```tsx
8
+ const flow = useFeedbackFlow(requestId, featureId);
9
+ const promptRef = useRef<HTMLDivElement>(null);
10
+ const copy = createFeedbackCopy({
11
+ prompt: 'Rate this answer',
12
+ timeOptions: [5, 15, 30].map((value) => ({ value, label: `${value} minutes` })),
13
+ severityLabels: { critical: 'Blocking' },
14
+ });
15
+ const theme = { accent: '#6d28d9', text: '#1e293b', muted: '#64748b' };
16
+
17
+ return <>
18
+ <FeedbackPrompt flow={flow} promptRef={promptRef} copy={copy} theme={theme} />
19
+ <FeedbackModal
20
+ flow={flow}
21
+ anchorRef={promptRef}
22
+ copy={copy}
23
+ theme={theme}
24
+ slots={{ gap: <YourOptionalGapContent /> }}
25
+ onSubmit={(feedback) => submitFeedback(feedback)}
26
+ />
27
+ </>;
28
+ ```
29
+
30
+ `createFeedbackCopy` accepts partial overrides for every visible label, question, hint, placeholder, and option. `FeedbackTheme` controls the prompt, popup, tooltip, overlay, and severity colors. `slots` and `extraContent` allow callers to add their own React content. The `onSubmit` payload includes the request ID, feature ID, numeric rating, time saved, useful text, gap descriptions, impact levels, and screenshot `File` objects.
31
+
32
+ Useful and gap answers shorter than `usefulMinimumWords` show a configurable helper directly below their question and keep the existing answer in the same textarea. Continue is hidden until the minimum is met. Decimal ratings advance on selection without an Enter hint. Each completed step is saved by POST or PATCH with `metadata.feedbackProgress`; an interrupted response reopens at its next unanswered step, while only the final save marks feedback complete. The remote template's local tarball must be updated separately to receive these source changes.
33
+
34
+ `FeedbackExperience` and `FeedbackResponseControls` use `mockFeedbackApi` by default. The mock stores records in browser `localStorage` (with an in-memory fallback), and fetches an existing record on mount so Review can reopen previously submitted answers. `upsertFeedback` handles both first submission and later edits; `getFeedback` looks up a record by `query_id`, `feature_id`, and `user_email`. Supply a custom `FeedbackApi` through the `api` prop when a backend is available. The host's `onSubmit` callback still runs after a successful save.
35
+
36
+ The API record mirrors the proposed tables: `feedback_form`, `positive_feedback`, `gaps`, and `feedback_attachment`. Chat `query_id` comes from tracking metadata; feature feedback only needs `featureId`. `user_email`/`trace_id` come from tracking. IDs and timestamps remain stable on updates. Embeddings are backend-generated and omitted. Screenshot references use `mock://` URLs until an upload endpoint exists; the mock does not upload file bytes.
37
+
38
+ `createHttpFeedbackApi({ baseUrl, readBaseUrl, getAuthToken, uploadAttachment })` reads chat feedback with GET `/feedback?user_email=...&session_id=...` and non-chat feedback with GET `/feedback?user_email=...&feature_id=...`. When a session ID is supplied, the GET never falls back to `feature_id`, even if the response query ID is not available yet. It selects the relevant chat record from the session's results by its `query_id`. POST creates and PATCH `/feedback/{feedback_id}` updates records. A chat submission requires `metadata.queryId` and sends `query_id`; non-chat feedback sends `feature_id`, never both. If the non-chat feature ID is not a UUID, the adapter derives a stable UUID-shaped ID from the host IDs. `getAuthToken` supplies a fresh Bearer token for every GET, POST, and PATCH request. `readBaseUrl` is optional when reads use a different deployment. Browser-local state is only a fallback when GET fails. The request body follows the live API's `positive_feedback` and `negative_feedback` arrays. Hosts can provide an upload function; it must upload each `File` and return its `gs://` URL before the feedback request is sent. Original host IDs remain in metadata for matching older records.
39
+
40
+ Hosts may include `metadata.queryId`, `metadata.sessionId`, and `metadata.requestId` to send top-level `query_id`, `session_id`, and `request_id` with feedback. Chat reads filter by `session_id`, then match each response by `query_id`; `query_id` is never a GET parameter. All three are optional for non-chat integrations. `FeedbackExperience.requestId` is optional for features; its internal UI identity falls back to `featureId`, but no `request_id` or `query_id` is sent in the feature API payload.
41
+
42
+ For screenshots, use `createFeedbackScreenshotUploader({ baseUrl: VITE_BASE_API_URL, getAuthToken })` as `uploadAttachment` and `createFeedbackScreenshotUrlResolver` with the same options as `getAttachmentPreviewUrl`. The library POSTs the file as multipart form data to `/backend/gcs/direct-gcs-upload` (or `/gcs/direct-gcs-upload` when the base URL already ends in `/backend`) and sends the returned `gs_url` in the feedback attachment payload. To preview a saved screenshot, it POSTs `{ gsutil_url: gsUrl }` to CW's `/gcs/get-signed-url/` and uses the returned short-lived `signed_url`, rather than the upload response's unsigned `public_url`. Signed URLs are cached in memory by API base URL, auth token, and GCS path until just before expiry; concurrent requests share one call, and a failed image can force-refresh its URL. Clicking the thumbnail opens a larger preview; the remove icon takes that screenshot out of the draft and subsequent feedback payload without deleting the GCS object. The form shows an error and stays open if upload or feedback submission fails. No project ID is needed for this endpoint.
43
+
44
+ For mode tracking, pass `tracking` to `FeedbackExperience`, or call `createFeedbackMetadata(tracking)` when using `useFeedbackFlow` directly. Standard fields are `mode`, `modeId`, `owner`, `featureContext`, `userEmail`, and `traceId`. Put any integration-specific fields under `custom`; they are included alongside the standard fields in `submission.metadata`. For example:
45
+
46
+ ```tsx
47
+ <FeedbackExperience
48
+ featureId={featureId}
49
+ featureAvailabilityModeId={modeId}
50
+ featureAvailabilityApi={createHttpFeatureAvailabilityApi({
51
+ baseUrl: 'https://devllmstudio.creativeworkspace.ai',
52
+ getAuthToken: () => authStore.getState().token,
53
+ })}
54
+ tracking={{
55
+ mode: 'Agent Genie',
56
+ modeId,
57
+ owner: user.email,
58
+ featureContext: 'BlockedIncidents',
59
+ }}
60
+ onSubmit={(feedback) => saveFeedback(feedback)}
61
+ />
62
+ ```
63
+
64
+ The availability API is consulted only when `requestId` is omitted. It GETs `/segment_click_analytics/v1/api/mode-features/{modeId}` with the current Bearer token, shares one list request across cards, and matches `feature_id`. A feature card is hidden until its matching response arrives. Each non-null `start_date`, `end_date`, and `ui_new_ttl` is checked; a null field adds no restriction. Missing features or failed responses hide the card. `createMockFeatureAvailabilityApi` remains available for tests. The existing `metadata` prop still works. When both are provided, explicit standard fields in `tracking` take precedence. The components use default copy when it is omitted or incomplete. `FeedbackErrorBoundary` can wrap each surface so a rendering error hides only the feedback UI. The modal also tolerates browsers without `ResizeObserver` and shows a retry message only if the final save fails.
@@ -0,0 +1,17 @@
1
+ import { Component, type ReactNode } from 'react';
2
+ interface Props {
3
+ children: ReactNode;
4
+ resetKey: string;
5
+ }
6
+ interface State {
7
+ failed: boolean;
8
+ }
9
+ /** A feedback rendering failure must not replace the surrounding chat. */
10
+ export declare class FeedbackErrorBoundary extends Component<Props, State> {
11
+ state: State;
12
+ static getDerivedStateFromError(): State;
13
+ componentDidCatch(error: Error): void;
14
+ componentDidUpdate(previousProps: Props): void;
15
+ render(): ReactNode;
16
+ }
17
+ export {};
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeedbackErrorBoundary = void 0;
4
+ const react_1 = require("react");
5
+ /** A feedback rendering failure must not replace the surrounding chat. */
6
+ class FeedbackErrorBoundary extends react_1.Component {
7
+ constructor() {
8
+ super(...arguments);
9
+ this.state = { failed: false };
10
+ }
11
+ static getDerivedStateFromError() {
12
+ return { failed: true };
13
+ }
14
+ componentDidCatch(error) {
15
+ console.error('Feedback UI failed to render', error);
16
+ }
17
+ componentDidUpdate(previousProps) {
18
+ if (previousProps.resetKey !== this.props.resetKey && this.state.failed) {
19
+ this.setState({ failed: false });
20
+ }
21
+ }
22
+ render() {
23
+ return this.state.failed ? null : this.props.children;
24
+ }
25
+ }
26
+ exports.FeedbackErrorBoundary = FeedbackErrorBoundary;
@@ -0,0 +1,27 @@
1
+ import { type CSSProperties, type ReactNode } from "react";
2
+ import { type FeedbackCopy, type FeedbackExperienceClassNames, type FeedbackExperienceStyles, type FeedbackMetadata, type FeedbackSubmission, type FeedbackTheme, type FeedbackTrackingContext } from "./types";
3
+ import type { FeedbackApi } from "./feedbackApi";
4
+ import { type FeatureAvailabilityApi } from "./featureAvailability";
5
+ export interface FeedbackExperienceProps {
6
+ /** Optional for feature-level feedback; chat responses should provide their request ID. */
7
+ requestId?: string;
8
+ featureId: string;
9
+ /** Consulted only for feature-level feedback, never for chat responses. */
10
+ featureAvailabilityApi?: FeatureAvailabilityApi;
11
+ /** Backend mode UUID used to list its features. */
12
+ featureAvailabilityModeId?: string;
13
+ metadata?: FeedbackMetadata;
14
+ tracking?: FeedbackTrackingContext;
15
+ copy?: Partial<FeedbackCopy>;
16
+ theme?: FeedbackTheme;
17
+ onSubmit?: (submission: FeedbackSubmission) => void | Promise<void>;
18
+ api?: FeedbackApi;
19
+ className?: string;
20
+ style?: CSSProperties;
21
+ styles?: FeedbackExperienceStyles;
22
+ classNames?: FeedbackExperienceClassNames;
23
+ align?: "start" | "center" | "end";
24
+ slots?: Partial<Record<"rating" | "time" | "gap" | "success", ReactNode>>;
25
+ }
26
+ /** Standalone experience for hosts that do not need chat-specific anchoring. */
27
+ export declare function FeedbackExperience(props: FeedbackExperienceProps): JSX.Element | null;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeedbackExperience = FeedbackExperience;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const react_dom_1 = require("react-dom");
7
+ const FeedbackErrorBoundary_1 = require("./FeedbackErrorBoundary");
8
+ const FeedbackModal_1 = require("./FeedbackModal");
9
+ const FeedbackPrompt_1 = require("./FeedbackPrompt");
10
+ const types_1 = require("./types");
11
+ const tracking_1 = require("./tracking");
12
+ const useFeedbackFlow_1 = require("./useFeedbackFlow");
13
+ const useFeedbackPersistence_1 = require("./useFeedbackPersistence");
14
+ const featureAvailability_1 = require("./featureAvailability");
15
+ /** Standalone experience for hosts that do not need chat-specific anchoring. */
16
+ function FeedbackExperience(props) {
17
+ const available = (0, featureAvailability_1.useFeatureAvailability)(props.featureAvailabilityModeId, props.featureId, props.featureAvailabilityApi, !props.requestId);
18
+ if (!available)
19
+ return null;
20
+ return (0, jsx_runtime_1.jsx)(FeedbackExperienceContent, { ...props, requestId: props.requestId || props.featureId });
21
+ }
22
+ function FeedbackExperienceContent({ requestId, featureId, metadata, tracking, copy, theme, onSubmit, api, className, style, styles, classNames, align = "start", slots, }) {
23
+ const feedbackMetadata = (0, tracking_1.createFeedbackMetadata)(tracking, metadata);
24
+ const flow = (0, useFeedbackFlow_1.useFeedbackFlow)(requestId, featureId, feedbackMetadata);
25
+ const { saveFeedback, saveProgress, saveOnDismiss, isLoading, isSaving, saveError } = (0, useFeedbackPersistence_1.useFeedbackPersistence)(flow, feedbackMetadata, onSubmit, api);
26
+ const anchorRef = (0, react_1.useRef)(null);
27
+ const [promptPosition, setPromptPosition] = (0, react_1.useState)(null);
28
+ const prompt = ((0, jsx_runtime_1.jsx)(FeedbackPrompt_1.FeedbackPrompt, { flow: flow, loading: isLoading, disabled: isSaving, promptRef: anchorRef, copy: copy ?? types_1.defaultFeedbackCopy, theme: theme, styles: styles?.prompt, classNames: classNames?.prompt, onRatingSelected: (rating) => {
29
+ const nextStep = rating === 3 || rating === 4 ? "rating" : rating <= 2 ? "gap" : "time";
30
+ void saveProgress({ requestId, featureId, rating, gaps: [], metadata: feedbackMetadata }, nextStep)
31
+ .catch((error) => console.error("Could not save rating", error));
32
+ }, onBeforeOpen: () => {
33
+ const rect = anchorRef.current?.getBoundingClientRect();
34
+ if (rect)
35
+ setPromptPosition({ left: rect.left, top: rect.top });
36
+ } }));
37
+ const floating = flow.step !== "closed" && promptPosition && typeof document !== "undefined";
38
+ return ((0, jsx_runtime_1.jsx)(FeedbackErrorBoundary_1.FeedbackErrorBoundary, { resetKey: requestId, children: (0, jsx_runtime_1.jsxs)("div", { className: [className, classNames?.container].filter(Boolean).join(" "), style: {
39
+ display: "flex",
40
+ justifyContent: align === "end" ? "flex-end" : align === "center" ? "center" : "flex-start",
41
+ ...style,
42
+ ...styles?.container,
43
+ }, children: [floating
44
+ ? (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { className: classNames?.floatingWrapper, style: {
45
+ position: "fixed",
46
+ left: promptPosition.left,
47
+ top: promptPosition.top,
48
+ zIndex: 10002,
49
+ width: "max-content",
50
+ maxWidth: "calc(100vw - 24px)",
51
+ ...styles?.floatingWrapper,
52
+ }, children: prompt }), document.body)
53
+ : prompt, (0, jsx_runtime_1.jsx)(FeedbackModal_1.FeedbackModal, { flow: flow, anchorRef: anchorRef, copy: copy, theme: theme, onSubmit: saveFeedback, onProgress: saveProgress, onDismiss: saveOnDismiss, saveError: saveError, resolveScreenshotUrl: api?.getAttachmentPreviewUrl, slots: slots, styles: styles?.modal, classNames: classNames?.modal })] }) }));
54
+ }
@@ -0,0 +1,27 @@
1
+ import { type CSSProperties, type ReactNode, type RefObject } from 'react';
2
+ import { type FeedbackCopy, type FeedbackModalClassNames, type FeedbackModalStyles, type FeedbackProgressStep, type FeedbackSubmission, type FeedbackTheme } from './types';
3
+ import type { FeedbackFlowController } from './useFeedbackFlow';
4
+ export interface FeedbackModalProps {
5
+ flow: FeedbackFlowController;
6
+ copy?: Partial<FeedbackCopy>;
7
+ theme?: FeedbackTheme;
8
+ onSubmit?: (feedback: FeedbackSubmission) => void | Promise<void>;
9
+ onProgress?: (feedback: FeedbackSubmission, nextStep: FeedbackProgressStep) => Promise<void>;
10
+ onDismiss?: (feedback: FeedbackSubmission, nextStep: FeedbackProgressStep, completed: boolean) => Promise<void>;
11
+ saving?: boolean;
12
+ saveError?: boolean;
13
+ resolveScreenshotUrl?: (gcsUrl: string, forceRefresh?: boolean) => Promise<string>;
14
+ anchorRef: RefObject<HTMLDivElement>;
15
+ overlayHostRef?: RefObject<HTMLElement>;
16
+ anchorKey?: boolean;
17
+ slots?: Partial<Record<'rating' | 'time' | 'gap' | 'success', ReactNode>>;
18
+ className?: string;
19
+ style?: CSSProperties;
20
+ styles?: FeedbackModalStyles;
21
+ classNames?: FeedbackModalClassNames;
22
+ backdropClassName?: string;
23
+ backdropStyle?: CSSProperties;
24
+ dialogClassName?: string;
25
+ dialogStyle?: CSSProperties;
26
+ }
27
+ export declare function FeedbackModal(props: FeedbackModalProps): JSX.Element | null;