From dee3c181f663a560e870d2fd927eaa976ffa9a06 Mon Sep 17 00:00:00 2001 From: Hermann_Kitio Date: Tue, 21 Apr 2026 03:24:17 +0300 Subject: [PATCH] =?UTF-8?q?feat(simulations):=20bouton=20Suggestions=20d'i?= =?UTF-8?q?d=C3=A9es=20+=20modal=20DeepSeek=20(G5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/IdeesSuggestions.tsx | 91 +++++++++++++++++++ .../simulations/components/SimulationForm.tsx | 56 +++++++++++- .../simulations/pages/SimulationPage.tsx | 1 + 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 src/features/simulations/components/IdeesSuggestions.tsx diff --git a/src/features/simulations/components/IdeesSuggestions.tsx b/src/features/simulations/components/IdeesSuggestions.tsx new file mode 100644 index 0000000..dcb68c3 --- /dev/null +++ b/src/features/simulations/components/IdeesSuggestions.tsx @@ -0,0 +1,91 @@ +/** + * Modal — suggestions d'idées DeepSeek (tâche G5). + * + * Présentationnel pur. La fermeture déclenche `onClose` qui doit appeler + * `reset()` du hook useIdees côté parent pour vider le cache de mutation. + * + * Règle H : aucune logique métier. Règle L : tokens Direction H uniquement. + */ + +import { Loader2, Lightbulb } from 'lucide-react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/shared/components/ui/dialog' +import type { ApiError } from '@/shared/types/api' + +function mapIdeesError(err: ApiError | null): string | null { + if (!err) return null + switch (err.code) { + case 'AUTH_REQUIRED': + return 'Votre session a expiré. Reconnectez-vous.' + case 'VALIDATION_ERROR': + case 'INVALID_BODY': + return 'Écrivez au moins 30 mots avant de demander des suggestions.' + default: + return 'Suggestions indisponibles. Réessayez dans quelques instants.' + } +} + +interface Props { + idees: string[] | null + isLoading: boolean + error: ApiError | null + isOpen: boolean + onClose: () => void +} + +export function IdeesSuggestions({ idees, isLoading, error, isOpen, onClose }: Props) { + const message = mapIdeesError(error) + + return ( + { + if (!open) onClose() + }} + > + + + + + + Pour prolonger votre rédaction, inspirez-vous de ces pistes. + + + + {isLoading && ( +
+
+ )} + + {!isLoading && message && ( +
+ {message} +
+ )} + + {!isLoading && !message && idees && idees.length > 0 && ( +
    + {idees.map((idee, i) => ( +
  • +
  • + ))} +
+ )} +
+
+ ) +} diff --git a/src/features/simulations/components/SimulationForm.tsx b/src/features/simulations/components/SimulationForm.tsx index d6fffeb..6fee654 100644 --- a/src/features/simulations/components/SimulationForm.tsx +++ b/src/features/simulations/components/SimulationForm.tsx @@ -11,18 +11,23 @@ */ import { useEffect, useRef, useState, type FormEvent } from 'react' -import { Clock, Loader2, Shuffle } from 'lucide-react' +import { Clock, Lightbulb, Loader2, Shuffle } from 'lucide-react' import { z } from 'zod' import { Button } from '@/shared/components/ui/button' import { formatTache } from '@/entities/production/lib' +import { hasAccess, type Plan } from '@/entities/user/lib' import type { SujetData, Tache } from '@/entities/production/types' import type { ApiError } from '@/shared/types/api' import { countWords, getSimulationConfig } from '../lib/simulationConfig' import { useTimer } from '../hooks/useTimer' +import { useIdees } from '../hooks/useIdees' import { SujetDisplay } from './SujetDisplay' import { SpecialCharsKeyboard } from './SpecialCharsKeyboard' import { TimerDisplay } from './TimerDisplay' import { WordCountBar } from './WordCountBar' +import { IdeesSuggestions } from './IdeesSuggestions' + +const MIN_WORDS_IDEES = 30 const textSchema = z.object({ texte: z @@ -49,6 +54,7 @@ function mapCorrectError(err: ApiError | null): string | null { interface Props { tache: Tache sujet: SujetData | null + plan: Plan isSubmitting: boolean error: ApiError | null onSubmit: (texte: string) => void @@ -59,6 +65,7 @@ interface Props { export function SimulationForm({ tache, sujet, + plan, isSubmitting, error, onSubmit, @@ -69,12 +76,38 @@ export function SimulationForm({ const hasAutoSubmittedRef = useRef(false) const [texte, setTexte] = useState('') const [fieldError, setFieldError] = useState(null) + const [isIdeesOpen, setIsIdeesOpen] = useState(false) const config = getSimulationConfig(tache) const wordCount = countWords(texte) const canSubmit = wordCount >= config.motsMin const timer = useTimer(config.dureeMinutes, !isSubmitting) + const idees = useIdees() + + const tipsAllowed = hasAccess(plan, 'tips') + const ideesDisabled = + isSubmitting || + idees.isLoading || + !sujet || + !tipsAllowed || + wordCount < MIN_WORDS_IDEES + const ideesTitle = !tipsAllowed + ? 'Disponible en Standard' + : wordCount < MIN_WORDS_IDEES + ? `Écrivez au moins ${MIN_WORDS_IDEES} mots` + : undefined + + function handleIdeesClick() { + if (!sujet) return + setIsIdeesOpen(true) + idees.fetchIdees({ consigne: sujet.consigne, contenu: texte }) + } + + function handleIdeesClose() { + setIsIdeesOpen(false) + idees.reset() + } useEffect(() => { const el = textareaRef.current @@ -149,7 +182,18 @@ export function SimulationForm({ {sujet && ( -
+
+