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>
194 lines
5.4 KiB
TypeScript
194 lines
5.4 KiB
TypeScript
'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>
|
|
</>
|
|
);
|
|
}
|