@aleph-alpha/chat-kit 3.1.2 → 3.2.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/dist/{CkInput.vue_vue_type_script_setup_true_lang-DKzc21Yp.js → CkInput.vue_vue_type_script_setup_true_lang-B-4xnH2c.js} +1 -1
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +63 -37
- package/dist/chat-kit.css +1 -0
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts +6 -0
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/composables/fileUpload/types.d.ts +3 -0
- package/dist/composables/fileUpload/types.d.ts.map +1 -1
- package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
- package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -1
- package/dist/composables/index.js +1 -1
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +3 -1
- package/dist/handlers/useMarkdownParser.d.ts +1 -0
- package/dist/handlers/useMarkdownParser.d.ts.map +1 -1
- package/dist/helpers/unified/remarkNormalizeMath.d.ts +24 -0
- package/dist/helpers/unified/remarkNormalizeMath.d.ts.map +1 -0
- package/dist/{index-BgQf4bMm.js → index-DAmWGYNH.js} +38 -6
- package/dist/index.js +3 -3
- package/dist/useChatKitLabels-D5gNv8se.js +28704 -0
- package/package.json +9 -4
- package/src/adapters/responses-api.spec.ts +156 -0
- package/src/adapters/responses-api.ts +125 -58
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +50 -0
- package/src/composables/fileUpload/types.ts +6 -0
- package/src/composables/fileUpload/useFileUpload.ts +7 -1
- package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +74 -24
- package/src/composables/fileUpload/useFileUploadWrapper.ts +44 -4
- package/src/handlers/index.ts +4 -0
- package/src/handlers/useMarkdownParser.spec.ts +65 -0
- package/src/handlers/useMarkdownParser.ts +79 -6
- package/src/helpers/unified/remarkNormalizeMath.spec.ts +84 -0
- package/src/helpers/unified/remarkNormalizeMath.ts +134 -0
- package/dist/useChatKitLabels-CLsWWwgG.js +0 -12751
- /package/dist/{purify.es-md74IFlY.js → katex.min-md74IFlY.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aleph-alpha/chat-kit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Chat kit package for building chat interfaces with Vue",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -67,14 +67,16 @@
|
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@aleph-alpha/ui-library": "^2.8.3",
|
|
69
69
|
"dompurify": "^3.3.2",
|
|
70
|
+
"katex": "^0.17.0",
|
|
71
|
+
"rehype-katex": "^7.0.1",
|
|
70
72
|
"rehype-stringify": "^10.0.1",
|
|
71
73
|
"remark-gfm": "^4.0.1",
|
|
74
|
+
"remark-math": "^6.0.0",
|
|
72
75
|
"remark-parse": "^11.0.0",
|
|
73
76
|
"remark-rehype": "^11.1.2",
|
|
74
77
|
"unified": "^11.0.5"
|
|
75
78
|
},
|
|
76
79
|
"devDependencies": {
|
|
77
|
-
"@aleph-alpha/config-css": "^1.2.0",
|
|
78
80
|
"@storybook/addon-a11y": "10.2.17",
|
|
79
81
|
"@storybook/addon-docs": "10.2.17",
|
|
80
82
|
"@storybook/addon-themes": "10.2.17",
|
|
@@ -95,9 +97,12 @@
|
|
|
95
97
|
"unocss-preset-animations": "^1.3.0",
|
|
96
98
|
"unocss-preset-shadcn": "^1.0.1",
|
|
97
99
|
"vite": "^7.3.2",
|
|
98
|
-
"vitest": "
|
|
100
|
+
"vitest": "3.2.6",
|
|
99
101
|
"vue": "^3.5.27",
|
|
100
|
-
"vue-tsc": "^3.2.4"
|
|
102
|
+
"vue-tsc": "^3.2.4",
|
|
103
|
+
"@aleph-alpha/prettier-config-frontend": "2.1.0",
|
|
104
|
+
"@aleph-alpha/tsconfig-frontend": "0.7.0",
|
|
105
|
+
"@aleph-alpha/eslint-config-frontend": "0.7.0"
|
|
101
106
|
},
|
|
102
107
|
"scripts": {
|
|
103
108
|
"build": "vite build && vue-tsc --project tsconfig.build.json --declaration --emitDeclarationOnly --outDir dist",
|
|
@@ -441,6 +441,162 @@ describe('responsesToTree', () => {
|
|
|
441
441
|
}
|
|
442
442
|
});
|
|
443
443
|
|
|
444
|
+
it('merges a tool-loop continuation (response with no user input) into the same assistant turn', () => {
|
|
445
|
+
// One user turn that runs a multi-step tool loop is persisted as several
|
|
446
|
+
// chained responses: the first carries the user input, each follow-up
|
|
447
|
+
// carries a tool result (no user input). They are one assistant turn and
|
|
448
|
+
// must render as one message with a single thinking trail (SPHER-5147).
|
|
449
|
+
const tree = responsesToTree([
|
|
450
|
+
makeResponse({
|
|
451
|
+
id: 'r1',
|
|
452
|
+
request_input: [userInput('Compare Tokyo and Delhi populations')],
|
|
453
|
+
output: [
|
|
454
|
+
reasoningOutput('rs-1', 'search for Tokyo'),
|
|
455
|
+
toolOutput({ id: 'fc-1', type: 'function_call', name: 'search' }),
|
|
456
|
+
],
|
|
457
|
+
}),
|
|
458
|
+
makeResponse({
|
|
459
|
+
id: 'r2',
|
|
460
|
+
previous_response_id: 'r1',
|
|
461
|
+
// Tool-result continuation — no user input on the wire.
|
|
462
|
+
request_input: [],
|
|
463
|
+
completed_at: 1_777_456_999,
|
|
464
|
+
output: [
|
|
465
|
+
reasoningOutput('rs-2', 'now search for Delhi'),
|
|
466
|
+
toolOutput({ id: 'fc-2', type: 'function_call', name: 'search' }),
|
|
467
|
+
assistantOutput('msg-final', 'Tokyo is larger'),
|
|
468
|
+
],
|
|
469
|
+
}),
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
// Exactly one user node and one (merged) assistant node — not two
|
|
473
|
+
// assistant bubbles. r1 has no message output, so its assistant node id
|
|
474
|
+
// falls back to `assistant-r1`.
|
|
475
|
+
expect(Object.keys(tree.nodes)).toHaveLength(2);
|
|
476
|
+
expect(tree.nodes['msg-final']).toBeUndefined();
|
|
477
|
+
|
|
478
|
+
const assistant = tree.nodes['assistant-r1']!;
|
|
479
|
+
expect(assistant.role).toBe('assistant');
|
|
480
|
+
expect(tree.nodes['user-r1-0']!.childrenIds).toEqual(['assistant-r1']);
|
|
481
|
+
// Reasoning + tool from both round-trips, in wire order, then final text.
|
|
482
|
+
expect(assistant.content.map((c) => c.type)).toEqual([
|
|
483
|
+
'reasoning',
|
|
484
|
+
'tool',
|
|
485
|
+
'reasoning',
|
|
486
|
+
'tool',
|
|
487
|
+
'text',
|
|
488
|
+
]);
|
|
489
|
+
// responseId stays the originating round-trip (the one that opened the
|
|
490
|
+
// turn), preserving the documented "originating StoredResponse.id" stamp.
|
|
491
|
+
expect(assistant.metadata.responseId).toBe('r1');
|
|
492
|
+
// createdAt advances to the latest folded round-trip so it is never
|
|
493
|
+
// stamped earlier than content it now holds.
|
|
494
|
+
expect(assistant.createdAt).toEqual(new Date(1_777_456_999 * 1000));
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('merges a tool-loop continuation in a later turn, leaving earlier turns intact', () => {
|
|
498
|
+
// The fix must work for any turn, not just the first: a simple turn,
|
|
499
|
+
// followed by a second turn that runs a tool loop. The second turn's
|
|
500
|
+
// continuation folds into its own assistant node; turn one is untouched.
|
|
501
|
+
const tree = responsesToTree([
|
|
502
|
+
makeResponse({
|
|
503
|
+
id: 'r1',
|
|
504
|
+
request_input: [userInput('Hi')],
|
|
505
|
+
output: [assistantOutput('msg-a1', 'Hello!')],
|
|
506
|
+
}),
|
|
507
|
+
makeResponse({
|
|
508
|
+
id: 'r2',
|
|
509
|
+
previous_response_id: 'r1',
|
|
510
|
+
request_input: [userInput('Now compare Tokyo and Delhi')],
|
|
511
|
+
output: [
|
|
512
|
+
reasoningOutput('rs-2', 'search Tokyo'),
|
|
513
|
+
toolOutput({ id: 'fc-2', type: 'function_call', name: 'search' }),
|
|
514
|
+
],
|
|
515
|
+
}),
|
|
516
|
+
makeResponse({
|
|
517
|
+
id: 'r3',
|
|
518
|
+
previous_response_id: 'r2',
|
|
519
|
+
request_input: [], // tool-result continuation of turn two
|
|
520
|
+
output: [
|
|
521
|
+
reasoningOutput('rs-3', 'search Delhi'),
|
|
522
|
+
toolOutput({ id: 'fc-3', type: 'function_call', name: 'search' }),
|
|
523
|
+
assistantOutput('msg-a3', 'Tokyo is larger'),
|
|
524
|
+
],
|
|
525
|
+
}),
|
|
526
|
+
]);
|
|
527
|
+
|
|
528
|
+
// Turn one: untouched, single reply.
|
|
529
|
+
const a1 = tree.nodes['msg-a1']!;
|
|
530
|
+
expect(getText(a1.content)).toBe('Hello!');
|
|
531
|
+
expect(a1.parentId).toBe('user-r1-0');
|
|
532
|
+
|
|
533
|
+
// Turn two: its continuation (r3) merged into turn two's assistant node
|
|
534
|
+
// (id falls back to `assistant-r2` since r2 has no message output).
|
|
535
|
+
expect(tree.nodes['msg-a3']).toBeUndefined();
|
|
536
|
+
const a2 = tree.nodes['assistant-r2']!;
|
|
537
|
+
expect(a2.parentId).toBe('user-r2-0');
|
|
538
|
+
expect(tree.nodes['user-r2-0']!.parentId).toBe('msg-a1');
|
|
539
|
+
expect(a2.content.map((c) => c.type)).toEqual([
|
|
540
|
+
'reasoning',
|
|
541
|
+
'tool',
|
|
542
|
+
'reasoning',
|
|
543
|
+
'tool',
|
|
544
|
+
'text',
|
|
545
|
+
]);
|
|
546
|
+
// Four message nodes total: two user, two assistant (one per turn).
|
|
547
|
+
expect(Object.keys(tree.nodes)).toHaveLength(4);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('still starts a new assistant turn when a follow-up response carries a user input', () => {
|
|
551
|
+
// Guard against over-merging: a genuine next turn (user input present)
|
|
552
|
+
// must remain its own message, not fold into the previous answer.
|
|
553
|
+
const tree = responsesToTree([
|
|
554
|
+
makeResponse({
|
|
555
|
+
id: 'r1',
|
|
556
|
+
request_input: [userInput('First')],
|
|
557
|
+
output: [assistantOutput('msg-a1', 'A1')],
|
|
558
|
+
}),
|
|
559
|
+
makeResponse({
|
|
560
|
+
id: 'r2',
|
|
561
|
+
previous_response_id: 'r1',
|
|
562
|
+
request_input: [userInput('Second')],
|
|
563
|
+
output: [assistantOutput('msg-a2', 'A2')],
|
|
564
|
+
}),
|
|
565
|
+
]);
|
|
566
|
+
|
|
567
|
+
expect(tree.nodes['user-r2-0']!.parentId).toBe('msg-a1');
|
|
568
|
+
expect(tree.nodes['msg-a2']!.parentId).toBe('user-r2-0');
|
|
569
|
+
expect(tree.nodes['msg-a2']!.metadata.responseId).toBe('r2');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('does not merge a response that carries a user input even when its content is empty', () => {
|
|
573
|
+
// A user-role input with empty content produces no user node, so a
|
|
574
|
+
// node-shape-only check would wrongly treat this as a continuation. The
|
|
575
|
+
// merge keys off `request_input` directly, so the assistant reply stays
|
|
576
|
+
// its own node rather than folding into the previous turn.
|
|
577
|
+
const tree = responsesToTree([
|
|
578
|
+
makeResponse({
|
|
579
|
+
id: 'r1',
|
|
580
|
+
request_input: [userInput('First')],
|
|
581
|
+
output: [assistantOutput('msg-a1', 'A1')],
|
|
582
|
+
}),
|
|
583
|
+
makeResponse({
|
|
584
|
+
id: 'r2',
|
|
585
|
+
previous_response_id: 'r1',
|
|
586
|
+
request_input: [{ role: 'user', content: '' }],
|
|
587
|
+
output: [assistantOutput('msg-a2', 'A2')],
|
|
588
|
+
}),
|
|
589
|
+
]);
|
|
590
|
+
|
|
591
|
+
const a1 = tree.nodes['msg-a1']!;
|
|
592
|
+
const a2 = tree.nodes['msg-a2']!;
|
|
593
|
+
expect(a2).toBeDefined();
|
|
594
|
+
expect(getText(a2.content)).toBe('A2');
|
|
595
|
+
// a2 is its own node, not merged into a1's content.
|
|
596
|
+
expect(getText(a1.content)).toBe('A1');
|
|
597
|
+
expect(a2.parentId).toBe('msg-a1');
|
|
598
|
+
});
|
|
599
|
+
|
|
444
600
|
it('chains the next response under a reasoning-only assistant turn', () => {
|
|
445
601
|
const tree = responsesToTree([
|
|
446
602
|
makeResponse({
|
|
@@ -513,6 +513,124 @@ function responseToMessageNodes(resp: StoredResponse): ResponseMessageNode[] {
|
|
|
513
513
|
return nodes;
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
+
// Insert a node and wire it under `parentId`; latest child wins as the active
|
|
517
|
+
// branch (surfaces the most recent sibling branch by default).
|
|
518
|
+
function appendNode(
|
|
519
|
+
treeNodes: Record<string, ResponseTreeMessage>,
|
|
520
|
+
node: ResponseMessageNode,
|
|
521
|
+
parentId: string | null,
|
|
522
|
+
): void {
|
|
523
|
+
treeNodes[node.id] = {
|
|
524
|
+
id: node.id,
|
|
525
|
+
parentId,
|
|
526
|
+
childrenIds: [],
|
|
527
|
+
activeChildIndex: 0,
|
|
528
|
+
role: node.role,
|
|
529
|
+
content: node.content,
|
|
530
|
+
status: 'completed',
|
|
531
|
+
createdAt: node.createdAt,
|
|
532
|
+
metadata: node.metadata,
|
|
533
|
+
};
|
|
534
|
+
if (parentId === null) return;
|
|
535
|
+
const parent = treeNodes[parentId];
|
|
536
|
+
if (!parent) return;
|
|
537
|
+
parent.childrenIds.push(node.id);
|
|
538
|
+
parent.activeChildIndex = parent.childrenIds.length - 1;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// A user-role input on the wire delimits a turn. Read from `request_input`
|
|
542
|
+
// directly: `responseToMessageNodes` drops empty user inputs, so a node-shape
|
|
543
|
+
// check could misread an empty new turn as a continuation.
|
|
544
|
+
function responseHasUserInput(resp: StoredResponse): boolean {
|
|
545
|
+
return (resp.request_input ?? []).some(
|
|
546
|
+
(input) => (input as { role?: string }).role === 'user',
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// True when `resp` continues the assistant turn anchored at `parentNode`: a
|
|
551
|
+
// tool-loop round-trip (assistant-only output, no new user input) rather than a
|
|
552
|
+
// new turn. The Responses API stores each round-trip as its own response, but
|
|
553
|
+
// they are one turn. New turns and regeneration branches carry a user input.
|
|
554
|
+
function isAssistantContinuation(
|
|
555
|
+
parentNode: ResponseTreeMessage,
|
|
556
|
+
messageNodes: ResponseMessageNode[],
|
|
557
|
+
resp: StoredResponse,
|
|
558
|
+
): boolean {
|
|
559
|
+
return (
|
|
560
|
+
parentNode.role === 'assistant' &&
|
|
561
|
+
messageNodes.length > 0 &&
|
|
562
|
+
messageNodes.every((node) => node.role === 'assistant') &&
|
|
563
|
+
!responseHasUserInput(resp)
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Fold a continuation round-trip's output onto the turn it extends, so the turn
|
|
568
|
+
// stays one message with one "thinking" trail — matching the live path;
|
|
569
|
+
// otherwise the trail splits on reload (SPHER-5147). `responseId` is left as the
|
|
570
|
+
// originating response, per ResponseTreeMessageMetadata's contract.
|
|
571
|
+
function foldContinuation(
|
|
572
|
+
parentNode: ResponseTreeMessage,
|
|
573
|
+
messageNodes: ResponseMessageNode[],
|
|
574
|
+
): void {
|
|
575
|
+
for (const node of messageNodes) {
|
|
576
|
+
parentNode.content.push(...node.content);
|
|
577
|
+
// Advance the turn's timestamp as later round-trips fold in, so it is
|
|
578
|
+
// never stamped earlier than content it now holds (consistent with the
|
|
579
|
+
// per-response `completed_at` stamp used elsewhere). Only moves forward.
|
|
580
|
+
if (node.createdAt.getTime() > parentNode.createdAt.getTime()) {
|
|
581
|
+
parentNode.createdAt = node.createdAt;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Mutable accumulators threaded through the single build pass.
|
|
587
|
+
interface TreeBuildState {
|
|
588
|
+
treeNodes: Record<string, ResponseTreeMessage>;
|
|
589
|
+
// For each response, the id of its last visible message — the attachment
|
|
590
|
+
// point for any future child response that chains onto it. Empty responses
|
|
591
|
+
// fall back to their parent's attachment point so the chain stays connected.
|
|
592
|
+
tailMessageByResponse: Map<string, string | null>;
|
|
593
|
+
rootMessageId: string | null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Wire one response into the tree, extending `state` in place. Tool-loop
|
|
597
|
+
// continuations fold onto the turn they extend; everything else appends under
|
|
598
|
+
// the chain parent resolved from `previous_response_id`.
|
|
599
|
+
function linkResponse(state: TreeBuildState, resp: StoredResponse): void {
|
|
600
|
+
const { treeNodes, tailMessageByResponse } = state;
|
|
601
|
+
const parentResponseId =
|
|
602
|
+
resp.previous_response_id &&
|
|
603
|
+
tailMessageByResponse.has(resp.previous_response_id)
|
|
604
|
+
? resp.previous_response_id
|
|
605
|
+
: null;
|
|
606
|
+
let chainParent =
|
|
607
|
+
parentResponseId !== null
|
|
608
|
+
? (tailMessageByResponse.get(parentResponseId) ?? null)
|
|
609
|
+
: null;
|
|
610
|
+
|
|
611
|
+
const messageNodes = responseToMessageNodes(resp);
|
|
612
|
+
|
|
613
|
+
const parentNode = chainParent !== null ? treeNodes[chainParent] : null;
|
|
614
|
+
if (parentNode && isAssistantContinuation(parentNode, messageNodes, resp)) {
|
|
615
|
+
foldContinuation(parentNode, messageNodes);
|
|
616
|
+
tailMessageByResponse.set(resp.id, parentNode.id);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
let firstId: string | null = null;
|
|
621
|
+
for (const node of messageNodes) {
|
|
622
|
+
appendNode(treeNodes, node, chainParent);
|
|
623
|
+
if (firstId === null) firstId = node.id;
|
|
624
|
+
chainParent = node.id;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (state.rootMessageId === null && firstId !== null) {
|
|
628
|
+
state.rootMessageId = firstId;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
tailMessageByResponse.set(resp.id, chainParent);
|
|
632
|
+
}
|
|
633
|
+
|
|
516
634
|
/**
|
|
517
635
|
* Convert an OpenAI Responses API list (already ordered by `created_at`
|
|
518
636
|
* ascending) into a chat-kit `ConversationTree`.
|
|
@@ -535,66 +653,15 @@ export function responsesToTree(
|
|
|
535
653
|
return { nodes: {}, rootId: null };
|
|
536
654
|
}
|
|
537
655
|
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const tailMessageByResponse = new Map<string, string | null>();
|
|
544
|
-
let rootMessageId: string | null = null;
|
|
545
|
-
|
|
546
|
-
function appendNode(node: ResponseMessageNode, parentId: string | null) {
|
|
547
|
-
treeNodes[node.id] = {
|
|
548
|
-
id: node.id,
|
|
549
|
-
parentId,
|
|
550
|
-
childrenIds: [],
|
|
551
|
-
activeChildIndex: 0,
|
|
552
|
-
role: node.role,
|
|
553
|
-
content: node.content,
|
|
554
|
-
status: 'completed',
|
|
555
|
-
createdAt: node.createdAt,
|
|
556
|
-
metadata: node.metadata,
|
|
557
|
-
};
|
|
558
|
-
if (parentId !== null) {
|
|
559
|
-
const parent = treeNodes[parentId];
|
|
560
|
-
if (parent) {
|
|
561
|
-
parent.childrenIds.push(node.id);
|
|
562
|
-
// Latest insertion wins as the active branch — within a single
|
|
563
|
-
// response there's only one push, so this is a no-op there; for
|
|
564
|
-
// sibling branches it surfaces the most recent one by default.
|
|
565
|
-
parent.activeChildIndex = parent.childrenIds.length - 1;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
656
|
+
const state: TreeBuildState = {
|
|
657
|
+
treeNodes: {},
|
|
658
|
+
tailMessageByResponse: new Map<string, string | null>(),
|
|
659
|
+
rootMessageId: null,
|
|
660
|
+
};
|
|
569
661
|
|
|
570
662
|
for (const resp of responses) {
|
|
571
|
-
|
|
572
|
-
resp.previous_response_id &&
|
|
573
|
-
tailMessageByResponse.has(resp.previous_response_id)
|
|
574
|
-
? resp.previous_response_id
|
|
575
|
-
: null;
|
|
576
|
-
let chainParent =
|
|
577
|
-
parentResponseId !== null
|
|
578
|
-
? (tailMessageByResponse.get(parentResponseId) ?? null)
|
|
579
|
-
: null;
|
|
580
|
-
|
|
581
|
-
const messageNodes = responseToMessageNodes(resp);
|
|
582
|
-
let firstId: string | null = null;
|
|
583
|
-
for (const node of messageNodes) {
|
|
584
|
-
appendNode(node, chainParent);
|
|
585
|
-
if (firstId === null) firstId = node.id;
|
|
586
|
-
chainParent = node.id;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
if (rootMessageId === null && firstId !== null) {
|
|
590
|
-
rootMessageId = firstId;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Track the attachment point for descendants. If the response had no
|
|
594
|
-
// visible messages, pass through the parent's tail so children of this
|
|
595
|
-
// response still land in the right place.
|
|
596
|
-
tailMessageByResponse.set(resp.id, chainParent);
|
|
663
|
+
linkResponse(state, resp);
|
|
597
664
|
}
|
|
598
665
|
|
|
599
|
-
return { nodes: treeNodes, rootId: rootMessageId };
|
|
666
|
+
return { nodes: state.treeNodes, rootId: state.rootMessageId };
|
|
600
667
|
}
|
|
@@ -344,6 +344,56 @@ export const GfmFeatures: Story = {
|
|
|
344
344
|
parameters: { docs: { source: { code: gfmFeaturesTemplateSource } } },
|
|
345
345
|
};
|
|
346
346
|
|
|
347
|
+
const mathTemplateSource = `<script setup lang="ts">
|
|
348
|
+
import { CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
|
|
349
|
+
const content = \`# Math Rendering
|
|
350
|
+
|
|
351
|
+
Inline math: $E = mc^2$
|
|
352
|
+
|
|
353
|
+
Display math:
|
|
354
|
+
|
|
355
|
+
$$
|
|
356
|
+
\\\\int_{-\\\\infty}^{\\\\infty} e^{-x^2} dx = \\\\sqrt{\\\\pi}
|
|
357
|
+
$$
|
|
358
|
+
|
|
359
|
+
The quadratic formula is $x = \\\\frac{-b \\\\pm \\\\sqrt{b^2 - 4ac}}{2a}$.
|
|
360
|
+
\`;
|
|
361
|
+
</script>
|
|
362
|
+
|
|
363
|
+
<template>
|
|
364
|
+
<CkMarkdownRenderer :content="content" />
|
|
365
|
+
</template>`;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Renders inline and display math expressions using KaTeX.
|
|
369
|
+
* Inline math uses single dollar signs ($...$) and display math uses
|
|
370
|
+
* double dollar signs ($$...$$).
|
|
371
|
+
*/
|
|
372
|
+
export const MathExpressions: Story = {
|
|
373
|
+
args: {
|
|
374
|
+
content: [
|
|
375
|
+
'# Math Rendering',
|
|
376
|
+
'',
|
|
377
|
+
'Inline math: $E = mc^2$',
|
|
378
|
+
'',
|
|
379
|
+
'Display math:',
|
|
380
|
+
'',
|
|
381
|
+
'$$',
|
|
382
|
+
'\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}',
|
|
383
|
+
'$$',
|
|
384
|
+
'',
|
|
385
|
+
'The quadratic formula is $x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$.',
|
|
386
|
+
'',
|
|
387
|
+
'A matrix:',
|
|
388
|
+
'',
|
|
389
|
+
'$$',
|
|
390
|
+
'\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}',
|
|
391
|
+
'$$',
|
|
392
|
+
].join('\n'),
|
|
393
|
+
},
|
|
394
|
+
parameters: { docs: { source: { code: mathTemplateSource } } },
|
|
395
|
+
};
|
|
396
|
+
|
|
347
397
|
function visitTextNodes(node: any, transform: (value: string) => string): void {
|
|
348
398
|
if (node.type === 'text') {
|
|
349
399
|
node.value = transform(node.value);
|
|
@@ -34,6 +34,8 @@ export interface EndpointResolver {
|
|
|
34
34
|
files: (storeId: string) => string;
|
|
35
35
|
file: (storeId: string, fileId: string) => string;
|
|
36
36
|
filePublicUrl: (storeId: string, fileId: string) => string;
|
|
37
|
+
workflows: (storeId: string) => string;
|
|
38
|
+
createWorkflow: (storeId: string) => string;
|
|
37
39
|
workflowRun: (storeId: string, workflowId: string) => string;
|
|
38
40
|
workflowRunEventStream: (
|
|
39
41
|
storeId: string,
|
|
@@ -47,6 +49,9 @@ export const defaultEndpoints: EndpointResolver = {
|
|
|
47
49
|
file: (storeId, fileId) => `search-stores/${storeId}/files/${fileId}`,
|
|
48
50
|
filePublicUrl: (storeId, fileId) =>
|
|
49
51
|
`search-stores/${storeId}/files/${fileId}/public-urls?ttl=1h`,
|
|
52
|
+
workflows: (storeId) =>
|
|
53
|
+
`search-stores/${storeId}/workflows?limit=20&offset=0&created_after=&created_before=`,
|
|
54
|
+
createWorkflow: (storeId) => `search-stores/${storeId}/workflows`,
|
|
50
55
|
workflowRun: (storeId, workflowId) =>
|
|
51
56
|
`search-stores/${storeId}/workflows/${workflowId}/runs`,
|
|
52
57
|
workflowRunEventStream: (storeId, workflowId, runId) =>
|
|
@@ -410,6 +415,7 @@ export interface UseFileUploadReturn {
|
|
|
410
415
|
updateFileMetadata: (id: string, meta: Record<string, unknown>) => void;
|
|
411
416
|
clearState: () => void;
|
|
412
417
|
setStoreId: (id: string) => void;
|
|
418
|
+
setWorkflowId: (id: string) => void;
|
|
413
419
|
getPublicUrl: (fileId: string) => Promise<string>;
|
|
414
420
|
|
|
415
421
|
scopeBy: (metadataKey: string, value: string) => ScopedFileView;
|
|
@@ -469,8 +469,9 @@ function deriveStateKey(
|
|
|
469
469
|
export function useFileUpload(
|
|
470
470
|
options: UseFileUploadOptions,
|
|
471
471
|
): UseFileUploadReturn {
|
|
472
|
-
const { client
|
|
472
|
+
const { client } = options;
|
|
473
473
|
let storeId = options.storeId ?? '';
|
|
474
|
+
let workflowId = options.workflowId;
|
|
474
475
|
const endpoints: EndpointResolver = options.endpoints ?? defaultEndpoints;
|
|
475
476
|
const retryPolicy: RetryPolicy = options.retryPolicy ?? DEFAULT_RETRY_POLICY;
|
|
476
477
|
const constraints: FileConstraints = {
|
|
@@ -1257,6 +1258,10 @@ export function useFileUpload(
|
|
|
1257
1258
|
storeId = id;
|
|
1258
1259
|
}
|
|
1259
1260
|
|
|
1261
|
+
function setWorkflowId(id: string) {
|
|
1262
|
+
workflowId = id;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1260
1265
|
async function getPublicUrl(fileId: string): Promise<string> {
|
|
1261
1266
|
const res = await client.post<{ url: string; expires_at: string }>(
|
|
1262
1267
|
endpoints.filePublicUrl(storeId, fileId),
|
|
@@ -1277,6 +1282,7 @@ export function useFileUpload(
|
|
|
1277
1282
|
updateFileMetadata,
|
|
1278
1283
|
clearState,
|
|
1279
1284
|
setStoreId,
|
|
1285
|
+
setWorkflowId,
|
|
1280
1286
|
getPublicUrl,
|
|
1281
1287
|
|
|
1282
1288
|
scopeBy,
|