Spaces:
Running
Running
github update
Browse files- README.md +2 -2
- app/components/Desktop.tsx +22 -111
- app/components/Dock.tsx +7 -16
- app/components/DraggableDesktopIcon.tsx +6 -12
- app/components/FileManager.tsx +261 -141
- app/components/FlutterRunner.tsx +279 -240
- app/components/LaTeXEditor.tsx +263 -283
- app/components/Terminal.tsx +0 -227
- app/components/WebBrowser.tsx +0 -277
- app/components/WebBrowserApp.tsx +0 -441
README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
---
|
| 2 |
title: ReubenOS
|
| 3 |
emoji: 🖥️
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
| 8 |
pinned: false
|
|
|
|
| 1 |
---
|
| 2 |
title: ReubenOS
|
| 3 |
emoji: 🖥️
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
| 8 |
pinned: false
|
app/components/Desktop.tsx
CHANGED
|
@@ -10,10 +10,10 @@ import { DraggableDesktopIcon } from './DraggableDesktopIcon'
|
|
| 10 |
import { HelpModal } from './HelpModal'
|
| 11 |
import { DesktopContextMenu } from './DesktopContextMenu'
|
| 12 |
import { BackgroundSelector } from './BackgroundSelector'
|
| 13 |
-
|
| 14 |
import { GeminiChat } from './GeminiChat'
|
| 15 |
import { Clock } from './Clock'
|
| 16 |
-
|
| 17 |
import { SpotlightSearch } from './SpotlightSearch'
|
| 18 |
import { ContextMenu } from './ContextMenu'
|
| 19 |
import { AboutModal } from './AboutModal'
|
|
@@ -41,9 +41,9 @@ export function Desktop() {
|
|
| 41 |
const [fileManagerOpen, setFileManagerOpen] = useState(true)
|
| 42 |
const [calendarOpen, setCalendarOpen] = useState(false)
|
| 43 |
const [clockOpen, setClockOpen] = useState(false)
|
| 44 |
-
|
| 45 |
const [geminiChatOpen, setGeminiChatOpen] = useState(false)
|
| 46 |
-
|
| 47 |
const [spotlightOpen, setSpotlightOpen] = useState(false)
|
| 48 |
const [contextMenuVisible, setContextMenuVisible] = useState(false)
|
| 49 |
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
|
|
@@ -69,9 +69,9 @@ export function Desktop() {
|
|
| 69 |
const [fileManagerMinimized, setFileManagerMinimized] = useState(false)
|
| 70 |
const [calendarMinimized, setCalendarMinimized] = useState(false)
|
| 71 |
const [clockMinimized, setClockMinimized] = useState(false)
|
| 72 |
-
|
| 73 |
const [geminiChatMinimized, setGeminiChatMinimized] = useState(false)
|
| 74 |
-
|
| 75 |
const [sessionManagerMinimized, setSessionManagerMinimized] = useState(false)
|
| 76 |
const [flutterRunnerMinimized, setFlutterRunnerMinimized] = useState(false)
|
| 77 |
const [researchBrowserMinimized, setResearchBrowserMinimized] = useState(false)
|
|
@@ -108,14 +108,7 @@ export function Desktop() {
|
|
| 108 |
setClockOpen(false)
|
| 109 |
}
|
| 110 |
|
| 111 |
-
const openBrowser = () => {
|
| 112 |
-
setBrowserOpen(true)
|
| 113 |
-
setBrowserMinimized(false)
|
| 114 |
-
}
|
| 115 |
|
| 116 |
-
const closeBrowser = () => {
|
| 117 |
-
setBrowserOpen(false)
|
| 118 |
-
}
|
| 119 |
|
| 120 |
const openGeminiChat = () => {
|
| 121 |
setGeminiChatOpen(true)
|
|
@@ -126,14 +119,7 @@ export function Desktop() {
|
|
| 126 |
setGeminiChatOpen(false)
|
| 127 |
}
|
| 128 |
|
| 129 |
-
const openTerminal = () => {
|
| 130 |
-
setTerminalOpen(true)
|
| 131 |
-
setTerminalMinimized(false)
|
| 132 |
-
}
|
| 133 |
|
| 134 |
-
const closeTerminal = () => {
|
| 135 |
-
setTerminalOpen(false)
|
| 136 |
-
}
|
| 137 |
|
| 138 |
const openSessionManager = () => {
|
| 139 |
setSessionManagerOpen(true)
|
|
@@ -193,15 +179,11 @@ export function Desktop() {
|
|
| 193 |
case 'clock':
|
| 194 |
openClock()
|
| 195 |
break
|
| 196 |
-
|
| 197 |
-
openBrowser()
|
| 198 |
-
break
|
| 199 |
case 'gemini':
|
| 200 |
openGeminiChat()
|
| 201 |
break
|
| 202 |
-
|
| 203 |
-
openTerminal()
|
| 204 |
-
break
|
| 205 |
case 'sessions':
|
| 206 |
openSessionManager()
|
| 207 |
break
|
|
@@ -420,19 +402,7 @@ export function Desktop() {
|
|
| 420 |
})
|
| 421 |
}
|
| 422 |
|
| 423 |
-
|
| 424 |
-
minimizedApps.push({
|
| 425 |
-
id: 'browser',
|
| 426 |
-
label: 'Browser',
|
| 427 |
-
icon: (
|
| 428 |
-
<div className="bg-white w-full h-full rounded-xl overflow-hidden relative flex items-center justify-center">
|
| 429 |
-
<div className="absolute inset-0 bg-gradient-to-tr from-blue-500 to-cyan-300" />
|
| 430 |
-
<Globe size={20} weight="light" className="text-white relative z-10" />
|
| 431 |
-
</div>
|
| 432 |
-
),
|
| 433 |
-
onRestore: () => setBrowserMinimized(false)
|
| 434 |
-
})
|
| 435 |
-
}
|
| 436 |
|
| 437 |
if (geminiChatMinimized && geminiChatOpen) {
|
| 438 |
minimizedApps.push({
|
|
@@ -447,18 +417,7 @@ export function Desktop() {
|
|
| 447 |
})
|
| 448 |
}
|
| 449 |
|
| 450 |
-
|
| 451 |
-
minimizedApps.push({
|
| 452 |
-
id: 'terminal',
|
| 453 |
-
label: 'Terminal',
|
| 454 |
-
icon: (
|
| 455 |
-
<div className="bg-black w-full h-full rounded-xl flex items-center justify-center border border-gray-700">
|
| 456 |
-
<TerminalIcon size={20} weight="bold" className="text-green-400" />
|
| 457 |
-
</div>
|
| 458 |
-
),
|
| 459 |
-
onRestore: () => setTerminalMinimized(false)
|
| 460 |
-
})
|
| 461 |
-
}
|
| 462 |
|
| 463 |
if (sessionManagerMinimized && sessionManagerOpen) {
|
| 464 |
minimizedApps.push({
|
|
@@ -561,13 +520,12 @@ export function Desktop() {
|
|
| 561 |
onOpenFileManager={openFileManager}
|
| 562 |
onOpenCalendar={openCalendar}
|
| 563 |
onOpenClock={openClock}
|
| 564 |
-
|
| 565 |
onOpenGeminiChat={openGeminiChat}
|
| 566 |
openApps={{
|
| 567 |
files: fileManagerOpen,
|
| 568 |
calendar: calendarOpen,
|
| 569 |
clock: clockOpen,
|
| 570 |
-
browser: browserOpen,
|
| 571 |
gemini: geminiChatOpen
|
| 572 |
}}
|
| 573 |
minimizedApps={minimizedApps}
|
|
@@ -586,16 +544,7 @@ export function Desktop() {
|
|
| 586 |
onDoubleClick={() => openFileManager('')}
|
| 587 |
/>
|
| 588 |
</div>
|
| 589 |
-
|
| 590 |
-
<DraggableDesktopIcon
|
| 591 |
-
id="browser"
|
| 592 |
-
label="Browser"
|
| 593 |
-
iconType="browser"
|
| 594 |
-
initialPosition={{ x: 0, y: 0 }}
|
| 595 |
-
onClick={() => { }}
|
| 596 |
-
onDoubleClick={openBrowser}
|
| 597 |
-
/>
|
| 598 |
-
</div>
|
| 599 |
<div className="pointer-events-auto w-24 h-24">
|
| 600 |
<DraggableDesktopIcon
|
| 601 |
id="gemini"
|
|
@@ -626,16 +575,7 @@ export function Desktop() {
|
|
| 626 |
onDoubleClick={openCalendar}
|
| 627 |
/>
|
| 628 |
</div>
|
| 629 |
-
|
| 630 |
-
<DraggableDesktopIcon
|
| 631 |
-
id="terminal"
|
| 632 |
-
label="Terminal"
|
| 633 |
-
iconType="terminal"
|
| 634 |
-
initialPosition={{ x: 0, y: 0 }}
|
| 635 |
-
onClick={() => { }}
|
| 636 |
-
onDoubleClick={openTerminal}
|
| 637 |
-
/>
|
| 638 |
-
</div>
|
| 639 |
<div className="pointer-events-auto w-24 h-24">
|
| 640 |
<DraggableDesktopIcon
|
| 641 |
id="harddrive"
|
|
@@ -711,6 +651,7 @@ export function Desktop() {
|
|
| 711 |
onOpenFlutterApp={openFlutterRunner}
|
| 712 |
onMinimize={() => setFileManagerMinimized(true)}
|
| 713 |
sessionId={userSession}
|
|
|
|
| 714 |
/>
|
| 715 |
</motion.div>
|
| 716 |
)}
|
|
@@ -753,43 +694,9 @@ export function Desktop() {
|
|
| 753 |
</motion.div>
|
| 754 |
)}
|
| 755 |
|
| 756 |
-
{browserOpen && (
|
| 757 |
-
<motion.div
|
| 758 |
-
key="browser"
|
| 759 |
-
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 760 |
-
animate={{
|
| 761 |
-
scale: browserMinimized ? 0.1 : 1,
|
| 762 |
-
opacity: browserMinimized ? 0 : 1,
|
| 763 |
-
y: browserMinimized ? 400 : 0,
|
| 764 |
-
x: browserMinimized ? '-20%' : 0
|
| 765 |
-
}}
|
| 766 |
-
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 767 |
-
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 768 |
-
className="absolute inset-0"
|
| 769 |
-
style={{ pointerEvents: browserMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 770 |
-
>
|
| 771 |
-
<WebBrowserApp onClose={closeBrowser} onMinimize={() => setBrowserMinimized(true)} />
|
| 772 |
-
</motion.div>
|
| 773 |
-
)}
|
| 774 |
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
key="terminal"
|
| 778 |
-
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 779 |
-
animate={{
|
| 780 |
-
scale: terminalMinimized ? 0.1 : 1,
|
| 781 |
-
opacity: terminalMinimized ? 0 : 1,
|
| 782 |
-
y: terminalMinimized ? 400 : 0,
|
| 783 |
-
x: terminalMinimized ? '30%' : 0
|
| 784 |
-
}}
|
| 785 |
-
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 786 |
-
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 787 |
-
className="absolute inset-0"
|
| 788 |
-
style={{ pointerEvents: terminalMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 789 |
-
>
|
| 790 |
-
<Terminal onClose={closeTerminal} onMinimize={() => setTerminalMinimized(true)} />
|
| 791 |
-
</motion.div>
|
| 792 |
-
)}
|
| 793 |
|
| 794 |
{sessionManagerOpen && sessionInitialized && (
|
| 795 |
<motion.div
|
|
@@ -831,8 +738,12 @@ export function Desktop() {
|
|
| 831 |
style={{ pointerEvents: flutterRunnerMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 832 |
>
|
| 833 |
<FlutterRunner
|
| 834 |
-
|
| 835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
onMinimize={() => setFlutterRunnerMinimized(true)}
|
| 837 |
/>
|
| 838 |
</motion.div>
|
|
|
|
| 10 |
import { HelpModal } from './HelpModal'
|
| 11 |
import { DesktopContextMenu } from './DesktopContextMenu'
|
| 12 |
import { BackgroundSelector } from './BackgroundSelector'
|
| 13 |
+
|
| 14 |
import { GeminiChat } from './GeminiChat'
|
| 15 |
import { Clock } from './Clock'
|
| 16 |
+
|
| 17 |
import { SpotlightSearch } from './SpotlightSearch'
|
| 18 |
import { ContextMenu } from './ContextMenu'
|
| 19 |
import { AboutModal } from './AboutModal'
|
|
|
|
| 41 |
const [fileManagerOpen, setFileManagerOpen] = useState(true)
|
| 42 |
const [calendarOpen, setCalendarOpen] = useState(false)
|
| 43 |
const [clockOpen, setClockOpen] = useState(false)
|
| 44 |
+
|
| 45 |
const [geminiChatOpen, setGeminiChatOpen] = useState(false)
|
| 46 |
+
|
| 47 |
const [spotlightOpen, setSpotlightOpen] = useState(false)
|
| 48 |
const [contextMenuVisible, setContextMenuVisible] = useState(false)
|
| 49 |
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
|
|
|
|
| 69 |
const [fileManagerMinimized, setFileManagerMinimized] = useState(false)
|
| 70 |
const [calendarMinimized, setCalendarMinimized] = useState(false)
|
| 71 |
const [clockMinimized, setClockMinimized] = useState(false)
|
| 72 |
+
|
| 73 |
const [geminiChatMinimized, setGeminiChatMinimized] = useState(false)
|
| 74 |
+
|
| 75 |
const [sessionManagerMinimized, setSessionManagerMinimized] = useState(false)
|
| 76 |
const [flutterRunnerMinimized, setFlutterRunnerMinimized] = useState(false)
|
| 77 |
const [researchBrowserMinimized, setResearchBrowserMinimized] = useState(false)
|
|
|
|
| 108 |
setClockOpen(false)
|
| 109 |
}
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
const openGeminiChat = () => {
|
| 114 |
setGeminiChatOpen(true)
|
|
|
|
| 119 |
setGeminiChatOpen(false)
|
| 120 |
}
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
const openSessionManager = () => {
|
| 125 |
setSessionManagerOpen(true)
|
|
|
|
| 179 |
case 'clock':
|
| 180 |
openClock()
|
| 181 |
break
|
| 182 |
+
|
|
|
|
|
|
|
| 183 |
case 'gemini':
|
| 184 |
openGeminiChat()
|
| 185 |
break
|
| 186 |
+
|
|
|
|
|
|
|
| 187 |
case 'sessions':
|
| 188 |
openSessionManager()
|
| 189 |
break
|
|
|
|
| 402 |
})
|
| 403 |
}
|
| 404 |
|
| 405 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
|
| 407 |
if (geminiChatMinimized && geminiChatOpen) {
|
| 408 |
minimizedApps.push({
|
|
|
|
| 417 |
})
|
| 418 |
}
|
| 419 |
|
| 420 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
|
| 422 |
if (sessionManagerMinimized && sessionManagerOpen) {
|
| 423 |
minimizedApps.push({
|
|
|
|
| 520 |
onOpenFileManager={openFileManager}
|
| 521 |
onOpenCalendar={openCalendar}
|
| 522 |
onOpenClock={openClock}
|
| 523 |
+
|
| 524 |
onOpenGeminiChat={openGeminiChat}
|
| 525 |
openApps={{
|
| 526 |
files: fileManagerOpen,
|
| 527 |
calendar: calendarOpen,
|
| 528 |
clock: clockOpen,
|
|
|
|
| 529 |
gemini: geminiChatOpen
|
| 530 |
}}
|
| 531 |
minimizedApps={minimizedApps}
|
|
|
|
| 544 |
onDoubleClick={() => openFileManager('')}
|
| 545 |
/>
|
| 546 |
</div>
|
| 547 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
<div className="pointer-events-auto w-24 h-24">
|
| 549 |
<DraggableDesktopIcon
|
| 550 |
id="gemini"
|
|
|
|
| 575 |
onDoubleClick={openCalendar}
|
| 576 |
/>
|
| 577 |
</div>
|
| 578 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 579 |
<div className="pointer-events-auto w-24 h-24">
|
| 580 |
<DraggableDesktopIcon
|
| 581 |
id="harddrive"
|
|
|
|
| 651 |
onOpenFlutterApp={openFlutterRunner}
|
| 652 |
onMinimize={() => setFileManagerMinimized(true)}
|
| 653 |
sessionId={userSession}
|
| 654 |
+
onOpenApp={handleOpenApp}
|
| 655 |
/>
|
| 656 |
</motion.div>
|
| 657 |
)}
|
|
|
|
| 694 |
</motion.div>
|
| 695 |
)}
|
| 696 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
|
| 698 |
+
|
| 699 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 700 |
|
| 701 |
{sessionManagerOpen && sessionInitialized && (
|
| 702 |
<motion.div
|
|
|
|
| 738 |
style={{ pointerEvents: flutterRunnerMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 739 |
>
|
| 740 |
<FlutterRunner
|
| 741 |
+
initialCode={activeFlutterApp?.dartCode}
|
| 742 |
+
sessionId={userSession}
|
| 743 |
+
onClose={() => {
|
| 744 |
+
setFlutterRunnerOpen(false)
|
| 745 |
+
setActiveFlutterApp(null)
|
| 746 |
+
}}
|
| 747 |
onMinimize={() => setFlutterRunnerMinimized(true)}
|
| 748 |
/>
|
| 749 |
</motion.div>
|
app/components/Dock.tsx
CHANGED
|
@@ -5,11 +5,14 @@ import {
|
|
| 5 |
Folder,
|
| 6 |
Calendar as CalendarIcon,
|
| 7 |
Clock as ClockIcon,
|
| 8 |
-
|
| 9 |
Sparkle,
|
| 10 |
Trash,
|
| 11 |
FolderOpen,
|
| 12 |
-
Compass
|
|
|
|
|
|
|
|
|
|
| 13 |
} from '@phosphor-icons/react'
|
| 14 |
import { motion } from 'framer-motion'
|
| 15 |
import { DynamicClockIcon } from './DynamicClockIcon'
|
|
@@ -26,7 +29,7 @@ interface DockProps {
|
|
| 26 |
onOpenFileManager: (path: string) => void
|
| 27 |
onOpenCalendar: () => void
|
| 28 |
onOpenClock: () => void
|
| 29 |
-
|
| 30 |
onOpenGeminiChat: () => void
|
| 31 |
openApps: { [key: string]: boolean }
|
| 32 |
minimizedApps?: MinimizedApp[]
|
|
@@ -70,7 +73,7 @@ export function Dock({
|
|
| 70 |
onOpenFileManager,
|
| 71 |
onOpenCalendar,
|
| 72 |
onOpenClock,
|
| 73 |
-
|
| 74 |
onOpenGeminiChat,
|
| 75 |
openApps,
|
| 76 |
minimizedApps = []
|
|
@@ -89,18 +92,6 @@ export function Dock({
|
|
| 89 |
isActive: openApps['files'],
|
| 90 |
className: ''
|
| 91 |
},
|
| 92 |
-
{
|
| 93 |
-
icon: (
|
| 94 |
-
<div className="bg-white w-full h-full rounded-xl overflow-hidden relative flex items-center justify-center">
|
| 95 |
-
<div className="absolute inset-0 bg-gradient-to-tr from-blue-500 to-cyan-300" />
|
| 96 |
-
<Compass size={28} weight="light" className="text-white relative z-10 md:scale-125" />
|
| 97 |
-
</div>
|
| 98 |
-
),
|
| 99 |
-
label: 'Browser',
|
| 100 |
-
onClick: onOpenBrowser,
|
| 101 |
-
isActive: openApps['browser'],
|
| 102 |
-
className: ''
|
| 103 |
-
},
|
| 104 |
{
|
| 105 |
icon: (
|
| 106 |
<div className="bg-white w-full h-full rounded-xl flex items-center justify-center border border-gray-200">
|
|
|
|
| 5 |
Folder,
|
| 6 |
Calendar as CalendarIcon,
|
| 7 |
Clock as ClockIcon,
|
| 8 |
+
|
| 9 |
Sparkle,
|
| 10 |
Trash,
|
| 11 |
FolderOpen,
|
| 12 |
+
Compass,
|
| 13 |
+
Flask,
|
| 14 |
+
DeviceMobile,
|
| 15 |
+
Function
|
| 16 |
} from '@phosphor-icons/react'
|
| 17 |
import { motion } from 'framer-motion'
|
| 18 |
import { DynamicClockIcon } from './DynamicClockIcon'
|
|
|
|
| 29 |
onOpenFileManager: (path: string) => void
|
| 30 |
onOpenCalendar: () => void
|
| 31 |
onOpenClock: () => void
|
| 32 |
+
|
| 33 |
onOpenGeminiChat: () => void
|
| 34 |
openApps: { [key: string]: boolean }
|
| 35 |
minimizedApps?: MinimizedApp[]
|
|
|
|
| 73 |
onOpenFileManager,
|
| 74 |
onOpenCalendar,
|
| 75 |
onOpenClock,
|
| 76 |
+
|
| 77 |
onOpenGeminiChat,
|
| 78 |
openApps,
|
| 79 |
minimizedApps = []
|
|
|
|
| 92 |
isActive: openApps['files'],
|
| 93 |
className: ''
|
| 94 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
{
|
| 96 |
icon: (
|
| 97 |
<div className="bg-white w-full h-full rounded-xl flex items-center justify-center border border-gray-200">
|
app/components/DraggableDesktopIcon.tsx
CHANGED
|
@@ -8,9 +8,10 @@ import {
|
|
| 8 |
Clock,
|
| 9 |
Globe,
|
| 10 |
Sparkle,
|
| 11 |
-
|
|
|
|
|
|
|
| 12 |
HardDrives,
|
| 13 |
-
Compass,
|
| 14 |
Code,
|
| 15 |
Lightning,
|
| 16 |
Key
|
|
@@ -65,11 +66,10 @@ export function DraggableDesktopIcon({
|
|
| 65 |
<DynamicClockIcon />
|
| 66 |
</div>
|
| 67 |
)
|
| 68 |
-
case '
|
| 69 |
return (
|
| 70 |
-
<div className="bg-
|
| 71 |
-
<
|
| 72 |
-
<Compass size={36} weight="light" className="text-white relative z-10" />
|
| 73 |
</div>
|
| 74 |
)
|
| 75 |
case 'gemini':
|
|
@@ -78,12 +78,6 @@ export function DraggableDesktopIcon({
|
|
| 78 |
<Sparkle size={32} weight="fill" className="text-blue-500" />
|
| 79 |
</div>
|
| 80 |
)
|
| 81 |
-
case 'terminal':
|
| 82 |
-
return (
|
| 83 |
-
<div className="bg-gray-800 w-full h-full rounded-xl flex items-center justify-center border border-gray-600">
|
| 84 |
-
<span className="text-white font-mono font-bold text-xl">>_</span>
|
| 85 |
-
</div>
|
| 86 |
-
)
|
| 87 |
case 'harddrive':
|
| 88 |
return (
|
| 89 |
<div className="bg-gray-200 w-full h-full rounded-lg flex items-center justify-center border border-gray-300 relative">
|
|
|
|
| 8 |
Clock,
|
| 9 |
Globe,
|
| 10 |
Sparkle,
|
| 11 |
+
Flask,
|
| 12 |
+
DeviceMobile,
|
| 13 |
+
Function,
|
| 14 |
HardDrives,
|
|
|
|
| 15 |
Code,
|
| 16 |
Lightning,
|
| 17 |
Key
|
|
|
|
| 66 |
<DynamicClockIcon />
|
| 67 |
</div>
|
| 68 |
)
|
| 69 |
+
case 'research':
|
| 70 |
return (
|
| 71 |
+
<div className="bg-gradient-to-br from-purple-400 to-pink-500 w-full h-full rounded-xl flex items-center justify-center shadow-lg">
|
| 72 |
+
<Flask size={32} weight="fill" className="text-white" />
|
|
|
|
| 73 |
</div>
|
| 74 |
)
|
| 75 |
case 'gemini':
|
|
|
|
| 78 |
<Sparkle size={32} weight="fill" className="text-blue-500" />
|
| 79 |
</div>
|
| 80 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
case 'harddrive':
|
| 82 |
return (
|
| 83 |
<div className="bg-gray-200 w-full h-full rounded-lg flex items-center justify-center border border-gray-300 relative">
|
app/components/FileManager.tsx
CHANGED
|
@@ -22,7 +22,16 @@ import {
|
|
| 22 |
Eye,
|
| 23 |
Users,
|
| 24 |
Globe,
|
| 25 |
-
Code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
} from '@phosphor-icons/react'
|
| 27 |
import { motion } from 'framer-motion'
|
| 28 |
import { FilePreview } from './FilePreview'
|
|
@@ -36,6 +45,7 @@ interface FileManagerProps {
|
|
| 36 |
onMaximize?: () => void
|
| 37 |
onOpenFlutterApp?: (appFile: any) => void
|
| 38 |
sessionId?: string
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
interface FileItem {
|
|
@@ -50,7 +60,7 @@ interface FileItem {
|
|
| 50 |
pubspecYaml?: string
|
| 51 |
}
|
| 52 |
|
| 53 |
-
export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp, onMinimize, onMaximize, sessionId }: FileManagerProps) {
|
| 54 |
const [files, setFiles] = useState<FileItem[]>([])
|
| 55 |
const [loading, setLoading] = useState(true)
|
| 56 |
const [searchQuery, setSearchQuery] = useState('')
|
|
@@ -58,14 +68,35 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 58 |
const [uploadModalOpen, setUploadModalOpen] = useState(false)
|
| 59 |
const [previewFile, setPreviewFile] = useState<FileItem | null>(null)
|
| 60 |
const [isPublicFolder, setIsPublicFolder] = useState(false)
|
|
|
|
| 61 |
|
| 62 |
// Load files when path changes or sessionId becomes available
|
| 63 |
useEffect(() => {
|
| 64 |
// Check if this is the public folder
|
| 65 |
setIsPublicFolder(currentPath === 'public' || currentPath.startsWith('public/'))
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
// Only load files if we have a sessionId (for non-public folders) or if it's a public folder
|
| 68 |
const isPublic = currentPath === 'public' || currentPath.startsWith('public/')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
if (isPublic || sessionId) {
|
| 70 |
loadFiles()
|
| 71 |
}
|
|
@@ -96,7 +127,7 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 96 |
return
|
| 97 |
}
|
| 98 |
|
| 99 |
-
// Add public folder to root directory
|
| 100 |
if (currentPath === '') {
|
| 101 |
const publicFolder = {
|
| 102 |
name: 'Public Folder',
|
|
@@ -230,7 +261,7 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 230 |
if (file.path === 'public' || file.name === 'Public Folder') {
|
| 231 |
return <Users size={48} weight="fill" className="text-purple-400" />
|
| 232 |
}
|
| 233 |
-
return <FolderIcon size={48} weight="fill" className="text-
|
| 234 |
}
|
| 235 |
|
| 236 |
if (file.type === 'flutter_app') {
|
|
@@ -279,6 +310,31 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 279 |
return !relativePath.includes('/')
|
| 280 |
})
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
return (
|
| 283 |
<>
|
| 284 |
<Window
|
|
@@ -288,158 +344,222 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 288 |
onClose={onClose}
|
| 289 |
onMinimize={onMinimize}
|
| 290 |
onMaximize={onMaximize}
|
| 291 |
-
width={
|
| 292 |
-
height={
|
| 293 |
x={60}
|
| 294 |
y={60}
|
| 295 |
className="file-manager-window"
|
| 296 |
>
|
| 297 |
-
<div className="flex
|
| 298 |
-
{/*
|
| 299 |
-
<div className="
|
| 300 |
-
<div className="
|
|
|
|
|
|
|
|
|
|
| 301 |
<button
|
| 302 |
-
onClick={() =>
|
| 303 |
-
|
| 304 |
-
onNavigate(parent)
|
| 305 |
-
}}
|
| 306 |
-
disabled={!currentPath}
|
| 307 |
-
className="p-1.5 hover:bg-[#e8e8e8] rounded disabled:opacity-40 disabled:hover:bg-transparent"
|
| 308 |
>
|
| 309 |
-
<
|
|
|
|
| 310 |
</button>
|
| 311 |
<button
|
| 312 |
-
onClick={() =>
|
| 313 |
-
className=
|
| 314 |
>
|
| 315 |
-
<
|
|
|
|
| 316 |
</button>
|
| 317 |
-
</div>
|
| 318 |
-
|
| 319 |
-
<div className="flex items-center gap-1 px-2 py-1.5 bg-white border border-[#ddd] rounded flex-1">
|
| 320 |
-
<House size={14} weight="regular" className="text-[#666]" />
|
| 321 |
-
<span className="text-xs text-[#666]">/</span>
|
| 322 |
-
<span className="text-xs text-[#2c2c2c]">data/documents/{currentPath}</span>
|
| 323 |
-
</div>
|
| 324 |
-
|
| 325 |
-
<div className="flex items-center gap-1">
|
| 326 |
<button
|
| 327 |
-
onClick={
|
| 328 |
-
className=
|
| 329 |
-
title="New Folder"
|
| 330 |
>
|
| 331 |
-
<
|
|
|
|
| 332 |
</button>
|
| 333 |
<button
|
| 334 |
-
onClick={() =>
|
| 335 |
-
className=
|
| 336 |
-
title="Upload File"
|
| 337 |
>
|
| 338 |
-
<
|
|
|
|
| 339 |
</button>
|
| 340 |
-
<
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
/>
|
| 349 |
-
</div>
|
| 350 |
-
</div>
|
| 351 |
</div>
|
| 352 |
|
| 353 |
-
{/*
|
| 354 |
-
<div className="flex-1
|
| 355 |
-
{
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
</div>
|
| 363 |
-
|
| 364 |
-
<div className="flex items-center
|
| 365 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
</div>
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
<button
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
} else if (file.type === 'flutter_app' && onOpenFlutterApp) {
|
| 379 |
-
onOpenFlutterApp(file)
|
| 380 |
-
} else {
|
| 381 |
-
handlePreview(file)
|
| 382 |
-
}
|
| 383 |
-
}}
|
| 384 |
-
className="flex flex-col items-center gap-2 p-2 hover:bg-[#f0f0f0] rounded w-full"
|
| 385 |
>
|
| 386 |
-
<div className="w-16 h-16 flex items-center justify-center">
|
| 387 |
-
{
|
| 388 |
</div>
|
| 389 |
-
<span className="text-
|
| 390 |
-
{file.name}
|
| 391 |
-
</span>
|
| 392 |
-
{file.size && (
|
| 393 |
-
<span className="text-xs text-[#666]">
|
| 394 |
-
{formatFileSize(file.size)}
|
| 395 |
-
</span>
|
| 396 |
-
)}
|
| 397 |
</button>
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
|
|
|
| 419 |
>
|
| 420 |
-
<
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
</div>
|
| 444 |
</div>
|
| 445 |
</Window>
|
|
@@ -485,45 +605,45 @@ function UploadModal({
|
|
| 485 |
}
|
| 486 |
|
| 487 |
return (
|
| 488 |
-
<div className="fixed inset-0 bg-black/
|
| 489 |
-
<div className="bg-white rounded-
|
| 490 |
-
<h2 className="text-lg font-semibold mb-4">Upload File</h2>
|
| 491 |
|
| 492 |
<div className="space-y-4">
|
| 493 |
<div>
|
| 494 |
-
<label className="block text-sm font-medium mb-2">Select File</label>
|
| 495 |
<input
|
| 496 |
type="file"
|
| 497 |
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
|
| 498 |
-
className="w-full p-2 border rounded"
|
| 499 |
/>
|
| 500 |
</div>
|
| 501 |
|
| 502 |
<div>
|
| 503 |
-
<label className="block text-sm font-medium mb-2">Upload to Folder</label>
|
| 504 |
<input
|
| 505 |
type="text"
|
| 506 |
value={targetFolder}
|
| 507 |
onChange={(e) => setTargetFolder(e.target.value)}
|
| 508 |
placeholder="e.g., homework/math"
|
| 509 |
-
className="w-full p-2 border rounded"
|
| 510 |
/>
|
| 511 |
-
<p className="text-xs text-gray-
|
| 512 |
Leave empty for root, or enter path like "folder/subfolder"
|
| 513 |
</p>
|
| 514 |
</div>
|
| 515 |
|
| 516 |
-
<div className="flex justify-end gap-2">
|
| 517 |
<button
|
| 518 |
onClick={onClose}
|
| 519 |
-
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
| 520 |
>
|
| 521 |
Cancel
|
| 522 |
</button>
|
| 523 |
<button
|
| 524 |
onClick={handleSubmit}
|
| 525 |
disabled={!selectedFile}
|
| 526 |
-
className="px-4 py-2 bg-
|
| 527 |
>
|
| 528 |
Upload
|
| 529 |
</button>
|
|
|
|
| 22 |
Eye,
|
| 23 |
Users,
|
| 24 |
Globe,
|
| 25 |
+
Code,
|
| 26 |
+
AppWindow,
|
| 27 |
+
Desktop,
|
| 28 |
+
DownloadSimple,
|
| 29 |
+
Flask,
|
| 30 |
+
DeviceMobile,
|
| 31 |
+
Function,
|
| 32 |
+
Calendar as CalendarIcon,
|
| 33 |
+
Clock as ClockIcon,
|
| 34 |
+
Sparkle
|
| 35 |
} from '@phosphor-icons/react'
|
| 36 |
import { motion } from 'framer-motion'
|
| 37 |
import { FilePreview } from './FilePreview'
|
|
|
|
| 45 |
onMaximize?: () => void
|
| 46 |
onOpenFlutterApp?: (appFile: any) => void
|
| 47 |
sessionId?: string
|
| 48 |
+
onOpenApp?: (appId: string) => void
|
| 49 |
}
|
| 50 |
|
| 51 |
interface FileItem {
|
|
|
|
| 60 |
pubspecYaml?: string
|
| 61 |
}
|
| 62 |
|
| 63 |
+
export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp, onMinimize, onMaximize, sessionId, onOpenApp }: FileManagerProps) {
|
| 64 |
const [files, setFiles] = useState<FileItem[]>([])
|
| 65 |
const [loading, setLoading] = useState(true)
|
| 66 |
const [searchQuery, setSearchQuery] = useState('')
|
|
|
|
| 68 |
const [uploadModalOpen, setUploadModalOpen] = useState(false)
|
| 69 |
const [previewFile, setPreviewFile] = useState<FileItem | null>(null)
|
| 70 |
const [isPublicFolder, setIsPublicFolder] = useState(false)
|
| 71 |
+
const [sidebarSelection, setSidebarSelection] = useState('documents') // 'applications', 'desktop', 'documents', 'downloads', 'public'
|
| 72 |
|
| 73 |
// Load files when path changes or sessionId becomes available
|
| 74 |
useEffect(() => {
|
| 75 |
// Check if this is the public folder
|
| 76 |
setIsPublicFolder(currentPath === 'public' || currentPath.startsWith('public/'))
|
| 77 |
|
| 78 |
+
// Update sidebar selection based on path
|
| 79 |
+
if (currentPath === 'public' || currentPath.startsWith('public/')) {
|
| 80 |
+
setSidebarSelection('public')
|
| 81 |
+
} else if (currentPath === 'Applications') {
|
| 82 |
+
setSidebarSelection('applications')
|
| 83 |
+
} else if (currentPath === 'Desktop') {
|
| 84 |
+
setSidebarSelection('desktop')
|
| 85 |
+
} else if (currentPath === 'Downloads') {
|
| 86 |
+
setSidebarSelection('downloads')
|
| 87 |
+
} else {
|
| 88 |
+
setSidebarSelection('documents')
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
// Only load files if we have a sessionId (for non-public folders) or if it's a public folder
|
| 92 |
const isPublic = currentPath === 'public' || currentPath.startsWith('public/')
|
| 93 |
+
|
| 94 |
+
if (currentPath === 'Applications') {
|
| 95 |
+
setLoading(false)
|
| 96 |
+
setFiles([]) // Applications are handled separately
|
| 97 |
+
return
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
if (isPublic || sessionId) {
|
| 101 |
loadFiles()
|
| 102 |
}
|
|
|
|
| 127 |
return
|
| 128 |
}
|
| 129 |
|
| 130 |
+
// Add public folder to root directory if in root
|
| 131 |
if (currentPath === '') {
|
| 132 |
const publicFolder = {
|
| 133 |
name: 'Public Folder',
|
|
|
|
| 261 |
if (file.path === 'public' || file.name === 'Public Folder') {
|
| 262 |
return <Users size={48} weight="fill" className="text-purple-400" />
|
| 263 |
}
|
| 264 |
+
return <FolderIcon size={48} weight="fill" className="text-blue-400" />
|
| 265 |
}
|
| 266 |
|
| 267 |
if (file.type === 'flutter_app') {
|
|
|
|
| 310 |
return !relativePath.includes('/')
|
| 311 |
})
|
| 312 |
|
| 313 |
+
const handleSidebarClick = (item: string) => {
|
| 314 |
+
setSidebarSelection(item)
|
| 315 |
+
if (item === 'applications') {
|
| 316 |
+
onNavigate('Applications')
|
| 317 |
+
} else if (item === 'desktop') {
|
| 318 |
+
onNavigate('Desktop')
|
| 319 |
+
} else if (item === 'documents') {
|
| 320 |
+
onNavigate('')
|
| 321 |
+
} else if (item === 'downloads') {
|
| 322 |
+
onNavigate('Downloads')
|
| 323 |
+
} else if (item === 'public') {
|
| 324 |
+
onNavigate('public')
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
const applications = [
|
| 329 |
+
{ id: 'files', name: 'Finder', icon: <FolderIcon size={48} weight="fill" className="text-blue-500" /> },
|
| 330 |
+
{ id: 'calendar', name: 'Calendar', icon: <CalendarIcon size={48} weight="regular" className="text-red-500" /> },
|
| 331 |
+
{ id: 'clock', name: 'Clock', icon: <ClockIcon size={48} weight="regular" className="text-black" /> },
|
| 332 |
+
{ id: 'gemini', name: 'Gemini', icon: <Sparkle size={48} weight="fill" className="text-blue-500" /> },
|
| 333 |
+
{ id: 'research', name: 'Research', icon: <Flask size={48} weight="fill" className="text-purple-500" /> },
|
| 334 |
+
{ id: 'flutter-editor', name: 'Flutter IDE', icon: <DeviceMobile size={48} weight="fill" className="text-cyan-500" /> },
|
| 335 |
+
{ id: 'latex-editor', name: 'LaTeX Studio', icon: <Function size={48} weight="bold" className="text-black" /> },
|
| 336 |
+
]
|
| 337 |
+
|
| 338 |
return (
|
| 339 |
<>
|
| 340 |
<Window
|
|
|
|
| 344 |
onClose={onClose}
|
| 345 |
onMinimize={onMinimize}
|
| 346 |
onMaximize={onMaximize}
|
| 347 |
+
width={1000}
|
| 348 |
+
height={650}
|
| 349 |
x={60}
|
| 350 |
y={60}
|
| 351 |
className="file-manager-window"
|
| 352 |
>
|
| 353 |
+
<div className="flex h-full bg-[#F5F5F5]">
|
| 354 |
+
{/* Sidebar */}
|
| 355 |
+
<div className="w-48 bg-[#F3F3F3]/90 backdrop-blur-xl border-r border-gray-200 pt-4 flex flex-col">
|
| 356 |
+
<div className="px-4 mb-2">
|
| 357 |
+
<span className="text-xs font-bold text-gray-400">Favorites</span>
|
| 358 |
+
</div>
|
| 359 |
+
<nav className="space-y-1 px-2">
|
| 360 |
<button
|
| 361 |
+
onClick={() => handleSidebarClick('applications')}
|
| 362 |
+
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm ${sidebarSelection === 'applications' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
>
|
| 364 |
+
<AppWindow size={18} weight="fill" className="text-blue-500" />
|
| 365 |
+
Applications
|
| 366 |
</button>
|
| 367 |
<button
|
| 368 |
+
onClick={() => handleSidebarClick('desktop')}
|
| 369 |
+
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm ${sidebarSelection === 'desktop' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`}
|
| 370 |
>
|
| 371 |
+
<Desktop size={18} weight="fill" className="text-cyan-500" />
|
| 372 |
+
Desktop
|
| 373 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
<button
|
| 375 |
+
onClick={() => handleSidebarClick('documents')}
|
| 376 |
+
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm ${sidebarSelection === 'documents' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`}
|
|
|
|
| 377 |
>
|
| 378 |
+
<FileText size={18} weight="fill" className="text-gray-500" />
|
| 379 |
+
Documents
|
| 380 |
</button>
|
| 381 |
<button
|
| 382 |
+
onClick={() => handleSidebarClick('downloads')}
|
| 383 |
+
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm ${sidebarSelection === 'downloads' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`}
|
|
|
|
| 384 |
>
|
| 385 |
+
<DownloadSimple size={18} weight="fill" className="text-blue-500" />
|
| 386 |
+
Downloads
|
| 387 |
</button>
|
| 388 |
+
<button
|
| 389 |
+
onClick={() => handleSidebarClick('public')}
|
| 390 |
+
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm ${sidebarSelection === 'public' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`}
|
| 391 |
+
>
|
| 392 |
+
<Users size={18} weight="fill" className="text-purple-500" />
|
| 393 |
+
Public
|
| 394 |
+
</button>
|
| 395 |
+
</nav>
|
|
|
|
|
|
|
|
|
|
| 396 |
</div>
|
| 397 |
|
| 398 |
+
{/* Main Content */}
|
| 399 |
+
<div className="flex-1 flex flex-col bg-white">
|
| 400 |
+
{/* Toolbar */}
|
| 401 |
+
<div className="h-12 bg-white border-b border-gray-200 flex items-center px-4 gap-4 justify-between">
|
| 402 |
+
<div className="flex items-center gap-2">
|
| 403 |
+
<button
|
| 404 |
+
onClick={() => {
|
| 405 |
+
const parent = currentPath.split('/').slice(0, -1).join('/')
|
| 406 |
+
onNavigate(parent)
|
| 407 |
+
}}
|
| 408 |
+
disabled={!currentPath || currentPath === 'Applications'}
|
| 409 |
+
className="p-1.5 hover:bg-gray-100 rounded-md disabled:opacity-30 transition-colors"
|
| 410 |
+
>
|
| 411 |
+
<CaretLeft size={18} weight="bold" className="text-gray-600" />
|
| 412 |
+
</button>
|
| 413 |
+
<span className="text-sm font-semibold text-gray-700">
|
| 414 |
+
{currentPath === '' ? 'Documents' : currentPath}
|
| 415 |
+
</span>
|
| 416 |
</div>
|
| 417 |
+
|
| 418 |
+
<div className="flex items-center gap-2">
|
| 419 |
+
{currentPath !== 'Applications' && (
|
| 420 |
+
<>
|
| 421 |
+
<button
|
| 422 |
+
onClick={handleCreateFolder}
|
| 423 |
+
className="p-1.5 hover:bg-gray-100 rounded-md transition-colors"
|
| 424 |
+
title="New Folder"
|
| 425 |
+
>
|
| 426 |
+
<Plus size={18} weight="bold" className="text-gray-600" />
|
| 427 |
+
</button>
|
| 428 |
+
<button
|
| 429 |
+
onClick={() => setUploadModalOpen(true)}
|
| 430 |
+
className="p-1.5 hover:bg-gray-100 rounded-md transition-colors"
|
| 431 |
+
title="Upload File"
|
| 432 |
+
>
|
| 433 |
+
<Upload size={18} weight="bold" className="text-gray-600" />
|
| 434 |
+
</button>
|
| 435 |
+
</>
|
| 436 |
+
)}
|
| 437 |
+
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-100 rounded-md w-48">
|
| 438 |
+
<MagnifyingGlass size={14} weight="bold" className="text-gray-400" />
|
| 439 |
+
<input
|
| 440 |
+
type="text"
|
| 441 |
+
placeholder="Search"
|
| 442 |
+
value={searchQuery}
|
| 443 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 444 |
+
className="bg-transparent text-sm outline-none w-full placeholder-gray-400"
|
| 445 |
+
/>
|
| 446 |
+
</div>
|
| 447 |
</div>
|
| 448 |
+
</div>
|
| 449 |
+
|
| 450 |
+
{/* File Grid */}
|
| 451 |
+
<div className="flex-1 overflow-auto p-6">
|
| 452 |
+
{currentPath === 'Applications' ? (
|
| 453 |
+
<div className="grid grid-cols-5 gap-6">
|
| 454 |
+
{applications.map((app) => (
|
| 455 |
<button
|
| 456 |
+
key={app.id}
|
| 457 |
+
onDoubleClick={() => onOpenApp && onOpenApp(app.id)}
|
| 458 |
+
className="flex flex-col items-center gap-3 p-4 hover:bg-blue-50 rounded-xl transition-colors group"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
>
|
| 460 |
+
<div className="w-16 h-16 flex items-center justify-center drop-shadow-sm group-hover:scale-105 transition-transform">
|
| 461 |
+
{app.icon}
|
| 462 |
</div>
|
| 463 |
+
<span className="text-sm font-medium text-gray-700">{app.name}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
</button>
|
| 465 |
+
))}
|
| 466 |
+
</div>
|
| 467 |
+
) : (
|
| 468 |
+
<>
|
| 469 |
+
{!sessionId && !isPublicFolder ? (
|
| 470 |
+
<div className="flex items-center justify-center h-full text-gray-400 text-sm">
|
| 471 |
+
Initializing session...
|
| 472 |
+
</div>
|
| 473 |
+
) : loading ? (
|
| 474 |
+
<div className="flex items-center justify-center h-full text-gray-400 text-sm">
|
| 475 |
+
Loading files...
|
| 476 |
+
</div>
|
| 477 |
+
) : currentLevelFiles.length === 0 ? (
|
| 478 |
+
<div className="flex items-center justify-center h-full text-gray-400 text-sm">
|
| 479 |
+
{searchQuery ? 'No files found' : 'Folder is empty'}
|
| 480 |
+
</div>
|
| 481 |
+
) : (
|
| 482 |
+
<div className="grid grid-cols-5 gap-6">
|
| 483 |
+
{currentLevelFiles.map((file) => (
|
| 484 |
+
<div
|
| 485 |
+
key={file.path}
|
| 486 |
+
className="group relative"
|
| 487 |
>
|
| 488 |
+
<button
|
| 489 |
+
onDoubleClick={() => {
|
| 490 |
+
if (file.type === 'folder') {
|
| 491 |
+
onNavigate(file.path)
|
| 492 |
+
} else if (file.type === 'flutter_app' && onOpenFlutterApp) {
|
| 493 |
+
onOpenFlutterApp(file)
|
| 494 |
+
} else {
|
| 495 |
+
handlePreview(file)
|
| 496 |
+
}
|
| 497 |
+
}}
|
| 498 |
+
className="flex flex-col items-center gap-3 p-4 hover:bg-blue-50 rounded-xl w-full transition-colors"
|
| 499 |
+
>
|
| 500 |
+
<div className="w-16 h-16 flex items-center justify-center drop-shadow-sm">
|
| 501 |
+
{getFileIcon(file)}
|
| 502 |
+
</div>
|
| 503 |
+
<span className="text-sm font-medium text-gray-700 text-center break-all w-full line-clamp-2">
|
| 504 |
+
{file.name}
|
| 505 |
+
</span>
|
| 506 |
+
{file.size && (
|
| 507 |
+
<span className="text-xs text-gray-400">
|
| 508 |
+
{formatFileSize(file.size)}
|
| 509 |
+
</span>
|
| 510 |
+
)}
|
| 511 |
+
</button>
|
| 512 |
+
|
| 513 |
+
{/* File Actions */}
|
| 514 |
+
{file.type === 'file' && (
|
| 515 |
+
<div className="absolute top-2 right-2 hidden group-hover:flex gap-1 bg-white/90 rounded-lg shadow-sm p-1">
|
| 516 |
+
<button
|
| 517 |
+
onClick={(e) => {
|
| 518 |
+
e.stopPropagation()
|
| 519 |
+
handlePreview(file)
|
| 520 |
+
}}
|
| 521 |
+
className="p-1.5 hover:bg-gray-100 rounded-md text-gray-600"
|
| 522 |
+
title="Preview"
|
| 523 |
+
>
|
| 524 |
+
<Eye size={14} weight="bold" />
|
| 525 |
+
</button>
|
| 526 |
+
<button
|
| 527 |
+
onClick={(e) => {
|
| 528 |
+
e.stopPropagation()
|
| 529 |
+
handleDownload(file)
|
| 530 |
+
}}
|
| 531 |
+
className="p-1.5 hover:bg-gray-100 rounded-md text-gray-600"
|
| 532 |
+
title="Download"
|
| 533 |
+
>
|
| 534 |
+
<Download size={14} weight="bold" />
|
| 535 |
+
</button>
|
| 536 |
+
<button
|
| 537 |
+
onClick={(e) => {
|
| 538 |
+
e.stopPropagation()
|
| 539 |
+
handleDelete(file)
|
| 540 |
+
}}
|
| 541 |
+
className="p-1.5 hover:bg-red-50 rounded-md text-red-500"
|
| 542 |
+
title="Delete"
|
| 543 |
+
>
|
| 544 |
+
<Trash size={14} weight="bold" />
|
| 545 |
+
</button>
|
| 546 |
+
</div>
|
| 547 |
+
)}
|
| 548 |
+
</div>
|
| 549 |
+
))}
|
| 550 |
+
</div>
|
| 551 |
+
)}
|
| 552 |
+
</>
|
| 553 |
+
)}
|
| 554 |
+
</div>
|
| 555 |
|
| 556 |
+
{/* Status Bar */}
|
| 557 |
+
<div className="h-8 bg-white border-t border-gray-200 flex items-center px-4 text-xs text-gray-500 font-medium">
|
| 558 |
+
{currentPath === 'Applications'
|
| 559 |
+
? `${applications.length} items`
|
| 560 |
+
: `${currentLevelFiles.length} items • ${files.filter(f => f.type === 'folder').length} folders • ${files.filter(f => f.type === 'file').length} files`
|
| 561 |
+
}
|
| 562 |
+
</div>
|
| 563 |
</div>
|
| 564 |
</div>
|
| 565 |
</Window>
|
|
|
|
| 605 |
}
|
| 606 |
|
| 607 |
return (
|
| 608 |
+
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm flex items-center justify-center z-[100]">
|
| 609 |
+
<div className="bg-white rounded-xl shadow-2xl p-6 w-[400px] border border-gray-200">
|
| 610 |
+
<h2 className="text-lg font-semibold mb-4 text-gray-800">Upload File</h2>
|
| 611 |
|
| 612 |
<div className="space-y-4">
|
| 613 |
<div>
|
| 614 |
+
<label className="block text-sm font-medium mb-2 text-gray-600">Select File</label>
|
| 615 |
<input
|
| 616 |
type="file"
|
| 617 |
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
|
| 618 |
+
className="w-full p-2 border border-gray-300 rounded-lg text-sm"
|
| 619 |
/>
|
| 620 |
</div>
|
| 621 |
|
| 622 |
<div>
|
| 623 |
+
<label className="block text-sm font-medium mb-2 text-gray-600">Upload to Folder</label>
|
| 624 |
<input
|
| 625 |
type="text"
|
| 626 |
value={targetFolder}
|
| 627 |
onChange={(e) => setTargetFolder(e.target.value)}
|
| 628 |
placeholder="e.g., homework/math"
|
| 629 |
+
className="w-full p-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
|
| 630 |
/>
|
| 631 |
+
<p className="text-xs text-gray-400 mt-1">
|
| 632 |
Leave empty for root, or enter path like "folder/subfolder"
|
| 633 |
</p>
|
| 634 |
</div>
|
| 635 |
|
| 636 |
+
<div className="flex justify-end gap-2 pt-2">
|
| 637 |
<button
|
| 638 |
onClick={onClose}
|
| 639 |
+
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg text-sm font-medium transition-colors"
|
| 640 |
>
|
| 641 |
Cancel
|
| 642 |
</button>
|
| 643 |
<button
|
| 644 |
onClick={handleSubmit}
|
| 645 |
disabled={!selectedFile}
|
| 646 |
+
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 text-sm font-medium shadow-lg shadow-blue-500/20 transition-all"
|
| 647 |
>
|
| 648 |
Upload
|
| 649 |
</button>
|
app/components/FlutterRunner.tsx
CHANGED
|
@@ -1,288 +1,327 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import React, { useState,
|
| 4 |
-
import Window from './Window'
|
| 5 |
-
import { motion, AnimatePresence } from 'framer-motion'
|
| 6 |
-
import { Copy, CaretRight, CaretLeft, Code as CodeIcon, Play, FloppyDisk, PencilSimple } from '@phosphor-icons/react'
|
| 7 |
import Editor from '@monaco-editor/react'
|
| 8 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
interface FlutterRunnerProps {
|
| 11 |
-
file: {
|
| 12 |
-
name: string
|
| 13 |
-
dartCode: string
|
| 14 |
-
dependencies?: string[]
|
| 15 |
-
pubspecYaml?: string
|
| 16 |
-
}
|
| 17 |
onClose: () => void
|
| 18 |
onMinimize?: () => void
|
| 19 |
onMaximize?: () => void
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
const [saved, setSaved] = useState(false)
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
const checkMobile = () => {
|
| 35 |
-
const mobile = window.innerWidth <= 1024
|
| 36 |
-
setIsMobile(mobile)
|
| 37 |
-
// Auto-close sidebar on mobile landscape for more iframe space
|
| 38 |
-
if (mobile && window.innerHeight < window.innerWidth) {
|
| 39 |
-
setSidebarOpen(false)
|
| 40 |
-
}
|
| 41 |
-
}
|
| 42 |
-
checkMobile()
|
| 43 |
-
window.addEventListener('resize', checkMobile)
|
| 44 |
-
return () => window.removeEventListener('resize', checkMobile)
|
| 45 |
-
}, [])
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
|
|
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
-
} catch (error) {
|
| 89 |
-
console.error('Failed to save:', error)
|
| 90 |
-
} finally {
|
| 91 |
-
setSaving(false)
|
| 92 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
return (
|
| 96 |
<Window
|
| 97 |
id="flutter-runner"
|
| 98 |
-
title=
|
| 99 |
isOpen={true}
|
| 100 |
onClose={onClose}
|
| 101 |
onMinimize={onMinimize}
|
| 102 |
onMaximize={onMaximize}
|
| 103 |
width={1200}
|
| 104 |
-
height={
|
| 105 |
-
x={
|
| 106 |
-
y={
|
| 107 |
-
className="flutter-
|
| 108 |
>
|
| 109 |
-
<div className="flex
|
| 110 |
-
{/*
|
| 111 |
-
|
| 112 |
-
<div className="
|
| 113 |
-
<div className="
|
| 114 |
-
|
| 115 |
</div>
|
| 116 |
-
<div>
|
| 117 |
-
<
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
</div>
|
| 120 |
</div>
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
<button
|
| 146 |
-
onClick={
|
| 147 |
-
|
| 148 |
-
|
| 149 |
>
|
| 150 |
-
<
|
| 151 |
-
{saving ? 'Saving...' : saved ? 'Saved!' : 'Save Changes'}
|
| 152 |
</button>
|
| 153 |
-
|
| 154 |
</div>
|
| 155 |
-
</div>
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
</div>
|
| 166 |
</div>
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
theme="vs-dark"
|
| 175 |
-
options={{
|
| 176 |
-
minimap: { enabled: false },
|
| 177 |
-
fontSize: 14,
|
| 178 |
-
lineNumbers: 'on',
|
| 179 |
-
scrollBeyondLastLine: false,
|
| 180 |
-
automaticLayout: true,
|
| 181 |
-
tabSize: 2,
|
| 182 |
-
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
| 183 |
-
padding: { top: 20, bottom: 20 }
|
| 184 |
-
}}
|
| 185 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
</div>
|
| 187 |
-
|
| 188 |
</div>
|
| 189 |
-
|
| 190 |
-
{/* Collapsible Sidebar */}
|
| 191 |
-
<AnimatePresence>
|
| 192 |
-
{sidebarOpen && (
|
| 193 |
-
<motion.div
|
| 194 |
-
initial={{ width: 0, opacity: 0 }}
|
| 195 |
-
animate={{ width: isMobile ? '100%' : 350, opacity: 1 }}
|
| 196 |
-
exit={{ width: 0, opacity: 0 }}
|
| 197 |
-
transition={{ duration: 0.2 }}
|
| 198 |
-
className={`${isMobile ? 'absolute right-0 top-0 bottom-0 z-10' : ''} border-l border-[#333] bg-[#252526] flex flex-col shadow-xl`}
|
| 199 |
-
>
|
| 200 |
-
{/* Sidebar Header */}
|
| 201 |
-
<div className="h-12 border-b border-[#333] flex items-center justify-between px-4">
|
| 202 |
-
<span className="font-medium text-sm text-gray-300">Project Details</span>
|
| 203 |
-
<button
|
| 204 |
-
onClick={() => setSidebarOpen(false)}
|
| 205 |
-
className="hover:bg-white/10 p-1.5 rounded-md transition-colors text-gray-400 hover:text-white"
|
| 206 |
-
>
|
| 207 |
-
<CaretRight size={16} weight="bold" />
|
| 208 |
-
</button>
|
| 209 |
-
</div>
|
| 210 |
-
|
| 211 |
-
{/* Sidebar Content */}
|
| 212 |
-
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
| 213 |
-
{/* Dart Code Section */}
|
| 214 |
-
<div className="space-y-3">
|
| 215 |
-
<div className="flex items-center justify-between">
|
| 216 |
-
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-wider">Source Code</h3>
|
| 217 |
-
<button
|
| 218 |
-
onClick={handleCopyCode}
|
| 219 |
-
className="flex items-center gap-1.5 px-2 py-1 bg-[#333] hover:bg-[#444] text-gray-300 text-xs rounded transition-colors border border-[#444]"
|
| 220 |
-
>
|
| 221 |
-
<Copy size={12} weight="bold" />
|
| 222 |
-
{copySuccess ? 'Copied!' : 'Copy'}
|
| 223 |
-
</button>
|
| 224 |
-
</div>
|
| 225 |
-
<div className="bg-[#1E1E1E] rounded-lg p-3 border border-[#333] max-h-48 overflow-y-auto custom-scrollbar">
|
| 226 |
-
<pre className="text-xs text-blue-300 font-mono whitespace-pre-wrap break-words">
|
| 227 |
-
{code}
|
| 228 |
-
</pre>
|
| 229 |
-
</div>
|
| 230 |
-
</div>
|
| 231 |
-
|
| 232 |
-
{/* Dependencies Section */}
|
| 233 |
-
{file.dependencies && file.dependencies.length > 0 && (
|
| 234 |
-
<div className="space-y-3">
|
| 235 |
-
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-wider">Dependencies</h3>
|
| 236 |
-
<div className="bg-[#1E1E1E] rounded-lg p-3 border border-[#333]">
|
| 237 |
-
<ul className="space-y-2">
|
| 238 |
-
{file.dependencies.map((dep, index) => (
|
| 239 |
-
<li key={index} className="text-xs font-mono text-green-400 flex items-center gap-2">
|
| 240 |
-
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
|
| 241 |
-
{dep}
|
| 242 |
-
</li>
|
| 243 |
-
))}
|
| 244 |
-
</ul>
|
| 245 |
-
</div>
|
| 246 |
-
</div>
|
| 247 |
-
)}
|
| 248 |
-
|
| 249 |
-
{/* Pubspec.yaml Section */}
|
| 250 |
-
{pubspec && (
|
| 251 |
-
<div className="space-y-3">
|
| 252 |
-
<div className="flex items-center justify-between">
|
| 253 |
-
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-wider">pubspec.yaml</h3>
|
| 254 |
-
<button
|
| 255 |
-
onClick={handleCopyPubspec}
|
| 256 |
-
className="flex items-center gap-1.5 px-2 py-1 bg-[#333] hover:bg-[#444] text-gray-300 text-xs rounded transition-colors border border-[#444]"
|
| 257 |
-
>
|
| 258 |
-
<Copy size={12} weight="bold" />
|
| 259 |
-
Copy
|
| 260 |
-
</button>
|
| 261 |
-
</div>
|
| 262 |
-
<div className="bg-[#1E1E1E] rounded-lg p-3 border border-[#333] max-h-48 overflow-y-auto custom-scrollbar">
|
| 263 |
-
<pre className="text-xs text-yellow-400 font-mono whitespace-pre-wrap break-words">
|
| 264 |
-
{pubspec}
|
| 265 |
-
</pre>
|
| 266 |
-
</div>
|
| 267 |
-
</div>
|
| 268 |
-
)}
|
| 269 |
-
</div>
|
| 270 |
-
</motion.div>
|
| 271 |
-
)}
|
| 272 |
-
</AnimatePresence>
|
| 273 |
-
|
| 274 |
-
{/* Sidebar Toggle Button (when closed) */}
|
| 275 |
-
{!sidebarOpen && (
|
| 276 |
-
<button
|
| 277 |
-
onClick={() => setSidebarOpen(true)}
|
| 278 |
-
className="absolute right-0 top-4 bg-[#252526] border-l border-t border-b border-[#333] text-gray-400 hover:text-white p-2 rounded-l-lg shadow-lg transition-all z-10"
|
| 279 |
-
>
|
| 280 |
-
<CaretLeft size={20} weight="bold" />
|
| 281 |
-
</button>
|
| 282 |
-
)}
|
| 283 |
</div>
|
| 284 |
</div>
|
| 285 |
</Window>
|
| 286 |
)
|
| 287 |
}
|
| 288 |
-
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
+
import React, { useState, useEffect } from 'react'
|
|
|
|
|
|
|
|
|
|
| 4 |
import Editor from '@monaco-editor/react'
|
| 5 |
+
import {
|
| 6 |
+
Play,
|
| 7 |
+
Download,
|
| 8 |
+
X,
|
| 9 |
+
Minus,
|
| 10 |
+
Square,
|
| 11 |
+
DeviceMobile,
|
| 12 |
+
SidebarSimple,
|
| 13 |
+
FileCode,
|
| 14 |
+
ArrowsClockwise,
|
| 15 |
+
Check,
|
| 16 |
+
CaretRight,
|
| 17 |
+
CaretDown
|
| 18 |
+
} from '@phosphor-icons/react'
|
| 19 |
+
import Window from './Window'
|
| 20 |
|
| 21 |
interface FlutterRunnerProps {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
onClose: () => void
|
| 23 |
onMinimize?: () => void
|
| 24 |
onMaximize?: () => void
|
| 25 |
+
initialCode?: string
|
| 26 |
+
sessionId?: string
|
| 27 |
}
|
| 28 |
|
| 29 |
+
interface FileNode {
|
| 30 |
+
id: string
|
| 31 |
+
name: string
|
| 32 |
+
type: 'file' | 'folder'
|
| 33 |
+
content?: string
|
| 34 |
+
children?: FileNode[]
|
| 35 |
+
isOpen?: boolean
|
| 36 |
+
}
|
|
|
|
| 37 |
|
| 38 |
+
export function FlutterRunner({ onClose, onMinimize, onMaximize, initialCode, sessionId }: FlutterRunnerProps) {
|
| 39 |
+
const [code, setCode] = useState(initialCode || `import 'package:flutter/material.dart';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
void main() {
|
| 42 |
+
runApp(const MyApp());
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
class MyApp extends StatelessWidget {
|
| 46 |
+
const MyApp({super.key});
|
| 47 |
+
|
| 48 |
+
@override
|
| 49 |
+
Widget build(BuildContext context) {
|
| 50 |
+
return MaterialApp(
|
| 51 |
+
title: 'Flutter Demo',
|
| 52 |
+
theme: ThemeData(
|
| 53 |
+
primarySwatch: Colors.blue,
|
| 54 |
+
useMaterial3: true,
|
| 55 |
+
),
|
| 56 |
+
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
| 57 |
+
);
|
| 58 |
}
|
| 59 |
+
}
|
| 60 |
|
| 61 |
+
class MyHomePage extends StatefulWidget {
|
| 62 |
+
const MyHomePage({super.key, required this.title});
|
| 63 |
+
|
| 64 |
+
final String title;
|
| 65 |
+
|
| 66 |
+
@override
|
| 67 |
+
State<MyHomePage> createState() => _MyHomePageState();
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
class _MyHomePageState extends State<MyHomePage> {
|
| 71 |
+
int _counter = 0;
|
| 72 |
+
|
| 73 |
+
void _incrementCounter() {
|
| 74 |
+
setState(() {
|
| 75 |
+
_counter++;
|
| 76 |
+
});
|
| 77 |
}
|
| 78 |
|
| 79 |
+
@override
|
| 80 |
+
Widget build(BuildContext context) {
|
| 81 |
+
return Scaffold(
|
| 82 |
+
appBar: AppBar(
|
| 83 |
+
title: Text(widget.title),
|
| 84 |
+
),
|
| 85 |
+
body: Center(
|
| 86 |
+
child: Column(
|
| 87 |
+
mainAxisAlignment: MainAxisAlignment.center,
|
| 88 |
+
children: <Widget>[
|
| 89 |
+
const Text(
|
| 90 |
+
'You have pushed the button this many times:',
|
| 91 |
+
),
|
| 92 |
+
Text(
|
| 93 |
+
'$_counter',
|
| 94 |
+
style: Theme.of(context).textTheme.headlineMedium,
|
| 95 |
+
),
|
| 96 |
+
],
|
| 97 |
+
),
|
| 98 |
+
),
|
| 99 |
+
floatingActionButton: FloatingActionButton(
|
| 100 |
+
onPressed: _incrementCounter,
|
| 101 |
+
tooltip: 'Increment',
|
| 102 |
+
child: const Icon(Icons.add),
|
| 103 |
+
),
|
| 104 |
+
);
|
| 105 |
+
}
|
| 106 |
+
}`)
|
| 107 |
+
|
| 108 |
+
const [isRunning, setIsRunning] = useState(false)
|
| 109 |
+
const [key, setKey] = useState(0)
|
| 110 |
+
const [showSidebar, setShowSidebar] = useState(true)
|
| 111 |
+
const [files, setFiles] = useState<FileNode[]>([
|
| 112 |
+
{
|
| 113 |
+
id: 'root',
|
| 114 |
+
name: 'lib',
|
| 115 |
+
type: 'folder',
|
| 116 |
+
isOpen: true,
|
| 117 |
+
children: [
|
| 118 |
+
{ id: 'main', name: 'main.dart', type: 'file', content: code }
|
| 119 |
+
]
|
| 120 |
+
}
|
| 121 |
+
])
|
| 122 |
+
const [activeFileId, setActiveFileId] = useState('main')
|
| 123 |
+
const [isSaving, setIsSaving] = useState(false)
|
| 124 |
|
| 125 |
+
// Auto-save to session
|
| 126 |
+
useEffect(() => {
|
| 127 |
+
const saveToSession = async () => {
|
| 128 |
+
if (!sessionId) return
|
| 129 |
+
setIsSaving(true)
|
| 130 |
+
try {
|
| 131 |
+
await fetch('/api/session/code', {
|
| 132 |
+
method: 'POST',
|
| 133 |
+
headers: {
|
| 134 |
+
'Content-Type': 'application/json',
|
| 135 |
+
'x-session-id': sessionId
|
| 136 |
+
},
|
| 137 |
+
body: JSON.stringify({
|
| 138 |
+
type: 'flutter',
|
| 139 |
+
code,
|
| 140 |
+
filename: 'main.dart'
|
| 141 |
+
})
|
| 142 |
+
})
|
| 143 |
+
} catch (error) {
|
| 144 |
+
console.error('Failed to save to session:', error)
|
| 145 |
+
} finally {
|
| 146 |
+
setIsSaving(false)
|
| 147 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
}
|
| 149 |
+
|
| 150 |
+
const debounce = setTimeout(saveToSession, 2000)
|
| 151 |
+
return () => clearTimeout(debounce)
|
| 152 |
+
}, [code, sessionId])
|
| 153 |
+
|
| 154 |
+
const handleRun = () => {
|
| 155 |
+
setIsRunning(true)
|
| 156 |
+
setKey(prev => prev + 1)
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
const handleDownload = () => {
|
| 160 |
+
const blob = new Blob([code], { type: 'text/plain' })
|
| 161 |
+
const url = URL.createObjectURL(blob)
|
| 162 |
+
const a = document.createElement('a')
|
| 163 |
+
a.href = url
|
| 164 |
+
a.download = 'main.dart'
|
| 165 |
+
a.click()
|
| 166 |
+
URL.revokeObjectURL(url)
|
| 167 |
}
|
| 168 |
|
| 169 |
return (
|
| 170 |
<Window
|
| 171 |
id="flutter-runner"
|
| 172 |
+
title="Flutter IDE"
|
| 173 |
isOpen={true}
|
| 174 |
onClose={onClose}
|
| 175 |
onMinimize={onMinimize}
|
| 176 |
onMaximize={onMaximize}
|
| 177 |
width={1200}
|
| 178 |
+
height={800}
|
| 179 |
+
x={40}
|
| 180 |
+
y={40}
|
| 181 |
+
className="flutter-ide-window"
|
| 182 |
>
|
| 183 |
+
<div className="flex h-full bg-[#1e1e1e] text-gray-300 font-sans">
|
| 184 |
+
{/* Sidebar */}
|
| 185 |
+
{showSidebar && (
|
| 186 |
+
<div className="w-64 bg-[#252526] border-r border-[#333] flex flex-col">
|
| 187 |
+
<div className="h-9 px-4 flex items-center text-xs font-bold text-gray-500 uppercase tracking-wider">
|
| 188 |
+
Explorer
|
| 189 |
</div>
|
| 190 |
+
<div className="flex-1 overflow-y-auto py-2">
|
| 191 |
+
<div className="px-2">
|
| 192 |
+
<div className="flex items-center gap-1 py-1 px-2 text-sm text-gray-300 hover:bg-[#2a2d2e] rounded cursor-pointer">
|
| 193 |
+
<CaretDown size={12} weight="bold" />
|
| 194 |
+
<span className="font-bold">PROJECT</span>
|
| 195 |
+
</div>
|
| 196 |
+
<div className="pl-4">
|
| 197 |
+
{files.map(file => (
|
| 198 |
+
<div key={file.id}>
|
| 199 |
+
<div className="flex items-center gap-2 py-1 px-2 text-sm hover:bg-[#2a2d2e] rounded cursor-pointer text-blue-400">
|
| 200 |
+
<CaretDown size={12} weight="bold" />
|
| 201 |
+
{file.name}
|
| 202 |
+
</div>
|
| 203 |
+
{file.children?.map(child => (
|
| 204 |
+
<div
|
| 205 |
+
key={child.id}
|
| 206 |
+
onClick={() => setActiveFileId(child.id)}
|
| 207 |
+
className={`flex items-center gap-2 py-1 px-2 ml-4 text-sm rounded cursor-pointer ${activeFileId === child.id ? 'bg-[#37373d] text-white' : 'text-gray-400 hover:bg-[#2a2d2e]'
|
| 208 |
+
}`}
|
| 209 |
+
>
|
| 210 |
+
<FileCode size={14} />
|
| 211 |
+
{child.name}
|
| 212 |
+
</div>
|
| 213 |
+
))}
|
| 214 |
+
</div>
|
| 215 |
+
))}
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
</div>
|
| 219 |
</div>
|
| 220 |
+
)}
|
| 221 |
+
|
| 222 |
+
{/* Main Content */}
|
| 223 |
+
<div className="flex-1 flex flex-col min-w-0">
|
| 224 |
+
{/* Toolbar */}
|
| 225 |
+
<div className="h-10 bg-[#2d2d2d] border-b border-[#1e1e1e] flex items-center justify-between px-4">
|
| 226 |
+
<div className="flex items-center gap-2">
|
| 227 |
+
<button
|
| 228 |
+
onClick={() => setShowSidebar(!showSidebar)}
|
| 229 |
+
className={`p-1.5 rounded hover:bg-[#3e3e42] ${showSidebar ? 'text-white' : 'text-gray-500'}`}
|
| 230 |
+
title="Toggle Sidebar"
|
| 231 |
+
>
|
| 232 |
+
<SidebarSimple size={16} />
|
| 233 |
+
</button>
|
| 234 |
+
<div className="h-4 w-[1px] bg-gray-600 mx-2" />
|
| 235 |
+
<div className="flex items-center gap-2 px-3 py-1 bg-[#1e1e1e] rounded text-xs text-gray-400 border border-[#333]">
|
| 236 |
+
<FileCode size={14} className="text-blue-400" />
|
| 237 |
+
main.dart
|
| 238 |
+
</div>
|
| 239 |
+
{isSaving && (
|
| 240 |
+
<span className="text-xs text-gray-500 flex items-center gap-1">
|
| 241 |
+
<ArrowsClockwise size={12} className="animate-spin" />
|
| 242 |
+
Syncing...
|
| 243 |
+
</span>
|
| 244 |
+
)}
|
| 245 |
+
</div>
|
| 246 |
+
|
| 247 |
+
<div className="flex items-center gap-2">
|
| 248 |
+
<button
|
| 249 |
+
onClick={handleRun}
|
| 250 |
+
className="flex items-center gap-2 px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded text-xs font-medium transition-colors"
|
| 251 |
+
>
|
| 252 |
+
<Play size={14} weight="fill" />
|
| 253 |
+
Run
|
| 254 |
+
</button>
|
| 255 |
<button
|
| 256 |
+
onClick={handleDownload}
|
| 257 |
+
className="p-1.5 text-gray-400 hover:text-white hover:bg-[#3e3e42] rounded transition-colors"
|
| 258 |
+
title="Download Code"
|
| 259 |
>
|
| 260 |
+
<Download size={16} />
|
|
|
|
| 261 |
</button>
|
| 262 |
+
</div>
|
| 263 |
</div>
|
|
|
|
| 264 |
|
| 265 |
+
{/* Split View: Editor & Preview */}
|
| 266 |
+
<div className="flex-1 flex overflow-hidden">
|
| 267 |
+
{/* Editor */}
|
| 268 |
+
<div className="flex-1 border-r border-[#333]">
|
| 269 |
+
<Editor
|
| 270 |
+
height="100%"
|
| 271 |
+
defaultLanguage="dart"
|
| 272 |
+
theme="vs-dark"
|
| 273 |
+
value={code}
|
| 274 |
+
onChange={(value) => setCode(value || '')}
|
| 275 |
+
options={{
|
| 276 |
+
minimap: { enabled: false },
|
| 277 |
+
fontSize: 14,
|
| 278 |
+
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
| 279 |
+
lineNumbers: 'on',
|
| 280 |
+
scrollBeyondLastLine: false,
|
| 281 |
+
automaticLayout: true,
|
| 282 |
+
padding: { top: 16, bottom: 16 },
|
| 283 |
+
renderLineHighlight: 'all',
|
| 284 |
+
smoothScrolling: true,
|
| 285 |
+
cursorBlinking: 'smooth',
|
| 286 |
+
cursorSmoothCaretAnimation: 'on'
|
| 287 |
+
}}
|
| 288 |
+
/>
|
| 289 |
+
</div>
|
| 290 |
+
|
| 291 |
+
{/* Preview */}
|
| 292 |
+
<div className="w-[400px] bg-[#1e1e1e] flex flex-col border-l border-[#333]">
|
| 293 |
+
<div className="h-8 bg-[#252526] border-b border-[#333] flex items-center justify-between px-3">
|
| 294 |
+
<span className="text-xs font-bold text-gray-500 uppercase">Preview</span>
|
| 295 |
+
<div className="flex gap-1">
|
| 296 |
+
<div className="w-2 h-2 rounded-full bg-red-500/50" />
|
| 297 |
+
<div className="w-2 h-2 rounded-full bg-yellow-500/50" />
|
| 298 |
+
<div className="w-2 h-2 rounded-full bg-green-500/50" />
|
| 299 |
</div>
|
| 300 |
</div>
|
| 301 |
+
<div className="flex-1 bg-white relative overflow-hidden">
|
| 302 |
+
<iframe
|
| 303 |
+
key={key}
|
| 304 |
+
src={`https://dartpad.dev/embed-flutter.html?theme=light&run=true&split=0&code=${encodeURIComponent(code)}`}
|
| 305 |
+
className="w-full h-full border-0"
|
| 306 |
+
sandbox="allow-scripts allow-same-origin allow-popups"
|
| 307 |
+
title="Flutter Preview"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
/>
|
| 309 |
+
{!isRunning && (
|
| 310 |
+
<div className="absolute inset-0 bg-black/5 flex items-center justify-center backdrop-blur-[1px]">
|
| 311 |
+
<button
|
| 312 |
+
onClick={handleRun}
|
| 313 |
+
className="px-4 py-2 bg-blue-500 text-white rounded-lg shadow-lg hover:bg-blue-600 transition-colors flex items-center gap-2 text-sm font-medium"
|
| 314 |
+
>
|
| 315 |
+
<Play size={16} weight="fill" />
|
| 316 |
+
Run App
|
| 317 |
+
</button>
|
| 318 |
+
</div>
|
| 319 |
+
)}
|
| 320 |
</div>
|
| 321 |
+
</div>
|
| 322 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
</div>
|
| 324 |
</div>
|
| 325 |
</Window>
|
| 326 |
)
|
| 327 |
}
|
|
|
app/components/LaTeXEditor.tsx
CHANGED
|
@@ -223,303 +223,283 @@ This document demonstrates LaTeX capabilities for: ${prompt}
|
|
| 223 |
setDocuments(updatedDocs)
|
| 224 |
setCurrentDocument(newDoc)
|
| 225 |
|
| 226 |
-
//
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
const handleDeleteDocument = async (docId: string) => {
|
| 252 |
-
const updatedDocs = documents.filter(d => d.id !== docId)
|
| 253 |
-
setDocuments(updatedDocs)
|
| 254 |
-
|
| 255 |
-
const storageKey = `latex_documents_${sessionId}`
|
| 256 |
-
localStorage.setItem(storageKey, JSON.stringify(updatedDocs))
|
| 257 |
-
|
| 258 |
-
if (currentDocument?.id === docId) {
|
| 259 |
-
setCurrentDocument(null)
|
| 260 |
-
setContent('\\documentclass{article}\n\\begin{document}\nNew Document\n\\end{document}')
|
| 261 |
-
}
|
| 262 |
-
|
| 263 |
-
// Delete from backend
|
| 264 |
-
await fetch('/api/latex/delete', {
|
| 265 |
-
method: 'DELETE',
|
| 266 |
-
headers: { 'Content-Type': 'application/json' },
|
| 267 |
-
body: JSON.stringify({
|
| 268 |
-
documentId: docId,
|
| 269 |
-
sessionId
|
| 270 |
-
})
|
| 271 |
-
})
|
| 272 |
-
}
|
| 273 |
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
|
| 285 |
-
|
| 286 |
-
const blob =
|
| 287 |
-
const url = URL.createObjectURL(blob)
|
| 288 |
-
const a = document.createElement('a')
|
| 289 |
-
a.href = url
|
| 290 |
-
a.download = `${currentDocument?.name || 'document'}.pdf`
|
| 291 |
-
a.click()
|
| 292 |
-
URL.revokeObjectURL(url)
|
| 293 |
-
} else {
|
| 294 |
-
// Fallback: Download as .tex file
|
| 295 |
-
const blob = new Blob([content], { type: 'text/plain' })
|
| 296 |
const url = URL.createObjectURL(blob)
|
| 297 |
const a = document.createElement('a')
|
| 298 |
a.href = url
|
| 299 |
-
a.download =
|
| 300 |
a.click()
|
| 301 |
URL.revokeObjectURL(url)
|
| 302 |
}
|
| 303 |
-
} catch (error) {
|
| 304 |
-
console.error('Failed to compile PDF:', error)
|
| 305 |
-
// Download as .tex file
|
| 306 |
-
const blob = new Blob([content], { type: 'text/plain' })
|
| 307 |
-
const url = URL.createObjectURL(blob)
|
| 308 |
-
const a = document.createElement('a')
|
| 309 |
-
a.href = url
|
| 310 |
-
a.download = `${currentDocument?.name || 'document'}.tex`
|
| 311 |
-
a.click()
|
| 312 |
-
URL.revokeObjectURL(url)
|
| 313 |
-
}
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
const handleCopyLaTeX = async () => {
|
| 317 |
-
try {
|
| 318 |
-
await navigator.clipboard.writeText(content)
|
| 319 |
-
setCopySuccess(true)
|
| 320 |
-
setTimeout(() => setCopySuccess(false), 2000)
|
| 321 |
-
} catch (err) {
|
| 322 |
-
console.error('Failed to copy:', err)
|
| 323 |
-
}
|
| 324 |
-
}
|
| 325 |
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
</button>
|
| 377 |
-
|
| 378 |
-
<button
|
| 379 |
-
onClick={() => setShowPreview(!showPreview)}
|
| 380 |
-
className={`p-2 rounded-lg transition-colors ${showPreview ? 'bg-blue-50 text-blue-600' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'
|
| 381 |
-
}`}
|
| 382 |
-
title="Toggle Preview"
|
| 383 |
-
>
|
| 384 |
-
<Eye size={20} weight={showPreview ? "fill" : "regular"} />
|
| 385 |
-
</button>
|
| 386 |
-
</div>
|
| 387 |
-
</div>
|
| 388 |
-
|
| 389 |
-
{/* Claude Prompt Bar */}
|
| 390 |
-
<div className="bg-white border-b border-gray-200 px-6 py-4">
|
| 391 |
-
<div className="flex gap-3 max-w-4xl mx-auto">
|
| 392 |
-
<div className="flex-1 flex items-center bg-gray-50 rounded-xl px-4 py-2.5 border border-gray-200 focus-within:border-purple-500 focus-within:ring-2 focus-within:ring-purple-500/20 transition-all">
|
| 393 |
-
<Sparkle size={20} className="text-purple-500 mr-3" weight="fill" />
|
| 394 |
-
<input
|
| 395 |
-
type="text"
|
| 396 |
-
value={claudePrompt}
|
| 397 |
-
onChange={(e) => setClaudePrompt(e.target.value)}
|
| 398 |
-
onKeyPress={(e) => e.key === 'Enter' && handleGenerateLaTeX()}
|
| 399 |
-
placeholder="Ask AI to generate equations, tables, or entire sections..."
|
| 400 |
-
className="flex-1 outline-none text-sm bg-transparent text-gray-900 placeholder-gray-400"
|
| 401 |
-
disabled={isGenerating}
|
| 402 |
-
/>
|
| 403 |
-
</div>
|
| 404 |
-
<button
|
| 405 |
-
onClick={handleGenerateLaTeX}
|
| 406 |
-
disabled={isGenerating || !claudePrompt.trim()}
|
| 407 |
-
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white rounded-xl font-medium text-sm transition-all shadow-lg shadow-purple-600/20 disabled:opacity-50 disabled:shadow-none"
|
| 408 |
-
>
|
| 409 |
-
{isGenerating ? 'Generating...' : 'Generate'}
|
| 410 |
-
</button>
|
| 411 |
-
</div>
|
| 412 |
-
</div>
|
| 413 |
-
|
| 414 |
-
{/* Main Content */}
|
| 415 |
-
<div className="flex flex-1 overflow-hidden">
|
| 416 |
-
{/* Editor Panel */}
|
| 417 |
-
<div className={`${showPreview ? 'w-1/2' : 'flex-1'} flex flex-col border-r border-gray-200 bg-white`}>
|
| 418 |
-
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
|
| 419 |
-
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Source Code</span>
|
| 420 |
-
<div className="flex items-center gap-2">
|
| 421 |
-
<button
|
| 422 |
-
onClick={handleCopyLaTeX}
|
| 423 |
-
className="text-xs text-gray-500 hover:text-gray-900 flex items-center gap-1"
|
| 424 |
-
>
|
| 425 |
-
<Copy size={12} />
|
| 426 |
-
{copySuccess ? 'Copied' : 'Copy'}
|
| 427 |
-
</button>
|
| 428 |
</div>
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
}}
|
| 467 |
-
dangerouslySetInnerHTML={{ __html: renderedContent }}
|
| 468 |
-
/>
|
| 469 |
</div>
|
| 470 |
-
</div>
|
| 471 |
-
)}
|
| 472 |
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
</div>
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
<FileText size={16} className={currentDocument?.id === doc.id ? "text-purple-500" : "text-gray-400"} weight={currentDocument?.id === doc.id ? "fill" : "regular"} />
|
| 497 |
-
<span className={`text-sm font-medium truncate ${currentDocument?.id === doc.id ? "text-gray-900" : "text-gray-600"}`}>
|
| 498 |
-
{doc.name}
|
| 499 |
-
</span>
|
| 500 |
</div>
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
</div>
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
</div>
|
| 518 |
-
</div>
|
| 519 |
-
</div>
|
| 520 |
|
| 521 |
-
|
| 522 |
-
|
| 523 |
.math-display {
|
| 524 |
margin: 1.5em 0;
|
| 525 |
text-align: center;
|
|
@@ -578,7 +558,7 @@ This document demonstrates LaTeX capabilities for: ${prompt}
|
|
| 578 |
margin-bottom: 0.5em;
|
| 579 |
}
|
| 580 |
`}</style>
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
}
|
|
|
|
| 223 |
setDocuments(updatedDocs)
|
| 224 |
setCurrentDocument(newDoc)
|
| 225 |
|
| 226 |
+
// Auto-save to session
|
| 227 |
+
useEffect(() => {
|
| 228 |
+
const saveToSession = async () => {
|
| 229 |
+
if (!sessionId) return
|
| 230 |
+
setIsSaving(true)
|
| 231 |
+
try {
|
| 232 |
+
await fetch('/api/session/code', {
|
| 233 |
+
method: 'POST',
|
| 234 |
+
headers: {
|
| 235 |
+
'Content-Type': 'application/json',
|
| 236 |
+
'x-session-id': sessionId
|
| 237 |
+
},
|
| 238 |
+
body: JSON.stringify({
|
| 239 |
+
type: 'latex',
|
| 240 |
+
code,
|
| 241 |
+
filename: 'main.tex'
|
| 242 |
+
})
|
| 243 |
+
})
|
| 244 |
+
} catch (error) {
|
| 245 |
+
console.error('Failed to save to session:', error)
|
| 246 |
+
} finally {
|
| 247 |
+
setIsSaving(false)
|
| 248 |
+
}
|
| 249 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
+
const debounce = setTimeout(saveToSession, 2000)
|
| 252 |
+
return () => clearTimeout(debounce)
|
| 253 |
+
}, [code, sessionId])
|
| 254 |
+
|
| 255 |
+
// Render LaTeX preview
|
| 256 |
+
useEffect(() => {
|
| 257 |
+
if (previewRef.current) {
|
| 258 |
+
try {
|
| 259 |
+
// Simple regex-based rendering for demo purposes
|
| 260 |
+
// In a real app, you'd use a full LaTeX engine or server-side rendering
|
| 261 |
+
const text = code
|
| 262 |
+
.replace(/\\section\{([^}]+)\}/g, '<h2>$1</h2>')
|
| 263 |
+
.replace(/\\subsection\{([^}]+)\}/g, '<h3>$1</h3>')
|
| 264 |
+
.replace(/\\textbf\{([^}]+)\}/g, '<b>$1</b>')
|
| 265 |
+
.replace(/\\textit\{([^}]+)\}/g, '<i>$1</i>')
|
| 266 |
+
.replace(/\\maketitle/, '<div class="title"><h1>My LaTeX Document</h1><p>Reuben OS</p><p>' + new Date().toLocaleDateString() + '</p></div>')
|
| 267 |
+
.replace(/\\begin\{document\}/, '')
|
| 268 |
+
.replace(/\\end\{document\}/, '')
|
| 269 |
+
.replace(/\\documentclass\{[^}]+\}/, '')
|
| 270 |
+
.replace(/\\usepackage\{[^}]+\}/, '')
|
| 271 |
+
.replace(/\\title\{[^}]+\}/, '')
|
| 272 |
+
.replace(/\\author\{[^}]+\}/, '')
|
| 273 |
+
.replace(/\\date\{[^}]+\}/, '')
|
| 274 |
+
|
| 275 |
+
// Split by math delimiters to render KaTeX
|
| 276 |
+
const parts = text.split(/(\\\[[\s\S]*?\\\]|\$[^$]+\$)/)
|
| 277 |
+
previewRef.current.innerHTML = ''
|
| 278 |
+
|
| 279 |
+
parts.forEach(part => {
|
| 280 |
+
if (part.startsWith('\\[') || part.startsWith('$')) {
|
| 281 |
+
const math = part.replace(/^\\\[|\\\]$|^\$|\$$/g, '')
|
| 282 |
+
const span = document.createElement('span')
|
| 283 |
+
try {
|
| 284 |
+
katex.render(math, span, { throwOnError: false, displayMode: part.startsWith('\\[') })
|
| 285 |
+
previewRef.current?.appendChild(span)
|
| 286 |
+
} catch (e) {
|
| 287 |
+
previewRef.current?.appendChild(document.createTextNode(part))
|
| 288 |
+
}
|
| 289 |
+
} else {
|
| 290 |
+
const div = document.createElement('div')
|
| 291 |
+
div.innerHTML = part
|
| 292 |
+
previewRef.current?.appendChild(div)
|
| 293 |
+
}
|
| 294 |
+
})
|
| 295 |
+
} catch (e) {
|
| 296 |
+
console.error('Render error:', e)
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
}, [code])
|
| 300 |
|
| 301 |
+
const handleDownload = () => {
|
| 302 |
+
const blob = new Blob([code], { type: 'text/plain' })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
const url = URL.createObjectURL(blob)
|
| 304 |
const a = document.createElement('a')
|
| 305 |
a.href = url
|
| 306 |
+
a.download = 'main.tex'
|
| 307 |
a.click()
|
| 308 |
URL.revokeObjectURL(url)
|
| 309 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
|
| 311 |
+
return (
|
| 312 |
+
<Window
|
| 313 |
+
id="latex-editor"
|
| 314 |
+
title="LaTeX Studio"
|
| 315 |
+
isOpen={true}
|
| 316 |
+
onClose={onClose}
|
| 317 |
+
onMinimize={onMinimize}
|
| 318 |
+
onMaximize={onMaximize}
|
| 319 |
+
width={1200}
|
| 320 |
+
height={800}
|
| 321 |
+
x={60}
|
| 322 |
+
y={60}
|
| 323 |
+
className="latex-studio-window"
|
| 324 |
+
>
|
| 325 |
+
<div className="flex h-full bg-[#1e1e1e] text-gray-300 font-sans">
|
| 326 |
+
{/* Sidebar */}
|
| 327 |
+
{showSidebar && (
|
| 328 |
+
<div className="w-64 bg-[#252526] border-r border-[#333] flex flex-col">
|
| 329 |
+
<div className="h-9 px-4 flex items-center text-xs font-bold text-gray-500 uppercase tracking-wider">
|
| 330 |
+
Explorer
|
| 331 |
+
</div>
|
| 332 |
+
<div className="flex-1 overflow-y-auto py-2">
|
| 333 |
+
<div className="px-2">
|
| 334 |
+
<div className="flex items-center gap-1 py-1 px-2 text-sm text-gray-300 hover:bg-[#2a2d2e] rounded cursor-pointer">
|
| 335 |
+
<CaretDown size={12} weight="bold" />
|
| 336 |
+
<span className="font-bold">PROJECT</span>
|
| 337 |
+
</div>
|
| 338 |
+
<div className="pl-4">
|
| 339 |
+
{files.map(file => (
|
| 340 |
+
<div key={file.id}>
|
| 341 |
+
<div className="flex items-center gap-2 py-1 px-2 text-sm hover:bg-[#2a2d2e] rounded cursor-pointer text-blue-400">
|
| 342 |
+
<CaretDown size={12} weight="bold" />
|
| 343 |
+
{file.name}
|
| 344 |
+
</div>
|
| 345 |
+
{file.children?.map(child => (
|
| 346 |
+
<div
|
| 347 |
+
key={child.id}
|
| 348 |
+
onClick={() => setActiveFileId(child.id)}
|
| 349 |
+
className={`flex items-center gap-2 py-1 px-2 ml-4 text-sm rounded cursor-pointer ${activeFileId === child.id ? 'bg-[#37373d] text-white' : 'text-gray-400 hover:bg-[#2a2d2e]'
|
| 350 |
+
}`}
|
| 351 |
+
>
|
| 352 |
+
<FileText size={14} />
|
| 353 |
+
{child.name}
|
| 354 |
+
</div>
|
| 355 |
+
))}
|
| 356 |
+
</div>
|
| 357 |
+
))}
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
</div>
|
| 362 |
+
)}
|
| 363 |
+
|
| 364 |
+
{/* Main Content */}
|
| 365 |
+
<div className="flex-1 flex flex-col min-w-0">
|
| 366 |
+
{/* Toolbar */}
|
| 367 |
+
<div className="h-10 bg-[#2d2d2d] border-b border-[#1e1e1e] flex items-center justify-between px-4">
|
| 368 |
+
<div className="flex items-center gap-2">
|
| 369 |
+
<button
|
| 370 |
+
onClick={() => setShowSidebar(!showSidebar)}
|
| 371 |
+
className={`p-1.5 rounded hover:bg-[#3e3e42] ${showSidebar ? 'text-white' : 'text-gray-500'}`}
|
| 372 |
+
title="Toggle Sidebar"
|
| 373 |
+
>
|
| 374 |
+
<SidebarSimple size={16} />
|
| 375 |
+
</button>
|
| 376 |
+
<div className="h-4 w-[1px] bg-gray-600 mx-2" />
|
| 377 |
+
<div className="flex items-center gap-2 px-3 py-1 bg-[#1e1e1e] rounded text-xs text-gray-400 border border-[#333]">
|
| 378 |
+
<FileText size={14} className="text-green-400" />
|
| 379 |
+
main.tex
|
| 380 |
+
</div>
|
| 381 |
+
{isSaving && (
|
| 382 |
+
<span className="text-xs text-gray-500 flex items-center gap-1">
|
| 383 |
+
<ArrowsClockwise size={12} className="animate-spin" />
|
| 384 |
+
Syncing...
|
| 385 |
+
</span>
|
| 386 |
+
)}
|
| 387 |
+
</div>
|
| 388 |
|
| 389 |
+
<div className="flex items-center gap-2">
|
| 390 |
+
<button
|
| 391 |
+
className="flex items-center gap-2 px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded text-xs font-medium transition-colors"
|
| 392 |
+
>
|
| 393 |
+
<Play size={14} weight="fill" />
|
| 394 |
+
Compile
|
| 395 |
+
</button>
|
| 396 |
+
<button
|
| 397 |
+
onClick={handleDownload}
|
| 398 |
+
className="p-1.5 text-gray-400 hover:text-white hover:bg-[#3e3e42] rounded transition-colors"
|
| 399 |
+
title="Download PDF"
|
| 400 |
+
>
|
| 401 |
+
<Download size={16} />
|
| 402 |
+
</button>
|
| 403 |
+
</div>
|
|
|
|
|
|
|
|
|
|
| 404 |
</div>
|
|
|
|
|
|
|
| 405 |
|
| 406 |
+
{/* Split View: Editor & Preview */}
|
| 407 |
+
<div className="flex-1 flex overflow-hidden">
|
| 408 |
+
{/* Editor */}
|
| 409 |
+
<div className="flex-1 border-r border-[#333]">
|
| 410 |
+
<Editor
|
| 411 |
+
height="100%"
|
| 412 |
+
defaultLanguage="latex"
|
| 413 |
+
theme="vs-dark"
|
| 414 |
+
value={code}
|
| 415 |
+
onChange={(value) => setCode(value || '')}
|
| 416 |
+
options={{
|
| 417 |
+
minimap: { enabled: false },
|
| 418 |
+
fontSize: 14,
|
| 419 |
+
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
| 420 |
+
lineNumbers: 'on',
|
| 421 |
+
scrollBeyondLastLine: false,
|
| 422 |
+
automaticLayout: true,
|
| 423 |
+
padding: { top: 16, bottom: 16 },
|
| 424 |
+
renderLineHighlight: 'all',
|
| 425 |
+
smoothScrolling: true,
|
| 426 |
+
cursorBlinking: 'smooth',
|
| 427 |
+
cursorSmoothCaretAnimation: 'on'
|
| 428 |
+
}}
|
| 429 |
+
/>
|
| 430 |
+
</div>
|
| 431 |
|
| 432 |
+
{/* Preview */}
|
| 433 |
+
<div className="w-[500px] bg-[#525659] flex flex-col border-l border-[#333]">
|
| 434 |
+
<div className="h-8 bg-[#323639] border-b border-[#2b2b2b] flex items-center justify-between px-3 shadow-md z-10">
|
| 435 |
+
<span className="text-xs font-bold text-gray-400 uppercase">PDF Preview</span>
|
| 436 |
+
<div className="flex items-center gap-2">
|
| 437 |
+
<span className="text-xs text-gray-400">100%</span>
|
| 438 |
+
</div>
|
| 439 |
+
</div>
|
| 440 |
+
<div className="flex-1 overflow-y-auto p-8 flex justify-center">
|
| 441 |
+
<div
|
| 442 |
+
className="bg-white w-full max-w-[800px] min-h-[1000px] shadow-2xl p-12 text-black font-serif"
|
| 443 |
+
ref={previewRef}
|
| 444 |
+
style={{
|
| 445 |
+
boxShadow: '0 0 20px rgba(0,0,0,0.5)'
|
| 446 |
+
}}
|
| 447 |
+
dangerouslySetInnerHTML={{ __html: renderedContent }}
|
| 448 |
+
/>
|
| 449 |
+
</div>
|
| 450 |
</div>
|
| 451 |
+
)}
|
| 452 |
+
|
| 453 |
+
{/* Documents Sidebar */}
|
| 454 |
+
<div className="w-64 bg-gray-50 border-l border-gray-200 flex flex-col">
|
| 455 |
+
<div className="p-4 border-b border-gray-200">
|
| 456 |
+
<span className="text-xs font-bold text-gray-400 uppercase tracking-wider">Saved Documents</span>
|
| 457 |
+
</div>
|
| 458 |
+
|
| 459 |
+
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
| 460 |
+
{documents.length === 0 ? (
|
| 461 |
+
<div className="text-gray-400 text-sm text-center py-8 italic">
|
| 462 |
+
No saved documents
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
</div>
|
| 464 |
+
) : (
|
| 465 |
+
documents.map(doc => (
|
| 466 |
+
<div
|
| 467 |
+
key={doc.id}
|
| 468 |
+
className={`group rounded-lg p-3 cursor-pointer border transition-all ${currentDocument?.id === doc.id
|
| 469 |
+
? 'bg-white border-purple-200 shadow-sm ring-1 ring-purple-500/20'
|
| 470 |
+
: 'bg-transparent border-transparent hover:bg-white hover:border-gray-200'
|
| 471 |
+
}`}
|
| 472 |
+
onClick={() => handleLoadDocument(doc)}
|
| 473 |
+
>
|
| 474 |
+
<div className="flex items-center justify-between mb-1">
|
| 475 |
+
<div className="flex items-center gap-2 overflow-hidden">
|
| 476 |
+
<FileText size={16} className={currentDocument?.id === doc.id ? "text-purple-500" : "text-gray-400"} weight={currentDocument?.id === doc.id ? "fill" : "regular"} />
|
| 477 |
+
<span className={`text-sm font-medium truncate ${currentDocument?.id === doc.id ? "text-gray-900" : "text-gray-600"}`}>
|
| 478 |
+
{doc.name}
|
| 479 |
+
</span>
|
| 480 |
+
</div>
|
| 481 |
+
<button
|
| 482 |
+
onClick={(e) => {
|
| 483 |
+
e.stopPropagation()
|
| 484 |
+
handleDeleteDocument(doc.id)
|
| 485 |
+
}}
|
| 486 |
+
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-50 rounded text-gray-400 hover:text-red-500 transition-all"
|
| 487 |
+
>
|
| 488 |
+
<Trash size={14} />
|
| 489 |
+
</button>
|
| 490 |
+
</div>
|
| 491 |
+
<div className="text-[10px] text-gray-400 pl-6">
|
| 492 |
+
{doc.modifiedAt.toLocaleDateString()}
|
| 493 |
+
</div>
|
| 494 |
+
</div>
|
| 495 |
+
))
|
| 496 |
+
)}
|
| 497 |
</div>
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
|
|
|
|
|
|
|
|
|
| 500 |
|
| 501 |
+
{/* Style for rendered content */}
|
| 502 |
+
<style jsx global>{`
|
| 503 |
.math-display {
|
| 504 |
margin: 1.5em 0;
|
| 505 |
text-align: center;
|
|
|
|
| 558 |
margin-bottom: 0.5em;
|
| 559 |
}
|
| 560 |
`}</style>
|
| 561 |
+
</div>
|
| 562 |
+
</Window>
|
| 563 |
+
)
|
| 564 |
+
}
|
app/components/Terminal.tsx
DELETED
|
@@ -1,227 +0,0 @@
|
|
| 1 |
-
'use client'
|
| 2 |
-
|
| 3 |
-
import React, { useState, useRef, useEffect } from 'react'
|
| 4 |
-
import Window from './Window'
|
| 5 |
-
|
| 6 |
-
interface TerminalProps {
|
| 7 |
-
onClose: () => void
|
| 8 |
-
onMinimize?: () => void
|
| 9 |
-
onMaximize?: () => void
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
interface FileSystem {
|
| 13 |
-
[key: string]: string[]
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export function Terminal({ onClose, onMinimize, onMaximize }: TerminalProps) {
|
| 17 |
-
const [history, setHistory] = useState<string[]>([
|
| 18 |
-
'Last login: ' + new Date().toLocaleString() + ' on ttys000'
|
| 19 |
-
])
|
| 20 |
-
const [currentInput, setCurrentInput] = useState('')
|
| 21 |
-
const [currentPath, setCurrentPath] = useState('~')
|
| 22 |
-
const inputRef = useRef<HTMLInputElement>(null)
|
| 23 |
-
const outputRef = useRef<HTMLDivElement>(null)
|
| 24 |
-
|
| 25 |
-
const fileSystem: FileSystem = {
|
| 26 |
-
'~': ['Desktop', 'Documents', 'Downloads', 'Pictures', 'Music', 'Videos'],
|
| 27 |
-
'Desktop': ['screenshot.png', 'notes.txt', 'project'],
|
| 28 |
-
'Documents': ['resume.pdf', 'budget.xlsx', 'report.docx'],
|
| 29 |
-
'Downloads': ['installer.dmg', 'image.jpg', 'archive.zip'],
|
| 30 |
-
'Pictures': ['vacation.jpg', 'family.png'],
|
| 31 |
-
'Music': ['song.mp3', 'playlist.m3u'],
|
| 32 |
-
'Videos': ['tutorial.mp4', 'presentation.mov'],
|
| 33 |
-
'project': ['index.html', 'style.css', 'script.js', 'README.md']
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
useEffect(() => {
|
| 37 |
-
if (outputRef.current) {
|
| 38 |
-
outputRef.current.scrollTop = outputRef.current.scrollHeight
|
| 39 |
-
}
|
| 40 |
-
}, [history])
|
| 41 |
-
|
| 42 |
-
const handleCommand = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
| 43 |
-
if (e.key === 'Enter') {
|
| 44 |
-
const cmd = currentInput.trim()
|
| 45 |
-
const newHistory = [...history]
|
| 46 |
-
|
| 47 |
-
// Add command to history
|
| 48 |
-
newHistory.push(`guest@studyos:${currentPath} $ ${cmd}`)
|
| 49 |
-
|
| 50 |
-
// Process command
|
| 51 |
-
const args = cmd.split(' ')
|
| 52 |
-
const command = args[0].toLowerCase()
|
| 53 |
-
|
| 54 |
-
switch (command) {
|
| 55 |
-
case 'help':
|
| 56 |
-
newHistory.push('Available commands:')
|
| 57 |
-
newHistory.push(' help - Show this help message')
|
| 58 |
-
newHistory.push(' ls - List directory contents')
|
| 59 |
-
newHistory.push(' cd - Change directory')
|
| 60 |
-
newHistory.push(' pwd - Print working directory')
|
| 61 |
-
newHistory.push(' clear - Clear terminal')
|
| 62 |
-
newHistory.push(' date - Show current date and time')
|
| 63 |
-
newHistory.push(' whoami - Display current user')
|
| 64 |
-
newHistory.push(' echo - Display message')
|
| 65 |
-
newHistory.push(' cat - Display file contents')
|
| 66 |
-
newHistory.push(' mkdir - Create directory')
|
| 67 |
-
newHistory.push(' touch - Create file')
|
| 68 |
-
newHistory.push(' rm - Remove file')
|
| 69 |
-
newHistory.push(' open - Open application')
|
| 70 |
-
break
|
| 71 |
-
|
| 72 |
-
case 'ls':
|
| 73 |
-
const files = fileSystem[currentPath] || []
|
| 74 |
-
if (files.length > 0) {
|
| 75 |
-
const fileList = files.map(f => {
|
| 76 |
-
const isDir = fileSystem[f] !== undefined
|
| 77 |
-
return isDir ?
|
| 78 |
-
`\x1b[34m${f}/\x1b[0m` : // Blue for directories
|
| 79 |
-
f
|
| 80 |
-
}).join(' ')
|
| 81 |
-
newHistory.push(fileList)
|
| 82 |
-
} else {
|
| 83 |
-
newHistory.push('Directory is empty')
|
| 84 |
-
}
|
| 85 |
-
break
|
| 86 |
-
|
| 87 |
-
case 'cd':
|
| 88 |
-
if (args[1] === '..') {
|
| 89 |
-
setCurrentPath('~')
|
| 90 |
-
} else if (args[1] === '~' || !args[1]) {
|
| 91 |
-
setCurrentPath('~')
|
| 92 |
-
} else if (fileSystem[args[1]]) {
|
| 93 |
-
setCurrentPath(args[1])
|
| 94 |
-
} else {
|
| 95 |
-
newHistory.push(`cd: no such file or directory: ${args[1]}`)
|
| 96 |
-
}
|
| 97 |
-
break
|
| 98 |
-
|
| 99 |
-
case 'pwd':
|
| 100 |
-
newHistory.push(currentPath === '~' ? '/home/guest' : `/home/guest/${currentPath}`)
|
| 101 |
-
break
|
| 102 |
-
|
| 103 |
-
case 'clear':
|
| 104 |
-
setHistory(['Last login: ' + new Date().toLocaleString() + ' on ttys000'])
|
| 105 |
-
setCurrentInput('')
|
| 106 |
-
return
|
| 107 |
-
|
| 108 |
-
case 'date':
|
| 109 |
-
newHistory.push(new Date().toString())
|
| 110 |
-
break
|
| 111 |
-
|
| 112 |
-
case 'whoami':
|
| 113 |
-
newHistory.push('guest')
|
| 114 |
-
break
|
| 115 |
-
|
| 116 |
-
case 'echo':
|
| 117 |
-
newHistory.push(args.slice(1).join(' '))
|
| 118 |
-
break
|
| 119 |
-
|
| 120 |
-
case 'cat':
|
| 121 |
-
if (args[1]) {
|
| 122 |
-
const files = fileSystem[currentPath] || []
|
| 123 |
-
if (files.includes(args[1])) {
|
| 124 |
-
newHistory.push(`Contents of ${args[1]}:`)
|
| 125 |
-
newHistory.push('(This is a simulated file content)')
|
| 126 |
-
} else {
|
| 127 |
-
newHistory.push(`cat: ${args[1]}: No such file`)
|
| 128 |
-
}
|
| 129 |
-
} else {
|
| 130 |
-
newHistory.push('Usage: cat <filename>')
|
| 131 |
-
}
|
| 132 |
-
break
|
| 133 |
-
|
| 134 |
-
case 'mkdir':
|
| 135 |
-
if (args[1]) {
|
| 136 |
-
newHistory.push(`Directory '${args[1]}' created`)
|
| 137 |
-
} else {
|
| 138 |
-
newHistory.push('Usage: mkdir <dirname>')
|
| 139 |
-
}
|
| 140 |
-
break
|
| 141 |
-
|
| 142 |
-
case 'touch':
|
| 143 |
-
if (args[1]) {
|
| 144 |
-
newHistory.push(`File '${args[1]}' created`)
|
| 145 |
-
} else {
|
| 146 |
-
newHistory.push('Usage: touch <filename>')
|
| 147 |
-
}
|
| 148 |
-
break
|
| 149 |
-
|
| 150 |
-
case 'rm':
|
| 151 |
-
if (args[1]) {
|
| 152 |
-
newHistory.push(`File '${args[1]}' removed`)
|
| 153 |
-
} else {
|
| 154 |
-
newHistory.push('Usage: rm <filename>')
|
| 155 |
-
}
|
| 156 |
-
break
|
| 157 |
-
|
| 158 |
-
case 'open':
|
| 159 |
-
if (args[1]) {
|
| 160 |
-
newHistory.push(`Opening ${args[1]}...`)
|
| 161 |
-
} else {
|
| 162 |
-
newHistory.push('Usage: open <application>')
|
| 163 |
-
}
|
| 164 |
-
break
|
| 165 |
-
|
| 166 |
-
case '':
|
| 167 |
-
break
|
| 168 |
-
|
| 169 |
-
default:
|
| 170 |
-
newHistory.push(`zsh: command not found: ${command}`)
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
setHistory(newHistory)
|
| 174 |
-
setCurrentInput('')
|
| 175 |
-
}
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
return (
|
| 179 |
-
<Window
|
| 180 |
-
id="terminal"
|
| 181 |
-
title="Terminal — zsh — 80×24"
|
| 182 |
-
isOpen={true}
|
| 183 |
-
onClose={onClose}
|
| 184 |
-
onMinimize={onMinimize}
|
| 185 |
-
onMaximize={onMaximize}
|
| 186 |
-
width={600}
|
| 187 |
-
height={400}
|
| 188 |
-
x={120}
|
| 189 |
-
y={120}
|
| 190 |
-
darkMode={true}
|
| 191 |
-
className="terminal-window"
|
| 192 |
-
>
|
| 193 |
-
<div
|
| 194 |
-
className="flex-1 bg-[#1e1e1e] p-2 font-mono text-sm text-white overflow-auto"
|
| 195 |
-
ref={outputRef}
|
| 196 |
-
onClick={() => inputRef.current?.focus()}
|
| 197 |
-
>
|
| 198 |
-
{history.map((line, index) => (
|
| 199 |
-
<div
|
| 200 |
-
key={index}
|
| 201 |
-
className="whitespace-pre-wrap"
|
| 202 |
-
dangerouslySetInnerHTML={{
|
| 203 |
-
__html: line
|
| 204 |
-
.replace(/\x1b\[34m/g, '<span style="color: #4FC3F7;">')
|
| 205 |
-
.replace(/\x1b\[0m/g, '</span>')
|
| 206 |
-
.replace(/guest@studyos/g, '<span style="color: #4CAF50;">guest@studyos</span>')
|
| 207 |
-
}}
|
| 208 |
-
/>
|
| 209 |
-
))}
|
| 210 |
-
<div className="flex">
|
| 211 |
-
<span className="text-green-400 mr-2">guest@studyos:{currentPath} $</span>
|
| 212 |
-
<input
|
| 213 |
-
ref={inputRef}
|
| 214 |
-
type="text"
|
| 215 |
-
value={currentInput}
|
| 216 |
-
onChange={(e) => setCurrentInput(e.target.value)}
|
| 217 |
-
onKeyDown={handleCommand}
|
| 218 |
-
className="flex-1 bg-transparent border-none outline-none text-white"
|
| 219 |
-
autoFocus
|
| 220 |
-
autoComplete="off"
|
| 221 |
-
spellCheck={false}
|
| 222 |
-
/>
|
| 223 |
-
</div>
|
| 224 |
-
</div>
|
| 225 |
-
</Window>
|
| 226 |
-
)
|
| 227 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/components/WebBrowser.tsx
DELETED
|
@@ -1,277 +0,0 @@
|
|
| 1 |
-
'use client'
|
| 2 |
-
|
| 3 |
-
import React, { useState, useRef } from 'react'
|
| 4 |
-
import { Globe, RefreshCw, ArrowLeft, ArrowRight, Home, Lock, ExternalLink, X, Maximize2, Minimize2 } from 'lucide-react'
|
| 5 |
-
|
| 6 |
-
interface Tab {
|
| 7 |
-
id: string
|
| 8 |
-
url: string
|
| 9 |
-
title: string
|
| 10 |
-
isActive: boolean
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
export default function WebBrowser() {
|
| 14 |
-
const [tabs, setTabs] = useState<Tab[]>([
|
| 15 |
-
{ id: '1', url: 'https://www.google.com', title: 'Google', isActive: true }
|
| 16 |
-
])
|
| 17 |
-
const [currentUrl, setCurrentUrl] = useState('https://www.google.com')
|
| 18 |
-
const [inputUrl, setInputUrl] = useState('https://www.google.com')
|
| 19 |
-
const [isLoading, setIsLoading] = useState(false)
|
| 20 |
-
const [isFullscreen, setIsFullscreen] = useState(false)
|
| 21 |
-
const [showBrowser, setShowBrowser] = useState(false)
|
| 22 |
-
const iframeRef = useRef<HTMLIFrameElement>(null)
|
| 23 |
-
|
| 24 |
-
const handleNavigate = (url: string) => {
|
| 25 |
-
// Ensure URL has protocol
|
| 26 |
-
let finalUrl = url
|
| 27 |
-
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
| 28 |
-
finalUrl = 'https://' + url
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
setCurrentUrl(finalUrl)
|
| 32 |
-
setInputUrl(finalUrl)
|
| 33 |
-
setIsLoading(true)
|
| 34 |
-
|
| 35 |
-
// Update active tab
|
| 36 |
-
setTabs(prevTabs =>
|
| 37 |
-
prevTabs.map(tab =>
|
| 38 |
-
tab.isActive ? { ...tab, url: finalUrl } : tab
|
| 39 |
-
)
|
| 40 |
-
)
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
const handleRefresh = () => {
|
| 44 |
-
if (iframeRef.current) {
|
| 45 |
-
const currentSrc = iframeRef.current.src
|
| 46 |
-
iframeRef.current.src = ''
|
| 47 |
-
setTimeout(() => {
|
| 48 |
-
if (iframeRef.current) {
|
| 49 |
-
iframeRef.current.src = currentSrc
|
| 50 |
-
}
|
| 51 |
-
}, 10)
|
| 52 |
-
}
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
const handleBack = () => {
|
| 56 |
-
// Browser history would be handled by iframe internally
|
| 57 |
-
window.history.back()
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
const handleForward = () => {
|
| 61 |
-
window.history.forward()
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
const handleHome = () => {
|
| 65 |
-
handleNavigate('https://www.google.com')
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
const addNewTab = () => {
|
| 69 |
-
const newTab: Tab = {
|
| 70 |
-
id: Date.now().toString(),
|
| 71 |
-
url: 'https://www.google.com',
|
| 72 |
-
title: 'New Tab',
|
| 73 |
-
isActive: true
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
setTabs(prevTabs => [
|
| 77 |
-
...prevTabs.map(tab => ({ ...tab, isActive: false })),
|
| 78 |
-
newTab
|
| 79 |
-
])
|
| 80 |
-
setCurrentUrl(newTab.url)
|
| 81 |
-
setInputUrl(newTab.url)
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
const switchTab = (tabId: string) => {
|
| 85 |
-
const tab = tabs.find(t => t.id === tabId)
|
| 86 |
-
if (tab) {
|
| 87 |
-
setTabs(prevTabs =>
|
| 88 |
-
prevTabs.map(t => ({ ...t, isActive: t.id === tabId }))
|
| 89 |
-
)
|
| 90 |
-
setCurrentUrl(tab.url)
|
| 91 |
-
setInputUrl(tab.url)
|
| 92 |
-
}
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
const closeTab = (tabId: string) => {
|
| 96 |
-
if (tabs.length === 1) return // Don't close last tab
|
| 97 |
-
|
| 98 |
-
const tabIndex = tabs.findIndex(t => t.id === tabId)
|
| 99 |
-
const wasActive = tabs[tabIndex].isActive
|
| 100 |
-
|
| 101 |
-
const newTabs = tabs.filter(t => t.id !== tabId)
|
| 102 |
-
|
| 103 |
-
if (wasActive) {
|
| 104 |
-
const newActiveIndex = tabIndex > 0 ? tabIndex - 1 : 0
|
| 105 |
-
newTabs[newActiveIndex].isActive = true
|
| 106 |
-
setCurrentUrl(newTabs[newActiveIndex].url)
|
| 107 |
-
setInputUrl(newTabs[newActiveIndex].url)
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
setTabs(newTabs)
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
const openInNewWindow = () => {
|
| 114 |
-
window.open(currentUrl, '_blank')
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
if (!showBrowser) {
|
| 118 |
-
return (
|
| 119 |
-
<button
|
| 120 |
-
onClick={() => setShowBrowser(true)}
|
| 121 |
-
className="fixed bottom-4 right-4 z-50 px-4 py-2 bg-blue-600 text-white rounded-lg shadow-lg hover:bg-blue-700 transition-colors flex items-center gap-2"
|
| 122 |
-
>
|
| 123 |
-
<Globe size={20} />
|
| 124 |
-
Open Browser
|
| 125 |
-
</button>
|
| 126 |
-
)
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
return (
|
| 130 |
-
<div className={`fixed ${isFullscreen ? 'inset-0 z-[9999]' : 'inset-4 z-40'} bg-white rounded-lg shadow-2xl flex flex-col transition-all duration-300`}>
|
| 131 |
-
{/* Browser Header */}
|
| 132 |
-
<div className="bg-gray-100 border-b border-gray-300 rounded-t-lg">
|
| 133 |
-
{/* Tabs */}
|
| 134 |
-
<div className="flex items-center bg-gray-200 px-2 py-1 overflow-x-auto">
|
| 135 |
-
{tabs.map(tab => (
|
| 136 |
-
<div
|
| 137 |
-
key={tab.id}
|
| 138 |
-
className={`flex items-center px-3 py-1.5 mr-1 rounded-t-md cursor-pointer transition-colors ${
|
| 139 |
-
tab.isActive ? 'bg-white' : 'bg-gray-100 hover:bg-gray-50'
|
| 140 |
-
}`}
|
| 141 |
-
onClick={() => switchTab(tab.id)}
|
| 142 |
-
>
|
| 143 |
-
<Globe size={14} className="mr-2 text-gray-600" />
|
| 144 |
-
<span className="text-sm max-w-[150px] truncate">{tab.title}</span>
|
| 145 |
-
<button
|
| 146 |
-
onClick={(e) => {
|
| 147 |
-
e.stopPropagation()
|
| 148 |
-
closeTab(tab.id)
|
| 149 |
-
}}
|
| 150 |
-
className="ml-2 p-0.5 hover:bg-gray-200 rounded"
|
| 151 |
-
>
|
| 152 |
-
<X size={14} />
|
| 153 |
-
</button>
|
| 154 |
-
</div>
|
| 155 |
-
))}
|
| 156 |
-
<button
|
| 157 |
-
onClick={addNewTab}
|
| 158 |
-
className="px-2 py-1 text-gray-600 hover:bg-gray-200 rounded"
|
| 159 |
-
>
|
| 160 |
-
+
|
| 161 |
-
</button>
|
| 162 |
-
</div>
|
| 163 |
-
|
| 164 |
-
{/* Navigation Bar */}
|
| 165 |
-
<div className="flex items-center gap-2 p-2 bg-white">
|
| 166 |
-
<button
|
| 167 |
-
onClick={handleBack}
|
| 168 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors"
|
| 169 |
-
title="Back"
|
| 170 |
-
>
|
| 171 |
-
<ArrowLeft size={18} />
|
| 172 |
-
</button>
|
| 173 |
-
<button
|
| 174 |
-
onClick={handleForward}
|
| 175 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors"
|
| 176 |
-
title="Forward"
|
| 177 |
-
>
|
| 178 |
-
<ArrowRight size={18} />
|
| 179 |
-
</button>
|
| 180 |
-
<button
|
| 181 |
-
onClick={handleRefresh}
|
| 182 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors"
|
| 183 |
-
title="Refresh"
|
| 184 |
-
>
|
| 185 |
-
<RefreshCw size={18} className={isLoading ? 'animate-spin' : ''} />
|
| 186 |
-
</button>
|
| 187 |
-
<button
|
| 188 |
-
onClick={handleHome}
|
| 189 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors"
|
| 190 |
-
title="Home"
|
| 191 |
-
>
|
| 192 |
-
<Home size={18} />
|
| 193 |
-
</button>
|
| 194 |
-
|
| 195 |
-
{/* URL Bar */}
|
| 196 |
-
<div className="flex-1 flex items-center bg-gray-50 rounded-md px-3 py-1.5 border border-gray-300">
|
| 197 |
-
<Lock size={14} className="text-green-600 mr-2" />
|
| 198 |
-
<input
|
| 199 |
-
type="text"
|
| 200 |
-
value={inputUrl}
|
| 201 |
-
onChange={(e) => setInputUrl(e.target.value)}
|
| 202 |
-
onKeyPress={(e) => {
|
| 203 |
-
if (e.key === 'Enter') {
|
| 204 |
-
handleNavigate(inputUrl)
|
| 205 |
-
}
|
| 206 |
-
}}
|
| 207 |
-
className="flex-1 bg-transparent outline-none text-sm"
|
| 208 |
-
placeholder="Enter URL..."
|
| 209 |
-
/>
|
| 210 |
-
</div>
|
| 211 |
-
|
| 212 |
-
<button
|
| 213 |
-
onClick={openInNewWindow}
|
| 214 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors"
|
| 215 |
-
title="Open in New Window"
|
| 216 |
-
>
|
| 217 |
-
<ExternalLink size={18} />
|
| 218 |
-
</button>
|
| 219 |
-
<button
|
| 220 |
-
onClick={() => setIsFullscreen(!isFullscreen)}
|
| 221 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors"
|
| 222 |
-
title={isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
|
| 223 |
-
>
|
| 224 |
-
{isFullscreen ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
|
| 225 |
-
</button>
|
| 226 |
-
<button
|
| 227 |
-
onClick={() => setShowBrowser(false)}
|
| 228 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors text-red-600"
|
| 229 |
-
title="Close Browser"
|
| 230 |
-
>
|
| 231 |
-
<X size={18} />
|
| 232 |
-
</button>
|
| 233 |
-
</div>
|
| 234 |
-
</div>
|
| 235 |
-
|
| 236 |
-
{/* Browser Content */}
|
| 237 |
-
<div className="flex-1 relative bg-white">
|
| 238 |
-
{/* Loading Indicator */}
|
| 239 |
-
{isLoading && (
|
| 240 |
-
<div className="absolute top-0 left-0 w-full h-1 bg-blue-600 animate-pulse z-10" />
|
| 241 |
-
)}
|
| 242 |
-
|
| 243 |
-
{/* iframe Content */}
|
| 244 |
-
<iframe
|
| 245 |
-
ref={iframeRef}
|
| 246 |
-
src={currentUrl}
|
| 247 |
-
className="w-full h-full border-0"
|
| 248 |
-
onLoad={() => {
|
| 249 |
-
setIsLoading(false)
|
| 250 |
-
// Try to get the title from the iframe (may be blocked by CORS)
|
| 251 |
-
try {
|
| 252 |
-
const title = iframeRef.current?.contentDocument?.title || 'Web Page'
|
| 253 |
-
setTabs(prevTabs =>
|
| 254 |
-
prevTabs.map(tab =>
|
| 255 |
-
tab.isActive ? { ...tab, title } : tab
|
| 256 |
-
)
|
| 257 |
-
)
|
| 258 |
-
} catch (e) {
|
| 259 |
-
// CORS will block this for most external sites
|
| 260 |
-
console.log('Could not access iframe title due to CORS')
|
| 261 |
-
}
|
| 262 |
-
}}
|
| 263 |
-
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
| 264 |
-
title="Web Browser"
|
| 265 |
-
/>
|
| 266 |
-
|
| 267 |
-
{/* CORS Notice */}
|
| 268 |
-
<div className="absolute bottom-4 right-4 bg-yellow-100 border border-yellow-400 rounded-lg p-3 max-w-sm">
|
| 269 |
-
<p className="text-xs text-yellow-800">
|
| 270 |
-
<strong>Note:</strong> Some websites may not display due to security restrictions (CORS).
|
| 271 |
-
For full browsing, consider using a proxy service or browser extension.
|
| 272 |
-
</p>
|
| 273 |
-
</div>
|
| 274 |
-
</div>
|
| 275 |
-
</div>
|
| 276 |
-
)
|
| 277 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/components/WebBrowserApp.tsx
DELETED
|
@@ -1,441 +0,0 @@
|
|
| 1 |
-
'use client'
|
| 2 |
-
|
| 3 |
-
import React, { useState, useRef } from 'react'
|
| 4 |
-
import { Globe, RefreshCw, ArrowLeft, ArrowRight, Home, Lock, ExternalLink, AlertTriangle, Shield, X } from 'lucide-react'
|
| 5 |
-
import Window from './Window'
|
| 6 |
-
|
| 7 |
-
interface Tab {
|
| 8 |
-
id: string
|
| 9 |
-
url: string
|
| 10 |
-
title: string
|
| 11 |
-
isActive: boolean
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
interface WebBrowserAppProps {
|
| 15 |
-
onClose: () => void
|
| 16 |
-
onMinimize?: () => void
|
| 17 |
-
onMaximize?: () => void
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
export default function WebBrowserApp({ onClose, onMinimize, onMaximize }: WebBrowserAppProps) {
|
| 21 |
-
const [tabs, setTabs] = useState<Tab[]>([
|
| 22 |
-
{ id: '1', url: 'https://www.google.com/webhp?igu=1', title: 'Google', isActive: true }
|
| 23 |
-
])
|
| 24 |
-
const [currentUrl, setCurrentUrl] = useState('https://www.google.com/webhp?igu=1')
|
| 25 |
-
const [inputUrl, setInputUrl] = useState('https://www.google.com')
|
| 26 |
-
const [isLoading, setIsLoading] = useState(false)
|
| 27 |
-
const [isMaximized, setIsMaximized] = useState(false)
|
| 28 |
-
const [useProxy, setUseProxy] = useState(false)
|
| 29 |
-
const [showSettings, setShowSettings] = useState(false)
|
| 30 |
-
const [showHomePage, setShowHomePage] = useState(true)
|
| 31 |
-
const iframeRef = useRef<HTMLIFrameElement>(null)
|
| 32 |
-
|
| 33 |
-
// List of sites that work well in iframes
|
| 34 |
-
const iframeFriendlySites = [
|
| 35 |
-
{ name: 'Google', url: 'https://www.google.com/webhp?igu=1' },
|
| 36 |
-
{ name: 'Wikipedia', url: 'https://www.wikipedia.org' },
|
| 37 |
-
{ name: 'MDN Web Docs', url: 'https://developer.mozilla.org' },
|
| 38 |
-
{ name: 'Stack Overflow', url: 'https://stackoverflow.com' },
|
| 39 |
-
{ name: 'GitHub', url: 'https://github.com' },
|
| 40 |
-
{ name: 'DuckDuckGo', url: 'https://duckduckgo.com' }
|
| 41 |
-
]
|
| 42 |
-
|
| 43 |
-
const getProxiedUrl = (url: string) => {
|
| 44 |
-
// Use a CORS proxy service - you can replace this with your own proxy
|
| 45 |
-
if (useProxy) {
|
| 46 |
-
// Option 1: Use allorigins proxy (free, public) - SLOW but works
|
| 47 |
-
return `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`
|
| 48 |
-
|
| 49 |
-
// Option 2: Use cors-anywhere (may require your own deployment)
|
| 50 |
-
// return `https://cors-anywhere.herokuapp.com/${url}`
|
| 51 |
-
|
| 52 |
-
// Option 3: Use your own backend proxy endpoint
|
| 53 |
-
// return `/api/proxy?url=${encodeURIComponent(url)}`
|
| 54 |
-
}
|
| 55 |
-
return url
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
const handleNavigate = (url: string) => {
|
| 59 |
-
// Ensure URL has protocol
|
| 60 |
-
let finalUrl = url
|
| 61 |
-
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
| 62 |
-
// Check if it looks like a search query
|
| 63 |
-
if (!url.includes('.') || url.includes(' ')) {
|
| 64 |
-
finalUrl = `https://www.google.com/search?q=${encodeURIComponent(url)}&igu=1`
|
| 65 |
-
} else {
|
| 66 |
-
finalUrl = 'https://' + url
|
| 67 |
-
}
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
setShowHomePage(false)
|
| 71 |
-
const proxiedUrl = getProxiedUrl(finalUrl)
|
| 72 |
-
setCurrentUrl(proxiedUrl)
|
| 73 |
-
setInputUrl(finalUrl)
|
| 74 |
-
setIsLoading(true)
|
| 75 |
-
|
| 76 |
-
// Update active tab
|
| 77 |
-
setTabs(prevTabs =>
|
| 78 |
-
prevTabs.map(tab =>
|
| 79 |
-
tab.isActive ? { ...tab, url: finalUrl, title: new URL(finalUrl).hostname } : tab
|
| 80 |
-
)
|
| 81 |
-
)
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
const handleRefresh = () => {
|
| 85 |
-
if (iframeRef.current) {
|
| 86 |
-
setIsLoading(true)
|
| 87 |
-
const currentSrc = iframeRef.current.src
|
| 88 |
-
iframeRef.current.src = ''
|
| 89 |
-
setTimeout(() => {
|
| 90 |
-
if (iframeRef.current) {
|
| 91 |
-
iframeRef.current.src = currentSrc
|
| 92 |
-
}
|
| 93 |
-
}, 10)
|
| 94 |
-
}
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
const handleBack = () => {
|
| 98 |
-
window.history.back()
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
const handleForward = () => {
|
| 102 |
-
window.history.forward()
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
const handleHome = () => {
|
| 106 |
-
setShowHomePage(true)
|
| 107 |
-
setCurrentUrl('')
|
| 108 |
-
setInputUrl('')
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
const addNewTab = () => {
|
| 112 |
-
const newTab: Tab = {
|
| 113 |
-
id: Date.now().toString(),
|
| 114 |
-
url: 'https://www.google.com/webhp?igu=1',
|
| 115 |
-
title: 'New Tab',
|
| 116 |
-
isActive: true
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
setTabs(prevTabs => [
|
| 120 |
-
...prevTabs.map(tab => ({ ...tab, isActive: false })),
|
| 121 |
-
newTab
|
| 122 |
-
])
|
| 123 |
-
handleNavigate(newTab.url)
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
const switchTab = (tabId: string) => {
|
| 127 |
-
const tab = tabs.find(t => t.id === tabId)
|
| 128 |
-
if (tab) {
|
| 129 |
-
setTabs(prevTabs =>
|
| 130 |
-
prevTabs.map(t => ({ ...t, isActive: t.id === tabId }))
|
| 131 |
-
)
|
| 132 |
-
handleNavigate(tab.url)
|
| 133 |
-
}
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
const closeTab = (tabId: string, e: React.MouseEvent) => {
|
| 137 |
-
e.stopPropagation()
|
| 138 |
-
if (tabs.length === 1) return
|
| 139 |
-
|
| 140 |
-
const tabIndex = tabs.findIndex(t => t.id === tabId)
|
| 141 |
-
const wasActive = tabs[tabIndex].isActive
|
| 142 |
-
|
| 143 |
-
const newTabs = tabs.filter(t => t.id !== tabId)
|
| 144 |
-
|
| 145 |
-
if (wasActive) {
|
| 146 |
-
const newActiveIndex = tabIndex > 0 ? tabIndex - 1 : 0
|
| 147 |
-
newTabs[newActiveIndex].isActive = true
|
| 148 |
-
handleNavigate(newTabs[newActiveIndex].url)
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
setTabs(newTabs)
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
const openInNewWindow = () => {
|
| 155 |
-
const activeTab = tabs.find(t => t.isActive)
|
| 156 |
-
if (activeTab) {
|
| 157 |
-
window.open(activeTab.url, '_blank')
|
| 158 |
-
}
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
return (
|
| 162 |
-
<Window
|
| 163 |
-
id="browser"
|
| 164 |
-
title="Web Browser"
|
| 165 |
-
isOpen={true}
|
| 166 |
-
onClose={onClose}
|
| 167 |
-
onMinimize={onMinimize}
|
| 168 |
-
onMaximize={onMaximize}
|
| 169 |
-
width={1200}
|
| 170 |
-
height={700}
|
| 171 |
-
x={window.innerWidth / 2 - 600}
|
| 172 |
-
y={window.innerHeight / 2 - 350}
|
| 173 |
-
className="browser-window"
|
| 174 |
-
>
|
| 175 |
-
{/* Browser Header */}
|
| 176 |
-
<div className="bg-gray-100 border-b border-gray-300">
|
| 177 |
-
{/* Tabs */}
|
| 178 |
-
<div className="flex items-center bg-gray-200 px-2 py-1 overflow-x-auto">
|
| 179 |
-
{tabs.map(tab => (
|
| 180 |
-
<div
|
| 181 |
-
key={tab.id}
|
| 182 |
-
className={`flex items-center px-3 py-1.5 mr-1 rounded-t-md cursor-pointer transition-colors min-w-[150px] max-w-[200px] ${tab.isActive ? 'bg-white' : 'bg-gray-100 hover:bg-gray-50'
|
| 183 |
-
}`}
|
| 184 |
-
onClick={() => switchTab(tab.id)}
|
| 185 |
-
>
|
| 186 |
-
<Globe size={14} className="mr-2 text-gray-600 flex-shrink-0" />
|
| 187 |
-
<span className="text-sm truncate flex-1">{tab.title}</span>
|
| 188 |
-
{tabs.length > 1 && (
|
| 189 |
-
<button
|
| 190 |
-
onClick={(e) => closeTab(tab.id, e)}
|
| 191 |
-
className="ml-2 p-0.5 hover:bg-gray-200 rounded flex-shrink-0"
|
| 192 |
-
>
|
| 193 |
-
<X size={14} />
|
| 194 |
-
</button>
|
| 195 |
-
)}
|
| 196 |
-
</div>
|
| 197 |
-
))}
|
| 198 |
-
<button
|
| 199 |
-
onClick={addNewTab}
|
| 200 |
-
className="px-2 py-1 text-gray-600 hover:bg-gray-200 rounded"
|
| 201 |
-
>
|
| 202 |
-
+
|
| 203 |
-
</button>
|
| 204 |
-
</div>
|
| 205 |
-
|
| 206 |
-
{/* Navigation Bar */}
|
| 207 |
-
<div className="flex items-center gap-2 p-2 bg-white">
|
| 208 |
-
<button
|
| 209 |
-
onClick={handleBack}
|
| 210 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors"
|
| 211 |
-
title="Back"
|
| 212 |
-
>
|
| 213 |
-
<ArrowLeft size={18} />
|
| 214 |
-
</button>
|
| 215 |
-
<button
|
| 216 |
-
onClick={handleForward}
|
| 217 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors"
|
| 218 |
-
title="Forward"
|
| 219 |
-
>
|
| 220 |
-
<ArrowRight size={18} />
|
| 221 |
-
</button>
|
| 222 |
-
<button
|
| 223 |
-
onClick={handleRefresh}
|
| 224 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors"
|
| 225 |
-
title="Refresh"
|
| 226 |
-
>
|
| 227 |
-
<RefreshCw size={18} className={isLoading ? 'animate-spin' : ''} />
|
| 228 |
-
</button>
|
| 229 |
-
<button
|
| 230 |
-
onClick={handleHome}
|
| 231 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors"
|
| 232 |
-
title="Home"
|
| 233 |
-
>
|
| 234 |
-
<Home size={18} />
|
| 235 |
-
</button>
|
| 236 |
-
|
| 237 |
-
{/* URL Bar */}
|
| 238 |
-
<div className="flex-1 flex items-center bg-gray-50 rounded-md px-3 py-1.5 border border-gray-300">
|
| 239 |
-
{useProxy ? (
|
| 240 |
-
<Shield size={14} className="text-green-600 mr-2" />
|
| 241 |
-
) : (
|
| 242 |
-
<Lock size={14} className="text-gray-400 mr-2" />
|
| 243 |
-
)}
|
| 244 |
-
<input
|
| 245 |
-
type="text"
|
| 246 |
-
value={inputUrl}
|
| 247 |
-
onChange={(e) => setInputUrl(e.target.value)}
|
| 248 |
-
onKeyPress={(e) => {
|
| 249 |
-
if (e.key === 'Enter') {
|
| 250 |
-
handleNavigate(inputUrl)
|
| 251 |
-
}
|
| 252 |
-
}}
|
| 253 |
-
className="flex-1 bg-transparent outline-none text-sm"
|
| 254 |
-
placeholder="Enter URL or search..."
|
| 255 |
-
/>
|
| 256 |
-
</div>
|
| 257 |
-
|
| 258 |
-
<button
|
| 259 |
-
onClick={() => setShowSettings(!showSettings)}
|
| 260 |
-
className={`p-2 rounded transition-colors ${showSettings ? 'bg-blue-100' : 'hover:bg-gray-100'}`}
|
| 261 |
-
title="Settings"
|
| 262 |
-
>
|
| 263 |
-
<Shield size={18} className={useProxy ? 'text-green-600' : 'text-gray-400'} />
|
| 264 |
-
</button>
|
| 265 |
-
<button
|
| 266 |
-
onClick={openInNewWindow}
|
| 267 |
-
className="p-2 hover:bg-gray-100 rounded transition-colors"
|
| 268 |
-
title="Open in New Window"
|
| 269 |
-
>
|
| 270 |
-
<ExternalLink size={18} />
|
| 271 |
-
</button>
|
| 272 |
-
</div>
|
| 273 |
-
|
| 274 |
-
{/* Settings Panel */}
|
| 275 |
-
{showSettings && (
|
| 276 |
-
<div className="px-4 py-3 bg-blue-50 border-b border-blue-200">
|
| 277 |
-
<div className="flex items-center justify-between mb-2">
|
| 278 |
-
<label className="flex items-center gap-2 cursor-pointer">
|
| 279 |
-
<input
|
| 280 |
-
type="checkbox"
|
| 281 |
-
checked={useProxy}
|
| 282 |
-
onChange={(e) => setUseProxy(e.target.checked)}
|
| 283 |
-
className="w-4 h-4"
|
| 284 |
-
/>
|
| 285 |
-
<span className="text-sm font-medium">Use CORS Proxy</span>
|
| 286 |
-
<span className="text-xs text-gray-600">(Bypass cross-origin restrictions)</span>
|
| 287 |
-
</label>
|
| 288 |
-
</div>
|
| 289 |
-
<div className="text-xs text-gray-600">
|
| 290 |
-
<p className="mb-1">• Proxy allows accessing most websites but may affect performance</p>
|
| 291 |
-
<p>• Some interactive features may not work through proxy</p>
|
| 292 |
-
</div>
|
| 293 |
-
</div>
|
| 294 |
-
)}
|
| 295 |
-
|
| 296 |
-
{/* Quick Links */}
|
| 297 |
-
<div className="px-4 py-2 bg-gray-50 border-b flex items-center gap-2 overflow-x-auto">
|
| 298 |
-
<span className="text-xs text-gray-600 mr-2">Quick Links:</span>
|
| 299 |
-
{iframeFriendlySites.map(site => (
|
| 300 |
-
<button
|
| 301 |
-
key={site.name}
|
| 302 |
-
onClick={() => handleNavigate(site.url)}
|
| 303 |
-
className="px-3 py-1 text-xs bg-white border border-gray-300 rounded hover:bg-blue-50 hover:border-blue-400 transition-colors whitespace-nowrap"
|
| 304 |
-
>
|
| 305 |
-
{site.name}
|
| 306 |
-
</button>
|
| 307 |
-
))}
|
| 308 |
-
</div>
|
| 309 |
-
</div>
|
| 310 |
-
|
| 311 |
-
{/* Browser Content */}
|
| 312 |
-
<div className="flex-1 relative bg-white overflow-hidden">
|
| 313 |
-
{/* Loading Indicator */}
|
| 314 |
-
{isLoading && !showHomePage && (
|
| 315 |
-
<div className="absolute top-0 left-0 w-full h-1 bg-blue-600 z-20">
|
| 316 |
-
<div className="h-full bg-blue-400 animate-pulse" />
|
| 317 |
-
</div>
|
| 318 |
-
)}
|
| 319 |
-
|
| 320 |
-
{/* Home Page */}
|
| 321 |
-
{showHomePage ? (
|
| 322 |
-
<div className="w-full h-full flex flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-purple-50 p-8">
|
| 323 |
-
<div className="max-w-2xl w-full space-y-8">
|
| 324 |
-
{/* Browser Logo */}
|
| 325 |
-
<div className="flex flex-col items-center gap-4">
|
| 326 |
-
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center shadow-lg">
|
| 327 |
-
<Globe size={48} className="text-white" />
|
| 328 |
-
</div>
|
| 329 |
-
<h1 className="text-4xl font-bold text-gray-800">Web Browser</h1>
|
| 330 |
-
<p className="text-gray-600 text-center">Browse the web with speed and simplicity</p>
|
| 331 |
-
</div>
|
| 332 |
-
|
| 333 |
-
{/* Search Box */}
|
| 334 |
-
<div className="w-full">
|
| 335 |
-
<div className="relative">
|
| 336 |
-
<input
|
| 337 |
-
type="text"
|
| 338 |
-
placeholder="Search or enter website address..."
|
| 339 |
-
className="w-full px-6 py-4 text-lg rounded-full border-2 border-gray-300 focus:border-blue-500 focus:outline-none shadow-md"
|
| 340 |
-
onKeyPress={(e) => {
|
| 341 |
-
if (e.key === 'Enter') {
|
| 342 |
-
const value = (e.target as HTMLInputElement).value
|
| 343 |
-
if (value.trim()) {
|
| 344 |
-
handleNavigate(value)
|
| 345 |
-
}
|
| 346 |
-
}
|
| 347 |
-
}}
|
| 348 |
-
/>
|
| 349 |
-
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400">
|
| 350 |
-
Press Enter ↵
|
| 351 |
-
</div>
|
| 352 |
-
</div>
|
| 353 |
-
</div>
|
| 354 |
-
|
| 355 |
-
{/* Quick Access Grid */}
|
| 356 |
-
<div>
|
| 357 |
-
<h2 className="text-sm font-medium text-gray-600 mb-3">Quick Access</h2>
|
| 358 |
-
<div className="grid grid-cols-3 gap-4">
|
| 359 |
-
{iframeFriendlySites.map(site => (
|
| 360 |
-
<button
|
| 361 |
-
key={site.name}
|
| 362 |
-
onClick={() => handleNavigate(site.url)}
|
| 363 |
-
className="p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow border border-gray-200 hover:border-blue-400 group"
|
| 364 |
-
>
|
| 365 |
-
<div className="flex flex-col items-center gap-2">
|
| 366 |
-
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-100 to-purple-100 flex items-center justify-center group-hover:from-blue-200 group-hover:to-purple-200 transition-colors">
|
| 367 |
-
<Globe size={24} className="text-blue-600" />
|
| 368 |
-
</div>
|
| 369 |
-
<span className="text-sm font-medium text-gray-700">{site.name}</span>
|
| 370 |
-
</div>
|
| 371 |
-
</button>
|
| 372 |
-
))}
|
| 373 |
-
</div>
|
| 374 |
-
</div>
|
| 375 |
-
|
| 376 |
-
{/* Performance Tip */}
|
| 377 |
-
<div className="bg-blue-100 border border-blue-300 rounded-lg p-4">
|
| 378 |
-
<div className="flex items-start gap-3">
|
| 379 |
-
<Shield size={20} className="text-blue-600 mt-0.5 flex-shrink-0" />
|
| 380 |
-
<div className="text-sm">
|
| 381 |
-
<p className="text-blue-900 font-medium mb-1">⚡ Fast Direct Access</p>
|
| 382 |
-
<p className="text-blue-800">
|
| 383 |
-
For best performance, websites are loaded directly without proxy. Some sites may block iframe access due to security policies.
|
| 384 |
-
</p>
|
| 385 |
-
</div>
|
| 386 |
-
</div>
|
| 387 |
-
</div>
|
| 388 |
-
</div>
|
| 389 |
-
</div>
|
| 390 |
-
) : (
|
| 391 |
-
<>
|
| 392 |
-
{/* iframe Content */}
|
| 393 |
-
<iframe
|
| 394 |
-
ref={iframeRef}
|
| 395 |
-
src={currentUrl}
|
| 396 |
-
className="w-full h-full border-0"
|
| 397 |
-
onLoad={() => {
|
| 398 |
-
setIsLoading(false)
|
| 399 |
-
// Try to get the title (may be blocked by CORS)
|
| 400 |
-
try {
|
| 401 |
-
if (iframeRef.current?.contentDocument?.title) {
|
| 402 |
-
const title = iframeRef.current.contentDocument.title
|
| 403 |
-
setTabs(prevTabs =>
|
| 404 |
-
prevTabs.map(tab =>
|
| 405 |
-
tab.isActive ? { ...tab, title } : tab
|
| 406 |
-
)
|
| 407 |
-
)
|
| 408 |
-
}
|
| 409 |
-
} catch (e) {
|
| 410 |
-
// Expected for cross-origin content
|
| 411 |
-
}
|
| 412 |
-
}}
|
| 413 |
-
onError={() => {
|
| 414 |
-
setIsLoading(false)
|
| 415 |
-
}}
|
| 416 |
-
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
|
| 417 |
-
title="Web Browser"
|
| 418 |
-
/>
|
| 419 |
-
|
| 420 |
-
{/* Info Panel - only show if using proxy */}
|
| 421 |
-
{useProxy && (
|
| 422 |
-
<div className="absolute bottom-4 right-4 bg-yellow-100 border border-yellow-400 rounded-lg p-3 max-w-sm z-10">
|
| 423 |
-
<div className="flex items-start gap-2">
|
| 424 |
-
<AlertTriangle size={16} className="text-yellow-700 mt-0.5" />
|
| 425 |
-
<div>
|
| 426 |
-
<p className="text-xs text-yellow-800 font-medium mb-1">
|
| 427 |
-
Using Proxy (Slower)
|
| 428 |
-
</p>
|
| 429 |
-
<p className="text-xs text-yellow-700">
|
| 430 |
-
Proxy adds extra latency. Disable in settings for faster loading.
|
| 431 |
-
</p>
|
| 432 |
-
</div>
|
| 433 |
-
</div>
|
| 434 |
-
</div>
|
| 435 |
-
)}
|
| 436 |
-
</>
|
| 437 |
-
)}
|
| 438 |
-
</div>
|
| 439 |
-
</Window>
|
| 440 |
-
)
|
| 441 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|