Team Ai
Apppublic

openenv-testing/echo_env

sourceHugging Faceupdated 11mo agoView on Hugging Face
0likes
web_interface.py1614 linesDownload Raw Back to env_server
1# Copyright (c) Meta Platforms, Inc. and affiliates.2# All rights reserved.3#4# This source code is licensed under the BSD-style license found in the5# LICENSE file in the root directory of this source tree.6 7"""8Web interface for OpenEnv environments.9 10This module provides a web-based interface for interacting with OpenEnv environments,11including a two-pane layout for HumanAgent interaction and state observation.12"""13 14from __future__ import annotations15 16import json17import time18from dataclasses import asdict, dataclass19from typing import Any, Dict, List, Optional, Type20from datetime import datetime21 22from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request23from fastapi.responses import HTMLResponse, FileResponse24from fastapi.staticfiles import StaticFiles25from pydantic import BaseModel26 27from .interfaces import Environment28from .types import Action, Observation, State, EnvironmentMetadata29 30 31def load_environment_metadata(env: Environment, env_name: Optional[str] = None) -> EnvironmentMetadata:32    """33    Load environment metadata including README content.34    35    Args:36        env: The environment instance37        env_name: Optional environment name for README file lookup38        39    Returns:40        EnvironmentMetadata with loaded information41    """42    # Try to get metadata from environment if it has a method for it43    if hasattr(env, 'get_metadata'):44        return env.get_metadata()45    46    # Default metadata47    metadata = EnvironmentMetadata(48        name=env_name or env.__class__.__name__,49        description=f"{env.__class__.__name__} environment",50        version="1.0.0"51    )52    53    # Try to load README from file system54    readme_content = _load_readme_from_filesystem(env_name)55    if readme_content:56        metadata.readme_content = readme_content57    58    return metadata59 60 61def _load_readme_from_filesystem(env_name: Optional[str]) -> Optional[str]:62    """63    Load README content from the filesystem.64    65    Tries multiple locations:66    1. Container filesystem: /app/README.md67    2. Local development: src/envs/{env_name}/README.md68    3. Environment variable: ENV_README_PATH69    """70    import os71    from pathlib import Path72    73    # Try container filesystem first74    container_readme = Path("/app/README.md")75    if container_readme.exists():76        try:77            return container_readme.read_text(encoding='utf-8')78        except Exception:79            pass80    81    # Try environment variable path82    custom_path = os.environ.get("ENV_README_PATH")83    if custom_path and Path(custom_path).exists():84        try:85            return Path(custom_path).read_text(encoding='utf-8')86        except Exception:87            pass88    89    # Try local development path90    if env_name:91        local_readme = Path(f"src/envs/{env_name}/README.md")92        if local_readme.exists():93            try:94                return local_readme.read_text(encoding='utf-8')95            except Exception:96                pass97    98    return None99 100 101@dataclass102class ActionLog:103    """Log entry for an action taken."""104    timestamp: str105    action: Dict[str, Any]106    observation: Dict[str, Any]107    reward: Optional[float]108    done: bool109    step_count: int110 111 112@dataclass113class EpisodeState:114    """Current episode state for the web interface."""115    episode_id: Optional[str]116    step_count: int117    current_observation: Optional[Dict[str, Any]]118    action_logs: List[ActionLog]119    is_reset: bool = True120 121 122class WebInterfaceManager:123    """Manages the web interface for an environment."""124    125    def __init__(126        self,127        env: Environment,128        action_cls: Type[Action],129        observation_cls: Type[Observation],130        metadata: Optional[EnvironmentMetadata] = None,131    ):132        self.env = env133        self.action_cls = action_cls134        self.observation_cls = observation_cls135        self.metadata = metadata or EnvironmentMetadata(136            name=env.__class__.__name__,137            description=f"{env.__class__.__name__} environment"138        )139        self.episode_state = EpisodeState(140            episode_id=None,141            step_count=0,142            current_observation=None,143            action_logs=[]144        )145        self.connected_clients: List[WebSocket] = []146    147    async def connect_websocket(self, websocket: WebSocket):148        """Connect a new WebSocket client."""149        await websocket.accept()150        self.connected_clients.append(websocket)151        152        # Send current state to the new client153        await self._send_state_update()154    155    async def disconnect_websocket(self, websocket: WebSocket):156        """Disconnect a WebSocket client."""157        if websocket in self.connected_clients:158            self.connected_clients.remove(websocket)159    160    async def _send_state_update(self):161        """Send current state to all connected clients."""162        if not self.connected_clients:163            return164            165        state_data = {166            "type": "state_update",167            "episode_state": asdict(self.episode_state)168        }169        170        # Send to all connected clients171        disconnected_clients = []172        for client in self.connected_clients:173            try:174                await client.send_text(json.dumps(state_data))175            except:176                disconnected_clients.append(client)177        178        # Remove disconnected clients179        for client in disconnected_clients:180            self.connected_clients.remove(client)181    182    async def reset_environment(self) -> Dict[str, Any]:183        """Reset the environment and update state."""184        observation = self.env.reset()185        state = self.env.state186        187        # Update episode state188        self.episode_state.episode_id = state.episode_id189        self.episode_state.step_count = 0190        self.episode_state.current_observation = asdict(observation)191        self.episode_state.action_logs = []192        self.episode_state.is_reset = True193        194        # Send state update195        await self._send_state_update()196        197        return {198            "observation": asdict(observation),199            "reward": observation.reward,200            "done": observation.done,201        }202    203    async def step_environment(self, action_data: Dict[str, Any]) -> Dict[str, Any]:204        """Execute a step in the environment and update state."""205        # Deserialize action206        action = self._deserialize_action(action_data)207        208        # Execute step209        observation = self.env.step(action)210        state = self.env.state211        212        # Create action log213        action_log = ActionLog(214            timestamp=datetime.now().isoformat(),215            action=asdict(action),216            observation=asdict(observation),217            reward=observation.reward,218            done=observation.done,219            step_count=state.step_count220        )221        222        # Update episode state223        self.episode_state.episode_id = state.episode_id224        self.episode_state.step_count = state.step_count225        self.episode_state.current_observation = asdict(observation)226        self.episode_state.action_logs.append(action_log)227        self.episode_state.is_reset = False228        229        # Send state update230        await self._send_state_update()231        232        return {233            "observation": asdict(observation),234            "reward": observation.reward,235            "done": observation.done,236        }237    238    def get_state(self) -> Dict[str, Any]:239        """Get current environment state."""240        state = self.env.state241        return asdict(state)242    243    def _deserialize_action(self, action_data: Dict[str, Any]) -> Action:244        """Convert JSON dict to Action instance."""245        metadata = action_data.pop("metadata", {})246        247        # Handle tensor fields that come from JSON as lists248        processed_data = {}249        for key, value in action_data.items():250            if key == "tokens" and isinstance(value, (list, str)):251                # Convert list or string to tensor252                if isinstance(value, str):253                    # If it's a string, try to parse it as a list of numbers254                    try:255                        import json256                        value = json.loads(value)257                    except:258                        # If parsing fails, treat as empty list259                        value = []260                if isinstance(value, list):261                    import torch262                    processed_data[key] = torch.tensor(value, dtype=torch.long)263                else:264                    processed_data[key] = value265            elif key == "action_id" and isinstance(value, str):266                # Convert action_id from string to int267                try:268                    processed_data[key] = int(value)269                except ValueError:270                    # If conversion fails, keep original value271                    processed_data[key] = value272            else:273                processed_data[key] = value274        275        action = self.action_cls(**processed_data)276        action.metadata = metadata277        return action278 279 280def create_web_interface_app(281    env: Environment,282    action_cls: Type[Action],283    observation_cls: Type[Observation],284    env_name: Optional[str] = None,285) -> FastAPI:286    """287    Create a FastAPI application with web interface for the given environment.288    289    Args:290        env: The Environment instance to serve291        action_cls: The Action subclass this environment expects292        observation_cls: The Observation subclass this environment returns293        env_name: Optional environment name for README loading294        295    Returns:296        FastAPI application instance with web interface297    """298    from .http_server import create_fastapi_app299    300    # Create the base environment app301    app = create_fastapi_app(env, action_cls, observation_cls)302    303    # Load environment metadata304    metadata = load_environment_metadata(env, env_name)305    306    # Create web interface manager307    web_manager = WebInterfaceManager(env, action_cls, observation_cls, metadata)308    309    # Add web interface routes310    @app.get("/web", response_class=HTMLResponse)311    async def web_interface():312        """Serve the web interface."""313        return get_web_interface_html(action_cls, web_manager.metadata)314    315    @app.get("/web/metadata")316    async def web_metadata():317        """Get environment metadata."""318        return asdict(web_manager.metadata)319    320    @app.websocket("/ws")321    async def websocket_endpoint(websocket: WebSocket):322        """WebSocket endpoint for real-time updates."""323        await web_manager.connect_websocket(websocket)324        try:325            while True:326                # Keep connection alive327                await websocket.receive_text()328        except WebSocketDisconnect:329            await web_manager.disconnect_websocket(websocket)330    331    @app.post("/web/reset")332    async def web_reset():333        """Reset endpoint for web interface."""334        return await web_manager.reset_environment()335    336    @app.post("/web/step")337    async def web_step(request: Dict[str, Any]):338        """Step endpoint for web interface."""339        # Check if this is a message-based request (chat environment)340        if "message" in request:341            message = request["message"]342            # Convert message to action using the environment's message_to_action method343            action = web_manager.env.message_to_action(message)344            action_data = {"tokens": action.tokens.tolist()}345        else:346            action_data = request.get("action", {})347        348        return await web_manager.step_environment(action_data)349    350    @app.get("/web/state")351    async def web_state():352        """State endpoint for web interface."""353        return web_manager.get_state()354    355    return app356 357 358def get_web_interface_html(action_cls: Type[Action], metadata: Optional[EnvironmentMetadata] = None) -> str:359    """Generate the HTML for the web interface."""360    361    # Check if this is a chat environment by looking for tokens field362    is_chat_env = False363    if hasattr(action_cls, '__dataclass_fields__'):364        for field_name, field_info in action_cls.__dataclass_fields__.items():365            if field_name == 'tokens' and hasattr(field_info.type, '__name__') and 'Tensor' in field_info.type.__name__:366                is_chat_env = True367                break368    369    # Get action fields for dynamic form generation with enhanced metadata370    action_fields = _extract_action_fields(action_cls)371    372    return f"""373<!DOCTYPE html>374<html lang="en">375<head>376    <meta charset="UTF-8">377    <meta name="viewport" content="width=device-width, initial-scale=1.0">378    <title>OpenEnv Web Interface</title>379    <style>380        * {{381            margin: 0;382            padding: 0;383            box-sizing: border-box;384        }}385        386        body {{387            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;388            background-color: #f5f5f5;389            height: 100vh;390            overflow: hidden;391        }}392        393        .container {{394            display: flex;395            height: 100vh;396        }}397        398        .left-pane {{399            width: 50%;400            background: white;401            border-right: 1px solid #e0e0e0;402            display: flex;403            flex-direction: column;404        }}405        406        .right-pane {{407            width: 50%;408            background: #fafafa;409            display: flex;410            flex-direction: column;411        }}412        413        .pane-header {{414            padding: 20px;415            border-bottom: 1px solid #e0e0e0;416            background: #f8f9fa;417            font-weight: 600;418            font-size: 16px;419        }}420        421        .pane-content {{422            flex: 1;423            padding: 20px;424            overflow-y: auto;425        }}426        427        .action-form {{428            background: white;429            border: 1px solid #e0e0e0;430            border-radius: 8px;431            padding: 20px;432            margin-bottom: 20px;433        }}434        435        .form-group {{436            margin-bottom: 15px;437        }}438        439        .form-group label {{440            display: block;441            margin-bottom: 5px;442            font-weight: 500;443            color: #333;444        }}445        446        .form-group input, .form-group textarea {{447            width: 100%;448            padding: 8px 12px;449            border: 1px solid #ddd;450            border-radius: 4px;451            font-size: 14px;452        }}453        454        .form-group input:focus, .form-group textarea:focus {{455            outline: none;456            border-color: #007bff;457            box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);458        }}459        460        .btn {{461            background: #007bff;462            color: white;463            border: none;464            padding: 10px 20px;465            border-radius: 4px;466            cursor: pointer;467            font-size: 14px;468            margin-right: 10px;469            margin-bottom: 10px;470        }}471        472        .btn:hover {{473            background: #0056b3;474        }}475        476        .btn:disabled {{477            background: #6c757d;478            cursor: not-allowed;479        }}480        481        .btn-secondary {{482            background: #6c757d;483        }}484        485        .btn-secondary:hover {{486            background: #545b62;487        }}488        489        .state-display {{490            background: white;491            border: 1px solid #e0e0e0;492            border-radius: 8px;493            padding: 15px;494            margin-bottom: 20px;495        }}496        497        .state-item {{498            margin-bottom: 8px;499        }}500        501        .state-label {{502            font-weight: 500;503            color: #666;504        }}505        506        .state-value {{507            color: #333;508            font-family: monospace;509        }}510        511        .logs-container {{512            background: white;513            border: 1px solid #e0e0e0;514            border-radius: 8px;515            padding: 15px;516            max-height: 400px;517            overflow-y: auto;518        }}519        520        .log-entry {{521            border-bottom: 1px solid #f0f0f0;522            padding: 10px 0;523        }}524        525        .log-entry:last-child {{526            border-bottom: none;527        }}528        529        .log-timestamp {{530            font-size: 12px;531            color: #666;532            margin-bottom: 5px;533        }}534        535        .log-action {{536            background: #e3f2fd;537            padding: 8px;538            border-radius: 4px;539            margin-bottom: 5px;540            font-family: monospace;541            font-size: 12px;542        }}543        544        .log-observation {{545            background: #f3e5f5;546            padding: 8px;547            border-radius: 4px;548            font-family: monospace;549            font-size: 12px;550        }}551        552        .log-reward {{553            font-weight: 600;554            color: #28a745;555        }}556        557        .log-done {{558            font-weight: 600;559            color: #dc3545;560        }}561        562        .status-indicator {{563            display: inline-block;564            width: 8px;565            height: 8px;566            border-radius: 50%;567            margin-right: 8px;568        }}569        570        .status-connected {{571            background: #28a745;572        }}573        574        .status-disconnected {{575            background: #dc3545;576        }}577        578        .json-display {{579            background: #f8f9fa;580            border: 1px solid #e9ecef;581            border-radius: 4px;582            padding: 10px;583            font-family: monospace;584            font-size: 12px;585            white-space: pre-wrap;586            max-height: 200px;587            overflow-y: auto;588        }}589        590        /* Chat Interface Styles */591        .chat-interface {{592            background: white;593            border: 1px solid #e0e0e0;594            border-radius: 8px;595            padding: 20px;596            margin-bottom: 20px;597        }}598        599        .chat-messages {{600            background: #f8f9fa;601            border: 1px solid #e0e0e0;602            border-radius: 8px;603            padding: 15px;604            margin-bottom: 15px;605            max-height: 400px;606            overflow-y: auto;607        }}608        609        .chat-message {{610            margin-bottom: 15px;611            padding: 10px;612            border-radius: 8px;613        }}614        615        .chat-message:last-child {{616            margin-bottom: 0;617        }}618        619        .chat-message.user {{620            background: #e3f2fd;621            margin-left: 20px;622        }}623        624        .chat-message.assistant {{625            background: #f3e5f5;626            margin-right: 20px;627        }}628        629        .chat-message.system {{630            background: #e8f5e8;631            font-style: italic;632        }}633        634        .message-role {{635            font-weight: 600;636            font-size: 12px;637            color: #666;638            margin-bottom: 5px;639        }}640        641        .message-content {{642            font-size: 14px;643            line-height: 1.4;644        }}645        646        .chat-input-container {{647            border-top: 1px solid #e0e0e0;648            padding-top: 15px;649        }}650        651        .role-selector {{652            margin-bottom: 10px;653        }}654        655        .role-selector label {{656            font-weight: 500;657            margin-right: 10px;658        }}659        660        .role-selector select {{661            padding: 5px 10px;662            border: 1px solid #ddd;663            border-radius: 4px;664        }}665        666        .message-input {{667            display: flex;668            gap: 10px;669            align-items: flex-end;670        }}671        672        .message-input textarea {{673            flex: 1;674            padding: 10px;675            border: 1px solid #ddd;676            border-radius: 4px;677            resize: vertical;678            font-family: inherit;679        }}680        681        .message-input textarea:focus {{682            outline: none;683            border-color: #007bff;684            box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);685        }}686        687        /* Instructions Section Styles */688        .instructions-section {{689            background: white;690            border: 1px solid #e0e0e0;691            border-radius: 8px;692            padding: 20px;693            margin-bottom: 20px;694        }}695        696        .instructions-header {{697            display: flex;698            justify-content: space-between;699            align-items: center;700            margin-bottom: 15px;701        }}702        703        .instructions-title {{704            font-size: 18px;705            font-weight: 600;706            color: #333;707            margin: 0;708        }}709        710        .instructions-toggle {{711            background: #f8f9fa;712            border: 1px solid #dee2e6;713            border-radius: 4px;714            padding: 5px 10px;715            cursor: pointer;716            font-size: 12px;717            color: #6c757d;718        }}719        720        .instructions-toggle:hover {{721            background: #e9ecef;722        }}723        724        .instructions-content {{725            display: none;726            max-height: 400px;727            overflow-y: auto;728            border-top: 1px solid #e0e0e0;729            padding-top: 15px;730        }}731        732        .instructions-content.expanded {{733            display: block;734        }}735        736        .instructions-content h1,737        .instructions-content h2,738        .instructions-content h3 {{739            color: #333;740            margin-top: 20px;741            margin-bottom: 10px;742        }}743        744        .instructions-content h1 {{745            font-size: 24px;746            border-bottom: 2px solid #007bff;747            padding-bottom: 10px;748        }}749        750        .instructions-content h2 {{751            font-size: 20px;752        }}753        754        .instructions-content h3 {{755            font-size: 16px;756        }}757        758        .instructions-content p {{759            margin-bottom: 10px;760            line-height: 1.6;761        }}762        763        .instructions-content code {{764            background: #f8f9fa;765            padding: 2px 4px;766            border-radius: 3px;767            font-family: monospace;768            font-size: 14px;769        }}770        771        .instructions-content pre {{772            background: #f8f9fa;773            border: 1px solid #e9ecef;774            border-radius: 4px;775            padding: 15px;776            overflow-x: auto;777            margin: 10px 0;778        }}779        780        .instructions-content pre code {{781            background: none;782            padding: 0;783        }}784        785        .instructions-content ul,786        .instructions-content ol {{787            margin: 10px 0;788            padding-left: 20px;789        }}790        791        .instructions-content li {{792            margin-bottom: 5px;793        }}794        795        .instructions-content table {{796            border-collapse: collapse;797            width: 100%;798            margin: 15px 0;799        }}800        801        .instructions-content th,802        .instructions-content td {{803            border: 1px solid #dee2e6;804            padding: 8px 12px;805            text-align: left;806        }}807        808        .instructions-content th {{809            background: #f8f9fa;810            font-weight: 600;811        }}812        813        /* Enhanced Form Styles */814        .help-text {{815            display: block;816            margin-top: 5px;817            font-size: 12px;818            color: #6c757d;819            font-style: italic;820        }}821        822        .form-group label {{823            font-weight: 500;824            color: #333;825            margin-bottom: 5px;826        }}827        828        .form-group select {{829            width: 100%;830            padding: 8px 12px;831            border: 1px solid #ddd;832            border-radius: 4px;833            font-size: 14px;834            background-color: white;835        }}836        837        .form-group select:focus {{838            outline: none;839            border-color: #007bff;840            box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);841        }}842        843        .form-group textarea {{844            width: 100%;845            padding: 8px 12px;846            border: 1px solid #ddd;847            border-radius: 4px;848            font-size: 14px;849            font-family: inherit;850            resize: vertical;851        }}852        853        .form-group textarea:focus {{854            outline: none;855            border-color: #007bff;856            box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);857        }}858        859        .form-group input[type="number"] {{860            width: 100%;861            padding: 8px 12px;862            border: 1px solid #ddd;863            border-radius: 4px;864            font-size: 14px;865        }}866        867        .form-group input[type="number"]:focus {{868            outline: none;869            border-color: #007bff;870            box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);871        }}872        873        .form-group input[type="text"]:focus {{874            outline: none;875            border-color: #007bff;876            box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);877        }}878        879        .required-indicator {{880            color: #dc3545;881            font-weight: bold;882        }}883        884        .form-group .field-description {{885            font-size: 11px;886            color: #666;887            margin-top: 2px;888            font-style: italic;889        }}890    </style>891</head>892<body>893    <div class="container">894        <!-- Left Pane: HumanAgent Interface -->895        <div class="left-pane">896            <div class="pane-header">897                <span class="status-indicator status-disconnected" id="connection-status"></span>898                HumanAgent Interface899            </div>900            <div class="pane-content">901                <!-- Instructions Section -->902                {_generate_instructions_section(metadata)}903                904                <!-- Action Form or Chat Interface -->905                {_generate_action_interface(action_fields, is_chat_env)}906                907                <!-- Control Buttons -->908                <div style="margin-bottom: 20px;">909                    <button class="btn btn-secondary" id="reset-btn">Reset Environment</button>910                    <button class="btn btn-secondary" id="state-btn">Get State</button>911                </div>912                913                <!-- Current State Display -->914                <div class="state-display">915                    <h3>Current State</h3>916                    <div id="current-state">917                        <div class="state-item">918                            <span class="state-label">Status:</span>919                            <span class="state-value" id="env-status">Not initialized</span>920                        </div>921                        <div class="state-item">922                            <span class="state-label">Episode ID:</span>923                            <span class="state-value" id="episode-id">-</span>924                        </div>925                        <div class="state-item">926                            <span class="state-label">Step Count:</span>927                            <span class="state-value" id="step-count">0</span>928                        </div>929                    </div>930                </div>931            </div>932        </div>933        934        <!-- Right Pane: State Observer -->935        <div class="right-pane">936            <div class="pane-header">937                State Observer938            </div>939            <div class="pane-content">940                <!-- Current Observation -->941                <div class="state-display">942                    <h3>Current Observation</h3>943                    <div id="current-observation" class="json-display">944                        No observation yet945                    </div>946                </div>947                948                <!-- Action Logs -->949                <div class="logs-container">950                    <h3>Action History</h3>951                    <div id="action-logs">952                        No actions taken yet953                    </div>954                </div>955            </div>956        </div>957    </div>958 959    <script>960        class OpenEnvWebInterface {{961            constructor() {{962                this.ws = null;963                this.isConnected = false;964                this.init();965            }}966            967            init() {{968                this.connectWebSocket();969                this.setupEventListeners();970            }}971            972            connectWebSocket() {{973                const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';974                const wsUrl = `${{protocol}}//${{window.location.host}}/ws`;975                976                this.ws = new WebSocket(wsUrl);977                978                this.ws.onopen = () => {{979                    this.isConnected = true;980                    this.updateConnectionStatus(true);981                    console.log('WebSocket connected');982                }};983                984                this.ws.onmessage = (event) => {{985                    const data = JSON.parse(event.data);986                    if (data.type === 'state_update') {{987                        this.updateUI(data.episode_state);988                    }}989                }};990                991                this.ws.onclose = () => {{992                    this.isConnected = false;993                    this.updateConnectionStatus(false);994                    console.log('WebSocket disconnected');995                    // Attempt to reconnect after 3 seconds996                    setTimeout(() => this.connectWebSocket(), 3000);997                }};998                999                this.ws.onerror = (error) => {{1000                    console.error('WebSocket error:', error);1001                }};1002            }}1003            1004            setupEventListeners() {{1005                // Instructions toggle1006                const instructionsToggle = document.getElementById('instructions-toggle');1007                const instructionsContent = document.getElementById('instructions-content');1008                if (instructionsToggle && instructionsContent) {{1009                    instructionsToggle.addEventListener('click', () => {{1010                        instructionsContent.classList.toggle('expanded');1011                        instructionsToggle.textContent = instructionsContent.classList.contains('expanded') 1012                            ? 'Hide Instructions' : 'Show Instructions';1013                    }});1014                }}1015                1016                // Check if this is a chat environment1017                const isChatEnv = document.getElementById('chat-messages') !== null;1018                1019                if (isChatEnv) {{1020                    // Chat environment event listeners1021                    document.getElementById('send-message-btn').addEventListener('click', () => {{1022                        this.sendMessage();1023                    }});1024                    1025                    // Send message on Enter (but allow Shift+Enter for new lines)1026                    document.getElementById('message-input').addEventListener('keydown', (e) => {{1027                        if (e.key === 'Enter' && !e.shiftKey) {{1028                            e.preventDefault();1029                            this.sendMessage();1030                        }}1031                    }});1032                }} else {{1033                    // Traditional action form submission1034                    const actionForm = document.getElementById('action-form');1035                    if (actionForm) {{1036                        actionForm.addEventListener('submit', (e) => {{1037                            e.preventDefault();1038                            this.submitAction();1039                        }});1040                    }}1041                }}1042                1043                // Reset button1044                document.getElementById('reset-btn').addEventListener('click', () => {{1045                    this.resetEnvironment();1046                }});1047                1048                // State button1049                document.getElementById('state-btn').addEventListener('click', () => {{1050                    this.getState();1051                }});1052            }}1053            1054            async sendMessage() {{1055                const messageInput = document.getElementById('message-input');1056                const roleSelect = document.getElementById('message-role');1057                const message = messageInput.value.trim();1058                const role = roleSelect.value;1059                1060                if (!message) {{1061                    return;1062                }}1063                1064                // Add message to chat display immediately1065                this.addMessageToChat(role, message);1066                1067                // Clear input1068                messageInput.value = '';1069                1070                try {{1071                    // Send message to server to convert to action and step1072                    const response = await fetch('/web/step', {{1073                        method: 'POST',1074                        headers: {{ 'Content-Type': 'application/json' }},1075                        body: JSON.stringify({{ 1076                            message: {{1077                                role: role,1078                                content: message1079                            }}1080                        }})1081                    }});1082                    1083                    if (!response.ok) {{1084                        throw new Error(`HTTP error! status: ${{response.status}}`);1085                    }}1086                    1087                    const result = await response.json();1088                    console.log('Message sent:', result);1089                }} catch (error) {{1090                    console.error('Error sending message:', error);1091                    alert('Error sending message: ' + error.message);1092                }}1093            }}1094            1095            addMessageToChat(role, content) {{1096                const chatMessages = document.getElementById('chat-messages');1097                const messageDiv = document.createElement('div');1098                messageDiv.className = `chat-message ${{role}}`;1099                1100                messageDiv.innerHTML = `1101                    <div class="message-role">${{role.charAt(0).toUpperCase() + role.slice(1)}}</div>1102                    <div class="message-content">${{content}}</div>1103                `;1104                1105                chatMessages.appendChild(messageDiv);1106                chatMessages.scrollTop = chatMessages.scrollHeight;1107            }}1108            1109            async submitAction() {{1110                const formData = new FormData(document.getElementById('action-form'));1111                const action = {{}};1112                1113                // Collect form data1114                for (const [key, value] of formData.entries()) {{1115                    if (value !== '') {{1116                        // Handle tensor fields (tokens) - convert comma-separated string to array1117                        if (key === 'tokens') {{1118                            try {{1119                                action[key] = value.split(',').map(x => parseInt(x.trim())).filter(x => !isNaN(x));1120                            }} catch (e) {{1121                                console.error('Error parsing tokens:', e);1122                                action[key] = [];1123                            }}1124                        }} else {{1125                            action[key] = value;1126                        }}1127                    }}1128                }}1129                1130                try {{1131                    const response = await fetch('/web/step', {{1132                        method: 'POST',1133                        headers: {{ 'Content-Type': 'application/json' }},1134                        body: JSON.stringify({{ action }})1135                    }});1136                    1137                    if (!response.ok) {{1138                        throw new Error(`HTTP error! status: ${{response.status}}`);1139                    }}1140                    1141                    const result = await response.json();1142                    console.log('Step result:', result);1143                }} catch (error) {{1144                    console.error('Error submitting action:', error);1145                    alert('Error submitting action: ' + error.message);1146                }}1147            }}1148            1149            async resetEnvironment() {{1150                try {{1151                    const response = await fetch('/web/reset', {{1152                        method: 'POST',1153                        headers: {{ 'Content-Type': 'application/json' }}1154                    }});1155                    1156                    if (!response.ok) {{1157                        throw new Error(`HTTP error! status: ${{response.status}}`);1158                    }}1159                    1160                    const result = await response.json();1161                    console.log('Reset result:', result);1162                }} catch (error) {{1163                    console.error('Error resetting environment:', error);1164                    alert('Error resetting environment: ' + error.message);1165                }}1166            }}1167            1168            async getState() {{1169                try {{1170                    const response = await fetch('/web/state');1171                    const state = await response.json();1172                    console.log('Current state:', state);1173                    alert('Current state: ' + JSON.stringify(state, null, 2));1174                }} catch (error) {{1175                    console.error('Error getting state:', error);1176                    alert('Error getting state: ' + error.message);1177                }}1178            }}1179            1180            updateConnectionStatus(connected) {{1181                const indicator = document.getElementById('connection-status');1182                if (connected) {{1183                    indicator.className = 'status-indicator status-connected';1184                }} else {{1185                    indicator.className = 'status-indicator status-disconnected';1186                }}1187            }}1188            1189            updateUI(episodeState) {{1190                // Check if this is a chat environment1191                const isChatEnv = document.getElementById('chat-messages') !== null;1192                1193                // Update current state1194                document.getElementById('env-status').textContent = 1195                    episodeState.is_reset ? 'Reset' : 'Running';1196                document.getElementById('episode-id').textContent = 1197                    episodeState.episode_id || '-';1198                document.getElementById('step-count').textContent = 1199                    episodeState.step_count.toString();1200                

Showing the first 1,200 of 1614 lines. Download the file for the rest.