diff --git a/src/backend/clara/agents/simulation_agent.py b/src/backend/clara/agents/simulation_agent.py index 4a2ba12..9fcf844 100644 --- a/src/backend/clara/agents/simulation_agent.py +++ b/src/backend/clara/agents/simulation_agent.py @@ -12,7 +12,9 @@ from collections.abc import AsyncGenerator from dataclasses import dataclass, field from datetime import datetime, timedelta +from urllib.parse import urlparse +import anthropic import httpx from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient @@ -483,6 +485,148 @@ async def fetch_website_context(url: str) -> str: return f"(Could not fetch website content: {e})" +def extract_company_name_from_url(url: str) -> str: + """Extract a clean company name from a URL.""" + try: + parsed = urlparse(url) + hostname = parsed.hostname or "" + + # Remove common prefixes + hostname = hostname.lower() + if hostname.startswith("www."): + hostname = hostname[4:] + + # Get the main domain part (before TLD) + parts = hostname.split(".") + if len(parts) >= 2: + # Return the main domain name (e.g., "facebook" from "facebook.com") + return parts[0].capitalize() + return hostname.capitalize() + except Exception: + return "the company" + + +async def search_company_products(url: str, role: str | None = None) -> str: + """Use web search to gather information about a company's products. + + Uses Claude with web search to understand what products/services + the company offers, which provides richer context for the persona. + + Args: + url: The company's website URL + role: Optional role to focus the search (e.g., "Product Manager for Instagram") + + Returns: + A summary of the company's products and services + """ + company_name = extract_company_name_from_url(url) + + # Build a focused search query + if role: + # Extract product name if role mentions it (e.g., "Product Manager for Instagram") + role_lower = role.lower() + search_query = f"{company_name} products services overview" + if "for " in role_lower: + product_focus = role.split("for ")[-1].strip() + search_query = f"{company_name} {product_focus} product features" + else: + search_query = f"{company_name} products services overview" + + logger.info(f"Searching for company products: {search_query}") + + try: + client = anthropic.Anthropic() + + # Build the research prompt + focus_item = "" + if role and "for " in role.lower(): + focus_item = f"\n5. Specifically focus on: {role.split('for ')[-1]}" + + research_prompt = f"""Research {company_name} (website: {url}) and provide a \ +comprehensive summary of their products and services. + +Focus on: +1. Main products and services offered +2. Key features and capabilities +3. Target customers/market +4. Recent developments or notable features{focus_item} + +Provide a detailed summary that would help someone understand what it's like \ +to work at this company in a product/technical role.""" + + # Use Claude with web search tool + response = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=2048, + tools=[{"type": "web_search_20250305", "name": "web_search"}], + messages=[{"role": "user", "content": research_prompt}], + ) + + # Extract the text response + result_text = "" + for block in response.content: + if hasattr(block, "text"): + result_text += block.text + + if result_text: + logger.info(f"Successfully gathered company context via web search for {company_name}") + return result_text + + return f"(Could not find detailed product information for {company_name})" + + except anthropic.APIError as e: + logger.warning(f"Anthropic API error during web search: {e}") + return f"(Web search unavailable: {e})" + except Exception as e: + logger.warning(f"Error during company product search: {e}") + return f"(Could not search for company products: {e})" + + +async def gather_company_context(url: str, role: str | None = None) -> str: + """Gather comprehensive context about a company using web search and website scraping. + + This combines web search results with direct website content for richer context. + + Args: + url: The company's website URL + role: Optional role to focus the research + + Returns: + Combined context about the company + """ + # Run web search and website fetch in parallel + search_task = asyncio.create_task(search_company_products(url, role)) + website_task = asyncio.create_task(fetch_website_context(url)) + + search_result, website_result = await asyncio.gather( + search_task, website_task, return_exceptions=True + ) + + # Handle any exceptions + if isinstance(search_result, Exception): + search_result = f"(Web search failed: {search_result})" + if isinstance(website_result, Exception): + website_result = f"(Website fetch failed: {website_result})" + + # Combine results + company_name = extract_company_name_from_url(url) + context_parts = [f"## Company: {company_name}"] + + if search_result and not search_result.startswith("("): + context_parts.append("\n### Product & Service Information (from web search)") + context_parts.append(search_result) + + if website_result and not website_result.startswith("("): + context_parts.append("\n### Website Content") + context_parts.append(website_result[:2000]) # Limit website content + + if len(context_parts) == 1: + # Neither source worked + return f"(Could not gather information about {company_name})" + + return "\n".join(context_parts) + + class SimulationSessionManager: """Manages simulation sessions with thread-safe operations.""" @@ -532,9 +676,12 @@ async def create_session( if model and model not in VALID_MODELS: raise ValueError(f"Invalid model '{model}'. Must be one of: {', '.join(VALID_MODELS)}") - # Fetch website context if persona has a URL (outside lock for performance) + # Gather company context using web search if persona has a URL + # (outside lock for performance) if persona and persona.company_url and not persona.company_context: - persona.company_context = await fetch_website_context(persona.company_url) + persona.company_context = await gather_company_context( + persona.company_url, persona.role + ) session = SimulationSession( session_id=session_id, @@ -562,9 +709,11 @@ async def update_persona( """Update the persona for a session.""" session = self._sessions.get(session_id) if session: - # Fetch website context if needed + # Gather company context using web search if needed if persona.company_url and not persona.company_context: - persona.company_context = await fetch_website_context(persona.company_url) + persona.company_context = await gather_company_context( + persona.company_url, persona.role + ) # Need to restart session with new persona await session.stop() diff --git a/src/backend/clara/api/simulation_sessions.py b/src/backend/clara/api/simulation_sessions.py index d18849a..60b807a 100644 --- a/src/backend/clara/api/simulation_sessions.py +++ b/src/backend/clara/api/simulation_sessions.py @@ -17,7 +17,12 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession -from clara.agents.simulation_agent import VALID_MODELS, AGUIEvent, simulation_manager +from clara.agents.simulation_agent import ( + VALID_MODELS, + AGUIEvent, + PersonaConfig, + simulation_manager, +) from clara.db.models import DesignSession from clara.db.session import get_db from clara.security import InputSanitizer @@ -85,6 +90,79 @@ class SimulationStateResponse(BaseModel): messages: list[dict] +class PersonaRequest(BaseModel): + """Request model for persona configuration.""" + role: str = Field( + ..., min_length=1, max_length=200, + description="Role like 'Product Manager', 'Senior Engineer'" + ) + company_url: str | None = Field( + None, description="Website URL to fetch company context from" + ) + name: str | None = Field( + None, max_length=100, description="Optional name for the persona" + ) + experience_years: int | None = Field( + None, ge=0, le=50, description="Years of experience" + ) + communication_style: str = Field( + "professional", description="Style: professional, casual, detailed, brief" + ) + + @field_validator('company_url') + @classmethod + def validate_url(cls, v: str | None) -> str | None: + if v is not None: + v = v.strip() + if not v.startswith(('http://', 'https://')): + raise ValueError("URL must start with http:// or https://") + return v + + @field_validator('communication_style') + @classmethod + def validate_style(cls, v: str) -> str: + valid_styles = {"professional", "casual", "detailed", "brief"} + if v not in valid_styles: + raise ValueError(f"Style must be one of: {', '.join(valid_styles)}") + return v + + +class CreateAutoSimulationRequest(BaseModel): + """Request to create an automated simulation session.""" + system_prompt: str = Field(..., min_length=1, max_length=50000) + persona: PersonaRequest + model: str | None = Field( + None, description="Model: sonnet (default), haiku (fast), opus (capable)" + ) + + @field_validator('system_prompt') + @classmethod + def sanitize_prompt(cls, v: str) -> str: + return InputSanitizer.sanitize_system_prompt(v) + + @field_validator('model') + @classmethod + def validate_model(cls, v: str | None) -> str | None: + if v is not None and v not in VALID_MODELS: + valid = ', '.join(sorted(VALID_MODELS)) + raise ValueError(f"Invalid model '{v}'. Must be one of: {valid}") + return v + + +class AutoSimulationResponse(BaseModel): + """Response after creating an automated simulation session.""" + session_id: str + system_prompt_preview: str + model: str + persona_role: str + persona_name: str | None + + +class RunAutoSimulationRequest(BaseModel): + """Request to run an automated simulation.""" + num_turns: int = Field(5, ge=1, le=20, description="Number of conversation turns") + + class BlueprintAgent(BaseModel): """Validated agent from blueprint state.""" name: str | None = None @@ -300,3 +378,187 @@ async def event_generator() -> AsyncGenerator[str, None]: "X-Accel-Buffering": "no", } ) + + +# ============================================================================ +# Automated Simulation Endpoints +# ============================================================================ + + +@router.post("/auto", response_model=AutoSimulationResponse) +async def create_auto_simulation( + request: CreateAutoSimulationRequest, +) -> AutoSimulationResponse: + """Create an automated simulation session with a persona. + + The persona will act as the interviewee, responding to the interview agent + automatically. You can optionally provide a company URL to fetch context. + """ + session_id = str(uuid.uuid4()) + + # Convert request persona to PersonaConfig + persona = PersonaConfig( + role=request.persona.role, + company_url=request.persona.company_url, + name=request.persona.name, + experience_years=request.persona.experience_years, + communication_style=request.persona.communication_style, + ) + + session = await simulation_manager.create_session( + session_id=session_id, + interviewer_prompt=request.system_prompt, + persona=persona, + model=request.model, + ) + + logger.info(f"Created auto-simulation session {session_id} with persona: {persona.role}") + + prompt = request.system_prompt + preview = prompt[:200] + "..." if len(prompt) > 200 else prompt + + return AutoSimulationResponse( + session_id=session_id, + system_prompt_preview=preview, + model=session.model, + persona_role=persona.role, + persona_name=persona.name, + ) + + +@router.post("/auto/from-design-session/{design_session_id}", response_model=AutoSimulationResponse) +async def create_auto_simulation_from_design_session( + design_session_id: str, + persona: PersonaRequest, + model: str | None = None, + db: AsyncSession = Depends(get_db), +) -> AutoSimulationResponse: + """Create an automated simulation from a design session's blueprint. + + Uses the system prompt from the blueprint and the provided persona. + """ + # Validate model if provided + if model is not None and model not in VALID_MODELS: + valid = ', '.join(sorted(VALID_MODELS)) + raise HTTPException( + status_code=400, + detail=f"Invalid model '{model}'. Must be one of: {valid}" + ) + + try: + result = await db.execute( + select(DesignSession).where(DesignSession.id == design_session_id) + ) + design_session = result.scalar_one_or_none() + except SQLAlchemyError: + logger.exception("Database error fetching design session") + raise HTTPException(status_code=500, detail="Database error") + + if not design_session: + raise HTTPException(status_code=404, detail="Design session not found") + + # Validate blueprint_state + try: + blueprint = BlueprintState.model_validate(design_session.blueprint_state or {}) + except Exception as e: + logger.warning(f"Invalid blueprint state: {e}") + raise HTTPException( + status_code=400, + detail="Invalid blueprint configuration. Please re-run the design process." + ) + + if not blueprint.agents: + raise HTTPException( + status_code=400, + detail="No agents configured in blueprint. Complete the design process first." + ) + + agent = blueprint.agents[0] + system_prompt = agent.system_prompt + + if not system_prompt: + raise HTTPException( + status_code=400, + detail="No system prompt found in agent configuration. Complete Phase 3 first." + ) + + system_prompt = InputSanitizer.sanitize_system_prompt(system_prompt) + + session_id = str(uuid.uuid4()) + + # Convert request persona to PersonaConfig + persona_config = PersonaConfig( + role=persona.role, + company_url=persona.company_url, + name=persona.name, + experience_years=persona.experience_years, + communication_style=persona.communication_style, + ) + + session = await simulation_manager.create_session( + session_id=session_id, + interviewer_prompt=system_prompt, + persona=persona_config, + model=model, + ) + + logger.info( + f"Created auto-simulation {session_id} from design {design_session_id} " + f"with persona: {persona_config.role}" + ) + + preview = system_prompt[:200] + "..." if len(system_prompt) > 200 else system_prompt + + return AutoSimulationResponse( + session_id=session_id, + system_prompt_preview=preview, + model=session.model, + persona_role=persona_config.role, + persona_name=persona_config.name, + ) + + +@router.post("/{session_id}/run-auto") +async def run_auto_simulation( + session_id: str, + request: RunAutoSimulationRequest = RunAutoSimulationRequest(), +): + """Run an automated simulation conversation. + + The interview agent and simulated user will have a back-and-forth + conversation for the specified number of turns. Streams SSE events + for both sides of the conversation. + """ + session = await simulation_manager.get_session(session_id) + + if not session: + raise HTTPException(status_code=404, detail="Simulation session not found") + + if not session.persona: + raise HTTPException( + status_code=400, + detail="Session has no persona. Use /auto endpoint for auto-simulation." + ) + + async def event_generator() -> AsyncGenerator[str, None]: + """Generate SSE events from the auto-simulation.""" + try: + async for event in session.run_auto_simulation(num_turns=request.num_turns): + yield format_sse_event(event) + except Exception as e: + logger.exception("Error running auto-simulation") + error_event = AGUIEvent( + type="ERROR", + data={"message": str(e)} + ) + yield format_sse_event(error_event) + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + } + ) diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 0c87362..9a9409c 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -3,6 +3,7 @@ import ProjectsPage from './pages/ProjectsPage' import ProjectDetailPage from './pages/ProjectDetailPage' import { DesignAssistantPage } from './pages/DesignAssistantPage' import { SimulationPage } from './pages/SimulationPage' +import { AutomatedSimulationPage } from './pages/AutomatedSimulationPage' function AppLayout({ children }: { children: React.ReactNode }) { return ( @@ -21,14 +22,15 @@ function AppLayout({ children }: { children: React.ReactNode }) { function App() { const location = useLocation() - const isFullScreenPage = location.pathname.includes('/design') || location.pathname.includes('/simulate') + const isFullScreenPage = location.pathname.includes('/design') || location.pathname.includes('/simulate') || location.pathname.includes('/auto-simulate') - // Design Assistant and Simulation use full-screen layout + // Design Assistant and Simulation pages use full-screen layout if (isFullScreenPage) { return ( } /> } /> + } /> ) } diff --git a/src/frontend/src/api/simulation-sessions.ts b/src/frontend/src/api/simulation-sessions.ts index 509a72f..89d5a71 100644 --- a/src/frontend/src/api/simulation-sessions.ts +++ b/src/frontend/src/api/simulation-sessions.ts @@ -25,6 +25,30 @@ export interface SimulationState { messages: Array<{ role: 'user' | 'assistant'; content: string }>; } +export type CommunicationStyle = 'professional' | 'casual' | 'detailed' | 'brief'; + +export interface PersonaConfig { + role: string; + company_url?: string; + name?: string; + experience_years?: number; + communication_style?: CommunicationStyle; +} + +export interface CreateAutoSimulationRequest { + system_prompt: string; + persona: PersonaConfig; + model?: SimulationModel; +} + +export interface AutoSimulationResponse { + session_id: string; + system_prompt_preview: string; + model: SimulationModel; + persona_role: string; + persona_name: string | null; +} + /** * Create a new simulation session with a custom prompt. */ @@ -171,3 +195,110 @@ export async function* sendSimulationMessage( } } } + +// ============================================================================ +// Automated Simulation API Functions +// ============================================================================ + +export interface AutoSimulationEvent { + type: string; + delta?: string; + content?: string; + message?: string; + role?: string; + turns?: number; +} + +/** + * Create an automated simulation session with a persona. + */ +export async function createAutoSimulation( + request: CreateAutoSimulationRequest +): Promise { + const response = await fetch(`${API_BASE}/auto`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.detail || `Failed to create auto-simulation: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Create an automated simulation from a design session's blueprint. + */ +export async function createAutoSimulationFromDesignSession( + designSessionId: string, + persona: PersonaConfig, + model?: SimulationModel +): Promise { + const params = new URLSearchParams(); + if (model) params.set('model', model); + + const url = `${API_BASE}/auto/from-design-session/${designSessionId}${params.toString() ? `?${params}` : ''}`; + + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(persona), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.detail || `Failed to create auto-simulation: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Run an automated simulation and stream the conversation. + */ +export async function* runAutoSimulation( + sessionId: string, + numTurns: number = 5 +): AsyncGenerator { + const response = await fetch(`${API_BASE}/${sessionId}/run-auto`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ num_turns: numTurns }), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.detail || `Failed to run auto-simulation: ${response.statusText}`); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('No response body'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + yield data; + } catch { + // Skip invalid JSON + } + } + } + } +} diff --git a/src/frontend/src/pages/AutomatedSimulationPage.tsx b/src/frontend/src/pages/AutomatedSimulationPage.tsx new file mode 100644 index 0000000..90dc951 --- /dev/null +++ b/src/frontend/src/pages/AutomatedSimulationPage.tsx @@ -0,0 +1,595 @@ +/** + * Automated Simulation Page - Run simulated interviews with AI personas. + * + * Users configure a persona (role, company, experience) and watch the + * interview agent have a conversation with the simulated interviewee. + */ + +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useParams, Link, useSearchParams } from 'react-router-dom'; +import clsx from 'clsx'; +import { + createAutoSimulation, + createAutoSimulationFromDesignSession, + runAutoSimulation, + deleteSimulation, + SimulationModel, + CommunicationStyle, + PersonaConfig, +} from '../api/simulation-sessions'; + +interface Message { + id: string; + role: 'interviewer' | 'interviewee'; + content: string; + isStreaming?: boolean; +} + +type SimulationStatus = 'configuring' | 'ready' | 'running' | 'completed' | 'error'; + +const DEFAULT_SYSTEM_PROMPT = `You are an expert discovery interviewer. Your goal is to understand the interviewee's role, responsibilities, challenges, and workflows. + +Guidelines: +- Start by introducing yourself and explaining the purpose of the interview +- Ask open-ended questions to encourage detailed responses +- Follow up on interesting points to dig deeper +- Be empathetic and create a comfortable environment +- Cover key topics: current processes, pain points, tools used, and desired improvements +- Summarize key findings periodically`; + +export function AutomatedSimulationPage() { + const { projectId } = useParams<{ projectId: string }>(); + const [searchParams] = useSearchParams(); + const designSessionId = searchParams.get('designSessionId'); + const messagesEndRef = useRef(null); + + // Session state + const [sessionId, setSessionId] = useState(null); + const [status, setStatus] = useState('configuring'); + const [error, setError] = useState(null); + + // Persona configuration + const [persona, setPersona] = useState({ + role: 'Product Manager', + company_url: '', + name: '', + experience_years: 5, + communication_style: 'professional', + }); + + // Simulation settings + const [systemPrompt, setSystemPrompt] = useState(DEFAULT_SYSTEM_PROMPT); + const [selectedModel, setSelectedModel] = useState('sonnet'); + const [numTurns, setNumTurns] = useState(5); + + // Messages + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + // Auto-scroll + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + // Cleanup session on unmount + useEffect(() => { + return () => { + if (sessionId) { + deleteSimulation(sessionId).catch(() => {}); + } + }; + }, [sessionId]); + + // Load system prompt from design session if provided + useEffect(() => { + async function loadDesignSession() { + if (!designSessionId) return; + + try { + setIsLoading(true); + // We'll create the session when starting, but could pre-fetch the prompt here + } catch (err) { + console.error('Failed to load design session:', err); + } finally { + setIsLoading(false); + } + } + + loadDesignSession(); + }, [designSessionId]); + + const handleStartSimulation = useCallback(async () => { + try { + setIsLoading(true); + setError(null); + setMessages([]); + + // Create the auto-simulation session + let result; + if (designSessionId) { + result = await createAutoSimulationFromDesignSession( + designSessionId, + persona, + selectedModel + ); + } else { + result = await createAutoSimulation({ + system_prompt: systemPrompt, + persona, + model: selectedModel, + }); + } + + setSessionId(result.session_id); + setStatus('ready'); + + // Now run the simulation + setStatus('running'); + + for await (const event of runAutoSimulation(result.session_id, numTurns)) { + if (event.type === 'TEXT_MESSAGE_CONTENT') { + // Interviewer (assistant) message content + setMessages((prev) => { + const lastMsg = prev[prev.length - 1]; + if (lastMsg?.role === 'interviewer' && lastMsg?.isStreaming) { + return prev.map((msg, i) => + i === prev.length - 1 + ? { ...msg, content: msg.content + (event.delta || '') } + : msg + ); + } + // New interviewer message + return [ + ...prev, + { + id: `interviewer-${Date.now()}`, + role: 'interviewer', + content: event.delta || '', + isStreaming: true, + }, + ]; + }); + } else if (event.type === 'TEXT_MESSAGE_END') { + // Interviewer message complete + setMessages((prev) => + prev.map((msg) => + msg.role === 'interviewer' && msg.isStreaming + ? { ...msg, isStreaming: false } + : msg + ) + ); + } else if (event.type === 'SIMULATED_USER_CONTENT') { + // Interviewee (simulated user) message content + setMessages((prev) => { + const lastMsg = prev[prev.length - 1]; + if (lastMsg?.role === 'interviewee' && lastMsg?.isStreaming) { + return prev.map((msg, i) => + i === prev.length - 1 + ? { ...msg, content: msg.content + (event.delta || '') } + : msg + ); + } + // New interviewee message + return [ + ...prev, + { + id: `interviewee-${Date.now()}`, + role: 'interviewee', + content: event.delta || '', + isStreaming: true, + }, + ]; + }); + } else if (event.type === 'SIMULATED_USER_END') { + // Interviewee message complete + setMessages((prev) => + prev.map((msg) => + msg.role === 'interviewee' && msg.isStreaming + ? { ...msg, isStreaming: false } + : msg + ) + ); + } else if (event.type === 'SIMULATION_COMPLETE') { + setStatus('completed'); + } else if (event.type === 'ERROR') { + setError(event.message || 'An error occurred'); + setStatus('error'); + } + } + + setStatus('completed'); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to start simulation'; + setError(errorMessage); + setStatus('error'); + } finally { + setIsLoading(false); + } + }, [designSessionId, persona, systemPrompt, selectedModel, numTurns]); + + const handleReset = useCallback(() => { + if (sessionId) { + deleteSimulation(sessionId).catch(() => {}); + } + setSessionId(null); + setStatus('configuring'); + setMessages([]); + setError(null); + }, [sessionId]); + + if (!projectId) { + return ( +
+

No project selected

+
+ ); + } + + return ( +
+ {/* Configuration Panel */} +
+
+
+ + + + + +

Automated Simulation

+
+

+ Configure a persona to simulate an interview conversation +

+
+ +
+ {/* Persona Configuration */} +
+

Persona

+
+
+ + setPersona((p) => ({ ...p, role: e.target.value }))} + placeholder="e.g., Product Manager, Senior Engineer" + disabled={status !== 'configuring'} + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-50" + /> +
+ +
+ + setPersona((p) => ({ ...p, name: e.target.value || undefined }))} + placeholder="e.g., Sarah Chen" + disabled={status !== 'configuring'} + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-50" + /> +
+ +
+ + setPersona((p) => ({ ...p, company_url: e.target.value || undefined }))} + placeholder="https://company.com" + disabled={status !== 'configuring'} + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-50" + /> +

+ We'll fetch context about the company to inform responses +

+
+ +
+ + setPersona((p) => ({ ...p, experience_years: parseInt(e.target.value) || undefined }))} + min={0} + max={50} + disabled={status !== 'configuring'} + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-50" + /> +
+ +
+ + +
+
+
+ + {/* Simulation Settings */} +
+

Settings

+
+
+ + +
+ +
+ + setNumTurns(parseInt(e.target.value))} + min={1} + max={20} + disabled={status !== 'configuring'} + className="w-full" + /> +

+ Number of back-and-forth exchanges +

+
+
+
+ + {/* System Prompt (only if no design session) */} + {!designSessionId && ( +
+

Interview Prompt

+