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

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 (55) hide show
  1. package/README.md +47 -21
  2. package/dist/AgentAbout.js +6 -2
  3. package/dist/AgentAbout.js.map +1 -1
  4. package/dist/AgentChat.d.ts +7 -1
  5. package/dist/AgentChat.js +154 -2
  6. package/dist/AgentChat.js.map +1 -1
  7. package/dist/AgentSpace.d.ts +12 -3
  8. package/dist/AgentSpace.js +53 -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/automation/marks.js +7 -3
  14. package/dist/automation/marks.js.map +1 -1
  15. package/dist/automation/messages.d.ts +4 -0
  16. package/dist/automation/messages.js +5 -1
  17. package/dist/automation/messages.js.map +1 -1
  18. package/dist/automation/when.js +8 -0
  19. package/dist/automation/when.js.map +1 -1
  20. package/dist/client.d.ts +14 -4
  21. package/dist/client.js +7 -2
  22. package/dist/client.js.map +1 -1
  23. package/dist/contract.d.ts +9 -4
  24. package/dist/es.d.ts +2 -0
  25. package/dist/es.js +22 -3
  26. package/dist/es.js.map +1 -1
  27. package/dist/frames.d.ts +7 -1
  28. package/dist/frames.js.map +1 -1
  29. package/dist/index.d.ts +5 -4
  30. package/dist/index.js +5 -4
  31. package/dist/surface/Form.js +1 -1
  32. package/dist/types.d.ts +11 -0
  33. package/dist/types.js.map +1 -1
  34. package/dist/useConversation.d.ts +2 -0
  35. package/dist/useConversation.js +16 -0
  36. package/dist/useConversation.js.map +1 -1
  37. package/dist/widget.js +17 -16
  38. package/package.json +1 -1
  39. package/src/AgentAbout.tsx +4 -0
  40. package/src/AgentChat.tsx +203 -1
  41. package/src/AgentSpace.tsx +89 -13
  42. package/src/Threads.tsx +218 -0
  43. package/src/automation/marks.tsx +7 -0
  44. package/src/automation/messages.ts +8 -0
  45. package/src/automation/when.ts +4 -0
  46. package/src/client.ts +26 -10
  47. package/src/contract.ts +240 -26
  48. package/src/es.ts +21 -0
  49. package/src/frames.ts +3 -1
  50. package/src/index.ts +9 -1
  51. package/src/styles.css +191 -0
  52. package/src/types.ts +11 -0
  53. package/src/useConversation.ts +10 -0
  54. package/src/widget/element.tsx +13 -4
  55. package/src/widget/widget.css +13 -0
package/src/es.ts CHANGED
@@ -10,6 +10,7 @@ import type { TaskMessages } from "./AgentTasks";
10
10
  import type { AutomationMessages, DayRef } from "./automation/messages";
11
11
  import type { BrowserMessages } from "./BrowserView";
12
12
  import type { KitMessages } from "./kit/messages";
13
+ import type { ThreadMessages } from "./Threads";
13
14
  import type { VoiceCallMessages } from "./VoiceCall";
14
15
 
15
16
  /** The article before a time: "la 1:00", "las 8:00". */
@@ -76,6 +77,8 @@ export const AUTOMATION_MESSAGES_ES: AutomationMessages = {
76
77
  on: (often, when) => `${often}, ${when.charAt(0).toLowerCase()}${when.slice(1)}`,
77
78
  custom: "Con un horario propio",
78
79
  webhook: "Cuando otro sistema la llama",
80
+ manual: "Cuando alguien la inicia",
81
+ after: (automation) => (automation ? `Tras «${automation}»` : "Tras otra automatización"),
79
82
  platform: "Cuando llega un mensaje",
80
83
  event: (app, event) =>
81
84
  event
@@ -118,6 +121,8 @@ export const AUTOMATION_MESSAGES_ES: AutomationMessages = {
118
121
  platform: "un mensaje",
119
122
  agent: "se lo pidieron en el chat",
120
123
  test: "prueba",
124
+ api: "tu app",
125
+ after: "tras otra",
121
126
  })[by],
122
127
  statuses: {
123
128
  running: "En curso",
@@ -310,6 +315,10 @@ export const CHAT_MESSAGES_ES: ChatMessages = {
310
315
  inputs: {},
311
316
  allow: "Aprobar",
312
317
  decline: "Rechazar",
318
+ approvals: (count) => `${count} acciones esperan tu aprobación`,
319
+ approveAll: "Aprobar todo",
320
+ approveSome: (count) => `Aprobar ${count}`,
321
+ declineAll: "Rechazar todo",
313
322
  confirming: "Esperando la confirmación del negocio",
314
323
  handedOff: "Pasada a una persona",
315
324
  handover: (holder, by, name) => {
@@ -417,6 +426,17 @@ export const TASK_MESSAGES_ES: TaskMessages = {
417
426
  failed: "No se ha podido cambiar. Inténtalo de nuevo.",
418
427
  };
419
428
 
429
+ export const THREAD_MESSAGES_ES: ThreadMessages = {
430
+ open: "Conversaciones",
431
+ title: "Tus conversaciones",
432
+ back: "Volver a la conversación",
433
+ new: "Nueva conversación",
434
+ untitled: "Conversación nueva",
435
+ unread: (count) => (count === 1 ? "1 mensaje sin leer" : `${count} mensajes sin leer`),
436
+ waiting: "Te espera",
437
+ empty: "Aún no hay nada aquí.",
438
+ };
439
+
420
440
  export const SPACE_MESSAGES_ES: SpaceMessages = {
421
441
  modes: { label: "Modo", text: "Texto", voice: "Voz" },
422
442
  again: "Volver a llamar",
@@ -428,4 +448,5 @@ export const SPACE_MESSAGES_ES: SpaceMessages = {
428
448
  chat: CHAT_MESSAGES_ES,
429
449
  voice: VOICE_CALL_MESSAGES_ES,
430
450
  tasks: TASK_MESSAGES_ES,
451
+ threads: THREAD_MESSAGES_ES,
431
452
  };
package/src/frames.ts CHANGED
@@ -52,13 +52,15 @@ export type Frame =
52
52
 
53
53
  /**
54
54
  * Up: the person says something (words, files they uploaded to the conversation, or both), acts
55
- * on a surface, or stops the reply on its way; takes the browser or gives it back, and acts on it
55
+ * on a surface (several things at once: a yes or no on each call a reply waits on), or stops
56
+ * the reply on its way; takes the browser or gives it back, and acts on it
56
57
  * while they hold it. And what a tool of the host's answered, run in the page: its output, or
57
58
  * why it failed.
58
59
  */
59
60
  export type UpFrame =
60
61
  | { type: "say"; text: string; files?: string[] }
61
62
  | ({ type: "action" } & Action)
63
+ | { type: "actions"; actions: ({ type: "action" } & Action)[] }
62
64
  | { type: "stop" }
63
65
  | { type: "browser.control"; to: BrowserControl }
64
66
  | { type: "browser.close" }
package/src/index.ts CHANGED
@@ -97,6 +97,14 @@ export type {
97
97
  export { taskCard } from "./automation/types";
98
98
  export { AgentTasks, TASK_MESSAGES, TASKS_POLL_MS, useTasks } from "./AgentTasks";
99
99
  export type { AgentTasksProps, TaskMessages, TaskPart } from "./AgentTasks";
100
+ export { Conversation, THREAD_MESSAGES, THREADS_POLL_MS, Threads, useUnread } from "./Threads";
101
+ export type {
102
+ ConversationProps,
103
+ ThreadMessages,
104
+ ThreadPart,
105
+ ThreadsProps,
106
+ Unread,
107
+ } from "./Threads";
100
108
  export { AgentCard } from "./AgentCard";
101
109
  export type { AgentCardProps, CardField, CardPart } from "./AgentCard";
102
110
  export { AgentChat, CHAT_MESSAGES } from "./AgentChat";
@@ -110,6 +118,7 @@ export {
110
118
  KIT_MESSAGES_ES,
111
119
  SPACE_MESSAGES_ES,
112
120
  TASK_MESSAGES_ES,
121
+ THREAD_MESSAGES_ES,
113
122
  VOICE_CALL_MESSAGES_ES,
114
123
  } from "./es";
115
124
  export { VOICE_CALL_MESSAGES, VoiceCall } from "./VoiceCall";
@@ -235,7 +244,6 @@ export type {
235
244
  ChannelWeb,
236
245
  ContactGlance,
237
246
  ConversationPerson,
238
- Conversation,
239
247
  ConversationDetail,
240
248
  ConversationFile,
241
249
  Decides,
package/src/styles.css CHANGED
@@ -633,6 +633,45 @@
633
633
  line-height: 1.375;
634
634
  }
635
635
 
636
+ /* Several actions waiting for the person's leave, in one card: a row for each, its box ticked
637
+ and what it would take under it, editable; one button at the foot. */
638
+ .sc-chat__requests > ul {
639
+ display: flex;
640
+ flex-direction: column;
641
+ margin: 0;
642
+ padding: 0;
643
+ list-style: none;
644
+ }
645
+
646
+ .sc-chat__ask {
647
+ display: flex;
648
+ flex-direction: column;
649
+ gap: calc(var(--_space) * 2.5);
650
+ padding: calc(var(--_space) * 3) calc(var(--_space) * 4);
651
+ }
652
+
653
+ .sc-chat__ask + .sc-chat__ask {
654
+ border-top: 1px solid var(--_line);
655
+ }
656
+
657
+ .sc-chat__ask > .sc-check > span {
658
+ font-weight: var(--_font-weight-medium);
659
+ line-height: 1.375;
660
+ }
661
+
662
+ .sc-chat__ask-fields {
663
+ display: grid;
664
+ gap: calc(var(--_space) * 2.5);
665
+ padding-left: calc(var(--_space) * 6.5);
666
+ }
667
+
668
+ .sc-chat__requests > footer {
669
+ display: flex;
670
+ justify-content: flex-end;
671
+ padding: calc(var(--_space) * 3) calc(var(--_space) * 4);
672
+ border-top: 1px solid var(--_line);
673
+ }
674
+
636
675
  /* On a person's page, in place of the approval card: what waits for the business's yes, a quiet
637
676
  line under the steps; the person has nothing to press. */
638
677
  .sc-chat__confirming {
@@ -6427,6 +6466,158 @@ button.sc-attachment__main:focus-visible {
6427
6466
  outline-offset: -2px;
6428
6467
  }
6429
6468
 
6469
+ /* Over the chat, one line: the way to the person's threads (a dot when another has something
6470
+ for them) and, at its end, their tasks. */
6471
+ .sc-space__bar {
6472
+ display: flex;
6473
+ flex-shrink: 0;
6474
+ align-items: stretch;
6475
+ border-bottom: 1px solid var(--_line);
6476
+ }
6477
+
6478
+ .sc-space__bar > .sc-space__threads,
6479
+ .sc-space__bar > .sc-space__tasks {
6480
+ width: auto;
6481
+ border-bottom: 0;
6482
+ }
6483
+
6484
+ .sc-space__bar > .sc-space__tasks:not(:only-child) {
6485
+ margin-left: auto;
6486
+ text-align: right;
6487
+ }
6488
+
6489
+ /* The way to the person's threads; a dot when another thread has something for them. */
6490
+ .sc-space__threads {
6491
+ display: flex;
6492
+ flex-shrink: 0;
6493
+ align-items: center;
6494
+ gap: calc(var(--_space) * 1.5);
6495
+ width: 100%;
6496
+ padding: calc(var(--_space) * 2) calc(var(--_space) * 3.5);
6497
+ border: 0;
6498
+ border-bottom: 1px solid var(--_line);
6499
+ background: transparent;
6500
+ color: var(--_muted);
6501
+ font: inherit;
6502
+ font-size: var(--_font-size-xs);
6503
+ font-weight: var(--_font-weight-medium);
6504
+ text-align: left;
6505
+ cursor: pointer;
6506
+ transition:
6507
+ background-color 0.12s,
6508
+ color 0.12s;
6509
+ }
6510
+
6511
+ .sc-space__threads:hover {
6512
+ background: var(--_wash);
6513
+ color: var(--_fg);
6514
+ }
6515
+
6516
+ .sc-space__threads:focus-visible {
6517
+ outline: 2px solid var(--_accent);
6518
+ outline-offset: -2px;
6519
+ }
6520
+
6521
+ .sc-space__dot,
6522
+ .sc-threads__dot {
6523
+ flex-shrink: 0;
6524
+ width: 7px;
6525
+ height: 7px;
6526
+ border-radius: var(--_radius-full);
6527
+ background: var(--_accent);
6528
+ }
6529
+
6530
+ /* The person's threads in the chat's place: the way back, their title and another to start, then
6531
+ each by its title with what is unread. */
6532
+ .sc-threads {
6533
+ display: flex;
6534
+ flex: 1;
6535
+ flex-direction: column;
6536
+ min-height: 0;
6537
+ overflow-y: auto;
6538
+ }
6539
+
6540
+ .sc-threads__head {
6541
+ display: flex;
6542
+ flex-shrink: 0;
6543
+ align-items: center;
6544
+ gap: calc(var(--_space) * 1.5);
6545
+ padding: calc(var(--_space) * 2) calc(var(--_space) * 3);
6546
+ border-bottom: 1px solid var(--_line);
6547
+ }
6548
+
6549
+ .sc-threads__title {
6550
+ flex: 1;
6551
+ margin: 0;
6552
+ font-size: var(--_font-size);
6553
+ font-weight: var(--_font-weight-bold);
6554
+ }
6555
+
6556
+ .sc-threads__list {
6557
+ display: flex;
6558
+ flex-direction: column;
6559
+ margin: 0;
6560
+ padding: calc(var(--_space) * 1.5);
6561
+ list-style: none;
6562
+ }
6563
+
6564
+ .sc-threads__item {
6565
+ display: flex;
6566
+ align-items: center;
6567
+ gap: calc(var(--_space) * 2);
6568
+ width: 100%;
6569
+ padding: calc(var(--_space) * 2.5) calc(var(--_space) * 2.5);
6570
+ border: 0;
6571
+ border-radius: var(--_radius);
6572
+ background: transparent;
6573
+ color: var(--_fg);
6574
+ font: inherit;
6575
+ text-align: left;
6576
+ cursor: pointer;
6577
+ transition: background-color 0.12s;
6578
+ }
6579
+
6580
+ .sc-threads__item:hover {
6581
+ background: var(--_wash);
6582
+ }
6583
+
6584
+ .sc-threads__item:focus-visible {
6585
+ outline: 2px solid var(--_accent);
6586
+ outline-offset: -2px;
6587
+ }
6588
+
6589
+ .sc-threads__item--current {
6590
+ background: var(--_wash);
6591
+ font-weight: var(--_font-weight-medium);
6592
+ }
6593
+
6594
+ .sc-threads__name {
6595
+ flex: 1;
6596
+ min-width: 0;
6597
+ overflow: hidden;
6598
+ text-overflow: ellipsis;
6599
+ white-space: nowrap;
6600
+ }
6601
+
6602
+ .sc-threads__badge {
6603
+ flex-shrink: 0;
6604
+ min-width: 20px;
6605
+ padding: 1px calc(var(--_space) * 1.5);
6606
+ border-radius: var(--_radius-full);
6607
+ background: var(--_accent);
6608
+ color: var(--_accent-fg);
6609
+ font-size: var(--_font-size-xs);
6610
+ font-variant-numeric: tabular-nums;
6611
+ font-weight: var(--_font-weight-medium);
6612
+ text-align: center;
6613
+ }
6614
+
6615
+ .sc-threads__empty {
6616
+ margin: 0;
6617
+ padding: calc(var(--_space) * 4) calc(var(--_space) * 3.5);
6618
+ color: var(--_muted);
6619
+ }
6620
+
6430
6621
  /* The tasks in the chat's place: the way back and their title, then each as an automation
6431
6622
  reads, a hairline between them. */
6432
6623
  .sc-tasks {
package/src/types.ts CHANGED
@@ -90,6 +90,12 @@ export interface Conversation {
90
90
  * host's app (`host_person`, its `contact` then) or the widget's visitor.
91
91
  */
92
92
  person?: ConversationPerson | null | undefined;
93
+ /**
94
+ * For the person it is with: how many of the business's messages came since they last read it,
95
+ * and whether it waits on them (a question, their yes); left out by an older API.
96
+ */
97
+ unread?: number | undefined;
98
+ waiting_on_person?: boolean | undefined;
93
99
  }
94
100
 
95
101
  /** Who a person's token or a publishable key started a conversation for. */
@@ -237,6 +243,8 @@ export interface Request {
237
243
  input: Record<string, unknown>;
238
244
  /** The person's when left out (an API from before it). */
239
245
  decides?: Decides | undefined;
246
+ /** The JSON Schema of its input, which an edit given with the yes must fit. */
247
+ schema?: Record<string, unknown> | null | undefined;
240
248
  }
241
249
 
242
250
  /** An action a reply called: what, how it went, and what it drew. */
@@ -306,6 +314,9 @@ export interface Message {
306
314
  heard?: string | null | undefined;
307
315
  /** On a channel, a person's message: what was decided of it; null otherwise. */
308
316
  decision?: Decision | null | undefined;
317
+ /** The agent's words first, without a turn of the person's, and the run whose code sent them. */
318
+ proactive?: boolean | undefined;
319
+ run_id?: string | null | undefined;
309
320
  created_at: string;
310
321
  }
311
322
 
@@ -444,6 +444,8 @@ export interface LiveConversation {
444
444
  say: (text: string, files?: readonly string[]) => boolean;
445
445
  /** What the person did on a surface; false when the socket is closed. */
446
446
  act: (action: Action) => boolean;
447
+ /** Several things done at once (a yes or no on each call a reply waits on), sent together. */
448
+ actAll: (actions: readonly Action[]) => boolean;
447
449
  /** Stops the reply on its way; false when the socket is closed. */
448
450
  stop: () => boolean;
449
451
  /** The conversation's browser while it is open: where it is, who holds it. */
@@ -605,6 +607,13 @@ export function useConversation(
605
607
  return true;
606
608
  };
607
609
 
610
+ const actAll = (actions: readonly Action[]): boolean => {
611
+ const sent = actions.map((one) => ({ type: "action" as const, ...one }));
612
+ if (!live.send({ type: "actions", actions: sent })) return false;
613
+ for (const action of actions) dispatch({ type: "acted", action });
614
+ return true;
615
+ };
616
+
608
617
  return {
609
618
  messages: state.messages,
610
619
  draft: state.draft,
@@ -615,6 +624,7 @@ export function useConversation(
615
624
  error: state.error,
616
625
  say,
617
626
  act,
627
+ actAll,
618
628
  stop: () => live.send({ type: "stop" }),
619
629
  browser: state.browser,
620
630
  watchScreen: screen.watch,
@@ -5,6 +5,7 @@ import { createRoot, type Root } from "react-dom/client";
5
5
 
6
6
  import type { ChatSteps } from "../AgentChat";
7
7
  import { AgentSpace, SPACE_MESSAGES } from "../AgentSpace";
8
+ import { useUnread } from "../Threads";
8
9
  import { createClient } from "../client";
9
10
  import type { Credentials } from "../credentials";
10
11
  import { SPACE_MESSAGES_ES } from "../es";
@@ -61,9 +62,11 @@ export interface WidgetSettings {
61
62
  }
62
63
 
63
64
  /** The launcher's words, by language. */
64
- const LAUNCHER: Readonly<Record<Language, { open: string; close: string; chat: string }>> = {
65
- en: { open: "Open the chat", close: "Close the chat", chat: "Chat" },
66
- es: { open: "Abrir el chat", close: "Cerrar el chat", chat: "Chat" },
65
+ const LAUNCHER: Readonly<
66
+ Record<Language, { open: string; close: string; chat: string; news: string }>
67
+ > = {
68
+ en: { open: "Open the chat", close: "Close the chat", chat: "Chat", news: "something new" },
69
+ es: { open: "Abrir el chat", close: "Cerrar el chat", chat: "Chat", news: "hay novedades" },
67
70
  };
68
71
 
69
72
  /**
@@ -254,6 +257,11 @@ export function Widget({ settings, look = null }: WidgetProps) {
254
257
  const [open, setOpen] = useState(false);
255
258
  const words = LAUNCHER[language];
256
259
  const floating = mode === "floating";
260
+ // While closed, a dot on the launcher says something waits (an unread message, a question), for
261
+ // someone who talked before: nobody new is asked after until they open it.
262
+ const returning = keptConversation(agent) !== undefined;
263
+ const news = useUnread(client, agent, floating && !open && returning && client.who === "person");
264
+ const waits = news.unread > 0 || news.waiting;
257
265
  const header = settings.header || look?.header === true;
258
266
 
259
267
  const space = (
@@ -295,7 +303,7 @@ export function Widget({ settings, look = null }: WidgetProps) {
295
303
  <button
296
304
  type="button"
297
305
  part="launcher"
298
- aria-label={open ? words.close : words.open}
306
+ aria-label={open ? words.close : waits ? `${words.open}: ${words.news}` : words.open}
299
307
  aria-expanded={open}
300
308
  onClick={() => {
301
309
  setOpen(!open);
@@ -307,6 +315,7 @@ export function Widget({ settings, look = null }: WidgetProps) {
307
315
  ) : (
308
316
  <ChatCircleDotsIcon aria-hidden weight="fill" />
309
317
  )}
318
+ {!open && waits && <span aria-hidden className="sc-widget__dot" />}
310
319
  </button>
311
320
  </div>
312
321
  );
@@ -104,6 +104,19 @@ signalcore-chat[shadow="none"][mode="inline"] {
104
104
  height: 46%;
105
105
  }
106
106
 
107
+ /* While the chat is closed and something waits in it (an unread message, a question): a dot at
108
+ the launcher's corner, ringed in the page's surface so it reads on the accent. */
109
+ .sc-widget__dot {
110
+ position: absolute;
111
+ top: 6%;
112
+ right: 6%;
113
+ width: 22%;
114
+ height: 22%;
115
+ border: 2px solid var(--_surface);
116
+ border-radius: var(--_radius-full);
117
+ background: var(--_danger);
118
+ }
119
+
107
120
  .sc-widget--floating > .sc-widget__panel {
108
121
  position: fixed;
109
122
  right: var(--_offset);