khtsly commited on
Commit
116c073
·
verified ·
1 Parent(s): 647aa7b

Upload 4 files

Browse files
Files changed (4) hide show
  1. README.md +6 -4
  2. app.py +311 -0
  3. index.html +550 -0
  4. requirements.txt +11 -0
README.md CHANGED
@@ -1,13 +1,15 @@
1
  ---
2
  title: Luau Coder Preview
3
- emoji: 🐠
4
- colorFrom: pink
5
- colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.29.1
8
  python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Luau Coder Preview
3
+ emoji: 🦭
4
+ colorFrom: gray
5
+ colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.29.1
8
  python_version: '3.12'
9
  app_file: app.py
10
+ pinned: true
11
+ license: apache-2.0
12
+ short_description: Inference
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gc
2
+ import os
3
+ import tempfile
4
+ from collections.abc import Generator
5
+ from functools import wraps
6
+ from pathlib import Path
7
+ from threading import Lock, Thread
8
+
9
+ import spaces
10
+ import torch
11
+ import transformers
12
+ from fastapi.responses import HTMLResponse
13
+ from gradio import Server
14
+ from transformers import AutoModelForCausalLM, AutoTokenizer, LogitsProcessor, LogitsProcessorList, TextIteratorStreamer
15
+
16
+ print(f"Transformers version: {transformers.__version__}")
17
+
18
+ REPO_ID = "khtsly/Luau-Coder-1.0"
19
+ TOKENIZER_REPO_ID = "khtsly/luau-coder-1.0-preview-tokenizer"
20
+ HF_TOKEN = os.environ.get("HF_TOKEN")
21
+
22
+ # Available model variants. The front-end populates its selector from the
23
+ # /models endpoint, so adding a new entry here is enough to offer it.
24
+ # Only one variant is kept loaded at a time; switching unloads the previous.
25
+ MODEL_VARIANTS = {
26
+ "0.5B-Base": {"repo_id": f"{REPO_ID}-0.5B-Base", "enabled": True},
27
+ "1.4B-Base (Stage1)": {"repo_id": f"{REPO_ID}-1.4B-Base-Stage1", "enabled": True},
28
+ }
29
+ DEFAULT_VARIANT = "0.5B-Base"
30
+
31
+
32
+ def _repair_checkpoint_keys(weights_path: Path) -> bool:
33
+ """Remove an obsolete composite-model prefix before Transformers loads."""
34
+ from safetensors import safe_open
35
+
36
+ with safe_open(weights_path, framework="pt", device="cpu") as f:
37
+ keys = list(f.keys())
38
+
39
+ prefixed = [key for key in keys if key.startswith("language_model.")]
40
+ if not prefixed:
41
+ return False
42
+ if len(prefixed) != len(keys):
43
+ raise RuntimeError(
44
+ f"Refusing to repair mixed checkpoint keys in {weights_path}: "
45
+ f"{len(prefixed)} of {len(keys)} use the language_model. prefix"
46
+ )
47
+
48
+ fixed_keys = [key.removeprefix("language_model.") for key in keys]
49
+ if len(set(fixed_keys)) != len(fixed_keys):
50
+ raise RuntimeError(f"Prefix removal would create duplicate keys in {weights_path}")
51
+
52
+ from safetensors.torch import load_file, save_file
53
+
54
+ print(f"Repairing {len(keys)} checkpoint keys in {weights_path} ...")
55
+ state = load_file(weights_path, device="cpu")
56
+ fixed = {
57
+ key.removeprefix("language_model."): tensor.contiguous()
58
+ for key, tensor in state.items()
59
+ }
60
+ tmp_path = weights_path.with_name(f".{weights_path.name}.repair.tmp")
61
+ try:
62
+ save_file(fixed, tmp_path)
63
+ os.replace(tmp_path, weights_path)
64
+ finally:
65
+ tmp_path.unlink(missing_ok=True)
66
+ print("Checkpoint key repair complete")
67
+ return True
68
+
69
+
70
+ def _prepare_model_source(variant: str) -> str:
71
+ """Return a local model directory whose safetensors keys are loadable."""
72
+ repo_id = MODEL_VARIANTS[variant]["repo_id"]
73
+ cache_dir = Path(tempfile.gettempdir()) / f"luau-coder-1.0-{variant}"
74
+
75
+ # Local override for the default variant (checked-in model.safetensors).
76
+ if variant == DEFAULT_VARIANT:
77
+ app_dir = Path(__file__).resolve().parent
78
+ weights_path = app_dir / "model.safetensors"
79
+ if weights_path.is_file():
80
+ _repair_checkpoint_keys(weights_path)
81
+ return str(app_dir)
82
+
83
+ from huggingface_hub import snapshot_download
84
+
85
+ snapshot_download(
86
+ repo_id=repo_id,
87
+ local_dir=cache_dir,
88
+ token=HF_TOKEN,
89
+ allow_patterns=["*.json", "*.py", "*.jinja", "*.safetensors"],
90
+ )
91
+ weights_path = cache_dir / "model.safetensors"
92
+
93
+ if not weights_path.is_file():
94
+ raise FileNotFoundError(f"No model.safetensors found in {cache_dir}")
95
+
96
+ _repair_checkpoint_keys(weights_path)
97
+ return str(cache_dir)
98
+
99
+
100
+ tokenizer = AutoTokenizer.from_pretrained(
101
+ TOKENIZER_REPO_ID,
102
+ trust_remote_code=True,
103
+ token=HF_TOKEN
104
+ )
105
+
106
+ _model = None
107
+ _current_variant = None
108
+ _model_lock = Lock()
109
+
110
+
111
+ def _unload_current_model() -> None:
112
+ """Free the currently loaded variant so the next one can take its place."""
113
+ global _model, _current_variant
114
+ if _model is not None:
115
+ print(f"Unloading model variant: {_current_variant}")
116
+ del _model
117
+ _model = None
118
+ _current_variant = None
119
+ gc.collect()
120
+ if torch.cuda.is_available():
121
+ torch.cuda.empty_cache()
122
+
123
+
124
+ def ensure_model(variant: str):
125
+ """Load `variant` on demand, unloading whatever is currently loaded."""
126
+ global _model, _current_variant
127
+ if variant not in MODEL_VARIANTS:
128
+ print(f"Unknown model variant {variant!r}; falling back to {DEFAULT_VARIANT}")
129
+ variant = DEFAULT_VARIANT
130
+ if not MODEL_VARIANTS[variant].get("enabled", True):
131
+ print(f"Model variant {variant!r} is not enabled yet; falling back to {DEFAULT_VARIANT}")
132
+ variant = DEFAULT_VARIANT
133
+ with _model_lock:
134
+ if _model is not None and _current_variant == variant:
135
+ return _model
136
+ _unload_current_model()
137
+ repo_id = MODEL_VARIANTS[variant]["repo_id"]
138
+ print(f"Loading model variant: {variant} ({repo_id}) ...")
139
+ source = _prepare_model_source(variant)
140
+ loaded = AutoModelForCausalLM.from_pretrained(
141
+ source,
142
+ trust_remote_code=True,
143
+ dtype=torch.bfloat16,
144
+ token=HF_TOKEN,
145
+ device_map="cuda",
146
+ )
147
+ _install_transformers_516_cache_compat(loaded)
148
+ _model = loaded
149
+ _current_variant = variant
150
+ print(f"Model variant ready: {variant}")
151
+ return _model
152
+
153
+
154
+ def _install_transformers_516_cache_compat(loaded_model) -> None:
155
+ """Handle empty generation positions with older Kimi remote code."""
156
+ inner_model = loaded_model.model
157
+ original_forward = inner_model.forward
158
+
159
+ @wraps(original_forward)
160
+ def compatible_forward(*args, **kwargs):
161
+ for name in ("cache_position", "position_ids"):
162
+ value = kwargs.get(name)
163
+ numel = getattr(value, "numel", None)
164
+ if callable(numel) and numel() == 0:
165
+ kwargs[name] = None
166
+ return original_forward(*args, **kwargs)
167
+
168
+ inner_model.forward = compatible_forward
169
+
170
+
171
+ class PresencePenaltyLogitsProcessor(LogitsProcessor):
172
+ """OpenAI-style presence penalty for HF generate (which lacks it natively).
173
+
174
+ Subtracts `penalty` once from the logit of every unique token already
175
+ present in the context, discouraging the model from reusing tokens.
176
+ """
177
+
178
+ def __init__(self, penalty: float):
179
+ self.penalty = float(penalty)
180
+
181
+ def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:
182
+ if self.penalty == 0:
183
+ return scores
184
+ for batch_idx, ids in enumerate(input_ids):
185
+ for token_id in set(ids.tolist()):
186
+ if 0 <= token_id < scores.shape[-1]:
187
+ scores[batch_idx, token_id] -= self.penalty
188
+ return scores
189
+
190
+
191
+ demo = Server()
192
+
193
+
194
+ @demo.api()
195
+ @spaces.GPU(duration=60)
196
+ def predict(
197
+ message: str,
198
+ history: list[list] | None = None,
199
+ max_tokens: int = 32,
200
+ temperature: float = 0.6,
201
+ top_p: float = 0.95,
202
+ top_k: int = 50,
203
+ repetition_penalty: float = 1.0,
204
+ presence_penalty: float = 0.0,
205
+ model_id: str = "0.5B-Base",
206
+ ) -> Generator[str, None, None]:
207
+ # History is accepted for front-end chat compatibility; generation uses
208
+ # the current message only to preserve base-model continuation behavior.
209
+ raw_input_text = message
210
+ max_tokens = int(max_tokens)
211
+ active_model = ensure_model(model_id)
212
+ if next(active_model.parameters()).device.type != "cuda":
213
+ active_model.to("cuda")
214
+ inputs = tokenizer(raw_input_text, return_tensors="pt")
215
+ token_count = inputs["input_ids"].shape[1]
216
+ print(f"Generation request: {len(raw_input_text)} chars, {token_count} tokens")
217
+
218
+ if token_count == 0:
219
+ seed_token_id = tokenizer.bos_token_id
220
+ if seed_token_id is None:
221
+ candidate = tokenizer.convert_tokens_to_ids("[BOS]")
222
+ if isinstance(candidate, int) and candidate >= 0:
223
+ seed_token_id = candidate
224
+ if seed_token_id is None:
225
+ yield "Please enter a non-empty prompt."
226
+ return
227
+ inputs["input_ids"] = torch.tensor([[seed_token_id]], dtype=torch.long)
228
+ inputs["attention_mask"] = torch.ones((1, 1), dtype=torch.long)
229
+ token_count = 1
230
+ print(f"Empty prompt: seeded generation with token {seed_token_id}")
231
+
232
+ inputs = inputs.to("cuda")
233
+ streamer = TextIteratorStreamer(tokenizer, skip_prompt=True)
234
+
235
+ generation_kwargs = dict(
236
+ **inputs,
237
+ streamer=streamer,
238
+ max_new_tokens=int(max_tokens),
239
+ pad_token_id=tokenizer.eos_token_id,
240
+ use_cache=True,
241
+ generation_mode=True
242
+ )
243
+
244
+ temperature = float(temperature)
245
+ top_p = float(top_p)
246
+ top_k = int(top_k)
247
+ repetition_penalty = float(repetition_penalty)
248
+ presence_penalty = float(presence_penalty)
249
+
250
+ if temperature > 0.0:
251
+ generation_kwargs.update(do_sample=True, temperature=temperature)
252
+ if 0.0 < top_p < 1.0:
253
+ generation_kwargs["top_p"] = top_p
254
+ if top_k > 0:
255
+ generation_kwargs["top_k"] = top_k
256
+ else:
257
+ generation_kwargs.update(do_sample=False)
258
+
259
+ if repetition_penalty != 1.0:
260
+ generation_kwargs["repetition_penalty"] = repetition_penalty
261
+
262
+ if presence_penalty != 0.0:
263
+ generation_kwargs["logits_processor"] = LogitsProcessorList(
264
+ [PresencePenaltyLogitsProcessor(presence_penalty)]
265
+ )
266
+
267
+ generation_error = []
268
+
269
+ def run_generation():
270
+ try:
271
+ active_model.generate(**generation_kwargs)
272
+ except Exception as exc:
273
+ generation_error.append(exc)
274
+ streamer.end()
275
+
276
+ thread = Thread(target=run_generation)
277
+ thread.start()
278
+
279
+ output_buffer = ""
280
+ for new_text in streamer:
281
+ output_buffer += new_text
282
+ yield output_buffer
283
+ thread.join()
284
+
285
+ if generation_error:
286
+ exc = generation_error[0]
287
+ message_out = f"Generation failed: {type(exc).__name__}: {exc}"
288
+ print(message_out)
289
+ yield f"{output_buffer}\n\n{message_out}" if output_buffer else message_out
290
+
291
+
292
+ @demo.get("/models")
293
+ async def list_models():
294
+ return {
295
+ "default": DEFAULT_VARIANT,
296
+ "variants": [
297
+ {"id": name, "enabled": bool(cfg.get("enabled", True))}
298
+ for name, cfg in sorted(MODEL_VARIANTS.items())
299
+ ],
300
+ }
301
+
302
+
303
+ @demo.get("/", response_class=HTMLResponse)
304
+ async def homepage():
305
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
306
+ with open(html_path, "r", encoding="utf-8") as f:
307
+ return f.read()
308
+
309
+
310
+ if __name__ == "__main__":
311
+ demo.launch(show_error=True)
index.html ADDED
@@ -0,0 +1,550 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Luau Coder Preview | khtsly</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
9
+ <script src="https://unpkg.com/lucide@latest"></script>
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/18.0.12/lib/marked.umd.min.js"></script>
11
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.18.7/katex.min.css">
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.18.7/katex.min.js"></script>
13
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.18.7/contrib/auto-render.min.js"></script>
14
+ <style>
15
+ :root {
16
+ --bg: #ffffff;
17
+ --blue: #00A2FF;
18
+ --cyan: #0077C8;
19
+ --text: #1e293b;
20
+ --text-muted: #64748b;
21
+ --glass: rgba(0, 0, 0, 0.02);
22
+ --glass-border: #e2e8f0;
23
+ --accent: #00A2FF;
24
+ }
25
+
26
+ body {
27
+ font-family: 'Inter', sans-serif;
28
+ background-color: var(--bg);
29
+ color: var(--text);
30
+ height: 100vh;
31
+ margin: 0;
32
+ display: flex;
33
+ flex-direction: column;
34
+ overflow: hidden;
35
+ }
36
+
37
+ .chat-scroll-area {
38
+ flex: 1;
39
+ overflow-y: auto;
40
+ padding-bottom: 140px;
41
+ -webkit-overflow-scrolling: touch;
42
+ scroll-behavior: smooth;
43
+ }
44
+
45
+ .chat-scroll-area::-webkit-scrollbar { width: 4px; }
46
+ .chat-scroll-area::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
47
+
48
+ .message-bubble {
49
+ max-width: 85%;
50
+ padding: 15px;
51
+ animation: fadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
52
+ }
53
+
54
+ @keyframes fadeIn {
55
+ from { opacity: 0; transform: translateY(12px); }
56
+ to { opacity: 1; transform: translateY(0); }
57
+ }
58
+
59
+ .user-message {
60
+ background: linear-gradient(135deg, var(--blue), var(--cyan));
61
+ color: #ffffff;
62
+ box-shadow: 0 4px 14px rgba(0, 162, 255, 0.28);
63
+ border-radius: 20px 20px 4px 20px;
64
+ }
65
+
66
+ .bot-message {
67
+ background: #f8fafc;
68
+ border: 1px solid var(--glass-border);
69
+ border-radius: 20px 20px 20px 4px;
70
+ }
71
+
72
+ .typing-dot {
73
+ width: 4px; height: 4px;
74
+ background: var(--accent);
75
+ border-radius: 50%;
76
+ animation: bounce 1.4s infinite ease-in-out;
77
+ }
78
+
79
+ @keyframes bounce {
80
+ 0%, 80%, 100% { transform: scale(0.3); opacity: 0.4; }
81
+ 40% { transform: scale(1); opacity: 1; }
82
+ }
83
+
84
+ .input-pill {
85
+ background: #ffffff;
86
+ border: 1px solid var(--glass-border);
87
+ transition: all 0.3s ease;
88
+ }
89
+
90
+ .input-pill:focus-within {
91
+ border-color: rgba(0, 162, 255, 0.45);
92
+ box-shadow: 0 0 0 3px rgba(0, 162, 255, 0.12);
93
+ }
94
+
95
+ .logo-glow {
96
+ filter: drop-shadow(0 0 8px rgba(0, 162, 255, 0.35));
97
+ }
98
+
99
+ .send-btn {
100
+ background: linear-gradient(135deg, #334155, #0f172a);
101
+ box-shadow: 0 4px 12px rgba(15, 23, 42, 0.35);
102
+ transition: all 0.3s ease;
103
+ }
104
+ .send-btn:hover:not(:disabled) { transform: scale(1.05); filter: brightness(1.25); }
105
+
106
+ .settings-panel {
107
+ background: rgba(255, 255, 255, 0.98);
108
+ backdrop-filter: blur(20px);
109
+ border-left: 1px solid var(--glass-border);
110
+ transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
111
+ }
112
+
113
+ .control-slider {
114
+ -webkit-appearance: none;
115
+ width: 100%;
116
+ height: 4px;
117
+ background: #e2e8f0;
118
+ border-radius: 2px;
119
+ outline: none;
120
+ }
121
+ .control-slider::-webkit-slider-thumb {
122
+ -webkit-appearance: none;
123
+ width: 12px; height: 12px;
124
+ background: var(--accent);
125
+ border-radius: 50%;
126
+ cursor: pointer;
127
+ }
128
+
129
+ .bot-message pre {
130
+ background: #f1f5f9;
131
+ border: 1px solid #e2e8f0;
132
+ border-radius: 10px;
133
+ padding: 12px;
134
+ overflow-x: auto;
135
+ }
136
+ .bot-message code { color: #0284C7; }
137
+ </style>
138
+ </head>
139
+ <body>
140
+
141
+ <header class="h-16 flex items-center justify-between px-6 md:px-10 shrink-0 z-50 border-b border-slate-200 bg-white">
142
+ <div class="flex items-center gap-3">
143
+ <div class="w-10 h-10 logo-glow rounded-lg flex items-center justify-center overflow-hidden" style="background: #0f172a;">
144
+ <img src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Luau_Logo_%28Programming_Language%29.svg"
145
+ alt="Luau" class="w-9 h-9">
146
+ </div>
147
+ <div>
148
+ <h1 class="text-lg font-bold text-slate-800">Luau Coder</h1>
149
+ <p class="text-[10px] text-slate-400 uppercase tracking-[0.2em] font-semibold">By khtsly &middot; Preview</p>
150
+ </div>
151
+ </div>
152
+ <div class="flex items-center gap-4">
153
+ <button id="toggle-settings" class="p-2 rounded-xl hover:bg-slate-100 text-slate-500 hover:text-slate-800 transition-all">
154
+ <i data-lucide="sliders-horizontal" class="w-5 h-5"></i>
155
+ </button>
156
+ </div>
157
+ </header>
158
+
159
+ <div id="settings-panel" class="fixed top-0 right-0 h-full w-72 z-[100] translate-x-full settings-panel p-6 flex flex-col gap-6 shadow-[-12px_0_40px_rgba(0,0,0,0.08)] overflow-y-auto">
160
+ <div class="flex items-center justify-between">
161
+ <h2 class="text-base font-bold text-slate-800">Settings</h2>
162
+ <button id="close-settings" class="text-slate-400 hover:text-slate-700">
163
+ <i data-lucide="x" class="w-5 h-5"></i>
164
+ </button>
165
+ </div>
166
+
167
+ <div class="space-y-5">
168
+ <div class="space-y-2">
169
+ <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest">
170
+ <span>Temperature</span>
171
+ <span id="temp-val">0.6</span>
172
+ </div>
173
+ <input type="range" id="temp-slider" min="0" max="1.5" step="0.05" value="0.6" class="control-slider">
174
+ <p class="text-[11px] text-slate-400">0.0 = Greedy</p>
175
+ </div>
176
+
177
+ <div class="space-y-2">
178
+ <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest">
179
+ <span>Max tokens</span>
180
+ <span id="tokens-val">32</span>
181
+ </div>
182
+ <input type="range" id="tokens-slider" min="16" max="256" step="1" value="32" class="control-slider">
183
+ </div>
184
+
185
+ <div class="space-y-2">
186
+ <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest">
187
+ <span>Top-p</span>
188
+ <span id="topp-val">0.95</span>
189
+ </div>
190
+ <input type="range" id="topp-slider" min="0" max="1" step="0.05" value="0.95" class="control-slider">
191
+ <p class="text-[11px] text-slate-400">1.0 = Off</p>
192
+ </div>
193
+
194
+ <div class="space-y-2">
195
+ <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest">
196
+ <span>Top-k</span>
197
+ <span id="topk-val">50</span>
198
+ </div>
199
+ <input type="range" id="topk-slider" min="0" max="100" step="1" value="50" class="control-slider">
200
+ <p class="text-[11px] text-slate-400">0 = Off</p>
201
+ </div>
202
+
203
+ <div class="space-y-2">
204
+ <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest">
205
+ <span>Repetition penalty</span>
206
+ <span id="rep-val">1.0</span>
207
+ </div>
208
+ <input type="range" id="rep-slider" min="1" max="2" step="0.05" value="1.0" class="control-slider">
209
+ <p class="text-[11px] text-slate-400">1.0 = Off</p>
210
+ </div>
211
+
212
+ <div class="space-y-2">
213
+ <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest">
214
+ <span>Presence penalty</span>
215
+ <span id="pres-val">0.0</span>
216
+ </div>
217
+ <input type="range" id="pres-slider" min="0" max="2" step="0.1" value="0.0" class="control-slider">
218
+ <p class="text-[11px] text-slate-400">0.0 = Off</p>
219
+ </div>
220
+
221
+ <button onclick="clearHistory()" class="w-full py-3 rounded-xl bg-red-50 border border-red-200 text-red-600 text-sm font-bold hover:bg-red-100 transition-all flex items-center justify-center gap-2 mt-4">
222
+ <i data-lucide="trash-2" class="w-4 h-4"></i>
223
+ Clear History
224
+ </button>
225
+ </div>
226
+ </div>
227
+
228
+ <main id="chat-messages" class="chat-scroll-area px-4 flex-1">
229
+ <div class="max-w-3xl mx-auto space-y-6 pt-6 pb-32" id="chat-container">
230
+ <div class="flex gap-3 items-start">
231
+ <div class="bot-message message-bubble shadow-sm">
232
+ <p class="text-slate-700 leading-relaxed text-[15px]">
233
+ Hello!
234
+ </p>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </main>
239
+
240
+ <div class="fixed bottom-0 left-0 right-0 p-4 md:p-8 pointer-events-none z-50">
241
+ <div class="max-w-3xl mx-auto pointer-events-auto">
242
+ <div class="input-pill rounded-[1.5rem] p-2 shadow-lg">
243
+ <textarea id="user-input" placeholder="Enter a prompt or code to continue..." rows="1"
244
+ class="w-full bg-transparent border-none focus:ring-0 text-slate-800 placeholder-slate-400 py-3 px-3 resize-none max-h-40 leading-relaxed font-medium outline-none"></textarea>
245
+ <div class="flex items-center justify-between pl-2 pr-1 pb-1">
246
+ <div class="relative" id="model-picker">
247
+ <button id="model-select-btn" title="Model variant" class="text-xs font-semibold border border-slate-200 rounded-full pl-3 pr-8 py-1.5 bg-slate-50 text-slate-600 hover:bg-slate-100 hover:text-slate-800 transition-all focus:outline-none focus:border-sky-300 focus:ring-2 focus:ring-sky-100 max-w-[11rem] truncate">
248
+ 0.5B-Base
249
+ </button>
250
+ <i data-lucide="chevron-down" class="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
251
+ <div id="model-menu" class="hidden absolute bottom-full left-0 mb-2 min-w-[10rem] bg-white border border-slate-200 rounded-2xl shadow-xl p-1.5 z-50"></div>
252
+ </div>
253
+ <button id="send-btn" class="send-btn w-9 h-9 text-white rounded-full flex items-center justify-center shrink-0">
254
+ <i data-lucide="arrow-up" class="w-5 h-5" id="send-icon"></i>
255
+ </button>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ </div>
260
+
261
+ <script type="module">
262
+ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
263
+
264
+ lucide.createIcons();
265
+
266
+ const chatContainer = document.getElementById('chat-container');
267
+ const chatScrollArea = document.getElementById('chat-messages');
268
+ const userInput = document.getElementById('user-input');
269
+ const sendBtn = document.getElementById('send-btn');
270
+ const settingsPanel = document.getElementById('settings-panel');
271
+ const toggleSettings = document.getElementById('toggle-settings');
272
+ const closeSettings = document.getElementById('close-settings');
273
+ const tempSlider = document.getElementById('temp-slider');
274
+ const tokensSlider = document.getElementById('tokens-slider');
275
+ const modelSelectBtn = document.getElementById('model-select-btn');
276
+ const modelMenu = document.getElementById('model-menu');
277
+ const modelPicker = document.getElementById('model-picker');
278
+ const toppSlider = document.getElementById('topp-slider');
279
+ const topkSlider = document.getElementById('topk-slider');
280
+ const repSlider = document.getElementById('rep-slider');
281
+ const presSlider = document.getElementById('pres-slider');
282
+
283
+ let client = null;
284
+ let chatHistory = [];
285
+ let currentJob = null;
286
+ let isSettingsOpen = false;
287
+ let currentModel = "0.5B-Base";
288
+ let modelVariants = [
289
+ { id: "0.5B-Base", enabled: true },
290
+ { id: "1.4B-Base (Stage1)", enabled: true },
291
+ ];
292
+ let pendingModelLoad = false;
293
+
294
+ async function init() {
295
+ try {
296
+ client = await Client.connect(window.location.origin, { events: ["data", "status"] });
297
+ } catch (err) {
298
+ console.error("Gradio connection error", err);
299
+ }
300
+ }
301
+ init();
302
+ loadModelOptions();
303
+
304
+ async function loadModelOptions() {
305
+ let fallback = "0.5B-Base";
306
+ try {
307
+ const res = await fetch(window.location.origin + "/models");
308
+ if (res.ok) {
309
+ const data = await res.json();
310
+ if (Array.isArray(data.variants) && data.variants.length) {
311
+ modelVariants = data.variants.map((v) =>
312
+ typeof v === "string" ? { id: v, enabled: true } : { id: v.id, enabled: v.enabled !== false }
313
+ );
314
+ }
315
+ if (data.default) fallback = data.default;
316
+ }
317
+ } catch (err) {
318
+ console.error("Model list fetch error", err);
319
+ }
320
+ const enabledIds = modelVariants.filter((v) => v.enabled).map((v) => v.id);
321
+ currentModel = enabledIds.includes(fallback) ? fallback : enabledIds[0];
322
+ renderModelMenu();
323
+ }
324
+
325
+ function renderModelMenu() {
326
+ modelMenu.innerHTML = "";
327
+ for (const v of modelVariants) {
328
+ const item = document.createElement("button");
329
+ const isCurrent = v.id === currentModel;
330
+ if (v.enabled) {
331
+ item.className = `w-full flex items-center justify-between gap-3 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${isCurrent ? "bg-sky-50 text-sky-700" : "text-slate-600 hover:bg-slate-100"}`;
332
+ } else {
333
+ item.className = "w-full flex items-center justify-between gap-3 px-3 py-2 rounded-xl text-xs font-semibold text-slate-400 opacity-70 cursor-not-allowed";
334
+ item.disabled = true;
335
+ item.title = "Coming soon";
336
+ }
337
+ const left = document.createElement("span");
338
+ left.className = "flex items-center gap-2 min-w-0";
339
+ const label = document.createElement("span");
340
+ label.className = "truncate";
341
+ label.textContent = v.id;
342
+ left.appendChild(label);
343
+ if (!v.enabled) {
344
+ const badge = document.createElement("span");
345
+ badge.className = "text-[9px] font-bold tracking-widest px-1.5 py-0.5 rounded-md bg-amber-100 text-amber-700 border border-amber-200 shrink-0";
346
+ badge.textContent = "SOON";
347
+ left.appendChild(badge);
348
+ }
349
+ item.appendChild(left);
350
+ if (isCurrent) {
351
+ const check = document.createElement("i");
352
+ check.setAttribute("data-lucide", "check");
353
+ check.className = "w-3.5 h-3.5 shrink-0";
354
+ item.appendChild(check);
355
+ }
356
+ if (v.enabled) {
357
+ item.onclick = (e) => { e.stopPropagation(); selectModel(v.id); };
358
+ }
359
+ modelMenu.appendChild(item);
360
+ }
361
+ modelSelectBtn.textContent = currentModel;
362
+ if (window.lucide) lucide.createIcons();
363
+ }
364
+
365
+ function toggleModelMenu(open) {
366
+ const willOpen = open === undefined ? modelMenu.classList.contains("hidden") : open;
367
+ modelMenu.classList.toggle("hidden", !willOpen);
368
+ }
369
+
370
+ function selectModel(next) {
371
+ toggleModelMenu(false);
372
+ if (next === currentModel) return;
373
+ if (chatHistory.length > 0) {
374
+ if (!confirm(`Switch model to ${next}? Conversation history will be cleared.`)) return;
375
+ }
376
+ currentModel = next;
377
+ pendingModelLoad = true;
378
+ clearHistory();
379
+ renderModelMenu();
380
+ }
381
+
382
+ function renderMath(el) {
383
+ if (window.renderMathInElement) {
384
+ renderMathInElement(el, {
385
+ delimiters: [
386
+ {left: '$$', right: '$$', display: true},
387
+ {left: '$', right: '$', display: false},
388
+ ],
389
+ throwOnError: false
390
+ });
391
+ }
392
+ }
393
+
394
+ function appendMessage(role, text = '') {
395
+ const div = document.createElement('div');
396
+ div.className = `flex gap-3 items-start ${role === 'user' ? 'flex-row-reverse' : ''}`;
397
+ const bubbleClass = role === 'user' ? 'user-message' : 'bot-message';
398
+ div.innerHTML = `
399
+ <div class="${bubbleClass} message-bubble shadow-sm">
400
+ <div class="content-container leading-relaxed text-[15px]">${role === 'user' ? escapeHtml(text) : marked.parse(text)}</div>
401
+ </div>
402
+ `;
403
+ chatContainer.appendChild(div);
404
+ if (role === 'bot') renderMath(div.querySelector('.content-container'));
405
+ chatScrollArea.scrollTo({ top: chatScrollArea.scrollHeight, behavior: 'smooth' });
406
+ return div;
407
+ }
408
+
409
+ function escapeHtml(s) {
410
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
411
+ }
412
+
413
+ function updateBotMessage(div, fullText) {
414
+ const contentContainer = div.querySelector('.content-container');
415
+ if (fullText) {
416
+ contentContainer.innerHTML = marked.parse(fullText);
417
+ } else {
418
+ contentContainer.innerHTML = '';
419
+ }
420
+ renderMath(contentContainer);
421
+ chatScrollArea.scrollTo({ top: chatScrollArea.scrollHeight, behavior: 'smooth' });
422
+ return fullText;
423
+ }
424
+
425
+ async function sendMessage() {
426
+ const text = userInput.value.trim();
427
+ if (!text) return;
428
+
429
+ userInput.value = '';
430
+ userInput.style.height = 'auto';
431
+ appendMessage('user', text);
432
+
433
+ sendBtn.disabled = true;
434
+ const botDiv = appendMessage('bot', '');
435
+ const contentContainer = botDiv.querySelector('.content-container');
436
+ if (pendingModelLoad) {
437
+ contentContainer.innerHTML = `<p class="text-slate-500">Loading model ${escapeHtml(currentModel)}… first load may take a while.</p>`;
438
+ pendingModelLoad = false;
439
+ } else {
440
+ contentContainer.innerHTML = '<div class="flex gap-1.5 py-2"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div>';
441
+ }
442
+
443
+ let isStopped = false;
444
+ sendBtn.onclick = () => {
445
+ if (currentJob) {
446
+ currentJob.cancel();
447
+ isStopped = true;
448
+ resetSendBtn();
449
+ }
450
+ };
451
+
452
+ try {
453
+ currentJob = client.submit("/predict", {
454
+ message: text,
455
+ history: chatHistory,
456
+ max_tokens: parseInt(tokensSlider.value),
457
+ temperature: parseFloat(tempSlider.value),
458
+ top_p: parseFloat(toppSlider.value),
459
+ top_k: parseInt(topkSlider.value),
460
+ repetition_penalty: parseFloat(repSlider.value),
461
+ presence_penalty: parseFloat(presSlider.value),
462
+ model_id: currentModel,
463
+ });
464
+
465
+ let finalAnswer = "";
466
+ for await (const msg of currentJob) {
467
+ if (isStopped) break;
468
+ if (msg.type === "data" && msg.data) {
469
+ finalAnswer = updateBotMessage(botDiv, msg.data[0]);
470
+ } else if (msg.type === "status" && msg.stage === "complete") {
471
+ break;
472
+ } else if (msg.type === "status" && msg.stage === "error") {
473
+ throw new Error(msg.message || "Generation failed");
474
+ }
475
+ }
476
+
477
+ if (!isStopped && finalAnswer) {
478
+ chatHistory.push([text, finalAnswer]);
479
+ }
480
+ } catch (err) {
481
+ console.error(err);
482
+ if (!isStopped) {
483
+ contentContainer.innerHTML = '<p class="text-red-500">Error: please try again.</p>';
484
+ }
485
+ } finally {
486
+ resetSendBtn();
487
+ currentJob = null;
488
+ }
489
+ }
490
+
491
+ function resetSendBtn() {
492
+ sendBtn.disabled = false;
493
+ sendBtn.onclick = sendMessage;
494
+ }
495
+
496
+ window.clearHistory = function() {
497
+ chatHistory = [];
498
+ chatContainer.innerHTML = `
499
+ <div class="flex gap-3 items-start">
500
+ <div class="bot-message message-bubble shadow-sm">
501
+ <p class="text-slate-700 leading-relaxed text-[15px]">History cleared. How can I help you?</p>
502
+ </div>
503
+ </div>
504
+ `;
505
+ toggleSettingsSidebar(false);
506
+ };
507
+
508
+ function toggleSettingsSidebar(open) {
509
+ isSettingsOpen = open;
510
+ settingsPanel.classList.toggle('translate-x-full', !open);
511
+ settingsPanel.classList.toggle('translate-x-0', open);
512
+ }
513
+
514
+ toggleSettings.onclick = (e) => { e.stopPropagation(); toggleSettingsSidebar(true); };
515
+ closeSettings.onclick = () => toggleSettingsSidebar(false);
516
+ document.addEventListener('click', (e) => {
517
+ if (isSettingsOpen && !settingsPanel.contains(e.target) && !toggleSettings.contains(e.target)) {
518
+ toggleSettingsSidebar(false);
519
+ }
520
+ if (!modelMenu.classList.contains('hidden') && !modelPicker.contains(e.target)) {
521
+ toggleModelMenu(false);
522
+ }
523
+ });
524
+ document.addEventListener('keydown', (e) => {
525
+ if (e.key === 'Escape') toggleModelMenu(false);
526
+ });
527
+
528
+ modelSelectBtn.onclick = (e) => { e.stopPropagation(); toggleModelMenu(); };
529
+
530
+ tempSlider.oninput = () => document.getElementById('temp-val').textContent = tempSlider.value;
531
+ tokensSlider.oninput = () => document.getElementById('tokens-val').textContent = tokensSlider.value;
532
+ toppSlider.oninput = () => document.getElementById('topp-val').textContent = parseFloat(toppSlider.value).toFixed(2).replace(/0$/, '');
533
+ topkSlider.oninput = () => document.getElementById('topk-val').textContent = topkSlider.value;
534
+ repSlider.oninput = () => document.getElementById('rep-val').textContent = parseFloat(repSlider.value).toFixed(2).replace(/0$/, '');
535
+ presSlider.oninput = () => document.getElementById('pres-val').textContent = parseFloat(presSlider.value).toFixed(1);
536
+
537
+ sendBtn.onclick = sendMessage;
538
+ userInput.onkeydown = (e) => {
539
+ if (e.key === 'Enter' && !e.shiftKey) {
540
+ e.preventDefault();
541
+ sendMessage();
542
+ }
543
+ };
544
+ userInput.oninput = () => {
545
+ userInput.style.height = 'auto';
546
+ userInput.style.height = userInput.scrollHeight + 'px';
547
+ };
548
+ </script>
549
+ </body>
550
+ </html>
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ spaces
3
+ torch
4
+ beautifulsoup4
5
+ huggingface_hub
6
+ transformers==5.18.0
7
+ einops
8
+ triton
9
+ fla-core
10
+ accelerate
11
+ https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.9.47/flash_attn-2.8.3+cu132torch2.13-cp312-cp312-linux_x86_64.whl