@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,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
+ }
@@ -0,0 +1,8 @@
1
+ import { activityLabelKeys } from '@cortex/client';
2
+ import { SubtleActivity } from './SubtleActivity';
3
+
4
+ const LABELS = activityLabelKeys('code');
5
+
6
+ export function ToolExecuteCodeAnimated() {
7
+ return <SubtleActivity labels={LABELS} />;
8
+ }
@@ -0,0 +1,8 @@
1
+ import { activityLabelKeys } from '@cortex/client';
2
+ import { SubtleActivity } from './SubtleActivity';
3
+
4
+ const LABELS = activityLabelKeys('graph');
5
+
6
+ export function ToolQueryGraphAnimated() {
7
+ return <SubtleActivity labels={LABELS} />;
8
+ }
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
@@ -0,0 +1,4 @@
1
+ /** The one line of clsx this package needs: joins the truthy class names. */
2
+ export function cx(...parts: (string | false | null | undefined)[]) {
3
+ return parts.filter(Boolean).join(' ');
4
+ }
package/src/format.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Token counts as Angular's DecimalPipe renders them. */
2
+ export function num(value: number) {
3
+ return value.toLocaleString('en-US');
4
+ }