uusi projekti
This commit is contained in:
@@ -109,14 +109,21 @@ impl LlmEngine {
|
||||
let model = self.model.borrow().clone();
|
||||
|
||||
let default_stop: Vec<String> = vec![
|
||||
"<|im_end|>".into(), "\n###".into(), "\nExplanation".into(),
|
||||
"\nNote:".into(), "\nPlease note".into(), "\nThis is".into(),
|
||||
"\n```\n\n".into(), "\n// Example".into(), "\n# Example".into(),
|
||||
"<|im_end|>".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,
|
||||
"prompt": prompt,
|
||||
"messages": messages,
|
||||
"stream": false,
|
||||
"options": {
|
||||
"num_predict": opts.max_tokens,
|
||||
@@ -126,16 +133,13 @@ impl LlmEngine {
|
||||
"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 resp = self.client.post(format!("{}/api/generate", self.ollama_url))
|
||||
let resp = self.client.post(format!("{}/api/chat", self.ollama_url))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Ollama generate: {}", e))?;
|
||||
.map_err(|e| format!("Ollama chat: {}", e))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Ollama HTTP {}", resp.status()));
|
||||
@@ -144,7 +148,7 @@ impl LlmEngine {
|
||||
let body: serde_json::Value = resp.json().await
|
||||
.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 eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
|
||||
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 {
|
||||
// Poistetaan kaikki ```-rivit ja kielitunnisteet (```python, ```rust jne.)
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
let filtered: Vec<&str> = lines.into_iter().filter(|line| {
|
||||
let trimmed = line.trim();
|
||||
// Poista rivit jotka ovat pelkkiä ``` tai ```kielitunniste
|
||||
if trimmed.starts_with("```") {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
trimmed != "```" && !(trimmed.starts_with("```") && !trimmed[3..].contains('`'))
|
||||
}).collect();
|
||||
let mut result = 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()
|
||||
filtered.join("\n").trim().to_string()
|
||||
}
|
||||
|
||||
pub struct GenerateResult {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use serde_json::json;
|
||||
use std::io::IsTerminal;
|
||||
use sysinfo::System;
|
||||
use tokio_tungstenite::connect_async;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
@@ -362,13 +363,17 @@ async fn main() {
|
||||
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();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = tui_dashboard::run_dashboard(ui_state, cmd_tx).await {
|
||||
tracing::error!("Pääluupin TUI kaatui: {}", e);
|
||||
}
|
||||
});
|
||||
if std::io::stdin().is_terminal() {
|
||||
tokio::spawn(async move {
|
||||
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
|
||||
let mut available_models = None;
|
||||
@@ -418,6 +423,48 @@ async fn main() {
|
||||
st.status = "ACTIVE".to_string();
|
||||
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 network_active_nodes: usize,
|
||||
pub network_total_tasks: u64,
|
||||
// Mallivalikko
|
||||
pub model_picker_open: bool,
|
||||
pub model_picker_items: Vec<String>,
|
||||
pub model_picker_idx: usize,
|
||||
}
|
||||
|
||||
impl DashboardState {
|
||||
@@ -51,6 +55,9 @@ impl DashboardState {
|
||||
last_tokens_sec: 0.0,
|
||||
network_active_nodes: 1, // oletetaan itsemme
|
||||
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() => {
|
||||
if let Some(Ok(Event::Key(key))) = ev {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => {
|
||||
// Palautetaan näyttö ja suljetaan ohjelma
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
std::process::exit(0);
|
||||
let picker_open = state.read().await.model_picker_open;
|
||||
|
||||
if picker_open {
|
||||
// Mallivalikko auki — navigointi
|
||||
match key.code {
|
||||
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') => {
|
||||
let _ = cmd_tx.send("pause".to_string());
|
||||
} else {
|
||||
// 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 ---
|
||||
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)
|
||||
.style(Style::default().fg(status_color).add_modifier(Modifier::BOLD))
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user