@kohryan/moodui 0.0.10 → 0.0.11

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 CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  #!/usr/bin/env node
3
- import {
4
- generateReactFromPrompt
5
- } from "./chunk-WRMZ2MC6.mjs";
6
3
 
7
4
  // src/cli.ts
8
5
  import fs6 from "fs/promises";
@@ -524,6 +521,562 @@ defineLazyProperty(apps, "browser", () => "browser");
524
521
  defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
525
522
  var open_default = open;
526
523
 
524
+ // src/validate.ts
525
+ function validateMoodUISpec(input) {
526
+ const errors = [];
527
+ if (!isObject(input)) {
528
+ return { ok: false, errors: ["spec must be an object"] };
529
+ }
530
+ const version = input.version;
531
+ if (version !== 1) errors.push("spec.version must be 1");
532
+ const root = input.root;
533
+ const rootResult = validateNode(root, "spec.root");
534
+ if (!rootResult.ok) errors.push(...rootResult.errors);
535
+ if (errors.length > 0) return { ok: false, errors };
536
+ return { ok: true, value: input };
537
+ }
538
+ function assertMoodUISpec(input) {
539
+ const result = validateMoodUISpec(input);
540
+ if (!result.ok) {
541
+ const message = ["Invalid MoodUI spec:", ...result.errors.map((e) => `- ${e}`)].join("\n");
542
+ throw new Error(message);
543
+ }
544
+ return result.value;
545
+ }
546
+ function validateNode(input, path3) {
547
+ const errors = [];
548
+ if (!isObject(input)) return { ok: false, errors: [`${path3} must be an object`] };
549
+ const type = input.type;
550
+ if (!isString(type)) return { ok: false, errors: [`${path3}.type must be a string`] };
551
+ switch (type) {
552
+ case "box": {
553
+ const children = input.children;
554
+ if (children != null) {
555
+ if (!Array.isArray(children)) {
556
+ errors.push(`${path3}.children must be an array`);
557
+ } else {
558
+ for (let i = 0; i < children.length; i += 1) {
559
+ const child = children[i];
560
+ const childResult = validateNode(child, `${path3}.children[${i}]`);
561
+ if (!childResult.ok) errors.push(...childResult.errors);
562
+ }
563
+ }
564
+ }
565
+ break;
566
+ }
567
+ case "text": {
568
+ const props = input.props;
569
+ if (!isObject(props)) errors.push(`${path3}.props must be an object`);
570
+ else if (!isString(props.value)) errors.push(`${path3}.props.value must be a string`);
571
+ break;
572
+ }
573
+ case "button": {
574
+ const props = input.props;
575
+ if (!isObject(props)) errors.push(`${path3}.props must be an object`);
576
+ else if (!isString(props.label)) errors.push(`${path3}.props.label must be a string`);
577
+ break;
578
+ }
579
+ case "input": {
580
+ const props = input.props;
581
+ if (props != null && !isObject(props)) errors.push(`${path3}.props must be an object if provided`);
582
+ break;
583
+ }
584
+ case "image": {
585
+ const props = input.props;
586
+ if (!isObject(props)) errors.push(`${path3}.props must be an object`);
587
+ else if (!isString(props.src)) errors.push(`${path3}.props.src must be a string`);
588
+ break;
589
+ }
590
+ case "spacer": {
591
+ const props = input.props;
592
+ if (props != null && !isObject(props)) errors.push(`${path3}.props must be an object if provided`);
593
+ break;
594
+ }
595
+ default:
596
+ errors.push(`${path3}.type must be one of: box | text | button | input | image | spacer`);
597
+ }
598
+ if (errors.length > 0) return { ok: false, errors };
599
+ return { ok: true, value: input };
600
+ }
601
+ function isObject(value) {
602
+ return typeof value === "object" && value !== null && !Array.isArray(value);
603
+ }
604
+ function isString(value) {
605
+ return typeof value === "string";
606
+ }
607
+
608
+ // src/ai/generateSpec.ts
609
+ async function generateMoodUISpec(llm, options) {
610
+ const maxAttempts = options.maxAttempts ?? 2;
611
+ if (maxAttempts < 1) throw new Error("maxAttempts must be >= 1");
612
+ const system = buildSystemPrompt();
613
+ const user = buildUserPrompt(options.prompt);
614
+ let lastRaw = "";
615
+ let lastError = void 0;
616
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
617
+ const messages = [
618
+ { role: "system", content: system },
619
+ { role: "user", content: user }
620
+ ];
621
+ if (attempt > 1) {
622
+ messages.push({
623
+ role: "user",
624
+ content: "Perbaiki output kamu supaya valid JSON dan valid MoodUISpec. Output hanya JSON."
625
+ });
626
+ }
627
+ const raw = await llm.chat({
628
+ model: options.model,
629
+ temperature: options.temperature,
630
+ messages
631
+ });
632
+ lastRaw = raw;
633
+ try {
634
+ const json = parseFirstJsonObject(raw);
635
+ const spec = assertMoodUISpec(json);
636
+ return { spec, raw };
637
+ } catch (e) {
638
+ lastError = e;
639
+ }
640
+ }
641
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
642
+ throw new Error(`Failed to generate valid MoodUISpec. Last error: ${message}
643
+
644
+ Raw:
645
+ ${lastRaw}`);
646
+ }
647
+ function buildSystemPrompt() {
648
+ return [
649
+ "Kamu adalah generator JSON untuk MoodUI.",
650
+ "TUGAS: keluarkan 1 objek JSON yang valid, sesuai schema MoodUISpec versi 1.",
651
+ "JANGAN keluarkan markdown, JANGAN ada penjelasan, JANGAN pakai backticks.",
652
+ "",
653
+ "MoodUISpec shape:",
654
+ "{",
655
+ ' "version": 1,',
656
+ ' "root": MoodUINode',
657
+ "}",
658
+ "",
659
+ "MoodUINode union:",
660
+ "- box: { type:'box', props?: { direction?, gap?, align?, justify?, wrap?, ...common }, children?: MoodUINode[] }",
661
+ "- text: { type:'text', props: { value: string, as?, color?, fontSize?, fontWeight?, textAlign?, ...common } }",
662
+ "- button: { type:'button', props: { label: string, variant?, actionId?, disabled?, ...common } }",
663
+ "- input: { type:'input', props?: { name?, placeholder?, defaultValue?, ...common } }",
664
+ "- image: { type:'image', props: { src: string, alt?, fit?, ...common } }",
665
+ "- spacer: { type:'spacer', props?: { size? } }",
666
+ "",
667
+ "common props:",
668
+ "- id, testId, className, style(object), padding, margin, background, borderRadius, width, height",
669
+ "",
670
+ "Rules:",
671
+ "- root wajib ada",
672
+ "- minimal pakai box sebagai container utama",
673
+ "- semua string pakai double quotes (JSON standard)",
674
+ "- jangan pakai function / JS expression apa pun"
675
+ ].join("\n");
676
+ }
677
+ function buildUserPrompt(prompt) {
678
+ return ["Buat UI dari request ini:", prompt].join("\n");
679
+ }
680
+ function parseFirstJsonObject(text) {
681
+ const start = text.indexOf("{");
682
+ if (start < 0) throw new Error("No JSON object found");
683
+ let depth = 0;
684
+ let inString = false;
685
+ let escaped = false;
686
+ for (let i = start; i < text.length; i += 1) {
687
+ const ch = text[i];
688
+ if (inString) {
689
+ if (escaped) {
690
+ escaped = false;
691
+ } else if (ch === "\\") {
692
+ escaped = true;
693
+ } else if (ch === '"') {
694
+ inString = false;
695
+ }
696
+ continue;
697
+ }
698
+ if (ch === '"') {
699
+ inString = true;
700
+ continue;
701
+ }
702
+ if (ch === "{") depth += 1;
703
+ else if (ch === "}") depth -= 1;
704
+ if (depth === 0) {
705
+ const candidate = text.slice(start, i + 1);
706
+ return JSON.parse(candidate);
707
+ }
708
+ }
709
+ throw new Error("Unterminated JSON object");
710
+ }
711
+
712
+ // src/react/renderReact.ts
713
+ function renderReact(specInput, options = {}) {
714
+ const spec = assertMoodUISpec(specInput);
715
+ const componentName = options.componentName ?? "MoodUIScreen";
716
+ const jsx = renderNode(spec.root, { indent: 2, onActionProp: "onAction" });
717
+ return [
718
+ 'import * as React from "react";',
719
+ "",
720
+ "export type MoodUIScreenActionHandler = (actionId: string) => void;",
721
+ "",
722
+ "export type MoodUIScreenProps = {",
723
+ " onAction?: MoodUIScreenActionHandler;",
724
+ "};",
725
+ "",
726
+ `export function ${componentName}(props: MoodUIScreenProps) {`,
727
+ " const { onAction } = props;",
728
+ " return (",
729
+ jsx,
730
+ " );",
731
+ "}",
732
+ ""
733
+ ].join("\n");
734
+ }
735
+ function renderNode(node, ctx) {
736
+ switch (node.type) {
737
+ case "box":
738
+ return renderElement("div", node, ctx, () => {
739
+ const children = node.children ?? [];
740
+ if (children.length === 0) return [];
741
+ return children.map((child) => renderNode(child, { ...ctx, indent: ctx.indent + 2 }));
742
+ });
743
+ case "text": {
744
+ const as = node.props?.as ?? "p";
745
+ const text = escapeText(node.props?.value ?? "");
746
+ return renderElement(as, node, ctx, () => [indent(ctx.indent + 2) + text]);
747
+ }
748
+ case "button": {
749
+ const label = escapeText(node.props?.label ?? "");
750
+ const actionId = node.props?.actionId;
751
+ return renderElement(
752
+ "button",
753
+ node,
754
+ ctx,
755
+ () => [indent(ctx.indent + 2) + label],
756
+ actionId ? { onClick: `() => ${ctx.onActionProp}?.(${serializeJsValue(actionId)})` } : void 0
757
+ );
758
+ }
759
+ case "input":
760
+ return renderSelfClosingElement("input", node, ctx);
761
+ case "image":
762
+ return renderSelfClosingElement("img", node, ctx);
763
+ case "spacer": {
764
+ const size = node.props?.size ?? 8;
765
+ const style = { width: normalizeCssValue(size), height: normalizeCssValue(size) };
766
+ return renderElement(
767
+ "div",
768
+ { type: "box", props: { style }, children: [] },
769
+ ctx,
770
+ () => []
771
+ );
772
+ }
773
+ }
774
+ }
775
+ function renderElement(tag, node, ctx, renderChildren, extraProps) {
776
+ const children = renderChildren();
777
+ const propParts = buildProps(tag, node, extraProps);
778
+ const open2 = `<${tag}${propParts.length ? " " + propParts.join(" ") : ""}>`;
779
+ const close = `</${tag}>`;
780
+ if (children.length === 0) return indent(ctx.indent) + open2 + close;
781
+ return [
782
+ indent(ctx.indent) + open2,
783
+ ...children,
784
+ indent(ctx.indent) + close
785
+ ].join("\n");
786
+ }
787
+ function renderSelfClosingElement(tag, node, ctx) {
788
+ const propParts = buildProps(tag, node);
789
+ return indent(ctx.indent) + `<${tag}${propParts.length ? " " + propParts.join(" ") : ""} />`;
790
+ }
791
+ function buildProps(tag, node, extraProps) {
792
+ const props = node.props ?? {};
793
+ const out = [];
794
+ if (props.id) out.push(`id=${serializeJsxAttrValue(props.id)}`);
795
+ if (props.testId) out.push(`data-testid=${serializeJsxAttrValue(props.testId)}`);
796
+ if (props.className) out.push(`className=${serializeJsxAttrValue(props.className)}`);
797
+ const computedStyle = computeStyle(tag, node);
798
+ const mergedStyle = mergeStyle(computedStyle, props.style) ?? {};
799
+ if (tag === "input") {
800
+ if (props.name) out.push(`name=${serializeJsxAttrValue(props.name)}`);
801
+ if (props.placeholder) out.push(`placeholder=${serializeJsxAttrValue(props.placeholder)}`);
802
+ if (props.defaultValue) out.push(`defaultValue=${serializeJsxAttrValue(props.defaultValue)}`);
803
+ }
804
+ if (tag === "img") {
805
+ if (props.src) out.push(`src=${serializeJsxAttrValue(props.src)}`);
806
+ if (props.alt) out.push(`alt=${serializeJsxAttrValue(props.alt)}`);
807
+ if (props.fit) mergedStyle.objectFit = props.fit;
808
+ }
809
+ if (Object.keys(mergedStyle).length > 0) {
810
+ out.push(`style={(${serializeJsValue(mergedStyle)} as React.CSSProperties)}`);
811
+ }
812
+ if (extraProps) {
813
+ for (const [k, v] of Object.entries(extraProps)) out.push(`${k}={${v}}`);
814
+ }
815
+ return out;
816
+ }
817
+ function computeStyle(tag, node) {
818
+ const props = node.props ?? {};
819
+ const style = {};
820
+ if (props.width != null) style.width = normalizeCssValue(props.width);
821
+ if (props.height != null) style.height = normalizeCssValue(props.height);
822
+ if (props.background != null) style.background = props.background;
823
+ if (props.borderRadius != null) style.borderRadius = normalizeCssValue(props.borderRadius);
824
+ applySpacing(style, "margin", props.margin);
825
+ applySpacing(style, "padding", props.padding);
826
+ if (node.type === "box") {
827
+ style.display = "flex";
828
+ style.flexDirection = normalizeFlexDirection(props.direction) ?? "column";
829
+ if (props.gap != null) style.gap = normalizeCssValue(props.gap);
830
+ if (props.align != null) style.alignItems = props.align;
831
+ if (props.justify != null) style.justifyContent = props.justify;
832
+ if (props.wrap != null) style.flexWrap = props.wrap;
833
+ }
834
+ if (node.type === "text") {
835
+ if (props.color != null) style.color = props.color;
836
+ if (props.fontSize != null) style.fontSize = normalizeCssValue(props.fontSize);
837
+ if (props.fontWeight != null) style.fontWeight = props.fontWeight;
838
+ if (props.textAlign != null) style.textAlign = props.textAlign;
839
+ if (tag.startsWith("h")) style.margin = 0;
840
+ }
841
+ if (node.type === "button") {
842
+ style.cursor = "pointer";
843
+ style.border = "none";
844
+ style.padding = "10px 14px";
845
+ style.borderRadius = 10;
846
+ const variant = props.variant ?? "primary";
847
+ if (variant === "primary") {
848
+ style.background = "#111827";
849
+ style.color = "#ffffff";
850
+ } else if (variant === "secondary") {
851
+ style.background = "#e5e7eb";
852
+ style.color = "#111827";
853
+ } else {
854
+ style.background = "transparent";
855
+ style.color = "#111827";
856
+ }
857
+ if (props.disabled) {
858
+ style.opacity = 0.6;
859
+ style.cursor = "not-allowed";
860
+ }
861
+ }
862
+ if (node.type === "input") {
863
+ style.padding = "10px 12px";
864
+ style.borderRadius = 10;
865
+ style.border = "1px solid #d1d5db";
866
+ style.outline = "none";
867
+ }
868
+ if (node.type === "image") {
869
+ if (props.fit != null) style.objectFit = props.fit;
870
+ style.maxWidth = "100%";
871
+ }
872
+ return style;
873
+ }
874
+ function applySpacing(style, kind, value) {
875
+ if (value == null) return;
876
+ if (typeof value === "number" || typeof value === "string") {
877
+ style[kind] = normalizeCssValue(value);
878
+ return;
879
+ }
880
+ if (typeof value !== "object" || Array.isArray(value)) return;
881
+ const v = value;
882
+ const all = v.all;
883
+ const x = v.x;
884
+ const y = v.y;
885
+ if (all != null) style[kind] = normalizeCssValue(all);
886
+ if (x != null) {
887
+ style[`${kind}Left`] = normalizeCssValue(x);
888
+ style[`${kind}Right`] = normalizeCssValue(x);
889
+ }
890
+ if (y != null) {
891
+ style[`${kind}Top`] = normalizeCssValue(y);
892
+ style[`${kind}Bottom`] = normalizeCssValue(y);
893
+ }
894
+ if (v.top != null) style[`${kind}Top`] = normalizeCssValue(v.top);
895
+ if (v.right != null) style[`${kind}Right`] = normalizeCssValue(v.right);
896
+ if (v.bottom != null) style[`${kind}Bottom`] = normalizeCssValue(v.bottom);
897
+ if (v.left != null) style[`${kind}Left`] = normalizeCssValue(v.left);
898
+ }
899
+ function mergeStyle(a, b) {
900
+ if (!a && !b) return void 0;
901
+ if (!a) return b;
902
+ if (!b) return a;
903
+ return { ...a, ...b };
904
+ }
905
+ function normalizeCssValue(value) {
906
+ if (typeof value === "number") return value;
907
+ if (typeof value === "string") return value;
908
+ return value;
909
+ }
910
+ function normalizeFlexDirection(value) {
911
+ if (value === "row" || value === "column") return value;
912
+ if (value === "horizontal") return "row";
913
+ if (value === "vertical") return "column";
914
+ return void 0;
915
+ }
916
+ function serializeJsxAttrValue(value) {
917
+ return `{${serializeJsValue(value)}}`;
918
+ }
919
+ function serializeJsValue(value) {
920
+ if (value === null) return "null";
921
+ if (value === void 0) return "undefined";
922
+ if (typeof value === "string") return JSON.stringify(value);
923
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
924
+ if (Array.isArray(value)) return `[${value.map(serializeJsValue).join(", ")}]`;
925
+ if (typeof value === "object") {
926
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => `${safeObjectKey(k)}: ${serializeJsValue(v)}`);
927
+ return `{ ${entries.join(", ")} }`;
928
+ }
929
+ return "undefined";
930
+ }
931
+ function safeObjectKey(key) {
932
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) return key;
933
+ return JSON.stringify(key);
934
+ }
935
+ function escapeText(value) {
936
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
937
+ }
938
+ function indent(spaces) {
939
+ return " ".repeat(spaces);
940
+ }
941
+
942
+ // src/llm/gemini.ts
943
+ function createGeminiClient(options) {
944
+ const baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com").replace(/\/+$/, "");
945
+ const fetchFn = options.fetchFn ?? fetch;
946
+ const apiKey = options.apiKey;
947
+ return {
948
+ async chat(req) {
949
+ const { systemInstruction, contents } = toGeminiContents(req.messages);
950
+ const url = `${baseUrl}/v1beta/models/${encodeURIComponent(req.model)}:generateContent?key=${encodeURIComponent(apiKey)}`;
951
+ const res = await fetchFn(url, {
952
+ method: "POST",
953
+ headers: { "content-type": "application/json" },
954
+ body: JSON.stringify({
955
+ ...systemInstruction ? { systemInstruction } : {},
956
+ contents,
957
+ generationConfig: req.temperature == null ? void 0 : { temperature: req.temperature }
958
+ })
959
+ });
960
+ if (!res.ok) {
961
+ const text2 = await safeReadText(res);
962
+ throw new Error(`Gemini generateContent failed (${res.status}): ${text2}`);
963
+ }
964
+ const json = await res.json();
965
+ const parts = json?.candidates?.[0]?.content?.parts;
966
+ const text = Array.isArray(parts) ? parts.map((p) => typeof p?.text === "string" ? p.text : "").join("") : void 0;
967
+ if (typeof text !== "string" || text.length === 0) throw new Error("Gemini response missing candidates[0].content.parts[].text");
968
+ return text;
969
+ }
970
+ };
971
+ }
972
+ function toGeminiContents(messages) {
973
+ const systemTexts = messages.filter((m) => m.role === "system").map((m) => m.content).filter((t) => t.trim().length > 0);
974
+ const systemInstruction = systemTexts.length > 0 ? { role: "system", parts: [{ text: systemTexts.join("\n") }] } : void 0;
975
+ const contents = messages.filter((m) => m.role !== "system").map((m) => ({
976
+ role: m.role === "assistant" ? "model" : "user",
977
+ parts: [{ text: m.content }]
978
+ }));
979
+ if (contents.length === 0) {
980
+ contents.push({ role: "user", parts: [{ text: "" }] });
981
+ }
982
+ return { systemInstruction, contents };
983
+ }
984
+ async function safeReadText(res) {
985
+ try {
986
+ return await res.text();
987
+ } catch {
988
+ return "";
989
+ }
990
+ }
991
+
992
+ // src/llm/ollama.ts
993
+ function createOllamaClient(options = {}) {
994
+ const baseUrl = options.baseUrl ?? "http://localhost:11434";
995
+ const fetchFn = options.fetchFn ?? fetch;
996
+ return {
997
+ async chat(req) {
998
+ const res = await fetchFn(`${baseUrl}/api/chat`, {
999
+ method: "POST",
1000
+ headers: { "content-type": "application/json" },
1001
+ body: JSON.stringify({
1002
+ model: req.model,
1003
+ messages: req.messages,
1004
+ stream: false,
1005
+ options: req.temperature == null ? void 0 : { temperature: req.temperature }
1006
+ })
1007
+ });
1008
+ if (!res.ok) {
1009
+ const text = await safeReadText2(res);
1010
+ throw new Error(`Ollama chat failed (${res.status}): ${text}`);
1011
+ }
1012
+ const json = await res.json();
1013
+ const content = json?.message?.content;
1014
+ if (typeof content !== "string") throw new Error("Ollama response missing message.content");
1015
+ return content;
1016
+ }
1017
+ };
1018
+ }
1019
+ async function safeReadText2(res) {
1020
+ try {
1021
+ return await res.text();
1022
+ } catch {
1023
+ return "";
1024
+ }
1025
+ }
1026
+
1027
+ // src/llm/openaiCompatible.ts
1028
+ function createOpenAICompatibleClient(options) {
1029
+ const fetchFn = options.fetchFn ?? fetch;
1030
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
1031
+ const apiKey = options.apiKey;
1032
+ const defaultHeaders = options.defaultHeaders ?? {};
1033
+ return {
1034
+ async chat(req) {
1035
+ const res = await fetchFn(`${baseUrl}/v1/chat/completions`, {
1036
+ method: "POST",
1037
+ headers: {
1038
+ "content-type": "application/json",
1039
+ authorization: `Bearer ${apiKey}`,
1040
+ ...defaultHeaders
1041
+ },
1042
+ body: JSON.stringify({
1043
+ model: req.model,
1044
+ messages: req.messages,
1045
+ temperature: req.temperature
1046
+ })
1047
+ });
1048
+ if (!res.ok) {
1049
+ const text = await safeReadText3(res);
1050
+ throw new Error(`Chat completion failed (${res.status}): ${text}`);
1051
+ }
1052
+ const json = await res.json();
1053
+ const content = json?.choices?.[0]?.message?.content;
1054
+ if (typeof content !== "string") throw new Error("Response missing choices[0].message.content");
1055
+ return content;
1056
+ }
1057
+ };
1058
+ }
1059
+ async function safeReadText3(res) {
1060
+ try {
1061
+ return await res.text();
1062
+ } catch {
1063
+ return "";
1064
+ }
1065
+ }
1066
+
1067
+ // src/ai/generateReactFromPrompt.ts
1068
+ async function generateReactFromPrompt(options) {
1069
+ 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 });
1070
+ const { spec, raw } = await generateMoodUISpec(llm, {
1071
+ model: options.model,
1072
+ prompt: options.prompt,
1073
+ temperature: options.temperature,
1074
+ maxAttempts: options.maxAttempts
1075
+ });
1076
+ const code = renderReact(spec, { componentName: options.componentName });
1077
+ return { spec, raw, code };
1078
+ }
1079
+
527
1080
  // src/cli.ts
528
1081
  var __dirname2 = path2.dirname(fileURLToPath2(import.meta.url));
529
1082
  async function main() {
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  "use strict";
3
2
  var __create = Object.create;
4
3
  var __defProp = Object.defineProperty;
package/dist/index.mjs CHANGED
@@ -1,36 +1,340 @@
1
- #!/usr/bin/env node
2
- import {
3
- assertMoodUISpec,
4
- createGeminiClient,
5
- createOllamaClient,
6
- createOpenAICompatibleClient,
7
- generateMoodUISpec,
8
- generateReactFromPrompt,
9
- renderReact,
10
- renderReactJSX,
11
- validateMoodUISpec
12
- } from "./chunk-WRMZ2MC6.mjs";
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;
22
+ }
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) {
29
+ 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;
43
+ }
44
+ 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;
49
+ }
50
+ 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;
55
+ }
56
+ 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;
60
+ }
61
+ 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 jsx3 = 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
+ jsx3,
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
+ case "spacer": {
140
+ 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
+ );
148
+ }
149
+ }
150
+ }
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;
192
+ }
193
+ function computeStyle(tag, node) {
194
+ const props = node.props ?? {};
195
+ 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);
200
+ applySpacing(style, "margin", props.margin);
201
+ applySpacing(style, "padding", props.padding);
202
+ if (node.type === "box") {
203
+ 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;
209
+ }
210
+ 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;
216
+ }
217
+ if (node.type === "button") {
218
+ style.cursor = "pointer";
219
+ style.border = "none";
220
+ style.padding = "10px 14px";
221
+ style.borderRadius = 10;
222
+ const variant = props.variant ?? "primary";
223
+ if (variant === "primary") {
224
+ style.background = "#111827";
225
+ style.color = "#ffffff";
226
+ } else if (variant === "secondary") {
227
+ style.background = "#e5e7eb";
228
+ style.color = "#111827";
229
+ } else {
230
+ style.background = "transparent";
231
+ style.color = "#111827";
232
+ }
233
+ if (props.disabled) {
234
+ style.opacity = 0.6;
235
+ style.cursor = "not-allowed";
236
+ }
237
+ }
238
+ if (node.type === "input") {
239
+ style.padding = "10px 12px";
240
+ style.borderRadius = 10;
241
+ style.border = "1px solid #d1d5db";
242
+ style.outline = "none";
243
+ }
244
+ if (node.type === "image") {
245
+ if (props.fit != null) style.objectFit = props.fit;
246
+ style.maxWidth = "100%";
247
+ }
248
+ return style;
249
+ }
250
+ function applySpacing(style, kind, value) {
251
+ if (value == null) return;
252
+ if (typeof value === "number" || typeof value === "string") {
253
+ style[kind] = normalizeCssValue(value);
254
+ return;
255
+ }
256
+ if (typeof value !== "object" || Array.isArray(value)) return;
257
+ const v = value;
258
+ const all = v.all;
259
+ const x = v.x;
260
+ const y = v.y;
261
+ if (all != null) style[kind] = normalizeCssValue(all);
262
+ if (x != null) {
263
+ style[`${kind}Left`] = normalizeCssValue(x);
264
+ style[`${kind}Right`] = normalizeCssValue(x);
265
+ }
266
+ if (y != null) {
267
+ style[`${kind}Top`] = normalizeCssValue(y);
268
+ style[`${kind}Bottom`] = normalizeCssValue(y);
269
+ }
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 };
280
+ }
281
+ function normalizeCssValue(value) {
282
+ if (typeof value === "number") return value;
283
+ 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
+ return void 0;
291
+ }
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
+ }
13
317
 
14
318
  // src/react/MoodUIRuntime.tsx
15
319
  import * as React from "react";
16
320
  import { Fragment as Fragment2, jsx } from "react/jsx-runtime";
17
321
  function MoodUIRuntime(props) {
18
- return /* @__PURE__ */ jsx(Fragment2, { children: renderNode(props.spec.root, props.onAction) });
322
+ return /* @__PURE__ */ jsx(Fragment2, { children: renderNode2(props.spec.root, props.onAction) });
19
323
  }
20
- function renderNode(node, onAction) {
324
+ function renderNode2(node, onAction) {
21
325
  switch (node.type) {
22
326
  case "box": {
23
- const style = computeStyle(node);
24
- const children = node.children?.map((c, i) => /* @__PURE__ */ jsx(React.Fragment, { children: renderNode(c, onAction) }, i));
327
+ const style = computeStyle2(node);
328
+ const children = node.children?.map((c, i) => /* @__PURE__ */ jsx(React.Fragment, { children: renderNode2(c, onAction) }, i));
25
329
  return /* @__PURE__ */ jsx("div", { ...commonAttrs(node), style, children });
26
330
  }
27
331
  case "text": {
28
332
  const Tag = node.props?.as ?? "p";
29
- const style = computeStyle(node);
333
+ const style = computeStyle2(node);
30
334
  return /* @__PURE__ */ jsx(Tag, { ...commonAttrs(node), style, children: node.props?.value ?? "" });
31
335
  }
32
336
  case "button": {
33
- const style = computeStyle(node);
337
+ const style = computeStyle2(node);
34
338
  const actionId = node.props?.actionId;
35
339
  return /* @__PURE__ */ jsx(
36
340
  "button",
@@ -44,7 +348,7 @@ function renderNode(node, onAction) {
44
348
  );
45
349
  }
46
350
  case "input": {
47
- const style = computeStyle(node);
351
+ const style = computeStyle2(node);
48
352
  return /* @__PURE__ */ jsx(
49
353
  "input",
50
354
  {
@@ -57,7 +361,7 @@ function renderNode(node, onAction) {
57
361
  );
58
362
  }
59
363
  case "image": {
60
- const style = computeStyle(node);
364
+ const style = computeStyle2(node);
61
365
  return /* @__PURE__ */ jsx("img", { ...commonAttrs(node), style, src: node.props?.src, alt: node.props?.alt ?? "" });
62
366
  }
63
367
  case "spacer": {
@@ -74,7 +378,7 @@ function commonAttrs(node) {
74
378
  "data-testid": typeof p.testId === "string" ? p.testId : void 0
75
379
  };
76
380
  }
77
- function computeStyle(node) {
381
+ function computeStyle2(node) {
78
382
  const props = node.props ?? {};
79
383
  const style = {};
80
384
  const width = toCssValue(props.width);
@@ -84,8 +388,8 @@ function computeStyle(node) {
84
388
  if (height != null) style.height = height;
85
389
  if (typeof props.background === "string") style.background = props.background;
86
390
  if (borderRadius != null) style.borderRadius = borderRadius;
87
- applySpacing(style, "margin", props.margin);
88
- applySpacing(style, "padding", props.padding);
391
+ applySpacing2(style, "margin", props.margin);
392
+ applySpacing2(style, "padding", props.padding);
89
393
  if (node.type === "box") {
90
394
  style.display = "flex";
91
395
  if (props.direction === "row" || props.direction === "column") style.flexDirection = props.direction;
@@ -139,7 +443,7 @@ function computeStyle(node) {
139
443
  }
140
444
  return style;
141
445
  }
142
- function applySpacing(style, kind, value) {
446
+ function applySpacing2(style, kind, value) {
143
447
  if (value == null) return;
144
448
  const s = style;
145
449
  if (typeof value === "number" || typeof value === "string") {
@@ -173,6 +477,250 @@ function toCssValue(value) {
173
477
 
174
478
  // src/react/MoodUIPromptPlayground.tsx
175
479
  import * as React2 from "react";
480
+
481
+ // src/ai/generateSpec.ts
482
+ async function generateMoodUISpec(llm, options) {
483
+ const maxAttempts = options.maxAttempts ?? 2;
484
+ if (maxAttempts < 1) throw new Error("maxAttempts must be >= 1");
485
+ const system = buildSystemPrompt();
486
+ const user = buildUserPrompt(options.prompt);
487
+ let lastRaw = "";
488
+ let lastError = void 0;
489
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
490
+ const messages = [
491
+ { role: "system", content: system },
492
+ { role: "user", content: user }
493
+ ];
494
+ if (attempt > 1) {
495
+ messages.push({
496
+ role: "user",
497
+ content: "Perbaiki output kamu supaya valid JSON dan valid MoodUISpec. Output hanya JSON."
498
+ });
499
+ }
500
+ const raw = await llm.chat({
501
+ model: options.model,
502
+ temperature: options.temperature,
503
+ messages
504
+ });
505
+ lastRaw = raw;
506
+ try {
507
+ const json = parseFirstJsonObject(raw);
508
+ const spec = assertMoodUISpec(json);
509
+ return { spec, raw };
510
+ } catch (e) {
511
+ lastError = e;
512
+ }
513
+ }
514
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
515
+ throw new Error(`Failed to generate valid MoodUISpec. Last error: ${message}
516
+
517
+ Raw:
518
+ ${lastRaw}`);
519
+ }
520
+ function buildSystemPrompt() {
521
+ return [
522
+ "Kamu adalah generator JSON untuk MoodUI.",
523
+ "TUGAS: keluarkan 1 objek JSON yang valid, sesuai schema MoodUISpec versi 1.",
524
+ "JANGAN keluarkan markdown, JANGAN ada penjelasan, JANGAN pakai backticks.",
525
+ "",
526
+ "MoodUISpec shape:",
527
+ "{",
528
+ ' "version": 1,',
529
+ ' "root": MoodUINode',
530
+ "}",
531
+ "",
532
+ "MoodUINode union:",
533
+ "- box: { type:'box', props?: { direction?, gap?, align?, justify?, wrap?, ...common }, children?: MoodUINode[] }",
534
+ "- text: { type:'text', props: { value: string, as?, color?, fontSize?, fontWeight?, textAlign?, ...common } }",
535
+ "- button: { type:'button', props: { label: string, variant?, actionId?, disabled?, ...common } }",
536
+ "- input: { type:'input', props?: { name?, placeholder?, defaultValue?, ...common } }",
537
+ "- image: { type:'image', props: { src: string, alt?, fit?, ...common } }",
538
+ "- spacer: { type:'spacer', props?: { size? } }",
539
+ "",
540
+ "common props:",
541
+ "- id, testId, className, style(object), padding, margin, background, borderRadius, width, height",
542
+ "",
543
+ "Rules:",
544
+ "- root wajib ada",
545
+ "- minimal pakai box sebagai container utama",
546
+ "- semua string pakai double quotes (JSON standard)",
547
+ "- jangan pakai function / JS expression apa pun"
548
+ ].join("\n");
549
+ }
550
+ function buildUserPrompt(prompt) {
551
+ return ["Buat UI dari request ini:", prompt].join("\n");
552
+ }
553
+ function parseFirstJsonObject(text) {
554
+ const start = text.indexOf("{");
555
+ if (start < 0) throw new Error("No JSON object found");
556
+ let depth = 0;
557
+ let inString = false;
558
+ let escaped = false;
559
+ for (let i = start; i < text.length; i += 1) {
560
+ const ch = text[i];
561
+ if (inString) {
562
+ if (escaped) {
563
+ escaped = false;
564
+ } else if (ch === "\\") {
565
+ escaped = true;
566
+ } else if (ch === '"') {
567
+ inString = false;
568
+ }
569
+ continue;
570
+ }
571
+ if (ch === '"') {
572
+ inString = true;
573
+ continue;
574
+ }
575
+ if (ch === "{") depth += 1;
576
+ else if (ch === "}") depth -= 1;
577
+ if (depth === 0) {
578
+ const candidate = text.slice(start, i + 1);
579
+ return JSON.parse(candidate);
580
+ }
581
+ }
582
+ throw new Error("Unterminated JSON object");
583
+ }
584
+
585
+ // src/llm/gemini.ts
586
+ function createGeminiClient(options) {
587
+ const baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com").replace(/\/+$/, "");
588
+ const fetchFn = options.fetchFn ?? fetch;
589
+ const apiKey = options.apiKey;
590
+ return {
591
+ async chat(req) {
592
+ const { systemInstruction, contents } = toGeminiContents(req.messages);
593
+ const url = `${baseUrl}/v1beta/models/${encodeURIComponent(req.model)}:generateContent?key=${encodeURIComponent(apiKey)}`;
594
+ const res = await fetchFn(url, {
595
+ method: "POST",
596
+ headers: { "content-type": "application/json" },
597
+ body: JSON.stringify({
598
+ ...systemInstruction ? { systemInstruction } : {},
599
+ contents,
600
+ generationConfig: req.temperature == null ? void 0 : { temperature: req.temperature }
601
+ })
602
+ });
603
+ if (!res.ok) {
604
+ const text2 = await safeReadText(res);
605
+ throw new Error(`Gemini generateContent failed (${res.status}): ${text2}`);
606
+ }
607
+ const json = await res.json();
608
+ const parts = json?.candidates?.[0]?.content?.parts;
609
+ const text = Array.isArray(parts) ? parts.map((p) => typeof p?.text === "string" ? p.text : "").join("") : void 0;
610
+ if (typeof text !== "string" || text.length === 0) throw new Error("Gemini response missing candidates[0].content.parts[].text");
611
+ return text;
612
+ }
613
+ };
614
+ }
615
+ function toGeminiContents(messages) {
616
+ const systemTexts = messages.filter((m) => m.role === "system").map((m) => m.content).filter((t) => t.trim().length > 0);
617
+ const systemInstruction = systemTexts.length > 0 ? { role: "system", parts: [{ text: systemTexts.join("\n") }] } : void 0;
618
+ const contents = messages.filter((m) => m.role !== "system").map((m) => ({
619
+ role: m.role === "assistant" ? "model" : "user",
620
+ parts: [{ text: m.content }]
621
+ }));
622
+ if (contents.length === 0) {
623
+ contents.push({ role: "user", parts: [{ text: "" }] });
624
+ }
625
+ return { systemInstruction, contents };
626
+ }
627
+ async function safeReadText(res) {
628
+ try {
629
+ return await res.text();
630
+ } catch {
631
+ return "";
632
+ }
633
+ }
634
+
635
+ // src/llm/ollama.ts
636
+ function createOllamaClient(options = {}) {
637
+ const baseUrl = options.baseUrl ?? "http://localhost:11434";
638
+ const fetchFn = options.fetchFn ?? fetch;
639
+ return {
640
+ async chat(req) {
641
+ const res = await fetchFn(`${baseUrl}/api/chat`, {
642
+ method: "POST",
643
+ headers: { "content-type": "application/json" },
644
+ body: JSON.stringify({
645
+ model: req.model,
646
+ messages: req.messages,
647
+ stream: false,
648
+ options: req.temperature == null ? void 0 : { temperature: req.temperature }
649
+ })
650
+ });
651
+ if (!res.ok) {
652
+ const text = await safeReadText2(res);
653
+ throw new Error(`Ollama chat failed (${res.status}): ${text}`);
654
+ }
655
+ const json = await res.json();
656
+ const content = json?.message?.content;
657
+ if (typeof content !== "string") throw new Error("Ollama response missing message.content");
658
+ return content;
659
+ }
660
+ };
661
+ }
662
+ async function safeReadText2(res) {
663
+ try {
664
+ return await res.text();
665
+ } catch {
666
+ return "";
667
+ }
668
+ }
669
+
670
+ // src/llm/openaiCompatible.ts
671
+ function createOpenAICompatibleClient(options) {
672
+ const fetchFn = options.fetchFn ?? fetch;
673
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
674
+ const apiKey = options.apiKey;
675
+ const defaultHeaders = options.defaultHeaders ?? {};
676
+ return {
677
+ async chat(req) {
678
+ const res = await fetchFn(`${baseUrl}/v1/chat/completions`, {
679
+ method: "POST",
680
+ headers: {
681
+ "content-type": "application/json",
682
+ authorization: `Bearer ${apiKey}`,
683
+ ...defaultHeaders
684
+ },
685
+ body: JSON.stringify({
686
+ model: req.model,
687
+ messages: req.messages,
688
+ temperature: req.temperature
689
+ })
690
+ });
691
+ if (!res.ok) {
692
+ const text = await safeReadText3(res);
693
+ throw new Error(`Chat completion failed (${res.status}): ${text}`);
694
+ }
695
+ const json = await res.json();
696
+ const content = json?.choices?.[0]?.message?.content;
697
+ if (typeof content !== "string") throw new Error("Response missing choices[0].message.content");
698
+ return content;
699
+ }
700
+ };
701
+ }
702
+ async function safeReadText3(res) {
703
+ try {
704
+ return await res.text();
705
+ } catch {
706
+ return "";
707
+ }
708
+ }
709
+
710
+ // src/ai/generateReactFromPrompt.ts
711
+ async function generateReactFromPrompt(options) {
712
+ 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 });
713
+ const { spec, raw } = await generateMoodUISpec(llm, {
714
+ model: options.model,
715
+ prompt: options.prompt,
716
+ temperature: options.temperature,
717
+ maxAttempts: options.maxAttempts
718
+ });
719
+ const code = renderReact(spec, { componentName: options.componentName });
720
+ return { spec, raw, code };
721
+ }
722
+
723
+ // src/react/MoodUIPromptPlayground.tsx
176
724
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
177
725
  function MoodUIPromptPlayground(props) {
178
726
  const provider = props.provider ?? "gemini";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kohryan/moodui",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "MoodUI - AI-friendly UI spec + React renderer",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,