@agent-native/toolkit 0.223.0 → 0.223.1-nightly-20261103004150

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.
@@ -6,15 +6,27 @@ import { Textarea } from "@agent-native/toolkit/ui/textarea";
6
6
  import { cn } from "@agent-native/toolkit/utils";
7
7
  import * as PopoverPrimitive from "@radix-ui/react-popover";
8
8
  import { IconThumbUp, IconThumbDown } from "@tabler/icons-react";
9
- import { useState, useCallback, useEffect, useId, useRef } from "react";
9
+ import {
10
+ useState,
11
+ useCallback,
12
+ useEffect,
13
+ useId,
14
+ useRef,
15
+ type ReactNode,
16
+ } from "react";
10
17
 
11
18
  import { submitFeedbackForm } from "../feedback/index.js";
12
19
 
13
20
  export interface ThumbsFeedbackProps {
14
21
  threadId: string;
15
- runId: string;
16
- messageSeq: number;
22
+ runId?: string;
23
+ messageSeq?: number;
17
24
  className?: string;
25
+ renderNegativeTrigger?: (
26
+ toggle: () => void,
27
+ open: boolean,
28
+ contentId: string,
29
+ ) => ReactNode;
18
30
  }
19
31
 
20
32
  type Selection = "up" | "down" | null;
@@ -24,7 +36,7 @@ const CONFIRMATION_DURATION_MS = 1400;
24
36
  type TextFeedbackDelivery = {
25
37
  value: string;
26
38
  observabilityDelivered: boolean;
27
- sharedFormDelivered: boolean;
39
+ sharedFormStatus: "pending" | "submitted" | "unconfigured";
28
40
  idempotencyKey: string;
29
41
  };
30
42
 
@@ -43,6 +55,7 @@ export function ThumbsFeedback({
43
55
  runId,
44
56
  messageSeq,
45
57
  className,
58
+ renderNegativeTrigger,
46
59
  }: ThumbsFeedbackProps) {
47
60
  const t = useT();
48
61
  const { session } = useSession();
@@ -51,7 +64,11 @@ export function ThumbsFeedback({
51
64
  const [popoverOpen, setPopoverOpen] = useState(false);
52
65
  const [textFeedback, setTextFeedback] = useState("");
53
66
  const [textSubmitting, setTextSubmitting] = useState(false);
67
+ const [textSubmitFailed, setTextSubmitFailed] = useState(false);
54
68
  const feedbackInputId = useId();
69
+ const feedbackContentId = useId();
70
+ const triggerAnchorRef = useRef<HTMLSpanElement>(null);
71
+ const restoreTriggerFocusRef = useRef(false);
55
72
  const feedbackInputRef = useRef<HTMLTextAreaElement>(null);
56
73
  const feedbackOpenedAtRef = useRef(0);
57
74
  const textSubmissionInFlightRef = useRef(false);
@@ -157,12 +174,13 @@ export function ThumbsFeedback({
157
174
  : {
158
175
  value,
159
176
  observabilityDelivered: false,
160
- sharedFormDelivered: false,
177
+ sharedFormStatus: "pending" as const,
161
178
  idempotencyKey: createFeedbackIdempotencyKey(),
162
179
  };
163
180
  textFeedbackDeliveryRef.current = delivery;
164
181
  textSubmissionInFlightRef.current = true;
165
182
  setTextSubmitting(true);
183
+ setTextSubmitFailed(false);
166
184
 
167
185
  const deliveries: Promise<void>[] = [];
168
186
  if (!delivery.observabilityDelivered) {
@@ -174,7 +192,7 @@ export function ThumbsFeedback({
174
192
  ),
175
193
  );
176
194
  }
177
- if (!delivery.sharedFormDelivered) {
195
+ if (delivery.sharedFormStatus === "pending") {
178
196
  deliveries.push(
179
197
  submitFeedbackForm({
180
198
  value,
@@ -184,8 +202,8 @@ export function ThumbsFeedback({
184
202
  activeRunId: runId,
185
203
  submitterEmail: session?.email,
186
204
  })
187
- .then(() => {
188
- delivery.sharedFormDelivered = true;
205
+ .then((result) => {
206
+ delivery.sharedFormStatus = result;
189
207
  })
190
208
  .catch((reason) => {
191
209
  console.error(
@@ -198,11 +216,16 @@ export function ThumbsFeedback({
198
216
 
199
217
  void Promise.all(deliveries)
200
218
  .then(() => {
201
- if (!delivery.observabilityDelivered || !delivery.sharedFormDelivered) {
219
+ if (
220
+ !delivery.observabilityDelivered ||
221
+ delivery.sharedFormStatus === "pending"
222
+ ) {
223
+ setTextSubmitFailed(true);
202
224
  return;
203
225
  }
204
226
  textFeedbackDeliveryRef.current = null;
205
227
  setTextFeedback("");
228
+ restoreTriggerFocusRef.current = true;
206
229
  setPopoverOpen(false);
207
230
  })
208
231
  .finally(() => {
@@ -216,53 +239,71 @@ export function ThumbsFeedback({
216
239
  <span role="status" aria-live="polite" className="sr-only">
217
240
  {confirmed ? t("agentChat.feedback.submitted") : ""}
218
241
  </span>
219
- <button
220
- type="button"
221
- aria-label={t("agentChat.feedback.thumbsUp")}
222
- title={t("agentChat.feedback.thumbsUp")}
223
- aria-pressed={selection === "up"}
224
- onClick={handleThumbsUp}
225
- className={cn(
226
- "flex h-6 w-6 items-center justify-center rounded transition-transform duration-200",
227
- selection === "up"
228
- ? "text-foreground"
229
- : "text-muted-foreground/70 hover:text-muted-foreground hover:bg-accent/50",
230
- confirmed === "up" && "scale-110",
231
- )}
232
- >
233
- <IconThumbUp
234
- size={16}
235
- stroke={selection === "up" ? 2.5 : 1.5}
236
- fill={selection === "up" ? "currentColor" : "none"}
237
- />
238
- </button>
242
+ {!renderNegativeTrigger && (
243
+ <button
244
+ type="button"
245
+ aria-label={t("agentChat.feedback.thumbsUp")}
246
+ title={t("agentChat.feedback.thumbsUp")}
247
+ aria-pressed={selection === "up"}
248
+ onClick={handleThumbsUp}
249
+ className={cn(
250
+ "flex h-6 w-6 items-center justify-center rounded transition-transform duration-200",
251
+ selection === "up"
252
+ ? "text-foreground"
253
+ : "text-muted-foreground/70 hover:text-muted-foreground hover:bg-accent/50",
254
+ confirmed === "up" && "scale-110",
255
+ )}
256
+ >
257
+ <IconThumbUp
258
+ size={16}
259
+ stroke={selection === "up" ? 2.5 : 1.5}
260
+ fill={selection === "up" ? "currentColor" : "none"}
261
+ />
262
+ </button>
263
+ )}
239
264
 
240
265
  <PopoverPrimitive.Root open={popoverOpen} onOpenChange={setPopoverOpen}>
241
- <PopoverPrimitive.Trigger asChild>
242
- <button
243
- type="button"
244
- aria-label={t("agentChat.feedback.thumbsDown")}
245
- title={t("agentChat.feedback.thumbsDown")}
246
- aria-pressed={selection === "down"}
247
- onClick={handleThumbsDown}
248
- className={cn(
249
- "flex h-6 w-6 items-center justify-center rounded transition-transform duration-200",
250
- selection === "down"
251
- ? "text-foreground"
252
- : "text-muted-foreground/70 hover:text-muted-foreground hover:bg-accent/50",
253
- confirmed === "down" && "scale-110",
254
- )}
255
- >
256
- <IconThumbDown
257
- size={16}
258
- stroke={selection === "down" ? 2.5 : 1.5}
259
- fill={selection === "down" ? "currentColor" : "none"}
260
- />
261
- </button>
262
- </PopoverPrimitive.Trigger>
266
+ {renderNegativeTrigger ? (
267
+ <PopoverPrimitive.Anchor asChild>
268
+ <span ref={triggerAnchorRef}>
269
+ {renderNegativeTrigger(
270
+ () => {
271
+ if (!popoverOpen) feedbackOpenedAtRef.current = Date.now();
272
+ setPopoverOpen((open) => !open);
273
+ },
274
+ popoverOpen,
275
+ feedbackContentId,
276
+ )}
277
+ </span>
278
+ </PopoverPrimitive.Anchor>
279
+ ) : (
280
+ <PopoverPrimitive.Trigger asChild>
281
+ <button
282
+ type="button"
283
+ aria-label={t("agentChat.feedback.thumbsDown")}
284
+ title={t("agentChat.feedback.thumbsDown")}
285
+ aria-pressed={selection === "down"}
286
+ onClick={handleThumbsDown}
287
+ className={cn(
288
+ "flex h-6 w-6 items-center justify-center rounded transition-transform duration-200",
289
+ selection === "down"
290
+ ? "text-foreground"
291
+ : "text-muted-foreground/70 hover:text-muted-foreground hover:bg-accent/50",
292
+ confirmed === "down" && "scale-110",
293
+ )}
294
+ >
295
+ <IconThumbDown
296
+ size={16}
297
+ stroke={selection === "down" ? 2.5 : 1.5}
298
+ fill={selection === "down" ? "currentColor" : "none"}
299
+ />
300
+ </button>
301
+ </PopoverPrimitive.Trigger>
302
+ )}
263
303
 
264
304
  <PopoverPrimitive.Portal>
265
305
  <PopoverPrimitive.Content
306
+ id={feedbackContentId}
266
307
  side="bottom"
267
308
  align="start"
268
309
  sideOffset={4}
@@ -271,6 +312,27 @@ export function ThumbsFeedback({
271
312
  event.preventDefault();
272
313
  feedbackInputRef.current?.focus();
273
314
  }}
315
+ onEscapeKeyDown={() => {
316
+ restoreTriggerFocusRef.current = true;
317
+ }}
318
+ onInteractOutside={(event) => {
319
+ if (
320
+ renderNegativeTrigger &&
321
+ event.target instanceof Node &&
322
+ triggerAnchorRef.current?.contains(event.target)
323
+ )
324
+ event.preventDefault();
325
+ }}
326
+ onCloseAutoFocus={(event) => {
327
+ if (!renderNegativeTrigger) return;
328
+ event.preventDefault();
329
+ if (restoreTriggerFocusRef.current) {
330
+ triggerAnchorRef.current
331
+ ?.querySelector<HTMLButtonElement>("button")
332
+ ?.focus();
333
+ }
334
+ restoreTriggerFocusRef.current = false;
335
+ }}
274
336
  className="z-[300] w-[min(320px,calc(100vw-24px))] overflow-hidden rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-lg outline-none origin-[var(--radix-popover-content-transform-origin)]"
275
337
  >
276
338
  <form
@@ -307,6 +369,11 @@ export function ThumbsFeedback({
307
369
  maxLength={2000}
308
370
  className="min-h-20 resize-none px-2.5 py-2 text-xs"
309
371
  />
372
+ {textSubmitFailed ? (
373
+ <p role="alert" className="text-xs text-destructive">
374
+ {t("agentChat.composer.submitFailed")}
375
+ </p>
376
+ ) : null}
310
377
  <div className="flex items-center justify-between gap-2">
311
378
  <span className="text-[11px] text-muted-foreground/75">
312
379
  {t("agentChat.feedback.keyboardHint").replace(