diff --git a/frontend/src/app/pages/AgentChat/shell/FeedbackDialog.tsx b/frontend/src/app/pages/AgentChat/shell/FeedbackDialog.tsx new file mode 100644 index 00000000..5c99d64d --- /dev/null +++ b/frontend/src/app/pages/AgentChat/shell/FeedbackDialog.tsx @@ -0,0 +1,102 @@ +import React, { useState } from 'react'; +import Dialog from '@mui/material/Dialog'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import { useClaudeTokens } from '@/shared/styles/ThemeContext'; +import { report } from '@/shared/serviceClient'; + +export type Sentiment = 'up' | 'down'; + +interface Props { + open: boolean; + sentiment: Sentiment; + sessionId: string; + messageId: string; + onClose: () => void; + onSubmitted: () => void; +} + +const FeedbackDialog: React.FC = ({ open, sentiment, sessionId, messageId, onClose, onSubmitted }) => { + const c = useClaudeTokens(); + const [comment, setComment] = useState(''); + + const isUp = sentiment === 'up'; + + const handleSubmit = () => { + // Rides the same analytics channel as everything else (batches + offline + // spools to the cloud). Fire-and-forget, so the dialog closes instantly. + report('feedback', sentiment, { message_id: messageId, session_id: sessionId, comment: comment.trim() }, { immediate: true }); + setComment(''); + onSubmitted(); + }; + + const handleClose = () => { + setComment(''); + onClose(); + }; + + return ( + + + + {isUp ? 'Give positive feedback' : 'Give negative feedback'} + + + + Please provide details: (optional) + + setComment(e.target.value)} + placeholder={isUp ? 'What was good about this response?' : 'What went wrong with this response?'} + sx={{ + '& .MuiOutlinedInput-root': { + bgcolor: c.bg.page, + color: c.text.primary, + fontSize: '0.9rem', + '& fieldset': { borderColor: c.border.medium }, + '&:hover fieldset': { borderColor: c.border.strong }, + '&.Mui-focused fieldset': { borderColor: c.accent.primary }, + }, + '& textarea::placeholder': { color: c.text.tertiary, opacity: 1 }, + }} + /> + + + Shared with OpenSwarm to help improve your agents. We send your rating and note, not your conversation. + + + + + + + + + ); +}; + +export default FeedbackDialog; diff --git a/frontend/src/app/pages/AgentChat/shell/MessageActionBar.tsx b/frontend/src/app/pages/AgentChat/shell/MessageActionBar.tsx index 0530c4a8..63e3e05d 100644 --- a/frontend/src/app/pages/AgentChat/shell/MessageActionBar.tsx +++ b/frontend/src/app/pages/AgentChat/shell/MessageActionBar.tsx @@ -11,7 +11,12 @@ import ReplayIcon from '@mui/icons-material/Replay'; import CallSplitIcon from '@mui/icons-material/CallSplit'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt'; +import ThumbUpAltIcon from '@mui/icons-material/ThumbUpAlt'; +import ThumbDownOffAltIcon from '@mui/icons-material/ThumbDownOffAlt'; +import ThumbDownAltIcon from '@mui/icons-material/ThumbDownAlt'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; +import FeedbackDialog, { Sentiment } from './FeedbackDialog'; interface BranchNavProps { currentIndex: number; @@ -27,6 +32,8 @@ interface Props { onRegenerate?: () => void; onBranch?: () => void; branchNav?: BranchNavProps; + sessionId?: string; + messageId?: string; } const btnSx = (c: ReturnType) => ({ @@ -43,9 +50,13 @@ const MessageActionBar: React.FC = ({ onRegenerate, onBranch, branchNav, + sessionId, + messageId, }) => { const c = useClaudeTokens(); const [copied, setCopied] = useState(false); + const [dialogSentiment, setDialogSentiment] = useState(null); + const [submitted, setSubmitted] = useState(null); const handleCopy = () => { onCopy(); @@ -54,6 +65,7 @@ const MessageActionBar: React.FC = ({ }; const isUser = role === 'user'; + const canRate = !isUser && !!sessionId && !!messageId; return ( = ({ {copied ? : } + {canRate && ( + <> + + setDialogSentiment('up')} sx={btnSx(c)}> + {submitted === 'up' + ? + : } + + + + setDialogSentiment('down')} sx={btnSx(c)}> + {submitted === 'down' + ? + : } + + + + )} {onRegenerate && ( @@ -146,6 +176,16 @@ const MessageActionBar: React.FC = ({ )} )} + {canRate && dialogSentiment && ( + setDialogSentiment(null)} + onSubmitted={() => { setSubmitted(dialogSentiment); setDialogSentiment(null); }} + /> + )} ); }; @@ -159,4 +199,6 @@ export default React.memo(MessageActionBar, (prev, next) => ( && !!prev.onBranch === !!next.onBranch && prev.branchNav?.currentIndex === next.branchNav?.currentIndex && prev.branchNav?.totalBranches === next.branchNav?.totalBranches + && prev.messageId === next.messageId + && prev.sessionId === next.sessionId ));