โ† Back to 2026-02-06
fastapichat-uiuser-feedbackmarkdown-renderingpostgresql

Building Interactive Chat Features: Copy, Feedback, and Markdown Support

Oliยท

Building Interactive Chat Features: Copy, Feedback, and Markdown Support

Ever wondered what it takes to transform a basic chat interface into something that feels polished and production-ready? Recently, I tackled three essential features that every modern chat application needs: copy-to-clipboard functionality, user feedback mechanisms, and proper markdown rendering. Here's how it went down, including one database migration lesson that I won't soon forget.

The Mission: Three Features, One Sprint

The goal was straightforward: enhance our chat interface with:

  • Copy-to-clipboard buttons for easy message sharing
  • Thumbs up/down feedback system for response quality tracking
  • Markdown rendering to make bot responses more readable and visually appealing

What seemed like a simple UI enhancement turned into a full-stack feature implementation touching everything from database schema to CSS styling.

The Backend Foundation

Database Schema Changes

First up was extending our Message model to support feedback storage:

# app/models/message.py
class Message(SQLModel, table=True):
    # ... existing fields
    feedback: str | None = None  # "positive", "negative", or None

The feedback system needed to be simple but flexible, so I went with a string field that accepts predefined values rather than a boolean. This approach leaves room for future expansion (maybe "helpful", "creative", "accurate" categories down the line).

RESTful Feedback API

The API design followed REST conventions with a dedicated feedback endpoint:

# PATCH /v1/chat/{chat_id}/messages/{message_id}/feedback
class FeedbackRequest(BaseModel):
    feedback: Literal["positive", "negative"] | None

@router.patch("/{chat_id}/messages/{message_id}/feedback")
async def update_message_feedback(
    chat_id: str,
    message_id: str, 
    request: FeedbackRequest,
    # ... auth and validation
):
    # Update logic here

I chose PATCH over PUT since we're updating a single field, and the endpoint supports clearing feedback by sending null. The API also includes proper tenant isolation - users can only provide feedback on messages from their own chats.

Frontend Magic: From Plain Text to Rich Interactions

Interactive Chat Bubbles

The UI transformation was where things got visually exciting. Each assistant message now sports a clean action bar with three buttons:

<div class="chat-actions">
    <button class="chat-action-btn" @click="copyToClipboard(message.content)">
        ๐Ÿ“‹ Copy
    </button>
    <button class="chat-action-btn" @click="submitFeedback(message.id, 'positive')">
        ๐Ÿ‘
    </button>
    <button class="chat-action-btn" @click="submitFeedback(message.id, 'negative')">
        ๐Ÿ‘Ž
    </button>
</div>

The buttons provide immediate visual feedback - thumbs turn green or red when active, and the copy button briefly shows a checkmark after successful copying.

Markdown Rendering with Security

Bot responses now support full markdown formatting using marked.js for parsing and DOMPurify for XSS protection:

function renderMarkdown(content) {
    if (!content) return '';
    const html = marked.parse(content);
    return DOMPurify.sanitize(html);
}

This combo gives us rich text formatting while maintaining security. Code blocks, lists, headings, and even tables now render beautifully in the chat interface.

Dark Theme Markdown Styling

Getting markdown to look good in a dark glassmorphism theme required custom CSS:

.markdown-body {
    color: #e5e7eb;
    line-height: 1.6;
}

.markdown-body pre {
    background: rgba(17, 24, 39, 0.8);
    border: 1px solid rgba(75, 85, 99, 0.3);
    border-radius: 6px;
    padding: 1rem;
}

.markdown-body code {
    background: rgba(31, 41, 55, 0.6);
    color: #fbbf24;
    padding: 0.125rem 0.25rem;
    border-radius: 3px;
}

The result? Code snippets and formatted text that actually looks native to the interface instead of like an afterthought.

Lessons Learned: The Database Migration Gotcha

Here's where things got interesting (and by interesting, I mean "completely broke for 20 minutes").

The Problem

After adding the feedback field to my SQLModel and restarting the server, I was greeted with:

asyncpg.exceptions.UndefinedColumnError: column messages.feedback does not exist

The Misconception

I had assumed that SQLModel's create_all() method would handle adding new columns to existing tables. Wrong. This method only creates tables that don't exist - it won't alter existing schemas.

The Solution

Manual database migration to the rescue:

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

The Takeaway

For production applications, this reinforced the importance of proper migration tools like Alembic. SQLModel's create_all() is great for initial setup and testing, but any schema changes to existing tables need explicit migration handling.

Testing: Ensuring Rock-Solid Functionality

The feedback system got comprehensive test coverage across five key scenarios:

def test_positive_feedback():
    # Test setting positive feedback
    
def test_toggle_feedback():
    # Test changing from positive to negative
    
def test_clear_feedback():
    # Test setting feedback back to null
    
def test_feedback_on_user_message_rejected():
    # Test that user messages can't receive feedback
    
def test_cross_tenant_feedback_isolation():
    # Test that users can't give feedback on other users' chats

All 65 tests passing means the new features integrate cleanly without breaking existing functionality.

The Results

What started as a simple UI enhancement became a full-featured interactive chat experience:

  • Copy functionality that works reliably across browsers
  • Visual feedback system with persistent state management
  • Rich markdown rendering that makes bot responses more engaging and readable
  • Responsive design with wider chat modals to accommodate the new features

What's Next?

The foundation is solid, but there's always room for improvement:

  1. Syntax highlighting for code blocks using highlight.js
  2. Persistent feedback state in the "Try It" modal across page refreshes
  3. Analytics dashboard to track feedback patterns and response quality trends

Building chat interfaces teaches you that the devil really is in the details. Users expect copy buttons to work instantly, feedback to persist properly, and markdown to render beautifully. Getting all these pieces to work together seamlessly requires attention to everything from API design to CSS specificity.

The database migration lesson alone made this sprint worthwhile - sometimes the biggest learning comes from the unexpected failures, not the planned successes.