@signalcore/ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +566 -2
- package/dist/SignalcoreProvider.d.ts +113 -0
- package/dist/SignalcoreProvider.js +84 -0
- package/dist/automations/AppMarks.d.ts +11 -0
- package/dist/automations/AppMarks.js +18 -0
- package/dist/automations/AutomationCard.d.ts +27 -0
- package/dist/automations/AutomationCard.js +53 -0
- package/dist/automations/AutomationsWindow.d.ts +38 -0
- package/dist/automations/AutomationsWindow.js +57 -0
- package/dist/automations/Card.d.ts +46 -0
- package/dist/automations/Card.js +45 -0
- package/dist/automations/ScheduleMarks.d.ts +13 -0
- package/dist/automations/ScheduleMarks.js +24 -0
- package/dist/automations/presentationIcons.d.ts +7 -0
- package/dist/automations/presentationIcons.js +67 -0
- package/dist/automations/schedule.d.ts +25 -0
- package/dist/automations/schedule.js +59 -0
- package/dist/automations/time.d.ts +3 -0
- package/dist/automations/time.js +25 -0
- package/dist/browser/BrowserView.d.ts +43 -0
- package/dist/browser/BrowserView.js +94 -0
- package/dist/browser/room.d.ts +21 -0
- package/dist/browser/room.js +33 -0
- package/dist/browser/useBrowserSession.d.ts +17 -0
- package/dist/browser/useBrowserSession.js +67 -0
- package/dist/builder/BuilderChat.d.ts +32 -0
- package/dist/builder/BuilderChat.js +41 -0
- package/dist/builder/BuilderTranscript.d.ts +49 -0
- package/dist/builder/BuilderTranscript.js +62 -0
- package/dist/builder/StatusLine.d.ts +8 -0
- package/dist/builder/StatusLine.js +17 -0
- package/dist/builder/answer.d.ts +7 -0
- package/dist/builder/answer.js +8 -0
- package/dist/builder/blocks.d.ts +31 -0
- package/dist/builder/blocks.js +74 -0
- package/dist/builder/items.d.ts +15 -0
- package/dist/builder/items.js +21 -0
- package/dist/builder/steps.d.ts +20 -0
- package/dist/builder/steps.js +120 -0
- package/dist/builder/time.d.ts +4 -0
- package/dist/builder/time.js +24 -0
- package/dist/builder/useBuildSession.d.ts +36 -0
- package/dist/builder/useBuildSession.js +130 -0
- package/dist/builder/workingOn.d.ts +7 -0
- package/dist/builder/workingOn.js +15 -0
- package/dist/chat/AgentChat.d.ts +37 -0
- package/dist/chat/AgentChat.js +21 -0
- package/dist/chat/ChatView.d.ts +25 -0
- package/dist/chat/ChatView.js +34 -0
- package/dist/chat/Composer.d.ts +20 -0
- package/dist/chat/Composer.js +22 -0
- package/dist/chat/MessageRow.d.ts +22 -0
- package/dist/chat/MessageRow.js +32 -0
- package/dist/chat/arrival.d.ts +15 -0
- package/dist/chat/arrival.js +25 -0
- package/dist/chat/history.d.ts +24 -0
- package/dist/chat/history.js +45 -0
- package/dist/chat/moments.d.ts +25 -0
- package/dist/chat/moments.js +72 -0
- package/dist/chat/types.d.ts +12 -0
- package/dist/chat/types.js +1 -0
- package/dist/chat/useComposer.d.ts +46 -0
- package/dist/chat/useComposer.js +62 -0
- package/dist/chat/useConversation.d.ts +35 -0
- package/dist/chat/useConversation.js +71 -0
- package/dist/chat/useStickToBottom.d.ts +9 -0
- package/dist/chat/useStickToBottom.js +33 -0
- package/dist/chat/useTurns.d.ts +49 -0
- package/dist/chat/useTurns.js +111 -0
- package/dist/classes.d.ts +2 -0
- package/dist/classes.js +4 -0
- package/dist/client.d.ts +65 -0
- package/dist/client.js +1 -0
- package/dist/development.d.ts +5 -0
- package/dist/development.js +8 -0
- package/dist/flow/FlowWindow.d.ts +42 -0
- package/dist/flow/FlowWindow.js +126 -0
- package/dist/flow/index.d.ts +16 -0
- package/dist/flow/index.js +16 -0
- package/dist/flow/kinds.d.ts +23 -0
- package/dist/flow/kinds.js +48 -0
- package/dist/flow/map.d.ts +44 -0
- package/dist/flow/map.js +225 -0
- package/dist/flow/nodePaths.d.ts +7 -0
- package/dist/flow/nodePaths.js +17 -0
- package/dist/flow/relations.d.ts +42 -0
- package/dist/flow/relations.js +174 -0
- package/dist/host.d.ts +124 -0
- package/dist/host.js +1 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +45 -0
- package/dist/live/LiveView.d.ts +28 -0
- package/dist/live/LiveView.js +55 -0
- package/dist/live/capture.d.ts +20 -0
- package/dist/live/capture.js +100 -0
- package/dist/live/follow.d.ts +26 -0
- package/dist/live/follow.js +72 -0
- package/dist/live/paint.d.ts +13 -0
- package/dist/live/paint.js +77 -0
- package/dist/live/useLiveStream.d.ts +16 -0
- package/dist/live/useLiveStream.js +57 -0
- package/dist/look.d.ts +33 -0
- package/dist/look.js +241 -0
- package/dist/messages/en.d.ts +3 -0
- package/dist/messages/en.js +251 -0
- package/dist/messages/es.d.ts +3 -0
- package/dist/messages/es.js +251 -0
- package/dist/messages/index.d.ts +16 -0
- package/dist/messages/index.js +33 -0
- package/dist/messages/types.d.ts +246 -0
- package/dist/messages/types.js +1 -0
- package/dist/parts/Button.d.ts +18 -0
- package/dist/parts/Button.js +17 -0
- package/dist/parts/Icon.d.ts +39 -0
- package/dist/parts/Icon.js +37 -0
- package/dist/parts/Input.d.ts +6 -0
- package/dist/parts/Input.js +11 -0
- package/dist/parts/Spinner.d.ts +2 -0
- package/dist/parts/Spinner.js +7 -0
- package/dist/patterns.d.ts +15 -0
- package/dist/patterns.js +5 -0
- package/dist/presets.d.ts +82 -0
- package/dist/presets.js +81 -0
- package/dist/primitives.d.ts +22 -0
- package/dist/primitives.js +58 -0
- package/dist/request/RequestCard.d.ts +34 -0
- package/dist/request/RequestCard.js +101 -0
- package/dist/request/approval/ApprovalPreview.d.ts +5 -0
- package/dist/request/approval/ApprovalPreview.js +27 -0
- package/dist/request/approval/shapes.d.ts +32 -0
- package/dist/request/approval/shapes.js +68 -0
- package/dist/request/apps.d.ts +11 -0
- package/dist/request/apps.js +27 -0
- package/dist/request/cards.d.ts +12 -0
- package/dist/request/cards.js +24 -0
- package/dist/request/deliver.d.ts +12 -0
- package/dist/request/deliver.js +21 -0
- package/dist/request/handOver.d.ts +18 -0
- package/dist/request/handOver.js +33 -0
- package/dist/request/kinds/Approval.d.ts +3 -0
- package/dist/request/kinds/Approval.js +9 -0
- package/dist/request/kinds/Choice.d.ts +3 -0
- package/dist/request/kinds/Choice.js +8 -0
- package/dist/request/kinds/Connect.d.ts +9 -0
- package/dist/request/kinds/Connect.js +119 -0
- package/dist/request/kinds/FreeText.d.ts +3 -0
- package/dist/request/kinds/FreeText.js +20 -0
- package/dist/request/kinds/HandOver.d.ts +13 -0
- package/dist/request/kinds/HandOver.js +20 -0
- package/dist/request/kinds/Secret.d.ts +7 -0
- package/dist/request/kinds/Secret.js +49 -0
- package/dist/request/kinds/Upload.d.ts +6 -0
- package/dist/request/kinds/Upload.js +47 -0
- package/dist/request/types.d.ts +83 -0
- package/dist/request/types.js +1 -0
- package/dist/safeUrl.d.ts +6 -0
- package/dist/safeUrl.js +8 -0
- package/dist/stream/builder.d.ts +176 -0
- package/dist/stream/builder.js +202 -0
- package/dist/stream/reduce.d.ts +51 -0
- package/dist/stream/reduce.js +58 -0
- package/dist/stream/untilAborted.d.ts +7 -0
- package/dist/stream/untilAborted.js +32 -0
- package/dist/stream/useRunStream.d.ts +26 -0
- package/dist/stream/useRunStream.js +137 -0
- package/dist/styles.css +2318 -0
- package/dist/surface/HostNode.d.ts +14 -0
- package/dist/surface/HostNode.js +30 -0
- package/dist/surface/Node.d.ts +7 -0
- package/dist/surface/Node.js +49 -0
- package/dist/surface/Surface.d.ts +21 -0
- package/dist/surface/Surface.js +58 -0
- package/dist/surface/SurfaceBoundary.d.ts +26 -0
- package/dist/surface/SurfaceBoundary.js +20 -0
- package/dist/surface/bindings.d.ts +14 -0
- package/dist/surface/bindings.js +37 -0
- package/dist/surface/check.d.ts +27 -0
- package/dist/surface/check.js +102 -0
- package/dist/surface/format.d.ts +10 -0
- package/dist/surface/format.js +32 -0
- package/dist/surface/hosts.d.ts +6 -0
- package/dist/surface/hosts.js +11 -0
- package/dist/surface/scene.d.ts +21 -0
- package/dist/surface/scene.js +8 -0
- package/dist/surface/templates/Browser.d.ts +6 -0
- package/dist/surface/templates/Browser.js +11 -0
- package/dist/surface/templates/Chart.d.ts +6 -0
- package/dist/surface/templates/Chart.js +13 -0
- package/dist/surface/templates/Choices.d.ts +3 -0
- package/dist/surface/templates/Choices.js +12 -0
- package/dist/surface/templates/Form.d.ts +3 -0
- package/dist/surface/templates/Form.js +44 -0
- package/dist/surface/templates/KeyValue.d.ts +3 -0
- package/dist/surface/templates/KeyValue.js +7 -0
- package/dist/surface/templates/Layout.d.ts +5 -0
- package/dist/surface/templates/Layout.js +16 -0
- package/dist/surface/templates/Live.d.ts +6 -0
- package/dist/surface/templates/Live.js +9 -0
- package/dist/surface/templates/Media.d.ts +5 -0
- package/dist/surface/templates/Media.js +13 -0
- package/dist/surface/templates/Slots.d.ts +3 -0
- package/dist/surface/templates/Slots.js +19 -0
- package/dist/surface/templates/Table.d.ts +3 -0
- package/dist/surface/templates/Table.js +12 -0
- package/dist/surface/templates/Text.d.ts +4 -0
- package/dist/surface/templates/Text.js +15 -0
- package/dist/surface/templates/index.d.ts +7 -0
- package/dist/surface/templates/index.js +32 -0
- package/dist/surface/types.d.ts +68 -0
- package/dist/surface/types.js +6 -0
- package/dist/theme.d.ts +62 -0
- package/dist/theme.js +9 -0
- package/dist/useTokens.d.ts +7 -0
- package/dist/useTokens.js +41 -0
- package/dist/voice/Orb.d.ts +11 -0
- package/dist/voice/Orb.js +42 -0
- package/dist/voice/VoiceWindow.d.ts +18 -0
- package/dist/voice/VoiceWindow.js +35 -0
- package/dist/voice/index.d.ts +13 -0
- package/dist/voice/index.js +12 -0
- package/dist/voice/meter.d.ts +16 -0
- package/dist/voice/meter.js +52 -0
- package/dist/voice/orbColors.d.ts +16 -0
- package/dist/voice/orbColors.js +75 -0
- package/dist/voice/pushable.d.ts +16 -0
- package/dist/voice/pushable.js +56 -0
- package/dist/voice/useDictation.d.ts +24 -0
- package/dist/voice/useDictation.js +60 -0
- package/dist/voice/useReducedMotion.d.ts +5 -0
- package/dist/voice/useReducedMotion.js +19 -0
- package/dist/voice/useVoice.d.ts +70 -0
- package/dist/voice/useVoice.js +197 -0
- package/dist/voice/useVoicePrimitives.d.ts +7 -0
- package/dist/voice/useVoicePrimitives.js +11 -0
- package/dist/voice/voice.d.ts +55 -0
- package/dist/voice/voice.js +9 -0
- package/dist/voice/webSpeech.d.ts +33 -0
- package/dist/voice/webSpeech.js +258 -0
- package/dist/widget.js +13 -0
- package/dist/widget.js.map +1 -0
- package/dist/windows/AgentSpace.d.ts +78 -0
- package/dist/windows/AgentSpace.js +135 -0
- package/dist/windows/Captions.d.ts +13 -0
- package/dist/windows/Captions.js +22 -0
- package/dist/windows/Dock.d.ts +37 -0
- package/dist/windows/Dock.js +22 -0
- package/dist/windows/Journal.d.ts +15 -0
- package/dist/windows/Journal.js +19 -0
- package/dist/windows/KindMark.d.ts +7 -0
- package/dist/windows/KindMark.js +31 -0
- package/dist/windows/Pending.d.ts +8 -0
- package/dist/windows/Pending.js +17 -0
- package/dist/windows/Stage.d.ts +18 -0
- package/dist/windows/Stage.js +21 -0
- package/dist/windows/StatusLabel.d.ts +9 -0
- package/dist/windows/StatusLabel.js +7 -0
- package/dist/windows/Strip.d.ts +17 -0
- package/dist/windows/Strip.js +42 -0
- package/dist/windows/SurfaceWindow.d.ts +20 -0
- package/dist/windows/SurfaceWindow.js +12 -0
- package/dist/windows/WindowFrame.d.ts +23 -0
- package/dist/windows/WindowFrame.js +22 -0
- package/dist/windows/hearing.d.ts +11 -0
- package/dist/windows/hearing.js +26 -0
- package/dist/windows/kinds.d.ts +47 -0
- package/dist/windows/kinds.js +28 -0
- package/dist/windows/reduce.d.ts +98 -0
- package/dist/windows/reduce.js +151 -0
- package/dist/windows/registry.d.ts +18 -0
- package/dist/windows/registry.js +61 -0
- package/dist/windows/reveal.d.ts +9 -0
- package/dist/windows/reveal.js +6 -0
- package/dist/windows/space.d.ts +21 -0
- package/dist/windows/space.js +6 -0
- package/dist/windows/status.d.ts +11 -0
- package/dist/windows/status.js +30 -0
- package/dist/windows/types.d.ts +15 -0
- package/dist/windows/types.js +1 -0
- package/dist/windows/useLoaded.d.ts +44 -0
- package/dist/windows/useLoaded.js +58 -0
- package/dist/windows/useUnread.d.ts +7 -0
- package/dist/windows/useUnread.js +21 -0
- package/dist/windows/useWindows.d.ts +70 -0
- package/dist/windows/useWindows.js +60 -0
- package/package.json +81 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SignalCore
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,567 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @signalcore/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SignalCore's interface as React components: what an agent shows (`Surface`), what it asks the
|
|
4
|
+
owner (`RequestCard`), and the two chats built from them (`AgentChat` for the people an agent
|
|
5
|
+
serves, `BuilderChat` for the owner who shapes it), drawn exactly the way the product draws them.
|
|
6
|
+
The web app uses this package, so what you embed is what we ship. For a page that is not React
|
|
7
|
+
there is a `<signalcore-chat>` [widget](#the-widget).
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @signalcore/ui @signalcore/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Needs React 19. The styles are one CSS file. Besides the SDK, the only dependencies are the
|
|
14
|
+
graph library of the agent's map (`@xyflow/react`, which only `@signalcore/ui/flow` imports, see
|
|
15
|
+
[Windows](#windows-of-an-agent)) and the orb of the voice window (`orb-ui`, MIT, which only
|
|
16
|
+
`@signalcore/ui/voice` loads).
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { Signalcore } from "@signalcore/sdk";
|
|
20
|
+
import { RequestCard, SignalcoreProvider, Surface, useRunStream } from "@signalcore/ui";
|
|
21
|
+
import "@signalcore/ui/styles.css";
|
|
22
|
+
|
|
23
|
+
const client = new Signalcore({ apiKey });
|
|
24
|
+
|
|
25
|
+
function Run({ runId }: { runId: string }) {
|
|
26
|
+
const run = useRunStream(runId);
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
{run.surfaces.map((surface) => (
|
|
30
|
+
<Surface key={surface.id} surface={surface} onAction={(action) => send(action)} />
|
|
31
|
+
))}
|
|
32
|
+
{run.requests.map((card) => (
|
|
33
|
+
<RequestCard key={card.request_id} card={card} />
|
|
34
|
+
))}
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
<SignalcoreProvider client={client}>
|
|
40
|
+
<Run runId="run_..." />
|
|
41
|
+
</SignalcoreProvider>;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## The two chats
|
|
45
|
+
|
|
46
|
+
One renderer, two chats, split by who is talking (ADR 0009). Both reach the API through the
|
|
47
|
+
provider's `client`, and both are made of parts a host can compose.
|
|
48
|
+
|
|
49
|
+
**`AgentChat`** is the chat of the people an agent **serves**: their messages and the agent's, the
|
|
50
|
+
agent's surfaces drawn inline (with their text twin where they cannot be), and a box to write in.
|
|
51
|
+
It speaks to the agent as the person and can never change it.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { Signalcore } from "@signalcore/sdk";
|
|
55
|
+
import { AgentChat, SignalcoreProvider } from "@signalcore/ui";
|
|
56
|
+
import "@signalcore/ui/styles.css";
|
|
57
|
+
|
|
58
|
+
// In the browser: a token your backend minted for this person (never a secret key).
|
|
59
|
+
const client = new Signalcore({ endUserToken });
|
|
60
|
+
|
|
61
|
+
<SignalcoreProvider client={client}>
|
|
62
|
+
<AgentChat agentId="agt_…" thread="order-7" placeholder="Ask about your order" />
|
|
63
|
+
</SignalcoreProvider>;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Prop | What |
|
|
67
|
+
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `agentId` | The agent the person talks to. |
|
|
69
|
+
| `thread` | The conversation to speak in; the agent remembers it. The API's default if none. |
|
|
70
|
+
| `placeholder` | The box's hint. `autoFocus` focuses it. |
|
|
71
|
+
| `onSurfaceAction` | Tells you what the person picked or submitted. The chat has already sent it. |
|
|
72
|
+
| `loadHistory` | `(thread) => Promise<ChatMessage[]>`. Without it the chat reads the caller's own conversation through `endUsers.myConversation` when the client has it (an end user's token or a publishable key do), and otherwise starts empty. `messagesFromConversation` turns the API's messages into `ChatMessage`s. |
|
|
73
|
+
|
|
74
|
+
- A message is sent with `agents.invoke({ input, thread })`; a pick or a form on a surface goes
|
|
75
|
+
back as `agents.invoke({ action, thread })`, shown as what the person said ("Martes 10:00"). Only
|
|
76
|
+
the last thing said takes a pick: once the person answers, a surface is a record.
|
|
77
|
+
- **It never shows an approval, a connection or a secret.** When the answer has `waiting_for`, the
|
|
78
|
+
run paused for the owner to confirm something: the person sees a quiet "waiting for a
|
|
79
|
+
confirmation", the chat follows the run (`useRunStream`) until it is done, and then shows what
|
|
80
|
+
arrived. The send button is not usable while a turn runs; the box stays free to write in.
|
|
81
|
+
- Accessible: new words from the agent go to a live region, the box and the conversation are
|
|
82
|
+
labelled, and everything works from the keyboard (Enter sends, Shift+Enter breaks the line).
|
|
83
|
+
|
|
84
|
+
**`BuilderChat`** is the chat of the owner who **shapes** the agent: their messages, what the
|
|
85
|
+
builder says and does as it works (steps open to what came back), the surfaces it shows, the cards
|
|
86
|
+
it asks them to answer in place, and what changed.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
<BuilderChat agentId="agt_…" sessionId={sessionId} onSession={setSessionId} />
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A turn streams as it happens and has a stop button (Esc too); answering a card tells the builder in
|
|
93
|
+
the owner's words; reopening a conversation replays it with the cards still waiting. It needs a
|
|
94
|
+
client with the builder's calls (`build.sessions`, `requests`), which `Signalcore` with an owner's
|
|
95
|
+
key has. A host that draws the conversation its own way builds it from what `BuilderChat` is made
|
|
96
|
+
of:
|
|
97
|
+
|
|
98
|
+
- `useBuildSession({ agentId, sessionId, onSession, model, onEvent, onTurnEnd })` gives the
|
|
99
|
+
transcript, `loading`, `send` and `stop`;
|
|
100
|
+
- `reduceTranscript` is the pure fold over the builder's events behind it (live and replayed
|
|
101
|
+
history give the same transcript);
|
|
102
|
+
- `BuilderTranscript` draws the blocks, with `renderers` for the ones that are only content
|
|
103
|
+
(Markdown for the builder's words, a card that activates a change): the request cards and the
|
|
104
|
+
surfaces are always the platform's;
|
|
105
|
+
- `StatusLine`, `Composer` (with slots for `tools` and `context`, and a held `value`), and
|
|
106
|
+
`describeStep` complete it.
|
|
107
|
+
|
|
108
|
+
## Voice
|
|
109
|
+
|
|
110
|
+
`VoiceWindow` is a conversation by voice with an agent (ADR 0011): an orb that listens, thinks and
|
|
111
|
+
speaks, the state in words under it, and captions of what was heard and said. The person chooses
|
|
112
|
+
it by pressing the button; the agent never starts it. It is its own entry, so a page that never
|
|
113
|
+
speaks does not load the orb:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { SignalcoreProvider } from "@signalcore/ui";
|
|
117
|
+
import { VoiceWindow } from "@signalcore/ui/voice";
|
|
118
|
+
|
|
119
|
+
<SignalcoreProvider client={client}>
|
|
120
|
+
<VoiceWindow agentId="agt_…" thread="order-7" onSurfaces={(surfaces) => place(surfaces)} />
|
|
121
|
+
</SignalcoreProvider>;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- Its turns are the chat's (`agents.invoke`, in the thread) with `modality: "voice"`, so the agent
|
|
125
|
+
keeps its answer short and puts the detail in a surface, which goes to `onSurfaces`: the window
|
|
126
|
+
draws none. Like the chat, it never shows an approval, a connection or a secret.
|
|
127
|
+
- `useVoice({ agentId, thread, voice, lang, continuous, onTurn, onSurfaces })` is the loop behind
|
|
128
|
+
it (listen, send, think, speak, listen again) for a host that lays it out itself: `state`,
|
|
129
|
+
`level`, `transcript`, `captions`, `start`, `stop`, `mute`.
|
|
130
|
+
- A `Voice` is how to listen and speak: `listen()` (an async iterable of what is said, interim
|
|
131
|
+
then final), `speak(text)`, their stops, and an observable `state` and `level`. `webSpeechVoice`
|
|
132
|
+
is the browser's own (`SpeechRecognition` and `speechSynthesis`) and the default; a provider's
|
|
133
|
+
transport comes behind the same interface. The level is the microphone's while listening and an
|
|
134
|
+
estimate from the words while speaking, since `speechSynthesis` has no audio to measure.
|
|
135
|
+
- `Orb` is a replaceable primitive (`components={{ Orb }}`, props `state`, `level`, `size`,
|
|
136
|
+
`label`, `className`). The default is the `circle` theme of `orb-ui` in your tokens; under
|
|
137
|
+
`prefers-reduced-motion` it is a still disc in a ring whose outline says the state.
|
|
138
|
+
- `useDictation({ lang, onFinal })` (from the core entry) dictates into a message box with the
|
|
139
|
+
same browser voice.
|
|
140
|
+
|
|
141
|
+
## The widget
|
|
142
|
+
|
|
143
|
+
`<signalcore-chat>` is `AgentChat` as a web component in a shadow root, for pages that are not
|
|
144
|
+
React. One script, with React and the SDK bundled in (`dist/widget.js`, about 108 kB gzipped):
|
|
145
|
+
|
|
146
|
+
```html
|
|
147
|
+
<script src="https://signalcore.ai/app/widget.js"></script>
|
|
148
|
+
|
|
149
|
+
<!-- An anonymous visitor: a publishable key, bound to the origins it lists. -->
|
|
150
|
+
<signalcore-chat agent="agt_…" publishable-key="pk_live_…"></signalcore-chat>
|
|
151
|
+
|
|
152
|
+
<!-- A person of your product: a token your backend minted for them. -->
|
|
153
|
+
<signalcore-chat agent="agt_…" end-user-token="…" thread="order-7"></signalcore-chat>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Attribute | What |
|
|
157
|
+
| -------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `agent`, `thread`, `placeholder` | What `AgentChat` takes. `autofocus` focuses the box. |
|
|
159
|
+
| `end-user-token` | The SDK's `endUserToken`. |
|
|
160
|
+
| `publishable-key`, `visitor-id` | The SDK's `publishableKey` and `visitorId`. Without a `visitor-id` the widget keeps one in this browser. |
|
|
161
|
+
| `base-url` | Where the API is (`https://signalcore.ai` by default). |
|
|
162
|
+
| `locale` | `es` (the default) or `en`: the language of its words and dates. See [Words](#words). |
|
|
163
|
+
|
|
164
|
+
The `client` property, set from JavaScript, wins over the attributes: give it any `UiClient` (an SDK
|
|
165
|
+
client with other options, your own proxy). A secret key is not an option here, on purpose. The
|
|
166
|
+
`messages` property, also from JavaScript, changes single words (see [Words](#words)).
|
|
167
|
+
|
|
168
|
+
It draws in a shadow root with the package's styles inside, so the page and the widget do not
|
|
169
|
+
touch. The page themes it from outside:
|
|
170
|
+
|
|
171
|
+
- **Tokens.** `--sc-*` custom properties on the element or above it inherit in
|
|
172
|
+
(`signalcore-chat { --sc-accent: #b4532a; height: 600px }`), and so do the page's font and text
|
|
173
|
+
color. The element is `display: block; height: 32rem` until your CSS says otherwise.
|
|
174
|
+
- **Dark mode.** The widget follows a `data-theme` of `light` or `dark` on itself or the nearest
|
|
175
|
+
ancestor (usually `<html>`), and `prefers-color-scheme` without one.
|
|
176
|
+
- **Parts.** Every slot is a part: `signalcore-chat::part(root)`, `::part(composer)`,
|
|
177
|
+
`::part(message)` (and `::part(message--person)`, `::part(message--agent)`), and the rest of
|
|
178
|
+
`SLOTS`.
|
|
179
|
+
- **Events.** `surface-action` (bubbles, composed) carries what the person picked in `detail`.
|
|
180
|
+
|
|
181
|
+
**`<signalcore-space>`** is [`AgentSpace`](#the-shell-agentspace) in the same script, taking the
|
|
182
|
+
same credentials: `<signalcore-space agent="agt_…" end-user-token="…" windows="flow" mode="text">`.
|
|
183
|
+
`windows` lists the app's windows to open (the first takes the stage), `mode` is `text` or `voice`
|
|
184
|
+
(the element writes the person's choice back and dispatches `mode-change`), and `viewer` is
|
|
185
|
+
`end_user` (the default: a widget serves the people of your product) or `owner`, for a page that
|
|
186
|
+
sets an owner's `client`. The script carries the conversation, the browser, the live stream and the
|
|
187
|
+
pages the agent places, and neither the flow map nor the voice: those windows show what the agent
|
|
188
|
+
said they show, and the voice stays on text. It is `display: block; height: 36rem` by default.
|
|
189
|
+
|
|
190
|
+
If it cannot start (no agent, credentials that make no sense) it shows a quiet line and says why in
|
|
191
|
+
the console. `pnpm build` produces `dist/widget.js` after the library; `import "@signalcore/ui/widget"`
|
|
192
|
+
registers the element from a bundler instead. From JavaScript,
|
|
193
|
+
`defineSignalcoreChat({ tag, createClient })` registers it under another tag, or with your own way
|
|
194
|
+
of making the client.
|
|
195
|
+
|
|
196
|
+
## What it draws
|
|
197
|
+
|
|
198
|
+
**`Surface`** draws a surface of the `signalcore-basic.v1` catalog: cards, key-values, tables,
|
|
199
|
+
charts, callouts, images, links, choice lists, slot pickers, forms, a live browser view and live
|
|
200
|
+
streams. A
|
|
201
|
+
surface is data, never code: it draws only catalog components (and the host's own, see
|
|
202
|
+
[Components](#your-own-components)), resolves their bindings, and shows only `https://` addresses.
|
|
203
|
+
|
|
204
|
+
- What the user does (a pick, a form, a button of your own component) calls
|
|
205
|
+
`onAction({ surface_id, name, data, label })`, the shape the API's `Action` expects. Without
|
|
206
|
+
`onAction` the surface is a record: nothing on it can be pressed.
|
|
207
|
+
- **Every surface carries a text twin** (`fallback_text`). A surface whose payload is damaged, that
|
|
208
|
+
names a component nobody here knows (or one of yours that this page has not registered), or whose
|
|
209
|
+
drawing throws shows its text twin instead, and the rest of your page carries on.
|
|
210
|
+
- The browser view frames a live page in a sandbox, only from an `https://` address.
|
|
211
|
+
|
|
212
|
+
**`RequestCard`** draws what an agent needs from the owner: a question, a choice, an app to
|
|
213
|
+
connect, a file, an approval, a secret, or the controls of its browser. These are the owner's
|
|
214
|
+
decisions about safety, so the platform draws them and nobody else can: an agent cannot fake an
|
|
215
|
+
"Aprobar" button because the catalog has no such component, and **a host cannot replace the card
|
|
216
|
+
either**. It follows levels 1 to 4 of [Styling](#styling) (tokens, presets, `classNames`,
|
|
217
|
+
`unstyled`) and takes `apps` to say how your apps look; the provider's `components` never reach it.
|
|
218
|
+
|
|
219
|
+
Answering goes through the provider's `client` (`requests.resolve`; a secret goes to `vault.put`, a
|
|
220
|
+
file to `documents.addFile`, an app is checked with `connections.refresh`). Without a client,
|
|
221
|
+
`onResolve` is how the answer gets anywhere. Secrets and files never pass through your callbacks.
|
|
222
|
+
|
|
223
|
+
**`useRunStream(runId)`** follows a run through `client.runs.stream` and gives you the events, the
|
|
224
|
+
surfaces and the requests (as cards) it accumulates, and `status` (`connecting`, `streaming`,
|
|
225
|
+
`paused`, `done`, `failed`). When the run pauses on a request it closes the stream and opens it
|
|
226
|
+
again after the last event once the request is answered, wherever that happened. A host that never
|
|
227
|
+
draws the cards passes `{ cards: false }`, and the API is not asked what they say.
|
|
228
|
+
|
|
229
|
+
## Windows of an agent
|
|
230
|
+
|
|
231
|
+
The chats show an agent talking; a window shows the agent itself, as its owner reads it. The web
|
|
232
|
+
app's agent page and map are these two, drawn by the platform from facts and never generated. They
|
|
233
|
+
are **owner windows**: they read the business's overview and the agent's package, so they need an
|
|
234
|
+
owner's credential (`agents:read`), and an end user's token gets a `403`.
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
import { AutomationsWindow, SignalcoreProvider } from "@signalcore/ui";
|
|
238
|
+
import { FlowWindow } from "@signalcore/ui/flow";
|
|
239
|
+
|
|
240
|
+
<SignalcoreProvider client={client}>
|
|
241
|
+
<AutomationsWindow agentId="agt_..." onOpenFlow={showSteps} onAsk={tellTheBuilder} />
|
|
242
|
+
<div style={{ height: 480 }}>
|
|
243
|
+
<FlowWindow agentId="agt_..." onOpen={showPart} />
|
|
244
|
+
</div>
|
|
245
|
+
</SignalcoreProvider>;
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
- **`AutomationsWindow`** is what the agent does: a card for each channel it answers on and each
|
|
249
|
+
automation, with what a model wrote about the part over what the platform knows (when it runs,
|
|
250
|
+
its apps, its state, how the last run went). `focusedId`, `attention` and `building` follow the
|
|
251
|
+
builder to the automation it is working on. `AutomationCard`, `ScheduleMarks` and `AppMarks` are
|
|
252
|
+
its parts, exported for a page you lay out yourself.
|
|
253
|
+
- **`FlowWindow`** is the agent as a map of its channels, automations, skills, actions, records and
|
|
254
|
+
apps, with a line for each call, read from its package. A part is picked with a press (`onSelect`,
|
|
255
|
+
`selectedId`) and opened with a second one or Enter (`onOpen`). It is in its own entry because
|
|
256
|
+
`@xyflow/react` and its stylesheet are not small: the core entry imports nothing from it, and a
|
|
257
|
+
test keeps it that way. `agentMap`, `relationsOf` and `kindOf` are exported from it as plain
|
|
258
|
+
functions.
|
|
259
|
+
- **They fetch for themselves** when given `agentId` and the client has `agents.overview` (and
|
|
260
|
+
`agents.get` for the map's files); they say they are loading, or that they could not load. What
|
|
261
|
+
is passed as props (`overview`, `files`) is what is drawn, and nothing is asked. `apps` says how
|
|
262
|
+
your apps look, as in `RequestCard`.
|
|
263
|
+
- They follow levels 1 to 4 of [Styling](#styling); their slots are `automations`, `automation`,
|
|
264
|
+
`schedule`, `app-marks`, `flow`, `flow__node` and `status`.
|
|
265
|
+
|
|
266
|
+
## The shell: `AgentSpace`
|
|
267
|
+
|
|
268
|
+
Everything a person sees of an agent is a window ([ADR 0011](../../docs/adr/0011-windows.md)), and
|
|
269
|
+
`AgentSpace` is where they sit: a **stage** with the one that matters now, a **strip** of the open
|
|
270
|
+
ones and a **dock** to talk from. The conversation is always one of them. The agent adds more as it
|
|
271
|
+
works, placing a page of its own (`show` with a `window`: a generated window, a surface) or opening
|
|
272
|
+
one of the app's (`open_window`: `automations`, `flow`, `voice`), and the person closes and reopens
|
|
273
|
+
them.
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
import { AgentSpace, AutomationsWindow, SignalcoreProvider } from "@signalcore/ui";
|
|
277
|
+
import { FlowWindow } from "@signalcore/ui/flow";
|
|
278
|
+
import { VoiceWindow } from "@signalcore/ui/voice";
|
|
279
|
+
|
|
280
|
+
// Hoisted: a new object each render would draw the shell again for nothing.
|
|
281
|
+
const windows = { automations: AutomationsWindow, flow: FlowWindow, voice: VoiceWindow };
|
|
282
|
+
|
|
283
|
+
<SignalcoreProvider client={client} viewer="owner" windows={windows}>
|
|
284
|
+
<AgentSpace
|
|
285
|
+
agentId="agt_…"
|
|
286
|
+
thread="order-7"
|
|
287
|
+
windows={["automations"]}
|
|
288
|
+
renderVoice={(voice) => <VoiceWindow {...voice} />}
|
|
289
|
+
/>
|
|
290
|
+
</SignalcoreProvider>;
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
- **The registry** is the provider's `windows`: the component that draws each kind of default
|
|
294
|
+
window, laid over the package's own (`chat`, `browser`, `live`). `automations`, `flow` and
|
|
295
|
+
`voice` are yours to register, each from its own import, so a page that never shows the map does
|
|
296
|
+
not load its graph library. Every component takes the same props (`DefaultWindowProps`: the
|
|
297
|
+
agent, the thread, a label, the agent's words for the window, what the host opened it with), so
|
|
298
|
+
the windows of this package plug in as they are. A kind with none shows what the agent said the
|
|
299
|
+
window shows (the text twin), never an error.
|
|
300
|
+
- **`viewer`** says who is looking, because the client does not: `"owner"` (the default) or
|
|
301
|
+
`"end_user"`. An end user's stage never opens the owner's windows (`END_USER_WINDOW_KINDS`:
|
|
302
|
+
`chat` and `voice`), whoever asks, the host or the agent.
|
|
303
|
+
- **A pick on a generated window** goes back to the agent as the next turn, the way a pick in the
|
|
304
|
+
chat does, and the transcript says where a page went ("→ Opened in Sales") instead of drawing it
|
|
305
|
+
twice. A window the person closed stays closed until the agent puts something new in it. The
|
|
306
|
+
stage is rebuilt from the conversation's history when it opens, with no resource of its own.
|
|
307
|
+
- **Voice** is the dock's `renderVoice`: when the person speaks, the dock is the voice and the
|
|
308
|
+
conversation collapses to captions. Without `renderVoice` there is no button for it.
|
|
309
|
+
- **`useWindows`** is the state of the stage without the shell, for a host that lays windows out
|
|
310
|
+
itself: derived from facts (`place`, `restore`, `observe` a run's journal) and from what the
|
|
311
|
+
person does (`focus`, `close`, `reopen`). `WindowFrame`, `SurfaceWindow`, `Strip` and `Dock` are
|
|
312
|
+
the pieces the shell is made of; `renderStrip` and `renderDock` replace two of them.
|
|
313
|
+
- Tab goes strip, stage, dock; Esc closes the window on the stage (never the conversation); under
|
|
314
|
+
720px of its own width the strip becomes tabs. Its slots are `space`, `strip`, `window`, `dock`
|
|
315
|
+
and `captions`, and `<signalcore-space>` is it as a web component (see [The widget](#the-widget)).
|
|
316
|
+
|
|
317
|
+
## Live streams
|
|
318
|
+
|
|
319
|
+
What a run shows as it happens (the browser it drives, a preview its code renders) is a live
|
|
320
|
+
stream, and **`LiveView`** watches one: the pictures it sends, drawn on a canvas as they arrive.
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
<SignalcoreProvider client={client}>
|
|
324
|
+
<LiveView streamId="liv_..." aspect="16:9" input={false} onStatus={(status) => {}} />
|
|
325
|
+
</SignalcoreProvider>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
- **Where it comes from.** The `live` template (a `Live` component in a card) draws a `LiveView`,
|
|
329
|
+
and the `Browser` component and the hand-over card draw one when they name a `stream_id`, in
|
|
330
|
+
preference to the hosted browser's live URL and to screenshots. `client` needs `live.connect`,
|
|
331
|
+
which the SDK's `Signalcore` has (`client.live`); a host that reaches the API with a cookie
|
|
332
|
+
supplies it with `new Live({ baseUrl })` from `@signalcore/sdk`.
|
|
333
|
+
- **The picture.** The canvas takes the stream's size from its first frame (`aspect` holds its shape
|
|
334
|
+
until then), shows the newest picture and never a queue, and has a role of `img` with a label
|
|
335
|
+
(`label`, "Vista en directo" by default, "Live view" in English). A quiet word says how it is
|
|
336
|
+
("en directo", "reconectando", "terminado", "no disponible"; "live", "reconnecting", "ended",
|
|
337
|
+
"unavailable" in English). It does not animate for people who prefer reduced motion.
|
|
338
|
+
- **Dropped connections** are opened again, after half a second and then twice as long up to ten
|
|
339
|
+
seconds. A stream that ended, or that the server refuses (not signed in, not allowed, no longer
|
|
340
|
+
there), is not. A stream that never answered is given up on after three tries: `onStatus` then
|
|
341
|
+
says `unavailable`, and the browser view goes back to its live URL or its screenshots.
|
|
342
|
+
- **Input.** With `input`, once the stream says it takes input, the pointer (clicks, double clicks,
|
|
343
|
+
drags), the wheel and the keys go to the stream in its own pixels, and the canvas can be reached
|
|
344
|
+
with the keyboard: keys are sent only while it has the focus, Tab leaves it, and keys held when it
|
|
345
|
+
loses the focus are let go. Whether it is the person's turn is for the stream's producer to say; a
|
|
346
|
+
browser session takes input only while the agent has handed the controls over, which is when the
|
|
347
|
+
hand-over card turns `input` on.
|
|
348
|
+
|
|
349
|
+
## Styling
|
|
350
|
+
|
|
351
|
+
Five levels, from nothing to everything. Each one is optional, and each works without the next.
|
|
352
|
+
|
|
353
|
+
**1. Inherit.** Dropped into any page it uses **your font, your text color and your color scheme**
|
|
354
|
+
(`font-family: inherit`, `color: inherit`, tints of `currentColor` for lines and muted text). Dark
|
|
355
|
+
mode follows `prefers-color-scheme`, and an ancestor with `data-theme="dark"` (or `"light"`)
|
|
356
|
+
decides when you set it. This is the whole of the work for many pages.
|
|
357
|
+
|
|
358
|
+
**2. Tokens.** Everything else is a `--sc-*` custom property with a default. Set what you want
|
|
359
|
+
different, in CSS or through the provider, which writes them as inline custom properties on its
|
|
360
|
+
own wrapper element:
|
|
361
|
+
|
|
362
|
+
| Token | What |
|
|
363
|
+
| ----------------------------------------------- | ----------------------------------------------------------- |
|
|
364
|
+
| `--sc-accent`, `--sc-accent-fg` | The owner acting: primary buttons, selection, links |
|
|
365
|
+
| `--sc-bg`, `--sc-surface`, `--sc-sunken` | What it sits on, a card's face, a recessed area |
|
|
366
|
+
| `--sc-line`, `--sc-line-strong` | Borders |
|
|
367
|
+
| `--sc-text`, `--sc-muted`, `--sc-subtle` | Ink (default: your text color) |
|
|
368
|
+
| `--sc-danger`, `--sc-ok`, `--sc-signal` | State, each with a `-soft` tint |
|
|
369
|
+
| `--sc-radius`, `--sc-density` | The corner of a card; how much room things take (1 is ours) |
|
|
370
|
+
| `--sc-font`, `--sc-font-mono`, `--sc-font-size` | Default: your font; the reading size is 14px |
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
<SignalcoreProvider theme={{ accent: "var(--primary)", radius: "var(--radius)", density: 0.9 }} />
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**3. Presets, found for you.** By default (`theme="auto"`) the provider asks your page's root for
|
|
377
|
+
the variables of the design systems it knows and maps the tokens onto the first it finds:
|
|
378
|
+
**shadcn** (`--primary`, `--background`, `--card`, `--border`, `--foreground`…), **Radix Themes**
|
|
379
|
+
(`--accent-9`, `--color-background`, `--gray-6`…) and **Material 3** (`--md-sys-color-primary`,
|
|
380
|
+
`--md-sys-color-surface`…). So in a shadcn app the surfaces already use your primary, your card and
|
|
381
|
+
your radius, in light and in dark, with no configuration. A page that uses none keeps level 1.
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
<SignalcoreProvider /> {/* auto */}
|
|
385
|
+
<SignalcoreProvider theme="shadcn" /> {/* skip the search: Radix needs this inside its <Theme> */}
|
|
386
|
+
<SignalcoreProvider theme={{ accent: "var(--brand)" }} /> {/* over the detected system */}
|
|
387
|
+
<SignalcoreProvider theme={{ preset: "material", radius: "4px" }} />
|
|
388
|
+
<SignalcoreProvider theme="none" /> {/* nothing mapped */}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The search reads, and never writes: the tokens go on the provider's wrapper, not on your `:root`.
|
|
392
|
+
It runs in the browser only (a server renders as if none were used, and the client then finds
|
|
393
|
+
out), and again when `data-theme` or `class` on `<html>` changes. The tables of variables are in
|
|
394
|
+
`src/presets.ts`; adding a system is adding a table.
|
|
395
|
+
|
|
396
|
+
**4. Class hooks.** Every element is a named slot with a stable BEM class and attributes:
|
|
397
|
+
`sc-card`, `sc-card__title`, `sc-button sc-button--primary`, `data-sc="button"`,
|
|
398
|
+
`data-sc-mod="primary sm"`. `classNames` adds yours by slot (the list is `SLOTS`), or by slot and
|
|
399
|
+
modifier:
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
<SignalcoreProvider
|
|
403
|
+
classNames={{ card: "rounded-xl border bg-card", "button--primary": "bg-primary" }}
|
|
404
|
+
/>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**`unstyled`** leaves out the shipped styles (the elements and classes stay), for hosts that style
|
|
408
|
+
everything through `classNames`, as a Tailwind-only app does. The wrapper is then a plain `div`;
|
|
409
|
+
give it `classNames={{ ui: "contents" }}`. The shipped styles sit in a CSS layer (`signalcore`)
|
|
410
|
+
and under the wrapper, so your own unlayered CSS wins without a fight over specificity. A host that
|
|
411
|
+
has layers of its own (Tailwind v4's) says where ours go, before anything fills them, so its
|
|
412
|
+
utilities win over our styles on what it draws around our parts:
|
|
413
|
+
`@layer theme, base, signalcore, components, utilities;` first in its stylesheet, then
|
|
414
|
+
`@import "tailwindcss";`. Layers declared later win, so without that line ours would.
|
|
415
|
+
|
|
416
|
+
**5. Your own components.** `components` takes the primitives surfaces are built from, the
|
|
417
|
+
catalog's components and components of your own, by name. See
|
|
418
|
+
[Your own components](#your-own-components).
|
|
419
|
+
|
|
420
|
+
**Patterns.** Where our components have two layouts, you choose; the defaults are the product's.
|
|
421
|
+
They are classes like any other (`sc-table--compact`), and apply to our components only: a
|
|
422
|
+
primitive you replace draws itself.
|
|
423
|
+
|
|
424
|
+
```tsx
|
|
425
|
+
<SignalcoreProvider patterns={{ table: "compact", actions: "footer", card: "raised" }} />
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
| Pattern | Values | What |
|
|
429
|
+
| --------- | ---------------------------------- | ----------------------------------------------------------- |
|
|
430
|
+
| `table` | `comfortable` (default), `compact` | Rows with room, or close together for a lot of records |
|
|
431
|
+
| `actions` | `inline` (default), `footer` | A form's button under its fields, or set apart on a line |
|
|
432
|
+
| `card` | `flat` (default), `raised` | A bordered area, or one that also lifts a hair off the page |
|
|
433
|
+
|
|
434
|
+
## Words
|
|
435
|
+
|
|
436
|
+
Every word the components say themselves (buttons, statuses, card headings, the builder's step
|
|
437
|
+
verbs, the times above a conversation) comes from a dictionary. Two ship with the package, Spanish
|
|
438
|
+
(the default, as the product speaks) and English, and the provider picks one and lets you change
|
|
439
|
+
single words:
|
|
440
|
+
|
|
441
|
+
```tsx
|
|
442
|
+
<SignalcoreProvider locale="en" messages={{ send: "Post", verbs: { edit: "Change" } }} />
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
- **`locale`** is `"es"` (default) or `"en"`. It decides the words and how dates and times are
|
|
446
|
+
written ("Hoy 10:32" and "12 oct" in Spanish, "Today 10:32 AM" and "Oct 12" in English).
|
|
447
|
+
- **`messages`** is any part of the dictionary, laid over the locale's: a word you leave out stays
|
|
448
|
+
as it was. `Messages` is its type, and the key names are in `src/messages/types.ts`. Words that
|
|
449
|
+
carry a name or a number are functions (`connectWith: (app) => "Connect " + app`), and the
|
|
450
|
+
groups (`verbs`, what the builder is doing; `liveStatus`; `voiceStates`; and the windows'
|
|
451
|
+
`automations` and `flow`) can change one entry at a time, at any depth
|
|
452
|
+
(`{ flow: { verbs: { calls: "runs" } } }`). Define the object
|
|
453
|
+
outside the render, so the components are not drawn again for nothing.
|
|
454
|
+
- **Another language.** The package has the two. For a third, give every word as `messages` (the
|
|
455
|
+
type makes sure none is missing) over the `locale` whose dates are closest.
|
|
456
|
+
- **The widget** takes the language as an attribute and the words as a property:
|
|
457
|
+
|
|
458
|
+
```html
|
|
459
|
+
<signalcore-chat agent="agt_…" publishable-key="pk_live_…" locale="en"></signalcore-chat>
|
|
460
|
+
<script>
|
|
461
|
+
document.querySelector("signalcore-chat").messages = { send: "Post" };
|
|
462
|
+
</script>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
- **Not components.** `describeStep` and `workingOn`, which a host that draws the builder's
|
|
466
|
+
conversation itself calls, are Spanish unless they are handed the words:
|
|
467
|
+
`describeStep(name, input, useMessages())`.
|
|
468
|
+
|
|
469
|
+
## Your own components
|
|
470
|
+
|
|
471
|
+
`components` on the provider takes your own components, by name. Three kinds:
|
|
472
|
+
|
|
473
|
+
- **Primitives** (`Button`, `Card`, `Table`, `Input`, `Select`, with typed props): every surface is
|
|
474
|
+
built from yours, so the catalog is drawn with your design system. They add nothing to the
|
|
475
|
+
catalog; a surface cannot name them. The cards that ask the owner to decide never use them.
|
|
476
|
+
`Orb`, the voice window's, is one too (see [Voice](#voice)).
|
|
477
|
+
- **Catalog components** (`SlotPicker`): a surface that uses the name is drawn by your component,
|
|
478
|
+
given the properties the agent filled (bindings resolved), its `children` drawn, and `onAction`
|
|
479
|
+
to report what the user does. They are not checked: read them loosely.
|
|
480
|
+
- **Components of your own** (`PropertyCard`): things of your product that an agent shows in your
|
|
481
|
+
design, with props the API checks. See below.
|
|
482
|
+
|
|
483
|
+
```tsx
|
|
484
|
+
<SignalcoreProvider components={{ Button: MyButton, SlotPicker: MySlots, PropertyCard }} />
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### A component of your own
|
|
488
|
+
|
|
489
|
+
An agent can show a listing, a quote or a ticket as your product draws it, if SignalCore knows the
|
|
490
|
+
component: its name, a schema for its props and a text twin. It is registered twice, in the page
|
|
491
|
+
and with the API.
|
|
492
|
+
|
|
493
|
+
**In the page**, a React component that takes the props and, when the user can answer it, reports
|
|
494
|
+
with `onAction`:
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
import { AgentChat, SignalcoreProvider, type HostAction } from "@signalcore/ui";
|
|
498
|
+
|
|
499
|
+
interface PropertyCardProps {
|
|
500
|
+
address: string;
|
|
501
|
+
price: number;
|
|
502
|
+
onAction?: (action: HostAction) => void;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function PropertyCard({ address, price, onAction }: PropertyCardProps) {
|
|
506
|
+
const rate = (fit: boolean) =>
|
|
507
|
+
onAction?.({
|
|
508
|
+
name: "rate",
|
|
509
|
+
data: { address, fit },
|
|
510
|
+
label: `${fit ? "Good fit" : "Not a fit"}: ${address}`,
|
|
511
|
+
});
|
|
512
|
+
return (
|
|
513
|
+
<article>
|
|
514
|
+
<h3>{address}</h3>
|
|
515
|
+
<p>{price} €</p>
|
|
516
|
+
<button disabled={!onAction} onClick={() => rate(true)}>
|
|
517
|
+
Good fit
|
|
518
|
+
</button>
|
|
519
|
+
<button disabled={!onAction} onClick={() => rate(false)}>
|
|
520
|
+
Not a fit
|
|
521
|
+
</button>
|
|
522
|
+
</article>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
<SignalcoreProvider components={{ PropertyCard }}>
|
|
527
|
+
<AgentChat agentId="agt_…" />
|
|
528
|
+
</SignalcoreProvider>;
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**With the API**, once for your organization: `client.ui.components.put(...)` in the SDKs takes the
|
|
532
|
+
name, a JSON Schema for the props and the text twin (see the SDK's reference for its fields). The
|
|
533
|
+
agent then uses the component in a surface with a node of its own kind:
|
|
534
|
+
|
|
535
|
+
```json
|
|
536
|
+
{
|
|
537
|
+
"id": "home",
|
|
538
|
+
"type": "host",
|
|
539
|
+
"component": "PropertyCard",
|
|
540
|
+
"props": { "address": "Calle Mayor 3", "price": 250000 }
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
What your component is given, and what it can count on:
|
|
545
|
+
|
|
546
|
+
- **`props`, exactly as the agent sent them.** The API validated them against the schema you
|
|
547
|
+
registered before it sent the surface, so you can rely on their shape. They are values: a
|
|
548
|
+
binding (`{ "path": … }`) is not looked up in them. There are no `children`.
|
|
549
|
+
- **`onAction({ name, data, label })`**, to report what the user does. It goes back through the
|
|
550
|
+
surface's action path like a pick on a list: `AgentChat` sends it to the agent as what the
|
|
551
|
+
person said (`label`, "Good fit: Calle Mayor 3"), and `Surface` hands your `onAction` the
|
|
552
|
+
`{ surface_id, name, data, label }` the API's `Action` expects. It is `undefined` when the surface
|
|
553
|
+
takes no actions (it is a record of what was shown), so disable what answers. The node's props
|
|
554
|
+
cannot replace it.
|
|
555
|
+
- **The text twin is already in the surface's `fallback_text`.** The API appends your component's
|
|
556
|
+
twin to the surface's, so where a surface cannot be drawn whole the user reads all of it in words,
|
|
557
|
+
never an empty box, and the rest of your page carries on. That happens when this page has not
|
|
558
|
+
registered the name (a development build says which in the console), when the props are not an
|
|
559
|
+
object, and when your component throws.
|
|
560
|
+
- **A primitive's name is not yours to use.** A node cannot name `Button`, `Card`, `Table`, `Input`
|
|
561
|
+
or `Select`, and `RequestCard` never draws your components.
|
|
562
|
+
|
|
563
|
+
## Your own client
|
|
564
|
+
|
|
565
|
+
`client` is the part of the SDK the components call (`UiClient`): `Signalcore` has all of it. If you
|
|
566
|
+
reach the API another way (a session cookie instead of a key), supply the same calls (for streams,
|
|
567
|
+
`live: new Live({ baseUrl: location.origin })`).
|