import os from typing import Optional from nicegui import ui from components import AsyncElement from llm_connector import LLMBackend class MainPage(AsyncElement): backend: LLMBackend scene_manager = None # Will hold SceneManager instance selected_character: Optional[str] = None memory_viewer = None chat_container = None scene_info_container = None async def build(self): # pylint: disable=W0221 backend: LLMBackend = {'base_url': os.environ['BACKEND_BASE_URL'], 'api_token': os.environ['BACKEND_API_TOKEN'], 'model': os.environ['BACKEND_MODEL']} self.backend = backend # Initialize mock scene manager (will be replaced with real one) await self._initialize_scene() # Header with ui.header().classes('bg-gradient-to-r from-purple-600 to-indigo-600 text-white'): ui.label('🎭 Living Agents').classes('text-2xl font-bold') ui.label('Multi-Agent Roleplay with Stanford Memory Architecture').classes( 'text-sm opacity-90') self.classes('w-full') with self: # Main container with three columns with ui.row().classes('w-full p-4 gap-4'): # Left Panel - Scene Control & Characters with ui.column().classes('w-1/4 gap-4'): # Scene Information Card with ui.card().classes('w-full'): ui.label('📍 Scene Control').classes('text-lg font-bold mb-2') self.scene_info_container = ui.column().classes('w-full gap-2') with self.scene_info_container: self._create_scene_info() ui.separator() # Time controls with ui.row().classes('w-full gap-2 mt-2'): ui.button('⏰ +1 Hour', on_click=lambda: self._advance_time(1)).classes('flex-1') ui.button('📅 +1 Day', on_click=lambda: self._advance_time(24)).classes('flex-1') # Characters List with ui.card().classes('w-full'): ui.label('👥 Characters').classes('text-lg font-bold mb-2') # Character cards with ui.column().classes('w-full gap-2'): # Alice with ui.card().classes('w-full p-3 cursor-pointer hover:bg-gray-50').on( 'click', lambda: self._select_character('Alice')): with ui.row().classes('items-center gap-2'): ui.icon('person', size='sm').classes('text-purple-500') with ui.column().classes('flex-1'): ui.label('Alice').classes('font-semibold') ui.label('Literature Student, 23').classes( 'text-xs text-gray-500') with ui.row().classes('gap-1 mt-1'): ui.badge('📚 10 memories', color='purple').classes('text-xs') ui.badge('💭 0 reflections', color='indigo').classes('text-xs') # Bob with ui.card().classes('w-full p-3 cursor-pointer hover:bg-gray-50').on( 'click', lambda: self._select_character('Bob')): with ui.row().classes('items-center gap-2'): ui.icon('person', size='sm').classes('text-blue-500') with ui.column().classes('flex-1'): ui.label('Bob').classes('font-semibold') ui.label('Software Developer, 28').classes( 'text-xs text-gray-500') with ui.row().classes('gap-1 mt-1'): ui.badge('📚 8 memories', color='purple').classes('text-xs') ui.badge('💭 0 reflections', color='indigo').classes('text-xs') # Emma with ui.card().classes('w-full p-3 cursor-pointer hover:bg-gray-50').on( 'click', lambda: self._select_character('Emma')): with ui.row().classes('items-center gap-2'): ui.icon('person', size='sm').classes('text-pink-500') with ui.column().classes('flex-1'): ui.label('Emma').classes('font-semibold') ui.label('Barista & Artist, 25').classes( 'text-xs text-gray-500') with ui.row().classes('gap-1 mt-1'): ui.badge('📚 7 memories', color='purple').classes('text-xs') ui.badge('💭 0 reflections', color='indigo').classes('text-xs') # Character Summary - moved here to be under Characters with ui.card().classes('w-full'): ui.label('📝 Character Summary').classes('text-lg font-bold mb-2') self.character_summary = ui.column().classes('w-full') with self.character_summary: ui.label('Select a character to see their summary').classes( 'text-sm text-gray-500 italic') # Middle Panel - Interaction & Chat with ui.column().classes('w-1/2 gap-4'): # Interaction Controls with ui.card().classes('w-full'): ui.label('💬 Interactions').classes('text-lg font-bold mb-2') # Character-to-User interaction with ui.column().classes('w-full gap-2'): ui.label('Talk to Character').classes('font-semibold text-sm') with ui.row().classes('w-full gap-2'): self.user_input = ui.input( placeholder='Say something to the selected character...' ).classes('flex-1') ui.button('Send', on_click=self._send_to_character).props( 'icon=send color=primary') ui.separator() # Character-to-Character interaction with ui.column().classes('w-full gap-2 mt-2'): ui.label('Character Interaction').classes('font-semibold text-sm') with ui.row().classes('w-full gap-2'): self.char1_select = ui.select( ['Alice', 'Bob', 'Emma'], label='Character 1', value='Alice' ).classes('flex-1') self.char2_select = ui.select( ['Alice', 'Bob', 'Emma'], label='Character 2', value='Bob' ).classes('flex-1') self.interaction_context = ui.input( placeholder='Context for interaction...' ).classes('w-full') ui.button( 'Make them interact', on_click=self._character_interaction ).props('icon=forum color=secondary').classes('w-full') # Chat History with ui.card().classes('w-full flex-1'): with ui.row().classes('w-full items-center mb-2'): ui.label('🗨️ Conversation History').classes('text-lg font-bold') ui.space() ui.button(icon='delete', on_click=self._clear_chat).props('flat round size=sm') # Scrollable chat container with ui.scroll_area().classes('w-full h-96 border rounded p-2'): self.chat_container = ui.column().classes('w-full gap-2') with self.chat_container: # Welcome message with ui.chat_message(name='System', sent=False).classes('w-full'): ui.label( 'Welcome to the Living Agents roleplay system! ' 'Select a character and start interacting.' ).classes('text-sm') # Right Panel - Memory Stream with ui.column().classes('w-1/4 gap-4'): # Memory Stream Viewer with ui.card().classes('w-full flex-1'): with ui.row().classes('w-full items-center mb-2'): ui.label('🧠 Memory Stream').classes('text-lg font-bold') ui.space() # Memory type filter self.memory_filter = ui.select( ['all', 'observation', 'reflection', 'plan'], value='all', on_change=self._update_memory_view ).props('dense outlined').classes('w-24') # Scrollable memory list with ui.scroll_area().classes('w-full h-96 border rounded p-2'): self.memory_viewer = ui.column().classes('w-full gap-2') with self.memory_viewer: ui.label('Select a character to view memories').classes('text-sm text-gray-500 italic') # Footer with stats with ui.footer().classes('bg-gray-100 text-gray-600 text-sm'): with ui.row().classes('w-full justify-center items-center gap-4'): ui.label('🎯 Stanford Memory Architecture') ui.label('|') self.stats_label = ui.label('Total Memories: 0 | Reflections: 0') ui.label('|') ui.label('⚡ Powered by Custom LLM Connector') def _create_scene_info(self): """Create scene information display""" with ui.row().classes('w-full justify-between'): ui.label('Location:').classes('text-sm font-semibold') ui.label('Cozy Coffee Shop').classes('text-sm') with ui.row().classes('w-full justify-between'): ui.label('Time:').classes('text-sm font-semibold') ui.label('2:30 PM').classes('text-sm') with ui.row().classes('w-full justify-between'): ui.label('Atmosphere:').classes('text-sm font-semibold') ui.label('Quiet and peaceful').classes('text-sm') async def _initialize_scene(self): """Initialize the scene with mock data (will be replaced with real SceneManager)""" # This will be replaced with actual SceneManager initialization ui.notify('🎬 Scene initialized with 3 characters', type='positive') async def _select_character(self, character_name: str): """Select a character and update UI""" self.selected_character = character_name ui.notify(f'Selected: {character_name}', type='info') # Update character summary self.character_summary.clear() with self.character_summary: ui.label(f'{character_name}').classes('font-bold text-lg') ui.separator() if character_name == 'Alice': ui.label('Age: 23').classes('text-sm') ui.label('Occupation: Graduate student').classes('text-sm') ui.label('Personality: Introverted, observant, loves mystery novels').classes('text-sm mt-2') ui.label('Current Goal: Finish thesis chapter').classes('text-sm text-blue-600 mt-2') elif character_name == 'Bob': ui.label('Age: 28').classes('text-sm') ui.label('Occupation: Senior Developer').classes('text-sm') ui.label('Personality: Confident, helpful, technical').classes('text-sm mt-2') ui.label('Current Goal: Launch new feature').classes('text-sm text-blue-600 mt-2') elif character_name == 'Emma': ui.label('Age: 25').classes('text-sm') ui.label('Occupation: Barista & Art Student').classes('text-sm') ui.label('Personality: Energetic, social, creative').classes('text-sm mt-2') ui.label('Current Goal: Organize art show').classes('text-sm text-blue-600 mt-2') # Update memory viewer await self._update_memory_view() async def _update_memory_view(self): """Update the memory stream viewer""" if not self.selected_character: return self.memory_viewer.clear() with self.memory_viewer: # Mock memories for demonstration memories = [ ('observation', 'Arrived at the coffee shop', 8, '10:00 AM'), ('observation', 'Ordered my usual latte', 3, '10:05 AM'), ('observation', 'Saw a familiar face by the window', 6, '10:30 AM'), ('reflection', 'I seem to come here when I need to focus', 7, '11:00 AM'), ('plan', 'Work on thesis for 2 hours', 5, '11:30 AM'), ] filter_type = self.memory_filter.value for mem_type, description, importance, time in memories: if filter_type == 'all' or filter_type == mem_type: with ui.card().classes('w-full p-2'): with ui.row().classes('w-full items-start gap-2'): # Memory type icon if mem_type == 'observation': ui.icon('visibility', size='xs').classes('text-blue-500 mt-1') elif mem_type == 'reflection': ui.icon('psychology', size='xs').classes('text-purple-500 mt-1') else: ui.icon('event', size='xs').classes('text-green-500 mt-1') with ui.column().classes('flex-1'): ui.label(description).classes('text-sm') with ui.row().classes('gap-2 mt-1'): ui.badge(f'⭐ {importance}', color='orange').classes('text-xs') ui.label(time).classes('text-xs text-gray-500') async def _send_to_character(self): """Send message to selected character""" if not self.selected_character: ui.notify('Please select a character first', type='warning') return if not self.user_input.value: return message = self.user_input.value self.user_input.value = '' # Add user message to chat with self.chat_container: with ui.chat_message(name='You', sent=True).classes('w-full'): ui.label(message).classes('text-sm') # Mock response (will be replaced with actual agent response) with self.chat_container: with ui.chat_message(name=self.selected_character, sent=False).classes('w-full'): spinner = ui.spinner('dots') # Simulate thinking await ui.run_javascript('window.scrollTo(0, document.body.scrollHeight)') ui.notify(f'🧠 {self.selected_character} is thinking...', type='info') # Mock response after delay await ui.timer(1.5, lambda: self._add_character_response(spinner)) def _add_character_response(self, spinner): """Add character response to chat""" spinner.delete() parent = spinner.parent_slot.parent with parent: if self.selected_character == 'Alice': ui.label("*nervously adjusts glasses* Oh, um, hello there. I was just working on my thesis chapter about Victorian gothic literature. The coffee here helps me concentrate.").classes('text-sm') elif self.selected_character == 'Bob': ui.label("Hey! Yeah, I'm actually debugging some code right now. This new feature is giving me some trouble, but I think I'm close to solving it. How's your day going?").classes('text-sm') else: ui.label("Hi! Welcome to our little coffee shop! I just finished a new sketch during my break - been trying to capture the afternoon light through the windows. Can I get you anything?").classes('text-sm') async def _character_interaction(self): """Make two characters interact""" char1 = self.char1_select.value char2 = self.char2_select.value context = self.interaction_context.value or "meeting at the coffee shop" if char1 == char2: ui.notify("Characters can't interact with themselves", type='warning') return # Add interaction to chat with self.chat_container: with ui.chat_message(name='Scene', sent=False).classes('w-full'): ui.label(f'🎬 {char1} and {char2} interact: {context}').classes('text-sm italic text-gray-600') # Mock interaction with ui.chat_message(name=char1, sent=False).classes('w-full'): ui.label("Oh, hi there! I didn't expect to see you here today.").classes('text-sm') with ui.chat_message(name=char2, sent=False).classes('w-full'): ui.label("Hey! Yeah, this is my usual spot. How have you been?").classes('text-sm') ui.notify(f'💬 {char1} and {char2} had an interaction', type='positive') def _advance_time(self, hours: int): """Advance scene time""" ui.notify(f'⏰ Advanced time by {hours} hour(s)', type='info') # Update scene info self.scene_info_container.clear() with self.scene_info_container: self._create_scene_info() # Add time advancement to chat with self.chat_container: with ui.chat_message(name='System', sent=False).classes('w-full'): ui.label(f'⏰ Time advanced by {hours} hour(s). Characters update their plans...').classes('text-sm italic text-gray-600') def _clear_chat(self): """Clear chat history""" self.chat_container.clear() with self.chat_container: with ui.chat_message(name='System', sent=False).classes('w-full'): ui.label('Chat history cleared. Ready for new interactions!').classes('text-sm') ui.notify('Chat cleared', type='info')