@adea-ai/ui 0.68.1 → 0.69.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 (46) hide show
  1. package/README.md +79 -15
  2. package/dist/NOTICE +55 -24
  3. package/dist/components/conversation/atomic/chat-composer.d.ts +20 -0
  4. package/dist/components/conversation/atomic/chat-composer.d.ts.map +1 -0
  5. package/dist/components/conversation/atomic/chat-composer.js +69 -0
  6. package/dist/components/conversation/atomic/chat-composer.js.map +1 -0
  7. package/dist/components/conversation/atomic/index.d.ts +3 -0
  8. package/dist/components/conversation/atomic/index.d.ts.map +1 -0
  9. package/dist/components/conversation/atomic/index.js +2 -0
  10. package/dist/components/conversation/chat-composer.d.ts +12 -21
  11. package/dist/components/conversation/chat-composer.d.ts.map +1 -1
  12. package/dist/components/conversation/chat-composer.js +33 -403
  13. package/dist/components/conversation/chat-composer.js.map +1 -1
  14. package/dist/components/conversation/internal/chat-composer-shell.d.ts +38 -0
  15. package/dist/components/conversation/internal/chat-composer-shell.d.ts.map +1 -0
  16. package/dist/components/conversation/internal/chat-composer-shell.js +423 -0
  17. package/dist/components/conversation/internal/chat-composer-shell.js.map +1 -0
  18. package/dist/components/conversation/paste-preview.d.ts +11 -0
  19. package/dist/components/conversation/paste-preview.d.ts.map +1 -0
  20. package/dist/components/conversation/paste-preview.js +28 -0
  21. package/dist/components/conversation/paste-preview.js.map +1 -0
  22. package/dist/components/conversation/paste-token-editor.d.ts +26 -0
  23. package/dist/components/conversation/paste-token-editor.d.ts.map +1 -0
  24. package/dist/components/conversation/paste-token-editor.js +570 -0
  25. package/dist/components/conversation/paste-token-editor.js.map +1 -0
  26. package/dist/components/conversation/paste-tokens.d.ts.map +1 -1
  27. package/dist/components/conversation/paste-tokens.js +11 -2
  28. package/dist/components/conversation/paste-tokens.js.map +1 -1
  29. package/dist/r/conversation.json +26 -0
  30. package/dist/r/registry.json +26 -0
  31. package/dist/r/src/components/conversation/atomic/chat-composer.tsx +57 -0
  32. package/dist/r/src/components/conversation/atomic/index.ts +6 -0
  33. package/dist/r/src/components/conversation/chat-composer.tsx +39 -331
  34. package/dist/r/src/components/conversation/internal/chat-composer-shell.tsx +390 -0
  35. package/dist/r/src/components/conversation/paste-preview.ts +35 -0
  36. package/dist/r/src/components/conversation/paste-token-editor.tsx +672 -0
  37. package/dist/r/src/components/conversation/paste-tokens.ts +15 -2
  38. package/package.json +1 -1
  39. package/registry.json +26 -0
  40. package/src/components/conversation/atomic/chat-composer.tsx +57 -0
  41. package/src/components/conversation/atomic/index.ts +6 -0
  42. package/src/components/conversation/chat-composer.tsx +39 -331
  43. package/src/components/conversation/internal/chat-composer-shell.tsx +390 -0
  44. package/src/components/conversation/paste-preview.ts +35 -0
  45. package/src/components/conversation/paste-token-editor.tsx +672 -0
  46. package/src/components/conversation/paste-tokens.ts +15 -2
@@ -0,0 +1,390 @@
1
+ /*
2
+ * Substantially translated from KiroCrew website/src/components/ChatInput.tsx
3
+ * at 283e136c0f902e965a535a7c9548c57c7504fed0.
4
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
5
+ * Licensed under Apache-2.0; see LICENSE and NOTICE.
6
+ */
7
+ import { ChevronsDownUp, ChevronsUpDown, MoreHorizontal, ArrowUp } from 'lucide-solid'
8
+ import {
9
+ Show,
10
+ createEffect,
11
+ createSignal,
12
+ on,
13
+ onCleanup,
14
+ onMount,
15
+ type Accessor,
16
+ type JSX,
17
+ } from 'solid-js'
18
+ import type {
19
+ ChatComposerControl,
20
+ ChatComposerSharedProps,
21
+ ChatComposerSubmitInput,
22
+ } from '../chat-composer'
23
+ import { Button } from '../../ui/button/button'
24
+ import {
25
+ DropdownMenu,
26
+ DropdownMenuContent,
27
+ DropdownMenuItem,
28
+ DropdownMenuTrigger,
29
+ } from '../../ui/dropdown-menu/dropdown-menu'
30
+ import { Spinner } from '../../ui/spinner/spinner'
31
+ import { BusySendButton, type BusySendMode } from '../busy-send-button'
32
+ import { createComposerImeGuard } from '../ime-guard'
33
+
34
+ /** Private typed seam: public wrappers supply the native or atomic input. */
35
+ type ChatComposerInputRenderProps = {
36
+ ref: (element: HTMLTextAreaElement | undefined) => void
37
+ value: Accessor<string>
38
+ resetKey: Accessor<unknown>
39
+ inputLabel: Accessor<string>
40
+ placeholder: Accessor<string>
41
+ disabled: Accessor<boolean | undefined>
42
+ readOnly: Accessor<boolean>
43
+ composing: Accessor<boolean>
44
+ onCompositionStart: () => void
45
+ onCompositionEnd: () => void
46
+ onFocus: () => void
47
+ onBlur: () => void
48
+ onKeyDown: (event: KeyboardEvent) => void
49
+ }
50
+
51
+ type ChatComposerShellProps<Extra extends object> = ChatComposerSharedProps & {
52
+ createSubmitExtra: (input: ChatComposerSubmitInput) => Extra
53
+ onSubmit: (input: ChatComposerSubmitInput & Extra) => void | Promise<void>
54
+ renderInput: (props: ChatComposerInputRenderProps) => JSX.Element
55
+ }
56
+
57
+ /**
58
+ * Kiro's composer assembly: context/options, adjacent band, approval/notices,
59
+ * staged content, input/action groups and a context shelf. Reading collapse
60
+ * really unmounts the input and shelf and offers a labeled way back with draft
61
+ * preview. The host retains the only draft, identities, preferences and services.
62
+ *
63
+ * Kobalte owns the options menu. Scoped imperative typing intent replaces the
64
+ * donor global lookup/broadcast so a second composer cannot receive this draft
65
+ * or focus. No animation starts from hidden geometry, no global shortcut, and no
66
+ * runtime/permission/voice/optimizer authority is copied. More specific chips,
67
+ * approval decisions and staged-content semantics are supplied by their owners.
68
+ */
69
+ export function ChatComposerShell<Extra extends object>(props: ChatComposerShellProps<Extra>) {
70
+ const ime = createComposerImeGuard()
71
+ const [pending, setPending] = createSignal(false)
72
+ const [error, setError] = createSignal<string | null>(null)
73
+ let field: HTMLTextAreaElement | undefined
74
+ let restoreBar: HTMLButtonElement | undefined
75
+ let focusFrame: number | undefined
76
+ let focusTarget: 'input' | 'bar' | undefined
77
+ let selection:
78
+ | { start: number; end: number; direction: 'forward' | 'backward' | 'none' }
79
+ | undefined
80
+ let generation = 0
81
+ let active = true
82
+ let composing = false
83
+ const collapsed = () => props.collapse?.value === true
84
+ const unavailable = () => props.disabled || props.readOnly || !!props.blockedReason
85
+ const cancelFocus = () => {
86
+ if (focusFrame !== undefined) cancelAnimationFrame(focusFrame)
87
+ focusFrame = undefined
88
+ }
89
+ const requestFocus = (target: 'input' | 'bar') => {
90
+ cancelFocus()
91
+ focusTarget = target
92
+ focusFrame = requestAnimationFrame(() => {
93
+ focusFrame = undefined
94
+ if (!active || props.approvalFocus) return
95
+ if (target === 'bar' && collapsed()) restoreBar?.focus()
96
+ if (target === 'input' && !collapsed() && !props.disabled && field) {
97
+ field.focus()
98
+ if (selection)
99
+ field.setSelectionRange(
100
+ Math.min(selection.start, field.value.length),
101
+ Math.min(selection.end, field.value.length),
102
+ selection.direction
103
+ )
104
+ }
105
+ focusTarget = undefined
106
+ })
107
+ }
108
+ const changeCollapsed = (value: boolean) => {
109
+ if (!props.collapse || props.approvalFocus) return
110
+ if (value && field)
111
+ selection = {
112
+ start: field.selectionStart,
113
+ end: field.selectionEnd,
114
+ direction: field.selectionDirection,
115
+ }
116
+ props.collapse.onChange(value)
117
+ requestFocus(value ? 'bar' : 'input')
118
+ }
119
+ const control: ChatComposerControl = {
120
+ focus() {
121
+ if (collapsed() || props.approvalFocus || props.disabled || !field) return false
122
+ field.focus()
123
+ return true
124
+ },
125
+ expandAndFocus() {
126
+ if (props.approvalFocus || props.disabled) return false
127
+ if (collapsed()) changeCollapsed(false)
128
+ else requestFocus('input')
129
+ return true
130
+ },
131
+ }
132
+ onMount(() => props.controlRef?.(control))
133
+ createEffect(
134
+ on(
135
+ () => props.resetKey,
136
+ () => {
137
+ generation += 1
138
+ selection = undefined
139
+ composing = false
140
+ cancelFocus()
141
+ focusTarget = undefined
142
+ setPending(false)
143
+ setError(null)
144
+ ime.onBlur()
145
+ },
146
+ { defer: true }
147
+ )
148
+ )
149
+ onCleanup(() => {
150
+ active = false
151
+ generation += 1
152
+ cancelFocus()
153
+ props.controlRef?.(undefined)
154
+ })
155
+ const action = (): 'send' | BusySendMode => props.busy?.mode ?? 'send'
156
+ const canSend = (selected: 'send' | BusySendMode = action()) =>
157
+ !unavailable() &&
158
+ !pending() &&
159
+ (props.sendableActions?.[selected] ?? props.value.trim().length > 0) &&
160
+ !(selected !== 'send' && props.busy?.unavailable?.[selected])
161
+ const submit = async (selected: 'send' | BusySendMode = action()) => {
162
+ if (!canSend(selected)) return
163
+ const started = generation
164
+ const base: ChatComposerSubmitInput = { text: props.value, action: selected }
165
+ const input = { ...base, ...props.createSubmitExtra(base) }
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
+ const handleInputKeyDown = (event: KeyboardEvent) => {
186
+ if (event.key === 'Escape' && error()) {
187
+ setError(null)
188
+ return
189
+ }
190
+ if (event.key !== 'Enter' || event.shiftKey || event.defaultPrevented) return
191
+ if (ime.claimEnter(event)) {
192
+ const selected =
193
+ props.busy && (event.metaKey || event.ctrlKey)
194
+ ? props.busy.mode === 'steer'
195
+ ? 'queue'
196
+ : 'steer'
197
+ : action()
198
+ void submit(selected)
199
+ }
200
+ }
201
+ const handleCompositionStart = () => {
202
+ composing = true
203
+ ime.onCompositionStart()
204
+ }
205
+ const handleCompositionEnd = () => {
206
+ composing = false
207
+ ime.onCompositionEnd()
208
+ }
209
+ const handleBlur = () => {
210
+ composing = false
211
+ ime.onBlur()
212
+ }
213
+ return (
214
+ <div data-slot="chat-composer" class="mx-auto flex w-full max-w-4xl flex-col px-4 pb-1">
215
+ <Show when={!props.approvalFocus}>
216
+ <Show when={props.knowledge}>
217
+ <div data-slot="composer-knowledge">{props.knowledge}</div>
218
+ </Show>
219
+ <Show when={props.followUps}>
220
+ <div data-slot="composer-follow-ups">{props.followUps}</div>
221
+ </Show>
222
+ </Show>
223
+ <Show when={props.band}>
224
+ <div data-slot="composer-band">{props.band}</div>
225
+ </Show>
226
+ <div aria-hidden="true" class="h-1.5 shrink-0" />
227
+ <Show when={props.approval}>
228
+ <div data-slot="composer-approval">{props.approval}</div>
229
+ </Show>
230
+ <Show when={props.notices}>
231
+ <div data-slot="composer-notices">{props.notices}</div>
232
+ </Show>
233
+ <Show when={props.blockedReason}>
234
+ <p role="status" class="text-muted-foreground mb-1 text-xs">
235
+ {props.blockedReason}
236
+ </p>
237
+ </Show>
238
+ <Show when={error()}>
239
+ {(message) => (
240
+ <p role="alert" class="text-destructive mb-1 text-xs">
241
+ {message()}
242
+ </p>
243
+ )}
244
+ </Show>
245
+ <Show when={!props.approvalFocus}>
246
+ <Show
247
+ when={!collapsed()}
248
+ fallback={
249
+ <Button
250
+ ref={(element) => {
251
+ restoreBar = element
252
+ onCleanup(() => {
253
+ if (restoreBar === element) restoreBar = undefined
254
+ })
255
+ }}
256
+ type="button"
257
+ variant="outline"
258
+ size="sm"
259
+ class="w-full justify-start"
260
+ aria-expanded={false}
261
+ aria-label="Show the message input"
262
+ onClick={() => changeCollapsed(false)}
263
+ >
264
+ <ChevronsUpDown />
265
+ <span aria-hidden="true" class="shrink-0">
266
+ Show the message input
267
+ </span>
268
+ <Show when={preview()}>
269
+ <span aria-hidden="true" class="min-w-0 flex-1 truncate">
270
+ {preview()}
271
+ </span>
272
+ </Show>
273
+ </Button>
274
+ }
275
+ >
276
+ <form
277
+ data-slot="composer-input-box"
278
+ class="border-input bg-card flex flex-col overflow-hidden rounded-2xl border transition-colors focus-within:border-ring"
279
+ onSubmit={(event) => {
280
+ event.preventDefault()
281
+ void submit()
282
+ }}
283
+ >
284
+ <Show when={props.attachments}>
285
+ <div data-slot="composer-staged-content">{props.attachments}</div>
286
+ </Show>
287
+ {props.renderInput({
288
+ ref: (element) => {
289
+ field = element
290
+ if (element)
291
+ onCleanup(() => {
292
+ if (field === element) field = undefined
293
+ })
294
+ },
295
+ value: () => props.value,
296
+ resetKey: () => props.resetKey,
297
+ inputLabel: () => props.inputLabel ?? 'Message',
298
+ placeholder: () => props.placeholder ?? 'Write a message…',
299
+ disabled: () => props.disabled,
300
+ readOnly: () => props.readOnly || pending(),
301
+ composing: () => composing,
302
+ onCompositionStart: handleCompositionStart,
303
+ onCompositionEnd: handleCompositionEnd,
304
+ onFocus: ime.onFocus,
305
+ onBlur: handleBlur,
306
+ onKeyDown: handleInputKeyDown,
307
+ })}
308
+ <div
309
+ data-slot="composer-action-row"
310
+ class="flex items-center justify-between gap-2 px-2.5 pb-2 pt-0.5"
311
+ >
312
+ <div class="flex min-w-0 items-center gap-1">
313
+ <Show when={props.collapse || props.menuItems}>
314
+ <DropdownMenu placement="top-start">
315
+ <DropdownMenuTrigger
316
+ as={Button}
317
+ type="button"
318
+ variant="ghost"
319
+ size="icon-sm"
320
+ aria-label="Message input options"
321
+ >
322
+ <MoreHorizontal />
323
+ </DropdownMenuTrigger>
324
+ <DropdownMenuContent
325
+ onCloseAutoFocus={(event) => {
326
+ if (focusTarget) event.preventDefault()
327
+ }}
328
+ >
329
+ {props.menuItems}
330
+ <Show when={props.collapse}>
331
+ <DropdownMenuItem onSelect={() => changeCollapsed(true)}>
332
+ <ChevronsDownUp />
333
+ <div class="flex min-w-0 flex-col">
334
+ <span>Collapse the message input</span>
335
+ <span class="text-muted-foreground text-xs">
336
+ Make room to read; keep your draft
337
+ </span>
338
+ </div>
339
+ </DropdownMenuItem>
340
+ </Show>
341
+ </DropdownMenuContent>
342
+ </DropdownMenu>
343
+ </Show>
344
+ {props.leadingActions}
345
+ </div>
346
+ <div class="flex shrink-0 items-center gap-1">
347
+ {props.trailingActions}
348
+ <Show
349
+ when={props.busy}
350
+ fallback={
351
+ <Button
352
+ type="submit"
353
+ size="icon-sm"
354
+ disabled={!canSend()}
355
+ aria-label={pending() ? 'Sending message' : 'Send message'}
356
+ >
357
+ <Show when={pending()} fallback={<ArrowUp />}>
358
+ <Spinner size="sm" label={false} />
359
+ </Show>
360
+ </Button>
361
+ }
362
+ >
363
+ {(busy) => (
364
+ <BusySendButton
365
+ {...busy()}
366
+ alternateActionHint={
367
+ canSend(busy().mode === 'steer' ? 'queue' : 'steer')
368
+ ? 'Ctrl/Cmd+Enter uses the other action'
369
+ : undefined
370
+ }
371
+ disabled={!canSend()}
372
+ onFire={() => {
373
+ void submit()
374
+ }}
375
+ />
376
+ )}
377
+ </Show>
378
+ </div>
379
+ </div>
380
+ </form>
381
+ <Show when={props.context}>
382
+ <div data-slot="composer-context" class="flex min-w-0 items-center gap-2 pt-1">
383
+ {props.context}
384
+ </div>
385
+ </Show>
386
+ </Show>
387
+ </Show>
388
+ </div>
389
+ )
390
+ }
@@ -0,0 +1,35 @@
1
+ export const PASTE_PREVIEW_MAX_LINES = 12
2
+ export const PASTE_PREVIEW_MAX_CHARACTERS = 1200
3
+
4
+ export interface PastePreviewSummary {
5
+ text: string
6
+ hiddenLines: number
7
+ hiddenCharacters: number
8
+ scannedCharacters: number
9
+ }
10
+
11
+ /** Build a bounded preview without splitting or joining the full paste content. */
12
+ export function summarizePastePreview(content: string, totalLines: number): PastePreviewSummary {
13
+ const scanLimit = Math.min(content.length, PASTE_PREVIEW_MAX_CHARACTERS)
14
+ let end = 0
15
+ let visibleLines = 1
16
+ let scannedCharacters = 0
17
+
18
+ while (end < scanLimit) {
19
+ const character = content.charCodeAt(end)
20
+ scannedCharacters += 1
21
+ if (character === 10) {
22
+ if (visibleLines === PASTE_PREVIEW_MAX_LINES) break
23
+ visibleLines += 1
24
+ }
25
+ end += 1
26
+ }
27
+
28
+ const text = content.slice(0, end)
29
+ return {
30
+ text,
31
+ hiddenLines: Math.max(0, totalLines - visibleLines),
32
+ hiddenCharacters: Math.max(0, content.length - text.length),
33
+ scannedCharacters,
34
+ }
35
+ }