lvwerra HF Staff commited on
Commit
2d52286
·
1 Parent(s): fcc734a
Files changed (7) hide show
  1. .gitignore +218 -0
  2. backend/__pycache__/command.cpython-312.pyc +0 -0
  3. backend/command.py +2 -1
  4. backend/main.py +124 -12
  5. index.html +139 -121
  6. script.js +221 -24
  7. style.css +313 -5
.gitignore CHANGED
@@ -1 +1,219 @@
1
  settings.json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  settings.json
2
+
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[codz]
7
+ *$py.class
8
+
9
+ # C extensions
10
+ *.so
11
+
12
+ # Distribution / packaging
13
+ .Python
14
+ build/
15
+ develop-eggs/
16
+ dist/
17
+ downloads/
18
+ eggs/
19
+ .eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ share/python-wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+ MANIFEST
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ *.manifest
36
+ *.spec
37
+
38
+ # Installer logs
39
+ pip-log.txt
40
+ pip-delete-this-directory.txt
41
+
42
+ # Unit test / coverage reports
43
+ htmlcov/
44
+ .tox/
45
+ .nox/
46
+ .coverage
47
+ .coverage.*
48
+ .cache
49
+ nosetests.xml
50
+ coverage.xml
51
+ *.cover
52
+ *.py.cover
53
+ .hypothesis/
54
+ .pytest_cache/
55
+ cover/
56
+
57
+ # Translations
58
+ *.mo
59
+ *.pot
60
+
61
+ # Django stuff:
62
+ *.log
63
+ local_settings.py
64
+ db.sqlite3
65
+ db.sqlite3-journal
66
+
67
+ # Flask stuff:
68
+ instance/
69
+ .webassets-cache
70
+
71
+ # Scrapy stuff:
72
+ .scrapy
73
+
74
+ # Sphinx documentation
75
+ docs/_build/
76
+
77
+ # PyBuilder
78
+ .pybuilder/
79
+ target/
80
+
81
+ # Jupyter Notebook
82
+ .ipynb_checkpoints
83
+
84
+ # IPython
85
+ profile_default/
86
+ ipython_config.py
87
+
88
+ # pyenv
89
+ # For a library or package, you might want to ignore these files since the code is
90
+ # intended to run in multiple environments; otherwise, check them in:
91
+ # .python-version
92
+
93
+ # pipenv
94
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
96
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
97
+ # install all needed dependencies.
98
+ # Pipfile.lock
99
+
100
+ # UV
101
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
102
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
103
+ # commonly ignored for libraries.
104
+ # uv.lock
105
+
106
+ # poetry
107
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
108
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
109
+ # commonly ignored for libraries.
110
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
111
+ # poetry.lock
112
+ # poetry.toml
113
+
114
+ # pdm
115
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
116
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
117
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
118
+ # pdm.lock
119
+ # pdm.toml
120
+ .pdm-python
121
+ .pdm-build/
122
+
123
+ # pixi
124
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
125
+ # pixi.lock
126
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
127
+ # in the .venv directory. It is recommended not to include this directory in version control.
128
+ .pixi
129
+
130
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
131
+ __pypackages__/
132
+
133
+ # Celery stuff
134
+ celerybeat-schedule
135
+ celerybeat.pid
136
+
137
+ # Redis
138
+ *.rdb
139
+ *.aof
140
+ *.pid
141
+
142
+ # RabbitMQ
143
+ mnesia/
144
+ rabbitmq/
145
+ rabbitmq-data/
146
+
147
+ # ActiveMQ
148
+ activemq-data/
149
+
150
+ # SageMath parsed files
151
+ *.sage.py
152
+
153
+ # Environments
154
+ .env
155
+ .envrc
156
+ .venv
157
+ env/
158
+ venv/
159
+ ENV/
160
+ env.bak/
161
+ venv.bak/
162
+
163
+ # Spyder project settings
164
+ .spyderproject
165
+ .spyproject
166
+
167
+ # Rope project settings
168
+ .ropeproject
169
+
170
+ # mkdocs documentation
171
+ /site
172
+
173
+ # mypy
174
+ .mypy_cache/
175
+ .dmypy.json
176
+ dmypy.json
177
+
178
+ # Pyre type checker
179
+ .pyre/
180
+
181
+ # pytype static type analyzer
182
+ .pytype/
183
+
184
+ # Cython debug symbols
185
+ cython_debug/
186
+
187
+ # PyCharm
188
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
189
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
190
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
191
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
192
+ # .idea/
193
+
194
+ # Abstra
195
+ # Abstra is an AI-powered process automation framework.
196
+ # Ignore directories containing user credentials, local state, and settings.
197
+ # Learn more at https://abstra.io/docs
198
+ .abstra/
199
+
200
+ # Visual Studio Code
201
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
202
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
203
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
204
+ # you could uncomment the following to ignore the entire vscode folder
205
+ # .vscode/
206
+
207
+ # Ruff stuff:
208
+ .ruff_cache/
209
+
210
+ # PyPI configuration file
211
+ .pypirc
212
+
213
+ # Marimo
214
+ marimo/_static/
215
+ marimo/_lsp/
216
+ __marimo__/
217
+
218
+ # Streamlit
219
+ .streamlit/secrets.toml
backend/__pycache__/command.cpython-312.pyc DELETED
Binary file (4.72 kB)
 
backend/command.py CHANGED
@@ -143,7 +143,8 @@ def stream_command_center(client, model: str, messages: List[Dict]):
143
  yield {
144
  "type": "launch",
145
  "notebook_type": notebook_type,
146
- "initial_message": initial_message
 
147
  }
148
 
149
  # Add tool call to message history for context
 
143
  yield {
144
  "type": "launch",
145
  "notebook_type": notebook_type,
146
+ "initial_message": initial_message,
147
+ "tool_call_id": tool_call.id
148
  }
149
 
150
  # Add tool call to message history for context
backend/main.py CHANGED
@@ -6,6 +6,7 @@ from typing import List, Optional, Dict
6
  import json
7
  import httpx
8
  import os
 
9
 
10
  app = FastAPI(title="Productive API")
11
 
@@ -39,6 +40,14 @@ except ImportError:
39
  SANDBOXES: Dict[str, any] = {}
40
  SANDBOX_TIMEOUT = 300
41
 
 
 
 
 
 
 
 
 
42
  # CORS middleware for frontend connection
43
  app.add_middleware(
44
  CORSMiddleware,
@@ -167,6 +176,23 @@ Focus on being conversational, helpful, and easy to understand.
167
  }
168
 
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  class Message(BaseModel):
171
  role: str
172
  content: str
@@ -209,7 +235,8 @@ async def stream_code_notebook(
209
  token: Optional[str],
210
  model: str,
211
  e2b_key: str,
212
- session_id: str
 
213
  ):
214
  """Handle code notebook with execution capabilities"""
215
 
@@ -238,6 +265,9 @@ async def stream_code_notebook(
238
  {"role": "system", "content": system_prompt}
239
  ] + messages
240
 
 
 
 
241
  # Stream code execution
242
  for update in stream_code_execution(client, model, full_messages, sbx):
243
  # Forward updates to frontend
@@ -288,7 +318,8 @@ async def stream_research_notebook(
288
  serper_key: str,
289
  sub_agent_model: Optional[str] = None,
290
  parallel_workers: Optional[int] = None,
291
- max_websites: Optional[int] = None
 
292
  ):
293
  """Handle research notebook with web search"""
294
 
@@ -314,6 +345,10 @@ async def stream_research_notebook(
314
  # Get system prompt for research
315
  system_prompt = SYSTEM_PROMPTS.get("research", "")
316
 
 
 
 
 
317
  # Use sub-agent model if provided, otherwise fall back to main model
318
  analysis_model = sub_agent_model if sub_agent_model else model
319
 
@@ -338,31 +373,62 @@ async def stream_command_center_notebook(
338
  messages: List[dict],
339
  endpoint: str,
340
  token: Optional[str],
341
- model: str
 
342
  ):
343
  """Handle command center with tool-based notebook launching"""
344
 
345
  if not COMMAND_AVAILABLE:
346
  # Fallback to regular chat if command tools not available
347
- async for chunk in stream_chat_response(messages, endpoint, token, model, "command"):
348
  yield chunk
349
  return
350
 
 
 
351
  try:
352
  # Create OpenAI client
353
  client = OpenAI(base_url=endpoint, api_key=token)
354
 
 
 
 
 
355
  # Add system prompt for command center
356
  system_prompt = SYSTEM_PROMPTS["command"]
357
- full_messages = [
358
- {"role": "system", "content": system_prompt}
359
- ] + messages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
  # Stream command center execution
362
  for update in stream_command_center(client, model, full_messages):
363
  # Forward updates to frontend
364
  yield f"data: {json.dumps(update)}\n\n"
365
 
 
 
 
 
 
 
 
 
366
  except Exception as e:
367
  import traceback
368
  error_message = f"Command center error: {str(e)}\n{traceback.format_exc()}"
@@ -375,7 +441,8 @@ async def stream_chat_response(
375
  endpoint: str,
376
  token: Optional[str],
377
  model: str,
378
- notebook_type: str
 
379
  ):
380
  """Proxy stream from user's configured LLM endpoint"""
381
 
@@ -392,6 +459,9 @@ async def stream_chat_response(
392
  {"role": "system", "content": system_prompt}
393
  ] + messages
394
 
 
 
 
395
  # Handle Hugging Face endpoint with fallback to HF_TOKEN
396
  if not token and "huggingface.co" in endpoint:
397
  token = os.getenv("HF_TOKEN")
@@ -543,6 +613,9 @@ async def chat_stream(request: ChatRequest):
543
  # Convert Pydantic models to dicts
544
  messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
545
 
 
 
 
546
  # Route to code execution handler for code notebooks
547
  if request.notebook_type == "code":
548
  # Use notebook_id as session key, fallback to "default" if not provided
@@ -555,7 +628,8 @@ async def chat_stream(request: ChatRequest):
555
  request.token,
556
  request.model or "gpt-4",
557
  request.e2b_key or "",
558
- session_id
 
559
  ),
560
  media_type="text/event-stream",
561
  headers={
@@ -575,7 +649,9 @@ async def chat_stream(request: ChatRequest):
575
  request.model or "gpt-4",
576
  request.serper_key or "",
577
  request.research_sub_agent_model,
578
- request.research_parallel_workers
 
 
579
  ),
580
  media_type="text/event-stream",
581
  headers={
@@ -592,7 +668,8 @@ async def chat_stream(request: ChatRequest):
592
  messages,
593
  request.endpoint,
594
  request.token,
595
- request.model or "gpt-4"
 
596
  ),
597
  media_type="text/event-stream",
598
  headers={
@@ -609,7 +686,8 @@ async def chat_stream(request: ChatRequest):
609
  request.endpoint,
610
  request.token,
611
  request.model or "gpt-4",
612
- request.notebook_type
 
613
  ),
614
  media_type="text/event-stream",
615
  headers={
@@ -680,6 +758,40 @@ async def stop_sandbox(request: SandboxStopRequest):
680
  return {"success": True, "message": "No sandbox found for this session"}
681
 
682
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
  @app.get("/health")
684
  async def health():
685
  """Health check endpoint"""
 
6
  import json
7
  import httpx
8
  import os
9
+ from datetime import datetime
10
 
11
  app = FastAPI(title="Productive API")
12
 
 
40
  SANDBOXES: Dict[str, any] = {}
41
  SANDBOX_TIMEOUT = 300
42
 
43
+ # Debug: Store message history for debugging per tab
44
+ # Structure: {tab_id: [{call_number: int, timestamp: str, messages: List[dict]}]}
45
+ MESSAGE_HISTORY: Dict[str, List[Dict]] = {}
46
+
47
+ # Conversation history per tab (persistent across requests)
48
+ # Structure: {tab_id: [messages...]}
49
+ CONVERSATION_HISTORY: Dict[str, List[Dict]] = {}
50
+
51
  # CORS middleware for frontend connection
52
  app.add_middleware(
53
  CORSMiddleware,
 
176
  }
177
 
178
 
179
+ def record_api_call(tab_id: str, messages: List[dict]):
180
+ """Record an API call for debugging purposes"""
181
+ global MESSAGE_HISTORY
182
+
183
+ if tab_id not in MESSAGE_HISTORY:
184
+ MESSAGE_HISTORY[tab_id] = []
185
+
186
+ call_number = len(MESSAGE_HISTORY[tab_id]) + 1
187
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
188
+
189
+ MESSAGE_HISTORY[tab_id].append({
190
+ "call_number": call_number,
191
+ "timestamp": timestamp,
192
+ "messages": messages
193
+ })
194
+
195
+
196
  class Message(BaseModel):
197
  role: str
198
  content: str
 
235
  token: Optional[str],
236
  model: str,
237
  e2b_key: str,
238
+ session_id: str,
239
+ tab_id: str = "default"
240
  ):
241
  """Handle code notebook with execution capabilities"""
242
 
 
265
  {"role": "system", "content": system_prompt}
266
  ] + messages
267
 
268
+ # Store for debugging
269
+ record_api_call(tab_id, full_messages)
270
+
271
  # Stream code execution
272
  for update in stream_code_execution(client, model, full_messages, sbx):
273
  # Forward updates to frontend
 
318
  serper_key: str,
319
  sub_agent_model: Optional[str] = None,
320
  parallel_workers: Optional[int] = None,
321
+ max_websites: Optional[int] = None,
322
+ tab_id: str = "default"
323
  ):
324
  """Handle research notebook with web search"""
325
 
 
345
  # Get system prompt for research
346
  system_prompt = SYSTEM_PROMPTS.get("research", "")
347
 
348
+ # Store for debugging (simplified version for research)
349
+ full_messages = [{"role": "system", "content": system_prompt}] + messages
350
+ record_api_call(tab_id, full_messages)
351
+
352
  # Use sub-agent model if provided, otherwise fall back to main model
353
  analysis_model = sub_agent_model if sub_agent_model else model
354
 
 
373
  messages: List[dict],
374
  endpoint: str,
375
  token: Optional[str],
376
+ model: str,
377
+ tab_id: str = "0"
378
  ):
379
  """Handle command center with tool-based notebook launching"""
380
 
381
  if not COMMAND_AVAILABLE:
382
  # Fallback to regular chat if command tools not available
383
+ async for chunk in stream_chat_response(messages, endpoint, token, model, "command", tab_id):
384
  yield chunk
385
  return
386
 
387
+ global CONVERSATION_HISTORY
388
+
389
  try:
390
  # Create OpenAI client
391
  client = OpenAI(base_url=endpoint, api_key=token)
392
 
393
+ # Get or initialize conversation history for this tab
394
+ if tab_id not in CONVERSATION_HISTORY:
395
+ CONVERSATION_HISTORY[tab_id] = []
396
+
397
  # Add system prompt for command center
398
  system_prompt = SYSTEM_PROMPTS["command"]
399
+
400
+ # Build full messages: system + stored history + new messages
401
+ print(f"DEBUG: tab_id={tab_id}, incoming messages={messages}")
402
+ print(f"DEBUG: stored history length={len(CONVERSATION_HISTORY[tab_id])}")
403
+
404
+ # On first call, history is empty, so just use incoming messages
405
+ # On subsequent calls, append only new messages
406
+ if not CONVERSATION_HISTORY[tab_id]:
407
+ # First message - use all incoming messages
408
+ full_messages = [{"role": "system", "content": system_prompt}] + messages
409
+ print(f"DEBUG: First call, full_messages length={len(full_messages)}")
410
+ else:
411
+ # Subsequent messages - use stored history + incoming messages
412
+ # The incoming messages should only be the new user message
413
+ full_messages = [{"role": "system", "content": system_prompt}] + CONVERSATION_HISTORY[tab_id] + messages
414
+ print(f"DEBUG: Subsequent call, full_messages length={len(full_messages)}")
415
+
416
+ # Store for debugging
417
+ record_api_call(tab_id, full_messages)
418
 
419
  # Stream command center execution
420
  for update in stream_command_center(client, model, full_messages):
421
  # Forward updates to frontend
422
  yield f"data: {json.dumps(update)}\n\n"
423
 
424
+ # After streaming completes, update persistent conversation history
425
+ # Remove system prompt and filter out empty assistant messages
426
+ conversation_without_system = [
427
+ msg for msg in full_messages
428
+ if msg.get("role") != "system" and not (msg.get("role") == "assistant" and not msg.get("content") and not msg.get("tool_calls"))
429
+ ]
430
+ CONVERSATION_HISTORY[tab_id] = conversation_without_system
431
+
432
  except Exception as e:
433
  import traceback
434
  error_message = f"Command center error: {str(e)}\n{traceback.format_exc()}"
 
441
  endpoint: str,
442
  token: Optional[str],
443
  model: str,
444
+ notebook_type: str,
445
+ tab_id: str = "default"
446
  ):
447
  """Proxy stream from user's configured LLM endpoint"""
448
 
 
459
  {"role": "system", "content": system_prompt}
460
  ] + messages
461
 
462
+ # Store for debugging
463
+ record_api_call(tab_id, full_messages)
464
+
465
  # Handle Hugging Face endpoint with fallback to HF_TOKEN
466
  if not token and "huggingface.co" in endpoint:
467
  token = os.getenv("HF_TOKEN")
 
613
  # Convert Pydantic models to dicts
614
  messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
615
 
616
+ # Get tab_id for debugging
617
+ tab_id = request.notebook_id or "0"
618
+
619
  # Route to code execution handler for code notebooks
620
  if request.notebook_type == "code":
621
  # Use notebook_id as session key, fallback to "default" if not provided
 
628
  request.token,
629
  request.model or "gpt-4",
630
  request.e2b_key or "",
631
+ session_id,
632
+ tab_id
633
  ),
634
  media_type="text/event-stream",
635
  headers={
 
649
  request.model or "gpt-4",
650
  request.serper_key or "",
651
  request.research_sub_agent_model,
652
+ request.research_parallel_workers,
653
+ None,
654
+ tab_id
655
  ),
656
  media_type="text/event-stream",
657
  headers={
 
668
  messages,
669
  request.endpoint,
670
  request.token,
671
+ request.model or "gpt-4",
672
+ tab_id
673
  ),
674
  media_type="text/event-stream",
675
  headers={
 
686
  request.endpoint,
687
  request.token,
688
  request.model or "gpt-4",
689
+ request.notebook_type,
690
+ tab_id
691
  ),
692
  media_type="text/event-stream",
693
  headers={
 
758
  return {"success": True, "message": "No sandbox found for this session"}
759
 
760
 
761
+ @app.post("/api/conversation/add-tool-response")
762
+ async def add_tool_response(request: dict):
763
+ """Add a tool response to the conversation history when a notebook returns a result"""
764
+ global CONVERSATION_HISTORY
765
+
766
+ tab_id = request.get("tab_id", "0")
767
+ tool_call_id = request.get("tool_call_id")
768
+ content = request.get("content")
769
+
770
+ if not tool_call_id or not content:
771
+ return {"success": False, "error": "tool_call_id and content are required"}
772
+
773
+ # Initialize if needed
774
+ if tab_id not in CONVERSATION_HISTORY:
775
+ CONVERSATION_HISTORY[tab_id] = []
776
+
777
+ # Add tool response to conversation history
778
+ CONVERSATION_HISTORY[tab_id].append({
779
+ "role": "tool",
780
+ "tool_call_id": tool_call_id,
781
+ "content": content
782
+ })
783
+
784
+ return {"success": True}
785
+
786
+
787
+ @app.get("/api/debug/messages/{tab_id}")
788
+ async def get_debug_messages(tab_id: str):
789
+ """Get the message history for a specific tab for debugging"""
790
+ if tab_id in MESSAGE_HISTORY:
791
+ return {"calls": MESSAGE_HISTORY[tab_id]}
792
+ return {"calls": []}
793
+
794
+
795
  @app.get("/health")
796
  async def health():
797
  """Health check endpoint"""
index.html CHANGED
@@ -26,6 +26,7 @@
26
  </div>
27
  </div>
28
  <div class="tab-bar-spacer"></div>
 
29
  <button class="settings-btn" id="settingsBtn">SETTINGS</button>
30
  </div>
31
 
@@ -44,6 +45,7 @@
44
  <button class="launcher-btn" data-type="code">CODE</button>
45
  <button class="launcher-btn" data-type="research">RESEARCH</button>
46
  <button class="launcher-btn" data-type="chat">CHAT</button>
 
47
  </div>
48
  </div>
49
 
@@ -141,144 +143,160 @@
141
  </div>
142
  </div>
143
 
144
- <!-- Settings Tab -->
145
- <div class="tab-content" data-content-id="settings">
146
- <div class="settings-interface">
147
- <div class="settings-header">
148
- <h2>SETTINGS</h2>
149
- </div>
150
- <div class="settings-body">
151
- <div class="settings-section">
152
- <label class="settings-label">
153
- <span class="label-text">LLM ENDPOINT</span>
154
- <span class="label-description">OpenAI-compatible API endpoint (e.g., https://api.openai.com/v1)</span>
155
- </label>
156
- <input type="text" id="setting-endpoint" class="settings-input" placeholder="https://api.openai.com/v1">
157
- </div>
158
 
159
- <div class="settings-section">
160
- <label class="settings-label">
161
- <span class="label-text">API TOKEN (OPTIONAL)</span>
162
- <span class="label-description">Authentication token for the LLM API</span>
163
- </label>
164
- <input type="password" id="setting-token" class="settings-input" placeholder="Leave empty if not required">
165
- </div>
 
 
 
 
 
 
 
 
166
 
167
- <div class="settings-section">
168
- <label class="settings-label">
169
- <span class="label-text">MODEL</span>
170
- <span class="label-description">Default model name (e.g., gpt-4, gpt-3.5-turbo)</span>
171
- </label>
172
- <input type="text" id="setting-model" class="settings-input" placeholder="gpt-4">
173
- </div>
174
 
175
- <div class="settings-section">
176
- <label class="settings-label">
177
- <span class="label-text">E2B API KEY (OPTIONAL)</span>
178
- <span class="label-description">Required for code execution in CODE notebooks</span>
179
- </label>
180
- <input type="password" id="setting-e2b-key" class="settings-input" placeholder="Leave empty if not using code execution">
181
- </div>
182
 
183
- <div class="settings-section">
184
- <label class="settings-label">
185
- <span class="label-text">SERPER API KEY (OPTIONAL)</span>
186
- <span class="label-description">Required for web search in RESEARCH notebooks</span>
187
- </label>
188
- <input type="password" id="setting-serper-key" class="settings-input" placeholder="Leave empty if not using research">
189
- </div>
190
 
191
- <div class="settings-section">
192
- <label class="settings-label">
193
- <span class="label-text">NOTEBOOK-SPECIFIC MODELS (OPTIONAL)</span>
194
- <span class="label-description">Override default model for specific notebook types</span>
195
- </label>
196
- <div style="display: grid; grid-template-columns: auto 1fr; gap: 8px; align-items: center;">
197
- <label style="font-size: 11px; color: #666;">AGENT:</label>
198
- <input type="text" id="setting-model-agent" class="settings-input" placeholder="Use default">
199
 
200
- <label style="font-size: 11px; color: #666;">CODE:</label>
201
- <input type="text" id="setting-model-code" class="settings-input" placeholder="Use default">
 
 
 
 
 
 
202
 
203
- <label style="font-size: 11px; color: #666;">RESEARCH:</label>
204
- <input type="text" id="setting-model-research" class="settings-input" placeholder="Use default">
205
 
206
- <label style="font-size: 11px; color: #666;">CHAT:</label>
207
- <input type="text" id="setting-model-chat" class="settings-input" placeholder="Use default">
208
- </div>
209
- </div>
210
 
211
- <div class="settings-section">
212
- <label class="settings-label">
213
- <span class="label-text">RESEARCH SUB-AGENT MODEL (OPTIONAL)</span>
214
- <span class="label-description">Smaller/faster model for analyzing individual web pages during research</span>
215
- </label>
216
- <input type="text" id="setting-research-sub-agent-model" class="settings-input" placeholder="Use research model or default">
217
- </div>
218
 
219
- <div class="settings-section">
220
- <label class="settings-label">
221
- <span class="label-text">RESEARCH PARALLEL WORKERS (OPTIONAL)</span>
222
- <span class="label-description">Number of web pages to analyze in parallel (default: 8)</span>
223
- </label>
224
- <input type="number" id="setting-research-parallel-workers" class="settings-input" placeholder="8" min="1" max="20">
225
- </div>
226
 
227
- <div class="settings-section">
228
- <label class="settings-label">
229
- <span class="label-text">RESEARCH MAX WEBSITES (OPTIONAL)</span>
230
- <span class="label-description">Maximum number of websites to analyze per research session (default: 50)</span>
231
- </label>
232
- <input type="number" id="setting-research-max-websites" class="settings-input" placeholder="50" min="10" max="200">
233
- </div>
234
 
235
- <div class="settings-section">
236
- <label class="settings-label">
237
- <span class="label-text">THEME COLOR</span>
238
- <span class="label-description">Color theme for action widgets and accents</span>
239
- </label>
240
- <div class="theme-color-picker" id="theme-color-picker">
241
- <div class="theme-option" data-theme="forest">
242
- <div class="theme-preview" style="border-color: #1b5e20; background: #e8f5e9;"></div>
243
- <span class="theme-name">Forest</span>
244
- </div>
245
- <div class="theme-option" data-theme="sapphire">
246
- <div class="theme-preview" style="border-color: #0d47a1; background: #e3f2fd;"></div>
247
- <span class="theme-name">Sapphire</span>
248
- </div>
249
- <div class="theme-option" data-theme="ocean">
250
- <div class="theme-preview" style="border-color: #00796b; background: #e0f2f1;"></div>
251
- <span class="theme-name">Ocean</span>
252
- </div>
253
- <div class="theme-option" data-theme="midnight">
254
- <div class="theme-preview" style="border-color: #283593; background: #e8eaf6;"></div>
255
- <span class="theme-name">Midnight</span>
256
- </div>
257
- <div class="theme-option" data-theme="steel">
258
- <div class="theme-preview" style="border-color: #455a64; background: #eceff1;"></div>
259
- <span class="theme-name">Steel</span>
260
- </div>
261
- <div class="theme-option" data-theme="depths">
262
- <div class="theme-preview" style="border-color: #01579b; background: #e3f2fd;"></div>
263
- <span class="theme-name">Depths</span>
264
- </div>
265
- <div class="theme-option" data-theme="ember">
266
- <div class="theme-preview" style="border-color: #b71c1c; background: #fbe9e7;"></div>
267
- <span class="theme-name">Ember</span>
268
- </div>
269
- </div>
270
- <input type="hidden" id="setting-theme-color" value="forest">
271
- </div>
272
 
273
- <div class="settings-actions">
274
- <button class="settings-save-btn" id="saveSettingsBtn">SAVE</button>
275
- <button class="settings-cancel-btn" id="cancelSettingsBtn">CANCEL</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  </div>
277
-
278
- <div class="settings-status" id="settingsStatus"></div>
279
  </div>
 
 
 
 
 
 
280
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  </div>
 
282
  </div>
283
  </div>
284
 
 
26
  </div>
27
  </div>
28
  <div class="tab-bar-spacer"></div>
29
+ <button class="debug-btn" id="debugBtn">DEBUG</button>
30
  <button class="settings-btn" id="settingsBtn">SETTINGS</button>
31
  </div>
32
 
 
45
  <button class="launcher-btn" data-type="code">CODE</button>
46
  <button class="launcher-btn" data-type="research">RESEARCH</button>
47
  <button class="launcher-btn" data-type="chat">CHAT</button>
48
+ <button class="debug-btn" id="debugBtn">DEBUG</button>
49
  </div>
50
  </div>
51
 
 
143
  </div>
144
  </div>
145
 
146
+ </div>
147
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
+ <!-- Settings Panel -->
150
+ <div class="settings-panel" id="settingsPanel">
151
+ <div class="settings-panel-header">
152
+ <h3>SETTINGS</h3>
153
+ <button class="settings-panel-close" id="settingsPanelClose">×</button>
154
+ </div>
155
+ <div class="settings-panel-body" id="settingsPanelBody">
156
+ <div class="settings-body">
157
+ <div class="settings-section">
158
+ <label class="settings-label">
159
+ <span class="label-text">LLM ENDPOINT</span>
160
+ <span class="label-description">OpenAI-compatible API endpoint (e.g., https://api.openai.com/v1)</span>
161
+ </label>
162
+ <input type="text" id="setting-endpoint" class="settings-input" placeholder="https://api.openai.com/v1">
163
+ </div>
164
 
165
+ <div class="settings-section">
166
+ <label class="settings-label">
167
+ <span class="label-text">API TOKEN (OPTIONAL)</span>
168
+ <span class="label-description">Authentication token for the LLM API</span>
169
+ </label>
170
+ <input type="password" id="setting-token" class="settings-input" placeholder="Leave empty if not required">
171
+ </div>
172
 
173
+ <div class="settings-section">
174
+ <label class="settings-label">
175
+ <span class="label-text">MODEL</span>
176
+ <span class="label-description">Default model name (e.g., gpt-4, gpt-3.5-turbo)</span>
177
+ </label>
178
+ <input type="text" id="setting-model" class="settings-input" placeholder="gpt-4">
179
+ </div>
180
 
181
+ <div class="settings-section">
182
+ <label class="settings-label">
183
+ <span class="label-text">E2B API KEY (OPTIONAL)</span>
184
+ <span class="label-description">Required for code execution in CODE notebooks</span>
185
+ </label>
186
+ <input type="password" id="setting-e2b-key" class="settings-input" placeholder="Leave empty if not using code execution">
187
+ </div>
188
 
189
+ <div class="settings-section">
190
+ <label class="settings-label">
191
+ <span class="label-text">SERPER API KEY (OPTIONAL)</span>
192
+ <span class="label-description">Required for web search in RESEARCH notebooks</span>
193
+ </label>
194
+ <input type="password" id="setting-serper-key" class="settings-input" placeholder="Leave empty if not using research">
195
+ </div>
 
196
 
197
+ <div class="settings-section">
198
+ <label class="settings-label">
199
+ <span class="label-text">NOTEBOOK-SPECIFIC MODELS (OPTIONAL)</span>
200
+ <span class="label-description">Override default model for specific notebook types</span>
201
+ </label>
202
+ <div style="display: grid; grid-template-columns: auto 1fr; gap: 8px; align-items: center;">
203
+ <label style="font-size: 11px; color: #666;">AGENT:</label>
204
+ <input type="text" id="setting-model-agent" class="settings-input" placeholder="Use default">
205
 
206
+ <label style="font-size: 11px; color: #666;">CODE:</label>
207
+ <input type="text" id="setting-model-code" class="settings-input" placeholder="Use default">
208
 
209
+ <label style="font-size: 11px; color: #666;">RESEARCH:</label>
210
+ <input type="text" id="setting-model-research" class="settings-input" placeholder="Use default">
 
 
211
 
212
+ <label style="font-size: 11px; color: #666;">CHAT:</label>
213
+ <input type="text" id="setting-model-chat" class="settings-input" placeholder="Use default">
214
+ </div>
215
+ </div>
 
 
 
216
 
217
+ <div class="settings-section">
218
+ <label class="settings-label">
219
+ <span class="label-text">RESEARCH SUB-AGENT MODEL (OPTIONAL)</span>
220
+ <span class="label-description">Smaller/faster model for analyzing individual web pages during research</span>
221
+ </label>
222
+ <input type="text" id="setting-research-sub-agent-model" class="settings-input" placeholder="Use research model or default">
223
+ </div>
224
 
225
+ <div class="settings-section">
226
+ <label class="settings-label">
227
+ <span class="label-text">RESEARCH PARALLEL WORKERS (OPTIONAL)</span>
228
+ <span class="label-description">Number of web pages to analyze in parallel (default: 8)</span>
229
+ </label>
230
+ <input type="number" id="setting-research-parallel-workers" class="settings-input" placeholder="8" min="1" max="20">
231
+ </div>
232
 
233
+ <div class="settings-section">
234
+ <label class="settings-label">
235
+ <span class="label-text">RESEARCH MAX WEBSITES (OPTIONAL)</span>
236
+ <span class="label-description">Maximum number of websites to analyze per research session (default: 50)</span>
237
+ </label>
238
+ <input type="number" id="setting-research-max-websites" class="settings-input" placeholder="50" min="10" max="200">
239
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
+ <div class="settings-section">
242
+ <label class="settings-label">
243
+ <span class="label-text">THEME COLOR</span>
244
+ <span class="label-description">Color theme for action widgets and accents</span>
245
+ </label>
246
+ <div class="theme-color-picker" id="theme-color-picker">
247
+ <div class="theme-option" data-theme="forest">
248
+ <div class="theme-preview" style="border-color: #1b5e20; background: #e8f5e9;"></div>
249
+ <span class="theme-name">Forest</span>
250
+ </div>
251
+ <div class="theme-option" data-theme="sapphire">
252
+ <div class="theme-preview" style="border-color: #0d47a1; background: #e3f2fd;"></div>
253
+ <span class="theme-name">Sapphire</span>
254
+ </div>
255
+ <div class="theme-option" data-theme="ocean">
256
+ <div class="theme-preview" style="border-color: #00796b; background: #e0f2f1;"></div>
257
+ <span class="theme-name">Ocean</span>
258
+ </div>
259
+ <div class="theme-option" data-theme="midnight">
260
+ <div class="theme-preview" style="border-color: #283593; background: #e8eaf6;"></div>
261
+ <span class="theme-name">Midnight</span>
262
+ </div>
263
+ <div class="theme-option" data-theme="steel">
264
+ <div class="theme-preview" style="border-color: #455a64; background: #eceff1;"></div>
265
+ <span class="theme-name">Steel</span>
266
+ </div>
267
+ <div class="theme-option" data-theme="depths">
268
+ <div class="theme-preview" style="border-color: #01579b; background: #e3f2fd;"></div>
269
+ <span class="theme-name">Depths</span>
270
+ </div>
271
+ <div class="theme-option" data-theme="ember">
272
+ <div class="theme-preview" style="border-color: #b71c1c; background: #fbe9e7;"></div>
273
+ <span class="theme-name">Ember</span>
274
  </div>
 
 
275
  </div>
276
+ <input type="hidden" id="setting-theme-color" value="forest">
277
+ </div>
278
+
279
+ <div class="settings-actions">
280
+ <button class="settings-save-btn" id="saveSettingsBtn">SAVE</button>
281
+ <button class="settings-cancel-btn" id="cancelSettingsBtn">CANCEL</button>
282
  </div>
283
+
284
+ <div class="settings-status" id="settingsStatus"></div>
285
+ </div>
286
+ </div>
287
+ </div>
288
+
289
+ <!-- Debug Panel -->
290
+ <div class="debug-panel" id="debugPanel">
291
+ <div class="debug-header">
292
+ <h3>DEBUG: Message History</h3>
293
+ <button class="debug-close" id="debugClose">×</button>
294
+ </div>
295
+ <div class="debug-body">
296
+ <div class="debug-controls">
297
+ <button class="debug-refresh" id="debugRefresh">Refresh</button>
298
  </div>
299
+ <pre class="debug-content" id="debugContent">No message history available yet.</pre>
300
  </div>
301
  </div>
302
 
script.js CHANGED
@@ -18,6 +18,9 @@ let settings = {
18
  // Track action widgets for result updates (maps tabId -> widget element)
19
  const actionWidgets = {};
20
 
 
 
 
21
  // Track notebook counters for each type
22
  let notebookCounters = {
23
  'agent': 0,
@@ -42,6 +45,15 @@ document.addEventListener('DOMContentLoaded', () => {
42
  });
43
 
44
  function initializeEventListeners() {
 
 
 
 
 
 
 
 
 
45
  // Launcher buttons in command center
46
  document.querySelectorAll('.launcher-btn').forEach(btn => {
47
  btn.addEventListener('click', (e) => {
@@ -107,13 +119,8 @@ function initializeEventListeners() {
107
  }
108
  });
109
 
110
- // Settings button
111
- const settingsBtn = document.getElementById('settingsBtn');
112
- if (settingsBtn) {
113
- settingsBtn.addEventListener('click', () => {
114
- openSettings();
115
- });
116
- }
117
 
118
  // Save settings button
119
  const saveSettingsBtn = document.getElementById('saveSettingsBtn');
@@ -127,7 +134,12 @@ function initializeEventListeners() {
127
  const cancelSettingsBtn = document.getElementById('cancelSettingsBtn');
128
  if (cancelSettingsBtn) {
129
  cancelSettingsBtn.addEventListener('click', () => {
130
- switchToTab(0); // Go back to command center
 
 
 
 
 
131
  });
132
  }
133
 
@@ -337,7 +349,7 @@ async function sendMessage(tabId) {
337
  userMsg.className = 'message user';
338
  userMsg.innerHTML = `
339
  <div class="message-label">USER</div>
340
- <div class="message-content">${escapeHtml(message)}</div>
341
  `;
342
  chatContainer.appendChild(userMsg);
343
 
@@ -356,8 +368,16 @@ async function sendMessage(tabId) {
356
  // Determine notebook type from chat container ID
357
  const notebookType = getNotebookTypeFromContainer(chatContainer);
358
 
359
- // Get conversation history (including the just-added user message)
360
- const messages = getConversationHistory(chatContainer);
 
 
 
 
 
 
 
 
361
 
362
  // Stream response from backend
363
  await streamChatResponse(messages, chatContainer, notebookType, tabId);
@@ -438,8 +458,29 @@ function getConversationHistory(chatContainer) {
438
  if (label === 'user') {
439
  messages.push({ role: 'user', content: content });
440
  } else if (label === 'assistant') {
441
- // Don't include empty or special messages
442
- if (content.trim() && !content.includes('msg-')) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  messages.push({ role: 'assistant', content: content });
444
  }
445
  }
@@ -629,9 +670,13 @@ async function streamChatResponse(messages, chatContainer, notebookType, tabId)
629
  // Tool-based notebook launch from command center
630
  const notebookType = data.notebook_type;
631
  const initialMessage = data.initial_message;
 
632
 
 
633
  handleActionToken(notebookType, initialMessage, (targetTabId) => {
634
  showActionWidget(chatContainer, notebookType, initialMessage, targetTabId);
 
 
635
  });
636
 
637
  // Reset current message element so any subsequent thinking starts fresh
@@ -824,7 +869,7 @@ function showActionWidget(chatContainer, action, message, targetTabId) {
824
  actionWidgets[targetTabId] = widget;
825
  }
826
 
827
- function updateActionWidgetWithResult(tabId, resultContent, figures) {
828
  const widget = actionWidgets[tabId];
829
  if (!widget) return;
830
 
@@ -837,6 +882,27 @@ function updateActionWidgetWithResult(tabId, resultContent, figures) {
837
  statusText.textContent = statusText.textContent.replace('Opening', 'Completed').replace(/\.+$/, '');
838
  }
839
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
840
  // Replace <figure_x> tags with placeholders BEFORE markdown processing
841
  let processedContent = resultContent;
842
  const figurePlaceholders = {};
@@ -1136,9 +1202,6 @@ function openSettings() {
1136
  const status = document.getElementById('settingsStatus');
1137
  status.className = 'settings-status';
1138
  status.textContent = '';
1139
-
1140
- // Switch to settings view
1141
- switchToTab('settings');
1142
  }
1143
 
1144
  function saveSettings() {
@@ -1215,43 +1278,50 @@ const themeColors = {
1215
  border: '#1b5e20',
1216
  bg: '#e8f5e9',
1217
  hoverBg: '#c8e6c9',
1218
- accent: '#1b5e20'
 
1219
  },
1220
  sapphire: {
1221
  border: '#0d47a1',
1222
  bg: '#e3f2fd',
1223
  hoverBg: '#bbdefb',
1224
- accent: '#0d47a1'
 
1225
  },
1226
  ocean: {
1227
  border: '#00796b',
1228
  bg: '#e0f2f1',
1229
  hoverBg: '#b2dfdb',
1230
- accent: '#004d40'
 
1231
  },
1232
  midnight: {
1233
  border: '#283593',
1234
  bg: '#e8eaf6',
1235
  hoverBg: '#c5cae9',
1236
- accent: '#1a237e'
 
1237
  },
1238
  steel: {
1239
  border: '#455a64',
1240
  bg: '#eceff1',
1241
  hoverBg: '#cfd8dc',
1242
- accent: '#263238'
 
1243
  },
1244
  depths: {
1245
  border: '#01579b',
1246
  bg: '#e3f2fd',
1247
  hoverBg: '#bbdefb',
1248
- accent: '#01579b'
 
1249
  },
1250
  ember: {
1251
  border: '#b71c1c',
1252
  bg: '#fbe9e7',
1253
  hoverBg: '#ffccbc',
1254
- accent: '#b71c1c'
 
1255
  }
1256
  };
1257
 
@@ -1264,6 +1334,7 @@ function applyTheme(themeName) {
1264
  root.style.setProperty('--theme-bg', theme.bg);
1265
  root.style.setProperty('--theme-hover-bg', theme.hoverBg);
1266
  root.style.setProperty('--theme-accent', theme.accent);
 
1267
  }
1268
 
1269
  // Export settings for use in API calls
@@ -1387,3 +1458,129 @@ function openImageModal(src) {
1387
  modalImg.src = src;
1388
  modal.style.display = 'block';
1389
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  // Track action widgets for result updates (maps tabId -> widget element)
19
  const actionWidgets = {};
20
 
21
+ // Track tool call IDs for result updates (maps tabId -> tool_call_id)
22
+ const toolCallIds = {};
23
+
24
  // Track notebook counters for each type
25
  let notebookCounters = {
26
  'agent': 0,
 
45
  });
46
 
47
  function initializeEventListeners() {
48
+ // Constrain text selection to message-content only
49
+ document.addEventListener('selectstart', (e) => {
50
+ const target = e.target;
51
+ const messageContent = target.closest('.message-content');
52
+ if (!messageContent && target.closest('.message')) {
53
+ e.preventDefault();
54
+ }
55
+ });
56
+
57
  // Launcher buttons in command center
58
  document.querySelectorAll('.launcher-btn').forEach(btn => {
59
  btn.addEventListener('click', (e) => {
 
119
  }
120
  });
121
 
122
+ // Settings button - now handled in settings panel section below
123
+ // (removed old openSettings() call)
 
 
 
 
 
124
 
125
  // Save settings button
126
  const saveSettingsBtn = document.getElementById('saveSettingsBtn');
 
134
  const cancelSettingsBtn = document.getElementById('cancelSettingsBtn');
135
  if (cancelSettingsBtn) {
136
  cancelSettingsBtn.addEventListener('click', () => {
137
+ const settingsPanel = document.getElementById('settingsPanel');
138
+ const settingsBtn = document.getElementById('settingsBtn');
139
+ const appContainer = document.querySelector('.app-container');
140
+ if (settingsPanel) settingsPanel.classList.remove('active');
141
+ if (settingsBtn) settingsBtn.classList.remove('active');
142
+ if (appContainer) appContainer.classList.remove('panel-open');
143
  });
144
  }
145
 
 
349
  userMsg.className = 'message user';
350
  userMsg.innerHTML = `
351
  <div class="message-label">USER</div>
352
+ <div class="message-content">${escapeHtml(message.trim())}</div>
353
  `;
354
  chatContainer.appendChild(userMsg);
355
 
 
368
  // Determine notebook type from chat container ID
369
  const notebookType = getNotebookTypeFromContainer(chatContainer);
370
 
371
+ // For command center, only send the new user message (backend maintains history)
372
+ // For other notebooks, send full conversation history
373
+ let messages;
374
+ if (notebookType === 'command') {
375
+ // Only send the last user message
376
+ messages = [{ role: 'user', content: message }];
377
+ } else {
378
+ // Get full conversation history for other notebooks
379
+ messages = getConversationHistory(chatContainer);
380
+ }
381
 
382
  // Stream response from backend
383
  await streamChatResponse(messages, chatContainer, notebookType, tabId);
 
458
  if (label === 'user') {
459
  messages.push({ role: 'user', content: content });
460
  } else if (label === 'assistant') {
461
+ // Check if this message has a tool call
462
+ const toolCallData = msg.getAttribute('data-tool-call');
463
+ if (toolCallData) {
464
+ // This is a tool call message - add it in the proper format
465
+ const toolCall = JSON.parse(toolCallData);
466
+ messages.push({
467
+ role: 'assistant',
468
+ content: '',
469
+ tool_calls: [{
470
+ id: 'tool_' + Date.now(),
471
+ type: 'function',
472
+ function: {
473
+ name: `launch_${toolCall.notebook_type}_notebook`,
474
+ arguments: JSON.stringify({
475
+ task: toolCall.message,
476
+ topic: toolCall.message,
477
+ message: toolCall.message
478
+ })
479
+ }
480
+ }]
481
+ });
482
+ } else if (content.trim() && !content.includes('msg-') && !content.includes('→ Launched')) {
483
+ // Regular assistant message (exclude launch notifications)
484
  messages.push({ role: 'assistant', content: content });
485
  }
486
  }
 
670
  // Tool-based notebook launch from command center
671
  const notebookType = data.notebook_type;
672
  const initialMessage = data.initial_message;
673
+ const toolCallId = data.tool_call_id;
674
 
675
+ // The action widget will show the launch, no need for a visible assistant message
676
  handleActionToken(notebookType, initialMessage, (targetTabId) => {
677
  showActionWidget(chatContainer, notebookType, initialMessage, targetTabId);
678
+ // Store tool call ID for this notebook tab so we can send result back
679
+ toolCallIds[targetTabId] = toolCallId;
680
  });
681
 
682
  // Reset current message element so any subsequent thinking starts fresh
 
869
  actionWidgets[targetTabId] = widget;
870
  }
871
 
872
+ async function updateActionWidgetWithResult(tabId, resultContent, figures) {
873
  const widget = actionWidgets[tabId];
874
  if (!widget) return;
875
 
 
882
  statusText.textContent = statusText.textContent.replace('Opening', 'Completed').replace(/\.+$/, '');
883
  }
884
 
885
+ // Send result back to backend to update conversation history
886
+ const toolCallId = toolCallIds[tabId];
887
+ if (toolCallId) {
888
+ try {
889
+ // Get the command center tab ID (tab 0)
890
+ const commandTabId = '0';
891
+
892
+ await fetch('http://localhost:8000/api/conversation/add-tool-response', {
893
+ method: 'POST',
894
+ headers: { 'Content-Type': 'application/json' },
895
+ body: JSON.stringify({
896
+ tab_id: commandTabId,
897
+ tool_call_id: toolCallId,
898
+ content: resultContent
899
+ })
900
+ });
901
+ } catch (error) {
902
+ console.error('Failed to update conversation history with result:', error);
903
+ }
904
+ }
905
+
906
  // Replace <figure_x> tags with placeholders BEFORE markdown processing
907
  let processedContent = resultContent;
908
  const figurePlaceholders = {};
 
1202
  const status = document.getElementById('settingsStatus');
1203
  status.className = 'settings-status';
1204
  status.textContent = '';
 
 
 
1205
  }
1206
 
1207
  function saveSettings() {
 
1278
  border: '#1b5e20',
1279
  bg: '#e8f5e9',
1280
  hoverBg: '#c8e6c9',
1281
+ accent: '#1b5e20',
1282
+ accentRgb: '27, 94, 32'
1283
  },
1284
  sapphire: {
1285
  border: '#0d47a1',
1286
  bg: '#e3f2fd',
1287
  hoverBg: '#bbdefb',
1288
+ accent: '#0d47a1',
1289
+ accentRgb: '13, 71, 161'
1290
  },
1291
  ocean: {
1292
  border: '#00796b',
1293
  bg: '#e0f2f1',
1294
  hoverBg: '#b2dfdb',
1295
+ accent: '#004d40',
1296
+ accentRgb: '0, 77, 64'
1297
  },
1298
  midnight: {
1299
  border: '#283593',
1300
  bg: '#e8eaf6',
1301
  hoverBg: '#c5cae9',
1302
+ accent: '#1a237e',
1303
+ accentRgb: '26, 35, 126'
1304
  },
1305
  steel: {
1306
  border: '#455a64',
1307
  bg: '#eceff1',
1308
  hoverBg: '#cfd8dc',
1309
+ accent: '#263238',
1310
+ accentRgb: '38, 50, 56'
1311
  },
1312
  depths: {
1313
  border: '#01579b',
1314
  bg: '#e3f2fd',
1315
  hoverBg: '#bbdefb',
1316
+ accent: '#01579b',
1317
+ accentRgb: '1, 87, 155'
1318
  },
1319
  ember: {
1320
  border: '#b71c1c',
1321
  bg: '#fbe9e7',
1322
  hoverBg: '#ffccbc',
1323
+ accent: '#b71c1c',
1324
+ accentRgb: '183, 28, 28'
1325
  }
1326
  };
1327
 
 
1334
  root.style.setProperty('--theme-bg', theme.bg);
1335
  root.style.setProperty('--theme-hover-bg', theme.hoverBg);
1336
  root.style.setProperty('--theme-accent', theme.accent);
1337
+ root.style.setProperty('--theme-accent-rgb', theme.accentRgb);
1338
  }
1339
 
1340
  // Export settings for use in API calls
 
1458
  modalImg.src = src;
1459
  modal.style.display = 'block';
1460
  }
1461
+
1462
+ // ============= DEBUG PANEL =============
1463
+
1464
+ const debugPanel = document.getElementById('debugPanel');
1465
+ const debugBtn = document.getElementById('debugBtn');
1466
+ const debugClose = document.getElementById('debugClose');
1467
+ const debugRefresh = document.getElementById('debugRefresh');
1468
+ const debugContent = document.getElementById('debugContent');
1469
+
1470
+ // Toggle debug panel
1471
+ if (debugBtn) {
1472
+ debugBtn.addEventListener('click', () => {
1473
+ const isOpening = !debugPanel.classList.contains('active');
1474
+
1475
+ // Close settings panel if open
1476
+ settingsPanel.classList.remove('active');
1477
+ settingsBtn.classList.remove('active');
1478
+
1479
+ // Toggle debug panel
1480
+ debugPanel.classList.toggle('active');
1481
+ debugBtn.classList.toggle('active');
1482
+
1483
+ // Shift content when opening, unshift when closing
1484
+ if (isOpening) {
1485
+ appContainer.classList.add('panel-open');
1486
+ loadDebugMessages();
1487
+ } else {
1488
+ appContainer.classList.remove('panel-open');
1489
+ }
1490
+ });
1491
+ }
1492
+
1493
+ // Close debug panel
1494
+ if (debugClose) {
1495
+ debugClose.addEventListener('click', () => {
1496
+ debugPanel.classList.remove('active');
1497
+ debugBtn.classList.remove('active');
1498
+ appContainer.classList.remove('panel-open');
1499
+ });
1500
+ }
1501
+
1502
+ // Refresh debug messages
1503
+ if (debugRefresh) {
1504
+ debugRefresh.addEventListener('click', () => {
1505
+ loadDebugMessages();
1506
+ });
1507
+ }
1508
+
1509
+ // Load debug messages from backend
1510
+ async function loadDebugMessages() {
1511
+ try {
1512
+ debugContent.innerHTML = '<div style="padding: 10px; color: #666;">Loading...</div>';
1513
+
1514
+ // Get current active tab ID
1515
+ const activeTab = document.querySelector('.tab.active');
1516
+ const tabId = activeTab ? activeTab.dataset.tabId : '0';
1517
+
1518
+ const response = await fetch(`http://localhost:8000/api/debug/messages/${tabId}`);
1519
+
1520
+ if (!response.ok) {
1521
+ throw new Error(`HTTP error! status: ${response.status}`);
1522
+ }
1523
+
1524
+ const data = await response.json();
1525
+
1526
+ if (data.calls && data.calls.length > 0) {
1527
+ // Create collapsible UI for each API call
1528
+ let html = '';
1529
+ data.calls.forEach((call, index) => {
1530
+ const isExpanded = index === data.calls.length - 1; // Expand only the last call by default
1531
+ html += `<div class="debug-call-item"><div class="debug-call-header" onclick="toggleDebugCall(${index})"><span class="debug-call-arrow" id="arrow-${index}">${isExpanded ? '▼' : '▶'}</span><span class="debug-call-title">LLM Call #${call.call_number}</span><span class="debug-call-time">${call.timestamp}</span></div><pre class="debug-call-content" id="call-${index}" style="display: ${isExpanded ? 'block' : 'none'};">${JSON.stringify(call.messages, null, 2)}</pre></div>`;
1532
+ });
1533
+ debugContent.innerHTML = html;
1534
+ } else {
1535
+ debugContent.innerHTML = '<div style="padding: 10px; color: #666;">No message history available yet.<br><br>Send a message in this tab to see the message history here.</div>';
1536
+ }
1537
+ } catch (error) {
1538
+ console.error('Failed to load debug messages:', error);
1539
+ debugContent.innerHTML = `<div style="padding: 10px; color: #d32f2f;">Error loading debug messages: ${error.message}</div>`;
1540
+ }
1541
+ }
1542
+
1543
+ // Toggle debug call expansion
1544
+ window.toggleDebugCall = function(index) {
1545
+ const content = document.getElementById(`call-${index}`);
1546
+ const arrow = document.getElementById(`arrow-${index}`);
1547
+ if (content.style.display === 'none') {
1548
+ content.style.display = 'block';
1549
+ arrow.textContent = '▼';
1550
+ } else {
1551
+ content.style.display = 'none';
1552
+ arrow.textContent = '▶';
1553
+ }
1554
+ }
1555
+
1556
+ // ============= SETTINGS PANEL =============
1557
+
1558
+ const settingsPanel = document.getElementById('settingsPanel');
1559
+ const settingsPanelBody = document.getElementById('settingsPanelBody');
1560
+ const settingsPanelClose = document.getElementById('settingsPanelClose');
1561
+ const settingsBtn = document.getElementById('settingsBtn');
1562
+ const appContainer = document.querySelector('.app-container');
1563
+
1564
+
1565
+ // Open settings panel when SETTINGS button is clicked
1566
+ if (settingsBtn) {
1567
+ settingsBtn.addEventListener('click', () => {
1568
+ openSettings(); // Populate form fields with current values
1569
+ settingsPanel.classList.add('active');
1570
+ settingsBtn.classList.add('active');
1571
+ appContainer.classList.add('panel-open');
1572
+ // Close debug panel if open
1573
+ debugPanel.classList.remove('active');
1574
+ debugBtn.classList.remove('active');
1575
+ });
1576
+ }
1577
+
1578
+ // Close settings panel
1579
+ if (settingsPanelClose) {
1580
+ settingsPanelClose.addEventListener('click', () => {
1581
+ settingsPanel.classList.remove('active');
1582
+ settingsBtn.classList.remove('active');
1583
+ appContainer.classList.remove('panel-open');
1584
+ });
1585
+ }
1586
+
style.css CHANGED
@@ -10,6 +10,7 @@
10
  --theme-bg: #e8f5e9;
11
  --theme-hover-bg: #c8e6c9;
12
  --theme-accent: #1b5e20;
 
13
  }
14
 
15
  body {
@@ -20,6 +21,15 @@ body {
20
  overflow: hidden;
21
  }
22
 
 
 
 
 
 
 
 
 
 
23
  .app-container {
24
  display: flex;
25
  flex-direction: column;
@@ -54,7 +64,7 @@ body {
54
  display: flex;
55
  align-items: center;
56
  gap: 8px;
57
- padding: 12px 20px;
58
  background: #f5f5f5;
59
  color: #666;
60
  cursor: pointer;
@@ -124,7 +134,7 @@ body {
124
  color: #666;
125
  border: none;
126
  border-right: 1px solid #ccc;
127
- padding: 12px 20px;
128
  font-size: 12px;
129
  font-weight: 500;
130
  letter-spacing: 1px;
@@ -138,6 +148,11 @@ body {
138
  color: #1a1a1a;
139
  }
140
 
 
 
 
 
 
141
  .new-tab-wrapper {
142
  position: static;
143
  display: flex;
@@ -149,7 +164,7 @@ body {
149
  color: #666;
150
  border: none;
151
  border-right: 1px solid #ccc;
152
- padding: 12px 20px;
153
  font-size: 16px;
154
  cursor: pointer;
155
  transition: all 0.2s;
@@ -202,6 +217,11 @@ body {
202
  overflow: hidden;
203
  position: relative;
204
  z-index: 1;
 
 
 
 
 
205
  }
206
 
207
  .tab-content {
@@ -468,7 +488,19 @@ body {
468
  margin-bottom: 16px;
469
  display: flex;
470
  flex-direction: column;
471
- gap: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
472
  }
473
 
474
  .system-message {
@@ -487,6 +519,12 @@ body {
487
  color: #666;
488
  text-transform: uppercase;
489
  letter-spacing: 1px;
 
 
 
 
 
 
490
  }
491
 
492
  .message-content {
@@ -497,6 +535,33 @@ body {
497
  line-height: 1.6;
498
  white-space: pre-wrap;
499
  word-wrap: break-word;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  }
501
 
502
  .message-content ul,
@@ -532,10 +597,14 @@ body {
532
  }
533
 
534
  .message-content p {
535
- margin-bottom: 8px;
536
  white-space: pre-wrap;
537
  }
538
 
 
 
 
 
539
  .message-content code {
540
  background: #e0e0e0;
541
  padding: 2px 6px;
@@ -1754,3 +1823,242 @@ body {
1754
  font-family: 'JetBrains Mono', monospace;
1755
  font-size: 12px;
1756
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  --theme-bg: #e8f5e9;
11
  --theme-hover-bg: #c8e6c9;
12
  --theme-accent: #1b5e20;
13
+ --theme-accent-rgb: 27, 94, 32;
14
  }
15
 
16
  body {
 
21
  overflow: hidden;
22
  }
23
 
24
+ /* Global selection override - disable everywhere by default */
25
+ *::selection {
26
+ background: transparent;
27
+ }
28
+
29
+ *::-moz-selection {
30
+ background: transparent;
31
+ }
32
+
33
  .app-container {
34
  display: flex;
35
  flex-direction: column;
 
64
  display: flex;
65
  align-items: center;
66
  gap: 8px;
67
+ padding: 8px 16px;
68
  background: #f5f5f5;
69
  color: #666;
70
  cursor: pointer;
 
134
  color: #666;
135
  border: none;
136
  border-right: 1px solid #ccc;
137
+ padding: 8px 16px;
138
  font-size: 12px;
139
  font-weight: 500;
140
  letter-spacing: 1px;
 
148
  color: #1a1a1a;
149
  }
150
 
151
+ .settings-btn.active {
152
+ background: var(--theme-accent);
153
+ color: white;
154
+ }
155
+
156
  .new-tab-wrapper {
157
  position: static;
158
  display: flex;
 
164
  color: #666;
165
  border: none;
166
  border-right: 1px solid #ccc;
167
+ padding: 8px 16px;
168
  font-size: 16px;
169
  cursor: pointer;
170
  transition: all 0.2s;
 
217
  overflow: hidden;
218
  position: relative;
219
  z-index: 1;
220
+ transition: margin-right 0.3s ease;
221
+ }
222
+
223
+ .panel-open .content-area {
224
+ margin-right: 600px;
225
  }
226
 
227
  .tab-content {
 
488
  margin-bottom: 16px;
489
  display: flex;
490
  flex-direction: column;
491
+ gap: 2px;
492
+ user-select: none;
493
+ -webkit-user-select: none;
494
+ -moz-user-select: none;
495
+ -ms-user-select: none;
496
+ }
497
+
498
+ .message::selection {
499
+ background: transparent;
500
+ }
501
+
502
+ .message > *:not(.message-content)::selection {
503
+ background: transparent;
504
  }
505
 
506
  .system-message {
 
519
  color: #666;
520
  text-transform: uppercase;
521
  letter-spacing: 1px;
522
+ user-select: none;
523
+ padding: 2px 0;
524
+ }
525
+
526
+ .user .message-label {
527
+ color: var(--theme-accent);
528
  }
529
 
530
  .message-content {
 
535
  line-height: 1.6;
536
  white-space: pre-wrap;
537
  word-wrap: break-word;
538
+ user-select: text;
539
+ -webkit-user-select: text;
540
+ -moz-user-select: text;
541
+ -ms-user-select: text;
542
+ }
543
+
544
+ .message-content::selection {
545
+ background: rgba(var(--theme-accent-rgb), 0.2) !important;
546
+ }
547
+
548
+ .message-content::-moz-selection {
549
+ background: rgba(var(--theme-accent-rgb), 0.2) !important;
550
+ }
551
+
552
+ .message-content *::selection {
553
+ background: rgba(var(--theme-accent-rgb), 0.2) !important;
554
+ }
555
+
556
+ .message-content *::-moz-selection {
557
+ background: rgba(var(--theme-accent-rgb), 0.2) !important;
558
+ }
559
+
560
+ .message-content * {
561
+ user-select: text;
562
+ -webkit-user-select: text;
563
+ -moz-user-select: text;
564
+ -ms-user-select: text;
565
  }
566
 
567
  .message-content ul,
 
597
  }
598
 
599
  .message-content p {
600
+ margin: 0;
601
  white-space: pre-wrap;
602
  }
603
 
604
+ .message-content p:not(:last-child) {
605
+ margin-bottom: 8px;
606
+ }
607
+
608
  .message-content code {
609
  background: #e0e0e0;
610
  padding: 2px 6px;
 
1823
  font-family: 'JetBrains Mono', monospace;
1824
  font-size: 12px;
1825
  }
1826
+
1827
+ /* Debug Panel */
1828
+ .debug-panel {
1829
+ position: fixed;
1830
+ top: 37px;
1831
+ right: -600px;
1832
+ width: 600px;
1833
+ height: calc(100vh - 37px);
1834
+ background: white;
1835
+ border-left: 2px solid var(--theme-accent);
1836
+ z-index: 1000;
1837
+ display: flex;
1838
+ flex-direction: column;
1839
+ transition: right 0.3s ease;
1840
+ }
1841
+
1842
+ .debug-panel.active {
1843
+ right: 0;
1844
+ }
1845
+
1846
+ .debug-header {
1847
+ padding: 8px 16px;
1848
+ border-bottom: 1px solid #e0e0e0;
1849
+ display: flex;
1850
+ justify-content: space-between;
1851
+ align-items: center;
1852
+ background: var(--theme-accent);
1853
+ }
1854
+
1855
+ .debug-header h3 {
1856
+ margin: 0;
1857
+ font-size: 12px;
1858
+ font-weight: 600;
1859
+ color: white;
1860
+ text-transform: uppercase;
1861
+ letter-spacing: 0.5px;
1862
+ }
1863
+
1864
+ .debug-close {
1865
+ background: none;
1866
+ border: none;
1867
+ font-size: 20px;
1868
+ color: white;
1869
+ cursor: pointer;
1870
+ padding: 0;
1871
+ width: 24px;
1872
+ height: 24px;
1873
+ display: flex;
1874
+ align-items: center;
1875
+ justify-content: center;
1876
+ border-radius: 4px;
1877
+ transition: background 0.2s;
1878
+ }
1879
+
1880
+ .debug-close:hover {
1881
+ background: rgba(255, 255, 255, 0.2);
1882
+ }
1883
+
1884
+ .debug-body {
1885
+ flex: 1;
1886
+ overflow: hidden;
1887
+ display: flex;
1888
+ flex-direction: column;
1889
+ }
1890
+
1891
+ .debug-controls {
1892
+ padding: 12px 20px;
1893
+ border-bottom: 1px solid #e0e0e0;
1894
+ background: #f9f9f9;
1895
+ }
1896
+
1897
+ .debug-refresh {
1898
+ padding: 6px 12px;
1899
+ background: var(--theme-accent);
1900
+ color: white;
1901
+ border: none;
1902
+ border-radius: 4px;
1903
+ font-family: 'JetBrains Mono', monospace;
1904
+ font-size: 11px;
1905
+ font-weight: 600;
1906
+ cursor: pointer;
1907
+ text-transform: uppercase;
1908
+ letter-spacing: 0.5px;
1909
+ transition: opacity 0.2s;
1910
+ }
1911
+
1912
+ .debug-refresh:hover {
1913
+ opacity: 0.9;
1914
+ }
1915
+
1916
+ .debug-content {
1917
+ flex: 1;
1918
+ padding: 0;
1919
+ margin: 0;
1920
+ overflow: auto;
1921
+ font-family: 'JetBrains Mono', monospace;
1922
+ font-size: 12px;
1923
+ line-height: 1.6;
1924
+ background: white;
1925
+ }
1926
+
1927
+ .debug-call-item {
1928
+ border-bottom: 1px solid #e0e0e0;
1929
+ background: white;
1930
+ }
1931
+
1932
+ .debug-call-header {
1933
+ padding: 10px 16px;
1934
+ background: white;
1935
+ cursor: pointer;
1936
+ display: flex;
1937
+ align-items: center;
1938
+ gap: 10px;
1939
+ transition: background 0.2s;
1940
+ user-select: none;
1941
+ }
1942
+
1943
+ .debug-call-header:hover {
1944
+ background: #f5f5f5;
1945
+ }
1946
+
1947
+ .debug-call-arrow {
1948
+ color: #666;
1949
+ font-size: 10px;
1950
+ width: 12px;
1951
+ display: inline-block;
1952
+ }
1953
+
1954
+ .debug-call-title {
1955
+ font-weight: 600;
1956
+ color: #333;
1957
+ flex: 1;
1958
+ }
1959
+
1960
+ .debug-call-time {
1961
+ color: #999;
1962
+ font-size: 10px;
1963
+ }
1964
+
1965
+ .debug-call-content {
1966
+ margin: 0;
1967
+ padding: 12px 16px;
1968
+ background: #fafafa;
1969
+ border-top: 1px solid #e0e0e0;
1970
+ white-space: pre-wrap;
1971
+ word-wrap: break-word;
1972
+ overflow-x: auto;
1973
+ }
1974
+
1975
+ .debug-btn {
1976
+ background: #f5f5f5;
1977
+ color: #666;
1978
+ border: none;
1979
+ border-right: 1px solid #ccc;
1980
+ padding: 8px 16px;
1981
+ font-size: 12px;
1982
+ font-weight: 500;
1983
+ letter-spacing: 1px;
1984
+ cursor: pointer;
1985
+ transition: all 0.2s;
1986
+ font-family: inherit;
1987
+ }
1988
+
1989
+ .debug-btn:hover {
1990
+ background: #eee;
1991
+ color: #1a1a1a;
1992
+ }
1993
+
1994
+ .debug-btn.active {
1995
+ background: var(--theme-accent);
1996
+ color: white;
1997
+ }
1998
+
1999
+ /* Settings Panel (side panel like debug) */
2000
+ .settings-panel {
2001
+ position: fixed;
2002
+ top: 37px;
2003
+ right: -600px;
2004
+ width: 600px;
2005
+ height: calc(100vh - 37px);
2006
+ background: white;
2007
+ border-left: 2px solid var(--theme-accent);
2008
+ z-index: 1000;
2009
+ display: flex;
2010
+ flex-direction: column;
2011
+ transition: right 0.3s ease;
2012
+ overflow-y: auto;
2013
+ }
2014
+
2015
+ .settings-panel.active {
2016
+ right: 0;
2017
+ }
2018
+
2019
+ .settings-panel-header {
2020
+ padding: 8px 16px;
2021
+ border-bottom: 1px solid #e0e0e0;
2022
+ display: flex;
2023
+ justify-content: space-between;
2024
+ align-items: center;
2025
+ background: var(--theme-accent);
2026
+ position: sticky;
2027
+ top: 0;
2028
+ z-index: 1;
2029
+ }
2030
+
2031
+ .settings-panel-header h3 {
2032
+ margin: 0;
2033
+ font-size: 12px;
2034
+ font-weight: 600;
2035
+ color: white;
2036
+ text-transform: uppercase;
2037
+ letter-spacing: 0.5px;
2038
+ }
2039
+
2040
+ .settings-panel-close {
2041
+ background: none;
2042
+ border: none;
2043
+ font-size: 20px;
2044
+ color: white;
2045
+ cursor: pointer;
2046
+ padding: 0;
2047
+ width: 24px;
2048
+ height: 24px;
2049
+ display: flex;
2050
+ align-items: center;
2051
+ justify-content: center;
2052
+ border-radius: 4px;
2053
+ transition: background 0.2s;
2054
+ }
2055
+
2056
+ .settings-panel-close:hover {
2057
+ background: rgba(255, 255, 255, 0.2);
2058
+ }
2059
+
2060
+ .settings-panel-body {
2061
+ flex: 1;
2062
+ padding: 20px;
2063
+ overflow-y: auto;
2064
+ }