Skip to content

Commit 6bf62fb

Browse files
committed
Add URL rooting
1 parent 9dcd8d1 commit 6bf62fb

File tree

2 files changed

+117
-1
lines changed

2 files changed

+117
-1
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ An AI Agent Trajectory Explorer built with Hugo and JavaScript for viewing AI ag
66

77
- **Experiment Navigation**: Browse through different experiments with previous/next buttons and dropdown selection
88
- **Instance Navigation**: Navigate through instances within each experiment
9+
- **URL Routing**: Direct links to specific experiments and instances with deep linking support
10+
- **Browser Navigation**: Full support for browser back/forward buttons and history
911
- **Overview Display**: Shows exit status, model statistics, API calls, and costs
1012
- **Message Formatting**: Displays all conversation messages with proper formatting for different roles (system, user, assistant)
1113
- **Responsive Design**: Works on desktop and mobile devices
@@ -67,6 +69,33 @@ hugo server
6769

6870
Then open http://localhost:1313 in your browser.
6971

72+
### URL Routing
73+
74+
The trajectory viewer supports direct linking to specific experiments and instances:
75+
76+
**Experiment URLs:**
77+
- `http://localhost:1313/example-experiemnt` - View specific experiment
78+
- `http://localhost:1313/experiment-1` - View experiment-1
79+
- `http://localhost:1313/` - Home page with no experiment selected
80+
81+
**Instance URLs:**
82+
- `http://localhost:1313/example-experiemnt/example_instance` - View specific experiment and instance
83+
- `http://localhost:1313/experiment-1/instance_1` - View experiment-1, instance_1
84+
85+
**Features:**
86+
- **Deep Linking**: Share URLs to specific experiments or instances
87+
- **Browser Navigation**: Back/forward buttons work correctly
88+
- **Auto-Selection**: When navigating to an experiment URL, the first instance is automatically selected
89+
- **URL Updates**: URLs update automatically as you navigate through the interface
90+
- **Bookmarking**: Bookmark specific trajectories and return to them later
91+
92+
**URL Structure:**
93+
```
94+
/ # Home page (no selection)
95+
/{experiment-name} # Experiment with first instance
96+
/{experiment-name}/{instance-name} # Specific experiment and instance
97+
```
98+
7099
### Adding New Experiments
71100

72101
1. Create a new folder in the `data/` directory with your experiment name

static/js/trajectory-viewer.js

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class TrajectoryViewer {
1212
this.initTheme();
1313
await this.loadExperiments();
1414
this.setupEventListeners();
15+
this.initFromURL();
1516
this.updateUI();
1617
}
1718

@@ -74,6 +75,73 @@ class TrajectoryViewer {
7475

7576
console.log('Discovery complete. Found experiments:', this.experiments);
7677
}
78+
79+
initFromURL() {
80+
const { experimentName, instanceName } = this.parseURLSegments();
81+
if (experimentName && this.experiments.some(exp => exp.name === experimentName)) {
82+
this.selectExperiment(experimentName).then(() => {
83+
// If instance is specified in URL and exists, select it
84+
if (instanceName && this.currentExperiment &&
85+
this.currentExperiment.instances.includes(instanceName)) {
86+
this.selectInstance(instanceName);
87+
}
88+
});
89+
}
90+
}
91+
92+
parseURLSegments() {
93+
const path = window.location.pathname;
94+
// Remove leading slash and extract segments
95+
const segments = path.replace(/^\//, '').split('/').filter(s => s);
96+
return {
97+
experimentName: segments[0] || null,
98+
instanceName: segments[1] || null
99+
};
100+
}
101+
102+
parseURLForExperiment() {
103+
// Keep this method for backward compatibility
104+
return this.parseURLSegments().experimentName;
105+
}
106+
107+
updateURL(experimentName, instanceName = null) {
108+
let newPath = '/';
109+
if (experimentName) {
110+
newPath += experimentName;
111+
if (instanceName) {
112+
newPath += `/${instanceName}`;
113+
}
114+
}
115+
116+
// Update URL without reloading the page
117+
if (window.location.pathname !== newPath) {
118+
window.history.pushState(
119+
{ experiment: experimentName, instance: instanceName },
120+
'',
121+
newPath
122+
);
123+
}
124+
}
125+
126+
handlePopState(event) {
127+
// Handle browser back/forward navigation
128+
const { experimentName, instanceName } = this.parseURLSegments();
129+
if (experimentName && this.experiments.some(exp => exp.name === experimentName)) {
130+
this.selectExperiment(experimentName).then(() => {
131+
// If instance is specified in URL and exists, select it
132+
if (instanceName && this.currentExperiment &&
133+
this.currentExperiment.instances.includes(instanceName)) {
134+
this.selectInstance(instanceName);
135+
}
136+
});
137+
} else {
138+
// No experiment in URL, clear selection
139+
this.currentExperiment = null;
140+
this.currentInstance = null;
141+
this.trajectoryData = null;
142+
this.updateUI();
143+
}
144+
}
77145

78146
async discoverInstancesForExperiment(experimentName, instancePatterns) {
79147
const foundInstances = [];
@@ -126,6 +194,11 @@ class TrajectoryViewer {
126194
document.getElementById('theme-toggle').addEventListener('click', () => {
127195
this.toggleTheme();
128196
});
197+
198+
// Browser navigation (back/forward)
199+
window.addEventListener('popstate', (event) => {
200+
this.handlePopState(event);
201+
});
129202
}
130203

131204
initTheme() {
@@ -268,12 +341,22 @@ class TrajectoryViewer {
268341
}
269342

270343
async selectExperiment(experimentName) {
271-
if (!experimentName) return;
344+
if (!experimentName) {
345+
this.currentExperiment = null;
346+
this.currentInstance = null;
347+
this.trajectoryData = null;
348+
this.updateURL('');
349+
this.updateUI();
350+
return;
351+
}
272352

273353
this.currentExperiment = this.experiments.find(exp => exp.name === experimentName);
274354
this.currentInstance = null;
275355
this.trajectoryData = null;
276356

357+
// Update URL with experiment name
358+
this.updateURL(experimentName);
359+
277360
// Auto-select first instance if available
278361
if (this.currentExperiment && this.currentExperiment.instances.length > 0) {
279362
await this.selectInstance(this.currentExperiment.instances[0]);
@@ -286,6 +369,10 @@ class TrajectoryViewer {
286369
if (!instanceName || !this.currentExperiment) return;
287370

288371
this.currentInstance = instanceName;
372+
373+
// Update URL with both experiment and instance
374+
this.updateURL(this.currentExperiment.name, instanceName);
375+
289376
await this.loadTrajectoryData();
290377
this.updateUI();
291378
}

0 commit comments

Comments
 (0)