@kohryan/moodui 0.0.1 → 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/chunk-5D6KNM5J.mjs +570 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +715 -0
- package/dist/cli.mjs +137 -0
- package/dist/index.d.mts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +482 -116
- package/dist/index.mjs +279 -475
- package/package.json +7 -2
package/dist/index.mjs
CHANGED
|
@@ -1,254 +1,148 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
24
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
else if (!isString(props.src)) errors.push(`${path}.props.src must be a string`);
|
|
65
|
-
break;
|
|
59
|
+
const style = computeStyle(node);
|
|
60
|
+
return /* @__PURE__ */ jsx("img", { ...commonAttrs(node), style, src: node.props?.src, alt: node.props?.alt ?? "" });
|
|
66
61
|
}
|
|
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]);
|
|
123
|
-
}
|
|
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
|
-
|
|
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
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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) out.push(`style={${serializeJsValue(mergedStyle)}}`);
|
|
186
|
-
if (extraProps) {
|
|
187
|
-
for (const [k, v] of Object.entries(extraProps)) out.push(`${k}={${v}}`);
|
|
188
|
-
}
|
|
189
|
-
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
|
+
};
|
|
190
75
|
}
|
|
191
|
-
function computeStyle(
|
|
76
|
+
function computeStyle(node) {
|
|
192
77
|
const props = node.props ?? {};
|
|
193
78
|
const style = {};
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (
|
|
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;
|
|
198
86
|
applySpacing(style, "margin", props.margin);
|
|
199
87
|
applySpacing(style, "padding", props.padding);
|
|
200
88
|
if (node.type === "box") {
|
|
201
89
|
style.display = "flex";
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
if (props.
|
|
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;
|
|
207
97
|
}
|
|
208
98
|
if (node.type === "text") {
|
|
209
|
-
if (props.color
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
if (props.
|
|
213
|
-
if (
|
|
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;
|
|
214
106
|
}
|
|
215
107
|
if (node.type === "button") {
|
|
216
|
-
style.cursor = "pointer";
|
|
108
|
+
style.cursor = node.props?.disabled ? "not-allowed" : "pointer";
|
|
217
109
|
style.border = "none";
|
|
218
|
-
style.padding = "10px 14px";
|
|
219
|
-
style.borderRadius = 10;
|
|
220
|
-
const variant = props
|
|
110
|
+
if (style.padding == null) style.padding = "10px 14px";
|
|
111
|
+
if (style.borderRadius == null) style.borderRadius = 10;
|
|
112
|
+
const variant = node.props?.variant ?? "primary";
|
|
221
113
|
if (variant === "primary") {
|
|
222
|
-
style.background = "#111827";
|
|
223
|
-
style.color = "#ffffff";
|
|
114
|
+
if (style.background == null) style.background = "#111827";
|
|
115
|
+
if (style.color == null) style.color = "#ffffff";
|
|
224
116
|
} else if (variant === "secondary") {
|
|
225
|
-
style.background = "#e5e7eb";
|
|
226
|
-
style.color = "#111827";
|
|
117
|
+
if (style.background == null) style.background = "#e5e7eb";
|
|
118
|
+
if (style.color == null) style.color = "#111827";
|
|
227
119
|
} else {
|
|
228
|
-
style.background = "transparent";
|
|
229
|
-
style.color = "#111827";
|
|
230
|
-
}
|
|
231
|
-
if (props.disabled) {
|
|
232
|
-
style.opacity = 0.6;
|
|
233
|
-
style.cursor = "not-allowed";
|
|
120
|
+
if (style.background == null) style.background = "transparent";
|
|
121
|
+
if (style.color == null) style.color = "#111827";
|
|
234
122
|
}
|
|
123
|
+
if (node.props?.disabled) style.opacity = 0.6;
|
|
235
124
|
}
|
|
236
125
|
if (node.type === "input") {
|
|
237
|
-
style.padding = "10px 12px";
|
|
238
|
-
style.borderRadius = 10;
|
|
239
|
-
style.border = "1px solid #d1d5db";
|
|
240
|
-
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";
|
|
241
130
|
}
|
|
242
131
|
if (node.type === "image") {
|
|
243
|
-
if (
|
|
244
|
-
|
|
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);
|
|
245
138
|
}
|
|
246
139
|
return style;
|
|
247
140
|
}
|
|
248
141
|
function applySpacing(style, kind, value) {
|
|
249
142
|
if (value == null) return;
|
|
143
|
+
const s = style;
|
|
250
144
|
if (typeof value === "number" || typeof value === "string") {
|
|
251
|
-
|
|
145
|
+
s[kind] = toCssValue(value);
|
|
252
146
|
return;
|
|
253
147
|
}
|
|
254
148
|
if (typeof value !== "object" || Array.isArray(value)) return;
|
|
@@ -256,291 +150,201 @@ function applySpacing(style, kind, value) {
|
|
|
256
150
|
const all = v.all;
|
|
257
151
|
const x = v.x;
|
|
258
152
|
const y = v.y;
|
|
259
|
-
if (all != null)
|
|
153
|
+
if (all != null) s[kind] = toCssValue(all);
|
|
260
154
|
if (x != null) {
|
|
261
|
-
|
|
262
|
-
|
|
155
|
+
s[`${kind}Left`] = toCssValue(x);
|
|
156
|
+
s[`${kind}Right`] = toCssValue(x);
|
|
263
157
|
}
|
|
264
158
|
if (y != null) {
|
|
265
|
-
|
|
266
|
-
|
|
159
|
+
s[`${kind}Top`] = toCssValue(y);
|
|
160
|
+
s[`${kind}Bottom`] = toCssValue(y);
|
|
267
161
|
}
|
|
268
|
-
if (v.top != null)
|
|
269
|
-
if (v.right != null)
|
|
270
|
-
if (v.bottom != null)
|
|
271
|
-
if (v.left != null)
|
|
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);
|
|
272
166
|
}
|
|
273
|
-
function
|
|
274
|
-
if (!a && !b) return void 0;
|
|
275
|
-
if (!a) return b;
|
|
276
|
-
if (!b) return a;
|
|
277
|
-
return { ...a, ...b };
|
|
278
|
-
}
|
|
279
|
-
function normalizeCssValue(value) {
|
|
167
|
+
function toCssValue(value) {
|
|
280
168
|
if (typeof value === "number") return value;
|
|
281
169
|
if (typeof value === "string") return value;
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
function serializeJsxAttrValue(value) {
|
|
285
|
-
return `{${serializeJsValue(value)}}`;
|
|
286
|
-
}
|
|
287
|
-
function serializeJsValue(value) {
|
|
288
|
-
if (value === null) return "null";
|
|
289
|
-
if (value === void 0) return "undefined";
|
|
290
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
291
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
292
|
-
if (Array.isArray(value)) return `[${value.map(serializeJsValue).join(", ")}]`;
|
|
293
|
-
if (typeof value === "object") {
|
|
294
|
-
const entries = Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => `${safeObjectKey(k)}: ${serializeJsValue(v)}`);
|
|
295
|
-
return `{ ${entries.join(", ")} }`;
|
|
296
|
-
}
|
|
297
|
-
return "undefined";
|
|
298
|
-
}
|
|
299
|
-
function safeObjectKey(key) {
|
|
300
|
-
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) return key;
|
|
301
|
-
return JSON.stringify(key);
|
|
302
|
-
}
|
|
303
|
-
function escapeText(value) {
|
|
304
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
305
|
-
}
|
|
306
|
-
function indent(spaces) {
|
|
307
|
-
return " ".repeat(spaces);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// src/llm/ollama.ts
|
|
311
|
-
function createOllamaClient(options = {}) {
|
|
312
|
-
const baseUrl = options.baseUrl ?? "http://localhost:11434";
|
|
313
|
-
const fetchFn = options.fetchFn ?? fetch;
|
|
314
|
-
return {
|
|
315
|
-
async chat(req) {
|
|
316
|
-
const res = await fetchFn(`${baseUrl}/api/chat`, {
|
|
317
|
-
method: "POST",
|
|
318
|
-
headers: { "content-type": "application/json" },
|
|
319
|
-
body: JSON.stringify({
|
|
320
|
-
model: req.model,
|
|
321
|
-
messages: req.messages,
|
|
322
|
-
stream: false,
|
|
323
|
-
options: req.temperature == null ? void 0 : { temperature: req.temperature }
|
|
324
|
-
})
|
|
325
|
-
});
|
|
326
|
-
if (!res.ok) {
|
|
327
|
-
const text = await safeReadText(res);
|
|
328
|
-
throw new Error(`Ollama chat failed (${res.status}): ${text}`);
|
|
329
|
-
}
|
|
330
|
-
const json = await res.json();
|
|
331
|
-
const content = json?.message?.content;
|
|
332
|
-
if (typeof content !== "string") throw new Error("Ollama response missing message.content");
|
|
333
|
-
return content;
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
async function safeReadText(res) {
|
|
338
|
-
try {
|
|
339
|
-
return await res.text();
|
|
340
|
-
} catch {
|
|
341
|
-
return "";
|
|
342
|
-
}
|
|
170
|
+
return void 0;
|
|
343
171
|
}
|
|
344
172
|
|
|
345
|
-
// src/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
temperature: req.temperature
|
|
364
|
-
})
|
|
365
|
-
});
|
|
366
|
-
if (!res.ok) {
|
|
367
|
-
const text = await safeReadText2(res);
|
|
368
|
-
throw new Error(`Chat completion failed (${res.status}): ${text}`);
|
|
369
|
-
}
|
|
370
|
-
const json = await res.json();
|
|
371
|
-
const content = json?.choices?.[0]?.message?.content;
|
|
372
|
-
if (typeof content !== "string") throw new Error("Response missing choices[0].message.content");
|
|
373
|
-
return content;
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
async function safeReadText2(res) {
|
|
378
|
-
try {
|
|
379
|
-
return await res.text();
|
|
380
|
-
} catch {
|
|
381
|
-
return "";
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// src/llm/gemini.ts
|
|
386
|
-
function createGeminiClient(options) {
|
|
387
|
-
const baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com").replace(/\/+$/, "");
|
|
388
|
-
const fetchFn = options.fetchFn ?? fetch;
|
|
389
|
-
const apiKey = options.apiKey;
|
|
390
|
-
return {
|
|
391
|
-
async chat(req) {
|
|
392
|
-
const { systemInstruction, contents } = toGeminiContents(req.messages);
|
|
393
|
-
const url = `${baseUrl}/v1beta/models/${encodeURIComponent(req.model)}:generateContent?key=${encodeURIComponent(apiKey)}`;
|
|
394
|
-
const res = await fetchFn(url, {
|
|
395
|
-
method: "POST",
|
|
396
|
-
headers: { "content-type": "application/json" },
|
|
397
|
-
body: JSON.stringify({
|
|
398
|
-
...systemInstruction ? { systemInstruction } : {},
|
|
399
|
-
contents,
|
|
400
|
-
generationConfig: req.temperature == null ? void 0 : { temperature: req.temperature }
|
|
401
|
-
})
|
|
402
|
-
});
|
|
403
|
-
if (!res.ok) {
|
|
404
|
-
const text2 = await safeReadText3(res);
|
|
405
|
-
throw new Error(`Gemini generateContent failed (${res.status}): ${text2}`);
|
|
406
|
-
}
|
|
407
|
-
const json = await res.json();
|
|
408
|
-
const parts = json?.candidates?.[0]?.content?.parts;
|
|
409
|
-
const text = Array.isArray(parts) ? parts.map((p) => typeof p?.text === "string" ? p.text : "").join("") : void 0;
|
|
410
|
-
if (typeof text !== "string" || text.length === 0) throw new Error("Gemini response missing candidates[0].content.parts[].text");
|
|
411
|
-
return text;
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
function toGeminiContents(messages) {
|
|
416
|
-
const systemTexts = messages.filter((m) => m.role === "system").map((m) => m.content).filter((t) => t.trim().length > 0);
|
|
417
|
-
const systemInstruction = systemTexts.length > 0 ? { role: "system", parts: [{ text: systemTexts.join("\n") }] } : void 0;
|
|
418
|
-
const contents = messages.filter((m) => m.role !== "system").map((m) => ({
|
|
419
|
-
role: m.role === "assistant" ? "model" : "user",
|
|
420
|
-
parts: [{ text: m.content }]
|
|
421
|
-
}));
|
|
422
|
-
if (contents.length === 0) {
|
|
423
|
-
contents.push({ role: "user", parts: [{ text: "" }] });
|
|
424
|
-
}
|
|
425
|
-
return { systemInstruction, contents };
|
|
426
|
-
}
|
|
427
|
-
async function safeReadText3(res) {
|
|
428
|
-
try {
|
|
429
|
-
return await res.text();
|
|
430
|
-
} catch {
|
|
431
|
-
return "";
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// src/ai/generateSpec.ts
|
|
436
|
-
async function generateMoodUISpec(llm, options) {
|
|
437
|
-
const maxAttempts = options.maxAttempts ?? 2;
|
|
438
|
-
if (maxAttempts < 1) throw new Error("maxAttempts must be >= 1");
|
|
439
|
-
const system = buildSystemPrompt();
|
|
440
|
-
const user = buildUserPrompt(options.prompt);
|
|
441
|
-
let lastRaw = "";
|
|
442
|
-
let lastError = void 0;
|
|
443
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
444
|
-
const messages = [
|
|
445
|
-
{ role: "system", content: system },
|
|
446
|
-
{ role: "user", content: user }
|
|
447
|
-
];
|
|
448
|
-
if (attempt > 1) {
|
|
449
|
-
messages.push({
|
|
450
|
-
role: "user",
|
|
451
|
-
content: "Perbaiki output kamu supaya valid JSON dan valid MoodUISpec. Output hanya JSON."
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
const raw = await llm.chat({
|
|
455
|
-
model: options.model,
|
|
456
|
-
temperature: options.temperature,
|
|
457
|
-
messages
|
|
458
|
-
});
|
|
459
|
-
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);
|
|
460
191
|
try {
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
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);
|
|
464
215
|
} catch (e) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
throw new Error(`Failed to generate valid MoodUISpec. Last error: ${message}
|
|
470
|
-
|
|
471
|
-
Raw:
|
|
472
|
-
${lastRaw}`);
|
|
473
|
-
}
|
|
474
|
-
function buildSystemPrompt() {
|
|
475
|
-
return [
|
|
476
|
-
"Kamu adalah generator JSON untuk MoodUI.",
|
|
477
|
-
"TUGAS: keluarkan 1 objek JSON yang valid, sesuai schema MoodUISpec versi 1.",
|
|
478
|
-
"JANGAN keluarkan markdown, JANGAN ada penjelasan, JANGAN pakai backticks.",
|
|
479
|
-
"",
|
|
480
|
-
"MoodUISpec shape:",
|
|
481
|
-
"{",
|
|
482
|
-
' "version": 1,',
|
|
483
|
-
' "root": MoodUINode',
|
|
484
|
-
"}",
|
|
485
|
-
"",
|
|
486
|
-
"MoodUINode union:",
|
|
487
|
-
"- box: { type:'box', props?: { direction?, gap?, align?, justify?, wrap?, ...common }, children?: MoodUINode[] }",
|
|
488
|
-
"- text: { type:'text', props: { value: string, as?, color?, fontSize?, fontWeight?, textAlign?, ...common } }",
|
|
489
|
-
"- button: { type:'button', props: { label: string, variant?, actionId?, disabled?, ...common } }",
|
|
490
|
-
"- input: { type:'input', props?: { name?, placeholder?, defaultValue?, ...common } }",
|
|
491
|
-
"- image: { type:'image', props: { src: string, alt?, fit?, ...common } }",
|
|
492
|
-
"- spacer: { type:'spacer', props?: { size? } }",
|
|
493
|
-
"",
|
|
494
|
-
"common props:",
|
|
495
|
-
"- id, testId, className, style(object), padding, margin, background, borderRadius, width, height",
|
|
496
|
-
"",
|
|
497
|
-
"Rules:",
|
|
498
|
-
"- root wajib ada",
|
|
499
|
-
"- minimal pakai box sebagai container utama",
|
|
500
|
-
"- semua string pakai double quotes (JSON standard)",
|
|
501
|
-
"- jangan pakai function / JS expression apa pun"
|
|
502
|
-
].join("\n");
|
|
503
|
-
}
|
|
504
|
-
function buildUserPrompt(prompt) {
|
|
505
|
-
return ["Buat UI dari request ini:", prompt].join("\n");
|
|
506
|
-
}
|
|
507
|
-
function parseFirstJsonObject(text) {
|
|
508
|
-
const start = text.indexOf("{");
|
|
509
|
-
if (start < 0) throw new Error("No JSON object found");
|
|
510
|
-
let depth = 0;
|
|
511
|
-
let inString = false;
|
|
512
|
-
let escaped = false;
|
|
513
|
-
for (let i = start; i < text.length; i += 1) {
|
|
514
|
-
const ch = text[i];
|
|
515
|
-
if (inString) {
|
|
516
|
-
if (escaped) {
|
|
517
|
-
escaped = false;
|
|
518
|
-
} else if (ch === "\\") {
|
|
519
|
-
escaped = true;
|
|
520
|
-
} else if (ch === '"') {
|
|
521
|
-
inString = false;
|
|
522
|
-
}
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
if (ch === '"') {
|
|
526
|
-
inString = true;
|
|
527
|
-
continue;
|
|
216
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
217
|
+
setError(msg);
|
|
218
|
+
} finally {
|
|
219
|
+
setLoading(false);
|
|
528
220
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
+
] });
|
|
537
338
|
}
|
|
538
339
|
export {
|
|
340
|
+
MoodUIPromptPlayground,
|
|
341
|
+
MoodUIRuntime,
|
|
539
342
|
assertMoodUISpec,
|
|
540
343
|
createGeminiClient,
|
|
541
344
|
createOllamaClient,
|
|
542
345
|
createOpenAICompatibleClient,
|
|
543
346
|
generateMoodUISpec,
|
|
347
|
+
generateReactFromPrompt,
|
|
544
348
|
renderReact,
|
|
545
349
|
renderReactJSX,
|
|
546
350
|
validateMoodUISpec
|