@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.
- package/dist/action-card/action-card.d.ts +26 -0
- package/dist/action-card/action-card.js +93 -0
- package/dist/action-card/field-list.d.ts +9 -0
- package/dist/action-card/field-list.js +18 -0
- package/dist/action-card/index.d.ts +3 -0
- package/dist/app/app.d.ts +3 -1
- package/dist/app/app.js +20 -7
- package/dist/app/assistant-shell.js +39 -0
- package/dist/app-shell/global-search.d.ts +1 -1
- package/dist/assistant-dock/assistant-dock.d.ts +10 -10
- package/dist/assistant-dock/assistant-dock.js +39 -74
- package/dist/assistant-dock/index.d.ts +5 -0
- package/dist/assistant-dock/part-view.d.ts +8 -0
- package/dist/assistant-dock/part-view.js +47 -0
- package/dist/assistant-dock/use-assistant-client.d.ts +4 -0
- package/dist/assistant-dock/use-assistant-client.js +17 -0
- package/dist/assistant-dock/use-assistant.d.ts +11 -0
- package/dist/assistant-dock/use-assistant.js +27 -0
- package/dist/auth/use-api-token.d.ts +7 -0
- package/dist/auth/use-api-token.js +37 -0
- package/dist/chat/chat-message.d.ts +8 -0
- package/dist/chat/chat-message.js +11 -0
- package/dist/chat/chat-thread.d.ts +12 -0
- package/dist/chat/chat-thread.js +23 -0
- package/dist/chat/composer.d.ts +12 -0
- package/dist/{assistant-dock → chat}/composer.js +5 -4
- package/dist/chat/index.d.ts +4 -0
- package/dist/compact-table/compact-table.d.ts +13 -0
- package/dist/compact-table/compact-table.js +49 -0
- package/dist/compact-table/format-cell.js +9 -0
- package/dist/data/meta-data.d.ts +4 -0
- package/dist/data/use-meta.js +2 -1
- package/dist/diff-view/diff-view.d.ts +13 -0
- package/dist/diff-view/diff-view.js +62 -0
- package/dist/diff-view/diff.d.ts +13 -0
- package/dist/diff-view/diff.js +29 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.js +16 -2
- package/dist/side-panel/index.d.ts +2 -0
- package/dist/side-panel/side-panel.d.ts +17 -0
- package/dist/side-panel/side-panel.js +40 -0
- package/dist/step-list/step-list.d.ts +12 -0
- package/dist/step-list/step-list.js +40 -0
- package/package.json +4 -4
- package/dist/assistant-dock/chat-message.js +0 -9
- package/dist/assistant-dock/dock-item.d.ts +0 -4
- package/dist/assistant-dock/dock-item.js +0 -37
- package/dist/assistant-dock/model.d.ts +0 -84
- package/dist/assistant-dock/model.js +0 -26
- package/dist/assistant-dock/plan-card.d.ts +0 -13
- package/dist/assistant-dock/plan-card.js +0 -133
- package/dist/assistant-dock/progress-card.js +0 -18
- package/dist/assistant-dock/query-card.js +0 -52
- package/dist/assistant-dock/queue-card.js +0 -31
- package/dist/assistant-dock/result-card.js +0 -77
- package/dist/assistant-panel/content.d.ts +0 -7
- 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,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 };
|
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 {
|
|
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 =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,
|
|
14
|
-
export declare const AssistantDock: ({
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|