@tscircuit/fake-snippets 0.0.66 → 0.0.67

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 (73) hide show
  1. package/bun-tests/fake-snippets-api/fixtures/get-circuit-json.ts +5 -143
  2. package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +1 -4
  3. package/bun-tests/fake-snippets-api/fixtures/start-server.ts +7 -3
  4. package/bun-tests/fake-snippets-api/routes/order_quotes/create.test.ts +20 -56
  5. package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +2 -2
  6. package/bun-tests/fake-snippets-api/routes/package_releases/update.test.ts +1 -1
  7. package/bun-tests/fake-snippets-api/routes/packages/images.test.ts +0 -11
  8. package/bun.lock +15 -17
  9. package/dist/bundle.js +32 -39
  10. package/fake-snippets-api/routes/api/order_quotes/create.ts +30 -37
  11. package/fake-snippets-api/routes/api/order_quotes/get.ts +5 -8
  12. package/package.json +4 -3
  13. package/src/App.tsx +0 -7
  14. package/src/ContextProviders.tsx +2 -0
  15. package/src/components/DownloadButtonAndMenu.tsx +1 -4
  16. package/src/components/Footer.tsx +5 -2
  17. package/src/components/HeaderLogin.tsx +37 -54
  18. package/src/components/ImageWithFallback.tsx +37 -0
  19. package/src/components/JLCPCBImportDialog.tsx +43 -24
  20. package/src/components/PackageCard.tsx +2 -2
  21. package/src/components/{SnippetLink.tsx → PackageLink.tsx} +8 -16
  22. package/src/components/PackageSearchResults.tsx +87 -0
  23. package/src/components/PackagesList.tsx +3 -3
  24. package/src/components/PageSearchComponent.tsx +9 -9
  25. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +5 -28
  26. package/src/components/ViewPackagePage/components/main-content-header.tsx +8 -8
  27. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +24 -14
  28. package/src/components/ViewPackagePage/components/package-header.tsx +6 -1
  29. package/src/components/package-port/CodeEditor.tsx +13 -10
  30. package/src/components/package-port/CodeEditorHeader.tsx +1 -1
  31. package/src/components/package-port/EditorNav.tsx +2 -2
  32. package/src/hooks/use-global-store.ts +1 -0
  33. package/src/hooks/use-shiki-highlighter.ts +13 -6
  34. package/src/lib/download-fns/download-gltf.ts +3 -10
  35. package/src/lib/handleManualEditsImport.tsx +1 -1
  36. package/src/lib/types.ts +4 -2
  37. package/src/pages/dashboard.tsx +3 -4
  38. package/src/pages/editor.tsx +20 -14
  39. package/src/pages/latest.tsx +25 -26
  40. package/src/pages/search.tsx +120 -19
  41. package/src/pages/trending.tsx +14 -58
  42. package/src/pages/user-profile.tsx +13 -8
  43. package/bun-tests/fake-snippets-api/routes/snippets/add_star.test.ts +0 -84
  44. package/bun-tests/fake-snippets-api/routes/snippets/create.test.ts +0 -53
  45. package/bun-tests/fake-snippets-api/routes/snippets/delete.test.ts +0 -82
  46. package/bun-tests/fake-snippets-api/routes/snippets/download.test.ts +0 -90
  47. package/bun-tests/fake-snippets-api/routes/snippets/generate_from_jlcpcb.test.ts +0 -16
  48. package/bun-tests/fake-snippets-api/routes/snippets/get.test.ts +0 -163
  49. package/bun-tests/fake-snippets-api/routes/snippets/get_image.test.ts +0 -117
  50. package/bun-tests/fake-snippets-api/routes/snippets/images.test.ts +0 -114
  51. package/bun-tests/fake-snippets-api/routes/snippets/list.test.ts +0 -169
  52. package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +0 -50
  53. package/bun-tests/fake-snippets-api/routes/snippets/list_trending.test.ts +0 -72
  54. package/bun-tests/fake-snippets-api/routes/snippets/remove_star.test.ts +0 -80
  55. package/bun-tests/fake-snippets-api/routes/snippets/search.test.ts +0 -75
  56. package/bun-tests/fake-snippets-api/routes/snippets/star-count.test.ts +0 -51
  57. package/bun-tests/fake-snippets-api/routes/snippets/update.test.ts +0 -175
  58. package/src/components/AiChatInterface.tsx +0 -229
  59. package/src/components/CodeAndPreview.tsx +0 -289
  60. package/src/components/CodeEditor.tsx +0 -539
  61. package/src/components/CodeEditorHeader.tsx +0 -135
  62. package/src/components/EditorNav.tsx +0 -502
  63. package/src/components/PreviewContent.tsx +0 -372
  64. package/src/components/SnippetCard.tsx +0 -159
  65. package/src/components/SnippetList.tsx +0 -71
  66. package/src/hooks/use-compiled-tsx.ts +0 -37
  67. package/src/hooks/use-run-tsx/construct-circuit.tsx +0 -62
  68. package/src/hooks/use-run-tsx/index.tsx +0 -256
  69. package/src/hooks/use-save-snippet.ts +0 -66
  70. package/src/hooks/use-typecheck.ts +0 -54
  71. package/src/lib/utils/getSyntaxError.ts +0 -13
  72. package/src/pages/ai.tsx +0 -92
  73. package/src/pages/view-snippet.tsx +0 -166
@@ -1,229 +0,0 @@
1
- import { useState, useRef, useEffect } from "react"
2
- import { Button } from "@/components/ui/button"
3
- import ChatInput from "./ChatInput"
4
- import { useAiApi } from "@/hooks/use-ai-api"
5
- import { createCircuitBoard1Template } from "@tscircuit/prompt-benchmarks"
6
- import { TextDelta } from "@anthropic-ai/sdk/resources/messages.mjs"
7
- import { MagicWandIcon } from "@radix-ui/react-icons"
8
- import { AiChatMessage } from "./AiChatMessage"
9
- import { useLocation } from "wouter"
10
- import { useSnippet } from "@/hooks/use-snippet"
11
- import { Edit2 } from "lucide-react"
12
- import { SnippetLink } from "./SnippetLink"
13
- import { useGlobalStore } from "@/hooks/use-global-store"
14
- import { useSignIn } from "@/hooks/use-sign-in"
15
- import { extractCodefence } from "extract-codefence"
16
- import { PrefetchPageLink } from "./PrefetchPageLink"
17
-
18
- export default function AIChatInterface({
19
- code,
20
- hasUnsavedChanges,
21
- snippetId,
22
- onCodeChange,
23
- onStartStreaming,
24
- onStopStreaming,
25
- errorMessage,
26
- disabled,
27
- }: {
28
- code: string
29
- disabled?: boolean
30
- hasUnsavedChanges: boolean
31
- snippetId?: string | null
32
- onCodeChange: (code: string) => void
33
- onStartStreaming: () => void
34
- onStopStreaming: () => void
35
- errorMessage: string | null
36
- }) {
37
- const [messages, setMessages] = useState<AiChatMessage[]>([])
38
- const [isStreaming, setIsStreaming] = useState(false)
39
- const anthropic = useAiApi()
40
- const messagesEndRef = useRef<HTMLDivElement>(null)
41
- const { data: snippet } = useSnippet(snippetId!)
42
- const [currentCodeBlock, setCurrentCodeBlock] = useState<string | null>(null)
43
- const [location, navigate] = useLocation()
44
- const isStreamingRef = useRef(false)
45
- const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
46
- const signIn = useSignIn()
47
-
48
- useEffect(() => {
49
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
50
- }, [messages])
51
-
52
- const addMessage = async (message: string) => {
53
- const newMessages = messages.concat([
54
- {
55
- sender: "user",
56
- content: message,
57
- },
58
- {
59
- sender: "bot",
60
- content: "",
61
- codeVersion: messages.filter((m) => m.sender === "bot").length,
62
- },
63
- ])
64
- setMessages(newMessages)
65
- setIsStreaming(true)
66
- onStartStreaming()
67
-
68
- try {
69
- const stream = await anthropic.messages.stream({
70
- model: "claude-3-sonnet-20240229",
71
- system: createCircuitBoard1Template({
72
- currentCode: code,
73
- }),
74
- messages: [
75
- // TODO: include previous messages
76
- {
77
- role: "user",
78
- content: message,
79
- },
80
- ],
81
- max_tokens: 1000,
82
- })
83
-
84
- let accumulatedContent = ""
85
- let isInCodeBlock = false
86
-
87
- for await (const chunk of stream) {
88
- if (chunk.type === "content_block_delta") {
89
- const chunkText = (chunk.delta as TextDelta).text
90
- accumulatedContent += chunkText
91
-
92
- if (chunkText.includes("```")) {
93
- isInCodeBlock = !isInCodeBlock
94
- if (isInCodeBlock) {
95
- setCurrentCodeBlock("")
96
- } else {
97
- const codeContent = extractCodefence(accumulatedContent)
98
- if (codeContent) {
99
- onCodeChange(codeContent)
100
- }
101
- setCurrentCodeBlock(null)
102
- }
103
- } else if (isInCodeBlock) {
104
- setCurrentCodeBlock((prev) => {
105
- const updatedCode = (prev || "") + chunkText
106
- onCodeChange(updatedCode)
107
- return updatedCode
108
- })
109
- }
110
-
111
- setMessages((prevMessages) => {
112
- const updatedMessages = [...prevMessages]
113
- updatedMessages[updatedMessages.length - 1].content =
114
- accumulatedContent
115
- return updatedMessages
116
- })
117
- }
118
- }
119
- } catch (error) {
120
- console.error("Error streaming response:", error)
121
- setMessages((prevMessages) => {
122
- const updatedMessages = [...prevMessages]
123
- updatedMessages[updatedMessages.length - 1].content =
124
- "An error occurred while generating the response."
125
- return updatedMessages
126
- })
127
- } finally {
128
- setIsStreaming(false)
129
- onStopStreaming()
130
- }
131
- }
132
-
133
- useEffect(() => {
134
- const searchParams = new URLSearchParams(
135
- window.location.search.split("?")[1],
136
- )
137
- const initialPrompt = searchParams.get("initial_prompt")
138
-
139
- if (initialPrompt && messages.length === 0 && !isStreamingRef.current) {
140
- isStreamingRef.current = true
141
- addMessage(initialPrompt)
142
- }
143
- }, [])
144
-
145
- return (
146
- <div className="flex flex-col h-[calc(100vh-60px)] max-w-2xl mx-auto p-4 bg-gray-100">
147
- <div className="flex-1 overflow-y-auto space-y-4 mb-4">
148
- {snippet && (
149
- <div className="flex pl-4 p-2 rounded items-center bg-white border border-gray-200 text-sm mb-4 shadow-sm">
150
- <SnippetLink snippet={snippet} />
151
- <div className="flex-grow" />
152
- <PrefetchPageLink href={`/editor?snippet_id=${snippet.snippet_id}`}>
153
- <Button
154
- size="sm"
155
- className="text-xs"
156
- variant="ghost"
157
- disabled={hasUnsavedChanges}
158
- >
159
- Open in Editor
160
- <Edit2 className="w-3 h-3 ml-2 opacity-60" />
161
- </Button>
162
- </PrefetchPageLink>
163
- </div>
164
- )}
165
- {messages.length === 0 && isLoggedIn && (
166
- <div className="text-gray-500 text-xl text-center pt-[30vh] flex flex-col items-center">
167
- <div>Submit a prompt to {snippet ? "edit!" : "get started!"}</div>
168
- <div className="mt-2">
169
- This is our legacy AI chat interface. For a better experience,
170
- please use{" "}
171
- <PrefetchPageLink href="https://chat.tscircuit.com">
172
- chat.tscircuit.com
173
- </PrefetchPageLink>
174
- .
175
- </div>
176
- <div className="text-6xl mt-4">↓</div>
177
- </div>
178
- )}
179
- {!isLoggedIn && (
180
- <div className="text-gray-500 text-xl text-center pt-[30vh] flex flex-col items-center">
181
- <div>
182
- Sign in use the AI chat or{" "}
183
- <PrefetchPageLink
184
- className="text-blue-500 underline"
185
- href="/quickstart"
186
- >
187
- use the regular editor
188
- </PrefetchPageLink>
189
- </div>
190
- <div className="mt-4 flex gap-2">
191
- <Button onClick={() => signIn()}>Sign In</Button>
192
- <Button onClick={() => signIn()} variant="outline">
193
- Sign Up
194
- </Button>
195
- </div>
196
- </div>
197
- )}
198
- {messages.map((message, index) => (
199
- <AiChatMessage key={index} message={message} />
200
- ))}
201
- <div ref={messagesEndRef} />
202
- </div>
203
- {code && errorMessage && !isStreaming && (
204
- <div className="flex justify-end mr-6">
205
- <Button
206
- onClick={() => {
207
- addMessage(`Fix this error: ${errorMessage}`)
208
- }}
209
- disabled={!isLoggedIn}
210
- className="mb-2 bg-green-50 hover:bg-green-100"
211
- variant="outline"
212
- >
213
- <MagicWandIcon className="w-4 h-4 mr-2" />
214
- <span className="font-bold">Fix Error with AI</span>
215
- <span className="italic font-normal ml-2">
216
- "{errorMessage.slice(0, 26)}..."
217
- </span>
218
- </Button>
219
- </div>
220
- )}
221
- <ChatInput
222
- onSubmit={async (message: string) => {
223
- addMessage(message)
224
- }}
225
- disabled={isStreaming || !isLoggedIn}
226
- />
227
- </div>
228
- )
229
- }
@@ -1,289 +0,0 @@
1
- import { CodeEditor } from "@/components/CodeEditor"
2
- import { usePackageVisibilitySettingsDialog } from "@/components/dialogs/package-visibility-settings-dialog"
3
- import { useAxios } from "@/hooks/use-axios"
4
- import { useCreateSnippetMutation } from "@/hooks/use-create-snippet-mutation"
5
- import { useGlobalStore } from "@/hooks/use-global-store"
6
- import { useToast } from "@/hooks/use-toast"
7
- import { useUrlParams } from "@/hooks/use-url-params"
8
- import useWarnUserOnPageChange from "@/hooks/use-warn-user-on-page-change"
9
- import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
10
- import { getSnippetTemplate } from "@/lib/get-snippet-template"
11
- import { cn } from "@/lib/utils"
12
- import { parseJsonOrNull } from "@/lib/utils/parseJsonOrNull"
13
- import type { Snippet } from "fake-snippets-api/lib/db/schema"
14
- import { Loader2 } from "lucide-react"
15
- import { useEffect, useMemo, useState } from "react"
16
- import { useMutation, useQueryClient } from "react-query"
17
- import EditorNav from "./EditorNav"
18
- import { SuspenseRunFrame } from "./SuspenseRunFrame"
19
- import { applyEditEventsToManualEditsFile } from "@tscircuit/core"
20
-
21
- interface Props {
22
- snippet?: Snippet | null
23
- }
24
-
25
- export function CodeAndPreview({ snippet }: Props) {
26
- const axios = useAxios()
27
- const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
28
- const urlParams = useUrlParams()
29
- const templateFromUrl = useMemo(
30
- () => (urlParams.template ? getSnippetTemplate(urlParams.template) : null),
31
- [],
32
- )
33
- const defaultCode = useMemo(() => {
34
- return (
35
- decodeUrlHashToText(window.location.toString()) ??
36
- snippet?.code ??
37
- // If the snippet_id is in the url, use an empty string as the default
38
- // code until the snippet code is loaded
39
- (urlParams.snippet_id && "") ??
40
- templateFromUrl?.code
41
- )
42
- }, [])
43
-
44
- // Initialize with template or snippet's manual edits if available
45
- const [manualEditsFileContent, setManualEditsFileContent] = useState<
46
- string | null
47
- >(null)
48
- const [code, setCode] = useState(defaultCode ?? "")
49
- const [dts, setDts] = useState("")
50
- const [showPreview, setShowPreview] = useState(true)
51
- const [lastRunCode, setLastRunCode] = useState(defaultCode ?? "")
52
- const [fullScreen, setFullScreen] = useState(false)
53
- const [circuitJson, setCircuitJson] = useState<any>(null)
54
- const {
55
- Dialog: PackageVisibilitySettingsDialog,
56
- openDialog: openPackageVisibilitySettingsDialog,
57
- } = usePackageVisibilitySettingsDialog()
58
- const [isPrivate, setIsPrivate] = useState(false)
59
-
60
- const snippetType: "board" | "package" | "model" | "footprint" =
61
- snippet?.snippet_type ??
62
- (templateFromUrl?.type as any) ??
63
- urlParams.snippet_type
64
-
65
- useEffect(() => {
66
- if (snippet?.code) {
67
- setCode(snippet.code)
68
- setLastRunCode(snippet.code)
69
- }
70
- }, [Boolean(snippet)])
71
-
72
- const { toast } = useToast()
73
-
74
- useEffect(() => {
75
- if (snippet?.manual_edits_json_content) {
76
- setManualEditsFileContent(snippet.manual_edits_json_content ?? "")
77
- }
78
- }, [Boolean(snippet?.manual_edits_json_content)])
79
-
80
- const userImports = useMemo(
81
- () => ({
82
- "./manual-edits.json": parseJsonOrNull(manualEditsFileContent) ?? "",
83
- }),
84
- [manualEditsFileContent],
85
- )
86
-
87
- const qc = useQueryClient()
88
-
89
- const updateSnippetMutation = useMutation({
90
- mutationFn: async () => {
91
- if (!snippet) throw new Error("No snippet to update")
92
-
93
- const updateSnippetPayload = {
94
- snippet_id: snippet.snippet_id,
95
- code: code,
96
- dts: dts,
97
- // compiled_js: compiledJs,
98
- circuit_json: circuitJson,
99
- manual_edits_json_content: manualEditsFileContent,
100
- }
101
-
102
- try {
103
- const response = await axios.post(
104
- "/snippets/update",
105
- updateSnippetPayload,
106
- )
107
- return response.data
108
- } catch (error: any) {
109
- const responseStatus = error?.status ?? error?.response?.status
110
- // We would normally only do this if the error is a 413, but we're not
111
- // able to check the status properly because of the browser CORS policy
112
- // (the PAYLOAD_TOO_LARGE error does not have the proper CORS headers)
113
- if (
114
- import.meta.env.VITE_ALTERNATE_REGISTRY_URL &&
115
- (responseStatus === undefined || responseStatus === 413)
116
- ) {
117
- console.log(`Failed to update snippet, attempting alternate registry`)
118
- const response = await axios.post(
119
- `${import.meta.env.VITE_ALTERNATE_REGISTRY_URL}/snippets/update`,
120
- updateSnippetPayload,
121
- )
122
- return response.data
123
- }
124
- throw error
125
- }
126
- },
127
- onSuccess: () => {
128
- qc.invalidateQueries({ queryKey: ["snippets", snippet?.snippet_id] })
129
- toast({
130
- title: "Snippet saved",
131
- description: "Your changes have been saved successfully.",
132
- })
133
- },
134
- onError: (error) => {
135
- console.error("Error saving snippet:", error)
136
- toast({
137
- title: "Error",
138
- description:
139
- error instanceof Error
140
- ? error.message
141
- : "Failed to save the snippet. Please try again.",
142
- variant: "destructive",
143
- })
144
- },
145
- })
146
-
147
- const createSnippetMutation = useCreateSnippetMutation()
148
- const [lastSavedAt, setLastSavedAt] = useState(Date.now())
149
-
150
- const handleSave = async () => {
151
- if (hasUnrunChanges) {
152
- toast({
153
- title: "Warning",
154
- description: "You must run the snippet before saving your changes.",
155
- variant: "destructive",
156
- })
157
- return
158
- }
159
-
160
- if (!snippet && isLoggedIn) {
161
- openPackageVisibilitySettingsDialog()
162
- return
163
- }
164
-
165
- setLastSavedAt(Date.now())
166
- if (snippet) {
167
- updateSnippetMutation.mutate()
168
- } else {
169
- createSnippetMutation.mutate({
170
- code,
171
- circuit_json: circuitJson as any,
172
- manual_edits_json_content: manualEditsFileContent ?? "",
173
- })
174
- }
175
- }
176
-
177
- const hasManualEditsChanged =
178
- (snippet?.manual_edits_json_content ?? "") !==
179
- (manualEditsFileContent ?? "")
180
-
181
- const hasUnsavedChanges =
182
- !updateSnippetMutation.isLoading &&
183
- Date.now() - lastSavedAt > 1000 &&
184
- (snippet?.code !== code || hasManualEditsChanged)
185
-
186
- const hasUnrunChanges = code !== lastRunCode
187
-
188
- useWarnUserOnPageChange({ hasUnsavedChanges })
189
-
190
- const fsMap = useMemo(() => {
191
- return {
192
- "index.tsx": code,
193
- "manual-edits.json": manualEditsFileContent || "",
194
- }
195
- }, [code, manualEditsFileContent])
196
-
197
- if (!snippet && (urlParams.snippet_id || urlParams.should_create_snippet)) {
198
- return (
199
- <div className="flex items-center justify-center h-64">
200
- <div className="flex flex-col items-center justify-center">
201
- <div className="text-lg text-gray-500 mb-4">Loading</div>
202
- <Loader2 className="w-16 h-16 animate-spin text-gray-400" />
203
- </div>
204
- </div>
205
- )
206
- }
207
-
208
- return (
209
- <div className="flex flex-col">
210
- <EditorNav
211
- circuitJson={circuitJson}
212
- snippet={snippet}
213
- snippetType={snippetType}
214
- code={code}
215
- isSaving={updateSnippetMutation.isLoading}
216
- hasUnsavedChanges={hasUnsavedChanges}
217
- onSave={() => handleSave()}
218
- onTogglePreview={() => setShowPreview(!showPreview)}
219
- previewOpen={showPreview}
220
- canSave={!hasUnrunChanges} // Disable save if there are unrun changes
221
- />
222
- <div className={`flex ${showPreview ? "flex-col md:flex-row" : ""}`}>
223
- <div
224
- className={cn(
225
- "hidden flex-col md:flex border-r border-gray-200 bg-gray-50",
226
- showPreview ? "w-full md:w-1/2" : "w-full flex",
227
- )}
228
- >
229
- <CodeEditor
230
- initialCode={code}
231
- manualEditsFileContent={manualEditsFileContent ?? ""}
232
- onManualEditsFileContentChanged={(newContent) => {
233
- setManualEditsFileContent(newContent)
234
- }}
235
- onCodeChange={(newCode) => {
236
- setCode(newCode)
237
- }}
238
- onDtsChange={(newDts) => setDts(newDts)}
239
- />
240
- </div>
241
- {showPreview && (
242
- <div
243
- className={cn(
244
- "flex p-0 flex-col min-h-[640px]",
245
- fullScreen
246
- ? "fixed inset-0 z-50 bg-white p-4 overflow-hidden"
247
- : "w-full md:w-1/2",
248
- )}
249
- >
250
- <SuspenseRunFrame
251
- showRunButton
252
- forceLatestEvalVersion
253
- onRenderStarted={() => {
254
- setLastRunCode(code)
255
- }}
256
- onRenderFinished={({ circuitJson }) => {
257
- setCircuitJson(circuitJson)
258
- }}
259
- onEditEvent={(event) => {
260
- const newManualEditsFileContent =
261
- applyEditEventsToManualEditsFile({
262
- circuitJson: circuitJson,
263
- editEvents: [event],
264
- manualEditsFile: JSON.parse(manualEditsFileContent ?? "{}"),
265
- })
266
- setManualEditsFileContent(
267
- JSON.stringify(newManualEditsFileContent, null, 2),
268
- )
269
- }}
270
- fsMap={fsMap}
271
- />
272
- </div>
273
- )}
274
- </div>
275
- <PackageVisibilitySettingsDialog
276
- initialIsPrivate={false}
277
- onSave={(isPrivate: boolean) => {
278
- setLastSavedAt(Date.now())
279
- createSnippetMutation.mutate({
280
- code,
281
- circuit_json: circuitJson as any,
282
- manual_edits_json_content: manualEditsFileContent ?? "",
283
- is_private: isPrivate,
284
- })
285
- }}
286
- />
287
- </div>
288
- )
289
- }