Compare commits
10 Commits
560bee1369
...
v0.2.4
| Author | SHA1 | Date | |
|---|---|---|---|
| afc7f9bcee | |||
| 5d2027b2ca | |||
| 8a4d515eed | |||
| 987a370a05 | |||
| f75e7f07e9 | |||
| eb6f720fcc | |||
| e25d0ea8f2 | |||
| 4d1e89da34 | |||
| bf535b6256 | |||
| 29e1c440c6 |
@@ -5,7 +5,8 @@ RUN apt-get update && apt-get install -y \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml ./
|
||||||
|
COPY Cargo.loc[k] ./
|
||||||
COPY hub/Cargo.toml hub/Cargo.toml
|
COPY hub/Cargo.toml hub/Cargo.toml
|
||||||
COPY node/Cargo.toml node/Cargo.toml
|
COPY node/Cargo.toml node/Cargo.toml
|
||||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
services:
|
services:
|
||||||
# NVIDIA GPU -solmu
|
# Ollama NVIDIA GPU:lla
|
||||||
native-node-nvidia:
|
ollama-nvidia:
|
||||||
build:
|
image: ollama/ollama:latest
|
||||||
context: .
|
container_name: kipina-ollama
|
||||||
dockerfile: Dockerfile.native-node
|
ports:
|
||||||
container_name: kipina-node-nvidia
|
- "11434:11434"
|
||||||
environment:
|
volumes:
|
||||||
- HUB_URL=wss://kipina.studio/ws
|
- ollama-models:/root/.ollama
|
||||||
- ALLOCATED_GB=4
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
@@ -16,6 +15,65 @@ services:
|
|||||||
- driver: nvidia
|
- driver: nvidia
|
||||||
count: all
|
count: all
|
||||||
capabilities: [gpu]
|
capabilities: [gpu]
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- ollama
|
||||||
|
profiles:
|
||||||
|
- nvidia
|
||||||
|
|
||||||
|
# Ollama AMD ROCm GPU:lla
|
||||||
|
ollama-amd:
|
||||||
|
image: ollama/ollama:rocm
|
||||||
|
container_name: kipina-ollama
|
||||||
|
ports:
|
||||||
|
- "11434:11434"
|
||||||
|
volumes:
|
||||||
|
- ollama-models:/root/.ollama
|
||||||
|
restart: unless-stopped
|
||||||
|
devices:
|
||||||
|
- /dev/kfd:/dev/kfd
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
group_add:
|
||||||
|
- video
|
||||||
|
- render
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- ollama
|
||||||
|
profiles:
|
||||||
|
- amd
|
||||||
|
|
||||||
|
# Ollama CPU:lla
|
||||||
|
ollama-cpu:
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
container_name: kipina-ollama
|
||||||
|
ports:
|
||||||
|
- "11434:11434"
|
||||||
|
volumes:
|
||||||
|
- ollama-models:/root/.ollama
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- ollama
|
||||||
|
profiles:
|
||||||
|
- cpu
|
||||||
|
|
||||||
|
# NVIDIA GPU -solmu
|
||||||
|
native-node-nvidia:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.native-node
|
||||||
|
container_name: kipina-node-nvidia
|
||||||
|
environment:
|
||||||
|
- HUB_URL=wss://kipina.studio/ws
|
||||||
|
- OLLAMA_URL=http://ollama:11434
|
||||||
|
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||||
|
- ALLOCATED_GB=4
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- ollama-nvidia
|
||||||
profiles:
|
profiles:
|
||||||
- nvidia
|
- nvidia
|
||||||
|
|
||||||
@@ -27,14 +85,12 @@ services:
|
|||||||
container_name: kipina-node-amd
|
container_name: kipina-node-amd
|
||||||
environment:
|
environment:
|
||||||
- HUB_URL=wss://kipina.studio/ws
|
- HUB_URL=wss://kipina.studio/ws
|
||||||
|
- OLLAMA_URL=http://ollama:11434
|
||||||
|
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||||
- ALLOCATED_GB=4
|
- ALLOCATED_GB=4
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
devices:
|
depends_on:
|
||||||
- /dev/kfd:/dev/kfd
|
- ollama-amd
|
||||||
- /dev/dri:/dev/dri
|
|
||||||
group_add:
|
|
||||||
- video
|
|
||||||
- render
|
|
||||||
profiles:
|
profiles:
|
||||||
- amd
|
- amd
|
||||||
|
|
||||||
@@ -46,7 +102,14 @@ services:
|
|||||||
container_name: kipina-node-cpu
|
container_name: kipina-node-cpu
|
||||||
environment:
|
environment:
|
||||||
- HUB_URL=wss://kipina.studio/ws
|
- HUB_URL=wss://kipina.studio/ws
|
||||||
|
- OLLAMA_URL=http://ollama:11434
|
||||||
|
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||||
- ALLOCATED_GB=2
|
- ALLOCATED_GB=2
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- ollama-cpu
|
||||||
profiles:
|
profiles:
|
||||||
- cpu
|
- cpu
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ollama-models:
|
||||||
|
|||||||
@@ -11,21 +11,19 @@ services:
|
|||||||
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub!
|
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub!
|
||||||
command: bash -c "cd node && wasm-pack build --release --target web --out-dir ../static/pkg && cd ../hub && cargo run"
|
command: bash -c "cd node && wasm-pack build --release --target web --out-dir ../static/pkg && cd ../hub && cargo run"
|
||||||
|
|
||||||
# Ollama — LLM-inferenssi GPU:lla (NVIDIA/AMD/Apple)
|
# Ollama — LLM-inferenssi
|
||||||
|
# NVIDIA: vaihda image → ollama/ollama:latest ja lisää deploy.resources (ks. README)
|
||||||
|
# CPU: vaihda image → ollama/ollama:latest ja poista devices
|
||||||
ollama:
|
ollama:
|
||||||
image: ollama/ollama:latest
|
image: ollama/ollama:rocm
|
||||||
container_name: kipina_ollama
|
container_name: kipina_ollama
|
||||||
ports:
|
ports:
|
||||||
- "11434:11434"
|
- "11434:11434"
|
||||||
volumes:
|
volumes:
|
||||||
- ollama-models:/root/.ollama
|
- ollama-models:/root/.ollama
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
devices:
|
||||||
- driver: nvidia
|
- /dev/kfd
|
||||||
count: all
|
- /dev/dri
|
||||||
capabilities: [gpu]
|
|
||||||
profiles:
|
profiles:
|
||||||
- native
|
- native
|
||||||
|
|
||||||
@@ -36,12 +34,11 @@ services:
|
|||||||
dockerfile: Dockerfile.native-node
|
dockerfile: Dockerfile.native-node
|
||||||
container_name: kipina_native_node
|
container_name: kipina_native_node
|
||||||
environment:
|
environment:
|
||||||
- HUB_URL=ws://agentic-poc:3000/ws
|
- HUB_URL=wss://kipina.studio/ws
|
||||||
- OLLAMA_URL=http://ollama:11434
|
- OLLAMA_URL=http://ollama:11434
|
||||||
- OLLAMA_MODEL=qwen2.5-coder:7b
|
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||||
- ALLOCATED_GB=4
|
- ALLOCATED_GB=4
|
||||||
depends_on:
|
depends_on:
|
||||||
- agentic-poc
|
|
||||||
- ollama
|
- ollama
|
||||||
profiles:
|
profiles:
|
||||||
- native
|
- native
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hub"
|
name = "hub"
|
||||||
version = "0.2.2"
|
version = "0.2.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -8,17 +8,47 @@ pub struct LlmEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LlmEngine {
|
impl LlmEngine {
|
||||||
pub fn load() -> Result<Self, String> {
|
pub async fn load() -> Result<Self, String> {
|
||||||
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://localhost:11434".to_string());
|
|
||||||
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:7b".to_string());
|
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:7b".to_string());
|
||||||
|
|
||||||
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
|
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(600))
|
.timeout(std::time::Duration::from_secs(600))
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(3))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| format!("HTTP client: {}", e))?;
|
.map_err(|e| format!("HTTP client: {}", e))?;
|
||||||
|
|
||||||
|
// Jos OLLAMA_URL on asetettu, käytetään sitä suoraan
|
||||||
|
let ollama_url = if let Ok(url) = std::env::var("OLLAMA_URL") {
|
||||||
|
tracing::info!("Ollama backend (env): {}", url);
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
// Haistellaan Ollamaa tunnetuista osoitteista
|
||||||
|
let candidates = [
|
||||||
|
"http://localhost:11434",
|
||||||
|
"http://127.0.0.1:11434",
|
||||||
|
"http://ollama:11434",
|
||||||
|
"http://host.docker.internal:11434",
|
||||||
|
];
|
||||||
|
let mut found = None;
|
||||||
|
for url in &candidates {
|
||||||
|
let probe = reqwest::Client::builder()
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.build().unwrap_or(client.clone());
|
||||||
|
if let Ok(resp) = probe.get(format!("{}/api/version", url)).send().await {
|
||||||
|
if resp.status().is_success() {
|
||||||
|
tracing::info!("Ollama löytyi osoitteesta: {}", url);
|
||||||
|
found = Some(url.to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found.unwrap_or_else(|| {
|
||||||
|
tracing::warn!("Ollamaa ei löytynyt — käytetään oletusta http://localhost:11434");
|
||||||
|
"http://localhost:11434".to_string()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
|
||||||
Ok(LlmEngine { ollama_url, model: RefCell::new(model), client })
|
Ok(LlmEngine { ollama_url, model: RefCell::new(model), client })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ async fn main() {
|
|||||||
|
|
||||||
// Ollama-backend
|
// Ollama-backend
|
||||||
tracing::info!("Alustetaan Ollama-yhteyttä...");
|
tracing::info!("Alustetaan Ollama-yhteyttä...");
|
||||||
let llm = match inference::LlmEngine::load() {
|
let llm = match inference::LlmEngine::load().await {
|
||||||
Ok(engine) => {
|
Ok(engine) => {
|
||||||
// Varmistetaan malli (ollama pull) — odotetaan kunnes valmis
|
// Varmistetaan malli (ollama pull) — odotetaan kunnes valmis
|
||||||
match engine.ensure_model().await {
|
match engine.ensure_model().await {
|
||||||
|
|||||||
@@ -118,6 +118,27 @@ async fn run_ai_tensor_inference(difficulty: usize) -> String {
|
|||||||
format!("PoC {} Matmul ({}x{}) >> {}", backend_name, active_workload_size, active_workload_size, result)
|
format!("PoC {} Matmul ({}x{}) >> {}", backend_name, active_workload_size, active_workload_size, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// JS-exportti: tokenisoi tekstin ja palauttaa JSON-merkkijonon
|
||||||
|
/// Tokenizer ladataan IndexedDB:stä (täytyy olla ladattu aiemmin)
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn tokenize_js(text: String) -> Result<String, JsValue> {
|
||||||
|
let cached_tok = storage::load_from_idb("tokenizer.json").await.unwrap_or(None);
|
||||||
|
let Some(bytes) = cached_tok else {
|
||||||
|
// Yritetään ladata verkosta
|
||||||
|
let resp = reqwest::get("https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B/resolve/main/tokenizer.json").await
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Tokenizer-lataus epäonnistui: {}", e)))?;
|
||||||
|
let bytes = resp.bytes().await
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Tokenizer-lataus epäonnistui: {}", e)))?;
|
||||||
|
let _ = storage::save_to_idb("tokenizer.json", &bytes).await;
|
||||||
|
let tokenizer = tokenizers::Tokenizer::from_bytes(&bytes)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Tokenizer-parsinta: {}", e)))?;
|
||||||
|
return Ok(tokenize_text(&tokenizer, &text).to_string());
|
||||||
|
};
|
||||||
|
let tokenizer = tokenizers::Tokenizer::from_bytes(&bytes)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Tokenizer-parsinta: {}", e)))?;
|
||||||
|
Ok(tokenize_text(&tokenizer, &text).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Tokenisoi yhden tekstin ja palauttaa metriikat
|
/// Tokenisoi yhden tekstin ja palauttaa metriikat
|
||||||
fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::Value {
|
fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::Value {
|
||||||
let char_count = text.chars().count();
|
let char_count = text.chars().count();
|
||||||
|
|||||||
@@ -36,29 +36,13 @@ sanan osa, kokonainen sana tai välilyönti. Tokenisaatio tehdään
|
|||||||
BPE-algoritmilla (Byte Pair Encoding) joka oppii yleisimmät
|
BPE-algoritmilla (Byte Pair Encoding) joka oppii yleisimmät
|
||||||
merkkijonot harjoitusdatasta.
|
merkkijonot harjoitusdatasta.
|
||||||
|
|
||||||
### Esimerkki: koodi
|
|
||||||
|
|
||||||
```
|
|
||||||
"print('Hello')" → [print] [(' ] [Hello] [')] = 4 tokenia
|
|
||||||
"tulosta('Hei')" → [tul] [osta] [(' ] [He] [i] [')] = 6 tokenia
|
|
||||||
```
|
|
||||||
|
|
||||||
Koodi tokenisoidaan tehokkaasti koska `print`, `def`, `return` yms.
|
|
||||||
ovat kokonaisia tokeneita. Suomenkielinen `tulosta` joudutaan pilkkomaan
|
|
||||||
osiin koska se ei esiinny harjoitusdatassa kokonaisena.
|
|
||||||
|
|
||||||
### Esimerkki: suomi vs. englanti
|
### Esimerkki: suomi vs. englanti
|
||||||
|
|
||||||
Sama lause kahdella kielellä Qwen2.5-Coder -tokenisaattorilla:
|
Alla oikea tokenisointitulos Qwen2.5-Coder-tokenisaattorilla. Jokainen
|
||||||
|
värikoodattu lohko on yksi tokeni — huomaa miten suomi vaatii enemmän
|
||||||
|
tokeneita saman merkityksen välittämiseen:
|
||||||
|
|
||||||
| | Teksti | Tokenit | Määrä | Merkkejä/token |
|

|
||||||
|---|---|---|---|---|
|
|
||||||
| EN | The cat sat on the mat | [The] [ cat] [ sat] [ on] [ the] [ mat] | **6** | 3.7 |
|
|
||||||
| FI | Kissa istui matolla | [K] [issa] [ ist] [ui] [ mat] [olla] | **6** | 3.2 |
|
|
||||||
| EN | Distributed computing in the browser | [Dist] [ributed] [ computing] [ in] [ the] [ browser] | **6** | 6.0 |
|
|
||||||
| FI | Hajautettu laskenta selaimessa | [H] [aj] [au] [tettu] [ las] [kenta] [ sel] [aim] [essa] | **9** | 3.3 |
|
|
||||||
| EN | Write a function that sorts a list | [Write] [ a] [ function] [ that] [ sorts] [ a] [ list] | **7** | 5.0 |
|
|
||||||
| FI | Kirjoita funktio joka lajittelee listan | [K] [irj] [oita] [ funkt] [io] [ joka] [ laj] [ittel] [ee] [ listan] | **10** | 4.0 |
|
|
||||||
|
|
||||||
**Huomaa miten:**
|
**Huomaa miten:**
|
||||||
- Englannin yleiset sanat (`the`, `in`, `a`, `function`) ovat kokonaisia tokeneita
|
- Englannin yleiset sanat (`the`, `in`, `a`, `function`) ovat kokonaisia tokeneita
|
||||||
|
|||||||
BIN
network-poc/static/images/tokenization-example.png
Normal file
BIN
network-poc/static/images/tokenization-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -463,8 +463,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar-card img {
|
.avatar-card img {
|
||||||
width: 48px;
|
width: 50px;
|
||||||
height: 48px;
|
height: 50px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
border: 2px solid rgba(240, 246, 252, 0.1);
|
border: 2px solid rgba(240, 246, 252, 0.1);
|
||||||
@@ -2840,26 +2840,62 @@ ${fixableFiles}`;
|
|||||||
const agentNames = { manager: 'Manageri', coder: 'Koodari', tester: 'DevOps', qa: 'QA', data: 'Data' };
|
const agentNames = { manager: 'Manageri', coder: 'Koodari', tester: 'DevOps', qa: 'QA', data: 'Data' };
|
||||||
const agentColors = { manager: '#d29922', coder: '#3fb950', tester: '#58a6ff', qa: '#a371f7', data: '#d2a8ff' };
|
const agentColors = { manager: '#d29922', coder: '#3fb950', tester: '#58a6ff', qa: '#a371f7', data: '#d2a8ff' };
|
||||||
|
|
||||||
|
// Syntaksikorostus: kevyt regex-pohjainen highlighter
|
||||||
|
function highlightCode(code, filename) {
|
||||||
|
let h = code.replace(/&/g,'&').replace(/</g,'<');
|
||||||
|
if (filename.endsWith('.py') || filename.endsWith('.toml') || filename.endsWith('.yml') || filename.endsWith('.yaml')) {
|
||||||
|
// Kommentit
|
||||||
|
h = h.replace(/(#[^\n]*)/g, '<span style="color:#8b949e;font-style:italic">$1</span>');
|
||||||
|
// Stringit (kolmois- ja yksittäiset)
|
||||||
|
h = h.replace(/("""[\s\S]*?"""|'''[\s\S]*?''')/g, '<span style="color:#a5d6ff">$1</span>');
|
||||||
|
h = h.replace(/((?<![\\])(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'))/g, '<span style="color:#a5d6ff">$1</span>');
|
||||||
|
// Avainsanat
|
||||||
|
h = h.replace(/\b(def|class|import|from|return|if|elif|else|for|in|while|try|except|finally|with|as|raise|yield|async|await|True|False|None|not|and|or|is|lambda)\b/g, '<span style="color:#ff7b72">$1</span>');
|
||||||
|
// Dekoraattorit
|
||||||
|
h = h.replace(/^(\s*@\w+)/gm, '<span style="color:#d2a8ff">$1</span>');
|
||||||
|
// Numerot
|
||||||
|
h = h.replace(/\b(\d+\.?\d*)\b/g, '<span style="color:#79c0ff">$1</span>');
|
||||||
|
} else if (filename.endsWith('.html')) {
|
||||||
|
h = h.replace(/(<\/?[\w-]+)/g, '<span style="color:#7ee787">$1</span>');
|
||||||
|
h = h.replace(/([\w-]+)=/g, '<span style="color:#79c0ff">$1</span>=');
|
||||||
|
h = h.replace(/((?<!=)"[^"]*")/g, '<span style="color:#a5d6ff">$1</span>');
|
||||||
|
} else if (filename === 'Dockerfile') {
|
||||||
|
h = h.replace(/^(FROM|RUN|COPY|WORKDIR|EXPOSE|CMD|ENV|ARG|ENTRYPOINT|ADD|VOLUME|LABEL|USER)/gm, '<span style="color:#ff7b72">$1</span>');
|
||||||
|
h = h.replace(/(#[^\n]*)/g, '<span style="color:#8b949e;font-style:italic">$1</span>');
|
||||||
|
h = h.replace(/((?<!=)"[^"]*")/g, '<span style="color:#a5d6ff">$1</span>');
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
const stepsHtml = steps.map((s, i) => {
|
const stepsHtml = steps.map((s, i) => {
|
||||||
const color = agentColors[s.agent] || '#8b949e';
|
const color = agentColors[s.agent] || '#8b949e';
|
||||||
const icon = s.status === 'done' ? '✓' : '◷';
|
const icon = s.status === 'done' ? '✓' : '◷';
|
||||||
const outputPreview = (s.output || '').substring(0, 500);
|
const outputPreview = (s.output || '').substring(0, 500);
|
||||||
|
const highlighted = outputPreview ? highlightCode(outputPreview, s.label) : '';
|
||||||
return `
|
return `
|
||||||
<div style="margin-bottom:16px;border:1px solid #30363d;border-radius:6px;overflow:hidden">
|
<div style="margin-bottom:12px;border:1px solid #30363d;border-radius:8px;overflow:hidden">
|
||||||
<div style="background:#161b22;padding:8px 12px;display:flex;justify-content:space-between;align-items:center">
|
<div style="background:#161b22;padding:10px 14px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #21262d">
|
||||||
<span><span style="color:${s.status === 'done' ? '#3fb950' : '#d29922'}">${icon}</span> <strong style="color:${color}">${agentNames[s.agent] || s.agent}</strong> — ${s.label}</span>
|
<span><span style="color:${s.status === 'done' ? '#3fb950' : '#d29922'};font-size:14px">${icon}</span> <strong style="color:${color}">${agentNames[s.agent] || s.agent}</strong> <span style="color:#8b949e">—</span> ${s.label}</span>
|
||||||
<span style="color:#8b949e;font-size:12px">Vaihe ${i + 1}</span>
|
<span style="color:#484f58;font-size:11px;background:#0d1117;padding:2px 8px;border-radius:10px">Vaihe ${i + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
${s.input ? `<details><summary style="padding:6px 12px;color:#8b949e;font-size:12px;cursor:pointer">Prompti</summary><pre style="margin:0;padding:8px 12px;background:#010409;font-size:11px;overflow-x:auto;white-space:pre-wrap;color:#8b949e">${s.input.replace(/</g,'<').substring(0, 1000)}</pre></details>` : ''}
|
${s.input ? `<details><summary style="padding:6px 14px;color:#8b949e;font-size:12px;cursor:pointer;border-bottom:1px solid #21262d">▸ Prompti</summary><pre style="margin:0;padding:10px 14px;background:#010409;font-size:11px;overflow-x:auto;white-space:pre-wrap;color:#8b949e;line-height:1.5">${s.input.replace(/</g,'<').substring(0, 1000)}</pre></details>` : ''}
|
||||||
${outputPreview ? `<pre style="margin:0;padding:8px 12px;background:#0d1117;font-size:12px;overflow-x:auto;white-space:pre-wrap;color:#c9d1d9">${outputPreview.replace(/</g,'<')}</pre>` : ''}
|
${highlighted ? `<pre style="margin:0;padding:10px 14px;background:#0d1117;font-size:12px;overflow-x:auto;white-space:pre-wrap;color:#c9d1d9;line-height:1.6">${highlighted}</pre>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const filesHtml = fileEntries.map(([name, content]) => `
|
const filesHtml = fileEntries.map(([name, content]) => {
|
||||||
<div style="margin-bottom:16px;border:1px solid #30363d;border-radius:6px;overflow:hidden">
|
const ext = name.split('.').pop();
|
||||||
<div style="background:#161b22;padding:8px 12px;font-weight:600;color:#58a6ff">${name}</div>
|
const langLabel = {py:'Python',html:'HTML',toml:'TOML',yml:'YAML',yaml:'YAML',md:'Markdown'}[ext] || ext.toUpperCase();
|
||||||
<pre style="margin:0;padding:12px;background:#010409;font-size:12px;overflow-x:auto;white-space:pre-wrap;color:#c9d1d9">${(content || '').replace(/</g,'<')}</pre>
|
const lines = (content || '').split('\\n').length;
|
||||||
</div>`).join('');
|
return `
|
||||||
|
<div style="margin-bottom:12px;border:1px solid #30363d;border-radius:8px;overflow:hidden">
|
||||||
|
<div style="background:#161b22;padding:8px 14px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #21262d">
|
||||||
|
<span style="font-weight:600;color:#58a6ff">${name}</span>
|
||||||
|
<span style="color:#484f58;font-size:11px">${langLabel} · ${lines} riviä</span>
|
||||||
|
</div>
|
||||||
|
<pre style="margin:0;padding:12px 14px;background:#010409;font-size:12px;overflow-x:auto;white-space:pre-wrap;color:#c9d1d9;line-height:1.6">${highlightCode(content || '', name)}</pre>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
const staticHtml = (staticIssues || []).length > 0
|
const staticHtml = (staticIssues || []).length > 0
|
||||||
? `<div style="margin-bottom:16px;padding:12px;background:#1c1206;border:1px solid #d29922;border-radius:6px">
|
? `<div style="margin-bottom:16px;padding:12px;background:#1c1206;border:1px solid #d29922;border-radius:6px">
|
||||||
@@ -2876,14 +2912,18 @@ ${fixableFiles}`;
|
|||||||
<title>Kipinä Raportti — ${task}</title>
|
<title>Kipinä Raportti — ${task}</title>
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #0d1117; color: #c9d1d9; padding: 20px; max-width: 900px; margin: 0 auto; }
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; max-width: 960px; margin: 0 auto; line-height: 1.5; }
|
||||||
h1 { color: #58a6ff; margin-bottom: 4px; }
|
h1 { color: #f0f6fc; margin-bottom: 4px; font-size: 24px; }
|
||||||
h2 { color: #c9d1d9; margin: 24px 0 12px; border-bottom: 1px solid #30363d; padding-bottom: 6px; }
|
h2 { color: #c9d1d9; margin: 28px 0 14px; border-bottom: 1px solid #30363d; padding-bottom: 8px; font-size: 18px; }
|
||||||
h3 { color: #8b949e; margin: 16px 0 8px; }
|
h3 { color: #8b949e; margin: 16px 0 8px; }
|
||||||
pre { font-family: 'Courier New', monospace; }
|
pre { font-family: ui-monospace, "Cascadia Code", "SF Mono", Menlo, monospace; tab-size: 4; }
|
||||||
a { color: #58a6ff; }
|
a { color: #58a6ff; }
|
||||||
details summary { list-style: none; }
|
details summary { list-style: none; cursor: pointer; }
|
||||||
details summary::-webkit-details-marker { display: none; }
|
details summary::-webkit-details-marker { display: none; }
|
||||||
|
details[open] summary { color: #58a6ff; }
|
||||||
|
.sl-badge { display: inline-block; border: 1px solid; border-radius: 6px; padding: 3px 10px; margin: 2px; font-size: 11px; cursor: help; white-space: nowrap; position: relative; transition: transform 0.15s; }
|
||||||
|
.sl-badge:hover { transform: translateY(-1px); filter: brightness(1.3); }
|
||||||
|
.sl-badge[data-tip]:hover::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: #1c2028; border: 1px solid #30363d; border-radius: 6px; padding: 8px 12px; font-size: 11px; color: #c9d1d9; white-space: pre-wrap; max-width: 350px; min-width: 180px; z-index: 10; box-shadow: 0 4px 12px rgba(0,0,0,0.5); pointer-events: none; line-height: 1.5; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -2903,7 +2943,7 @@ ${staticHtml}
|
|||||||
${filesHtml}
|
${filesHtml}
|
||||||
|
|
||||||
<hr style="border-color:#30363d;margin:24px 0">
|
<hr style="border-color:#30363d;margin:24px 0">
|
||||||
<p style="color:#8b949e;font-size:12px">Generoitu Kipinä Agentic Playground v0.2.2 — <a href="https://kipina.studio">kipina.studio</a></p>
|
<p style="color:#8b949e;font-size:12px">Generoitu Kipinä Agentic Playground v0.2.4 — <a href="https://kipina.studio">kipina.studio</a></p>
|
||||||
</body></html>`;
|
</body></html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2932,16 +2972,23 @@ ${filesHtml}
|
|||||||
for (var bi = 0; bi < aSteps.length; bi++) {
|
for (var bi = 0; bi < aSteps.length; bi++) {
|
||||||
var st = aSteps[bi];
|
var st = aSteps[bi];
|
||||||
var desc = stepDescs[st.label] || st.label;
|
var desc = stepDescs[st.label] || st.label;
|
||||||
var outPrev = (st.output || '').substring(0, 100).replace(/</g, '<').replace(/\n/g, ' ');
|
var outPrev = (st.output || '').substring(0, 150).replace(/</g, '<').replace(/"/g, '"');
|
||||||
var tip = desc + (outPrev ? ' — ' + outPrev : '');
|
// Rivitys tooltipiin: jokainen rivi omalle rivilleen
|
||||||
var icon = st.status === 'done' ? '✓' : '◷';
|
var tipLines = [desc];
|
||||||
if (bi > 0) badges += '<span style="color:#30363d;margin:0 1px">→</span>';
|
if (outPrev) {
|
||||||
badges += '<span title="' + tip.replace(/"/g, '"') + '" style="display:inline-block;background:' + bg + ';border:1px solid ' + color + ';border-radius:4px;padding:3px 8px;margin:2px;font-size:11px;color:' + color + ';cursor:help;white-space:nowrap">' + icon + ' ' + st.label.replace(/</g, '<') + '</span>';
|
tipLines.push('');
|
||||||
|
var outLines = outPrev.split(/\n/).slice(0, 6);
|
||||||
|
for (var li = 0; li < outLines.length; li++) tipLines.push(outLines[li]);
|
||||||
|
if ((st.output || '').split(/\n/).length > 6) tipLines.push('...');
|
||||||
}
|
}
|
||||||
rows += '<div style="display:flex;align-items:center;gap:8px;margin:4px 0"><span style="min-width:70px;font-weight:600;font-size:12px;color:' + color + '">' + label + '</span><div style="display:flex;flex-wrap:wrap;align-items:center">' + badges + '</div></div>';
|
var icon = st.status === 'done' ? '✓' : '◷';
|
||||||
|
if (bi > 0) badges += '<span style="color:#484f58;margin:0 2px">→</span>';
|
||||||
|
badges += '<span class="sl-badge" style="background:' + bg + ';border-color:' + color + ';color:' + color + '" data-tip="' + tipLines.join(' ') + '">' + icon + ' ' + st.label.replace(/</g, '<') + '</span>';
|
||||||
|
}
|
||||||
|
rows += '<div style="display:flex;align-items:center;gap:10px;margin:5px 0;padding:4px 0;border-bottom:1px solid #21262d"><span style="min-width:70px;font-weight:600;font-size:12px;color:' + color + '">' + label + '</span><div style="display:flex;flex-wrap:wrap;align-items:center;gap:2px">' + badges + '</div></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '<div style="background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:12px">' + rows + '</div>';
|
return '<div style="background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:14px 16px">' + rows + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yksinkertainen pipeline (vanha: manageri → koodari → testaaja)
|
// Yksinkertainen pipeline (vanha: manageri → koodari → testaaja)
|
||||||
@@ -4517,6 +4564,7 @@ ${filesHtml}
|
|||||||
|
|
||||||
function inlineFormat(text) {
|
function inlineFormat(text) {
|
||||||
return text
|
return text
|
||||||
|
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;border-radius:8px;border:1px solid #30363d;margin:12px 0;display:block">')
|
||||||
.replace(/`([^`]+)`/g, '<code style="background:#161b22;padding:2px 6px;border-radius:3px;font-size:13px;color:#e6edf3">$1</code>')
|
.replace(/`([^`]+)`/g, '<code style="background:#161b22;padding:2px 6px;border-radius:3px;font-size:13px;color:#e6edf3">$1</code>')
|
||||||
.replace(/\*\*([^*]+)\*\*/g, '<strong style="color:#e6edf3">$1</strong>')
|
.replace(/\*\*([^*]+)\*\*/g, '<strong style="color:#e6edf3">$1</strong>')
|
||||||
.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||||
|
|||||||
Reference in New Issue
Block a user