@@ -11,80 +11,282 @@ import {
1111 ArtifactContent ,
1212} from "@/src/components/ai-elements/artifact"
1313import { CodeBlock } from "@/src/components/ai-elements/code-block"
14- import { CopyIcon , DownloadIcon } from "lucide-react"
15- import { useCallback } from "react"
16- import { BundledLanguage } from "shiki"
14+ import { Button } from "@/ui/button"
15+ import {
16+ Dialog ,
17+ DialogContent ,
18+ DialogHeader ,
19+ DialogTitle ,
20+ } from "@/ui/dialog"
21+ import {
22+ CopyIcon ,
23+ DownloadIcon ,
24+ PlayIcon ,
25+ MaximizeIcon ,
26+ CheckIcon ,
27+ Code2Icon ,
28+ } from "lucide-react"
29+ import { useState , useCallback } from "react"
30+ import type { BundledLanguage } from "shiki"
31+ import { AgentCodeSandbox } from "./agent-web-preview"
1732
1833export interface ArtifactData {
1934 id : string
2035 title : string
2136 description ?: string
22- type : "code" | "markdown" | "json" | "text"
37+ type : "code" | "markdown" | "json" | "text" | "html" | "react"
2338 language ?: string
2439 content : string
2540}
2641
2742export interface AgentArtifactProps {
2843 artifact : ArtifactData
2944 onClose ?: ( ) => void
45+ onCodeUpdate ?: ( artifactId : string , newCode : string ) => void
3046}
3147
32- export function AgentArtifact ( { artifact, onClose } : AgentArtifactProps ) {
48+ // Languages that support live preview
49+ const PREVIEWABLE_LANGUAGES = [
50+ "tsx" ,
51+ "jsx" ,
52+ "typescript" ,
53+ "javascript" ,
54+ "html" ,
55+ "react" ,
56+ ]
57+
58+ export function AgentArtifact ( {
59+ artifact,
60+ onClose,
61+ onCodeUpdate,
62+ } : AgentArtifactProps ) {
63+ const [ copied , setCopied ] = useState ( false )
64+ const [ isEditorOpen , setIsEditorOpen ] = useState ( false )
65+ const [ editedCode , setEditedCode ] = useState ( artifact . content )
66+
67+ const isPreviewable =
68+ artifact . type === "code" &&
69+ PREVIEWABLE_LANGUAGES . includes ( artifact . language ?. toLowerCase ( ) || "" )
70+
3371 const handleCopy = useCallback ( async ( ) => {
3472 try {
35- await navigator . clipboard . writeText ( artifact . content )
73+ await navigator . clipboard . writeText ( editedCode )
74+ setCopied ( true )
75+ setTimeout ( ( ) => setCopied ( false ) , 2000 )
3676 } catch ( err ) {
3777 console . error ( "Failed to copy:" , err )
3878 }
39- } , [ artifact . content ] )
79+ } , [ editedCode ] )
4080
4181 const handleDownload = useCallback ( ( ) => {
4282 const extensions : Record < string , string > = {
4383 code : artifact . language || "txt" ,
4484 markdown : "md" ,
4585 json : "json" ,
4686 text : "txt" ,
87+ html : "html" ,
88+ react : "tsx" ,
4789 }
4890 const ext = extensions [ artifact . type ] || "txt"
4991 const filename = `${ artifact . title . toLowerCase ( ) . replace ( / \s + / g, "-" ) } .${ ext } `
5092
51- const blob = new Blob ( [ artifact . content ] , { type : "text/plain" } )
93+ const blob = new Blob ( [ editedCode ] , { type : "text/plain" } )
5294 const url = URL . createObjectURL ( blob )
5395 const a = document . createElement ( "a" )
5496 a . href = url
5597 a . download = filename
5698 a . click ( )
5799 URL . revokeObjectURL ( url )
58- } , [ artifact ] )
100+ } , [ editedCode , artifact ] )
101+
102+ const handleOpenEditor = useCallback ( ( ) => {
103+ setIsEditorOpen ( true )
104+ } , [ ] )
105+
106+ const handleCloseEditor = useCallback ( ( ) => {
107+ setIsEditorOpen ( false )
108+ } , [ ] )
109+
110+ const handleCodeChange = useCallback (
111+ ( newCode : string ) => {
112+ setEditedCode ( newCode )
113+ onCodeUpdate ?.( artifact . id , newCode )
114+ } ,
115+ [ artifact . id , onCodeUpdate ]
116+ )
117+
118+ const language : BundledLanguage = ( artifact . language ||
119+ ( artifact . type === "json"
120+ ? "json"
121+ : artifact . type === "html"
122+ ? "html"
123+ : artifact . type === "react"
124+ ? "tsx"
125+ : "plaintext" ) ) as BundledLanguage
126+
127+ return (
128+ < >
129+ < Artifact className = "my-4" >
130+ < ArtifactHeader >
131+ < div className = "flex flex-col gap-0.5" >
132+ < ArtifactTitle > { artifact . title } </ ArtifactTitle >
133+ { artifact . description && (
134+ < ArtifactDescription > { artifact . description } </ ArtifactDescription >
135+ ) }
136+ </ div >
137+ < ArtifactActions >
138+ { isPreviewable && (
139+ < ArtifactAction
140+ tooltip = "Open in Live Editor"
141+ icon = { PlayIcon }
142+ onClick = { handleOpenEditor }
143+ />
144+ ) }
145+ < ArtifactAction
146+ tooltip = { copied ? "Copied!" : "Copy" }
147+ icon = { copied ? CheckIcon : CopyIcon }
148+ onClick = { handleCopy }
149+ />
150+ < ArtifactAction
151+ tooltip = "Download"
152+ icon = { DownloadIcon }
153+ onClick = { handleDownload }
154+ />
155+ { onClose && < ArtifactClose onClick = { onClose } /> }
156+ </ ArtifactActions >
157+ </ ArtifactHeader >
158+ < ArtifactContent className = "p-0" >
159+ < div className = "relative" >
160+ < CodeBlock code = { editedCode } language = { language } />
161+ { isPreviewable && (
162+ < Button
163+ variant = "secondary"
164+ size = "sm"
165+ className = "absolute bottom-3 right-3 gap-1.5 shadow-md"
166+ onClick = { handleOpenEditor }
167+ >
168+ < Code2Icon className = "size-3.5" />
169+ Open Live Editor
170+ </ Button >
171+ ) }
172+ </ div >
173+ </ ArtifactContent >
174+ </ Artifact >
175+
176+ { /* Live Editor Dialog */ }
177+ < Dialog open = { isEditorOpen } onOpenChange = { setIsEditorOpen } >
178+ < DialogContent className = "max-w-6xl h-[85vh] p-0 gap-0" >
179+ < DialogHeader className = "sr-only" >
180+ < DialogTitle > Live Code Editor - { artifact . title } </ DialogTitle >
181+ </ DialogHeader >
182+ < div className = "h-full" >
183+ < AgentCodeSandbox
184+ code = { editedCode }
185+ language = { artifact . language || "tsx" }
186+ title = { artifact . title }
187+ onClose = { handleCloseEditor }
188+ onCodeChange = { handleCodeChange }
189+ editable = { true }
190+ />
191+ </ div >
192+ </ DialogContent >
193+ </ Dialog >
194+ </ >
195+ )
196+ }
59197
60- const language : BundledLanguage = ( artifact . language || ( artifact . type === "json" ? "json" : "plaintext" ) ) as BundledLanguage
198+ // Compact artifact card for inline display
199+ interface AgentArtifactCompactProps {
200+ artifact : ArtifactData
201+ onClick ?: ( ) => void
202+ }
203+
204+ export function AgentArtifactCompact ( {
205+ artifact,
206+ onClick,
207+ } : AgentArtifactCompactProps ) {
208+ const isPreviewable =
209+ artifact . type === "code" &&
210+ PREVIEWABLE_LANGUAGES . includes ( artifact . language ?. toLowerCase ( ) || "" )
61211
62212 return (
63- < Artifact className = "my-4" >
64- < ArtifactHeader >
65- < div className = "flex flex-col gap-0.5" >
66- < ArtifactTitle > { artifact . title } </ ArtifactTitle >
67- { artifact . description && (
68- < ArtifactDescription > { artifact . description } </ ArtifactDescription >
69- ) }
213+ < button
214+ onClick = { onClick }
215+ className = "group flex w-full items-center gap-3 rounded-lg border bg-card p-3 text-left transition-colors hover:bg-muted/50"
216+ >
217+ < div className = "flex size-10 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary" >
218+ < Code2Icon className = "size-5" />
219+ </ div >
220+ < div className = "min-w-0 flex-1" >
221+ < p className = "truncate text-sm font-medium" > { artifact . title } </ p >
222+ < p className = "truncate text-xs text-muted-foreground" >
223+ { artifact . language || artifact . type } •{ " " }
224+ { artifact . content . split ( "\n" ) . length } lines
225+ </ p >
226+ </ div >
227+ { isPreviewable && (
228+ < div className = "flex items-center gap-1 text-xs text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" >
229+ < PlayIcon className = "size-3" />
230+ Preview
70231 </ div >
71- < ArtifactActions >
72- < ArtifactAction
73- tooltip = "Copy"
74- icon = { CopyIcon }
75- onClick = { handleCopy }
76- />
77- < ArtifactAction
78- tooltip = "Download"
79- icon = { DownloadIcon }
80- onClick = { handleDownload }
81- />
82- { onClose && < ArtifactClose onClick = { onClose } /> }
83- </ ArtifactActions >
84- </ ArtifactHeader >
85- < ArtifactContent className = "p-0" >
86- < CodeBlock code = { artifact . content } language = { language } />
87- </ ArtifactContent >
88- </ Artifact >
232+ ) }
233+ < MaximizeIcon className = "size-4 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
234+ </ button >
235+ )
236+ }
237+
238+ // Floating action button for quick access to editor
239+ interface ArtifactEditorFABProps {
240+ artifact : ArtifactData
241+ onCodeChange ?: ( newCode : string ) => void
242+ }
243+
244+ export function ArtifactEditorFAB ( {
245+ artifact,
246+ onCodeChange,
247+ } : ArtifactEditorFABProps ) {
248+ const [ isOpen , setIsOpen ] = useState ( false )
249+ const [ code , setCode ] = useState ( artifact . content )
250+
251+ const isPreviewable =
252+ artifact . type === "code" &&
253+ PREVIEWABLE_LANGUAGES . includes ( artifact . language ?. toLowerCase ( ) || "" )
254+
255+ if ( ! isPreviewable ) return null
256+
257+ const handleCodeChange = ( newCode : string ) => {
258+ setCode ( newCode )
259+ onCodeChange ?.( newCode )
260+ }
261+
262+ return (
263+ < >
264+ < Button
265+ variant = "outline"
266+ size = "icon"
267+ className = "fixed bottom-6 right-6 z-50 size-12 rounded-full shadow-lg"
268+ onClick = { ( ) => setIsOpen ( true ) }
269+ >
270+ < PlayIcon className = "size-5" />
271+ </ Button >
272+
273+ < Dialog open = { isOpen } onOpenChange = { setIsOpen } >
274+ < DialogContent className = "max-w-6xl h-[85vh] p-0 gap-0" >
275+ < DialogHeader className = "sr-only" >
276+ < DialogTitle > Live Code Editor</ DialogTitle >
277+ </ DialogHeader >
278+ < div className = "h-full" >
279+ < AgentCodeSandbox
280+ code = { code }
281+ language = { artifact . language || "tsx" }
282+ title = { artifact . title }
283+ onClose = { ( ) => setIsOpen ( false ) }
284+ onCodeChange = { handleCodeChange }
285+ editable = { true }
286+ />
287+ </ div >
288+ </ DialogContent >
289+ </ Dialog >
290+ </ >
89291 )
90292}
0 commit comments