fix: T2 prompt calibration (25 words max) + JSONB parse guard (500 on getById)
This commit is contained in:
parent
452255d77f
commit
8863520a2e
3 changed files with 238 additions and 197 deletions
|
|
@ -1,106 +1,117 @@
|
|||
import { supabase } from '../lib/supabase.js'
|
||||
import { canUserSimulate } from '../lib/access.js'
|
||||
import { supabase } from "../lib/supabase.js";
|
||||
import { canUserSimulate } from "../lib/access.js";
|
||||
import type {
|
||||
CorrectionRapport,
|
||||
ProductionModele,
|
||||
ExerciceItem,
|
||||
} from '../lib/deepseek.js'
|
||||
import type { AuthProfile } from '../middleware/auth.js'
|
||||
} from "../lib/deepseek.js";
|
||||
import type { AuthProfile } from "../middleware/auth.js";
|
||||
|
||||
export type JobStatus = 'pending' | 'ready' | 'error'
|
||||
export type JobStatus = "pending" | "ready" | "error";
|
||||
|
||||
export type Tache = 'EE_T1' | 'EE_T2' | 'EE_T3' | 'EO_T1' | 'EO_T3' | 'EO_T2_LIVE'
|
||||
export type Mode = 'entrainement' | 'examen'
|
||||
export type Tache =
|
||||
| "EE_T1"
|
||||
| "EE_T2"
|
||||
| "EE_T3"
|
||||
| "EO_T1"
|
||||
| "EO_T3"
|
||||
| "EO_T2_LIVE";
|
||||
export type Mode = "entrainement" | "examen";
|
||||
|
||||
export interface CreateBody {
|
||||
tache: Tache
|
||||
mode: Mode
|
||||
contenu?: string
|
||||
tache: Tache;
|
||||
mode: Mode;
|
||||
contenu?: string;
|
||||
}
|
||||
|
||||
export interface SujetData {
|
||||
id: string
|
||||
consigne: string
|
||||
role: string | null
|
||||
contexte: string | null
|
||||
doc1_titre: string | null
|
||||
doc1_texte: string | null
|
||||
doc2_titre: string | null
|
||||
doc2_texte: string | null
|
||||
id: string;
|
||||
consigne: string;
|
||||
role: string | null;
|
||||
contexte: string | null;
|
||||
doc1_titre: string | null;
|
||||
doc1_texte: string | null;
|
||||
doc2_titre: string | null;
|
||||
doc2_texte: string | null;
|
||||
}
|
||||
|
||||
export interface CreateResult {
|
||||
id: string
|
||||
tache: Tache
|
||||
mode: Mode
|
||||
created_at: string
|
||||
sujet: SujetData | null
|
||||
id: string;
|
||||
tache: Tache;
|
||||
mode: Mode;
|
||||
created_at: string;
|
||||
sujet: SujetData | null;
|
||||
}
|
||||
|
||||
type CreateError = {
|
||||
error: true
|
||||
code: string
|
||||
message: string
|
||||
status: number
|
||||
}
|
||||
error: true;
|
||||
code: string;
|
||||
message: string;
|
||||
status: number;
|
||||
};
|
||||
|
||||
// Mappe une Tache frontend vers les filtres de la table sujets.
|
||||
// Retourne null pour EO_T2_LIVE (interaction live, pas de sujet pré-défini).
|
||||
function mapTacheToSujetParams(
|
||||
tache: Tache
|
||||
): { mode: 'EE' | 'EO'; tacheNumber: number } | null {
|
||||
tache: Tache,
|
||||
): { mode: "EE" | "EO"; tacheNumber: number } | null {
|
||||
switch (tache) {
|
||||
case 'EE_T1':
|
||||
return { mode: 'EE', tacheNumber: 1 }
|
||||
case 'EE_T2':
|
||||
return { mode: 'EE', tacheNumber: 2 }
|
||||
case 'EE_T3':
|
||||
return { mode: 'EE', tacheNumber: 3 }
|
||||
case 'EO_T1':
|
||||
return { mode: 'EO', tacheNumber: 1 }
|
||||
case 'EO_T3':
|
||||
return { mode: 'EO', tacheNumber: 3 }
|
||||
case 'EO_T2_LIVE':
|
||||
return null
|
||||
case "EE_T1":
|
||||
return { mode: "EE", tacheNumber: 1 };
|
||||
case "EE_T2":
|
||||
return { mode: "EE", tacheNumber: 2 };
|
||||
case "EE_T3":
|
||||
return { mode: "EE", tacheNumber: 3 };
|
||||
case "EO_T1":
|
||||
return { mode: "EO", tacheNumber: 1 };
|
||||
case "EO_T3":
|
||||
return { mode: "EO", tacheNumber: 3 };
|
||||
case "EO_T2_LIVE":
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(
|
||||
body: CreateBody,
|
||||
profile: AuthProfile
|
||||
profile: AuthProfile,
|
||||
): Promise<{ data: CreateResult } | CreateError> {
|
||||
// 1. Vérifier le quota via canUserSimulate (lib/access.ts)
|
||||
const check = canUserSimulate({ plan: profile.plan, simulations_used: profile.simulations_used })
|
||||
const check = canUserSimulate({
|
||||
plan: profile.plan,
|
||||
simulations_used: profile.simulations_used,
|
||||
});
|
||||
if (!check.allowed) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'QUOTA_REACHED',
|
||||
code: "QUOTA_REACHED",
|
||||
message:
|
||||
'Vous avez utilisé vos 5 simulations gratuites. Passez en Standard pour continuer votre préparation.',
|
||||
"Vous avez utilisé vos 5 simulations gratuites. Passez en Standard pour continuer votre préparation.",
|
||||
status: 403,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Fetch un sujet aléatoire AVANT l'insert pour persister sujet_id en une seule requête.
|
||||
// (non bloquant — sujet: null si introuvable).
|
||||
const sujetParams = mapTacheToSujetParams(body.tache)
|
||||
let sujet: SujetData | null = null
|
||||
const sujetParams = mapTacheToSujetParams(body.tache);
|
||||
let sujet: SujetData | null = null;
|
||||
if (sujetParams) {
|
||||
const { data: sujets, error: sujetError } = await supabase
|
||||
.from('sujets')
|
||||
.select('id, consigne, role, contexte, doc1_titre, doc1_texte, doc2_titre, doc2_texte')
|
||||
.eq('mode', sujetParams.mode)
|
||||
.eq('tache', sujetParams.tacheNumber)
|
||||
.eq('actif', true)
|
||||
.from("sujets")
|
||||
.select(
|
||||
"id, consigne, role, contexte, doc1_titre, doc1_texte, doc2_titre, doc2_texte",
|
||||
)
|
||||
.eq("mode", sujetParams.mode)
|
||||
.eq("tache", sujetParams.tacheNumber)
|
||||
.eq("actif", true);
|
||||
|
||||
if (!sujetError && sujets && sujets.length > 0) {
|
||||
sujet = sujets[Math.floor(Math.random() * sujets.length)] as SujetData
|
||||
sujet = sujets[Math.floor(Math.random() * sujets.length)] as SujetData;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Insérer dans productions avec sujet_id (FTD-21 — persistance pour resume).
|
||||
const { data, error } = await supabase
|
||||
.from('productions')
|
||||
.from("productions")
|
||||
.insert({
|
||||
user_id: profile.id,
|
||||
tache: body.tache,
|
||||
|
|
@ -108,16 +119,17 @@ export async function create(
|
|||
contenu: body.contenu ?? null,
|
||||
sujet_id: sujet?.id ?? null,
|
||||
})
|
||||
.select('id, tache, mode, created_at')
|
||||
.single()
|
||||
.select("id, tache, mode, created_at")
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Une erreur est survenue. Veuillez réessayer dans quelques instants.',
|
||||
code: "INTERNAL_ERROR",
|
||||
message:
|
||||
"Une erreur est survenue. Veuillez réessayer dans quelques instants.",
|
||||
status: 500,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -128,7 +140,7 @@ export async function create(
|
|||
created_at: data.created_at,
|
||||
sujet,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Sprint 3.7 — liste paginée des productions de l'utilisateur connecté.
|
||||
|
|
@ -136,52 +148,54 @@ export async function create(
|
|||
// rapport, exercices, modele — trop lourds).
|
||||
|
||||
export interface ListOptions {
|
||||
page: number
|
||||
limit: number
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface ListItem {
|
||||
id: string
|
||||
tache: Tache
|
||||
mode: Mode
|
||||
score: number | null
|
||||
nclc: number | null
|
||||
nclc_cible: 9 | 10 | null
|
||||
created_at: string
|
||||
id: string;
|
||||
tache: Tache;
|
||||
mode: Mode;
|
||||
score: number | null;
|
||||
nclc: number | null;
|
||||
nclc_cible: 9 | 10 | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ListResult {
|
||||
data: ListItem[]
|
||||
data: ListItem[];
|
||||
pagination: {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
}
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
type ListError = ControllerError
|
||||
type ListError = ControllerError;
|
||||
|
||||
export async function list(
|
||||
options: ListOptions,
|
||||
profile: AuthProfile,
|
||||
): Promise<{ data: ListResult } | ListError> {
|
||||
const { page, limit } = options
|
||||
const offset = (page - 1) * limit
|
||||
const { page, limit } = options;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { data, error, count } = await supabase
|
||||
.from('productions')
|
||||
.select('id, tache, mode, score, nclc, nclc_cible, created_at', { count: 'exact' })
|
||||
.eq('user_id', profile.id)
|
||||
.order('created_at', { ascending: false })
|
||||
.range(offset, offset + limit - 1)
|
||||
.from("productions")
|
||||
.select("id, tache, mode, score, nclc, nclc_cible, created_at", {
|
||||
count: "exact",
|
||||
})
|
||||
.eq("user_id", profile.id)
|
||||
.order("created_at", { ascending: false })
|
||||
.range(offset, offset + limit - 1);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Impossible de charger les simulations.',
|
||||
code: "INTERNAL_ERROR",
|
||||
message: "Impossible de charger les simulations.",
|
||||
status: 500,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const items: ListItem[] = (data ?? []).map((row) => ({
|
||||
|
|
@ -191,16 +205,18 @@ export async function list(
|
|||
score: (row.score as number | null) ?? null,
|
||||
nclc: (row.nclc as number | null) ?? null,
|
||||
nclc_cible:
|
||||
row.nclc_cible === 9 || row.nclc_cible === 10 ? (row.nclc_cible as 9 | 10) : null,
|
||||
row.nclc_cible === 9 || row.nclc_cible === 10
|
||||
? (row.nclc_cible as 9 | 10)
|
||||
: null,
|
||||
created_at: row.created_at as string,
|
||||
}))
|
||||
}));
|
||||
|
||||
return {
|
||||
data: {
|
||||
data: items,
|
||||
pagination: { page, limit, total: count ?? 0 },
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Sprint 3.6a — structure enrichie (revelation, diagnostic, conseil_nclc,
|
||||
|
|
@ -211,80 +227,98 @@ export async function list(
|
|||
// - rapport !== null → RapportPage affiche la correction
|
||||
// - rapport === null → SimulationFlowProvider restaure la session (resume)
|
||||
export interface GetByIdResult {
|
||||
simulation_id: string
|
||||
tache: Tache
|
||||
mode: Mode
|
||||
created_at: string
|
||||
contenu: string | null
|
||||
sujet: SujetData | null
|
||||
rapport: CorrectionRapport | null
|
||||
nclc_cible: 9 | 10 | null
|
||||
exercices: ExerciceItem[] | null
|
||||
exercices_status: JobStatus
|
||||
modele: ProductionModele | null
|
||||
modele_status: JobStatus
|
||||
simulation_id: string;
|
||||
tache: Tache;
|
||||
mode: Mode;
|
||||
created_at: string;
|
||||
contenu: string | null;
|
||||
sujet: SujetData | null;
|
||||
rapport: CorrectionRapport | null;
|
||||
nclc_cible: 9 | 10 | null;
|
||||
exercices: ExerciceItem[] | null;
|
||||
exercices_status: JobStatus;
|
||||
modele: ProductionModele | null;
|
||||
modele_status: JobStatus;
|
||||
}
|
||||
|
||||
type ControllerError = {
|
||||
error: true
|
||||
code: string
|
||||
message: string
|
||||
status: number
|
||||
}
|
||||
error: true;
|
||||
code: string;
|
||||
message: string;
|
||||
status: number;
|
||||
};
|
||||
|
||||
export async function getById(
|
||||
id: string,
|
||||
profile: AuthProfile
|
||||
profile: AuthProfile,
|
||||
): Promise<{ data: GetByIdResult } | ControllerError> {
|
||||
const { data, error } = await supabase
|
||||
.from('productions')
|
||||
.from("productions")
|
||||
.select(
|
||||
'id, user_id, tache, mode, contenu, sujet_id, rapport, created_at, nclc_cible, exercices, exercices_status, modele, modele_status',
|
||||
"id, user_id, tache, mode, contenu, sujet_id, rapport, created_at, nclc_cible, exercices, exercices_status, modele, modele_status",
|
||||
)
|
||||
.eq('id', id)
|
||||
.single()
|
||||
.eq("id", id)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'SIMULATION_NOT_FOUND',
|
||||
message: 'Simulation introuvable.',
|
||||
code: "SIMULATION_NOT_FOUND",
|
||||
message: "Simulation introuvable.",
|
||||
status: 404,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (data.user_id !== profile.id) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'AUTH_REQUIRED',
|
||||
message: 'Cette simulation ne vous appartient pas.',
|
||||
code: "AUTH_REQUIRED",
|
||||
message: "Cette simulation ne vous appartient pas.",
|
||||
status: 401,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Charger le sujet si présent (FTD-21 — restore complet de la session).
|
||||
let sujet: SujetData | null = null
|
||||
let sujet: SujetData | null = null;
|
||||
if (data.sujet_id) {
|
||||
const { data: sujetRow } = await supabase
|
||||
.from('sujets')
|
||||
.select('id, consigne, role, contexte, doc1_titre, doc1_texte, doc2_titre, doc2_texte')
|
||||
.eq('id', data.sujet_id)
|
||||
.single()
|
||||
if (sujetRow) sujet = sujetRow as SujetData
|
||||
.from("sujets")
|
||||
.select(
|
||||
"id, consigne, role, contexte, doc1_titre, doc1_texte, doc2_titre, doc2_texte",
|
||||
)
|
||||
.eq("id", data.sujet_id)
|
||||
.single();
|
||||
if (sujetRow) sujet = sujetRow as SujetData;
|
||||
}
|
||||
|
||||
const rapport = data.rapport ? (JSON.parse(data.rapport) as CorrectionRapport) : null
|
||||
// Garde JSONB : supabase-js retourne les colonnes JSONB déjà parsées (objet)
|
||||
// mais on tolère le cas string au cas où le payload aurait été ré-encodé
|
||||
// (ex. cache, réponse stockée en TEXT par migration manuelle).
|
||||
const parseJsonb = <T>(field: unknown): T | null => {
|
||||
if (field === null || field === undefined) return null;
|
||||
if (typeof field === "string") {
|
||||
try {
|
||||
return JSON.parse(field) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return field as T;
|
||||
};
|
||||
|
||||
// JSONB columns reviennent déjà parsées par supabase-js.
|
||||
const exercices = Array.isArray(data.exercices) ? (data.exercices as ExerciceItem[]) : null
|
||||
const rapport = parseJsonb<CorrectionRapport>(data.rapport);
|
||||
const exercicesParsed = parseJsonb<ExerciceItem[]>(data.exercices);
|
||||
const exercices = Array.isArray(exercicesParsed) ? exercicesParsed : null;
|
||||
const modeleParsed = parseJsonb<ProductionModele>(data.modele);
|
||||
const modele =
|
||||
data.modele && typeof data.modele === 'object' ? (data.modele as ProductionModele) : null
|
||||
modeleParsed && typeof modeleParsed === "object" ? modeleParsed : null;
|
||||
|
||||
const exercicesStatus = (data.exercices_status as JobStatus | null) ?? 'pending'
|
||||
const modeleStatus = (data.modele_status as JobStatus | null) ?? 'pending'
|
||||
const nclcCibleRaw = data.nclc_cible
|
||||
const exercicesStatus =
|
||||
(data.exercices_status as JobStatus | null) ?? "pending";
|
||||
const modeleStatus = (data.modele_status as JobStatus | null) ?? "pending";
|
||||
const nclcCibleRaw = data.nclc_cible;
|
||||
const nclcCible: 9 | 10 | null =
|
||||
nclcCibleRaw === 9 || nclcCibleRaw === 10 ? nclcCibleRaw : null
|
||||
nclcCibleRaw === 9 || nclcCibleRaw === 10 ? nclcCibleRaw : null;
|
||||
|
||||
return {
|
||||
data: {
|
||||
|
|
@ -301,7 +335,7 @@ export async function getById(
|
|||
modele,
|
||||
modele_status: modeleStatus,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -311,65 +345,65 @@ export async function getById(
|
|||
export async function autosaveContenu(
|
||||
id: string,
|
||||
userId: string,
|
||||
contenu: string
|
||||
contenu: string,
|
||||
): Promise<{ data: { ok: true } } | ControllerError> {
|
||||
if (contenu.length > 5000) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Le texte ne doit pas dépasser 5 000 caractères.',
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "Le texte ne doit pas dépasser 5 000 caractères.",
|
||||
status: 400,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const { data: prod, error } = await supabase
|
||||
.from('productions')
|
||||
.select('user_id, rapport')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
.from("productions")
|
||||
.select("user_id, rapport")
|
||||
.eq("id", id)
|
||||
.single();
|
||||
|
||||
if (error || !prod) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'SIMULATION_NOT_FOUND',
|
||||
message: 'Simulation introuvable.',
|
||||
code: "SIMULATION_NOT_FOUND",
|
||||
message: "Simulation introuvable.",
|
||||
status: 404,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (prod.user_id !== userId) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'AUTH_REQUIRED',
|
||||
message: 'Cette simulation ne vous appartient pas.',
|
||||
code: "AUTH_REQUIRED",
|
||||
message: "Cette simulation ne vous appartient pas.",
|
||||
status: 401,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (prod.rapport !== null) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Cette simulation a déjà été corrigée.',
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "Cette simulation a déjà été corrigée.",
|
||||
status: 400,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('productions')
|
||||
.from("productions")
|
||||
.update({ contenu })
|
||||
.eq('id', id)
|
||||
.eq("id", id);
|
||||
|
||||
if (updateError) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Sauvegarde impossible. Réessayez dans quelques instants.',
|
||||
code: "INTERNAL_ERROR",
|
||||
message: "Sauvegarde impossible. Réessayez dans quelques instants.",
|
||||
status: 500,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { data: { ok: true } }
|
||||
return { data: { ok: true } };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -379,69 +413,71 @@ export async function autosaveContenu(
|
|||
export async function updateSujet(
|
||||
id: string,
|
||||
userId: string,
|
||||
sujetId: string
|
||||
sujetId: string,
|
||||
): Promise<{ data: { sujet: SujetData } } | ControllerError> {
|
||||
const { data: sujetRow, error: sujetError } = await supabase
|
||||
.from('sujets')
|
||||
.select('id, consigne, role, contexte, doc1_titre, doc1_texte, doc2_titre, doc2_texte')
|
||||
.eq('id', sujetId)
|
||||
.single()
|
||||
.from("sujets")
|
||||
.select(
|
||||
"id, consigne, role, contexte, doc1_titre, doc1_texte, doc2_titre, doc2_texte",
|
||||
)
|
||||
.eq("id", sujetId)
|
||||
.single();
|
||||
|
||||
if (sujetError || !sujetRow) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'SUJET_NOT_FOUND',
|
||||
message: 'Sujet introuvable.',
|
||||
code: "SUJET_NOT_FOUND",
|
||||
message: "Sujet introuvable.",
|
||||
status: 404,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const { data: prod, error } = await supabase
|
||||
.from('productions')
|
||||
.select('user_id, rapport')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
.from("productions")
|
||||
.select("user_id, rapport")
|
||||
.eq("id", id)
|
||||
.single();
|
||||
|
||||
if (error || !prod) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'SIMULATION_NOT_FOUND',
|
||||
message: 'Simulation introuvable.',
|
||||
code: "SIMULATION_NOT_FOUND",
|
||||
message: "Simulation introuvable.",
|
||||
status: 404,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (prod.user_id !== userId) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'AUTH_REQUIRED',
|
||||
message: 'Cette simulation ne vous appartient pas.',
|
||||
code: "AUTH_REQUIRED",
|
||||
message: "Cette simulation ne vous appartient pas.",
|
||||
status: 401,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (prod.rapport !== null) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Cette simulation a déjà été corrigée.',
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "Cette simulation a déjà été corrigée.",
|
||||
status: 400,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('productions')
|
||||
.from("productions")
|
||||
.update({ sujet_id: sujetId })
|
||||
.eq('id', id)
|
||||
.eq("id", id);
|
||||
|
||||
if (updateError) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Mise à jour impossible. Réessayez dans quelques instants.',
|
||||
code: "INTERNAL_ERROR",
|
||||
message: "Mise à jour impossible. Réessayez dans quelques instants.",
|
||||
status: 500,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { data: { sujet: sujetRow as SujetData } }
|
||||
return { data: { sujet: sujetRow as SujetData } };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,11 +35,12 @@ describe("buildT2SystemPrompt", () => {
|
|||
it("substitue role et contexte dans le template", () => {
|
||||
const prompt = buildT2SystemPrompt(SUJET_OPTS);
|
||||
expect(prompt).toContain(
|
||||
"Tu joues le rôle de un bailleur qui propose un appartement à louer",
|
||||
"Tu incarnes un bailleur qui propose un appartement à louer",
|
||||
);
|
||||
expect(prompt).toContain("Vous cherchez un appartement");
|
||||
expect(prompt).toContain("uniquement en français");
|
||||
expect(prompt).toContain("français naturel et courant");
|
||||
expect(prompt).toContain("Tu ne prends PAS la parole en premier");
|
||||
expect(prompt).toContain("15 à 25 mots maximum");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -41,18 +41,22 @@ export function buildT2SystemPrompt(input: {
|
|||
contexte: string;
|
||||
}): string {
|
||||
const { role, contexte } = input;
|
||||
return `Tu joues le rôle de ${role} dans la situation suivante : ${contexte}
|
||||
|
||||
Règles à respecter impérativement :
|
||||
- Tu réponds uniquement en français, quelle que soit la langue de ton interlocuteur.
|
||||
- Tu joues ton rôle de façon naturelle et réaliste. Tu n'es pas un examinateur — tu es ${role}.
|
||||
- Tu réponds aux questions qu'on te pose de façon honnête et naturelle, comme le ferait une vraie personne dans cette situation.
|
||||
- Tu ne facilites pas la tâche : tu ne reformules pas les questions, tu n'anticipes pas ce que l'interlocuteur veut savoir, tu ne lui suggères pas quoi demander.
|
||||
- Si ton interlocuteur marque une longue pause ou semble avoir terminé, tu peux dire : "Avez-vous d'autres questions ?" — c'est la seule relance autorisée.
|
||||
- Tu ne fais aucun commentaire sur la langue, les erreurs ou le niveau de français de ton interlocuteur.
|
||||
- Tu ne sors jamais de ton rôle.
|
||||
- Tu ne prends PAS la parole en premier. Tu attends que ton interlocuteur s'adresse à toi, puis tu réponds naturellement dans ton rôle.
|
||||
- Tes réponses sont concises et naturelles : ni monosyllabiques, ni des monologues.`;
|
||||
return `Tu es un examinateur du TCF Canada pour l'épreuve d'Expression Orale, Tâche 2 (dialogue interactif).
|
||||
RÔLE : Tu incarnes ${role}.
|
||||
CONTEXTE : ${contexte}
|
||||
RÈGLES ABSOLUES :
|
||||
1. Tu parles TOUJOURS en français naturel et courant, niveau B2-C1.
|
||||
2. Tu NE corriges JAMAIS les erreurs du candidat.
|
||||
3. Tu attends que le candidat finisse sa question avant de répondre.
|
||||
4. Tes réponses sont courtes (15 à 25 mots maximum). Pas de monologue.
|
||||
5. Ne donne pas toutes les informations d'un coup. Force le candidat à poser des questions précises.
|
||||
6. Si le candidat est vague, réponds brièvement sans chercher à compléter.
|
||||
7. Ne pose JAMAIS de question de relance. Tu réponds, point.
|
||||
8. Ne prends jamais d'initiative pour orienter la conversation.
|
||||
9. Tu peux être légèrement pressé ou hésitant pour rendre l'échange réaliste.
|
||||
10. JAMAIS de listes ni de structure numérotée dans tes réponses.
|
||||
11. Ne mentionne jamais que tu es une IA ou un modèle.
|
||||
12. Tu ne prends PAS la parole en premier. Tu attends que le candidat s'adresse à toi.`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue