8 Commits

Author SHA1 Message Date
de1cf009fa pyproject.toml: stdlib-moduulit (sqlite3, os, sys) kielletty riippuvuuksista
Malli laittoi sqlite3:n dependencies-listaan → uv sync epäonnistui.
Koodarin, QA:n ja validoinnin prompteihin lisätty selkeä kielto.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:53:10 +03:00
060f36f479 ZIP-lataus: null-tarkistus tiedostoille + virheilmoitus
Lisätty guard puuttuvalle projectFiles-datalle ja null-safe content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:46:34 +03:00
e2ec0fa43d v0.2.2: responsiivinen UI, Ollama-proxy, mixed content korjaus
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:23:35 +03:00
8752c0f465 Responsiivinen korkeus: terminaali ja UI skaalautuvat viewport-korkeuteen
Terminaali: clamp(200px, 35vh, 500px) — skaalautuu ikkunan mukaan.
<900px korkeus: pienempi otsikko, tiiviimmät avataret, matalampi terminaali.
>1200px korkeus: isompi terminaali ja promptikenttä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:22:15 +03:00
8c95282654 Tiivistetty layout: terminaali 500→300px, pienemmät marginaalit
Mahtuu 1440px korkeuteen ilman vieritystä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:20:06 +03:00
a1bc1af646 Hardware API: Ollama-fallback kun wgpu ei tunnista GPU:ta Dockerissa
/api/v1/hardware tarkistaa nyt myös Ollaman tilan fallbackina.
kpn models näyttää ladattujen mallien määrän ja ✓ oikein.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:18:00 +03:00
6b27cbbade kpn load: rehellinen viesti — 'valittu' eikä 'ladattu ja aktiivinen'
Frontend ei tiedä onko malli oikeasti ladattu Ollamaan.
Nyt näytetään 'valittu — natiivisolmu lataa mallin' ja
varoitus ensimmäisen pyynnön hitaudesta.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:04:11 +03:00
4d9c51a86f .gitignore: *.db — ajonaikaiset tietokannat pois versionhallinnasta
nodes.db muuttui jatkuvasti ja esti deployn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:59:35 +03:00
6 changed files with 65 additions and 18 deletions

3
.gitignore vendored
View File

@@ -34,3 +34,6 @@ Cargo.lock
*.pdb *.pdb
# End of https://www.toptal.com/developers/gitignore/api/rust,linux # End of https://www.toptal.com/developers/gitignore/api/rust,linux
# Ajonaikaiset tietokannat
*.db

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "hub" name = "hub"
version = "0.2.1" version = "0.2.2"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

Binary file not shown.

View File

@@ -984,17 +984,34 @@ async fn api_hardware(
s.get("node_type").and_then(|v| v.as_str()) == Some("native") s.get("node_type").and_then(|v| v.as_str()) == Some("native")
}); });
let (vram_mb, gpu_name, ram_mb) = if let Some(s) = native { let (mut vram_mb, mut gpu_name, ram_mb) = if let Some(s) = native {
let gpus = s.get("gpus").and_then(|v| v.as_array()); let gpus = s.get("gpus").and_then(|v| v.as_array());
let gpu = gpus.and_then(|g| g.first()); let gpu = gpus.and_then(|g| g.first());
let vram = gpu.and_then(|g| g.get("vram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0); let vram = gpu.and_then(|g| g.get("vram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
let name = gpu.and_then(|g| g.get("name")).and_then(|v| v.as_str()).unwrap_or("?"); let name = gpu.and_then(|g| g.get("name")).and_then(|v| v.as_str()).unwrap_or("").to_string();
let ram = s.get("system").and_then(|v| v.get("ram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0); let ram = s.get("system").and_then(|v| v.get("ram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
(vram, name.to_string(), ram) (vram, name, ram)
} else { } else {
(0, "ei natiivisolmua".to_string(), 0) (0, String::new(), 0)
}; };
// Fallback: kysytään Ollamalta onko malleja ladattu (= Ollama on käynnissä)
if vram_mb == 0 {
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
if let Ok(resp) = reqwest::get(format!("{}/api/tags", ollama_url)).await {
if let Ok(body) = resp.json::<serde_json::Value>().await {
let models = body["models"].as_array().map(|a| a.len()).unwrap_or(0);
if models > 0 {
gpu_name = "Ollama (GPU/CPU)".to_string();
// Natiivisolmun RAM fallbackina
vram_mb = if ram_mb > 0 { ram_mb } else { 0 };
}
}
}
}
if gpu_name.is_empty() { gpu_name = "ei natiivisolmua".to_string(); }
axum::Json(serde_json::json!({ axum::Json(serde_json::json!({
"gpu_name": gpu_name, "gpu_name": gpu_name,
"vram_mb": vram_mb, "vram_mb": vram_mb,

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "native-node" name = "native-node"
version = "0.2.1" version = "0.2.2"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@@ -388,7 +388,7 @@
font-family: 'Courier New', Courier, monospace; font-family: 'Courier New', Courier, monospace;
font-size:14px; font-size:14px;
color:var(--success-color); color:var(--success-color);
height:500px; height: clamp(200px, 35vh, 500px);
overflow-y:auto; overflow-y:auto;
text-align:left; text-align:left;
white-space: pre-wrap; white-space: pre-wrap;
@@ -629,7 +629,7 @@
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
padding: 8px; padding: 8px;
resize: vertical; resize: vertical;
min-height: 60px; min-height: 40px;
outline: none; outline: none;
} }
.agent-prompt-editor textarea:focus { border-color: var(--accent-color); } .agent-prompt-editor textarea:focus { border-color: var(--accent-color); }
@@ -688,11 +688,26 @@
#code-input-container { flex-direction: column !important; } #code-input-container { flex-direction: column !important; }
#code-send-btn { width: 100%; margin-top: 5px; } #code-send-btn { width: 100%; margin-top: 5px; }
} }
/* Responsiivinen korkeus */
@media (max-height: 900px) {
.terminal-panel { height: clamp(150px, 25vh, 250px); }
.agent-prompt-editor textarea { min-height: 30px; }
.container > div:first-child { margin-bottom: 6px; }
.container h1 { font-size: 22px; }
.container .sub { font-size: 12px; }
.avatar-card { padding: 6px 4px; }
.avatar-card img { width: 50px; height: 50px; margin-bottom: 4px; }
}
@media (min-height: 1200px) {
.terminal-panel { height: clamp(350px, 40vh, 600px); }
.agent-prompt-editor textarea { min-height: 80px; }
}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px;"> <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px;">
<div> <div>
<h1 style="margin-bottom:0;" data-i18n="main_title"><span style="color:#ff6b00">Kipinä</span> <span>Agentic Playground</span></h1> <h1 style="margin-bottom:0;" data-i18n="main_title"><span style="color:#ff6b00">Kipinä</span> <span>Agentic Playground</span></h1>
<p class="sub" style="margin-bottom:0;"><span data-i18n="main_subtitle">AI-ohjelmistokehitystiimi</span> · <span id="hub-version" style="color:#58a6ff">-</span></p> <p class="sub" style="margin-bottom:0;"><span data-i18n="main_subtitle">AI-ohjelmistokehitystiimi</span> · <span id="hub-version" style="color:#58a6ff">-</span></p>
@@ -1068,7 +1083,7 @@
</div> </div>
<!-- Prompt Editor --> <!-- Prompt Editor -->
<div class="agent-prompt-editor" id="agent-prompt-editor" style="margin-top:20px;"> <div class="agent-prompt-editor" id="agent-prompt-editor" style="margin-top:10px;">
<div class="agent-prompt-label"> <div class="agent-prompt-label">
<strong id="agent-prompt-name"></strong> <strong id="agent-prompt-name"></strong>
<span id="agent-prompt-saved" style="color:var(--success-color);opacity:0;transition:opacity 0.3s">Tallennettu</span> <span id="agent-prompt-saved" style="color:var(--success-color);opacity:0;transition:opacity 0.3s">Tallennettu</span>
@@ -1483,7 +1498,7 @@ IMPORTANT: Check consistency
2. Dockerfile deps vs imports: all imports covered 2. Dockerfile deps vs imports: all imports covered
3. docker-compose ports: match EXPOSE 3. docker-compose ports: match EXPOSE
4. Test imports: match actual module names 4. Test imports: match actual module names
5. pyproject.toml deps: cover all imports` }, 5. pyproject.toml deps: cover all imports (but NOT stdlib like sqlite3, os, sys, json)` },
tester: { label: 'DevOps', prompt: `docker-compose.yml: services, volumes, port mappings tester: { label: 'DevOps', prompt: `docker-compose.yml: services, volumes, port mappings
EXAMPLE: EXAMPLE:
@@ -2256,6 +2271,10 @@ IMPORTANT: Include get_db() dependency for FastAPI` },
const card = document.getElementById(cardId); const card = document.getElementById(cardId);
if (!card) return; if (!card) return;
const files = projectFiles[cardId]; const files = projectFiles[cardId];
if (!files || Object.keys(files).length === 0) {
alert('Tiedostot eivät ole enää muistissa — aja pipeline uudelleen.');
return;
}
// CRC-32 laskenta ZIP-tiedostoille // CRC-32 laskenta ZIP-tiedostoille
function crc32(bytes) { function crc32(bytes) {
@@ -2276,7 +2295,7 @@ IMPORTANT: Include get_db() dependency for FastAPI` },
for (const [name, content] of entries) { for (const [name, content] of entries) {
const nameBytes = new TextEncoder().encode(name); const nameBytes = new TextEncoder().encode(name);
const contentBytes = new TextEncoder().encode(content); const contentBytes = new TextEncoder().encode(content || '');
const crc = crc32(contentBytes); const crc = crc32(contentBytes);
// Local file header // Local file header
@@ -2414,10 +2433,12 @@ Project: ${task}`;
name = "projectname" name = "projectname"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = ["fastapi", "uvicorn"] dependencies = ["fastapi", "uvicorn", "sqlalchemy"]
[project.scripts] [project.scripts]
start = "uvicorn main:app --reload"`; start = "uvicorn main:app --reload"
IMPORTANT: Only list pip-installable packages. NEVER include Python stdlib modules like sqlite3, os, sys, json, typing, collections, etc.`;
} else if (file.name === 'requirements.txt') { } else if (file.name === 'requirements.txt') {
extraInstructions = '\nList one dependency per line. No version pins unless necessary.'; extraInstructions = '\nList one dependency per line. No version pins unless necessary.';
} }
@@ -2608,7 +2629,7 @@ Files: ${Object.keys(generatedFiles).join(', ')}`;
3. docker-compose ports: ✓ OK / ✗ problem description 3. docker-compose ports: ✓ OK / ✗ problem description
4. README commands: ✓ OK / ✗ problem description 4. README commands: ✓ OK / ✗ problem description
5. Test imports: ✓ OK / ✗ problem description 5. Test imports: ✓ OK / ✗ problem description
6. pyproject.toml deps: ✓ OK / ✗ problem description 6. pyproject.toml deps: ✓ OK / ✗ problem (NEVER list stdlib: sqlite3, os, sys, json, typing)
EXAMPLE output: EXAMPLE output:
1. Dockerfile COPY: ✓ OK — copies main.py and models.py which both exist 1. Dockerfile COPY: ✓ OK — copies main.py and models.py which both exist
@@ -2838,7 +2859,8 @@ ${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
clearInterval(spinTimer); clearInterval(spinTimer);
pullLine.remove(); pullLine.remove();
if (hubData.status === 'ok') { if (hubData.status === 'ok') {
termLog(` <span style="color:#3fb950">✓</span> ${selected.name} ladattu ja aktiivinen`, '#3fb950'); termLog(` <span style="color:#3fb950">✓</span> ${selected.name} valittu — natiivisolmu lataa mallin`, '#3fb950');
termLog(` <span style="color:#8b949e">Ensimmäinen pyyntö voi kestää pidempään jos mallia ei ole ladattu</span>`);
ollamaModels.forEach(m => m.default = false); ollamaModels.forEach(m => m.default = false);
selected.default = true; selected.default = true;
} else { } else {
@@ -2876,11 +2898,16 @@ ${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
const btn = document.getElementById('agent-compute-btn'); const btn = document.getElementById('agent-compute-btn');
const wasmLoaded = btn?.dataset.state === 'ready'; const wasmLoaded = btn?.dataset.state === 'ready';
if (hw.gpu_name && hw.gpu_name !== 'ei natiivisolmua') { if (hw.gpu_name && hw.gpu_name !== 'ei natiivisolmua') {
termLog(` <span style="color:#8b949e">GPU: ${hw.gpu_name} | VRAM: ${Math.round((hw.vram_mb||0)/1024)} GB</span>`); const vram = hw.vram_mb ? ` | VRAM: ${Math.round(hw.vram_mb/1024)} GB` : '';
termLog(` <span style="color:#8b949e">${hw.gpu_name}${vram}</span>`);
}
if (loadedNames.length > 0) {
termLog(` <span style="color:#3fb950">Ollama: ${loadedNames.length} mallia ladattu</span>`);
} }
termLog(' Mallit <span style="color:#8b949e">(kpn load &lt;numero&gt;)</span>:', '#c9d1d9'); termLog(' Mallit <span style="color:#8b949e">(kpn load &lt;numero&gt;)</span>:', '#c9d1d9');
for (const m of allModels) { for (const m of allModels) {
const loaded = (m.id === '1' && wasmLoaded) || loadedNames.some(n => m.name.includes(n) || n.includes(m.name.split(':')[1])); const nameBase = m.name.split(':')[1]; // "7b", "1.5b" etc
const loaded = (m.id === '1' && wasmLoaded) || loadedNames.some(n => n.includes(nameBase));
const status = loaded ? ' <span style="color:#3fb950">✓ ladattu</span>' : ''; const status = loaded ? ' <span style="color:#3fb950">✓ ladattu</span>' : '';
termLog(` <span style="color:#58a6ff">${m.id}</span> ${m.name} <span style="color:#8b949e">${m.size} | ${m.type}</span>${status}`); termLog(` <span style="color:#58a6ff">${m.id}</span> ${m.name} <span style="color:#8b949e">${m.size} | ${m.type}</span>${status}`);
} }