@repzo/widget-react-native 1.0.5 → 1.0.6
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/lib/commonjs/api/client.js +8 -0
- package/lib/commonjs/api/client.js.map +1 -1
- package/lib/commonjs/i18n.js +14 -0
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/locales.js +13 -0
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +25 -0
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/types.js +3 -0
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +13 -0
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/RatingCard.js +347 -0
- package/lib/commonjs/ui/components/RatingCard.js.map +1 -0
- package/lib/commonjs/ui/screens/chat/ChatView.js +22 -1
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/api/client.js +8 -0
- package/lib/module/api/client.js.map +1 -1
- package/lib/module/i18n.js +14 -0
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/locales.js +13 -0
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/widget-store.js +25 -0
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/types.js +3 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +13 -0
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/RatingCard.js +341 -0
- package/lib/module/ui/components/RatingCard.js.map +1 -0
- package/lib/module/ui/screens/chat/ChatView.js +22 -1
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/typescript/commonjs/api/client.d.ts +7 -1
- package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/i18n.d.ts +13 -0
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts +11 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +17 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/RatingCard.d.ts +57 -0
- package/lib/typescript/commonjs/ui/components/RatingCard.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +11 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/module/api/client.d.ts +7 -1
- package/lib/typescript/module/api/client.d.ts.map +1 -1
- package/lib/typescript/module/i18n.d.ts +13 -0
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/state/widget-store.d.ts +11 -1
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +17 -0
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/RatingCard.d.ts +57 -0
- package/lib/typescript/module/ui/components/RatingCard.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +11 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/client.ts +13 -0
- package/src/i18n.ts +14 -0
- package/src/locales.ts +13 -0
- package/src/state/widget-store.ts +34 -0
- package/src/types.ts +19 -0
- package/src/ui/WidgetProvider.tsx +13 -0
- package/src/ui/components/RatingCard.tsx +355 -0
- package/src/ui/screens/chat/ChatView.tsx +26 -0
- package/src/ui/screens/chat/ChatView.types.ts +11 -0
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
* `attachments/pickers.ts`).
|
|
9
9
|
*/
|
|
10
10
|
import type { ChatViewProps } from './ChatView.types';
|
|
11
|
-
export declare function ChatView({ messages, loading, hasMoreMessages, agentTyping, color, agentName, agentAvatar, agentOnline, isOnline, teamName, teamAvatars, replyTimeLabel, locale, prefillText, offlineMessage, labels, attachmentSources, onPickAttachments, onSendMessage, onIdentifyVisitor, onLoadMore, onTyping, onBack, onClose, }: ChatViewProps): import("react").JSX.Element;
|
|
11
|
+
export declare function ChatView({ messages, loading, hasMoreMessages, agentTyping, color, agentName, agentAvatar, agentOnline, isOnline, teamName, teamAvatars, replyTimeLabel, locale, prefillText, offlineMessage, labels, attachmentSources, onPickAttachments, onSendMessage, onIdentifyVisitor, onRateConversation, defaultLocale, onLoadMore, onTyping, onBack, onClose, }: ChatViewProps): import("react").JSX.Element;
|
|
12
12
|
//# sourceMappingURL=ChatView.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAsDH,OAAO,KAAK,EAAc,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAElE,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,EACR,OAAO,EACP,eAAe,EACf,WAAW,EACX,KAAK,EACL,SAAS,EACT,WAAW,EACX,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,WAAgB,EAChB,cAAc,EACd,MAAM,EACN,WAAW,EACX,cAAc,EACd,MAAM,EACN,iBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,UAAU,EACV,QAAQ,EACR,MAAM,EACN,OAAO,GACR,EAAE,aAAa,+BAgiBf"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { TeamAvatar } from '../../../types';
|
|
2
2
|
/** Props and label bundle for the chat thread. */
|
|
3
3
|
import type { DraftAttachment, PickResult } from '../../../attachments/pickers';
|
|
4
|
-
import type { AgentUiAction, AttachmentSource, Message, OutgoingAttachment } from '../../../types';
|
|
4
|
+
import type { AgentUiAction, AttachmentSource, ConversationRatingResult, Message, OutgoingAttachment } from '../../../types';
|
|
5
5
|
import type { AgentUiBlockLabels } from '../../components/AgentUiBlocks';
|
|
6
6
|
import type { ImageViewerLabels } from '../../components/ImageViewer';
|
|
7
|
+
import type { RatingCardLabels } from '../../components/RatingCard';
|
|
7
8
|
export interface ChatLabels {
|
|
8
9
|
back: string;
|
|
9
10
|
loading: string;
|
|
@@ -27,6 +28,7 @@ export interface ChatLabels {
|
|
|
27
28
|
procedureSend: string;
|
|
28
29
|
procedureSent: string;
|
|
29
30
|
blocks: AgentUiBlockLabels;
|
|
31
|
+
rating: RatingCardLabels;
|
|
30
32
|
attach: AttachLabels;
|
|
31
33
|
viewer: ImageViewerLabels;
|
|
32
34
|
}
|
|
@@ -79,6 +81,14 @@ export interface ChatViewProps {
|
|
|
79
81
|
name?: string;
|
|
80
82
|
phone?: string;
|
|
81
83
|
}) => Promise<void>;
|
|
84
|
+
/** Answers a rating card; rejects with the server error (409 locked, 410 expired). */
|
|
85
|
+
onRateConversation?: (messageId: string, ratingId: string, data: {
|
|
86
|
+
rating: number;
|
|
87
|
+
reason?: string;
|
|
88
|
+
comment?: string;
|
|
89
|
+
}) => Promise<ConversationRatingResult>;
|
|
90
|
+
/** The widget's default language — rating reasons fall back to it. */
|
|
91
|
+
defaultLocale?: string;
|
|
82
92
|
onLoadMore: () => void;
|
|
83
93
|
onTyping: (isTyping: boolean) => void;
|
|
84
94
|
onBack: () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatView.types.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,kDAAkD;AAElD,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,OAAO,EACP,kBAAkB,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"ChatView.types.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/api/client.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { SecureStorage, TOKEN_KEY } from '../storage';
|
|
|
12
12
|
import type {
|
|
13
13
|
AgentUiAction,
|
|
14
14
|
Conversation,
|
|
15
|
+
ConversationRatingResult,
|
|
15
16
|
InitResponse,
|
|
16
17
|
Message,
|
|
17
18
|
OutboundMessage,
|
|
@@ -285,6 +286,18 @@ export class WidgetApiClient {
|
|
|
285
286
|
);
|
|
286
287
|
}
|
|
287
288
|
|
|
289
|
+
/** Answers (or changes) the rating card posted after a teammate closed the chat. */
|
|
290
|
+
rateConversation(
|
|
291
|
+
conversationId: string,
|
|
292
|
+
ratingId: string,
|
|
293
|
+
data: { rating: number; reason?: string | null; comment?: string | null }
|
|
294
|
+
): Promise<ConversationRatingResult> {
|
|
295
|
+
return this.request(
|
|
296
|
+
`/api/widget/conversations/${encodeURIComponent(conversationId)}/ratings/${encodeURIComponent(ratingId)}`,
|
|
297
|
+
{ method: 'POST', body: data }
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
288
301
|
identify(data: {
|
|
289
302
|
email?: string;
|
|
290
303
|
name?: string;
|
package/src/i18n.ts
CHANGED
|
@@ -118,6 +118,20 @@ export const DEFAULT_LABELS = {
|
|
|
118
118
|
messageSeen: 'Seen',
|
|
119
119
|
waitingForTeammate: 'Waiting for a teammate',
|
|
120
120
|
joinedConversation: 'joined the conversation',
|
|
121
|
+
// Conversation rating card, posted after a teammate closes the chat.
|
|
122
|
+
ratingQuestion: 'How would you rate this conversation?',
|
|
123
|
+
ratingReasonPrompt: 'What went wrong?',
|
|
124
|
+
ratingCommentPlaceholder: 'Tell us more (optional)',
|
|
125
|
+
ratingSubmit: 'Submit',
|
|
126
|
+
ratingThanks: 'Thanks for your feedback!',
|
|
127
|
+
ratingLocked: 'This rating can no longer be changed',
|
|
128
|
+
ratingExpired: 'This conversation can no longer be rated',
|
|
129
|
+
ratingFailed: "Couldn't save your rating. Please try again.",
|
|
130
|
+
rating1: 'Terrible',
|
|
131
|
+
rating2: 'Bad',
|
|
132
|
+
rating3: 'OK',
|
|
133
|
+
rating4: 'Great',
|
|
134
|
+
rating5: 'Amazing',
|
|
121
135
|
// Forms.
|
|
122
136
|
procedureSend: 'Send details',
|
|
123
137
|
procedureSent: 'Sent',
|
package/src/locales.ts
CHANGED
|
@@ -74,6 +74,19 @@ const ar: LabelMap = {
|
|
|
74
74
|
messageSeen: 'تمت المشاهدة',
|
|
75
75
|
waitingForTeammate: 'بانتظار أحد أعضاء الفريق',
|
|
76
76
|
joinedConversation: 'انضم إلى المحادثة',
|
|
77
|
+
ratingQuestion: 'كيف تقيّم هذه المحادثة؟',
|
|
78
|
+
ratingReasonPrompt: 'ما الذي لم يكن جيداً؟',
|
|
79
|
+
ratingCommentPlaceholder: 'أخبرنا المزيد (اختياري)',
|
|
80
|
+
ratingSubmit: 'إرسال',
|
|
81
|
+
ratingThanks: 'شكراً على ملاحظاتك!',
|
|
82
|
+
ratingLocked: 'لم يعد بالإمكان تغيير هذا التقييم',
|
|
83
|
+
ratingExpired: 'لم يعد بالإمكان تقييم هذه المحادثة',
|
|
84
|
+
ratingFailed: 'تعذّر حفظ تقييمك. حاول مرة أخرى.',
|
|
85
|
+
rating1: 'سيئ جداً',
|
|
86
|
+
rating2: 'سيئ',
|
|
87
|
+
rating3: 'مقبول',
|
|
88
|
+
rating4: 'جيد',
|
|
89
|
+
rating5: 'ممتاز',
|
|
77
90
|
procedureSend: 'إرسال التفاصيل',
|
|
78
91
|
procedureSent: 'تم الإرسال',
|
|
79
92
|
fieldRequired: '{field} مطلوب',
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
AgentUiAction,
|
|
23
23
|
BootOptions,
|
|
24
24
|
Conversation,
|
|
25
|
+
ConversationRatingResult,
|
|
25
26
|
Message,
|
|
26
27
|
OutgoingAttachment,
|
|
27
28
|
RepzoWidgetErrorCode,
|
|
@@ -1850,6 +1851,39 @@ export class WidgetStore {
|
|
|
1850
1851
|
await this.client.identify(data);
|
|
1851
1852
|
}
|
|
1852
1853
|
|
|
1854
|
+
/**
|
|
1855
|
+
* Answers a rating card in the thread on screen and mirrors the answer onto
|
|
1856
|
+
* its message, so the card keeps it across a re-render. Rejects with the
|
|
1857
|
+
* server's error (409 locked, 410 expired) for the card to show.
|
|
1858
|
+
*/
|
|
1859
|
+
async rateConversation(
|
|
1860
|
+
messageId: string,
|
|
1861
|
+
ratingId: string,
|
|
1862
|
+
data: { rating: number; reason?: string | null; comment?: string | null }
|
|
1863
|
+
): Promise<ConversationRatingResult> {
|
|
1864
|
+
const conversationId = this.state.activeConversationId;
|
|
1865
|
+
if (!conversationId) throw new RepzoWidgetError('No conversation is open', 0, 'http');
|
|
1866
|
+
const result = await this.client.rateConversation(conversationId, ratingId, data);
|
|
1867
|
+
if (this.state.activeConversationId === conversationId) {
|
|
1868
|
+
this.setState((current) => ({
|
|
1869
|
+
messages: current.messages.map((message) =>
|
|
1870
|
+
message.id === messageId
|
|
1871
|
+
? {
|
|
1872
|
+
...message,
|
|
1873
|
+
metadata: {
|
|
1874
|
+
...message.metadata,
|
|
1875
|
+
rating: result.rating,
|
|
1876
|
+
ratingReason: result.reason,
|
|
1877
|
+
ratingComment: result.comment,
|
|
1878
|
+
},
|
|
1879
|
+
}
|
|
1880
|
+
: message
|
|
1881
|
+
),
|
|
1882
|
+
}));
|
|
1883
|
+
}
|
|
1884
|
+
return result;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1853
1887
|
/** Fire-and-forget visitor typing ping, throttled well under the 30/min cap. */
|
|
1854
1888
|
notifyTyping(isTyping: boolean): void {
|
|
1855
1889
|
const conversationId = this.state.activeConversationId;
|
package/src/types.ts
CHANGED
|
@@ -480,6 +480,25 @@ export interface RepzoWidgetStorageAdapter {
|
|
|
480
480
|
* - `identity_verification_failed` — `userHash` missing/wrong while the
|
|
481
481
|
* channel enforces identity verification.
|
|
482
482
|
*/
|
|
483
|
+
/** A bad-rating reason on the rating card, labelled per language. */
|
|
484
|
+
export interface RatingReason {
|
|
485
|
+
id: string;
|
|
486
|
+
/** Locale code → label. */
|
|
487
|
+
labels: Record<string, string>;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** The visitor's answer to a conversation rating card. */
|
|
491
|
+
export interface ConversationRatingResult {
|
|
492
|
+
id: string;
|
|
493
|
+
/** 1 (Terrible) … 5 (Amazing). */
|
|
494
|
+
rating: number;
|
|
495
|
+
/** Id of the reason picked after a bad rating (1–2), if any. */
|
|
496
|
+
reason: string | null;
|
|
497
|
+
comment: string | null;
|
|
498
|
+
/** A comment was left: the rating can't change any more. */
|
|
499
|
+
locked: boolean;
|
|
500
|
+
}
|
|
501
|
+
|
|
483
502
|
export type RepzoWidgetErrorCode =
|
|
484
503
|
| 'network'
|
|
485
504
|
| 'http'
|
|
@@ -295,6 +295,7 @@ export function RepzoWidgetProvider({
|
|
|
295
295
|
teamAvatars={teamAvatars}
|
|
296
296
|
replyTimeLabel={theme.replyTimeLabel}
|
|
297
297
|
locale={state.locale}
|
|
298
|
+
defaultLocale={state.config?.defaultLocale}
|
|
298
299
|
prefillText={state.prefillText}
|
|
299
300
|
offlineMessage={theme.offlineMessage}
|
|
300
301
|
labels={{
|
|
@@ -320,6 +321,17 @@ export function RepzoWidgetProvider({
|
|
|
320
321
|
procedureSend: labels.procedureSend,
|
|
321
322
|
procedureSent: labels.procedureSent,
|
|
322
323
|
blocks: blockLabels,
|
|
324
|
+
rating: {
|
|
325
|
+
question: labels.ratingQuestion,
|
|
326
|
+
reasonPrompt: labels.ratingReasonPrompt,
|
|
327
|
+
commentPlaceholder: labels.ratingCommentPlaceholder,
|
|
328
|
+
submit: labels.ratingSubmit,
|
|
329
|
+
thanks: labels.ratingThanks,
|
|
330
|
+
locked: labels.ratingLocked,
|
|
331
|
+
expired: labels.ratingExpired,
|
|
332
|
+
failed: labels.ratingFailed,
|
|
333
|
+
scale: [labels.rating1, labels.rating2, labels.rating3, labels.rating4, labels.rating5],
|
|
334
|
+
},
|
|
323
335
|
attach: attachLabels,
|
|
324
336
|
viewer: {
|
|
325
337
|
close: labels.close,
|
|
@@ -331,6 +343,7 @@ export function RepzoWidgetProvider({
|
|
|
331
343
|
onPickAttachments={pickAttachments}
|
|
332
344
|
onSendMessage={handleSendMessage}
|
|
333
345
|
onIdentifyVisitor={(data) => store.identifyVisitor(data)}
|
|
346
|
+
onRateConversation={(messageId, ratingId, data) => store.rateConversation(messageId, ratingId, data)}
|
|
334
347
|
onLoadMore={() => store.loadMoreMessages()}
|
|
335
348
|
onTyping={(isTyping) => store.notifyTyping(isTyping)}
|
|
336
349
|
onBack={handleBack}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation rating card (CSAT), posted two minutes after a teammate closes
|
|
3
|
+
* the chat. Port of `client/src/widget/components/RatingCard.tsx`.
|
|
4
|
+
*
|
|
5
|
+
* Tapping a face saves it right away; after 😠/🙁 the visitor can also tap a
|
|
6
|
+
* reason, labelled in their language. A comment locks the rating. The server enforces the change window and answers
|
|
7
|
+
* 409 (locked) or 410 (expired), which the card shows instead of the faces'
|
|
8
|
+
* actions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
|
13
|
+
|
|
14
|
+
import type { ConversationRatingResult, RatingReason } from '../../types';
|
|
15
|
+
import { contrastingTextColor, fontSize, palette, radius, spacing, withAlpha } from '../styles/theme';
|
|
16
|
+
|
|
17
|
+
/** 1..5, worst to best — Intercom's five faces. */
|
|
18
|
+
const RATING_SCALE = [
|
|
19
|
+
{ value: 1, emoji: '😠' },
|
|
20
|
+
{ value: 2, emoji: '🙁' },
|
|
21
|
+
{ value: 3, emoji: '😐' },
|
|
22
|
+
{ value: 4, emoji: '🙂' },
|
|
23
|
+
{ value: 5, emoji: '🤩' },
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export interface RatingCardLabels {
|
|
27
|
+
question: string;
|
|
28
|
+
reasonPrompt: string;
|
|
29
|
+
commentPlaceholder: string;
|
|
30
|
+
submit: string;
|
|
31
|
+
thanks: string;
|
|
32
|
+
locked: string;
|
|
33
|
+
expired: string;
|
|
34
|
+
failed: string;
|
|
35
|
+
/** Accessibility names for the faces, worst to best. */
|
|
36
|
+
scale: [string, string, string, string, string];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 1–2 asks for a reason; 3 is neutral, 4–5 good. */
|
|
40
|
+
export const isBadRating = (rating: number | null) => rating != null && rating <= 2;
|
|
41
|
+
|
|
42
|
+
const isRatingReason = (value: unknown): value is RatingReason =>
|
|
43
|
+
!!value &&
|
|
44
|
+
typeof value === 'object' &&
|
|
45
|
+
typeof (value as RatingReason).id === 'string' &&
|
|
46
|
+
!!(value as RatingReason).labels &&
|
|
47
|
+
typeof (value as RatingReason).labels === 'object';
|
|
48
|
+
|
|
49
|
+
/** The rating card carried by a system message's metadata, or null. */
|
|
50
|
+
export function getRatingRequest(metadata: Record<string, unknown> | null | undefined) {
|
|
51
|
+
if (!metadata || metadata.event !== 'rating_request' || typeof metadata.ratingId !== 'string') return null;
|
|
52
|
+
const rating = typeof metadata.rating === 'number' && metadata.rating >= 1 && metadata.rating <= 5 ? metadata.rating : null;
|
|
53
|
+
const comment = typeof metadata.ratingComment === 'string' && metadata.ratingComment ? metadata.ratingComment : null;
|
|
54
|
+
const reason = typeof metadata.ratingReason === 'string' && metadata.ratingReason ? metadata.ratingReason : null;
|
|
55
|
+
const reasons = Array.isArray(metadata.ratingReasons) ? metadata.ratingReasons.filter(isRatingReason) : [];
|
|
56
|
+
return { ratingId: metadata.ratingId, rating, comment, reason, reasons };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A reason's label in the first of `locales` it has (full code, then its base
|
|
61
|
+
* language: "ar-SA" → "ar"), else English, else whatever it has.
|
|
62
|
+
*/
|
|
63
|
+
export function reasonLabel(reason: RatingReason, locales: Array<string | null | undefined>): string {
|
|
64
|
+
for (const locale of [...locales, 'en']) {
|
|
65
|
+
if (!locale) continue;
|
|
66
|
+
const label = reason.labels[locale] || reason.labels[locale.split('-')[0]!];
|
|
67
|
+
if (label) return label;
|
|
68
|
+
}
|
|
69
|
+
return Object.values(reason.labels).find(Boolean) ?? reason.id;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface RatingCardProps {
|
|
73
|
+
ratingId: string;
|
|
74
|
+
initialRating: number | null;
|
|
75
|
+
initialComment: string | null;
|
|
76
|
+
initialReason: string | null;
|
|
77
|
+
/** Offered after a bad rating (1–2). */
|
|
78
|
+
reasons: RatingReason[];
|
|
79
|
+
/** The visitor's language first, then the widget's default. */
|
|
80
|
+
locales: Array<string | null | undefined>;
|
|
81
|
+
color: string;
|
|
82
|
+
labels: RatingCardLabels;
|
|
83
|
+
onRate?: (
|
|
84
|
+
ratingId: string,
|
|
85
|
+
data: { rating: number; reason?: string; comment?: string }
|
|
86
|
+
) => Promise<ConversationRatingResult>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function RatingCard({
|
|
90
|
+
ratingId,
|
|
91
|
+
initialRating,
|
|
92
|
+
initialComment,
|
|
93
|
+
initialReason,
|
|
94
|
+
reasons,
|
|
95
|
+
locales,
|
|
96
|
+
color,
|
|
97
|
+
labels,
|
|
98
|
+
onRate,
|
|
99
|
+
}: RatingCardProps) {
|
|
100
|
+
const [rating, setRating] = useState<number | null>(initialRating);
|
|
101
|
+
const [reason, setReason] = useState<string | null>(initialReason);
|
|
102
|
+
const [comment, setComment] = useState<string | null>(initialComment);
|
|
103
|
+
const [draft, setDraft] = useState('');
|
|
104
|
+
const [pending, setPending] = useState(false);
|
|
105
|
+
const [closedReason, setClosedReason] = useState<'locked' | 'expired' | null>(initialComment ? 'locked' : null);
|
|
106
|
+
const [failed, setFailed] = useState(false);
|
|
107
|
+
const interactive = !!onRate && !closedReason && !pending;
|
|
108
|
+
const canSubmit = !pending && draft.trim().length > 0;
|
|
109
|
+
|
|
110
|
+
const submit = async (next: { rating: number; reason?: string; comment?: string }) => {
|
|
111
|
+
if (!onRate || pending) return;
|
|
112
|
+
setPending(true);
|
|
113
|
+
setFailed(false);
|
|
114
|
+
try {
|
|
115
|
+
const result = await onRate(ratingId, next);
|
|
116
|
+
setRating(result.rating);
|
|
117
|
+
setReason(result.reason ?? null);
|
|
118
|
+
setComment(result.comment);
|
|
119
|
+
if (result.locked) setClosedReason('locked');
|
|
120
|
+
setDraft('');
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const status = (error as { status?: number }).status;
|
|
123
|
+
if (status === 409) setClosedReason('locked');
|
|
124
|
+
else if (status === 410) setClosedReason('expired');
|
|
125
|
+
else setFailed(true);
|
|
126
|
+
} finally {
|
|
127
|
+
setPending(false);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const status =
|
|
132
|
+
closedReason === 'expired'
|
|
133
|
+
? labels.expired
|
|
134
|
+
: closedReason === 'locked' && !comment
|
|
135
|
+
? labels.locked
|
|
136
|
+
: failed
|
|
137
|
+
? labels.failed
|
|
138
|
+
: rating != null
|
|
139
|
+
? labels.thanks
|
|
140
|
+
: null;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<View style={styles.row}>
|
|
144
|
+
<View style={styles.card} testID="repzo-rating-card">
|
|
145
|
+
<Text style={styles.question}>{labels.question}</Text>
|
|
146
|
+
<View style={styles.faces} accessibilityRole="radiogroup">
|
|
147
|
+
{RATING_SCALE.map((entry, index) => {
|
|
148
|
+
const selected = rating === entry.value;
|
|
149
|
+
return (
|
|
150
|
+
<Pressable
|
|
151
|
+
key={entry.value}
|
|
152
|
+
accessibilityRole="radio"
|
|
153
|
+
accessibilityLabel={labels.scale[index]}
|
|
154
|
+
accessibilityState={{ checked: selected, disabled: !interactive }}
|
|
155
|
+
disabled={!interactive}
|
|
156
|
+
hitSlop={4}
|
|
157
|
+
onPress={() => {
|
|
158
|
+
if (!selected) void submit({ rating: entry.value });
|
|
159
|
+
}}
|
|
160
|
+
style={({ pressed }) => [
|
|
161
|
+
styles.face,
|
|
162
|
+
selected && { borderColor: color, backgroundColor: withAlpha(color, 0.08) },
|
|
163
|
+
rating != null && !selected && styles.faceDimmed,
|
|
164
|
+
pressed && styles.facePressed,
|
|
165
|
+
]}>
|
|
166
|
+
<Text style={styles.emoji}>{entry.emoji}</Text>
|
|
167
|
+
</Pressable>
|
|
168
|
+
);
|
|
169
|
+
})}
|
|
170
|
+
</View>
|
|
171
|
+
|
|
172
|
+
{status ? <Text style={[styles.status, failed && styles.statusFailed]}>{status}</Text> : null}
|
|
173
|
+
|
|
174
|
+
{isBadRating(rating) && reasons.length > 0 && (interactive || reason) ? (
|
|
175
|
+
<View style={styles.reasons}>
|
|
176
|
+
{interactive ? <Text style={styles.reasonPrompt}>{labels.reasonPrompt}</Text> : null}
|
|
177
|
+
<View style={styles.chips} accessibilityRole="radiogroup" accessibilityLabel={labels.reasonPrompt}>
|
|
178
|
+
{reasons
|
|
179
|
+
.filter((item) => interactive || item.id === reason)
|
|
180
|
+
.map((item) => {
|
|
181
|
+
const selected = item.id === reason;
|
|
182
|
+
return (
|
|
183
|
+
<Pressable
|
|
184
|
+
key={item.id}
|
|
185
|
+
accessibilityRole="radio"
|
|
186
|
+
accessibilityState={{ checked: selected, disabled: !interactive }}
|
|
187
|
+
disabled={!interactive}
|
|
188
|
+
onPress={() => {
|
|
189
|
+
if (!selected && rating != null) void submit({ rating, reason: item.id });
|
|
190
|
+
}}
|
|
191
|
+
style={({ pressed }) => [
|
|
192
|
+
styles.chip,
|
|
193
|
+
selected && { backgroundColor: color, borderColor: color },
|
|
194
|
+
pressed && styles.facePressed,
|
|
195
|
+
]}>
|
|
196
|
+
<Text style={[styles.chipText, selected && { color: contrastingTextColor(color) }]}>
|
|
197
|
+
{reasonLabel(item, locales)}
|
|
198
|
+
</Text>
|
|
199
|
+
</Pressable>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
</View>
|
|
203
|
+
</View>
|
|
204
|
+
) : null}
|
|
205
|
+
|
|
206
|
+
{comment ? (
|
|
207
|
+
<Text style={styles.comment}>{comment}</Text>
|
|
208
|
+
) : rating != null && !closedReason ? (
|
|
209
|
+
<View style={styles.form}>
|
|
210
|
+
<TextInput
|
|
211
|
+
value={draft}
|
|
212
|
+
onChangeText={setDraft}
|
|
213
|
+
editable={!pending}
|
|
214
|
+
multiline
|
|
215
|
+
maxLength={2000}
|
|
216
|
+
placeholder={labels.commentPlaceholder}
|
|
217
|
+
placeholderTextColor={palette.textSubtle}
|
|
218
|
+
style={styles.input}
|
|
219
|
+
/>
|
|
220
|
+
<Pressable
|
|
221
|
+
accessibilityRole="button"
|
|
222
|
+
disabled={!canSubmit}
|
|
223
|
+
onPress={() => {
|
|
224
|
+
const text = draft.trim();
|
|
225
|
+
if (text && rating != null) void submit({ rating, comment: text });
|
|
226
|
+
}}
|
|
227
|
+
style={({ pressed }) => [
|
|
228
|
+
styles.submit,
|
|
229
|
+
{ backgroundColor: color },
|
|
230
|
+
!canSubmit && styles.submitDisabled,
|
|
231
|
+
pressed && canSubmit && styles.facePressed,
|
|
232
|
+
]}>
|
|
233
|
+
<Text style={[styles.submitText, { color: contrastingTextColor(color) }]}>{labels.submit}</Text>
|
|
234
|
+
</Pressable>
|
|
235
|
+
</View>
|
|
236
|
+
) : null}
|
|
237
|
+
</View>
|
|
238
|
+
</View>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const styles = StyleSheet.create({
|
|
243
|
+
row: {
|
|
244
|
+
alignItems: 'center',
|
|
245
|
+
paddingVertical: spacing.md,
|
|
246
|
+
},
|
|
247
|
+
card: {
|
|
248
|
+
width: '100%',
|
|
249
|
+
maxWidth: 340,
|
|
250
|
+
gap: spacing.md,
|
|
251
|
+
padding: spacing.lg,
|
|
252
|
+
borderRadius: radius.xl,
|
|
253
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
254
|
+
borderColor: palette.border,
|
|
255
|
+
backgroundColor: palette.white,
|
|
256
|
+
},
|
|
257
|
+
question: {
|
|
258
|
+
textAlign: 'center',
|
|
259
|
+
color: palette.ink,
|
|
260
|
+
fontSize: fontSize.body,
|
|
261
|
+
fontWeight: '600',
|
|
262
|
+
},
|
|
263
|
+
faces: {
|
|
264
|
+
flexDirection: 'row',
|
|
265
|
+
justifyContent: 'center',
|
|
266
|
+
gap: spacing.sm,
|
|
267
|
+
},
|
|
268
|
+
face: {
|
|
269
|
+
width: 48,
|
|
270
|
+
height: 48,
|
|
271
|
+
borderRadius: 24,
|
|
272
|
+
borderWidth: 2,
|
|
273
|
+
borderColor: 'transparent',
|
|
274
|
+
alignItems: 'center',
|
|
275
|
+
justifyContent: 'center',
|
|
276
|
+
},
|
|
277
|
+
faceDimmed: {
|
|
278
|
+
opacity: 0.45,
|
|
279
|
+
},
|
|
280
|
+
facePressed: {
|
|
281
|
+
opacity: 0.7,
|
|
282
|
+
},
|
|
283
|
+
emoji: {
|
|
284
|
+
fontSize: 28,
|
|
285
|
+
},
|
|
286
|
+
status: {
|
|
287
|
+
textAlign: 'center',
|
|
288
|
+
color: palette.textMuted,
|
|
289
|
+
fontSize: fontSize.meta,
|
|
290
|
+
},
|
|
291
|
+
statusFailed: {
|
|
292
|
+
color: palette.danger,
|
|
293
|
+
},
|
|
294
|
+
reasons: {
|
|
295
|
+
gap: spacing.sm,
|
|
296
|
+
},
|
|
297
|
+
reasonPrompt: {
|
|
298
|
+
color: palette.ink,
|
|
299
|
+
fontSize: fontSize.meta,
|
|
300
|
+
fontWeight: '600',
|
|
301
|
+
},
|
|
302
|
+
chips: {
|
|
303
|
+
flexDirection: 'row',
|
|
304
|
+
flexWrap: 'wrap',
|
|
305
|
+
gap: spacing.sm,
|
|
306
|
+
},
|
|
307
|
+
chip: {
|
|
308
|
+
paddingHorizontal: spacing.md,
|
|
309
|
+
paddingVertical: spacing.sm,
|
|
310
|
+
borderRadius: radius.pill,
|
|
311
|
+
borderWidth: 1,
|
|
312
|
+
borderColor: palette.border,
|
|
313
|
+
backgroundColor: palette.white,
|
|
314
|
+
},
|
|
315
|
+
chipText: {
|
|
316
|
+
color: palette.ink,
|
|
317
|
+
fontSize: fontSize.meta,
|
|
318
|
+
},
|
|
319
|
+
comment: {
|
|
320
|
+
paddingHorizontal: spacing.md,
|
|
321
|
+
paddingVertical: spacing.sm,
|
|
322
|
+
borderRadius: radius.md,
|
|
323
|
+
backgroundColor: palette.surfaceAlt,
|
|
324
|
+
color: palette.ink,
|
|
325
|
+
fontSize: fontSize.meta,
|
|
326
|
+
writingDirection: 'auto',
|
|
327
|
+
},
|
|
328
|
+
form: {
|
|
329
|
+
gap: spacing.sm,
|
|
330
|
+
},
|
|
331
|
+
input: {
|
|
332
|
+
minHeight: 64,
|
|
333
|
+
paddingHorizontal: spacing.md,
|
|
334
|
+
paddingVertical: spacing.sm,
|
|
335
|
+
borderRadius: radius.md,
|
|
336
|
+
borderWidth: 1,
|
|
337
|
+
borderColor: palette.border,
|
|
338
|
+
color: palette.ink,
|
|
339
|
+
fontSize: fontSize.input,
|
|
340
|
+
textAlignVertical: 'top',
|
|
341
|
+
},
|
|
342
|
+
submit: {
|
|
343
|
+
alignSelf: 'flex-end',
|
|
344
|
+
paddingHorizontal: spacing.lg,
|
|
345
|
+
paddingVertical: spacing.sm,
|
|
346
|
+
borderRadius: radius.pill,
|
|
347
|
+
},
|
|
348
|
+
submitDisabled: {
|
|
349
|
+
opacity: 0.5,
|
|
350
|
+
},
|
|
351
|
+
submitText: {
|
|
352
|
+
fontSize: fontSize.meta,
|
|
353
|
+
fontWeight: '600',
|
|
354
|
+
},
|
|
355
|
+
});
|
|
@@ -45,6 +45,7 @@ import { AttachmentChips } from '../../components/AttachmentChips';
|
|
|
45
45
|
import { AttachmentMenu, showNativeAttachmentMenu } from '../../components/AttachmentMenu';
|
|
46
46
|
import { Avatar } from '../../components/Avatar';
|
|
47
47
|
import { ImageViewer } from '../../components/ImageViewer';
|
|
48
|
+
import { getRatingRequest, RatingCard } from '../../components/RatingCard';
|
|
48
49
|
import { WidgetErrorBoundary } from '../../components/ErrorBoundary';
|
|
49
50
|
import { GradientBackground } from '../../components/Gradient';
|
|
50
51
|
import { ArrowUpIcon, ChatOutlineIcon, ChevronIcon, ClockIcon, PaperclipIcon } from '../../components/icons';
|
|
@@ -82,6 +83,8 @@ export function ChatView({
|
|
|
82
83
|
onPickAttachments,
|
|
83
84
|
onSendMessage,
|
|
84
85
|
onIdentifyVisitor,
|
|
86
|
+
onRateConversation,
|
|
87
|
+
defaultLocale,
|
|
85
88
|
onLoadMore,
|
|
86
89
|
onTyping,
|
|
87
90
|
onBack,
|
|
@@ -489,6 +492,8 @@ export function ChatView({
|
|
|
489
492
|
color={color}
|
|
490
493
|
labels={labels}
|
|
491
494
|
onOpenImage={setViewerImage}
|
|
495
|
+
onRateConversation={onRateConversation}
|
|
496
|
+
locales={[locale, defaultLocale]}
|
|
492
497
|
senderLabel={
|
|
493
498
|
isAgentMessage && (startsRun || startsDay)
|
|
494
499
|
? message.senderName || agentName || labels.team
|
|
@@ -640,6 +645,8 @@ function MessageBubble({
|
|
|
640
645
|
onSubmitProcedureForm,
|
|
641
646
|
onAgentUiAction,
|
|
642
647
|
onOpenImage,
|
|
648
|
+
onRateConversation,
|
|
649
|
+
locales,
|
|
643
650
|
}: {
|
|
644
651
|
message: Message;
|
|
645
652
|
color: string;
|
|
@@ -650,6 +657,8 @@ function MessageBubble({
|
|
|
650
657
|
onSubmitProcedureForm?: (body: string) => void;
|
|
651
658
|
onAgentUiAction?: (action: AgentUiAction) => void | Promise<void>;
|
|
652
659
|
onOpenImage: (image: { url: string; name: string }) => void;
|
|
660
|
+
onRateConversation?: ChatViewProps['onRateConversation'];
|
|
661
|
+
locales: Array<string | null | undefined>;
|
|
653
662
|
}) {
|
|
654
663
|
const joinedName = message.metadata?.event === 'agent_joined' ? message.metadata?.agentName : null;
|
|
655
664
|
if (message.messageType === 'system' && typeof joinedName === 'string' && joinedName) {
|
|
@@ -668,6 +677,23 @@ function MessageBubble({
|
|
|
668
677
|
);
|
|
669
678
|
}
|
|
670
679
|
|
|
680
|
+
const ratingRequest = message.messageType === 'system' ? getRatingRequest(message.metadata) : null;
|
|
681
|
+
if (ratingRequest) {
|
|
682
|
+
return (
|
|
683
|
+
<RatingCard
|
|
684
|
+
ratingId={ratingRequest.ratingId}
|
|
685
|
+
initialRating={ratingRequest.rating}
|
|
686
|
+
initialComment={ratingRequest.comment}
|
|
687
|
+
initialReason={ratingRequest.reason}
|
|
688
|
+
reasons={ratingRequest.reasons}
|
|
689
|
+
locales={locales}
|
|
690
|
+
color={color}
|
|
691
|
+
labels={labels.rating}
|
|
692
|
+
onRate={onRateConversation ? (ratingId, data) => onRateConversation(message.id, ratingId, data) : undefined}
|
|
693
|
+
/>
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
671
697
|
if (message.messageType === 'system') {
|
|
672
698
|
const text = message.bodyPlain || stripHtml(message.body || '') || labels.conversationUpdated;
|
|
673
699
|
return (
|