|
|
<!DOCTYPE html> |
|
|
<html lang="es"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>DeepSeek IDE - Live Code Editor</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css"> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/dracula.min.css"> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script> |
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/python/python.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/htmlmixed/htmlmixed.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/css/css.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/xml/xml.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/php/php.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/ruby/ruby.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/shell/shell.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/sql/sql.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/swift/swift.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/go/go.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/rust/rust.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/addon/edit/matchbrackets.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/addon/edit/closebrackets.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/addon/comment/comment.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/addon/display/placeholder.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsdiff/5.0.0/diff.min.js"></script> |
|
|
<style> |
|
|
.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255, 255, 255, 0.1) !important; } |
|
|
.diff-added { background-color: rgba(40, 167, 69, 0.2); } |
|
|
.diff-removed { background-color: rgba(220, 53, 69, 0.2); } |
|
|
.diff-unchanged { opacity: 0.7; } |
|
|
.CodeMirror { height: 100% !important; font-size: 14px; } |
|
|
.resize-handle { width: 4px; background: rgba(156, 163, 175, 0.3); cursor: col-resize; } |
|
|
.resize-handle:hover { background: rgba(156, 163, 175, 0.6); } |
|
|
#output-container { scrollbar-width: thin; } |
|
|
#output-container::-webkit-scrollbar { width: 6px; } |
|
|
#output-container::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); } |
|
|
#output-container::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); } |
|
|
.tab-active { border-bottom: 2px solid #3b82f6; color: white !important; } |
|
|
.CodeMirror pre { padding: 0 8px; } |
|
|
.file-tab { position: relative; } |
|
|
.file-tab.active { background-color: rgba(59, 130, 246, 0.2); } |
|
|
.file-tab-close { visibility: hidden; } |
|
|
.file-tab:hover .file-tab-close { visibility: visible; } |
|
|
.history-item:hover { background-color: rgba(55, 65, 81, 0.5); } |
|
|
.prompt-container { transition: all 0.3s ease; } |
|
|
.prompt-container.collapsed { height: 40px; overflow: hidden; } |
|
|
.prompt-toggle { transform: rotate(0deg); transition: transform 0.3s ease; } |
|
|
.prompt-toggle.collapsed { transform: rotate(180deg); } |
|
|
.apply-diff-btn { transition: all 0.2s ease; } |
|
|
.apply-diff-btn:hover { transform: scale(1.05); } |
|
|
.code-block { position: relative; } |
|
|
.code-block-copy { position: absolute; top: 0.5rem; right: 0.5rem; opacity: 0; transition: opacity 0.2s ease; } |
|
|
.code-block:hover .code-block-copy { opacity: 1; } |
|
|
.ai-cursor { position: absolute; background-color: rgba(59, 130, 246, 0.3); border-left: 2px solid #3b82f6; } |
|
|
.ai-cursor-label { position: absolute; background-color: #3b82f6; color: white; font-size: 0.75rem; padding: 0.1rem 0.3rem; border-radius: 0.25rem; top: -1.25rem; left: -2px; white-space: nowrap; } |
|
|
.typewriter-cursor { position: absolute; width: 2px; background-color: #3b82f6; animation: blink 1s infinite; } |
|
|
@keyframes blink { |
|
|
0%, 100% { opacity: 1; } |
|
|
50% { opacity: 0; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-900 text-gray-100 h-screen flex flex-col overflow-hidden"> |
|
|
|
|
|
<header class="bg-gray-800 px-4 py-3 flex items-center justify-between border-b border-gray-700"> |
|
|
<div class="flex items-center space-x-2"> |
|
|
<i class="fas fa-code text-blue-400 text-xl"></i> |
|
|
<h1 class="text-xl font-bold">DeepSeek IDE</h1> |
|
|
</div> |
|
|
<div class="flex items-center space-x-4"> |
|
|
<div class="relative"> |
|
|
<input type="password" id="api-key" placeholder="DeepSeek API Key" |
|
|
class="bg-gray-700 px-3 py-1 rounded text-sm w-64 focus:outline-none focus:ring-1 focus:ring-blue-500"> |
|
|
<button id="save-key-btn" class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-blue-400"> |
|
|
<i class="fas fa-save"></i> |
|
|
</button> |
|
|
</div> |
|
|
<button id="new-file-btn" class="text-gray-400 hover:text-blue-400 px-2 py-1 rounded text-sm flex items-center"> |
|
|
<i class="fas fa-plus mr-1"></i> |
|
|
<span>New</span> |
|
|
</button> |
|
|
<button id="analyze-btn" class="bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded text-sm flex items-center space-x-1"> |
|
|
<i class="fas fa-play"></i> |
|
|
<span>Analyze</span> |
|
|
</button> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
|
|
|
<div id="prompt-container" class="prompt-container bg-gray-800 border-b border-gray-700 px-4 py-2"> |
|
|
<div class="flex justify-between items-center cursor-pointer" id="prompt-toggle"> |
|
|
<h3 class="text-sm font-medium flex items-center"> |
|
|
<i class="fas fa-comment-dots mr-2 text-blue-400"></i> |
|
|
Custom Prompt |
|
|
</h3> |
|
|
<i class="fas fa-chevron-up prompt-toggle text-gray-400"></i> |
|
|
</div> |
|
|
<div class="mt-2"> |
|
|
<textarea id="custom-prompt" placeholder="Describe what changes you want to make to the code (e.g. 'Optimize this function', 'Fix bugs', 'Explain how it works')..." |
|
|
class="w-full bg-gray-700 rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"></textarea> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="flex flex-1 overflow-hidden"> |
|
|
|
|
|
<div class="flex-1 flex flex-col border-r border-gray-700 relative"> |
|
|
<div class="bg-gray-800 px-3 py-2 flex items-center justify-between"> |
|
|
<div class="flex items-center space-x-2 overflow-x-auto"> |
|
|
|
|
|
</div> |
|
|
<div class="flex items-center space-x-2"> |
|
|
<select id="language-selector" class="bg-gray-700 text-sm rounded px-2 py-1"> |
|
|
<option value="javascript">JavaScript</option> |
|
|
<option value="python">Python</option> |
|
|
<option value="htmlmixed">HTML</option> |
|
|
<option value="css">CSS</option> |
|
|
<option value="text/x-java">Java</option> |
|
|
<option value="text/x-csrc">C</option> |
|
|
<option value="text/x-c++src">C++</option> |
|
|
<option value="text/x-csharp">C#</option> |
|
|
<option value="text/x-go">Go</option> |
|
|
<option value="text/x-rustsrc">Rust</option> |
|
|
<option value="text/x-swift">Swift</option> |
|
|
<option value="text/x-php">PHP</option> |
|
|
<option value="text/x-ruby">Ruby</option> |
|
|
<option value="text/x-sql">SQL</option> |
|
|
<option value="text/x-sh">Shell</option> |
|
|
</select> |
|
|
<button id="format-btn" class="text-gray-400 hover:text-blue-400 text-sm"> |
|
|
<i class="fas fa-align-left mr-1"></i>Format |
|
|
</button> |
|
|
<button id="clear-btn" class="text-gray-400 hover:text-red-400 text-sm"> |
|
|
<i class="fas fa-trash-alt mr-1"></i>Clear |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
<div id="editor" class="flex-1"></div> |
|
|
<div id="ai-cursor-container"></div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="resize-handle"></div> |
|
|
|
|
|
|
|
|
<div class="w-1/3 flex flex-col bg-gray-800"> |
|
|
<div class="bg-gray-800 px-3 py-2 flex border-b border-gray-700"> |
|
|
<button data-tab="diffs" class="tab-active px-3 py-1 text-sm font-medium"> |
|
|
<i class="fas fa-code-compare mr-1"></i>Diffs |
|
|
</button> |
|
|
<button data-tab="response" class="px-3 py-1 text-sm font-medium text-gray-400 hover:text-white"> |
|
|
<i class="fas fa-robot mr-1"></i>AI Response |
|
|
</button> |
|
|
<button data-tab="history" class="px-3 py-1 text-sm font-medium text-gray-400 hover:text-white"> |
|
|
<i class="fas fa-history mr-1"></i>History |
|
|
</button> |
|
|
<button data-tab="console" class="px-3 py-1 text-sm font-medium text-gray-400 hover:text-white"> |
|
|
<i class="fas fa-terminal mr-1"></i>Console |
|
|
</button> |
|
|
</div> |
|
|
<div id="output-container" class="flex-1 overflow-auto p-4"> |
|
|
<div id="diff-container" class="tab-content active"> |
|
|
<div class="text-center text-gray-500 py-8"> |
|
|
<i class="fas fa-code-compare text-3xl mb-2"></i> |
|
|
<p>Make changes to your code to see the differences here</p> |
|
|
</div> |
|
|
</div> |
|
|
<div id="response-container" class="tab-content hidden"> |
|
|
<div class="text-center text-gray-500 py-8"> |
|
|
<i class="fas fa-robot text-3xl mb-2"></i> |
|
|
<p>Click "Analyze" to get AI feedback on your code</p> |
|
|
</div> |
|
|
</div> |
|
|
<div id="history-container" class="tab-content hidden"> |
|
|
<div class="text-center text-gray-500 py-8"> |
|
|
<i class="fas fa-history text-3xl mb-2"></i> |
|
|
<p>Your analysis history will appear here</p> |
|
|
</div> |
|
|
</div> |
|
|
<div id="console-output" class="tab-content hidden"> |
|
|
<div class="text-center text-gray-500 py-8"> |
|
|
<i class="fas fa-terminal text-3xl mb-2"></i> |
|
|
<p>Console output will appear here</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
// File system and state management |
|
|
const state = { |
|
|
files: { |
|
|
'script.js': { |
|
|
content: `// Ejemplo de función en JavaScript |
|
|
function saludar(nombre) { |
|
|
return \`Hola, \${nombre}!\`; |
|
|
} |
|
|
|
|
|
console.log(saludar("Mundo"));`, |
|
|
language: 'javascript', |
|
|
lastContent: '' |
|
|
} |
|
|
}, |
|
|
currentFile: 'script.js', |
|
|
history: [], |
|
|
apiKey: localStorage.getItem('deepseek-api-key') || '', |
|
|
promptCollapsed: localStorage.getItem('prompt-collapsed') === 'true', |
|
|
proposedChanges: null, |
|
|
isApplyingChanges: false, |
|
|
typewriterInterval: null |
|
|
}; |
|
|
|
|
|
// Language mode mapping |
|
|
const languageModes = { |
|
|
'javascript': 'javascript', |
|
|
'python': 'python', |
|
|
'html': 'htmlmixed', |
|
|
'css': 'css', |
|
|
'java': 'text/x-java', |
|
|
'c': 'text/x-csrc', |
|
|
'cpp': 'text/x-c++src', |
|
|
'csharp': 'text/x-csharp', |
|
|
'go': 'text/x-go', |
|
|
'rust': 'text/x-rustsrc', |
|
|
'swift': 'text/x-swift', |
|
|
'php': 'text/x-php', |
|
|
'ruby': 'text/x-ruby', |
|
|
'sql': 'text/x-sql', |
|
|
'sh': 'text/x-sh', |
|
|
'htmlmixed': 'htmlmixed' |
|
|
}; |
|
|
|
|
|
// Initialize CodeMirror editor |
|
|
const editor = CodeMirror(document.getElementById('editor'), { |
|
|
mode: 'javascript', |
|
|
theme: 'dracula', |
|
|
lineNumbers: true, |
|
|
indentUnit: 4, |
|
|
tabSize: 4, |
|
|
lineWrapping: true, |
|
|
autoCloseBrackets: true, |
|
|
matchBrackets: true, |
|
|
extraKeys: { |
|
|
'Ctrl-Enter': analyzeCode, |
|
|
'Cmd-Enter': analyzeCode, |
|
|
'Ctrl-/': 'toggleComment', |
|
|
'Cmd-/': 'toggleComment', |
|
|
'Shift-Ctrl-F': formatCode, |
|
|
'Shift-Cmd-F': formatCode |
|
|
}, |
|
|
placeholder: '// Escribe tu código aquí...\n// Presiona Ctrl+Enter o Cmd+Enter para analizar' |
|
|
}); |
|
|
|
|
|
// Initialize the UI |
|
|
function initUI() { |
|
|
// Load API key if exists |
|
|
document.getElementById('api-key').value = state.apiKey; |
|
|
|
|
|
// Set up file tabs |
|
|
renderFileTabs(); |
|
|
loadFile(state.currentFile); |
|
|
|
|
|
// Set up language selector |
|
|
document.getElementById('language-selector').value = state.files[state.currentFile].language; |
|
|
|
|
|
// Set up initial diff |
|
|
updateDiff(); |
|
|
|
|
|
// Set up prompt toggle |
|
|
setupPromptToggle(); |
|
|
|
|
|
// Load custom prompt if exists |
|
|
const savedPrompt = localStorage.getItem('custom-prompt'); |
|
|
if (savedPrompt) { |
|
|
document.getElementById('custom-prompt').value = savedPrompt; |
|
|
} |
|
|
} |
|
|
|
|
|
// File management functions |
|
|
function renderFileTabs() { |
|
|
const tabsContainer = document.querySelector('.bg-gray-800 > div:first-child'); |
|
|
tabsContainer.innerHTML = ''; |
|
|
|
|
|
Object.keys(state.files).forEach(filename => { |
|
|
const isActive = filename === state.currentFile; |
|
|
const tab = document.createElement('div'); |
|
|
tab.className = `file-tab flex items-center px-3 py-1 rounded-t text-sm cursor-pointer mr-1 ${isActive ? 'active' : ''}`; |
|
|
tab.innerHTML = ` |
|
|
<span>${filename}</span> |
|
|
<button class="file-tab-close ml-2 text-gray-400 hover:text-red-400" data-file="${filename}"> |
|
|
<i class="fas fa-times text-xs"></i> |
|
|
</button> |
|
|
`; |
|
|
|
|
|
tab.addEventListener('click', () => switchFile(filename)); |
|
|
|
|
|
const closeBtn = tab.querySelector('.file-tab-close'); |
|
|
closeBtn.addEventListener('click', (e) => { |
|
|
e.stopPropagation(); |
|
|
closeFile(filename); |
|
|
}); |
|
|
|
|
|
tabsContainer.appendChild(tab); |
|
|
}); |
|
|
} |
|
|
|
|
|
function switchFile(filename) { |
|
|
if (filename === state.currentFile) return; |
|
|
|
|
|
// Save current file content |
|
|
state.files[state.currentFile].content = editor.getValue(); |
|
|
|
|
|
// Switch to new file |
|
|
state.currentFile = filename; |
|
|
loadFile(filename); |
|
|
|
|
|
// Update UI |
|
|
renderFileTabs(); |
|
|
document.getElementById('language-selector').value = state.files[filename].language; |
|
|
} |
|
|
|
|
|
function loadFile(filename) { |
|
|
const file = state.files[filename]; |
|
|
editor.setValue(file.content); |
|
|
|
|
|
// Set the editor mode based on file extension |
|
|
const mode = languageModes[file.language] || 'javascript'; |
|
|
editor.setOption('mode', mode); |
|
|
|
|
|
// Store the original content for diff |
|
|
file.lastContent = file.content; |
|
|
} |
|
|
|
|
|
function createNewFile() { |
|
|
const filename = prompt('Enter new file name (include extension):', 'newfile.js'); |
|
|
if (!filename) return; |
|
|
|
|
|
// Determine language from extension |
|
|
const extension = filename.split('.').pop().toLowerCase(); |
|
|
let language = 'javascript'; |
|
|
|
|
|
if (extension === 'py') language = 'python'; |
|
|
else if (extension === 'html') language = 'htmlmixed'; |
|
|
else if (extension === 'css') language = 'css'; |
|
|
else if (extension === 'java') language = 'text/x-java'; |
|
|
else if (extension === 'c') language = 'text/x-csrc'; |
|
|
else if (extension === 'cpp' || extension === 'c++') language = 'text/x-c++src'; |
|
|
else if (extension === 'cs') language = 'text/x-csharp'; |
|
|
else if (extension === 'go') language = 'text/x-go'; |
|
|
else if (extension === 'rs') language = 'text/x-rustsrc'; |
|
|
else if (extension === 'swift') language = 'text/x-swift'; |
|
|
else if (extension === 'php') language = 'text/x-php'; |
|
|
else if (extension === 'rb') language = 'text/x-ruby'; |
|
|
else if (extension === 'sql') language = 'text/x-sql'; |
|
|
else if (extension === 'sh') language = 'text/x-sh'; |
|
|
|
|
|
state.files[filename] = { |
|
|
content: '', |
|
|
language: language, |
|
|
lastContent: '' |
|
|
}; |
|
|
|
|
|
state.currentFile = filename; |
|
|
renderFileTabs(); |
|
|
loadFile(filename); |
|
|
document.getElementById('language-selector').value = language; |
|
|
} |
|
|
|
|
|
function closeFile(filename) { |
|
|
if (Object.keys(state.files).length <= 1) { |
|
|
alert('You must have at least one file open'); |
|
|
return; |
|
|
} |
|
|
|
|
|
delete state.files[filename]; |
|
|
|
|
|
// Switch to another file |
|
|
const remainingFiles = Object.keys(state.files); |
|
|
state.currentFile = remainingFiles[0]; |
|
|
|
|
|
renderFileTabs(); |
|
|
loadFile(state.currentFile); |
|
|
} |
|
|
|
|
|
// Language selector |
|
|
document.getElementById('language-selector').addEventListener('change', (e) => { |
|
|
const language = e.target.value; |
|
|
state.files[state.currentFile].language = language; |
|
|
|
|
|
// Set the editor mode |
|
|
const mode = languageModes[language] || 'javascript'; |
|
|
editor.setOption('mode', mode); |
|
|
}); |
|
|
|
|
|
// Prompt toggle functionality |
|
|
function setupPromptToggle() { |
|
|
const promptContainer = document.getElementById('prompt-container'); |
|
|
const promptToggle = document.getElementById('prompt-toggle'); |
|
|
const toggleIcon = promptToggle.querySelector('.prompt-toggle'); |
|
|
|
|
|
if (state.promptCollapsed) { |
|
|
promptContainer.classList.add('collapsed'); |
|
|
toggleIcon.classList.add('collapsed'); |
|
|
} |
|
|
|
|
|
promptToggle.addEventListener('click', () => { |
|
|
promptContainer.classList.toggle('collapsed'); |
|
|
toggleIcon.classList.toggle('collapsed'); |
|
|
state.promptCollapsed = !state.promptCollapsed; |
|
|
localStorage.setItem('prompt-collapsed', state.promptCollapsed); |
|
|
}); |
|
|
} |
|
|
|
|
|
// Tab switching functionality |
|
|
document.querySelectorAll('[data-tab]').forEach(tab => { |
|
|
tab.addEventListener('click', () => { |
|
|
// Update active tab |
|
|
document.querySelectorAll('[data-tab]').forEach(t => { |
|
|
t.classList.remove('tab-active'); |
|
|
t.classList.add('text-gray-400'); |
|
|
}); |
|
|
tab.classList.add('tab-active'); |
|
|
tab.classList.remove('text-gray-400'); |
|
|
|
|
|
// Show corresponding content |
|
|
const tabId = tab.getAttribute('data-tab'); |
|
|
document.querySelectorAll('.tab-content').forEach(content => { |
|
|
content.classList.add('hidden'); |
|
|
content.classList.remove('active'); |
|
|
}); |
|
|
|
|
|
const contentId = `${tabId}-container`; |
|
|
document.getElementById(contentId).classList.remove('hidden'); |
|
|
document.getElementById(contentId).classList.add('active'); |
|
|
}); |
|
|
}); |
|
|
|
|
|
// Button functionality |
|
|
document.getElementById('clear-btn').addEventListener('click', () => { |
|
|
if (confirm('¿Estás seguro de que quieres borrar todo el código?')) { |
|
|
editor.setValue(''); |
|
|
state.files[state.currentFile].content = ''; |
|
|
updateDiff(); |
|
|
} |
|
|
}); |
|
|
|
|
|
document.getElementById('format-btn').addEventListener('click', formatCode); |
|
|
document.getElementById('new-file-btn').addEventListener('click', createNewFile); |
|
|
document.getElementById('analyze-btn').addEventListener('click', analyzeCode); |
|
|
document.getElementById('save-key-btn').addEventListener('click', () => { |
|
|
const apiKey = document.getElementById('api-key').value; |
|
|
state.apiKey = apiKey; |
|
|
localStorage.setItem('deepseek-api-key', apiKey); |
|
|
showToast('API Key guardada localmente'); |
|
|
}); |
|
|
|
|
|
// Save custom prompt when changed |
|
|
document.getElementById('custom-prompt').addEventListener('change', (e) => { |
|
|
localStorage.setItem('custom-prompt', e.target.value); |
|
|
}); |
|
|
|
|
|
// Resize functionality |
|
|
const resizeHandle = document.querySelector('.resize-handle'); |
|
|
const editorPanel = document.querySelector('.flex-1.flex-col'); |
|
|
const outputPanel = document.querySelector('.w-1\\/3'); |
|
|
|
|
|
let isResizing = false; |
|
|
|
|
|
resizeHandle.addEventListener('mousedown', (e) => { |
|
|
isResizing = true; |
|
|
document.body.style.cursor = 'col-resize'; |
|
|
e.preventDefault(); |
|
|
}); |
|
|
|
|
|
document.addEventListener('mousemove', (e) => { |
|
|
if (!isResizing) return; |
|
|
|
|
|
const containerWidth = document.querySelector('.flex-1.overflow-hidden').clientWidth; |
|
|
const newEditorWidth = e.clientX; |
|
|
const newOutputWidth = containerWidth - e.clientX - 4; // 4px for the resize handle |
|
|
|
|
|
editorPanel.style.width = `${newEditorWidth}px`; |
|
|
outputPanel.style.width = `${newOutputWidth}px`; |
|
|
|
|
|
editor.refresh(); |
|
|
}); |
|
|
|
|
|
document.addEventListener('mouseup', () => { |
|
|
isResizing = false; |
|
|
document.body.style.cursor = ''; |
|
|
}); |
|
|
|
|
|
// Code formatting |
|
|
function formatCode() { |
|
|
const code = editor.getValue(); |
|
|
const language = state.files[state.currentFile].language; |
|
|
|
|
|
// Basic formatting for different languages |
|
|
let formattedCode = code; |
|
|
|
|
|
if (language === 'javascript' || language === 'text/x-java' || language === 'text/x-csharp') { |
|
|
formattedCode = formatCurlyBraceLanguage(code); |
|
|
} else if (language === 'python') { |
|
|
formattedCode = formatPython(code); |
|
|
} else if (language === 'htmlmixed') { |
|
|
formattedCode = formatHtml(code); |
|
|
} else if (language === 'css') { |
|
|
formattedCode = formatCss(code); |
|
|
} |
|
|
|
|
|
editor.setValue(formattedCode); |
|
|
showToast('Formato básico aplicado'); |
|
|
} |
|
|
|
|
|
function formatCurlyBraceLanguage(code) { |
|
|
const lines = code.split('\n'); |
|
|
let formattedCode = ''; |
|
|
let indentLevel = 0; |
|
|
|
|
|
for (const line of lines) { |
|
|
const trimmedLine = line.trim(); |
|
|
|
|
|
// Decrease indent if line closes a block |
|
|
if (trimmedLine.endsWith('}') || trimmedLine.endsWith(');')) { |
|
|
indentLevel = Math.max(0, indentLevel - 1); |
|
|
} |
|
|
|
|
|
// Add current indentation |
|
|
formattedCode += ' '.repeat(indentLevel) + trimmedLine + '\n'; |
|
|
|
|
|
// Increase indent if line opens a block |
|
|
if (trimmedLine.endsWith('{') || (trimmedLine.endsWith('(') && !trimmedLine.endsWith(');'))) { |
|
|
indentLevel += 1; |
|
|
} |
|
|
} |
|
|
|
|
|
return formattedCode.trim(); |
|
|
} |
|
|
|
|
|
function formatPython(code) { |
|
|
// Very basic Python formatting |
|
|
const lines = code.split('\n'); |
|
|
let formattedCode = ''; |
|
|
let indentLevel = 0; |
|
|
|
|
|
for (const line of lines) { |
|
|
const trimmedLine = line.trim(); |
|
|
|
|
|
// Decrease indent if line starts with dedent keywords |
|
|
if (trimmedLine.startsWith('return') || trimmedLine.startsWith('pass') || |
|
|
trimmedLine.startsWith('break') || trimmedLine.startsWith('continue') || |
|
|
trimmedLine.startsWith('raise')) { |
|
|
indentLevel = Math.max(0, indentLevel - 1); |
|
|
} |
|
|
|
|
|
// Add current indentation |
|
|
formattedCode += ' '.repeat(indentLevel) + trimmedLine + '\n'; |
|
|
|
|
|
// Increase indent if line ends with colon |
|
|
if (trimmedLine.endsWith(':')) { |
|
|
indentLevel += 1; |
|
|
} |
|
|
} |
|
|
|
|
|
return formattedCode.trim(); |
|
|
} |
|
|
|
|
|
function formatHtml(code) { |
|
|
// Basic HTML formatting |
|
|
const lines = code.split('\n'); |
|
|
let formattedCode = ''; |
|
|
let indentLevel = 0; |
|
|
|
|
|
for (const line of lines) { |
|
|
const trimmedLine = line.trim(); |
|
|
|
|
|
// Decrease indent for closing tags |
|
|
if (trimmedLine.startsWith('</')) { |
|
|
indentLevel = Math.max(0, indentLevel - 1); |
|
|
} |
|
|
|
|
|
// Add current indentation |
|
|
formattedCode += ' '.repeat(indentLevel) + trimmedLine + '\n'; |
|
|
|
|
|
// Increase indent for opening tags (unless they're self-closing) |
|
|
if (trimmedLine.startsWith('<') && !trimmedLine.startsWith('<!') && |
|
|
!trimmedLine.endsWith('/>') && !trimmedLine.includes('</')) { |
|
|
indentLevel += 1; |
|
|
} |
|
|
} |
|
|
|
|
|
return formattedCode.trim(); |
|
|
} |
|
|
|
|
|
function formatCss(code) { |
|
|
// Basic CSS formatting |
|
|
const lines = code.split('\n'); |
|
|
let formattedCode = ''; |
|
|
let indentLevel = 0; |
|
|
|
|
|
for (const line of lines) { |
|
|
const trimmedLine = line.trim(); |
|
|
|
|
|
// Decrease indent after closing brace |
|
|
if (trimmedLine.endsWith('}')) { |
|
|
indentLevel = Math.max(0, indentLevel - 1); |
|
|
} |
|
|
|
|
|
// Add current indentation |
|
|
formattedCode += ' '.repeat(indentLevel) + trimmedLine + '\n'; |
|
|
|
|
|
// Increase indent after opening brace |
|
|
if (trimmedLine.endsWith('{')) { |
|
|
indentLevel += 1; |
|
|
} |
|
|
} |
|
|
|
|
|
return formattedCode.trim(); |
|
|
} |
|
|
|
|
|
// Diff functionality |
|
|
function updateDiff() { |
|
|
const currentFile = state.files[state.currentFile]; |
|
|
const oldContent = currentFile.lastContent; |
|
|
const newContent = editor.getValue(); |
|
|
|
|
|
if (oldContent === newContent) return; |
|
|
|
|
|
const diff = Diff.diffLines(oldContent, newContent); |
|
|
const diffContainer = document.getElementById('diff-container'); |
|
|
|
|
|
let html = '<div class="font-mono text-sm">'; |
|
|
|
|
|
diff.forEach((part) => { |
|
|
const lines = part.value.split('\n'); |
|
|
if (lines[lines.length - 1] === '') lines.pop(); |
|
|
|
|
|
const className = part.added ? 'diff-added' : part.removed ? 'diff-removed' : 'diff-unchanged'; |
|
|
|
|
|
lines.forEach(line => { |
|
|
if (line.trim() === '') return; |
|
|
html += `<div class="${className} px-2 py-1 my-1 rounded">${escapeHtml(line)}</div>`; |
|
|
}); |
|
|
}); |
|
|
|
|
|
html += '</div>'; |
|
|
diffContainer.innerHTML = html; |
|
|
|
|
|
// Update last content if not empty |
|
|
if (newContent.trim() !== '') { |
|
|
currentFile.lastContent = newContent; |
|
|
} |
|
|
} |
|
|
|
|
|
// Show AI cursor in the editor |
|
|
function showAICursor(line, ch, length, label = 'AI') { |
|
|
const cursorContainer = document.getElementById('ai-cursor-container'); |
|
|
cursorContainer.innerHTML = ''; |
|
|
|
|
|
const lineHeight = editor.defaultTextHeight(); |
|
|
const charWidth = editor.defaultCharWidth(); |
|
|
|
|
|
const cursor = document.createElement('div'); |
|
|
cursor.className = 'ai-cursor'; |
|
|
cursor.style.top = `${line * lineHeight}px`; |
|
|
cursor.style.left = `${ch * charWidth}px`; |
|
|
cursor.style.height = `${lineHeight}px`; |
|
|
cursor.style.width = `${length * charWidth}px`; |
|
|
|
|
|
const cursorLabel = document.createElement('div'); |
|
|
cursorLabel.className = 'ai-cursor-label'; |
|
|
cursorLabel.textContent = label; |
|
|
|
|
|
cursor.appendChild(cursorLabel); |
|
|
cursorContainer.appendChild(cursor); |
|
|
} |
|
|
|
|
|
// Typewriter effect for applying changes |
|
|
function applyChangesWithTypewriter(newCode, callback) { |
|
|
const currentCode = editor.getValue(); |
|
|
const cursor = editor.getCursor(); |
|
|
|
|
|
// Clear any existing interval |
|
|
if (state.typewriterInterval) { |
|
|
clearInterval(state.typewriterInterval); |
|
|
} |
|
|
|
|
|
// Set the new code immediately but keep track for animation |
|
|
editor.setValue(newCode); |
|
|
|
|
|
// Move cursor to the first change position |
|
|
const diff = Diff.diffLines(currentCode, newCode); |
|
|
let firstChangePos = {line: 0, ch: 0}; |
|
|
|
|
|
for (let i = 0; i < diff.length; i++) { |
|
|
if (diff[i].added) { |
|
|
const lines = newCode.split('\n'); |
|
|
for (let j = 0; j < lines.length; j++) { |
|
|
if (lines[j].includes(diff[i].value.trim())) { |
|
|
firstChangePos = {line: j, ch: 0}; |
|
|
break; |
|
|
} |
|
|
} |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
editor.setCursor(firstChangePos); |
|
|
|
|
|
// Add typewriter cursor effect |
|
|
const typewriterCursor = document.createElement('div'); |
|
|
typewriterCursor.className = 'typewriter-cursor'; |
|
|
typewriterCursor.style.top = `${firstChangePos.line * editor.defaultTextHeight()}px`; |
|
|
typewriterCursor.style.left = `${firstChangePos.ch * editor.defaultCharWidth()}px`; |
|
|
typewriterCursor.style.height = `${editor.defaultTextHeight()}px`; |
|
|
document.getElementById('ai-cursor-container').appendChild(typewriterCursor); |
|
|
|
|
|
// Remove cursor after animation |
|
|
setTimeout(() => { |
|
|
typewriterCursor.remove(); |
|
|
if (callback) callback(); |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
// Analysis function with real API integration |
|
|
async function analyzeCode() { |
|
|
const code = editor.getValue(); |
|
|
const apiKey = state.apiKey; |
|
|
const customPrompt = document.getElementById('custom-prompt').value; |
|
|
|
|
|
if (!code.trim()) { |
|
|
showToast('Please enter some code to analyze', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!apiKey) { |
|
|
showToast('Please enter your DeepSeek API key', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
// Save current content |
|
|
state.files[state.currentFile].content = code; |
|
|
|
|
|
// Show loading state |
|
|
const responseContainer = document.getElementById('response-container'); |
|
|
responseContainer.innerHTML = ` |
|
|
<div class="flex flex-col items-center justify-center py-8"> |
|
|
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div> |
|
|
<p>Analizando tu código...</p> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
// Switch to response tab |
|
|
document.querySelector('[data-tab="response"]').click(); |
|
|
|
|
|
try { |
|
|
const messages = [ |
|
|
{ |
|
|
role: "system", |
|
|
content: `You are a helpful code assistant. Analyze the provided code and respond with: |
|
|
1. An explanation of what the code does |
|
|
2. Suggestions for improvement |
|
|
3. A modified version of the code with improvements |
|
|
4. A clear markdown formatted response with code blocks |
|
|
|
|
|
Important: When providing the modified code, include the ENTIRE code file with all changes applied, not just the modified parts. This is crucial for the diff tool to work properly. |
|
|
|
|
|
Format your response like this: |
|
|
### Analysis |
|
|
[Explanation of the code] |
|
|
|
|
|
### Suggestions |
|
|
[Bullet points with improvement suggestions] |
|
|
|
|
|
### Improved Code |
|
|
\`\`\`[language] |
|
|
[Complete modified code here] |
|
|
\`\`\` |
|
|
|
|
|
Additional notes: |
|
|
- Respond in the same language as the code comments |
|
|
- Preserve all original functionality |
|
|
- Include any necessary imports or boilerplate` |
|
|
} |
|
|
]; |
|
|
|
|
|
// Add custom prompt if provided |
|
|
if (customPrompt.trim()) { |
|
|
messages.push({ |
|
|
role: "user", |
|
|
content: customPrompt |
|
|
}); |
|
|
} |
|
|
|
|
|
// Add the code to analyze |
|
|
messages.push({ |
|
|
role: "user", |
|
|
content: `Here is my ${state.files[state.currentFile].language} code:\n\n${code}` |
|
|
}); |
|
|
|
|
|
const response = await fetch('https://api.deepseek.com/v1/chat/completions', { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
'Authorization': `Bearer ${apiKey}` |
|
|
}, |
|
|
body: JSON.stringify({ |
|
|
model: "deepseek-coder", |
|
|
messages: messages, |
|
|
temperature: 0.7, |
|
|
max_tokens: 2000 |
|
|
}) |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`API request failed with status ${response.status}`); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
const aiResponse = data.choices[0].message.content; |
|
|
|
|
|
// Extract the improved code from the response |
|
|
const improvedCodeMatch = aiResponse.match(/```(?:[a-zA-Z]*)\n([\s\S]*?)```/); |
|
|
let improvedCode = improvedCodeMatch ? improvedCodeMatch[1] : null; |
|
|
|
|
|
// Add to history |
|
|
state.history.unshift({ |
|
|
timestamp: new Date().toISOString(), |
|
|
code: code, |
|
|
response: aiResponse, |
|
|
prompt: customPrompt, |
|
|
improvedCode: improvedCode |
|
|
}); |
|
|
|
|
|
// Update history tab |
|
|
renderHistory(); |
|
|
|
|
|
// Format the response as HTML |
|
|
responseContainer.innerHTML = ` |
|
|
<div class="prose prose-invert max-w-none"> |
|
|
${formatAIResponse(aiResponse)} |
|
|
${improvedCode ? ` |
|
|
<div class="mt-6 flex space-x-3"> |
|
|
<button id="show-diff-btn" class="bg-blue-600 hover:bg-blue-700 px-3 py-2 rounded text-sm flex items-center space-x-1"> |
|
|
<i class="fas fa-code-compare mr-1"></i> |
|
|
<span>Show Changes</span> |
|
|
</button> |
|
|
<button id="apply-changes-btn" class="bg-green-600 hover:bg-green-700 px-3 py-2 rounded text-sm flex items-center space-x-1"> |
|
|
<i class="fas fa-magic mr-1"></i> |
|
|
<span>Apply Changes</span> |
|
|
</button> |
|
|
</div> |
|
|
` : ''} |
|
|
</div> |
|
|
`; |
|
|
|
|
|
// Add event listeners for buttons |
|
|
if (improvedCode) { |
|
|
document.getElementById('show-diff-btn').addEventListener('click', () => { |
|
|
showProposedDiff(code, improvedCode); |
|
|
}); |
|
|
|
|
|
document.getElementById('apply-changes-btn').addEventListener('click', () => { |
|
|
applyChangesWithTypewriter(improvedCode, () => { |
|
|
state.files[state.currentFile].content = improvedCode; |
|
|
state.files[state.currentFile].lastContent = improvedCode; |
|
|
showToast('Changes applied successfully'); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
showToast('Analysis completed successfully'); |
|
|
} catch (error) { |
|
|
console.error('API Error:', error); |
|
|
responseContainer.innerHTML = ` |
|
|
<div class="bg-red-900/30 p-4 rounded border border-red-700"> |
|
|
<h3 class="text-red-400 font-bold">Error</h3> |
|
|
<p>${escapeHtml(error.message)}</p> |
|
|
<p class="mt-2 text-sm">Please check your API key and network connection.</p> |
|
|
</div> |
|
|
`; |
|
|
showToast('Analysis failed', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
function formatAIResponse(text) { |
|
|
// Improved formatting with copy buttons for code blocks |
|
|
let html = escapeHtml(text) |
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') |
|
|
.replace(/\*(.*?)\*/g, '<em>$1</em>') |
|
|
.replace(/```(\w*)\n([\s\S]*?)```/g, (match, language, code) => { |
|
|
return ` |
|
|
<div class="code-block bg-gray-700 rounded my-3 relative"> |
|
|
<button class="code-block-copy bg-gray-600 hover:bg-gray-500 text-white text-xs px-2 py-1 rounded" |
|
|
onclick="navigator.clipboard.writeText(\`${escapeHtml(code).replace(/`/g, '\\`')}\`)"> |
|
|
<i class="fas fa-copy mr-1"></i>Copy |
|
|
</button> |
|
|
<pre class="p-3 overflow-x-auto"><code>${escapeHtml(code)}</code></pre> |
|
|
</div> |
|
|
`; |
|
|
}) |
|
|
.replace(/`(.*?)`/g, '<code class="bg-gray-700 px-1 rounded">$1</code>') |
|
|
.replace(/\n/g, '<br>'); |
|
|
|
|
|
return html; |
|
|
} |
|
|
|
|
|
function renderHistory() { |
|
|
const historyContainer = document.getElementById('history-container'); |
|
|
|
|
|
if (state.history.length === 0) { |
|
|
historyContainer.innerHTML = ` |
|
|
<div class="text-center text-gray-500 py-8"> |
|
|
<i class="fas fa-history text-3xl mb-2"></i> |
|
|
<p>No analysis history yet</p> |
|
|
</div> |
|
|
`; |
|
|
return; |
|
|
} |
|
|
|
|
|
let html = '<div class="space-y-4">'; |
|
|
|
|
|
state.history.forEach((item, index) => { |
|
|
const date = new Date(item.timestamp); |
|
|
const formattedDate = date.toLocaleString(); |
|
|
|
|
|
html += ` |
|
|
<div class="history-item bg-gray-700/50 p-3 rounded-lg cursor-pointer" data-index="${index}"> |
|
|
<div class="flex justify-between items-center mb-2"> |
|
|
<span class="text-sm font-medium">Analysis #${state.history.length - index}</span> |
|
|
<span class="text-xs text-gray-400">${formattedDate}</span> |
|
|
</div> |
|
|
${item.prompt ? `<div class="text-xs text-blue-400 mb-1">Prompt: "${escapeHtml(item.prompt.substring(0, 50))}${item.prompt.length > 50 ? '...' : ''}"</div>` : ''} |
|
|
<div class="text-sm text-gray-300 truncate">${escapeHtml(item.response.substring(0, 100))}...</div> |
|
|
${item.improvedCode ? `<div class="mt-2 text-xs text-green-400">Contains code modifications</div>` : ''} |
|
|
</div> |
|
|
`; |
|
|
}); |
|
|
|
|
|
html += '</div>'; |
|
|
historyContainer.innerHTML = html; |
|
|
|
|
|
// Add click handlers to history items |
|
|
document.querySelectorAll('.history-item').forEach(item => { |
|
|
item.addEventListener('click', () => { |
|
|
const index = item.getAttribute('data-index'); |
|
|
const historyItem = state.history[index]; |
|
|
|
|
|
document.getElementById('response-container').innerHTML = ` |
|
|
<div class="prose prose-invert max-w-none"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h3>Analysis from ${new Date(historyItem.timestamp).toLocaleString()}</h3> |
|
|
<div class="flex space-x-2"> |
|
|
<button id="load-code-btn" class="bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded text-xs"> |
|
|
<i class="fas fa-code mr-1"></i>Load Code |
|
|
</button> |
|
|
${historyItem.improvedCode ? ` |
|
|
<button id="apply-history-changes-btn" class="bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-xs"> |
|
|
<i class="fas fa-magic mr-1"></i>Apply Changes |
|
|
</button> |
|
|
` : ''} |
|
|
</div> |
|
|
</div> |
|
|
${historyItem.prompt ? `<div class="bg-gray-700/50 p-3 rounded mb-4"> |
|
|
<h4 class="text-blue-400 font-medium mb-1">Prompt:</h4> |
|
|
<p>${escapeHtml(historyItem.prompt)}</p> |
|
|
</div>` : ''} |
|
|
${formatAIResponse(historyItem.response)} |
|
|
</div> |
|
|
`; |
|
|
|
|
|
// Add handler for load code button |
|
|
document.getElementById('load-code-btn').addEventListener('click', () => { |
|
|
editor.setValue(historyItem.code); |
|
|
if (historyItem.prompt) { |
|
|
document.getElementById('custom-prompt').value = historyItem.prompt; |
|
|
localStorage.setItem('custom-prompt', historyItem.prompt); |
|
|
} |
|
|
showToast('Code loaded from history'); |
|
|
}); |
|
|
|
|
|
// Add handler for apply changes button |
|
|
if (historyItem.improvedCode) { |
|
|
document.getElementById('apply-history-changes-btn').addEventListener('click', () => { |
|
|
applyChangesWithTypewriter(historyItem.improvedCode, () => { |
|
|
state.files[state.currentFile].content = historyItem.improvedCode; |
|
|
state.files[state.currentFile].lastContent = historyItem.improvedCode; |
|
|
showToast('Changes applied successfully'); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
document.querySelector('[data-tab="response"]').click(); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
// Helper functions |
|
|
function escapeHtml(unsafe) { |
|
|
return unsafe |
|
|
.replace(/&/g, "&") |
|
|
.replace(/</g, "<") |
|
|
.replace(/>/g, ">") |
|
|
.replace(/"/g, """) |
|
|
.replace(/'/g, "'"); |
|
|
} |
|
|
|
|
|
function showToast(message, type = 'success') { |
|
|
const toast = document.createElement('div'); |
|
|
toast.className = `fixed bottom-4 right-4 px-4 py-2 rounded-md shadow-lg ${ |
|
|
type === 'success' ? 'bg-green-600' : 'bg-red-600' |
|
|
} text-white animate-fade-in`; |
|
|
toast.textContent = message; |
|
|
document.body.appendChild(toast); |
|
|
|
|
|
setTimeout(() => { |
|
|
toast.classList.remove('animate-fade-in'); |
|
|
toast.classList.add('animate-fade-out'); |
|
|
setTimeout(() => toast.remove(), 300); |
|
|
}, 3000); |
|
|
} |
|
|
|
|
|
// Auto-save and diff updates |
|
|
editor.on('change', () => { |
|
|
updateDiff(); |
|
|
|
|
|
// Auto-save every 5 seconds |
|
|
clearTimeout(window.autoSaveTimer); |
|
|
window.autoSaveTimer = setTimeout(() => { |
|
|
state.files[state.currentFile].content = editor.getValue(); |
|
|
}, 5000); |
|
|
}); |
|
|
|
|
|
// Initialize the app |
|
|
initUI(); |
|
|
|
|
|
// Add some CSS animations |
|
|
const style = document.createElement('style'); |
|
|
style.textContent = ` |
|
|
@keyframes fadeIn { |
|
|
from { opacity: 0; transform: translateY(10px); } |
|
|
to { opacity: 1; transform: translateY(0); } |
|
|
} |
|
|
@keyframes fadeOut { |
|
|
from { opacity: 1; transform: translateY(0); } |
|
|
to { opacity: 0; transform: translateY(10px); } |
|
|
} |
|
|
.animate-fade-in { |
|
|
animation: fadeIn 0.3s ease-out forwards; |
|
|
} |
|
|
.animate-fade-out { |
|
|
animation: fadeOut 0.3s ease-out forwards; |
|
|
} |
|
|
`; |
|
|
document.head.appendChild(style); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|