akhaliq HF Staff commited on
Commit
6e7902e
·
1 Parent(s): af7c15b

Initialize Step 3.7 Flash Gradio Server application

Browse files
Files changed (4) hide show
  1. app.py +103 -0
  2. static/app.js +593 -0
  3. static/index.html +205 -0
  4. static/style.css +1057 -0
app.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from fastapi import FastAPI
4
+ from fastapi.responses import HTMLResponse
5
+ from fastapi.staticfiles import StaticFiles
6
+ from gradio import Server
7
+ from openai import OpenAI
8
+
9
+ # Initialize the Gradio Server (which is a FastAPI subclass)
10
+ app = Server()
11
+
12
+ # Create static directory if it doesn't exist
13
+ os.makedirs("static", exist_ok=True)
14
+
15
+ @app.api()
16
+ def chat_with_step(
17
+ messages_json: str,
18
+ api_key: str,
19
+ reasoning_effort: str = "medium",
20
+ max_tokens: int = 2048,
21
+ temperature: float = 0.7
22
+ ) -> str:
23
+ """
24
+ API endpoint to call Step 3.7 Flash model via OpenAI-compatible API.
25
+ Takes conversation messages as a JSON-serialized string, API Key, and parameters.
26
+ Returns the assistant response along with any reasoning details.
27
+ """
28
+ try:
29
+ # Load messages from JSON string
30
+ messages = json.loads(messages_json)
31
+
32
+ # Use user-supplied key or fallback to server environment variable
33
+ key = api_key.strip() if api_key and api_key.strip() else os.environ.get("STEP_API_KEY", "")
34
+ if not key:
35
+ return json.dumps({
36
+ "status": "error",
37
+ "message": "StepFun API Key is required. Please enter your API Key in the settings sidebar."
38
+ })
39
+
40
+ # Initialize OpenAI client configured for StepFun
41
+ client = OpenAI(
42
+ api_key=key,
43
+ base_url="https://api.stepfun.ai/v1",
44
+ )
45
+
46
+ # Prepare parameters for the API call
47
+ params = {
48
+ "model": "step-3.7-flash",
49
+ "messages": messages,
50
+ "max_tokens": max_tokens,
51
+ "temperature": temperature
52
+ }
53
+
54
+ # Add reasoning effort if applicable (only for step-3.7-flash model family)
55
+ if reasoning_effort in ["low", "medium", "high"]:
56
+ params["reasoning_effort"] = reasoning_effort
57
+
58
+ # Perform completion request
59
+ response = client.chat.completions.create(**params)
60
+
61
+ # Extract assistant content
62
+ content = response.choices[0].message.content
63
+
64
+ # Capture reasoning content if returned by the API
65
+ # Step 3.7 reasoning models might put reasoning in choice.message.reasoning_content
66
+ reasoning_content = getattr(response.choices[0].message, "reasoning_content", "")
67
+
68
+ # Alternatively, if the model returns thoughts inside <think> tags, we can extract them
69
+ if not reasoning_content and content and "<think>" in content and "</think>" in content:
70
+ parts = content.split("</think>", 1)
71
+ reasoning_content = parts[0].replace("<think>", "").strip()
72
+ content = parts[1].strip()
73
+
74
+ return json.dumps({
75
+ "status": "success",
76
+ "content": content,
77
+ "reasoning_content": reasoning_content or ""
78
+ })
79
+
80
+ except Exception as e:
81
+ return json.dumps({
82
+ "status": "error",
83
+ "message": str(e)
84
+ })
85
+
86
+ @app.get("/")
87
+ async def homepage():
88
+ """Serves the main application landing page."""
89
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static", "index.html")
90
+ if os.path.exists(html_path):
91
+ with open(html_path, "r", encoding="utf-8") as f:
92
+ return HTMLResponse(content=f.read(), status_code=200)
93
+ return HTMLResponse(
94
+ content="<h1>Frontend is building. Please refresh in a few seconds...</h1>",
95
+ status_code=200
96
+ )
97
+
98
+ # Mount static folder for CSS, JS, and image assets
99
+ app.mount("/static", StaticFiles(directory="static"), name="static")
100
+
101
+ if __name__ == "__main__":
102
+ # Launch Gradio Server (default port is 7860)
103
+ app.launch(show_error=True)
static/app.js ADDED
@@ -0,0 +1,593 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Import Hugging Face's Gradio Client
2
+ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
3
+
4
+ // Global Application State Management
5
+ const STATE = {
6
+ apiKey: localStorage.getItem("step_api_key") || "",
7
+ reasoningEffort: "medium",
8
+ maxTokens: 2048,
9
+ temperature: 0.7,
10
+ uploadedFiles: [], // Current prompt attachments
11
+ conversationHistory: [], // Conversation log sent to StepFun
12
+ gradioClient: null,
13
+ isThinking: false
14
+ };
15
+
16
+ // DOM Selections
17
+ const dom = {
18
+ apiKeyInput: document.getElementById("api-key-input"),
19
+ toggleKeyVisibility: document.getElementById("toggle-key-visibility"),
20
+ effortRadioButtons: document.querySelectorAll('input[name="reasoning-effort"]'),
21
+ maxTokensSlider: document.getElementById("max-tokens-slider"),
22
+ maxTokensVal: document.getElementById("max-tokens-val"),
23
+ temperatureSlider: document.getElementById("temperature-slider"),
24
+ temperatureVal: document.getElementById("temperature-val"),
25
+ dropZone: document.getElementById("drop-zone"),
26
+ fileUploader: document.getElementById("file-uploader"),
27
+ shelfInventory: document.getElementById("shelf-inventory"),
28
+ recipeCards: document.querySelectorAll(".recipe-card"),
29
+ clearChatBtn: document.getElementById("clear-chat-button"),
30
+ chatMessages: document.getElementById("chat-messages"),
31
+ quickShelfPreview: document.getElementById("quick-shelf-preview"),
32
+ quickUploadTrigger: document.getElementById("quick-upload-trigger"),
33
+ promptInput: document.getElementById("prompt-input"),
34
+ sendBtn: document.getElementById("send-button"),
35
+ sendIcon: document.getElementById("send-icon"),
36
+ sendSpinner: document.getElementById("send-spinner")
37
+ };
38
+
39
+ // Markdown configuration
40
+ marked.setOptions({
41
+ breaks: true,
42
+ highlight: function(code, lang) {
43
+ const language = hljs.getLanguage(lang) ? lang : 'plaintext';
44
+ return hljs.highlight(code, { language }).value;
45
+ }
46
+ });
47
+
48
+ // Setup Initial State & Event Handlers
49
+ async function initializeApp() {
50
+ // 1. Restore Credentials
51
+ if (STATE.apiKey) {
52
+ dom.apiKeyInput.value = STATE.apiKey;
53
+ }
54
+
55
+ // 2. Connect Gradio Client
56
+ try {
57
+ // connects to current host (where python app.py is served)
58
+ STATE.gradioClient = await Client.connect(window.location.origin);
59
+ console.log("Successfully connected to Gradio.Server backend.");
60
+ } catch (e) {
61
+ console.error("Gradio Client Connection Failed:", e);
62
+ appendSystemLog("System connection is restricted. Local commands might not be running.", true);
63
+ }
64
+
65
+ // 3. Register Settings Listeners
66
+ dom.apiKeyInput.addEventListener("input", (e) => {
67
+ STATE.apiKey = e.target.value;
68
+ localStorage.setItem("step_api_key", STATE.apiKey);
69
+ });
70
+
71
+ dom.toggleKeyVisibility.addEventListener("click", () => {
72
+ const type = dom.apiKeyInput.type === "password" ? "text" : "password";
73
+ dom.apiKeyInput.type = type;
74
+ });
75
+
76
+ dom.effortRadioButtons.forEach(radio => {
77
+ radio.addEventListener("change", (e) => {
78
+ STATE.reasoningEffort = e.target.value;
79
+ });
80
+ });
81
+
82
+ dom.maxTokensSlider.addEventListener("input", (e) => {
83
+ STATE.maxTokens = parseInt(e.target.value);
84
+ dom.maxTokensVal.textContent = STATE.maxTokens;
85
+ });
86
+
87
+ dom.temperatureSlider.addEventListener("input", (e) => {
88
+ STATE.temperature = parseFloat(e.target.value);
89
+ dom.temperatureVal.textContent = STATE.temperature.toFixed(1);
90
+ });
91
+
92
+ // 4. Register Files Upload Listeners
93
+ dom.quickUploadTrigger.addEventListener("click", () => dom.fileUploader.click());
94
+ dom.dropZone.addEventListener("click", () => dom.fileUploader.click());
95
+ dom.fileUploader.addEventListener("change", handleFileSelection);
96
+
97
+ // Dropzone Drag-and-Drop animations
98
+ ["dragenter", "dragover"].forEach(eventName => {
99
+ dom.dropZone.addEventListener(eventName, (e) => {
100
+ e.preventDefault();
101
+ dom.dropZone.classList.add("drag-active");
102
+ }, false);
103
+ });
104
+
105
+ ["dragleave", "drop"].forEach(eventName => {
106
+ dom.dropZone.addEventListener(eventName, (e) => {
107
+ e.preventDefault();
108
+ dom.dropZone.classList.remove("drag-active");
109
+ }, false);
110
+ });
111
+
112
+ dom.dropZone.addEventListener("drop", (e) => {
113
+ const dt = e.dataTransfer;
114
+ const files = dt.files;
115
+ processFiles(files);
116
+ });
117
+
118
+ // 5. Chat Operations
119
+ dom.sendBtn.addEventListener("click", triggerPromptSubmission);
120
+ dom.promptInput.addEventListener("keydown", (e) => {
121
+ if (e.key === "Enter" && !e.shiftKey) {
122
+ e.preventDefault();
123
+ triggerPromptSubmission();
124
+ }
125
+ });
126
+
127
+ dom.clearChatBtn.addEventListener("click", resetSandbox);
128
+
129
+ // 6. Recipe Recipes Console Setup
130
+ dom.recipeCards.forEach(card => {
131
+ card.addEventListener("click", () => {
132
+ const recipeType = card.getAttribute("data-recipe");
133
+ loadRecipe(recipeType);
134
+ });
135
+ });
136
+
137
+ // Auto-expand input textbox
138
+ dom.promptInput.addEventListener("input", () => {
139
+ dom.promptInput.style.height = "auto";
140
+ dom.promptInput.style.height = (dom.promptInput.scrollHeight) + "px";
141
+ });
142
+ }
143
+
144
+ // Handle File Select & Base64 Encoder
145
+ function handleFileSelection(e) {
146
+ processFiles(e.target.files);
147
+ }
148
+
149
+ function processFiles(files) {
150
+ if (!files.length) return;
151
+
152
+ Array.from(files).forEach(file => {
153
+ const reader = new FileReader();
154
+ reader.onload = (event) => {
155
+ const fileData = {
156
+ id: Math.random().toString(36).substring(2, 9),
157
+ name: file.name,
158
+ type: file.type,
159
+ size: (file.size / 1024 / 1024).toFixed(2) + " MB",
160
+ base64: event.target.result
161
+ };
162
+
163
+ STATE.uploadedFiles.push(fileData);
164
+ updateShelfUI();
165
+ };
166
+ reader.readAsDataURL(file);
167
+ });
168
+ }
169
+
170
+ // Update the Visual Attachments list
171
+ function updateShelfUI() {
172
+ // Clear inventory container
173
+ dom.shelfInventory.innerHTML = "";
174
+ dom.quickShelfPreview.innerHTML = "";
175
+
176
+ if (STATE.uploadedFiles.length === 0) {
177
+ dom.shelfInventory.innerHTML = `<div class="empty-shelf-text">No media loaded. Files are held in local state for your next prompts.</div>`;
178
+ return;
179
+ }
180
+
181
+ STATE.uploadedFiles.forEach(file => {
182
+ // 1. Sidebar Chip
183
+ const chip = document.createElement("div");
184
+ chip.className = "media-chip";
185
+
186
+ let previewHtml = "";
187
+ if (file.type.startsWith("image/")) {
188
+ previewHtml = `<img src="${file.base64}" alt="${file.name}">`;
189
+ } else if (file.type.startsWith("video/")) {
190
+ previewHtml = `🎬`;
191
+ } else {
192
+ previewHtml = `📎`;
193
+ }
194
+
195
+ chip.innerHTML = `
196
+ <div class="media-chip-preview">${previewHtml}</div>
197
+ <div class="media-chip-details">
198
+ <div class="media-chip-name">${file.name}</div>
199
+ <div class="media-chip-meta">
200
+ <span>${file.type.split("/")[1].toUpperCase()}</span>
201
+ <span>${file.size}</span>
202
+ </div>
203
+ </div>
204
+ <button class="media-chip-remove" data-id="${file.id}">
205
+ <svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
206
+ </button>
207
+ `;
208
+
209
+ chip.querySelector(".media-chip-remove").addEventListener("click", () => {
210
+ removeFile(file.id);
211
+ });
212
+
213
+ dom.shelfInventory.appendChild(chip);
214
+
215
+ // 2. Chat Quick Preview Item
216
+ const previewItem = document.createElement("div");
217
+ previewItem.className = "quick-preview-item";
218
+ previewItem.title = file.name;
219
+
220
+ if (file.type.startsWith("image/")) {
221
+ previewItem.innerHTML = `<img src="${file.base64}"><div class="quick-preview-badge"></div>`;
222
+ } else {
223
+ previewItem.innerHTML = `<div class="media-chip-preview" style="width:100%;height:100%;">🎬</div><div class="quick-preview-badge"></div>`;
224
+ }
225
+
226
+ dom.quickShelfPreview.appendChild(previewItem);
227
+ });
228
+ }
229
+
230
+ function removeFile(id) {
231
+ STATE.uploadedFiles = STATE.uploadedFiles.filter(f => f.id !== id);
232
+ updateShelfUI();
233
+ }
234
+
235
+ // Load Play Recipes template prompts
236
+ function loadRecipe(recipeType) {
237
+ // Standard mock files
238
+ const mockImageBase64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
239
+
240
+ // Reset shelf
241
+ STATE.uploadedFiles = [];
242
+
243
+ if (recipeType === "whiteboard") {
244
+ dom.promptInput.value = "Here is a snapshot of our whiteboard project plan sketch. Translate this visual sequence of tasks and boxes into a clean, itemized roadmap plan with a structured markdown table.";
245
+ STATE.uploadedFiles.push({
246
+ id: "recipe-whiteboard",
247
+ name: "whiteboard_gantt.png",
248
+ type: "image/png",
249
+ size: "0.02 MB",
250
+ base64: mockImageBase64
251
+ });
252
+ } else if (recipeType === "code") {
253
+ dom.promptInput.value = "Review this dashboard interface design. Generate beautiful, fully responsive HTML structure styling using elegant flex grids to match it.";
254
+ STATE.uploadedFiles.push({
255
+ id: "recipe-dashboard",
256
+ name: "dashboard_layout.png",
257
+ type: "image/png",
258
+ size: "0.02 MB",
259
+ base64: mockImageBase64
260
+ });
261
+ } else if (recipeType === "receipt") {
262
+ dom.promptInput.value = "Tabulate the items inside this receipt into a clean markdown table. Breakdown the tax and total, and perform a step-by-step reasoning calculation to check if the item sums match the total amount listed.";
263
+ STATE.uploadedFiles.push({
264
+ id: "recipe-receipt",
265
+ name: "grocery_bill.jpg",
266
+ type: "image/jpeg",
267
+ size: "0.02 MB",
268
+ base64: mockImageBase64
269
+ });
270
+ } else if (recipeType === "diagnostic") {
271
+ dom.promptInput.value = "This is a recorded visual sequence of steps leading to a runtime exception crash. Provide a detailed reconstruction of the event timeline, summarize the diagnostic signals, and suggest an engineering hotfix.";
272
+ // Simulating loading a short recording clip
273
+ STATE.uploadedFiles.push({
274
+ id: "recipe-diagnostics",
275
+ name: "console_bug.mp4",
276
+ type: "video/mp4",
277
+ size: "1.45 MB",
278
+ base64: mockImageBase64 // Represented via base64 for simplicity
279
+ });
280
+ }
281
+
282
+ updateShelfUI();
283
+ dom.promptInput.dispatchEvent(new Event("input"));
284
+ dom.promptInput.focus();
285
+ }
286
+
287
+ // Submitting prompts to Server
288
+ async function triggerPromptSubmission() {
289
+ if (STATE.isThinking) return;
290
+
291
+ const promptText = dom.promptInput.value.trim();
292
+ if (!promptText && STATE.uploadedFiles.length === 0) return;
293
+
294
+ // Check key
295
+ if (!STATE.apiKey && !dom.apiKeyInput.placeholder.includes("env key")) {
296
+ appendSystemLog("StepFun API Key is required. Please set it in the credentials input or configure STEP_API_KEY environment variable.", true);
297
+ return;
298
+ }
299
+
300
+ // Set UI loading state
301
+ setLoadingState(true);
302
+
303
+ // 1. Format user message content
304
+ // Content starts with the user's textual input prompt
305
+ const contentArray = [];
306
+ if (promptText) {
307
+ contentArray.push({
308
+ type: "text",
309
+ text: promptText
310
+ });
311
+ }
312
+
313
+ // Append attachments
314
+ STATE.uploadedFiles.forEach(file => {
315
+ if (file.type.startsWith("image/")) {
316
+ contentArray.push({
317
+ type: "image_url",
318
+ image_url: {
319
+ url: file.base64
320
+ }
321
+ });
322
+ } else if (file.type.startsWith("video/")) {
323
+ contentArray.push({
324
+ type: "video_url",
325
+ video_url: {
326
+ url: file.base64
327
+ }
328
+ });
329
+ }
330
+ });
331
+
332
+ const userMessage = {
333
+ role: "user",
334
+ content: contentArray
335
+ };
336
+
337
+ // 2. Add message to visible chat list
338
+ appendUserBubble(promptText, STATE.uploadedFiles);
339
+
340
+ // Add user message to backend conversation history log
341
+ STATE.conversationHistory.push(userMessage);
342
+
343
+ // Empty active UI text box and attachment shelf
344
+ dom.promptInput.value = "";
345
+ dom.promptInput.style.height = "auto";
346
+ const currentPromptAttachments = [...STATE.uploadedFiles];
347
+ STATE.uploadedFiles = [];
348
+ updateShelfUI();
349
+
350
+ // 3. Connect to Gradio.Server API
351
+ try {
352
+ if (!STATE.gradioClient) {
353
+ throw new Error("Gradio server is not connected.");
354
+ }
355
+
356
+ // Generate response bubble placeholder
357
+ const responseId = appendAssistantPlaceholderBubble();
358
+ const startTime = Date.now();
359
+
360
+ // Call the chat_with_step endpoint
361
+ // Arguments: [messages_json, api_key, reasoning_effort, max_tokens, temperature]
362
+ const result = await STATE.gradioClient.predict("/chat_with_step", [
363
+ JSON.stringify(STATE.conversationHistory),
364
+ STATE.apiKey,
365
+ STATE.reasoningEffort,
366
+ STATE.maxTokens,
367
+ STATE.temperature
368
+ ]);
369
+
370
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
371
+
372
+ // Parse results
373
+ const data = JSON.parse(result.data[0]);
374
+
375
+ if (data.status === "error") {
376
+ updateAssistantBubble(responseId, `⚠️ **API Error:** ${data.message}`, "", duration);
377
+ STATE.conversationHistory.pop(); // Remove failed prompt from history
378
+ } else {
379
+ // Update bubble with result markdown and thoughts
380
+ updateAssistantBubble(responseId, data.content, data.reasoning_content, duration);
381
+
382
+ // Append assistant completion to thread history
383
+ STATE.conversationHistory.push({
384
+ role: "assistant",
385
+ content: data.content
386
+ });
387
+ }
388
+
389
+ } catch (e) {
390
+ console.error(e);
391
+ appendSystemLog(`Connection exception: ${e.message}`, true);
392
+ setLoadingState(false);
393
+ }
394
+
395
+ setLoadingState(false);
396
+ }
397
+
398
+ // UI State Switcher
399
+ function setLoadingState(loading) {
400
+ STATE.isThinking = loading;
401
+ if (loading) {
402
+ dom.sendIcon.style.display = "none";
403
+ dom.sendSpinner.style.display = "block";
404
+ dom.sendBtn.disabled = true;
405
+ } else {
406
+ dom.sendIcon.style.display = "block";
407
+ dom.sendSpinner.style.display = "none";
408
+ dom.sendBtn.disabled = false;
409
+ }
410
+ }
411
+
412
+ // Append User Chat Message Bubble to Screen
413
+ function appendUserBubble(text, files) {
414
+ // Hide welcome box if present
415
+ const welcome = document.querySelector(".welcome-box");
416
+ if (welcome) welcome.style.display = "none";
417
+
418
+ const bubble = document.createElement("div");
419
+ bubble.className = "message-bubble user";
420
+
421
+ let attachmentsHtml = "";
422
+ if (files.length > 0) {
423
+ attachmentsHtml = `<div class="bubble-attachments">`;
424
+ files.forEach(file => {
425
+ if (file.type.startsWith("image/")) {
426
+ attachmentsHtml += `
427
+ <div class="bubble-attachment-card">
428
+ <img src="${file.base64}">
429
+ <div class="bubble-attachment-label">IMG</div>
430
+ </div>
431
+ `;
432
+ } else {
433
+ attachmentsHtml += `
434
+ <div class="bubble-attachment-card">
435
+ <div class="media-chip-preview" style="width:100%;height:100%;">🎬</div>
436
+ <div class="bubble-attachment-label">VIDEO</div>
437
+ </div>
438
+ `;
439
+ }
440
+ });
441
+ attachmentsHtml += `</div>`;
442
+ }
443
+
444
+ bubble.innerHTML = `
445
+ <div class="message-meta">User</div>
446
+ <div class="message-body">
447
+ <div class="message-text">${escapeHtml(text)}</div>
448
+ ${attachmentsHtml}
449
+ </div>
450
+ `;
451
+
452
+ dom.chatMessages.appendChild(bubble);
453
+ scrollToBottom();
454
+ }
455
+
456
+ // Append Assistant Loader/Placeholder to Chat Feed
457
+ function appendAssistantPlaceholderBubble() {
458
+ const id = "assistant-" + Math.random().toString(36).substring(2, 9);
459
+ const bubble = document.createElement("div");
460
+ bubble.className = "message-bubble assistant";
461
+ bubble.id = id;
462
+
463
+ bubble.innerHTML = `
464
+ <div class="message-meta">Step 3.7 Flash</div>
465
+ <div class="message-body">
466
+ <div class="thought-container" id="${id}-thought-box">
467
+ <div class="thought-header">
468
+ <div class="thought-title-group">
469
+ <div class="spinner" style="width:12px;height:12px;border-width:1.5px;"></div>
470
+ <span>Reasoning...</span>
471
+ </div>
472
+ <span class="thought-timer" id="${id}-timer">0.0s</span>
473
+ </div>
474
+ </div>
475
+ <div class="message-text markdown-body" id="${id}-text-box">
476
+ <span class="text-muted">Analyzing context and constructing reasoning chain...</span>
477
+ </div>
478
+ </div>
479
+ `;
480
+
481
+ dom.chatMessages.appendChild(bubble);
482
+ scrollToBottom();
483
+
484
+ // Start UI thought Timer
485
+ let seconds = 0.0;
486
+ const timerEl = document.getElementById(`${id}-timer`);
487
+ const interval = setInterval(() => {
488
+ if (!STATE.isThinking || !document.getElementById(id)) {
489
+ clearInterval(interval);
490
+ return;
491
+ }
492
+ seconds += 0.1;
493
+ timerEl.textContent = seconds.toFixed(1) + "s";
494
+ }, 100);
495
+
496
+ return id;
497
+ }
498
+
499
+ // Update Assistant Placeholder with Final API Results
500
+ function updateAssistantBubble(id, content, reasoning, duration) {
501
+ const bubble = document.getElementById(id);
502
+ if (!bubble) return;
503
+
504
+ const thoughtBox = document.getElementById(`${id}-thought-box`);
505
+ const textBox = document.getElementById(`${id}-text-box`);
506
+
507
+ // 1. Update Collapsible thoughts
508
+ if (reasoning) {
509
+ thoughtBox.innerHTML = `
510
+ <div class="thought-header" id="${id}-thought-toggle">
511
+ <div class="thought-title-group">
512
+ <span>🧠 Thought Process</span>
513
+ </div>
514
+ <div style="display:flex;align-items:center;gap:10px;">
515
+ <span class="thought-timer">Thought for ${duration}s</span>
516
+ <span class="thought-toggle-icon">▼</span>
517
+ </div>
518
+ </div>
519
+ <div class="thought-content">${escapeHtml(reasoning)}</div>
520
+ `;
521
+
522
+ // Add toggle expand/collapse behavior
523
+ const toggleBtn = document.getElementById(`${id}-thought-toggle`);
524
+ toggleBtn.addEventListener("click", () => {
525
+ thoughtBox.classList.toggle("collapsed");
526
+ });
527
+ } else {
528
+ // If model skipped reasoning, hide thinking layout entirely
529
+ thoughtBox.style.display = "none";
530
+ }
531
+
532
+ // 2. Render final markdown text
533
+ textBox.innerHTML = marked.parse(content);
534
+
535
+ // Highlight code blocks
536
+ textBox.querySelectorAll("pre code").forEach((el) => {
537
+ hljs.highlightElement(el);
538
+ });
539
+
540
+ scrollToBottom();
541
+ }
542
+
543
+ // Reset chat log
544
+ function resetSandbox() {
545
+ STATE.conversationHistory = [];
546
+ STATE.uploadedFiles = [];
547
+ updateShelfUI();
548
+ dom.chatMessages.innerHTML = `
549
+ <div class="welcome-box">
550
+ <div class="welcome-icon">⚡</div>
551
+ <h2>Sandbox Reset</h2>
552
+ <p>Conversation log history has been cleared. Load a recipe below or upload files to begin fresh.</p>
553
+ </div>
554
+ `;
555
+ appendSystemLog("Conversation context cleared successfully.");
556
+ }
557
+
558
+ // Helper: Append system errors or status notifications
559
+ function appendSystemLog(message, isError = false) {
560
+ const log = document.createElement("div");
561
+ log.className = "message-bubble assistant";
562
+ log.innerHTML = `
563
+ <div class="message-meta">System</div>
564
+ <div class="message-body" style="background-color: ${isError ? 'rgba(244,63,94,0.06)' : 'rgba(20,184,166,0.06)'}; border-color: ${isError ? 'rgba(244,63,94,0.2)' : 'rgba(20,184,166,0.2)'};">
565
+ <div class="message-text" style="color: ${isError ? varColor('danger') : varColor('accent-teal')}; font-size:12px; font-weight:500;">
566
+ ${isError ? '🛑' : 'ℹ️'} ${message}
567
+ </div>
568
+ </div>
569
+ `;
570
+ dom.chatMessages.appendChild(log);
571
+ scrollToBottom();
572
+ }
573
+
574
+ function varColor(name) {
575
+ return getComputedStyle(document.documentElement).getPropertyValue(`--${name}`).trim();
576
+ }
577
+
578
+ function escapeHtml(text) {
579
+ if (!text) return "";
580
+ return text
581
+ .replace(/&/g, "&amp;")
582
+ .replace(/</g, "&lt;")
583
+ .replace(/>/g, "&gt;")
584
+ .replace(/"/g, "&quot;")
585
+ .replace(/'/g, "&#039;");
586
+ }
587
+
588
+ function scrollToBottom() {
589
+ dom.chatMessages.scrollTop = dom.chatMessages.scrollHeight;
590
+ }
591
+
592
+ // Initialise Application when loaded
593
+ window.addEventListener("DOMContentLoaded", initializeApp);
static/index.html ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Step 3.7 Flash - Multimodal Reasoning Playground</title>
7
+ <!-- Google Fonts -->
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@300;400;500;600&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
11
+ <!-- Highlight.js for Code Highlighting -->
12
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
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">
18
+ </head>
19
+ <body>
20
+ <div class="app-container">
21
+ <!-- Sidebar Navigation & Parameter Controls -->
22
+ <aside class="sidebar">
23
+ <div class="sidebar-header">
24
+ <div class="logo-container">
25
+ <span class="logo-icon">⚡</span>
26
+ <div class="logo-text">
27
+ <h1>Step 3.7 Flash</h1>
28
+ <span class="badge">NATIVE MULTIMODAL</span>
29
+ </div>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="sidebar-content">
34
+ <!-- API Credentials -->
35
+ <section class="config-section">
36
+ <h2 class="section-title">Credentials</h2>
37
+ <div class="form-group">
38
+ <label for="api-key-input">StepFun API Key</label>
39
+ <div class="password-wrapper">
40
+ <input type="password" id="api-key-input" placeholder="Enter your key or use env key" autocomplete="off">
41
+ <button type="button" id="toggle-key-visibility" class="icon-button" title="Show/Hide Key">
42
+ <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="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
43
+ </button>
44
+ </div>
45
+ <span class="input-tip">Visit StepFun Console to obtain an API Key.</span>
46
+ </div>
47
+ </section>
48
+
49
+ <!-- Model Parameters -->
50
+ <section class="config-section">
51
+ <h2 class="section-title">Parameters</h2>
52
+
53
+ <div class="form-group">
54
+ <label>Reasoning Effort</label>
55
+ <div class="effort-picker">
56
+ <label class="effort-pill">
57
+ <input type="radio" name="reasoning-effort" value="low">
58
+ <span>Low</span>
59
+ </label>
60
+ <label class="effort-pill">
61
+ <input type="radio" name="reasoning-effort" value="medium" checked>
62
+ <span>Medium</span>
63
+ </label>
64
+ <label class="effort-pill">
65
+ <input type="radio" name="reasoning-effort" value="high">
66
+ <span>High</span>
67
+ </label>
68
+ </div>
69
+ <span class="input-tip">Low: fast summary/Q&amp;A. High: complex math/code.</span>
70
+ </div>
71
+
72
+ <div class="form-group">
73
+ <div class="label-row">
74
+ <label for="max-tokens-slider">Max Output Tokens</label>
75
+ <span class="value-display" id="max-tokens-val">2048</span>
76
+ </div>
77
+ <input type="range" id="max-tokens-slider" min="256" max="8192" step="256" value="2048">
78
+ </div>
79
+
80
+ <div class="form-group">
81
+ <div class="label-row">
82
+ <label for="temperature-slider">Temperature</label>
83
+ <span class="value-display" id="temperature-val">0.7</span>
84
+ </div>
85
+ <input type="range" id="temperature-slider" min="0.0" max="1.5" step="0.1" value="0.7">
86
+ </div>
87
+ </section>
88
+
89
+ <!-- Media Upload Desk -->
90
+ <section class="config-section">
91
+ <h2 class="section-title">Multimodal Shelf</h2>
92
+ <div class="upload-zone" id="drop-zone">
93
+ <input type="file" id="file-uploader" accept="image/*,video/*" multiple style="display: none;">
94
+ <svg class="upload-icon" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>
95
+ <p>Drag files here or <span>browse</span></p>
96
+ <span class="upload-limit">Images (PNG, JPG, WebP) | Videos (&lt;128MB, MP4)</span>
97
+ </div>
98
+ <div class="shelf-inventory" id="shelf-inventory">
99
+ <!-- Uploaded files will populate here dynamically -->
100
+ <div class="empty-shelf-text">No media loaded. Files are held in local state for your next prompts.</div>
101
+ </div>
102
+ </section>
103
+
104
+ <!-- Cookbooks & Quick Prompts -->
105
+ <section class="config-section">
106
+ <h2 class="section-title">Playground Recipes</h2>
107
+ <div class="recipes-grid">
108
+ <div class="recipe-card" data-recipe="whiteboard">
109
+ <span class="recipe-icon">✏️</span>
110
+ <div class="recipe-details">
111
+ <h3>Whiteboard Plan</h3>
112
+ <p>Turn a sketch into a project roadmap table.</p>
113
+ </div>
114
+ </div>
115
+ <div class="recipe-card" data-recipe="code">
116
+ <span class="recipe-icon">💻</span>
117
+ <div class="recipe-details">
118
+ <h3>Screenshot to Code</h3>
119
+ <p>Extract design elements to code.</p>
120
+ </div>
121
+ </div>
122
+ <div class="recipe-card" data-recipe="receipt">
123
+ <span class="recipe-icon">📊</span>
124
+ <div class="recipe-details">
125
+ <h3>Receipt Auditor</h3>
126
+ <p>Tabulate receipts with reasoning check.</p>
127
+ </div>
128
+ </div>
129
+ <div class="recipe-card" data-recipe="diagnostic">
130
+ <span class="recipe-icon">🎥</span>
131
+ <div class="recipe-details">
132
+ <h3>Video Diagnostics</h3>
133
+ <p>Reconstruct timeline events from video clip.</p>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </section>
138
+ </div>
139
+
140
+ <div class="sidebar-footer">
141
+ <div class="connection-status">
142
+ <span class="status-indicator online"></span>
143
+ <span>Gradio Server Queued</span>
144
+ </div>
145
+ </div>
146
+ </aside>
147
+
148
+ <!-- Main Chat Area -->
149
+ <main class="chat-container">
150
+ <header class="chat-header">
151
+ <div class="header-main">
152
+ <h2>Workspace Playground</h2>
153
+ <span class="model-badge">step-3.7-flash</span>
154
+ </div>
155
+ <div class="header-actions">
156
+ <button id="clear-chat-button" class="btn btn-secondary flex-center" title="Reset Sandbox">
157
+ <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
158
+ <span>Clear Chat</span>
159
+ </button>
160
+ </div>
161
+ </header>
162
+
163
+ <!-- Chat Message Feed -->
164
+ <div class="chat-messages" id="chat-messages">
165
+ <!-- Welcome greeting message -->
166
+ <div class="welcome-box">
167
+ <div class="welcome-icon">⚡</div>
168
+ <h2>Welcome to Step 3.7 Flash Playground</h2>
169
+ <p>Experience ultra-fast, state-of-the-art multimodal reasoning. This client communicates natively with your backend via <strong>Gradio.Server</strong>, using robust FastAPI queuing and threading.</p>
170
+ <div class="feature-tags">
171
+ <span class="tag">💡 High-Fidelity Thoughts</span>
172
+ <span class="tag">🖼️ Multi-Image Compares</span>
173
+ <span class="tag">🎬 Native Video Support</span>
174
+ <span class="tag">⚡ Low Latency Queuing</span>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <!-- Input Controls and Chat Bar -->
180
+ <footer class="chat-footer">
181
+ <!-- Quick Shelf Preview above input bar -->
182
+ <div class="quick-shelf-preview" id="quick-shelf-preview"></div>
183
+
184
+ <div class="input-panel">
185
+ <button class="btn btn-icon flex-center" id="quick-upload-trigger" title="Attach Media File">
186
+ <svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>
187
+ </button>
188
+
189
+ <div class="textarea-wrapper">
190
+ <textarea id="prompt-input" rows="1" placeholder="Ask anything, or drop an image/video here..." autofocus></textarea>
191
+ </div>
192
+
193
+ <button class="btn btn-primary btn-send flex-center" id="send-button" title="Send message to model">
194
+ <svg id="send-icon" viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
195
+ <div id="send-spinner" class="spinner" style="display: none;"></div>
196
+ </button>
197
+ </div>
198
+ </footer>
199
+ </main>
200
+ </div>
201
+
202
+ <!-- Gradio Client JS Library Connection -->
203
+ <script type="module" src="/static/app.js"></script>
204
+ </body>
205
+ </html>
static/style.css ADDED
@@ -0,0 +1,1057 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* CSS Variables for Premium Dark Theme Design System */
2
+ :root {
3
+ --bg-primary: #080c14;
4
+ --bg-secondary: #0e1626;
5
+ --bg-sidebar: #0a0e1a;
6
+ --bg-glass: rgba(15, 23, 42, 0.65);
7
+ --border-glass: rgba(99, 102, 241, 0.12);
8
+ --border-focus: rgba(99, 102, 241, 0.4);
9
+
10
+ --accent-indigo: #6366f1;
11
+ --accent-teal: #14b8a6;
12
+ --accent-indigo-gradient: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
13
+ --accent-teal-gradient: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
14
+ --accent-fusion-gradient: linear-gradient(135deg, #6366f1 0%, #14b8a6 100%);
15
+
16
+ --text-primary: #f8fafc;
17
+ --text-secondary: #94a3b8;
18
+ --text-muted: #64748b;
19
+ --danger: #f43f5e;
20
+
21
+ --shadow-sm: 0 2px 8px -2px rgba(0, 0, 0, 0.5);
22
+ --shadow-md: 0 4px 20px -4px rgba(0, 0, 0, 0.7);
23
+ --shadow-lg: 0 10px 30px -5px rgba(0, 0, 0, 0.8), 0 0 20px -2px rgba(99, 102, 241, 0.1);
24
+
25
+ --font-heading: 'Outfit', sans-serif;
26
+ --font-body: 'Inter', sans-serif;
27
+ --font-mono: 'Fira Code', monospace;
28
+
29
+ --transition-smooth: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
30
+ --border-radius-sm: 8px;
31
+ --border-radius-md: 12px;
32
+ --border-radius-lg: 16px;
33
+ }
34
+
35
+ /* Reset & Base Styling */
36
+ * {
37
+ margin: 0;
38
+ padding: 0;
39
+ box-sizing: border-box;
40
+ }
41
+
42
+ body {
43
+ background-color: var(--bg-primary);
44
+ color: var(--text-primary);
45
+ font-family: var(--font-body);
46
+ font-size: 15px;
47
+ line-height: 1.6;
48
+ overflow: hidden;
49
+ height: 100vh;
50
+ width: 100vw;
51
+ }
52
+
53
+ /* Scrollbar styling */
54
+ ::-webkit-scrollbar {
55
+ width: 6px;
56
+ height: 6px;
57
+ }
58
+
59
+ ::-webkit-scrollbar-track {
60
+ background: transparent;
61
+ }
62
+
63
+ ::-webkit-scrollbar-thumb {
64
+ background: rgba(255, 255, 255, 0.1);
65
+ border-radius: 10px;
66
+ }
67
+
68
+ ::-webkit-scrollbar-thumb:hover {
69
+ background: rgba(99, 102, 241, 0.3);
70
+ }
71
+
72
+ /* Container Structure */
73
+ .app-container {
74
+ display: flex;
75
+ height: 100vh;
76
+ width: 100vw;
77
+ position: relative;
78
+ background-image:
79
+ radial-gradient(at 10% 20%, rgba(99, 102, 241, 0.05) 0px, transparent 50%),
80
+ radial-gradient(at 90% 80%, rgba(20, 184, 166, 0.05) 0px, transparent 50%);
81
+ }
82
+
83
+ /* Sidebar Layout */
84
+ .sidebar {
85
+ width: 380px;
86
+ background-color: var(--bg-sidebar);
87
+ border-right: 1px solid var(--border-glass);
88
+ display: flex;
89
+ flex-direction: column;
90
+ height: 100%;
91
+ flex-shrink: 0;
92
+ backdrop-filter: blur(10px);
93
+ }
94
+
95
+ .sidebar-header {
96
+ padding: 24px;
97
+ border-bottom: 1px solid var(--border-glass);
98
+ }
99
+
100
+ .logo-container {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 12px;
104
+ }
105
+
106
+ .logo-icon {
107
+ font-size: 28px;
108
+ background: var(--accent-fusion-gradient);
109
+ -webkit-background-clip: text;
110
+ -webkit-text-fill-color: transparent;
111
+ filter: drop-shadow(0 0 8px rgba(99, 102, 241, 0.4));
112
+ animation: pulse 3s infinite ease-in-out;
113
+ }
114
+
115
+ .logo-text h1 {
116
+ font-family: var(--font-heading);
117
+ font-size: 20px;
118
+ font-weight: 700;
119
+ letter-spacing: -0.5px;
120
+ color: var(--text-primary);
121
+ }
122
+
123
+ .badge {
124
+ font-size: 10px;
125
+ font-weight: 600;
126
+ text-transform: uppercase;
127
+ letter-spacing: 1px;
128
+ background: rgba(99, 102, 241, 0.15);
129
+ color: var(--accent-indigo);
130
+ padding: 2px 8px;
131
+ border-radius: 100px;
132
+ border: 1px solid rgba(99, 102, 241, 0.25);
133
+ display: inline-block;
134
+ margin-top: 2px;
135
+ }
136
+
137
+ .sidebar-content {
138
+ flex: 1;
139
+ overflow-y: auto;
140
+ padding: 24px;
141
+ display: flex;
142
+ flex-direction: column;
143
+ gap: 28px;
144
+ }
145
+
146
+ /* Section styling inside sidebar */
147
+ .config-section {
148
+ display: flex;
149
+ flex-direction: column;
150
+ gap: 16px;
151
+ }
152
+
153
+ .section-title {
154
+ font-family: var(--font-heading);
155
+ font-size: 13px;
156
+ font-weight: 600;
157
+ text-transform: uppercase;
158
+ letter-spacing: 1.5px;
159
+ color: var(--text-secondary);
160
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
161
+ padding-bottom: 8px;
162
+ }
163
+
164
+ /* Form inputs & Groups */
165
+ .form-group {
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: 8px;
169
+ }
170
+
171
+ .form-group label {
172
+ font-family: var(--font-heading);
173
+ font-size: 13px;
174
+ font-weight: 500;
175
+ color: var(--text-primary);
176
+ }
177
+
178
+ .label-row {
179
+ display: flex;
180
+ justify-content: space-between;
181
+ align-items: center;
182
+ }
183
+
184
+ .value-display {
185
+ font-family: var(--font-mono);
186
+ font-size: 11px;
187
+ background: rgba(255, 255, 255, 0.05);
188
+ padding: 1px 6px;
189
+ border-radius: 4px;
190
+ color: var(--accent-teal);
191
+ }
192
+
193
+ .input-tip {
194
+ font-size: 11px;
195
+ color: var(--text-muted);
196
+ }
197
+
198
+ /* Credentials visibility toggler input */
199
+ .password-wrapper {
200
+ position: relative;
201
+ display: flex;
202
+ align-items: center;
203
+ }
204
+
205
+ input[type="text"],
206
+ input[type="password"] {
207
+ width: 100%;
208
+ background-color: rgba(15, 23, 42, 0.4);
209
+ border: 1px solid var(--border-glass);
210
+ border-radius: var(--border-radius-sm);
211
+ color: var(--text-primary);
212
+ padding: 10px 14px;
213
+ font-size: 13px;
214
+ font-family: var(--font-body);
215
+ transition: var(--transition-smooth);
216
+ }
217
+
218
+ input[type="password"] {
219
+ padding-right: 40px;
220
+ }
221
+
222
+ input[type="text"]:focus,
223
+ input[type="password"]:focus {
224
+ outline: none;
225
+ border-color: var(--accent-indigo);
226
+ box-shadow: 0 0 10px rgba(99, 102, 241, 0.15);
227
+ background-color: rgba(15, 23, 42, 0.8);
228
+ }
229
+
230
+ .password-wrapper .icon-button {
231
+ position: absolute;
232
+ right: 8px;
233
+ background: transparent;
234
+ border: none;
235
+ color: var(--text-secondary);
236
+ cursor: pointer;
237
+ padding: 6px;
238
+ border-radius: 4px;
239
+ transition: var(--transition-smooth);
240
+ }
241
+
242
+ .password-wrapper .icon-button:hover {
243
+ color: var(--text-primary);
244
+ background-color: rgba(255, 255, 255, 0.05);
245
+ }
246
+
247
+ /* Range input customizing */
248
+ input[type="range"] {
249
+ -webkit-appearance: none;
250
+ width: 100%;
251
+ height: 4px;
252
+ background: rgba(255, 255, 255, 0.1);
253
+ border-radius: 2px;
254
+ outline: none;
255
+ }
256
+
257
+ input[type="range"]::-webkit-slider-thumb {
258
+ -webkit-appearance: none;
259
+ appearance: none;
260
+ width: 14px;
261
+ height: 14px;
262
+ border-radius: 50%;
263
+ background: var(--accent-indigo);
264
+ cursor: pointer;
265
+ transition: var(--transition-smooth);
266
+ box-shadow: 0 0 8px rgba(99, 102, 241, 0.5);
267
+ }
268
+
269
+ input[type="range"]::-webkit-slider-thumb:hover {
270
+ background: var(--accent-teal);
271
+ box-shadow: 0 0 10px rgba(20, 184, 166, 0.6);
272
+ transform: scale(1.1);
273
+ }
274
+
275
+ /* Effort Radio Pills Picker */
276
+ .effort-picker {
277
+ display: flex;
278
+ background-color: rgba(15, 23, 42, 0.4);
279
+ border: 1px solid var(--border-glass);
280
+ padding: 3px;
281
+ border-radius: var(--border-radius-sm);
282
+ gap: 2px;
283
+ }
284
+
285
+ .effort-pill {
286
+ flex: 1;
287
+ cursor: pointer;
288
+ text-align: center;
289
+ position: relative;
290
+ }
291
+
292
+ .effort-pill input {
293
+ position: absolute;
294
+ opacity: 0;
295
+ width: 0;
296
+ height: 0;
297
+ }
298
+
299
+ .effort-pill span {
300
+ display: block;
301
+ padding: 8px 0;
302
+ font-size: 12px;
303
+ font-weight: 500;
304
+ border-radius: 6px;
305
+ color: var(--text-secondary);
306
+ transition: var(--transition-smooth);
307
+ font-family: var(--font-heading);
308
+ }
309
+
310
+ .effort-pill input:checked + span {
311
+ background: var(--accent-indigo-gradient);
312
+ color: var(--text-primary);
313
+ box-shadow: var(--shadow-sm);
314
+ }
315
+
316
+ .effort-pill:hover span:not(input:checked + span) {
317
+ color: var(--text-primary);
318
+ background-color: rgba(255, 255, 255, 0.03);
319
+ }
320
+
321
+ /* Upload zone interface */
322
+ .upload-zone {
323
+ border: 1.5px dashed var(--border-glass);
324
+ border-radius: var(--border-radius-md);
325
+ padding: 20px;
326
+ text-align: center;
327
+ cursor: pointer;
328
+ background: rgba(15, 23, 42, 0.2);
329
+ transition: var(--transition-smooth);
330
+ display: flex;
331
+ flex-direction: column;
332
+ align-items: center;
333
+ gap: 8px;
334
+ }
335
+
336
+ .upload-zone:hover {
337
+ border-color: var(--accent-indigo);
338
+ background: rgba(99, 102, 241, 0.03);
339
+ }
340
+
341
+ .upload-zone.drag-active {
342
+ border-color: var(--accent-teal);
343
+ background: rgba(20, 184, 166, 0.06);
344
+ transform: scale(0.98);
345
+ }
346
+
347
+ .upload-icon {
348
+ color: var(--text-secondary);
349
+ transition: var(--transition-smooth);
350
+ }
351
+
352
+ .upload-zone:hover .upload-icon {
353
+ color: var(--accent-indigo);
354
+ transform: translateY(-2px);
355
+ }
356
+
357
+ .upload-zone p {
358
+ font-size: 13px;
359
+ font-weight: 500;
360
+ }
361
+
362
+ .upload-zone p span {
363
+ color: var(--accent-indigo);
364
+ font-weight: 600;
365
+ }
366
+
367
+ .upload-limit {
368
+ font-size: 10px;
369
+ color: var(--text-muted);
370
+ }
371
+
372
+ /* Shelf Inventory */
373
+ .shelf-inventory {
374
+ display: flex;
375
+ flex-direction: column;
376
+ gap: 8px;
377
+ max-height: 200px;
378
+ overflow-y: auto;
379
+ padding-right: 4px;
380
+ }
381
+
382
+ .empty-shelf-text {
383
+ font-size: 11px;
384
+ color: var(--text-muted);
385
+ text-align: center;
386
+ padding: 10px 0;
387
+ font-style: italic;
388
+ }
389
+
390
+ .media-chip {
391
+ display: flex;
392
+ align-items: center;
393
+ background-color: rgba(255, 255, 255, 0.03);
394
+ border: 1px solid var(--border-glass);
395
+ border-radius: var(--border-radius-sm);
396
+ padding: 6px 10px;
397
+ gap: 10px;
398
+ transition: var(--transition-smooth);
399
+ position: relative;
400
+ overflow: hidden;
401
+ }
402
+
403
+ .media-chip:hover {
404
+ border-color: rgba(99, 102, 241, 0.3);
405
+ background-color: rgba(255, 255, 255, 0.05);
406
+ }
407
+
408
+ .media-chip-preview {
409
+ width: 32px;
410
+ height: 32px;
411
+ border-radius: 4px;
412
+ object-fit: cover;
413
+ background-color: #000;
414
+ flex-shrink: 0;
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: center;
418
+ font-size: 14px;
419
+ border: 1px solid rgba(255, 255, 255, 0.1);
420
+ }
421
+
422
+ .media-chip-preview img,
423
+ .media-chip-preview video {
424
+ width: 100%;
425
+ height: 100%;
426
+ object-fit: cover;
427
+ }
428
+
429
+ .media-chip-details {
430
+ flex: 1;
431
+ min-width: 0;
432
+ }
433
+
434
+ .media-chip-name {
435
+ font-size: 12px;
436
+ font-weight: 500;
437
+ white-space: nowrap;
438
+ overflow: hidden;
439
+ text-overflow: ellipsis;
440
+ }
441
+
442
+ .media-chip-meta {
443
+ font-size: 10px;
444
+ color: var(--text-muted);
445
+ display: flex;
446
+ gap: 8px;
447
+ }
448
+
449
+ .media-chip-remove {
450
+ background: transparent;
451
+ border: none;
452
+ color: var(--text-muted);
453
+ cursor: pointer;
454
+ padding: 4px;
455
+ border-radius: 4px;
456
+ display: flex;
457
+ align-items: center;
458
+ justify-content: center;
459
+ transition: var(--transition-smooth);
460
+ }
461
+
462
+ .media-chip-remove:hover {
463
+ color: var(--danger);
464
+ background-color: rgba(244, 63, 94, 0.1);
465
+ }
466
+
467
+ /* Recipes Grids */
468
+ .recipes-grid {
469
+ display: grid;
470
+ grid-template-columns: 1fr 1fr;
471
+ gap: 8px;
472
+ }
473
+
474
+ .recipe-card {
475
+ background-color: rgba(255, 255, 255, 0.02);
476
+ border: 1px solid var(--border-glass);
477
+ border-radius: var(--border-radius-sm);
478
+ padding: 10px;
479
+ cursor: pointer;
480
+ transition: var(--transition-smooth);
481
+ display: flex;
482
+ gap: 8px;
483
+ align-items: flex-start;
484
+ }
485
+
486
+ .recipe-card:hover {
487
+ background-color: rgba(99, 102, 241, 0.05);
488
+ border-color: rgba(99, 102, 241, 0.25);
489
+ transform: translateY(-2px);
490
+ }
491
+
492
+ .recipe-icon {
493
+ font-size: 16px;
494
+ background: rgba(255, 255, 255, 0.03);
495
+ padding: 6px;
496
+ border-radius: 6px;
497
+ }
498
+
499
+ .recipe-details h3 {
500
+ font-family: var(--font-heading);
501
+ font-size: 11px;
502
+ font-weight: 600;
503
+ color: var(--text-primary);
504
+ }
505
+
506
+ .recipe-details p {
507
+ font-size: 9px;
508
+ color: var(--text-muted);
509
+ line-height: 1.3;
510
+ margin-top: 2px;
511
+ }
512
+
513
+ /* Sidebar Footer area */
514
+ .sidebar-footer {
515
+ padding: 16px 24px;
516
+ border-top: 1px solid var(--border-glass);
517
+ }
518
+
519
+ .connection-status {
520
+ display: flex;
521
+ align-items: center;
522
+ gap: 8px;
523
+ font-size: 11px;
524
+ color: var(--text-secondary);
525
+ }
526
+
527
+ .status-indicator {
528
+ width: 6px;
529
+ height: 6px;
530
+ border-radius: 50%;
531
+ position: relative;
532
+ }
533
+
534
+ .status-indicator.online {
535
+ background-color: var(--accent-teal);
536
+ box-shadow: 0 0 6px var(--accent-teal);
537
+ }
538
+
539
+ /* Main Chat Container Area */
540
+ .chat-container {
541
+ flex: 1;
542
+ display: flex;
543
+ flex-direction: column;
544
+ height: 100%;
545
+ background-color: rgba(11, 15, 25, 0.5);
546
+ }
547
+
548
+ /* Chat Header */
549
+ .chat-header {
550
+ padding: 18px 30px;
551
+ border-bottom: 1px solid var(--border-glass);
552
+ display: flex;
553
+ justify-content: space-between;
554
+ align-items: center;
555
+ backdrop-filter: blur(10px);
556
+ z-index: 10;
557
+ }
558
+
559
+ .header-main h2 {
560
+ font-family: var(--font-heading);
561
+ font-size: 16px;
562
+ font-weight: 600;
563
+ color: var(--text-primary);
564
+ }
565
+
566
+ .model-badge {
567
+ font-family: var(--font-mono);
568
+ font-size: 11px;
569
+ color: var(--accent-teal);
570
+ background-color: rgba(20, 184, 166, 0.08);
571
+ border: 1px solid rgba(20, 184, 166, 0.2);
572
+ padding: 1px 6px;
573
+ border-radius: 4px;
574
+ display: inline-block;
575
+ margin-top: 2px;
576
+ }
577
+
578
+ .flex-center {
579
+ display: flex;
580
+ align-items: center;
581
+ justify-content: center;
582
+ }
583
+
584
+ /* Buttons */
585
+ .btn {
586
+ border-radius: var(--border-radius-sm);
587
+ font-family: var(--font-heading);
588
+ font-weight: 500;
589
+ font-size: 12px;
590
+ cursor: pointer;
591
+ transition: var(--transition-smooth);
592
+ border: none;
593
+ outline: none;
594
+ gap: 6px;
595
+ }
596
+
597
+ .btn-secondary {
598
+ background-color: rgba(255, 255, 255, 0.04);
599
+ border: 1px solid var(--border-glass);
600
+ color: var(--text-primary);
601
+ padding: 8px 12px;
602
+ }
603
+
604
+ .btn-secondary:hover {
605
+ background-color: rgba(255, 255, 255, 0.08);
606
+ border-color: rgba(255, 255, 255, 0.15);
607
+ }
608
+
609
+ .btn-icon {
610
+ background: transparent;
611
+ color: var(--text-secondary);
612
+ border: 1px solid var(--border-glass);
613
+ border-radius: var(--border-radius-sm);
614
+ width: 38px;
615
+ height: 38px;
616
+ }
617
+
618
+ .btn-icon:hover {
619
+ color: var(--text-primary);
620
+ background-color: rgba(255, 255, 255, 0.05);
621
+ border-color: rgba(255, 255, 255, 0.1);
622
+ }
623
+
624
+ .btn-primary {
625
+ background: var(--accent-indigo-gradient);
626
+ color: var(--text-primary);
627
+ padding: 8px 16px;
628
+ font-weight: 600;
629
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.25);
630
+ }
631
+
632
+ .btn-primary:hover {
633
+ transform: translateY(-1px);
634
+ box-shadow: 0 6px 16px rgba(99, 102, 241, 0.35);
635
+ }
636
+
637
+ .btn-primary:active {
638
+ transform: translateY(0);
639
+ }
640
+
641
+ /* Messages area */
642
+ .chat-messages {
643
+ flex: 1;
644
+ overflow-y: auto;
645
+ padding: 30px;
646
+ display: flex;
647
+ flex-direction: column;
648
+ gap: 24px;
649
+ }
650
+
651
+ /* Welcome Box styling */
652
+ .welcome-box {
653
+ max-width: 600px;
654
+ margin: 40px auto;
655
+ background-color: var(--bg-glass);
656
+ border: 1px solid var(--border-glass);
657
+ padding: 40px;
658
+ border-radius: var(--border-radius-lg);
659
+ text-align: center;
660
+ box-shadow: var(--shadow-lg);
661
+ backdrop-filter: blur(16px);
662
+ animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1);
663
+ }
664
+
665
+ .welcome-icon {
666
+ font-size: 40px;
667
+ width: 70px;
668
+ height: 70px;
669
+ background: rgba(99, 102, 241, 0.1);
670
+ border: 1px solid rgba(99, 102, 241, 0.2);
671
+ border-radius: 50%;
672
+ margin: 0 auto 20px;
673
+ display: flex;
674
+ align-items: center;
675
+ justify-content: center;
676
+ box-shadow: 0 0 20px rgba(99, 102, 241, 0.15);
677
+ }
678
+
679
+ .welcome-box h2 {
680
+ font-family: var(--font-heading);
681
+ font-size: 22px;
682
+ font-weight: 700;
683
+ margin-bottom: 12px;
684
+ }
685
+
686
+ .welcome-box p {
687
+ color: var(--text-secondary);
688
+ font-size: 13.5px;
689
+ margin-bottom: 24px;
690
+ line-height: 1.5;
691
+ }
692
+
693
+ .feature-tags {
694
+ display: flex;
695
+ flex-wrap: wrap;
696
+ gap: 8px;
697
+ justify-content: center;
698
+ }
699
+
700
+ .tag {
701
+ font-size: 11px;
702
+ font-weight: 500;
703
+ background-color: rgba(255, 255, 255, 0.03);
704
+ border: 1px solid rgba(255, 255, 255, 0.06);
705
+ padding: 4px 10px;
706
+ border-radius: 100px;
707
+ color: var(--text-secondary);
708
+ }
709
+
710
+ /* Chat bubble entries */
711
+ .message-bubble {
712
+ display: flex;
713
+ flex-direction: column;
714
+ gap: 8px;
715
+ max-width: 80%;
716
+ animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
717
+ }
718
+
719
+ .message-bubble.user {
720
+ align-self: flex-end;
721
+ }
722
+
723
+ .message-bubble.assistant {
724
+ align-self: flex-start;
725
+ }
726
+
727
+ .message-meta {
728
+ font-size: 11px;
729
+ color: var(--text-muted);
730
+ font-weight: 500;
731
+ margin: 0 4px;
732
+ }
733
+
734
+ .message-bubble.user .message-meta {
735
+ text-align: right;
736
+ }
737
+
738
+ .message-body {
739
+ padding: 14px 18px;
740
+ border-radius: var(--border-radius-md);
741
+ font-size: 14.5px;
742
+ line-height: 1.65;
743
+ position: relative;
744
+ box-shadow: var(--shadow-sm);
745
+ }
746
+
747
+ .message-bubble.user .message-body {
748
+ background-color: rgba(99, 102, 241, 0.15);
749
+ border: 1px solid rgba(99, 102, 241, 0.25);
750
+ border-bottom-right-radius: 4px;
751
+ color: var(--text-primary);
752
+ }
753
+
754
+ .message-bubble.assistant .message-body {
755
+ background-color: var(--bg-glass);
756
+ border: 1px solid var(--border-glass);
757
+ border-bottom-left-radius: 4px;
758
+ color: var(--text-primary);
759
+ backdrop-filter: blur(10px);
760
+ }
761
+
762
+ /* Rendered Multimodal media inside Chat Bubbles */
763
+ .bubble-attachments {
764
+ display: grid;
765
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
766
+ gap: 8px;
767
+ margin-top: 10px;
768
+ max-width: 400px;
769
+ }
770
+
771
+ .bubble-attachment-card {
772
+ border-radius: var(--border-radius-sm);
773
+ overflow: hidden;
774
+ border: 1px solid rgba(255, 255, 255, 0.1);
775
+ background-color: #000;
776
+ position: relative;
777
+ aspect-ratio: 16/10;
778
+ }
779
+
780
+ .bubble-attachment-card img,
781
+ .bubble-attachment-card video {
782
+ width: 100%;
783
+ height: 100%;
784
+ object-fit: cover;
785
+ }
786
+
787
+ .bubble-attachment-label {
788
+ position: absolute;
789
+ bottom: 0;
790
+ left: 0;
791
+ right: 0;
792
+ background: rgba(0, 0, 0, 0.7);
793
+ color: #fff;
794
+ font-size: 8px;
795
+ text-align: center;
796
+ padding: 2px;
797
+ font-family: var(--font-mono);
798
+ }
799
+
800
+ /* Collapsible Thought Process Box for reasoning models */
801
+ .thought-container {
802
+ margin-bottom: 12px;
803
+ border: 1px solid rgba(99, 102, 241, 0.15);
804
+ border-radius: var(--border-radius-sm);
805
+ overflow: hidden;
806
+ background-color: rgba(99, 102, 241, 0.02);
807
+ }
808
+
809
+ .thought-header {
810
+ display: flex;
811
+ justify-content: space-between;
812
+ align-items: center;
813
+ padding: 8px 12px;
814
+ background-color: rgba(99, 102, 241, 0.05);
815
+ cursor: pointer;
816
+ user-select: none;
817
+ transition: var(--transition-smooth);
818
+ }
819
+
820
+ .thought-header:hover {
821
+ background-color: rgba(99, 102, 241, 0.08);
822
+ }
823
+
824
+ .thought-title-group {
825
+ display: flex;
826
+ align-items: center;
827
+ gap: 6px;
828
+ font-family: var(--font-heading);
829
+ font-size: 11px;
830
+ font-weight: 600;
831
+ color: var(--accent-indigo);
832
+ }
833
+
834
+ .thought-timer {
835
+ font-family: var(--font-mono);
836
+ font-size: 10px;
837
+ color: var(--text-secondary);
838
+ }
839
+
840
+ .thought-toggle-icon {
841
+ font-size: 10px;
842
+ color: var(--text-secondary);
843
+ transition: transform 0.2s ease;
844
+ }
845
+
846
+ .thought-container.collapsed .thought-toggle-icon {
847
+ transform: rotate(-90deg);
848
+ }
849
+
850
+ .thought-content {
851
+ padding: 12px 14px;
852
+ font-family: var(--font-mono);
853
+ font-size: 12px;
854
+ color: var(--text-secondary);
855
+ background-color: rgba(8, 12, 20, 0.4);
856
+ border-top: 1px solid rgba(99, 102, 241, 0.1);
857
+ white-space: pre-wrap;
858
+ max-height: 250px;
859
+ overflow-y: auto;
860
+ }
861
+
862
+ .thought-container.collapsed .thought-content {
863
+ display: none;
864
+ }
865
+
866
+ /* Markdown typography rules within message bodies */
867
+ .markdown-body h1,
868
+ .markdown-body h2,
869
+ .markdown-body h3 {
870
+ margin-top: 14px;
871
+ margin-bottom: 8px;
872
+ font-family: var(--font-heading);
873
+ font-weight: 600;
874
+ }
875
+
876
+ .markdown-body h1 { font-size: 18px; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 4px; }
877
+ .markdown-body h2 { font-size: 15px; }
878
+ .markdown-body h3 { font-size: 13.5px; }
879
+
880
+ .markdown-body p {
881
+ margin-bottom: 12px;
882
+ }
883
+
884
+ .markdown-body p:last-child {
885
+ margin-bottom: 0;
886
+ }
887
+
888
+ .markdown-body code {
889
+ font-family: var(--font-mono);
890
+ font-size: 12px;
891
+ background-color: rgba(255, 255, 255, 0.08);
892
+ padding: 2px 6px;
893
+ border-radius: 4px;
894
+ color: var(--accent-teal);
895
+ }
896
+
897
+ .markdown-body pre {
898
+ margin: 14px 0;
899
+ border-radius: var(--border-radius-sm);
900
+ overflow: hidden;
901
+ border: 1px solid rgba(255,255,255,0.08);
902
+ }
903
+
904
+ .markdown-body pre code {
905
+ display: block;
906
+ padding: 12px 14px;
907
+ overflow-x: auto;
908
+ background-color: #0c101b !important;
909
+ color: #e2e8f0;
910
+ }
911
+
912
+ .markdown-body table {
913
+ width: 100%;
914
+ border-collapse: collapse;
915
+ margin: 14px 0;
916
+ font-size: 13px;
917
+ }
918
+
919
+ .markdown-body th,
920
+ .markdown-body td {
921
+ border: 1px solid rgba(255,255,255,0.08);
922
+ padding: 8px 12px;
923
+ text-align: left;
924
+ }
925
+
926
+ .markdown-body th {
927
+ background-color: rgba(255,255,255,0.03);
928
+ font-weight: 600;
929
+ }
930
+
931
+ .markdown-body ul,
932
+ .markdown-body ol {
933
+ margin-left: 20px;
934
+ margin-bottom: 12px;
935
+ }
936
+
937
+ .markdown-body li {
938
+ margin-bottom: 4px;
939
+ }
940
+
941
+ /* Chat Input Console Footer */
942
+ .chat-footer {
943
+ padding: 20px 30px;
944
+ border-top: 1px solid var(--border-glass);
945
+ background-color: var(--bg-sidebar);
946
+ }
947
+
948
+ /* Quick upload preview shelf */
949
+ .quick-shelf-preview {
950
+ display: flex;
951
+ gap: 8px;
952
+ margin-bottom: 12px;
953
+ overflow-x: auto;
954
+ padding-bottom: 4px;
955
+ }
956
+
957
+ .quick-preview-item {
958
+ width: 48px;
959
+ height: 48px;
960
+ border-radius: 6px;
961
+ border: 1.5px solid var(--accent-indigo);
962
+ overflow: hidden;
963
+ position: relative;
964
+ flex-shrink: 0;
965
+ box-shadow: 0 0 8px rgba(99, 102, 241, 0.3);
966
+ }
967
+
968
+ .quick-preview-item img,
969
+ .quick-preview-item video {
970
+ width: 100%;
971
+ height: 100%;
972
+ object-fit: cover;
973
+ }
974
+
975
+ .quick-preview-badge {
976
+ position: absolute;
977
+ top: 2px;
978
+ right: 2px;
979
+ background-color: var(--accent-teal);
980
+ width: 12px;
981
+ height: 12px;
982
+ border-radius: 50%;
983
+ border: 1px solid #fff;
984
+ }
985
+
986
+ /* Actual input bar layout */
987
+ .input-panel {
988
+ display: flex;
989
+ align-items: center;
990
+ background-color: rgba(15, 23, 42, 0.4);
991
+ border: 1px solid var(--border-glass);
992
+ border-radius: var(--border-radius-md);
993
+ padding: 6px 10px;
994
+ gap: 8px;
995
+ transition: var(--transition-smooth);
996
+ }
997
+
998
+ .input-panel:focus-within {
999
+ border-color: var(--accent-indigo);
1000
+ box-shadow: 0 0 14px rgba(99, 102, 241, 0.1);
1001
+ background-color: rgba(15, 23, 42, 0.8);
1002
+ }
1003
+
1004
+ .textarea-wrapper {
1005
+ flex: 1;
1006
+ }
1007
+
1008
+ .input-panel textarea {
1009
+ width: 100%;
1010
+ background: transparent;
1011
+ border: none;
1012
+ outline: none;
1013
+ color: var(--text-primary);
1014
+ font-family: var(--font-body);
1015
+ font-size: 14px;
1016
+ padding: 10px 4px;
1017
+ resize: none;
1018
+ max-height: 120px;
1019
+ }
1020
+
1021
+ .btn-send {
1022
+ width: 38px;
1023
+ height: 38px;
1024
+ border-radius: var(--border-radius-sm);
1025
+ padding: 0;
1026
+ flex-shrink: 0;
1027
+ }
1028
+
1029
+ /* Loading animations spinner */
1030
+ .spinner {
1031
+ width: 16px;
1032
+ height: 16px;
1033
+ border: 2px solid rgba(255, 255, 255, 0.3);
1034
+ border-top: 2px solid #fff;
1035
+ border-radius: 50%;
1036
+ animation: spin 0.8s linear infinite;
1037
+ }
1038
+
1039
+ @keyframes spin {
1040
+ 0% { transform: rotate(0deg); }
1041
+ 100% { transform: rotate(360deg); }
1042
+ }
1043
+
1044
+ @keyframes pulse {
1045
+ 0%, 100% { transform: scale(1); opacity: 1; }
1046
+ 50% { transform: scale(1.05); opacity: 0.85; }
1047
+ }
1048
+
1049
+ @keyframes fadeIn {
1050
+ from { opacity: 0; transform: translateY(10px); }
1051
+ to { opacity: 1; transform: translateY(0); }
1052
+ }
1053
+
1054
+ @keyframes slideUp {
1055
+ from { opacity: 0; transform: translateY(20px); }
1056
+ to { opacity: 1; transform: translateY(0); }
1057
+ }