@signalcore/ui 1.0.0-beta.2 → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +126 -21
  2. package/dist/AgentAbout.js +6 -2
  3. package/dist/AgentAbout.js.map +1 -1
  4. package/dist/AgentChat.d.ts +25 -3
  5. package/dist/AgentChat.js +302 -28
  6. package/dist/AgentChat.js.map +1 -1
  7. package/dist/AgentSpace.d.ts +25 -4
  8. package/dist/AgentSpace.js +55 -11
  9. package/dist/AgentSpace.js.map +1 -1
  10. package/dist/Threads.d.ts +73 -0
  11. package/dist/Threads.js +140 -0
  12. package/dist/Threads.js.map +1 -0
  13. package/dist/View.d.ts +48 -0
  14. package/dist/View.js +101 -0
  15. package/dist/View.js.map +1 -0
  16. package/dist/automation/marks.js +7 -3
  17. package/dist/automation/marks.js.map +1 -1
  18. package/dist/automation/messages.d.ts +4 -0
  19. package/dist/automation/messages.js +5 -1
  20. package/dist/automation/messages.js.map +1 -1
  21. package/dist/automation/when.js +8 -0
  22. package/dist/automation/when.js.map +1 -1
  23. package/dist/blocks.d.ts +83 -2
  24. package/dist/client.d.ts +23 -5
  25. package/dist/client.js +8 -2
  26. package/dist/client.js.map +1 -1
  27. package/dist/contract.d.ts +9 -4
  28. package/dist/es.d.ts +2 -0
  29. package/dist/es.js +34 -3
  30. package/dist/es.js.map +1 -1
  31. package/dist/frames.d.ts +7 -1
  32. package/dist/frames.js.map +1 -1
  33. package/dist/index.d.ts +14 -7
  34. package/dist/index.js +11 -4
  35. package/dist/kit/Agenda.d.ts +25 -0
  36. package/dist/kit/Agenda.js +119 -0
  37. package/dist/kit/Agenda.js.map +1 -0
  38. package/dist/kit/Combobox.js +1 -1
  39. package/dist/kit/DateField.js +1 -1
  40. package/dist/kit/Kanban.d.ts +17 -0
  41. package/dist/kit/Kanban.js +157 -0
  42. package/dist/kit/Kanban.js.map +1 -0
  43. package/dist/kit/TimeField.js +1 -1
  44. package/dist/kit/Timeline.d.ts +14 -0
  45. package/dist/kit/Timeline.js +71 -0
  46. package/dist/kit/Timeline.js.map +1 -0
  47. package/dist/kit/context.d.ts +1 -1
  48. package/dist/kit/context.js.map +1 -1
  49. package/dist/kit/messages.d.ts +22 -0
  50. package/dist/kit/messages.js +10 -0
  51. package/dist/kit/messages.js.map +1 -1
  52. package/dist/surface/Form.js +1 -1
  53. package/dist/surface/Placed.d.ts +20 -0
  54. package/dist/surface/Placed.js +70 -0
  55. package/dist/surface/Placed.js.map +1 -0
  56. package/dist/surface/Surface.d.ts +4 -2
  57. package/dist/surface/Surface.js +78 -2
  58. package/dist/surface/Surface.js.map +1 -1
  59. package/dist/surface/places.d.ts +16 -0
  60. package/dist/surface/places.js +15 -0
  61. package/dist/surface/places.js.map +1 -0
  62. package/dist/surface/scope.d.ts +17 -1
  63. package/dist/surface/scope.js +1 -0
  64. package/dist/surface/scope.js.map +1 -1
  65. package/dist/types.d.ts +23 -0
  66. package/dist/types.js.map +1 -1
  67. package/dist/useConversation.d.ts +2 -0
  68. package/dist/useConversation.js +16 -0
  69. package/dist/useConversation.js.map +1 -1
  70. package/dist/widget.js +17 -16
  71. package/package.json +1 -1
  72. package/src/AgentAbout.tsx +4 -0
  73. package/src/AgentChat.tsx +417 -34
  74. package/src/AgentSpace.tsx +106 -14
  75. package/src/Threads.tsx +218 -0
  76. package/src/View.tsx +117 -0
  77. package/src/automation/marks.tsx +7 -0
  78. package/src/automation/messages.ts +8 -0
  79. package/src/automation/when.ts +4 -0
  80. package/src/blocks.ts +100 -2
  81. package/src/client.ts +42 -10
  82. package/src/contract.ts +404 -29
  83. package/src/es.ts +30 -0
  84. package/src/frames.ts +3 -1
  85. package/src/index.ts +36 -3
  86. package/src/kit/Agenda.tsx +161 -0
  87. package/src/kit/Kanban.tsx +182 -0
  88. package/src/kit/Timeline.tsx +58 -0
  89. package/src/kit/context.tsx +45 -1
  90. package/src/kit/messages.ts +31 -0
  91. package/src/styles.css +825 -66
  92. package/src/surface/Placed.tsx +98 -0
  93. package/src/surface/Surface.tsx +92 -1
  94. package/src/surface/places.ts +19 -0
  95. package/src/surface/scope.ts +24 -1
  96. package/src/types.ts +24 -0
  97. package/src/useConversation.ts +10 -0
  98. package/src/widget/element.tsx +54 -7
  99. package/src/widget/widget.css +13 -0
package/README.md CHANGED
@@ -185,6 +185,62 @@ couldn't begin again), `close`, `agent` (its name when the face can't be read),
185
185
  (`--text`, `--voice`; `aria-checked` on the one on), `sc-space__caption` (`--person`, `--agent`),
186
186
  `sc-space__status`.
187
187
 
188
+ ### Panels, modals and your own blocks
189
+
190
+ What the agent shows can stand apart from the conversation (`show` and `ask_person` with
191
+ `where`, v3/UI.md, Surfaces): in a **panel** beside the chat (over it, where the space is narrow:
192
+ the rep's week while they talk), or in a **modal** over the page (something to answer now; once
193
+ used it closes and the chat goes on). Each opens as it comes, and a conversation that opens
194
+ waiting on one opens it; the reply keeps a quiet line, "Ray abrió tu semana" (`opened`), that
195
+ opens it again. Your page decides what it allows: `surfaces={{ panel: true, modal: false }}`
196
+ (both by default); a place left out shows in the conversation, as on a channel. `AgentChat`
197
+ takes the same `surfaces`.
198
+
199
+ `blocks` registers your own blocks by the name each was declared with (`PUT /v1/blocks/{name}`):
200
+ `blocks={{ "demoprep.deal-card": DealCard }}`. The agent gives only props, which the API checked
201
+ against your schema; your component gets `{props, text, active, onAction}` (`HostBlockProps`),
202
+ and a block it has no component for shows its words in a quiet card (`sc-surface__host`).
203
+
204
+ ```tsx
205
+ function DealCard({ props }: HostBlockProps<{ title: string; amount: number }>) {
206
+ return <YourDealCard title={props.title} amount={props.amount} />;
207
+ }
208
+
209
+ <AgentSpace
210
+ client={client}
211
+ agentId="agt_…"
212
+ surfaces={{ panel: true, modal: true }}
213
+ blocks={{ "demoprep.deal-card": DealCard }}
214
+ />;
215
+ ```
216
+
217
+ Words: `opened(name, title)`, `closePlaced`. Parts: `sc-chat__placed` (`--open`), `sc-chat--placed`
218
+ (a panel is open), `sc-placed` (`--panel`, `--modal`, the latter a `<dialog>`),
219
+ `sc-placed__head`, `sc-placed__title`, `sc-placed__body`.
220
+
221
+ ### Threads, unread, and several yeses at once
222
+
223
+ A person keeps several conversations (threads) with the agent, each with its title: one your
224
+ backend opened for them (`conversations.start` with `contact`, `title`, `say`), one they began.
225
+ `AgentSpace` shows a line over the chat, **Conversaciones**, with a dot while another thread has
226
+ something for them; it lists them by title with how many of the agent's messages are unread, to
227
+ go to one or start another (`threads={false}` turns it off). The chat reads what comes as it
228
+ shows it on a person's page (`POST /conversations/{id}/read`), so `unread` follows; the widget's
229
+ launcher wears a dot while something waits, for someone who talked before.
230
+
231
+ For your own layout: `useUnread(client, agentId)` (`{threads, unread, waiting, refresh}`, read
232
+ every 20 s and when the page comes back into sight), `<Threads threads current onSelect onNew
233
+ onBack />` and `<Conversation client id … />` (the chat of one thread, `AgentChat`'s props).
234
+ Words: `THREAD_MESSAGES`, `THREAD_MESSAGES_ES`. Parts: `sc-threads`, `sc-threads__head`,
235
+ `sc-threads__title`, `sc-threads__list`, `sc-threads__item` (`--current`), `sc-threads__name`,
236
+ `sc-threads__badge`, `sc-threads__dot`, `sc-space__threads`, `sc-space__dot`.
237
+
238
+ When one reply waits on the person's yes for several actions, they come in one card
239
+ (`sc-chat__requests`): a box ticked for each, what each would take under it, editable, and one
240
+ button, **Aprobar todo** (or as many as are ticked), sending a yes for each ticked (with what was
241
+ changed: `data.input`) and a no for the rest, in one `actions` frame. Words: `approvals`,
242
+ `approveAll`, `approveSome`, `declineAll`.
243
+
188
244
  ## The widget
189
245
 
190
246
  `<signalcore-chat>` for pages without React: one script, React and the stylesheet inside it,
@@ -234,6 +290,10 @@ flow at the element's size (560px tall unless your CSS says). A visitor's conver
234
290
  | `stylesheet` | your own CSS for the widget: the CSS itself (anything with a `{`) or its address; adopted inside the shadow root, after ours. Also the element's `stylesheet` property |
235
291
  | `unstyled` | our look's defaults drop away (grounds, lines, shadows, corners, every ink but the page's text), the layout stays: for a host that styles everything |
236
292
  | `shadow` | `none`: no shadow root; the widget draws in the page (our stylesheet goes first in `<head>`, so your CSS wins), for a host whose global CSS should apply directly. Read once, as the element connects |
293
+ | `surfaces` | where the agent may put what it shows besides the conversation: `panel`, `modal`, both (`"panel modal"`, the default) or none (`""`) |
294
+
295
+ Your own blocks go as the element's `blocks` property, React components the widget's React
296
+ draws: `document.querySelector("signalcore-chat").blocks = { "demoprep.deal-card": DealCard }`.
237
297
 
238
298
  By default the widget takes your page's type and ink: the font, the base size and the text colour
239
299
  the element inherits come through the shadow root and stand in for our defaults (read as it draws,
@@ -317,23 +377,26 @@ from its credentials: `"business"` (our app's cookie) or `"person"` (a token, a
317
377
  visitor); the chat draws a yes the business decides by it, and shows a person none of the
318
378
  agent's steps (`steps`).
319
379
 
320
- | Method | Does |
321
- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
322
- | `agents.face(id)` | what a person may know of the agent (`GET /agents/{id}/face`): `AgentFace {id, name, role, icon, voice: {enabled}}` |
323
- | `agents.about(id)` | what the agent says about itself (`about`, null until written; `about_at`, null once stale) and its `facts` |
324
- | `agents.describe(id)` | has the agent write about itself anew (a model call, a few seconds); answers like `about` |
325
- | `agents.files(id)` | the agent's own files (`StoredFile`: `id, path, media_type, size, searchable, created_at`), by path |
326
- | `agents.putFile(id, path, blob, searchable)` | stores the blob's bytes under the path (replacing a file of that name), its type as the file's; `searchable` makes it knowledge `search_files` reads (text, Markdown, JSON, YAML, PDF). 50 MB at most, 5 GB per organization |
327
- | `agents.deleteFile(id, path)` | removes one of the agent's own files |
328
- | `agents.memories(id)` | what the agent remembered (`MemoryFact`: `id, text, created_at`), newest first |
329
- | `agents.forget(id, memoryId)` | takes one memory from it |
330
- | `agents.call(id)` | the owner's call with the agent from our app (`POST /agents/{id}/voice/token`, `/api` only): `CallToken {url, token, room, call_id, conversation_id}`, LiveKit's address and a token for the call's room, and the call's conversation (a test), made now |
331
- | `files.list()` · `files.put(path, blob, searchable)` · `files.delete(path)` | the organization's files, shared with every agent: the same three as the agent's own |
332
- | `conversations.get(id)` | one conversation with its messages, who answers it (`handled_by`: `agent`, `person` or `nobody`) and every change of it (`handovers`); on a channel, `channel`, `contact` (`muted`) and each message's `author`, `delivery` and `decision` |
333
- | `conversations.start(agentId)` | a new conversation with an agent |
334
- | `conversations.call(agentId)` | a call with the agent for whoever may talk to it, a person's as theirs (`POST /agents/{id}/calls`): a `CallToken`, what `VoiceCall`'s `connect` answers, with the call's own conversation, made now, where what is said lands |
335
- | `channels.list(agentId?)` | the organization's channels, or one agent's (`Channel`: `id, agent_id, kind, provider, name, external_id, status, detail, settings, created_at`), the disconnected ones too |
336
- | `liveUrl(conversationId, { fresh? })` | the socket's address, signed in (a promise): `ws:` beside `http:` and `wss:` beside `https:`, with `?token=` (and `&visitor=`) but in our app; `fresh` asks `getToken` for a new token first |
380
+ | Method | Does |
381
+ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
382
+ | `agents.face(id)` | what a person may know of the agent (`GET /agents/{id}/face`): `AgentFace {id, name, role, icon, voice: {enabled}}` |
383
+ | `agents.about(id)` | what the agent says about itself (`about`, null until written; `about_at`, null once stale) and its `facts` |
384
+ | `agents.describe(id)` | has the agent write about itself anew (a model call, a few seconds); answers like `about` |
385
+ | `agents.files(id)` | the agent's own files (`StoredFile`: `id, path, media_type, size, searchable, created_at`), by path |
386
+ | `agents.putFile(id, path, blob, searchable)` | stores the blob's bytes under the path (replacing a file of that name), its type as the file's; `searchable` makes it knowledge `search_files` reads (text, Markdown, JSON, YAML, PDF). 50 MB at most, 5 GB per organization |
387
+ | `agents.deleteFile(id, path)` | removes one of the agent's own files |
388
+ | `agents.memories(id)` | what the agent remembered (`MemoryFact`: `id, text, created_at`), newest first |
389
+ | `agents.forget(id, memoryId)` | takes one memory from it |
390
+ | `agents.call(id)` | **app only** (`POST /agents/{id}/voice/token` is `/api`'s, 404 on `/v1`): the owner's call with the agent from our app, `CallToken {url, token, room, call_id, conversation_id}`, LiveKit's address and a token for the call's room, and the call's conversation (a test), made now. A host's page uses `conversations.call(agentId)` |
391
+ | `files.list()` · `files.put(path, blob, searchable)` · `files.delete(path)` | the organization's files, shared with every agent: the same three as the agent's own |
392
+ | `conversations.get(id)` | one conversation with its messages, who answers it (`handled_by`: `agent`, `person` or `nobody`) and every change of it (`handovers`); on a channel, `channel`, `contact` (`muted`) and each message's `author`, `delivery` and `decision` |
393
+ | `conversations.list(agentId)` | the agent's conversations, newest first (a person's own), each with `title`, `unread` and `waiting_on_person` |
394
+ | `conversations.start(agentId, { title })` | a new conversation with an agent, titled when given |
395
+ | `conversations.read(id)` | how far the person has read it: all of it, now (the chat calls it as it shows what comes) |
396
+ | `conversations.rename(id, title)` | its title, as the person gives it |
397
+ | `conversations.call(agentId)` | a call with the agent for whoever may talk to it, a person's as theirs (`POST /agents/{id}/calls`): a `CallToken`, what `VoiceCall`'s `connect` answers, with the call's own conversation, made now, where what is said lands |
398
+ | `channels.list(agentId?)` | the organization's channels, or one agent's (`Channel`: `id, agent_id, kind, provider, name, external_id, status, detail, settings, created_at`), the disconnected ones too |
399
+ | `liveUrl(conversationId, { fresh? })` | the socket's address, signed in (a promise): `ws:` beside `http:` and `wss:` beside `https:`, with `?token=` (and `&visitor=`) but in our app; `fresh` asks `getToken` for a new token first |
337
400
 
338
401
  ## The frames
339
402
 
@@ -378,15 +441,15 @@ from how the call's promise settled).
378
441
  the page and its answer goes up (each call once; one told again is answered again with what it
379
442
  gave; the turn stays under way until its step settles); without it each is refused with
380
443
  `NO_TOOL`. It answers `{ messages, draft, pending,
381
- awaiting, open, loading, error, say(text), act(action), stop(), browser,
444
+ awaiting, open, loading, error, say(text), act(action), actAll(actions), stop(), browser,
382
445
  watchScreen(listener), control(to), input(event), holder, handovers }`. The history, read each
383
446
  time the socket opens and kept live by its frames;
384
447
  `pending` from the person's message to `done`, `stopped` or a turn error, `draft` the reply so
385
448
  far. What `step`, `ui` and `request` frames say lands on the reply's steps, so a message is
386
449
  the one place a step is drawn from, live or read back. `awaiting` is the step the turn waits
387
450
  on: a surface to answer, or an action waiting for the person's leave (the step's `request`,
388
- `decides: "person"`; `act` on its id with `approve` or `decline`); `say` is refused until they
389
- act. A yes the business gives (`decides: "business"`) waits on its step and holds nothing; the
451
+ `decides: "person"`; `act` on its id with `approve` or `decline`, `actAll` on several at once,
452
+ a yes's `data.input` changing what it runs with); `say` is refused until they act. A yes the business gives (`decides: "business"`) waits on its step and holds nothing; the
390
453
  person's answer on one, refused `business`, puts it back as the business's. `onError(code)`
391
454
  hears the socket's codes and `load` (the
392
455
  history could not be read): the host words them, the hook shows no text; `tool` (an answer of
@@ -509,7 +572,7 @@ an agent, the one a customer has on the phone, through LiveKit (`livekit-client`
509
572
  when it mounts and ends when it unmounts (key it on the attempt for another). It opens the
510
573
  microphone first (no call is made for someone who can't be heard), then asks `connect()` where to
511
574
  join (`{url, token}`: our API's `client.conversations.call(agentId)` answers both, with the
512
- call's conversation; `client.agents.call(id)` for the owner's test), joins the room and publishes the microphone; the agent's voice plays as it comes,
575
+ call's conversation; `client.agents.call(id)` for the owner's test in our own app, never on `/v1`), joins the room and publishes the microphone; the agent's voice plays as it comes,
513
576
  and the call is on once the agent joins (its time counts from then).
514
577
 
515
578
  What it draws, centred: the `Orb`, large (`orbSize`, 280 px; `orb`: `"wave"`, the default, or
@@ -705,6 +768,48 @@ works, a check and "Conectada" in its place (`sc-surface__connect--done`), and a
705
768
  it failed. It changes as the connection does, from the `ui` frames the API sends again on the
706
769
  same id; before the API has asked for it (`connection` null), only the app as the agent named it.
707
770
 
771
+ The kit's blocks of time and work: a `calendar` (`Agenda`: a day's events, or a week's from its
772
+ `start`, at their hours; one with a `url` opens its page, with the calendar's `action` the rest
773
+ send `{event}`; narrow, the days go one under another, an empty one a quiet line; 680px and up,
774
+ the week stands as seven columns), a `timeline` (`Timeline`: dated things the newest first, each
775
+ a dot on a hairline, in its `tone` when it has one, its `status` a quiet word), a `board`
776
+ (`Kanban`: cards in columns; with its `action`, a card dragged to another column, or moved from
777
+ its button's list of the others, goes there at once and sends `{card, column}`, labelled
778
+ `board.moved`). A `host` block is one of yours, drawn by the component `blocks` registers under
779
+ its `name` (a component that fails as it draws leaves its words, and the rest draws). Words:
780
+ `calendar` (`allDay`, `empty`), `board` (`move`, `moveTo`, `moved`, `empty`). Parts:
781
+ `sc-agenda` (`--day`), `sc-agenda__days`, `sc-agenda__day` (`--today`, `--empty`),
782
+ `sc-agenda__date`, `sc-agenda__weekday`, `sc-agenda__number`, `sc-agenda__events`,
783
+ `sc-agenda__event`, `sc-agenda__time`, `sc-agenda__what`, `sc-agenda__title`,
784
+ `sc-agenda__detail`, `sc-agenda__empty`; `sc-timeline`, `sc-timeline__item` (`--info`,
785
+ `--success`, `--warning`, `--danger`), `sc-timeline__dot`, `sc-timeline__body`,
786
+ `sc-timeline__head`, `sc-timeline__title`, `sc-timeline__status`, `sc-timeline__when`,
787
+ `sc-timeline__text`; `sc-board`, `sc-board__column` (`--over`), `sc-board__head`,
788
+ `sc-board__title`, `sc-board__count`, `sc-board__cards`, `sc-board__card` (`--dragging`),
789
+ `sc-board__card-title`, `sc-board__card-subtitle`, `sc-board__card-meta`, `sc-board__move`,
790
+ `sc-board__menu`, `sc-board__empty`.
791
+
792
+ ## View
793
+
794
+ `<View client name contact? blocks? components? classNames? messages? locale? onAction? />` draws
795
+ an agent's view where your page puts it: what the agent keeps on show for each person (their
796
+ next meeting, their week), a dashboard's spec rendered for one contact with their own rows
797
+ (`GET /views/{name}/render`). With a person's token it is theirs; for the business, `contact`
798
+ names whom (their id). It is read when shown and again every `VIEW_POLL_MS` (30 s), so it shows
799
+ what the rows hold now; while it is first read an empty box holds its place, and a view that
800
+ isn't there (or isn't the person's) draws nothing. `useView(client, name, contact?)` gives
801
+ `{view, problem, gone}` for your own layout (`view.title`, `view.blocks`, `view.text`).
802
+
803
+ ```tsx
804
+ <View
805
+ client={client}
806
+ name="proxima_reunion"
807
+ blocks={{ "demoprep.deal-card": DealCard }}
808
+ messages={KIT_MESSAGES_ES}
809
+ locale={es}
810
+ />
811
+ ```
812
+
708
813
  ## AgentAbout
709
814
 
710
815
  `<AgentAbout client agentId name messages? classNames? controls? apps? locale? view? onViewChange? onOpenFile? onOpenConversation? />`: Qué puede, made to sit
@@ -45,7 +45,9 @@ const ABOUT_MESSAGES = {
45
45
  schedule: "At a time",
46
46
  webhook: "When called",
47
47
  event: "On an event",
48
- platform: "On a message"
48
+ platform: "On a message",
49
+ manual: "When started",
50
+ after: "After another"
49
51
  },
50
52
  topics: {
51
53
  apps: "Apps",
@@ -576,7 +578,9 @@ const KIND_ICONS = {
576
578
  schedule: "clock",
577
579
  webhook: "code",
578
580
  event: "bell",
579
- platform: "message"
581
+ platform: "message",
582
+ manual: "hand",
583
+ after: "check"
580
584
  };
581
585
  /** An app's mark: the host's logo when it draws one, else the provider's, bare. */
582
586
  function appMark(app, apps, size) {