Nipun commited on
Commit
0611eaf
·
1 Parent(s): 685321f

Clean Apple-style UI + CSV/YAML/Markdown/Image formats + response headers tab

Browse files
Files changed (1) hide show
  1. app.py +365 -215
app.py CHANGED
@@ -87,275 +87,333 @@ def ui():
87
  <style>
88
  * { box-sizing: border-box; margin: 0; padding: 0; }
89
  body {
90
- font-family: 'Segoe UI', system-ui, sans-serif;
91
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
92
  min-height: 100vh;
93
- color: #e4e4e4;
94
- padding: 20px;
95
- }
96
- .container { max-width: 1200px; margin: 0 auto; }
97
- h1 {
98
- text-align: center;
99
- margin-bottom: 10px;
100
- font-size: 2.5em;
101
- background: linear-gradient(90deg, #00d9ff, #00ff88);
102
- -webkit-background-clip: text;
103
- -webkit-text-fill-color: transparent;
104
  }
105
- .subtitle { text-align: center; color: #888; margin-bottom: 30px; }
106
- .main-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
107
- @media (max-width: 900px) { .main-grid { grid-template-columns: 1fr; } }
108
 
109
- .panel {
110
- background: rgba(255,255,255,0.05);
111
- border-radius: 16px;
112
- padding: 20px;
113
- border: 1px solid rgba(255,255,255,0.1);
114
- }
115
- .panel h2 {
116
- font-size: 1.1em;
117
- margin-bottom: 15px;
118
- color: #00d9ff;
119
  display: flex;
120
  align-items: center;
121
- gap: 8px;
 
 
 
 
 
 
 
 
 
 
122
  }
 
123
 
124
- /* Request Section */
125
- .request-line {
126
  display: flex;
127
- gap: 10px;
128
- margin-bottom: 15px;
 
 
 
 
129
  }
130
  select, input, textarea, button {
131
  font-family: inherit;
132
  font-size: 14px;
133
  border: none;
134
- border-radius: 8px;
135
  outline: none;
136
  }
137
  .method-select {
138
- padding: 12px 15px;
139
- background: #2d2d44;
140
- color: #fff;
141
  font-weight: 600;
142
  cursor: pointer;
143
- min-width: 110px;
144
  }
145
  .url-input {
146
  flex: 1;
147
- padding: 12px 15px;
148
- background: #2d2d44;
149
- color: #fff;
 
150
  }
151
- .url-input::placeholder { color: #666; }
152
  .send-btn {
153
- padding: 12px 30px;
154
- background: linear-gradient(90deg, #00d9ff, #00ff88);
155
- color: #1a1a2e;
156
- font-weight: 700;
 
157
  cursor: pointer;
158
- transition: transform 0.2s, box-shadow 0.2s;
 
 
 
 
 
 
 
 
 
159
  }
160
- .send-btn:hover {
161
- transform: translateY(-2px);
162
- box-shadow: 0 5px 20px rgba(0,217,255,0.3);
 
 
 
 
163
  }
164
- .send-btn:active { transform: translateY(0); }
165
 
166
  /* Tabs */
167
  .tabs {
168
  display: flex;
169
- gap: 5px;
170
- margin-bottom: 10px;
171
  }
172
  .tab {
173
- padding: 8px 16px;
174
- background: transparent;
175
- color: #888;
 
176
  cursor: pointer;
177
- border-radius: 6px;
178
- transition: all 0.2s;
 
 
179
  }
180
- .tab:hover { color: #fff; }
181
- .tab.active { background: #2d2d44; color: #00d9ff; }
182
 
183
  .tab-content { display: none; }
184
  .tab-content.active { display: block; }
185
 
 
 
186
  textarea {
187
  width: 100%;
188
- min-height: 150px;
189
  padding: 12px;
190
- background: #1a1a2e;
191
- color: #e4e4e4;
192
  resize: vertical;
193
- font-family: 'Fira Code', 'Consolas', monospace;
194
  font-size: 13px;
195
- line-height: 1.5;
 
196
  }
197
 
198
- /* Response */
199
- .response-meta {
200
  display: flex;
201
- gap: 15px;
202
- margin-bottom: 15px;
203
- flex-wrap: wrap;
204
- }
205
- .meta-item {
206
- padding: 6px 12px;
207
- background: #2d2d44;
208
- border-radius: 6px;
209
- font-size: 13px;
210
  }
211
- .status-ok { color: #00ff88; }
212
- .status-err { color: #ff6b6b; }
213
- .status-warn { color: #ffd93d; }
214
-
215
- .response-body {
216
- background: #1a1a2e;
217
- border-radius: 8px;
218
- padding: 15px;
219
- font-family: 'Fira Code', 'Consolas', monospace;
220
- font-size: 13px;
221
- line-height: 1.6;
222
  overflow-x: auto;
223
  white-space: pre-wrap;
224
  word-break: break-word;
225
- max-height: 400px;
226
  overflow-y: auto;
 
227
  }
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  /* Examples */
230
- .examples {
 
 
 
 
 
 
 
 
 
231
  display: grid;
232
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
233
- gap: 10px;
234
- margin-top: 20px;
235
  }
236
  .example-btn {
237
- padding: 12px 15px;
238
- background: rgba(255,255,255,0.05);
239
- border: 1px solid rgba(255,255,255,0.1);
240
- border-radius: 10px;
 
 
 
241
  cursor: pointer;
242
  text-align: left;
243
- transition: all 0.2s;
244
  }
245
  .example-btn:hover {
246
- background: rgba(0,217,255,0.1);
247
- border-color: rgba(0,217,255,0.3);
248
  }
249
- .example-method {
250
- display: inline-block;
251
- padding: 2px 8px;
252
  border-radius: 4px;
253
- font-size: 11px;
254
  font-weight: 700;
255
- margin-right: 8px;
256
  }
257
- .method-GET { background: #00ff88; color: #1a1a2e; }
258
- .method-POST { background: #ffd93d; color: #1a1a2e; }
259
- .method-PUT { background: #00d9ff; color: #1a1a2e; }
260
- .method-DELETE { background: #ff6b6b; color: #1a1a2e; }
261
- .method-PATCH { background: #c084fc; color: #1a1a2e; }
262
- .example-title { font-weight: 600; margin-bottom: 4px; }
263
- .example-desc { font-size: 12px; color: #888; }
264
-
265
- .links { text-align: center; margin-top: 20px; }
266
- .links a {
267
- color: #00d9ff;
268
- text-decoration: none;
269
- margin: 0 15px;
 
270
  }
271
- .links a:hover { text-decoration: underline; }
272
 
273
- /* Loading */
274
- .loading { opacity: 0.6; pointer-events: none; }
275
-
276
- /* JSON syntax highlighting */
277
- .json-key { color: #00d9ff; }
278
- .json-string { color: #00ff88; }
279
- .json-number { color: #ffd93d; }
280
- .json-boolean { color: #c084fc; }
281
- .json-null { color: #888; }
282
  </style>
283
  </head>
284
  <body>
285
  <div class="container">
286
- <h1>API Explorer</h1>
287
- <p class="subtitle">Learn HTTP methods by doing - no setup required</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
- <div class="main-grid">
 
290
  <div class="panel">
291
- <h2>Request</h2>
292
- <div class="request-line">
293
- <select class="method-select" id="method">
294
- <option value="GET">GET</option>
295
- <option value="POST">POST</option>
296
- <option value="PUT">PUT</option>
297
- <option value="PATCH">PATCH</option>
298
- <option value="DELETE">DELETE</option>
299
- </select>
300
- <input type="text" class="url-input" id="url" placeholder="/hello" value="/hello">
301
- <button class="send-btn" id="send">Send</button>
302
  </div>
303
-
304
- <div class="tabs">
305
- <button class="tab active" data-tab="body">Body</button>
306
- <button class="tab" data-tab="headers">Headers</button>
307
- </div>
308
-
309
- <div class="tab-content active" id="body-tab">
310
- <textarea id="body" placeholder='{"name": "Laptop", "price": 999.99, "quantity": 5}'></textarea>
311
- </div>
312
- <div class="tab-content" id="headers-tab">
313
- <textarea id="headers" placeholder="Content-Type: application/json">Content-Type: application/json</textarea>
314
  </div>
315
  </div>
316
 
 
317
  <div class="panel">
318
- <h2>Response</h2>
319
- <div class="response-meta" id="meta">
320
- <span class="meta-item">Status: <span id="status">-</span></span>
321
- <span class="meta-item">Time: <span id="time">-</span></span>
322
- <span class="meta-item">Size: <span id="size">-</span></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  </div>
324
- <div class="response-body" id="response">Send a request to see the response...</div>
325
  </div>
326
  </div>
327
 
328
- <div class="panel" style="margin-top: 20px;">
329
- <h2>Try These Examples</h2>
330
- <div class="examples" id="examples"></div>
331
- </div>
332
-
333
- <div class="links">
334
- <a href="/docs">Swagger Docs</a>
335
- <a href="/api">API Info</a>
336
  </div>
337
  </div>
338
 
339
  <script>
340
  const examples = [
341
- { method: 'GET', url: '/hello', title: 'Hello World', desc: 'Simplest GET request' },
342
- { method: 'GET', url: '/time', title: 'Server Time', desc: 'Get current timestamp' },
343
- { method: 'GET', url: '/greet?name=Student', title: 'Query Parameter', desc: 'Pass data via URL' },
344
- { method: 'GET', url: '/greet/Alice', title: 'Path Parameter', desc: 'Data in the URL path' },
345
- { method: 'GET', url: '/items', title: 'List Items', desc: 'Get all items in store' },
346
- { method: 'GET', url: '/items/1', title: 'Get One Item', desc: 'Fetch item by ID' },
347
- { method: 'POST', url: '/items', title: 'Create Item', desc: 'Add new item to store', body: '{"name": "Laptop", "price": 999.99, "quantity": 5}' },
348
- { method: 'POST', url: '/echo', title: 'Echo JSON', desc: 'Server echoes your data', body: '{"message": "Hello!", "count": 42}' },
349
- { method: 'PUT', url: '/items/1', title: 'Replace Item', desc: 'Full update of item', body: '{"name": "Green Apple", "price": 2.50, "quantity": 200}' },
350
- { method: 'PATCH', url: '/items/1', title: 'Partial Update', desc: 'Update only price', body: '{"price": 1.99}' },
351
- { method: 'DELETE', url: '/items/3', title: 'Delete Item', desc: 'Remove item from store' },
352
- { method: 'GET', url: '/format/json', title: 'JSON Response', desc: 'Default JSON format' },
353
- { method: 'GET', url: '/format/xml', title: 'XML Response', desc: 'XML formatted data' },
354
- { method: 'GET', url: '/format/html', title: 'HTML Response', desc: 'Returns HTML page' },
355
- { method: 'GET', url: '/headers', title: 'See Headers', desc: 'View request headers' },
356
- { method: 'GET', url: '/status/404', title: 'Error 404', desc: 'Test error handling' },
357
- { method: 'GET', url: '/status/201', title: 'Status 201', desc: 'Created status code' },
358
- { method: 'GET', url: '/method', title: 'Method Check', desc: 'Shows HTTP method used' },
 
 
 
359
  ];
360
 
361
  // Render examples
@@ -364,40 +422,43 @@ def ui():
364
  const btn = document.createElement('button');
365
  btn.className = 'example-btn';
366
  btn.innerHTML = `
367
- <div class="example-title">
368
- <span class="example-method method-${ex.method}">${ex.method}</span>
369
- ${ex.title}
 
370
  </div>
371
- <div class="example-desc">${ex.url}</div>
372
  `;
373
- btn.onclick = () => loadExample(ex);
 
 
 
 
374
  examplesContainer.appendChild(btn);
375
  });
376
 
377
- function loadExample(ex) {
378
- document.getElementById('method').value = ex.method;
379
- document.getElementById('url').value = ex.url;
380
- document.getElementById('body').value = ex.body || '';
381
- }
382
-
383
  // Tab switching
384
- document.querySelectorAll('.tab').forEach(tab => {
385
- tab.onclick = () => {
386
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
387
- document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
388
- tab.classList.add('active');
389
- document.getElementById(tab.dataset.tab + '-tab').classList.add('active');
390
- };
391
- });
 
 
 
 
 
 
392
 
393
- // Syntax highlighting for JSON
394
- function highlightJSON(json) {
395
- if (typeof json !== 'string') json = JSON.stringify(json, null, 2);
396
- return json
397
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
398
  .replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
399
  .replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
400
- .replace(/: (\\d+\\.?\\d*)/g, ': <span class="json-number">$1</span>')
401
  .replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>')
402
  .replace(/: (null)/g, ': <span class="json-null">$1</span>');
403
  }
@@ -420,7 +481,8 @@ def ui():
420
  opts.body = body;
421
  }
422
 
423
- document.getElementById('send').classList.add('loading');
 
424
  const start = performance.now();
425
 
426
  try {
@@ -428,15 +490,27 @@ def ui():
428
  const elapsed = Math.round(performance.now() - start);
429
  const text = await res.text();
430
 
431
- // Update status
432
  const statusEl = document.getElementById('status');
433
  statusEl.textContent = res.status + ' ' + res.statusText;
434
- statusEl.className = res.ok ? 'status-ok' : (res.status >= 500 ? 'status-err' : 'status-warn');
435
 
 
436
  document.getElementById('time').textContent = elapsed + 'ms';
437
- document.getElementById('size').textContent = (text.length / 1024).toFixed(2) + ' KB';
 
 
 
 
 
 
 
 
 
 
 
438
 
439
- // Try to format as JSON
440
  let display;
441
  try {
442
  const json = JSON.parse(text);
@@ -448,17 +522,18 @@ def ui():
448
 
449
  } catch (err) {
450
  document.getElementById('status').textContent = 'Error';
451
- document.getElementById('status').className = 'status-err';
452
  document.getElementById('response').textContent = 'Request failed: ' + err.message;
 
 
453
  }
454
 
455
- document.getElementById('send').classList.remove('loading');
 
456
  }
457
 
458
  document.getElementById('send').onclick = sendRequest;
459
  document.getElementById('url').onkeydown = (e) => { if (e.key === 'Enter') sendRequest(); };
460
-
461
- // Send initial request
462
  sendRequest();
463
  </script>
464
  </body>
@@ -678,6 +753,81 @@ def format_xml():
678
  </response>"""
679
  return Response(content=xml_content, media_type="application/xml")
680
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
  # ============================================================================
682
  # EDUCATIONAL ENDPOINTS
683
  # ============================================================================
 
87
  <style>
88
  * { box-sizing: border-box; margin: 0; padding: 0; }
89
  body {
90
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
91
+ background: #f5f5f7;
92
  min-height: 100vh;
93
+ color: #1d1d1f;
94
+ font-size: 14px;
95
+ line-height: 1.5;
 
 
 
 
 
 
 
 
96
  }
 
 
 
97
 
98
+ .container { max-width: 1400px; margin: 0 auto; padding: 20px; }
99
+
100
+ header {
 
 
 
 
 
 
 
101
  display: flex;
102
  align-items: center;
103
+ justify-content: space-between;
104
+ padding: 16px 0;
105
+ border-bottom: 1px solid #d2d2d7;
106
+ margin-bottom: 24px;
107
+ }
108
+ h1 { font-size: 21px; font-weight: 600; }
109
+ .header-links a {
110
+ color: #06c;
111
+ text-decoration: none;
112
+ margin-left: 24px;
113
+ font-size: 13px;
114
  }
115
+ .header-links a:hover { text-decoration: underline; }
116
 
117
+ /* Request Bar */
118
+ .request-bar {
119
  display: flex;
120
+ gap: 8px;
121
+ background: #fff;
122
+ padding: 8px;
123
+ border-radius: 10px;
124
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
125
+ margin-bottom: 20px;
126
  }
127
  select, input, textarea, button {
128
  font-family: inherit;
129
  font-size: 14px;
130
  border: none;
 
131
  outline: none;
132
  }
133
  .method-select {
134
+ padding: 10px 12px;
135
+ background: #f5f5f7;
136
+ border-radius: 6px;
137
  font-weight: 600;
138
  cursor: pointer;
139
+ color: #1d1d1f;
140
  }
141
  .url-input {
142
  flex: 1;
143
+ padding: 10px 12px;
144
+ background: #f5f5f7;
145
+ border-radius: 6px;
146
+ font-family: 'SF Mono', Menlo, monospace;
147
  }
 
148
  .send-btn {
149
+ padding: 10px 24px;
150
+ background: #007aff;
151
+ color: #fff;
152
+ font-weight: 500;
153
+ border-radius: 6px;
154
  cursor: pointer;
155
+ transition: background 0.2s;
156
+ }
157
+ .send-btn:hover { background: #0056b3; }
158
+ .send-btn:disabled { background: #86868b; cursor: not-allowed; }
159
+
160
+ /* Main Layout */
161
+ .main-layout {
162
+ display: grid;
163
+ grid-template-columns: 1fr 1fr;
164
+ gap: 20px;
165
  }
166
+ @media (max-width: 1000px) { .main-layout { grid-template-columns: 1fr; } }
167
+
168
+ .panel {
169
+ background: #fff;
170
+ border-radius: 10px;
171
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
172
+ overflow: hidden;
173
  }
 
174
 
175
  /* Tabs */
176
  .tabs {
177
  display: flex;
178
+ border-bottom: 1px solid #d2d2d7;
179
+ background: #fafafa;
180
  }
181
  .tab {
182
+ padding: 10px 16px;
183
+ font-size: 12px;
184
+ font-weight: 500;
185
+ color: #86868b;
186
  cursor: pointer;
187
+ border-bottom: 2px solid transparent;
188
+ margin-bottom: -1px;
189
+ background: none;
190
+ transition: color 0.2s;
191
  }
192
+ .tab:hover { color: #1d1d1f; }
193
+ .tab.active { color: #007aff; border-bottom-color: #007aff; }
194
 
195
  .tab-content { display: none; }
196
  .tab-content.active { display: block; }
197
 
198
+ .panel-body { padding: 16px; }
199
+
200
  textarea {
201
  width: 100%;
202
+ min-height: 180px;
203
  padding: 12px;
204
+ background: #f5f5f7;
205
+ border-radius: 6px;
206
  resize: vertical;
207
+ font-family: 'SF Mono', Menlo, monospace;
208
  font-size: 13px;
209
+ line-height: 1.6;
210
+ color: #1d1d1f;
211
  }
212
 
213
+ /* Response Panel */
214
+ .status-bar {
215
  display: flex;
216
+ gap: 20px;
217
+ padding: 12px 16px;
218
+ background: #fafafa;
219
+ border-bottom: 1px solid #d2d2d7;
220
+ font-size: 12px;
 
 
 
 
221
  }
222
+ .status-item { color: #86868b; }
223
+ .status-value { font-weight: 600; color: #1d1d1f; margin-left: 6px; }
224
+ .status-ok { color: #34c759; }
225
+ .status-err { color: #ff3b30; }
226
+ .status-warn { color: #ff9500; }
227
+
228
+ .response-content {
229
+ padding: 16px;
230
+ font-family: 'SF Mono', Menlo, monospace;
231
+ font-size: 12px;
232
+ line-height: 1.7;
233
  overflow-x: auto;
234
  white-space: pre-wrap;
235
  word-break: break-word;
236
+ max-height: 500px;
237
  overflow-y: auto;
238
+ background: #fff;
239
  }
240
 
241
+ /* Headers display */
242
+ .headers-list {
243
+ font-family: 'SF Mono', Menlo, monospace;
244
+ font-size: 12px;
245
+ }
246
+ .header-row {
247
+ display: flex;
248
+ padding: 6px 0;
249
+ border-bottom: 1px solid #f0f0f0;
250
+ }
251
+ .header-row:last-child { border-bottom: none; }
252
+ .header-name {
253
+ width: 200px;
254
+ flex-shrink: 0;
255
+ font-weight: 600;
256
+ color: #86868b;
257
+ }
258
+ .header-value { color: #1d1d1f; word-break: break-all; }
259
+
260
  /* Examples */
261
+ .examples-section { margin-top: 20px; }
262
+ .examples-section h3 {
263
+ font-size: 13px;
264
+ font-weight: 600;
265
+ color: #86868b;
266
+ margin-bottom: 12px;
267
+ text-transform: uppercase;
268
+ letter-spacing: 0.5px;
269
+ }
270
+ .examples-grid {
271
  display: grid;
272
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
273
+ gap: 8px;
 
274
  }
275
  .example-btn {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 10px;
279
+ padding: 10px 12px;
280
+ background: #fff;
281
+ border: 1px solid #d2d2d7;
282
+ border-radius: 8px;
283
  cursor: pointer;
284
  text-align: left;
285
+ transition: all 0.15s;
286
  }
287
  .example-btn:hover {
288
+ border-color: #007aff;
289
+ background: #f5f5f7;
290
  }
291
+ .method-badge {
292
+ padding: 2px 6px;
 
293
  border-radius: 4px;
294
+ font-size: 10px;
295
  font-weight: 700;
296
+ font-family: 'SF Mono', Menlo, monospace;
297
  }
298
+ .method-GET { background: #e8f5e9; color: #2e7d32; }
299
+ .method-POST { background: #fff3e0; color: #ef6c00; }
300
+ .method-PUT { background: #e3f2fd; color: #1565c0; }
301
+ .method-DELETE { background: #ffebee; color: #c62828; }
302
+ .method-PATCH { background: #f3e5f5; color: #7b1fa2; }
303
+ .example-info { flex: 1; min-width: 0; }
304
+ .example-title { font-weight: 500; font-size: 13px; }
305
+ .example-url {
306
+ font-size: 11px;
307
+ color: #86868b;
308
+ font-family: 'SF Mono', Menlo, monospace;
309
+ white-space: nowrap;
310
+ overflow: hidden;
311
+ text-overflow: ellipsis;
312
  }
 
313
 
314
+ /* JSON highlighting - subtle */
315
+ .json-key { color: #007aff; }
316
+ .json-string { color: #c41a16; }
317
+ .json-number { color: #1c00cf; }
318
+ .json-boolean { color: #aa0d91; }
319
+ .json-null { color: #86868b; }
 
 
 
320
  </style>
321
  </head>
322
  <body>
323
  <div class="container">
324
+ <header>
325
+ <h1>API Explorer</h1>
326
+ <div class="header-links">
327
+ <a href="/docs">Swagger Docs</a>
328
+ <a href="/api">API Info</a>
329
+ </div>
330
+ </header>
331
+
332
+ <div class="request-bar">
333
+ <select class="method-select" id="method">
334
+ <option value="GET">GET</option>
335
+ <option value="POST">POST</option>
336
+ <option value="PUT">PUT</option>
337
+ <option value="PATCH">PATCH</option>
338
+ <option value="DELETE">DELETE</option>
339
+ </select>
340
+ <input type="text" class="url-input" id="url" placeholder="/hello" value="/hello">
341
+ <button class="send-btn" id="send">Send</button>
342
+ </div>
343
 
344
+ <div class="main-layout">
345
+ <!-- Request Panel -->
346
  <div class="panel">
347
+ <div class="tabs" id="request-tabs">
348
+ <button class="tab active" data-tab="req-body">Body</button>
349
+ <button class="tab" data-tab="req-headers">Headers</button>
 
 
 
 
 
 
 
 
350
  </div>
351
+ <div class="panel-body">
352
+ <div class="tab-content active" id="req-body-tab">
353
+ <textarea id="body" placeholder='{"name": "Laptop", "price": 999.99, "quantity": 5}'></textarea>
354
+ </div>
355
+ <div class="tab-content" id="req-headers-tab">
356
+ <textarea id="headers" placeholder="Content-Type: application/json">Content-Type: application/json</textarea>
357
+ </div>
 
 
 
 
358
  </div>
359
  </div>
360
 
361
+ <!-- Response Panel -->
362
  <div class="panel">
363
+ <div class="status-bar">
364
+ <div class="status-item">Status<span class="status-value" id="status">-</span></div>
365
+ <div class="status-item">Time<span class="status-value" id="time">-</span></div>
366
+ <div class="status-item">Size<span class="status-value" id="size">-</span></div>
367
+ <div class="status-item">Type<span class="status-value" id="content-type">-</span></div>
368
+ </div>
369
+ <div class="tabs" id="response-tabs">
370
+ <button class="tab active" data-tab="res-body">Body</button>
371
+ <button class="tab" data-tab="res-headers">Headers</button>
372
+ <button class="tab" data-tab="res-raw">Raw</button>
373
+ </div>
374
+ <div class="tab-content active" id="res-body-tab">
375
+ <div class="response-content" id="response">Send a request to see the response</div>
376
+ </div>
377
+ <div class="tab-content" id="res-headers-tab">
378
+ <div class="panel-body">
379
+ <div class="headers-list" id="response-headers"></div>
380
+ </div>
381
+ </div>
382
+ <div class="tab-content" id="res-raw-tab">
383
+ <div class="response-content" id="response-raw"></div>
384
  </div>
 
385
  </div>
386
  </div>
387
 
388
+ <div class="examples-section">
389
+ <h3>Examples</h3>
390
+ <div class="examples-grid" id="examples"></div>
 
 
 
 
 
391
  </div>
392
  </div>
393
 
394
  <script>
395
  const examples = [
396
+ { method: 'GET', url: '/hello', title: 'Hello World' },
397
+ { method: 'GET', url: '/time', title: 'Server Time' },
398
+ { method: 'GET', url: '/greet?name=Student', title: 'Query Param' },
399
+ { method: 'GET', url: '/greet/Alice', title: 'Path Param' },
400
+ { method: 'GET', url: '/items', title: 'List Items' },
401
+ { method: 'GET', url: '/items/1', title: 'Get Item' },
402
+ { method: 'POST', url: '/items', title: 'Create Item', body: '{"name": "Laptop", "price": 999.99, "quantity": 5}' },
403
+ { method: 'POST', url: '/echo', title: 'Echo JSON', body: '{"message": "Hello!", "count": 42}' },
404
+ { method: 'PUT', url: '/items/1', title: 'Replace Item', body: '{"name": "Green Apple", "price": 2.50, "quantity": 200}' },
405
+ { method: 'PATCH', url: '/items/1', title: 'Partial Update', body: '{"price": 1.99}' },
406
+ { method: 'DELETE', url: '/items/3', title: 'Delete Item' },
407
+ { method: 'GET', url: '/format/json', title: 'JSON Format' },
408
+ { method: 'GET', url: '/format/xml', title: 'XML Format' },
409
+ { method: 'GET', url: '/format/html', title: 'HTML Format' },
410
+ { method: 'GET', url: '/format/csv', title: 'CSV Format' },
411
+ { method: 'GET', url: '/format/yaml', title: 'YAML Format' },
412
+ { method: 'GET', url: '/format/markdown', title: 'Markdown' },
413
+ { method: 'GET', url: '/format/image', title: 'PNG Image' },
414
+ { method: 'GET', url: '/headers', title: 'View Headers' },
415
+ { method: 'GET', url: '/status/404', title: 'Error 404' },
416
+ { method: 'GET', url: '/status/201', title: 'Status 201' },
417
  ];
418
 
419
  // Render examples
 
422
  const btn = document.createElement('button');
423
  btn.className = 'example-btn';
424
  btn.innerHTML = `
425
+ <span class="method-badge method-${ex.method}">${ex.method}</span>
426
+ <div class="example-info">
427
+ <div class="example-title">${ex.title}</div>
428
+ <div class="example-url">${ex.url}</div>
429
  </div>
 
430
  `;
431
+ btn.onclick = () => {
432
+ document.getElementById('method').value = ex.method;
433
+ document.getElementById('url').value = ex.url;
434
+ document.getElementById('body').value = ex.body || '';
435
+ };
436
  examplesContainer.appendChild(btn);
437
  });
438
 
 
 
 
 
 
 
439
  // Tab switching
440
+ function setupTabs(containerId) {
441
+ const container = document.getElementById(containerId);
442
+ container.querySelectorAll('.tab').forEach(tab => {
443
+ tab.onclick = () => {
444
+ container.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
445
+ tab.classList.add('active');
446
+ const panel = container.closest('.panel') || document;
447
+ panel.querySelectorAll(':scope > .tab-content, :scope > .panel-body > .tab-content').forEach(c => c.classList.remove('active'));
448
+ document.getElementById(tab.dataset.tab + '-tab').classList.add('active');
449
+ };
450
+ });
451
+ }
452
+ setupTabs('request-tabs');
453
+ setupTabs('response-tabs');
454
 
455
+ // JSON highlighting
456
+ function highlightJSON(str) {
457
+ return str
 
458
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
459
  .replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
460
  .replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
461
+ .replace(/: (-?\\d+\\.?\\d*)/g, ': <span class="json-number">$1</span>')
462
  .replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>')
463
  .replace(/: (null)/g, ': <span class="json-null">$1</span>');
464
  }
 
481
  opts.body = body;
482
  }
483
 
484
+ document.getElementById('send').disabled = true;
485
+ document.getElementById('send').textContent = 'Sending...';
486
  const start = performance.now();
487
 
488
  try {
 
490
  const elapsed = Math.round(performance.now() - start);
491
  const text = await res.text();
492
 
493
+ // Status
494
  const statusEl = document.getElementById('status');
495
  statusEl.textContent = res.status + ' ' + res.statusText;
496
+ statusEl.className = 'status-value ' + (res.ok ? 'status-ok' : (res.status >= 500 ? 'status-err' : 'status-warn'));
497
 
498
+ // Meta
499
  document.getElementById('time').textContent = elapsed + 'ms';
500
+ document.getElementById('size').textContent = text.length > 1024 ? (text.length / 1024).toFixed(1) + ' KB' : text.length + ' B';
501
+ document.getElementById('content-type').textContent = res.headers.get('content-type')?.split(';')[0] || '-';
502
+
503
+ // Response headers
504
+ const headersHtml = [];
505
+ res.headers.forEach((value, name) => {
506
+ headersHtml.push(`<div class="header-row"><span class="header-name">${name}</span><span class="header-value">${value}</span></div>`);
507
+ });
508
+ document.getElementById('response-headers').innerHTML = headersHtml.join('') || '<div style="color:#86868b">No headers</div>';
509
+
510
+ // Raw response
511
+ document.getElementById('response-raw').textContent = text;
512
 
513
+ // Formatted response
514
  let display;
515
  try {
516
  const json = JSON.parse(text);
 
522
 
523
  } catch (err) {
524
  document.getElementById('status').textContent = 'Error';
525
+ document.getElementById('status').className = 'status-value status-err';
526
  document.getElementById('response').textContent = 'Request failed: ' + err.message;
527
+ document.getElementById('response-headers').innerHTML = '';
528
+ document.getElementById('response-raw').textContent = '';
529
  }
530
 
531
+ document.getElementById('send').disabled = false;
532
+ document.getElementById('send').textContent = 'Send';
533
  }
534
 
535
  document.getElementById('send').onclick = sendRequest;
536
  document.getElementById('url').onkeydown = (e) => { if (e.key === 'Enter') sendRequest(); };
 
 
537
  sendRequest();
538
  </script>
539
  </body>
 
753
  </response>"""
754
  return Response(content=xml_content, media_type="application/xml")
755
 
756
+ @app.get("/format/csv", tags=["Response Formats"])
757
+ def format_csv():
758
+ """Returns data as CSV (spreadsheet format)"""
759
+ csv_content = """id,name,price,quantity,description
760
+ 1,Apple,1.50,100,Fresh red apple
761
+ 2,Banana,0.75,150,Yellow banana
762
+ 3,Orange,2.00,80,Juicy orange"""
763
+ return Response(
764
+ content=csv_content,
765
+ media_type="text/csv",
766
+ headers={"Content-Disposition": "inline; filename=items.csv"}
767
+ )
768
+
769
+ @app.get("/format/markdown", tags=["Response Formats"])
770
+ def format_markdown():
771
+ """Returns data as Markdown"""
772
+ md_content = """# User Profile
773
+
774
+ | Field | Value |
775
+ |-------|-------|
776
+ | Name | Alice |
777
+ | Age | 30 |
778
+ | City | Mumbai |
779
+
780
+ ## About
781
+ This is a **markdown** formatted response.
782
+
783
+ - Supports *italic* text
784
+ - Supports **bold** text
785
+ - Supports `code` blocks
786
+
787
+ ```python
788
+ print("Hello, World!")
789
+ ```
790
+ """
791
+ return Response(content=md_content, media_type="text/markdown")
792
+
793
+ @app.get("/format/yaml", tags=["Response Formats"])
794
+ def format_yaml():
795
+ """Returns data as YAML"""
796
+ yaml_content = """# User Data in YAML format
797
+ format: YAML
798
+ content_type: application/x-yaml
799
+ data:
800
+ user:
801
+ name: Alice
802
+ age: 30
803
+ city: Mumbai
804
+ hobbies:
805
+ - reading
806
+ - coding
807
+ - hiking
808
+ """
809
+ return Response(content=yaml_content, media_type="application/x-yaml")
810
+
811
+ @app.get("/format/image", tags=["Response Formats"])
812
+ def format_image():
813
+ """Returns a tiny PNG image (1x1 red pixel)"""
814
+ # Minimal valid PNG: 1x1 red pixel
815
+ import base64
816
+ png_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="
817
+ png_bytes = base64.b64decode(png_base64)
818
+ return Response(content=png_bytes, media_type="image/png")
819
+
820
+ @app.get("/format/binary", tags=["Response Formats"])
821
+ def format_binary():
822
+ """Returns raw binary data (demonstrates non-text response)"""
823
+ # Some example bytes
824
+ binary_data = bytes([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0xFF, 0xFE, 0x00, 0x01])
825
+ return Response(
826
+ content=binary_data,
827
+ media_type="application/octet-stream",
828
+ headers={"Content-Disposition": "inline; filename=data.bin"}
829
+ )
830
+
831
  # ============================================================================
832
  # EDUCATIONAL ENDPOINTS
833
  # ============================================================================