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,256 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { useChainId, useAccount } from 'wagmi';
5
+ import { arbitrumSepolia } from 'wagmi/chains';
6
+ import { Button } from '@/components/ui/button';
7
+ import { Droplet, ExternalLink, Info } from 'lucide-react';
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuLabel,
13
+ DropdownMenuSeparator,
14
+ DropdownMenuTrigger,
15
+ } from '@/components/ui/dropdown-menu';
16
+ import { Alert, AlertDescription } from '@/components/ui/alert';
17
+
18
+ const FAUCETS = [
19
+ {
20
+ name: 'Triangle Platform',
21
+ url: 'https://faucet.triangleplatform.com/arbitrum/sepolia',
22
+ description: '0.001 ETH - No requirements',
23
+ recommended: true,
24
+ },
25
+ {
26
+ name: 'LearnWeb3 Faucet',
27
+ url: 'https://learnweb3.io/faucets/arbitrum_sepolia',
28
+ description: '0.0001 ETH - GitHub login',
29
+ recommended: true,
30
+ },
31
+ {
32
+ name: 'Arbitrum Bridge (Sepolia → Arb Sepolia)',
33
+ url: 'https://bridge.arbitrum.io/?destinationChain=arbitrum-sepolia&sourceChain=sepolia',
34
+ description: 'Bridge from Ethereum Sepolia',
35
+ isBridge: true,
36
+ },
37
+ {
38
+ name: 'Alchemy Faucet',
39
+ url: 'https://www.alchemy.com/faucets/arbitrum-sepolia',
40
+ description: 'Requires Alchemy account',
41
+ },
42
+ {
43
+ name: 'Chainlink Faucet',
44
+ url: 'https://faucets.chain.link/arbitrum-sepolia',
45
+ description: 'Requires mainnet balance',
46
+ },
47
+
48
+ // Orbit chain faucets
49
+ {
50
+ name: 'XAI Testnet Faucet',
51
+ url: 'https://faucet.xai.games',
52
+ description: 'Get sXAI testnet tokens',
53
+ isOrbit: true,
54
+ },
55
+ {
56
+ name: 'ApeChain Curtis Faucet',
57
+ url: 'https://curtis.hub.caldera.xyz/',
58
+ description: 'Get APE testnet tokens (via Caldera)',
59
+ isOrbit: true,
60
+ },
61
+ {
62
+ name: 'Nitrogen Testnet Faucet',
63
+ url: 'https://nitrogen-faucet.altlayer.io/',
64
+ description: 'Get ETH for Nitrogen testnet',
65
+ isOrbit: true,
66
+ },
67
+ ];
68
+
69
+ // Ethereum Sepolia faucets (to bridge from)
70
+ const ETH_SEPOLIA_FAUCETS = [
71
+ {
72
+ name: 'Alchemy Sepolia Faucet',
73
+ url: 'https://www.alchemy.com/faucets/ethereum-sepolia',
74
+ description: 'Get ETH Sepolia first',
75
+ },
76
+ {
77
+ name: 'Infura Sepolia Faucet',
78
+ url: 'https://www.infura.io/faucet/sepolia',
79
+ description: 'Alternative Sepolia source',
80
+ },
81
+ ];
82
+
83
+ export function FaucetButton() {
84
+ const [mounted, setMounted] = useState(false);
85
+ useEffect(() => setMounted(true), []);
86
+
87
+ const chainId = useChainId();
88
+ const { address } = useAccount();
89
+
90
+ // ✅ Prevent hydration mismatch: render nothing until client mounted
91
+ if (!mounted) {
92
+ return null;
93
+ }
94
+
95
+ const recommendedFaucets = FAUCETS.filter((f) => f.recommended);
96
+ const bridgeFaucets = FAUCETS.filter((f) => f.isBridge);
97
+ const orbitFaucets = FAUCETS.filter((f) => f.isOrbit);
98
+ const otherFaucets = FAUCETS.filter((f) => !f.recommended && !f.isBridge && !f.isOrbit);
99
+
100
+ return (
101
+ <DropdownMenu>
102
+ <DropdownMenuTrigger asChild>
103
+ <Button variant="outline" size="sm" className="hidden sm:flex">
104
+ <Droplet className="h-4 w-4 mr-2" />
105
+ Faucet
106
+ </Button>
107
+ </DropdownMenuTrigger>
108
+
109
+ <DropdownMenuContent align="end" className="w-80 max-h-96 overflow-y-auto custom-scrollbar">
110
+ <DropdownMenuLabel className="flex items-center gap-2">
111
+ <Droplet className="h-4 w-4" />
112
+ Get Free Testnet ETH
113
+ </DropdownMenuLabel>
114
+
115
+ <div className="px-2 py-2">
116
+ <Alert className="border-blue-500/20 bg-blue-500/10">
117
+ <Info className="h-4 w-4 text-blue-400" />
118
+ <AlertDescription className="text-xs text-blue-400">
119
+ You're on <strong>Arbitrum Sepolia</strong> (testnet) - all ETH is free!
120
+ </AlertDescription>
121
+ </Alert>
122
+ </div>
123
+
124
+ <DropdownMenuSeparator />
125
+
126
+ <DropdownMenuLabel className="text-xs text-muted-foreground">
127
+ Direct Faucets (Recommended)
128
+ </DropdownMenuLabel>
129
+
130
+ {recommendedFaucets.map((faucet) => (
131
+ <DropdownMenuItem key={faucet.name} asChild className="cursor-pointer">
132
+ <a
133
+ href={faucet.url}
134
+ target="_blank"
135
+ rel="noopener noreferrer"
136
+ className="flex items-start justify-between w-full"
137
+ >
138
+ <div className="flex-1">
139
+ <div className="font-medium text-sm flex items-center gap-2">
140
+ {faucet.name}
141
+ <span className="text-xs bg-green-500/20 text-green-400 px-1.5 py-0.5 rounded">
142
+ Easy
143
+ </span>
144
+ </div>
145
+ <div className="text-xs text-muted-foreground">{faucet.description}</div>
146
+ </div>
147
+ <ExternalLink className="h-4 w-4 ml-2 shrink-0" />
148
+ </a>
149
+ </DropdownMenuItem>
150
+ ))}
151
+
152
+ <DropdownMenuSeparator />
153
+
154
+ <DropdownMenuLabel className="text-xs text-muted-foreground">
155
+ Bridge Method (2 Steps)
156
+ </DropdownMenuLabel>
157
+
158
+ <div className="px-2 py-2 text-xs text-muted-foreground space-y-2">
159
+ <div>
160
+ <strong>Step 1:</strong> Get ETH Sepolia from:
161
+ </div>
162
+ {ETH_SEPOLIA_FAUCETS.map((faucet) => (
163
+ <a
164
+ key={faucet.name}
165
+ href={faucet.url}
166
+ target="_blank"
167
+ rel="noopener noreferrer"
168
+ className="flex items-center gap-2 hover:text-foreground pl-4"
169
+ >
170
+ <ExternalLink className="h-3 w-3" />
171
+ {faucet.name}
172
+ </a>
173
+ ))}
174
+ <div className="pt-1">
175
+ <strong>Step 2:</strong> Bridge to Arbitrum Sepolia:
176
+ </div>
177
+ </div>
178
+
179
+ {bridgeFaucets.map((faucet) => (
180
+ <DropdownMenuItem key={faucet.name} asChild className="cursor-pointer">
181
+ <a
182
+ href={faucet.url}
183
+ target="_blank"
184
+ rel="noopener noreferrer"
185
+ className="flex items-start justify-between w-full"
186
+ >
187
+ <div className="flex-1">
188
+ <div className="font-medium text-sm">{faucet.name}</div>
189
+ <div className="text-xs text-muted-foreground">{faucet.description}</div>
190
+ </div>
191
+ <ExternalLink className="h-4 w-4 ml-2 shrink-0" />
192
+ </a>
193
+ </DropdownMenuItem>
194
+ ))}
195
+
196
+ {/* ✅ Orbit section */}
197
+ {orbitFaucets.length > 0 && (
198
+ <>
199
+ <DropdownMenuSeparator />
200
+ <DropdownMenuLabel className="text-xs text-muted-foreground">
201
+ Orbit Chain Faucets
202
+ </DropdownMenuLabel>
203
+
204
+ {orbitFaucets.map((faucet) => (
205
+ <DropdownMenuItem key={faucet.name} asChild className="cursor-pointer">
206
+ <a
207
+ href={faucet.url}
208
+ target="_blank"
209
+ rel="noopener noreferrer"
210
+ className="flex items-start justify-between w-full"
211
+ >
212
+ <div className="flex-1">
213
+ <div className="font-medium text-sm">{faucet.name}</div>
214
+ <div className="text-xs text-muted-foreground">{faucet.description}</div>
215
+ </div>
216
+ <ExternalLink className="h-4 w-4 ml-2 shrink-0" />
217
+ </a>
218
+ </DropdownMenuItem>
219
+ ))}
220
+ </>
221
+ )}
222
+
223
+ <DropdownMenuSeparator />
224
+
225
+ <DropdownMenuLabel className="text-xs text-muted-foreground">Other Options</DropdownMenuLabel>
226
+
227
+ {otherFaucets.map((faucet) => (
228
+ <DropdownMenuItem key={faucet.name} asChild className="cursor-pointer">
229
+ <a
230
+ href={faucet.url}
231
+ target="_blank"
232
+ rel="noopener noreferrer"
233
+ className="flex items-start justify-between w-full"
234
+ >
235
+ <div className="flex-1">
236
+ <div className="font-medium text-sm">{faucet.name}</div>
237
+ <div className="text-xs text-muted-foreground">{faucet.description}</div>
238
+ </div>
239
+ <ExternalLink className="h-4 w-4 ml-2 shrink-0" />
240
+ </a>
241
+ </DropdownMenuItem>
242
+ ))}
243
+
244
+ {address && (
245
+ <>
246
+ <DropdownMenuSeparator />
247
+ <div className="px-2 py-2 text-xs text-muted-foreground">
248
+ <strong>Your Address:</strong>
249
+ <code className="block mt-1 break-all bg-muted p-1 rounded">{address}</code>
250
+ </div>
251
+ </>
252
+ )}
253
+ </DropdownMenuContent>
254
+ </DropdownMenu>
255
+ );
256
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {}
22
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
@@ -0,0 +1,75 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback } from "react";
4
+
5
+ interface UseAICompletionReturn {
6
+ isLoading: boolean;
7
+ completion: string;
8
+ error: string | null;
9
+ generateCompletion: (prompt: string, context: string) => Promise<void>;
10
+ clearCompletion: () => void;
11
+ }
12
+
13
+ export function useAICompletion(): UseAICompletionReturn {
14
+ const [isLoading, setIsLoading] = useState(false);
15
+ const [completion, setCompletion] = useState("");
16
+ const [error, setError] = useState<string | null>(null);
17
+
18
+ const generateCompletion = useCallback(
19
+ async (prompt: string, context: string) => {
20
+ setIsLoading(true);
21
+ setCompletion("");
22
+ setError(null);
23
+
24
+ try {
25
+ const response = await fetch("/api/complete", {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({ prompt, context }),
29
+ });
30
+
31
+ if (!response.ok) {
32
+ throw new Error("Failed to generate completion");
33
+ }
34
+
35
+ const reader = response.body?.getReader();
36
+ const decoder = new TextDecoder();
37
+
38
+ if (!reader) {
39
+ throw new Error("No response body");
40
+ }
41
+
42
+ let accumulatedText = "";
43
+
44
+ while (true) {
45
+ const { done, value } = await reader.read();
46
+ if (done) break;
47
+
48
+ const chunk = decoder.decode(value, { stream: true });
49
+ accumulatedText += chunk;
50
+ setCompletion(accumulatedText);
51
+ }
52
+ } catch (err) {
53
+ const errorMessage =
54
+ err instanceof Error ? err.message : "Completion failed";
55
+ setError(errorMessage);
56
+ } finally {
57
+ setIsLoading(false);
58
+ }
59
+ },
60
+ []
61
+ );
62
+
63
+ const clearCompletion = useCallback(() => {
64
+ setCompletion("");
65
+ setError(null);
66
+ }, []);
67
+
68
+ return {
69
+ isLoading,
70
+ completion,
71
+ error,
72
+ generateCompletion,
73
+ clearCompletion,
74
+ };
75
+ }
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback } from "react";
4
+ import { ContractInteractionData } from "@/types/blockchain";
5
+ import { BlockchainURLInfo } from "@/lib/url-parser";
6
+ import {
7
+ fetchContractForInteraction,
8
+ BlockchainLoadProgress,
9
+ } from "@/lib/blockchain-loader";
10
+
11
+ export function useBlockchainLoader() {
12
+ const [isLoading, setIsLoading] = useState(false);
13
+ const [progress, setProgress] = useState<BlockchainLoadProgress | null>(null);
14
+ const [error, setError] = useState<string | null>(null);
15
+
16
+ const loadFromBlockchain = useCallback(
17
+ async (
18
+ urlInfo: BlockchainURLInfo
19
+ ): Promise<ContractInteractionData | null> => {
20
+ setIsLoading(true);
21
+ setError(null);
22
+ setProgress(null);
23
+
24
+ try {
25
+ const contractData = await fetchContractForInteraction(
26
+ urlInfo,
27
+ (progressUpdate) => {
28
+ setProgress(progressUpdate);
29
+ }
30
+ );
31
+
32
+ setIsLoading(false);
33
+ return contractData;
34
+ } catch (err) {
35
+ const errorMessage =
36
+ err instanceof Error ? err.message : "Unknown error";
37
+ setError(errorMessage);
38
+ setIsLoading(false);
39
+ return null;
40
+ }
41
+ },
42
+ []
43
+ );
44
+
45
+ const reset = useCallback(() => {
46
+ setIsLoading(false);
47
+ setProgress(null);
48
+ setError(null);
49
+ }, []);
50
+
51
+ return {
52
+ isLoading,
53
+ progress,
54
+ error,
55
+ loadFromBlockchain,
56
+ reset,
57
+ };
58
+ }
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useRef } from "react";
4
+
5
+ export interface Message {
6
+ id: string;
7
+ role: "user" | "assistant" | "system";
8
+ content: string;
9
+ timestamp: number;
10
+ }
11
+
12
+ interface UseChatReturn {
13
+ messages: Message[];
14
+ isLoading: boolean;
15
+ error: string | null;
16
+ sendMessage: (content: string, context?: string) => Promise<void>;
17
+ clearMessages: () => void;
18
+ }
19
+
20
+ export function useChat(): UseChatReturn {
21
+ const [messages, setMessages] = useState<Message[]>([]);
22
+ const [isLoading, setIsLoading] = useState(false);
23
+ const [error, setError] = useState<string | null>(null);
24
+ const abortControllerRef = useRef<AbortController | null>(null);
25
+
26
+ const sendMessage = useCallback(
27
+ async (content: string, context?: string) => {
28
+ if (!content.trim()) return;
29
+
30
+ // Add user message
31
+ const userMessage: Message = {
32
+ id: `user-${Date.now()}`,
33
+ role: "user",
34
+ content: content.trim(),
35
+ timestamp: Date.now(),
36
+ };
37
+
38
+ setMessages((prev) => [...prev, userMessage]);
39
+ setIsLoading(true);
40
+ setError(null);
41
+
42
+ // Create assistant message placeholder
43
+ const assistantMessageId = `assistant-${Date.now()}`;
44
+ const assistantMessage: Message = {
45
+ id: assistantMessageId,
46
+ role: "assistant",
47
+ content: "",
48
+ timestamp: Date.now(),
49
+ };
50
+
51
+ setMessages((prev) => [...prev, assistantMessage]);
52
+
53
+ try {
54
+ // Cancel previous request if exists
55
+ if (abortControllerRef.current) {
56
+ abortControllerRef.current.abort();
57
+ }
58
+
59
+ abortControllerRef.current = new AbortController();
60
+
61
+ const response = await fetch("/api/chat", {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify({
65
+ messages: messages.map((m) => ({
66
+ role: m.role,
67
+ content: m.content,
68
+ })),
69
+ context,
70
+ }),
71
+ signal: abortControllerRef.current.signal,
72
+ });
73
+
74
+ if (!response.ok) {
75
+ throw new Error("Failed to get response");
76
+ }
77
+
78
+ const reader = response.body?.getReader();
79
+ const decoder = new TextDecoder();
80
+
81
+ if (!reader) {
82
+ throw new Error("No response body");
83
+ }
84
+
85
+ let accumulatedContent = "";
86
+
87
+ while (true) {
88
+ const { done, value } = await reader.read();
89
+ if (done) break;
90
+
91
+ const chunk = decoder.decode(value, { stream: true });
92
+ accumulatedContent += chunk;
93
+
94
+ // Update assistant message with accumulated content
95
+ setMessages((prev) =>
96
+ prev.map((msg) =>
97
+ msg.id === assistantMessageId
98
+ ? { ...msg, content: accumulatedContent }
99
+ : msg
100
+ )
101
+ );
102
+ }
103
+ } catch (err) {
104
+ if (err instanceof Error && err.name === "AbortError") {
105
+ // Request was cancelled, ignore
106
+ return;
107
+ }
108
+
109
+ const errorMessage =
110
+ err instanceof Error ? err.message : "Failed to send message";
111
+ setError(errorMessage);
112
+
113
+ // Remove failed assistant message
114
+ setMessages((prev) =>
115
+ prev.filter((msg) => msg.id !== assistantMessageId)
116
+ );
117
+ } finally {
118
+ setIsLoading(false);
119
+ abortControllerRef.current = null;
120
+ }
121
+ },
122
+ [messages]
123
+ );
124
+
125
+ const clearMessages = useCallback(() => {
126
+ setMessages([]);
127
+ setError(null);
128
+ }, []);
129
+
130
+ return {
131
+ messages,
132
+ isLoading,
133
+ error,
134
+ sendMessage,
135
+ clearMessages,
136
+ };
137
+ }