488 lines
19 KiB
Rust
488 lines
19 KiB
Rust
use std::{
|
|
io::{BufRead, BufReader},
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
use egui::{CollapsingResponse, Id, InnerResponse, Ui};
|
|
use serde::{Deserialize, Serialize};
|
|
use zxq5_egui::combobox::{self, text_combobox};
|
|
|
|
use crate::{
|
|
editors::settings_editor::ProjectSettings,
|
|
services::llm::{LLMConfig, LLMHandle, LLMRequest, ReasoningEffort, ResponseToken},
|
|
};
|
|
//
|
|
// #[derive(Clone)]
|
|
// pub struct ContentAI {
|
|
// pub open: bool,
|
|
//
|
|
// // model input
|
|
// pub content: String,
|
|
// pub instruction: String,
|
|
// pub context_override: String,
|
|
// pub system_prompt: String,
|
|
//
|
|
// // model settings
|
|
// pub max_tokens: usize,
|
|
// pub temperature: f32,
|
|
// pub reasoning_effort: ReasoningEffort,
|
|
// pub model_override: String,
|
|
//
|
|
// // model output
|
|
// pub reasoning: Arc<Mutex<String>>,
|
|
// pub result: Arc<Mutex<String>>,
|
|
// pub ready: Arc<Mutex<ReadyState>>,
|
|
// }
|
|
|
|
#[derive(Debug)]
|
|
pub struct AIChatUI {
|
|
pub instruction: String,
|
|
pub context: String,
|
|
pub system_prompt: String,
|
|
|
|
pub config: LLMConfig,
|
|
pub request: Option<LLMRequest>,
|
|
|
|
reasoning_out: String,
|
|
content_out: String,
|
|
}
|
|
|
|
impl Clone for AIChatUI {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
instruction: self.instruction.clone(),
|
|
context: self.context.clone(),
|
|
system_prompt: self.system_prompt.clone(),
|
|
config: self.config.clone(),
|
|
request: None,
|
|
reasoning_out: self.reasoning_out.clone(),
|
|
content_out: self.content_out.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AIChatUI {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
instruction: String::new(),
|
|
context: String::new(),
|
|
system_prompt: String::new(),
|
|
|
|
config: LLMConfig::default(),
|
|
request: None,
|
|
|
|
reasoning_out: String::new(),
|
|
content_out: String::new(),
|
|
}
|
|
}
|
|
|
|
pub fn update(&mut self) {
|
|
if let Some(request) = &mut self.request {
|
|
for token in request.stream_content() {
|
|
match token {
|
|
ResponseToken::Content(s) => self.content_out.push_str(&s),
|
|
ResponseToken::Reasoning(s) => self.reasoning_out.push_str(&s),
|
|
ResponseToken::End => {
|
|
self.request = None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn ui(&mut self, ui: &mut egui::Ui, content: &mut String) {
|
|
egui::Panel::right("ai_assistant").show_inside(ui, |ui| {
|
|
collapsing_panel("Settings", ui, |ui| {
|
|
egui::Grid::new("continue_grid")
|
|
.num_columns(2)
|
|
.striped(true)
|
|
.show(ui, |ui| {
|
|
ui.label("Max Tokens");
|
|
ui.add(
|
|
egui::DragValue::new(&mut self.config.max_tokens)
|
|
.range(128..=u32::MAX)
|
|
.speed(128),
|
|
);
|
|
ui.end_row();
|
|
|
|
ui.label("Temperature");
|
|
ui.add(
|
|
egui::DragValue::new(&mut self.config.temperature)
|
|
.range(0.0..=2.0)
|
|
.speed(0.1),
|
|
);
|
|
|
|
ui.label("Reasoning effort");
|
|
|
|
combobox::combobox(
|
|
ui,
|
|
&mut self.config.reasoning_effort,
|
|
&[
|
|
ReasoningEffort::Minimal,
|
|
ReasoningEffort::Low,
|
|
ReasoningEffort::Medium,
|
|
ReasoningEffort::High,
|
|
],
|
|
);
|
|
|
|
ui.end_row();
|
|
|
|
ui.label("Model override");
|
|
|
|
combobox::text_combobox(
|
|
ui,
|
|
&mut self.config.model,
|
|
&["openai/gpt-oss-20b", "google/gemma"],
|
|
);
|
|
|
|
// ui.add(egui::TextEdit::singleline(&mut self.config.model));
|
|
ui.end_row();
|
|
});
|
|
});
|
|
|
|
collapsing_panel("Instructions", ui, |ui| {
|
|
egui::ScrollArea::vertical()
|
|
.auto_shrink([false, false])
|
|
.max_height(ui.available_height())
|
|
.show(ui, |ui| {
|
|
ui.add(
|
|
egui::TextEdit::multiline(&mut self.instruction)
|
|
.desired_width(ui.available_width())
|
|
.hint_text("Writing Instructions"),
|
|
);
|
|
})
|
|
});
|
|
|
|
collapsing_panel("Context", ui, |ui| {
|
|
egui::ScrollArea::vertical()
|
|
.auto_shrink([false, false])
|
|
.max_height(ui.available_height())
|
|
.show(ui, |ui| {
|
|
ui.add(
|
|
egui::TextEdit::multiline(&mut self.context)
|
|
.desired_width(ui.available_width())
|
|
.hint_text("Any additional context"),
|
|
);
|
|
})
|
|
});
|
|
|
|
collapsing_panel("System Prompt", ui, |ui| {
|
|
egui::ScrollArea::vertical()
|
|
.auto_shrink([false, false])
|
|
.max_height(ui.available_height())
|
|
.show(ui, |ui| {
|
|
ui.add(
|
|
egui::TextEdit::multiline(&mut self.system_prompt)
|
|
.desired_width(ui.available_width())
|
|
.hint_text("System prompt"),
|
|
);
|
|
})
|
|
});
|
|
|
|
ui.label("hello world");
|
|
});
|
|
}
|
|
}
|
|
|
|
fn collapsing_panel<R>(
|
|
id: &'static str,
|
|
ui: &mut Ui,
|
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
|
) -> CollapsingResponse<R> {
|
|
egui::CollapsingHeader::new(id)
|
|
.default_open(true)
|
|
.show(ui, |ui| {
|
|
egui::Panel::top(id)
|
|
.resizable(true)
|
|
.show_inside(ui, add_contents)
|
|
.inner
|
|
})
|
|
}
|
|
//
|
|
// impl ContentAI {
|
|
// pub fn new(content: String) -> Self {
|
|
// Self {
|
|
// // model input
|
|
// content,
|
|
// instruction: String::new(),
|
|
// context_override: String::new(),
|
|
// system_prompt: String::new(),
|
|
//
|
|
// // model settings
|
|
// max_tokens: 2048,
|
|
// reasoning_effort: ReasoningEffort::default(),
|
|
// temperature: 0.7,
|
|
// model_override: String::new(),
|
|
// reasoning: Arc::new(Mutex::new(String::new())),
|
|
//
|
|
// // output
|
|
// result: Arc::new(Mutex::new(String::new())),
|
|
// ready: Arc::new(Mutex::new(ReadyState::Idle)),
|
|
//
|
|
// // ui
|
|
// open: true,
|
|
// }
|
|
// }
|
|
//
|
|
// pub fn ui(&mut self, ui: &mut egui::Ui, project: &mut ProjectSettings) {
|
|
// let is_open = self.open;
|
|
//
|
|
// if is_open {
|
|
// egui::SidePanel::right("ai_assistant").show_inside(ui, |ui| {
|
|
// Self::ui_main(self, ui, project);
|
|
// });
|
|
// }
|
|
// self.open = is_open;
|
|
// }
|
|
//
|
|
// fn ui_output_box(&mut self, ui: &mut egui::Ui, project: &mut ProjectSettings) {
|
|
// let mut ready_lock = self.ready.lock().unwrap();
|
|
//
|
|
// ui.horizontal(|ui| {
|
|
// if *ready_lock == ReadyState::Generating {
|
|
// if ui.button("Cancel").clicked() {
|
|
// *ready_lock = ReadyState::Halted;
|
|
// }
|
|
// if ui.button("Stop").clicked() {
|
|
// *ready_lock = ReadyState::Idle;
|
|
// }
|
|
// ui.spinner();
|
|
// ui.label("Generating...");
|
|
// }
|
|
//
|
|
// if *ready_lock == ReadyState::Idle {
|
|
// let continue_content = || {
|
|
// let content = self.content.clone();
|
|
// let project = project.clone();
|
|
// let result = self.result.clone();
|
|
// let reasoning = self.reasoning.clone();
|
|
// let ready = self.ready.clone();
|
|
//
|
|
// let options = AIOptions {
|
|
// max_completion_tokens: self.max_tokens,
|
|
// reasoning_effort: self.reasoning_effort,
|
|
// temperature: self.temperature,
|
|
// model_override: if !self.model_override.is_empty() {
|
|
// Some(self.model_override.clone())
|
|
// } else {
|
|
// None
|
|
// },
|
|
// };
|
|
//
|
|
// let ai_input = AIInput {
|
|
// system_prompt: self.system_prompt.clone(),
|
|
// user_prompt: format!(
|
|
// "{}\n\n{} {}",
|
|
// self.instruction.clone(),
|
|
// project.ai_context.clone(),
|
|
// self.context_override.clone()
|
|
// ),
|
|
// previous_content: content.clone(),
|
|
// structure: None,
|
|
// };
|
|
//
|
|
// result.lock().unwrap().clear();
|
|
//
|
|
// std::thread::spawn(move || {
|
|
// let result = crate::llm_integration::content_llm::continue_content(
|
|
// ai_input,
|
|
// options,
|
|
// project,
|
|
// result,
|
|
// reasoning,
|
|
// ready.clone(),
|
|
// );
|
|
// if let Err(e) = result {
|
|
// eprintln!("Error in content generation: {e}");
|
|
// }
|
|
// });
|
|
// };
|
|
//
|
|
// if ui.button("Generate ").clicked() {
|
|
// continue_content();
|
|
// }
|
|
//
|
|
// ui.label("Idle");
|
|
// }
|
|
//
|
|
// // show regardless of state
|
|
// if ui.button("Insert").clicked() {
|
|
// *ready_lock = ReadyState::Ready;
|
|
// }
|
|
//
|
|
// if ui.button("Clear").clicked() {
|
|
// self.result.lock().unwrap().clear();
|
|
// self.reasoning.lock().unwrap().clear();
|
|
// }
|
|
// });
|
|
//
|
|
// ui.spacing();
|
|
//
|
|
// ui.vertical(|ui| {
|
|
// egui::TopBottomPanel::top("reasoning_output")
|
|
// .resizable(true)
|
|
// .show_inside(ui, |ui| {
|
|
// egui::ScrollArea::both()
|
|
// .auto_shrink([false, true])
|
|
// .id_salt("reasoning_output")
|
|
// .max_width(ui.available_width())
|
|
// // .max_height(ui.available_height() / 3.0)
|
|
// .show(ui, |ui| {
|
|
// ui.add(
|
|
// egui::TextEdit::multiline(&mut *self.reasoning.lock().unwrap())
|
|
// .font(egui::TextStyle::Monospace)
|
|
// .interactive(false)
|
|
// .desired_rows(5)
|
|
// .frame(false)
|
|
// .desired_width(ui.available_width())
|
|
// .lock_focus(true)
|
|
// .hint_text("Reasoning will appear here..."),
|
|
// );
|
|
// });
|
|
// });
|
|
//
|
|
// egui::ScrollArea::both()
|
|
// .auto_shrink([false, false])
|
|
// .id_salt("llm_output")
|
|
// .max_width(ui.available_width())
|
|
// .show(ui, |ui| {
|
|
// ui.add(
|
|
// egui::TextEdit::multiline(&mut *self.result.lock().unwrap())
|
|
// .font(egui::TextStyle::Monospace)
|
|
// .interactive(false)
|
|
// .desired_rows(0)
|
|
// .frame(false)
|
|
// .desired_width(ui.available_width())
|
|
// .lock_focus(true)
|
|
// .hint_text("Content will appear here..."),
|
|
// );
|
|
// });
|
|
// });
|
|
// }
|
|
//
|
|
// fn ui_main(&mut self, ui: &mut egui::Ui, project: &mut ProjectSettings) {
|
|
// {
|
|
// ui.weak("(The model will see current file content)");
|
|
//
|
|
// egui::CollapsingHeader::new("Settings")
|
|
// .default_open(true)
|
|
// .show(ui, |ui| {
|
|
// egui::Grid::new("continue_grid")
|
|
// .num_columns(2)
|
|
// .striped(true)
|
|
// .show(ui, |ui| {
|
|
// ui.label("Max Tokens");
|
|
// ui.add(
|
|
// egui::DragValue::new(&mut self.max_tokens)
|
|
// .range(128..=u32::MAX)
|
|
// .speed(128),
|
|
// );
|
|
// ui.end_row();
|
|
//
|
|
// ui.label("Temperature");
|
|
// ui.add(
|
|
// egui::DragValue::new(&mut self.temperature)
|
|
// .range(0.0..=2.0)
|
|
// .speed(0.1),
|
|
// );
|
|
//
|
|
// ui.label("Reasoning effort");
|
|
//
|
|
// egui::ComboBox::from_id_salt("reasoning_effort")
|
|
// .selected_text(self.reasoning_effort.to_string())
|
|
// .show_ui(ui, |ui| {
|
|
// ui.selectable_value(
|
|
// &mut self.reasoning_effort,
|
|
// ReasoningEffort::Minimal,
|
|
// "Minimal",
|
|
// );
|
|
// ui.selectable_value(
|
|
// &mut self.reasoning_effort,
|
|
// ReasoningEffort::Low,
|
|
// "Low",
|
|
// );
|
|
// ui.selectable_value(
|
|
// &mut self.reasoning_effort,
|
|
// ReasoningEffort::Medium,
|
|
// "Medium",
|
|
// );
|
|
// ui.selectable_value(
|
|
// &mut self.reasoning_effort,
|
|
// ReasoningEffort::High,
|
|
// "High",
|
|
// );
|
|
// });
|
|
//
|
|
// ui.end_row();
|
|
//
|
|
// ui.label("Model override");
|
|
// ui.add(egui::TextEdit::singleline(&mut self.model_override));
|
|
// ui.end_row();
|
|
// });
|
|
// });
|
|
//
|
|
// egui::TopBottomPanel::top("continue_instruction")
|
|
// .resizable(true)
|
|
// .show_inside(ui, |ui| {
|
|
// egui::CollapsingHeader::new("Instructions")
|
|
// .default_open(true)
|
|
// .show(ui, |ui| {
|
|
// egui::ScrollArea::vertical()
|
|
// .auto_shrink([false, false])
|
|
// .max_height(ui.available_height())
|
|
// .show(ui, |ui| {
|
|
// ui.add(
|
|
// egui::TextEdit::multiline(&mut self.instruction)
|
|
// .frame(false)
|
|
// .desired_width(ui.available_width())
|
|
// .hint_text("Writing Instructions"),
|
|
// );
|
|
// });
|
|
// });
|
|
// });
|
|
//
|
|
// egui::TopBottomPanel::top("continue_context")
|
|
// .resizable(true)
|
|
// .show_inside(ui, |ui| {
|
|
// egui::CollapsingHeader::new("Context")
|
|
// .default_open(true)
|
|
// .show(ui, |ui| {
|
|
// egui::ScrollArea::vertical()
|
|
// .auto_shrink([false, false])
|
|
// .max_height(ui.available_height())
|
|
// .show(ui, |ui| {
|
|
// ui.add(
|
|
// egui::TextEdit::multiline(&mut self.context_override)
|
|
// .frame(false)
|
|
// .desired_width(ui.available_width())
|
|
// .hint_text("Any additional context?"),
|
|
// );
|
|
// });
|
|
// });
|
|
// });
|
|
//
|
|
// egui::TopBottomPanel::top("continue_system_prompt")
|
|
// .resizable(true)
|
|
// .show_inside(ui, |ui| {
|
|
// egui::CollapsingHeader::new("System prompt")
|
|
// .default_open(true)
|
|
// .show(ui, |ui| {
|
|
// egui::ScrollArea::vertical()
|
|
// .auto_shrink([false, false])
|
|
// .max_height(ui.available_height())
|
|
// .show(ui, |ui| {
|
|
// ui.add(
|
|
// egui::TextEdit::multiline(&mut self.system_prompt)
|
|
// .frame(false)
|
|
// .desired_width(ui.available_width())
|
|
// .hint_text("System prompt"),
|
|
// );
|
|
// });
|
|
// });
|
|
// });
|
|
//
|
|
// self.ui_output_box(ui, project);
|
|
// }
|
|
// }
|
|
// }
|