Skip to content

Commit d117067

Browse files
committed
Enh(viewer): Lazy-load changes
1 parent 540835f commit d117067

6 files changed

Lines changed: 485 additions & 94 deletions

File tree

codeclash/viewer/app.py

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,14 @@ def parse_game_metadata(self) -> GameMetadata:
416416
all_logs=all_logs,
417417
)
418418

419-
def parse_trajectory(self, player_name: str, round_num: int) -> TrajectoryInfo | None:
420-
"""Parse a specific trajectory file"""
419+
def parse_trajectory(self, player_name: str, round_num: int, *, load_diffs: bool = False) -> TrajectoryInfo | None:
420+
"""Parse a specific trajectory file
421+
422+
Args:
423+
player_name: Name of the player
424+
round_num: Round number
425+
load_diffs: If True, load diff data. If False (default), skip loading diffs for performance
426+
"""
421427
player_dir = self.log_dir / "players" / player_name
422428
if not player_dir.exists():
423429
return None
@@ -430,25 +436,28 @@ def parse_trajectory(self, player_name: str, round_num: int) -> TrajectoryInfo |
430436
info = data.get("info", {})
431437
model_stats = info.get("model_stats", {})
432438

433-
# Get diff data from changes file
439+
# Get diff data from changes file only if requested
434440
diff = incremental_diff = modified_files = None
435-
changes_file = player_dir / f"changes_r{round_num}.json"
436-
if changes_file.exists():
437-
try:
438-
changes_data = json.loads(changes_file.read_text())
439-
diff = changes_data.get("full_diff", "")
440-
incremental_diff = changes_data.get("incremental_diff", "")
441-
modified_files = changes_data.get("modified_files", {})
442-
except (json.JSONDecodeError, KeyError):
443-
pass
444-
445-
# Filter and split diffs by files
446-
filtered_diff = filter_git_diff(diff) if diff else ""
447-
filtered_incremental_diff = filter_git_diff(incremental_diff) if incremental_diff else ""
448-
diff_by_files = split_git_diff_by_files(filtered_diff) if filtered_diff else {}
449-
incremental_diff_by_files = (
450-
split_git_diff_by_files(filtered_incremental_diff) if filtered_incremental_diff else {}
451-
)
441+
diff_by_files = incremental_diff_by_files = None
442+
443+
if load_diffs:
444+
changes_file = player_dir / f"changes_r{round_num}.json"
445+
if changes_file.exists():
446+
try:
447+
changes_data = json.loads(changes_file.read_text())
448+
diff = changes_data.get("full_diff", "")
449+
incremental_diff = changes_data.get("incremental_diff", "")
450+
modified_files = changes_data.get("modified_files", {})
451+
452+
# Filter and split diffs by files
453+
filtered_diff = filter_git_diff(diff) if diff else ""
454+
filtered_incremental_diff = filter_git_diff(incremental_diff) if incremental_diff else ""
455+
diff_by_files = split_git_diff_by_files(filtered_diff) if filtered_diff else {}
456+
incremental_diff_by_files = (
457+
split_git_diff_by_files(filtered_incremental_diff) if filtered_incremental_diff else {}
458+
)
459+
except (json.JSONDecodeError, KeyError):
460+
pass
452461

453462
return TrajectoryInfo(
454463
player_id=player_name,
@@ -792,17 +801,16 @@ def render_game_viewer(folder_path: Path, selected_folder: str) -> str:
792801
parser = LogParser(folder_path)
793802
metadata = parser.parse_game_metadata()
794803

795-
# Group trajectories by round
804+
# Group trajectories by round (without loading diffs)
796805
trajectories_by_round = {}
797806
for player_name, round_num in parser.get_available_trajectories():
798807
if round_num not in trajectories_by_round:
799808
trajectories_by_round[round_num] = []
800-
trajectory = parser.parse_trajectory(player_name, round_num)
809+
trajectory = parser.parse_trajectory(player_name, round_num, load_diffs=False)
801810
if trajectory:
802811
trajectories_by_round[round_num].append(trajectory)
803812

804-
# Get analysis data
805-
analysis_data = parser.analyze_line_counts()
813+
# Get analysis data (skip line counting - will be loaded on demand)
806814
sim_wins_data = parser.analyze_sim_wins_per_round()
807815
matrix_data = parser.load_matrix_analysis()
808816

@@ -815,7 +823,6 @@ def render_game_viewer(folder_path: Path, selected_folder: str) -> str:
815823
selected_folder_path=str(folder_path),
816824
metadata=metadata,
817825
trajectories_by_round=trajectories_by_round,
818-
analysis_data=analysis_data,
819826
sim_wins_data=sim_wins_data,
820827
matrix_data=matrix_data,
821828
navigation=navigation_info,
@@ -1244,4 +1251,48 @@ def load_log():
12441251
return jsonify({"success": False, "error": f"Error reading file: {str(e)}"}), 500
12451252

12461253

1254+
@app.route("/load-trajectory-diffs")
1255+
def load_trajectory_diffs():
1256+
"""Load trajectory diff data on demand"""
1257+
selected_folder = request.args.get("folder")
1258+
player_name = request.args.get("player")
1259+
round_num = request.args.get("round")
1260+
1261+
if not all([selected_folder, player_name, round_num]):
1262+
return jsonify({"success": False, "error": "Missing required parameters"}), 400
1263+
1264+
try:
1265+
round_num = int(round_num)
1266+
except ValueError:
1267+
return jsonify({"success": False, "error": "Invalid round number"}), 400
1268+
1269+
try:
1270+
# Validate the selected folder exists and is a game folder
1271+
folder_path = LOG_BASE_DIR / selected_folder
1272+
if not folder_path.exists() or not is_game_folder(folder_path):
1273+
return jsonify({"success": False, "error": "Invalid game folder"}), 404
1274+
1275+
# Parse trajectory with diffs loaded
1276+
parser = LogParser(folder_path)
1277+
trajectory = parser.parse_trajectory(player_name, round_num, load_diffs=True)
1278+
1279+
if not trajectory:
1280+
return jsonify({"success": False, "error": "Trajectory not found"}), 404
1281+
1282+
return jsonify(
1283+
{
1284+
"success": True,
1285+
"diff": trajectory.diff,
1286+
"incremental_diff": trajectory.incremental_diff,
1287+
"modified_files": trajectory.modified_files,
1288+
"diff_by_files": trajectory.diff_by_files,
1289+
"incremental_diff_by_files": trajectory.incremental_diff_by_files,
1290+
}
1291+
)
1292+
1293+
except Exception as e:
1294+
logger.error(f"Error loading trajectory diffs: {e}", exc_info=True)
1295+
return jsonify({"success": False, "error": str(e)}), 500
1296+
1297+
12471298
# Use run_viewer.py to launch the application

codeclash/viewer/static/css/style.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2145,3 +2145,45 @@ summary:focus {
21452145
height: 2rem;
21462146
border-width: 0.25rem;
21472147
}
2148+
2149+
/* Diff loading UI */
2150+
.diff-load-placeholder,
2151+
.incremental-diff-load-placeholder,
2152+
.modified-files-load-placeholder,
2153+
.analysis-load-placeholder {
2154+
padding: 2rem;
2155+
text-align: center;
2156+
}
2157+
2158+
.load-diffs-btn,
2159+
.load-analysis-btn {
2160+
font-size: 1rem;
2161+
padding: 0.75rem 1.5rem;
2162+
border-radius: 0.5rem;
2163+
transition: all 0.2s ease;
2164+
}
2165+
2166+
.load-diffs-btn:hover,
2167+
.load-analysis-btn:hover {
2168+
transform: translateY(-2px);
2169+
box-shadow: 0 4px 8px rgba(0, 255, 136, 0.3);
2170+
}
2171+
2172+
.diff-loading-spinner,
2173+
.analysis-loading-spinner {
2174+
padding: 2rem;
2175+
text-align: center;
2176+
display: flex;
2177+
align-items: center;
2178+
justify-content: center;
2179+
gap: 1rem;
2180+
color: var(--text-secondary);
2181+
font-size: 1rem;
2182+
}
2183+
2184+
.diff-loading-spinner .spinner-border,
2185+
.analysis-loading-spinner .spinner-border {
2186+
width: 2rem;
2187+
height: 2rem;
2188+
border-width: 0.25rem;
2189+
}

codeclash/viewer/static/js/analysis.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@ function initializeAnalysis() {
1414
return; // No analysis data available
1515
}
1616

17+
// Check if the element has any content before parsing
18+
const textContent = analysisDataElement.textContent.trim();
19+
if (!textContent) {
20+
return; // No data loaded yet (will be loaded on demand)
21+
}
22+
1723
try {
18-
analysisData = JSON.parse(analysisDataElement.textContent);
24+
analysisData = JSON.parse(textContent);
1925

2026
if (
2127
analysisData &&
@@ -280,6 +286,29 @@ function createOverviewScoresChart() {
280286
});
281287
}
282288

289+
/**
290+
* Render line count chart with dynamically loaded data
291+
* This is called by app.js after loading analysis data
292+
*/
293+
window.renderLineCountChart = function (data) {
294+
analysisData = data;
295+
if (
296+
analysisData &&
297+
analysisData.all_files &&
298+
analysisData.all_files.length > 0
299+
) {
300+
setupFileDropdown();
301+
createLineCountChart(analysisData.all_files[0]); // Start with first file
302+
}
303+
};
304+
305+
// Listen for dynamically loaded analysis data
306+
document.addEventListener("analysisDataLoaded", function (event) {
307+
if (event.detail) {
308+
window.renderLineCountChart(event.detail);
309+
}
310+
});
311+
283312
// Initialize all analyses when DOM is loaded
284313
document.addEventListener("DOMContentLoaded", function () {
285314
initializeAnalysis();

0 commit comments

Comments
 (0)