SafeSight's picture
socket efficiency
ed88c5d
Raw
History Blame Contribute Delete
11 kB
import { useMemo, useCallback, useRef, useEffect, useState } from 'react';
import { useEditorStore } from '../../store/editorStore';
import { useProjectStore } from '../../store/projectStore';
import { useCollaborationStore, Collaborator } from '../../store/collaborationStore';
import { useProjectSocket } from '../../hooks/useWebSocket';
import { useParams } from 'react-router-dom';
import { getBlocksForFramework } from '../../blocks/registry';
import { compileWeb } from '../../compilers/web';
import { compileElectron } from '../../compilers/electron';
import { compileMaui } from '../../compilers/maui';
import { compileNodeJS } from '../../compilers/nodejs';
import { SyntaxHighlight } from './SyntaxHighlight';
import {
Download, Copy, FileCode,
Eye, Code2, SplitSquareHorizontal
} from 'lucide-react';
import { saveAs } from 'file-saver';
import JSZip from 'jszip';
import { getColor } from '../Collaboration/CollaboratorAvatars';
export default function CodeView() {
const { id } = useParams<{ id: string }>();
const { currentProject } = useProjectStore();
const { activeFileId, fileTree, viewMode, setViewMode, visualElements, activeFileContent, setActiveFileContent } = useEditorStore();
const collaborators = useCollaborationStore((s) => s.collaborators);
const { emitCursorMove, emitFileChanged } = useProjectSocket(id);
const editorRef = useRef<HTMLDivElement>(null);
const [isEditing, setIsEditing] = useState(false);
const compiledCode = useMemo(() => {
if (!currentProject) return '';
if (isEditing) return activeFileContent;
const activeFile = fileTree.find(f => f.id === activeFileId);
const blocks = getBlocksForFramework(currentProject.framework);
const compilerOptions = {
blocks, fileTree, visualElements, activeFile,
projectName: currentProject.name,
};
switch (currentProject.framework) {
case 'web': return compileWeb(compilerOptions);
case 'electron': return compileElectron(compilerOptions);
case 'maui': return compileMaui(compilerOptions);
case 'nodejs': return compileNodeJS(compilerOptions);
default: return '// Select a framework to generate code';
}
}, [currentProject, activeFileId, fileTree, visualElements, isEditing, activeFileContent]);
const collaboratorCursors = useMemo(() => {
if (!activeFileId) return [];
return collaborators.filter((c) => c.activeFileId === activeFileId && c.cursor && c.cursor.line != null);
}, [collaborators, activeFileId]);
const lines = useMemo(() => compiledCode.split('\n'), [compiledCode]);
const handleEditorClick = useCallback((e: React.MouseEvent) => {
const rect = editorRef.current?.getBoundingClientRect();
if (!rect) return;
const lineHeight = 20;
const line = Math.floor((e.clientY - rect.top + editorRef.current!.scrollTop) / lineHeight);
const ch = Math.min(0, 0);
if (activeFileId) {
emitCursorMove(activeFileId, { line: Math.max(0, line), ch });
}
}, [activeFileId, emitCursorMove]);
const fileChangeTimer = useRef<number>();
const cursorThrottle = useRef<number>();
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const content = e.target.value;
setActiveFileContent(content);
// Debounce file_changed to 300ms to avoid flooding when typing
if (activeFileId) {
clearTimeout(fileChangeTimer.current);
fileChangeTimer.current = window.setTimeout(() => {
emitFileChanged(activeFileId, content);
}, 300);
}
// Track cursor position
const textarea = e.target;
const pos = textarea.selectionStart;
const before = content.substring(0, pos);
const line = before.split('\n').length - 1;
const ch = before.length - before.lastIndexOf('\n') - 1;
clearTimeout(cursorThrottle.current);
cursorThrottle.current = window.setTimeout(() => {
if (activeFileId) emitCursorMove(activeFileId, { line, ch });
}, 100);
}, [activeFileId, emitCursorMove, emitFileChanged, setActiveFileContent]);
const handleDownload = useCallback(async () => {
if (!currentProject) return;
const zip = new JSZip();
const addFilesToZip = (files: any[], currentPath: string = '') => {
for (const file of files) {
if (file.type === 'folder') {
if (file.children) addFilesToZip(file.children, currentPath + file.name + '/');
} else {
zip.file(currentPath + file.name, file.content || '');
}
}
};
addFilesToZip(fileTree);
const blob = await zip.generateAsync({ type: 'blob' });
saveAs(blob, `${currentProject.name}.zip`);
}, [currentProject, fileTree]);
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(compiledCode);
}, [compiledCode]);
const scriptFileName = useMemo(() => {
const scriptFile = fileTree.find(f => f.type === 'js' || f.type === 'typescript');
return scriptFile?.name || fileTree.find(f => f.id === activeFileId)?.name || 'output';
}, [fileTree, activeFileId]);
return (
<div className="flex flex-col h-full">
{/* Code Toolbar */}
<div className="h-10 glass border-b border-surface-700 flex items-center justify-between px-4">
<div className="flex items-center gap-2">
<FileCode className="w-4 h-4 text-primary-400" />
<span className="text-sm font-medium text-white">{scriptFileName}</span>
<span className="badge-web">{currentProject?.framework || 'web'}</span>
</div>
<div className="flex items-center gap-2">
{!isEditing && (
<div className="flex bg-surface-800 rounded-lg p-0.5">
<button onClick={() => setViewMode('design')}
className={`px-2 py-1 rounded-md text-xs ${viewMode === 'design' ? 'bg-primary-600 text-white' : 'text-surface-400'}`}>
<Code2 className="w-3.5 h-3.5" />
</button>
<button onClick={() => setViewMode('split')}
className={`px-2 py-1 rounded-md text-xs ${viewMode === 'split' ? 'bg-primary-600 text-white' : 'text-surface-400'}`}>
<SplitSquareHorizontal className="w-3.5 h-3.5" />
</button>
</div>
)}
<button onClick={() => setIsEditing(!isEditing)}
className={`btn-ghost px-2 py-1 text-xs ${isEditing ? 'text-primary-400 bg-primary-500/10' : ''}`}
title={isEditing ? 'View compiled code' : 'Edit source directly'}>
<Code2 className="w-3.5 h-3.5" />
{isEditing ? 'Compiled' : 'Edit'}
</button>
<div className="w-px h-4 bg-surface-700" />
<button onClick={handleCopy} className="btn-ghost p-1.5" title="Copy to clipboard">
<Copy className="w-3.5 h-3.5" />
</button>
<button onClick={handleDownload} className="btn-ghost p-1.5" title="Download project as ZIP">
<Download className="w-3.5 h-3.5" />
</button>
</div>
</div>
{/* Code Content */}
<div className="flex-1 overflow-auto">
{isEditing ? (
<div className="relative h-full">
<textarea
value={activeFileContent}
onChange={handleTextChange}
className="w-full h-full bg-transparent text-transparent caret-white resize-none font-mono text-sm leading-5 p-4 absolute inset-0 z-10 outline-none"
spellCheck={false}
/>
<div className="pointer-events-none p-4">
<SyntaxHighlight code={activeFileContent} language={getLanguage(currentProject?.framework || 'web', fileTree.find(f => f.id === activeFileId)?.type)} />
</div>
{collaboratorCursors.length > 0 && (
<div className="absolute inset-0 pointer-events-none z-20">
{collaboratorCursors.map((c) => (
<div
key={c.userId}
className="absolute left-0 w-0.5 h-5"
style={{
top: `${c.cursor!.line * 20 + 16}px`,
backgroundColor: getColor(c.userId),
}}
>
<span
className="absolute left-0.5 -top-4 text-[9px] px-1 py-0.5 rounded-r whitespace-nowrap text-white font-bold"
style={{ backgroundColor: getColor(c.userId) }}
>
{c.username}
</span>
</div>
))}
</div>
)}
</div>
) : (
<div ref={editorRef} onClick={handleEditorClick} className="relative">
{compiledCode ? (
<>
<SyntaxHighlight code={compiledCode} language={getLanguage(currentProject?.framework || 'web', fileTree.find(f => f.id === activeFileId)?.type)} />
{collaboratorCursors.length > 0 && (
<div className="absolute inset-0 pointer-events-none top-0 left-0">
{collaboratorCursors.map((c) => (
<div
key={c.userId}
className="absolute left-0 w-0.5 h-5"
style={{
top: `${c.cursor!.line * 20 + 16}px`,
backgroundColor: getColor(c.userId),
}}
>
<span
className="absolute left-0.5 -top-4 text-[9px] px-1 py-0.5 rounded-r whitespace-nowrap text-white font-bold"
style={{ backgroundColor: getColor(c.userId) }}
>
{c.username}
</span>
</div>
))}
</div>
)}
</>
) : (
<div className="flex items-center justify-center h-full text-surface-500 text-sm">
<div className="text-center">
<Code2 className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p>Add blocks to see generated code</p>
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}
function getLanguage(framework: string, fileType?: string): string {
if (fileType) {
if (fileType === 'js' || fileType === 'javascript') return 'javascript';
if (fileType === 'ts' || fileType === 'typescript') return 'typescript';
if (fileType === 'html' || fileType === 'xaml') return 'xml';
if (fileType === 'csharp' || fileType === 'cs') return 'csharp';
if (fileType === 'css') return 'css';
if (fileType === 'json') return 'json';
}
switch (framework) {
case 'web': return 'javascript';
case 'electron': return 'typescript';
case 'maui': return 'xml';
case 'nodejs': return 'javascript';
default: return 'javascript';
}
}