seawolf2357 commited on
Commit
dd8a0c9
Β·
verified Β·
1 Parent(s): f10a9e1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +349 -55
app.py CHANGED
@@ -6,6 +6,7 @@ import os
6
  # ============================================
7
  # MiniMax-M2.1 Streaming Chat
8
  # Dual Provider: Novita (HF) + MiniMax Official API
 
9
  # ============================================
10
 
11
  # Provider μƒνƒœ 관리
@@ -28,9 +29,9 @@ class ProviderManager:
28
 
29
  def get_status(self):
30
  if self.current_provider == "novita":
31
- return "🟒 Novita"
32
  else:
33
- return "🟑 MiniMax"
34
 
35
  def switch_to_minimax(self):
36
  self.current_provider = "minimax"
@@ -75,13 +76,13 @@ def chat_with_minimax(messages):
75
  if chunk.choices and chunk.choices[0].delta.content:
76
  yield chunk.choices[0].delta.content
77
 
78
- def respond(message, history):
79
  """
80
  Streaming chat with automatic fallback
 
81
  """
82
  if not message.strip():
83
- yield ""
84
- return
85
 
86
  # Build API messages
87
  api_messages = [{
@@ -89,11 +90,9 @@ def respond(message, history):
89
  "content": "You are MiniMax-M2.1, a helpful AI assistant built by MiniMax. You excel at coding, tool use, and complex reasoning tasks. Respond in the same language as the user."
90
  }]
91
 
92
- # Add history (handle both dict and tuple formats)
93
  for h in history:
94
- if isinstance(h, dict):
95
- api_messages.append({"role": h.get("role", "user"), "content": h.get("content", "")})
96
- elif isinstance(h, (list, tuple)) and len(h) == 2:
97
  if h[0]:
98
  api_messages.append({"role": "user", "content": h[0]})
99
  if h[1]:
@@ -102,13 +101,14 @@ def respond(message, history):
102
  api_messages.append({"role": "user", "content": message})
103
 
104
  response_text = ""
 
105
 
106
  # 1μ°¨: Novita
107
  if provider.novita_available and provider.current_provider == "novita":
108
  try:
109
  for chunk in chat_with_novita(api_messages):
110
  response_text += chunk
111
- yield response_text
112
  return
113
  except Exception as e:
114
  error_msg = str(e).lower()
@@ -121,113 +121,407 @@ def respond(message, history):
121
  # 2μ°¨: MiniMax
122
  try:
123
  if not os.environ.get("MINIMAX_API_KEY"):
124
- yield "❌ Error: MINIMAX_API_KEY not configured. Please add it to Space Secrets."
125
  return
126
 
127
  for chunk in chat_with_minimax(api_messages):
128
  response_text += chunk
129
- yield response_text
130
  return
131
 
132
  except Exception as e:
133
  if "rate limit" not in str(e).lower():
134
  provider.reset_to_novita()
135
- yield f"❌ Error: {str(e)}"
 
 
 
 
 
 
 
 
 
136
 
137
  # ============================================
138
- # Header HTML with embedded styles
139
  # ============================================
140
- header_html = """
141
- <style>
142
  @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
143
 
 
144
  .gradio-container {
145
  background-color: #FEF9C3 !important;
146
  background-image: radial-gradient(#1F2937 1px, transparent 1px) !important;
147
  background-size: 20px 20px !important;
 
148
  font-family: 'Comic Neue', cursive, sans-serif !important;
149
  }
150
 
151
- footer { display: none !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
- h1 {
 
154
  font-family: 'Bangers', cursive !important;
155
  color: #1F2937 !important;
156
- text-shadow: 4px 4px 0px #FACC15, 6px 6px 0px #1F2937 !important;
 
 
 
 
 
 
157
  letter-spacing: 3px !important;
 
 
 
 
 
 
 
 
 
 
 
158
  }
159
 
160
- textarea {
 
 
 
 
 
 
161
  border: 3px solid #1F2937 !important;
162
  border-radius: 8px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  font-family: 'Comic Neue', cursive !important;
 
 
164
  }
165
 
166
- button {
 
 
 
 
 
 
 
 
 
 
167
  background: #3B82F6 !important;
168
  border: 3px solid #1F2937 !important;
169
  border-radius: 8px !important;
170
- color: white !important;
171
  font-family: 'Bangers', cursive !important;
172
- box-shadow: 4px 4px 0px #1F2937 !important;
 
 
 
 
 
173
  }
174
 
175
- button:hover {
 
176
  background: #2563EB !important;
177
  transform: translate(-2px, -2px) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  box-shadow: 6px 6px 0px #1F2937 !important;
179
  }
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  .chatbot {
182
  border: 3px solid #1F2937 !important;
183
  border-radius: 12px !important;
184
  box-shadow: 6px 6px 0px #1F2937 !important;
 
185
  }
186
 
 
187
  pre, code {
188
  background: #1F2937 !important;
189
  color: #10B981 !important;
 
190
  border-radius: 6px !important;
 
191
  }
192
- </style>
193
 
194
- <div style="text-align: center; margin: 10px 0;">
195
- <a href="https://www.humangen.ai" target="_blank">
196
- <img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
197
- </a>
 
 
 
 
198
 
199
- </div>
 
 
 
200
 
201
- <div style="background: linear-gradient(135deg, #3B82F6, #8B5CF6); border: 3px solid #1F2937; border-radius: 12px; padding: 15px; color: white; box-shadow: 5px 5px 0px #1F2937; margin: 15px 0;">
202
- <div style="display: flex; justify-content: space-around; flex-wrap: wrap; text-align: center;">
203
- <div><strong style="font-size: 1.5rem;">230B</strong><br><span style="font-size: 0.9rem;">Total Params</span></div>
204
- <div><strong style="font-size: 1.5rem;">10B</strong><br><span style="font-size: 0.9rem;">Active Params</span></div>
205
- <div><strong style="font-size: 1.5rem;">88.6</strong><br><span style="font-size: 0.9rem;">VIBE Score</span></div>
206
- <div><strong style="font-size: 1.5rem;">#1</strong><br><span style="font-size: 0.9rem;">Open Source</span></div>
207
- </div>
208
- </div>
 
 
 
 
 
209
 
210
- <p style="text-align: center; font-family: 'Comic Neue', sans-serif; font-weight: bold; color: #1F2937;">
211
- ⚑ Claude Sonnet 4.5 μˆ˜μ€€μ˜ μ½”λ”© & μ—μ΄μ „νŠΈ μ„±λŠ₯! | πŸ”„ Novita ν¬λ ˆλ”§ μ†Œμ§„ μ‹œ MiniMax API둜 μžλ™ μ „ν™˜
212
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  """
214
 
215
  # ============================================
216
- # Create ChatInterface
217
  # ============================================
218
- demo = gr.ChatInterface(
219
- fn=respond,
220
- title="πŸ€– MINIMAX-M2.1 CHAT πŸ’¬",
221
- description=header_html,
222
- examples=[
223
- "Python으둜 ν€΅μ†ŒνŠΈ μ•Œκ³ λ¦¬μ¦˜μ„ κ΅¬ν˜„ν•΄μ€˜",
224
- "React둜 Todo μ•± μ»΄ν¬λ„ŒνŠΈλ₯Ό λ§Œλ“€μ–΄μ€˜",
225
- "Docker와 Kubernetes의 차이점을 μ„€λͺ…ν•΄μ€˜",
226
- "FastAPI둜 JWT 인증 κ΅¬ν˜„ν•΄μ€˜",
227
- "λ¨Έμ‹ λŸ¬λ‹ λͺ¨λΈ 배포 νŒŒμ΄ν”„λΌμΈμ„ μ„€κ³„ν•΄μ€˜",
228
- ],
229
- theme=gr.themes.Soft(),
230
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
  if __name__ == "__main__":
233
  demo.launch()
 
6
  # ============================================
7
  # MiniMax-M2.1 Streaming Chat
8
  # Dual Provider: Novita (HF) + MiniMax Official API
9
+ # Comic Classic Theme
10
  # ============================================
11
 
12
  # Provider μƒνƒœ 관리
 
29
 
30
  def get_status(self):
31
  if self.current_provider == "novita":
32
+ return "🟒 Novita (HuggingFace)"
33
  else:
34
+ return "🟑 MiniMax Official API"
35
 
36
  def switch_to_minimax(self):
37
  self.current_provider = "minimax"
 
76
  if chunk.choices and chunk.choices[0].delta.content:
77
  yield chunk.choices[0].delta.content
78
 
79
+ def chat_respond(message, history):
80
  """
81
  Streaming chat with automatic fallback
82
+ Returns: tuple (history, status)
83
  """
84
  if not message.strip():
85
+ return history, provider.get_status()
 
86
 
87
  # Build API messages
88
  api_messages = [{
 
90
  "content": "You are MiniMax-M2.1, a helpful AI assistant built by MiniMax. You excel at coding, tool use, and complex reasoning tasks. Respond in the same language as the user."
91
  }]
92
 
93
+ # Add history
94
  for h in history:
95
+ if isinstance(h, (list, tuple)) and len(h) == 2:
 
 
96
  if h[0]:
97
  api_messages.append({"role": "user", "content": h[0]})
98
  if h[1]:
 
101
  api_messages.append({"role": "user", "content": message})
102
 
103
  response_text = ""
104
+ current_status = provider.get_status()
105
 
106
  # 1μ°¨: Novita
107
  if provider.novita_available and provider.current_provider == "novita":
108
  try:
109
  for chunk in chat_with_novita(api_messages):
110
  response_text += chunk
111
+ yield history + [[message, response_text]], "🟒 Novita (HuggingFace)"
112
  return
113
  except Exception as e:
114
  error_msg = str(e).lower()
 
121
  # 2μ°¨: MiniMax
122
  try:
123
  if not os.environ.get("MINIMAX_API_KEY"):
124
+ yield history + [[message, "❌ MINIMAX_API_KEY not configured."]], "πŸ”΄ No API Key"
125
  return
126
 
127
  for chunk in chat_with_minimax(api_messages):
128
  response_text += chunk
129
+ yield history + [[message, response_text]], "🟑 MiniMax Official API"
130
  return
131
 
132
  except Exception as e:
133
  if "rate limit" not in str(e).lower():
134
  provider.reset_to_novita()
135
+ yield history + [[message, f"❌ Error: {str(e)}"]], "πŸ”΄ Error"
136
+
137
+ def reset_provider_fn():
138
+ """μˆ˜λ™μœΌλ‘œ Novita둜 리셋"""
139
+ provider.reset_to_novita()
140
+ return "βœ… Reset to Novita!"
141
+
142
+ def clear_chat_fn():
143
+ """μ±„νŒ… μ΄ˆκΈ°ν™”"""
144
+ return [], "", provider.get_status()
145
 
146
  # ============================================
147
+ # 🎨 Comic Classic Theme CSS
148
  # ============================================
149
+ css = """
150
+ /* ===== 🎨 Google Fonts Import ===== */
151
  @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
152
 
153
+ /* ===== 🎨 Comic Classic 배경 ===== */
154
  .gradio-container {
155
  background-color: #FEF9C3 !important;
156
  background-image: radial-gradient(#1F2937 1px, transparent 1px) !important;
157
  background-size: 20px 20px !important;
158
+ min-height: 100vh !important;
159
  font-family: 'Comic Neue', cursive, sans-serif !important;
160
  }
161
 
162
+ /* ===== ν—ˆκΉ…νŽ˜μ΄μŠ€ 헀더 μˆ¨κΉ€ ===== */
163
+ .huggingface-space-header,
164
+ #space-header,
165
+ .space-header,
166
+ [class*="space-header"],
167
+ .svelte-1ed2p3z,
168
+ .space-header-badge,
169
+ [data-testid="space-header"] {
170
+ display: none !important;
171
+ visibility: hidden !important;
172
+ height: 0 !important;
173
+ width: 0 !important;
174
+ }
175
+
176
+ /* ===== Footer μˆ¨κΉ€ ===== */
177
+ footer,
178
+ .footer,
179
+ .gradio-container footer,
180
+ .built-with,
181
+ [class*="footer"],
182
+ .gradio-footer,
183
+ .show-api,
184
+ .built-with-gradio {
185
+ display: none !important;
186
+ visibility: hidden !important;
187
+ height: 0 !important;
188
+ }
189
 
190
+ /* ===== 🎨 헀더 타이틀 ===== */
191
+ .header-text h1 {
192
  font-family: 'Bangers', cursive !important;
193
  color: #1F2937 !important;
194
+ font-size: 3.5rem !important;
195
+ font-weight: 400 !important;
196
+ text-align: center !important;
197
+ margin-bottom: 0.5rem !important;
198
+ text-shadow:
199
+ 4px 4px 0px #FACC15,
200
+ 6px 6px 0px #1F2937 !important;
201
  letter-spacing: 3px !important;
202
+ -webkit-text-stroke: 2px #1F2937 !important;
203
+ }
204
+
205
+ /* ===== 🎨 μ„œλΈŒνƒ€μ΄ν‹€ ===== */
206
+ .subtitle {
207
+ text-align: center !important;
208
+ font-family: 'Comic Neue', cursive !important;
209
+ font-size: 1.2rem !important;
210
+ color: #1F2937 !important;
211
+ margin-bottom: 1.5rem !important;
212
+ font-weight: 700 !important;
213
  }
214
 
215
+ /* ===== 🎨 μΉ΄λ“œ/νŒ¨λ„ ===== */
216
+ .gr-panel,
217
+ .gr-box,
218
+ .gr-form,
219
+ .block,
220
+ .gr-group {
221
+ background: #FFFFFF !important;
222
  border: 3px solid #1F2937 !important;
223
  border-radius: 8px !important;
224
+ box-shadow: 6px 6px 0px #1F2937 !important;
225
+ transition: all 0.2s ease !important;
226
+ }
227
+
228
+ .gr-panel:hover,
229
+ .block:hover {
230
+ transform: translate(-2px, -2px) !important;
231
+ box-shadow: 8px 8px 0px #1F2937 !important;
232
+ }
233
+
234
+ /* ===== 🎨 μž…λ ₯ ν•„λ“œ ===== */
235
+ textarea,
236
+ input[type="text"],
237
+ input[type="number"] {
238
+ background: #FFFFFF !important;
239
+ border: 3px solid #1F2937 !important;
240
+ border-radius: 8px !important;
241
+ color: #1F2937 !important;
242
  font-family: 'Comic Neue', cursive !important;
243
+ font-size: 1rem !important;
244
+ font-weight: 700 !important;
245
  }
246
 
247
+ textarea:focus,
248
+ input[type="text"]:focus {
249
+ border-color: #3B82F6 !important;
250
+ box-shadow: 4px 4px 0px #3B82F6 !important;
251
+ outline: none !important;
252
+ }
253
+
254
+ /* ===== 🎨 Primary λ²„νŠΌ ===== */
255
+ .gr-button-primary,
256
+ button.primary,
257
+ .gr-button.primary {
258
  background: #3B82F6 !important;
259
  border: 3px solid #1F2937 !important;
260
  border-radius: 8px !important;
261
+ color: #FFFFFF !important;
262
  font-family: 'Bangers', cursive !important;
263
+ font-weight: 400 !important;
264
+ font-size: 1.3rem !important;
265
+ letter-spacing: 2px !important;
266
+ padding: 14px 28px !important;
267
+ box-shadow: 5px 5px 0px #1F2937 !important;
268
+ text-shadow: 1px 1px 0px #1F2937 !important;
269
  }
270
 
271
+ .gr-button-primary:hover,
272
+ button.primary:hover {
273
  background: #2563EB !important;
274
  transform: translate(-2px, -2px) !important;
275
+ box-shadow: 7px 7px 0px #1F2937 !important;
276
+ }
277
+
278
+ .gr-button-primary:active,
279
+ button.primary:active {
280
+ transform: translate(3px, 3px) !important;
281
+ box-shadow: 2px 2px 0px #1F2937 !important;
282
+ }
283
+
284
+ /* ===== 🎨 Secondary λ²„νŠΌ ===== */
285
+ .gr-button-secondary,
286
+ button.secondary {
287
+ background: #EF4444 !important;
288
+ border: 3px solid #1F2937 !important;
289
+ border-radius: 8px !important;
290
+ color: #FFFFFF !important;
291
+ font-family: 'Bangers', cursive !important;
292
+ font-weight: 400 !important;
293
+ font-size: 1.1rem !important;
294
+ box-shadow: 4px 4px 0px #1F2937 !important;
295
+ text-shadow: 1px 1px 0px #1F2937 !important;
296
+ }
297
+
298
+ .gr-button-secondary:hover,
299
+ button.secondary:hover {
300
+ background: #DC2626 !important;
301
+ transform: translate(-2px, -2px) !important;
302
  box-shadow: 6px 6px 0px #1F2937 !important;
303
  }
304
 
305
+ /* ===== 🎨 둜그/μƒνƒœ 좜λ ₯ ===== */
306
+ .status-box textarea {
307
+ background: #1F2937 !important;
308
+ color: #10B981 !important;
309
+ font-family: 'Courier New', monospace !important;
310
+ font-size: 0.9rem !important;
311
+ border: 3px solid #10B981 !important;
312
+ border-radius: 8px !important;
313
+ box-shadow: 4px 4px 0px #10B981 !important;
314
+ }
315
+
316
+ /* ===== 🎨 μ•„μ½”λ””μ–Έ ===== */
317
+ .gr-accordion {
318
+ background: #FACC15 !important;
319
+ border: 3px solid #1F2937 !important;
320
+ border-radius: 8px !important;
321
+ box-shadow: 4px 4px 0px #1F2937 !important;
322
+ }
323
+
324
+ /* ===== 🎨 μ±„νŒ… μ˜μ—­ ===== */
325
  .chatbot {
326
  border: 3px solid #1F2937 !important;
327
  border-radius: 12px !important;
328
  box-shadow: 6px 6px 0px #1F2937 !important;
329
+ background: #FFFFFF !important;
330
  }
331
 
332
+ /* ===== 🎨 μ½”λ“œ 블둝 ===== */
333
  pre, code {
334
  background: #1F2937 !important;
335
  color: #10B981 !important;
336
+ border: 2px solid #10B981 !important;
337
  border-radius: 6px !important;
338
+ font-family: 'Courier New', monospace !important;
339
  }
 
340
 
341
+ /* ===== 🎨 라벨 ===== */
342
+ label,
343
+ .gr-input-label,
344
+ .gr-block-label {
345
+ color: #1F2937 !important;
346
+ font-family: 'Comic Neue', cursive !important;
347
+ font-weight: 700 !important;
348
+ }
349
 
350
+ /* ===== 🎨 μŠ€ν¬λ‘€λ°” ===== */
351
+ ::-webkit-scrollbar {
352
+ width: 12px;
353
+ }
354
 
355
+ ::-webkit-scrollbar-track {
356
+ background: #FEF9C3;
357
+ border: 2px solid #1F2937;
358
+ }
359
+
360
+ ::-webkit-scrollbar-thumb {
361
+ background: #3B82F6;
362
+ border: 2px solid #1F2937;
363
+ }
364
+
365
+ ::-webkit-scrollbar-thumb:hover {
366
+ background: #EF4444;
367
+ }
368
 
369
+ /* ===== 🎨 선택 ν•˜μ΄λΌμ΄νŠΈ ===== */
370
+ ::selection {
371
+ background: #FACC15;
372
+ color: #1F2937;
373
+ }
374
+
375
+ /* ===== λ°˜μ‘ν˜• ===== */
376
+ @media (max-width: 768px) {
377
+ .header-text h1 {
378
+ font-size: 2.2rem !important;
379
+ text-shadow: 3px 3px 0px #FACC15, 4px 4px 0px #1F2937 !important;
380
+ }
381
+
382
+ .gr-button-primary,
383
+ button.primary {
384
+ padding: 12px 20px !important;
385
+ font-size: 1.1rem !important;
386
+ }
387
+ }
388
  """
389
 
390
  # ============================================
391
+ # Gradio Interface (gr.Blocks μŠ€νƒ€μΌ)
392
  # ============================================
393
+ with gr.Blocks(fill_height=True, css=css) as demo:
394
+
395
+ # HOME Badge
396
+ gr.HTML("""
397
+ <div style="text-align: center; margin: 20px 0 10px 0;">
398
+ <a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
399
+ <img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
400
+ </a>
401
+ <a href="https://huggingface.co/MiniMaxAI/MiniMax-M2.1" target="_blank" style="text-decoration: none; margin-left: 10px;">
402
+ <img src="https://img.shields.io/static/v1?label=πŸ€—&message=MiniMax-M2.1&color=ff6f00&labelColor=1F2937&style=for-the-badge" alt="Model">
403
+ </a>
404
+ </div>
405
+ """)
406
+
407
+ # Header Title
408
+ gr.Markdown(
409
+ """# πŸ€– MINIMAX-M2.1 CHAT πŸ’¬""",
410
+ elem_classes="header-text"
411
+ )
412
+
413
+ gr.Markdown(
414
+ """<p class="subtitle">⚑ Claude Sonnet 4.5 μˆ˜μ€€μ˜ μ½”λ”© & μ—μ΄μ „νŠΈ μ„±λŠ₯! 230B νŒŒλΌλ―Έν„° μ˜€ν”ˆμ†ŒμŠ€ λͺ¨λΈ πŸš€</p>"""
415
+ )
416
+
417
+ # Model Info Box
418
+ gr.HTML("""
419
+ <div style="background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%); border: 3px solid #1F2937; border-radius: 12px; padding: 15px; color: white; box-shadow: 5px 5px 0px #1F2937; margin: 0 auto 20px auto; max-width: 800px;">
420
+ <div style="display: flex; justify-content: space-around; flex-wrap: wrap; text-align: center;">
421
+ <div><strong style="font-size: 1.5rem;">230B</strong><br><span style="font-size: 0.9rem;">Total Params</span></div>
422
+ <div><strong style="font-size: 1.5rem;">10B</strong><br><span style="font-size: 0.9rem;">Active Params</span></div>
423
+ <div><strong style="font-size: 1.5rem;">88.6</strong><br><span style="font-size: 0.9rem;">VIBE Score</span></div>
424
+ <div><strong style="font-size: 1.5rem;">#1</strong><br><span style="font-size: 0.9rem;">Open Source</span></div>
425
+ </div>
426
+ </div>
427
+ """)
428
+
429
+ with gr.Row(equal_height=False):
430
+ # Left Column - Chat
431
+ with gr.Column(scale=2, min_width=400):
432
+ # Provider Status
433
+ with gr.Row():
434
+ provider_status = gr.Textbox(
435
+ value="🟒 Novita (HuggingFace)",
436
+ label="πŸ”Œ Current Provider",
437
+ interactive=False,
438
+ elem_classes="status-box",
439
+ scale=3
440
+ )
441
+ reset_btn = gr.Button("πŸ”„ Reset", variant="secondary", scale=1)
442
+
443
+ # Chatbot
444
+ chatbot = gr.Chatbot(
445
+ label="πŸ’¬ Chat",
446
+ height=450,
447
+ show_label=False,
448
+ elem_classes="chatbot"
449
+ )
450
+
451
+ # Input Row
452
+ with gr.Row():
453
+ msg = gr.Textbox(
454
+ label="",
455
+ placeholder="λ©”μ‹œμ§€λ₯Ό μž…λ ₯ν•˜μ„Έμš”... (μ½”λ”©, 뢄석, μ°½μž‘ λ“± 무엇이든!)",
456
+ scale=8,
457
+ container=False
458
+ )
459
+ submit_btn = gr.Button("πŸ“€ SEND", variant="primary", scale=2)
460
+
461
+ # Clear Button
462
+ clear_btn = gr.Button("πŸ—‘οΈ CLEAR CHAT", variant="secondary")
463
+
464
+ # Right Column - Info
465
+ with gr.Column(scale=1, min_width=300):
466
+ with gr.Accordion("πŸ’‘ Example Prompts", open=True):
467
+ gr.Markdown("""
468
+ **μ½”λ”©:**
469
+ - Python으둜 ν€΅μ†ŒνŠΈ κ΅¬ν˜„ν•΄μ€˜
470
+ - React Todo μ•± λ§Œλ“€μ–΄μ€˜
471
+ - FastAPI JWT 인증 κ΅¬ν˜„ν•΄μ€˜
472
+
473
+ **μ„€λͺ…:**
474
+ - Docker vs Kubernetes 차이점
475
+ - REST API 섀계 베슀트 ν”„λž™ν‹°μŠ€
476
+
477
+ **뢄석:**
478
+ - λ¨Έμ‹ λŸ¬λ‹ 배포 νŒŒμ΄ν”„λΌμΈ 섀계
479
+ """)
480
+
481
+ with gr.Accordion("πŸ”Œ Provider Info", open=False):
482
+ gr.Markdown("""
483
+ ### λ“€μ–Ό ν”„λ‘œλ°”μ΄λ”
484
+
485
+ **1μ°¨: Novita (HuggingFace)**
486
+ - HF PRO ν¬λ ˆλ”§ μ‚¬μš©
487
+
488
+ **2μ°¨: MiniMax Official API**
489
+ - ν¬λ ˆλ”§ μ†Œμ§„ μ‹œ μžλ™ μ „ν™˜
490
+ - ν˜„μž¬ ν•œμ‹œμ  무료
491
+
492
+ ### Secrets μ„€μ •
493
+ ```
494
+ HF_TOKEN
495
+ MINIMAX_API_KEY
496
+ ```
497
+ """)
498
+
499
+ with gr.Accordion("🎯 Model Info", open=False):
500
+ gr.Markdown("""
501
+ - **πŸ† SOTA μ½”λ”©** β€” Claude Sonnet 4.5 λŠ₯κ°€
502
+ - **🌍 λ‹€κ΅­μ–΄** β€” Python, Rust, Java, Go, C++
503
+ - **πŸ€– μ—μ΄μ „νŠΈ** β€” λ©€ν‹°μŠ€ν… νƒœμŠ€ν¬
504
+ - **πŸ’‘ μΆ”λ‘ ** β€” Interleaved Thinking
505
+ - **πŸ”§ 도ꡬ** β€” ν•¨μˆ˜ 호좜 지원
506
+ """)
507
+
508
+ # Event Handlers
509
+ def respond(message, history):
510
+ if not message.strip():
511
+ yield history, provider.get_status()
512
+ return
513
+ for result in chat_respond(message, history):
514
+ yield result
515
+
516
+ # Connect events
517
+ msg.submit(respond, [msg, chatbot], [chatbot, provider_status]).then(
518
+ lambda: "", outputs=[msg]
519
+ )
520
+ submit_btn.click(respond, [msg, chatbot], [chatbot, provider_status]).then(
521
+ lambda: "", outputs=[msg]
522
+ )
523
+ clear_btn.click(clear_chat_fn, outputs=[chatbot, msg, provider_status])
524
+ reset_btn.click(reset_provider_fn, outputs=[provider_status])
525
 
526
  if __name__ == "__main__":
527
  demo.launch()