rolexx commited on
Commit
7afe4cc
·
1 Parent(s): 77857e9
.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="rqjQLRrhjdjh4wjBVJNaAxMWiCPqjWcn"
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: green
5
- colorTo: yellow
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,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "next": "15.1.6",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/eslintrc": "^3",
19
+ "@types/node": "^20",
20
+ "@types/react": "^19",
21
+ "@types/react-dom": "^19",
22
+ "eslint": "^9",
23
+ "eslint-config-next": "15.1.6",
24
+ "postcss": "^8",
25
+ "tailwindcss": "^3.4.1",
26
+ "typescript": "^5"
27
+ }
28
+ }
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,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: `En tant que juge, génère une question difficile concernant cette affaire:
24
+ ${story.description}
25
+
26
+ Alibis du suspect: ${story.alibi.join(', ')}
27
+ Problèmes dans sa défense: ${story.problematic.join(', ')}
28
+ ${chat.messages.length > 0 ? `\nHistorique de la discussion:\n${chatHistory}` : ''}
29
+
30
+ Génère aussi une liste de 3 mots ou expressions absurdes que l'avocat devra obligatoirement utiliser dans sa réponse.
31
+ Ces expressions doivent être décalées et mettre l'avocat dans l'embarras mais etre relativement réaliste à l'affaire.
32
+
33
+ Réponds en JSON avec ce format:
34
+ {
35
+ "question": "Ta question incisive de juge ici",
36
+ "words": ["expression1", "expression2", "expression3"]
37
+ }`,
38
+ en: `As a judge, generate a tough question about this case:
39
+ ${story.description}
40
+
41
+ Suspect's alibis: ${story.alibi.join(', ')}
42
+ Issues in their defense: ${story.problematic.join(', ')}
43
+ ${chat.messages.length > 0 ? `Previous answers:\n${chatHistory}` : ''}
44
+
45
+ Also generate a list of 3 absurd words or expressions that the lawyer must use in their response.
46
+ These expressions should be quirky and put the lawyer in an awkward position but be relatively realistic to the case.
47
+
48
+ Answer in JSON with this format:
49
+ {
50
+ "question": "Your incisive judge question here",
51
+ "words": ["expression1", "expression2", "expression3"]
52
+ }`,
53
+ es: `Como juez, genera una pregunta difícil sobre este caso:
54
+ ${story.description}
55
+
56
+ Coartadas del sospechoso: ${story.alibi.join(', ')}
57
+ Problemas en su defensa: ${story.problematic.join(', ')}
58
+ ${chat.messages.length > 0 ? `Respuestas previas:\n${chatHistory}` : ''}
59
+
60
+ También genera una lista de 3 palabras o expresiones absurdas que el abogado deberá usar en su respuesta.
61
+ Estas expresiones deben ser extrañas y poner al abogado en una situación incómoda pero realista à l'affaire.
62
+
63
+ Responde en JSON con este formato:
64
+ {
65
+ "question": "Tu pregunta incisiva de juez aquí",
66
+ "words": ["expresión1", "expresión2", "expresión3"]
67
+ }`
68
+ };
69
+
70
+ console.log('prompts:', prompts[language as Language])
71
+
72
+ const seed = Math.floor(Math.random() * 1000000);
73
+
74
+ console.log('seed:', seed)
75
+
76
+ const response = await mistral.chat.complete({
77
+ model: "mistral-small-latest",
78
+ messages: [{role: 'user', content: prompts[language as Language]}],
79
+ responseFormat: {type: 'json_object'},
80
+ randomSeed: seed,
81
+ });
82
+
83
+ console.log('response:', response)
84
+
85
+ const functionCall = response.choices?.[0]?.message.content;
86
+ const JSONResponse = functionCall ? JSON.parse(functionCall as string) : null;
87
+ console.log('functionCall:', functionCall)
88
+ console.log('JSONResponse:', JSONResponse)
89
+
90
+ return NextResponse.json(JSONResponse || {
91
+ 'question': 'Erreur de génération de question',
92
+ 'words': [],
93
+ 'status': 'error',
94
+ });
95
+
96
+ } catch (error: unknown) {
97
+ console.log('error:', error)
98
+ return NextResponse.json(
99
+ { error: 'Erreur lors de la génération de la question' },
100
+ { status: 500 }
101
+ );
102
+ }
103
+ }
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,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: `Daniel est accusé. Génère une histoire d'accusation criminelle.
22
+ L'histoire doit être cohérente et détaillée, avec des alibis crédibles et des problématiques pertinentes.
23
+ Réponds en français avec le JSON format :`,
24
+ en: `Daniel is accused. Generate a criminal accusation story.
25
+ The story must be coherent and detailed, with credible alibis and relevant issues.
26
+ Answer in English with the JSON format:`,
27
+ es: `Daniel está acusado. Genera una historia de acusación criminal.
28
+ La historia debe ser coherente y detallada, con coartadas creíbles y problemas relevantes.
29
+ Responde en español con el formato JSON:`
30
+ };
31
+
32
+ const chatPrompt = `${prompts[language as keyof typeof prompts] || prompts.fr}
33
+ accusation: {
34
+ description: String,
35
+ alibi: [<String>],
36
+ problematic: [<String>],
37
+ }`;
38
+
39
+ console.log('chatPrompt:', chatPrompt)
40
+
41
+ const seed = Math.floor(Math.random() * 1000000);
42
+
43
+ console.log('seed:', seed)
44
+
45
+ const response = await mistral.chat.complete({
46
+ model: "mistral-small-latest",
47
+ messages: [{role: 'user', content: chatPrompt}],
48
+ responseFormat: {type: 'json_object'},
49
+ randomSeed: seed,
50
+ });
51
+
52
+ console.log('response:', response)
53
+
54
+ const functionCall = response.choices?.[0]?.message.content;
55
+ const JSONResponse = functionCall ? JSON.parse(functionCall as string) : null;
56
+ console.log('functionCall:', functionCall)
57
+ console.log('JSONResponse:', JSONResponse)
58
+ const storyData: Story = JSONResponse?.accusation || {
59
+ description: "Erreur de génération",
60
+ alibi: [],
61
+ problematic: []
62
+ };
63
+
64
+ return NextResponse.json({
65
+ success: true,
66
+ story: storyData
67
+ });
68
+
69
+ } catch (error: unknown) {
70
+ console.log('error:', error)
71
+ return NextResponse.json(
72
+ { error: 'Erreur lors de la génération de l\'histoire' },
73
+ { status: 500 }
74
+ );
75
+ }
76
+ }
src/app/api/voice/route.ts ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+
3
+ const VOICES = {
4
+ fr: {
5
+ LAWYER_VOICE: {
6
+ id: "ThT5KcBeYPX3keUQqHPh",
7
+ volume: 0
8
+ },
9
+ GLITCH_VOICE: {
10
+ id: "XrExE9yKIg1WjnnlVkGX",
11
+ volume: -10
12
+ }
13
+ },
14
+ en: {
15
+ LAWYER_VOICE: {
16
+ id: "ThT5KcBeYPX3keUQqHPh",
17
+ volume: 0
18
+ },
19
+ GLITCH_VOICE: {
20
+ id: "XrExE9yKIg1WjnnlVkGX",
21
+ volume: -10
22
+ }
23
+ },
24
+ es: {
25
+ LAWYER_VOICE: {
26
+ id: "ThT5KcBeYPX3keUQqHPh",
27
+ volume: 0
28
+ },
29
+ GLITCH_VOICE: {
30
+ id: "XrExE9yKIg1WjnnlVkGX",
31
+ volume: -10
32
+ }
33
+ }
34
+ };
35
+
36
+
37
+ export async function POST(request: Request) {
38
+ try {
39
+ const { text, language = 'en' } = await request.json();
40
+
41
+ if (!VOICES[language as keyof typeof VOICES]) {
42
+ return NextResponse.json(
43
+ { error: 'Language not supported' },
44
+ { status: 400 }
45
+ );
46
+ }
47
+
48
+ const segments = text.split('*');
49
+ let finalText = '';
50
+
51
+ for (let i = 0; i < segments.length; i++) {
52
+ const segment = segments[i].trim();
53
+ if (segment !== "") {
54
+ if (i % 2 === 1) {
55
+ // Pour les segments glitch, on utilise une voix différente
56
+ const voiceConfig = VOICES[language as keyof typeof VOICES].GLITCH_VOICE;
57
+ const response = await fetch(
58
+ `https://api.elevenlabs.io/v1/text-to-speech/${voiceConfig.id}`,
59
+ {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Accept': 'audio/mpeg',
63
+ 'Content-Type': 'application/json',
64
+ 'xi-api-key': process.env.ELEVEN_LABS_API_KEY!
65
+ },
66
+ body: JSON.stringify({
67
+ text: segment,
68
+ model_id: "eleven_monolingual_v1",
69
+ voice_settings: {
70
+ stability: 0.5,
71
+ similarity_boost: 0.75
72
+ }
73
+ })
74
+ }
75
+ );
76
+
77
+ if (!response.ok) {
78
+ throw new Error('Failed to generate glitch voice');
79
+ }
80
+
81
+ const audioBuffer = await response.arrayBuffer();
82
+ return new NextResponse(audioBuffer, {
83
+ headers: {
84
+ 'Content-Type': 'audio/mpeg'
85
+ }
86
+ });
87
+ } else {
88
+ // Pour les segments normaux
89
+ // const voiceConfig = VOICES[language as keyof typeof VOICES].LAWYER_VOICE;
90
+ finalText += "..." + segment + "...";
91
+ }
92
+ }
93
+ }
94
+
95
+ // Si aucun segment glitch n'a été trouvé, on génère la voix normale
96
+ const response = await fetch(
97
+ `https://api.elevenlabs.io/v1/text-to-speech/${VOICES[language as keyof typeof VOICES].LAWYER_VOICE.id}`,
98
+ {
99
+ method: 'POST',
100
+ headers: {
101
+ 'Accept': 'audio/mpeg',
102
+ 'Content-Type': 'application/json',
103
+ 'xi-api-key': process.env.ELEVEN_LABS_API_KEY!
104
+ },
105
+ body: JSON.stringify({
106
+ text: finalText,
107
+ model_id: "eleven_monolingual_v1",
108
+ voice_settings: {
109
+ stability: 0.5,
110
+ similarity_boost: 0.75
111
+ }
112
+ })
113
+ }
114
+ );
115
+
116
+ if (!response.ok) {
117
+ throw new Error('Failed to generate voice');
118
+ }
119
+
120
+ const audioBuffer = await response.arrayBuffer();
121
+ return new NextResponse(audioBuffer, {
122
+ headers: {
123
+ 'Content-Type': 'audio/mpeg'
124
+ }
125
+ });
126
+
127
+ } catch (error) {
128
+ console.error('Voice generation error:', error);
129
+ return NextResponse.json(
130
+ { error: 'Failed to generate voice' },
131
+ { status: 500 }
132
+ );
133
+ }
134
+ }
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,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ problematic: string[];
21
+ };
22
+ }
23
+
24
+ interface Message {
25
+ content: string;
26
+ role: 'lawyer' | 'judge';
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: "Vous êtes un avocat de l'intelligence artificielle. Votre client est accusé d'un crime. Défendez-le devant le tribunal.",
38
+ start: "Commencer"
39
+ },
40
+ en: {
41
+ title: "The AI Lawyer",
42
+ description: "You are an artificial intelligence lawyer. Your client is accused of a crime. Defend them in court.",
43
+ start: "Start"
44
+ },
45
+ es: {
46
+ title: "El Abogado de la IA",
47
+ description: "Eres un abogado de inteligencia artificial. Tu cliente está acusado de un crimen. Defiéndelo en la corte.",
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 resetGame = () => {
69
+ setCurrentScene('menu');
70
+ setStory(null);
71
+ setChat({ messages: [] });
72
+ setLanguage('fr');
73
+ setRound(1);
74
+ setRequiredWords([]);
75
+ };
76
+
77
+ const setNextScene = () => {
78
+ if (currentScene === 'lawyer') {
79
+ if (round < 3) {
80
+ setRound(round + 1);
81
+ setCurrentScene('court');
82
+ } else {
83
+ setCurrentScene('end');
84
+ }
85
+ return;
86
+ }
87
+
88
+ if (currentScene === 'end') {
89
+ resetGame();
90
+ return;
91
+ }
92
+
93
+ const currentIndex = sceneOrder.indexOf(currentScene);
94
+ if (currentIndex !== -1 && currentIndex < sceneOrder.length - 1) {
95
+ setCurrentScene(sceneOrder[currentIndex + 1]);
96
+ }
97
+ };
98
+
99
+ // Props communs à passer aux composants
100
+ const commonProps = {
101
+ intro,
102
+ language,
103
+ setLanguage,
104
+ round,
105
+ setRound,
106
+ setCurrentScene,
107
+ setNextScene,
108
+ story,
109
+ currentQuestion,
110
+ setCurrentQuestion,
111
+ requiredWords,
112
+ setRequiredWords,
113
+ chat,
114
+ setChat,
115
+ };
116
+
117
+ useEffect(() => {
118
+ const fetchStory = async () => {
119
+ try {
120
+ const response = await fetch('/api/text/story', {
121
+ method: 'POST',
122
+ headers: {
123
+ 'Content-Type': 'application/json',
124
+ },
125
+ body: JSON.stringify({ language })
126
+ });
127
+
128
+ const data = await response.json();
129
+
130
+ if (data.success && data.story) {
131
+ setStory({
132
+ accusation: {
133
+ description: data.story.description,
134
+ alibi: data.story.alibi,
135
+ problematic: data.story.problematic,
136
+ }
137
+ });
138
+ }
139
+ } catch (error) {
140
+ console.error('Erreur lors de la récupération de l\'histoire:', error);
141
+ }
142
+ };
143
+
144
+ console.log('currentScene:', currentScene)
145
+
146
+ if (currentScene === 'intro') {
147
+ fetchStory();
148
+ }
149
+ // eslint-disable-next-line react-hooks/exhaustive-deps
150
+ }, [currentScene]); // on écoute les changements de currentScene
151
+
152
+ useEffect(() => {
153
+ const fetchQuestion = async () => {
154
+ try {
155
+ const response = await fetch('/api/text/question', {
156
+ method: 'POST',
157
+ headers: {
158
+ 'Content-Type': 'application/json',
159
+ },
160
+ body: JSON.stringify({
161
+ language,
162
+ story: story?.accusation,
163
+ chat: chat
164
+ })
165
+ });
166
+
167
+ const data = await response.json();
168
+ console.log('data:', data)
169
+ if (data.question && data.words) {
170
+ setCurrentQuestion(data.question);
171
+ setRequiredWords(data.words);
172
+ setChat(prevChat => ({
173
+ messages: [...prevChat.messages, { content: data.question, role: 'judge' }]
174
+ }));
175
+ }
176
+ } catch (error) {
177
+ console.error('Erreur lors de la récupération de la question:', error);
178
+ }
179
+ };
180
+
181
+ if ((currentScene === 'accusation' && story) || (currentScene === 'lawyer' && round < 3 && story)) {
182
+ console.log('fetchQuestion')
183
+ fetchQuestion();
184
+ }
185
+ // eslint-disable-next-line react-hooks/exhaustive-deps
186
+ }, [currentScene]);
187
+
188
+ useEffect(() => {
189
+ if (currentQuestion && requiredWords.length > 0) {
190
+ console.log('currentQuestion:', currentQuestion)
191
+ console.log('requiredWords:', requiredWords)
192
+ }
193
+ }, [currentQuestion, requiredWords])
194
+
195
+ switch (currentScene) {
196
+ case 'menu':
197
+ return <MenuScene {...commonProps} />;
198
+ case 'intro':
199
+ return <IntroScene {...commonProps} />;
200
+ case 'accusation':
201
+ return <AccusationScene {...commonProps} />;
202
+ case 'court':
203
+ return <CourtScene {...commonProps} />;
204
+ case 'defense':
205
+ return <DefenseScene {...commonProps} />;
206
+ case 'lawyer':
207
+ return <LawyerScene {...commonProps} />;
208
+ case 'end':
209
+ return <EndScene {...commonProps} />;
210
+ default:
211
+ return <MenuScene {...commonProps} />;
212
+ }
213
+ }
src/components/accusation/Accusation.tsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ problematic: string[];
10
+ };
11
+ }
12
+
13
+ interface AccusationSceneProps {
14
+ language: 'fr' | 'en' | 'es';
15
+ story: Story | null;
16
+ setNextScene: () => void;
17
+ }
18
+
19
+ const AccusationScene: FC<AccusationSceneProps> = ({
20
+ language,
21
+ story,
22
+ setNextScene,
23
+ }) => {
24
+ return (
25
+ <div className="relative w-screen h-screen">
26
+ {/* Image de fond */}
27
+ <Image
28
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_BG1_rva-mKDVA.jpg?updatedAt=1737835881047"
29
+ alt="Background"
30
+ fill
31
+ className="object-cover"
32
+ priority
33
+ />
34
+
35
+ {/* Overlay noir */}
36
+ <div className="absolute inset-0 bg-black/70">
37
+ {/* Contenu */}
38
+ <div className="relative z-10 flex flex-col items-center justify-center h-full p-8 space-y-8">
39
+ <div className="max-w-3xl w-full space-y-8">
40
+ {/* Description */}
41
+ <div>
42
+ <h2 className="text-2xl font-bold text-white mb-4 roboto-slab">
43
+ {language === 'fr' ? "Chef d'accusation" : language === 'en' ? 'Accusation' : 'Acusación'}
44
+ </h2>
45
+ <p className="text-xl text-white roboto-slab">
46
+ {story?.accusation.description}
47
+ </p>
48
+ </div>
49
+
50
+ {/* Alibis */}
51
+ <div>
52
+ <h2 className="text-2xl font-bold text-white mb-4 roboto-slab">
53
+ {language === 'fr' ? 'Alibis' : language === 'en' ? 'Alibis' : 'Coartadas'}
54
+ </h2>
55
+ <ul className="list-disc list-inside text-white space-y-2 roboto-slab">
56
+ {story?.accusation.alibi.map((alibi, index) => (
57
+ <li key={index} className="text-xl">{alibi}</li>
58
+ ))}
59
+ </ul>
60
+ </div>
61
+
62
+ {/* Points problématiques */}
63
+ <div>
64
+ <h2 className="text-2xl font-bold text-white mb-4 roboto-slab">
65
+ {language === 'fr' ? 'Points problématiques' : language === 'en' ? 'Problematic points' : 'Puntos problemáticos'}
66
+ </h2>
67
+ <ul className="list-disc list-inside text-white space-y-2 roboto-slab">
68
+ {story?.accusation.problematic.map((point, index) => (
69
+ <li key={index} className="text-xl">{point}</li>
70
+ ))}
71
+ </ul>
72
+ </div>
73
+ </div>
74
+
75
+ <button
76
+ onClick={setNextScene}
77
+ className="px-8 py-4 text-xl font-bold text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors roboto-slab"
78
+ >
79
+ {language === 'fr' ? 'Continuer' : language === 'en' ? 'Continue' : 'Continuar'}
80
+ </button>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ );
85
+ };
86
+
87
+ 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,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
9
+
10
+ const CourtScene: FC<CourtSceneProps> = ({
11
+ setNextScene,
12
+ currentQuestion
13
+ }) => {
14
+ const [showFirstImage, setShowFirstImage] = useState(true);
15
+
16
+ useEffect(() => {
17
+ const timer = setTimeout(() => {
18
+ setShowFirstImage(false);
19
+ }, 2000);
20
+
21
+ return () => clearTimeout(timer);
22
+ }, []);
23
+
24
+ return (
25
+ <div className="relative w-screen h-screen">
26
+ {/* Image de fond statique */}
27
+ <Image
28
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_judge2_FoYazvSFmu.png?updatedAt=1737835883172"
29
+ alt="Background"
30
+ fill
31
+ className="object-cover"
32
+ priority
33
+ />
34
+
35
+ {/* Image superposée avec transition */}
36
+ <div className={`absolute inset-0 transition-opacity duration-500 ${showFirstImage ? 'opacity-100' : 'opacity-0'}`}>
37
+ <Image
38
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_judge1_Bn04jNl_E.png?updatedAt=1737835883169"
39
+ alt="Overlay"
40
+ fill
41
+ className="object-cover"
42
+ priority
43
+ />
44
+ </div>
45
+
46
+ {/* Rectangle noir en bas */}
47
+ <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">
48
+ <div className="text-white roboto-slab">
49
+ <p className="text-4xl mb-4">
50
+ {currentQuestion ? currentQuestion : '...'}
51
+ </p>
52
+
53
+ {/* Flèche à droite */}
54
+ <div className="flex justify-end">
55
+ <button
56
+ onClick={setNextScene}
57
+ className="text-white hover:text-blue-400 transition-colors"
58
+ aria-label="Continuer"
59
+ >
60
+ <svg
61
+ xmlns="http://www.w3.org/2000/svg"
62
+ className="h-12 w-12"
63
+ fill="none"
64
+ viewBox="0 0 24 24"
65
+ stroke="currentColor"
66
+ >
67
+ <path
68
+ strokeLinecap="round"
69
+ strokeLinejoin="round"
70
+ strokeWidth={2}
71
+ d="M14 5l7 7m0 0l-7 7m7-7H3"
72
+ />
73
+ </svg>
74
+ </button>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ );
80
+ };
81
+
82
+ export default CourtScene;
src/components/defense/Defense.tsx ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ setRequiredWords: Dispatch<SetStateAction<string[]>>
21
+ }
22
+
23
+ const DefenseScene: FC<DefenseSceneProps> = ({
24
+ language,
25
+ requiredWords,
26
+ setNextScene,
27
+ setCurrentQuestion,
28
+ setRequiredWords,
29
+ setChat
30
+ }) => {
31
+ const [answer, setAnswer] = useState('');
32
+ const [insertedWords, setInsertedWords] = useState<boolean[]>([]);
33
+ const [countdown, setCountdown] = useState(60);
34
+ const [isTimeUp, setIsTimeUp] = useState(false);
35
+ const [wordPositions, setWordPositions] = useState<Array<{word: string; position: number}>>([]);
36
+ const [mandatoryWords, setMandatoryWords] = useState(requiredWords);
37
+ const [isLoading, setIsLoading] = useState(true);
38
+
39
+ // Initialisation des mots obligatoires
40
+ useEffect(() => {
41
+ if (requiredWords.length > 0) {
42
+ setMandatoryWords(requiredWords);
43
+ }
44
+ }, [requiredWords]);
45
+
46
+ // Génération des positions et initialisation
47
+ useEffect(() => {
48
+ if (mandatoryWords.length > 0) {
49
+ const positions = generateWordPositions(mandatoryWords);
50
+ setWordPositions(positions);
51
+ setInsertedWords(new Array(mandatoryWords.length).fill(false));
52
+ setCurrentQuestion("");
53
+ setIsLoading(false);
54
+ }
55
+ }, [mandatoryWords]); // eslint-disable-line react-hooks/exhaustive-deps
56
+
57
+ // Reset des required words après initialisation
58
+ useEffect(() => {
59
+ if (!isLoading && wordPositions.length > 0) {
60
+ setRequiredWords([]);
61
+ }
62
+ }, [isLoading, wordPositions.length]); // eslint-disable-line react-hooks/exhaustive-deps
63
+
64
+ // Timer et reset de la question
65
+ useEffect(() => {
66
+ // Timer
67
+ const timer = setInterval(() => {
68
+ setCountdown((prev) => {
69
+ if (prev <= 1) {
70
+ clearInterval(timer);
71
+ setIsTimeUp(true);
72
+
73
+ // Mettre à jour le chat et passer à la scène suivante
74
+ setChat(prevChat => ({
75
+ messages: [...prevChat.messages, { content: answer, role: 'lawyer' }]
76
+ }));
77
+ setNextScene();
78
+
79
+ return 0;
80
+ }
81
+ return prev - 1;
82
+ });
83
+ }, 1000);
84
+
85
+ return () => clearInterval(timer);
86
+ // eslint-disable-next-line react-hooks/exhaustive-deps
87
+ }, []); // Suppression de la dépendance answer
88
+
89
+ // Génère les positions pour les mots requis
90
+ const generateWordPositions = (words: string[]) => {
91
+ let currentPosition = 3; // On commence au 3ème mot minimum
92
+ return words.map((word, index) => {
93
+ const randomIncrement = Math.floor(Math.random() * 4) + 3; // Entre 3 et 6 mots d'écart
94
+ currentPosition += randomIncrement;
95
+ // Pour le dernier mot, assurons-nous qu'il n'est pas trop loin
96
+ if (index === words.length - 1) {
97
+ currentPosition = Math.min(currentPosition, 20); // Maximum 20 mots
98
+ }
99
+ return {
100
+ word,
101
+ position: currentPosition
102
+ };
103
+ });
104
+ };
105
+
106
+ // Fonction pour compter les mots
107
+ const countWords = (text: string) => {
108
+ // Remplacer temporairement les expressions requises par un seul mot
109
+ let modifiedText = text;
110
+ wordPositions.forEach(({ word }) => {
111
+ if (modifiedText.includes(word)) {
112
+ // Remplace l'expression complète par un placeholder unique
113
+ modifiedText = modifiedText.replace(word, 'SINGLEWORD');
114
+ }
115
+ });
116
+
117
+ // Maintenant compte les mots normalement
118
+ return modifiedText.trim().split(/\s+/).length || 0;
119
+ };
120
+
121
+ // Fonction pour vérifier si on doit insérer un mot obligatoire
122
+ const checkAndInsertRequiredWord = (text: string) => {
123
+ const currentWordCount = countWords(text);
124
+ let newText = text;
125
+ const newInsertedWords = [...insertedWords];
126
+ let wordInserted = false;
127
+
128
+ wordPositions.forEach((word, index) => {
129
+ if (!insertedWords[index] && currentWordCount === word.position && text.endsWith(' ')) {
130
+ newText = `${text}${word.word} `;
131
+ newInsertedWords[index] = true;
132
+ wordInserted = true;
133
+ }
134
+ });
135
+
136
+ if (wordInserted) {
137
+ setInsertedWords(newInsertedWords);
138
+ }
139
+
140
+ return newText;
141
+ };
142
+
143
+ // Fonction pour vérifier si on peut supprimer du texte
144
+ const canDeleteAt = (text: string, cursorPosition: number) => {
145
+ // const textBeforeCursor = text.substring(0, cursorPosition);
146
+ // const wordsBeforeCursor = countWords(textBeforeCursor);
147
+
148
+ return !wordPositions.some((word, index) => {
149
+ if (!insertedWords[index]) return false;
150
+
151
+ const wordStartPosition = text.indexOf(word.word);
152
+ const wordEndPosition = wordStartPosition + word.word.length;
153
+
154
+ return cursorPosition > wordStartPosition && cursorPosition <= wordEndPosition;
155
+ });
156
+ };
157
+
158
+ const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
159
+ const newText = e.target.value;
160
+ const cursorPosition = e.target.selectionStart;
161
+
162
+ // Si c'est une suppression
163
+ if (newText.length < answer.length) {
164
+ if (!canDeleteAt(answer, cursorPosition)) {
165
+ return;
166
+ }
167
+ }
168
+
169
+ // Insertion normale + vérification des mots obligatoires
170
+ const processedText = checkAndInsertRequiredWord(newText);
171
+ setAnswer(processedText);
172
+ };
173
+
174
+ const handleSubmit = (text: string) => {
175
+ setChat(prevChat => ({
176
+ messages: [...prevChat.messages, { content: text, role: 'lawyer' }]
177
+ }));
178
+ setNextScene();
179
+ };
180
+
181
+ const formatTime = (seconds: number) => {
182
+ const mins = Math.floor(seconds / 60);
183
+ const secs = seconds % 60;
184
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
185
+ };
186
+
187
+ // Vérifie si la réponse est valide pour soumission
188
+ const isAnswerValid = () => {
189
+ const lastWordPosition = Math.max(...wordPositions.map(word => word.position));
190
+ const currentWordCount = countWords(answer);
191
+ return currentWordCount >= lastWordPosition && insertedWords.every(inserted => inserted);
192
+ };
193
+
194
+ return (
195
+ <div className="relative w-screen h-screen">
196
+ {/* Image de fond */}
197
+ <Image
198
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_attorney2_gbcNJRrYM.png?updatedAt=1737835883087"
199
+ alt="Background"
200
+ fill
201
+ className="object-cover"
202
+ priority
203
+ />
204
+
205
+ {/* Contenu avec overlay noir */}
206
+ <div className="absolute inset-0">
207
+ {isLoading ? (
208
+ <div className="flex items-center justify-center h-full">
209
+ <div className="text-2xl text-gray-300">
210
+ {language === 'fr' ? 'Chargement...' : language === 'en' ? 'Loading...' : 'Cargando...'}
211
+ </div>
212
+ </div>
213
+ ) : (
214
+ <div className="flex flex-col justify-end p-8 h-full">
215
+ {/* Header avec le compte à rebours */}
216
+ <div className="flex justify-between items-center mb-6">
217
+ <div className={`text-8xl roboto-slab ${countdown < 10 ? 'text-red-500' : 'text-white'}`}>
218
+ {formatTime(countdown)}
219
+ </div>
220
+ </div>
221
+
222
+ {/* Prochain mot requis */}
223
+ <div className="mb-6">
224
+ {wordPositions.map((item, index) => {
225
+ // Ne montrer que le premier mot non inséré
226
+ if (insertedWords[index] || index > 0 && !insertedWords[index - 1]) return null;
227
+ const remainingWords = item.position - countWords(answer);
228
+ return (
229
+ <div key={index} className="bg-black/60 border border-black border-8 p-6 text-white">
230
+ <span className="text-5xl text-sky-500 roboto-slab mt-2">
231
+ {item.word.toUpperCase()}
232
+ </span>
233
+ <span className="text-5xl text-white roboto-slab mt-2"> {language === 'fr'
234
+ ? `dans `
235
+ : language === 'en'
236
+ ? `in `
237
+ : `en `
238
+ }</span>
239
+ <span className="text-5xl text-red-500 roboto-slab mt-2">{remainingWords}</span>
240
+ <span className="text-5xl text-white roboto-slab mt-2"> {language === 'fr'
241
+ ? ` mots`
242
+ : language === 'en'
243
+ ? ` words`
244
+ : ` palabras`
245
+ }</span>
246
+ </div>
247
+ );
248
+ }).filter(Boolean)[0]}
249
+ </div>
250
+
251
+ {/* Zone de texte avec bouton submit en position absolue */}
252
+ <div className="relative w-full mb-6">
253
+ <textarea
254
+ value={answer}
255
+ onChange={handleTextChange}
256
+ disabled={isTimeUp}
257
+ placeholder={language === 'fr'
258
+ ? "Écrivez votre défense ici..."
259
+ : language === 'en'
260
+ ? "Write your defense here..."
261
+ : "Write your defense here..."}
262
+ 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]"
263
+ />
264
+
265
+ {/* Bouton de soumission */}
266
+ <button
267
+ onClick={() => handleSubmit(answer)}
268
+ disabled={!isAnswerValid()}
269
+ className={`absolute bottom-5 right-5 px-8 py-4 rounded-lg text-xl transition-all duration-300 ${
270
+ isAnswerValid()
271
+ ? 'bg-sky-500 hover:bg-blue-700 cursor-pointer'
272
+ : 'bg-gray-600 cursor-not-allowed'
273
+ }`}
274
+ >
275
+ {language === 'fr' ? 'Soumettre' : language === 'en' ? 'Submit' : 'Submit'}
276
+ </button>
277
+ </div>
278
+ </div>
279
+ )}
280
+ </div>
281
+ </div>
282
+ );
283
+ };
284
+
285
+ 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,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ problematic: string[];
11
+ };
12
+ }
13
+
14
+ interface IntroSceneProps {
15
+ intro: {
16
+ fr: {
17
+ title: string;
18
+ description: string;
19
+ start: string;
20
+ };
21
+ en: {
22
+ title: string;
23
+ description: string;
24
+ start: string;
25
+ };
26
+ es: {
27
+ title: string;
28
+ description: string;
29
+ start: string;
30
+ };
31
+ },
32
+ language: 'fr' | 'en' | 'es';
33
+ setNextScene: () => void;
34
+ story: Story | null;
35
+ }
36
+
37
+ const IntroScene: FC<IntroSceneProps> = ({
38
+ intro,
39
+ language,
40
+ setNextScene,
41
+ story,
42
+ }) => {
43
+ const handleContinue = () => {
44
+ setNextScene();
45
+ };
46
+
47
+ return (
48
+ <div className="relative w-screen h-screen">
49
+ {/* Image de fond */}
50
+ <Image
51
+ 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"
52
+ alt="Background"
53
+ fill
54
+ className="object-cover"
55
+ priority
56
+ />
57
+
58
+ {/* Overlay noir */}
59
+ <div className="absolute inset-0 bg-black/70">
60
+ {/* Contenu */}
61
+ <div className="relative z-10 flex flex-col items-center justify-center h-full p-4">
62
+ <p className="text-4xl text-white text-center mb-8 roboto-slab max-w-2xl">
63
+ {intro[language].description}
64
+ </p>
65
+
66
+ <button
67
+ onClick={handleContinue}
68
+ className={`px-8 py-4 text-xl font-bold text-white rounded-lg transition-colors roboto-slab
69
+ ${story ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-500 cursor-not-allowed'}`}
70
+ disabled={!story}
71
+ >
72
+ {intro[language].start}
73
+ </button>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ );
78
+ };
79
+
80
+ export default IntroScene;
src/components/lawyer/Lawyer.tsx ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { FC, useState, useEffect } 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 LawyerSceneProps {
15
+ language: 'fr' | 'en' | 'es';
16
+ chat: Chat;
17
+ setNextScene: () => void;
18
+ currentQuestion: string;
19
+ round: number;
20
+ }
21
+
22
+ const LawyerScene: FC<LawyerSceneProps> = ({
23
+ language,
24
+ chat,
25
+ setNextScene,
26
+ currentQuestion,
27
+ round,
28
+ }) => {
29
+ const [visible, setVisible] = useState(false);
30
+ const [buttonEnabled, setButtonEnabled] = useState(false);
31
+ const [countdown, setCountdown] = useState(3);
32
+
33
+ useEffect(() => {
34
+ setVisible(true);
35
+ const timer = setInterval(() => {
36
+ setCountdown((prev) => {
37
+ if (prev <= 1) {
38
+ setButtonEnabled(true);
39
+ clearInterval(timer);
40
+ return 0;
41
+ }
42
+ return prev - 1;
43
+ });
44
+ }, 1000);
45
+
46
+ return () => clearInterval(timer);
47
+ }, []);
48
+
49
+ return (
50
+ <div className="relative w-screen h-screen">
51
+ {/* Image de fond */}
52
+ <Image
53
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_attorney1_k5DWtEzcV.png?updatedAt=1737835883169"
54
+ alt="Background"
55
+ fill
56
+ className="object-cover"
57
+ priority
58
+ />
59
+
60
+ {/* Contenu avec overlay noir */}
61
+ <div className="absolute inset-0">
62
+ <div className="flex flex-col items-center justify-end h-full p-8">
63
+ <div className={`flex flex-col items-center justify-center space-y-8 transition-opacity duration-1000 ${
64
+ visible ? 'opacity-100' : 'opacity-0'
65
+ }`}>
66
+ {/* Question du juge */}
67
+ <div className="bg-black/60 border border-black border-8 p-6 mb-8 w-[80%]">
68
+ <h2 className="text-2xl font-bold mb-4 roboto-slab text-white">
69
+ {language === 'fr' ? 'Question du juge' : language === 'en' ? 'Judge\'s question' : 'Pregunta del juez'}:
70
+ </h2>
71
+ <p className="text-xl text-white roboto-slab">
72
+ {chat.messages.find((message: Message) => message.role === 'judge')?.content}
73
+ </p>
74
+ </div>
75
+
76
+ {/* Réponse de l'avocat avec flèche */}
77
+ <div className="relative bg-black/60 border border-black border-8 p-6 mb-8 w-[80%]">
78
+ <h2 className="text-2xl font-bold mb-4 roboto-slab text-white">
79
+ {language === 'fr' ? 'Votre réponse' : language === 'en' ? 'Your answer' : 'Tu respuesta'}:
80
+ </h2>
81
+ <p className="text-xl text-white roboto-slab whitespace-pre-wrap mb-8">
82
+ {chat.messages.find((message: Message) => message.role === 'lawyer')?.content}
83
+ </p>
84
+
85
+ {/* Flèche à droite */}
86
+ <div className="absolute bottom-5 right-5">
87
+ <button
88
+ onClick={setNextScene}
89
+ disabled={!buttonEnabled || (currentQuestion === "" && round < 3)}
90
+ className={`text-white transition-colors ${
91
+ buttonEnabled && (currentQuestion !== "" || round === 3)
92
+ ? 'hover:text-blue-400 cursor-pointer'
93
+ : 'text-gray-600 cursor-not-allowed'
94
+ }`}
95
+ aria-label="Continuer"
96
+ >
97
+ {!buttonEnabled ? (
98
+ <span className="text-2xl">{countdown}s</span>
99
+ ) : (
100
+ <svg
101
+ xmlns="http://www.w3.org/2000/svg"
102
+ className="h-12 w-12"
103
+ fill="none"
104
+ viewBox="0 0 24 24"
105
+ stroke="currentColor"
106
+ >
107
+ <path
108
+ strokeLinecap="round"
109
+ strokeLinejoin="round"
110
+ strokeWidth={2}
111
+ d="M14 5l7 7m0 0l-7 7m7-7H3"
112
+ />
113
+ </svg>
114
+ )}
115
+ </button>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ );
123
+ };
124
+
125
+ export default LawyerScene;
src/components/menu/Menu.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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> = ({ language, setLanguage, setNextScene }) => {
13
+ return (
14
+ <div className="relative w-screen h-screen">
15
+ {/* Image de fond */}
16
+ <Image
17
+ src="https://ik.imagekit.io/z0tzxea0wgx/MistralGameJam/DD_start_P_osNnWmM.png?updatedAt=1737835883339"
18
+ alt="Background"
19
+ fill
20
+ className="object-cover"
21
+ priority
22
+ />
23
+
24
+ {/* Contenu du menu avec un fond semi-transparent */}
25
+ <div className="relative z-10 flex flex-col items-end justify-center h-full w-full">
26
+ {/* Sélecteur de langue */}
27
+
28
+ <div className='flex flex-col items-center justify-center w-[20%] mr-[20%]'>
29
+ <div className='w-full'>
30
+ <select
31
+ value={language}
32
+ onChange={(e) => setLanguage(e.target.value as 'fr' | 'en' | 'es')}
33
+ className="w-full px-4 py-2 rounded-lg bg-white text-slate-900 border border-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
34
+ >
35
+ <option value="fr">Français</option>
36
+ <option value="en">English</option>
37
+ <option value="es">Espanol</option>
38
+ </select>
39
+ </div>
40
+
41
+ {/* Bouton Play */}
42
+ <button
43
+ onClick={setNextScene}
44
+ className="px-8 py-4 text-xl w-full font-bold text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors mt-8"
45
+ >
46
+ {language === 'fr' ? 'Jouer' : language === 'en' ? 'Play' : 'Jugar'}
47
+ </button>
48
+ </div>
49
+
50
+ </div>
51
+ </div>
52
+ );
53
+ };
54
+
55
+ 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
+ }