@weft-js/warp 0.0.0-stage → 0.1.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.
- package/LICENSE +16 -0
- package/README.md +55 -2
- package/package.json +43 -4
- package/plugin.d.mts +30 -0
- package/plugin.mjs +358 -0
- package/src/Accordion.tsx +58 -0
- package/src/Alert.tsx +25 -0
- package/src/Badge.tsx +17 -0
- package/src/Button.tsx +100 -0
- package/src/Calendar.tsx +162 -0
- package/src/Canvas.tsx +110 -0
- package/src/Card.tsx +29 -0
- package/src/Chart.tsx +133 -0
- package/src/Charts.tsx +69 -0
- package/src/Chip.tsx +27 -0
- package/src/Choice.tsx +105 -0
- package/src/CodeBlock.tsx +65 -0
- package/src/CodeInput.tsx +130 -0
- package/src/Combobox.tsx +113 -0
- package/src/CommandPalette.tsx +348 -0
- package/src/DataGrid.tsx +561 -0
- package/src/DateField.tsx +221 -0
- package/src/Dither.tsx +129 -0
- package/src/Drawer.tsx +41 -0
- package/src/DurationField.tsx +98 -0
- package/src/Feed.tsx +74 -0
- package/src/Field.tsx +147 -0
- package/src/Icon.tsx +8 -0
- package/src/Kbd.tsx +6 -0
- package/src/KeyValueEditor.tsx +114 -0
- package/src/Layout.tsx +109 -0
- package/src/Markdown.tsx +63 -0
- package/src/MentionsInput.tsx +150 -0
- package/src/Menu.tsx +111 -0
- package/src/Modal.tsx +40 -0
- package/src/MonthPicker.tsx +190 -0
- package/src/Nav.tsx +20 -0
- package/src/NumberField.tsx +78 -0
- package/src/Pagination.tsx +122 -0
- package/src/PaymentCard.tsx +63 -0
- package/src/Picker.tsx +120 -0
- package/src/PlaceField.tsx +126 -0
- package/src/Popover.tsx +34 -0
- package/src/Progress.tsx +26 -0
- package/src/RecurrenceField.tsx +126 -0
- package/src/RichSelect.tsx +243 -0
- package/src/RichTextEditor.tsx +207 -0
- package/src/ScrollGrid.tsx +190 -0
- package/src/Segments.tsx +70 -0
- package/src/Select.tsx +192 -0
- package/src/Shape.tsx +20 -0
- package/src/Shell.tsx +308 -0
- package/src/SideNav.tsx +147 -0
- package/src/SignaturePad.tsx +120 -0
- package/src/Skeleton.tsx +14 -0
- package/src/Slider.tsx +42 -0
- package/src/Spinner.tsx +7 -0
- package/src/Table.tsx +528 -0
- package/src/Tabs.tsx +67 -0
- package/src/Tag.tsx +15 -0
- package/src/Ticker.tsx +36 -0
- package/src/TimeField.tsx +237 -0
- package/src/Toast.tsx +56 -0
- package/src/Toggle.tsx +42 -0
- package/src/Tooltip.tsx +32 -0
- package/src/Type.tsx +46 -0
- package/src/VirtualList.tsx +386 -0
- package/src/admin/ApiKeys.tsx +136 -0
- package/src/admin/Audit.tsx +93 -0
- package/src/admin/Billing.tsx +123 -0
- package/src/admin/Cart.tsx +96 -0
- package/src/admin/Checkout.tsx +300 -0
- package/src/admin/CsvImport.tsx +174 -0
- package/src/admin/Danger.tsx +110 -0
- package/src/admin/Integrations.tsx +200 -0
- package/src/admin/InviteDialog.tsx +64 -0
- package/src/admin/Members.tsx +186 -0
- package/src/admin/Order.tsx +63 -0
- package/src/admin/PlainTable.tsx +22 -0
- package/src/admin/Plans.tsx +167 -0
- package/src/admin/SaveBar.tsx +112 -0
- package/src/admin/Secret.tsx +37 -0
- package/src/admin/Settings.tsx +76 -0
- package/src/admin/StepBar.tsx +35 -0
- package/src/admin/TagsInput.tsx +79 -0
- package/src/admin/atoms.tsx +33 -0
- package/src/admin/index.ts +16 -0
- package/src/agent/Actions.tsx +85 -0
- package/src/agent/AgentPlan.tsx +49 -0
- package/src/agent/ArtifactPanel.tsx +80 -0
- package/src/agent/Assistant.tsx +96 -0
- package/src/agent/Checkpoint.tsx +14 -0
- package/src/agent/Cite.tsx +39 -0
- package/src/agent/ContextMeter.tsx +31 -0
- package/src/agent/Conversation.tsx +34 -0
- package/src/agent/Message.tsx +53 -0
- package/src/agent/ModelPicker.tsx +65 -0
- package/src/agent/Prompt.tsx +197 -0
- package/src/agent/Prose.tsx +54 -0
- package/src/agent/Question.tsx +52 -0
- package/src/agent/Reasoning.tsx +19 -0
- package/src/agent/Shimmer.tsx +7 -0
- package/src/agent/Sources.tsx +23 -0
- package/src/agent/SpeechInput.tsx +102 -0
- package/src/agent/Suggestions.tsx +22 -0
- package/src/agent/ToolCall.tsx +91 -0
- package/src/agent/TraceViewer.tsx +55 -0
- package/src/agent/bits.tsx +40 -0
- package/src/agent/index.ts +22 -0
- package/src/bind.ts +42 -0
- package/src/collab/AutoTrack.tsx +42 -0
- package/src/collab/Comments.tsx +94 -0
- package/src/collab/Composer.tsx +173 -0
- package/src/collab/ConversationList.tsx +73 -0
- package/src/collab/Cursors.tsx +35 -0
- package/src/collab/DataIntegrity.tsx +45 -0
- package/src/collab/Kanban.tsx +185 -0
- package/src/collab/Lock.tsx +23 -0
- package/src/collab/PresenceStack.tsx +67 -0
- package/src/collab/ShareDialog.tsx +118 -0
- package/src/collab/Status.tsx +42 -0
- package/src/collab/SyncedForm.tsx +19 -0
- package/src/collab/Thread.tsx +273 -0
- package/src/collab/VersionHistory.tsx +49 -0
- package/src/collab/ViewportRail.tsx +49 -0
- package/src/collab/index.ts +16 -0
- package/src/collab/peer.tsx +16 -0
- package/src/fields.ts +5 -0
- package/src/flow/AuthScreen.tsx +40 -0
- package/src/flow/AuthSteps.tsx +120 -0
- package/src/flow/Checklist.tsx +84 -0
- package/src/flow/ConnBanner.tsx +147 -0
- package/src/flow/Flow.tsx +63 -0
- package/src/flow/MfaSetup.tsx +103 -0
- package/src/flow/Otp.tsx +114 -0
- package/src/flow/ReauthDialog.tsx +47 -0
- package/src/flow/ShortcutsOverlay.tsx +60 -0
- package/src/flow/SignGate.tsx +98 -0
- package/src/flow/SignIn.tsx +80 -0
- package/src/flow/SignUp.tsx +71 -0
- package/src/flow/Sso.tsx +38 -0
- package/src/flow/SystemPage.tsx +247 -0
- package/src/flow/Tour.tsx +108 -0
- package/src/flow/index.ts +17 -0
- package/src/flow/kit.tsx +170 -0
- package/src/index.ts +74 -0
- package/src/layout.css +4 -0
- package/src/links.css +4 -0
- package/src/live/ActivityFeed.tsx +49 -0
- package/src/live/Bell.tsx +62 -0
- package/src/live/Connection.tsx +49 -0
- package/src/live/Inbox.tsx +220 -0
- package/src/live/MeetingRoom.tsx +298 -0
- package/src/live/NotificationPreferences.tsx +193 -0
- package/src/live/Prejoin.tsx +140 -0
- package/src/live/PushSoftAsk.tsx +37 -0
- package/src/live/TransportPicker.tsx +163 -0
- package/src/live/index.ts +12 -0
- package/src/live/notify.tsx +16 -0
- package/src/live/shared.tsx +39 -0
- package/src/parts.tsx +93 -0
- package/src/schedule/Agenda.tsx +173 -0
- package/src/schedule/AvailabilityEditor.tsx +164 -0
- package/src/schedule/BookingCalendar.tsx +132 -0
- package/src/schedule/Calendar.tsx +746 -0
- package/src/schedule/EventEditor.tsx +87 -0
- package/src/schedule/EventPopover.tsx +65 -0
- package/src/schedule/MeetingPlanner.tsx +214 -0
- package/src/schedule/MoveSheet.tsx +78 -0
- package/src/schedule/YearView.tsx +163 -0
- package/src/schedule/bits.tsx +51 -0
- package/src/schedule/index.ts +10 -0
- package/src/skeleton-css.ts +4 -0
- package/src/styles.ts +10 -0
- package/src/tokens.d.ts +2 -0
- package/src/type.css +15 -0
- package/src/ui.ts +26 -0
- package/src/use-ui.ts +10 -0
- package/tailwind.css +38 -0
- package/tokens/color.mjs +66 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { untrack } from '@weft-js/core'
|
|
2
|
+
import { uid } from '@weft-js/warp-core'
|
|
3
|
+
import { Glyph } from '../parts'
|
|
4
|
+
import '../styles'
|
|
5
|
+
import '@weft-js/warp-core/styles/flows/tokens.css'
|
|
6
|
+
import '@weft-js/warp-core/styles/flows/base.css'
|
|
7
|
+
import '@weft-js/warp-core/styles/rich/helpers.css'
|
|
8
|
+
import '@weft-js/warp-core/styles/layout/tokens.css'
|
|
9
|
+
import '@weft-js/warp-core/styles/layout/row.css'
|
|
10
|
+
import '@weft-js/warp-core/styles/layout/stack.css'
|
|
11
|
+
import '@weft-js/warp-core/styles/flows/settings.css'
|
|
12
|
+
import '@weft-js/warp-core/styles/flows/settings-section.css'
|
|
13
|
+
import '@weft-js/warp-core/styles/flows/setting-row.css'
|
|
14
|
+
|
|
15
|
+
/** One link of the settings nav. */
|
|
16
|
+
export type SettingsNavItem = { id: string; label: string; glyph?: string; tone?: 'critical' | undefined }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The settings frame (09 §4.1): a TabNav-like strip that becomes a sticky column at 760px of its own width, and a form
|
|
20
|
+
* holding the sections. A nav press scrolls to the section and moves focus to its heading.
|
|
21
|
+
*/
|
|
22
|
+
export function SettingsLayout({ label = 'Settings sections', sections, current, onNavigate, children }: { label?: string | undefined; sections: SettingsNavItem[]; current: string; onNavigate?: (id: string) => void; children: unknown }) {
|
|
23
|
+
const go = (e: MouseEvent, id: string) => {
|
|
24
|
+
e.preventDefault()
|
|
25
|
+
onNavigate?.(id)
|
|
26
|
+
const doc = (e.currentTarget as Element).ownerDocument
|
|
27
|
+
const sec = doc.getElementById(id)
|
|
28
|
+
const reduce = doc.defaultView?.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
29
|
+
sec?.scrollIntoView?.({ behavior: reduce ? 'auto' : 'smooth', block: 'start' })
|
|
30
|
+
sec?.querySelector<HTMLElement>('header :is(h3, h4)')?.focus({ preventScroll: true })
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
<div data-scope="settings">
|
|
34
|
+
<div data-part="layout">
|
|
35
|
+
<nav data-part="nav" aria-label={label}>
|
|
36
|
+
{sections.map(s => (
|
|
37
|
+
<a key={s.id} href={`#${s.id}`} data-tone={s.tone} aria-current={s.id === current ? 'true' : undefined} onClick={(e: MouseEvent) => go(e, s.id)}>
|
|
38
|
+
{s.glyph && <Glyph d={s.glyph} />}{s.label}
|
|
39
|
+
</a>
|
|
40
|
+
))}
|
|
41
|
+
</nav>
|
|
42
|
+
<form data-part="content" novalidate>{children}</form>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A heading, one line, then one bordered group of rows (09 §4.1). `tone="critical"` is the danger zone's border. */
|
|
49
|
+
export function SettingsSection({ id, title, description, tone, as: heading = 'h3', children }: { id: string; title: unknown; description?: unknown; tone?: 'critical' | undefined; as?: 'h3' | 'h4' | undefined; children: unknown }) {
|
|
50
|
+
return (
|
|
51
|
+
<section data-scope="settings-section" id={id} data-tone={tone} aria-labelledby={`${id}-h`}>
|
|
52
|
+
<header>
|
|
53
|
+
{heading === 'h4' ? <h4 id={`${id}-h`} tabindex="-1">{title}</h4> : <h3 id={`${id}-h`} tabindex="-1">{title}</h3>}
|
|
54
|
+
{description !== undefined && <p>{description}</p>}
|
|
55
|
+
</header>
|
|
56
|
+
<div data-part="group">{children}</div>
|
|
57
|
+
</section>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Label and help at the inline-start, the control at the inline-end, stacking under 520px (09 §4.1). `children` may be
|
|
63
|
+
* a function of the label's and help's ids, so a Switch can be labelled by them instead of repeating the words.
|
|
64
|
+
*/
|
|
65
|
+
export function SettingRow({ title, description, layout, children }: { title?: unknown; description?: unknown; layout?: 'stack'; children: unknown }) {
|
|
66
|
+
const id = uid('row')
|
|
67
|
+
const body = () => (typeof children === 'function' ? children({ label: `${id}-l`, description: `${id}-d` }) : children)
|
|
68
|
+
const stacked = untrack(() => title === undefined)
|
|
69
|
+
if (stacked) return <div data-scope="setting-row" data-layout={layout}>{body()}</div>
|
|
70
|
+
return (
|
|
71
|
+
<div data-scope="setting-row" data-layout={layout}>
|
|
72
|
+
<div><b id={`${id}-l`}>{title}</b><p id={`${id}-d`}>{description}</p></div>
|
|
73
|
+
<div data-part="control">{body()}</div>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CheckIcon } from '@weft-js/warp-icons'
|
|
2
|
+
import { derived } from '@weft-js/core'
|
|
3
|
+
import '../parts'
|
|
4
|
+
import '@weft-js/warp-core/styles/rich/helpers.css'
|
|
5
|
+
import '@weft-js/warp-core/styles/layout/tokens.css'
|
|
6
|
+
import '@weft-js/warp-core/styles/layout/row.css'
|
|
7
|
+
import '@weft-js/warp-core/styles/layout/stack.css'
|
|
8
|
+
import '@weft-js/warp-core/styles/components/progress.css'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Steps (05 §4) with the compact form 09 §6.2 asks for: steps.css hides the list under 520px and shows
|
|
12
|
+
* `[data-part="compact"]`, "Payment, step 4 of 5" over a bar, which parts' Steps doesn't render.
|
|
13
|
+
*/
|
|
14
|
+
export function StepBar({ label, steps, current, error }: { label: string; steps: { id: string; title: string }[]; current: number; error?: number }) {
|
|
15
|
+
const at = derived(() => Math.min(current, steps.length - 1))
|
|
16
|
+
return (
|
|
17
|
+
<nav data-scope="steps" aria-label={label}>
|
|
18
|
+
<ol>
|
|
19
|
+
{steps.map((s, i) => (
|
|
20
|
+
<li key={s.id} data-part="item" data-state={i === error ? 'error' : i < current ? 'complete' : i === current ? 'current' : undefined} aria-current={i === current ? 'step' : undefined}>
|
|
21
|
+
<span data-part="indicator"><CheckIcon /></span>
|
|
22
|
+
<span data-part="title">{s.title}</span>
|
|
23
|
+
</li>
|
|
24
|
+
))}
|
|
25
|
+
</ol>
|
|
26
|
+
<div data-part="compact">
|
|
27
|
+
<div data-scope="row" data-justify="between">
|
|
28
|
+
<b data-part="compact-title">{steps[at.value]!.title}</b>
|
|
29
|
+
<span class="muted">Step {at.value + 1} of {steps.length}</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div data-scope="progress" data-part="root" style={`--_v:${(at.value + 1) / steps.length}`}><div data-part="track"><div data-part="range"></div></div></div>
|
|
32
|
+
</div>
|
|
33
|
+
</nav>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { addTags, announce } from '@weft-js/warp-core'
|
|
2
|
+
import { derived, state, type State } from '@weft-js/core'
|
|
3
|
+
import { Field } from '../Field'
|
|
4
|
+
import { Tag } from '../Tag'
|
|
5
|
+
import { afterFlush } from './atoms'
|
|
6
|
+
import '@weft-js/warp-core/styles/inputs/choice.css'
|
|
7
|
+
|
|
8
|
+
/** Props for TagsInput. */
|
|
9
|
+
export interface TagsInputProps {
|
|
10
|
+
label: string
|
|
11
|
+
hint?: unknown
|
|
12
|
+
error?: unknown
|
|
13
|
+
value: State<string[]>
|
|
14
|
+
/** A false tag stays, critical: the field's error says why, once for all of them. */
|
|
15
|
+
valid?: (tag: string) => boolean
|
|
16
|
+
/** Called when the tags change, to clear a stale error. */
|
|
17
|
+
onEdit?: (() => void) | undefined
|
|
18
|
+
/** Enter with nothing typed: the form's submit. */
|
|
19
|
+
onSubmit?: () => void
|
|
20
|
+
/** Focuses the field when it mounts. */
|
|
21
|
+
autofocus?: boolean
|
|
22
|
+
[attr: string]: unknown
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* TagsInput (09 §4.2): Enter, comma or a pasted list adds tags; an invalid one stays as a critical tag; Backspace
|
|
27
|
+
* highlights the last tag, then removes it. warp had none, so it lives here. Changes are announced politely.
|
|
28
|
+
*/
|
|
29
|
+
export function TagsInput({ label, hint, error, value, valid, onEdit, onSubmit, ...rest }: TagsInputProps) {
|
|
30
|
+
const draft = state('')
|
|
31
|
+
const armed = state(false)
|
|
32
|
+
let host: HTMLElement | undefined
|
|
33
|
+
const say = (text: string) => { if (host) announce(host.ownerDocument, text) }
|
|
34
|
+
const add = (raw: string) => {
|
|
35
|
+
const { list, added } = addTags(value.value, raw)
|
|
36
|
+
draft.value = ''
|
|
37
|
+
if (!added.length) return
|
|
38
|
+
value.value = list
|
|
39
|
+
armed.value = false
|
|
40
|
+
say(`Added ${added.join(', ')}. ${list.length} in total.`)
|
|
41
|
+
onEdit?.()
|
|
42
|
+
}
|
|
43
|
+
const drop = (tag: string) => {
|
|
44
|
+
value.value = value.value.filter(t => t !== tag)
|
|
45
|
+
armed.value = false
|
|
46
|
+
say(`Removed ${tag}.`)
|
|
47
|
+
onEdit?.()
|
|
48
|
+
afterFlush(() => host?.focus())
|
|
49
|
+
}
|
|
50
|
+
const key = (e: KeyboardEvent) => {
|
|
51
|
+
const text = draft.value.trim()
|
|
52
|
+
if ((e.key === 'Enter' || e.key === ',') && text) { e.preventDefault(); add(text) }
|
|
53
|
+
else if (e.key === 'Enter') { e.preventDefault(); onSubmit?.() }
|
|
54
|
+
else if (e.key === 'Backspace' && !draft.value && value.value.length) {
|
|
55
|
+
if (armed.value) drop(value.value[value.value.length - 1]!)
|
|
56
|
+
else armed.value = true
|
|
57
|
+
} else if (e.key !== 'Backspace') armed.value = false
|
|
58
|
+
}
|
|
59
|
+
const paste = (e: ClipboardEvent) => {
|
|
60
|
+
const t = e.clipboardData?.getData('text') ?? ''
|
|
61
|
+
if (/[\s,;]/.test(t.trim())) { e.preventDefault(); add(`${draft.value} ${t}`) }
|
|
62
|
+
}
|
|
63
|
+
const last = derived(() => value.value[value.value.length - 1])
|
|
64
|
+
return (
|
|
65
|
+
<Field
|
|
66
|
+
wrap
|
|
67
|
+
label={label}
|
|
68
|
+
hint={hint}
|
|
69
|
+
error={error ?? ''}
|
|
70
|
+
filled={value.value.length > 0 || !!draft.value}
|
|
71
|
+
control={p => (
|
|
72
|
+
<>
|
|
73
|
+
{value.value.map(t => <Tag key={t} label={t} tone={valid && !valid(t) ? 'critical' : undefined} data-highlighted={armed.value && t === last.value ? '' : undefined} onRemove={() => drop(t)}>{t}</Tag>)}
|
|
74
|
+
<input ref={n => (host = n)} data-part="input" id={p.id} placeholder=" " autocomplete="off" enterkeyhint="enter" aria-describedby={p.describedby} aria-invalid={p.invalid ? 'true' : undefined} value={draft.value} onInput={(e: Event) => { draft.value = (e.currentTarget as HTMLInputElement).value; armed.value = false }} onKeyDown={key} onPaste={paste} onBlur={() => { if (draft.value.trim()) add(draft.value) }} {...rest} />
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Small pieces the admin pack shares. Internal: not exported from the barrel.
|
|
2
|
+
import { flush, type State, state, untrack, watch } from '@weft-js/core'
|
|
3
|
+
import { CompactSelect } from '../Select'
|
|
4
|
+
import '../fields'
|
|
5
|
+
import '@weft-js/warp-core/styles/inputs/choice.css'
|
|
6
|
+
import '@weft-js/warp-core/styles/inputs/text.css'
|
|
7
|
+
|
|
8
|
+
/** A value given plainly or as a State box. */
|
|
9
|
+
export const read = <T,>(v: T | State<T>): T => (v !== null && typeof v === 'object' && !Array.isArray(v) && 'value' in (v as object) ? (v as State<T>).value : v as T)
|
|
10
|
+
|
|
11
|
+
/** The sweep's two spans, in every swept host. */
|
|
12
|
+
export const Sweep = () => <><span data-part="sweep-base" aria-hidden="true"></span><span data-part="sweep" aria-hidden="true"></span></>
|
|
13
|
+
|
|
14
|
+
/** Runs after the DOM has caught up: a focus move onto something this handler just rendered. */
|
|
15
|
+
export const afterFlush = (fn: () => void) => queueMicrotask(() => { flush(); fn() })
|
|
16
|
+
|
|
17
|
+
/** Goes true with `open` and stays: a closed overlay carries nothing until first opened, and keeps its close animation. */
|
|
18
|
+
export function latch(open: State<boolean>) {
|
|
19
|
+
const on = state(untrack(() => open.value))
|
|
20
|
+
watch(() => open.value, v => { if (v) on.value = true })
|
|
21
|
+
return on
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A close that can refuse, for dialogs that mustn't go yet. Modal only reads and writes `.value`. */
|
|
25
|
+
export const guarded = (open: State<boolean>, may: () => boolean) => ({
|
|
26
|
+
get value() { return open.value },
|
|
27
|
+
set value(v: boolean) { if (v || may()) open.value = v },
|
|
28
|
+
}) as State<boolean>
|
|
29
|
+
|
|
30
|
+
/** The compact select in a table cell (specimen `roleSel`). */
|
|
31
|
+
export const InlineSelect = ({ label, value, options, describedby, onValueChange }: { label: string; value: string; options: { value: string; label: string }[]; describedby?: string | undefined; onValueChange: (value: string) => void }) => (
|
|
32
|
+
<CompactSelect label={label} value={value} options={options} describedby={describedby} onValueChange={onValueChange} />
|
|
33
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The admin pack: see docs/design/warp. One file per component family; each imports only its own CSS.
|
|
2
|
+
export { SettingsLayout, SettingsSection, SettingRow, type SettingsNavItem } from './Settings'
|
|
3
|
+
export { SaveBar, type SaveBarProps, type FieldErrors } from './SaveBar'
|
|
4
|
+
export { DangerZone, ConfirmDialog, TypeToConfirm, type ConfirmDialogProps, type TypeToConfirmProps } from './Danger'
|
|
5
|
+
export { MembersTable, SeatsMeter, RolesMatrix, type Member, type Invite, type Role, type Permission, type MembersTableProps } from './Members'
|
|
6
|
+
export { InviteDialog, type InviteDialogProps } from './InviteDialog'
|
|
7
|
+
export { TagsInput, type TagsInputProps } from './TagsInput'
|
|
8
|
+
export { ApiKeys, type ApiKey, type KeyDraft, type ApiKeysProps } from './ApiKeys'
|
|
9
|
+
export { PlanPicker, UsageMeter, UsageMeters, type BillingPlan, type PlanPickerProps, type UsageMeterProps } from './Plans'
|
|
10
|
+
export { Invoices, PaymentMethods, PayFrame, type Invoice, type InvoiceStatus, type PaymentMethod, type PayFrameProps } from './Billing'
|
|
11
|
+
export { AuditLog, type AuditEvent, type AuditLogProps } from './Audit'
|
|
12
|
+
export { IntegrationsGrid, Webhooks, type Integration, type IntegrationState, type IntegrationsGridProps, type Webhook, type WebhooksProps } from './Integrations'
|
|
13
|
+
export { CsvImport, type CsvImportProps } from './CsvImport'
|
|
14
|
+
export { CartDrawer, CartButton, LineItem, type CartDrawerProps } from './Cart'
|
|
15
|
+
export { Checkout, type CheckoutProps, type CheckoutStep, type ShippingOption, type Coupon, type OrderDraft, type PlaceOrderResult } from './Checkout'
|
|
16
|
+
export { OrderConfirmation, OrderTracker, type OrderConfirmationProps, type OrderStage } from './Order'
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { BranchIcon, CopyIcon, LeftIcon, RefreshIcon, RightIcon, ThumbsDownIcon, ThumbsUpIcon } from '@weft-js/warp-icons'
|
|
2
|
+
import { announce, popover } from '@weft-js/warp-core'
|
|
3
|
+
import { derived, state } from '@weft-js/core'
|
|
4
|
+
import { Badge } from '../Badge'
|
|
5
|
+
import { Button } from '../Button'
|
|
6
|
+
import { use } from '@weft-js/warp-core/weft'
|
|
7
|
+
import { IconButton } from './bits'
|
|
8
|
+
import '../styles'
|
|
9
|
+
import '@weft-js/warp-core/styles/components/popover.css'
|
|
10
|
+
import '@weft-js/warp-core/styles/app/branch.css'
|
|
11
|
+
|
|
12
|
+
const say = (e: Event, text: string) => announce((e.currentTarget as Element).ownerDocument, text)
|
|
13
|
+
|
|
14
|
+
/** BranchSwitcher (07 §1.3): `‹ 2/2 ›`, shown once a turn has more than one version. `index` counts from 0. */
|
|
15
|
+
export function BranchSwitcher({ index, count, onChange }: { index: number; count: number; onChange: (index: number) => void }) {
|
|
16
|
+
const go = (e: Event, to: number) => { onChange(to); say(e, `Version ${to + 1} of ${count}`) }
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
{count > 1 && (
|
|
20
|
+
<span data-scope="branch" role="group" aria-label="Versions">
|
|
21
|
+
<button type="button" aria-label="Previous version" disabled={index <= 0} onClick={e => go(e, index - 1)}><LeftIcon /></button>
|
|
22
|
+
<span data-numeric="">{index + 1}/{count}</span>
|
|
23
|
+
<button type="button" aria-label="Next version" disabled={index >= count - 1} onClick={e => go(e, index + 1)}><RightIcon /></button>
|
|
24
|
+
</span>
|
|
25
|
+
)}
|
|
26
|
+
</>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A reader's verdict on an answer; null until they give one. */
|
|
31
|
+
export type Feedback = 'up' | 'down' | null
|
|
32
|
+
|
|
33
|
+
/** Props for MessageActions. */
|
|
34
|
+
export interface MessageActionsProps {
|
|
35
|
+
/** Adds Copy, and is called with the pressed button. Copy the message text here. */
|
|
36
|
+
onCopy?: ((el: HTMLElement) => void) | undefined
|
|
37
|
+
/** Called with `up`, `down` or null when thumbs change, and the reason picked for a thumbs down. */
|
|
38
|
+
onFeedback?: ((value: Feedback, reason?: string) => void) | undefined
|
|
39
|
+
reasons?: string[] | undefined
|
|
40
|
+
/** Adds Regenerate: ask for a new answer. */
|
|
41
|
+
onRegenerate?: (() => void) | undefined
|
|
42
|
+
/** Adds Branch: start a new conversation from this message. */
|
|
43
|
+
onBranch?: (() => void) | undefined
|
|
44
|
+
stopped?: boolean | undefined
|
|
45
|
+
/** Which of several answers is shown, with arrows between them. */
|
|
46
|
+
branch?: ({ index: number; count: number; onChange: (index: number) => void }) | undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Message actions (07 §1.3): copy, good and bad answer (toggles; bad opens a short reason popover), regenerate, branch
|
|
51
|
+
* and the switcher. Regenerate, branch and thumbs render only when they have a handler.
|
|
52
|
+
*/
|
|
53
|
+
export function MessageActions({ onCopy, onFeedback, reasons = ['Wrong', 'Too long', 'Missed the point'], onRegenerate, onBranch, stopped, branch }: MessageActionsProps) {
|
|
54
|
+
const value = state<Feedback>(null)
|
|
55
|
+
const asking = state(false)
|
|
56
|
+
const has = derived(() => branch)
|
|
57
|
+
let bad: HTMLElement | undefined
|
|
58
|
+
let panel: HTMLElement | undefined
|
|
59
|
+
use(popover, () => panel, () => ({ open: asking.value, trigger: bad!, side: 'top' as const, onClose: () => (asking.value = false) }))
|
|
60
|
+
const rate = (e: Event, to: 'up' | 'down') => {
|
|
61
|
+
const next = value.value === to ? null : to
|
|
62
|
+
value.value = next
|
|
63
|
+
onFeedback?.(next)
|
|
64
|
+
if (next === 'down') asking.value = true
|
|
65
|
+
else if (next) say(e, 'Thanks for the feedback.')
|
|
66
|
+
}
|
|
67
|
+
const because = (e: Event, reason: string) => { onFeedback?.('down', reason); say(e, 'Thanks for the feedback.') }
|
|
68
|
+
return (
|
|
69
|
+
<div data-part="actions">
|
|
70
|
+
{stopped && <Badge tone="neutral">Stopped · kept what arrived</Badge>}
|
|
71
|
+
{onCopy && <IconButton icon={CopyIcon} label="Copy" onClick={(e: Event) => onCopy(e.currentTarget as HTMLElement)} />}
|
|
72
|
+
{onFeedback && <IconButton icon={ThumbsUpIcon} label="Good answer" aria-pressed={value.value === 'up' ? 'true' : 'false'} onClick={(e: Event) => rate(e, 'up')} />}
|
|
73
|
+
{onFeedback && <IconButton icon={ThumbsDownIcon} label="Bad answer" aria-pressed={value.value === 'down' ? 'true' : 'false'} aria-haspopup="dialog" ref={(n: HTMLElement) => (bad = n)} onClick={(e: Event) => rate(e, 'down')} />}
|
|
74
|
+
{onFeedback && (
|
|
75
|
+
<div ref={n => (panel = n)} data-scope="popover" data-part="content" role="dialog" popover="manual" aria-label="What went wrong?">
|
|
76
|
+
<p data-part="title">What went wrong?</p>
|
|
77
|
+
<div>{reasons.map(r => <Button key={r} emphasis="ghost" tone="neutral" size="sm" data-close-popover="" onClick={(e: Event) => because(e, r)}>{r}</Button>)}</div>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
{onRegenerate && <IconButton icon={RefreshIcon} label="Regenerate" onClick={onRegenerate} />}
|
|
81
|
+
{onBranch && <IconButton icon={BranchIcon} label="Branch from here" onClick={onBranch} />}
|
|
82
|
+
{has.value && <BranchSwitcher index={has.value.index} count={has.value.count} onChange={has.value.onChange} />}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CheckIcon, ClockIcon, HandIcon, XIcon } from '@weft-js/warp-icons'
|
|
2
|
+
import type { PlanStep } from '@weft-js/warp-core'
|
|
3
|
+
import { derived } from '@weft-js/core'
|
|
4
|
+
import { Bar, IconButton } from './bits'
|
|
5
|
+
import '../styles'
|
|
6
|
+
import '@weft-js/warp-core/styles/components/spinner.css'
|
|
7
|
+
import '@weft-js/warp-core/styles/app/plan.css'
|
|
8
|
+
|
|
9
|
+
/** A message waiting for the agent to finish. */
|
|
10
|
+
export type QueuedMessage = { id: string; text: string }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Plan: the task list and the Queue (07 §1.1, §1.2). Steps are pending, running, done or blocked; messages sent during
|
|
14
|
+
* a run wait in order under them and can be removed. Named AgentPlan so it can't be mistaken for a billing plan.
|
|
15
|
+
*/
|
|
16
|
+
export function AgentPlan({ steps, queue, title = 'Plan', onRemoveQueued }: { steps: PlanStep[]; queue?: QueuedMessage[]; title?: unknown; onRemoveQueued?: (id: string) => void }) {
|
|
17
|
+
const done = derived(() => steps.filter(s => s.state === 'done').length)
|
|
18
|
+
const waiting = derived(() => queue ?? [])
|
|
19
|
+
const note = (s: PlanStep) => s.note ?? (s.state === 'blocked' ? 'Waiting for your approval' : s.state === 'running' ? 'In progress' : undefined)
|
|
20
|
+
return (
|
|
21
|
+
<div data-scope="plan">
|
|
22
|
+
<div data-part="header"><h4>{title}</h4><small data-numeric="">{done.value}/{steps.length}</small></div>
|
|
23
|
+
<Bar label="Plan progress" value={steps.length ? done.value / steps.length : 0} />
|
|
24
|
+
<ol aria-label="Plan steps">
|
|
25
|
+
{steps.map(s => (
|
|
26
|
+
<li key={s.id} data-state={s.state}>
|
|
27
|
+
<span data-part="box">
|
|
28
|
+
{s.state === 'done' && <CheckIcon style="inline-size:12px;block-size:12px;stroke-width:2.2" />}
|
|
29
|
+
{s.state === 'running' && <span data-scope="spinner" style="--_size:10px;color:var(--warp-color-accent-text)"></span>}
|
|
30
|
+
{s.state === 'blocked' && <HandIcon style="inline-size:11px;block-size:11px" />}
|
|
31
|
+
</span>
|
|
32
|
+
<span>{s.title}{note(s) !== undefined && <small>{note(s)}</small>}</span>
|
|
33
|
+
</li>
|
|
34
|
+
))}
|
|
35
|
+
</ol>
|
|
36
|
+
<div data-part="queue" role="group" aria-label="Queued messages">
|
|
37
|
+
<span data-part="eyebrow">Queue · {waiting.value.length}</span>
|
|
38
|
+
{waiting.value.length === 0 && <small>Messages you send while a run is busy wait here, in order.</small>}
|
|
39
|
+
{waiting.value.map(q => (
|
|
40
|
+
<div key={q.id}>
|
|
41
|
+
<ClockIcon style="inline-size:14px;block-size:14px;color:var(--warp-color-text-subtle)" />
|
|
42
|
+
<span>{q.text}</span>
|
|
43
|
+
<IconButton icon={XIcon} label={`Remove from queue: ${q.text}`} onClick={() => onRemoveQueued?.(q.id)} />
|
|
44
|
+
</div>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { CopyIcon, ExternalIcon } from '@weft-js/warp-icons'
|
|
2
|
+
import { announce, copyConfirm, indicator, nextIndex, uid } from '@weft-js/warp-core'
|
|
3
|
+
import { derived, state, untrack, type State } from '@weft-js/core'
|
|
4
|
+
import { Badge } from '../Badge'
|
|
5
|
+
import { use } from '@weft-js/warp-core/weft'
|
|
6
|
+
import { IconButton, held, Sweep, useSweep } from './bits'
|
|
7
|
+
import '../styles'
|
|
8
|
+
import '@weft-js/warp-core/styles/components/tabs.css'
|
|
9
|
+
import '@weft-js/warp-core/styles/app/artifact.css'
|
|
10
|
+
import '@weft-js/warp-core/styles/app/versions.css'
|
|
11
|
+
|
|
12
|
+
/** One saved version of an artifact. */
|
|
13
|
+
export type ArtifactVersion = { id: string; label: string; meta?: string | undefined; code?: string; named?: boolean }
|
|
14
|
+
const VIEWS = ['Preview', 'Code', 'Versions'] as const
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ArtifactPanel (07 §6.4): title, version badge, Copy; APG tabs Preview, Code, Versions. Versions is a listbox with a
|
|
18
|
+
* roving tab stop, selection following focus; picking one is `onVersion`, and Code shows that version's code. Copy
|
|
19
|
+
* confirms the panel's sweep. It fills its container, so a rail, a Sheet or a full route all hold it.
|
|
20
|
+
*/
|
|
21
|
+
export function ArtifactPanel({ title, versions, version, onVersion, code, onOpen, children }: { title: unknown; versions: ArtifactVersion[]; version?: string | State<string | undefined> | undefined; onVersion?: (id: string) => void; code?: string; onOpen?: () => void; children?: unknown }) {
|
|
22
|
+
const id = uid('art')
|
|
23
|
+
const view = state<(typeof VIEWS)[number]>('Preview')
|
|
24
|
+
const picked = held<string | undefined>(() => version, untrack(() => versions[0]?.id), () => (v: string | undefined) => onVersion?.(v!))
|
|
25
|
+
const current = derived(() => versions.find(v => v.id === picked.value) ?? versions[0])
|
|
26
|
+
const source = derived(() => current.value?.code ?? code ?? '')
|
|
27
|
+
const badge = derived(() => current.value?.label.split(' ·')[0])
|
|
28
|
+
let root: HTMLElement | undefined
|
|
29
|
+
let list: HTMLElement | undefined
|
|
30
|
+
useSweep()
|
|
31
|
+
use(indicator, () => list, () => ({ selected: '[aria-selected="true"]', x: '--_ix', w: '--_iw', at: view.value }))
|
|
32
|
+
const go = (v: (typeof VIEWS)[number]) => (view.value = v)
|
|
33
|
+
const onTab = (e: KeyboardEvent) => {
|
|
34
|
+
const to = nextIndex(e.key, VIEWS.indexOf(view.value), VIEWS.length, { rtl: getComputedStyle(e.currentTarget as Element).direction === 'rtl' })
|
|
35
|
+
if (to < 0) return
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
go(VIEWS[to]!)
|
|
38
|
+
list!.querySelectorAll<HTMLElement>('[role="tab"]')[to]?.focus()
|
|
39
|
+
}
|
|
40
|
+
const choose = (v: ArtifactVersion) => {
|
|
41
|
+
picked.set(v.id)
|
|
42
|
+
announce(root!.ownerDocument, `Previewing ${v.label}`)
|
|
43
|
+
queueMicrotask(() => root!.querySelectorAll<HTMLElement>('[role="option"]')[versions.findIndex(x => x.id === v.id)]?.focus())
|
|
44
|
+
}
|
|
45
|
+
const onList = (e: KeyboardEvent) => {
|
|
46
|
+
const at = versions.findIndex(v => v.id === current.value?.id)
|
|
47
|
+
const to = nextIndex(e.key, at, versions.length, { orientation: 'vertical' })
|
|
48
|
+
if (to < 0) return
|
|
49
|
+
e.preventDefault()
|
|
50
|
+
choose(versions[to]!)
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
<section ref={n => (root = n)} data-scope="artifact" data-sweep="" aria-labelledby={`${id}-n`}>
|
|
54
|
+
<Sweep />
|
|
55
|
+
<div data-part="head">
|
|
56
|
+
<b id={`${id}-n`}>{title}</b>
|
|
57
|
+
{badge.value !== undefined && <Badge tone="neutral">{badge.value}</Badge>}
|
|
58
|
+
<IconButton icon={CopyIcon} label="Copy" onClick={(e: Event) => copyConfirm(e.currentTarget as Element, source.value, root)} />
|
|
59
|
+
{onOpen && <IconButton icon={ExternalIcon} label="Open in editor" onClick={onOpen} />}
|
|
60
|
+
</div>
|
|
61
|
+
<div data-scope="tabs" data-part="root" style="padding:8px 12px 0;gap:0">
|
|
62
|
+
<div ref={n => (list = n)} data-part="list" role="tablist" aria-label="Artifact views" onKeyDown={onTab}>
|
|
63
|
+
{VIEWS.map((v, i) => <button key={v} data-part="trigger" role="tab" type="button" id={`${id}-t${i}`} aria-controls={`${id}-p${i}`} aria-selected={v === view.value ? 'true' : 'false'} tabindex={v === view.value ? undefined : '-1'} onClick={() => go(v)}>{v}</button>)}
|
|
64
|
+
<span data-part="indicator" aria-hidden="true" data-instant=""></span>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div data-part="panel" role="tabpanel" id={`${id}-p0`} aria-labelledby={`${id}-t0`} tabindex="0" hidden={view.value !== 'Preview'}><div data-part="preview">{children}</div></div>
|
|
68
|
+
<div data-part="panel" role="tabpanel" id={`${id}-p1`} aria-labelledby={`${id}-t1`} tabindex="0" hidden={view.value !== 'Code'}><pre>{source.value}</pre></div>
|
|
69
|
+
<div data-part="panel" role="tabpanel" id={`${id}-p2`} aria-labelledby={`${id}-t2`} hidden={view.value !== 'Versions'}>
|
|
70
|
+
<ol data-scope="versions" role="listbox" aria-label="Versions" onKeyDown={onList}>
|
|
71
|
+
{versions.map(v => (
|
|
72
|
+
<li key={v.id} id={`${id}-v-${v.id}`} role="option" aria-selected={v.id === current.value?.id ? 'true' : 'false'} aria-current={v.id === current.value?.id ? 'true' : undefined} tabindex={v.id === current.value?.id ? '0' : '-1'} data-named={v.named ?? v.id === current.value?.id ? '' : undefined} onClick={() => choose(v)}>
|
|
73
|
+
<i></i><span>{v.label}{v.meta !== undefined && <small>{v.meta}</small>}</span>
|
|
74
|
+
</li>
|
|
75
|
+
))}
|
|
76
|
+
</ol>
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { SparkleIcon } from '@weft-js/warp-icons'
|
|
2
|
+
import { announce, answerText, copyConfirm, uid, type AgentTool, type Answer, type AnswerBlock } from '@weft-js/warp-core'
|
|
3
|
+
import { derived, onMount, untrack, watch } from '@weft-js/core'
|
|
4
|
+
import { Alert } from '../Alert'
|
|
5
|
+
import { Button } from '../Button'
|
|
6
|
+
import { MessageActions, type Feedback } from './Actions'
|
|
7
|
+
import { Prose } from './Prose'
|
|
8
|
+
import { Question } from './Question'
|
|
9
|
+
import { Reasoning } from './Reasoning'
|
|
10
|
+
import { Sources } from './Sources'
|
|
11
|
+
import { ToolCall } from './ToolCall'
|
|
12
|
+
import '../styles'
|
|
13
|
+
import '@weft-js/warp-core/styles/app/assistant.css'
|
|
14
|
+
|
|
15
|
+
/** Props for Assistant. */
|
|
16
|
+
export interface AssistantProps {
|
|
17
|
+
/** The folded value: `foldAnswer(events)`, or what a stream folds to. */
|
|
18
|
+
answer: Answer
|
|
19
|
+
/** The turn number, for its name: "Assistant answer 2". */
|
|
20
|
+
n?: number
|
|
21
|
+
arrive?: boolean
|
|
22
|
+
/** Was there on load: approvals and questions take no focus and announce nothing. */
|
|
23
|
+
quiet?: boolean | undefined
|
|
24
|
+
/** Render a block of answer text yourself (Markdown, say); the default is `Prose`. */
|
|
25
|
+
render?: (text: string, answer: Answer) => unknown
|
|
26
|
+
/** Called when a tool call is approved, with its arguments as edited and whether to always allow it. */
|
|
27
|
+
onApprove?: (tool: AgentTool, args: string, always: boolean) => void
|
|
28
|
+
/** Called when a tool call is denied. */
|
|
29
|
+
onDeny?: (tool: AgentTool) => void
|
|
30
|
+
alwaysAllow?: (tool: AgentTool) => string | undefined
|
|
31
|
+
/** Called with a question's id and the answer picked or typed. */
|
|
32
|
+
onAnswer?: (id: string, answer: string | string[]) => void
|
|
33
|
+
/** Called with `up`, `down` or null when thumbs change, and the reason picked for a thumbs down. */
|
|
34
|
+
onFeedback?: (value: Feedback, reason?: string) => void
|
|
35
|
+
/** Adds Regenerate: ask for a new answer. */
|
|
36
|
+
onRegenerate?: () => void
|
|
37
|
+
/** Adds Branch: start a new conversation from this answer. */
|
|
38
|
+
onBranch?: () => void
|
|
39
|
+
/** Which of several answers is shown, with arrows between them. */
|
|
40
|
+
branch?: { index: number; count: number; onChange: (index: number) => void }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Thought = Extract<AnswerBlock, { kind: 'reasoning' }>
|
|
44
|
+
type Said = Extract<AnswerBlock, { kind: 'text' }>
|
|
45
|
+
type Asked = Extract<AnswerBlock, { kind: 'question' }>
|
|
46
|
+
|
|
47
|
+
function Block({ block, base, p }: { block: AnswerBlock; base: string; p: AssistantProps }) {
|
|
48
|
+
const tool = derived(() => p.answer.tools.find(t => t.id === block.id))
|
|
49
|
+
const kind = derived(() => block.kind)
|
|
50
|
+
// once per kind: a row's nodes are read again whenever the list changes
|
|
51
|
+
const view = derived(() => {
|
|
52
|
+
const k = kind.value
|
|
53
|
+
return untrack(() => {
|
|
54
|
+
if (k === 'reasoning') return <Reasoning text={(block as Thought).text} streaming={(block as Thought).streaming} seconds={(block as Thought).seconds} />
|
|
55
|
+
if (k === 'text') return p.render ? <>{p.render((block as Said).text, p.answer)}</> : <Prose text={(block as Said).text} sources={p.answer.sources} base={base} />
|
|
56
|
+
if (k === 'question') return <Question question={(block as Asked).question} quiet={p.quiet} onAnswer={a => p.onAnswer?.(block.id, a)} />
|
|
57
|
+
return <ToolCall call={tool.value!} quiet={p.quiet} alwaysAllow={p.alwaysAllow?.(tool.value!)} onApprove={(a, al) => p.onApprove?.(tool.value!, a, al)} onDeny={() => p.onDeny?.(tool.value!)} />
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
return <>{view.value}</>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The assistant's turn (07 §1.1, §1.2): renders the folded Answer only, so replay and hydration take the same path.
|
|
65
|
+
* Caret while streaming, aria-busy until done; the end of a turn is announced once, never each token.
|
|
66
|
+
*/
|
|
67
|
+
export function Assistant(p: AssistantProps) {
|
|
68
|
+
const base = uid('ans')
|
|
69
|
+
const st = derived(() => p.answer.state)
|
|
70
|
+
const busy = derived(() => st.value === 'waiting' || st.value === 'streaming' || st.value === 'approval')
|
|
71
|
+
const empty = derived(() => p.answer.blocks.length === 0 && st.value !== 'error')
|
|
72
|
+
const blocks = derived(() => p.answer.blocks)
|
|
73
|
+
const many = derived(() => p.answer.sources.length > 0)
|
|
74
|
+
let root: HTMLElement | undefined
|
|
75
|
+
onMount(() => {
|
|
76
|
+
watch(() => st.value, (s, prev) => {
|
|
77
|
+
if (prev === undefined || !root) return
|
|
78
|
+
const doc = root.ownerDocument
|
|
79
|
+
if (s === 'done') announce(doc, 'Answer ready')
|
|
80
|
+
if (s === 'stopped') announce(doc, 'Stopped')
|
|
81
|
+
if (s === 'error') announce(doc, 'The answer failed', true)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
return (
|
|
85
|
+
<article ref={n => (root = n)} data-scope="assistant" data-state={st.value} aria-label={p.n !== undefined ? `Assistant answer ${p.n}` : 'Assistant answer'} aria-busy={busy.value ? 'true' : undefined} data-arrive={p.arrive ? '' : undefined}>
|
|
86
|
+
<span data-part="mark"><SparkleIcon /></span>
|
|
87
|
+
<div data-part="content">
|
|
88
|
+
{empty.value && <p></p>}
|
|
89
|
+
{blocks.value.map(b => <Block key={b.id} block={b} base={base} p={p} />)}
|
|
90
|
+
{st.value === 'error' && <Alert tone="critical" title="The answer failed">{p.answer.error}{p.onRegenerate && <> <Button emphasis="link" tone="neutral" onClick={p.onRegenerate}>Retry</Button></>}</Alert>}
|
|
91
|
+
{many.value && <Sources items={p.answer.sources} base={base} />}
|
|
92
|
+
</div>
|
|
93
|
+
{!busy.value && <MessageActions stopped={st.value === 'stopped'} onCopy={el => void copyConfirm(el, answerText(untrack(() => p.answer)))} onFeedback={p.onFeedback} onRegenerate={p.onRegenerate} onBranch={p.onBranch} branch={p.branch} />}
|
|
94
|
+
</article>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { UndoIcon } from '@weft-js/warp-icons'
|
|
2
|
+
import { Button } from '../Button'
|
|
3
|
+
import '../styles'
|
|
4
|
+
import '@weft-js/warp-core/styles/app/checkpoint.css'
|
|
5
|
+
|
|
6
|
+
/** Checkpoint (07 §1.3): a hairline with a label and Restore. Restoring forks a branch; `onRestore` decides how. */
|
|
7
|
+
export function Checkpoint({ label, onRestore }: { label: unknown; onRestore?: () => void }) {
|
|
8
|
+
return (
|
|
9
|
+
<div data-scope="checkpoint">
|
|
10
|
+
<span>{label}</span>
|
|
11
|
+
{onRestore && <Button emphasis="ghost" tone="neutral" size="sm" onClick={onRestore}><UndoIcon />Restore</Button>}
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { citePreview, installSweep, type AgentSource } from '@weft-js/warp-core'
|
|
2
|
+
import { state } from '@weft-js/core'
|
|
3
|
+
import { use } from '@weft-js/warp-core/weft'
|
|
4
|
+
import '../styles'
|
|
5
|
+
import '@weft-js/warp-core/styles/components/popover.css'
|
|
6
|
+
import '@weft-js/warp-core/styles/app/cite.css'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A citation (07 §1.3, §1.6): a link to source n named "Source n", with a preview on hover and focus. The preview sits
|
|
10
|
+
* next in the DOM, so Tab reaches its link. Pressing moves focus to the source card and confirms it.
|
|
11
|
+
*/
|
|
12
|
+
export function Cite({ n, source, href }: { n: number; source?: AgentSource | undefined; href: string }) {
|
|
13
|
+
const open = state(false)
|
|
14
|
+
let link: HTMLAnchorElement | undefined
|
|
15
|
+
let panel: HTMLElement | undefined
|
|
16
|
+
use(citePreview, () => (source ? link : undefined), () => ({ panel: panel!, onOpen: (o: boolean) => (open.value = o) }))
|
|
17
|
+
const jump = (e: MouseEvent) => {
|
|
18
|
+
const a = e.currentTarget as Element
|
|
19
|
+
const doc = a.ownerDocument
|
|
20
|
+
const card = doc.getElementById(a.getAttribute('href')!.slice(1)) // the page's id: a hydrated server id isn't this side's
|
|
21
|
+
if (!card) return
|
|
22
|
+
e.preventDefault()
|
|
23
|
+
card.focus()
|
|
24
|
+
installSweep(doc).confirm(card)
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
<span style="display:contents">
|
|
28
|
+
<a ref={el => (link = el)} href={href} data-scope="cite" aria-label={`Source ${n}`} aria-expanded={source ? (open.value ? 'true' : 'false') : undefined} onClick={jump}>{n}</a>
|
|
29
|
+
{source && (
|
|
30
|
+
<span ref={el => (panel = el)} data-scope="popover" data-part="content" popover="manual">
|
|
31
|
+
<span data-part="title" style="display:block">{source.title}</span>
|
|
32
|
+
{source.path !== undefined && <span data-part="description" style="display:block">{source.path}</span>}
|
|
33
|
+
{source.snippet !== undefined && <span>{source.snippet}</span>}
|
|
34
|
+
{source.url !== undefined && <a href={source.url} data-part="open">Open source</a>}
|
|
35
|
+
</span>
|
|
36
|
+
)}
|
|
37
|
+
</span>
|
|
38
|
+
)
|
|
39
|
+
}
|