create-stylus-ide 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.
- package/Readme.MD +1515 -0
- package/cli.js +28 -0
- package/frontend/.vscode/settings.json +9 -0
- package/frontend/app/api/chat/route.ts +101 -0
- package/frontend/app/api/check-setup/route.ts +93 -0
- package/frontend/app/api/cleanup/route.ts +14 -0
- package/frontend/app/api/compile/route.ts +95 -0
- package/frontend/app/api/compile-stream/route.ts +98 -0
- package/frontend/app/api/complete/route.ts +86 -0
- package/frontend/app/api/deploy/route.ts +118 -0
- package/frontend/app/api/export-abi/route.ts +58 -0
- package/frontend/app/favicon.ico +0 -0
- package/frontend/app/globals.css +177 -0
- package/frontend/app/layout.tsx +29 -0
- package/frontend/app/ml/page.tsx +694 -0
- package/frontend/app/page.tsx +1132 -0
- package/frontend/app/providers.tsx +18 -0
- package/frontend/app/qlearning/page.tsx +188 -0
- package/frontend/app/raytracing/page.tsx +268 -0
- package/frontend/components/abi/ABIDialog.tsx +132 -0
- package/frontend/components/ai/AICompletionPopup.tsx +76 -0
- package/frontend/components/ai/ChatPanel.tsx +292 -0
- package/frontend/components/ai/QuickActions.tsx +128 -0
- package/frontend/components/blockchain/BlockchainContractBanner.tsx +64 -0
- package/frontend/components/blockchain/BlockchainLoadingDialog.tsx +188 -0
- package/frontend/components/deploy/DeployDialog.tsx +334 -0
- package/frontend/components/editor/FileTabs.tsx +181 -0
- package/frontend/components/editor/MonacoEditor.tsx +306 -0
- package/frontend/components/file-tree/ContextMenu.tsx +110 -0
- package/frontend/components/file-tree/DeleteConfirmDialog.tsx +61 -0
- package/frontend/components/file-tree/FileInputDialog.tsx +97 -0
- package/frontend/components/file-tree/FileNode.tsx +60 -0
- package/frontend/components/file-tree/FileTree.tsx +259 -0
- package/frontend/components/file-tree/FileTreeSkeleton.tsx +26 -0
- package/frontend/components/file-tree/FolderNode.tsx +105 -0
- package/frontend/components/github/GitHubLoadingDialog.tsx +201 -0
- package/frontend/components/github/GitHubMetadataBanner.tsx +61 -0
- package/frontend/components/github/LoadFromGitHubDialog.tsx +125 -0
- package/frontend/components/github/URLCopyButton.tsx +60 -0
- package/frontend/components/interact/ContractInteraction.tsx +323 -0
- package/frontend/components/interact/ContractPlaceholder.tsx +41 -0
- package/frontend/components/orbit/BenchmarkDialog.tsx +342 -0
- package/frontend/components/orbit/OrbitExplorer.tsx +273 -0
- package/frontend/components/project/ProjectActions.tsx +176 -0
- package/frontend/components/q-learning/ContractConfig.tsx +172 -0
- package/frontend/components/q-learning/MazeGrid.tsx +346 -0
- package/frontend/components/q-learning/PathAnimation.tsx +384 -0
- package/frontend/components/q-learning/QTableHeatmap.tsx +300 -0
- package/frontend/components/q-learning/TrainingForm.tsx +349 -0
- package/frontend/components/ray-tracing/ContractConfig.tsx +245 -0
- package/frontend/components/ray-tracing/MintingForm.tsx +280 -0
- package/frontend/components/ray-tracing/RenderCanvas.tsx +228 -0
- package/frontend/components/ray-tracing/RenderingPanel.tsx +259 -0
- package/frontend/components/ray-tracing/StyleControls.tsx +217 -0
- package/frontend/components/setup/SetupGuide.tsx +290 -0
- package/frontend/components/ui/KeyboardShortcutHint.tsx +74 -0
- package/frontend/components/ui/alert-dialog.tsx +157 -0
- package/frontend/components/ui/alert.tsx +66 -0
- package/frontend/components/ui/badge.tsx +46 -0
- package/frontend/components/ui/button.tsx +62 -0
- package/frontend/components/ui/card.tsx +92 -0
- package/frontend/components/ui/context-menu.tsx +252 -0
- package/frontend/components/ui/dialog.tsx +143 -0
- package/frontend/components/ui/dropdown-menu.tsx +257 -0
- package/frontend/components/ui/input.tsx +21 -0
- package/frontend/components/ui/label.tsx +24 -0
- package/frontend/components/ui/progress.tsx +31 -0
- package/frontend/components/ui/scroll-area.tsx +58 -0
- package/frontend/components/ui/select.tsx +190 -0
- package/frontend/components/ui/separator.tsx +28 -0
- package/frontend/components/ui/sheet.tsx +139 -0
- package/frontend/components/ui/skeleton.tsx +13 -0
- package/frontend/components/ui/slider.tsx +63 -0
- package/frontend/components/ui/sonner.tsx +40 -0
- package/frontend/components/ui/tabs.tsx +66 -0
- package/frontend/components/ui/textarea.tsx +18 -0
- package/frontend/components/wallet/ConnectButton.tsx +167 -0
- package/frontend/components/wallet/FaucetButton.tsx +256 -0
- package/frontend/components.json +22 -0
- package/frontend/eslint.config.mjs +18 -0
- package/frontend/hooks/useAICompletion.ts +75 -0
- package/frontend/hooks/useBlockchainLoader.ts +58 -0
- package/frontend/hooks/useChats.ts +137 -0
- package/frontend/hooks/useCompilation.ts +173 -0
- package/frontend/hooks/useFileTabs.ts +178 -0
- package/frontend/hooks/useGitHubLoader.ts +50 -0
- package/frontend/hooks/useKeyboardShortcuts.ts +47 -0
- package/frontend/hooks/usePanelState.ts +115 -0
- package/frontend/hooks/useProjectState.ts +276 -0
- package/frontend/hooks/useResponsive.ts +29 -0
- package/frontend/lib/abi-parser.ts +58 -0
- package/frontend/lib/blockchain-api.ts +374 -0
- package/frontend/lib/blockchain-explorers.ts +75 -0
- package/frontend/lib/blockchain-loader.ts +112 -0
- package/frontend/lib/cargo-template.ts +64 -0
- package/frontend/lib/compilation.ts +529 -0
- package/frontend/lib/constants.ts +31 -0
- package/frontend/lib/deployment.ts +176 -0
- package/frontend/lib/file-utils.ts +83 -0
- package/frontend/lib/github-api.ts +246 -0
- package/frontend/lib/github-loader.ts +369 -0
- package/frontend/lib/ml-contract-template.txt +900 -0
- package/frontend/lib/orbit-chains.ts +181 -0
- package/frontend/lib/output-formatter.ts +68 -0
- package/frontend/lib/project-manager.ts +632 -0
- package/frontend/lib/ray-tracing-abi.ts +206 -0
- package/frontend/lib/storage.ts +189 -0
- package/frontend/lib/templates.ts +1662 -0
- package/frontend/lib/url-parser.ts +188 -0
- package/frontend/lib/utils.ts +6 -0
- package/frontend/lib/wagmi-config.ts +24 -0
- package/frontend/next.config.ts +7 -0
- package/frontend/package-lock.json +16259 -0
- package/frontend/package.json +60 -0
- package/frontend/postcss.config.mjs +7 -0
- package/frontend/public/file.svg +1 -0
- package/frontend/public/globe.svg +1 -0
- package/frontend/public/ml-weights/.gitkeep +0 -0
- package/frontend/public/ml-weights/model.pkl +0 -0
- package/frontend/public/ml-weights/model_weights.json +27102 -0
- package/frontend/public/ml-weights/test_samples.json +7888 -0
- package/frontend/public/next.svg +1 -0
- package/frontend/public/vercel.svg +1 -0
- package/frontend/public/window.svg +1 -0
- package/frontend/scripts/check-env.js +52 -0
- package/frontend/scripts/setup.js +285 -0
- package/frontend/tailwind.config.ts +64 -0
- package/frontend/tsconfig.json +34 -0
- package/frontend/types/blockchain.ts +63 -0
- package/frontend/types/github.ts +54 -0
- package/frontend/types/project.ts +106 -0
- package/ml-training/README.md +56 -0
- package/ml-training/train_tiny_model.py +325 -0
- package/ml-training/update_template.py +59 -0
- package/package.json +30 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from '@/components/ui/dialog';
|
|
11
|
+
import { Button } from '@/components/ui/button';
|
|
12
|
+
import { Input } from '@/components/ui/input';
|
|
13
|
+
import { Label } from '@/components/ui/label';
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from '@/components/ui/select';
|
|
21
|
+
import { Loader2, ExternalLink, Copy, Check } from 'lucide-react';
|
|
22
|
+
import { useChainId } from 'wagmi';
|
|
23
|
+
import { arbitrum, arbitrumSepolia } from 'wagmi/chains';
|
|
24
|
+
import { orbitChains } from '@/lib/orbit-chains';
|
|
25
|
+
|
|
26
|
+
interface DeployDialogProps {
|
|
27
|
+
open: boolean;
|
|
28
|
+
onOpenChange: (open: boolean) => void;
|
|
29
|
+
sessionId: string | null;
|
|
30
|
+
onDeploySuccess?: (contractAddress: string, txHash?: string) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type RpcOption = { name: string; url: string };
|
|
34
|
+
|
|
35
|
+
// Arbitrum RPCs
|
|
36
|
+
const ARBITRUM_SEPOLIA_RPCS: RpcOption[] = [
|
|
37
|
+
{ name: 'Arbitrum Official', url: 'https://sepolia-rollup.arbitrum.io/rpc' },
|
|
38
|
+
// ⚠️ demo is rate-limited — replace with your own key
|
|
39
|
+
{ name: 'Alchemy (demo - rate limited)', url: 'https://arb-sepolia.g.alchemy.com/v2/demo' },
|
|
40
|
+
{ name: 'Chainstack', url: 'https://arbitrum-sepolia.core.chainstack.com/rpc/demo' },
|
|
41
|
+
{ name: 'Public Node', url: 'https://arbitrum-sepolia-rpc.publicnode.com' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const ARBITRUM_ONE_RPCS: RpcOption[] = [
|
|
45
|
+
{ name: 'Arbitrum Official', url: 'https://arb1.arbitrum.io/rpc' },
|
|
46
|
+
{ name: 'Alchemy (demo - rate limited)', url: 'https://arb-mainnet.g.alchemy.com/v2/demo' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Orbit RPCs (public/free where available)
|
|
50
|
+
const ORBIT_RPCS_BY_CHAIN_ID: Record<number, RpcOption[]> = {
|
|
51
|
+
// Xai Testnet v2 (37714555429)
|
|
52
|
+
37714555429: [
|
|
53
|
+
{ name: 'Xai Official', url: 'https://testnet-v2.xai-chain.net/rpc' },
|
|
54
|
+
{ name: 'Ankr (public)', url: 'https://rpc.ankr.com/xai_testnet' },
|
|
55
|
+
{ name: 'thirdweb (public)', url: 'https://37714555429.rpc.thirdweb.com' },
|
|
56
|
+
// Note: this endpoint may be rate-limited depending on provider policy
|
|
57
|
+
{ name: 'QuickNode (public)', url: 'https://xai-testnet.rpc.quicknode.com' },
|
|
58
|
+
],
|
|
59
|
+
|
|
60
|
+
// ApeChain Curtis Testnet (33111)
|
|
61
|
+
33111: [
|
|
62
|
+
{ name: 'Caldera (official)', url: 'https://curtis.rpc.caldera.xyz/http' },
|
|
63
|
+
{ name: 'ApeChain (Chainlist RPC)', url: 'https://rpc.curtis.apechain.com' },
|
|
64
|
+
{ name: 'dRPC (public)', url: 'https://apechain-curtis.drpc.org' },
|
|
65
|
+
{ name: 'thirdweb (public)', url: 'https://33111.rpc.thirdweb.com' },
|
|
66
|
+
// Keeping Tenderly as optional; if it fails for writes, use Caldera/ApeChain RPC
|
|
67
|
+
{ name: 'Tenderly Gateway (may be limited)', url: 'https://curtis.gateway.tenderly.co' },
|
|
68
|
+
],
|
|
69
|
+
|
|
70
|
+
// Nitrogen (Orbit Celestia) Testnet (96384675468)
|
|
71
|
+
96384675468: [
|
|
72
|
+
{ name: 'AltLayer (official)', url: 'https://nitrogen-rpc.altlayer.io' },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export function DeployDialog({
|
|
77
|
+
open,
|
|
78
|
+
onOpenChange,
|
|
79
|
+
sessionId,
|
|
80
|
+
onDeploySuccess,
|
|
81
|
+
}: DeployDialogProps) {
|
|
82
|
+
const [privateKey, setPrivateKey] = useState('');
|
|
83
|
+
const [selectedRpc, setSelectedRpc] = useState<string>('');
|
|
84
|
+
const [isDeploying, setIsDeploying] = useState(false);
|
|
85
|
+
const [deployResult, setDeployResult] = useState<{
|
|
86
|
+
contractAddress?: string;
|
|
87
|
+
txHash?: string;
|
|
88
|
+
error?: string;
|
|
89
|
+
} | null>(null);
|
|
90
|
+
const [copiedAddress, setCopiedAddress] = useState(false);
|
|
91
|
+
const [copiedTx, setCopiedTx] = useState(false);
|
|
92
|
+
|
|
93
|
+
const chainId = useChainId();
|
|
94
|
+
|
|
95
|
+
const orbitInfo = useMemo(() => orbitChains.find((c) => c.id === chainId), [chainId]);
|
|
96
|
+
|
|
97
|
+
const chainDisplayName = useMemo(() => {
|
|
98
|
+
if (chainId === arbitrumSepolia.id) return 'Arbitrum Sepolia';
|
|
99
|
+
if (chainId === arbitrum.id) return 'Arbitrum One';
|
|
100
|
+
return orbitInfo?.name ?? `Chain ${chainId}`;
|
|
101
|
+
}, [chainId, orbitInfo]);
|
|
102
|
+
|
|
103
|
+
const availableRpcs: RpcOption[] = useMemo(() => {
|
|
104
|
+
if (chainId === arbitrumSepolia.id) return ARBITRUM_SEPOLIA_RPCS;
|
|
105
|
+
if (chainId === arbitrum.id) return ARBITRUM_ONE_RPCS;
|
|
106
|
+
|
|
107
|
+
// Orbit chain specific list
|
|
108
|
+
const orbitRpcs = ORBIT_RPCS_BY_CHAIN_ID[chainId];
|
|
109
|
+
if (orbitRpcs?.length) return orbitRpcs;
|
|
110
|
+
|
|
111
|
+
// Fallback: if chain is in orbitChains, at least show its default rpcUrls
|
|
112
|
+
if (orbitInfo?.chain?.rpcUrls?.default?.http?.length) {
|
|
113
|
+
return orbitInfo.chain.rpcUrls.default.http.map((url, idx) => ({
|
|
114
|
+
name: idx === 0 ? 'Default (from chain config)' : `RPC ${idx + 1}`,
|
|
115
|
+
url,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return [];
|
|
120
|
+
}, [chainId, orbitInfo]);
|
|
121
|
+
|
|
122
|
+
// ✅ Set default RPC safely (no setState during render)
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
setSelectedRpc((prev) => {
|
|
125
|
+
if (prev && availableRpcs.some((r) => r.url === prev)) return prev;
|
|
126
|
+
return availableRpcs[0]?.url ?? '';
|
|
127
|
+
});
|
|
128
|
+
}, [availableRpcs]);
|
|
129
|
+
|
|
130
|
+
const explorerBase = useMemo(() => {
|
|
131
|
+
if (chainId === arbitrumSepolia.id) return 'https://sepolia.arbiscan.io';
|
|
132
|
+
if (chainId === arbitrum.id) return 'https://arbiscan.io';
|
|
133
|
+
|
|
134
|
+
const url = orbitInfo?.chain?.blockExplorers?.default?.url;
|
|
135
|
+
return url ? url.replace(/\/$/, '') : null;
|
|
136
|
+
}, [chainId, orbitInfo]);
|
|
137
|
+
|
|
138
|
+
const getExplorerUrl = (address: string) => {
|
|
139
|
+
const base = explorerBase ?? 'https://sepolia.arbiscan.io';
|
|
140
|
+
return `${base}/address/${address}`;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const getTxExplorerUrl = (txHash: string) => {
|
|
144
|
+
const base = explorerBase ?? 'https://sepolia.arbiscan.io';
|
|
145
|
+
return `${base}/tx/${txHash}`;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const reset = () => {
|
|
149
|
+
setPrivateKey('');
|
|
150
|
+
setDeployResult(null);
|
|
151
|
+
setCopiedAddress(false);
|
|
152
|
+
setCopiedTx(false);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ✅ Dialog expects (open:boolean). Reset only on close.
|
|
156
|
+
const handleOpenChange = (nextOpen: boolean) => {
|
|
157
|
+
if (!nextOpen) reset();
|
|
158
|
+
onOpenChange(nextOpen);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const handleDeploy = async () => {
|
|
162
|
+
if (!sessionId || !privateKey || !selectedRpc) {
|
|
163
|
+
alert('Please provide a private key and select an RPC endpoint');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setIsDeploying(true);
|
|
168
|
+
setDeployResult(null);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const response = await fetch('/api/deploy', {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
body: JSON.stringify({ sessionId, privateKey, rpcUrl: selectedRpc }),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const result = await response.json();
|
|
178
|
+
|
|
179
|
+
if (result.success && result.contractAddress) {
|
|
180
|
+
setDeployResult({ contractAddress: result.contractAddress, txHash: result.txHash });
|
|
181
|
+
onDeploySuccess?.(result.contractAddress, result.txHash);
|
|
182
|
+
} else {
|
|
183
|
+
setDeployResult({ error: result.error || 'Deployment failed' });
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
setDeployResult({ error: error instanceof Error ? error.message : 'Deployment failed' });
|
|
187
|
+
} finally {
|
|
188
|
+
setIsDeploying(false);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const copyToClipboard = async (text: string, type: 'address' | 'tx') => {
|
|
193
|
+
await navigator.clipboard.writeText(text);
|
|
194
|
+
if (type === 'address') {
|
|
195
|
+
setCopiedAddress(true);
|
|
196
|
+
setTimeout(() => setCopiedAddress(false), 2000);
|
|
197
|
+
} else {
|
|
198
|
+
setCopiedTx(true);
|
|
199
|
+
setTimeout(() => setCopiedTx(false), 2000);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
205
|
+
<DialogContent className="sm:max-w-md">
|
|
206
|
+
<DialogHeader>
|
|
207
|
+
<DialogTitle>Deploy Contract</DialogTitle>
|
|
208
|
+
<DialogDescription>
|
|
209
|
+
Deploy your compiled contract to <strong>{chainDisplayName}</strong>
|
|
210
|
+
</DialogDescription>
|
|
211
|
+
</DialogHeader>
|
|
212
|
+
|
|
213
|
+
{!deployResult ? (
|
|
214
|
+
<div className="space-y-4">
|
|
215
|
+
<div className="space-y-2">
|
|
216
|
+
<Label htmlFor="rpc-select">RPC Endpoint</Label>
|
|
217
|
+
<Select value={selectedRpc} onValueChange={setSelectedRpc} disabled={availableRpcs.length === 0}>
|
|
218
|
+
<SelectTrigger>
|
|
219
|
+
<SelectValue placeholder={availableRpcs.length ? 'Select RPC endpoint' : 'No RPCs available'} />
|
|
220
|
+
</SelectTrigger>
|
|
221
|
+
<SelectContent>
|
|
222
|
+
{availableRpcs.map((rpc) => (
|
|
223
|
+
<SelectItem key={rpc.url} value={rpc.url}>
|
|
224
|
+
{rpc.name}
|
|
225
|
+
</SelectItem>
|
|
226
|
+
))}
|
|
227
|
+
</SelectContent>
|
|
228
|
+
</Select>
|
|
229
|
+
|
|
230
|
+
<p className="text-xs text-muted-foreground">
|
|
231
|
+
Tip: If deployment fails, switch RPC and retry (some public RPCs can rate-limit writes).
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div className="space-y-2">
|
|
236
|
+
<Label htmlFor="private-key">Private Key</Label>
|
|
237
|
+
<Input
|
|
238
|
+
id="private-key"
|
|
239
|
+
type="password"
|
|
240
|
+
placeholder="0x..."
|
|
241
|
+
value={privateKey}
|
|
242
|
+
onChange={(e) => setPrivateKey(e.target.value)}
|
|
243
|
+
disabled={isDeploying}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<Button
|
|
248
|
+
onClick={handleDeploy}
|
|
249
|
+
disabled={isDeploying || !privateKey || !selectedRpc || !sessionId}
|
|
250
|
+
className="w-full"
|
|
251
|
+
>
|
|
252
|
+
{isDeploying ? (
|
|
253
|
+
<>
|
|
254
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
255
|
+
Deploying...
|
|
256
|
+
</>
|
|
257
|
+
) : (
|
|
258
|
+
'Deploy Contract'
|
|
259
|
+
)}
|
|
260
|
+
</Button>
|
|
261
|
+
</div>
|
|
262
|
+
) : deployResult.error ? (
|
|
263
|
+
<div className="space-y-4">
|
|
264
|
+
<div className="bg-destructive/10 border border-destructive/20 p-4 rounded-md">
|
|
265
|
+
<p className="text-sm text-destructive font-medium mb-2">Deployment Failed</p>
|
|
266
|
+
<p className="text-xs text-destructive/80 whitespace-pre-wrap wrap-break-words">
|
|
267
|
+
{deployResult.error}
|
|
268
|
+
</p>
|
|
269
|
+
</div>
|
|
270
|
+
<Button onClick={() => setDeployResult(null)} variant="outline" className="w-full">
|
|
271
|
+
Try Again
|
|
272
|
+
</Button>
|
|
273
|
+
</div>
|
|
274
|
+
) : (
|
|
275
|
+
<div className="space-y-4">
|
|
276
|
+
<div className="bg-green-500/10 border border-green-500/20 p-4 rounded-md space-y-3">
|
|
277
|
+
<p className="text-sm text-green-500 font-medium">✓ Deployment Successful!</p>
|
|
278
|
+
|
|
279
|
+
{deployResult.contractAddress && (
|
|
280
|
+
<div className="space-y-2">
|
|
281
|
+
<Label className="text-xs">Contract Address</Label>
|
|
282
|
+
<div className="flex items-center gap-2">
|
|
283
|
+
<code className="flex-1 text-xs bg-muted p-2 rounded font-mono break-all">
|
|
284
|
+
{deployResult.contractAddress}
|
|
285
|
+
</code>
|
|
286
|
+
<Button
|
|
287
|
+
size="sm"
|
|
288
|
+
variant="ghost"
|
|
289
|
+
onClick={() => copyToClipboard(deployResult.contractAddress!, 'address')}
|
|
290
|
+
>
|
|
291
|
+
{copiedAddress ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
|
292
|
+
</Button>
|
|
293
|
+
<Button size="sm" variant="ghost" asChild>
|
|
294
|
+
<a href={getExplorerUrl(deployResult.contractAddress)} target="_blank" rel="noopener noreferrer">
|
|
295
|
+
<ExternalLink className="h-4 w-4" />
|
|
296
|
+
</a>
|
|
297
|
+
</Button>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{deployResult.txHash && (
|
|
303
|
+
<div className="space-y-2">
|
|
304
|
+
<Label className="text-xs">Transaction Hash</Label>
|
|
305
|
+
<div className="flex items-center gap-2">
|
|
306
|
+
<code className="flex-1 text-xs bg-muted p-2 rounded font-mono break-all">
|
|
307
|
+
{deployResult.txHash}
|
|
308
|
+
</code>
|
|
309
|
+
<Button
|
|
310
|
+
size="sm"
|
|
311
|
+
variant="ghost"
|
|
312
|
+
onClick={() => copyToClipboard(deployResult.txHash!, 'tx')}
|
|
313
|
+
>
|
|
314
|
+
{copiedTx ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
|
315
|
+
</Button>
|
|
316
|
+
<Button size="sm" variant="ghost" asChild>
|
|
317
|
+
<a href={getTxExplorerUrl(deployResult.txHash)} target="_blank" rel="noopener noreferrer">
|
|
318
|
+
<ExternalLink className="h-4 w-4" />
|
|
319
|
+
</a>
|
|
320
|
+
</Button>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<Button onClick={() => handleOpenChange(false)} className="w-full">
|
|
327
|
+
Done
|
|
328
|
+
</Button>
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
</DialogContent>
|
|
332
|
+
</Dialog>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { X, Plus, FileCode, Edit2, MoreHorizontal } from 'lucide-react';
|
|
5
|
+
import type { FileTab } from '@/hooks/useFileTabs';
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuTrigger,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
} from '@/components/ui/dropdown-menu';
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
|
|
15
|
+
interface FileTabsProps {
|
|
16
|
+
tabs: FileTab[];
|
|
17
|
+
activeTabId: string | null;
|
|
18
|
+
onTabClick: (id: string) => void;
|
|
19
|
+
onTabClose: (id: string) => void;
|
|
20
|
+
onNewFile: (type: 'rust' | 'toml' | 'markdown') => void;
|
|
21
|
+
onRenameTab?: (id: string, newName: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function FileTabs({
|
|
25
|
+
tabs,
|
|
26
|
+
activeTabId,
|
|
27
|
+
onTabClick,
|
|
28
|
+
onTabClose,
|
|
29
|
+
onNewFile,
|
|
30
|
+
onRenameTab,
|
|
31
|
+
}: FileTabsProps) {
|
|
32
|
+
const [renamingTabId, setRenamingTabId] = useState<string | null>(null);
|
|
33
|
+
const [renameValue, setRenameValue] = useState('');
|
|
34
|
+
|
|
35
|
+
const handleRename = (tabId: string, currentName: string) => {
|
|
36
|
+
setRenamingTabId(tabId);
|
|
37
|
+
setRenameValue(currentName);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleRenameSubmit = (tabId: string) => {
|
|
41
|
+
if (onRenameTab && renameValue.trim() && renameValue !== tabs.find(t => t.id === tabId)?.name) {
|
|
42
|
+
onRenameTab(tabId, renameValue.trim());
|
|
43
|
+
}
|
|
44
|
+
setRenamingTabId(null);
|
|
45
|
+
setRenameValue('');
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleRenameCancel = () => {
|
|
49
|
+
setRenamingTabId(null);
|
|
50
|
+
setRenameValue('');
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const isRenameable = (tab: FileTab) => {
|
|
54
|
+
// Allow renaming for new files (not the main lib.rs)
|
|
55
|
+
return tab.id !== 'main' && onRenameTab;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="h-12 border-b border-border flex items-center gap-1 px-2 overflow-x-auto">
|
|
60
|
+
{tabs.map((tab) => (
|
|
61
|
+
<div
|
|
62
|
+
key={tab.id}
|
|
63
|
+
className={`
|
|
64
|
+
flex items-center gap-2 px-3 py-1.5 rounded-t-md
|
|
65
|
+
transition-colors group relative
|
|
66
|
+
${activeTabId === tab.id
|
|
67
|
+
? 'bg-card text-foreground'
|
|
68
|
+
: 'bg-muted/50 text-muted-foreground hover:bg-muted'
|
|
69
|
+
}
|
|
70
|
+
`}
|
|
71
|
+
>
|
|
72
|
+
<div
|
|
73
|
+
className="flex items-center gap-1.5 cursor-pointer"
|
|
74
|
+
onClick={() => onTabClick(tab.id)}
|
|
75
|
+
>
|
|
76
|
+
<FileCode className="h-3 w-3" />
|
|
77
|
+
{renamingTabId === tab.id ? (
|
|
78
|
+
<input
|
|
79
|
+
type="text"
|
|
80
|
+
value={renameValue}
|
|
81
|
+
onChange={(e) => setRenameValue(e.target.value)}
|
|
82
|
+
onBlur={() => handleRenameSubmit(tab.id)}
|
|
83
|
+
onKeyDown={(e) => {
|
|
84
|
+
if (e.key === 'Enter') {
|
|
85
|
+
handleRenameSubmit(tab.id);
|
|
86
|
+
} else if (e.key === 'Escape') {
|
|
87
|
+
handleRenameCancel();
|
|
88
|
+
}
|
|
89
|
+
}}
|
|
90
|
+
className="text-sm bg-transparent border-none outline-none focus:ring-1 focus:ring-primary rounded px-1 min-w-0 w-20"
|
|
91
|
+
autoFocus
|
|
92
|
+
onClick={(e) => e.stopPropagation()}
|
|
93
|
+
/>
|
|
94
|
+
) : (
|
|
95
|
+
<span className="text-sm whitespace-nowrap">
|
|
96
|
+
{tab.name}
|
|
97
|
+
{tab.isModified && <span className="text-blue-400 ml-1">•</span>}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100">
|
|
103
|
+
{isRenameable(tab) && renamingTabId !== tab.id && (
|
|
104
|
+
<DropdownMenu>
|
|
105
|
+
<DropdownMenuTrigger asChild>
|
|
106
|
+
<Button
|
|
107
|
+
variant="ghost"
|
|
108
|
+
size="sm"
|
|
109
|
+
className="h-5 w-5 p-0"
|
|
110
|
+
onClick={(e) => e.stopPropagation()}
|
|
111
|
+
>
|
|
112
|
+
<MoreHorizontal className="h-3 w-3" />
|
|
113
|
+
</Button>
|
|
114
|
+
</DropdownMenuTrigger>
|
|
115
|
+
<DropdownMenuContent align="end">
|
|
116
|
+
<DropdownMenuItem onClick={() => handleRename(tab.id, tab.name)}>
|
|
117
|
+
<Edit2 className="h-3 w-3 mr-2" />
|
|
118
|
+
Rename
|
|
119
|
+
</DropdownMenuItem>
|
|
120
|
+
{tabs.length > 1 && (
|
|
121
|
+
<>
|
|
122
|
+
<DropdownMenuSeparator />
|
|
123
|
+
<DropdownMenuItem
|
|
124
|
+
onClick={() => onTabClose(tab.id)}
|
|
125
|
+
className="text-destructive focus:text-destructive"
|
|
126
|
+
>
|
|
127
|
+
<X className="h-3 w-3 mr-2" />
|
|
128
|
+
Close
|
|
129
|
+
</DropdownMenuItem>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
</DropdownMenuContent>
|
|
133
|
+
</DropdownMenu>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{tabs.length > 1 && !isRenameable(tab) && (
|
|
137
|
+
<Button
|
|
138
|
+
variant="ghost"
|
|
139
|
+
size="sm"
|
|
140
|
+
className="h-5 w-5 p-0"
|
|
141
|
+
onClick={(e) => {
|
|
142
|
+
e.stopPropagation();
|
|
143
|
+
onTabClose(tab.id);
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<X className="h-3 w-3" />
|
|
147
|
+
</Button>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
))}
|
|
152
|
+
|
|
153
|
+
{/* New File Button */}
|
|
154
|
+
<DropdownMenu>
|
|
155
|
+
<DropdownMenuTrigger asChild>
|
|
156
|
+
<Button
|
|
157
|
+
variant="ghost"
|
|
158
|
+
size="sm"
|
|
159
|
+
className="h-8 w-8 p-0 ml-2"
|
|
160
|
+
>
|
|
161
|
+
<Plus className="h-4 w-4" />
|
|
162
|
+
</Button>
|
|
163
|
+
</DropdownMenuTrigger>
|
|
164
|
+
<DropdownMenuContent>
|
|
165
|
+
<DropdownMenuItem onClick={() => onNewFile('rust')}>
|
|
166
|
+
<FileCode className="h-4 w-4 mr-2" />
|
|
167
|
+
New Rust File
|
|
168
|
+
</DropdownMenuItem>
|
|
169
|
+
<DropdownMenuItem onClick={() => onNewFile('toml')}>
|
|
170
|
+
<FileCode className="h-4 w-4 mr-2" />
|
|
171
|
+
New TOML File
|
|
172
|
+
</DropdownMenuItem>
|
|
173
|
+
<DropdownMenuItem onClick={() => onNewFile('markdown')}>
|
|
174
|
+
<FileCode className="h-4 w-4 mr-2" />
|
|
175
|
+
New Markdown File
|
|
176
|
+
</DropdownMenuItem>
|
|
177
|
+
</DropdownMenuContent>
|
|
178
|
+
</DropdownMenu>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|