Building Interactive Chat Features: From Database Migrations to Markdown Rendering
Building Interactive Chat Features: From Database Migrations to Markdown Rendering
Ever had one of those development sessions where everything should work perfectly, but the database has other plans? Today I'm sharing the journey of adding interactive chat features to MiniRAG - a project that taught me some valuable lessons about PostgreSQL migrations and the joy of seeing 67 green test assertions.
What We Built
The goal was straightforward: transform a basic chat interface into something users would actually want to interact with. This meant adding three key features:
1. User Feedback System
Users can now give thumbs up/down feedback on chat responses. This required:
- Adding a
feedbackcolumn to theMessagemodel - Creating a new
PATCHendpoint:/v1/chat/{chat_id}/messages/{message_id}/feedback - Building interactive UI buttons that persist feedback state
2. Rich Markdown Rendering
Chat responses now support full markdown with syntax highlighting, code blocks, tables, and more. The implementation uses:
- marked.js for markdown parsing
- DOMPurify for XSS protection
- Custom dark glassmorphism CSS styling
3. Enhanced Chat Experience
The chat interface got some UX love:
- Copy-to-clipboard functionality
- Wider modal dialogs (48rem instead of 36rem)
- Responsive action buttons
- Better visual feedback for user interactions
The Technical Implementation
Backend: FastAPI + SQLModel
The feedback system started with extending the database schema:
# app/models/message.py
class Message(SQLModel, table=True):
# ... existing fields
feedback: Optional[str] = Field(default=None, max_length=20)
The API endpoint handles feedback submission with proper validation:
@router.patch("/{chat_id}/messages/{message_id}/feedback")
async def submit_feedback(
chat_id: UUID,
message_id: UUID,
feedback_request: FeedbackRequest,
# ... validation logic
):
# Toggle logic: same feedback clears it, different feedback updates it
Frontend: Alpine.js + Vanilla JavaScript
The markdown rendering pipeline ensures security while maintaining flexibility:
function renderMarkdown(text) {
const rawHtml = marked.parse(text || '');
return DOMPurify.sanitize(rawHtml);
}
Each chat bubble now renders with rich formatting:
<div x-html="renderMarkdown(message.content)"
class="markdown-body text-gray-100">
</div>
Lessons Learned: The Hard Way
Database Migration Gotcha
The biggest stumbling block came from a fundamental misunderstanding of SQLModel's create_all() method:
What I Expected: Adding a new field to an existing model would automatically add the column to PostgreSQL.
What Actually Happened:
asyncpg.exceptions.UndefinedColumnError: column messages.feedback does not exist
The Reality Check: create_all() only creates new tables - it doesn't modify existing ones. For existing tables, you need explicit migrations:
ALTER TABLE messages ADD COLUMN feedback VARCHAR(20) DEFAULT NULL;
This experience highlighted the importance of proper migration tooling. While the manual ALTER TABLE worked for development, production systems need something more robust like Alembic.
Test Infrastructure Dependencies
Another "gotcha" came from the test suite. Newman integration tests were failing because they expected fixture files that didn't exist in the repository:
8 upload-related assertions failed - fixtures/test_upload.txt and fixtures/test_bad.exe didn't exist
The Fix: Creating proper fixture files and ensuring they're version controlled. Sometimes the simplest solutions are the ones we overlook.
The Results: 67 Green Assertions
After working through these challenges, the payoff was immediate:
- ✅ 65 pytest tests passing
- ✅ 67/67 Newman assertions green
- ✅ Rich markdown rendering in chat bubbles
- ✅ Interactive feedback system
- ✅ Clean, responsive UI
What's Next
With the core functionality solid, there are some exciting enhancements on the horizon:
- API Test Coverage: Adding the new feedback endpoints to the Postman collection
- Syntax Highlighting: Integrating highlight.js for better code block rendering
- Migration Strategy: Setting up Alembic for proper schema version control
Key Takeaways
- Database Migrations Matter: Don't assume ORM tools handle schema changes automatically
- Test Dependencies: Version control your test fixtures - future you will thank present you
- Security First: Always sanitize user-generated HTML, even from trusted markdown parsers
- UX Details Count: Small touches like copy buttons and feedback interactions significantly improve user experience
Building chat interfaces teaches you to think about the entire user journey - from database consistency to visual feedback. While the technical challenges can be frustrating in the moment, there's something deeply satisfying about seeing that wall of green test results.
Have you run into similar database migration gotchas? I'd love to hear about your experiences in the comments below.