diff --git a/pyproject.toml b/pyproject.toml index b6d5b59..7977d1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "sentence-transformers>=3.4.1", "tqdm>=4.67.1", "trame>=3.9.0", + "trame-code>=1.0.2", "trame-vtk>=2.8.0", "trame-vuetify>=3", "tree_sitter>=0.23", @@ -144,6 +145,7 @@ module = [ "vtkmodules.*", "trame.*", "trame_vtk.*", + "trame_code.*", "openai.*", "click.*", "chromadb.*", diff --git a/src/vtk_prompt/controllers/conversation.py b/src/vtk_prompt/controllers/conversation.py index 7791959..0e43740 100644 --- a/src/vtk_prompt/controllers/conversation.py +++ b/src/vtk_prompt/controllers/conversation.py @@ -120,6 +120,58 @@ def save_conversation(app: Any) -> str: return "" +def copy_generated_code(app: Any) -> None: + """Copy generated code to clipboard via client-side trigger.""" + code = app.state.generated_code + if code: + app.server.js_call("trame.utils.vtk_prompt.copy_to_clipboard", code) + + +def download_generated_code(app: Any) -> str: + """Download generated code as a .py file with VTK renderer initialization.""" + code = app.state.generated_code + if not code: + return "" + + # Remove the injected renderer comment if present + code_lines = code.split("\n") + filtered_lines = [ + line + for line in code_lines + if not line.strip().startswith("# renderer is a vtkRenderer") + and not line.strip().startswith("# Use your own vtkRenderer") + ] + clean_code = "\n".join(filtered_lines) + + # Add standalone VTK renderer initialization + standalone_code = """import vtk + +# Create renderer, render window, and interactor +renderer = vtk.vtkRenderer() +render_window = vtk.vtkRenderWindow() +render_window.AddRenderer(renderer) +render_window_interactor = vtk.vtkRenderWindowInteractor() +render_window_interactor.SetRenderWindow(render_window) + +# Set background color +renderer.SetBackground(0.1, 0.1, 0.1) + +""" + + # Add the generated code + standalone_code += clean_code + + # Add render window display code + standalone_code += """ + +# Display the render window +render_window.Render() +render_window_interactor.Start() +""" + + return standalone_code + + def _parse_assistant_content(content: str) -> tuple[str | None, str | None]: """Parse assistant message content for explanation and code.""" try: diff --git a/src/vtk_prompt/state/initializer.py b/src/vtk_prompt/state/initializer.py index ba9acce..350c0fa 100644 --- a/src/vtk_prompt/state/initializer.py +++ b/src/vtk_prompt/state/initializer.py @@ -59,6 +59,15 @@ def initialize_state(app: Any) -> None: app.state.toast_visible = False app.state.toast_color = "warning" + # Monaco Editor options + app.state.editor_options = { + "readOnly": True, + "minimap": {"enabled": True}, + "lineNumbers": "on", + "scrollBeyondLastLine": False, + "fontSize": 13, + } + # API configuration state app.state.use_cloud_models = True # Toggle between cloud and local app.state.tab_index = 0 # Tab navigation state diff --git a/src/vtk_prompt/ui/layout/content.py b/src/vtk_prompt/ui/layout/content.py index 483b169..0f1dc96 100644 --- a/src/vtk_prompt/ui/layout/content.py +++ b/src/vtk_prompt/ui/layout/content.py @@ -9,6 +9,7 @@ from trame.widgets import html from trame.widgets import vuetify3 as vuetify +from trame_code.widgets import code # type: ignore[import-not-found] from trame_vtk.widgets import vtk as vtk_widgets from .conversation_history import build_conversation_history @@ -104,22 +105,34 @@ def build_content(layout: Any, app: Any) -> None: # Middle column - Generated code view with vuetify.VCol(cols=4): # Generated code panel - with vuetify.VCard(readonly=True, classes="h-100 mt-2"): - vuetify.VCardTitle("Generated Code") + with vuetify.VCard(readonly=True, classes="h-100"): + with vuetify.VCardTitle(classes="d-flex align-center"): + html.Span("Generated Code") + vuetify.VSpacer() + with vuetify.VTooltip(text="Download as .py", location="bottom"): + with vuetify.Template(v_slot_activator="{ props }"): + with vuetify.VBtn( + icon=True, + density="compact", + variant="text", + v_bind="props", + click=( + "trame.utils.vtk_prompt.download_generated_code(trame)" + ), + disabled=("!generated_code",), + ): + vuetify.VIcon("mdi-download") with vuetify.VCardText(style="height: calc(100% - 50px);"): - vuetify.VTextarea( - v_model=("generated_code", ""), - readonly=True, - solo=True, - hide_details=True, - no_resize=True, - classes="overflow-y-auto fill-height", - style="font-family: monospace;", - placeholder="Generated VTK code will appear here...", + code.Editor( + model_value=("generated_code", ""), + language="python", + theme=("theme_mode === 'dark' ? 'vs-dark' : 'vs'",), + options=("editor_options",), + style="width: 100%; height: calc(100% - 75px);", ) # Right column - VTK viewer and prompt - with vuetify.VCol(cols=5): + with vuetify.VCol(cols=5, classes="mb-2"): with vuetify.VRow(no_gutters=True, classes="fill-height"): # Top: VTK render view with vuetify.VCard(classes="h-75 w-100"): diff --git a/src/vtk_prompt/ui/layout/toolbar.py b/src/vtk_prompt/ui/layout/toolbar.py index 031f505..ab6dda3 100644 --- a/src/vtk_prompt/ui/layout/toolbar.py +++ b/src/vtk_prompt/ui/layout/toolbar.py @@ -19,47 +19,48 @@ def build_toolbar(layout: Any, app: Any) -> None: vuetify.VSpacer() # Settings buttons - with vuetify.VTooltip( - text="Load or download files", - location="bottom", + with vuetify.VBtnGroup( + variant="outlined", + rounded="lg", + color="primary", + classes="mr-4", + divided=True, ): - with vuetify.Template(v_slot_activator="{ props }"): - with vuetify.VBtn( - icon=True, - v_bind="props", - click="advanced_settings_open = true; active_settings_tab = 'files';", - classes="mr-4", - color="primary", - ): - vuetify.VIcon("mdi-file-cog-outline") + with vuetify.VTooltip( + text="Load or download files", + location="bottom", + ): + with vuetify.Template(v_slot_activator="{ props }"): + with vuetify.VBtn( + icon=True, + v_bind="props", + click="advanced_settings_open = true; active_settings_tab = 'files';", + ): + vuetify.VIcon("mdi-file-cog-outline") - with vuetify.VTooltip( - text="Change model settings", - location="bottom", - ): - with vuetify.Template(v_slot_activator="{ props }"): - with vuetify.VBtn( - icon=True, - v_bind="props", - click="advanced_settings_open = true; active_settings_tab = 'model';", - classes="mr-4", - color="primary", - ): - vuetify.VIcon("mdi-brain") + with vuetify.VTooltip( + text="Change model settings", + location="bottom", + ): + with vuetify.Template(v_slot_activator="{ props }"): + with vuetify.VBtn( + icon=True, + v_bind="props", + click="advanced_settings_open = true; active_settings_tab = 'model';", + ): + vuetify.VIcon("mdi-brain") - with vuetify.VTooltip( - text="Advanced settings", - location="bottom", - ): - with vuetify.Template(v_slot_activator="{ props }"): - with vuetify.VBtn( - icon=True, - v_bind="props", - click="advanced_settings_open = true; active_settings_tab = 'advanced';", - classes="mr-4", - color="primary", - ): - vuetify.VIcon("mdi-cog-outline") + with vuetify.VTooltip( + text="Advanced settings", + location="bottom", + ): + with vuetify.Template(v_slot_activator="{ props }"): + with vuetify.VBtn( + icon=True, + v_bind="props", + click="advanced_settings_open = true; active_settings_tab = 'advanced';", + ): + vuetify.VIcon("mdi-cog-outline") # Theme switcher vuetify.VSwitch( @@ -69,4 +70,5 @@ def build_toolbar(layout: Any, app: Any) -> None: true_value="light", false_value="dark", append_icon=("theme_mode === 'light' ? 'mdi-weather-sunny' : 'mdi-weather-night'",), + icon_color=("theme_mode === 'light' ? 'orange-darken-4' : 'purple-lighten-4'",), ) diff --git a/src/vtk_prompt/utils.js b/src/vtk_prompt/utils.js index 5cbfeae..610275f 100644 --- a/src/vtk_prompt/utils.js +++ b/src/vtk_prompt/utils.js @@ -1,3 +1,6 @@ +window.trame = window.trame || {}; +window.trame.utils = window.trame.utils || {}; + window.trame.utils.vtk_prompt = { rules: { json_file(obj) { @@ -10,4 +13,36 @@ window.trame.utils.vtk_prompt = { return true; }, }, + copy_to_clipboard: function (text) { + if (navigator && navigator.clipboard) { + navigator.clipboard.writeText(text).catch((err) => { + console.error("Failed to copy to clipboard:", err); + }); + } else { + console.error("Clipboard API not available"); + } + }, + download_file: function (content, filename, mimeType) { + mimeType = mimeType || "text/plain"; + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }, + download_generated_code: function (trame) { + trame.trigger("download_generated_code").then((code) => { + if (code) { + window.trame.utils.vtk_prompt.download_file( + code, + "vtk_generated.py", + "text/x-python", + ); + } + }); + }, }; diff --git a/src/vtk_prompt/vtk_prompt_ui.py b/src/vtk_prompt/vtk_prompt_ui.py index 7fd9ffc..162de2d 100644 --- a/src/vtk_prompt/vtk_prompt_ui.py +++ b/src/vtk_prompt/vtk_prompt_ui.py @@ -213,6 +213,16 @@ def toggle_favorite_conversation(self, conversation_index: int) -> None: """Toggle favorite status for a conversation.""" conversation.toggle_favorite_conversation(self, conversation_index) + @controller.set("copy_generated_code") + def copy_generated_code(self) -> None: + """Copy generated code to clipboard.""" + conversation.copy_generated_code(self) + + @trigger("download_generated_code") + def download_generated_code(self) -> str: + """Download generated code as a .py file.""" + return conversation.download_generated_code(self) + @trigger("save_conversation") def save_conversation(self) -> str: """Save current conversation history as JSON string."""