@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,211 @@
1
+ import type { ClipboardEvent, DragEvent, KeyboardEvent } from 'react';
2
+ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
3
+ import { ATTACHMENT_MIME_TYPES } from '@cortex/contracts/wire';
4
+ import { useCortex } from '../context';
5
+ import { cx } from '../cx';
6
+ import { AttachmentQueue } from './AttachmentQueue';
7
+
8
+ const ACCEPTED_TYPES = ATTACHMENT_MIME_TYPES.join(',');
9
+
10
+ export type ChatComposerHandle = {
11
+ focusInput: () => void;
12
+ };
13
+
14
+ // forwardRef rather than React 19's ref-as-prop: the package peers on react >=18,
15
+ // where a plain function component never receives `ref`.
16
+ // fallow-ignore-next-line complexity -- 1:1 transcription of the Angular source; restructuring would risk parity drift
17
+ export const ChatComposer = forwardRef<ChatComposerHandle>(function ChatComposer(_props, ref) {
18
+ const { t, queue, isAgentWorking, send, abort } = useCortex();
19
+ const [text, setText] = useState('');
20
+ const [dragging, setDragging] = useState(false);
21
+ const messageInput = useRef<HTMLTextAreaElement>(null);
22
+ const fileInput = useRef<HTMLInputElement>(null);
23
+
24
+ useImperativeHandle(ref, () => ({
25
+ focusInput: () => messageInput.current?.focus(),
26
+ }));
27
+
28
+ // Auto-focus after a turn, from in here rather than only via the widget's
29
+ // focusInput call: that call races the re-render that re-enables the
30
+ // textarea (focusing a still-disabled input is a no-op). This effect runs
31
+ // after the enabling render has committed.
32
+ const wasWorking = useRef(isAgentWorking);
33
+ useEffect(() => {
34
+ if (wasWorking.current && !isAgentWorking) messageInput.current?.focus();
35
+ wasWorking.current = isAgentWorking;
36
+ }, [isAgentWorking]);
37
+
38
+ const canSend = !queue.busy && (Boolean(text.trim()) || queue.hasReady);
39
+
40
+ function onSend() {
41
+ if (isAgentWorking || !canSend) {
42
+ return;
43
+ }
44
+
45
+ const captionKey = 'translate_attachment_caption';
46
+ const translated = t(captionKey);
47
+ const caption =
48
+ translated !== captionKey ? translated : 'Please take a look at the attached files.';
49
+ const prompt = text.trim() || caption;
50
+ const attachments = queue.consumeReady();
51
+
52
+ setText('');
53
+ void send(
54
+ prompt,
55
+ attachments.map((attachment) => attachment.summary),
56
+ );
57
+ }
58
+
59
+ function onKeydown(event: KeyboardEvent<HTMLTextAreaElement>) {
60
+ if (event.key === 'Enter' && !event.shiftKey) {
61
+ event.preventDefault();
62
+ onSend();
63
+ }
64
+ }
65
+
66
+ function onDragOver(event: DragEvent<HTMLDivElement>) {
67
+ event.preventDefault();
68
+ setDragging(true);
69
+ }
70
+
71
+ function onDragLeave(event: DragEvent<HTMLDivElement>) {
72
+ // Moving onto a child bubbles a dragleave up to the composer; only a pointer that
73
+ // has actually left it should drop the highlight.
74
+ const area = event.currentTarget;
75
+ if (!area.contains(event.relatedTarget as Node | null)) {
76
+ setDragging(false);
77
+ }
78
+ }
79
+
80
+ function onDrop(event: DragEvent<HTMLDivElement>) {
81
+ event.preventDefault();
82
+ setDragging(false);
83
+ queue.accept([...event.dataTransfer.files]);
84
+ }
85
+
86
+ function onPaste(event: ClipboardEvent<HTMLTextAreaElement>) {
87
+ const files = event.clipboardData.files;
88
+ if (!files.length) return;
89
+
90
+ event.preventDefault();
91
+ queue.accept([...files]);
92
+ }
93
+
94
+ return (
95
+ <div
96
+ className="cortex-widget__input-area"
97
+ onDragOver={onDragOver}
98
+ onDragLeave={onDragLeave}
99
+ onDrop={onDrop}
100
+ >
101
+ <AttachmentQueue />
102
+
103
+ <div
104
+ className={cx(
105
+ 'cortex-widget__input-box',
106
+ isAgentWorking && 'cortex-widget__input-box--disabled',
107
+ !isAgentWorking && 'cortex-widget__input-box--enabled',
108
+ dragging && 'cortex-widget__input-box--dragging',
109
+ )}
110
+ >
111
+ <textarea
112
+ ref={messageInput}
113
+ onKeyDown={onKeydown}
114
+ onPaste={onPaste}
115
+ value={text}
116
+ onChange={(event) => setText(event.target.value)}
117
+ placeholder={
118
+ dragging
119
+ ? t('translate_drop_files_here')
120
+ : isAgentWorking
121
+ ? ''
122
+ : t('translate_type_a_message')
123
+ }
124
+ disabled={isAgentWorking}
125
+ rows={1}
126
+ className={cx(
127
+ 'cortex-widget__textarea',
128
+ isAgentWorking && 'cortex-widget__textarea--disabled',
129
+ )}
130
+ />
131
+ {isAgentWorking ? (
132
+ /* Stop button */
133
+ <button onClick={() => void abort()} className="cortex-stop-btn">
134
+ {/* Pulsing ring */}
135
+ <span className="cortex-stop-btn__ring"></span>
136
+ {/* Stop icon (rounded square) */}
137
+ <svg
138
+ width="12"
139
+ height="12"
140
+ viewBox="0 0 12 12"
141
+ fill="none"
142
+ className="cortex-stop-btn__icon"
143
+ >
144
+ <rect x="1" y="1" width="10" height="10" rx="2.5" fill="currentColor" />
145
+ </svg>
146
+ </button>
147
+ ) : (
148
+ <div className="cortex-widget__input-actions">
149
+ {/* Attach button */}
150
+ <button
151
+ type="button"
152
+ onClick={() => fileInput.current?.click()}
153
+ className="cortex-widget__attach-btn"
154
+ aria-label={t('translate_attach_files')}
155
+ >
156
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
157
+ <path
158
+ d="M10.5 5.5 6.2 9.8a1.4 1.4 0 0 0 2 2l4.6-4.6a2.8 2.8 0 0 0-4-4L4.2 7.8a4.2 4.2 0 0 0 6 6l4-4"
159
+ stroke="currentColor"
160
+ strokeWidth="1.3"
161
+ strokeLinecap="round"
162
+ strokeLinejoin="round"
163
+ />
164
+ </svg>
165
+ </button>
166
+ <input
167
+ ref={fileInput}
168
+ type="file"
169
+ multiple
170
+ accept={ACCEPTED_TYPES}
171
+ onChange={(event) => {
172
+ queue.accept([...(event.target.files ?? [])]);
173
+
174
+ // Cleared so picking the same file again still fires `change`.
175
+ event.target.value = '';
176
+ }}
177
+ className="cortex-widget__file-input"
178
+ />
179
+
180
+ {/* Send button */}
181
+ <button
182
+ onClick={onSend}
183
+ className={cx(
184
+ 'cortex-widget__send-btn',
185
+ !canSend && 'cortex-widget__send-btn--empty',
186
+ canSend && 'cortex-widget__send-btn--ready',
187
+ )}
188
+ disabled={!canSend}
189
+ >
190
+ <svg
191
+ width="14"
192
+ height="14"
193
+ viewBox="0 0 16 16"
194
+ fill="none"
195
+ className="cortex-widget__send-icon"
196
+ >
197
+ <path
198
+ d="M3 8h10M9 4l4 4-4 4"
199
+ stroke="currentColor"
200
+ strokeWidth="1.5"
201
+ strokeLinecap="round"
202
+ strokeLinejoin="round"
203
+ />
204
+ </svg>
205
+ </button>
206
+ </div>
207
+ )}
208
+ </div>
209
+ </div>
210
+ );
211
+ });
@@ -0,0 +1,73 @@
1
+ import { useRef, useState } from 'react';
2
+ import type { MouseEvent } from 'react';
3
+ import { cx } from '../cx';
4
+
5
+ type CopyButtonProps = {
6
+ value: string;
7
+ className?: string;
8
+ };
9
+
10
+ export function CopyButton({ value, className }: CopyButtonProps) {
11
+ const [copied, setCopied] = useState(false);
12
+ const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
13
+
14
+ async function copy(event: MouseEvent<HTMLButtonElement>) {
15
+ event.stopPropagation();
16
+ event.preventDefault();
17
+
18
+ try {
19
+ await navigator.clipboard.writeText(value);
20
+ setCopied(true);
21
+
22
+ if (resetTimer.current) clearTimeout(resetTimer.current);
23
+ resetTimer.current = setTimeout(() => setCopied(false), 1500);
24
+ } catch {
25
+ // Clipboard API not available — silent fail
26
+ }
27
+ }
28
+
29
+ return (
30
+ <span className={cx('cortex-copy-btn', className)}>
31
+ <button
32
+ className={cx('cortex-copy-btn__button', copied && 'cortex-copy-btn__button--copied')}
33
+ onClick={(event) => void copy(event)}
34
+ aria-label={copied ? 'Copied' : 'Copy to clipboard'}
35
+ type="button"
36
+ >
37
+ {copied ? (
38
+ <svg
39
+ className="cortex-copy-btn__icon cortex-copy-btn__icon--check"
40
+ width="13"
41
+ height="13"
42
+ viewBox="0 0 24 24"
43
+ fill="none"
44
+ >
45
+ <path
46
+ d="M5 13l4 4L19 7"
47
+ stroke="currentColor"
48
+ strokeWidth="2.5"
49
+ strokeLinecap="round"
50
+ strokeLinejoin="round"
51
+ />
52
+ </svg>
53
+ ) : (
54
+ <svg
55
+ className="cortex-copy-btn__icon"
56
+ width="13"
57
+ height="13"
58
+ viewBox="0 0 24 24"
59
+ fill="none"
60
+ >
61
+ <rect x="9" y="9" width="12" height="12" rx="2" stroke="currentColor" strokeWidth="2" />
62
+ <path
63
+ d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
64
+ stroke="currentColor"
65
+ strokeWidth="2"
66
+ strokeLinecap="round"
67
+ />
68
+ </svg>
69
+ )}
70
+ </button>
71
+ </span>
72
+ );
73
+ }