Skip to content

Commit 61bddbd

Browse files
committed
Add web preview sandbox and revamp UI components
Add AgentWebPreview and AgentCodeSandbox with live-editing/sandbox support and integrate into chat artifacts/messages. Overhaul multiple UI areas including landing pages (hero, features, agents, cta, stats, testimonials, trust), contact form, footer, navbar, tools list, and new workflows list. Add runtimeContext docs and miscellaneous accessibility and UX improvements
1 parent 160868e commit 61bddbd

21 files changed

Lines changed: 5190 additions & 623 deletions

app/chat/components/agent-artifact.tsx

Lines changed: 237 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,80 +11,282 @@ import {
1111
ArtifactContent,
1212
} from "@/src/components/ai-elements/artifact"
1313
import { 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

1833
export 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

2742
export 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
}

app/chat/components/agent-checkpoint.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,30 @@ import { BookmarkIcon, RotateCcwIcon } from "lucide-react"
1010
interface AgentCheckpointProps {
1111
messageIndex: number
1212
timestamp?: Date
13-
onRestore: (messageIndex: number) => void
13+
label?: string
14+
onRestore: () => void
1415
}
1516

1617
export function AgentCheckpoint({
1718
messageIndex,
1819
timestamp,
20+
label,
1921
onRestore,
2022
}: AgentCheckpointProps) {
21-
const label = timestamp
22-
? `Restore to ${timestamp.toLocaleTimeString()}`
23-
: "Restore checkpoint"
23+
const displayLabel = label
24+
? label
25+
: timestamp
26+
? `Restore to ${timestamp.toLocaleTimeString()}`
27+
: "Restore checkpoint"
2428

2529
return (
2630
<Checkpoint>
2731
<CheckpointIcon>
2832
<BookmarkIcon className="size-4 shrink-0 text-primary" />
2933
</CheckpointIcon>
3034
<CheckpointTrigger
31-
onClick={() => onRestore(messageIndex)}
32-
tooltip={label}
35+
onClick={onRestore}
36+
tooltip={displayLabel}
3337
className="gap-1"
3438
>
3539
<RotateCcwIcon className="size-3" />

0 commit comments

Comments
 (0)