@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.
Files changed (37) hide show
  1. package/dist/index.d.ts +70 -0
  2. package/dist/index.js +3 -0
  3. package/dist/styles.css +2652 -0
  4. package/dist/theme.css +69 -0
  5. package/index.ts +17 -0
  6. package/package.json +67 -0
  7. package/src/chat-session.tsx +379 -0
  8. package/src/components/AttachmentQueue.tsx +75 -0
  9. package/src/components/ChatComposer.tsx +211 -0
  10. package/src/components/CopyButton.tsx +73 -0
  11. package/src/components/CortexChatWidget.tsx +538 -0
  12. package/src/components/JsonTree.tsx +111 -0
  13. package/src/components/LlmUsageBreakdown.tsx +42 -0
  14. package/src/components/LlmUsageChips.tsx +37 -0
  15. package/src/components/Message.tsx +81 -0
  16. package/src/components/MessageAbortedFlag.tsx +29 -0
  17. package/src/components/MessageAttachments.tsx +55 -0
  18. package/src/components/MessageList.tsx +102 -0
  19. package/src/components/MessageLlmInspector.tsx +184 -0
  20. package/src/components/MessagePart.tsx +54 -0
  21. package/src/components/MessageReasoningAnimated.tsx +12 -0
  22. package/src/components/MessageReasoningPart.tsx +76 -0
  23. package/src/components/MessageTextPart.tsx +78 -0
  24. package/src/components/MessageTokenUsage.tsx +145 -0
  25. package/src/components/MessageToolCallAnimated.tsx +86 -0
  26. package/src/components/MessageToolCallOutcome.tsx +95 -0
  27. package/src/components/MessageToolCallPart.tsx +95 -0
  28. package/src/components/MessageToolCallStatus.tsx +35 -0
  29. package/src/components/SubtleActivity.tsx +74 -0
  30. package/src/components/ThreadList.tsx +219 -0
  31. package/src/components/ToolAnimation.tsx +22 -0
  32. package/src/components/ToolExecuteCodeAnimated.tsx +8 -0
  33. package/src/components/ToolQueryGraphAnimated.tsx +8 -0
  34. package/src/config.ts +14 -0
  35. package/src/context.ts +48 -0
  36. package/src/cx.ts +4 -0
  37. 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
+ }