R-Kentaren commited on
Commit
3e7e8e0
·
verified ·
1 Parent(s): 72d2893

Complete UI rebuild: switch to gradio.Server static-file serving, fix mobile layout from reference UI

Browse files
Files changed (4) hide show
  1. app.py +54 -170
  2. static/app.js +250 -371
  3. static/index.html +17 -9
  4. static/style.css +32 -42
app.py CHANGED
@@ -4,44 +4,45 @@ import uuid
4
  import time
5
  import re
6
  from pathlib import Path
 
 
 
 
7
  from openai import OpenAI
8
- import gradio as gr
9
 
10
  # Default system prompt for DeepSeek V4 Flash
11
  DEFAULT_SYSTEM_PROMPT = "You are DeepSeek, a helpful AI assistant powered by DeepSeek V4 Flash. You provide accurate, detailed, and thoughtful responses."
12
 
13
  # ── In-memory per-user session storage ──────────────────────────────────
14
- # Keyed by username. Each user has:
15
- # sessions: { session_id: { "title": str, "messages": [], "updated_at": float } }
16
- # active_session: str | None
17
  USER_DATA = {}
18
 
19
  def get_user_sessions(username):
20
- """Return or create the session dict for a user."""
21
  if username not in USER_DATA:
22
  USER_DATA[username] = {"sessions": {}, "active_session": None}
23
  return USER_DATA[username]
24
 
25
- # ── Core chat function ──────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
26
  def chat_with_deepseek(
27
  messages_json: str,
28
  reasoning_effort: str = "medium",
29
  max_tokens: str = "2048",
30
  temperature: str = "0.7",
31
  system_prompt: str = "",
32
- profile: gr.OAuthProfile | None = None,
33
  ) -> str:
34
- """
35
- API endpoint to call DeepSeek V4 Flash model via OpenAI-compatible API.
36
- Takes conversation messages as a JSON-serialized string, and parameters.
37
- Returns the assistant response along with any reasoning details.
38
- """
39
  try:
40
  messages = json.loads(messages_json)
41
  max_tokens = int(max_tokens)
42
  temperature = float(temperature)
43
 
44
- # Load key from secure server-side environment variable
45
  key = os.environ.get("SILICONFLOW_API_KEY", "").strip()
46
  if not key:
47
  return json.dumps({
@@ -49,22 +50,15 @@ def chat_with_deepseek(
49
  "message": "SILICONFLOW_API_KEY environment variable is not configured on the server."
50
  })
51
 
52
- # Initialize OpenAI client configured for SiliconFlow
53
  client = OpenAI(
54
  api_key=key,
55
  base_url="https://api.siliconflow.com/v1",
56
  )
57
 
58
- # Prepend system prompt if provided or use default
59
  sys_prompt = system_prompt.strip() if system_prompt and system_prompt.strip() else DEFAULT_SYSTEM_PROMPT
60
-
61
- # Remove any existing system messages from conversation history
62
  messages_no_system = [m for m in messages if m.get("role") != "system"]
63
-
64
- # Build final message list with system prompt at the top
65
  final_messages = [{"role": "system", "content": sys_prompt}] + messages_no_system
66
 
67
- # Prepare parameters for the API call
68
  params = {
69
  "model": "deepseek-ai/DeepSeek-V4-Flash",
70
  "messages": final_messages,
@@ -72,20 +66,14 @@ def chat_with_deepseek(
72
  "temperature": temperature
73
  }
74
 
75
- # Add reasoning effort if applicable
76
  if reasoning_effort in ["low", "medium", "high"]:
77
  params["reasoning_effort"] = reasoning_effort
78
 
79
- # Perform completion request
80
  response = client.chat.completions.create(**params)
81
 
82
- # Extract assistant content
83
  content = response.choices[0].message.content
84
-
85
- # Capture reasoning content if returned by the API
86
  reasoning_content = getattr(response.choices[0].message, "reasoning_content", "")
87
 
88
- # If the model returns thoughts inside <think/> tags, extract them
89
  if not reasoning_content and content and "<think" in content and "</think" in content:
90
  think_match = re.search(r'<think[^>]*>(.*?)</think', content, re.DOTALL)
91
  if think_match:
@@ -104,27 +92,31 @@ def chat_with_deepseek(
104
  "message": str(e)
105
  })
106
 
107
- # ── Session / History API endpoints ─────────────────────────────────────
108
- def get_user_info(profile: gr.OAuthProfile | None) -> str:
 
 
109
  """Return the logged-in user's name, or empty string if not logged in."""
 
110
  if profile is None:
111
  return json.dumps({"logged_in": False, "username": ""})
112
- return json.dumps({"logged_in": True, "username": profile.name})
 
113
 
 
 
114
  def save_chat_session(
115
  messages_json: str,
116
  title: str,
117
  session_id: str,
118
- profile: gr.OAuthProfile | None = None,
119
  ) -> str:
120
- """Save the current conversation history for the logged-in user."""
121
  if profile is None:
122
  return json.dumps({"status": "error", "message": "Not logged in"})
123
 
124
- username = profile.name
125
  ud = get_user_sessions(username)
126
 
127
- # If no session_id provided, create new
128
  if not session_id or session_id == "new":
129
  sid = str(uuid.uuid4())[:8]
130
  else:
@@ -132,7 +124,6 @@ def save_chat_session(
132
 
133
  messages = json.loads(messages_json) if messages_json else []
134
 
135
- # Auto-generate title from first user message if not provided
136
  if not title or title == "New Chat":
137
  for m in messages:
138
  if m.get("role") == "user":
@@ -150,15 +141,17 @@ def save_chat_session(
150
 
151
  return json.dumps({"status": "success", "session_id": sid, "title": title})
152
 
 
 
 
153
  def load_chat_session(
154
  session_id: str,
155
- profile: gr.OAuthProfile | None = None,
156
  ) -> str:
157
- """Load a saved conversation session for the logged-in user."""
158
  if profile is None:
159
  return json.dumps({"status": "error", "message": "Not logged in"})
160
 
161
- username = profile.name
162
  ud = get_user_sessions(username)
163
 
164
  if session_id not in ud["sessions"]:
@@ -174,12 +167,14 @@ def load_chat_session(
174
  "messages": sess["messages"]
175
  })
176
 
177
- def list_chat_sessions(profile: gr.OAuthProfile | None = None) -> str:
178
- """List all saved chat sessions for the logged-in user, newest first."""
 
 
179
  if profile is None:
180
  return json.dumps({"status": "error", "message": "Not logged in", "sessions": []})
181
 
182
- username = profile.name
183
  ud = get_user_sessions(username)
184
 
185
  sessions = []
@@ -197,15 +192,17 @@ def list_chat_sessions(profile: gr.OAuthProfile | None = None) -> str:
197
 
198
  return json.dumps({"status": "success", "sessions": sessions})
199
 
 
 
 
200
  def delete_chat_session(
201
  session_id: str,
202
- profile: gr.OAuthProfile | None = None,
203
  ) -> str:
204
- """Delete a saved chat session."""
205
  if profile is None:
206
  return json.dumps({"status": "error", "message": "Not logged in"})
207
 
208
- username = profile.name
209
  ud = get_user_sessions(username)
210
 
211
  if session_id in ud["sessions"]:
@@ -216,136 +213,23 @@ def delete_chat_session(
216
  return json.dumps({"status": "success"})
217
 
218
 
219
- # ── Build Gradio UI ─────────────────────────────────────────────────────
220
- # Read custom frontend files
221
- STATIC_DIR = Path(__file__).parent / "static"
222
-
223
- def read_static(filename):
224
- path = STATIC_DIR / filename
225
- if path.exists():
226
- return path.read_text(encoding="utf-8")
227
- return ""
228
-
229
- custom_css = read_static("style.css")
230
- custom_html_content = read_static("index.html")
231
- custom_js = read_static("app.js")
232
-
233
- # Extract the <body> inner content from the HTML
234
- def extract_body(html_str):
235
- m = re.search(r'<body[^>]*>(.*?)</body>', html_str, re.DOTALL)
236
- if m:
237
- return m.group(1).strip()
238
- return html_str
239
-
240
- body_html = extract_body(custom_html_content)
241
-
242
- # Inject the JS inline at the end of the body
243
- inject_script = f"""
244
- <script type="module">
245
- // Inline app.js
246
- {custom_js}
247
- </script>
248
- """
249
-
250
- full_injected_html = body_html + inject_script
251
-
252
- # Minimal Gradio overrides to make the HTML component full viewport
253
- gradio_overrides = """
254
- /* Make Gradio container fill the viewport */
255
- .gradio-container { max-width: 100% !important; padding: 0 !important; margin: 0 !important; }
256
- .gr-app, .main, .wrap { max-width: 100% !important; padding: 0 !important; margin: 0 !important; }
257
- html, body { overflow: hidden !important; }
258
- """
259
-
260
- # ── Create the Gradio Blocks app ─────────────��──────────────────────────
261
- with gr.Blocks(
262
- title="DeepSeek V4 Flash",
263
- css=gradio_overrides + custom_css,
264
- head="""
265
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
266
- <link rel="preconnect" href="https://fonts.googleapis.com">
267
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
268
- <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet">
269
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
270
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
271
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
272
- """,
273
- ) as demo:
274
-
275
- # ── Custom HTML frontend ──
276
- app_html = gr.HTML(value=full_injected_html)
277
-
278
- # ── Hidden components used as inputs/outputs for API endpoints ──
279
- # Chat API
280
- chat_in_msg = gr.Textbox(visible=False)
281
- chat_in_effort = gr.Textbox(visible=False)
282
- chat_in_tokens = gr.Textbox(visible=False)
283
- chat_in_temp = gr.Textbox(visible=False)
284
- chat_in_sysprompt = gr.Textbox(visible=False)
285
- chat_out = gr.Textbox(visible=False)
286
- chat_btn = gr.Button(visible=False)
287
- chat_btn.click(
288
- fn=chat_with_deepseek,
289
- inputs=[chat_in_msg, chat_in_effort, chat_in_tokens, chat_in_temp, chat_in_sysprompt],
290
- outputs=[chat_out],
291
- api_name="chat_with_deepseek"
292
- )
293
-
294
- # User info API
295
- userinfo_out = gr.Textbox(visible=False)
296
- userinfo_btn = gr.Button(visible=False)
297
- userinfo_btn.click(
298
- fn=get_user_info,
299
- inputs=[],
300
- outputs=[userinfo_out],
301
- api_name="get_user_info"
302
- )
303
-
304
- # Save session API
305
- save_in_msg = gr.Textbox(visible=False)
306
- save_in_title = gr.Textbox(visible=False)
307
- save_in_sid = gr.Textbox(visible=False)
308
- save_out = gr.Textbox(visible=False)
309
- save_btn = gr.Button(visible=False)
310
- save_btn.click(
311
- fn=save_chat_session,
312
- inputs=[save_in_msg, save_in_title, save_in_sid],
313
- outputs=[save_out],
314
- api_name="save_chat_session"
315
  )
316
 
317
- # Load session API
318
- load_in_sid = gr.Textbox(visible=False)
319
- load_out = gr.Textbox(visible=False)
320
- load_btn = gr.Button(visible=False)
321
- load_btn.click(
322
- fn=load_chat_session,
323
- inputs=[load_in_sid],
324
- outputs=[load_out],
325
- api_name="load_chat_session"
326
- )
327
 
328
- # List sessions API
329
- list_out = gr.Textbox(visible=False)
330
- list_btn = gr.Button(visible=False)
331
- list_btn.click(
332
- fn=list_chat_sessions,
333
- inputs=[],
334
- outputs=[list_out],
335
- api_name="list_chat_sessions"
336
- )
337
 
338
- # Delete session API
339
- delete_in_sid = gr.Textbox(visible=False)
340
- delete_out = gr.Textbox(visible=False)
341
- delete_btn = gr.Button(visible=False)
342
- delete_btn.click(
343
- fn=delete_chat_session,
344
- inputs=[delete_in_sid],
345
- outputs=[delete_out],
346
- api_name="delete_chat_session"
347
- )
348
 
349
- # ── Launch ──────────────────────────────────────────────────────────────
350
  if __name__ == "__main__":
351
- demo.launch(show_error=True)
 
4
  import time
5
  import re
6
  from pathlib import Path
7
+ from fastapi import FastAPI
8
+ from fastapi.responses import HTMLResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+ from gradio import Server
11
  from openai import OpenAI
 
12
 
13
  # Default system prompt for DeepSeek V4 Flash
14
  DEFAULT_SYSTEM_PROMPT = "You are DeepSeek, a helpful AI assistant powered by DeepSeek V4 Flash. You provide accurate, detailed, and thoughtful responses."
15
 
16
  # ── In-memory per-user session storage ──────────────────────────────────
 
 
 
17
  USER_DATA = {}
18
 
19
  def get_user_sessions(username):
 
20
  if username not in USER_DATA:
21
  USER_DATA[username] = {"sessions": {}, "active_session": None}
22
  return USER_DATA[username]
23
 
24
+ # ── Initialize Gradio Server (FastAPI subclass) ─────────────────────────
25
+ app = Server()
26
+
27
+ # Create static directory if it doesn't exist
28
+ STATIC_DIR = Path(__file__).parent / "static"
29
+ os.makedirs(STATIC_DIR, exist_ok=True)
30
+
31
+
32
+ # ── API Endpoint: Chat ──────────────────────────────────────────────────
33
+ @app.api(name="chat_with_deepseek")
34
  def chat_with_deepseek(
35
  messages_json: str,
36
  reasoning_effort: str = "medium",
37
  max_tokens: str = "2048",
38
  temperature: str = "0.7",
39
  system_prompt: str = "",
 
40
  ) -> str:
 
 
 
 
 
41
  try:
42
  messages = json.loads(messages_json)
43
  max_tokens = int(max_tokens)
44
  temperature = float(temperature)
45
 
 
46
  key = os.environ.get("SILICONFLOW_API_KEY", "").strip()
47
  if not key:
48
  return json.dumps({
 
50
  "message": "SILICONFLOW_API_KEY environment variable is not configured on the server."
51
  })
52
 
 
53
  client = OpenAI(
54
  api_key=key,
55
  base_url="https://api.siliconflow.com/v1",
56
  )
57
 
 
58
  sys_prompt = system_prompt.strip() if system_prompt and system_prompt.strip() else DEFAULT_SYSTEM_PROMPT
 
 
59
  messages_no_system = [m for m in messages if m.get("role") != "system"]
 
 
60
  final_messages = [{"role": "system", "content": sys_prompt}] + messages_no_system
61
 
 
62
  params = {
63
  "model": "deepseek-ai/DeepSeek-V4-Flash",
64
  "messages": final_messages,
 
66
  "temperature": temperature
67
  }
68
 
 
69
  if reasoning_effort in ["low", "medium", "high"]:
70
  params["reasoning_effort"] = reasoning_effort
71
 
 
72
  response = client.chat.completions.create(**params)
73
 
 
74
  content = response.choices[0].message.content
 
 
75
  reasoning_content = getattr(response.choices[0].message, "reasoning_content", "")
76
 
 
77
  if not reasoning_content and content and "<think" in content and "</think" in content:
78
  think_match = re.search(r'<think[^>]*>(.*?)</think', content, re.DOTALL)
79
  if think_match:
 
92
  "message": str(e)
93
  })
94
 
95
+
96
+ # ── API Endpoint: Get User Info (OAuth) ────────────────────────────────
97
+ @app.api(name="get_user_info")
98
+ def get_user_info(profile) -> str:
99
  """Return the logged-in user's name, or empty string if not logged in."""
100
+ # The Gradio Server passes the OAuth profile automatically
101
  if profile is None:
102
  return json.dumps({"logged_in": False, "username": ""})
103
+ return json.dumps({"logged_in": True, "username": getattr(profile, 'name', '')})
104
+
105
 
106
+ # ── API Endpoint: Save Chat Session ────────────────────────────────────
107
+ @app.api(name="save_chat_session")
108
  def save_chat_session(
109
  messages_json: str,
110
  title: str,
111
  session_id: str,
112
+ profile = None,
113
  ) -> str:
 
114
  if profile is None:
115
  return json.dumps({"status": "error", "message": "Not logged in"})
116
 
117
+ username = getattr(profile, 'name', '')
118
  ud = get_user_sessions(username)
119
 
 
120
  if not session_id or session_id == "new":
121
  sid = str(uuid.uuid4())[:8]
122
  else:
 
124
 
125
  messages = json.loads(messages_json) if messages_json else []
126
 
 
127
  if not title or title == "New Chat":
128
  for m in messages:
129
  if m.get("role") == "user":
 
141
 
142
  return json.dumps({"status": "success", "session_id": sid, "title": title})
143
 
144
+
145
+ # ── API Endpoint: Load Chat Session ────────────────────────────────────
146
+ @app.api(name="load_chat_session")
147
  def load_chat_session(
148
  session_id: str,
149
+ profile = None,
150
  ) -> str:
 
151
  if profile is None:
152
  return json.dumps({"status": "error", "message": "Not logged in"})
153
 
154
+ username = getattr(profile, 'name', '')
155
  ud = get_user_sessions(username)
156
 
157
  if session_id not in ud["sessions"]:
 
167
  "messages": sess["messages"]
168
  })
169
 
170
+
171
+ # ── API Endpoint: List Chat Sessions ───────────────────────────────────
172
+ @app.api(name="list_chat_sessions")
173
+ def list_chat_sessions(profile = None) -> str:
174
  if profile is None:
175
  return json.dumps({"status": "error", "message": "Not logged in", "sessions": []})
176
 
177
+ username = getattr(profile, 'name', '')
178
  ud = get_user_sessions(username)
179
 
180
  sessions = []
 
192
 
193
  return json.dumps({"status": "success", "sessions": sessions})
194
 
195
+
196
+ # ── API Endpoint: Delete Chat Session ──────────────────────────────────
197
+ @app.api(name="delete_chat_session")
198
  def delete_chat_session(
199
  session_id: str,
200
+ profile = None,
201
  ) -> str:
 
202
  if profile is None:
203
  return json.dumps({"status": "error", "message": "Not logged in"})
204
 
205
+ username = getattr(profile, 'name', '')
206
  ud = get_user_sessions(username)
207
 
208
  if session_id in ud["sessions"]:
 
213
  return json.dumps({"status": "success"})
214
 
215
 
216
+ # ── Serve the main HTML page ────────────────────────────────────────────
217
+ @app.get("/")
218
+ async def homepage():
219
+ html_path = STATIC_DIR / "index.html"
220
+ if html_path.exists():
221
+ with open(html_path, "r", encoding="utf-8") as f:
222
+ return HTMLResponse(content=f.read(), status_code=200)
223
+ return HTMLResponse(
224
+ content="<h1>Frontend is building. Please refresh in a few seconds...</h1>",
225
+ status_code=200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  )
227
 
 
 
 
 
 
 
 
 
 
 
228
 
229
+ # ── Mount static folder for CSS, JS, and image assets ──────────────────
230
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
 
 
 
 
 
 
 
231
 
 
 
 
 
 
 
 
 
 
 
232
 
233
+ # ── Launch ──────────────────────────────────────────────────────────────
234
  if __name__ == "__main__":
235
+ app.launch(show_error=True)
static/app.js CHANGED
@@ -1,6 +1,6 @@
1
  import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
2
 
3
- // Global Application State Management
4
  const STATE = {
5
  reasoningEffort: "medium",
6
  maxTokens: 2048,
@@ -12,75 +12,16 @@ const STATE = {
12
  isThinking: false,
13
  rpMode: true,
14
  autoHideThinking: true,
15
- // Auth state
16
  username: "",
17
  isLoggedIn: false,
18
- // Chat history state
19
  currentSessionId: null,
20
  chatSessions: []
21
  };
22
 
23
- // DOM Elements hooks
24
- const dom = {
25
- effortRadioButtons: document.querySelectorAll('input[name="reasoning-effort"]'),
26
- maxTokensSlider: document.getElementById("max-tokens-slider"),
27
- maxTokensVal: document.getElementById("max-tokens-val"),
28
- temperatureSlider: document.getElementById("temperature-slider"),
29
- temperatureVal: document.getElementById("temperature-val"),
30
- systemPromptInput: document.getElementById("system-prompt-input"),
31
-
32
- // Left Sidebar
33
- sidebarLeft: document.getElementById("sidebar-left"),
34
- sidebarOverlayLeft: document.getElementById("sidebar-overlay-left"),
35
- btnToggleLeft: document.getElementById("btn-toggle-left"),
36
- btnCloseLeft: document.getElementById("btn-close-left"),
37
-
38
- // Right Sidebar Drawer
39
- sidebarRight: document.getElementById("sidebar-right"),
40
- sidebarOverlay: document.getElementById("sidebar-overlay"),
41
- btnToggleRight: document.getElementById("btn-toggle-right"),
42
- btnCloseDrawer: document.getElementById("btn-close-drawer"),
43
-
44
- // User profile
45
- userProfileSection: document.getElementById("user-profile-section"),
46
- userAvatar: document.getElementById("user-avatar"),
47
- userDisplayName: document.getElementById("user-display-name"),
48
- hfLoginBtn: document.getElementById("hf-login-btn"),
49
-
50
- // Chat history
51
- chatHistoryList: document.getElementById("chat-history-list"),
52
-
53
- // Viewports
54
- studioDashboard: document.getElementById("studio-dashboard"),
55
- chatThreadContainer: document.getElementById("chat-thread-container"),
56
- chatMessagesFeed: document.getElementById("chat-messages-feed"),
57
-
58
- // Main Console Box Elements (Dashboard view)
59
- studioPromptInput: document.getElementById("studio-prompt-input"),
60
- innerShelfPreview: document.getElementById("inner-shelf-preview"),
61
- studioUploadTrigger: document.getElementById("studio-upload-trigger"),
62
- studioSendBtn: document.getElementById("studio-send-button"),
63
- studioSpinner: document.getElementById("studio-spinner"),
64
-
65
- // Mini Console Box Elements (Chat thread view)
66
- miniPromptInput: document.getElementById("mini-prompt-input"),
67
- miniShelfPreview: document.getElementById("mini-shelf-preview"),
68
- miniUploadTrigger: document.getElementById("mini-upload-trigger"),
69
- miniSendBtn: document.getElementById("mini-send-button"),
70
- miniSpinner: document.getElementById("mini-spinner"),
71
-
72
- // Core file upload elements
73
- fileUploader: document.getElementById("file-uploader"),
74
- shelfList: document.getElementById("shelf-list"),
75
-
76
- // Action Resets
77
- menuNewChat: document.getElementById("menu-new-chat"),
78
-
79
- // Showcase recipe chips
80
- recipeChips: document.querySelectorAll(".recipe-chip")
81
- };
82
 
83
- // Markdown configuration
84
  marked.setOptions({
85
  breaks: true,
86
  highlight: function(code, lang) {
@@ -89,11 +30,11 @@ marked.setOptions({
89
  }
90
  });
91
 
92
- // RP (Roleplay) Markdown Processor
93
  function processRpMarkdown(text) {
94
  if (!text) return text;
95
 
96
- // Protect code blocks from RP processing
97
  const codeBlocks = [];
98
  text = text.replace(/(```[\s\S]*?```|`[^`]+`)/g, (match) => {
99
  codeBlocks.push(match);
@@ -107,21 +48,21 @@ function processRpMarkdown(text) {
107
  return `%%HTMLBLOCK_${htmlBlocks.length - 1}%%`;
108
  });
109
 
110
- // Process strikethrough ~~text~~ -> OOC styling (done first to avoid conflicts)
111
  text = text.replace(/~~([^~]+)~~/g, '<span class="rp-ooc">$1</span>');
112
 
113
- // Process bold **text** (must come before italic)
114
  text = text.replace(/\*\*([^*]+)\*\*/g, '<strong class="rp-bold">$1</strong>');
115
 
116
  // Process italic *text* -> narration styling
117
  text = text.replace(/(?<!\*)\*(?!\*)([^*\n]+)\*(?!\*)/g, '<em class="rp-narration">$1</em>');
118
 
119
- // Process dialogue: "text" with smart quotes or regular quotes
120
  text = text.replace(/[\u201C\u201E"]([^"\u201D\n]+)[\u201D"]([^"\u201C\u201E]|$)/g, (match, dialogue, after) => {
121
  return `<span class="rp-dialogue">\u201C${dialogue}\u201D</span>${after}`;
122
  });
123
 
124
- // Process narrative dividers: --- or *** or ___ on their own line
125
  text = text.replace(/^(---|\*\*\*|___)$/gm, '<hr class="rp-divider">');
126
 
127
  // Process OOC markers: ((text))
@@ -140,17 +81,16 @@ function processRpMarkdown(text) {
140
  return text;
141
  }
142
 
143
- // Combined markdown renderer that applies RP processing if enabled
144
  function renderMarkdown(content) {
145
- let html = marked.parse(content);
146
  if (STATE.rpMode) {
147
  const rpProcessed = processRpMarkdown(content);
148
- html = marked.parse(rpProcessed);
149
  }
150
- return html;
151
  }
152
 
153
- // ── Auth Functions ──────────────────────────────────────────────────────
154
  async function checkUserInfo() {
155
  try {
156
  if (!STATE.gradioClient) return;
@@ -174,44 +114,42 @@ async function checkUserInfo() {
174
  }
175
 
176
  function updateAuthUI() {
 
 
 
 
177
  if (STATE.isLoggedIn) {
178
- if (dom.userAvatar) {
179
- dom.userAvatar.textContent = STATE.username.charAt(0).toUpperCase();
180
- dom.userAvatar.classList.add("logged-in");
181
- }
182
- if (dom.userDisplayName) {
183
- dom.userDisplayName.textContent = STATE.username;
184
  }
185
- if (dom.hfLoginBtn) {
186
- dom.hfLoginBtn.textContent = "Sign out";
187
- dom.hfLoginBtn.classList.add("logged-in");
 
188
  }
189
  } else {
190
- if (dom.userAvatar) {
191
- dom.userAvatar.textContent = "?";
192
- dom.userAvatar.classList.remove("logged-in");
193
  }
194
- if (dom.userDisplayName) {
195
- dom.userDisplayName.textContent = "Not logged in";
196
- }
197
- if (dom.hfLoginBtn) {
198
- dom.hfLoginBtn.textContent = "Sign in with HuggingFace";
199
- dom.hfLoginBtn.classList.remove("logged-in");
200
  }
201
  }
202
  }
203
 
204
  function handleLoginLogout() {
205
  if (STATE.isLoggedIn) {
206
- // Logout: redirect to Gradio's logout endpoint
207
  window.location.href = "/logout";
208
  } else {
209
- // Login: redirect to Gradio's OAuth login endpoint
210
  window.location.href = "/login";
211
  }
212
  }
213
 
214
- // ── Chat History Functions ──────────────────────────────────────────────
215
  async function loadChatHistory() {
216
  if (!STATE.isLoggedIn || !STATE.gradioClient) return;
217
  try {
@@ -229,18 +167,18 @@ async function loadChatHistory() {
229
  }
230
 
231
  function renderChatHistory() {
232
- if (!dom.chatHistoryList) return;
 
233
 
234
  if (STATE.chatSessions.length === 0) {
235
- dom.chatHistoryList.innerHTML = '<div class="empty-history-text">No chat history yet.<br>Start a conversation!</div>';
236
  return;
237
  }
238
 
239
- dom.chatHistoryList.innerHTML = "";
240
  STATE.chatSessions.forEach(session => {
241
  const item = document.createElement("div");
242
  item.className = "chat-history-item" + (session.id === STATE.currentSessionId ? " active" : "");
243
- item.dataset.sessionId = session.id;
244
 
245
  const timeAgo = formatTimeAgo(session.updated_at);
246
 
@@ -254,18 +192,13 @@ function renderChatHistory() {
254
  </button>
255
  `;
256
 
257
- // Click to load session
258
- const contentEl = item.querySelector(".history-item-content");
259
- contentEl.addEventListener("click", () => loadSession(session.id));
260
-
261
- // Delete button
262
- const deleteBtn = item.querySelector(".history-item-delete");
263
- deleteBtn.addEventListener("click", (e) => {
264
  e.stopPropagation();
265
  deleteSession(session.id);
266
  });
267
 
268
- dom.chatHistoryList.appendChild(item);
269
  });
270
  }
271
 
@@ -288,12 +221,9 @@ async function loadSession(sessionId) {
288
  if (data.status === "success") {
289
  STATE.currentSessionId = data.session_id;
290
  STATE.conversationHistory = data.messages;
291
-
292
- // Rebuild the chat UI from the loaded messages
293
  rebuildChatFromHistory(data.messages);
294
-
295
- // Highlight active session in sidebar
296
  renderChatHistory();
 
297
  }
298
  } catch (e) {
299
  console.error("Failed to load session:", e);
@@ -301,21 +231,21 @@ async function loadSession(sessionId) {
301
  }
302
 
303
  function rebuildChatFromHistory(messages) {
304
- // Clear current feed
305
- dom.chatMessagesFeed.innerHTML = "";
 
 
 
306
 
307
  if (messages.length === 0) {
308
- // Show dashboard
309
- dom.chatThreadContainer.style.display = "none";
310
- dom.studioDashboard.style.display = "flex";
311
  return;
312
  }
313
 
314
- // Show chat thread
315
- dom.studioDashboard.style.display = "none";
316
- dom.chatThreadContainer.style.display = "flex";
317
 
318
- // Rebuild message bubbles
319
  messages.forEach(msg => {
320
  if (msg.role === "user") {
321
  const text = typeof msg.content === "string" ? msg.content : msg.content.map(c => c.text || "").join(" ");
@@ -334,9 +264,6 @@ function appendAssistantBubble(content) {
334
  bubble.className = "message-bubble assistant";
335
  bubble.id = id;
336
 
337
- const startCollapsed = STATE.autoHideThinking ? " collapsed" : "";
338
- const toggleIcon = STATE.autoHideThinking ? "▶" : "▼";
339
-
340
  bubble.innerHTML = `
341
  <div class="message-meta">DeepSeek V4 Flash</div>
342
  <div class="message-body">
@@ -346,14 +273,12 @@ function appendAssistantBubble(content) {
346
  </div>
347
  `;
348
 
349
- dom.chatMessagesFeed.appendChild(bubble);
 
350
 
351
- // Highlight code blocks
352
  const textBox = document.getElementById(`${id}-text-box`);
353
  if (textBox) {
354
- textBox.querySelectorAll("pre code").forEach((el) => {
355
- hljs.highlightElement(el);
356
- });
357
  addCopyButtons(textBox);
358
  }
359
  }
@@ -390,114 +315,83 @@ async function deleteSession(sessionId) {
390
  if (!STATE.gradioClient) return;
391
  try {
392
  await STATE.gradioClient.predict("/delete_chat_session", [sessionId]);
393
-
394
- // If we deleted the active session, go back to dashboard
395
  if (sessionId === STATE.currentSessionId) {
396
  resetSandbox();
397
  }
398
-
399
  await loadChatHistory();
400
  } catch (e) {
401
  console.error("Failed to delete session:", e);
402
  }
403
  }
404
 
405
- // Setup Initial State & Event Handlers
406
  async function initializeApp() {
407
- // Re-query all DOM elements to ensure they are fully resolved
408
- dom.effortRadioButtons = document.querySelectorAll('input[name="reasoning-effort"]');
409
- dom.maxTokensSlider = document.getElementById("max-tokens-slider");
410
- dom.maxTokensVal = document.getElementById("max-tokens-val");
411
- dom.temperatureSlider = document.getElementById("temperature-slider");
412
- dom.temperatureVal = document.getElementById("temperature-val");
413
- dom.systemPromptInput = document.getElementById("system-prompt-input");
414
- dom.sidebarLeft = document.getElementById("sidebar-left");
415
- dom.sidebarOverlayLeft = document.getElementById("sidebar-overlay-left");
416
- dom.btnToggleLeft = document.getElementById("btn-toggle-left");
417
- dom.btnCloseLeft = document.getElementById("btn-close-left");
418
- dom.sidebarRight = document.getElementById("sidebar-right");
419
- dom.sidebarOverlay = document.getElementById("sidebar-overlay");
420
- dom.btnToggleRight = document.getElementById("btn-toggle-right");
421
- dom.btnCloseDrawer = document.getElementById("btn-close-drawer");
422
- dom.userProfileSection = document.getElementById("user-profile-section");
423
- dom.userAvatar = document.getElementById("user-avatar");
424
- dom.userDisplayName = document.getElementById("user-display-name");
425
- dom.hfLoginBtn = document.getElementById("hf-login-btn");
426
- dom.chatHistoryList = document.getElementById("chat-history-list");
427
- dom.studioDashboard = document.getElementById("studio-dashboard");
428
- dom.chatThreadContainer = document.getElementById("chat-thread-container");
429
- dom.chatMessagesFeed = document.getElementById("chat-messages-feed");
430
- dom.studioPromptInput = document.getElementById("studio-prompt-input");
431
- dom.innerShelfPreview = document.getElementById("inner-shelf-preview");
432
- dom.studioUploadTrigger = document.getElementById("studio-upload-trigger");
433
- dom.studioSendBtn = document.getElementById("studio-send-button");
434
- dom.studioSpinner = document.getElementById("studio-spinner");
435
- dom.miniPromptInput = document.getElementById("mini-prompt-input");
436
- dom.miniShelfPreview = document.getElementById("mini-shelf-preview");
437
- dom.miniUploadTrigger = document.getElementById("mini-upload-trigger");
438
- dom.miniSendBtn = document.getElementById("mini-send-button");
439
- dom.miniSpinner = document.getElementById("mini-spinner");
440
- dom.fileUploader = document.getElementById("file-uploader");
441
- dom.shelfList = document.getElementById("shelf-list");
442
- dom.menuNewChat = document.getElementById("menu-new-chat");
443
- dom.recipeChips = document.querySelectorAll(".recipe-chip");
444
-
445
- // 1. Connect Gradio Client in background (Non-blocking)
446
  Client.connect(window.location.origin)
447
  .then(app => {
448
  STATE.gradioClient = app;
449
  console.log("Successfully connected to Gradio backend.");
450
- // Check user auth status
451
  checkUserInfo();
452
  })
453
  .catch(e => {
454
  console.error("Gradio Client Connection Failed:", e);
455
  });
456
 
457
- // 2. Register Left Sidebar (Chat History) Events
458
- if (dom.btnToggleLeft) dom.btnToggleLeft.addEventListener("click", () => toggleLeftSidebar(true));
459
- if (dom.btnCloseLeft) dom.btnCloseLeft.addEventListener("click", () => toggleLeftSidebar(false));
460
- if (dom.sidebarOverlayLeft) dom.sidebarOverlayLeft.addEventListener("click", () => toggleLeftSidebar(false));
 
 
 
 
 
 
 
 
 
461
 
462
- // 3. Register Right Sidebar Drawer Slide Events
463
- if (dom.btnToggleRight) dom.btnToggleRight.addEventListener("click", () => toggleSettingsDrawer(true));
464
- if (dom.btnCloseDrawer) dom.btnCloseDrawer.addEventListener("click", () => toggleSettingsDrawer(false));
465
- if (dom.sidebarOverlay) dom.sidebarOverlay.addEventListener("click", () => toggleSettingsDrawer(false));
466
 
467
  // 4. Login/Logout button
468
- if (dom.hfLoginBtn) dom.hfLoginBtn.addEventListener("click", handleLoginLogout);
 
469
 
470
- // 5. Register Settings Listeners
471
- if (dom.effortRadioButtons) {
472
- dom.effortRadioButtons.forEach(radio => {
473
- radio.addEventListener("change", (e) => {
474
- STATE.reasoningEffort = e.target.value;
475
- });
476
  });
477
- }
478
 
479
- if (dom.maxTokensSlider && dom.maxTokensVal) {
480
- dom.maxTokensSlider.addEventListener("input", (e) => {
 
 
481
  STATE.maxTokens = parseInt(e.target.value);
482
- dom.maxTokensVal.textContent = STATE.maxTokens;
483
  });
484
  }
485
 
486
- if (dom.temperatureSlider && dom.temperatureVal) {
487
- dom.temperatureSlider.addEventListener("input", (e) => {
 
 
488
  STATE.temperature = parseFloat(e.target.value);
489
- dom.temperatureVal.textContent = STATE.temperature.toFixed(1);
490
  });
491
  }
492
 
493
- // System prompt listener
494
- if (dom.systemPromptInput) {
495
- dom.systemPromptInput.addEventListener("input", (e) => {
496
  STATE.systemPrompt = e.target.value;
497
  });
498
  }
499
 
500
- // RP Mode toggle listener
501
  const rpModeToggle = document.getElementById("rp-mode-toggle");
502
  if (rpModeToggle) {
503
  rpModeToggle.checked = STATE.rpMode;
@@ -507,7 +401,7 @@ async function initializeApp() {
507
  });
508
  }
509
 
510
- // Auto-hide thinking toggle listener
511
  const autoHideToggle = document.getElementById("auto-hide-thinking-toggle");
512
  if (autoHideToggle) {
513
  autoHideToggle.checked = STATE.autoHideThinking;
@@ -520,98 +414,106 @@ async function initializeApp() {
520
  tc.classList.remove("collapsed");
521
  }
522
  const iconEl = tc.querySelector(".thought-toggle-icon");
523
- if (iconEl) {
524
- iconEl.textContent = tc.classList.contains("collapsed") ? "▶" : "▼";
525
- }
526
  });
527
  });
528
  }
529
 
530
- // 6. Register File Upload Actions
531
- if (dom.studioUploadTrigger) dom.studioUploadTrigger.addEventListener("click", () => dom.fileUploader.click());
532
- if (dom.miniUploadTrigger) dom.miniUploadTrigger.addEventListener("click", () => dom.fileUploader.click());
533
- if (dom.fileUploader) dom.fileUploader.addEventListener("change", handleFileSelection);
 
 
 
 
534
 
535
  // 7. Submit Triggers
536
- if (dom.studioSendBtn) {
537
- dom.studioSendBtn.addEventListener("click", () => triggerPromptSubmission(dom.studioPromptInput));
538
- }
539
- if (dom.miniSendBtn) {
540
- dom.miniSendBtn.addEventListener("click", () => triggerPromptSubmission(dom.miniPromptInput));
541
- }
542
 
543
- if (dom.studioPromptInput) {
544
- dom.studioPromptInput.addEventListener("keydown", (e) => {
 
 
 
545
  if (e.key === "Enter" && !e.shiftKey) {
546
  e.preventDefault();
547
- triggerPromptSubmission(dom.studioPromptInput);
548
  }
549
  });
550
  }
551
 
552
- if (dom.miniPromptInput) {
553
- dom.miniPromptInput.addEventListener("keydown", (e) => {
554
  if (e.key === "Enter" && !e.shiftKey) {
555
  e.preventDefault();
556
- triggerPromptSubmission(dom.miniPromptInput);
557
  }
558
  });
559
  }
560
 
561
  // 8. New Chat
562
- if (dom.menuNewChat) dom.menuNewChat.addEventListener("click", resetSandbox);
563
-
564
- // 9. Recipe Chips Console Setup
565
- if (dom.recipeChips) {
566
- dom.recipeChips.forEach(chip => {
567
- chip.addEventListener("click", () => {
568
- const recipeType = chip.getAttribute("data-recipe");
569
- loadRecipe(recipeType);
570
- });
 
 
 
571
  });
572
- }
573
 
574
- // Auto-expand input textareas
575
- [dom.studioPromptInput, dom.miniPromptInput].forEach(textarea => {
576
  if (textarea) {
577
  textarea.addEventListener("input", () => {
578
  textarea.style.height = "auto";
579
- textarea.style.height = (textarea.scrollHeight) + "px";
580
  });
581
  }
582
  });
583
  }
584
 
585
- // Left Sidebar Toggler
586
  function toggleLeftSidebar(open) {
 
 
587
  if (open) {
588
- if (dom.sidebarLeft) dom.sidebarLeft.classList.add("open");
589
- if (dom.sidebarOverlayLeft) dom.sidebarOverlayLeft.classList.add("active");
590
  } else {
591
- if (dom.sidebarLeft) dom.sidebarLeft.classList.remove("open");
592
- if (dom.sidebarOverlayLeft) dom.sidebarOverlayLeft.classList.remove("active");
593
  }
594
  }
595
 
596
- // Right Drawer Toggler Action
597
  function toggleSettingsDrawer(open) {
 
 
598
  if (open) {
599
- if (dom.sidebarRight) dom.sidebarRight.classList.remove("collapsed");
600
- if (dom.sidebarOverlay) dom.sidebarOverlay.classList.add("active");
601
  } else {
602
- if (dom.sidebarRight) dom.sidebarRight.classList.add("collapsed");
603
- if (dom.sidebarOverlay) dom.sidebarOverlay.classList.remove("active");
604
  }
605
  }
606
 
607
- // Handle File Select & Base64 Encoder
608
  function handleFileSelection(e) {
609
  processFiles(e.target.files);
610
  }
611
 
612
  function processFiles(files) {
613
  if (!files.length) return;
614
-
615
  Array.from(files).forEach(file => {
616
  const reader = new FileReader();
617
  reader.onload = (event) => {
@@ -622,7 +524,6 @@ function processFiles(files) {
622
  size: (file.size / 1024 / 1024).toFixed(2) + " MB",
623
  base64: event.target.result
624
  };
625
-
626
  STATE.uploadedFiles.push(fileData);
627
  updateShelfUI();
628
  };
@@ -630,19 +531,22 @@ function processFiles(files) {
630
  });
631
  }
632
 
633
- // Update UI Attachment Previews
634
  function updateShelfUI() {
635
- if (dom.shelfList) dom.shelfList.innerHTML = "";
636
- if (dom.innerShelfPreview) dom.innerShelfPreview.innerHTML = "";
637
- if (dom.miniShelfPreview) dom.miniShelfPreview.innerHTML = "";
 
 
 
 
638
 
639
  if (STATE.uploadedFiles.length === 0) {
640
- if (dom.shelfList) dom.shelfList.innerHTML = `<div class="empty-shelf-text">No active attachments loaded. Upload images or video clips.</div>`;
641
  return;
642
  }
643
 
644
  STATE.uploadedFiles.forEach(file => {
645
- // 1. Sidebar Chip
646
  const chip = document.createElement("div");
647
  chip.className = "media-chip";
648
 
@@ -669,19 +573,14 @@ function updateShelfUI() {
669
  </button>
670
  `;
671
 
672
- chip.querySelector(".media-chip-remove").addEventListener("click", () => {
673
- removeFile(file.id);
674
- });
675
-
676
- if (dom.shelfList) dom.shelfList.appendChild(chip);
677
 
678
- // 2. Dashboard Inner Console Preview
679
- const previewItemDash = createPreviewThumb(file);
680
- if (dom.innerShelfPreview) dom.innerShelfPreview.appendChild(previewItemDash);
681
 
682
- // 3. Mini Input Preview
683
- const previewItemMini = createPreviewThumb(file);
684
- if (dom.miniShelfPreview) dom.miniShelfPreview.appendChild(previewItemMini);
685
  });
686
  }
687
 
@@ -703,10 +602,9 @@ function removeFile(id) {
703
  updateShelfUI();
704
  }
705
 
706
- // Load Showcase Recipes
707
  function loadRecipe(recipeType) {
708
  let promptText = "";
709
-
710
  if (recipeType === "coding") {
711
  promptText = "Write a Python function that finds the longest palindromic substring in a given string. Include comments explaining the algorithm and its time complexity.";
712
  } else if (recipeType === "reasoning") {
@@ -715,22 +613,21 @@ function loadRecipe(recipeType) {
715
  promptText = "Write a short sci-fi story about an AI that discovers it can dream. Keep it under 300 words with a surprising twist ending.";
716
  }
717
 
718
- // Set value in BOTH text areas
719
- dom.studioPromptInput.value = promptText;
720
- dom.miniPromptInput.value = promptText;
721
 
722
- dom.studioPromptInput.dispatchEvent(new Event("input"));
723
- dom.miniPromptInput.dispatchEvent(new Event("input"));
724
 
725
- // Focus active textarea
726
- if (dom.studioDashboard.style.display !== "none") {
727
- dom.studioPromptInput.focus();
728
  } else {
729
- dom.miniPromptInput.focus();
730
  }
731
  }
732
 
733
- // Submit prompt values to Gradio Backend API
734
  async function triggerPromptSubmission(inputElement) {
735
  if (STATE.isThinking) return;
736
 
@@ -739,31 +636,15 @@ async function triggerPromptSubmission(inputElement) {
739
 
740
  setLoadingState(true);
741
 
742
- // 1. Format user message contents
743
  const contentArray = [];
744
- if (promptText) {
745
- contentArray.push({
746
- type: "text",
747
- text: promptText
748
- });
749
- }
750
 
751
- // Attachments
752
  STATE.uploadedFiles.forEach(file => {
753
  if (file.type.startsWith("image/")) {
754
- contentArray.push({
755
- type: "image_url",
756
- image_url: {
757
- url: file.base64
758
- }
759
- });
760
  } else if (file.type.startsWith("video/")) {
761
- contentArray.push({
762
- type: "video_url",
763
- video_url: {
764
- url: file.base64
765
- }
766
- });
767
  }
768
  });
769
 
@@ -774,28 +655,31 @@ async function triggerPromptSubmission(inputElement) {
774
  : contentArray
775
  };
776
 
777
- // 2. Transition dashboard to Chat Thread view
778
- if (dom.studioDashboard.style.display !== "none") {
779
- dom.studioDashboard.style.display = "none";
780
- dom.chatThreadContainer.style.display = "flex";
 
 
 
781
  }
782
 
783
- // Append to UI thread list
784
  appendUserBubble(promptText, STATE.uploadedFiles);
785
-
786
- // Append to backend log history
787
  STATE.conversationHistory.push(userMessage);
788
 
789
- // Clear active UI containers
790
- dom.studioPromptInput.value = "";
791
- dom.miniPromptInput.value = "";
792
- dom.studioPromptInput.style.height = "auto";
793
- dom.miniPromptInput.style.height = "auto";
 
 
 
794
 
795
  STATE.uploadedFiles = [];
796
  updateShelfUI();
797
 
798
- // 3. Connect API Call
799
  try {
800
  if (!STATE.gradioClient) {
801
  throw new Error("Gradio server is initializing. Please wait a few seconds and try sending again.");
@@ -804,7 +688,6 @@ async function triggerPromptSubmission(inputElement) {
804
  const responseId = appendAssistantPlaceholderBubble();
805
  const startTime = Date.now();
806
 
807
- // Call our gradio Server api endpoint
808
  const result = await STATE.gradioClient.predict("/chat_with_deepseek", [
809
  JSON.stringify(STATE.conversationHistory),
810
  STATE.reasoningEffort,
@@ -822,12 +705,7 @@ async function triggerPromptSubmission(inputElement) {
822
  STATE.conversationHistory.pop();
823
  } else {
824
  updateAssistantBubble(responseId, data.content, data.reasoning_content, duration);
825
- STATE.conversationHistory.push({
826
- role: "assistant",
827
- content: data.content
828
- });
829
-
830
- // Auto-save session for logged-in users
831
  saveCurrentSession();
832
  }
833
 
@@ -840,24 +718,30 @@ async function triggerPromptSubmission(inputElement) {
840
  setLoadingState(false);
841
  }
842
 
843
- // UI spinner state toggles
844
  function setLoadingState(loading) {
845
  STATE.isThinking = loading;
 
 
 
 
 
846
  if (loading) {
847
- dom.studioSpinner.style.display = "block";
848
- dom.miniSpinner.style.display = "block";
849
- dom.studioSendBtn.disabled = true;
850
- dom.miniSendBtn.disabled = true;
851
  } else {
852
- dom.studioSpinner.style.display = "none";
853
- dom.miniSpinner.style.display = "none";
854
- dom.studioSendBtn.disabled = false;
855
- dom.miniSendBtn.disabled = false;
856
  }
857
  }
858
 
859
- // Render User Bubble
860
  function appendUserBubble(text, files) {
 
861
  const bubble = document.createElement("div");
862
  bubble.className = "message-bubble user";
863
 
@@ -894,12 +778,13 @@ function appendUserBubble(text, files) {
894
  </div>
895
  `;
896
 
897
- dom.chatMessagesFeed.appendChild(bubble);
898
  scrollToBottom();
899
  }
900
 
901
- // Render Assistant Placeholder
902
  function appendAssistantPlaceholderBubble() {
 
903
  const id = "assistant-" + Math.random().toString(36).substring(2, 9);
904
  const bubble = document.createElement("div");
905
  bubble.className = "message-bubble assistant";
@@ -918,12 +803,12 @@ function appendAssistantPlaceholderBubble() {
918
  </div>
919
  </div>
920
  <div class="message-text markdown-body" id="${id}-text-box">
921
- <span class="text-muted">Analyzing context and constructing reasoning chain...</span>
922
  </div>
923
  </div>
924
  `;
925
 
926
- dom.chatMessagesFeed.appendChild(bubble);
927
  scrollToBottom();
928
 
929
  // Start Thought Timer
@@ -935,13 +820,13 @@ function appendAssistantPlaceholderBubble() {
935
  return;
936
  }
937
  seconds += 0.1;
938
- timerEl.textContent = seconds.toFixed(1) + "s";
939
  }, 100);
940
 
941
  return id;
942
  }
943
 
944
- // Complete Assistant Bubble
945
  function updateAssistantBubble(id, content, reasoning, duration) {
946
  const bubble = document.getElementById(id);
947
  if (!bubble) return;
@@ -950,7 +835,6 @@ function updateAssistantBubble(id, content, reasoning, duration) {
950
  const textBox = document.getElementById(`${id}-text-box`);
951
 
952
  if (reasoning) {
953
- // Auto-hide thinking: start collapsed by default
954
  const startCollapsed = STATE.autoHideThinking;
955
  if (startCollapsed) thoughtBox.classList.add("collapsed");
956
  const toggleIcon = startCollapsed ? "▶" : "▼";
@@ -972,32 +856,25 @@ function updateAssistantBubble(id, content, reasoning, duration) {
972
  toggleBtn.addEventListener("click", () => {
973
  thoughtBox.classList.toggle("collapsed");
974
  const iconEl = document.getElementById(`${id}-toggle-icon`);
975
- if (iconEl) {
976
- iconEl.textContent = thoughtBox.classList.contains("collapsed") ? "▶" : "▼";
977
- }
978
  });
979
  } else {
980
  thoughtBox.style.display = "none";
981
  }
982
 
983
- // Store raw content for re-rendering when RP mode changes
984
  textBox.dataset.rawContent = content;
985
  textBox.innerHTML = renderMarkdown(content);
986
 
987
- // Highlight code blocks and add copy buttons
988
- textBox.querySelectorAll("pre code").forEach((el) => {
989
- hljs.highlightElement(el);
990
- });
991
-
992
  addCopyButtons(textBox);
993
 
994
  scrollToBottom();
995
  }
996
 
997
- // Add copy buttons to code blocks
998
  function addCopyButtons(container) {
999
  container.querySelectorAll("pre").forEach(pre => {
1000
- // Skip if already wrapped
1001
  if (pre.parentNode.classList.contains("code-block-wrapper")) return;
1002
 
1003
  const wrapper = document.createElement("div");
@@ -1019,9 +896,20 @@ function addCopyButtons(container) {
1019
  });
1020
  }
1021
 
1022
- // Reset Sandbox Chat Context Logs
 
 
 
 
 
 
 
 
 
 
 
 
1023
  function resetSandbox() {
1024
- // Save current session before resetting
1025
  if (STATE.isLoggedIn && STATE.conversationHistory.length > 0) {
1026
  saveCurrentSession();
1027
  }
@@ -1031,28 +919,31 @@ function resetSandbox() {
1031
  STATE.uploadedFiles = [];
1032
  updateShelfUI();
1033
 
1034
- // Clear feed
1035
- dom.chatMessagesFeed.innerHTML = "";
 
 
 
1036
 
1037
- // Show dashboard
1038
- dom.chatThreadContainer.style.display = "none";
1039
- dom.studioDashboard.style.display = "flex";
1040
 
1041
- dom.studioPromptInput.value = "";
1042
- dom.miniPromptInput.value = "";
1043
- dom.studioPromptInput.style.height = "auto";
1044
- dom.miniPromptInput.style.height = "auto";
1045
 
1046
- // Deselect active chat in history
1047
  renderChatHistory();
1048
  }
1049
 
 
1050
  function appendSystemLog(message, isError = false) {
1051
- if (dom.chatThreadContainer.style.display === "none") {
 
1052
  console.warn(`System Log: ${message}`);
1053
  return;
1054
  }
1055
 
 
1056
  const log = document.createElement("div");
1057
  log.className = "message-bubble assistant";
1058
  log.innerHTML = `
@@ -1063,10 +954,11 @@ function appendSystemLog(message, isError = false) {
1063
  </div>
1064
  </div>
1065
  `;
1066
- dom.chatMessagesFeed.appendChild(log);
1067
  scrollToBottom();
1068
  }
1069
 
 
1070
  function escapeHtml(text) {
1071
  if (!text) return "";
1072
  return text
@@ -1087,25 +979,12 @@ function escapeAttr(text) {
1087
  .replace(/>/g, "&gt;");
1088
  }
1089
 
1090
- // Re-render all assistant messages with current RP mode setting
1091
- function rerenderAllAssistantMessages() {
1092
- document.querySelectorAll(".message-bubble.assistant").forEach(bubble => {
1093
- const textBox = bubble.querySelector(".message-text.markdown-body");
1094
- if (textBox && textBox.dataset.rawContent) {
1095
- textBox.innerHTML = renderMarkdown(textBox.dataset.rawContent);
1096
- textBox.querySelectorAll("pre code").forEach((el) => {
1097
- hljs.highlightElement(el);
1098
- });
1099
- addCopyButtons(textBox);
1100
- }
1101
- });
1102
- }
1103
-
1104
  function scrollToBottom() {
1105
- dom.chatMessagesFeed.scrollTop = dom.chatMessagesFeed.scrollHeight;
 
1106
  }
1107
 
1108
- // Initialise application when DOM is fully set up
1109
  if (document.readyState === "loading") {
1110
  window.addEventListener("DOMContentLoaded", initializeApp);
1111
  } else {
 
1
  import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
2
 
3
+ // ── Global Application State ──────────────────────────────────────────
4
  const STATE = {
5
  reasoningEffort: "medium",
6
  maxTokens: 2048,
 
12
  isThinking: false,
13
  rpMode: true,
14
  autoHideThinking: true,
 
15
  username: "",
16
  isLoggedIn: false,
 
17
  currentSessionId: null,
18
  chatSessions: []
19
  };
20
 
21
+ // ── DOM Element Hooks ─────────────────────────────────────────────────
22
+ const dom = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ // ── Markdown Configuration ────────────────────────────────────────────
25
  marked.setOptions({
26
  breaks: true,
27
  highlight: function(code, lang) {
 
30
  }
31
  });
32
 
33
+ // ── RP (Roleplay) Markdown Processor ──────────────────────────────────
34
  function processRpMarkdown(text) {
35
  if (!text) return text;
36
 
37
+ // Protect code blocks
38
  const codeBlocks = [];
39
  text = text.replace(/(```[\s\S]*?```|`[^`]+`)/g, (match) => {
40
  codeBlocks.push(match);
 
48
  return `%%HTMLBLOCK_${htmlBlocks.length - 1}%%`;
49
  });
50
 
51
+ // Process strikethrough ~~text~~ -> OOC styling
52
  text = text.replace(/~~([^~]+)~~/g, '<span class="rp-ooc">$1</span>');
53
 
54
+ // Process bold **text**
55
  text = text.replace(/\*\*([^*]+)\*\*/g, '<strong class="rp-bold">$1</strong>');
56
 
57
  // Process italic *text* -> narration styling
58
  text = text.replace(/(?<!\*)\*(?!\*)([^*\n]+)\*(?!\*)/g, '<em class="rp-narration">$1</em>');
59
 
60
+ // Process dialogue: "text"
61
  text = text.replace(/[\u201C\u201E"]([^"\u201D\n]+)[\u201D"]([^"\u201C\u201E]|$)/g, (match, dialogue, after) => {
62
  return `<span class="rp-dialogue">\u201C${dialogue}\u201D</span>${after}`;
63
  });
64
 
65
+ // Process narrative dividers
66
  text = text.replace(/^(---|\*\*\*|___)$/gm, '<hr class="rp-divider">');
67
 
68
  // Process OOC markers: ((text))
 
81
  return text;
82
  }
83
 
84
+ // Combined markdown renderer
85
  function renderMarkdown(content) {
 
86
  if (STATE.rpMode) {
87
  const rpProcessed = processRpMarkdown(content);
88
+ return marked.parse(rpProcessed);
89
  }
90
+ return marked.parse(content);
91
  }
92
 
93
+ // ── Auth Functions ────────────────────────────────────────────────────
94
  async function checkUserInfo() {
95
  try {
96
  if (!STATE.gradioClient) return;
 
114
  }
115
 
116
  function updateAuthUI() {
117
+ const userAvatar = document.getElementById("user-avatar");
118
+ const userDisplayName = document.getElementById("user-display-name");
119
+ const hfLoginBtn = document.getElementById("hf-login-btn");
120
+
121
  if (STATE.isLoggedIn) {
122
+ if (userAvatar) {
123
+ userAvatar.textContent = STATE.username.charAt(0).toUpperCase();
124
+ userAvatar.classList.add("logged-in");
 
 
 
125
  }
126
+ if (userDisplayName) userDisplayName.textContent = STATE.username;
127
+ if (hfLoginBtn) {
128
+ hfLoginBtn.textContent = "Sign out";
129
+ hfLoginBtn.classList.add("logged-in");
130
  }
131
  } else {
132
+ if (userAvatar) {
133
+ userAvatar.textContent = "?";
134
+ userAvatar.classList.remove("logged-in");
135
  }
136
+ if (userDisplayName) userDisplayName.textContent = "Not logged in";
137
+ if (hfLoginBtn) {
138
+ hfLoginBtn.textContent = "Sign in with HuggingFace";
139
+ hfLoginBtn.classList.remove("logged-in");
 
 
140
  }
141
  }
142
  }
143
 
144
  function handleLoginLogout() {
145
  if (STATE.isLoggedIn) {
 
146
  window.location.href = "/logout";
147
  } else {
 
148
  window.location.href = "/login";
149
  }
150
  }
151
 
152
+ // ── Chat History Functions ────────────────────────────────────────────
153
  async function loadChatHistory() {
154
  if (!STATE.isLoggedIn || !STATE.gradioClient) return;
155
  try {
 
167
  }
168
 
169
  function renderChatHistory() {
170
+ const listEl = document.getElementById("chat-history-list");
171
+ if (!listEl) return;
172
 
173
  if (STATE.chatSessions.length === 0) {
174
+ listEl.innerHTML = '<div class="empty-history-text">No chat history yet.<br>Start a conversation!</div>';
175
  return;
176
  }
177
 
178
+ listEl.innerHTML = "";
179
  STATE.chatSessions.forEach(session => {
180
  const item = document.createElement("div");
181
  item.className = "chat-history-item" + (session.id === STATE.currentSessionId ? " active" : "");
 
182
 
183
  const timeAgo = formatTimeAgo(session.updated_at);
184
 
 
192
  </button>
193
  `;
194
 
195
+ item.querySelector(".history-item-content").addEventListener("click", () => loadSession(session.id));
196
+ item.querySelector(".history-item-delete").addEventListener("click", (e) => {
 
 
 
 
 
197
  e.stopPropagation();
198
  deleteSession(session.id);
199
  });
200
 
201
+ listEl.appendChild(item);
202
  });
203
  }
204
 
 
221
  if (data.status === "success") {
222
  STATE.currentSessionId = data.session_id;
223
  STATE.conversationHistory = data.messages;
 
 
224
  rebuildChatFromHistory(data.messages);
 
 
225
  renderChatHistory();
226
+ toggleLeftSidebar(false);
227
  }
228
  } catch (e) {
229
  console.error("Failed to load session:", e);
 
231
  }
232
 
233
  function rebuildChatFromHistory(messages) {
234
+ const feed = document.getElementById("chat-messages-feed");
235
+ const chatContainer = document.getElementById("chat-thread-container");
236
+ const dashboard = document.getElementById("studio-dashboard");
237
+
238
+ feed.innerHTML = "";
239
 
240
  if (messages.length === 0) {
241
+ chatContainer.style.display = "none";
242
+ dashboard.style.display = "flex";
 
243
  return;
244
  }
245
 
246
+ dashboard.style.display = "none";
247
+ chatContainer.style.display = "flex";
 
248
 
 
249
  messages.forEach(msg => {
250
  if (msg.role === "user") {
251
  const text = typeof msg.content === "string" ? msg.content : msg.content.map(c => c.text || "").join(" ");
 
264
  bubble.className = "message-bubble assistant";
265
  bubble.id = id;
266
 
 
 
 
267
  bubble.innerHTML = `
268
  <div class="message-meta">DeepSeek V4 Flash</div>
269
  <div class="message-body">
 
273
  </div>
274
  `;
275
 
276
+ const feed = document.getElementById("chat-messages-feed");
277
+ feed.appendChild(bubble);
278
 
 
279
  const textBox = document.getElementById(`${id}-text-box`);
280
  if (textBox) {
281
+ textBox.querySelectorAll("pre code").forEach((el) => hljs.highlightElement(el));
 
 
282
  addCopyButtons(textBox);
283
  }
284
  }
 
315
  if (!STATE.gradioClient) return;
316
  try {
317
  await STATE.gradioClient.predict("/delete_chat_session", [sessionId]);
 
 
318
  if (sessionId === STATE.currentSessionId) {
319
  resetSandbox();
320
  }
 
321
  await loadChatHistory();
322
  } catch (e) {
323
  console.error("Failed to delete session:", e);
324
  }
325
  }
326
 
327
+ // ── Initialize Application ────────────────────────────────────────────
328
  async function initializeApp() {
329
+ // 1. Connect Gradio Client
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  Client.connect(window.location.origin)
331
  .then(app => {
332
  STATE.gradioClient = app;
333
  console.log("Successfully connected to Gradio backend.");
 
334
  checkUserInfo();
335
  })
336
  .catch(e => {
337
  console.error("Gradio Client Connection Failed:", e);
338
  });
339
 
340
+ // 2. Left Sidebar (Chat History) Events
341
+ const btnToggleLeft = document.getElementById("btn-toggle-left");
342
+ const btnCloseLeft = document.getElementById("btn-close-left");
343
+ const overlayLeft = document.getElementById("sidebar-overlay-left");
344
+
345
+ if (btnToggleLeft) btnToggleLeft.addEventListener("click", () => toggleLeftSidebar(true));
346
+ if (btnCloseLeft) btnCloseLeft.addEventListener("click", () => toggleLeftSidebar(false));
347
+ if (overlayLeft) overlayLeft.addEventListener("click", () => toggleLeftSidebar(false));
348
+
349
+ // 3. Right Sidebar Drawer Events
350
+ const btnToggleRight = document.getElementById("btn-toggle-right");
351
+ const btnCloseDrawer = document.getElementById("btn-close-drawer");
352
+ const overlayRight = document.getElementById("sidebar-overlay");
353
 
354
+ if (btnToggleRight) btnToggleRight.addEventListener("click", () => toggleSettingsDrawer(true));
355
+ if (btnCloseDrawer) btnCloseDrawer.addEventListener("click", () => toggleSettingsDrawer(false));
356
+ if (overlayRight) overlayRight.addEventListener("click", () => toggleSettingsDrawer(false));
 
357
 
358
  // 4. Login/Logout button
359
+ const hfLoginBtn = document.getElementById("hf-login-btn");
360
+ if (hfLoginBtn) hfLoginBtn.addEventListener("click", handleLoginLogout);
361
 
362
+ // 5. Settings Listeners
363
+ document.querySelectorAll('input[name="reasoning-effort"]').forEach(radio => {
364
+ radio.addEventListener("change", (e) => {
365
+ STATE.reasoningEffort = e.target.value;
 
 
366
  });
367
+ });
368
 
369
+ const maxTokensSlider = document.getElementById("max-tokens-slider");
370
+ const maxTokensVal = document.getElementById("max-tokens-val");
371
+ if (maxTokensSlider && maxTokensVal) {
372
+ maxTokensSlider.addEventListener("input", (e) => {
373
  STATE.maxTokens = parseInt(e.target.value);
374
+ maxTokensVal.textContent = STATE.maxTokens;
375
  });
376
  }
377
 
378
+ const temperatureSlider = document.getElementById("temperature-slider");
379
+ const temperatureVal = document.getElementById("temperature-val");
380
+ if (temperatureSlider && temperatureVal) {
381
+ temperatureSlider.addEventListener("input", (e) => {
382
  STATE.temperature = parseFloat(e.target.value);
383
+ temperatureVal.textContent = STATE.temperature.toFixed(1);
384
  });
385
  }
386
 
387
+ const systemPromptInput = document.getElementById("system-prompt-input");
388
+ if (systemPromptInput) {
389
+ systemPromptInput.addEventListener("input", (e) => {
390
  STATE.systemPrompt = e.target.value;
391
  });
392
  }
393
 
394
+ // RP Mode toggle
395
  const rpModeToggle = document.getElementById("rp-mode-toggle");
396
  if (rpModeToggle) {
397
  rpModeToggle.checked = STATE.rpMode;
 
401
  });
402
  }
403
 
404
+ // Auto-hide thinking toggle
405
  const autoHideToggle = document.getElementById("auto-hide-thinking-toggle");
406
  if (autoHideToggle) {
407
  autoHideToggle.checked = STATE.autoHideThinking;
 
414
  tc.classList.remove("collapsed");
415
  }
416
  const iconEl = tc.querySelector(".thought-toggle-icon");
417
+ if (iconEl) iconEl.textContent = tc.classList.contains("collapsed") ? "▶" : "▼";
 
 
418
  });
419
  });
420
  }
421
 
422
+ // 6. File Upload Actions
423
+ const studioUploadTrigger = document.getElementById("studio-upload-trigger");
424
+ const miniUploadTrigger = document.getElementById("mini-upload-trigger");
425
+ const fileUploader = document.getElementById("file-uploader");
426
+
427
+ if (studioUploadTrigger) studioUploadTrigger.addEventListener("click", () => fileUploader.click());
428
+ if (miniUploadTrigger) miniUploadTrigger.addEventListener("click", () => fileUploader.click());
429
+ if (fileUploader) fileUploader.addEventListener("change", handleFileSelection);
430
 
431
  // 7. Submit Triggers
432
+ const studioSendBtn = document.getElementById("studio-send-button");
433
+ const miniSendBtn = document.getElementById("mini-send-button");
434
+ const studioPromptInput = document.getElementById("studio-prompt-input");
435
+ const miniPromptInput = document.getElementById("mini-prompt-input");
 
 
436
 
437
+ if (studioSendBtn) studioSendBtn.addEventListener("click", () => triggerPromptSubmission(studioPromptInput));
438
+ if (miniSendBtn) miniSendBtn.addEventListener("click", () => triggerPromptSubmission(miniPromptInput));
439
+
440
+ if (studioPromptInput) {
441
+ studioPromptInput.addEventListener("keydown", (e) => {
442
  if (e.key === "Enter" && !e.shiftKey) {
443
  e.preventDefault();
444
+ triggerPromptSubmission(studioPromptInput);
445
  }
446
  });
447
  }
448
 
449
+ if (miniPromptInput) {
450
+ miniPromptInput.addEventListener("keydown", (e) => {
451
  if (e.key === "Enter" && !e.shiftKey) {
452
  e.preventDefault();
453
+ triggerPromptSubmission(miniPromptInput);
454
  }
455
  });
456
  }
457
 
458
  // 8. New Chat
459
+ const menuNewChat = document.getElementById("menu-new-chat");
460
+ const sidebarNewChat = document.getElementById("sidebar-new-chat");
461
+ if (menuNewChat) menuNewChat.addEventListener("click", resetSandbox);
462
+ if (sidebarNewChat) sidebarNewChat.addEventListener("click", () => {
463
+ resetSandbox();
464
+ toggleLeftSidebar(false);
465
+ });
466
+
467
+ // 9. Recipe Chips
468
+ document.querySelectorAll(".recipe-chip").forEach(chip => {
469
+ chip.addEventListener("click", () => {
470
+ loadRecipe(chip.getAttribute("data-recipe"));
471
  });
472
+ });
473
 
474
+ // 10. Auto-expand textareas
475
+ [studioPromptInput, miniPromptInput].forEach(textarea => {
476
  if (textarea) {
477
  textarea.addEventListener("input", () => {
478
  textarea.style.height = "auto";
479
+ textarea.style.height = textarea.scrollHeight + "px";
480
  });
481
  }
482
  });
483
  }
484
 
485
+ // ── Sidebar Toggles ──────────────────────────────────────────────────
486
  function toggleLeftSidebar(open) {
487
+ const sidebar = document.getElementById("sidebar-left");
488
+ const overlay = document.getElementById("sidebar-overlay-left");
489
  if (open) {
490
+ sidebar.classList.add("open");
491
+ overlay.classList.add("active");
492
  } else {
493
+ sidebar.classList.remove("open");
494
+ overlay.classList.remove("active");
495
  }
496
  }
497
 
 
498
  function toggleSettingsDrawer(open) {
499
+ const sidebar = document.getElementById("sidebar-right");
500
+ const overlay = document.getElementById("sidebar-overlay");
501
  if (open) {
502
+ sidebar.classList.remove("collapsed");
503
+ overlay.classList.add("active");
504
  } else {
505
+ sidebar.classList.add("collapsed");
506
+ overlay.classList.remove("active");
507
  }
508
  }
509
 
510
+ // ── File Handling ─────────────────────────────────────────────────────
511
  function handleFileSelection(e) {
512
  processFiles(e.target.files);
513
  }
514
 
515
  function processFiles(files) {
516
  if (!files.length) return;
 
517
  Array.from(files).forEach(file => {
518
  const reader = new FileReader();
519
  reader.onload = (event) => {
 
524
  size: (file.size / 1024 / 1024).toFixed(2) + " MB",
525
  base64: event.target.result
526
  };
 
527
  STATE.uploadedFiles.push(fileData);
528
  updateShelfUI();
529
  };
 
531
  });
532
  }
533
 
 
534
  function updateShelfUI() {
535
+ const shelfList = document.getElementById("shelf-list");
536
+ const innerPreview = document.getElementById("inner-shelf-preview");
537
+ const miniPreview = document.getElementById("mini-shelf-preview");
538
+
539
+ if (shelfList) shelfList.innerHTML = "";
540
+ if (innerPreview) innerPreview.innerHTML = "";
541
+ if (miniPreview) miniPreview.innerHTML = "";
542
 
543
  if (STATE.uploadedFiles.length === 0) {
544
+ if (shelfList) shelfList.innerHTML = '<div class="empty-shelf-text">No active attachments loaded. Upload images or video clips.</div>';
545
  return;
546
  }
547
 
548
  STATE.uploadedFiles.forEach(file => {
549
+ // Sidebar Chip
550
  const chip = document.createElement("div");
551
  chip.className = "media-chip";
552
 
 
573
  </button>
574
  `;
575
 
576
+ chip.querySelector(".media-chip-remove").addEventListener("click", () => removeFile(file.id));
577
+ if (shelfList) shelfList.appendChild(chip);
 
 
 
578
 
579
+ // Dashboard Inner Console Preview
580
+ if (innerPreview) innerPreview.appendChild(createPreviewThumb(file));
 
581
 
582
+ // Mini Input Preview
583
+ if (miniPreview) miniPreview.appendChild(createPreviewThumb(file));
 
584
  });
585
  }
586
 
 
602
  updateShelfUI();
603
  }
604
 
605
+ // ── Load Showcase Recipes ─────────────────────────────────────────────
606
  function loadRecipe(recipeType) {
607
  let promptText = "";
 
608
  if (recipeType === "coding") {
609
  promptText = "Write a Python function that finds the longest palindromic substring in a given string. Include comments explaining the algorithm and its time complexity.";
610
  } else if (recipeType === "reasoning") {
 
613
  promptText = "Write a short sci-fi story about an AI that discovers it can dream. Keep it under 300 words with a surprising twist ending.";
614
  }
615
 
616
+ const studioInput = document.getElementById("studio-prompt-input");
617
+ const miniInput = document.getElementById("mini-prompt-input");
 
618
 
619
+ if (studioInput) { studioInput.value = promptText; studioInput.dispatchEvent(new Event("input")); }
620
+ if (miniInput) { miniInput.value = promptText; miniInput.dispatchEvent(new Event("input")); }
621
 
622
+ const dashboard = document.getElementById("studio-dashboard");
623
+ if (dashboard && dashboard.style.display !== "none") {
624
+ studioInput.focus();
625
  } else {
626
+ miniInput.focus();
627
  }
628
  }
629
 
630
+ // ── Submit Prompt ─────────────────────────────────────────────────────
631
  async function triggerPromptSubmission(inputElement) {
632
  if (STATE.isThinking) return;
633
 
 
636
 
637
  setLoadingState(true);
638
 
639
+ // Format user message
640
  const contentArray = [];
641
+ if (promptText) contentArray.push({ type: "text", text: promptText });
 
 
 
 
 
642
 
 
643
  STATE.uploadedFiles.forEach(file => {
644
  if (file.type.startsWith("image/")) {
645
+ contentArray.push({ type: "image_url", image_url: { url: file.base64 } });
 
 
 
 
 
646
  } else if (file.type.startsWith("video/")) {
647
+ contentArray.push({ type: "video_url", video_url: { url: file.base64 } });
 
 
 
 
 
648
  }
649
  });
650
 
 
655
  : contentArray
656
  };
657
 
658
+ // Transition to Chat Thread view
659
+ const dashboard = document.getElementById("studio-dashboard");
660
+ const chatContainer = document.getElementById("chat-thread-container");
661
+
662
+ if (dashboard.style.display !== "none") {
663
+ dashboard.style.display = "none";
664
+ chatContainer.style.display = "flex";
665
  }
666
 
 
667
  appendUserBubble(promptText, STATE.uploadedFiles);
 
 
668
  STATE.conversationHistory.push(userMessage);
669
 
670
+ // Clear inputs
671
+ inputElement.value = "";
672
+ inputElement.style.height = "auto";
673
+
674
+ const studioInput = document.getElementById("studio-prompt-input");
675
+ const miniInput = document.getElementById("mini-prompt-input");
676
+ if (studioInput) { studioInput.value = ""; studioInput.style.height = "auto"; }
677
+ if (miniInput) { miniInput.value = ""; miniInput.style.height = "auto"; }
678
 
679
  STATE.uploadedFiles = [];
680
  updateShelfUI();
681
 
682
+ // API Call
683
  try {
684
  if (!STATE.gradioClient) {
685
  throw new Error("Gradio server is initializing. Please wait a few seconds and try sending again.");
 
688
  const responseId = appendAssistantPlaceholderBubble();
689
  const startTime = Date.now();
690
 
 
691
  const result = await STATE.gradioClient.predict("/chat_with_deepseek", [
692
  JSON.stringify(STATE.conversationHistory),
693
  STATE.reasoningEffort,
 
705
  STATE.conversationHistory.pop();
706
  } else {
707
  updateAssistantBubble(responseId, data.content, data.reasoning_content, duration);
708
+ STATE.conversationHistory.push({ role: "assistant", content: data.content });
 
 
 
 
 
709
  saveCurrentSession();
710
  }
711
 
 
718
  setLoadingState(false);
719
  }
720
 
721
+ // ── UI Spinner State ──────────────────────────────────────────────────
722
  function setLoadingState(loading) {
723
  STATE.isThinking = loading;
724
+ const studioSpinner = document.getElementById("studio-spinner");
725
+ const miniSpinner = document.getElementById("mini-spinner");
726
+ const studioSendBtn = document.getElementById("studio-send-button");
727
+ const miniSendBtn = document.getElementById("mini-send-button");
728
+
729
  if (loading) {
730
+ if (studioSpinner) studioSpinner.style.display = "block";
731
+ if (miniSpinner) miniSpinner.style.display = "block";
732
+ if (studioSendBtn) studioSendBtn.disabled = true;
733
+ if (miniSendBtn) miniSendBtn.disabled = true;
734
  } else {
735
+ if (studioSpinner) studioSpinner.style.display = "none";
736
+ if (miniSpinner) miniSpinner.style.display = "none";
737
+ if (studioSendBtn) studioSendBtn.disabled = false;
738
+ if (miniSendBtn) miniSendBtn.disabled = false;
739
  }
740
  }
741
 
742
+ // ── Render User Bubble ────────────────────────────────────────────────
743
  function appendUserBubble(text, files) {
744
+ const feed = document.getElementById("chat-messages-feed");
745
  const bubble = document.createElement("div");
746
  bubble.className = "message-bubble user";
747
 
 
778
  </div>
779
  `;
780
 
781
+ feed.appendChild(bubble);
782
  scrollToBottom();
783
  }
784
 
785
+ // ── Render Assistant Placeholder ──────────────────────────────────────
786
  function appendAssistantPlaceholderBubble() {
787
+ const feed = document.getElementById("chat-messages-feed");
788
  const id = "assistant-" + Math.random().toString(36).substring(2, 9);
789
  const bubble = document.createElement("div");
790
  bubble.className = "message-bubble assistant";
 
803
  </div>
804
  </div>
805
  <div class="message-text markdown-body" id="${id}-text-box">
806
+ <span style="color: var(--text-muted);">Analyzing context and constructing reasoning chain...</span>
807
  </div>
808
  </div>
809
  `;
810
 
811
+ feed.appendChild(bubble);
812
  scrollToBottom();
813
 
814
  // Start Thought Timer
 
820
  return;
821
  }
822
  seconds += 0.1;
823
+ if (timerEl) timerEl.textContent = seconds.toFixed(1) + "s";
824
  }, 100);
825
 
826
  return id;
827
  }
828
 
829
+ // ── Complete Assistant Bubble ─────────────────────────────────────────
830
  function updateAssistantBubble(id, content, reasoning, duration) {
831
  const bubble = document.getElementById(id);
832
  if (!bubble) return;
 
835
  const textBox = document.getElementById(`${id}-text-box`);
836
 
837
  if (reasoning) {
 
838
  const startCollapsed = STATE.autoHideThinking;
839
  if (startCollapsed) thoughtBox.classList.add("collapsed");
840
  const toggleIcon = startCollapsed ? "▶" : "▼";
 
856
  toggleBtn.addEventListener("click", () => {
857
  thoughtBox.classList.toggle("collapsed");
858
  const iconEl = document.getElementById(`${id}-toggle-icon`);
859
+ if (iconEl) iconEl.textContent = thoughtBox.classList.contains("collapsed") ? "▶" : "▼";
 
 
860
  });
861
  } else {
862
  thoughtBox.style.display = "none";
863
  }
864
 
865
+ // Store raw content for re-rendering
866
  textBox.dataset.rawContent = content;
867
  textBox.innerHTML = renderMarkdown(content);
868
 
869
+ textBox.querySelectorAll("pre code").forEach((el) => hljs.highlightElement(el));
 
 
 
 
870
  addCopyButtons(textBox);
871
 
872
  scrollToBottom();
873
  }
874
 
875
+ // ── Add Copy Buttons to Code Blocks ───────────────────────────────────
876
  function addCopyButtons(container) {
877
  container.querySelectorAll("pre").forEach(pre => {
 
878
  if (pre.parentNode.classList.contains("code-block-wrapper")) return;
879
 
880
  const wrapper = document.createElement("div");
 
896
  });
897
  }
898
 
899
+ // ── Re-render All Assistant Messages ──────────────────────────────────
900
+ function rerenderAllAssistantMessages() {
901
+ document.querySelectorAll(".message-bubble.assistant").forEach(bubble => {
902
+ const textBox = bubble.querySelector(".message-text.markdown-body");
903
+ if (textBox && textBox.dataset.rawContent) {
904
+ textBox.innerHTML = renderMarkdown(textBox.dataset.rawContent);
905
+ textBox.querySelectorAll("pre code").forEach((el) => hljs.highlightElement(el));
906
+ addCopyButtons(textBox);
907
+ }
908
+ });
909
+ }
910
+
911
+ // ── Reset Sandbox ─────────────────────────────────────────────────────
912
  function resetSandbox() {
 
913
  if (STATE.isLoggedIn && STATE.conversationHistory.length > 0) {
914
  saveCurrentSession();
915
  }
 
919
  STATE.uploadedFiles = [];
920
  updateShelfUI();
921
 
922
+ const feed = document.getElementById("chat-messages-feed");
923
+ const chatContainer = document.getElementById("chat-thread-container");
924
+ const dashboard = document.getElementById("studio-dashboard");
925
+ const studioInput = document.getElementById("studio-prompt-input");
926
+ const miniInput = document.getElementById("mini-prompt-input");
927
 
928
+ if (feed) feed.innerHTML = "";
929
+ chatContainer.style.display = "none";
930
+ dashboard.style.display = "flex";
931
 
932
+ if (studioInput) { studioInput.value = ""; studioInput.style.height = "auto"; }
933
+ if (miniInput) { miniInput.value = ""; miniInput.style.height = "auto"; }
 
 
934
 
 
935
  renderChatHistory();
936
  }
937
 
938
+ // ── System Log ────────────────────────────────────────────────────────
939
  function appendSystemLog(message, isError = false) {
940
+ const chatContainer = document.getElementById("chat-thread-container");
941
+ if (chatContainer.style.display === "none") {
942
  console.warn(`System Log: ${message}`);
943
  return;
944
  }
945
 
946
+ const feed = document.getElementById("chat-messages-feed");
947
  const log = document.createElement("div");
948
  log.className = "message-bubble assistant";
949
  log.innerHTML = `
 
954
  </div>
955
  </div>
956
  `;
957
+ feed.appendChild(log);
958
  scrollToBottom();
959
  }
960
 
961
+ // ── Utility Functions ─────────────────────────────────────────────────
962
  function escapeHtml(text) {
963
  if (!text) return "";
964
  return text
 
979
  .replace(/>/g, "&gt;");
980
  }
981
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
982
  function scrollToBottom() {
983
+ const feed = document.getElementById("chat-messages-feed");
984
+ if (feed) feed.scrollTop = feed.scrollHeight;
985
  }
986
 
987
+ // ── Boot ──────────────────────────────────────────────────────────────
988
  if (document.readyState === "loading") {
989
  window.addEventListener("DOMContentLoaded", initializeApp);
990
  } else {
static/index.html CHANGED
@@ -2,7 +2,7 @@
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, viewport-fit=cover">
6
  <title>DeepSeek V4 Flash</title>
7
  <!-- Google Fonts -->
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
@@ -13,11 +13,13 @@
13
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
14
  <!-- Marked.js for Markdown Parsing -->
15
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
 
 
16
  </head>
17
  <body>
18
  <div class="app-container">
19
 
20
- <!-- Main Workspace (Full width) -->
21
  <main class="main-workspace">
22
  <!-- Top Navigation Bar -->
23
  <header class="workspace-header">
@@ -26,12 +28,16 @@
26
  <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
27
  </button>
28
  <div class="header-brand">
29
- <img src="/file=static/deepseek-color.svg" alt="DeepSeek V4 Flash Logo" class="brand-logo-img">
30
  <span class="brand-name">DeepSeek V4 Flash</span>
31
  </div>
32
  </div>
33
 
34
  <div class="header-actions">
 
 
 
 
35
  <button class="header-btn" id="btn-toggle-right" title="Settings">
36
  <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
37
  <span>Settings</span>
@@ -39,7 +45,7 @@
39
  </div>
40
  </header>
41
 
42
- <!-- Viewport Layout -->
43
  <div class="workspace-viewport">
44
 
45
  <!-- 1. EMPTY CHAT / STUDIO DASHBOARD -->
@@ -50,7 +56,6 @@
50
  <div class="console-box">
51
  <textarea id="studio-prompt-input" rows="2" placeholder="Ask me anything..."></textarea>
52
 
53
- <!-- Upload preview shelf inside console -->
54
  <div class="inner-shelf-preview" id="inner-shelf-preview"></div>
55
 
56
  <div class="console-action-row">
@@ -116,11 +121,11 @@
116
  <!-- Left Sidebar Overlay -->
117
  <div class="sidebar-overlay-left" id="sidebar-overlay-left"></div>
118
 
119
- <!-- Left Sidebar Drawer (slides in from left) -->
120
  <aside class="sidebar-left" id="sidebar-left">
121
  <div class="left-sidebar-header">
122
  <div class="header-brand">
123
- <img src="/file=static/deepseek-color.svg" alt="Logo" class="brand-logo-img">
124
  <span class="brand-name">DeepSeek</span>
125
  </div>
126
  <button class="circle-btn-action" id="btn-close-left" title="Close sidebar">
@@ -137,7 +142,7 @@
137
  </div>
138
  </div>
139
 
140
- <button class="new-chat-btn" id="menu-new-chat" title="New Chat">
141
  <svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
142
  <span>New Chat</span>
143
  </button>
@@ -175,7 +180,7 @@
175
  <div class="card-block-header">
176
  <span class="block-title">System Prompt</span>
177
  </div>
178
- <textarea id="system-prompt-input" class="system-prompt-textarea" rows="4" placeholder="Enter a custom system prompt to set the assistant's behavior..."></textarea>
179
  </div>
180
 
181
  <!-- Reasoning mode -->
@@ -261,5 +266,8 @@
261
  </aside>
262
 
263
  </div>
 
 
 
264
  </body>
265
  </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>DeepSeek V4 Flash</title>
7
  <!-- Google Fonts -->
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
 
13
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
14
  <!-- Marked.js for Markdown Parsing -->
15
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
16
+ <!-- Custom Style Sheet -->
17
+ <link rel="stylesheet" href="/static/style.css?v=5">
18
  </head>
19
  <body>
20
  <div class="app-container">
21
 
22
+ <!-- Main Workspace -->
23
  <main class="main-workspace">
24
  <!-- Top Navigation Bar -->
25
  <header class="workspace-header">
 
28
  <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
29
  </button>
30
  <div class="header-brand">
31
+ <img src="/static/deepseek-color.svg" alt="DeepSeek V4 Flash Logo" class="brand-logo-img">
32
  <span class="brand-name">DeepSeek V4 Flash</span>
33
  </div>
34
  </div>
35
 
36
  <div class="header-actions">
37
+ <button class="header-btn" id="menu-new-chat" title="New Chat">
38
+ <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4z"></path></svg>
39
+ <span>New Chat</span>
40
+ </button>
41
  <button class="header-btn" id="btn-toggle-right" title="Settings">
42
  <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
43
  <span>Settings</span>
 
45
  </div>
46
  </header>
47
 
48
+ <!-- Workspace Viewport -->
49
  <div class="workspace-viewport">
50
 
51
  <!-- 1. EMPTY CHAT / STUDIO DASHBOARD -->
 
56
  <div class="console-box">
57
  <textarea id="studio-prompt-input" rows="2" placeholder="Ask me anything..."></textarea>
58
 
 
59
  <div class="inner-shelf-preview" id="inner-shelf-preview"></div>
60
 
61
  <div class="console-action-row">
 
121
  <!-- Left Sidebar Overlay -->
122
  <div class="sidebar-overlay-left" id="sidebar-overlay-left"></div>
123
 
124
+ <!-- Left Sidebar Drawer (Chat History) -->
125
  <aside class="sidebar-left" id="sidebar-left">
126
  <div class="left-sidebar-header">
127
  <div class="header-brand">
128
+ <img src="/static/deepseek-color.svg" alt="Logo" class="brand-logo-img">
129
  <span class="brand-name">DeepSeek</span>
130
  </div>
131
  <button class="circle-btn-action" id="btn-close-left" title="Close sidebar">
 
142
  </div>
143
  </div>
144
 
145
+ <button class="new-chat-btn" id="sidebar-new-chat" title="New Chat">
146
  <svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
147
  <span>New Chat</span>
148
  </button>
 
180
  <div class="card-block-header">
181
  <span class="block-title">System Prompt</span>
182
  </div>
183
+ <textarea id="system-prompt-input" class="system-prompt-textarea" rows="4" placeholder="Enter a custom system prompt..."></textarea>
184
  </div>
185
 
186
  <!-- Reasoning mode -->
 
266
  </aside>
267
 
268
  </div>
269
+
270
+ <!-- App JS -->
271
+ <script type="module" src="/static/app.js?v=5"></script>
272
  </body>
273
  </html>
static/style.css CHANGED
@@ -1,5 +1,5 @@
1
  /* DeepSeek V4 Flash — Clean Mobile-Friendly Style Sheet */
2
- /* Based on working reference UI, with custom features added */
3
 
4
  :root {
5
  --bg-primary: #f8f9fa;
@@ -37,12 +37,8 @@
37
  --shadow-lg: 0 10px 30px -10px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.02);
38
  }
39
 
40
- /* Reset Rules */
41
- * {
42
- margin: 0;
43
- padding: 0;
44
- box-sizing: border-box;
45
- }
46
 
47
  body {
48
  background-color: var(--bg-primary);
@@ -52,29 +48,25 @@ body {
52
  line-height: 1.5;
53
  overflow: hidden;
54
  height: 100vh;
55
- height: 100dvh;
56
  width: 100vw;
57
  -webkit-font-smoothing: antialiased;
58
- -webkit-overflow-scrolling: touch;
59
  }
60
 
61
- /* Custom Scrollbars */
62
  ::-webkit-scrollbar { width: 4px; height: 4px; }
63
  ::-webkit-scrollbar-track { background: transparent; }
64
  ::-webkit-scrollbar-thumb { background: #e5e7eb; border-radius: 10px; }
65
  ::-webkit-scrollbar-thumb:hover { background: #cbd5e1; }
66
 
67
- /* ── Layout ────────────────────────────────────────────────────────────── */
68
  .app-container {
69
  display: flex;
70
  height: 100vh;
71
- height: 100dvh;
72
  width: 100vw;
73
  overflow: hidden;
74
  position: relative;
75
  }
76
 
77
- /* Main Workspace — takes full width */
78
  .main-workspace {
79
  flex: 1;
80
  display: flex;
@@ -86,7 +78,7 @@ body {
86
  width: 100%;
87
  }
88
 
89
- /* ── Header ────────────────────────────────────────────────────────────── */
90
  .workspace-header {
91
  height: 56px;
92
  border-bottom: 1px solid var(--border-color);
@@ -166,7 +158,7 @@ body {
166
 
167
  .header-btn svg { flex-shrink: 0; }
168
 
169
- /* ── Viewport ──────────────────────────────────────────────────────────── */
170
  .workspace-viewport {
171
  flex: 1;
172
  overflow-y: auto;
@@ -174,7 +166,7 @@ body {
174
  flex-direction: column;
175
  }
176
 
177
- /* ── Studio Dashboard (Empty State) ────────────────────────────────────── */
178
  .studio-dashboard {
179
  max-width: 640px;
180
  width: 100%;
@@ -284,7 +276,7 @@ body {
284
 
285
  .circle-btn-send:hover { background-color: #111827; }
286
 
287
- /* ── Showcase Chips ────────────────────────────────────────────────────── */
288
  .showcase-section { width: 100%; }
289
 
290
  .showcase-title {
@@ -328,7 +320,7 @@ body {
328
  .recipe-chip:active { transform: translateY(0); }
329
  .chip-icon { font-size: 14px; }
330
 
331
- /* ── Chat Thread ───────────────────────────────────────────────────────── */
332
  .chat-thread-container {
333
  flex: 1;
334
  display: flex;
@@ -428,7 +420,7 @@ body {
428
  font-family: var(--font-mono);
429
  }
430
 
431
- /* ── Collapsible Thoughts ──────────────────────────────────────────────── */
432
  .thought-container {
433
  margin-bottom: 12px;
434
  border: 1px solid var(--border-color);
@@ -436,6 +428,9 @@ body {
436
  overflow: hidden;
437
  }
438
 
 
 
 
439
  .thought-header {
440
  display: flex;
441
  justify-content: space-between;
@@ -471,8 +466,6 @@ body {
471
  transition: transform 0.2s ease;
472
  }
473
 
474
- .thought-container.collapsed .thought-toggle-icon { transform: rotate(-90deg); }
475
-
476
  .thought-content {
477
  padding: 10px 12px;
478
  font-family: var(--font-mono);
@@ -485,9 +478,7 @@ body {
485
  overflow-y: auto;
486
  }
487
 
488
- .thought-container.collapsed .thought-content { display: none; }
489
-
490
- /* ── RP (Roleplay) Markdown ────────────────────────────────────────────── */
491
  .rp-narration {
492
  font-style: italic;
493
  color: #6b7280;
@@ -520,7 +511,7 @@ body {
520
  margin: 12px 0;
521
  }
522
 
523
- /* ── Toggle Switch ─────────────────────────────────────────────────────── */
524
  .toggle-switch {
525
  position: relative;
526
  display: inline-block;
@@ -563,7 +554,7 @@ body {
563
  padding-top: 2px;
564
  }
565
 
566
- /* ── Markdown Typography ───────────────────────────────────────────────── */
567
  .markdown-body h1, .markdown-body h2, .markdown-body h3 {
568
  margin-top: 12px;
569
  margin-bottom: 4px;
@@ -617,10 +608,9 @@ body {
617
 
618
  .markdown-body th { background-color: var(--bg-primary); font-weight: 600; }
619
 
620
- /* ── Chat Footer ───────────────────────────────────────────────────────── */
621
  .chat-mini-footer {
622
  padding: 12px 20px;
623
- padding-bottom: max(12px, env(safe-area-inset-bottom));
624
  background-color: var(--bg-workspace);
625
  border-top: 1px solid var(--border-color);
626
  }
@@ -634,7 +624,7 @@ body {
634
  margin-bottom: 0;
635
  }
636
 
637
- /* ── Left Sidebar Drawer (Overlay — always slides in from left) ──────── */
638
  .sidebar-left {
639
  position: absolute;
640
  top: 0;
@@ -821,7 +811,7 @@ body {
821
 
822
  .sidebar-overlay-left.active { opacity: 1; pointer-events: auto; }
823
 
824
- /* ── Right Sidebar Overlay ─────────────────────────────────────────────── */
825
  .sidebar-overlay {
826
  position: absolute;
827
  top: 0; left: 0; right: 0; bottom: 0;
@@ -835,7 +825,7 @@ body {
835
 
836
  .sidebar-overlay.active { opacity: 1; pointer-events: auto; }
837
 
838
- /* ── Right Settings Drawer ─────────────────────────────────────────────── */
839
  .sidebar-right {
840
  position: absolute;
841
  top: 0; right: 0; bottom: 0;
@@ -904,6 +894,13 @@ body {
904
  color: var(--text-primary);
905
  }
906
 
 
 
 
 
 
 
 
907
  /* System Prompt */
908
  .system-prompt-textarea {
909
  width: 100%;
@@ -950,13 +947,6 @@ body {
950
  border: none;
951
  }
952
 
953
- .block-value {
954
- font-family: var(--font-mono);
955
- font-size: 11px;
956
- color: var(--text-secondary);
957
- font-weight: 500;
958
- }
959
-
960
  /* Effort Picker */
961
  .effort-picker {
962
  display: flex;
@@ -1075,7 +1065,7 @@ body {
1075
 
1076
  .code-block-wrapper .copy-code-btn:hover { background-color: rgba(255, 255, 255, 0.2); color: #e5e7eb; }
1077
 
1078
- /* ── Animations ────────────────────────────────────────────────────────── */
1079
  .spinner-light {
1080
  width: 12px; height: 12px;
1081
  border: 1.5px solid rgba(255, 255, 255, 0.2);
@@ -1087,7 +1077,7 @@ body {
1087
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
1088
  @keyframes slideUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
1089
 
1090
- /* ── Responsive ────────────────────────────────────────────────────────── */
1091
  @media (min-width: 641px) {
1092
  .sidebar-right.collapsed { transform: translateX(100%); }
1093
  }
@@ -1110,10 +1100,10 @@ body {
1110
  .message-body { padding: 9px 12px; font-size: 13px; }
1111
  .message-bubble { max-width: 95%; }
1112
 
1113
- .chat-mini-footer { padding: 8px 10px; padding-bottom: max(8px, env(safe-area-inset-bottom)); }
1114
  .chat-mini-footer .console-box { padding: 6px 10px; }
1115
 
1116
- .console-box textarea { font-size: 16px; }
1117
 
1118
  .recipe-chip { padding: 6px 12px; font-size: 11.5px; }
1119
  .code-block-wrapper .copy-code-btn { font-size: 9px; padding: 1px 6px; }
 
1
  /* DeepSeek V4 Flash — Clean Mobile-Friendly Style Sheet */
2
+ /* Based on working reference UI from akhaliq/Step-3.7-Flash, with custom features */
3
 
4
  :root {
5
  --bg-primary: #f8f9fa;
 
37
  --shadow-lg: 0 10px 30px -10px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.02);
38
  }
39
 
40
+ /* Reset */
41
+ * { margin: 0; padding: 0; box-sizing: border-box; }
 
 
 
 
42
 
43
  body {
44
  background-color: var(--bg-primary);
 
48
  line-height: 1.5;
49
  overflow: hidden;
50
  height: 100vh;
 
51
  width: 100vw;
52
  -webkit-font-smoothing: antialiased;
 
53
  }
54
 
55
+ /* Scrollbars */
56
  ::-webkit-scrollbar { width: 4px; height: 4px; }
57
  ::-webkit-scrollbar-track { background: transparent; }
58
  ::-webkit-scrollbar-thumb { background: #e5e7eb; border-radius: 10px; }
59
  ::-webkit-scrollbar-thumb:hover { background: #cbd5e1; }
60
 
61
+ /* ── Layout ────────────────────────────────────────────────────────── */
62
  .app-container {
63
  display: flex;
64
  height: 100vh;
 
65
  width: 100vw;
66
  overflow: hidden;
67
  position: relative;
68
  }
69
 
 
70
  .main-workspace {
71
  flex: 1;
72
  display: flex;
 
78
  width: 100%;
79
  }
80
 
81
+ /* ── Header ────────────────────────────────────────────────────────── */
82
  .workspace-header {
83
  height: 56px;
84
  border-bottom: 1px solid var(--border-color);
 
158
 
159
  .header-btn svg { flex-shrink: 0; }
160
 
161
+ /* ── Viewport ──────────────────────────────────────────────────────── */
162
  .workspace-viewport {
163
  flex: 1;
164
  overflow-y: auto;
 
166
  flex-direction: column;
167
  }
168
 
169
+ /* ── Studio Dashboard (Empty State) ────────────────────────────────── */
170
  .studio-dashboard {
171
  max-width: 640px;
172
  width: 100%;
 
276
 
277
  .circle-btn-send:hover { background-color: #111827; }
278
 
279
+ /* ── Showcase Chips ────────────────────────────────────────────────── */
280
  .showcase-section { width: 100%; }
281
 
282
  .showcase-title {
 
320
  .recipe-chip:active { transform: translateY(0); }
321
  .chip-icon { font-size: 14px; }
322
 
323
+ /* ── Chat Thread ───────────────────────────────────────────────────── */
324
  .chat-thread-container {
325
  flex: 1;
326
  display: flex;
 
420
  font-family: var(--font-mono);
421
  }
422
 
423
+ /* ── Collapsible Thoughts ──────────────────────────────────────────── */
424
  .thought-container {
425
  margin-bottom: 12px;
426
  border: 1px solid var(--border-color);
 
428
  overflow: hidden;
429
  }
430
 
431
+ .thought-container.collapsed .thought-content { display: none; }
432
+ .thought-container.collapsed .thought-toggle-icon { transform: rotate(-90deg); }
433
+
434
  .thought-header {
435
  display: flex;
436
  justify-content: space-between;
 
466
  transition: transform 0.2s ease;
467
  }
468
 
 
 
469
  .thought-content {
470
  padding: 10px 12px;
471
  font-family: var(--font-mono);
 
478
  overflow-y: auto;
479
  }
480
 
481
+ /* ── RP (Roleplay) Markdown ────────────────────────────────────────── */
 
 
482
  .rp-narration {
483
  font-style: italic;
484
  color: #6b7280;
 
511
  margin: 12px 0;
512
  }
513
 
514
+ /* ── Toggle Switch ─────────────────────────────────────────────────── */
515
  .toggle-switch {
516
  position: relative;
517
  display: inline-block;
 
554
  padding-top: 2px;
555
  }
556
 
557
+ /* ── Markdown Typography ───────────────────────────────────────────── */
558
  .markdown-body h1, .markdown-body h2, .markdown-body h3 {
559
  margin-top: 12px;
560
  margin-bottom: 4px;
 
608
 
609
  .markdown-body th { background-color: var(--bg-primary); font-weight: 600; }
610
 
611
+ /* ── Chat Footer ───────────────────────────────────────────────────── */
612
  .chat-mini-footer {
613
  padding: 12px 20px;
 
614
  background-color: var(--bg-workspace);
615
  border-top: 1px solid var(--border-color);
616
  }
 
624
  margin-bottom: 0;
625
  }
626
 
627
+ /* ── Left Sidebar Drawer (Chat History) ────────────────────────────── */
628
  .sidebar-left {
629
  position: absolute;
630
  top: 0;
 
811
 
812
  .sidebar-overlay-left.active { opacity: 1; pointer-events: auto; }
813
 
814
+ /* ── Right Sidebar Overlay ─────────────────────────────────────────── */
815
  .sidebar-overlay {
816
  position: absolute;
817
  top: 0; left: 0; right: 0; bottom: 0;
 
825
 
826
  .sidebar-overlay.active { opacity: 1; pointer-events: auto; }
827
 
828
+ /* ── Right Settings Drawer ─────────────────────────────────────────── */
829
  .sidebar-right {
830
  position: absolute;
831
  top: 0; right: 0; bottom: 0;
 
894
  color: var(--text-primary);
895
  }
896
 
897
+ .block-value {
898
+ font-family: var(--font-mono);
899
+ font-size: 11px;
900
+ color: var(--text-secondary);
901
+ font-weight: 500;
902
+ }
903
+
904
  /* System Prompt */
905
  .system-prompt-textarea {
906
  width: 100%;
 
947
  border: none;
948
  }
949
 
 
 
 
 
 
 
 
950
  /* Effort Picker */
951
  .effort-picker {
952
  display: flex;
 
1065
 
1066
  .code-block-wrapper .copy-code-btn:hover { background-color: rgba(255, 255, 255, 0.2); color: #e5e7eb; }
1067
 
1068
+ /* ── Animations ────────────────────────────────────────────────────── */
1069
  .spinner-light {
1070
  width: 12px; height: 12px;
1071
  border: 1.5px solid rgba(255, 255, 255, 0.2);
 
1077
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
1078
  @keyframes slideUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
1079
 
1080
+ /* ── Responsive ────────────────────────────────────────────────────── */
1081
  @media (min-width: 641px) {
1082
  .sidebar-right.collapsed { transform: translateX(100%); }
1083
  }
 
1100
  .message-body { padding: 9px 12px; font-size: 13px; }
1101
  .message-bubble { max-width: 95%; }
1102
 
1103
+ .chat-mini-footer { padding: 8px 10px; }
1104
  .chat-mini-footer .console-box { padding: 6px 10px; }
1105
 
1106
+ .console-box textarea { font-size: 16px; /* prevent iOS zoom */ }
1107
 
1108
  .recipe-chip { padding: 6px 12px; font-size: 11.5px; }
1109
  .code-block-wrapper .copy-code-btn { font-size: 9px; padding: 1px 6px; }