@theihtisham/devtools-with-cloud 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.env.example +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +73 -0
  4. package/docker-compose.yml +23 -0
  5. package/jest.config.js +7 -0
  6. package/next-env.d.ts +5 -0
  7. package/next.config.mjs +22 -0
  8. package/package.json +82 -0
  9. package/postcss.config.js +6 -0
  10. package/prisma/schema.prisma +105 -0
  11. package/prisma/seed.ts +211 -0
  12. package/src/app/(app)/ai/page.tsx +122 -0
  13. package/src/app/(app)/collections/page.tsx +155 -0
  14. package/src/app/(app)/environments/page.tsx +96 -0
  15. package/src/app/(app)/history/page.tsx +107 -0
  16. package/src/app/(app)/import/page.tsx +102 -0
  17. package/src/app/(app)/layout.tsx +60 -0
  18. package/src/app/(app)/settings/page.tsx +79 -0
  19. package/src/app/(app)/workspace/page.tsx +284 -0
  20. package/src/app/api/ai/discover/route.ts +17 -0
  21. package/src/app/api/ai/explain/route.ts +29 -0
  22. package/src/app/api/ai/generate-tests/route.ts +37 -0
  23. package/src/app/api/ai/suggest/route.ts +29 -0
  24. package/src/app/api/collections/[id]/route.ts +66 -0
  25. package/src/app/api/collections/route.ts +48 -0
  26. package/src/app/api/environments/route.ts +40 -0
  27. package/src/app/api/export/openapi/route.ts +17 -0
  28. package/src/app/api/export/postman/route.ts +18 -0
  29. package/src/app/api/import/curl/route.ts +18 -0
  30. package/src/app/api/import/har/route.ts +20 -0
  31. package/src/app/api/import/openapi/route.ts +21 -0
  32. package/src/app/api/import/postman/route.ts +21 -0
  33. package/src/app/api/proxy/route.ts +35 -0
  34. package/src/app/api/requests/[id]/execute/route.ts +85 -0
  35. package/src/app/api/requests/[id]/history/route.ts +23 -0
  36. package/src/app/api/requests/[id]/route.ts +66 -0
  37. package/src/app/api/requests/route.ts +49 -0
  38. package/src/app/api/workspaces/route.ts +38 -0
  39. package/src/app/globals.css +99 -0
  40. package/src/app/layout.tsx +24 -0
  41. package/src/app/page.tsx +182 -0
  42. package/src/components/ai/ai-panel.tsx +65 -0
  43. package/src/components/ai/code-explainer.tsx +51 -0
  44. package/src/components/ai/endpoint-discovery.tsx +62 -0
  45. package/src/components/ai/test-generator.tsx +49 -0
  46. package/src/components/collections/collection-actions.tsx +36 -0
  47. package/src/components/collections/collection-tree.tsx +55 -0
  48. package/src/components/collections/folder-creator.tsx +54 -0
  49. package/src/components/landing/comparison.tsx +43 -0
  50. package/src/components/landing/cta.tsx +16 -0
  51. package/src/components/landing/features.tsx +24 -0
  52. package/src/components/landing/hero.tsx +23 -0
  53. package/src/components/response/body-viewer.tsx +33 -0
  54. package/src/components/response/headers-viewer.tsx +23 -0
  55. package/src/components/response/status-badge.tsx +25 -0
  56. package/src/components/response/test-results.tsx +50 -0
  57. package/src/components/response/timing-chart.tsx +39 -0
  58. package/src/components/ui/badge.tsx +24 -0
  59. package/src/components/ui/button.tsx +32 -0
  60. package/src/components/ui/code-editor.tsx +51 -0
  61. package/src/components/ui/dialog.tsx +56 -0
  62. package/src/components/ui/dropdown.tsx +63 -0
  63. package/src/components/ui/input.tsx +22 -0
  64. package/src/components/ui/key-value-editor.tsx +75 -0
  65. package/src/components/ui/select.tsx +24 -0
  66. package/src/components/ui/tabs.tsx +85 -0
  67. package/src/components/ui/textarea.tsx +22 -0
  68. package/src/components/ui/toast.tsx +54 -0
  69. package/src/components/workspace/request-panel.tsx +38 -0
  70. package/src/components/workspace/response-panel.tsx +81 -0
  71. package/src/components/workspace/sidebar.tsx +52 -0
  72. package/src/components/workspace/split-pane.tsx +49 -0
  73. package/src/components/workspace/tabs/auth-tab.tsx +94 -0
  74. package/src/components/workspace/tabs/body-tab.tsx +41 -0
  75. package/src/components/workspace/tabs/headers-tab.tsx +23 -0
  76. package/src/components/workspace/tabs/params-tab.tsx +23 -0
  77. package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
  78. package/src/components/workspace/url-bar.tsx +53 -0
  79. package/src/hooks/use-ai.ts +115 -0
  80. package/src/hooks/use-collection.ts +71 -0
  81. package/src/hooks/use-environment.ts +73 -0
  82. package/src/hooks/use-request.ts +111 -0
  83. package/src/lib/ai/endpoint-discovery.ts +158 -0
  84. package/src/lib/ai/explainer.ts +127 -0
  85. package/src/lib/ai/suggester.ts +164 -0
  86. package/src/lib/ai/test-generator.ts +161 -0
  87. package/src/lib/auth/api-key.ts +28 -0
  88. package/src/lib/auth/aws-sig.ts +131 -0
  89. package/src/lib/auth/basic.ts +17 -0
  90. package/src/lib/auth/bearer.ts +15 -0
  91. package/src/lib/auth/oauth2.ts +155 -0
  92. package/src/lib/auth/types.ts +16 -0
  93. package/src/lib/db/client.ts +15 -0
  94. package/src/lib/env/manager.ts +32 -0
  95. package/src/lib/env/resolver.ts +30 -0
  96. package/src/lib/exporters/openapi.ts +193 -0
  97. package/src/lib/exporters/postman.ts +140 -0
  98. package/src/lib/graphql/builder.ts +249 -0
  99. package/src/lib/graphql/formatter.ts +147 -0
  100. package/src/lib/graphql/index.ts +43 -0
  101. package/src/lib/graphql/introspection.ts +175 -0
  102. package/src/lib/graphql/types.ts +99 -0
  103. package/src/lib/graphql/validator.ts +216 -0
  104. package/src/lib/http/client.ts +112 -0
  105. package/src/lib/http/proxy.ts +83 -0
  106. package/src/lib/http/request-builder.ts +214 -0
  107. package/src/lib/http/response-parser.ts +106 -0
  108. package/src/lib/http/timing.ts +63 -0
  109. package/src/lib/importers/curl-parser.ts +346 -0
  110. package/src/lib/importers/har-parser.ts +128 -0
  111. package/src/lib/importers/openapi.ts +324 -0
  112. package/src/lib/importers/postman.ts +312 -0
  113. package/src/lib/test-runner/assertions.ts +163 -0
  114. package/src/lib/test-runner/reporter.ts +90 -0
  115. package/src/lib/test-runner/runner.ts +69 -0
  116. package/src/lib/utils/api-response.ts +85 -0
  117. package/src/lib/utils/cn.ts +6 -0
  118. package/src/lib/utils/content-type.ts +123 -0
  119. package/src/lib/utils/download.ts +53 -0
  120. package/src/lib/utils/errors.ts +92 -0
  121. package/src/lib/utils/format.ts +142 -0
  122. package/src/lib/utils/syntax-highlight.ts +108 -0
  123. package/src/lib/utils/validation.ts +231 -0
  124. package/src/lib/websocket/client.ts +182 -0
  125. package/src/lib/websocket/frames.ts +96 -0
  126. package/src/lib/websocket/history.ts +121 -0
  127. package/src/lib/websocket/index.ts +25 -0
  128. package/src/lib/websocket/types.ts +57 -0
  129. package/src/types/ai.ts +28 -0
  130. package/src/types/collection.ts +24 -0
  131. package/src/types/environment.ts +16 -0
  132. package/src/types/request.ts +54 -0
  133. package/src/types/response.ts +37 -0
  134. package/tailwind.config.ts +82 -0
  135. package/tests/lib/env/resolver.test.ts +108 -0
  136. package/tests/lib/graphql/builder.test.ts +349 -0
  137. package/tests/lib/graphql/formatter.test.ts +99 -0
  138. package/tests/lib/http/request-builder.test.ts +160 -0
  139. package/tests/lib/http/response-parser.test.ts +150 -0
  140. package/tests/lib/http/timing.test.ts +188 -0
  141. package/tests/lib/importers/curl-parser.test.ts +245 -0
  142. package/tests/lib/test-runner/assertions.test.ts +342 -0
  143. package/tests/lib/utils/cn.test.ts +46 -0
  144. package/tests/lib/utils/content-type.test.ts +175 -0
  145. package/tests/lib/utils/format.test.ts +188 -0
  146. package/tests/lib/utils/validation.test.ts +237 -0
  147. package/tests/lib/websocket/history.test.ts +186 -0
  148. package/tsconfig.json +29 -0
  149. package/tsconfig.tsbuildinfo +1 -0
  150. package/vitest.config.ts +21 -0
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { cn } from '@/lib/utils/cn';
5
+
6
+ interface Toast {
7
+ id: string;
8
+ message: string;
9
+ type: 'success' | 'error' | 'info';
10
+ }
11
+
12
+ let addToastFn: ((message: string, type?: 'success' | 'error' | 'info') => void) | null = null;
13
+
14
+ export function toast(message: string, type: 'success' | 'error' | 'info' = 'info') {
15
+ addToastFn?.(message, type);
16
+ }
17
+
18
+ export function ToastProvider({ children }: { children: React.ReactNode }) {
19
+ const [toasts, setToasts] = useState<Toast[]>([]);
20
+
21
+ const addToast = useCallback((message: string, type: 'success' | 'error' | 'info' = 'info') => {
22
+ const id = Math.random().toString(36).slice(2);
23
+ setToasts((prev) => [...prev, { id, message, type }]);
24
+ setTimeout(() => {
25
+ setToasts((prev) => prev.filter((t) => t.id !== id));
26
+ }, 3000);
27
+ }, []);
28
+
29
+ useEffect(() => {
30
+ addToastFn = addToast;
31
+ return () => { addToastFn = null; };
32
+ }, [addToast]);
33
+
34
+ return (
35
+ <>
36
+ {children}
37
+ <div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
38
+ {toasts.map((t) => (
39
+ <div
40
+ key={t.id}
41
+ className={cn(
42
+ 'px-4 py-2 rounded-lg text-sm shadow-lg animate-slide-in',
43
+ t.type === 'success' && 'bg-green-500/90 text-white',
44
+ t.type === 'error' && 'bg-destructive/90 text-destructive-foreground',
45
+ t.type === 'info' && 'bg-primary/90 text-primary-foreground',
46
+ )}
47
+ >
48
+ {t.message}
49
+ </div>
50
+ ))}
51
+ </div>
52
+ </>
53
+ );
54
+ }
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { cn } from '@/lib/utils/cn';
5
+
6
+ interface RequestPanelProps {
7
+ activeTab: string;
8
+ onTabChange: (tab: string) => void;
9
+ children: React.ReactNode;
10
+ }
11
+
12
+ const REQUEST_TABS = ['params', 'headers', 'body', 'auth', 'scripts'];
13
+
14
+ export function RequestPanel({ activeTab, onTabChange, children }: RequestPanelProps) {
15
+ return (
16
+ <div className="flex flex-col h-full">
17
+ <div className="flex border-b border-border">
18
+ {REQUEST_TABS.map((tab) => (
19
+ <button
20
+ key={tab}
21
+ onClick={() => onTabChange(tab)}
22
+ className={cn(
23
+ 'px-4 py-2 text-sm capitalize transition-colors',
24
+ activeTab === tab
25
+ ? 'text-primary border-b-2 border-primary'
26
+ : 'text-muted-foreground hover:text-foreground',
27
+ )}
28
+ >
29
+ {tab}
30
+ </button>
31
+ ))}
32
+ </div>
33
+ <div className="flex-1 overflow-auto p-4">
34
+ {children}
35
+ </div>
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,81 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils/cn';
4
+
5
+ interface ResponsePanelProps {
6
+ status: number | null;
7
+ statusText: string | null;
8
+ duration: number | null;
9
+ size: number | null;
10
+ body: string | null;
11
+ error?: string | null;
12
+ }
13
+
14
+ export function ResponsePanel({ status, statusText, duration, size, body, error }: ResponsePanelProps) {
15
+ if (error) {
16
+ return (
17
+ <div className="flex-1 flex items-center justify-center p-4">
18
+ <div className="text-destructive text-sm">{error}</div>
19
+ </div>
20
+ );
21
+ }
22
+
23
+ if (status === null) {
24
+ return (
25
+ <div className="flex-1 flex items-center justify-center text-muted-foreground">
26
+ <div className="text-center">
27
+ <p className="text-lg mb-2">No response yet</p>
28
+ <p className="text-sm">Enter a URL and click Send</p>
29
+ </div>
30
+ </div>
31
+ );
32
+ }
33
+
34
+ return (
35
+ <div className="flex flex-col h-full">
36
+ {/* Status bar */}
37
+ <div className="flex items-center gap-4 p-3 border-b border-border">
38
+ <span className={cn(
39
+ 'px-2 py-1 rounded text-sm font-mono font-medium',
40
+ status < 300 && 'bg-green-500/10 text-green-400',
41
+ status >= 300 && status < 400 && 'bg-blue-500/10 text-blue-400',
42
+ status >= 400 && status < 500 && 'bg-orange-500/10 text-orange-400',
43
+ status >= 500 && 'bg-red-500/10 text-red-400',
44
+ )}>
45
+ {status} {statusText}
46
+ </span>
47
+ {duration != null && (
48
+ <span className="text-sm text-muted-foreground">{duration}ms</span>
49
+ )}
50
+ {size != null && (
51
+ <span className="text-sm text-muted-foreground">{formatBytes(size)}</span>
52
+ )}
53
+ </div>
54
+
55
+ {/* Body */}
56
+ <div className="flex-1 overflow-auto p-4">
57
+ {body && (
58
+ <pre className="text-sm font-mono text-foreground whitespace-pre-wrap break-all">
59
+ {tryFormatJson(body)}
60
+ </pre>
61
+ )}
62
+ </div>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ function formatBytes(bytes: number): string {
68
+ if (bytes === 0) return '0 B';
69
+ const k = 1024;
70
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
71
+ const units = ['B', 'KB', 'MB', 'GB'];
72
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${units[i] ?? 'B'}`;
73
+ }
74
+
75
+ function tryFormatJson(body: string): string {
76
+ try {
77
+ return JSON.stringify(JSON.parse(body), null, 2);
78
+ } catch {
79
+ return body;
80
+ }
81
+ }
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+ import { cn } from '@/lib/utils/cn';
6
+
7
+ const navItems = [
8
+ { href: '/workspace', label: 'Workspace' },
9
+ { href: '/collections', label: 'Collections' },
10
+ { href: '/environments', label: 'Environments' },
11
+ { href: '/history', label: 'History' },
12
+ { href: '/ai', label: 'AI' },
13
+ { href: '/import', label: 'Import' },
14
+ { href: '/settings', label: 'Settings' },
15
+ ];
16
+
17
+ interface SidebarProps {
18
+ className?: string;
19
+ }
20
+
21
+ export function Sidebar({ className }: SidebarProps) {
22
+ const pathname = usePathname();
23
+
24
+ return (
25
+ <aside className={cn('w-56 border-r border-border flex flex-col shrink-0', className)}>
26
+ <div className="p-4 border-b border-border">
27
+ <Link href="/" className="flex items-center gap-2">
28
+ <div className="h-7 w-7 rounded-md bg-primary flex items-center justify-center">
29
+ <span className="text-primary-foreground font-bold text-xs">AT</span>
30
+ </div>
31
+ <span className="font-bold text-foreground">APITester</span>
32
+ </Link>
33
+ </div>
34
+ <nav className="flex-1 p-2">
35
+ {navItems.map((item) => (
36
+ <Link
37
+ key={item.href}
38
+ href={item.href}
39
+ className={cn(
40
+ 'flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors mb-1',
41
+ pathname === item.href || pathname?.startsWith(item.href + '/')
42
+ ? 'bg-primary/10 text-primary'
43
+ : 'text-muted-foreground hover:text-foreground hover:bg-accent',
44
+ )}
45
+ >
46
+ {item.label}
47
+ </Link>
48
+ ))}
49
+ </nav>
50
+ </aside>
51
+ );
52
+ }
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useCallback, type ReactNode } from 'react';
4
+
5
+ interface SplitPaneProps {
6
+ left: ReactNode;
7
+ right: ReactNode;
8
+ defaultSplit?: number;
9
+ minSize?: number;
10
+ className?: string;
11
+ }
12
+
13
+ export function SplitPane({ left, right, defaultSplit = 50, minSize = 200, className }: SplitPaneProps) {
14
+ const [split, setSplit] = useState(defaultSplit);
15
+ const containerRef = useRef<HTMLDivElement>(null);
16
+ const dragging = useRef(false);
17
+
18
+ const onMouseDown = useCallback(() => {
19
+ dragging.current = true;
20
+ const onMouseMove = (e: MouseEvent) => {
21
+ if (!dragging.current || !containerRef.current) return;
22
+ const rect = containerRef.current.getBoundingClientRect();
23
+ const pct = ((e.clientX - rect.left) / rect.width) * 100;
24
+ setSplit(Math.max(minSize / rect.width * 100, Math.min(100 - minSize / rect.width * 100, pct)));
25
+ };
26
+ const onMouseUp = () => {
27
+ dragging.current = false;
28
+ document.removeEventListener('mousemove', onMouseMove);
29
+ document.removeEventListener('mouseup', onMouseUp);
30
+ };
31
+ document.addEventListener('mousemove', onMouseMove);
32
+ document.addEventListener('mouseup', onMouseUp);
33
+ }, [minSize]);
34
+
35
+ return (
36
+ <div ref={containerRef} className="flex h-full">
37
+ <div style={{ width: `${split}%` }} className="overflow-hidden">
38
+ {left}
39
+ </div>
40
+ <div
41
+ className="w-1 cursor-col-resize bg-border hover:bg-primary/30 transition-colors"
42
+ onMouseDown={onMouseDown}
43
+ />
44
+ <div style={{ width: `${100 - split}%` }} className="overflow-hidden">
45
+ {right}
46
+ </div>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ const AUTH_TYPES = ['none', 'basic', 'bearer', 'apikey', 'oauth2', 'aws'];
6
+
7
+ interface AuthTabProps {
8
+ auth: Record<string, string> | null;
9
+ onChange: (auth: Record<string, string> | null) => void;
10
+ }
11
+
12
+ export function AuthTab({ auth, onChange }: AuthTabProps) {
13
+ const authType = auth?.['type'] ?? 'none';
14
+
15
+ return (
16
+ <div>
17
+ <div className="flex items-center gap-2 mb-3">
18
+ <span className="text-sm text-muted-foreground">Type:</span>
19
+ <select
20
+ value={authType}
21
+ onChange={(e) => {
22
+ const type = e.target.value;
23
+ if (type === 'none') onChange(null);
24
+ else onChange({ type, ...getDefaultAuthFields(type) });
25
+ }}
26
+ className="px-2 py-1 rounded border border-border bg-background text-sm text-foreground"
27
+ >
28
+ {AUTH_TYPES.map((t) => (
29
+ <option key={t} value={t}>{t}</option>
30
+ ))}
31
+ </select>
32
+ </div>
33
+
34
+ {authType === 'basic' && (
35
+ <div className="space-y-2">
36
+ <input
37
+ type="text"
38
+ value={auth?.['username'] ?? ''}
39
+ onChange={(e) => onChange({ ...auth!, username: e.target.value })}
40
+ placeholder="Username"
41
+ className="w-full px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm"
42
+ />
43
+ <input
44
+ type="password"
45
+ value={auth?.['password'] ?? ''}
46
+ onChange={(e) => onChange({ ...auth!, password: e.target.value })}
47
+ placeholder="Password"
48
+ className="w-full px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm"
49
+ />
50
+ </div>
51
+ )}
52
+
53
+ {authType === 'bearer' && (
54
+ <input
55
+ type="text"
56
+ value={auth?.['token'] ?? ''}
57
+ onChange={(e) => onChange({ ...auth!, token: e.target.value })}
58
+ placeholder="Token"
59
+ className="w-full px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm"
60
+ />
61
+ )}
62
+
63
+ {authType === 'apikey' && (
64
+ <div className="space-y-2">
65
+ <input
66
+ type="text"
67
+ value={auth?.['key'] ?? ''}
68
+ onChange={(e) => onChange({ ...auth!, key: e.target.value })}
69
+ placeholder="Key name"
70
+ className="w-full px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm"
71
+ />
72
+ <input
73
+ type="text"
74
+ value={auth?.['value'] ?? ''}
75
+ onChange={(e) => onChange({ ...auth!, value: e.target.value })}
76
+ placeholder="Key value"
77
+ className="w-full px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm"
78
+ />
79
+ </div>
80
+ )}
81
+ </div>
82
+ );
83
+ }
84
+
85
+ function getDefaultAuthFields(type: string): Record<string, string> {
86
+ switch (type) {
87
+ case 'basic': return { username: '', password: '' };
88
+ case 'bearer': return { token: '', prefix: 'Bearer' };
89
+ case 'apikey': return { key: '', value: '', addTo: 'header' };
90
+ case 'oauth2': return { grantType: 'authorization_code', clientId: '', clientSecret: '' };
91
+ case 'aws': return { accessKey: '', secretKey: '', region: '', service: '' };
92
+ default: return {};
93
+ }
94
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { CodeEditor } from '@/components/ui/code-editor';
5
+
6
+ const BODY_TYPES = ['none', 'json', 'urlencoded', 'raw', 'graphql'];
7
+
8
+ interface BodyTabProps {
9
+ body: string;
10
+ bodyType: string;
11
+ onBodyChange: (body: string) => void;
12
+ onBodyTypeChange: (bodyType: string) => void;
13
+ }
14
+
15
+ export function BodyTab({ body, bodyType, onBodyChange, onBodyTypeChange }: BodyTabProps) {
16
+ return (
17
+ <div>
18
+ <div className="flex items-center gap-2 mb-3">
19
+ <span className="text-sm text-muted-foreground">Type:</span>
20
+ <select
21
+ value={bodyType}
22
+ onChange={(e) => onBodyTypeChange(e.target.value)}
23
+ className="px-2 py-1 rounded border border-border bg-background text-sm text-foreground"
24
+ >
25
+ {BODY_TYPES.map((t) => (
26
+ <option key={t} value={t}>{t}</option>
27
+ ))}
28
+ </select>
29
+ </div>
30
+ {bodyType !== 'none' && (
31
+ <CodeEditor
32
+ value={body}
33
+ onChange={onBodyChange}
34
+ language={bodyType === 'json' || bodyType === 'graphql' ? 'json' : 'text'}
35
+ placeholder={`Enter ${bodyType} body...`}
36
+ className="h-64"
37
+ />
38
+ )}
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ import { KeyValueEditor } from '@/components/ui/key-value-editor';
4
+ import type { KeyValue } from '@/lib/utils/validation';
5
+
6
+ interface HeadersTabProps {
7
+ headers: KeyValue[];
8
+ onChange: (headers: KeyValue[]) => void;
9
+ }
10
+
11
+ export function HeadersTab({ headers, onChange }: HeadersTabProps) {
12
+ return (
13
+ <div>
14
+ <h3 className="text-sm font-medium text-foreground mb-2">Request Headers</h3>
15
+ <KeyValueEditor
16
+ pairs={headers}
17
+ onChange={onChange}
18
+ keyPlaceholder="Header name"
19
+ valuePlaceholder="Header value"
20
+ />
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ import { KeyValueEditor } from '@/components/ui/key-value-editor';
4
+ import type { KeyValue } from '@/lib/utils/validation';
5
+
6
+ interface ParamsTabProps {
7
+ params: KeyValue[];
8
+ onChange: (params: KeyValue[]) => void;
9
+ }
10
+
11
+ export function ParamsTab({ params, onChange }: ParamsTabProps) {
12
+ return (
13
+ <div>
14
+ <h3 className="text-sm font-medium text-foreground mb-2">Query Parameters</h3>
15
+ <KeyValueEditor
16
+ pairs={params}
17
+ onChange={onChange}
18
+ keyPlaceholder="Parameter name"
19
+ valuePlaceholder="Parameter value"
20
+ />
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import { CodeEditor } from '@/components/ui/code-editor';
4
+
5
+ interface PreRequestTabProps {
6
+ script: string;
7
+ onChange: (script: string) => void;
8
+ }
9
+
10
+ export function PreRequestTab({ script, onChange }: PreRequestTabProps) {
11
+ return (
12
+ <div>
13
+ <h3 className="text-sm font-medium text-foreground mb-2">Pre-request Script</h3>
14
+ <p className="text-xs text-muted-foreground mb-3">
15
+ JavaScript code that runs before the request is sent.
16
+ </p>
17
+ <CodeEditor
18
+ value={script}
19
+ onChange={onChange}
20
+ language="javascript"
21
+ placeholder="// Write your pre-request script here"
22
+ className="h-64"
23
+ />
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils/cn';
4
+ import type { HttpMethod } from '@/lib/utils/validation';
5
+
6
+ const METHODS: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
7
+
8
+ interface UrlBarProps {
9
+ method: HttpMethod;
10
+ url: string;
11
+ loading: boolean;
12
+ onMethodChange: (method: HttpMethod) => void;
13
+ onUrlChange: (url: string) => void;
14
+ onSend: () => void;
15
+ }
16
+
17
+ export function UrlBar({ method, url, loading, onMethodChange, onUrlChange, onSend }: UrlBarProps) {
18
+ return (
19
+ <div className="flex items-center gap-2 p-3 border-b border-border">
20
+ <select
21
+ value={method}
22
+ onChange={(e) => onMethodChange(e.target.value as HttpMethod)}
23
+ className={cn(
24
+ 'px-3 py-2 rounded-md border border-border bg-background text-sm font-medium font-mono',
25
+ method === 'GET' && 'text-method-get',
26
+ method === 'POST' && 'text-method-post',
27
+ method === 'PUT' && 'text-method-put',
28
+ method === 'DELETE' && 'text-method-delete',
29
+ method === 'PATCH' && 'text-method-patch',
30
+ )}
31
+ >
32
+ {METHODS.map((m) => (
33
+ <option key={m} value={m}>{m}</option>
34
+ ))}
35
+ </select>
36
+ <input
37
+ type="text"
38
+ value={url}
39
+ onChange={(e) => onUrlChange(e.target.value)}
40
+ onKeyDown={(e) => e.key === 'Enter' && onSend()}
41
+ placeholder="Enter request URL"
42
+ className="flex-1 px-3 py-2 rounded-md border border-border bg-background text-foreground text-sm font-mono placeholder:text-muted-foreground"
43
+ />
44
+ <button
45
+ onClick={onSend}
46
+ disabled={loading || !url.trim()}
47
+ className="px-6 py-2 rounded-md bg-primary text-primary-foreground font-medium text-sm hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
48
+ >
49
+ {loading ? 'Sending...' : 'Send'}
50
+ </button>
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+
5
+ interface UseAiReturn {
6
+ loading: boolean;
7
+ result: string | null;
8
+ error: string | null;
9
+ generateTests: (response: unknown) => Promise<void>;
10
+ discoverEndpoints: (code: string, framework?: string) => Promise<void>;
11
+ explainResponse: (response: unknown, question?: string) => Promise<void>;
12
+ suggestImprovements: (request: unknown, response?: unknown) => Promise<void>;
13
+ clear: () => void;
14
+ }
15
+
16
+ export function useAi(): UseAiReturn {
17
+ const [loading, setLoading] = useState(false);
18
+ const [result, setResult] = useState<string | null>(null);
19
+ const [error, setError] = useState<string | null>(null);
20
+
21
+ const generateTests = useCallback(async (response: unknown) => {
22
+ setLoading(true);
23
+ setError(null);
24
+ try {
25
+ const res = await fetch('/api/ai/generate-tests', {
26
+ method: 'POST',
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: JSON.stringify({ requestId: 'manual', response }),
29
+ });
30
+ const data = await res.json();
31
+ if (data.success) {
32
+ setResult(typeof data.data === 'string' ? data.data : JSON.stringify(data.data, null, 2));
33
+ } else {
34
+ setError(data.error?.message ?? 'Failed to generate tests');
35
+ }
36
+ } catch (err) {
37
+ setError(err instanceof Error ? err.message : 'Failed to generate tests');
38
+ } finally {
39
+ setLoading(false);
40
+ }
41
+ }, []);
42
+
43
+ const discoverEndpoints = useCallback(async (code: string, framework: string = 'auto') => {
44
+ setLoading(true);
45
+ setError(null);
46
+ try {
47
+ const res = await fetch('/api/ai/discover', {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: JSON.stringify({ code, framework, workspaceId: 'default' }),
51
+ });
52
+ const data = await res.json();
53
+ if (data.success) {
54
+ setResult(JSON.stringify(data.data, null, 2));
55
+ } else {
56
+ setError(data.error?.message ?? 'Failed to discover endpoints');
57
+ }
58
+ } catch (err) {
59
+ setError(err instanceof Error ? err.message : 'Failed to discover endpoints');
60
+ } finally {
61
+ setLoading(false);
62
+ }
63
+ }, []);
64
+
65
+ const explainResponse = useCallback(async (response: unknown, question?: string) => {
66
+ setLoading(true);
67
+ setError(null);
68
+ try {
69
+ const res = await fetch('/api/ai/explain', {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({ response, question }),
73
+ });
74
+ const data = await res.json();
75
+ if (data.success) {
76
+ setResult(typeof data.data === 'string' ? data.data : JSON.stringify(data.data, null, 2));
77
+ } else {
78
+ setError(data.error?.message ?? 'Failed to explain response');
79
+ }
80
+ } catch (err) {
81
+ setError(err instanceof Error ? err.message : 'Failed to explain response');
82
+ } finally {
83
+ setLoading(false);
84
+ }
85
+ }, []);
86
+
87
+ const suggestImprovements = useCallback(async (request: unknown, response?: unknown) => {
88
+ setLoading(true);
89
+ setError(null);
90
+ try {
91
+ const res = await fetch('/api/ai/suggest', {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify({ request, response }),
95
+ });
96
+ const data = await res.json();
97
+ if (data.success) {
98
+ setResult(JSON.stringify(data.data, null, 2));
99
+ } else {
100
+ setError(data.error?.message ?? 'Failed to get suggestions');
101
+ }
102
+ } catch (err) {
103
+ setError(err instanceof Error ? err.message : 'Failed to get suggestions');
104
+ } finally {
105
+ setLoading(false);
106
+ }
107
+ }, []);
108
+
109
+ const clear = useCallback(() => {
110
+ setResult(null);
111
+ setError(null);
112
+ }, []);
113
+
114
+ return { loading, result, error, generateTests, discoverEndpoints, explainResponse, suggestImprovements, clear };
115
+ }