@pikku/assistant-ui 0.12.4 → 0.12.6
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/CHANGELOG.md +12 -1
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.js +6 -1
- package/dist/cjs/model-capabilities.d.ts +1 -0
- package/dist/cjs/model-capabilities.js +16 -0
- package/dist/cjs/pikku-agent-chat.d.ts +4 -0
- package/dist/cjs/pikku-agent-chat.js +91 -63
- package/dist/cjs/use-file-attachment.d.ts +26 -0
- package/dist/cjs/use-file-attachment.js +99 -0
- package/dist/cjs/use-pikku-agent-runtime.d.ts +4 -0
- package/dist/cjs/use-pikku-agent-runtime.js +72 -35
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/model-capabilities.d.ts +1 -0
- package/dist/esm/model-capabilities.js +13 -0
- package/dist/esm/pikku-agent-chat.d.ts +4 -0
- package/dist/esm/pikku-agent-chat.js +102 -67
- package/dist/esm/use-file-attachment.d.ts +26 -0
- package/dist/esm/use-file-attachment.js +85 -0
- package/dist/esm/use-pikku-agent-runtime.d.ts +4 -0
- package/dist/esm/use-pikku-agent-runtime.js +72 -19
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/index.ts +3 -0
- package/src/model-capabilities.ts +14 -0
- package/src/pikku-agent-chat.tsx +362 -156
- package/src/use-file-attachment.ts +110 -0
- package/src/use-pikku-agent-runtime.ts +109 -15
|
@@ -73,7 +73,7 @@ function parseSSEStream(reader) {
|
|
|
73
73
|
* Shared helper: consume an SSE stream and populate text/toolCalls.
|
|
74
74
|
* Returns an array of PendingApprovals when the stream requests them, or empty when done.
|
|
75
75
|
*/
|
|
76
|
-
function processStream(reader, text, toolCalls, yieldContent, onFinish) {
|
|
76
|
+
function processStream(reader, text, toolCalls, structuredParts, yieldContent, onFinish) {
|
|
77
77
|
return __awaiter(this, void 0, void 0, function* () {
|
|
78
78
|
var _a, e_1, _b, _c;
|
|
79
79
|
const pendingApprovals = [];
|
|
@@ -122,6 +122,31 @@ function processStream(reader, text, toolCalls, yieldContent, onFinish) {
|
|
|
122
122
|
}
|
|
123
123
|
break;
|
|
124
124
|
}
|
|
125
|
+
case 'generative-ui': {
|
|
126
|
+
const nextPart = {
|
|
127
|
+
type: 'generative-ui',
|
|
128
|
+
spec: event.spec,
|
|
129
|
+
};
|
|
130
|
+
const existingIndex = structuredParts.findIndex((part) => part.type === 'generative-ui');
|
|
131
|
+
if (existingIndex === -1)
|
|
132
|
+
structuredParts.push(nextPart);
|
|
133
|
+
else
|
|
134
|
+
structuredParts[existingIndex] = nextPart;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'data': {
|
|
138
|
+
const nextPart = {
|
|
139
|
+
type: 'data',
|
|
140
|
+
name: event.name,
|
|
141
|
+
data: event.data,
|
|
142
|
+
};
|
|
143
|
+
const existingIndex = structuredParts.findIndex((part) => part.type === 'data' && part.name === event.name);
|
|
144
|
+
if (existingIndex === -1)
|
|
145
|
+
structuredParts.push(nextPart);
|
|
146
|
+
else
|
|
147
|
+
structuredParts[existingIndex] = nextPart;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
125
150
|
case 'approval-request':
|
|
126
151
|
pendingApprovals.push({
|
|
127
152
|
toolCallId: event.toolCallId,
|
|
@@ -205,13 +230,32 @@ function resolvePikkuToolStatus(status, result) {
|
|
|
205
230
|
return { type: 'error' };
|
|
206
231
|
return { type: 'completed' };
|
|
207
232
|
}
|
|
208
|
-
function
|
|
233
|
+
function buildRichContent(text, structuredParts, toolCalls) {
|
|
209
234
|
const content = [];
|
|
210
235
|
if (text.value)
|
|
211
236
|
content.push({ type: 'text', text: text.value });
|
|
237
|
+
content.push(...structuredParts);
|
|
212
238
|
content.push(...toolCalls);
|
|
213
239
|
return content;
|
|
214
240
|
}
|
|
241
|
+
function buildContentFromAgentResult(result) {
|
|
242
|
+
const content = [];
|
|
243
|
+
if (typeof result === 'string') {
|
|
244
|
+
if (result)
|
|
245
|
+
content.push({ type: 'text', text: result });
|
|
246
|
+
return content;
|
|
247
|
+
}
|
|
248
|
+
if (!result || typeof result !== 'object')
|
|
249
|
+
return content;
|
|
250
|
+
const record = result;
|
|
251
|
+
if (typeof record.text === 'string' && record.text) {
|
|
252
|
+
content.push({ type: 'text', text: record.text });
|
|
253
|
+
}
|
|
254
|
+
if (record.ui != null) {
|
|
255
|
+
content.push({ type: 'generative-ui', spec: record.ui });
|
|
256
|
+
}
|
|
257
|
+
return content;
|
|
258
|
+
}
|
|
215
259
|
function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDecisionsRef, setPendingApprovalsRef, onFinishRef) {
|
|
216
260
|
return {
|
|
217
261
|
run(_a) {
|
|
@@ -237,6 +281,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
|
|
|
237
281
|
// The last resume triggers continuation (next LLM step).
|
|
238
282
|
let lastText = { value: '' };
|
|
239
283
|
let lastToolCalls = [];
|
|
284
|
+
let lastStructuredParts = [];
|
|
240
285
|
let nextApprovals = [];
|
|
241
286
|
for (let i = 0; i < decisions.length; i++) {
|
|
242
287
|
const decision = decisions[i];
|
|
@@ -262,8 +307,9 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
|
|
|
262
307
|
}
|
|
263
308
|
const text = { value: '' };
|
|
264
309
|
const toolCalls = [];
|
|
310
|
+
const structuredParts = [];
|
|
265
311
|
const reader = resumeResponse.body.getReader();
|
|
266
|
-
const streamApprovals = yield __await(processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
|
|
312
|
+
const streamApprovals = yield __await(processStream(reader, text, toolCalls, structuredParts, () => { }, i === decisions.length - 1
|
|
267
313
|
? ((_d = onFinishRef.current) !== null && _d !== void 0 ? _d : undefined)
|
|
268
314
|
: undefined)
|
|
269
315
|
// Keep the last resume's output (it has continuation content)
|
|
@@ -271,12 +317,13 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
|
|
|
271
317
|
// Keep the last resume's output (it has continuation content)
|
|
272
318
|
lastText = text;
|
|
273
319
|
lastToolCalls = toolCalls;
|
|
320
|
+
lastStructuredParts = structuredParts;
|
|
274
321
|
if (streamApprovals.length > 0) {
|
|
275
322
|
nextApprovals = streamApprovals;
|
|
276
323
|
}
|
|
277
324
|
}
|
|
278
325
|
// Build content from the last resume's output
|
|
279
|
-
const content =
|
|
326
|
+
const content = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
|
|
280
327
|
if (nextApprovals.length > 0) {
|
|
281
328
|
// More approvals from continuation — show them
|
|
282
329
|
pendingApprovalsRef.current = nextApprovals;
|
|
@@ -301,7 +348,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
|
|
|
301
348
|
lastToolCalls.push(approvalToolCall);
|
|
302
349
|
}
|
|
303
350
|
}
|
|
304
|
-
const updatedContent =
|
|
351
|
+
const updatedContent = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
|
|
305
352
|
yield yield __await({
|
|
306
353
|
content: updatedContent,
|
|
307
354
|
status: {
|
|
@@ -332,14 +379,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
|
|
|
332
379
|
response = yield __await(fetch(`${opts.api}/${opts.agentName}/stream`, {
|
|
333
380
|
method: 'POST',
|
|
334
381
|
headers: Object.assign({ 'Content-Type': 'application/json' }, opts.headers),
|
|
335
|
-
body: JSON.stringify({
|
|
336
|
-
agentName: opts.agentName,
|
|
337
|
-
message: messageText,
|
|
338
|
-
threadId: opts.threadId,
|
|
339
|
-
resourceId: opts.resourceId,
|
|
340
|
-
model: opts.model,
|
|
341
|
-
temperature: opts.temperature,
|
|
342
|
-
}),
|
|
382
|
+
body: JSON.stringify(Object.assign({ agentName: opts.agentName, message: messageText, threadId: opts.threadId, resourceId: opts.resourceId, model: opts.model, temperature: opts.temperature }, (opts.context ? { context: opts.context } : {}))),
|
|
343
383
|
signal: abortSignal,
|
|
344
384
|
credentials: opts.credentials,
|
|
345
385
|
}));
|
|
@@ -380,14 +420,15 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
|
|
|
380
420
|
}
|
|
381
421
|
const text = { value: '' };
|
|
382
422
|
const toolCalls = [];
|
|
423
|
+
const structuredParts = [];
|
|
383
424
|
let pendingContent = null;
|
|
384
425
|
const yieldContent = () => {
|
|
385
|
-
const content =
|
|
426
|
+
const content = buildRichContent(text, structuredParts, toolCalls);
|
|
386
427
|
if (content.length > 0)
|
|
387
428
|
pendingContent = content;
|
|
388
429
|
};
|
|
389
430
|
const reader = response.body.getReader();
|
|
390
|
-
const approvals = yield __await(processStream(reader, text, toolCalls, yieldContent, (_e = onFinishRef.current) !== null && _e !== void 0 ? _e : undefined));
|
|
431
|
+
const approvals = yield __await(processStream(reader, text, toolCalls, structuredParts, yieldContent, (_e = onFinishRef.current) !== null && _e !== void 0 ? _e : undefined));
|
|
391
432
|
if (approvals.length === 0) {
|
|
392
433
|
// No approval needed — yield final content and done
|
|
393
434
|
if (pendingContent) {
|
|
@@ -427,7 +468,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
|
|
|
427
468
|
toolCalls.splice(i, 1);
|
|
428
469
|
}
|
|
429
470
|
}
|
|
430
|
-
const content =
|
|
471
|
+
const content = buildRichContent(text, structuredParts, toolCalls);
|
|
431
472
|
yield yield __await({
|
|
432
473
|
content,
|
|
433
474
|
status: {
|
|
@@ -497,7 +538,12 @@ const convertDbMessages = (dbMessages) => {
|
|
|
497
538
|
continue;
|
|
498
539
|
const parts = [];
|
|
499
540
|
if (msg.content) {
|
|
500
|
-
|
|
541
|
+
if (Array.isArray(msg.content)) {
|
|
542
|
+
parts.push(...msg.content);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
parts.push({ type: 'text', text: msg.content });
|
|
546
|
+
}
|
|
501
547
|
}
|
|
502
548
|
if (Array.isArray(msg.toolCalls)) {
|
|
503
549
|
for (const tc of msg.toolCalls) {
|
|
@@ -587,6 +633,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
|
|
|
587
633
|
// Resume uses SSE (same as streaming mode)
|
|
588
634
|
let lastText = { value: '' };
|
|
589
635
|
let lastToolCalls = [];
|
|
636
|
+
let lastStructuredParts = [];
|
|
590
637
|
let nextApprovals = [];
|
|
591
638
|
for (let i = 0; i < decisions.length; i++) {
|
|
592
639
|
const decision = decisions[i];
|
|
@@ -611,17 +658,19 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
|
|
|
611
658
|
}
|
|
612
659
|
const text = { value: '' };
|
|
613
660
|
const toolCalls = [];
|
|
661
|
+
const structuredParts = [];
|
|
614
662
|
const reader = resumeResponse.body.getReader();
|
|
615
|
-
const streamApprovals = yield __await(processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
|
|
663
|
+
const streamApprovals = yield __await(processStream(reader, text, toolCalls, structuredParts, () => { }, i === decisions.length - 1
|
|
616
664
|
? ((_d = onFinishRef.current) !== null && _d !== void 0 ? _d : undefined)
|
|
617
665
|
: undefined));
|
|
618
666
|
lastText = text;
|
|
619
667
|
lastToolCalls = toolCalls;
|
|
668
|
+
lastStructuredParts = structuredParts;
|
|
620
669
|
if (streamApprovals.length > 0) {
|
|
621
670
|
nextApprovals = streamApprovals;
|
|
622
671
|
}
|
|
623
672
|
}
|
|
624
|
-
const content =
|
|
673
|
+
const content = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
|
|
625
674
|
if (nextApprovals.length > 0) {
|
|
626
675
|
pendingApprovalsRef.current = nextApprovals;
|
|
627
676
|
setPendingApprovalsRef.current(nextApprovals);
|
|
@@ -644,7 +693,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
|
|
|
644
693
|
lastToolCalls.push(approvalToolCall);
|
|
645
694
|
}
|
|
646
695
|
}
|
|
647
|
-
const updatedContent =
|
|
696
|
+
const updatedContent = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
|
|
648
697
|
yield yield __await({
|
|
649
698
|
content: updatedContent,
|
|
650
699
|
status: {
|
|
@@ -673,14 +722,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
|
|
|
673
722
|
const response = yield __await(fetch(`${opts.api}/${opts.agentName}`, {
|
|
674
723
|
method: 'POST',
|
|
675
724
|
headers: Object.assign({ 'Content-Type': 'application/json' }, opts.headers),
|
|
676
|
-
body: JSON.stringify({
|
|
677
|
-
agentName: opts.agentName,
|
|
678
|
-
message: messageText,
|
|
679
|
-
threadId: opts.threadId,
|
|
680
|
-
resourceId: opts.resourceId,
|
|
681
|
-
model: opts.model,
|
|
682
|
-
temperature: opts.temperature,
|
|
683
|
-
}),
|
|
725
|
+
body: JSON.stringify(Object.assign({ agentName: opts.agentName, message: messageText, threadId: opts.threadId, resourceId: opts.resourceId, model: opts.model, temperature: opts.temperature }, (opts.context ? { context: opts.context } : {}))),
|
|
684
726
|
signal: abortSignal,
|
|
685
727
|
credentials: opts.credentials,
|
|
686
728
|
}));
|
|
@@ -701,9 +743,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
|
|
|
701
743
|
: {})), (approval.reason ? { __approvalReason: approval.reason } : {})),
|
|
702
744
|
}));
|
|
703
745
|
const content = [];
|
|
704
|
-
|
|
705
|
-
content.push({ type: 'text', text: json.result });
|
|
706
|
-
}
|
|
746
|
+
content.push(...buildContentFromAgentResult(json.result));
|
|
707
747
|
content.push(...toolCalls);
|
|
708
748
|
yield yield __await({
|
|
709
749
|
content,
|
|
@@ -716,10 +756,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
|
|
|
716
756
|
}
|
|
717
757
|
// No approvals — yield complete content
|
|
718
758
|
(_f = onFinishRef.current) === null || _f === void 0 ? void 0 : _f.call(onFinishRef);
|
|
719
|
-
const content =
|
|
720
|
-
if (json.result) {
|
|
721
|
-
content.push({ type: 'text', text: String(json.result) });
|
|
722
|
-
}
|
|
759
|
+
const content = buildContentFromAgentResult(json.result);
|
|
723
760
|
if (content.length > 0) {
|
|
724
761
|
yield yield __await({ content });
|
|
725
762
|
}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -2,3 +2,6 @@ export { usePikkuAgentRuntime, usePikkuAgentNonStreamingRuntime, PikkuApprovalCo
|
|
|
2
2
|
export type { PikkuAgentRuntimeOptions, PendingApproval, PikkuApprovalContextValue, PikkuToolStatusType, PikkuToolStatus, MissingCredentialPayload, } from './use-pikku-agent-runtime.js';
|
|
3
3
|
export { PikkuAgentChat } from './pikku-agent-chat.js';
|
|
4
4
|
export type { PikkuAgentChatProps } from './pikku-agent-chat.js';
|
|
5
|
+
export { useFileAttachment, INLINE_SIZE_LIMIT } from './use-file-attachment.js';
|
|
6
|
+
export type { PendingFile, UploadAttachmentFn } from './use-file-attachment.js';
|
|
7
|
+
export { modelSupportsVision } from './model-capabilities.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { usePikkuAgentRuntime, usePikkuAgentNonStreamingRuntime, PikkuApprovalContext, usePikkuApproval, convertDbMessages, isDeniedResult, resolvePikkuToolStatus, } from './use-pikku-agent-runtime.js';
|
|
2
2
|
export { PikkuAgentChat } from './pikku-agent-chat.js';
|
|
3
|
+
export { useFileAttachment, INLINE_SIZE_LIMIT } from './use-file-attachment.js';
|
|
4
|
+
export { modelSupportsVision } from './model-capabilities.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function modelSupportsVision(modelID: string): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const MODEL_VISION_SUPPORT = new Set([
|
|
2
|
+
'claude-sonnet-4-5',
|
|
3
|
+
'claude-opus-4-1',
|
|
4
|
+
'gpt-4o-mini',
|
|
5
|
+
'gpt-5',
|
|
6
|
+
'gpt-5-mini',
|
|
7
|
+
'gpt-5-codex',
|
|
8
|
+
'gemini-2.5-pro',
|
|
9
|
+
'gemini-2.5-flash',
|
|
10
|
+
]);
|
|
11
|
+
export function modelSupportsVision(modelID) {
|
|
12
|
+
return MODEL_VISION_SUPPORT.has(modelID);
|
|
13
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type ComponentType, type ReactNode } from 'react';
|
|
1
2
|
import { type ToolCallMessagePartComponent } from '@assistant-ui/react';
|
|
2
3
|
import { type PikkuAgentRuntimeOptions } from './use-pikku-agent-runtime.js';
|
|
3
4
|
export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
|
|
5
|
+
initialPrompt?: string;
|
|
4
6
|
emptyMessage?: string;
|
|
5
7
|
/** Hide tool calls from the chat display.
|
|
6
8
|
* - `true`: hide all non-approval tool calls
|
|
@@ -21,5 +23,7 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
|
|
|
21
23
|
* (which still respects `hideToolCalls` and the approval-request UI).
|
|
22
24
|
*/
|
|
23
25
|
toolComponents?: Record<string, ToolCallMessagePartComponent>;
|
|
26
|
+
renderAssistantText?: (text: string) => ReactNode;
|
|
27
|
+
generativeUIComponents?: Record<string, ComponentType<any>>;
|
|
24
28
|
}
|
|
25
29
|
export declare function PikkuAgentChat(props: PikkuAgentChatProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -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
|
-
|
|
326
|
-
|
|
327
|
-
overflow: 'hidden',
|
|
348
|
+
position: 'relative',
|
|
349
|
+
zIndex: 2,
|
|
328
350
|
display: 'flex',
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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:
|
|
336
|
-
|
|
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
|
-
|
|
368
|
+
color: colors.text,
|
|
340
369
|
fontSize: 14,
|
|
341
370
|
fontFamily: 'inherit',
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
} }), _jsx(
|
|
346
|
-
width:
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type PendingFile = {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
previewUrl: string;
|
|
6
|
+
contentUrl: string;
|
|
7
|
+
isImage: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type UploadAttachmentFn = (args: {
|
|
10
|
+
contentType: string;
|
|
11
|
+
sizeBytes: number;
|
|
12
|
+
}) => Promise<{
|
|
13
|
+
uploadUrl: string;
|
|
14
|
+
signedReadUrl: string;
|
|
15
|
+
uploadMethod?: string;
|
|
16
|
+
}>;
|
|
17
|
+
export declare const INLINE_SIZE_LIMIT: number;
|
|
18
|
+
export declare function useFileAttachment(upload: UploadAttachmentFn): {
|
|
19
|
+
pendingFiles: PendingFile[];
|
|
20
|
+
uploading: boolean;
|
|
21
|
+
uploadError: string | null;
|
|
22
|
+
fileInputRef: import("react").RefObject<HTMLInputElement | null>;
|
|
23
|
+
handleFileChange: (e: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
|
|
24
|
+
removeFile: (id: string) => void;
|
|
25
|
+
clearFiles: () => void;
|
|
26
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
export const INLINE_SIZE_LIMIT = 1 * 1024 * 1024;
|
|
3
|
+
export function useFileAttachment(upload) {
|
|
4
|
+
const [pendingFiles, setPendingFiles] = useState([]);
|
|
5
|
+
const [uploading, setUploading] = useState(false);
|
|
6
|
+
const [uploadError, setUploadError] = useState(null);
|
|
7
|
+
const fileInputRef = useRef(null);
|
|
8
|
+
const handleFileChange = useCallback(async (e) => {
|
|
9
|
+
const files = Array.from(e.target.files ?? []);
|
|
10
|
+
e.target.value = '';
|
|
11
|
+
if (!files.length)
|
|
12
|
+
return;
|
|
13
|
+
setUploading(true);
|
|
14
|
+
setUploadError(null);
|
|
15
|
+
try {
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const previewUrl = URL.createObjectURL(file);
|
|
18
|
+
let contentUrl;
|
|
19
|
+
if (file.type.startsWith('image/') &&
|
|
20
|
+
file.size <= INLINE_SIZE_LIMIT) {
|
|
21
|
+
contentUrl = await new Promise((resolve, reject) => {
|
|
22
|
+
const reader = new FileReader();
|
|
23
|
+
reader.onload = (ev) => resolve(ev.target.result);
|
|
24
|
+
reader.onerror = reject;
|
|
25
|
+
reader.readAsDataURL(file);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const { uploadUrl, signedReadUrl, uploadMethod } = await upload({
|
|
30
|
+
contentType: file.type,
|
|
31
|
+
sizeBytes: file.size,
|
|
32
|
+
});
|
|
33
|
+
const resp = await fetch(uploadUrl, {
|
|
34
|
+
method: uploadMethod ?? 'PUT',
|
|
35
|
+
body: file,
|
|
36
|
+
headers: { 'Content-Type': file.type },
|
|
37
|
+
});
|
|
38
|
+
if (!resp.ok)
|
|
39
|
+
throw new Error(`Upload failed (${resp.status})`);
|
|
40
|
+
contentUrl = signedReadUrl;
|
|
41
|
+
}
|
|
42
|
+
setPendingFiles((prev) => [
|
|
43
|
+
...prev,
|
|
44
|
+
{
|
|
45
|
+
id: `file_${Date.now().toString(36)}`,
|
|
46
|
+
name: file.name,
|
|
47
|
+
mimeType: file.type,
|
|
48
|
+
previewUrl,
|
|
49
|
+
contentUrl,
|
|
50
|
+
isImage: file.type.startsWith('image/'),
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
setUploadError(err instanceof Error ? err.message : 'Upload failed');
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
setUploading(false);
|
|
60
|
+
}
|
|
61
|
+
}, [upload]);
|
|
62
|
+
const removeFile = useCallback((id) => {
|
|
63
|
+
setPendingFiles((prev) => {
|
|
64
|
+
const f = prev.find((x) => x.id === id);
|
|
65
|
+
if (f)
|
|
66
|
+
URL.revokeObjectURL(f.previewUrl);
|
|
67
|
+
return prev.filter((x) => x.id !== id);
|
|
68
|
+
});
|
|
69
|
+
}, []);
|
|
70
|
+
const clearFiles = useCallback(() => {
|
|
71
|
+
setPendingFiles((prev) => {
|
|
72
|
+
prev.forEach((f) => URL.revokeObjectURL(f.previewUrl));
|
|
73
|
+
return [];
|
|
74
|
+
});
|
|
75
|
+
}, []);
|
|
76
|
+
return {
|
|
77
|
+
pendingFiles,
|
|
78
|
+
uploading,
|
|
79
|
+
uploadError,
|
|
80
|
+
fileInputRef,
|
|
81
|
+
handleFileChange,
|
|
82
|
+
removeFile,
|
|
83
|
+
clearFiles,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -10,6 +10,10 @@ export interface PikkuAgentRuntimeOptions {
|
|
|
10
10
|
headers?: Record<string, string>;
|
|
11
11
|
model?: string;
|
|
12
12
|
temperature?: number;
|
|
13
|
+
/** Structured context injected into the agent's system instructions.
|
|
14
|
+
* Provide upfront state (e.g. current org/project/branch/deployment IDs)
|
|
15
|
+
* so the agent can call tools without asking the user. */
|
|
16
|
+
context?: string;
|
|
13
17
|
}
|
|
14
18
|
export interface PendingApproval {
|
|
15
19
|
toolCallId: string;
|