Upload folder using huggingface_hub
Browse files- src/eurus/retrieval.py +57 -2
- web/agent_wrapper.py +28 -3
- web/static/css/style.css +68 -0
- web/static/js/chat.js +103 -0
- web/templates/index.html +1 -1
src/eurus/retrieval.py
CHANGED
|
@@ -31,6 +31,45 @@ from eurus.memory import get_memory
|
|
| 31 |
logger = logging.getLogger(__name__)
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
def _format_coord(value: float) -> str:
|
| 35 |
"""Format coordinates for stable, filename-safe identifiers."""
|
| 36 |
if abs(value) < 0.005:
|
|
@@ -448,10 +487,18 @@ def retrieve_era5_data(
|
|
| 448 |
# Size guard β prevent downloading datasets larger than the configured limit
|
| 449 |
estimated_gb = ds_out.nbytes / (1024 ** 3)
|
| 450 |
if estimated_gb > CONFIG.max_download_size_gb:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
return (
|
| 452 |
f"Error: Estimated download size ({estimated_gb:.1f} GB) exceeds the "
|
| 453 |
f"{CONFIG.max_download_size_gb} GB limit.\n"
|
| 454 |
-
f"Try narrowing the time range or spatial area."
|
|
|
|
|
|
|
| 455 |
)
|
| 456 |
if estimated_gb > 1.0:
|
| 457 |
logger.info(
|
|
@@ -527,6 +574,12 @@ def retrieve_era5_data(
|
|
| 527 |
logger.info(f"Retrying in {wait_time:.1f}s...")
|
| 528 |
time.sleep(wait_time)
|
| 529 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
return (
|
| 531 |
f"Error: Failed after {CONFIG.max_retries} attempts.\n"
|
| 532 |
f"Last error: {error_msg}\n\n"
|
|
@@ -534,7 +587,9 @@ def retrieve_era5_data(
|
|
| 534 |
f"1. Check your ARRAYLAKE_API_KEY\n"
|
| 535 |
f"2. Verify internet connection\n"
|
| 536 |
f"3. Try a smaller date range or region\n"
|
| 537 |
-
f"4. Check if variable '{short_var}' is available"
|
|
|
|
|
|
|
| 538 |
)
|
| 539 |
|
| 540 |
return "Error: Unexpected failure in retrieval logic."
|
|
|
|
| 31 |
logger = logging.getLogger(__name__)
|
| 32 |
|
| 33 |
|
| 34 |
+
def _arraylake_snippet(
|
| 35 |
+
variable: str,
|
| 36 |
+
query_type: str,
|
| 37 |
+
start_date: str,
|
| 38 |
+
end_date: str,
|
| 39 |
+
min_lat: float,
|
| 40 |
+
max_lat: float,
|
| 41 |
+
min_lon: float,
|
| 42 |
+
max_lon: float,
|
| 43 |
+
) -> str:
|
| 44 |
+
"""Generate a ready-to-paste Python snippet for direct Arraylake access."""
|
| 45 |
+
# Convert negative lons to 0-360 for ERA5
|
| 46 |
+
era5_min = min_lon % 360 if min_lon < 0 else min_lon
|
| 47 |
+
era5_max = max_lon % 360 if max_lon < 0 else max_lon
|
| 48 |
+
return (
|
| 49 |
+
f"\nπ¦ Reproduce this download yourself (copy-paste into Jupyter):\n"
|
| 50 |
+
f"```python\n"
|
| 51 |
+
f"import os, xarray as xr\n"
|
| 52 |
+
f"from arraylake import Client\n"
|
| 53 |
+
f"\n"
|
| 54 |
+
f"client = Client(token=os.environ['ARRAYLAKE_API_KEY'])\n"
|
| 55 |
+
f"repo = client.get_repo('{CONFIG.data_source}')\n"
|
| 56 |
+
f"session = repo.readonly_session('main')\n"
|
| 57 |
+
f"\n"
|
| 58 |
+
f"ds = xr.open_dataset(session.store, engine='zarr',\n"
|
| 59 |
+
f" consolidated=False, zarr_format=3,\n"
|
| 60 |
+
f" chunks=None, group='{query_type}')\n"
|
| 61 |
+
f"\n"
|
| 62 |
+
f"subset = ds['{variable}'].sel(\n"
|
| 63 |
+
f" time=slice('{start_date}', '{end_date}'),\n"
|
| 64 |
+
f" latitude=slice({max_lat}, {min_lat}), # ERA5: descending\n"
|
| 65 |
+
f" longitude=slice({era5_min}, {era5_max}),\n"
|
| 66 |
+
f")\n"
|
| 67 |
+
f"\n"
|
| 68 |
+
f"subset.load().to_dataset(name='{variable}').to_zarr('my_data.zarr', mode='w')\n"
|
| 69 |
+
f"```"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
def _format_coord(value: float) -> str:
|
| 74 |
"""Format coordinates for stable, filename-safe identifiers."""
|
| 75 |
if abs(value) < 0.005:
|
|
|
|
| 487 |
# Size guard β prevent downloading datasets larger than the configured limit
|
| 488 |
estimated_gb = ds_out.nbytes / (1024 ** 3)
|
| 489 |
if estimated_gb > CONFIG.max_download_size_gb:
|
| 490 |
+
snippet = _arraylake_snippet(
|
| 491 |
+
short_var, query_type, start_date, end_date,
|
| 492 |
+
min_latitude, max_latitude,
|
| 493 |
+
min_longitude if min_longitude >= 0 else min_longitude % 360,
|
| 494 |
+
max_longitude if max_longitude >= 0 else max_longitude % 360,
|
| 495 |
+
)
|
| 496 |
return (
|
| 497 |
f"Error: Estimated download size ({estimated_gb:.1f} GB) exceeds the "
|
| 498 |
f"{CONFIG.max_download_size_gb} GB limit.\n"
|
| 499 |
+
f"Try narrowing the time range or spatial area.\n\n"
|
| 500 |
+
f"Alternatively, fetch it yourself with this snippet:\n\n"
|
| 501 |
+
f"{snippet}"
|
| 502 |
)
|
| 503 |
if estimated_gb > 1.0:
|
| 504 |
logger.info(
|
|
|
|
| 574 |
logger.info(f"Retrying in {wait_time:.1f}s...")
|
| 575 |
time.sleep(wait_time)
|
| 576 |
else:
|
| 577 |
+
snippet = _arraylake_snippet(
|
| 578 |
+
short_var, query_type, start_date, end_date,
|
| 579 |
+
min_latitude, max_latitude,
|
| 580 |
+
min_longitude if min_longitude >= 0 else min_longitude % 360,
|
| 581 |
+
max_longitude if max_longitude >= 0 else max_longitude % 360,
|
| 582 |
+
)
|
| 583 |
return (
|
| 584 |
f"Error: Failed after {CONFIG.max_retries} attempts.\n"
|
| 585 |
f"Last error: {error_msg}\n\n"
|
|
|
|
| 587 |
f"1. Check your ARRAYLAKE_API_KEY\n"
|
| 588 |
f"2. Verify internet connection\n"
|
| 589 |
f"3. Try a smaller date range or region\n"
|
| 590 |
+
f"4. Check if variable '{short_var}' is available\n\n"
|
| 591 |
+
f"Manual retrieval snippet:\n\n"
|
| 592 |
+
f"{snippet}"
|
| 593 |
)
|
| 594 |
|
| 595 |
return "Error: Unexpected failure in retrieval logic."
|
web/agent_wrapper.py
CHANGED
|
@@ -25,6 +25,7 @@ from langchain.agents import create_agent
|
|
| 25 |
|
| 26 |
# IMPORT FROM EURUS PACKAGE - SINGLE SOURCE OF TRUTH
|
| 27 |
from eurus.config import CONFIG, AGENT_SYSTEM_PROMPT
|
|
|
|
| 28 |
from eurus.memory import get_memory, SmartConversationMemory # Singleton for datasets, per-session for chat
|
| 29 |
from eurus.tools import get_all_tools
|
| 30 |
from eurus.tools.repl import PythonREPLTool
|
|
@@ -181,12 +182,14 @@ class AgentSession:
|
|
| 181 |
lambda: self._agent.invoke({"messages": self._messages}, config=config)
|
| 182 |
)
|
| 183 |
|
| 184 |
-
#
|
|
|
|
| 185 |
self._messages = result["messages"]
|
|
|
|
| 186 |
|
| 187 |
-
# Parse messages to show tool calls made
|
| 188 |
tool_calls_made = []
|
| 189 |
-
for msg in
|
| 190 |
if hasattr(msg, 'tool_calls') and msg.tool_calls:
|
| 191 |
for tc in msg.tool_calls:
|
| 192 |
tool_name = tc.get('name', 'unknown')
|
|
@@ -198,6 +201,24 @@ class AgentSession:
|
|
| 198 |
await stream_callback("status", f"π οΈ Used tools: {tools_str}")
|
| 199 |
await asyncio.sleep(0.5)
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
# Extract response
|
| 202 |
last_message = self._messages[-1]
|
| 203 |
|
|
@@ -244,6 +265,10 @@ class AgentSession:
|
|
| 244 |
# Default to plot (png, jpg, etc.)
|
| 245 |
await stream_callback("plot", "", data=base64_data, path=filepath, code=code)
|
| 246 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
# Save to memory
|
| 248 |
self._conversation.add_message("assistant", response_text)
|
| 249 |
|
|
|
|
| 25 |
|
| 26 |
# IMPORT FROM EURUS PACKAGE - SINGLE SOURCE OF TRUTH
|
| 27 |
from eurus.config import CONFIG, AGENT_SYSTEM_PROMPT
|
| 28 |
+
from eurus.retrieval import _arraylake_snippet
|
| 29 |
from eurus.memory import get_memory, SmartConversationMemory # Singleton for datasets, per-session for chat
|
| 30 |
from eurus.tools import get_all_tools
|
| 31 |
from eurus.tools.repl import PythonREPLTool
|
|
|
|
| 182 |
lambda: self._agent.invoke({"messages": self._messages}, config=config)
|
| 183 |
)
|
| 184 |
|
| 185 |
+
# Only scan NEW messages from this turn
|
| 186 |
+
prev_count = len(self._messages)
|
| 187 |
self._messages = result["messages"]
|
| 188 |
+
new_messages = self._messages[prev_count:]
|
| 189 |
|
| 190 |
+
# Parse NEW messages to show tool calls made
|
| 191 |
tool_calls_made = []
|
| 192 |
+
for msg in new_messages:
|
| 193 |
if hasattr(msg, 'tool_calls') and msg.tool_calls:
|
| 194 |
for tc in msg.tool_calls:
|
| 195 |
tool_name = tc.get('name', 'unknown')
|
|
|
|
| 201 |
await stream_callback("status", f"π οΈ Used tools: {tools_str}")
|
| 202 |
await asyncio.sleep(0.5)
|
| 203 |
|
| 204 |
+
# Collect Arraylake snippet from NEW messages only
|
| 205 |
+
arraylake_snippets = []
|
| 206 |
+
for msg in new_messages:
|
| 207 |
+
if hasattr(msg, 'tool_calls') and msg.tool_calls:
|
| 208 |
+
for tc in msg.tool_calls:
|
| 209 |
+
if tc.get('name') == 'retrieve_era5_data':
|
| 210 |
+
args = tc.get('args', {})
|
| 211 |
+
arraylake_snippets.append(_arraylake_snippet(
|
| 212 |
+
variable=args.get('variable_id', 'sst'),
|
| 213 |
+
query_type=args.get('query_type', 'spatial'),
|
| 214 |
+
start_date=args.get('start_date', ''),
|
| 215 |
+
end_date=args.get('end_date', ''),
|
| 216 |
+
min_lat=args.get('min_latitude', -90),
|
| 217 |
+
max_lat=args.get('max_latitude', 90),
|
| 218 |
+
min_lon=args.get('min_longitude', 0),
|
| 219 |
+
max_lon=args.get('max_longitude', 360),
|
| 220 |
+
))
|
| 221 |
+
|
| 222 |
# Extract response
|
| 223 |
last_message = self._messages[-1]
|
| 224 |
|
|
|
|
| 265 |
# Default to plot (png, jpg, etc.)
|
| 266 |
await stream_callback("plot", "", data=base64_data, path=filepath, code=code)
|
| 267 |
|
| 268 |
+
# Send Arraylake snippets AFTER response + plots exist in DOM
|
| 269 |
+
for snippet in arraylake_snippets:
|
| 270 |
+
await stream_callback("arraylake_snippet", snippet)
|
| 271 |
+
|
| 272 |
# Save to memory
|
| 273 |
self._conversation.add_message("assistant", response_text)
|
| 274 |
|
web/static/css/style.css
CHANGED
|
@@ -880,4 +880,72 @@ dialog .close-modal:hover {
|
|
| 880 |
.save-keys-btn:disabled {
|
| 881 |
opacity: 0.5;
|
| 882 |
cursor: not-allowed;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
}
|
|
|
|
| 880 |
.save-keys-btn:disabled {
|
| 881 |
opacity: 0.5;
|
| 882 |
cursor: not-allowed;
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
/* ===== ARRAYLAKE SNIPPET (in-message) ===== */
|
| 886 |
+
.arraylake-snippet-section {
|
| 887 |
+
margin-top: 0.75rem;
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
.arraylake-actions {
|
| 891 |
+
display: flex;
|
| 892 |
+
gap: 0.5rem;
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
.arraylake-btn {
|
| 896 |
+
padding: 0.5rem 0.875rem;
|
| 897 |
+
font-size: 0.75rem;
|
| 898 |
+
font-weight: 500;
|
| 899 |
+
border: 1px solid var(--glass-border);
|
| 900 |
+
border-radius: 0.375rem;
|
| 901 |
+
background: var(--bg-tertiary);
|
| 902 |
+
color: var(--text-secondary);
|
| 903 |
+
cursor: pointer;
|
| 904 |
+
transition: all 0.15s ease;
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
.arraylake-btn:hover {
|
| 908 |
+
border-color: var(--accent-primary);
|
| 909 |
+
color: var(--accent-primary);
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
.arraylake-code {
|
| 913 |
+
margin-top: 0.5rem;
|
| 914 |
+
border-radius: 0.5rem;
|
| 915 |
+
overflow: hidden;
|
| 916 |
+
border: 1px solid var(--glass-border);
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
.arraylake-code pre {
|
| 920 |
+
margin: 0;
|
| 921 |
+
padding: 0.875rem;
|
| 922 |
+
background: var(--code-bg);
|
| 923 |
+
overflow-x: auto;
|
| 924 |
+
font-size: 0.8125rem;
|
| 925 |
+
line-height: 1.5;
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
.arraylake-code code {
|
| 929 |
+
font-family: 'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace;
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
.copy-snippet-btn {
|
| 933 |
+
display: block;
|
| 934 |
+
margin: 0;
|
| 935 |
+
padding: 0.5rem 0.875rem;
|
| 936 |
+
width: 100%;
|
| 937 |
+
font-size: 0.75rem;
|
| 938 |
+
font-weight: 500;
|
| 939 |
+
border: none;
|
| 940 |
+
border-top: 1px solid var(--glass-border);
|
| 941 |
+
background: var(--bg-tertiary);
|
| 942 |
+
color: var(--text-secondary);
|
| 943 |
+
cursor: pointer;
|
| 944 |
+
transition: all 0.15s ease;
|
| 945 |
+
text-align: center;
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
.copy-snippet-btn:hover {
|
| 949 |
+
color: var(--accent-primary);
|
| 950 |
+
background: var(--bg-secondary);
|
| 951 |
}
|
web/static/js/chat.js
CHANGED
|
@@ -430,6 +430,10 @@ class EurusChat {
|
|
| 430 |
this.sendBtn.disabled = false;
|
| 431 |
break;
|
| 432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
case 'error':
|
| 434 |
this.showError(data.content);
|
| 435 |
this.sendBtn.disabled = false;
|
|
@@ -677,6 +681,105 @@ class EurusChat {
|
|
| 677 |
this.currentAssistantMessage = null;
|
| 678 |
}
|
| 679 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 680 |
showError(message) {
|
| 681 |
this.removeThinkingIndicator();
|
| 682 |
|
|
|
|
| 430 |
this.sendBtn.disabled = false;
|
| 431 |
break;
|
| 432 |
|
| 433 |
+
case 'arraylake_snippet':
|
| 434 |
+
this.addArraylakeSnippet(data.content);
|
| 435 |
+
break;
|
| 436 |
+
|
| 437 |
case 'error':
|
| 438 |
this.showError(data.content);
|
| 439 |
this.sendBtn.disabled = false;
|
|
|
|
| 681 |
this.currentAssistantMessage = null;
|
| 682 |
}
|
| 683 |
|
| 684 |
+
addArraylakeSnippet(snippetText) {
|
| 685 |
+
// Find the latest assistant message
|
| 686 |
+
const messages = this.messagesContainer.querySelectorAll('.assistant-message');
|
| 687 |
+
const targetMsg = messages.length > 0 ? messages[messages.length - 1] : null;
|
| 688 |
+
if (!targetMsg) return;
|
| 689 |
+
|
| 690 |
+
// Strip markdown fences for raw code display
|
| 691 |
+
let cleanCode = snippetText
|
| 692 |
+
.replace(/^\n?π¦[^\n]*\n/, '')
|
| 693 |
+
.replace(/^```python\n?/, '')
|
| 694 |
+
.replace(/\n?```$/, '')
|
| 695 |
+
.trim();
|
| 696 |
+
|
| 697 |
+
// Find the last plot figure's action bar to add button inline
|
| 698 |
+
const figures = targetMsg.querySelectorAll('.plot-figure');
|
| 699 |
+
const lastFigure = figures.length > 0 ? figures[figures.length - 1] : null;
|
| 700 |
+
const actionsDiv = lastFigure ? lastFigure.querySelector('.plot-actions') : null;
|
| 701 |
+
|
| 702 |
+
if (actionsDiv) {
|
| 703 |
+
// Add button inline with Enlarge/Download/Show Code
|
| 704 |
+
const btn = document.createElement('button');
|
| 705 |
+
btn.className = 'code-btn';
|
| 706 |
+
btn.title = 'Arraylake Code';
|
| 707 |
+
btn.textContent = 'π¦ Arraylake Code';
|
| 708 |
+
actionsDiv.appendChild(btn);
|
| 709 |
+
|
| 710 |
+
// Add code block to figure (same pattern as Show Code)
|
| 711 |
+
const codeDiv = document.createElement('div');
|
| 712 |
+
codeDiv.className = 'plot-code';
|
| 713 |
+
codeDiv.style.display = 'none';
|
| 714 |
+
|
| 715 |
+
const pre = document.createElement('pre');
|
| 716 |
+
const codeEl = document.createElement('code');
|
| 717 |
+
codeEl.className = 'language-python hljs';
|
| 718 |
+
try {
|
| 719 |
+
codeEl.innerHTML = hljs.highlight(cleanCode, { language: 'python' }).value;
|
| 720 |
+
} catch (e) {
|
| 721 |
+
codeEl.textContent = cleanCode;
|
| 722 |
+
}
|
| 723 |
+
pre.appendChild(codeEl);
|
| 724 |
+
codeDiv.appendChild(pre);
|
| 725 |
+
|
| 726 |
+
// Copy button inside code block
|
| 727 |
+
const copyBtn = document.createElement('button');
|
| 728 |
+
copyBtn.className = 'copy-snippet-btn';
|
| 729 |
+
copyBtn.textContent = 'Copy Code';
|
| 730 |
+
copyBtn.addEventListener('click', () => {
|
| 731 |
+
navigator.clipboard.writeText(cleanCode).then(() => {
|
| 732 |
+
copyBtn.textContent = 'β Copied!';
|
| 733 |
+
setTimeout(() => copyBtn.textContent = 'Copy Code', 2000);
|
| 734 |
+
});
|
| 735 |
+
});
|
| 736 |
+
codeDiv.appendChild(copyBtn);
|
| 737 |
+
lastFigure.appendChild(codeDiv);
|
| 738 |
+
|
| 739 |
+
// Toggle
|
| 740 |
+
btn.addEventListener('click', () => {
|
| 741 |
+
if (codeDiv.style.display === 'none') {
|
| 742 |
+
codeDiv.style.display = 'block';
|
| 743 |
+
btn.textContent = 'π¦ Hide Arraylake';
|
| 744 |
+
} else {
|
| 745 |
+
codeDiv.style.display = 'none';
|
| 746 |
+
btn.textContent = 'π¦ Arraylake Code';
|
| 747 |
+
}
|
| 748 |
+
});
|
| 749 |
+
} else {
|
| 750 |
+
// No plot figure β add as standalone section under the message
|
| 751 |
+
const wrapper = document.createElement('div');
|
| 752 |
+
wrapper.className = 'arraylake-snippet-section';
|
| 753 |
+
wrapper.innerHTML = `
|
| 754 |
+
<div class="plot-actions">
|
| 755 |
+
<button class="code-btn" title="Arraylake Code">π¦ Arraylake Code</button>
|
| 756 |
+
</div>
|
| 757 |
+
<div class="plot-code" style="display: none;">
|
| 758 |
+
<pre><code class="language-python hljs"></code></pre>
|
| 759 |
+
</div>
|
| 760 |
+
`;
|
| 761 |
+
const codeEl = wrapper.querySelector('code');
|
| 762 |
+
try {
|
| 763 |
+
codeEl.innerHTML = hljs.highlight(cleanCode, { language: 'python' }).value;
|
| 764 |
+
} catch (e) {
|
| 765 |
+
codeEl.textContent = cleanCode;
|
| 766 |
+
}
|
| 767 |
+
const btn = wrapper.querySelector('.code-btn');
|
| 768 |
+
const codeDiv = wrapper.querySelector('.plot-code');
|
| 769 |
+
btn.addEventListener('click', () => {
|
| 770 |
+
if (codeDiv.style.display === 'none') {
|
| 771 |
+
codeDiv.style.display = 'block';
|
| 772 |
+
btn.textContent = 'π¦ Hide Arraylake';
|
| 773 |
+
} else {
|
| 774 |
+
codeDiv.style.display = 'none';
|
| 775 |
+
btn.textContent = 'π¦ Arraylake Code';
|
| 776 |
+
}
|
| 777 |
+
});
|
| 778 |
+
targetMsg.appendChild(wrapper);
|
| 779 |
+
}
|
| 780 |
+
this.scrollToBottom();
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
showError(message) {
|
| 784 |
this.removeThinkingIndicator();
|
| 785 |
|
web/templates/index.html
CHANGED
|
@@ -60,5 +60,5 @@
|
|
| 60 |
{% endblock %}
|
| 61 |
|
| 62 |
{% block scripts %}
|
| 63 |
-
<script src="/static/js/chat.js?v=
|
| 64 |
{% endblock %}
|
|
|
|
| 60 |
{% endblock %}
|
| 61 |
|
| 62 |
{% block scripts %}
|
| 63 |
+
<script src="/static/js/chat.js?v=20260219b"></script>
|
| 64 |
{% endblock %}
|