akhaliq HF Staff commited on
Commit
0796f2b
·
1 Parent(s): fdb3fe4

Add custom gradio.Server app with interactive reasoning UI

Browse files
Files changed (3) hide show
  1. app.py +355 -0
  2. index.html +1443 -0
  3. requirements.txt +7 -0
app.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import torch
4
+ from fastapi.responses import HTMLResponse
5
+ from gradio import Server
6
+
7
+ # Initialize the Gradio Server (extends FastAPI)
8
+ app = Server()
9
+
10
+ # Define HTML path
11
+ HTML_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
12
+
13
+ # Check if spaces library is available (for Hugging Face Spaces ZeroGPU)
14
+ try:
15
+ import spaces
16
+ has_spaces = True
17
+ print("Hugging Face Spaces library loaded successfully.")
18
+ except ImportError:
19
+ has_spaces = False
20
+ print("Hugging Face Spaces library not found. Running in standard environment.")
21
+
22
+ # Set device
23
+ device = "cuda" if torch.cuda.is_available() else "cpu"
24
+ print(f"System detected device: {device}")
25
+
26
+ # Determine if model should be loaded
27
+ force_load = os.environ.get("FORCE_MODEL_LOAD", "false").lower() == "true"
28
+ is_fallback = True
29
+ model = None
30
+ tokenizer = None
31
+
32
+ MODEL_ID = "WeiboAI/VibeThinker-3B"
33
+
34
+ if device == "cuda" or force_load:
35
+ try:
36
+ from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer, GenerationConfig
37
+ from threading import Thread
38
+
39
+ print(f"Attempting to load model '{MODEL_ID}'...")
40
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
41
+
42
+ if torch.backends.mps.is_available() and force_load:
43
+ print("Loading model on MPS (Apple Silicon GPU) with float16...")
44
+ model = AutoModelForCausalLM.from_pretrained(
45
+ MODEL_ID,
46
+ torch_dtype=torch.float16,
47
+ low_cpu_mem_usage=True
48
+ ).to("mps")
49
+ else:
50
+ print("Loading model in bfloat16...")
51
+ model = AutoModelForCausalLM.from_pretrained(
52
+ MODEL_ID,
53
+ torch_dtype=torch.bfloat16,
54
+ device_map="auto",
55
+ low_cpu_mem_usage=True
56
+ )
57
+
58
+ is_fallback = False
59
+ print("Model loaded successfully!")
60
+ except Exception as e:
61
+ print(f"Error loading model: {e}")
62
+ print("Falling back to simulation mode.")
63
+ is_fallback = True
64
+ else:
65
+ print("No CUDA GPU detected and FORCE_MODEL_LOAD is false. Falling back to simulation mode.")
66
+ is_fallback = True
67
+
68
+
69
+ def get_mock_response(message: str) -> tuple[str, str]:
70
+ """Generates detailed mock thinking and answers for local development testing."""
71
+ message_lower = message.lower()
72
+
73
+ if "palindrom" in message_lower or "leetcode" in message_lower or "code" in message_lower or "python" in message_lower:
74
+ thought = (
75
+ "1. We need to solve the Longest Palindromic Substring problem.\n"
76
+ "2. Let's analyze the constraints and possible approaches.\n"
77
+ " - Approach 1: Brute Force. Check all O(N^2) substrings. Checking takes O(N), total O(N^3). Too slow.\n"
78
+ " - Approach 2: Dynamic Programming. Let DP[i][j] be true if substring s[i..j] is a palindrome.\n"
79
+ " - DP[i][j] = (s[i] == s[j]) && (j - i < 3 || DP[i+1][j-1])\n"
80
+ " - Time complexity: O(N^2), Space complexity: O(N^2).\n"
81
+ " - Approach 3: Expand Around Center. For each index, expand outward for both odd and even length palindromes.\n"
82
+ " - Time complexity: O(N^2), Space complexity: O(1). This is optimal in terms of space.\n"
83
+ " - Approach 4: Manacher's Algorithm. Dynamic programming combined with centers expansion. O(N) time and space.\n"
84
+ "3. Let's implement the Expand Around Center approach as it is highly readable and O(1) space.\n"
85
+ "4. Verification: check boundary cases like single character, empty string, string with all identical characters, etc.\n"
86
+ "5. Formulate final response with explanation, code, and complexity analysis."
87
+ )
88
+ body = (
89
+ "Here is the optimal Python implementation of the **Longest Palindromic Substring** problem using the **Expand Around Center** approach (\\(O(N^2)\\) time, \\(O(1)\\) space).\n\n"
90
+ "### Expand Around Center (Python)\n\n"
91
+ "```python\n"
92
+ "class Solution:\n"
93
+ " def longestPalindrome(self, s: str) -> str:\n"
94
+ " if not s or len(s) < 1:\n"
95
+ " return \"\"\n"
96
+ " \n"
97
+ " start, end = 0, 0\n"
98
+ " \n"
99
+ " def expand_around_center(left: int, right: int) -> int:\n"
100
+ " while left >= 0 and right < len(s) and s[left] == s[right]:\n"
101
+ " left -= 1\n"
102
+ " right += 1\n"
103
+ " # Return the length of the palindrome found\n"
104
+ " return right - left - 1\n"
105
+ " \n"
106
+ " for i in range(len(s)):\n"
107
+ " # Odd-length palindromes (single character center)\n"
108
+ " len1 = expand_around_center(i, i)\n"
109
+ " # Even-length palindromes (two character center)\n"
110
+ " len2 = expand_around_center(i, i + 1)\n"
111
+ " \n"
112
+ " max_len = max(len1, len2)\n"
113
+ " if max_len > end - start:\n"
114
+ " # Adjust start and end indices based on current center\n"
115
+ " start = i - (max_len - 1) // 2\n"
116
+ " end = i + max_len // 2\n"
117
+ " \n"
118
+ " return s[start:end + 1]\n"
119
+ "```\n\n"
120
+ "### Complexity Analysis\n"
121
+ "- **Time Complexity:** \\(O(N^2)\\). We expand around \\(2N - 1\\) centers. For each center, expansion can take up to \\(O(N)\\) steps.\n"
122
+ "- **Space Complexity:** \\(O(1)\\). Only constant extra space is used."
123
+ )
124
+ elif "deck" in message_lower or "probab" in message_lower or "card" in message_lower or "math" in message_lower or "solve" in message_lower or "equation" in message_lower:
125
+ thought = (
126
+ "1. The user is asking a probability/math question: 'If a card is drawn from a standard deck, what is the probability that it is a spade or a face card?'\n"
127
+ "2. Let's define the sample space and events:\n"
128
+ " - Total cards in a standard deck: N(S) = 52.\n"
129
+ " - Event A: Drawing a spade. There are 13 spades in a deck. So N(A) = 13.\n"
130
+ " - Event B: Drawing a face card (Jack, Queen, King). There are 3 face cards per suit, and 4 suits. So N(B) = 3 * 4 = 12.\n"
131
+ " - We need to find the probability of Spade OR Face Card: P(A or B).\n"
132
+ "3. Let's recall the addition rule of probability:\n"
133
+ " - P(A or B) = P(A) + P(B) - P(A and B)\n"
134
+ "4. What is Event (A and B)? It is drawing a card that is both a spade AND a face card.\n"
135
+ " - These are the Jack of Spades, Queen of Spades, and King of Spades. N(A and B) = 3.\n"
136
+ "5. Let's plug the numbers in:\n"
137
+ " - P(A) = 13/52\n"
138
+ " - P(B) = 12/52\n"
139
+ " - P(A and B) = 3/52\n"
140
+ " - P(A or B) = 13/52 + 12/52 - 3/52 = (13 + 12 - 3)/52 = 22/52.\n"
141
+ "6. Simplify the fraction:\n"
142
+ " - 22/52 = 11/26.\n"
143
+ " - Decimal value: ~0.4231 (or 42.3%).\n"
144
+ "7. Structure the explanation clearly, showing the formulas, steps, and intermediate values using LaTeX mathematical notations."
145
+ )
146
+ body = (
147
+ "To find the probability that a randomly drawn card from a standard deck is either a **spade** or a **face card**, we can use the addition rule of probability.\n\n"
148
+ "### 1. Identify the Sample Spaces\n"
149
+ "- **Total cards in a deck:** \\(N(S) = 52\\)\n"
150
+ "- **Spades in a deck (Event \\(A\\)):** There are 13 spades. Hence, \\(N(A) = 13\\).\n"
151
+ "- **Face cards in a deck (Event \\(B\\)):** There are 3 face cards (Jack, Queen, King) per suit, across 4 suits. Hence, \\(N(B) = 3 \\times 4 = 12\\).\n\n"
152
+ "### 2. Find the Intersection (Spade Face Cards)\n"
153
+ "Some cards belong to both sets: Jack of Spades, Queen of Spades, and King of Spades. \n"
154
+ "Let this intersection be Event \\(A \\cap B\\):\n"
155
+ "\\[N(A \\cap B) = 3\\]\n\n"
156
+ "### 3. Apply the Addition Rule\n"
157
+ "The probability of the union of two events is given by:\n"
158
+ "\\[P(A \\cup B) = P(A) + P(B) - P(A \\cap B)\\]\n\n"
159
+ "Substitute the values:\n"
160
+ "\\[P(A \\cup B) = \\frac{13}{52} + \\frac{12}{52} - \\frac{3}{52}\\]\n"
161
+ "\\[P(A \\cup B) = \\frac{13 + 12 - 3}{52} = \\frac{22}{52}\\]\n\n"
162
+ "### 4. Simplify the Result\n"
163
+ "Reducing \\(\\frac{22}{52}\\) by dividing the numerator and denominator by 2:\n"
164
+ "\\[P(A \\cup B) = \\frac{11}{26} \\approx 0.4231 \\text{ (or } 42.31\\%\\text{)}\\]\n\n"
165
+ "**Conclusion:** The probability of drawing a spade or a face card is **\\(\\frac{11}{26}\\)**, which is approximately **42.3%**."
166
+ )
167
+ elif "box" in message_lower or "fruit" in message_lower or "label" in message_lower or "logic" in message_lower:
168
+ thought = (
169
+ "1. Three boxes: Box A (labeled Apples), Box B (labeled Oranges), Box C (labeled Mixed).\n"
170
+ "2. Fact: *Every single label is incorrect*.\n"
171
+ " - Box labeled Apples has Oranges or Mixed.\n"
172
+ " - Box labeled Oranges has Apples or Mixed.\n"
173
+ " - Box labeled Mixed has Apples or Oranges.\n"
174
+ "3. Let's draw a fruit from the \"Mixed\" box. Why?\n"
175
+ " - Since the label \"Mixed\" is wrong, it must contain either 100% Apples or 100% Oranges.\n"
176
+ " - If we draw a fruit and it's an Apple, then the \"Mixed\" box is 100% Apples.\n"
177
+ " - Now we have the remaining boxes labeled \"Apples\" and \"Oranges\".\n"
178
+ " - The remaining contents are \"Oranges\" and \"Mixed\".\n"
179
+ " - The box labeled \"Oranges\" cannot contain Oranges (as all labels are wrong). So it must contain \"Mixed\".\n"
180
+ " - Consequently, the box labeled \"Apples\" must contain \"Oranges\".\n"
181
+ "4. What if we drew an Orange? By symmetry:\n"
182
+ " - The \"Mixed\" box is 100% Oranges.\n"
183
+ " - The box labeled \"Apples\" cannot contain Apples, so it must contain \"Mixed\".\n"
184
+ " - The box labeled \"Oranges\" must contain \"Apples\".\n"
185
+ "5. The proof is sound and deterministic. Explain case by case with a markdown table."
186
+ )
187
+ body = (
188
+ "This is a classic logical puzzle. The key to solving it is to **draw a fruit from the box labeled \"Mixed\"**.\n\n"
189
+ "Here is the step-by-step reasoning:\n\n"
190
+ "### 1. Understand the Rules\n"
191
+ "- There are three boxes containing: **Apples**, **Oranges**, or **Mixed (both)**.\n"
192
+ "- **Crucial Fact:** *All* labels are currently incorrect. This means:\n"
193
+ " - The box labeled **\"Apples\"** does *not* contain only apples (it contains either Oranges or Mixed).\n"
194
+ " - The box labeled **\"Oranges\"** does *not* contain only oranges (it contains either Apples or Mixed).\n"
195
+ " - The box labeled **\"Mixed\"** does *not* contain both (it contains either 100% Apples or 100% Oranges).\n\n"
196
+ "### 2. Choose the \"Mixed\" Box First\n"
197
+ "Since you know the box labeled \"Mixed\" is *definitely* not mixed, it must be either **100% Apples** or **100% Oranges**.\n\n"
198
+ "- Draw one fruit from the box labeled **\"Mixed\"**.\n\n"
199
+ "#### Case A: You draw an Apple\n"
200
+ "1. Since this box cannot be mixed, and it contains an apple, it **must contain only Apples**. (Label this box **Apples**).\n"
201
+ "2. You have two boxes left, labeled **\"Apples\"** and **\"Oranges\"**, and two contents left to assign: **Oranges** and **Mixed**.\n"
202
+ "3. Look at the box labeled **\"Oranges\"**. Because all labels are wrong, this box *cannot* contain Oranges. Therefore, it **must contain Mixed**.\n"
203
+ "4. This leaves the box labeled **\"Apples\"** to **contain only Oranges**.\n\n"
204
+ "#### Case B: You draw an Orange\n"
205
+ "1. Since this box cannot be mixed, and it contains an orange, it **must contain only Oranges**. (Label this box **Oranges**).\n"
206
+ "2. You have two boxes left, labeled **\"Apples\"** and **\"Oranges\"**, and two contents left to assign: **Apples** and **Mixed**.\n"
207
+ "3. Look at the box labeled **\"Apples\"**. Because all labels are wrong, this box *cannot* contain Apples. Therefore, it **must contain Mixed**.\n"
208
+ "4. This leaves the box labeled **\"Oranges\"** to **contain only Apples**.\n\n"
209
+ "### Summary Table\n\n"
210
+ "| Label on Box | Drawn Fruit | Actual Contents | Box 2 Actual | Box 3 Actual |\n"
211
+ "| :--- | :--- | :--- | :--- | :--- |\n"
212
+ "| **\"Mixed\"** | Apple 🍎 | **Apples** | **\"Oranges\"** label $\\rightarrow$ **Mixed** | **\"Apples\"** label $\\rightarrow$ **Oranges** |\n"
213
+ "| **\"Mixed\"** | Orange 🍊 | **Oranges** | **\"Apples\"** label $\\rightarrow$ **Mixed** | **\"Oranges\"** label $\\rightarrow$ **Apples** |\n\n"
214
+ "By drawing just **one fruit from the \"Mixed\" box**, you can confidently relabel all three boxes!"
215
+ )
216
+ else:
217
+ thought = (
218
+ "1. The user prompt is general: '" + message + "'.\n"
219
+ "2. Let's formulate a structured response that explains who I am and how I can help.\n"
220
+ "3. Demonstrate reasoning by showing my system stats, architecture (Qwen2.5-Coder backbone), and optimal use cases (coding, math, logic).\n"
221
+ "4. Conclude with a helpful, inviting sign-off."
222
+ )
223
+ body = (
224
+ "Hello! I am **VibeThinker-3B**, a small language model optimized for deep, verifiable reasoning in math, coding, and STEM.\n\n"
225
+ "To answer your request, here is a summary of how I operate:\n\n"
226
+ "### 1. Model Capabilities\n"
227
+ "- **Architecture:** Finetuned on top of Qwen2.5-Coder-3B using a curriculum-based SFT pipeline and Reinforcement Learning (MGPO).\n"
228
+ "- **Reasoning Process:** I generate intermediate thoughts inside `<think>...</think>` tags to verify my hypotheses and correct mistakes before showing the final result.\n\n"
229
+ "### 2. Suggested Prompts\n"
230
+ "- **Math:** Ask me complex algebra, probability, or number theory questions.\n"
231
+ "- **Coding:** Give me algorithmic coding challenges, code optimization tasks, or debugging requests.\n"
232
+ "- **Logic:** Present me with riddles, brain teasers, or rule-based scheduling problems.\n\n"
233
+ "Feel free to write a mathematical problem or code request to see me think through it step-by-step!"
234
+ )
235
+ return thought, body
236
+
237
+
238
+ def simulate_inference(message: str):
239
+ """Simulates token-by-token streaming of the reasoning and body response."""
240
+ thought, body = get_mock_response(message)
241
+ full_text = f"<think>\n{thought}\n</think>\n{body}"
242
+
243
+ accumulated = ""
244
+ words = []
245
+ current_word = ""
246
+ for char in full_text:
247
+ if char in (" ", "\n"):
248
+ if current_word:
249
+ words.append(current_word)
250
+ current_word = ""
251
+ words.append(char)
252
+ else:
253
+ current_word += char
254
+ if current_word:
255
+ words.append(current_word)
256
+
257
+ for word in words:
258
+ accumulated += word
259
+ yield accumulated
260
+ # Simulating slightly slower output for thinking steps to make it feel deliberate
261
+ if "<think>" in accumulated and "</think>" not in accumulated:
262
+ time.sleep(0.012 if word not in ("\n", " ") else 0.004)
263
+ else:
264
+ time.sleep(0.006 if word not in ("\n", " ") else 0.002)
265
+
266
+
267
+ def infer_real(message: str, system_prompt: str, temperature: float, top_p: float, max_tokens: int):
268
+ """Executes actual model generation using the transformers library."""
269
+ from transformers import TextIteratorStreamer, GenerationConfig
270
+ from threading import Thread
271
+
272
+ messages = []
273
+ if system_prompt:
274
+ messages.append({"role": "system", "content": system_prompt})
275
+ messages.append({"role": "user", "content": message})
276
+
277
+ text = tokenizer.apply_chat_template(
278
+ messages,
279
+ tokenize=False,
280
+ add_generation_prompt=True,
281
+ )
282
+ model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
283
+
284
+ streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
285
+
286
+ # Configure generation
287
+ gen_kwargs = {
288
+ "max_new_tokens": max_tokens,
289
+ "do_sample": True if temperature > 0.0 else False,
290
+ "top_k": None,
291
+ }
292
+ if temperature > 0.0:
293
+ gen_kwargs["temperature"] = temperature
294
+ gen_kwargs["top_p"] = top_p
295
+
296
+ generation_config = GenerationConfig(**gen_kwargs)
297
+
298
+ generation_kwargs = dict(
299
+ **model_inputs,
300
+ streamer=streamer,
301
+ generation_config=generation_config
302
+ )
303
+
304
+ thread = Thread(target=model.generate, kwargs=generation_kwargs)
305
+ thread.start()
306
+
307
+ accumulated = ""
308
+ for new_text in streamer:
309
+ accumulated += new_text
310
+ yield accumulated
311
+
312
+
313
+ # Define API Endpoint
314
+ # Using the wrapper helper to handle spaces.GPU decorator safely
315
+ if has_spaces:
316
+ @app.api()
317
+ @spaces.GPU
318
+ def predict(
319
+ message: str,
320
+ system_prompt: str = "You are VibeThinker, a helpful and harmless AI assistant specialized in reasoning.",
321
+ temperature: float = 1.0,
322
+ top_p: float = 0.95,
323
+ max_tokens: int = 4096
324
+ ):
325
+ if is_fallback:
326
+ yield from simulate_inference(message)
327
+ else:
328
+ yield from infer_real(message, system_prompt, temperature, top_p, max_tokens)
329
+ else:
330
+ @app.api()
331
+ def predict(
332
+ message: str,
333
+ system_prompt: str = "You are VibeThinker, a helpful and harmless AI assistant specialized in reasoning.",
334
+ temperature: float = 1.0,
335
+ top_p: float = 0.95,
336
+ max_tokens: int = 4096
337
+ ):
338
+ if is_fallback:
339
+ yield from simulate_inference(message)
340
+ else:
341
+ yield from infer_real(message, system_prompt, temperature, top_p, max_tokens)
342
+
343
+
344
+ # Standard FastAPI Route to serve the frontend html
345
+ @app.get("/", response_class=HTMLResponse)
346
+ def homepage():
347
+ try:
348
+ with open(HTML_PATH, "r", encoding="utf-8") as f:
349
+ return HTMLResponse(content=f.read())
350
+ except FileNotFoundError:
351
+ return HTMLResponse(content="<h1>index.html not found! Please create the frontend page.</h1>", status_code=404)
352
+
353
+
354
+ if __name__ == "__main__":
355
+ app.launch(show_error=True)
index.html ADDED
@@ -0,0 +1,1443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>VibeThinker-3B - Verifiable Reasoning SLM</title>
7
+
8
+ <!-- Fonts -->
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
12
+
13
+ <!-- FontAwesome Icons -->
14
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
15
+
16
+ <!-- Prism.js Tomorrow Theme for Code Highlighting -->
17
+ <link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
18
+
19
+ <!-- MathJax Configuration for LaTeX Math -->
20
+ <script>
21
+ window.MathJax = {
22
+ tex: {
23
+ inlineMath: [['$', '$'], ['\\(', '\\)']],
24
+ displayMath: [['$$', '$$'], ['\\[', '\\]']],
25
+ processEscapes: true
26
+ },
27
+ options: {
28
+ skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
29
+ }
30
+ };
31
+ </script>
32
+ <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
33
+
34
+ <style>
35
+ /* Premium Modern CSS Style Guide */
36
+ :root {
37
+ --bg-dark: #070a13;
38
+ --bg-light-dark: #0f1524;
39
+ --primary: #8b5cf6;
40
+ --primary-hover: #7c3aed;
41
+ --primary-glow: rgba(139, 92, 246, 0.4);
42
+ --secondary: #6366f1;
43
+ --accent: #06b6d4;
44
+ --accent-glow: rgba(6, 182, 212, 0.3);
45
+ --text-main: #f8fafc;
46
+ --text-muted: #94a3b8;
47
+ --border-color: rgba(255, 255, 255, 0.08);
48
+ --card-bg: rgba(15, 21, 36, 0.7);
49
+ --sidebar-width: 320px;
50
+ }
51
+
52
+ * {
53
+ box-sizing: border-box;
54
+ margin: 0;
55
+ padding: 0;
56
+ }
57
+
58
+ body {
59
+ font-family: 'Outfit', sans-serif;
60
+ background-color: var(--bg-dark);
61
+ color: var(--text-main);
62
+ height: 100vh;
63
+ overflow: hidden;
64
+ display: flex;
65
+ position: relative;
66
+ }
67
+
68
+ /* Ambient background glowing blobs */
69
+ .glow-blob {
70
+ position: absolute;
71
+ width: 500px;
72
+ height: 500px;
73
+ border-radius: 50%;
74
+ background: radial-gradient(circle, var(--primary-glow) 0%, rgba(0,0,0,0) 70%);
75
+ top: -150px;
76
+ right: -150px;
77
+ z-index: 0;
78
+ pointer-events: none;
79
+ filter: blur(80px);
80
+ }
81
+
82
+ .glow-blob-2 {
83
+ position: absolute;
84
+ width: 600px;
85
+ height: 600px;
86
+ border-radius: 50%;
87
+ background: radial-gradient(circle, var(--accent-glow) 0%, rgba(0,0,0,0) 70%);
88
+ bottom: -200px;
89
+ left: -200px;
90
+ z-index: 0;
91
+ pointer-events: none;
92
+ filter: blur(100px);
93
+ }
94
+
95
+ /* Layout Wrapper */
96
+ .app-container {
97
+ display: flex;
98
+ width: 100%;
99
+ height: 100vh;
100
+ z-index: 1;
101
+ position: relative;
102
+ }
103
+
104
+ /* Sidebar Styling */
105
+ .sidebar {
106
+ width: var(--sidebar-width);
107
+ background-color: rgba(15, 21, 36, 0.85);
108
+ border-right: 1px solid var(--border-color);
109
+ backdrop-filter: blur(20px);
110
+ padding: 24px;
111
+ display: flex;
112
+ flex-direction: column;
113
+ justify-content: space-between;
114
+ overflow-y: auto;
115
+ transition: transform 0.3s ease;
116
+ }
117
+
118
+ .brand-section {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 12px;
122
+ margin-bottom: 32px;
123
+ }
124
+
125
+ .brand-logo {
126
+ font-size: 32px;
127
+ animation: float 3s ease-in-out infinite;
128
+ }
129
+
130
+ .brand-title {
131
+ font-weight: 700;
132
+ font-size: 22px;
133
+ background: linear-gradient(135deg, #fff 30%, var(--primary) 100%);
134
+ -webkit-background-clip: text;
135
+ -webkit-text-fill-color: transparent;
136
+ letter-spacing: -0.5px;
137
+ }
138
+
139
+ .brand-badge {
140
+ background: rgba(139, 92, 246, 0.2);
141
+ border: 1px solid var(--primary);
142
+ color: #c084fc;
143
+ font-size: 11px;
144
+ padding: 2px 8px;
145
+ border-radius: 12px;
146
+ font-weight: 600;
147
+ }
148
+
149
+ .sidebar-section {
150
+ margin-bottom: 24px;
151
+ }
152
+
153
+ .section-title {
154
+ font-size: 12px;
155
+ font-weight: 600;
156
+ text-transform: uppercase;
157
+ letter-spacing: 1.5px;
158
+ color: var(--text-muted);
159
+ margin-bottom: 16px;
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 8px;
163
+ }
164
+
165
+ /* Config Sliders */
166
+ .parameter-group {
167
+ margin-bottom: 18px;
168
+ }
169
+
170
+ .parameter-label {
171
+ display: flex;
172
+ justify-content: space-between;
173
+ font-size: 13px;
174
+ color: var(--text-main);
175
+ margin-bottom: 6px;
176
+ }
177
+
178
+ .parameter-value {
179
+ font-weight: 600;
180
+ color: var(--accent);
181
+ }
182
+
183
+ input[type="range"] {
184
+ width: 100%;
185
+ height: 6px;
186
+ background: rgba(255, 255, 255, 0.1);
187
+ border-radius: 3px;
188
+ outline: none;
189
+ -webkit-appearance: none;
190
+ }
191
+
192
+ input[type="range"]::-webkit-slider-thumb {
193
+ -webkit-appearance: none;
194
+ width: 14px;
195
+ height: 14px;
196
+ border-radius: 50%;
197
+ background: var(--accent);
198
+ cursor: pointer;
199
+ box-shadow: 0 0 10px var(--accent-glow);
200
+ transition: transform 0.1s ease;
201
+ }
202
+
203
+ input[type="range"]::-webkit-slider-thumb:hover {
204
+ transform: scale(1.2);
205
+ }
206
+
207
+ .textarea-config {
208
+ width: 100%;
209
+ background: rgba(0, 0, 0, 0.25);
210
+ border: 1px solid var(--border-color);
211
+ border-radius: 8px;
212
+ color: var(--text-main);
213
+ padding: 10px;
214
+ font-family: inherit;
215
+ font-size: 13px;
216
+ resize: vertical;
217
+ min-height: 80px;
218
+ outline: none;
219
+ transition: border-color 0.2s;
220
+ }
221
+
222
+ .textarea-config:focus {
223
+ border-color: var(--primary);
224
+ }
225
+
226
+ /* Example Presets */
227
+ .presets-container {
228
+ display: flex;
229
+ flex-direction: column;
230
+ gap: 8px;
231
+ }
232
+
233
+ .preset-btn {
234
+ width: 100%;
235
+ background: rgba(255, 255, 255, 0.03);
236
+ border: 1px solid var(--border-color);
237
+ color: var(--text-main);
238
+ padding: 10px 12px;
239
+ border-radius: 8px;
240
+ cursor: pointer;
241
+ text-align: left;
242
+ font-size: 13px;
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 10px;
246
+ transition: all 0.2s ease;
247
+ }
248
+
249
+ .preset-btn i {
250
+ color: var(--primary);
251
+ font-size: 14px;
252
+ }
253
+
254
+ .preset-btn:hover {
255
+ background: rgba(139, 92, 246, 0.1);
256
+ border-color: var(--primary);
257
+ transform: translateY(-1px);
258
+ }
259
+
260
+ .sidebar-footer {
261
+ border-top: 1px solid var(--border-color);
262
+ padding-top: 16px;
263
+ font-size: 12px;
264
+ color: var(--text-muted);
265
+ display: flex;
266
+ flex-direction: column;
267
+ gap: 8px;
268
+ }
269
+
270
+ .sidebar-link {
271
+ color: var(--accent);
272
+ text-decoration: none;
273
+ display: flex;
274
+ align-items: center;
275
+ gap: 6px;
276
+ transition: color 0.2s;
277
+ }
278
+
279
+ .sidebar-link:hover {
280
+ color: #22d3ee;
281
+ }
282
+
283
+ /* Main Chat Panel */
284
+ .main-panel {
285
+ flex: 1;
286
+ display: flex;
287
+ flex-direction: column;
288
+ height: 100%;
289
+ background: rgba(7, 10, 19, 0.3);
290
+ backdrop-filter: blur(10px);
291
+ position: relative;
292
+ }
293
+
294
+ .chat-header {
295
+ height: 70px;
296
+ border-bottom: 1px solid var(--border-color);
297
+ padding: 0 32px;
298
+ display: flex;
299
+ align-items: center;
300
+ justify-content: space-between;
301
+ background: rgba(15, 21, 36, 0.4);
302
+ }
303
+
304
+ .header-info {
305
+ display: flex;
306
+ flex-direction: column;
307
+ }
308
+
309
+ .header-title {
310
+ font-size: 18px;
311
+ font-weight: 600;
312
+ display: flex;
313
+ align-items: center;
314
+ gap: 8px;
315
+ }
316
+
317
+ .status-container {
318
+ display: flex;
319
+ align-items: center;
320
+ gap: 6px;
321
+ font-size: 13px;
322
+ color: var(--text-muted);
323
+ }
324
+
325
+ .status-dot {
326
+ width: 8px;
327
+ height: 8px;
328
+ border-radius: 50%;
329
+ background-color: #22c55e;
330
+ box-shadow: 0 0 8px #22c55e;
331
+ }
332
+
333
+ .status-dot.busy {
334
+ background-color: #eab308;
335
+ box-shadow: 0 0 8px #eab308;
336
+ animation: pulse 1.5s infinite;
337
+ }
338
+
339
+ /* Chat Messages */
340
+ .chat-history {
341
+ flex: 1;
342
+ overflow-y: auto;
343
+ padding: 32px;
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap: 24px;
347
+ scroll-behavior: smooth;
348
+ }
349
+
350
+ .welcome-card {
351
+ max-width: 720px;
352
+ margin: 40px auto;
353
+ background: var(--card-bg);
354
+ border: 1px solid var(--border-color);
355
+ border-radius: 16px;
356
+ padding: 32px;
357
+ text-align: center;
358
+ box-shadow: 0 20px 40px rgba(0,0,0,0.3);
359
+ }
360
+
361
+ .welcome-icon {
362
+ font-size: 48px;
363
+ margin-bottom: 16px;
364
+ display: inline-block;
365
+ }
366
+
367
+ .welcome-title {
368
+ font-size: 24px;
369
+ font-weight: 700;
370
+ margin-bottom: 12px;
371
+ }
372
+
373
+ .welcome-description {
374
+ color: var(--text-muted);
375
+ font-size: 15px;
376
+ line-height: 1.6;
377
+ margin-bottom: 24px;
378
+ }
379
+
380
+ .welcome-grid {
381
+ display: grid;
382
+ grid-template-columns: 1fr 1fr;
383
+ gap: 16px;
384
+ text-align: left;
385
+ }
386
+
387
+ .welcome-feature {
388
+ padding: 16px;
389
+ background: rgba(255, 255, 255, 0.02);
390
+ border: 1px solid var(--border-color);
391
+ border-radius: 12px;
392
+ }
393
+
394
+ .welcome-feature-title {
395
+ font-weight: 600;
396
+ font-size: 14px;
397
+ margin-bottom: 6px;
398
+ color: var(--accent);
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 8px;
402
+ }
403
+
404
+ .welcome-feature-desc {
405
+ font-size: 12px;
406
+ color: var(--text-muted);
407
+ line-height: 1.4;
408
+ }
409
+
410
+ /* Message Bubbles */
411
+ .message-row {
412
+ display: flex;
413
+ width: 100%;
414
+ margin-bottom: 8px;
415
+ }
416
+
417
+ .message-row.user {
418
+ justify-content: flex-end;
419
+ }
420
+
421
+ .message-row.assistant {
422
+ justify-content: flex-start;
423
+ }
424
+
425
+ .message-bubble {
426
+ max-width: 80%;
427
+ border-radius: 16px;
428
+ padding: 16px 20px;
429
+ position: relative;
430
+ box-shadow: 0 4px 15px rgba(0,0,0,0.15);
431
+ line-height: 1.6;
432
+ }
433
+
434
+ .message-row.user .message-bubble {
435
+ background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
436
+ color: #fff;
437
+ border-bottom-right-radius: 4px;
438
+ }
439
+
440
+ .message-row.assistant .message-bubble {
441
+ background: var(--card-bg);
442
+ border: 1px solid var(--border-color);
443
+ color: var(--text-main);
444
+ border-top-left-radius: 4px;
445
+ }
446
+
447
+ .message-meta {
448
+ font-size: 11px;
449
+ color: var(--text-muted);
450
+ margin-top: 6px;
451
+ display: flex;
452
+ align-items: center;
453
+ gap: 8px;
454
+ }
455
+
456
+ /* Collapsible Thought Box styling */
457
+ .thought-container {
458
+ margin-bottom: 14px;
459
+ border: 1px solid rgba(139, 92, 246, 0.2);
460
+ background: rgba(139, 92, 246, 0.03);
461
+ border-radius: 10px;
462
+ overflow: hidden;
463
+ }
464
+
465
+ .thought-header {
466
+ padding: 8px 12px;
467
+ background: rgba(139, 92, 246, 0.08);
468
+ font-size: 12.5px;
469
+ font-weight: 600;
470
+ color: #c084fc;
471
+ cursor: pointer;
472
+ display: flex;
473
+ justify-content: space-between;
474
+ align-items: center;
475
+ user-select: none;
476
+ transition: background 0.2s;
477
+ }
478
+
479
+ .thought-header:hover {
480
+ background: rgba(139, 92, 246, 0.12);
481
+ }
482
+
483
+ .thought-title-left {
484
+ display: flex;
485
+ align-items: center;
486
+ gap: 6px;
487
+ }
488
+
489
+ .thought-timer {
490
+ font-size: 11px;
491
+ color: var(--text-muted);
492
+ font-weight: normal;
493
+ }
494
+
495
+ .thought-content {
496
+ padding: 12px;
497
+ font-size: 13px;
498
+ font-family: 'Fira Code', monospace;
499
+ color: #cbd5e1;
500
+ border-top: 1px solid rgba(139, 92, 246, 0.1);
501
+ white-space: pre-wrap;
502
+ max-height: 250px;
503
+ overflow-y: auto;
504
+ background: rgba(0, 0, 0, 0.15);
505
+ line-height: 1.5;
506
+ }
507
+
508
+ .thought-status-spinner {
509
+ font-size: 12px;
510
+ animation: spin 1s linear infinite;
511
+ color: var(--primary);
512
+ }
513
+
514
+ .thought-status-done {
515
+ color: #22c55e;
516
+ }
517
+
518
+ /* Code block inside chat message styling */
519
+ pre {
520
+ margin: 12px 0;
521
+ border-radius: 8px;
522
+ overflow: hidden;
523
+ border: 1px solid var(--border-color);
524
+ position: relative;
525
+ }
526
+
527
+ code {
528
+ font-family: 'Fira Code', monospace;
529
+ font-size: 14px;
530
+ }
531
+
532
+ p code, li code {
533
+ background: rgba(255, 255, 255, 0.1);
534
+ padding: 2px 6px;
535
+ border-radius: 4px;
536
+ font-size: 13.5px;
537
+ }
538
+
539
+ /* Markdown styling updates */
540
+ .message-body h1, .message-body h2, .message-body h3 {
541
+ margin-top: 16px;
542
+ margin-bottom: 8px;
543
+ font-weight: 600;
544
+ color: #fff;
545
+ }
546
+ .message-body h1 { font-size: 18px; border-bottom: 1px solid var(--border-color); padding-bottom: 4px; }
547
+ .message-body h2 { font-size: 16px; }
548
+ .message-body h3 { font-size: 14.5px; }
549
+
550
+ .message-body p {
551
+ margin-bottom: 10px;
552
+ }
553
+
554
+ .message-body ul, .message-body ol {
555
+ margin-left: 20px;
556
+ margin-bottom: 10px;
557
+ }
558
+
559
+ .message-body table {
560
+ width: 100%;
561
+ border-collapse: collapse;
562
+ margin: 14px 0;
563
+ font-size: 13px;
564
+ }
565
+
566
+ .message-body th, .message-body td {
567
+ border: 1px solid var(--border-color);
568
+ padding: 8px 12px;
569
+ text-align: left;
570
+ }
571
+
572
+ .message-body th {
573
+ background: rgba(255, 255, 255, 0.05);
574
+ font-weight: 600;
575
+ }
576
+
577
+ /* Copy Button overlay in Code Blocks */
578
+ .code-header {
579
+ background: rgba(0, 0, 0, 0.4);
580
+ padding: 6px 12px;
581
+ font-size: 11px;
582
+ color: var(--text-muted);
583
+ display: flex;
584
+ justify-content: space-between;
585
+ align-items: center;
586
+ border-bottom: 1px solid var(--border-color);
587
+ }
588
+
589
+ .copy-btn {
590
+ background: transparent;
591
+ border: none;
592
+ color: var(--text-muted);
593
+ cursor: pointer;
594
+ font-size: 12px;
595
+ transition: color 0.2s;
596
+ }
597
+
598
+ .copy-btn:hover {
599
+ color: #fff;
600
+ }
601
+
602
+ /* Input Panel */
603
+ .input-panel {
604
+ padding: 24px 32px 32px;
605
+ border-top: 1px solid var(--border-color);
606
+ background: rgba(15, 21, 36, 0.6);
607
+ backdrop-filter: blur(10px);
608
+ }
609
+
610
+ .input-wrapper {
611
+ max-width: 900px;
612
+ margin: 0 auto;
613
+ background: rgba(0, 0, 0, 0.3);
614
+ border: 1px solid var(--border-color);
615
+ border-radius: 14px;
616
+ padding: 8px 12px;
617
+ display: flex;
618
+ flex-direction: column;
619
+ gap: 8px;
620
+ position: relative;
621
+ transition: border-color 0.2s, box-shadow 0.2s;
622
+ }
623
+
624
+ .input-wrapper:focus-within {
625
+ border-color: var(--primary);
626
+ box-shadow: 0 0 15px rgba(139, 92, 246, 0.25);
627
+ }
628
+
629
+ .chat-textarea {
630
+ width: 100%;
631
+ background: transparent;
632
+ border: none;
633
+ outline: none;
634
+ color: var(--text-main);
635
+ font-family: inherit;
636
+ font-size: 15px;
637
+ resize: none;
638
+ min-height: 48px;
639
+ max-height: 200px;
640
+ padding: 8px 4px;
641
+ line-height: 1.5;
642
+ }
643
+
644
+ .input-actions {
645
+ display: flex;
646
+ justify-content: space-between;
647
+ align-items: center;
648
+ border-top: 1px solid rgba(255, 255, 255, 0.04);
649
+ padding-top: 8px;
650
+ }
651
+
652
+ .input-left-buttons {
653
+ display: flex;
654
+ gap: 8px;
655
+ }
656
+
657
+ .action-icon-btn {
658
+ background: transparent;
659
+ border: none;
660
+ width: 32px;
661
+ height: 32px;
662
+ border-radius: 6px;
663
+ color: var(--text-muted);
664
+ cursor: pointer;
665
+ display: flex;
666
+ align-items: center;
667
+ justify-content: center;
668
+ transition: background 0.2s, color 0.2s;
669
+ }
670
+
671
+ .action-icon-btn:hover {
672
+ background: rgba(255, 255, 255, 0.05);
673
+ color: var(--text-main);
674
+ }
675
+
676
+ .send-btn {
677
+ background: var(--primary);
678
+ border: none;
679
+ padding: 8px 16px;
680
+ border-radius: 8px;
681
+ color: #fff;
682
+ font-weight: 600;
683
+ font-size: 13px;
684
+ cursor: pointer;
685
+ display: flex;
686
+ align-items: center;
687
+ gap: 6px;
688
+ transition: background 0.2s, transform 0.1s;
689
+ }
690
+
691
+ .send-btn:hover {
692
+ background: var(--primary-hover);
693
+ }
694
+
695
+ .send-btn:active {
696
+ transform: scale(0.97);
697
+ }
698
+
699
+ .send-btn.stop {
700
+ background: #ef4444;
701
+ }
702
+
703
+ .send-btn.stop:hover {
704
+ background: #dc2626;
705
+ }
706
+
707
+ .info-footer {
708
+ text-align: center;
709
+ font-size: 11px;
710
+ color: var(--text-muted);
711
+ margin-top: 8px;
712
+ }
713
+
714
+ /* Loading placeholder */
715
+ .typing-indicator {
716
+ display: flex;
717
+ gap: 4px;
718
+ padding: 6px 12px;
719
+ }
720
+
721
+ .dot {
722
+ width: 6px;
723
+ height: 6px;
724
+ background-color: var(--text-muted);
725
+ border-radius: 50%;
726
+ animation: bounce 1.4s infinite ease-in-out both;
727
+ }
728
+
729
+ .dot:nth-child(1) { animation-delay: -0.32s; }
730
+ .dot:nth-child(2) { animation-delay: -0.16s; }
731
+
732
+ /* Animations */
733
+ @keyframes float {
734
+ 0%, 100% { transform: translateY(0); }
735
+ 50% { transform: translateY(-6px); }
736
+ }
737
+
738
+ @keyframes spin {
739
+ 100% { transform: rotate(360deg); }
740
+ }
741
+
742
+ @keyframes pulse {
743
+ 0%, 100% { opacity: 1; transform: scale(1); }
744
+ 50% { opacity: 0.5; transform: scale(0.92); }
745
+ }
746
+
747
+ @keyframes bounce {
748
+ 0%, 80%, 100% { transform: scale(0); }
749
+ 40% { transform: scale(1.0); }
750
+ }
751
+
752
+ /* Mobile Responsive */
753
+ .sidebar-toggle {
754
+ display: none;
755
+ background: transparent;
756
+ border: none;
757
+ color: var(--text-main);
758
+ font-size: 20px;
759
+ cursor: pointer;
760
+ }
761
+
762
+ @media (max-width: 768px) {
763
+ .sidebar {
764
+ position: absolute;
765
+ left: 0;
766
+ top: 0;
767
+ height: 100%;
768
+ z-index: 10;
769
+ transform: translateX(-100%);
770
+ }
771
+ .sidebar.open {
772
+ transform: translateX(0);
773
+ }
774
+ .sidebar-toggle {
775
+ display: block;
776
+ }
777
+ .welcome-grid {
778
+ grid-template-columns: 1fr;
779
+ }
780
+ }
781
+ </style>
782
+ </head>
783
+ <body>
784
+
785
+ <!-- Background Ambience -->
786
+ <div class="glow-blob"></div>
787
+ <div class="glow-blob-2"></div>
788
+
789
+ <!-- App Wrapper -->
790
+ <div class="app-container">
791
+
792
+ <!-- Sidebar Panel -->
793
+ <aside class="sidebar" id="sidebar">
794
+ <div>
795
+ <div class="brand-section">
796
+ <div class="brand-logo">😻</div>
797
+ <div>
798
+ <h1 class="brand-title">VibeThinker</h1>
799
+ <span class="brand-badge">3B Scale</span>
800
+ </div>
801
+ </div>
802
+
803
+ <!-- Configuration Settings -->
804
+ <div class="sidebar-section">
805
+ <h2 class="section-title"><i class="fa-solid fa-sliders"></i> Parameters</h2>
806
+
807
+ <div class="parameter-group">
808
+ <div class="parameter-label">
809
+ <span>Temperature</span>
810
+ <span class="parameter-value" id="val-temp">1.0</span>
811
+ </div>
812
+ <input type="range" id="param-temp" min="0.0" max="1.5" step="0.05" value="1.0">
813
+ </div>
814
+
815
+ <div class="parameter-group">
816
+ <div class="parameter-label">
817
+ <span>Top-P</span>
818
+ <span class="parameter-value" id="val-topp">0.95</span>
819
+ </div>
820
+ <input type="range" id="param-topp" min="0.0" max="1.0" step="0.01" value="0.95">
821
+ </div>
822
+
823
+ <div class="parameter-group">
824
+ <div class="parameter-label">
825
+ <span>Max Tokens</span>
826
+ <span class="parameter-value" id="val-tokens">4096</span>
827
+ </div>
828
+ <input type="range" id="param-tokens" min="256" max="32768" step="256" value="4096">
829
+ </div>
830
+
831
+ <div class="parameter-group">
832
+ <div class="parameter-label">
833
+ <span>System Prompt</span>
834
+ </div>
835
+ <textarea class="textarea-config" id="param-system">You are VibeThinker, a helpful and harmless AI assistant specialized in reasoning. You solve complex problems step-by-step to verify correctness.</textarea>
836
+ </div>
837
+ </div>
838
+
839
+ <!-- Preset Prompts -->
840
+ <div class="sidebar-section">
841
+ <h2 class="section-title"><i class="fa-solid fa-lightbulb"></i> Presets</h2>
842
+ <div class="presets-container">
843
+ <button class="preset-btn" onclick="loadPreset('card')">
844
+ <i class="fa-solid fa-calculator"></i> Spade & Face Cards
845
+ </button>
846
+ <button class="preset-btn" onclick="loadPreset('palindrome')">
847
+ <i class="fa-solid fa-code"></i> Longest Palindrome
848
+ </button>
849
+ <button class="preset-btn" onclick="loadPreset('logic')">
850
+ <i class="fa-solid fa-puzzle-piece"></i> Labeled Fruit Boxes
851
+ </button>
852
+ </div>
853
+ </div>
854
+ </div>
855
+
856
+ <!-- Sidebar Footer -->
857
+ <div class="sidebar-footer">
858
+ <a href="https://huggingface.co/WeiboAI/VibeThinker-3B" target="_blank" class="sidebar-link">
859
+ <i class="fa-brands fa-hugging-face"></i> Model Card 🤗
860
+ </a>
861
+ <a href="https://arxiv.org/abs/2606.16140" target="_blank" class="sidebar-link">
862
+ <i class="fa-regular fa-file-pdf"></i> Research Paper
863
+ </a>
864
+ <div>MIT License • WeiboAI 2026</div>
865
+ </div>
866
+ </aside>
867
+
868
+ <!-- Main Panel -->
869
+ <main class="main-panel">
870
+
871
+ <!-- Chat Header -->
872
+ <header class="chat-header">
873
+ <button class="sidebar-toggle" id="sidebar-toggle" onclick="toggleSidebar()">
874
+ <i class="fa-solid fa-bars"></i>
875
+ </button>
876
+ <div class="header-info">
877
+ <div class="header-title">VibeThinker-3B Reasoning Sandbox</div>
878
+ <div class="status-container">
879
+ <div class="status-dot" id="status-indicator"></div>
880
+ <span id="status-text">Server Ready</span>
881
+ </div>
882
+ </div>
883
+ <div>
884
+ <!-- Clean logo design linking HF -->
885
+ <a href="https://huggingface.co" target="_blank" style="color: inherit; text-decoration: none; font-size: 24px; filter: drop-shadow(0 0 8px rgba(255,255,255,0.15));">🤗</a>
886
+ </div>
887
+ </header>
888
+
889
+ <!-- Chat History -->
890
+ <section class="chat-history" id="chat-history">
891
+
892
+ <!-- Welcome Card -->
893
+ <div class="welcome-card" id="welcome-card">
894
+ <span class="welcome-icon">🤗</span>
895
+ <h2 class="welcome-title">VibeThinker-3B Engine</h2>
896
+ <p class="welcome-description">
897
+ VibeThinker-3B is a 3-billion parameter Small Language Model (SLM) trained using curriculum post-training SFT and multi-domain Reinforcement Learning. It achieves near-frontier results on math, coding, and STEM by using a dedicated chain-of-thought strategy.
898
+ </p>
899
+ <div class="welcome-grid">
900
+ <div class="welcome-feature">
901
+ <div class="welcome-feature-title"><i class="fa-solid fa-brain"></i> Chain-of-Thought</div>
902
+ <div class="welcome-feature-desc">Analyzes problems thoroughly inside <code>&lt;think&gt;</code> tags before giving the final answer.</div>
903
+ </div>
904
+ <div class="welcome-feature">
905
+ <div class="welcome-feature-title"><i class="fa-solid fa-code"></i> Coding Logic</div>
906
+ <div class="welcome-feature-desc">Excellent at LeetCode, algorithms, complexity analysis, and programming constructs.</div>
907
+ </div>
908
+ <div class="welcome-feature">
909
+ <div class="welcome-feature-title"><i class="fa-solid fa-calculator"></i> STEM Focus</div>
910
+ <div class="welcome-feature-desc">Verifiably correct solutions to mathematical and reasoning challenges.</div>
911
+ </div>
912
+ <div class="welcome-feature">
913
+ <div class="welcome-feature-title"><i class="fa-solid fa-cloud"></i> Gradio Server</div>
914
+ <div class="welcome-feature-desc">Custom web front-end backed by Gradio's advanced queuing & ZeroGPU engine.</div>
915
+ </div>
916
+ </div>
917
+ </div>
918
+
919
+ </section>
920
+
921
+ <!-- Input Panel -->
922
+ <footer class="input-panel">
923
+ <div class="input-wrapper">
924
+ <textarea class="chat-textarea" id="chat-input" placeholder="Ask a math, logic, or programming question..." rows="1" oninput="autoResizeTextarea(this)"></textarea>
925
+
926
+ <div class="input-actions">
927
+ <div class="input-left-buttons">
928
+ <button class="action-icon-btn" title="Clear Chat" onclick="clearChat()">
929
+ <i class="fa-regular fa-trash-can"></i>
930
+ </button>
931
+ <button class="action-icon-btn" title="Preset Library" onclick="toggleSidebar()">
932
+ <i class="fa-solid fa-sliders"></i>
933
+ </button>
934
+ </div>
935
+
936
+ <button class="send-btn" id="submit-btn" onclick="handleSubmit()">
937
+ <i class="fa-solid fa-paper-plane" id="submit-icon"></i> <span id="submit-text">Send</span>
938
+ </button>
939
+ </div>
940
+ </div>
941
+ <div class="info-footer">
942
+ VibeThinker-3B can produce incorrect reasoning paths. Verify output before critical use.
943
+ </div>
944
+ </footer>
945
+
946
+ </main>
947
+
948
+ </div>
949
+
950
+ <!-- Gradio Client & Execution Logic -->
951
+ <script type="module">
952
+ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
953
+
954
+ let client = null;
955
+ let activeJob = null;
956
+ let isGenerating = false;
957
+ let currentMessageId = null;
958
+ let timerInterval = null;
959
+
960
+ // Connect to Gradio.Server backend
961
+ async function initClient() {
962
+ try {
963
+ updateStatus("connecting", "Initializing connection...");
964
+ client = await Client.connect(window.location.origin);
965
+ updateStatus("ready", "Server Ready");
966
+ } catch (err) {
967
+ console.error("Failed to connect to server:", err);
968
+ updateStatus("error", "Error Connecting to Server");
969
+ }
970
+ }
971
+
972
+ // Connect on load
973
+ window.addEventListener("DOMContentLoaded", () => {
974
+ initClient();
975
+ setupSliders();
976
+ });
977
+
978
+ // Setup configuration sliders value update
979
+ function setupSliders() {
980
+ const sliders = [
981
+ { id: "param-temp", valId: "val-temp" },
982
+ { id: "param-topp", valId: "val-topp" },
983
+ { id: "param-tokens", valId: "val-tokens" }
984
+ ];
985
+
986
+ sliders.forEach(slider => {
987
+ const input = document.getElementById(slider.id);
988
+ const valueSpan = document.getElementById(slider.valId);
989
+
990
+ input.addEventListener("input", () => {
991
+ valueSpan.textContent = input.value;
992
+ });
993
+ });
994
+ }
995
+
996
+ // Toggle sidebar on mobile
997
+ window.toggleSidebar = function() {
998
+ const sidebar = document.getElementById("sidebar");
999
+ sidebar.classList.toggle("open");
1000
+ };
1001
+
1002
+ // Load example presets
1003
+ window.loadPreset = function(type) {
1004
+ const chatInput = document.getElementById("chat-input");
1005
+ if (type === 'palindrome') {
1006
+ chatInput.value = "Write a Python function to solve the Longest Palindromic Substring problem using dynamic programming, with full complexity analysis.";
1007
+ } else if (type === 'card') {
1008
+ chatInput.value = "If a card is drawn from a standard deck, what is the probability that it is a spade or a face card?";
1009
+ } else if (type === 'logic') {
1010
+ chatInput.value = "Three boxes are labeled 'Apples', 'Oranges', and 'Mixed'. All labels are incorrect. You can pick one fruit from one box. How can you label all boxes correctly?";
1011
+ }
1012
+ autoResizeTextarea(chatInput);
1013
+
1014
+ // Close sidebar on mobile after choosing preset
1015
+ if (window.innerWidth <= 768) {
1016
+ document.getElementById("sidebar").classList.remove("open");
1017
+ }
1018
+ chatInput.focus();
1019
+ };
1020
+
1021
+ // Auto-resize textarea as user types
1022
+ window.autoResizeTextarea = function(element) {
1023
+ element.style.height = "auto";
1024
+ element.style.height = (element.scrollHeight) + "px";
1025
+ };
1026
+
1027
+ // Update server status UI
1028
+ function updateStatus(status, text) {
1029
+ const indicator = document.getElementById("status-indicator");
1030
+ const textSpan = document.getElementById("status-text");
1031
+
1032
+ textSpan.textContent = text;
1033
+
1034
+ indicator.className = "status-dot";
1035
+ if (status === "connecting") {
1036
+ indicator.classList.add("busy");
1037
+ } else if (status === "generating") {
1038
+ indicator.classList.add("busy");
1039
+ } else if (status === "ready") {
1040
+ // defaults to green
1041
+ } else if (status === "error") {
1042
+ indicator.style.backgroundColor = "#ef4444";
1043
+ indicator.style.boxShadow = "0 0 8px #ef4444";
1044
+ }
1045
+ }
1046
+
1047
+ // Main Submit handling
1048
+ window.handleSubmit = async function() {
1049
+ if (isGenerating) {
1050
+ // Stop action
1051
+ if (activeJob) {
1052
+ activeJob.cancel();
1053
+ isGenerating = false;
1054
+ updateStatus("ready", "Generation Stopped");
1055
+ resetSubmitButton();
1056
+ }
1057
+ return;
1058
+ }
1059
+
1060
+ const inputField = document.getElementById("chat-input");
1061
+ const prompt = inputField.value.trim();
1062
+
1063
+ if (!prompt) return;
1064
+ if (!client) {
1065
+ alert("Server connection is not established. Trying to reconnect...");
1066
+ initClient();
1067
+ return;
1068
+ }
1069
+
1070
+ // Hide welcome card if visible
1071
+ const welcomeCard = document.getElementById("welcome-card");
1072
+ if (welcomeCard) welcomeCard.remove();
1073
+
1074
+ // Clear input and adjust size
1075
+ inputField.value = "";
1076
+ autoResizeTextarea(inputField);
1077
+
1078
+ // Append user bubble
1079
+ appendMessage("user", prompt);
1080
+
1081
+ // Create empty assistant bubble for streaming
1082
+ currentMessageId = appendMessage("assistant", "", true);
1083
+
1084
+ // Change button state to "Stop"
1085
+ setSubmitButtonToStop();
1086
+ isGenerating = true;
1087
+ updateStatus("generating", "Thinking...");
1088
+
1089
+ // Fetch parameters
1090
+ const temp = parseFloat(document.getElementById("param-temp").value);
1091
+ const topp = parseFloat(document.getElementById("param-topp").value);
1092
+ const tokens = parseInt(document.getElementById("param-tokens").value);
1093
+ const system = document.getElementById("param-system").value;
1094
+
1095
+ // Start elapsed timer
1096
+ let elapsedSeconds = 0;
1097
+ const timerSpan = document.getElementById(`timer-${currentMessageId}`);
1098
+ if (timerSpan) {
1099
+ timerInterval = setInterval(() => {
1100
+ elapsedSeconds += 0.1;
1101
+ timerSpan.textContent = `Thinking for ${elapsedSeconds.toFixed(1)}s`;
1102
+ }, 100);
1103
+ }
1104
+
1105
+ try {
1106
+ // Submit stream request using submit() instead of predict()
1107
+ activeJob = client.submit("/predict", {
1108
+ message: prompt,
1109
+ system_prompt: system,
1110
+ temperature: temp,
1111
+ top_p: topp,
1112
+ max_tokens: tokens
1113
+ });
1114
+
1115
+ activeJob.on("data", (dataEvent) => {
1116
+ const streamedText = dataEvent.data[0];
1117
+ updateAssistantStream(currentMessageId, streamedText);
1118
+ });
1119
+
1120
+ activeJob.on("status", (statusEvent) => {
1121
+ // Can monitor queue status here if needed
1122
+ });
1123
+
1124
+ activeJob.on("error", (err) => {
1125
+ console.error("Job error:", err);
1126
+ clearInterval(timerInterval);
1127
+ updateMessageError(currentMessageId, "An error occurred during text generation.");
1128
+ isGenerating = false;
1129
+ resetSubmitButton();
1130
+ updateStatus("ready", "Error");
1131
+ });
1132
+
1133
+ } catch (err) {
1134
+ console.error("Submission failed:", err);
1135
+ clearInterval(timerInterval);
1136
+ updateMessageError(currentMessageId, "Failed to submit request.");
1137
+ isGenerating = false;
1138
+ resetSubmitButton();
1139
+ updateStatus("ready", "Failed");
1140
+ }
1141
+ };
1142
+
1143
+ function setSubmitButtonToStop() {
1144
+ const btn = document.getElementById("submit-btn");
1145
+ const icon = document.getElementById("submit-icon");
1146
+ const text = document.getElementById("submit-text");
1147
+
1148
+ btn.className = "send-btn stop";
1149
+ icon.className = "fa-solid fa-circle-stop";
1150
+ text.textContent = "Stop";
1151
+ }
1152
+
1153
+ function resetSubmitButton() {
1154
+ const btn = document.getElementById("submit-btn");
1155
+ const icon = document.getElementById("submit-icon");
1156
+ const text = document.getElementById("submit-text");
1157
+
1158
+ btn.className = "send-btn";
1159
+ icon.className = "fa-solid fa-paper-plane";
1160
+ text.textContent = "Send";
1161
+ }
1162
+
1163
+ // Helper to format/parse text stream for collapsible thoughts
1164
+ function parseStreamedText(text) {
1165
+ let thinkingText = "";
1166
+ let responseText = "";
1167
+ let isThinking = false;
1168
+ let hasThoughtCompleted = false;
1169
+
1170
+ if (text.includes("<think>")) {
1171
+ const startIndex = text.indexOf("<think>") + 7;
1172
+ if (text.includes("</think>")) {
1173
+ const endIndex = text.indexOf("</think>");
1174
+ thinkingText = text.substring(startIndex, endIndex).trim();
1175
+ responseText = text.substring(endIndex + 8).trim();
1176
+ hasThoughtCompleted = true;
1177
+ } else {
1178
+ thinkingText = text.substring(startIndex);
1179
+ isThinking = true;
1180
+ }
1181
+ } else {
1182
+ responseText = text;
1183
+ }
1184
+
1185
+ return {
1186
+ thinkingText,
1187
+ responseText,
1188
+ isThinking,
1189
+ hasThoughtCompleted
1190
+ };
1191
+ }
1192
+
1193
+ // Append a message bubble to the chat
1194
+ function appendMessage(sender, text, isLoading = false) {
1195
+ const chatHistory = document.getElementById("chat-history");
1196
+ const messageId = "msg-" + Date.now();
1197
+
1198
+ const row = document.createElement("div");
1199
+ row.className = `message-row ${sender}`;
1200
+ row.id = messageId;
1201
+
1202
+ const bubble = document.createElement("div");
1203
+ bubble.className = "message-bubble";
1204
+
1205
+ if (sender === "user") {
1206
+ const textNode = document.createElement("div");
1207
+ textNode.textContent = text;
1208
+ bubble.appendChild(textNode);
1209
+ } else {
1210
+ // Assistant template layout with collapsible thought block
1211
+ bubble.innerHTML = `
1212
+ <div class="thought-container" id="thought-box-${messageId}" style="display: none;">
1213
+ <div class="thought-header" onclick="toggleThought('${messageId}')">
1214
+ <div class="thought-title-left">
1215
+ <i class="fa-solid fa-brain"></i>
1216
+ <span id="thought-label-${messageId}">Thinking Process</span>
1217
+ <span class="thought-timer" id="timer-${messageId}">Thinking...</span>
1218
+ </div>
1219
+ <div id="thought-status-${messageId}">
1220
+ <i class="fa-solid fa-spinner thought-status-spinner"></i>
1221
+ </div>
1222
+ </div>
1223
+ <div class="thought-content" id="thought-content-${messageId}"></div>
1224
+ </div>
1225
+ <div class="message-body" id="body-${messageId}">
1226
+ ${isLoading ? '<div class="typing-indicator"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>' : ''}
1227
+ </div>
1228
+ `;
1229
+ }
1230
+
1231
+ // Meta timestamp
1232
+ const meta = document.createElement("div");
1233
+ meta.className = "message-meta";
1234
+ const timeStr = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1235
+ meta.innerHTML = `<i class="fa-regular fa-clock"></i> ${timeStr}`;
1236
+ bubble.appendChild(meta);
1237
+
1238
+ row.appendChild(bubble);
1239
+ chatHistory.appendChild(row);
1240
+ chatHistory.scrollTop = chatHistory.scrollHeight;
1241
+
1242
+ return messageId;
1243
+ }
1244
+
1245
+ // Toggle Collapsible thought accordion
1246
+ window.toggleThought = function(messageId) {
1247
+ const content = document.getElementById(`thought-content-${messageId}`);
1248
+ if (content.style.display === "none") {
1249
+ content.style.display = "block";
1250
+ } else {
1251
+ content.style.display = "none";
1252
+ }
1253
+ };
1254
+
1255
+ // Update assistant response bubble on stream
1256
+ function updateAssistantStream(messageId, fullText) {
1257
+ const parsed = parseStreamedText(fullText);
1258
+ const thoughtBox = document.getElementById(`thought-box-${messageId}`);
1259
+ const thoughtContent = document.getElementById(`thought-content-${messageId}`);
1260
+ const thoughtLabel = document.getElementById(`thought-label-${messageId}`);
1261
+ const thoughtStatus = document.getElementById(`thought-status-${messageId}`);
1262
+ const bodyDiv = document.getElementById(`body-${messageId}`);
1263
+
1264
+ // Update Thought process box
1265
+ if (parsed.thinkingText) {
1266
+ thoughtBox.style.display = "block";
1267
+ thoughtContent.textContent = parsed.thinkingText;
1268
+
1269
+ if (parsed.hasThoughtCompleted) {
1270
+ // Stop timer and change spinner to check
1271
+ clearInterval(timerInterval);
1272
+ const timerSpan = document.getElementById(`timer-${messageId}`);
1273
+ const durationStr = timerSpan.textContent.replace("Thinking for ", "");
1274
+ timerSpan.textContent = `Thought for ${durationStr}`;
1275
+
1276
+ thoughtLabel.textContent = "Reasoning Process Complete";
1277
+ thoughtStatus.innerHTML = '<i class="fa-solid fa-circle-check thought-status-done"></i>';
1278
+
1279
+ // Collapse thought content once reasoning finishes and final response starts
1280
+ if (parsed.responseText && thoughtContent.style.display !== "none" && !thoughtContent.dataset.collapsedOnce) {
1281
+ thoughtContent.style.display = "none";
1282
+ thoughtContent.dataset.collapsedOnce = "true";
1283
+ }
1284
+ }
1285
+ }
1286
+
1287
+ // Update response body with parsed Markdown
1288
+ if (parsed.responseText) {
1289
+ bodyDiv.innerHTML = marked.parse(parsed.responseText);
1290
+
1291
+ // Highlight code syntax
1292
+ Prism.highlightAllUnder(bodyDiv);
1293
+
1294
+ // Setup code block copy buttons
1295
+ setupCodeBlockHeaders(bodyDiv);
1296
+ } else if (parsed.hasThoughtCompleted && !parsed.responseText) {
1297
+ bodyDiv.innerHTML = '<div class="typing-indicator"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>';
1298
+ }
1299
+
1300
+ // Typeset Math equations once completed
1301
+ if (parsed.hasThoughtCompleted && (fullText.endsWith(".") || fullText.endsWith("```") || !isGenerating)) {
1302
+ debouncedMathTypeset();
1303
+ }
1304
+
1305
+ const chatHistory = document.getElementById("chat-history");
1306
+ chatHistory.scrollTop = chatHistory.scrollHeight;
1307
+
1308
+ // Handle stream completion check
1309
+ if (parsed.hasThoughtCompleted && parsed.responseText && activeJob && activeJob.is_complete) {
1310
+ finalizeGeneration(messageId);
1311
+ }
1312
+ }
1313
+
1314
+ // Debounce MathJax typeset to avoid layout thrashing during fast streams
1315
+ let mathTimeout = null;
1316
+ function debouncedMathTypeset() {
1317
+ clearTimeout(mathTimeout);
1318
+ mathTimeout = setTimeout(() => {
1319
+ if (window.MathJax && window.MathJax.typesetPromise) {
1320
+ window.MathJax.typesetPromise().catch((err) => console.log("MathJax typesetting error: ", err));
1321
+ }
1322
+ }, 500);
1323
+ }
1324
+
1325
+ // Setup headers and copy buttons for code segments
1326
+ function setupCodeBlockHeaders(container) {
1327
+ const codeBlocks = container.querySelectorAll("pre");
1328
+ codeBlocks.forEach((pre) => {
1329
+ if (pre.querySelector(".code-header")) return; // Header already exists
1330
+
1331
+ const code = pre.querySelector("code");
1332
+ let lang = "code";
1333
+ // Extract language class e.g., language-python
1334
+ const classNames = code.className.split(" ");
1335
+ classNames.forEach(cls => {
1336
+ if (cls.startsWith("language-")) {
1337
+ lang = cls.replace("language-", "");
1338
+ }
1339
+ });
1340
+
1341
+ const header = document.createElement("div");
1342
+ header.className = "code-header";
1343
+ header.innerHTML = `
1344
+ <span>${lang.toUpperCase()}</span>
1345
+ <button class="copy-btn" onclick="copyCode(this)"><i class="fa-regular fa-copy"></i> Copy</button>
1346
+ `;
1347
+ pre.insertBefore(header, code);
1348
+ });
1349
+ }
1350
+
1351
+ // Code copy implementation
1352
+ window.copyCode = function(button) {
1353
+ const pre = button.closest("pre");
1354
+ const code = pre.querySelector("code").textContent;
1355
+ navigator.clipboard.writeText(code).then(() => {
1356
+ button.innerHTML = '<i class="fa-solid fa-check" style="color: #22c55e"></i> Copied!';
1357
+ setTimeout(() => {
1358
+ button.innerHTML = '<i class="fa-regular fa-copy"></i> Copy';
1359
+ }, 2000);
1360
+ });
1361
+ };
1362
+
1363
+ function updateMessageError(messageId, text) {
1364
+ const bodyDiv = document.getElementById(`body-${messageId}`);
1365
+ if (bodyDiv) {
1366
+ bodyDiv.innerHTML = `<span style="color: #ef4444; font-weight: 500;"><i class="fa-solid fa-triangle-exclamation"></i> ${text}</span>`;
1367
+ }
1368
+ }
1369
+
1370
+ // Finalize generation
1371
+ function finalizeGeneration(messageId) {
1372
+ clearInterval(timerInterval);
1373
+ isGenerating = false;
1374
+ resetSubmitButton();
1375
+ updateStatus("ready", "Server Ready");
1376
+
1377
+ // Final full typeset pass
1378
+ if (window.MathJax && window.MathJax.typesetPromise) {
1379
+ window.MathJax.typesetPromise();
1380
+ }
1381
+ }
1382
+
1383
+ // Override activeJob data completion hook
1384
+ // Since activeJob event on completed is standard, we finalize when streaming finishes
1385
+ document.getElementById("chat-input").addEventListener("keydown", (e) => {
1386
+ if (e.key === "Enter" && !e.shiftKey) {
1387
+ e.preventDefault();
1388
+ handleSubmit();
1389
+ }
1390
+ });
1391
+
1392
+ // Clear entire conversation
1393
+ window.clearChat = function() {
1394
+ if (isGenerating) {
1395
+ if (activeJob) activeJob.cancel();
1396
+ isGenerating = false;
1397
+ resetSubmitButton();
1398
+ }
1399
+
1400
+ const chatHistory = document.getElementById("chat-history");
1401
+ chatHistory.innerHTML = `
1402
+ <div class="welcome-card" id="welcome-card">
1403
+ <span class="welcome-icon">🤗</span>
1404
+ <h2 class="welcome-title">VibeThinker-3B Engine</h2>
1405
+ <p class="welcome-description">
1406
+ VibeThinker-3B is a 3-billion parameter Small Language Model (SLM) trained using curriculum post-training SFT and multi-domain Reinforcement Learning. It achieves near-frontier results on math, coding, and STEM by using a dedicated chain-of-thought strategy.
1407
+ </p>
1408
+ <div class="welcome-grid">
1409
+ <div class="welcome-feature">
1410
+ <div class="welcome-feature-title"><i class="fa-solid fa-brain"></i> Chain-of-Thought</div>
1411
+ <div class="welcome-feature-desc">Analyzes problems thoroughly inside <code>&lt;think&gt;</code> tags before giving the final answer.</div>
1412
+ </div>
1413
+ <div class="welcome-feature">
1414
+ <div class="welcome-feature-title"><i class="fa-solid fa-code"></i> Coding Logic</div>
1415
+ <div class="welcome-feature-desc">Excellent at LeetCode, algorithms, complexity analysis, and programming constructs.</div>
1416
+ </div>
1417
+ <div class="welcome-feature">
1418
+ <div class="welcome-feature-title"><i class="fa-solid fa-calculator"></i> STEM Focus</div>
1419
+ <div class="welcome-feature-desc">Verifiably correct solutions to mathematical and reasoning challenges.</div>
1420
+ </div>
1421
+ <div class="welcome-feature">
1422
+ <div class="welcome-feature-title"><i class="fa-solid fa-cloud"></i> Gradio Server</div>
1423
+ <div class="welcome-feature-desc">Custom web front-end backed by Gradio's advanced queuing & ZeroGPU engine.</div>
1424
+ </div>
1425
+ </div>
1426
+ </div>
1427
+ `;
1428
+ updateStatus("ready", "Server Ready");
1429
+ };
1430
+
1431
+ // Hook finalize generation on custom check if necessary
1432
+ // We add an interval hook to check if activeJob has finished and set isGenerating = false
1433
+ setInterval(() => {
1434
+ if (isGenerating && activeJob) {
1435
+ // If the job is resolved or cancelled
1436
+ if (activeJob.status === "complete" || activeJob.status === "error") {
1437
+ finalizeGeneration(currentMessageId);
1438
+ }
1439
+ }
1440
+ }, 1000);
1441
+ </script>
1442
+ </body>
1443
+ </html>
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=6.19.0
2
+ transformers>=4.54.0
3
+ torch
4
+ accelerate
5
+ fastapi
6
+ uvicorn
7
+ huggingface_hub