@tscircuit/fake-snippets 0.0.99 → 0.0.101
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 +302 -808
- package/dist/bundle.js +466 -414
- package/dist/index.d.ts +4 -2
- package/dist/index.js +25 -0
- package/fake-snippets-api/lib/db/db-client.ts +32 -0
- package/fake-snippets-api/routes/api/accounts/search.ts +20 -0
- package/fake-snippets-api/routes/api/packages/create.ts +14 -3
- package/package.json +7 -5
- package/src/App.tsx +58 -2
- package/src/components/CircuitJsonImportDialog.tsx +10 -5
- package/src/components/CmdKMenu.tsx +154 -19
- package/src/components/Header2.tsx +106 -25
- package/src/components/PackageBuildsPage/package-build-header.tsx +19 -15
- package/src/components/ViewPackagePage/components/important-files-view.tsx +304 -172
- package/src/components/ViewPackagePage/components/main-content-header.tsx +2 -2
- package/src/components/ViewPackagePage/components/repo-page-content.tsx +9 -0
- package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +10 -3
- package/src/components/ViewPackagePage/components/sidebar.tsx +3 -1
- package/src/components/dialogs/edit-package-details-dialog.tsx +3 -2
- package/src/components/package-port/CodeAndPreview.tsx +1 -1
- package/src/components/package-port/CodeEditor.tsx +2 -2
- package/src/components/package-port/CodeEditorHeader.tsx +20 -16
- package/src/hooks/use-create-package-mutation.ts +1 -1
- package/src/hooks/useFileManagement.ts +1 -5
- package/src/lib/utils/package-utils.ts +0 -3
- package/src/pages/landing.tsx +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from "react"
|
|
3
|
+
import { useState, useEffect, useMemo, useCallback } from "react"
|
|
4
4
|
import {
|
|
5
5
|
Edit,
|
|
6
6
|
FileText,
|
|
@@ -9,14 +9,15 @@ import {
|
|
|
9
9
|
CopyCheck,
|
|
10
10
|
Loader2,
|
|
11
11
|
RefreshCcwIcon,
|
|
12
|
+
SparklesIcon,
|
|
12
13
|
} from "lucide-react"
|
|
13
14
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
14
15
|
import { Button } from "@/components/ui/button"
|
|
15
|
-
import { usePackageFile
|
|
16
|
+
import { usePackageFile } from "@/hooks/use-package-files"
|
|
16
17
|
import { ShikiCodeViewer } from "./ShikiCodeViewer"
|
|
17
|
-
import { SparklesIcon } from "lucide-react"
|
|
18
18
|
import MarkdownViewer from "./markdown-viewer"
|
|
19
19
|
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
20
|
+
import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
|
|
20
21
|
|
|
21
22
|
interface PackageFile {
|
|
22
23
|
package_file_id: string
|
|
@@ -36,6 +37,16 @@ interface ImportantFilesViewProps {
|
|
|
36
37
|
aiReviewText?: string | null
|
|
37
38
|
aiReviewRequested?: boolean
|
|
38
39
|
onRequestAiReview?: () => void
|
|
40
|
+
onLicenseFileRequested?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type TabType = "ai" | "ai-review" | "file"
|
|
44
|
+
|
|
45
|
+
interface TabInfo {
|
|
46
|
+
type: TabType
|
|
47
|
+
filePath?: string | null
|
|
48
|
+
label: string
|
|
49
|
+
icon: React.ReactNode
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
export default function ImportantFilesView({
|
|
@@ -48,89 +59,235 @@ export default function ImportantFilesView({
|
|
|
48
59
|
isLoading = false,
|
|
49
60
|
onEditClicked,
|
|
50
61
|
packageAuthorOwner,
|
|
62
|
+
onLicenseFileRequested,
|
|
51
63
|
}: ImportantFilesViewProps) {
|
|
52
|
-
const [
|
|
53
|
-
const [activeTab, setActiveTab] = useState<string | null>(null)
|
|
64
|
+
const [activeTab, setActiveTab] = useState<TabInfo | null>(null)
|
|
54
65
|
const [copyState, setCopyState] = useState<"copy" | "copied">("copy")
|
|
55
66
|
const { session: user } = useGlobalStore()
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
// Memoized computed values
|
|
69
|
+
const hasAiContent = useMemo(
|
|
70
|
+
() => Boolean(aiDescription || aiUsageInstructions),
|
|
71
|
+
[aiDescription, aiUsageInstructions],
|
|
72
|
+
)
|
|
73
|
+
const hasAiReview = useMemo(() => Boolean(aiReviewText), [aiReviewText])
|
|
74
|
+
const isOwner = useMemo(
|
|
75
|
+
() => user?.github_username === packageAuthorOwner,
|
|
76
|
+
[user?.github_username, packageAuthorOwner],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// File type utilities
|
|
80
|
+
const isLicenseFile = useCallback((filePath: string) => {
|
|
81
|
+
const lowerPath = filePath.toLowerCase()
|
|
82
|
+
return (
|
|
83
|
+
lowerPath === "license" ||
|
|
84
|
+
lowerPath.endsWith("/license") ||
|
|
85
|
+
lowerPath === "license.txt" ||
|
|
86
|
+
lowerPath.endsWith("/license.txt") ||
|
|
87
|
+
lowerPath === "license.md" ||
|
|
88
|
+
lowerPath.endsWith("/license.md")
|
|
89
|
+
)
|
|
90
|
+
}, [])
|
|
91
|
+
|
|
92
|
+
const isReadmeFile = useCallback((filePath: string) => {
|
|
93
|
+
const lowerPath = filePath.toLowerCase()
|
|
94
|
+
return lowerPath.endsWith("readme.md") || lowerPath.endsWith("readme")
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
const isCodeFile = useCallback((filePath: string) => {
|
|
98
|
+
return (
|
|
99
|
+
filePath.endsWith(".js") ||
|
|
100
|
+
filePath.endsWith(".jsx") ||
|
|
101
|
+
filePath.endsWith(".ts") ||
|
|
102
|
+
filePath.endsWith(".tsx")
|
|
103
|
+
)
|
|
104
|
+
}, [])
|
|
105
|
+
|
|
106
|
+
const isMarkdownFile = useCallback(
|
|
107
|
+
(filePath: string) => {
|
|
108
|
+
return filePath.endsWith(".md") || isReadmeFile(filePath)
|
|
109
|
+
},
|
|
110
|
+
[isReadmeFile],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const getFileName = useCallback((path: string) => {
|
|
114
|
+
const parts = path.split("/")
|
|
115
|
+
return parts[parts.length - 1]
|
|
116
|
+
}, [])
|
|
117
|
+
|
|
118
|
+
const getFileIcon = useCallback(
|
|
119
|
+
(path: string) => {
|
|
120
|
+
return isCodeFile(path) ? (
|
|
121
|
+
<Code className="h-3.5 w-3.5 mr-1.5" />
|
|
122
|
+
) : (
|
|
123
|
+
<FileText className="h-3.5 w-3.5 mr-1.5" />
|
|
124
|
+
)
|
|
125
|
+
},
|
|
126
|
+
[isCodeFile],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
// Available tabs computation
|
|
130
|
+
const availableTabs = useMemo((): TabInfo[] => {
|
|
131
|
+
const tabs: TabInfo[] = []
|
|
132
|
+
|
|
133
|
+
tabs.push({
|
|
134
|
+
type: "ai",
|
|
135
|
+
filePath: null,
|
|
136
|
+
label: "Description",
|
|
137
|
+
icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
tabs.push({
|
|
141
|
+
type: "ai-review",
|
|
142
|
+
filePath: null,
|
|
143
|
+
label: "AI Review",
|
|
144
|
+
icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
importantFiles.forEach((file) => {
|
|
148
|
+
tabs.push({
|
|
149
|
+
type: "file",
|
|
150
|
+
filePath: file.file_path,
|
|
151
|
+
label: getFileName(file.file_path),
|
|
152
|
+
icon: getFileIcon(file.file_path),
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
return tabs
|
|
157
|
+
}, [hasAiContent, importantFiles, getFileName, getFileIcon])
|
|
158
|
+
|
|
159
|
+
// Find default tab with fallback logic
|
|
160
|
+
const getDefaultTab = useCallback((): TabInfo | null => {
|
|
161
|
+
if (isLoading || availableTabs.length === 0) return null
|
|
162
|
+
|
|
163
|
+
// Priority 1: README file
|
|
164
|
+
const readmeTab = availableTabs.find(
|
|
165
|
+
(tab) =>
|
|
166
|
+
tab.type === "file" && tab.filePath && isReadmeFile(tab.filePath),
|
|
167
|
+
)
|
|
168
|
+
if (readmeTab) return readmeTab
|
|
169
|
+
|
|
170
|
+
// Priority 2: AI content
|
|
171
|
+
const aiTab = availableTabs.find((tab) => tab.type === "ai")
|
|
172
|
+
if (aiTab) return aiTab
|
|
173
|
+
|
|
174
|
+
// Priority 3: AI review
|
|
175
|
+
const aiReviewTab = availableTabs.find((tab) => tab.type === "ai-review")
|
|
176
|
+
if (aiReviewTab) return aiReviewTab
|
|
177
|
+
|
|
178
|
+
// Priority 4: First file
|
|
179
|
+
const firstFileTab = availableTabs.find((tab) => tab.type === "file")
|
|
180
|
+
if (firstFileTab) return firstFileTab
|
|
181
|
+
|
|
182
|
+
return null
|
|
183
|
+
}, [isLoading, availableTabs, isReadmeFile])
|
|
184
|
+
|
|
185
|
+
// Handle copy functionality
|
|
186
|
+
const handleCopy = useCallback(() => {
|
|
187
|
+
let textToCopy = ""
|
|
188
|
+
|
|
189
|
+
if (activeTab?.type === "ai-review" && aiReviewText) {
|
|
190
|
+
textToCopy = aiReviewText
|
|
191
|
+
} else if (activeTab?.type === "file" && activeFileContent) {
|
|
192
|
+
textToCopy = activeFileContent
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (textToCopy) {
|
|
196
|
+
navigator.clipboard.writeText(textToCopy)
|
|
60
197
|
setCopyState("copied")
|
|
61
198
|
setTimeout(() => setCopyState("copy"), 500)
|
|
62
|
-
return
|
|
63
199
|
}
|
|
64
|
-
|
|
65
|
-
setCopyState("copied")
|
|
66
|
-
setTimeout(() => setCopyState("copy"), 500)
|
|
67
|
-
}
|
|
68
|
-
// Determine if we have AI content
|
|
69
|
-
const hasAiContent = Boolean(aiDescription || aiUsageInstructions)
|
|
70
|
-
const hasAiReview = Boolean(aiReviewText)
|
|
200
|
+
}, [activeTab, aiReviewText])
|
|
71
201
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
202
|
+
// Handle tab selection with validation
|
|
203
|
+
const selectTab = useCallback(
|
|
204
|
+
(tab: TabInfo) => {
|
|
205
|
+
// Validate that the tab still exists (for file tabs)
|
|
206
|
+
if (tab.type === "file" && tab.filePath) {
|
|
207
|
+
const fileExists = importantFiles.some(
|
|
208
|
+
(file) => file.file_path === tab.filePath,
|
|
209
|
+
)
|
|
210
|
+
if (!fileExists) {
|
|
211
|
+
// File was deleted, fallback to default tab
|
|
212
|
+
const defaultTab = getDefaultTab()
|
|
213
|
+
setActiveTab(defaultTab)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
setActiveTab(tab)
|
|
218
|
+
},
|
|
219
|
+
[importantFiles, getDefaultTab],
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
// Handle license file request
|
|
75
223
|
useEffect(() => {
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
(file) =>
|
|
82
|
-
file.file_path.toLowerCase().endsWith("readme.md") ||
|
|
83
|
-
file.file_path.toLowerCase().endsWith("readme"),
|
|
84
|
-
)
|
|
224
|
+
if (onLicenseFileRequested && importantFiles.length > 0) {
|
|
225
|
+
const licenseTab = availableTabs.find(
|
|
226
|
+
(tab) =>
|
|
227
|
+
tab.type === "file" && tab.filePath && isLicenseFile(tab.filePath),
|
|
228
|
+
)
|
|
85
229
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} else if (hasAiReview) {
|
|
94
|
-
setActiveTab("ai-review")
|
|
95
|
-
setActiveFilePath(null)
|
|
96
|
-
} else if (importantFiles.length > 0) {
|
|
97
|
-
// Third priority: First important file
|
|
98
|
-
setActiveFilePath(importantFiles[0].file_path)
|
|
99
|
-
setActiveTab("file")
|
|
230
|
+
if (licenseTab) {
|
|
231
|
+
setActiveTab(licenseTab)
|
|
232
|
+
} else {
|
|
233
|
+
// License file not found, fallback to default
|
|
234
|
+
const defaultTab = getDefaultTab()
|
|
235
|
+
setActiveTab(defaultTab)
|
|
236
|
+
}
|
|
100
237
|
}
|
|
101
238
|
}, [
|
|
102
|
-
|
|
103
|
-
aiUsageInstructions,
|
|
104
|
-
aiReviewText,
|
|
105
|
-
hasAiContent,
|
|
106
|
-
hasAiReview,
|
|
239
|
+
onLicenseFileRequested,
|
|
107
240
|
importantFiles,
|
|
108
|
-
|
|
109
|
-
|
|
241
|
+
availableTabs,
|
|
242
|
+
isLicenseFile,
|
|
243
|
+
getDefaultTab,
|
|
110
244
|
])
|
|
111
245
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
246
|
+
// Set default tab when no tab is active
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (activeTab === null && !isLoading) {
|
|
249
|
+
const defaultTab = getDefaultTab()
|
|
250
|
+
setActiveTab(defaultTab)
|
|
251
|
+
}
|
|
252
|
+
}, [activeTab, isLoading, getDefaultTab])
|
|
117
253
|
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
254
|
+
// Validate active tab still exists (handles file deletion)
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (activeTab?.type === "file" && activeTab.filePath) {
|
|
257
|
+
const fileExists = importantFiles.some(
|
|
258
|
+
(file) => file.file_path === activeTab.filePath,
|
|
259
|
+
)
|
|
260
|
+
if (!fileExists) {
|
|
261
|
+
// Active file was deleted, fallback to default
|
|
262
|
+
const defaultTab = getDefaultTab()
|
|
263
|
+
setActiveTab(defaultTab)
|
|
264
|
+
}
|
|
127
265
|
}
|
|
128
|
-
|
|
129
|
-
}
|
|
266
|
+
}, [activeTab, importantFiles, getDefaultTab])
|
|
130
267
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
268
|
+
const { circuitJson } = useCurrentPackageCircuitJson()
|
|
269
|
+
|
|
270
|
+
// Get active file content
|
|
271
|
+
const partialActiveFile = useMemo(() => {
|
|
272
|
+
if (activeTab?.type !== "file" || !activeTab.filePath) return null
|
|
273
|
+
return importantFiles.find((file) => file.file_path === activeTab.filePath)
|
|
274
|
+
}, [activeTab, importantFiles])
|
|
275
|
+
|
|
276
|
+
const { data: activeFileFull } = usePackageFile(
|
|
277
|
+
partialActiveFile
|
|
278
|
+
? {
|
|
279
|
+
file_path: partialActiveFile.file_path,
|
|
280
|
+
package_release_id: partialActiveFile.package_release_id,
|
|
281
|
+
}
|
|
282
|
+
: null,
|
|
283
|
+
{ keepPreviousData: true },
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
const activeFileContent = activeFileFull?.content_text || ""
|
|
287
|
+
|
|
288
|
+
// Render content based on active tab
|
|
289
|
+
const renderAiContent = useCallback(
|
|
290
|
+
() => (
|
|
134
291
|
<div className="markdown-content">
|
|
135
292
|
{aiDescription && (
|
|
136
293
|
<div className="mb-6">
|
|
@@ -145,12 +302,11 @@ export default function ImportantFilesView({
|
|
|
145
302
|
</div>
|
|
146
303
|
)}
|
|
147
304
|
</div>
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const renderAiReviewContent = () => {
|
|
152
|
-
const isOwner = user?.github_username === packageAuthorOwner
|
|
305
|
+
),
|
|
306
|
+
[aiDescription, aiUsageInstructions],
|
|
307
|
+
)
|
|
153
308
|
|
|
309
|
+
const renderAiReviewContent = useCallback(() => {
|
|
154
310
|
if (!aiReviewText && !aiReviewRequested) {
|
|
155
311
|
return (
|
|
156
312
|
<div className="flex flex-col items-center justify-center py-8 px-4">
|
|
@@ -166,7 +322,15 @@ export default function ImportantFilesView({
|
|
|
166
322
|
from our AI assistant.
|
|
167
323
|
</p>
|
|
168
324
|
</div>
|
|
169
|
-
{isOwner ? (
|
|
325
|
+
{!isOwner ? (
|
|
326
|
+
<p className="text-sm text-gray-500">
|
|
327
|
+
Only the package owner can generate an AI review
|
|
328
|
+
</p>
|
|
329
|
+
) : !Boolean(circuitJson) ? (
|
|
330
|
+
<p className="text-sm text-gray-500">
|
|
331
|
+
Circuit JSON is required for AI review.
|
|
332
|
+
</p>
|
|
333
|
+
) : (
|
|
170
334
|
<Button
|
|
171
335
|
onClick={onRequestAiReview}
|
|
172
336
|
size="sm"
|
|
@@ -175,10 +339,6 @@ export default function ImportantFilesView({
|
|
|
175
339
|
<SparklesIcon className="h-4 w-4 mr-2" />
|
|
176
340
|
Request AI Review
|
|
177
341
|
</Button>
|
|
178
|
-
) : (
|
|
179
|
-
<p className="text-sm text-gray-500">
|
|
180
|
-
Only the package owner can generate an AI review
|
|
181
|
-
</p>
|
|
182
342
|
)}
|
|
183
343
|
</div>
|
|
184
344
|
</div>
|
|
@@ -206,22 +366,61 @@ export default function ImportantFilesView({
|
|
|
206
366
|
}
|
|
207
367
|
|
|
208
368
|
return <MarkdownViewer markdownContent={aiReviewText || ""} />
|
|
209
|
-
}
|
|
369
|
+
}, [aiReviewText, aiReviewRequested, isOwner, onRequestAiReview])
|
|
210
370
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
371
|
+
const renderFileContent = useCallback(() => {
|
|
372
|
+
if (!activeTab?.filePath || !activeFileContent) {
|
|
373
|
+
return <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (isMarkdownFile(activeTab.filePath)) {
|
|
377
|
+
return <MarkdownViewer markdownContent={activeFileContent} />
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (isCodeFile(activeTab.filePath)) {
|
|
381
|
+
return (
|
|
382
|
+
<div className="overflow-x-auto">
|
|
383
|
+
<ShikiCodeViewer
|
|
384
|
+
code={activeFileContent}
|
|
385
|
+
filePath={activeTab.filePath}
|
|
386
|
+
/>
|
|
387
|
+
</div>
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
|
|
392
|
+
}, [activeTab, activeFileContent, isMarkdownFile, isCodeFile])
|
|
393
|
+
|
|
394
|
+
const renderTabContent = useCallback(() => {
|
|
395
|
+
if (!activeTab) return null
|
|
396
|
+
|
|
397
|
+
switch (activeTab.type) {
|
|
398
|
+
case "ai":
|
|
399
|
+
return renderAiContent()
|
|
400
|
+
case "ai-review":
|
|
401
|
+
return renderAiReviewContent()
|
|
402
|
+
case "file":
|
|
403
|
+
return renderFileContent()
|
|
404
|
+
default:
|
|
405
|
+
return null
|
|
406
|
+
}
|
|
407
|
+
}, [activeTab, renderAiContent, renderAiReviewContent, renderFileContent])
|
|
408
|
+
|
|
409
|
+
// Tab styling helper
|
|
410
|
+
const getTabClassName = useCallback(
|
|
411
|
+
(tab: TabInfo) => {
|
|
412
|
+
const isActive =
|
|
413
|
+
activeTab?.type === tab.type &&
|
|
414
|
+
(tab.type !== "file" || activeTab?.filePath === tab.filePath)
|
|
415
|
+
|
|
416
|
+
return `flex items-center px-3 py-1.5 rounded-md text-xs flex-shrink-0 whitespace-nowrap ${
|
|
417
|
+
isActive
|
|
418
|
+
? "bg-gray-200 dark:bg-[#30363d] font-medium"
|
|
419
|
+
: "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
|
|
420
|
+
}`
|
|
421
|
+
},
|
|
422
|
+
[activeTab],
|
|
223
423
|
)
|
|
224
|
-
const activeFileContent = activeFileFull?.content_text || ""
|
|
225
424
|
|
|
226
425
|
if (isLoading) {
|
|
227
426
|
return (
|
|
@@ -249,7 +448,7 @@ export default function ImportantFilesView({
|
|
|
249
448
|
)
|
|
250
449
|
}
|
|
251
450
|
|
|
252
|
-
if (importantFiles.length === 0) {
|
|
451
|
+
if (importantFiles.length === 0 && !hasAiContent && !hasAiReview) {
|
|
253
452
|
return (
|
|
254
453
|
<div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
|
|
255
454
|
<div className="flex items-center pl-2 pr-4 py-2 bg-gray-100 dark:bg-[#161b22] border-b border-gray-200 dark:border-[#30363d]">
|
|
@@ -268,68 +467,24 @@ export default function ImportantFilesView({
|
|
|
268
467
|
)
|
|
269
468
|
}
|
|
270
469
|
|
|
271
|
-
const isOwner = user?.github_username === packageAuthorOwner
|
|
272
|
-
|
|
273
470
|
return (
|
|
274
471
|
<div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
|
|
275
472
|
<div className="flex items-center pl-2 pr-4 py-2 bg-gray-100 dark:bg-[#161b22] border-b border-gray-200 dark:border-[#30363d]">
|
|
276
473
|
<div className="flex items-center space-x-2 overflow-x-auto no-scrollbar flex-1 min-w-0">
|
|
277
|
-
{
|
|
278
|
-
{hasAiContent && (
|
|
279
|
-
<button
|
|
280
|
-
className={`flex items-center px-3 py-1.5 rounded-md text-xs flex-shrink-0 whitespace-nowrap ${
|
|
281
|
-
activeTab === "ai"
|
|
282
|
-
? "bg-gray-200 dark:bg-[#30363d] font-medium"
|
|
283
|
-
: "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
|
|
284
|
-
}`}
|
|
285
|
-
onClick={() => {
|
|
286
|
-
setActiveTab("ai")
|
|
287
|
-
setActiveFilePath(null)
|
|
288
|
-
}}
|
|
289
|
-
>
|
|
290
|
-
<SparklesIcon className="h-3.5 w-3.5 mr-1.5" />
|
|
291
|
-
<span>Description</span>
|
|
292
|
-
</button>
|
|
293
|
-
)}
|
|
294
|
-
|
|
295
|
-
{/* AI Review Tab */}
|
|
296
|
-
<button
|
|
297
|
-
className={`flex items-center px-3 py-1.5 rounded-md text-xs flex-shrink-0 whitespace-nowrap ${
|
|
298
|
-
activeTab === "ai-review"
|
|
299
|
-
? "bg-gray-200 dark:bg-[#30363d] font-medium"
|
|
300
|
-
: "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
|
|
301
|
-
}`}
|
|
302
|
-
onClick={() => {
|
|
303
|
-
setActiveTab("ai-review")
|
|
304
|
-
setActiveFilePath(null)
|
|
305
|
-
}}
|
|
306
|
-
>
|
|
307
|
-
<SparklesIcon className="h-3.5 w-3.5 mr-1.5" />
|
|
308
|
-
<span>AI Review</span>
|
|
309
|
-
</button>
|
|
310
|
-
|
|
311
|
-
{/* File Tabs */}
|
|
312
|
-
{importantFiles.map((file) => (
|
|
474
|
+
{availableTabs.map((tab, index) => (
|
|
313
475
|
<button
|
|
314
|
-
key={
|
|
315
|
-
className={
|
|
316
|
-
|
|
317
|
-
? "bg-gray-200 dark:bg-[#30363d] font-medium"
|
|
318
|
-
: "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
|
|
319
|
-
}`}
|
|
320
|
-
onClick={() => {
|
|
321
|
-
setActiveTab("file")
|
|
322
|
-
setActiveFilePath(file.file_path)
|
|
323
|
-
}}
|
|
476
|
+
key={`${tab.type}-${tab.filePath || index}`}
|
|
477
|
+
className={getTabClassName(tab)}
|
|
478
|
+
onClick={() => selectTab(tab)}
|
|
324
479
|
>
|
|
325
|
-
{
|
|
326
|
-
<span>{
|
|
480
|
+
{tab.icon}
|
|
481
|
+
<span>{tab.label}</span>
|
|
327
482
|
</button>
|
|
328
483
|
))}
|
|
329
484
|
</div>
|
|
330
485
|
<div className="ml-auto flex items-center">
|
|
331
|
-
{((activeTab === "file" && activeFileContent) ||
|
|
332
|
-
(activeTab === "ai-review" && aiReviewText)) && (
|
|
486
|
+
{((activeTab?.type === "file" && activeFileContent) ||
|
|
487
|
+
(activeTab?.type === "ai-review" && aiReviewText)) && (
|
|
333
488
|
<button
|
|
334
489
|
className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md transition-all duration-300"
|
|
335
490
|
onClick={handleCopy}
|
|
@@ -342,7 +497,7 @@ export default function ImportantFilesView({
|
|
|
342
497
|
<span className="sr-only">Copy</span>
|
|
343
498
|
</button>
|
|
344
499
|
)}
|
|
345
|
-
{activeTab === "ai-review" && aiReviewText && isOwner && (
|
|
500
|
+
{activeTab?.type === "ai-review" && aiReviewText && isOwner && (
|
|
346
501
|
<button
|
|
347
502
|
className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md ml-1"
|
|
348
503
|
onClick={onRequestAiReview}
|
|
@@ -352,10 +507,10 @@ export default function ImportantFilesView({
|
|
|
352
507
|
<span className="sr-only">Re-request AI Review</span>
|
|
353
508
|
</button>
|
|
354
509
|
)}
|
|
355
|
-
{activeTab === "file" && (
|
|
510
|
+
{activeTab?.type === "file" && (
|
|
356
511
|
<button
|
|
357
512
|
className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md"
|
|
358
|
-
onClick={() => onEditClicked?.(
|
|
513
|
+
onClick={() => onEditClicked?.(activeTab.filePath)}
|
|
359
514
|
>
|
|
360
515
|
<Edit className="h-4 w-4" />
|
|
361
516
|
<span className="sr-only">Edit</span>
|
|
@@ -363,30 +518,7 @@ export default function ImportantFilesView({
|
|
|
363
518
|
)}
|
|
364
519
|
</div>
|
|
365
520
|
</div>
|
|
366
|
-
<div className="p-4 bg-white dark:bg-[#0d1117]">
|
|
367
|
-
{activeTab === "ai" ? (
|
|
368
|
-
renderAiContent()
|
|
369
|
-
) : activeTab === "ai-review" ? (
|
|
370
|
-
renderAiReviewContent()
|
|
371
|
-
) : activeFilePath &&
|
|
372
|
-
(activeFilePath.endsWith(".md") ||
|
|
373
|
-
activeFilePath?.toLowerCase().endsWith("readme")) ? (
|
|
374
|
-
<MarkdownViewer markdownContent={activeFileContent} />
|
|
375
|
-
) : activeFilePath &&
|
|
376
|
-
(activeFilePath.endsWith(".js") ||
|
|
377
|
-
activeFilePath.endsWith(".jsx") ||
|
|
378
|
-
activeFilePath.endsWith(".ts") ||
|
|
379
|
-
activeFilePath.endsWith(".tsx")) ? (
|
|
380
|
-
<div className="overflow-x-auto">
|
|
381
|
-
<ShikiCodeViewer
|
|
382
|
-
code={activeFileContent}
|
|
383
|
-
filePath={activeFilePath}
|
|
384
|
-
/>
|
|
385
|
-
</div>
|
|
386
|
-
) : (
|
|
387
|
-
<pre className="whitespace-pre-wrap">{activeFileContent}</pre>
|
|
388
|
-
)}
|
|
389
|
-
</div>
|
|
521
|
+
<div className="p-4 bg-white dark:bg-[#0d1117]">{renderTabContent()}</div>
|
|
390
522
|
</div>
|
|
391
523
|
)
|
|
392
524
|
}
|
|
@@ -105,7 +105,7 @@ export default function MainContentHeader({
|
|
|
105
105
|
<DropdownMenuItem disabled={!Boolean(packageInfo)} asChild>
|
|
106
106
|
<a
|
|
107
107
|
href={`/editor?package_id=${packageInfo?.package_id}`}
|
|
108
|
-
className="cursor-pointer
|
|
108
|
+
className="cursor-pointer px-2 py-3"
|
|
109
109
|
>
|
|
110
110
|
<Pencil className="h-4 w-4 mx-3" />
|
|
111
111
|
Edit Online
|
|
@@ -115,7 +115,7 @@ export default function MainContentHeader({
|
|
|
115
115
|
<DropdownMenuItem
|
|
116
116
|
disabled={!Boolean(packageInfo)}
|
|
117
117
|
onClick={handleDownloadZip}
|
|
118
|
-
className="cursor-pointer
|
|
118
|
+
className="cursor-pointer px-2 py-3"
|
|
119
119
|
>
|
|
120
120
|
<Package2 className="h-4 w-4 mx-3" />
|
|
121
121
|
Download ZIP
|
|
@@ -57,6 +57,8 @@ export default function RepoPageContent({
|
|
|
57
57
|
const [pendingAiReviewId, setPendingAiReviewId] = useState<string | null>(
|
|
58
58
|
null,
|
|
59
59
|
)
|
|
60
|
+
const [licenseFileRequested, setLicenseFileRequested] =
|
|
61
|
+
useState<boolean>(false)
|
|
60
62
|
const queryClient = useQueryClient()
|
|
61
63
|
const { data: aiReview } = useAiReview(pendingAiReviewId, {
|
|
62
64
|
refetchInterval: (data) => (data && !data.ai_review_text ? 2000 : false),
|
|
@@ -82,6 +84,11 @@ export default function RepoPageContent({
|
|
|
82
84
|
Boolean(pendingAiReviewId) ||
|
|
83
85
|
isRequestingAiReview
|
|
84
86
|
|
|
87
|
+
const handleLicenseFileRequest = () => {
|
|
88
|
+
setLicenseFileRequested(true)
|
|
89
|
+
setTimeout(() => setLicenseFileRequested(false), 100)
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
// Handle initial view selection and hash-based view changes
|
|
86
93
|
useEffect(() => {
|
|
87
94
|
if (isCircuitJsonLoading) return
|
|
@@ -222,6 +229,7 @@ export default function RepoPageContent({
|
|
|
222
229
|
})
|
|
223
230
|
}
|
|
224
231
|
}}
|
|
232
|
+
onLicenseFileRequested={licenseFileRequested}
|
|
225
233
|
/>
|
|
226
234
|
</div>
|
|
227
235
|
|
|
@@ -235,6 +243,7 @@ export default function RepoPageContent({
|
|
|
235
243
|
// Update URL hash when view changes
|
|
236
244
|
window.location.hash = view
|
|
237
245
|
}}
|
|
246
|
+
onLicenseClick={handleLicenseFileRequest}
|
|
238
247
|
/>
|
|
239
248
|
</div>
|
|
240
249
|
{/* Releases section - Only visible on small screens */}
|