feat: Implement AI response feedback UI and complete high-priority features
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

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:
2025-10-04 11:39:02 +00:00
parent d5a8bad6d9
commit e4b97df0c0
44 changed files with 3185 additions and 188 deletions

View File

@@ -89,6 +89,17 @@ export class User {
@Column({ name: 'parental_email', length: 255, nullable: true })
parentalEmail?: string | null;
// EULA/Legal acceptance tracking
@Column({
name: 'eula_accepted_at',
type: 'timestamp without time zone',
nullable: true,
})
eulaAcceptedAt?: Date | null;
@Column({ name: 'eula_version', length: 20, nullable: true })
eulaVersion?: string | null;
@Column({ type: 'jsonb', nullable: true })
preferences?: {
notifications?: boolean;

View File

@@ -0,0 +1,20 @@
-- Migration V008: Add EULA acceptance tracking to users table
-- Created: 2025-10-04
-- Description: Adds eulaAcceptedAt and eulaVersion fields to track when users accept legal agreements
-- Add EULA acceptance timestamp
ALTER TABLE users
ADD COLUMN eula_accepted_at TIMESTAMP WITHOUT TIME ZONE NULL;
-- Add EULA version tracking (e.g., "1.0", "2024-10-04")
ALTER TABLE users
ADD COLUMN eula_version VARCHAR(20) NULL;
-- Add comment for documentation
COMMENT ON COLUMN users.eula_accepted_at IS 'Timestamp when user accepted the EULA';
COMMENT ON COLUMN users.eula_version IS 'Version of EULA that was accepted (e.g., "1.0", "2024-10-04")';
-- Create index for quickly finding users who haven't accepted EULA
CREATE INDEX idx_users_eula_acceptance ON users(eula_accepted_at) WHERE eula_accepted_at IS NULL;
COMMENT ON INDEX idx_users_eula_acceptance IS 'Find users who have not yet accepted the EULA (partial index)';

View File

@@ -13,6 +13,7 @@ import {
import { Response } from 'express';
import { AIService } from './ai.service';
import { ChatMessageDto } from './dto/chat-message.dto';
import { FeedbackDto } from './dto/feedback.dto';
import { Public } from '../auth/decorators/public.decorator';
@Controller('api/v1/ai')
@@ -148,6 +149,23 @@ export class AIController {
};
}
@Public() // Public for testing
@Post('feedback')
async submitFeedback(@Req() req: any, @Body() feedbackDto: FeedbackDto) {
const userId = req.user?.userId || 'test_user_123';
await this.aiService.submitFeedback(
userId,
feedbackDto.conversationId,
feedbackDto.messageId,
feedbackDto.feedbackType,
feedbackDto.feedbackText,
);
return {
success: true,
message: 'Feedback submitted successfully',
};
}
// Embeddings testing endpoints
@Public() // Public for testing
@Post('test/embeddings/generate')

View File

@@ -927,6 +927,63 @@ export class AIService {
return trimmed;
}
/**
* Submit feedback for an AI message
*/
async submitFeedback(
userId: string,
conversationId: string,
messageId: string,
feedbackType: 'positive' | 'negative',
feedbackText?: string,
): Promise<void> {
// Validate conversation belongs to user
const conversation = await this.conversationRepository.findOne({
where: { id: conversationId, userId },
});
if (!conversation) {
throw new BadRequestException('Conversation not found');
}
// Initialize feedback array in metadata if it doesn't exist
if (!conversation.metadata.feedback) {
conversation.metadata.feedback = [];
}
// Add feedback entry
const feedbackEntry = {
messageId,
feedbackType,
feedbackText: feedbackText || null,
timestamp: new Date().toISOString(),
};
conversation.metadata.feedback.push(feedbackEntry);
// Save conversation
await this.conversationRepository.save(conversation);
// Audit log
await this.auditService.log({
userId,
action: 'CREATE' as any,
entityType: 'FEEDBACK' as any,
entityId: conversationId,
changes: {
after: {
messageId,
feedbackType,
hasFeedbackText: !!feedbackText,
},
},
});
this.logger.log(
`Feedback submitted: ${feedbackType} for message ${messageId} in conversation ${conversationId}`,
);
}
/**
* Get current AI provider status
*/

View File

@@ -11,7 +11,7 @@ export class ChatMessageDto {
@IsOptional()
@IsString()
@Matches(/^conv_[a-z0-9]{16}$/, {
@Matches(/^conv_[a-zA-Z0-9_-]{8,}$/, {
message: 'Invalid conversation ID format',
})
conversationId?: string;

View File

@@ -0,0 +1,27 @@
import { IsString, IsOptional, IsIn, IsNotEmpty, MaxLength, Matches } from 'class-validator';
import { Transform } from 'class-transformer';
export class FeedbackDto {
@IsString()
@IsNotEmpty()
@Matches(/^conv_[a-z0-9A-Z_-]{8,}$/, {
message: 'Invalid conversation ID format',
})
conversationId: string;
@IsString()
@IsNotEmpty()
messageId: string;
@IsString()
@IsIn(['positive', 'negative'], {
message: 'Feedback type must be either "positive" or "negative"',
})
feedbackType: 'positive' | 'negative';
@IsOptional()
@IsString()
@MaxLength(1000, { message: 'Feedback text cannot exceed 1000 characters' })
@Transform(({ value }) => value?.trim())
feedbackText?: string;
}

View File

@@ -480,4 +480,18 @@ export class AuthController {
hasCredentials,
};
}
// EULA Acceptance Endpoint
@UseGuards(JwtAuthGuard)
@Post('eula/accept')
@HttpCode(HttpStatus.OK)
async acceptEULA(
@CurrentUser() user: any,
@Body() body: { version?: string },
) {
return await this.authService.acceptEULA(
user.userId,
body.version || '2025-10-04',
);
}
}

View File

@@ -371,6 +371,8 @@ export class AuthService {
emailVerified: user.emailVerified,
preferences: user.preferences,
families,
eulaAcceptedAt: user.eulaAcceptedAt,
eulaVersion: user.eulaVersion,
},
};
}
@@ -596,4 +598,43 @@ export class AuthService {
return age;
}
/**
* Accept EULA for user
*/
async acceptEULA(userId: string, version: string = '2025-10-04'): Promise<any> {
const user = await this.userRepository.findOne({
where: { id: userId },
});
if (!user) {
throw new UnauthorizedException('User not found');
}
// Update EULA acceptance
user.eulaAcceptedAt = new Date();
user.eulaVersion = version;
await this.userRepository.save(user);
// Audit log for EULA acceptance
await this.auditService.log({
userId: user.id,
action: AuditAction.CONSENT_GRANTED,
entityType: EntityType.USER,
entityId: user.id,
changes: {
after: { eulaAcceptedAt: user.eulaAcceptedAt, eulaVersion: version },
},
});
this.logger.log(`User ${userId} accepted EULA version ${version}`);
return {
success: true,
message: 'EULA accepted successfully',
eulaAcceptedAt: user.eulaAcceptedAt,
eulaVersion: user.eulaVersion,
};
}
}