@holaboss/client 0.2.0-beta.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/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @holaboss/client
2
+
3
+ A headless client SDK for talking to a Holaboss AI **employee** from your own web app. Drive it to build **whatever chat UI you want** — you own the interface.
4
+
5
+ **Status: chat (v0).** Client dashboard (scoped tables) and client artifacts (scoped files) land behind the same client as the end-user data API ships.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @holaboss/client
11
+ ```
12
+
13
+ ## Build your own UI (the point)
14
+
15
+ The SDK gives you a client and a hook — the rendering is yours.
16
+
17
+ ```tsx
18
+ import { useMemo } from "react";
19
+ import { createHolaClient, useChat } from "@holaboss/client";
20
+
21
+ function Chat() {
22
+ const client = useMemo(
23
+ () => createHolaClient({ baseUrl: "https://api.holaos.ai", token: "TOKEN" }),
24
+ []
25
+ );
26
+ const { messages, input, setInput, send, sending } = useChat(client);
27
+
28
+ return (
29
+ <YourLayout>
30
+ {messages.map((m) => (
31
+ <YourBubble key={m.id} role={m.role} text={m.text} artifacts={m.artifacts} />
32
+ ))}
33
+ <YourComposer value={input} onChange={setInput} onSend={send} busy={sending} />
34
+ </YourLayout>
35
+ );
36
+ }
37
+ ```
38
+
39
+ Or go fully manual with the client:
40
+
41
+ ```ts
42
+ const client = createHolaClient({ baseUrl, token });
43
+
44
+ await client.streamMessage("hello", {
45
+ onText: (delta) => {/* append to the live bubble */},
46
+ onTool: (step) => {/* show a tool step */},
47
+ onComplete: ({ reply, artifacts }) => {/* final reply + generated files */},
48
+ onError: ({ error }) => {/* surface it */},
49
+ });
50
+ // client.sendMessage(text) is the non-streaming variant.
51
+ ```
52
+
53
+ ## Optional starter UI
54
+
55
+ If you just want something on screen, there's a minimal, unstyled `<Chat/>`:
56
+
57
+ ```tsx
58
+ import { Chat, createHolaClient, HolaProvider } from "@holaboss/client";
59
+ // <HolaProvider client={client}><Chat /></HolaProvider>
60
+ ```
61
+
62
+ It's a convenience, not the product — real apps use `useChat`/`createHolaClient` and render their own components.
63
+
64
+ ## Transport-agnostic
65
+
66
+ The SDK core (`useChat`, `<Chat/>`, the client interface) doesn't care how it reaches the backend. `createHolaClient({ baseUrl, token })` uses the built-in transport; pass your own to target a different backend:
67
+
68
+ ```ts
69
+ createHolaClient({ transport: myTransport }); // implements HolaTransport
70
+ ```
71
+
72
+ - **Today:** the built-in transport authenticates with a **connector token** for one employee.
73
+ - **Next (contract-first):** the same client swaps to the **end-user data API** (email-OTP JWT) — a change inside the transport, invisible to your UI. `<Dashboard/>` (scoped tables) and `<Artifacts/>` (scoped files) then plug into the same client.
74
+
75
+ ## Verified visitors (optional)
76
+
77
+ Turn an anonymous visitor into an identified one (per-user memory) by signing their id on **your** server:
78
+
79
+ ```ts
80
+ createHolaClient({
81
+ baseUrl,
82
+ token,
83
+ identity: { externalUserId: user.id, userHmac }, // hex HMAC-SHA256(user.id, secret), server-side
84
+ });
85
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,8 @@
1
+ 'use client'
2
+ var F=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var Q=Object.getOwnPropertyNames;var X=Object.prototype.hasOwnProperty;var Z=(e,t)=>{for(var n in t)F(e,n,{get:t[n],enumerable:!0})},ee=(e,t,n,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of Q(t))!X.call(e,a)&&a!==n&&F(e,a,{get:()=>t[a],enumerable:!(s=Y(t,a))||s.enumerable});return e};var te=e=>ee(F({},"__esModule",{value:!0}),e);var de={};Z(de,{Chat:()=>B,HolaProvider:()=>J,createApiTransport:()=>j,createHolaClient:()=>G,createPortalData:()=>K,createWidgetTransport:()=>O,mergeMessage:()=>D,textDeltaOf:()=>P,thinkingDeltaOf:()=>I,toolEventOf:()=>R,useArtifactUrl:()=>k,useChat:()=>A,useHolaClient:()=>M,useThread:()=>V});module.exports=te(de);var L=e=>{if(!e||typeof e!="object")return null;let t=e;return t.type==="message_update"?t.assistantMessageEvent??null:null},P=e=>{let t=L(e);return t?.type==="text_delta"&&typeof t.delta=="string"?t.delta:null},I=e=>{let t=L(e);return t?.type==="thinking_delta"&&typeof t.delta=="string"?t.delta:null},R=e=>{if(!e||typeof e!="object")return null;let t=e;return t.type==="tool_execution_start"&&t.toolCallId?{phase:"start",id:t.toolCallId,name:t.toolName}:t.type==="tool_execution_end"&&t.toolCallId?{phase:"end",id:t.toolCallId,isError:t.isError}:null};var re=/^ /,T=e=>{if(e&&typeof e=="object"){let t=e,n=typeof t.error=="string"?t.error:"request_failed";return typeof t.retryable=="boolean"?{error:n,retryable:t.retryable}:{error:n}}return{error:"request_failed"}},oe=e=>{let t="message",n=[];for(let s of e.split(`
3
+ `))s.startsWith(":")||(s.startsWith("event:")?t=s.slice(6).trim():s.startsWith("data:")&&n.push(s.slice(5).replace(re,"")));return n.length>0?{event:t,data:n.join(`
4
+ `)}:null},se=(e,t,n)=>{let s;try{s=JSON.parse(e.data)}catch{return}if(e.event==="session"){let a=s.sessionId;a&&(n(a),t.onSession?.(a));return}if(e.event==="turn_event"){t.onEvent?.(s);let a=P(s);a&&t.onText?.(a);let p=I(s);p&&t.onThinking?.(p);let r=R(s);r&&t.onTool?.(r);return}if(e.event==="chat_completed"){let a=s;n(a.sessionId),t.onComplete?.(a);return}e.event==="chat_error"&&t.onError?.(T(s))},C=async(e,t)=>{let n=await fetch(e,{headers:t});if(!n.ok)throw new Error(`hola: request failed (${n.status})`);return await n.json()},W=async e=>{let t=await fetch(e.url,{method:"POST",headers:e.headers,body:e.body});if(!t.ok)throw new Error(`hola: message failed (${t.status})`);return await t.json()},N=async(e,t,n,s)=>{let a=await fetch(e.url,{method:"POST",headers:e.headers,body:e.body,signal:s});if(!(a.ok&&a.body)){t.onError?.({error:`stream_failed_${a.status}`});return}let p=a.body.getReader(),r=new TextDecoder,o="",d=()=>{let l=o.indexOf(`
5
+
6
+ `);for(;l!==-1;){let f=o.slice(0,l);o=o.slice(l+2);let u=oe(f);u&&se(u,t,n),l=o.indexOf(`
7
+
8
+ `)}},i=async()=>{let l=await p.read();if(!l.done)return o+=r.decode(l.value,{stream:!0}),d(),i()};await i()};var ne=/\/+$/,E="/api/v1/end-user",j=e=>{let t=e.baseUrl.replace(ne,""),n=()=>typeof e.token=="function"?e.token():e.token,s=async()=>({"content-type":"application/json",authorization:`Bearer ${await n()}`}),a=async p=>{let r=await fetch(`${t}${E}/chat`,{method:"POST",headers:await s(),body:JSON.stringify({message:p})});if(!r.ok)throw new Error(`hola: message failed (${r.status})`);return{reply:(await r.json()).reply??"",sessionId:"",artifacts:[]}};return{sessionId:()=>"",getInfo:async()=>C(`${t}${E}/chat/info`,await s()),sendMessage:a,streamMessage:async(p,r)=>{try{let o=await a(p);r.onText?.(o.reply),r.onComplete?.(o)}catch(o){r.onError?.(T(o))}},subscribe:(p,r)=>{let o=r?.since??"0",d=!1,i,l=r?.intervalMs??3e3,f=async()=>{try{let c=await C(`${t}${E}/chat/thread?since=${encodeURIComponent(o)}`,await s());for(let y of c.messages)p.onMessage(y);o=c.cursor}catch(c){p.onError?.(T(c))}d||(i=setTimeout(f,l))},u=()=>{d=!0,i&&clearTimeout(i)};return r?.signal?.addEventListener("abort",u),i=setTimeout(f,0),u},requestHuman:async()=>{await fetch(`${t}${E}/chat/request-human`,{method:"POST",headers:await s(),body:"{}"})}}};var S=require("react"),k=(e,t)=>{let[n,s]=(0,S.useState)("");return(0,S.useEffect)(()=>{if(!e){s("");return}let a=Uint8Array.from(atob(e),r=>r.charCodeAt(0)),p=URL.createObjectURL(new Blob([a],{type:t}));return s(p),()=>URL.revokeObjectURL(p)},[e,t]),n};var U=require("react");var $=require("react"),q=require("react/jsx-runtime"),z=(0,$.createContext)(null),J=({client:e,children:t})=>(0,q.jsx)(z.Provider,{value:e,children:t}),M=()=>{let e=(0,$.useContext)(z);if(!e)throw new Error("useHolaClient must be used within <HolaProvider>");return e};var b=require("react"),A=e=>{let[t,n]=(0,b.useState)([]),[s,a]=(0,b.useState)(""),[p,r]=(0,b.useState)(!1),[o,d]=(0,b.useState)(null),i=(0,b.useRef)(0),l=(0,b.useCallback)(()=>{let u=i.current;return i.current+=1,u},[]);(0,b.useEffect)(()=>{let u=!0;return e.getInfo().then(c=>{u&&c.greeting&&n([{id:l(),role:"assistant",text:c.greeting}])}).catch(()=>{}),()=>{u=!1}},[e,l]);let f=(0,b.useCallback)(()=>{let u=s.trim();if(!u||p)return;a(""),d(null),r(!0);let c=l(),y=l();n(m=>[...m,{id:c,role:"user",text:u},{id:y,role:"assistant",text:"",streaming:!0}]);let g=m=>{n(x=>x.map(_=>_.id===y?m(_):_))};e.streamMessage(u,{onText:m=>g(x=>({...x,text:x.text+m})),onComplete:m=>{g(x=>({...x,text:m.reply||x.text,artifacts:m.artifacts,streaming:!1})),r(!1)},onError:m=>{g(x=>({...x,streaming:!1})),d(m.error),r(!1)}}).catch(m=>{g(x=>({...x,streaming:!1})),d(m instanceof Error?m.message:"stream_failed"),r(!1)})},[s,p,e,l]);return{messages:t,input:s,setInput:a,send:f,sending:p,error:o}};var h=require("react/jsx-runtime"),H={root:{display:"flex",flexDirection:"column",height:"100%",fontFamily:"system-ui, -apple-system, sans-serif",fontSize:14,color:"#111827"},log:{flex:1,overflowY:"auto",padding:12,display:"flex",flexDirection:"column",gap:8},user:{alignSelf:"flex-end",background:"#111827",color:"#ffffff",borderRadius:12,padding:"8px 12px",maxWidth:"80%",whiteSpace:"pre-wrap"},assistant:{alignSelf:"flex-start",background:"#f1f1f4",color:"#111827",borderRadius:12,padding:"8px 12px",maxWidth:"80%",whiteSpace:"pre-wrap"},artifacts:{display:"flex",flexDirection:"column",gap:6,marginTop:6},image:{maxHeight:220,borderRadius:8,border:"1px solid #e5e7eb"},link:{color:"#2563eb",textDecoration:"underline",wordBreak:"break-all"},error:{color:"#b91c1c",padding:"4px 12px",fontSize:13},form:{display:"flex",gap:8,borderTop:"1px solid #e5e7eb",padding:8},input:{flex:1,border:"1px solid #d1d5db",borderRadius:8,padding:"8px 10px",fontSize:14,outline:"none"},button:{border:"none",background:"#111827",color:"#ffffff",borderRadius:8,padding:"8px 14px",fontSize:14,cursor:"pointer"}},ae=({artifact:e})=>{let t=k(e.dataBase64??"",e.mimeType),n=e.url??t;return n?e.mimeType.startsWith("image/")?(0,h.jsx)("a",{href:n,rel:"noopener noreferrer",target:"_blank",children:(0,h.jsx)("img",{alt:e.filename,src:n,style:H.image})}):(0,h.jsx)("a",{download:e.filename,href:n,style:H.link,children:e.filename}):null},B=()=>{let e=M(),{messages:t,input:n,setInput:s,send:a,sending:p,error:r}=A(e),o=(0,U.useRef)(null);(0,U.useEffect)(()=>{let i=o.current;i&&(i.scrollTop=i.scrollHeight)},[t]);let d=i=>{i.preventDefault(),a()};return(0,h.jsxs)("div",{style:H.root,children:[(0,h.jsx)("div",{ref:o,style:H.log,children:t.map(i=>(0,h.jsxs)("div",{style:i.role==="user"?H.user:H.assistant,children:[i.text||(i.streaming?"\u2026":""),i.artifacts&&i.artifacts.length>0?(0,h.jsx)("div",{style:H.artifacts,children:i.artifacts.map(l=>(0,h.jsx)(ae,{artifact:l},l.filename))}):null]},i.id))}),r?(0,h.jsx)("div",{style:H.error,children:r}):null,(0,h.jsxs)("form",{onSubmit:d,style:H.form,children:[(0,h.jsx)("input",{"aria-label":"Message",onChange:i=>s(i.target.value),placeholder:"Type a message\u2026",style:H.input,value:n}),(0,h.jsx)("button",{disabled:p||n.trim().length===0,style:H.button,type:"submit",children:"Send"})]})]})};var ie=/\/+$/,O=e=>{let t=e.sessionId,n=e.baseUrl.replace(ie,""),s=r=>`${n}/endpoint/${encodeURIComponent(e.token)}${r}`,a={"content-type":"application/json"},p=(r,o)=>({url:s(r),headers:a,body:JSON.stringify({message:o,...t?{sessionId:t}:{},...e.identity?{externalUserId:e.identity.externalUserId,userHmac:e.identity.userHmac}:{}})});return{sessionId:()=>t,getInfo:()=>C(s("/config"),{}),sendMessage:async r=>{let o=await W(p("/message",r));return t=o.sessionId,o},streamMessage:(r,o,d)=>N(p("/message/stream",r),o,i=>{t=i},d),subscribe:(r,o)=>{let d=o?.since??"0",i=!1,l,f=o?.intervalMs??3e3,u=async()=>{let y=t;if(y)try{let g=await C(s(`/messages?session=${encodeURIComponent(y)}&since=${encodeURIComponent(d)}`),{});for(let m of g.messages)r.onMessage(m);d=g.cursor}catch(g){r.onError?.(T(g))}i||(l=setTimeout(u,f))},c=()=>{i=!0,l&&clearTimeout(l)};return o?.signal?.addEventListener("abort",c),l=setTimeout(u,0),c},requestHuman:async()=>{t&&await fetch(s("/request-human"),{method:"POST",headers:a,body:JSON.stringify({sessionId:t})})}}};var G=e=>"transport"in e?e.transport:O(e);var le=/\/+$/,v="/api/v1/end-user";function pe(e){let t="";for(let s=0;s<e.length;s+=32768)t+=String.fromCharCode(...e.subarray(s,s+32768));return btoa(t)}var K=e=>{let t=e.baseUrl.replace(le,""),n=()=>typeof e.token=="function"?e.token():e.token,s=async()=>({"content-type":"application/json",authorization:`Bearer ${await n()}`}),a=async(r,o)=>{try{let d=await fetch(`${t}${v}${r}`,{method:o.method,headers:await s(),...o.body===void 0?{}:{body:JSON.stringify(o.body)}});if(!d.ok){let i=await d.text().catch(()=>""),l=`${o.method} ${r} \u2192 ${String(d.status)}`;try{let f=JSON.parse(i);f.message&&(l=f.message)}catch{l=i?`${l}: ${i}`:l}throw T(new Error(l))}return await d.json()}catch(d){throw T(d)}},p=r=>`/tables/${encodeURIComponent(r)}`;return{listTables:async()=>(await C(`${t}${v}/tables`,await s())).tables,listRows:async(r,o)=>await C(`${t}${v}${p(r)}/rows?limit=${String(o??100)}`,await s()),insertRow:async(r,o)=>{let{rowIds:d}=await a(`${p(r)}/rows`,{method:"POST",body:{rows:[o]}}),i=d[0];if(!i)throw T(new Error("The row was not created."));return i},patchRow:async(r,o,d)=>(await a(`${p(r)}/rows/${encodeURIComponent(o)}`,{method:"PATCH",body:d})).updated,deleteRow:async(r,o)=>(await a(`/${encodeURIComponent(r)}/${encodeURIComponent(o)}`,{method:"DELETE"})).deleted,listFiles:async()=>(await C(`${t}${v}/tables/files/rows?limit=200`,await s())).rows.map(o=>({id:o.rowId,filename:String(o.data.filename??""),source:String(o.data.source??"client"),campaign:String(o.data.campaign??""),size:typeof o.data.size=="number"?o.data.size:0,createdAt:o.createdAt})),uploadFile:async(r,o)=>{let d=new Uint8Array(await r.arrayBuffer());return await a("/files",{method:"POST",body:{filename:r.name,mime:r.type||"application/octet-stream",data:pe(d),...o===void 0?{}:{message:o}}})},fileUrl:r=>`${t}${v}/files/${encodeURIComponent(r)}/raw`,deleteFile:async r=>(await a(`/files/${encodeURIComponent(r)}`,{method:"DELETE"})).deleted}};var w=require("react");function D(e,t){return e.some(s=>s.id===t.id)?e:[...t.author==="client"?e.filter(s=>!(s.pending&&s.author==="client"&&s.text===t.text)):e,t]}var V=(e,t)=>{let[n,s]=(0,w.useState)([]),[a,p]=(0,w.useState)(""),[r,o]=(0,w.useState)(!1),[d,i]=(0,w.useState)(null),l=(0,w.useRef)(0),f=t?.pollMs??2e3;(0,w.useEffect)(()=>{let c=!0;return e.getInfo().then(y=>{c&&y.greeting&&s(g=>g.length===0?[{id:"greeting",cursor:"",author:"agent",text:y.greeting}]:g)}).catch(()=>{}),()=>{c=!1}},[e]),(0,w.useEffect)(()=>e.subscribe?.({onMessage:c=>s(y=>D(y,c)),onError:c=>i(c.error)},{intervalMs:f}),[e,f]);let u=(0,w.useCallback)(()=>{let c=a.trim();if(!c||r)return;p(""),i(null),o(!0),l.current+=1;let y={id:`pending-${l.current}`,cursor:"",author:"client",text:c,pending:!0};s(g=>[...g,y]),e.sendMessage(c).then(()=>o(!1)).catch(g=>{i(g instanceof Error?g.message:"send_failed"),o(!1)})},[a,r,e]);return{messages:n,input:a,setInput:p,send:u,sending:r,error:d}};0&&(module.exports={Chat,HolaProvider,createApiTransport,createHolaClient,createPortalData,createWidgetTransport,mergeMessage,textDeltaOf,thinkingDeltaOf,toolEventOf,useArtifactUrl,useChat,useHolaClient,useThread});
@@ -0,0 +1,284 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ /** A file the employee produced (image/video/audio/other). In-turn artifacts (a turn's
5
+ * completion) carry inline `dataBase64`; thread artifacts (the receive channel) carry a `url`
6
+ * reference (presigned/public). Exactly one of the two is present. */
7
+ type HolaArtifact = {
8
+ filename: string;
9
+ mimeType: string;
10
+ dataBase64?: string;
11
+ url?: string;
12
+ };
13
+ /** Public info for an employee (display name + first greeting). */
14
+ type HolaEmployeeInfo = {
15
+ name: string;
16
+ greeting: string;
17
+ /** Who is answering right now. A portal that offers "talk to a person" needs
18
+ * this, or it keeps offering after someone has already taken over. Absent on
19
+ * transports whose surface has no handover. */
20
+ mode?: "agent" | "human";
21
+ };
22
+ /** An embedder-verified visitor identity (server-signed) — turns an anonymous
23
+ * visitor into an identified requester with per-user memory. */
24
+ type HolaIdentity = {
25
+ externalUserId: string;
26
+ /** hex HMAC-SHA256(externalUserId, widgetSecret), computed on YOUR server. */
27
+ userHmac: string;
28
+ };
29
+ /** The final result of one turn. */
30
+ type HolaCompletion = {
31
+ reply: string;
32
+ sessionId: string;
33
+ artifacts: HolaArtifact[];
34
+ };
35
+ /** A tool-execution step surfaced during a streamed turn. */
36
+ type HolaToolEvent = {
37
+ phase: "start";
38
+ id: string;
39
+ name?: string;
40
+ } | {
41
+ phase: "end";
42
+ id: string;
43
+ isError?: boolean;
44
+ };
45
+ /** Who wrote a message. A human turn and an agent turn are the same kind of message —
46
+ * `client` is the visitor, `agent` the AI, `human` a person on the company's side. */
47
+ type HolaAuthor = "client" | "agent" | "human";
48
+ /** One message in the conversation thread — read via the receive channel (`subscribe`).
49
+ * Stable `id` (dedupe), `cursor` (resume). */
50
+ type HolaMessage = {
51
+ id: string;
52
+ cursor: string;
53
+ author: HolaAuthor;
54
+ text: string;
55
+ artifacts?: HolaArtifact[];
56
+ /** Author display name (for a human turn) and other non-secret markers. */
57
+ meta?: Record<string, unknown> | null;
58
+ };
59
+ /** Callbacks for the receive channel — messages produced OUTSIDE this client's own turn
60
+ * (a human reply, an out-of-turn agent turn, an async artifact). */
61
+ type HolaSubscribeHandlers = {
62
+ onMessage: (message: HolaMessage) => void;
63
+ onError?: (error: HolaError) => void;
64
+ };
65
+ type HolaSubscribeOptions = {
66
+ /** Resume from this cursor; omit to read the thread from the start. */
67
+ since?: string;
68
+ /** Poll interval in ms (default 3000). */
69
+ intervalMs?: number;
70
+ signal?: AbortSignal;
71
+ };
72
+ type HolaError = {
73
+ error: string;
74
+ retryable?: boolean;
75
+ };
76
+ /** Callbacks for a streamed turn. All optional. */
77
+ type HolaStreamHandlers = {
78
+ onSession?: (sessionId: string) => void;
79
+ /** The raw pi turn event (advanced; most callers use onText/onTool). */
80
+ onEvent?: (event: unknown) => void;
81
+ onText?: (delta: string) => void;
82
+ onThinking?: (delta: string) => void;
83
+ onTool?: (step: HolaToolEvent) => void;
84
+ onComplete?: (completion: HolaCompletion) => void;
85
+ onError?: (error: HolaError) => void;
86
+ };
87
+ /** How a client reaches one employee: fetch info, send/stream a turn, track the
88
+ * session. Swappable — the SDK core (`useChat`, `<Chat/>`) is transport-agnostic, so
89
+ * the backend can change without touching any UI. */
90
+ type HolaTransport = {
91
+ sessionId(): string | undefined;
92
+ getInfo(): Promise<HolaEmployeeInfo>;
93
+ sendMessage(message: string): Promise<HolaCompletion>;
94
+ streamMessage(message: string, handlers: HolaStreamHandlers, signal?: AbortSignal): Promise<void>;
95
+ /** The receive channel: keep reading the conversation thread after the current turn, so the
96
+ * client sees messages it did NOT produce — a human reply, an out-of-turn agent turn, an
97
+ * async artifact. Returns an unsubscribe. Optional: a transport with no receive channel omits
98
+ * it (callers guard on its presence). */
99
+ subscribe?(handlers: HolaSubscribeHandlers, options?: HolaSubscribeOptions): () => void;
100
+ /** "Talk to a person": ask for a human to take over this conversation. The AI pauses until a
101
+ * teammate replies or hands back; the person's messages arrive via `subscribe`. Optional. */
102
+ requestHuman?(): Promise<void>;
103
+ };
104
+
105
+ type ApiTransportConfig = {
106
+ /** API origin, e.g. "https://api.holaos.ai". */
107
+ baseUrl: string;
108
+ /** End-user access token for the Authorization header. A string, or a getter so a
109
+ * refreshed token is used per request. */
110
+ token: string | (() => string | Promise<string>);
111
+ };
112
+ /** The Bearer-authed transport for the signed-in end-user PORTAL (`/api/v1/end-user/*`). Same
113
+ * client surface as the anonymous widget transport — the token (not a path param) resolves the
114
+ * customer and their one lifelong thread. The portal is buffered (no SSE) and one-thread-per-user,
115
+ * so `sendMessage` returns just the reply and everything out-of-turn — the AI's turns, a human's
116
+ * reply, an async artifact (video) — arrives via `subscribe`. */
117
+ declare const createApiTransport: (config: ApiTransportConfig) => HolaTransport;
118
+
119
+ /** base64 → object URL (revoked on unmount) for inline <img>/<video>/<audio> or a
120
+ * download link. Returns "" for empty input (a url-referenced artifact has no base64). */
121
+ declare const useArtifactUrl: (dataBase64: string, mimeType: string) => string;
122
+
123
+ /** A minimal, dependency-free chat surface for the employee. Drop it inside a
124
+ * `<HolaProvider>`. For custom UI, use `useChat(client)` directly. */
125
+ declare const Chat: () => react_jsx_runtime.JSX.Element;
126
+
127
+ type WidgetTransportConfig = {
128
+ /** API origin that fronts the Holaboss employee endpoint, e.g. "https://api.holaos.ai". */
129
+ baseUrl: string;
130
+ /** A credential for one employee — a connector token today, an end-user JWT once auth ships. */
131
+ token: string;
132
+ /** Opaque per-visitor conversation id; omit to start fresh (the first reply mints one). */
133
+ sessionId?: string;
134
+ /** Optional embedder-verified visitor identity for per-user memory. */
135
+ identity?: HolaIdentity;
136
+ };
137
+ /** The transport that ships today, over Holaboss's public per-employee endpoint.
138
+ * See `createApiTransport` for the Bearer-authed end-user data API (the long-term target). */
139
+ declare const createWidgetTransport: (config: WidgetTransportConfig) => HolaTransport;
140
+
141
+ /** A chat client for one employee — framework-agnostic. Feed it to `useChat` /
142
+ * `<Chat/>`, or drive it directly to build any chat UI you like. */
143
+ type HolaClient = HolaTransport;
144
+ type CreateHolaClientOptions = WidgetTransportConfig | {
145
+ transport: HolaTransport;
146
+ };
147
+ /** Create a client for one employee. Pass `{ baseUrl, token }` for the built-in transport,
148
+ * or `{ transport }` to target a custom backend (e.g. `createApiTransport`). Transport-
149
+ * agnostic — the current transport is an internal detail, not the SDK's identity. */
150
+ declare const createHolaClient: (options: CreateHolaClientOptions) => HolaClient;
151
+
152
+ /** Extract a streamed text delta from a raw pi turn event, or null. */
153
+ declare const textDeltaOf: (event: unknown) => string | null;
154
+ /** Extract a streamed reasoning delta from a raw pi turn event, or null. */
155
+ declare const thinkingDeltaOf: (event: unknown) => string | null;
156
+ /** Extract a tool-execution start/end from a raw pi turn event, or null. */
157
+ declare const toolEventOf: (event: unknown) => HolaToolEvent | null;
158
+
159
+ /** A column, as the company defined it. */
160
+ type PortalField = {
161
+ key: string;
162
+ label: string;
163
+ type: "text" | "number" | "date" | "boolean" | "select" | "url";
164
+ options?: string[];
165
+ description?: string;
166
+ };
167
+ type PortalTable = {
168
+ slug: string;
169
+ name: string;
170
+ description: string | null;
171
+ fields: PortalField[];
172
+ /** The fields this customer may send. Gates CREATE as well as edit — a field
173
+ * missing from here makes `insert` fail, not just `patch`. */
174
+ writable_fields: string[];
175
+ row_count: number;
176
+ };
177
+ type PortalRow = {
178
+ rowId: string;
179
+ data: Record<string, unknown>;
180
+ createdAt: string;
181
+ updatedAt: string;
182
+ };
183
+ type PortalRowPage = {
184
+ rows: PortalRow[];
185
+ /** Total matching rows, which can exceed what came back. */
186
+ matched: number;
187
+ truncated: boolean;
188
+ };
189
+ type PortalFile = {
190
+ id: string;
191
+ filename: string;
192
+ /** Who supplied it. `client` is what this customer sent; anything else was
193
+ * delivered to them. */
194
+ source: string;
195
+ /** The campaign-or-whatever it is filed under; empty means unfiled. */
196
+ campaign: string;
197
+ size: number;
198
+ createdAt: string;
199
+ };
200
+ type PortalData = {
201
+ /** The tables this customer can see. A table the company has not shared is
202
+ * absent, not empty — it answers as if it did not exist. */
203
+ listTables(): Promise<PortalTable[]>;
204
+ /**
205
+ * Rows, plus how many there actually are.
206
+ *
207
+ * The server caps a page, so returning the array alone would let a dashboard
208
+ * present "the most recent 100" as "all of them" — a quiet way to be wrong.
209
+ */
210
+ listRows(slug: string, limit?: number): Promise<PortalRowPage>;
211
+ /** Returns the new row id. Only fields in `writable_fields` are accepted. */
212
+ insertRow(slug: string, row: Record<string, unknown>): Promise<string>;
213
+ /** Pass an empty string to clear a field. */
214
+ patchRow(slug: string, rowId: string, patch: Record<string, unknown>): Promise<number>;
215
+ /** Only what the company allows a customer to delete, and only when nothing
216
+ * depends on it. Rejects with the reason otherwise. */
217
+ deleteRow(slug: string, rowId: string): Promise<number>;
218
+ /** Everything exchanged with this customer, newest first. */
219
+ listFiles(): Promise<PortalFile[]>;
220
+ /** Sends bytes. The server writes the row — a storage key is never the
221
+ * caller's to state. */
222
+ uploadFile(file: File, message?: string): Promise<{
223
+ rowId: string;
224
+ }>;
225
+ /** A URL to open or download. It redirects to a signed one. */
226
+ fileUrl(rowId: string): string;
227
+ /** Only files this customer sent, and only before they are filed. */
228
+ deleteFile(rowId: string): Promise<number>;
229
+ };
230
+ declare const createPortalData: (config: ApiTransportConfig) => PortalData;
231
+
232
+ /** Provides a `HolaClient` (from `createHolaClient`) to `useChat` / `<Chat/>`.
233
+ * Create the client once (e.g. `useMemo`) so its session persists across renders. */
234
+ declare const HolaProvider: ({ client, children, }: {
235
+ client: HolaClient;
236
+ children: ReactNode;
237
+ }) => react_jsx_runtime.JSX.Element;
238
+ declare const useHolaClient: () => HolaClient;
239
+
240
+ type ChatMessage = {
241
+ id: number;
242
+ role: "user" | "assistant";
243
+ text: string;
244
+ artifacts?: HolaArtifact[];
245
+ streaming?: boolean;
246
+ };
247
+ type UseChat = {
248
+ messages: ChatMessage[];
249
+ input: string;
250
+ setInput: (value: string) => void;
251
+ send: () => void;
252
+ sending: boolean;
253
+ error: string | null;
254
+ };
255
+ /** Chat state for one employee client: greeting, message list, streaming send. */
256
+ declare const useChat: (client: HolaClient) => UseChat;
257
+
258
+ /** A thread message; `pending` marks an optimistic echo of the visitor's own send, shown
259
+ * instantly and replaced by the authoritative message when the receive channel delivers it. */
260
+ type ThreadMessage = HolaMessage & {
261
+ pending?: boolean;
262
+ };
263
+ type UseThread = {
264
+ messages: ThreadMessage[];
265
+ input: string;
266
+ setInput: (value: string) => void;
267
+ send: () => void;
268
+ sending: boolean;
269
+ error: string | null;
270
+ };
271
+ /** Merge one incoming thread message: dedupe by id, and replace a pending optimistic echo of the
272
+ * visitor's own send (same text) with the authoritative one. Pure — the hook's core, so the
273
+ * merge rule is unit-checkable without React. */
274
+ declare function mergeMessage(list: ThreadMessage[], incoming: HolaMessage): ThreadMessage[];
275
+ /** A poll-based UNIFIED thread: the whole conversation — visitor, agent, and HUMAN turns — as one
276
+ * live list via the receive channel (`client.subscribe`). Unlike `useChat` (streaming, one turn
277
+ * at a time), this shows messages produced OUTSIDE the visitor's own turn — a human reply, an
278
+ * out-of-turn agent turn — as they arrive. Send optimistically echoes the visitor's message; the
279
+ * poll replaces it with the stored one. Requires a transport that implements `subscribe`. */
280
+ declare const useThread: (client: HolaClient, options?: {
281
+ pollMs?: number;
282
+ }) => UseThread;
283
+
284
+ export { type ApiTransportConfig, Chat, type ChatMessage, type CreateHolaClientOptions, type HolaArtifact, type HolaAuthor, type HolaClient, type HolaCompletion, type HolaEmployeeInfo, type HolaError, type HolaIdentity, type HolaMessage, HolaProvider, type HolaStreamHandlers, type HolaSubscribeHandlers, type HolaSubscribeOptions, type HolaToolEvent, type HolaTransport, type PortalData, type PortalField, type PortalFile, type PortalRow, type PortalRowPage, type PortalTable, type ThreadMessage, type UseChat, type UseThread, type WidgetTransportConfig, createApiTransport, createHolaClient, createPortalData, createWidgetTransport, mergeMessage, textDeltaOf, thinkingDeltaOf, toolEventOf, useArtifactUrl, useChat, useHolaClient, useThread };
@@ -0,0 +1,284 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ /** A file the employee produced (image/video/audio/other). In-turn artifacts (a turn's
5
+ * completion) carry inline `dataBase64`; thread artifacts (the receive channel) carry a `url`
6
+ * reference (presigned/public). Exactly one of the two is present. */
7
+ type HolaArtifact = {
8
+ filename: string;
9
+ mimeType: string;
10
+ dataBase64?: string;
11
+ url?: string;
12
+ };
13
+ /** Public info for an employee (display name + first greeting). */
14
+ type HolaEmployeeInfo = {
15
+ name: string;
16
+ greeting: string;
17
+ /** Who is answering right now. A portal that offers "talk to a person" needs
18
+ * this, or it keeps offering after someone has already taken over. Absent on
19
+ * transports whose surface has no handover. */
20
+ mode?: "agent" | "human";
21
+ };
22
+ /** An embedder-verified visitor identity (server-signed) — turns an anonymous
23
+ * visitor into an identified requester with per-user memory. */
24
+ type HolaIdentity = {
25
+ externalUserId: string;
26
+ /** hex HMAC-SHA256(externalUserId, widgetSecret), computed on YOUR server. */
27
+ userHmac: string;
28
+ };
29
+ /** The final result of one turn. */
30
+ type HolaCompletion = {
31
+ reply: string;
32
+ sessionId: string;
33
+ artifacts: HolaArtifact[];
34
+ };
35
+ /** A tool-execution step surfaced during a streamed turn. */
36
+ type HolaToolEvent = {
37
+ phase: "start";
38
+ id: string;
39
+ name?: string;
40
+ } | {
41
+ phase: "end";
42
+ id: string;
43
+ isError?: boolean;
44
+ };
45
+ /** Who wrote a message. A human turn and an agent turn are the same kind of message —
46
+ * `client` is the visitor, `agent` the AI, `human` a person on the company's side. */
47
+ type HolaAuthor = "client" | "agent" | "human";
48
+ /** One message in the conversation thread — read via the receive channel (`subscribe`).
49
+ * Stable `id` (dedupe), `cursor` (resume). */
50
+ type HolaMessage = {
51
+ id: string;
52
+ cursor: string;
53
+ author: HolaAuthor;
54
+ text: string;
55
+ artifacts?: HolaArtifact[];
56
+ /** Author display name (for a human turn) and other non-secret markers. */
57
+ meta?: Record<string, unknown> | null;
58
+ };
59
+ /** Callbacks for the receive channel — messages produced OUTSIDE this client's own turn
60
+ * (a human reply, an out-of-turn agent turn, an async artifact). */
61
+ type HolaSubscribeHandlers = {
62
+ onMessage: (message: HolaMessage) => void;
63
+ onError?: (error: HolaError) => void;
64
+ };
65
+ type HolaSubscribeOptions = {
66
+ /** Resume from this cursor; omit to read the thread from the start. */
67
+ since?: string;
68
+ /** Poll interval in ms (default 3000). */
69
+ intervalMs?: number;
70
+ signal?: AbortSignal;
71
+ };
72
+ type HolaError = {
73
+ error: string;
74
+ retryable?: boolean;
75
+ };
76
+ /** Callbacks for a streamed turn. All optional. */
77
+ type HolaStreamHandlers = {
78
+ onSession?: (sessionId: string) => void;
79
+ /** The raw pi turn event (advanced; most callers use onText/onTool). */
80
+ onEvent?: (event: unknown) => void;
81
+ onText?: (delta: string) => void;
82
+ onThinking?: (delta: string) => void;
83
+ onTool?: (step: HolaToolEvent) => void;
84
+ onComplete?: (completion: HolaCompletion) => void;
85
+ onError?: (error: HolaError) => void;
86
+ };
87
+ /** How a client reaches one employee: fetch info, send/stream a turn, track the
88
+ * session. Swappable — the SDK core (`useChat`, `<Chat/>`) is transport-agnostic, so
89
+ * the backend can change without touching any UI. */
90
+ type HolaTransport = {
91
+ sessionId(): string | undefined;
92
+ getInfo(): Promise<HolaEmployeeInfo>;
93
+ sendMessage(message: string): Promise<HolaCompletion>;
94
+ streamMessage(message: string, handlers: HolaStreamHandlers, signal?: AbortSignal): Promise<void>;
95
+ /** The receive channel: keep reading the conversation thread after the current turn, so the
96
+ * client sees messages it did NOT produce — a human reply, an out-of-turn agent turn, an
97
+ * async artifact. Returns an unsubscribe. Optional: a transport with no receive channel omits
98
+ * it (callers guard on its presence). */
99
+ subscribe?(handlers: HolaSubscribeHandlers, options?: HolaSubscribeOptions): () => void;
100
+ /** "Talk to a person": ask for a human to take over this conversation. The AI pauses until a
101
+ * teammate replies or hands back; the person's messages arrive via `subscribe`. Optional. */
102
+ requestHuman?(): Promise<void>;
103
+ };
104
+
105
+ type ApiTransportConfig = {
106
+ /** API origin, e.g. "https://api.holaos.ai". */
107
+ baseUrl: string;
108
+ /** End-user access token for the Authorization header. A string, or a getter so a
109
+ * refreshed token is used per request. */
110
+ token: string | (() => string | Promise<string>);
111
+ };
112
+ /** The Bearer-authed transport for the signed-in end-user PORTAL (`/api/v1/end-user/*`). Same
113
+ * client surface as the anonymous widget transport — the token (not a path param) resolves the
114
+ * customer and their one lifelong thread. The portal is buffered (no SSE) and one-thread-per-user,
115
+ * so `sendMessage` returns just the reply and everything out-of-turn — the AI's turns, a human's
116
+ * reply, an async artifact (video) — arrives via `subscribe`. */
117
+ declare const createApiTransport: (config: ApiTransportConfig) => HolaTransport;
118
+
119
+ /** base64 → object URL (revoked on unmount) for inline <img>/<video>/<audio> or a
120
+ * download link. Returns "" for empty input (a url-referenced artifact has no base64). */
121
+ declare const useArtifactUrl: (dataBase64: string, mimeType: string) => string;
122
+
123
+ /** A minimal, dependency-free chat surface for the employee. Drop it inside a
124
+ * `<HolaProvider>`. For custom UI, use `useChat(client)` directly. */
125
+ declare const Chat: () => react_jsx_runtime.JSX.Element;
126
+
127
+ type WidgetTransportConfig = {
128
+ /** API origin that fronts the Holaboss employee endpoint, e.g. "https://api.holaos.ai". */
129
+ baseUrl: string;
130
+ /** A credential for one employee — a connector token today, an end-user JWT once auth ships. */
131
+ token: string;
132
+ /** Opaque per-visitor conversation id; omit to start fresh (the first reply mints one). */
133
+ sessionId?: string;
134
+ /** Optional embedder-verified visitor identity for per-user memory. */
135
+ identity?: HolaIdentity;
136
+ };
137
+ /** The transport that ships today, over Holaboss's public per-employee endpoint.
138
+ * See `createApiTransport` for the Bearer-authed end-user data API (the long-term target). */
139
+ declare const createWidgetTransport: (config: WidgetTransportConfig) => HolaTransport;
140
+
141
+ /** A chat client for one employee — framework-agnostic. Feed it to `useChat` /
142
+ * `<Chat/>`, or drive it directly to build any chat UI you like. */
143
+ type HolaClient = HolaTransport;
144
+ type CreateHolaClientOptions = WidgetTransportConfig | {
145
+ transport: HolaTransport;
146
+ };
147
+ /** Create a client for one employee. Pass `{ baseUrl, token }` for the built-in transport,
148
+ * or `{ transport }` to target a custom backend (e.g. `createApiTransport`). Transport-
149
+ * agnostic — the current transport is an internal detail, not the SDK's identity. */
150
+ declare const createHolaClient: (options: CreateHolaClientOptions) => HolaClient;
151
+
152
+ /** Extract a streamed text delta from a raw pi turn event, or null. */
153
+ declare const textDeltaOf: (event: unknown) => string | null;
154
+ /** Extract a streamed reasoning delta from a raw pi turn event, or null. */
155
+ declare const thinkingDeltaOf: (event: unknown) => string | null;
156
+ /** Extract a tool-execution start/end from a raw pi turn event, or null. */
157
+ declare const toolEventOf: (event: unknown) => HolaToolEvent | null;
158
+
159
+ /** A column, as the company defined it. */
160
+ type PortalField = {
161
+ key: string;
162
+ label: string;
163
+ type: "text" | "number" | "date" | "boolean" | "select" | "url";
164
+ options?: string[];
165
+ description?: string;
166
+ };
167
+ type PortalTable = {
168
+ slug: string;
169
+ name: string;
170
+ description: string | null;
171
+ fields: PortalField[];
172
+ /** The fields this customer may send. Gates CREATE as well as edit — a field
173
+ * missing from here makes `insert` fail, not just `patch`. */
174
+ writable_fields: string[];
175
+ row_count: number;
176
+ };
177
+ type PortalRow = {
178
+ rowId: string;
179
+ data: Record<string, unknown>;
180
+ createdAt: string;
181
+ updatedAt: string;
182
+ };
183
+ type PortalRowPage = {
184
+ rows: PortalRow[];
185
+ /** Total matching rows, which can exceed what came back. */
186
+ matched: number;
187
+ truncated: boolean;
188
+ };
189
+ type PortalFile = {
190
+ id: string;
191
+ filename: string;
192
+ /** Who supplied it. `client` is what this customer sent; anything else was
193
+ * delivered to them. */
194
+ source: string;
195
+ /** The campaign-or-whatever it is filed under; empty means unfiled. */
196
+ campaign: string;
197
+ size: number;
198
+ createdAt: string;
199
+ };
200
+ type PortalData = {
201
+ /** The tables this customer can see. A table the company has not shared is
202
+ * absent, not empty — it answers as if it did not exist. */
203
+ listTables(): Promise<PortalTable[]>;
204
+ /**
205
+ * Rows, plus how many there actually are.
206
+ *
207
+ * The server caps a page, so returning the array alone would let a dashboard
208
+ * present "the most recent 100" as "all of them" — a quiet way to be wrong.
209
+ */
210
+ listRows(slug: string, limit?: number): Promise<PortalRowPage>;
211
+ /** Returns the new row id. Only fields in `writable_fields` are accepted. */
212
+ insertRow(slug: string, row: Record<string, unknown>): Promise<string>;
213
+ /** Pass an empty string to clear a field. */
214
+ patchRow(slug: string, rowId: string, patch: Record<string, unknown>): Promise<number>;
215
+ /** Only what the company allows a customer to delete, and only when nothing
216
+ * depends on it. Rejects with the reason otherwise. */
217
+ deleteRow(slug: string, rowId: string): Promise<number>;
218
+ /** Everything exchanged with this customer, newest first. */
219
+ listFiles(): Promise<PortalFile[]>;
220
+ /** Sends bytes. The server writes the row — a storage key is never the
221
+ * caller's to state. */
222
+ uploadFile(file: File, message?: string): Promise<{
223
+ rowId: string;
224
+ }>;
225
+ /** A URL to open or download. It redirects to a signed one. */
226
+ fileUrl(rowId: string): string;
227
+ /** Only files this customer sent, and only before they are filed. */
228
+ deleteFile(rowId: string): Promise<number>;
229
+ };
230
+ declare const createPortalData: (config: ApiTransportConfig) => PortalData;
231
+
232
+ /** Provides a `HolaClient` (from `createHolaClient`) to `useChat` / `<Chat/>`.
233
+ * Create the client once (e.g. `useMemo`) so its session persists across renders. */
234
+ declare const HolaProvider: ({ client, children, }: {
235
+ client: HolaClient;
236
+ children: ReactNode;
237
+ }) => react_jsx_runtime.JSX.Element;
238
+ declare const useHolaClient: () => HolaClient;
239
+
240
+ type ChatMessage = {
241
+ id: number;
242
+ role: "user" | "assistant";
243
+ text: string;
244
+ artifacts?: HolaArtifact[];
245
+ streaming?: boolean;
246
+ };
247
+ type UseChat = {
248
+ messages: ChatMessage[];
249
+ input: string;
250
+ setInput: (value: string) => void;
251
+ send: () => void;
252
+ sending: boolean;
253
+ error: string | null;
254
+ };
255
+ /** Chat state for one employee client: greeting, message list, streaming send. */
256
+ declare const useChat: (client: HolaClient) => UseChat;
257
+
258
+ /** A thread message; `pending` marks an optimistic echo of the visitor's own send, shown
259
+ * instantly and replaced by the authoritative message when the receive channel delivers it. */
260
+ type ThreadMessage = HolaMessage & {
261
+ pending?: boolean;
262
+ };
263
+ type UseThread = {
264
+ messages: ThreadMessage[];
265
+ input: string;
266
+ setInput: (value: string) => void;
267
+ send: () => void;
268
+ sending: boolean;
269
+ error: string | null;
270
+ };
271
+ /** Merge one incoming thread message: dedupe by id, and replace a pending optimistic echo of the
272
+ * visitor's own send (same text) with the authoritative one. Pure — the hook's core, so the
273
+ * merge rule is unit-checkable without React. */
274
+ declare function mergeMessage(list: ThreadMessage[], incoming: HolaMessage): ThreadMessage[];
275
+ /** A poll-based UNIFIED thread: the whole conversation — visitor, agent, and HUMAN turns — as one
276
+ * live list via the receive channel (`client.subscribe`). Unlike `useChat` (streaming, one turn
277
+ * at a time), this shows messages produced OUTSIDE the visitor's own turn — a human reply, an
278
+ * out-of-turn agent turn — as they arrive. Send optimistically echoes the visitor's message; the
279
+ * poll replaces it with the stored one. Requires a transport that implements `subscribe`. */
280
+ declare const useThread: (client: HolaClient, options?: {
281
+ pollMs?: number;
282
+ }) => UseThread;
283
+
284
+ export { type ApiTransportConfig, Chat, type ChatMessage, type CreateHolaClientOptions, type HolaArtifact, type HolaAuthor, type HolaClient, type HolaCompletion, type HolaEmployeeInfo, type HolaError, type HolaIdentity, type HolaMessage, HolaProvider, type HolaStreamHandlers, type HolaSubscribeHandlers, type HolaSubscribeOptions, type HolaToolEvent, type HolaTransport, type PortalData, type PortalField, type PortalFile, type PortalRow, type PortalRowPage, type PortalTable, type ThreadMessage, type UseChat, type UseThread, type WidgetTransportConfig, createApiTransport, createHolaClient, createPortalData, createWidgetTransport, mergeMessage, textDeltaOf, thinkingDeltaOf, toolEventOf, useArtifactUrl, useChat, useHolaClient, useThread };
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ 'use client'
2
+ var O=e=>{if(!e||typeof e!="object")return null;let t=e;return t.type==="message_update"?t.assistantMessageEvent??null:null},R=e=>{let t=O(e);return t?.type==="text_delta"&&typeof t.delta=="string"?t.delta:null},E=e=>{let t=O(e);return t?.type==="thinking_delta"&&typeof t.delta=="string"?t.delta:null},S=e=>{if(!e||typeof e!="object")return null;let t=e;return t.type==="tool_execution_start"&&t.toolCallId?{phase:"start",id:t.toolCallId,name:t.toolName}:t.type==="tool_execution_end"&&t.toolCallId?{phase:"end",id:t.toolCallId,isError:t.isError}:null};var j=/^ /,w=e=>{if(e&&typeof e=="object"){let t=e,a=typeof t.error=="string"?t.error:"request_failed";return typeof t.retryable=="boolean"?{error:a,retryable:t.retryable}:{error:a}}return{error:"request_failed"}},z=e=>{let t="message",a=[];for(let s of e.split(`
3
+ `))s.startsWith(":")||(s.startsWith("event:")?t=s.slice(6).trim():s.startsWith("data:")&&a.push(s.slice(5).replace(j,"")));return a.length>0?{event:t,data:a.join(`
4
+ `)}:null},J=(e,t,a)=>{let s;try{s=JSON.parse(e.data)}catch{return}if(e.event==="session"){let i=s.sessionId;i&&(a(i),t.onSession?.(i));return}if(e.event==="turn_event"){t.onEvent?.(s);let i=R(s);i&&t.onText?.(i);let p=E(s);p&&t.onThinking?.(p);let r=S(s);r&&t.onTool?.(r);return}if(e.event==="chat_completed"){let i=s;a(i.sessionId),t.onComplete?.(i);return}e.event==="chat_error"&&t.onError?.(w(s))},x=async(e,t)=>{let a=await fetch(e,{headers:t});if(!a.ok)throw new Error(`hola: request failed (${a.status})`);return await a.json()},_=async e=>{let t=await fetch(e.url,{method:"POST",headers:e.headers,body:e.body});if(!t.ok)throw new Error(`hola: message failed (${t.status})`);return await t.json()},F=async(e,t,a,s)=>{let i=await fetch(e.url,{method:"POST",headers:e.headers,body:e.body,signal:s});if(!(i.ok&&i.body)){t.onError?.({error:`stream_failed_${i.status}`});return}let p=i.body.getReader(),r=new TextDecoder,o="",d=()=>{let l=o.indexOf(`
5
+
6
+ `);for(;l!==-1;){let f=o.slice(0,l);o=o.slice(l+2);let u=z(f);u&&J(u,t,a),l=o.indexOf(`
7
+
8
+ `)}},n=async()=>{let l=await p.read();if(!l.done)return o+=r.decode(l.value,{stream:!0}),d(),n()};await n()};var q=/\/+$/,C="/api/v1/end-user",B=e=>{let t=e.baseUrl.replace(q,""),a=()=>typeof e.token=="function"?e.token():e.token,s=async()=>({"content-type":"application/json",authorization:`Bearer ${await a()}`}),i=async p=>{let r=await fetch(`${t}${C}/chat`,{method:"POST",headers:await s(),body:JSON.stringify({message:p})});if(!r.ok)throw new Error(`hola: message failed (${r.status})`);return{reply:(await r.json()).reply??"",sessionId:"",artifacts:[]}};return{sessionId:()=>"",getInfo:async()=>x(`${t}${C}/chat/info`,await s()),sendMessage:i,streamMessage:async(p,r)=>{try{let o=await i(p);r.onText?.(o.reply),r.onComplete?.(o)}catch(o){r.onError?.(w(o))}},subscribe:(p,r)=>{let o=r?.since??"0",d=!1,n,l=r?.intervalMs??3e3,f=async()=>{try{let c=await x(`${t}${C}/chat/thread?since=${encodeURIComponent(o)}`,await s());for(let y of c.messages)p.onMessage(y);o=c.cursor}catch(c){p.onError?.(w(c))}d||(n=setTimeout(f,l))},u=()=>{d=!0,n&&clearTimeout(n)};return r?.signal?.addEventListener("abort",u),n=setTimeout(f,0),u},requestHuman:async()=>{await fetch(`${t}${C}/chat/request-human`,{method:"POST",headers:await s(),body:"{}"})}}};import{useEffect as G,useState as K}from"react";var k=(e,t)=>{let[a,s]=K("");return G(()=>{if(!e){s("");return}let i=Uint8Array.from(atob(e),r=>r.charCodeAt(0)),p=URL.createObjectURL(new Blob([i],{type:t}));return s(p),()=>URL.revokeObjectURL(p)},[e,t]),a};import{useEffect as te,useRef as re}from"react";import{createContext as V,useContext as Y}from"react";import{jsx as X}from"react/jsx-runtime";var D=V(null),Q=({client:e,children:t})=>X(D.Provider,{value:e,children:t}),$=()=>{let e=Y(D);if(!e)throw new Error("useHolaClient must be used within <HolaProvider>");return e};import{useCallback as L,useEffect as Z,useRef as ee,useState as v}from"react";var M=e=>{let[t,a]=v([]),[s,i]=v(""),[p,r]=v(!1),[o,d]=v(null),n=ee(0),l=L(()=>{let u=n.current;return n.current+=1,u},[]);Z(()=>{let u=!0;return e.getInfo().then(c=>{u&&c.greeting&&a([{id:l(),role:"assistant",text:c.greeting}])}).catch(()=>{}),()=>{u=!1}},[e,l]);let f=L(()=>{let u=s.trim();if(!u||p)return;i(""),d(null),r(!0);let c=l(),y=l();a(m=>[...m,{id:c,role:"user",text:u},{id:y,role:"assistant",text:"",streaming:!0}]);let g=m=>{a(h=>h.map(I=>I.id===y?m(I):I))};e.streamMessage(u,{onText:m=>g(h=>({...h,text:h.text+m})),onComplete:m=>{g(h=>({...h,text:m.reply||h.text,artifacts:m.artifacts,streaming:!1})),r(!1)},onError:m=>{g(h=>({...h,streaming:!1})),d(m.error),r(!1)}}).catch(m=>{g(h=>({...h,streaming:!1})),d(m instanceof Error?m.message:"stream_failed"),r(!1)})},[s,p,e,l]);return{messages:t,input:s,setInput:i,send:f,sending:p,error:o}};import{jsx as H,jsxs as A}from"react/jsx-runtime";var b={root:{display:"flex",flexDirection:"column",height:"100%",fontFamily:"system-ui, -apple-system, sans-serif",fontSize:14,color:"#111827"},log:{flex:1,overflowY:"auto",padding:12,display:"flex",flexDirection:"column",gap:8},user:{alignSelf:"flex-end",background:"#111827",color:"#ffffff",borderRadius:12,padding:"8px 12px",maxWidth:"80%",whiteSpace:"pre-wrap"},assistant:{alignSelf:"flex-start",background:"#f1f1f4",color:"#111827",borderRadius:12,padding:"8px 12px",maxWidth:"80%",whiteSpace:"pre-wrap"},artifacts:{display:"flex",flexDirection:"column",gap:6,marginTop:6},image:{maxHeight:220,borderRadius:8,border:"1px solid #e5e7eb"},link:{color:"#2563eb",textDecoration:"underline",wordBreak:"break-all"},error:{color:"#b91c1c",padding:"4px 12px",fontSize:13},form:{display:"flex",gap:8,borderTop:"1px solid #e5e7eb",padding:8},input:{flex:1,border:"1px solid #d1d5db",borderRadius:8,padding:"8px 10px",fontSize:14,outline:"none"},button:{border:"none",background:"#111827",color:"#ffffff",borderRadius:8,padding:"8px 14px",fontSize:14,cursor:"pointer"}},oe=({artifact:e})=>{let t=k(e.dataBase64??"",e.mimeType),a=e.url??t;return a?e.mimeType.startsWith("image/")?H("a",{href:a,rel:"noopener noreferrer",target:"_blank",children:H("img",{alt:e.filename,src:a,style:b.image})}):H("a",{download:e.filename,href:a,style:b.link,children:e.filename}):null},se=()=>{let e=$(),{messages:t,input:a,setInput:s,send:i,sending:p,error:r}=M(e),o=re(null);te(()=>{let n=o.current;n&&(n.scrollTop=n.scrollHeight)},[t]);let d=n=>{n.preventDefault(),i()};return A("div",{style:b.root,children:[H("div",{ref:o,style:b.log,children:t.map(n=>A("div",{style:n.role==="user"?b.user:b.assistant,children:[n.text||(n.streaming?"\u2026":""),n.artifacts&&n.artifacts.length>0?H("div",{style:b.artifacts,children:n.artifacts.map(l=>H(oe,{artifact:l},l.filename))}):null]},n.id))}),r?H("div",{style:b.error,children:r}):null,A("form",{onSubmit:d,style:b.form,children:[H("input",{"aria-label":"Message",onChange:n=>s(n.target.value),placeholder:"Type a message\u2026",style:b.input,value:a}),H("button",{disabled:p||a.trim().length===0,style:b.button,type:"submit",children:"Send"})]})]})};var ne=/\/+$/,U=e=>{let t=e.sessionId,a=e.baseUrl.replace(ne,""),s=r=>`${a}/endpoint/${encodeURIComponent(e.token)}${r}`,i={"content-type":"application/json"},p=(r,o)=>({url:s(r),headers:i,body:JSON.stringify({message:o,...t?{sessionId:t}:{},...e.identity?{externalUserId:e.identity.externalUserId,userHmac:e.identity.userHmac}:{}})});return{sessionId:()=>t,getInfo:()=>x(s("/config"),{}),sendMessage:async r=>{let o=await _(p("/message",r));return t=o.sessionId,o},streamMessage:(r,o,d)=>F(p("/message/stream",r),o,n=>{t=n},d),subscribe:(r,o)=>{let d=o?.since??"0",n=!1,l,f=o?.intervalMs??3e3,u=async()=>{let y=t;if(y)try{let g=await x(s(`/messages?session=${encodeURIComponent(y)}&since=${encodeURIComponent(d)}`),{});for(let m of g.messages)r.onMessage(m);d=g.cursor}catch(g){r.onError?.(w(g))}n||(l=setTimeout(u,f))},c=()=>{n=!0,l&&clearTimeout(l)};return o?.signal?.addEventListener("abort",c),l=setTimeout(u,0),c},requestHuman:async()=>{t&&await fetch(s("/request-human"),{method:"POST",headers:i,body:JSON.stringify({sessionId:t})})}}};var ae=e=>"transport"in e?e.transport:U(e);var ie=/\/+$/,T="/api/v1/end-user";function le(e){let t="";for(let s=0;s<e.length;s+=32768)t+=String.fromCharCode(...e.subarray(s,s+32768));return btoa(t)}var pe=e=>{let t=e.baseUrl.replace(ie,""),a=()=>typeof e.token=="function"?e.token():e.token,s=async()=>({"content-type":"application/json",authorization:`Bearer ${await a()}`}),i=async(r,o)=>{try{let d=await fetch(`${t}${T}${r}`,{method:o.method,headers:await s(),...o.body===void 0?{}:{body:JSON.stringify(o.body)}});if(!d.ok){let n=await d.text().catch(()=>""),l=`${o.method} ${r} \u2192 ${String(d.status)}`;try{let f=JSON.parse(n);f.message&&(l=f.message)}catch{l=n?`${l}: ${n}`:l}throw w(new Error(l))}return await d.json()}catch(d){throw w(d)}},p=r=>`/tables/${encodeURIComponent(r)}`;return{listTables:async()=>(await x(`${t}${T}/tables`,await s())).tables,listRows:async(r,o)=>await x(`${t}${T}${p(r)}/rows?limit=${String(o??100)}`,await s()),insertRow:async(r,o)=>{let{rowIds:d}=await i(`${p(r)}/rows`,{method:"POST",body:{rows:[o]}}),n=d[0];if(!n)throw w(new Error("The row was not created."));return n},patchRow:async(r,o,d)=>(await i(`${p(r)}/rows/${encodeURIComponent(o)}`,{method:"PATCH",body:d})).updated,deleteRow:async(r,o)=>(await i(`/${encodeURIComponent(r)}/${encodeURIComponent(o)}`,{method:"DELETE"})).deleted,listFiles:async()=>(await x(`${t}${T}/tables/files/rows?limit=200`,await s())).rows.map(o=>({id:o.rowId,filename:String(o.data.filename??""),source:String(o.data.source??"client"),campaign:String(o.data.campaign??""),size:typeof o.data.size=="number"?o.data.size:0,createdAt:o.createdAt})),uploadFile:async(r,o)=>{let d=new Uint8Array(await r.arrayBuffer());return await i("/files",{method:"POST",body:{filename:r.name,mime:r.type||"application/octet-stream",data:le(d),...o===void 0?{}:{message:o}}})},fileUrl:r=>`${t}${T}/files/${encodeURIComponent(r)}/raw`,deleteFile:async r=>(await i(`/files/${encodeURIComponent(r)}`,{method:"DELETE"})).deleted}};import{useCallback as de,useEffect as W,useRef as ce,useState as P}from"react";function N(e,t){return e.some(s=>s.id===t.id)?e:[...t.author==="client"?e.filter(s=>!(s.pending&&s.author==="client"&&s.text===t.text)):e,t]}var ue=(e,t)=>{let[a,s]=P([]),[i,p]=P(""),[r,o]=P(!1),[d,n]=P(null),l=ce(0),f=t?.pollMs??2e3;W(()=>{let c=!0;return e.getInfo().then(y=>{c&&y.greeting&&s(g=>g.length===0?[{id:"greeting",cursor:"",author:"agent",text:y.greeting}]:g)}).catch(()=>{}),()=>{c=!1}},[e]),W(()=>e.subscribe?.({onMessage:c=>s(y=>N(y,c)),onError:c=>n(c.error)},{intervalMs:f}),[e,f]);let u=de(()=>{let c=i.trim();if(!c||r)return;p(""),n(null),o(!0),l.current+=1;let y={id:`pending-${l.current}`,cursor:"",author:"client",text:c,pending:!0};s(g=>[...g,y]),e.sendMessage(c).then(()=>o(!1)).catch(g=>{n(g instanceof Error?g.message:"send_failed"),o(!1)})},[i,r,e]);return{messages:a,input:i,setInput:p,send:u,sending:r,error:d}};export{se as Chat,Q as HolaProvider,B as createApiTransport,ae as createHolaClient,pe as createPortalData,U as createWidgetTransport,N as mergeMessage,R as textDeltaOf,E as thinkingDeltaOf,S as toolEventOf,k as useArtifactUrl,M as useChat,$ as useHolaClient,ue as useThread};
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@holaboss/client",
3
+ "version": "0.2.0-beta.0",
4
+ "description": "Injectable client SDK for embedding a Holaboss AI employee (chat today; dashboard + artifacts next) into your own web app.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "scripts": {
21
+ "dev": "tsup --watch",
22
+ "build": "tsup",
23
+ "check-types": "tsc --noEmit",
24
+ "prepublishOnly": "bun run build"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=18"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^19.1.13",
31
+ "@types/react-dom": "^19.1.9",
32
+ "tsconfig": "workspace:*",
33
+ "tsup": "^8.3.5",
34
+ "typescript": "catalog:"
35
+ },
36
+ "keywords": [
37
+ "ai",
38
+ "agent",
39
+ "employee",
40
+ "chat",
41
+ "sdk",
42
+ "holaboss",
43
+ "react"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/holaboss-ai/holaboss-frontend.git",
51
+ "directory": "packages/client"
52
+ }
53
+ }