// Reply and session feedback shared by web and native: the one feedback read // per session (with recovery), saved turn ratings, the submission payload with // a retained idempotency key, and the dialog copy. No DOM, no React. import type { CreateFeedbackRequest, Feedback, FeedbackSentiment, OpenGeniClient, } from "@opengeni/sdk"; import { type OpenGeniText } from "./messages/runtime"; export type FeedbackClient = Pick; /** One shared read per session, with recovery after a transient network failure. */ export function loadSessionFeedback( client: Pick, workspaceId: string, sessionId: string, onLoaded: (result: Awaited>) => void, ): () => void { let current = true; let delay = 1_000; let timer: ReturnType | undefined; const load = () => { void client.listOwnFeedback(workspaceId, { sessionId, includeTurns: true }).then( (result) => { if (current) onLoaded(result); }, () => { if (!current) return; timer = setTimeout(load, delay); delay = Math.min(delay * 2, 30_000); }, ); }; load(); return () => { current = false; clearTimeout(timer); }; } /** The first saved rating per turn (the list is newest-first). */ export function turnRatingsFromFeedback( feedback: readonly Feedback[], ): Record { const ratings: Record = {}; for (const entry of feedback) { if (entry.turnId && entry.sentiment && !ratings[entry.turnId]) ratings[entry.turnId] = entry.sentiment; } return ratings; } export type FeedbackPayload = Omit; /** * The request to send, reusing the previous idempotency key when the user * retries the identical submission (a lost response must not duplicate it). */ export function feedbackSubmission( payload: FeedbackPayload, previous: CreateFeedbackRequest | null, newKey: () => string, ): CreateFeedbackRequest { if ( previous && previous.sessionId === payload.sessionId && previous.turnId === payload.turnId && previous.sentiment === payload.sentiment && previous.comment === payload.comment ) { return previous; } return { ...payload, idempotencyKey: newKey() }; } export function feedbackDialogCopy( t: OpenGeniText, target: { sessionId?: string | undefined; turnId?: string | undefined; }, ) { return { title: target.turnId ? t("sessionFeedbackModel.rateThisReply") : target.sessionId ? t("sessionFeedbackModel.rateThisSession") : t("sessionFeedbackModel.sendFeedback"), description: target.sessionId ? t("sessionFeedbackModel.shareWhatWorkedOrWhat") : t("sessionFeedbackModel.tellUsWhatCouldMake"), }; } export function feedbackSentimentLabel(t: OpenGeniText, sentiment: FeedbackSentiment): string { return sentiment === "positive" ? t("sessionFeedbackModel.thumbsUp") : t("sessionFeedbackModel.thumbsDown"); }