@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,122 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ export default function AiPage() {
6
+ const [activeFeature, setActiveFeature] = useState<'generate' | 'discover' | 'explain' | 'suggest'>('generate');
7
+ const [input, setInput] = useState('');
8
+ const [output, setOutput] = useState('');
9
+ const [loading, setLoading] = useState(false);
10
+
11
+ const handleSubmit = async () => {
12
+ if (!input.trim()) return;
13
+ setLoading(true);
14
+ setOutput('');
15
+
16
+ try {
17
+ let url = '';
18
+ let body: Record<string, unknown> = {};
19
+
20
+ switch (activeFeature) {
21
+ case 'generate':
22
+ url = '/api/ai/generate-tests';
23
+ body = { requestId: 'manual', response: input };
24
+ break;
25
+ case 'discover':
26
+ url = '/api/ai/discover';
27
+ body = { code: input, framework: 'auto', workspaceId: 'default' };
28
+ break;
29
+ case 'explain':
30
+ url = '/api/ai/explain';
31
+ body = { response: input };
32
+ break;
33
+ case 'suggest':
34
+ url = '/api/ai/suggest';
35
+ body = { request: input };
36
+ break;
37
+ }
38
+
39
+ const res = await fetch(url, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify(body),
43
+ });
44
+
45
+ const data = await res.json();
46
+ if (data.success) {
47
+ setOutput(typeof data.data === 'string' ? data.data : JSON.stringify(data.data, null, 2));
48
+ } else {
49
+ setOutput(`Error: ${data.error?.message ?? 'Unknown error'}`);
50
+ }
51
+ } catch (err) {
52
+ setOutput(`Error: ${err instanceof Error ? err.message : 'Request failed'}`);
53
+ } finally {
54
+ setLoading(false);
55
+ }
56
+ };
57
+
58
+ const features = [
59
+ { key: 'generate' as const, label: 'Generate Tests', description: 'Generate test assertions from API responses' },
60
+ { key: 'discover' as const, label: 'Discover Endpoints', description: 'Auto-discover API endpoints from code' },
61
+ { key: 'explain' as const, label: 'Explain Response', description: 'Get AI explanations of API responses' },
62
+ { key: 'suggest' as const, label: 'Suggest Improvements', description: 'Get suggestions for API improvements' },
63
+ ];
64
+
65
+ return (
66
+ <div className="p-6">
67
+ <h1 className="text-2xl font-bold text-foreground mb-6">AI Assistant</h1>
68
+
69
+ {/* Feature Selection */}
70
+ <div className="grid grid-cols-4 gap-3 mb-6">
71
+ {features.map((feature) => (
72
+ <button
73
+ key={feature.key}
74
+ onClick={() => { setActiveFeature(feature.key); setInput(''); setOutput(''); }}
75
+ className={`p-3 rounded-lg border text-left transition-colors ${
76
+ activeFeature === feature.key
77
+ ? 'border-primary bg-primary/10'
78
+ : 'border-border hover:border-primary/50'
79
+ }`}
80
+ >
81
+ <div className="font-medium text-foreground text-sm">{feature.label}</div>
82
+ <div className="text-xs text-muted-foreground mt-1">{feature.description}</div>
83
+ </button>
84
+ ))}
85
+ </div>
86
+
87
+ {/* Input */}
88
+ <div className="mb-4">
89
+ <label className="block text-sm font-medium text-foreground mb-2">
90
+ {activeFeature === 'generate' && 'Paste a JSON response'}
91
+ {activeFeature === 'discover' && 'Paste source code to scan'}
92
+ {activeFeature === 'explain' && 'Paste a response to explain'}
93
+ {activeFeature === 'suggest' && 'Paste a request to analyze'}
94
+ </label>
95
+ <textarea
96
+ value={input}
97
+ onChange={(e) => setInput(e.target.value)}
98
+ className="w-full h-40 px-3 py-2 rounded-md border border-border bg-background text-foreground font-mono text-sm resize-y"
99
+ placeholder={activeFeature === 'discover' ? '// Paste Express/FastAPI/Next.js code here...' : '{ "key": "value" }'}
100
+ />
101
+ </div>
102
+
103
+ <button
104
+ onClick={handleSubmit}
105
+ disabled={loading || !input.trim()}
106
+ className="px-6 py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 disabled:opacity-50 transition-colors"
107
+ >
108
+ {loading ? 'Processing...' : 'Run'}
109
+ </button>
110
+
111
+ {/* Output */}
112
+ {output && (
113
+ <div className="mt-6">
114
+ <label className="block text-sm font-medium text-foreground mb-2">Result</label>
115
+ <pre className="p-4 rounded-lg border border-border bg-muted/50 text-foreground font-mono text-sm whitespace-pre-wrap overflow-auto max-h-96">
116
+ {output}
117
+ </pre>
118
+ </div>
119
+ )}
120
+ </div>
121
+ );
122
+ }
@@ -0,0 +1,155 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+
5
+ interface Collection {
6
+ id: string;
7
+ name: string;
8
+ description?: string;
9
+ workspaceId: string;
10
+ parentId?: string;
11
+ sortOrder: number;
12
+ createdAt: string;
13
+ }
14
+
15
+ export default function CollectionsPage() {
16
+ const [collections, setCollections] = useState<Collection[]>([]);
17
+ const [loading, setLoading] = useState(true);
18
+ const [showCreate, setShowCreate] = useState(false);
19
+ const [newName, setNewName] = useState('');
20
+ const [newDescription, setNewDescription] = useState('');
21
+
22
+ const fetchCollections = useCallback(async () => {
23
+ try {
24
+ const res = await fetch('/api/collections');
25
+ const data = await res.json();
26
+ if (data.success) {
27
+ setCollections(data.data);
28
+ }
29
+ } catch {
30
+ // Collections may not be available without DB
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ fetchCollections();
38
+ }, [fetchCollections]);
39
+
40
+ const createCollection = async () => {
41
+ if (!newName.trim()) return;
42
+ try {
43
+ const res = await fetch('/api/collections', {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify({
47
+ name: newName,
48
+ description: newDescription || undefined,
49
+ workspaceId: 'default',
50
+ }),
51
+ });
52
+ const data = await res.json();
53
+ if (data.success) {
54
+ setCollections((prev) => [...prev, data.data]);
55
+ setShowCreate(false);
56
+ setNewName('');
57
+ setNewDescription('');
58
+ }
59
+ } catch {
60
+ // Handle error
61
+ }
62
+ };
63
+
64
+ const deleteCollection = async (id: string) => {
65
+ try {
66
+ await fetch(`/api/collections/${id}`, { method: 'DELETE' });
67
+ setCollections((prev) => prev.filter((c) => c.id !== id));
68
+ } catch {
69
+ // Handle error
70
+ }
71
+ };
72
+
73
+ return (
74
+ <div className="p-6">
75
+ <div className="flex items-center justify-between mb-6">
76
+ <h1 className="text-2xl font-bold text-foreground">Collections</h1>
77
+ <button
78
+ onClick={() => setShowCreate(true)}
79
+ className="px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 transition-colors"
80
+ >
81
+ New Collection
82
+ </button>
83
+ </div>
84
+
85
+ {showCreate && (
86
+ <div className="border border-border rounded-lg p-4 mb-6">
87
+ <h2 className="text-lg font-semibold text-foreground mb-3">Create Collection</h2>
88
+ <div className="space-y-3">
89
+ <input
90
+ type="text"
91
+ value={newName}
92
+ onChange={(e) => setNewName(e.target.value)}
93
+ placeholder="Collection name"
94
+ className="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground text-sm"
95
+ />
96
+ <textarea
97
+ value={newDescription}
98
+ onChange={(e) => setNewDescription(e.target.value)}
99
+ placeholder="Description (optional)"
100
+ className="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground text-sm"
101
+ rows={2}
102
+ />
103
+ <div className="flex gap-2">
104
+ <button
105
+ onClick={createCollection}
106
+ className="px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm"
107
+ >
108
+ Create
109
+ </button>
110
+ <button
111
+ onClick={() => { setShowCreate(false); setNewName(''); setNewDescription(''); }}
112
+ className="px-4 py-2 border border-border rounded-md text-sm text-foreground"
113
+ >
114
+ Cancel
115
+ </button>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ )}
120
+
121
+ {loading ? (
122
+ <div className="text-muted-foreground text-center py-8">Loading collections...</div>
123
+ ) : collections.length === 0 ? (
124
+ <div className="text-center py-12">
125
+ <p className="text-muted-foreground mb-2">No collections yet</p>
126
+ <p className="text-sm text-muted-foreground">Create a collection to organize your API requests</p>
127
+ </div>
128
+ ) : (
129
+ <div className="space-y-3">
130
+ {collections.map((collection) => (
131
+ <div
132
+ key={collection.id}
133
+ className="border border-border rounded-lg p-4 hover:border-primary/50 transition-colors"
134
+ >
135
+ <div className="flex items-center justify-between">
136
+ <div>
137
+ <h3 className="font-medium text-foreground">{collection.name}</h3>
138
+ {collection.description && (
139
+ <p className="text-sm text-muted-foreground mt-1">{collection.description}</p>
140
+ )}
141
+ </div>
142
+ <button
143
+ onClick={() => deleteCollection(collection.id)}
144
+ className="text-sm text-destructive hover:text-destructive/80 transition-colors"
145
+ >
146
+ Delete
147
+ </button>
148
+ </div>
149
+ </div>
150
+ ))}
151
+ </div>
152
+ )}
153
+ </div>
154
+ );
155
+ }
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+
5
+ interface Environment {
6
+ id: string;
7
+ name: string;
8
+ workspaceId: string;
9
+ isDefault: boolean;
10
+ variables: Array<{
11
+ key: string;
12
+ value: string;
13
+ type: string;
14
+ enabled: boolean;
15
+ }>;
16
+ }
17
+
18
+ export default function EnvironmentsPage() {
19
+ const [environments, setEnvironments] = useState<Environment[]>([]);
20
+ const [loading, setLoading] = useState(true);
21
+
22
+ const fetchEnvironments = useCallback(async () => {
23
+ try {
24
+ const res = await fetch('/api/environments');
25
+ const data = await res.json();
26
+ if (data.success) {
27
+ setEnvironments(data.data);
28
+ }
29
+ } catch {
30
+ // Environments may not be available without DB
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ fetchEnvironments();
38
+ }, [fetchEnvironments]);
39
+
40
+ return (
41
+ <div className="p-6">
42
+ <div className="flex items-center justify-between mb-6">
43
+ <h1 className="text-2xl font-bold text-foreground">Environments</h1>
44
+ </div>
45
+
46
+ {loading ? (
47
+ <div className="text-muted-foreground text-center py-8">Loading environments...</div>
48
+ ) : environments.length === 0 ? (
49
+ <div className="text-center py-12">
50
+ <p className="text-muted-foreground mb-2">No environments configured</p>
51
+ <p className="text-sm text-muted-foreground">
52
+ Create environments to manage variables for different stages (dev, staging, prod)
53
+ </p>
54
+ </div>
55
+ ) : (
56
+ <div className="space-y-4">
57
+ {environments.map((env) => (
58
+ <div
59
+ key={env.id}
60
+ className="border border-border rounded-lg p-4"
61
+ >
62
+ <div className="flex items-center gap-2 mb-3">
63
+ <h3 className="font-medium text-foreground">{env.name}</h3>
64
+ {env.isDefault && (
65
+ <span className="px-2 py-0.5 bg-primary/10 text-primary text-xs rounded-full">
66
+ Default
67
+ </span>
68
+ )}
69
+ </div>
70
+ {env.variables.length > 0 ? (
71
+ <div className="space-y-1">
72
+ {env.variables
73
+ .filter((v) => v.enabled)
74
+ .map((variable, i) => (
75
+ <div key={i} className="flex items-center gap-2 text-sm font-mono">
76
+ <span className="text-primary">{variable.key}</span>
77
+ <span className="text-muted-foreground">=</span>
78
+ <span className="text-foreground">
79
+ {variable.type === 'secret' ? '********' : variable.value}
80
+ </span>
81
+ {variable.type !== 'string' && (
82
+ <span className="text-xs text-muted-foreground">({variable.type})</span>
83
+ )}
84
+ </div>
85
+ ))}
86
+ </div>
87
+ ) : (
88
+ <p className="text-sm text-muted-foreground">No variables defined</p>
89
+ )}
90
+ </div>
91
+ ))}
92
+ </div>
93
+ )}
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { cn } from '@/lib/utils/cn';
5
+
6
+ interface HistoryEntry {
7
+ id: string;
8
+ method: string;
9
+ url: string;
10
+ status?: number;
11
+ statusText?: string;
12
+ duration?: number;
13
+ createdAt: string;
14
+ }
15
+
16
+ export default function HistoryPage() {
17
+ const [history, setHistory] = useState<HistoryEntry[]>([]);
18
+ const [loading, setLoading] = useState(true);
19
+
20
+ const fetchHistory = useCallback(async () => {
21
+ try {
22
+ const res = await fetch('/api/requests');
23
+ const data = await res.json();
24
+ if (data.success && Array.isArray(data.data)) {
25
+ // Flatten all requests' history
26
+ const allHistory: HistoryEntry[] = [];
27
+ for (const req of data.data) {
28
+ const histRes = await fetch(`/api/requests/${req.id}/history`);
29
+ const histData = await histRes.json();
30
+ if (histData.success && Array.isArray(histData.data)) {
31
+ allHistory.push(...histData.data);
32
+ }
33
+ }
34
+ allHistory.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
35
+ setHistory(allHistory);
36
+ }
37
+ } catch {
38
+ // History may not be available without DB
39
+ } finally {
40
+ setLoading(false);
41
+ }
42
+ }, []);
43
+
44
+ useEffect(() => {
45
+ fetchHistory();
46
+ }, [fetchHistory]);
47
+
48
+ return (
49
+ <div className="p-6">
50
+ <div className="flex items-center justify-between mb-6">
51
+ <h1 className="text-2xl font-bold text-foreground">Request History</h1>
52
+ </div>
53
+
54
+ {loading ? (
55
+ <div className="text-muted-foreground text-center py-8">Loading history...</div>
56
+ ) : history.length === 0 ? (
57
+ <div className="text-center py-12">
58
+ <p className="text-muted-foreground mb-2">No request history</p>
59
+ <p className="text-sm text-muted-foreground">
60
+ Send requests from the workspace to see them here
61
+ </p>
62
+ </div>
63
+ ) : (
64
+ <div className="space-y-2">
65
+ {history.map((entry) => (
66
+ <div
67
+ key={entry.id}
68
+ className="flex items-center gap-4 p-3 border border-border rounded-lg hover:bg-accent/50 transition-colors"
69
+ >
70
+ <span className={cn(
71
+ 'px-2 py-0.5 rounded text-xs font-mono font-medium',
72
+ entry.method === 'GET' && 'text-method-get',
73
+ entry.method === 'POST' && 'text-method-post',
74
+ entry.method === 'PUT' && 'text-method-put',
75
+ entry.method === 'DELETE' && 'text-method-delete',
76
+ entry.method === 'PATCH' && 'text-method-patch',
77
+ )}>
78
+ {entry.method}
79
+ </span>
80
+ <span className="text-sm text-foreground font-mono flex-1 truncate">
81
+ {entry.url}
82
+ </span>
83
+ {entry.status && (
84
+ <span className={cn(
85
+ 'text-sm font-mono',
86
+ entry.status < 300 && 'text-green-400',
87
+ entry.status >= 400 && 'text-red-400',
88
+ entry.status >= 300 && entry.status < 400 && 'text-blue-400',
89
+ )}>
90
+ {entry.status}
91
+ </span>
92
+ )}
93
+ {entry.duration != null && (
94
+ <span className="text-sm text-muted-foreground">
95
+ {entry.duration}ms
96
+ </span>
97
+ )}
98
+ <span className="text-xs text-muted-foreground">
99
+ {new Date(entry.createdAt).toLocaleString()}
100
+ </span>
101
+ </div>
102
+ ))}
103
+ </div>
104
+ )}
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ type ImportFormat = 'openapi' | 'postman' | 'curl' | 'har';
6
+
7
+ export default function ImportPage() {
8
+ const [format, setFormat] = useState<ImportFormat>('openapi');
9
+ const [input, setInput] = useState('');
10
+ const [loading, setLoading] = useState(false);
11
+ const [result, setResult] = useState<string | null>(null);
12
+
13
+ const handleImport = async () => {
14
+ if (!input.trim()) return;
15
+ setLoading(true);
16
+ setResult(null);
17
+
18
+ try {
19
+ const res = await fetch(`/api/import/${format}`, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json' },
22
+ body: JSON.stringify(
23
+ format === 'openapi'
24
+ ? { spec: input, workspaceId: 'default' }
25
+ : format === 'postman'
26
+ ? { collection: input, workspaceId: 'default' }
27
+ : format === 'curl'
28
+ ? { command: input, collectionId: 'default' }
29
+ : { har: input, collectionId: 'default' },
30
+ ),
31
+ });
32
+
33
+ const data = await res.json();
34
+ if (data.success) {
35
+ setResult(`Successfully imported: ${JSON.stringify(data.data, null, 2).slice(0, 500)}`);
36
+ } else {
37
+ setResult(`Error: ${data.error?.message ?? 'Import failed'}`);
38
+ }
39
+ } catch (err) {
40
+ setResult(`Error: ${err instanceof Error ? err.message : 'Import failed'}`);
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ };
45
+
46
+ const formats: { key: ImportFormat; label: string; placeholder: string }[] = [
47
+ { key: 'openapi', label: 'OpenAPI / Swagger', placeholder: 'Paste your OpenAPI 3.x JSON or YAML spec...' },
48
+ { key: 'postman', label: 'Postman Collection', placeholder: 'Paste your Postman Collection v2.1 JSON...' },
49
+ { key: 'curl', label: 'cURL Command', placeholder: 'curl -X GET https://api.example.com/users -H "Accept: application/json"' },
50
+ { key: 'har', label: 'HAR File', placeholder: 'Paste your HAR (HTTP Archive) JSON...' },
51
+ ];
52
+
53
+ return (
54
+ <div className="p-6 max-w-4xl">
55
+ <h1 className="text-2xl font-bold text-foreground mb-6">Import</h1>
56
+
57
+ {/* Format Selection */}
58
+ <div className="flex gap-2 mb-6">
59
+ {formats.map((f) => (
60
+ <button
61
+ key={f.key}
62
+ onClick={() => { setFormat(f.key); setInput(''); setResult(null); }}
63
+ className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
64
+ format === f.key
65
+ ? 'bg-primary text-primary-foreground'
66
+ : 'border border-border text-foreground hover:bg-accent'
67
+ }`}
68
+ >
69
+ {f.label}
70
+ </button>
71
+ ))}
72
+ </div>
73
+
74
+ {/* Input */}
75
+ <div className="mb-4">
76
+ <textarea
77
+ value={input}
78
+ onChange={(e) => setInput(e.target.value)}
79
+ className="w-full h-64 px-3 py-2 rounded-md border border-border bg-background text-foreground font-mono text-sm resize-y"
80
+ placeholder={formats.find((f) => f.key === format)?.placeholder}
81
+ />
82
+ </div>
83
+
84
+ <button
85
+ onClick={handleImport}
86
+ disabled={loading || !input.trim()}
87
+ className="px-6 py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 disabled:opacity-50 transition-colors"
88
+ >
89
+ {loading ? 'Importing...' : 'Import'}
90
+ </button>
91
+
92
+ {/* Result */}
93
+ {result && (
94
+ <div className="mt-6">
95
+ <pre className="p-4 rounded-lg border border-border bg-muted/50 text-foreground font-mono text-sm whitespace-pre-wrap overflow-auto max-h-64">
96
+ {result}
97
+ </pre>
98
+ </div>
99
+ )}
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,60 @@
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', icon: '{ }' },
9
+ { href: '/collections', label: 'Collections', icon: '#' },
10
+ { href: '/environments', label: 'Environments', icon: '$' },
11
+ { href: '/history', label: 'History', icon: '<>' },
12
+ { href: '/ai', label: 'AI Assistant', icon: '*' },
13
+ { href: '/import', label: 'Import', icon: '+' },
14
+ { href: '/settings', label: 'Settings', icon: '@' },
15
+ ];
16
+
17
+ export default function AppLayout({ children }: { children: React.ReactNode }) {
18
+ const pathname = usePathname();
19
+
20
+ return (
21
+ <div className="flex h-screen bg-background">
22
+ {/* Sidebar */}
23
+ <aside className="w-56 border-r border-border flex flex-col shrink-0">
24
+ <div className="p-4 border-b border-border">
25
+ <Link href="/" className="flex items-center gap-2">
26
+ <div className="h-7 w-7 rounded-md bg-primary flex items-center justify-center">
27
+ <span className="text-primary-foreground font-bold text-xs">AT</span>
28
+ </div>
29
+ <span className="font-bold text-foreground">APITester</span>
30
+ </Link>
31
+ </div>
32
+ <nav className="flex-1 p-2">
33
+ {navItems.map((item) => (
34
+ <Link
35
+ key={item.href}
36
+ href={item.href}
37
+ className={cn(
38
+ 'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors mb-1',
39
+ pathname === item.href || pathname?.startsWith(item.href + '/')
40
+ ? 'bg-primary/10 text-primary'
41
+ : 'text-muted-foreground hover:text-foreground hover:bg-accent',
42
+ )}
43
+ >
44
+ <span className="font-mono text-xs w-5 text-center">{item.icon}</span>
45
+ {item.label}
46
+ </Link>
47
+ ))}
48
+ </nav>
49
+ <div className="p-4 border-t border-border text-xs text-muted-foreground">
50
+ v1.0.0
51
+ </div>
52
+ </aside>
53
+
54
+ {/* Main Content */}
55
+ <main className="flex-1 overflow-auto">
56
+ {children}
57
+ </main>
58
+ </div>
59
+ );
60
+ }