File size: 5,775 Bytes
306086a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import os, re, io, zipfile, tempfile, time
import gradio as gr
from anthropic import Anthropic

# -------------------- CONFIG --------------------
MODEL_ID = "claude-sonnet-4-5-20250929"
SYSTEM_PROMPT = (
    "You are an expert full-stack developer. When the user asks for an app or site, "
    "return complete production-ready code artifacts in Markdown code fences labeled "
    "```html```, ```css```, and ```js```. Always include index.html, optionally styles.css and app.js. "
    "Generate beautiful, functional, responsive designs using pure HTML, CSS, and JS."
)

# -------------------- HELPERS --------------------
def parse_artifacts(text: str):
    files = {}
    blocks = re.findall(r"```(html|css|js)\s+([\s\S]*?)```", text, re.I)
    for lang, code in blocks:
        name = {"html": "index.html", "css": "styles.css", "js": "app.js"}[lang.lower()]
        if name in files:
            base, ext = name.split(".")
            n = 2
            while f"{base}{n}.{ext}" in files:
                n += 1
            name = f"{base}{n}.{ext}"
        files[name] = code.strip()
    if not files:
        esc = gr.utils.escape_html(text)
        files["index.html"] = f"<!doctype html><meta charset='utf-8'><title>Artifact</title><pre>{esc}</pre>"
    if "index.html" not in files:
        files["index.html"] = "<!doctype html><meta charset='utf-8'><title>Artifact</title><h1>Artifact</h1>"
    return files

def render_srcdoc(files: dict):
    html = files.get("index.html", "")
    css = files.get("styles.css", "")
    js = files.get("app.js", "")
    # Inline CSS + JS for sandbox preview
    if "</head>" in html:
        html = html.replace("</head>", f"<style>\n{css}\n</style></head>")
    else:
        html = f"<!doctype html><head><meta charset='utf-8'><style>{css}</style></head>{html}"
    if "</body>" in html:
        html = html.replace("</body>", f"<script>\n{js}\n</script></body>")
    else:
        html = f"{html}<script>{js}</script>"
    return html

def make_zip_path(files: dict):
    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
    with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as z:
        for name, code in files.items():
            z.writestr(name, code)
    tmp.flush()
    return tmp.name

def call_claude(api_key: str, prompt: str):
    client = Anthropic(api_key=api_key)
    t0 = time.time()
    resp = client.messages.create(
        model=MODEL_ID,
        max_tokens=4000,
        temperature=0.45,
        system=SYSTEM_PROMPT,
        messages=[{"role": "user", "content": prompt}],
        timeout=120,
    )
    latency = int((time.time() - t0) * 1000)
    content = "".join(getattr(c, "text", "") for c in resp.content)
    files = parse_artifacts(content)
    return files, content, latency

# -------------------- UI --------------------
with gr.Blocks(fill_height=True, theme=gr.themes.Soft()) as demo:
    gr.Markdown(
        "# 🌐 ZEN Artifact Builder — Claude Sonnet 4.5\n"
        "Describe any app or website and see it appear live below."
    )

    with gr.Row():
        api_key = gr.Textbox(
            label="ANTHROPIC_API_KEY", type="password", placeholder="sk-ant-…"
        )
        prompt = gr.Textbox(
            label="Describe your app/site",
            lines=6,
            placeholder="Example: responsive dark-mode portfolio with glass panels and smooth animations.",
        )

    generate_btn = gr.Button("✨ Generate", variant="primary")

    with gr.Row():
        with gr.Tab("Live Preview"):
            preview = gr.HTML(
                elem_id="preview-pane",
                value="<div style='display:flex;align-items:center;justify-content:center;height:100%;color:#aaa;'>Awaiting generation…</div>",
            )
        with gr.Tab("Artifacts"):
            file_select = gr.Dropdown(label="Select file", choices=[], interactive=True)
            editor = gr.Code(language="html", label="Code Editor", value="")
            save_btn = gr.Button("💾 Save")
        with gr.Tab("Raw Output & Export"):
            raw_output = gr.Textbox(label="Model Output (raw)", lines=12)
            latency_box = gr.Number(label="Latency (ms)")
            zip_file = gr.File(label="Download ZIP", interactive=False)

    files_state = gr.State({})

    demo.css = """
    #preview-pane {
        height: 85vh !important;
        min-height: 550px;
        border: 1px solid #ccc;
        border-radius: 10px;
        overflow: auto;
        background: #fff;
    }
    """

    # -------------------- FUNCTIONS --------------------
    def generate(api_key, prompt):
        if not api_key:
            raise gr.Error("Please enter your Anthropic API key.")
        files, raw, latency = call_claude(api_key, prompt)
        srcdoc = render_srcdoc(files)
        zip_path = make_zip_path(files)
        names = list(files.keys())
        first = names[0] if names else ""
        return (
            files,
            gr.update(value=srcdoc),
            gr.update(choices=names, value=first),
            gr.update(value=files.get(first, "")),
            raw,
            latency,
            zip_path,
        )

    generate_btn.click(
        generate,
        inputs=[api_key, prompt],
        outputs=[files_state, preview, file_select, editor, raw_output, latency_box, zip_file],
    )

    def load_editor(files, name):
        return files.get(name, "")

    file_select.change(load_editor, inputs=[files_state, file_select], outputs=editor)

    def save_file(files, name, code):
        files = dict(files)
        if name:
            files[name] = code
        return files, gr.update(value=render_srcdoc(files))

    save_btn.click(save_file, inputs=[files_state, file_select, editor], outputs=[files_state, preview])

demo.launch()