@mesob/ai-react 0.5.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.
@@ -0,0 +1,30 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type AIFormProps<TData = unknown> = {
5
+ code: string;
6
+ zodSchema: unknown;
7
+ /** Sent as `<Context>` to the API (user, locale, etc.). */
8
+ context?: string;
9
+ defaultPrompt?: string;
10
+ defaultCount?: number;
11
+ multiple?: boolean;
12
+ onGenerate?: (value: TData) => void;
13
+ };
14
+
15
+ declare function AIForm<TData = unknown>({ code, zodSchema, context, defaultPrompt, defaultCount, multiple, onGenerate, }: AIFormProps<TData>): react_jsx_runtime.JSX.Element;
16
+
17
+ type AiProviderProps = {
18
+ api?: string;
19
+ children: ReactNode;
20
+ };
21
+ declare function AiProvider({ api, children, }: AiProviderProps): react_jsx_runtime.JSX.Element;
22
+ declare function useApi(): {
23
+ hooks: any;
24
+ };
25
+ declare const $api: {
26
+ useQuery(args_0: string, args_1: string, ...args: unknown[]): any;
27
+ useMutation(args_0: string, args_1: string, ...args: unknown[]): any;
28
+ };
29
+
30
+ export { $api, AIForm, type AIFormProps, AiProvider, useApi };
package/dist/index.js ADDED
@@ -0,0 +1,370 @@
1
+ "use client";
2
+
3
+ // src/components/ai-form.tsx
4
+ import {
5
+ Button,
6
+ Field,
7
+ FieldContent,
8
+ FieldError,
9
+ Modal,
10
+ NumberInput
11
+ } from "@mesob/ui/components";
12
+ import { IconSparkles } from "@tabler/icons-react";
13
+ import { useId, useState } from "react";
14
+ import * as z from "zod/v4";
15
+
16
+ // src/provider.tsx
17
+ import createFetchClient from "openapi-fetch";
18
+ import createClient from "openapi-react-query";
19
+ import { createContext, useContext, useMemo } from "react";
20
+ import { jsx } from "react/jsx-runtime";
21
+ var DEFAULT_AI_API = "/api/ai";
22
+ var AiContext = createContext({
23
+ api: DEFAULT_AI_API,
24
+ hooks: null
25
+ });
26
+ function AiProvider({
27
+ api = DEFAULT_AI_API,
28
+ children
29
+ }) {
30
+ const fetchClient = useMemo(
31
+ () => createFetchClient({ baseUrl: api }),
32
+ [api]
33
+ );
34
+ const hooks = useMemo(() => createClient(fetchClient), [fetchClient]);
35
+ return /* @__PURE__ */ jsx(AiContext.Provider, { value: { api, hooks }, children });
36
+ }
37
+ function useApi() {
38
+ const context = useContext(AiContext);
39
+ if (!context.hooks) {
40
+ throw new Error("useApi must be used within AiProvider");
41
+ }
42
+ return { hooks: context.hooks };
43
+ }
44
+ var $api = {
45
+ useQuery(...args) {
46
+ const { hooks } = useApi();
47
+ return hooks.useQuery(...args);
48
+ },
49
+ useMutation(...args) {
50
+ const { hooks } = useApi();
51
+ return hooks.useMutation(...args);
52
+ }
53
+ };
54
+
55
+ // src/components/ai-form.tsx
56
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
57
+ function AIForm({
58
+ code,
59
+ zodSchema,
60
+ context,
61
+ defaultPrompt,
62
+ defaultCount,
63
+ multiple = false,
64
+ onGenerate
65
+ }) {
66
+ const [promptValue, setPromptValue] = useState(defaultPrompt ?? "");
67
+ const [countValue, setCountValue] = useState(
68
+ defaultCount ?? ""
69
+ );
70
+ const [promptError, setPromptError] = useState(null);
71
+ const [countError, setCountError] = useState(null);
72
+ const [requestError, setRequestError] = useState(null);
73
+ const [open, setOpen] = useState(false);
74
+ const aiFormMutation = $api.useMutation("post", "/ai-form");
75
+ const countEnabled = multiple || defaultCount !== void 0;
76
+ const isLoading = aiFormMutation.isPending;
77
+ const triggerText = multiple ? "AI Bulk Generate" : "AI Fill";
78
+ const modalTitle = multiple ? "AI Bulk Generate" : "AI Fill";
79
+ const modalSubtitle = multiple ? "Type a prompt and AI will generate multiple structured items." : "Type a prompt and AI will fill the form.";
80
+ const submitText = multiple ? "Bulk Generate" : "Fill Form";
81
+ const loadingAriaLabel = multiple ? "Generating bulk items" : "Generating Fill Form";
82
+ const submitAriaLabel = isLoading ? loadingAriaLabel : submitText;
83
+ const sparklesGradientId = useId();
84
+ const formId = useId();
85
+ const handleOpen = () => {
86
+ setRequestError(null);
87
+ setPromptValue(defaultPrompt ?? "");
88
+ setCountValue(defaultCount ?? "");
89
+ setOpen(true);
90
+ };
91
+ const handleClose = () => {
92
+ setOpen(false);
93
+ };
94
+ const handleValidatedSubmit = async (event) => {
95
+ event.preventDefault();
96
+ const validated = validateAiFormInput({
97
+ prompt: promptValue,
98
+ countValue,
99
+ countEnabled
100
+ });
101
+ if (!validated.ok) {
102
+ setPromptError(validated.promptError);
103
+ setCountError(validated.countError);
104
+ return;
105
+ }
106
+ if (aiFormMutation.isPending) {
107
+ return;
108
+ }
109
+ setPromptError(null);
110
+ setCountError(null);
111
+ setRequestError(null);
112
+ let result;
113
+ try {
114
+ const schema = toAiFormSchema(z.toJSONSchema(zodSchema));
115
+ result = await aiFormMutation.mutateAsync({
116
+ body: buildAiFormRequestBody({
117
+ code,
118
+ prompt: validated.prompt,
119
+ context,
120
+ count: validated.count,
121
+ schema
122
+ })
123
+ });
124
+ } catch (err) {
125
+ setRequestError(parseAiFormHttpError(err));
126
+ return;
127
+ }
128
+ if (isAiFormSuccess(result)) {
129
+ if (onGenerate) {
130
+ onGenerate(result.data);
131
+ }
132
+ resetLocalState({
133
+ setPromptValue,
134
+ setCountValue,
135
+ defaultPrompt,
136
+ defaultCount
137
+ });
138
+ setOpen(false);
139
+ return;
140
+ }
141
+ const failureMsg = getAiFormFailureMessage(result);
142
+ setRequestError(failureMsg);
143
+ };
144
+ return /* @__PURE__ */ jsx2(
145
+ Modal,
146
+ {
147
+ open,
148
+ onOpenChange: (nextOpen) => setOpen(nextOpen),
149
+ size: "lg",
150
+ title: modalTitle,
151
+ subtitle: modalSubtitle,
152
+ trigger: /* @__PURE__ */ jsxs(
153
+ Button,
154
+ {
155
+ type: "button",
156
+ variant: "outline",
157
+ size: "sm",
158
+ className: "ai-fill-button group relative overflow-visible! rounded-full gap-2 isolate",
159
+ onClick: handleOpen,
160
+ disabled: isLoading,
161
+ children: [
162
+ /* @__PURE__ */ jsx2(
163
+ "span",
164
+ {
165
+ "aria-hidden": "true",
166
+ className: "ai-fill-ring pointer-events-none absolute inset-0 z-20 opacity-0 rounded-full"
167
+ }
168
+ ),
169
+ /* @__PURE__ */ jsx2(
170
+ IconSparkles,
171
+ {
172
+ className: "relative z-10 size-4 animate-bounce transition-transform group-hover:scale-110",
173
+ color: `url(#${sparklesGradientId})`,
174
+ children: /* @__PURE__ */ jsx2("defs", { children: /* @__PURE__ */ jsxs(
175
+ "linearGradient",
176
+ {
177
+ id: sparklesGradientId,
178
+ x1: "4",
179
+ y1: "4",
180
+ x2: "20",
181
+ y2: "20",
182
+ gradientUnits: "userSpaceOnUse",
183
+ children: [
184
+ /* @__PURE__ */ jsx2("stop", { offset: "0%", stopColor: "#c084fc" }),
185
+ /* @__PURE__ */ jsx2("stop", { offset: "50%", stopColor: "#a855f7" }),
186
+ /* @__PURE__ */ jsx2("stop", { offset: "100%", stopColor: "#7c3aed" })
187
+ ]
188
+ }
189
+ ) })
190
+ }
191
+ ),
192
+ /* @__PURE__ */ jsx2("span", { className: "relative z-10", children: triggerText })
193
+ ]
194
+ }
195
+ ),
196
+ bodyProps: { className: "space-y-3" },
197
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
198
+ /* @__PURE__ */ jsx2(Button, { type: "button", variant: "secondary", onClick: handleClose, children: "Cancel" }),
199
+ /* @__PURE__ */ jsx2(
200
+ Button,
201
+ {
202
+ type: "submit",
203
+ form: formId,
204
+ loading: isLoading,
205
+ "aria-label": submitAriaLabel,
206
+ children: submitText
207
+ }
208
+ )
209
+ ] }),
210
+ children: /* @__PURE__ */ jsxs("form", { id: formId, onSubmit: handleValidatedSubmit, className: "space-y-3", children: [
211
+ requestError ? /* @__PURE__ */ jsx2("div", { className: "rounded-xl border border-destructive/25 bg-destructive/8 px-3 py-2 text-sm text-destructive", children: requestError }) : null,
212
+ /* @__PURE__ */ jsx2(Field, { "data-invalid": !!promptError, children: /* @__PURE__ */ jsxs(FieldContent, { children: [
213
+ /* @__PURE__ */ jsx2(
214
+ "textarea",
215
+ {
216
+ "data-slot": "textarea",
217
+ name: "prompt",
218
+ disabled: isLoading,
219
+ rows: 5,
220
+ style: { minHeight: "8rem" },
221
+ "aria-label": "Generate form",
222
+ value: promptValue,
223
+ onChange: (event) => {
224
+ const value = event.target.value;
225
+ setPromptValue(value);
226
+ if (promptError && value.trim()) {
227
+ setPromptError(null);
228
+ }
229
+ },
230
+ onKeyDown: (event) => {
231
+ if (event.key === "Enter" && !event.shiftKey) {
232
+ event.preventDefault();
233
+ event.currentTarget.form?.requestSubmit();
234
+ }
235
+ },
236
+ placeholder: "Ask AI to generate structured form values",
237
+ className: "cn-textarea placeholder:text-muted-foreground min-h-32 w-full resize-y border border-border/70 px-2.5 py-2 text-sm leading-6 outline-none disabled:cursor-not-allowed disabled:opacity-50"
238
+ }
239
+ ),
240
+ /* @__PURE__ */ jsx2(FieldError, { children: promptError })
241
+ ] }) }),
242
+ countEnabled ? /* @__PURE__ */ jsx2(Field, { "data-invalid": !!countError, children: /* @__PURE__ */ jsxs(FieldContent, { children: [
243
+ /* @__PURE__ */ jsxs("div", { className: "max-w-44", children: [
244
+ /* @__PURE__ */ jsx2(
245
+ "label",
246
+ {
247
+ htmlFor: `${formId}-count`,
248
+ className: "mb-1 block text-sm font-medium",
249
+ children: "How many item"
250
+ }
251
+ ),
252
+ /* @__PURE__ */ jsx2(
253
+ NumberInput,
254
+ {
255
+ id: `${formId}-count`,
256
+ value: countValue,
257
+ onChange: (next) => {
258
+ setCountValue(next);
259
+ if (countError) {
260
+ setCountError(null);
261
+ }
262
+ },
263
+ min: 1,
264
+ max: 100,
265
+ step: 1,
266
+ controls: true,
267
+ disabled: isLoading
268
+ }
269
+ )
270
+ ] }),
271
+ /* @__PURE__ */ jsx2(FieldError, { children: countError })
272
+ ] }) }) : null
273
+ ] })
274
+ }
275
+ );
276
+ }
277
+ function validateAiFormInput(args) {
278
+ const trimmedPrompt = args.prompt.trim();
279
+ if (!trimmedPrompt) {
280
+ return {
281
+ ok: false,
282
+ promptError: "Prompt is required.",
283
+ countError: null
284
+ };
285
+ }
286
+ if (!args.countEnabled) {
287
+ return {
288
+ ok: true,
289
+ prompt: trimmedPrompt
290
+ };
291
+ }
292
+ const parsedCount = parseCount(args.countValue);
293
+ if (parsedCount == null) {
294
+ return {
295
+ ok: false,
296
+ promptError: null,
297
+ countError: "Count must be between 1 and 100."
298
+ };
299
+ }
300
+ return {
301
+ ok: true,
302
+ prompt: trimmedPrompt,
303
+ count: parsedCount
304
+ };
305
+ }
306
+ function buildAiFormRequestBody(args) {
307
+ const trimmedContext = args.context == null ? "" : args.context.trim();
308
+ const contextBody = trimmedContext.length > 0 ? { context: trimmedContext } : {};
309
+ const countBody = args.count == null ? {} : { count: args.count };
310
+ return {
311
+ code: args.code,
312
+ prompt: args.prompt,
313
+ ...contextBody,
314
+ ...countBody,
315
+ schema: args.schema
316
+ };
317
+ }
318
+ function resetLocalState(args) {
319
+ args.setPromptValue(args.defaultPrompt ?? "");
320
+ args.setCountValue(args.defaultCount ?? "");
321
+ }
322
+ function parseCount(value) {
323
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value).trim(), 10);
324
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) {
325
+ return null;
326
+ }
327
+ return parsed;
328
+ }
329
+ function isAiFormSuccess(value) {
330
+ return typeof value === "object" && value !== null && "ok" in value && value.ok === true;
331
+ }
332
+ function isAiFormFailure(value) {
333
+ return typeof value === "object" && value !== null && "ok" in value && value.ok === false;
334
+ }
335
+ function getAiFormFailureMessage(result) {
336
+ if (!isAiFormFailure(result)) {
337
+ return "AI request failed.";
338
+ }
339
+ return formatAiFormErrorMessage(result.error?.code, result.error?.message);
340
+ }
341
+ function parseAiFormHttpError(err) {
342
+ if (err && typeof err === "object" && "ok" in err && err.ok === false && "error" in err) {
343
+ const e = err.error;
344
+ return formatAiFormErrorMessage(e?.code, e?.message);
345
+ }
346
+ if (err instanceof Error && err.message) {
347
+ return err.message;
348
+ }
349
+ return "Something went wrong. Check the AI API and try again.";
350
+ }
351
+ function formatAiFormErrorMessage(code, message) {
352
+ const msg = typeof message === "string" && message.length > 0 ? message : "";
353
+ if (code === "INSUFFICIENT_INPUT") {
354
+ return msg || "That prompt does not contain enough to fill the form.";
355
+ }
356
+ return msg || "AI request failed.";
357
+ }
358
+ function toAiFormSchema(schema) {
359
+ if (typeof schema !== "object" || schema === null || !("type" in schema) || schema.type !== "object") {
360
+ throw new Error('AI form schema root must be type "object".');
361
+ }
362
+ return schema;
363
+ }
364
+ export {
365
+ $api,
366
+ AIForm,
367
+ AiProvider,
368
+ useApi
369
+ };
370
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/ai-form.tsx","../src/provider.tsx"],"sourcesContent":["'use client';\n\nimport {\n Button,\n Field,\n FieldContent,\n FieldError,\n Modal,\n NumberInput,\n} from '@mesob/ui/components';\nimport { IconSparkles } from '@tabler/icons-react';\nimport { type FormEvent, useId, useState } from 'react';\nimport * as z from 'zod/v4';\nimport { $api } from '../provider';\nimport type { AIFormProps } from '../types';\n\nexport type { AIFormProps } from '../types';\n\nexport function AIForm<TData = unknown>({\n code,\n zodSchema,\n context,\n defaultPrompt,\n defaultCount,\n multiple = false,\n onGenerate,\n}: AIFormProps<TData>) {\n const [promptValue, setPromptValue] = useState(defaultPrompt ?? '');\n const [countValue, setCountValue] = useState<number | string>(\n defaultCount ?? '',\n );\n const [promptError, setPromptError] = useState<string | null>(null);\n const [countError, setCountError] = useState<string | null>(null);\n const [requestError, setRequestError] = useState<string | null>(null);\n const [open, setOpen] = useState(false);\n const aiFormMutation = $api.useMutation('post', '/ai-form');\n const countEnabled = multiple || defaultCount !== undefined;\n const isLoading = aiFormMutation.isPending;\n const triggerText = multiple ? 'AI Bulk Generate' : 'AI Fill';\n const modalTitle = multiple ? 'AI Bulk Generate' : 'AI Fill';\n const modalSubtitle = multiple\n ? 'Type a prompt and AI will generate multiple structured items.'\n : 'Type a prompt and AI will fill the form.';\n const submitText = multiple ? 'Bulk Generate' : 'Fill Form';\n const loadingAriaLabel = multiple\n ? 'Generating bulk items'\n : 'Generating Fill Form';\n const submitAriaLabel = isLoading ? loadingAriaLabel : submitText;\n const sparklesGradientId = useId();\n const formId = useId();\n const handleOpen = () => {\n setRequestError(null);\n setPromptValue(defaultPrompt ?? '');\n setCountValue(defaultCount ?? '');\n setOpen(true);\n };\n const handleClose = () => {\n setOpen(false);\n };\n\n const handleValidatedSubmit = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n const validated = validateAiFormInput({\n prompt: promptValue,\n countValue,\n countEnabled,\n });\n if (!validated.ok) {\n setPromptError(validated.promptError);\n setCountError(validated.countError);\n return;\n }\n\n if (aiFormMutation.isPending) {\n return;\n }\n\n setPromptError(null);\n setCountError(null);\n setRequestError(null);\n\n let result: unknown;\n try {\n const schema = toAiFormSchema(z.toJSONSchema(zodSchema as never));\n result = await aiFormMutation.mutateAsync({\n body: buildAiFormRequestBody({\n code,\n prompt: validated.prompt,\n context,\n count: validated.count,\n schema,\n }),\n });\n } catch (err: unknown) {\n setRequestError(parseAiFormHttpError(err));\n return;\n }\n if (isAiFormSuccess(result)) {\n if (onGenerate) {\n onGenerate(result.data as TData);\n }\n resetLocalState({\n setPromptValue,\n setCountValue,\n defaultPrompt,\n defaultCount,\n });\n setOpen(false);\n return;\n }\n const failureMsg = getAiFormFailureMessage(result);\n setRequestError(failureMsg);\n };\n\n return (\n <Modal\n open={open}\n onOpenChange={(nextOpen) => setOpen(nextOpen)}\n size=\"lg\"\n title={modalTitle}\n subtitle={modalSubtitle}\n trigger={\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"ai-fill-button group relative overflow-visible! rounded-full gap-2 isolate\"\n onClick={handleOpen}\n disabled={isLoading}\n >\n <span\n aria-hidden=\"true\"\n className=\"ai-fill-ring pointer-events-none absolute inset-0 z-20 opacity-0 rounded-full\"\n />\n <IconSparkles\n className=\"relative z-10 size-4 animate-bounce transition-transform group-hover:scale-110\"\n color={`url(#${sparklesGradientId})`}\n >\n <defs>\n <linearGradient\n id={sparklesGradientId}\n x1=\"4\"\n y1=\"4\"\n x2=\"20\"\n y2=\"20\"\n gradientUnits=\"userSpaceOnUse\"\n >\n <stop offset=\"0%\" stopColor=\"#c084fc\" />\n <stop offset=\"50%\" stopColor=\"#a855f7\" />\n <stop offset=\"100%\" stopColor=\"#7c3aed\" />\n </linearGradient>\n </defs>\n </IconSparkles>\n <span className=\"relative z-10\">{triggerText}</span>\n </Button>\n }\n bodyProps={{ className: 'space-y-3' }}\n footer={\n <>\n <Button type=\"button\" variant=\"secondary\" onClick={handleClose}>\n Cancel\n </Button>\n <Button\n type=\"submit\"\n form={formId}\n loading={isLoading}\n aria-label={submitAriaLabel}\n >\n {submitText}\n </Button>\n </>\n }\n >\n <form id={formId} onSubmit={handleValidatedSubmit} className=\"space-y-3\">\n {requestError ? (\n <div className=\"rounded-xl border border-destructive/25 bg-destructive/8 px-3 py-2 text-sm text-destructive\">\n {requestError}\n </div>\n ) : null}\n <Field data-invalid={!!promptError}>\n <FieldContent>\n <textarea\n data-slot=\"textarea\"\n name=\"prompt\"\n disabled={isLoading}\n rows={5}\n style={{ minHeight: '8rem' }}\n aria-label=\"Generate form\"\n value={promptValue}\n onChange={(event) => {\n const value = event.target.value;\n setPromptValue(value);\n if (promptError && value.trim()) {\n setPromptError(null);\n }\n }}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n event.currentTarget.form?.requestSubmit();\n }\n }}\n placeholder=\"Ask AI to generate structured form values\"\n className=\"cn-textarea placeholder:text-muted-foreground min-h-32 w-full resize-y border border-border/70 px-2.5 py-2 text-sm leading-6 outline-none disabled:cursor-not-allowed disabled:opacity-50\"\n />\n <FieldError>{promptError}</FieldError>\n </FieldContent>\n </Field>\n {countEnabled ? (\n <Field data-invalid={!!countError}>\n <FieldContent>\n <div className=\"max-w-44\">\n <label\n htmlFor={`${formId}-count`}\n className=\"mb-1 block text-sm font-medium\"\n >\n How many item\n </label>\n <NumberInput\n id={`${formId}-count`}\n value={countValue}\n onChange={(next) => {\n setCountValue(next);\n if (countError) {\n setCountError(null);\n }\n }}\n min={1}\n max={100}\n step={1}\n controls\n disabled={isLoading}\n />\n </div>\n <FieldError>{countError}</FieldError>\n </FieldContent>\n </Field>\n ) : null}\n </form>\n </Modal>\n );\n}\n\nfunction validateAiFormInput(args: {\n prompt: string;\n countValue: number | string;\n countEnabled: boolean;\n}):\n | { ok: true; prompt: string; count?: number }\n | { ok: false; promptError: string | null; countError: string | null } {\n const trimmedPrompt = args.prompt.trim();\n if (!trimmedPrompt) {\n return {\n ok: false,\n promptError: 'Prompt is required.',\n countError: null,\n };\n }\n\n if (!args.countEnabled) {\n return {\n ok: true,\n prompt: trimmedPrompt,\n };\n }\n\n const parsedCount = parseCount(args.countValue);\n if (parsedCount == null) {\n return {\n ok: false,\n promptError: null,\n countError: 'Count must be between 1 and 100.',\n };\n }\n\n return {\n ok: true,\n prompt: trimmedPrompt,\n count: parsedCount,\n };\n}\n\nfunction buildAiFormRequestBody(args: {\n code: string;\n prompt: string;\n context?: string;\n count?: number;\n schema: Record<string, unknown> & { type: 'object' };\n}) {\n const trimmedContext = args.context == null ? '' : args.context.trim();\n const contextBody =\n trimmedContext.length > 0 ? { context: trimmedContext } : {};\n const countBody = args.count == null ? {} : { count: args.count };\n\n return {\n code: args.code,\n prompt: args.prompt,\n ...contextBody,\n ...countBody,\n schema: args.schema,\n };\n}\n\nfunction resetLocalState(args: {\n setPromptValue: (v: string) => void;\n setCountValue: (v: number | string) => void;\n defaultPrompt?: string;\n defaultCount?: number;\n}) {\n args.setPromptValue(args.defaultPrompt ?? '');\n args.setCountValue(args.defaultCount ?? '');\n}\n\nfunction parseCount(value: number | string): number | null {\n const parsed =\n typeof value === 'number'\n ? value\n : Number.parseInt(String(value).trim(), 10);\n if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) {\n return null;\n }\n return parsed;\n}\n\nfunction isAiFormSuccess(value: unknown): value is {\n ok: true;\n data?: unknown;\n} {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'ok' in value &&\n (value as { ok?: unknown }).ok === true\n );\n}\n\nfunction isAiFormFailure(value: unknown): value is {\n ok: false;\n error?: {\n code?: string;\n message?: string;\n };\n} {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'ok' in value &&\n (value as { ok?: unknown }).ok === false\n );\n}\n\nfunction getAiFormFailureMessage(result: unknown): string {\n if (!isAiFormFailure(result)) {\n return 'AI request failed.';\n }\n return formatAiFormErrorMessage(result.error?.code, result.error?.message);\n}\n\n/** openapi-react-query throws the parsed JSON body on 4xx/5xx. */\nfunction parseAiFormHttpError(err: unknown): string {\n if (\n err &&\n typeof err === 'object' &&\n 'ok' in err &&\n (err as { ok: unknown }).ok === false &&\n 'error' in err\n ) {\n const e = (err as { error?: { code?: unknown; message?: unknown } }).error;\n return formatAiFormErrorMessage(e?.code, e?.message);\n }\n if (err instanceof Error && err.message) {\n return err.message;\n }\n return 'Something went wrong. Check the AI API and try again.';\n}\n\nfunction formatAiFormErrorMessage(code: unknown, message: unknown): string {\n const msg = typeof message === 'string' && message.length > 0 ? message : '';\n if (code === 'INSUFFICIENT_INPUT') {\n return msg || 'That prompt does not contain enough to fill the form.';\n }\n return msg || 'AI request failed.';\n}\n\nfunction toAiFormSchema(\n schema: unknown,\n): Record<string, unknown> & { type: 'object' } {\n if (\n typeof schema !== 'object' ||\n schema === null ||\n !('type' in schema) ||\n (schema as { type?: unknown }).type !== 'object'\n ) {\n throw new Error('AI form schema root must be type \"object\".');\n }\n\n return schema as Record<string, unknown> & { type: 'object' };\n}\n","'use client';\n\nimport createFetchClient from 'openapi-fetch';\nimport createClient from 'openapi-react-query';\nimport { createContext, type ReactNode, useContext, useMemo } from 'react';\nimport type { paths } from './data/openapi';\n\ntype AiContextValue = {\n api: string;\n // biome-ignore lint/suspicious/noExplicitAny: openapi hooks\n hooks: any;\n};\n\nconst DEFAULT_AI_API = '/api/ai';\nlet hasWarnedFallbackApi = false;\n\nconst AiContext = createContext<AiContextValue>({\n api: DEFAULT_AI_API,\n hooks: null,\n});\n\ntype AiProviderProps = {\n api?: string;\n children: ReactNode;\n};\n\nexport function AiProvider({\n api = DEFAULT_AI_API,\n children,\n}: AiProviderProps) {\n const fetchClient = useMemo(\n () => createFetchClient<paths>({ baseUrl: api }),\n [api],\n );\n const hooks = useMemo(() => createClient(fetchClient), [fetchClient]);\n\n return (\n <AiContext.Provider value={{ api, hooks }}>{children}</AiContext.Provider>\n );\n}\n\nexport function useAiApi(explicitApi?: string) {\n const context = useContext(AiContext);\n const resolvedApi = explicitApi ?? context.api;\n\n if (\n process.env.NODE_ENV === 'development' &&\n resolvedApi === DEFAULT_AI_API &&\n !hasWarnedFallbackApi\n ) {\n hasWarnedFallbackApi = true;\n // biome-ignore lint/suspicious/noConsole: dev-only runtime diagnostic\n console.warn(\n 'AI API URL fell back to /api/ai. Wrap your app in <AiProvider api=\"...\">.',\n );\n }\n\n return resolvedApi;\n}\n\nexport function useApi() {\n const context = useContext(AiContext);\n if (!context.hooks) {\n throw new Error('useApi must be used within AiProvider');\n }\n\n return { hooks: context.hooks };\n}\n\ntype HookArgs = [string, string, ...unknown[]];\n\nexport const $api = {\n useQuery(...args: HookArgs) {\n const { hooks } = useApi();\n\n return hooks.useQuery(...args);\n },\n useMutation(...args: HookArgs) {\n const { hooks } = useApi();\n\n return hooks.useMutation(...args);\n },\n};\n"],"mappings":"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","names":["jsx"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@mesob/ai-react",
3
+ "version": "0.5.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./data/openapi": {
14
+ "types": "./src/data/openapi.d.ts",
15
+ "default": "./src/data/openapi.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "dependencies": {
22
+ "@tabler/icons-react": "^3.35.0",
23
+ "openapi-fetch": "^0.15.0",
24
+ "openapi-react-query": "^0.5.1",
25
+ "@mesob/ui": "0.5.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/react": "^19",
29
+ "@types/react-dom": "^19",
30
+ "openapi-typescript": "^7.0.0",
31
+ "tsup": "^8.5.0",
32
+ "typescript": "^5.7.2",
33
+ "zod": "^4.3.6"
34
+ },
35
+ "peerDependencies": {
36
+ "@tanstack/react-query": "^5.0.0",
37
+ "react": "^19.2.0",
38
+ "react-dom": "^19.2.0",
39
+ "zod": "^4.1.13"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "dev": "tsup --watch",
47
+ "generate:client": "node scripts/generate-client.mjs",
48
+ "lint": "biome check --write .",
49
+ "check-types": "tsc --noEmit"
50
+ }
51
+ }