Spaces:
Sleeping
Sleeping
wip
Browse files- .dockerignore +26 -0
- .env +2 -0
- .gitattributes +2 -0
- .gitignore +38 -0
- Dockerfile +25 -0
- README.md +7 -5
- eslint.config.mjs +16 -0
- next.config.js +14 -0
- next.config.ts +14 -0
- package-lock.json +0 -0
- package.json +28 -0
- pnpm-lock.yaml +0 -0
- postcss.config.mjs +8 -0
- public/file.svg +1 -0
- public/globe.svg +1 -0
- public/next.svg +1 -0
- public/vercel.svg +1 -0
- public/window.svg +1 -0
- src/app/GameContext.tsx +105 -0
- src/app/api/text/question/route.ts +103 -0
- src/app/api/text/route.ts +30 -0
- src/app/api/text/story/route.ts +76 -0
- src/app/api/voice/route.ts +134 -0
- src/app/favicon.ico +0 -0
- src/app/globals.css +46 -0
- src/app/layout.tsx +63 -0
- src/app/page.tsx +213 -0
- src/components/accusation/Accusation.tsx +87 -0
- src/components/common/Layout.tsx +19 -0
- src/components/components/LanguageSelector.tsx +29 -0
- src/components/court/Court.tsx +82 -0
- src/components/defense/Defense.tsx +285 -0
- src/components/end/End.tsx +50 -0
- src/components/intro/Intro.tsx +80 -0
- src/components/lawyer/Lawyer.tsx +125 -0
- src/components/menu/Menu.tsx +55 -0
- tailwind.config.ts +18 -0
- tsconfig.json +27 -0
.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:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
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 |
+
}
|