diff --git a/CLAUDE.md b/CLAUDE.md index 6d747e8e..353fc0a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,7 +85,6 @@ python scripts/quality-ratchet.py show * **Before**: One project per agent (broken architecture) * **After**: Multiple agents (orchestrator, backend, frontend, test, review) collaborate on same project * **Tests**: 59/59 passing (100%) - Full multi-agent test coverage -- 2025-11-14: 007-context-management Phase 2-5 complete - Context storage, scoring, and tier assignment ✅ * Phase 2: Foundational layer (Pydantic models, migrations, database methods, TokenCounter) * Phase 3: Context item storage (save/load/get context with persistence) * Phase 4: Importance scoring with hybrid exponential decay algorithm (T027-T036) diff --git a/codeframe/core/models.py b/codeframe/core/models.py index 1886e5d3..5e01307b 100644 --- a/codeframe/core/models.py +++ b/codeframe/core/models.py @@ -562,3 +562,83 @@ def to_blocker_message(self) -> str: msg += f"\n---\n\n{self.summary}" return msg + + +# ============================================================================ +# Discovery Answer UI Models (Feature: 012-discovery-answer-ui) +# ============================================================================ + + +class DiscoveryAnswer(BaseModel): + """Request model for discovery answer submission.""" + + model_config = ConfigDict(str_strip_whitespace=True) + + answer: str = Field( + ..., + min_length=1, + max_length=5000, + description="User's answer to the current discovery question" + ) + + @field_validator('answer') + @classmethod + def validate_answer(cls, v: str) -> str: + """Ensure answer is not empty after trimming.""" + trimmed = v.strip() + if not trimmed: + raise ValueError("Answer cannot be empty or whitespace only") + if len(trimmed) > 5000: + raise ValueError("Answer cannot exceed 5000 characters") + return trimmed + + +class DiscoveryAnswerResponse(BaseModel): + """Response model for discovery answer submission.""" + + success: bool = Field( + ..., + description="Whether the answer was successfully processed" + ) + + next_question: Optional[str] = Field( + None, + description="Next discovery question text (null if discovery complete)" + ) + + is_complete: bool = Field( + ..., + description="Whether the discovery phase is complete" + ) + + current_index: int = Field( + ..., + ge=0, + description="Current question index (0-based)" + ) + + total_questions: int = Field( + ..., + gt=0, + description="Total number of discovery questions" + ) + + progress_percentage: float = Field( + ..., + ge=0.0, + le=100.0, + description="Discovery completion percentage (0.0 - 100.0)" + ) + + model_config = ConfigDict( + json_schema_extra={ + "example": { + "success": True, + "next_question": "What tech stack are you planning to use?", + "is_complete": False, + "current_index": 3, + "total_questions": 20, + "progress_percentage": 15.0 + } + } + ) diff --git a/codeframe/ui/server.py b/codeframe/ui/server.py index 8e10e5cc..2fed67b2 100644 --- a/codeframe/ui/server.py +++ b/codeframe/ui/server.py @@ -18,6 +18,8 @@ BlockerResolve, ContextItemCreateModel, ContextItemResponse, + DiscoveryAnswer, + DiscoveryAnswerResponse, ) from codeframe.persistence.database import Database from codeframe.ui.models import ( @@ -583,6 +585,139 @@ async def get_chat_history(project_id: int, limit: int = 100, offset: int = 0): return {"messages": messages} +@app.post("/api/projects/{project_id}/discovery/answer") +async def submit_discovery_answer(project_id: int, answer_data: DiscoveryAnswer): + """Submit answer to current discovery question (Feature: 012-discovery-answer-ui, US5). + + Implementation following TDD approach (T041-T044): + - Validates project exists and is in discovery phase + - Processes answer through Lead Agent + - Broadcasts WebSocket events for real-time UI updates + - Returns updated discovery status + + Args: + project_id: Project ID + answer_data: Answer submission data (Pydantic model with validation) + + Returns: + DiscoveryAnswerResponse with next question and progress + + Raises: + HTTPException: + - 400: Validation error or wrong phase + - 404: Project not found + - 500: Missing API key or processing error + """ + from codeframe.ui.websocket_broadcasts import ( + broadcast_discovery_answer_submitted, + broadcast_discovery_question_presented, + broadcast_discovery_completed, + ) + + # T041: Validate project exists + project = app.state.db.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail=f"Project {project_id} not found") + + # T041: Validate project is in discovery phase + if project.get("phase") != "discovery": + raise HTTPException( + status_code=400, + detail=f"Project is not in discovery phase. Current phase: {project.get('phase')}" + ) + + # T042: Validate ANTHROPIC_API_KEY is available + api_key = os.environ.get("ANTHROPIC_API_KEY") + if not api_key: + raise HTTPException( + status_code=500, + detail="ANTHROPIC_API_KEY environment variable is not set. Cannot process discovery answers." + ) + + # T042: Get Lead Agent and process answer + try: + agent = LeadAgent( + project_id=project_id, + db=app.state.db, + api_key=api_key + ) + + # CRITICAL: Validate discovery is active before processing answer + status = agent.get_discovery_status() + if status.get('state') != 'discovering': + raise HTTPException( + status_code=400, + detail=f"Discovery is not active. Current state: {status.get('state')}. " + f"Please start discovery first by calling POST /api/projects/{project_id}/discovery/start" + ) + + # Process the answer (trimmed by Pydantic validator) + agent.process_discovery_answer(answer_data.answer) + + # Get updated discovery status after processing + status = agent.get_discovery_status() + + except HTTPException: + # Re-raise HTTPExceptions as-is + raise + except Exception as e: + logger.error(f"Failed to process discovery answer for project {project_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to process answer: {str(e)}") + + # T043: Compute derived values from status to match LeadAgent.get_discovery_status() format + is_complete = status.get("state") == "completed" + total_questions = status.get("total_required", 0) + answered_count = status.get("answered_count", 0) + current_question_index = answered_count # Index is based on how many answered + current_question_id = status.get("current_question", {}).get("id", "") + current_question_text = status.get("current_question", {}).get("question", "") + progress_percentage = status.get("progress_percentage", 0.0) + + # T043: Broadcast WebSocket events + try: + # Broadcast answer submitted event + await broadcast_discovery_answer_submitted( + manager=manager, + project_id=project_id, + question_id=current_question_id, + answer_preview=answer_data.answer[:100], # First 100 chars + current_index=current_question_index, + total_questions=total_questions, + ) + + # Broadcast appropriate follow-up event + if is_complete: + await broadcast_discovery_completed( + manager=manager, + project_id=project_id, + total_answers=answered_count, + next_phase="prd_generation", + ) + else: + await broadcast_discovery_question_presented( + manager=manager, + project_id=project_id, + question_id=current_question_id, + question_text=current_question_text, + current_index=current_question_index, + total_questions=total_questions, + ) + + except Exception as e: + logger.warning(f"Failed to broadcast WebSocket events for project {project_id}: {e}") + # Non-fatal - continue with response + + # T044: Generate and return response + return DiscoveryAnswerResponse( + success=True, + next_question=current_question_text if not is_complete else None, + is_complete=is_complete, + current_index=current_question_index, + total_questions=total_questions, + progress_percentage=progress_percentage, + ) + + @app.get("/api/projects/{project_id}/prd") async def get_project_prd(project_id: int): """Get PRD for a project (cf-26). diff --git a/codeframe/ui/websocket_broadcasts.py b/codeframe/ui/websocket_broadcasts.py index ea65c391..0fddc8f0 100644 --- a/codeframe/ui/websocket_broadcasts.py +++ b/codeframe/ui/websocket_broadcasts.py @@ -257,15 +257,21 @@ async def broadcast_progress_update( total: Total number of tasks percentage: Optional progress percentage (auto-calculated if not provided) """ - if percentage is None and total > 0: - percentage = int((completed / total) * 100) + # Calculate progress percentage with safety checks + if percentage is None: + if total <= 0: + percentage = 0.0 + else: + percentage = round((float(completed) / float(total)) * 100, 1) + # Clamp to [0.0, 100.0] range + percentage = max(0.0, min(100.0, percentage)) message = { "type": "progress_update", "project_id": project_id, "completed": completed, "total": total, - "percentage": percentage if percentage is not None else 0, + "percentage": percentage, "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), } @@ -624,3 +630,157 @@ async def broadcast_blocker_expired( logger.debug(f"Broadcast blocker_expired: blocker {blocker_id} by {agent_id}") except Exception as e: logger.error(f"Failed to broadcast blocker expired: {e}") + + +# ============================================================================ +# Discovery Answer UI Broadcasts (Feature: 012-discovery-answer-ui) +# ============================================================================ + + +async def broadcast_discovery_answer_submitted( + manager, + project_id: int, + question_id: str, + answer_preview: str, + current_index: int, + total_questions: int, +) -> None: + """ + Broadcast when user submits discovery answer. + + Args: + manager: ConnectionManager instance + project_id: Project ID + question_id: Unique identifier of the answered question + answer_preview: First 100 chars of the answer + current_index: Current question index (0-based) + total_questions: Total number of questions + """ + # Calculate progress percentage with safety checks + if total_questions <= 0: + percentage = 0.0 + else: + # Ensure values are treated as numbers and compute percentage + percentage = round((float(current_index) / float(total_questions)) * 100, 1) + # Clamp to [0.0, 100.0] range + percentage = max(0.0, min(100.0, percentage)) + + message = { + "type": "discovery_answer_submitted", + "project_id": project_id, + "question_id": question_id, + "answer_preview": answer_preview[:100], # Limit to 100 chars + "progress": { + "current": current_index, + "total": total_questions, + "percentage": percentage, + }, + "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), + } + + try: + await manager.broadcast(message) + logger.debug(f"Broadcast discovery_answer_submitted: question {question_id}") + except Exception as e: + logger.error(f"Failed to broadcast discovery answer submission: {e}") + + +async def broadcast_discovery_question_presented( + manager, + project_id: int, + question_id: str, + question_text: str, + current_index: int, + total_questions: int, +) -> None: + """ + Broadcast when next discovery question is presented. + + Args: + manager: ConnectionManager instance + project_id: Project ID + question_id: Unique identifier of the question + question_text: Full text of the question + current_index: Current question number (1-based for display) + total_questions: Total number of questions + """ + message = { + "type": "discovery_question_presented", + "project_id": project_id, + "question_id": question_id, + "question_text": question_text, + "current_index": current_index, + "total_questions": total_questions, + "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), + } + + try: + await manager.broadcast(message) + logger.debug(f"Broadcast discovery_question_presented: {question_id}") + except Exception as e: + logger.error(f"Failed to broadcast discovery question presented: {e}") + + +async def broadcast_discovery_progress_updated( + manager, + project_id: int, + current_index: int, + total_questions: int, + percentage: float, +) -> None: + """ + Broadcast discovery progress updates. + + Args: + manager: ConnectionManager instance + project_id: Project ID + current_index: Current question index (0-based) + total_questions: Total number of questions + percentage: Completion percentage (0.0 - 100.0) + """ + message = { + "type": "discovery_progress_updated", + "project_id": project_id, + "progress": { + "current": current_index, + "total": total_questions, + "percentage": percentage, + }, + "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), + } + + try: + await manager.broadcast(message) + logger.debug(f"Broadcast discovery_progress_updated: {percentage}%") + except Exception as e: + logger.error(f"Failed to broadcast discovery progress update: {e}") + + +async def broadcast_discovery_completed( + manager, + project_id: int, + total_answers: int, + next_phase: str = "prd_generation", +) -> None: + """ + Broadcast when discovery phase is completed. + + Args: + manager: ConnectionManager instance + project_id: Project ID + total_answers: Total number of answers collected + next_phase: Next project phase (default: prd_generation) + """ + message = { + "type": "discovery_completed", + "project_id": project_id, + "total_answers": total_answers, + "next_phase": next_phase, + "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), + } + + try: + await manager.broadcast(message) + logger.debug(f"Broadcast discovery_completed: {total_answers} answers") + except Exception as e: + logger.error(f"Failed to broadcast discovery completion: {e}") diff --git a/specs/012-discovery-answer-ui/contracts/README.md b/specs/012-discovery-answer-ui/contracts/README.md new file mode 100644 index 00000000..1c0e656f --- /dev/null +++ b/specs/012-discovery-answer-ui/contracts/README.md @@ -0,0 +1,272 @@ +# API Contracts: Discovery Answer UI Integration + +**Feature**: 012-discovery-answer-ui +**Date**: 2025-11-19 + +--- + +## Overview + +This directory contains formal API contract specifications for the Discovery Answer UI Integration feature. + +--- + +## Files + +### 1. `api.openapi.yaml` + +**Format**: OpenAPI 3.1.0 +**Purpose**: REST API specification for discovery answer submission endpoint + +**Contents**: +- POST `/api/projects/{project_id}/discovery/answer` + - Request schema: `DiscoveryAnswer` + - Response schema: `DiscoveryAnswerResponse` + - Error responses: 400, 404, 422, 500 +- Complete examples for all request/response scenarios +- Validation rules for answer length (1-5000 chars) + +**Tools**: +- Validator: [openapi-validator](https://github.com/IBM/openapi-validator-cli) +- Code Generation: [openapi-generator](https://openapi-generator.tech/) +- Documentation: [Redoc](https://redocly.com/) or [Swagger UI](https://swagger.io/tools/swagger-ui/) + +**Usage**: +```bash +# Validate OpenAPI spec +npx @openapitools/openapi-generator-cli validate -i api.openapi.yaml + +# Generate TypeScript client +npx @openapitools/openapi-generator-cli generate \ + -i api.openapi.yaml \ + -g typescript-fetch \ + -o ./generated/api-client + +# View documentation +npx redoc-cli serve api.openapi.yaml +``` + +--- + +### 2. `websocket.yaml` + +**Format**: AsyncAPI 2.6.0 +**Purpose**: WebSocket event specifications for real-time discovery updates + +**Contents**: +- `discovery_answer_submitted` - Answer submission confirmation +- `discovery_question_presented` - Next question presentation +- `discovery_progress_updated` - Progress percentage updates +- `discovery_completed` - Discovery phase completion + +**Tools**: +- Validator: [asyncapi-cli](https://github.com/asyncapi/cli) +- Code Generation: [asyncapi-codegen](https://github.com/asyncapi/generator) +- Documentation: [asyncapi/html-template](https://github.com/asyncapi/html-template) + +**Usage**: +```bash +# Validate AsyncAPI spec +npx asyncapi validate websocket.yaml + +# Generate HTML documentation +npx asyncapi generate fromTemplate websocket.yaml @asyncapi/html-template \ + -o ./generated/websocket-docs + +# Generate TypeScript types +npx asyncapi generate models typescript websocket.yaml \ + -o ./generated/websocket-types +``` + +--- + +## Contract Validation + +### Pre-Commit Checks + +Add to `.pre-commit-config.yaml`: +```yaml +- repo: local + hooks: + - id: validate-openapi + name: Validate OpenAPI specs + entry: npx @openapitools/openapi-generator-cli validate + language: system + files: \.openapi\.yaml$ + + - id: validate-asyncapi + name: Validate AsyncAPI specs + entry: npx asyncapi validate + language: system + files: websocket\.yaml$ +``` + +### Manual Validation + +```bash +# Validate all contracts +cd specs/012-discovery-answer-ui/contracts + +# OpenAPI validation +npx @openapitools/openapi-generator-cli validate -i api.openapi.yaml + +# AsyncAPI validation +npx asyncapi validate websocket.yaml +``` + +--- + +## Integration with Backend + +### FastAPI OpenAPI Integration + +FastAPI automatically generates OpenAPI specs from route decorators. Our `api.openapi.yaml` serves as: + +1. **Design Document**: Specification-first approach +2. **Validation**: Ensure implementation matches spec +3. **Client Generation**: Generate TypeScript client for frontend +4. **Testing**: Generate test cases from examples + +**Comparison**: +```bash +# Generate OpenAPI from running FastAPI server +curl http://localhost:8080/openapi.json > generated-spec.json + +# Compare with our specification +npx openapi-diff api.openapi.yaml generated-spec.json +``` + +--- + +## Integration with Frontend + +### TypeScript Client Generation + +```bash +# Generate type-safe API client +npx @openapitools/openapi-generator-cli generate \ + -i contracts/api.openapi.yaml \ + -g typescript-fetch \ + -o web-ui/src/generated/api + +# Use in frontend +import { DiscoveryApi } from '@/generated/api'; + +const api = new DiscoveryApi(); +const response = await api.submitDiscoveryAnswer({ + projectId: 123, + discoveryAnswer: { answer: "My answer text" } +}); +``` + +### WebSocket Type Generation + +```bash +# Generate TypeScript types from AsyncAPI +npx asyncapi generate models typescript websocket.yaml \ + -o web-ui/src/generated/websocket-types + +# Use in frontend +import { DiscoveryAnswerSubmittedPayload } from '@/generated/websocket-types'; + +ws.onmessage = (event) => { + const message: DiscoveryAnswerSubmittedPayload = JSON.parse(event.data); + if (message.type === 'discovery_answer_submitted') { + console.log('Progress:', message.progress.percentage); + } +}; +``` + +--- + +## Contract-Driven Development Workflow + +1. **Design Phase**: + - Write OpenAPI/AsyncAPI specs first + - Review with team + - Validate specs + +2. **Implementation Phase**: + - Generate types and clients from specs + - Implement backend endpoints matching spec + - Implement frontend using generated client + - Run contract tests + +3. **Verification Phase**: + - Compare implementation against spec + - Ensure all examples work + - Validate error responses + +4. **Documentation Phase**: + - Generate API docs from specs + - Host docs with Redoc/AsyncAPI HTML + - Link from main README + +--- + +## Testing with Contracts + +### Contract Testing (Pact) + +```typescript +// Frontend contract test +import { pactWith } from 'jest-pact'; +import { DiscoveryApi } from '@/generated/api'; + +pactWith({ consumer: 'WebUI', provider: 'DiscoveryAPI' }, (interaction) => { + interaction('submit discovery answer', () => { + given('project 123 exists and is in discovery phase'); + uponReceiving('a valid answer submission'); + withRequest({ + method: 'POST', + path: '/api/projects/123/discovery/answer', + body: { answer: 'Valid answer text' } + }); + willRespondWith({ + status: 200, + body: { + success: true, + next_question: 'What tech stack?', + is_complete: false, + current_index: 3, + total_questions: 20, + progress_percentage: 15.0 + } + }); + }); + + it('submits answer successfully', async () => { + const api = new DiscoveryApi(); + const response = await api.submitDiscoveryAnswer({ ... }); + expect(response.success).toBe(true); + }); +}); +``` + +--- + +## Versioning + +**Current Version**: 1.0.0 + +**Semantic Versioning**: +- **MAJOR**: Breaking changes to request/response schemas +- **MINOR**: New optional fields or endpoints +- **PATCH**: Documentation updates, examples, clarifications + +**Changelog**: +- `1.0.0` (2025-11-19): Initial specification for discovery answer submission + +--- + +## References + +- [OpenAPI 3.1 Specification](https://spec.openapis.org/oas/v3.1.0) +- [AsyncAPI 2.6 Specification](https://www.asyncapi.com/docs/reference/specification/v2.6.0) +- [FastAPI OpenAPI Support](https://fastapi.tiangolo.com/advanced/extending-openapi/) +- [TypeScript Fetch Client Generator](https://openapi-generator.tech/docs/generators/typescript-fetch/) + +--- + +**Contracts Complete** ✅ +**Ready for Implementation**: Backend and frontend can proceed independently using these specifications diff --git a/specs/012-discovery-answer-ui/contracts/api.openapi.yaml b/specs/012-discovery-answer-ui/contracts/api.openapi.yaml new file mode 100644 index 00000000..2abc13c5 --- /dev/null +++ b/specs/012-discovery-answer-ui/contracts/api.openapi.yaml @@ -0,0 +1,270 @@ +openapi: 3.1.0 +info: + title: Discovery Answer API + description: | + API endpoint for submitting user answers during the discovery phase. + This feature enables users to answer Socratic discovery questions through an interactive UI. + version: 1.0.0 + contact: + name: CodeFRAME Team + +servers: + - url: http://localhost:8080 + description: Local development server + - url: http://localhost:3000 + description: Frontend development proxy + +tags: + - name: Discovery + description: Discovery phase operations + +paths: + /api/projects/{project_id}/discovery/answer: + post: + summary: Submit Discovery Answer + description: | + Submit user's answer to the current discovery question. + + **Workflow**: + 1. User types answer in textarea (1-5000 chars) + 2. User clicks Submit or presses Ctrl+Enter + 3. Frontend sends POST request + 4. Backend validates answer and project state + 5. Lead Agent processes answer and advances to next question + 6. WebSocket broadcasts notify connected clients + 7. Response includes next question or completion state + + **State Transitions**: + - Answer submitted → Next question presented (if questions remaining) + - Final answer submitted → Discovery complete → PRD generation begins + operationId: submitDiscoveryAnswer + tags: + - Discovery + parameters: + - name: project_id + in: path + required: true + description: Unique identifier of the project + schema: + type: integer + format: int64 + minimum: 1 + example: 123 + requestBody: + required: true + description: User's answer to the current discovery question + content: + application/json: + schema: + $ref: '#/components/schemas/DiscoveryAnswer' + examples: + valid_answer: + summary: Valid answer submission + value: + answer: "Build a task management system for remote teams with real-time collaboration features." + short_answer: + summary: Short valid answer + value: + answer: "Yes, we plan to use TypeScript and React." + long_answer: + summary: Long valid answer (max 5000 chars) + value: + answer: "Our target users are software development teams working remotely across different time zones. They need a tool that enables asynchronous collaboration, provides clear task visibility, and integrates with existing development workflows. The system should support multiple project types, allow flexible task organization, and provide real-time notifications without being intrusive. Key pain points include: scattered communication across tools, lack of context in task assignments, difficulty tracking progress across time zones, and integration overhead with existing tools like GitHub, Slack, and Jira..." + responses: + '200': + description: Answer successfully processed + content: + application/json: + schema: + $ref: '#/components/schemas/DiscoveryAnswerResponse' + examples: + next_question: + summary: More questions remaining + value: + success: true + next_question: "What tech stack are you planning to use?" + is_complete: false + current_index: 3 + total_questions: 20 + progress_percentage: 15.0 + discovery_complete: + summary: All questions answered + value: + success: true + next_question: null + is_complete: true + current_index: 20 + total_questions: 20 + progress_percentage: 100.0 + '400': + description: Validation error or invalid project state + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + empty_answer: + summary: Empty or whitespace-only answer + value: + detail: "Answer must be between 1 and 5000 characters" + too_long: + summary: Answer exceeds maximum length + value: + detail: "Answer must be between 1 and 5000 characters" + wrong_phase: + summary: Project not in discovery phase + value: + detail: "Project is not in discovery phase" + '404': + description: Project not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + not_found: + summary: Project does not exist + value: + detail: "Project not found" + '422': + description: Request validation failed (Pydantic) + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + examples: + missing_field: + summary: Required field missing + value: + detail: + - loc: ["body", "answer"] + msg: "Field required" + type: "missing" + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + server_error: + summary: Unexpected server error + value: + detail: "Internal server error" + +components: + schemas: + DiscoveryAnswer: + type: object + required: + - answer + properties: + answer: + type: string + minLength: 1 + maxLength: 5000 + description: | + User's answer to the current discovery question. + Whitespace is automatically trimmed before validation. + example: "Build a task management system for remote teams with real-time collaboration features." + additionalProperties: false + + DiscoveryAnswerResponse: + type: object + required: + - success + - is_complete + - current_index + - total_questions + - progress_percentage + properties: + success: + type: boolean + description: Whether the answer was successfully processed + example: true + next_question: + type: string + nullable: true + description: | + Next discovery question text. + Null when discovery is complete. + example: "What tech stack are you planning to use?" + is_complete: + type: boolean + description: Whether the discovery phase is complete + example: false + current_index: + type: integer + format: int32 + minimum: 0 + description: Current question index (0-based) + example: 3 + total_questions: + type: integer + format: int32 + minimum: 1 + description: Total number of discovery questions + example: 20 + progress_percentage: + type: number + format: float + minimum: 0.0 + maximum: 100.0 + description: Discovery completion percentage (0.0 - 100.0) + example: 15.0 + additionalProperties: false + + ErrorResponse: + type: object + required: + - detail + properties: + detail: + type: string + description: Human-readable error message + example: "Project is not in discovery phase" + additionalProperties: false + + ValidationError: + type: object + required: + - detail + properties: + detail: + type: array + description: List of validation errors + items: + type: object + required: + - loc + - msg + - type + properties: + loc: + type: array + description: Location of the error (path to field) + items: + type: string + example: ["body", "answer"] + msg: + type: string + description: Error message + example: "Field required" + type: + type: string + description: Error type identifier + example: "missing" + additionalProperties: false + + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + description: | + API key authentication (if enabled in production). + Local development typically does not require authentication. + +security: + - {} # No authentication required for local development + - ApiKeyAuth: [] # Optional API key for production diff --git a/specs/012-discovery-answer-ui/contracts/websocket.yaml b/specs/012-discovery-answer-ui/contracts/websocket.yaml new file mode 100644 index 00000000..c760e71e --- /dev/null +++ b/specs/012-discovery-answer-ui/contracts/websocket.yaml @@ -0,0 +1,241 @@ +asyncapi: 2.6.0 +info: + title: Discovery WebSocket Events + version: 1.0.0 + description: | + WebSocket event specifications for discovery answer real-time updates. + The frontend subscribes to these events to update the UI in real-time as users submit answers. + +servers: + development: + url: ws://localhost:8080/ws + protocol: ws + description: Local development WebSocket server + +channels: + discovery/events: + description: Discovery-related real-time events + subscribe: + summary: Subscribe to discovery events + message: + oneOf: + - $ref: '#/components/messages/DiscoveryAnswerSubmitted' + - $ref: '#/components/messages/DiscoveryQuestionPresented' + - $ref: '#/components/messages/DiscoveryProgressUpdated' + - $ref: '#/components/messages/DiscoveryCompleted' + +components: + messages: + DiscoveryAnswerSubmitted: + name: discovery_answer_submitted + title: Discovery Answer Submitted + summary: Broadcasted when a user submits a discovery answer + contentType: application/json + payload: + $ref: '#/components/schemas/DiscoveryAnswerSubmittedPayload' + examples: + - name: answer_submitted + summary: User submitted answer to question 3 + payload: + type: discovery_answer_submitted + project_id: 123 + question_id: q_target_users + answer_preview: "Developers building web applications..." + progress: + current: 3 + total: 20 + percentage: 15.0 + timestamp: "2025-11-19T14:30:00Z" + + DiscoveryQuestionPresented: + name: discovery_question_presented + title: Discovery Question Presented + summary: Broadcasted when next discovery question is presented to user + contentType: application/json + payload: + $ref: '#/components/schemas/DiscoveryQuestionPresentedPayload' + examples: + - name: next_question + summary: Next question presented after answer submission + payload: + type: discovery_question_presented + project_id: 123 + question_id: q_tech_stack + question_text: "What tech stack are you planning to use?" + current_index: 4 + total_questions: 20 + timestamp: "2025-11-19T14:30:01Z" + + DiscoveryProgressUpdated: + name: discovery_progress_updated + title: Discovery Progress Updated + summary: Broadcasted when discovery progress percentage changes + contentType: application/json + payload: + $ref: '#/components/schemas/DiscoveryProgressUpdatedPayload' + examples: + - name: progress_update + summary: Progress updated after answer submission + payload: + type: discovery_progress_updated + project_id: 123 + progress: + current: 5 + total: 20 + percentage: 25.0 + timestamp: "2025-11-19T14:35:00Z" + + DiscoveryCompleted: + name: discovery_completed + title: Discovery Completed + summary: Broadcasted when all discovery questions have been answered + contentType: application/json + payload: + $ref: '#/components/schemas/DiscoveryCompletedPayload' + examples: + - name: discovery_complete + summary: All 20 questions answered, moving to PRD generation + payload: + type: discovery_completed + project_id: 123 + total_answers: 20 + next_phase: prd_generation + timestamp: "2025-11-19T15:00:00Z" + + schemas: + DiscoveryAnswerSubmittedPayload: + type: object + required: + - type + - project_id + - question_id + - answer_preview + - progress + - timestamp + properties: + type: + type: string + const: discovery_answer_submitted + project_id: + type: integer + description: ID of the project + question_id: + type: string + description: Unique identifier of the answered question + answer_preview: + type: string + maxLength: 100 + description: First 100 characters of the answer + progress: + $ref: '#/components/schemas/ProgressInfo' + timestamp: + type: string + format: date-time + description: ISO 8601 timestamp (UTC with Z suffix) + + DiscoveryQuestionPresentedPayload: + type: object + required: + - type + - project_id + - question_id + - question_text + - current_index + - total_questions + - timestamp + properties: + type: + type: string + const: discovery_question_presented + project_id: + type: integer + description: ID of the project + question_id: + type: string + description: Unique identifier of the question + question_text: + type: string + description: Full text of the question + current_index: + type: integer + minimum: 1 + description: Current question number (1-based for display) + total_questions: + type: integer + minimum: 1 + description: Total number of questions + timestamp: + type: string + format: date-time + description: ISO 8601 timestamp (UTC with Z suffix) + + DiscoveryProgressUpdatedPayload: + type: object + required: + - type + - project_id + - progress + - timestamp + properties: + type: + type: string + const: discovery_progress_updated + project_id: + type: integer + description: ID of the project + progress: + $ref: '#/components/schemas/ProgressInfo' + timestamp: + type: string + format: date-time + description: ISO 8601 timestamp (UTC with Z suffix) + + DiscoveryCompletedPayload: + type: object + required: + - type + - project_id + - total_answers + - next_phase + - timestamp + properties: + type: + type: string + const: discovery_completed + project_id: + type: integer + description: ID of the project + total_answers: + type: integer + minimum: 1 + description: Total number of answers collected + next_phase: + type: string + enum: [prd_generation, planning] + description: Next project phase after discovery + timestamp: + type: string + format: date-time + description: ISO 8601 timestamp (UTC with Z suffix) + + ProgressInfo: + type: object + required: + - current + - total + - percentage + properties: + current: + type: integer + minimum: 0 + description: Current question index (0-based internally) + total: + type: integer + minimum: 1 + description: Total number of questions + percentage: + type: number + format: float + minimum: 0.0 + maximum: 100.0 + description: Completion percentage (0.0 - 100.0) diff --git a/specs/012-discovery-answer-ui/data-model.md b/specs/012-discovery-answer-ui/data-model.md new file mode 100644 index 00000000..36568621 --- /dev/null +++ b/specs/012-discovery-answer-ui/data-model.md @@ -0,0 +1,543 @@ +# Data Model: Discovery Answer UI Integration + +**Feature**: 012-discovery-answer-ui +**Date**: 2025-11-19 +**Phase**: 1 (Design & Contracts) + +--- + +## Overview + +This feature uses existing database schema (`memory` table) for discovery answer persistence. No new tables or migrations required. + +--- + +## Entities + +### 1. DiscoveryAnswer (Backend - Pydantic) + +**Purpose**: Validate incoming discovery answer submissions from frontend + +**Location**: `codeframe/ui/app.py` (inline) or `codeframe/core/models.py` + +**Schema**: +```python +from pydantic import BaseModel, Field, field_validator + +class DiscoveryAnswer(BaseModel): + """Request model for discovery answer submission.""" + + answer: str = Field( + ..., + min_length=1, + max_length=5000, + description="User's answer to the current discovery question" + ) + + @field_validator('answer') + @classmethod + def validate_answer(cls, v: str) -> str: + """Ensure answer is not empty after trimming.""" + trimmed = v.strip() + if not trimmed: + raise ValueError("Answer cannot be empty or whitespace only") + if len(trimmed) > 5000: + raise ValueError("Answer cannot exceed 5000 characters") + return trimmed +``` + +**Validation Rules**: +- Required field (not nullable) +- Min length: 1 character (after trimming) +- Max length: 5000 characters +- Whitespace-only answers rejected +- Automatic trimming before validation + +--- + +### 2. DiscoveryAnswerResponse (Backend - Pydantic) + +**Purpose**: Standardized API response after answer submission + +**Location**: `codeframe/ui/app.py` (inline) or `codeframe/core/models.py` + +**Schema**: +```python +from typing import Optional +from pydantic import BaseModel, Field + +class DiscoveryAnswerResponse(BaseModel): + """Response model for discovery answer submission.""" + + success: bool = Field( + ..., + description="Whether the answer was successfully processed" + ) + + next_question: Optional[str] = Field( + None, + description="Next discovery question text (null if discovery complete)" + ) + + is_complete: bool = Field( + ..., + description="Whether discovery phase is complete" + ) + + current_index: int = Field( + ..., + ge=0, + description="Current question index (0-based)" + ) + + total_questions: int = Field( + ..., + gt=0, + description="Total number of discovery questions" + ) + + progress_percentage: float = Field( + ..., + ge=0.0, + le=100.0, + description="Discovery completion percentage (0.0 - 100.0)" + ) + + class Config: + json_schema_extra = { + "example": { + "success": True, + "next_question": "What tech stack are you planning to use?", + "is_complete": False, + "current_index": 3, + "total_questions": 20, + "progress_percentage": 15.0 + } + } +``` + +**State Transitions**: +- `is_complete = False` → More questions remaining +- `is_complete = True` → Discovery finished, PRD generation starts +- `next_question = null` when `is_complete = True` + +--- + +### 3. DiscoveryState (Frontend - TypeScript) + +**Purpose**: Client-side representation of discovery progress + +**Location**: `web-ui/src/types/discovery.ts` + +**Schema**: +```typescript +export interface DiscoveryState { + /** Current phase: discovering | prd_generation | complete */ + phase: 'discovering' | 'prd_generation' | 'complete'; + + /** Current question being presented to user */ + currentQuestion: string | null; + + /** Unique identifier for current question */ + currentQuestionId: string | null; + + /** Current question number (1-based for display) */ + currentQuestionIndex: number; + + /** Total number of questions in discovery */ + totalQuestions: number; + + /** Number of questions answered so far */ + answeredCount: number; + + /** Discovery completion percentage (0-100) */ + progressPercentage: number; + + /** Whether all questions have been answered */ + isComplete: boolean; +} + +export interface DiscoveryAnswer { + /** User's answer text */ + answer: string; +} + +export interface DiscoveryProgressProps { + /** Project ID for API calls */ + projectId: number; + + /** Refresh interval in milliseconds (default: 5000) */ + refreshInterval?: number; +} +``` + +**Frontend State Management**: +```typescript +// Component state +const [discovery, setDiscovery] = useState(null); +const [answer, setAnswer] = useState(''); +const [isSubmitting, setIsSubmitting] = useState(false); +const [error, setError] = useState(null); +const [successMessage, setSuccessMessage] = useState(null); +``` + +--- + +### 4. Memory Table (Database - Existing) + +**Purpose**: Persistent storage for discovery answers + +**Location**: SQLite database (`.codeframe/state.db`) + +**Schema** (from `codeframe/persistence/database.py:193-206`): +```sql +CREATE TABLE IF NOT EXISTS memory ( + id INTEGER PRIMARY KEY, + project_id INTEGER REFERENCES projects(id), + category TEXT CHECK(category IN ( + 'pattern', 'decision', 'gotcha', 'preference', + 'conversation', 'discovery_state', 'discovery_answers', 'prd' + )), + key TEXT, + value TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) +``` + +**Discovery Answer Records**: +```sql +-- Example discovery answer +INSERT INTO memory (project_id, category, key, value) VALUES ( + 123, + 'discovery_answers', + 'q_project_problem', + 'Build a task management system for remote teams' +); +``` + +**Query Patterns**: +```python +# Get all discovery answers for a project +answers = db.get_project_memories( + project_id=123, + category="discovery_answers" +) + +# Get specific answer +answer = db.get_memory( + project_id=123, + category="discovery_answers", + key="q_project_problem" +) + +# Create new answer +db.create_memory( + project_id=123, + category="discovery_answers", + key="q_target_users", + value="Developers building web applications" +) +``` + +--- + +## Entity Relationships + +``` +┌─────────────────────┐ +│ Projects Table │ +│ (id, phase, ...) │ +└──────────┬──────────┘ + │ 1 + │ + │ * +┌──────────▼──────────────────────────────────┐ +│ Memory Table │ +│ - id (PK) │ +│ - project_id (FK → projects.id) │ +│ - category = 'discovery_answers' │ +│ - key = question_id (e.g., 'q_problem') │ +│ - value = answer text │ +│ - created_at, updated_at │ +└─────────────────────────────────────────────┘ + │ + │ read/write + │ +┌──────────▼──────────┐ +│ Lead Agent │ +│ - process_discovery│ +│ _answer() │ +│ - get_discovery │ +│ _status() │ +└─────────────────────┘ + │ + │ HTTP API + │ +┌──────────▼──────────────────────┐ +│ POST /api/projects/:id/ │ +│ discovery/answer │ +│ Request: DiscoveryAnswer │ +│ Response: DiscoveryAnswerResp │ +└─────────────────────────────────┘ + │ + │ WebSocket + │ +┌──────────▼──────────────────────┐ +│ WebSocket Broadcasts │ +│ - discovery_answer_submitted │ +│ - discovery_question_presented │ +│ - discovery_progress_updated │ +│ - discovery_completed │ +└─────────────────────────────────┘ + │ + │ subscribe + │ +┌──────────▼──────────────────────┐ +│ Frontend (React) │ +│ - DiscoveryProgress component │ +│ - DiscoveryState management │ +│ - WebSocket event handlers │ +└─────────────────────────────────┘ +``` + +--- + +## State Machine + +### Discovery Phase States + +``` +┌──────────────┐ +│ init │ User creates project +└──────┬───────┘ + │ + │ start_discovery() + ▼ +┌──────────────────┐ +│ discovering │ User answers questions +│ (phase) │ +└──────┬───────────┘ + │ + │ For each question: + │ - User submits answer + │ - process_discovery_answer(answer) + │ - Returns next question + │ + │ Loop until current_index >= total_questions + │ + ▼ +┌──────────────────┐ +│ complete │ All questions answered +│ (phase) │ +└──────┬───────────┘ + │ + │ generate_prd() + ▼ +┌──────────────────┐ +│ prd_generation │ Creating PRD document +│ (phase) │ +└──────┬───────────┘ + │ + │ PRD created + ▼ +┌──────────────────┐ +│ planning │ Project moves to planning phase +│ (phase) │ +└──────────────────┘ +``` + +### Answer Submission Flow + +``` +User types answer in textarea + │ + ▼ +User clicks Submit OR presses Ctrl+Enter + │ + ▼ +Frontend validation (1-5000 chars) + │ + ├─ FAIL → Show error message + │ + └─ PASS + │ + ▼ + POST /api/projects/:id/discovery/answer + { answer: "trimmed text" } + │ + ▼ + Backend validation (Pydantic) + │ + ├─ FAIL → 400 error + │ + └─ PASS + │ + ▼ + lead_agent.process_discovery_answer(answer) + │ + ├─ Save to memory table + ├─ Increment question index + ├─ Check completion + │ + ▼ + get_discovery_status() + │ + ▼ + Broadcast WebSocket events + ├─ discovery_answer_submitted + ├─ discovery_question_presented (if not complete) + └─ discovery_completed (if complete) + │ + ▼ + Return DiscoveryAnswerResponse + { + success: true, + next_question: "...", + is_complete: false, + current_index: 3, + total_questions: 20, + progress_percentage: 15.0 + } + │ + ▼ + Frontend receives response + │ + ├─ Clear answer input + ├─ Show success message + ├─ Update progress bar + ├─ Display next question + └─ (OR) Transition to PRD generation if complete +``` + +--- + +## Validation Rules + +### Frontend Validation + +**Answer Text**: +- Min length: 1 character (after trim) +- Max length: 5000 characters +- Cannot be empty or whitespace-only +- Character counter warning at >4500 chars + +**Submit Button**: +- Disabled when: `answer.trim().length === 0` +- Disabled when: `isSubmitting === true` +- Enabled when: `answer.trim().length > 0 && !isSubmitting` + +### Backend Validation + +**Pydantic Model** (`DiscoveryAnswer`): +- Field required (not null, not undefined) +- Min length: 1 (after trim) +- Max length: 5000 +- Automatic whitespace trimming +- ValueError if validation fails + +**Business Logic Validation**: +- Project must exist (`404` if not found) +- Project phase must be `"discovery"` (`400` if not) +- Discovery state must be `"discovering"` (not `"complete"`) + +--- + +## Performance Considerations + +### Database Queries + +**Optimizations**: +- Use `project_id` index for fast lookups +- Use composite index on `(project_id, category)` for discovery answers +- Limit query results with `LIMIT` clause + +**Expected Load**: +- 20 questions per project (typical) +- 20 INSERT operations per discovery session +- 1 SELECT per answer submission (to get next question) +- Low write frequency (human typing speed ~1 answer/minute) + +### API Response Time + +**Target**: <2 seconds end-to-end + +**Breakdown**: +- Frontend validation: <16ms (60fps) +- Network latency: ~50-200ms +- Backend processing: ~100-500ms + - Pydantic validation: <10ms + - Database write: <50ms + - Lead Agent processing: <100ms + - WebSocket broadcast: <50ms +- Frontend update: <100ms + +**LLM Processing**: If Lead Agent uses LLM to generate next question, add +1-2 seconds + +--- + +## Error Handling + +### HTTP Error Codes + +| Code | Scenario | Response Body | +|------|----------|---------------| +| `200` | Success | `DiscoveryAnswerResponse` | +| `400` | Validation failure | `{ detail: "Answer must be between 1 and 5000 characters" }` | +| `400` | Wrong phase | `{ detail: "Project is not in discovery phase" }` | +| `404` | Project not found | `{ detail: "Project not found" }` | +| `500` | Server error | `{ detail: "Internal server error" }` | + +### Frontend Error States + +**Validation Errors** (client-side): +- Empty answer: "Answer cannot be empty" +- Too long: "Answer exceeds 5000 character limit" +- Display in red box below submit button +- Keep answer in textarea (don't clear) +- Re-enable submit button after fixing + +**API Errors** (server-side): +- 400/422: Display `error.detail` message +- 500: "Server error occurred. Please try again." +- Network failure: "Failed to submit answer. Please check your connection." +- Display in red box below submit button +- Keep answer in textarea (allow retry) + +--- + +## WebSocket Message Formats + +See [contracts/websocket.yaml](./contracts/websocket.yaml) for complete schemas. + +**Example - Answer Submitted**: +```json +{ + "type": "discovery_answer_submitted", + "project_id": 123, + "question_id": "q_target_users", + "answer_preview": "Developers building web applications...", + "progress": { + "current": 3, + "total": 20, + "percentage": 15.0 + }, + "timestamp": "2025-11-19T14:30:00Z" +} +``` + +--- + +## Data Flow Summary + +1. **User Input** → Frontend validation → Submit button enabled +2. **Submit** → POST /api/projects/:id/discovery/answer → Backend validation +3. **Backend** → Lead Agent processes → Saves to memory table +4. **Completion Check** → If complete: discovery_completed, else: next question +5. **WebSocket** → Broadcast events → Frontend updates in real-time +6. **Response** → Frontend displays next question → Clear input → Update progress + +--- + +**Data Model Phase Complete** ✅ +**Next Step**: Generate API contracts (OpenAPI specification) diff --git a/specs/012-discovery-answer-ui/plan.md b/specs/012-discovery-answer-ui/plan.md new file mode 100644 index 00000000..1e806710 --- /dev/null +++ b/specs/012-discovery-answer-ui/plan.md @@ -0,0 +1,163 @@ +# Implementation Plan: Discovery Answer UI Integration + +**Branch**: `012-discovery-answer-ui` | **Date**: 2025-11-19 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/012-discovery-answer-ui/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. + +## Summary + +Enable users to answer discovery questions through an interactive UI with textarea input, character counter, submit button, and keyboard shortcuts. Currently, users can see questions but cannot answer them (showstopper UX issue). This feature adds answer submission with POST /api/projects/:id/discovery/answer, real-time validation, success/error messaging, and automatic progression to next questions. + +## Technical Context + +**Language/Version**: +- Backend: Python 3.11+ (async/await patterns) +- Frontend: TypeScript 5.3+ (strict mode) + +**Primary Dependencies**: +- Backend: FastAPI 0.100+, Pydantic 2.0+, AsyncAnthropic (for Lead Agent) +- Frontend: React 18, Next.js 14 (App Router), Tailwind CSS 3 + +**Storage**: +- SQLite (aiosqlite) for project state +- Discovery answers stored in projects table or separate discovery_answers table (NEEDS CLARIFICATION) + +**Testing**: +- Backend: pytest with async support +- Frontend: Jest 29+ (existing setup from Feature 2) + +**Target Platform**: +- Backend: Linux/macOS server (FastAPI/uvicorn) +- Frontend: Modern browsers (Chrome 90+, Firefox 88+, Safari 14+) + +**Project Type**: Web application (monorepo: backend + frontend) + +**Performance Goals**: +- Answer submission response: <2s (including LLM processing time) +- UI updates: <100ms +- Character counter: <16ms (60fps) + +**Constraints**: +- Answer length: 1-5000 characters +- Discovery session: 20 questions typical +- Must work with existing DiscoveryProgress component + +**Scale/Scope**: +- ~200 lines frontend implementation +- ~80 lines backend implementation +- 13 frontend tests + 7 backend tests = 20 tests total + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +### I. Test-First Development ✅ PASS +- **Status**: COMPLIANT +- **Evidence**: All 10 user stories have clear acceptance criteria that translate to tests +- **Plan**: Write tests first for both frontend and backend before implementation +- **Test Count**: 20 tests planned (13 frontend + 7 backend) + +### II. Async-First Architecture ✅ PASS +- **Status**: COMPLIANT +- **Evidence**: Backend endpoint uses `async def`, Lead Agent integration uses AsyncAnthropic +- **Details**: `/api/projects/:id/discovery/answer` follows async/await pattern +- **No Blocking Operations**: All I/O operations use async + +### III. Context Efficiency ✅ PASS +- **Status**: COMPLIANT (Not Applicable - No Agent Context Changes) +- **Reason**: This feature modifies UI and API only, no changes to context management system +- **Note**: Discovery answers may be added to context by Lead Agent, but that's handled by existing system + +### IV. Multi-Agent Coordination ✅ PASS +- **Status**: COMPLIANT +- **Evidence**: Lead Agent coordinates discovery via existing architecture +- **Integration**: New endpoint calls `lead_agent.process_discovery_answer()` following established pattern +- **No Direct Communication**: Frontend → API → Lead Agent (hierarchical) + +### V. Observability & Traceability ✅ PASS +- **Status**: COMPLIANT +- **Evidence**: + - Success/error messages provide user feedback + - API logging via FastAPI standard logging + - Discovery state persisted in SQLite +- **Missing**: WebSocket broadcast for answer submission (NEEDS CLARIFICATION - is this required?) + +### VI. Type Safety ✅ PASS +- **Status**: COMPLIANT +- **Evidence**: + - Frontend: TypeScript strict mode, interfaces for DiscoveryState + - Backend: Pydantic model for DiscoveryAnswer with validation + - React: Props interfaces defined for DiscoveryProgress +- **No `any` types**: All types explicitly defined + +### VII. Incremental Delivery ✅ PASS +- **Status**: COMPLIANT +- **Evidence**: User stories prioritized P1 (critical) → P2 (enhancement) +- **MVP**: US1, US2, US3, US5, US6, US7 (core flow) = deliverable independently +- **Enhancement**: US4 (keyboard shortcut), US8-US10 (polish) + +--- + +### GATE EVALUATION: ✅ PASS + +All constitution principles satisfied. Proceed to Phase 0 (Research). + +**Open Questions**: +1. Where are discovery answers stored in database? (projects table vs discovery_answers table) +2. Is WebSocket broadcast required for answer submission events? +3. Does Lead Agent.process_discovery_answer() method exist or need creation? + +## Project Structure + +### Documentation (this feature) + +``` +specs/[###-feature]/ +├── plan.md # This file (/speckit.plan command output) +├── research.md # Phase 0 output (/speckit.plan command) +├── data-model.md # Phase 1 output (/speckit.plan command) +├── quickstart.md # Phase 1 output (/speckit.plan command) +├── contracts/ # Phase 1 output (/speckit.plan command) +└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) +``` + +### Source Code (repository root) + +``` +# Web application structure (monorepo) +codeframe/ +├── ui/ +│ └── app.py # MODIFIED: Add POST /api/projects/:id/discovery/answer +├── agents/ +│ └── lead_agent.py # MODIFIED: Add process_discovery_answer() method (if needed) +└── persistence/ + └── database.py # POSSIBLY MODIFIED: Add discovery answer persistence + +tests/ +├── api/ +│ └── test_discovery_endpoints.py # NEW: 7 backend tests +└── agents/ + └── test_lead_agent.py # MODIFIED: Add discovery answer processing tests (if needed) + +web-ui/ +├── src/ +│ ├── components/ +│ │ ├── DiscoveryProgress.tsx # MODIFIED: Add answer input UI +│ │ └── __tests__/ +│ │ └── DiscoveryProgress.test.tsx # MODIFIED: Add 13 new tests +│ └── types/ +│ └── discovery.ts # MODIFIED/NEW: Define DiscoveryState interface +└── __tests__/ + └── integration/ + └── discovery-answer-flow.test.tsx # NEW: 2 integration tests +``` + +**Structure Decision**: Web application (Option 2) - Existing monorepo structure with backend (Python/FastAPI) and frontend (React/Next.js). This feature modifies existing components and adds one new backend endpoint. No new directories required. + +## Complexity Tracking + +*Fill ONLY if Constitution Check has violations that must be justified* + +**No violations detected**. This feature follows all constitution principles and adds minimal complexity to existing components. + diff --git a/specs/012-discovery-answer-ui/quickstart.md b/specs/012-discovery-answer-ui/quickstart.md new file mode 100644 index 00000000..9711c259 --- /dev/null +++ b/specs/012-discovery-answer-ui/quickstart.md @@ -0,0 +1,493 @@ +# Quickstart: Discovery Answer UI Integration + +**Feature**: 012-discovery-answer-ui +**Date**: 2025-11-19 +**Audience**: Developers implementing or testing this feature + +--- + +## Overview + +This feature enables users to answer discovery questions through an interactive UI with real-time feedback. It adds answer submission capability to the existing `DiscoveryProgress` component. + +**Key Components**: +- Frontend: Enhanced `DiscoveryProgress.tsx` with answer input UI +- Backend: POST `/api/projects/:id/discovery/answer` endpoint +- WebSocket: 4 broadcast events for real-time updates +- Database: Uses existing `memory` table with category `"discovery_answers"` + +--- + +## Prerequisites + +1. **Backend Running**: + ```bash + # Start FastAPI server + uvicorn codeframe.ui.app:app --reload --port 8080 + ``` + +2. **Frontend Running**: + ```bash + # Start Next.js development server + cd web-ui + npm run dev + ``` + +3. **Project in Discovery Phase**: + ```python + # Create project and start discovery + project_id = await db.create_project(name="test-project", description="Test") + lead_agent = LeadAgent(project_id=project_id) + lead_agent.start_discovery() + ``` + +--- + +## User Workflow + +### Step 1: Navigate to Dashboard + +``` +Open browser: http://localhost:3000/projects/123 +``` + +**What You See**: +- Dashboard with DiscoveryProgress component +- Current discovery question displayed +- Empty textarea with placeholder text +- Disabled submit button (no answer yet) +- Character counter: "0 / 5000 characters" + +### Step 2: Type Answer + +``` +User types: "Build a task management system for remote teams" +``` + +**What Happens**: +- Character counter updates: "47 / 5000 characters" +- Submit button becomes enabled +- Textarea remains editable + +### Step 3: Submit Answer + +**Option A: Click Submit Button** +``` +User clicks: "Submit Answer" +``` + +**Option B: Keyboard Shortcut** +``` +User presses: Ctrl+Enter +``` + +**What Happens**: +1. Frontend validation checks (1-5000 chars) +2. Submit button shows "Submitting..." +3. Textarea and button disabled +4. POST request sent to `/api/projects/123/discovery/answer` +5. Backend processes answer via Lead Agent +6. WebSocket events broadcasted: + - `discovery_answer_submitted` + - `discovery_question_presented` (next question) + - `discovery_progress_updated` +7. Success message shown: "Answer submitted! Loading next question..." +8. After 1 second: + - Success message disappears + - Next question displayed + - Textarea cleared + - Submit button re-enabled + - Progress bar updates: 5% → 10% + +### Step 4: Continue Until Complete + +**Repeat Steps 2-3 for remaining questions** + +When final question answered: +- `discovery_completed` WebSocket event broadcasted +- UI transitions to PRD generation phase +- Spinner shown: "Discovery complete! Generating PRD..." +- Progress bar: 100% + +--- + +## API Integration + +### Submit Answer (cURL Example) + +```bash +curl -X POST http://localhost:8080/api/projects/123/discovery/answer \ + -H "Content-Type: application/json" \ + -d '{ + "answer": "Build a task management system for remote teams with real-time collaboration features." + }' +``` + +**Success Response (200)**: +```json +{ + "success": true, + "next_question": "What tech stack are you planning to use?", + "is_complete": false, + "current_index": 3, + "total_questions": 20, + "progress_percentage": 15.0 +} +``` + +**Error Response (400 - Empty Answer)**: +```json +{ + "detail": "Answer must be between 1 and 5000 characters" +} +``` + +**Error Response (404 - Project Not Found)**: +```json +{ + "detail": "Project not found" +} +``` + +--- + +## WebSocket Integration + +### Frontend Subscription + +```typescript +// Subscribe to discovery events +const ws = new WebSocket('ws://localhost:8080/ws'); + +ws.onmessage = (event) => { + const message = JSON.parse(event.data); + + switch (message.type) { + case 'discovery_answer_submitted': + console.log('Answer submitted:', message.answer_preview); + console.log('Progress:', message.progress.percentage + '%'); + break; + + case 'discovery_question_presented': + console.log('Next question:', message.question_text); + setCurrentQuestion(message.question_text); + setProgress(message.current_index, message.total_questions); + break; + + case 'discovery_progress_updated': + console.log('Progress updated:', message.progress.percentage + '%'); + updateProgressBar(message.progress.percentage); + break; + + case 'discovery_completed': + console.log('Discovery complete! Total answers:', message.total_answers); + console.log('Next phase:', message.next_phase); + transitionToPRDGeneration(); + break; + } +}; +``` + +### Example WebSocket Messages + +**Answer Submitted**: +```json +{ + "type": "discovery_answer_submitted", + "project_id": 123, + "question_id": "q_target_users", + "answer_preview": "Developers building web applications...", + "progress": { + "current": 3, + "total": 20, + "percentage": 15.0 + }, + "timestamp": "2025-11-19T14:30:00Z" +} +``` + +**Next Question Presented**: +```json +{ + "type": "discovery_question_presented", + "project_id": 123, + "question_id": "q_tech_stack", + "question_text": "What tech stack are you planning to use?", + "current_index": 4, + "total_questions": 20, + "timestamp": "2025-11-19T14:30:01Z" +} +``` + +**Discovery Completed**: +```json +{ + "type": "discovery_completed", + "project_id": 123, + "total_answers": 20, + "next_phase": "prd_generation", + "timestamp": "2025-11-19T15:00:00Z" +} +``` + +--- + +## Testing + +### Manual Testing Checklist + +- [ ] **Submit Valid Answer** + - Type answer (10-100 chars) + - Click submit button + - Verify success message appears + - Verify next question displays + - Verify progress bar updates + +- [ ] **Keyboard Shortcut** + - Type answer + - Press Ctrl+Enter + - Verify same behavior as clicking submit + +- [ ] **Validation Errors** + - Submit empty answer → Error: "Answer cannot be empty" + - Submit whitespace-only answer → Error: "Answer must be between 1 and 5000 characters" + - Type 5001 chars → Error: "Answer exceeds 5000 character limit" + +- [ ] **Character Counter** + - Type text → Verify counter updates + - Type 4501 chars → Verify counter turns red + - Delete text → Verify counter updates + +- [ ] **Loading States** + - Click submit → Verify button shows "Submitting..." + - Verify textarea disabled during submission + - Verify submit button disabled during submission + +- [ ] **Progress Bar** + - Submit answer → Verify progress bar animates to new percentage + - Verify smooth transition (300ms duration) + +- [ ] **Discovery Completion** + - Answer all 20 questions + - Verify discovery_completed event + - Verify UI transitions to PRD generation + - Verify spinner shown + - Verify 100% progress + +### Unit Testing (Jest) + +```bash +# Run all DiscoveryProgress tests +cd web-ui +npm test -- src/components/__tests__/DiscoveryProgress.test.tsx + +# Expected: 13 tests passing +# - Answer textarea renders +# - Character counter updates +# - Submit button disabled when answer empty +# - Submit button disabled during submission +# - Ctrl+Enter triggers submit +# - Validation error for empty answer +# - Validation error for answer > 5000 chars +# - Success message displays after submit +# - Error message displays on API failure +# - Answer cleared after successful submit +# - Progress bar updates after submit +# - Next question appears after submit +# - Discovery completion state (100% progress) +``` + +### Backend Testing (pytest) + +```bash +# Run discovery endpoint tests +pytest tests/api/test_discovery_endpoints.py -v + +# Expected: 7 tests passing +# - POST /api/projects/:id/discovery/answer success (200) +# - POST with empty answer returns 400 +# - POST with answer > 5000 chars returns 400 +# - POST with invalid project_id returns 404 +# - POST when not in discovery phase returns 400 +# - LeadAgent.process_discovery_answer() called correctly +# - Response includes next_question, is_complete, current_index +``` + +--- + +## Troubleshooting + +### Issue: Submit Button Always Disabled + +**Cause**: Answer validation failing (empty or whitespace-only) + +**Solution**: +- Check answer.trim().length > 0 +- Verify state updates correctly on textarea change +- Check console for validation errors + +### Issue: No Next Question After Submit + +**Cause**: API request failing or WebSocket not connected + +**Solution**: +1. Check browser DevTools Network tab for API response +2. Verify WebSocket connection status: `ws.readyState === 1` (OPEN) +3. Check backend logs for errors +4. Verify Lead Agent is in "discovering" state + +### Issue: Progress Bar Not Updating + +**Cause**: WebSocket event not received or state not updating + +**Solution**: +1. Check WebSocket messages in DevTools (WS tab) +2. Verify `discovery_progress_updated` event received +3. Check state management: `setDiscovery(newState)` +4. Verify progress calculation: `(current / total) * 100` + +### Issue: Character Counter Shows Wrong Value + +**Cause**: State not syncing with textarea value + +**Solution**: +- Verify `onChange={(e) => setAnswer(e.target.value)}` +- Check `answer.length` calculation +- Ensure no debouncing delays + +--- + +## Code Examples + +### Frontend: Answer Submission Handler + +```typescript +const submitAnswer = async () => { + // Validation + if (!answer.trim() || answer.length > 5000) { + setError('Answer must be between 1 and 5000 characters'); + return; + } + + // Start submission + setIsSubmitting(true); + setError(null); + onSubmit?.(); // Parent shows loading spinner + + try { + // API call + const response = await fetch( + `/api/projects/${projectId}/discovery/answer`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ answer: answer.trim() }), + } + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Failed to submit answer'); + } + + // Success + setSuccessMessage('Answer submitted! Loading next question...'); + setAnswer(''); // Clear input + + // Refresh discovery state after 1 second + setTimeout(() => { + fetchDiscoveryState(); + setSuccessMessage(null); + }, 1000); + + } catch (err) { + console.error('Failed to submit answer:', err); + setError(err.message); + onError?.(err); + } finally { + setIsSubmitting(false); + } +}; +``` + +### Backend: API Endpoint Implementation + +```python +from pydantic import BaseModel, Field + +class DiscoveryAnswer(BaseModel): + answer: str = Field(..., min_length=1, max_length=5000) + +@app.post("/api/projects/{project_id}/discovery/answer") +async def submit_discovery_answer( + project_id: int, + answer: DiscoveryAnswer, +): + """Submit answer to current discovery question.""" + # Validate project + project = await db.get_project(project_id) + if not project: + raise HTTPException(404, "Project not found") + + if project.phase != "discovery": + raise HTTPException(400, "Project is not in discovery phase") + + # Get Lead Agent + lead_agent = await get_lead_agent(project_id) + + # Process answer + result_message = lead_agent.process_discovery_answer(answer.answer) + + # Get updated status + status = lead_agent.get_discovery_status() + + # Broadcast WebSocket events + await broadcast_discovery_answer_submitted( + manager=websocket_manager, + project_id=project_id, + question_id=status["current_question_id"], + answer_preview=answer.answer[:100], + current_index=status["current_question_index"], + total_questions=status["total_questions"], + ) + + if status["is_complete"]: + await broadcast_discovery_completed( + manager=websocket_manager, + project_id=project_id, + total_answers=status["answered_count"], + ) + else: + await broadcast_discovery_question_presented( + manager=websocket_manager, + project_id=project_id, + question_id=status["current_question_id"], + question_text=status["current_question"], + current_index=status["current_question_index"], + total_questions=status["total_questions"], + ) + + # Return response + return { + "success": True, + "next_question": status.get("current_question"), + "is_complete": status["is_complete"], + "current_index": status["current_question_index"], + "total_questions": status["total_questions"], + "progress_percentage": status["progress_percentage"], + } +``` + +--- + +## Next Steps + +After implementation: + +1. **Run Tests**: Ensure all 20 tests passing (13 frontend + 7 backend) +2. **Manual Testing**: Complete full discovery session (20 questions) +3. **Code Review**: Verify TypeScript types, error handling, accessibility +4. **Documentation**: Update CLAUDE.md with new endpoint +5. **Merge**: Create PR with conventional commit message + +**Ready for Implementation**: All design and planning complete ✅ diff --git a/specs/012-discovery-answer-ui/research.md b/specs/012-discovery-answer-ui/research.md new file mode 100644 index 00000000..468b9b44 --- /dev/null +++ b/specs/012-discovery-answer-ui/research.md @@ -0,0 +1,469 @@ +# Research: Discovery Answer UI Integration + +**Feature**: 012-discovery-answer-ui +**Date**: 2025-11-19 +**Phase**: 0 (Research & Design) + +--- + +## Research Questions + +This document answers the "NEEDS CLARIFICATION" items identified in plan.md: + +1. Where are discovery answers stored in the database? +2. Is WebSocket broadcast required for answer submission events? +3. Does `LeadAgent.process_discovery_answer()` method exist or need creation? + +--- + +## Question 1: Discovery Answer Storage + +### Decision +Use the existing `memory` table with category `"discovery_answers"`. + +### Evidence + +**Database Schema** (`codeframe/persistence/database.py:193-206`): +```python +CREATE TABLE IF NOT EXISTS memory ( + id INTEGER PRIMARY KEY, + project_id INTEGER REFERENCES projects(id), + category TEXT CHECK(category IN ( + 'pattern', 'decision', 'gotcha', 'preference', + 'conversation', 'discovery_state', 'discovery_answers', 'prd' + )), + key TEXT, + value TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) +``` + +**Lead Agent Implementation** (`codeframe/agents/lead_agent.py:211-250, 325-330`): +```python +# Loading discovery answers +answer_memories = [m for m in all_memories if m["category"] == "discovery_answers"] + +# Saving discovery answers +self.db.create_memory( + project_id=self.project_id, + category="discovery_answers", + key=self._current_question_id, + value=answer, +) +``` + +### Rationale + +**Why this approach**: +- ✅ Schema already exists with CHECK constraint validation +- ✅ Leverages existing `create_memory()` and `get_project_memories()` methods +- ✅ Consistent with discovery state storage pattern +- ✅ Supports project-scoped storage for multi-project collaboration +- ✅ No migration required + +**Storage Format**: +- **category**: `"discovery_answers"` +- **key**: Question ID (e.g., `"q_project_problem"`, `"q_target_users"`) +- **value**: User's answer text (JSON string if structured data needed) +- **project_id**: Scopes answers to specific project + +**Alternatives Considered**: + +1. **Separate `discovery_answers` table** + - **Rejected**: Adds complexity without benefit + - **Why**: memory table provides flexible key-value storage already + +2. **Store in `projects` table as JSON column** + - **Rejected**: Violates normalization, difficult to query + - **Why**: memory table provides better queryability and history + +--- + +## Question 2: WebSocket Broadcast Requirements + +### Decision +WebSocket broadcasts ARE required. Add 4 new broadcast functions following existing patterns. + +### Evidence + +**Existing Broadcast Infrastructure** (`codeframe/ui/websocket_broadcasts.py:1-627`): +- Comprehensive broadcast functions for: tasks, agents, tests, commits, blockers, activity +- Standard pattern: async functions with manager, logging, error handling +- Test coverage: `tests/ui/test_websocket_broadcasts.py` + +**Example Pattern** (from `broadcast_blocker_created:490-540`): +```python +async def broadcast_blocker_created( + manager, + project_id: int, + blocker_id: int, + question: str, + priority: str, +) -> None: + """Broadcast blocker creation to connected clients.""" + message = { + "type": "blocker_created", + "project_id": project_id, + "blocker": { + "id": blocker_id, + "question": question, + "priority": priority, + }, + "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), + } + try: + await manager.broadcast(message) + logger.debug(f"Broadcast blocker_created: {blocker_id}") + except Exception as e: + logger.error(f"Failed to broadcast blocker creation: {e}") +``` + +### Required Broadcast Events + +**1. `discovery_answer_submitted`** +```python +async def broadcast_discovery_answer_submitted( + manager, + project_id: int, + question_id: str, + answer_preview: str, # First 100 chars + current_index: int, + total_questions: int, +) -> None: + """Broadcast when user submits discovery answer.""" + message = { + "type": "discovery_answer_submitted", + "project_id": project_id, + "question_id": question_id, + "answer_preview": answer_preview, + "progress": { + "current": current_index, + "total": total_questions, + "percentage": round((current_index / total_questions) * 100, 1), + }, + "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), + } + await manager.broadcast(message) +``` + +**2. `discovery_question_presented`** +```python +async def broadcast_discovery_question_presented( + manager, + project_id: int, + question_id: str, + question_text: str, + current_index: int, + total_questions: int, +) -> None: + """Broadcast when next discovery question is presented.""" + message = { + "type": "discovery_question_presented", + "project_id": project_id, + "question_id": question_id, + "question_text": question_text, + "current_index": current_index, + "total_questions": total_questions, + "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), + } + await manager.broadcast(message) +``` + +**3. `discovery_progress_updated`** +```python +async def broadcast_discovery_progress_updated( + manager, + project_id: int, + current_index: int, + total_questions: int, + percentage: float, +) -> None: + """Broadcast discovery progress updates.""" + message = { + "type": "discovery_progress_updated", + "project_id": project_id, + "progress": { + "current": current_index, + "total": total_questions, + "percentage": percentage, + }, + "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), + } + await manager.broadcast(message) +``` + +**4. `discovery_completed`** +```python +async def broadcast_discovery_completed( + manager, + project_id: int, + total_answers: int, + next_phase: str = "prd_generation", +) -> None: + """Broadcast when discovery phase is completed.""" + message = { + "type": "discovery_completed", + "project_id": project_id, + "total_answers": total_answers, + "next_phase": next_phase, + "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), + } + await manager.broadcast(message) +``` + +### Rationale + +**Why broadcasts are required**: +- ✅ Dashboard needs real-time updates for discovery progress +- ✅ Multiple users may be monitoring project (team collaboration) +- ✅ Answer submissions should trigger immediate UI feedback +- ✅ Completion triggers phase transition to planning in UI +- ✅ Consistent with existing real-time update architecture + +**Frontend Integration**: +- WebSocket subscription in DiscoveryProgress component +- Update progress bar on `discovery_progress_updated` +- Display next question on `discovery_question_presented` +- Transition UI on `discovery_completed` +- Show confirmation on `discovery_answer_submitted` + +**Testing**: +- Add 4 unit tests to `tests/ui/test_websocket_broadcasts.py` +- Follow existing test patterns (message format validation, timestamp checks) + +--- + +## Question 3: Lead Agent Discovery Method + +### Decision +Use existing `process_discovery_answer()` method. **NO new method creation needed.** + +### Evidence + +**Method Exists** (`codeframe/agents/lead_agent.py:303-352`): +```python +def process_discovery_answer(self, answer: str) -> str: + """Process user answer during discovery phase. + + Saves the answer, advances to next question, and checks for completion. + """ + if self._discovery_state != "discovering": + logger.warning(f"process_discovery_answer called in state: {self._discovery_state}") + return "Discovery is not active. Call start_discovery() first." + + # Save current answer + if self._current_question_id: + self._discovery_answers[self._current_question_id] = answer + self.answer_capture.capture_answer(self._current_question_id, answer) + + # Persist to database + self.db.create_memory( + project_id=self.project_id, + category="discovery_answers", + key=self._current_question_id, + value=answer, + ) + + # Advance to next question + self._current_question_index += 1 + + # Check if complete + if self._current_question_index >= len(self._questions): + self._discovery_state = "complete" + self._save_discovery_state() + return "Discovery complete! Ready to generate PRD." + + # Return next question + next_question_data = self._questions[self._current_question_index] + self._current_question_id = next_question_data["id"] + self._save_discovery_state() + + return next_question_data["text"] +``` + +**Related Methods**: +- `start_discovery()` (lines 276-301) - Initializes discovery state +- `get_discovery_status()` (lines 354-408) - Returns comprehensive discovery status +- `_load_discovery_state()` (lines 211-250) - Loads from database on init +- `_save_discovery_state()` (lines 251-274) - Persists state to database +- `generate_prd()` (lines 423-482) - Creates PRD after completion + +### Workflow + +``` +1. Project Created → Lead Agent Initialized + ↓ +2. Call lead_agent.start_discovery() + ↓ (transitions to "discovering" state) + ↓ +3. User submits answer → API calls lead_agent.process_discovery_answer(answer) + ↓ (saves to memory table, advances question index) + ↓ +4. API endpoint returns: + - next_question_text + - current_index + - total_questions + - is_complete (boolean) + ↓ +5. Repeat step 3 until is_complete = true + ↓ +6. Call lead_agent.generate_prd() + ↓ (creates PRD from all answers) + ↓ +7. Project transitions to "planning" phase +``` + +### API Endpoint Integration + +**Implementation** (`codeframe/ui/app.py`): +```python +from pydantic import BaseModel, Field + +class DiscoveryAnswer(BaseModel): + answer: str = Field(..., min_length=1, max_length=5000) + +@app.post("/api/projects/{project_id}/discovery/answer") +async def submit_discovery_answer( + project_id: int, + answer: DiscoveryAnswer, +): + """Submit answer to current discovery question.""" + # Validate project exists + project = await db.get_project(project_id) + if not project: + raise HTTPException(404, "Project not found") + + # Validate discovery phase + if project.phase != "discovery": + raise HTTPException(400, "Project is not in discovery phase") + + # Get Lead Agent (from project manager or cache) + lead_agent = await get_lead_agent(project_id) + + # Process answer using existing method + result_message = lead_agent.process_discovery_answer(answer.answer) + + # Get updated discovery status + status = lead_agent.get_discovery_status() + + # Broadcast events + await broadcast_discovery_answer_submitted( + manager=websocket_manager, + project_id=project_id, + question_id=status["current_question_id"], + answer_preview=answer.answer[:100], + current_index=status["current_question_index"], + total_questions=status["total_questions"], + ) + + if status["is_complete"]: + await broadcast_discovery_completed( + manager=websocket_manager, + project_id=project_id, + total_answers=status["answered_count"], + ) + else: + await broadcast_discovery_question_presented( + manager=websocket_manager, + project_id=project_id, + question_id=status["current_question_id"], + question_text=status["current_question"], + current_index=status["current_question_index"], + total_questions=status["total_questions"], + ) + + # Return updated state to frontend + return { + "success": True, + "next_question": status.get("current_question"), + "is_complete": status["is_complete"], + "current_index": status["current_question_index"], + "total_questions": status["total_questions"], + "progress_percentage": status["progress_percentage"], + } +``` + +### Rationale + +**Why use existing method**: +- ✅ Method already implements complete workflow +- ✅ Database persistence already implemented +- ✅ State transitions handled (discovering → complete) +- ✅ Integrates with `AnswerCapture` for structured data extraction +- ✅ Follows existing patterns (error handling, logging) +- ✅ Returns appropriate next question or completion message +- ✅ Tested in `tests/agents/test_lead_agent.py` and `tests/planning/test_prd_generation.py` + +**No modifications needed**: +- Method signature is sufficient: `process_discovery_answer(answer: str) -> str` +- Return value contains next question text (empty string if complete) +- Discovery status available via `get_discovery_status()` method +- Completion detection built-in (checks question index) + +--- + +## Summary of Research Findings + +| Question | Decision | Requires New Code | Complexity | +|----------|----------|-------------------|------------| +| **Storage** | Use memory table, category="discovery_answers" | ❌ No | Low - existing schema | +| **WebSocket** | Add 4 broadcast functions | ✅ Yes | Low - follow patterns | +| **Lead Agent** | Use existing process_discovery_answer() | ❌ No | Low - method complete | + +### Updated Technical Context + +The research answers all "NEEDS CLARIFICATION" items: + +- **Storage**: `memory` table, category `"discovery_answers"` ✅ +- **WebSocket**: Required - add 4 broadcast functions ✅ +- **Lead Agent**: Existing method - use `process_discovery_answer()` ✅ + +### Implementation Impact + +**Backend Work**: +- Create 1 API endpoint (POST /api/projects/:id/discovery/answer) +- Add 4 WebSocket broadcast functions +- **Total**: ~120 lines of code + +**Frontend Work**: +- Modify DiscoveryProgress component (~200 lines) +- Subscribe to 4 WebSocket events +- **Total**: ~200 lines of code + +**Testing Work**: +- 7 backend tests (endpoint + broadcasts) +- 13 frontend tests (UI interactions) +- 2 integration tests (full flow) +- **Total**: 22 tests + +### Alternatives Considered + +**Alternative 1: Skip WebSocket broadcasts** +- **Rejected**: Dashboard would not update in real-time +- **Why**: Inconsistent with existing architecture, poor UX + +**Alternative 2: Create new Lead Agent method** +- **Rejected**: `process_discovery_answer()` already exists +- **Why**: Unnecessary duplication, adds complexity + +**Alternative 3: Store answers in separate table** +- **Rejected**: memory table provides sufficient functionality +- **Why**: No migration needed, leverages existing methods + +--- + +## Next Steps (Phase 1) + +With all research questions answered, proceed to Phase 1 (Design & Contracts): + +1. **Generate data-model.md**: Document discovery answer entities and relationships +2. **Generate contracts/**: Create OpenAPI spec for POST /api/projects/:id/discovery/answer +3. **Generate quickstart.md**: Document feature usage and integration points +4. **Update agent context**: Run `.specify/scripts/bash/update-agent-context.sh claude` + +--- + +**Research Phase Complete**: All "NEEDS CLARIFICATION" items resolved ✅ +**Gate Check**: Constitution compliance re-verified after research ✅ +**Ready for Phase 1**: Design & Contracts generation diff --git a/specs/012-discovery-answer-ui/spec.md b/specs/012-discovery-answer-ui/spec.md new file mode 100644 index 00000000..8b87d52a --- /dev/null +++ b/specs/012-discovery-answer-ui/spec.md @@ -0,0 +1,584 @@ +# Feature Specification: Discovery Answer UI Integration + +**Feature ID**: 012-discovery-answer-ui +**Sprint**: 9.5 - Critical UX Fixes +**Priority**: P0 - CRITICAL WORKFLOW +**Effort**: 6 hours +**Branch**: 012-discovery-answer-ui + +--- + +## Problem Statement + +### Current Behavior (BROKEN UX) + +Users can see discovery questions but cannot answer them: + +``` +Dashboard → DiscoveryProgress component shows: +"Current Question: What problem does your project solve?" +↓ +User sees question (read-only display) +↓ +NO input field, NO submit button +↓ +User confused: "Where do I type my answer?" +↓ +User gives up (8/10 UX complexity score) +``` + +### Expected Behavior + +``` +Dashboard → DiscoveryProgress shows: +"Question 2 of 20: What problem does your project solve?" +↓ +Textarea with placeholder: "Type your answer here..." +↓ +Character counter: "0 / 5000 characters" +↓ +Submit button enabled when answer > 0 chars +↓ +User types answer → clicks Submit OR Ctrl+Enter +↓ +POST /api/projects/:id/discovery/answer +↓ +Success message: "Answer submitted! Loading next question..." +↓ +Next question appears (1 second delay) +↓ +Progress bar updates: 10% → 15% +``` + +### Impact + +**Severity**: SHOWSTOPPER - Discovery phase is completely unusable without this feature. + +**Users Affected**: 100% of new users attempting to use CodeFRAME for the first time. + +**Business Impact**: Without answer input, users cannot complete the discovery phase, making the entire product unusable for its primary workflow. + +--- + +## User Stories + +### US1: Answer Input Field (P1 - CRITICAL) + +**As a** new user creating a project +**I want to** see an answer textarea below the current discovery question +**So that** I can type my response to the AI agent's question + +**Acceptance Criteria**: +- ✅ Textarea renders below current question display +- ✅ Placeholder text: "Type your answer here... (Ctrl+Enter to submit)" +- ✅ 6 rows tall by default +- ✅ Resizing disabled (resize-none class) +- ✅ Full width of container +- ✅ `maxLength={5000}` attribute enforced +- ✅ Focused state: blue ring (focus:ring-2 focus:ring-blue-500) +- ✅ Disabled state: gray background when isSubmitting=true +- ✅ Error state: red border when validation fails + +**Technical Notes**: +- Component: `web-ui/src/components/DiscoveryProgress.tsx` +- State: `const [answer, setAnswer] = useState('')` +- Validation: 1-5000 characters + +--- + +### US2: Character Counter (P1 - CRITICAL) + +**As a** user typing an answer +**I want to** see a character counter showing current/max length +**So that** I know how much I can write and avoid exceeding the limit + +**Acceptance Criteria**: +- ✅ Counter displays: "{count} / 5000 characters" +- ✅ Updates in real-time as user types +- ✅ Color changes to red when > 4500 characters (warning) +- ✅ Positioned below textarea, left-aligned +- ✅ Text size: text-sm +- ✅ Default color: text-gray-500 +- ✅ Warning color: text-red-600 + +**Technical Notes**: +- Value: `answer.length` +- Conditional styling: `answer.length > 4500 ? 'text-red-600' : 'text-gray-500'` + +--- + +### US3: Submit Button (P1 - CRITICAL) + +**As a** user who has typed an answer +**I want to** click a "Submit Answer" button +**So that** I can send my response to the AI agent and move to the next question + +**Acceptance Criteria**: +- ✅ Button text: "Submit Answer" (default) or "Submitting..." (loading) +- ✅ Positioned to the right of character counter +- ✅ Enabled when `answer.trim().length > 0` +- ✅ Disabled when `answer.trim().length === 0` or `isSubmitting === true` +- ✅ Disabled state: gray background (bg-gray-400), no hover +- ✅ Enabled state: blue background (bg-blue-600), hover:bg-blue-700 +- ✅ Rounded corners: rounded-lg +- ✅ Padding: py-2 px-6 +- ✅ Font weight: font-semibold + +**Technical Notes**: +- onClick handler: `submitAnswer()` +- Disabled logic: `disabled={isSubmitting || !answer.trim()}` + +--- + +### US4: Keyboard Shortcut (P2 - ENHANCEMENT) + +**As a** power user typing answers +**I want to** press Ctrl+Enter to submit my answer +**So that** I can complete discovery faster without using the mouse + +**Acceptance Criteria**: +- ✅ Ctrl+Enter triggers submit (same as button click) +- ✅ Works only when textarea is focused +- ✅ Does NOT submit if answer is empty +- ✅ Hint text below textarea: "💡 Tip: Press [Ctrl+Enter] to submit" +- ✅ Hint text size: text-xs +- ✅ Hint text color: text-gray-500 +- ✅ Centered alignment: text-center +- ✅ `` styling: px-2 py-1 bg-gray-100 border border-gray-300 rounded + +**Technical Notes**: +- Event handler: `onKeyDown={handleKeyPress}` +- Check: `e.ctrlKey && e.key === 'Enter'` +- Call: `submitAnswer()` + +--- + +### US5: Answer Submission (P1 - CRITICAL) + +**As a** user clicking submit +**I want to** send my answer to the backend API +**So that** the Lead Agent can process it and generate the next question + +**Acceptance Criteria**: +- ✅ POST request to `/api/projects/:id/discovery/answer` +- ✅ Request body: `{ answer: answer.trim() }` +- ✅ Content-Type: application/json +- ✅ Loading state: `isSubmitting = true` before API call +- ✅ All inputs disabled during submission (textarea + button) +- ✅ Success handling: clear answer, show success message, refresh state +- ✅ Error handling: show error message, keep answer, re-enable inputs +- ✅ Response expected: `{ success: true, next_question, is_complete, current_index }` + +**Technical Notes**: +- API client: Use `fetch()` (no abstraction needed for POST) +- Error states: 400 (validation), 404 (project not found), 500 (server error) +- Success flow: `setSuccessMessage()` → wait 1s → `fetchDiscoveryState()` → clear message + +--- + +### US6: Success Message (P1 - CRITICAL) + +**As a** user who submitted an answer +**I want to** see a confirmation message +**So that** I know my answer was saved successfully + +**Acceptance Criteria**: +- ✅ Message text: "Answer submitted! Loading next question..." +- ✅ Background: bg-green-50 +- ✅ Border: border border-green-200 +- ✅ Text color: text-green-800 +- ✅ Padding: p-3 +- ✅ Rounded corners: rounded-lg +- ✅ Display duration: 1 second +- ✅ Auto-dismiss after discovery state refreshes +- ✅ Position: Below submit button, above keyboard hint + +**Technical Notes**: +- State: `const [successMessage, setSuccessMessage] = useState(null)` +- Timing: `setTimeout(() => { fetchDiscoveryState(); setSuccessMessage(null); }, 1000)` + +--- + +### US7: Error Handling (P1 - CRITICAL) + +**As a** user experiencing submission errors +**I want to** see clear error messages +**So that** I understand what went wrong and can retry + +**Acceptance Criteria**: +- ✅ Validation error: "Answer must be between 1 and 5000 characters" +- ✅ API error (400): Display backend error message +- ✅ API error (500): "Server error occurred. Please try again." +- ✅ Network error: "Failed to submit answer. Please check your connection." +- ✅ Error background: bg-red-50 +- ✅ Error border: border border-red-200 +- ✅ Error text color: text-red-800 +- ✅ Error padding: p-3 +- ✅ Error rounded corners: rounded-lg +- ✅ Position: Below submit button, above keyboard hint +- ✅ Textarea red border when error present +- ✅ Answer preserved when error occurs (not cleared) + +**Technical Notes**: +- State: `const [error, setError] = useState(null)` +- Validation: Check before API call +- API errors: Parse `response.json().detail` or use generic message +- Network errors: Catch in try/catch block + +--- + +### US8: Progress Bar Update (P1 - CRITICAL) + +**As a** user submitting answers +**I want to** see the progress bar update after each answer +**So that** I understand how far through discovery I am + +**Acceptance Criteria**: +- ✅ Progress bar width: `(currentIndex / totalQuestions) * 100%` +- ✅ Progress percentage text: `Math.round((currentIndex / totalQuestions) * 100)` +- ✅ Smooth transition: transition-all duration-300 +- ✅ Updates automatically after successful submission +- ✅ Question counter updates: "2 of 20" → "3 of 20" +- ✅ No page reload required (SPA behavior) + +**Technical Notes**: +- Data source: `fetchDiscoveryState()` after submit +- Updates: `setDiscovery(updatedData)` triggers re-render +- Existing component: Progress bar already exists, just needs refresh + +--- + +### US9: Next Question Display (P1 - CRITICAL) + +**As a** user who completed an answer +**I want to** see the next question appear automatically +**So that** I can continue the discovery process seamlessly + +**Acceptance Criteria**: +- ✅ Next question appears 1 second after success message +- ✅ Previous answer is cleared from textarea +- ✅ Textarea remains focused (optional UX enhancement) +- ✅ Question number increments: "Question 2" → "Question 3" +- ✅ New question text replaces old question +- ✅ No page refresh or navigation +- ✅ Smooth transition (no flashing) + +**Technical Notes**: +- Trigger: `fetchDiscoveryState()` called after 1s delay +- Update: `setDiscovery(newState)` updates UI +- Clear input: `setAnswer('')` after success +- Focus management: Optional `textareaRef.current?.focus()` + +--- + +### US10: Discovery Completion (P2 - ENHANCEMENT) + +**As a** user answering the final question +**I want to** see a completion state instead of another question +**So that** I know discovery is finished and PRD generation is starting + +**Acceptance Criteria**: +- ✅ When `is_complete === true`, hide answer UI +- ✅ Show loading spinner (size: lg) +- ✅ Show message: "Discovery complete! Generating PRD..." +- ✅ Progress bar: 100% width +- ✅ Percentage: "100% complete" +- ✅ Centered layout: text-center py-8 +- ✅ Spinner from existing component: `` + +**Technical Notes**: +- Condition: `discovery?.phase === 'prd_generation'` +- This state likely already exists in DiscoveryProgress component +- No API changes needed (backend already handles this) + +--- + +## Backend Requirements + +### API Endpoint: POST /api/projects/:id/discovery/answer + +**Request**: +```json +{ + "answer": "string (1-5000 chars, trimmed)" +} +``` + +**Response (Success - 200)**: +```json +{ + "success": true, + "next_question": "What tech stack are you planning to use?", + "is_complete": false, + "current_index": 3, + "total_questions": 20 +} +``` + +**Response (Completion - 200)**: +```json +{ + "success": true, + "next_question": null, + "is_complete": true, + "current_index": 20, + "total_questions": 20 +} +``` + +**Response (Validation Error - 400)**: +```json +{ + "detail": "Answer must be between 1 and 5000 characters" +} +``` + +**Response (Not Found - 404)**: +```json +{ + "detail": "Project not found" +} +``` + +**Response (Wrong Phase - 400)**: +```json +{ + "detail": "Project is not in discovery phase" +} +``` + +**Backend Implementation** (`codeframe/ui/app.py`): +```python +from pydantic import BaseModel, Field + +class DiscoveryAnswer(BaseModel): + answer: str = Field(..., min_length=1, max_length=5000) + +@app.post("/api/projects/{project_id}/discovery/answer") +async def submit_discovery_answer( + project_id: int, + answer: DiscoveryAnswer, +): + """Submit answer to current discovery question.""" + # Validate project exists + project = await db.get_project(project_id) + if not project: + raise HTTPException(404, "Project not found") + + # Validate discovery phase + if project.phase != "discovery": + raise HTTPException(400, "Project is not in discovery phase") + + # Validate answer length + if not answer.answer.strip() or len(answer.answer) > 5000: + raise HTTPException(400, "Answer must be between 1 and 5000 characters") + + # Get Lead Agent + lead_agent = await get_lead_agent(project_id) + + # Process answer + result = await lead_agent.process_discovery_answer(answer.answer) + + # Return updated state + return { + "success": True, + "next_question": result.next_question, + "is_complete": result.is_complete, + "current_index": result.current_index, + "total_questions": result.total_questions, + } +``` + +--- + +## Non-Functional Requirements + +### Performance +- Answer submission response time: < 2 seconds (includes LLM processing) +- UI updates after submission: < 100ms +- Character counter updates: No perceptible lag (<16ms for 60fps) +- Progress bar animation: 300ms smooth transition + +### Accessibility +- Textarea must have proper `aria-label` or associated `