| name | react-frontend |
| description | React components for Chat, Evaluation, Report, Admin with TypeScript, Tailwind, hooks |
React Frontend — CEI-001
Project Structure
src/
├── components/
│ ├── chat/
│ │ ├── ChatWindow.tsx
│ │ ├── ChatMessage.tsx
│ │ ├── ChatInput.tsx
│ │ └── useChat.ts (custom hook)
│ ├── evaluation/
│ │ ├── EvaluationWizard.tsx
│ │ ├── QuestionCard.tsx
│ │ ├── ProgressBar.tsx
│ │ └── useEvaluation.ts
│ ├── report/
│ │ ├── ReportDashboard.tsx
│ │ ├── ScoreCard.tsx
│ │ ├── RadarChart.tsx
│ │ └── useReport.ts
│ ├── admin/
│ │ ├── DocumentList.tsx
│ │ ├── DocumentUpload.tsx
│ │ ├── PipelineStatus.tsx
│ │ └── useDocuments.ts
│ └── shared/
│ ├── Navbar.tsx
│ ├── Footer.tsx
│ ├── Button.tsx
│ └── Card.tsx
├── pages/
│ ├── HomePage.tsx
│ ├── ChatPage.tsx
│ ├── EvaluationPage.tsx
│ ├── ReportPage.tsx
│ ├── AdminPage.tsx
│ └── LoginPage.tsx
├── services/
│ ├── api.ts (axios client with auth)
│ ├── chatService.ts
│ ├── evaluationService.ts
│ ├── reportService.ts
│ └── adminService.ts
├── hooks/
│ ├── useAuth.ts
│ ├── useAPI.ts
│ └── useLocalStorage.ts
├── store/
│ ├── authStore.ts (Zustand)
│ ├── evaluationStore.ts
│ └── chatStore.ts
├── types/
│ ├── api.ts
│ ├── evaluation.ts
│ ├── chat.ts
│ └── admin.ts
├── styles/
│ └── tailwind.css
└── App.tsx
Component Pattern with TypeScript
Basic Component
import React, { FC } from 'react';
interface EvaluationHeaderProps {
title: string;
progress: number;
onBack: () => void;
}
export const EvaluationHeader: FC<EvaluationHeaderProps> = ({
title,
progress,
onBack
}) => {
return (
<div className="bg-white border-b">
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
<button
onClick={onBack}
className="text-gray-600 hover:text-gray-900"
>
← Retour
</button>
<div className="flex-1 mx-4">
<h1 className="text-2xl font-bold text-gray-900">{title}</h1>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
</div>
</div>
</div>
);
};
Chat Component with Streaming
import React, { useState, useRef, useEffect } from 'react';
import { chatService } from '@/services/chatService';
interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
sources?: Array<{ title: string; excerpt: string }>;
timestamp: Date;
}
export const ChatWindow: FC = () => {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
= () => {
e.();
(!inputValue.()) ;
: = {
: .().(),
: ,
: inputValue,
: ()
};
( [...prev, userMessage]);
();
();
{
assistantContent = ;
: [] = [];
stream = chatService.({
: ,
: inputValue
});
( chunk stream) {
(chunk. === ) {
assistantContent += chunk.;
( {
newMessages = [...prev];
(newMessages[newMessages. - ]?. === ) {
newMessages[newMessages. - ]. = assistantContent;
}
newMessages;
});
} (chunk. === ) {
sources = chunk.;
}
}
: = {
: .().(),
: ,
: assistantContent,
sources,
: ()
};
( [...prev, assistantMessage]);
} (error) {
.(, error);
( [...prev, {
: .().(),
: ,
: ,
: ()
}]);
} {
();
}
};
(
);
};
Form with Validation (Evaluation)
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const questionSchema = z.object({
answer: z.enum(['oui', 'non', 'partiellement']),
comment: z.string().optional()
});
type QuestionFormData = z.infer<typeof questionSchema>;
interface QuestionCardProps {
question: {
id: string;
text: string;
category: string;
type: 'yesno' | 'scale' | 'multiple';
};
onSubmit: (answer: QuestionFormData) => void;
}
export const QuestionCard: FC<QuestionCardProps> = () => {
{ control, handleSubmit, : { errors } } = useForm<>({
: (questionSchema)
});
(
);
};
Custom Hooks
import { useState, useCallback } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { chatService } from '@/services/chatService';
export const useChat = (conversationId: string) => {
const [messages, setMessages] = useState([]);
const { data: history, isLoading } = useQuery({
queryKey: ['chat', conversationId],
queryFn: () => chatService.getHistory(conversationId)
});
const sendMutation = useMutation({
mutationFn: (content: string) =>
chatService.sendMessage({ conversation_id: conversationId, content }),
onSuccess: (response) => {
setMessages(prev => [...prev, response]);
}
});
return {
messages: history || [],
isLoading,
sendMessage: sendMutation.mutate,
isLoading: sendMutation.isPending
};
};
Auth & API Client
import axios from 'axios';
import { useAuthStore } from '@/store/authStore';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000'
});
api.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
useAuthStore.getState().logout();
}
return Promise.reject(error);
}
);
export default api;
State Management (Zustand)
import { create } from 'zustand';
interface AuthStore {
user: any | null;
token: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
export const useAuthStore = create<AuthStore>((set) => ({
user: null,
token: localStorage.getItem('token'),
login: async (email, password) => {
const response = await api.post('/auth/login', { email, password });
const { access_token, user } = response.data;
localStorage.setItem('token', access_token);
set({ token: access_token, user });
},
logout: () => {
localStorage.removeItem();
({ : , : });
}
}));
Types
export interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
sources?: Array<{ title: string; excerpt: string }>;
timestamp: string;
}
export interface Evaluation {
id: string;
userId: string;
status: 'in_progress' | 'completed';
answers: Record<string, string>;
score: number;
createdAt: string;
completedAt?: string;
}
export interface EvaluationQuestion {
id: string;
moduleId: string;
text: string;
type: 'yesno' | 'scale' | 'multiple';
weight: number;
}
Tailwind Classes
- Spacing:
px-4, py-2, mx-4, gap-2
- Colors:
bg-blue-600, text-gray-900, border-gray-200
- Layout:
flex, grid, container mx-auto
- Responsive:
md:, lg:
- States:
hover:, focus:, disabled:, dark:
Conventions
- Files: PascalCase for components, camelCase for hooks/services
- Props interfaces end with
Props
- Type everything (no
any)
- Hooks use
use prefix
- Components use FC type or function syntax
- Import order: react, third-party, local
- Error boundaries for error handling
- Accessible: alt text, labels, semantic HTML
- No hardcoded strings (use constants/i18n)