@karibukit/ranger-widget 0.1.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.
@@ -0,0 +1,14 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface RangerChatProps {
4
+ apiUrl: string;
5
+ propertySlug: string;
6
+ embedToken: string;
7
+ mode: 'embedded' | 'floating';
8
+ riveUrl?: string;
9
+ className?: string;
10
+ }
11
+
12
+ declare function RangerChat(props: RangerChatProps): react_jsx_runtime.JSX.Element;
13
+
14
+ export { RangerChat, type RangerChatProps };
package/dist/index.js ADDED
@@ -0,0 +1,506 @@
1
+ // src/EmbeddedChat.tsx
2
+ import { useEffect as useEffect4, useState as useState5, useCallback as useCallback3 } from "react";
3
+
4
+ // src/hooks/useRangerSession.ts
5
+ import { useState, useCallback, useEffect } from "react";
6
+ var STORAGE_PREFIX = "ranger_session_";
7
+ function useRangerSession(apiUrl, propertySlug, embedToken) {
8
+ const storageKey = `${STORAGE_PREFIX}${propertySlug}`;
9
+ const [state, setState] = useState({
10
+ token: null,
11
+ sessionId: null,
12
+ config: null,
13
+ isCreating: false,
14
+ error: null
15
+ });
16
+ useEffect(() => {
17
+ try {
18
+ const stored = localStorage.getItem(storageKey);
19
+ if (stored) {
20
+ const parsed = JSON.parse(stored);
21
+ const payload = JSON.parse(atob(parsed.token.split(".")[1]));
22
+ if (payload.exp * 1e3 > Date.now()) {
23
+ setState((s) => ({ ...s, token: parsed.token, sessionId: parsed.sessionId, config: parsed.config }));
24
+ return;
25
+ }
26
+ }
27
+ } catch {
28
+ }
29
+ localStorage.removeItem(storageKey);
30
+ }, [storageKey]);
31
+ const createSession = useCallback(async () => {
32
+ if (state.isCreating) return;
33
+ setState((s) => ({ ...s, isCreating: true, error: null }));
34
+ try {
35
+ const res = await fetch(`${apiUrl}/api/ranger/web/session`, {
36
+ method: "POST",
37
+ headers: {
38
+ "Content-Type": "application/json",
39
+ "X-Widget-Token": embedToken
40
+ },
41
+ body: JSON.stringify({ propertySlug })
42
+ });
43
+ if (!res.ok) {
44
+ const err = await res.json();
45
+ throw new Error(err.error?.message || "Session creation failed");
46
+ }
47
+ const data = await res.json();
48
+ const newState = { token: data.token, sessionId: data.sessionId, config: data.config, isCreating: false, error: null };
49
+ setState(newState);
50
+ localStorage.setItem(storageKey, JSON.stringify({ token: data.token, sessionId: data.sessionId, config: data.config }));
51
+ } catch (err) {
52
+ setState((s) => ({ ...s, isCreating: false, error: err instanceof Error ? err.message : "Session creation failed" }));
53
+ }
54
+ }, [apiUrl, propertySlug, embedToken, storageKey, state.isCreating]);
55
+ const clearSession = useCallback(() => {
56
+ localStorage.removeItem(storageKey);
57
+ setState({ token: null, sessionId: null, config: null, isCreating: false, error: null });
58
+ }, [storageKey]);
59
+ return { ...state, createSession, clearSession };
60
+ }
61
+
62
+ // src/ChatPanel.tsx
63
+ import { useRef, useEffect as useEffect2, useState as useState3 } from "react";
64
+
65
+ // src/hooks/useChat.ts
66
+ import { useState as useState2, useCallback as useCallback2 } from "react";
67
+ var messageCounter = 0;
68
+ function useChat(apiUrl, token) {
69
+ const [messages, setMessages] = useState2([]);
70
+ const [isLoading, setIsLoading] = useState2(false);
71
+ const [chatState, setChatState] = useState2("idle");
72
+ const sendMessage = useCallback2(async (text, retryMessageId) => {
73
+ if (!token || isLoading) return;
74
+ const clientMessageId = retryMessageId || `msg_${Date.now()}_${++messageCounter}`;
75
+ if (!retryMessageId) {
76
+ const visitorMsg = { id: clientMessageId, role: "visitor", content: text };
77
+ setMessages((prev) => [...prev, visitorMsg]);
78
+ }
79
+ setIsLoading(true);
80
+ setChatState("thinking");
81
+ try {
82
+ const res = await fetch(`${apiUrl}/api/ranger/web/chat`, {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ "Authorization": `Bearer ${token}`
87
+ },
88
+ body: JSON.stringify({ message: text, clientMessageId })
89
+ });
90
+ if (!res.ok) {
91
+ const err = await res.json();
92
+ throw new Error(err.error?.message || "Chat failed");
93
+ }
94
+ const contentType = res.headers.get("content-type") || "";
95
+ if (contentType.includes("application/json")) {
96
+ const data = await res.json();
97
+ if (res.status === 202 || data.error?.code === "TURN_IN_PROGRESS") {
98
+ setIsLoading(false);
99
+ setChatState("idle");
100
+ setTimeout(() => sendMessage(text, clientMessageId), 2e3);
101
+ return;
102
+ }
103
+ setChatState("responding");
104
+ const assistantMsg = {
105
+ id: `resp_${clientMessageId}`,
106
+ role: "assistant",
107
+ content: data.content,
108
+ suggestions: data.suggestions
109
+ };
110
+ setMessages((prev) => [...prev, assistantMsg]);
111
+ setChatState("idle");
112
+ setIsLoading(false);
113
+ return;
114
+ }
115
+ const reader = res.body?.getReader();
116
+ const decoder = new TextDecoder();
117
+ if (!reader) throw new Error("No response body");
118
+ let buffer = "";
119
+ while (true) {
120
+ const { done, value } = await reader.read();
121
+ if (done) break;
122
+ buffer += decoder.decode(value, { stream: true });
123
+ const lines = buffer.split("\n");
124
+ buffer = lines.pop() || "";
125
+ for (const line of lines) {
126
+ if (line.startsWith("data: ")) {
127
+ try {
128
+ const data = JSON.parse(line.slice(6));
129
+ if (data.content !== void 0) {
130
+ setChatState("responding");
131
+ const assistantMsg = {
132
+ id: `resp_${clientMessageId}`,
133
+ role: "assistant",
134
+ content: data.content,
135
+ suggestions: data.suggestions
136
+ };
137
+ setMessages((prev) => [...prev, assistantMsg]);
138
+ }
139
+ } catch {
140
+ }
141
+ }
142
+ }
143
+ }
144
+ } catch {
145
+ const errorMsg = {
146
+ id: `err_${clientMessageId}`,
147
+ role: "assistant",
148
+ content: "Something went wrong. Please try again."
149
+ };
150
+ setMessages((prev) => [...prev, errorMsg]);
151
+ } finally {
152
+ setChatState("idle");
153
+ setIsLoading(false);
154
+ }
155
+ }, [apiUrl, token, isLoading]);
156
+ const clearMessages = useCallback2(() => setMessages([]), []);
157
+ return { messages, isLoading, chatState, sendMessage, clearMessages };
158
+ }
159
+
160
+ // src/MessageBubble.tsx
161
+ import { jsx, jsxs } from "react/jsx-runtime";
162
+ function MessageBubble({ message, onSuggestionClick, isLast }) {
163
+ return /* @__PURE__ */ jsxs("div", { className: `ranger-message ranger-message--${message.role}`, children: [
164
+ /* @__PURE__ */ jsx("div", { className: "ranger-bubble", children: message.content }),
165
+ isLast && message.role === "assistant" && message.suggestions && message.suggestions.length > 0 && /* @__PURE__ */ jsx("div", { className: "ranger-chips", children: message.suggestions.map((s) => /* @__PURE__ */ jsx(
166
+ "button",
167
+ {
168
+ className: "ranger-chip",
169
+ onClick: () => onSuggestionClick?.(s),
170
+ type: "button",
171
+ children: s
172
+ },
173
+ s
174
+ )) })
175
+ ] });
176
+ }
177
+
178
+ // src/SuggestionChips.tsx
179
+ import { jsx as jsx2 } from "react/jsx-runtime";
180
+ function SuggestionChips({ suggestions, onSelect }) {
181
+ if (!suggestions.length) return null;
182
+ return /* @__PURE__ */ jsx2("div", { className: "ranger-chips", children: suggestions.map((s) => /* @__PURE__ */ jsx2(
183
+ "button",
184
+ {
185
+ className: "ranger-chip",
186
+ onClick: () => onSelect(s),
187
+ type: "button",
188
+ children: s
189
+ },
190
+ s
191
+ )) });
192
+ }
193
+
194
+ // src/ChatPanel.tsx
195
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
196
+ function ChatPanel({ apiUrl, token, config, onNewChat, onChatStateChange, className }) {
197
+ const { messages, isLoading, chatState, sendMessage, clearMessages } = useChat(apiUrl, token);
198
+ const [input, setInput] = useState3("");
199
+ const messagesEndRef = useRef(null);
200
+ const textareaRef = useRef(null);
201
+ useEffect2(() => {
202
+ onChatStateChange?.(chatState);
203
+ }, [chatState, onChatStateChange]);
204
+ useEffect2(() => {
205
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
206
+ }, [messages, isLoading]);
207
+ useEffect2(() => {
208
+ const el = textareaRef.current;
209
+ if (!el) return;
210
+ el.style.height = "auto";
211
+ el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
212
+ }, [input]);
213
+ const handleSend = () => {
214
+ const text = input.trim();
215
+ if (!text || isLoading || !token) return;
216
+ setInput("");
217
+ sendMessage(text);
218
+ };
219
+ const handleKeyDown = (e) => {
220
+ if (e.key === "Enter" && !e.shiftKey) {
221
+ e.preventDefault();
222
+ handleSend();
223
+ }
224
+ };
225
+ const handleSuggestion = (text) => {
226
+ if (isLoading || !token) return;
227
+ sendMessage(text);
228
+ };
229
+ const handleNewChat = () => {
230
+ clearMessages();
231
+ onNewChat?.();
232
+ };
233
+ const showWelcome = messages.length === 0 && !isLoading;
234
+ return /* @__PURE__ */ jsxs2("div", { className: `ranger-widget ranger-panel${className ? ` ${className}` : ""}`, children: [
235
+ /* @__PURE__ */ jsxs2("div", { className: "ranger-header", children: [
236
+ /* @__PURE__ */ jsx3("span", { className: "ranger-header-name", children: config.assistantName }),
237
+ messages.length > 0 && /* @__PURE__ */ jsx3(
238
+ "button",
239
+ {
240
+ className: "ranger-new-chat-btn",
241
+ onClick: handleNewChat,
242
+ type: "button",
243
+ style: { marginLeft: "auto", background: "none", border: "none", color: "rgba(255,255,255,0.8)", cursor: "pointer", fontSize: 12 },
244
+ children: "New chat"
245
+ }
246
+ )
247
+ ] }),
248
+ /* @__PURE__ */ jsxs2("div", { className: "ranger-messages", children: [
249
+ showWelcome && /* @__PURE__ */ jsx3("div", { className: "ranger-message ranger-message--assistant", children: /* @__PURE__ */ jsx3("div", { className: "ranger-bubble", children: config.welcomeMessage }) }),
250
+ showWelcome && config.welcomeSuggestions.length > 0 && /* @__PURE__ */ jsx3(SuggestionChips, { suggestions: config.welcomeSuggestions, onSelect: handleSuggestion }),
251
+ messages.map((msg, i) => /* @__PURE__ */ jsx3(
252
+ MessageBubble,
253
+ {
254
+ message: msg,
255
+ onSuggestionClick: handleSuggestion,
256
+ isLast: i === messages.length - 1
257
+ },
258
+ msg.id
259
+ )),
260
+ isLoading && chatState === "thinking" && /* @__PURE__ */ jsxs2("div", { className: "ranger-thinking", children: [
261
+ /* @__PURE__ */ jsx3("span", {}),
262
+ /* @__PURE__ */ jsx3("span", {}),
263
+ /* @__PURE__ */ jsx3("span", {})
264
+ ] }),
265
+ /* @__PURE__ */ jsx3("div", { ref: messagesEndRef })
266
+ ] }),
267
+ /* @__PURE__ */ jsxs2("div", { className: "ranger-input-area", children: [
268
+ /* @__PURE__ */ jsx3(
269
+ "textarea",
270
+ {
271
+ ref: textareaRef,
272
+ className: "ranger-input",
273
+ value: input,
274
+ onChange: (e) => setInput(e.target.value),
275
+ onKeyDown: handleKeyDown,
276
+ placeholder: token ? "Type a message\u2026" : "Starting session\u2026",
277
+ disabled: !token || isLoading,
278
+ rows: 1
279
+ }
280
+ ),
281
+ /* @__PURE__ */ jsx3(
282
+ "button",
283
+ {
284
+ className: "ranger-send-btn",
285
+ onClick: handleSend,
286
+ disabled: !input.trim() || !token || isLoading,
287
+ type: "button",
288
+ "aria-label": "Send",
289
+ children: /* @__PURE__ */ jsx3("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx3("path", { d: "M14 8L2 2l3 6-3 6 12-6z", fill: "currentColor" }) })
290
+ }
291
+ )
292
+ ] })
293
+ ] });
294
+ }
295
+
296
+ // src/RangerCharacter.tsx
297
+ import { useState as useState4, useEffect as useEffect3, useRef as useRef2 } from "react";
298
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
299
+ function RangerCharacter({ state, size = 96, className, riveUrl }) {
300
+ const [riveLoaded, setRiveLoaded] = useState4(false);
301
+ const [riveFailed, setRiveFailed] = useState4(false);
302
+ const canvasRef = useRef2(null);
303
+ useEffect3(() => {
304
+ if (!riveUrl) {
305
+ setRiveFailed(true);
306
+ return;
307
+ }
308
+ let cancelled = false;
309
+ import("@rive-app/react-canvas").then(({ Rive }) => {
310
+ if (cancelled || !canvasRef.current) return;
311
+ const canvas = document.createElement("canvas");
312
+ canvas.width = size;
313
+ canvas.height = size;
314
+ canvasRef.current.appendChild(canvas);
315
+ const r = new Rive({
316
+ src: riveUrl,
317
+ canvas,
318
+ stateMachines: "main",
319
+ autoplay: true,
320
+ onLoad: () => {
321
+ if (!cancelled) setRiveLoaded(true);
322
+ },
323
+ onLoadError: () => {
324
+ if (!cancelled) setRiveFailed(true);
325
+ }
326
+ });
327
+ const inputs = r.stateMachineInputs("main") || [];
328
+ const stateInput = inputs.find((i) => i.name === "state");
329
+ const stateMap = { idle: 0, listening: 1, thinking: 2, responding: 3 };
330
+ if (stateInput) stateInput.value = stateMap[state];
331
+ return () => {
332
+ cancelled = true;
333
+ r.cleanup();
334
+ };
335
+ }).catch(() => {
336
+ if (!cancelled) setRiveFailed(true);
337
+ });
338
+ return () => {
339
+ cancelled = true;
340
+ };
341
+ }, [riveUrl, size]);
342
+ const showFallback = riveFailed || !riveUrl && !riveLoaded;
343
+ return /* @__PURE__ */ jsxs3(
344
+ "div",
345
+ {
346
+ className,
347
+ style: { width: size, height: size, display: "inline-flex", alignItems: "center", justifyContent: "center" },
348
+ children: [
349
+ !showFallback && /* @__PURE__ */ jsx4("div", { ref: canvasRef, style: { width: size, height: size } }),
350
+ showFallback && /* @__PURE__ */ jsx4(RangerSVG, { state, size })
351
+ ]
352
+ }
353
+ );
354
+ }
355
+ function RangerSVG({ state, size }) {
356
+ const colors = {
357
+ idle: "#a08060",
358
+ listening: "#7ba05b",
359
+ thinking: "#6b7ab8",
360
+ responding: "#c07850"
361
+ };
362
+ const color = colors[state];
363
+ const isAnimating = state === "thinking" || state === "responding";
364
+ return /* @__PURE__ */ jsxs3(
365
+ "svg",
366
+ {
367
+ width: size,
368
+ height: size,
369
+ viewBox: "0 0 96 96",
370
+ fill: "none",
371
+ xmlns: "http://www.w3.org/2000/svg",
372
+ style: isAnimating ? { animation: "ranger-pulse 1.5s ease-in-out infinite" } : void 0,
373
+ children: [
374
+ /* @__PURE__ */ jsx4("style", { children: `@keyframes ranger-pulse { 0%,100%{opacity:1} 50%{opacity:0.7} }` }),
375
+ /* @__PURE__ */ jsx4("ellipse", { cx: "48", cy: "28", rx: "22", ry: "5", fill: color, opacity: "0.9" }),
376
+ /* @__PURE__ */ jsx4("rect", { x: "34", y: "16", width: "28", height: "14", rx: "4", fill: color }),
377
+ /* @__PURE__ */ jsx4("circle", { cx: "48", cy: "44", r: "16", fill: "#f5d6b0" }),
378
+ state === "thinking" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
379
+ /* @__PURE__ */ jsx4("path", { d: "M40 42 Q42 40 44 42", stroke: "#5a3e28", strokeWidth: "1.5", strokeLinecap: "round" }),
380
+ /* @__PURE__ */ jsx4("path", { d: "M52 42 Q54 40 56 42", stroke: "#5a3e28", strokeWidth: "1.5", strokeLinecap: "round" })
381
+ ] }) : state === "responding" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
382
+ /* @__PURE__ */ jsx4("circle", { cx: "42", cy: "42", r: "3", fill: "#5a3e28" }),
383
+ /* @__PURE__ */ jsx4("circle", { cx: "54", cy: "42", r: "3", fill: "#5a3e28" }),
384
+ /* @__PURE__ */ jsx4("circle", { cx: "43", cy: "41", r: "1", fill: "#fff" }),
385
+ /* @__PURE__ */ jsx4("circle", { cx: "55", cy: "41", r: "1", fill: "#fff" })
386
+ ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
387
+ /* @__PURE__ */ jsx4("ellipse", { cx: "42", cy: "43", rx: "3", ry: "2.5", fill: "#5a3e28" }),
388
+ /* @__PURE__ */ jsx4("ellipse", { cx: "54", cy: "43", rx: "3", ry: "2.5", fill: "#5a3e28" })
389
+ ] }),
390
+ state === "idle" ? /* @__PURE__ */ jsx4("path", { d: "M43 50 Q48 54 53 50", stroke: "#c4855a", strokeWidth: "1.5", strokeLinecap: "round" }) : state === "listening" ? /* @__PURE__ */ jsx4("ellipse", { cx: "48", cy: "51", rx: "4", ry: "2.5", fill: "#c4855a" }) : state === "thinking" ? /* @__PURE__ */ jsx4("path", { d: "M44 51 Q48 49 52 51", stroke: "#c4855a", strokeWidth: "1.5", strokeLinecap: "round" }) : /* @__PURE__ */ jsx4("path", { d: "M43 49 Q48 55 53 49", stroke: "#c4855a", strokeWidth: "2", strokeLinecap: "round" }),
391
+ /* @__PURE__ */ jsx4("path", { d: "M30 64 Q30 58 48 58 Q66 58 66 64 L70 82 H26 Z", fill: color }),
392
+ /* @__PURE__ */ jsx4("rect", { x: "43", y: "62", width: "10", height: "8", rx: "2", fill: "#fff", opacity: "0.8" }),
393
+ /* @__PURE__ */ jsx4("circle", { cx: "48", cy: "66", r: "2", fill: color })
394
+ ]
395
+ }
396
+ );
397
+ }
398
+
399
+ // src/EmbeddedChat.tsx
400
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
401
+ function EmbeddedChat({ apiUrl, propertySlug, embedToken, className, riveUrl }) {
402
+ const session = useRangerSession(apiUrl, propertySlug, embedToken);
403
+ const [charState, setCharState] = useState5("idle");
404
+ useEffect4(() => {
405
+ if (!session.token && !session.isCreating) {
406
+ session.createSession();
407
+ }
408
+ }, [session.token, session.isCreating]);
409
+ const handleChatStateChange = useCallback3((s) => {
410
+ setCharState(s);
411
+ }, []);
412
+ if (session.error) {
413
+ return /* @__PURE__ */ jsx5("div", { className: `ranger-widget ranger-embedded${className ? ` ${className}` : ""}`, children: /* @__PURE__ */ jsx5("p", { style: { color: "#ef4444", fontSize: 14 }, children: "Unable to load chat. Please try again later." }) });
414
+ }
415
+ if (!session.config) {
416
+ return /* @__PURE__ */ jsx5("div", { className: `ranger-widget ranger-embedded${className ? ` ${className}` : ""}`, children: /* @__PURE__ */ jsx5("div", { style: { padding: 24, color: "#78716c", fontSize: 14 }, children: "Loading\u2026" }) });
417
+ }
418
+ return /* @__PURE__ */ jsxs4("div", { className: `ranger-widget ranger-embedded${className ? ` ${className}` : ""}`, children: [
419
+ /* @__PURE__ */ jsxs4("div", { className: "ranger-embedded-character", children: [
420
+ /* @__PURE__ */ jsx5(RangerCharacter, { state: charState, size: 160, riveUrl }),
421
+ /* @__PURE__ */ jsx5("div", { style: { fontSize: 13, color: "#78716c", textAlign: "center", fontWeight: 500 }, children: session.config.assistantName })
422
+ ] }),
423
+ /* @__PURE__ */ jsx5("div", { className: "ranger-embedded-chat", children: /* @__PURE__ */ jsx5(
424
+ ChatPanel,
425
+ {
426
+ apiUrl,
427
+ token: session.token,
428
+ config: session.config,
429
+ onNewChat: session.clearSession,
430
+ onChatStateChange: handleChatStateChange
431
+ }
432
+ ) })
433
+ ] });
434
+ }
435
+
436
+ // src/FloatingChat.tsx
437
+ import { useState as useState6, useEffect as useEffect5, useCallback as useCallback4 } from "react";
438
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
439
+ function FloatingChat({ apiUrl, propertySlug, embedToken, className, riveUrl }) {
440
+ const [isOpen, setIsOpen] = useState6(false);
441
+ const [charState, setCharState] = useState6("idle");
442
+ const session = useRangerSession(apiUrl, propertySlug, embedToken);
443
+ useEffect5(() => {
444
+ if (isOpen && !session.token && !session.isCreating) {
445
+ session.createSession();
446
+ }
447
+ }, [isOpen, session.token, session.isCreating]);
448
+ const handleNewChat = () => {
449
+ session.clearSession();
450
+ setIsOpen(false);
451
+ };
452
+ const handleChatStateChange = useCallback4((s) => {
453
+ setCharState(s);
454
+ }, []);
455
+ const primaryColor = session.config?.primaryColor;
456
+ return /* @__PURE__ */ jsxs5("div", { className: `ranger-widget${className ? ` ${className}` : ""}`, children: [
457
+ isOpen && session.config && /* @__PURE__ */ jsx6("div", { className: "ranger-float-panel", children: /* @__PURE__ */ jsx6(
458
+ ChatPanel,
459
+ {
460
+ apiUrl,
461
+ token: session.token,
462
+ config: session.config,
463
+ onNewChat: handleNewChat,
464
+ onChatStateChange: handleChatStateChange
465
+ }
466
+ ) }),
467
+ isOpen && !session.config && /* @__PURE__ */ jsx6(
468
+ "div",
469
+ {
470
+ className: "ranger-float-panel",
471
+ style: { display: "flex", alignItems: "center", justifyContent: "center", background: "#fff" },
472
+ children: /* @__PURE__ */ jsx6("div", { style: { color: "#78716c", fontSize: 14 }, children: "Loading\u2026" })
473
+ }
474
+ ),
475
+ /* @__PURE__ */ jsx6(
476
+ "button",
477
+ {
478
+ className: "ranger-float-btn",
479
+ onClick: () => setIsOpen((o) => !o),
480
+ type: "button",
481
+ "aria-label": isOpen ? "Close chat" : "Open chat",
482
+ style: primaryColor ? { "--ranger-primary": primaryColor } : void 0,
483
+ children: isOpen ? /* @__PURE__ */ jsx6("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsx6("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }) : /* @__PURE__ */ jsx6(
484
+ RangerCharacter,
485
+ {
486
+ state: charState,
487
+ size: 36,
488
+ riveUrl
489
+ }
490
+ )
491
+ }
492
+ )
493
+ ] });
494
+ }
495
+
496
+ // src/RangerChat.tsx
497
+ import { jsx as jsx7 } from "react/jsx-runtime";
498
+ function RangerChat(props) {
499
+ if (props.mode === "floating") {
500
+ return /* @__PURE__ */ jsx7(FloatingChat, { ...props, mode: "floating" });
501
+ }
502
+ return /* @__PURE__ */ jsx7(EmbeddedChat, { ...props, mode: "embedded" });
503
+ }
504
+ export {
505
+ RangerChat
506
+ };
@@ -0,0 +1,295 @@
1
+ /* Ranger Widget Styles */
2
+ :root {
3
+ --ranger-primary: #a08060;
4
+ --ranger-primary-hover: #8a6e50;
5
+ --ranger-bg: #ffffff;
6
+ --ranger-surface: #f5f5f4;
7
+ --ranger-border: #e7e5e4;
8
+ --ranger-text: #1c1917;
9
+ --ranger-text-muted: #78716c;
10
+ --ranger-visitor-bubble: #a08060;
11
+ --ranger-visitor-text: #ffffff;
12
+ --ranger-assistant-bubble: #f5f5f4;
13
+ --ranger-assistant-text: #1c1917;
14
+ --ranger-radius: 12px;
15
+ --ranger-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
16
+ }
17
+
18
+ /* Container */
19
+ .ranger-widget {
20
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
21
+ font-size: 14px;
22
+ line-height: 1.5;
23
+ color: var(--ranger-text);
24
+ box-sizing: border-box;
25
+ }
26
+
27
+ .ranger-widget *,
28
+ .ranger-widget *::before,
29
+ .ranger-widget *::after {
30
+ box-sizing: inherit;
31
+ }
32
+
33
+ /* Chat panel */
34
+ .ranger-panel {
35
+ display: flex;
36
+ flex-direction: column;
37
+ background: var(--ranger-bg);
38
+ border-radius: var(--ranger-radius);
39
+ overflow: hidden;
40
+ }
41
+
42
+ /* Header */
43
+ .ranger-header {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 10px;
47
+ padding: 14px 16px;
48
+ background: var(--ranger-primary);
49
+ color: #fff;
50
+ }
51
+
52
+ .ranger-header-name {
53
+ font-weight: 600;
54
+ font-size: 15px;
55
+ }
56
+
57
+ /* Messages */
58
+ .ranger-messages {
59
+ flex: 1;
60
+ overflow-y: auto;
61
+ padding: 16px;
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 10px;
65
+ scroll-behavior: smooth;
66
+ }
67
+
68
+ .ranger-message {
69
+ display: flex;
70
+ flex-direction: column;
71
+ max-width: 80%;
72
+ }
73
+
74
+ .ranger-message--visitor {
75
+ align-self: flex-end;
76
+ align-items: flex-end;
77
+ }
78
+
79
+ .ranger-message--assistant {
80
+ align-self: flex-start;
81
+ align-items: flex-start;
82
+ }
83
+
84
+ .ranger-bubble {
85
+ padding: 10px 14px;
86
+ border-radius: 16px;
87
+ white-space: pre-wrap;
88
+ word-break: break-word;
89
+ }
90
+
91
+ .ranger-message--visitor .ranger-bubble {
92
+ background: var(--ranger-visitor-bubble);
93
+ color: var(--ranger-visitor-text);
94
+ border-bottom-right-radius: 4px;
95
+ }
96
+
97
+ .ranger-message--assistant .ranger-bubble {
98
+ background: var(--ranger-assistant-bubble);
99
+ color: var(--ranger-assistant-text);
100
+ border-bottom-left-radius: 4px;
101
+ }
102
+
103
+ /* Suggestion chips */
104
+ .ranger-chips {
105
+ display: flex;
106
+ flex-wrap: wrap;
107
+ gap: 6px;
108
+ margin-top: 6px;
109
+ }
110
+
111
+ .ranger-chip {
112
+ padding: 6px 12px;
113
+ border-radius: 20px;
114
+ border: 1px solid var(--ranger-primary);
115
+ background: transparent;
116
+ color: var(--ranger-primary);
117
+ font-size: 13px;
118
+ cursor: pointer;
119
+ transition: background 0.15s, color 0.15s;
120
+ white-space: nowrap;
121
+ }
122
+
123
+ .ranger-chip:hover {
124
+ background: var(--ranger-primary);
125
+ color: #fff;
126
+ }
127
+
128
+ /* Input area */
129
+ .ranger-input-area {
130
+ display: flex;
131
+ align-items: flex-end;
132
+ gap: 8px;
133
+ padding: 12px 16px;
134
+ border-top: 1px solid var(--ranger-border);
135
+ background: var(--ranger-bg);
136
+ }
137
+
138
+ .ranger-input {
139
+ flex: 1;
140
+ resize: none;
141
+ border: 1px solid var(--ranger-border);
142
+ border-radius: 20px;
143
+ padding: 9px 14px;
144
+ font-size: 14px;
145
+ font-family: inherit;
146
+ color: var(--ranger-text);
147
+ background: var(--ranger-surface);
148
+ outline: none;
149
+ max-height: 120px;
150
+ min-height: 38px;
151
+ line-height: 1.4;
152
+ transition: border-color 0.15s;
153
+ }
154
+
155
+ .ranger-input:focus {
156
+ border-color: var(--ranger-primary);
157
+ }
158
+
159
+ .ranger-send-btn {
160
+ width: 36px;
161
+ height: 36px;
162
+ border-radius: 50%;
163
+ border: none;
164
+ background: var(--ranger-primary);
165
+ color: #fff;
166
+ cursor: pointer;
167
+ display: flex;
168
+ align-items: center;
169
+ justify-content: center;
170
+ flex-shrink: 0;
171
+ transition: background 0.15s;
172
+ }
173
+
174
+ .ranger-send-btn:hover:not(:disabled) {
175
+ background: var(--ranger-primary-hover);
176
+ }
177
+
178
+ .ranger-send-btn:disabled {
179
+ opacity: 0.5;
180
+ cursor: not-allowed;
181
+ }
182
+
183
+ /* Thinking indicator */
184
+ .ranger-thinking {
185
+ display: flex;
186
+ gap: 4px;
187
+ padding: 10px 14px;
188
+ background: var(--ranger-assistant-bubble);
189
+ border-radius: 16px;
190
+ border-bottom-left-radius: 4px;
191
+ width: fit-content;
192
+ }
193
+
194
+ .ranger-thinking span {
195
+ width: 6px;
196
+ height: 6px;
197
+ border-radius: 50%;
198
+ background: var(--ranger-text-muted);
199
+ animation: ranger-bounce 1.2s ease-in-out infinite;
200
+ }
201
+
202
+ .ranger-thinking span:nth-child(2) { animation-delay: 0.2s; }
203
+ .ranger-thinking span:nth-child(3) { animation-delay: 0.4s; }
204
+
205
+ @keyframes ranger-bounce {
206
+ 0%, 60%, 100% { transform: translateY(0); }
207
+ 30% { transform: translateY(-6px); }
208
+ }
209
+
210
+ /* Floating button */
211
+ .ranger-float-btn {
212
+ position: fixed;
213
+ bottom: 24px;
214
+ right: 24px;
215
+ width: 56px;
216
+ height: 56px;
217
+ border-radius: 50%;
218
+ background: var(--ranger-primary);
219
+ color: #fff;
220
+ border: none;
221
+ cursor: pointer;
222
+ display: flex;
223
+ align-items: center;
224
+ justify-content: center;
225
+ box-shadow: var(--ranger-shadow);
226
+ transition: transform 0.2s, background 0.15s;
227
+ z-index: 9999;
228
+ }
229
+
230
+ .ranger-float-btn:hover {
231
+ transform: scale(1.08);
232
+ background: var(--ranger-primary-hover);
233
+ }
234
+
235
+ /* Floating panel */
236
+ .ranger-float-panel {
237
+ position: fixed;
238
+ bottom: 92px;
239
+ right: 24px;
240
+ width: 360px;
241
+ height: 540px;
242
+ box-shadow: var(--ranger-shadow);
243
+ border-radius: var(--ranger-radius);
244
+ overflow: hidden;
245
+ z-index: 9998;
246
+ display: flex;
247
+ flex-direction: column;
248
+ }
249
+
250
+ @media (max-width: 480px) {
251
+ .ranger-float-panel {
252
+ bottom: 0;
253
+ right: 0;
254
+ width: 100vw;
255
+ height: 100dvh;
256
+ border-radius: 0;
257
+ }
258
+
259
+ .ranger-float-btn {
260
+ bottom: 16px;
261
+ right: 16px;
262
+ }
263
+ }
264
+
265
+ /* Embedded layout */
266
+ .ranger-embedded {
267
+ display: grid;
268
+ grid-template-columns: 220px 1fr;
269
+ gap: 24px;
270
+ align-items: stretch;
271
+ }
272
+
273
+ @media (max-width: 640px) {
274
+ .ranger-embedded {
275
+ grid-template-columns: 1fr;
276
+ grid-template-rows: auto 1fr;
277
+ }
278
+ }
279
+
280
+ .ranger-embedded-character {
281
+ display: flex;
282
+ flex-direction: column;
283
+ align-items: center;
284
+ justify-content: center;
285
+ gap: 12px;
286
+ }
287
+
288
+ .ranger-embedded-chat {
289
+ min-height: 480px;
290
+ display: flex;
291
+ flex-direction: column;
292
+ border: 1px solid var(--ranger-border);
293
+ border-radius: var(--ranger-radius);
294
+ overflow: hidden;
295
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@karibukit/ranger-widget",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ },
12
+ "./styles.css": "./dist/styles.css"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup src/index.ts --format esm --dts --external react --external react-dom && cp src/styles.css dist/styles.css",
19
+ "dev": "tsup src/index.ts --format esm --dts --watch --external react --external react-dom"
20
+ },
21
+ "peerDependencies": {
22
+ "react": "^18.0.0",
23
+ "react-dom": "^18.0.0"
24
+ },
25
+ "dependencies": {
26
+ "@rive-app/react-canvas": "^4.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/react": "^18.0.0",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.0.0"
32
+ }
33
+ }