init
This commit is contained in:
367
pages/page_main.py
Normal file
367
pages/page_main.py
Normal file
@@ -0,0 +1,367 @@
|
||||
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')
|
||||
Reference in New Issue
Block a user