@giovchat/react-native 0.0.0-stage → 0.3.0

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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +390 -2
  3. package/dist/GiovChat.d.ts +135 -0
  4. package/dist/GiovChat.d.ts.map +1 -0
  5. package/dist/GiovChat.js +294 -0
  6. package/dist/GiovChat.js.map +1 -0
  7. package/dist/attachments.d.ts +121 -0
  8. package/dist/attachments.d.ts.map +1 -0
  9. package/dist/attachments.js +185 -0
  10. package/dist/attachments.js.map +1 -0
  11. package/dist/brand-assets.d.ts +6 -0
  12. package/dist/brand-assets.d.ts.map +1 -0
  13. package/dist/brand-assets.js +9 -0
  14. package/dist/brand-assets.js.map +1 -0
  15. package/dist/brandingStyle.d.ts +38 -0
  16. package/dist/brandingStyle.d.ts.map +1 -0
  17. package/dist/brandingStyle.js +71 -0
  18. package/dist/brandingStyle.js.map +1 -0
  19. package/dist/client.d.ts +226 -0
  20. package/dist/client.d.ts.map +1 -0
  21. package/dist/client.js +891 -0
  22. package/dist/client.js.map +1 -0
  23. package/dist/color.d.ts +25 -0
  24. package/dist/color.d.ts.map +1 -0
  25. package/dist/color.js +78 -0
  26. package/dist/color.js.map +1 -0
  27. package/dist/components/Attachments.d.ts +13 -0
  28. package/dist/components/Attachments.d.ts.map +1 -0
  29. package/dist/components/Attachments.js +42 -0
  30. package/dist/components/Attachments.js.map +1 -0
  31. package/dist/components/Branding.d.ts +29 -0
  32. package/dist/components/Branding.d.ts.map +1 -0
  33. package/dist/components/Branding.js +71 -0
  34. package/dist/components/Branding.js.map +1 -0
  35. package/dist/components/ChatMarkdown.d.ts +15 -0
  36. package/dist/components/ChatMarkdown.d.ts.map +1 -0
  37. package/dist/components/ChatMarkdown.js +72 -0
  38. package/dist/components/ChatMarkdown.js.map +1 -0
  39. package/dist/components/GiovBotMark.d.ts +6 -0
  40. package/dist/components/GiovBotMark.d.ts.map +1 -0
  41. package/dist/components/GiovBotMark.js +11 -0
  42. package/dist/components/GiovBotMark.js.map +1 -0
  43. package/dist/components/GreetingBubble.d.ts +7 -0
  44. package/dist/components/GreetingBubble.d.ts.map +1 -0
  45. package/dist/components/GreetingBubble.js +12 -0
  46. package/dist/components/GreetingBubble.js.map +1 -0
  47. package/dist/components/Header.d.ts +18 -0
  48. package/dist/components/Header.d.ts.map +1 -0
  49. package/dist/components/Header.js +67 -0
  50. package/dist/components/Header.js.map +1 -0
  51. package/dist/components/MessageBubble.d.ts +23 -0
  52. package/dist/components/MessageBubble.d.ts.map +1 -0
  53. package/dist/components/MessageBubble.js +51 -0
  54. package/dist/components/MessageBubble.js.map +1 -0
  55. package/dist/components/PreChatForm.d.ts +11 -0
  56. package/dist/components/PreChatForm.d.ts.map +1 -0
  57. package/dist/components/PreChatForm.js +72 -0
  58. package/dist/components/PreChatForm.js.map +1 -0
  59. package/dist/components/RatingCard.d.ts +23 -0
  60. package/dist/components/RatingCard.d.ts.map +1 -0
  61. package/dist/components/RatingCard.js +107 -0
  62. package/dist/components/RatingCard.js.map +1 -0
  63. package/dist/components/TypingIndicator.d.ts +9 -0
  64. package/dist/components/TypingIndicator.d.ts.map +1 -0
  65. package/dist/components/TypingIndicator.js +18 -0
  66. package/dist/components/TypingIndicator.js.map +1 -0
  67. package/dist/components/WidgetStyles.d.ts +3 -0
  68. package/dist/components/WidgetStyles.d.ts.map +1 -0
  69. package/dist/components/WidgetStyles.js +9 -0
  70. package/dist/components/WidgetStyles.js.map +1 -0
  71. package/dist/components/bubbleStyles.d.ts +47 -0
  72. package/dist/components/bubbleStyles.d.ts.map +1 -0
  73. package/dist/components/bubbleStyles.js +19 -0
  74. package/dist/components/bubbleStyles.js.map +1 -0
  75. package/dist/components/index.d.ts +11 -0
  76. package/dist/components/index.d.ts.map +1 -0
  77. package/dist/components/index.js +14 -0
  78. package/dist/components/index.js.map +1 -0
  79. package/dist/errors.d.ts +20 -0
  80. package/dist/errors.d.ts.map +1 -0
  81. package/dist/errors.js +26 -0
  82. package/dist/errors.js.map +1 -0
  83. package/dist/i18n.d.ts +96 -0
  84. package/dist/i18n.d.ts.map +1 -0
  85. package/dist/i18n.js +392 -0
  86. package/dist/i18n.js.map +1 -0
  87. package/dist/identity.d.ts +41 -0
  88. package/dist/identity.d.ts.map +1 -0
  89. package/dist/identity.js +41 -0
  90. package/dist/identity.js.map +1 -0
  91. package/dist/index.d.ts +30 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +64 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/markdown.d.ts +56 -0
  96. package/dist/markdown.d.ts.map +1 -0
  97. package/dist/markdown.js +219 -0
  98. package/dist/markdown.js.map +1 -0
  99. package/dist/outbox.d.ts +68 -0
  100. package/dist/outbox.d.ts.map +1 -0
  101. package/dist/outbox.js +168 -0
  102. package/dist/outbox.js.map +1 -0
  103. package/dist/protocol.d.ts +9 -0
  104. package/dist/protocol.d.ts.map +1 -0
  105. package/dist/protocol.js +39 -0
  106. package/dist/protocol.js.map +1 -0
  107. package/dist/rating.d.ts +41 -0
  108. package/dist/rating.d.ts.map +1 -0
  109. package/dist/rating.js +165 -0
  110. package/dist/rating.js.map +1 -0
  111. package/dist/ratingStore.d.ts +36 -0
  112. package/dist/ratingStore.d.ts.map +1 -0
  113. package/dist/ratingStore.js +55 -0
  114. package/dist/ratingStore.js.map +1 -0
  115. package/dist/styleOverrides.d.ts +45 -0
  116. package/dist/styleOverrides.d.ts.map +1 -0
  117. package/dist/styleOverrides.js +63 -0
  118. package/dist/styleOverrides.js.map +1 -0
  119. package/dist/types.d.ts +156 -0
  120. package/dist/types.d.ts.map +1 -0
  121. package/dist/types.js +3 -0
  122. package/dist/types.js.map +1 -0
  123. package/dist/unread.d.ts +9 -0
  124. package/dist/unread.d.ts.map +1 -0
  125. package/dist/unread.js +49 -0
  126. package/dist/unread.js.map +1 -0
  127. package/dist/useChatSession.d.ts +95 -0
  128. package/dist/useChatSession.d.ts.map +1 -0
  129. package/dist/useChatSession.js +965 -0
  130. package/dist/useChatSession.js.map +1 -0
  131. package/package.json +60 -4
  132. package/src/GiovChat.tsx +640 -0
  133. package/src/attachments.ts +218 -0
  134. package/src/brand-assets.ts +5 -0
  135. package/src/brandingStyle.ts +87 -0
  136. package/src/client.ts +848 -0
  137. package/src/color.ts +74 -0
  138. package/src/components/Attachments.tsx +75 -0
  139. package/src/components/Branding.tsx +94 -0
  140. package/src/components/ChatMarkdown.tsx +111 -0
  141. package/src/components/GiovBotMark.tsx +19 -0
  142. package/src/components/GreetingBubble.tsx +20 -0
  143. package/src/components/Header.tsx +131 -0
  144. package/src/components/MessageBubble.tsx +137 -0
  145. package/src/components/PreChatForm.tsx +183 -0
  146. package/src/components/RatingCard.tsx +219 -0
  147. package/src/components/TypingIndicator.tsx +34 -0
  148. package/src/components/WidgetStyles.tsx +5 -0
  149. package/src/components/bubbleStyles.ts +16 -0
  150. package/src/components/index.ts +14 -0
  151. package/src/errors.ts +33 -0
  152. package/src/i18n.ts +474 -0
  153. package/src/identity.ts +54 -0
  154. package/src/index.ts +85 -0
  155. package/src/markdown.ts +227 -0
  156. package/src/outbox.ts +175 -0
  157. package/src/protocol.ts +36 -0
  158. package/src/rating.ts +169 -0
  159. package/src/ratingStore.ts +65 -0
  160. package/src/styleOverrides.ts +98 -0
  161. package/src/types.ts +170 -0
  162. package/src/unread.ts +39 -0
  163. package/src/useChatSession.ts +841 -0
@@ -0,0 +1,183 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ ScrollView,
4
+ StyleSheet,
5
+ Text,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ View,
9
+ } from "react-native";
10
+ import type { GiovChatContact, RequestUserInfo } from "../types";
11
+ import { useWidgetStyles } from "./WidgetStyles";
12
+ import { useGiovChatStrings } from "../i18n";
13
+ import { textOn } from "../color";
14
+
15
+ export interface PreChatFormProps {
16
+ fields: RequestUserInfo;
17
+ primaryColor: string;
18
+ dark: boolean;
19
+ initialValues?: GiovChatContact;
20
+ onSubmit: (info: GiovChatContact) => void;
21
+ }
22
+
23
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
24
+
25
+ export function PreChatForm({
26
+ fields,
27
+ primaryColor,
28
+ dark,
29
+ initialValues,
30
+ onSubmit,
31
+ }: PreChatFormProps) {
32
+ const custom = useWidgetStyles();
33
+ const strings = useGiovChatStrings();
34
+ const [firstName, setFirstName] = useState(initialValues?.firstName ?? "");
35
+ const [lastName, setLastName] = useState(initialValues?.lastName ?? "");
36
+ const [email, setEmail] = useState(initialValues?.email ?? "");
37
+ const [phone, setPhone] = useState(initialValues?.phone ?? "");
38
+
39
+ const bg = dark ? "#111" : "#fff";
40
+ const textColor = dark ? "#f0f0f0" : "#111";
41
+ const labelColor = dark ? "#aaa" : "#555";
42
+ const inputBg = dark ? "#1c1c1c" : "#fafafa";
43
+ const inputBorder = dark ? "#333" : "#e5e5e5";
44
+ const placeholderColor = custom.hint.color ?? (dark ? "#555" : "#c0c0c0");
45
+
46
+ const isValid =
47
+ (!fields.firstName || firstName.trim().length > 0) &&
48
+ (!fields.lastName || lastName.trim().length > 0) &&
49
+ (!fields.email || EMAIL_RE.test(email.trim())) &&
50
+ (!fields.phone || phone.trim().length > 0);
51
+
52
+ const handleSubmit = () => {
53
+ if (!isValid) return;
54
+ onSubmit({
55
+ firstName: firstName.trim() || undefined,
56
+ lastName: lastName.trim() || undefined,
57
+ email: email.trim() || undefined,
58
+ phone: phone.trim() || undefined,
59
+ });
60
+ };
61
+
62
+ const inputStyle = [
63
+ styles.input,
64
+ { color: textColor, backgroundColor: inputBg, borderColor: inputBorder },
65
+ custom.input,
66
+ ];
67
+
68
+ return (
69
+ <ScrollView
70
+ style={[styles.scroll, { backgroundColor: bg }, custom.form]}
71
+ contentContainerStyle={styles.content}
72
+ keyboardShouldPersistTaps="handled"
73
+ >
74
+ <Text style={[styles.heading, { color: textColor }, custom.formTitle]}>{strings.preChatTitle}</Text>
75
+
76
+ {fields.firstName && (
77
+ <View style={styles.field}>
78
+ <Text style={[styles.label, { color: labelColor }, custom.label]}>{strings.required(strings.firstName)}</Text>
79
+ <TextInput
80
+ style={inputStyle}
81
+ value={firstName}
82
+ accessibilityLabel={strings.required(strings.firstName)}
83
+ onChangeText={setFirstName}
84
+ placeholder={strings.firstName}
85
+ placeholderTextColor={placeholderColor}
86
+ autoCapitalize="words"
87
+ autoCorrect={false}
88
+ returnKeyType="next"
89
+ />
90
+ </View>
91
+ )}
92
+
93
+ {fields.lastName && (
94
+ <View style={styles.field}>
95
+ <Text style={[styles.label, { color: labelColor }, custom.label]}>{strings.required(strings.lastName)}</Text>
96
+ <TextInput
97
+ style={inputStyle}
98
+ value={lastName}
99
+ accessibilityLabel={strings.required(strings.lastName)}
100
+ onChangeText={setLastName}
101
+ placeholder={strings.lastName}
102
+ placeholderTextColor={placeholderColor}
103
+ autoCapitalize="words"
104
+ autoCorrect={false}
105
+ returnKeyType="next"
106
+ />
107
+ </View>
108
+ )}
109
+
110
+ {fields.email && (
111
+ <View style={styles.field}>
112
+ <Text style={[styles.label, { color: labelColor }, custom.label]}>{strings.required(strings.email)}</Text>
113
+ <TextInput
114
+ style={inputStyle}
115
+ value={email}
116
+ accessibilityLabel={strings.required(strings.email)}
117
+ onChangeText={setEmail}
118
+ placeholder="you@example.com"
119
+ placeholderTextColor={placeholderColor}
120
+ keyboardType="email-address"
121
+ autoCapitalize="none"
122
+ autoCorrect={false}
123
+ returnKeyType="next"
124
+ />
125
+ </View>
126
+ )}
127
+
128
+ {fields.phone && (
129
+ <View style={styles.field}>
130
+ <Text style={[styles.label, { color: labelColor }, custom.label]}>{strings.required(strings.phone)}</Text>
131
+ <TextInput
132
+ style={inputStyle}
133
+ value={phone}
134
+ accessibilityLabel={strings.required(strings.phone)}
135
+ onChangeText={setPhone}
136
+ placeholder="+1 555 000 0000"
137
+ placeholderTextColor={placeholderColor}
138
+ keyboardType="phone-pad"
139
+ returnKeyType="done"
140
+ />
141
+ </View>
142
+ )}
143
+
144
+ <TouchableOpacity
145
+ style={[
146
+ styles.submitBtn,
147
+ { backgroundColor: primaryColor },
148
+ custom.button,
149
+ !isValid && styles.submitBtnDisabled,
150
+ ]}
151
+ onPress={handleSubmit}
152
+ disabled={!isValid}
153
+ accessibilityRole="button"
154
+ accessibilityLabel={strings.startChat}
155
+ >
156
+ <Text style={[styles.submitBtnText, { color: textOn(custom.button.backgroundColor ?? primaryColor) }, custom.buttonText]}>{strings.startChat}</Text>
157
+ </TouchableOpacity>
158
+ </ScrollView>
159
+ );
160
+ }
161
+
162
+ const styles = StyleSheet.create({
163
+ scroll: { flex: 1 },
164
+ content: { padding: 20, flexGrow: 1 },
165
+ heading: { fontSize: 18, fontWeight: "700", marginBottom: 24 },
166
+ field: { marginBottom: 16 },
167
+ label: { fontSize: 13, fontWeight: "600", marginBottom: 6 },
168
+ input: {
169
+ borderWidth: 1,
170
+ borderRadius: 10,
171
+ paddingHorizontal: 14,
172
+ paddingVertical: 11,
173
+ fontSize: 14,
174
+ },
175
+ submitBtn: {
176
+ borderRadius: 10,
177
+ paddingVertical: 14,
178
+ alignItems: "center",
179
+ marginTop: 8,
180
+ },
181
+ submitBtnDisabled: { opacity: 0.45 },
182
+ submitBtnText: { color: "#fff", fontWeight: "700", fontSize: 15 },
183
+ });
@@ -0,0 +1,219 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Text, TextInput, TouchableOpacity, View } from "react-native";
3
+ import type { GiovFeedbackValue, GiovRatingBlock } from "../types";
4
+ import { useGiovChatStrings } from "../i18n";
5
+ import { GiovChatError } from "../errors";
6
+ import { MAX_FEEDBACK_REASON, MAX_RATING_COMMENT, nextFeedback, ratingFailure, ratingPhase } from "../rating";
7
+ import { textOn } from "../color";
8
+
9
+ /**
10
+ * The conversation rating card (a `rating` block) and the thumbs under GiovBot answers. The card keeps
11
+ * the visitor's pick on screen while the answer is on its way and takes it back, with a short message,
12
+ * when it fails. Faces are emoji, so nothing native is needed (Expo Go included).
13
+ */
14
+ const FACES = ["😞", "🙁", "😐", "🙂", "😄"];
15
+
16
+ export type RateFn = (requestId: string, score: number, comment: string) => Promise<void>;
17
+ export type VoteFn = (messageId: string, value: GiovFeedbackValue, reason?: string) => Promise<void>;
18
+
19
+ export interface RatingCardProps {
20
+ block: GiovRatingBlock;
21
+ primaryColor: string;
22
+ dark: boolean;
23
+ /** Without it the card is read-only (a hidden or unsupported feature). */
24
+ onRate?: RateFn;
25
+ /** Where the card was drawn; sets the test ids. */
26
+ testIDPrefix?: string;
27
+ }
28
+
29
+ export function RatingCard({ block, primaryColor, dark, onRate, testIDPrefix = "giov-rating" }: RatingCardProps) {
30
+ const strings = useGiovChatStrings();
31
+ const [now, setNow] = useState(() => Date.now());
32
+ const [pick, setPick] = useState(0);
33
+ const [comment, setComment] = useState("");
34
+ const [editing, setEditing] = useState(false);
35
+ const [pending, setPending] = useState<{ score: number; comment: string } | null>(null);
36
+ const [error, setError] = useState("");
37
+ const [lock, setLock] = useState<"" | "closed" | "replaced">("");
38
+
39
+ // Read-only from `closesAt` on, without waiting for another render.
40
+ useEffect(() => {
41
+ const wait = block.closesAt - Date.now();
42
+ if (wait <= 0) return;
43
+ const timer = setTimeout(() => setNow(Date.now()), Math.min(wait, 2 ** 31 - 1));
44
+ return () => clearTimeout(timer);
45
+ }, [block.closesAt, now]);
46
+
47
+ const ink = dark ? "#f0f0f0" : "#111";
48
+ const quiet = dark ? "#aaa" : "#555";
49
+ const border = dark ? "#3a3a3a" : "#d4d4d4";
50
+ const label = (score: number) => strings.ratingFace(score);
51
+
52
+ if (lock === "replaced" || ratingPhase(block, now) === "hidden") return null;
53
+ const closed = lock === "closed" || ratingPhase(block, now) === "closed";
54
+ const score = pending?.score ?? block.score;
55
+ const text = pending ? pending.comment : block.comment;
56
+
57
+ if (score && !editing) {
58
+ return (
59
+ <View testID={`${testIDPrefix}-done`} accessibilityRole="summary" accessibilityLabel={`${strings.yourRating}: ${label(score)}`} style={{ marginTop: 8, gap: 6 }}>
60
+ <Text style={{ color: ink, fontSize: 14, fontWeight: "600" }}>
61
+ <Text accessibilityElementsHidden importantForAccessibility="no-hide-descendants">{FACES[score - 1]} </Text>
62
+ {strings.ratingThanks} · {label(score)}
63
+ </Text>
64
+ {text ? <Text style={{ color: quiet, fontSize: 13 }}>{text}</Text> : null}
65
+ {!closed && onRate ? (
66
+ <TouchableOpacity
67
+ testID={`${testIDPrefix}-change`}
68
+ accessibilityRole="button"
69
+ accessibilityLabel={strings.ratingChange}
70
+ disabled={!!pending}
71
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
72
+ onPress={() => { setPick(score); setComment(text ?? ""); setError(""); setEditing(true); }}
73
+ >
74
+ <Text style={{ color: primaryColor, fontSize: 13, fontWeight: "600" }}>{strings.ratingChange}</Text>
75
+ </TouchableOpacity>
76
+ ) : null}
77
+ </View>
78
+ );
79
+ }
80
+ if (closed) return <Text testID={`${testIDPrefix}-closed`} style={{ color: quiet, fontSize: 13, marginTop: 8 }}>{strings.ratingClosed}</Text>;
81
+
82
+ const submit = async () => {
83
+ if (!pick || pending || !onRate) return;
84
+ const next = { score: pick, comment: comment.trim() };
85
+ setError("");
86
+ setPending(next);
87
+ setEditing(false);
88
+ try {
89
+ await onRate(block.requestId, next.score, next.comment);
90
+ } catch (failure) {
91
+ const code = failure instanceof GiovChatError ? failure.code : "network";
92
+ const kind = ratingFailure(code);
93
+ if (kind === "retry") {
94
+ setEditing(true);
95
+ setError(code === "too_many_changes" ? strings.ratingTooMany
96
+ : code === "rate_limited" ? strings.ratingWait(failure instanceof GiovChatError ? failure.retryAfterSeconds ?? 30 : 30)
97
+ : strings.ratingFailed);
98
+ } else setLock(kind);
99
+ } finally {
100
+ setPending(null);
101
+ }
102
+ };
103
+
104
+ return (
105
+ <View testID={`${testIDPrefix}-form`} accessibilityLabel={strings.rateTitle} style={{ marginTop: 8, gap: 8 }}>
106
+ <View accessibilityRole="radiogroup" accessibilityLabel={strings.yourRating} style={{ flexDirection: "row", gap: 6 }}>
107
+ {FACES.map((face, index) => {
108
+ const n = index + 1;
109
+ return (
110
+ <TouchableOpacity
111
+ key={n}
112
+ testID={`${testIDPrefix}-face-${n}`}
113
+ accessibilityRole="radio"
114
+ accessibilityLabel={label(n)}
115
+ accessibilityState={{ checked: pick === n }}
116
+ onPress={() => setPick(n)}
117
+ style={{ width: 44, height: 44, borderRadius: 22, alignItems: "center", justifyContent: "center", borderWidth: pick === n ? 2 : 1, borderColor: pick === n ? primaryColor : border }}
118
+ >
119
+ <Text style={{ fontSize: 22 }}>{face}</Text>
120
+ </TouchableOpacity>
121
+ );
122
+ })}
123
+ </View>
124
+ <TextInput
125
+ testID={`${testIDPrefix}-comment`}
126
+ value={comment}
127
+ onChangeText={setComment}
128
+ maxLength={MAX_RATING_COMMENT}
129
+ multiline
130
+ placeholder={strings.ratingComment}
131
+ placeholderTextColor={dark ? "#777" : "#999"}
132
+ accessibilityLabel={strings.ratingComment}
133
+ style={{ color: ink, borderWidth: 1, borderColor: border, borderRadius: 10, paddingHorizontal: 10, paddingVertical: 8, fontSize: 14, minHeight: 56, maxHeight: 120 }}
134
+ />
135
+ {error ? <Text accessibilityRole="alert" style={{ color: "#dc2626", fontSize: 13 }}>{error}</Text> : null}
136
+ <View style={{ flexDirection: "row", gap: 12, alignItems: "center" }}>
137
+ <TouchableOpacity
138
+ testID={`${testIDPrefix}-send`}
139
+ accessibilityRole="button"
140
+ accessibilityLabel={strings.ratingSend}
141
+ accessibilityState={{ disabled: !pick || !!pending || !onRate }}
142
+ disabled={!pick || !!pending || !onRate}
143
+ onPress={submit}
144
+ style={{ backgroundColor: primaryColor, borderRadius: 8, paddingHorizontal: 18, paddingVertical: 9, opacity: !pick || !onRate ? 0.4 : 1 }}
145
+ >
146
+ <Text style={{ color: textOn(primaryColor), fontSize: 14, fontWeight: "600" }}>{strings.ratingSend}</Text>
147
+ </TouchableOpacity>
148
+ {block.score ? (
149
+ <TouchableOpacity testID={`${testIDPrefix}-cancel`} accessibilityRole="button" accessibilityLabel={strings.ratingCancel} onPress={() => setEditing(false)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
150
+ <Text style={{ color: quiet, fontSize: 14 }}>{strings.ratingCancel}</Text>
151
+ </TouchableOpacity>
152
+ ) : null}
153
+ </View>
154
+ </View>
155
+ );
156
+ }
157
+
158
+ /** Thumbs under a GiovBot answer: one vote, changeable; a thumbs-down offers an optional reason. */
159
+ export function AnswerFeedback({ messageId, value, dark, primaryColor, onVote }: {
160
+ messageId: string; value?: "up" | "down"; dark: boolean; primaryColor: string; onVote: VoteFn;
161
+ }) {
162
+ const strings = useGiovChatStrings();
163
+ const [asking, setAsking] = useState(false);
164
+ const [reason, setReason] = useState("");
165
+ const [failed, setFailed] = useState(false);
166
+ const quiet = dark ? "#aaa" : "#555";
167
+ const tap = async (tapped: "up" | "down") => {
168
+ const next = nextFeedback(value, tapped);
169
+ setFailed(false);
170
+ setReason("");
171
+ setAsking(next === "down");
172
+ try { await onVote(messageId, next); } catch { setAsking(false); setFailed(true); }
173
+ };
174
+ const sendReason = async () => {
175
+ setAsking(false);
176
+ if (reason.trim()) await onVote(messageId, "down", reason.trim()).catch(() => setFailed(true));
177
+ };
178
+ return (
179
+ <View testID={`giov-feedback-${messageId}`} accessibilityRole="toolbar" accessibilityLabel={strings.answerGroup} style={{ marginTop: 2, marginLeft: 52 }}>
180
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
181
+ {(["up", "down"] as const).map((kind) => (
182
+ <TouchableOpacity
183
+ key={kind}
184
+ testID={`giov-feedback-${kind}`}
185
+ accessibilityRole="button"
186
+ accessibilityLabel={kind === "up" ? strings.answerGood : strings.answerBad}
187
+ accessibilityState={{ selected: value === kind }}
188
+ onPress={() => tap(kind)}
189
+ hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
190
+ style={{ width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center", backgroundColor: value === kind ? primaryColor : "transparent", opacity: value && value !== kind ? 0.4 : 1 }}
191
+ >
192
+ <Text style={{ fontSize: 16 }}>{kind === "up" ? "👍" : "👎"}</Text>
193
+ </TouchableOpacity>
194
+ ))}
195
+ {failed ? <Text accessibilityRole="alert" style={{ color: "#dc2626", fontSize: 12 }}>{strings.answerFailed}</Text> : null}
196
+ </View>
197
+ {asking ? (
198
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8, marginTop: 4 }}>
199
+ <TextInput
200
+ testID="giov-feedback-reason"
201
+ value={reason}
202
+ onChangeText={setReason}
203
+ maxLength={MAX_FEEDBACK_REASON}
204
+ placeholder={strings.answerReason}
205
+ placeholderTextColor={dark ? "#777" : "#999"}
206
+ accessibilityLabel={strings.answerReason}
207
+ style={{ flex: 1, color: dark ? "#f0f0f0" : "#111", borderWidth: 1, borderColor: dark ? "#3a3a3a" : "#d4d4d4", borderRadius: 10, paddingHorizontal: 10, paddingVertical: 6, fontSize: 13 }}
208
+ />
209
+ <TouchableOpacity testID="giov-feedback-send" accessibilityRole="button" accessibilityLabel={strings.ratingSend} onPress={sendReason} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
210
+ <Text style={{ color: primaryColor, fontSize: 13, fontWeight: "600" }}>{strings.ratingSend}</Text>
211
+ </TouchableOpacity>
212
+ <TouchableOpacity testID="giov-feedback-skip" accessibilityRole="button" accessibilityLabel={strings.answerSkip} onPress={() => setAsking(false)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
213
+ <Text style={{ color: quiet, fontSize: 13 }}>{strings.answerSkip}</Text>
214
+ </TouchableOpacity>
215
+ </View>
216
+ ) : null}
217
+ </View>
218
+ );
219
+ }
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+ import { Text, View } from "react-native";
3
+ import { bubbleStyles as s } from "./bubbleStyles";
4
+ import { useWidgetStyles } from "./WidgetStyles";
5
+ import { GiovBotMark } from "./GiovBotMark";
6
+ import { useGiovChatStrings } from "../i18n";
7
+
8
+ export interface TypingIndicatorProps {
9
+ dark: boolean;
10
+ /** Only explicit server AI identity enables the AI mark. */
11
+ isBot?: boolean;
12
+ botName?: string;
13
+ }
14
+
15
+ export function TypingIndicator({ dark, isBot, botName }: TypingIndicatorProps) {
16
+ const custom = useWidgetStyles();
17
+ const strings = useGiovChatStrings();
18
+ const isAI = isBot === true;
19
+ return (
20
+ <View
21
+ style={[s.wrapper, s.left, isAI && s.assistantRow]}
22
+ // One element, so screen readers say the label instead of the dots (iOS ignores a label otherwise).
23
+ accessible
24
+ accessibilityLabel={isAI ? strings.botTyping(botName?.trim() || "GiovBot") : strings.teamTyping}
25
+ >
26
+ {isAI && <GiovBotMark />}
27
+ <View style={[s.bubble, s.agentBubble, dark && s.agentBubbleDark, custom.agentBubble]}>
28
+ <Text style={[{ color: dark ? "#666" : "#aaa", fontSize: 20, letterSpacing: 4 }, custom.agentText]}>
29
+ • • •
30
+ </Text>
31
+ </View>
32
+ </View>
33
+ );
34
+ }
@@ -0,0 +1,5 @@
1
+ import { createContext, useContext } from "react";
2
+ import { resolveStyleOverrides } from "../styleOverrides";
3
+
4
+ export const WidgetStylesContext = createContext(resolveStyleOverrides(undefined));
5
+ export const useWidgetStyles = () => useContext(WidgetStylesContext);
@@ -0,0 +1,16 @@
1
+ import { StyleSheet } from "react-native";
2
+
3
+ /** Shared styles for chat bubble components. */
4
+ export const bubbleStyles = StyleSheet.create({
5
+ wrapper: { marginHorizontal: 16, marginBottom: 8 },
6
+ right: { alignItems: "flex-end" },
7
+ left: { alignItems: "flex-start" },
8
+ assistantRow: { flexDirection: "row", gap: 8 },
9
+ bubble: { maxWidth: "78%", borderRadius: 16, padding: 12 },
10
+ visitorBubble: { borderBottomRightRadius: 4 },
11
+ agentBubble: { backgroundColor: "#f0f0f0", borderBottomLeftRadius: 4 },
12
+ agentBubbleDark: { backgroundColor: "#242424" },
13
+ sender: { fontSize: 11, fontWeight: "600", marginBottom: 3 },
14
+ content: { fontSize: 14, lineHeight: 20 },
15
+ timestamp: { fontSize: 10, marginTop: 4, textAlign: "right" },
16
+ });
@@ -0,0 +1,14 @@
1
+ export { Header } from "./Header";
2
+ export type { HeaderProps } from "./Header";
3
+
4
+ export { MessageBubble } from "./MessageBubble";
5
+ export type { MessageBubbleProps } from "./MessageBubble";
6
+
7
+ export { GreetingBubble } from "./GreetingBubble";
8
+ export type { GreetingBubbleProps } from "./GreetingBubble";
9
+
10
+ export { TypingIndicator } from "./TypingIndicator";
11
+ export type { TypingIndicatorProps } from "./TypingIndicator";
12
+
13
+ export { PreChatForm } from "./PreChatForm";
14
+ export type { PreChatFormProps } from "./PreChatForm";
package/src/errors.ts ADDED
@@ -0,0 +1,33 @@
1
+ export type GiovChatErrorCode =
2
+ | "network" | "timeout" | "invalid_response" | "invalid_session" | "conversation_closed"
3
+ | "connection_failed" | "disconnected" | "send_failed" | "upload_failed" | "workspace_busy" | "storage" | "server"
4
+ /** `401 invalid_user_hash`: the workspace requires a signed identity and `userHash` is wrong or missing. */
5
+ | "identity_rejected"
6
+ /** `429 rate_limited`: wait `retryAfterSeconds` before trying again. */
7
+ | "rate_limited"
8
+ /** Ratings and answer feedback (docs/protocol, "Conversation ratings"). `unsupported`: the server has no such routes. */
9
+ | "unsupported" | "invalid_rating" | "invalid_feedback" | "request_not_found" | "message_not_found"
10
+ | "request_replaced" | "rating_closed" | "too_many_changes";
11
+
12
+ export class GiovChatError extends Error {
13
+ constructor(
14
+ public readonly code: GiovChatErrorCode,
15
+ message: string,
16
+ public readonly status?: number,
17
+ /** From `Retry-After` on `429` and `503` answers, in seconds. */
18
+ public readonly retryAfterSeconds?: number,
19
+ ) {
20
+ super(message);
21
+ this.name = "GiovChatError";
22
+ }
23
+ }
24
+
25
+ export function chatError(error: unknown): GiovChatError {
26
+ return error instanceof GiovChatError ? error : new GiovChatError("network", "Unable to reach chat. Please try again.");
27
+ }
28
+
29
+ /** `Retry-After` in seconds (the protocol never sends a date); undefined when absent or malformed. */
30
+ export function retryAfterSeconds(value: string | null | undefined): number | undefined {
31
+ const seconds = Number(value?.trim());
32
+ return value?.trim() && Number.isFinite(seconds) && seconds >= 0 ? Math.min(seconds, 3600) : undefined;
33
+ }