@kohryan/moodui 0.0.2 → 0.0.3

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/index.mjs CHANGED
@@ -1,256 +1,148 @@
1
- // src/validate.ts
2
- function validateMoodUISpec(input) {
3
- const errors = [];
4
- if (!isObject(input)) {
5
- return { ok: false, errors: ["spec must be an object"] };
6
- }
7
- const version = input.version;
8
- if (version !== 1) errors.push("spec.version must be 1");
9
- const root = input.root;
10
- const rootResult = validateNode(root, "spec.root");
11
- if (!rootResult.ok) errors.push(...rootResult.errors);
12
- if (errors.length > 0) return { ok: false, errors };
13
- return { ok: true, value: input };
14
- }
15
- function assertMoodUISpec(input) {
16
- const result = validateMoodUISpec(input);
17
- if (!result.ok) {
18
- const message = ["Invalid MoodUI spec:", ...result.errors.map((e) => `- ${e}`)].join("\n");
19
- throw new Error(message);
20
- }
21
- return result.value;
1
+ import {
2
+ assertMoodUISpec,
3
+ createGeminiClient,
4
+ createOllamaClient,
5
+ createOpenAICompatibleClient,
6
+ generateMoodUISpec,
7
+ generateReactFromPrompt,
8
+ renderReact,
9
+ renderReactJSX,
10
+ validateMoodUISpec
11
+ } from "./chunk-5D6KNM5J.mjs";
12
+
13
+ // src/react/MoodUIRuntime.tsx
14
+ import * as React from "react";
15
+ import { Fragment as Fragment2, jsx } from "react/jsx-runtime";
16
+ function MoodUIRuntime(props) {
17
+ return /* @__PURE__ */ jsx(Fragment2, { children: renderNode(props.spec.root, props.onAction) });
22
18
  }
23
- function validateNode(input, path) {
24
- const errors = [];
25
- if (!isObject(input)) return { ok: false, errors: [`${path} must be an object`] };
26
- const type = input.type;
27
- if (!isString(type)) return { ok: false, errors: [`${path}.type must be a string`] };
28
- switch (type) {
19
+ function renderNode(node, onAction) {
20
+ switch (node.type) {
29
21
  case "box": {
30
- const children = input.children;
31
- if (children != null) {
32
- if (!Array.isArray(children)) {
33
- errors.push(`${path}.children must be an array`);
34
- } else {
35
- for (let i = 0; i < children.length; i += 1) {
36
- const child = children[i];
37
- const childResult = validateNode(child, `${path}.children[${i}]`);
38
- if (!childResult.ok) errors.push(...childResult.errors);
39
- }
40
- }
41
- }
42
- break;
22
+ const style = computeStyle(node);
23
+ const children = node.children?.map((c, i) => /* @__PURE__ */ jsx(React.Fragment, { children: renderNode(c, onAction) }, i));
24
+ return /* @__PURE__ */ jsx("div", { ...commonAttrs(node), style, children });
43
25
  }
44
26
  case "text": {
45
- const props = input.props;
46
- if (!isObject(props)) errors.push(`${path}.props must be an object`);
47
- else if (!isString(props.value)) errors.push(`${path}.props.value must be a string`);
48
- break;
27
+ const Tag = node.props?.as ?? "p";
28
+ const style = computeStyle(node);
29
+ return /* @__PURE__ */ jsx(Tag, { ...commonAttrs(node), style, children: node.props?.value ?? "" });
49
30
  }
50
31
  case "button": {
51
- const props = input.props;
52
- if (!isObject(props)) errors.push(`${path}.props must be an object`);
53
- else if (!isString(props.label)) errors.push(`${path}.props.label must be a string`);
54
- break;
32
+ const style = computeStyle(node);
33
+ const actionId = node.props?.actionId;
34
+ return /* @__PURE__ */ jsx(
35
+ "button",
36
+ {
37
+ ...commonAttrs(node),
38
+ style,
39
+ disabled: node.props?.disabled ?? false,
40
+ onClick: actionId ? () => onAction?.(actionId) : void 0,
41
+ children: node.props?.label ?? ""
42
+ }
43
+ );
55
44
  }
56
45
  case "input": {
57
- const props = input.props;
58
- if (props != null && !isObject(props)) errors.push(`${path}.props must be an object if provided`);
59
- break;
46
+ const style = computeStyle(node);
47
+ return /* @__PURE__ */ jsx(
48
+ "input",
49
+ {
50
+ ...commonAttrs(node),
51
+ style,
52
+ name: node.props?.name,
53
+ placeholder: node.props?.placeholder,
54
+ defaultValue: node.props?.defaultValue
55
+ }
56
+ );
60
57
  }
61
58
  case "image": {
62
- const props = input.props;
63
- if (!isObject(props)) errors.push(`${path}.props must be an object`);
64
- else if (!isString(props.src)) errors.push(`${path}.props.src must be a string`);
65
- break;
66
- }
67
- case "spacer": {
68
- const props = input.props;
69
- if (props != null && !isObject(props)) errors.push(`${path}.props must be an object if provided`);
70
- break;
71
- }
72
- default:
73
- errors.push(`${path}.type must be one of: box | text | button | input | image | spacer`);
74
- }
75
- if (errors.length > 0) return { ok: false, errors };
76
- return { ok: true, value: input };
77
- }
78
- function isObject(value) {
79
- return typeof value === "object" && value !== null && !Array.isArray(value);
80
- }
81
- function isString(value) {
82
- return typeof value === "string";
83
- }
84
-
85
- // src/react/renderReact.ts
86
- function renderReact(specInput, options = {}) {
87
- const spec = assertMoodUISpec(specInput);
88
- const componentName = options.componentName ?? "MoodUIScreen";
89
- const jsx = renderNode(spec.root, { indent: 2, onActionProp: "onAction" });
90
- return [
91
- 'import * as React from "react";',
92
- "",
93
- "export type MoodUIScreenActionHandler = (actionId: string) => void;",
94
- "",
95
- "export type MoodUIScreenProps = {",
96
- " onAction?: MoodUIScreenActionHandler;",
97
- "};",
98
- "",
99
- `export function ${componentName}(props: MoodUIScreenProps) {`,
100
- " const { onAction } = props;",
101
- " return (",
102
- jsx,
103
- " );",
104
- "}",
105
- ""
106
- ].join("\n");
107
- }
108
- function renderReactJSX(node) {
109
- return renderNode(node, { indent: 0, onActionProp: "onAction" }).trimEnd();
110
- }
111
- function renderNode(node, ctx) {
112
- switch (node.type) {
113
- case "box":
114
- return renderElement("div", node, ctx, () => {
115
- const children = node.children ?? [];
116
- if (children.length === 0) return [];
117
- return children.map((child) => renderNode(child, { ...ctx, indent: ctx.indent + 2 }));
118
- });
119
- case "text": {
120
- const as = node.props?.as ?? "p";
121
- const text = escapeText(node.props?.value ?? "");
122
- return renderElement(as, node, ctx, () => [indent(ctx.indent + 2) + text]);
59
+ const style = computeStyle(node);
60
+ return /* @__PURE__ */ jsx("img", { ...commonAttrs(node), style, src: node.props?.src, alt: node.props?.alt ?? "" });
123
61
  }
124
- case "button": {
125
- const label = escapeText(node.props?.label ?? "");
126
- const actionId = node.props?.actionId;
127
- return renderElement(
128
- "button",
129
- node,
130
- ctx,
131
- () => [indent(ctx.indent + 2) + label],
132
- actionId ? { onClick: `() => ${ctx.onActionProp}?.(${serializeJsValue(actionId)})` } : void 0
133
- );
134
- }
135
- case "input":
136
- return renderSelfClosingElement("input", node, ctx);
137
- case "image":
138
- return renderSelfClosingElement("img", node, ctx);
139
62
  case "spacer": {
140
63
  const size = node.props?.size ?? 8;
141
- const style = { width: normalizeCssValue(size), height: normalizeCssValue(size) };
142
- return renderElement(
143
- "div",
144
- { type: "box", props: { style }, children: [] },
145
- ctx,
146
- () => []
147
- );
64
+ return /* @__PURE__ */ jsx("div", { style: { width: size, height: size } });
148
65
  }
149
66
  }
150
67
  }
151
- function renderElement(tag, node, ctx, renderChildren, extraProps) {
152
- const children = renderChildren();
153
- const propParts = buildProps(tag, node, extraProps);
154
- const open = `<${tag}${propParts.length ? " " + propParts.join(" ") : ""}>`;
155
- const close = `</${tag}>`;
156
- if (children.length === 0) return indent(ctx.indent) + open + close;
157
- return [
158
- indent(ctx.indent) + open,
159
- ...children,
160
- indent(ctx.indent) + close
161
- ].join("\n");
162
- }
163
- function renderSelfClosingElement(tag, node, ctx) {
164
- const propParts = buildProps(tag, node);
165
- return indent(ctx.indent) + `<${tag}${propParts.length ? " " + propParts.join(" ") : ""} />`;
166
- }
167
- function buildProps(tag, node, extraProps) {
168
- const props = node.props ?? {};
169
- const out = [];
170
- if (props.id) out.push(`id=${serializeJsxAttrValue(props.id)}`);
171
- if (props.testId) out.push(`data-testid=${serializeJsxAttrValue(props.testId)}`);
172
- if (props.className) out.push(`className=${serializeJsxAttrValue(props.className)}`);
173
- const computedStyle = computeStyle(tag, node);
174
- const mergedStyle = mergeStyle(computedStyle, props.style) ?? {};
175
- if (tag === "input") {
176
- if (props.name) out.push(`name=${serializeJsxAttrValue(props.name)}`);
177
- if (props.placeholder) out.push(`placeholder=${serializeJsxAttrValue(props.placeholder)}`);
178
- if (props.defaultValue) out.push(`defaultValue=${serializeJsxAttrValue(props.defaultValue)}`);
179
- }
180
- if (tag === "img") {
181
- if (props.src) out.push(`src=${serializeJsxAttrValue(props.src)}`);
182
- if (props.alt) out.push(`alt=${serializeJsxAttrValue(props.alt)}`);
183
- if (props.fit) mergedStyle.objectFit = props.fit;
184
- }
185
- if (Object.keys(mergedStyle).length > 0) {
186
- out.push(`style={(${serializeJsValue(mergedStyle)} as React.CSSProperties)}`);
187
- }
188
- if (extraProps) {
189
- for (const [k, v] of Object.entries(extraProps)) out.push(`${k}={${v}}`);
190
- }
191
- return out;
68
+ function commonAttrs(node) {
69
+ const p = node.props ?? {};
70
+ return {
71
+ id: typeof p.id === "string" ? p.id : void 0,
72
+ className: typeof p.className === "string" ? p.className : void 0,
73
+ "data-testid": typeof p.testId === "string" ? p.testId : void 0
74
+ };
192
75
  }
193
- function computeStyle(tag, node) {
76
+ function computeStyle(node) {
194
77
  const props = node.props ?? {};
195
78
  const style = {};
196
- if (props.width != null) style.width = normalizeCssValue(props.width);
197
- if (props.height != null) style.height = normalizeCssValue(props.height);
198
- if (props.background != null) style.background = props.background;
199
- if (props.borderRadius != null) style.borderRadius = normalizeCssValue(props.borderRadius);
79
+ const width = toCssValue(props.width);
80
+ const height = toCssValue(props.height);
81
+ const borderRadius = toCssValue(props.borderRadius);
82
+ if (width != null) style.width = width;
83
+ if (height != null) style.height = height;
84
+ if (typeof props.background === "string") style.background = props.background;
85
+ if (borderRadius != null) style.borderRadius = borderRadius;
200
86
  applySpacing(style, "margin", props.margin);
201
87
  applySpacing(style, "padding", props.padding);
202
88
  if (node.type === "box") {
203
89
  style.display = "flex";
204
- style.flexDirection = normalizeFlexDirection(props.direction) ?? "column";
205
- if (props.gap != null) style.gap = normalizeCssValue(props.gap);
206
- if (props.align != null) style.alignItems = props.align;
207
- if (props.justify != null) style.justifyContent = props.justify;
208
- if (props.wrap != null) style.flexWrap = props.wrap;
90
+ if (props.direction === "row" || props.direction === "column") style.flexDirection = props.direction;
91
+ else style.flexDirection = "column";
92
+ const gap = toCssValue(props.gap);
93
+ if (gap != null) style.gap = gap;
94
+ if (typeof props.align === "string") style.alignItems = props.align;
95
+ if (typeof props.justify === "string") style.justifyContent = props.justify;
96
+ if (props.wrap === "nowrap" || props.wrap === "wrap") style.flexWrap = props.wrap;
209
97
  }
210
98
  if (node.type === "text") {
211
- if (props.color != null) style.color = props.color;
212
- if (props.fontSize != null) style.fontSize = normalizeCssValue(props.fontSize);
213
- if (props.fontWeight != null) style.fontWeight = props.fontWeight;
214
- if (props.textAlign != null) style.textAlign = props.textAlign;
215
- if (tag.startsWith("h")) style.margin = 0;
99
+ if (typeof props.color === "string") style.color = props.color;
100
+ const fontSize = toCssValue(props.fontSize);
101
+ if (fontSize != null) style.fontSize = fontSize;
102
+ if (typeof props.fontWeight === "string" || typeof props.fontWeight === "number") style.fontWeight = props.fontWeight;
103
+ if (typeof props.textAlign === "string") style.textAlign = props.textAlign;
104
+ const asTag = node.props?.as;
105
+ if (typeof asTag === "string" && asTag.startsWith("h")) style.margin = 0;
216
106
  }
217
107
  if (node.type === "button") {
218
- style.cursor = "pointer";
108
+ style.cursor = node.props?.disabled ? "not-allowed" : "pointer";
219
109
  style.border = "none";
220
- style.padding = "10px 14px";
221
- style.borderRadius = 10;
222
- const variant = props.variant ?? "primary";
110
+ if (style.padding == null) style.padding = "10px 14px";
111
+ if (style.borderRadius == null) style.borderRadius = 10;
112
+ const variant = node.props?.variant ?? "primary";
223
113
  if (variant === "primary") {
224
- style.background = "#111827";
225
- style.color = "#ffffff";
114
+ if (style.background == null) style.background = "#111827";
115
+ if (style.color == null) style.color = "#ffffff";
226
116
  } else if (variant === "secondary") {
227
- style.background = "#e5e7eb";
228
- style.color = "#111827";
117
+ if (style.background == null) style.background = "#e5e7eb";
118
+ if (style.color == null) style.color = "#111827";
229
119
  } else {
230
- style.background = "transparent";
231
- style.color = "#111827";
232
- }
233
- if (props.disabled) {
234
- style.opacity = 0.6;
235
- style.cursor = "not-allowed";
120
+ if (style.background == null) style.background = "transparent";
121
+ if (style.color == null) style.color = "#111827";
236
122
  }
123
+ if (node.props?.disabled) style.opacity = 0.6;
237
124
  }
238
125
  if (node.type === "input") {
239
- style.padding = "10px 12px";
240
- style.borderRadius = 10;
241
- style.border = "1px solid #d1d5db";
242
- style.outline = "none";
126
+ if (style.padding == null) style.padding = "10px 12px";
127
+ if (style.borderRadius == null) style.borderRadius = 10;
128
+ if (style.border == null) style.border = "1px solid #d1d5db";
129
+ if (style.outline == null) style.outline = "none";
243
130
  }
244
131
  if (node.type === "image") {
245
- if (props.fit != null) style.objectFit = props.fit;
246
- style.maxWidth = "100%";
132
+ if (style.maxWidth == null) style.maxWidth = "100%";
133
+ const fit = node.props?.fit;
134
+ if (fit != null) style.objectFit = fit;
135
+ }
136
+ if (props.style && typeof props.style === "object" && !Array.isArray(props.style)) {
137
+ Object.assign(style, props.style);
247
138
  }
248
139
  return style;
249
140
  }
250
141
  function applySpacing(style, kind, value) {
251
142
  if (value == null) return;
143
+ const s = style;
252
144
  if (typeof value === "number" || typeof value === "string") {
253
- style[kind] = normalizeCssValue(value);
145
+ s[kind] = toCssValue(value);
254
146
  return;
255
147
  }
256
148
  if (typeof value !== "object" || Array.isArray(value)) return;
@@ -258,305 +150,195 @@ function applySpacing(style, kind, value) {
258
150
  const all = v.all;
259
151
  const x = v.x;
260
152
  const y = v.y;
261
- if (all != null) style[kind] = normalizeCssValue(all);
153
+ if (all != null) s[kind] = toCssValue(all);
262
154
  if (x != null) {
263
- style[`${kind}Left`] = normalizeCssValue(x);
264
- style[`${kind}Right`] = normalizeCssValue(x);
155
+ s[`${kind}Left`] = toCssValue(x);
156
+ s[`${kind}Right`] = toCssValue(x);
265
157
  }
266
158
  if (y != null) {
267
- style[`${kind}Top`] = normalizeCssValue(y);
268
- style[`${kind}Bottom`] = normalizeCssValue(y);
159
+ s[`${kind}Top`] = toCssValue(y);
160
+ s[`${kind}Bottom`] = toCssValue(y);
269
161
  }
270
- if (v.top != null) style[`${kind}Top`] = normalizeCssValue(v.top);
271
- if (v.right != null) style[`${kind}Right`] = normalizeCssValue(v.right);
272
- if (v.bottom != null) style[`${kind}Bottom`] = normalizeCssValue(v.bottom);
273
- if (v.left != null) style[`${kind}Left`] = normalizeCssValue(v.left);
274
- }
275
- function mergeStyle(a, b) {
276
- if (!a && !b) return void 0;
277
- if (!a) return b;
278
- if (!b) return a;
279
- return { ...a, ...b };
162
+ if (v.top != null) s[`${kind}Top`] = toCssValue(v.top);
163
+ if (v.right != null) s[`${kind}Right`] = toCssValue(v.right);
164
+ if (v.bottom != null) s[`${kind}Bottom`] = toCssValue(v.bottom);
165
+ if (v.left != null) s[`${kind}Left`] = toCssValue(v.left);
280
166
  }
281
- function normalizeCssValue(value) {
167
+ function toCssValue(value) {
282
168
  if (typeof value === "number") return value;
283
169
  if (typeof value === "string") return value;
284
- return value;
285
- }
286
- function normalizeFlexDirection(value) {
287
- if (value === "row" || value === "column") return value;
288
- if (value === "horizontal") return "row";
289
- if (value === "vertical") return "column";
290
170
  return void 0;
291
171
  }
292
- function serializeJsxAttrValue(value) {
293
- return `{${serializeJsValue(value)}}`;
294
- }
295
- function serializeJsValue(value) {
296
- if (value === null) return "null";
297
- if (value === void 0) return "undefined";
298
- if (typeof value === "string") return JSON.stringify(value);
299
- if (typeof value === "number" || typeof value === "boolean") return String(value);
300
- if (Array.isArray(value)) return `[${value.map(serializeJsValue).join(", ")}]`;
301
- if (typeof value === "object") {
302
- const entries = Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => `${safeObjectKey(k)}: ${serializeJsValue(v)}`);
303
- return `{ ${entries.join(", ")} }`;
304
- }
305
- return "undefined";
306
- }
307
- function safeObjectKey(key) {
308
- if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) return key;
309
- return JSON.stringify(key);
310
- }
311
- function escapeText(value) {
312
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
313
- }
314
- function indent(spaces) {
315
- return " ".repeat(spaces);
316
- }
317
-
318
- // src/llm/ollama.ts
319
- function createOllamaClient(options = {}) {
320
- const baseUrl = options.baseUrl ?? "http://localhost:11434";
321
- const fetchFn = options.fetchFn ?? fetch;
322
- return {
323
- async chat(req) {
324
- const res = await fetchFn(`${baseUrl}/api/chat`, {
325
- method: "POST",
326
- headers: { "content-type": "application/json" },
327
- body: JSON.stringify({
328
- model: req.model,
329
- messages: req.messages,
330
- stream: false,
331
- options: req.temperature == null ? void 0 : { temperature: req.temperature }
332
- })
333
- });
334
- if (!res.ok) {
335
- const text = await safeReadText(res);
336
- throw new Error(`Ollama chat failed (${res.status}): ${text}`);
337
- }
338
- const json = await res.json();
339
- const content = json?.message?.content;
340
- if (typeof content !== "string") throw new Error("Ollama response missing message.content");
341
- return content;
342
- }
343
- };
344
- }
345
- async function safeReadText(res) {
346
- try {
347
- return await res.text();
348
- } catch {
349
- return "";
350
- }
351
- }
352
-
353
- // src/llm/openaiCompatible.ts
354
- function createOpenAICompatibleClient(options) {
355
- const fetchFn = options.fetchFn ?? fetch;
356
- const baseUrl = options.baseUrl.replace(/\/+$/, "");
357
- const apiKey = options.apiKey;
358
- const defaultHeaders = options.defaultHeaders ?? {};
359
- return {
360
- async chat(req) {
361
- const res = await fetchFn(`${baseUrl}/v1/chat/completions`, {
362
- method: "POST",
363
- headers: {
364
- "content-type": "application/json",
365
- authorization: `Bearer ${apiKey}`,
366
- ...defaultHeaders
367
- },
368
- body: JSON.stringify({
369
- model: req.model,
370
- messages: req.messages,
371
- temperature: req.temperature
372
- })
373
- });
374
- if (!res.ok) {
375
- const text = await safeReadText2(res);
376
- throw new Error(`Chat completion failed (${res.status}): ${text}`);
377
- }
378
- const json = await res.json();
379
- const content = json?.choices?.[0]?.message?.content;
380
- if (typeof content !== "string") throw new Error("Response missing choices[0].message.content");
381
- return content;
382
- }
383
- };
384
- }
385
- async function safeReadText2(res) {
386
- try {
387
- return await res.text();
388
- } catch {
389
- return "";
390
- }
391
- }
392
172
 
393
- // src/llm/gemini.ts
394
- function createGeminiClient(options) {
395
- const baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com").replace(/\/+$/, "");
396
- const fetchFn = options.fetchFn ?? fetch;
397
- const apiKey = options.apiKey;
398
- return {
399
- async chat(req) {
400
- const { systemInstruction, contents } = toGeminiContents(req.messages);
401
- const url = `${baseUrl}/v1beta/models/${encodeURIComponent(req.model)}:generateContent?key=${encodeURIComponent(apiKey)}`;
402
- const res = await fetchFn(url, {
403
- method: "POST",
404
- headers: { "content-type": "application/json" },
405
- body: JSON.stringify({
406
- ...systemInstruction ? { systemInstruction } : {},
407
- contents,
408
- generationConfig: req.temperature == null ? void 0 : { temperature: req.temperature }
409
- })
410
- });
411
- if (!res.ok) {
412
- const text2 = await safeReadText3(res);
413
- throw new Error(`Gemini generateContent failed (${res.status}): ${text2}`);
414
- }
415
- const json = await res.json();
416
- const parts = json?.candidates?.[0]?.content?.parts;
417
- const text = Array.isArray(parts) ? parts.map((p) => typeof p?.text === "string" ? p.text : "").join("") : void 0;
418
- if (typeof text !== "string" || text.length === 0) throw new Error("Gemini response missing candidates[0].content.parts[].text");
419
- return text;
420
- }
421
- };
422
- }
423
- function toGeminiContents(messages) {
424
- const systemTexts = messages.filter((m) => m.role === "system").map((m) => m.content).filter((t) => t.trim().length > 0);
425
- const systemInstruction = systemTexts.length > 0 ? { role: "system", parts: [{ text: systemTexts.join("\n") }] } : void 0;
426
- const contents = messages.filter((m) => m.role !== "system").map((m) => ({
427
- role: m.role === "assistant" ? "model" : "user",
428
- parts: [{ text: m.content }]
429
- }));
430
- if (contents.length === 0) {
431
- contents.push({ role: "user", parts: [{ text: "" }] });
432
- }
433
- return { systemInstruction, contents };
434
- }
435
- async function safeReadText3(res) {
436
- try {
437
- return await res.text();
438
- } catch {
439
- return "";
440
- }
441
- }
442
-
443
- // src/ai/generateSpec.ts
444
- async function generateMoodUISpec(llm, options) {
445
- const maxAttempts = options.maxAttempts ?? 2;
446
- if (maxAttempts < 1) throw new Error("maxAttempts must be >= 1");
447
- const system = buildSystemPrompt();
448
- const user = buildUserPrompt(options.prompt);
449
- let lastRaw = "";
450
- let lastError = void 0;
451
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
452
- const messages = [
453
- { role: "system", content: system },
454
- { role: "user", content: user }
455
- ];
456
- if (attempt > 1) {
457
- messages.push({
458
- role: "user",
459
- content: "Perbaiki output kamu supaya valid JSON dan valid MoodUISpec. Output hanya JSON."
460
- });
461
- }
462
- const raw = await llm.chat({
463
- model: options.model,
464
- temperature: options.temperature,
465
- messages
466
- });
467
- lastRaw = raw;
173
+ // src/react/MoodUIPromptPlayground.tsx
174
+ import * as React2 from "react";
175
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
176
+ function MoodUIPromptPlayground(props) {
177
+ const provider = props.provider ?? "gemini";
178
+ const [model, setModel] = React2.useState(props.model ?? "gemini-3-flash-preview");
179
+ const [apiKey, setApiKey] = React2.useState(props.apiKey ?? "");
180
+ const [baseUrl, setBaseUrl] = React2.useState(props.baseUrl ?? "");
181
+ const [prompt, setPrompt] = React2.useState(
182
+ props.defaultPrompt ?? "Buat UI mood tracker: judul, input mood, tombol Simpan (actionId save_mood), dan section riwayat."
183
+ );
184
+ const [loading, setLoading] = React2.useState(false);
185
+ const [error, setError] = React2.useState(null);
186
+ const [spec, setSpec] = React2.useState(null);
187
+ const [code, setCode] = React2.useState("");
188
+ const onGenerate = React2.useCallback(async () => {
189
+ setLoading(true);
190
+ setError(null);
468
191
  try {
469
- const json = parseFirstJsonObject(raw);
470
- const spec = assertMoodUISpec(json);
471
- return { spec, raw };
192
+ const result = provider === "ollama" ? await generateReactFromPrompt({
193
+ provider: "ollama",
194
+ model,
195
+ baseUrl: baseUrl || void 0,
196
+ prompt,
197
+ componentName: props.componentName
198
+ }) : provider === "openai-compatible" ? await generateReactFromPrompt({
199
+ provider: "openai-compatible",
200
+ model,
201
+ baseUrl: baseUrl || "",
202
+ apiKey,
203
+ prompt,
204
+ componentName: props.componentName
205
+ }) : await generateReactFromPrompt({
206
+ provider: "gemini",
207
+ model,
208
+ apiKey,
209
+ baseUrl: baseUrl || void 0,
210
+ prompt,
211
+ componentName: props.componentName
212
+ });
213
+ setSpec(result.spec);
214
+ setCode(result.code);
472
215
  } catch (e) {
473
- lastError = e;
474
- }
475
- }
476
- const message = lastError instanceof Error ? lastError.message : String(lastError);
477
- throw new Error(`Failed to generate valid MoodUISpec. Last error: ${message}
478
-
479
- Raw:
480
- ${lastRaw}`);
481
- }
482
- function buildSystemPrompt() {
483
- return [
484
- "Kamu adalah generator JSON untuk MoodUI.",
485
- "TUGAS: keluarkan 1 objek JSON yang valid, sesuai schema MoodUISpec versi 1.",
486
- "JANGAN keluarkan markdown, JANGAN ada penjelasan, JANGAN pakai backticks.",
487
- "",
488
- "MoodUISpec shape:",
489
- "{",
490
- ' "version": 1,',
491
- ' "root": MoodUINode',
492
- "}",
493
- "",
494
- "MoodUINode union:",
495
- "- box: { type:'box', props?: { direction?, gap?, align?, justify?, wrap?, ...common }, children?: MoodUINode[] }",
496
- "- text: { type:'text', props: { value: string, as?, color?, fontSize?, fontWeight?, textAlign?, ...common } }",
497
- "- button: { type:'button', props: { label: string, variant?, actionId?, disabled?, ...common } }",
498
- "- input: { type:'input', props?: { name?, placeholder?, defaultValue?, ...common } }",
499
- "- image: { type:'image', props: { src: string, alt?, fit?, ...common } }",
500
- "- spacer: { type:'spacer', props?: { size? } }",
501
- "",
502
- "common props:",
503
- "- id, testId, className, style(object), padding, margin, background, borderRadius, width, height",
504
- "",
505
- "Rules:",
506
- "- root wajib ada",
507
- "- minimal pakai box sebagai container utama",
508
- "- semua string pakai double quotes (JSON standard)",
509
- "- jangan pakai function / JS expression apa pun"
510
- ].join("\n");
511
- }
512
- function buildUserPrompt(prompt) {
513
- return ["Buat UI dari request ini:", prompt].join("\n");
514
- }
515
- function parseFirstJsonObject(text) {
516
- const start = text.indexOf("{");
517
- if (start < 0) throw new Error("No JSON object found");
518
- let depth = 0;
519
- let inString = false;
520
- let escaped = false;
521
- for (let i = start; i < text.length; i += 1) {
522
- const ch = text[i];
523
- if (inString) {
524
- if (escaped) {
525
- escaped = false;
526
- } else if (ch === "\\") {
527
- escaped = true;
528
- } else if (ch === '"') {
529
- inString = false;
530
- }
531
- continue;
216
+ const msg = e instanceof Error ? e.message : String(e);
217
+ setError(msg);
218
+ } finally {
219
+ setLoading(false);
532
220
  }
533
- if (ch === '"') {
534
- inString = true;
535
- continue;
536
- }
537
- if (ch === "{") depth += 1;
538
- else if (ch === "}") depth -= 1;
539
- if (depth === 0) {
540
- const candidate = text.slice(start, i + 1);
541
- return JSON.parse(candidate);
542
- }
543
- }
544
- throw new Error("Unterminated JSON object");
545
- }
546
-
547
- // src/ai/generateReactFromPrompt.ts
548
- async function generateReactFromPrompt(options) {
549
- const llm = options.provider === "gemini" ? createGeminiClient({ apiKey: options.apiKey, baseUrl: options.baseUrl }) : options.provider === "ollama" ? createOllamaClient({ baseUrl: options.baseUrl }) : createOpenAICompatibleClient({ apiKey: options.apiKey, baseUrl: options.baseUrl });
550
- const { spec, raw } = await generateMoodUISpec(llm, {
551
- model: options.model,
552
- prompt: options.prompt,
553
- temperature: options.temperature,
554
- maxAttempts: options.maxAttempts
555
- });
556
- const code = renderReact(spec, { componentName: options.componentName });
557
- return { spec, raw, code };
221
+ }, [apiKey, baseUrl, model, prompt, props.componentName, provider]);
222
+ return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }, children: [
223
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
224
+ /* @__PURE__ */ jsx2("div", { style: { fontWeight: 700, fontSize: 16 }, children: "MoodUI Prompt" }),
225
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" }, children: [
226
+ /* @__PURE__ */ jsx2(
227
+ "input",
228
+ {
229
+ value: provider,
230
+ readOnly: true,
231
+ style: {
232
+ padding: "10px 12px",
233
+ borderRadius: 10,
234
+ border: "1px solid #d1d5db",
235
+ background: "#f3f4f6",
236
+ width: 200
237
+ }
238
+ }
239
+ ),
240
+ /* @__PURE__ */ jsx2(
241
+ "input",
242
+ {
243
+ value: model,
244
+ onChange: (e) => setModel(e.target.value),
245
+ placeholder: "model",
246
+ style: { padding: "10px 12px", borderRadius: 10, border: "1px solid #d1d5db", flex: "1 1 220px" }
247
+ }
248
+ )
249
+ ] }),
250
+ provider !== "ollama" ? /* @__PURE__ */ jsx2(
251
+ "input",
252
+ {
253
+ value: apiKey,
254
+ onChange: (e) => setApiKey(e.target.value),
255
+ placeholder: "API key",
256
+ type: "password",
257
+ style: { padding: "10px 12px", borderRadius: 10, border: "1px solid #d1d5db" }
258
+ }
259
+ ) : null,
260
+ /* @__PURE__ */ jsx2(
261
+ "input",
262
+ {
263
+ value: baseUrl,
264
+ onChange: (e) => setBaseUrl(e.target.value),
265
+ placeholder: provider === "ollama" ? "baseUrl (optional) e.g. http://localhost:11434" : "baseUrl (optional)",
266
+ style: { padding: "10px 12px", borderRadius: 10, border: "1px solid #d1d5db" }
267
+ }
268
+ ),
269
+ /* @__PURE__ */ jsx2(
270
+ "textarea",
271
+ {
272
+ value: prompt,
273
+ onChange: (e) => setPrompt(e.target.value),
274
+ rows: 10,
275
+ style: {
276
+ padding: 12,
277
+ borderRadius: 12,
278
+ border: "1px solid #d1d5db",
279
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
280
+ fontSize: 12
281
+ }
282
+ }
283
+ ),
284
+ /* @__PURE__ */ jsx2(
285
+ "button",
286
+ {
287
+ type: "button",
288
+ onClick: onGenerate,
289
+ disabled: loading,
290
+ style: {
291
+ padding: "10px 12px",
292
+ borderRadius: 10,
293
+ border: "1px solid #111827",
294
+ background: "#111827",
295
+ color: "#ffffff",
296
+ cursor: loading ? "not-allowed" : "pointer"
297
+ },
298
+ children: loading ? "Generating..." : "Generate"
299
+ }
300
+ ),
301
+ error ? /* @__PURE__ */ jsx2(
302
+ "pre",
303
+ {
304
+ style: {
305
+ margin: 0,
306
+ padding: 12,
307
+ borderRadius: 12,
308
+ border: "1px solid #7f1d1d",
309
+ background: "#fef2f2",
310
+ color: "#7f1d1d",
311
+ whiteSpace: "pre-wrap"
312
+ },
313
+ children: error
314
+ }
315
+ ) : null
316
+ ] }),
317
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
318
+ /* @__PURE__ */ jsx2("div", { style: { fontWeight: 700, fontSize: 16 }, children: "Preview" }),
319
+ /* @__PURE__ */ jsx2("div", { style: { minHeight: 240, padding: 16, borderRadius: 16, background: "#ffffff", border: "1px solid #e5e7eb" }, children: spec ? /* @__PURE__ */ jsx2(MoodUIRuntime, { spec }) : /* @__PURE__ */ jsx2("div", { style: { color: "#6b7280" }, children: "Belum ada hasil" }) }),
320
+ /* @__PURE__ */ jsx2("div", { style: { fontWeight: 700, fontSize: 16 }, children: "React Code" }),
321
+ /* @__PURE__ */ jsx2(
322
+ "textarea",
323
+ {
324
+ value: code,
325
+ readOnly: true,
326
+ rows: 12,
327
+ style: {
328
+ padding: 12,
329
+ borderRadius: 12,
330
+ border: "1px solid #d1d5db",
331
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
332
+ fontSize: 12
333
+ }
334
+ }
335
+ )
336
+ ] })
337
+ ] });
558
338
  }
559
339
  export {
340
+ MoodUIPromptPlayground,
341
+ MoodUIRuntime,
560
342
  assertMoodUISpec,
561
343
  createGeminiClient,
562
344
  createOllamaClient,