uusi projekti
This commit is contained in:
131
kipina-node
Executable file
131
kipina-node
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Kipinä Node — lataa oikea binääri ja käynnistä
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BASE_URL="https://kipina.studio/download"
|
||||||
|
HUB_URL="${KIPINA_HUB:-wss://kipina.studio/ws}"
|
||||||
|
OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}"
|
||||||
|
|
||||||
|
# Tunnista OS ja arkkitehtuuri
|
||||||
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
|
||||||
|
case "$OS-$ARCH" in
|
||||||
|
darwin-arm64) BINARY="kipina-node-macos-arm64" ;;
|
||||||
|
darwin-x86_64) BINARY="kipina-node-macos-arm64" ;; # Rosetta
|
||||||
|
linux-x86_64) BINARY="kipina-node-linux-x86_64" ;;
|
||||||
|
linux-aarch64) BINARY="kipina-node-linux-arm64" ;;
|
||||||
|
*) echo "Ei tuettu: $OS-$ARCH"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ╔══════════════════════════════════════╗"
|
||||||
|
echo " ║ Kipinä Agentic Node ║"
|
||||||
|
echo " ╚══════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
echo " OS: $OS ($ARCH)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Etsi Ollama-instanssit
|
||||||
|
CANDIDATES=(
|
||||||
|
"http://localhost:11434"
|
||||||
|
"http://127.0.0.1:11434"
|
||||||
|
"http://ollama:11434"
|
||||||
|
"http://host.docker.internal:11434"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Lisää OLLAMA_URL listaan jos asetettu ja ei jo mukana
|
||||||
|
if [ -n "$OLLAMA_URL" ]; then
|
||||||
|
ALREADY=false
|
||||||
|
for c in "${CANDIDATES[@]}"; do
|
||||||
|
[ "$c" = "$OLLAMA_URL" ] && ALREADY=true
|
||||||
|
done
|
||||||
|
$ALREADY || CANDIDATES=("$OLLAMA_URL" "${CANDIDATES[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Etsitään Ollama-instansseja..."
|
||||||
|
FOUND=()
|
||||||
|
for url in "${CANDIDATES[@]}"; do
|
||||||
|
if curl -s --connect-timeout 1 "$url/api/tags" &>/dev/null; then
|
||||||
|
FOUND+=("$url")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#FOUND[@]} -eq 0 ]; then
|
||||||
|
# Ei löytynyt — yritä käynnistää lokaali
|
||||||
|
if command -v ollama &>/dev/null; then
|
||||||
|
echo " Käynnistetään Ollama..."
|
||||||
|
ollama serve &>/dev/null &
|
||||||
|
sleep 3
|
||||||
|
if curl -s --connect-timeout 1 "http://localhost:11434/api/tags" &>/dev/null; then
|
||||||
|
OLLAMA_URL="http://localhost:11434"
|
||||||
|
echo " ✓ Ollama käynnistetty ($OLLAMA_URL)"
|
||||||
|
else
|
||||||
|
echo " ✗ Ollaman käynnistys epäonnistui."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo " ✗ Ollamaa ei löytynyt."
|
||||||
|
echo " Kontti/remote: OLLAMA_URL=http://HOST:11434 ./kipina-node"
|
||||||
|
echo " Asenna: curl -fsSL https://ollama.ai/install.sh | sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ ${#FOUND[@]} -eq 1 ]; then
|
||||||
|
OLLAMA_URL="${FOUND[0]}"
|
||||||
|
echo " ✓ Ollama löytyi: $OLLAMA_URL"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo " Löytyi ${#FOUND[@]} Ollama-instanssia:"
|
||||||
|
echo ""
|
||||||
|
for i in "${!FOUND[@]}"; do
|
||||||
|
echo " $((i+1))) ${FOUND[$i]}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
read -p " Valitse [1-${#FOUND[@]}]: " -r CHOICE
|
||||||
|
if [[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le ${#FOUND[@]} ]; then
|
||||||
|
OLLAMA_URL="${FOUND[$((CHOICE-1))]}"
|
||||||
|
else
|
||||||
|
OLLAMA_URL="${FOUND[0]}"
|
||||||
|
echo " Käytetään oletusta: $OLLAMA_URL"
|
||||||
|
fi
|
||||||
|
echo " ✓ Valittu: $OLLAMA_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " Hub: $HUB_URL"
|
||||||
|
echo " Ollama: $OLLAMA_URL"
|
||||||
|
if [ -n "$KIPINA_MODEL" ]; then
|
||||||
|
echo " Malli: $KIPINA_MODEL (Ympäristömuuttujasta)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lataa binääri
|
||||||
|
BIN_PATH="./kipina-node-bin"
|
||||||
|
if [ -f "$BIN_PATH" ]; then
|
||||||
|
echo ""
|
||||||
|
read -p " Löydettiin vanha kipina-node-bin lokaalisti. Haluatko poistaa sen ja ladata uusimman version? [Y/n] " -r DEL_CHOICE
|
||||||
|
if [[ "$DEL_CHOICE" =~ ^[Nn]$ ]]; then
|
||||||
|
echo " ✓ Käytetään lokaalia versiota."
|
||||||
|
else
|
||||||
|
rm -f "$BIN_PATH"
|
||||||
|
echo " ✓ Vanha binääri poistettu ja korvataan uudella."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$BIN_PATH" ]; then
|
||||||
|
echo " Ladataan tuorein $BINARY..."
|
||||||
|
curl -sSL "$BASE_URL/$BINARY" -o "$BIN_PATH"
|
||||||
|
chmod +x "$BIN_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ✓ Siirrytään Kipinä Noden hallintaan..."
|
||||||
|
echo " Ctrl+C pysäyttää"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -n "$KIPINA_MODEL" ]; then
|
||||||
|
export OLLAMA_MODEL="$KIPINA_MODEL"
|
||||||
|
fi
|
||||||
|
export HUB_URL="$HUB_URL"
|
||||||
|
export OLLAMA_URL="$OLLAMA_URL"
|
||||||
|
exec "$BIN_PATH"
|
||||||
BIN
kipina-node-bin
Executable file
BIN
kipina-node-bin
Executable file
Binary file not shown.
@@ -1 +1 @@
|
|||||||
5f005820535910a5052a33cfcfc0bd6909d11c25
|
dirty-3e9cdd70c60dadfb970cee47ebbd912c
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
33
network-poc/frontend/public/templates/data-analytics.json
Normal file
33
network-poc/frontend/public/templates/data-analytics.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "Data Analytics Pipeline",
|
||||||
|
"description": "ETL, analysis, and visualization with Docker (MariaDB + Jupyter)",
|
||||||
|
"keywords": ["data", "analytics", "csv", "etl", "visualization", "statistics", "dashboard", "jupyter", "pandas", "matplotlib"],
|
||||||
|
"files": {
|
||||||
|
"etl.py": {
|
||||||
|
"description": "Data loading, cleaning, and transformation",
|
||||||
|
"example": "import pandas as pd\nfrom pathlib import Path\nfrom sqlalchemy import create_engine\n\nDB_URL = \"mysql+pymysql://root:secret@localhost:3306/analytics\"\nengine = create_engine(DB_URL)\n\ndef load_csv(path: str) -> pd.DataFrame:\n df = pd.read_csv(path)\n print(f\"Loaded {len(df)} rows from {path}\")\n return df\n\ndef clean(df: pd.DataFrame) -> pd.DataFrame:\n df = df.dropna(subset=[\"x\", \"y\"])\n df = df[(df[\"x\"] >= 0) & (df[\"y\"] >= 0)] # Remove outliers\n df[\"timestamp\"] = pd.to_datetime(df[\"timestamp\"])\n return df.sort_values(\"timestamp\").reset_index(drop=True)\n\ndef to_database(df: pd.DataFrame, table: str):\n df.to_sql(table, engine, if_exists=\"replace\", index=False)\n print(f\"Wrote {len(df)} rows to {table}\")\n\nif __name__ == \"__main__\":\n for csv_file in sorted(Path(\"data\").glob(\"*.csv\")):\n df = load_csv(str(csv_file))\n df = clean(df)\n to_database(df, \"measurements\")",
|
||||||
|
"instructions": "Write the ETL pipeline:\n- Load CSV files from data/ directory using pandas\n- Clean: remove nulls, filter outliers, parse timestamps\n- Transform: convert units, compute derived columns\n- Load into MariaDB via SQLAlchemy\n- Make it runnable as a standalone script"
|
||||||
|
},
|
||||||
|
"analysis.py": {
|
||||||
|
"description": "Statistical analysis and metrics computation",
|
||||||
|
"example": "import pandas as pd\nfrom sqlalchemy import create_engine\n\nDB_URL = \"mysql+pymysql://root:secret@localhost:3306/analytics\"\nengine = create_engine(DB_URL)\n\ndef load_data() -> pd.DataFrame:\n return pd.read_sql(\"SELECT * FROM measurements\", engine)\n\ndef summary_stats(df: pd.DataFrame) -> dict:\n return {\n \"total_rows\": len(df),\n \"date_range\": f\"{df['timestamp'].min()} to {df['timestamp'].max()}\",\n \"unique_entities\": df[\"entity_id\"].nunique(),\n }\n\ndef hourly_distribution(df: pd.DataFrame) -> pd.DataFrame:\n df[\"hour\"] = df[\"timestamp\"].dt.hour\n return df.groupby(\"hour\").size().reset_index(name=\"count\")\n\nif __name__ == \"__main__\":\n df = load_data()\n stats = summary_stats(df)\n for k, v in stats.items():\n print(f\"{k}: {v}\")",
|
||||||
|
"instructions": "Write analysis functions:\n- Load cleaned data from MariaDB\n- Compute summary statistics (counts, date ranges, distributions)\n- Time-based analysis (hourly, daily, weekly patterns)\n- Group-level metrics (per entity, per zone)\n- Return DataFrames and dicts suitable for visualization"
|
||||||
|
},
|
||||||
|
"visualize.py": {
|
||||||
|
"description": "Charts and visualizations with matplotlib",
|
||||||
|
"example": "import matplotlib.pyplot as plt\nimport pandas as pd\nfrom analysis import load_data, hourly_distribution\n\ndef plot_heatmap(df: pd.DataFrame, title: str, output: str):\n fig, ax = plt.subplots(figsize=(12, 8))\n scatter = ax.scatter(df[\"x\"], df[\"y\"], c=df[\"density\"], cmap=\"hot\", alpha=0.5, s=2)\n ax.set_title(title)\n ax.set_xlabel(\"x\")\n ax.set_ylabel(\"y\")\n ax.invert_yaxis()\n plt.colorbar(scatter, label=\"Density\")\n plt.tight_layout()\n plt.savefig(output, dpi=150)\n print(f\"Saved {output}\")\n\ndef plot_bar(df: pd.DataFrame, x: str, y: str, title: str, output: str):\n fig, ax = plt.subplots(figsize=(10, 5))\n ax.bar(df[x], df[y], color=\"steelblue\")\n ax.set_title(title)\n ax.set_xlabel(x)\n ax.set_ylabel(y)\n plt.tight_layout()\n plt.savefig(output, dpi=150)\n\nif __name__ == \"__main__\":\n df = load_data()\n hourly = hourly_distribution(df)\n plot_bar(hourly, \"hour\", \"count\", \"Hourly Distribution\", \"output/hourly.png\")",
|
||||||
|
"instructions": "Write visualization functions:\n- Import analysis functions for data\n- Heatmaps, bar charts, line charts as appropriate\n- Save figures to output/ directory (PNG, 150 DPI)\n- Use matplotlib with clear titles, labels, colorbars\n- Make it runnable as standalone to generate all charts"
|
||||||
|
},
|
||||||
|
"docker-compose.yml": {
|
||||||
|
"description": "Docker Compose stack for database and Jupyter",
|
||||||
|
"example": "services:\n db:\n image: mariadb:11\n environment:\n MYSQL_ROOT_PASSWORD: secret\n MYSQL_DATABASE: analytics\n ports:\n - \"3306:3306\"\n volumes:\n - db_data:/var/lib/mysql\n\n jupyter:\n image: jupyter/scipy-notebook:latest\n ports:\n - \"8888:8888\"\n volumes:\n - .:/home/jovyan/work\n environment:\n JUPYTER_TOKEN: kipina\n depends_on:\n - db\n\nvolumes:\n db_data:",
|
||||||
|
"instructions": "Write docker-compose.yml:\n- MariaDB service with persistent volume\n- JupyterLab service with project mounted\n- Correct environment variables\n- Port mappings for local development\n- Write ONLY the YAML, no explanations"
|
||||||
|
},
|
||||||
|
"pyproject.toml": {
|
||||||
|
"description": "Project dependencies",
|
||||||
|
"example": "[project]\nname = \"analytics\"\nversion = \"0.1.0\"\nrequires-python = \">=3.11\"\ndependencies = [\n \"pandas\",\n \"matplotlib\",\n \"sqlalchemy\",\n \"pymysql\",\n]\n\n[project.scripts]\netl = \"python etl.py\"\nanalyze = \"python analysis.py\"\nvisualize = \"python visualize.py\"",
|
||||||
|
"instructions": "Use [project] format (PEP 621). List all data science dependencies. Add scripts for ETL, analysis, and visualization."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"order": ["etl.py", "analysis.py", "visualize.py", "docker-compose.yml", "pyproject.toml"]
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "FastAPI CRUD",
|
"name": "FastAPI CRUD",
|
||||||
"description": "REST API with SQLite database",
|
"description": "REST API with SQLite database",
|
||||||
|
"keywords": ["api", "rest", "crud", "endpoint", "fastapi", "web", "backend", "server", "database", "sqlite"],
|
||||||
"files": {
|
"files": {
|
||||||
"models.py": {
|
"models.py": {
|
||||||
"description": "SQLAlchemy models, engine, and session",
|
"description": "SQLAlchemy models, engine, and session",
|
||||||
|
|||||||
@@ -501,10 +501,16 @@ OUTPUT FORMAT:
|
|||||||
|
|
||||||
// Wasm-autostart vain jos natiivisolmua ei löydy (tarkistetaan onopen:ssa)
|
// Wasm-autostart vain jos natiivisolmua ei löydy (tarkistetaan onopen:ssa)
|
||||||
|
|
||||||
|
// === Pipeline-keskeytys ===
|
||||||
|
let pipelineAbort = null; // AbortController tai null
|
||||||
|
|
||||||
// === kpnRun: lähettää promptin mallille ===
|
// === kpnRun: lähettää promptin mallille ===
|
||||||
const activeStreams = {};
|
const activeStreams = {};
|
||||||
|
|
||||||
async function kpnRun(model, prompt, silent, agentOpts) {
|
async function kpnRun(model, prompt, silent, agentOpts) {
|
||||||
|
// Tarkistetaan keskeytys
|
||||||
|
if (pipelineAbort?.signal?.aborted) return null;
|
||||||
|
|
||||||
const taskId = crypto.randomUUID();
|
const taskId = crypto.randomUUID();
|
||||||
const statusDiv = document.createElement('div');
|
const statusDiv = document.createElement('div');
|
||||||
statusDiv.className = 'terminal-line';
|
statusDiv.className = 'terminal-line';
|
||||||
@@ -514,10 +520,6 @@ OUTPUT FORMAT:
|
|||||||
termPanel.scrollTop = termPanel.scrollHeight;
|
termPanel.scrollTop = termPanel.scrollHeight;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ei odotetaan Wasmia — lähetetään suoraan hubille.
|
|
||||||
// Jos hub löytää natiivisolmun, vastaus tulee nopeasti.
|
|
||||||
// Jos 503, käynnistetään Wasm-fallback.
|
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
const streamDiv = document.createElement('div');
|
const streamDiv = document.createElement('div');
|
||||||
streamDiv.className = 'terminal-line';
|
streamDiv.className = 'terminal-line';
|
||||||
@@ -535,18 +537,18 @@ OUTPUT FORMAT:
|
|||||||
model,
|
model,
|
||||||
prompt,
|
prompt,
|
||||||
task_id: taskId,
|
task_id: taskId,
|
||||||
system_prompt: opts.systemPrompt || settings.systemPrompt || undefined,
|
system_prompt: opts.prompt || settings.systemPrompt || undefined,
|
||||||
temperature: opts.temperature ?? settings.temperature ?? undefined,
|
temperature: opts.temperature ?? settings.temperature ?? undefined,
|
||||||
top_k: opts.topK ?? settings.topK ?? undefined,
|
top_k: opts.topK ?? settings.topK ?? undefined,
|
||||||
max_tokens: opts.maxTokens ?? settings.maxTokens ?? undefined,
|
max_tokens: opts.maxTokens ?? settings.maxTokens ?? undefined,
|
||||||
repeat_penalty: opts.repeatPenalty ?? settings.repeatPenalty ?? undefined,
|
repeat_penalty: opts.repeatPenalty ?? settings.repeatPenalty ?? undefined,
|
||||||
stop: settings.stopSequences ? settings.stopSequences.split('\\n').filter(Boolean) : undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch('/api/v1/chat/completions', {
|
const res = await fetch('/api/v1/chat/completions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
|
signal: pipelineAbort?.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 503 && !wasmNodeStarted) {
|
if (res.status === 503 && !wasmNodeStarted) {
|
||||||
@@ -619,7 +621,7 @@ OUTPUT FORMAT:
|
|||||||
const kpnExamples = {
|
const kpnExamples = {
|
||||||
'kpn run coder': ['"hello world in python"','"fibonacci in rust"','"quicksort in javascript"'],
|
'kpn run coder': ['"hello world in python"','"fibonacci in rust"','"quicksort in javascript"'],
|
||||||
'kpn run coder-3b': ['"REST API with Flask"','"binary search tree"'],
|
'kpn run coder-3b': ['"REST API with Flask"','"binary search tree"'],
|
||||||
'kpn project': ['"FastAPI + SQLite REST API"','"CLI tool for CSV processing"'],
|
'kpn project': ['"FastAPI + SQLite REST API"','"UWB indoor positioning analytics — CSV cart tracking data, heatmaps, statistics, Docker (MariaDB + Jupyter)"','"CLI tool for CSV processing"'],
|
||||||
'kpn pipeline': ['"todo-sovellus"','"laskin pythonilla"'],
|
'kpn pipeline': ['"todo-sovellus"','"laskin pythonilla"'],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -753,15 +755,30 @@ OUTPUT FORMAT:
|
|||||||
|
|
||||||
// === Template-pohjainen projektipipeline ===
|
// === Template-pohjainen projektipipeline ===
|
||||||
let templates = {};
|
let templates = {};
|
||||||
|
const TEMPLATE_FILES = ['fastapi-crud.json', 'data-analytics.json'];
|
||||||
|
|
||||||
// Ladataan mallipohjat
|
// Ladataan mallipohjat
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
for (const file of TEMPLATE_FILES) {
|
||||||
const res = await fetch('/templates/fastapi-crud.json');
|
try {
|
||||||
if (res.ok) { const t = await res.json(); templates[t.name] = t; }
|
const res = await fetch(`/templates/${file}`);
|
||||||
} catch(e) {}
|
if (res.ok) { const t = await res.json(); templates[t.name] = t; }
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// Valitaan mallipohja Asiakkaan briefin perusteella (keywords-match)
|
||||||
|
function selectTemplate(brief) {
|
||||||
|
const lower = brief.toLowerCase();
|
||||||
|
let best = null, bestScore = 0;
|
||||||
|
for (const t of Object.values(templates)) {
|
||||||
|
const keywords = t.keywords || [];
|
||||||
|
const score = keywords.filter(k => lower.includes(k)).length;
|
||||||
|
if (score > bestScore) { bestScore = score; best = t; }
|
||||||
|
}
|
||||||
|
return best; // null = vapaa tila
|
||||||
|
}
|
||||||
|
|
||||||
function explainStep(title, explanation) {
|
function explainStep(title, explanation) {
|
||||||
termLog(`\n <span style="color:#a371f7;font-size:12px">💡 ${esc(title)}</span>`);
|
termLog(`\n <span style="color:#a371f7;font-size:12px">💡 ${esc(title)}</span>`);
|
||||||
termLog(` <span style="color:#8b949e;font-size:12px">${esc(explanation)}</span>`);
|
termLog(` <span style="color:#8b949e;font-size:12px">${esc(explanation)}</span>`);
|
||||||
@@ -769,18 +786,11 @@ OUTPUT FORMAT:
|
|||||||
|
|
||||||
async function kpnProject(task) {
|
async function kpnProject(task) {
|
||||||
const cli = agents.client || Object.values(agents)[0];
|
const cli = agents.client || Object.values(agents)[0];
|
||||||
|
const mgr = agents.manager || Object.values(agents)[1];
|
||||||
const cdr = agents.coder || Object.values(agents)[2];
|
const cdr = agents.coder || Object.values(agents)[2];
|
||||||
|
|
||||||
// Etsitään sopivin mallipohja
|
|
||||||
const template = Object.values(templates)[0]; // Toistaiseksi vain FastAPI CRUD
|
|
||||||
if (!template) {
|
|
||||||
termLog(' ✗ Mallipohjia ei ladattu', '#f85149');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ ${esc(template.name)} — ${esc(task)} ━━━</span>`);
|
|
||||||
|
|
||||||
// Asiakas: jalostaa vaatimukset
|
// Asiakas: jalostaa vaatimukset
|
||||||
|
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Projekti — ${esc(task)} ━━━</span>`);
|
||||||
termLog(`\n<span style="color:#f0883e;font-weight:bold">[0] ${esc(cli.name)}</span> — vaatimusmäärittely`);
|
termLog(`\n<span style="color:#f0883e;font-weight:bold">[0] ${esc(cli.name)}</span> — vaatimusmäärittely`);
|
||||||
highlightAgent('client');
|
highlightAgent('client');
|
||||||
explainStep('Vaatimusmäärittely', `${cli.name} muotoilee idean selkeiksi vaatimuksiksi: ominaisuudet, datamallit, rajapinnat.`);
|
explainStep('Vaatimusmäärittely', `${cli.name} muotoilee idean selkeiksi vaatimuksiksi: ominaisuudet, datamallit, rajapinnat.`);
|
||||||
@@ -788,37 +798,72 @@ OUTPUT FORMAT:
|
|||||||
if (!brief) { termLog(' ✗ Vaatimusmäärittely epäonnistui', '#f85149'); return; }
|
if (!brief) { termLog(' ✗ Vaatimusmäärittely epäonnistui', '#f85149'); return; }
|
||||||
termLog(` <span style="color:#8b949e">Vaatimukset valmiit → Manageri</span>`);
|
termLog(` <span style="color:#8b949e">Vaatimukset valmiit → Manageri</span>`);
|
||||||
|
|
||||||
explainStep('Mallipohja', `Käytetään "${template.name}" -mallipohjaa jossa ${template.order.length} tiedostoa: ${template.order.join(', ')}. Jokainen tiedosto generoidaan järjestyksessä, ja aiemmat tiedostot annetaan kontekstina seuraavalle.`);
|
// Valitaan mallipohja automaattisesti briefin perusteella
|
||||||
|
const template = selectTemplate(brief);
|
||||||
|
|
||||||
|
// Tiedostolista: mallipohjasta tai managerin dynaamisesta suunnitelmasta
|
||||||
|
let fileOrder = [];
|
||||||
|
let fileDefs = {};
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
// Mallipohja löytyi — käytetään sen rakennetta
|
||||||
|
fileOrder = template.order;
|
||||||
|
fileDefs = template.files;
|
||||||
|
explainStep('Mallipohja', `Tunnistettiin "${template.name}" — ${fileOrder.length} tiedostoa: ${fileOrder.join(', ')}.`);
|
||||||
|
} else {
|
||||||
|
// Vapaa tila — Manageri päättää tiedostorakenteen
|
||||||
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] ${esc(mgr.name)}</span> — tiedostorakenne`);
|
||||||
|
highlightAgent('manager');
|
||||||
|
explainStep('Vapaa tila', 'Sopivaa mallipohjaa ei löytynyt. Manageri suunnittelee tiedostorakenteen vaatimusten perusteella.');
|
||||||
|
const planPrompt = `PROJECT REQUIREMENTS:\n${brief}\n\nPlan the file structure for this project. List each file on its own line:\nfilename.ext: one-line description\n\nMaximum 6 files. List dependency files first.`;
|
||||||
|
const plan = await kpnRun(mgr.model, planPrompt, false, mgr);
|
||||||
|
if (!plan) { termLog(' ✗ Suunnittelu epäonnistui', '#f85149'); return; }
|
||||||
|
|
||||||
|
// Parsitaan managerin tuottama tiedostolista
|
||||||
|
for (const line of plan.split('\n')) {
|
||||||
|
const m = line.match(/^\s*[-*]?\s*(\S+\.\w+)\s*[:\-–]\s*(.+)/);
|
||||||
|
if (m) {
|
||||||
|
const fname = m[1].replace(/^`|`$/g, '');
|
||||||
|
fileOrder.push(fname);
|
||||||
|
fileDefs[fname] = { description: m[2].trim(), instructions: m[2].trim() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fileOrder.length === 0) {
|
||||||
|
termLog(' ✗ Manageri ei tuottanut tiedostolistaa', '#f85149');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
explainStep('Suunnitelma', `${fileOrder.length} tiedostoa: ${fileOrder.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
const files = {};
|
const files = {};
|
||||||
|
|
||||||
for (let i = 0; i < template.order.length; i++) {
|
for (let i = 0; i < fileOrder.length; i++) {
|
||||||
const fileName = template.order[i];
|
const fileName = fileOrder[i];
|
||||||
const fileDef = template.files[fileName];
|
const fileDef = fileDefs[fileName];
|
||||||
if (!fileDef) continue;
|
if (!fileDef) continue;
|
||||||
|
|
||||||
const step = i + 1;
|
const step = i + 1;
|
||||||
// Valitaan oikea agentti tiedostotyypin mukaan
|
// Valitaan oikea agentti tiedostotyypin mukaan
|
||||||
const isDbFile = fileName === 'models.py' || fileName === 'database.py';
|
const isDbFile = fileName === 'models.py' || fileName === 'database.py' || fileName === 'etl.py';
|
||||||
const dataAgent = agents.data || Object.values(agents)[3];
|
const dataAgent = agents.data || Object.values(agents)[3];
|
||||||
const fileAgent = isDbFile && dataAgent ? dataAgent : cdr;
|
const fileAgent = isDbFile && dataAgent ? dataAgent : cdr;
|
||||||
const fileAgentKey = isDbFile && dataAgent ? 'data' : 'coder';
|
const fileAgentKey = isDbFile && dataAgent ? 'data' : 'coder';
|
||||||
|
|
||||||
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${step}/${template.order.length}] ${esc(fileAgent.name)}</span> — ${esc(fileName)}`);
|
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${step}/${fileOrder.length}] ${esc(fileAgent.name)}</span> — ${esc(fileName)}`);
|
||||||
highlightAgent(fileAgentKey);
|
highlightAgent(fileAgentKey);
|
||||||
|
|
||||||
// Opettava selitys: miksi tämä tiedosto, mitä se sisältää
|
explainStep(fileName, fileDef.instructions || fileDef.description);
|
||||||
explainStep(fileName, fileDef.instructions);
|
|
||||||
|
|
||||||
// Rakennetaan prompti: esimerkki + konteksti + ohje
|
// Rakennetaan prompti
|
||||||
let prompt = '';
|
let prompt = '';
|
||||||
|
|
||||||
// Agentin system prompt (data-agentti models.py:lle, koodari muille)
|
|
||||||
if (fileAgent.prompt) prompt += fileAgent.prompt + '\n\n';
|
if (fileAgent.prompt) prompt += fileAgent.prompt + '\n\n';
|
||||||
|
|
||||||
// Esimerkki (few-shot)
|
// Esimerkki (vain mallipohjatilassa)
|
||||||
prompt += `EXAMPLE of ${fileName} (for a different project, adapt to this one):\n`;
|
if (fileDef.example) {
|
||||||
prompt += '```\n' + fileDef.example + '\n```\n\n';
|
prompt += `EXAMPLE of ${fileName} (for a different project, adapt to this one):\n`;
|
||||||
|
prompt += '```\n' + fileDef.example + '\n```\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
// Aiemmin generoidut tiedostot (konteksti)
|
// Aiemmin generoidut tiedostot (konteksti)
|
||||||
const prevFiles = Object.entries(files);
|
const prevFiles = Object.entries(files);
|
||||||
@@ -834,8 +879,8 @@ OUTPUT FORMAT:
|
|||||||
|
|
||||||
// Tehtävä
|
// Tehtävä
|
||||||
prompt += `NOW write "${fileName}" for THIS project: ${task}\n`;
|
prompt += `NOW write "${fileName}" for THIS project: ${task}\n`;
|
||||||
prompt += fileDef.instructions + '\n';
|
if (fileDef.instructions) prompt += fileDef.instructions + '\n';
|
||||||
prompt += 'Adapt the example to match the project description. Import from already written files. Write ONLY the code, no explanations.';
|
prompt += 'Adapt to the project requirements. Import from already written files. Write ONLY the code, no explanations.';
|
||||||
|
|
||||||
const code = await kpnRun(fileAgent.model, prompt, false, fileAgent);
|
const code = await kpnRun(fileAgent.model, prompt, false, fileAgent);
|
||||||
if (!code) {
|
if (!code) {
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ set -e
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
echo "=== Kipinä Studio Local Development ==="
|
echo "=== Kipinä Studio Local Development ==="
|
||||||
|
|
||||||
|
# Tapetaan vanhat prosessit portissa 3000
|
||||||
|
if lsof -ti:3000 >/dev/null 2>&1; then
|
||||||
|
echo "[0] Tapetaan vanhat prosessit portissa 3000..."
|
||||||
|
lsof -ti:3000 | xargs kill -9 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
echo "[1/3] Rakennetaan frontend..."
|
echo "[1/3] Rakennetaan frontend..."
|
||||||
cd "$SCRIPT_DIR/frontend"
|
cd "$SCRIPT_DIR/frontend"
|
||||||
@@ -12,18 +19,32 @@ npm run build --silent 2>&1 | tail -1
|
|||||||
|
|
||||||
# Hub
|
# Hub
|
||||||
echo "[2/3] Käynnistetään hub..."
|
echo "[2/3] Käynnistetään hub..."
|
||||||
cd "$SCRIPT_DIR/hub"
|
cd "$SCRIPT_DIR"
|
||||||
cargo run &
|
STATIC_DIR="$SCRIPT_DIR/frontend/dist" cargo run -p hub &
|
||||||
HUB_PID=$!
|
HUB_PID=$!
|
||||||
sleep 3
|
sleep 2
|
||||||
|
|
||||||
# Native-node (jos Ollama on käynnissä)
|
# Native-node (jos Ollama on käynnissä)
|
||||||
if curl -s http://localhost:11434/api/tags >/dev/null 2>&1; then
|
if curl -s http://localhost:11434/api/tags >/dev/null 2>&1; then
|
||||||
echo "[3/3] Ollama löytyi — käynnistetään native-node..."
|
# Valitaan automaattisesti ensimmäinen qwen-coder -malli
|
||||||
cd "$SCRIPT_DIR/native-node"
|
MODEL=$(curl -s http://localhost:11434/api/tags | python3 -c "
|
||||||
HUB_URL=ws://localhost:3000/ws cargo run --no-default-features &
|
import sys, json
|
||||||
NODE_PID=$!
|
models = json.load(sys.stdin).get('models', [])
|
||||||
echo " Native-node PID: $NODE_PID"
|
for m in models:
|
||||||
|
if 'coder' in m['name']:
|
||||||
|
print(m['name']); break
|
||||||
|
else:
|
||||||
|
if models: print(models[0]['name'])
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$MODEL" ]; then
|
||||||
|
echo "[3/3] Ollama löytyi — käynnistetään native-node (malli: $MODEL)..."
|
||||||
|
HUB_URL=ws://localhost:3000/ws OLLAMA_MODEL="$MODEL" cargo run -p native-node --no-default-features &
|
||||||
|
NODE_PID=$!
|
||||||
|
echo " Native-node PID: $NODE_PID"
|
||||||
|
else
|
||||||
|
echo "[3/3] Ollama käynnissä mutta ei malleja — asenna: ollama pull qwen2.5-coder:7b"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "[3/3] Ollama ei käynnissä — käytetään selaimen Wasm-laskentaa"
|
echo "[3/3] Ollama ei käynnissä — käytetään selaimen Wasm-laskentaa"
|
||||||
echo " Nopeampi: ollama serve & ollama pull qwen2.5-coder:7b && ./local.sh"
|
echo " Nopeampi: ollama serve & ollama pull qwen2.5-coder:7b && ./local.sh"
|
||||||
@@ -33,5 +54,11 @@ echo ""
|
|||||||
echo "=== http://localhost:3000 ==="
|
echo "=== http://localhost:3000 ==="
|
||||||
echo " Ctrl+C pysäyttää"
|
echo " Ctrl+C pysäyttää"
|
||||||
|
|
||||||
|
# Avataan selain
|
||||||
|
open http://localhost:3000 2>/dev/null || xdg-open http://localhost:3000 2>/dev/null || true
|
||||||
|
|
||||||
|
# Siivotaan lapset Ctrl+C:llä
|
||||||
|
trap 'echo ""; echo "Pysäytetään..."; kill $HUB_PID $NODE_PID 2>/dev/null; exit 0' INT TERM
|
||||||
|
|
||||||
# Odotetaan hub-prosessia
|
# Odotetaan hub-prosessia
|
||||||
wait $HUB_PID
|
wait $HUB_PID
|
||||||
|
|||||||
@@ -109,14 +109,21 @@ impl LlmEngine {
|
|||||||
let model = self.model.borrow().clone();
|
let model = self.model.borrow().clone();
|
||||||
|
|
||||||
let default_stop: Vec<String> = vec![
|
let default_stop: Vec<String> = vec![
|
||||||
"<|im_end|>".into(), "\n###".into(), "\nExplanation".into(),
|
"<|im_end|>".into(),
|
||||||
"\nNote:".into(), "\nPlease note".into(), "\nThis is".into(),
|
|
||||||
"\n```\n\n".into(), "\n// Example".into(), "\n# Example".into(),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut body = serde_json::json!({
|
// Rakennetaan messages-lista (chat API)
|
||||||
|
let mut messages = Vec::new();
|
||||||
|
if let Some(ref sp) = opts.system_prompt {
|
||||||
|
if !sp.is_empty() {
|
||||||
|
messages.push(serde_json::json!({"role": "system", "content": sp}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages.push(serde_json::json!({"role": "user", "content": prompt}));
|
||||||
|
|
||||||
|
let body = serde_json::json!({
|
||||||
"model": model,
|
"model": model,
|
||||||
"prompt": prompt,
|
"messages": messages,
|
||||||
"stream": false,
|
"stream": false,
|
||||||
"options": {
|
"options": {
|
||||||
"num_predict": opts.max_tokens,
|
"num_predict": opts.max_tokens,
|
||||||
@@ -126,16 +133,13 @@ impl LlmEngine {
|
|||||||
"stop": opts.stop.as_ref().unwrap_or(&default_stop),
|
"stop": opts.stop.as_ref().unwrap_or(&default_stop),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if let Some(ref sp) = opts.system_prompt {
|
|
||||||
body.as_object_mut().unwrap().insert("system".to_string(), serde_json::json!(sp));
|
|
||||||
}
|
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let resp = self.client.post(format!("{}/api/generate", self.ollama_url))
|
let resp = self.client.post(format!("{}/api/chat", self.ollama_url))
|
||||||
.json(&body)
|
.json(&body)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Ollama generate: {}", e))?;
|
.map_err(|e| format!("Ollama chat: {}", e))?;
|
||||||
|
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
return Err(format!("Ollama HTTP {}", resp.status()));
|
return Err(format!("Ollama HTTP {}", resp.status()));
|
||||||
@@ -144,7 +148,7 @@ impl LlmEngine {
|
|||||||
let body: serde_json::Value = resp.json().await
|
let body: serde_json::Value = resp.json().await
|
||||||
.map_err(|e| format!("Ollama JSON: {}", e))?;
|
.map_err(|e| format!("Ollama JSON: {}", e))?;
|
||||||
|
|
||||||
let text = body["response"].as_str().unwrap_or("").to_string();
|
let text = body["message"]["content"].as_str().unwrap_or("").to_string();
|
||||||
let _total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
|
let _total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
|
||||||
let eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
|
let eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
|
||||||
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
|
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
|
||||||
@@ -163,40 +167,15 @@ impl LlmEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Siivoa markdown-koodiblokki-merkit ja selitystekstit
|
/// Siivoa markdown-koodiblokki-merkit vastauksesta
|
||||||
fn strip_code_fences(text: &str) -> String {
|
fn strip_code_fences(text: &str) -> String {
|
||||||
// Poistetaan kaikki ```-rivit ja kielitunnisteet (```python, ```rust jne.)
|
|
||||||
let lines: Vec<&str> = text.lines().collect();
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
let filtered: Vec<&str> = lines.into_iter().filter(|line| {
|
let filtered: Vec<&str> = lines.into_iter().filter(|line| {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
// Poista rivit jotka ovat pelkkiä ``` tai ```kielitunniste
|
// Poista rivit jotka ovat pelkkiä ``` tai ```kielitunniste
|
||||||
if trimmed.starts_with("```") {
|
trimmed != "```" && !(trimmed.starts_with("```") && !trimmed[3..].contains('`'))
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}).collect();
|
}).collect();
|
||||||
let mut result = filtered.join("\n").trim().to_string();
|
filtered.join("\n").trim().to_string()
|
||||||
|
|
||||||
// Poista selitysteksti lopusta (kaikki rivin "\nPlease note" jälkeen jne.)
|
|
||||||
let lower = result.to_lowercase();
|
|
||||||
for stop in &["\nplease note", "\nthis is a basic", "\nthis code", "\nnote that", "\nremember to", "\nyou can", "\nto run"] {
|
|
||||||
if let Some(pos) = lower.find(stop) {
|
|
||||||
result = result[..pos].trim_end().to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Poista johdantolauseet alusta
|
|
||||||
let lower = result.to_lowercase();
|
|
||||||
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
|
|
||||||
if lower.starts_with(prefix) {
|
|
||||||
if let Some(nl) = result.find('\n') {
|
|
||||||
result = result[nl + 1..].to_string();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.trim().to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GenerateResult {
|
pub struct GenerateResult {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use std::io::IsTerminal;
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
use tokio_tungstenite::connect_async;
|
use tokio_tungstenite::connect_async;
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
@@ -362,13 +363,17 @@ async fn main() {
|
|||||||
st.push_log("System", format!("Malli valmis: {}", active_model), None);
|
st.push_log("System", format!("Malli valmis: {}", active_model), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Käynnistetään graafinen TUI vasta kun TUI:n Prompt (LlmEngine::load) on ohitettu!
|
// Käynnistetään graafinen TUI vain jos stdin on terminaali (ei taustaprosessina)
|
||||||
let ui_state = tui_state.clone();
|
let ui_state = tui_state.clone();
|
||||||
tokio::spawn(async move {
|
if std::io::stdin().is_terminal() {
|
||||||
if let Err(e) = tui_dashboard::run_dashboard(ui_state, cmd_tx).await {
|
tokio::spawn(async move {
|
||||||
tracing::error!("Pääluupin TUI kaatui: {}", e);
|
if let Err(e) = tui_dashboard::run_dashboard(ui_state, cmd_tx).await {
|
||||||
}
|
tracing::error!("Pääluupin TUI kaatui: {}", e);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tracing::info!("Ei terminaalia — TUI ohitettu, lokitetaan stdoutiin");
|
||||||
|
};
|
||||||
|
|
||||||
// Haetaan paikalliset mallit hubille lähetettäväksi
|
// Haetaan paikalliset mallit hubille lähetettäväksi
|
||||||
let mut available_models = None;
|
let mut available_models = None;
|
||||||
@@ -418,6 +423,48 @@ async fn main() {
|
|||||||
st.status = "ACTIVE".to_string();
|
st.status = "ACTIVE".to_string();
|
||||||
st.push_log("System", "Suoritus jatkuu...".to_string(), None);
|
st.push_log("System", "Suoritus jatkuu...".to_string(), None);
|
||||||
}
|
}
|
||||||
|
} else if cmd_str == "fetch_models" {
|
||||||
|
// Haetaan mallit Ollamasta ja avataan valikkö
|
||||||
|
if let Some(ref engine) = llm {
|
||||||
|
match engine.fetch_models().await {
|
||||||
|
Ok(tags) => {
|
||||||
|
let models: Vec<String> = tags.get("models")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| arr.iter()
|
||||||
|
.filter_map(|m| m.get("name").and_then(|n| n.as_str()).map(|s| s.to_string()))
|
||||||
|
.collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.model_picker_items = models;
|
||||||
|
st.model_picker_idx = 0;
|
||||||
|
st.model_picker_open = true;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.push_log("System", format!("Mallilistan haku epäonnistui: {}", e), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(model) = cmd_str.strip_prefix("change_model:") {
|
||||||
|
// TUI:sta valittu malli — vaihdetaan
|
||||||
|
if let Some(ref engine) = llm {
|
||||||
|
engine.set_model(model.to_string());
|
||||||
|
match engine.ensure_model().await {
|
||||||
|
Ok(()) => {
|
||||||
|
tracing::info!("Malli vaihdettu: {}", model);
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.model_name = model.to_string();
|
||||||
|
st.push_log("System", format!("Malli vaihdettu: {}", model), None);
|
||||||
|
// Ilmoitetaan hubille
|
||||||
|
let auth = build_auth_message(allocated_gb, model, available_models.clone());
|
||||||
|
let _ = write.send(Message::Text(auth)).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.push_log("System", format!("Mallin vaihto epäonnistui: {}", e), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ pub struct DashboardState {
|
|||||||
pub last_tokens_sec: f64,
|
pub last_tokens_sec: f64,
|
||||||
pub network_active_nodes: usize,
|
pub network_active_nodes: usize,
|
||||||
pub network_total_tasks: u64,
|
pub network_total_tasks: u64,
|
||||||
|
// Mallivalikko
|
||||||
|
pub model_picker_open: bool,
|
||||||
|
pub model_picker_items: Vec<String>,
|
||||||
|
pub model_picker_idx: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DashboardState {
|
impl DashboardState {
|
||||||
@@ -51,6 +55,9 @@ impl DashboardState {
|
|||||||
last_tokens_sec: 0.0,
|
last_tokens_sec: 0.0,
|
||||||
network_active_nodes: 1, // oletetaan itsemme
|
network_active_nodes: 1, // oletetaan itsemme
|
||||||
network_total_tasks: 0,
|
network_total_tasks: 0,
|
||||||
|
model_picker_open: false,
|
||||||
|
model_picker_items: Vec::new(),
|
||||||
|
model_picker_idx: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,20 +95,53 @@ pub async fn run_dashboard(
|
|||||||
}
|
}
|
||||||
ev = reader.next() => {
|
ev = reader.next() => {
|
||||||
if let Some(Ok(Event::Key(key))) = ev {
|
if let Some(Ok(Event::Key(key))) = ev {
|
||||||
match key.code {
|
let picker_open = state.read().await.model_picker_open;
|
||||||
KeyCode::Char('q') | KeyCode::Esc => {
|
|
||||||
// Palautetaan näyttö ja suljetaan ohjelma
|
if picker_open {
|
||||||
disable_raw_mode()?;
|
// Mallivalikko auki — navigointi
|
||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
match key.code {
|
||||||
std::process::exit(0);
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
|
let mut st = state.write().await;
|
||||||
|
if st.model_picker_idx > 0 { st.model_picker_idx -= 1; }
|
||||||
|
}
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => {
|
||||||
|
let mut st = state.write().await;
|
||||||
|
let max = st.model_picker_items.len().saturating_sub(1);
|
||||||
|
if st.model_picker_idx < max { st.model_picker_idx += 1; }
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
let mut st = state.write().await;
|
||||||
|
let idx = st.model_picker_idx;
|
||||||
|
if let Some(model) = st.model_picker_items.get(idx).cloned() {
|
||||||
|
st.model_picker_open = false;
|
||||||
|
st.push_log("System", format!("Vaihdetaan malliin: {}...", model), None);
|
||||||
|
let _ = cmd_tx.send(format!("change_model:{}", model));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Esc | KeyCode::Char('m') | KeyCode::Char('M') => {
|
||||||
|
state.write().await.model_picker_open = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
KeyCode::Char('p') | KeyCode::Char('P') => {
|
} else {
|
||||||
let _ = cmd_tx.send("pause".to_string());
|
// Normaali tila
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => {
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
KeyCode::Char('p') | KeyCode::Char('P') => {
|
||||||
|
let _ = cmd_tx.send("pause".to_string());
|
||||||
|
}
|
||||||
|
KeyCode::Char('r') | KeyCode::Char('R') | KeyCode::Char('s') => {
|
||||||
|
let _ = cmd_tx.send("resume".to_string());
|
||||||
|
}
|
||||||
|
KeyCode::Char('m') | KeyCode::Char('M') => {
|
||||||
|
let _ = cmd_tx.send("fetch_models".to_string());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
KeyCode::Char('r') | KeyCode::Char('R') | KeyCode::Char('s') => {
|
|
||||||
let _ = cmd_tx.send("resume".to_string());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,10 +254,43 @@ fn ui(f: &mut ratatui::Frame, st: &DashboardState) {
|
|||||||
|
|
||||||
// --- Footer / Status ---
|
// --- Footer / Status ---
|
||||||
let status_color = if st.status == "ACTIVE" { Color::Green } else { Color::Yellow };
|
let status_color = if st.status == "ACTIVE" { Color::Green } else { Color::Yellow };
|
||||||
let status_text = format!(" Tila: {} | Komennot: [P] Pause / [R] Työhön / [Q] Sulje ", st.status);
|
let status_text = format!(" Tila: {} | [P] Pause [R] Työhön [M] Malli [Q] Sulje ", st.status);
|
||||||
let footer = Paragraph::new(status_text)
|
let footer = Paragraph::new(status_text)
|
||||||
.style(Style::default().fg(status_color).add_modifier(Modifier::BOLD))
|
.style(Style::default().fg(status_color).add_modifier(Modifier::BOLD))
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(Block::default().borders(Borders::ALL));
|
.block(Block::default().borders(Borders::ALL));
|
||||||
f.render_widget(footer, chunks[2]);
|
f.render_widget(footer, chunks[2]);
|
||||||
|
|
||||||
|
// --- Mallivalikko-overlay ---
|
||||||
|
if st.model_picker_open && !st.model_picker_items.is_empty() {
|
||||||
|
let area = f.area();
|
||||||
|
let popup_h = (st.model_picker_items.len() as u16 + 4).min(area.height - 4);
|
||||||
|
let popup_w = 50.min(area.width - 4);
|
||||||
|
let popup = ratatui::layout::Rect::new(
|
||||||
|
(area.width - popup_w) / 2,
|
||||||
|
(area.height - popup_h) / 2,
|
||||||
|
popup_w,
|
||||||
|
popup_h,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tausta
|
||||||
|
f.render_widget(ratatui::widgets::Clear, popup);
|
||||||
|
|
||||||
|
let items: Vec<ratatui::text::Line> = st.model_picker_items.iter().enumerate().map(|(i, name)| {
|
||||||
|
if i == st.model_picker_idx {
|
||||||
|
ratatui::text::Line::from(format!(" ▸ {} ", name))
|
||||||
|
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||||
|
} else {
|
||||||
|
ratatui::text::Line::from(format!(" {} ", name))
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let picker = Paragraph::new(items)
|
||||||
|
.block(Block::default()
|
||||||
|
.title(" Vaihda malli [↑↓] Enter=valitse Esc=peruuta ")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default().fg(Color::Cyan)));
|
||||||
|
f.render_widget(picker, popup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
83
projektit/projekti1.md
Normal file
83
projektit/projekti1.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
title: UWB-paikannus sisätiloihin (2024)
|
||||||
|
tags: project
|
||||||
|
slideOptions:
|
||||||
|
text-align: left,
|
||||||
|
transition: slide,
|
||||||
|
theme: white,
|
||||||
|
hideAddressBar: true,
|
||||||
|
touch: true,
|
||||||
|
slideNumber: true,
|
||||||
|
controls: true,
|
||||||
|
controlsLayout: 'bottom-right'
|
||||||
|
spotlight:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# UWB-paikannus sisätiloihin (2026)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Ennakkotietona annettakoon pisteiden kohdistaminen yllä olevaan kuvatiedostoon:
|
||||||
|
|
||||||
|
- y=0 yläreunan seinän sisäpinta, x=0 kassakoneiden keskilinja
|
||||||
|
- y=5220 alareunan seinän sisäpinta, x=10406 oikealla seinän sisäpinta
|
||||||
|
|
||||||
|
Paikassa n. 100, 2500 on yksi latausasema kärryille, siinä on sisäänkäynti kauppaan (turvaportit)
|
||||||
|
Paikassa n. 900, 3600 on varsinainen latausasema joka on alakerrassa. Sieltä on liukuportaat vasemmalle kuvassa. Siellä ei ole kunnollista paikannusta joka aiheuttaa paikan hyppimistä. Nämä latausasemat eivät ole kiinnostavia tietoja.
|
||||||
|
|
||||||
|
Eli yhden yksikön muutos koordinaateissa vastaa noin yhden senttimetrin muutosta "kartalla"?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dataformaatti
|
||||||
|
|
||||||
|
Datan formaatti on esitetty alla. Data itsessään on taltioituna csv-tiedostoihin. CSV-tiedostoja on paljon ja niissä on miljoonia rivejä, joten raakadatan käsittely voi olla raskasta.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Varsinaisen ETL-/ELT-prosessin jälkeen data pitäisi olla esikäsitelty ja siivottu. Tämä prosessi on kuitenkin syytä tehdä heti alkuun, jotta myöhemmät dataan liittyvät operaatiot olisivat nopeampia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Tehtävälistaa:
|
||||||
|
|
||||||
|
- Data platform
|
||||||
|
- MariaDB-tietokantakontti
|
||||||
|
- Jupyterlab-kontti ETL-prosessia ja data-analyysia varten
|
||||||
|
|
||||||
|
- Visualisointi historiadatan perusteella
|
||||||
|
- Kärryjen liikkeet kaupan layoutissa
|
||||||
|
|
||||||
|
- Outlierit pois datasta (x,y-pisteet, jotka ylittävät rajat)
|
||||||
|
- Läpimenoaika, "kuumat alueet" (eli missä on vietetty aikaa)
|
||||||
|
- Tilastoja
|
||||||
|
- Datan ajallinen täsmällisyys (näytevälin dt keskiarvo ja keskihajonta)
|
||||||
|
- Datan paikannustäsmällisyys (outlayreiden esiintymistaajuus, paikannuksen kohina eli x,y-koordinaatin heittelehtiminen luonnottomasti)
|
||||||
|
- Raportteja päivä-, viikko-, kuukausitason "liikennöinnistä"
|
||||||
|
- Läpimenoaikojen tilastointi (eri aukioloajan tunteina, eri päivinä, ruuhkahuippujen / hiljaisimpien aikojen löytäminen)
|
||||||
|
- Kuinka monta kassaa on käytössä eri aukioloajan tunteina, eri päivinä
|
||||||
|
- Kassajonojen kertyminen (kuinka monta asiakasta jonottaa kuinka monessa jonossa)
|
||||||
|
- (x,y)-koordinaattien skaalaus mittayksikköön [m]
|
||||||
|
- Keskimääräinen kärryjen kulkema matka
|
||||||
|
- Kärryjen nopeus [km/h], nopeuden liukuva keskiarvoistus (valon nopeudella / mach-nopeuksilla tapahtuvien liikkeiden karsiminen pois)
|
||||||
|
- Ostoskärryjen tasainen kierto, onko kärryjä, jotka ovat erittäin paljon/vähän käytössä
|
||||||
|
- Visualisointeja ja tilastoja
|
||||||
|
- kuumat alueet visualisoituna pohjakuvaan
|
||||||
|
- eri aikaväleinä: 9.00-11.00; 11.00-13.00; 13.00-15.00; 15.00-17.00; 17.00-19.00; 19.00-21.00
|
||||||
|
- eri viikonpäivinä
|
||||||
|
- Tilastot ja kuvaajat yllä mainitusta (esim histogrammit)
|
||||||
|
- Useamman datalähteen yhdistäminen
|
||||||
|
- Esim. avoimen säätietohistorian yhdistäminen eri tuntien kävijämääriin
|
||||||
|
- Jotain muuta, asiakkaalle mahdollisesti lisäarvoa tuottavaa - keksikää jotain jännää!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Reference in New Issue
Block a user