Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
total overhaul with new inputMode option
  • Loading branch information
laander committed Sep 9, 2025
commit cd66efc7d99f4c1322063ac353fc3fef14cdd92f
17 changes: 10 additions & 7 deletions packages/browser-sdk/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,25 @@
const urlParams = new URLSearchParams(window.location.search);
const publishableKey = urlParams.get("publishableKey");
const flagKey = urlParams.get("flagKey") ?? "huddles";

const onFeedbackClick = (e) => {
reflag.requestFeedback({
flagKey,
inputMode: "comment-and-score",
position: { type: "POPOVER", anchor: e.currentTarget },
});
};
</script>
<style>
body {
font-family: sans-serif;
}
#start-huddle {
border: 1px solid black;
padding: 10px;
margin: 100px;
}
</style>
<div id="start-huddle" style="display: none">
<button
onClick="bucket.requestFeedback({flagKey, requireSatisfactionScore: false, position: {type: 'POPOVER', anchor: event.currentTarget}})"
>
Give feedback!
</button>
<button onClick="onFeedbackClick(event)">Give feedback!</button>
<button onClick="reflag.track(flagKey)">Start huddle</button>
</div>

Expand Down
38 changes: 22 additions & 16 deletions packages/browser-sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
RequestFeedbackOptions,
} from "./feedback/feedback";
import * as feedbackLib from "./feedback/ui";
import { OpenFeedbackFormOptions } from "./feedback/ui/types";
import {
CheckEvent,
FallbackFlagOverride,
Expand All @@ -24,6 +25,12 @@ import { showToolbarToggle } from "./toolbar";
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
const isNode = typeof document === "undefined"; // deno supports "window" but not "document" according to https://remix.run/docs/en/main/guides/gotchas

const VALID_INPUT_MODES: Required<OpenFeedbackFormOptions["inputMode"]>[] = [
"comment-and-score",
"comment-only",
"score-only",
];

/**
* (Internal) User context.
*
Expand Down Expand Up @@ -701,6 +708,13 @@ export class ReflagClient {
return;
}

if (options.inputMode && !VALID_INPUT_MODES.includes(options.inputMode)) {
this.logger.error(
"`requestFeedback` call ignored. Invalid `inputMode` provided",
);
return;
}

const feedbackData = {
flagKey: options.flagKey,
companyId:
Expand All @@ -720,30 +734,22 @@ export class ReflagClient {
position: options.position || this.requestFeedbackOptions.position,
translations:
options.translations || this.requestFeedbackOptions.translations,
openWithCommentVisible: options.openWithCommentVisible,
requireSatisfactionScore: options.requireSatisfactionScore,
inputMode: options.inputMode,
onClose: options.onClose,
onDismiss: options.onDismiss,
onScoreSubmit: async (data) => {
const res = await this.feedback({
...feedbackData,
...data,
});

if (res) {
const json = await res.json();
return { feedbackId: json.feedbackId };
}
return { feedbackId: undefined };
},
onSubmit: async (data) => {
// Default onSubmit handler
await this.feedback({
const res = await this.feedback({
...feedbackData,
...data,
});

options.onAfterSubmit?.(data);
if (!res) return;

const json = res ? await res.json() : {};
options.onAfterSubmit?.({ ...data, ...json });

return json;
},
});
}, 1);
Expand Down
18 changes: 5 additions & 13 deletions packages/browser-sdk/src/feedback/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ export class AutoFeedback {
message: FeedbackPrompt,
completionHandler: FeedbackPromptCompletionHandler,
) {
let feedbackId: string | undefined = undefined;
const feedbackId: string | undefined = undefined;

await this.feedbackPromptEvent({
promptId: message.promptId,
Expand Down Expand Up @@ -435,24 +435,16 @@ export class AutoFeedback {
feedbackLib.openFeedbackForm({
key: message.featureId,
title: message.question,
onScoreSubmit: async (data) => {
const res = await replyCallback(data);
feedbackId = res.feedbackId;
return { feedbackId: res.feedbackId };
},
onSubmit: async (data) => {
await replyCallback(data);
options.onAfterSubmit?.(data);
const res = await replyCallback(data);
if (!res) return;
options.onAfterSubmit?.({ ...data, ...res });
return res;
},
onDismiss: () => replyCallback(null),
position: this.position,
translations: this.feedbackTranslations,
...options,
openWithCommentVisible:
options.requireSatisfactionScore === false
? true
: options.openWithCommentVisible,
requireSatisfactionScore: options.requireSatisfactionScore,
});
},
};
Expand Down
8 changes: 4 additions & 4 deletions packages/browser-sdk/src/feedback/ui/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
user-select: none;
position: relative;
white-space: nowrap;
height: 2rem;
height: 1.85rem;
padding-inline-start: 0.75rem;
padding-inline-end: 0.75rem;
gap: 0.5em;
Expand All @@ -27,11 +27,11 @@
&.primary {
background-color: var(
--reflag-feedback-dialog-primary-button-background-color,
white
#09090b
);
color: var(--reflag-feedback-dialog-primary-button-color, #1e1f24);
color: var(--reflag-feedback-dialog-primary-button-color, white);
border: 1px solid
var(--reflag-feedback-dialog-primary-border-color, #d8d9df);
var(--reflag-feedback-dialog-primary-border-color, #09090b);
}

&:disabled {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-sdk/src/feedback/ui/FeedbackDialog.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.dialog {
position: fixed;
width: 210px;
padding: 16px 22px 10px;
padding: 16px 16px 16px;
font-size: var(--reflag-feedback-dialog-font-size, 1rem);
font-family: var(
--reflag-feedback-dialog-font-family,
Expand Down
51 changes: 19 additions & 32 deletions packages/browser-sdk/src/feedback/ui/FeedbackDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { FeedbackForm } from "./FeedbackForm";
import styles from "./index.css?inline";
import { RadialProgress } from "./RadialProgress";
import {
FeedbackScoreSubmission,
FeedbackSubmission,
OpenFeedbackFormOptions,
WithRequired,
Expand All @@ -25,26 +24,19 @@ export type FeedbackDialogProps = WithRequired<
const INACTIVE_DURATION_MS = 20 * 1000;
const SUCCESS_DURATION_MS = 3 * 1000;

export type FormState = "idle" | "submitting" | "submitted" | "error";

export const FeedbackDialog: FunctionComponent<FeedbackDialogProps> = ({
key,
title = DEFAULT_TRANSLATIONS.DefaultQuestionLabel,
position,
translations = DEFAULT_TRANSLATIONS,
openWithCommentVisible = false,
requireSatisfactionScore = true,
inputMode = "comment-and-score",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about the background here. My thinking is that if someone submits a comment, we'll eventually be able to deduct the sentiment. So it ends up feeling weird to ever have the comment first and then the satisfaction second? I could imagine the options being:

  • comment only
  • satisfaction + comment shows up (default today)
  • satisfaction + comment visible (default today)

I know this is late feedback, apologies.

Copy link
Contributor Author

@laander laander Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inspiration was Vercel (from the Linear issue):

@makwarth: Here's how Vercel does it, which is the way to do it, imo:
CleanShot 2025-01-15 at 11  53 11@2x

I'm curious about the background here. My thinking is that if someone submits a comment, we'll eventually be able to deduct the sentiment. So it ends up feeling weird to ever have the comment first and then the satisfaction second

Wouldn't that be an argument for the opposite? Why have the score first if it can be derived from the comment?

There can also be a value in specifying the score manually like Vercel; either to be clear about intent or if you don't have time to write a comment but still want to give a nod up/down.

satisfaction + comment shows up

In general, we're moving away from a primary satisfaction score towards it being additional/optional. Having two out of three options put score front and center is misleading and not useful IMO.

I don't see much use for the sequential screens anymore and it complicates implementation a lot. IF you're looking to gather a score for a bigger audience (like post GA release), I think a score-only UI is fine? For all other cases, the comment field is preferred

onClose,
onDismiss,
onSubmit,
onScoreSubmit,
}) => {
// If requireSatisfactionScore is false, always show comment field
const effectiveOpenWithCommentVisible =
requireSatisfactionScore === false ? true : openWithCommentVisible;

const [feedbackId, setFeedbackId] = useState<string | undefined>(undefined);
const [scoreState, setScoreState] = useState<
"idle" | "submitting" | "submitted"
>("idle");
const [formState, setFormState] = useState<FormState>("idle");

const { isOpen, close } = useDialog({ onClose, initialValue: true });

Expand All @@ -56,24 +48,21 @@ export const FeedbackDialog: FunctionComponent<FeedbackDialogProps> = ({

const submit = useCallback(
async (data: Omit<FeedbackSubmission, "feedbackId">) => {
await onSubmit({ ...data, feedbackId });
autoClose.startWithDuration(SUCCESS_DURATION_MS);
},
[autoClose, feedbackId, onSubmit],
);

const submitScore = useCallback(
async (data: Omit<FeedbackScoreSubmission, "feedbackId">) => {
if (onScoreSubmit !== undefined) {
setScoreState("submitting");

const res = await onScoreSubmit({ ...data, feedbackId });
setFeedbackId(res.feedbackId);
setScoreState("submitted");
try {
setFormState("submitting");
const res = await onSubmit({ ...data });
if (!res) throw new Error("Failed to submit feedback");
setFormState("submitted");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Feedback Dialog Fails on Void Return

The FeedbackDialog's submit function incorrectly treats a void or undefined return from the onSubmit callback as an error. While onSubmit's type signature allows void, the current logic throws an error and shows an error state to the user, even when the submission might have been handled successfully.

Fix in Cursor Fix in Web

autoClose.startWithDuration(SUCCESS_DURATION_MS);
} catch (error) {
setFormState("error");
console.error("Couldn't submit feedback with Reflag:", error);
throw error;
}
},
[feedbackId, onScoreSubmit],
[autoClose, onSubmit],
);

const dismiss = useCallback(() => {
autoClose.stop();
close();
Expand All @@ -94,14 +83,12 @@ export const FeedbackDialog: FunctionComponent<FeedbackDialogProps> = ({
<>
<FeedbackForm
key={key}
openWithCommentVisible={effectiveOpenWithCommentVisible}
inputMode={inputMode}
question={title}
requireSatisfactionScore={requireSatisfactionScore}
scoreState={scoreState}
t={{ ...DEFAULT_TRANSLATIONS, ...translations }}
onInteraction={autoClose.stop}
onScoreSubmit={submitScore}
onSubmit={submit}
formState={formState}
/>

<button class="close" onClick={dismiss}>
Expand All @@ -111,7 +98,7 @@ export const FeedbackDialog: FunctionComponent<FeedbackDialogProps> = ({
progress={1.0 - autoClose.elapsedFraction}
/>
)}
<Close />
<Close width={18} height={18} />
</button>
</>
</Dialog>
Expand Down
13 changes: 4 additions & 9 deletions packages/browser-sdk/src/feedback/ui/FeedbackForm.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.container {
overflow-y: hidden;
transition: max-height 400ms cubic-bezier(0.65, 0, 0.35, 1);
max-height: 999px;
}

.form {
Expand Down Expand Up @@ -33,21 +34,16 @@
align-items: flex-start;
gap: 10px;
transition: opacity 400ms cubic-bezier(0.65, 0, 0.35, 1);

opacity: 0;
position: absolute;
top: 0;
left: 0;
}

.title {
color: var(--reflag-feedback-dialog-color, #1e1f24);
font-size: 15px;
font-size: 14px;
font-weight: 400;
line-height: 115%;
text-wrap: balance;
max-width: calc(100% - 20px);
margin-bottom: 6px;
margin-bottom: 0px;
line-height: 1.3;
}

Expand Down Expand Up @@ -111,8 +107,7 @@
.error {
margin: 0;
color: var(--reflag-feedback-dialog-error-color, #e53e3e);
font-size: 0.8125em;
font-weight: 500;
font-size: 13px;
}

.submitted {
Expand Down
Loading
Loading