Nipun commited on
Commit
0f02098
·
1 Parent(s): 0611eaf

Add Copy as curl/Python/fetch/HTTPie + Preview tab for HTML/JSON/CSV/XML/Markdown

Browse files
Files changed (1) hide show
  1. app.py +371 -0
app.py CHANGED
@@ -157,6 +157,69 @@ def ui():
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;
@@ -317,6 +380,56 @@ def ui():
317
  .json-number { color: #1c00cf; }
318
  .json-boolean { color: #aa0d91; }
319
  .json-null { color: #86868b; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  </style>
321
  </head>
322
  <body>
@@ -339,7 +452,29 @@ def ui():
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 -->
@@ -368,12 +503,18 @@ def ui():
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>
@@ -520,6 +661,9 @@ def ui():
520
  }
521
  document.getElementById('response').innerHTML = display;
522
 
 
 
 
523
  } catch (err) {
524
  document.getElementById('status').textContent = 'Error';
525
  document.getElementById('status').className = 'status-value status-err';
@@ -535,6 +679,233 @@ def ui():
535
  document.getElementById('send').onclick = sendRequest;
536
  document.getElementById('url').onkeydown = (e) => { if (e.key === 'Enter') sendRequest(); };
537
  sendRequest();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  </script>
539
  </body>
540
  </html>
 
157
  .send-btn:hover { background: #0056b3; }
158
  .send-btn:disabled { background: #86868b; cursor: not-allowed; }
159
 
160
+ /* Copy dropdown */
161
+ .copy-dropdown {
162
+ position: relative;
163
+ }
164
+ .copy-btn {
165
+ padding: 10px 16px;
166
+ background: #fff;
167
+ border: 1px solid #d2d2d7;
168
+ border-radius: 6px;
169
+ cursor: pointer;
170
+ font-weight: 500;
171
+ color: #1d1d1f;
172
+ transition: all 0.2s;
173
+ }
174
+ .copy-btn:hover { background: #f5f5f7; border-color: #007aff; }
175
+ .copy-menu {
176
+ display: none;
177
+ position: absolute;
178
+ top: 100%;
179
+ right: 0;
180
+ margin-top: 4px;
181
+ background: #fff;
182
+ border: 1px solid #d2d2d7;
183
+ border-radius: 8px;
184
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
185
+ z-index: 100;
186
+ min-width: 160px;
187
+ overflow: hidden;
188
+ }
189
+ .copy-menu.show { display: block; }
190
+ .copy-option {
191
+ display: block;
192
+ width: 100%;
193
+ padding: 10px 14px;
194
+ text-align: left;
195
+ background: none;
196
+ border: none;
197
+ cursor: pointer;
198
+ font-size: 13px;
199
+ color: #1d1d1f;
200
+ transition: background 0.1s;
201
+ }
202
+ .copy-option:hover { background: #f5f5f7; }
203
+ .copy-option-label { font-weight: 500; }
204
+ .copy-option-desc { font-size: 11px; color: #86868b; }
205
+
206
+ /* Toast notification */
207
+ .toast {
208
+ position: fixed;
209
+ bottom: 20px;
210
+ left: 50%;
211
+ transform: translateX(-50%) translateY(100px);
212
+ background: #1d1d1f;
213
+ color: #fff;
214
+ padding: 12px 24px;
215
+ border-radius: 8px;
216
+ font-size: 13px;
217
+ opacity: 0;
218
+ transition: all 0.3s;
219
+ z-index: 1000;
220
+ }
221
+ .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
222
+
223
  /* Main Layout */
224
  .main-layout {
225
  display: grid;
 
380
  .json-number { color: #1c00cf; }
381
  .json-boolean { color: #aa0d91; }
382
  .json-null { color: #86868b; }
383
+
384
+ /* Preview */
385
+ .preview-container {
386
+ padding: 16px;
387
+ min-height: 200px;
388
+ max-height: 500px;
389
+ overflow: auto;
390
+ }
391
+ .preview-placeholder { color: #86868b; }
392
+ .preview-iframe {
393
+ width: 100%;
394
+ min-height: 400px;
395
+ border: 1px solid #d2d2d7;
396
+ border-radius: 6px;
397
+ background: #fff;
398
+ }
399
+ .preview-image {
400
+ max-width: 100%;
401
+ border-radius: 6px;
402
+ border: 1px solid #d2d2d7;
403
+ }
404
+ .preview-json {
405
+ background: #f5f5f7;
406
+ padding: 16px;
407
+ border-radius: 8px;
408
+ font-family: 'SF Mono', Menlo, monospace;
409
+ font-size: 13px;
410
+ line-height: 1.6;
411
+ }
412
+ .preview-json .key { color: #007aff; font-weight: 500; }
413
+ .preview-json .string { color: #c41a16; }
414
+ .preview-json .number { color: #1c00cf; }
415
+ .preview-json .boolean { color: #aa0d91; }
416
+ .preview-json .null { color: #86868b; font-style: italic; }
417
+ .preview-json .bracket { color: #1d1d1f; }
418
+ .preview-json details { margin-left: 20px; }
419
+ .preview-json summary { cursor: pointer; margin-left: -20px; }
420
+ .preview-json summary:hover { color: #007aff; }
421
+ .preview-text {
422
+ background: #f5f5f7;
423
+ padding: 16px;
424
+ border-radius: 8px;
425
+ font-family: 'SF Mono', Menlo, monospace;
426
+ font-size: 13px;
427
+ white-space: pre-wrap;
428
+ }
429
+ .preview-xml { color: #1d1d1f; }
430
+ .preview-xml .tag { color: #aa0d91; }
431
+ .preview-xml .attr { color: #007aff; }
432
+ .preview-xml .value { color: #c41a16; }
433
  </style>
434
  </head>
435
  <body>
 
452
  </select>
453
  <input type="text" class="url-input" id="url" placeholder="/hello" value="/hello">
454
  <button class="send-btn" id="send">Send</button>
455
+ <div class="copy-dropdown">
456
+ <button class="copy-btn" id="copy-toggle">Copy as...</button>
457
+ <div class="copy-menu" id="copy-menu">
458
+ <button class="copy-option" data-format="curl">
459
+ <div class="copy-option-label">cURL</div>
460
+ <div class="copy-option-desc">Command line</div>
461
+ </button>
462
+ <button class="copy-option" data-format="python">
463
+ <div class="copy-option-label">Python requests</div>
464
+ <div class="copy-option-desc">requests library</div>
465
+ </button>
466
+ <button class="copy-option" data-format="fetch">
467
+ <div class="copy-option-label">JavaScript fetch</div>
468
+ <div class="copy-option-desc">Browser/Node.js</div>
469
+ </button>
470
+ <button class="copy-option" data-format="httpie">
471
+ <div class="copy-option-label">HTTPie</div>
472
+ <div class="copy-option-desc">CLI for humans</div>
473
+ </button>
474
+ </div>
475
+ </div>
476
  </div>
477
+ <div class="toast" id="toast">Copied to clipboard!</div>
478
 
479
  <div class="main-layout">
480
  <!-- Request Panel -->
 
503
  </div>
504
  <div class="tabs" id="response-tabs">
505
  <button class="tab active" data-tab="res-body">Body</button>
506
+ <button class="tab" data-tab="res-preview">Preview</button>
507
  <button class="tab" data-tab="res-headers">Headers</button>
508
  <button class="tab" data-tab="res-raw">Raw</button>
509
  </div>
510
  <div class="tab-content active" id="res-body-tab">
511
  <div class="response-content" id="response">Send a request to see the response</div>
512
  </div>
513
+ <div class="tab-content" id="res-preview-tab">
514
+ <div class="preview-container" id="preview-container">
515
+ <div class="preview-placeholder">Send a request to see preview</div>
516
+ </div>
517
+ </div>
518
  <div class="tab-content" id="res-headers-tab">
519
  <div class="panel-body">
520
  <div class="headers-list" id="response-headers"></div>
 
661
  }
662
  document.getElementById('response').innerHTML = display;
663
 
664
+ // Preview
665
+ renderPreview(text, res.headers.get('content-type') || '');
666
+
667
  } catch (err) {
668
  document.getElementById('status').textContent = 'Error';
669
  document.getElementById('status').className = 'status-value status-err';
 
679
  document.getElementById('send').onclick = sendRequest;
680
  document.getElementById('url').onkeydown = (e) => { if (e.key === 'Enter') sendRequest(); };
681
  sendRequest();
682
+
683
+ // Preview rendering
684
+ function renderPreview(text, contentType) {
685
+ const container = document.getElementById('preview-container');
686
+ const ct = contentType.toLowerCase();
687
+
688
+ if (ct.includes('text/html')) {
689
+ // Render HTML in sandboxed iframe
690
+ const iframe = document.createElement('iframe');
691
+ iframe.className = 'preview-iframe';
692
+ iframe.sandbox = 'allow-same-origin';
693
+ iframe.srcdoc = text;
694
+ container.innerHTML = '';
695
+ container.appendChild(iframe);
696
+ } else if (ct.includes('image/')) {
697
+ // Show image (requires blob URL for binary)
698
+ const url = document.getElementById('url').value;
699
+ container.innerHTML = `<img class="preview-image" src="${url}" alt="Image preview">`;
700
+ } else if (ct.includes('application/json') || ct.includes('json')) {
701
+ // Interactive JSON tree
702
+ try {
703
+ const json = JSON.parse(text);
704
+ container.innerHTML = '<div class="preview-json">' + renderJSONTree(json) + '</div>';
705
+ } catch {
706
+ container.innerHTML = '<div class="preview-text">' + text.replace(/</g, '&lt;') + '</div>';
707
+ }
708
+ } else if (ct.includes('xml')) {
709
+ // Syntax highlighted XML
710
+ const highlighted = text
711
+ .replace(/</g, '&lt;').replace(/>/g, '&gt;')
712
+ .replace(/&lt;(\\/?[\\w:-]+)/g, '&lt;<span class="tag">$1</span>')
713
+ .replace(/(\\w+)=("[^"]*")/g, '<span class="attr">$1</span>=<span class="value">$2</span>');
714
+ container.innerHTML = '<pre class="preview-text preview-xml">' + highlighted + '</pre>';
715
+ } else if (ct.includes('csv')) {
716
+ // Render CSV as table
717
+ container.innerHTML = renderCSVTable(text);
718
+ } else if (ct.includes('markdown')) {
719
+ // Basic markdown rendering
720
+ container.innerHTML = '<div class="preview-text">' + renderBasicMarkdown(text) + '</div>';
721
+ } else {
722
+ // Plain text
723
+ container.innerHTML = '<pre class="preview-text">' + text.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</pre>';
724
+ }
725
+ }
726
+
727
+ function renderJSONTree(obj, level = 0) {
728
+ if (obj === null) return '<span class="null">null</span>';
729
+ if (typeof obj === 'boolean') return '<span class="boolean">' + obj + '</span>';
730
+ if (typeof obj === 'number') return '<span class="number">' + obj + '</span>';
731
+ if (typeof obj === 'string') return '<span class="string">"' + obj.replace(/</g, '&lt;') + '"</span>';
732
+
733
+ if (Array.isArray(obj)) {
734
+ if (obj.length === 0) return '<span class="bracket">[]</span>';
735
+ let html = '<span class="bracket">[</span>';
736
+ if (level < 2) {
737
+ html += '<br>' + obj.map((v, i) =>
738
+ ' '.repeat(level + 1) + renderJSONTree(v, level + 1) + (i < obj.length - 1 ? ',' : '')
739
+ ).join('<br>') + '<br>' + ' '.repeat(level);
740
+ } else {
741
+ html += obj.map((v, i) => renderJSONTree(v, level + 1) + (i < obj.length - 1 ? ', ' : '')).join('');
742
+ }
743
+ html += '<span class="bracket">]</span>';
744
+ return html;
745
+ }
746
+
747
+ if (typeof obj === 'object') {
748
+ const keys = Object.keys(obj);
749
+ if (keys.length === 0) return '<span class="bracket">{}</span>';
750
+ let html = '<span class="bracket">{</span>';
751
+ if (level < 2) {
752
+ html += '<br>' + keys.map((k, i) =>
753
+ ' '.repeat(level + 1) + '<span class="key">"' + k + '"</span>: ' + renderJSONTree(obj[k], level + 1) + (i < keys.length - 1 ? ',' : '')
754
+ ).join('<br>') + '<br>' + ' '.repeat(level);
755
+ } else {
756
+ html += keys.map((k, i) =>
757
+ '<span class="key">"' + k + '"</span>: ' + renderJSONTree(obj[k], level + 1) + (i < keys.length - 1 ? ', ' : '')
758
+ ).join('');
759
+ }
760
+ html += '<span class="bracket">}</span>';
761
+ return html;
762
+ }
763
+ return String(obj);
764
+ }
765
+
766
+ function renderCSVTable(csv) {
767
+ const lines = csv.trim().split('\\n');
768
+ if (lines.length === 0) return '<div class="preview-text">Empty CSV</div>';
769
+ let html = '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
770
+ lines.forEach((line, i) => {
771
+ const cells = line.split(',');
772
+ const tag = i === 0 ? 'th' : 'td';
773
+ const style = 'padding:8px 12px;border:1px solid #d2d2d7;text-align:left;' + (i === 0 ? 'background:#f5f5f7;font-weight:600;' : '');
774
+ html += '<tr>' + cells.map(c => `<${tag} style="${style}">${c.trim()}</${tag}>`).join('') + '</tr>';
775
+ });
776
+ html += '</table>';
777
+ return html;
778
+ }
779
+
780
+ function renderBasicMarkdown(md) {
781
+ return md
782
+ .replace(/^### (.+)$/gm, '<h3 style="margin:12px 0 8px;font-size:16px;">$1</h3>')
783
+ .replace(/^## (.+)$/gm, '<h2 style="margin:16px 0 8px;font-size:18px;">$1</h2>')
784
+ .replace(/^# (.+)$/gm, '<h1 style="margin:20px 0 12px;font-size:22px;">$1</h1>')
785
+ .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')
786
+ .replace(/\\*(.+?)\\*/g, '<em>$1</em>')
787
+ .replace(/`([^`]+)`/g, '<code style="background:#e8e8e8;padding:2px 6px;border-radius:4px;">$1</code>')
788
+ .replace(/^- (.+)$/gm, '<li style="margin:4px 0;">$1</li>')
789
+ .replace(/\\n/g, '<br>');
790
+ }
791
+
792
+ // Copy as... functionality
793
+ const copyToggle = document.getElementById('copy-toggle');
794
+ const copyMenu = document.getElementById('copy-menu');
795
+ const toast = document.getElementById('toast');
796
+
797
+ copyToggle.onclick = (e) => {
798
+ e.stopPropagation();
799
+ copyMenu.classList.toggle('show');
800
+ };
801
+
802
+ document.addEventListener('click', () => copyMenu.classList.remove('show'));
803
+
804
+ function getBaseUrl() {
805
+ return window.location.origin;
806
+ }
807
+
808
+ function generateCode(format) {
809
+ const method = document.getElementById('method').value;
810
+ const url = document.getElementById('url').value;
811
+ const body = document.getElementById('body').value;
812
+ const headersText = document.getElementById('headers').value;
813
+ const fullUrl = getBaseUrl() + url;
814
+
815
+ const headers = {};
816
+ headersText.split('\\n').forEach(line => {
817
+ const [key, ...vals] = line.split(':');
818
+ if (key && vals.length) headers[key.trim()] = vals.join(':').trim();
819
+ });
820
+
821
+ const hasBody = ['POST', 'PUT', 'PATCH'].includes(method) && body;
822
+
823
+ switch (format) {
824
+ case 'curl': {
825
+ let cmd = `curl -X ${method} "${fullUrl}"`;
826
+ for (const [k, v] of Object.entries(headers)) {
827
+ cmd += ` \\\\\\n -H "${k}: ${v}"`;
828
+ }
829
+ if (hasBody) {
830
+ cmd += ` \\\\\\n -d '${body.replace(/'/g, "\\'")}'`;
831
+ }
832
+ return cmd;
833
+ }
834
+ case 'python': {
835
+ let code = `import requests\\n\\n`;
836
+ code += `url = "${fullUrl}"\\n`;
837
+ if (Object.keys(headers).length) {
838
+ code += `headers = ${JSON.stringify(headers, null, 4)}\\n`;
839
+ }
840
+ if (hasBody) {
841
+ code += `data = ${body}\\n\\n`;
842
+ code += `response = requests.${method.toLowerCase()}(url`;
843
+ if (Object.keys(headers).length) code += `, headers=headers`;
844
+ code += `, json=data)`;
845
+ } else {
846
+ code += `\\nresponse = requests.${method.toLowerCase()}(url`;
847
+ if (Object.keys(headers).length) code += `, headers=headers`;
848
+ code += `)`;
849
+ }
850
+ code += `\\nprint(response.status_code)\\nprint(response.json())`;
851
+ return code;
852
+ }
853
+ case 'fetch': {
854
+ let code = `fetch("${fullUrl}", {\\n`;
855
+ code += ` method: "${method}",\\n`;
856
+ if (Object.keys(headers).length) {
857
+ code += ` headers: ${JSON.stringify(headers, null, 4).replace(/\\n/g, '\\n ')},\\n`;
858
+ }
859
+ if (hasBody) {
860
+ code += ` body: JSON.stringify(${body})\\n`;
861
+ }
862
+ code += `})\\n`;
863
+ code += `.then(res => res.json())\\n`;
864
+ code += `.then(data => console.log(data))\\n`;
865
+ code += `.catch(err => console.error(err));`;
866
+ return code;
867
+ }
868
+ case 'httpie': {
869
+ let cmd = `http ${method} "${fullUrl}"`;
870
+ for (const [k, v] of Object.entries(headers)) {
871
+ cmd += ` "${k}:${v}"`;
872
+ }
873
+ if (hasBody) {
874
+ try {
875
+ const jsonBody = JSON.parse(body);
876
+ for (const [k, v] of Object.entries(jsonBody)) {
877
+ const val = typeof v === 'string' ? `="${v}"` : `:=${JSON.stringify(v)}`;
878
+ cmd += ` ${k}${val}`;
879
+ }
880
+ } catch {
881
+ cmd += ` --raw '${body}'`;
882
+ }
883
+ }
884
+ return cmd;
885
+ }
886
+ }
887
+ }
888
+
889
+ function showToast(msg) {
890
+ toast.textContent = msg;
891
+ toast.classList.add('show');
892
+ setTimeout(() => toast.classList.remove('show'), 2000);
893
+ }
894
+
895
+ document.querySelectorAll('.copy-option').forEach(btn => {
896
+ btn.onclick = async (e) => {
897
+ e.stopPropagation();
898
+ const format = btn.dataset.format;
899
+ const code = generateCode(format);
900
+ try {
901
+ await navigator.clipboard.writeText(code.replace(/\\\\n/g, '\\n'));
902
+ showToast(`Copied as ${format.toUpperCase()}!`);
903
+ } catch {
904
+ showToast('Failed to copy');
905
+ }
906
+ copyMenu.classList.remove('show');
907
+ };
908
+ });
909
  </script>
910
  </body>
911
  </html>