@sevenfold/setto-client 0.20.1 → 0.21.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/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type { SettoImageProps } from './SettoImage';
|
|
|
15
15
|
export { SettoRepeater } from './SettoRepeater';
|
|
16
16
|
export type { SettoRepeaterProps } from './SettoRepeater';
|
|
17
17
|
export { SettoForm, FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS, fieldTypeHasOptions, fieldTypeHasPlaceholder, formFieldsPrefix, isFormFieldType, readLocaleFields, settoSubmissionUrl, } from './modules/forms';
|
|
18
|
+
export { SettoChat } from './modules/chat';
|
|
18
19
|
export type { SettoFormProps, SettoFormClassNames, FormFieldProp, FormSettings, FormSubmissionRow, FormValue, FormValues, ResolvedFormField, SubmissionEnvelope, } from './modules/forms';
|
|
19
20
|
export { SETTO_MODULES } from './modules';
|
|
20
21
|
export type { SettoModule } from './modules';
|
package/dist/setto-client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { useState, useRef, useEffect, useMemo, createContext, useContext, useCallback, useLayoutEffect, useSyncExternalStore, forwardRef, Fragment as Fragment$1
|
|
3
|
+
import { useState, useRef, useEffect, useMemo, useId, useInsertionEffect, createContext, useContext, useCallback, useLayoutEffect, useSyncExternalStore, forwardRef, Fragment as Fragment$1 } from "react";
|
|
4
4
|
import { useTranslation } from "react-i18next";
|
|
5
5
|
import * as ReactDOM from "react-dom";
|
|
6
6
|
import { createPortal } from "react-dom";
|
|
@@ -118,8 +118,8 @@ function redirectForVisitor(pathname, languages, browserLanguages, stored, prima
|
|
|
118
118
|
const base = wanted.toLowerCase().split("-")[0];
|
|
119
119
|
if (!base) continue;
|
|
120
120
|
if (base === primary.toLowerCase().split("-")[0]) return null;
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
121
|
+
const norwegian2 = ["no", "nb", "nn"];
|
|
122
|
+
if (norwegian2.includes(base) && norwegian2.includes(primary.toLowerCase())) return null;
|
|
123
123
|
const hit = languages.find((tag) => tag.toLowerCase().split("-")[0] === base);
|
|
124
124
|
if (hit && hit !== primary) return pathForLanguage(pathname, hit, languages, primary);
|
|
125
125
|
}
|
|
@@ -22032,6 +22032,234 @@ function EditModeShell({ children, themeStore, active }) {
|
|
|
22032
22032
|
active && sectionToolbarRoot && themeStore && SectionTools ? createPortal(/* @__PURE__ */ jsx(SectionTools, {}), sectionToolbarRoot) : null
|
|
22033
22033
|
] });
|
|
22034
22034
|
}
|
|
22035
|
+
const VISITOR_KEY = "setto-chat-visitor";
|
|
22036
|
+
function visitorId() {
|
|
22037
|
+
try {
|
|
22038
|
+
const stored = window.localStorage.getItem(VISITOR_KEY);
|
|
22039
|
+
if (stored && /^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(stored)) return stored;
|
|
22040
|
+
const id = crypto.randomUUID();
|
|
22041
|
+
window.localStorage.setItem(VISITOR_KEY, id);
|
|
22042
|
+
return id;
|
|
22043
|
+
} catch {
|
|
22044
|
+
return crypto.randomUUID();
|
|
22045
|
+
}
|
|
22046
|
+
}
|
|
22047
|
+
function norwegian(locale) {
|
|
22048
|
+
return locale.toLowerCase().startsWith("no");
|
|
22049
|
+
}
|
|
22050
|
+
function intro(locale, name) {
|
|
22051
|
+
return norwegian(locale) ? `Hei! Jeg er Barista, ${name ? `${name}s` : "nettstedets"} AI-assistent. Jeg svarer ut fra det teamet har delt her, og hjelper deg videre til dem når det trengs.` : `Hello! I’m Barista, the AI assistant for ${name ?? "this website"}. I answer from what the team has shared here and help you reach them when needed.`;
|
|
22052
|
+
}
|
|
22053
|
+
function contactTeam() {
|
|
22054
|
+
const link = document.querySelector(
|
|
22055
|
+
'a[href*="kontakt" i], a[href*="contact" i], a[href^="mailto:"]'
|
|
22056
|
+
);
|
|
22057
|
+
if (link) link.click();
|
|
22058
|
+
else window.location.hash = "kontakt";
|
|
22059
|
+
}
|
|
22060
|
+
function SettoChat() {
|
|
22061
|
+
const { config } = useSetto();
|
|
22062
|
+
const { i18n } = useTranslation();
|
|
22063
|
+
const [settings, setSettings] = useState(null);
|
|
22064
|
+
const [open, setOpen] = useState(false);
|
|
22065
|
+
const [messages, setMessages] = useState([]);
|
|
22066
|
+
const [draft, setDraft] = useState("");
|
|
22067
|
+
const [sending, setSending] = useState(false);
|
|
22068
|
+
const conversationId = useRef(null);
|
|
22069
|
+
const visitor = useRef(null);
|
|
22070
|
+
const logRef = useRef(null);
|
|
22071
|
+
const formId = useId();
|
|
22072
|
+
useChatStyles();
|
|
22073
|
+
useEffect(() => {
|
|
22074
|
+
if (window.location.pathname.startsWith("/setto")) return;
|
|
22075
|
+
let cancelled = false;
|
|
22076
|
+
fetch(`${config.apiUrl}/sites/${encodeURIComponent(config.siteId)}/chat/settings/public`, {
|
|
22077
|
+
headers: { Accept: "application/json" }
|
|
22078
|
+
}).then(async (response) => response.ok ? await response.json() : null).then((next) => {
|
|
22079
|
+
if (!cancelled && next?.enabled) setSettings(next);
|
|
22080
|
+
}).catch(() => {
|
|
22081
|
+
});
|
|
22082
|
+
return () => {
|
|
22083
|
+
cancelled = true;
|
|
22084
|
+
};
|
|
22085
|
+
}, [config.apiUrl, config.siteId]);
|
|
22086
|
+
useEffect(() => {
|
|
22087
|
+
if (!open || messages.length > 0 || !settings) return;
|
|
22088
|
+
setMessages([{ role: "assistant", content: intro(i18n.language, settings.displayName) }]);
|
|
22089
|
+
}, [open, messages.length, settings, i18n.language]);
|
|
22090
|
+
useEffect(() => {
|
|
22091
|
+
if (open) logRef.current?.scrollTo({ top: logRef.current.scrollHeight });
|
|
22092
|
+
}, [messages, open]);
|
|
22093
|
+
if (!settings || window.location.pathname.startsWith("/setto")) return null;
|
|
22094
|
+
const locale = i18n.language || "no";
|
|
22095
|
+
const labels = norwegian(locale) ? {
|
|
22096
|
+
open: `Snakk med ${settings.displayName}`,
|
|
22097
|
+
close: "Lukk chatten",
|
|
22098
|
+
title: "Spør Barista",
|
|
22099
|
+
disclosure: "AI-assistent · svarer fra nettstedets godkjente informasjon",
|
|
22100
|
+
placeholder: "Skriv spørsmålet ditt …",
|
|
22101
|
+
send: "Send",
|
|
22102
|
+
sending: "Svarer …",
|
|
22103
|
+
contact: "Kontakt teamet",
|
|
22104
|
+
failed: "Jeg får ikke svart akkurat nå. Kontakt gjerne teamet direkte."
|
|
22105
|
+
} : {
|
|
22106
|
+
open: `Chat with ${settings.displayName}`,
|
|
22107
|
+
close: "Close chat",
|
|
22108
|
+
title: "Ask Barista",
|
|
22109
|
+
disclosure: "AI assistant · answers from this website’s approved information",
|
|
22110
|
+
placeholder: "Write your question …",
|
|
22111
|
+
send: "Send",
|
|
22112
|
+
sending: "Replying …",
|
|
22113
|
+
contact: "Contact the team",
|
|
22114
|
+
failed: "I cannot reply right now. Please contact the team directly."
|
|
22115
|
+
};
|
|
22116
|
+
const submit = async (event) => {
|
|
22117
|
+
event.preventDefault();
|
|
22118
|
+
const message = draft.trim();
|
|
22119
|
+
if (!message || sending) return;
|
|
22120
|
+
setDraft("");
|
|
22121
|
+
setSending(true);
|
|
22122
|
+
setMessages((previous) => [...previous, { role: "visitor", content: message }, { role: "assistant", content: "" }]);
|
|
22123
|
+
visitor.current ??= visitorId();
|
|
22124
|
+
try {
|
|
22125
|
+
const response = await fetch(
|
|
22126
|
+
`${config.apiUrl}/sites/${encodeURIComponent(config.siteId)}/chat/messages`,
|
|
22127
|
+
{
|
|
22128
|
+
method: "POST",
|
|
22129
|
+
headers: { "Content-Type": "application/json", Accept: "text/plain, application/json" },
|
|
22130
|
+
body: JSON.stringify({
|
|
22131
|
+
conversationId: conversationId.current ?? void 0,
|
|
22132
|
+
visitorId: visitor.current,
|
|
22133
|
+
locale,
|
|
22134
|
+
page: window.location.href,
|
|
22135
|
+
message
|
|
22136
|
+
})
|
|
22137
|
+
}
|
|
22138
|
+
);
|
|
22139
|
+
const headerConversation = response.headers.get("X-Setto-Conversation");
|
|
22140
|
+
if (headerConversation) conversationId.current = headerConversation;
|
|
22141
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
22142
|
+
if (!response.ok) throw new Error(`chat_failed_${response.status}`);
|
|
22143
|
+
if (contentType.includes("application/json")) {
|
|
22144
|
+
const body = await response.json();
|
|
22145
|
+
if (body.conversationId) conversationId.current = body.conversationId;
|
|
22146
|
+
setMessages((previous) => replaceLastAssistant(previous, body.answer || labels.failed));
|
|
22147
|
+
return;
|
|
22148
|
+
}
|
|
22149
|
+
const reader = response.body?.getReader();
|
|
22150
|
+
if (!reader) throw new Error("chat_missing_stream");
|
|
22151
|
+
const decoder = new TextDecoder();
|
|
22152
|
+
let answer = "";
|
|
22153
|
+
while (true) {
|
|
22154
|
+
const { done, value } = await reader.read();
|
|
22155
|
+
if (done) break;
|
|
22156
|
+
answer += decoder.decode(value, { stream: true });
|
|
22157
|
+
setMessages((previous) => replaceLastAssistant(previous, answer));
|
|
22158
|
+
}
|
|
22159
|
+
answer += decoder.decode();
|
|
22160
|
+
if (!answer.trim()) setMessages((previous) => replaceLastAssistant(previous, labels.failed));
|
|
22161
|
+
} catch {
|
|
22162
|
+
setMessages((previous) => replaceLastAssistant(previous, labels.failed));
|
|
22163
|
+
} finally {
|
|
22164
|
+
setSending(false);
|
|
22165
|
+
}
|
|
22166
|
+
};
|
|
22167
|
+
return /* @__PURE__ */ jsxs("aside", { className: "setto-chat", "aria-label": labels.title, "data-setto-ui": true, children: [
|
|
22168
|
+
open ? /* @__PURE__ */ jsxs("section", { className: "setto-chat__panel", role: "dialog", "aria-modal": "false", "aria-labelledby": `${formId}-title`, children: [
|
|
22169
|
+
/* @__PURE__ */ jsxs("header", { className: "setto-chat__header", children: [
|
|
22170
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
22171
|
+
/* @__PURE__ */ jsx("p", { className: "setto-chat__eyebrow", children: "BARISTA" }),
|
|
22172
|
+
/* @__PURE__ */ jsx("h2", { id: `${formId}-title`, children: labels.title }),
|
|
22173
|
+
/* @__PURE__ */ jsx("p", { children: labels.disclosure })
|
|
22174
|
+
] }),
|
|
22175
|
+
/* @__PURE__ */ jsx("button", { className: "setto-chat__close", type: "button", onClick: () => setOpen(false), "aria-label": labels.close, children: "×" })
|
|
22176
|
+
] }),
|
|
22177
|
+
/* @__PURE__ */ jsx("div", { className: "setto-chat__log", ref: logRef, "aria-live": "polite", children: messages.map((item, index) => /* @__PURE__ */ jsx("div", { className: `setto-chat__message setto-chat__message--${item.role}`, children: item.content || /* @__PURE__ */ jsx("span", { className: "setto-chat__typing", children: "···" }) }, `${item.role}-${index}`)) }),
|
|
22178
|
+
/* @__PURE__ */ jsx("div", { className: "setto-chat__contact", children: /* @__PURE__ */ jsxs("button", { type: "button", onClick: contactTeam, children: [
|
|
22179
|
+
labels.contact,
|
|
22180
|
+
" ",
|
|
22181
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "↗" })
|
|
22182
|
+
] }) }),
|
|
22183
|
+
/* @__PURE__ */ jsxs("form", { className: "setto-chat__composer", onSubmit: submit, children: [
|
|
22184
|
+
/* @__PURE__ */ jsx("label", { className: "setto-chat__sr", htmlFor: `${formId}-input`, children: labels.placeholder }),
|
|
22185
|
+
/* @__PURE__ */ jsx(
|
|
22186
|
+
"textarea",
|
|
22187
|
+
{
|
|
22188
|
+
id: `${formId}-input`,
|
|
22189
|
+
value: draft,
|
|
22190
|
+
rows: 1,
|
|
22191
|
+
maxLength: 1500,
|
|
22192
|
+
onChange: (event) => setDraft(event.target.value),
|
|
22193
|
+
placeholder: labels.placeholder,
|
|
22194
|
+
disabled: sending,
|
|
22195
|
+
onKeyDown: (event) => {
|
|
22196
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
22197
|
+
event.preventDefault();
|
|
22198
|
+
event.currentTarget.form?.requestSubmit();
|
|
22199
|
+
}
|
|
22200
|
+
}
|
|
22201
|
+
}
|
|
22202
|
+
),
|
|
22203
|
+
/* @__PURE__ */ jsx("button", { type: "submit", disabled: !draft.trim() || sending, children: sending ? labels.sending : labels.send })
|
|
22204
|
+
] })
|
|
22205
|
+
] }) : null,
|
|
22206
|
+
/* @__PURE__ */ jsxs("button", { className: "setto-chat__launcher", type: "button", onClick: () => setOpen(true), "aria-expanded": open, children: [
|
|
22207
|
+
/* @__PURE__ */ jsx("span", { className: "setto-chat__launcher-mark", "aria-hidden": "true", children: "✦" }),
|
|
22208
|
+
/* @__PURE__ */ jsx("span", { children: labels.open })
|
|
22209
|
+
] })
|
|
22210
|
+
] });
|
|
22211
|
+
}
|
|
22212
|
+
function replaceLastAssistant(messages, content) {
|
|
22213
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
22214
|
+
if (messages[index]?.role === "assistant") {
|
|
22215
|
+
const next = [...messages];
|
|
22216
|
+
next[index] = { role: "assistant", content };
|
|
22217
|
+
return next;
|
|
22218
|
+
}
|
|
22219
|
+
}
|
|
22220
|
+
return [...messages, { role: "assistant", content }];
|
|
22221
|
+
}
|
|
22222
|
+
function useChatStyles() {
|
|
22223
|
+
useInsertionEffect(() => {
|
|
22224
|
+
const style = document.createElement("style");
|
|
22225
|
+
style.dataset.settoChat = "";
|
|
22226
|
+
style.textContent = `
|
|
22227
|
+
.setto-chat { position: fixed; right: max(16px, env(safe-area-inset-right)); bottom: max(16px, env(safe-area-inset-bottom)); z-index: 2147483000; color: #231d17; font-family: Archivo, ui-sans-serif, sans-serif; }
|
|
22228
|
+
.setto-chat *, .setto-chat *::before, .setto-chat *::after { box-sizing: border-box; }
|
|
22229
|
+
.setto-chat__launcher { border: 0; min-height: 54px; max-width: min(290px, calc(100vw - 32px)); border-radius: 999px; padding: 8px 18px 8px 9px; display: flex; align-items: center; gap: 10px; background: #251d16; color: #fff9ef; box-shadow: 0 14px 40px rgba(35,29,23,.28); font: 600 14px/1 Archivo, ui-sans-serif, sans-serif; cursor: pointer; transition: transform .18s ease, box-shadow .18s ease; }
|
|
22230
|
+
.setto-chat__launcher:hover { transform: translateY(-2px); box-shadow: 0 18px 46px rgba(35,29,23,.36); }
|
|
22231
|
+
.setto-chat__launcher-mark { width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; background: #df6540; color: #fff8eb; font-size: 18px; }
|
|
22232
|
+
.setto-chat__panel { width: min(390px, calc(100vw - 32px)); height: min(590px, calc(100dvh - 104px)); margin-bottom: 12px; overflow: hidden; display: grid; grid-template-rows: auto 1fr auto auto; border: 1px solid rgba(37,29,22,.15); border-radius: 20px; background: #fffaf1; box-shadow: 0 22px 70px rgba(35,29,23,.3); }
|
|
22233
|
+
.setto-chat__header { padding: 20px 20px 16px; display: flex; justify-content: space-between; gap: 14px; border-bottom: 1px solid #eadfce; background: radial-gradient(circle at 0 0, #f4d4a9, transparent 52%), #fffaf1; }
|
|
22234
|
+
.setto-chat__eyebrow { margin: 0 0 4px; color: #b54628; font-size: 10px; font-weight: 700; letter-spacing: .14em; }
|
|
22235
|
+
.setto-chat__header h2 { margin: 0; color: #211a14; font-family: "Instrument Serif", Georgia, serif; font-size: 29px; font-weight: 400; line-height: 1; }
|
|
22236
|
+
.setto-chat__header p:not(.setto-chat__eyebrow) { margin: 7px 0 0; max-width: 280px; color: #70665a; font-size: 12px; line-height: 1.35; }
|
|
22237
|
+
.setto-chat__close { width: 32px; height: 32px; border: 0; border-radius: 50%; background: rgba(64,47,31,.07); color: #4f453a; font: 300 28px/27px Georgia, serif; cursor: pointer; }
|
|
22238
|
+
.setto-chat__log { min-height: 0; overflow: auto; padding: 18px; display: flex; flex-direction: column; gap: 10px; scroll-behavior: smooth; }
|
|
22239
|
+
.setto-chat__message { max-width: 88%; padding: 11px 13px; border-radius: 14px; font-size: 14px; line-height: 1.48; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
22240
|
+
.setto-chat__message--assistant { align-self: flex-start; background: #efe7da; color: #302820; border-bottom-left-radius: 3px; }
|
|
22241
|
+
.setto-chat__message--visitor { align-self: flex-end; background: #2b221b; color: #fffaf1; border-bottom-right-radius: 3px; }
|
|
22242
|
+
.setto-chat__typing { display: inline-block; min-width: 24px; animation: setto-chat-pulse 1.1s ease-in-out infinite; letter-spacing: 2px; }
|
|
22243
|
+
@keyframes setto-chat-pulse { 50% { opacity: .3; } }
|
|
22244
|
+
.setto-chat__contact { padding: 0 18px 12px; }
|
|
22245
|
+
.setto-chat__contact button { border: 0; padding: 0; background: transparent; color: #b54628; font: 600 12px/1.4 Archivo, ui-sans-serif, sans-serif; cursor: pointer; }
|
|
22246
|
+
.setto-chat__composer { padding: 12px; display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 8px; border-top: 1px solid #eadfce; background: #fffdf8; }
|
|
22247
|
+
.setto-chat__composer textarea { width: 100%; max-height: 100px; resize: none; border: 1px solid #d8ccba; border-radius: 10px; background: #fffaf1; color: #231d17; padding: 10px; font: 14px/1.35 Archivo, ui-sans-serif, sans-serif; outline: none; }
|
|
22248
|
+
.setto-chat__composer textarea:focus { border-color: #b54628; box-shadow: 0 0 0 3px rgba(181,70,40,.12); }
|
|
22249
|
+
.setto-chat__composer button { min-height: 40px; border: 0; border-radius: 9px; padding: 0 13px; background: #b54628; color: #fffaf1; font: 600 13px Archivo, ui-sans-serif, sans-serif; cursor: pointer; }
|
|
22250
|
+
.setto-chat__composer button:disabled { opacity: .5; cursor: default; }
|
|
22251
|
+
.setto-chat__sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
22252
|
+
@media (max-width: 480px) { .setto-chat { right: 12px; bottom: max(12px, env(safe-area-inset-bottom)); } .setto-chat__panel { width: calc(100vw - 24px); height: min(610px, calc(100dvh - 88px)); } .setto-chat__launcher { min-height: 50px; } }
|
|
22253
|
+
@media (prefers-reduced-motion: reduce) { .setto-chat *, .setto-chat *::before, .setto-chat *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } }
|
|
22254
|
+
`;
|
|
22255
|
+
document.head.appendChild(style);
|
|
22256
|
+
return () => style.remove();
|
|
22257
|
+
}, []);
|
|
22258
|
+
}
|
|
22259
|
+
const chatModule = {
|
|
22260
|
+
id: "chat",
|
|
22261
|
+
label: "Chat"
|
|
22262
|
+
};
|
|
22035
22263
|
const SettoContext = createContext(null);
|
|
22036
22264
|
function SettoProvider({ config, children }) {
|
|
22037
22265
|
const supabase = useMemo(() => getSupabase(config.supabase), [config.supabase]);
|
|
@@ -22141,6 +22369,7 @@ function SettoProvider({ config, children }) {
|
|
|
22141
22369
|
return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
22142
22370
|
/* @__PURE__ */ jsx(EditModeShell, { active: editShellActive, themeStore: value.themeStore, children }),
|
|
22143
22371
|
/* @__PURE__ */ jsx(SettoChrome, {}),
|
|
22372
|
+
/* @__PURE__ */ jsx(SettoChat, {}),
|
|
22144
22373
|
/* @__PURE__ */ jsx(AutoLanguageSwitcher, { languages })
|
|
22145
22374
|
] }) });
|
|
22146
22375
|
}
|
|
@@ -23909,7 +24138,7 @@ const domainModule = {
|
|
|
23909
24138
|
id: "domain",
|
|
23910
24139
|
label: "Domene"
|
|
23911
24140
|
};
|
|
23912
|
-
const SETTO_MODULES = [formsModule, domainModule];
|
|
24141
|
+
const SETTO_MODULES = [formsModule, chatModule, domainModule];
|
|
23913
24142
|
function SettoVideo({ srcKey, posterKey, title, className }) {
|
|
23914
24143
|
const { t, i18n } = useTranslation();
|
|
23915
24144
|
const { store } = useSetto();
|
|
@@ -24949,6 +25178,7 @@ export {
|
|
|
24949
25178
|
SettoAnimation,
|
|
24950
25179
|
SettoBlock,
|
|
24951
25180
|
SettoBlockTree,
|
|
25181
|
+
SettoChat,
|
|
24952
25182
|
SettoChrome,
|
|
24953
25183
|
SettoContext,
|
|
24954
25184
|
SettoForm,
|