@aganzefelicite/responsekit-react 0.1.0 → 0.2.1

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 CHANGED
@@ -15,21 +15,16 @@ components — safely, and without ever crashing on unknown or invalid data.
15
15
 
16
16
  ## Install
17
17
 
18
- Not published to npm yet — install from a local build of this repo (`@responsekit/schema`
19
- is bundled in, so it's a single tarball):
20
-
21
18
  ```bash
22
- # in the repo root
23
- pnpm pack:local
24
- # in your app
25
- pnpm add /ABS/PATH/responsekit-react-0.0.0.tgz react react-dom
19
+ pnpm add @aganzefelicite/responsekit-react react react-dom
26
20
  ```
27
21
 
28
22
  ```ts
29
23
  import "@aganzefelicite/responsekit-react/styles.css"; // optional default styling
30
24
  ```
31
25
 
32
- See [docs/INTEGRATION.md](../../docs/INTEGRATION.md) for `pnpm link` and publishing options.
26
+ The response-contract package (types + zod validators + SSE reducer) is bundled in and
27
+ re-exported, so there's nothing else to install. `react`/`react-dom` are peer deps (>=18).
33
28
 
34
29
  ## Quick start
35
30
 
@@ -52,8 +47,9 @@ backend). The message endpoint is POST + `text/event-stream`; the SDK drives
52
47
 
53
48
  ## API
54
49
 
55
- **Components** — `AiRenderer`, `AiStream`
50
+ **Components** — `AiRenderer`, `AiStream`, `AiWidget` (draggable/resizable pop-up)
56
51
  **Hooks** — `useAiStream`, `useChat`
52
+ **Actions** — `withDefaultActions`, `runDefaultAction`, `performDownload`, `performOpenLink`, `triggerDownload`
57
53
  **Registry** — `registerBlock`, `unregisterBlock`, `resolveBlockComponent`
58
54
  **Default blocks** — `TextBlock`, `KpiBlock`, `ChartBlock`, `TableBlock`, `InsightBlock`,
59
55
  `RecommendationBlock`, `FilterBlock`, `FallbackBlock`, `defaultBlockComponents`
@@ -77,10 +73,32 @@ registerBlock({ type: "MAP", component: RwandaMap });
77
73
  Resolution order per block type: `components` prop → globally registered → built-in default
78
74
  → graceful fallback.
79
75
 
76
+ ## Floating widget
77
+
78
+ Wrap any content in a draggable, resizable pop-up with a launcher button:
79
+
80
+ ```tsx
81
+ import { AiWidget } from "@aganzefelicite/responsekit-react";
82
+
83
+ <AiWidget title="Assistant" defaultOpen>
84
+ {/* useChat + AiRenderer, a single AiRenderer, or your own chat UI */}
85
+ </AiWidget>;
86
+ ```
87
+
88
+ Drag by the header, resize from the bottom-right corner, toggle via the launcher (or
89
+ control `open`/`onOpenChange`). Chrome only — it never touches your data flow.
90
+
80
91
  ## Actions
81
92
 
82
93
  Blocks emit intent through one `onAction(action)` callback (`DOWNLOAD`, `OPEN_LINK`,
83
- `REFRESH`, `CUSTOM`, or host-defined). The SDK never runs business logic.
94
+ `REFRESH`, `CUSTOM`, or host-defined). The SDK never runs business logic — but for the two
95
+ actions with an obvious browser behavior it ships a default. Wrap your handler with
96
+ `withDefaultActions` and CSV/JSON/Blob downloads and link-opens work out of the box:
97
+
98
+ ```tsx
99
+ import { withDefaultActions } from "@aganzefelicite/responsekit-react";
100
+ <AiRenderer response={r} onAction={withDefaultActions((a) => track(a))} />;
101
+ ```
84
102
 
85
103
  ## License
86
104
 
package/dist/index.cjs CHANGED
@@ -4981,9 +4981,259 @@ function useChat(options) {
4981
4981
  react.useEffect(() => () => abortRef.current?.abort(), []);
4982
4982
  return { conversationId, turns, isStreaming, error, send, stop, reset, newConversation };
4983
4983
  }
4984
+ var MARGIN = 24;
4985
+ var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
4986
+ var viewport = () => ({
4987
+ w: typeof window === "undefined" ? 1024 : window.innerWidth,
4988
+ h: typeof window === "undefined" ? 768 : window.innerHeight
4989
+ });
4990
+ function AiWidget({
4991
+ children,
4992
+ title = "Assistant",
4993
+ headerActions,
4994
+ defaultOpen = false,
4995
+ open: openProp,
4996
+ onOpenChange,
4997
+ initialPosition,
4998
+ initialSize = { width: 384, height: 560 },
4999
+ minWidth = 300,
5000
+ minHeight = 360,
5001
+ showLauncher = true,
5002
+ launcher,
5003
+ className
5004
+ }) {
5005
+ const isControlled = openProp !== void 0;
5006
+ const [openState, setOpenState] = react.useState(defaultOpen);
5007
+ const open = isControlled ? openProp : openState;
5008
+ const setOpen = react.useCallback(
5009
+ (next) => {
5010
+ if (!isControlled) setOpenState(next);
5011
+ onOpenChange?.(next);
5012
+ },
5013
+ [isControlled, onOpenChange]
5014
+ );
5015
+ const [size, setSize] = react.useState(initialSize);
5016
+ const [pos, setPos] = react.useState(initialPosition ?? null);
5017
+ react.useEffect(() => {
5018
+ if (pos) return;
5019
+ const { w, h } = viewport();
5020
+ setPos({
5021
+ x: Math.max(MARGIN, w - size.width - MARGIN),
5022
+ y: Math.max(MARGIN, h - size.height - MARGIN)
5023
+ });
5024
+ }, [pos]);
5025
+ react.useEffect(() => {
5026
+ if (typeof window === "undefined") return;
5027
+ const onResize = () => {
5028
+ setPos((p) => {
5029
+ if (!p) return p;
5030
+ const { w, h } = viewport();
5031
+ return {
5032
+ x: clamp(p.x, 0, Math.max(0, w - size.width)),
5033
+ y: clamp(p.y, 0, Math.max(0, h - size.height))
5034
+ };
5035
+ });
5036
+ };
5037
+ window.addEventListener("resize", onResize);
5038
+ return () => window.removeEventListener("resize", onResize);
5039
+ }, [size.width, size.height]);
5040
+ const dragRef = react.useRef(null);
5041
+ const onHeaderPointerDown = (e) => {
5042
+ if (e.button !== 0 || !pos) return;
5043
+ if (e.target.closest("button, a, input, select")) return;
5044
+ dragRef.current = { px: e.clientX, py: e.clientY, ox: pos.x, oy: pos.y };
5045
+ e.currentTarget.setPointerCapture(e.pointerId);
5046
+ };
5047
+ const onHeaderPointerMove = (e) => {
5048
+ const d = dragRef.current;
5049
+ if (!d) return;
5050
+ const { w, h } = viewport();
5051
+ setPos({
5052
+ x: clamp(d.ox + (e.clientX - d.px), 0, Math.max(0, w - size.width)),
5053
+ y: clamp(d.oy + (e.clientY - d.py), 0, Math.max(0, h - size.height))
5054
+ });
5055
+ };
5056
+ const endHeaderPointer = (e) => {
5057
+ dragRef.current = null;
5058
+ if (e.currentTarget.hasPointerCapture?.(e.pointerId)) {
5059
+ e.currentTarget.releasePointerCapture(e.pointerId);
5060
+ }
5061
+ };
5062
+ const resizeRef = react.useRef(null);
5063
+ const onResizePointerDown = (e) => {
5064
+ if (e.button !== 0) return;
5065
+ e.stopPropagation();
5066
+ resizeRef.current = { px: e.clientX, py: e.clientY, ow: size.width, oh: size.height };
5067
+ e.currentTarget.setPointerCapture(e.pointerId);
5068
+ };
5069
+ const onResizePointerMove = (e) => {
5070
+ const r = resizeRef.current;
5071
+ if (!r || !pos) return;
5072
+ const { w, h } = viewport();
5073
+ setSize({
5074
+ width: clamp(r.ow + (e.clientX - r.px), minWidth, w - pos.x - MARGIN / 2),
5075
+ height: clamp(r.oh + (e.clientY - r.py), minHeight, h - pos.y - MARGIN / 2)
5076
+ });
5077
+ };
5078
+ const endResizePointer = (e) => {
5079
+ resizeRef.current = null;
5080
+ if (e.currentTarget.hasPointerCapture?.(e.pointerId)) {
5081
+ e.currentTarget.releasePointerCapture(e.pointerId);
5082
+ }
5083
+ };
5084
+ if (!open) {
5085
+ if (!showLauncher) return null;
5086
+ return /* @__PURE__ */ jsxRuntime.jsx(
5087
+ "button",
5088
+ {
5089
+ type: "button",
5090
+ className: "rk-widget-launcher",
5091
+ onClick: () => setOpen(true),
5092
+ "aria-label": `Open ${title}`,
5093
+ children: launcher ?? /* @__PURE__ */ jsxRuntime.jsx(LauncherIcon, {})
5094
+ }
5095
+ );
5096
+ }
5097
+ const panelStyle = pos ? {
5098
+ left: pos.x,
5099
+ top: pos.y,
5100
+ width: size.width,
5101
+ height: size.height
5102
+ } : { visibility: "hidden" };
5103
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5104
+ "div",
5105
+ {
5106
+ className: `rk-widget${className ? ` ${className}` : ""}`,
5107
+ style: panelStyle,
5108
+ role: "dialog",
5109
+ "aria-label": title,
5110
+ children: [
5111
+ /* @__PURE__ */ jsxRuntime.jsxs(
5112
+ "div",
5113
+ {
5114
+ className: "rk-widget-header",
5115
+ onPointerDown: onHeaderPointerDown,
5116
+ onPointerMove: onHeaderPointerMove,
5117
+ onPointerUp: endHeaderPointer,
5118
+ onPointerCancel: endHeaderPointer,
5119
+ children: [
5120
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rk-widget-title", children: title }),
5121
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rk-widget-header-actions", children: [
5122
+ headerActions,
5123
+ /* @__PURE__ */ jsxRuntime.jsx(
5124
+ "button",
5125
+ {
5126
+ type: "button",
5127
+ className: "rk-widget-close",
5128
+ onClick: () => setOpen(false),
5129
+ "aria-label": `Close ${title}`,
5130
+ children: "\xD7"
5131
+ }
5132
+ )
5133
+ ] })
5134
+ ]
5135
+ }
5136
+ ),
5137
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rk-widget-body", children }),
5138
+ /* @__PURE__ */ jsxRuntime.jsx(
5139
+ "div",
5140
+ {
5141
+ className: "rk-widget-resize",
5142
+ onPointerDown: onResizePointerDown,
5143
+ onPointerMove: onResizePointerMove,
5144
+ onPointerUp: endResizePointer,
5145
+ onPointerCancel: endResizePointer,
5146
+ role: "separator",
5147
+ "aria-label": "Resize",
5148
+ "aria-orientation": "horizontal"
5149
+ }
5150
+ )
5151
+ ]
5152
+ }
5153
+ );
5154
+ }
5155
+ function LauncherIcon() {
5156
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(
5157
+ "path",
5158
+ {
5159
+ d: "M21 11.5a8.38 8.38 0 0 1-8.5 8.5 9.3 9.3 0 0 1-4-.9L3 21l1.9-5.5a8.38 8.38 0 0 1-.9-4A8.5 8.5 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5Z",
5160
+ stroke: "currentColor",
5161
+ strokeWidth: "1.8",
5162
+ strokeLinecap: "round",
5163
+ strokeLinejoin: "round"
5164
+ }
5165
+ ) });
5166
+ }
5167
+
5168
+ // src/download.ts
5169
+ var MIME = {
5170
+ CSV: "text/csv",
5171
+ JSON: "application/json",
5172
+ TXT: "text/plain"
5173
+ };
5174
+ function inBrowser() {
5175
+ return typeof document !== "undefined" && typeof URL !== "undefined";
5176
+ }
5177
+ function triggerDownload(blob, filename) {
5178
+ if (!inBrowser()) return;
5179
+ const url = URL.createObjectURL(blob);
5180
+ const a = document.createElement("a");
5181
+ a.href = url;
5182
+ a.download = filename;
5183
+ a.rel = "noopener";
5184
+ document.body.appendChild(a);
5185
+ a.click();
5186
+ a.remove();
5187
+ setTimeout(() => URL.revokeObjectURL(url), 0);
5188
+ }
5189
+ function performDownload(action) {
5190
+ if (!inBrowser()) return false;
5191
+ const { data, format, filename } = action;
5192
+ if (data === null || data === void 0) return false;
5193
+ const fmt = typeof format === "string" ? format.toUpperCase() : void 0;
5194
+ let blob;
5195
+ let name = filename;
5196
+ if (data instanceof Blob) {
5197
+ blob = data;
5198
+ name ?? (name = "download");
5199
+ } else if (typeof data === "string") {
5200
+ const mime = fmt && MIME[fmt] || "text/plain";
5201
+ blob = new Blob([data], { type: `${mime};charset=utf-8` });
5202
+ name ?? (name = `download.${(fmt ?? "TXT").toLowerCase()}`);
5203
+ } else {
5204
+ blob = new Blob([JSON.stringify(data, null, 2)], {
5205
+ type: "application/json;charset=utf-8"
5206
+ });
5207
+ name ?? (name = "download.json");
5208
+ }
5209
+ triggerDownload(blob, name);
5210
+ return true;
5211
+ }
5212
+ function performOpenLink(action) {
5213
+ if (typeof window === "undefined" || !action.href) return false;
5214
+ window.open(action.href, action.target ?? "_blank", "noopener,noreferrer");
5215
+ return true;
5216
+ }
5217
+ function runDefaultAction(action) {
5218
+ switch (action.type) {
5219
+ case "DOWNLOAD":
5220
+ return performDownload(action);
5221
+ case "OPEN_LINK":
5222
+ return performOpenLink(action);
5223
+ default:
5224
+ return false;
5225
+ }
5226
+ }
5227
+ function withDefaultActions(onAction) {
5228
+ return (action) => {
5229
+ runDefaultAction(action);
5230
+ onAction?.(action);
5231
+ };
5232
+ }
4984
5233
 
4985
5234
  exports.AiRenderer = AiRenderer;
4986
5235
  exports.AiStream = AiStream;
5236
+ exports.AiWidget = AiWidget;
4987
5237
  exports.ChartBlock = ChartBlock;
4988
5238
  exports.ErrorBoundary = ErrorBoundary;
4989
5239
  exports.ErrorView = ErrorView;
@@ -5024,6 +5274,8 @@ exports.noopAction = noopAction;
5024
5274
  exports.parseAiResponse = parseAiResponse;
5025
5275
  exports.parseBlock = parseBlock;
5026
5276
  exports.parseSseEvent = parseSseEvent;
5277
+ exports.performDownload = performDownload;
5278
+ exports.performOpenLink = performOpenLink;
5027
5279
  exports.progressEventSchema = progressEventSchema;
5028
5280
  exports.progressPhaseSchema = progressPhaseSchema;
5029
5281
  exports.recommendationBlockSchema = recommendationBlockSchema;
@@ -5036,6 +5288,7 @@ exports.responseEndSchema = responseEndSchema;
5036
5288
  exports.responseMetadataSchema = responseMetadataSchema;
5037
5289
  exports.responseStartSchema = responseStartSchema;
5038
5290
  exports.responseTypeSchema = responseTypeSchema;
5291
+ exports.runDefaultAction = runDefaultAction;
5039
5292
  exports.safeParseAiResponse = safeParseAiResponse;
5040
5293
  exports.severitySchema = severitySchema;
5041
5294
  exports.streamMessage = streamMessage;
@@ -5046,8 +5299,10 @@ exports.textBlockSchema = textBlockSchema;
5046
5299
  exports.textFormatSchema = textFormatSchema;
5047
5300
  exports.textResponseSchema = textResponseSchema;
5048
5301
  exports.trendSchema = trendSchema;
5302
+ exports.triggerDownload = triggerDownload;
5049
5303
  exports.unregisterBlock = unregisterBlock;
5050
5304
  exports.useAiStream = useAiStream;
5051
5305
  exports.useChat = useChat;
5306
+ exports.withDefaultActions = withDefaultActions;
5052
5307
  //# sourceMappingURL=index.cjs.map
5053
5308
  //# sourceMappingURL=index.cjs.map