@tscircuit/fake-snippets 0.0.87 → 0.0.89

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 (75) hide show
  1. package/api/generated-index.js +96 -14
  2. package/bun-tests/fake-snippets-api/routes/proxy.test.ts +42 -0
  3. package/bun.lock +187 -206
  4. package/dist/bundle.js +207 -101
  5. package/fake-snippets-api/routes/api/package_releases/create.ts +1 -1
  6. package/fake-snippets-api/routes/api/proxy.ts +128 -0
  7. package/package.json +57 -50
  8. package/renovate.json +2 -1
  9. package/src/App.tsx +22 -3
  10. package/src/ContextProviders.tsx +2 -0
  11. package/src/build-watcher.ts +52 -0
  12. package/src/components/CmdKMenu.tsx +533 -197
  13. package/src/components/DownloadButtonAndMenu.tsx +104 -26
  14. package/src/components/FileSidebar.tsx +11 -1
  15. package/src/components/Header.tsx +5 -1
  16. package/src/components/Header2.tsx +7 -2
  17. package/src/components/HeaderLogin.tsx +1 -1
  18. package/src/components/PackageBuildsPage/LogContent.tsx +25 -22
  19. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +6 -6
  20. package/src/components/PackageBuildsPage/build-preview-content.tsx +5 -5
  21. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +15 -13
  22. package/src/components/PackageBuildsPage/package-build-header.tsx +17 -16
  23. package/src/components/PackageCard.tsx +66 -16
  24. package/src/components/PrefetchPageLink.tsx +66 -15
  25. package/src/components/SearchComponent.tsx +2 -2
  26. package/src/components/SuspenseRunFrame.tsx +14 -2
  27. package/src/components/ViewPackagePage/components/important-files-view.tsx +97 -22
  28. package/src/components/ViewPackagePage/components/main-content-header.tsx +27 -3
  29. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +2 -2
  30. package/src/components/ViewPackagePage/components/repo-page-content.tsx +49 -34
  31. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +2 -2
  32. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +20 -12
  33. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +0 -7
  34. package/src/components/ViewPackagePage/utils/fuzz-search.ts +121 -0
  35. package/src/components/ViewPackagePage/utils/is-hidden-file.ts +4 -0
  36. package/src/components/ViewPackagePage/utils/is-package-file-important.ts +18 -5
  37. package/src/components/dialogs/confirm-delete-package-dialog.tsx +1 -1
  38. package/src/components/dialogs/confirm-discard-changes-dialog.tsx +73 -0
  39. package/src/components/dialogs/edit-package-details-dialog.tsx +2 -2
  40. package/src/components/dialogs/view-ts-files-dialog.tsx +478 -42
  41. package/src/components/package-port/CodeAndPreview.tsx +16 -0
  42. package/src/components/package-port/CodeEditor.tsx +113 -11
  43. package/src/components/package-port/CodeEditorHeader.tsx +39 -4
  44. package/src/components/package-port/EditorNav.tsx +41 -15
  45. package/src/components/package-port/GlobalFindReplace.tsx +681 -0
  46. package/src/components/package-port/QuickOpen.tsx +241 -0
  47. package/src/components/ui/dialog.tsx +1 -1
  48. package/src/components/ui/tree-view.tsx +1 -1
  49. package/src/global.d.ts +3 -0
  50. package/src/hooks/use-ai-review.ts +31 -0
  51. package/src/hooks/use-current-package-release.ts +5 -1
  52. package/src/hooks/use-download-zip.ts +50 -0
  53. package/src/hooks/use-hotkey.ts +116 -0
  54. package/src/hooks/use-package-by-package-id.ts +1 -0
  55. package/src/hooks/use-package-by-package-name.ts +1 -0
  56. package/src/hooks/use-package-files.ts +3 -0
  57. package/src/hooks/use-package-release.ts +5 -1
  58. package/src/hooks/use-package.ts +1 -0
  59. package/src/hooks/use-request-ai-review-mutation.ts +14 -6
  60. package/src/hooks/use-snippet.ts +1 -0
  61. package/src/hooks/useFileManagement.ts +26 -8
  62. package/src/hooks/usePackageFilesLoader.ts +3 -1
  63. package/src/index.css +11 -0
  64. package/src/lib/decodeUrlHashToFsMap.ts +17 -0
  65. package/src/lib/download-fns/download-circuit-png.ts +88 -0
  66. package/src/lib/download-fns/download-png-utils.ts +31 -0
  67. package/src/lib/encodeFsMapToUrlHash.ts +13 -0
  68. package/src/lib/populate-query-cache-with-ssr-data.ts +39 -38
  69. package/src/lib/ts-lib-cache.ts +47 -0
  70. package/src/lib/types.ts +2 -0
  71. package/src/main.tsx +7 -0
  72. package/src/pages/dashboard.tsx +8 -5
  73. package/src/pages/user-profile.tsx +1 -1
  74. package/src/pages/view-package.tsx +15 -7
  75. package/vite.config.ts +100 -1
@@ -0,0 +1,241 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
+ import {
3
+ Search,
4
+ Code,
5
+ Braces,
6
+ FileIcon,
7
+ Hash,
8
+ BookOpen,
9
+ FileText,
10
+ } from "lucide-react"
11
+ import type { PackageFile } from "./CodeAndPreview"
12
+ import { fuzzyMatch } from "../ViewPackagePage/utils/fuzz-search"
13
+
14
+ interface ScoredFile extends PackageFile {
15
+ score: number
16
+ matches: number[]
17
+ }
18
+
19
+ interface QuickOpenProps {
20
+ files: PackageFile[]
21
+ currentFile: string | null
22
+ onFileSelect: (path: string) => void
23
+ onClose: () => void
24
+ }
25
+
26
+ const QuickOpen = ({
27
+ files,
28
+ currentFile,
29
+ onFileSelect,
30
+ onClose,
31
+ }: QuickOpenProps) => {
32
+ const [query, setQuery] = useState("")
33
+ const [selected, setSelected] = useState(0)
34
+ const inputRef = useRef<HTMLInputElement>(null)
35
+ const selectedItemRef = useRef<HTMLDivElement>(null)
36
+
37
+ const scoredFiles = useMemo((): ScoredFile[] => {
38
+ if (!query) return files.map((f) => ({ ...f, score: 0, matches: [] }))
39
+
40
+ return files
41
+ .map((file) => {
42
+ const { score, matches } = fuzzyMatch(query, file.path)
43
+ return { ...file, score, matches }
44
+ })
45
+ .filter((f) => f.score >= 0)
46
+ .sort((a, b) => {
47
+ // Current file gets priority
48
+ if (a.path === currentFile) return -1
49
+ if (b.path === currentFile) return 1
50
+ // Then by score
51
+ return b.score - a.score
52
+ })
53
+ }, [files, query, currentFile])
54
+
55
+ const handleKeyDown = useCallback(
56
+ (e: React.KeyboardEvent) => {
57
+ switch (e.key) {
58
+ case "ArrowDown":
59
+ e.preventDefault()
60
+ setSelected((prev) => Math.min(prev + 1, scoredFiles.length - 1))
61
+ break
62
+ case "ArrowUp":
63
+ e.preventDefault()
64
+ setSelected((prev) => Math.max(prev - 1, 0))
65
+ break
66
+ case "Enter":
67
+ e.preventDefault()
68
+ if (scoredFiles[selected]) {
69
+ onFileSelect(scoredFiles[selected].path)
70
+ onClose()
71
+ }
72
+ break
73
+ case "Escape":
74
+ e.preventDefault()
75
+ onClose()
76
+ break
77
+ }
78
+ },
79
+ [scoredFiles, selected, onFileSelect, onClose],
80
+ )
81
+
82
+ useEffect(() => {
83
+ inputRef.current?.focus()
84
+ }, [])
85
+
86
+ useEffect(() => {
87
+ setSelected(0)
88
+ }, [query])
89
+
90
+ useEffect(() => {
91
+ if (selectedItemRef.current) {
92
+ selectedItemRef.current.scrollIntoView({
93
+ behavior: "smooth",
94
+ block: "nearest",
95
+ })
96
+ }
97
+ }, [selected])
98
+
99
+ const getFileIcon = useCallback((path: string) => {
100
+ const ext = path.split(".").pop()?.toLowerCase()
101
+ const iconProps = { size: 16, className: "" }
102
+
103
+ switch (ext) {
104
+ case "tsx":
105
+ iconProps.className = "text-blue-500"
106
+ return <Code {...iconProps} />
107
+ case "ts":
108
+ iconProps.className = "text-blue-600"
109
+ return <Code {...iconProps} />
110
+ case "jsx":
111
+ iconProps.className = "text-cyan-500"
112
+ return <Code {...iconProps} />
113
+ case "js":
114
+ iconProps.className = "text-yellow-500"
115
+ return <Code {...iconProps} />
116
+ case "json":
117
+ iconProps.className = "text-orange-500"
118
+ return <Braces {...iconProps} />
119
+ case "md":
120
+ iconProps.className = "text-gray-600"
121
+ return <BookOpen {...iconProps} />
122
+ case "css":
123
+ iconProps.className = "text-pink-500"
124
+ return <Hash {...iconProps} />
125
+ case "html":
126
+ iconProps.className = "text-red-500"
127
+ return <FileText {...iconProps} />
128
+ default:
129
+ iconProps.className = "text-gray-400"
130
+ return <FileIcon {...iconProps} />
131
+ }
132
+ }, [])
133
+
134
+ const renderHighlighted = useCallback(
135
+ (file: ScoredFile) => {
136
+ if (!query) return file.path
137
+
138
+ const chars = file.path.split("")
139
+ return chars.map((char, i) => (
140
+ <span key={i} className={file.matches.includes(i) ? "bg-blue-200" : ""}>
141
+ {char}
142
+ </span>
143
+ ))
144
+ },
145
+ [query],
146
+ )
147
+
148
+ return (
149
+ <div
150
+ className="fixed inset-0 bg-black/20 backdrop-blur-sm flex items-start justify-center pt-16 sm:pt-20 z-50 p-4"
151
+ onClick={(e) => {
152
+ if (e.target === e.currentTarget) {
153
+ onClose()
154
+ }
155
+ }}
156
+ >
157
+ <div
158
+ className="bg-white/95 backdrop-blur-md rounded-xl shadow-2xl border border-slate-200/50 w-full max-w-lg mx-auto"
159
+ onClick={(e) => e.stopPropagation()}
160
+ >
161
+ <div className="p-4 border-b border-slate-100">
162
+ <div className="relative">
163
+ <Search
164
+ size={16}
165
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
166
+ />
167
+ <input
168
+ ref={inputRef}
169
+ value={query}
170
+ onChange={(e) => setQuery(e.target.value)}
171
+ onKeyDown={handleKeyDown}
172
+ placeholder="Search files..."
173
+ className="w-full pl-10 pr-4 py-2.5 text-sm bg-slate-50/80 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-950/10 focus:border-slate-400 transition-all duration-200"
174
+ />
175
+ </div>
176
+ </div>
177
+
178
+ <div className="max-h-80 overflow-y-auto py-2">
179
+ {scoredFiles.length === 0 ? (
180
+ <div className="p-6 text-center select-none text-slate-400 text-sm">
181
+ {query ? "No matching files" : "No files"}
182
+ </div>
183
+ ) : (
184
+ scoredFiles.map((file, index) => (
185
+ <div
186
+ key={file.path}
187
+ ref={index === selected ? selectedItemRef : null}
188
+ onClick={() => {
189
+ onFileSelect(file.path)
190
+ onClose()
191
+ }}
192
+ className={`px-4 py-2 cursor-pointer flex items-center gap-3 transition-all duration-150 ${
193
+ index === selected
194
+ ? "bg-slate-100 border-r-2 border-slate-500"
195
+ : "hover:bg-slate-50"
196
+ }`}
197
+ >
198
+ <div className="flex-shrink-0">{getFileIcon(file.path)}</div>
199
+ <div className="flex-1 min-w-0 text-sm font-mono text-slate-900 truncate">
200
+ {renderHighlighted(file)}
201
+ </div>
202
+ {file.path === currentFile && (
203
+ <span className="text-xs px-2 py-0.5 rounded-full bg-slate-100 text-slate-700">
204
+ current
205
+ </span>
206
+ )}
207
+ </div>
208
+ ))
209
+ )}
210
+ </div>
211
+
212
+ <div className="px-4 select-none py-3 text-xs border-t border-slate-100 bg-slate-50/50 rounded-b-xl">
213
+ <div className="flex items-center justify-between text-slate-500">
214
+ <div className="flex gap-4">
215
+ <span className="flex items-center gap-1.5">
216
+ <kbd className="px-2 py-1 font-mono bg-white border border-slate-200 rounded shadow-sm">
217
+ ↑↓
218
+ </kbd>
219
+ navigate
220
+ </span>
221
+ <span className="flex items-center gap-1.5">
222
+ <kbd className="px-2 py-1 font-mono bg-white border border-slate-200 rounded shadow-sm">
223
+
224
+ </kbd>
225
+ select
226
+ </span>
227
+ </div>
228
+ <span className="flex items-center gap-1.5">
229
+ <kbd className="px-2 py-1 font-mono bg-white border border-slate-200 rounded shadow-sm">
230
+ esc
231
+ </kbd>
232
+ close
233
+ </span>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ )
239
+ }
240
+
241
+ export default QuickOpen
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
36
36
  <DialogPrimitive.Content
37
37
  ref={ref}
38
38
  className={cn(
39
- "fixed left-[50%] top-[50%] z-[100] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-slate-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-slate-800 dark:bg-slate-950",
39
+ "fixed left-[50%] max-w-[95vw] md:max-w-lg top-[50%] z-[100] grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border border-slate-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg dark:border-slate-800 dark:bg-slate-950",
40
40
  className,
41
41
  )}
42
42
  {...props}
@@ -404,7 +404,7 @@ const TreeLeaf = React.forwardRef<
404
404
  default={defaultLeafIcon}
405
405
  />
406
406
  <span className="flex-grow text-sm truncate">{item.name}</span>
407
- <div onClick={(e) => e.stopPropagation()}>
407
+ <div className="flex items-center" onClick={(e) => e.stopPropagation()}>
408
408
  <TreeActions isSelected={true}>{item.actions}</TreeActions>
409
409
  </div>
410
410
  </div>
@@ -0,0 +1,3 @@
1
+ interface Window {
2
+ TSC_BUILD_ID?: string
3
+ }
@@ -0,0 +1,31 @@
1
+ import type { AiReview } from "fake-snippets-api/lib/db/schema"
2
+ import { useQuery } from "react-query"
3
+ import { useAxios } from "./use-axios"
4
+
5
+ export const useAiReview = (
6
+ aiReviewId: string | null,
7
+ options?: {
8
+ refetchInterval?:
9
+ | number
10
+ | false
11
+ | ((data: AiReview | undefined) => number | false)
12
+ },
13
+ ) => {
14
+ const axios = useAxios()
15
+
16
+ return useQuery<AiReview, Error & { status: number }>(
17
+ ["aiReview", aiReviewId],
18
+ async () => {
19
+ if (!aiReviewId) throw new Error("aiReviewId is required")
20
+ const { data } = await axios.get("/ai_reviews/get", {
21
+ params: { ai_review_id: aiReviewId },
22
+ })
23
+ return data.ai_review as AiReview
24
+ },
25
+ {
26
+ enabled: Boolean(aiReviewId),
27
+ refetchInterval: options?.refetchInterval,
28
+ refetchOnWindowFocus: false,
29
+ },
30
+ )
31
+ }
@@ -2,11 +2,15 @@ import { useParams } from "wouter"
2
2
  import { useCurrentPackageId } from "./use-current-package-id"
3
3
  import { usePackageRelease } from "./use-package-release"
4
4
  import { useUrlParams } from "./use-url-params"
5
+ import type { PackageRelease } from "fake-snippets-api/lib/db/schema"
5
6
 
6
7
  export const useCurrentPackageRelease = (options?: {
7
8
  include_ai_review?: boolean
8
9
  include_logs?: boolean
9
- refetchInterval?: number
10
+ refetchInterval?:
11
+ | number
12
+ | false
13
+ | ((data: PackageRelease | undefined) => number | false)
10
14
  }) => {
11
15
  const { packageId } = useCurrentPackageId()
12
16
  const urlParams = useUrlParams()
@@ -0,0 +1,50 @@
1
+ import { useCallback } from "react"
2
+ import JSZip from "jszip"
3
+ import { saveAs } from "file-saver"
4
+ import { useAxios } from "@/hooks/use-axios"
5
+ import { Package, PackageFile } from "fake-snippets-api/lib/db/schema"
6
+ import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
7
+
8
+ export const useDownloadZip = () => {
9
+ const axios = useAxios()
10
+
11
+ const downloadZip = useCallback(
12
+ async (packageInfo: Package, packageFiles: PackageFile[]) => {
13
+ if (!packageInfo || !packageFiles) return
14
+
15
+ const zip = new JSZip()
16
+
17
+ const visibleFiles = packageFiles.filter(
18
+ (file) => !isHiddenFile(file.file_path),
19
+ )
20
+
21
+ for (const file of visibleFiles) {
22
+ try {
23
+ const response = await axios.post("/package_files/get", {
24
+ package_file_id: file.package_file_id,
25
+ })
26
+
27
+ const content = response.data.package_file?.content_text || ""
28
+
29
+ const cleanPath = file.file_path.startsWith("/")
30
+ ? file.file_path.slice(1)
31
+ : file.file_path
32
+
33
+ zip.file(cleanPath, content)
34
+ } catch (error) {
35
+ console.error(
36
+ `Failed to fetch content for file ${file.file_path}:`,
37
+ error,
38
+ )
39
+ }
40
+ }
41
+
42
+ const blob = await zip.generateAsync({ type: "blob" })
43
+ const fileName = `${packageInfo.unscoped_name || packageInfo.name}.zip`
44
+ saveAs(blob, fileName)
45
+ },
46
+ [axios],
47
+ )
48
+
49
+ return { downloadZip }
50
+ }
@@ -0,0 +1,116 @@
1
+ import { useEffect, useRef } from "react"
2
+
3
+ /**
4
+ * Efficient hook for handling keyboard shortcuts
5
+ *
6
+ * Examples:
7
+ * - useHotkeyCombo("cmd+b", () => toggleSidebar())
8
+ * - useHotkeyCombo("ctrl+s", () => save())
9
+ * - useHotkeyCombo("Escape", () => closeModal())
10
+ * - useHotkey("Enter", () => submit(), { meta: true })
11
+ */
12
+
13
+ type HotkeyModifiers = {
14
+ ctrl?: boolean
15
+ alt?: boolean
16
+ shift?: boolean
17
+ meta?: boolean
18
+ }
19
+
20
+ type HotkeyCallback = (event: KeyboardEvent) => void
21
+
22
+ type HotkeyOptions = {
23
+ preventDefault?: boolean
24
+ stopPropagation?: boolean
25
+ target?: EventTarget | null
26
+ }
27
+
28
+ export const useHotkey = (
29
+ key: string,
30
+ callback: HotkeyCallback,
31
+ modifiers: HotkeyModifiers = {},
32
+ options: HotkeyOptions = {},
33
+ ) => {
34
+ const callbackRef = useRef(callback)
35
+ const {
36
+ preventDefault = true,
37
+ stopPropagation = false,
38
+ target = document,
39
+ } = options
40
+
41
+ useEffect(() => {
42
+ callbackRef.current = callback
43
+ }, [callback])
44
+
45
+ useEffect(() => {
46
+ if (!target) return
47
+
48
+ const handleKeyDown = (event: KeyboardEvent) => {
49
+ const keyMatches = event.key.toLowerCase() === key.toLowerCase()
50
+
51
+ // Check that all required modifiers are pressed
52
+ const ctrlOk = modifiers.ctrl ? event.ctrlKey : true
53
+ const altOk = modifiers.alt ? event.altKey : true
54
+ const shiftOk = modifiers.shift ? event.shiftKey : true
55
+ const metaOk = modifiers.meta ? event.metaKey || event.ctrlKey : true
56
+
57
+ if (keyMatches && ctrlOk && altOk && shiftOk && metaOk) {
58
+ if (preventDefault) {
59
+ event.preventDefault()
60
+ }
61
+ if (stopPropagation) {
62
+ event.stopPropagation()
63
+ }
64
+ callbackRef.current(event)
65
+ }
66
+ }
67
+
68
+ target.addEventListener("keydown", handleKeyDown as EventListener)
69
+ return () =>
70
+ target.removeEventListener("keydown", handleKeyDown as EventListener)
71
+ }, [
72
+ key,
73
+ modifiers.ctrl,
74
+ modifiers.alt,
75
+ modifiers.shift,
76
+ modifiers.meta,
77
+ preventDefault,
78
+ stopPropagation,
79
+ target,
80
+ ])
81
+ }
82
+
83
+ export const useHotkeyCombo = (
84
+ combo: string,
85
+ callback: HotkeyCallback,
86
+ options: HotkeyOptions = {},
87
+ ) => {
88
+ const modifiers: HotkeyModifiers = {}
89
+ const parts = combo
90
+ .toLowerCase()
91
+ .split("+")
92
+ .map((part) => part.trim())
93
+
94
+ const keyPart = parts[parts.length - 1]
95
+ const modifierParts = parts.slice(0, -1)
96
+
97
+ modifierParts.forEach((part) => {
98
+ switch (part) {
99
+ case "ctrl":
100
+ modifiers.ctrl = true
101
+ break
102
+ case "cmd":
103
+ case "meta":
104
+ modifiers.meta = true
105
+ break
106
+ case "alt":
107
+ modifiers.alt = true
108
+ break
109
+ case "shift":
110
+ modifiers.shift = true
111
+ break
112
+ }
113
+ })
114
+
115
+ useHotkey(keyPart, callback, modifiers, options)
116
+ }
@@ -18,6 +18,7 @@ export const usePackageById = (packageId: string | null) => {
18
18
  {
19
19
  retry: false,
20
20
  enabled: Boolean(packageId),
21
+ refetchOnWindowFocus: false,
21
22
  },
22
23
  )
23
24
  }
@@ -18,6 +18,7 @@ export const usePackageByName = (packageName: string | null) => {
18
18
  {
19
19
  retry: false,
20
20
  enabled: Boolean(packageName),
21
+ refetchOnWindowFocus: false,
21
22
  },
22
23
  )
23
24
  }
@@ -47,6 +47,7 @@ export const usePackageFile = (
47
47
  {
48
48
  retry: false,
49
49
  enabled: Boolean(query),
50
+ refetchOnWindowFocus: false,
50
51
  ...(opts as any),
51
52
  },
52
53
  )
@@ -112,6 +113,8 @@ export const usePackageFiles = (packageReleaseId?: string | null) => {
112
113
  },
113
114
  {
114
115
  enabled: Boolean(packageReleaseId),
116
+ refetchOnWindowFocus: false,
117
+ staleTime: 0,
115
118
  },
116
119
  )
117
120
  }
@@ -25,7 +25,10 @@ type PackageReleaseQuery = (
25
25
  export const usePackageRelease = (
26
26
  query: PackageReleaseQuery | null,
27
27
  options?: {
28
- refetchInterval?: number
28
+ refetchInterval?:
29
+ | number
30
+ | false
31
+ | ((data: PackageRelease | undefined) => number | false)
29
32
  },
30
33
  ) => {
31
34
  const axios = useAxios()
@@ -52,6 +55,7 @@ export const usePackageRelease = (
52
55
  retry: false,
53
56
  enabled: Boolean(query),
54
57
  refetchInterval: options?.refetchInterval,
58
+ refetchOnWindowFocus: false,
55
59
  },
56
60
  )
57
61
  }
@@ -18,6 +18,7 @@ export const usePackage = (packageId: string | null) => {
18
18
  {
19
19
  enabled: Boolean(packageId),
20
20
  retry: false,
21
+ refetchOnWindowFocus: false,
21
22
  },
22
23
  )
23
24
  }
@@ -1,35 +1,43 @@
1
1
  import { useMutation, useQueryClient } from "react-query"
2
2
  import { useAxios } from "./use-axios"
3
3
  import { useToast } from "./use-toast"
4
- import type { PackageRelease } from "fake-snippets-api/lib/db/schema"
4
+ import type { AiReview, PackageRelease } from "fake-snippets-api/lib/db/schema"
5
5
 
6
6
  export const useRequestAiReviewMutation = ({
7
7
  onSuccess,
8
- }: { onSuccess?: (packageRelease: PackageRelease) => void } = {}) => {
8
+ }: {
9
+ onSuccess?: (packageRelease: PackageRelease, aiReview: AiReview) => void
10
+ } = {}) => {
9
11
  const axios = useAxios()
10
12
  const { toast } = useToast()
11
13
  const queryClient = useQueryClient()
12
14
 
13
15
  return useMutation(
14
16
  async ({ package_release_id }: { package_release_id: string }) => {
15
- await axios.post("/ai_reviews/create", {
17
+ const { data: createData } = await axios.post("/ai_reviews/create", {
16
18
  package_release_id,
17
19
  })
20
+ const ai_review = createData.ai_review as AiReview
21
+
18
22
  const { data } = await axios.post(
19
23
  "/package_releases/get",
20
24
  { package_release_id },
21
25
  { params: { include_ai_review: true } },
22
26
  )
23
- return data.package_release as PackageRelease
27
+
28
+ return {
29
+ package_release: data.package_release as PackageRelease,
30
+ ai_review,
31
+ }
24
32
  },
25
33
  {
26
- onSuccess: (packageRelease) => {
34
+ onSuccess: ({ package_release, ai_review }) => {
27
35
  toast({
28
36
  title: "AI review requested",
29
37
  description: "An AI review has been generated.",
30
38
  })
31
39
  queryClient.invalidateQueries(["packageRelease"])
32
- onSuccess?.(packageRelease)
40
+ onSuccess?.(package_release, ai_review)
33
41
  },
34
42
  onError: (error: any) => {
35
43
  toast({
@@ -18,6 +18,7 @@ export const useSnippet = (snippetId: string | null) => {
18
18
  {
19
19
  enabled: Boolean(snippetId),
20
20
  retry: false,
21
+ refetchOnWindowFocus: false,
21
22
  },
22
23
  )
23
24
  }
@@ -8,6 +8,7 @@ import {
8
8
  import { Package } from "fake-snippets-api/lib/db/schema"
9
9
  import { usePackageFiles } from "./use-package-files"
10
10
  import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
11
+ import { decodeUrlHashToFsMap } from "@/lib/decodeUrlHashToFsMap"
11
12
  import { usePackageFilesLoader } from "./usePackageFilesLoader"
12
13
  import { useGlobalStore } from "./use-global-store"
13
14
  import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
@@ -15,7 +16,7 @@ import { useUpdatePackageFilesMutation } from "./useUpdatePackageFilesMutation"
15
16
  import { useCreatePackageReleaseMutation } from "./use-create-package-release-mutation"
16
17
  import { useCreatePackageMutation } from "./use-create-package-mutation"
17
18
  import { findTargetFile } from "@/lib/utils/findTargetFile"
18
- import { createSnippetUrl } from "@tscircuit/create-snippet-url"
19
+ import { encodeFsMapToUrlHash } from "@/lib/encodeFsMapToUrlHash"
19
20
 
20
21
  export interface ICreateFileProps {
21
22
  newFileName: string
@@ -117,6 +118,22 @@ export function useFileManagement({
117
118
 
118
119
  useEffect(() => {
119
120
  if (!currentPackage || isLoadingPackageFilesWithContent) {
121
+ const decodedFsMap = decodeUrlHashToFsMap(window.location.toString())
122
+
123
+ if (decodedFsMap && Object.keys(decodedFsMap).length > 0) {
124
+ const filesFromUrl = Object.entries(decodedFsMap).map(
125
+ ([path, content]) => ({
126
+ path,
127
+ content: String(content),
128
+ }),
129
+ )
130
+ const targetFile = findTargetFile(filesFromUrl, fileChoosen)
131
+ setLocalFiles(filesFromUrl)
132
+ setInitialFiles([])
133
+ setCurrentFile(targetFile?.path || filesFromUrl[0]?.path || null)
134
+ return
135
+ }
136
+
120
137
  setLocalFiles([
121
138
  {
122
139
  path: "index.tsx",
@@ -243,14 +260,15 @@ export function useFileManagement({
243
260
 
244
261
  debounceTimeoutRef.current = setTimeout(() => {
245
262
  try {
246
- const mainFile =
247
- files.find((f) => f.path === currentFile) ||
248
- files.find((f) => f.path === "index.tsx") ||
249
- files[0]
250
-
251
- if (mainFile.content.length > 50000) return
263
+ const map = files.reduce(
264
+ (acc, f) => {
265
+ acc[f.path] = f.content || ""
266
+ return acc
267
+ },
268
+ {} as Record<string, string>,
269
+ )
252
270
 
253
- const snippetUrl = createSnippetUrl(mainFile.content)
271
+ const snippetUrl = encodeFsMapToUrlHash(map)
254
272
  if (typeof snippetUrl !== "string") return
255
273
 
256
274
  const currentUrl = new URL(window.location.href)
@@ -36,7 +36,9 @@ export function usePackageFilesLoader(pkg?: Package) {
36
36
  const content = response.data.package_file?.content_text
37
37
  return { path: file.file_path, content: content ?? "" }
38
38
  },
39
- staleTime: 2,
39
+ refetchOnWindowFocus: false,
40
+ refetchOnMount: false,
41
+ cacheTime: 0,
40
42
  })) ?? [],
41
43
  )
42
44