Building Interactive Chat Features: Copy, Feedback, and Markdown Support
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:
- Syntax highlighting for code blocks using highlight.js
- Persistent feedback state in the "Try It" modal across page refreshes
- 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.