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.
Files changed (135) hide show
  1. package/Readme.MD +1515 -0
  2. package/cli.js +28 -0
  3. package/frontend/.vscode/settings.json +9 -0
  4. package/frontend/app/api/chat/route.ts +101 -0
  5. package/frontend/app/api/check-setup/route.ts +93 -0
  6. package/frontend/app/api/cleanup/route.ts +14 -0
  7. package/frontend/app/api/compile/route.ts +95 -0
  8. package/frontend/app/api/compile-stream/route.ts +98 -0
  9. package/frontend/app/api/complete/route.ts +86 -0
  10. package/frontend/app/api/deploy/route.ts +118 -0
  11. package/frontend/app/api/export-abi/route.ts +58 -0
  12. package/frontend/app/favicon.ico +0 -0
  13. package/frontend/app/globals.css +177 -0
  14. package/frontend/app/layout.tsx +29 -0
  15. package/frontend/app/ml/page.tsx +694 -0
  16. package/frontend/app/page.tsx +1132 -0
  17. package/frontend/app/providers.tsx +18 -0
  18. package/frontend/app/qlearning/page.tsx +188 -0
  19. package/frontend/app/raytracing/page.tsx +268 -0
  20. package/frontend/components/abi/ABIDialog.tsx +132 -0
  21. package/frontend/components/ai/AICompletionPopup.tsx +76 -0
  22. package/frontend/components/ai/ChatPanel.tsx +292 -0
  23. package/frontend/components/ai/QuickActions.tsx +128 -0
  24. package/frontend/components/blockchain/BlockchainContractBanner.tsx +64 -0
  25. package/frontend/components/blockchain/BlockchainLoadingDialog.tsx +188 -0
  26. package/frontend/components/deploy/DeployDialog.tsx +334 -0
  27. package/frontend/components/editor/FileTabs.tsx +181 -0
  28. package/frontend/components/editor/MonacoEditor.tsx +306 -0
  29. package/frontend/components/file-tree/ContextMenu.tsx +110 -0
  30. package/frontend/components/file-tree/DeleteConfirmDialog.tsx +61 -0
  31. package/frontend/components/file-tree/FileInputDialog.tsx +97 -0
  32. package/frontend/components/file-tree/FileNode.tsx +60 -0
  33. package/frontend/components/file-tree/FileTree.tsx +259 -0
  34. package/frontend/components/file-tree/FileTreeSkeleton.tsx +26 -0
  35. package/frontend/components/file-tree/FolderNode.tsx +105 -0
  36. package/frontend/components/github/GitHubLoadingDialog.tsx +201 -0
  37. package/frontend/components/github/GitHubMetadataBanner.tsx +61 -0
  38. package/frontend/components/github/LoadFromGitHubDialog.tsx +125 -0
  39. package/frontend/components/github/URLCopyButton.tsx +60 -0
  40. package/frontend/components/interact/ContractInteraction.tsx +323 -0
  41. package/frontend/components/interact/ContractPlaceholder.tsx +41 -0
  42. package/frontend/components/orbit/BenchmarkDialog.tsx +342 -0
  43. package/frontend/components/orbit/OrbitExplorer.tsx +273 -0
  44. package/frontend/components/project/ProjectActions.tsx +176 -0
  45. package/frontend/components/q-learning/ContractConfig.tsx +172 -0
  46. package/frontend/components/q-learning/MazeGrid.tsx +346 -0
  47. package/frontend/components/q-learning/PathAnimation.tsx +384 -0
  48. package/frontend/components/q-learning/QTableHeatmap.tsx +300 -0
  49. package/frontend/components/q-learning/TrainingForm.tsx +349 -0
  50. package/frontend/components/ray-tracing/ContractConfig.tsx +245 -0
  51. package/frontend/components/ray-tracing/MintingForm.tsx +280 -0
  52. package/frontend/components/ray-tracing/RenderCanvas.tsx +228 -0
  53. package/frontend/components/ray-tracing/RenderingPanel.tsx +259 -0
  54. package/frontend/components/ray-tracing/StyleControls.tsx +217 -0
  55. package/frontend/components/setup/SetupGuide.tsx +290 -0
  56. package/frontend/components/ui/KeyboardShortcutHint.tsx +74 -0
  57. package/frontend/components/ui/alert-dialog.tsx +157 -0
  58. package/frontend/components/ui/alert.tsx +66 -0
  59. package/frontend/components/ui/badge.tsx +46 -0
  60. package/frontend/components/ui/button.tsx +62 -0
  61. package/frontend/components/ui/card.tsx +92 -0
  62. package/frontend/components/ui/context-menu.tsx +252 -0
  63. package/frontend/components/ui/dialog.tsx +143 -0
  64. package/frontend/components/ui/dropdown-menu.tsx +257 -0
  65. package/frontend/components/ui/input.tsx +21 -0
  66. package/frontend/components/ui/label.tsx +24 -0
  67. package/frontend/components/ui/progress.tsx +31 -0
  68. package/frontend/components/ui/scroll-area.tsx +58 -0
  69. package/frontend/components/ui/select.tsx +190 -0
  70. package/frontend/components/ui/separator.tsx +28 -0
  71. package/frontend/components/ui/sheet.tsx +139 -0
  72. package/frontend/components/ui/skeleton.tsx +13 -0
  73. package/frontend/components/ui/slider.tsx +63 -0
  74. package/frontend/components/ui/sonner.tsx +40 -0
  75. package/frontend/components/ui/tabs.tsx +66 -0
  76. package/frontend/components/ui/textarea.tsx +18 -0
  77. package/frontend/components/wallet/ConnectButton.tsx +167 -0
  78. package/frontend/components/wallet/FaucetButton.tsx +256 -0
  79. package/frontend/components.json +22 -0
  80. package/frontend/eslint.config.mjs +18 -0
  81. package/frontend/hooks/useAICompletion.ts +75 -0
  82. package/frontend/hooks/useBlockchainLoader.ts +58 -0
  83. package/frontend/hooks/useChats.ts +137 -0
  84. package/frontend/hooks/useCompilation.ts +173 -0
  85. package/frontend/hooks/useFileTabs.ts +178 -0
  86. package/frontend/hooks/useGitHubLoader.ts +50 -0
  87. package/frontend/hooks/useKeyboardShortcuts.ts +47 -0
  88. package/frontend/hooks/usePanelState.ts +115 -0
  89. package/frontend/hooks/useProjectState.ts +276 -0
  90. package/frontend/hooks/useResponsive.ts +29 -0
  91. package/frontend/lib/abi-parser.ts +58 -0
  92. package/frontend/lib/blockchain-api.ts +374 -0
  93. package/frontend/lib/blockchain-explorers.ts +75 -0
  94. package/frontend/lib/blockchain-loader.ts +112 -0
  95. package/frontend/lib/cargo-template.ts +64 -0
  96. package/frontend/lib/compilation.ts +529 -0
  97. package/frontend/lib/constants.ts +31 -0
  98. package/frontend/lib/deployment.ts +176 -0
  99. package/frontend/lib/file-utils.ts +83 -0
  100. package/frontend/lib/github-api.ts +246 -0
  101. package/frontend/lib/github-loader.ts +369 -0
  102. package/frontend/lib/ml-contract-template.txt +900 -0
  103. package/frontend/lib/orbit-chains.ts +181 -0
  104. package/frontend/lib/output-formatter.ts +68 -0
  105. package/frontend/lib/project-manager.ts +632 -0
  106. package/frontend/lib/ray-tracing-abi.ts +206 -0
  107. package/frontend/lib/storage.ts +189 -0
  108. package/frontend/lib/templates.ts +1662 -0
  109. package/frontend/lib/url-parser.ts +188 -0
  110. package/frontend/lib/utils.ts +6 -0
  111. package/frontend/lib/wagmi-config.ts +24 -0
  112. package/frontend/next.config.ts +7 -0
  113. package/frontend/package-lock.json +16259 -0
  114. package/frontend/package.json +60 -0
  115. package/frontend/postcss.config.mjs +7 -0
  116. package/frontend/public/file.svg +1 -0
  117. package/frontend/public/globe.svg +1 -0
  118. package/frontend/public/ml-weights/.gitkeep +0 -0
  119. package/frontend/public/ml-weights/model.pkl +0 -0
  120. package/frontend/public/ml-weights/model_weights.json +27102 -0
  121. package/frontend/public/ml-weights/test_samples.json +7888 -0
  122. package/frontend/public/next.svg +1 -0
  123. package/frontend/public/vercel.svg +1 -0
  124. package/frontend/public/window.svg +1 -0
  125. package/frontend/scripts/check-env.js +52 -0
  126. package/frontend/scripts/setup.js +285 -0
  127. package/frontend/tailwind.config.ts +64 -0
  128. package/frontend/tsconfig.json +34 -0
  129. package/frontend/types/blockchain.ts +63 -0
  130. package/frontend/types/github.ts +54 -0
  131. package/frontend/types/project.ts +106 -0
  132. package/ml-training/README.md +56 -0
  133. package/ml-training/train_tiny_model.py +325 -0
  134. package/ml-training/update_template.py +59 -0
  135. package/package.json +30 -0
@@ -0,0 +1,176 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef } from 'react';
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuTrigger,
10
+ } from '@/components/ui/dropdown-menu';
11
+ import {
12
+ AlertDialog,
13
+ AlertDialogAction,
14
+ AlertDialogCancel,
15
+ AlertDialogContent,
16
+ AlertDialogDescription,
17
+ AlertDialogFooter,
18
+ AlertDialogHeader,
19
+ AlertDialogTitle,
20
+ } from '@/components/ui/alert-dialog';
21
+ import { Button } from '@/components/ui/button';
22
+ import {
23
+ FileDown,
24
+ FileUp,
25
+ Save,
26
+ RotateCcw,
27
+ FolderOpen,
28
+ } from 'lucide-react';
29
+ import { ProjectState } from '@/types/project';
30
+ import { exportProject, importProject, getLastSaveTime } from '@/lib/storage';
31
+
32
+ interface ProjectActionsProps {
33
+ project: ProjectState;
34
+ onImport: (project: ProjectState) => void;
35
+ onReset: () => void;
36
+ onSave: () => void;
37
+ }
38
+
39
+ export function ProjectActions({
40
+ project,
41
+ onImport,
42
+ onReset,
43
+ onSave,
44
+ }: ProjectActionsProps) {
45
+ const [showResetDialog, setShowResetDialog] = useState(false);
46
+ const fileInputRef = useRef<HTMLInputElement>(null);
47
+ const lastSave = getLastSaveTime();
48
+
49
+ const handleExport = () => {
50
+ exportProject(project);
51
+ };
52
+
53
+ const handleImportClick = () => {
54
+ fileInputRef.current?.click();
55
+ };
56
+
57
+ const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
58
+ const file = e.target.files?.[0];
59
+ if (!file) return;
60
+
61
+ try {
62
+ const importedProject = await importProject(file);
63
+ onImport(importedProject);
64
+
65
+ // Reset input
66
+ if (fileInputRef.current) {
67
+ fileInputRef.current.value = '';
68
+ }
69
+ } catch (error) {
70
+ alert(error instanceof Error ? error.message : 'Failed to import project');
71
+ }
72
+ };
73
+
74
+ const formatLastSave = () => {
75
+ if (!lastSave) return 'Never';
76
+
77
+ const now = new Date();
78
+ const diff = now.getTime() - lastSave.getTime();
79
+ const seconds = Math.floor(diff / 1000);
80
+ const minutes = Math.floor(seconds / 60);
81
+ const hours = Math.floor(minutes / 60);
82
+
83
+ if (seconds < 60) return 'Just now';
84
+ if (minutes < 60) return `${minutes}m ago`;
85
+ if (hours < 24) return `${hours}h ago`;
86
+ return lastSave.toLocaleDateString();
87
+ };
88
+
89
+ return (
90
+ <>
91
+ <DropdownMenu>
92
+ <DropdownMenuTrigger asChild>
93
+ <Button variant="ghost" size="sm" className="gap-2">
94
+ <FolderOpen className="h-4 w-4" />
95
+ <span className="hidden md:inline">Project</span>
96
+ </Button>
97
+ </DropdownMenuTrigger>
98
+
99
+ <DropdownMenuContent align="end" className="w-56">
100
+ {/* <div className="px-2 py-1.5 text-xs text-muted-foreground">
101
+ {project.name}
102
+ </div> */}
103
+ {/* <div className="px-2 pb-2 text-xs text-muted-foreground">
104
+ Last saved: {formatLastSave()}
105
+ </div> */}
106
+
107
+ {/* <DropdownMenuSeparator /> */}
108
+
109
+ <DropdownMenuItem onClick={onSave} className="cursor-pointer">
110
+ <Save className="h-4 w-4 mr-2" />
111
+ Save Now
112
+ </DropdownMenuItem>
113
+
114
+ <DropdownMenuItem onClick={handleExport} className="cursor-pointer">
115
+ <FileDown className="h-4 w-4 mr-2" />
116
+ Export Project
117
+ </DropdownMenuItem>
118
+
119
+ <DropdownMenuItem onClick={handleImportClick} className="cursor-pointer">
120
+ <FileUp className="h-4 w-4 mr-2" />
121
+ Import Project
122
+ </DropdownMenuItem>
123
+
124
+ <DropdownMenuSeparator />
125
+
126
+ <DropdownMenuItem
127
+ onClick={() => setShowResetDialog(true)}
128
+ className="cursor-pointer text-red-600 focus:text-red-600"
129
+ >
130
+ <RotateCcw className="h-4 w-4 mr-2" />
131
+ Reset Project
132
+ </DropdownMenuItem>
133
+ </DropdownMenuContent>
134
+ </DropdownMenu>
135
+
136
+ {/* Hidden file input */}
137
+ <input
138
+ ref={fileInputRef}
139
+ type="file"
140
+ accept=".json"
141
+ onChange={handleFileSelect}
142
+ className="hidden"
143
+ />
144
+
145
+ {/* Reset confirmation dialog */}
146
+ <AlertDialog open={showResetDialog} onOpenChange={setShowResetDialog}>
147
+ <AlertDialogContent>
148
+ <AlertDialogHeader>
149
+ <AlertDialogTitle>Reset Project?</AlertDialogTitle>
150
+ <AlertDialogDescription>
151
+ This will delete all files and reset to a new empty project.
152
+ <br />
153
+ <br />
154
+ <strong>This action cannot be undone.</strong>
155
+ <br />
156
+ Consider exporting your project first.
157
+ </AlertDialogDescription>
158
+ </AlertDialogHeader>
159
+
160
+ <AlertDialogFooter>
161
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
162
+ <AlertDialogAction
163
+ onClick={() => {
164
+ onReset();
165
+ setShowResetDialog(false);
166
+ }}
167
+ className="bg-red-600 hover:bg-red-700"
168
+ >
169
+ Reset Project
170
+ </AlertDialogAction>
171
+ </AlertDialogFooter>
172
+ </AlertDialogContent>
173
+ </AlertDialog>
174
+ </>
175
+ );
176
+ }
@@ -0,0 +1,172 @@
1
+ //contractconfig.tsx
2
+ 'use client';
3
+
4
+ import { useState } from 'react';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
7
+ import { Input } from '@/components/ui/input';
8
+ import { Label } from '@/components/ui/label';
9
+ import { CheckCircle2, XCircle, Loader2 } from 'lucide-react';
10
+ import { usePublicClient } from 'wagmi';
11
+ import { arbitrumSepolia } from 'wagmi/chains';
12
+ import { parseAbi } from 'viem';
13
+
14
+ const QLEARNING_ABI = parseAbi([
15
+ 'function getMazeConfig() external view returns (uint256, uint256, uint256, uint256, uint256, uint256)',
16
+ 'function getTrainingInfo() external view returns (bool, uint256)',
17
+ ]);
18
+
19
+ interface ContractConfigProps {
20
+ // Input field value
21
+ contractAddress: string;
22
+ setContractAddress: (address: string) => void;
23
+
24
+ // Connected status (UI)
25
+ isConnected: boolean;
26
+ setIsConnected: (connected: boolean) => void;
27
+
28
+ // ✅ NEW: connected address used by readers (stable, only changes when Connect succeeds)
29
+ setConnectedContractAddress: (address: string) => void;
30
+
31
+ // Maze + trained status pushed to parent
32
+ setMazeConfig: (config: any) => void;
33
+ setIsTrained: (trained: boolean) => void;
34
+ }
35
+
36
+ export function ContractConfig({
37
+ contractAddress,
38
+ setContractAddress,
39
+ isConnected,
40
+ setIsConnected,
41
+ setConnectedContractAddress,
42
+ setMazeConfig,
43
+ setIsTrained,
44
+ }: ContractConfigProps) {
45
+ const [isLoading, setIsLoading] = useState(false);
46
+ const [trainingInfo, setTrainingInfo] = useState<{ trained: boolean; episodes: number } | null>(null);
47
+ const publicClient = usePublicClient({ chainId: arbitrumSepolia.id });
48
+
49
+ const handleConnect = async () => {
50
+ const addr = contractAddress.trim();
51
+
52
+ if (!addr || !publicClient) {
53
+ alert('Please enter a contract address');
54
+ return;
55
+ }
56
+
57
+ setIsLoading(true);
58
+
59
+ try {
60
+ // Load maze configuration
61
+ const config = await publicClient.readContract({
62
+ address: addr as `0x${string}`,
63
+ abi: QLEARNING_ABI,
64
+ functionName: 'getMazeConfig',
65
+ });
66
+
67
+ setMazeConfig({
68
+ size: Number(config[0]),
69
+ numActions: Number(config[1]),
70
+ startRow: Number(config[2]),
71
+ startCol: Number(config[3]),
72
+ goalRow: Number(config[4]),
73
+ goalCol: Number(config[5]),
74
+ });
75
+
76
+ // Load training info
77
+ const training = await publicClient.readContract({
78
+ address: addr as `0x${string}`,
79
+ abi: QLEARNING_ABI,
80
+ functionName: 'getTrainingInfo',
81
+ });
82
+
83
+ const trained = Boolean(training[0]);
84
+ const episodes = Number(training[1]);
85
+
86
+ setTrainingInfo({ trained, episodes });
87
+
88
+ // ✅ Push trained status to parent so other widgets don’t flicker/mismatch
89
+ setIsTrained(trained);
90
+
91
+ // ✅ IMPORTANT: “Connected address” is locked-in and used by MazeGrid/reads
92
+ setConnectedContractAddress(addr);
93
+
94
+ setIsConnected(true);
95
+ } catch (error) {
96
+ console.error('Connection failed:', error);
97
+ alert('Failed to connect to contract. Please check the address.');
98
+ setIsConnected(false);
99
+ setConnectedContractAddress('');
100
+ setIsTrained(false);
101
+ setTrainingInfo(null);
102
+ } finally {
103
+ setIsLoading(false);
104
+ }
105
+ };
106
+
107
+ return (
108
+ <Card>
109
+ <CardHeader>
110
+ <CardTitle>Contract Configuration</CardTitle>
111
+ <CardDescription>Connect to your deployed Q-Learning contract</CardDescription>
112
+ </CardHeader>
113
+ <CardContent className="space-y-4">
114
+ <div className="space-y-2">
115
+ <Label htmlFor="contract-address">Contract Address</Label>
116
+ <Input
117
+ id="contract-address"
118
+ placeholder="0x..."
119
+ value={contractAddress}
120
+ onChange={(e) => setContractAddress(e.target.value)}
121
+ disabled={isLoading}
122
+ />
123
+ <p className="text-xs text-muted-foreground">Deploy the Q-Learning template from the IDE first</p>
124
+ </div>
125
+
126
+ <Button onClick={handleConnect} disabled={isLoading || !contractAddress.trim()} className="w-full">
127
+ {isLoading ? (
128
+ <>
129
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
130
+ Connecting...
131
+ </>
132
+ ) : (
133
+ <>
134
+ {isConnected ? <CheckCircle2 className="h-4 w-4 mr-2" /> : <XCircle className="h-4 w-4 mr-2" />}
135
+ {isConnected ? 'Connected' : 'Connect to Contract'}
136
+ </>
137
+ )}
138
+ </Button>
139
+
140
+ {isConnected && trainingInfo && (
141
+ <div className="space-y-2 pt-4 border-t border-border">
142
+ <h4 className="text-sm font-semibold">Contract Status</h4>
143
+ <div className="space-y-1 text-sm">
144
+ <div className="flex justify-between">
145
+ <span className="text-muted-foreground">Trained:</span>
146
+ <span className={trainingInfo.trained ? 'text-green-500' : 'text-yellow-500'}>
147
+ {trainingInfo.trained ? 'Yes' : 'No'}
148
+ </span>
149
+ </div>
150
+ <div className="flex justify-between">
151
+ <span className="text-muted-foreground">Episodes:</span>
152
+ <span>{trainingInfo.episodes}</span>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ )}
157
+
158
+ {!isConnected && (
159
+ <div className="bg-blue-500/10 border border-blue-500/20 p-3 rounded-md text-sm text-blue-400">
160
+ <p className="font-medium mb-1">Quick Start:</p>
161
+ <ol className="list-decimal list-inside space-y-1 text-xs">
162
+ <li>Load Q-Learning template in IDE</li>
163
+ <li>Compile the contract</li>
164
+ <li>Deploy to Arbitrum Sepolia</li>
165
+ <li>Paste the contract address here</li>
166
+ </ol>
167
+ </div>
168
+ )}
169
+ </CardContent>
170
+ </Card>
171
+ );
172
+ }
@@ -0,0 +1,346 @@
1
+ //mazegrid.tsx
2
+ 'use client';
3
+
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
+ import { usePublicClient } from 'wagmi';
6
+ import { arbitrumSepolia } from 'wagmi/chains';
7
+ import { parseAbi } from 'viem';
8
+ import { Loader2, RefreshCw } from 'lucide-react';
9
+
10
+ const QLEARNING_ABI = parseAbi([
11
+ 'function getPolicy(uint256 row, uint256 col) external view returns (uint256)',
12
+ 'function isTrained() external view returns (bool)',
13
+ ]);
14
+
15
+ const HARDCODED_MAZE = [
16
+ [0, 0, 0, 0, 0],
17
+ [0, 1, 1, 1, 0],
18
+ [0, 0, 0, 0, 0],
19
+ [0, 1, 1, 1, 0],
20
+ [0, 0, 0, 0, 0],
21
+ ];
22
+
23
+ interface MazeGridProps {
24
+ size: number;
25
+ startPos: [number, number];
26
+ goalPos: [number, number];
27
+ contractAddress: string;
28
+ isConnected: boolean;
29
+ refreshNonce?: number; // increments when you want a forced refresh
30
+ }
31
+
32
+ type CacheEntry = {
33
+ trained: boolean;
34
+ policy: number[][];
35
+ inFlight?: Promise<{ trained: boolean; policy: number[][] }>;
36
+ updatedAt: number;
37
+ };
38
+
39
+ const POLICY_CACHE = new Map<string, CacheEntry>();
40
+ const CACHE_TTL_MS = 60_000; // 1 min: adjust if you want
41
+
42
+ export function MazeGrid({
43
+ size,
44
+ startPos,
45
+ goalPos,
46
+ contractAddress,
47
+ isConnected,
48
+ refreshNonce = 0,
49
+ }: MazeGridProps) {
50
+ const [policy, setPolicy] = useState<number[][]>([]);
51
+ const [isTrained, setIsTrained] = useState(false);
52
+ const [isLoading, setIsLoading] = useState(false);
53
+ const [loadingProgress, setLoadingProgress] = useState('');
54
+ const [error, setError] = useState<string | null>(null);
55
+
56
+ const publicClient = usePublicClient({ chainId: arbitrumSepolia.id });
57
+
58
+ const normalizedAddress = useMemo(
59
+ () => contractAddress.trim().toLowerCase(),
60
+ [contractAddress]
61
+ );
62
+
63
+ const cacheKey = useMemo(
64
+ () => `${arbitrumSepolia.id}:${normalizedAddress}`,
65
+ [normalizedAddress]
66
+ );
67
+
68
+ const runIdRef = useRef(0);
69
+
70
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
71
+
72
+ const retryCall = async <T,>(
73
+ fn: () => Promise<T>,
74
+ maxRetries = 3,
75
+ delayMs = 700
76
+ ): Promise<T> => {
77
+ let lastError: Error | null = null;
78
+ for (let i = 0; i < maxRetries; i++) {
79
+ try {
80
+ return await fn();
81
+ } catch (err) {
82
+ lastError = err instanceof Error ? err : new Error(String(err));
83
+ if (i < maxRetries - 1) await sleep(delayMs * (i + 1));
84
+ }
85
+ }
86
+ throw lastError!;
87
+ };
88
+
89
+ const loadMazeData = useCallback(
90
+ async (forceReload: boolean) => {
91
+ if (!publicClient) return;
92
+ if (!isConnected) return;
93
+ if (!normalizedAddress) return;
94
+
95
+ const myRun = ++runIdRef.current;
96
+ const isCurrent = () => runIdRef.current === myRun;
97
+ const safe = (fn: () => void) => {
98
+ if (!isCurrent()) return;
99
+ fn();
100
+ };
101
+
102
+ const cached = POLICY_CACHE.get(cacheKey);
103
+ const cacheFresh =
104
+ cached &&
105
+ cached.policy?.length > 0 &&
106
+ Date.now() - cached.updatedAt < CACHE_TTL_MS;
107
+
108
+ // Use fresh cache immediately (no loading state, no flicker)
109
+ if (!forceReload && cacheFresh) {
110
+ safe(() => {
111
+ setIsTrained(cached!.trained);
112
+ setPolicy(cached!.policy);
113
+ setError(null);
114
+ setIsLoading(false);
115
+ setLoadingProgress('');
116
+ });
117
+ return;
118
+ }
119
+
120
+ // If there’s an in-flight request and we aren't forcing, dedupe onto it
121
+ if (!forceReload && cached?.inFlight) {
122
+ safe(() => {
123
+ setIsLoading(true);
124
+ setLoadingProgress('Loading policy (deduped)...');
125
+ setError(null);
126
+ });
127
+
128
+ try {
129
+ const res = await cached.inFlight;
130
+ safe(() => {
131
+ setIsTrained(res.trained);
132
+ setPolicy(res.policy);
133
+ setError(null);
134
+ });
135
+ } catch (err) {
136
+ const msg = err instanceof Error ? err.message : String(err);
137
+ safe(() => setError(`Failed to load policy: ${msg.slice(0, 140)}`));
138
+ } finally {
139
+ safe(() => {
140
+ setIsLoading(false);
141
+ setLoadingProgress('');
142
+ });
143
+ }
144
+ return;
145
+ }
146
+
147
+ if (forceReload) POLICY_CACHE.delete(cacheKey);
148
+
149
+ // IMPORTANT: keep previous policy visible while loading (no arrow flicker)
150
+ safe(() => {
151
+ setIsLoading(true);
152
+ setLoadingProgress('Checking training status...');
153
+ setError(null);
154
+ });
155
+
156
+ const inFlight = (async () => {
157
+ const trained = await retryCall(async () => {
158
+ return await publicClient.readContract({
159
+ address: normalizedAddress as `0x${string}`,
160
+ abi: QLEARNING_ABI,
161
+ functionName: 'isTrained',
162
+ });
163
+ });
164
+
165
+ if (!trained) return { trained: false, policy: [] as number[][] };
166
+
167
+ safe(() => setLoadingProgress('Loading policy (multicall)...'));
168
+
169
+ const cellsToFetch: { row: number; col: number }[] = [];
170
+ for (let row = 0; row < size; row++) {
171
+ for (let col = 0; col < size; col++) {
172
+ if (HARDCODED_MAZE[row]?.[col] !== 1) cellsToFetch.push({ row, col });
173
+ }
174
+ }
175
+
176
+ const contracts = cellsToFetch.map(({ row, col }) => ({
177
+ address: normalizedAddress as `0x${string}`,
178
+ abi: QLEARNING_ABI,
179
+ functionName: 'getPolicy' as const,
180
+ args: [BigInt(row), BigInt(col)] as const,
181
+ }));
182
+
183
+ const results = await retryCall(async () => {
184
+ return await publicClient.multicall({
185
+ contracts,
186
+ allowFailure: true,
187
+ });
188
+ }, 3, 900);
189
+
190
+ const policyData: number[][] = Array.from({ length: size }, () =>
191
+ Array.from({ length: size }, () => 0)
192
+ );
193
+
194
+ for (let i = 0; i < cellsToFetch.length; i++) {
195
+ const { row, col } = cellsToFetch[i];
196
+ const r: any = results[i];
197
+ if (r?.status === 'success') policyData[row][col] = Number(r.result);
198
+ }
199
+
200
+ return { trained: true, policy: policyData };
201
+ })();
202
+
203
+ POLICY_CACHE.set(cacheKey, {
204
+ trained: cached?.trained ?? false,
205
+ policy: cached?.policy ?? [],
206
+ inFlight,
207
+ updatedAt: Date.now(),
208
+ });
209
+
210
+ try {
211
+ const res = await inFlight;
212
+ POLICY_CACHE.set(cacheKey, {
213
+ trained: res.trained,
214
+ policy: res.policy,
215
+ updatedAt: Date.now(),
216
+ });
217
+
218
+ safe(() => {
219
+ setIsTrained(res.trained);
220
+ setPolicy(res.policy);
221
+ setError(null);
222
+ });
223
+ } catch (err) {
224
+ POLICY_CACHE.delete(cacheKey);
225
+
226
+ const msg = err instanceof Error ? err.message : String(err);
227
+ safe(() => {
228
+ if (msg.toLowerCase().includes('rate') || msg.includes('Failed to fetch')) {
229
+ setError('RPC rate limited. Wait a moment and click “Refresh Policy”.');
230
+ } else {
231
+ setError(`Failed to load policy: ${msg.slice(0, 140)}`);
232
+ }
233
+ });
234
+ } finally {
235
+ safe(() => {
236
+ setIsLoading(false);
237
+ setLoadingProgress('');
238
+ });
239
+ }
240
+ },
241
+ [publicClient, isConnected, normalizedAddress, cacheKey, size]
242
+ );
243
+
244
+ // Initial load + reload when publicClient becomes ready
245
+ useEffect(() => {
246
+ if (!isConnected || !normalizedAddress) return;
247
+ loadMazeData(false);
248
+
249
+ return () => {
250
+ // invalidate any pending async completions
251
+ runIdRef.current += 1;
252
+ };
253
+ }, [isConnected, normalizedAddress, publicClient, loadMazeData]);
254
+
255
+ // Forced refresh without unmounting
256
+ useEffect(() => {
257
+ if (!isConnected || !normalizedAddress) return;
258
+ if (refreshNonce === 0) return;
259
+ loadMazeData(true);
260
+ }, [refreshNonce, isConnected, normalizedAddress, loadMazeData]);
261
+
262
+ const handleRefresh = () => loadMazeData(true);
263
+
264
+ const getActionArrow = (action: number) => ['↑', '↓', '←', '→'][action] ?? '?';
265
+
266
+ const getCellColor = (row: number, col: number) => {
267
+ if (row === startPos[0] && col === startPos[1]) return 'bg-green-500';
268
+ if (row === goalPos[0] && col === goalPos[1]) return 'bg-red-500';
269
+ if (HARDCODED_MAZE[row]?.[col] === 1) return 'bg-gray-700';
270
+ return 'bg-muted';
271
+ };
272
+
273
+ return (
274
+ <div className="flex flex-col items-center gap-4">
275
+ {isLoading && (
276
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
277
+ <Loader2 className="h-4 w-4 animate-spin" />
278
+ {loadingProgress || 'Loading...'}
279
+ </div>
280
+ )}
281
+
282
+ {error && (
283
+ <div className="w-full bg-red-500/10 border border-red-500/20 p-3 rounded-md">
284
+ <p className="text-sm text-red-400">{error}</p>
285
+ <button
286
+ onClick={handleRefresh}
287
+ disabled={isLoading}
288
+ className="mt-2 flex items-center gap-1 text-xs text-red-300 hover:text-red-200 underline disabled:opacity-50"
289
+ >
290
+ <RefreshCw className="h-3 w-3" />
291
+ Refresh Policy
292
+ </button>
293
+ </div>
294
+ )}
295
+
296
+ <div className="grid gap-1" style={{ gridTemplateColumns: `repeat(${size}, 1fr)` }}>
297
+ {Array.from({ length: size }).map((_, row) =>
298
+ Array.from({ length: size }).map((_, col) => (
299
+ <div
300
+ key={`${row}-${col}`}
301
+ className={`
302
+ w-16 h-16 rounded-md flex items-center justify-center
303
+ ${getCellColor(row, col)}
304
+ border border-border
305
+ relative
306
+ `}
307
+ >
308
+ <span className="absolute top-1 left-1 text-xs text-muted-foreground">
309
+ {row},{col}
310
+ </span>
311
+
312
+ {isTrained &&
313
+ HARDCODED_MAZE[row]?.[col] !== 1 &&
314
+ !(row === goalPos[0] && col === goalPos[1]) &&
315
+ policy[row]?.[col] !== undefined && (
316
+ <span className="text-2xl font-bold">{getActionArrow(policy[row][col])}</span>
317
+ )}
318
+
319
+ {row === startPos[0] && col === startPos[1] && (
320
+ <span className="absolute bottom-1 text-xs font-semibold text-white">START</span>
321
+ )}
322
+ {row === goalPos[0] && col === goalPos[1] && (
323
+ <span className="absolute bottom-1 text-xs font-semibold text-white">GOAL</span>
324
+ )}
325
+ </div>
326
+ ))
327
+ )}
328
+ </div>
329
+
330
+ {!isTrained && !isLoading && (
331
+ <p className="text-sm text-muted-foreground">Train the agent to see the learned policy arrows</p>
332
+ )}
333
+
334
+ {isTrained && !error && (
335
+ <button
336
+ onClick={handleRefresh}
337
+ disabled={isLoading}
338
+ className="flex items-center gap-1 text-sm text-primary hover:underline disabled:opacity-50"
339
+ >
340
+ <RefreshCw className="h-3 w-3" />
341
+ Refresh Policy
342
+ </button>
343
+ )}
344
+ </div>
345
+ );
346
+ }