@adecore/agents-react 0.24.0 → 0.24.1
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/chat/ui/Composer.js
CHANGED
|
@@ -39,6 +39,7 @@ import { useNotice } from './composer/use-notice.js';
|
|
|
39
39
|
import { usePendingRequests } from './composer/use-pending-requests.js';
|
|
40
40
|
import { usePromptHistory } from './composer/use-prompt-history.js';
|
|
41
41
|
import { usePromptHold } from './composer/use-prompt-hold.js';
|
|
42
|
+
import { surfacePartOf } from './composer/surface.js';
|
|
42
43
|
import { useTakeBack } from './composer/use-take-back.js';
|
|
43
44
|
function sendLabelKey(busy, steerable, delivery) {
|
|
44
45
|
if (!busy) {
|
|
@@ -121,29 +122,40 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
121
122
|
setText: (next) => box.setText(next),
|
|
122
123
|
submit
|
|
123
124
|
});
|
|
125
|
+
const pointSurface = (event) => {
|
|
126
|
+
event.currentTarget.toggleAttribute('data-text-cursor', surfacePartOf(event.target, event.currentTarget) !== 'action');
|
|
127
|
+
};
|
|
128
|
+
const pressSurface = (event) => {
|
|
129
|
+
if (!writable || event.button !== 0 || event.defaultPrevented || surfacePartOf(event.target, event.currentTarget) !== 'surface') {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Nothing around the field selects, so the default would only blur it on the way, closing its picker and lifting a held prompt card.
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
inputRef.current?.setCaret(Infinity);
|
|
135
|
+
};
|
|
124
136
|
return (_jsxs("div", { className: "chat-composer-content pointer-events-none relative z-10 w-full", children: [!atEnd && !readOnly && (_jsx("div", { className: "absolute inset-x-0 bottom-full mb-2 flex justify-center", children: _jsx(Tooltip, { label: t('composer.jumpToEnd'), name: true, children: _jsx(Surface, { render: _jsx("button", { onClick: () => scrollTimelineToEnd(chatId) }), className: "pointer-events-auto grid h-8 w-8 place-items-center rounded-full text-text-muted hover:text-text", children: _jsx(Icon, { icon: ChevronDown, size: 16 }) }) }) })), _jsx(ChatActivity, { chatId: chatId }), _jsx("div", { className: clsx('@container/composer pointer-events-auto flex flex-col overflow-hidden rounded-2xl border shadow-float backdrop-blur-[14px] focus-ring-within', files.dragging
|
|
125
137
|
? 'border-accent bg-[color-mix(in_srgb,var(--accent-soft)_60%,var(--surface-raised))]'
|
|
126
|
-
: 'border-border bg-[color-mix(in_srgb,var(--surface-raised)_92%,transparent)]'), ...files.dropTarget, children:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
138
|
+
: 'border-border bg-[color-mix(in_srgb,var(--surface-raised)_92%,transparent)]'), ...files.dropTarget, children: _jsx(PromptComposer, { chatId: chatId, pending: pending, focused: focused, onShowElsewhere: answerPromptsElsewhere, disabled: disabled, hasDraft: !empty, denyReason: capabilities?.denyReason === true, onAllAnswered: () => inputRef.current?.focus(), hold: hold, children: _jsxs("div", { className: clsx(writable && 'data-[text-cursor]:cursor-text'), onPointerOver: pointSurface, onMouseDown: pressSurface, children: [_jsx(LimitDock, { chatId: chatId, info: info }), _jsx(GoalDock, { chatId: chatId, info: info, goals: capabilities?.goals }), compactionOffer && (_jsx(ResumeCompactionDock, { tokens: compactionOffer.tokens, onCompact: controls.compact, onDismiss: () => dismissResumeCompaction(scope, chatId, compactionOffer.turnId) })), _jsx(ComposerMenu, { menus: menus }), queue.length > 0 && (_jsx(QueuedMessages, { chatId: chatId, queue: queue, paused: info.queuePaused === true, takingBack: takingBack !== null, onEdit: (message) => void takeBack(message) })), draft.quote !== '' && (_jsx(DraftQuote, { chatId: chatId, quote: draft.quote, source: draft.quoteSource, onRemove: box.removeQuote, onSourceGone: () => notice.setNotice(t('rows.user.quoteGone')) })), draft.chats.length > 0 && _jsx(DraftChats, { chats: draft.chats, onRemove: box.removeChat }), draft.attachments.length > 0 && _jsx(DraftAttachments, { attachments: draft.attachments, onRemove: box.removeAttachment }), _jsx(ComposerInput, { ref: inputRef, dictationToolbar: dictationToolbar, className: "composer-input select-text text-sm leading-normal text-text", value: draft.text, placeholder: placeholder, disabled: !writable, tabbable: focused, extensions: editorExtensions, aria: menus.inputAria, onChange: (value, selection, state) => {
|
|
139
|
+
hold.noteInput();
|
|
140
|
+
box.setText(value);
|
|
141
|
+
menus.setMenuIndex(0);
|
|
142
|
+
menus.trackTriggers(value, selection, state);
|
|
143
|
+
history.edited(value);
|
|
144
|
+
}, onSelectionChange: menus.trackTriggers, onBlur: () => {
|
|
145
|
+
menus.close();
|
|
146
|
+
hold.release('blur');
|
|
147
|
+
}, onComposing: hold.setComposing, onKeyDown: onKeyDown, onPaste: files.onPaste }), notice.notice && (_jsx("div", { className: "px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5", role: "alert", children: notice.notice })), guard.visible && (_jsxs("div", { className: clsx('px-5 pb-1 text-right text-xs tabular-nums @max-md/composer:px-3.5', guard.tooLong ? 'text-status-error' : 'text-text-faint'), children: [formatNumber(guard.count), " / ", formatNumber(PROMPT_MAX_CHARS), guard.tooLong && ` ${t('composer.tooLong')}`] })), _jsx(ComposerControls, { chatId: chatId, info: info, providerFixed: providerFixed, writable: writable, attachable: capabilities?.attachments !== false, controls: controls, send: {
|
|
148
|
+
visible: !busy || !empty,
|
|
149
|
+
disabled: empty || !writable || guard.tooLong,
|
|
150
|
+
label: t(sendLabelKey(busy, steerable, sendDelivery)),
|
|
151
|
+
kbd: sendShortcut(sendKey, draft.text.includes('\n')),
|
|
152
|
+
onSend: (alternate) => submit(alternate ? 'alternate' : 'default')
|
|
153
|
+
}, dictationToolbar: setDictationToolbar, onFiles: (picked) => {
|
|
154
|
+
files.addFiles(picked);
|
|
155
|
+
inputRef.current?.focus();
|
|
156
|
+
}, onRestore: restoreStashed, insert: (added) => {
|
|
157
|
+
if (writable) {
|
|
158
|
+
inputRef.current?.insert(added);
|
|
159
|
+
}
|
|
160
|
+
} })] }) }) }), _jsx(PromptDialog, { open: controls.confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: controls.forceClearThread, onOpenChange: controls.cancelClear })] }));
|
|
149
161
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function surfacePartOf(target: EventTarget | null, surface: Element): 'input' | 'action' | 'surface';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* Anything with an action of its own; `data-composer-action` marks one a role cannot name, such as a control of the host's. */
|
|
2
|
+
const ACTIONS = [
|
|
3
|
+
'a',
|
|
4
|
+
'button',
|
|
5
|
+
'input',
|
|
6
|
+
'select',
|
|
7
|
+
'textarea',
|
|
8
|
+
'label',
|
|
9
|
+
'summary',
|
|
10
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
11
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
12
|
+
'[role="button"]',
|
|
13
|
+
'[role="link"]',
|
|
14
|
+
'[role="menuitem"]',
|
|
15
|
+
'[role="menuitemcheckbox"]',
|
|
16
|
+
'[role="menuitemradio"]',
|
|
17
|
+
'[role="option"]',
|
|
18
|
+
'[role="listbox"]',
|
|
19
|
+
'[role="checkbox"]',
|
|
20
|
+
'[role="radio"]',
|
|
21
|
+
'[role="switch"]',
|
|
22
|
+
'[role="tab"]',
|
|
23
|
+
'[role="slider"]',
|
|
24
|
+
'[role="combobox"]',
|
|
25
|
+
'[data-composer-action]'
|
|
26
|
+
].join(', ');
|
|
27
|
+
/*
|
|
28
|
+
* Where a pointer is on the composer: in the text field, on an action, or on the space around them,
|
|
29
|
+
* which stands in for the text field. A target outside the surface is an action, as an event from a
|
|
30
|
+
* popup portaled out of it still reaches the surface through React.
|
|
31
|
+
*/
|
|
32
|
+
export function surfacePartOf(target, surface) {
|
|
33
|
+
// Not `instanceof Element`, which fails for an element of another window.
|
|
34
|
+
const element = target;
|
|
35
|
+
if (typeof element?.closest !== 'function' || !surface.contains(element)) {
|
|
36
|
+
return 'action';
|
|
37
|
+
}
|
|
38
|
+
const input = element.closest('.cm-editor');
|
|
39
|
+
if (input !== null && surface.contains(input)) {
|
|
40
|
+
return 'input';
|
|
41
|
+
}
|
|
42
|
+
const action = element.closest(ACTIONS);
|
|
43
|
+
return action !== null && surface.contains(action) ? 'action' : 'surface';
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents-react",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -67,11 +67,11 @@
|
|
|
67
67
|
"test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@adecore/agent-contracts": "0.24.
|
|
71
|
-
"@adecore/editor": "0.24.
|
|
72
|
-
"@adecore/editor-react": "0.24.
|
|
73
|
-
"@adecore/intelligent-ui": "0.24.
|
|
74
|
-
"@adecore/merge": "0.24.
|
|
70
|
+
"@adecore/agent-contracts": "0.24.1",
|
|
71
|
+
"@adecore/editor": "0.24.1",
|
|
72
|
+
"@adecore/editor-react": "0.24.1",
|
|
73
|
+
"@adecore/intelligent-ui": "0.24.1",
|
|
74
|
+
"@adecore/merge": "0.24.1",
|
|
75
75
|
"@codemirror/commands": "^6.11.1",
|
|
76
76
|
"@codemirror/language": "^6.12.4",
|
|
77
77
|
"@codemirror/state": "^6.7.6",
|
|
@@ -93,14 +93,14 @@
|
|
|
93
93
|
"zustand": "^5.0.15"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
|
-
"@adecore/ui": "^0.24.
|
|
96
|
+
"@adecore/ui": "^0.24.1",
|
|
97
97
|
"i18next": "^26.4.2",
|
|
98
98
|
"react": "^19.3.0",
|
|
99
99
|
"react-dom": "^19.3.0",
|
|
100
100
|
"react-i18next": "^17.0.15"
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@adecore/ui": "0.24.
|
|
103
|
+
"@adecore/ui": "0.24.1",
|
|
104
104
|
"@types/bun": "^1.4.2",
|
|
105
105
|
"@types/react": "^19.3.0",
|
|
106
106
|
"@types/react-dom": "^19.3.0",
|
package/src/chat/ui/Composer.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore, type MouseEvent, type PointerEvent } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { ChevronDown } from 'lucide-react';
|
|
@@ -40,6 +40,7 @@ import { useNotice } from './composer/use-notice';
|
|
|
40
40
|
import { usePendingRequests } from './composer/use-pending-requests';
|
|
41
41
|
import { usePromptHistory } from './composer/use-prompt-history';
|
|
42
42
|
import { usePromptHold } from './composer/use-prompt-hold';
|
|
43
|
+
import { surfacePartOf } from './composer/surface';
|
|
43
44
|
import { useTakeBack } from './composer/use-take-back';
|
|
44
45
|
|
|
45
46
|
interface ComposerProps {
|
|
@@ -154,6 +155,19 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
154
155
|
submit
|
|
155
156
|
});
|
|
156
157
|
|
|
158
|
+
const pointSurface = (event: PointerEvent<HTMLDivElement>) => {
|
|
159
|
+
event.currentTarget.toggleAttribute('data-text-cursor', surfacePartOf(event.target, event.currentTarget) !== 'action');
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const pressSurface = (event: MouseEvent<HTMLDivElement>) => {
|
|
163
|
+
if (!writable || event.button !== 0 || event.defaultPrevented || surfacePartOf(event.target, event.currentTarget) !== 'surface') {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Nothing around the field selects, so the default would only blur it on the way, closing its picker and lifting a held prompt card.
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
inputRef.current?.setCaret(Infinity);
|
|
169
|
+
};
|
|
170
|
+
|
|
157
171
|
return (
|
|
158
172
|
<div className="chat-composer-content pointer-events-none relative z-10 w-full">
|
|
159
173
|
{/* Over the composer rather than in the thread, since the composer's height is the one it always clears.
|
|
@@ -191,104 +205,106 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
191
205
|
onAllAnswered={() => inputRef.current?.focus()}
|
|
192
206
|
hold={hold}
|
|
193
207
|
>
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
<div className={clsx(writable && 'data-[text-cursor]:cursor-text')} onPointerOver={pointSurface} onMouseDown={pressSurface}>
|
|
209
|
+
<LimitDock chatId={chatId} info={info} />
|
|
210
|
+
<GoalDock chatId={chatId} info={info} goals={capabilities?.goals} />
|
|
211
|
+
{compactionOffer && (
|
|
212
|
+
<ResumeCompactionDock
|
|
213
|
+
tokens={compactionOffer.tokens}
|
|
214
|
+
onCompact={controls.compact}
|
|
215
|
+
onDismiss={() => dismissResumeCompaction(scope, chatId, compactionOffer.turnId)}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
<ComposerMenu menus={menus} />
|
|
219
|
+
{queue.length > 0 && (
|
|
220
|
+
<QueuedMessages
|
|
221
|
+
chatId={chatId}
|
|
222
|
+
queue={queue}
|
|
223
|
+
paused={info.queuePaused === true}
|
|
224
|
+
takingBack={takingBack !== null}
|
|
225
|
+
onEdit={(message) => void takeBack(message)}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
{draft.quote !== '' && (
|
|
229
|
+
<DraftQuote
|
|
230
|
+
chatId={chatId}
|
|
231
|
+
quote={draft.quote}
|
|
232
|
+
source={draft.quoteSource}
|
|
233
|
+
onRemove={box.removeQuote}
|
|
234
|
+
onSourceGone={() => notice.setNotice(t('rows.user.quoteGone'))}
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
237
|
+
{draft.chats.length > 0 && <DraftChats chats={draft.chats} onRemove={box.removeChat} />}
|
|
238
|
+
{draft.attachments.length > 0 && <DraftAttachments attachments={draft.attachments} onRemove={box.removeAttachment} />}
|
|
239
|
+
<ComposerInput
|
|
240
|
+
ref={inputRef}
|
|
241
|
+
dictationToolbar={dictationToolbar}
|
|
242
|
+
className="composer-input select-text text-sm leading-normal text-text"
|
|
243
|
+
value={draft.text}
|
|
244
|
+
placeholder={placeholder}
|
|
245
|
+
disabled={!writable}
|
|
246
|
+
tabbable={focused}
|
|
247
|
+
extensions={editorExtensions}
|
|
248
|
+
aria={menus.inputAria}
|
|
249
|
+
onChange={(value, selection, state) => {
|
|
250
|
+
hold.noteInput();
|
|
251
|
+
box.setText(value);
|
|
252
|
+
menus.setMenuIndex(0);
|
|
253
|
+
menus.trackTriggers(value, selection, state);
|
|
254
|
+
history.edited(value);
|
|
255
|
+
}}
|
|
256
|
+
onSelectionChange={menus.trackTriggers}
|
|
257
|
+
onBlur={() => {
|
|
258
|
+
menus.close();
|
|
259
|
+
hold.release('blur');
|
|
260
|
+
}}
|
|
261
|
+
onComposing={hold.setComposing}
|
|
262
|
+
onKeyDown={onKeyDown}
|
|
263
|
+
onPaste={files.onPaste}
|
|
201
264
|
/>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
265
|
+
{notice.notice && (
|
|
266
|
+
<div className="px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5" role="alert">
|
|
267
|
+
{notice.notice}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
{guard.visible && (
|
|
271
|
+
<div
|
|
272
|
+
className={clsx(
|
|
273
|
+
'px-5 pb-1 text-right text-xs tabular-nums @max-md/composer:px-3.5',
|
|
274
|
+
guard.tooLong ? 'text-status-error' : 'text-text-faint'
|
|
275
|
+
)}
|
|
276
|
+
>
|
|
277
|
+
{formatNumber(guard.count)} / {formatNumber(PROMPT_MAX_CHARS)}
|
|
278
|
+
{guard.tooLong && ` ${t('composer.tooLong')}`}
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
<ComposerControls
|
|
206
282
|
chatId={chatId}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
283
|
+
info={info}
|
|
284
|
+
providerFixed={providerFixed}
|
|
285
|
+
writable={writable}
|
|
286
|
+
attachable={capabilities?.attachments !== false}
|
|
287
|
+
controls={controls}
|
|
288
|
+
send={{
|
|
289
|
+
visible: !busy || !empty,
|
|
290
|
+
disabled: empty || !writable || guard.tooLong,
|
|
291
|
+
label: t(sendLabelKey(busy, steerable, sendDelivery)),
|
|
292
|
+
kbd: sendShortcut(sendKey, draft.text.includes('\n')),
|
|
293
|
+
onSend: (alternate) => submit(alternate ? 'alternate' : 'default')
|
|
294
|
+
}}
|
|
295
|
+
dictationToolbar={setDictationToolbar}
|
|
296
|
+
onFiles={(picked) => {
|
|
297
|
+
files.addFiles(picked);
|
|
298
|
+
inputRef.current?.focus();
|
|
299
|
+
}}
|
|
300
|
+
onRestore={restoreStashed}
|
|
301
|
+
insert={(added) => {
|
|
302
|
+
if (writable) {
|
|
303
|
+
inputRef.current?.insert(added);
|
|
304
|
+
}
|
|
305
|
+
}}
|
|
211
306
|
/>
|
|
212
|
-
|
|
213
|
-
{draft.quote !== '' && (
|
|
214
|
-
<DraftQuote
|
|
215
|
-
chatId={chatId}
|
|
216
|
-
quote={draft.quote}
|
|
217
|
-
source={draft.quoteSource}
|
|
218
|
-
onRemove={box.removeQuote}
|
|
219
|
-
onSourceGone={() => notice.setNotice(t('rows.user.quoteGone'))}
|
|
220
|
-
/>
|
|
221
|
-
)}
|
|
222
|
-
{draft.chats.length > 0 && <DraftChats chats={draft.chats} onRemove={box.removeChat} />}
|
|
223
|
-
{draft.attachments.length > 0 && <DraftAttachments attachments={draft.attachments} onRemove={box.removeAttachment} />}
|
|
224
|
-
<ComposerInput
|
|
225
|
-
ref={inputRef}
|
|
226
|
-
dictationToolbar={dictationToolbar}
|
|
227
|
-
className="composer-input select-text text-sm leading-normal text-text"
|
|
228
|
-
value={draft.text}
|
|
229
|
-
placeholder={placeholder}
|
|
230
|
-
disabled={!writable}
|
|
231
|
-
tabbable={focused}
|
|
232
|
-
extensions={editorExtensions}
|
|
233
|
-
aria={menus.inputAria}
|
|
234
|
-
onChange={(value, selection, state) => {
|
|
235
|
-
hold.noteInput();
|
|
236
|
-
box.setText(value);
|
|
237
|
-
menus.setMenuIndex(0);
|
|
238
|
-
menus.trackTriggers(value, selection, state);
|
|
239
|
-
history.edited(value);
|
|
240
|
-
}}
|
|
241
|
-
onSelectionChange={menus.trackTriggers}
|
|
242
|
-
onBlur={() => {
|
|
243
|
-
menus.close();
|
|
244
|
-
hold.release('blur');
|
|
245
|
-
}}
|
|
246
|
-
onComposing={hold.setComposing}
|
|
247
|
-
onKeyDown={onKeyDown}
|
|
248
|
-
onPaste={files.onPaste}
|
|
249
|
-
/>
|
|
250
|
-
{notice.notice && (
|
|
251
|
-
<div className="px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5" role="alert">
|
|
252
|
-
{notice.notice}
|
|
253
|
-
</div>
|
|
254
|
-
)}
|
|
255
|
-
{guard.visible && (
|
|
256
|
-
<div
|
|
257
|
-
className={clsx(
|
|
258
|
-
'px-5 pb-1 text-right text-xs tabular-nums @max-md/composer:px-3.5',
|
|
259
|
-
guard.tooLong ? 'text-status-error' : 'text-text-faint'
|
|
260
|
-
)}
|
|
261
|
-
>
|
|
262
|
-
{formatNumber(guard.count)} / {formatNumber(PROMPT_MAX_CHARS)}
|
|
263
|
-
{guard.tooLong && ` ${t('composer.tooLong')}`}
|
|
264
|
-
</div>
|
|
265
|
-
)}
|
|
266
|
-
<ComposerControls
|
|
267
|
-
chatId={chatId}
|
|
268
|
-
info={info}
|
|
269
|
-
providerFixed={providerFixed}
|
|
270
|
-
writable={writable}
|
|
271
|
-
attachable={capabilities?.attachments !== false}
|
|
272
|
-
controls={controls}
|
|
273
|
-
send={{
|
|
274
|
-
visible: !busy || !empty,
|
|
275
|
-
disabled: empty || !writable || guard.tooLong,
|
|
276
|
-
label: t(sendLabelKey(busy, steerable, sendDelivery)),
|
|
277
|
-
kbd: sendShortcut(sendKey, draft.text.includes('\n')),
|
|
278
|
-
onSend: (alternate) => submit(alternate ? 'alternate' : 'default')
|
|
279
|
-
}}
|
|
280
|
-
dictationToolbar={setDictationToolbar}
|
|
281
|
-
onFiles={(picked) => {
|
|
282
|
-
files.addFiles(picked);
|
|
283
|
-
inputRef.current?.focus();
|
|
284
|
-
}}
|
|
285
|
-
onRestore={restoreStashed}
|
|
286
|
-
insert={(added) => {
|
|
287
|
-
if (writable) {
|
|
288
|
-
inputRef.current?.insert(added);
|
|
289
|
-
}
|
|
290
|
-
}}
|
|
291
|
-
/>
|
|
307
|
+
</div>
|
|
292
308
|
</PromptComposer>
|
|
293
309
|
</div>
|
|
294
310
|
<PromptDialog
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* Anything with an action of its own; `data-composer-action` marks one a role cannot name, such as a control of the host's. */
|
|
2
|
+
const ACTIONS = [
|
|
3
|
+
'a',
|
|
4
|
+
'button',
|
|
5
|
+
'input',
|
|
6
|
+
'select',
|
|
7
|
+
'textarea',
|
|
8
|
+
'label',
|
|
9
|
+
'summary',
|
|
10
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
11
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
12
|
+
'[role="button"]',
|
|
13
|
+
'[role="link"]',
|
|
14
|
+
'[role="menuitem"]',
|
|
15
|
+
'[role="menuitemcheckbox"]',
|
|
16
|
+
'[role="menuitemradio"]',
|
|
17
|
+
'[role="option"]',
|
|
18
|
+
'[role="listbox"]',
|
|
19
|
+
'[role="checkbox"]',
|
|
20
|
+
'[role="radio"]',
|
|
21
|
+
'[role="switch"]',
|
|
22
|
+
'[role="tab"]',
|
|
23
|
+
'[role="slider"]',
|
|
24
|
+
'[role="combobox"]',
|
|
25
|
+
'[data-composer-action]'
|
|
26
|
+
].join(', ');
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* Where a pointer is on the composer: in the text field, on an action, or on the space around them,
|
|
30
|
+
* which stands in for the text field. A target outside the surface is an action, as an event from a
|
|
31
|
+
* popup portaled out of it still reaches the surface through React.
|
|
32
|
+
*/
|
|
33
|
+
export function surfacePartOf(target: EventTarget | null, surface: Element): 'input' | 'action' | 'surface' {
|
|
34
|
+
// Not `instanceof Element`, which fails for an element of another window.
|
|
35
|
+
const element = target as Element | null;
|
|
36
|
+
if (typeof element?.closest !== 'function' || !surface.contains(element)) {
|
|
37
|
+
return 'action';
|
|
38
|
+
}
|
|
39
|
+
const input = element.closest('.cm-editor');
|
|
40
|
+
if (input !== null && surface.contains(input)) {
|
|
41
|
+
return 'input';
|
|
42
|
+
}
|
|
43
|
+
const action = element.closest(ACTIONS);
|
|
44
|
+
return action !== null && surface.contains(action) ? 'action' : 'surface';
|
|
45
|
+
}
|