Reubencf commited on
Commit
95df94d
Β·
1 Parent(s): 4a962db

Implement TextEditor with LaTeX to PDF conversion using node-pdflatex

Browse files
app/api/data/route.ts CHANGED
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
2
  import fs from 'fs'
3
  import path from 'path'
4
  import { writeFile, mkdir, unlink, readdir, stat } from 'fs/promises'
 
5
 
6
  // Use /data for Hugging Face Spaces persistent storage, fallback to public/data for local dev
7
  const DATA_DIR = process.env.SPACE_ID
@@ -122,56 +123,31 @@ export async function POST(request: NextRequest) {
122
 
123
  // Auto-convert .tex files to PDF
124
  if (fileName.endsWith('.tex')) {
125
- console.log('✨ Detected .tex file, auto-converting to PDF...')
126
  try {
127
- console.log('πŸ“‘ Sending LaTeX content to LaTeX.Online API...')
128
- console.log(` Content length: ${content.length} characters`)
129
 
130
- // Compile LaTeX to PDF using LaTeX.Online
131
- const compileResponse = await fetch('https://latexonline.cc/compile', {
132
- method: 'POST',
133
- headers: {
134
- 'Content-Type': 'text/plain',
135
- },
136
- body: content,
137
- signal: AbortSignal.timeout(30000) // 30 second timeout
 
 
 
 
 
 
 
 
 
138
  })
139
-
140
- console.log(`πŸ“₯ Response status: ${compileResponse.status} ${compileResponse.statusText}`)
141
-
142
- if (compileResponse.ok) {
143
- const pdfBuffer = await compileResponse.arrayBuffer()
144
- const pdfFileName = fileName.replace('.tex', '.pdf')
145
- const pdfFilePath = path.join(targetDir, pdfFileName)
146
-
147
- console.log(`πŸ’Ύ PDF size: ${pdfBuffer.byteLength} bytes`)
148
- console.log(`πŸ“ Saving PDF to: ${pdfFilePath}`)
149
-
150
- await writeFile(pdfFilePath, Buffer.from(pdfBuffer))
151
- console.log(`βœ… PDF generated successfully: ${pdfFileName}`)
152
-
153
- return NextResponse.json({
154
- success: true,
155
- pdfGenerated: true,
156
- pdfFileName: pdfFileName,
157
- message: `LaTeX file saved and converted to ${pdfFileName}`
158
- })
159
- } else {
160
- const errorText = await compileResponse.text()
161
- console.error('❌ PDF compilation failed:')
162
- console.error(' Status:', compileResponse.status)
163
- console.error(' Error:', errorText)
164
- return NextResponse.json({
165
- success: true,
166
- pdfGenerated: false,
167
- error: `LaTeX file saved but PDF generation failed: ${compileResponse.status}`,
168
- details: errorText.substring(0, 500)
169
- })
170
- }
171
  } catch (pdfError) {
172
  console.error('❌ Error generating PDF:', pdfError)
173
- console.error(' Error type:', pdfError instanceof Error ? pdfError.name : typeof pdfError)
174
- console.error(' Error message:', pdfError instanceof Error ? pdfError.message : String(pdfError))
175
  return NextResponse.json({
176
  success: true,
177
  pdfGenerated: false,
 
2
  import fs from 'fs'
3
  import path from 'path'
4
  import { writeFile, mkdir, unlink, readdir, stat } from 'fs/promises'
5
+ import pdflatex from 'node-pdflatex'
6
 
7
  // Use /data for Hugging Face Spaces persistent storage, fallback to public/data for local dev
8
  const DATA_DIR = process.env.SPACE_ID
 
123
 
124
  // Auto-convert .tex files to PDF
125
  if (fileName.endsWith('.tex')) {
126
+ console.log('✨ Detected .tex file, auto-converting to PDF using node-pdflatex...')
127
  try {
128
+ console.log(`πŸ“„ LaTeX content length: ${content.length} characters`)
 
129
 
130
+ // Compile LaTeX to PDF using node-pdflatex
131
+ const pdfBuffer = await pdflatex(content)
132
+
133
+ const pdfFileName = fileName.replace('.tex', '.pdf')
134
+ const pdfFilePath = path.join(targetDir, pdfFileName)
135
+
136
+ console.log(`πŸ’Ύ PDF size: ${pdfBuffer.byteLength} bytes`)
137
+ console.log(`πŸ“ Saving PDF to: ${pdfFilePath}`)
138
+
139
+ await writeFile(pdfFilePath, pdfBuffer)
140
+ console.log(`βœ… PDF generated successfully: ${pdfFileName}`)
141
+
142
+ return NextResponse.json({
143
+ success: true,
144
+ pdfGenerated: true,
145
+ pdfFileName: pdfFileName,
146
+ message: `LaTeX file saved and converted to ${pdfFileName}`
147
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  } catch (pdfError) {
149
  console.error('❌ Error generating PDF:', pdfError)
150
+ console.error(' Error details:', pdfError instanceof Error ? pdfError.message : String(pdfError))
 
151
  return NextResponse.json({
152
  success: true,
153
  pdfGenerated: false,
app/components/Desktop.tsx CHANGED
@@ -21,6 +21,7 @@ import { AboutModal } from './AboutModal'
21
 
22
  import { FlutterRunner } from './FlutterRunner'
23
  import { QuizApp } from './QuizApp'
 
24
  import { motion, AnimatePresence } from 'framer-motion'
25
  import { SystemPowerOverlay } from './SystemPowerOverlay'
26
  import {
@@ -66,6 +67,8 @@ export function Desktop() {
66
 
67
  const [flutterCodeEditorOpen, setFlutterCodeEditorOpen] = useState(false)
68
  const [quizAppOpen, setQuizAppOpen] = useState(false)
 
 
69
 
70
  // Minimized states
71
  const [fileManagerMinimized, setFileManagerMinimized] = useState(false)
@@ -80,6 +83,7 @@ export function Desktop() {
80
 
81
  const [flutterCodeEditorMinimized, setFlutterCodeEditorMinimized] = useState(false)
82
  const [quizAppMinimized, setQuizAppMinimized] = useState(false)
 
83
 
84
  const [powerState, setPowerState] = useState<'active' | 'sleep' | 'restart' | 'shutdown'>('active')
85
  const [globalZIndex, setGlobalZIndex] = useState(1000)
@@ -106,6 +110,7 @@ export function Desktop() {
106
 
107
  setFlutterCodeEditorOpen(false)
108
  setQuizAppOpen(false)
 
109
 
110
  // Reset all minimized states
111
  setFileManagerMinimized(false)
@@ -117,6 +122,7 @@ export function Desktop() {
117
 
118
  setFlutterCodeEditorMinimized(false)
119
  setQuizAppMinimized(false)
 
120
 
121
  // Reset window z-indices
122
  setWindowZIndices({})
@@ -231,6 +237,19 @@ export function Desktop() {
231
  setQuizAppMinimized(false)
232
  }
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  const handleOpenApp = (appId: string) => {
235
  switch (appId) {
236
  case 'files':
@@ -505,6 +524,23 @@ export function Desktop() {
505
  })
506
  }
507
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  // Debug info
509
  useEffect(() => {
510
  console.log('App States:', {
@@ -689,6 +725,7 @@ export function Desktop() {
689
  onFocus={() => bringWindowToFront('fileManager')}
690
  zIndex={windowZIndices.fileManager || 1000}
691
  onOpenApp={handleOpenApp}
 
692
  />
693
  </motion.div>
694
  )}
@@ -872,6 +909,35 @@ export function Desktop() {
872
  />
873
  </motion.div>
874
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
875
  </AnimatePresence>
876
  </div>
877
  </div>
 
21
 
22
  import { FlutterRunner } from './FlutterRunner'
23
  import { QuizApp } from './QuizApp'
24
+ import { TextEditor } from './TextEditor'
25
  import { motion, AnimatePresence } from 'framer-motion'
26
  import { SystemPowerOverlay } from './SystemPowerOverlay'
27
  import {
 
67
 
68
  const [flutterCodeEditorOpen, setFlutterCodeEditorOpen] = useState(false)
69
  const [quizAppOpen, setQuizAppOpen] = useState(false)
70
+ const [textEditorOpen, setTextEditorOpen] = useState(false)
71
+ const [activeTextFile, setActiveTextFile] = useState<{content: string, fileName: string, filePath: string, passkey: string} | null>(null)
72
 
73
  // Minimized states
74
  const [fileManagerMinimized, setFileManagerMinimized] = useState(false)
 
83
 
84
  const [flutterCodeEditorMinimized, setFlutterCodeEditorMinimized] = useState(false)
85
  const [quizAppMinimized, setQuizAppMinimized] = useState(false)
86
+ const [textEditorMinimized, setTextEditorMinimized] = useState(false)
87
 
88
  const [powerState, setPowerState] = useState<'active' | 'sleep' | 'restart' | 'shutdown'>('active')
89
  const [globalZIndex, setGlobalZIndex] = useState(1000)
 
110
 
111
  setFlutterCodeEditorOpen(false)
112
  setQuizAppOpen(false)
113
+ setTextEditorOpen(false)
114
 
115
  // Reset all minimized states
116
  setFileManagerMinimized(false)
 
122
 
123
  setFlutterCodeEditorMinimized(false)
124
  setQuizAppMinimized(false)
125
+ setTextEditorMinimized(false)
126
 
127
  // Reset window z-indices
128
  setWindowZIndices({})
 
237
  setQuizAppMinimized(false)
238
  }
239
 
240
+ const openTextEditor = (fileData: {content: string, fileName: string, filePath: string, passkey: string}) => {
241
+ setActiveTextFile(fileData)
242
+ setTextEditorOpen(true)
243
+ setTextEditorMinimized(false)
244
+ bringWindowToFront('textEditor')
245
+ }
246
+
247
+ const closeTextEditor = () => {
248
+ setTextEditorOpen(false)
249
+ setTextEditorMinimized(false)
250
+ setActiveTextFile(null)
251
+ }
252
+
253
  const handleOpenApp = (appId: string) => {
254
  switch (appId) {
255
  case 'files':
 
524
  })
525
  }
526
 
527
+ if (textEditorMinimized && textEditorOpen) {
528
+ minimizedApps.push({
529
+ id: 'text-editor',
530
+ label: activeTextFile?.fileName || 'Text Editor',
531
+ icon: (
532
+ <div className="bg-gradient-to-br from-gray-700 to-gray-900 w-full h-full rounded-[22%] flex items-center justify-center shadow-lg border-[0.5px] border-white/20 relative overflow-hidden">
533
+ <div className="absolute inset-0 bg-gradient-to-b from-white/10 to-transparent opacity-30" />
534
+ <FileText size={20} weight="fill" className="text-blue-400 relative z-10 drop-shadow-md" />
535
+ </div>
536
+ ),
537
+ onRestore: () => {
538
+ setTextEditorMinimized(false)
539
+ bringWindowToFront('textEditor')
540
+ }
541
+ })
542
+ }
543
+
544
  // Debug info
545
  useEffect(() => {
546
  console.log('App States:', {
 
725
  onFocus={() => bringWindowToFront('fileManager')}
726
  zIndex={windowZIndices.fileManager || 1000}
727
  onOpenApp={handleOpenApp}
728
+ onOpenTextFile={openTextEditor}
729
  />
730
  </motion.div>
731
  )}
 
909
  />
910
  </motion.div>
911
  )}
912
+
913
+ {textEditorOpen && activeTextFile && (
914
+ <motion.div
915
+ key="text-editor"
916
+ initial={{ opacity: 0, scale: 0.95 }}
917
+ animate={{
918
+ opacity: textEditorMinimized ? 0 : 1,
919
+ scale: textEditorMinimized ? 0.9 : 1,
920
+ y: textEditorMinimized ? 100 : 0,
921
+ }}
922
+ exit={{ opacity: 0, scale: 0.95 }}
923
+ transition={{ duration: 0.2 }}
924
+ style={{
925
+ pointerEvents: textEditorMinimized ? 'none' : 'auto',
926
+ display: textEditorMinimized ? 'none' : 'block'
927
+ }}
928
+ >
929
+ <TextEditor
930
+ initialContent={activeTextFile.content}
931
+ fileName={activeTextFile.fileName}
932
+ filePath={activeTextFile.filePath}
933
+ passkey={activeTextFile.passkey}
934
+ onClose={closeTextEditor}
935
+ onMinimize={() => setTextEditorMinimized(true)}
936
+ onFocus={() => bringWindowToFront('textEditor')}
937
+ zIndex={windowZIndices.textEditor || 1000}
938
+ />
939
+ </motion.div>
940
+ )}
941
  </AnimatePresence>
942
  </div>
943
  </div>
app/components/FileManager.tsx CHANGED
@@ -51,6 +51,7 @@ interface FileManagerProps {
51
  zIndex?: number
52
  onOpenFlutterApp?: (appFile: any) => void
53
  onOpenApp?: (appId: string) => void
 
54
  }
55
 
56
  interface FileItem {
@@ -65,7 +66,7 @@ interface FileItem {
65
  pubspecYaml?: string
66
  }
67
 
68
- export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp, onMinimize, onMaximize, onFocus, zIndex, onOpenApp }: FileManagerProps) {
69
  const [files, setFiles] = useState<FileItem[]>([])
70
  const [loading, setLoading] = useState(false)
71
  const [searchQuery, setSearchQuery] = useState('')
@@ -78,6 +79,18 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
78
  const [showPasskeyModal, setShowPasskeyModal] = useState(false)
79
  const [tempPasskey, setTempPasskey] = useState('')
80
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  // Load files when path or selection changes
82
  useEffect(() => {
83
  if (currentPath === 'Applications') {
@@ -525,8 +538,8 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
525
  console.error('Error loading file:', error)
526
  if (onOpenApp) onOpenApp('flutter-editor')
527
  }
528
- } else if (file.extension === 'tex') {
529
- // Load the file content and open LaTeX Editor with it
530
  try {
531
  let fileContent = ''
532
  if (sidebarSelection === 'secure' && passkey) {
@@ -534,21 +547,26 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
534
  const data = await response.json()
535
  const fileData = data.files?.find((f: any) => f.name === file.name)
536
  fileContent = fileData?.content || ''
537
-
538
- // Store for auto-save in LaTeX Editor
539
- sessionStorage.setItem('currentPasskey', passkey)
540
- sessionStorage.setItem('currentFileName', file.name)
 
 
541
  }
542
 
543
- // Open LaTeX Editor with the file content
544
- if (onOpenApp) {
545
- // Store the content temporarily for the LaTeX Editor to pick up
546
- sessionStorage.setItem('latexFileContent', fileContent)
547
- onOpenApp('latex-editor')
 
 
 
548
  }
549
  } catch (error) {
550
  console.error('Error loading file:', error)
551
- if (onOpenApp) onOpenApp('latex-editor')
552
  }
553
  } else {
554
  handlePreview(file)
 
51
  zIndex?: number
52
  onOpenFlutterApp?: (appFile: any) => void
53
  onOpenApp?: (appId: string) => void
54
+ onOpenTextFile?: (fileData: {content: string, fileName: string, filePath: string, passkey: string}) => void
55
  }
56
 
57
  interface FileItem {
 
66
  pubspecYaml?: string
67
  }
68
 
69
+ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp, onMinimize, onMaximize, onFocus, zIndex, onOpenApp, onOpenTextFile }: FileManagerProps) {
70
  const [files, setFiles] = useState<FileItem[]>([])
71
  const [loading, setLoading] = useState(false)
72
  const [searchQuery, setSearchQuery] = useState('')
 
79
  const [showPasskeyModal, setShowPasskeyModal] = useState(false)
80
  const [tempPasskey, setTempPasskey] = useState('')
81
 
82
+ // Helper function to check if a file can be edited as text
83
+ const isTextFile = (extension: string): boolean => {
84
+ const textExtensions = [
85
+ 'txt', 'md', 'tex', 'json', 'xml', 'html', 'htm', 'css', 'scss', 'sass',
86
+ 'js', 'jsx', 'ts', 'tsx', 'py', 'java', 'c', 'cpp', 'h', 'hpp',
87
+ 'sh', 'bash', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf',
88
+ 'sql', 'php', 'rb', 'go', 'rs', 'swift', 'kt', 'scala',
89
+ 'r', 'dart', 'vue', 'svelte', 'astro', 'csv', 'log'
90
+ ]
91
+ return textExtensions.includes(extension.toLowerCase())
92
+ }
93
+
94
  // Load files when path or selection changes
95
  useEffect(() => {
96
  if (currentPath === 'Applications') {
 
538
  console.error('Error loading file:', error)
539
  if (onOpenApp) onOpenApp('flutter-editor')
540
  }
541
+ } else if (isTextFile(file.extension || '')) {
542
+ // Load the file content and open Text Editor with it
543
  try {
544
  let fileContent = ''
545
  if (sidebarSelection === 'secure' && passkey) {
 
547
  const data = await response.json()
548
  const fileData = data.files?.find((f: any) => f.name === file.name)
549
  fileContent = fileData?.content || ''
550
+ } else if (sidebarSelection === 'public') {
551
+ // Load from public folder
552
+ const response = await fetch(`/api/public?path=${encodeURIComponent(file.path)}`)
553
+ if (response.ok) {
554
+ fileContent = await response.text()
555
+ }
556
  }
557
 
558
+ // Open Text Editor with the file content
559
+ if (onOpenTextFile) {
560
+ onOpenTextFile({
561
+ content: fileContent,
562
+ fileName: file.name,
563
+ filePath: currentPath,
564
+ passkey: passkey
565
+ })
566
  }
567
  } catch (error) {
568
  console.error('Error loading file:', error)
569
+ alert('Failed to load file for editing')
570
  }
571
  } else {
572
  handlePreview(file)
app/components/TextEditor.tsx ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect } from 'react'
4
+ import Editor from '@monaco-editor/react'
5
+ import {
6
+ FloppyDisk,
7
+ X,
8
+ Minus,
9
+ Square,
10
+ Check,
11
+ FileText,
12
+ WarningCircle
13
+ } from '@phosphor-icons/react'
14
+ import Window from './Window'
15
+
16
+ interface TextEditorProps {
17
+ onClose: () => void
18
+ onMinimize?: () => void
19
+ onMaximize?: () => void
20
+ onFocus?: () => void
21
+ zIndex?: number
22
+ initialContent?: string
23
+ fileName?: string
24
+ filePath?: string
25
+ passkey?: string
26
+ }
27
+
28
+ export function TextEditor({
29
+ onClose,
30
+ onMinimize,
31
+ onMaximize,
32
+ onFocus,
33
+ zIndex,
34
+ initialContent = '',
35
+ fileName = 'untitled.txt',
36
+ filePath = '',
37
+ passkey = ''
38
+ }: TextEditorProps) {
39
+ const [code, setCode] = useState(initialContent)
40
+ const [isSaving, setIsSaving] = useState(false)
41
+ const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
42
+ const [lastSaved, setLastSaved] = useState<Date | null>(null)
43
+ const [hasChanges, setHasChanges] = useState(false)
44
+
45
+ // Detect file language from extension
46
+ const getLanguage = (filename: string) => {
47
+ const ext = filename.split('.').pop()?.toLowerCase()
48
+ switch (ext) {
49
+ case 'tex':
50
+ return 'latex'
51
+ case 'js':
52
+ case 'jsx':
53
+ return 'javascript'
54
+ case 'ts':
55
+ case 'tsx':
56
+ return 'typescript'
57
+ case 'py':
58
+ return 'python'
59
+ case 'java':
60
+ return 'java'
61
+ case 'cpp':
62
+ case 'cc':
63
+ case 'cxx':
64
+ return 'cpp'
65
+ case 'c':
66
+ case 'h':
67
+ return 'c'
68
+ case 'cs':
69
+ return 'csharp'
70
+ case 'json':
71
+ return 'json'
72
+ case 'xml':
73
+ return 'xml'
74
+ case 'html':
75
+ case 'htm':
76
+ return 'html'
77
+ case 'css':
78
+ return 'css'
79
+ case 'md':
80
+ return 'markdown'
81
+ case 'sh':
82
+ case 'bash':
83
+ return 'shell'
84
+ case 'dart':
85
+ return 'dart'
86
+ default:
87
+ return 'plaintext'
88
+ }
89
+ }
90
+
91
+ const language = getLanguage(fileName)
92
+
93
+ // Track changes
94
+ useEffect(() => {
95
+ if (code !== initialContent) {
96
+ setHasChanges(true)
97
+ setSaveStatus('idle')
98
+ }
99
+ }, [code, initialContent])
100
+
101
+ const handleSave = async () => {
102
+ if (!passkey) {
103
+ alert('Please enter your passkey first!')
104
+ return
105
+ }
106
+
107
+ setIsSaving(true)
108
+ setSaveStatus('saving')
109
+
110
+ try {
111
+ const response = await fetch('/api/data', {
112
+ method: 'POST',
113
+ headers: { 'Content-Type': 'application/json' },
114
+ body: JSON.stringify({
115
+ action: 'save_file',
116
+ passkey: passkey,
117
+ fileName: fileName,
118
+ content: code,
119
+ folder: filePath
120
+ })
121
+ })
122
+
123
+ const result = await response.json()
124
+
125
+ if (result.success) {
126
+ setSaveStatus('saved')
127
+ setLastSaved(new Date())
128
+ setHasChanges(false)
129
+
130
+ // Show PDF generation message if applicable
131
+ if (result.pdfGenerated) {
132
+ setTimeout(() => {
133
+ alert(`βœ… File saved!\n\nπŸ“„ PDF also generated: ${result.pdfFileName}`)
134
+ }, 100)
135
+ }
136
+
137
+ // Reset status after 3 seconds
138
+ setTimeout(() => {
139
+ if (saveStatus === 'saved') setSaveStatus('idle')
140
+ }, 3000)
141
+ } else {
142
+ setSaveStatus('error')
143
+ alert(`Error saving file: ${result.error || 'Unknown error'}`)
144
+ }
145
+ } catch (error) {
146
+ console.error('Save error:', error)
147
+ setSaveStatus('error')
148
+ alert('Failed to save file. Please try again.')
149
+ } finally {
150
+ setIsSaving(false)
151
+ }
152
+ }
153
+
154
+ // Keyboard shortcut: Cmd/Ctrl + S to save
155
+ useEffect(() => {
156
+ const handleKeyDown = (e: KeyboardEvent) => {
157
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
158
+ e.preventDefault()
159
+ handleSave()
160
+ }
161
+ }
162
+
163
+ window.addEventListener('keydown', handleKeyDown)
164
+ return () => window.removeEventListener('keydown', handleKeyDown)
165
+ }, [code, passkey, fileName, filePath])
166
+
167
+ return (
168
+ <Window
169
+ id="text-editor"
170
+ title={`Text Editor - ${fileName}`}
171
+ isOpen={true}
172
+ onClose={onClose}
173
+ onMinimize={onMinimize}
174
+ onMaximize={onMaximize}
175
+ onFocus={onFocus}
176
+ zIndex={zIndex}
177
+ width={1000}
178
+ height={700}
179
+ x={100}
180
+ y={80}
181
+ className="text-editor-window"
182
+ >
183
+ <div className="flex flex-col h-full bg-[#1e1e1e]">
184
+ {/* Toolbar */}
185
+ <div className="h-12 bg-[#2d2d2d] border-b border-[#1e1e1e] flex items-center justify-between px-4">
186
+ <div className="flex items-center gap-3">
187
+ <div className="flex items-center gap-2 px-3 py-1.5 bg-[#1e1e1e] rounded text-xs text-gray-400 border border-[#333]">
188
+ <FileText size={14} className="text-blue-400" />
189
+ <span>{fileName}</span>
190
+ </div>
191
+
192
+ {hasChanges && (
193
+ <span className="text-xs text-yellow-500">● Modified</span>
194
+ )}
195
+
196
+ {saveStatus === 'saved' && (
197
+ <div className="flex items-center gap-1 text-xs text-green-500">
198
+ <Check size={14} />
199
+ <span>Saved {lastSaved?.toLocaleTimeString()}</span>
200
+ </div>
201
+ )}
202
+
203
+ {saveStatus === 'error' && (
204
+ <div className="flex items-center gap-1 text-xs text-red-500">
205
+ <WarningCircle size={14} />
206
+ <span>Save failed</span>
207
+ </div>
208
+ )}
209
+ </div>
210
+
211
+ <div className="flex items-center gap-2">
212
+ <button
213
+ onClick={handleSave}
214
+ disabled={isSaving || !hasChanges}
215
+ className="flex items-center gap-2 px-4 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded text-xs font-medium transition-colors"
216
+ >
217
+ {isSaving ? (
218
+ <>
219
+ <div className="w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin" />
220
+ <span>Saving...</span>
221
+ </>
222
+ ) : (
223
+ <>
224
+ <FloppyDisk size={14} weight="fill" />
225
+ <span>Save (⌘S)</span>
226
+ </>
227
+ )}
228
+ </button>
229
+ </div>
230
+ </div>
231
+
232
+ {/* Editor */}
233
+ <div className="flex-1">
234
+ <Editor
235
+ height="100%"
236
+ language={language}
237
+ theme="vs-dark"
238
+ value={code}
239
+ onChange={(value) => setCode(value || '')}
240
+ options={{
241
+ minimap: { enabled: true },
242
+ fontSize: 14,
243
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
244
+ lineNumbers: 'on',
245
+ scrollBeyondLastLine: false,
246
+ automaticLayout: true,
247
+ padding: { top: 16, bottom: 16 },
248
+ renderLineHighlight: 'all',
249
+ smoothScrolling: true,
250
+ cursorBlinking: 'smooth',
251
+ cursorSmoothCaretAnimation: 'on',
252
+ wordWrap: 'on',
253
+ wrappingIndent: 'indent',
254
+ tabSize: 2,
255
+ insertSpaces: true
256
+ }}
257
+ />
258
+ </div>
259
+
260
+ {/* Status Bar */}
261
+ <div className="h-6 bg-[#007acc] flex items-center justify-between px-4 text-xs text-white">
262
+ <div className="flex items-center gap-4">
263
+ <span>{language.toUpperCase()}</span>
264
+ <span>|</span>
265
+ <span>{code.split('\n').length} lines</span>
266
+ <span>|</span>
267
+ <span>{code.length} characters</span>
268
+ </div>
269
+ <div>
270
+ {fileName.endsWith('.tex') && <span>πŸ“„ PDF will be auto-generated on save</span>}
271
+ </div>
272
+ </div>
273
+ </div>
274
+ </Window>
275
+ )
276
+ }
277
+
package-lock.json CHANGED
@@ -27,6 +27,7 @@
27
  "monaco-editor": "^0.54.0",
28
  "next": "16.0.1",
29
  "node-fetch": "^3.3.2",
 
30
  "officegen": "^0.6.5",
31
  "pdf-parse": "^2.4.5",
32
  "pdfkit": "^0.17.2",
@@ -10624,6 +10625,12 @@
10624
  "marked": "14.0.0"
10625
  }
10626
  },
 
 
 
 
 
 
10627
  "node_modules/motion-dom": {
10628
  "version": "12.23.23",
10629
  "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
@@ -10846,6 +10853,28 @@
10846
  "node": ">= 12"
10847
  }
10848
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10849
  "node_modules/node-releases": {
10850
  "version": "2.0.27",
10851
  "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -11134,6 +11163,15 @@
11134
  "node": ">= 0.8.0"
11135
  }
11136
  },
 
 
 
 
 
 
 
 
 
11137
  "node_modules/own-keys": {
11138
  "version": "1.0.1",
11139
  "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
 
27
  "monaco-editor": "^0.54.0",
28
  "next": "16.0.1",
29
  "node-fetch": "^3.3.2",
30
+ "node-pdflatex": "^0.3.0",
31
  "officegen": "^0.6.5",
32
  "pdf-parse": "^2.4.5",
33
  "pdfkit": "^0.17.2",
 
10625
  "marked": "14.0.0"
10626
  }
10627
  },
10628
+ "node_modules/monolite": {
10629
+ "version": "0.4.6",
10630
+ "resolved": "https://registry.npmjs.org/monolite/-/monolite-0.4.6.tgz",
10631
+ "integrity": "sha512-iVm3qzFvDKxF3B3bFTZG5Knu6knL83wCmXBhFT/bOKTT1JU5LAk5Fq+4R/abYA6mYmDNmlXlq1CIeSC/UQ9bOg==",
10632
+ "license": "MIT"
10633
+ },
10634
  "node_modules/motion-dom": {
10635
  "version": "12.23.23",
10636
  "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
 
10853
  "node": ">= 12"
10854
  }
10855
  },
10856
+ "node_modules/node-pdflatex": {
10857
+ "version": "0.3.0",
10858
+ "resolved": "https://registry.npmjs.org/node-pdflatex/-/node-pdflatex-0.3.0.tgz",
10859
+ "integrity": "sha512-hVMAmFpETyZbwdo3LPthqliu73I3O6tblQggljwdsQQjyByI6Cv8t4ytDEI7ZnfhgKMvPvDXaCo9yxHkQ0j7CQ==",
10860
+ "license": "MIT",
10861
+ "dependencies": {
10862
+ "monolite": "^0.4.5",
10863
+ "tmp": "0.0.33"
10864
+ }
10865
+ },
10866
+ "node_modules/node-pdflatex/node_modules/tmp": {
10867
+ "version": "0.0.33",
10868
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
10869
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
10870
+ "license": "MIT",
10871
+ "dependencies": {
10872
+ "os-tmpdir": "~1.0.2"
10873
+ },
10874
+ "engines": {
10875
+ "node": ">=0.6.0"
10876
+ }
10877
+ },
10878
  "node_modules/node-releases": {
10879
  "version": "2.0.27",
10880
  "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
 
11163
  "node": ">= 0.8.0"
11164
  }
11165
  },
11166
+ "node_modules/os-tmpdir": {
11167
+ "version": "1.0.2",
11168
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
11169
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
11170
+ "license": "MIT",
11171
+ "engines": {
11172
+ "node": ">=0.10.0"
11173
+ }
11174
+ },
11175
  "node_modules/own-keys": {
11176
  "version": "1.0.1",
11177
  "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
package.json CHANGED
@@ -31,6 +31,7 @@
31
  "monaco-editor": "^0.54.0",
32
  "next": "16.0.1",
33
  "node-fetch": "^3.3.2",
 
34
  "officegen": "^0.6.5",
35
  "pdf-parse": "^2.4.5",
36
  "pdfkit": "^0.17.2",
 
31
  "monaco-editor": "^0.54.0",
32
  "next": "16.0.1",
33
  "node-fetch": "^3.3.2",
34
+ "node-pdflatex": "^0.3.0",
35
  "officegen": "^0.6.5",
36
  "pdf-parse": "^2.4.5",
37
  "pdfkit": "^0.17.2",