import os from typing import Optional, List, Dict from nicegui import ui, binding from components import AsyncElement from llm_connector import LLMBackend from living_agents import Character from .character import CharacterCard from .scene_control import SceneControl from .conversation_history import ConversationHistory from .create_character import CharacterTemplateImportDialog, CharacterCreationDialog from living_agents import RoleplaySystem, Character, LLMAgent from utils import load_characters import logging logger = logging.getLogger(__name__) class MainPage(AsyncElement): backend: LLMBackend system: RoleplaySystem selected_character: Optional[Character] = None chat_container = None scene_info_container = None characters: List[Character] initial_memories: Dict[Character, List[str]] memory_filter: ui.select is_generating = binding.BindableProperty() async def setup_characters(self): self.is_generating = True await self.system.setup_characters(self.characters) for char, memories in self.initial_memories.items(): for memory in memories: await self.system.agents[char].perceive(memory) self.is_generating = False async def build(self): # pylint: disable=W0221 loaded_characters = load_characters() self.characters = [char for char in loaded_characters] self.initial_memories = {char: memories for char, memories in loaded_characters.items()} self.system = RoleplaySystem() # Header with self.element: with ui.row().classes( 'bg-gradient-to-r from-purple-600 to-indigo-600 text-white q-pa-sm items-center w-full'): ui.label('🎭 Living Agents').classes('text-2xl font-bold') ui.label('Multi-Agent Roleplay with Stanford Memory Architecture').classes('text-sm opacity-90') # Top Row: Characters with ui.row().classes('w-full gap-2 items-center'): for char in self.characters: (await CharacterCard.create(ui.card, char)) \ .classes('p-2 cursor-pointer hover:bg-gray-50 gap-1') \ .on('click', lambda c=char: self._select_character(c)) ui.space() ui.button(icon='add', on_click=self._open_character_creation).props('fab') # Selected Character with ui.row().classes('w-full flex-1'): # Character card with ui.card().classes('h-full max-w-96'): await self._character_view() # type: ignore with ui.row().classes('w-full items-center mb-2'): ui.label('🧠 Memories').classes('text-lg font-bold') ui.space() # Memory type filter self.memory_filter = ui.select(['all', 'observation', 'reflection', 'plan'], value='all', on_change=self._memory_view.refresh).props( 'dense outlined').classes('w-24') await self._memory_view() # type: ignore # Conversation History (takes full height) with ui.card().classes('flex-1 h-full'): ui.label('🗨️ Conversation History').classes('text-lg font-bold') # Scrollable chat container - takes remaining height with ui.scroll_area().classes('w-full').style('height: calc(100% - 3rem)'): 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') # ui.timer(0.5, self.setup_characters, once=True) # Main container as a column with ui.row().classes('w-full'): with ui.column().classes('flex-1 bg-red-200 h-full'): # Interactions Card with ui.card().classes('w-full'): ui.label('💬 Interactions').classes('text-lg font-bold mb-1') # Character-to-User interaction with ui.column().classes('w-full gap-1'): ui.label('Talk to Character').classes('font-semibold text-sm') with ui.row().classes('w-full gap-1'): self.user_input = ui.input( placeholder='Say something to the selected character...').classes('flex-1').props( 'dense') ui.button('Send', on_click=self._send_to_character).props( 'icon=send color=primary').bind_enabled_from(self, 'is_generating', backward=lambda v: not v) 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').props('dense') self.char2_select = ui.select(['Alice', 'Bob', 'Emma'], label='Character 2', value='Bob').classes('flex-1').props('dense') self.interaction_context = ui.input(placeholder='Context for interaction...').classes( 'w-full').props('dense') ui.button('Make them interact', on_click=self._character_interaction).props( 'icon=forum color=secondary').classes('w-full').bind_enabled_from(self, 'is_generating', backward=lambda v: not v) # Bottom Row: Scene Control, Interaction with ui.card().classes('h-full'): ui.label('📍 Scene Control').classes('text-lg font-bold mb-1') with ui.row().classes('w-full'): with ui.column().classes('gap-2'): with ui.row().classes('justify-between'): ui.label('Location:').classes('text-sm font-semibold') ui.label('Cozy Coffee Shop').classes('text-sm') with ui.row().classes('justify-between'): ui.label('Time:').classes('text-sm font-semibold') ui.label('2:30 PM').classes('text-sm') with ui.row().classes('justify-between'): ui.label('Atmosphere:').classes('text-sm font-semibold') ui.label('Quiet and peaceful').classes('text-sm') ui.space() with ui.column().classes('gap-2'): ui.button('⏰ +1 Hour').classes('flex-1').bind_enabled_from(self, 'is_generating', backward=lambda v: not v) ui.button('📅 +1 Day').classes('flex-1').bind_enabled_from(self, 'is_generating', backward=lambda v: not v) # move outside of page generation # ui.timer(0.5, self.setup_characters, once=True) async def _open_character_creation(self): # with ui.dialog() as dialog, ui.card(): # (await CharacterCreationDialog.create(ui.column)).classes('w-full') dialog = (await CharacterTemplateImportDialog.create(ui.dialog)).classes('w-full') result = await dialog if result: dialog = (await CharacterCreationDialog.create(ui.dialog, template=result)).classes('w-full').props( 'persistent') result = await dialog print(result) async def _select_character(self, character: Character): """Select a character and update UI""" self.selected_character = character self._character_view.refresh() # type: ignore self._memory_view.refresh() @ui.refreshable async def _character_view(self): with ui.column().classes('w-full gap-0'): if self.selected_character is None: ui.label('Select a character to see their summary and Memories').classes( 'text-sm text-gray-500 italic') return ui.label(f'{self.selected_character.name}').classes('font-bold text-lg') ui.separator() with ui.column().classes('gap-0'): ui.label(f'Age: {self.selected_character.age}').classes('text-sm') ui.label(f'Occupation: {self.selected_character.occupation}').classes('text-sm') ui.label(f'Personality: {self.selected_character.personality}').classes('text-sm') ui.separator() ui.label('Goals').classes('font-bold text-lg') with ui.list().props('dense separator'): for goal in self.selected_character.goals: ui.item(goal) @ui.refreshable async def _memory_view(self): """Update the memory stream viewer""" if self.selected_character: with ui.scroll_area().classes('w-full border rounded p-0 flex-1'): with ui.column().classes('gap-2'): memories = self.system.get_character_memories(self.selected_character, self.memory_filter.value if self.memory_filter.value else 'all') for memory in memories: with ui.card().classes('w-full p-1'): with ui.row().classes('gap-2'): with ui.column().classes('flex-1'): ui.label(memory.description).classes('text-sm') with ui.row().classes('gap-2 mt-1 items-center'): if memory.memory_type == 'observation': ui.icon('visibility', size='xs').classes('text-blue-500 mt-1') elif memory.memory_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') ui.badge(f'⭐ {memory.importance_score}', color='orange').classes('text-xs') ui.label(memory.creation_time.strftime('%Y-%m-%d %H:%M:%S')).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.name, 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')