@kohryan/moodui 0.0.14 → 0.0.15
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/cli.mjs +25 -1
- package/dist/ui/assets/index-ViFb9uE_.js +58 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/src/ui/main.tsx +310 -7
- package/dist/ui/assets/index-CMk6XQXy.js +0 -58
package/dist/ui/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>MoodUI - AI UI Generator</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-ViFb9uE_.js"></script>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/ui/main.tsx
CHANGED
|
@@ -1,11 +1,314 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
|
-
import { MoodUIPromptPlayground } from "../index";
|
|
3
|
+
import { MoodUIPromptPlayground, generateReactFromPrompt, MoodUIRuntime } from "../index";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
function App() {
|
|
6
|
+
const [provider, setProvider] = React.useState<"gemini" | "ollama" | "openai-compatible">("gemini");
|
|
7
|
+
const [model, setModel] = React.useState("gemini-3-flash-preview");
|
|
8
|
+
const [apiKey, setApiKey] = React.useState("");
|
|
9
|
+
const [baseUrl, setBaseUrl] = React.useState("");
|
|
10
|
+
const [prompt, setPrompt] = React.useState(
|
|
11
|
+
"Buat UI mood tracker: judul, input mood, tombol Simpan (actionId save_mood), dan section riwayat."
|
|
12
|
+
);
|
|
13
|
+
const [componentName, setComponentName] = React.useState("MoodScreen");
|
|
14
|
+
const [outputPath, setOutputPath] = React.useState("src/generated/MoodScreen.tsx");
|
|
15
|
+
|
|
16
|
+
const [loading, setLoading] = React.useState(false);
|
|
17
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
18
|
+
const [spec, setSpec] = React.useState<any>(null);
|
|
19
|
+
const [code, setCode] = React.useState("");
|
|
20
|
+
const [copied, setCopied] = React.useState(false);
|
|
21
|
+
const [saveStatus, setSaveStatus] = React.useState<"idle" | "saving" | "success" | "error">("idle");
|
|
22
|
+
|
|
23
|
+
const onGenerate = React.useCallback(async () => {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
setCopied(false);
|
|
27
|
+
setSaveStatus("idle");
|
|
28
|
+
try {
|
|
29
|
+
const result = await generateReactFromPrompt({
|
|
30
|
+
provider,
|
|
31
|
+
model,
|
|
32
|
+
apiKey: apiKey || undefined,
|
|
33
|
+
baseUrl: baseUrl || undefined,
|
|
34
|
+
prompt,
|
|
35
|
+
componentName
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
setSpec(result.spec);
|
|
39
|
+
setCode(result.code);
|
|
40
|
+
setOutputPath(`src/generated/${componentName}.tsx`);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
43
|
+
setError(msg);
|
|
44
|
+
} finally {
|
|
45
|
+
setLoading(false);
|
|
46
|
+
}
|
|
47
|
+
}, [apiKey, baseUrl, model, prompt, componentName, provider]);
|
|
48
|
+
|
|
49
|
+
const onCopyCode = React.useCallback(async () => {
|
|
50
|
+
if (!code) return;
|
|
51
|
+
try {
|
|
52
|
+
await navigator.clipboard.writeText(code);
|
|
53
|
+
setCopied(true);
|
|
54
|
+
setTimeout(() => setCopied(false), 2000);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error("Failed to copy:", e);
|
|
57
|
+
}
|
|
58
|
+
}, [code]);
|
|
59
|
+
|
|
60
|
+
const onDownloadCode = React.useCallback(() => {
|
|
61
|
+
if (!code) return;
|
|
62
|
+
const blob = new Blob([code], { type: "text/plain" });
|
|
63
|
+
const url = URL.createObjectURL(blob);
|
|
64
|
+
const a = document.createElement("a");
|
|
65
|
+
a.href = url;
|
|
66
|
+
a.download = `${componentName}.tsx`;
|
|
67
|
+
document.body.appendChild(a);
|
|
68
|
+
a.click();
|
|
69
|
+
document.body.removeChild(a);
|
|
70
|
+
URL.revokeObjectURL(url);
|
|
71
|
+
}, [code, componentName]);
|
|
72
|
+
|
|
73
|
+
const onSaveToProject = React.useCallback(async () => {
|
|
74
|
+
if (!code) return;
|
|
75
|
+
setSaveStatus("saving");
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch("/api/save-file", {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/json" },
|
|
80
|
+
body: JSON.stringify({ path: outputPath, code })
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const result = await response.json();
|
|
84
|
+
if (result.success) {
|
|
85
|
+
setSaveStatus("success");
|
|
86
|
+
setTimeout(() => setSaveStatus("idle"), 3000);
|
|
87
|
+
} else {
|
|
88
|
+
throw new Error(result.error);
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
92
|
+
console.error("Failed to save to project:", e);
|
|
93
|
+
setSaveStatus("error");
|
|
94
|
+
setTimeout(() => setSaveStatus("idle"), 3000);
|
|
95
|
+
}
|
|
96
|
+
}, [code, outputPath]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div style={{ minHeight: "100vh", padding: 24, background: "#0b1220" }}>
|
|
100
|
+
<div style={{ maxWidth: 1100, margin: "0 auto", background: "#fff", padding: 16, borderRadius: 16 }}>
|
|
101
|
+
<div style={{ marginBottom: 16, paddingBottom: 16, borderBottom: "1px solid #e5e7eb" }}>
|
|
102
|
+
<h2 style={{ margin: "0 0 12px 0", fontSize: 20, fontWeight: 700 }}>MoodUI - AI UI Generator</h2>
|
|
103
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
|
|
104
|
+
<div>
|
|
105
|
+
<label style={{ display: "block", marginBottom: 4, fontSize: 12, fontWeight: 600 }}>Component Name</label>
|
|
106
|
+
<input
|
|
107
|
+
value={componentName}
|
|
108
|
+
onChange={(e) => setComponentName(e.target.value)}
|
|
109
|
+
style={{ width: "100%", padding: "10px 12px", borderRadius: 10, border: "1px solid #d1d5db" }}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
<div>
|
|
113
|
+
<label style={{ display: "block", marginBottom: 4, fontSize: 12, fontWeight: 600 }}>Output Path</label>
|
|
114
|
+
<input
|
|
115
|
+
value={outputPath}
|
|
116
|
+
onChange={(e) => setOutputPath(e.target.value)}
|
|
117
|
+
style={{ width: "100%", padding: "10px 12px", borderRadius: 10, border: "1px solid #d1d5db" }}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
|
|
124
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
125
|
+
<div style={{ fontWeight: 700, fontSize: 16 }}>MoodUI Prompt</div>
|
|
126
|
+
|
|
127
|
+
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|
128
|
+
<select
|
|
129
|
+
value={provider}
|
|
130
|
+
onChange={(e) => setProvider(e.target.value as any)}
|
|
131
|
+
style={{
|
|
132
|
+
padding: "10px 12px",
|
|
133
|
+
borderRadius: 10,
|
|
134
|
+
border: "1px solid #d1d5db",
|
|
135
|
+
background: "#ffffff",
|
|
136
|
+
width: 200
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<option value="gemini">gemini</option>
|
|
140
|
+
<option value="ollama">ollama</option>
|
|
141
|
+
<option value="openai-compatible">openai-compatible</option>
|
|
142
|
+
</select>
|
|
143
|
+
<input
|
|
144
|
+
value={model}
|
|
145
|
+
onChange={(e) => setModel(e.target.value)}
|
|
146
|
+
placeholder="model"
|
|
147
|
+
style={{ padding: "10px 12px", borderRadius: 10, border: "1px solid #d1d5db", flex: "1 1 220px" }}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{provider !== "ollama" ? (
|
|
152
|
+
<input
|
|
153
|
+
value={apiKey}
|
|
154
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
155
|
+
placeholder="API key"
|
|
156
|
+
type="password"
|
|
157
|
+
style={{ padding: "10px 12px", borderRadius: 10, border: "1px solid #d1d5db" }}
|
|
158
|
+
/>
|
|
159
|
+
) : null}
|
|
160
|
+
|
|
161
|
+
<input
|
|
162
|
+
value={baseUrl}
|
|
163
|
+
onChange={(e) => setBaseUrl(e.target.value)}
|
|
164
|
+
placeholder={provider === "ollama" ? "baseUrl (optional) e.g. http://localhost:11434" : "baseUrl (optional)"}
|
|
165
|
+
style={{ padding: "10px 12px", borderRadius: 10, border: "1px solid #d1d5db" }}
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
<textarea
|
|
169
|
+
value={prompt}
|
|
170
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
171
|
+
rows={10}
|
|
172
|
+
style={{
|
|
173
|
+
padding: 12,
|
|
174
|
+
borderRadius: 12,
|
|
175
|
+
border: "1px solid #d1d5db",
|
|
176
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
177
|
+
fontSize: 12
|
|
178
|
+
}}
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onClick={onGenerate}
|
|
184
|
+
disabled={loading}
|
|
185
|
+
style={{
|
|
186
|
+
padding: "10px 12px",
|
|
187
|
+
borderRadius: 10,
|
|
188
|
+
border: "1px solid #111827",
|
|
189
|
+
background: "#111827",
|
|
190
|
+
color: "#ffffff",
|
|
191
|
+
cursor: loading ? "not-allowed" : "pointer"
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
{loading ? "Generating..." : "Generate"}
|
|
195
|
+
</button>
|
|
196
|
+
|
|
197
|
+
{error ? (
|
|
198
|
+
<pre
|
|
199
|
+
style={{
|
|
200
|
+
margin: 0,
|
|
201
|
+
padding: 12,
|
|
202
|
+
borderRadius: 12,
|
|
203
|
+
border: "1px solid #7f1d1d",
|
|
204
|
+
background: "#fef2f2",
|
|
205
|
+
color: "#7f1d1d",
|
|
206
|
+
whiteSpace: "pre-wrap"
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
{error}
|
|
210
|
+
</pre>
|
|
211
|
+
) : null}
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
215
|
+
<div style={{ fontWeight: 700, fontSize: 16 }}>Preview</div>
|
|
216
|
+
<div style={{ minHeight: 240, padding: 16, borderRadius: 16, background: "#ffffff", border: "1px solid #e5e7eb" }}>
|
|
217
|
+
{spec ? (
|
|
218
|
+
<MoodUIRuntime spec={spec} />
|
|
219
|
+
) : (
|
|
220
|
+
<div style={{ color: "#6b7280" }}>Belum ada hasil</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
225
|
+
<div style={{ fontWeight: 700, fontSize: 16 }}>React Code</div>
|
|
226
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
227
|
+
<button
|
|
228
|
+
type="button"
|
|
229
|
+
onClick={onCopyCode}
|
|
230
|
+
disabled={!code}
|
|
231
|
+
style={{
|
|
232
|
+
padding: "6px 12px",
|
|
233
|
+
borderRadius: 8,
|
|
234
|
+
border: copied ? "1px solid #16a34a" : "1px solid #6b7280",
|
|
235
|
+
background: copied ? "#dcfce7" : "#ffffff",
|
|
236
|
+
color: copied ? "#16a34a" : "#374151",
|
|
237
|
+
cursor: !code ? "not-allowed" : "pointer",
|
|
238
|
+
fontSize: 12
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
{copied ? "Copied!" : "Copy"}
|
|
242
|
+
</button>
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={onSaveToProject}
|
|
246
|
+
disabled={!code || saveStatus === "saving"}
|
|
247
|
+
style={{
|
|
248
|
+
padding: "6px 12px",
|
|
249
|
+
borderRadius: 8,
|
|
250
|
+
border: saveStatus === "success"
|
|
251
|
+
? "1px solid #16a34a"
|
|
252
|
+
: saveStatus === "error"
|
|
253
|
+
? "1px solid #7f1d1d"
|
|
254
|
+
: "1px solid #059669",
|
|
255
|
+
background: saveStatus === "success"
|
|
256
|
+
? "#dcfce7"
|
|
257
|
+
: saveStatus === "error"
|
|
258
|
+
? "#fef2f2"
|
|
259
|
+
: "#059669",
|
|
260
|
+
color: saveStatus === "success"
|
|
261
|
+
? "#16a34a"
|
|
262
|
+
: saveStatus === "error"
|
|
263
|
+
? "#7f1d1d"
|
|
264
|
+
: "#ffffff",
|
|
265
|
+
cursor: !code || saveStatus === "saving" ? "not-allowed" : "pointer",
|
|
266
|
+
fontSize: 12
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
{saveStatus === "saving"
|
|
270
|
+
? "Saving..."
|
|
271
|
+
: saveStatus === "success"
|
|
272
|
+
? "Saved!"
|
|
273
|
+
: saveStatus === "error"
|
|
274
|
+
? "Error!"
|
|
275
|
+
: "Save to Project"}
|
|
276
|
+
</button>
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
onClick={onDownloadCode}
|
|
280
|
+
disabled={!code}
|
|
281
|
+
style={{
|
|
282
|
+
padding: "6px 12px",
|
|
283
|
+
borderRadius: 8,
|
|
284
|
+
border: "1px solid #111827",
|
|
285
|
+
background: "#111827",
|
|
286
|
+
color: "#ffffff",
|
|
287
|
+
cursor: !code ? "not-allowed" : "pointer",
|
|
288
|
+
fontSize: 12
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
Download
|
|
292
|
+
</button>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
<textarea
|
|
296
|
+
value={code}
|
|
297
|
+
readOnly
|
|
298
|
+
rows={12}
|
|
299
|
+
style={{
|
|
300
|
+
padding: 12,
|
|
301
|
+
borderRadius: 12,
|
|
302
|
+
border: "1px solid #d1d5db",
|
|
303
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
304
|
+
fontSize: 12
|
|
305
|
+
}}
|
|
306
|
+
/>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
9
310
|
</div>
|
|
10
|
-
|
|
11
|
-
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
createRoot(document.getElementById("root")!).render(<App />);
|