Awesome-omni-skill react-frontend
React components for Chat, Evaluation, Report, Admin with TypeScript, Tailwind, hooks
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/react-frontend-majiayu000" ~/.claude/skills/diegosouzapw-awesome-omni-skill-react-frontend-026537 && rm -rf "$T"
manifest:
skills/development/react-frontend-majiayu000/SKILL.mdsource content
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]); const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault(); if (!inputValue.trim()) return; // Add user message const userMessage: ChatMessage = { id: Date.now().toString(), role: 'user', content: inputValue, timestamp: new Date() }; setMessages(prev => [...prev, userMessage]); setInputValue(''); setIsLoading(true); try { // Stream assistant response let assistantContent = ''; let sources: ChatMessage['sources'] = []; const stream = await chatService.sendMessage({ conversation_id: 'current', // From context content: inputValue }); for await (const chunk of stream) { if (chunk.type === 'content') { assistantContent += chunk.data; // Update last message in real-time setMessages(prev => { const newMessages = [...prev]; if (newMessages[newMessages.length - 1]?.role === 'assistant') { newMessages[newMessages.length - 1].content = assistantContent; } return newMessages; }); } else if (chunk.type === 'sources') { sources = chunk.data; } } // Add assistant message const assistantMessage: ChatMessage = { id: Date.now().toString(), role: 'assistant', content: assistantContent, sources, timestamp: new Date() }; setMessages(prev => [...prev, assistantMessage]); } catch (error) { console.error('Error sending message:', error); // Add error message setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: 'Erreur: Impossible de générer une réponse.', timestamp: new Date() }]); } finally { setIsLoading(false); } }; return ( <div className="flex flex-col h-screen bg-gray-50"> {/* Messages */} <div className="flex-1 overflow-y-auto p-4 space-y-4"> {messages.map(msg => ( <ChatMessage key={msg.id} message={msg} /> ))} {isLoading && <TypingIndicator />} <div ref={messagesEndRef} /> </div> {/* Input */} <div className="border-t bg-white p-4"> <form onSubmit={handleSendMessage} className="flex gap-2"> <input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="Posez votre question..." disabled={isLoading} className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" /> <button type="submit" disabled={isLoading || !inputValue.trim()} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50" > Envoyer </button> </form> </div> </div> ); };
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> = ({ question, onSubmit }) => { const { control, handleSubmit, formState: { errors } } = useForm<QuestionFormData>({ resolver: zodResolver(questionSchema) }); return ( <form onSubmit={handleSubmit(onSubmit)} className="bg-white rounded-lg shadow p-6"> <h3 className="text-lg font-semibold mb-4">{question.text}</h3> <div className="space-y-3 mb-6"> {['oui', 'non', 'partiellement'].map(option => ( <label key={option} className="flex items-center"> <Controller name="answer" control={control} render={({ field }) => ( <input {...field} type="radio" value={option} className="w-4 h-4 text-blue-600" /> )} /> <span className="ml-3 capitalize">{option}</span> </label> ))} </div> {errors.answer && ( <p className="text-red-600 text-sm mb-4">{errors.answer.message}</p> )} <Controller name="comment" control={control} render={({ field }) => ( <textarea {...field} placeholder="Commentaires optionnels..." rows={3} className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" /> )} /> <button type="submit" className="mt-4 w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" > Suivant </button> </form> ); };
Custom Hooks
// hooks/useChat.ts 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
// services/api.ts 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)
// store/authStore.ts 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('token'); set({ token: null, user: null }); } }));
Types
// types/api.ts 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-4gap-2 - Colors:
,bg-blue-600
,text-gray-900border-gray-200 - Layout:
,flex
,gridcontainer 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
prefixuse - 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)