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