Files
maternal-app/maternal-web/components/features/ai-chat/MessageFeedback.tsx
Andrei e4b97df0c0
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
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>
2025-10-04 11:39:02 +00:00

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>
</>
);
}