Spaces:
Running
Running
Implement TextEditor with LaTeX to PDF conversion using node-pdflatex
Browse files- app/api/data/route.ts +21 -45
- app/components/Desktop.tsx +66 -0
- app/components/FileManager.tsx +31 -13
- app/components/TextEditor.tsx +277 -0
- package-lock.json +38 -0
- package.json +1 -0
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(
|
| 128 |
-
console.log(` Content length: ${content.length} characters`)
|
| 129 |
|
| 130 |
-
// Compile LaTeX to PDF using
|
| 131 |
-
const
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 529 |
-
// Load the file content and open
|
| 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 |
-
//
|
| 539 |
-
|
| 540 |
-
|
|
|
|
|
|
|
| 541 |
}
|
| 542 |
|
| 543 |
-
// Open
|
| 544 |
-
if (
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
|
|
|
|
|
|
|
|
|
| 548 |
}
|
| 549 |
} catch (error) {
|
| 550 |
console.error('Error loading file:', error)
|
| 551 |
-
|
| 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",
|