Nitish commited on
Commit
4e5e784
·
1 Parent(s): 2236518

feat: add interactive web dashboard for space

Browse files
Files changed (4) hide show
  1. server/app.py +8 -0
  2. static/index.html +124 -0
  3. static/main.js +156 -0
  4. static/style.css +305 -0
server/app.py CHANGED
@@ -2,6 +2,8 @@ import os
2
  import uvicorn
3
  from fastapi import FastAPI, HTTPException, Query
4
  from fastapi.middleware.cors import CORSMiddleware
 
 
5
 
6
  from .models import CodeReviewAction, CodeReviewState, StepResponse, ResetResponse
7
  from .environment import CodeReviewEnvironment
@@ -22,8 +24,14 @@ app.add_middleware(
22
  allow_headers=["*"],
23
  )
24
 
 
 
25
  env = CodeReviewEnvironment()
26
 
 
 
 
 
27
 
28
  @app.get("/health")
29
  def health():
 
2
  import uvicorn
3
  from fastapi import FastAPI, HTTPException, Query
4
  from fastapi.middleware.cors import CORSMiddleware
5
+ from fastapi.staticfiles import StaticFiles
6
+ from fastapi.responses import FileResponse
7
 
8
  from .models import CodeReviewAction, CodeReviewState, StepResponse, ResetResponse
9
  from .environment import CodeReviewEnvironment
 
24
  allow_headers=["*"],
25
  )
26
 
27
+ app.mount("/static", StaticFiles(directory="static"), name="static")
28
+
29
  env = CodeReviewEnvironment()
30
 
31
+ @app.get("/")
32
+ def read_index():
33
+ return FileResponse("static/index.html")
34
+
35
 
36
  @app.get("/health")
37
  def health():
static/index.html ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Code Security Review Environment</title>
7
+ <meta name="description" content="RL Environment for training AI agents to detect bugs and security vulnerabilities.">
8
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="/static/style.css">
10
+ <!-- Include Highlight.js for code formatting -->
11
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/tokyo-night-dark.min.css">
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
13
+ </head>
14
+ <body>
15
+ <div id="app-background"></div>
16
+ <div id="particle-overlay"></div>
17
+
18
+ <main class="container">
19
+ <header class="glass-panel">
20
+ <h1>Code Security RL Environment</h1>
21
+ <p>Interactive baseline evaluation for AI Agents.</p>
22
+ </header>
23
+
24
+ <div class="dashboard">
25
+ <!-- Left Column: Environment Observation -->
26
+ <section class="glass-panel observation-panel" id="observation-section">
27
+ <div class="panel-header">
28
+ <h2><span class="icon">👁️</span> Environment State</h2>
29
+ <div class="badge-row">
30
+ <span id="badge-difficulty" class="badge">Loading...</span>
31
+ <span id="badge-step" class="badge">Step 0/0</span>
32
+ </div>
33
+ </div>
34
+
35
+ <div class="task-info">
36
+ <strong>Task:</strong> <span id="task-description">Initializing environment...</span>
37
+ </div>
38
+
39
+ <div id="feedback-container" class="feedback-info hidden">
40
+ <strong>Previous Feedback:</strong> <span id="previous-feedback"></span>
41
+ </div>
42
+
43
+ <div class="code-container">
44
+ <div class="code-header">
45
+ <span id="lang-badge">Language: Unknown</span>
46
+ </div>
47
+ <pre><code id="code-snippet" class="language-python"># Awaiting initialization...</code></pre>
48
+ </div>
49
+ </section>
50
+
51
+ <!-- Right Column: Agent Action Form -->
52
+ <section class="glass-panel action-panel" id="action-section">
53
+ <div class="panel-header">
54
+ <h2><span class="icon">🛠️</span> Agent Action</h2>
55
+ </div>
56
+
57
+ <form id="action-form">
58
+ <div class="form-group toggle-group">
59
+ <label for="input-bug-identified">Bug Identified</label>
60
+ <select id="input-bug-identified" required>
61
+ <option value="true" selected>Yes</option>
62
+ <option value="false">No</option>
63
+ </select>
64
+ </div>
65
+
66
+ <div class="form-group">
67
+ <label for="input-bug-type">Bug Type</label>
68
+ <select id="input-bug-type" required>
69
+ <option value="off-by-one">Off-by-one</option>
70
+ <option value="logic-error">Logic Error</option>
71
+ <option value="security-vulnerability">Security Vulnerability</option>
72
+ <option value="null-dereference">Null Dereference</option>
73
+ <option value="none">None</option>
74
+ </select>
75
+ </div>
76
+
77
+ <div class="form-group">
78
+ <label for="input-severity">Severity</label>
79
+ <select id="input-severity" required>
80
+ <option value="none">None</option>
81
+ <option value="low">Low</option>
82
+ <option value="medium">Medium</option>
83
+ <option value="high">High</option>
84
+ <option value="critical">Critical</option>
85
+ </select>
86
+ </div>
87
+
88
+ <div class="form-group">
89
+ <label for="input-bug-location">Bug Location</label>
90
+ <input type="text" id="input-bug-location" placeholder="e.g., fetch_records() line 4" required>
91
+ </div>
92
+
93
+ <div class="form-group">
94
+ <label for="input-bug-description">Description</label>
95
+ <textarea id="input-bug-description" rows="3" placeholder="Explain the vulnerability..." required></textarea>
96
+ </div>
97
+
98
+ <div class="form-group">
99
+ <label for="input-suggested-fix">Suggested Fix</label>
100
+ <textarea id="input-suggested-fix" rows="3" placeholder="Provide corrected code or explanation..." required></textarea>
101
+ </div>
102
+
103
+ <button type="submit" id="btn-submit-action" class="primary-btn">Submit Action</button>
104
+ <button type="button" id="btn-reset-env" class="secondary-btn">Reset Environment</button>
105
+ </form>
106
+ </section>
107
+ </div>
108
+
109
+ <!-- Sticky Status Toast -->
110
+ <div id="reward-toast" class="toast hidden">
111
+ <div class="toast-content">
112
+ <span class="toast-icon">✨</span>
113
+ <div class="toast-text">
114
+ <h3 id="toast-title">Reward Received</h3>
115
+ <p id="toast-message">Score: 0.0</p>
116
+ </div>
117
+ </div>
118
+ <button id="toast-close">&times;</button>
119
+ </div>
120
+ </main>
121
+
122
+ <script src="/static/main.js"></script>
123
+ </body>
124
+ </html>
static/main.js ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ // DOM Elements
3
+ const elements = {
4
+ badgeDifficulty: document.getElementById('badge-difficulty'),
5
+ badgeStep: document.getElementById('badge-step'),
6
+ taskDescription: document.getElementById('task-description'),
7
+ codeSnippet: document.getElementById('code-snippet'),
8
+ langBadge: document.getElementById('lang-badge'),
9
+ feedbackContainer: document.getElementById('feedback-container'),
10
+ previousFeedback: document.getElementById('previous-feedback'),
11
+
12
+ form: document.getElementById('action-form'),
13
+ submitBtn: document.getElementById('btn-submit-action'),
14
+ resetBtn: document.getElementById('btn-reset-env'),
15
+
16
+ toast: document.getElementById('reward-toast'),
17
+ toastTitle: document.getElementById('toast-title'),
18
+ toastMessage: document.getElementById('toast-message'),
19
+ toastClose: document.getElementById('toast-close'),
20
+
21
+ // Inputs
22
+ inputBugIdentified: document.getElementById('input-bug-identified'),
23
+ inputBugType: document.getElementById('input-bug-type'),
24
+ inputSeverity: document.getElementById('input-severity'),
25
+ inputBugLocation: document.getElementById('input-bug-location'),
26
+ inputBugDescription: document.getElementById('input-bug-description'),
27
+ inputSuggestedFix: document.getElementById('input-suggested-fix')
28
+ };
29
+
30
+ let isDone = false;
31
+
32
+ // Initialize Environment
33
+ async function resetEnvironment(difficulty = 'easy') {
34
+ elements.submitBtn.disabled = true;
35
+ elements.resetBtn.disabled = true;
36
+ isDone = false;
37
+
38
+ try {
39
+ const res = await fetch(`/reset?difficulty=${difficulty}`, { method: 'POST' });
40
+ if (!res.ok) throw new Error('Failed to reset environment');
41
+ const data = await res.json();
42
+ updateObservation(data.observation);
43
+
44
+ // clear form
45
+ elements.form.reset();
46
+ document.getElementById('observation-section').classList.remove('environment-done');
47
+ hideToast();
48
+ } catch (e) {
49
+ showToast('Error', e.message, true);
50
+ } finally {
51
+ elements.submitBtn.disabled = false;
52
+ elements.resetBtn.disabled = false;
53
+ }
54
+ }
55
+
56
+ function updateObservation(obs) {
57
+ elements.badgeDifficulty.textContent = obs.difficulty.toUpperCase();
58
+ elements.badgeStep.textContent = `Step ${obs.step_number}/${obs.max_steps}`;
59
+ elements.taskDescription.textContent = obs.task_description;
60
+ elements.langBadge.textContent = `Language: ${obs.language}`;
61
+
62
+ // Update code block and highlight
63
+ elements.codeSnippet.textContent = obs.code_snippet;
64
+ elements.codeSnippet.className = `language-${obs.language}`;
65
+ hljs.highlightElement(elements.codeSnippet);
66
+
67
+ if (obs.previous_feedback) {
68
+ elements.previousFeedback.textContent = obs.previous_feedback;
69
+ elements.feedbackContainer.classList.remove('hidden');
70
+ } else {
71
+ elements.feedbackContainer.classList.add('hidden');
72
+ }
73
+
74
+ if (obs.step_number >= obs.max_steps) {
75
+ isDone = true;
76
+ }
77
+ }
78
+
79
+ // Submit Step
80
+ elements.form.addEventListener('submit', async (e) => {
81
+ e.preventDefault();
82
+ if (isDone) {
83
+ showToast('Environment Finished', 'Please reset to start a new episode.', true);
84
+ return;
85
+ }
86
+
87
+ const action = {
88
+ bug_identified: elements.inputBugIdentified.value === 'true',
89
+ bug_location: elements.inputBugLocation.value,
90
+ bug_type: elements.inputBugType.value,
91
+ bug_description: elements.inputBugDescription.value,
92
+ severity: elements.inputSeverity.value,
93
+ suggested_fix: elements.inputSuggestedFix.value
94
+ };
95
+
96
+ elements.submitBtn.disabled = true;
97
+ elements.submitBtn.textContent = "Submitting...";
98
+
99
+ try {
100
+ const res = await fetch('/step', {
101
+ method: 'POST',
102
+ headers: { 'Content-Type': 'application/json' },
103
+ body: JSON.stringify(action)
104
+ });
105
+
106
+ if (!res.ok) {
107
+ const err = await res.json();
108
+ throw new Error(err.detail || 'Failed to submit action');
109
+ }
110
+
111
+ const data = await res.json();
112
+ updateObservation(data.observation);
113
+
114
+ if (data.done) {
115
+ isDone = true;
116
+ const totalScore = data.info?.total_score || data.reward;
117
+ showToast('Episode Completed!', `Final Score: ${totalScore.toFixed(2)}`, false);
118
+ document.getElementById('observation-section').classList.add('environment-done');
119
+ } else {
120
+ showToast('Step Evaluated', `Step Reward: ${data.reward.toFixed(2)}`, false);
121
+ }
122
+ } catch (e) {
123
+ showToast('Action Failed', e.message, true);
124
+ } finally {
125
+ elements.submitBtn.disabled = false;
126
+ elements.submitBtn.textContent = "Submit Action";
127
+ }
128
+ });
129
+
130
+ // Reset button
131
+ elements.resetBtn.addEventListener('click', () => {
132
+ const randomDifficulty = ['easy', 'medium', 'hard'][Math.floor(Math.random() * 3)];
133
+ resetEnvironment(randomDifficulty);
134
+ });
135
+
136
+ // Toast functionality
137
+ let toastTimeout;
138
+ function showToast(title, message, isError = false) {
139
+ elements.toastTitle.textContent = title;
140
+ elements.toastMessage.textContent = message;
141
+ elements.toastMessage.style.color = isError ? 'var(--error)' : 'var(--success)';
142
+ elements.toast.classList.remove('hidden');
143
+
144
+ clearTimeout(toastTimeout);
145
+ toastTimeout = setTimeout(hideToast, 4000);
146
+ }
147
+
148
+ function hideToast() {
149
+ elements.toast.classList.add('hidden');
150
+ }
151
+
152
+ elements.toastClose.addEventListener('click', hideToast);
153
+
154
+ // Initial Load
155
+ resetEnvironment();
156
+ });
static/style.css ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-primary: #0a0a0f;
3
+ --bg-card: rgba(20, 20, 30, 0.6);
4
+ --bg-glass: rgba(255, 255, 255, 0.03);
5
+ --border-glass: rgba(255, 255, 255, 0.08);
6
+ --accent-primary: #7c3aed;
7
+ --accent-glow: rgba(124, 58, 237, 0.5);
8
+ --accent-secondary: #0ea5e9;
9
+ --text-main: #f8fafc;
10
+ --text-muted: #94a3b8;
11
+ --success: #10b981;
12
+ --error: #ef4444;
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ margin: 0;
18
+ padding: 0;
19
+ }
20
+
21
+ body {
22
+ font-family: 'Outfit', sans-serif;
23
+ color: var(--text-main);
24
+ background-color: var(--bg-primary);
25
+ min-height: 100vh;
26
+ padding: 2rem;
27
+ position: relative;
28
+ overflow-x: hidden;
29
+ }
30
+
31
+ /* Background Gradients & Animation */
32
+ #app-background {
33
+ position: fixed;
34
+ top: 0;
35
+ left: 0;
36
+ width: 100%;
37
+ height: 100%;
38
+ background: radial-gradient(circle at 15% 50%, rgba(124, 58, 237, 0.15), transparent 25%),
39
+ radial-gradient(circle at 85% 30%, rgba(14, 165, 233, 0.15), transparent 25%);
40
+ z-index: -2;
41
+ animation: pulseBg 10s ease-in-out infinite alternate;
42
+ }
43
+
44
+ @keyframes pulseBg {
45
+ 0% { transform: scale(1); }
46
+ 100% { transform: scale(1.1); }
47
+ }
48
+
49
+ .container {
50
+ max-width: 1200px;
51
+ margin: 0 auto;
52
+ }
53
+
54
+ header {
55
+ margin-bottom: 2rem;
56
+ text-align: center;
57
+ padding: 2rem;
58
+ }
59
+
60
+ h1 {
61
+ font-size: 2.5rem;
62
+ background: linear-gradient(to right, #a855f7, #38bdf8);
63
+ -webkit-background-clip: text;
64
+ -webkit-text-fill-color: transparent;
65
+ margin-bottom: 0.5rem;
66
+ }
67
+
68
+ p {
69
+ color: var(--text-muted);
70
+ }
71
+
72
+ .glass-panel {
73
+ background: var(--bg-glass);
74
+ backdrop-filter: blur(16px);
75
+ -webkit-backdrop-filter: blur(16px);
76
+ border: 1px solid var(--border-glass);
77
+ border-radius: 16px;
78
+ padding: 1.5rem;
79
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
80
+ }
81
+
82
+ .dashboard {
83
+ display: grid;
84
+ grid-template-columns: 1fr 1fr;
85
+ gap: 2rem;
86
+ align-items: start;
87
+ }
88
+
89
+ @media (max-width: 900px) {
90
+ .dashboard {
91
+ grid-template-columns: 1fr;
92
+ }
93
+ }
94
+
95
+ /* Common Panel Header */
96
+ .panel-header {
97
+ display: flex;
98
+ justify-content: space-between;
99
+ align-items: center;
100
+ border-bottom: 1px solid var(--border-glass);
101
+ padding-bottom: 1rem;
102
+ margin-bottom: 1rem;
103
+ }
104
+
105
+ h2 {
106
+ font-size: 1.25rem;
107
+ font-weight: 600;
108
+ display: flex;
109
+ align-items: center;
110
+ gap: 0.5rem;
111
+ }
112
+
113
+ .badge-row {
114
+ display: flex;
115
+ gap: 0.5rem;
116
+ }
117
+
118
+ .badge {
119
+ background: rgba(255, 255, 255, 0.1);
120
+ padding: 0.25rem 0.75rem;
121
+ border-radius: 9999px;
122
+ font-size: 0.75rem;
123
+ font-weight: 600;
124
+ letter-spacing: 0.05em;
125
+ text-transform: uppercase;
126
+ }
127
+
128
+ /* Observation Panel */
129
+ .task-info {
130
+ margin-bottom: 1rem;
131
+ font-size: 1rem;
132
+ line-height: 1.5;
133
+ }
134
+
135
+ .feedback-info {
136
+ background: rgba(239, 68, 68, 0.1);
137
+ border: 1px solid rgba(239, 68, 68, 0.2);
138
+ border-radius: 8px;
139
+ padding: 1rem;
140
+ margin-bottom: 1rem;
141
+ font-size: 0.9rem;
142
+ }
143
+
144
+ .hidden {
145
+ display: none !important;
146
+ }
147
+
148
+ .code-container {
149
+ background: #1a1b26;
150
+ border-radius: 8px;
151
+ overflow: hidden;
152
+ border: 1px solid var(--border-glass);
153
+ }
154
+
155
+ .code-header {
156
+ background: #16161e;
157
+ padding: 0.5rem 1rem;
158
+ font-size: 0.75rem;
159
+ color: var(--text-muted);
160
+ border-bottom: 1px solid #292e42;
161
+ display: flex;
162
+ justify-content: flex-end;
163
+ }
164
+
165
+ pre {
166
+ margin: 0;
167
+ padding: 1rem;
168
+ font-family: 'Roboto Mono', monospace;
169
+ font-size: 0.9rem;
170
+ overflow-x: auto;
171
+ }
172
+
173
+ /* Action Panel Form */
174
+ .form-group {
175
+ margin-bottom: 1.25rem;
176
+ }
177
+
178
+ label {
179
+ display: block;
180
+ font-size: 0.85rem;
181
+ font-weight: 500;
182
+ color: var(--text-muted);
183
+ margin-bottom: 0.5rem;
184
+ }
185
+
186
+ input, select, textarea {
187
+ width: 100%;
188
+ background: rgba(0, 0, 0, 0.2);
189
+ border: 1px solid var(--border-glass);
190
+ border-radius: 8px;
191
+ color: var(--text-main);
192
+ padding: 0.75rem 1rem;
193
+ font-family: inherit;
194
+ font-size: 0.95rem;
195
+ transition: all 0.2s;
196
+ }
197
+
198
+ input:focus, select:focus, textarea:focus {
199
+ outline: none;
200
+ border-color: var(--accent-primary);
201
+ box-shadow: 0 0 0 2px var(--accent-glow);
202
+ }
203
+
204
+ select option {
205
+ background: var(--bg-primary);
206
+ color: var(--text-main);
207
+ }
208
+
209
+ button {
210
+ width: 100%;
211
+ padding: 0.875rem;
212
+ border: none;
213
+ border-radius: 8px;
214
+ font-family: inherit;
215
+ font-weight: 600;
216
+ font-size: 1rem;
217
+ cursor: pointer;
218
+ transition: all 0.2s;
219
+ margin-bottom: 1rem;
220
+ }
221
+
222
+ .primary-btn {
223
+ background: linear-gradient(to right, var(--accent-primary), var(--accent-secondary));
224
+ color: white;
225
+ box-shadow: 0 4px 15px var(--accent-glow);
226
+ }
227
+
228
+ .primary-btn:hover {
229
+ transform: translateY(-2px);
230
+ box-shadow: 0 6px 20px var(--accent-glow);
231
+ }
232
+
233
+ .secondary-btn {
234
+ background: transparent;
235
+ border: 1px solid var(--border-glass);
236
+ color: var(--text-main);
237
+ }
238
+
239
+ .secondary-btn:hover {
240
+ background: rgba(255, 255, 255, 0.05);
241
+ }
242
+
243
+ /* Toast */
244
+ .toast {
245
+ position: fixed;
246
+ bottom: 2rem;
247
+ right: 2rem;
248
+ background: rgba(20, 20, 30, 0.9);
249
+ border: 1px solid var(--border-glass);
250
+ padding: 1rem;
251
+ border-radius: 8px;
252
+ display: flex;
253
+ justify-content: space-between;
254
+ align-items: center;
255
+ box-shadow: 0 10px 25px rgba(0,0,0,0.5);
256
+ transform: translateY(100px);
257
+ opacity: 0;
258
+ animation: slideUp 0.4s forwards;
259
+ z-index: 100;
260
+ min-width: 300px;
261
+ }
262
+
263
+ @keyframes slideUp {
264
+ to {
265
+ transform: translateY(0);
266
+ opacity: 1;
267
+ }
268
+ }
269
+
270
+ .toast-content {
271
+ display: flex;
272
+ align-items: center;
273
+ gap: 1rem;
274
+ }
275
+
276
+ .toast-icon {
277
+ font-size: 1.5rem;
278
+ }
279
+
280
+ #toast-title {
281
+ font-size: 0.9rem;
282
+ margin-bottom: 0.2rem;
283
+ }
284
+
285
+ #toast-message {
286
+ font-size: 1.1rem;
287
+ font-weight: 600;
288
+ color: var(--success);
289
+ }
290
+
291
+ #toast-close {
292
+ background: transparent;
293
+ border: none;
294
+ color: var(--text-muted);
295
+ font-size: 1.5rem;
296
+ cursor: pointer;
297
+ padding: 0;
298
+ width: auto;
299
+ margin: 0;
300
+ }
301
+
302
+ .environment-done {
303
+ border-color: var(--success);
304
+ box-shadow: 0 0 20px rgba(16, 185, 129, 0.2);
305
+ }