← Back to 2026-02-06
pythonfastapichat-uimarkdownpostgresqltesting

Building Interactive Chat Features: From User Feedback to Markdown Rendering

Oli·

Building Interactive Chat Features: From User Feedback to Markdown Rendering

Ever wondered what it takes to transform a basic chat interface into something users actually want to interact with? Today I'm sharing the journey of adding essential features to our MiniRAG chat application - complete with the stumbling blocks and "aha!" moments that made it interesting.

The Mission: Making Chat Feel Alive

Our goal was ambitious but clear: transform a plain text chat into a rich, interactive experience. We needed:

  • User feedback system - thumbs up/down for message quality
  • Markdown rendering - because nobody wants to read unformatted code blocks
  • Copy functionality - let users grab responses easily
  • Robust testing - because breaking chat is breaking trust

By the end of this session, we went from 0 to 65 passing pytest tests and 75/75 Newman API assertions. Here's how we got there.

The Backend Foundation: Adding Feedback

First stop: the database. We needed to track user feedback on chat messages, which meant extending our Message model:

# app/models/message.py
class Message(SQLModel, table=True):
    # ... existing fields ...
    feedback: Optional[str] = Field(default=None, max_length=20)

Then we added the API endpoint to handle feedback submission:

# app/api/v1/chat.py
@router.patch("/chat/{chat_id}/messages/{message_id}/feedback")
async def update_message_feedback(
    chat_id: int,
    message_id: int, 
    feedback_data: MessageFeedbackUpdate,
    # ... validation logic ...
):
    # Update feedback and return updated message

The beauty of this approach? Simple string values ("positive", "negative", null) that are easy to query and analyze later.

Frontend Magic: Actions and Markdown

On the frontend, we added action buttons to every chat message:

// dashboard/js/api.js
async function submitFeedback(chatId, messageId, feedback) {
    const response = await fetch(`/api/v1/chat/${chatId}/messages/${messageId}/feedback`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ feedback })
    });
    return response.json();
}

But the real game-changer was markdown rendering. Instead of displaying raw text, we integrated marked.js with DOMPurify for safe HTML rendering:

// dashboard/js/app.js  
function renderMarkdown(text) {
    const html = marked.parse(text);
    return DOMPurify.sanitize(html);
}

Now code blocks, lists, and formatted text render beautifully - exactly what you'd expect from a modern chat interface.

Testing: The Newman Collection Challenge

Here's where things got interesting. We wanted comprehensive API testing with Postman/Newman, but ran into a classic issue: file dependencies.

Our upload tests were failing because Newman couldn't find the fixture files:

# Before: 67/75 assertions passed (missing fixtures)
# After: 75/75 assertions passed 

The fix was simple but crucial - we created proper fixture files:

fixtures/
├── test_upload.txt    # Valid upload file
└── test_bad.exe      # Invalid file type for testing

Our Postman collection now includes comprehensive feedback testing:

  • Submit positive feedback
  • Toggle to negative feedback
  • Clear feedback (set to null)
  • Handle invalid scenarios

Lessons Learned: Database Migrations the Hard Way

The Challenge: Adding the feedback column to our existing Message model seemed straightforward. Just update the SQLModel and restart the server, right?

The Reality: UndefinedColumnError: column messages.feedback does not exist

The Insight: SQLModel's create_all() only creates new tables - it doesn't alter existing ones. We had to manually run:

ALTER TABLE messages ADD COLUMN feedback VARCHAR(20) DEFAULT NULL;

This was a perfect reminder that while ORMs are powerful, understanding the underlying database operations is crucial. For future projects, setting up Alembic migrations from day one would save this headache.

The Results: A Complete Chat Experience

After all the changes, here's what users now experience:

  1. Rich formatting - Code blocks, lists, and emphasis render properly
  2. Interactive feedback - One-click thumbs up/down on any message
  3. Easy copying - Grab any response with a single click
  4. Wider chat windows - More space for complex conversations (48rem width)
  5. Comprehensive testing - 65 Python tests + 75 API assertions

What's Next?

With the foundation solid, we're eyeing some exciting enhancements:

  • Syntax highlighting with highlight.js for code blocks
  • Feedback analytics dashboard to track message quality trends
  • Proper migration system using Alembic for schema changes

Key Takeaways

  1. User experience matters - Small touches like markdown rendering and copy buttons make a huge difference
  2. Test thoroughly - Having comprehensive API tests caught edge cases we would have missed
  3. Plan for data evolution - Set up migration tools early, before you need them
  4. Iterate quickly - Sometimes manual fixes (like the ALTER TABLE) are okay to maintain momentum

Building chat features taught us that the devil really is in the details. But when those details come together - proper formatting, easy interactions, and reliable functionality - the result is something users genuinely enjoy using.

What chat features have you found most impactful in your applications? I'd love to hear about your experiences in the comments below.