@ai-me-chat/react 0.1.0 → 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/README.md +159 -0
- package/dist/index.cjs +47 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -10
- package/dist/index.d.ts +118 -10
- package/dist/index.js +47 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -42,6 +42,165 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
42
42
|
- **`useAIMe()`** — full chat state (messages, input, submit) for custom UIs
|
|
43
43
|
- **`useAIMeContext()`** — access provider context
|
|
44
44
|
|
|
45
|
+
## Syncing Client State After Tool Execution
|
|
46
|
+
|
|
47
|
+
When the AI executes a tool that mutates data (POST/PUT/DELETE), your client-side
|
|
48
|
+
state may be stale. Use `onToolComplete` to trigger a refresh and `onMessageComplete`
|
|
49
|
+
to know when the full response is done:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
"use client";
|
|
53
|
+
|
|
54
|
+
import { useRouter } from "next/navigation";
|
|
55
|
+
import { AIMeProvider, AIMeChat } from "@ai-me-chat/react";
|
|
56
|
+
|
|
57
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
58
|
+
const router = useRouter();
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<AIMeProvider endpoint="/api/ai-me">
|
|
62
|
+
{children}
|
|
63
|
+
<AIMeChat
|
|
64
|
+
onToolComplete={(tool) => {
|
|
65
|
+
// Refresh the page whenever the AI calls a mutating tool.
|
|
66
|
+
// You can narrow by tool.name or tool.httpMethod for finer control.
|
|
67
|
+
router.refresh();
|
|
68
|
+
}}
|
|
69
|
+
onMessageComplete={(message) => {
|
|
70
|
+
// The assistant finished its full response — all tool calls are done.
|
|
71
|
+
console.log("Assistant reply:", message.content);
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
</AIMeProvider>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`onToolComplete` fires once per tool execution, immediately after the result
|
|
80
|
+
is available in the message stream. Fields:
|
|
81
|
+
|
|
82
|
+
| Field | Type | Description |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `name` | `string` | Tool (function) name |
|
|
85
|
+
| `httpMethod` | `string \| undefined` | HTTP method, if surfaced |
|
|
86
|
+
| `path` | `string \| undefined` | API path called, if surfaced |
|
|
87
|
+
| `result` | `unknown` | Raw tool result |
|
|
88
|
+
| `requiresConfirmation` | `boolean \| undefined` | Whether confirmation was required |
|
|
89
|
+
|
|
90
|
+
`onMessageComplete` fires once when the assistant finishes a full response
|
|
91
|
+
(status transitions from `"streaming"` to `"ready"`). Fields:
|
|
92
|
+
|
|
93
|
+
| Field | Type | Description |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `role` | `string` | Always `"assistant"` |
|
|
96
|
+
| `content` | `string` | Concatenated text content |
|
|
97
|
+
| `toolCalls` | `unknown[] \| undefined` | Tool-call parts, if any |
|
|
98
|
+
|
|
99
|
+
## Custom Confirmation Rendering
|
|
100
|
+
|
|
101
|
+
By default, AI-Me shows its built-in `<AIMeConfirm>` dialog before executing
|
|
102
|
+
any tool that requires user confirmation (destructive actions, etc.).
|
|
103
|
+
|
|
104
|
+
Use `renderConfirmation` on `<AIMeChat>` to replace the default dialog with
|
|
105
|
+
your own UI — a branded modal, a slide-over panel, an inline card, whatever
|
|
106
|
+
fits your design system:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
"use client";
|
|
110
|
+
|
|
111
|
+
import { AIMeProvider, AIMeChat } from "@ai-me-chat/react";
|
|
112
|
+
|
|
113
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
114
|
+
return (
|
|
115
|
+
<AIMeProvider endpoint="/api/ai-me">
|
|
116
|
+
{children}
|
|
117
|
+
<AIMeChat
|
|
118
|
+
renderConfirmation={({ tool, params, onConfirm, onCancel }) => (
|
|
119
|
+
<MyConfirmModal
|
|
120
|
+
title={`Run "${tool.name}"?`}
|
|
121
|
+
description={tool.description}
|
|
122
|
+
details={`${tool.httpMethod} ${tool.path}`}
|
|
123
|
+
params={params}
|
|
124
|
+
onConfirm={onConfirm}
|
|
125
|
+
onCancel={onCancel}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
/>
|
|
129
|
+
</AIMeProvider>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The `renderConfirmation` callback receives:
|
|
135
|
+
|
|
136
|
+
| Prop | Type | Description |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `tool.name` | `string` | Tool (function) name |
|
|
139
|
+
| `tool.httpMethod` | `string` | HTTP method, e.g. `"POST"` |
|
|
140
|
+
| `tool.path` | `string` | API path, e.g. `"/api/projects"` |
|
|
141
|
+
| `tool.description` | `string` | Human-readable description |
|
|
142
|
+
| `params` | `Record<string, unknown>` | Resolved call parameters |
|
|
143
|
+
| `onConfirm` | `() => void` | Call to proceed with execution |
|
|
144
|
+
| `onCancel` | `() => void` | Call to abort |
|
|
145
|
+
|
|
146
|
+
If `renderConfirmation` is omitted, the default dialog is used.
|
|
147
|
+
|
|
148
|
+
## Navigation Intents (Action Callbacks)
|
|
149
|
+
|
|
150
|
+
Sometimes the AI should guide the UI — navigate to a route, pre-fill a form,
|
|
151
|
+
open a modal — rather than making an API call directly. Use the `onAction`
|
|
152
|
+
prop on `<AIMeProvider>` to handle these client-side intents:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
"use client";
|
|
156
|
+
|
|
157
|
+
import { useRouter } from "next/navigation";
|
|
158
|
+
import { AIMeProvider, AIMeChat } from "@ai-me-chat/react";
|
|
159
|
+
|
|
160
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
161
|
+
const router = useRouter();
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<AIMeProvider
|
|
165
|
+
endpoint="/api/ai-me"
|
|
166
|
+
onAction={(action) => {
|
|
167
|
+
switch (action.type) {
|
|
168
|
+
case "navigate":
|
|
169
|
+
router.push(action.href as string);
|
|
170
|
+
break;
|
|
171
|
+
case "prefill":
|
|
172
|
+
// Broadcast to a form using a custom event, context, or state manager
|
|
173
|
+
window.dispatchEvent(
|
|
174
|
+
new CustomEvent("ai-me:prefill", { detail: action.fields }),
|
|
175
|
+
);
|
|
176
|
+
break;
|
|
177
|
+
case "open-modal":
|
|
178
|
+
// Open whichever modal the AI identified
|
|
179
|
+
openModal(action.modalId as string);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
{children}
|
|
185
|
+
<AIMeChat />
|
|
186
|
+
</AIMeProvider>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The `onAction` callback receives an object with at least a `type` field plus
|
|
192
|
+
any additional payload the tool provides:
|
|
193
|
+
|
|
194
|
+
| Field | Type | Description |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| `type` | `string` | Action kind — `"navigate"`, `"prefill"`, `"open-modal"`, etc. |
|
|
197
|
+
| `...rest` | `unknown` | Flexible payload defined per action type |
|
|
198
|
+
|
|
199
|
+
The `onAction` callback is stored in context and available to any component
|
|
200
|
+
via `useAIMeContext().onAction`. The actual tool registrations that emit these
|
|
201
|
+
actions live in your AI-Me handler (server-side), so client and server
|
|
202
|
+
concerns stay separated.
|
|
203
|
+
|
|
45
204
|
## Theming
|
|
46
205
|
|
|
47
206
|
```tsx
|
package/dist/index.cjs
CHANGED
|
@@ -44,8 +44,8 @@ function useAIMeContext() {
|
|
|
44
44
|
|
|
45
45
|
// src/provider.tsx
|
|
46
46
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
47
|
-
function AIMeProvider({ endpoint, headers, children }) {
|
|
48
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AIMeContext, { value: { endpoint, headers }, children });
|
|
47
|
+
function AIMeProvider({ endpoint, headers, onAction, children }) {
|
|
48
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AIMeContext, { value: { endpoint, headers, onAction }, children });
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// src/chat.tsx
|
|
@@ -356,13 +356,17 @@ function AIMeChat({
|
|
|
356
356
|
welcomeMessage = "Hi! I can help you navigate and use this app. What would you like to do?",
|
|
357
357
|
suggestedPrompts,
|
|
358
358
|
defaultOpen = false,
|
|
359
|
-
onToggle
|
|
359
|
+
onToggle,
|
|
360
|
+
onToolComplete,
|
|
361
|
+
onMessageComplete
|
|
360
362
|
}) {
|
|
361
363
|
const [open, setOpen] = (0, import_react4.useState)(defaultOpen);
|
|
362
364
|
const messagesEndRef = (0, import_react4.useRef)(null);
|
|
363
365
|
const inputRef = (0, import_react4.useRef)(null);
|
|
364
366
|
const panelRef = (0, import_react4.useRef)(null);
|
|
365
367
|
const triggerRef = (0, import_react4.useRef)(null);
|
|
368
|
+
const firedToolResults = (0, import_react4.useRef)(/* @__PURE__ */ new Set());
|
|
369
|
+
const prevStatus = (0, import_react4.useRef)(null);
|
|
366
370
|
const {
|
|
367
371
|
messages,
|
|
368
372
|
input,
|
|
@@ -393,6 +397,44 @@ function AIMeChat({
|
|
|
393
397
|
(0, import_react4.useEffect)(() => {
|
|
394
398
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
395
399
|
}, [messages]);
|
|
400
|
+
(0, import_react4.useEffect)(() => {
|
|
401
|
+
if (!onToolComplete) return;
|
|
402
|
+
for (const message of messages) {
|
|
403
|
+
for (const part of message.parts) {
|
|
404
|
+
if (part.type !== "tool-result") continue;
|
|
405
|
+
const id = part.toolCallId;
|
|
406
|
+
const dedupeKey = id ?? `${message.id}:${part.type}`;
|
|
407
|
+
if (firedToolResults.current.has(dedupeKey)) continue;
|
|
408
|
+
firedToolResults.current.add(dedupeKey);
|
|
409
|
+
onToolComplete({
|
|
410
|
+
name: part.toolName ?? "",
|
|
411
|
+
result: part.result
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}, [messages, onToolComplete]);
|
|
416
|
+
(0, import_react4.useEffect)(() => {
|
|
417
|
+
const prev = prevStatus.current;
|
|
418
|
+
prevStatus.current = status;
|
|
419
|
+
if (!onMessageComplete) return;
|
|
420
|
+
if (status !== "ready") return;
|
|
421
|
+
if (prev !== "streaming" && prev !== "submitted") return;
|
|
422
|
+
let lastAssistant;
|
|
423
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
424
|
+
if (messages[i].role === "assistant") {
|
|
425
|
+
lastAssistant = messages[i];
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (!lastAssistant) return;
|
|
430
|
+
const textContent = lastAssistant.parts.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
431
|
+
const toolCalls = lastAssistant.parts.filter((p) => p.type === "tool-call");
|
|
432
|
+
onMessageComplete({
|
|
433
|
+
role: lastAssistant.role,
|
|
434
|
+
content: textContent,
|
|
435
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0
|
|
436
|
+
});
|
|
437
|
+
}, [status, messages, onMessageComplete]);
|
|
396
438
|
(0, import_react4.useEffect)(() => {
|
|
397
439
|
if (open) {
|
|
398
440
|
panelRef.current?.focus();
|
|
@@ -455,7 +497,8 @@ function AIMeChat({
|
|
|
455
497
|
bottom: 24,
|
|
456
498
|
...position === "bottom-right" ? { right: 24 } : { left: 24 },
|
|
457
499
|
width: 380,
|
|
458
|
-
|
|
500
|
+
height: "70vh",
|
|
501
|
+
maxHeight: 600,
|
|
459
502
|
display: open ? "flex" : "none",
|
|
460
503
|
flexDirection: "column",
|
|
461
504
|
fontFamily: "var(--ai-me-font)",
|