mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Add emoji reactions feature to messages in chat interface
Co-authored-by: matt <matt@roocode.com>
This commit is contained in:
parent
298908fe1d
commit
cbdecd15ea
7 changed files with 277 additions and 0 deletions
74
emoji-reactions-implementation.md
Normal file
74
emoji-reactions-implementation.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Emoji Reactions Feature Implementation
|
||||
|
||||
## Overview
|
||||
Successfully implemented an emoji reaction feature that allows users to add emoji reactions to any task messages they can see in the Roo Code VS Code extension.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Message Type Extension
|
||||
**File:** `packages/types/src/message.ts`
|
||||
- Extended `ClineMessage` schema to include `reactions?: Record<string, number>`
|
||||
- Reactions map emoji strings to reaction counts
|
||||
|
||||
### 2. EmojiReactions Component
|
||||
**File:** `webview-ui/src/components/chat/EmojiReactions.tsx`
|
||||
- Standalone React component for displaying and managing reactions
|
||||
- Features:
|
||||
- 16 common emoji picker (👍, 👎, ❤️, 😂, 😮, 😢, 😡, 🎉, 🚀, 👀, 💯, 🔥, ⭐, ✅, ❌, 🤔)
|
||||
- Click-to-toggle reactions (click existing to remove, click new to add)
|
||||
- Displays reaction counts as clickable buttons
|
||||
- Outside-click to close picker functionality
|
||||
|
||||
### 3. Message Protocol Extension
|
||||
**File:** `src/shared/WebviewMessage.ts`
|
||||
- Added `"addReaction"` and `"removeReaction"` message types
|
||||
- Added `messageTs?: number` and `emoji?: string` properties for reaction data
|
||||
|
||||
### 4. Backend Message Handling
|
||||
**File:** `src/core/webview/webviewMessageHandler.ts`
|
||||
- Added handlers for `addReaction` and `removeReaction` messages
|
||||
- Delegates to Task class methods for processing
|
||||
|
||||
### 5. Task Class Methods
|
||||
**File:** `src/core/task/Task.ts`
|
||||
- `addReaction(messageTs: number, emoji: string)`: Increments reaction count
|
||||
- `removeReaction(messageTs: number, emoji: string)`: Decrements reaction count
|
||||
- Automatic persistence and webview state synchronization
|
||||
|
||||
### 6. UI Integration
|
||||
**File:** `webview-ui/src/components/chat/ChatRow.tsx`
|
||||
- Integrated EmojiReactions component into key message types:
|
||||
- Text messages (`message.say === "text"`)
|
||||
- Completion results (`message.say === "completion_result"` and `message.ask === "completion_result"`)
|
||||
- User feedback messages (`message.say === "user_feedback"`)
|
||||
- Reactions only display for complete (non-partial) messages
|
||||
- Handlers for adding/removing reactions via VSCode message passing
|
||||
|
||||
## Key Features
|
||||
|
||||
### User Experience
|
||||
- **Intuitive interaction**: Click existing reactions to remove, click new emojis to add
|
||||
- **Visual feedback**: Reaction counts displayed on buttons
|
||||
- **Easy access**: Emoji picker appears on hover with smiling face icon
|
||||
- **Persistent**: Reactions are saved with messages and persist across sessions
|
||||
|
||||
### Technical Features
|
||||
- **Real-time updates**: Changes sync immediately across the interface
|
||||
- **Proper persistence**: Reactions saved to task message storage
|
||||
- **Type safety**: Full TypeScript support with proper type definitions
|
||||
- **Performance**: Minimal re-renders with proper React optimization
|
||||
|
||||
### Message Types Supporting Reactions
|
||||
1. **Text responses** from the AI assistant
|
||||
2. **Task completion results** (both ask and say types)
|
||||
3. **User feedback messages** that users send
|
||||
|
||||
## Usage
|
||||
Users can now:
|
||||
1. See a small 😊 button appear on eligible messages
|
||||
2. Click it to open an emoji picker with 16 common reaction emojis
|
||||
3. Click any emoji to add a reaction
|
||||
4. Click existing reaction buttons to remove their reaction
|
||||
5. See reaction counts update in real-time
|
||||
|
||||
The feature seamlessly integrates into the existing chat interface without disrupting the current user experience while adding a new dimension of interaction and feedback capability.
|
||||
|
|
@ -154,6 +154,7 @@ export const clineMessageSchema = z.object({
|
|||
progressStatus: toolProgressStatusSchema.optional(),
|
||||
contextCondense: contextCondenseSchema.optional(),
|
||||
isProtected: z.boolean().optional(),
|
||||
reactions: z.record(z.string(), z.number()).optional(), // emoji -> count mapping
|
||||
})
|
||||
|
||||
export type ClineMessage = z.infer<typeof clineMessageSchema>
|
||||
|
|
|
|||
|
|
@ -373,6 +373,34 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
await this.saveClineMessages()
|
||||
}
|
||||
|
||||
public async addReaction(messageTs: number, emoji: string) {
|
||||
const message = this.clineMessages.find(m => m.ts === messageTs)
|
||||
if (message) {
|
||||
if (!message.reactions) {
|
||||
message.reactions = {}
|
||||
}
|
||||
message.reactions[emoji] = (message.reactions[emoji] || 0) + 1
|
||||
await this.saveClineMessages()
|
||||
|
||||
const provider = this.providerRef.deref()
|
||||
await provider?.postStateToWebview()
|
||||
}
|
||||
}
|
||||
|
||||
public async removeReaction(messageTs: number, emoji: string) {
|
||||
const message = this.clineMessages.find(m => m.ts === messageTs)
|
||||
if (message && message.reactions && message.reactions[emoji]) {
|
||||
message.reactions[emoji] = Math.max(0, message.reactions[emoji] - 1)
|
||||
if (message.reactions[emoji] === 0) {
|
||||
delete message.reactions[emoji]
|
||||
}
|
||||
await this.saveClineMessages()
|
||||
|
||||
const provider = this.providerRef.deref()
|
||||
await provider?.postStateToWebview()
|
||||
}
|
||||
}
|
||||
|
||||
private async updateClineMessage(message: ClineMessage) {
|
||||
const provider = this.providerRef.deref()
|
||||
await provider?.postMessageToWebview({ type: "messageUpdated", clineMessage: message })
|
||||
|
|
|
|||
|
|
@ -1727,5 +1727,21 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
break
|
||||
}
|
||||
case "addReaction":
|
||||
if (message.messageTs !== undefined && message.emoji) {
|
||||
const currentTask = provider.getCurrentCline()
|
||||
if (currentTask) {
|
||||
await currentTask.addReaction(message.messageTs, message.emoji)
|
||||
}
|
||||
}
|
||||
break
|
||||
case "removeReaction":
|
||||
if (message.messageTs !== undefined && message.emoji) {
|
||||
const currentTask = provider.getCurrentCline()
|
||||
if (currentTask) {
|
||||
await currentTask.removeReaction(message.messageTs, message.emoji)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,8 @@ export interface WebviewMessage {
|
|||
| "switchTab"
|
||||
| "profileThresholds"
|
||||
| "shareTaskSuccess"
|
||||
| "addReaction"
|
||||
| "removeReaction"
|
||||
text?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
disabled?: boolean
|
||||
|
|
@ -213,6 +215,8 @@ export interface WebviewMessage {
|
|||
mpInstallOptions?: InstallMarketplaceItemOptions
|
||||
config?: Record<string, any> // Add config to the payload
|
||||
visibility?: ShareVisibility // For share visibility
|
||||
messageTs?: number // For reaction messages
|
||||
emoji?: string // For reaction messages
|
||||
}
|
||||
|
||||
export const checkoutDiffPayloadSchema = z.object({
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { CommandExecutionError } from "./CommandExecutionError"
|
|||
import { AutoApprovedRequestLimitWarning } from "./AutoApprovedRequestLimitWarning"
|
||||
import { CondenseContextErrorRow, CondensingContextRow, ContextCondenseRow } from "./ContextCondenseRow"
|
||||
import CodebaseSearchResultsDisplay from "./CodebaseSearchResultsDisplay"
|
||||
import EmojiReactions from "./EmojiReactions"
|
||||
|
||||
interface ChatRowProps {
|
||||
message: ClineMessage
|
||||
|
|
@ -112,6 +113,23 @@ export const ChatRowContent = ({
|
|||
onToggleExpand(message.ts)
|
||||
}, [onToggleExpand, message.ts])
|
||||
|
||||
// Emoji reaction handlers
|
||||
const handleAddReaction = useCallback((emoji: string) => {
|
||||
vscode.postMessage({
|
||||
type: "addReaction",
|
||||
messageTs: message.ts,
|
||||
emoji
|
||||
})
|
||||
}, [message.ts])
|
||||
|
||||
const handleRemoveReaction = useCallback((emoji: string) => {
|
||||
vscode.postMessage({
|
||||
type: "removeReaction",
|
||||
messageTs: message.ts,
|
||||
emoji
|
||||
})
|
||||
}, [message.ts])
|
||||
|
||||
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
|
||||
if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
|
||||
const info = safeJsonParse<ClineApiReqInfo>(message.text)
|
||||
|
|
@ -975,6 +993,14 @@ export const ChatRowContent = ({
|
|||
return (
|
||||
<div>
|
||||
<Markdown markdown={message.text} partial={message.partial} />
|
||||
{!message.partial && (
|
||||
<EmojiReactions
|
||||
messageTs={message.ts}
|
||||
reactions={message.reactions}
|
||||
onAddReaction={handleAddReaction}
|
||||
onRemoveReaction={handleRemoveReaction}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
case "user_feedback":
|
||||
|
|
@ -999,6 +1025,13 @@ export const ChatRowContent = ({
|
|||
{message.images && message.images.length > 0 && (
|
||||
<Thumbnails images={message.images} style={{ marginTop: "8px" }} />
|
||||
)}
|
||||
<EmojiReactions
|
||||
messageTs={message.ts}
|
||||
reactions={message.reactions}
|
||||
onAddReaction={handleAddReaction}
|
||||
onRemoveReaction={handleRemoveReaction}
|
||||
className="px-2 pb-1"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
case "user_feedback_diff":
|
||||
|
|
@ -1035,6 +1068,12 @@ export const ChatRowContent = ({
|
|||
</div>
|
||||
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
|
||||
<Markdown markdown={message.text} />
|
||||
<EmojiReactions
|
||||
messageTs={message.ts}
|
||||
reactions={message.reactions}
|
||||
onAddReaction={handleAddReaction}
|
||||
onRemoveReaction={handleRemoveReaction}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
@ -1191,6 +1230,14 @@ export const ChatRowContent = ({
|
|||
</div>
|
||||
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
|
||||
<Markdown markdown={message.text} partial={message.partial} />
|
||||
{!message.partial && (
|
||||
<EmojiReactions
|
||||
messageTs={message.ts}
|
||||
reactions={message.reactions}
|
||||
onAddReaction={handleAddReaction}
|
||||
onRemoveReaction={handleRemoveReaction}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
107
webview-ui/src/components/chat/EmojiReactions.tsx
Normal file
107
webview-ui/src/components/chat/EmojiReactions.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import React, { useState, useRef, useEffect } from "react"
|
||||
import { Button } from "@src/components/ui"
|
||||
import { cn } from "@src/lib/utils"
|
||||
|
||||
interface EmojiReactionsProps {
|
||||
messageTs: number
|
||||
reactions?: Record<string, number>
|
||||
onAddReaction: (emoji: string) => void
|
||||
onRemoveReaction: (emoji: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const COMMON_EMOJIS = [
|
||||
"👍", "👎", "❤️", "😂", "😮", "😢", "😡", "🎉",
|
||||
"🚀", "👀", "💯", "🔥", "⭐", "✅", "❌", "🤔"
|
||||
]
|
||||
|
||||
export const EmojiReactions = ({
|
||||
messageTs,
|
||||
reactions = {},
|
||||
onAddReaction,
|
||||
onRemoveReaction,
|
||||
className,
|
||||
}) => {
|
||||
const [showPicker, setShowPicker] = useState(false)
|
||||
const pickerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Close picker when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (pickerRef.current && !pickerRef.current.contains(event.target as Node)) {
|
||||
setShowPicker(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (showPicker) {
|
||||
document.addEventListener("mousedown", handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside)
|
||||
}
|
||||
}, [showPicker])
|
||||
|
||||
const handleEmojiClick = (emoji: string) => {
|
||||
if (reactions && reactions[emoji] && reactions[emoji] > 0) {
|
||||
onRemoveReaction(emoji)
|
||||
} else {
|
||||
onAddReaction(emoji)
|
||||
}
|
||||
setShowPicker(false)
|
||||
}
|
||||
|
||||
const hasReactions = Object.keys(reactions).some(emoji => reactions[emoji] > 0)
|
||||
|
||||
return (
|
||||
<div className={cn("relative flex items-center gap-1 mt-2", className)}>
|
||||
{/* Existing reactions */}
|
||||
{reactions && Object.entries(reactions)
|
||||
.filter(([_, count]) => (count as number) > 0)
|
||||
.map(([emoji, count]) => (
|
||||
<Button
|
||||
key={emoji}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 px-2 py-0 text-xs bg-vscode-button-secondaryBackground hover:bg-vscode-button-secondaryHoverBackground border-vscode-button-border"
|
||||
onClick={() => handleEmojiClick(emoji)}
|
||||
>
|
||||
<span className="mr-1">{emoji}</span>
|
||||
<span>{count as number}</span>
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{/* Add reaction button */}
|
||||
<div className="relative" ref={pickerRef}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-xs hover:bg-vscode-button-secondaryHoverBackground"
|
||||
onClick={() => setShowPicker(!showPicker)}
|
||||
title="Add reaction"
|
||||
>
|
||||
<span className="text-sm">😊</span>
|
||||
</Button>
|
||||
|
||||
{/* Emoji picker */}
|
||||
{showPicker && (
|
||||
<div className="absolute top-full left-0 mt-1 p-2 bg-vscode-dropdown-background border border-vscode-dropdown-border rounded shadow-lg z-50 grid grid-cols-8 gap-1 max-w-64">
|
||||
{COMMON_EMOJIS.map((emoji) => (
|
||||
<Button
|
||||
key={emoji}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 hover:bg-vscode-list-hoverBackground"
|
||||
onClick={() => handleEmojiClick(emoji)}
|
||||
>
|
||||
{emoji}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmojiReactions
|
||||
Loading…
Add table
Reference in a new issue