Reubencf commited on
Commit
7f20600
·
1 Parent(s): bc139ec

github update

Browse files
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
  title: ReubenOS
3
  emoji: 🖥️
4
- colorFrom: blue
5
- colorTo: purple
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
- import WebBrowserApp from './WebBrowserApp'
14
  import { GeminiChat } from './GeminiChat'
15
  import { Clock } from './Clock'
16
- import { Terminal } from './Terminal'
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
- const [browserOpen, setBrowserOpen] = useState(false)
45
  const [geminiChatOpen, setGeminiChatOpen] = useState(false)
46
- const [terminalOpen, setTerminalOpen] = useState(false)
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
- const [browserMinimized, setBrowserMinimized] = useState(false)
73
  const [geminiChatMinimized, setGeminiChatMinimized] = useState(false)
74
- const [terminalMinimized, setTerminalMinimized] = useState(false)
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
- case 'browser':
197
- openBrowser()
198
- break
199
  case 'gemini':
200
  openGeminiChat()
201
  break
202
- case 'terminal':
203
- openTerminal()
204
- break
205
  case 'sessions':
206
  openSessionManager()
207
  break
@@ -420,19 +402,7 @@ export function Desktop() {
420
  })
421
  }
422
 
423
- if (browserMinimized && browserOpen) {
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
- if (terminalMinimized && terminalOpen) {
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
- onOpenBrowser={openBrowser}
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
- <div className="pointer-events-auto w-24 h-24">
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
- <div className="pointer-events-auto w-24 h-24">
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
- {terminalOpen && (
776
- <motion.div
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
- file={activeFlutterApp}
835
- onClose={closeFlutterRunner}
 
 
 
 
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
- Globe,
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
- onOpenBrowser: () => void
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
- onOpenBrowser,
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
- Terminal,
 
 
12
  HardDrives,
13
- Compass,
14
  Code,
15
  Lightning,
16
  Key
@@ -65,11 +66,10 @@ export function DraggableDesktopIcon({
65
  <DynamicClockIcon />
66
  </div>
67
  )
68
- case 'browser':
69
  return (
70
- <div className="bg-white w-full h-full rounded-xl overflow-hidden relative flex items-center justify-center">
71
- <div className="absolute inset-0 bg-gradient-to-tr from-blue-500 to-cyan-300" />
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">&gt;_</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-orange-400" />
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={900}
292
- height={600}
293
  x={60}
294
  y={60}
295
  className="file-manager-window"
296
  >
297
- <div className="flex flex-col h-full">
298
- {/* Toolbar */}
299
- <div className="h-12 bg-[#fafafa] border-b border-[#e0e0e0] flex items-center px-3 gap-2">
300
- <div className="flex items-center gap-1">
 
 
 
301
  <button
302
- onClick={() => {
303
- const parent = currentPath.split('/').slice(0, -1).join('/')
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
- <CaretLeft size={16} weight="bold" />
 
310
  </button>
311
  <button
312
- onClick={() => onNavigate('')}
313
- className="p-1.5 hover:bg-[#e8e8e8] rounded"
314
  >
315
- <House size={16} weight="regular" />
 
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={handleCreateFolder}
328
- className="p-1.5 hover:bg-[#e8e8e8] rounded"
329
- title="New Folder"
330
  >
331
- <Plus size={16} weight="bold" />
 
332
  </button>
333
  <button
334
- onClick={() => setUploadModalOpen(true)}
335
- className="p-1.5 hover:bg-[#e8e8e8] rounded"
336
- title="Upload File"
337
  >
338
- <Upload size={16} weight="regular" />
 
339
  </button>
340
- <div className="flex items-center gap-1 px-2 py-1 bg-white border border-[#ddd] rounded">
341
- <MagnifyingGlass size={14} weight="regular" className="text-[#666]" />
342
- <input
343
- type="text"
344
- placeholder="Search..."
345
- value={searchQuery}
346
- onChange={(e) => setSearchQuery(e.target.value)}
347
- className="text-xs outline-none w-32"
348
- />
349
- </div>
350
- </div>
351
  </div>
352
 
353
- {/* File List */}
354
- <div className="flex-1 overflow-auto p-4 bg-white">
355
- {!sessionId && !isPublicFolder ? (
356
- <div className="flex items-center justify-center h-full text-[#999] text-sm">
357
- Initializing session...
358
- </div>
359
- ) : loading ? (
360
- <div className="flex items-center justify-center h-full text-[#999] text-sm">
361
- Loading files...
 
 
 
 
 
 
 
 
 
362
  </div>
363
- ) : currentLevelFiles.length === 0 ? (
364
- <div className="flex items-center justify-center h-full text-[#999] text-sm">
365
- {searchQuery ? 'No files found' : 'Folder is empty'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  </div>
367
- ) : (
368
- <div className="grid grid-cols-6 gap-4">
369
- {currentLevelFiles.map((file) => (
370
- <div
371
- key={file.path}
372
- className="group relative"
373
- >
374
  <button
375
- onClick={() => {
376
- if (file.type === 'folder') {
377
- onNavigate(file.path)
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
- {getFileIcon(file)}
388
  </div>
389
- <span className="text-xs text-[#2c2c2c] text-center break-all w-full line-clamp-2">
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
- {/* File Actions */}
400
- {file.type === 'file' && (
401
- <div className="absolute top-1 right-1 hidden group-hover:flex gap-1">
402
- <button
403
- onClick={(e) => {
404
- e.stopPropagation()
405
- handlePreview(file)
406
- }}
407
- className="p-1 bg-white rounded shadow hover:bg-[#f0f0f0]"
408
- title="Preview"
409
- >
410
- <Eye size={14} />
411
- </button>
412
- <button
413
- onClick={(e) => {
414
- e.stopPropagation()
415
- handleDownload(file)
416
- }}
417
- className="p-1 bg-white rounded shadow hover:bg-[#f0f0f0]"
418
- title="Download"
 
419
  >
420
- <Download size={14} />
421
- </button>
422
- <button
423
- onClick={(e) => {
424
- e.stopPropagation()
425
- handleDelete(file)
426
- }}
427
- className="p-1 bg-white rounded shadow hover:bg-red-100"
428
- title="Delete"
429
- >
430
- <Trash size={14} className="text-red-600" />
431
- </button>
432
- </div>
433
- )}
434
- </div>
435
- ))}
436
- </div>
437
- )}
438
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
 
440
- {/* Status Bar */}
441
- <div className="h-6 bg-[#f6f5f4] border-t border-[#d0d0d0] flex items-center px-3 text-xs text-[#666]">
442
- {currentLevelFiles.length} items • {files.filter(f => f.type === 'folder').length} folders • {files.filter(f => f.type === 'file').length} files
 
 
 
 
443
  </div>
444
  </div>
445
  </Window>
@@ -485,45 +605,45 @@ function UploadModal({
485
  }
486
 
487
  return (
488
- <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
489
- <div className="bg-white rounded-lg p-6 w-[400px]">
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-500 mt-1">
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-[#E95420] text-white rounded hover:bg-[#d14818] disabled:opacity-50"
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, useRef, useEffect } from 'react'
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 { DartPadEmbed } from './DartPadEmbed'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- export function FlutterRunner({ file, onClose, onMinimize, onMaximize }: FlutterRunnerProps) {
23
- const [mode, setMode] = useState<'preview' | 'edit'>('preview') // preview or edit mode
24
- const [sidebarOpen, setSidebarOpen] = useState(true)
25
- const [copySuccess, setCopySuccess] = useState(false)
26
- const [isMobile, setIsMobile] = useState(false)
27
- const [code, setCode] = useState(file.dartCode)
28
- const [pubspec, setPubspec] = useState(file.pubspecYaml || '')
29
- const [saving, setSaving] = useState(false)
30
- const [saved, setSaved] = useState(false)
31
 
32
- // Detect mobile and auto-close sidebar on mobile landscape
33
- React.useEffect(() => {
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
- const handleCopyCode = async () => {
48
- try {
49
- await navigator.clipboard.writeText(code)
50
- setCopySuccess(true)
51
- setTimeout(() => setCopySuccess(false), 2000)
52
- } catch (err) {
53
- console.error('Failed to copy code:', err)
54
- }
 
 
 
 
 
 
 
 
 
55
  }
 
56
 
57
- const handleCopyPubspec = async () => {
58
- if (pubspec) {
59
- try {
60
- await navigator.clipboard.writeText(pubspec)
61
- setCopySuccess(true)
62
- setTimeout(() => setCopySuccess(false), 2000)
63
- } catch (err) {
64
- console.error('Failed to copy pubspec:', err)
65
- }
66
- }
 
 
 
 
 
 
67
  }
68
 
69
- const handleSave = async () => {
70
- setSaving(true)
71
- try {
72
- // Save the updated Flutter app
73
- const response = await fetch('/api/flutter/update', {
74
- method: 'PUT',
75
- headers: { 'Content-Type': 'application/json' },
76
- body: JSON.stringify({
77
- name: file.name,
78
- dartCode: code,
79
- pubspecYaml: pubspec,
80
- dependencies: file.dependencies
81
- })
82
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- if (response.ok) {
85
- setSaved(true)
86
- setTimeout(() => setSaved(false), 2000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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={`Flutter App: ${file.name}`}
99
  isOpen={true}
100
  onClose={onClose}
101
  onMinimize={onMinimize}
102
  onMaximize={onMaximize}
103
  width={1200}
104
- height={700}
105
- x={100}
106
- y={80}
107
- className="flutter-runner-window"
108
  >
109
- <div className="flex flex-col h-full bg-[#1E1E1E] text-gray-300">
110
- {/* Top Toolbar */}
111
- <div className="window-drag-handle h-14 bg-[#252526] border-b border-[#333] flex items-center justify-between px-4 shadow-sm cursor-move">
112
- <div className="flex items-center gap-3">
113
- <div className="w-8 h-8 bg-blue-500/20 rounded-lg flex items-center justify-center">
114
- <CodeIcon size={20} weight="bold" className="text-blue-400" />
115
  </div>
116
- <div>
117
- <h3 className="text-white font-medium text-sm">{file.name}</h3>
118
- <p className="text-xs text-gray-500">Flutter Project</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  </div>
120
  </div>
121
- <div className="flex items-center gap-2 bg-[#1E1E1E] p-1 rounded-lg border border-[#333]">
122
- <button
123
- onClick={() => setMode('preview')}
124
- className={`flex items-center gap-2 px-3 py-1.5 rounded-md transition-all text-sm font-medium ${mode === 'preview'
125
- ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/20'
126
- : 'text-gray-400 hover:text-white hover:bg-white/5'
127
- }`}
128
- >
129
- <Play size={16} weight="bold" />
130
- Preview
131
- </button>
132
- <button
133
- onClick={() => setMode('edit')}
134
- className={`flex items-center gap-2 px-3 py-1.5 rounded-md transition-all text-sm font-medium ${mode === 'edit'
135
- ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/20'
136
- : 'text-gray-400 hover:text-white hover:bg-white/5'
137
- }`}
138
- >
139
- <PencilSimple size={16} weight="bold" />
140
- Edit Code
141
- </button>
142
- </div>
143
- <div className="flex items-center gap-2">
144
- {mode === 'edit' && (
 
 
 
 
 
 
 
 
 
 
 
145
  <button
146
- onClick={handleSave}
147
- disabled={saving}
148
- className="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-500 text-white rounded-lg transition-all text-sm font-medium disabled:opacity-50 shadow-lg shadow-green-900/20"
149
  >
150
- <FloppyDisk size={16} weight="bold" />
151
- {saving ? 'Saving...' : saved ? 'Saved!' : 'Save Changes'}
152
  </button>
153
- )}
154
  </div>
155
- </div>
156
 
157
- {/* Main Content Area */}
158
- <div className="flex flex-1 overflow-hidden relative">
159
- {/* Left Panel - Code Editor or DartPad Preview */}
160
- <div className="flex-1 relative bg-[#1E1E1E]">
161
- {mode === 'preview' ? (
162
- <div className="w-full h-full overflow-hidden bg-[#1E1E1E] flex items-center justify-center">
163
- <div className="w-full h-full relative">
164
- <DartPadEmbed code={code} theme="dark" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  </div>
166
  </div>
167
- ) : (
168
- <div className="w-full h-full bg-[#1E1E1E]">
169
- <Editor
170
- height="100%"
171
- defaultLanguage="dart"
172
- value={code}
173
- onChange={(value) => setCode(value || '')}
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
- // Save to localStorage with session ID
227
- const storageKey = `latex_documents_${sessionId}`
228
- localStorage.setItem(storageKey, JSON.stringify(updatedDocs))
229
-
230
- // Also save to backend
231
- await fetch('/api/latex/save', {
232
- method: 'POST',
233
- headers: { 'Content-Type': 'application/json' },
234
- body: JSON.stringify({
235
- document: newDoc,
236
- sessionId
237
- })
238
- })
239
- } catch (error) {
240
- console.error('Failed to save:', error)
241
- } finally {
242
- setSaving(false)
243
- }
244
- }
245
-
246
- const handleLoadDocument = (doc: LaTeXDocument) => {
247
- setContent(doc.content)
248
- setCurrentDocument(doc)
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
- const handleDownloadPDF = async () => {
275
- try {
276
- const response = await fetch('/api/latex/compile', {
277
- method: 'POST',
278
- headers: { 'Content-Type': 'application/json' },
279
- body: JSON.stringify({
280
- latex: content,
281
- sessionId
282
- })
283
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
- if (response.ok) {
286
- const blob = await response.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 = `${currentDocument?.name || 'document'}.tex`
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
- return (
327
- <Window
328
- id="latex-editor"
329
- title="LaTeX Studio"
330
- isOpen={true}
331
- onClose={onClose}
332
- onMinimize={onMinimize}
333
- onMaximize={onMaximize}
334
- width={1400}
335
- height={800}
336
- x={50}
337
- y={50}
338
- className="latex-editor-window"
339
- >
340
- <div className="flex flex-col h-full bg-[#F5F5F7]">
341
- {/* Header */}
342
- <div className="window-drag-handle h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 shadow-sm z-10 cursor-move">
343
- <div className="flex items-center gap-4">
344
- <div className="w-10 h-10 bg-black rounded-xl flex items-center justify-center shadow-lg">
345
- <MathOperations size={24} weight="bold" className="text-white" />
346
- </div>
347
- <div>
348
- <h1 className="text-gray-900 font-bold text-lg leading-tight">LaTeX Studio</h1>
349
- <div className="flex items-center gap-2">
350
- <span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Project</span>
351
- {currentDocument && (
352
- <span className="text-xs text-gray-400">• {currentDocument.name}</span>
353
- )}
354
- </div>
355
- </div>
356
- </div>
357
-
358
- <div className="flex items-center gap-3">
359
- <div className="h-8 w-px bg-gray-200 mx-2" />
360
-
361
- <button
362
- onClick={handleSaveDocument}
363
- disabled={saving}
364
- className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-all text-sm font-medium"
365
- >
366
- <FloppyDisk size={18} />
367
- {saving ? 'Saving...' : 'Save'}
368
- </button>
369
-
370
- <button
371
- onClick={handleDownloadPDF}
372
- className="flex items-center gap-2 px-4 py-2 bg-black hover:bg-gray-800 text-white rounded-lg transition-all text-sm font-medium shadow-lg shadow-black/20"
373
- >
374
- <Download size={18} weight="bold" />
375
- Export PDF
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
- </div>
430
- <div className="flex-1">
431
- <Editor
432
- height="100%"
433
- defaultLanguage="latex"
434
- value={content}
435
- onChange={(value) => setContent(value || '')}
436
- theme="light"
437
- options={{
438
- minimap: { enabled: true },
439
- fontSize: 15,
440
- lineNumbers: 'on',
441
- wordWrap: 'on',
442
- automaticLayout: true,
443
- fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
444
- padding: { top: 20, bottom: 20 },
445
- renderLineHighlight: 'all',
446
- }}
447
- />
448
- </div>
449
- </div>
 
 
 
 
 
450
 
451
- {/* Preview Panel */}
452
- {showPreview && (
453
- <div className="w-1/2 flex flex-col bg-[#F5F5F7]">
454
- <div className="flex items-center justify-between px-4 py-2 bg-white border-b border-gray-200">
455
- <span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Live Preview</span>
456
- </div>
457
- <div className="flex-1 overflow-auto p-8">
458
- <div
459
- ref={previewRef}
460
- className="bg-white shadow-xl min-h-full p-12 mx-auto max-w-[210mm]"
461
- style={{
462
- fontFamily: "'Latin Modern Roman', 'Computer Modern', serif",
463
- fontSize: '18px',
464
- lineHeight: '1.6',
465
- color: '#1a1a1a'
466
- }}
467
- dangerouslySetInnerHTML={{ __html: renderedContent }}
468
- />
469
  </div>
470
- </div>
471
- )}
472
 
473
- {/* Documents Sidebar */}
474
- <div className="w-64 bg-gray-50 border-l border-gray-200 flex flex-col">
475
- <div className="p-4 border-b border-gray-200">
476
- <span className="text-xs font-bold text-gray-400 uppercase tracking-wider">Saved Documents</span>
477
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
- <div className="flex-1 overflow-y-auto p-3 space-y-2">
480
- {documents.length === 0 ? (
481
- <div className="text-gray-400 text-sm text-center py-8 italic">
482
- No saved documents
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  </div>
484
- ) : (
485
- documents.map(doc => (
486
- <div
487
- key={doc.id}
488
- className={`group rounded-lg p-3 cursor-pointer border transition-all ${currentDocument?.id === doc.id
489
- ? 'bg-white border-purple-200 shadow-sm ring-1 ring-purple-500/20'
490
- : 'bg-transparent border-transparent hover:bg-white hover:border-gray-200'
491
- }`}
492
- onClick={() => handleLoadDocument(doc)}
493
- >
494
- <div className="flex items-center justify-between mb-1">
495
- <div className="flex items-center gap-2 overflow-hidden">
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
- <button
502
- onClick={(e) => {
503
- e.stopPropagation()
504
- handleDeleteDocument(doc.id)
505
- }}
506
- className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-50 rounded text-gray-400 hover:text-red-500 transition-all"
507
- >
508
- <Trash size={14} />
509
- </button>
510
- </div>
511
- <div className="text-[10px] text-gray-400 pl-6">
512
- {doc.modifiedAt.toLocaleDateString()}
513
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  </div>
515
- ))
516
- )}
517
- </div>
518
- </div>
519
- </div>
520
 
521
- {/* Style for rendered content */}
522
- <style jsx global>{`
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
- </div>
582
- </Window>
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
- }