mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-31 04:09:44 +02:00
[eric] chat: add thumbs up/down feedback with a why-modal routed to cloud analytics
This commit is contained in:
@@ -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<Props> = ({ 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 (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
PaperProps={{ sx: { bgcolor: c.bg.elevated, borderRadius: 3, p: 1, minWidth: 420, maxWidth: 480 } }}
|
||||
>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography sx={{ color: c.text.primary, fontSize: '1.15rem', fontWeight: 600, mb: 2 }}>
|
||||
{isUp ? 'Give positive feedback' : 'Give negative feedback'}
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ color: c.text.secondary, fontSize: '0.85rem', mb: 1 }}>
|
||||
Please provide details: (optional)
|
||||
</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={3}
|
||||
value={comment}
|
||||
onChange={(e) => 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 },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography sx={{ color: c.text.tertiary, fontSize: '0.75rem', fontStyle: 'italic', mt: 1.5 }}>
|
||||
Shared with OpenSwarm to help improve your agents. We send your rating and note, not your conversation.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 2 }}>
|
||||
<Button onClick={handleClose} sx={{ color: c.text.secondary, textTransform: 'none' }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="contained"
|
||||
sx={{
|
||||
bgcolor: c.text.primary,
|
||||
color: c.bg.page,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
'&:hover': { bgcolor: c.text.secondary },
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackDialog;
|
||||
@@ -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<typeof useClaudeTokens>) => ({
|
||||
@@ -43,9 +50,13 @@ const MessageActionBar: React.FC<Props> = ({
|
||||
onRegenerate,
|
||||
onBranch,
|
||||
branchNav,
|
||||
sessionId,
|
||||
messageId,
|
||||
}) => {
|
||||
const c = useClaudeTokens();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [dialogSentiment, setDialogSentiment] = useState<Sentiment | null>(null);
|
||||
const [submitted, setSubmitted] = useState<Sentiment | null>(null);
|
||||
|
||||
const handleCopy = () => {
|
||||
onCopy();
|
||||
@@ -54,6 +65,7 @@ const MessageActionBar: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const isUser = role === 'user';
|
||||
const canRate = !isUser && !!sessionId && !!messageId;
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -130,6 +142,24 @@ const MessageActionBar: React.FC<Props> = ({
|
||||
{copied ? <CheckIcon sx={{ fontSize: 16 }} /> : <ContentCopyIcon sx={{ fontSize: 16 }} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{canRate && (
|
||||
<>
|
||||
<Tooltip title="Give positive feedback" arrow>
|
||||
<IconButton size="small" onClick={() => setDialogSentiment('up')} sx={btnSx(c)}>
|
||||
{submitted === 'up'
|
||||
? <ThumbUpAltIcon sx={{ fontSize: 16, color: c.accent.primary }} />
|
||||
: <ThumbUpOffAltIcon sx={{ fontSize: 16 }} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Give negative feedback" arrow>
|
||||
<IconButton size="small" onClick={() => setDialogSentiment('down')} sx={btnSx(c)}>
|
||||
{submitted === 'down'
|
||||
? <ThumbDownAltIcon sx={{ fontSize: 16, color: c.accent.primary }} />
|
||||
: <ThumbDownOffAltIcon sx={{ fontSize: 16 }} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
{onRegenerate && (
|
||||
<Tooltip title="Regenerate" arrow>
|
||||
<IconButton size="small" onClick={onRegenerate} sx={btnSx(c)}>
|
||||
@@ -146,6 +176,16 @@ const MessageActionBar: React.FC<Props> = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{canRate && dialogSentiment && (
|
||||
<FeedbackDialog
|
||||
open
|
||||
sentiment={dialogSentiment}
|
||||
sessionId={sessionId!}
|
||||
messageId={messageId!}
|
||||
onClose={() => setDialogSentiment(null)}
|
||||
onSubmitted={() => { setSubmitted(dialogSentiment); setDialogSentiment(null); }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user