@pikku/assistant-ui 0.12.4 → 0.12.5

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.
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { createContext, useContext, useState, useMemo, } from 'react';
2
+ import { createContext, useContext, useState, useMemo, useEffect, useRef, } from 'react';
3
3
  import Markdown from 'react-markdown';
4
- import { AssistantRuntimeProvider, ThreadPrimitive, MessagePrimitive, ComposerPrimitive, } from '@assistant-ui/react';
4
+ import { AssistantRuntimeProvider, ThreadPrimitive, MessagePrimitive, ComposerPrimitive, useComposerRuntime, } from '@assistant-ui/react';
5
5
  import { usePikkuAgentRuntime, PikkuApprovalContext, usePikkuApproval, resolvePikkuToolStatus, } from './use-pikku-agent-runtime.js';
6
6
  const lightColors = {
7
7
  bg: '#ffffff',
@@ -46,6 +46,8 @@ const darkColors = {
46
46
  const ColorsContext = createContext(lightColors);
47
47
  const HideToolCallsContext = createContext(undefined);
48
48
  const ToolComponentsContext = createContext(undefined);
49
+ const GenerativeUIComponentsContext = createContext(undefined);
50
+ const RenderAssistantTextContext = createContext(undefined);
49
51
  function shouldHideToolCall(hideToolCalls, toolName) {
50
52
  if (!hideToolCalls)
51
53
  return false;
@@ -296,6 +298,8 @@ const UserMessage = () => {
296
298
  const AssistantMessage = () => {
297
299
  const colors = useContext(ColorsContext);
298
300
  const toolComponents = useContext(ToolComponentsContext);
301
+ const generativeUIComponents = useContext(GenerativeUIComponentsContext);
302
+ const renderAssistantText = useContext(RenderAssistantTextContext);
299
303
  return (_jsx("div", { style: {
300
304
  display: 'flex',
301
305
  justifyContent: 'flex-start',
@@ -305,11 +309,18 @@ const AssistantMessage = () => {
305
309
  borderRadius: 12,
306
310
  backgroundColor: colors.assistantBubble,
307
311
  }, children: [_jsx(MessagePrimitive.Content, { components: {
308
- Text: ({ text }) => _jsx(MarkdownText, { text: text, colors: colors }),
312
+ Text: ({ text }) => renderAssistantText ? (_jsx(_Fragment, { children: renderAssistantText(text) })) : (_jsx(MarkdownText, { text: text, colors: colors })),
309
313
  tools: {
310
314
  by_name: toolComponents,
311
315
  Fallback: (props) => (_jsx(ToolCallDisplay, { toolCallId: props.toolCallId, toolName: props.toolName, args: props.args, result: props.result, status: resolvePikkuToolStatus(props.status, props.result), addResult: props.addResult })),
312
316
  },
317
+ ...(generativeUIComponents
318
+ ? {
319
+ generativeUI: {
320
+ components: generativeUIComponents,
321
+ },
322
+ }
323
+ : {}),
313
324
  } }), _jsx(MessagePrimitive.If, { last: true, children: _jsx(ThreadPrimitive.If, { running: true, children: _jsx("div", { style: {
314
325
  display: 'flex',
315
326
  alignItems: 'center',
@@ -319,88 +330,112 @@ const AssistantMessage = () => {
319
330
  color: colors.textMuted,
320
331
  }, children: "Thinking..." }) }) })] })] }) }));
321
332
  };
333
+ const ComposerPrefill = ({ text }) => {
334
+ const composer = useComposerRuntime();
335
+ const filled = useRef(false);
336
+ useEffect(() => {
337
+ if (filled.current || !text)
338
+ return;
339
+ filled.current = true;
340
+ if (composer.getState().text === '')
341
+ composer.setText(text);
342
+ }, [text, composer]);
343
+ return null;
344
+ };
322
345
  const PikkuComposer = ({ disabled, }) => {
323
346
  const colors = useContext(ColorsContext);
324
347
  return (_jsx("div", { style: { padding: '8px 0 16px' }, children: _jsx(ComposerPrimitive.Root, { children: _jsxs("div", { style: {
325
- border: `1px solid ${colors.border}`,
326
- borderRadius: 12,
327
- overflow: 'hidden',
348
+ position: 'relative',
349
+ zIndex: 2,
328
350
  display: 'flex',
329
- alignItems: 'flex-end',
330
- padding: '6px 12px',
331
- gap: 8,
351
+ flexDirection: 'column',
352
+ gap: 10,
353
+ backgroundColor: colors.assistantBubble,
354
+ border: `1px solid ${colors.border}`,
355
+ borderRadius: 24,
356
+ padding: '14px 12px 10px',
357
+ boxShadow: darkColors.text === colors.text
358
+ ? '0 14px 30px rgba(0,0,0,0.24)'
359
+ : '0 14px 30px rgba(0,0,0,0.08)',
332
360
  ...(disabled
333
361
  ? { opacity: 0.5, pointerEvents: 'none' }
334
362
  : {}),
335
- }, children: [_jsx(ComposerPrimitive.Input, { placeholder: disabled ? 'Respond to approval request above...' : 'Message...', rows: 2, disabled: disabled, style: {
336
- flex: 1,
363
+ }, children: [_jsx(ComposerPrimitive.Input, { placeholder: disabled ? 'Respond to approval request above...' : 'Message...', rows: 1, disabled: disabled ?? false, style: {
364
+ width: '100%',
365
+ background: 'transparent',
337
366
  border: 'none',
338
367
  outline: 'none',
339
- resize: 'none',
368
+ color: colors.text,
340
369
  fontSize: 14,
341
370
  fontFamily: 'inherit',
342
- padding: '4px 0',
343
- background: colors.inputBg,
344
- color: colors.text,
345
- } }), _jsx(ComposerPrimitive.Send, { disabled: disabled, style: {
346
- width: 28,
347
- height: 28,
348
- borderRadius: '50%',
349
- border: 'none',
350
- background: disabled ? colors.textMuted : colors.sendBg,
351
- color: colors.sendColor,
352
- cursor: disabled ? 'not-allowed' : 'pointer',
371
+ resize: 'none',
372
+ lineHeight: 1.5,
373
+ overflowY: 'auto',
374
+ } }), _jsx("div", { style: {
375
+ width: '100%',
353
376
  display: 'flex',
354
377
  alignItems: 'center',
355
- justifyContent: 'center',
356
- flexShrink: 0,
357
- marginBottom: 2,
358
- fontSize: 14,
359
- }, children: "\u25B6" })] }) }) }));
378
+ gap: 6,
379
+ minWidth: 0,
380
+ justifyContent: 'flex-end',
381
+ }, children: _jsx(ComposerPrimitive.Send, { disabled: disabled ?? false, style: {
382
+ width: 30,
383
+ height: 30,
384
+ borderRadius: 999,
385
+ border: `1px solid ${colors.border}`,
386
+ background: '#e8e8e8',
387
+ color: '#111111',
388
+ cursor: disabled ? 'not-allowed' : 'pointer',
389
+ display: 'flex',
390
+ alignItems: 'center',
391
+ justifyContent: 'center',
392
+ flexShrink: 0,
393
+ transition: 'background-color 150ms ease, color 150ms ease, border-color 150ms ease',
394
+ }, children: "\u2191" }) })] }) }) }));
360
395
  };
361
396
  export function PikkuAgentChat(props) {
362
- const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents, ...runtimeOptions } = props;
397
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents, renderAssistantText, generativeUIComponents, initialPrompt, ...runtimeOptions } = props;
363
398
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } = usePikkuAgentRuntime(runtimeOptions);
364
399
  const colors = dark ? darkColors : lightColors;
365
- return (_jsx(ColorsContext.Provider, { value: colors, children: _jsx(PikkuApprovalContext.Provider, { value: { pendingApprovals, handleApproval }, children: _jsx(HideToolCallsContext.Provider, { value: hideToolCalls, children: _jsx(ToolComponentsContext.Provider, { value: toolComponents, children: _jsx(AssistantRuntimeProvider, { runtime: runtime, children: _jsx("div", { style: {
366
- height: '100%',
367
- display: 'flex',
368
- flexDirection: 'column',
369
- background: colors.bg,
370
- }, children: _jsxs(ThreadPrimitive.Root, { style: {
371
- display: 'flex',
372
- flexDirection: 'column',
373
- flex: 1,
374
- minHeight: 0,
375
- }, children: [_jsx(ThreadPrimitive.Viewport, { style: {
376
- flex: 1,
377
- minHeight: 0,
378
- overflowY: 'auto',
379
- }, children: _jsxs("div", { style: {
380
- maxWidth: maxWidth === 'none' ? undefined : maxWidth,
381
- margin: '0 auto',
382
- padding: 16,
400
+ return (_jsx(ColorsContext.Provider, { value: colors, children: _jsx(PikkuApprovalContext.Provider, { value: { pendingApprovals, handleApproval }, children: _jsx(HideToolCallsContext.Provider, { value: hideToolCalls, children: _jsx(ToolComponentsContext.Provider, { value: toolComponents, children: _jsx(GenerativeUIComponentsContext.Provider, { value: generativeUIComponents, children: _jsx(RenderAssistantTextContext.Provider, { value: renderAssistantText, children: _jsxs(AssistantRuntimeProvider, { runtime: runtime, children: [_jsx(ComposerPrefill, { text: initialPrompt }), _jsx("div", { style: {
401
+ height: '100%',
402
+ display: 'flex',
403
+ flexDirection: 'column',
404
+ background: colors.bg,
405
+ }, children: _jsxs(ThreadPrimitive.Root, { style: {
383
406
  display: 'flex',
384
407
  flexDirection: 'column',
385
- gap: 16,
386
- }, children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { style: {
408
+ flex: 1,
409
+ minHeight: 0,
410
+ }, children: [_jsx(ThreadPrimitive.Viewport, { style: {
411
+ flex: 1,
412
+ minHeight: 0,
413
+ overflowY: 'auto',
414
+ }, children: _jsxs("div", { style: {
415
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
416
+ margin: '0 auto',
417
+ padding: 16,
387
418
  display: 'flex',
388
- alignItems: 'center',
389
- justifyContent: 'center',
390
- minHeight: 300,
391
- color: colors.textMuted,
392
- textAlign: 'center',
393
- fontSize: 14,
394
- }, children: emptyMessage ??
395
- (props.threadId
396
- ? 'Send a message to start the conversation.'
397
- : 'Start a new conversation.') }) }), _jsx(ThreadPrimitive.Messages, { components: {
398
- UserMessage,
399
- AssistantMessage,
400
- } })] }) }), _jsx("div", { style: {
401
- maxWidth: maxWidth === 'none' ? undefined : maxWidth,
402
- margin: '0 auto',
403
- width: '100%',
404
- padding: '0 16px',
405
- }, children: _jsx(PikkuComposer, { disabled: isAwaitingApproval }) })] }) }) }) }) }) }) }));
419
+ flexDirection: 'column',
420
+ gap: 16,
421
+ }, children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { style: {
422
+ display: 'flex',
423
+ alignItems: 'center',
424
+ justifyContent: 'center',
425
+ minHeight: 300,
426
+ color: colors.textMuted,
427
+ textAlign: 'center',
428
+ fontSize: 14,
429
+ }, children: emptyMessage ??
430
+ (props.threadId
431
+ ? 'Send a message to start the conversation.'
432
+ : 'Start a new conversation.') }) }), _jsx(ThreadPrimitive.Messages, { components: {
433
+ UserMessage,
434
+ AssistantMessage,
435
+ } })] }) }), _jsx("div", { style: {
436
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
437
+ margin: '0 auto',
438
+ width: '100%',
439
+ padding: '0 16px',
440
+ }, children: _jsx(PikkuComposer, { disabled: isAwaitingApproval }) })] }) })] }) }) }) }) }) }) }));
406
441
  }
@@ -34,7 +34,7 @@ async function* parseSSEStream(reader) {
34
34
  * Shared helper: consume an SSE stream and populate text/toolCalls.
35
35
  * Returns an array of PendingApprovals when the stream requests them, or empty when done.
36
36
  */
37
- async function processStream(reader, text, toolCalls, yieldContent, onFinish) {
37
+ async function processStream(reader, text, toolCalls, structuredParts, yieldContent, onFinish) {
38
38
  const pendingApprovals = [];
39
39
  for await (const event of parseSSEStream(reader)) {
40
40
  switch (event.type) {
@@ -77,6 +77,31 @@ async function processStream(reader, text, toolCalls, yieldContent, onFinish) {
77
77
  }
78
78
  break;
79
79
  }
80
+ case 'generative-ui': {
81
+ const nextPart = {
82
+ type: 'generative-ui',
83
+ spec: event.spec,
84
+ };
85
+ const existingIndex = structuredParts.findIndex((part) => part.type === 'generative-ui');
86
+ if (existingIndex === -1)
87
+ structuredParts.push(nextPart);
88
+ else
89
+ structuredParts[existingIndex] = nextPart;
90
+ break;
91
+ }
92
+ case 'data': {
93
+ const nextPart = {
94
+ type: 'data',
95
+ name: event.name,
96
+ data: event.data,
97
+ };
98
+ const existingIndex = structuredParts.findIndex((part) => part.type === 'data' && part.name === event.name);
99
+ if (existingIndex === -1)
100
+ structuredParts.push(nextPart);
101
+ else
102
+ structuredParts[existingIndex] = nextPart;
103
+ break;
104
+ }
80
105
  case 'approval-request':
81
106
  pendingApprovals.push({
82
107
  toolCallId: event.toolCallId,
@@ -151,13 +176,32 @@ export function resolvePikkuToolStatus(status, result) {
151
176
  return { type: 'error' };
152
177
  return { type: 'completed' };
153
178
  }
154
- function buildContent(text, toolCalls) {
179
+ function buildRichContent(text, structuredParts, toolCalls) {
155
180
  const content = [];
156
181
  if (text.value)
157
182
  content.push({ type: 'text', text: text.value });
183
+ content.push(...structuredParts);
158
184
  content.push(...toolCalls);
159
185
  return content;
160
186
  }
187
+ function buildContentFromAgentResult(result) {
188
+ const content = [];
189
+ if (typeof result === 'string') {
190
+ if (result)
191
+ content.push({ type: 'text', text: result });
192
+ return content;
193
+ }
194
+ if (!result || typeof result !== 'object')
195
+ return content;
196
+ const record = result;
197
+ if (typeof record.text === 'string' && record.text) {
198
+ content.push({ type: 'text', text: record.text });
199
+ }
200
+ if (record.ui != null) {
201
+ content.push({ type: 'generative-ui', spec: record.ui });
202
+ }
203
+ return content;
204
+ }
161
205
  function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDecisionsRef, setPendingApprovalsRef, onFinishRef) {
162
206
  return {
163
207
  async *run({ messages, abortSignal }) {
@@ -181,6 +225,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
181
225
  // The last resume triggers continuation (next LLM step).
182
226
  let lastText = { value: '' };
183
227
  let lastToolCalls = [];
228
+ let lastStructuredParts = [];
184
229
  let nextApprovals = [];
185
230
  for (let i = 0; i < decisions.length; i++) {
186
231
  const decision = decisions[i];
@@ -209,19 +254,21 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
209
254
  }
210
255
  const text = { value: '' };
211
256
  const toolCalls = [];
257
+ const structuredParts = [];
212
258
  const reader = resumeResponse.body.getReader();
213
- const streamApprovals = await processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
259
+ const streamApprovals = await processStream(reader, text, toolCalls, structuredParts, () => { }, i === decisions.length - 1
214
260
  ? (onFinishRef.current ?? undefined)
215
261
  : undefined);
216
262
  // Keep the last resume's output (it has continuation content)
217
263
  lastText = text;
218
264
  lastToolCalls = toolCalls;
265
+ lastStructuredParts = structuredParts;
219
266
  if (streamApprovals.length > 0) {
220
267
  nextApprovals = streamApprovals;
221
268
  }
222
269
  }
223
270
  // Build content from the last resume's output
224
- const content = buildContent(lastText, lastToolCalls);
271
+ const content = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
225
272
  if (nextApprovals.length > 0) {
226
273
  // More approvals from continuation — show them
227
274
  pendingApprovalsRef.current = nextApprovals;
@@ -249,7 +296,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
249
296
  lastToolCalls.push(approvalToolCall);
250
297
  }
251
298
  }
252
- const updatedContent = buildContent(lastText, lastToolCalls);
299
+ const updatedContent = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
253
300
  yield {
254
301
  content: updatedContent,
255
302
  status: {
@@ -328,14 +375,15 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
328
375
  }
329
376
  const text = { value: '' };
330
377
  const toolCalls = [];
378
+ const structuredParts = [];
331
379
  let pendingContent = null;
332
380
  const yieldContent = () => {
333
- const content = buildContent(text, toolCalls);
381
+ const content = buildRichContent(text, structuredParts, toolCalls);
334
382
  if (content.length > 0)
335
383
  pendingContent = content;
336
384
  };
337
385
  const reader = response.body.getReader();
338
- const approvals = await processStream(reader, text, toolCalls, yieldContent, onFinishRef.current ?? undefined);
386
+ const approvals = await processStream(reader, text, toolCalls, structuredParts, yieldContent, onFinishRef.current ?? undefined);
339
387
  if (approvals.length === 0) {
340
388
  // No approval needed — yield final content and done
341
389
  if (pendingContent) {
@@ -378,7 +426,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
378
426
  toolCalls.splice(i, 1);
379
427
  }
380
428
  }
381
- const content = buildContent(text, toolCalls);
429
+ const content = buildRichContent(text, structuredParts, toolCalls);
382
430
  yield {
383
431
  content,
384
432
  status: {
@@ -446,7 +494,12 @@ export const convertDbMessages = (dbMessages) => {
446
494
  continue;
447
495
  const parts = [];
448
496
  if (msg.content) {
449
- parts.push({ type: 'text', text: msg.content });
497
+ if (Array.isArray(msg.content)) {
498
+ parts.push(...msg.content);
499
+ }
500
+ else {
501
+ parts.push({ type: 'text', text: msg.content });
502
+ }
450
503
  }
451
504
  if (Array.isArray(msg.toolCalls)) {
452
505
  for (const tc of msg.toolCalls) {
@@ -533,6 +586,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
533
586
  // Resume uses SSE (same as streaming mode)
534
587
  let lastText = { value: '' };
535
588
  let lastToolCalls = [];
589
+ let lastStructuredParts = [];
536
590
  let nextApprovals = [];
537
591
  for (let i = 0; i < decisions.length; i++) {
538
592
  const decision = decisions[i];
@@ -560,17 +614,19 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
560
614
  }
561
615
  const text = { value: '' };
562
616
  const toolCalls = [];
617
+ const structuredParts = [];
563
618
  const reader = resumeResponse.body.getReader();
564
- const streamApprovals = await processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
619
+ const streamApprovals = await processStream(reader, text, toolCalls, structuredParts, () => { }, i === decisions.length - 1
565
620
  ? (onFinishRef.current ?? undefined)
566
621
  : undefined);
567
622
  lastText = text;
568
623
  lastToolCalls = toolCalls;
624
+ lastStructuredParts = structuredParts;
569
625
  if (streamApprovals.length > 0) {
570
626
  nextApprovals = streamApprovals;
571
627
  }
572
628
  }
573
- const content = buildContent(lastText, lastToolCalls);
629
+ const content = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
574
630
  if (nextApprovals.length > 0) {
575
631
  pendingApprovalsRef.current = nextApprovals;
576
632
  setPendingApprovalsRef.current(nextApprovals);
@@ -596,7 +652,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
596
652
  lastToolCalls.push(approvalToolCall);
597
653
  }
598
654
  }
599
- const updatedContent = buildContent(lastText, lastToolCalls);
655
+ const updatedContent = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
600
656
  yield {
601
657
  content: updatedContent,
602
658
  status: {
@@ -656,9 +712,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
656
712
  },
657
713
  }));
658
714
  const content = [];
659
- if (json.result) {
660
- content.push({ type: 'text', text: json.result });
661
- }
715
+ content.push(...buildContentFromAgentResult(json.result));
662
716
  content.push(...toolCalls);
663
717
  yield {
664
718
  content,
@@ -671,10 +725,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
671
725
  }
672
726
  // No approvals — yield complete content
673
727
  onFinishRef.current?.();
674
- const content = [];
675
- if (json.result) {
676
- content.push({ type: 'text', text: String(json.result) });
677
- }
728
+ const content = buildContentFromAgentResult(json.result);
678
729
  if (content.length > 0) {
679
730
  yield { content };
680
731
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/assistant-ui",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "type": "module",