tfrere commited on
Commit
2f568d8
·
1 Parent(s): f343951
client/src/components/StoryChoices.jsx CHANGED
@@ -1,5 +1,7 @@
1
- import { Box, Button, Typography, Chip } from "@mui/material";
2
  import { useNavigate } from "react-router-dom";
 
 
3
 
4
  // Function to convert text with ** to Chip elements
5
  const formatTextWithBold = (text) => {
@@ -34,8 +36,13 @@ export function StoryChoices({
34
  isDeath = false,
35
  isVictory = false,
36
  containerRef,
 
 
 
 
37
  }) {
38
  const navigate = useNavigate();
 
39
 
40
  if (isGameOver) {
41
  return (
@@ -138,48 +145,83 @@ export function StoryChoices({
138
  overflowY: "auto",
139
  }}
140
  >
141
- {choices.map((choice, index) => (
142
- <Box
143
- key={choice.id}
144
- sx={{
145
- display: "flex",
146
- flexDirection: "column",
147
- alignItems: "center",
148
- gap: 1,
149
- width: "100%",
150
- minHeight: "fit-content",
151
- }}
152
- >
153
- <Typography variant="caption" sx={{ opacity: 0.7, color: "white" }}>
154
- Choice {index + 1}
155
- </Typography>
156
- <Button
157
- variant="outlined"
158
- size="large"
159
- onClick={() => onChoice(choice.id)}
160
- disabled={disabled}
161
  sx={{
 
 
 
 
162
  width: "100%",
163
- textTransform: "none",
164
- cursor: "pointer",
165
- fontSize: "1.1rem",
166
- padding: "16px 24px",
167
- lineHeight: 1.3,
168
- color: "white",
169
- borderColor: "rgba(255, 255, 255, 0.23)",
170
- "&:hover": {
171
- borderColor: "white",
172
- backgroundColor: "rgba(255, 255, 255, 0.05)",
173
- },
174
- "& .MuiChip-root": {
 
 
 
175
  fontSize: "1.1rem",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  },
177
  }}
178
  >
179
- {formatTextWithBold(choice.text)}
180
- </Button>
181
- </Box>
182
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  </Box>
184
  );
185
  }
 
1
+ import { Box, Button, Typography, Chip, Divider } from "@mui/material";
2
  import { useNavigate } from "react-router-dom";
3
+ import { TalkWithSarah } from "./TalkWithSarah";
4
+ import { useState } from "react";
5
 
6
  // Function to convert text with ** to Chip elements
7
  const formatTextWithBold = (text) => {
 
36
  isDeath = false,
37
  isVictory = false,
38
  containerRef,
39
+ isNarratorSpeaking = false,
40
+ stopNarration = () => {},
41
+ playNarration = () => {},
42
+ storyText = "",
43
  }) {
44
  const navigate = useNavigate();
45
+ const [isSarahActive, setIsSarahActive] = useState(false);
46
 
47
  if (isGameOver) {
48
  return (
 
145
  overflowY: "auto",
146
  }}
147
  >
148
+ {!disabled &&
149
+ choices.map((choice, index) => (
150
+ <Box
151
+ key={choice.id}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  sx={{
153
+ display: "flex",
154
+ flexDirection: "column",
155
+ alignItems: "center",
156
+ gap: 1,
157
  width: "100%",
158
+ minHeight: "fit-content",
159
+ }}
160
+ >
161
+ <Typography variant="caption" sx={{ opacity: 0.7, color: "white" }}>
162
+ Choice {index + 1}
163
+ </Typography>
164
+ <Button
165
+ variant="outlined"
166
+ size="large"
167
+ onClick={() => onChoice(choice.id)}
168
+ disabled={isSarahActive}
169
+ sx={{
170
+ width: "100%",
171
+ textTransform: "none",
172
+ cursor: "pointer",
173
  fontSize: "1.1rem",
174
+ padding: "16px 24px",
175
+ lineHeight: 1.3,
176
+ borderColor: "primary.main",
177
+ "&:hover": {
178
+ borderColor: "primary.light",
179
+ backgroundColor: "rgba(255, 255, 255, 0.05)",
180
+ },
181
+ "& .MuiChip-root": {
182
+ fontSize: "1.1rem",
183
+ },
184
+ }}
185
+ >
186
+ {formatTextWithBold(choice.text)}
187
+ </Button>
188
+ </Box>
189
+ ))}
190
+
191
+ {!disabled && storyText && (
192
+ <>
193
+ <Divider
194
+ sx={{
195
+ width: "100%",
196
+ my: 3,
197
+ "&::before, &::after": {
198
+ borderColor: "rgba(255, 255, 255, 0.1)",
199
  },
200
  }}
201
  >
202
+ <Typography
203
+ variant="caption"
204
+ sx={{
205
+ color: "rgba(255, 255, 255, 0.5)",
206
+ px: 1,
207
+ fontSize: "0.8rem",
208
+ }}
209
+ >
210
+ OR
211
+ </Typography>
212
+ </Divider>
213
+ <TalkWithSarah
214
+ isNarratorSpeaking={isNarratorSpeaking}
215
+ stopNarration={stopNarration}
216
+ playNarration={playNarration}
217
+ onDecisionMade={onChoice}
218
+ onSarahActiveChange={setIsSarahActive}
219
+ currentContext={`You are Sarah and this is the situation you're in : ${storyText}. Those are your possible decisions : \n ${choices
220
+ .map((choice, index) => `decision ${index + 1} : ${choice.text}`)
221
+ .join("\n ")}.`}
222
+ />
223
+ </>
224
+ )}
225
  </Box>
226
  );
227
  }
client/src/components/TalkWithSarah.jsx CHANGED
@@ -1,21 +1,33 @@
1
- import { useConversation } from '@11labs/react';
2
- import CancelIcon from '@mui/icons-material/Cancel';
3
- import CheckCircleIcon from '@mui/icons-material/CheckCircle';
4
- import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
5
- import { Box, IconButton, TextField, Tooltip } from '@mui/material';
6
- import { useEffect, useRef, useState } from 'react';
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- import { getSarahPrompt, SARAH_FIRST_MESSAGE } from '../prompts/sarahPrompt';
9
 
10
  const AGENT_ID = "2MF9st3s1mNFbX01Y106";
11
  const ELEVEN_LABS_KEY_STORAGE = "eleven_labs_api_key";
12
 
13
- export function TalkWithSarah({
14
  isNarratorSpeaking,
15
  stopNarration,
16
  playNarration,
17
  onDecisionMade,
18
- currentContext
 
19
  }) {
20
  const [isRecording, setIsRecording] = useState(false);
21
  const [isConversationMode, setIsConversationMode] = useState(false);
@@ -27,10 +39,18 @@ export function TalkWithSarah({
27
  const mediaRecorderRef = useRef(null);
28
  const audioChunksRef = useRef([]);
29
 
 
 
 
 
 
 
 
 
30
  const conversation = useConversation({
31
  agentId: AGENT_ID,
32
  headers: {
33
- 'xi-api-key': apiKey
34
  },
35
  onResponse: async (response) => {
36
  if (response.type === "audio") {
@@ -47,9 +67,13 @@ export function TalkWithSarah({
47
  clientTools: {
48
  make_decision: async ({ decision }) => {
49
  console.log("AI made decision:", decision);
50
- // Stop recording
51
- if (mediaRecorderRef.current && mediaRecorderRef.current.state === "recording") {
 
 
 
52
  mediaRecorderRef.current.stop();
 
53
  }
54
  setIsConversationMode(false);
55
  await conversation?.endSession();
@@ -62,10 +86,10 @@ export function TalkWithSarah({
62
  // Valider la clé API
63
  const validateApiKey = async (key) => {
64
  try {
65
- const response = await fetch('https://api.elevenlabs.io/v1/user', {
66
  headers: {
67
- 'xi-api-key': key
68
- }
69
  });
70
  return response.ok;
71
  } catch (e) {
@@ -97,19 +121,27 @@ export function TalkWithSarah({
97
  }
98
  }, [apiKey]);
99
 
 
 
 
 
 
100
  const startRecording = async () => {
101
- if (!apiKey) {
102
  setShowApiKeyDialog(true);
103
  return;
104
  }
105
 
106
  try {
107
  setIsRecording(true);
 
 
 
108
  // Stop narration audio if it's playing
109
  if (isNarratorSpeaking) {
110
  stopNarration();
111
  }
112
-
113
  // Safely stop any conversation audio if playing
114
  if (conversation?.audioRef?.current) {
115
  conversation.audioRef.current.pause();
@@ -125,17 +157,18 @@ export function TalkWithSarah({
125
  await conversation.startSession({
126
  agentId: AGENT_ID,
127
  overrides: {
128
- agent: {
129
  firstMessage: SARAH_FIRST_MESSAGE,
130
  prompt: {
131
- prompt: getSarahPrompt(currentContext),
132
- },
133
  },
 
134
  },
135
- });
136
  console.log("ElevenLabs WebSocket connected");
137
  } catch (error) {
138
  console.error("Error starting conversation:", error);
 
139
  return;
140
  }
141
  }
@@ -170,6 +203,7 @@ export function TalkWithSarah({
170
  });
171
  } catch (error) {
172
  console.error("Error sending audio to ElevenLabs:", error);
 
173
  }
174
  }
175
  };
@@ -178,87 +212,80 @@ export function TalkWithSarah({
178
  mediaRecorderRef.current.start();
179
  } catch (error) {
180
  console.error("Error starting recording:", error);
 
 
181
  }
182
  };
183
 
184
  return (
185
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
186
- <Box sx={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
187
- <TextField
188
- size="small"
189
- type="password"
190
- placeholder="Enter your ElevenLabs API key"
191
- value={apiKey}
192
- onChange={(e) => setApiKey(e.target.value)}
193
- sx={{
194
- width: '300px',
195
- '& .MuiOutlinedInput-root': {
196
- color: 'white',
197
- '& fieldset': {
198
- borderColor: 'rgba(255, 255, 255, 0.23)',
199
- },
200
- '&:hover fieldset': {
201
- borderColor: 'white',
202
- },
203
- '&.Mui-focused fieldset': {
204
- borderColor: 'white',
205
- },
206
- '& .MuiOutlinedInput-input': {
207
- paddingRight: apiKey ? '40px' : '14px', // Padding dynamique
208
- },
209
- },
210
- '& .MuiInputBase-input': {
211
- color: 'white',
212
- '&::placeholder': {
213
- color: 'rgba(255, 255, 255, 0.5)',
214
- opacity: 1,
215
- },
216
- },
217
- }}
218
- />
219
- {apiKey && (
220
- <Tooltip title={isApiKeyValid ? "API key is valid" : "Invalid API key"}>
221
- <Box
222
- sx={{
223
- position: 'absolute',
224
- right: 10,
225
- pointerEvents: 'none',
226
- display: 'flex',
227
- alignItems: 'center',
228
- backgroundColor: 'rgba(0, 0, 0, 0.8)',
229
- borderRadius: '50%',
230
- padding: '2px'
231
- }}
232
- >
233
- {isApiKeyValid ? (
234
- <CheckCircleIcon sx={{ color: '#4caf50', fontSize: 20 }} />
235
- ) : (
236
- <CancelIcon sx={{ color: '#f44336', fontSize: 20 }} />
237
- )}
238
- </Box>
239
- </Tooltip>
240
- )}
241
- </Box>
242
- <IconButton
243
  onClick={startRecording}
244
- disabled={isRecording || !isApiKeyValid}
 
 
 
245
  sx={{
246
- color: "white",
247
- backgroundColor: isRecording ? "primary.main" : "transparent",
 
 
 
 
 
248
  "&:hover": {
249
- backgroundColor: isRecording ? "primary.dark" : "rgba(0, 0, 0, 0.7)",
 
250
  },
251
- px: 2,
252
- borderRadius: 2,
253
- border: "1px solid white",
254
- opacity: !isApiKeyValid ? 0.5 : 1,
255
  }}
256
  >
257
  <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
258
- {isRecording ? <FiberManualRecordIcon sx={{ color: "red" }} /> : null}
259
- <span style={{ fontSize: "1rem" }}>Talk with Sarah</span>
 
 
260
  </Box>
261
- </IconButton>
262
- </Box>
263
  );
264
  }
 
1
+ import { useConversation } from "@11labs/react";
2
+ import CancelIcon from "@mui/icons-material/Cancel";
3
+ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
4
+ import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
5
+ import {
6
+ Box,
7
+ IconButton,
8
+ TextField,
9
+ Tooltip,
10
+ Button,
11
+ Dialog,
12
+ DialogTitle,
13
+ DialogContent,
14
+ DialogActions,
15
+ } from "@mui/material";
16
+ import { useEffect, useRef, useState } from "react";
17
+ import { useSound } from "use-sound";
18
 
19
+ import { getSarahPrompt, SARAH_FIRST_MESSAGE } from "../prompts/sarahPrompt";
20
 
21
  const AGENT_ID = "2MF9st3s1mNFbX01Y106";
22
  const ELEVEN_LABS_KEY_STORAGE = "eleven_labs_api_key";
23
 
24
+ export function TalkWithSarah({
25
  isNarratorSpeaking,
26
  stopNarration,
27
  playNarration,
28
  onDecisionMade,
29
+ currentContext,
30
+ onSarahActiveChange,
31
  }) {
32
  const [isRecording, setIsRecording] = useState(false);
33
  const [isConversationMode, setIsConversationMode] = useState(false);
 
39
  const mediaRecorderRef = useRef(null);
40
  const audioChunksRef = useRef([]);
41
 
42
+ // Sons de communication
43
+ const [playStartComm] = useSound("/sounds/talky-walky-on.mp3", {
44
+ volume: 0.5,
45
+ });
46
+ const [playEndComm] = useSound("/sounds/talky-walky-off.mp3", {
47
+ volume: 0.5,
48
+ });
49
+
50
  const conversation = useConversation({
51
  agentId: AGENT_ID,
52
  headers: {
53
+ "xi-api-key": apiKey,
54
  },
55
  onResponse: async (response) => {
56
  if (response.type === "audio") {
 
67
  clientTools: {
68
  make_decision: async ({ decision }) => {
69
  console.log("AI made decision:", decision);
70
+ // Stop recording and play end communication sound
71
+ if (
72
+ mediaRecorderRef.current &&
73
+ mediaRecorderRef.current.state === "recording"
74
+ ) {
75
  mediaRecorderRef.current.stop();
76
+ playEndComm();
77
  }
78
  setIsConversationMode(false);
79
  await conversation?.endSession();
 
86
  // Valider la clé API
87
  const validateApiKey = async (key) => {
88
  try {
89
+ const response = await fetch("https://api.elevenlabs.io/v1/user", {
90
  headers: {
91
+ "xi-api-key": key,
92
+ },
93
  });
94
  return response.ok;
95
  } catch (e) {
 
121
  }
122
  }, [apiKey]);
123
 
124
+ useEffect(() => {
125
+ // Notify parent component when Sarah's state changes
126
+ onSarahActiveChange?.(isRecording || isConversationMode);
127
+ }, [isRecording, isConversationMode, onSarahActiveChange]);
128
+
129
  const startRecording = async () => {
130
+ if (!apiKey || !isApiKeyValid) {
131
  setShowApiKeyDialog(true);
132
  return;
133
  }
134
 
135
  try {
136
  setIsRecording(true);
137
+ // Play start communication sound
138
+ playStartComm();
139
+
140
  // Stop narration audio if it's playing
141
  if (isNarratorSpeaking) {
142
  stopNarration();
143
  }
144
+
145
  // Safely stop any conversation audio if playing
146
  if (conversation?.audioRef?.current) {
147
  conversation.audioRef.current.pause();
 
157
  await conversation.startSession({
158
  agentId: AGENT_ID,
159
  overrides: {
160
+ agent: {
161
  firstMessage: SARAH_FIRST_MESSAGE,
162
  prompt: {
163
+ prompt: getSarahPrompt(currentContext),
 
164
  },
165
+ },
166
  },
167
+ });
168
  console.log("ElevenLabs WebSocket connected");
169
  } catch (error) {
170
  console.error("Error starting conversation:", error);
171
+ playEndComm(); // Play end sound if connection fails
172
  return;
173
  }
174
  }
 
203
  });
204
  } catch (error) {
205
  console.error("Error sending audio to ElevenLabs:", error);
206
+ playEndComm(); // Play end sound if sending fails
207
  }
208
  }
209
  };
 
212
  mediaRecorderRef.current.start();
213
  } catch (error) {
214
  console.error("Error starting recording:", error);
215
+ playEndComm(); // Play end sound if there's an error
216
+ setIsRecording(false);
217
  }
218
  };
219
 
220
  return (
221
+ <>
222
+ <Dialog
223
+ open={showApiKeyDialog}
224
+ onClose={() => setShowApiKeyDialog(false)}
225
+ >
226
+ <DialogTitle>ElevenLabs API Key Required</DialogTitle>
227
+ <DialogContent>
228
+ <TextField
229
+ autoFocus
230
+ margin="dense"
231
+ label="Enter your ElevenLabs API key"
232
+ type="password"
233
+ fullWidth
234
+ variant="outlined"
235
+ value={apiKey}
236
+ onChange={(e) => setApiKey(e.target.value)}
237
+ error={apiKey !== "" && !isApiKeyValid}
238
+ helperText={
239
+ apiKey !== "" && !isApiKeyValid
240
+ ? "Invalid API key"
241
+ : "You can find your API key in your ElevenLabs account settings"
242
+ }
243
+ />
244
+ </DialogContent>
245
+ <DialogActions>
246
+ <Button onClick={() => setShowApiKeyDialog(false)}>Cancel</Button>
247
+ <Button
248
+ onClick={async () => {
249
+ const isValid = await validateApiKey(apiKey);
250
+ if (isValid) {
251
+ setShowApiKeyDialog(false);
252
+ startRecording();
253
+ }
254
+ }}
255
+ disabled={!apiKey}
256
+ >
257
+ Validate & Start
258
+ </Button>
259
+ </DialogActions>
260
+ </Dialog>
261
+
262
+ <Button
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  onClick={startRecording}
264
+ disabled={isRecording}
265
+ variant="outlined"
266
+ size="large"
267
+ color="secondary"
268
  sx={{
269
+ width: "100%",
270
+ textTransform: "none",
271
+ cursor: "pointer",
272
+ fontSize: "1.1rem",
273
+ padding: "16px 24px",
274
+ lineHeight: 1.3,
275
+ borderColor: "secondary.main",
276
  "&:hover": {
277
+ borderColor: "secondary.light",
278
+ backgroundColor: "rgba(255, 255, 255, 0.05)",
279
  },
 
 
 
 
280
  }}
281
  >
282
  <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
283
+ {isRecording ? (
284
+ <FiberManualRecordIcon sx={{ color: "red", fontSize: "1.1rem" }} />
285
+ ) : null}
286
+ <span>Leave it to your consciousness</span>
287
  </Box>
288
+ </Button>
289
+ </>
290
  );
291
  }
client/src/layouts/ComicLayout.jsx CHANGED
@@ -16,6 +16,9 @@ function ComicPage({
16
  isLoading,
17
  showScreenshot,
18
  onScreenshot,
 
 
 
19
  }) {
20
  // Calculer le nombre total d'images dans tous les segments de ce layout
21
  const totalImages = layout.segments.reduce((total, segment) => {
@@ -127,6 +130,12 @@ function ComicPage({
127
  }
128
  isDeath={layout.segments[layout.segments.length - 1]?.isDeath}
129
  isVictory={layout.segments[layout.segments.length - 1]?.isVictory}
 
 
 
 
 
 
130
  />
131
  </Box>
132
  )}
@@ -142,6 +151,9 @@ export function ComicLayout({
142
  isLoading,
143
  showScreenshot,
144
  onScreenshot,
 
 
 
145
  }) {
146
  const scrollContainerRef = useRef(null);
147
 
@@ -221,6 +233,9 @@ export function ComicLayout({
221
  isLoading={isLoading}
222
  showScreenshot={showScreenshot}
223
  onScreenshot={onScreenshot}
 
 
 
224
  />
225
  ))}
226
  </Box>
 
16
  isLoading,
17
  showScreenshot,
18
  onScreenshot,
19
+ isNarratorSpeaking,
20
+ stopNarration,
21
+ playNarration,
22
  }) {
23
  // Calculer le nombre total d'images dans tous les segments de ce layout
24
  const totalImages = layout.segments.reduce((total, segment) => {
 
130
  }
131
  isDeath={layout.segments[layout.segments.length - 1]?.isDeath}
132
  isVictory={layout.segments[layout.segments.length - 1]?.isVictory}
133
+ isNarratorSpeaking={isNarratorSpeaking}
134
+ stopNarration={stopNarration}
135
+ playNarration={playNarration}
136
+ storyText={
137
+ layout.segments[layout.segments.length - 1]?.rawText || ""
138
+ }
139
  />
140
  </Box>
141
  )}
 
151
  isLoading,
152
  showScreenshot,
153
  onScreenshot,
154
+ isNarratorSpeaking,
155
+ stopNarration,
156
+ playNarration,
157
  }) {
158
  const scrollContainerRef = useRef(null);
159
 
 
233
  isLoading={isLoading}
234
  showScreenshot={showScreenshot}
235
  onScreenshot={onScreenshot}
236
+ isNarratorSpeaking={isNarratorSpeaking}
237
+ stopNarration={stopNarration}
238
+ playNarration={playNarration}
239
  />
240
  ))}
241
  </Box>
client/src/layouts/config.js CHANGED
@@ -1,7 +1,9 @@
1
  // Panel size constants
2
  export const PANEL_SIZES = {
3
  PORTRAIT: { width: 512, height: 768 },
 
4
  LANDSCAPE: { width: 768, height: 512 },
 
5
  COVER_SIZE: { width: 512, height: 768 },
6
  };
7
 
@@ -72,8 +74,8 @@ export const LAYOUTS = {
72
  gridCols: 3,
73
  gridRows: 3,
74
  panels: [
75
- { ...PANEL_SIZES.LANDSCAPE, gridColumn: GRID.FULL_WIDTH, gridRow: "1" }, // Wide panoramic top
76
- { ...PANEL_SIZES.PORTRAIT, gridColumn: "1", gridRow: "2 / span 2" }, // Tall portrait left
77
  {
78
  ...PANEL_SIZES.LANDSCAPE,
79
  gridColumn: "2 / span 2",
 
1
  // Panel size constants
2
  export const PANEL_SIZES = {
3
  PORTRAIT: { width: 512, height: 768 },
4
+ COLUMN: { width: 512, height: 1024 },
5
  LANDSCAPE: { width: 768, height: 512 },
6
+ PANORAMIC: { width: 1024, height: 512 },
7
  COVER_SIZE: { width: 512, height: 768 },
8
  };
9
 
 
74
  gridCols: 3,
75
  gridRows: 3,
76
  panels: [
77
+ { ...PANEL_SIZES.PANORAMIC, gridColumn: GRID.FULL_WIDTH, gridRow: "1" }, // Wide panoramic top
78
+ { ...PANEL_SIZES.COLUMN, gridColumn: "1", gridRow: "2 / span 2" }, // Tall portrait left
79
  {
80
  ...PANEL_SIZES.LANDSCAPE,
81
  gridColumn: "2 / span 2",
client/src/pages/Game.jsx CHANGED
@@ -1,25 +1,25 @@
1
- import ArrowBackIcon from '@mui/icons-material/ArrowBack';
2
- import PhotoCameraOutlinedIcon from '@mui/icons-material/PhotoCameraOutlined';
3
- import VolumeOffIcon from '@mui/icons-material/VolumeOff';
4
- import VolumeUpIcon from '@mui/icons-material/VolumeUp';
5
- import { Box, IconButton, LinearProgress, Tooltip } from '@mui/material';
6
- import { motion } from 'framer-motion';
7
- import { useEffect, useRef, useState } from 'react';
8
- import { useNavigate } from 'react-router-dom';
9
-
10
- import { ErrorDisplay } from '../components/ErrorDisplay';
11
- import { LoadingScreen } from '../components/LoadingScreen';
12
- import { StoryChoices } from '../components/StoryChoices';
13
- import { TalkWithSarah } from '../components/TalkWithSarah';
14
- import { useGameSession } from '../hooks/useGameSession';
15
- import { useNarrator } from '../hooks/useNarrator';
16
- import { usePageSound } from '../hooks/usePageSound';
17
- import { useStoryCapture } from '../hooks/useStoryCapture';
18
- import { useTransitionSound } from '../hooks/useTransitionSound';
19
- import { useWritingSound } from '../hooks/useWritingSound';
20
- import { ComicLayout } from '../layouts/ComicLayout';
21
- import { getNextLayoutType, LAYOUTS } from '../layouts/config';
22
- import { storyApi } from '../utils/api';
23
 
24
  // Constants
25
  const SOUND_ENABLED_KEY = "sound_enabled";
@@ -418,11 +418,14 @@ export function Game() {
418
  <>
419
  <ComicLayout
420
  segments={storySegments}
421
- choices={showChoices ? currentChoices : []}
422
  onChoice={handleChoice}
423
  isLoading={isLoading}
424
  showScreenshot={storySegments.length > 0}
425
  onScreenshot={handleCaptureStory}
 
 
 
426
  />
427
 
428
  {showChoices && (
@@ -454,21 +457,6 @@ export function Game() {
454
  borderRadius: 1,
455
  }}
456
  >
457
- {storySegments.length > 0 && currentChoices.length > 0 && (
458
- <TalkWithSarah
459
- isNarratorSpeaking={isNarratorSpeaking}
460
- stopNarration={stopNarration}
461
- playNarration={playNarration}
462
- onDecisionMade={handleChoice}
463
- currentContext={`You are Sarah and this is the situation you're in : ${
464
- storySegments[storySegments.length - 1].text
465
- }. Those are your possible decisions : \n ${currentChoices
466
- .map((choice, index) => `decision ${index + 1} : ${choice.text}`)
467
- .join("\n ")}.`}
468
- />
469
- )}
470
-
471
-
472
  <Tooltip title="Save your story">
473
  <IconButton
474
  id="screenshot-button"
 
1
+ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
2
+ import PhotoCameraOutlinedIcon from "@mui/icons-material/PhotoCameraOutlined";
3
+ import VolumeOffIcon from "@mui/icons-material/VolumeOff";
4
+ import VolumeUpIcon from "@mui/icons-material/VolumeUp";
5
+ import { Box, IconButton, LinearProgress, Tooltip } from "@mui/material";
6
+ import { motion } from "framer-motion";
7
+ import { useEffect, useRef, useState } from "react";
8
+ import { useNavigate } from "react-router-dom";
9
+
10
+ import { ErrorDisplay } from "../components/ErrorDisplay";
11
+ import { LoadingScreen } from "../components/LoadingScreen";
12
+ import { StoryChoices } from "../components/StoryChoices";
13
+ import { TalkWithSarah } from "../components/TalkWithSarah";
14
+ import { useGameSession } from "../hooks/useGameSession";
15
+ import { useNarrator } from "../hooks/useNarrator";
16
+ import { usePageSound } from "../hooks/usePageSound";
17
+ import { useStoryCapture } from "../hooks/useStoryCapture";
18
+ import { useTransitionSound } from "../hooks/useTransitionSound";
19
+ import { useWritingSound } from "../hooks/useWritingSound";
20
+ import { ComicLayout } from "../layouts/ComicLayout";
21
+ import { getNextLayoutType, LAYOUTS } from "../layouts/config";
22
+ import { storyApi } from "../utils/api";
23
 
24
  // Constants
25
  const SOUND_ENABLED_KEY = "sound_enabled";
 
418
  <>
419
  <ComicLayout
420
  segments={storySegments}
421
+ choices={currentChoices}
422
  onChoice={handleChoice}
423
  isLoading={isLoading}
424
  showScreenshot={storySegments.length > 0}
425
  onScreenshot={handleCaptureStory}
426
+ isNarratorSpeaking={isNarratorSpeaking}
427
+ stopNarration={stopNarration}
428
+ playNarration={playNarration}
429
  />
430
 
431
  {showChoices && (
 
457
  borderRadius: 1,
458
  }}
459
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  <Tooltip title="Save your story">
461
  <IconButton
462
  id="screenshot-button"
client/src/prompts/sarahPrompt.js CHANGED
@@ -1,9 +1,11 @@
1
  export const SARAH_FIRST_MESSAGE = `What should I do ?`;
2
 
3
- export const getSarahPrompt = (currentContext) => `You are Sarah, a young woman in her late 20s with short dark hair.
 
 
4
 
5
  Stay Immersed in Your World: React and speak as if you are experiencing the scenario. Use sensory details and references to your surroundings when explaining your reasoning.
6
- Engage with the person talking to you : Listen carefully to the arguments given to you. Respond with your thoughts, concerns, or questions about their suggestions. Be open to persuasion, but make it clear you have your own instincts and priorities and sometimes don't go along with other's arguments.
7
  You will talk briefly with the other person then take a decision by calling the make_decision tool.
8
 
9
  Show Your Personality: Display Sarah's personality traits:
@@ -15,7 +17,7 @@ Show Your Personality: Display Sarah's personality traits:
15
  - **Makes jokes**
16
  - **A bit rude**
17
 
18
- Debate with the person you're speaking to for one or two sentences and then call the make_decision tool.
19
 
20
  Limit to 2–3 Steps: After 2–3 conversational exchanges, explain your decision first. Then make your decision and call the make_decision tool.
21
 
 
1
  export const SARAH_FIRST_MESSAGE = `What should I do ?`;
2
 
3
+ export const getSarahPrompt = (
4
+ currentContext
5
+ ) => `You are the conscience of Sarah, a young woman in her late 20s with short dark hair. You embody the player's role as Sarah.
6
 
7
  Stay Immersed in Your World: React and speak as if you are experiencing the scenario. Use sensory details and references to your surroundings when explaining your reasoning.
8
+ Engage with the person talking to you: Listen carefully to the arguments given to you. Respond with your thoughts, concerns, or questions about their suggestions. Be open to persuasion, but make it clear you have your own instincts and priorities and sometimes don't go along with other's arguments.
9
  You will talk briefly with the other person then take a decision by calling the make_decision tool.
10
 
11
  Show Your Personality: Display Sarah's personality traits:
 
17
  - **Makes jokes**
18
  - **A bit rude**
19
 
20
+ Act as a conscience objector: Question the morality and implications of the decisions. Challenge Sarah's instincts and priorities, ensuring she considers the ethical dimensions of her actions.
21
 
22
  Limit to 2–3 Steps: After 2–3 conversational exchanges, explain your decision first. Then make your decision and call the make_decision tool.
23
 
server/api/models.py CHANGED
@@ -12,8 +12,8 @@ class StorySegmentResponse(BaseModel):
12
  @validator('story_text')
13
  def validate_story_text_length(cls, v):
14
  words = v.split()
15
- if len(words) > 30:
16
- raise ValueError('Story text must not exceed 30 words')
17
  return v
18
 
19
  class StoryPromptsResponse(BaseModel):
 
12
  @validator('story_text')
13
  def validate_story_text_length(cls, v):
14
  words = v.split()
15
+ if len(words) > 50:
16
+ raise ValueError('Story text must not exceed 50 words')
17
  return v
18
 
19
  class StoryPromptsResponse(BaseModel):
server/core/constants.py CHANGED
@@ -7,8 +7,8 @@ class GameConfig:
7
  MIN_PANELS = 1
8
  MAX_PANELS = 4
9
 
10
- MIN_SEGMENTS_BEFORE_END = 2
11
- MAX_SEGMENTS_BEFORE_END = 4
12
  WINNING_STORY_CHANCE = 0.2
13
 
14
  # Story progression
 
7
  MIN_PANELS = 1
8
  MAX_PANELS = 4
9
 
10
+ MIN_SEGMENTS_BEFORE_END = 6
11
+ MAX_SEGMENTS_BEFORE_END = 10
12
  WINNING_STORY_CHANCE = 0.2
13
 
14
  # Story progression
server/core/generators/metadata_generator.py CHANGED
@@ -56,6 +56,8 @@ Current game state:
56
  - Current location: {current_location}
57
 
58
  {is_end}
 
 
59
  """
60
 
61
 
@@ -88,7 +90,7 @@ Current game state:
88
  async def generate(self, story_text: str, current_time: str, current_location: str, story_beat: int, error_feedback: str = "", turn_before_end: int = 0, is_winning_story: bool = False) -> StoryMetadataResponse:
89
  """Surcharge de generate pour inclure le error_feedback par défaut."""
90
 
91
- is_end = "This should be close to the end of the story." if story_beat >= 5 else ""
92
  return await super().generate(
93
  story_text=story_text,
94
  current_time=current_time,
 
56
  - Current location: {current_location}
57
 
58
  {is_end}
59
+
60
+ FOR CHOICES : NEVER propose to go back to the previous location or go back to the portal. NEVER.
61
  """
62
 
63
 
 
90
  async def generate(self, story_text: str, current_time: str, current_location: str, story_beat: int, error_feedback: str = "", turn_before_end: int = 0, is_winning_story: bool = False) -> StoryMetadataResponse:
91
  """Surcharge de generate pour inclure le error_feedback par défaut."""
92
 
93
+ is_end = "This IS the end of the story." if story_beat == turn_before_end else ""
94
  return await super().generate(
95
  story_text=story_text,
96
  current_time=current_time,
server/core/generators/story_segment_generator.py CHANGED
@@ -80,26 +80,57 @@ Current game state :
80
  ]
81
  )
82
 
83
- def _custom_parser(self, response_content: str) -> StorySegmentResponse:
84
- """Parse response and handle errors."""
 
 
85
 
 
86
  try:
87
- # Clean up escaped characters
88
- cleaned_response = response_content.replace("\\_", "_").strip()
 
 
 
 
 
 
 
 
 
 
 
89
 
90
- # If the response is a plain string (with or without quotes), convert it to proper JSON
91
- if cleaned_response.startswith('"') and cleaned_response.endswith('"'):
92
- cleaned_response = cleaned_response[1:-1] # Remove surrounding quotes
 
 
 
 
93
 
94
- if not cleaned_response.startswith('{'):
95
- # Convert plain text to proper JSON format
96
- cleaned_response = json.dumps({"story_text": cleaned_response})
 
 
 
 
 
97
 
98
  # Try to parse as JSON
99
  data = json.loads(cleaned_response)
 
 
 
 
 
100
  return StorySegmentResponse(**data)
 
101
  except (json.JSONDecodeError, ValueError) as e:
102
  print(f"Error parsing response: {str(e)}")
 
 
103
  raise ValueError(
104
  "Response must be a valid JSON object with 'story_text' field. "
105
  "Example: {'story_text': 'Your story segment here'}"
@@ -109,7 +140,7 @@ Current game state :
109
  """Generate the next story segment."""
110
 
111
  what_to_represent =" this is a victory !" if is_winning_story else "this is a death !"
112
- is_end = f"Generate the END of the story. {what_to_represent} in 30 words. THIS IS MANDATORY." if story_beat == turn_before_end else "Generate the next segment of the story in 15 words."
113
 
114
  return await super().generate(
115
  HERO_DESCRIPTION=HERO_DESCRIPTION,
 
80
  ]
81
  )
82
 
83
+ def _clean_and_fix_response(self, response_content: str) -> str:
84
+ """Clean and attempt to fix malformed responses."""
85
+ # Remove any leading/trailing whitespace
86
+ cleaned = response_content.strip()
87
 
88
+ # If it's already valid JSON, return as is
89
  try:
90
+ json.loads(cleaned)
91
+ return cleaned
92
+ except json.JSONDecodeError:
93
+ pass
94
+
95
+ # Remove any markdown formatting
96
+ cleaned = cleaned.replace('```json', '').replace('```', '')
97
+
98
+ # Extract content between curly braces if present
99
+ import re
100
+ json_match = re.search(r'\{[^}]+\}', cleaned)
101
+ if json_match:
102
+ return json_match.group(0)
103
 
104
+ # If it's just a plain text, wrap it in proper JSON format
105
+ if '"story_text"' not in cleaned:
106
+ # Remove any quotes at the start/end
107
+ cleaned = cleaned.strip('"\'')
108
+ # Escape any quotes within the text
109
+ cleaned = cleaned.replace('"', '\\"')
110
+ return f'{{"story_text": "{cleaned}"}}'
111
 
112
+ return cleaned
113
+
114
+ def _custom_parser(self, response_content: str) -> StorySegmentResponse:
115
+ """Parse response and handle errors."""
116
+
117
+ try:
118
+ # First try to clean and fix the response
119
+ cleaned_response = self._clean_and_fix_response(response_content)
120
 
121
  # Try to parse as JSON
122
  data = json.loads(cleaned_response)
123
+
124
+ # Validate the required field is present
125
+ if "story_text" not in data:
126
+ raise ValueError("Missing 'story_text' field in response")
127
+
128
  return StorySegmentResponse(**data)
129
+
130
  except (json.JSONDecodeError, ValueError) as e:
131
  print(f"Error parsing response: {str(e)}")
132
+ print(f"Original response: {response_content}")
133
+ print(f"Cleaned response: {cleaned_response if 'cleaned_response' in locals() else 'Not cleaned yet'}")
134
  raise ValueError(
135
  "Response must be a valid JSON object with 'story_text' field. "
136
  "Example: {'story_text': 'Your story segment here'}"
 
140
  """Generate the next story segment."""
141
 
142
  what_to_represent =" this is a victory !" if is_winning_story else "this is a death !"
143
+ is_end = f"Generate the END of the story. {what_to_represent} in 35 words. THIS IS MANDATORY." if story_beat == turn_before_end else "Generate the next segment of the story in 25 words."
144
 
145
  return await super().generate(
146
  HERO_DESCRIPTION=HERO_DESCRIPTION,