diff --git a/src/vtk_prompt/cli.py b/src/vtk_prompt/cli.py index 2234ee1..d4dbd17 100644 --- a/src/vtk_prompt/cli.py +++ b/src/vtk_prompt/cli.py @@ -162,7 +162,7 @@ def main( top_k=top_k, rag=rag, retry_attempts=retry_attempts, - provider=provider, + _provider=provider, custom_prompt=custom_prompt_data, ) diff --git a/src/vtk_prompt/controllers/conversation.py b/src/vtk_prompt/controllers/conversation.py index c16637b..7791959 100644 --- a/src/vtk_prompt/controllers/conversation.py +++ b/src/vtk_prompt/controllers/conversation.py @@ -83,6 +83,36 @@ def navigate_conversation_right(app: Any) -> None: _update_navigation_state(app) +def navigate_to_conversation(app: Any, target_index: int) -> None: + """Navigate directly to a specific conversation pair by index.""" + if not app.state.conversation_navigation: + return + + nav_length = len(app.state.conversation_navigation) + if target_index < 0 or target_index >= nav_length: + return + + app.state.conversation_index = target_index + _process_conversation_pair(app, target_index) + _update_navigation_state(app) + + +def toggle_favorite_conversation(app: Any, conversation_index: int) -> None: + """Toggle favorite status for a conversation by index.""" + if not hasattr(app.state, "favorited_conversations"): + app.state.favorited_conversations = [] + + current_favorites = app.state.favorited_conversations[:] + + if conversation_index in current_favorites: + current_favorites.remove(conversation_index) + else: + current_favorites.append(conversation_index) + + # Force reactivity by replacing the entire array + app.state.favorited_conversations = current_favorites + + def save_conversation(app: Any) -> str: """Save current conversation history as JSON string.""" if hasattr(app, "prompt_client") and app.prompt_client is not None: @@ -183,7 +213,7 @@ def _process_conversation_pair(app: Any, pair_index: int | None = None) -> None: else: query_text = user_content - app.state.query_text = query_text + return query_text def _process_loaded_conversation( diff --git a/src/vtk_prompt/state/initializer.py b/src/vtk_prompt/state/initializer.py index 66ce7a6..ba9acce 100644 --- a/src/vtk_prompt/state/initializer.py +++ b/src/vtk_prompt/state/initializer.py @@ -46,6 +46,9 @@ def initialize_state(app: Any) -> None: app.state.can_navigate_left = False app.state.can_navigate_right = False app.state.is_viewing_history = False + app.state.history_sort_order = "newest" # Conversation history sort order + app.state.favorited_conversations = [] # List of favorited conversation indices + app.state.history_filter_mode = "all" # Filter mode: "all" or "favorites" # Prompt file state variables app.state.prompt_object = None diff --git a/src/vtk_prompt/ui/layout/content.py b/src/vtk_prompt/ui/layout/content.py index 68fd2c8..483b169 100644 --- a/src/vtk_prompt/ui/layout/content.py +++ b/src/vtk_prompt/ui/layout/content.py @@ -11,6 +11,8 @@ from trame.widgets import vuetify3 as vuetify from trame_vtk.widgets import vtk as vtk_widgets +from .conversation_history import build_conversation_history + def build_content(layout: Any, app: Any) -> None: """Build the main content area with code panels and VTK viewer.""" @@ -19,8 +21,8 @@ def build_content(layout: Any, app: Any) -> None: classes="fluid fill-height", style="min-width: 100%; padding: 0!important;" ): with vuetify.VRow(rows=12, classes="fill-height px-4 pt-1 pb-1"): - # Left column - Generated code view - with vuetify.VCol(cols=6): + # Left column - Prompt and conversation history + with vuetify.VCol(cols=3, classes="fill-height"): # Prompt input with vuetify.VCard(classes="h-25"): with vuetify.VCardText(classes="h-100"): @@ -64,15 +66,6 @@ def build_content(layout: Any, app: Any) -> None: ) with html.Div(classes="d-flex", style="height: calc(100% - 75px);"): - with vuetify.VBtn( - variant="tonal", - icon=True, - rounded="0", - disabled=("!can_navigate_left",), - classes="h-auto mr-1", - click=app.ctrl.navigate_conversation_left, - ): - vuetify.VIcon("mdi-arrow-left-circle") # Query input vuetify.VTextarea( label="Describe VTK visualization", @@ -83,30 +76,6 @@ def build_content(layout: Any, app: Any) -> None: hide_details=True, no_resize=True, ) - with vuetify.VBtn( - color=( - "conversation_index ===" - + " conversation_navigation.length - 1" - + " ? 'success' : 'default'", - "default", - ), - variant="tonal", - icon=True, - rounded="0", - disabled=("!can_navigate_right",), - click=app.ctrl.navigate_conversation_right, - ): - vuetify.VIcon( - "mdi-arrow-right-circle", - v_show="conversation_index <" - + " conversation_navigation.length - 1", - ) - vuetify.VIcon( - "mdi-message-plus", - v_show="conversation_index ===" - + " conversation_navigation.length - 1", - ) - # Generate button vuetify.VBtn( "Generate Code", @@ -129,8 +98,13 @@ def build_content(layout: Any, app: Any) -> None: v_show="use_cloud_models && !api_token.trim()", ) + # Bottom: Conversation History + build_conversation_history(app) + + # Middle column - Generated code view + with vuetify.VCol(cols=4): # Generated code panel - with vuetify.VCard(readonly=True, classes="h-75 mt-2"): + with vuetify.VCard(readonly=True, classes="h-100 mt-2"): vuetify.VCardTitle("Generated Code") with vuetify.VCardText(style="height: calc(100% - 50px);"): vuetify.VTextarea( @@ -145,7 +119,7 @@ def build_content(layout: Any, app: Any) -> None: ) # Right column - VTK viewer and prompt - with vuetify.VCol(cols=6): + with vuetify.VCol(cols=5): 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/conversation_history.py b/src/vtk_prompt/ui/layout/conversation_history.py new file mode 100644 index 0000000..49d26f6 --- /dev/null +++ b/src/vtk_prompt/ui/layout/conversation_history.py @@ -0,0 +1,135 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2024, The PyTK developers. +# Distributed under a BSD-3-Clause license. +# See the LICENSE file for details. + +"""Conversation History Component for VTK Prompt UI.""" + +from typing import Any + +from trame.widgets import html +from trame.widgets import vuetify3 as vuetify + + +def build_conversation_history(app: Any) -> None: + """Build the conversation history component with clickable conversation cards.""" + # Bottom: Conversation History + with vuetify.VCard(classes="h-75 w-100 mt-2"): + with vuetify.VCardTitle("Conversation History", classes="d-flex align-center"): + vuetify.VSpacer() + + # Sort toggle button + with vuetify.VTooltip(text="Toggle sort order", location="bottom"): + with vuetify.Template(v_slot_activator="{ props }"): + vuetify.VBtn( + icon=( + "history_sort_order === 'newest'" + + " ? 'mdi-sort-descending' : 'mdi-sort-ascending'", + "mdi-sort-descending", + ), + click=( + "history_sort_order = " + + "(history_sort_order === 'newest')" + + " ? 'oldest' : 'newest'" + ), + variant="text", + density="compact", + color="primary", + disabled=("conversation_navigation.length === 0", False), + v_bind="props", + ) + + # Filter toggle button + with vuetify.VTooltip(text="Toggle favorites filter", location="bottom"): + with vuetify.Template(v_slot_activator="{ props }"): + vuetify.VBtn( + icon=( + "history_filter_mode === 'favorites'" + + " ? 'mdi-heart' : 'mdi-heart-off'", + "mdi-format-list-bulleted", + ), + click=( + "history_filter_mode = (history_filter_mode === 'all')" + + " ? 'favorites' : 'all'" + ), + variant="text", + density="compact", + color=( + "history_filter_mode === 'favorites' ? 'red' : 'primary'", + "primary", + ), + disabled=( + "conversation_navigation.length === 0" + + " || favorited_conversations.length === 0", + False, + ), + v_bind="props", + ) + + with vuetify.VCardText(style="height: calc(100% - 50px); overflow-y: auto;"): + # Show message when no history + vuetify.VAlert( + text="No conversation history yet." + " Start by generating some VTK code!", + type="info", + variant="tonal", + v_show="conversation_navigation.length === 0", + ) + + # Conversation history list + with vuetify.VCard( + v_for=( + "item in (history_sort_order === 'newest'" + + " ? conversation_navigation.slice().reverse()" + + ".map((pair, idx) => ({pair, originalIndex: " + + "conversation_navigation.length - 1 - idx}))" + + " : conversation_navigation.map((pair, idx) => " + + "({pair, originalIndex: idx})))" + + ".filter(item => history_filter_mode === 'all' || " + + "favorited_conversations.includes(item.originalIndex))" + ), + key="item.originalIndex", + density="compact", + v_show="conversation_navigation.length > 0", + color=( + "conversation_index === item.originalIndex" + " ? 'primary' : 'secondary'", + "secondary", + ), + variant=( + "conversation_index === item.originalIndex" + " ? 'outlined' : 'default'", + "default", + ), + ): + # Track favorited prompts + with vuetify.VCardTitle(classes="text-end"): + vuetify.VIcon( + click=( + app.ctrl.toggle_favorite_conversation, + "[item.originalIndex]", + ), + icon=( + "favorited_conversations.includes(item.originalIndex) ? " + + "'mdi-heart' : 'mdi-heart-outline'", + "mdi-heart-outline", + ), + size="small", + color=( + "favorited_conversations.includes(item.originalIndex) ? " + + "'red' : 'grey'", + "grey", + ), + ) + with vuetify.VCardText( + click=( + app.ctrl.navigate_to_conversation, + "[item.originalIndex]", + ), + rounded=True, + classes="mb-2", + ): + # User query preview + html.Span( + "{{ (item.pair.user.content.includes('')" + + " ? item.pair.user.content.split('')[1]" + + " : item.pair.user.content)" + + ".trim().replace(/^Request:\\s*/i, '') }}" + ) diff --git a/src/vtk_prompt/vtk_prompt_ui.py b/src/vtk_prompt/vtk_prompt_ui.py index 8cbfa94..7fd9ffc 100644 --- a/src/vtk_prompt/vtk_prompt_ui.py +++ b/src/vtk_prompt/vtk_prompt_ui.py @@ -203,6 +203,16 @@ def navigate_conversation_right(self) -> None: """Navigate to next conversation pair.""" conversation.navigate_conversation_right(self) + @controller.set("navigate_to_conversation") + def navigate_to_conversation(self, target_index: int) -> None: + """Navigate directly to a specific conversation pair.""" + conversation.navigate_to_conversation(self, target_index) + + @controller.set("toggle_favorite_conversation") + def toggle_favorite_conversation(self, conversation_index: int) -> None: + """Toggle favorite status for a conversation.""" + conversation.toggle_favorite_conversation(self, conversation_index) + @trigger("save_conversation") def save_conversation(self) -> str: """Save current conversation history as JSON string.""" @@ -224,7 +234,7 @@ def _build_ui(self) -> None: self.state.main_drawer = False with SinglePageLayout( - self.server, theme=("theme_mode", "light"), style="max-height: 100vh;" + self.server, theme=("theme_mode", "light"), style="max-height: 100vh; overflow: hidden;" ) as layout: layout.title.set_text("VTK Prompt UI") @@ -233,6 +243,9 @@ def _build_ui(self) -> None: build_content(layout, self) build_settings_dialog(layout, self) + with layout.footer as footer: + footer.hide() + def start(self) -> None: """Start the trame server.""" self.server.start()