too much
This commit is contained in:
24
pages/character.py
Normal file
24
pages/character.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from nicegui import ui
|
||||
from components import AsyncElement
|
||||
from living_agents import Character
|
||||
|
||||
|
||||
class CharacterCard(AsyncElement):
|
||||
character: Character
|
||||
|
||||
async def build(self, character: Character) -> None:
|
||||
self.character = character
|
||||
|
||||
with self.element:
|
||||
with ui.row().classes('items-center gap-1'):
|
||||
ui.icon('person', size='sm').classes('text-purple-500')
|
||||
with ui.column().classes('gap-1'):
|
||||
ui.label().bind_text_from(self.character, 'name').classes('font-semibold')
|
||||
ui.label(f'{self.character.occupation}, {self.character.age}').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')
|
||||
|
||||
async def _select_character(self):
|
||||
await self._select_character_callback(self.character)
|
||||
print(self.character)
|
||||
32
pages/conversation_history.py
Normal file
32
pages/conversation_history.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from nicegui import ui
|
||||
from components import AsyncElement
|
||||
from living_agents import Character
|
||||
|
||||
|
||||
class ConversationHistory(AsyncElement):
|
||||
|
||||
chat_container: ui.column
|
||||
|
||||
async def build(self) -> None:
|
||||
|
||||
self.classes('w-full')
|
||||
with self:
|
||||
with ui.column().classes('flex-1 gap-4'):
|
||||
|
||||
# Conversation History (takes full height)
|
||||
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').props('flat round size=sm')
|
||||
|
||||
# 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')
|
||||
425
pages/create_character.py
Normal file
425
pages/create_character.py
Normal file
@@ -0,0 +1,425 @@
|
||||
import random
|
||||
from enum import Enum
|
||||
|
||||
import yaml
|
||||
from docutils.nodes import reference
|
||||
from nicegui import ui
|
||||
from typing import TypedDict, List, Dict, Optional
|
||||
from pathlib import Path
|
||||
from living_agents import Character, CharacterAgent, LLMAgent
|
||||
from pprint import pprint
|
||||
import asyncio
|
||||
|
||||
from components import AsyncElement
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
import yaml
|
||||
|
||||
from living_agents.datatypes import CharacterTemplate
|
||||
|
||||
|
||||
class CharacterTemplateImportDialog(AsyncElement):
|
||||
_selected_template: Optional[CharacterTemplate]
|
||||
_templates: List[CharacterTemplate]
|
||||
|
||||
async def build(self, *args, **kwargs) -> None:
|
||||
self._selected_template = None
|
||||
self._templates = []
|
||||
|
||||
with self.element:
|
||||
with ui.card().classes('max-w-4xl mx-auto shadow-xl'):
|
||||
await self._header()
|
||||
await self._load_templates()
|
||||
if not self._templates:
|
||||
await self._empty_state()
|
||||
else:
|
||||
with ui.card_section().classes('p-2 w-full'):
|
||||
await self._template_selection()
|
||||
await self._template_preview()
|
||||
|
||||
async def _header(self):
|
||||
"""Beautiful header section"""
|
||||
with ui.card_section().classes('bg-gradient-to-r from-purple-50 to-blue-50 border-b'):
|
||||
with ui.row().classes('w-full items-center'):
|
||||
ui.icon('library_books', size='2.5rem').classes('text-purple-600')
|
||||
with ui.column().classes('flex-1 ml-4'):
|
||||
ui.label('Import Character Template').classes('text-2xl font-bold text-gray-800')
|
||||
ui.label('Choose a character template to create your roleplay agent').classes('text-gray-600')
|
||||
|
||||
async def _load_templates(self):
|
||||
"""Load templates with error handling"""
|
||||
try:
|
||||
self._templates = self.load_character_templates()
|
||||
except Exception as e:
|
||||
with ui.card_section():
|
||||
with ui.row().classes('items-center p-4 bg-red-50 border border-red-200 rounded-lg'):
|
||||
ui.icon('error', size='2rem').classes('text-red-600')
|
||||
with ui.column().classes('ml-3'):
|
||||
ui.label('Failed to load templates').classes('font-bold text-red-800')
|
||||
ui.label(str(e)).classes('text-red-600 text-sm')
|
||||
return
|
||||
|
||||
async def _empty_state(self):
|
||||
"""Show when no templates found"""
|
||||
with ui.card_section().classes('text-center py-12'):
|
||||
ui.icon('folder_open', size='4rem').classes('text-gray-400 mb-4')
|
||||
ui.label('No character templates found').classes('text-xl font-bold text-gray-600 mb-2')
|
||||
ui.label('Add .yml files to the character_templates directory').classes('text-gray-500')
|
||||
|
||||
async def _template_selection(self):
|
||||
"""Simple but rich dropdown selection"""
|
||||
ui.label('Choose Character Template').classes('text-lg font-bold text-gray-800 mb-3')
|
||||
|
||||
# Prepare rich options
|
||||
options = {}
|
||||
for i, template in enumerate(self._templates):
|
||||
obs = len(template.get('observations', []))
|
||||
refl = len(template.get('reflections', []))
|
||||
plans = len(template.get('plans', []))
|
||||
total = obs + refl + plans
|
||||
|
||||
# Create rich display text
|
||||
options[i] = f"📚 {template['name']} ({total} memories: {obs}obs, {refl}refl, {plans}plans)"
|
||||
|
||||
# Styled select
|
||||
select = ui.select(
|
||||
options,
|
||||
label='Template',
|
||||
on_change=lambda e: self._select_template_by_index(e.value),
|
||||
clearable=False
|
||||
).classes('w-full').props('outlined dense use-input hide-selected fill-input')
|
||||
|
||||
# Custom styling for the dropdown
|
||||
select.props('menu-props="dense"')
|
||||
select.style('font-family: ui-monospace, monospace;') # Monospace for alignment
|
||||
|
||||
def _select_template_by_index(self, index: int):
|
||||
"""Select template by index from dropdown"""
|
||||
if 0 <= index < len(self._templates):
|
||||
self._selected_template = self._templates[index]
|
||||
self._template_preview.refresh()
|
||||
|
||||
@ui.refreshable
|
||||
async def _template_preview(self):
|
||||
"""Right panel showing template details"""
|
||||
if not self._selected_template:
|
||||
await self._no_selection_state()
|
||||
return
|
||||
|
||||
# Memory sections with tabs
|
||||
await self._memory_sections()
|
||||
ui.button('Create Character', icon='person_add',
|
||||
on_click=lambda: self.submit(self._selected_template)).classes(
|
||||
'bg-gradient-to-r from-blue-500 to-purple-600 text-white px-6 py-2 w-full')
|
||||
|
||||
async def _no_selection_state(self):
|
||||
"""Show when no template is selected"""
|
||||
with ui.column().classes('w-full h-96 items-center justify-center'):
|
||||
ui.icon('touch_app', size='3rem').classes('text-gray-400 mb-4')
|
||||
ui.label('Select a template to preview').classes('text-xl text-gray-600 mb-2')
|
||||
ui.label('Choose from the list on the left to see the character details').classes('text-gray-500')
|
||||
|
||||
async def _memory_sections(self):
|
||||
"""Tabbed view of memory sections"""
|
||||
with ui.tabs().classes('w-full') as tabs:
|
||||
observations_tab = ui.tab('Observations', icon='visibility')
|
||||
reflections_tab = ui.tab('Reflections', icon='lightbulb')
|
||||
plans_tab = ui.tab('Plans', icon='track_changes')
|
||||
|
||||
with ui.tab_panels(tabs, value=observations_tab).classes('w-full mt-4'):
|
||||
# Observations panel
|
||||
with ui.tab_panel(observations_tab):
|
||||
await self._render_memory_section('observations', 'blue')
|
||||
|
||||
# Reflections panel
|
||||
with ui.tab_panel(reflections_tab):
|
||||
await self._render_memory_section('reflections', 'purple')
|
||||
|
||||
# Plans panel
|
||||
with ui.tab_panel(plans_tab):
|
||||
await self._render_memory_section('plans', 'green')
|
||||
|
||||
async def _render_memory_section(self, section_key: str, color: str):
|
||||
"""Render a memory section with scrollable list"""
|
||||
memories = self._selected_template.get(section_key, [])
|
||||
|
||||
if not memories:
|
||||
with ui.card().classes(f'border-{color}-200 bg-{color}-50'):
|
||||
with ui.card_section().classes('text-center py-8'):
|
||||
ui.label(f'No {section_key} defined').classes('text-gray-600')
|
||||
return
|
||||
|
||||
# Scrollable memory list
|
||||
with ui.scroll_area().classes('border border-gray-200 rounded-lg'):
|
||||
with ui.list().classes('w-full'):
|
||||
for i, memory in enumerate(memories):
|
||||
await self._render_memory_item(memory, i, color)
|
||||
|
||||
async def _render_memory_item(self, memory: str, index: int, color: str):
|
||||
"""Individual memory item with nice styling"""
|
||||
with ui.item().classes('border-b border-gray-100 hover:bg-gray-50 transition-colors'):
|
||||
# with ui.item_section().classes('p-4'):
|
||||
with ui.row().classes('w-full items-start'):
|
||||
# Index badge
|
||||
ui.badge(str(index + 1), color=color).classes('mr-4 mt-1 text-xs min-w-6')
|
||||
|
||||
# Memory text (with text wrapping)
|
||||
ui.label(memory).classes('flex-1 text-gray-800 leading-relaxed').style(
|
||||
'white-space: normal; word-wrap: break-word;')
|
||||
|
||||
def _select_template(self, template: CharacterTemplate):
|
||||
"""Select a template and refresh the preview"""
|
||||
self._selected_template = template
|
||||
# self.main_content.refresh()
|
||||
|
||||
@staticmethod
|
||||
def load_character_templates() -> List[CharacterTemplate]:
|
||||
"""Load character templates from YAML files"""
|
||||
characters_dir = 'character_templates'
|
||||
characters_path = Path(characters_dir)
|
||||
character_templates: List[CharacterTemplate] = []
|
||||
|
||||
if not characters_path.exists():
|
||||
raise FileNotFoundError(f"Characters directory '{characters_dir}' not found")
|
||||
|
||||
if not characters_path.is_dir():
|
||||
raise ValueError(f"'{characters_dir}' is not a directory")
|
||||
|
||||
# Find all YAML files
|
||||
yaml_files = list(characters_path.glob("*.yaml")) + list(characters_path.glob("*.yml"))
|
||||
|
||||
if not yaml_files:
|
||||
raise ValueError(f"No YAML files found in '{characters_dir}'")
|
||||
|
||||
for yaml_file in yaml_files:
|
||||
try:
|
||||
with open(yaml_file, 'r', encoding='utf-8') as file:
|
||||
data = yaml.safe_load(file)
|
||||
|
||||
required_fields = ['observations', 'reflections', 'plans']
|
||||
missing_fields = [field for field in required_fields if field not in data]
|
||||
|
||||
if missing_fields:
|
||||
print(f"Warning: File '{yaml_file.name}' missing fields: {missing_fields}")
|
||||
continue
|
||||
|
||||
# Create template
|
||||
character_templates.append(CharacterTemplate(
|
||||
name=str(yaml_file.stem).replace('_', ' ').title(),
|
||||
observations=data.get('observations', []),
|
||||
reflections=data.get('reflections', []),
|
||||
plans=data.get('plans', []),
|
||||
yaml_file=yaml_file.name
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading '{yaml_file.name}': {e}")
|
||||
continue
|
||||
|
||||
return character_templates
|
||||
|
||||
def get_selected_template(self) -> Optional[CharacterTemplate]:
|
||||
"""Get the currently selected template"""
|
||||
return self._selected_template
|
||||
|
||||
|
||||
class CreationStep(Enum):
|
||||
STARTING = "starting"
|
||||
OBSERVATIONS = "observations"
|
||||
REFLECTIONS = "reflections"
|
||||
PLANS = "plans"
|
||||
OVERVIEW = "overview"
|
||||
COMPLETE = "complete"
|
||||
|
||||
|
||||
class CharacterCreationDialog(AsyncElement):
|
||||
_template: CharacterTemplate
|
||||
_character: Optional[Character]
|
||||
_character_agent: CharacterAgent
|
||||
_current_step: CreationStep
|
||||
_steps_completed: set[CreationStep]
|
||||
_creation_task: Optional[asyncio.Task]
|
||||
|
||||
async def build(self, template: CharacterTemplate, *args, **kwargs) -> None:
|
||||
self._template = template
|
||||
print(template)
|
||||
self._character = None
|
||||
self._current_step = CreationStep.STARTING
|
||||
self._steps_completed = set()
|
||||
self._creation_task = None
|
||||
|
||||
with self.element:
|
||||
with ui.card().classes('w-96 mx-auto shadow-lg'):
|
||||
await self.character_creation_view()
|
||||
|
||||
# Start creation automatically
|
||||
self._creation_task = asyncio.create_task(self._create_character_internal())
|
||||
|
||||
@ui.refreshable
|
||||
async def character_creation_view(self):
|
||||
if self._character:
|
||||
await self._completion_view()
|
||||
return
|
||||
|
||||
await self._progress_view()
|
||||
|
||||
async def _progress_view(self):
|
||||
# Header
|
||||
with ui.row().classes('w-full items-center mb-6 p-4 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg'):
|
||||
ui.icon('psychology', size='2rem').classes('text-blue-600')
|
||||
with ui.column().classes('flex-1 ml-3'):
|
||||
ui.label(f'Creating {self._template["name"]}').classes('text-xl font-bold text-gray-800')
|
||||
ui.label('Building character memories and personality...').classes('text-sm text-gray-600')
|
||||
|
||||
# Progress bar
|
||||
steps = [CreationStep.OBSERVATIONS, CreationStep.REFLECTIONS, CreationStep.PLANS, CreationStep.OVERVIEW]
|
||||
progress_value = (len(self._steps_completed) / len(steps)) * 100
|
||||
ui.linear_progress(value=progress_value / 100).classes('mb-6')
|
||||
ui.label(f'{len(self._steps_completed)}/{len(steps)} steps complete').classes(
|
||||
'text-center text-sm text-gray-600 mb-4')
|
||||
|
||||
# Steps list
|
||||
steps_info = [
|
||||
(CreationStep.OBSERVATIONS, 'visibility', 'Loading Observations',
|
||||
'Injecting basic memories and experiences'),
|
||||
(CreationStep.REFLECTIONS, 'lightbulb', 'Processing Reflections',
|
||||
'Generating deeper insights and understanding'),
|
||||
(CreationStep.PLANS, 'track_changes', 'Installing Plans', 'Setting goals and future intentions'),
|
||||
(CreationStep.OVERVIEW, 'summarize', 'Extracting Profile', 'Creating character overview with AI'),
|
||||
]
|
||||
|
||||
with ui.list().classes('w-full space-y-2'):
|
||||
for step, icon, title, description in steps_info:
|
||||
await self._render_step_item(step, icon, title, description)
|
||||
|
||||
async def _render_step_item(self, step: CreationStep, icon: str, title: str, description: str):
|
||||
# Status logic
|
||||
if step in self._steps_completed:
|
||||
status = 'complete'
|
||||
icon_color = 'text-green-600'
|
||||
bg_color = 'bg-green-50 border-green-200'
|
||||
elif step == self._current_step:
|
||||
status = 'current'
|
||||
icon_color = 'text-blue-600'
|
||||
bg_color = 'bg-blue-50 border-blue-200'
|
||||
else:
|
||||
status = 'pending'
|
||||
icon_color = 'text-gray-400'
|
||||
bg_color = 'bg-gray-50 border-gray-200'
|
||||
|
||||
with ui.item().classes(f'rounded-lg border-2 {bg_color} transition-all duration-300'):
|
||||
with ui.row().classes('w-full items-center p-3'):
|
||||
|
||||
# Icon/Spinner
|
||||
if status == 'complete':
|
||||
ui.icon('check_circle').classes(f'{icon_color} text-2xl')
|
||||
elif status == 'current':
|
||||
ui.spinner(size='lg').classes('text-blue-600')
|
||||
else:
|
||||
ui.icon(icon).classes(f'{icon_color} text-2xl')
|
||||
|
||||
# Text
|
||||
with ui.column().classes('flex-1 ml-4'):
|
||||
ui.label(title).classes(
|
||||
f'font-semibold {"text-green-800" if status == "complete" else "text-blue-800" if status == "current" else "text-gray-600"}')
|
||||
ui.label(description).classes('text-sm text-gray-600 mt-1')
|
||||
|
||||
# Badge
|
||||
if status == 'complete':
|
||||
ui.badge('Done', color='positive').classes('ml-auto')
|
||||
elif status == 'current':
|
||||
ui.badge('Processing...', color='info').classes('ml-auto animate-pulse')
|
||||
|
||||
async def _completion_view(self):
|
||||
with ui.column().classes('w-full items-center p-6'):
|
||||
# Success icon
|
||||
with ui.row().classes('items-center justify-center mb-6 p-4 bg-green-50 rounded-full w-20 h-20 mx-auto'):
|
||||
ui.icon('celebration', size='3rem').classes('text-green-600')
|
||||
|
||||
ui.label(f'{self._character.name} is ready!').classes('text-2xl font-bold text-green-800 text-center mb-2')
|
||||
ui.label('Character creation completed successfully').classes('text-gray-600 text-center mb-6')
|
||||
|
||||
# Character preview
|
||||
with ui.card().classes('w-full bg-gradient-to-br from-green-50 to-blue-50 border border-green-200'):
|
||||
with ui.card_section():
|
||||
with ui.row().classes('items-start gap-4'):
|
||||
with ui.avatar().classes('bg-gradient-to-br from-blue-400 to-purple-600 text-white text-xl'):
|
||||
ui.label(self._character.name[0].upper())
|
||||
|
||||
with ui.column().classes('flex-1'):
|
||||
ui.label(f'{self._character.name}, {self._character.age}').classes('font-bold text-lg')
|
||||
ui.label(self._character.occupation).classes('text-blue-600 font-medium')
|
||||
ui.label(self._character.personality).classes('text-gray-700 text-sm mt-1')
|
||||
|
||||
# Done button
|
||||
with ui.row().classes('gap-3 mt-6'):
|
||||
ui.button('Start Chatting', icon='chat').classes(
|
||||
'bg-gradient-to-r from-blue-500 to-purple-600 text-white')
|
||||
|
||||
async def _create_character_internal(self):
|
||||
"""Mock character creation with realistic timing"""
|
||||
try:
|
||||
# self._character = Character(name=self._template['name'])
|
||||
# self._character_agent = CharacterAgent()
|
||||
# Step 1: Observations
|
||||
self._current_step = CreationStep.OBSERVATIONS
|
||||
self.character_creation_view.refresh()
|
||||
await asyncio.sleep(2.0) # Simulate loading observations
|
||||
|
||||
self._steps_completed.add(CreationStep.OBSERVATIONS)
|
||||
self.character_creation_view.refresh()
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
# Step 2: Reflections
|
||||
self._current_step = CreationStep.REFLECTIONS
|
||||
self.character_creation_view.refresh()
|
||||
await asyncio.sleep(2.8) # Reflections take longer
|
||||
|
||||
self._steps_completed.add(CreationStep.REFLECTIONS)
|
||||
self.character_creation_view.refresh()
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
# Step 3: Plans
|
||||
self._current_step = CreationStep.PLANS
|
||||
self.character_creation_view.refresh()
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
self._steps_completed.add(CreationStep.PLANS)
|
||||
self.character_creation_view.refresh()
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
# Step 4: AI Overview Extraction
|
||||
self._current_step = CreationStep.OVERVIEW
|
||||
self.character_creation_view.refresh()
|
||||
await asyncio.sleep(3.2) # AI takes longest
|
||||
|
||||
# Mock character creation
|
||||
self._character = self._create_mock_character()
|
||||
|
||||
self._steps_completed.add(CreationStep.OVERVIEW)
|
||||
self._current_step = CreationStep.COMPLETE
|
||||
|
||||
self.character_creation_view.refresh()
|
||||
|
||||
except Exception as e:
|
||||
ui.notify(f'Error creating character: {str(e)}', color='negative')
|
||||
|
||||
def _create_mock_character(self) -> Character:
|
||||
"""Create mock character based on template"""
|
||||
mock_personalities = [
|
||||
"Shy and thoughtful with hidden depths",
|
||||
"Confident and outgoing tech enthusiast",
|
||||
"Creative and energetic with artistic flair"
|
||||
]
|
||||
|
||||
return Character(
|
||||
name=self._template["name"],
|
||||
age=random.randint(22, 35),
|
||||
personality=random.choice(mock_personalities),
|
||||
occupation=f"{self._template['name']}'s profession",
|
||||
location="Coffee shop"
|
||||
)
|
||||
|
||||
def get_created_character(self) -> Optional[Character]:
|
||||
return self._character
|
||||
@@ -1,129 +1,111 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
from nicegui import ui
|
||||
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
|
||||
scene_manager = None # Will hold SceneManager instance
|
||||
selected_character: Optional[str] = None
|
||||
memory_viewer = None
|
||||
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
|
||||
|
||||
backend: LLMBackend = {'base_url': os.environ['BACKEND_BASE_URL'],
|
||||
'api_token': os.environ['BACKEND_API_TOKEN'],
|
||||
'model': os.environ['BACKEND_MODEL']}
|
||||
loaded_characters = load_characters()
|
||||
|
||||
self.backend = backend
|
||||
|
||||
# Initialize mock scene manager (will be replaced with real one)
|
||||
await self._initialize_scene()
|
||||
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 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')
|
||||
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')
|
||||
|
||||
self.classes('w-full')
|
||||
with self:
|
||||
# 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')
|
||||
|
||||
# Main container with three columns
|
||||
with ui.row().classes('w-full p-4 gap-4'):
|
||||
# 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')
|
||||
|
||||
# Left Panel - Scene Control & Characters
|
||||
with ui.column().classes('w-1/4 gap-4'):
|
||||
# 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')
|
||||
|
||||
# Scene Information Card
|
||||
# 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('📍 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')
|
||||
ui.label('💬 Interactions').classes('text-lg font-bold mb-1')
|
||||
|
||||
# Character-to-User interaction
|
||||
with ui.column().classes('w-full gap-2'):
|
||||
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-2'):
|
||||
with ui.row().classes('w-full gap-1'):
|
||||
self.user_input = ui.input(
|
||||
placeholder='Say something to the selected character...'
|
||||
).classes('flex-1')
|
||||
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')
|
||||
'icon=send color=primary').bind_enabled_from(self, 'is_generating',
|
||||
backward=lambda v: not v)
|
||||
|
||||
ui.separator()
|
||||
|
||||
@@ -131,153 +113,101 @@ class MainPage(AsyncElement):
|
||||
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')
|
||||
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')
|
||||
|
||||
# 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')
|
||||
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)
|
||||
|
||||
# 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')
|
||||
# move outside of page generation
|
||||
# ui.timer(0.5, self.setup_characters, once=True)
|
||||
|
||||
# Right Panel - Memory Stream
|
||||
with ui.column().classes('w-1/4 gap-4'):
|
||||
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)
|
||||
|
||||
# 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):
|
||||
async def _select_character(self, character: Character):
|
||||
"""Select a character and update UI"""
|
||||
self.selected_character = character_name
|
||||
ui.notify(f'Selected: {character_name}', type='info')
|
||||
self.selected_character = character
|
||||
self._character_view.refresh() # type: ignore
|
||||
|
||||
# Update character summary
|
||||
self.character_summary.clear()
|
||||
with self.character_summary:
|
||||
ui.label(f'{character_name}').classes('font-bold text-lg')
|
||||
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)
|
||||
|
||||
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):
|
||||
@ui.refreshable
|
||||
async def _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')
|
||||
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"""
|
||||
@@ -298,7 +228,7 @@ class MainPage(AsyncElement):
|
||||
|
||||
# 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'):
|
||||
with ui.chat_message(name=self.selected_character.name, sent=False).classes('w-full'):
|
||||
spinner = ui.spinner('dots')
|
||||
|
||||
# Simulate thinking
|
||||
@@ -314,11 +244,17 @@ class MainPage(AsyncElement):
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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"""
|
||||
@@ -356,7 +292,8 @@ class MainPage(AsyncElement):
|
||||
# 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')
|
||||
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"""
|
||||
|
||||
12
pages/scene_control.py
Normal file
12
pages/scene_control.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from nicegui import ui
|
||||
from components import AsyncElement
|
||||
from living_agents import Character
|
||||
|
||||
|
||||
class SceneControl(AsyncElement):
|
||||
|
||||
async def build(self) -> None:
|
||||
|
||||
self.classes('w-full')
|
||||
with self:
|
||||
pass
|
||||
Reference in New Issue
Block a user