@m6d/cortex-angular 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,63 +1,64 @@
1
1
  # @m6d/cortex-angular
2
2
 
3
- Angular AI chat UI library. Provides a self-contained chat widget with streaming messages, tool call visualization, composer attachments, syntax highlighting, and i18n support.
3
+ Angular chat widget for Cortex agents: streaming messages, tool call visualization, composer attachments, syntax highlighting, and i18n. `@m6d/cortex-react` is the same widget for React.
4
4
 
5
5
  ## Usage
6
6
 
7
7
  ```ts
8
- import { CortexChatWidgetComponent, CortexAngularConfig } from '@m6d/cortex-angular';
8
+ import { CortexChatWidgetComponent, type CortexAngularConfig } from '@m6d/cortex-angular';
9
9
 
10
10
  @Component({
11
11
  imports: [CortexChatWidgetComponent],
12
12
  template: `<cortex-chat-widget [config]="config" />`,
13
13
  })
14
14
  export class MyComponent {
15
- protected readonly config: CortexAngularConfig = {
15
+ protected readonly config = {
16
16
  locale: 'en',
17
17
  transport: {
18
- baseUrl: '/api/agents/assistant',
18
+ baseUrl: 'https://engine.cortex.example.com/agents/support',
19
19
  getHeaders: () => ({ Authorization: `Bearer ${token}` }),
20
20
  },
21
- // The WebSocket URL is derived from baseUrl (…/agents/assistant/ws); set
22
- // `wsUrl` only to override it, and point it at that same agent-scoped path.
23
- };
21
+ } satisfies CortexAngularConfig;
24
22
  }
25
23
  ```
26
24
 
27
- No root-level providers needed — each widget instance is fully self-contained. You can render multiple widgets with different configs on the same page.
28
-
29
- ### Configuration
30
-
31
- `CortexAngularConfig` is the shared `CortexConfig` plus two Angular-only slots
32
- (`toolComponents`, `injector`). `@m6d/cortex-react` takes the same shared shape.
33
-
34
- | Property | Type | Description |
35
- | -------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------ |
36
- | `locale` | `'en' \| 'ar'` | Active locale (default: `'en'`) |
37
- | `transport.baseUrl` | `string \| () => string` | Agent API base URL |
38
- | `transport.getHeaders` | `() => Headers \| Promise<Headers>` | Request headers provider; `Headers` is `Record<string, string>` |
39
- | `wsUrl` | `string \| false` | WebSocket URL override; `false` disables the socket (optional) |
40
- | `viewMode` | `'helper' \| 'full'` | Compact panel or full layout with a sidebar (default: `'helper'`) |
41
- | `theme` | `'light' \| 'dark' \| 'system'` | Color scheme (default: `'light'`) |
42
- | `agentName` | `string \| { en, ar }` | Header title while no thread is selected (optional) |
43
- | `showDebugButton` | `boolean` | Show/hide the debug toggle button (default: `false`) |
44
- | `mediaRenderers` | `Record<string, MediaRenderer>` | Replace or add the media fences the assistant can draw (optional) |
45
- | `trustSameOriginEmbeds` | `boolean` | Allow embedded tool pages served from this page's own origin (default: `false`) |
46
- | `hooks.onToolCall` | `(toolCall, context) => unknown` | Answers a static client tool; return `undefined` for tools you do not own (optional) |
47
- | `hooks.onThreadSelected` | `(thread) => void \| Promise<void>` | Runs when a thread opens (optional) |
48
- | `hooks.onThreadDeselected` | `(thread) => void \| Promise<void>` | Runs when a thread closes (optional) |
49
- | `toolComponents` | `Record<string, Type<CortexToolComponentInputs>>` | Custom Angular components for tool rendering (optional) |
50
- | `injector` | `Injector` | Injector for `toolComponents` (optional) |
51
-
52
- `transport.getHeaders` can return any request headers needed by the host app. WebSocket auth continues to derive the `token` query param from a bearer `Authorization` header when present.
25
+ The widget attaches its own stylesheet; add `@m6d/cortex-angular/theme.css` to
26
+ your styles to override its tokens in one place (see Theming).
27
+
28
+ `baseUrl` is the agent's URL: the engine's public origin (`ENGINE_PUBLIC_URL`) plus `/agents/<agent slug>`. Add the page's origin to `ENGINE_CORS_ORIGINS`, or proxy the engine under your own origin as the samples do. The WebSocket URL is `baseUrl` plus `/ws`; set `wsUrl` only to override it, and point it at that same agent-scoped path. The bearer is the end user's token, which Cortex verifies the way the agent's auth says: a JWKS, a shared secret, the agent's server, or nobody for an anonymous agent.
29
+
30
+ No root-level providers are needed. Each widget is self-contained, so one page can render several widgets with different configs.
31
+
32
+ ## Configuration
33
+
34
+ `CortexAngularConfig` is the shared `CortexConfig` plus two Angular-only fields, `toolComponents` and `injector`.
35
+
36
+ | Property | Type | Description |
37
+ | -------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------- |
38
+ | `locale` | `'en' \| 'ar'` | Active locale (default: `'en'`) |
39
+ | `transport.baseUrl` | `string \| () => string` | The agent's URL, `<engine origin>/agents/<slug>` |
40
+ | `transport.getHeaders` | `() => Headers \| Promise<Headers>` | Request headers provider; `Headers` is `Record<string, string>` |
41
+ | `wsUrl` | `string \| false` | WebSocket URL override; `false` disables the socket (optional) |
42
+ | `viewMode` | `'helper' \| 'full'` | Compact panel or full layout with a sidebar (default: `'helper'`) |
43
+ | `theme` | `'light' \| 'dark' \| 'system'` | Color scheme (default: `'light'`) |
44
+ | `agentName` | `string \| { en, ar }` | Header title while no thread is selected (optional) |
45
+ | `showDebugButton` | `boolean` | Show/hide the debug toggle button (default: `false`) |
46
+ | `mediaRenderers` | `Record<string, MediaRenderer>` | Replace or add the media fences the assistant can draw (optional) |
47
+ | `trustSameOriginEmbeds` | `boolean` | Allow embedded tool pages served from this page's own origin (default: `false`) |
48
+ | `hooks.onToolCall` | `(toolCall, context) => unknown` | Answers a client tool; return `undefined` for tools you do not own (optional) |
49
+ | `hooks.onThreadSelected` | `(thread) => void \| Promise<void>` | Runs when a thread opens (optional) |
50
+ | `hooks.onThreadDeselected` | `(thread) => void \| Promise<void>` | Runs when a thread closes (optional) |
51
+ | `toolComponents` | `Record<string, Type<CortexToolComponentInputs>>` | Custom Angular components for tool rendering (optional) |
52
+ | `injector` | `Injector` | Injector for `toolComponents` (optional) |
53
+
54
+ `transport.getHeaders` returns the headers every call carries, usually a bearer `Authorization`. Called straight from another origin, the engine accepts only `Authorization` and the headers the widget sets itself; route anything else through a same-origin proxy. The WebSocket takes its `token` query param from a bearer `Authorization` header when there is one.
53
55
 
54
56
  ## Custom parts
55
57
 
56
58
  Put an `ng-template` marked `cortexSlot` inside the widget to draw one of its
57
59
  parts yourself: `header`, `landing` (the blank chat) or `threadList` (the
58
- history's list). Each replaces the widget's default and receives the same
59
- context the default is drawn from, typed by the slot name. Use only the slots
60
- you need:
60
+ history's list). Each receives the context the default is drawn from, typed by
61
+ the slot name. Use only the slots you need:
61
62
 
62
63
  ```ts
63
64
  import { NgTemplateOutlet } from '@angular/common';
@@ -83,7 +84,9 @@ import { CortexChatWidgetComponent, CortexSlotDirective } from '@m6d/cortex-angu
83
84
 
84
85
  <ng-template cortexSlot="landing" let-landing>
85
86
  <div class="cortex-widget__landing">
86
- <h1>{{ landing.greeting }}</h1>
87
+ @if (landing.greeting) {
88
+ <h1>{{ landing.greeting }}</h1>
89
+ }
87
90
  @for (suggestion of landing.suggestions; track $index) {
88
91
  <button (click)="landing.send(suggestion.prompt)">{{ suggestion.title }}</button>
89
92
  }
@@ -116,19 +119,19 @@ for typing your own rows and chips.
116
119
 
117
120
  One header serves both screens; `screen` says which is showing.
118
121
 
119
- | Field | Type | Description |
120
- | -------------- | --------------------- | -------------------------------------------------------------- |
121
- | `screen` | `'chat' \| 'threads'` | Which screen the header is on |
122
- | `viewMode` | `'helper' \| 'full'` | The configured view mode |
123
- | `title` | `string \| null` | The open thread's title; `null` on a new chat |
124
- | `agentName` | `string \| undefined` | `agentName` in the widget's locale |
125
- | `threadCount` | `number` | How many conversations the history lists; 0 while it loads |
126
- | `isWorking` | `boolean` | The agent is answering |
127
- | `debug` | `{ shown, on }` | Whether `showDebugButton` is set, and whether debug mode is on |
128
- | `newChat` | `() => void` | Starts a new chat |
129
- | `openHistory` | `() => void` | Shows the history: its screen, or the sidebar in full mode |
130
- | `closeHistory` | `() => void` | Returns to the open chat |
131
- | `toggleDebug` | `() => void` | Switches debug mode |
122
+ | Field | Type | Description |
123
+ | -------------- | --------------------- | ------------------------------------------------------------------ |
124
+ | `screen` | `'chat' \| 'threads'` | Which screen the header is on |
125
+ | `viewMode` | `'helper' \| 'full'` | The configured view mode |
126
+ | `title` | `string \| null` | The open thread's title; `null` on a new chat |
127
+ | `agentName` | `string \| undefined` | `agentName` in the widget's locale |
128
+ | `threadCount` | `number \| undefined` | How many conversations the history lists; undefined while it loads |
129
+ | `isWorking` | `boolean` | The agent is answering |
130
+ | `debug` | `{ shown, on }` | Whether `showDebugButton` is set, and whether debug mode is on |
131
+ | `newChat` | `() => void` | Starts a new chat |
132
+ | `openHistory` | `() => void` | Shows the history: its screen, or the sidebar in full mode |
133
+ | `closeHistory` | `() => void` | Returns to the open chat |
134
+ | `toggleDebug` | `() => void` | Switches debug mode |
132
135
 
133
136
  ### `landing`
134
137
 
@@ -137,7 +140,7 @@ landing for it.
137
140
 
138
141
  | Field | Type | Description |
139
142
  | ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------- |
140
- | `greeting` | `string` | The agent's greeting in the widget's locale, or the built-in one |
143
+ | `greeting` | `string` | The agent's greeting in the widget's locale, or the built-in one; empty until the handshake answers |
141
144
  | `suggestions` | `{ title, prompt }[]` | Empty until the handshake answers, and for an agent that takes no messages |
142
145
  | `threads` | `ThreadSummary[] \| undefined` | Newest first; `undefined` while the first load is out |
143
146
  | `composer` | `TemplateRef` | The widget's composer, or its "messages disabled" notice; place it with `ngTemplateOutlet` |
@@ -159,9 +162,11 @@ The history's list. Its header is the `header` slot.
159
162
 
160
163
  ## Theming
161
164
 
162
- Every token in `theme.css` can be overridden on `:root` or on any ancestor of
163
- the widget. Beside the colors, radius and `--cortex-font-size`, four tokens
164
- ship unset and fall back when you leave them out:
165
+ Every color is a `--cortex-color-*` token whose light and dark defaults ship
166
+ with the widget; `theme` picks the side. `@m6d/cortex-angular/theme.css` lists every
167
+ token with its default, and any of them can be overridden on `:root` or on any
168
+ ancestor of the widget. Beside the colors, radius and `--cortex-font-size`,
169
+ four tokens ship unset and fall back when you leave them out:
165
170
 
166
171
  | Token | Falls back to | Styles |
167
172
  | -------------------------------- | --------------------------- | ------------------------------------ |
@@ -170,12 +175,12 @@ ship unset and fall back when you leave them out:
170
175
  | `--cortex-color-send-empty` | `transparent` | The send button with nothing to send |
171
176
  | `--cortex-color-send-empty-text` | `--cortex-color-text-muted` | Its icon |
172
177
 
173
- The composer resets its own focus and disabled styles, so a host's global
178
+ The composer resets its own focus and disabled styles, so a page's global
174
179
  `textarea:focus` ring or disabled fill does not reach it.
175
180
 
176
181
  ## Tables
177
182
 
178
- A markdown table in an answer scrolls sideways instead of squeezing its
183
+ A markdown table in an answer scrolls sideways rather than squeezing its
179
184
  columns. Once it has finished arriving, hovering it (or focusing into it)
180
185
  shows two actions at its top end: expand, which opens it in a dialog with room
181
186
  for every column, and export, which downloads it as an `.xlsx` file. Numbers
@@ -183,45 +188,46 @@ export as numbers; the header row is bold.
183
188
 
184
189
  ## Client tools
185
190
 
186
- Control-Center tools published with type "embedded" render inside the chat
187
- as an embedded flow (payment, signature, ...): the widget asks the server to
188
- initiate, shows the returned page in a sandboxed iframe (inline card or
189
- modal, per tool config), and submits the page's `cortex:client-tool`
190
- postMessage outcome back to the run as-is — anything that must actually be
191
- true lives in the integrating backend's own records. No widget configuration
192
- is needed. One caveat: an embed served from the host page's **own origin** is
193
- refused by default, because a same-origin iframe cannot be sandboxed — it
194
- could reach into the host page, and embed URLs come from Control Center tool
195
- config, whose administrator is not necessarily the site owner. When both are
196
- the same party and the embeds are the site's own pages, set
191
+ Tools of type `embedded` render inside the chat as an embedded flow (payment,
192
+ signature, ...): the widget asks Cortex to initiate, shows the returned page in
193
+ a sandboxed iframe (inline card or modal, per tool config), and submits the
194
+ page's `cortex:client-tool` postMessage outcome back to the run as-is.
195
+ Anything that must actually be true lives in your backend's own records. No
196
+ widget configuration is needed. One caveat: an embed served from the page's
197
+ **own origin** is refused by default, because a same-origin iframe cannot be
198
+ sandboxed: it could reach into the page, and embed URLs come from the tool's
199
+ definition in Cortex, whose administrator is not necessarily the site owner.
200
+ When both are the same party and the embeds are the site's own pages, set
197
201
  `trustSameOriginEmbeds: true` to render them anyway (with full access, as any
198
- of the site's own pages has). Other pending tool calls keep the existing `toolComponents` /
199
- `hooks.onToolCall` behavior. That hook is also how static client tools work:
200
- declare an executor-less tool in the server config and answer it here by
201
- returning its output from `hooks.onToolCall` for that tool name. Consumer `hooks.onToolCall` implementations must
202
- return `undefined` for tool names they do not own — answering an unknown
203
- pending call would settle an embedded flow before the user completes it.
202
+ of the site's own pages has).
203
+
204
+ Other pending tool calls go to `toolComponents` or `hooks.onToolCall`.
205
+ That is how a tool of type `client` ("Answered by your app" in the console)
206
+ works: return its output from `hooks.onToolCall` for that tool name, or draw it
207
+ with a tool component. A `hooks.onToolCall` must return `undefined` for tool
208
+ names it does not own, since answering an unknown pending call would settle an
209
+ embedded flow before the user completes it.
204
210
 
205
211
  ## Media blocks
206
212
 
207
213
  The assistant can answer with more than text. A fenced block tagged `mermaid`,
208
- `vega-lite`, `map` or `report` renders as a diagram, chart, map or report inside the message,
209
- with a placeholder while the block is still streaming and an error card (with
210
- the source) when it cannot be drawn. An image attachment the model references
211
- as `![alt](attachment#<id>)` on its own line renders inline.
214
+ `vega-lite`, `map` or `report` renders as a diagram, chart, map or report
215
+ inside the message, with a placeholder while the block is streaming and an
216
+ error card (with the source) when it cannot be drawn. An image attachment the
217
+ model references as `![alt](attachment#<id>)` on its own line renders inline.
212
218
 
213
219
  The renderers are peer dependencies your bundler must be able to resolve, but
214
220
  they load only when a block of their kind first appears: `mermaid` for
215
221
  diagrams, `vega`, `vega-lite` and `vega-embed` for charts, and `leaflet` for
216
222
  maps (plus its stylesheet, `leaflet/dist/leaflet.css`, in your page). Maps
217
223
  draw OpenStreetMap tiles and need no key. A block whose library fails to load
218
- at runtime shows the error card.
224
+ shows the error card.
219
225
 
220
226
  A report is an HTML fragment the agent writes. The widget removes scripts,
221
227
  images, links and anything that would load a URL, draws the rest in a shadow
222
228
  root that takes its colors from your `--cortex-color-*` theme, and draws each
223
229
  `<figure data-chart>` inside it with the chart renderer. Which forms an agent
224
- writes is its `media` setting on the server; reports are off by default.
230
+ writes is its `media` setting in Cortex; reports are off by default.
225
231
 
226
232
  Leaflet finds its marker images next to wherever the build put the one the
227
233
  stylesheet references, so an Angular build needs the other two copied there as