Monaco Editor -välilehti: selainpohjainen koodieditori agenttien tuottamalle koodille

Uusi "Editor"-välilehti jossa:
- Monaco Editor (VS Coden ydin) CDN:stä, dark-teema
- Tiedostopuu vasemmalla (klikataan tiedostoa)
- Välilehdet ylhäällä (useita tiedostoja auki)
- Kielitunnistus tiedostopäätteestä (Python, Rust, JS, ...)
- "Avaa editorissa" -nappi projektikorteissa

Monaco ladataan taustalla requestIdleCallback:llä — ei hidasta
sivun käynnistymistä. Editor alustetaan vasta kun sitä tarvitaan.

Projektikortin "Avaa editorissa" -nappi:
1. Avaa Editor-välilehden
2. Luo Monaco-mallit jokaiselle tiedostolle
3. Renderöi tiedostopuun ja välilehdet
4. Avaa ensimmäisen tiedoston editoriin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaakko Vanhala
2026-04-09 17:04:59 +03:00
parent 0dc53dba1c
commit b6a8fa2671

View File

@@ -15,6 +15,7 @@ import AgentChat from "../components/AgentChat.astro";
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
window.mermaid = mermaid;
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/editor/editor.main.css">
<link rel="stylesheet" href="/src/styles/global.css">
<style>
/* Scoped styles can go here */
@@ -42,6 +43,7 @@ import AgentChat from "../components/AgentChat.astro";
<div class="main-tab" onclick="switchMainTab('builder')" data-i18n="tab_builder">Agent Builder</div>
<div class="main-tab" onclick="switchMainTab('gallery')" data-i18n="tab_gallery">Galleria</div>
<div class="main-tab" onclick="switchMainTab('guide')" data-i18n="tab_guide">Opas</div>
<div class="main-tab" onclick="switchMainTab('editor')" data-i18n="tab_editor">Editor</div>
</div>
<div style="display: flex; gap: 8px; opacity: 0.65; transform: scale(0.9); transform-origin: bottom right;">
<div class="main-tab" onclick="switchMainTab('network')" data-i18n="tab_network">Laskentaverkko</div>
@@ -580,6 +582,24 @@ ZIP-paketti sisältäen:
</div>
</div>
<!-- PANEELI: Editor -->
<div id="panel-editor" class="main-panel">
<div style="display:flex;height:calc(100vh - 160px);gap:0;border:1px solid var(--border-color);border-radius:6px;overflow:hidden">
<!-- Tiedostopuu -->
<div id="editor-filetree" style="width:200px;min-width:150px;background:#0d1117;border-right:1px solid var(--border-color);overflow-y:auto;font-family:'Courier New',monospace;font-size:13px">
<div style="padding:10px 12px;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;border-bottom:1px solid var(--border-color)">Tiedostot</div>
<div id="editor-file-list" style="padding:4px 0">
<div style="padding:8px 16px;color:#8b949e;font-size:12px">Ei tiedostoja.<br><br>Generoi projekti:<br><code style="color:#58a6ff">kpn project "..."</code><br>ja klikkaa "Avaa editorissa"</div>
</div>
</div>
<!-- Monaco Editor -->
<div style="flex:1;display:flex;flex-direction:column">
<div id="editor-tabs" style="display:flex;background:#0d1117;border-bottom:1px solid var(--border-color);min-height:35px;align-items:flex-end;padding:0 8px;gap:2px;overflow-x:auto"></div>
<div id="monaco-container" style="flex:1"></div>
</div>
</div>
</div>
</div>
<script type="module" is:inline>
@@ -1683,6 +1703,7 @@ IMPORTANT: Include get_db() dependency for FastAPI` },
<span style="display:flex;gap:6px">
<button onclick="copyAllFiles('${cardId}')" style="background:none;border:1px solid #30363d;color:#8b949e;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Kopioi kaikki tiedostot leikepöydälle">Kopioi kaikki</button>
<button onclick="downloadZip('${cardId}')" style="background:none;border:1px solid #30363d;color:#58a6ff;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Lataa projekti ZIP-tiedostona">Lataa ZIP</button>
<button onclick="openInEditor(JSON.parse(document.getElementById('${cardId}').dataset.files))" style="background:none;border:1px solid #3fb950;color:#3fb950;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Avaa Monaco-editorissa">Avaa editorissa</button>
${reportUrl ? `<a href="${reportUrl}" target="_blank" style="background:none;border:1px solid #a371f7;color:#a371f7;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer;text-decoration:none">📄 Raportti</a>` : ''}
</span>
</div>
@@ -4525,5 +4546,126 @@ uv run python crew.py "FastAPI + SQLite CRUD API"
</div>
</div>
</div>
<!-- Monaco Editor loader -->
<script is:inline>
// AMD-loader Monacolle
window.MonacoEnvironment = {
getWorkerUrl: function(workerId, label) {
return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
self.MonacoEnvironment = { baseUrl: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/' };
importScripts('https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/base/worker/workerMain.js');
`)}`;
}
};
// Kielitunnistus tiedostopäätteestä
function langFromFilename(name) {
const ext = name.split('.').pop().toLowerCase();
const map = { py: 'python', rs: 'rust', js: 'javascript', ts: 'typescript', toml: 'toml', json: 'json', html: 'html', css: 'css', md: 'markdown', txt: 'plaintext', yaml: 'yaml', yml: 'yaml', sh: 'shell', sql: 'sql' };
return map[ext] || 'plaintext';
}
// Globaali editor-state
window._editorFiles = {}; // { "main.py": "code...", ... }
window._editorModels = {}; // Monaco-mallit per tiedosto
window._activeFile = null;
window._monacoEditor = null;
window._monacoLoaded = false;
// Ladataan Monaco taustalla idle-aikana — ei blockata sivun latautumista
if ('requestIdleCallback' in window) {
requestIdleCallback(() => window.initMonaco(), { timeout: 5000 });
} else {
setTimeout(() => window.initMonaco(), 3000);
}
window.initMonaco = async function() {
if (window._monacoLoaded) return;
window._monacoLoaded = true;
await new Promise((resolve) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js';
script.onload = () => {
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs' }});
require(['vs/editor/editor.main'], () => resolve());
};
document.head.appendChild(script);
});
window._monacoEditor = monaco.editor.create(document.getElementById('monaco-container'), {
value: '// Valitse tiedosto vasemmalta tai generoi projekti agentilla\n',
language: 'plaintext',
theme: 'vs-dark',
fontSize: 14,
minimap: { enabled: false },
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
padding: { top: 10 },
});
};
// Avaa tiedostot editoriin (kutsutaan projektikortin "Avaa editorissa" -napista)
window.openInEditor = function(files) {
window._editorFiles = { ...files };
// Vaihda Editor-välilehteen
if (typeof switchMainTab === 'function') switchMainTab('editor');
// Alusta Monaco tarvittaessa
window.initMonaco().then(() => {
// Luo Monaco-mallit
for (const [name, code] of Object.entries(files)) {
const lang = langFromFilename(name);
if (window._editorModels[name]) {
window._editorModels[name].setValue(code);
} else {
window._editorModels[name] = monaco.editor.createModel(code, lang);
}
}
// Renderöi tiedostopuu
const fileList = document.getElementById('editor-file-list');
fileList.innerHTML = Object.keys(files).map(name =>
`<div class="editor-file-item" data-file="${name}" onclick="openEditorFile('${name}')" style="padding:6px 16px;cursor:pointer;color:#c9d1d9;font-size:13px;display:flex;align-items:center;gap:6px;border-left:2px solid transparent">`
+ `<span style="color:#8b949e;font-size:11px">${langFromFilename(name) === 'python' ? '🐍' : langFromFilename(name) === 'rust' ? '🦀' : '📄'}</span>`
+ `${name}</div>`
).join('');
// Renderöi välilehdet
const tabs = document.getElementById('editor-tabs');
tabs.innerHTML = Object.keys(files).map(name =>
`<div class="editor-tab" data-file="${name}" onclick="openEditorFile('${name}')" style="padding:6px 12px;cursor:pointer;font-size:12px;color:#8b949e;border:1px solid transparent;border-bottom:none;border-radius:4px 4px 0 0;white-space:nowrap">${name}</div>`
).join('');
// Avaa ensimmäinen tiedosto
const firstName = Object.keys(files)[0];
if (firstName) openEditorFile(firstName);
});
};
// Vaihda aktiivinen tiedosto
window.openEditorFile = function(name) {
if (!window._editorModels[name] || !window._monacoEditor) return;
window._activeFile = name;
window._monacoEditor.setModel(window._editorModels[name]);
// Päivitä visuaalinen tila
document.querySelectorAll('.editor-file-item').forEach(el => {
const active = el.dataset.file === name;
el.style.background = active ? '#161b22' : 'transparent';
el.style.borderLeftColor = active ? '#58a6ff' : 'transparent';
el.style.color = active ? '#e6edf3' : '#c9d1d9';
});
document.querySelectorAll('.editor-tab').forEach(el => {
const active = el.dataset.file === name;
el.style.background = active ? '#161b22' : 'transparent';
el.style.color = active ? '#58a6ff' : '#8b949e';
el.style.borderColor = active ? '#30363d' : 'transparent';
el.style.borderBottomColor = active ? '#161b22' : 'transparent';
});
};
</script>
</body>
</html>