rubis/apps/api/app/controllers/ai_controller.ts
ordinarthur 9e531e32a9 feat(plans): wizard de création de plan custom + génération IA Mistral
Backend
- migration : champs contact_first_name / contact_last_name (nullable)
  sur clients pour personnaliser les variables de relance
- POST /api/v1/plans : création de plan custom avec slug auto-généré
  (suffixé en cas de collision, "nouveau"/"new"/"create" réservés)
- POST /api/v1/ai/generate-relance : génération de subject+body via
  mistral-small-latest, avec brief utilisateur et tonalité ciblée
- mail_dispatcher : nouvelles variables {{daysLate}}, {{issueDate}},
  {{user.fullName}}, {{user.companyName}}, {{client.contactFirstName}},
  {{client.contactLastName}} (helper buildRelanceVars exposé pour preview)
- send_relance_job preload désormais l'organization pour exposer son name

Frontend
- /plans/nouveau : wizard 4 étapes (Identité → Cadence → Messages → Récap)
  - Stepper en haut, navigation guidée, validation par étape
  - Étape 1 : nom + tonalité globale (4 cards Doux/Standard/Ferme/Strict)
    avec aperçu de la cadence par défaut associée
  - Étape 2 : timeline horizontale (rail rubis-glow + nœuds ◆ teintés
    selon la tonalité), édition décalage/ton de l'étape sélectionnée
  - Étape 3 : édition par étape avec preview live à droite, chips de
    variables cliquables, bouton "Générer avec l'IA" qui ouvre une modale
    Mistral (brief + résultat + régénérer)
  - Étape 4 : récap avec preview de chaque email rendu sur un client fictif
- Détection des variables sensibles → warning si X clients existants n'ont
  pas le champ contactFirstName/contactLastName rempli (UX informative,
  fallback vide à l'envoi)
- "Dupliquer" sur chaque card de plan → /plans/nouveau?from=<slug>
  pour pré-remplir le wizard à partir d'un plan existant
- ClientCreateDialog : ajout des champs prénom/nom du contact dédié
- TEMPLATE_VARIABLES étendu, helper renderTemplate côté front en miroir
  exact de l'implémentation API
- MSW handlers ai/plans/clients alignés sur le nouveau contrat

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 22:55:00 +02:00

51 lines
1.8 KiB
TypeScript

import vine from '@vinejs/vine'
import { Exception } from '@adonisjs/core/exceptions'
import type { HttpContext } from '@adonisjs/core/http'
import { generateRelance } from '#services/ai_relance_generator'
const RELANCE_TONES = ['amical', 'courtois', 'ferme', 'mise_en_demeure'] as const
const generateRelanceValidator = vine.create({
tone: vine.enum(RELANCE_TONES),
offsetDays: vine.number().min(-30).max(180),
// Brief libre. On accepte vide : Mistral génère alors un message standard
// pour la tonalité + timing donnés.
prompt: vine.string().maxLength(1000).optional(),
planContext: vine.string().maxLength(500).optional(),
})
/**
* Endpoints IA. V1 : uniquement génération de templates de relance pour le
* wizard de création de plan custom. Mistral est déjà utilisé pour l'OCR
* (cf. mistral_ocr_provider.ts) — on réutilise la même clé API.
*/
export default class AiController {
/**
* POST /ai/generate-relance
*
* Génère subject + body avec des placeholders Mustache prêts à insérer.
* L'utilisateur peut régénérer pour avoir une variante.
*/
async generateRelance({ auth, request, response }: HttpContext) {
auth.getUserOrFail() // auth requise
const payload = await request.validateUsing(generateRelanceValidator)
try {
const result = await generateRelance({
tone: payload.tone,
offsetDays: payload.offsetDays,
prompt: payload.prompt ?? '',
planContext: payload.planContext,
})
return response.json({ data: result })
} catch (err) {
// On wrap pour passer par le handler global et garder le format
// d'erreur uniforme côté front.
throw new Exception(
err instanceof Error ? err.message : 'Génération IA indisponible',
{ status: 502, code: 'ai_generation_failed' }
)
}
}
}