@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,37 @@
|
|
|
1
|
+
import type { TokenUsage } from '@cortex/contracts/wire';
|
|
2
|
+
import { cachePercent } from '@cortex/client';
|
|
3
|
+
import { num } from '../format';
|
|
4
|
+
|
|
5
|
+
type LlmUsageChipsProps = {
|
|
6
|
+
usage: TokenUsage;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The one-line token summary on a collapsed LLM request: in, out, and how much of
|
|
11
|
+
* the input came from cache.
|
|
12
|
+
*/
|
|
13
|
+
export function LlmUsageChips({ usage }: LlmUsageChipsProps) {
|
|
14
|
+
const percent = cachePercent(usage);
|
|
15
|
+
|
|
16
|
+
// The outer span stands in for Angular's <cortex-llm-usage-chips> host: as
|
|
17
|
+
// the flex item it keeps the metrics span itself out of flex-item
|
|
18
|
+
// blockification (display and auto-margin resolution differ otherwise).
|
|
19
|
+
return (
|
|
20
|
+
<span>
|
|
21
|
+
<span className="cortex-llm-inspector__step-metrics">
|
|
22
|
+
<span className="cortex-llm-inspector__metric">
|
|
23
|
+
<span className="cortex-llm-inspector__dot cortex-llm-inspector__dot--input"></span>
|
|
24
|
+
{num(usage.input.total)}
|
|
25
|
+
</span>
|
|
26
|
+
<span className="cortex-llm-inspector__metric">
|
|
27
|
+
<span className="cortex-llm-inspector__dot cortex-llm-inspector__dot--output"></span>
|
|
28
|
+
{num(usage.output.total)}
|
|
29
|
+
</span>
|
|
30
|
+
{/* Not a truthiness check: a genuine 0% is falsy and would silently vanish. */}
|
|
31
|
+
{percent !== null ? (
|
|
32
|
+
<span className="cortex-llm-inspector__cache-pct">{percent}%</span>
|
|
33
|
+
) : null}
|
|
34
|
+
</span>
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { UIMessage } from '@tanstack/ai';
|
|
2
|
+
import { isHiddenInAnimatedMode, newestAssistantMessage } from '@cortex/client';
|
|
3
|
+
import { useCortex } from '../context';
|
|
4
|
+
import { MessageAbortedFlag } from './MessageAbortedFlag';
|
|
5
|
+
import { MessageAttachments } from './MessageAttachments';
|
|
6
|
+
import { MessageLlmInspector } from './MessageLlmInspector';
|
|
7
|
+
import { MessagePart } from './MessagePart';
|
|
8
|
+
import { MessageTokenUsage } from './MessageTokenUsage';
|
|
9
|
+
|
|
10
|
+
type MessageProps = {
|
|
11
|
+
message: UIMessage;
|
|
12
|
+
debugMode?: boolean;
|
|
13
|
+
animate?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function Message(props: MessageProps) {
|
|
17
|
+
const { message, debugMode = false, animate = false } = props;
|
|
18
|
+
const { messages, isAgentWorking, messageMetadata } = useCortex();
|
|
19
|
+
|
|
20
|
+
const isStreaming = isAgentWorking && newestAssistantMessage(messages)?.id === message.id;
|
|
21
|
+
const isAssistant = message.role === 'assistant';
|
|
22
|
+
|
|
23
|
+
const parts = message.parts;
|
|
24
|
+
const visibleParts = parts.filter((part, index) => {
|
|
25
|
+
// A tool result is already rendered as the outcome of the call it answers.
|
|
26
|
+
if (part.type === 'tool-result') return false;
|
|
27
|
+
|
|
28
|
+
return debugMode || !isHiddenInAnimatedMode(part, index === parts.length - 1, isStreaming);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** Only a streaming message's last part can still be receiving text. */
|
|
32
|
+
const streamingPartIndex = isStreaming ? visibleParts.length - 1 : -1;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Usage, model id, the aborted flag and attachments are cortex's own envelope
|
|
36
|
+
* around the SDK's message rather than fields on it, so they are looked up by
|
|
37
|
+
* id against the transcript the server served.
|
|
38
|
+
*/
|
|
39
|
+
const metadata = messageMetadata.get(message.id);
|
|
40
|
+
const isAborted = Boolean(metadata?.isAborted);
|
|
41
|
+
const tokenUsage = metadata?.tokenUsage;
|
|
42
|
+
const attachments = message.role === 'user' ? (metadata?.attachments ?? []) : [];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The debug strip only makes sense once the message has settled, and only where
|
|
46
|
+
* there is something to show: usage figures, or an assistant turn whose LLM
|
|
47
|
+
* requests can be inspected.
|
|
48
|
+
*/
|
|
49
|
+
const showsDebugZone = debugMode && !isStreaming && (Boolean(tokenUsage) || isAssistant);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="cortex-message">
|
|
53
|
+
{visibleParts.length > 0 && (
|
|
54
|
+
<div className="cortex-message-parts">
|
|
55
|
+
{visibleParts.map((part, index) => (
|
|
56
|
+
<MessagePart
|
|
57
|
+
key={index}
|
|
58
|
+
part={part}
|
|
59
|
+
message={message}
|
|
60
|
+
debugMode={debugMode}
|
|
61
|
+
animate={animate}
|
|
62
|
+
streaming={index === streamingPartIndex}
|
|
63
|
+
/>
|
|
64
|
+
))}
|
|
65
|
+
|
|
66
|
+
{attachments.length > 0 && <MessageAttachments attachments={attachments} />}
|
|
67
|
+
|
|
68
|
+
{isAborted && <MessageAbortedFlag />}
|
|
69
|
+
|
|
70
|
+
{showsDebugZone && (
|
|
71
|
+
<div className="cortex-message-debug-zone">
|
|
72
|
+
{tokenUsage && <MessageTokenUsage usage={tokenUsage} modelId={metadata?.modelId} />}
|
|
73
|
+
|
|
74
|
+
{isAssistant && <MessageLlmInspector messageId={message.id} />}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useCortex } from '../context';
|
|
2
|
+
|
|
3
|
+
export function MessageAbortedFlag() {
|
|
4
|
+
const { t } = useCortex();
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<div className="cortex-aborted-flag">
|
|
8
|
+
<span className="cortex-aborted-flag__line" />
|
|
9
|
+
<span className="cortex-aborted-flag__label">
|
|
10
|
+
<svg
|
|
11
|
+
className="cortex-aborted-flag__icon"
|
|
12
|
+
width="12"
|
|
13
|
+
height="12"
|
|
14
|
+
viewBox="0 0 12 12"
|
|
15
|
+
fill="none"
|
|
16
|
+
>
|
|
17
|
+
<path
|
|
18
|
+
d="M6 1.5v5M6 8.75v.5"
|
|
19
|
+
stroke="currentColor"
|
|
20
|
+
strokeWidth="1.4"
|
|
21
|
+
strokeLinecap="round"
|
|
22
|
+
/>
|
|
23
|
+
</svg>
|
|
24
|
+
{t('translate_aborted')}
|
|
25
|
+
</span>
|
|
26
|
+
<span className="cortex-aborted-flag__line" />
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AttachmentSummary } from '@cortex/contracts/wire';
|
|
2
|
+
import { saveBlob } from '@cortex/client';
|
|
3
|
+
import { useCortex } from '../context';
|
|
4
|
+
|
|
5
|
+
type MessageAttachmentsProps = {
|
|
6
|
+
attachments: AttachmentSummary[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** The files a user message was sent with, as recorded by the server. */
|
|
10
|
+
export function MessageAttachments(props: MessageAttachmentsProps) {
|
|
11
|
+
const { api, t } = useCortex();
|
|
12
|
+
|
|
13
|
+
async function download(id: string) {
|
|
14
|
+
const { blob, name } = await api.downloadAttachment(id);
|
|
15
|
+
saveBlob(blob, name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="cortex-message-attachments">
|
|
20
|
+
{props.attachments.map((attachment) => (
|
|
21
|
+
<button
|
|
22
|
+
key={attachment.id}
|
|
23
|
+
type="button"
|
|
24
|
+
onClick={() => void download(attachment.id)}
|
|
25
|
+
className="cortex-message-attachment"
|
|
26
|
+
aria-label={`${t('translate_download')}: ${attachment.filename}`}
|
|
27
|
+
>
|
|
28
|
+
<svg viewBox="0 0 16 16" className="cortex-message-attachment__icon" fill="none">
|
|
29
|
+
<path
|
|
30
|
+
d="M4 1.5h5.172a2 2 0 0 1 1.414.586l2.328 2.328a2 2 0 0 1 .586 1.414V12.5a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2Z"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
strokeWidth="1.25"
|
|
33
|
+
/>
|
|
34
|
+
<path
|
|
35
|
+
d="M9.5 1.5v2a2 2 0 0 0 2 2h2"
|
|
36
|
+
stroke="currentColor"
|
|
37
|
+
strokeWidth="1.25"
|
|
38
|
+
strokeLinecap="round"
|
|
39
|
+
/>
|
|
40
|
+
</svg>
|
|
41
|
+
<span className="cortex-message-attachment__name">{attachment.filename}</span>
|
|
42
|
+
<svg viewBox="0 0 16 16" className="cortex-message-attachment__dl-icon" fill="none">
|
|
43
|
+
<path
|
|
44
|
+
d="M8 3v7m0 0L5.5 7.5M8 10l2.5-2.5M3 13h10"
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
strokeWidth="1.5"
|
|
47
|
+
strokeLinecap="round"
|
|
48
|
+
strokeLinejoin="round"
|
|
49
|
+
/>
|
|
50
|
+
</svg>
|
|
51
|
+
</button>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useCortex } from '../context';
|
|
3
|
+
import { Message } from './Message';
|
|
4
|
+
|
|
5
|
+
type MessageListProps = {
|
|
6
|
+
className?: string;
|
|
7
|
+
debugMode: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function MessageList(props: MessageListProps) {
|
|
11
|
+
const { messages, selectedThread } = useCortex();
|
|
12
|
+
|
|
13
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
const shouldScrollToBottom = useRef(true);
|
|
15
|
+
const isNearBottom = useRef(true);
|
|
16
|
+
const scrollToBottomQueued = useRef(false);
|
|
17
|
+
const [animateNewParts, setAnimateNewParts] = useState(false);
|
|
18
|
+
|
|
19
|
+
function scrollToBottom() {
|
|
20
|
+
const el = containerRef.current;
|
|
21
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function scheduleScrollToBottom() {
|
|
25
|
+
if (scrollToBottomQueued.current) return;
|
|
26
|
+
|
|
27
|
+
scrollToBottomQueued.current = true;
|
|
28
|
+
|
|
29
|
+
queueMicrotask(() => {
|
|
30
|
+
scrollToBottomQueued.current = false;
|
|
31
|
+
scrollToBottom();
|
|
32
|
+
shouldScrollToBottom.current = false;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const el = containerRef.current;
|
|
38
|
+
scheduleScrollToBottom();
|
|
39
|
+
setAnimateNewParts(true);
|
|
40
|
+
|
|
41
|
+
if (!el || typeof MutationObserver === 'undefined') return;
|
|
42
|
+
|
|
43
|
+
const observer = new MutationObserver(() => {
|
|
44
|
+
if (!shouldScrollToBottom.current && !isNearBottom.current) return;
|
|
45
|
+
|
|
46
|
+
scheduleScrollToBottom();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
observer.observe(el, { childList: true, subtree: true, characterData: true });
|
|
50
|
+
|
|
51
|
+
return () => observer.disconnect();
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
// There is no event bus here, so selecting a thread IS Angular's
|
|
56
|
+
// `onThreadSelected`: the transcript it loads arrives unanimated.
|
|
57
|
+
const threadId = selectedThread?.id;
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setAnimateNewParts(false);
|
|
60
|
+
shouldScrollToBottom.current = true;
|
|
61
|
+
queueMicrotask(() => setAnimateNewParts(true));
|
|
62
|
+
}, [threadId]);
|
|
63
|
+
|
|
64
|
+
// ...and a message appearing under the user's name IS `onSend`.
|
|
65
|
+
const lastMessage = messages[messages.length - 1];
|
|
66
|
+
const lastUserMessageId = lastMessage?.role === 'user' ? lastMessage.id : undefined;
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (lastUserMessageId) shouldScrollToBottom.current = true;
|
|
69
|
+
}, [lastUserMessageId]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!shouldScrollToBottom.current && !isNearBottom.current) return;
|
|
73
|
+
|
|
74
|
+
scheduleScrollToBottom();
|
|
75
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
+
}, [messages]);
|
|
77
|
+
|
|
78
|
+
function onScroll() {
|
|
79
|
+
const el = containerRef.current;
|
|
80
|
+
if (!el) return;
|
|
81
|
+
isNearBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < 100;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Two nested elements on purpose: Angular's host carried the widget's
|
|
85
|
+
// `cortex-widget__messages` (flex sizing, overflow hidden) around the
|
|
86
|
+
// scrolling `cortex-message-list`. Merging them would stack conflicting
|
|
87
|
+
// overflow rules on one node.
|
|
88
|
+
return (
|
|
89
|
+
<div className={props.className}>
|
|
90
|
+
<div ref={containerRef} className="cortex-message-list" onScroll={onScroll}>
|
|
91
|
+
{messages.map((message) => (
|
|
92
|
+
<Message
|
|
93
|
+
key={message.id}
|
|
94
|
+
message={message}
|
|
95
|
+
debugMode={props.debugMode}
|
|
96
|
+
animate={animateNewParts}
|
|
97
|
+
/>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { parseJsonText, prettyJsonText, type LlmRequest } from '@cortex/client';
|
|
3
|
+
import { useCortex } from '../context';
|
|
4
|
+
import { cx } from '../cx';
|
|
5
|
+
import { CopyButton } from './CopyButton';
|
|
6
|
+
import { JsonTree } from './JsonTree';
|
|
7
|
+
import { LlmUsageBreakdown } from './LlmUsageBreakdown';
|
|
8
|
+
import { LlmUsageChips } from './LlmUsageChips';
|
|
9
|
+
|
|
10
|
+
type MessageLlmInspectorProps = {
|
|
11
|
+
messageId: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function MessageLlmInspector({ messageId }: MessageLlmInspectorProps) {
|
|
15
|
+
const { t, api } = useCortex();
|
|
16
|
+
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [requests, setRequests] = useState<LlmRequest[]>([]);
|
|
20
|
+
const [expandedStep, setExpandedStep] = useState<string | null>(null);
|
|
21
|
+
const [activeTab, setActiveTab] = useState<Record<string, 'prompt' | 'response'>>({});
|
|
22
|
+
|
|
23
|
+
const loaded = useRef(false);
|
|
24
|
+
|
|
25
|
+
async function toggle() {
|
|
26
|
+
if (!loaded.current) {
|
|
27
|
+
setLoading(true);
|
|
28
|
+
try {
|
|
29
|
+
setRequests(await api.listLlmRequests(messageId));
|
|
30
|
+
} finally {
|
|
31
|
+
setLoading(false);
|
|
32
|
+
loaded.current = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setOpen((v) => !v);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toggleStep(id: string) {
|
|
40
|
+
setExpandedStep((current) => (current === id ? null : id));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getTab(id: string) {
|
|
44
|
+
return activeTab[id] ?? 'prompt';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setTab(id: string, tab: 'prompt' | 'response') {
|
|
48
|
+
setActiveTab((current) => ({ ...current, [id]: tab }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={cx('cortex-llm-inspector', open && 'cortex-llm-inspector--open')}>
|
|
53
|
+
<button className="cortex-llm-inspector__trigger" onClick={() => void toggle()}>
|
|
54
|
+
<svg
|
|
55
|
+
className="cortex-llm-inspector__icon"
|
|
56
|
+
width="14"
|
|
57
|
+
height="14"
|
|
58
|
+
viewBox="0 0 16 16"
|
|
59
|
+
fill="none"
|
|
60
|
+
>
|
|
61
|
+
<path
|
|
62
|
+
d="M6 2a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM0 6a6 6 0 1 1 10.89 3.477l4.817 4.816a1 1 0 0 1-1.414 1.414l-4.816-4.816A6 6 0 0 1 0 6Z"
|
|
63
|
+
fill="currentColor"
|
|
64
|
+
/>
|
|
65
|
+
</svg>
|
|
66
|
+
<span>{t('translate_inspect_llm_requests')}</span>
|
|
67
|
+
|
|
68
|
+
{loading ? (
|
|
69
|
+
<span className="cortex-llm-inspector__loading">{t('translate_loading')}</span>
|
|
70
|
+
) : requests.length > 0 ? (
|
|
71
|
+
<span className="cortex-llm-inspector__badge">{requests.length}</span>
|
|
72
|
+
) : null}
|
|
73
|
+
|
|
74
|
+
<svg
|
|
75
|
+
className="cortex-llm-inspector__chevron"
|
|
76
|
+
width="12"
|
|
77
|
+
height="12"
|
|
78
|
+
viewBox="0 0 12 12"
|
|
79
|
+
fill="none"
|
|
80
|
+
>
|
|
81
|
+
<path
|
|
82
|
+
d="M3 4.5L6 7.5L9 4.5"
|
|
83
|
+
stroke="currentColor"
|
|
84
|
+
strokeWidth="1.25"
|
|
85
|
+
strokeLinecap="round"
|
|
86
|
+
strokeLinejoin="round"
|
|
87
|
+
/>
|
|
88
|
+
</svg>
|
|
89
|
+
</button>
|
|
90
|
+
|
|
91
|
+
<div className="cortex-llm-inspector__panel-wrapper">
|
|
92
|
+
<div className="cortex-llm-inspector__panel-inner">
|
|
93
|
+
<div className="cortex-llm-inspector__panel">
|
|
94
|
+
{requests.length === 0 && !loading ? (
|
|
95
|
+
<div className="cortex-llm-inspector__empty">{t('translate_no_llm_requests')}</div>
|
|
96
|
+
) : null}
|
|
97
|
+
|
|
98
|
+
{requests.map((req, idx) => (
|
|
99
|
+
<div
|
|
100
|
+
key={req.id}
|
|
101
|
+
className={cx(
|
|
102
|
+
'cortex-llm-inspector__step',
|
|
103
|
+
expandedStep === req.id && 'cortex-llm-inspector__step--expanded',
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
<button
|
|
107
|
+
className="cortex-llm-inspector__step-header"
|
|
108
|
+
onClick={() => toggleStep(req.id)}
|
|
109
|
+
>
|
|
110
|
+
<span className="cortex-llm-inspector__step-label">
|
|
111
|
+
{t('translate_step_n', { number: idx + 1 })}
|
|
112
|
+
</span>
|
|
113
|
+
|
|
114
|
+
{req.tokenUsage ? <LlmUsageChips usage={req.tokenUsage} /> : null}
|
|
115
|
+
|
|
116
|
+
<svg
|
|
117
|
+
className="cortex-llm-inspector__step-chevron"
|
|
118
|
+
width="10"
|
|
119
|
+
height="10"
|
|
120
|
+
viewBox="0 0 12 12"
|
|
121
|
+
fill="none"
|
|
122
|
+
>
|
|
123
|
+
<path
|
|
124
|
+
d="M3 4.5L6 7.5L9 4.5"
|
|
125
|
+
stroke="currentColor"
|
|
126
|
+
strokeWidth="1.25"
|
|
127
|
+
strokeLinecap="round"
|
|
128
|
+
strokeLinejoin="round"
|
|
129
|
+
/>
|
|
130
|
+
</svg>
|
|
131
|
+
</button>
|
|
132
|
+
|
|
133
|
+
<div className="cortex-llm-inspector__step-body-wrapper">
|
|
134
|
+
<div className="cortex-llm-inspector__step-body-inner">
|
|
135
|
+
<div className="cortex-llm-inspector__step-body">
|
|
136
|
+
{req.tokenUsage ? <LlmUsageBreakdown usage={req.tokenUsage} /> : null}
|
|
137
|
+
|
|
138
|
+
<div className="cortex-llm-inspector__tabs">
|
|
139
|
+
<button
|
|
140
|
+
className={cx(
|
|
141
|
+
'cortex-llm-inspector__tab',
|
|
142
|
+
getTab(req.id) === 'prompt' && 'cortex-llm-inspector__tab--active',
|
|
143
|
+
)}
|
|
144
|
+
onClick={() => setTab(req.id, 'prompt')}
|
|
145
|
+
>
|
|
146
|
+
{t('translate_request')}
|
|
147
|
+
</button>
|
|
148
|
+
<button
|
|
149
|
+
className={cx(
|
|
150
|
+
'cortex-llm-inspector__tab',
|
|
151
|
+
getTab(req.id) === 'response' && 'cortex-llm-inspector__tab--active',
|
|
152
|
+
)}
|
|
153
|
+
onClick={() => setTab(req.id, 'response')}
|
|
154
|
+
>
|
|
155
|
+
{t('translate_response')}
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div className="cortex-llm-inspector__json-pane">
|
|
160
|
+
<CopyButton
|
|
161
|
+
className="cortex-llm-inspector__json-copy"
|
|
162
|
+
value={
|
|
163
|
+
getTab(req.id) === 'prompt'
|
|
164
|
+
? prettyJsonText(req.prompt)
|
|
165
|
+
: prettyJsonText(req.output)
|
|
166
|
+
}
|
|
167
|
+
/>
|
|
168
|
+
{getTab(req.id) === 'prompt' ? (
|
|
169
|
+
<JsonTree data={parseJsonText(req.prompt)} expandDepth={2} />
|
|
170
|
+
) : (
|
|
171
|
+
<JsonTree data={parseJsonText(req.output)} expandDepth={2} />
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { MessagePart as MessagePartData, UIMessage } from '@tanstack/ai';
|
|
2
|
+
import { useCortex } from '../context';
|
|
3
|
+
import { cx } from '../cx';
|
|
4
|
+
import { MessageReasoningAnimated } from './MessageReasoningAnimated';
|
|
5
|
+
import { MessageReasoningPart } from './MessageReasoningPart';
|
|
6
|
+
import { MessageTextPart } from './MessageTextPart';
|
|
7
|
+
import { MessageToolCallPart } from './MessageToolCallPart';
|
|
8
|
+
import { ToolAnimation } from './ToolAnimation';
|
|
9
|
+
|
|
10
|
+
type MessagePartProps = {
|
|
11
|
+
message: UIMessage;
|
|
12
|
+
part: MessagePartData;
|
|
13
|
+
debugMode?: boolean;
|
|
14
|
+
animate?: boolean;
|
|
15
|
+
/** A part carries no state of its own, so being live is decided by position. */
|
|
16
|
+
streaming?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function MessagePart(props: MessagePartProps) {
|
|
20
|
+
const { message, part, debugMode = false, animate = false, streaming = false } = props;
|
|
21
|
+
const { t } = useCortex();
|
|
22
|
+
|
|
23
|
+
function renderPart() {
|
|
24
|
+
switch (part.type) {
|
|
25
|
+
case 'text':
|
|
26
|
+
return <MessageTextPart textPart={part} role={message.role} streaming={streaming} />;
|
|
27
|
+
case 'thinking':
|
|
28
|
+
return debugMode ? (
|
|
29
|
+
<MessageReasoningPart reasoningPart={part} streaming={streaming} />
|
|
30
|
+
) : (
|
|
31
|
+
<MessageReasoningAnimated />
|
|
32
|
+
);
|
|
33
|
+
case 'tool-call':
|
|
34
|
+
return debugMode ? (
|
|
35
|
+
<MessageToolCallPart toolCallPart={part} />
|
|
36
|
+
) : (
|
|
37
|
+
<ToolAnimation toolCallPart={part} message={message} />
|
|
38
|
+
);
|
|
39
|
+
default:
|
|
40
|
+
// A part kind this widget does not know how to draw.
|
|
41
|
+
return (
|
|
42
|
+
<p className="cortex-unhandled-type">
|
|
43
|
+
{t('translate_unhandled_type')} {part.type}
|
|
44
|
+
</p>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className={cx('cortex-message-part', animate && 'cortex-message-part--animated')}>
|
|
51
|
+
{renderPart()}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { activityLabelKeys } from '@cortex/client';
|
|
2
|
+
import { SubtleActivity } from './SubtleActivity';
|
|
3
|
+
|
|
4
|
+
const LABELS = activityLabelKeys('reasoning');
|
|
5
|
+
|
|
6
|
+
export function MessageReasoningAnimated() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="cortex-reasoning-animated">
|
|
9
|
+
<SubtleActivity labels={LABELS} />
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ThinkingPart } from '@tanstack/ai';
|
|
2
|
+
import { useCortex } from '../context';
|
|
3
|
+
import { cx } from '../cx';
|
|
4
|
+
|
|
5
|
+
type MessageReasoningPartProps = {
|
|
6
|
+
reasoningPart: ThinkingPart;
|
|
7
|
+
streaming?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function MessageReasoningPart(props: MessageReasoningPartProps) {
|
|
11
|
+
const { reasoningPart, streaming = false } = props;
|
|
12
|
+
const { t } = useCortex();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<details className="cortex-reasoning-details">
|
|
16
|
+
<summary className="cortex-reasoning-details__summary">
|
|
17
|
+
<div className="cortex-reasoning-details__header">
|
|
18
|
+
<span className="cortex-reasoning-details__icon">
|
|
19
|
+
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
20
|
+
<path
|
|
21
|
+
d="M10 2C6.686 2 4 4.686 4 8c0 1.655.672 3.154 1.757 4.243.362.363.576.858.576 1.371V14.5a1 1 0 0 0 1 1h5.334a1 1 0 0 0 1-1v-.886c0-.513.214-1.008.576-1.371A5.978 5.978 0 0 0 16 8c0-3.314-2.686-6-6-6Z"
|
|
22
|
+
stroke="currentColor"
|
|
23
|
+
strokeWidth="1.4"
|
|
24
|
+
strokeLinecap="round"
|
|
25
|
+
strokeLinejoin="round"
|
|
26
|
+
/>
|
|
27
|
+
<path
|
|
28
|
+
d="M7.5 17.5h5M8.5 8a2 2 0 0 1 2-2"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
strokeWidth="1.4"
|
|
31
|
+
strokeLinecap="round"
|
|
32
|
+
strokeLinejoin="round"
|
|
33
|
+
/>
|
|
34
|
+
</svg>
|
|
35
|
+
</span>
|
|
36
|
+
|
|
37
|
+
<div className="cortex-reasoning-details__title-group">
|
|
38
|
+
<div className="cortex-reasoning-details__title-row">
|
|
39
|
+
<div className="cortex-reasoning-details__title">{t('translate_reasoning')}</div>
|
|
40
|
+
<span
|
|
41
|
+
className={cx(
|
|
42
|
+
'cortex-reasoning-details__badge',
|
|
43
|
+
streaming
|
|
44
|
+
? 'cortex-reasoning-details__badge--streaming'
|
|
45
|
+
: 'cortex-reasoning-details__badge--done',
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
{streaming ? 'Streaming' : 'Done'}
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<span className="cortex-reasoning-details__chevron" aria-hidden="true">
|
|
55
|
+
<svg width="14" height="14" viewBox="0 0 20 20" fill="none">
|
|
56
|
+
<path
|
|
57
|
+
d="m5.75 8.25 4.25 4.25 4.25-4.25"
|
|
58
|
+
stroke="currentColor"
|
|
59
|
+
strokeWidth="1.5"
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
strokeLinejoin="round"
|
|
62
|
+
/>
|
|
63
|
+
</svg>
|
|
64
|
+
</span>
|
|
65
|
+
</summary>
|
|
66
|
+
|
|
67
|
+
<div className="cortex-reasoning-details__body">
|
|
68
|
+
<div className="cortex-reasoning-details__content">
|
|
69
|
+
<pre className="cortex-reasoning-details__pre">
|
|
70
|
+
{reasoningPart.content.trim() ? reasoningPart.content : 'No reasoning provided.'}
|
|
71
|
+
</pre>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</details>
|
|
75
|
+
);
|
|
76
|
+
}
|