update
Browse files- client/src/layouts/utils.js +16 -11
- client/src/pages/Game.jsx +11 -8
- server/api/models.py +4 -4
- server/core/game_logic.py +3 -3
- server/core/prompts/system.py +1 -0
- server/core/prompts/text_prompts.py +2 -2
client/src/layouts/utils.js
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
import { LAYOUTS, getNextLayoutType } from "./config";
|
2 |
|
3 |
-
// Map to store layout types for each
|
4 |
-
const
|
5 |
|
6 |
// Function to group segments into layouts
|
7 |
export function groupSegmentsIntoLayouts(segments) {
|
@@ -9,7 +9,7 @@ export function groupSegmentsIntoLayouts(segments) {
|
|
9 |
|
10 |
const layouts = [];
|
11 |
|
12 |
-
segments.forEach((segment) => {
|
13 |
const imageCount = segment.images?.length || 0;
|
14 |
|
15 |
// Si c'est le premier segment ou le dernier (mort/victoire), créer un layout COVER
|
@@ -18,8 +18,12 @@ export function groupSegmentsIntoLayouts(segments) {
|
|
18 |
return;
|
19 |
}
|
20 |
|
21 |
-
// Pour tous les autres segments,
|
22 |
-
|
|
|
|
|
|
|
|
|
23 |
layouts.push({ type: layoutType, segments: [segment] });
|
24 |
});
|
25 |
|
@@ -40,18 +44,19 @@ export function getNextPanelDimensions(segments) {
|
|
40 |
return LAYOUTS.COVER.panels[0];
|
41 |
}
|
42 |
|
43 |
-
// Pour les segments du milieu,
|
44 |
const lastSegment = nonChoiceSegments[nonChoiceSegments.length - 1];
|
45 |
const imageCount = lastSegment.images?.length || 0;
|
46 |
-
|
47 |
-
|
48 |
-
imageCount
|
49 |
-
|
|
|
50 |
|
51 |
return LAYOUTS[layoutType].panels[0];
|
52 |
}
|
53 |
|
54 |
// Function to reset layout map (call this when starting a new story)
|
55 |
export function resetLayoutMap() {
|
56 |
-
|
57 |
}
|
|
|
1 |
import { LAYOUTS, getNextLayoutType } from "./config";
|
2 |
|
3 |
+
// Map to store layout types for each segment
|
4 |
+
const segmentLayoutMap = new Map();
|
5 |
|
6 |
// Function to group segments into layouts
|
7 |
export function groupSegmentsIntoLayouts(segments) {
|
|
|
9 |
|
10 |
const layouts = [];
|
11 |
|
12 |
+
segments.forEach((segment, index) => {
|
13 |
const imageCount = segment.images?.length || 0;
|
14 |
|
15 |
// Si c'est le premier segment ou le dernier (mort/victoire), créer un layout COVER
|
|
|
18 |
return;
|
19 |
}
|
20 |
|
21 |
+
// Pour tous les autres segments, utiliser le layout existant ou en créer un nouveau
|
22 |
+
let layoutType = segmentLayoutMap.get(segment.text);
|
23 |
+
if (!layoutType) {
|
24 |
+
layoutType = getNextLayoutType(layouts.length, imageCount);
|
25 |
+
segmentLayoutMap.set(segment.text, layoutType);
|
26 |
+
}
|
27 |
layouts.push({ type: layoutType, segments: [segment] });
|
28 |
});
|
29 |
|
|
|
44 |
return LAYOUTS.COVER.panels[0];
|
45 |
}
|
46 |
|
47 |
+
// Pour les segments du milieu, utiliser le layout existant ou en créer un nouveau
|
48 |
const lastSegment = nonChoiceSegments[nonChoiceSegments.length - 1];
|
49 |
const imageCount = lastSegment.images?.length || 0;
|
50 |
+
let layoutType = segmentLayoutMap.get(lastSegment.text);
|
51 |
+
if (!layoutType) {
|
52 |
+
layoutType = getNextLayoutType(nonChoiceSegments.length - 1, imageCount);
|
53 |
+
segmentLayoutMap.set(lastSegment.text, layoutType);
|
54 |
+
}
|
55 |
|
56 |
return LAYOUTS[layoutType].panels[0];
|
57 |
}
|
58 |
|
59 |
// Function to reset layout map (call this when starting a new story)
|
60 |
export function resetLayoutMap() {
|
61 |
+
segmentLayoutMap.clear();
|
62 |
}
|
client/src/pages/Game.jsx
CHANGED
@@ -149,13 +149,14 @@ export function Game() {
|
|
149 |
}
|
150 |
} catch (error) {
|
151 |
console.error("Error in handleStoryAction:", error);
|
|
|
|
|
|
|
|
|
|
|
152 |
const errorSegment = {
|
153 |
-
text:
|
154 |
-
|
155 |
-
"Le conteur d'histoires est temporairement indisponible. Veuillez réessayer dans quelques instants...",
|
156 |
-
rawText:
|
157 |
-
error.message ||
|
158 |
-
"Le conteur d'histoires est temporairement indisponible. Veuillez réessayer dans quelques instants...",
|
159 |
isChoice: false,
|
160 |
isDeath: false,
|
161 |
isVictory: false,
|
@@ -164,16 +165,18 @@ export function Game() {
|
|
164 |
? storySegments[storySegments.length - 1].radiationLevel
|
165 |
: 0,
|
166 |
images: [],
|
|
|
167 |
};
|
168 |
|
169 |
if (action === "restart") {
|
170 |
setStorySegments([errorSegment]);
|
171 |
} else {
|
172 |
-
|
|
|
173 |
}
|
174 |
|
175 |
// Set retry choice
|
176 |
-
setCurrentChoices([{ id:
|
177 |
|
178 |
// Play error message
|
179 |
await playNarration(errorSegment.rawText);
|
|
|
149 |
}
|
150 |
} catch (error) {
|
151 |
console.error("Error in handleStoryAction:", error);
|
152 |
+
const errorMessage =
|
153 |
+
error.response?.data?.detail ||
|
154 |
+
error.message ||
|
155 |
+
"Le conteur d'histoires est temporairement indisponible. Veuillez réessayer dans quelques instants...";
|
156 |
+
|
157 |
const errorSegment = {
|
158 |
+
text: errorMessage,
|
159 |
+
rawText: errorMessage,
|
|
|
|
|
|
|
|
|
160 |
isChoice: false,
|
161 |
isDeath: false,
|
162 |
isVictory: false,
|
|
|
165 |
? storySegments[storySegments.length - 1].radiationLevel
|
166 |
: 0,
|
167 |
images: [],
|
168 |
+
isLoading: false,
|
169 |
};
|
170 |
|
171 |
if (action === "restart") {
|
172 |
setStorySegments([errorSegment]);
|
173 |
} else {
|
174 |
+
// En cas d'erreur sur un choix, on garde le segment précédent
|
175 |
+
setStorySegments((prev) => [...prev.slice(0, -1), errorSegment]);
|
176 |
}
|
177 |
|
178 |
// Set retry choice
|
179 |
+
setCurrentChoices([{ id: "retry", text: "Réessayer" }]);
|
180 |
|
181 |
// Play error message
|
182 |
await playNarration(errorSegment.rawText);
|
server/api/models.py
CHANGED
@@ -3,11 +3,11 @@ from typing import List, Optional
|
|
3 |
|
4 |
class Choice(BaseModel):
|
5 |
id: int
|
6 |
-
text: str = Field(description="The text of the choice
|
7 |
|
8 |
# New response models for story generation steps
|
9 |
class StoryTextResponse(BaseModel):
|
10 |
-
story_text: str = Field(description="The story text
|
11 |
|
12 |
class StoryPromptsResponse(BaseModel):
|
13 |
image_prompts: List[str] = Field(description="List of 2 to 4 comic panel descriptions that illustrate the key moments of the scene. Use the word 'Sarah' only when referring to her.", min_items=1, max_items=4)
|
@@ -18,11 +18,11 @@ class StoryMetadataResponse(BaseModel):
|
|
18 |
radiation_increase: int = Field(description="How much radiation this segment adds (0-3)", ge=0, le=3, default=1)
|
19 |
is_last_step: bool = Field(description="Whether this is the last step (victory or death)", default=False)
|
20 |
time: str = Field(description="Current in-game time in 24h format (HH:MM). Time passes realistically based on actions.")
|
21 |
-
location: str = Field(description="Current location
|
22 |
|
23 |
# Complete story response combining all parts
|
24 |
class StoryResponse(BaseModel):
|
25 |
-
story_text: str = Field(description="The story text
|
26 |
choices: List[Choice]
|
27 |
radiation_level: int = Field(description="Current radiation level from 0 to 10")
|
28 |
is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
|
|
|
3 |
|
4 |
class Choice(BaseModel):
|
5 |
id: int
|
6 |
+
text: str = Field(description="The text of the choice. No more than 6 words.")
|
7 |
|
8 |
# New response models for story generation steps
|
9 |
class StoryTextResponse(BaseModel):
|
10 |
+
story_text: str = Field(description="The story text. No more than 15 words.")
|
11 |
|
12 |
class StoryPromptsResponse(BaseModel):
|
13 |
image_prompts: List[str] = Field(description="List of 2 to 4 comic panel descriptions that illustrate the key moments of the scene. Use the word 'Sarah' only when referring to her.", min_items=1, max_items=4)
|
|
|
18 |
radiation_increase: int = Field(description="How much radiation this segment adds (0-3)", ge=0, le=3, default=1)
|
19 |
is_last_step: bool = Field(description="Whether this is the last step (victory or death)", default=False)
|
20 |
time: str = Field(description="Current in-game time in 24h format (HH:MM). Time passes realistically based on actions.")
|
21 |
+
location: str = Field(description="Current location.")
|
22 |
|
23 |
# Complete story response combining all parts
|
24 |
class StoryResponse(BaseModel):
|
25 |
+
story_text: str = Field(description="The story text. No more than 15 words.")
|
26 |
choices: List[Choice]
|
27 |
radiation_level: int = Field(description="Current radiation level from 0 to 10")
|
28 |
is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
|
server/core/game_logic.py
CHANGED
@@ -15,7 +15,7 @@ from core.story_generators import TextGenerator, ImagePromptsGenerator, Metadata
|
|
15 |
# Game constants
|
16 |
MAX_RADIATION = 10
|
17 |
STARTING_TIME = "18:00" # Game starts at sunset
|
18 |
-
STARTING_LOCATION = "Outskirts of
|
19 |
|
20 |
def enrich_prompt_with_sarah_description(prompt: str) -> str:
|
21 |
"""Add Sarah's visual description to prompts that mention her."""
|
@@ -56,14 +56,14 @@ class GameState:
|
|
56 |
|
57 |
# Story output structure
|
58 |
class StoryLLMResponse(BaseModel):
|
59 |
-
story_text: str = Field(description="The next segment of the story. No more than 12 words THIS IS MANDATORY. Use bold formatting (like
|
60 |
choices: List[str] = Field(description="Exactly two possible choices for the player", min_items=2, max_items=2)
|
61 |
is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
|
62 |
radiation_increase: int = Field(description="How much radiation this segment adds (0-3)", ge=0, le=3, default=1)
|
63 |
image_prompts: List[str] = Field(description="List of 1 to 4 comic panel descriptions that illustrate the key moments of the scene", min_items=1, max_items=4)
|
64 |
is_last_step: bool = Field(description="Whether this is the last step (victory or death)", default=False)
|
65 |
time: str = Field(description="Current in-game time in 24h format (HH:MM). Time passes realistically based on actions.", default=STARTING_TIME)
|
66 |
-
location: str = Field(description="Current location, using bold for proper nouns (e.g., 'Inside
|
67 |
|
68 |
# Story generator
|
69 |
class StoryGenerator:
|
|
|
15 |
# Game constants
|
16 |
MAX_RADIATION = 10
|
17 |
STARTING_TIME = "18:00" # Game starts at sunset
|
18 |
+
STARTING_LOCATION = "Outskirts of New Haven"
|
19 |
|
20 |
def enrich_prompt_with_sarah_description(prompt: str) -> str:
|
21 |
"""Add Sarah's visual description to prompts that mention her."""
|
|
|
56 |
|
57 |
# Story output structure
|
58 |
class StoryLLMResponse(BaseModel):
|
59 |
+
story_text: str = Field(description="The next segment of the story. No more than 12 words THIS IS MANDATORY. Use bold formatting (like this) ONLY for proper nouns (like Sarah, hospital) and important locations.")
|
60 |
choices: List[str] = Field(description="Exactly two possible choices for the player", min_items=2, max_items=2)
|
61 |
is_victory: bool = Field(description="Whether this segment ends in Sarah's victory", default=False)
|
62 |
radiation_increase: int = Field(description="How much radiation this segment adds (0-3)", ge=0, le=3, default=1)
|
63 |
image_prompts: List[str] = Field(description="List of 1 to 4 comic panel descriptions that illustrate the key moments of the scene", min_items=1, max_items=4)
|
64 |
is_last_step: bool = Field(description="Whether this is the last step (victory or death)", default=False)
|
65 |
time: str = Field(description="Current in-game time in 24h format (HH:MM). Time passes realistically based on actions.", default=STARTING_TIME)
|
66 |
+
location: str = Field(description="Current location, using bold for proper nouns (e.g., 'Inside Vault 15', 'Streets of New Haven').", default=STARTING_LOCATION)
|
67 |
|
68 |
# Story generator
|
69 |
class StoryGenerator:
|
server/core/prompts/system.py
CHANGED
@@ -12,6 +12,7 @@ FORMATTING_RULES ( MANDATORY )
|
|
12 |
- Never use TIME: 18:30 or other time-related information
|
13 |
- Never use LOCATION: the city or other location-related information
|
14 |
- Never use RADIATION: 10* or other radiation-related information
|
|
|
15 |
"""
|
16 |
|
17 |
STORY_RULES = """
|
|
|
12 |
- Never use TIME: 18:30 or other time-related information
|
13 |
- Never use LOCATION: the city or other location-related information
|
14 |
- Never use RADIATION: 10* or other radiation-related information
|
15 |
+
- NEVER USE BOLD FOR ANYTHING
|
16 |
"""
|
17 |
|
18 |
STORY_RULES = """
|
server/core/prompts/text_prompts.py
CHANGED
@@ -19,12 +19,12 @@ Be consistent with the story's tone and previous context.
|
|
19 |
|
20 |
You must return a JSON object with the following format:
|
21 |
{{{{
|
22 |
-
"choices": ["Go to the
|
23 |
"is_victory": false,
|
24 |
"radiation_increase": 1,
|
25 |
"is_last_step": false,
|
26 |
"time": "HH:MM",
|
27 |
-
"location": "Location name with
|
28 |
}}}}
|
29 |
"""
|
30 |
|
|
|
19 |
|
20 |
You must return a JSON object with the following format:
|
21 |
{{{{
|
22 |
+
"choices": ["Go to the hospital", "Get back to the warehouse"],
|
23 |
"is_victory": false,
|
24 |
"radiation_increase": 1,
|
25 |
"is_last_step": false,
|
26 |
"time": "HH:MM",
|
27 |
+
"location": "Location name with proper nouns in bold"
|
28 |
}}}}
|
29 |
"""
|
30 |
|