broken commit, but NEARLY working. (MEGACOMMIT), changed project file language to json, rewrote the object model for templates and objects. in progress rewriting the object model for everything else. rewrote the file handling backend/abstraction. still need to migrate the explorer over, or just rewrite the explorer from scratch because it's truly an abomination of bad design :skull::wilted_rose:
Continuous integration / build (push) Failing after 2m15s
Continuous integration / build (push) Failing after 2m15s
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
/target
|
||||
*/target
|
||||
/project
|
||||
/project*
|
||||
Cargo.lock
|
||||
*.pkg.tar.zst
|
||||
/pkg
|
||||
|
||||
Generated
+2249
-1453
File diff suppressed because it is too large
Load Diff
+22
-19
@@ -4,25 +4,28 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
eframe = "0.32.0"
|
||||
egui = { version = "0.32.0", features = ["serde"] }
|
||||
egui_extras = { version = "0.32.0", features = [
|
||||
"chrono",
|
||||
"datepicker",
|
||||
"file",
|
||||
"image",
|
||||
] }
|
||||
egui_file = "0.23.0"
|
||||
image = { version = "0.25.6", features = ["jpeg", "png"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.141"
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
thiserror = "2.0.14"
|
||||
egui_commonmark = { version = "0.21.1", features = ["embedded_image"] }
|
||||
uuid = { version = "1.17.0", features = ["v4"] }
|
||||
reqwest = { version = "0.12.23", features = ["blocking", "json"] }
|
||||
tempfile = "3.20.0"
|
||||
itertools = "0.14.0"
|
||||
eframe = "0.36.2"
|
||||
# eframe = "0.32.0"
|
||||
egui = { version = "0.36.1", features = ["serde"] }
|
||||
egui_commonmark = { version = "0.25.0", features = ["embedded_image"] }
|
||||
egui_extras = { version = "0.36.1", features = ["datepicker", "file", "image"] }
|
||||
egui_file = "0.28.0"
|
||||
image = "0.25.10"
|
||||
jiff = { version = "0.2.38", features = ["serde"] }
|
||||
md5 = "0.8.1"
|
||||
notify = "8.2.0"
|
||||
reqwest = { version = "0.13.5", features = ["blocking", "json"] }
|
||||
ron = "0.12.2"
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
serde_json = "1.0.154"
|
||||
uuid = { version = "1.28.0", features = ["v4"] }
|
||||
|
||||
# thiserror = "2.0.14"
|
||||
# tempfile = "3.20.0"
|
||||
# itertools = "0.14.0"
|
||||
|
||||
zxq5_egui = { version = "0.1.0", registry = "gitea", path = "../../libraries/zxq5_egui" }
|
||||
zxq5_id_lib = { version = "0.1.0", registry = "gitea", path = "../../libraries/zxq5_id_lib" }
|
||||
|
||||
[features]
|
||||
default = ["native", "llm"]
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
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);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1 @@
|
||||
pub mod llm;
|
||||
@@ -2,7 +2,7 @@ use egui::{TextEdit, vec2};
|
||||
|
||||
use crate::{
|
||||
PROJECT_FOLDER,
|
||||
filesystem::{FILESYSTEM, FsError, LegacyFileSystem},
|
||||
services::persist::{FILESYSTEM, FsError, LegacyFileSystem},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -65,7 +65,6 @@ impl Asset {
|
||||
ui.strong("Filename:");
|
||||
if TextEdit::singleline(&mut self.new_name)
|
||||
.desired_width(f32::INFINITY)
|
||||
.frame(false)
|
||||
.show(ui)
|
||||
.response
|
||||
.changed()
|
||||
|
||||
@@ -3,15 +3,16 @@ use std::path::Path;
|
||||
use egui::TextEdit;
|
||||
use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
|
||||
use serde::{self, Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
editors::{settings_editor::ProjectSettings, tags::Tag},
|
||||
filesystem::{FILESYSTEM, LegacyFileSystem},
|
||||
services::persist::{FILESYSTEM, LegacyFileSystem},
|
||||
util,
|
||||
};
|
||||
|
||||
#[cfg(feature = "llm")]
|
||||
use crate::llm_integration::content_llm::{ContentAI, ReadyState};
|
||||
use crate::components::llm::AIChatUI;
|
||||
|
||||
pub struct MainEditor {
|
||||
pub content: ContentSection,
|
||||
@@ -21,7 +22,8 @@ pub struct MainEditor {
|
||||
preview_cache: CommonMarkCache,
|
||||
|
||||
#[cfg(feature = "llm")]
|
||||
dialog: ContentAI,
|
||||
dialog: AIChatUI,
|
||||
// dialog: ContentAI,
|
||||
#[cfg(feature = "llm")]
|
||||
pub show_ai: bool,
|
||||
}
|
||||
@@ -52,7 +54,7 @@ pub struct ContentSection {
|
||||
pub description: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
pub tags: Vec<Uuid>,
|
||||
|
||||
#[serde(default)]
|
||||
pub content: String,
|
||||
@@ -80,7 +82,7 @@ impl ContentSection {
|
||||
|
||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
FILESYSTEM.write(
|
||||
Path::new(&format!("documents/{id}.json", id = &self.id)),
|
||||
Path::new(&format!("documents/{id}.ron", id = &self.id)),
|
||||
self.clone(),
|
||||
)?;
|
||||
self.saved = true;
|
||||
@@ -88,7 +90,7 @@ impl ContentSection {
|
||||
}
|
||||
|
||||
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut section: Self = FILESYSTEM.read(Path::new(&format!("documents/{id}.json")))?;
|
||||
let mut section: Self = FILESYSTEM.read(Path::new(&format!("documents/{id}.ron")))?;
|
||||
section.saved = true;
|
||||
section.id = id.to_string();
|
||||
Ok(section)
|
||||
@@ -114,7 +116,8 @@ impl MainEditor {
|
||||
#[cfg(feature = "llm")]
|
||||
show_ai: false,
|
||||
#[cfg(feature = "llm")]
|
||||
dialog: ContentAI::new(String::new()),
|
||||
// dialog: ContentAI::new(String::new()),
|
||||
dialog: AIChatUI::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +132,8 @@ impl MainEditor {
|
||||
#[cfg(feature = "llm")]
|
||||
show_ai: false,
|
||||
#[cfg(feature = "llm")]
|
||||
dialog: ContentAI::new(String::new()),
|
||||
dialog: AIChatUI::new(),
|
||||
// dialog: ContentAI::new(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +175,7 @@ impl MainEditor {
|
||||
if ui.button("Delete").clicked() {
|
||||
FILESYSTEM
|
||||
.delete(Path::new(&format!(
|
||||
"documents/{id}.json",
|
||||
"documents/{id}.ron",
|
||||
id = &self.content.id
|
||||
)))
|
||||
.unwrap();
|
||||
@@ -207,11 +211,7 @@ impl MainEditor {
|
||||
.show(ui, |ui| {
|
||||
ui.strong("Name");
|
||||
if ui
|
||||
.add(
|
||||
TextEdit::singleline(&mut self.content.title)
|
||||
.desired_width(f32::INFINITY)
|
||||
.frame(false),
|
||||
)
|
||||
.add(TextEdit::singleline(&mut self.content.title).desired_width(f32::INFINITY))
|
||||
.changed()
|
||||
{
|
||||
self.content.saved = false;
|
||||
@@ -222,8 +222,7 @@ impl MainEditor {
|
||||
if ui
|
||||
.add(
|
||||
TextEdit::singleline(&mut self.content.description)
|
||||
.desired_width(f32::INFINITY)
|
||||
.frame(false),
|
||||
.desired_width(f32::INFINITY),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
@@ -242,18 +241,20 @@ impl MainEditor {
|
||||
if self.show_ai && project.ai_enabled() {
|
||||
let dialog = &mut self.dialog;
|
||||
|
||||
dialog.content = self.content.content.clone();
|
||||
dialog.ui(ui, project);
|
||||
self.dialog.ui(ui, &mut self.content.content);
|
||||
|
||||
if *dialog.ready.lock().unwrap() == ReadyState::Ready {
|
||||
self.content
|
||||
.content
|
||||
.push_str(&dialog.result.lock().unwrap());
|
||||
self.content.saved = false;
|
||||
*dialog.ready.lock().unwrap() = ReadyState::Idle;
|
||||
} else if *dialog.ready.lock().unwrap() == ReadyState::Halted {
|
||||
*dialog.ready.lock().unwrap() = ReadyState::Idle;
|
||||
}
|
||||
// dialog.content = self.content.content.clone();
|
||||
// dialog.ui(ui, project);
|
||||
//
|
||||
// if *dialog.ready.lock().unwrap() == ReadyState::Ready {
|
||||
// self.content
|
||||
// .content
|
||||
// .push_str(&dialog.result.lock().unwrap());
|
||||
// self.content.saved = false;
|
||||
// *dialog.ready.lock().unwrap() = ReadyState::Idle;
|
||||
// } else if *dialog.ready.lock().unwrap() == ReadyState::Halted {
|
||||
// *dialog.ready.lock().unwrap() = ReadyState::Idle;
|
||||
// }
|
||||
}
|
||||
|
||||
if self.show_preview {
|
||||
@@ -263,7 +264,7 @@ impl MainEditor {
|
||||
self.editor_ui(ui, project);
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ctx: &egui::Context, project: &mut ProjectSettings) {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, project: &mut ProjectSettings) {
|
||||
// Show the editor window if enabled
|
||||
let mut show = self.show_editor;
|
||||
if show {
|
||||
@@ -273,11 +274,11 @@ impl MainEditor {
|
||||
.default_width(1000.0)
|
||||
.default_height(800.0)
|
||||
.open(&mut show)
|
||||
.show(ctx, |ui| {
|
||||
.show(ui, |ui| {
|
||||
self.render_ui(project, ui);
|
||||
});
|
||||
} else {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::CentralPanel::default().show(ui, |ui| {
|
||||
self.render_ui(project, ui);
|
||||
});
|
||||
}
|
||||
@@ -288,9 +289,8 @@ impl MainEditor {
|
||||
|
||||
fn preview_ui(&mut self, ui: &mut egui::Ui) {
|
||||
// Preview area
|
||||
egui::SidePanel::right("preview_panel")
|
||||
egui::Panel::right("preview_panel")
|
||||
.resizable(true)
|
||||
.default_width(ui.available_width() / 2.0)
|
||||
.show_inside(ui, |ui| {
|
||||
// Preview area with centered content and max width
|
||||
egui::ScrollArea::both()
|
||||
@@ -343,7 +343,6 @@ impl MainEditor {
|
||||
.id_source("MainEditor_editor")
|
||||
.font(egui::TextStyle::Monospace)
|
||||
.interactive(true)
|
||||
.frame(false)
|
||||
.lock_focus(true)
|
||||
.hint_text("Type here...")
|
||||
.desired_width(max_width as f32),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::model::WorldcoderCtx;
|
||||
|
||||
pub mod asset_editor;
|
||||
pub mod content_editor;
|
||||
pub mod note_editor;
|
||||
@@ -5,3 +7,7 @@ pub mod object_editor;
|
||||
pub mod settings_editor;
|
||||
pub mod tags;
|
||||
pub mod template_editor;
|
||||
|
||||
pub trait Editor {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, ctx: &mut WorldcoderCtx);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ use std::path::Path;
|
||||
|
||||
use egui::TextEdit;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
editors::tags::Tag,
|
||||
filesystem::{FILESYSTEM, LegacyFileSystem},
|
||||
services::persist::{FILESYSTEM, LegacyFileSystem},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -20,7 +21,7 @@ pub struct Note {
|
||||
pub subject: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
pub tags: Vec<Uuid>,
|
||||
|
||||
#[serde(skip)]
|
||||
#[serde(default = "default_saved")]
|
||||
@@ -58,13 +59,13 @@ impl Note {
|
||||
|
||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let id = &self.id;
|
||||
FILESYSTEM.write(Path::new(&format!("notes/{id}.json")), self.clone())?;
|
||||
FILESYSTEM.write(Path::new(&format!("notes/{id}.ron")), self.clone())?;
|
||||
self.saved = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut note: Self = FILESYSTEM.read(Path::new(&format!("notes/{id}.json")))?;
|
||||
let mut note: Self = FILESYSTEM.read(Path::new(&format!("notes/{id}.ron")))?;
|
||||
note.id = id.to_string();
|
||||
note.saved = true;
|
||||
Ok(note)
|
||||
@@ -98,7 +99,7 @@ impl Note {
|
||||
|
||||
if ui.button("Delete").clicked() {
|
||||
if let Err(e) =
|
||||
FILESYSTEM.delete(Path::new(&format!("notes/{id}.json", id = self.id)))
|
||||
FILESYSTEM.delete(Path::new(&format!("notes/{id}.ron", id = self.id)))
|
||||
{
|
||||
eprintln!("Failed to delete: {e}");
|
||||
}
|
||||
@@ -114,7 +115,6 @@ impl Note {
|
||||
ui.separator();
|
||||
if TextEdit::singleline(&mut self.name)
|
||||
.desired_width(f32::INFINITY)
|
||||
.frame(false)
|
||||
.show(ui)
|
||||
.response
|
||||
.changed()
|
||||
@@ -144,7 +144,6 @@ impl Note {
|
||||
ui.separator();
|
||||
if TextEdit::singleline(&mut self.subject)
|
||||
.desired_width(f32::INFINITY)
|
||||
.frame(false)
|
||||
.show(ui)
|
||||
.response
|
||||
.changed()
|
||||
@@ -164,7 +163,6 @@ impl Note {
|
||||
if TextEdit::multiline(&mut self.content)
|
||||
.desired_width(f32::INFINITY)
|
||||
.desired_rows(5)
|
||||
.frame(false)
|
||||
.show(ui)
|
||||
.response
|
||||
.changed()
|
||||
|
||||
+401
-152
@@ -1,110 +1,34 @@
|
||||
use core::f32;
|
||||
use egui::{CollapsingHeader, RichText, Sense, TextEdit, Ui, UiBuilder, vec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::{path::Path, str::FromStr};
|
||||
use uuid::Uuid;
|
||||
use zxq5_id_lib::Identifiable;
|
||||
|
||||
use crate::{
|
||||
PROJECT_FOLDER, RightPanelContent,
|
||||
editors::{
|
||||
tags::Tag,
|
||||
template_editor::{FieldValue, Template},
|
||||
editors::{Editor, tags::Tag},
|
||||
model::{
|
||||
ModelElement, WorldcoderCtx,
|
||||
object::Object,
|
||||
template::{FieldDefinition, FieldType, FieldValue, Template},
|
||||
},
|
||||
filesystem::{FILESYSTEM, LegacyFileSystem},
|
||||
util,
|
||||
services::persist::{FILESYSTEM, LegacyFileSystem},
|
||||
util::{self, WorldcoderError},
|
||||
};
|
||||
|
||||
pub type ObjectId = String;
|
||||
pub struct ObjectEditor {
|
||||
pub object: Object,
|
||||
pub template: Template,
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ObjectInstance {
|
||||
// template info
|
||||
pub id: ObjectId,
|
||||
pub template_id: String,
|
||||
|
||||
// instance info
|
||||
pub name: String,
|
||||
pub fields: std::collections::HashMap<String, FieldValue>,
|
||||
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub saved: bool,
|
||||
|
||||
#[serde(skip)]
|
||||
pub dialog: Option<egui_file::FileDialog>,
|
||||
pub error: Option<WorldcoderError>,
|
||||
}
|
||||
|
||||
impl Clone for ObjectInstance {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
id: self.id.clone(),
|
||||
template_id: self.template_id.clone(),
|
||||
name: self.name.clone(),
|
||||
fields: self.fields.clone(),
|
||||
tags: self.tags.clone(),
|
||||
saved: self.saved,
|
||||
dialog: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ObjectInstance {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
template_id: "new_template_instance".to_string(),
|
||||
name: "new_object".to_string(),
|
||||
fields: std::collections::HashMap::new(),
|
||||
tags: Vec::new(),
|
||||
saved: false,
|
||||
dialog: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectInstance {
|
||||
pub fn new(template: &Template) -> Self {
|
||||
let mut fields = std::collections::HashMap::new();
|
||||
|
||||
for field in &template.fields {
|
||||
fields.insert(field.name.clone(), FieldValue::from_type(&field.field_type));
|
||||
}
|
||||
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
template_id: template.id.clone(),
|
||||
name: "new_object".to_string(),
|
||||
fields,
|
||||
tags: Vec::new(),
|
||||
saved: false,
|
||||
dialog: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let id = &self.id;
|
||||
FILESYSTEM.write(Path::new(&format!("objects/{id}.json")), self.clone())?;
|
||||
self.saved = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut instance: Self = FILESYSTEM.read(Path::new(&format!("objects/{id}.json")))?;
|
||||
instance.saved = true;
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
pub fn ui(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
template: &Template,
|
||||
right_panel: &mut Option<RightPanelContent>,
|
||||
objects: &mut [ObjectInstance],
|
||||
) {
|
||||
let _ = right_panel;
|
||||
impl Editor for ObjectEditor {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, ctx: &mut crate::model::WorldcoderCtx) {
|
||||
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||
if let Err(e) = self.save() {
|
||||
if let Err(e) = self.object.save(ctx) {
|
||||
eprintln!("Failed to save: {e}");
|
||||
}
|
||||
}
|
||||
@@ -112,32 +36,28 @@ impl ObjectInstance {
|
||||
ui.vertical(|ui| {
|
||||
// Show save status and button
|
||||
|
||||
util::saved_status(ui, self.saved, &self.id, &self.name);
|
||||
util::saved_status(
|
||||
ui,
|
||||
self.saved,
|
||||
&self.object.id().to_string(),
|
||||
&self.object.name,
|
||||
);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Save").clicked() {
|
||||
if let Err(e) = self.save() {
|
||||
if let Err(e) = self.object.save(ctx) {
|
||||
eprintln!("Failed to save: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("Create Copy").clicked() {
|
||||
let mut copy = self.clone();
|
||||
copy.id = uuid::Uuid::new_v4().to_string();
|
||||
copy.dialog = None;
|
||||
copy.name = format!("{} (Copy)", self.name);
|
||||
copy.save().unwrap();
|
||||
|
||||
*right_panel = Some(RightPanelContent::Object(Box::new(copy)));
|
||||
self.object = self.object.clone().copy();
|
||||
}
|
||||
|
||||
if ui.button("Delete").clicked() {
|
||||
let id = &self.id;
|
||||
FILESYSTEM
|
||||
.delete(Path::new(&format!("objects/{id}.json")))
|
||||
.expect("Failed to delete object");
|
||||
|
||||
*right_panel = Some(RightPanelContent::None);
|
||||
if let Err(e) = self.object.delete(ctx) {
|
||||
self.error = Some(WorldcoderError::from_fs(e));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -151,9 +71,8 @@ impl ObjectInstance {
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
ui.separator();
|
||||
let _ = TextEdit::singleline(&mut self.name)
|
||||
let _ = TextEdit::singleline(&mut self.object.name)
|
||||
.desired_width(f32::INFINITY)
|
||||
.frame(false)
|
||||
.show(ui)
|
||||
.response;
|
||||
ui.separator();
|
||||
@@ -163,12 +82,12 @@ impl ObjectInstance {
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
ui.separator();
|
||||
Tag::selector_ui(&mut self.tags, ui, Some(&mut self.saved));
|
||||
Tag::selector_ui(&mut self.object.tags, ui, Some(&mut self.saved));
|
||||
ui.separator();
|
||||
});
|
||||
|
||||
for field_def in &template.fields {
|
||||
if let Some(field_value) = self.fields.get_mut(&field_def.name) {
|
||||
for field_def in &self.template.fields {
|
||||
if let Some(field_value) = self.object.fields.get_mut(&field_def.name) {
|
||||
let id = ui.make_persistent_id(format!("field_{}", field_def.name));
|
||||
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||
ui.ctx(),
|
||||
@@ -179,13 +98,11 @@ impl ObjectInstance {
|
||||
ui.strong(&field_def.name);
|
||||
})
|
||||
.body(|ui| {
|
||||
if let Some(desc) = &field_def.description {
|
||||
ui.label(RichText::new(desc).italics().weak());
|
||||
}
|
||||
ui.label(RichText::new(&field_def.description).italics().weak());
|
||||
|
||||
ui.separator();
|
||||
|
||||
Self::render_field(field_value, ui, &mut self.saved, objects);
|
||||
Self::render_field(field_def, field_value, ui, &mut self.saved, ctx);
|
||||
|
||||
ui.separator();
|
||||
});
|
||||
@@ -194,18 +111,30 @@ impl ObjectInstance {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectEditor {
|
||||
pub fn new(object: Object, template: Template) -> Self {
|
||||
Self {
|
||||
object,
|
||||
template,
|
||||
|
||||
saved: false,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_field(
|
||||
field_value: &mut FieldValue,
|
||||
definition: &FieldDefinition,
|
||||
value: &mut FieldValue,
|
||||
ui: &mut egui::Ui,
|
||||
saved: &mut bool,
|
||||
objects: &mut [ObjectInstance],
|
||||
ctx: &mut WorldcoderCtx,
|
||||
) {
|
||||
match field_value {
|
||||
FieldValue::SingleLine(value) => {
|
||||
if TextEdit::singleline(value)
|
||||
match (&definition.field_type, value) {
|
||||
(FieldType::SingleLine, FieldValue::SingleLine(text)) => {
|
||||
if TextEdit::singleline(text)
|
||||
.desired_width(f32::INFINITY)
|
||||
.frame(false)
|
||||
.show(ui)
|
||||
.response
|
||||
.changed()
|
||||
@@ -213,11 +142,10 @@ impl ObjectInstance {
|
||||
*saved = false;
|
||||
}
|
||||
}
|
||||
FieldValue::MultiLine(value) => {
|
||||
if TextEdit::multiline(value)
|
||||
(FieldType::MultiLine, FieldValue::MultiLine(text)) => {
|
||||
if TextEdit::multiline(text)
|
||||
.desired_width(f32::INFINITY)
|
||||
.desired_rows(5)
|
||||
.frame(false)
|
||||
.show(ui)
|
||||
.response
|
||||
.changed()
|
||||
@@ -225,32 +153,31 @@ impl ObjectInstance {
|
||||
*saved = false;
|
||||
}
|
||||
}
|
||||
FieldValue::Date(value) => {
|
||||
let response = ui.add(egui_extras::DatePickerButton::new(value));
|
||||
(FieldType::Date, FieldValue::Date(date)) => {
|
||||
let response = ui.add(egui_extras::DatePickerButton::new(date));
|
||||
if response.changed() {
|
||||
*saved = false;
|
||||
}
|
||||
}
|
||||
FieldValue::Number(value) => {
|
||||
let response = ui.add(egui::DragValue::new(value).speed(0.1));
|
||||
(FieldType::Number, FieldValue::Number(num)) => {
|
||||
let response = ui.add(egui::DragValue::new(num).speed(0.1));
|
||||
if response.changed() {
|
||||
*saved = false;
|
||||
}
|
||||
}
|
||||
FieldValue::Image(value) => {
|
||||
(FieldType::Image, FieldValue::Image(image)) => {
|
||||
ui.scope_builder(UiBuilder::new().sense(Sense::HOVER), |ui| {
|
||||
let id = ui.make_persistent_id("is_hovered");
|
||||
let should_show = value.is_empty()
|
||||
let should_show = image.is_empty()
|
||||
|| ui.response().hovered()
|
||||
|| ui.memory(|mem| mem.data.get_temp(id).unwrap_or(false))
|
||||
|| !PROJECT_FOLDER.join("assets").join(&value).exists();
|
||||
|| !PROJECT_FOLDER.join("assets").join(&image).exists();
|
||||
|
||||
// Simple path input for now
|
||||
if should_show {
|
||||
let response = TextEdit::singleline(value)
|
||||
let response = TextEdit::singleline(image)
|
||||
.hint_text("Asset name (ignore file extension)")
|
||||
.desired_width(f32::INFINITY)
|
||||
.frame(false)
|
||||
.show(ui)
|
||||
.response;
|
||||
|
||||
@@ -264,8 +191,8 @@ impl ObjectInstance {
|
||||
}
|
||||
|
||||
// If we have a valid path, try to display a preview
|
||||
if !value.is_empty() {
|
||||
let path = PROJECT_FOLDER.join("assets").join(&value);
|
||||
if !image.is_empty() {
|
||||
let path = PROJECT_FOLDER.join("assets").join(&image);
|
||||
|
||||
if let Ok(bytes) = FILESYSTEM.read_bytes(&path) {
|
||||
let image_source = egui::ImageSource::Bytes {
|
||||
@@ -279,29 +206,27 @@ impl ObjectInstance {
|
||||
}
|
||||
});
|
||||
}
|
||||
FieldValue::Link(template_id) => {
|
||||
ObjectInstance::selector_ui(template_id, objects, ui, saved)
|
||||
(FieldType::Link(template_id), FieldValue::Link(object_id)) => {
|
||||
Self::selector_ui(template_id.clone(), object_id, ui, saved, ctx);
|
||||
}
|
||||
FieldValue::Links(_template_ids) => {
|
||||
(FieldType::Links(template_id), FieldValue::Links(object_ids)) => {
|
||||
let mut value = String::new();
|
||||
if ui.text_edit_singleline(&mut value).changed() {
|
||||
*saved = false;
|
||||
}
|
||||
}
|
||||
(_, _) => todo!("ERROR CASE: Handle when field type and field value do not match!!"),
|
||||
};
|
||||
}
|
||||
|
||||
fn selector_ui(
|
||||
selected: &mut ObjectId,
|
||||
objects: &mut [ObjectInstance],
|
||||
template: Uuid,
|
||||
selected: &mut Option<Uuid>,
|
||||
ui: &mut egui::Ui,
|
||||
saved: &mut bool,
|
||||
ctx: &mut WorldcoderCtx,
|
||||
) {
|
||||
if !selected.is_empty() {
|
||||
if let Ok(object) = ObjectInstance::load(selected) {
|
||||
ui.strong(&object.name);
|
||||
}
|
||||
}
|
||||
let objects: Vec<Object> = Vec::new();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let id = ui.make_persistent_id("new_object");
|
||||
@@ -310,9 +235,7 @@ impl ObjectInstance {
|
||||
let mut object_selection: usize =
|
||||
ctx.memory(|mem| mem.data.get_temp::<usize>(id).unwrap_or(0));
|
||||
|
||||
if objects.is_empty() {
|
||||
ui.label("No objects available");
|
||||
} else {
|
||||
if !objects.is_empty() {
|
||||
egui::ComboBox::from_id_salt(id)
|
||||
.selected_text(&objects[object_selection].name)
|
||||
.show_ui(ui, |ui| {
|
||||
@@ -320,6 +243,8 @@ impl ObjectInstance {
|
||||
ui.selectable_value(&mut object_selection, i, &obj.name);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.label("No objects available");
|
||||
}
|
||||
|
||||
let ctx = ui.ctx();
|
||||
@@ -328,14 +253,338 @@ impl ObjectInstance {
|
||||
});
|
||||
|
||||
if ui.button("Set").clicked() && object_selection < objects.len() {
|
||||
*selected = objects[object_selection].id.clone();
|
||||
*selected = Some(objects[object_selection].id().clone());
|
||||
*saved = false;
|
||||
}
|
||||
|
||||
if ui.button("Remove").clicked() {
|
||||
*selected = String::new();
|
||||
selected.take();
|
||||
*saved = false;
|
||||
}
|
||||
});
|
||||
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// pub type ObjectId = String;
|
||||
//
|
||||
// #[derive(Debug, Serialize, Deserialize)]
|
||||
// pub struct ObjectInstance {
|
||||
// // template info
|
||||
// pub id: ObjectId,
|
||||
// pub template_id: String,
|
||||
//
|
||||
// // instance info
|
||||
// pub name: String,
|
||||
// pub fields: std::collections::HashMap<String, FieldValue>,
|
||||
//
|
||||
// #[serde(default)]
|
||||
// pub tags: Vec<String>,
|
||||
//
|
||||
// #[serde(skip)]
|
||||
// pub saved: bool,
|
||||
//
|
||||
// #[serde(skip)]
|
||||
// pub dialog: Option<egui_file::FileDialog>,
|
||||
// }
|
||||
//
|
||||
// impl Clone for ObjectInstance {
|
||||
// fn clone(&self) -> Self {
|
||||
// Self {
|
||||
// id: self.id.clone(),
|
||||
// template_id: self.template_id.clone(),
|
||||
// name: self.name.clone(),
|
||||
// fields: self.fields.clone(),
|
||||
// tags: self.tags.clone(),
|
||||
// saved: self.saved,
|
||||
// dialog: None,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl Default for ObjectInstance {
|
||||
// fn default() -> Self {
|
||||
// Self {
|
||||
// id: uuid::Uuid::new_v4().to_string(),
|
||||
// template_id: "new_template_instance".to_string(),
|
||||
// name: "new_object".to_string(),
|
||||
// fields: std::collections::HashMap::new(),
|
||||
// tags: Vec::new(),
|
||||
// saved: false,
|
||||
// dialog: None,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl ObjectInstance {
|
||||
// pub fn new(template: &Template) -> Self {
|
||||
// let mut fields = std::collections::HashMap::new();
|
||||
//
|
||||
// for field in &template.fields {
|
||||
// fields.insert(field.name.clone(), FieldValue::from_type(&field.field_type));
|
||||
// }
|
||||
//
|
||||
// Self {
|
||||
// id: uuid::Uuid::new_v4().to_string(),
|
||||
// template_id: template.id().to_string(),
|
||||
// name: "new_object".to_string(),
|
||||
// fields,
|
||||
// tags: Vec::new(),
|
||||
// saved: false,
|
||||
// dialog: None,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// let id = &self.id;
|
||||
// FILESYSTEM.write(Path::new(&format!("objects/{id}.ron")), self.clone())?;
|
||||
// self.saved = true;
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
// let mut instance: Self = FILESYSTEM.read(Path::new(&format!("objects/{id}.ron")))?;
|
||||
// instance.saved = true;
|
||||
// Ok(instance)
|
||||
// }
|
||||
//
|
||||
// pub fn ui(
|
||||
// &mut self,
|
||||
// ui: &mut Ui,
|
||||
// template: &Template,
|
||||
// right_panel: &mut Option<RightPanelContent>,
|
||||
// objects: &mut [ObjectInstance],
|
||||
// ) {
|
||||
// let _ = right_panel;
|
||||
// if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||
// if let Err(e) = self.save() {
|
||||
// eprintln!("Failed to save: {e}");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// ui.vertical(|ui| {
|
||||
// // Show save status and button
|
||||
//
|
||||
// util::saved_status(ui, self.saved, &self.id, &self.name);
|
||||
//
|
||||
// ui.horizontal(|ui| {
|
||||
// if ui.button("Save").clicked() {
|
||||
// if let Err(e) = self.save() {
|
||||
// eprintln!("Failed to save: {e}");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if ui.button("Create Copy").clicked() {
|
||||
// let mut copy = self.clone();
|
||||
// copy.id = uuid::Uuid::new_v4().to_string();
|
||||
// copy.dialog = None;
|
||||
// copy.name = format!("{} (Copy)", self.name);
|
||||
// copy.save().unwrap();
|
||||
//
|
||||
// *right_panel = Some(RightPanelContent::Object(Box::new(copy)));
|
||||
// }
|
||||
//
|
||||
// if ui.button("Delete").clicked() {
|
||||
// let id = &self.id;
|
||||
// FILESYSTEM
|
||||
// .delete(Path::new(&format!("objects/{id}.ron")))
|
||||
// .expect("Failed to delete object");
|
||||
//
|
||||
// *right_panel = Some(RightPanelContent::None);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// ui.separator();
|
||||
//
|
||||
// egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
// // Render each field
|
||||
//
|
||||
// // allow name to be edited
|
||||
// CollapsingHeader::new("Name")
|
||||
// .default_open(true)
|
||||
// .show(ui, |ui| {
|
||||
// ui.separator();
|
||||
// let _ = TextEdit::singleline(&mut self.name)
|
||||
// .desired_width(f32::INFINITY)
|
||||
// .show(ui)
|
||||
// .response;
|
||||
// ui.separator();
|
||||
// });
|
||||
//
|
||||
// CollapsingHeader::new("Tags")
|
||||
// .default_open(true)
|
||||
// .show(ui, |ui| {
|
||||
// ui.separator();
|
||||
// Tag::selector_ui(&mut self.tags, ui, Some(&mut self.saved));
|
||||
// ui.separator();
|
||||
// });
|
||||
//
|
||||
// for field_def in &template.fields {
|
||||
// if let Some(field_value) = self.fields.get_mut(&field_def.name) {
|
||||
// let id = ui.make_persistent_id(format!("field_{}", field_def.name));
|
||||
// egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||
// ui.ctx(),
|
||||
// id,
|
||||
// true,
|
||||
// )
|
||||
// .show_header(ui, |ui| {
|
||||
// ui.strong(&field_def.name);
|
||||
// })
|
||||
// .body(|ui| {
|
||||
// ui.label(RichText::new(&field_def.description).italics().weak());
|
||||
//
|
||||
// ui.separator();
|
||||
//
|
||||
// Self::render_field(field_value, ui, &mut self.saved, objects);
|
||||
//
|
||||
// ui.separator();
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// fn render_field(
|
||||
// field_value: &mut FieldValue,
|
||||
// ui: &mut egui::Ui,
|
||||
// saved: &mut bool,
|
||||
// objects: &mut [ObjectInstance],
|
||||
// ) {
|
||||
// match field_value {
|
||||
// FieldValue::SingleLine(value) => {
|
||||
// if TextEdit::singleline(value)
|
||||
// .desired_width(f32::INFINITY)
|
||||
// .show(ui)
|
||||
// .response
|
||||
// .changed()
|
||||
// {
|
||||
// *saved = false;
|
||||
// }
|
||||
// }
|
||||
// FieldValue::MultiLine(value) => {
|
||||
// if TextEdit::multiline(value)
|
||||
// .desired_width(f32::INFINITY)
|
||||
// .desired_rows(5)
|
||||
// .show(ui)
|
||||
// .response
|
||||
// .changed()
|
||||
// {
|
||||
// *saved = false;
|
||||
// }
|
||||
// }
|
||||
// FieldValue::Date(value) => {
|
||||
// let response = ui.add(egui_extras::DatePickerButton::new(value));
|
||||
// if response.changed() {
|
||||
// *saved = false;
|
||||
// }
|
||||
// }
|
||||
// FieldValue::Number(value) => {
|
||||
// let response = ui.add(egui::DragValue::new(value).speed(0.1));
|
||||
// if response.changed() {
|
||||
// *saved = false;
|
||||
// }
|
||||
// }
|
||||
// FieldValue::Image(value) => {
|
||||
// ui.scope_builder(UiBuilder::new().sense(Sense::HOVER), |ui| {
|
||||
// let id = ui.make_persistent_id("is_hovered");
|
||||
// let should_show = value.is_empty()
|
||||
// || ui.response().hovered()
|
||||
// || ui.memory(|mem| mem.data.get_temp(id).unwrap_or(false))
|
||||
// || !PROJECT_FOLDER.join("assets").join(&value).exists();
|
||||
//
|
||||
// // Simple path input for now
|
||||
// if should_show {
|
||||
// let response = TextEdit::singleline(value)
|
||||
// .hint_text("Asset name (ignore file extension)")
|
||||
// .desired_width(f32::INFINITY)
|
||||
// .show(ui)
|
||||
// .response;
|
||||
//
|
||||
// if response.changed() {
|
||||
// *saved = false;
|
||||
// }
|
||||
//
|
||||
// ui.memory_mut(|mem| {
|
||||
// *mem.data.get_temp_mut_or_insert_with(id, || true) = response.hovered();
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// // If we have a valid path, try to display a preview
|
||||
// if !value.is_empty() {
|
||||
// let path = PROJECT_FOLDER.join("assets").join(&value);
|
||||
//
|
||||
// if let Ok(bytes) = FILESYSTEM.read_bytes(&path) {
|
||||
// let image_source = egui::ImageSource::Bytes {
|
||||
// uri: std::borrow::Cow::Owned(path.to_str().unwrap().to_string()),
|
||||
// bytes: bytes.into(),
|
||||
// };
|
||||
// ui.add(
|
||||
// egui::Image::new(image_source).max_size(vec2(256.0, f32::INFINITY)),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// FieldValue::Link(template_id) => {
|
||||
// ObjectInstance::selector_ui(template_id, objects, ui, saved)
|
||||
// }
|
||||
// FieldValue::Links(_template_ids) => {
|
||||
// let mut value = String::new();
|
||||
// if ui.text_edit_singleline(&mut value).changed() {
|
||||
// *saved = false;
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// fn selector_ui(
|
||||
// selected: &mut ObjectId,
|
||||
// objects: &mut [ObjectInstance],
|
||||
// ui: &mut egui::Ui,
|
||||
// saved: &mut bool,
|
||||
// ) {
|
||||
// if !selected.is_empty() {
|
||||
// if let Ok(object) = ObjectInstance::load(selected) {
|
||||
// ui.strong(&object.name);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// ui.horizontal(|ui| {
|
||||
// let id = ui.make_persistent_id("new_object");
|
||||
//
|
||||
// let ctx = ui.ctx();
|
||||
// let mut object_selection: usize =
|
||||
// ctx.memory(|mem| mem.data.get_temp::<usize>(id).unwrap_or(0));
|
||||
//
|
||||
// if objects.is_empty() {
|
||||
// ui.label("No objects available");
|
||||
// } else {
|
||||
// egui::ComboBox::from_id_salt(id)
|
||||
// .selected_text(&objects[object_selection].name)
|
||||
// .show_ui(ui, |ui| {
|
||||
// for (i, obj) in objects.iter().enumerate() {
|
||||
// ui.selectable_value(&mut object_selection, i, &obj.name);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// let ctx = ui.ctx();
|
||||
// ctx.memory_mut(|mem| {
|
||||
// *mem.data.get_temp_mut_or_default::<usize>(id) = object_selection;
|
||||
// });
|
||||
//
|
||||
// if ui.button("Set").clicked() && object_selection < objects.len() {
|
||||
// *selected = objects[object_selection].id.clone();
|
||||
// *saved = false;
|
||||
// }
|
||||
//
|
||||
// if ui.button("Remove").clicked() {
|
||||
// *selected = String::new();
|
||||
// *saved = false;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use chrono::NaiveDate;
|
||||
use egui::TextEdit;
|
||||
use egui_extras::DatePickerButton;
|
||||
use jiff::{Zoned, civil::Date};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{
|
||||
filesystem::{FILESYSTEM, LegacyFileSystem},
|
||||
services::persist::{FILESYSTEM, LegacyFileSystem},
|
||||
util::saved_status,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct ProjectSettings {
|
||||
date: NaiveDate,
|
||||
date: Date,
|
||||
project_name: String,
|
||||
project_author: String,
|
||||
project_description: String,
|
||||
@@ -40,7 +40,7 @@ impl ProjectSettings {
|
||||
}
|
||||
|
||||
pub fn load() -> Self {
|
||||
if let Ok(mut proj) = FILESYSTEM.read::<Self>(Path::new("project.json")) {
|
||||
if let Ok(mut proj) = FILESYSTEM.read::<Self>(Path::new("project.ron")) {
|
||||
proj.saved = true;
|
||||
proj.global_settings = EditorSettings::load_global();
|
||||
proj.local_overrides = EditorSettings::load();
|
||||
@@ -52,7 +52,7 @@ impl ProjectSettings {
|
||||
|
||||
pub fn save(&mut self) {
|
||||
FILESYSTEM
|
||||
.write(Path::new("project.json"), self.clone())
|
||||
.write(Path::new("project.ron"), self.clone())
|
||||
.unwrap();
|
||||
|
||||
self.global_settings.save();
|
||||
@@ -181,7 +181,6 @@ impl ProjectSettings {
|
||||
if ui.add(TextEdit::multiline(&mut self.ai_context)
|
||||
.font(egui::TextStyle::Monospace)
|
||||
.interactive(true)
|
||||
.frame(false)
|
||||
.lock_focus(true)
|
||||
.hint_text("What is this project about? what should the LLM know when generating content for this project?")).changed() { self.saved = false };
|
||||
ui.end_row();
|
||||
@@ -298,7 +297,7 @@ impl ProjectSettings {
|
||||
impl Default for ProjectSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
date: chrono::Local::now().naive_local().into(),
|
||||
date: Zoned::now().date(),
|
||||
project_name: "New Project".to_string(),
|
||||
project_author: "Your Name".to_string(),
|
||||
project_description: "Description of your project".to_string(),
|
||||
@@ -363,7 +362,7 @@ impl EditorSettings {
|
||||
}
|
||||
|
||||
pub fn load() -> Self {
|
||||
if let Ok(mut settings) = FILESYSTEM.read::<Self>(Path::new("settings.json")) {
|
||||
if let Ok(mut settings) = FILESYSTEM.read::<Self>(Path::new("settings.ron")) {
|
||||
settings.is_global = false;
|
||||
return settings;
|
||||
}
|
||||
@@ -375,7 +374,7 @@ impl EditorSettings {
|
||||
let path = if self.is_global {
|
||||
FILESYSTEM.config_path()
|
||||
} else {
|
||||
PathBuf::from("settings.json")
|
||||
PathBuf::from("settings.ron")
|
||||
};
|
||||
|
||||
FILESYSTEM.write(path.as_path(), self.clone()).unwrap()
|
||||
|
||||
+19
-22
@@ -1,11 +1,12 @@
|
||||
use egui::{Response, RichText, TextEdit};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{PROJECT_FOLDER, util};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Tag {
|
||||
pub id: String,
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub color: egui::Color32,
|
||||
@@ -14,13 +15,13 @@ pub struct Tag {
|
||||
pub saved: bool,
|
||||
|
||||
#[serde(skip)]
|
||||
pub error: Option<util::Error>,
|
||||
pub error: Option<util::WorldcoderError>,
|
||||
}
|
||||
|
||||
impl Default for Tag {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
color: egui::Color32::from_rgb(20, 20, 20),
|
||||
@@ -78,7 +79,7 @@ impl Tag {
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
util::saved_status(ui, self.saved, &self.id, &self.name);
|
||||
util::saved_status(ui, self.saved, &self.id.to_string(), &self.name);
|
||||
|
||||
if let Some(error) = &mut self.error {
|
||||
error.show(ui);
|
||||
@@ -86,7 +87,9 @@ impl Tag {
|
||||
|
||||
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||
if let Err(e) = self.save() {
|
||||
self.error = Some(util::Error::new(format!("Failed to save tag: {e}")));
|
||||
self.error = Some(util::WorldcoderError::new(format!(
|
||||
"Failed to save tag: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,11 +99,7 @@ impl Tag {
|
||||
.show(ui, |ui| {
|
||||
ui.strong("Name");
|
||||
if ui
|
||||
.add(
|
||||
TextEdit::singleline(&mut self.name)
|
||||
.desired_width(f32::INFINITY)
|
||||
.frame(false),
|
||||
)
|
||||
.add(TextEdit::singleline(&mut self.name).desired_width(f32::INFINITY))
|
||||
.changed()
|
||||
{
|
||||
self.saved = false;
|
||||
@@ -109,11 +108,7 @@ impl Tag {
|
||||
|
||||
ui.strong("Description");
|
||||
if ui
|
||||
.add(
|
||||
TextEdit::singleline(&mut self.description)
|
||||
.desired_width(f32::INFINITY)
|
||||
.frame(false),
|
||||
)
|
||||
.add(TextEdit::singleline(&mut self.description).desired_width(f32::INFINITY))
|
||||
.changed()
|
||||
{
|
||||
self.saved = false;
|
||||
@@ -128,7 +123,7 @@ impl Tag {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn selector_ui(tag_ids: &mut Vec<String>, ui: &mut egui::Ui, saved: Option<&mut bool>) {
|
||||
pub fn selector_ui(tag_ids: &mut Vec<Uuid>, ui: &mut egui::Ui, saved: Option<&mut bool>) {
|
||||
// remove duplicate tag ids
|
||||
tag_ids.sort();
|
||||
tag_ids.dedup();
|
||||
@@ -177,7 +172,7 @@ impl Tag {
|
||||
});
|
||||
|
||||
for (i, tag_id) in tag_ids.iter().enumerate() {
|
||||
if let Ok(mut tag) = Self::load(tag_id) {
|
||||
if let Ok(mut tag) = Self::load(*tag_id) {
|
||||
if tag.display_ui(ui) {
|
||||
remove.push(i)
|
||||
}
|
||||
@@ -202,14 +197,16 @@ impl Tag {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let path = PROJECT_FOLDER.join("tags").join(format!("{id}.json"));
|
||||
pub fn load(id: Uuid) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let path = PROJECT_FOLDER.join("tags").join(format!("{id}.ron"));
|
||||
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if self.name.is_empty() {
|
||||
self.error = Some(util::Error::new("Tag name cannot be empty".to_string()));
|
||||
self.error = Some(util::WorldcoderError::new(
|
||||
"Tag name cannot be empty".to_string(),
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -217,7 +214,7 @@ impl Tag {
|
||||
|
||||
let path = PROJECT_FOLDER
|
||||
.join("tags")
|
||||
.join(format!("{}.json", &self.id));
|
||||
.join(format!("{}.ron", &self.id));
|
||||
let content = serde_json::to_string_pretty(self)?;
|
||||
std::fs::write(path, content)?;
|
||||
self.saved = true;
|
||||
@@ -232,7 +229,7 @@ impl Tag {
|
||||
if tags_folder.exists() {
|
||||
for entry in std::fs::read_dir(tags_folder).unwrap() {
|
||||
let path = entry.unwrap().path();
|
||||
if path.is_file() && path.extension().unwrap() == "json" {
|
||||
if path.is_file() && path.extension().unwrap() == "ron" {
|
||||
let tag =
|
||||
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
|
||||
tags.push(tag);
|
||||
|
||||
+74
-247
@@ -1,244 +1,101 @@
|
||||
use chrono::NaiveDate;
|
||||
use core::fmt;
|
||||
use egui::ScrollArea;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::mem;
|
||||
use zxq5_id_lib::Identifiable;
|
||||
|
||||
use crate::{
|
||||
RightPanelContent,
|
||||
editors::object_editor::ObjectInstance,
|
||||
filesystem::{FILESYSTEM, LegacyFileSystem},
|
||||
util::{self, Error},
|
||||
editors::Editor,
|
||||
model::{
|
||||
ModelElement,
|
||||
template::{FieldDefinition, FieldType, Template},
|
||||
},
|
||||
util::{self, WorldcoderError},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum FieldType {
|
||||
Image,
|
||||
SingleLine,
|
||||
MultiLine,
|
||||
Date,
|
||||
Number,
|
||||
Link { template_id: Option<String> },
|
||||
Links,
|
||||
}
|
||||
pub struct TemplateEditor {
|
||||
// the data to edit
|
||||
pub template: Template,
|
||||
|
||||
impl Default for FieldType {
|
||||
fn default() -> Self {
|
||||
Self::SingleLine
|
||||
}
|
||||
}
|
||||
// UI Fields
|
||||
pub new_field: FieldDefinition,
|
||||
|
||||
impl FieldType {
|
||||
fn types() -> Vec<FieldType> {
|
||||
vec![
|
||||
FieldType::Image,
|
||||
FieldType::SingleLine,
|
||||
FieldType::MultiLine,
|
||||
FieldType::Date,
|
||||
FieldType::Number,
|
||||
FieldType::Link { template_id: None },
|
||||
FieldType::Links,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum FieldValue {
|
||||
Image(String),
|
||||
SingleLine(String),
|
||||
MultiLine(String),
|
||||
Date(NaiveDate),
|
||||
Number(f64),
|
||||
Link(String),
|
||||
Links(Vec<String>),
|
||||
}
|
||||
|
||||
impl FieldValue {
|
||||
pub fn from_type(_type: &FieldType) -> Self {
|
||||
match _type {
|
||||
FieldType::Image => Self::Image(String::new()),
|
||||
FieldType::SingleLine => Self::SingleLine(String::new()),
|
||||
FieldType::MultiLine => Self::MultiLine(String::new()),
|
||||
FieldType::Date => Self::Date(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()),
|
||||
FieldType::Number => Self::Number(0.0),
|
||||
FieldType::Link { template_id: None } => Self::Link(String::new()),
|
||||
FieldType::Link {
|
||||
template_id: Some(template_id),
|
||||
} => Self::Link(template_id.clone()),
|
||||
FieldType::Links => Self::Links(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FieldValue {
|
||||
fn default() -> Self {
|
||||
Self::SingleLine(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FieldDefinition {
|
||||
pub name: String,
|
||||
pub field_type: FieldType,
|
||||
pub required: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub on_preview: bool,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Template {
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
|
||||
pub description: Option<String>,
|
||||
pub fields: Vec<FieldDefinition>,
|
||||
|
||||
#[serde(skip)]
|
||||
// State
|
||||
pub saved: bool,
|
||||
|
||||
#[serde(skip)]
|
||||
pub error: Option<Error>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub new_field_name: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub new_field_type: FieldType,
|
||||
|
||||
#[serde(skip)]
|
||||
pub new_field_required: bool,
|
||||
|
||||
#[serde(skip)]
|
||||
pub new_field_description: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub new_field_on_preview: bool,
|
||||
pub error: Option<WorldcoderError>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Template {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Template")
|
||||
.field("name", &self.name)
|
||||
.field("id", &self.id)
|
||||
.field("description", &self.description)
|
||||
.field("fields", &self.fields)
|
||||
.field("saved", &self.saved)
|
||||
.field("new_field_name", &self.new_field_name)
|
||||
.field("new_field_type", &self.new_field_type)
|
||||
.field("new_field_required", &self.new_field_required)
|
||||
.field("new_field_description", &self.new_field_description)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Template {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
id: self.id.clone(),
|
||||
description: self.description.clone(),
|
||||
fields: self.fields.clone(),
|
||||
saved: self.saved,
|
||||
error: None,
|
||||
|
||||
new_field_name: "".to_string(),
|
||||
new_field_type: FieldType::default(),
|
||||
new_field_required: false,
|
||||
new_field_description: "".to_string(),
|
||||
new_field_on_preview: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Template {
|
||||
impl Default for TemplateEditor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "New Template".to_string(),
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
description: Some(String::from("Placeholder description")),
|
||||
fields: Vec::new(),
|
||||
template: Template::default(),
|
||||
new_field: FieldDefinition::default(),
|
||||
|
||||
saved: false,
|
||||
error: None,
|
||||
|
||||
new_field_name: "".to_string(),
|
||||
new_field_type: FieldType::default(),
|
||||
new_field_required: false,
|
||||
new_field_description: "".to_string(),
|
||||
new_field_on_preview: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Template {
|
||||
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut template = FILESYSTEM.read::<Self>(Path::new(&format!("templates/{id}.json")))?;
|
||||
template.saved = true;
|
||||
Ok(template)
|
||||
impl TemplateEditor {
|
||||
pub fn new(template: Template) -> Self {
|
||||
Self {
|
||||
template,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let id = &self.id;
|
||||
FILESYSTEM.write(Path::new(&format!("templates/{id}.json")), self.clone())?;
|
||||
self.saved = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, new_instance: &mut Option<RightPanelContent>) {
|
||||
if let Some(error) = &mut self.error {
|
||||
error.show(ui);
|
||||
impl Editor for TemplateEditor {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, ctx: &mut crate::model::WorldcoderCtx) {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
if let Some(e) = &mut self.error {
|
||||
e.show(ui);
|
||||
}
|
||||
|
||||
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||
if let Err(e) = self.save() {
|
||||
eprintln!("Failed to save: {e}");
|
||||
if let Err(e) = self.template.save(ctx) {
|
||||
self.error = Some(WorldcoderError::from_fs(e));
|
||||
} else {
|
||||
self.saved = true;
|
||||
}
|
||||
}
|
||||
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
util::saved_status(ui, self.saved, &self.id, &self.name);
|
||||
util::saved_status(
|
||||
ui,
|
||||
self.saved,
|
||||
&self.template.id().to_string(),
|
||||
&self.template.name,
|
||||
);
|
||||
// TODO: remove to_string() for the UUID!!
|
||||
|
||||
// Save/Cancel buttons
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Save").clicked() {
|
||||
if let Err(e) = self.save() {
|
||||
eprintln!("Failed to save: {e}");
|
||||
if let Err(e) = self.template.save(ctx) {
|
||||
self.error = Some(WorldcoderError::from_fs(e));
|
||||
} else {
|
||||
self.saved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("Create Copy").clicked() {
|
||||
let mut copy = self.clone();
|
||||
copy.id = uuid::Uuid::new_v4().to_string();
|
||||
copy.name = format!("{} (Copy)", self.name);
|
||||
copy.save().unwrap();
|
||||
self.template = self.template.clone().copy();
|
||||
}
|
||||
|
||||
if ui.button("Delete").clicked() {
|
||||
let id = &self.id;
|
||||
FILESYSTEM
|
||||
.delete(Path::new(&format!("templates/{id}.json")))
|
||||
.unwrap();
|
||||
*new_instance = Some(RightPanelContent::None);
|
||||
if let Err(e) = self.template.delete(ctx) {
|
||||
self.error = Some(WorldcoderError::from_fs(e));
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("Cancel").clicked() {
|
||||
// load default state
|
||||
*self = Self::load(&self.id).unwrap();
|
||||
if ui.button("Reset").clicked() {
|
||||
if let Err(e) = self.template.reload(ctx) {
|
||||
self.error = Some(WorldcoderError::from_fs(e));
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("Use Template").clicked() {
|
||||
if self.saved {
|
||||
*new_instance = Some(RightPanelContent::Object(Box::new(
|
||||
ObjectInstance::new(self),
|
||||
)));
|
||||
} else {
|
||||
self.error = Some(Error::new(
|
||||
"You must save the template before creating a new instance!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
todo!();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -246,32 +103,24 @@ impl Template {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateEditor {
|
||||
pub fn editor_ui(&mut self, ui: &mut egui::Ui) {
|
||||
egui::Grid::new("template_grid")
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Template Name:");
|
||||
if ui
|
||||
.add(egui::TextEdit::singleline(&mut self.name).frame(false))
|
||||
ui.add(egui::TextEdit::singleline(&mut self.template.name))
|
||||
.changed()
|
||||
{
|
||||
self.saved = false;
|
||||
}
|
||||
.then(|| self.saved = false);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Description:");
|
||||
if ui
|
||||
.add(
|
||||
egui::TextEdit::multiline(self.description.get_or_insert_with(String::new))
|
||||
.desired_rows(1)
|
||||
.frame(false),
|
||||
)
|
||||
ui.add(egui::TextEdit::multiline(&mut self.template.description).desired_rows(1))
|
||||
.changed()
|
||||
{
|
||||
self.saved = false;
|
||||
}
|
||||
.then(|| self.saved = false);
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
@@ -302,7 +151,7 @@ impl Template {
|
||||
|
||||
// List of fields
|
||||
let mut to_remove = None;
|
||||
for (i, field) in self.fields.iter_mut().enumerate() {
|
||||
for (i, field) in self.template.fields.iter_mut().enumerate() {
|
||||
let id = ui.make_persistent_id(format!("field_{i}"));
|
||||
|
||||
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
||||
@@ -313,7 +162,6 @@ impl Template {
|
||||
ui.strong(field.name.clone());
|
||||
})
|
||||
.body(|ui| {
|
||||
ui.separator();
|
||||
egui::Grid::new("field_grid")
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
@@ -356,23 +204,17 @@ impl Template {
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Description:");
|
||||
if ui
|
||||
.text_edit_singleline(
|
||||
field.description.get_or_insert_with(String::new),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
if ui.text_edit_singleline(&mut field.description).changed() {
|
||||
self.saved = false;
|
||||
}
|
||||
ui.end_row();
|
||||
ui.separator();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Remove field if needed
|
||||
if let Some(index) = to_remove {
|
||||
self.fields.remove(index);
|
||||
self.template.fields.remove(index);
|
||||
self.saved = false;
|
||||
}
|
||||
|
||||
@@ -386,16 +228,16 @@ impl Template {
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Name:");
|
||||
ui.text_edit_singleline(&mut self.new_field_name);
|
||||
ui.text_edit_singleline(&mut self.new_field.name);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Type:");
|
||||
egui::ComboBox::from_id_salt("new_field_type")
|
||||
.selected_text(format!("{:?}", self.new_field_type))
|
||||
.selected_text(format!("{:?}", self.new_field.field_type))
|
||||
.show_ui(ui, |ui| {
|
||||
for variant in FieldType::types() {
|
||||
ui.selectable_value(
|
||||
&mut self.new_field_type,
|
||||
&mut self.new_field.field_type,
|
||||
variant.clone(),
|
||||
format!("{variant:?}"),
|
||||
);
|
||||
@@ -404,36 +246,21 @@ impl Template {
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Required:");
|
||||
ui.checkbox(&mut self.new_field_required, "");
|
||||
ui.checkbox(&mut self.new_field.required, "");
|
||||
ui.end_row();
|
||||
|
||||
ui.label("On Preview:");
|
||||
ui.checkbox(&mut self.new_field_on_preview, "");
|
||||
ui.checkbox(&mut self.new_field.on_preview, "");
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Description:");
|
||||
ui.text_edit_singleline(&mut self.new_field_description);
|
||||
ui.text_edit_singleline(&mut self.new_field.description);
|
||||
ui.end_row();
|
||||
|
||||
if ui.button("Add Field").clicked() && !self.new_field_name.is_empty() {
|
||||
self.fields.push(FieldDefinition {
|
||||
name: self.new_field_name.clone(),
|
||||
field_type: self.new_field_type.clone(),
|
||||
on_preview: self.new_field_on_preview,
|
||||
required: self.new_field_required,
|
||||
description: if self.new_field_description.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.new_field_description.clone())
|
||||
},
|
||||
});
|
||||
|
||||
if ui.button("Add Field").clicked() && !self.new_field.name.is_empty() {
|
||||
let field = mem::replace(&mut self.new_field, FieldDefinition::default());
|
||||
self.template.fields.push(field);
|
||||
self.saved = false;
|
||||
|
||||
// Reset new field form
|
||||
self.new_field_name.clear();
|
||||
self.new_field_required = false;
|
||||
self.new_field_description.clear();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+34
-16
@@ -1,19 +1,19 @@
|
||||
use std::path::Path;
|
||||
|
||||
use zxq5_id_lib::Identifiable;
|
||||
|
||||
use crate::{
|
||||
PROJECT_FOLDER, RightPanelContent,
|
||||
content_editor::MainEditor,
|
||||
editors::{
|
||||
asset_editor::Asset, content_editor::ContentSection, object_editor::ObjectInstance,
|
||||
tags::Tag, template_editor::Template,
|
||||
},
|
||||
filesystem::{FILESYSTEM, FsError, LegacyFileSystem},
|
||||
editors::{asset_editor::Asset, content_editor::ContentSection, tags::Tag},
|
||||
model::{object::Object, template::Template},
|
||||
note_editor::Note,
|
||||
services::persist::{FILESYSTEM, FsError, LegacyFileSystem},
|
||||
};
|
||||
|
||||
pub struct Explorer {
|
||||
templates: Vec<Template>,
|
||||
objects: Vec<ObjectInstance>,
|
||||
objects: Vec<Object>,
|
||||
notes: Vec<Note>,
|
||||
documents: Vec<MainEditor>,
|
||||
tags: Vec<Tag>,
|
||||
@@ -30,7 +30,7 @@ impl Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn objects(&self) -> Vec<ObjectInstance> {
|
||||
pub fn objects(&self) -> Vec<Object> {
|
||||
self.objects.clone()
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ impl Explorer {
|
||||
self.load_notes().expect("Failed to load notes");
|
||||
self.load_documents().expect("Failed to load documents");
|
||||
self.load_tags().expect("Failed to load tags");
|
||||
self.load_assets().expect("Failed to load assets");
|
||||
|
||||
ui.vertical(|ui| {
|
||||
self.render_templates(ui, to_load);
|
||||
@@ -65,7 +66,7 @@ impl Explorer {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Templates");
|
||||
if ui.button("+").clicked() {
|
||||
*to_load = Some(RightPanelContent::template(Some(Template::default())));
|
||||
*to_load = Some(RightPanelContent::template(Template::default()));
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -80,26 +81,29 @@ impl Explorer {
|
||||
.show_header(ui, |ui| {
|
||||
// load the template
|
||||
if ui.selectable_label(false, template.name.clone()).clicked() {
|
||||
*to_load = Some(RightPanelContent::template(Some(template.clone())));
|
||||
*to_load = Some(RightPanelContent::template(template.clone()));
|
||||
}
|
||||
|
||||
// create a new object based on this template
|
||||
if ui.button("+").clicked() {
|
||||
*to_load = Some(RightPanelContent::object(Some(ObjectInstance::new(
|
||||
template,
|
||||
))));
|
||||
*to_load = Some(RightPanelContent::object(
|
||||
Object::new(&template),
|
||||
template.clone(),
|
||||
));
|
||||
}
|
||||
})
|
||||
.body(|ui| {
|
||||
for object in &self.objects {
|
||||
if object.template_id == template.id {
|
||||
if object.template == template.id() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(10.0);
|
||||
|
||||
// load the object
|
||||
if ui.selectable_label(false, &object.name).clicked() {
|
||||
*to_load =
|
||||
Some(RightPanelContent::object(Some(object.clone())));
|
||||
*to_load = Some(RightPanelContent::object(
|
||||
object.clone(),
|
||||
template.clone(),
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -277,6 +281,15 @@ impl Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_assets(&mut self) -> Result<(), FsError> {
|
||||
let path = Path::new("assets");
|
||||
if !FILESYSTEM.exists(path) {
|
||||
FILESYSTEM.mkdir(path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// load templates from the templates folder
|
||||
fn load_templates(&mut self) -> Result<(), FsError> {
|
||||
let path = Path::new("templates");
|
||||
@@ -304,7 +317,7 @@ impl Explorer {
|
||||
}
|
||||
let mut objects = Vec::new();
|
||||
for entry in FILESYSTEM.lsfiles(path)? {
|
||||
match FILESYSTEM.read::<ObjectInstance>(&entry) {
|
||||
match FILESYSTEM.read::<Object>(&entry) {
|
||||
Ok(o) => objects.push(o),
|
||||
Err(err) => eprintln!("Could not parse file {entry:?}: {err}"),
|
||||
}
|
||||
@@ -355,6 +368,11 @@ impl Explorer {
|
||||
}
|
||||
|
||||
fn load_tags(&mut self) -> Result<(), FsError> {
|
||||
let path = Path::new("tags");
|
||||
if !FILESYSTEM.exists(path) {
|
||||
FILESYSTEM.mkdir(path)?;
|
||||
}
|
||||
|
||||
self.tags = Tag::load_all();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
pub mod editors;
|
||||
|
||||
pub mod llm_integration;
|
||||
|
||||
pub mod components;
|
||||
mod explorer;
|
||||
mod model;
|
||||
pub mod services;
|
||||
pub mod util;
|
||||
|
||||
use std::{path::PathBuf, sync::LazyLock};
|
||||
|
||||
use egui::ScrollArea;
|
||||
|
||||
use crate::{
|
||||
editors::{
|
||||
Editor, asset_editor::Asset, content_editor, note_editor, object_editor::ObjectEditor,
|
||||
settings_editor::ProjectSettings, tags::Tag, template_editor::TemplateEditor,
|
||||
},
|
||||
explorer::Explorer,
|
||||
model::{WorldcoderCtx, object::Object, template::Template},
|
||||
};
|
||||
|
||||
pub(crate) static VERSION: &str = "0.1.0";
|
||||
pub(crate) static PROJECT_FOLDER: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||
let mut path = std::env::current_dir().unwrap();
|
||||
path.push("project");
|
||||
path
|
||||
});
|
||||
|
||||
pub struct Interface {
|
||||
right_panel_content: RightPanelContent,
|
||||
editor: content_editor::MainEditor,
|
||||
explorer: Explorer,
|
||||
project: ProjectSettings,
|
||||
|
||||
ctx: WorldcoderCtx,
|
||||
}
|
||||
|
||||
impl eframe::App for Interface {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
egui_extras::install_image_loaders(ui);
|
||||
self.configure_appearance(ui);
|
||||
self.render_top_panel(ui);
|
||||
self.render_left_panel(ui);
|
||||
self.render_right_panel(ui);
|
||||
self.render_main_content(ui);
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RightPanelContent {
|
||||
Template(Box<TemplateEditor>),
|
||||
Object(Box<ObjectEditor>),
|
||||
Note(Box<note_editor::Note>),
|
||||
Tag(Tag),
|
||||
Asset(Box<Asset>),
|
||||
None,
|
||||
}
|
||||
|
||||
impl RightPanelContent {
|
||||
fn template(template: Template) -> Self {
|
||||
Self::Template(Box::new(TemplateEditor::new(template)))
|
||||
}
|
||||
|
||||
fn object(instance: Object, template: Template) -> Self {
|
||||
Self::Object(Box::new(ObjectEditor::new(instance, template)))
|
||||
}
|
||||
|
||||
fn note(note: Option<note_editor::Note>) -> Self {
|
||||
Self::Note(Box::new(note.unwrap_or_default()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Interface {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
right_panel_content: RightPanelContent::None,
|
||||
editor: content_editor::MainEditor::new(),
|
||||
explorer: Explorer::new(),
|
||||
project: ProjectSettings::load(),
|
||||
|
||||
ctx: WorldcoderCtx::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_top_panel(&mut self, ui: &mut egui::Ui) {
|
||||
// Top bar with actions
|
||||
egui::Panel::top("top").show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
// title widget
|
||||
ui.heading("WorldCoder");
|
||||
ui.separator();
|
||||
|
||||
// version
|
||||
ui.label(VERSION);
|
||||
|
||||
// Settings
|
||||
if ui.button("Settings").clicked() {
|
||||
self.project.open();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if self.project.open {
|
||||
let mut open = self.project.open;
|
||||
egui::Window::new("Settings")
|
||||
.open(&mut open)
|
||||
.show(ui, |ui| {
|
||||
self.project.ui(ui);
|
||||
});
|
||||
self.project.open = open;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_left_panel(&mut self, ui: &mut egui::Ui) {
|
||||
// Left panel - File browser
|
||||
egui::Panel::left("file_browser")
|
||||
.resizable(true)
|
||||
.show(ui, |ui| {
|
||||
ui.heading("Explorer");
|
||||
|
||||
let mut to_load: Option<RightPanelContent> = None;
|
||||
let mut load_doc: Option<content_editor::MainEditor> = None;
|
||||
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
self.explorer.ui(&mut to_load, &mut load_doc, ui);
|
||||
});
|
||||
|
||||
if let Some(to_load) = to_load {
|
||||
self.right_panel_content = to_load;
|
||||
}
|
||||
|
||||
if let Some(load_doc) = load_doc {
|
||||
self.editor = load_doc;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn render_right_panel(&mut self, ui: &mut egui::Ui) {
|
||||
// Main content area
|
||||
egui::Panel::right("templates").show(ui, |ui| {
|
||||
let mut new_instance: Option<RightPanelContent> = None;
|
||||
|
||||
match &mut self.right_panel_content {
|
||||
// an instance of a template
|
||||
RightPanelContent::Object(object) => {
|
||||
// load template from path
|
||||
|
||||
let mut right_panel = None;
|
||||
|
||||
todo!();
|
||||
// let template = Template::load(&object.template_id).unwrap();
|
||||
// ScrollArea::vertical().show(ui, |ui| {
|
||||
// object.ui(
|
||||
// ui,
|
||||
// &template,
|
||||
// &mut right_panel,
|
||||
// &mut self.explorer.objects(),
|
||||
// );
|
||||
// });
|
||||
|
||||
if let Some(right_panel) = right_panel {
|
||||
self.right_panel_content = right_panel;
|
||||
}
|
||||
}
|
||||
|
||||
// an editable template
|
||||
RightPanelContent::Template(editor) => {
|
||||
editor.ui(ui, &mut self.ctx);
|
||||
}
|
||||
|
||||
RightPanelContent::Note(note) => note.ui(ui),
|
||||
|
||||
RightPanelContent::Tag(tag) => tag.ui(ui),
|
||||
|
||||
RightPanelContent::Asset(asset) => asset.ui(ui),
|
||||
|
||||
RightPanelContent::None => {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label("No template loaded to edit.");
|
||||
if ui.button("Back").clicked() {
|
||||
self.right_panel_content = RightPanelContent::None;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(new) = new_instance {
|
||||
self.right_panel_content = new;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// render main content area
|
||||
fn render_main_content(&mut self, ui: &mut egui::Ui) {
|
||||
self.editor.ui(ui, &mut self.project);
|
||||
}
|
||||
|
||||
// configure appearance of UI elements
|
||||
fn configure_appearance(&self, ui: &mut egui::Ui) {
|
||||
// configure appearance of UI elements
|
||||
let mut visuals = egui::Visuals::dark();
|
||||
visuals.window_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||
visuals.panel_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||
visuals.widgets.inactive.fg_stroke =
|
||||
egui::Stroke::from((1.0, egui::Color32::from_rgb(255, 255, 255)));
|
||||
visuals.widgets.inactive.bg_stroke =
|
||||
egui::Stroke::from((1.0, egui::Color32::from_rgb(60, 60, 60)));
|
||||
visuals.widgets.inactive.corner_radius = egui::CornerRadius::from(4);
|
||||
visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||
visuals.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||
visuals.widgets.inactive.expansion = 1.0;
|
||||
|
||||
ui.set_visuals(visuals);
|
||||
|
||||
// setup fonts.
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
fonts.font_data.insert(
|
||||
"JetBrains Mono Nerd Font".to_string(),
|
||||
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
|
||||
"../font/JetBrainsMonoNerdFontMono_Regular.ttf",
|
||||
))),
|
||||
);
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Proportional)
|
||||
.or_default()
|
||||
.insert(0, "JetBrains Mono Nerd Font".to_string());
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Monospace)
|
||||
.or_default()
|
||||
.insert(0, "JetBrains Mono Nerd Font".to_string());
|
||||
|
||||
ui.set_fonts(fonts);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Interface {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Interface {
|
||||
fn drop(&mut self) {
|
||||
self.project.save();
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ impl ContentAI {
|
||||
let is_open = self.open;
|
||||
|
||||
if is_open {
|
||||
egui::SidePanel::right("ai_assistant").show_inside(ui, |ui| {
|
||||
egui::Panel::right("ai_assistant").show(ui, |ui| {
|
||||
Self::ui_main(self, ui, project);
|
||||
});
|
||||
}
|
||||
@@ -149,9 +149,9 @@ impl ContentAI {
|
||||
ui.spacing();
|
||||
|
||||
ui.vertical(|ui| {
|
||||
egui::TopBottomPanel::top("reasoning_output")
|
||||
egui::Panel::top("reasoning_output")
|
||||
.resizable(true)
|
||||
.show_inside(ui, |ui| {
|
||||
.show(ui, |ui| {
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink([false, true])
|
||||
.id_salt("reasoning_output")
|
||||
@@ -163,7 +163,6 @@ impl ContentAI {
|
||||
.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..."),
|
||||
@@ -181,7 +180,6 @@ impl ContentAI {
|
||||
.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..."),
|
||||
@@ -251,9 +249,9 @@ impl ContentAI {
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::top("continue_instruction")
|
||||
egui::Panel::top("continue_instruction")
|
||||
.resizable(true)
|
||||
.show_inside(ui, |ui| {
|
||||
.show(ui, |ui| {
|
||||
egui::CollapsingHeader::new("Instructions")
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
@@ -263,7 +261,6 @@ impl ContentAI {
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut self.instruction)
|
||||
.frame(false)
|
||||
.desired_width(ui.available_width())
|
||||
.hint_text("Writing Instructions"),
|
||||
);
|
||||
@@ -271,9 +268,9 @@ impl ContentAI {
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::top("continue_context")
|
||||
egui::Panel::top("continue_context")
|
||||
.resizable(true)
|
||||
.show_inside(ui, |ui| {
|
||||
.show(ui, |ui| {
|
||||
egui::CollapsingHeader::new("Context")
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
@@ -283,7 +280,6 @@ impl ContentAI {
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut self.context_override)
|
||||
.frame(false)
|
||||
.desired_width(ui.available_width())
|
||||
.hint_text("Any additional context?"),
|
||||
);
|
||||
@@ -291,9 +287,9 @@ impl ContentAI {
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::top("continue_system_prompt")
|
||||
egui::Panel::top("continue_system_prompt")
|
||||
.resizable(true)
|
||||
.show_inside(ui, |ui| {
|
||||
.show(ui, |ui| {
|
||||
egui::CollapsingHeader::new("System prompt")
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
@@ -303,7 +299,6 @@ impl ContentAI {
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut self.system_prompt)
|
||||
.frame(false)
|
||||
.desired_width(ui.available_width())
|
||||
.hint_text("System prompt"),
|
||||
);
|
||||
|
||||
+1
-250
@@ -1,33 +1,4 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use std::{path::PathBuf, sync::LazyLock};
|
||||
|
||||
use egui::ScrollArea;
|
||||
|
||||
mod editors;
|
||||
mod explorer;
|
||||
|
||||
#[cfg(feature = "llm")]
|
||||
mod llm_integration;
|
||||
|
||||
mod util;
|
||||
|
||||
mod filesystem;
|
||||
|
||||
use crate::{
|
||||
editors::{
|
||||
asset_editor::Asset, content_editor, note_editor, object_editor::ObjectInstance,
|
||||
settings_editor::ProjectSettings, tags::Tag, template_editor::Template,
|
||||
},
|
||||
explorer::Explorer,
|
||||
};
|
||||
|
||||
static VERSION: &str = "0.1.0";
|
||||
static PROJECT_FOLDER: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||
let mut path = std::env::current_dir().unwrap();
|
||||
path.push("project");
|
||||
path
|
||||
});
|
||||
use worldcoder::Interface;
|
||||
|
||||
fn main() {
|
||||
let app = Interface::new();
|
||||
@@ -42,223 +13,3 @@ fn main() {
|
||||
|
||||
eprintln!("App closed");
|
||||
}
|
||||
|
||||
pub struct Interface {
|
||||
right_panel_content: RightPanelContent,
|
||||
editor: content_editor::MainEditor,
|
||||
explorer: Explorer,
|
||||
project: ProjectSettings,
|
||||
}
|
||||
|
||||
impl eframe::App for Interface {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui_extras::install_image_loaders(ctx);
|
||||
|
||||
self.configure_appearance(ctx);
|
||||
self.render_top_panel(ctx);
|
||||
self.render_left_panel(ctx);
|
||||
self.render_right_panel(ctx);
|
||||
self.render_main_content(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RightPanelContent {
|
||||
Template(Box<Template>),
|
||||
Object(Box<ObjectInstance>),
|
||||
Note(Box<note_editor::Note>),
|
||||
Tag(Tag),
|
||||
Asset(Box<Asset>),
|
||||
None,
|
||||
}
|
||||
|
||||
impl RightPanelContent {
|
||||
fn template(template: Option<Template>) -> Self {
|
||||
Self::Template(Box::new(template.unwrap_or_default()))
|
||||
}
|
||||
|
||||
fn object(instance: Option<ObjectInstance>) -> Self {
|
||||
Self::Object(Box::new(instance.unwrap_or_default()))
|
||||
}
|
||||
|
||||
fn note(note: Option<note_editor::Note>) -> Self {
|
||||
Self::Note(Box::new(note.unwrap_or_default()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Interface {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
right_panel_content: RightPanelContent::None,
|
||||
editor: content_editor::MainEditor::new(),
|
||||
explorer: Explorer::new(),
|
||||
project: ProjectSettings::load(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_top_panel(&mut self, ctx: &egui::Context) {
|
||||
// Top bar with actions
|
||||
egui::TopBottomPanel::top("top").show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
// title widget
|
||||
ui.heading("WorldCoder");
|
||||
ui.separator();
|
||||
|
||||
// version
|
||||
ui.label(VERSION);
|
||||
|
||||
// Settings
|
||||
if ui.button("Settings").clicked() {
|
||||
self.project.open();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if self.project.open {
|
||||
let mut open = self.project.open;
|
||||
egui::Window::new("Settings")
|
||||
.open(&mut open)
|
||||
.show(ctx, |ui| {
|
||||
self.project.ui(ui);
|
||||
});
|
||||
self.project.open = open;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_left_panel(&mut self, ctx: &egui::Context) {
|
||||
// Left panel - File browser
|
||||
egui::SidePanel::left("file_browser")
|
||||
.resizable(true)
|
||||
.default_width(250.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.heading("Explorer");
|
||||
|
||||
let mut to_load: Option<RightPanelContent> = None;
|
||||
let mut load_doc: Option<content_editor::MainEditor> = None;
|
||||
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
self.explorer.ui(&mut to_load, &mut load_doc, ui);
|
||||
});
|
||||
|
||||
if let Some(to_load) = to_load {
|
||||
self.right_panel_content = to_load;
|
||||
}
|
||||
|
||||
if let Some(load_doc) = load_doc {
|
||||
self.editor = load_doc;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn render_right_panel(&mut self, ctx: &egui::Context) {
|
||||
// Main content area
|
||||
egui::SidePanel::right("templates").show(ctx, |ui| {
|
||||
let mut new_instance: Option<RightPanelContent> = None;
|
||||
|
||||
match &mut self.right_panel_content {
|
||||
// an instance of a template
|
||||
RightPanelContent::Object(object) => {
|
||||
// load template from path
|
||||
|
||||
let mut right_panel = None;
|
||||
|
||||
let template = Template::load(&object.template_id).unwrap();
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
object.ui(
|
||||
ui,
|
||||
&template,
|
||||
&mut right_panel,
|
||||
&mut self.explorer.objects(),
|
||||
);
|
||||
});
|
||||
|
||||
if let Some(right_panel) = right_panel {
|
||||
self.right_panel_content = right_panel;
|
||||
}
|
||||
}
|
||||
|
||||
// an editable template
|
||||
RightPanelContent::Template(template) => {
|
||||
template.ui(ui, &mut new_instance);
|
||||
}
|
||||
|
||||
RightPanelContent::Note(note) => note.ui(ui),
|
||||
|
||||
RightPanelContent::Tag(tag) => tag.ui(ui),
|
||||
|
||||
RightPanelContent::Asset(asset) => asset.ui(ui),
|
||||
|
||||
RightPanelContent::None => {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label("No template loaded to edit.");
|
||||
if ui.button("Back").clicked() {
|
||||
self.right_panel_content = RightPanelContent::None;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(new) = new_instance {
|
||||
self.right_panel_content = new;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// render main content area
|
||||
fn render_main_content(&mut self, ctx: &egui::Context) {
|
||||
self.editor.ui(ctx, &mut self.project);
|
||||
}
|
||||
|
||||
// configure appearance of UI elements
|
||||
fn configure_appearance(&self, ctx: &egui::Context) {
|
||||
// configure appearance of UI elements
|
||||
let mut visuals = egui::Visuals::dark();
|
||||
visuals.window_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||
visuals.panel_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||
visuals.widgets.inactive.fg_stroke =
|
||||
egui::Stroke::from((1.0, egui::Color32::from_rgb(255, 255, 255)));
|
||||
visuals.widgets.inactive.bg_stroke =
|
||||
egui::Stroke::from((1.0, egui::Color32::from_rgb(60, 60, 60)));
|
||||
visuals.widgets.inactive.corner_radius = egui::CornerRadius::from(4);
|
||||
visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||
visuals.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||
visuals.widgets.inactive.expansion = 1.0;
|
||||
|
||||
ctx.set_visuals(visuals);
|
||||
|
||||
// setup fonts.
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
fonts.font_data.insert(
|
||||
"JetBrains Mono Nerd Font".to_string(),
|
||||
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
|
||||
"../font/JetBrainsMonoNerdFontMono_Regular.ttf",
|
||||
))),
|
||||
);
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Proportional)
|
||||
.or_default()
|
||||
.insert(0, "JetBrains Mono Nerd Font".to_string());
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Monospace)
|
||||
.or_default()
|
||||
.insert(0, "JetBrains Mono Nerd Font".to_string());
|
||||
|
||||
ctx.set_fonts(fonts);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Interface {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Interface {
|
||||
fn drop(&mut self) {
|
||||
self.project.save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
use zxq5_id_lib::Identifiable;
|
||||
|
||||
use crate::{
|
||||
PROJECT_FOLDER,
|
||||
services::persist::{FileSystem, FsError, native::Fs},
|
||||
};
|
||||
|
||||
pub mod object;
|
||||
pub mod template;
|
||||
|
||||
pub struct WorldcoderCtx {
|
||||
fs: Box<dyn FileSystem>,
|
||||
}
|
||||
|
||||
impl WorldcoderCtx {
|
||||
pub fn new(fs: Box<dyn FileSystem>) -> Self {
|
||||
Self { fs }
|
||||
}
|
||||
|
||||
pub fn fs(&mut self) -> &mut dyn FileSystem {
|
||||
self.fs.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WorldcoderCtx {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fs: Box::new(Fs::new(PROJECT_FOLDER.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ModelElement: Identifiable + DeserializeOwned + Serialize {
|
||||
/// File extension to use for actually loading the files.
|
||||
/// This will probably be ron in all cases, but I guess change it if the file type changes
|
||||
/// means we can support JSON fairly easily as well.
|
||||
const EXTENSION: &'static str = "ron";
|
||||
|
||||
/// Sub-directory to store elements of the type in, eg setting a value of "templates" means all elements
|
||||
/// of this type will be stored as <project_name>/templates/<template_name>.<extension>
|
||||
const PROJECT_SUBDIR: &'static str;
|
||||
|
||||
/// Creates a copy of the given file. implementors of this trait need to implement this method manually.
|
||||
/// All implementations should create a copy of the file with:
|
||||
/// - A new UUID
|
||||
/// - A name along the lines of "<old_name> (Copy)"
|
||||
/// - All the other fields being identical.
|
||||
fn copy(&self) -> Self;
|
||||
|
||||
/// gets the path of the element with the given UUID
|
||||
/// override this in order to set a custom path. the default is "typeid()/uuid()"
|
||||
fn path_from(uuid: Uuid) -> PathBuf {
|
||||
PathBuf::from(Self::PROJECT_SUBDIR)
|
||||
.join(uuid.to_string())
|
||||
.with_extension(Self::EXTENSION)
|
||||
}
|
||||
|
||||
/// get the path of the current element
|
||||
fn path(&self) -> PathBuf {
|
||||
Self::path_from(self.id())
|
||||
}
|
||||
|
||||
/// loads the file with the given UUID from disk.
|
||||
fn load(uuid: Uuid, ctx: &mut WorldcoderCtx) -> Result<Self, FsError> {
|
||||
let bytes = ctx.fs().read(&Self::path_from(uuid))?;
|
||||
ron::from_str(&String::from_utf8_lossy(&bytes)).map_err(FsError::Spanned)
|
||||
}
|
||||
|
||||
/// Reloads the file, replacing itself with it's on-disk data in-place.
|
||||
fn reload(&mut self, ctx: &mut WorldcoderCtx) -> Result<(), FsError> {
|
||||
let bytes = ctx.fs().read(&Self::path_from(self.id()))?;
|
||||
*self = ron::from_str(&String::from_utf8_lossy(&bytes)).map_err(FsError::Spanned)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Saves the current file to disk.
|
||||
fn save(&self, ctx: &mut WorldcoderCtx) -> Result<(), FsError> {
|
||||
let content = ron::to_string(self).map_err(FsError::Ron)?;
|
||||
ctx.fs()
|
||||
.write(&Self::path_from(self.id()), content.as_bytes())
|
||||
}
|
||||
|
||||
/// deletes the current element on disk.
|
||||
fn delete(&self, ctx: &mut WorldcoderCtx) -> Result<(), FsError> {
|
||||
ctx.fs().delete(&Self::path_from(self.id()))
|
||||
}
|
||||
|
||||
/// deletes the element with the given id on disk.
|
||||
fn delete_by_id(uuid: Uuid, ctx: &mut WorldcoderCtx) -> Result<(), FsError> {
|
||||
ctx.fs().delete(&Self::path_from(uuid))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use zxq5_id_lib::Identifiable;
|
||||
|
||||
use crate::{
|
||||
model::{
|
||||
ModelElement, WorldcoderCtx,
|
||||
template::{FieldValue, Template},
|
||||
},
|
||||
services::persist::FsError,
|
||||
};
|
||||
|
||||
#[derive(Identifiable, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Object {
|
||||
#[uuid]
|
||||
uuid: Uuid,
|
||||
pub template: Uuid,
|
||||
|
||||
pub tags: Vec<Uuid>,
|
||||
|
||||
pub name: String,
|
||||
pub fields: HashMap<String, FieldValue>,
|
||||
}
|
||||
|
||||
impl Object {
|
||||
pub fn new(template: &Template) -> Self {
|
||||
let fields = template
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f| (f.name.clone(), FieldValue::new_from(&f.field_type)))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
uuid: Uuid::new_v4(),
|
||||
template: template.id(),
|
||||
name: format!("New {}", template.name),
|
||||
fields,
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn template(&self, ctx: &mut WorldcoderCtx) -> Result<Template, FsError> {
|
||||
Template::load(self.template, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelElement for Object {
|
||||
const PROJECT_SUBDIR: &'static str = "objects";
|
||||
|
||||
fn copy(&self) -> Self {
|
||||
Self {
|
||||
uuid: Uuid::new_v4(),
|
||||
template: self.template,
|
||||
name: format!("{} (Copy)", self.name),
|
||||
tags: self.tags.clone(),
|
||||
fields: self.fields.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
use jiff::civil::Date;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use zxq5_id_lib::Identifiable;
|
||||
|
||||
use crate::model::ModelElement;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum FieldType {
|
||||
Image,
|
||||
SingleLine,
|
||||
MultiLine,
|
||||
Date,
|
||||
Number,
|
||||
Link(Uuid),
|
||||
Links(Uuid),
|
||||
}
|
||||
|
||||
impl Default for FieldType {
|
||||
fn default() -> Self {
|
||||
Self::SingleLine
|
||||
}
|
||||
}
|
||||
|
||||
impl FieldType {
|
||||
pub fn types() -> Vec<FieldType> {
|
||||
vec![
|
||||
FieldType::Image,
|
||||
FieldType::SingleLine,
|
||||
FieldType::MultiLine,
|
||||
FieldType::Date,
|
||||
FieldType::Number,
|
||||
// FieldType::Link { template_id: None },
|
||||
// FieldType::Links { template_id: None },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Identifiable, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Template {
|
||||
#[uuid]
|
||||
uuid: Uuid,
|
||||
pub name: String,
|
||||
|
||||
pub description: String,
|
||||
pub fields: Vec<FieldDefinition>,
|
||||
}
|
||||
|
||||
impl ModelElement for Template {
|
||||
const PROJECT_SUBDIR: &'static str = "templates";
|
||||
|
||||
fn copy(&self) -> Self {
|
||||
Self {
|
||||
uuid: Uuid::new_v4(),
|
||||
name: format!("{} (Copy)", self.name),
|
||||
description: self.description.clone(),
|
||||
fields: self.fields.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Template {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
uuid: Uuid::new_v4(),
|
||||
name: String::from("New template"),
|
||||
|
||||
description: String::new(),
|
||||
fields: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FieldDefinition {
|
||||
pub name: String,
|
||||
pub field_type: FieldType,
|
||||
pub required: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub on_preview: bool,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl Default for FieldDefinition {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
field_type: FieldType::SingleLine,
|
||||
required: true,
|
||||
on_preview: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum FieldValue {
|
||||
Image(String),
|
||||
SingleLine(String),
|
||||
MultiLine(String),
|
||||
Date(Date),
|
||||
Number(f64),
|
||||
Link(Option<Uuid>),
|
||||
Links(Vec<Uuid>),
|
||||
}
|
||||
|
||||
impl FieldValue {
|
||||
pub fn new_from(_type: &FieldType) -> Self {
|
||||
match _type {
|
||||
FieldType::Image => Self::Image(String::new()),
|
||||
FieldType::SingleLine => Self::SingleLine(String::new()),
|
||||
FieldType::MultiLine => Self::MultiLine(String::new()),
|
||||
FieldType::Date => Self::Date(Date::new(1970, 1, 1).unwrap()),
|
||||
FieldType::Number => Self::Number(0.0),
|
||||
FieldType::Link(_) => Self::Link(None), // TODO!
|
||||
FieldType::Links(_) => Self::Links(Vec::new()), // TODO!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FieldValue {
|
||||
fn default() -> Self {
|
||||
Self::SingleLine(String::new())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
mod model;
|
||||
|
||||
use model::*;
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
sync::mpsc::{self, Receiver, Sender},
|
||||
};
|
||||
|
||||
pub use model::{LLMConfig, ReasoningEffort, ResponseToken};
|
||||
|
||||
pub struct LLMHandle {
|
||||
cursor_pos: usize,
|
||||
request: Option<LLMRequest>,
|
||||
config: LLMConfig,
|
||||
reasoning_output: String,
|
||||
}
|
||||
|
||||
impl LLMHandle {
|
||||
pub fn new(config: LLMConfig) -> Self {
|
||||
Self {
|
||||
cursor_pos: 0,
|
||||
request: None,
|
||||
config,
|
||||
reasoning_output: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request(&mut self, prompt: String, cursor_pos: usize) {
|
||||
self.request = LLMRequest::new(prompt, &self.config);
|
||||
self.cursor_pos = cursor_pos;
|
||||
}
|
||||
|
||||
pub fn get_latest(&mut self, content: &mut String) -> bool {
|
||||
let mut changed = false;
|
||||
if let Some(request) = &mut self.request {
|
||||
for token in request.stream_content() {
|
||||
changed = true;
|
||||
match token {
|
||||
ResponseToken::Content(c) => {
|
||||
content.insert_str(self.cursor_pos, &c);
|
||||
self.cursor_pos += c.len();
|
||||
}
|
||||
ResponseToken::Reasoning(r) => self.reasoning_output.push_str(&r),
|
||||
ResponseToken::End => {
|
||||
self.request = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LLMRequest {
|
||||
prompt: String,
|
||||
rx: Receiver<ResponseToken>,
|
||||
}
|
||||
|
||||
impl LLMRequest {
|
||||
pub fn new(prompt: String, config: &LLMConfig) -> Option<Self> {
|
||||
let (tx, rx) = mpsc::channel::<ResponseToken>();
|
||||
|
||||
let prompt_clone = prompt.clone();
|
||||
let config_clone = config.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
println!("running req");
|
||||
Self::run_request(prompt_clone, config_clone, tx);
|
||||
});
|
||||
|
||||
return Some(Self { prompt, rx });
|
||||
}
|
||||
|
||||
pub fn stream_content(&mut self) -> Vec<ResponseToken> {
|
||||
let mut response = Vec::new();
|
||||
while let Ok(token) = self.rx.try_recv() {
|
||||
response.push(token);
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
/// internal associated function - runs in another thread, streaming in the chat responses
|
||||
fn run_request(prompt: String, config: LLMConfig, sender: Sender<ResponseToken>) {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
let messages = vec![Message {
|
||||
role: "user".to_string(),
|
||||
content: prompt.clone(),
|
||||
}];
|
||||
|
||||
let request = ChatRequest::new(messages, &config);
|
||||
|
||||
const API_URL: &'static str = "http://localhost:1234";
|
||||
const API_KEY: &'static str = "sk-lm-wkUllcKn:m9FmJNjaraHKJlw6DMmg";
|
||||
|
||||
println!("sending request");
|
||||
|
||||
let result = client
|
||||
.post(API_URL.to_string() + "/v1/chat/completions")
|
||||
.bearer_auth(API_KEY)
|
||||
.json(&request)
|
||||
.send();
|
||||
|
||||
println!("result: {result:?}");
|
||||
|
||||
let Ok(response) = result else {
|
||||
println!("failed!");
|
||||
return;
|
||||
};
|
||||
|
||||
println!("response: {response:?}");
|
||||
|
||||
let reader = BufReader::new(response);
|
||||
for line in reader.lines() {
|
||||
let Ok(line) = line else {
|
||||
eprintln!("failed to read line: {line:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
if line == "data: [DONE]" {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(json_str) = line.strip_prefix("data: ") {
|
||||
if let Ok(chunk) = serde_json::from_str::<StreamingChatResponse>(json_str) {
|
||||
println!("chunk: {chunk:?}");
|
||||
|
||||
if let Some(content) = chunk.choices[0].delta.content.clone() {
|
||||
println!("content: {content}");
|
||||
sender.send(ResponseToken::Content(content));
|
||||
}
|
||||
|
||||
if let Some(reasoning_content) =
|
||||
chunk.choices[0].delta.reasoning_content.clone()
|
||||
{
|
||||
println!("reasoning_content: {reasoning_content}");
|
||||
sender.send(ResponseToken::Reasoning(reasoning_content));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Copy, Clone, Debug, PartialEq, Default)]
|
||||
pub enum ReasoningEffort {
|
||||
#[serde(rename = "minimal")]
|
||||
Minimal,
|
||||
|
||||
#[default]
|
||||
#[serde(rename = "low")]
|
||||
Low,
|
||||
#[serde(rename = "medium")]
|
||||
Medium,
|
||||
#[serde(rename = "high")]
|
||||
High,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ReasoningEffort {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ReasoningEffort::Minimal => write!(f, "Minimal"),
|
||||
ReasoningEffort::Low => write!(f, "Low"),
|
||||
ReasoningEffort::Medium => write!(f, "Medium"),
|
||||
ReasoningEffort::High => write!(f, "High"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ResponseToken {
|
||||
Content(String),
|
||||
Reasoning(String),
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LLMConfig {
|
||||
pub reasoning_effort: ReasoningEffort,
|
||||
pub model: String,
|
||||
pub max_tokens: usize,
|
||||
pub temperature: f32,
|
||||
}
|
||||
|
||||
impl Default for LLMConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
reasoning_effort: ReasoningEffort::Medium,
|
||||
model: "google/gemma-4-12b-qat".to_string(),
|
||||
max_tokens: 4096,
|
||||
temperature: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Message {
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ChatRequest {
|
||||
messages: Vec<Message>,
|
||||
temperature: f32,
|
||||
max_tokens: usize,
|
||||
stream: bool,
|
||||
reasoning_effort: ReasoningEffort,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
impl ChatRequest {
|
||||
pub fn new(messages: Vec<Message>, config: &LLMConfig) -> Self {
|
||||
Self {
|
||||
reasoning_effort: config.reasoning_effort,
|
||||
temperature: config.temperature,
|
||||
messages,
|
||||
max_tokens: config.max_tokens,
|
||||
stream: true,
|
||||
model: Some(config.model.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Streaming response structures
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct StreamingChatResponse {
|
||||
pub choices: Vec<StreamingChoice>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct StreamingChoice {
|
||||
pub delta: Delta,
|
||||
#[serde(default)]
|
||||
#[allow(unused)]
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Delta {
|
||||
#[serde(default)]
|
||||
#[allow(unused)]
|
||||
pub role: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub content: Option<String>,
|
||||
#[serde(default)]
|
||||
pub reasoning_content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ChatResponse {
|
||||
#[allow(unused)]
|
||||
pub choices: Vec<Choice>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Choice {
|
||||
#[allow(unused)]
|
||||
pub message: Message,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#[cfg(feature = "llm")]
|
||||
pub mod llm;
|
||||
pub mod persist;
|
||||
@@ -2,18 +2,22 @@ use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
sync::LazyLock,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
use crate::PROJECT_FOLDER;
|
||||
use crate::filesystem::native::NativeFileSystem;
|
||||
use crate::model::ModelElement;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
pub mod native;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
use native::NativeFileSystem;
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
pub mod web;
|
||||
|
||||
@@ -24,6 +28,12 @@ pub static FILESYSTEM: LazyLock<NativeFileSystem> = LazyLock::new(|| {
|
||||
return Box::new(web::WebFileSystem::new());
|
||||
});
|
||||
|
||||
pub trait FileSystem: Send + Sync {
|
||||
fn read(&self, path: &Path) -> Result<Vec<u8>, FsError>;
|
||||
fn write(&self, path: &Path, data: &[u8]) -> Result<(), FsError>;
|
||||
fn delete(&self, path: &Path) -> Result<(), FsError>;
|
||||
}
|
||||
|
||||
pub trait LegacyFileSystem {
|
||||
fn read<T: DeserializeOwned>(&self, path: &Path) -> Result<T, FsError>;
|
||||
fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, FsError>;
|
||||
@@ -45,6 +55,8 @@ pub trait LegacyFileSystem {
|
||||
pub enum FsError {
|
||||
Io(io::Error),
|
||||
Serde(serde_json::Error),
|
||||
Ron(ron::error::Error),
|
||||
Spanned(ron::error::SpannedError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FsError {
|
||||
@@ -52,6 +64,8 @@ impl std::fmt::Display for FsError {
|
||||
match self {
|
||||
FsError::Io(e) => write!(f, "IO error: {e}"),
|
||||
FsError::Serde(e) => write!(f, "Serialization error: {e}"),
|
||||
FsError::Ron(e) => write!(f, "Serialization error: {e}"),
|
||||
FsError::Spanned(e) => write!(f, "Serialization error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +75,8 @@ impl std::error::Error for FsError {
|
||||
match self {
|
||||
FsError::Io(e) => Some(e),
|
||||
FsError::Serde(e) => Some(e),
|
||||
FsError::Ron(e) => Some(e),
|
||||
FsError::Spanned(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,28 +92,3 @@ impl From<serde_json::Error> for FsError {
|
||||
FsError::Serde(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Id(String);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub trait FileSystem {
|
||||
fn load<T: DeserializeOwned>(&self, id: Id) -> Result<T, FsError>;
|
||||
fn save<T: Serialize>(&self, id: Id, data: T) -> Result<(), FsError>;
|
||||
fn mkdir(&self, path: Path) -> Result<(), FsError>;
|
||||
fn exists(&self, path: Path) -> bool;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Index {
|
||||
file_cache: HashMap<Id, PathBuf>,
|
||||
project_root: Directory,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Directory {
|
||||
name: String,
|
||||
id: Id,
|
||||
children: HashMap<Id, Directory>,
|
||||
files: Vec<Id>,
|
||||
}
|
||||
@@ -1,11 +1,54 @@
|
||||
use std::fs;
|
||||
use std::io::{ErrorKind, Read};
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::filesystem::{FsError, LegacyFileSystem};
|
||||
use super::{FileSystem, FsError, LegacyFileSystem};
|
||||
|
||||
pub struct Fs {
|
||||
project_root: PathBuf,
|
||||
}
|
||||
|
||||
impl Fs {
|
||||
/// Resolve the user supplied *relative* path against the project root.
|
||||
#[inline]
|
||||
fn full_path(&self, path: &Path) -> PathBuf {
|
||||
self.project_root.join(path)
|
||||
}
|
||||
|
||||
pub fn new(root: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
project_root: root.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystem for Fs {
|
||||
fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
|
||||
fs::read(self.full_path(path)).map_err(FsError::Io)
|
||||
}
|
||||
|
||||
fn write(&self, path: &Path, data: &[u8]) -> Result<(), FsError> {
|
||||
fs::File::create(self.full_path(path))?
|
||||
.write(data)
|
||||
.map_err(FsError::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete(&self, path: &Path) -> Result<(), FsError> {
|
||||
let full_path = self.full_path(path);
|
||||
match fs::remove_file(&full_path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) if e.kind() == ErrorKind::IsADirectory => {
|
||||
// Remove a directory tree.
|
||||
fs::remove_dir_all(&full_path).map_err(FsError::Io)
|
||||
}
|
||||
Err(e) => Err(FsError::Io(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NativeFileSystem {
|
||||
project_root: PathBuf,
|
||||
@@ -29,8 +72,8 @@ impl NativeFileSystem {
|
||||
impl LegacyFileSystem for NativeFileSystem {
|
||||
fn read<T: DeserializeOwned>(&self, path: &Path) -> Result<T, FsError> {
|
||||
let full_path = self.full_path(path);
|
||||
let file = fs::File::open(full_path).map_err(FsError::Io)?;
|
||||
serde_json::from_reader(file).map_err(FsError::Serde)
|
||||
let content = fs::read_to_string(full_path).map_err(FsError::Io)?;
|
||||
ron::from_str(&content).map_err(FsError::Spanned)
|
||||
}
|
||||
|
||||
fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, FsError> {
|
||||
@@ -48,8 +91,12 @@ impl LegacyFileSystem for NativeFileSystem {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let file = fs::File::create(full_path)?;
|
||||
serde_json::to_writer(file, &data).map_err(FsError::Serde)
|
||||
let content = ron::to_string(&data).map_err(FsError::Ron)?;
|
||||
fs::File::create(full_path)?
|
||||
.write(content.as_bytes())
|
||||
.map_err(FsError::Io)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete(&self, path: &Path) -> Result<(), FsError> {
|
||||
@@ -104,12 +151,12 @@ impl LegacyFileSystem for NativeFileSystem {
|
||||
|
||||
fn config_path(&self) -> PathBuf {
|
||||
match std::env::var("HOME") {
|
||||
Ok(path) => PathBuf::from(path + "/.config/worldcoder/settings.json"),
|
||||
Ok(path) => PathBuf::from(path + "/.config/worldcoder/settings.ron"),
|
||||
Err(_) => {
|
||||
eprintln!(
|
||||
"XDG_CONFIG_HOME not set, using default path of ~/.config/worldcoder/settings.json"
|
||||
"XDG_CONFIG_HOME not set, using default path of ~/.config/worldcoder/settings.ron"
|
||||
);
|
||||
"~/.config/worldcoder/settings.json".into()
|
||||
"~/.config/worldcoder/settings.ron".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -3,12 +3,23 @@ use egui::{
|
||||
scroll_area::{ScrollBarVisibility, ScrollSource},
|
||||
};
|
||||
|
||||
pub struct Error {
|
||||
use crate::services::persist::FsError;
|
||||
|
||||
pub struct WorldcoderError {
|
||||
message: String,
|
||||
visible: bool,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
impl WorldcoderError {
|
||||
pub fn from_fs(err: FsError) -> Self {
|
||||
match err {
|
||||
FsError::Io(e) => Self::new(e.to_string()),
|
||||
FsError::Serde(e) => Self::new(e.to_string()),
|
||||
FsError::Ron(e) => Self::new(e.to_string()),
|
||||
FsError::Spanned(e) => Self::new(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(message: String) -> Self {
|
||||
Self {
|
||||
message,
|
||||
|
||||
Reference in New Issue
Block a user