@protobase/ui 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/action-card/action-card.d.ts +26 -0
  2. package/dist/action-card/action-card.js +93 -0
  3. package/dist/action-card/field-list.d.ts +9 -0
  4. package/dist/action-card/field-list.js +18 -0
  5. package/dist/action-card/index.d.ts +3 -0
  6. package/dist/app/app.d.ts +3 -1
  7. package/dist/app/app.js +20 -7
  8. package/dist/app/assistant-shell.js +39 -0
  9. package/dist/app-shell/global-search.d.ts +1 -1
  10. package/dist/assistant-dock/assistant-dock.d.ts +10 -10
  11. package/dist/assistant-dock/assistant-dock.js +39 -74
  12. package/dist/assistant-dock/index.d.ts +5 -0
  13. package/dist/assistant-dock/part-view.d.ts +8 -0
  14. package/dist/assistant-dock/part-view.js +47 -0
  15. package/dist/assistant-dock/use-assistant-client.d.ts +4 -0
  16. package/dist/assistant-dock/use-assistant-client.js +17 -0
  17. package/dist/assistant-dock/use-assistant.d.ts +11 -0
  18. package/dist/assistant-dock/use-assistant.js +27 -0
  19. package/dist/auth/use-api-token.d.ts +7 -0
  20. package/dist/auth/use-api-token.js +37 -0
  21. package/dist/chat/chat-message.d.ts +8 -0
  22. package/dist/chat/chat-message.js +11 -0
  23. package/dist/chat/chat-thread.d.ts +12 -0
  24. package/dist/chat/chat-thread.js +23 -0
  25. package/dist/chat/composer.d.ts +12 -0
  26. package/dist/{assistant-dock → chat}/composer.js +5 -4
  27. package/dist/chat/index.d.ts +4 -0
  28. package/dist/compact-table/compact-table.d.ts +13 -0
  29. package/dist/compact-table/compact-table.js +49 -0
  30. package/dist/compact-table/format-cell.js +9 -0
  31. package/dist/data/meta-data.d.ts +4 -0
  32. package/dist/data/use-meta.js +2 -1
  33. package/dist/diff-view/diff-view.d.ts +13 -0
  34. package/dist/diff-view/diff-view.js +62 -0
  35. package/dist/diff-view/diff.d.ts +13 -0
  36. package/dist/diff-view/diff.js +29 -0
  37. package/dist/index.d.ts +19 -2
  38. package/dist/index.js +16 -2
  39. package/dist/side-panel/index.d.ts +2 -0
  40. package/dist/side-panel/side-panel.d.ts +17 -0
  41. package/dist/side-panel/side-panel.js +40 -0
  42. package/dist/step-list/step-list.d.ts +12 -0
  43. package/dist/step-list/step-list.js +40 -0
  44. package/package.json +4 -4
  45. package/dist/assistant-dock/chat-message.js +0 -9
  46. package/dist/assistant-dock/dock-item.d.ts +0 -4
  47. package/dist/assistant-dock/dock-item.js +0 -37
  48. package/dist/assistant-dock/model.d.ts +0 -84
  49. package/dist/assistant-dock/model.js +0 -26
  50. package/dist/assistant-dock/plan-card.d.ts +0 -13
  51. package/dist/assistant-dock/plan-card.js +0 -133
  52. package/dist/assistant-dock/progress-card.js +0 -18
  53. package/dist/assistant-dock/query-card.js +0 -52
  54. package/dist/assistant-dock/queue-card.js +0 -31
  55. package/dist/assistant-dock/result-card.js +0 -77
  56. package/dist/assistant-panel/content.d.ts +0 -7
  57. package/dist/assistant-panel/step-list.js +0 -30
@@ -0,0 +1,26 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/action-card/action-card.d.ts
3
+ export type CardTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
4
+ /** A button of a card. With `confirm`, clicking it asks that question first. `hint` is a line under the buttons. */
5
+ export type CardAction = {
6
+ id: string;
7
+ label: string;
8
+ style?: 'primary' | 'secondary' | 'ghost' | 'danger';
9
+ disabled?: boolean;
10
+ hint?: string;
11
+ confirm?: string;
12
+ };
13
+ export type ActionCardProps = {
14
+ title: string;
15
+ tone?: CardTone;
16
+ /** Short text at the end of the title row. */
17
+ badge?: string;
18
+ /** A muted line under the content. */
19
+ note?: string;
20
+ actions?: CardAction[];
21
+ onAction?: (id: string) => void;
22
+ children?: ReactNode;
23
+ };
24
+ /** A card with a toned title row, any content, a note and buttons: the building block of rich chat messages. */
25
+ export declare const ActionCard: ({ title, tone, badge, note, actions, onAction, children }: ActionCardProps) => import("react").JSX.Element;
26
+ //#endregion
@@ -0,0 +1,93 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { Dialog } from "../primitives/dialog.js";
4
+ import { useState } from "react";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/action-card/action-card.tsx
7
+ const frames = {
8
+ neutral: {
9
+ border: "border-border-strong",
10
+ header: "bg-surface text-foreground"
11
+ },
12
+ info: {
13
+ border: "border-primary-border",
14
+ header: "bg-primary-soft text-primary-text"
15
+ },
16
+ success: {
17
+ border: "border-success/40",
18
+ header: "bg-success-soft text-success-text"
19
+ },
20
+ warning: {
21
+ border: "border-warning/40",
22
+ header: "bg-warning-soft text-warning-text"
23
+ },
24
+ danger: {
25
+ border: "border-danger/40",
26
+ header: "bg-danger-soft text-danger-text"
27
+ }
28
+ };
29
+ /** A card with a toned title row, any content, a note and buttons: the building block of rich chat messages. */
30
+ const ActionCard = ({ title, tone = "neutral", badge, note, actions = [], onAction, children }) => {
31
+ const [confirming, setConfirming] = useState();
32
+ const hints = actions.flatMap((action) => action.hint ? [action.hint] : []);
33
+ const click = (action) => action.confirm ? setConfirming(action) : onAction?.(action.id);
34
+ return /* @__PURE__ */ jsxs("section", {
35
+ "aria-label": title,
36
+ className: cn("shrink-0 overflow-hidden rounded-lg border bg-background whitespace-normal text-foreground shadow-sm", frames[tone].border),
37
+ children: [
38
+ /* @__PURE__ */ jsxs("header", {
39
+ className: cn("flex items-center gap-2 px-3 py-2", frames[tone].header),
40
+ children: [/* @__PURE__ */ jsx("h3", {
41
+ className: "min-w-0 flex-1 text-[13px] font-semibold",
42
+ children: title
43
+ }), badge && /* @__PURE__ */ jsx("span", {
44
+ className: "shrink-0 text-xs font-medium",
45
+ children: badge
46
+ })]
47
+ }),
48
+ (children || note || actions.length > 0) && /* @__PURE__ */ jsxs("div", {
49
+ className: "space-y-2.5 px-3 py-2.5",
50
+ children: [
51
+ children,
52
+ note && /* @__PURE__ */ jsx("p", {
53
+ className: "text-xs text-muted-foreground",
54
+ children: note
55
+ }),
56
+ actions.length > 0 && /* @__PURE__ */ jsx("div", {
57
+ className: "flex flex-wrap gap-2 pt-0.5",
58
+ children: actions.map((action) => /* @__PURE__ */ jsx(Button, {
59
+ size: "sm",
60
+ variant: action.style ?? "secondary",
61
+ className: cn(action.style === "primary" && "flex-1"),
62
+ disabled: action.disabled,
63
+ onClick: () => click(action),
64
+ children: action.label
65
+ }, action.id))
66
+ }),
67
+ hints.map((hint) => /* @__PURE__ */ jsx("p", {
68
+ className: "text-xs text-muted-foreground",
69
+ children: hint
70
+ }, hint))
71
+ ]
72
+ }),
73
+ confirming && /* @__PURE__ */ jsx(Dialog, {
74
+ title: `${confirming.label}?`,
75
+ description: confirming.confirm,
76
+ onClose: () => setConfirming(void 0),
77
+ actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
78
+ onClick: () => setConfirming(void 0),
79
+ children: "Cancel"
80
+ }), /* @__PURE__ */ jsx(Button, {
81
+ variant: confirming.style === "danger" ? "danger" : "primary",
82
+ onClick: () => {
83
+ setConfirming(void 0);
84
+ onAction?.(confirming.id);
85
+ },
86
+ children: confirming.label
87
+ })] })
88
+ })
89
+ ]
90
+ });
91
+ };
92
+ //#endregion
93
+ export { ActionCard };
@@ -0,0 +1,9 @@
1
+ //#region src/action-card/field-list.d.ts
2
+ /** Labelled values, one per row, inside a card. */
3
+ export declare const FieldList: ({ fields }: {
4
+ fields: Array<{
5
+ label: string;
6
+ value: string;
7
+ }>;
8
+ }) => import("react").JSX.Element;
9
+ //#endregion
@@ -0,0 +1,18 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ //#region src/action-card/field-list.tsx
3
+ /** Labelled values, one per row, inside a card. */
4
+ const FieldList = ({ fields }) => /* @__PURE__ */ jsx("dl", {
5
+ className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 text-xs",
6
+ children: fields.map((field, index) => /* @__PURE__ */ jsxs("div", {
7
+ className: "contents",
8
+ children: [/* @__PURE__ */ jsx("dt", {
9
+ className: "text-muted-foreground",
10
+ children: field.label
11
+ }), /* @__PURE__ */ jsx("dd", {
12
+ className: "min-w-0 break-words",
13
+ children: field.value
14
+ })]
15
+ }, `${index}-${field.label}`))
16
+ });
17
+ //#endregion
18
+ export { FieldList };
@@ -0,0 +1,3 @@
1
+ import { ActionCard, ActionCardProps, CardAction, CardTone } from "./action-card.js";
2
+ import { FieldList } from "./field-list.js";
3
+ export { ActionCard, type ActionCardProps, type CardAction, type CardTone, FieldList };
package/dist/app/app.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { SidebarMode } from "../app-shell/sidebar-mode.js";
2
2
  import "../app-shell/index.js";
3
3
  import { ProjectUi } from "./pages/project-ui.js";
4
- import { AuthSession, Client } from "@protobase/client";
4
+ import { AssistantClient, AuthSession, Client } from "@protobase/client";
5
5
  //#region src/app/app.d.ts
6
6
  export type AppProps = {
7
7
  /** Where `/api/v1` lives. Defaults to the current origin. */
@@ -18,6 +18,8 @@ export type AppProps = {
18
18
  sidebarMode?: SidebarMode;
19
19
  /** The project's React code for composed pages: custom components and action handlers (`protobase.ui.tsx`). */
20
20
  ui?: ProjectUi;
21
+ /** A ready-made client of the assistant backend, for stories and tests; used only when `/meta` names an assistant. */
22
+ assistant?: AssistantClient;
21
23
  };
22
24
  /** The admin: sign-in first, then a sidebar from `/meta`, `/:page` composed pages, `/:resource` lists and `/:resource/:key` records. Fills its parent. */
23
25
  export declare const App: ({ auth, workspace, ...rest }: AppProps) => import("react").JSX.Element;
package/dist/app/app.js CHANGED
@@ -7,6 +7,7 @@ import { useRecord } from "../data/use-record.js";
7
7
  import { recordTitle } from "../live/model-helpers.js";
8
8
  import { humanize } from "../live/naming.js";
9
9
  import { ToastProvider } from "../toasts/toast-provider.js";
10
+ import { AssistantButton, AssistantPanel } from "./assistant-shell.js";
10
11
  import { breadcrumbFor } from "./breadcrumb-for.js";
11
12
  import { Router, matchRoute, useRouter } from "./router.js";
12
13
  import { MetaGate, Notice, useAdminMeta } from "./meta-gate.js";
@@ -19,10 +20,10 @@ import { ComposedPage } from "./pages/composed-page.js";
19
20
  import { RecordPage } from "./record-page.js";
20
21
  import { useGlobalSearch } from "./search/use-global-search.js";
21
22
  import { useNavRecent } from "./use-nav-recent.js";
22
- import { useEffect } from "react";
23
- import { jsx } from "react/jsx-runtime";
23
+ import { useEffect, useState } from "react";
24
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
24
25
  //#region src/app/app.tsx
25
- const Routes = ({ workspace, sidebarMode }) => {
26
+ const Routes = ({ workspace, sidebarMode, assistant }) => {
26
27
  const meta = useAdminMeta();
27
28
  const { state, signOut } = useAuth();
28
29
  if (state.kind !== "signed-in") throw new Error("The shell is only rendered for a signed-in user");
@@ -50,6 +51,17 @@ const Routes = ({ workspace, sidebarMode }) => {
50
51
  const { shell } = useProjectUi();
51
52
  const Actions = shell?.actions;
52
53
  const RightPanel = shell?.rightPanel;
54
+ const [assistantOpen, setAssistantOpen] = useState(false);
55
+ const assistantUrl = meta.assistant?.url;
56
+ const assistantButton = assistantUrl && /* @__PURE__ */ jsx(AssistantButton, {
57
+ open: assistantOpen,
58
+ onToggle: () => setAssistantOpen(!assistantOpen)
59
+ });
60
+ const assistantPanel = assistantUrl && assistantOpen && /* @__PURE__ */ jsx(AssistantPanel, {
61
+ url: assistantUrl,
62
+ client: assistant,
63
+ onClose: () => setAssistantOpen(false)
64
+ });
53
65
  const breadcrumb = breadcrumbFor({
54
66
  basePath,
55
67
  route,
@@ -77,8 +89,8 @@ const Routes = ({ workspace, sidebarMode }) => {
77
89
  groups: search.groups,
78
90
  onSelect: open
79
91
  } : void 0,
80
- actions: Actions && /* @__PURE__ */ jsx(Actions, {}),
81
- rightPanel: RightPanel && /* @__PURE__ */ jsx(RightPanel, {}),
92
+ actions: (Actions || assistantButton) && /* @__PURE__ */ jsxs(Fragment$1, { children: [Actions && /* @__PURE__ */ jsx(Actions, {}), assistantButton] }),
93
+ rightPanel: (RightPanel || assistantPanel) && /* @__PURE__ */ jsxs(Fragment$1, { children: [RightPanel && /* @__PURE__ */ jsx(RightPanel, {}), assistantPanel] }),
82
94
  children: !route.resource ? /* @__PURE__ */ jsx(Notice, {
83
95
  title: "Choose a resource",
84
96
  children: "Pick one from the sidebar."
@@ -88,7 +100,7 @@ const Routes = ({ workspace, sidebarMode }) => {
88
100
  }, `${route.resource}/${route.key}`) : /* @__PURE__ */ jsx(ListPage, { resource: route.resource }, route.resource)
89
101
  });
90
102
  };
91
- const Authenticated = ({ baseUrl, client, basePath, initialUrl, workspace, sidebarMode, ui }) => {
103
+ const Authenticated = ({ baseUrl, client, basePath, initialUrl, workspace, sidebarMode, ui, assistant }) => {
92
104
  const { session, markSignedOut } = useAuth();
93
105
  return /* @__PURE__ */ jsx(ApiProvider, {
94
106
  baseUrl,
@@ -102,7 +114,8 @@ const Authenticated = ({ baseUrl, client, basePath, initialUrl, workspace, sideb
102
114
  ui,
103
115
  children: /* @__PURE__ */ jsx(Routes, {
104
116
  workspace,
105
- sidebarMode
117
+ sidebarMode,
118
+ assistant
106
119
  })
107
120
  }) }) })
108
121
  })
@@ -0,0 +1,39 @@
1
+ import { Button } from "../primitives/button.js";
2
+ import { AssistantDock } from "../assistant-dock/assistant-dock.js";
3
+ import { useAssistant } from "../assistant-dock/use-assistant.js";
4
+ import { useAssistantClient } from "../assistant-dock/use-assistant-client.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { Sparkles } from "lucide-react";
7
+ //#region src/app/assistant-shell.tsx
8
+ const AssistantButton = ({ open, onToggle }) => /* @__PURE__ */ jsxs(Button, {
9
+ size: "sm",
10
+ variant: open ? "primary" : "secondary",
11
+ "aria-pressed": open,
12
+ onClick: onToggle,
13
+ children: [/* @__PURE__ */ jsx(Sparkles, { className: "size-3.5" }), "Assistant"]
14
+ });
15
+ const ConnectedDock = ({ client, onClose }) => {
16
+ const { state, offline, error, send, act } = useAssistant(client);
17
+ return /* @__PURE__ */ jsx(AssistantDock, {
18
+ state,
19
+ offline,
20
+ error,
21
+ onSend: send,
22
+ onAction: act,
23
+ onClose
24
+ });
25
+ };
26
+ const OwnDock = ({ url, onClose }) => /* @__PURE__ */ jsx(ConnectedDock, {
27
+ client: useAssistantClient(url),
28
+ onClose
29
+ });
30
+ /** The dock for the backend `/meta` names, signed in as the user; `client` replaces it in stories and tests. */
31
+ const AssistantPanel = ({ url, client, onClose }) => client ? /* @__PURE__ */ jsx(ConnectedDock, {
32
+ client,
33
+ onClose
34
+ }) : /* @__PURE__ */ jsx(OwnDock, {
35
+ url,
36
+ onClose
37
+ });
38
+ //#endregion
39
+ export { AssistantButton, AssistantPanel };
@@ -1 +1 @@
1
- import "react";
1
+ import { Ref } from "react";
@@ -1,15 +1,15 @@
1
- import { DockItem } from "./model.js";
2
- import { DockItemHandlers } from "./dock-item.js";
1
+ import { AssistantState } from "@protobase/schema";
3
2
  //#region src/assistant-dock/assistant-dock.d.ts
4
- export type AssistantDockProps = DockItemHandlers & {
5
- /** The chat in order: messages and the cards of its tasks and queries. */
6
- items: DockItem[];
7
- /** The organization's credits; Approve needs the plan's quote. */
8
- balance: number;
9
- replying?: boolean;
3
+ export type AssistantDockProps = {
4
+ state: AssistantState;
5
+ /** The connection to the backend is down and being retried. */
6
+ offline?: boolean;
7
+ /** Why the last message or click was not delivered. */
8
+ error?: string;
10
9
  onSend?: (text: string) => void;
10
+ onAction?: (partId: string, actionId: string) => void;
11
11
  onClose?: () => void;
12
12
  };
13
- /** The assistant beside the app: chat, plans to approve, the queue, progress, results and query tables. */
14
- export declare const AssistantDock: ({ items, balance, replying, onSend, onClose, onRequestChanges, ...handlers }: AssistantDockProps) => import("react").JSX.Element;
13
+ /** The assistant beside the app: renders a backend's chat, its cards and status line, and sends messages and clicks back. */
14
+ export declare const AssistantDock: ({ state, offline, error, onSend, onAction, onClose }: AssistantDockProps) => import("react").JSX.Element;
15
15
  //#endregion
@@ -1,80 +1,45 @@
1
- import { Button } from "../primitives/button.js";
2
- import { Composer } from "./composer.js";
3
- import { formatCredits } from "./model.js";
4
- import { DockItemView } from "./dock-item.js";
5
- import { useEffect, useRef, useState } from "react";
6
- import { jsx, jsxs } from "react/jsx-runtime";
7
- import { Sparkles, X } from "lucide-react";
1
+ import { ChatMessage } from "../chat/chat-message.js";
2
+ import { ChatThread } from "../chat/chat-thread.js";
3
+ import { Composer } from "../chat/composer.js";
4
+ import { SidePanel } from "../side-panel/side-panel.js";
5
+ import { PartView } from "./part-view.js";
6
+ import { useEffect, useRef } from "react";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ import { Sparkles } from "lucide-react";
8
9
  //#region src/assistant-dock/assistant-dock.tsx
9
- /** The assistant beside the app: chat, plans to approve, the queue, progress, results and query tables. */
10
- const AssistantDock = ({ items, balance, replying, onSend, onClose, onRequestChanges, ...handlers }) => {
11
- const list = useRef(null);
10
+ const defaultPlaceholder = "Ask a question...";
11
+ /** The assistant beside the app: renders a backend's chat, its cards and status line, and sends messages and clicks back. */
12
+ const AssistantDock = ({ state, offline, error, onSend, onAction, onClose }) => {
12
13
  const input = useRef(null);
13
- const [revising, setRevising] = useState(false);
14
14
  useEffect(() => {
15
- const element = list.current;
16
- if (element) element.scrollTop = element.scrollHeight;
17
- }, [items.length]);
18
- const requestChanges = (taskId) => {
19
- setRevising(true);
20
- input.current?.focus();
21
- onRequestChanges?.(taskId);
22
- };
23
- const send = (text) => {
24
- setRevising(false);
25
- onSend?.(text);
26
- };
27
- return /* @__PURE__ */ jsxs("aside", {
28
- "aria-label": "Assistant",
29
- className: "flex h-full w-[380px] max-w-full shrink-0 flex-col border-l border-border bg-surface",
30
- children: [
31
- /* @__PURE__ */ jsxs("header", {
32
- className: "flex h-12 shrink-0 items-center gap-2 border-b border-border bg-background px-4",
33
- children: [
34
- /* @__PURE__ */ jsx(Sparkles, { className: "size-4 text-primary" }),
35
- /* @__PURE__ */ jsx("h2", {
36
- className: "flex-1 text-[13px] font-semibold",
37
- children: "Assistant"
38
- }),
39
- /* @__PURE__ */ jsx("span", {
40
- className: "text-xs font-medium text-muted-foreground",
41
- children: formatCredits(balance)
42
- }),
43
- onClose && /* @__PURE__ */ jsx(Button, {
44
- variant: "ghost",
45
- size: "sm",
46
- className: "w-7 px-0",
47
- "aria-label": "Close",
48
- onClick: onClose,
49
- children: /* @__PURE__ */ jsx(X, { className: "size-4" })
50
- })
51
- ]
52
- }),
53
- /* @__PURE__ */ jsxs("div", {
54
- ref: list,
55
- className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4",
56
- children: [items.length === 0 && /* @__PURE__ */ jsx("p", {
57
- className: "m-auto max-w-60 text-center text-[13px] text-muted-foreground",
58
- children: "Ask for a change to this app, or a question about its data."
59
- }), items.map((item) => /* @__PURE__ */ jsx(DockItemView, {
60
- item,
61
- balance,
62
- handlers: {
63
- ...handlers,
64
- onRequestChanges: requestChanges
65
- }
66
- }, item.id))]
67
- }),
68
- /* @__PURE__ */ jsx("footer", {
69
- className: "shrink-0 border-t border-border bg-background p-3",
70
- children: /* @__PURE__ */ jsx(Composer, {
71
- inputRef: input,
72
- onSend: send,
73
- replying,
74
- placeholder: revising ? "What should change in the plan?" : "Ask for a change or about your data..."
75
- })
76
- })
77
- ]
15
+ if (state.placeholder) input.current?.focus();
16
+ }, [state.placeholder]);
17
+ return /* @__PURE__ */ jsx(SidePanel, {
18
+ title: "Assistant",
19
+ icon: /* @__PURE__ */ jsx(Sparkles, { className: "size-4 text-primary" }),
20
+ status: offline ? "Reconnecting..." : state.status,
21
+ onClose,
22
+ footer: /* @__PURE__ */ jsxs(Fragment$1, { children: [error && /* @__PURE__ */ jsx("p", {
23
+ role: "alert",
24
+ className: "text-xs text-danger-text",
25
+ children: error
26
+ }), /* @__PURE__ */ jsx(Composer, {
27
+ inputRef: input,
28
+ onSend,
29
+ busy: state.replying,
30
+ placeholder: state.placeholder ?? defaultPlaceholder
31
+ })] }),
32
+ children: /* @__PURE__ */ jsx(ChatThread, {
33
+ length: state.messages.reduce((sum, message) => sum + message.parts.length, 0),
34
+ empty: "Ask a question about this app or its data.",
35
+ children: state.messages.map((message) => /* @__PURE__ */ jsx(ChatMessage, {
36
+ from: message.from,
37
+ children: message.parts.map((part) => /* @__PURE__ */ jsx(PartView, {
38
+ part,
39
+ onAction
40
+ }, part.id))
41
+ }, message.id))
42
+ })
78
43
  });
79
44
  };
80
45
  //#endregion
@@ -0,0 +1,5 @@
1
+ import { AssistantDock, AssistantDockProps } from "./assistant-dock.js";
2
+ import { PartView } from "./part-view.js";
3
+ import { useAssistant } from "./use-assistant.js";
4
+ import { useAssistantClient } from "./use-assistant-client.js";
5
+ export { AssistantDock, type AssistantDockProps, PartView, useAssistant, useAssistantClient };
@@ -0,0 +1,8 @@
1
+ import { AssistantPart } from "@protobase/schema";
2
+ //#region src/assistant-dock/part-view.d.ts
3
+ /** One part of a message, drawn with the primitive it names. */
4
+ export declare const PartView: ({ part, onAction }: {
5
+ part: AssistantPart;
6
+ onAction?: (partId: string, actionId: string) => void;
7
+ }) => import("react").JSX.Element;
8
+ //#endregion
@@ -0,0 +1,47 @@
1
+ import { ActionCard } from "../action-card/action-card.js";
2
+ import { FieldList } from "../action-card/field-list.js";
3
+ import { CompactTable } from "../compact-table/compact-table.js";
4
+ import { DiffView } from "../diff-view/diff-view.js";
5
+ import { StepList } from "../step-list/step-list.js";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/assistant-dock/part-view.tsx
8
+ /** One part of a message, drawn with the primitive it names. */
9
+ const PartView = ({ part, onAction }) => {
10
+ switch (part.type) {
11
+ case "text": return /* @__PURE__ */ jsx("p", { children: part.text });
12
+ case "table": return /* @__PURE__ */ jsx(CompactTable, {
13
+ columns: part.columns,
14
+ rows: part.rows,
15
+ caption: part.caption,
16
+ truncated: part.truncated
17
+ });
18
+ case "card": return /* @__PURE__ */ jsxs(ActionCard, {
19
+ title: part.title,
20
+ tone: part.tone,
21
+ badge: part.badge,
22
+ note: part.note,
23
+ actions: part.actions,
24
+ onAction: (actionId) => onAction?.(part.id, actionId),
25
+ children: [
26
+ part.body && /* @__PURE__ */ jsx("p", {
27
+ className: "text-[13px]",
28
+ children: part.body
29
+ }),
30
+ part.code && /* @__PURE__ */ jsx("pre", {
31
+ className: "overflow-x-auto rounded-md bg-surface px-2.5 py-2 font-mono text-[11px] whitespace-pre-wrap break-words",
32
+ children: part.code
33
+ }),
34
+ part.fields && part.fields.length > 0 && /* @__PURE__ */ jsx(FieldList, { fields: part.fields }),
35
+ part.steps && part.steps.length > 0 && /* @__PURE__ */ jsx(StepList, { steps: part.steps }),
36
+ part.diffs?.map((diff, index) => /* @__PURE__ */ jsx(DiffView, {
37
+ source: diff.source,
38
+ start: diff.start,
39
+ path: diff.path,
40
+ className: "rounded-md border border-border"
41
+ }, `${index}-${diff.path}`))
42
+ ]
43
+ });
44
+ }
45
+ };
46
+ //#endregion
47
+ export { PartView };
@@ -0,0 +1,4 @@
1
+ //#region src/assistant-dock/use-assistant-client.d.ts
2
+ /** A client of the backend at `url`, authenticated as the signed-in user; `undefined` until their token is there. */
3
+ export declare const useAssistantClient: (url: string) => import("@protobase/client").AssistantClient | undefined;
4
+ //#endregion
@@ -0,0 +1,17 @@
1
+ import { useApiToken } from "../auth/use-api-token.js";
2
+ import { useMemo, useRef } from "react";
3
+ import { createAssistantClient } from "@protobase/client";
4
+ //#region src/assistant-dock/use-assistant-client.ts
5
+ /** A client of the backend at `url`, authenticated as the signed-in user; `undefined` until their token is there. */
6
+ const useAssistantClient = (url) => {
7
+ const token = useApiToken();
8
+ const latest = useRef(token);
9
+ latest.current = token;
10
+ const ready = token !== void 0;
11
+ return useMemo(() => ready ? createAssistantClient({
12
+ url,
13
+ token: () => latest.current
14
+ }) : void 0, [url, ready]);
15
+ };
16
+ //#endregion
17
+ export { useAssistantClient };
@@ -0,0 +1,11 @@
1
+ import { AssistantClient } from "@protobase/client";
2
+ //#region src/assistant-dock/use-assistant.d.ts
3
+ /** Follows an assistant backend's event stream into state, with its two requests; `undefined` client means not yet. */
4
+ export declare const useAssistant: (client: AssistantClient | undefined) => {
5
+ state: import("@protobase/schema").AssistantState;
6
+ offline: boolean;
7
+ error: string | undefined;
8
+ send: (text: string) => void | undefined;
9
+ act: (partId: string, actionId: string) => void | undefined;
10
+ };
11
+ //#endregion
@@ -0,0 +1,27 @@
1
+ import { useEffect, useState } from "react";
2
+ import { applyAssistantEvent, emptyAssistantState } from "@protobase/schema";
3
+ //#region src/assistant-dock/use-assistant.ts
4
+ const messageOf = (error) => error instanceof Error ? error.message : "The assistant did not answer";
5
+ /** Follows an assistant backend's event stream into state, with its two requests; `undefined` client means not yet. */
6
+ const useAssistant = (client) => {
7
+ const [state, setState] = useState(emptyAssistantState);
8
+ const [offline, setOffline] = useState(false);
9
+ const [error, setError] = useState();
10
+ useEffect(() => {
11
+ if (!client) return;
12
+ return client.connect((event) => setState((current) => applyAssistantEvent(current, event)), (connection) => setOffline(connection.state === "retrying"));
13
+ }, [client]);
14
+ const deliver = (request) => {
15
+ setError(void 0);
16
+ request.catch((reason) => setError(messageOf(reason)));
17
+ };
18
+ return {
19
+ state,
20
+ offline,
21
+ error,
22
+ send: (text) => client && deliver(client.send(text)),
23
+ act: (partId, actionId) => client && deliver(client.act(partId, actionId))
24
+ };
25
+ };
26
+ //#endregion
27
+ export { useAssistant };
@@ -0,0 +1,7 @@
1
+ //#region src/auth/use-api-token.d.ts
2
+ /**
3
+ * The signed-in user's API token (a JWT from `/api/auth/token`), for calling another service as them. It changes
4
+ * before it expires, re-rendering the component; `undefined` while it loads and when nobody is signed in.
5
+ */
6
+ export declare const useApiToken: () => string | undefined;
7
+ //#endregion
@@ -0,0 +1,37 @@
1
+ import { useAuth } from "./auth-provider.js";
2
+ import { useEffect, useState } from "react";
3
+ import { tokenExpiry } from "@protobase/client";
4
+ //#region src/auth/use-api-token.ts
5
+ /** Asks for the token again this long before it expires; the session hands out a fresh one by then. */
6
+ const refreshLeadMs = 3e4;
7
+ /** How long until the hook reads `token` again: shortly before it expires, and never sooner than a second. */
8
+ const tokenRefreshDelay = (token, now = Date.now()) => Math.max(1e3, tokenExpiry(token) - now - refreshLeadMs);
9
+ /**
10
+ * The signed-in user's API token (a JWT from `/api/auth/token`), for calling another service as them. It changes
11
+ * before it expires, re-rendering the component; `undefined` while it loads and when nobody is signed in.
12
+ */
13
+ const useApiToken = () => {
14
+ const { state, session } = useAuth();
15
+ const signedIn = state.kind === "signed-in";
16
+ const [token, setToken] = useState();
17
+ useEffect(() => {
18
+ if (!signedIn) return;
19
+ let active = true;
20
+ let timer;
21
+ const read = async (previous) => {
22
+ const value = await session.token();
23
+ const fresh = value !== void 0 && value === previous ? await session.token({ refresh: true }) : value;
24
+ if (!active) return;
25
+ setToken(fresh);
26
+ if (fresh) timer = setTimeout(() => void read(fresh), tokenRefreshDelay(fresh));
27
+ };
28
+ read();
29
+ return () => {
30
+ active = false;
31
+ clearTimeout(timer);
32
+ };
33
+ }, [session, signedIn]);
34
+ return signedIn ? token : void 0;
35
+ };
36
+ //#endregion
37
+ export { tokenRefreshDelay, useApiToken };
@@ -0,0 +1,8 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/chat/chat-message.d.ts
3
+ /** One message of a chat: the user's as a bubble on the right, the other side's as plain content, cards included. */
4
+ export declare const ChatMessage: ({ from, children }: {
5
+ from: "user" | "assistant";
6
+ children: ReactNode;
7
+ }) => import("react").JSX.Element;
8
+ //#endregion
@@ -0,0 +1,11 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/chat/chat-message.tsx
4
+ /** One message of a chat: the user's as a bubble on the right, the other side's as plain content, cards included. */
5
+ const ChatMessage = ({ from, children }) => /* @__PURE__ */ jsx("div", {
6
+ "data-from": from,
7
+ className: cn("flex min-w-0 flex-col gap-2 whitespace-pre-wrap break-words text-[13px]", from === "user" ? "ml-8 self-end rounded-lg rounded-br-sm bg-primary px-3 py-2 text-primary-foreground shadow-sm" : "mr-4 text-foreground"),
8
+ children
9
+ });
10
+ //#endregion
11
+ export { ChatMessage };
@@ -0,0 +1,12 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/chat/chat-thread.d.ts
3
+ export type ChatThreadProps = {
4
+ children: ReactNode;
5
+ /** Shown centred while the chat is empty. */
6
+ empty?: ReactNode;
7
+ /** Changes whenever the chat grows, which scrolls it to the end. */
8
+ length: number;
9
+ };
10
+ /** The scrolling list of a chat's messages, kept at the newest one as messages arrive. */
11
+ export declare const ChatThread: ({ children, empty, length }: ChatThreadProps) => import("react").JSX.Element;
12
+ //#endregion