Skip to content

Commit 77c16c9

Browse files
Add IDE panel toggle and integrate IDE into preview tabs
1 parent 07527b0 commit 77c16c9

3 files changed

Lines changed: 176 additions & 43 deletions

File tree

app/page.tsx

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ export default function Home() {
6060
const [errorsEncountered, setErrorsEncountered] = useState(0)
6161
const [messages, setMessages] = useState<Message[]>([]);
6262
const [fragment, setFragment] = useState<DeepPartial<FragmentSchema>>();
63-
const [currentTab, setCurrentTab] = useState<'code' | 'fragment' | 'terminal' | 'interpreter' | 'editor'>('code');
63+
const [currentTab, setCurrentTab] = useState<'code' | 'fragment' | 'terminal' | 'interpreter' | 'editor' | 'files' | 'ide'>('code');
6464
const [selectedFile, setSelectedFile] = useState<{ path: string; content: string } | null>(null);
6565
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
66+
const [isPreviewPanelOpen, setIsPreviewPanelOpen] = useState(false);
6667
const [isAuthDialogOpen, setAuthDialog] = useState(false);
6768
const [authView, setAuthView] = useState<ViewType>('sign_in')
6869
const [, setIsRateLimited] = useState(false)
@@ -426,25 +427,50 @@ export default function Home() {
426427
if (!session) return
427428

428429
try {
429-
const response = await fetch('/api/files/content', {
430-
method: 'POST',
431-
headers: {
432-
'Content-Type': 'application/json',
433-
},
434-
body: JSON.stringify({
435-
sessionID: session.user.id,
436-
path,
437-
content
438-
}),
439-
})
430+
// Check if this is a sandbox file (when result.sbxId exists)
431+
if (result?.sbxId) {
432+
// Save to sandbox
433+
const response = await fetch(`/api/sandbox/${result.sbxId}/files/content`, {
434+
method: 'POST',
435+
headers: {
436+
'Content-Type': 'application/json',
437+
},
438+
body: JSON.stringify({
439+
path,
440+
content
441+
}),
442+
})
440443

441-
if (response.ok) {
442-
// Update the selected file state if it matches the saved file
443-
if (selectedFile?.path === path) {
444-
setSelectedFile({ path, content })
444+
if (response.ok) {
445+
// Update selected file only if it's the same file being edited
446+
if (selectedFile?.path === path) {
447+
setSelectedFile({ path, content })
448+
}
449+
} else {
450+
console.error('Failed to save sandbox file:', response.statusText)
445451
}
446452
} else {
447-
console.error('Failed to save file:', response.statusText)
453+
// Save to IDE workspace
454+
const response = await fetch('/api/files/content', {
455+
method: 'POST',
456+
headers: {
457+
'Content-Type': 'application/json',
458+
},
459+
body: JSON.stringify({
460+
sessionID: session.user.id,
461+
path,
462+
content
463+
}),
464+
})
465+
466+
if (response.ok) {
467+
// Update selected file only if it's the same file being edited
468+
if (selectedFile?.path === path) {
469+
setSelectedFile({ path, content })
470+
}
471+
} else {
472+
console.error('Failed to save file:', response.statusText)
473+
}
448474
}
449475
} catch (error) {
450476
console.error('Error saving file:', error)
@@ -537,7 +563,7 @@ export default function Home() {
537563
session ? "ml-16" : ""
538564
)}>
539565
<div
540-
className={`flex flex-col w-full h-screen max-w-[800px] mx-auto px-4 ${fragment ? 'col-span-1' : 'col-span-2'}`}
566+
className={`flex flex-col w-full h-screen max-w-[800px] mx-auto px-4 ${fragment || isPreviewPanelOpen ? 'col-span-1' : 'col-span-2'}`}
541567
>
542568
<NavBar
543569
session={session}
@@ -548,6 +574,13 @@ export default function Home() {
548574
canClear={messages.length > 0}
549575
canUndo={messages.length > 1 && !isLoading}
550576
onUndo={handleUndo}
577+
onTogglePanel={() => {
578+
setIsPreviewPanelOpen(!isPreviewPanelOpen)
579+
if (!isPreviewPanelOpen) {
580+
setCurrentTab('ide')
581+
}
582+
}}
583+
isPanelOpen={isPreviewPanelOpen || !!fragment}
551584
/>
552585

553586
<div className="flex justify-center mb-4">
@@ -599,11 +632,15 @@ export default function Home() {
599632
isPreviewLoading={isPreviewLoading}
600633
fragment={fragment}
601634
result={result as ExecutionResult}
602-
onClose={() => setFragment(undefined)}
635+
onClose={() => {
636+
setFragment(undefined)
637+
setIsPreviewPanelOpen(false)
638+
}}
603639
code={fragment?.code || ''}
604640
selectedFile={selectedFile}
641+
onSelectFile={setSelectedFile}
605642
onSave={handleSaveFile}
606-
executeCode={handleExecuteCode}
643+
executeCode={handleExecuteCode}
607644
/>
608645
</div>
609646
</main>

components/navbar.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ import {
1818
GitHubLogoIcon,
1919
} from '@radix-ui/react-icons'
2020
import { Session } from '@supabase/supabase-js'
21-
import { ArrowRight, LogOut, Trash, Undo, Settings, Menu } from 'lucide-react'
21+
import { ArrowRight, LogOut, Trash, Undo, Settings, Menu, PanelRightOpen, PanelRightClose } from 'lucide-react'
2222
import Link from 'next/link'
2323
import Image from 'next/image'
24-
import { HeroPillSecond } from './announcement'
2524
import { ThemeToggle } from './theme-toggle'
2625

2726
export function NavBar({
@@ -33,6 +32,8 @@ export function NavBar({
3332
onSocialClick,
3433
onUndo,
3534
canUndo,
35+
onTogglePanel,
36+
isPanelOpen,
3637
}: {
3738
session: Session | null
3839
showLogin: () => void
@@ -42,6 +43,8 @@ export function NavBar({
4243
onSocialClick: (target: 'github' | 'x' | 'discord') => void
4344
onUndo: () => void
4445
canUndo: boolean
46+
onTogglePanel?: () => void
47+
isPanelOpen?: boolean
4548
}) {
4649
return (
4750
<nav className="w-full flex bg-transparent py-4">
@@ -59,6 +62,26 @@ export function NavBar({
5962
</Link>
6063
</div>
6164
<div className="flex items-center gap-1 md:gap-4">
65+
{session && onTogglePanel && (
66+
<TooltipProvider>
67+
<Tooltip delayDuration={0}>
68+
<TooltipTrigger asChild>
69+
<Button
70+
variant="ghost"
71+
size="icon"
72+
onClick={onTogglePanel}
73+
>
74+
{isPanelOpen ? (
75+
<PanelRightClose className="h-4 w-4 md:h-5 md:w-5" />
76+
) : (
77+
<PanelRightOpen className="h-4 w-4 md:h-5 md:w-5" />
78+
)}
79+
</Button>
80+
</TooltipTrigger>
81+
<TooltipContent>{isPanelOpen ? 'Close panel' : 'Open IDE panel'}</TooltipContent>
82+
</Tooltip>
83+
</TooltipProvider>
84+
)}
6285
<TooltipProvider>
6386
<Tooltip delayDuration={0}>
6487
<TooltipTrigger asChild>

components/preview.tsx

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { FragmentPreview } from './fragment-preview'
33
import { FragmentTerminal } from './fragment-terminal'
44
import { FragmentInterpreter } from './fragment-interpreter'
55
import { CodeEditor } from './code-editor'
6+
import { SandboxFileTree } from './sandbox-file-tree'
7+
import { IDE } from './ide'
68
import { Button } from '@/components/ui/button'
79
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
810
import {
@@ -14,8 +16,8 @@ import {
1416
import { FragmentSchema } from '@/lib/schema'
1517
import { ExecutionResult } from '@/lib/types'
1618
import { DeepPartial } from 'ai'
17-
import { ChevronsRight, LoaderCircle, Terminal, Code, FileCode } from 'lucide-react'
18-
import { Dispatch, SetStateAction } from 'react'
19+
import { ChevronsRight, LoaderCircle, Terminal, Code, FileCode, FolderTree, Folder } from 'lucide-react'
20+
import { Dispatch, SetStateAction, useState } from 'react'
1921

2022
export function Preview({
2123
teamID,
@@ -29,37 +31,77 @@ export function Preview({
2931
onClose,
3032
code,
3133
selectedFile,
34+
onSelectFile,
3235
onSave,
3336
executeCode,
3437
}: {
3538
teamID: string | undefined
3639
accessToken: string | undefined
37-
selectedTab: 'code' | 'fragment' | 'terminal' | 'interpreter' | 'editor'
38-
onSelectedTabChange: Dispatch<SetStateAction<'code' | 'fragment' | 'terminal' | 'interpreter' | 'editor'>>
40+
selectedTab: 'code' | 'fragment' | 'terminal' | 'interpreter' | 'editor' | 'files' | 'ide'
41+
onSelectedTabChange: Dispatch<SetStateAction<'code' | 'fragment' | 'terminal' | 'interpreter' | 'editor' | 'files' | 'ide'>>
3942
isChatLoading: boolean
4043
isPreviewLoading: boolean
4144
fragment?: DeepPartial<FragmentSchema>
4245
result?: ExecutionResult
4346
onClose: () => void
4447
code?: string
4548
selectedFile?: { path: string; content: string } | null
49+
onSelectFile?: (file: { path: string; content: string }) => void
4650
onSave?: (path: string, content: string) => Promise<void>
4751
executeCode?: (code: string) => Promise<any>
4852
}) {
49-
if (!fragment) {
50-
return null
53+
const [isRefreshingFiles, setIsRefreshingFiles] = useState(false)
54+
55+
async function handleSelectSandboxFile(path: string) {
56+
if (!result?.sbxId) return
57+
58+
try {
59+
const response = await fetch(`/api/sandbox/${result.sbxId}/files/content?path=${encodeURIComponent(path)}`)
60+
const data = await response.json()
61+
62+
if (response.ok && data.content !== undefined) {
63+
// Update the selected file in the parent component
64+
if (onSelectFile) {
65+
onSelectFile({ path: data.path, content: data.content })
66+
}
67+
// Switch to editor tab
68+
onSelectedTabChange('editor')
69+
}
70+
} catch (error) {
71+
console.error('Error loading sandbox file:', error)
72+
}
73+
}
74+
75+
async function handleRefreshFiles() {
76+
if (!result?.sbxId) return
77+
78+
setIsRefreshingFiles(true)
79+
try {
80+
const response = await fetch(`/api/sandbox/${result.sbxId}/files`)
81+
const data = await response.json()
82+
83+
if (response.ok && data.files) {
84+
// Files refreshed - this would need to update result.files in parent
85+
console.log('Files refreshed:', data.files)
86+
}
87+
} catch (error) {
88+
console.error('Error refreshing files:', error)
89+
} finally {
90+
setIsRefreshingFiles(false)
91+
}
5192
}
5293

5394
return (
5495
<div className="absolute md:relative z-10 top-0 left-0 shadow-2xl md:rounded-tl-3xl md:rounded-bl-3xl md:border-l md:border-y bg-popover h-full w-full overflow-auto">
5596
<Tabs
5697
value={selectedTab}
57-
onValueChange={(value) =>
58-
onSelectedTabChange(value as 'code' | 'fragment' | 'terminal' | 'interpreter' | 'editor')
59-
}
98+
onValueChange={(value) => {
99+
console.log('Tab changed to:', value)
100+
onSelectedTabChange(value as 'code' | 'fragment' | 'terminal' | 'interpreter' | 'editor' | 'files' | 'ide')
101+
}}
60102
className="h-full flex flex-col items-start justify-start"
61103
>
62-
<div className="w-full p-2 grid grid-cols-3 items-center border-b">
104+
<div className="w-full p-2 grid grid-cols-3 items-center border-b relative z-10">
63105
<TooltipProvider>
64106
<Tooltip delayDuration={0}>
65107
<TooltipTrigger asChild>
@@ -75,8 +117,8 @@ export function Preview({
75117
<TooltipContent>Close sidebar</TooltipContent>
76118
</Tooltip>
77119
</TooltipProvider>
78-
<div className="flex justify-center">
79-
<TabsList className="px-1 py-0 border h-8">
120+
<div className="flex justify-center relative z-20">
121+
<TabsList className="px-1 py-0 border h-8 relative z-30">
80122
<TabsTrigger
81123
className="font-normal text-xs py-1 px-2 gap-1 flex items-center"
82124
value="code"
@@ -120,28 +162,41 @@ export function Preview({
120162
Interpreter
121163
</TabsTrigger>
122164
<TabsTrigger
123-
disabled={!selectedFile}
124165
className="font-normal text-xs py-1 px-2 gap-1 flex items-center"
125166
value="editor"
126167
>
127168
<FileCode className="h-3 w-3" />
128169
Editor
129170
</TabsTrigger>
171+
<TabsTrigger
172+
disabled={!result || !result.files || result.files.length === 0}
173+
className="font-normal text-xs py-1 px-2 gap-1 flex items-center"
174+
value="files"
175+
>
176+
<FolderTree className="h-3 w-3" />
177+
Files
178+
</TabsTrigger>
179+
<TabsTrigger
180+
className="font-normal text-xs py-1 px-2 gap-1 flex items-center"
181+
value="ide"
182+
>
183+
<Folder className="h-3 w-3" />
184+
IDE
185+
</TabsTrigger>
130186
</TabsList>
131187
</div>
132188
<div className="flex items-center justify-end gap-2">
133189
{/* Add any additional buttons here */}
134190
</div>
135191
</div>
136-
{fragment && (
137-
<div className="overflow-y-auto w-full h-full">
192+
<div className="overflow-y-auto w-full h-full">
138193
<TabsContent value="code" className="h-full">
139-
{fragment.code ? (
194+
{fragment?.code ? (
140195
<FragmentCode
141196
files={[
142197
{
143-
name: fragment.file_path || 'code.txt',
144-
content: fragment.code || '',
198+
name: fragment?.file_path || 'code.txt',
199+
content: fragment?.code || '',
145200
},
146201
]}
147202
/>
@@ -155,7 +210,7 @@ export function Preview({
155210
{result ? (
156211
<FragmentPreview
157212
result={result as ExecutionResult}
158-
code={code || fragment.code || ''}
213+
code={code || fragment?.code || ''}
159214
executeCode={executeCode || (async () => {})}
160215
/>
161216
) : (
@@ -181,7 +236,7 @@ export function Preview({
181236
{result && result.template === 'code-interpreter-v1' ? (
182237
<FragmentInterpreter
183238
result={result}
184-
code={code || fragment.code || ''}
239+
code={code || fragment?.code || ''}
185240
executeCode={executeCode || (async () => {})}
186241
/>
187242
) : result ? (
@@ -211,8 +266,26 @@ export function Preview({
211266
</div>
212267
)}
213268
</TabsContent>
269+
<TabsContent value="files" className="h-full">
270+
{result && result.files && result.files.length > 0 ? (
271+
<SandboxFileTree
272+
files={result.files}
273+
onSelectFile={handleSelectSandboxFile}
274+
onRefresh={handleRefreshFiles}
275+
isLoading={isRefreshingFiles}
276+
/>
277+
) : (
278+
<div className="flex items-center justify-center h-full text-muted-foreground">
279+
No sandbox files available
280+
</div>
281+
)}
282+
</TabsContent>
283+
<TabsContent value="ide" className="h-full m-0 p-0">
284+
<div className="h-full w-full">
285+
<IDE />
286+
</div>
287+
</TabsContent>
214288
</div>
215-
)}
216289
</Tabs>
217290
</div>
218291
)

0 commit comments

Comments
 (0)