Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Comment on lines +38 to +41

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Reorder environment variables to alphabetical order.

The static analysis tool (dotenv-linter) reports that VERCEL_TEAM_ID (line 38) should appear before VERCEL_TOKEN (line 36) to maintain alphabetical/conventional ordering.

🔧 Proposed reordering
-# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
-VERCEL_TOKEN=your_vercel_token
-# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
-VERCEL_TEAM_ID=your_vercel_team_id
+# VERCEL_PROJECT_ID: Found in Vercel Dashboard > [Project] > Settings > General (starts with 'prj_')
+VERCEL_PROJECT_ID=your_vercel_project_id
+# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
+VERCEL_TEAM_ID=your_vercel_team_id
+# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
+VERCEL_TOKEN=your_vercel_token
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
VERCEL_TOKEN=your_vercel_token
# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
VERCEL_TEAM_ID=your_vercel_team_id
# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
VERCEL_TEAM_ID=your_vercel_team_id
# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
VERCEL_TOKEN=your_vercel_token
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 38-38: [UnorderedKey] The VERCEL_TEAM_ID key should go before the VERCEL_TOKEN key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.local.example around lines 35 - 38, Reorder the environment variables
in the file to maintain alphabetical order as required by dotenv-linter. Move
the VERCEL_TEAM_ID variable block (including its comment lines) to appear before
the VERCEL_TOKEN variable block. This ensures that VERCEL_TEAM_ID comes before
VERCEL_TOKEN alphabetically, satisfying the linting rule for conventional
environment variable ordering.

Source: Linters/SAST tools

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 @@ -897,6 +898,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
33 changes: 33 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>
Comment on lines +25 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add accessible labels to preview controls and iframe.

Line 25 and Line 28 render icon-only actions without accessible names, and Line 41 renders an unlabeled iframe. This makes the preview controls/frame hard to use with assistive tech.

Suggested fix
-          <Button variant="ghost" size="icon" className="h-7 w-7" onClick={reload}>
+          <Button
+            variant="ghost"
+            size="icon"
+            className="h-7 w-7"
+            onClick={reload}
+            aria-label="Reload sandbox preview"
+          >
             <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">
+            <a
+              href={url}
+              target="_blank"
+              rel="noopener noreferrer"
+              aria-label="Open sandbox preview in a new tab"
+            >
               <ExternalLink className="h-3.5 w-3.5" />
             </a>
           </Button>
@@
         <iframe
           key={key}
           src={url}
+          title="Sandbox live preview"
           className="w-full h-full border-none"
           sandbox="allow-scripts allow-same-origin allow-forms"
           onLoad={() => setIsLoading(false)}
         />

Also applies to: 41-47

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/sandbox-preview.tsx` around lines 25 - 31, The preview controls
and iframe lack accessible names; add explicit accessible labels to the
icon-only controls and the iframe: give the reload Button (onClick={reload}) an
aria-label="Reload preview" (or wrap the icon with a visually-hidden <span> for
screen readers), give the external link anchor or the Button with asChild an
aria-label="Open preview in new tab" (or include hidden text), and add a
descriptive title and/or aria-label to the iframe (e.g., title="Live preview of
user sandbox" and aria-label="Sandbox preview iframe") so assistive tech can
identify each control and the frame; update the elements referenced (the Button
with onClick={reload}, the Button asChild wrapping <a href={url} ...>, and the
iframe element) accordingly.

</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