dlouapre HF Staff commited on
Commit
7f7230a
·
1 Parent(s): e9037fe

Improved UI

Browse files
IDEAS.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ - center the main title
3
+
4
+ ## Editor area (on the left)
5
+
6
+ - line numbering
7
+ - syntax highlighting of keywords (green), durations (blue), parameters or qualitative (orange), comments (gray)
8
+ - Undo / Redo buttons ?
9
+ - highlight the line in case of error at verification, with a message indicating the error below the script area
10
+
11
+ ## Robot area (on the right)
12
+
13
+ - better icons
14
+
15
+ ## Console area (bottom)
16
+ - only one console at the bottom. Simplified messages, output for both script and robot?
17
+
18
+ ## Misc
19
+
20
+
rmscript_app/static/app.js CHANGED
@@ -1,7 +1,6 @@
1
  // RMScript App - Frontend Application
2
- // Connects to the settings_app backend for compilation and server-side execution
3
 
4
- // API is served from same origin (settings_app)
5
  const API_BASE = '';
6
 
7
  // Global state
@@ -10,46 +9,98 @@ const state = {
10
  isExecuting: false
11
  };
12
 
13
- // DOM elements
14
- const elements = {
15
- editor: document.getElementById('editor'),
16
- compileStatus: document.getElementById('compileStatus'),
17
- robotStatus: document.getElementById('robotStatus'),
18
- console: document.getElementById('console'),
19
- irDisplay: document.getElementById('irDisplay'),
20
- executionInfo: document.getElementById('executionInfo'),
21
- executeBtn: document.getElementById('executeBtn')
22
- };
23
 
24
- // Example scripts
25
  const examples = {
26
- basic: `look left
 
 
27
  wait 1s
28
  look right
29
  wait 1s
30
- look center`,
31
-
32
- complex: `look left
33
- antenna both up
34
- wait 1s
35
- look right
36
- antenna both down
37
- wait 0.5s
38
- look center`,
 
 
 
 
 
 
39
 
40
- repeat: `repeat 3
 
 
41
  look left
42
- wait 0.5s
 
43
  look right
44
- wait 0.5s
45
- look center`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  };
47
 
48
- // Update compile status UI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  function updateCompileStatus(status, message) {
50
- const dot = status === 'success' ? 'green' : status === 'error' ? 'red' : 'gray';
51
- elements.compileStatus.className = `status ${status}`;
52
- elements.compileStatus.innerHTML = `<span><span class="status-dot ${dot}"></span>${message}</span>`;
53
  }
54
 
55
  // Log to console
@@ -66,25 +117,280 @@ function clearConsole() {
66
  elements.console.innerHTML = '';
67
  }
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  // Load example script
70
  function loadExample(exampleName) {
71
- if (examples[exampleName]) {
72
- elements.editor.value = examples[exampleName];
73
- log(`Loaded ${exampleName} example`, 'info');
 
 
74
  }
75
  }
76
 
77
  // Clear editor
78
  function clearEditor() {
79
- elements.editor.value = '';
 
 
 
 
 
80
  state.currentIR = null;
81
- elements.irDisplay.innerHTML = '<div style="color: #999; text-align: center; padding: 20px;">No IR yet. Verify a script first!</div>';
82
  updateCompileStatus('idle', 'Ready');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
84
 
85
- // Verify script (syntax and semantics only)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  async function verifyScript() {
87
- const source = elements.editor.value.trim();
88
 
89
  if (!source) {
90
  log('Editor is empty', 'warning');
@@ -109,19 +415,23 @@ async function verifyScript() {
109
  if (result.name) log(` Name: ${result.name}`, 'info');
110
  if (result.description) log(` Description: ${result.description}`, 'info');
111
 
112
- if (result.warnings.length > 0) {
113
  result.warnings.forEach(w => {
114
  log(` Warning (line ${w.line}): ${w.message}`, 'warning');
115
  });
116
  }
117
 
118
  updateCompileStatus('success', 'Valid script');
 
119
  } else {
120
  log('Verification failed', 'error');
121
  result.errors.forEach(e => {
122
  log(` Error (line ${e.line}): ${e.message}`, 'error');
123
  });
124
  updateCompileStatus('error', 'Invalid script');
 
 
 
125
  }
126
  } catch (error) {
127
  log(`Backend error: ${error.message}`, 'error');
@@ -131,7 +441,7 @@ async function verifyScript() {
131
 
132
  // Compile script to IR
133
  async function compileScript() {
134
- const source = elements.editor.value.trim();
135
 
136
  if (!source) {
137
  log('Editor is empty', 'warning');
@@ -153,7 +463,7 @@ async function compileScript() {
153
  if (result.success) {
154
  log(`Compiled ${result.ir.length} actions`, 'success');
155
 
156
- if (result.warnings.length > 0) {
157
  result.warnings.forEach(w => {
158
  log(` Warning (line ${w.line}): ${w.message}`, 'warning');
159
  });
@@ -179,14 +489,16 @@ async function compileScript() {
179
  }
180
 
181
  // Display IR in the UI
182
- function displayIR(ir) {
183
  if (!ir || ir.length === 0) {
184
- elements.irDisplay.innerHTML = '<div style="color: #999; text-align: center; padding: 20px;">No actions</div>';
185
  return;
186
  }
187
 
188
  const html = ir.map((action, idx) => {
189
  let details = '';
 
 
190
 
191
  if (action.type === 'action') {
192
  details = `Duration: ${action.duration}s`;
@@ -204,9 +516,12 @@ function displayIR(ir) {
204
  }
205
 
206
  return `
207
- <div class="ir-action">
208
- <div class="ir-action-type">${idx + 1}. ${action.type.toUpperCase()}</div>
209
- <div class="ir-action-details">${details}</div>
 
 
 
210
  </div>
211
  `;
212
  }).join('');
@@ -214,25 +529,77 @@ function displayIR(ir) {
214
  elements.irDisplay.innerHTML = html;
215
  }
216
 
217
- // Execute script on robot (server-side via SDK)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  async function executeScript() {
219
  if (state.isExecuting) {
220
  log('Already executing a script', 'warning');
221
  return;
222
  }
223
 
224
- // Compile the current editor content
225
  const ir = await compileScript();
226
  if (!ir) {
227
  log('Cannot execute - compilation failed', 'error');
 
228
  return;
229
  }
230
 
231
- // Execute via server-side SDK
232
  state.isExecuting = true;
233
  elements.executeBtn.disabled = true;
234
- clearExecutionInfo();
235
- logExecution('Sending to robot...', 'info');
 
 
 
 
 
 
236
 
237
  try {
238
  const response = await fetch(`${API_BASE}/api/execute`, {
@@ -243,40 +610,78 @@ async function executeScript() {
243
 
244
  const result = await response.json();
245
 
 
 
 
246
  if (result.success) {
247
- logExecution(`Execution complete! ${result.actions_executed} actions executed.`, 'success');
 
 
 
248
  } else {
249
- logExecution(`Execution failed: ${result.message}`, 'error');
 
 
 
250
  }
251
  } catch (error) {
252
- logExecution(`Execution error: ${error.message}`, 'error');
 
 
 
253
  } finally {
254
  state.isExecuting = false;
255
  elements.executeBtn.disabled = false;
 
 
256
  }
257
  }
258
 
259
- // Log execution info
260
  function logExecution(message, type = 'info') {
261
- const line = document.createElement('div');
262
- line.className = `console-line ${type}`;
263
- line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
264
- elements.executionInfo.appendChild(line);
265
- elements.executionInfo.scrollTop = elements.executionInfo.scrollHeight;
266
- }
267
-
268
- // Clear execution info
269
- function clearExecutionInfo() {
270
- elements.executionInfo.innerHTML = '';
271
  }
272
 
273
  // Initialize application
274
  function init() {
275
  console.log('Initializing RMScript App');
276
- loadExample('basic');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  console.log('RMScript App ready');
278
  }
279
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  // Start when DOM is loaded
281
  if (document.readyState === 'loading') {
282
  document.addEventListener('DOMContentLoaded', init);
 
1
  // RMScript App - Frontend Application
 
2
 
3
+ // API is served from same origin
4
  const API_BASE = '';
5
 
6
  // Global state
 
9
  isExecuting: false
10
  };
11
 
12
+ // Debounce timer for real-time validation
13
+ let validationTimer = null;
14
+ const VALIDATION_DELAY = 500;
 
 
 
 
 
 
 
15
 
16
+ // Example scripts with better names
17
  const examples = {
18
+ look_around: {
19
+ name: 'Look around',
20
+ code: `look left
21
  wait 1s
22
  look right
23
  wait 1s
24
+ look center`
25
+ },
26
+
27
+ nod: {
28
+ name: 'Nod',
29
+ code: `look up
30
+ wait 0.3s
31
+ look down
32
+ wait 0.3s
33
+ look up
34
+ wait 0.3s
35
+ look down
36
+ wait 0.3s
37
+ look center`
38
+ },
39
 
40
+ dance: {
41
+ name: 'Dance',
42
+ code: `repeat 3
43
  look left
44
+ antenna both up
45
+ wait 0.4s
46
  look right
47
+ antenna both down
48
+ wait 0.4s
49
+ look center
50
+ antenna both up`
51
+ },
52
+
53
+ antenna_wave: {
54
+ name: 'Antenna wave',
55
+ code: `antenna left up
56
+ wait 0.3s
57
+ antenna right up
58
+ antenna left down
59
+ wait 0.3s
60
+ antenna right down
61
+ antenna left up
62
+ wait 0.3s
63
+ antenna both up`
64
+ }
65
  };
66
 
67
+ // DOM elements (populated after DOM loads)
68
+ let elements = {};
69
+
70
+ // Action type to icon mapping
71
+ const ACTION_ICONS = {
72
+ action: '🔄',
73
+ wait: '⏳',
74
+ picture: '📷',
75
+ sound: '🔊'
76
+ };
77
+
78
+ function getActionIcon(action) {
79
+ if (action.type === 'action') {
80
+ if (action.antennas) return '📡';
81
+ if (action.head_pose) return '🔄';
82
+ if (action.body_yaw !== null && action.body_yaw !== undefined) return '↩️';
83
+ return '🔄';
84
+ }
85
+ return ACTION_ICONS[action.type] || '▶️';
86
+ }
87
+
88
+ // Get editor content
89
+ function getEditorContent() {
90
+ return elements.editor ? elements.editor.value : '';
91
+ }
92
+
93
+ // Set editor content
94
+ function setEditorContent(content) {
95
+ if (elements.editor) {
96
+ elements.editor.value = content;
97
+ }
98
+ }
99
+
100
+ // Update compile status UI (below editor)
101
  function updateCompileStatus(status, message) {
102
+ elements.compileStatus.className = `compile-status ${status}`;
103
+ elements.compileStatus.querySelector('.status-message').textContent = message;
 
104
  }
105
 
106
  // Log to console
 
117
  elements.console.innerHTML = '';
118
  }
119
 
120
+ // Check if editor has content
121
+ function editorHasContent() {
122
+ return getEditorContent().trim().length > 0;
123
+ }
124
+
125
+ // Handle dropdown selection for examples
126
+ function handleExampleSelect(selectElement) {
127
+ const exampleName = selectElement.value;
128
+ if (!exampleName) return;
129
+
130
+ selectElement.selectedIndex = 0;
131
+
132
+ if (editorHasContent()) {
133
+ if (!confirm(`Load "${examples[exampleName].name}" example? This will replace your current script.`)) {
134
+ return;
135
+ }
136
+ }
137
+ loadExample(exampleName);
138
+ }
139
+
140
  // Load example script
141
  function loadExample(exampleName) {
142
+ const example = examples[exampleName];
143
+ if (example) {
144
+ setEditorContent(example.code);
145
+ log(`Loaded "${example.name}" example`, 'info');
146
+ scheduleValidation();
147
  }
148
  }
149
 
150
  // Clear editor
151
  function clearEditor() {
152
+ if (editorHasContent()) {
153
+ if (!confirm('Clear the editor? This will remove your current script.')) {
154
+ return;
155
+ }
156
+ }
157
+ setEditorContent('');
158
  state.currentIR = null;
159
+ elements.irDisplay.innerHTML = '<div style="color: #999; text-align: center; padding: 20px;">No actions yet. Write a script and click Run!</div>';
160
  updateCompileStatus('idle', 'Ready');
161
+ hideError();
162
+ log('Editor cleared', 'info');
163
+ }
164
+
165
+ // localStorage key for saved scripts
166
+ const STORAGE_KEY = 'rmscript_saved_scripts';
167
+
168
+ function getSavedScripts() {
169
+ try {
170
+ const data = localStorage.getItem(STORAGE_KEY);
171
+ return data ? JSON.parse(data) : [];
172
+ } catch {
173
+ return [];
174
+ }
175
+ }
176
+
177
+ function saveSavedScripts(scripts) {
178
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(scripts));
179
+ }
180
+
181
+ function saveScript() {
182
+ const source = getEditorContent().trim();
183
+ if (!source) {
184
+ log('Cannot save empty script', 'warning');
185
+ return;
186
+ }
187
+
188
+ const name = prompt('Enter a name for your script:');
189
+ if (!name || !name.trim()) {
190
+ return;
191
+ }
192
+
193
+ const scripts = getSavedScripts();
194
+ const existingIndex = scripts.findIndex(s => s.name === name.trim());
195
+
196
+ const scriptData = {
197
+ name: name.trim(),
198
+ source: source,
199
+ savedAt: new Date().toISOString()
200
+ };
201
+
202
+ if (existingIndex >= 0) {
203
+ if (!confirm(`A script named "${name}" already exists. Overwrite it?`)) {
204
+ return;
205
+ }
206
+ scripts[existingIndex] = scriptData;
207
+ } else {
208
+ scripts.unshift(scriptData);
209
+ }
210
+
211
+ saveSavedScripts(scripts);
212
+ log(`Script saved as "${name}"`, 'success');
213
+ }
214
+
215
+ function showLoadDialog() {
216
+ const scripts = getSavedScripts();
217
+ const listContainer = document.getElementById('savedScriptsList');
218
+
219
+ if (scripts.length === 0) {
220
+ listContainer.innerHTML = '<div class="no-saved-scripts">No saved scripts yet.<br>Use the Save button to save your scripts.</div>';
221
+ } else {
222
+ listContainer.innerHTML = scripts.map((script, index) => {
223
+ const date = new Date(script.savedAt).toLocaleString();
224
+ return `
225
+ <div class="saved-script-item" onclick="loadSavedScript(${index})">
226
+ <div class="saved-script-info">
227
+ <div class="saved-script-name">${escapeHtml(script.name)}</div>
228
+ <div class="saved-script-date">Saved: ${date}</div>
229
+ </div>
230
+ <button class="saved-script-delete" onclick="deleteSavedScript(event, ${index})">Delete</button>
231
+ </div>
232
+ `;
233
+ }).join('');
234
+ }
235
+
236
+ document.getElementById('loadModal').style.display = 'flex';
237
+ }
238
+
239
+ function hideLoadDialog() {
240
+ document.getElementById('loadModal').style.display = 'none';
241
+ }
242
+
243
+ function loadSavedScript(index) {
244
+ const scripts = getSavedScripts();
245
+ if (index >= 0 && index < scripts.length) {
246
+ const script = scripts[index];
247
+
248
+ if (editorHasContent()) {
249
+ if (!confirm(`Load "${script.name}"? This will replace your current script.`)) {
250
+ return;
251
+ }
252
+ }
253
+
254
+ setEditorContent(script.source);
255
+ log(`Loaded "${script.name}"`, 'info');
256
+ scheduleValidation();
257
+ hideLoadDialog();
258
+ }
259
+ }
260
+
261
+ function deleteSavedScript(event, index) {
262
+ event.stopPropagation();
263
+ const scripts = getSavedScripts();
264
+
265
+ if (index >= 0 && index < scripts.length) {
266
+ const script = scripts[index];
267
+ if (confirm(`Delete "${script.name}"?`)) {
268
+ scripts.splice(index, 1);
269
+ saveSavedScripts(scripts);
270
+ log(`Deleted "${script.name}"`, 'info');
271
+ showLoadDialog();
272
+ }
273
+ }
274
+ }
275
+
276
+ function escapeHtml(text) {
277
+ const div = document.createElement('div');
278
+ div.textContent = text;
279
+ return div.innerHTML;
280
+ }
281
+
282
+ // Cheat sheet
283
+ function showCheatSheet() {
284
+ document.getElementById('cheatSheetModal').style.display = 'flex';
285
+ }
286
+
287
+ function hideCheatSheet() {
288
+ document.getElementById('cheatSheetModal').style.display = 'none';
289
+ }
290
+
291
+ // Browser Sound Feedback using Web Audio API
292
+ let audioContext = null;
293
+
294
+ function getAudioContext() {
295
+ if (!audioContext) {
296
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
297
+ }
298
+ return audioContext;
299
+ }
300
+
301
+ function playTone(frequency, duration, type = 'sine') {
302
+ try {
303
+ const ctx = getAudioContext();
304
+ const oscillator = ctx.createOscillator();
305
+ const gainNode = ctx.createGain();
306
+
307
+ oscillator.connect(gainNode);
308
+ gainNode.connect(ctx.destination);
309
+
310
+ oscillator.frequency.value = frequency;
311
+ oscillator.type = type;
312
+
313
+ gainNode.gain.setValueAtTime(0.3, ctx.currentTime);
314
+ gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration);
315
+
316
+ oscillator.start(ctx.currentTime);
317
+ oscillator.stop(ctx.currentTime + duration);
318
+ } catch (e) {
319
+ console.log('Audio not available:', e);
320
+ }
321
+ }
322
+
323
+ function playStartSound() {
324
+ playTone(523, 0.1);
325
+ setTimeout(() => playTone(659, 0.1), 100);
326
+ setTimeout(() => playTone(784, 0.15), 200);
327
+ }
328
+
329
+ function playSuccessSound() {
330
+ playTone(523, 0.2);
331
+ setTimeout(() => playTone(659, 0.2), 50);
332
+ setTimeout(() => playTone(784, 0.3), 100);
333
+ }
334
+
335
+ function playErrorSound() {
336
+ playTone(400, 0.15);
337
+ setTimeout(() => playTone(300, 0.2), 150);
338
+ }
339
+
340
+ // Show error in compile status
341
+ function showError(message, lineNumber) {
342
+ const prefix = lineNumber ? `Line ${lineNumber}: ` : '';
343
+ updateCompileStatus('error', prefix + message);
344
+ elements.editor.classList.add('has-error');
345
+ }
346
+
347
+ function hideError() {
348
+ elements.editor.classList.remove('has-error');
349
  }
350
 
351
+ // Real-time validation
352
+ async function validateRealtime() {
353
+ const source = getEditorContent().trim();
354
+
355
+ if (!source) {
356
+ updateCompileStatus('idle', 'Ready');
357
+ hideError();
358
+ return;
359
+ }
360
+
361
+ try {
362
+ const response = await fetch(`${API_BASE}/api/verify`, {
363
+ method: 'POST',
364
+ headers: { 'Content-Type': 'application/json' },
365
+ body: JSON.stringify({ source })
366
+ });
367
+
368
+ const result = await response.json();
369
+
370
+ if (result.success) {
371
+ updateCompileStatus('success', 'Valid script');
372
+ hideError();
373
+ } else {
374
+ updateCompileStatus('error', 'Invalid script');
375
+ if (result.errors && result.errors.length > 0) {
376
+ showError(result.errors[0].message, result.errors[0].line);
377
+ }
378
+ }
379
+ } catch (error) {
380
+ updateCompileStatus('error', 'Backend error');
381
+ }
382
+ }
383
+
384
+ function scheduleValidation() {
385
+ if (validationTimer) {
386
+ clearTimeout(validationTimer);
387
+ }
388
+ validationTimer = setTimeout(validateRealtime, VALIDATION_DELAY);
389
+ }
390
+
391
+ // Verify script
392
  async function verifyScript() {
393
+ const source = getEditorContent().trim();
394
 
395
  if (!source) {
396
  log('Editor is empty', 'warning');
 
415
  if (result.name) log(` Name: ${result.name}`, 'info');
416
  if (result.description) log(` Description: ${result.description}`, 'info');
417
 
418
+ if (result.warnings && result.warnings.length > 0) {
419
  result.warnings.forEach(w => {
420
  log(` Warning (line ${w.line}): ${w.message}`, 'warning');
421
  });
422
  }
423
 
424
  updateCompileStatus('success', 'Valid script');
425
+ hideError();
426
  } else {
427
  log('Verification failed', 'error');
428
  result.errors.forEach(e => {
429
  log(` Error (line ${e.line}): ${e.message}`, 'error');
430
  });
431
  updateCompileStatus('error', 'Invalid script');
432
+ if (result.errors && result.errors.length > 0) {
433
+ showError(result.errors[0].message, result.errors[0].line);
434
+ }
435
  }
436
  } catch (error) {
437
  log(`Backend error: ${error.message}`, 'error');
 
441
 
442
  // Compile script to IR
443
  async function compileScript() {
444
+ const source = getEditorContent().trim();
445
 
446
  if (!source) {
447
  log('Editor is empty', 'warning');
 
463
  if (result.success) {
464
  log(`Compiled ${result.ir.length} actions`, 'success');
465
 
466
+ if (result.warnings && result.warnings.length > 0) {
467
  result.warnings.forEach(w => {
468
  log(` Warning (line ${w.line}): ${w.message}`, 'warning');
469
  });
 
489
  }
490
 
491
  // Display IR in the UI
492
+ function displayIR(ir, currentActionIndex = -1) {
493
  if (!ir || ir.length === 0) {
494
+ elements.irDisplay.innerHTML = '<div style="color: #999; text-align: center; padding: 20px;">No actions yet. Write a script and click Run!</div>';
495
  return;
496
  }
497
 
498
  const html = ir.map((action, idx) => {
499
  let details = '';
500
+ const icon = getActionIcon(action);
501
+ const isCurrent = idx === currentActionIndex;
502
 
503
  if (action.type === 'action') {
504
  details = `Duration: ${action.duration}s`;
 
516
  }
517
 
518
  return `
519
+ <div class="ir-action${isCurrent ? ' current' : ''}" data-action-index="${idx}">
520
+ <span class="ir-action-icon">${icon}</span>
521
+ <div class="ir-action-content">
522
+ <div class="ir-action-type">${idx + 1}. ${action.type.toUpperCase()}</div>
523
+ <div class="ir-action-details">${details}</div>
524
+ </div>
525
  </div>
526
  `;
527
  }).join('');
 
529
  elements.irDisplay.innerHTML = html;
530
  }
531
 
532
+ // Update timeline status
533
+ function updateTimeline(status, message, icon = '🤖') {
534
+ const timeline = elements.executionTimeline;
535
+ if (!timeline) return;
536
+
537
+ timeline.innerHTML = `
538
+ <div class="timeline-status ${status}">
539
+ <span class="timeline-icon">${icon}</span>
540
+ <span class="timeline-text">${message}</span>
541
+ </div>
542
+ `;
543
+ }
544
+
545
+ // Helper to describe an action for timeline display
546
+ function describeAction(action) {
547
+ if (action.type === 'wait') return 'Wait';
548
+ if (action.type === 'picture') return 'Take picture';
549
+ if (action.type === 'sound') return `Play sound`;
550
+ if (action.type === 'action') {
551
+ if (action.head_pose) return 'Head movement';
552
+ if (action.antennas) return 'Antenna';
553
+ if (action.body_yaw !== null && action.body_yaw !== undefined) return 'Turn body';
554
+ return 'Movement';
555
+ }
556
+ return action.type;
557
+ }
558
+
559
+ // Sleep helper
560
+ function sleep(ms) {
561
+ return new Promise(resolve => setTimeout(resolve, ms));
562
+ }
563
+
564
+ // Simulate execution progress (runs in parallel with actual execution)
565
+ async function simulateProgress(ir) {
566
+ for (let i = 0; i < ir.length; i++) {
567
+ if (!state.isExecuting) break; // Stop if execution ended
568
+
569
+ displayIR(ir, i); // Highlight current action
570
+ const desc = describeAction(ir[i]);
571
+ updateTimeline('running', `Step ${i + 1}/${ir.length}: ${desc}`, '▶️');
572
+
573
+ // Wait for action duration
574
+ const duration = ir[i].duration || 0.5;
575
+ await sleep(duration * 1000);
576
+ }
577
+ }
578
+
579
+ // Execute script on robot
580
  async function executeScript() {
581
  if (state.isExecuting) {
582
  log('Already executing a script', 'warning');
583
  return;
584
  }
585
 
 
586
  const ir = await compileScript();
587
  if (!ir) {
588
  log('Cannot execute - compilation failed', 'error');
589
+ updateTimeline('error', 'Compilation failed', '❌');
590
  return;
591
  }
592
 
 
593
  state.isExecuting = true;
594
  elements.executeBtn.disabled = true;
595
+ elements.executeBtn.classList.add('running');
596
+ elements.executeBtn.textContent = 'Running...';
597
+ clearConsole();
598
+ log('Sending to robot...', 'info');
599
+ playStartSound();
600
+
601
+ // Start progress simulation in parallel with actual execution
602
+ const progressPromise = simulateProgress(ir);
603
 
604
  try {
605
  const response = await fetch(`${API_BASE}/api/execute`, {
 
610
 
611
  const result = await response.json();
612
 
613
+ // Wait for progress simulation to complete
614
+ await progressPromise;
615
+
616
  if (result.success) {
617
+ log(`Done! ${result.actions_executed} actions executed.`, 'success');
618
+ updateTimeline('idle', 'Robot connected. Ready to run!', '🤖');
619
+ displayIR(ir, -1); // Clear highlighting
620
+ playSuccessSound();
621
  } else {
622
+ log(`Execution failed: ${result.message}`, 'error');
623
+ updateTimeline('error', `Failed: ${result.message}`, '❌');
624
+ displayIR(ir, -1);
625
+ playErrorSound();
626
  }
627
  } catch (error) {
628
+ log(`Execution error: ${error.message}`, 'error');
629
+ updateTimeline('error', `Error: ${error.message}`, '❌');
630
+ displayIR(ir, -1);
631
+ playErrorSound();
632
  } finally {
633
  state.isExecuting = false;
634
  elements.executeBtn.disabled = false;
635
+ elements.executeBtn.classList.remove('running');
636
+ elements.executeBtn.textContent = 'Run on robot';
637
  }
638
  }
639
 
640
+ // Log execution info (now uses main console)
641
  function logExecution(message, type = 'info') {
642
+ log(message, type);
 
 
 
 
 
 
 
 
 
643
  }
644
 
645
  // Initialize application
646
  function init() {
647
  console.log('Initializing RMScript App');
648
+
649
+ // Get DOM elements
650
+ elements = {
651
+ editor: document.getElementById('editor'),
652
+ compileStatus: document.getElementById('compileStatus'),
653
+ console: document.getElementById('console'),
654
+ irDisplay: document.getElementById('irDisplay'),
655
+ executeBtn: document.getElementById('executeBtn'),
656
+ executionTimeline: document.getElementById('executionTimeline')
657
+ };
658
+
659
+ // Set up real-time validation on editor input
660
+ elements.editor.addEventListener('input', scheduleValidation);
661
+
662
+ // Load initial example
663
+ setEditorContent(examples.look_around.code);
664
+ log('Loaded "Look around" example', 'info');
665
+
666
+ // Trigger initial validation
667
+ setTimeout(validateRealtime, 100);
668
+
669
  console.log('RMScript App ready');
670
  }
671
 
672
+ // Make functions available globally for onclick handlers
673
+ window.handleExampleSelect = handleExampleSelect;
674
+ window.clearEditor = clearEditor;
675
+ window.saveScript = saveScript;
676
+ window.showLoadDialog = showLoadDialog;
677
+ window.hideLoadDialog = hideLoadDialog;
678
+ window.loadSavedScript = loadSavedScript;
679
+ window.deleteSavedScript = deleteSavedScript;
680
+ window.showCheatSheet = showCheatSheet;
681
+ window.hideCheatSheet = hideCheatSheet;
682
+ window.verifyScript = verifyScript;
683
+ window.executeScript = executeScript;
684
+
685
  // Start when DOM is loaded
686
  if (document.readyState === 'loading') {
687
  document.addEventListener('DOMContentLoaded', init);
rmscript_app/static/index.html CHANGED
@@ -3,62 +3,145 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>RMScript App</title>
7
  <link rel="stylesheet" href="/static/style.css">
8
  </head>
9
  <body>
 
 
 
 
 
 
10
  <div class="container">
11
  <!-- Left Panel: Editor -->
12
  <div class="panel editor-panel">
13
- <h1>RMScript Editor</h1>
14
- <p class="subtitle">Write and execute rmscript on your Reachy Mini</p>
15
-
16
- <div id="compileStatus" class="status idle">
17
- <span><span class="status-dot gray"></span>Ready</span>
18
- </div>
19
-
20
- <div class="section-title">Examples</div>
21
- <div class="example-buttons">
22
- <button class="secondary" onclick="loadExample('basic')">Basic</button>
23
- <button class="secondary" onclick="loadExample('complex')">Complex</button>
24
- <button class="secondary" onclick="loadExample('repeat')">Repeat</button>
25
- <button class="secondary" onclick="clearEditor()">Clear</button>
 
 
 
26
  </div>
27
 
28
- <textarea id="editor" class="editor" placeholder='look left
29
  wait 1s
30
- look right'></textarea>
 
 
 
 
 
31
 
32
  <div class="button-group">
33
- <button onclick="verifyScript()">Verify</button>
34
- <button id="executeBtn" onclick="executeScript()">Execute</button>
35
  </div>
36
 
37
  <div class="section-title">Console</div>
38
  <div id="console" class="console">
39
- <div class="console-line info">Ready. Write some rmscript and click Verify!</div>
40
  </div>
41
  </div>
42
 
43
- <!-- Right Panel: Execution Info -->
44
  <div class="panel">
45
- <h1>Robot Control</h1>
46
- <p class="subtitle">Scripts execute via SDK with proper timing</p>
47
 
48
- <div id="robotStatus" class="status connected">
49
- <span><span class="status-dot green"></span>App Running</span>
 
 
 
50
  </div>
51
 
52
- <div class="section-title">Compiled IR</div>
53
  <div id="irDisplay" class="ir-display">
54
  <div style="color: #999; text-align: center; padding: 20px;">
55
- No IR yet. Verify a script first!
56
  </div>
57
  </div>
58
 
59
- <div class="section-title" style="margin-top: 20px;">Execution Info</div>
60
- <div id="executionInfo" class="console" style="max-height: 150px;">
61
- <div class="console-line info">Ready to execute scripts</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  </div>
63
  </div>
64
  </div>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Program your Reachy Mini Robot</title>
7
  <link rel="stylesheet" href="/static/style.css">
8
  </head>
9
  <body>
10
+ <!-- Page Header -->
11
+ <header class="page-header">
12
+ <span class="header-icon">🤖</span>
13
+ <h1>Program your Reachy Mini Robot</h1>
14
+ </header>
15
+
16
  <div class="container">
17
  <!-- Left Panel: Editor -->
18
  <div class="panel editor-panel">
19
+ <div class="editor-toolbar">
20
+ <div class="toolbar-left">
21
+ <button class="btn-save small" onclick="saveScript()">Save</button>
22
+ <button class="btn-load small" onclick="showLoadDialog()">Load</button>
23
+ <button class="btn-clear small" onclick="clearEditor()">Clear</button>
24
+ <select id="examplesDropdown" class="examples-dropdown" onchange="handleExampleSelect(this)">
25
+ <option value="" disabled selected>Examples...</option>
26
+ <option value="look_around">Look around</option>
27
+ <option value="nod">Nod</option>
28
+ <option value="dance">Dance</option>
29
+ <option value="antenna_wave">Antenna wave</option>
30
+ </select>
31
+ </div>
32
+ <div class="toolbar-right">
33
+ <button class="btn-help small" onclick="showCheatSheet()">Help</button>
34
+ </div>
35
  </div>
36
 
37
+ <textarea id="editor" class="editor" placeholder="look left
38
  wait 1s
39
+ look right" spellcheck="false"></textarea>
40
+
41
+ <div id="compileStatus" class="compile-status idle">
42
+ <span class="status-dot"></span>
43
+ <span class="status-message">Ready</span>
44
+ </div>
45
 
46
  <div class="button-group">
47
+ <button id="executeBtn" onclick="executeScript()">Run on robot</button>
 
48
  </div>
49
 
50
  <div class="section-title">Console</div>
51
  <div id="console" class="console">
52
+ <div class="console-line info">Ready. Write a script and click Run!</div>
53
  </div>
54
  </div>
55
 
56
+ <!-- Right Panel: Robot Control -->
57
  <div class="panel">
58
+ <h2>Robot Control</h2>
 
59
 
60
+ <div id="executionTimeline" class="execution-timeline">
61
+ <div class="timeline-status idle">
62
+ <span class="timeline-icon">🤖</span>
63
+ <span class="timeline-text">Robot connected. Ready to run!</span>
64
+ </div>
65
  </div>
66
 
67
+ <div class="section-title">Robot's action list</div>
68
  <div id="irDisplay" class="ir-display">
69
  <div style="color: #999; text-align: center; padding: 20px;">
70
+ No actions yet. Write a script and click Run!
71
  </div>
72
  </div>
73
 
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Load Script Modal -->
78
+ <div id="loadModal" class="modal" style="display: none;">
79
+ <div class="modal-content">
80
+ <div class="modal-header">
81
+ <h3>Load Saved Script</h3>
82
+ <button class="modal-close" onclick="hideLoadDialog()">&times;</button>
83
+ </div>
84
+ <div class="modal-body">
85
+ <div id="savedScriptsList"></div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Cheat Sheet Modal -->
91
+ <div id="cheatSheetModal" class="modal" style="display: none;">
92
+ <div class="modal-content cheat-sheet-modal">
93
+ <div class="modal-header">
94
+ <h3>RMScript Commands</h3>
95
+ <button class="modal-close" onclick="hideCheatSheet()">&times;</button>
96
+ </div>
97
+ <div class="modal-body cheat-sheet-body">
98
+ <div class="cheat-section">
99
+ <h4>Head Movement</h4>
100
+ <code>look left</code> <code>look right</code> <code>look up</code> <code>look down</code> <code>look center</code>
101
+ <p class="cheat-note">Add angle: <code>look left 45</code> or use words: <code>look left tiny</code></p>
102
+ </div>
103
+
104
+ <div class="cheat-section">
105
+ <h4>Body Rotation</h4>
106
+ <code>turn left</code> <code>turn right</code> <code>turn center</code>
107
+ </div>
108
+
109
+ <div class="cheat-section">
110
+ <h4>Antennas</h4>
111
+ <code>antenna both up</code> <code>antenna left down</code> <code>antenna right up</code>
112
+ <p class="cheat-note">Directions: up, down, left, right</p>
113
+ </div>
114
+
115
+ <div class="cheat-section">
116
+ <h4>Timing</h4>
117
+ <code>wait 1s</code> <code>wait 0.5s</code>
118
+ <p class="cheat-note">Duration after commands: <code>look left fast</code> <code>look left slow</code></p>
119
+ </div>
120
+
121
+ <div class="cheat-section">
122
+ <h4>Repeat</h4>
123
+ <pre>repeat 3
124
+ look left
125
+ wait 0.5s
126
+ look right</pre>
127
+ <p class="cheat-note">Indent commands inside repeat blocks</p>
128
+ </div>
129
+
130
+ <div class="cheat-section">
131
+ <h4>Combine with "and"</h4>
132
+ <code>look left and antenna both up</code>
133
+ <p class="cheat-note">Movements happen simultaneously</p>
134
+ </div>
135
+
136
+ <div class="cheat-section">
137
+ <h4>Strength Words</h4>
138
+ <code>tiny</code> <code>small</code> <code>medium</code> <code>large</code> <code>maximum</code>
139
+ </div>
140
+
141
+ <div class="cheat-section">
142
+ <h4>Speed Words</h4>
143
+ <code>superfast</code> <code>fast</code> <code>slow</code> <code>superslow</code>
144
+ </div>
145
  </div>
146
  </div>
147
  </div>
rmscript_app/static/style.css CHANGED
@@ -11,6 +11,26 @@ body {
11
  padding: 20px;
12
  }
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  .container {
15
  max-width: 1400px;
16
  margin: 0 auto;
@@ -43,78 +63,118 @@ h1 {
43
  font-size: 14px;
44
  }
45
 
46
- .status {
47
- padding: 12px;
 
 
 
 
48
  border-radius: 8px;
49
  margin-bottom: 15px;
50
  font-size: 14px;
51
- display: flex;
52
- justify-content: space-between;
53
- align-items: center;
54
  }
55
 
56
- .status.idle {
57
  background: #f5f5f5;
58
  color: #666;
59
  }
60
 
61
- .status.success {
62
- background: #d4edda;
63
- color: #155724;
64
- }
65
-
66
- .status.error {
67
- background: #f8d7da;
68
- color: #721c24;
69
  }
70
 
71
- .status.connected {
72
  background: #d4edda;
73
  color: #155724;
74
  }
75
 
76
- .status.disconnected {
 
 
 
 
 
77
  background: #f8d7da;
78
  color: #721c24;
79
  }
80
 
 
 
 
 
81
  .status-dot {
82
  width: 10px;
83
  height: 10px;
84
  border-radius: 50%;
85
- margin-right: 8px;
86
- display: inline-block;
87
- }
88
-
89
- .status-dot.green {
90
- background: #28a745;
91
- box-shadow: 0 0 10px #28a745;
92
- }
93
-
94
- .status-dot.red {
95
- background: #dc3545;
96
- }
97
-
98
- .status-dot.gray {
99
- background: #999;
100
  }
101
 
 
102
  .editor {
103
  width: 100%;
104
- min-height: 400px;
 
 
105
  padding: 15px;
106
  font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
107
- font-size: 14px;
108
  border: 2px solid #e0e0e0;
109
  border-radius: 8px;
110
- resize: vertical;
111
  margin-bottom: 15px;
112
- line-height: 1.5;
 
 
113
  }
114
 
115
  .editor:focus {
116
  outline: none;
117
  border-color: #667eea;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  }
119
 
120
  .button-group {
@@ -167,6 +227,47 @@ button.danger:hover:not(:disabled) {
167
  background: #c82333;
168
  }
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  .console {
171
  background: #1e1e1e;
172
  color: #d4d4d4;
@@ -208,24 +309,11 @@ button.danger:hover:not(:disabled) {
208
  letter-spacing: 0.5px;
209
  }
210
 
211
- .example-buttons {
212
- display: flex;
213
- gap: 8px;
214
- margin-bottom: 15px;
215
- flex-wrap: wrap;
216
- }
217
-
218
- .example-buttons button {
219
- flex: 0 1 auto;
220
- padding: 8px 16px;
221
- font-size: 12px;
222
- }
223
-
224
  .ir-display {
225
  background: #f8f9fa;
226
- padding: 15px;
227
  border-radius: 8px;
228
- max-height: 300px;
229
  overflow-y: auto;
230
  font-size: 13px;
231
  margin-top: 15px;
@@ -233,8 +321,8 @@ button.danger:hover:not(:disabled) {
233
 
234
  .ir-action {
235
  background: white;
236
- padding: 10px;
237
- margin-bottom: 8px;
238
  border-radius: 4px;
239
  border-left: 4px solid #667eea;
240
  }
@@ -242,11 +330,281 @@ button.danger:hover:not(:disabled) {
242
  .ir-action-type {
243
  font-weight: 600;
244
  color: #667eea;
245
- margin-bottom: 4px;
 
246
  }
247
 
248
  .ir-action-details {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  font-size: 12px;
250
  color: #666;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  }
 
11
  padding: 20px;
12
  }
13
 
14
+ /* Page Header */
15
+ .page-header {
16
+ max-width: 1400px;
17
+ margin: 0 auto 20px auto;
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 12px;
21
+ }
22
+
23
+ .page-header h1 {
24
+ color: white;
25
+ font-size: 28px;
26
+ font-weight: 700;
27
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
28
+ }
29
+
30
+ .header-icon {
31
+ font-size: 32px;
32
+ }
33
+
34
  .container {
35
  max-width: 1400px;
36
  margin: 0 auto;
 
63
  font-size: 14px;
64
  }
65
 
66
+ /* Compile Status (below editor) */
67
+ .compile-status {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 8px;
71
+ padding: 10px 12px;
72
  border-radius: 8px;
73
  margin-bottom: 15px;
74
  font-size: 14px;
 
 
 
75
  }
76
 
77
+ .compile-status.idle {
78
  background: #f5f5f5;
79
  color: #666;
80
  }
81
 
82
+ .compile-status.idle .status-dot {
83
+ background: #999;
 
 
 
 
 
 
84
  }
85
 
86
+ .compile-status.success {
87
  background: #d4edda;
88
  color: #155724;
89
  }
90
 
91
+ .compile-status.success .status-dot {
92
+ background: #28a745;
93
+ box-shadow: 0 0 8px #28a745;
94
+ }
95
+
96
+ .compile-status.error {
97
  background: #f8d7da;
98
  color: #721c24;
99
  }
100
 
101
+ .compile-status.error .status-dot {
102
+ background: #dc3545;
103
+ }
104
+
105
  .status-dot {
106
  width: 10px;
107
  height: 10px;
108
  border-radius: 50%;
109
+ flex-shrink: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  }
111
 
112
+ /* Editor Textarea */
113
  .editor {
114
  width: 100%;
115
+ height: 350px;
116
+ min-height: 350px;
117
+ max-height: 350px;
118
  padding: 15px;
119
  font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
120
+ font-size: 16px;
121
  border: 2px solid #e0e0e0;
122
  border-radius: 8px;
123
+ resize: none;
124
  margin-bottom: 15px;
125
+ line-height: 1.6;
126
+ background: #fafafa;
127
+ color: #333;
128
  }
129
 
130
  .editor:focus {
131
  outline: none;
132
  border-color: #667eea;
133
+ background: white;
134
+ }
135
+
136
+ .editor.has-error {
137
+ border-color: #dc3545;
138
+ }
139
+
140
+ /* Editor Toolbar */
141
+ .editor-toolbar {
142
+ display: flex;
143
+ justify-content: space-between;
144
+ align-items: center;
145
+ margin-bottom: 15px;
146
+ }
147
+
148
+ .toolbar-left {
149
+ display: flex;
150
+ gap: 8px;
151
+ align-items: center;
152
+ }
153
+
154
+ .toolbar-right {
155
+ display: flex;
156
+ gap: 8px;
157
+ align-items: center;
158
+ }
159
+
160
+ /* Examples Dropdown */
161
+ .examples-dropdown {
162
+ padding: 8px 12px;
163
+ border: 2px solid #e0e0e0;
164
+ border-radius: 8px;
165
+ font-size: 14px;
166
+ background: white;
167
+ cursor: pointer;
168
+ min-width: 160px;
169
+ }
170
+
171
+ .examples-dropdown:hover {
172
+ border-color: #667eea;
173
+ }
174
+
175
+ .examples-dropdown:focus {
176
+ outline: none;
177
+ border-color: #667eea;
178
  }
179
 
180
  .button-group {
 
227
  background: #c82333;
228
  }
229
 
230
+ button.small {
231
+ flex: 0 1 auto;
232
+ padding: 8px 16px;
233
+ font-size: 12px;
234
+ }
235
+
236
+ /* Toolbar button colors */
237
+ button.btn-save {
238
+ background: #28a745;
239
+ }
240
+
241
+ button.btn-save:hover:not(:disabled) {
242
+ background: #218838;
243
+ }
244
+
245
+ button.btn-load {
246
+ background: #dc3545;
247
+ }
248
+
249
+ button.btn-load:hover:not(:disabled) {
250
+ background: #c82333;
251
+ }
252
+
253
+ button.btn-clear {
254
+ background: #e0e0e0;
255
+ color: #333;
256
+ }
257
+
258
+ button.btn-clear:hover:not(:disabled) {
259
+ background: #d0d0d0;
260
+ }
261
+
262
+ button.btn-help {
263
+ background: #ffc107;
264
+ color: #333;
265
+ }
266
+
267
+ button.btn-help:hover:not(:disabled) {
268
+ background: #e0a800;
269
+ }
270
+
271
  .console {
272
  background: #1e1e1e;
273
  color: #d4d4d4;
 
309
  letter-spacing: 0.5px;
310
  }
311
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  .ir-display {
313
  background: #f8f9fa;
314
+ padding: 10px;
315
  border-radius: 8px;
316
+ max-height: 500px;
317
  overflow-y: auto;
318
  font-size: 13px;
319
  margin-top: 15px;
 
321
 
322
  .ir-action {
323
  background: white;
324
+ padding: 6px 10px;
325
+ margin-bottom: 4px;
326
  border-radius: 4px;
327
  border-left: 4px solid #667eea;
328
  }
 
330
  .ir-action-type {
331
  font-weight: 600;
332
  color: #667eea;
333
+ margin-bottom: 2px;
334
+ font-size: 12px;
335
  }
336
 
337
  .ir-action-details {
338
+ font-size: 11px;
339
+ color: #666;
340
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
341
+ }
342
+
343
+ /* Modal Styles */
344
+ .modal {
345
+ position: fixed;
346
+ top: 0;
347
+ left: 0;
348
+ right: 0;
349
+ bottom: 0;
350
+ background: rgba(0, 0, 0, 0.5);
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: center;
354
+ z-index: 1000;
355
+ }
356
+
357
+ .modal-content {
358
+ background: white;
359
+ border-radius: 12px;
360
+ width: 90%;
361
+ max-width: 500px;
362
+ max-height: 80vh;
363
+ overflow: hidden;
364
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
365
+ }
366
+
367
+ .modal-header {
368
+ display: flex;
369
+ justify-content: space-between;
370
+ align-items: center;
371
+ padding: 20px;
372
+ border-bottom: 1px solid #e0e0e0;
373
+ }
374
+
375
+ .modal-header h3 {
376
+ margin: 0;
377
+ color: #333;
378
+ }
379
+
380
+ .modal-close {
381
+ background: none;
382
+ border: none;
383
+ font-size: 24px;
384
+ cursor: pointer;
385
+ color: #666;
386
+ padding: 0;
387
+ width: 32px;
388
+ height: 32px;
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: center;
392
+ flex: none;
393
+ }
394
+
395
+ .modal-close:hover {
396
+ color: #333;
397
+ background: #f0f0f0;
398
+ border-radius: 4px;
399
+ transform: none;
400
+ box-shadow: none;
401
+ }
402
+
403
+ .modal-body {
404
+ padding: 20px;
405
+ max-height: 60vh;
406
+ overflow-y: auto;
407
+ }
408
+
409
+ /* Saved Scripts List */
410
+ .saved-script-item {
411
+ display: flex;
412
+ justify-content: space-between;
413
+ align-items: center;
414
+ padding: 12px;
415
+ border: 1px solid #e0e0e0;
416
+ border-radius: 8px;
417
+ margin-bottom: 8px;
418
+ cursor: pointer;
419
+ transition: all 0.2s;
420
+ }
421
+
422
+ .saved-script-item:hover {
423
+ background: #f5f5f5;
424
+ border-color: #667eea;
425
+ }
426
+
427
+ .saved-script-info {
428
+ flex: 1;
429
+ }
430
+
431
+ .saved-script-name {
432
+ font-weight: 600;
433
+ color: #333;
434
+ margin-bottom: 4px;
435
+ }
436
+
437
+ .saved-script-date {
438
  font-size: 12px;
439
  color: #666;
440
+ }
441
+
442
+ .saved-script-delete {
443
+ background: #dc3545;
444
+ color: white;
445
+ border: none;
446
+ padding: 6px 12px;
447
+ border-radius: 4px;
448
+ cursor: pointer;
449
+ font-size: 12px;
450
+ flex: none;
451
+ }
452
+
453
+ .saved-script-delete:hover {
454
+ background: #c82333;
455
+ transform: none;
456
+ box-shadow: none;
457
+ }
458
+
459
+ .no-saved-scripts {
460
+ text-align: center;
461
+ color: #666;
462
+ padding: 40px 20px;
463
+ }
464
+
465
+ /* Execution Timeline */
466
+ .execution-timeline {
467
+ margin-bottom: 20px;
468
+ }
469
+
470
+ .timeline-status {
471
+ display: flex;
472
+ align-items: center;
473
+ gap: 12px;
474
+ padding: 15px;
475
+ border-radius: 8px;
476
+ font-size: 15px;
477
+ transition: all 0.3s;
478
+ }
479
+
480
+ .timeline-status.idle {
481
+ background: #f5f5f5;
482
+ color: #666;
483
+ }
484
+
485
+ .timeline-status.running {
486
+ background: #fff3cd;
487
+ color: #856404;
488
+ animation: pulse 1.5s infinite;
489
+ }
490
+
491
+ .timeline-status.done {
492
+ background: #d4edda;
493
+ color: #155724;
494
+ }
495
+
496
+ .timeline-status.error {
497
+ background: #f8d7da;
498
+ color: #721c24;
499
+ }
500
+
501
+ .timeline-icon {
502
+ font-size: 24px;
503
+ }
504
+
505
+ .timeline-text {
506
+ flex: 1;
507
+ }
508
+
509
+ @keyframes pulse {
510
+ 0%, 100% { opacity: 1; }
511
+ 50% { opacity: 0.7; }
512
+ }
513
+
514
+ /* Running Button Animation */
515
+ button.running {
516
+ position: relative;
517
+ background: #28a745 !important;
518
+ }
519
+
520
+ button.running::after {
521
+ content: '';
522
+ position: absolute;
523
+ top: 0;
524
+ left: 0;
525
+ right: 0;
526
+ bottom: 0;
527
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
528
+ animation: shimmer 1.5s infinite;
529
+ }
530
+
531
+ @keyframes shimmer {
532
+ 0% { transform: translateX(-100%); }
533
+ 100% { transform: translateX(100%); }
534
+ }
535
+
536
+ /* Action Icons in IR Display */
537
+ .ir-action {
538
+ display: flex;
539
+ align-items: flex-start;
540
+ gap: 12px;
541
+ }
542
+
543
+ .ir-action-icon {
544
+ font-size: 16px;
545
+ min-width: 24px;
546
+ text-align: center;
547
+ }
548
+
549
+ .ir-action-content {
550
+ flex: 1;
551
+ }
552
+
553
+ .ir-action.current {
554
+ background: #fff3cd;
555
+ border-left-color: #ffc107;
556
+ }
557
+
558
+ /* Cheat Sheet Modal */
559
+ .cheat-sheet-modal {
560
+ max-width: 600px;
561
+ }
562
+
563
+ .cheat-sheet-body {
564
+ max-height: 70vh;
565
+ }
566
+
567
+ .cheat-section {
568
+ margin-bottom: 20px;
569
+ padding-bottom: 15px;
570
+ border-bottom: 1px solid #e0e0e0;
571
+ }
572
+
573
+ .cheat-section:last-child {
574
+ border-bottom: none;
575
+ margin-bottom: 0;
576
+ }
577
+
578
+ .cheat-section h4 {
579
+ color: #667eea;
580
+ margin-bottom: 8px;
581
+ font-size: 14px;
582
+ }
583
+
584
+ .cheat-section code {
585
+ display: inline-block;
586
+ background: #f5f5f5;
587
+ padding: 4px 8px;
588
+ border-radius: 4px;
589
  font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
590
+ font-size: 13px;
591
+ margin: 2px 4px 2px 0;
592
+ color: #333;
593
+ }
594
+
595
+ .cheat-section pre {
596
+ background: #f5f5f5;
597
+ padding: 10px;
598
+ border-radius: 4px;
599
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
600
+ font-size: 13px;
601
+ margin: 8px 0;
602
+ overflow-x: auto;
603
+ }
604
+
605
+ .cheat-note {
606
+ font-size: 12px;
607
+ color: #666;
608
+ margin-top: 6px;
609
+ margin-bottom: 0;
610
  }