@tscircuit/fake-snippets 0.0.83 → 0.0.84

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.
@@ -0,0 +1,22 @@
1
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2
+ import { z } from "zod"
3
+ import { aiReviewSchema } from "fake-snippets-api/lib/db/schema"
4
+
5
+ export default withRouteSpec({
6
+ methods: ["POST"],
7
+ auth: "session",
8
+ jsonResponse: z.object({
9
+ ai_review: aiReviewSchema,
10
+ }),
11
+ })(async (req, ctx) => {
12
+ const ai_review = ctx.db.addAiReview({
13
+ ai_review_text: null,
14
+ start_processing_at: null,
15
+ finished_processing_at: null,
16
+ processing_error: null,
17
+ created_at: new Date().toISOString(),
18
+ display_status: "pending",
19
+ })
20
+
21
+ return ctx.json({ ai_review })
22
+ })
@@ -0,0 +1,24 @@
1
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2
+ import { z } from "zod"
3
+ import { aiReviewSchema } from "fake-snippets-api/lib/db/schema"
4
+
5
+ export default withRouteSpec({
6
+ methods: ["GET"],
7
+ auth: "session",
8
+ queryParams: z.object({
9
+ ai_review_id: z.string(),
10
+ }),
11
+ jsonResponse: z.object({
12
+ ai_review: aiReviewSchema,
13
+ }),
14
+ })(async (req, ctx) => {
15
+ const { ai_review_id } = req.query
16
+ const ai_review = ctx.db.getAiReviewById(ai_review_id)
17
+ if (!ai_review) {
18
+ return ctx.error(404, {
19
+ error_code: "ai_review_not_found",
20
+ message: "AI review not found",
21
+ })
22
+ }
23
+ return ctx.json({ ai_review })
24
+ })
@@ -0,0 +1,14 @@
1
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2
+ import { z } from "zod"
3
+ import { aiReviewSchema } from "fake-snippets-api/lib/db/schema"
4
+
5
+ export default withRouteSpec({
6
+ methods: ["GET"],
7
+ auth: "session",
8
+ jsonResponse: z.object({
9
+ ai_reviews: z.array(aiReviewSchema),
10
+ }),
11
+ })(async (req, ctx) => {
12
+ const ai_reviews = ctx.db.listAiReviews()
13
+ return ctx.json({ ai_reviews })
14
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fake-snippets",
3
- "version": "0.0.83",
3
+ "version": "0.0.84",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -86,7 +86,7 @@
86
86
  "circuit-json-to-bom-csv": "^0.0.6",
87
87
  "circuit-json-to-gerber": "^0.0.21",
88
88
  "circuit-json-to-pnp-csv": "^0.0.6",
89
- "circuit-json-to-readable-netlist": "^0.0.8",
89
+ "circuit-json-to-readable-netlist": "^0.0.13",
90
90
  "circuit-json-to-tscircuit": "^0.0.4",
91
91
  "class-variance-authority": "^0.7.1",
92
92
  "clsx": "^2.1.1",
@@ -145,8 +145,9 @@
145
145
  "@playwright/test": "^1.48.0",
146
146
  "@tailwindcss/typography": "^0.5.16",
147
147
  "@tscircuit/core": "^0.0.433",
148
+ "@tscircuit/eval": "^0.0.227",
148
149
  "@tscircuit/prompt-benchmarks": "^0.0.28",
149
- "@tscircuit/runframe": "^0.0.562",
150
+ "@tscircuit/runframe": "^0.0.578",
150
151
  "@types/babel__standalone": "^7.1.7",
151
152
  "@types/bun": "^1.1.10",
152
153
  "@types/country-list": "^2.1.4",
@@ -64,7 +64,7 @@ export const ContextProviders = ({ children }: any) => {
64
64
  <HelmetProvider>
65
65
  <PostHogIdentifier />
66
66
  {children}
67
- <Toaster />
67
+ <Toaster position="bottom-right" />
68
68
  </HelmetProvider>
69
69
  </QueryClientProvider>
70
70
  )
@@ -15,18 +15,15 @@ const SearchButtonComponent = () => {
15
15
  return (
16
16
  <div className="relative">
17
17
  {isExpanded ? (
18
- <div className="flex items-center gap-2">
19
- <div className="w-32 bg-white">
20
- <SearchComponent autofocus />
18
+ <div className="flex items-center gap-2 ml-8">
19
+ <div className="absolute -top-4 right-3 bg-white">
20
+ <SearchComponent
21
+ autofocus
22
+ closeOnClick={() => {
23
+ setIsExpanded(false)
24
+ }}
25
+ />
21
26
  </div>
22
- {/* <Button
23
- variant="ghost"
24
- size="icon"
25
- onClick={() => setIsExpanded(false)}
26
- className="h-8 w-8"
27
- >
28
- <X className="h-4 w-4" />
29
- </Button> */}
30
27
  </div>
31
28
  ) : (
32
29
  <>
@@ -58,13 +55,6 @@ export const Header2 = () => {
58
55
  const isLoggedIn = useGlobalStore((state) => Boolean(state.session))
59
56
  return (
60
57
  <>
61
- {/* <div className="absolute left-0 top-0 z-[9999999]">
62
- <div className="hidden xl:block">xl</div>
63
- <div className="hidden lg:block xl:hidden">lg</div>
64
- <div className="hidden md:block lg:hidden">md</div>
65
- <div className="hidden sm:block md:hidden">sm</div>
66
- <div className="hidden xs:block sm:hidden">xs</div>
67
- </div> */}
68
58
  <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
69
59
  <div className="container mx-auto flex h-16 items-center justify-between px-2 md:px-6">
70
60
  <div className="flex items-center gap-2">
@@ -6,10 +6,12 @@ import { useQuery } from "react-query"
6
6
  import { Alert } from "./ui/alert"
7
7
  import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
8
8
  import { PrefetchPageLink } from "./PrefetchPageLink"
9
+ import { CircuitBoard } from "lucide-react"
9
10
 
10
11
  interface SearchComponentProps {
11
12
  onResultsFetched?: (results: any[]) => void
12
13
  autofocus?: boolean
14
+ closeOnClick?: () => void
13
15
  }
14
16
 
15
17
  const LinkWithNewTabHandling = ({
@@ -45,13 +47,14 @@ const LinkWithNewTabHandling = ({
45
47
  const SearchComponent: React.FC<SearchComponentProps> = ({
46
48
  onResultsFetched,
47
49
  autofocus = false,
50
+ closeOnClick,
48
51
  }) => {
49
52
  const [searchQuery, setSearchQuery] = useState("")
50
53
  const [showResults, setShowResults] = useState(false)
51
54
  const axios = useAxios()
52
55
  const resultsRef = useRef<HTMLDivElement>(null)
53
56
  const inputRef = useRef<HTMLInputElement>(null)
54
- const [location] = useLocation()
57
+ const [location, setLocation] = useLocation()
55
58
  const snippetsBaseApiUrl = useSnippetsBaseApiUrl()
56
59
 
57
60
  const { data: searchResults, isLoading } = useQuery(
@@ -71,7 +74,9 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
71
74
 
72
75
  const handleSearch = (e: React.FormEvent) => {
73
76
  e.preventDefault()
74
- setShowResults(!!searchQuery)
77
+ if (searchQuery.trim()) {
78
+ setLocation(`/search?q=${encodeURIComponent(searchQuery.trim())}`)
79
+ }
75
80
  }
76
81
 
77
82
  // Focus input on mount
@@ -91,18 +96,31 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
91
96
  }
92
97
  }
93
98
 
99
+ const handleEscapeKey = (event: KeyboardEvent) => {
100
+ if (event.key === "Escape") {
101
+ setShowResults(false)
102
+ if (closeOnClick) {
103
+ closeOnClick()
104
+ }
105
+ }
106
+ }
107
+
94
108
  document.addEventListener("mousedown", handleClickOutside)
109
+ document.addEventListener("keydown", handleEscapeKey)
95
110
  return () => {
96
111
  document.removeEventListener("mousedown", handleClickOutside)
112
+ document.removeEventListener("keydown", handleEscapeKey)
97
113
  }
98
- }, [])
114
+ }, [closeOnClick])
99
115
 
100
116
  const shouldOpenInNewTab = location === "/editor" || location === "/ai"
101
117
  const shouldOpenInEditor = location === "/editor" || location === "/ai"
102
118
 
103
119
  return (
104
- <form onSubmit={handleSearch} className="relative">
120
+ <form onSubmit={handleSearch} autoComplete="off" className="relative w-44">
105
121
  <Input
122
+ autoComplete="off"
123
+ spellCheck={false}
106
124
  ref={inputRef}
107
125
  type="search"
108
126
  placeholder="Search"
@@ -112,12 +130,20 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
112
130
  setSearchQuery(e.target.value)
113
131
  setShowResults(!!e.target.value)
114
132
  }}
133
+ onKeyDown={(e) => {
134
+ if (e.key === "Backspace" && !searchQuery && closeOnClick) {
135
+ closeOnClick()
136
+ }
137
+ }}
115
138
  aria-label="Search packages"
116
139
  role="searchbox"
117
140
  />
118
141
  {isLoading && (
119
- <div className="absolute top-full left-0 right-0 mt-2 bg-white shadow-lg rounded-md z-10 p-2 flex items-center justify-center space-x-2">
120
- <span className="text-gray-500 text-sm">Loading...</span>
142
+ <div className="absolute top-full w-lg left-0 right-0 mt-1 bg-white shadow-lg rounded-lg border w-80 grid place-items-center py-4 z-10 p-3">
143
+ <div className="flex items-center space-x-2">
144
+ <div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
145
+ <span className="text-gray-600 text-sm">Searching...</span>
146
+ </div>
121
147
  </div>
122
148
  )}
123
149
 
@@ -139,12 +165,24 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
139
165
  shouldOpenInNewTab={shouldOpenInNewTab}
140
166
  className="flex"
141
167
  >
142
- <div className="w-12 h-12 overflow-hidden mr-2 flex-shrink-0 rounded-sm">
168
+ <div className="w-12 h-12 overflow-hidden mr-2 flex-shrink-0 rounded-sm bg-gray-50 border flex items-center justify-center">
143
169
  <img
144
170
  src={`${snippetsBaseApiUrl}/snippets/images/${pkg.name}/pcb.svg`}
145
171
  alt={`PCB preview for ${pkg.name}`}
146
172
  className="w-12 h-12 object-contain p-1 scale-[4] rotate-45"
173
+ onError={(e) => {
174
+ e.currentTarget.style.display = "none"
175
+ e.currentTarget.nextElementSibling?.classList.remove(
176
+ "hidden",
177
+ )
178
+ e.currentTarget.nextElementSibling?.classList.add(
179
+ "flex",
180
+ )
181
+ }}
147
182
  />
183
+ <div className="w-12 h-12 hidden items-center justify-center">
184
+ <CircuitBoard className="w-6 h-6 text-gray-300" />
185
+ </div>
148
186
  </div>
149
187
  <div className="flex-grow">
150
188
  <div className="font-medium text-blue-600 break-words text-xs">
@@ -161,7 +199,7 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
161
199
  ))}
162
200
  </ul>
163
201
  ) : (
164
- <Alert variant="default" className="p-4">
202
+ <Alert variant="default" className="p-4 text-center">
165
203
  No results found for "{searchQuery}"
166
204
  </Alert>
167
205
  )}
@@ -63,18 +63,21 @@ export default function ViewSnippetHeader() {
63
63
  onSuccess?.(forkedSnippet)
64
64
  },
65
65
  onError: (error: any) => {
66
- // Check if the error message contains 'already exists'
67
- if (error.message?.includes("already forked")) {
66
+ const message =
67
+ error?.data?.error?.message ||
68
+ error.message ||
69
+ "Failed to fork snippet. Please try again."
70
+ if (message.includes("already forked")) {
68
71
  toast({
69
72
  title: "Snippet already exists",
70
- description: error.message,
71
- variant: "destructive", // You can style this variant differently
73
+ description: message,
74
+ variant: "destructive",
72
75
  })
73
76
  } else {
74
77
  toast({
75
78
  title: "Error",
76
- description: "Failed to fork snippet. Please try again.",
77
- variant: "destructive", // Use destructive variant for errors
79
+ description: message,
80
+ variant: "destructive",
78
81
  })
79
82
  }
80
83
  console.error("Error forking snippet:", error)
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from "react"
1
+ import { useState } from "react"
2
2
  import {
3
3
  Select,
4
4
  SelectContent,
@@ -110,12 +110,12 @@ export const EditPackageDetailsDialog = ({
110
110
 
111
111
  const response = await axios.post("/packages/update", {
112
112
  package_id: packageId,
113
- description: formData.description,
114
- website: formData.website,
113
+ description: formData.description.trim(),
114
+ website: formData.website.trim(),
115
115
  is_private: formData.visibility == "private",
116
116
  default_view: formData.defaultView,
117
117
  ...(formData.unscopedPackageName !== unscopedPackageName && {
118
- name: formData.unscopedPackageName,
118
+ name: formData.unscopedPackageName.trim(),
119
119
  }),
120
120
  })
121
121
  if (response.status !== 200)
@@ -148,12 +148,7 @@ export const EditPackageDetailsDialog = ({
148
148
  })
149
149
  }
150
150
  }
151
- console.log(
152
- "formData.unscopedPackageName",
153
- formData.unscopedPackageName,
154
- "unscopedPackageName",
155
- unscopedPackageName,
156
- )
151
+
157
152
  if (formData.unscopedPackageName !== unscopedPackageName) {
158
153
  // Use router for client-side navigation
159
154
  window.history.replaceState(
@@ -42,16 +42,17 @@ export const useForkPackageMutation = ({
42
42
  onSuccess?.(result)
43
43
  },
44
44
  onError: (error: any) => {
45
- if (error.data.error_code === "cannot_fork_own_package") {
45
+ const message = error?.data?.error?.message
46
+ if (error?.data?.error_code === "cannot_fork_own_package") {
46
47
  toast({
47
48
  title: "Cannot Fork Package",
48
- description: "You cannot fork your own package.",
49
+ description: message || "You cannot fork your own package.",
49
50
  })
50
51
  return
51
52
  }
52
53
  toast({
53
54
  title: "Error",
54
- description: "Failed to fork package. Please try again.",
55
+ description: message || "Failed to fork package. Please try again.",
55
56
  variant: "destructive",
56
57
  })
57
58
  },
@@ -7,16 +7,18 @@ export const useSignIn = () => {
7
7
  const isUsingFakeApi = useIsUsingFakeApi()
8
8
  const setSession = useGlobalStore((s) => s.setSession)
9
9
  return () => {
10
+ const currentUrl = window.location.href.replace("127.0.0.1", "localhost")
11
+ const nextUrl = `${window.location.origin.replace("127.0.0.1", "localhost")}/authorize?redirect=${encodeURIComponent(currentUrl)}`
10
12
  if (!isUsingFakeApi) {
11
- const nextUrl = window.location.origin.replace("127.0.0.1", "localhost")
12
- window.location.href = `${snippetsBaseApiUrl}/internal/oauth/github/authorize?next=${nextUrl}/authorize`
13
+ window.location.href = `${snippetsBaseApiUrl}/internal/oauth/github/authorize?next=${encodeURIComponent(nextUrl)}`
13
14
  } else {
14
- setSession({
15
- account_id: "account-1234",
16
- github_username: "testuser",
17
- token: "1234",
18
- session_id: "session-1234",
19
- })
15
+ window.location.href = nextUrl
16
+ // setSession({
17
+ // account_id: "account-1234",
18
+ // github_username: "testuser",
19
+ // token: "1234",
20
+ // session_id: "session-1234",
21
+ // })
20
22
  }
21
23
  }
22
24
  }
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useState, useCallback } from "react"
1
+ import { useEffect, useMemo, useState, useCallback, useRef } from "react"
2
2
  import { isValidFileName } from "@/lib/utils/isValidFileName"
3
3
  import {
4
4
  DEFAULT_CODE,
@@ -6,11 +6,7 @@ import {
6
6
  PackageFile,
7
7
  } from "../components/package-port/CodeAndPreview"
8
8
  import { Package } from "fake-snippets-api/lib/db/schema"
9
- import {
10
- usePackageFile,
11
- usePackageFileById,
12
- usePackageFiles,
13
- } from "./use-package-files"
9
+ import { usePackageFiles } from "./use-package-files"
14
10
  import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
15
11
  import { usePackageFilesLoader } from "./usePackageFilesLoader"
16
12
  import { useGlobalStore } from "./use-global-store"
@@ -19,6 +15,7 @@ import { useUpdatePackageFilesMutation } from "./useUpdatePackageFilesMutation"
19
15
  import { useCreatePackageReleaseMutation } from "./use-create-package-release-mutation"
20
16
  import { useCreatePackageMutation } from "./use-create-package-mutation"
21
17
  import { findTargetFile } from "@/lib/utils/findTargetFile"
18
+ import { createSnippetUrl } from "@tscircuit/create-snippet-url"
22
19
 
23
20
  export interface ICreateFileProps {
24
21
  newFileName: string
@@ -55,17 +52,19 @@ export function useFileManagement({
55
52
  const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
56
53
  const loggedInUser = useGlobalStore((s) => s.session)
57
54
  const { toast } = useToast()
55
+ const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
58
56
  const {
59
57
  data: packageFilesWithContent,
60
58
  isLoading: isLoadingPackageFilesWithContent,
61
59
  } = usePackageFilesLoader(currentPackage)
62
60
  const { data: packageFilesMeta, isLoading: isLoadingPackageFiles } =
63
61
  usePackageFiles(currentPackage?.latest_package_release_id)
64
-
65
62
  const initialCodeContent = useMemo(() => {
66
63
  return (
67
- templateCode ??
68
- (decodeUrlHashToText(window.location.toString()) || DEFAULT_CODE)
64
+ (!!decodeUrlHashToText(window.location.toString()) &&
65
+ decodeUrlHashToText(window.location.toString()) !== ""
66
+ ? decodeUrlHashToText(window.location.toString())
67
+ : templateCode) || DEFAULT_CODE
69
68
  )
70
69
  }, [templateCode, currentPackage])
71
70
  const manualEditsFileContent = useMemo(() => {
@@ -193,7 +192,8 @@ export function useFileManagement({
193
192
  { path: newFileName, content: "" },
194
193
  ]
195
194
  setLocalFiles(updatedFiles)
196
- onFileSelect(newFileName)
195
+ // immediately select the newly created file
196
+ setCurrentFile(newFileName)
197
197
  return {
198
198
  newFileCreated: true,
199
199
  }
@@ -233,14 +233,76 @@ export function useFileManagement({
233
233
  })
234
234
  }
235
235
 
236
+ const saveToUrl = useCallback(
237
+ (files: PackageFile[]) => {
238
+ if (isLoggedIn || !files.length) return
239
+
240
+ if (debounceTimeoutRef.current) {
241
+ clearTimeout(debounceTimeoutRef.current)
242
+ }
243
+
244
+ debounceTimeoutRef.current = setTimeout(() => {
245
+ 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
252
+
253
+ const snippetUrl = createSnippetUrl(mainFile.content)
254
+ if (typeof snippetUrl !== "string") return
255
+
256
+ const currentUrl = new URL(window.location.href)
257
+ const urlParts = snippetUrl.split("#")
258
+
259
+ if (urlParts.length > 1 && urlParts[1]) {
260
+ const newHash = urlParts[1]
261
+ if (newHash.length > 8000) return
262
+
263
+ currentUrl.hash = newHash
264
+ const finalUrl = currentUrl.toString()
265
+
266
+ if (finalUrl.length <= 32000) {
267
+ window.history.replaceState(null, "", finalUrl)
268
+ }
269
+ }
270
+ } catch (error) {
271
+ console.warn("Failed to save code to URL:", error)
272
+ }
273
+ }, 1000)
274
+ },
275
+ [isLoggedIn, currentFile],
276
+ )
277
+
278
+ useEffect(() => {
279
+ if (!isLoggedIn && localFiles.length > 0) {
280
+ saveToUrl(localFiles)
281
+ }
282
+
283
+ return () => {
284
+ if (debounceTimeoutRef.current) {
285
+ clearTimeout(debounceTimeoutRef.current)
286
+ }
287
+ }
288
+ }, [localFiles, saveToUrl, isLoggedIn])
289
+
236
290
  const saveFiles = () => {
237
291
  if (!isLoggedIn) {
292
+ // For non-logged-in users, trigger immediate URL save
293
+ if (debounceTimeoutRef.current) {
294
+ clearTimeout(debounceTimeoutRef.current)
295
+ }
296
+ saveToUrl(localFiles)
297
+
238
298
  toast({
239
- title: "Not Logged In",
240
- description: "You must be logged in to save your package.",
299
+ title: "Code Saved to URL",
300
+ description:
301
+ "Your code has been saved to the URL. Bookmark this page to access your code later.",
241
302
  })
242
303
  return
243
304
  }
305
+
244
306
  if (!currentPackage) {
245
307
  openNewPackageSaveDialog()
246
308
  return
@@ -38,9 +38,10 @@ export const useForkPackageMutation = ({
38
38
  },
39
39
  onError: (error: any) => {
40
40
  console.error("Error forking package:", error)
41
+ const message = error?.data?.error?.message
41
42
  toast({
42
43
  title: "Error",
43
- description: "Failed to fork package. Please try again.",
44
+ description: message || "Failed to fork package. Please try again.",
44
45
  variant: "destructive",
45
46
  })
46
47
  },
@@ -41,9 +41,10 @@ export const useForkSnippetMutation = ({
41
41
  },
42
42
  onError: (error: any) => {
43
43
  console.error("Error forking snippet:", error)
44
+ const message = error?.data?.error?.message
44
45
  toast({
45
46
  title: "Error",
46
- description: "Failed to fork snippet. Please try again.",
47
+ description: message || "Failed to fork snippet. Please try again.",
47
48
  variant: "destructive",
48
49
  })
49
50
  },