Partial fix to fix prompt loading issues. Messages + Phase summary display on load broken.

This commit is contained in:
AlxAI 2025-02-08 14:14:39 -08:00
parent f9864f85d9
commit 16e3b45264
18 changed files with 377 additions and 94 deletions

View file

@ -0,0 +1,49 @@
import React from "react";
import PropTypes from "prop-types";
/**
* A simple bottom sheet that slides up from the bottom of the screen,
* showing the current phase summary.
*/
export function PhaseSummaryBottomSheet({ phase, summaryText, onClose }) {
return (
<div style={{
position: 'fixed',
bottom: 0,
left: 0,
width: '100%',
maxHeight: '40%',
backgroundColor: '#fff',
boxShadow: '0 -2px 8px rgba(0,0,0,0.25)',
zIndex: 9999,
overflowY: 'auto',
transition: 'transform 0.3s ease-in-out',
transform: 'translateY(0%)'
}}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0.5rem',
background: '#f2f2f2',
borderBottom: '1px solid #ccc'
}}>
<h5 className="mb-0">Summary for {phase}:</h5>
<button className="btn btn-sm btn-danger" onClick={onClose}>Close</button>
</div>
<div style={{
padding: '1rem',
overflowY: 'auto'
}}>
{summaryText || "No phase summary available."}
</div>
</div>
);
}
PhaseSummaryBottomSheet.propTypes = {
phase: PropTypes.string,
summaryText: PropTypes.string,
onClose: PropTypes.func.isRequired
};

View file

@ -21,6 +21,7 @@ export class MessageView extends React.Component {
// message
render() {
const message = this.props.message;
console.log("DEBUG: In <MessageView>, message prop =>", message);
const owner = this.props.owner;
const id = this.props.id ? {id: this.props.id} : {};
const messagesLines = message.message.replace('\r\n', '\n')

View file

@ -49,7 +49,7 @@ import {SvgModern} from "../maps/modern/SvgModern";
import {SvgPure} from "../maps/pure/SvgPure";
import {MapData} from "../utils/map_data";
import {Queue} from "../../diplomacy/utils/queue";
import {PhaseSummaryView} from "../components/phase_summary_view";
import {PhaseSummaryBottomSheet} from "../components/PhaseSummaryBottomSheet";
const HotKey = require('react-shortcut');
@ -140,7 +140,10 @@ export class ContentGame extends React.Component {
power: null,
orderBuildingType: null,
orderBuildingPath: [],
showAbbreviations: true
showAbbreviations: true,
showPhaseSummarySheet: false,
bottomSheetVisible: true,
summaryVisible: true
};
// Bind some class methods to this instance.
@ -807,15 +810,15 @@ export class ContentGame extends React.Component {
onChangePastPhaseIndex(increment) {
const selectObject = document.getElementById('select-past-phase');
if (selectObject) {
// Let's simply increase or decrease index of showed past phase.
const index = selectObject.selectedIndex;
const newIndex = index + (increment ? 1 : -1);
if (newIndex >= 0 && newIndex < selectObject.length) {
selectObject.selectedIndex = newIndex;
this.__change_past_phase(parseInt(selectObject.options[newIndex].value, 10), (increment ? 0 : 1));
}
const index = selectObject.selectedIndex;
const newIndex = index + (increment ? 1 : -1);
if (newIndex >= 0 && newIndex < selectObject.length) {
selectObject.selectedIndex = newIndex;
// Ensure we do parseInt with a base of 10
this.__change_past_phase(parseInt(selectObject.options[newIndex].value, 10));
}
}
}
}
onIncrementPastPhase(event) {
this.onChangePastPhaseIndex(true);
@ -885,21 +888,18 @@ export class ContentGame extends React.Component {
renderPastMessages(engine, role) {
const messageChannels = engine.getMessageChannels(role, true);
const tabNames = [];
for (let powerName of Object.keys(engine.powers)) if (powerName !== role)
tabNames.push(powerName);
for (let powerName of Object.keys(engine.powers)) {
if (powerName !== role) tabNames.push(powerName);
}
tabNames.sort();
tabNames.push('GLOBAL');
const titles = tabNames.map(tabName => (tabName === 'GLOBAL' ? tabName : tabName.substr(0, 3)));
const currentTabId = this.state.tabPastMessages || tabNames[0];
let summaryText = "";
if (engine.phase_summaries && engine.phase_summaries.hasOwnProperty(engine.phasePrior)) {
summaryText = engine.phase_summaries[engine.phasePrior];
}
return (
<div className={'panel-messages'} key={'panel-messages'}>
<PhaseSummaryView phase={engine.phasePrior} summaryText={summaryText} />
<div className={'panel-messages'} key={'panel-messages'}>
{/* Messages. */}
<Tabs menu={tabNames} titles={titles} onChange={this.onChangeTabPastMessages} active={currentTabId}>
{tabNames.map(protagonist => (
@ -1157,6 +1157,25 @@ export class ContentGame extends React.Component {
renderTabMessages(toDisplay, initialEngine, currentPowerName) {
const {engine, pastPhases, phaseIndex} = this.__get_engine_to_display(initialEngine);
let allMessages = [];
const history = engine.message_history || {};
for (const [key, val] of Object.entries(history)) {
if (val && typeof val === 'object') {
if (typeof val.values === 'function') {
const arr = Array.from(val.values());
allMessages.push(...arr);
} else {
allMessages.push(...Object.values(val));
}
} else {
console.log(`[renderTabMessages] Skipping key="${key}" =>`, val);
}
}
console.log("DEBUG: combined allMessages =>", allMessages);
if (!toDisplay) return null;
return (
<Tab id={'tab-phase-history'} display={toDisplay}>
<Row>
@ -1168,11 +1187,11 @@ export class ContentGame extends React.Component {
</div>
<div className={'col-xl'}>
{this.__form_phases(pastPhases, phaseIndex)}
{pastPhases[phaseIndex] === initialEngine.phase ? (
this.renderCurrentMessages(initialEngine, currentPowerName)
) : (
this.renderPastMessages(engine, currentPowerName)
)}
{pastPhases[phaseIndex] === initialEngine.phase ? (
this.renderCurrentMessages(initialEngine, currentPowerName)
) : (
this.renderPastMessages(engine, currentPowerName)
)}
</div>
</Row>
{toDisplay && <HotKey keys={['arrowleft']} onKeysCoincide={this.onDecrementPastPhase}/>}
@ -1224,9 +1243,18 @@ export class ContentGame extends React.Component {
// [ React.Component overridden methods.
render() {
this.props.data.displayed = true;
const page = this.context;
const engine = this.props.data;
// 1) Quick log to confirm engine is not null/undefined
console.log("Render content_game with engine:", engine);
if (!engine) {
console.warn("Engine is null or undefined. Rendering fallback or forcing a logout?");
return <main><div>Error: No engine found.</div></main>;
}
this.props.data.displayed = true;
const title = ContentGame.gameTitle(engine);
const navigation = [
['Help', () => page.dialog(onClose => <Help onClose={onClose}/>)],
@ -1343,6 +1371,39 @@ export class ContentGame extends React.Component {
</div>
);
// Guard referencing engine.phase_summaries
const phaseSummaries = engine.phase_summaries || {};
const currentPhase = engine.phase || "(unknown phase)";
let summaryText = "";
// Attempt to get a prior phase or fallback to current if prior is not available
const priorPhase = engine.phasePrior;
if (engine.phase_summaries) {
if (priorPhase && engine.phase_summaries.hasOwnProperty(priorPhase)) {
summaryText = engine.phase_summaries[priorPhase];
} else if (engine.phase && engine.phase_summaries.hasOwnProperty(engine.phase)) {
summaryText = engine.phase_summaries[engine.phase];
}
}
const summaryForCurrentPhase =
phaseSummaries.hasOwnProperty(currentPhase) ? phaseSummaries[currentPhase] : "";
// Example: Flatten messages from all known phases
let allMessages = [];
if (engine.message_history) {
for (const phaseName of Object.keys(engine.message_history)) {
const phaseMsgsDict = engine.message_history[phaseName];
// Convert { time_sent: messageObj, ... } to an array
const phaseMsgsArray = Object.values(phaseMsgsDict);
allMessages = allMessages.concat(phaseMsgsArray);
}
console.log('engine.message_history =>', engine.message_history);
for (const [key, value] of Object.entries(engine.message_history)) {
console.log('key =>', key, 'value =>', value);
}
}
console.log("DEBUG: content_game messages =>", allMessages, "length:", allMessages.length);
return (
<main>
<Helmet>
@ -1367,14 +1428,29 @@ export class ContentGame extends React.Component {
currentTabOrderCreation
)) || ''}
</Tabs>
{/* Debug log before rendering the bottom sheet */}
{console.log("Rendering PhaseSummaryBottomSheet with phase:", currentPhase, "summary length:", summaryForCurrentPhase.length)}
<PhaseSummaryBottomSheet
visible={this.state.summaryVisible}
phase={currentPhase}
summaryText={summaryText}
onClose={() => this.setState({ summaryVisible: false })}
/>
</main>
);
}
componentDidMount() {
this._isMounted = true;
window.scrollTo(0, 0);
if (this.props.data.client)
this.reloadDeadlineTimer(this.props.data.client);
if (this.props.data.client) {
this.reloadDeadlineTimer(this.props.data.client).then(() => {
if (this._isMounted) {
this.setState({ doneLoading: true });
}
});
}
this.props.data.displayed = true;
// Try to prevent scrolling when pressing keys Home and End.
document.onkeydown = (event) => {
@ -1395,6 +1471,7 @@ export class ContentGame extends React.Component {
}
componentWillUnmount() {
this._isMounted = false;
this.clearScheduleTimeout();
this.props.data.displayed = false;
document.onkeydown = null;

View file

@ -31,52 +31,107 @@ export function loadGameFromDisk() {
}
const reader = new FileReader();
reader.onload = () => {
const savedData = JSON.parse(reader.result);
const gameObject = {};
gameObject.game_id = `(local) ${savedData.id}`;
gameObject.map_name = savedData.map;
gameObject.rules = savedData.rules;
gameObject.state_history = {};
gameObject.message_history = {};
gameObject.order_history = {};
gameObject.result_history = {};
console.log('[loadGameFromDisk] Reading JSON file...');
let savedData;
try {
savedData = JSON.parse(reader.result);
} catch (e) {
onError('Could not parse JSON file: ' + e);
return;
}
// Load all saved phases (expect the latest one) to history fields.
const gameObject = {
game_id: `(local) ${savedData.id}`,
map_name: savedData.map,
rules: savedData.rules,
state_history: {},
message_history: {},
order_history: {},
result_history: {},
phase_summaries: {}
};
// Load older phases into history
for (let i = 0; i < savedData.phases.length - 1; ++i) {
const savedPhase = savedData.phases[i];
const gameState = savedPhase.state;
const phaseOrders = savedPhase.orders || {};
const phaseResults = savedPhase.results || {};
const phaseMessages = {};
if (savedPhase.messages) {
for (let message of savedPhase.messages) {
phaseMessages[message.time_sent] = message;
// 1) Fix or parse messages if they're a string
let phaseMessages = savedPhase.messages;
if (typeof phaseMessages === 'string') {
console.warn('[loadGameFromDisk] Phase', savedPhase.name,
'has messages as string. Attempting fallback parse...');
// If it starts with "SortedDict", we can't trivially parse.
// Minimal fallback: set to empty object or parse if you have a custom parser.
if (phaseMessages.startsWith('SortedDict{')) {
phaseMessages = {};
}
} else if (!phaseMessages) {
phaseMessages = {};
} else {
// Convert array -> object keyed by time_sent
const obj = {};
for (const msg of phaseMessages) {
if (msg && msg.time_sent !== undefined) {
obj[msg.time_sent] = msg;
}
}
phaseMessages = obj;
}
if (!gameState.name)
gameState.name = savedPhase.name;
if (!gameState.name) gameState.name = savedPhase.name;
gameObject.state_history[gameState.name] = gameState;
gameObject.message_history[gameState.name] = phaseMessages;
gameObject.order_history[gameState.name] = phaseOrders;
gameObject.result_history[gameState.name] = phaseResults;
// Summaries
if (savedPhase.summary) {
console.log(`[loadGameFromDisk] Loading summaries for phase`, gameState.name);
gameObject.phase_summaries[gameState.name] = savedPhase.summary;
} else {
console.log(`[loadGameFromDisk] No summary for phase ${savedPhase.name}`);
}
}
// Load latest phase separately and use it later to define the current game phase.
// Load latest phase
const latestPhase = savedData.phases[savedData.phases.length - 1];
const latestGameState = latestPhase.state;
const latestPhaseOrders = latestPhase.orders || {};
const latestPhaseResults = latestPhase.results || {};
const latestPhaseMessages = {};
if (latestPhase.messages) {
for (let message of latestPhase.messages) {
latestPhaseMessages[message.time_sent] = message;
let latestPhaseMessages = latestPhase.messages;
if (typeof latestPhaseMessages === 'string') {
console.warn('[loadGameFromDisk] Latest phase has messages as string. Fallback parse...');
if (latestPhaseMessages.startsWith('SortedDict{')) {
latestPhaseMessages = {};
}
} else if (!latestPhaseMessages) {
latestPhaseMessages = {};
} else {
const obj = {};
for (const msg of latestPhaseMessages) {
if (msg && msg.time_sent !== undefined) {
obj[msg.time_sent] = msg;
}
}
latestPhaseMessages = obj;
}
if (!latestGameState.name)
latestGameState.name = latestPhase.name;
// TODO: NB: What if latest phase in loaded JSON contains order results? Not sure if it is well handled.
gameObject.result_history[latestGameState.name] = latestPhaseResults;
if (latestPhase.summary) {
console.log(`[loadGameFromDisk] Loading summary for latest phase ${latestGameState.name}:`, latestPhase.summary);
gameObject.phase_summaries[latestGameState.name] = latestPhase.summary;
} else {
console.log(`[loadGameFromDisk] No summary for latest phase ${latestGameState.name}`);
}
// Final game metadata
gameObject.messages = [];
gameObject.role = STRINGS.OBSERVER_TYPE;
gameObject.status = STRINGS.COMPLETED;
@ -84,15 +139,17 @@ export function loadGameFromDisk() {
gameObject.deadline = 0;
gameObject.n_controls = 0;
gameObject.registration_password = '';
const game = new Game(gameObject);
// Set game current phase and state using latest phase found in JSON file.
// Set the current phase to the latest
game.setPhaseData({
name: latestGameState.name,
state: latestGameState,
orders: latestPhaseOrders,
messages: latestPhaseMessages
});
onLoad(game);
};
reader.readAsText(file);