@tribe-nest/forge 3.67.0 → 3.74.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.
- package/package.json +1 -1
- package/src/data/queries/_tests/formConsent.spec.ts +57 -0
- package/src/data/queries/_tests/websiteAgentChoice.spec.ts +155 -0
- package/src/data/queries/_tests/websiteAgentEmbed.spec.ts +52 -0
- package/src/data/queries/_tests/websiteAgentTranscript.spec.ts +119 -0
- package/src/data/queries/useCheckoutLink.ts +127 -0
- package/src/data/queries/useForms.ts +73 -4
- package/src/data/queries/useLeadMagnet.ts +9 -2
- package/src/data/queries/useWebsiteAgent.ts +205 -10
- package/src/i18n/de.json +35 -1
- package/src/i18n/en.json +35 -1
- package/src/index.ts +1 -0
- package/src/provider/ForgeProvider.tsx +12 -2
- package/src/runtime/pages.ts +1 -0
- package/src/types/models.ts +24 -0
- package/src/ui/headless/agent/useAiAgent.ts +171 -20
- package/src/ui/headless/agent/visitorId.ts +27 -0
- package/src/ui/headless/checkoutLink/useCheckoutLinkPayment.ts +116 -0
- package/src/ui/headless/forms/useEmailListForm.ts +45 -3
- package/src/ui/headless/forms/useSectionedForm.ts +26 -6
- package/src/ui/headless/index.ts +1 -0
- package/src/ui/index.ts +4 -2
- package/src/ui/styled/AiAgentWidget.tsx +202 -34
- package/src/ui/styled/Checkout.tsx +1 -113
- package/src/ui/styled/CheckoutLinkPayment.tsx +606 -0
- package/src/ui/styled/EmailListForm.tsx +28 -2
- package/src/ui/styled/FormRenderer.tsx +55 -3
- package/src/ui/styled/ShippingRateSelector.tsx +122 -0
- package/src/ui/styled/_tests/CheckoutLinkPayment.spec.tsx +316 -0
|
@@ -16,6 +16,29 @@ export interface WebsiteAgentConfig {
|
|
|
16
16
|
excludedPaths?: string[];
|
|
17
17
|
/** The agent also takes calls from the browser: show "Call us". */
|
|
18
18
|
voiceEnabled?: boolean;
|
|
19
|
+
/** Through an embed only: the business the key belongs to. */
|
|
20
|
+
profileId?: string;
|
|
21
|
+
/** Through an embed only: the business theme with the key's overrides. */
|
|
22
|
+
theme?: EmbedAgentTheme;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The look an embed key gives the widget (see packages/embed). */
|
|
26
|
+
export interface EmbedAgentTheme {
|
|
27
|
+
primary: string;
|
|
28
|
+
background: string;
|
|
29
|
+
text: string;
|
|
30
|
+
radius: number;
|
|
31
|
+
position: "bottom-right" | "bottom-left";
|
|
32
|
+
colorScheme: "business" | "light" | "dark";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where the website agent's public routes live. A TribeNest site uses
|
|
37
|
+
* `/public/agent`; an embed on another site goes through its key, whose
|
|
38
|
+
* gate answers CORS for the key's allowed origins only.
|
|
39
|
+
*/
|
|
40
|
+
export function agentApiPath(embedKey?: string | null): string {
|
|
41
|
+
return embedKey ? `/public/embed/${encodeURIComponent(embedKey)}/agent` : "/public/agent";
|
|
19
42
|
}
|
|
20
43
|
|
|
21
44
|
/** Widget bootstrap config for the website. When `enabled` is false the widget
|
|
@@ -23,11 +46,11 @@ export interface WebsiteAgentConfig {
|
|
|
23
46
|
* profile with several sites can give each its own; without it the backend
|
|
24
47
|
* answers with the profile's default agent. */
|
|
25
48
|
export function useWebsiteAgentConfig() {
|
|
26
|
-
const { client, profileId, websiteId } = useForge();
|
|
49
|
+
const { client, profileId, websiteId, embedKey } = useForge();
|
|
27
50
|
return useQuery<WebsiteAgentConfig>({
|
|
28
|
-
queryKey: ["website-agent-config", profileId, websiteId ?? null],
|
|
51
|
+
queryKey: ["website-agent-config", profileId, websiteId ?? null, embedKey ?? null],
|
|
29
52
|
queryFn: async () => {
|
|
30
|
-
const res = await client.get(
|
|
53
|
+
const res = await client.get(`${agentApiPath(embedKey)}/config`, {
|
|
31
54
|
params: { profileId, ...(websiteId ? { websiteId } : {}) },
|
|
32
55
|
});
|
|
33
56
|
return res.data;
|
|
@@ -37,12 +60,93 @@ export function useWebsiteAgentConfig() {
|
|
|
37
60
|
});
|
|
38
61
|
}
|
|
39
62
|
|
|
63
|
+
/** One option of a playbook choice. Live items (services) carry their details, and render as cards. */
|
|
64
|
+
export interface AgentChoiceOption {
|
|
65
|
+
value: string;
|
|
66
|
+
label: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
priceText?: string;
|
|
69
|
+
durationText?: string;
|
|
70
|
+
imageUrl?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A playbook Choose step, sent as its own event. `text` is the same choice
|
|
75
|
+
* rendered as plain text, for a widget that cannot draw buttons.
|
|
76
|
+
*/
|
|
77
|
+
export interface AgentChoiceEvent {
|
|
78
|
+
type: "choice";
|
|
79
|
+
stepId: string;
|
|
80
|
+
question: string;
|
|
81
|
+
multiple: false;
|
|
82
|
+
options: AgentChoiceOption[];
|
|
83
|
+
text: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A tapped option, sent back in place of a typed message. */
|
|
87
|
+
export interface AgentChoiceReply {
|
|
88
|
+
stepId: string;
|
|
89
|
+
value: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
40
92
|
export type AgentStreamEvent =
|
|
41
93
|
| { type: "text_delta"; delta: string }
|
|
42
94
|
| { type: "tool_use_start"; name: string }
|
|
43
95
|
| { type: "tool_use_result"; name: string }
|
|
44
96
|
| { type: "turn_complete" }
|
|
45
|
-
| { type: "error"; message: string }
|
|
97
|
+
| { type: "error"; message: string }
|
|
98
|
+
| AgentChoiceEvent;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The words of an assistant message without the choice's plain text. The
|
|
102
|
+
* server streams `choice.text` as the turn's last `text_delta` ("\n\n" + text,
|
|
103
|
+
* or the text alone when nothing came before it) so a widget without buttons
|
|
104
|
+
* still shows the options. A widget that draws the buttons hides that tail so
|
|
105
|
+
* the options are not shown twice. Only an exact suffix is removed.
|
|
106
|
+
*/
|
|
107
|
+
export function textWithoutChoice(text: string, choice: Pick<AgentChoiceEvent, "text"> | null | undefined): string {
|
|
108
|
+
const tail = choice?.text;
|
|
109
|
+
if (!tail) return text;
|
|
110
|
+
if (text === tail) return "";
|
|
111
|
+
if (text.endsWith(`\n\n${tail}`)) return text.slice(0, text.length - tail.length - 2);
|
|
112
|
+
return text;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A choice event in a shape the widget can draw. Anything else, including a
|
|
117
|
+
* choice with no usable options, is dropped rather than crashing the chat.
|
|
118
|
+
*/
|
|
119
|
+
export function parseChoiceEvent(raw: unknown): AgentChoiceEvent | null {
|
|
120
|
+
if (!raw || typeof raw !== "object") return null;
|
|
121
|
+
const ev = raw as Record<string, unknown>;
|
|
122
|
+
if (ev.type !== "choice" || typeof ev.stepId !== "string" || !Array.isArray(ev.options)) return null;
|
|
123
|
+
const str = (v: unknown) => (typeof v === "string" && v ? v : undefined);
|
|
124
|
+
const options: AgentChoiceOption[] = [];
|
|
125
|
+
for (const o of ev.options) {
|
|
126
|
+
if (!o || typeof o !== "object") continue;
|
|
127
|
+
const opt = o as Record<string, unknown>;
|
|
128
|
+
const value = typeof opt.value === "string" ? opt.value : typeof opt.value === "number" ? String(opt.value) : null;
|
|
129
|
+
const label = str(opt.label) ?? value;
|
|
130
|
+
if (!value || !label) continue;
|
|
131
|
+
options.push({
|
|
132
|
+
value,
|
|
133
|
+
label,
|
|
134
|
+
description: str(opt.description),
|
|
135
|
+
priceText: str(opt.priceText),
|
|
136
|
+
durationText: str(opt.durationText),
|
|
137
|
+
imageUrl: str(opt.imageUrl),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (!options.length) return null;
|
|
141
|
+
return {
|
|
142
|
+
type: "choice",
|
|
143
|
+
stepId: ev.stepId,
|
|
144
|
+
question: str(ev.question) ?? "",
|
|
145
|
+
multiple: false,
|
|
146
|
+
options,
|
|
147
|
+
text: str(ev.text) ?? "",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
46
150
|
|
|
47
151
|
/**
|
|
48
152
|
* Streams a chat turn over SSE using fetch (axios can't stream). Calls
|
|
@@ -54,17 +158,26 @@ export async function streamAgentMessage(params: {
|
|
|
54
158
|
profileId: string;
|
|
55
159
|
visitorId: string;
|
|
56
160
|
conversationId: string;
|
|
57
|
-
|
|
161
|
+
/** What the visitor typed. */
|
|
162
|
+
message?: string;
|
|
163
|
+
/** A tapped choice, sent instead of `message`. */
|
|
164
|
+
choice?: AgentChoiceReply;
|
|
58
165
|
onEvent: (ev: AgentStreamEvent) => void;
|
|
59
166
|
signal?: AbortSignal;
|
|
167
|
+
/** Through an embed: the key the request goes through. */
|
|
168
|
+
embedKey?: string | null;
|
|
60
169
|
}): Promise<void> {
|
|
61
|
-
const res = await fetch(`${params.apiUrl}/
|
|
170
|
+
const res = await fetch(`${params.apiUrl}${agentApiPath(params.embedKey)}/conversations/${params.conversationId}/messages`, {
|
|
62
171
|
method: "POST",
|
|
63
172
|
headers: {
|
|
64
173
|
"Content-Type": "application/json",
|
|
65
174
|
...(params.token ? { authorization: `Bearer ${params.token}` } : {}),
|
|
66
175
|
},
|
|
67
|
-
body: JSON.stringify({
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
profileId: params.profileId,
|
|
178
|
+
visitorId: params.visitorId,
|
|
179
|
+
...(params.choice ? { choice: params.choice } : { message: params.message }),
|
|
180
|
+
}),
|
|
68
181
|
signal: params.signal,
|
|
69
182
|
});
|
|
70
183
|
|
|
@@ -84,15 +197,96 @@ export async function streamAgentMessage(params: {
|
|
|
84
197
|
if (!line) continue;
|
|
85
198
|
const payload = line.slice(5).trim();
|
|
86
199
|
if (!payload) continue;
|
|
200
|
+
let parsed: unknown;
|
|
87
201
|
try {
|
|
88
|
-
|
|
202
|
+
parsed = JSON.parse(payload);
|
|
89
203
|
} catch {
|
|
90
|
-
|
|
204
|
+
continue; // a heartbeat or a malformed frame
|
|
205
|
+
}
|
|
206
|
+
if (!parsed || typeof parsed !== "object") continue;
|
|
207
|
+
// A choice is validated, so a widget never draws half an option list.
|
|
208
|
+
// Unknown types pass through; every consumer switches on `type` and
|
|
209
|
+
// ignores the rest.
|
|
210
|
+
const ev = (parsed as { type?: unknown }).type === "choice" ? parseChoiceEvent(parsed) : (parsed as AgentStreamEvent);
|
|
211
|
+
if (!ev) continue;
|
|
212
|
+
try {
|
|
213
|
+
params.onEvent(ev);
|
|
214
|
+
} catch {
|
|
215
|
+
/* a consumer's error must not end the stream */
|
|
91
216
|
}
|
|
92
217
|
}
|
|
93
218
|
}
|
|
94
219
|
}
|
|
95
220
|
|
|
221
|
+
/** One message of a saved conversation, as `GET /public/agent/conversations/:id` returns it. */
|
|
222
|
+
export interface AgentTranscriptMessage {
|
|
223
|
+
role: "user" | "assistant";
|
|
224
|
+
text: string;
|
|
225
|
+
/** The playbook choice this assistant message offered. */
|
|
226
|
+
choice?: AgentChoiceEvent;
|
|
227
|
+
/** The value the visitor picked, when the next message is one of the options' labels. */
|
|
228
|
+
picked?: string;
|
|
229
|
+
/** A choice that can no longer be answered: the conversation moved on past it. */
|
|
230
|
+
closed?: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A saved transcript in the shape the widget draws. Only user and assistant
|
|
235
|
+
* text survives; a saved choice is validated like a streamed one. A choice is
|
|
236
|
+
* answerable only while it is the last message: one the visitor answered is
|
|
237
|
+
* marked with the picked option (its label is the next user message), and one
|
|
238
|
+
* the conversation moved past is closed.
|
|
239
|
+
*/
|
|
240
|
+
export function restoreTranscriptMessages(raw: unknown): AgentTranscriptMessage[] {
|
|
241
|
+
const list = raw && typeof raw === "object" ? (raw as { messages?: unknown }).messages : undefined;
|
|
242
|
+
if (!Array.isArray(list)) return [];
|
|
243
|
+
const out: AgentTranscriptMessage[] = [];
|
|
244
|
+
for (const item of list) {
|
|
245
|
+
if (!item || typeof item !== "object") continue;
|
|
246
|
+
const m = item as Record<string, unknown>;
|
|
247
|
+
if (m.role !== "user" && m.role !== "assistant") continue;
|
|
248
|
+
const text = typeof m.text === "string" ? m.text : "";
|
|
249
|
+
const choice = m.role === "assistant" ? parseChoiceEvent(m.choice) : null;
|
|
250
|
+
if (!text && !choice) continue;
|
|
251
|
+
out.push({ role: m.role, text, ...(choice ? { choice } : {}) });
|
|
252
|
+
}
|
|
253
|
+
out.forEach((m, i) => {
|
|
254
|
+
if (!m.choice || i === out.length - 1) return;
|
|
255
|
+
const next = out[i + 1];
|
|
256
|
+
const picked = next?.role === "user" ? m.choice.options.find((o) => o.label === next.text.trim()) : undefined;
|
|
257
|
+
if (picked) m.picked = picked.value;
|
|
258
|
+
m.closed = true;
|
|
259
|
+
});
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Loads a saved conversation. Null when it is not this visitor's (any more),
|
|
265
|
+
* so the caller can forget the stored id and start afresh.
|
|
266
|
+
*/
|
|
267
|
+
export async function fetchAgentTranscript(params: {
|
|
268
|
+
apiUrl: string;
|
|
269
|
+
token?: string | null;
|
|
270
|
+
profileId: string;
|
|
271
|
+
visitorId: string;
|
|
272
|
+
conversationId: string;
|
|
273
|
+
signal?: AbortSignal;
|
|
274
|
+
embedKey?: string | null;
|
|
275
|
+
}): Promise<{ id: string; messages: AgentTranscriptMessage[] } | null> {
|
|
276
|
+
const qs = new URLSearchParams({ profileId: params.profileId, visitorId: params.visitorId });
|
|
277
|
+
const res = await fetch(
|
|
278
|
+
`${params.apiUrl}${agentApiPath(params.embedKey)}/conversations/${encodeURIComponent(params.conversationId)}?${qs.toString()}`,
|
|
279
|
+
{
|
|
280
|
+
headers: params.token ? { authorization: `Bearer ${params.token}` } : {},
|
|
281
|
+
signal: params.signal,
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
if (!res.ok) return null;
|
|
285
|
+
const body = (await res.json()) as { id?: unknown } | null;
|
|
286
|
+
if (!body || typeof body.id !== "string") return null;
|
|
287
|
+
return { id: body.id, messages: restoreTranscriptMessages(body) };
|
|
288
|
+
}
|
|
289
|
+
|
|
96
290
|
/** Creates a visitor conversation, returning its id (or null when disabled). */
|
|
97
291
|
export async function createAgentConversation(params: {
|
|
98
292
|
apiUrl: string;
|
|
@@ -100,8 +294,9 @@ export async function createAgentConversation(params: {
|
|
|
100
294
|
profileId: string;
|
|
101
295
|
websiteId?: string;
|
|
102
296
|
visitorId: string;
|
|
297
|
+
embedKey?: string | null;
|
|
103
298
|
}): Promise<{ id: string | null; requiresLogin?: boolean }> {
|
|
104
|
-
const res = await fetch(`${params.apiUrl}/
|
|
299
|
+
const res = await fetch(`${params.apiUrl}${agentApiPath(params.embedKey)}/conversations`, {
|
|
105
300
|
method: "POST",
|
|
106
301
|
headers: {
|
|
107
302
|
"Content-Type": "application/json",
|
package/src/i18n/de.json
CHANGED
|
@@ -191,6 +191,7 @@
|
|
|
191
191
|
"forge.ai_agent_call_room.speaking": "{name} spricht",
|
|
192
192
|
"forge.ai_agent_widget.aria_call": "Ruf uns an",
|
|
193
193
|
"forge.ai_agent_widget.aria_close": "Schließen",
|
|
194
|
+
"forge.ai_agent_widget.aria_message": "Nachricht",
|
|
194
195
|
"forge.ai_agent_widget.aria_open": "Chat-Assistent öffnen",
|
|
195
196
|
"forge.ai_agent_widget.aria_show_call": "Anruf anzeigen",
|
|
196
197
|
"forge.ai_agent_widget.call_again": "Erneut anrufen",
|
|
@@ -204,6 +205,7 @@
|
|
|
204
205
|
"forge.ai_agent_widget.call_error_unavailable": "Anrufe sind gerade nicht möglich.",
|
|
205
206
|
"forge.ai_agent_widget.call_hang_up": "Auflegen",
|
|
206
207
|
"forge.ai_agent_widget.call_try_again": "Erneut versuchen",
|
|
208
|
+
"forge.ai_agent_widget.choice_label": "Eine Option wählen",
|
|
207
209
|
"forge.ai_agent_widget.default_greeting": "Hi! Frag mich, was du willst.",
|
|
208
210
|
"forge.ai_agent_widget.default_title": "Assistent",
|
|
209
211
|
"forge.ai_agent_widget.input_placeholder": "Nachricht schreiben…",
|
|
@@ -713,6 +715,37 @@
|
|
|
713
715
|
"forge.checkout_confirmation.total": "Gesamt",
|
|
714
716
|
"forge.checkout_confirmation.total_paid": "Bezahlt gesamt",
|
|
715
717
|
"forge.checkout_confirmation.view_orders": "Bestellungen ansehen",
|
|
718
|
+
"forge.checkout_link_payment.browse_store": "Zum Shop",
|
|
719
|
+
"forge.checkout_link_payment.cancelled_body": "Das Unternehmen hat diesen Link zurückgezogen. Es wurde nichts berechnet.",
|
|
720
|
+
"forge.checkout_link_payment.cancelled_title": "Dieser Link ist nicht mehr verfügbar",
|
|
721
|
+
"forge.checkout_link_payment.each": "je {price}",
|
|
722
|
+
"forge.checkout_link_payment.ended_body": "Wir konnten dies nicht länger für Sie reservieren, deshalb ist es wieder im Verkauf. Es wurde nichts berechnet.",
|
|
723
|
+
"forge.checkout_link_payment.ended_title": "Diese Reservierung ist abgelaufen",
|
|
724
|
+
"forge.checkout_link_payment.held_until": "Für Sie reserviert bis {time}",
|
|
725
|
+
"forge.checkout_link_payment.hold_noun": "Artikel",
|
|
726
|
+
"forge.checkout_link_payment.hold_noun_booking": "diesen Termin",
|
|
727
|
+
"forge.checkout_link_payment.not_found_body": "Dieser Link ist ungültig. Prüfen Sie die Adresse in Ihrer E-Mail.",
|
|
728
|
+
"forge.checkout_link_payment.not_found_title": "Link nicht gefunden",
|
|
729
|
+
"forge.checkout_link_payment.other_times": "Anderen Termin wählen",
|
|
730
|
+
"forge.checkout_link_payment.paid_body": "Vielen Dank. Die Details sind unterwegs an Ihre E-Mail-Adresse.",
|
|
731
|
+
"forge.checkout_link_payment.paid_title": "Zahlung erhalten",
|
|
732
|
+
"forge.checkout_link_payment.pay_amount": "{amount} bezahlen",
|
|
733
|
+
"forge.checkout_link_payment.payment_unavailable": "Die Zahlung ist gerade nicht möglich. Bitte versuchen Sie es gleich noch einmal.",
|
|
734
|
+
"forge.checkout_link_payment.preparing_payment": "Zahlung wird vorbereitet…",
|
|
735
|
+
"forge.checkout_link_payment.quantity": "Anzahl: {quantity}",
|
|
736
|
+
"forge.checkout_link_payment.title": "Für Sie reserviert",
|
|
737
|
+
"forge.checkout_link_payment.too_late_body": "Ihre Zahlung ist nach dem Ende dieser Reservierung eingegangen, deshalb wurde nichts gebucht. Ihre Zahlung wird erstattet.",
|
|
738
|
+
"forge.checkout_link_payment.too_late_title": "Ihre Zahlung wird erstattet",
|
|
739
|
+
"forge.checkout_link_payment.total": "Gesamt",
|
|
740
|
+
"forge.checkout_link_payment.try_again": "Erneut versuchen",
|
|
741
|
+
"forge.checkout_link_payment.unpaid_body": "Ihre Zahlung ist nicht durchgegangen, und es wurde nichts berechnet. Ihre Reservierung besteht weiter, Sie können es erneut versuchen.",
|
|
742
|
+
"forge.checkout_link_payment.unpaid_title": "Zahlung nicht abgeschlossen",
|
|
743
|
+
"forge.checkout_link_payment.shipping_title": "Lieferadresse",
|
|
744
|
+
"forge.checkout_link_payment.ship_to": "Lieferung an",
|
|
745
|
+
"forge.checkout_link_payment.change_address": "Ändern",
|
|
746
|
+
"forge.checkout_link_payment.use_shipping": "Diesen Versand nutzen",
|
|
747
|
+
"forge.checkout_link_payment.shipping": "Versand",
|
|
748
|
+
"forge.checkout_link_payment.shipping_unavailable": "Für diese Adresse konnten wir keinen Versand finden. Prüfe sie und versuche es erneut.",
|
|
716
749
|
"forge.coaching_booking.aria_close": "Schließen",
|
|
717
750
|
"forge.coaching_booking.aria_next_month": "Nächster Monat",
|
|
718
751
|
"forge.coaching_booking.aria_next_week": "Nächste Woche",
|
|
@@ -2438,5 +2471,6 @@
|
|
|
2438
2471
|
"forge.work_task_detail.fallback_title": "Aufgabe",
|
|
2439
2472
|
"forge.work_task_detail.status_done": "Erledigt",
|
|
2440
2473
|
"forge.work_task_detail.status_open": "Offen",
|
|
2441
|
-
"forge.work_token_report.invalid_link": "Dieser Berichtslink ist ungültig oder abgelaufen."
|
|
2474
|
+
"forge.work_token_report.invalid_link": "Dieser Berichtslink ist ungültig oder abgelaufen.",
|
|
2475
|
+
"forge.form_renderer.consent_phone_required": "Geben Sie Ihre Telefonnummer ein, um Anrufen oder Nachrichten zuzustimmen."
|
|
2442
2476
|
}
|
package/src/i18n/en.json
CHANGED
|
@@ -191,6 +191,7 @@
|
|
|
191
191
|
"forge.ai_agent_call_room.speaking": "{name} is speaking",
|
|
192
192
|
"forge.ai_agent_widget.aria_call": "Call us",
|
|
193
193
|
"forge.ai_agent_widget.aria_close": "Close",
|
|
194
|
+
"forge.ai_agent_widget.aria_message": "Message",
|
|
194
195
|
"forge.ai_agent_widget.aria_open": "Open chat assistant",
|
|
195
196
|
"forge.ai_agent_widget.aria_show_call": "Show the call",
|
|
196
197
|
"forge.ai_agent_widget.call_again": "Call again",
|
|
@@ -204,6 +205,7 @@
|
|
|
204
205
|
"forge.ai_agent_widget.call_error_unavailable": "Calls are not available right now.",
|
|
205
206
|
"forge.ai_agent_widget.call_hang_up": "Hang up",
|
|
206
207
|
"forge.ai_agent_widget.call_try_again": "Try again",
|
|
208
|
+
"forge.ai_agent_widget.choice_label": "Choose an option",
|
|
207
209
|
"forge.ai_agent_widget.default_greeting": "Hi! Ask me anything.",
|
|
208
210
|
"forge.ai_agent_widget.default_title": "Assistant",
|
|
209
211
|
"forge.ai_agent_widget.input_placeholder": "Type a message…",
|
|
@@ -713,6 +715,37 @@
|
|
|
713
715
|
"forge.checkout_confirmation.total": "Total",
|
|
714
716
|
"forge.checkout_confirmation.total_paid": "Total paid",
|
|
715
717
|
"forge.checkout_confirmation.view_orders": "View your orders",
|
|
718
|
+
"forge.checkout_link_payment.browse_store": "Browse the store",
|
|
719
|
+
"forge.checkout_link_payment.cancelled_body": "The business withdrew this link. Nothing was charged.",
|
|
720
|
+
"forge.checkout_link_payment.cancelled_title": "This link is no longer available",
|
|
721
|
+
"forge.checkout_link_payment.each": "{price} each",
|
|
722
|
+
"forge.checkout_link_payment.ended_body": "We could not hold this for you any longer, so it went back on sale. Nothing was charged.",
|
|
723
|
+
"forge.checkout_link_payment.ended_title": "This reservation has ended",
|
|
724
|
+
"forge.checkout_link_payment.held_until": "Held for you until {time}",
|
|
725
|
+
"forge.checkout_link_payment.hold_noun": "items",
|
|
726
|
+
"forge.checkout_link_payment.hold_noun_booking": "this time",
|
|
727
|
+
"forge.checkout_link_payment.not_found_body": "This link is not valid. Check the address in your email.",
|
|
728
|
+
"forge.checkout_link_payment.not_found_title": "Link not found",
|
|
729
|
+
"forge.checkout_link_payment.other_times": "Pick another time",
|
|
730
|
+
"forge.checkout_link_payment.paid_body": "Thank you. The details are on their way to your email.",
|
|
731
|
+
"forge.checkout_link_payment.paid_title": "Payment received",
|
|
732
|
+
"forge.checkout_link_payment.pay_amount": "Pay {amount}",
|
|
733
|
+
"forge.checkout_link_payment.payment_unavailable": "Payment is not available right now. Please try again in a moment.",
|
|
734
|
+
"forge.checkout_link_payment.preparing_payment": "Preparing payment…",
|
|
735
|
+
"forge.checkout_link_payment.quantity": "Quantity: {quantity}",
|
|
736
|
+
"forge.checkout_link_payment.title": "Held for you",
|
|
737
|
+
"forge.checkout_link_payment.too_late_body": "Your payment arrived after this reservation had ended, so nothing was booked. Your payment will be refunded.",
|
|
738
|
+
"forge.checkout_link_payment.too_late_title": "Your payment will be refunded",
|
|
739
|
+
"forge.checkout_link_payment.total": "Total",
|
|
740
|
+
"forge.checkout_link_payment.try_again": "Try again",
|
|
741
|
+
"forge.checkout_link_payment.unpaid_body": "Your payment did not go through, and nothing was charged. Your reservation is still held, so you can try again.",
|
|
742
|
+
"forge.checkout_link_payment.unpaid_title": "Payment not completed",
|
|
743
|
+
"forge.checkout_link_payment.shipping_title": "Delivery address",
|
|
744
|
+
"forge.checkout_link_payment.ship_to": "Delivering to",
|
|
745
|
+
"forge.checkout_link_payment.change_address": "Change",
|
|
746
|
+
"forge.checkout_link_payment.use_shipping": "Use this shipping",
|
|
747
|
+
"forge.checkout_link_payment.shipping": "Shipping",
|
|
748
|
+
"forge.checkout_link_payment.shipping_unavailable": "We could not get shipping for this address. Check it and try again.",
|
|
716
749
|
"forge.coaching_booking.aria_close": "Close",
|
|
717
750
|
"forge.coaching_booking.aria_next_month": "Next month",
|
|
718
751
|
"forge.coaching_booking.aria_next_week": "Next week",
|
|
@@ -2438,5 +2471,6 @@
|
|
|
2438
2471
|
"forge.work_task_detail.fallback_title": "Task",
|
|
2439
2472
|
"forge.work_task_detail.status_done": "Done",
|
|
2440
2473
|
"forge.work_task_detail.status_open": "To do",
|
|
2441
|
-
"forge.work_token_report.invalid_link": "This report link is invalid or has expired."
|
|
2474
|
+
"forge.work_token_report.invalid_link": "This report link is invalid or has expired.",
|
|
2475
|
+
"forge.form_renderer.consent_phone_required": "Enter your phone number to agree to calls or messages."
|
|
2442
2476
|
}
|
package/src/index.ts
CHANGED
|
@@ -104,6 +104,7 @@ export * from "./data/queries/useMusicLink";
|
|
|
104
104
|
export * from "./data/queries/useEventSeries";
|
|
105
105
|
export * from "./data/queries/useInvoice";
|
|
106
106
|
export * from "./data/queries/usePaymentLink";
|
|
107
|
+
export * from "./data/queries/useCheckoutLink";
|
|
107
108
|
export * from "./data/queries/useLeadMagnet";
|
|
108
109
|
export * from "./data/queries/useBlogComments";
|
|
109
110
|
export * from "./data/queries/usePageActions";
|
|
@@ -31,6 +31,13 @@ export interface ForgeContextValue {
|
|
|
31
31
|
subdomain?: string;
|
|
32
32
|
/** Per-tenant publishable key (`pk_site_…`), sent as `x-forge-key`. */
|
|
33
33
|
publishableKey?: string;
|
|
34
|
+
/**
|
|
35
|
+
* An agent embed key (`pk_embed_...`), set only by the embed loader
|
|
36
|
+
* (packages/embed) on a site TribeNest does not host. The website agent then
|
|
37
|
+
* talks to `/public/embed/<key>/agent/*`, where the key, not `profileId`,
|
|
38
|
+
* decides the business and the agent, and nobody is signed in.
|
|
39
|
+
*/
|
|
40
|
+
embedKey?: string;
|
|
34
41
|
/** The configured Axios client, scoped to this tenant. */
|
|
35
42
|
client: AxiosInstance;
|
|
36
43
|
/** Current member bearer token, or null when anonymous. */
|
|
@@ -49,6 +56,8 @@ export interface ForgeClientProviderProps {
|
|
|
49
56
|
appId?: string;
|
|
50
57
|
subdomain?: string;
|
|
51
58
|
publishableKey?: string;
|
|
59
|
+
/** See `ForgeContextValue.embedKey`. */
|
|
60
|
+
embedKey?: string;
|
|
52
61
|
/** Optional initial token (e.g. restored from storage on mount). */
|
|
53
62
|
initialToken?: string | null;
|
|
54
63
|
children: React.ReactNode;
|
|
@@ -71,6 +80,7 @@ export const ForgeClientProvider = ({
|
|
|
71
80
|
appId,
|
|
72
81
|
subdomain,
|
|
73
82
|
publishableKey,
|
|
83
|
+
embedKey,
|
|
74
84
|
initialToken = null,
|
|
75
85
|
children,
|
|
76
86
|
}: ForgeClientProviderProps) => {
|
|
@@ -119,8 +129,8 @@ export const ForgeClientProvider = ({
|
|
|
119
129
|
);
|
|
120
130
|
|
|
121
131
|
const value = useMemo<ForgeContextValue>(
|
|
122
|
-
() => ({ apiUrl, rootDomain, profileId, websiteId, appId, subdomain, publishableKey, client, token, setToken }),
|
|
123
|
-
[apiUrl, rootDomain, profileId, websiteId, appId, subdomain, publishableKey, client, token, setToken],
|
|
132
|
+
() => ({ apiUrl, rootDomain, profileId, websiteId, appId, subdomain, publishableKey, embedKey, client, token, setToken }),
|
|
133
|
+
[apiUrl, rootDomain, profileId, websiteId, appId, subdomain, publishableKey, embedKey, client, token, setToken],
|
|
124
134
|
);
|
|
125
135
|
|
|
126
136
|
return <ForgeContext.Provider value={value}>{children}</ForgeContext.Provider>;
|
package/src/runtime/pages.ts
CHANGED
package/src/types/models.ts
CHANGED
|
@@ -2076,6 +2076,30 @@ export type FormField = {
|
|
|
2076
2076
|
order: number;
|
|
2077
2077
|
/** Which section this field belongs to (null on legacy flat forms). */
|
|
2078
2078
|
sectionId?: string | null;
|
|
2079
|
+
/**
|
|
2080
|
+
* Type-specific settings. A consent box (`call_consent`) keeps its channel and
|
|
2081
|
+
* the phone field it is bound to here.
|
|
2082
|
+
*/
|
|
2083
|
+
config?: { channel?: ConsentChannel; phoneFieldId?: string | null } | null;
|
|
2084
|
+
/**
|
|
2085
|
+
* On a consent box only: the exact text to show beside it, in the visitor's
|
|
2086
|
+
* language, as the server built it. Show `text` as it is (never rewrite it)
|
|
2087
|
+
* and send `textHash` back with the tick: that is what makes the tick evidence.
|
|
2088
|
+
*/
|
|
2089
|
+
consent?: FormFieldConsent | null;
|
|
2090
|
+
};
|
|
2091
|
+
|
|
2092
|
+
/** A channel a person can agree to be contacted on. */
|
|
2093
|
+
export type ConsentChannel = "ai_call" | "sms" | "whatsapp";
|
|
2094
|
+
|
|
2095
|
+
/** The locked consent wording the server serves with a consent box. */
|
|
2096
|
+
export type FormFieldConsent = {
|
|
2097
|
+
channel: ConsentChannel;
|
|
2098
|
+
phoneFieldId: string | null;
|
|
2099
|
+
locale: string;
|
|
2100
|
+
version: string;
|
|
2101
|
+
text: string;
|
|
2102
|
+
textHash: string;
|
|
2079
2103
|
};
|
|
2080
2104
|
|
|
2081
2105
|
/** Edge scope: field→field jump, section→section page flow, or in-page show/hide. */
|