@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,219 @@
|
|
|
1
|
+
import { relativeTimeLabel } from '@cortex/client';
|
|
2
|
+
import type { ThreadSummary } from '@cortex/contracts/wire';
|
|
3
|
+
import { useCortex } from '../context';
|
|
4
|
+
import { cx } from '../cx';
|
|
5
|
+
|
|
6
|
+
/** Rounded speech bubble with a tail, on the shared 16x16 icon grid. */
|
|
7
|
+
const BUBBLE_PATH =
|
|
8
|
+
'M13.5 7.6c0 2.4-2.5 4.4-5.5 4.4-.6 0-1.2-.08-1.7-.23L3 13l.8-2.3C3 9.8 2.5 8.7 2.5 7.6 2.5 5.2 5 3.2 8 3.2s5.5 2 5.5 4.4Z';
|
|
9
|
+
|
|
10
|
+
type ThreadListProps = {
|
|
11
|
+
className?: string;
|
|
12
|
+
onThreadSelected: (thread: ThreadSummary) => void;
|
|
13
|
+
onNewChatRequested: () => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The threads screen: header, list, loading skeleton and empty state.
|
|
18
|
+
*
|
|
19
|
+
* Carries the screen classes passed by the widget on its own root rather than
|
|
20
|
+
* adding a wrapper, so the sliding-panel layout sees the same DOM shape it did
|
|
21
|
+
* when this markup lived inline in the widget.
|
|
22
|
+
*/
|
|
23
|
+
export function ThreadList(props: ThreadListProps) {
|
|
24
|
+
const { config, t, threads, selectedThread, deleteThread } = useCortex();
|
|
25
|
+
const locale = config.locale ?? 'en';
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={props.className}>
|
|
29
|
+
{/* Threads header */}
|
|
30
|
+
<div className="cortex-widget__threads-header">
|
|
31
|
+
<div>
|
|
32
|
+
<h2 className="cortex-widget__threads-title">{t('translate_threads')}</h2>
|
|
33
|
+
<p className="cortex-widget__threads-count">
|
|
34
|
+
{t(threads?.length === 1 ? 'translate_one_conversation' : 'translate_n_conversations', {
|
|
35
|
+
count: threads?.length ?? 0,
|
|
36
|
+
})}
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
<button onClick={() => props.onNewChatRequested()} className="cortex-widget__new-chat-btn">
|
|
40
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
|
41
|
+
<path
|
|
42
|
+
d="M8 3v10M3 8h10"
|
|
43
|
+
stroke="currentColor"
|
|
44
|
+
strokeWidth="1.5"
|
|
45
|
+
strokeLinecap="round"
|
|
46
|
+
/>
|
|
47
|
+
</svg>
|
|
48
|
+
{t('translate_new')}
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Threads list */}
|
|
53
|
+
<div className="cortex-widget__threads-list">
|
|
54
|
+
{threads === undefined ? (
|
|
55
|
+
// Threads loading skeleton
|
|
56
|
+
[1, 2, 3, 4].map((i) => (
|
|
57
|
+
<div key={i} className="cortex-widget__thread-skeleton">
|
|
58
|
+
<div className="cortex-skeleton cortex-widget__thread-skeleton-icon"></div>
|
|
59
|
+
<div className="cortex-widget__thread-skeleton-lines">
|
|
60
|
+
<div
|
|
61
|
+
className="cortex-skeleton cortex-widget__thread-skeleton-line"
|
|
62
|
+
style={{ width: `${40 + i * 12}%` }}
|
|
63
|
+
></div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
))
|
|
67
|
+
) : (
|
|
68
|
+
<>
|
|
69
|
+
{threads.map((thread) => {
|
|
70
|
+
const isActive = thread.id === selectedThread?.id;
|
|
71
|
+
// Rendered once per paint: the label goes stale while the panel
|
|
72
|
+
// sits open, which is cheaper than a timer per row.
|
|
73
|
+
const time = relativeTimeLabel(thread.updatedAt, locale);
|
|
74
|
+
return (
|
|
75
|
+
<button
|
|
76
|
+
key={thread.id}
|
|
77
|
+
onClick={() => props.onThreadSelected(thread)}
|
|
78
|
+
className={cx(
|
|
79
|
+
'cortex-widget__thread-item',
|
|
80
|
+
isActive && 'cortex-widget__thread-item--active',
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
{/* Thread icon */}
|
|
84
|
+
<div
|
|
85
|
+
className={cx(
|
|
86
|
+
'cortex-widget__thread-icon',
|
|
87
|
+
isActive && 'cortex-widget__thread-icon--active',
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
|
91
|
+
<path
|
|
92
|
+
d={BUBBLE_PATH}
|
|
93
|
+
stroke="currentColor"
|
|
94
|
+
strokeWidth="1.3"
|
|
95
|
+
strokeLinecap="round"
|
|
96
|
+
strokeLinejoin="round"
|
|
97
|
+
/>
|
|
98
|
+
</svg>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Thread info */}
|
|
102
|
+
<div className="cortex-widget__thread-info">
|
|
103
|
+
<div className="cortex-widget__thread-title-row">
|
|
104
|
+
<p
|
|
105
|
+
className={cx(
|
|
106
|
+
'cortex-widget__thread-title',
|
|
107
|
+
isActive && 'cortex-widget__thread-title--active',
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
{thread.title ?? t('translate_untitled')}
|
|
111
|
+
</p>
|
|
112
|
+
|
|
113
|
+
{thread.isRunning && (
|
|
114
|
+
<span
|
|
115
|
+
className={cx(
|
|
116
|
+
'cortex-widget__thread-running',
|
|
117
|
+
isActive && 'cortex-widget__thread-running--active',
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
<span className="cortex-widget__thread-running-dot"></span>
|
|
121
|
+
{t('translate_running')}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{time && <p className="cortex-widget__thread-time">{time}</p>}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{/* Delete button */}
|
|
130
|
+
<span
|
|
131
|
+
role="button"
|
|
132
|
+
tabIndex={0}
|
|
133
|
+
onClick={(event) => {
|
|
134
|
+
event.stopPropagation();
|
|
135
|
+
void deleteThread(thread.id);
|
|
136
|
+
}}
|
|
137
|
+
onKeyDown={(event) => {
|
|
138
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
event.stopPropagation();
|
|
141
|
+
void deleteThread(thread.id);
|
|
142
|
+
}}
|
|
143
|
+
className={cx(
|
|
144
|
+
'cortex-widget__thread-delete',
|
|
145
|
+
isActive && 'cortex-widget__thread-delete--active',
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
148
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
|
149
|
+
<path
|
|
150
|
+
d="M4 4l8 8M12 4l-8 8"
|
|
151
|
+
stroke="currentColor"
|
|
152
|
+
strokeWidth="1.3"
|
|
153
|
+
strokeLinecap="round"
|
|
154
|
+
/>
|
|
155
|
+
</svg>
|
|
156
|
+
</span>
|
|
157
|
+
|
|
158
|
+
{/* Arrow */}
|
|
159
|
+
<svg
|
|
160
|
+
className={cx(
|
|
161
|
+
'cortex-widget__thread-arrow',
|
|
162
|
+
isActive && 'cortex-widget__thread-arrow--active',
|
|
163
|
+
)}
|
|
164
|
+
width="10"
|
|
165
|
+
height="10"
|
|
166
|
+
viewBox="0 0 16 16"
|
|
167
|
+
fill="none"
|
|
168
|
+
>
|
|
169
|
+
<path
|
|
170
|
+
d="M6 4l4 4-4 4"
|
|
171
|
+
stroke="currentColor"
|
|
172
|
+
strokeWidth="1.5"
|
|
173
|
+
strokeLinecap="round"
|
|
174
|
+
strokeLinejoin="round"
|
|
175
|
+
/>
|
|
176
|
+
</svg>
|
|
177
|
+
</button>
|
|
178
|
+
);
|
|
179
|
+
})}
|
|
180
|
+
|
|
181
|
+
{!threads.length && (
|
|
182
|
+
<div className="cortex-widget__threads-empty">
|
|
183
|
+
<div className="cortex-widget__threads-empty-icon">
|
|
184
|
+
<svg
|
|
185
|
+
width="18"
|
|
186
|
+
height="18"
|
|
187
|
+
viewBox="0 0 16 16"
|
|
188
|
+
fill="none"
|
|
189
|
+
className="cortex-widget__threads-empty-svg"
|
|
190
|
+
>
|
|
191
|
+
<path
|
|
192
|
+
d={BUBBLE_PATH}
|
|
193
|
+
stroke="currentColor"
|
|
194
|
+
strokeWidth="1.3"
|
|
195
|
+
strokeLinecap="round"
|
|
196
|
+
strokeLinejoin="round"
|
|
197
|
+
/>
|
|
198
|
+
</svg>
|
|
199
|
+
</div>
|
|
200
|
+
<p className="cortex-widget__threads-empty-title">
|
|
201
|
+
{t('translate_no_threads_yet')}
|
|
202
|
+
</p>
|
|
203
|
+
<p className="cortex-widget__threads-empty-subtitle">
|
|
204
|
+
{t('translate_start_a_new_conversation')}
|
|
205
|
+
</p>
|
|
206
|
+
<button
|
|
207
|
+
onClick={() => props.onNewChatRequested()}
|
|
208
|
+
className="cortex-widget__new-chat-btn cortex-widget__new-chat-btn--empty-state"
|
|
209
|
+
>
|
|
210
|
+
{t('translate_new_chat')}
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ToolCallPart, UIMessage } from '@tanstack/ai';
|
|
2
|
+
import { MessageToolCallAnimated } from './MessageToolCallAnimated';
|
|
3
|
+
import { ToolExecuteCodeAnimated } from './ToolExecuteCodeAnimated';
|
|
4
|
+
import { ToolQueryGraphAnimated } from './ToolQueryGraphAnimated';
|
|
5
|
+
|
|
6
|
+
type ToolAnimationProps = {
|
|
7
|
+
message: UIMessage;
|
|
8
|
+
toolCallPart: ToolCallPart;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Which animation stands in for a tool call while the user is not in debug mode.
|
|
13
|
+
* Two tools have a bespoke animation and the rest share a generic one — a decision
|
|
14
|
+
* that belongs beside those components rather than nested inside the part that
|
|
15
|
+
* merely needs *an* animation.
|
|
16
|
+
*/
|
|
17
|
+
export function ToolAnimation({ message, toolCallPart }: ToolAnimationProps) {
|
|
18
|
+
if (toolCallPart.name === 'queryGraph') return <ToolQueryGraphAnimated />;
|
|
19
|
+
if (toolCallPart.name === 'executeCode') return <ToolExecuteCodeAnimated />;
|
|
20
|
+
|
|
21
|
+
return <MessageToolCallAnimated toolCallPart={toolCallPart} message={message} />;
|
|
22
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { ToolCallPart, UIMessage } from '@tanstack/ai';
|
|
3
|
+
import type { CortexConfig } from '@cortex/client';
|
|
4
|
+
|
|
5
|
+
/** The props a consumer-supplied tool component receives from the widget. */
|
|
6
|
+
export type CortexToolComponentProps = {
|
|
7
|
+
toolCallPart: ToolCallPart;
|
|
8
|
+
message: UIMessage;
|
|
9
|
+
setOutput: (output: unknown) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type CortexReactConfig = CortexConfig & {
|
|
13
|
+
toolComponents?: Record<string, ComponentType<CortexToolComponentProps>>;
|
|
14
|
+
};
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { UIMessage } from '@tanstack/ai';
|
|
3
|
+
import type { AttachmentQueue, CortexApiClient, QueuedAttachment } from '@cortex/client';
|
|
4
|
+
import type { AttachmentSummary, MessageMetadata, ThreadSummary } from '@cortex/contracts/wire';
|
|
5
|
+
import type { CortexReactConfig } from './config';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What the widget's children read instead of Angular's injected services: the
|
|
9
|
+
* flat surface of CortexChatService/CortexThreadService plus the attachment
|
|
10
|
+
* queue and the api client, provided once by CortexChatWidget.
|
|
11
|
+
*/
|
|
12
|
+
export type CortexContextValue = {
|
|
13
|
+
config: CortexReactConfig;
|
|
14
|
+
t: (key: string, params?: Record<string, string | number>) => string;
|
|
15
|
+
api: CortexApiClient;
|
|
16
|
+
debugMode: boolean;
|
|
17
|
+
|
|
18
|
+
threads: ThreadSummary[] | undefined;
|
|
19
|
+
selectedThread: ThreadSummary | undefined;
|
|
20
|
+
deleteThread: (threadId: string) => Promise<void>;
|
|
21
|
+
|
|
22
|
+
messages: UIMessage[];
|
|
23
|
+
messageMetadata: ReadonlyMap<string, MessageMetadata>;
|
|
24
|
+
isAgentWorking: boolean;
|
|
25
|
+
isLoadingMessages: boolean;
|
|
26
|
+
hasPendingToolCalls: boolean;
|
|
27
|
+
send: (prompt: string, attachments?: AttachmentSummary[]) => Promise<void>;
|
|
28
|
+
abort: () => Promise<void>;
|
|
29
|
+
addToolResult: (toolCallId: string, toolName: string, output: unknown) => void;
|
|
30
|
+
|
|
31
|
+
queue: {
|
|
32
|
+
items: QueuedAttachment[];
|
|
33
|
+
uploading: boolean;
|
|
34
|
+
busy: boolean;
|
|
35
|
+
hasReady: boolean;
|
|
36
|
+
accept: (files: File[]) => void;
|
|
37
|
+
remove: (localId: string) => void;
|
|
38
|
+
consumeReady: AttachmentQueue['consumeReady'];
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const CortexContext = createContext<CortexContextValue | undefined>(undefined);
|
|
43
|
+
|
|
44
|
+
export function useCortex() {
|
|
45
|
+
const value = useContext(CortexContext);
|
|
46
|
+
if (!value) throw new Error('useCortex must be used inside <CortexChatWidget>');
|
|
47
|
+
return value;
|
|
48
|
+
}
|
package/src/cx.ts
ADDED
package/src/format.ts
ADDED