Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Having problem with use_change #1004

Open
Sahil-Chhoker opened this issue Feb 15, 2025 · 2 comments
Open

Having problem with use_change #1004

Sahil-Chhoker opened this issue Feb 15, 2025 · 2 comments

Comments

@Sahil-Chhoker
Copy link

I am trying to build a python console on the web using Solara but having problems with implementing use_change correctly.

Here is my python file:

from typing import Callable, List, Tuple, cast
import ipyvue
import solara
import sys
import io
import code
from solara.components.input import use_change

class Interpreter(code.InteractiveInterpreter):
    def __init__(self):
        super().__init__()
        self.output_buffer = io.StringIO()

    def run_code(self, command: str) -> str:
        """Execute code and capture output including errors."""
        if not command.strip():
            return ""

        sys.stdout = self.output_buffer
        sys.stderr = self.output_buffer
        
        try:
            result = self.runsource(command)
            output = self.output_buffer.getvalue()
            return output.strip() if output else ""
            
        except Exception as e:
            error_output = self.output_buffer.getvalue()
            if error_output:
                return error_output
            return f"{type(e).__name__}: {str(e)}"
            
        finally:
            sys.stdout = sys.__stdout__
            sys.stderr = sys.__stderr__
            self.output_buffer.truncate(0)
            self.output_buffer.seek(0)

class ConsoleHistory:
    def __init__(self):
        self.history: List[Tuple[str, str]] = []

    def add_entry(self, command: str, output: str) -> None:
        self.history.append((f">>> {command}", output))

    def clear(self) -> None:
        self.history.clear()

    def get_entries(self) -> List[Tuple[str, str]]:
        return self.history

class OutputFormatter:
    @staticmethod
    def format_error_output(output: str) -> str:
        """Clean up error output to display only the relevant error message."""
        if not output:
            return ""
            
        error_lines = output.strip().splitlines()
        if len(error_lines) >= 1:
            for line in reversed(error_lines):
                if "line" in line and "File" in line:
                    continue
                if ": " in line:
                    return line.strip()
        return output

    @staticmethod
    def format_entry(command: str, result: str) -> str:
        """Format a single console entry for display."""
        escaped_result = result.replace("<", "&lt;").replace(">", "&gt;")
        is_error = any(err in result for err in [
            "Error", "Exception", "TypeError", 
            "ValueError", "NameError", "ZeroDivisionError"
        ])
        
        if result:
            return f"""
            <div style="margin: 0px 0 0 0;">
                <div style="background-color: #f5f5f5; padding: 6px 8px; border-radius: 4px; font-family: 'Consolas', monospace; font-size: 0.9em;">
                <span style="color: #2196F3;">{">>> "}</span><span>{command.removeprefix(">>> ")}</span>
                </div>
                <div style="background-color: #ffffff; padding: 6px 8px; border-left: 3px solid {'#ff3860' if is_error else '#2196F3'}; margin-top: 2px; font-family: 'Consolas', monospace; font-size: 0.9em; {'color: #ff3860;' if is_error else ''}">
                {escaped_result}
                </div>
            </div>
            """
        else:
            return f"""
            <div style="margin: 0px 0 0 0;">
                <div style="background-color: #f5f5f5; padding: 6px 8px; border-radius: 4px; font-family: 'Consolas', monospace; font-size: 0.9em;">
                <span style="color: #2196F3;">{">>> "}</span><span>{command.removeprefix(">>> ")}</span>
                </div>
            </div>
            """

class ConsoleManager:
    def __init__(self):
        self.interpreter = Interpreter()
        self.history = ConsoleHistory()
        self.formatter = OutputFormatter()

    def execute_code(self, input_text: str, set_input_text: Callable) -> None:
        """Execute code and update history with cleaned output."""
        if input_text.strip():
            output = self.interpreter.run_code(input_text)
            cleaned_output = self.formatter.format_error_output(output)
            if "Traceback" in cleaned_output:
                cleaned_output = cleaned_output.splitlines()[-1]
            self.history.add_entry(input_text, f"Error ({cleaned_output})")
            set_input_text("")

    def clear_console(self) -> None:
        """Clear the console history."""
        self.history.clear()

console_manager = ConsoleManager()

@solara.component
def ConsoleSidebar():
    input_text, set_input_text = solara.use_state("")
    _, set_refresh = solara.use_state(0)

    with solara.Sidebar():
        solara.Markdown("## Console")
        
        with solara.Column(style={
            "height": "300px",
            "overflow-y": "auto",
            "gap": "0px",
            "box-shadow": "inset 0 0 10px rgba(0,0,0,0.1)",
            "border": "3px solid #e0e0e0",
            "border-radius": "6px",
            "padding": "8px"
        }):
            for cmd, result in console_manager.history.get_entries():
                solara.Markdown(console_manager.formatter.format_entry(cmd, result))

        input_element = solara.v.TextField(
            v_model=input_text,
            on_v_model=set_input_text,
            flat=True,
            style_="font-family: monospace;",
            label=">>>",
            outlined=True,
            placeholder="Enter Python code...",
            attributes={"spellcheck": "false"},
        )

        use_change(input_element, console_manager.execute_code(input_text, set_input_text), update_events=["keyup.enter"])

        with solara.Row():
            solara.Button(
                "Run", 
                on_click=lambda: console_manager.execute_code(input_text, set_input_text), 
                size="small"
            )
            solara.Button(
                "Clear", 
                on_click=lambda: [console_manager.clear_console(), set_refresh(lambda x: x + 1)], 
                size="small"
            )

@solara.component
def Page():
    ConsoleSidebar()
    solara.Markdown("# Main Content")

Page()

Error Message:

Traceback (most recent call last):
  File "C:\MASTER-FOLDER\GitHub\mesa-task\env\Lib\site-packages\reacton\core.py", line 1900, in _reconsolidate
    effect()
    ~~~~~~^^
  File "C:\MASTER-FOLDER\GitHub\mesa-task\env\Lib\site-packages\reacton\core.py", line 1131, in __call__
    self._cleanup = self.callable()
                    ~~~~~~~~~~~~~^^
  File "C:\MASTER-FOLDER\GitHub\mesa-task\env\Lib\site-packages\solara\components\input.py", line 24, in add_events
    widget = cast(ipyvue.VueWidget, solara.get_widget(el))
                                    ~~~~~~~~~~~~~~~~~^^^^
  File "C:\MASTER-FOLDER\GitHub\mesa-task\env\Lib\site-packages\reacton\core.py", line 766, in get_widget
    raise KeyError(f"Element {el} not found in all known widgets")  # for the component {context.widgets}")
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyError: "Element ipyvuetify.TextField(v_model = '', on_v_model = <function ...E77A728E0>, flat = True, style_ = 'font-family: monospace;', label = '>>>', outlined = True, placeholder = 'Enter Python code...', attributes = {'spellcheck': 'false'}) not found in all known widgets"

Any kind of help is appreciated!

@Sahil-Chhoker Sahil-Chhoker changed the title Having problem with use_change() Having problem with use_change Feb 15, 2025
@iisakkirotko
Copy link
Collaborator

iisakkirotko commented Feb 17, 2025

Hey @Sahil-Chhoker!

I think this happens because of the way solara.Sidebar is implemented. Essentially, the sidebar doesn't renders the children given to it directly, but rather adds them to a portal, which then renders them later. Because the the use_change hook is injected directly into the sidebar this causes an issue with the hook being executed before the other elements are actually rendered. This is mentioned in our documentation on the rules of hooks - "Hooks can only be called at the top level of a function component", but this restriction isn't actually enforced with an error. (I also noticed that we do this in our codebase, for example in solara/lab/components/chat.py, I'll open a separate issue for that).

You can fix the issue by not putting the hook directly within the sidebar, but rather wrapping it in a component. I made a PyCafe example with the modified code, feel free to take a look!

@Sahil-Chhoker
Copy link
Author

Thank you @iisakkirotko for your help, I get my mistake now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants