@adea-ai/ui 0.68.2 → 0.69.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/README.md +79 -15
- package/dist/NOTICE +55 -24
- package/dist/components/conversation/atomic/chat-composer.d.ts +20 -0
- package/dist/components/conversation/atomic/chat-composer.d.ts.map +1 -0
- package/dist/components/conversation/atomic/chat-composer.js +69 -0
- package/dist/components/conversation/atomic/chat-composer.js.map +1 -0
- package/dist/components/conversation/atomic/index.d.ts +3 -0
- package/dist/components/conversation/atomic/index.d.ts.map +1 -0
- package/dist/components/conversation/atomic/index.js +2 -0
- package/dist/components/conversation/chat-composer.d.ts +12 -21
- package/dist/components/conversation/chat-composer.d.ts.map +1 -1
- package/dist/components/conversation/chat-composer.js +33 -403
- package/dist/components/conversation/chat-composer.js.map +1 -1
- package/dist/components/conversation/internal/chat-composer-shell.d.ts +38 -0
- package/dist/components/conversation/internal/chat-composer-shell.d.ts.map +1 -0
- package/dist/components/conversation/internal/chat-composer-shell.js +423 -0
- package/dist/components/conversation/internal/chat-composer-shell.js.map +1 -0
- package/dist/components/conversation/paste-preview.d.ts +11 -0
- package/dist/components/conversation/paste-preview.d.ts.map +1 -0
- package/dist/components/conversation/paste-preview.js +28 -0
- package/dist/components/conversation/paste-preview.js.map +1 -0
- package/dist/components/conversation/paste-token-editor.d.ts +26 -0
- package/dist/components/conversation/paste-token-editor.d.ts.map +1 -0
- package/dist/components/conversation/paste-token-editor.js +570 -0
- package/dist/components/conversation/paste-token-editor.js.map +1 -0
- package/dist/components/conversation/paste-tokens.d.ts.map +1 -1
- package/dist/components/conversation/paste-tokens.js +11 -2
- package/dist/components/conversation/paste-tokens.js.map +1 -1
- package/dist/r/conversation.json +26 -0
- package/dist/r/registry.json +26 -0
- package/dist/r/src/components/conversation/atomic/chat-composer.tsx +57 -0
- package/dist/r/src/components/conversation/atomic/index.ts +6 -0
- package/dist/r/src/components/conversation/chat-composer.tsx +39 -331
- package/dist/r/src/components/conversation/internal/chat-composer-shell.tsx +390 -0
- package/dist/r/src/components/conversation/paste-preview.ts +35 -0
- package/dist/r/src/components/conversation/paste-token-editor.tsx +672 -0
- package/dist/r/src/components/conversation/paste-tokens.ts +15 -2
- package/package.json +1 -1
- package/registry.json +26 -0
- package/src/components/conversation/atomic/chat-composer.tsx +57 -0
- package/src/components/conversation/atomic/index.ts +6 -0
- package/src/components/conversation/chat-composer.tsx +39 -331
- package/src/components/conversation/internal/chat-composer-shell.tsx +390 -0
- package/src/components/conversation/paste-preview.ts +35 -0
- package/src/components/conversation/paste-token-editor.tsx +672 -0
- package/src/components/conversation/paste-tokens.ts +15 -2
|
@@ -4,18 +4,9 @@
|
|
|
4
4
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
5
5
|
* Licensed under Apache-2.0; see LICENSE and NOTICE.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
DropdownMenu,
|
|
12
|
-
DropdownMenuContent,
|
|
13
|
-
DropdownMenuItem,
|
|
14
|
-
DropdownMenuTrigger,
|
|
15
|
-
} from '../ui/dropdown-menu/dropdown-menu'
|
|
16
|
-
import { Spinner } from '../ui/spinner/spinner'
|
|
17
|
-
import { BusySendButton, type BusySendButtonProps, type BusySendMode } from './busy-send-button'
|
|
18
|
-
import { createComposerImeGuard } from './ime-guard'
|
|
7
|
+
import type { JSX } from 'solid-js'
|
|
8
|
+
import type { BusySendButtonProps, BusySendMode } from './busy-send-button'
|
|
9
|
+
import { ChatComposerShell } from './internal/chat-composer-shell'
|
|
19
10
|
|
|
20
11
|
export type ChatComposerControl = {
|
|
21
12
|
/** Focus this instance; false when it is unavailable or collapsed. */
|
|
@@ -23,11 +14,9 @@ export type ChatComposerControl = {
|
|
|
23
14
|
/** A typing intent, unlike merely navigating to a conversation. */
|
|
24
15
|
expandAndFocus: () => boolean
|
|
25
16
|
}
|
|
26
|
-
|
|
17
|
+
|
|
18
|
+
export type ChatComposerSharedProps = {
|
|
27
19
|
value: string
|
|
28
|
-
onValueChange: (value: string) => void
|
|
29
|
-
/** The host owns attachments and delivery; text/action are captured at the gesture. */
|
|
30
|
-
onSubmit: (input: { text: string; action: 'send' | BusySendMode }) => void | Promise<void>
|
|
31
20
|
/** Host scope fence; changes discard local feedback, not the host draft. */
|
|
32
21
|
resetKey?: unknown
|
|
33
22
|
disabled?: boolean
|
|
@@ -57,322 +46,41 @@ export type ChatComposerProps = {
|
|
|
57
46
|
approvalFocus?: boolean
|
|
58
47
|
}
|
|
59
48
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*/
|
|
49
|
+
export type ChatComposerSubmitInput = {
|
|
50
|
+
text: string
|
|
51
|
+
action: 'send' | BusySendMode
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ChatComposerProps = ChatComposerSharedProps & {
|
|
55
|
+
onValueChange: (value: string) => void
|
|
56
|
+
onSubmit: (input: ChatComposerSubmitInput) => void | Promise<void>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Plain-text entry. Atomic paste editing is available from the separate atomic subpath. */
|
|
72
60
|
export function ChatComposer(props: ChatComposerProps) {
|
|
73
|
-
const ime = createComposerImeGuard()
|
|
74
|
-
const [pending, setPending] = createSignal(false)
|
|
75
|
-
const [error, setError] = createSignal<string | null>(null)
|
|
76
|
-
let field: HTMLTextAreaElement | undefined
|
|
77
|
-
let restoreBar: HTMLButtonElement | undefined
|
|
78
|
-
let focusFrame: number | undefined
|
|
79
|
-
let focusTarget: 'input' | 'bar' | undefined
|
|
80
|
-
let selection:
|
|
81
|
-
| { start: number; end: number; direction: 'forward' | 'backward' | 'none' }
|
|
82
|
-
| undefined
|
|
83
|
-
let generation = 0
|
|
84
|
-
let active = true
|
|
85
|
-
const collapsed = () => props.collapse?.value === true
|
|
86
|
-
const unavailable = () => props.disabled || props.readOnly || !!props.blockedReason
|
|
87
|
-
const cancelFocus = () => {
|
|
88
|
-
if (focusFrame !== undefined) cancelAnimationFrame(focusFrame)
|
|
89
|
-
focusFrame = undefined
|
|
90
|
-
}
|
|
91
|
-
const requestFocus = (target: 'input' | 'bar') => {
|
|
92
|
-
cancelFocus()
|
|
93
|
-
focusTarget = target
|
|
94
|
-
focusFrame = requestAnimationFrame(() => {
|
|
95
|
-
focusFrame = undefined
|
|
96
|
-
if (!active || props.approvalFocus) return
|
|
97
|
-
if (target === 'bar' && collapsed()) restoreBar?.focus()
|
|
98
|
-
if (target === 'input' && !collapsed() && !props.disabled && field) {
|
|
99
|
-
field.focus()
|
|
100
|
-
if (selection)
|
|
101
|
-
field.setSelectionRange(
|
|
102
|
-
Math.min(selection.start, field.value.length),
|
|
103
|
-
Math.min(selection.end, field.value.length),
|
|
104
|
-
selection.direction
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
focusTarget = undefined
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
const changeCollapsed = (value: boolean) => {
|
|
111
|
-
if (!props.collapse || props.approvalFocus) return
|
|
112
|
-
if (value && field)
|
|
113
|
-
selection = {
|
|
114
|
-
start: field.selectionStart,
|
|
115
|
-
end: field.selectionEnd,
|
|
116
|
-
direction: field.selectionDirection,
|
|
117
|
-
}
|
|
118
|
-
props.collapse.onChange(value)
|
|
119
|
-
requestFocus(value ? 'bar' : 'input')
|
|
120
|
-
}
|
|
121
|
-
const control: ChatComposerControl = {
|
|
122
|
-
focus() {
|
|
123
|
-
if (collapsed() || props.approvalFocus || props.disabled || !field) return false
|
|
124
|
-
field.focus()
|
|
125
|
-
return true
|
|
126
|
-
},
|
|
127
|
-
expandAndFocus() {
|
|
128
|
-
if (props.approvalFocus || props.disabled) return false
|
|
129
|
-
if (collapsed()) changeCollapsed(false)
|
|
130
|
-
else requestFocus('input')
|
|
131
|
-
return true
|
|
132
|
-
},
|
|
133
|
-
}
|
|
134
|
-
onMount(() => props.controlRef?.(control))
|
|
135
|
-
createEffect(
|
|
136
|
-
on(
|
|
137
|
-
() => props.resetKey,
|
|
138
|
-
() => {
|
|
139
|
-
generation += 1
|
|
140
|
-
selection = undefined
|
|
141
|
-
cancelFocus()
|
|
142
|
-
focusTarget = undefined
|
|
143
|
-
setPending(false)
|
|
144
|
-
setError(null)
|
|
145
|
-
ime.onBlur()
|
|
146
|
-
},
|
|
147
|
-
{ defer: true }
|
|
148
|
-
)
|
|
149
|
-
)
|
|
150
|
-
onCleanup(() => {
|
|
151
|
-
active = false
|
|
152
|
-
generation += 1
|
|
153
|
-
cancelFocus()
|
|
154
|
-
props.controlRef?.(undefined)
|
|
155
|
-
})
|
|
156
|
-
const action = (): 'send' | BusySendMode => props.busy?.mode ?? 'send'
|
|
157
|
-
const canSend = (selected: 'send' | BusySendMode = action()) =>
|
|
158
|
-
!unavailable() &&
|
|
159
|
-
!pending() &&
|
|
160
|
-
(props.sendableActions?.[selected] ?? props.value.trim().length > 0) &&
|
|
161
|
-
!(selected !== 'send' && props.busy?.unavailable?.[selected])
|
|
162
|
-
const submit = async (selected: 'send' | BusySendMode = action()) => {
|
|
163
|
-
if (!canSend(selected)) return
|
|
164
|
-
const started = generation
|
|
165
|
-
const input = { text: props.value, action: selected }
|
|
166
|
-
setPending(true)
|
|
167
|
-
setError(null)
|
|
168
|
-
try {
|
|
169
|
-
await props.onSubmit(input)
|
|
170
|
-
} catch {
|
|
171
|
-
if (active && started === generation)
|
|
172
|
-
setError('Message not sent. Check your draft and retry when the connection recovers.')
|
|
173
|
-
} finally {
|
|
174
|
-
if (active && started === generation) setPending(false)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
const preview = () => {
|
|
178
|
-
const first =
|
|
179
|
-
props.value
|
|
180
|
-
.split('\n')
|
|
181
|
-
.find((line) => line.trim().length > 0)
|
|
182
|
-
?.trim() ?? ''
|
|
183
|
-
return first.length > 120 ? `${first.slice(0, 120)}…` : first
|
|
184
|
-
}
|
|
185
61
|
return (
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
</Show>
|
|
210
|
-
<Show when={error()}>
|
|
211
|
-
{(message) => (
|
|
212
|
-
<p role="alert" class="text-destructive mb-1 text-xs">
|
|
213
|
-
{message()}
|
|
214
|
-
</p>
|
|
215
|
-
)}
|
|
216
|
-
</Show>
|
|
217
|
-
<Show when={!props.approvalFocus}>
|
|
218
|
-
<Show
|
|
219
|
-
when={!collapsed()}
|
|
220
|
-
fallback={
|
|
221
|
-
<Button
|
|
222
|
-
ref={(element) => {
|
|
223
|
-
restoreBar = element
|
|
224
|
-
onCleanup(() => {
|
|
225
|
-
if (restoreBar === element) restoreBar = undefined
|
|
226
|
-
})
|
|
227
|
-
}}
|
|
228
|
-
type="button"
|
|
229
|
-
variant="outline"
|
|
230
|
-
size="sm"
|
|
231
|
-
class="w-full justify-start"
|
|
232
|
-
aria-expanded={false}
|
|
233
|
-
aria-label="Show the message input"
|
|
234
|
-
onClick={() => changeCollapsed(false)}
|
|
235
|
-
>
|
|
236
|
-
<ChevronsUpDown />
|
|
237
|
-
<span aria-hidden="true" class="shrink-0">
|
|
238
|
-
Show the message input
|
|
239
|
-
</span>
|
|
240
|
-
<Show when={preview()}>
|
|
241
|
-
<span aria-hidden="true" class="min-w-0 flex-1 truncate">
|
|
242
|
-
{preview()}
|
|
243
|
-
</span>
|
|
244
|
-
</Show>
|
|
245
|
-
</Button>
|
|
246
|
-
}
|
|
247
|
-
>
|
|
248
|
-
<form
|
|
249
|
-
data-slot="composer-input-box"
|
|
250
|
-
class="border-input bg-card flex flex-col overflow-hidden rounded-2xl border transition-colors focus-within:border-ring"
|
|
251
|
-
onSubmit={(event) => {
|
|
252
|
-
event.preventDefault()
|
|
253
|
-
void submit()
|
|
254
|
-
}}
|
|
255
|
-
>
|
|
256
|
-
<Show when={props.attachments}>
|
|
257
|
-
<div data-slot="composer-staged-content">{props.attachments}</div>
|
|
258
|
-
</Show>
|
|
259
|
-
<textarea
|
|
260
|
-
ref={(element) => {
|
|
261
|
-
field = element
|
|
262
|
-
onCleanup(() => {
|
|
263
|
-
if (field === element) field = undefined
|
|
264
|
-
})
|
|
265
|
-
}}
|
|
266
|
-
data-slot="composer-input"
|
|
267
|
-
aria-label={props.inputLabel ?? 'Message'}
|
|
268
|
-
class="field-sizing-content text-foreground placeholder:text-muted-foreground min-h-11 max-h-36 w-full resize-none border-0 bg-transparent px-4 py-3 text-sm outline-none disabled:opacity-50"
|
|
269
|
-
value={props.value}
|
|
270
|
-
onInput={(event) => props.onValueChange(event.currentTarget.value)}
|
|
271
|
-
disabled={props.disabled}
|
|
272
|
-
readOnly={props.readOnly || pending()}
|
|
273
|
-
placeholder={props.placeholder ?? 'Write a message…'}
|
|
274
|
-
rows={1}
|
|
275
|
-
onCompositionStart={ime.onCompositionStart}
|
|
276
|
-
onCompositionEnd={ime.onCompositionEnd}
|
|
277
|
-
onFocus={ime.onFocus}
|
|
278
|
-
onBlur={ime.onBlur}
|
|
279
|
-
onKeyDown={(event) => {
|
|
280
|
-
if (event.key === 'Escape' && error()) {
|
|
281
|
-
setError(null)
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
if (event.key !== 'Enter' || event.shiftKey || event.defaultPrevented) return
|
|
285
|
-
if (ime.claimEnter(event)) {
|
|
286
|
-
const selected =
|
|
287
|
-
props.busy && (event.metaKey || event.ctrlKey)
|
|
288
|
-
? props.busy.mode === 'steer'
|
|
289
|
-
? 'queue'
|
|
290
|
-
: 'steer'
|
|
291
|
-
: action()
|
|
292
|
-
void submit(selected)
|
|
293
|
-
}
|
|
294
|
-
}}
|
|
295
|
-
/>
|
|
296
|
-
<div
|
|
297
|
-
data-slot="composer-action-row"
|
|
298
|
-
class="flex items-center justify-between gap-2 px-2.5 pb-2 pt-0.5"
|
|
299
|
-
>
|
|
300
|
-
<div class="flex min-w-0 items-center gap-1">
|
|
301
|
-
<Show when={props.collapse || props.menuItems}>
|
|
302
|
-
<DropdownMenu placement="top-start">
|
|
303
|
-
<DropdownMenuTrigger
|
|
304
|
-
as={Button}
|
|
305
|
-
type="button"
|
|
306
|
-
variant="ghost"
|
|
307
|
-
size="icon-sm"
|
|
308
|
-
aria-label="Message input options"
|
|
309
|
-
>
|
|
310
|
-
<MoreHorizontal />
|
|
311
|
-
</DropdownMenuTrigger>
|
|
312
|
-
<DropdownMenuContent
|
|
313
|
-
onCloseAutoFocus={(event) => {
|
|
314
|
-
if (focusTarget) event.preventDefault()
|
|
315
|
-
}}
|
|
316
|
-
>
|
|
317
|
-
{props.menuItems}
|
|
318
|
-
<Show when={props.collapse}>
|
|
319
|
-
<DropdownMenuItem onSelect={() => changeCollapsed(true)}>
|
|
320
|
-
<ChevronsDownUp />
|
|
321
|
-
<div class="flex min-w-0 flex-col">
|
|
322
|
-
<span>Collapse the message input</span>
|
|
323
|
-
<span class="text-muted-foreground text-xs">
|
|
324
|
-
Make room to read; keep your draft
|
|
325
|
-
</span>
|
|
326
|
-
</div>
|
|
327
|
-
</DropdownMenuItem>
|
|
328
|
-
</Show>
|
|
329
|
-
</DropdownMenuContent>
|
|
330
|
-
</DropdownMenu>
|
|
331
|
-
</Show>
|
|
332
|
-
{props.leadingActions}
|
|
333
|
-
</div>
|
|
334
|
-
<div class="flex shrink-0 items-center gap-1">
|
|
335
|
-
{props.trailingActions}
|
|
336
|
-
<Show
|
|
337
|
-
when={props.busy}
|
|
338
|
-
fallback={
|
|
339
|
-
<Button
|
|
340
|
-
type="submit"
|
|
341
|
-
size="icon-sm"
|
|
342
|
-
disabled={!canSend()}
|
|
343
|
-
aria-label={pending() ? 'Sending message' : 'Send message'}
|
|
344
|
-
>
|
|
345
|
-
<Show when={pending()} fallback={<ArrowUp />}>
|
|
346
|
-
<Spinner size="sm" label={false} />
|
|
347
|
-
</Show>
|
|
348
|
-
</Button>
|
|
349
|
-
}
|
|
350
|
-
>
|
|
351
|
-
{(busy) => (
|
|
352
|
-
<BusySendButton
|
|
353
|
-
{...busy()}
|
|
354
|
-
alternateActionHint={
|
|
355
|
-
canSend(busy().mode === 'steer' ? 'queue' : 'steer')
|
|
356
|
-
? 'Ctrl/Cmd+Enter uses the other action'
|
|
357
|
-
: undefined
|
|
358
|
-
}
|
|
359
|
-
disabled={!canSend()}
|
|
360
|
-
onFire={() => {
|
|
361
|
-
void submit()
|
|
362
|
-
}}
|
|
363
|
-
/>
|
|
364
|
-
)}
|
|
365
|
-
</Show>
|
|
366
|
-
</div>
|
|
367
|
-
</div>
|
|
368
|
-
</form>
|
|
369
|
-
<Show when={props.context}>
|
|
370
|
-
<div data-slot="composer-context" class="flex min-w-0 items-center gap-2 pt-1">
|
|
371
|
-
{props.context}
|
|
372
|
-
</div>
|
|
373
|
-
</Show>
|
|
374
|
-
</Show>
|
|
375
|
-
</Show>
|
|
376
|
-
</div>
|
|
62
|
+
<ChatComposerShell
|
|
63
|
+
{...props}
|
|
64
|
+
createSubmitExtra={() => ({})}
|
|
65
|
+
renderInput={(input) => (
|
|
66
|
+
<textarea
|
|
67
|
+
ref={input.ref}
|
|
68
|
+
data-slot="composer-input"
|
|
69
|
+
aria-label={input.inputLabel()}
|
|
70
|
+
class="field-sizing-content text-foreground placeholder:text-muted-foreground min-h-11 max-h-36 w-full resize-none border-0 bg-transparent px-4 py-3 text-sm outline-none disabled:opacity-50"
|
|
71
|
+
value={input.value()}
|
|
72
|
+
onInput={(event) => props.onValueChange(event.currentTarget.value)}
|
|
73
|
+
disabled={input.disabled()}
|
|
74
|
+
readOnly={input.readOnly()}
|
|
75
|
+
placeholder={input.placeholder()}
|
|
76
|
+
rows={1}
|
|
77
|
+
onCompositionStart={input.onCompositionStart}
|
|
78
|
+
onCompositionEnd={input.onCompositionEnd}
|
|
79
|
+
onFocus={input.onFocus}
|
|
80
|
+
onBlur={input.onBlur}
|
|
81
|
+
onKeyDown={input.onKeyDown}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
/>
|
|
377
85
|
)
|
|
378
86
|
}
|