@tscircuit/fake-snippets 0.0.80 → 0.0.82

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 (37) hide show
  1. package/bun-tests/fake-snippets-api/routes/package_releases/create.test.ts +6 -0
  2. package/bun-tests/fake-snippets-api/routes/package_releases/update.test.ts +3 -0
  3. package/bun.lock +24 -18
  4. package/dist/bundle.js +25 -9
  5. package/dist/index.d.ts +10 -0
  6. package/dist/index.js +4 -1
  7. package/dist/schema.d.ts +16 -0
  8. package/dist/schema.js +4 -1
  9. package/fake-snippets-api/lib/db/schema.ts +4 -0
  10. package/fake-snippets-api/routes/api/package_releases/update.ts +14 -1
  11. package/fake-snippets-api/routes/api/packages/generate_from_jlcpcb.ts +3 -3
  12. package/package.json +5 -5
  13. package/src/components/JLCPCBImportDialog.tsx +164 -62
  14. package/src/components/PackageBuildsPage/LogContent.tsx +12 -5
  15. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +8 -7
  16. package/src/components/PackageBuildsPage/build-preview-content.tsx +1 -1
  17. package/src/components/PackageBuildsPage/collapsible-section.tsx +14 -46
  18. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +28 -10
  19. package/src/components/PackageBuildsPage/package-build-header.tsx +14 -2
  20. package/src/components/ViewPackagePage/components/build-status.tsx +24 -85
  21. package/src/components/ViewPackagePage/components/important-files-view.tsx +65 -2
  22. package/src/components/ViewPackagePage/components/repo-page-content.tsx +13 -0
  23. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +28 -5
  24. package/src/components/package-port/CodeAndPreview.tsx +7 -1
  25. package/src/components/package-port/CodeEditor.tsx +9 -1
  26. package/src/hooks/use-current-package-release.ts +4 -2
  27. package/src/hooks/use-now.ts +12 -0
  28. package/src/hooks/use-package-release.ts +3 -2
  29. package/src/hooks/use-rebuild-package-release-mutation.ts +41 -0
  30. package/src/hooks/use-request-ai-review-mutation.ts +45 -0
  31. package/src/hooks/useFileManagement.ts +1 -2
  32. package/src/lib/codemirror/basic-setup.ts +17 -1
  33. package/src/pages/dashboard.tsx +3 -1
  34. package/src/pages/editor.tsx +5 -1
  35. package/src/pages/user-profile.tsx +9 -2
  36. package/src/pages/view-package.tsx +1 -0
  37. package/.github/workflows/formatbot.yml +0 -63
@@ -1,6 +1,5 @@
1
1
  import type React from "react"
2
- import { ChevronRight, CheckCircle2 } from "lucide-react"
3
- import { Badge } from "@/components/ui/badge"
2
+ import { ChevronRight } from "lucide-react"
4
3
  import {
5
4
  Collapsible,
6
5
  CollapsibleContent,
@@ -8,16 +7,9 @@ import {
8
7
  } from "@/components/ui/collapsible"
9
8
  import { getColorForDisplayStatus } from "./getColorForDisplayStatus"
10
9
  import { PackageRelease } from "fake-snippets-api/lib/db/schema"
11
- import { ErrorObjectOrString, getErrorText } from "./ErrorObject"
10
+ import { ErrorObjectOrString } from "./ErrorObject"
12
11
  import { capitalCase } from "./capitalCase"
13
12
 
14
- type BadgeInfo = {
15
- text: string
16
- variant?: "default" | "secondary" | "destructive"
17
- className?: string
18
- icon?: React.ReactNode
19
- }
20
-
21
13
  interface CollapsibleSectionProps {
22
14
  title: string
23
15
  duration?: string
@@ -25,69 +17,45 @@ interface CollapsibleSectionProps {
25
17
  displayStatus?: PackageRelease["display_status"]
26
18
  isOpen: boolean
27
19
  onToggle: () => void
28
- badges?: Array<BadgeInfo>
29
20
  children?: React.ReactNode
30
21
  }
31
22
 
32
23
  export function CollapsibleSection({
33
24
  title,
34
25
  duration,
35
- error,
36
26
  displayStatus,
37
27
  isOpen,
38
28
  onToggle,
39
- badges = [],
40
29
  children,
41
30
  }: CollapsibleSectionProps) {
42
31
  return (
43
32
  <Collapsible open={isOpen} onOpenChange={onToggle}>
44
33
  <CollapsibleTrigger asChild>
45
- <div className="flex items-center justify-between p-4 bg-white border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-100">
46
- <div className="flex items-center gap-2">
34
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-4 bg-white border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-100">
35
+ <div className="flex items-center gap-2 min-w-0">
47
36
  <ChevronRight
48
- className={`w-4 h-4 transition-transform ${isOpen ? "rotate-90" : ""}`}
37
+ className={`w-4 h-4 flex-shrink-0 transition-transform ${isOpen ? "rotate-90" : ""}`}
49
38
  />
50
- <span className="font-medium">{title}</span>
39
+ <span className="font-medium truncate">{title}</span>
51
40
  </div>
52
- <div className="flex items-center gap-2">
53
- {[
54
- ...badges,
55
- ...(error
56
- ? [
57
- {
58
- text: getErrorText(error),
59
- variant: "destructive",
60
- } as BadgeInfo,
61
- ]
62
- : []),
63
- ].map((badge, index) => (
64
- <Badge
65
- key={index}
66
- variant={badge.variant || "secondary"}
67
- className={
68
- badge.className ||
69
- "bg-gray-200 text-gray-700 flex items-center gap-1"
70
- }
71
- >
72
- {badge.icon}
73
- {badge.text}
74
- </Badge>
75
- ))}
41
+ <div className="flex items-center gap-2 flex-shrink-0 ml-6 sm:ml-0">
76
42
  {duration && (
77
- <span className="text-sm text-gray-600">{duration}</span>
43
+ <span className="text-sm text-gray-600 whitespace-nowrap">
44
+ {duration}
45
+ </span>
78
46
  )}
79
47
  <div
80
- className={`w-2 h-2 rounded-lg ${getColorForDisplayStatus(displayStatus)}`}
48
+ className={`w-2 h-2 rounded-lg flex-shrink-0 ${getColorForDisplayStatus(displayStatus)}`}
81
49
  />
82
- <div className="text-gray-600 text-xs font-medium">
50
+ <div className="text-gray-600 text-xs font-medium whitespace-nowrap">
83
51
  {capitalCase(displayStatus) || "???"}
84
52
  </div>
85
53
  </div>
86
54
  </div>
87
55
  </CollapsibleTrigger>
88
56
  <CollapsibleContent>
89
- <div className="p-4 bg-white border-x border-b border-gray-200 rounded-b-lg">
90
- {children}
57
+ <div className="bg-white border-x border-b border-gray-200 rounded-b-lg overflow-hidden">
58
+ <div className="p-4 overflow-x-auto max-w-full">{children}</div>
91
59
  </div>
92
60
  </CollapsibleContent>
93
61
  </Collapsible>
@@ -1,8 +1,9 @@
1
- import { Globe, GitBranch, GitCommit, Clock } from "lucide-react"
2
1
  import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
3
- import { useParams } from "wouter"
2
+ import { useNow } from "@/hooks/use-now"
4
3
  import { timeAgo } from "@/lib/utils/timeAgo"
5
4
  import { PackageRelease } from "fake-snippets-api/lib/db/schema"
5
+ import { Clock, GitBranch, GitCommit, Globe } from "lucide-react"
6
+ import { useParams } from "wouter"
6
7
 
7
8
  const capitalCase = (str: string) => {
8
9
  return str.charAt(0).toUpperCase() + str.slice(1)
@@ -24,8 +25,9 @@ function getColorFromDisplayStatus(
24
25
  }
25
26
 
26
27
  export function PackageBuildDetailsPanel() {
27
- const { packageRelease } = useCurrentPackageRelease()
28
+ const { packageRelease } = useCurrentPackageRelease({ refetchInterval: 2000 })
28
29
  const { author } = useParams() // TODO use packageRelease.author_account_id when it's added by backed
30
+ const now = useNow(1000)
29
31
 
30
32
  if (!packageRelease) {
31
33
  // TODO show skeleton instead
@@ -55,6 +57,24 @@ export function PackageBuildDetailsPanel() {
55
57
  commit_sha,
56
58
  } = packageRelease
57
59
 
60
+ const buildStartedAt = (() => {
61
+ if (transpilation_started_at && circuit_json_build_started_at) {
62
+ return new Date(transpilation_started_at) <
63
+ new Date(circuit_json_build_started_at)
64
+ ? transpilation_started_at
65
+ : circuit_json_build_started_at
66
+ }
67
+ return transpilation_started_at || circuit_json_build_started_at || null
68
+ })()
69
+
70
+ const buildCompletedAt =
71
+ circuit_json_build_completed_at || transpilation_completed_at || null
72
+
73
+ const elapsedMs = buildStartedAt
74
+ ? (buildCompletedAt ? new Date(buildCompletedAt).getTime() : now) -
75
+ new Date(buildStartedAt).getTime()
76
+ : null
77
+
58
78
  return (
59
79
  <div className="space-y-6 bg-white p-4 border border-gray-200 rounded-lg">
60
80
  {/* Created */}
@@ -92,15 +112,13 @@ export function PackageBuildDetailsPanel() {
92
112
  <h3 className="text-sm font-medium text-gray-600 mb-2">Build Time</h3>
93
113
  <div className="flex items-center gap-2">
94
114
  <Clock className="w-4 h-4 text-gray-500" />
95
- {circuit_json_build_completed_at && (
96
- <span className="text-sm">
97
- {total_build_duration_ms
98
- ? `${Math.floor(total_build_duration_ms / 1000)}s`
99
- : ""}
100
- </span>
115
+ {elapsedMs !== null && (
116
+ <span className="text-sm">{Math.floor(elapsedMs / 1000)}s</span>
101
117
  )}
102
118
  <span className="text-sm text-gray-500">
103
- {timeAgo(circuit_json_build_completed_at, "waiting...")}
119
+ {buildStartedAt
120
+ ? `Started ${timeAgo(buildStartedAt)}`
121
+ : "waiting..."}
104
122
  </span>
105
123
  </div>
106
124
  </div>
@@ -1,10 +1,15 @@
1
- import { Github, RefreshCw } from "lucide-react"
2
1
  import { Button } from "@/components/ui/button"
2
+ import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
3
+ import { useRebuildPackageReleaseMutation } from "@/hooks/use-rebuild-package-release-mutation"
4
+ import { Github, RefreshCw } from "lucide-react"
3
5
  import { useParams } from "wouter"
4
6
  import { DownloadButtonAndMenu } from "../DownloadButtonAndMenu"
5
7
 
6
8
  export function PackageBuildHeader() {
7
9
  const { author, packageName } = useParams()
10
+ const { packageRelease } = useCurrentPackageRelease({ refetchInterval: 2000 })
11
+ const { mutate: rebuildPackage, isLoading } =
12
+ useRebuildPackageReleaseMutation()
8
13
 
9
14
  return (
10
15
  <div className="border-b border-gray-200 bg-white px-6 py-4">
@@ -38,9 +43,16 @@ export function PackageBuildHeader() {
38
43
  variant="outline"
39
44
  size="sm"
40
45
  className="border-gray-300 bg-white hover:bg-gray-50 text-xs sm:text-sm"
46
+ disabled={isLoading || !packageRelease}
47
+ onClick={() =>
48
+ packageRelease &&
49
+ rebuildPackage({
50
+ package_release_id: packageRelease.package_release_id,
51
+ })
52
+ }
41
53
  >
42
54
  <RefreshCw className="w-3 h-3 sm:w-4 sm:h-4 mr-1 sm:mr-2" />
43
- Rebuild
55
+ {isLoading ? "Rebuilding..." : "Rebuild"}
44
56
  </Button>
45
57
  <DownloadButtonAndMenu
46
58
  snippetUnscopedName={`${author}/${packageName}`}
@@ -1,99 +1,38 @@
1
- import { useState } from "react"
2
- import {
3
- Dialog,
4
- DialogContent,
5
- DialogHeader,
6
- DialogTitle,
7
- } from "@/components/ui/dialog"
8
- import { CheckCircle, XCircle, Check, X } from "lucide-react"
9
- import { cn } from "@/lib/utils"
1
+ import { CheckCircle, XCircle, Clock, Loader2 } from "lucide-react"
2
+ import { Link, useParams } from "wouter"
10
3
 
11
4
  export interface BuildStep {
12
5
  id: string
13
6
  name: string
14
- status: "success" | "failed"
15
- message?: string
7
+ status: "pending" | "running" | "success" | "error"
16
8
  }
17
9
 
18
10
  export interface BuildStatusProps {
19
11
  step: BuildStep
12
+ packageReleaseId: string
20
13
  }
21
14
 
22
- export const BuildStatus = ({ step }: BuildStatusProps) => {
23
- const [isDialogOpen, setIsDialogOpen] = useState(false)
15
+ export const BuildStatus = ({ step, packageReleaseId }: BuildStatusProps) => {
16
+ const { author, packageName } = useParams()
17
+ const href = `/${author}/${packageName}/builds?package_release_id=${packageReleaseId}`
24
18
 
25
19
  return (
26
- <>
27
- <div
28
- onClick={() => setIsDialogOpen(true)}
29
- className={"flex items-center cursor-pointer"}
30
- >
31
- {step.status === "success" ? (
32
- <CheckCircle className="h-4 w-4 mr-2 text-green-600 dark:text-[#8b949e]" />
33
- ) : (
34
- <XCircle className="h-4 w-4 mr-2 text-red-600 dark:text-[#8b949e]" />
35
- )}
36
- <span className="text-sm text-gray-500 dark:text-[#8b949e]">
37
- {step.name}
38
- </span>
39
- </div>
40
-
41
- <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
42
- <DialogContent className="sm:max-w-md">
43
- <DialogHeader>
44
- <DialogTitle className="flex items-center gap-2">
45
- {step.status === "success" ? (
46
- <>
47
- <CheckCircle className="h-5 w-5 text-green-600" />
48
- <span>Build Status: Passing</span>
49
- </>
50
- ) : (
51
- <>
52
- <XCircle className="h-5 w-5 text-red-600" />
53
- <span>Build Status: Failing</span>
54
- </>
55
- )}
56
- </DialogTitle>
57
- </DialogHeader>
58
-
59
- <div className="space-y-4">
60
- <div className="space-y-3">
61
- <div
62
- key={step.id}
63
- className={cn(
64
- "flex items-start gap-3 rounded-md border p-3",
65
- step.status === "success"
66
- ? "bg-green-50 border-green-200"
67
- : "bg-red-50 border-red-200",
68
- )}
69
- >
70
- <div
71
- className={cn(
72
- "rounded-full p-1 mt-0.5",
73
- step.status === "success"
74
- ? "bg-green-100 text-green-600"
75
- : "bg-red-100 text-red-600",
76
- )}
77
- >
78
- {step.status === "success" ? (
79
- <Check className="h-4 w-4" />
80
- ) : (
81
- <X className="h-4 w-4" />
82
- )}
83
- </div>
84
- <div>
85
- <div className="font-medium">{step.name}</div>
86
- {step.message && (
87
- <div className="text-sm text-muted-foreground mt-1">
88
- {step.message}
89
- </div>
90
- )}
91
- </div>
92
- </div>
93
- </div>
94
- </div>
95
- </DialogContent>
96
- </Dialog>
97
- </>
20
+ <Link href={href} className="flex items-center gap-2">
21
+ {step.status === "success" && (
22
+ <CheckCircle className="h-4 w-4 text-green-600 dark:text-[#8b949e]" />
23
+ )}
24
+ {step.status === "error" && (
25
+ <XCircle className="h-4 w-4 text-red-600 dark:text-[#8b949e]" />
26
+ )}
27
+ {step.status === "running" && (
28
+ <Loader2 className="h-4 w-4 text-blue-600 animate-spin dark:text-[#8b949e]" />
29
+ )}
30
+ {step.status === "pending" && (
31
+ <Clock className="h-4 w-4 text-yellow-600 dark:text-[#8b949e]" />
32
+ )}
33
+ <span className="text-sm text-gray-500 dark:text-[#8b949e]">
34
+ {step.name}
35
+ </span>
36
+ </Link>
98
37
  )
99
38
  }
@@ -23,12 +23,18 @@ interface ImportantFilesViewProps {
23
23
 
24
24
  aiDescription?: string
25
25
  aiUsageInstructions?: string
26
+ aiReviewText?: string | null
27
+ aiReviewRequested?: boolean
28
+ onRequestAiReview?: () => void
26
29
  }
27
30
 
28
31
  export default function ImportantFilesView({
29
32
  importantFiles = [],
30
33
  aiDescription,
31
34
  aiUsageInstructions,
35
+ aiReviewText,
36
+ aiReviewRequested,
37
+ onRequestAiReview,
32
38
  isLoading = false,
33
39
  onEditClicked,
34
40
  }: ImportantFilesViewProps) {
@@ -43,9 +49,15 @@ export default function ImportantFilesView({
43
49
  }
44
50
  // Determine if we have AI content
45
51
  const hasAiContent = Boolean(aiDescription || aiUsageInstructions)
52
+ const hasAiReview = Boolean(aiReviewText)
46
53
 
47
- // Select the appropriate tab/file when content changes
54
+ // Select the appropriate tab/file when content changes. Once the user has
55
+ // interacted with the tabs we keep their selection and only run this logic
56
+ // if no tab has been chosen yet.
48
57
  useEffect(() => {
58
+ if (activeTab !== null) return
59
+ if (isLoading) return
60
+
49
61
  // First priority: README file if it exists
50
62
  const readmeFile = importantFiles.find(
51
63
  (file) =>
@@ -60,12 +72,24 @@ export default function ImportantFilesView({
60
72
  // Second priority: AI content if available
61
73
  setActiveTab("ai")
62
74
  setActiveFilePath(null)
75
+ } else if (hasAiReview) {
76
+ setActiveTab("ai-review")
77
+ setActiveFilePath(null)
63
78
  } else if (importantFiles.length > 0) {
64
79
  // Third priority: First important file
65
80
  setActiveFilePath(importantFiles[0].file_path)
66
81
  setActiveTab("file")
67
82
  }
68
- }, [aiDescription, aiUsageInstructions, hasAiContent, importantFiles])
83
+ }, [
84
+ aiDescription,
85
+ aiUsageInstructions,
86
+ aiReviewText,
87
+ hasAiContent,
88
+ hasAiReview,
89
+ importantFiles,
90
+ activeTab,
91
+ isLoading,
92
+ ])
69
93
 
70
94
  // Get file name from path
71
95
  const getFileName = (path: string) => {
@@ -106,6 +130,27 @@ export default function ImportantFilesView({
106
130
  )
107
131
  }
108
132
 
133
+ const renderAiReviewContent = () => {
134
+ if (!aiReviewText && !aiReviewRequested) {
135
+ return (
136
+ <button
137
+ className="text-sm text-blue-600 dark:text-[#58a6ff] underline"
138
+ onClick={onRequestAiReview}
139
+ >
140
+ Request AI Review
141
+ </button>
142
+ )
143
+ }
144
+
145
+ if (!aiReviewText && aiReviewRequested) {
146
+ return (
147
+ <p className="text-sm">AI review requested. Please check back later.</p>
148
+ )
149
+ }
150
+
151
+ return <MarkdownViewer markdownContent={aiReviewText || ""} />
152
+ }
153
+
109
154
  // Get active file content
110
155
  const partialActiveFile = importantFiles.find(
111
156
  (file) => file.file_path === activeFilePath,
@@ -187,6 +232,22 @@ export default function ImportantFilesView({
187
232
  </button>
188
233
  )}
189
234
 
235
+ {/* AI Review Tab */}
236
+ <button
237
+ className={`flex items-center px-3 py-1.5 rounded-md text-xs ${
238
+ activeTab === "ai-review"
239
+ ? "bg-gray-200 dark:bg-[#30363d] font-medium"
240
+ : "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
241
+ }`}
242
+ onClick={() => {
243
+ setActiveTab("ai-review")
244
+ setActiveFilePath(null)
245
+ }}
246
+ >
247
+ <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />
248
+ <span>AI Review</span>
249
+ </button>
250
+
190
251
  {/* File Tabs */}
191
252
  {importantFiles.map((file) => (
192
253
  <button
@@ -232,6 +293,8 @@ export default function ImportantFilesView({
232
293
  <div className="p-4 bg-white dark:bg-[#0d1117]">
233
294
  {activeTab === "ai" ? (
234
295
  renderAiContent()
296
+ ) : activeTab === "ai-review" ? (
297
+ renderAiReviewContent()
235
298
  ) : activeFilePath && activeFilePath.endsWith(".md") ? (
236
299
  <MarkdownViewer markdownContent={activeFileContent} />
237
300
  ) : activeFilePath &&
@@ -20,6 +20,7 @@ import { useGlobalStore } from "@/hooks/use-global-store"
20
20
  import { useLocation } from "wouter"
21
21
  import { Package } from "fake-snippets-api/lib/db/schema"
22
22
  import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
23
+ import { useRequestAiReviewMutation } from "@/hooks/use-request-ai-review-mutation"
23
24
 
24
25
  interface PackageFile {
25
26
  package_file_id: string
@@ -34,6 +35,7 @@ interface RepoPageContentProps {
34
35
  packageFiles?: PackageFile[]
35
36
  importantFilePaths?: string[]
36
37
  packageInfo?: Package
38
+ packageRelease?: import("fake-snippets-api/lib/db/schema").PackageRelease
37
39
  onFileClicked?: (file: PackageFile) => void
38
40
  onEditClicked?: () => void
39
41
  }
@@ -41,6 +43,7 @@ interface RepoPageContentProps {
41
43
  export default function RepoPageContent({
42
44
  packageFiles,
43
45
  packageInfo,
46
+ packageRelease,
44
47
  onFileClicked,
45
48
  onEditClicked,
46
49
  }: RepoPageContentProps) {
@@ -48,6 +51,7 @@ export default function RepoPageContent({
48
51
  const session = useGlobalStore((s) => s.session)
49
52
  const { circuitJson, isLoading: isCircuitJsonLoading } =
50
53
  useCurrentPackageCircuitJson()
54
+ const { mutate: requestAiReview } = useRequestAiReviewMutation()
51
55
 
52
56
  // Handle initial view selection and hash-based view changes
53
57
  useEffect(() => {
@@ -198,6 +202,15 @@ export default function RepoPageContent({
198
202
  onEditClicked={onEditClicked}
199
203
  aiDescription={packageInfo?.ai_description ?? ""}
200
204
  aiUsageInstructions={packageInfo?.ai_usage_instructions ?? ""}
205
+ aiReviewText={packageRelease?.ai_review_text ?? null}
206
+ aiReviewRequested={packageRelease?.ai_review_requested ?? false}
207
+ onRequestAiReview={() => {
208
+ if (packageRelease) {
209
+ requestAiReview({
210
+ package_release_id: packageRelease.package_release_id,
211
+ })
212
+ }
213
+ }}
201
214
  />
202
215
  </div>
203
216
 
@@ -4,6 +4,27 @@ import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
4
4
  import { usePackageReleaseById } from "@/hooks/use-package-release"
5
5
  import { timeAgo } from "@/lib/utils/timeAgo"
6
6
  import { BuildStatus, BuildStep } from "./build-status"
7
+ import type { PackageRelease } from "fake-snippets-api/lib/db/schema"
8
+
9
+ function getTranspilationStatus(
10
+ pr?: PackageRelease | null,
11
+ ): BuildStep["status"] {
12
+ if (!pr) return "pending"
13
+ if (pr.transpilation_error) return "error"
14
+ if (pr.transpilation_in_progress) return "running"
15
+ if (pr.transpilation_completed_at) return "success"
16
+ if (pr.transpilation_started_at) return "running"
17
+ return "pending"
18
+ }
19
+
20
+ function getCircuitJsonStatus(pr?: PackageRelease | null): BuildStep["status"] {
21
+ if (!pr) return "pending"
22
+ if (pr.circuit_json_build_error) return "error"
23
+ if (pr.circuit_json_build_in_progress) return "running"
24
+ if (pr.circuit_json_build_completed_at) return "success"
25
+ if (pr.circuit_json_build_started_at) return "running"
26
+ return "pending"
27
+ }
7
28
 
8
29
  export default function SidebarReleasesSection() {
9
30
  const { packageInfo } = useCurrentPackageInfo()
@@ -15,14 +36,12 @@ export default function SidebarReleasesSection() {
15
36
  {
16
37
  id: "package_transpilation",
17
38
  name: "Package Transpilation",
18
- status: packageRelease?.has_transpiled ? "success" : "failed",
19
- message: packageRelease?.transpilation_error || undefined,
39
+ status: getTranspilationStatus(packageRelease),
20
40
  },
21
41
  {
22
42
  id: "circuit_json_build",
23
43
  name: "Circuit JSON Build",
24
- status: packageRelease?.circuit_json_build_error ? "failed" : "success",
25
- message: packageRelease?.circuit_json_build_error || undefined,
44
+ status: getCircuitJsonStatus(packageRelease),
26
45
  },
27
46
  ]
28
47
 
@@ -56,7 +75,11 @@ export default function SidebarReleasesSection() {
56
75
  </span>
57
76
  </div>
58
77
  {buildSteps.map((step) => (
59
- <BuildStatus key={step.id} step={step} />
78
+ <BuildStatus
79
+ key={step.id}
80
+ step={step}
81
+ packageReleaseId={packageRelease.package_release_id}
82
+ />
60
83
  ))}
61
84
  </div>
62
85
  {/* <a href="#" className="text-blue-600 dark:text-[#58a6ff] hover:underline text-sm">
@@ -17,6 +17,11 @@ import { useFileManagement } from "@/hooks/useFileManagement"
17
17
 
18
18
  interface Props {
19
19
  pkg?: Package
20
+ /**
21
+ * Optional project URL whose pathname will be used when
22
+ * reporting autorouting bugs
23
+ */
24
+ projectUrl?: string
20
25
  }
21
26
 
22
27
  export interface PackageFile {
@@ -45,7 +50,7 @@ export default () => (
45
50
  export const generateRandomPackageName = () =>
46
51
  `untitled-package-${Math.floor(Math.random() * 900) + 100}`
47
52
 
48
- export function CodeAndPreview({ pkg }: Props) {
53
+ export function CodeAndPreview({ pkg, projectUrl }: Props) {
49
54
  const { toast } = useToast()
50
55
  const urlParams = useUrlParams()
51
56
 
@@ -243,6 +248,7 @@ export function CodeAndPreview({ pkg }: Props) {
243
248
  handleEditEvent(event)
244
249
  }}
245
250
  fsMap={fsMap ?? {}}
251
+ projectUrl={projectUrl}
246
252
  />
247
253
  </div>
248
254
  )}
@@ -4,7 +4,7 @@ import { autocompletion } from "@codemirror/autocomplete"
4
4
  import { indentWithTab } from "@codemirror/commands"
5
5
  import { javascript } from "@codemirror/lang-javascript"
6
6
  import { json } from "@codemirror/lang-json"
7
- import { EditorState } from "@codemirror/state"
7
+ import { EditorState, Prec } from "@codemirror/state"
8
8
  import { Decoration, hoverTooltip, keymap } from "@codemirror/view"
9
9
  import { getImportsFromCode } from "@tscircuit/prompt-benchmarks/code-runner-utils"
10
10
  import type { ATABootstrapConfig } from "@typescript/ata"
@@ -237,6 +237,14 @@ export const CodeEditor = ({
237
237
  currentFile?.endsWith(".json")
238
238
  ? json()
239
239
  : javascript({ typescript: true, jsx: true }),
240
+ Prec.high(
241
+ keymap.of([
242
+ {
243
+ key: "Mod-Enter",
244
+ run: () => true,
245
+ },
246
+ ]),
247
+ ),
240
248
  keymap.of([indentWithTab]),
241
249
  EditorState.readOnly.of(readOnly || isSaving),
242
250
  EditorView.updateListener.of((update) => {
@@ -1,10 +1,11 @@
1
1
  import { useParams } from "wouter"
2
2
  import { useCurrentPackageId } from "./use-current-package-id"
3
- import { useUrlParams } from "./use-url-params"
4
3
  import { usePackageRelease } from "./use-package-release"
4
+ import { useUrlParams } from "./use-url-params"
5
5
 
6
6
  export const useCurrentPackageRelease = (options?: {
7
- include_logs: boolean
7
+ include_logs?: boolean
8
+ refetchInterval?: number
8
9
  }) => {
9
10
  const { packageId } = useCurrentPackageId()
10
11
  const urlParams = useUrlParams()
@@ -27,6 +28,7 @@ export const useCurrentPackageRelease = (options?: {
27
28
 
28
29
  const { data: packageRelease, ...rest } = usePackageRelease(query, {
29
30
  include_logs: options?.include_logs ?? false,
31
+ refetchInterval: options?.refetchInterval,
30
32
  })
31
33
  return { packageRelease, ...rest }
32
34
  }
@@ -0,0 +1,12 @@
1
+ import { useEffect, useState } from "react"
2
+
3
+ export const useNow = (intervalMs: number = 1000) => {
4
+ const [now, setNow] = useState(Date.now())
5
+
6
+ useEffect(() => {
7
+ const id = setInterval(() => setNow(Date.now()), intervalMs)
8
+ return () => clearInterval(id)
9
+ }, [intervalMs])
10
+
11
+ return now
12
+ }
@@ -1,5 +1,5 @@
1
1
  import type { PackageRelease } from "fake-snippets-api/lib/db/schema"
2
- import { useQuery } from "react-query"
2
+ import { type UseQueryOptions, useQuery } from "react-query"
3
3
  import { useAxios } from "./use-axios"
4
4
 
5
5
  type PackageReleaseQuery =
@@ -20,7 +20,7 @@ type PackageReleaseQuery =
20
20
 
21
21
  export const usePackageRelease = (
22
22
  query: PackageReleaseQuery | null,
23
- options?: { include_logs: boolean },
23
+ options?: { include_logs?: boolean; refetchInterval?: number },
24
24
  ) => {
25
25
  const axios = useAxios()
26
26
 
@@ -50,6 +50,7 @@ export const usePackageRelease = (
50
50
  {
51
51
  retry: false,
52
52
  enabled: Boolean(query),
53
+ refetchInterval: options?.refetchInterval,
53
54
  },
54
55
  )
55
56
  }