@djangocfg/ui-tools 2.1.395 → 2.1.397
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/package.json +6 -6
- package/src/tools/Chat/README.md +20 -1
- package/src/tools/Chat/components/ChatRoot.tsx +9 -0
- package/src/tools/Chat/components/MessageBubble.tsx +18 -0
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +1 -1
- package/src/tools/SpeechRecognition/core/languages-catalog.ts +1 -1
- package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +1 -1
- package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.397",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -154,8 +154,8 @@
|
|
|
154
154
|
"test:watch": "vitest"
|
|
155
155
|
},
|
|
156
156
|
"peerDependencies": {
|
|
157
|
-
"@djangocfg/i18n": "^2.1.
|
|
158
|
-
"@djangocfg/ui-core": "^2.1.
|
|
157
|
+
"@djangocfg/i18n": "^2.1.397",
|
|
158
|
+
"@djangocfg/ui-core": "^2.1.397",
|
|
159
159
|
"consola": "^3.4.2",
|
|
160
160
|
"lodash-es": "^4.18.1",
|
|
161
161
|
"lucide-react": "^0.545.0",
|
|
@@ -209,9 +209,9 @@
|
|
|
209
209
|
"material-file-icons": "^2.4.0"
|
|
210
210
|
},
|
|
211
211
|
"devDependencies": {
|
|
212
|
-
"@djangocfg/i18n": "^2.1.
|
|
213
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
214
|
-
"@djangocfg/ui-core": "^2.1.
|
|
212
|
+
"@djangocfg/i18n": "^2.1.397",
|
|
213
|
+
"@djangocfg/typescript-config": "^2.1.397",
|
|
214
|
+
"@djangocfg/ui-core": "^2.1.397",
|
|
215
215
|
"@types/lodash-es": "^4.17.12",
|
|
216
216
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
217
217
|
"@types/node": "^24.7.2",
|
package/src/tools/Chat/README.md
CHANGED
|
@@ -715,13 +715,32 @@ Stories live next to components — open `@djangocfg/playground`:
|
|
|
715
715
|
| `composerAttachmentTray` | Above composer textarea | `renderAttachmentTray({ attachments })` |
|
|
716
716
|
| `jumpToLatest` | Sticky overlay | `renderJumpToLatest({ unread, scrollToBottom })` |
|
|
717
717
|
| `renderToolCall` | Per tool-call panel | `(call) => ReactNode` |
|
|
718
|
-
| `renderAfterCalls` | After all tool panels | `(calls) => ReactNode`
|
|
718
|
+
| `renderAfterCalls` | After all tool panels | `(calls) => ReactNode` — **only renders when the message has tool calls** |
|
|
719
|
+
| `renderAfterMessage` | Below every assistant bubble | `(message) => ReactNode` — fires for every message, independent of `toolCalls` |
|
|
719
720
|
|
|
720
721
|
Flags:
|
|
721
722
|
|
|
722
723
|
- `hideComposer` — agent-pause / human-in-the-loop pause; composer is unmounted.
|
|
723
724
|
- `hideToolCalls` — show only `renderAfterCalls` rich UI without raw tool panels.
|
|
724
725
|
|
|
726
|
+
### Which slot for product widgets — `renderAfterCalls` vs `renderAfterMessage`?
|
|
727
|
+
|
|
728
|
+
Both put custom content under the assistant bubble; the difference is **what triggers them**.
|
|
729
|
+
|
|
730
|
+
- `renderAfterCalls` is gated on `message.toolCalls?.length > 0`. Use it when the widget is **derived from raw tool output** (read `call.output`) and you can rely on the host streaming the `tool_call` / `tool_result` SSE frames. Admin / dev flows typically work this way.
|
|
731
|
+
- `renderAfterMessage` fires **for every message**, regardless of `toolCalls`. Use it when the widget is driven by a side channel — e.g. typed `ui_payload` SSE frames the host emits independently of the raw tool surface. This is the correct slot when the public-prod stream **hides** `tool_call` events for security: the message lands with `toolCalls === undefined`, so `renderAfterCalls` would never mount, but `renderAfterMessage` still runs and the widget renders from the side channel.
|
|
732
|
+
|
|
733
|
+
Recommended pairing for a "vehicle cards" / "tax breakdown" / "chart" widget on a public chat:
|
|
734
|
+
|
|
735
|
+
```tsx
|
|
736
|
+
<ChatRoot
|
|
737
|
+
transport={transport}
|
|
738
|
+
renderAfterMessage={(m) => <VehicleCardsForMessage messageId={m.id} />}
|
|
739
|
+
/>
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
`VehicleCardsForMessage` subscribes to your `ui_payload` event bus (filtering by `m.id`) and returns `null` when there's nothing to show. Streaming-safe: the renderer is called for streaming messages too, so progressive UI (skeletons that fill in as payloads arrive) works as expected.
|
|
743
|
+
|
|
725
744
|
## Hotkeys
|
|
726
745
|
|
|
727
746
|
- `Enter` — send (or `Cmd+Enter` if `prefs.submitOn = 'cmd+enter'`).
|
|
@@ -68,6 +68,14 @@ export interface ChatRootProps {
|
|
|
68
68
|
// ---- render-prop slots (need access to data) --------------------------
|
|
69
69
|
/** Replace `<MessageBubble>` per message. */
|
|
70
70
|
renderMessage?: (m: ChatMessage, i: number) => ReactNode;
|
|
71
|
+
/**
|
|
72
|
+
* Render arbitrary content beneath every default `<MessageBubble>`
|
|
73
|
+
* (not invoked when `renderMessage` is set — the host owns layout
|
|
74
|
+
* in that case). Useful for product widgets driven by a side channel
|
|
75
|
+
* (e.g. `ui_payload` SSE frames) where the widget is per-message but
|
|
76
|
+
* not tied to the bubble's tool-calls array.
|
|
77
|
+
*/
|
|
78
|
+
renderAfterMessage?: (m: ChatMessage) => ReactNode;
|
|
71
79
|
/** Render the header lazily — receives the chat context. */
|
|
72
80
|
renderHeader?: (ctx: ChatContextValue) => ReactNode;
|
|
73
81
|
/** Render the empty-state lazily — receives a `setValue` to seed the composer. */
|
|
@@ -202,6 +210,7 @@ function ChatRootShell({ className, listClassName, slots }: ChatRootShellProps)
|
|
|
202
210
|
toolCallsProps={slots.toolCallsProps}
|
|
203
211
|
attachmentRenderers={slots.attachmentRenderers}
|
|
204
212
|
onAttachmentOpen={slots.onAttachmentOpen}
|
|
213
|
+
renderAfterMessage={slots.renderAfterMessage}
|
|
205
214
|
onCopy={() => copy(m.content)}
|
|
206
215
|
onRegenerate={() => void chat.regenerate(m.id)}
|
|
207
216
|
onDelete={() => chat.deleteMessage(m.id)}
|
|
@@ -79,6 +79,21 @@ export interface MessageBubbleProps {
|
|
|
79
79
|
* you need different behaviour per slot.
|
|
80
80
|
*/
|
|
81
81
|
streamingIndicator?: (m: ChatMessage) => ReactNode;
|
|
82
|
+
/**
|
|
83
|
+
* Render arbitrary content beneath the message body, regardless of
|
|
84
|
+
* whether the message carries tool calls. Used for product widgets
|
|
85
|
+
* that ride a side channel (e.g. `ui_payload` SSE frames driving
|
|
86
|
+
* vehicle cards, tax tables, charts) and therefore can't piggy-back
|
|
87
|
+
* on the `toolCallsRenderer` slot — the latter is gated on the
|
|
88
|
+
* message having a non-empty `toolCalls` array, which doesn't hold
|
|
89
|
+
* when raw tool events are hidden from public clients.
|
|
90
|
+
*
|
|
91
|
+
* Receives the message so the host can scope the widget by id /
|
|
92
|
+
* role / timing. Renders even on streaming messages, so progressive
|
|
93
|
+
* UI hints (skeletons that fill in as payloads arrive) work as
|
|
94
|
+
* expected.
|
|
95
|
+
*/
|
|
96
|
+
renderAfterMessage?: (m: ChatMessage) => ReactNode;
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
const MessageBubbleInner = ({
|
|
@@ -107,6 +122,7 @@ const MessageBubbleInner = ({
|
|
|
107
122
|
onDelete,
|
|
108
123
|
messageActionsExtra,
|
|
109
124
|
streamingIndicator,
|
|
125
|
+
renderAfterMessage,
|
|
110
126
|
}: MessageBubbleProps) => {
|
|
111
127
|
const isUser = isUserProp ?? message.role === 'user';
|
|
112
128
|
const isStreaming = !!message.isStreaming;
|
|
@@ -208,6 +224,8 @@ const MessageBubbleInner = ({
|
|
|
208
224
|
: <ToolCalls calls={message.toolCalls} {...toolCallsProps} />
|
|
209
225
|
: null}
|
|
210
226
|
|
|
227
|
+
{renderAfterMessage ? renderAfterMessage(message) : null}
|
|
228
|
+
|
|
211
229
|
{message.sources?.length && !isStreaming
|
|
212
230
|
? sourcesRenderer
|
|
213
231
|
? sourcesRenderer(message.sources)
|
|
@@ -223,7 +223,7 @@ export function countryFromTag(tag: string | null | undefined): string | null {
|
|
|
223
223
|
const parts = tag.split('-');
|
|
224
224
|
for (let i = parts.length - 1; i >= 0; i -= 1) {
|
|
225
225
|
const p = parts[i];
|
|
226
|
-
if (p.length === 2 && /^[A-Za-z]{2}$/.test(p)) return p.toUpperCase();
|
|
226
|
+
if (p && p.length === 2 && /^[A-Za-z]{2}$/.test(p)) return p.toUpperCase();
|
|
227
227
|
}
|
|
228
228
|
return null;
|
|
229
229
|
}
|
|
@@ -31,7 +31,7 @@ export function useMicLevel(stream: MediaStream | null): number {
|
|
|
31
31
|
const tick = (): void => {
|
|
32
32
|
analyser.getFloatTimeDomainData(buf);
|
|
33
33
|
let sum = 0;
|
|
34
|
-
for (let i = 0; i < buf.length; i += 1) sum += buf[i] * buf[i]
|
|
34
|
+
for (let i = 0; i < buf.length; i += 1) sum += buf[i]! * buf[i]!;
|
|
35
35
|
const rms = Math.sqrt(sum / buf.length);
|
|
36
36
|
// soft compression so loud peaks don't dominate the meter
|
|
37
37
|
setLevel(Math.min(1, rms * 2.5));
|
|
@@ -97,7 +97,7 @@ export function useSpeechLanguageInfo(): SpeechLanguageInfo {
|
|
|
97
97
|
const found = findSpeechLanguage(tag);
|
|
98
98
|
return {
|
|
99
99
|
tag,
|
|
100
|
-
iso: found?.language.iso ?? tag.split('-')[0].toLowerCase(),
|
|
100
|
+
iso: found?.language.iso ?? (tag.split('-')[0] ?? tag).toLowerCase(),
|
|
101
101
|
country: countryFromTag(tag),
|
|
102
102
|
name: found?.language.name ?? null,
|
|
103
103
|
englishName: found?.language.englishName ?? null,
|