@m6d/cortex-react 1.0.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/index.d.ts +70 -0
- package/dist/index.js +3 -0
- package/dist/styles.css +2652 -0
- package/dist/theme.css +69 -0
- package/index.ts +17 -0
- package/package.json +67 -0
- package/src/chat-session.tsx +379 -0
- package/src/components/AttachmentQueue.tsx +75 -0
- package/src/components/ChatComposer.tsx +211 -0
- package/src/components/CopyButton.tsx +73 -0
- package/src/components/CortexChatWidget.tsx +538 -0
- package/src/components/JsonTree.tsx +111 -0
- package/src/components/LlmUsageBreakdown.tsx +42 -0
- package/src/components/LlmUsageChips.tsx +37 -0
- package/src/components/Message.tsx +81 -0
- package/src/components/MessageAbortedFlag.tsx +29 -0
- package/src/components/MessageAttachments.tsx +55 -0
- package/src/components/MessageList.tsx +102 -0
- package/src/components/MessageLlmInspector.tsx +184 -0
- package/src/components/MessagePart.tsx +54 -0
- package/src/components/MessageReasoningAnimated.tsx +12 -0
- package/src/components/MessageReasoningPart.tsx +76 -0
- package/src/components/MessageTextPart.tsx +78 -0
- package/src/components/MessageTokenUsage.tsx +145 -0
- package/src/components/MessageToolCallAnimated.tsx +86 -0
- package/src/components/MessageToolCallOutcome.tsx +95 -0
- package/src/components/MessageToolCallPart.tsx +95 -0
- package/src/components/MessageToolCallStatus.tsx +35 -0
- package/src/components/SubtleActivity.tsx +74 -0
- package/src/components/ThreadList.tsx +219 -0
- package/src/components/ToolAnimation.tsx +22 -0
- package/src/components/ToolExecuteCodeAnimated.tsx +8 -0
- package/src/components/ToolQueryGraphAnimated.tsx +8 -0
- package/src/config.ts +14 -0
- package/src/context.ts +48 -0
- package/src/cx.ts +4 -0
- package/src/format.ts +4 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { TextPart, UIMessage } from '@tanstack/ai';
|
|
3
|
+
import { renderMarkdown, StreamTextSmoother } from '@cortex/client';
|
|
4
|
+
import { cx } from '../cx';
|
|
5
|
+
|
|
6
|
+
type MessageTextPartProps = {
|
|
7
|
+
role: UIMessage['role'];
|
|
8
|
+
textPart: TextPart;
|
|
9
|
+
streaming?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function MessageTextPart(props: MessageTextPartProps) {
|
|
13
|
+
const { role, textPart, streaming = false } = props;
|
|
14
|
+
|
|
15
|
+
// Non-streaming text paints on the first frame — only a streaming assistant
|
|
16
|
+
// part starts empty and drains in through the smoother (Angular's constructor
|
|
17
|
+
// effect ran before the first render, so it never committed an empty bubble).
|
|
18
|
+
const initialText = role === 'assistant' && streaming ? '' : textPart.content;
|
|
19
|
+
const [displayedText, setDisplayedText] = useState(initialText);
|
|
20
|
+
const displayedTextRef = useRef(initialText);
|
|
21
|
+
const smootherRef = useRef<StreamTextSmoother | null>(null);
|
|
22
|
+
const isFirstRenderRef = useRef(true);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
function setText(text: string) {
|
|
26
|
+
displayedTextRef.current = text;
|
|
27
|
+
setDisplayedText(text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (role !== 'assistant' || !streaming) {
|
|
31
|
+
smootherRef.current?.destroy();
|
|
32
|
+
smootherRef.current = null;
|
|
33
|
+
setText(textPart.content);
|
|
34
|
+
isFirstRenderRef.current = false;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!smootherRef.current) {
|
|
39
|
+
smootherRef.current = new StreamTextSmoother(setText);
|
|
40
|
+
smootherRef.current.seed(displayedTextRef.current);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isFirstRenderRef.current) {
|
|
44
|
+
isFirstRenderRef.current = false;
|
|
45
|
+
smootherRef.current.seed(textPart.content);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
smootherRef.current.update(textPart.content, false);
|
|
50
|
+
}, [role, streaming, textPart]);
|
|
51
|
+
|
|
52
|
+
useEffect(
|
|
53
|
+
() => () => {
|
|
54
|
+
smootherRef.current?.destroy();
|
|
55
|
+
smootherRef.current = null;
|
|
56
|
+
},
|
|
57
|
+
[],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
className={cx(
|
|
63
|
+
'cortex-text-part',
|
|
64
|
+
role === 'assistant' && 'cortex-text-part--assistant',
|
|
65
|
+
role === 'user' && 'cortex-text-part--user',
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
<div
|
|
69
|
+
className={cx(
|
|
70
|
+
'cortex-text-bubble',
|
|
71
|
+
role === 'assistant' && 'cortex-text-bubble--assistant',
|
|
72
|
+
role === 'user' && 'cortex-text-bubble--user',
|
|
73
|
+
)}
|
|
74
|
+
dangerouslySetInnerHTML={{ __html: renderMarkdown(displayedText) }}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { MessageMetadata } from '@cortex/contracts/wire';
|
|
3
|
+
import { cachePercent } from '@cortex/client';
|
|
4
|
+
import { useCortex } from '../context';
|
|
5
|
+
import { cx } from '../cx';
|
|
6
|
+
import { num } from '../format';
|
|
7
|
+
|
|
8
|
+
type MessageTokenUsageProps = {
|
|
9
|
+
usage: NonNullable<MessageMetadata['tokenUsage']>;
|
|
10
|
+
modelId?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function MessageTokenUsage({ usage, modelId }: MessageTokenUsageProps) {
|
|
14
|
+
const { t } = useCortex();
|
|
15
|
+
const [expanded, setExpanded] = useState(false);
|
|
16
|
+
|
|
17
|
+
const cacheRatio = cachePercent(usage) ?? 0;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={cx('cortex-token-usage', expanded && 'cortex-token-usage--expanded')}>
|
|
21
|
+
<button className="cortex-token-usage__summary" onClick={() => setExpanded((v) => !v)}>
|
|
22
|
+
<span className="cortex-token-usage__total">
|
|
23
|
+
<span className="cortex-token-usage__total-number">{num(usage.total)}</span>
|
|
24
|
+
<span className="cortex-token-usage__total-label">{t('translate_tokens')}</span>
|
|
25
|
+
</span>
|
|
26
|
+
|
|
27
|
+
{modelId ? <span className="cortex-token-usage__model">{modelId}</span> : null}
|
|
28
|
+
|
|
29
|
+
<span className="cortex-token-usage__pills">
|
|
30
|
+
<span className="cortex-token-usage__pill">
|
|
31
|
+
<span className="cortex-token-usage__dot cortex-token-usage__dot--input"></span>
|
|
32
|
+
{num(usage.input.total)}
|
|
33
|
+
</span>
|
|
34
|
+
<span className="cortex-token-usage__pill">
|
|
35
|
+
<span className="cortex-token-usage__dot cortex-token-usage__dot--output"></span>
|
|
36
|
+
{num(usage.output.total)}
|
|
37
|
+
</span>
|
|
38
|
+
</span>
|
|
39
|
+
|
|
40
|
+
{cacheRatio > 0 ? (
|
|
41
|
+
<span className="cortex-token-usage__cache-badge">{cacheRatio}%</span>
|
|
42
|
+
) : null}
|
|
43
|
+
|
|
44
|
+
<svg
|
|
45
|
+
className="cortex-token-usage__chevron"
|
|
46
|
+
width="12"
|
|
47
|
+
height="12"
|
|
48
|
+
viewBox="0 0 12 12"
|
|
49
|
+
fill="none"
|
|
50
|
+
>
|
|
51
|
+
<path
|
|
52
|
+
d="M3 4.5L6 7.5L9 4.5"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
strokeWidth="1.25"
|
|
55
|
+
strokeLinecap="round"
|
|
56
|
+
strokeLinejoin="round"
|
|
57
|
+
/>
|
|
58
|
+
</svg>
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
<div className="cortex-token-usage__details">
|
|
62
|
+
<div className="cortex-token-usage__details-inner">
|
|
63
|
+
<div className="cortex-token-usage__columns">
|
|
64
|
+
<div className="cortex-token-usage__col">
|
|
65
|
+
<div className="cortex-token-usage__col-header">
|
|
66
|
+
<span className="cortex-token-usage__dot cortex-token-usage__dot--input"></span>
|
|
67
|
+
<span className="cortex-token-usage__col-label">{t('translate_input')}</span>
|
|
68
|
+
<span className="cortex-token-usage__col-total">{num(usage.input.total)}</span>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="cortex-token-usage__rows">
|
|
71
|
+
{usage.input.noCache ? (
|
|
72
|
+
<div className="cortex-token-usage__row">
|
|
73
|
+
<span className="cortex-token-usage__row-label">{t('translate_fresh')}</span>
|
|
74
|
+
<span className="cortex-token-usage__row-value">
|
|
75
|
+
{num(usage.input.noCache)}
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
) : null}
|
|
79
|
+
{usage.input.cacheRead ? (
|
|
80
|
+
<div className="cortex-token-usage__row">
|
|
81
|
+
<span className="cortex-token-usage__row-label">
|
|
82
|
+
{t('translate_cache_read')}
|
|
83
|
+
</span>
|
|
84
|
+
<span className="cortex-token-usage__row-value">
|
|
85
|
+
{num(usage.input.cacheRead)}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
) : null}
|
|
89
|
+
{usage.input.cacheWrite ? (
|
|
90
|
+
<div className="cortex-token-usage__row">
|
|
91
|
+
<span className="cortex-token-usage__row-label">
|
|
92
|
+
{t('translate_cache_write')}
|
|
93
|
+
</span>
|
|
94
|
+
<span className="cortex-token-usage__row-value">
|
|
95
|
+
{num(usage.input.cacheWrite)}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
) : null}
|
|
99
|
+
</div>
|
|
100
|
+
{cacheRatio > 0 ? (
|
|
101
|
+
<div className="cortex-token-usage__cache-bar-row">
|
|
102
|
+
<div className="cortex-token-usage__cache-bar">
|
|
103
|
+
<div
|
|
104
|
+
className="cortex-token-usage__cache-fill"
|
|
105
|
+
style={{ width: `${cacheRatio}%` }}
|
|
106
|
+
></div>
|
|
107
|
+
</div>
|
|
108
|
+
<span className="cortex-token-usage__cache-label">
|
|
109
|
+
{t('translate_n_percent_cached', { percent: cacheRatio })}
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
) : null}
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div className="cortex-token-usage__col">
|
|
116
|
+
<div className="cortex-token-usage__col-header">
|
|
117
|
+
<span className="cortex-token-usage__dot cortex-token-usage__dot--output"></span>
|
|
118
|
+
<span className="cortex-token-usage__col-label">{t('translate_output')}</span>
|
|
119
|
+
<span className="cortex-token-usage__col-total">{num(usage.output.total)}</span>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="cortex-token-usage__rows">
|
|
122
|
+
{usage.output.text ? (
|
|
123
|
+
<div className="cortex-token-usage__row">
|
|
124
|
+
<span className="cortex-token-usage__row-label">{t('translate_text')}</span>
|
|
125
|
+
<span className="cortex-token-usage__row-value">{num(usage.output.text)}</span>
|
|
126
|
+
</div>
|
|
127
|
+
) : null}
|
|
128
|
+
{usage.output.reasoning ? (
|
|
129
|
+
<div className="cortex-token-usage__row">
|
|
130
|
+
<span className="cortex-token-usage__row-label">
|
|
131
|
+
{t('translate_reasoning')}
|
|
132
|
+
</span>
|
|
133
|
+
<span className="cortex-token-usage__row-value">
|
|
134
|
+
{num(usage.output.reasoning)}
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
) : null}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ToolCallPart, UIMessage } from '@tanstack/ai';
|
|
2
|
+
import { toolCallAnimation } from '@cortex/client';
|
|
3
|
+
import { cx } from '../cx';
|
|
4
|
+
import { useCortex } from '../context';
|
|
5
|
+
|
|
6
|
+
type MessageToolCallAnimatedProps = {
|
|
7
|
+
message: UIMessage;
|
|
8
|
+
toolCallPart: ToolCallPart;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function MessageToolCallAnimated({ message, toolCallPart }: MessageToolCallAnimatedProps) {
|
|
12
|
+
const { config, t, addToolResult } = useCortex();
|
|
13
|
+
|
|
14
|
+
const Custom = config.toolComponents?.[toolCallPart.name];
|
|
15
|
+
if (Custom) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="cortex-tool-call-animated">
|
|
18
|
+
<Custom
|
|
19
|
+
toolCallPart={toolCallPart}
|
|
20
|
+
message={message}
|
|
21
|
+
setOutput={(output) => addToolResult(toolCallPart.id, toolCallPart.name, output)}
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { state, active, titleKey } = toolCallAnimation(toolCallPart);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="cortex-tool-call-animated">
|
|
31
|
+
<div className="cortex-tool-pill">
|
|
32
|
+
{/* Spinner, then whichever glyph the call settled on */}
|
|
33
|
+
<span className="cortex-tool-pill__icon">
|
|
34
|
+
<span
|
|
35
|
+
className={cx(
|
|
36
|
+
'cortex-tool-pill__spinner',
|
|
37
|
+
active && 'cortex-tool-pill__spinner--visible',
|
|
38
|
+
)}
|
|
39
|
+
></span>
|
|
40
|
+
<svg
|
|
41
|
+
className={cx(
|
|
42
|
+
'cortex-tool-pill__svg',
|
|
43
|
+
'cortex-tool-pill__svg--check',
|
|
44
|
+
state === 'complete' && 'cortex-tool-pill__svg--visible',
|
|
45
|
+
)}
|
|
46
|
+
viewBox="0 0 20 20"
|
|
47
|
+
fill="none"
|
|
48
|
+
>
|
|
49
|
+
<path
|
|
50
|
+
d="M5.5 10.5 L8.5 13.5 L14.5 7"
|
|
51
|
+
stroke="currentColor"
|
|
52
|
+
strokeWidth="2"
|
|
53
|
+
strokeLinecap="round"
|
|
54
|
+
strokeLinejoin="round"
|
|
55
|
+
/>
|
|
56
|
+
</svg>
|
|
57
|
+
<svg
|
|
58
|
+
className={cx(
|
|
59
|
+
'cortex-tool-pill__svg',
|
|
60
|
+
'cortex-tool-pill__svg--error',
|
|
61
|
+
state === 'error' && 'cortex-tool-pill__svg--visible',
|
|
62
|
+
)}
|
|
63
|
+
viewBox="0 0 20 20"
|
|
64
|
+
fill="none"
|
|
65
|
+
>
|
|
66
|
+
<path
|
|
67
|
+
d="M6.5 6.5 L13.5 13.5 M13.5 6.5 L6.5 13.5"
|
|
68
|
+
stroke="currentColor"
|
|
69
|
+
strokeWidth="2"
|
|
70
|
+
strokeLinecap="round"
|
|
71
|
+
/>
|
|
72
|
+
</svg>
|
|
73
|
+
</span>
|
|
74
|
+
|
|
75
|
+
<span
|
|
76
|
+
className={cx(
|
|
77
|
+
'cortex-tool-pill__title',
|
|
78
|
+
state === 'error' && 'cortex-tool-pill__title--error',
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{t(titleKey)}
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ToolCallPart } from '@tanstack/ai';
|
|
2
|
+
import { toolCallOutputText } from '@cortex/client';
|
|
3
|
+
import { useCortex } from '../context';
|
|
4
|
+
import { CopyButton } from './CopyButton';
|
|
5
|
+
import { JsonTree } from './JsonTree';
|
|
6
|
+
|
|
7
|
+
type MessageToolCallOutcomeProps = {
|
|
8
|
+
toolCallPart: ToolCallPart;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* How a tool call ended: its output, its error, or the approval exchange that stopped
|
|
13
|
+
* it. Exactly one of those applies, so this is a single switch on the state rather
|
|
14
|
+
* than the four independent conditions the shape suggests.
|
|
15
|
+
*/
|
|
16
|
+
export function MessageToolCallOutcome({ toolCallPart }: MessageToolCallOutcomeProps) {
|
|
17
|
+
const { t } = useCortex();
|
|
18
|
+
const { state, approval } = toolCallPart;
|
|
19
|
+
const output: unknown = toolCallPart.output;
|
|
20
|
+
const outputText = toolCallOutputText(toolCallPart);
|
|
21
|
+
|
|
22
|
+
function section() {
|
|
23
|
+
if (state === 'complete') {
|
|
24
|
+
return (
|
|
25
|
+
<div className="dbg-tool__section dbg-tool__section--success">
|
|
26
|
+
<div className="dbg-tool__section-bar">
|
|
27
|
+
<span className="dbg-tool__section-label dbg-tool__section-label--success">
|
|
28
|
+
{t('translate_output')}
|
|
29
|
+
</span>
|
|
30
|
+
<span className="dbg-tool__section-lang">json</span>
|
|
31
|
+
<CopyButton value={outputText} />
|
|
32
|
+
</div>
|
|
33
|
+
<div dir="ltr" className="dbg-tool__tree">
|
|
34
|
+
<JsonTree data={output} expandDepth={2} />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (state === 'error') {
|
|
41
|
+
return (
|
|
42
|
+
<div className="dbg-tool__section dbg-tool__section--error">
|
|
43
|
+
<div className="dbg-tool__section-bar">
|
|
44
|
+
<span className="dbg-tool__section-label dbg-tool__section-label--error">
|
|
45
|
+
{t('translate_error')}
|
|
46
|
+
</span>
|
|
47
|
+
<CopyButton value={outputText} />
|
|
48
|
+
</div>
|
|
49
|
+
<pre dir="ltr" className="dbg-tool__error-pre">
|
|
50
|
+
{outputText}
|
|
51
|
+
</pre>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (state === 'approval-requested') {
|
|
57
|
+
return (
|
|
58
|
+
<div className="dbg-tool__section dbg-tool__section--approval">
|
|
59
|
+
<div className="dbg-tool__section-bar">
|
|
60
|
+
<span className="dbg-tool__section-label dbg-tool__section-label--approval">
|
|
61
|
+
{t('translate_approval_requested')}
|
|
62
|
+
</span>
|
|
63
|
+
<span className="dbg-tool__section-lang">{approval?.id}</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="dbg-tool__message dbg-tool__message--approval">
|
|
66
|
+
{t('translate_waiting_for_approval')}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (state === 'approval-responded') {
|
|
73
|
+
return (
|
|
74
|
+
<div className="dbg-tool__section dbg-tool__section--approval">
|
|
75
|
+
<div className="dbg-tool__section-bar">
|
|
76
|
+
<span className="dbg-tool__section-label dbg-tool__section-label--approval">
|
|
77
|
+
{t('translate_approval_response')}
|
|
78
|
+
</span>
|
|
79
|
+
<span className="dbg-tool__section-lang">{approval?.id}</span>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="dbg-tool__message dbg-tool__message--approval">
|
|
82
|
+
{t(approval?.approved ? 'translate_tool_approved' : 'translate_tool_response_received')}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Angular renders this component inside its own host element, which keeps the
|
|
92
|
+
// outcome section out of `.dbg-tool__section:not(:first-child)` separators.
|
|
93
|
+
// This wrapper is that host's equivalent.
|
|
94
|
+
return <div>{section()}</div>;
|
|
95
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ToolCallPart } from '@tanstack/ai';
|
|
2
|
+
import { highlightCode, splitToolCallInput } from '@cortex/client';
|
|
3
|
+
import { useCortex } from '../context';
|
|
4
|
+
import { CopyButton } from './CopyButton';
|
|
5
|
+
import { JsonTree } from './JsonTree';
|
|
6
|
+
import { MessageToolCallOutcome } from './MessageToolCallOutcome';
|
|
7
|
+
import { MessageToolCallStatus } from './MessageToolCallStatus';
|
|
8
|
+
|
|
9
|
+
type MessageToolCallPartProps = {
|
|
10
|
+
toolCallPart: ToolCallPart;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One tool call in the debug view: what it was asked to do, and — delegated to the
|
|
15
|
+
* status badge and outcome panel — where it got to and how it ended.
|
|
16
|
+
*/
|
|
17
|
+
export function MessageToolCallPart({ toolCallPart }: MessageToolCallPartProps) {
|
|
18
|
+
const { t } = useCortex();
|
|
19
|
+
|
|
20
|
+
const { codeSnippets, remainingInput, remainingInputText } = splitToolCallInput(
|
|
21
|
+
toolCallPart.input,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<details className="dbg-tool" data-state={toolCallPart.state}>
|
|
26
|
+
<summary className="dbg-tool__summary">
|
|
27
|
+
<div className="dbg-tool__header">
|
|
28
|
+
<div className="dbg-tool__meta">
|
|
29
|
+
<div className="dbg-tool__title-row">
|
|
30
|
+
<span className="dbg-tool__name" title={toolCallPart.name}>
|
|
31
|
+
{toolCallPart.name}
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="dbg-tool__id-row">
|
|
35
|
+
<span className="dbg-tool__id">{toolCallPart.id}</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div className="dbg-tool__actions">
|
|
40
|
+
<MessageToolCallStatus toolCallPart={toolCallPart} />
|
|
41
|
+
|
|
42
|
+
<svg
|
|
43
|
+
className="dbg-tool__chevron"
|
|
44
|
+
width="14"
|
|
45
|
+
height="14"
|
|
46
|
+
viewBox="0 0 20 20"
|
|
47
|
+
fill="none"
|
|
48
|
+
>
|
|
49
|
+
<path
|
|
50
|
+
d="m5.75 8.25 4.25 4.25 4.25-4.25"
|
|
51
|
+
stroke="currentColor"
|
|
52
|
+
strokeWidth="1.5"
|
|
53
|
+
strokeLinecap="round"
|
|
54
|
+
strokeLinejoin="round"
|
|
55
|
+
/>
|
|
56
|
+
</svg>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</summary>
|
|
60
|
+
|
|
61
|
+
<div className="dbg-tool__body">
|
|
62
|
+
{codeSnippets.map((snippet) => (
|
|
63
|
+
<div className="dbg-tool__section" key={snippet.key}>
|
|
64
|
+
<div className="dbg-tool__section-bar">
|
|
65
|
+
<span className="dbg-tool__section-label">{snippet.key}</span>
|
|
66
|
+
<span className="dbg-tool__section-lang">{snippet.lang}</span>
|
|
67
|
+
<CopyButton value={snippet.value} />
|
|
68
|
+
</div>
|
|
69
|
+
<pre dir="ltr" className="dbg-tool__pre">
|
|
70
|
+
<code
|
|
71
|
+
className="hljs"
|
|
72
|
+
dangerouslySetInnerHTML={{ __html: highlightCode(snippet.value, snippet.lang) }}
|
|
73
|
+
></code>
|
|
74
|
+
</pre>
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
|
|
78
|
+
{!!remainingInput && (
|
|
79
|
+
<div className="dbg-tool__section">
|
|
80
|
+
<div className="dbg-tool__section-bar">
|
|
81
|
+
<span className="dbg-tool__section-label">{t('translate_input')}</span>
|
|
82
|
+
<span className="dbg-tool__section-lang">json</span>
|
|
83
|
+
<CopyButton value={remainingInputText} />
|
|
84
|
+
</div>
|
|
85
|
+
<div dir="ltr" className="dbg-tool__tree">
|
|
86
|
+
<JsonTree data={remainingInput} expandDepth={2} />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
<MessageToolCallOutcome toolCallPart={toolCallPart} />
|
|
92
|
+
</div>
|
|
93
|
+
</details>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ToolCallPart } from '@tanstack/ai';
|
|
2
|
+
import { toolCallBadge } from '@cortex/client';
|
|
3
|
+
import { cx } from '../cx';
|
|
4
|
+
import { useCortex } from '../context';
|
|
5
|
+
|
|
6
|
+
type MessageToolCallStatusProps = {
|
|
7
|
+
toolCallPart: ToolCallPart;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The badge in a tool call's header saying where the call has got to. Split out of
|
|
12
|
+
* the tool-call part so that part is left describing layout rather than branching
|
|
13
|
+
* seven ways on a state it does not otherwise care about.
|
|
14
|
+
*/
|
|
15
|
+
export function MessageToolCallStatus({ toolCallPart }: MessageToolCallStatusProps) {
|
|
16
|
+
const { t } = useCortex();
|
|
17
|
+
const badge = toolCallBadge(toolCallPart);
|
|
18
|
+
|
|
19
|
+
// The outer span stands in for Angular's <cortex-message-tool-call-status>
|
|
20
|
+
// host, keeping the badge itself out of flex-item blockification.
|
|
21
|
+
return (
|
|
22
|
+
<span>
|
|
23
|
+
<span
|
|
24
|
+
className={cx('dbg-tool__state', badge.modifier && `dbg-tool__state--${badge.modifier}`)}
|
|
25
|
+
>
|
|
26
|
+
{badge.pulse && (
|
|
27
|
+
<span
|
|
28
|
+
className={cx('dbg-tool__pulse', badge.pulse === 'violet' && 'dbg-tool__pulse--violet')}
|
|
29
|
+
></span>
|
|
30
|
+
)}
|
|
31
|
+
{t(badge.labelKey)}
|
|
32
|
+
</span>
|
|
33
|
+
</span>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useCortex } from '../context';
|
|
3
|
+
import { cx } from '../cx';
|
|
4
|
+
|
|
5
|
+
type LabelState = 'idle' | 'exiting' | 'enter-start' | 'entering';
|
|
6
|
+
|
|
7
|
+
type SubtleActivityProps = {
|
|
8
|
+
labels: string[];
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function SubtleActivity({ labels, className }: SubtleActivityProps) {
|
|
13
|
+
const { t } = useCortex();
|
|
14
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
15
|
+
const [labelState, setLabelState] = useState<LabelState>('idle');
|
|
16
|
+
|
|
17
|
+
// Read through a ref so a changing labels array never restarts the cycle.
|
|
18
|
+
const labelCount = useRef(labels.length);
|
|
19
|
+
labelCount.current = labels.length;
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
// The three timeouts run in sequence, so one handle covers all of them.
|
|
23
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
24
|
+
let frame: number | undefined;
|
|
25
|
+
|
|
26
|
+
function scheduleNextTransition() {
|
|
27
|
+
timeout = setTimeout(() => {
|
|
28
|
+
setLabelState('exiting');
|
|
29
|
+
|
|
30
|
+
timeout = setTimeout(() => {
|
|
31
|
+
setCurrentIndex((i) => (i + 1) % labelCount.current);
|
|
32
|
+
setLabelState('enter-start');
|
|
33
|
+
|
|
34
|
+
frame = requestAnimationFrame(() => {
|
|
35
|
+
setLabelState('entering');
|
|
36
|
+
|
|
37
|
+
timeout = setTimeout(() => {
|
|
38
|
+
setLabelState('idle');
|
|
39
|
+
scheduleNextTransition();
|
|
40
|
+
}, 300);
|
|
41
|
+
});
|
|
42
|
+
}, 300);
|
|
43
|
+
}, 2000);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
scheduleNextTransition();
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
if (timeout) clearTimeout(timeout);
|
|
50
|
+
if (frame) cancelAnimationFrame(frame);
|
|
51
|
+
};
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={cx('cortex-subtle-activity', className)}>
|
|
56
|
+
<div className="cortex-subtle-activity__dots">
|
|
57
|
+
<span className="cortex-subtle-activity__dot"></span>
|
|
58
|
+
<span className="cortex-subtle-activity__dot cortex-subtle-activity__dot--d1"></span>
|
|
59
|
+
<span className="cortex-subtle-activity__dot cortex-subtle-activity__dot--d2"></span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="cortex-subtle-activity__label-mask">
|
|
63
|
+
<span
|
|
64
|
+
className={cx(
|
|
65
|
+
'cortex-subtle-activity__label',
|
|
66
|
+
`cortex-subtle-activity__label--${labelState}`,
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{t(labels[currentIndex])}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|