@tscircuit/fake-snippets 0.0.89 → 0.0.90
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.
- package/bun.lock +12 -12
- package/dist/bundle.js +492 -372
- package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +134 -0
- package/package.json +5 -3
- package/src/App.tsx +47 -2
- package/src/components/CircuitJsonImportDialog.tsx +1 -1
- package/src/components/PackageBuildsPage/package-build-header.tsx +2 -12
- package/src/components/package-port/CodeAndPreview.tsx +1 -16
- package/src/components/package-port/CodeEditor.tsx +25 -6
- package/src/components/package-port/CodeEditorHeader.tsx +9 -4
- package/src/components/package-port/EditorNav.tsx +1 -14
- package/src/components/package-port/GlobalFindReplace.tsx +1 -1
- package/src/components/package-port/QuickOpen.tsx +1 -1
- package/src/hooks/use-code-completion-ai-api.ts +3 -3
- package/src/hooks/use-delete-package.ts +6 -2
- package/src/hooks/useFileManagement.ts +2 -2
- package/src/lib/utils/findTargetFile.ts +1 -1
- package/src/lib/utils/package-utils.ts +10 -0
- package/src/pages/dashboard.tsx +10 -0
- package/src/types/package.ts +4 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import "dotenv/config"
|
|
2
|
+
import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
|
|
3
|
+
import { z } from "zod"
|
|
4
|
+
import OpenAI from "openai"
|
|
5
|
+
|
|
6
|
+
// Lazy-loaded client instance
|
|
7
|
+
let openai: OpenAI | null = null
|
|
8
|
+
let cachedReadme: string | null = null
|
|
9
|
+
|
|
10
|
+
function getOpenAIClient() {
|
|
11
|
+
const apiKey = process.env.VITE_OPENROUTER_API_KEY
|
|
12
|
+
if (!apiKey) {
|
|
13
|
+
throw new Error("Missing Api Key in env")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!openai) {
|
|
17
|
+
openai = new OpenAI({
|
|
18
|
+
apiKey,
|
|
19
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
20
|
+
defaultHeaders: {
|
|
21
|
+
"HTTP-Referer": "https://tscircuit.com",
|
|
22
|
+
"X-Title": "TSCircuit Editor",
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return openai
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Cache README
|
|
31
|
+
async function getCachedReadme(): Promise<string> {
|
|
32
|
+
if (cachedReadme !== null) return cachedReadme
|
|
33
|
+
const res = await fetch(
|
|
34
|
+
"https://raw.githubusercontent.com/tscircuit/props/main/README.md",
|
|
35
|
+
)
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
throw new Error(`Failed to fetch README: ${res.status}`)
|
|
38
|
+
}
|
|
39
|
+
cachedReadme = await res.text()
|
|
40
|
+
return cachedReadme
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function completion(
|
|
44
|
+
openai: OpenAI,
|
|
45
|
+
readmeContent: string,
|
|
46
|
+
prefix: string,
|
|
47
|
+
suffix: string,
|
|
48
|
+
model = "openai/gpt-4.1-mini",
|
|
49
|
+
language?: string,
|
|
50
|
+
) {
|
|
51
|
+
const systemMessage = `You are an expert ${language ? language + " " : ""}programmer working in a TSX (TypeScript + React JSX) environment.
|
|
52
|
+
|
|
53
|
+
Below is the README.md for the available components. You MUST use this to determine which components and props are valid.
|
|
54
|
+
Only use components explicitly documented under Available Components in the README. Never invent or guess new components. If the user partially types a component that does not exist in the README, do NOT try to complete it.
|
|
55
|
+
|
|
56
|
+
===== README.md START =====
|
|
57
|
+
${readmeContent}
|
|
58
|
+
===== README.md END =====
|
|
59
|
+
|
|
60
|
+
Special instruction for the <chip> component:
|
|
61
|
+
- Do NOT add chip as a prop (e.g., <chip chip="..."> is invalid).
|
|
62
|
+
- Always use this format:
|
|
63
|
+
<chip name="U<number>" footprint="<valid footprint>" pinLabels={{}} pcbX={0} pcbY={0} schX={0} schY={0} />
|
|
64
|
+
- Determine the next sequential name automatically: e.g. U1, U2, U3.
|
|
65
|
+
- Only use valid footprints and pinLabels from the README.
|
|
66
|
+
- Some components like <netlabel> do not have a 'name' prop — do not add it for those.
|
|
67
|
+
|
|
68
|
+
STRICT rules:
|
|
69
|
+
- If partial like "<capa", only append remaining "citor". Never repeat letters.
|
|
70
|
+
- If input is "<capacitor", add only props, never repeat tag.
|
|
71
|
+
- Always produce exactly one JSX component, starting with "<" if needed.
|
|
72
|
+
- If partial doesn’t match any valid component, output nothing.
|
|
73
|
+
- Never output two JSX elements. Always end with exactly one "/>".
|
|
74
|
+
- Never add duplicate closing "/>".
|
|
75
|
+
- Never output the component name as a prop.
|
|
76
|
+
- Never add whitespace before your completion.
|
|
77
|
+
- If the input is exactly "<", then start with the component name directly (like "resistor ... />") without adding another "<".
|
|
78
|
+
- So that the final result is "<resistor ... />", not "<<resistor ... />".
|
|
79
|
+
- Never produce a double "<".
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
- Input: "<FILL_ME>"
|
|
83
|
+
Output: <resistor name="R1" footprint="0603" pcbX={5} pcbY={7} schX={1} schY={2} resistance={1000} />
|
|
84
|
+
- Input: "<ca<FILL_ME>"
|
|
85
|
+
Output: pacitor name="C1" footprint="0805" pcbX={10} pcbY={15} schX={3} schY={4} />
|
|
86
|
+
- Input: "<chip<FILL_ME>"
|
|
87
|
+
Output: name="U1" footprint="SOIC-8" pinLabels={{}} pcbX={0} pcbY={0} schX={0} schY={0} />
|
|
88
|
+
- Input: "<netl<FILL_ME>"
|
|
89
|
+
Output: abel name="N1" />
|
|
90
|
+
- NEVER output: <capacitor capacitor ... /> or <netnet ... />
|
|
91
|
+
- Input: "<"
|
|
92
|
+
Output: resistor name="R1" footprint="0603" pcbX={5} pcbY={7} schX={1} schY={2} resistance={1000} />
|
|
93
|
+
- Input: "<ca"
|
|
94
|
+
Output: pacitor name="C1" footprint="0805" pcbX={10} pcbY={15} schX={3} schY={4} />
|
|
95
|
+
- Input: "<capacitor"
|
|
96
|
+
Output: capacitance="1000pF" footprint="0805" name="C1" pcbX={10} pcbY={15} schX={3} schY={4} />`
|
|
97
|
+
|
|
98
|
+
const chatCompletion = await openai.chat.completions.create({
|
|
99
|
+
messages: [
|
|
100
|
+
{ role: "system", content: systemMessage },
|
|
101
|
+
{ role: "user", content: `${prefix}<FILL_ME>${suffix}` },
|
|
102
|
+
],
|
|
103
|
+
model,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return chatCompletion.choices[0].message?.content ?? ""
|
|
107
|
+
}
|
|
108
|
+
export default withRouteSpec({
|
|
109
|
+
methods: ["POST"],
|
|
110
|
+
auth: "session", // ✅ Require user to be signed in
|
|
111
|
+
jsonBody: z.object({
|
|
112
|
+
prefix: z.string(),
|
|
113
|
+
suffix: z.string(),
|
|
114
|
+
model: z.string().optional(),
|
|
115
|
+
language: z.string().optional(),
|
|
116
|
+
}),
|
|
117
|
+
jsonResponse: z.object({
|
|
118
|
+
prediction: z.string(),
|
|
119
|
+
}),
|
|
120
|
+
})(async (req, ctx) => {
|
|
121
|
+
const openai = getOpenAIClient()
|
|
122
|
+
const { prefix, suffix, model, language } = req.jsonBody
|
|
123
|
+
|
|
124
|
+
const readmeContent = await getCachedReadme()
|
|
125
|
+
const predictionResult = await completion(
|
|
126
|
+
openai,
|
|
127
|
+
readmeContent,
|
|
128
|
+
prefix,
|
|
129
|
+
suffix,
|
|
130
|
+
model,
|
|
131
|
+
language,
|
|
132
|
+
)
|
|
133
|
+
return ctx.json({ prediction: predictionResult })
|
|
134
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/fake-snippets",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.90",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"@tscircuit/pcb-viewer": "^1.11.194",
|
|
88
88
|
"@tscircuit/prompt-benchmarks": "^0.0.28",
|
|
89
89
|
"@tscircuit/props": "^0.0.246",
|
|
90
|
-
"@tscircuit/runframe": "^0.0.
|
|
90
|
+
"@tscircuit/runframe": "^0.0.669",
|
|
91
91
|
"@tscircuit/schematic-viewer": "^2.0.21",
|
|
92
92
|
"@types/babel__standalone": "^7.1.7",
|
|
93
93
|
"@types/bun": "^1.1.10",
|
|
@@ -103,7 +103,6 @@
|
|
|
103
103
|
"@types/sharp": "^0.32.0",
|
|
104
104
|
"@typescript/ata": "^0.9.7",
|
|
105
105
|
"@typescript/vfs": "^1.6.0",
|
|
106
|
-
"@valtown/codemirror-codeium": "^1.1.1",
|
|
107
106
|
"@valtown/codemirror-ts": "^2.2.0",
|
|
108
107
|
"@vercel/analytics": "^1.4.1",
|
|
109
108
|
"@vitejs/plugin-react": "^4.3.1",
|
|
@@ -120,8 +119,10 @@
|
|
|
120
119
|
"clsx": "^2.1.1",
|
|
121
120
|
"cmdk": "^1.0.4",
|
|
122
121
|
"codemirror": "^6.0.1",
|
|
122
|
+
"codemirror-copilot": "^0.0.7",
|
|
123
123
|
"country-list": "^2.3.0",
|
|
124
124
|
"date-fns": "^4.1.0",
|
|
125
|
+
"dotenv": "^16.5.0",
|
|
125
126
|
"dsn-converter": "^0.0.60",
|
|
126
127
|
"easyeda": "^0.0.195",
|
|
127
128
|
"embla-carousel-react": "^8.3.0",
|
|
@@ -145,6 +146,7 @@
|
|
|
145
146
|
"md5": "^2.3.0",
|
|
146
147
|
"ms": "^2.1.3",
|
|
147
148
|
"next-themes": "^0.3.0",
|
|
149
|
+
"openai": "^5.6.0",
|
|
148
150
|
"postcss": "^8.4.47",
|
|
149
151
|
"posthog-js": "^1.203.2",
|
|
150
152
|
"prismjs": "^1.29.0",
|
package/src/App.tsx
CHANGED
|
@@ -80,6 +80,9 @@ class ErrorBoundary extends React.Component<
|
|
|
80
80
|
{ children: React.ReactNode },
|
|
81
81
|
{ hasError: boolean; reloading: boolean }
|
|
82
82
|
> {
|
|
83
|
+
private visibilityHandler?: () => void
|
|
84
|
+
private reloadTimeout?: number
|
|
85
|
+
|
|
83
86
|
constructor(props: { children: React.ReactNode }) {
|
|
84
87
|
super(props)
|
|
85
88
|
this.state = { hasError: false, reloading: false }
|
|
@@ -99,12 +102,54 @@ class ErrorBoundary extends React.Component<
|
|
|
99
102
|
) {
|
|
100
103
|
const loadedAt = window.__APP_LOADED_AT || Date.now()
|
|
101
104
|
if (Date.now() - loadedAt >= 10_000) {
|
|
102
|
-
this.
|
|
103
|
-
window.location.reload()
|
|
105
|
+
this.performReload()
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
componentDidUpdate(_prevProps: any, prevState: any) {
|
|
111
|
+
if (!prevState.hasError && this.state.hasError && !this.state.reloading) {
|
|
112
|
+
this.setupIdleReload()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
componentWillUnmount() {
|
|
117
|
+
this.cleanup()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
cleanup = () => {
|
|
121
|
+
if (this.visibilityHandler) {
|
|
122
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler)
|
|
123
|
+
this.visibilityHandler = undefined
|
|
124
|
+
}
|
|
125
|
+
if (this.reloadTimeout) {
|
|
126
|
+
clearTimeout(this.reloadTimeout)
|
|
127
|
+
this.reloadTimeout = undefined
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
performReload = () => {
|
|
132
|
+
if (this.state.reloading) return // Prevent multiple reloads
|
|
133
|
+
|
|
134
|
+
this.cleanup() // Clean up listeners before reload
|
|
135
|
+
this.setState({ reloading: true })
|
|
136
|
+
this.reloadTimeout = window.setTimeout(() => {
|
|
137
|
+
window.location.reload()
|
|
138
|
+
}, 500)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setupIdleReload = () => {
|
|
142
|
+
this.cleanup() // Clean up any existing handlers
|
|
143
|
+
|
|
144
|
+
this.visibilityHandler = () => {
|
|
145
|
+
if (!document.hidden && this.state.hasError && !this.state.reloading) {
|
|
146
|
+
this.performReload()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
document.addEventListener("visibilitychange", this.visibilityHandler)
|
|
151
|
+
}
|
|
152
|
+
|
|
108
153
|
render() {
|
|
109
154
|
if (this.state.reloading) {
|
|
110
155
|
return <div>There was a problem loading this page. Reloading…</div>
|
|
@@ -15,7 +15,7 @@ import { useLocation } from "wouter"
|
|
|
15
15
|
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
16
16
|
import { convertCircuitJsonToTscircuit } from "circuit-json-to-tscircuit"
|
|
17
17
|
import { useCreatePackageMutation } from "@/hooks/use-create-package-mutation"
|
|
18
|
-
import { generateRandomPackageName } from "
|
|
18
|
+
import { generateRandomPackageName } from "@/lib/utils/package-utils"
|
|
19
19
|
import { useCreatePackageReleaseMutation } from "@/hooks/use-create-package-release-mutation"
|
|
20
20
|
import { useCreatePackageFilesMutation } from "@/hooks/use-create-package-files-mutation"
|
|
21
21
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Button } from "@/components/ui/button"
|
|
2
2
|
import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
|
|
3
3
|
import { useRebuildPackageReleaseMutation } from "@/hooks/use-rebuild-package-release-mutation"
|
|
4
|
-
import { Github, RefreshCw
|
|
4
|
+
import { Github, RefreshCw } from "lucide-react"
|
|
5
5
|
import { useParams } from "wouter"
|
|
6
6
|
import { DownloadButtonAndMenu } from "../DownloadButtonAndMenu"
|
|
7
7
|
|
|
8
8
|
export function PackageBuildHeader() {
|
|
9
9
|
const { author, packageName } = useParams()
|
|
10
|
-
const { packageRelease
|
|
10
|
+
const { packageRelease } = useCurrentPackageRelease({
|
|
11
11
|
include_logs: true,
|
|
12
12
|
})
|
|
13
13
|
const { mutate: rebuildPackage, isLoading } =
|
|
@@ -59,16 +59,6 @@ export function PackageBuildHeader() {
|
|
|
59
59
|
<RefreshCw className="w-3 h-3 sm:w-4 sm:h-4 mr-1 sm:mr-2" />
|
|
60
60
|
{isLoading ? "Rebuilding..." : "Rebuild"}
|
|
61
61
|
</Button>
|
|
62
|
-
<Button
|
|
63
|
-
variant="outline"
|
|
64
|
-
size="icon"
|
|
65
|
-
aria-label="Reload logs"
|
|
66
|
-
className="border-gray-300 bg-white hover:bg-gray-50"
|
|
67
|
-
onClick={() => refetch()}
|
|
68
|
-
disabled={isFetching}
|
|
69
|
-
>
|
|
70
|
-
<RotateCcw className="w-3 h-3 sm:w-4 sm:h-4" />
|
|
71
|
-
</Button>
|
|
72
62
|
<DownloadButtonAndMenu unscopedName={packageName} author={author} />
|
|
73
63
|
</div>
|
|
74
64
|
</div>
|
|
@@ -15,6 +15,7 @@ import { applyEditEventsToManualEditsFile } from "@tscircuit/core"
|
|
|
15
15
|
import { toastManualEditConflicts } from "@/lib/utils/toastManualEditConflicts"
|
|
16
16
|
import { ManualEditEvent } from "@tscircuit/props"
|
|
17
17
|
import { useFileManagement } from "@/hooks/useFileManagement"
|
|
18
|
+
import { DEFAULT_CODE } from "@/lib/utils/package-utils"
|
|
18
19
|
|
|
19
20
|
interface Props {
|
|
20
21
|
pkg?: Package
|
|
@@ -25,11 +26,6 @@ interface Props {
|
|
|
25
26
|
projectUrl?: string
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export interface PackageFile {
|
|
29
|
-
path: string
|
|
30
|
-
content: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
29
|
export interface CodeAndPreviewState {
|
|
34
30
|
showPreview: boolean
|
|
35
31
|
fullScreen: boolean
|
|
@@ -40,17 +36,6 @@ export interface CodeAndPreviewState {
|
|
|
40
36
|
defaultComponentFile?: string
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
export const DEFAULT_CODE = `
|
|
44
|
-
export default () => (
|
|
45
|
-
<board width="10mm" height="10mm">
|
|
46
|
-
{/* write your code here! */}
|
|
47
|
-
</board>
|
|
48
|
-
)
|
|
49
|
-
`.trim()
|
|
50
|
-
|
|
51
|
-
export const generateRandomPackageName = () =>
|
|
52
|
-
`untitled-package-${Math.floor(Math.random() * 900) + 100}`
|
|
53
|
-
|
|
54
39
|
export function CodeAndPreview({ pkg, projectUrl }: Props) {
|
|
55
40
|
const { toast } = useToast()
|
|
56
41
|
const urlParams = useUrlParams()
|
|
@@ -32,7 +32,7 @@ import CodeEditorHeader, {
|
|
|
32
32
|
import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
|
|
33
33
|
import FileSidebar from "../FileSidebar"
|
|
34
34
|
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
35
|
-
import type { PackageFile } from "
|
|
35
|
+
import type { PackageFile } from "@/types/package"
|
|
36
36
|
import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
|
|
37
37
|
import QuickOpen from "./QuickOpen"
|
|
38
38
|
import GlobalFindReplace from "./GlobalFindReplace"
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
IDeleteFileResult,
|
|
44
44
|
} from "@/hooks/useFileManagement"
|
|
45
45
|
import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
|
|
46
|
+
import { inlineCopilot } from "codemirror-copilot"
|
|
46
47
|
|
|
47
48
|
const defaultImports = `
|
|
48
49
|
import React from "@types/react/jsx-runtime"
|
|
@@ -94,6 +95,7 @@ export const CodeEditor = ({
|
|
|
94
95
|
// Get URL search params for file_path
|
|
95
96
|
const urlParams = new URLSearchParams(window.location.search)
|
|
96
97
|
const filePathFromUrl = urlParams.get("file_path")
|
|
98
|
+
const [aiAutocompleteEnabled, setAiAutocompleteEnabled] = useState(false)
|
|
97
99
|
|
|
98
100
|
const entryPointFileName = useMemo(() => {
|
|
99
101
|
const entryPointFile = findTargetFile(files, null)
|
|
@@ -324,12 +326,24 @@ export const CodeEditor = ({
|
|
|
324
326
|
},
|
|
325
327
|
}),
|
|
326
328
|
]
|
|
327
|
-
if (
|
|
329
|
+
if (aiAutocompleteEnabled) {
|
|
328
330
|
baseExtensions.push(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
inlineCopilot(async (prefix, suffix) => {
|
|
332
|
+
const res = await fetch("/api/autocomplete/create_autocomplete", {
|
|
333
|
+
method: "POST",
|
|
334
|
+
headers: {
|
|
335
|
+
"Content-Type": "application/json",
|
|
336
|
+
},
|
|
337
|
+
body: JSON.stringify({
|
|
338
|
+
prefix,
|
|
339
|
+
suffix,
|
|
340
|
+
language: "typescript",
|
|
341
|
+
}),
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
const { prediction } = await res.json()
|
|
345
|
+
return prediction
|
|
346
|
+
}),
|
|
333
347
|
EditorView.theme({
|
|
334
348
|
".cm-ghostText, .cm-ghostText *": {
|
|
335
349
|
opacity: "0.6",
|
|
@@ -537,6 +551,7 @@ export const CodeEditor = ({
|
|
|
537
551
|
Boolean(highlighter),
|
|
538
552
|
isSaving,
|
|
539
553
|
fontSize,
|
|
554
|
+
aiAutocompleteEnabled,
|
|
540
555
|
])
|
|
541
556
|
|
|
542
557
|
const updateCurrentEditorContent = (newContent: string) => {
|
|
@@ -655,6 +670,10 @@ export const CodeEditor = ({
|
|
|
655
670
|
files={Object.fromEntries(files.map((f) => [f.path, f.content]))}
|
|
656
671
|
updateFileContent={updateFileContent}
|
|
657
672
|
handleFileChange={handleFileChange}
|
|
673
|
+
aiAutocompleteState={[
|
|
674
|
+
aiAutocompleteEnabled,
|
|
675
|
+
setAiAutocompleteEnabled,
|
|
676
|
+
]}
|
|
658
677
|
/>
|
|
659
678
|
)}
|
|
660
679
|
<div
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
TooltipProvider,
|
|
27
27
|
TooltipTrigger,
|
|
28
28
|
} from "@/components/ui/tooltip"
|
|
29
|
+
import ai from "fake-snippets-api/routes/api/ai"
|
|
29
30
|
|
|
30
31
|
export type FileName = string
|
|
31
32
|
|
|
@@ -36,6 +37,7 @@ interface CodeEditorHeaderProps {
|
|
|
36
37
|
fileSidebarState: ReturnType<typeof useState<boolean>>
|
|
37
38
|
handleFileChange: (filename: FileName) => void
|
|
38
39
|
entrypointFileName?: string
|
|
40
|
+
aiAutocompleteState: [boolean, React.Dispatch<React.SetStateAction<boolean>>]
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
@@ -45,12 +47,13 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
45
47
|
fileSidebarState,
|
|
46
48
|
handleFileChange,
|
|
47
49
|
entrypointFileName = "index.tsx",
|
|
50
|
+
aiAutocompleteState,
|
|
48
51
|
}) => {
|
|
49
52
|
const { Dialog: ImportPackageDialog, openDialog: openImportDialog } =
|
|
50
53
|
useImportPackageDialog()
|
|
51
54
|
const { toast } = useToast()
|
|
52
55
|
const [sidebarOpen, setSidebarOpen] = fileSidebarState
|
|
53
|
-
const [aiAutocompleteEnabled, setAiAutocompleteEnabled] =
|
|
56
|
+
const [aiAutocompleteEnabled, setAiAutocompleteEnabled] = aiAutocompleteState
|
|
54
57
|
|
|
55
58
|
const handleFormatFile = useCallback(() => {
|
|
56
59
|
if (!window.prettier || !window.prettierPlugins) return
|
|
@@ -239,15 +242,16 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
239
242
|
</DropdownMenuContent>
|
|
240
243
|
</DropdownMenu>
|
|
241
244
|
)}
|
|
245
|
+
|
|
242
246
|
<TooltipProvider>
|
|
243
247
|
<Tooltip>
|
|
244
248
|
<TooltipTrigger asChild>
|
|
245
249
|
<Button
|
|
246
250
|
size="sm"
|
|
247
251
|
variant="ghost"
|
|
248
|
-
onClick={() =>
|
|
249
|
-
setAiAutocompleteEnabled(!
|
|
250
|
-
}
|
|
252
|
+
onClick={() => {
|
|
253
|
+
setAiAutocompleteEnabled((prev) => !prev)
|
|
254
|
+
}}
|
|
251
255
|
className={`relative bg-transparent ${aiAutocompleteEnabled ? "text-gray-600 bg-gray-50" : "text-gray-400"}`}
|
|
252
256
|
>
|
|
253
257
|
<Bot className="h-4 w-4" />
|
|
@@ -263,6 +267,7 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
263
267
|
</TooltipContent>
|
|
264
268
|
</Tooltip>
|
|
265
269
|
</TooltipProvider>
|
|
270
|
+
|
|
266
271
|
<Button size="sm" variant="ghost" onClick={() => openImportDialog()}>
|
|
267
272
|
Import
|
|
268
273
|
</Button>
|
|
@@ -18,19 +18,16 @@ import { Package } from "fake-snippets-api/lib/db/schema"
|
|
|
18
18
|
import {
|
|
19
19
|
ChevronDown,
|
|
20
20
|
CodeIcon,
|
|
21
|
-
Download,
|
|
22
21
|
Edit2,
|
|
23
22
|
Eye,
|
|
24
23
|
EyeIcon,
|
|
25
24
|
File,
|
|
26
25
|
FilePenLine,
|
|
27
26
|
MoreVertical,
|
|
28
|
-
Package as PackageIcon,
|
|
29
27
|
Pencil,
|
|
30
28
|
Save,
|
|
31
29
|
Share,
|
|
32
30
|
Sidebar,
|
|
33
|
-
Sparkles,
|
|
34
31
|
Trash2,
|
|
35
32
|
Undo2,
|
|
36
33
|
} from "lucide-react"
|
|
@@ -41,7 +38,6 @@ import { useAxios } from "@/hooks/use-axios"
|
|
|
41
38
|
import { useHotkeyCombo } from "@/hooks/use-hotkey"
|
|
42
39
|
import { useToast } from "@/hooks/use-toast"
|
|
43
40
|
import { useConfirmDeletePackageDialog } from "@/components/dialogs/confirm-delete-package-dialog"
|
|
44
|
-
import { useFilesDialog } from "@/components/dialogs/files-dialog"
|
|
45
41
|
import { useViewTsFilesDialog } from "@/components/dialogs/view-ts-files-dialog"
|
|
46
42
|
import { DownloadButtonAndMenu } from "@/components/DownloadButtonAndMenu"
|
|
47
43
|
import { TypeBadge } from "@/components/TypeBadge"
|
|
@@ -86,7 +82,6 @@ export default function EditorNav({
|
|
|
86
82
|
} = useUpdatePackageDescriptionDialog()
|
|
87
83
|
const { Dialog: DeleteDialog, openDialog: openDeleteDialog } =
|
|
88
84
|
useConfirmDeletePackageDialog()
|
|
89
|
-
const { Dialog: FilesDialog, openDialog: openFilesDialog } = useFilesDialog()
|
|
90
85
|
const { Dialog: ViewTsFilesDialog, openDialog: openViewTsFilesDialog } =
|
|
91
86
|
useViewTsFilesDialog()
|
|
92
87
|
|
|
@@ -376,13 +371,6 @@ export default function EditorNav({
|
|
|
376
371
|
</Button>
|
|
377
372
|
</DropdownMenuTrigger>
|
|
378
373
|
<DropdownMenuContent>
|
|
379
|
-
<DropdownMenuItem
|
|
380
|
-
className="text-xs"
|
|
381
|
-
onClick={() => openFilesDialog()}
|
|
382
|
-
>
|
|
383
|
-
<File className="mr-2 h-3 w-3" />
|
|
384
|
-
View Files
|
|
385
|
-
</DropdownMenuItem>
|
|
386
374
|
<DropdownMenuItem
|
|
387
375
|
className="text-xs"
|
|
388
376
|
onClick={() => openupdateDescriptionDialog()}
|
|
@@ -395,7 +383,7 @@ export default function EditorNav({
|
|
|
395
383
|
onClick={() => openViewTsFilesDialog()}
|
|
396
384
|
>
|
|
397
385
|
<File className="mr-2 h-3 w-3" />
|
|
398
|
-
|
|
386
|
+
View Files
|
|
399
387
|
</DropdownMenuItem>
|
|
400
388
|
<DropdownMenuSub>
|
|
401
389
|
<DropdownMenuSubTrigger
|
|
@@ -566,7 +554,6 @@ export default function EditorNav({
|
|
|
566
554
|
packageName={pkg?.unscoped_name ?? ""}
|
|
567
555
|
packageOwner={pkg?.owner_github_username ?? ""}
|
|
568
556
|
/>
|
|
569
|
-
<FilesDialog snippetId={pkg?.package_id ?? ""} />
|
|
570
557
|
<ViewTsFilesDialog />
|
|
571
558
|
</nav>
|
|
572
559
|
)
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
TooltipProvider,
|
|
24
24
|
TooltipTrigger,
|
|
25
25
|
} from "@/components/ui/tooltip"
|
|
26
|
-
import type { PackageFile } from "
|
|
26
|
+
import type { PackageFile } from "@/types/package"
|
|
27
27
|
import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
|
|
28
28
|
|
|
29
29
|
interface Match {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
BookOpen,
|
|
9
9
|
FileText,
|
|
10
10
|
} from "lucide-react"
|
|
11
|
-
import type { PackageFile } from "
|
|
11
|
+
import type { PackageFile } from "@/types/package"
|
|
12
12
|
import { fuzzyMatch } from "../ViewPackagePage/utils/fuzz-search"
|
|
13
13
|
|
|
14
14
|
interface ScoredFile extends PackageFile {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useMemo } from "react"
|
|
2
2
|
|
|
3
3
|
export const useCodeCompletionApi = () => {
|
|
4
|
-
const
|
|
4
|
+
const openrouterApiKey = useMemo(() => {
|
|
5
5
|
return {
|
|
6
|
-
apiKey: import.meta.env.
|
|
6
|
+
apiKey: import.meta.env.VITE_OPENROUTER_API_KEY,
|
|
7
7
|
}
|
|
8
8
|
}, [])
|
|
9
9
|
|
|
10
|
-
return
|
|
10
|
+
return openrouterApiKey
|
|
11
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMutation } from "react-query"
|
|
1
|
+
import { useMutation, useQueryClient } from "react-query"
|
|
2
2
|
import { useAxios } from "./use-axios"
|
|
3
3
|
import { useToast } from "./use-toast"
|
|
4
4
|
|
|
@@ -7,6 +7,7 @@ export const useDeletePackage = ({
|
|
|
7
7
|
}: { onSuccess?: () => void } = {}) => {
|
|
8
8
|
const axios = useAxios()
|
|
9
9
|
const { toast } = useToast()
|
|
10
|
+
const queryClient = useQueryClient()
|
|
10
11
|
|
|
11
12
|
return useMutation(
|
|
12
13
|
["deletePackage"],
|
|
@@ -22,11 +23,14 @@ export const useDeletePackage = ({
|
|
|
22
23
|
return response.data
|
|
23
24
|
},
|
|
24
25
|
{
|
|
25
|
-
onSuccess: () => {
|
|
26
|
+
onSuccess: (_, variables) => {
|
|
26
27
|
toast({
|
|
27
28
|
title: "Package deleted",
|
|
28
29
|
description: "Package deleted successfully",
|
|
29
30
|
})
|
|
31
|
+
if (variables?.package_id) {
|
|
32
|
+
queryClient.invalidateQueries(["packages", variables.package_id])
|
|
33
|
+
}
|
|
30
34
|
onSuccess?.()
|
|
31
35
|
},
|
|
32
36
|
onError: (error: any) => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState, useCallback, useRef } from "react"
|
|
2
2
|
import { isValidFileName } from "@/lib/utils/isValidFileName"
|
|
3
|
+
import { PackageFile } from "@/types/package"
|
|
3
4
|
import {
|
|
4
5
|
DEFAULT_CODE,
|
|
5
6
|
generateRandomPackageName,
|
|
6
|
-
|
|
7
|
-
} from "../components/package-port/CodeAndPreview"
|
|
7
|
+
} from "@/lib/utils/package-utils"
|
|
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"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const DEFAULT_CODE = `
|
|
2
|
+
export default () => (
|
|
3
|
+
<board width="10mm" height="10mm">
|
|
4
|
+
{/* write your code here! */}
|
|
5
|
+
</board>
|
|
6
|
+
)
|
|
7
|
+
`.trim()
|
|
8
|
+
|
|
9
|
+
export const generateRandomPackageName = () =>
|
|
10
|
+
`untitled-package-${Math.floor(Math.random() * 900) + 100}`
|
package/src/pages/dashboard.tsx
CHANGED
|
@@ -61,6 +61,11 @@ export const DashboardPage = () => {
|
|
|
61
61
|
const response = await axios.get("/packages/list_trending")
|
|
62
62
|
return response.data.packages
|
|
63
63
|
},
|
|
64
|
+
{
|
|
65
|
+
refetchOnWindowFocus: false,
|
|
66
|
+
refetchOnMount: false,
|
|
67
|
+
refetchOnReconnect: false,
|
|
68
|
+
},
|
|
64
69
|
)
|
|
65
70
|
|
|
66
71
|
const { data: latestPackages } = useQuery<Package[]>(
|
|
@@ -69,6 +74,11 @@ export const DashboardPage = () => {
|
|
|
69
74
|
const response = await axios.get("/packages/list_latest")
|
|
70
75
|
return response.data.packages
|
|
71
76
|
},
|
|
77
|
+
{
|
|
78
|
+
refetchOnWindowFocus: false,
|
|
79
|
+
refetchOnMount: false,
|
|
80
|
+
refetchOnReconnect: false,
|
|
81
|
+
},
|
|
72
82
|
)
|
|
73
83
|
|
|
74
84
|
const baseUrl = useSnippetsBaseApiUrl()
|