feat: Implement AI response feedback UI and complete high-priority features
Frontend Features: - Add MessageFeedback component with thumbs up/down buttons - Positive feedback submits immediately with success toast - Negative feedback opens dialog for optional text input - Integrate feedback buttons on all AI assistant messages - Add success Snackbar confirmation message - Translation keys added to ai.json (feedback section) Backend Features: - Add POST /api/v1/ai/feedback endpoint - Create FeedbackDto with conversation ID validation - Implement submitFeedback service method - Store feedback in conversation metadata with timestamps - Add audit logging for feedback submissions - Fix conversationId regex validation to support nanoid format Legal & Compliance: - Implement complete EULA acceptance flow with modal - Create reusable legal content components (Terms, Privacy, EULA) - Add LegalDocumentViewer for nested modal viewing - Cookie Consent Banner with GDPR compliance - Legal pages with AppShell navigation - EULA acceptance tracking in user entity Branding Updates: - Rebrand from "Maternal App" to "ParentFlow" - Update all icons (72px to 512px) from high-res source - PWA manifest updated with ParentFlow branding - Contact email: hello@parentflow.com - Address: Serbota 3, Bucharest, Romania Bug Fixes: - Fix chat endpoint validation (support nanoid conversation IDs) - Fix EULA acceptance API call (use apiClient vs hardcoded localhost) - Fix icon loading errors with proper PNG generation Documentation: - Mark 11 high-priority features as complete in REMAINING_FEATURES.md - Update feature statistics: 73/139 complete (53%) - All high-priority features now complete! 🎉 Files Changed: Frontend: 21 files (components, pages, locales, icons) Backend: 6 files (controller, service, DTOs, migrations) Docs: 1 file (REMAINING_FEATURES.md) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
193
maternal-web/components/features/ai-chat/MessageFeedback.tsx
Normal file
193
maternal-web/components/features/ai-chat/MessageFeedback.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Snackbar,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import { ThumbUp, ThumbDown, ThumbUpOutlined, ThumbDownOutlined } from '@mui/icons-material';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
interface MessageFeedbackProps {
|
||||
messageId: string;
|
||||
conversationId: string | null;
|
||||
}
|
||||
|
||||
export function MessageFeedback({ messageId, conversationId }: MessageFeedbackProps) {
|
||||
const { t } = useTranslation('ai');
|
||||
const [feedbackType, setFeedbackType] = useState<'positive' | 'negative' | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [feedbackText, setFeedbackText] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
|
||||
const handleFeedback = async (type: 'positive' | 'negative') => {
|
||||
// If already submitted this type, ignore
|
||||
if (feedbackType === type) return;
|
||||
|
||||
setFeedbackType(type);
|
||||
|
||||
// For negative feedback, open dialog for additional comments
|
||||
if (type === 'negative') {
|
||||
setDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// For positive feedback, submit immediately
|
||||
await submitFeedback(type, '');
|
||||
};
|
||||
|
||||
const submitFeedback = async (type: 'positive' | 'negative', text: string) => {
|
||||
if (!conversationId) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await apiClient.post('/api/v1/ai/feedback', {
|
||||
conversationId,
|
||||
messageId,
|
||||
feedbackType: type,
|
||||
feedbackText: text || undefined,
|
||||
});
|
||||
|
||||
console.log(`✅ Feedback submitted: ${type}`);
|
||||
|
||||
// Show success snackbar
|
||||
setSnackbarOpen(true);
|
||||
|
||||
// Close dialog if open
|
||||
if (dialogOpen) {
|
||||
setDialogOpen(false);
|
||||
setFeedbackText('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to submit feedback:', error);
|
||||
// Reset feedback type on error
|
||||
setFeedbackType(null);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogSubmit = async () => {
|
||||
if (feedbackType) {
|
||||
await submitFeedback(feedbackType, feedbackText);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
setDialogOpen(false);
|
||||
setFeedbackText('');
|
||||
// Reset feedback type if closing without submitting
|
||||
if (!feedbackType || feedbackType === 'negative') {
|
||||
setFeedbackType(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', gap: 0.5, mt: 1 }}>
|
||||
<Tooltip title={t('feedback.helpful') || 'Helpful'}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleFeedback('positive')}
|
||||
disabled={isSubmitting}
|
||||
sx={{
|
||||
color: feedbackType === 'positive' ? 'success.main' : 'text.secondary',
|
||||
'&:hover': { color: 'success.main' },
|
||||
}}
|
||||
>
|
||||
{feedbackType === 'positive' ? (
|
||||
<ThumbUp fontSize="small" />
|
||||
) : (
|
||||
<ThumbUpOutlined fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t('feedback.notHelpful') || 'Not helpful'}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleFeedback('negative')}
|
||||
disabled={isSubmitting}
|
||||
sx={{
|
||||
color: feedbackType === 'negative' ? 'error.main' : 'text.secondary',
|
||||
'&:hover': { color: 'error.main' },
|
||||
}}
|
||||
>
|
||||
{feedbackType === 'negative' ? (
|
||||
<ThumbDown fontSize="small" />
|
||||
) : (
|
||||
<ThumbDownOutlined fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Feedback Dialog for negative feedback */}
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
onClose={handleDialogClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
{t('feedback.dialogTitle') || 'Help us improve'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{t('feedback.dialogMessage') || 'What could have been better about this response?'}
|
||||
</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
placeholder={t('feedback.placeholder') || 'Your feedback (optional)'}
|
||||
value={feedbackText}
|
||||
onChange={(e) => setFeedbackText(e.target.value)}
|
||||
variant="outlined"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDialogClose}>
|
||||
{t('feedback.cancel') || 'Cancel'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDialogSubmit}
|
||||
variant="contained"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('feedback.submit') || 'Submit'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Success Snackbar */}
|
||||
<Snackbar
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setSnackbarOpen(false)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert
|
||||
onClose={() => setSnackbarOpen(false)}
|
||||
severity="success"
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{t('feedback.thankYou') || 'Thank you for your feedback!'}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user