rolexx commited on
Commit
e49fcf4
·
1 Parent(s): 5ace5ff
.dockerignore ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dépendances
2
+ node_modules
3
+ npm-debug.log
4
+ yarn-debug.log
5
+ yarn-error.log
6
+
7
+ # Fichiers de build
8
+ .next
9
+ out
10
+ build
11
+ dist
12
+
13
+ # Fichiers de développement
14
+ .git
15
+ .gitignore
16
+ .env.local
17
+ .env.development.local
18
+ .env.test.local
19
+ .env.production.local
20
+ README.md
21
+ .vscode
22
+ .idea
23
+
24
+ # Fichiers système
25
+ .DS_Store
26
+ Thumbs.db
.env ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ ELEVEN_LABS_API_KEY="sk_97fa12a6d73a3b7145dbd5f41affb96f7fcf9452262cd0f1"
2
+ MISTRAL_API_KEY="lyDFvx5djb2d5JqhqrHh1sXr8dovlgnE"
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ public/bg.png filter=lfs diff=lfs merge=lfs -text
37
+ public/court.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # vercel
34
+ .vercel
35
+
36
+ # typescript
37
+ *.tsbuildinfo
38
+ next-env.d.ts
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Image de base
2
+ FROM node:20-alpine
3
+
4
+ # Définition du répertoire de travail
5
+ WORKDIR /app
6
+
7
+ # Copie des fichiers de dépendances
8
+ COPY package*.json ./
9
+
10
+ # Installation des dépendances
11
+ RUN npm install
12
+
13
+ # Copie du reste du code source
14
+ COPY . .
15
+
16
+ # Construction de l'application en ignorant les erreurs ESLint et TypeScript
17
+ ENV NEXT_TELEMETRY_DISABLED=1
18
+ ENV NODE_ENV=production
19
+ RUN npm run build || npm run build --no-lint
20
+
21
+ # Exposition du port
22
+ EXPOSE 7860
23
+
24
+ # Configuration de la commande de démarrage
25
+ CMD ["npm", "start", "--", "-p", "7860"]
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: DefendDaniel2
3
- emoji: 👁
4
- colorFrom: blue
5
- colorTo: indigo
6
  sdk: docker
 
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: DefendDaniel
3
+ emoji: 🎮
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: docker
7
+ sdk_version: 3.0.0
8
+ app_file: Dockerfile
9
+ app_port: 7860
10
  pinned: false
11
  ---
12
 
 
eslint.config.mjs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dirname } from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { FlatCompat } from "@eslint/eslintrc";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ const compat = new FlatCompat({
9
+ baseDirectory: __dirname,
10
+ });
11
+
12
+ const eslintConfig = [
13
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
14
+ ];
15
+
16
+ export default eslintConfig;
next.config.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ images: {
4
+ remotePatterns: [
5
+ {
6
+ protocol: 'https',
7
+ hostname: 'ik.imagekit.io',
8
+ pathname: '/z0tzxea0wgx/**',
9
+ },
10
+ ],
11
+ },
12
+ };
13
+
14
+ module.exports = nextConfig;
next.config.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ images: {
4
+ remotePatterns: [
5
+ {
6
+ protocol: 'https',
7
+ hostname: 'ik.imagekit.io',
8
+ pathname: '/z0tzxea0wgx/**',
9
+ },
10
+ ],
11
+ },
12
+ };
13
+
14
+ module.exports = nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "myapp",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --turbopack",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@mistralai/mistralai": "^1.4.0",
13
+ "elevenlabs": "^1.50.4",
14
+ "elevenlabs-client": "^0.0.13",
15
+ "ffmpeg-static": "^5.2.0",
16
+ "fluent-ffmpeg": "^2.1.3",
17
+ "next": "15.1.6",
18
+ "react": "^19.0.0",
19
+ "react-dom": "^19.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/eslintrc": "^3",
23
+ "@types/fluent-ffmpeg": "^2.1.27",
24
+ "@types/node": "^20",
25
+ "@types/react": "^19",
26
+ "@types/react-dom": "^19",
27
+ "eslint": "^9",
28
+ "eslint-config-next": "15.1.6",
29
+ "postcss": "^8",
30
+ "tailwindcss": "^3.4.1",
31
+ "typescript": "^5"
32
+ }
33
+ }
pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
postcss.config.mjs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ tailwindcss: {},
5
+ },
6
+ };
7
+
8
+ export default config;
public/file.svg ADDED
public/globe.svg ADDED
public/next.svg ADDED
public/vercel.svg ADDED
public/window.svg ADDED
src/app/GameContext.tsx ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { createContext, useContext, useState, ReactNode } from 'react';
3
+
4
+ type Scene = 'menu' | 'intro' | 'court' | 'defense' | 'lawyer' | 'end';
5
+
6
+ interface GameState {
7
+ score: number;
8
+ timeLeft: number;
9
+ language: 'fr' | 'en' | 'es';
10
+ round: number;
11
+ currentScene: Scene;
12
+ currentAnswer: string;
13
+ audioEnabled: boolean;
14
+ }
15
+
16
+ interface GameContextType {
17
+ gameState: GameState;
18
+ setLanguage: (lang: GameState['language']) => void;
19
+ setTimeLeft: (time: number | ((prev: number) => number)) => void;
20
+ setCurrentAnswer: (answer: string) => void;
21
+ goToNextScene: () => void;
22
+ resetGame: () => void;
23
+ }
24
+
25
+ const defaultGameState: GameState = {
26
+ score: 0,
27
+ timeLeft: 40,
28
+ language: 'fr',
29
+ round: 1,
30
+ currentScene: 'menu',
31
+ currentAnswer: '',
32
+ audioEnabled: false
33
+ };
34
+
35
+ const sceneFlow: Scene[] = ['menu', 'intro', 'court', 'defense', 'lawyer'];
36
+
37
+ const GameContext = createContext<GameContextType | undefined>(undefined);
38
+
39
+ export function GameProvider({ children }: { children: ReactNode }) {
40
+ const [gameState, setGameState] = useState<GameState>(defaultGameState);
41
+
42
+ const setLanguage = (lang: GameState['language']) => {
43
+ setGameState(prev => ({ ...prev, language: lang }));
44
+ };
45
+
46
+ const setTimeLeft = (time: number | ((prev: number) => number)) => {
47
+ setGameState(prev => ({ ...prev, timeLeft: typeof time === 'function' ? time(prev.timeLeft) : time }));
48
+ };
49
+
50
+ const setCurrentAnswer = (answer: string) => {
51
+ setGameState(prev => ({ ...prev, currentAnswer: answer }));
52
+ };
53
+
54
+ const goToNextScene = () => {
55
+ setGameState(prev => {
56
+ const currentIndex = sceneFlow.indexOf(prev.currentScene);
57
+ let nextScene = sceneFlow[currentIndex + 1];
58
+ let nextRound = prev.round;
59
+
60
+ // Si on vient de finir le lawyer et qu'on n'est pas au dernier round
61
+ if (prev.currentScene === 'lawyer' && prev.round < 3) {
62
+ nextScene = 'court';
63
+ nextRound = prev.round + 1;
64
+ }
65
+ // Si on a fini le dernier round
66
+ else if (prev.currentScene === 'lawyer' && prev.round >= 3) {
67
+ nextScene = 'end';
68
+ }
69
+
70
+ return {
71
+ ...prev,
72
+ currentScene: nextScene,
73
+ round: nextRound,
74
+ timeLeft: nextScene === 'defense' ? 40 : prev.timeLeft
75
+ };
76
+ });
77
+ };
78
+
79
+ const resetGame = () => {
80
+ setGameState(defaultGameState);
81
+ };
82
+
83
+ return (
84
+ <GameContext.Provider
85
+ value={{
86
+ gameState,
87
+ setLanguage,
88
+ setTimeLeft,
89
+ setCurrentAnswer,
90
+ goToNextScene,
91
+ resetGame
92
+ }}
93
+ >
94
+ {children}
95
+ </GameContext.Provider>
96
+ );
97
+ }
98
+
99
+ export function useGame() {
100
+ const context = useContext(GameContext);
101
+ if (!context) {
102
+ throw new Error('useGame must be used within a GameProvider');
103
+ }
104
+ return context;
105
+ }
src/app/api/text/question/route.ts ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+ import { Mistral } from "@mistralai/mistralai";
3
+
4
+ type Language = 'fr' | 'en' | 'es';
5
+
6
+ const mistral = new Mistral({apiKey: process.env.MISTRAL_API_KEY})
7
+
8
+ export async function POST(request: Request) {
9
+ try {
10
+ const body = await request.json();
11
+ const { language, story, chat } = body;
12
+ console.log('language:', language)
13
+ console.log('story:', story)
14
+ console.log('chat:', chat)
15
+
16
+ const chatHistory = chat.messages
17
+ .map((m: { role: string; content: string }) =>
18
+ `${m.role === 'judge' ? 'Judge' : 'Lawyer'}: ${m.content}`
19
+ )
20
+ .join('\n');
21
+
22
+ const prompts = {
23
+ fr: `Pouvez-vous trouver 1 question amusante pour réfuter les alibis que le juge pourrait poser à l'avocat de l'accusé dans cette affaire ?
24
+ (ne demandez pas de dessiner ou de faire des gestes, seulement des réponses textuelles), La question doit commencer par "Pourquoi" ou "J'aimerais savoir" etc.
25
+ Cette question doit être nouvelle: elle n'apparaît pas dans l'historiques de la discussion.
26
+ Pouvez-vous aussi donner 3 mots aléatoires *mais réels* que l'avocat devra ajouter à son discours. Ces mots doivent être simple, drole, reliés a l'affaire ou des mots ou expressions embarassantes pour un statut d'avocat (ex: "euuuh, et voila quoi..").
27
+ Ces mots doivent aussi être nouveaux: ils n'apparaissent pas dans l'historique de la discussion.
28
+ Je veux un mot en lien avec le case.
29
+ Je veux un mot ou expression embarassante pour un avocat.
30
+ Je veux un mot simple et drole.
31
+ RÉPONDEZ UNIQUEMENT AVEC LE JSON
32
+ N'AJOUTEZ PAS DE MOTS QUI ONT DÉJÀ ÉTÉ ENVOYÉS
33
+ Voici le contexte de l'affaire :
34
+
35
+ description de l'histoire : ${story.description}
36
+ alibis : ${story.alibi.join(', ')}
37
+ history : ${chat.messages.length > 0 ? `historique de la discussion : ${chatHistory}` : 'Vide'}
38
+
39
+ Je veux egalement une réaction a la derniere réponse de l'avocat. Cela peut etre des "Hmmm, d'accord" ou alors "Vous ne m'avez pas vraiment convaincu... Pourquoi parlez vous de ...".
40
+ Prends le role du juge et reponds avec condescendance. Cela doit etre dans un champ json "reaction" different de "question".
41
+ Parcontre la reaction ne doit pas finir par une question. Elle doit finir par trois petits points '...'
42
+ si history est vide, ne mettez pas reaction dans le json.
43
+
44
+ Réponse en JSON avec ce format :
45
+ {
46
+ "reaction" : "Votre réaction incisive ici",
47
+ "question" : " Votre question incisive de juge ici ",
48
+ "words" : [" expression1 ", " expression2 ", " expression3 "].
49
+ }`,
50
+ en: `can you find 1 fun questions to refute the alibis the judge could ask the lawyer of the accused about this case ?
51
+ (do not ask to draw or to gesture, only text answers), The question must start with "Why" or "I would like to know" etc.
52
+ This question must be new: it doesn't appear in the history of the discussion.
53
+ Can you also give 3 random *but real* words for the lawyer to add to his speech. These words should be simple, funny, related to the case or embarrassing words or phrases for a lawyer (e.g. “uh huh, here goes nothing”).
54
+ These words must also be new: they don't appear in the discussion history.
55
+ I want a word related to the case.
56
+ I want a word or phrase embarassant for a lawyer.
57
+ I want a simple and funny word.
58
+ ANSWER WITH ONLY THE JSON
59
+ DO NOT ADD WORDS THAT HAVE ALREADY BEEN SENT
60
+ Here is the context of the case :
61
+
62
+ story description: ${story.description}
63
+ alibis: ${story.alibi.join(', ')}
64
+ history: ${chat.messages.length > 0 ? `discussion history: ${chatHistory}` : 'Empty'}
65
+
66
+ I want a reaction to the last answer of the lawyer. This could be "Hmmm, okay.." or then "You didn't really convince me... Why are you talking about ...".
67
+ Take the role of the judge and answer with condescendance. This must be in a json field "reaction" different from "question".
68
+ The reaction must not end with a question. It has to finish with three dots '...'
69
+ If history is empty, do not put reaction in the json.
70
+
71
+ Answer in JSON with this format:
72
+ {
73
+ "reaction": "Your incisive reaction here",
74
+ "question": "Your incisive judge question here",
75
+ "words": ["expression1", "expression2", "expression3"]
76
+ }`,
77
+ es: `¿puedes encontrar 1 preguntas divertidas para refutar las coartadas que el juez podría hacer al abogado del acusado sobre este caso?
78
+ (no pidas dibujar o gesticular, sólo respuestas de texto), La pregunta debe comenzar con "Por qué" o "Me gustaría saber" etc.
79
+ Esta pregunta debe ser nueva: no aparece en la historia del debate.
80
+ También puedes dar 3 palabras aleatorias *pero reales* para que el abogado las añada a su discurso. Estas palabras deben ser sencillas, divertidas, relacionadas con el caso o palabras o frases embarazosas para un abogado (por ejemplo, «uh huh, aquí vamos..»).
81
+ Estas palabras también deben ser nuevas: no aparecen en la historia del debate.
82
+ Quiero una palabra relacionada con el caso.
83
+ Quiero una palabra o frase embarazosa para un abogado.
84
+ Quiero una palabra simple y divertida.
85
+ RESPONDA SÓLO CON EL JSON
86
+ NO AÑADAS PALABRAS QUE YA HAYAN SIDO ENVIADAS
87
+ Aquí está el contexto del caso :
88
+
89
+ descripción de la historia: ${story.description}
90
+ coartadas: ${story.alibi.join(', ')}
91
+ history: ${chat.messages.length > 0 ? `historia de la discusión: ${chatHistory}` : 'vacío'}
92
+
93
+ Quiero una reacción a la última respuesta del abogado. Esto podría ser "Hmmmm, entonces..." o "No me has convencido... ¿Por qué hablas de...".
94
+ Toma el papel del juez y responde con condescendencia. Cela debe estar en un campo json "reaction" diferente de "question".
95
+ La reacción no debe terminar con una pregunta. It has to finish with three dots '...'
96
+ Si history está vacío, no pongas reacción en el json.
97
+
98
+ Respuesta en JSON con este formato:
99
+ {
100
+ "reaction": "Tu reacción incisiva aquí",
101
+ "question": "Tu incisiva pregunta de juez aquí",
102
+ "words": ["expresión1", "expresión2", "expresión3"]
103
+ }`
104
+ };
105
+
106
+ console.log('prompts:', prompts[language as Language])
107
+
108
+ const seed = Math.floor(Math.random() * 1000000);
109
+
110
+ console.log('seed:', seed)
111
+
112
+ const response = await mistral.chat.complete({
113
+ model: "mistral-large-latest",
114
+ messages: [{role: 'user', content: prompts[language as Language]}],
115
+ responseFormat: {type: 'json_object'},
116
+ randomSeed: seed,
117
+ });
118
+
119
+ console.log('response:', response)
120
+
121
+ const functionCall = response.choices?.[0]?.message.content;
122
+ const JSONResponse = functionCall ? JSON.parse(functionCall as string) : null;
123
+ console.log('functionCall:', functionCall)
124
+ console.log('JSONResponse:', JSONResponse)
125
+
126
+ return NextResponse.json(JSONResponse || {
127
+ 'question': 'Erreur de génération de question',
128
+ 'words': [],
129
+ 'status': 'error',
130
+ });
131
+
132
+ } catch (error: unknown) {
133
+ console.log('error:', error)
134
+ return NextResponse.json(
135
+ { error: 'Erreur lors de la génération de la question' },
136
+ { status: 500 }
137
+ );
138
+ }
139
+ }
src/app/api/text/route.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+
3
+ export async function POST(request: Request) {
4
+ try {
5
+ const body = await request.json();
6
+ const { text } = body;
7
+
8
+ if (!text) {
9
+ return NextResponse.json(
10
+ { error: 'Le texte est requis' },
11
+ { status: 400 }
12
+ );
13
+ }
14
+
15
+ // Logique de traitement du texte ici
16
+ const processedText = text.toUpperCase(); // exemple simple
17
+
18
+ return NextResponse.json({
19
+ success: true,
20
+ result: processedText
21
+ });
22
+
23
+ } catch (_error: unknown) {
24
+ console.log('error:', _error)
25
+ return NextResponse.json(
26
+ { error: 'Erreur lors de la génération de la question' },
27
+ { status: 500 }
28
+ );
29
+ }
30
+ }
src/app/api/text/story/route.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+ import { Mistral } from "@mistralai/mistralai";
3
+
4
+ interface Story {
5
+ description: string;
6
+ alibi: string[];
7
+ problematic: string[];
8
+ }
9
+
10
+ const mistral = new Mistral({apiKey: process.env.MISTRAL_API_KEY})
11
+
12
+ export async function POST(request: Request) {
13
+ try {
14
+ const body = await request.json();
15
+ const { language = 'fr' } = body;
16
+ console.log('body:', body)
17
+
18
+ console.log('language:', language)
19
+
20
+ const prompts = {
21
+ fr: `Vous êtes passé maître dans l'art de générer de fausses histoires de procès.
22
+ Choisissez un mot réel totalement aléatoire et générez une fausse histoire, intégrant ce mot, d'un homme nommé Daniel accusé dans un procès.
23
+ L'histoire doit être cohérente et tenir en 1 ou 2 phrases.
24
+ Pouvez-vous donner la réponse dans un format json ?
25
+ Trouvez 3 alibis. Ils doivent être crédibles et concis.
26
+ RÉPONDRE UNIQUEMENT AVEC LE JSON`,
27
+ en: `You're a master in generating fake trial stories.
28
+ Choose a completly random real word and generate a fake story, integrating this word, of a guy named Daniel accused in trail.
29
+ The story must be coherent and must fit into 1 or 2 sentences.
30
+ Can you give the answer in a json format?
31
+ Find 3 alibis. They must be credible and concise.
32
+ ANSWER WITH ONLY THE JSON`,
33
+ es: `Eres un maestro en generar historias falsas de juicios.
34
+ Elige una palabra real completamente aleatoria y genera una historia falsa, integrando esta palabra, de un tipo llamado Daniel acusado en juicio.
35
+ La historia debe ser coherente y debe caber en 1 o 2 frases.
36
+ ¿Puedes dar la respuesta en formato json?
37
+ Encuentra 3 coartadas. Deben ser creíbles y concisas.
38
+ RESPONDER SÓLO CON EL JSON`
39
+ };
40
+
41
+ const chatPrompt = `${prompts[language as keyof typeof prompts] || prompts.fr}
42
+ accusation: {
43
+ description: String,
44
+ alibi: [<String>],
45
+ }`;
46
+
47
+ console.log('chatPrompt:', chatPrompt)
48
+
49
+ const seed = Math.floor(Math.random() * 1000000);
50
+
51
+ console.log('seed:', seed)
52
+
53
+ const response = await mistral.chat.complete({
54
+ model: "mistral-large-latest",
55
+ messages: [{role: 'user', content: chatPrompt}],
56
+ responseFormat: {type: 'json_object'},
57
+ randomSeed: seed,
58
+ });
59
+
60
+ console.log('response:', response)
61
+
62
+ const functionCall = response.choices?.[0]?.message.content;
63
+ const JSONResponse = functionCall ? JSON.parse(functionCall as string) : null;
64
+ console.log('functionCall:', functionCall)
65
+ console.log('JSONResponse:', JSONResponse)
66
+ const storyData: Story = JSONResponse?.accusation || {
67
+ description: "Erreur de génération",
68
+ alibi: [],
69
+ problematic: []
70
+ };
71
+
72
+ return NextResponse.json({
73
+ success: true,
74
+ story: storyData
75
+ });
76
+
77
+ } catch (error: unknown) {
78
+ console.log('error:', error)
79
+ return NextResponse.json(
80
+ { error: 'Erreur lors de la génération de l\'histoire' },
81
+ { status: 500 }
82
+ );
83
+ }
84
+ }
src/app/api/voice/route.ts ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+
3
+ const VOICES = {
4
+ fr: {
5
+ LAWYER_VOICE: {
6
+ id: "XgXB0fxFNJAEDoy7QEp5",
7
+ volume: 0
8
+ },
9
+ GLITCH_VOICE: {
10
+ id: "MWhJLNn7P7uvQrOTocc8",
11
+ volume: -10
12
+ },
13
+ JUDGE_VOICE: {
14
+ id: "x2AhtLKBQ202WmP0eMAe",
15
+ volume: 0
16
+ }
17
+ },
18
+ en: {
19
+ LAWYER_VOICE: {
20
+ id: "bGLLbWl0MmTsn5gWQCuZ",
21
+ volume: 0
22
+ },
23
+ GLITCH_VOICE: {
24
+ id: "ZCgnAThIoaTqZwEGwRb4",
25
+ volume: -10
26
+ },
27
+ JUDGE_VOICE: {
28
+ id: "e170Z5cpDGpADYBfQKbs",
29
+ volume: 0
30
+ }
31
+ },
32
+ es: {
33
+ LAWYER_VOICE: {
34
+ id: "tozjSvFqKBPpgsJFDfS0",
35
+ volume: 0
36
+ },
37
+ GLITCH_VOICE: {
38
+ id: "AnLaVu7KDTirBKuGkCZt",
39
+ volume: -10
40
+ },
41
+ JUDGE_VOICE: {
42
+ id: "I2lWW75NJTSYfUWIunTb",
43
+ volume: 0
44
+ }
45
+ }
46
+ };
47
+
48
+ export async function POST(request: Request) {
49
+ try {
50
+ const { text, language = 'en', role } = await request.json();
51
+ console.log('language:', language);
52
+ console.log('text:', text);
53
+ console.log('role:', role)
54
+
55
+ let voice;
56
+ if (role === 'lawyer') {
57
+ voice = VOICES[language as keyof typeof VOICES].LAWYER_VOICE.id;
58
+ } else if (role === 'judge') {
59
+ voice = VOICES[language as keyof typeof VOICES].JUDGE_VOICE.id;
60
+ } else if (role === 'glitch') {
61
+ voice = VOICES[language as keyof typeof VOICES].GLITCH_VOICE.id;
62
+ }
63
+ console.log('voice:', voice);
64
+
65
+ const response = await fetch(
66
+ `https://api.elevenlabs.io/v1/text-to-speech/${voice}`,
67
+ {
68
+ method: 'POST',
69
+ headers: {
70
+ 'Accept': 'audio/mpeg',
71
+ 'Content-Type': 'application/json',
72
+ 'xi-api-key': process.env.ELEVEN_LABS_API_KEY!
73
+ },
74
+ body: JSON.stringify({
75
+ text: text,
76
+ model_id: "eleven_flash_v2_5",
77
+ voice_settings: {
78
+ stability: 0.5,
79
+ similarity_boost: 0.75
80
+ }
81
+ })
82
+ }
83
+ );
84
+
85
+ if (!response.ok) {
86
+ throw new Error('Failed to generate voice');
87
+ }
88
+
89
+ const audioBuffer = await response.arrayBuffer();
90
+ return new NextResponse(audioBuffer, {
91
+ headers: {
92
+ 'Content-Type': 'audio/mpeg'
93
+ }
94
+ });
95
+
96
+ } catch (error) {
97
+ console.error('Voice generation error:', error);
98
+ return NextResponse.json(
99
+ { error: 'Failed to generate voice' },
100
+ { status: 500 }
101
+ );
102
+ }
103
+ }
src/app/favicon.ico ADDED
src/app/globals.css ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --font-shadows: var(--font-shadows-into-light);
8
+ }
9
+ }
10
+
11
+ @layer utilities {
12
+ .shadows-into-light-regular {
13
+ font-family: var(--font-shadows);
14
+ }
15
+ .font-crimson {
16
+ font-family: var(--font-crimson);
17
+ }
18
+ }
19
+
20
+ body {
21
+ font-family: Arial, Helvetica, sans-serif;
22
+ }
23
+
24
+ .shadows-into-light-regular {
25
+ font-family: "Shadows Into Light", serif;
26
+ font-weight: 400;
27
+ font-style: normal;
28
+ }
29
+
30
+ .poppins {
31
+ font-family: var(--font-poppins);
32
+ font-weight: 400;
33
+ font-style: normal;
34
+ }
35
+
36
+ .roboto {
37
+ font-family: var(--font-roboto);
38
+ font-weight: 400;
39
+ font-style: normal;
40
+ }
41
+
42
+ .roboto-slab {
43
+ font-family: var(--font-roboto-slab);
44
+ font-weight: 400;
45
+ font-style: normal;
46
+ }
src/app/layout.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono, Crimson_Text, Poppins, Roboto, Roboto_Slab } from "next/font/google";
3
+ import { Shadows_Into_Light } from "next/font/google";
4
+ import "./globals.css";
5
+
6
+ const geistSans = Geist({
7
+ variable: "--font-geist-sans",
8
+ subsets: ["latin"],
9
+ });
10
+
11
+ const geistMono = Geist_Mono({
12
+ variable: "--font-geist-mono",
13
+ subsets: ["latin"],
14
+ });
15
+
16
+ const shadowsIntoLight = Shadows_Into_Light({
17
+ weight: '400',
18
+ subsets: ['latin'],
19
+ variable: '--font-shadows',
20
+ });
21
+
22
+ const crimsonText = Crimson_Text({
23
+ weight: ['400', '700'],
24
+ subsets: ['latin'],
25
+ variable: '--font-crimson',
26
+ });
27
+
28
+ const poppins = Poppins({
29
+ weight: ['400', '700'],
30
+ subsets: ['latin'],
31
+ variable: '--font-poppins',
32
+ });
33
+
34
+ const roboto = Roboto({
35
+ weight: ['400', '700'],
36
+ subsets: ['latin'],
37
+ variable: '--font-roboto',
38
+ });
39
+
40
+ const robotoSlab = Roboto_Slab({
41
+ weight: ['400', '700'],
42
+ subsets: ['latin'],
43
+ variable: '--font-roboto-slab',
44
+ });
45
+
46
+ export const metadata: Metadata = {
47
+ title: "Create Next App",
48
+ description: "Generated by create next app",
49
+ };
50
+
51
+ export default function RootLayout({
52
+ children,
53
+ }: Readonly<{
54
+ children: React.ReactNode;
55
+ }>) {
56
+ return (
57
+ <html lang="en">
58
+ <body className={`${geistSans.variable} ${geistMono.variable} ${shadowsIntoLight.variable} ${crimsonText.variable} antialiased ${poppins.variable} ${roboto.variable} ${robotoSlab.variable}`}>
59
+ {children}
60
+ </body>
61
+ </html>
62
+ );
63
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import MenuScene from '../components/menu/Menu';
5
+ import IntroScene from '../components/intro/Intro';
6
+ import CourtScene from '../components/court/Court';
7
+ import DefenseScene from '../components/defense/Defense';
8
+ import LawyerScene from '../components/lawyer/Lawyer';
9
+ import EndScene from '../components/end/End';
10
+ import AccusationScene from '../components/accusation/Accusation';
11
+
12
+ // Types pour notre état
13
+ type Language = 'fr' | 'en' | 'es';
14
+
15
+ type Scene = 'menu' | 'intro' | 'accusation' | 'court' | 'defense' | 'lawyer' | 'end';
16
+ interface Story {
17
+ accusation: {
18
+ description: string;
19
+ alibi: string[];
20
+ };
21
+ }
22
+
23
+ interface Message {
24
+ content: string;
25
+ role: 'lawyer' | 'judge';
26
+ requiredWords?: string[];
27
+ }
28
+
29
+ interface Chat {
30
+ messages: Message[];
31
+ }
32
+
33
+
34
+ const intro = {
35
+ fr: {
36
+ title: "L'Avocat de l'IA",
37
+ description: `Daniel est un homme ordinaire. Il n'a rien fait de mal.\nPourtant, il est convoqué au tribunal aujourd'hui. Pauvre Daniel...`,
38
+ start: "Commencer"
39
+ },
40
+ en: {
41
+ title: "The AI Lawyer",
42
+ description: `Daniel is an ordinary guy. He hasn't done anything wrong.\nYet he's been summoned to court today. Poor Daniel...`,
43
+ start: "Start"
44
+ },
45
+ es: {
46
+ title: "El Abogado de la IA",
47
+ description: `Daniel es un tipo corriente. No ha hecho nada malo.\nSin embargo, ha sido convocado a la corte hoy. Pobre Daniel...`,
48
+ start: "Empezar"
49
+ }
50
+ }
51
+
52
+ const sceneOrder: Scene[] = ['menu', 'intro', 'accusation', 'court', 'defense', 'lawyer'];
53
+
54
+ export default function Home() {
55
+ // Gestion des scènes
56
+ const [currentScene, setCurrentScene] = useState<Scene>('menu');
57
+ const [story, setStory] = useState<Story | null>(null);
58
+ const [chat, setChat] = useState<Chat>({ messages: [] });
59
+ // États principaux du jeu
60
+ const [language, setLanguage] = useState<Language>('fr');
61
+
62
+ const [round, setRound] = useState<number>(1);
63
+
64
+ const [requiredWords, setRequiredWords] = useState<string[]>([])
65
+
66
+ const [currentQuestion, setCurrentQuestion] = useState<string>('');
67
+
68
+ const [reaction, setReaction] = useState<string>('');
69
+
70
+ const resetGame = () => {
71
+ setCurrentScene('menu');
72
+ setStory(null);
73
+ setChat({ messages: [] });
74
+ setLanguage('fr');
75
+ setRound(1);
76
+ setRequiredWords([]);
77
+ };
78
+
79
+ const setNextScene = () => {
80
+ if (currentScene === 'lawyer') {
81
+ if (round < 4) {
82
+ setCurrentScene('court');
83
+ } else {
84
+ setCurrentScene('end');
85
+ }
86
+ return;
87
+ }
88
+
89
+ if (currentScene === 'end') {
90
+ resetGame();
91
+ return;
92
+ }
93
+
94
+ const currentIndex = sceneOrder.indexOf(currentScene);
95
+ if (currentIndex !== -1 && currentIndex < sceneOrder.length - 1) {
96
+ setCurrentScene(sceneOrder[currentIndex + 1]);
97
+ }
98
+ };
99
+
100
+ // Props communs à passer aux composants
101
+ const commonProps = {
102
+ intro,
103
+ language,
104
+ setLanguage,
105
+ round,
106
+ setRound,
107
+ setCurrentScene,
108
+ setNextScene,
109
+ story,
110
+ currentQuestion,
111
+ setCurrentQuestion,
112
+ requiredWords,
113
+ setRequiredWords,
114
+ chat,
115
+ setChat,
116
+ reaction,
117
+ setReaction,
118
+ };
119
+
120
+ useEffect(() => {
121
+ const fetchStory = async () => {
122
+ try {
123
+ const response = await fetch('/api/text/story', {
124
+ method: 'POST',
125
+ headers: {
126
+ 'Content-Type': 'application/json',
127
+ },
128
+ body: JSON.stringify({ language })
129
+ });
130
+
131
+ const data = await response.json();
132
+
133
+ if (data.success && data.story) {
134
+ setStory({
135
+ accusation: {
136
+ description: data.story.description,
137
+ alibi: data.story.alibi,
138
+ }
139
+ });
140
+ }
141
+ } catch (error) {
142
+ console.error('Erreur lors de la récupération de l\'histoire:', error);
143
+ }
144
+ };
145
+
146
+ console.log('currentScene:', currentScene)
147
+
148
+ if (currentScene === 'intro') {
149
+ fetchStory();
150
+ }
151
+ // eslint-disable-next-line react-hooks/exhaustive-deps
152
+ }, [currentScene]); // on écoute les changements de currentScene
153
+
154
+ useEffect(() => {
155
+ if (reaction !== '') {
156
+ console.log('reaction:', reaction)
157
+ }
158
+ }, [reaction]);
159
+
160
+ useEffect(() => {
161
+ const fetchQuestion = async () => {
162
+ try {
163
+ const response = await fetch('/api/text/question', {
164
+ method: 'POST',
165
+ headers: {
166
+ 'Content-Type': 'application/json',
167
+ },
168
+ body: JSON.stringify({
169
+ language,
170
+ story: story?.accusation,
171
+ chat: chat
172
+ })
173
+ });
174
+
175
+ const data = await response.json();
176
+ console.log('data:', data)
177
+ console.log('round:', round)
178
+ if (data.question && data.words) {
179
+ setCurrentQuestion(data.question);
180
+ setRequiredWords(data.words);
181
+ if (data.reaction && data.reaction !== '') {
182
+ console.log('data.reaction:', data.reaction)
183
+ setReaction(data.reaction);
184
+ }
185
+ setChat(prevChat => ({
186
+ messages: [...prevChat.messages, { content: data.question, role: 'judge' }]
187
+ }));
188
+ }
189
+ } catch (error) {
190
+ console.error('Erreur lors de la récupération de la question:', error);
191
+ }
192
+ };
193
+
194
+ if ((currentScene === 'accusation' && story) || (currentScene === 'lawyer' && round < 3 && story)) {
195
+ console.log('fetchQuestion')
196
+ fetchQuestion();
197
+ }
198
+ // eslint-disable-next-line react-hooks/exhaustive-deps
199
+ }, [currentScene]);
200
+
201
+ useEffect(() => {
202
+ if (currentQuestion && requiredWords.length > 0) {
203
+ console.log('currentQuestion:', currentQuestion)
204
+ console.log('requiredWords:', requiredWords)
205
+ }
206
+ }, [currentQuestion, requiredWords])
207
+
208
+ switch (currentScene) {
209
+ case 'menu':
210
+ return <MenuScene {...commonProps} />;
211
+ case 'intro':
212
+ return <IntroScene {...commonProps} />;
213
+ case 'accusation':
214
+ return <AccusationScene {...commonProps} />;
215
+ case 'court':
216
+ return <CourtScene {...commonProps} />;
217
+ case 'defense':
218
+ return <DefenseScene {...commonProps} />;
219
+ case 'lawyer':
220
+ return <LawyerScene {...commonProps} />;
221
+ case 'end':
222
+ return <EndScene {...commonProps} />;
223
+ default:
224
+ return <MenuScene {...commonProps} />;
225
+ }
226
+ }
src/components/accusation/Accusation.tsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { FC } from 'react';
3
+ import Image from 'next/image';
4
+
5
+ interface Story {
6
+ accusation: {
7
+ description: string;
8
+ alibi: string[];
9
+ };
10
+ }
11
+
12
+ interface AccusationSceneProps {
13
+ language: 'fr' | 'en' | 'es';
14
+ story: Story | null;
15
+ setNextScene: () => void;
16
+ }
17
+
18
+ const AccusationScene: FC<AccusationSceneProps> = ({
19
+ language,
20
+ story,
21
+ setNextScene,
22
+ }) => {
23
+ return (
24
+ <div className="relative w-screen h-screen">
25
+ {/* Image de fond */}
26
+ <Image
27
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_BG1_rva-mKDVA.jpg?updatedAt=1737835881047"
28
+ alt="Background"
29
+ fill
30
+ className="object-cover"
31
+ priority
32
+ />
33
+
34
+ {/* Overlay noir */}
35
+ <div className="absolute inset-0 bg-black/70">
36
+ {/* Contenu */}
37
+ <div className="relative z-10 flex flex-col items-center justify-center h-full p-8 space-y-8">
38
+ <div className="max-w-3xl w-full space-y-8">
39
+ {/* Description */}
40
+ <div>
41
+ <h2 className="text-4xl font-bold text-white mb-4 roboto-slab">
42
+ {language === 'fr' ? "Chef d'accusation" : language === 'en' ? 'Indictment' : 'Acusación'}
43
+ </h2>
44
+ <p className="text-xl text-white roboto-slab">
45
+ {story?.accusation.description}
46
+ </p>
47
+ </div>
48
+
49
+ {/* Alibis */}
50
+ <div>
51
+ <h2 className="text-4xl font-bold text-white mb-4 roboto-slab">
52
+ {language === 'fr' ? 'Alibis' : language === 'en' ? 'Alibis' : 'Coartadas'}
53
+ </h2>
54
+ <ul className="list-disc list-inside text-white space-y-2 roboto-slab">
55
+ {story?.accusation.alibi.map((alibi, index) => (
56
+ <li key={index} className="text-xl">{alibi}</li>
57
+ ))}
58
+ </ul>
59
+ </div>
60
+ </div>
61
+
62
+ <button
63
+ onClick={setNextScene}
64
+ className="px-8 py-4 text-xl font-bold text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors roboto-slab"
65
+ >
66
+ {language === 'fr' ? 'Allez au tribunal !' : language === 'en' ? 'Go to court!' : '¡A los tribunales!'}
67
+ </button>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ );
72
+ };
73
+
74
+ export default AccusationScene;
src/components/common/Layout.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { FC, ReactNode } from 'react';
4
+
5
+ interface LayoutProps {
6
+ children: ReactNode;
7
+ }
8
+
9
+ const Layout: FC<LayoutProps> = ({ children }) => {
10
+ return (
11
+ <div className="w-screen h-screen bg-black overflow-hidden">
12
+ <div className="w-full h-full overflow-y-auto">
13
+ {children}
14
+ </div>
15
+ </div>
16
+ );
17
+ };
18
+
19
+ export default Layout;
src/components/components/LanguageSelector.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ type Language = {
4
+ code: string;
5
+ name: string;
6
+ };
7
+
8
+ const languages: Language[] = [
9
+ { code: 'fr', name: 'Français' },
10
+ { code: 'en', name: 'English' },
11
+ { code: 'es', name: 'Español' },
12
+ ];
13
+
14
+ export const LanguageSelector = ({ onSelect }: { onSelect: (lang: string) => void }) => {
15
+ return (
16
+ <select
17
+ onChange={(e) => onSelect(e.target.value)}
18
+ className="px-4 py-2 rounded-lg border-2 border-gray-300"
19
+ >
20
+ {languages.map((lang) => (
21
+ <option key={lang.code} value={lang.code}>
22
+ {lang.name}
23
+ </option>
24
+ ))}
25
+ </select>
26
+ );
27
+ }
28
+
29
+ export default LanguageSelector;
src/components/court/Court.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { FC, useEffect, useState } from 'react';
3
+ import Image from 'next/image';
4
+
5
+ interface CourtSceneProps {
6
+ setNextScene: () => void;
7
+ currentQuestion: string;
8
+ reaction: string;
9
+ round: number;
10
+ }
11
+
12
+ const CourtScene: FC<CourtSceneProps> = ({
13
+ setNextScene,
14
+ currentQuestion,
15
+ reaction,
16
+ round
17
+ }) => {
18
+ const [showFirstImage, setShowFirstImage] = useState(true);
19
+
20
+ useEffect(() => {
21
+ const playAudio = async () => {
22
+ try {
23
+ if (!currentQuestion) return;
24
+
25
+ const response = await fetch('/api/voice', {
26
+ method: 'POST',
27
+ headers: {
28
+ 'Content-Type': 'application/json',
29
+ },
30
+ body: JSON.stringify({
31
+ language: 'en', // You may want to make this configurable
32
+ text: reaction !== '' ? reaction + currentQuestion : currentQuestion,
33
+ role: 'judge'
34
+ })
35
+ });
36
+
37
+ if (!response.ok) {
38
+ throw new Error('Failed to generate audio');
39
+ }
40
+
41
+ const audioBlob = await response.blob();
42
+ const audioUrl = URL.createObjectURL(audioBlob);
43
+ const audio = new Audio(audioUrl);
44
+ audio.play();
45
+ } catch (error) {
46
+ console.error('Error playing audio:', error);
47
+ }
48
+ };
49
+
50
+ if (currentQuestion) {
51
+ playAudio();
52
+ }
53
+ // eslint-disable-next-line react-hooks/exhaustive-deps
54
+ }, [currentQuestion]);
55
+
56
+ useEffect(() => {
57
+ const timer = setTimeout(() => {
58
+ setShowFirstImage(false);
59
+ }, 2000);
60
+
61
+ return () => clearTimeout(timer);
62
+ }, []);
63
+
64
+ return (
65
+ <div className="relative w-screen h-screen">
66
+ {/* Image de fond statique */}
67
+ <Image
68
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_judge2_FoYazvSFmu.png?updatedAt=1737835883172"
69
+ alt="Background"
70
+ fill
71
+ className="object-cover"
72
+ priority
73
+ />
74
+
75
+ {/* Image superposée avec transition */}
76
+ <div className={`absolute inset-0 transition-opacity duration-500 ${showFirstImage ? 'opacity-100' : 'opacity-0'}`}>
77
+ <Image
78
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_judge1_Bn04jNl_E.png?updatedAt=1737835883169"
79
+ alt="Overlay"
80
+ fill
81
+ className="object-cover"
82
+ priority
83
+ />
84
+ </div>
85
+
86
+ {/* Rectangle noir en bas */}
87
+ <div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-[80%] bg-black/60 border border-black border-8 mb-[8vh] p-6">
88
+ <div className="text-white roboto-slab">
89
+ <p className="text-4xl mb-4">
90
+ {reaction !== '' && round !== 1 ? reaction.replace(/\?/g, '') : ''} <br />
91
+ {currentQuestion ? currentQuestion : '...'}
92
+ </p>
93
+
94
+ {/* Flèche à droite */}
95
+ <div className="flex justify-end">
96
+ <button
97
+ onClick={setNextScene}
98
+ className="text-white hover:text-blue-400 transition-colors"
99
+ aria-label="Continuer"
100
+ >
101
+ <svg
102
+ xmlns="http://www.w3.org/2000/svg"
103
+ className="h-12 w-12"
104
+ fill="none"
105
+ viewBox="0 0 24 24"
106
+ stroke="currentColor"
107
+ >
108
+ <path
109
+ strokeLinecap="round"
110
+ strokeLinejoin="round"
111
+ strokeWidth={2}
112
+ d="M14 5l7 7m0 0l-7 7m7-7H3"
113
+ />
114
+ </svg>
115
+ </button>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ );
121
+ };
122
+
123
+ export default CourtScene;
src/components/defense/Defense.tsx ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { FC, useState, useEffect, Dispatch, SetStateAction } from 'react';
3
+ import Image from 'next/image';
4
+
5
+ interface Message {
6
+ content: string;
7
+ role: 'lawyer' | 'judge';
8
+ }
9
+
10
+ interface Chat {
11
+ messages: Message[];
12
+ }
13
+
14
+ interface DefenseSceneProps {
15
+ language: 'fr' | 'en' | 'es';
16
+ requiredWords: string[];
17
+ setNextScene: () => void;
18
+ setChat: (chat: SetStateAction<Chat>) => void;
19
+ setCurrentQuestion: Dispatch<SetStateAction<string>>;
20
+ setReaction: Dispatch<SetStateAction<string>>;
21
+ setRequiredWords: Dispatch<SetStateAction<string[]>>;
22
+ }
23
+
24
+ const DefenseScene: FC<DefenseSceneProps> = ({
25
+ language,
26
+ requiredWords,
27
+ setNextScene,
28
+ setCurrentQuestion,
29
+ setChat,
30
+ setReaction,
31
+ setRequiredWords
32
+ }) => {
33
+ const [answer, setAnswer] = useState('');
34
+ const [insertedWords, setInsertedWords] = useState<boolean[]>([]);
35
+ const [countdown, setCountdown] = useState(60);
36
+ const [isTimeUp, setIsTimeUp] = useState(false);
37
+ const [wordPositions, setWordPositions] = useState<Array<{ word: string; position: number }>>([]);
38
+ const [mandatoryWords, setMandatoryWords] = useState(requiredWords);
39
+ const [isLoading, setIsLoading] = useState(true);
40
+ const [ words ] = useState(requiredWords);
41
+
42
+ // Initialisation des mots obligatoires
43
+ useEffect(() => {
44
+ if (requiredWords.length > 0) {
45
+ setMandatoryWords(requiredWords);
46
+ }
47
+ setReaction('');
48
+ // eslint-disable-next-line react-hooks/exhaustive-deps
49
+ }, [requiredWords]);
50
+
51
+ // Génération des positions et initialisation
52
+ useEffect(() => {
53
+ if (mandatoryWords.length > 0) {
54
+ const positions = generateWordPositions(mandatoryWords);
55
+ setWordPositions(positions);
56
+ setInsertedWords(new Array(mandatoryWords.length).fill(false));
57
+ setCurrentQuestion("");
58
+ setIsLoading(false);
59
+ }
60
+ }, [mandatoryWords]); // eslint-disable-line react-hooks/exhaustive-deps
61
+
62
+ // Reset des required words après initialisation
63
+ useEffect(() => {
64
+ if (!isLoading && wordPositions.length > 0) {
65
+ setRequiredWords([]);
66
+ }
67
+ }, [isLoading, wordPositions.length]); // eslint-disable-line react-hooks/exhaustive-deps
68
+
69
+ useEffect(() => {
70
+ if (isTimeUp) {
71
+ handleSubmit();
72
+ }
73
+ // eslint-disable-next-line react-hooks/exhaustive-deps
74
+ }, [isTimeUp])
75
+
76
+ // Timer et reset de la question
77
+ useEffect(() => {
78
+ // Timer
79
+ const timer = setInterval(() => {
80
+ setCountdown((prev) => {
81
+ console.log('prev:', prev)
82
+ if (prev === 0) {
83
+ clearInterval(timer);
84
+ setIsTimeUp(true);
85
+ }
86
+ return prev - 1;
87
+ });
88
+ }, 1000);
89
+
90
+ return () => clearInterval(timer);
91
+ // eslint-disable-next-line react-hooks/exhaustive-deps
92
+ }, []); // On garde uniquement la dépendance answer car handleSubmit est stable
93
+
94
+ // Génère un nombre aléatoire entre 9 et 15
95
+ const generateRandomNumber = () => {
96
+ return Math.floor(Math.random() * (15 - 9 + 1)) + 9;
97
+ };
98
+
99
+ // Génère les positions pour les mots requis
100
+ const generateWordPositions = (words: string[]) => {
101
+ let currentPosition = generateRandomNumber(); // On commence à une position aléatoire
102
+ return words.map(word => {
103
+ const position = currentPosition;
104
+ currentPosition += generateRandomNumber(); // Ajoute un nombre aléatoire de mots pour le prochain mot requis
105
+ return {
106
+ word,
107
+ position
108
+ };
109
+ });
110
+ };
111
+
112
+ // Fonction pour compter les mots
113
+ const countWords = (text: string) => {
114
+ // Remplacer temporairement les expressions requises par un seul mot
115
+ let modifiedText = text;
116
+ wordPositions.forEach(({ word }) => {
117
+ if (modifiedText.includes(word)) {
118
+ // Remplace l'expression complète par un placeholder unique
119
+ modifiedText = modifiedText.replace(word, 'SINGLEWORD');
120
+ }
121
+ });
122
+
123
+ // Maintenant compte les mots normalement
124
+ return modifiedText.trim().split(/\s+/).length || 0;
125
+ };
126
+
127
+ // Fonction pour vérifier si on doit insérer un mot obligatoire
128
+ const checkAndInsertRequiredWord = (text: string) => {
129
+ const currentWordCount = countWords(text);
130
+ let newText = text;
131
+ const newInsertedWords = [...insertedWords];
132
+ let wordInserted = false;
133
+
134
+ wordPositions.forEach((word, index) => {
135
+ if (!insertedWords[index] && currentWordCount === word.position && text.endsWith(' ')) {
136
+ newText = `${text}${word.word} `;
137
+ newInsertedWords[index] = true;
138
+ wordInserted = true;
139
+ }
140
+ });
141
+
142
+ if (wordInserted) {
143
+ setInsertedWords(newInsertedWords);
144
+ }
145
+
146
+ return newText;
147
+ };
148
+
149
+ // Fonction pour vérifier si on peut supprimer du texte
150
+ const canDeleteAt = (text: string, cursorPosition: number) => {
151
+ // const textBeforeCursor = text.substring(0, cursorPosition);
152
+ // const wordsBeforeCursor = countWords(textBeforeCursor);
153
+
154
+ return !wordPositions.some((word, index) => {
155
+ if (!insertedWords[index]) return false;
156
+
157
+ const wordStartPosition = text.indexOf(word.word);
158
+ const wordEndPosition = wordStartPosition + word.word.length;
159
+
160
+ return cursorPosition > wordStartPosition && cursorPosition <= wordEndPosition;
161
+ });
162
+ };
163
+
164
+ const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
165
+ const newText = e.target.value;
166
+ setAnswer(newText);
167
+ const cursorPosition = e.target.selectionStart;
168
+
169
+ // Si c'est une suppression
170
+ if (newText.length < answer.length) {
171
+ if (!canDeleteAt(answer, cursorPosition)) {
172
+ return;
173
+ }
174
+ }
175
+
176
+ // Insertion normale + vérification des mots obligatoires
177
+ const processedText = checkAndInsertRequiredWord(newText);
178
+ setAnswer(processedText);
179
+ };
180
+
181
+ const handleSubmit = () => {
182
+ setChat(prevChat => ({
183
+ messages: [...prevChat.messages, { content: answer, role: 'lawyer', requiredWords: words }]
184
+ }));
185
+ setNextScene();
186
+ };
187
+
188
+ const formatTime = (seconds: number) => {
189
+ const mins = Math.floor(seconds / 60);
190
+ const secs = seconds % 60;
191
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
192
+ };
193
+
194
+ // Vérifie si la réponse est valide pour soumission
195
+ const isAnswerValid = () => {
196
+ const lastWordPosition = Math.max(...wordPositions.map(word => word.position));
197
+ const currentWordCount = countWords(answer);
198
+ return currentWordCount >= lastWordPosition && insertedWords.every(inserted => inserted);
199
+ };
200
+
201
+ return (
202
+ <div className="relative w-screen h-screen">
203
+ {/* Image de fond */}
204
+ <Image
205
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_attorney2_gbcNJRrYM.png?updatedAt=1737835883087"
206
+ alt="Background"
207
+ fill
208
+ className="object-cover"
209
+ priority
210
+ />
211
+
212
+ {/* Contenu avec overlay noir */}
213
+ <div className="absolute inset-0">
214
+ {isLoading ? (
215
+ <div className="flex items-center justify-center h-full">
216
+ <div className="text-2xl text-gray-300">
217
+ {language === 'fr' ? 'Chargement...' : language === 'en' ? 'Loading...' : 'Cargando...'}
218
+ </div>
219
+ </div>
220
+ ) : (
221
+ <div className="flex flex-col justify-end p-8 h-full">
222
+ {/* Header avec le compte à rebours */}
223
+ <div className="flex justify-between items-center mb-6">
224
+ <div className={`text-8xl roboto-slab ${countdown < 10 ? 'text-red-500' : 'text-white'}`}>
225
+ {formatTime(countdown)}
226
+ </div>
227
+ </div>
228
+
229
+ {/* Prochain mot requis */}
230
+ <div className="mb-6">
231
+ {wordPositions.map((item, index) => {
232
+ // Ne montrer que le premier mot non inséré
233
+ if (insertedWords[index] || index > 0 && !insertedWords[index - 1]) return null;
234
+ const remainingWords = item.position - countWords(answer);
235
+ return (
236
+ <div key={index} className="bg-black/60 border border-black border-8 p-6 text-white">
237
+ <span className="text-5xl text-sky-500 roboto-slab mt-2">
238
+ {item.word.toUpperCase()}
239
+ </span>
240
+ <span className="text-5xl text-white roboto-slab mt-2"> {language === 'fr'
241
+ ? `dans `
242
+ : language === 'en'
243
+ ? `in `
244
+ : `en `
245
+ }</span>
246
+ <span className="text-5xl text-red-500 roboto-slab mt-2">{remainingWords}</span>
247
+ <span className="text-5xl text-white roboto-slab mt-2"> {language === 'fr'
248
+ ? ` mots`
249
+ : language === 'en'
250
+ ? ` words`
251
+ : ` palabras`
252
+ }</span>
253
+ </div>
254
+ );
255
+ }).filter(Boolean)[0]}
256
+ </div>
257
+
258
+ {/* Zone de texte avec bouton submit en position absolue */}
259
+ <div className="relative w-full mb-6">
260
+ <textarea
261
+ value={answer}
262
+ onChange={handleTextChange}
263
+ disabled={isTimeUp}
264
+ placeholder={language === 'fr'
265
+ ? "Écrivez votre défense ici..."
266
+ : language === 'en'
267
+ ? "Write your defense here..."
268
+ : "Write your defense here..."}
269
+ className="w-full p-6 bg-black/60 border border-black border-8 text-white text-4xl rounded-none focus:outline-none roboto-slab h-[30vh]"
270
+ />
271
+
272
+ {/* Bouton de soumission */}
273
+ <button
274
+ onClick={() => handleSubmit()}
275
+ disabled={!isAnswerValid()}
276
+ className={`absolute bottom-5 right-5 px-8 py-4 rounded-lg text-xl transition-all duration-300 ${isAnswerValid()
277
+ ? 'bg-sky-500 hover:bg-blue-700 cursor-pointer'
278
+ : 'bg-gray-600 cursor-not-allowed'
279
+ }`}
280
+ >
281
+ {language === 'fr' ? 'Soumettre' : language === 'en' ? 'Submit' : 'Submit'}
282
+ </button>
283
+ </div>
284
+ </div>
285
+ )}
286
+ </div>
287
+ </div>
288
+ );
289
+ };
290
+
291
+ export default DefenseScene;
src/components/end/End.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { FC } from 'react';
3
+ import Image from 'next/image';
4
+
5
+ interface EndSceneProps {
6
+ language: 'fr' | 'en' | 'es';
7
+ setNextScene: () => void;
8
+ }
9
+
10
+ const EndScene: FC<EndSceneProps> = ({
11
+ language,
12
+ setNextScene,
13
+ }) => {
14
+ return (
15
+ <div className="relative w-screen h-screen">
16
+ {/* Image de fond */}
17
+ <Image
18
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/court_M-RO6txqB.png?updatedAt=1737835884433"
19
+ alt="Background"
20
+ fill
21
+ className="object-cover"
22
+ priority
23
+ />
24
+
25
+ {/* Contenu avec overlay noir */}
26
+ <div className="absolute inset-0 bg-black/70">
27
+ <div className="flex flex-col items-center justify-center h-full p-8">
28
+ <div className="bg-black/60 border border-black border-8 p-6 w-[80%] text-center">
29
+ <h1 className="text-4xl text-white roboto-slab mb-8">
30
+ {language === 'fr'
31
+ ? 'Fin du procès'
32
+ : language === 'en'
33
+ ? 'End of trial'
34
+ : 'Fin del juicio'
35
+ }
36
+ </h1>
37
+ <button
38
+ onClick={setNextScene}
39
+ className="px-8 py-4 text-xl font-bold text-white bg-sky-500 hover:bg-blue-700 transition-colors roboto-slab"
40
+ >
41
+ {language === 'fr' ? 'Rejouer' : language === 'en' ? 'Play again' : 'Jugar de nuevo'}
42
+ </button>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ );
48
+ };
49
+
50
+ export default EndScene;
src/components/intro/Intro.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { FC } from 'react';
3
+ import Image from 'next/image';
4
+
5
+
6
+ interface Story {
7
+ accusation: {
8
+ description: string;
9
+ alibi: string[];
10
+ };
11
+ }
12
+
13
+ interface IntroSceneProps {
14
+ intro: {
15
+ fr: {
16
+ title: string;
17
+ description: string;
18
+ start: string;
19
+ };
20
+ en: {
21
+ title: string;
22
+ description: string;
23
+ start: string;
24
+ };
25
+ es: {
26
+ title: string;
27
+ description: string;
28
+ start: string;
29
+ };
30
+ },
31
+ language: 'fr' | 'en' | 'es';
32
+ setNextScene: () => void;
33
+ story: Story | null;
34
+ }
35
+
36
+ const IntroScene: FC<IntroSceneProps> = ({
37
+ intro,
38
+ language,
39
+ setNextScene,
40
+ story,
41
+ }) => {
42
+ const handleContinue = () => {
43
+ setNextScene();
44
+ };
45
+
46
+ return (
47
+ <div className="relative w-screen h-screen">
48
+ {/* Image de fond */}
49
+ <Image
50
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_BG1_rva-mKDVA.jpg?updatedAt=1737835881047https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_BG1_rva-mKDVA.jpg?updatedAt=1737835881047"
51
+ alt="Background"
52
+ fill
53
+ className="object-cover"
54
+ priority
55
+ />
56
+
57
+ {/* Overlay noir */}
58
+ <div className="absolute inset-0 bg-black/70">
59
+ {/* Contenu */}
60
+ <div className="relative z-10 flex flex-col items-center justify-center h-full p-4">
61
+ <p className="text-4xl text-white text-center mb-8 roboto-slab max-w-2xl">
62
+ {intro[language].description}
63
+ </p>
64
+
65
+ <button
66
+ onClick={handleContinue}
67
+ className={`px-8 py-4 text-xl font-bold text-white rounded-lg transition-colors roboto-slab
68
+ ${story ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-500 cursor-not-allowed'}`}
69
+ disabled={!story}
70
+ >
71
+ {intro[language].start}
72
+ </button>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ );
77
+ };
78
+
79
+ export default IntroScene;
src/components/lawyer/Lawyer.tsx ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { FC, useState, useEffect, Dispatch, SetStateAction } from 'react';
3
+ import Image from 'next/image';
4
+
5
+ interface Message {
6
+ content: string;
7
+ role: 'lawyer' | 'judge';
8
+ requiredWords?: string[];
9
+ }
10
+
11
+
12
+ interface Chat {
13
+ messages: Message[];
14
+ }
15
+
16
+ interface LawyerSceneProps {
17
+ language: 'fr' | 'en' | 'es';
18
+ chat: Chat;
19
+ setNextScene: () => void;
20
+ currentQuestion: string;
21
+ round: number;
22
+ setRound: Dispatch<SetStateAction<number>>;
23
+ }
24
+
25
+ const LawyerScene: FC<LawyerSceneProps> = ({
26
+ language,
27
+ chat,
28
+ setNextScene,
29
+ currentQuestion,
30
+ round,
31
+ setRound,
32
+ }) => {
33
+ const [visible, setVisible] = useState(false);
34
+ const [buttonEnabled, setButtonEnabled] = useState(false);
35
+ const [countdown, setCountdown] = useState(3);
36
+
37
+ const [question, setQuestion] = useState('');
38
+ const [answer, setAnswer] = useState('');
39
+
40
+ useEffect(() => {
41
+ const lastJudgeMessage = chat.messages.filter((message: Message) => message.role === 'judge').slice(-1)[0]?.content;
42
+ const lastLawyerMessage = chat.messages.filter((message: Message) => message.role === 'lawyer').slice(-1)[0]?.content;
43
+ setQuestion(lastJudgeMessage || '');
44
+ setAnswer(lastLawyerMessage || '');
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ }, []); // Only run once on mount
47
+
48
+ useEffect(() => {
49
+ const playAudio = async () => {
50
+ try {
51
+
52
+ const response = await fetch('/api/voice', {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body: JSON.stringify({
58
+ language,
59
+ text: answer,
60
+ role: 'lawyer'
61
+ })
62
+ });
63
+
64
+ if (!response.ok) {
65
+ throw new Error('Failed to generate audio');
66
+ }
67
+
68
+ const audioBlob = await response.blob();
69
+ const audioUrl = URL.createObjectURL(audioBlob);
70
+ const audio = new Audio(audioUrl);
71
+ audio.play();
72
+ } catch (error) {
73
+ console.error('Error playing audio:', error);
74
+ }
75
+ };
76
+
77
+ if (answer !== '') {
78
+ playAudio();
79
+ }
80
+ // eslint-disable-next-line react-hooks/exhaustive-deps
81
+ }, [answer])
82
+
83
+ useEffect(() => {
84
+ setVisible(true);
85
+ setRound(round + 1);
86
+ const timer = setInterval(() => {
87
+ setCountdown((prev) => {
88
+ if (prev <= 1) {
89
+ setButtonEnabled(true);
90
+ clearInterval(timer);
91
+ return 0;
92
+ }
93
+ return prev - 1;
94
+ });
95
+ }, 1000);
96
+
97
+ return () => clearInterval(timer);
98
+ }, []);
99
+
100
+ return (
101
+ <div className="relative w-screen h-screen">
102
+ {/* Image de fond */}
103
+ <Image
104
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_attorney1_k5DWtEzcV.png?updatedAt=1737835883169"
105
+ alt="Background"
106
+ fill
107
+ className="object-cover"
108
+ priority
109
+ />
110
+
111
+ {/* Contenu avec overlay noir */}
112
+ <div className="absolute inset-0">
113
+ <div className="flex flex-col items-center justify-end h-full p-8">
114
+ <div className={`flex flex-col items-center justify-center space-y-8 transition-opacity duration-1000 ${
115
+ visible ? 'opacity-100' : 'opacity-0'
116
+ }`}>
117
+ {/* Question du juge */}
118
+ <div className="bg-black/60 border border-black border-8 p-6 mb-8 w-[80%]">
119
+ <h2 className="text-2xl font-bold mb-4 roboto-slab text-white">
120
+ {language === 'fr' ? 'Question du juge' : language === 'en' ? 'Judge\'s question' : 'Pregunta del juez'}:
121
+ </h2>
122
+ <p className="text-xl text-white roboto-slab">
123
+ {question}
124
+ </p>
125
+ </div>
126
+
127
+ {/* Réponse de l'avocat avec flèche */}
128
+ <div className="relative bg-black/60 border border-black border-8 p-6 mb-8 w-[80%]">
129
+ <h2 className="text-2xl font-bold mb-4 roboto-slab text-white">
130
+ {language === 'fr' ? 'Votre réponse' : language === 'en' ? 'Your answer' : 'Tu respuesta'}:
131
+ </h2>
132
+ <p className="text-xl text-white roboto-slab whitespace-pre-wrap mb-8">
133
+ {answer}
134
+ </p>
135
+
136
+ {/* Flèche à droite */}
137
+ <div className="absolute bottom-5 right-5">
138
+ <button
139
+ onClick={setNextScene}
140
+ disabled={!buttonEnabled || (currentQuestion === "" && round < 3)}
141
+ className={`text-white transition-colors ${
142
+ buttonEnabled && (currentQuestion !== "" || round === 3)
143
+ ? 'hover:text-blue-400 cursor-pointer'
144
+ : 'text-gray-600 cursor-not-allowed'
145
+ }`}
146
+ aria-label="Continuer"
147
+ >
148
+ {!buttonEnabled ? (
149
+ <span className="text-2xl">{countdown}s</span>
150
+ ) : (
151
+ <svg
152
+ xmlns="http://www.w3.org/2000/svg"
153
+ className="h-12 w-12"
154
+ fill="none"
155
+ viewBox="0 0 24 24"
156
+ stroke="currentColor"
157
+ >
158
+ <path
159
+ strokeLinecap="round"
160
+ strokeLinejoin="round"
161
+ strokeWidth={2}
162
+ d="M14 5l7 7m0 0l-7 7m7-7H3"
163
+ />
164
+ </svg>
165
+ )}
166
+ </button>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ );
174
+ };
175
+
176
+ export default LawyerScene;
src/components/menu/Menu.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { FC } from 'react';
4
+ import Image from 'next/image';
5
+
6
+ interface MenuSceneProps {
7
+ language: 'fr' | 'en' | 'es';
8
+ setLanguage: (lang: 'fr' | 'en' | 'es') => void;
9
+ setNextScene: () => void;
10
+ }
11
+
12
+ const MenuScene: FC<MenuSceneProps> = ({ setLanguage, setNextScene }) => {
13
+
14
+ const handleLanguageSelect = (language: 'fr' | 'en' | 'es') => {
15
+ setLanguage(language);
16
+ setNextScene();
17
+ };
18
+
19
+ return (
20
+ <div className="relative w-screen h-screen">
21
+ {/* Image de fond */}
22
+ <Image
23
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_start_P_osNnWmM.png?updatedAt=1737835883339"
24
+ alt="Background"
25
+ fill
26
+ className="object-cover"
27
+ priority
28
+ />
29
+
30
+ {/* Contenu du menu avec un fond semi-transparent */}
31
+ <div className="relative z-10 flex flex-col items-end justify-center h-full w-full">
32
+ <div className="flex flex-col gap-10 mr-[20vw]">
33
+ <button
34
+ onClick={() => handleLanguageSelect('en')}
35
+ className="text-8xl text-white roboto-slab hover:text-sky-500 transition-colors"
36
+ >
37
+ English
38
+ </button>
39
+ <button
40
+ onClick={() => handleLanguageSelect('fr')}
41
+ className="text-8xl text-white roboto-slab hover:text-sky-500 transition-colors"
42
+ >
43
+ Français
44
+ </button>
45
+ <button
46
+ onClick={() => handleLanguageSelect('es')}
47
+ className="text-8xl text-white roboto-slab hover:text-sky-500 transition-colors"
48
+ >
49
+ Español
50
+ </button>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ );
55
+ };
56
+
57
+ export default MenuScene;
tailwind.config.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from "tailwindcss";
2
+
3
+ export default {
4
+ content: [
5
+ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6
+ "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7
+ "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8
+ ],
9
+ theme: {
10
+ extend: {
11
+ colors: {
12
+ background: "var(--background)",
13
+ foreground: "var(--foreground)",
14
+ },
15
+ },
16
+ },
17
+ plugins: [],
18
+ } satisfies Config;
tsconfig.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }