@kohryan/moodui 0.0.13 → 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.
@@ -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-CMk6XQXy.js"></script>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kohryan/moodui",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "MoodUI - AI-friendly UI spec + React renderer",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
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
- createRoot(document.getElementById("root")!).render(
6
- <div style={{ minHeight: "100vh", padding: 24, background: "#0b1220" }}>
7
- <div style={{ maxWidth: 1100, margin: "0 auto", background: "#fff", padding: 16, borderRadius: 16 }}>
8
- <MoodUIPromptPlayground provider="gemini" model="gemini-3-flash-preview" />
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
- </div>
11
- );
311
+ );
312
+ }
313
+
314
+ createRoot(document.getElementById("root")!).render(<App />);
@@ -9,6 +9,9 @@ export default defineConfig({
9
9
  port: 3000,
10
10
  open: true
11
11
  },
12
+ build: {
13
+ emptyOutDir: true
14
+ },
12
15
  resolve: {
13
16
  alias: {
14
17
  "@kohryan/moodui": path.resolve(__dirname, "../index.ts")