helloadhavan commited on
Commit
13d012b
Β·
1 Parent(s): 9fd8196

init commit

Browse files
Files changed (5) hide show
  1. Dockerfile +16 -0
  2. app.py +80 -0
  3. index.html +120 -0
  4. static/app.js +154 -0
  5. static/style.css +454 -0
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.9
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+ CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:7860", "--timeout", "120"]
app.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GPT-2 Text Generation Server
3
+ =============================
4
+ A minimal Flask API that loads GPT-2 from Hugging Face Transformers and exposes
5
+ a single POST /generate endpoint. The model is loaded once at startup and kept
6
+ in memory, so generation requests are fast after the first cold start (~10-30s).
7
+
8
+ Endpoints
9
+ ---------
10
+ POST /generate
11
+ Body: { "prompt": str, "max_new_tokens": int, "temperature": float,
12
+ "top_p": float, "repetition_penalty": float }
13
+ Returns: { "generated_text": str, "prompt": str }
14
+
15
+ GET /
16
+ Serves the frontend HTML page.
17
+
18
+ Usage
19
+ -----
20
+ pip install flask transformers torch
21
+ python app.py
22
+ # Open http://localhost:5000
23
+ """
24
+
25
+ from flask import Flask, request, jsonify, send_from_directory
26
+ from transformers import GPT2LMHeadModel, AutoTokenizer
27
+ import torch
28
+ import os
29
+
30
+ app = Flask(__name__, static_folder="static")
31
+
32
+ print("Loading GPT-2 model and tokenizer...")
33
+ tokenizer = AutoTokenizer.from_pretrained("helloadhavan/llara1.1-100M-base")
34
+ model = GPT2LMHeadModel.from_pretrained("helloadhavan/llara1.1-100M-base")
35
+ model.eval()
36
+ print("Model ready.")
37
+
38
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
39
+ model.to(DEVICE)
40
+
41
+
42
+ @app.route("/")
43
+ def index():
44
+ return send_from_directory(".", "index.html")
45
+
46
+
47
+ @app.route("/generate", methods=["POST"])
48
+ def generate():
49
+ body = request.get_json(force=True)
50
+
51
+ prompt = body.get("prompt", "").strip()
52
+ if not prompt:
53
+ return jsonify({"error": "prompt is required"}), 400
54
+
55
+ max_new_tokens = int(body.get("max_new_tokens", 200))
56
+ temperature = float(body.get("temperature", 0.9))
57
+ top_p = float(body.get("top_p", 0.95))
58
+ repetition_penalty = float(body.get("repetition_penalty", 1.2))
59
+
60
+ inputs = tokenizer(prompt, return_tensors="pt").to(DEVICE)
61
+
62
+ with torch.no_grad():
63
+ output_ids = model.generate(
64
+ **inputs,
65
+ max_new_tokens=max_new_tokens,
66
+ do_sample=True,
67
+ temperature=temperature,
68
+ top_p=top_p,
69
+ repetition_penalty=repetition_penalty,
70
+ pad_token_id=tokenizer.eos_token_id,
71
+ )
72
+
73
+ full_text = tokenizer.decode(output_ids[0], skip_special_tokens=True)
74
+ generated_only = full_text[len(prompt):]
75
+
76
+ return jsonify({"generated_text": generated_only, "prompt": prompt})
77
+
78
+
79
+ if __name__ == "__main__":
80
+ app.run(debug=True, host="0.0.0.0", port=7860)
index.html ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <!--
3
+ GPT-2 Text Generation UI
4
+ ========================
5
+ Single-page interface for interacting with the Flask /generate endpoint.
6
+ All logic lives in static/app.js; styling in static/style.css.
7
+
8
+ Design language: Hugging Face-inspired β€” deep indigo background, purple-to-blue
9
+ gradients, Space Grotesk display type, monospace output pane.
10
+ -->
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="UTF-8" />
14
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
15
+ <title>GPT-2 Playground</title>
16
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
17
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
18
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
19
+ <link rel="stylesheet" href="static/style.css" />
20
+ </head>
21
+ <body>
22
+
23
+ <header class="site-header">
24
+ <div class="logo">
25
+ <svg width="28" height="28" viewBox="0 0 28 28" fill="none" aria-hidden="true">
26
+ <rect width="28" height="28" rx="8" fill="url(#logoGrad)"/>
27
+ <path d="M8 20 L14 8 L20 20" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
28
+ <path d="M10 16 H18" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
29
+ <defs>
30
+ <linearGradient id="logoGrad" x1="0" y1="0" x2="28" y2="28">
31
+ <stop offset="0%" stop-color="#7C3AED"/>
32
+ <stop offset="100%" stop-color="#2563EB"/>
33
+ </linearGradient>
34
+ </defs>
35
+ </svg>
36
+ <span>LLara Playground</span>
37
+ </div>
38
+ <div class="model-badge">gpt2 Β· HF Transformers</div>
39
+ </header>
40
+
41
+ <main class="main-layout">
42
+
43
+ <!-- ── Left column: controls ── -->
44
+ <aside class="controls-panel">
45
+ <h2 class="panel-title">Parameters</h2>
46
+
47
+ <div class="control-group">
48
+ <label for="maxTokens">Max new tokens <span class="value-display" id="maxTokensVal">200</span></label>
49
+ <input type="range" id="maxTokens" min="20" max="500" step="10" value="200" />
50
+ </div>
51
+
52
+ <div class="control-group">
53
+ <label for="temperature">Temperature <span class="value-display" id="temperatureVal">0.90</span></label>
54
+ <input type="range" id="temperature" min="0.1" max="2.0" step="0.05" value="0.90" />
55
+ </div>
56
+
57
+ <div class="control-group">
58
+ <label for="topP">Top-p <span class="value-display" id="topPVal">0.95</span></label>
59
+ <input type="range" id="topP" min="0.1" max="1.0" step="0.05" value="0.95" />
60
+ </div>
61
+
62
+ <div class="control-group">
63
+ <label for="repPenalty">Repetition penalty <span class="value-display" id="repPenaltyVal">1.20</span></label>
64
+ <input type="range" id="repPenalty" min="1.0" max="2.0" step="0.05" value="1.20" />
65
+ </div>
66
+
67
+ <div class="param-explainer">
68
+ <p><strong>Temperature</strong> β€” randomness. Higher = more creative, lower = more focused.</p>
69
+ <p><strong>Top-p</strong> β€” nucleus sampling. Keeps only the top cumulative probability mass.</p>
70
+ <p><strong>Repetition penalty</strong> β€” discourages repeated phrases.</p>
71
+ </div>
72
+ </aside>
73
+
74
+ <!-- ── Right column: prompt + output ── -->
75
+ <section class="generation-panel">
76
+
77
+ <div class="prompt-area">
78
+ <label for="promptInput" class="sr-only">Prompt</label>
79
+ <textarea
80
+ id="promptInput"
81
+ placeholder="Enter your prompt here β€” e.g. 'The future of artificial intelligence is'"
82
+ rows="5"
83
+ spellcheck="false"
84
+ ></textarea>
85
+ <div class="prompt-footer">
86
+ <span class="char-count" id="charCount">0 chars</span>
87
+ <button class="btn-generate" id="generateBtn">
88
+ <span class="btn-label">Generate</span>
89
+ <span class="btn-spinner" aria-hidden="true"></span>
90
+ </button>
91
+ </div>
92
+ </div>
93
+
94
+ <div class="output-container" id="outputContainer" aria-live="polite">
95
+ <div class="output-header">
96
+ <span class="output-label">Output</span>
97
+ <button class="btn-copy" id="copyBtn" title="Copy output" aria-label="Copy output" disabled>
98
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
99
+ <rect x="9" y="9" width="13" height="13" rx="2"/>
100
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
101
+ </svg>
102
+ Copy
103
+ </button>
104
+ </div>
105
+ <div class="output-body">
106
+ <div class="output-prompt" id="outputPrompt"></div>
107
+ <div class="output-generated" id="outputGenerated">
108
+ <span class="placeholder-text">Generated text will appear here…</span>
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ <div class="error-banner" id="errorBanner" role="alert" hidden></div>
114
+
115
+ </section>
116
+ </main>
117
+
118
+ <script src="static/app.js"></script>
119
+ </body>
120
+ </html>>
static/app.js ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ app.js β€” GPT-2 Playground
3
+ ==========================
4
+ Handles all client-side behaviour:
5
+
6
+ 1. Slider sync β€” live-update value badges next to each range input
7
+ 2. Char counter β€” updates as the user types in the prompt textarea
8
+ 3. Generation flow β€” POSTs to /generate, manages loading state on the button,
9
+ renders prompt + generated text in the output pane
10
+ 4. Copy button β€” copies the full output text and shows brief confirmation
11
+ 5. Error display β€” surfaces API or network errors in the error banner
12
+
13
+ No build step or framework required β€” plain ES2020, runs in any modern browser.
14
+ */
15
+
16
+ const $ = id => document.getElementById(id);
17
+
18
+ // ── Slider β†’ value badge sync ──────────────────────────────────────────────
19
+
20
+ const sliders = [
21
+ { input: $("maxTokens"), display: $("maxTokensVal"), decimals: 0 },
22
+ { input: $("temperature"), display: $("temperatureVal"), decimals: 2 },
23
+ { input: $("topP"), display: $("topPVal"), decimals: 2 },
24
+ { input: $("repPenalty"), display: $("repPenaltyVal"), decimals: 2 },
25
+ ];
26
+
27
+ function syncSlider({ input, display, decimals }) {
28
+ display.textContent = parseFloat(input.value).toFixed(decimals);
29
+ }
30
+
31
+ sliders.forEach(s => {
32
+ syncSlider(s);
33
+ s.input.addEventListener("input", () => syncSlider(s));
34
+ });
35
+
36
+ // ── Character counter ──────────────────────────────────────────────────────
37
+
38
+ const promptInput = $("promptInput");
39
+ const charCount = $("charCount");
40
+
41
+ promptInput.addEventListener("input", () => {
42
+ charCount.textContent = `${promptInput.value.length} chars`;
43
+ });
44
+
45
+ // ── Generation ─────────────────────────────────────────────────────────────
46
+
47
+ const generateBtn = $("generateBtn");
48
+ const outputPrompt = $("outputPrompt");
49
+ const outputGenerated = $("outputGenerated");
50
+ const errorBanner = $("errorBanner");
51
+ const copyBtn = $("copyBtn");
52
+
53
+ function setLoading(isLoading) {
54
+ generateBtn.disabled = isLoading;
55
+ generateBtn.classList.toggle("is-loading", isLoading);
56
+
57
+ // Apply the wider gradient background only during load so shimmer looks right
58
+ if (isLoading) {
59
+ generateBtn.style.backgroundImage =
60
+ "linear-gradient(90deg,#7c3aed,#6d28d9,#2563eb,#4f46e5,#7c3aed)";
61
+ generateBtn.style.backgroundSize = "300% 100%";
62
+ generateBtn.querySelector(".btn-label").textContent = "Generating…";
63
+ } else {
64
+ generateBtn.style.backgroundImage = "";
65
+ generateBtn.style.backgroundSize = "";
66
+ generateBtn.querySelector(".btn-label").textContent = "Generate";
67
+ }
68
+ }
69
+
70
+ function showError(message) {
71
+ errorBanner.textContent = message;
72
+ errorBanner.hidden = false;
73
+ }
74
+
75
+ function clearError() {
76
+ errorBanner.hidden = true;
77
+ errorBanner.textContent = "";
78
+ }
79
+
80
+ function renderOutput(prompt, generatedText) {
81
+ outputPrompt.textContent = prompt;
82
+ outputGenerated.textContent = generatedText;
83
+ outputGenerated.innerHTML = ""; // clear placeholder span
84
+ outputGenerated.textContent = generatedText;
85
+ copyBtn.disabled = false;
86
+ }
87
+
88
+ generateBtn.addEventListener("click", async () => {
89
+ const prompt = promptInput.value.trim();
90
+
91
+ if (!prompt) {
92
+ showError("Please enter a prompt before generating.");
93
+ return;
94
+ }
95
+
96
+ clearError();
97
+ setLoading(true);
98
+ copyBtn.disabled = true;
99
+ outputPrompt.textContent = "";
100
+ outputGenerated.innerHTML = '<span class="placeholder-text">Generating…</span>';
101
+
102
+ const payload = {
103
+ prompt,
104
+ max_new_tokens: parseInt($("maxTokens").value, 10),
105
+ temperature: parseFloat($("temperature").value),
106
+ top_p: parseFloat($("topP").value),
107
+ repetition_penalty: parseFloat($("repPenalty").value),
108
+ };
109
+
110
+ try {
111
+ const response = await fetch("/generate", {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify(payload),
115
+ });
116
+
117
+ if (!response.ok) {
118
+ const err = await response.json().catch(() => ({}));
119
+ throw new Error(err.error || `Server error ${response.status}`);
120
+ }
121
+
122
+ const data = await response.json();
123
+ renderOutput(data.prompt, data.generated_text);
124
+
125
+ } catch (err) {
126
+ showError(`Generation failed: ${err.message}`);
127
+ outputGenerated.innerHTML = '<span class="placeholder-text">Generated text will appear here…</span>';
128
+ } finally {
129
+ setLoading(false);
130
+ }
131
+ });
132
+
133
+ // ── Copy button ─────────────────────────────────────────────────────────────
134
+
135
+ copyBtn.addEventListener("click", () => {
136
+ const fullText = (outputPrompt.textContent + outputGenerated.textContent).trim();
137
+ if (!fullText) return;
138
+
139
+ navigator.clipboard.writeText(fullText).then(() => {
140
+ copyBtn.textContent = "βœ“ Copied!";
141
+ copyBtn.classList.add("copied");
142
+
143
+ setTimeout(() => {
144
+ copyBtn.innerHTML = `
145
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
146
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
147
+ <rect x="9" y="9" width="13" height="13" rx="2"/>
148
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
149
+ </svg>
150
+ Copy`;
151
+ copyBtn.classList.remove("copied");
152
+ }, 2000);
153
+ });
154
+ });
static/style.css ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ style.css β€” GPT-2 Playground
3
+ ==============================
4
+ Design tokens and component styles for the generation interface.
5
+
6
+ Color system
7
+ ------------
8
+ --bg-* dark indigo backgrounds (darkest β†’ surface)
9
+ --purple-* purple scale (accent, hover states)
10
+ --blue-* blue scale (gradient pair)
11
+ --text-* foreground hierarchy
12
+ --border subtle separator
13
+
14
+ Component hierarchy
15
+ -------------------
16
+ site-header
17
+ main-layout
18
+ controls-panel (parameter sliders)
19
+ generation-panel (textarea, output pane)
20
+ */
21
+
22
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
23
+
24
+ :root {
25
+ --bg-base: #0b0718;
26
+ --bg-surface: #13102b;
27
+ --bg-raised: #1c1840;
28
+ --bg-input: #0f0c24;
29
+
30
+ --purple-400: #84d2fc;
31
+ --purple-500: #5588f7;
32
+ --purple-600: #9333ea;
33
+ --purple-700: #7c3aed;
34
+
35
+ --blue-500: #3b82f6;
36
+ --blue-600: #2563eb;
37
+
38
+ --grad-primary: linear-gradient(135deg, #0054fd 0%, #2563eb 100%);
39
+ --grad-shimmer: linear-gradient(90deg, #2d31ff, #1068ea, #2563eb, #4f46e5, #7c3aed);
40
+
41
+ --text-primary: #ede9fe;
42
+ --text-secondary: #8ba5fa;
43
+ --text-muted: #5f68a6;
44
+ --text-on-grad: #ffffff;
45
+
46
+ --border: rgba(1, 189, 255, 0.25);
47
+ --border-focus: rgba(85, 136, 247, 0.6);
48
+
49
+ --radius-sm: 6px;
50
+ --radius-md: 10px;
51
+ --radius-lg: 16px;
52
+
53
+ --font-display: "Space Grotesk", sans-serif;
54
+ --font-body: "Inter", sans-serif;
55
+ --font-mono: "JetBrains Mono", monospace;
56
+ }
57
+
58
+ html, body {
59
+ height: 100%;
60
+ background-color: var(--bg-base);
61
+ color: var(--text-primary);
62
+ font-family: var(--font-body);
63
+ font-size: 14px;
64
+ line-height: 1.6;
65
+ -webkit-font-smoothing: antialiased;
66
+ }
67
+
68
+ /* ── Accessibility ── */
69
+ .sr-only {
70
+ position: absolute; width: 1px; height: 1px;
71
+ padding: 0; margin: -1px; overflow: hidden;
72
+ clip: rect(0,0,0,0); white-space: nowrap; border: 0;
73
+ }
74
+
75
+ /* ── Background ambient glow ── */
76
+ body::before {
77
+ content: "";
78
+ position: fixed; inset: 0; pointer-events: none; z-index: 0;
79
+ background:
80
+ radial-gradient(ellipse 60% 40% at 20% 10%, rgba(124,58,237,0.18) 0%, transparent 70%),
81
+ radial-gradient(ellipse 50% 35% at 80% 80%, rgba(37,99,235,0.14) 0%, transparent 70%);
82
+ }
83
+
84
+ /* ── Header ── */
85
+ .site-header {
86
+ position: relative; z-index: 10;
87
+ display: flex; align-items: center; justify-content: space-between;
88
+ padding: 16px 32px;
89
+ border-bottom: 1px solid var(--border);
90
+ background: rgba(11, 7, 24, 0.8);
91
+ backdrop-filter: blur(12px);
92
+ }
93
+
94
+ .logo {
95
+ display: flex; align-items: center; gap: 10px;
96
+ font-family: var(--font-display);
97
+ font-size: 17px; font-weight: 600;
98
+ color: var(--text-primary);
99
+ }
100
+
101
+ .model-badge {
102
+ font-family: var(--font-mono);
103
+ font-size: 11px;
104
+ color: var(--purple-400);
105
+ background: rgba(124, 58, 237, 0.15);
106
+ border: 1px solid rgba(124, 58, 237, 0.3);
107
+ padding: 4px 10px;
108
+ border-radius: 100px;
109
+ letter-spacing: 0.02em;
110
+ }
111
+
112
+ /* ── Layout ── */
113
+ .main-layout {
114
+ position: relative; z-index: 1;
115
+ display: grid;
116
+ grid-template-columns: 280px 1fr;
117
+ gap: 24px;
118
+ max-width: 1100px;
119
+ margin: 0 auto;
120
+ padding: 32px 24px 48px;
121
+ min-height: calc(100vh - 65px);
122
+ align-items: start;
123
+ }
124
+
125
+ /* ── Controls panel ── */
126
+ .controls-panel {
127
+ background: var(--bg-surface);
128
+ border: 1px solid var(--border);
129
+ border-radius: var(--radius-lg);
130
+ padding: 24px;
131
+ position: sticky;
132
+ top: 24px;
133
+ }
134
+
135
+ .panel-title {
136
+ font-family: var(--font-display);
137
+ font-size: 13px;
138
+ font-weight: 600;
139
+ letter-spacing: 0.08em;
140
+ text-transform: uppercase;
141
+ color: var(--text-secondary);
142
+ margin-bottom: 20px;
143
+ }
144
+
145
+ .control-group {
146
+ margin-bottom: 22px;
147
+ }
148
+
149
+ .control-group label {
150
+ display: flex;
151
+ justify-content: space-between;
152
+ align-items: center;
153
+ font-size: 13px;
154
+ font-weight: 500;
155
+ color: var(--text-primary);
156
+ margin-bottom: 8px;
157
+ }
158
+
159
+ .value-display {
160
+ font-family: var(--font-mono);
161
+ font-size: 12px;
162
+ color: var(--purple-400);
163
+ background: rgba(85, 123, 247, 0.12);
164
+ padding: 2px 7px;
165
+ border-radius: 4px;
166
+ min-width: 38px;
167
+ text-align: center;
168
+ }
169
+
170
+ /* Custom range slider */
171
+ input[type="range"] {
172
+ -webkit-appearance: none;
173
+ width: 100%;
174
+ height: 4px;
175
+ background: var(--bg-raised);
176
+ border-radius: 2px;
177
+ outline: none;
178
+ cursor: pointer;
179
+ }
180
+
181
+ input[type="range"]::-webkit-slider-thumb {
182
+ -webkit-appearance: none;
183
+ width: 16px; height: 16px;
184
+ border-radius: 50%;
185
+ background: var(--grad-primary);
186
+ background: linear-gradient(135deg, var(--purple-500), var(--blue-500));
187
+ box-shadow: 0 0 0 3px rgba(0, 31, 255, 0.2);
188
+ transition: box-shadow 0.2s;
189
+ }
190
+
191
+ input[type="range"]::-webkit-slider-thumb:hover {
192
+ box-shadow: 0 0 0 5px rgba(85, 131, 247, 0.3);
193
+ }
194
+
195
+ input[type="range"]::-moz-range-thumb {
196
+ width: 16px; height: 16px;
197
+ border: none; border-radius: 50%;
198
+ background: linear-gradient(135deg, var(--purple-500), var(--blue-500));
199
+ }
200
+
201
+ .param-explainer {
202
+ margin-top: 24px;
203
+ padding-top: 18px;
204
+ border-top: 1px solid var(--border);
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: 8px;
208
+ }
209
+
210
+ .param-explainer p {
211
+ font-size: 12px;
212
+ color: var(--text-muted);
213
+ line-height: 1.5;
214
+ }
215
+
216
+ .param-explainer strong {
217
+ color: var(--text-secondary);
218
+ font-weight: 500;
219
+ }
220
+
221
+ /* ── Generation panel ── */
222
+ .generation-panel {
223
+ display: flex;
224
+ flex-direction: column;
225
+ gap: 20px;
226
+ }
227
+
228
+ /* Prompt textarea */
229
+ .prompt-area {
230
+ background: var(--bg-surface);
231
+ border: 1px solid var(--border);
232
+ border-radius: var(--radius-lg);
233
+ overflow: hidden;
234
+ transition: border-color 0.2s;
235
+ }
236
+
237
+ .prompt-area:focus-within {
238
+ border-color: var(--border-focus);
239
+ box-shadow: 0 0 0 3px rgba(85, 98, 247, 0.1);
240
+ }
241
+
242
+ #promptInput {
243
+ width: 100%;
244
+ background: transparent;
245
+ border: none;
246
+ outline: none;
247
+ resize: none;
248
+ padding: 18px 20px 12px;
249
+ font-family: var(--font-body);
250
+ font-size: 15px;
251
+ color: var(--text-primary);
252
+ line-height: 1.65;
253
+ caret-color: var(--purple-400);
254
+ }
255
+
256
+ #promptInput::placeholder {
257
+ color: var(--text-muted);
258
+ }
259
+
260
+ .prompt-footer {
261
+ display: flex;
262
+ align-items: center;
263
+ justify-content: space-between;
264
+ padding: 10px 16px 14px;
265
+ border-top: 1px solid var(--border);
266
+ }
267
+
268
+ .char-count {
269
+ font-family: var(--font-mono);
270
+ font-size: 11px;
271
+ color: var(--text-muted);
272
+ }
273
+
274
+ /* Generate button */
275
+ .btn-generate {
276
+ display: inline-flex;
277
+ align-items: center;
278
+ gap: 8px;
279
+ padding: 9px 22px;
280
+ background: var(--grad-primary);
281
+ color: var(--text-on-grad);
282
+ font-family: var(--font-display);
283
+ font-size: 14px;
284
+ font-weight: 600;
285
+ border: none;
286
+ border-radius: var(--radius-md);
287
+ cursor: pointer;
288
+ position: relative;
289
+ overflow: hidden;
290
+ transition: opacity 0.15s, transform 0.1s;
291
+ }
292
+
293
+ .btn-generate:hover:not(:disabled) {
294
+ opacity: 0.92;
295
+ transform: translateY(-1px);
296
+ }
297
+
298
+ .btn-generate:active:not(:disabled) {
299
+ transform: translateY(0);
300
+ }
301
+
302
+ .btn-generate:disabled {
303
+ cursor: not-allowed;
304
+ opacity: 0.7;
305
+ }
306
+
307
+ /* Shimmer animation while generating */
308
+ .btn-generate.is-loading {
309
+ background-size: 300% 100%;
310
+ animation: shimmerBg 1.5s linear infinite;
311
+ }
312
+
313
+ @keyframes shimmerBg {
314
+ 0% { background-position: 100% 50%; }
315
+ 100% { background-position: 0% 50%; }
316
+ }
317
+
318
+ .btn-generate.is-loading .btn-label { opacity: 0; }
319
+ .btn-generate .btn-spinner { display: none; }
320
+
321
+ .btn-generate.is-loading .btn-spinner {
322
+ display: block;
323
+ position: absolute;
324
+ inset: 0; margin: auto;
325
+ width: 18px; height: 18px;
326
+ border: 2px solid rgba(255,255,255,0.3);
327
+ border-top-color: white;
328
+ border-radius: 50%;
329
+ animation: spin 0.8s linear infinite;
330
+ }
331
+
332
+ @keyframes spin {
333
+ to { transform: rotate(360deg); }
334
+ }
335
+
336
+ /* ── Output pane ── */
337
+ .output-container {
338
+ background: var(--bg-surface);
339
+ border: 1px solid var(--border);
340
+ border-radius: var(--radius-lg);
341
+ overflow: hidden;
342
+ }
343
+
344
+ .output-header {
345
+ display: flex;
346
+ align-items: center;
347
+ justify-content: space-between;
348
+ padding: 10px 16px;
349
+ border-bottom: 1px solid var(--border);
350
+ background: var(--bg-raised);
351
+ }
352
+
353
+ .output-label {
354
+ font-family: var(--font-display);
355
+ font-size: 12px;
356
+ font-weight: 600;
357
+ letter-spacing: 0.07em;
358
+ text-transform: uppercase;
359
+ color: var(--text-secondary);
360
+ }
361
+
362
+ .btn-copy {
363
+ display: inline-flex; align-items: center; gap: 5px;
364
+ padding: 4px 10px;
365
+ background: transparent;
366
+ border: 1px solid var(--border);
367
+ border-radius: var(--radius-sm);
368
+ color: var(--text-muted);
369
+ font-size: 12px;
370
+ cursor: pointer;
371
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
372
+ }
373
+
374
+ .btn-copy:hover:not(:disabled) {
375
+ color: var(--text-primary);
376
+ border-color: var(--purple-600);
377
+ background: rgba(124, 58, 237, 0.12);
378
+ }
379
+
380
+ .btn-copy:disabled {
381
+ opacity: 0.4;
382
+ cursor: not-allowed;
383
+ }
384
+
385
+ .btn-copy.copied {
386
+ color: #34d399;
387
+ border-color: rgba(52, 211, 153, 0.4);
388
+ }
389
+
390
+ .output-body {
391
+ padding: 20px;
392
+ min-height: 180px;
393
+ font-family: var(--font-mono);
394
+ font-size: 13.5px;
395
+ line-height: 1.75;
396
+ }
397
+
398
+ .output-prompt {
399
+ color: var(--text-muted);
400
+ white-space: pre-wrap;
401
+ word-break: break-word;
402
+ }
403
+
404
+ .output-prompt:not(:empty)::after {
405
+ content: "";
406
+ display: inline-block;
407
+ width: 0;
408
+ }
409
+
410
+ .output-generated {
411
+ color: var(--text-primary);
412
+ white-space: pre-wrap;
413
+ word-break: break-word;
414
+ }
415
+
416
+ .output-prompt:not(:empty) + .output-generated {
417
+ border-left: 2px solid var(--purple-700);
418
+ padding-left: 12px;
419
+ margin-top: 12px;
420
+ color: var(--text-primary);
421
+ }
422
+
423
+ .placeholder-text {
424
+ color: var(--text-muted);
425
+ font-family: var(--font-body);
426
+ font-size: 13px;
427
+ font-style: italic;
428
+ }
429
+
430
+ /* ── Error banner ── */
431
+ .error-banner {
432
+ padding: 12px 16px;
433
+ background: rgba(220, 38, 38, 0.12);
434
+ border: 1px solid rgba(220, 38, 38, 0.35);
435
+ border-radius: var(--radius-md);
436
+ color: #fca5a5;
437
+ font-size: 13px;
438
+ }
439
+
440
+ /* ── Responsive ── */
441
+ @media (max-width: 768px) {
442
+ .main-layout {
443
+ grid-template-columns: 1fr;
444
+ padding: 20px 16px 40px;
445
+ }
446
+
447
+ .controls-panel {
448
+ position: static;
449
+ }
450
+
451
+ .site-header {
452
+ padding: 14px 20px;
453
+ }
454
+ }