Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL_HERE
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY_HERE
SUPABASE_SERVICE_ROLE_KEY=YOUR_SUPABASE_SERVICE_ROLE_KEY_HERE
DATABASE_URL=postgresql://postgres:[YOUR-POSTGRES-PASSWORD]@[YOUR-SUPABASE-DB-HOST]:[PORT]/postgres

# Vercel Sandbox (MicroVM Infrastructure)
# Required for OIDC-based microVM authentication
VERCEL_TEAM_ID=your_vercel_team_id
VERCEL_PROJECT_ID=your_vercel_project_id
VERCEL_TOKEN=your_vercel_token
17 changes: 17 additions & 0 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { CopilotDisplay } from '@/components/copilot-display'
import RetrieveSection from '@/components/retrieve-section'
import { VideoSearchSection } from '@/components/video-search-section'
import { MapQueryHandler } from '@/components/map/map-query-handler'
import { SandboxSection } from '@/components/sandbox-section'

// Define the type for related queries
type RelatedQueries = {
Expand Down Expand Up @@ -840,6 +841,22 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
),
isCollapsed: isCollapsed.value
}
case 'sandbox': {
const logs = createStreamableValue(toolOutput.logs)
logs.done(toolOutput.logs)
return {
id,
component: (
<SandboxSection
logs={logs.value}
previewUrl={toolOutput.previewUrl}
exitCode={toolOutput.exitCode}
error={toolOutput.error}
/>
),
isCollapsed: isCollapsed.value
}
}
default:
console.warn(
`Unhandled tool result in getUIStateFromAIState: ${name}`
Expand Down
35 changes: 35 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions components/sandbox-logs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client'

import { useEffect, useRef } from 'react'
import { cn } from '@/lib/utils'

interface LogEntry {
type: 'stdout' | 'stderr'
content: string
}

interface SandboxLogsProps {
logs: LogEntry[]
isExecuting?: boolean
}

export function SandboxLogs({ logs, isExecuting }: SandboxLogsProps) {
const scrollRef = useRef<HTMLDivElement>(null)

useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
}
}, [logs])

return (
<div
ref={scrollRef}
className="bg-zinc-950 text-zinc-50 font-mono text-sm p-4 rounded-md overflow-auto max-h-[300px] border border-zinc-800"
>
{logs.map((log, index) => (
<div
key={index}
className={cn(
"whitespace-pre-wrap mb-1",
log.type === 'stderr' ? "text-red-400" : "text-zinc-300"
)}
>
{log.content}
</div>
))}
{isExecuting && (
<div className="flex items-center gap-2 text-zinc-500 mt-2">
<span className="w-1.5 h-1.5 bg-zinc-500 rounded-full animate-pulse" />
<span>Executing...</span>
</div>
)}
{logs.length === 0 && !isExecuting && (
<div className="text-zinc-500 italic">No output</div>
)}
</div>
)
}
51 changes: 51 additions & 0 deletions components/sandbox-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client'

import { useState } from 'react'
import { ExternalLink, Loader2, RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/button'

interface SandboxPreviewProps {
url: string
}

export function SandboxPreview({ url }: SandboxPreviewProps) {
const [isLoading, setIsLoading] = useState(true)
const [key, setKey] = useState(0)

const reload = () => {
setIsLoading(true)
setKey(prev => prev + 1)
}

return (
<div className="flex flex-col border rounded-md overflow-hidden bg-background">
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted/50">
<div className="text-xs font-mono truncate mr-4">{url}</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={reload}>
<RefreshCw className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" asChild>
<a href={url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-3.5 w-3.5" />
</a>
</Button>
</div>
</div>
<div className="relative w-full aspect-video bg-white">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-muted/20">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)}
<iframe
key={key}
src={url}
className="w-full h-full border-none"
sandbox="allow-scripts allow-same-origin allow-forms"
onLoad={() => setIsLoading(false)}
/>
</div>
</div>
)
}
51 changes: 51 additions & 0 deletions components/sandbox-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client'

import { useStreamableValue, StreamableValue } from 'ai/rsc'
import { Section } from '@/components/section'
import { SandboxLogs } from './sandbox-logs'
import { SandboxPreview } from './sandbox-preview'
import { Terminal } from 'lucide-react'

interface LogEntry {
type: 'stdout' | 'stderr'
content: string
}

interface SandboxSectionProps {
logs: StreamableValue<LogEntry[]>
previewUrl?: string
exitCode?: number
error?: string
}

export function SandboxSection({ logs, previewUrl, exitCode, error }: SandboxSectionProps) {
const [data, errorFromStream] = useStreamableValue(logs)
const isExecuting = data === undefined

return (
<Section title="Sandbox" isCollapsed={false}>
<div className="space-y-4">
<SandboxLogs logs={data || []} isExecuting={isExecuting} />

{previewUrl && (
<div className="mt-4">
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2 px-1">Live Preview</h4>
<SandboxPreview url={previewUrl} />
</div>
)}

{(error || !!errorFromStream) && (
<div className="p-3 text-xs bg-red-50 text-red-600 rounded border border-red-100 font-mono">
<strong>Error:</strong> {error || (errorFromStream as any)?.message || 'Execution failed'}
</div>
)}

{exitCode !== undefined && !previewUrl && (
<div className="text-[10px] text-muted-foreground font-mono px-1">
Process exited with code {exitCode}
</div>
)}
</div>
</Section>
)
}
7 changes: 6 additions & 1 deletion components/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
MessageCircleMore,
Newspaper,
Repeat2,
Search
Search,
Terminal
} from 'lucide-react'
import React from 'react'
import { Separator } from './ui/separator'
Expand All @@ -19,6 +20,7 @@ type SectionProps = {
size?: 'sm' | 'md' | 'lg'
title?: string
separator?: boolean
isCollapsed?: boolean
}

export const Section: React.FC<SectionProps> = ({
Expand Down Expand Up @@ -49,6 +51,9 @@ export const Section: React.FC<SectionProps> = ({
case 'Follow-up':
icon = <MessageCircleMore size={18} className="mr-2" />
break
case 'Sandbox':
icon = <Terminal size={18} className="mr-2" />
break
default:
icon = <Search size={18} className="mr-2" />
}
Expand Down
Loading