@kohryan/moodui 0.0.2 → 0.0.4
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 +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +489 -114
- package/dist/index.mjs +307 -493
- package/package.json +7 -2
package/dist/index.mjs
CHANGED
|
@@ -1,256 +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;
|
|
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
|
-
|
|
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) {
|
|
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(
|
|
76
|
+
function computeStyle(node) {
|
|
194
77
|
const props = node.props ?? {};
|
|
195
78
|
const style = {};
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
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;
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
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;
|
|
209
97
|
}
|
|
210
98
|
if (node.type === "text") {
|
|
211
|
-
if (props.color
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
if (props.
|
|
215
|
-
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;
|
|
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
|
|
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 (
|
|
246
|
-
|
|
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
|
-
|
|
145
|
+
s[kind] = toCssValue(value);
|
|
254
146
|
return;
|
|
255
147
|
}
|
|
256
148
|
if (typeof value !== "object" || Array.isArray(value)) return;
|
|
@@ -258,305 +150,227 @@ 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)
|
|
153
|
+
if (all != null) s[kind] = toCssValue(all);
|
|
262
154
|
if (x != null) {
|
|
263
|
-
|
|
264
|
-
|
|
155
|
+
s[`${kind}Left`] = toCssValue(x);
|
|
156
|
+
s[`${kind}Right`] = toCssValue(x);
|
|
265
157
|
}
|
|
266
158
|
if (y != null) {
|
|
267
|
-
|
|
268
|
-
|
|
159
|
+
s[`${kind}Top`] = toCssValue(y);
|
|
160
|
+
s[`${kind}Bottom`] = toCssValue(y);
|
|
269
161
|
}
|
|
270
|
-
if (v.top != null)
|
|
271
|
-
if (v.right != null)
|
|
272
|
-
if (v.bottom != null)
|
|
273
|
-
if (v.left != null)
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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/
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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."
|
|
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 [copied, setCopied] = React2.useState(false);
|
|
189
|
+
const onGenerate = React2.useCallback(async () => {
|
|
190
|
+
setLoading(true);
|
|
191
|
+
setError(null);
|
|
192
|
+
setCopied(false);
|
|
193
|
+
try {
|
|
194
|
+
const result = provider === "ollama" ? await generateReactFromPrompt({
|
|
195
|
+
provider: "ollama",
|
|
196
|
+
model,
|
|
197
|
+
baseUrl: baseUrl || void 0,
|
|
198
|
+
prompt,
|
|
199
|
+
componentName: props.componentName
|
|
200
|
+
}) : provider === "openai-compatible" ? await generateReactFromPrompt({
|
|
201
|
+
provider: "openai-compatible",
|
|
202
|
+
model,
|
|
203
|
+
baseUrl: baseUrl || "",
|
|
204
|
+
apiKey,
|
|
205
|
+
prompt,
|
|
206
|
+
componentName: props.componentName
|
|
207
|
+
}) : await generateReactFromPrompt({
|
|
208
|
+
provider: "gemini",
|
|
209
|
+
model,
|
|
210
|
+
apiKey,
|
|
211
|
+
baseUrl: baseUrl || void 0,
|
|
212
|
+
prompt,
|
|
213
|
+
componentName: props.componentName
|
|
460
214
|
});
|
|
215
|
+
setSpec(result.spec);
|
|
216
|
+
setCode(result.code);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
219
|
+
setError(msg);
|
|
220
|
+
} finally {
|
|
221
|
+
setLoading(false);
|
|
461
222
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
messages
|
|
466
|
-
});
|
|
467
|
-
lastRaw = raw;
|
|
223
|
+
}, [apiKey, baseUrl, model, prompt, props.componentName, provider]);
|
|
224
|
+
const onCopyCode = React2.useCallback(async () => {
|
|
225
|
+
if (!code) return;
|
|
468
226
|
try {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
227
|
+
await navigator.clipboard.writeText(code);
|
|
228
|
+
setCopied(true);
|
|
229
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
472
230
|
} catch (e) {
|
|
473
|
-
|
|
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;
|
|
532
|
-
}
|
|
533
|
-
if (ch === '"') {
|
|
534
|
-
inString = true;
|
|
535
|
-
continue;
|
|
231
|
+
console.error("Failed to copy:", e);
|
|
536
232
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
233
|
+
}, [code]);
|
|
234
|
+
return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }, children: [
|
|
235
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
|
|
236
|
+
/* @__PURE__ */ jsx2("div", { style: { fontWeight: 700, fontSize: 16 }, children: "MoodUI Prompt" }),
|
|
237
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" }, children: [
|
|
238
|
+
/* @__PURE__ */ jsx2(
|
|
239
|
+
"input",
|
|
240
|
+
{
|
|
241
|
+
value: provider,
|
|
242
|
+
readOnly: true,
|
|
243
|
+
style: {
|
|
244
|
+
padding: "10px 12px",
|
|
245
|
+
borderRadius: 10,
|
|
246
|
+
border: "1px solid #d1d5db",
|
|
247
|
+
background: "#f3f4f6",
|
|
248
|
+
width: 200
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
),
|
|
252
|
+
/* @__PURE__ */ jsx2(
|
|
253
|
+
"input",
|
|
254
|
+
{
|
|
255
|
+
value: model,
|
|
256
|
+
onChange: (e) => setModel(e.target.value),
|
|
257
|
+
placeholder: "model",
|
|
258
|
+
style: { padding: "10px 12px", borderRadius: 10, border: "1px solid #d1d5db", flex: "1 1 220px" }
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
] }),
|
|
262
|
+
provider !== "ollama" ? /* @__PURE__ */ jsx2(
|
|
263
|
+
"input",
|
|
264
|
+
{
|
|
265
|
+
value: apiKey,
|
|
266
|
+
onChange: (e) => setApiKey(e.target.value),
|
|
267
|
+
placeholder: "API key",
|
|
268
|
+
type: "password",
|
|
269
|
+
style: { padding: "10px 12px", borderRadius: 10, border: "1px solid #d1d5db" }
|
|
270
|
+
}
|
|
271
|
+
) : null,
|
|
272
|
+
/* @__PURE__ */ jsx2(
|
|
273
|
+
"input",
|
|
274
|
+
{
|
|
275
|
+
value: baseUrl,
|
|
276
|
+
onChange: (e) => setBaseUrl(e.target.value),
|
|
277
|
+
placeholder: provider === "ollama" ? "baseUrl (optional) e.g. http://localhost:11434" : "baseUrl (optional)",
|
|
278
|
+
style: { padding: "10px 12px", borderRadius: 10, border: "1px solid #d1d5db" }
|
|
279
|
+
}
|
|
280
|
+
),
|
|
281
|
+
/* @__PURE__ */ jsx2(
|
|
282
|
+
"textarea",
|
|
283
|
+
{
|
|
284
|
+
value: prompt,
|
|
285
|
+
onChange: (e) => setPrompt(e.target.value),
|
|
286
|
+
rows: 10,
|
|
287
|
+
style: {
|
|
288
|
+
padding: 12,
|
|
289
|
+
borderRadius: 12,
|
|
290
|
+
border: "1px solid #d1d5db",
|
|
291
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
292
|
+
fontSize: 12
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
),
|
|
296
|
+
/* @__PURE__ */ jsx2(
|
|
297
|
+
"button",
|
|
298
|
+
{
|
|
299
|
+
type: "button",
|
|
300
|
+
onClick: onGenerate,
|
|
301
|
+
disabled: loading,
|
|
302
|
+
style: {
|
|
303
|
+
padding: "10px 12px",
|
|
304
|
+
borderRadius: 10,
|
|
305
|
+
border: "1px solid #111827",
|
|
306
|
+
background: "#111827",
|
|
307
|
+
color: "#ffffff",
|
|
308
|
+
cursor: loading ? "not-allowed" : "pointer"
|
|
309
|
+
},
|
|
310
|
+
children: loading ? "Generating..." : "Generate"
|
|
311
|
+
}
|
|
312
|
+
),
|
|
313
|
+
error ? /* @__PURE__ */ jsx2(
|
|
314
|
+
"pre",
|
|
315
|
+
{
|
|
316
|
+
style: {
|
|
317
|
+
margin: 0,
|
|
318
|
+
padding: 12,
|
|
319
|
+
borderRadius: 12,
|
|
320
|
+
border: "1px solid #7f1d1d",
|
|
321
|
+
background: "#fef2f2",
|
|
322
|
+
color: "#7f1d1d",
|
|
323
|
+
whiteSpace: "pre-wrap"
|
|
324
|
+
},
|
|
325
|
+
children: error
|
|
326
|
+
}
|
|
327
|
+
) : null
|
|
328
|
+
] }),
|
|
329
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
|
|
330
|
+
/* @__PURE__ */ jsx2("div", { style: { fontWeight: 700, fontSize: 16 }, children: "Preview" }),
|
|
331
|
+
/* @__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" }) }),
|
|
332
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
|
|
333
|
+
/* @__PURE__ */ jsx2("div", { style: { fontWeight: 700, fontSize: 16 }, children: "React Code" }),
|
|
334
|
+
/* @__PURE__ */ jsx2(
|
|
335
|
+
"button",
|
|
336
|
+
{
|
|
337
|
+
type: "button",
|
|
338
|
+
onClick: onCopyCode,
|
|
339
|
+
disabled: !code,
|
|
340
|
+
style: {
|
|
341
|
+
padding: "6px 12px",
|
|
342
|
+
borderRadius: 8,
|
|
343
|
+
border: copied ? "1px solid #16a34a" : "1px solid #6b7280",
|
|
344
|
+
background: copied ? "#dcfce7" : "#ffffff",
|
|
345
|
+
color: copied ? "#16a34a" : "#374151",
|
|
346
|
+
cursor: !code ? "not-allowed" : "pointer",
|
|
347
|
+
fontSize: 12
|
|
348
|
+
},
|
|
349
|
+
children: copied ? "Copied!" : "Copy"
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
] }),
|
|
353
|
+
/* @__PURE__ */ jsx2(
|
|
354
|
+
"textarea",
|
|
355
|
+
{
|
|
356
|
+
value: code,
|
|
357
|
+
readOnly: true,
|
|
358
|
+
rows: 12,
|
|
359
|
+
style: {
|
|
360
|
+
padding: 12,
|
|
361
|
+
borderRadius: 12,
|
|
362
|
+
border: "1px solid #d1d5db",
|
|
363
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
364
|
+
fontSize: 12
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
] })
|
|
369
|
+
] });
|
|
558
370
|
}
|
|
559
371
|
export {
|
|
372
|
+
MoodUIPromptPlayground,
|
|
373
|
+
MoodUIRuntime,
|
|
560
374
|
assertMoodUISpec,
|
|
561
375
|
createGeminiClient,
|
|
562
376
|
createOllamaClient,
|