@calliopelabs/ui 0.14.0 → 0.18.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/README.md +235 -1
- package/dist/agent/AgentChat.d.ts +8 -1
- package/dist/agent/AgentChat.js +43 -2
- package/dist/agent/WorkflowRenderers.d.ts +61 -0
- package/dist/agent/WorkflowRenderers.js +102 -0
- package/dist/agent/controller.d.ts +19 -0
- package/dist/agent/controller.js +33 -0
- package/dist/agent/copilotkit/CopilotKitAgentProvider.js +2 -2
- package/dist/agent/index.d.ts +4 -2
- package/dist/agent/index.js +3 -1
- package/dist/agent/mock.d.ts +26 -4
- package/dist/agent/mock.js +68 -37
- package/dist/agent/renderers.d.ts +49 -0
- package/dist/agent/renderers.js +27 -0
- package/dist/appearance/brands.d.ts +17 -0
- package/dist/appearance/brands.js +14 -0
- package/dist/chat/MessageBubble.js +1 -1
- package/dist/chat/MessageList.d.ts +9 -2
- package/dist/chat/MessageList.js +11 -8
- package/dist/css/appearance.js +1 -1
- package/dist/css/calliope.css +5 -0
- package/dist/css/tokens.css +5 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +8 -1
- package/dist/primitives/Badge.d.ts +14 -0
- package/dist/primitives/Badge.js +25 -0
- package/dist/primitives/ContextMenu.d.ts +18 -0
- package/dist/primitives/ContextMenu.js +34 -0
- package/dist/primitives/Menubar.d.ts +21 -0
- package/dist/primitives/Menubar.js +43 -0
- package/dist/primitives/ScrollArea.d.ts +16 -0
- package/dist/primitives/ScrollArea.js +11 -0
- package/dist/primitives/Toast.d.ts +37 -0
- package/dist/primitives/Toast.js +43 -0
- package/dist/primitives/Tooltip.d.ts +16 -0
- package/dist/primitives/Tooltip.js +26 -0
- package/dist/provider/messages.d.ts +217 -0
- package/dist/provider/messages.js +238 -0
- package/dist/shell/RailPanel.js +1 -1
- package/dist/theme/css-layer.js +2 -1
- package/dist/theme/tokens.generated.d.ts +3 -0
- package/dist/theme/tokens.generated.js +3 -0
- package/dist/workbench/ActivityBar.d.ts +37 -0
- package/dist/workbench/ActivityBar.js +57 -0
- package/dist/workbench/BottomPanel.d.ts +30 -0
- package/dist/workbench/BottomPanel.js +20 -0
- package/dist/workbench/CodeEditor.d.ts +48 -0
- package/dist/workbench/CodeEditor.js +153 -0
- package/dist/workbench/CommandPalette.d.ts +49 -0
- package/dist/workbench/CommandPalette.js +114 -0
- package/dist/workbench/DataChart.d.ts +45 -0
- package/dist/workbench/DataChart.js +146 -0
- package/dist/workbench/DataGrid.d.ts +89 -0
- package/dist/workbench/DataGrid.js +590 -0
- package/dist/workbench/DataProfile.d.ts +17 -0
- package/dist/workbench/DataProfile.js +51 -0
- package/dist/workbench/EditorTabs.d.ts +62 -0
- package/dist/workbench/EditorTabs.js +115 -0
- package/dist/workbench/FileBrowser.d.ts +62 -0
- package/dist/workbench/FileBrowser.js +117 -0
- package/dist/workbench/FileGrid.d.ts +13 -0
- package/dist/workbench/FileGrid.js +114 -0
- package/dist/workbench/FileIcon.d.ts +10 -0
- package/dist/workbench/FileIcon.js +24 -0
- package/dist/workbench/FileList.d.ts +35 -0
- package/dist/workbench/FileList.js +160 -0
- package/dist/workbench/FilePreview.d.ts +66 -0
- package/dist/workbench/FilePreview.js +108 -0
- package/dist/workbench/FileTree.d.ts +37 -0
- package/dist/workbench/FileTree.js +218 -0
- package/dist/workbench/GridCell.d.ts +19 -0
- package/dist/workbench/GridCell.js +50 -0
- package/dist/workbench/GridDetails.d.ts +31 -0
- package/dist/workbench/GridDetails.js +78 -0
- package/dist/workbench/MenuBar.d.ts +49 -0
- package/dist/workbench/MenuBar.js +25 -0
- package/dist/workbench/PaneGroup.d.ts +50 -0
- package/dist/workbench/PaneGroup.js +146 -0
- package/dist/workbench/PendingChangesBar.d.ts +20 -0
- package/dist/workbench/PendingChangesBar.js +19 -0
- package/dist/workbench/Shortcuts.d.ts +50 -0
- package/dist/workbench/Shortcuts.js +109 -0
- package/dist/workbench/SqlEditor.d.ts +21 -0
- package/dist/workbench/SqlEditor.js +79 -0
- package/dist/workbench/StatusBar.d.ts +28 -0
- package/dist/workbench/StatusBar.js +26 -0
- package/dist/workbench/TruncationBanner.d.ts +11 -0
- package/dist/workbench/TruncationBanner.js +12 -0
- package/dist/workbench/UploadDropzone.d.ts +52 -0
- package/dist/workbench/UploadDropzone.js +115 -0
- package/dist/workbench/WorkbenchShell.d.ts +36 -0
- package/dist/workbench/WorkbenchShell.js +20 -0
- package/dist/workbench/dnd.d.ts +17 -0
- package/dist/workbench/dnd.js +77 -0
- package/dist/workbench/download.d.ts +11 -0
- package/dist/workbench/download.js +28 -0
- package/dist/workbench/editor-theme.d.ts +42 -0
- package/dist/workbench/editor-theme.js +113 -0
- package/dist/workbench/grid-format.d.ts +59 -0
- package/dist/workbench/grid-format.js +169 -0
- package/dist/workbench/grid-model.d.ts +75 -0
- package/dist/workbench/grid-model.js +259 -0
- package/dist/workbench/grid-selection.d.ts +69 -0
- package/dist/workbench/grid-selection.js +183 -0
- package/dist/workbench/grid-stats.d.ts +58 -0
- package/dist/workbench/grid-stats.js +185 -0
- package/dist/workbench/index.d.ts +44 -0
- package/dist/workbench/index.js +47 -0
- package/dist/workbench/keys.d.ts +23 -0
- package/dist/workbench/keys.js +105 -0
- package/dist/workbench/model.d.ts +72 -0
- package/dist/workbench/model.js +214 -0
- package/dist/workbench/palette.d.ts +17 -0
- package/dist/workbench/palette.js +45 -0
- package/dist/workbench/pending.d.ts +92 -0
- package/dist/workbench/pending.js +207 -0
- package/dist/workbench/selection.d.ts +16 -0
- package/dist/workbench/selection.js +46 -0
- package/dist/workbench/sql-completion.d.ts +107 -0
- package/dist/workbench/sql-completion.js +475 -0
- package/dist/workbench/typeahead.d.ts +15 -0
- package/dist/workbench/typeahead.js +22 -0
- package/dist/workflow/WorkflowRuntimeControls.d.ts +21 -0
- package/dist/workflow/WorkflowRuntimeControls.js +54 -0
- package/dist/workflow/runtime-controls.d.ts +29 -0
- package/dist/workflow/runtime-controls.js +10 -0
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ It is built Storybook first: every component and every screen pattern is built a
|
|
|
16
16
|
npm i @calliopelabs/ui
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Peer dependencies: `react` and `react-dom` 19, and `tailwindcss` 4 for styling.
|
|
19
|
+
Peer dependencies: `react` and `react-dom` 19, and `tailwindcss` 4 for styling. The opt-in agent entries add `@ag-ui/client` and `@copilotkit/react-core` (see [Agent](#agent)).
|
|
20
20
|
|
|
21
21
|
## Styles
|
|
22
22
|
|
|
@@ -428,6 +428,240 @@ const chat = useAgUiRun();
|
|
|
428
428
|
|
|
429
429
|
`agent` above is the host's own client. The catalog's `Chat/ChatPanel` composes the pieces the same way, driven by a recorded run. Their copy is in the `chat` message group.
|
|
430
430
|
|
|
431
|
+
## Agent
|
|
432
|
+
|
|
433
|
+
`CalliopeAgent` is the one embedded agent for every Calliope AI product: a floating bubble, the shell's right rail, inline in a page, or a page of its own. It speaks [AG-UI](https://docs.ag-ui.com) to any agent, directly or through a [CopilotKit](https://docs.copilotkit.ai) runtime, and draws every pixel with the chat kit above, on the product's tokens, mark and runtime brand. CopilotKit wires the runtime and runs the agent; its UI and stylesheet are never loaded.
|
|
434
|
+
|
|
435
|
+
It is an opt-in entry, so apps without an agent never load its dependencies:
|
|
436
|
+
|
|
437
|
+
```sh
|
|
438
|
+
npm i @ag-ui/client # @calliopelabs/ui/agent
|
|
439
|
+
npm i @ag-ui/client @copilotkit/react-core # @calliopelabs/ui/agent/copilotkit (1.79.1 or later, 1.x)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Both are optional peers. `npm run pack:check` fails if the main entry ever reaches them. Most of the entry's weight is `@ag-ui/client` itself (with its protobuf codec, about 150 KB gzipped; the agent's own code is about 24 KB minified), which a CopilotKit app already ships. To keep it off a page's first load, load the provider when the person first opens the assistant (`next/dynamic` with `ssr: false`) and keep it mounted after.
|
|
443
|
+
|
|
444
|
+
### A Next.js app with a CopilotKit runtime
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
// app/api/copilotkit/route.ts: the runtime, on the server (agents, keys and auth stay here)
|
|
448
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
449
|
+
import { CopilotRuntime, createCopilotRuntimeHandler } from "@copilotkit/runtime/v2";
|
|
450
|
+
|
|
451
|
+
const runtime = new CopilotRuntime({
|
|
452
|
+
agents: { default: new HttpAgent({ url: process.env.AGENT_URL! }) },
|
|
453
|
+
});
|
|
454
|
+
const handler = createCopilotRuntimeHandler({
|
|
455
|
+
runtime,
|
|
456
|
+
basePath: "/api/copilotkit",
|
|
457
|
+
mode: "single-route",
|
|
458
|
+
});
|
|
459
|
+
export const POST = (request: Request) => handler(request);
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
```tsx
|
|
463
|
+
// app/assistant.tsx
|
|
464
|
+
"use client";
|
|
465
|
+
import { CalliopeAgent, useAgentReadable, useAgentTool } from "@calliopelabs/ui/agent";
|
|
466
|
+
import { CopilotKitAgentProvider } from "@calliopelabs/ui/agent/copilotkit";
|
|
467
|
+
import { z } from "zod";
|
|
468
|
+
|
|
469
|
+
export function Assistant({ children, project }: { children: React.ReactNode; project: string }) {
|
|
470
|
+
return (
|
|
471
|
+
<CopilotKitAgentProvider
|
|
472
|
+
runtimeUrl="/api/copilotkit"
|
|
473
|
+
useSingleEndpoint
|
|
474
|
+
headers={{ "x-project": project }}
|
|
475
|
+
>
|
|
476
|
+
{children}
|
|
477
|
+
<CalliopeAgent mode="bubble" persistKey="app.assistant.open" />
|
|
478
|
+
</CopilotKitAgentProvider>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Anywhere under it, while mounted:
|
|
483
|
+
useAgentReadable("The open project", { project });
|
|
484
|
+
useAgentTool({
|
|
485
|
+
name: "open_scene",
|
|
486
|
+
parameters: z.object({ id: z.string() }),
|
|
487
|
+
handler: ({ id }) => router.push(`/scenes/${id}`),
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Mount it from the root layout (a Server Component) as `<Assistant project={slug}>{children}</Assistant>`. Tools carry functions, so they are registered in client components, never passed from a server one.
|
|
492
|
+
|
|
493
|
+
### A plain AG-UI endpoint
|
|
494
|
+
|
|
495
|
+
```tsx
|
|
496
|
+
"use client";
|
|
497
|
+
import { CalliopeAgent, CalliopeAgentProvider } from "@calliopelabs/ui/agent";
|
|
498
|
+
|
|
499
|
+
<CalliopeAgentProvider url="/api/agui" headers={{ "x-project": slug }} suggestions={startingPoints}>
|
|
500
|
+
<AppShell
|
|
501
|
+
{...shell}
|
|
502
|
+
rightRail={<CalliopeAgent mode="rail" open={open} onOpenChange={setOpen} />}
|
|
503
|
+
rightRailLabel="Assistant"
|
|
504
|
+
onToggleRightRail={() => setOpen((o) => !o)}
|
|
505
|
+
>
|
|
506
|
+
{children}
|
|
507
|
+
</AppShell>
|
|
508
|
+
</CalliopeAgentProvider>;
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
`url` makes an `HttpAgent` (same origin, so the session cookie goes along); `agent` takes any `AbstractAgent` instead. `createFunctionAgent(run)` turns a function that yields AG-UI events (or an existing non-AG-UI endpoint, adapted) into one.
|
|
512
|
+
|
|
513
|
+
### What it does
|
|
514
|
+
|
|
515
|
+
- **One conversation, one store.** `useCalliopeAgent()` returns the chat kit's `ChatView` (messages, tool calls, steps, interrupts, shared state, custom events) and the actions: `send(text, { parts })`, `stop()` (aborts the run and any tool waiting), `regenerate()`, `approve(call)` and `deny(call, reason)`, `resolveInterrupt(id, response)`, `respond(toolCallId, result)`, `getState()` and `setState(state)`, `switchThread(id)` (with the provider's `loadThread`) and `newThread()`, plus `running`, `waiting`, `unread` and `suggestions`.
|
|
516
|
+
- **Tools.** `useAgentTool(defineTool({ name, description, parameters, handler, render, followUp }))` offers a browser tool while the component is mounted. `parameters` is a Standard Schema (zod 4, valibot, arktype), which types and validates the arguments, or a JSON Schema. A tool with `render` and no `handler` is human-in-the-loop: the run waits until its card calls `respond(result)`, and the composer holds meanwhile.
|
|
517
|
+
- **Readables and suggestions.** `useAgentReadable(description, value)` sends page context with every run; `useAgentSuggestions([{ title, message }])` offers starting points. Both also come as provider props for the whole app.
|
|
518
|
+
- **Approvals.** A run that ends on an AG-UI interrupt pauses. A tool call it names gets approve and deny on its card (a denial always with a reason); any other interrupt gets `InterruptCard`, or the host's `renderers.interrupts[reason]`. The run resumes with AG-UI `resume` once every open interrupt has an answer; a new message cancels what is still open.
|
|
519
|
+
- **Generative UI.** `renderers` on `CalliopeAgent` maps tool names (backend tools too), `CUSTOM` event names and interrupt reasons to host components, and draws the shared state above the conversation. `workflowRenderers` draws the run and review components: `calliope.run` on `RunTimeline` (events with one `id` update one card), `calliope.artifact` on `ArtifactCard` with `DocumentReview`, `MediaReview` or `FormReview` (signing or sending the review resumes the run), and `calliope.question` interrupts on `WorkflowQuestionCard`.
|
|
520
|
+
- **Surfaces.** `mode="bubble"` (a launcher and a non-modal panel; a drawer on phones), `"rail"` (a `RailPanel` for `AppShell`'s `rightRail`; the shell's drawer on phones), `"inline"` and `"page"`. Open state is controlled, held, or remembered per person with `persistKey`; `shortcut` (`mod+/` by default) toggles it; answers that land while it is closed show as unread on the launcher. Slots: `renderContent` (the host's markdown), `attachments` and `sendOptions` (attachments as AG-UI input parts), `composerActions`, `modelPicker` (pass the choice in the provider's `forwardedProps`), `headerActions`, `empty`.
|
|
521
|
+
- **Telemetry and errors.** `onEvent(event)` sees every AG-UI event; `onError(error)` every failed run (also shown in the conversation with retry).
|
|
522
|
+
- **Testing.** `createMockAgent({ turns })` plays recorded turns; `createFunctionAgent(run)` makes an agent of any function that yields AG-UI events. Both are real `HttpAgent`s answered in the page, so stories and tests need no network.
|
|
523
|
+
|
|
524
|
+
Headers on the provider run in the browser: never put a secret there. Keys, the model and authorization stay on the server, in the runtime route or the agent. Copy is in the `agent` message group.
|
|
525
|
+
|
|
526
|
+
### Moving to it
|
|
527
|
+
|
|
528
|
+
| Product | Today | With `CalliopeAgent` |
|
|
529
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
530
|
+
| Astrolift Studio | `CopilotKitProvider` (1.79.1) and its own `AssistantPanel`, `AssistantTranscript`, `AssistantInput`, `SuggestionChips`, `AssistantLauncher`, `dock.tsx`, `useStored` open state | `CopilotKitAgentProvider runtimeUrl="/api/assistant" useSingleEndpoint agentId="studio" headers={{ [PROJECT_HEADER]: slug }}` (or keep its `CopilotKitProvider`: ours reuses it) and `<CalliopeAgent mode="rail" persistKey={openKey}>` in the shell's rail. The 15 `useHumanInTheLoop` actions become `useAgentTool({ name, parameters, render })` with `ActionCard` as `render` (calling `respond`); the whiteboard tools become `useAgentTool` with their zod schemas; `useAgentContext` readables become `useAgentReadable`; `startingPoints` becomes `useAgentSuggestions`; thread loading becomes `loadThread`. The server route and runner stay. |
|
|
531
|
+
| Zentinelle | `ChatBubble` (sparkle button, `Drawer`), `use-chat` parsing a custom `data:` stream, `MessageList`, `ChatInput`, `ModelSelector`, full-page `AssistantScreen` | `<CalliopeAgent mode="bubble">` and `mode="page"`, with `modelPicker` fed from `/assistant/providers` and the model in `forwardedProps`. Transport: port `/assistant/chat` to AG-UI (`pending_action` as an interrupt, `navigation` as a `CUSTOM` event drawn as a link chip), or until then adapt it with `createFunctionAgent` over `authenticatedFetch`. An approval resolves the interrupt; the host's `execute-tool` call with the approval token stays a host action in the interrupt renderer, gated on `canMutate`. |
|
|
532
|
+
| Astrolift app (Wayfinding) | `WayfindingBubble` and `use-wayfinding` posting to `/wayfinding/ask/` | `<CalliopeAgent mode="bubble">` over `createFunctionAgent` that posts the question and returns the answer as text and the routes as a `CUSTOM` event, drawn by a `renderers.custom.routes` component of `Link` chips that close the panel. No tools: read-only by construction. A 503 thrown from the function shows as the run's error. |
|
|
533
|
+
| Boilerworks django-nextjs-copilotkit | CopilotKit 1.63.1 v1 (`CopilotKit`, `CopilotSidebar`, `useCopilotAction`, `useCopilotReadable`, `react-ui` CSS re-themed) | Upgrade to `@copilotkit/react-core` 1.79+ and `@copilotkit/runtime/v2` (the runtime route keeps its `HttpAgent` to Django), then `CopilotKitAgentProvider runtimeUrl="/api/copilotkit" agentId="boilerworks_agent"` and `<CalliopeAgent mode="rail">` in place of `CopilotSidebar`. `useCopilotReadable` becomes `useAgentReadable`; `renderAndWaitForResponse` becomes `useAgentTool` with `render` and `respond`; the render-only `draft_form_definition` card becomes `renderers.tools.draft_form_definition`. Drop `@copilotkit/react-ui` and `copilot.css`. |
|
|
534
|
+
|
|
535
|
+
## Workbench
|
|
536
|
+
|
|
537
|
+
`@calliopelabs/ui/workbench` is an opt-in entry for IDE-style tools (File Manager, Loadr, Chat Studio): an app without one never loads it. Its styles come from the preset. The code editors and the chart need optional peers the host installs and loads itself (`monaco-editor` through `configureMonaco`, `recharts` through `DataChart`'s `loadCharts`), so the entry builds without either. Like the rest, it is host-driven: data comes in as props, every change goes out through a callback, and nothing fetches.
|
|
538
|
+
|
|
539
|
+
**New in 0.17.0** (calliopeai/calliope-ux#12, extracted from Loadr and serving Chat Studio's fork too): the IDE shell (`WorkbenchShell`, `MenuBar`, `ActivityBar`, `EditorTabs` and `EditorGroup`, `PaneGroup`, `BottomPanel`, `StatusBar`, `CommandPalette`, `useShortcuts`; #79), the `Menubar` primitive in the main entry (#79), `DataGrid` with editing and pending changes, `DataProfile` and `DataChart` (#81), and `CodeEditor` and `SqlEditor` on Monaco (#80). New dependency: `@tanstack/react-virtual`. New optional peers: `monaco-editor`, `recharts`. Nothing existing changed. The file browser, upload dropzone and preview shipped in 0.16.0.
|
|
540
|
+
|
|
541
|
+
- **`FileBrowser`**: breadcrumb (its crumbs are drop targets), list and grid views, sorting (folders first, natural order), selection, a context menu, drag-and-drop moves (Alt copies) and an optional folder tree. View, sort and selection work uncontrolled or controlled. Copy for an empty folder, a toolbar slot, and thumbnails for grid tiles.
|
|
542
|
+
- **`FileTree`**: a lazy tree on the WAI-ARIA tree pattern (arrows open and move, Home, End, typeahead). The host answers `onLoad(path)` with a `FileBranch` (`loading`, `error`, `ready`) per directory in `branches`; a failed branch retries through `onRetry`.
|
|
543
|
+
- **`FileList`** and **`FileGrid`**, the two views on their own: a grid (WAI-ARIA grid pattern: arrows between cells, selection follows focus, Shift extends, Cmd/Ctrl moves without selecting, Cmd/Ctrl+A, Escape, Enter opens, Delete, F2 renames) and a multi-select listbox of tiles with two-dimensional arrows.
|
|
544
|
+
- **`UploadDropzone`**: wraps an area (an overlay while desktop files hover, progress floating in a corner) or draws its own box with a picker. `accept` and `maxSize` filter into `onFiles` and `onReject`; the host's `uploads` (`queued`, `uploading` with a percentage or none, `done`, `error`, `canceled`) draw as progress bars with cancel and dismiss.
|
|
545
|
+
- **`FilePreview`**: CSV and TSV (parsed here, quoted fields included) and server-parsed tables in the library's `DataTable` with search, sort and paging; JSON pretty-printed; images; text; markdown through the host's `renderMarkdown` (the package ships no markdown parser, so without one the source shows); a binary fallback, loading, error and a truncation note.
|
|
546
|
+
- Helpers: `FileItem`, `sortFiles`, `previewKindFor`, `parseDelimited`, `formatBytes`, `canDrop`, `pathSegments`, `parentPath`, `joinPath`, `FileIcon`.
|
|
547
|
+
|
|
548
|
+
```tsx
|
|
549
|
+
import {
|
|
550
|
+
FileBrowser,
|
|
551
|
+
FilePreview,
|
|
552
|
+
UploadDropzone,
|
|
553
|
+
previewKindFor,
|
|
554
|
+
} from "@calliopelabs/ui/workbench";
|
|
555
|
+
|
|
556
|
+
<UploadDropzone onFiles={(files) => upload(files, path)} uploads={uploads} onCancel={cancel}>
|
|
557
|
+
<FileBrowser
|
|
558
|
+
path={path}
|
|
559
|
+
items={listing.data?.entries.map(toFileItem)}
|
|
560
|
+
error={listing.error}
|
|
561
|
+
onRetry={listing.refetch}
|
|
562
|
+
onNavigate={setPath}
|
|
563
|
+
onOpen={(item) => setPreview(item.path)}
|
|
564
|
+
onMove={(sources, target, { copy }) => (copy ? copyTo : moveTo)(sources, target)}
|
|
565
|
+
onDelete={confirmDelete}
|
|
566
|
+
contextMenu={(targets) => <FileActions targets={targets} />}
|
|
567
|
+
tree={{ root, branches, onLoad: loadFolder, onRetry: loadFolder }}
|
|
568
|
+
/>
|
|
569
|
+
</UploadDropzone>;
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Their copy is in the `workbench` message group. The catalog's **Workbench** section shows every state, light and dark, narrow, and large trees.
|
|
573
|
+
|
|
574
|
+
### The IDE shell
|
|
575
|
+
|
|
576
|
+
Extracted from Loadr's layout (shared with Chat Studio). Every region is host content and every toggle a prop; the shell holds no state but the pane sizes.
|
|
577
|
+
|
|
578
|
+
- **`WorkbenchShell`**: menu bar, activity bar, a resizable side bar, the editors over a bottom panel, an optional right-hand panel, and the status bar. Hidden regions stay mounted, so an open page keeps its state; a maximized panel collapses the editors without unmounting them. `storageKey` remembers pane sizes per person.
|
|
579
|
+
- **`MenuBar`**: menus from data (`WorkbenchMenu`: items with shortcuts, checkbox items, labels, separators) on the `Menubar` primitive (WAI-ARIA menubar). `start` and `end` slots for the product mark and name.
|
|
580
|
+
- **`ActivityBar`**: the rail of views as a vertical toolbar of toggle buttons (one tab stop, arrows move), with labels, badges, footer items, and items that toggle a panel instead of choosing a view. Choosing the open view passes `null`, so the host closes the side bar.
|
|
581
|
+
- **`EditorTabs`** and **`EditorGroup`**: the WAI-ARIA tabs pattern with an unsaved marker (spoken), close by button, middle click or Delete, reorder by drag or Alt+Shift+Left/Right, a menu of every tab once the strip overflows, a per-tab context menu, trailing actions and a focused-group marker for split layouts. `EditorGroup` adds the labelled tab panel.
|
|
582
|
+
- **`PaneGroup`** and **`Pane`**: resizable panes on the WAI-ARIA window-splitter pattern (focusable `separator`s; arrows step, Home and End go to the bounds; pointer drag). Bounds in percent or pixels, collapsed panes stay mounted, sizes persist through `storageKey` (the library's local store).
|
|
583
|
+
- **`BottomPanel`**: views as tabs with counts, maximize, restore and close.
|
|
584
|
+
- **`StatusBar`** and **`StatusBarItem`**: a labelled region with start and end slots; `tone` paints it with a status colour (a production connection in `error`).
|
|
585
|
+
- **`CommandPalette`**: a modal WAI-ARIA combobox over a grouped listbox with fuzzy ranking (`fuzzyScore`, `rankItems`), recent entries, key caps, a loading state, controlled query for prefix modes ("@" for objects), and `onItemKeyDown` for host keys on the highlighted entry. Focus returns where it was.
|
|
586
|
+
- **`useShortcuts`**, **`KeyboardShortcutsDialog`**, **`ShortcutKeys`**: a page-wide registry with one listener; chords are strings (`"Mod+K"`: Command on a Mac, Control elsewhere); bare keys stay out of text fields; the last mounted binding wins; the dialog lists whatever is registered. `parseShortcut`, `matchesShortcut`, `formatShortcut` are pure.
|
|
587
|
+
|
|
588
|
+
```tsx
|
|
589
|
+
useShortcuts([
|
|
590
|
+
{ id: "palette", keys: "Mod+K", label: "Command palette", run: () => setPalette(true) },
|
|
591
|
+
{ id: "sidebar", keys: "Mod+B", label: "Toggle side bar", run: toggleSidebar },
|
|
592
|
+
]);
|
|
593
|
+
|
|
594
|
+
<WorkbenchShell
|
|
595
|
+
storageKey="loadr"
|
|
596
|
+
menuBar={<MenuBar menus={menus} end="Loadr" />}
|
|
597
|
+
activityBar={<ActivityBar items={views} active={view} onActiveChange={setView} />}
|
|
598
|
+
sidebar={<Explorer />}
|
|
599
|
+
sidebarOpen={view !== null}
|
|
600
|
+
bottomPanel={<BottomPanel views={panels} maximized={max} onMaximizedChange={setMax} />}
|
|
601
|
+
bottomPanelOpen={panelOpen}
|
|
602
|
+
bottomPanelMaximized={max}
|
|
603
|
+
statusBar={<StatusBar tone={envTone} start={connection} end={version} />}
|
|
604
|
+
overlays={
|
|
605
|
+
<CommandPalette open={palette} onOpenChange={setPalette} items={commands} onSelect={run} />
|
|
606
|
+
}
|
|
607
|
+
>
|
|
608
|
+
<EditorGroup tabs={tabs} activeId={active} onSelect={setActive} onClose={close} onReorder={move}>
|
|
609
|
+
<ActivePage />
|
|
610
|
+
</EditorGroup>
|
|
611
|
+
</WorkbenchShell>;
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### Code and SQL editors
|
|
615
|
+
|
|
616
|
+
`CodeEditor` wraps Monaco once; `SqlEditor` adds schema-aware completion. Monaco is an optional peer dependency (`monaco-editor` `>=0.52 <1`) that the package never imports: the host hands over its own import, so its bundler resolves Monaco, puts it in its own chunk and fetches it the first time an editor mounts. An app without an editor never loads it; without a loader (or if it fails) a plain text area stands in.
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
// Once, at startup (Vite shown; the worker setup is the bundler's part).
|
|
620
|
+
import { configureMonaco } from "@calliopelabs/ui/workbench";
|
|
621
|
+
|
|
622
|
+
configureMonaco({
|
|
623
|
+
load: async () => {
|
|
624
|
+
const [monaco, worker] = await Promise.all([
|
|
625
|
+
import("monaco-editor"),
|
|
626
|
+
import("monaco-editor/editor/editor.worker?worker"),
|
|
627
|
+
]);
|
|
628
|
+
self.MonacoEnvironment = { getWorker: () => new worker.default() };
|
|
629
|
+
return monaco;
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
- **`CodeEditor`**: `value`, `onChange`, `language`, `label` (its accessible name), `readOnly`, `onRun` (Ctrl or Command with Enter, the selection or the whole buffer), `options` (Monaco's own), `onMount(editor, monaco)` with an optional cleanup, and a per-editor `loader`. Host value changes are applied as edits, so undo still works. The theme comes from the library's tokens under the editor (ground, text, accent, border; syntax from the chart series) and follows a change of ground or mode; Monaco themes are page-wide.
|
|
635
|
+
- **`SqlEditor`**: a `schema` (`tables` with optional `columns`, `schemas`, `databases`, `otherDatabases`) drives completion: tables after FROM and JOIN, columns after SELECT and WHERE (qualified when two tables share a name), `alias.` resolved against the statement under the cursor, `schema.` and `db.` qualifiers, keywords, functions and window-function snippets. Completion never waits: what the schema lacks (a table's columns, another database's tables) comes out once through `onSchemaRequest`, and the host passes a new `schema`.
|
|
636
|
+
- Pure helpers for anything else: `sqlCompletions(text, offset, schema)`, `sqlCursorContext`, `statementAt`, `extractTableRefs`, `resolveSqlDot`, `isDdlStatement` (a host's cue to refresh its schema), `editorTheme`, `readEditorPalette`.
|
|
637
|
+
|
|
638
|
+
### Data grid, profile and chart
|
|
639
|
+
|
|
640
|
+
Extracted from Loadr's result grid and data editor (Chat Studio runs a fork of the same files). Host-driven: rows in, changes and copies out through callbacks, nothing fetches, no toasts.
|
|
641
|
+
|
|
642
|
+
- **`DataGrid`**: `columns` (names, or `GridColumn` with `type` in any driver spelling, `primaryKey`, `foreignKey`, `nullable`; undeclared types are sniffed) and `rows`. Rows and columns are virtualized (`@tanstack/react-virtual`), so 50,000 rows by 30 columns is a few hundred nodes. The WAI-ARIA grid pattern: arrows, Home and End, Ctrl+Home and Ctrl+End, Page Up and Down, Shift extends, Ctrl or Command with A selects all, with C copies TSV (Shift adds headers); Enter opens the cell, Shift+Enter the row. Cell, row and column selection (Shift and Ctrl or Command with the mouse), sort with NULLs last, a filter, hidden columns, resize by pointer or keyboard (a focusable `separator`; widths persist under `storageKey` in the library's local store), selection statistics, copy as TSV, CSV, JSON, INSERT or Markdown, and export as CSV, TSV, JSON, JSON Lines, SQL INSERT or Markdown. `totalRows` or `hasMore` shows the truncation banner; `loading`, `error` and `onRetry` the states. `onCopy` hears every copy; `onExport` takes over export (each request carries the library's `file` for its formats, `null` for the host's own `exportFormats`, such as a workbook), and `downloadExport(file)` saves one.
|
|
643
|
+
- **Editing**: `editable` turns on in-place editing (Enter, F2, double click or typing; Enter and Tab keep and move, Escape cancels), add, duplicate and delete rows, and a `PendingChangesBar` with Commit and Discard. Existing rows change only when a column is a `primaryKey`; without one, rows can be added. Commit calls `onCommit(changeSet)`: inserts with the filled cells, updates keyed on the primary key with the changed cells and their old values (for an optimistic-concurrency check), deletes by key. Resolve and the grid clears the changes; reject with `{ message, key }` and they stay, with the failed row marked. `pending` and `onPendingChange` control the changes from outside. The model is pure (`editCell`, `addRow`, `deleteRows`, `revertRow`, `pendingCounts`, `toChangeSet`), and `changeSetToSql(changeSet, { table })` writes the statements for a host that runs SQL.
|
|
644
|
+
- **`DataProfile`**: a card per column (completeness, distinct, range, mean and median, top values), sampling at most `rowCap` rows (10,000) and saying so.
|
|
645
|
+
- **`DataChart`**: bar, line, area, scatter or pie of an X column and Y columns, in the theme's chart series. Recharts is an optional peer (`recharts` `^3`) that the package never imports: the host passes `loadCharts={() => import("recharts")}`, so an app without charts builds without it and one with them loads it when a chart opens. Plots at most `rowCap` rows (500).
|
|
646
|
+
- Also exported: `CellDetailDialog`, `RowDetailDrawer`, `TruncationBanner`, `GridCellValue`, `ColumnTypeIcon`, and the pure helpers (`viewRowIndexes`, `compareCells`, `normalizeColumnType`, `buildColumnMeta`, the selection model, `formatMatrix`, `exportMatrix`, `toTSV`, `toCSV`, `toInsert`, `toMarkdown`, `computeStats`, `profileColumn`).
|
|
647
|
+
|
|
648
|
+
```tsx
|
|
649
|
+
<DataGrid
|
|
650
|
+
label="public.people"
|
|
651
|
+
columns={[{ name: "id", type: "int8", primaryKey: true }, "name", "email"]}
|
|
652
|
+
rows={result.rows}
|
|
653
|
+
totalRows={result.rowCount}
|
|
654
|
+
storageKey="table:public.people"
|
|
655
|
+
tableName="public.people"
|
|
656
|
+
editable={editMode}
|
|
657
|
+
onCommit={(changes) => api.apply({ table: "public.people", ...changes })}
|
|
658
|
+
onExport={(request) =>
|
|
659
|
+
request.file ? downloadExport(request.file) : writeWorkbook(request.matrix)
|
|
660
|
+
}
|
|
661
|
+
exportFormats={[{ id: "xlsx", label: "Excel workbook" }]}
|
|
662
|
+
/>
|
|
663
|
+
```
|
|
664
|
+
|
|
431
665
|
## Appearance
|
|
432
666
|
|
|
433
667
|
The person sets four axes, per product: the **ground** (black, charcoal, graphite, emerald, navy, paper, mist; it decides light or dark), the **accent** (the product's own, a family accent, or any colour that stands 3:1 off the ground), the **density** (`compact` hairline rows or boxed `cards`) and the **corners** (0, 2, 4 or 10 px). `AppearancePicker` is the settings UI for all four.
|
|
@@ -3,6 +3,7 @@ import type { ChatContentRenderer } from "../chat/MessageBubble.js";
|
|
|
3
3
|
import { type ChatToolCall } from "../chat/model.js";
|
|
4
4
|
import { type ModelPickerProps } from "../chat/ModelPicker.js";
|
|
5
5
|
import type { SendOptions } from "./controller.js";
|
|
6
|
+
import { type AgentRenderers } from "./renderers.js";
|
|
6
7
|
export interface AgentChatProps {
|
|
7
8
|
/** The panel's title. Defaults to the product's assistant ("Astrolift AI assistant"). */
|
|
8
9
|
title?: string;
|
|
@@ -23,6 +24,12 @@ export interface AgentChatProps {
|
|
|
23
24
|
renderToolCall?: (call: ChatToolCall) => React.ReactNode;
|
|
24
25
|
/** The tool's name for people, per call. */
|
|
25
26
|
toolTitle?: (call: ChatToolCall) => string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Generative UI: host components for tool calls, custom events, interrupts
|
|
29
|
+
* and the shared state (see `AgentRenderers`; `workflowRenderers` draws the
|
|
30
|
+
* run and review components). `renderToolCall` still wins for a call.
|
|
31
|
+
*/
|
|
32
|
+
renderers?: AgentRenderers;
|
|
26
33
|
/** "line": a one-line status while running (bubble, rail). "steps": the run's steps on the run timeline (page). */
|
|
27
34
|
progress?: "line" | "steps" | "none";
|
|
28
35
|
/** Replaces the empty state (the suggestions still show under it). */
|
|
@@ -53,4 +60,4 @@ export declare function NewConversationButton({ className }: {
|
|
|
53
60
|
* their own calls; one that waits on the person gets `respond`, and the
|
|
54
61
|
* composer holds until it is answered.
|
|
55
62
|
*/
|
|
56
|
-
export declare function AgentChat({ title, hideHeader, headerActions, onClose, expanded, onExpandedChange, hideNewConversation, renderContent, renderToolCall, toolTitle, progress, empty, attachments, composerActions, modelPicker, sendOptions, placeholder, autoFocus, suggestionsAfterRun, className, }: AgentChatProps): React.JSX.Element;
|
|
63
|
+
export declare function AgentChat({ title, hideHeader, headerActions, onClose, expanded, onExpandedChange, hideNewConversation, renderContent, renderToolCall, toolTitle, renderers, progress, empty, attachments, composerActions, modelPicker, sendOptions, placeholder, autoFocus, suggestionsAfterRun, className, }: AgentChatProps): React.JSX.Element;
|
package/dist/agent/AgentChat.js
CHANGED
|
@@ -14,6 +14,8 @@ import { useUI } from "../provider/UIProvider.js";
|
|
|
14
14
|
import { EmptyState } from "../states/EmptyState.js";
|
|
15
15
|
import { StatusDot } from "../status/StatusDot.js";
|
|
16
16
|
import { useCalliopeAgent } from "./AgentProvider.js";
|
|
17
|
+
import { parseResult } from "./renderers.js";
|
|
18
|
+
import { InterruptCard } from "./WorkflowRenderers.js";
|
|
17
19
|
function ToolRender({ tool, call, waiting, respond, }) {
|
|
18
20
|
return (_jsx(_Fragment, { children: tool.render?.({
|
|
19
21
|
call,
|
|
@@ -43,7 +45,7 @@ function Suggestions({ suggestions, disabled, onPick, }) {
|
|
|
43
45
|
* their own calls; one that waits on the person gets `respond`, and the
|
|
44
46
|
* composer holds until it is answered.
|
|
45
47
|
*/
|
|
46
|
-
export function AgentChat({ title, hideHeader = false, headerActions, onClose, expanded, onExpandedChange, hideNewConversation = false, renderContent, renderToolCall, toolTitle, progress = "line", empty, attachments, composerActions, modelPicker, sendOptions, placeholder, autoFocus, suggestionsAfterRun = false, className, }) {
|
|
48
|
+
export function AgentChat({ title, hideHeader = false, headerActions, onClose, expanded, onExpandedChange, hideNewConversation = false, renderContent, renderToolCall, toolTitle, renderers, progress = "line", empty, attachments, composerActions, modelPicker, sendOptions, placeholder, autoFocus, suggestionsAfterRun = false, className, }) {
|
|
47
49
|
const agent = useCalliopeAgent();
|
|
48
50
|
const { messages } = useUI();
|
|
49
51
|
const product = useProduct();
|
|
@@ -64,13 +66,52 @@ export function AgentChat({ title, hideHeader = false, headerActions, onClose, e
|
|
|
64
66
|
const custom = renderToolCall?.(call);
|
|
65
67
|
if (custom !== undefined)
|
|
66
68
|
return custom;
|
|
69
|
+
const Renderer = renderers?.tools?.[call.name];
|
|
70
|
+
if (Renderer) {
|
|
71
|
+
return (_jsx(Renderer, { call: call, args: parseToolCallArgs(call.args), result: parseResult(call), agent: agent, ...(waiting.includes(call.id)
|
|
72
|
+
? { respond: (result) => agent.respond(call.id, result) }
|
|
73
|
+
: {}) }));
|
|
74
|
+
}
|
|
67
75
|
const tool = tools.find((t) => t.name === call.name && t.render);
|
|
68
76
|
if (!tool)
|
|
69
77
|
return undefined;
|
|
70
78
|
return (_jsx(ToolRender, { tool: tool, call: call, waiting: waiting.includes(call.id), respond: agent.respond }));
|
|
71
79
|
};
|
|
80
|
+
// Custom events, each after the timeline item it arrived after (the first, when before any).
|
|
81
|
+
const first = view.timeline[0];
|
|
82
|
+
const firstKey = first ? `${first.kind}:${first.id}` : null;
|
|
83
|
+
const drawAfter = (item) => {
|
|
84
|
+
const key = `${item.kind}:${item.id}`;
|
|
85
|
+
const here = agent.custom.filter((c) => renderers?.custom?.[c.name] && (c.after ?? firstKey) === key);
|
|
86
|
+
if (here.length === 0)
|
|
87
|
+
return null;
|
|
88
|
+
return here.map((c) => {
|
|
89
|
+
const Renderer = renderers.custom[c.name];
|
|
90
|
+
return (_jsx("div", { "data-slot": "agent-custom", className: "min-w-0", children: _jsx(Renderer, { name: c.name, value: c.value, history: c.history, agent: agent }) }, c.key));
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
// Interrupts that no tool card or artifact answers: drawn after the conversation.
|
|
94
|
+
const claimed = new Set(agent.custom.flatMap((c) => {
|
|
95
|
+
const id = c.value?.interruptId;
|
|
96
|
+
return renderers?.custom?.[c.name] && typeof id === "string" ? [id] : [];
|
|
97
|
+
}));
|
|
98
|
+
const loose = view.status === "interrupted"
|
|
99
|
+
? view.interrupts.filter((i) => !i.toolCallId && !claimed.has(i.id))
|
|
100
|
+
: [];
|
|
101
|
+
const footer = loose.length > 0
|
|
102
|
+
? loose.map((interrupt) => {
|
|
103
|
+
const Renderer = renderers?.interrupts?.[interrupt.reason] ?? InterruptCard;
|
|
104
|
+
return (_jsx(Renderer, { interrupt: interrupt, agent: agent, resolve: (payload) => agent.resolveInterrupt(interrupt.id, {
|
|
105
|
+
status: "resolved",
|
|
106
|
+
...(payload === undefined ? {} : { payload }),
|
|
107
|
+
}), cancel: () => agent.resolveInterrupt(interrupt.id, { status: "cancelled" }) }, interrupt.id));
|
|
108
|
+
})
|
|
109
|
+
: undefined;
|
|
110
|
+
const StateView = renderers?.state;
|
|
72
111
|
const step = [...view.steps].reverse().find((s) => s.status === "running");
|
|
73
|
-
return (_jsxs("section", { ref: root, "aria-labelledby": hideHeader ? undefined : headingId, "aria-label": hideHeader ? name : undefined, "data-slot": "agent-chat", className: cn("flex min-h-0 min-w-0 flex-col bg-card text-card-foreground", className), children: [!hideHeader && (_jsxs("div", { className: "flex min-w-0 items-center gap-1 border-b px-3 py-2", children: [_jsx("h2", { id: headingId, className: "min-w-0 flex-1 truncate text-sm font-semibold", children: name }), headerActions, !hideNewConversation && _jsx(NewConversationButton, {}), onExpandedChange && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": expanded ? t.restore : t.expand, title: expanded ? t.restore : t.expand, onClick: () => onExpandedChange(!expanded), children: expanded ? _jsx(Minimize2Icon, { "aria-hidden": true }) : _jsx(Maximize2Icon, { "aria-hidden": true }) })), onClose && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": t.close, title: t.close, onClick: onClose, children: _jsx(XIcon, { "aria-hidden": true }) }))] })), progress === "steps" && view.steps.length > 0 && (_jsx(AgentProgress, { view: view, className: "border-b px-3 py-2" })),
|
|
112
|
+
return (_jsxs("section", { ref: root, "aria-labelledby": hideHeader ? undefined : headingId, "aria-label": hideHeader ? name : undefined, "data-slot": "agent-chat", className: cn("flex min-h-0 min-w-0 flex-col bg-card text-card-foreground", className), children: [!hideHeader && (_jsxs("div", { className: "flex min-w-0 items-center gap-1 border-b px-3 py-2", children: [_jsx("h2", { id: headingId, className: "min-w-0 flex-1 truncate text-sm font-semibold", children: name }), headerActions, !hideNewConversation && _jsx(NewConversationButton, {}), onExpandedChange && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": expanded ? t.restore : t.expand, title: expanded ? t.restore : t.expand, onClick: () => onExpandedChange(!expanded), children: expanded ? _jsx(Minimize2Icon, { "aria-hidden": true }) : _jsx(Maximize2Icon, { "aria-hidden": true }) })), onClose && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": t.close, title: t.close, onClick: onClose, children: _jsx(XIcon, { "aria-hidden": true }) }))] })), progress === "steps" && view.steps.length > 0 && (_jsx(AgentProgress, { view: view, className: "border-b px-3 py-2" })), StateView && view.state !== undefined && (_jsx("div", { role: "region", "aria-label": t.sharedState, "data-slot": "agent-state",
|
|
113
|
+
// A scrolling region is reached by keyboard (WCAG 2.1.1).
|
|
114
|
+
tabIndex: 0, className: "max-h-48 min-w-0 shrink-0 overflow-y-auto border-b px-3 py-2", children: _jsx(StateView, { state: view.state, setState: agent.setState, agent: agent }) })), _jsx(MessageList, { view: view, renderContent: renderContent, renderToolCall: drawCall, renderAfter: drawAfter, footer: footer, toolTitle: toolTitle, onApprove: (call) => agent.approve(call), onDeny: (call, reason) => agent.deny(call, reason), onRetry: () => void agent.regenerate(), empty: _jsxs("div", { className: "flex flex-col items-center gap-4 py-6", children: [empty ?? _jsx(EmptyState, { title: t.emptyTitle, description: t.emptyDescription }), _jsx(Suggestions, { suggestions: suggestions, disabled: running, onPick: pick })] }), className: "min-h-0 flex-1" }), _jsxs("div", { className: "flex min-w-0 flex-col gap-2 border-t p-3", children: [suggestionsAfterRun && !fresh && !running && (_jsx(Suggestions, { suggestions: suggestions, disabled: running, onPick: pick })), _jsx("p", { role: "status", "data-slot": "agent-status", className: cn("flex min-w-0 items-center gap-2 text-xs text-muted-foreground", !(progress === "line" && running) && !blocked && "sr-only"), children: blocked ? (t.waitingOnYou) : running ? (_jsxs(_Fragment, { children: [_jsx(StatusDot, { tone: "info", pulse: true }), _jsx("span", { className: "truncate", children: step ? step.name : messages.chat.runStatus("running") })] })) : null }), _jsx(Composer, { onSubmit: send, running: running, onStop: agent.stop, disabled: blocked, placeholder: placeholder,
|
|
74
115
|
// eslint-disable-next-line jsx-a11y/no-autofocus -- the person just opened the assistant to type
|
|
75
116
|
autoFocus: autoFocus, attachments: attachments, actions: modelPicker || composerActions ? (_jsxs(_Fragment, { children: [modelPicker && _jsx(ModelPicker, { ...modelPicker }), composerActions] })) : undefined })] })] }));
|
|
76
117
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type DocumentReviewProps } from "../workflow/DocumentReview.js";
|
|
3
|
+
import { type FormReviewProps } from "../workflow/FormReview.js";
|
|
4
|
+
import { type MediaReviewProps } from "../workflow/MediaReview.js";
|
|
5
|
+
import type { WorkflowQuestion } from "../workflow/question.js";
|
|
6
|
+
import type { Artifact, RunStage } from "../workflow/run.js";
|
|
7
|
+
import type { AgentRenderers, CustomRendererProps, InterruptRendererProps } from "./renderers.js";
|
|
8
|
+
/** `calliope.run`: a run's stages as the agent reports them. Send it again with the same `id` to update. */
|
|
9
|
+
export interface RunProgressValue {
|
|
10
|
+
id?: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
stages: readonly RunStage[];
|
|
13
|
+
}
|
|
14
|
+
/** The review viewer an artifact opens in. */
|
|
15
|
+
export type ArtifactReviewValue = ({
|
|
16
|
+
viewer: "document";
|
|
17
|
+
} & Pick<DocumentReviewProps, "title" | "pages" | "annotations" | "signOffs" | "signOffRole">) | ({
|
|
18
|
+
viewer: "media";
|
|
19
|
+
} & Pick<MediaReviewProps, "kind" | "src" | "title" | "poster" | "captions" | "duration" | "comments">) | ({
|
|
20
|
+
viewer: "form";
|
|
21
|
+
} & Pick<FormReviewProps, "title" | "pages" | "fields" | "threshold">);
|
|
22
|
+
/**
|
|
23
|
+
* `calliope.artifact`: an output for the person to look at, and to review
|
|
24
|
+
* when the run waits on it. `interruptId` names the run's interrupt the
|
|
25
|
+
* review answers: signing a document resolves it with `{ action: "sign-off" }`,
|
|
26
|
+
* sending a form's review with `{ action: "form-review", fields }`.
|
|
27
|
+
*/
|
|
28
|
+
export interface ArtifactValue {
|
|
29
|
+
id: string;
|
|
30
|
+
artifact: Artifact;
|
|
31
|
+
review?: ArtifactReviewValue;
|
|
32
|
+
interruptId?: string;
|
|
33
|
+
}
|
|
34
|
+
/** `calliope.question` interrupts carry a question in `metadata.question`. */
|
|
35
|
+
export interface QuestionInterruptMetadata {
|
|
36
|
+
question: WorkflowQuestion;
|
|
37
|
+
}
|
|
38
|
+
/** The run timeline for `calliope.run` events: progress drawn in the conversation. */
|
|
39
|
+
export declare function RunProgressCard({ value }: CustomRendererProps<RunProgressValue>): React.JSX.Element;
|
|
40
|
+
/**
|
|
41
|
+
* An artifact the agent produced, with its review viewer (document, media or
|
|
42
|
+
* form) as the preview. While the run waits on its review, signing off or
|
|
43
|
+
* sending the form's review answers the interrupt and resumes the run.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ArtifactReviewCard({ value, agent }: CustomRendererProps<ArtifactValue>): React.JSX.Element;
|
|
46
|
+
/** A `calliope.question` interrupt: the runtime question card, its answers resuming the run. */
|
|
47
|
+
export declare function QuestionInterruptCard({ interrupt, resolve }: InterruptRendererProps): React.JSX.Element;
|
|
48
|
+
/**
|
|
49
|
+
* Any other open interrupt not tied to a tool call: the agent's question in
|
|
50
|
+
* its words, with approve and decline (a decline always with a reason).
|
|
51
|
+
*/
|
|
52
|
+
export declare function InterruptCard({ interrupt, resolve, className, }: InterruptRendererProps & {
|
|
53
|
+
className?: string;
|
|
54
|
+
}): React.JSX.Element;
|
|
55
|
+
/**
|
|
56
|
+
* The workflow run and review components as generative UI: `calliope.run`
|
|
57
|
+
* custom events draw the run timeline, `calliope.artifact` events draw the
|
|
58
|
+
* artifact with its document, media or form review, and `calliope.question`
|
|
59
|
+
* interrupts draw the runtime question card. Merge with the host's own.
|
|
60
|
+
*/
|
|
61
|
+
export declare const workflowRenderers: AgentRenderers;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { Button } from "../primitives/Button.js";
|
|
6
|
+
import { useUI } from "../provider/UIProvider.js";
|
|
7
|
+
import { ArtifactCard } from "../workflow/ArtifactCard.js";
|
|
8
|
+
import { DocumentReview } from "../workflow/DocumentReview.js";
|
|
9
|
+
import { FormReview } from "../workflow/FormReview.js";
|
|
10
|
+
import { MediaReview } from "../workflow/MediaReview.js";
|
|
11
|
+
import { RunTimeline } from "../workflow/RunTimeline.js";
|
|
12
|
+
import { WorkflowQuestionCard } from "../workflow/WorkflowQuestionCard.js";
|
|
13
|
+
/** The run timeline for `calliope.run` events: progress drawn in the conversation. */
|
|
14
|
+
export function RunProgressCard({ value }) {
|
|
15
|
+
return (_jsx("div", { className: "min-w-0 rounded-lg border bg-card p-3", children: _jsx(RunTimeline, { stages: value.stages, label: value.label }) }));
|
|
16
|
+
}
|
|
17
|
+
function openInterrupt(agent, id) {
|
|
18
|
+
if (!id || agent.view.status !== "interrupted")
|
|
19
|
+
return undefined;
|
|
20
|
+
return agent.view.interrupts.find((i) => i.id === id);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* An artifact the agent produced, with its review viewer (document, media or
|
|
24
|
+
* form) as the preview. While the run waits on its review, signing off or
|
|
25
|
+
* sending the form's review answers the interrupt and resumes the run.
|
|
26
|
+
*/
|
|
27
|
+
export function ArtifactReviewCard({ value, agent }) {
|
|
28
|
+
const { messages } = useUI();
|
|
29
|
+
const waiting = openInterrupt(agent, value.interruptId);
|
|
30
|
+
const [fields, setFields] = React.useState({});
|
|
31
|
+
const resolve = (payload) => waiting && agent.resolveInterrupt(waiting.id, { status: "resolved", payload });
|
|
32
|
+
const review = value.review;
|
|
33
|
+
let preview = null;
|
|
34
|
+
if (review?.viewer === "document") {
|
|
35
|
+
const { viewer: _, ...props } = review;
|
|
36
|
+
preview = (_jsx(DocumentReview, { ...props, ...(waiting
|
|
37
|
+
? { onSignOff: () => resolve({ action: "sign-off", artifactId: value.id }) }
|
|
38
|
+
: {}) }));
|
|
39
|
+
}
|
|
40
|
+
else if (review?.viewer === "media") {
|
|
41
|
+
const { viewer: _, ...props } = review;
|
|
42
|
+
preview = _jsx(MediaReview, { ...props });
|
|
43
|
+
}
|
|
44
|
+
else if (review?.viewer === "form") {
|
|
45
|
+
const { viewer: _, ...props } = review;
|
|
46
|
+
const current = props.fields.map((f) => fields[f.id] ?? f);
|
|
47
|
+
preview = (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(FormReview, { ...props, fields: current, ...(waiting
|
|
48
|
+
? {
|
|
49
|
+
onAccept: (field) => setFields((s) => ({ ...s, [field.id]: { ...field, status: "accepted" } })),
|
|
50
|
+
onCorrect: (field, corrected) => setFields((s) => ({
|
|
51
|
+
...s,
|
|
52
|
+
[field.id]: { ...field, status: "corrected", corrected },
|
|
53
|
+
})),
|
|
54
|
+
}
|
|
55
|
+
: {}) }), waiting && (_jsx(Button, { size: "sm", className: "self-end", onClick: () => resolve({
|
|
56
|
+
action: "form-review",
|
|
57
|
+
artifactId: value.id,
|
|
58
|
+
fields: current.map(({ id, status, corrected }) => ({ id, status, corrected })),
|
|
59
|
+
}), children: messages.agent.sendReview }))] }));
|
|
60
|
+
}
|
|
61
|
+
return _jsx(ArtifactCard, { artifact: value.artifact, preview: preview, headingLevel: "h3" });
|
|
62
|
+
}
|
|
63
|
+
/** A `calliope.question` interrupt: the runtime question card, its answers resuming the run. */
|
|
64
|
+
export function QuestionInterruptCard({ interrupt, resolve }) {
|
|
65
|
+
const question = interrupt.metadata?.question;
|
|
66
|
+
const [state, setState] = React.useState("waiting");
|
|
67
|
+
return (_jsx(WorkflowQuestionCard, { question: question ?? null, state: state, onSubmit: (submission) => {
|
|
68
|
+
setState("submitting");
|
|
69
|
+
resolve({ answers: submission.answers, revision: submission.revision });
|
|
70
|
+
} }));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Any other open interrupt not tied to a tool call: the agent's question in
|
|
74
|
+
* its words, with approve and decline (a decline always with a reason).
|
|
75
|
+
*/
|
|
76
|
+
export function InterruptCard({ interrupt, resolve, className, }) {
|
|
77
|
+
const { messages } = useUI();
|
|
78
|
+
const t = messages.agent;
|
|
79
|
+
const [declining, setDeclining] = React.useState(false);
|
|
80
|
+
const [reason, setReason] = React.useState("");
|
|
81
|
+
const reasonId = React.useId();
|
|
82
|
+
return (_jsxs("section", { "aria-label": t.interrupt, "data-slot": "agent-interrupt", className: cn("flex min-w-0 flex-col gap-2 rounded-lg border border-warning-border bg-warning-bg p-3 text-sm text-warning-fg", className), children: [_jsx("p", { className: "[overflow-wrap:anywhere]", children: interrupt.message ?? messages.chat.approvalNeeded }), declining ? (_jsxs("form", { className: "flex min-w-0 flex-col gap-2", onSubmit: (event) => {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
if (reason.trim())
|
|
85
|
+
resolve({ approved: false, reason: reason.trim() });
|
|
86
|
+
}, children: [_jsx("label", { htmlFor: reasonId, className: "text-xs font-medium", children: messages.chat.denyReason }), _jsx("textarea", { id: reasonId, value: reason, onChange: (event) => setReason(event.target.value), rows: 2, className: "min-w-0 rounded-md border border-input bg-background p-2 text-sm text-foreground" }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setDeclining(false), children: t.back }), _jsx(Button, { type: "submit", variant: "destructive", size: "sm", disabled: !reason.trim(), children: t.decline })] })] })) : (_jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setDeclining(true), children: t.decline }), _jsx(Button, { size: "sm", onClick: () => resolve({ approved: true }), children: messages.chat.approve })] }))] }));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The workflow run and review components as generative UI: `calliope.run`
|
|
90
|
+
* custom events draw the run timeline, `calliope.artifact` events draw the
|
|
91
|
+
* artifact with its document, media or form review, and `calliope.question`
|
|
92
|
+
* interrupts draw the runtime question card. Merge with the host's own.
|
|
93
|
+
*/
|
|
94
|
+
export const workflowRenderers = {
|
|
95
|
+
custom: {
|
|
96
|
+
"calliope.run": RunProgressCard,
|
|
97
|
+
"calliope.artifact": ArtifactReviewCard,
|
|
98
|
+
},
|
|
99
|
+
interrupts: {
|
|
100
|
+
"calliope.question": QuestionInterruptCard,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -38,6 +38,20 @@ export interface AgentControllerOptions {
|
|
|
38
38
|
/** Every AG-UI event, for the host's telemetry. */
|
|
39
39
|
onEvent?: (event: AgUiEvent) => void;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A custom event as the conversation draws it: events whose value carries
|
|
43
|
+
* the same `id` are one item (progress updating in place), placed where the
|
|
44
|
+
* first of them arrived.
|
|
45
|
+
*/
|
|
46
|
+
export interface AgentCustomItem {
|
|
47
|
+
key: string;
|
|
48
|
+
name: string;
|
|
49
|
+
/** The latest value. */
|
|
50
|
+
value: unknown;
|
|
51
|
+
history: readonly unknown[];
|
|
52
|
+
/** The timeline item it arrived after ("message:<id>", "tool-call:<id>"), or null before any. */
|
|
53
|
+
after: string | null;
|
|
54
|
+
}
|
|
41
55
|
export interface AgentSnapshot {
|
|
42
56
|
view: ChatView;
|
|
43
57
|
/** True from send until the agent is done, follow-ups and browser tools included. */
|
|
@@ -49,6 +63,8 @@ export interface AgentSnapshot {
|
|
|
49
63
|
unread: number;
|
|
50
64
|
suggestions: readonly AgentSuggestion[];
|
|
51
65
|
tools: readonly AgentTool[];
|
|
66
|
+
/** The conversation's custom events, grouped and placed (see `AgentCustomItem`). */
|
|
67
|
+
custom: readonly AgentCustomItem[];
|
|
52
68
|
}
|
|
53
69
|
export interface SendOptions {
|
|
54
70
|
/** Attachments and other parts after the text (AG-UI input content). */
|
|
@@ -76,6 +92,8 @@ export declare class AgentController {
|
|
|
76
92
|
private visible;
|
|
77
93
|
private read;
|
|
78
94
|
private snapshot;
|
|
95
|
+
/** Where each custom event arrived, by its index in the view. */
|
|
96
|
+
private anchors;
|
|
79
97
|
private disposed;
|
|
80
98
|
private connected;
|
|
81
99
|
private threadSeq;
|
|
@@ -97,6 +115,7 @@ export declare class AgentController {
|
|
|
97
115
|
private emit;
|
|
98
116
|
private finishedAnswers;
|
|
99
117
|
private build;
|
|
118
|
+
private customItems;
|
|
100
119
|
private bind;
|
|
101
120
|
/** The view the agent's own messages and state make, for a fresh start or a thread switch. */
|
|
102
121
|
private fromAgent;
|