Projektikortti: tiedostovälilehdet, kopioi per tiedosto, lataa ZIP

Pipeline-tulokset renderöidään interaktiivisena projektikorttina terminaaliin:

- Tiedostovälilehdet (klikkaa vaihtaaksesi: main.py | models.py | ...)
- Syntaksikorostus (highlight.js) jokaisessa tiedostossa
- "Kopioi"-nappi per tiedosto (leikepöydälle)
- "Kopioi kaikki" -nappi (kaikki tiedostot yhtenä tekstinä)
- "Lataa ZIP" -nappi (selaimessa generoitu ZIP ilman ulkoisia kirjastoja)

ZIP-generointi on toteutettu puhtaalla JavaScriptillä (uncompressed store)
ilman JSZip- tai muita riippuvuuksia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaakko Vanhala
2026-04-05 20:37:10 +03:00
parent d5ab6272d3
commit 4dff534fbf

View File

@@ -1870,6 +1870,148 @@
if (container) container.style.display = 'none';
}
// Projektikortti: tiedostovälilehdet + kopioi + lataa ZIP
function renderProjectCard(files, projectName) {
const fileEntries = Object.entries(files);
if (fileEntries.length === 0) return;
const cardId = 'proj-' + Date.now();
const tabsHtml = fileEntries.map(([name], i) =>
`<span class="proj-tab" data-card="${cardId}" data-idx="${i}" style="padding:4px 10px;cursor:pointer;border-radius:4px 4px 0 0;font-size:12px;${i === 0 ? 'background:#161b22;color:#58a6ff;border:1px solid #30363d;border-bottom:none' : 'color:#8b949e'}" onclick="switchProjectTab('${cardId}',${i})">${esc(name)}</span>`
).join('');
const panelsHtml = fileEntries.map(([name, code], i) =>
`<div class="proj-panel" data-card="${cardId}" data-idx="${i}" style="${i > 0 ? 'display:none' : ''}">
<div style="display:flex;justify-content:flex-end;padding:4px 8px;background:#0d1117;border-bottom:1px solid #21262d">
<button onclick="copyFileContent('${cardId}',${i})" style="background:none;border:1px solid #30363d;color:#8b949e;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Kopioi ${esc(name)} leikepöydälle">Kopioi</button>
</div>
<pre style="margin:0;padding:10px;font-size:12px;line-height:1.5;overflow-x:auto;white-space:pre-wrap">${highlightCode(code)}</pre>
</div>`
).join('');
const allText = fileEntries.map(([name, code]) => `# --- ${name} ---\n${code}`).join('\n\n');
const cardHtml = `
<div id="${cardId}" style="margin:8px 0;border:1px solid #30363d;border-radius:6px;background:#161b22;overflow:hidden" data-files='${esc(JSON.stringify(files))}'>
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#0d1117;border-bottom:1px solid #30363d">
<span style="color:#a371f7;font-weight:600;font-size:13px">${esc(projectName || 'Projekti')} <span style="color:#8b949e;font-weight:normal">(${fileEntries.length} tiedostoa)</span></span>
<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>
</span>
</div>
<div style="display:flex;gap:2px;padding:6px 8px 0;background:#0d1117">${tabsHtml}</div>
<div style="background:#161b22">${panelsHtml}</div>
</div>`;
const div = document.createElement('div');
div.innerHTML = cardHtml;
termPanel.appendChild(div.firstElementChild);
termPanel.scrollTop = termPanel.scrollHeight;
}
// Globaalit funktiot projektikortin interaktioille
window.switchProjectTab = function(cardId, idx) {
document.querySelectorAll(`.proj-tab[data-card="${cardId}"]`).forEach((tab, i) => {
tab.style.background = i === idx ? '#161b22' : 'transparent';
tab.style.color = i === idx ? '#58a6ff' : '#8b949e';
tab.style.border = i === idx ? '1px solid #30363d' : 'none';
tab.style.borderBottom = i === idx ? 'none' : '';
});
document.querySelectorAll(`.proj-panel[data-card="${cardId}"]`).forEach((panel, i) => {
panel.style.display = i === idx ? '' : 'none';
});
};
window.copyFileContent = function(cardId, idx) {
const card = document.getElementById(cardId);
if (!card) return;
const files = JSON.parse(card.dataset.files);
const entries = Object.entries(files);
if (entries[idx]) {
navigator.clipboard.writeText(entries[idx][1]);
// Visuaalinen palaute
const btn = card.querySelectorAll(`.proj-panel[data-idx="${idx}"] button`)[0];
if (btn) { const orig = btn.textContent; btn.textContent = '✓ Kopioitu'; setTimeout(() => btn.textContent = orig, 1500); }
}
};
window.copyAllFiles = function(cardId) {
const card = document.getElementById(cardId);
if (!card) return;
const files = JSON.parse(card.dataset.files);
const text = Object.entries(files).map(([name, code]) => `# --- ${name} ---\n${code}`).join('\n\n');
navigator.clipboard.writeText(text);
const btn = card.querySelector('[onclick*="copyAllFiles"]');
if (btn) { const orig = btn.textContent; btn.textContent = '✓ Kopioitu'; setTimeout(() => btn.textContent = orig, 1500); }
};
window.downloadZip = async function(cardId) {
const card = document.getElementById(cardId);
if (!card) return;
const files = JSON.parse(card.dataset.files);
// Luodaan ZIP ilman ulkoisia kirjastoja (yksinkertainen uncompressed ZIP)
const entries = Object.entries(files);
const parts = [];
const centralDir = [];
let offset = 0;
for (const [name, content] of entries) {
const nameBytes = new TextEncoder().encode(name);
const contentBytes = new TextEncoder().encode(content);
// Local file header
const header = new Uint8Array(30 + nameBytes.length);
const view = new DataView(header.buffer);
view.setUint32(0, 0x04034b50, true); // Signature
view.setUint16(4, 20, true); // Version needed
view.setUint16(8, 0, true); // Method: store
view.setUint32(18, contentBytes.length, true); // Compressed size
view.setUint32(22, contentBytes.length, true); // Uncompressed size
view.setUint16(26, nameBytes.length, true);
header.set(nameBytes, 30);
// Central directory entry
const cdEntry = new Uint8Array(46 + nameBytes.length);
const cdView = new DataView(cdEntry.buffer);
cdView.setUint32(0, 0x02014b50, true);
cdView.setUint16(4, 20, true);
cdView.setUint16(6, 20, true);
cdView.setUint32(20, contentBytes.length, true);
cdView.setUint32(24, contentBytes.length, true);
cdView.setUint16(28, nameBytes.length, true);
cdView.setUint32(42, offset, true);
cdEntry.set(nameBytes, 46);
parts.push(header, contentBytes);
centralDir.push(cdEntry);
offset += header.length + contentBytes.length;
}
const cdOffset = offset;
let cdSize = 0;
for (const cd of centralDir) { parts.push(cd); cdSize += cd.length; }
// End of central directory
const eocd = new Uint8Array(22);
const eocdView = new DataView(eocd.buffer);
eocdView.setUint32(0, 0x06054b50, true);
eocdView.setUint16(8, entries.length, true);
eocdView.setUint16(10, entries.length, true);
eocdView.setUint32(12, cdSize, true);
eocdView.setUint32(16, cdOffset, true);
parts.push(eocd);
const blob = new Blob(parts, { type: 'application/zip' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'project.zip';
a.click();
URL.revokeObjectURL(url);
};
// Pipeline: manageri → koodari (per tiedosto) → testaaja → korjausluuppi
async function kpnPipeline(task) {
pipelineClear();
@@ -1984,6 +2126,7 @@ Write the corrected code.`;
}
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis (${Object.keys(generatedFiles).length} tiedostoa) ━━━</span>`);
renderProjectCard(generatedFiles, task);
}
// Yksinkertainen pipeline (vanha: manageri → koodari → testaaja)