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:
@@ -1870,6 +1870,148 @@
|
|||||||
if (container) container.style.display = 'none';
|
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
|
// Pipeline: manageri → koodari (per tiedosto) → testaaja → korjausluuppi
|
||||||
async function kpnPipeline(task) {
|
async function kpnPipeline(task) {
|
||||||
pipelineClear();
|
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>`);
|
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)
|
// Yksinkertainen pipeline (vanha: manageri → koodari → testaaja)
|
||||||
|
|||||||
Reference in New Issue
Block a user