finally another commit

This commit is contained in:
2025-07-23 23:56:07 +01:00
parent 224300f3ea
commit ba468cafa7
30 changed files with 1591 additions and 318 deletions
+24 -10
View File
@@ -4,37 +4,51 @@ use crate::{PROJECT_FOLDER, util};
#[derive(Debug, Clone)]
pub struct Asset {
pub new_name: String,
pub name: String,
pub old_name: String,
pub saved: bool,
}
impl Asset {
pub fn open(name: String) -> Self {
Self {
old_name: name.clone(),
new_name: name.clone(),
name,
saved: false,
saved: true,
}
}
pub fn save(&mut self) {
let old_path = Self::path(&self.old_name);
let new_path = Self::path(&self.name);
let old_path = Self::path(&self.name);
let new_path = Self::path(&self.new_name);
println!("old_path: {old_path:?}");
println!("new_path: {new_path:?}");
// move from src dir to name path
std::fs::rename(&old_path, &new_path).unwrap();
if let Err(err) = std::fs::rename(&old_path, &new_path) {
match err.kind() {
std::io::ErrorKind::NotFound => {
let dir = new_path.parent().unwrap();
if !dir.exists() {
std::fs::create_dir_all(dir).unwrap();
}
std::fs::rename(&old_path, &new_path).unwrap();
}
_ => panic!("Failed to rename file: {err}"),
}
}
self.saved = true;
self.old_name = self.name.clone();
self.name = self.new_name.clone();
}
pub fn path(name: &str) -> std::path::PathBuf {
PROJECT_FOLDER.join("assets").join(format!("{name}.png"))
PROJECT_FOLDER.join("assets").join(name)
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.vertical(|ui| {
util::saved_status(ui, self.saved, &self.name, &self.name);
util::saved_status(ui, self.saved, &self.name, &self.new_name);
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl)
|| ui.button("Save").clicked()
@@ -46,7 +60,7 @@ impl Asset {
ui.horizontal(|ui| {
ui.strong("Filename:");
if TextEdit::singleline(&mut self.name)
if TextEdit::singleline(&mut self.new_name)
.desired_width(f32::INFINITY)
.frame(false)
.show(ui)
+34 -9
View File
@@ -1,8 +1,8 @@
use egui::TextEdit;
use egui::{TextEdit, text};
use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
use serde::{self, Deserialize, Serialize};
use crate::{PROJECT_FOLDER, editors::tags::Tag, util};
use crate::{PROJECT_FOLDER, editors::tags::Tag, llm_integration::content_llm::ai_enabled, util};
pub struct MainEditor {
pub content: ContentSection,
@@ -95,7 +95,7 @@ impl MainEditor {
Self {
content: ContentSection::new(),
show_editor: false, // Start with editor hidden
show_preview: true,
show_preview: false,
preview_cache: CommonMarkCache::default(),
}
}
@@ -104,7 +104,7 @@ impl MainEditor {
Self {
content,
show_editor: true,
show_preview: true,
show_preview: false,
preview_cache: CommonMarkCache::default(),
}
}
@@ -264,7 +264,7 @@ impl MainEditor {
}
fn editor_ui(&mut self, ui: &mut egui::Ui) {
egui::ScrollArea::both()
let response = egui::ScrollArea::both()
.auto_shrink([false, false])
.id_salt("editor_scroll")
.show(ui, |ui| {
@@ -290,14 +290,39 @@ impl MainEditor {
.hint_text("Type here...")
.desired_width(max_width as f32);
if ui
let mut ctx_menu = false;
let response = ui
.add_sized(
egui::vec2(max_width as f32 - 30.0, ui.available_height()),
text_edit,
)
.changed()
{
self.content.saved = false;
.on_hover_text("Right click to open context menu")
.context_menu(|ui| {
ctx_menu = true;
ui.menu_button("AI Actions", |ui| {
ui.add_enabled_ui(ai_enabled(), |ui| {
if ui.button("Summarise").clicked() {
println!("Summarise");
}
if ui.button("Continue").clicked() {
let content = self.content.content.clone();
let response =
crate::llm_integration::content_llm::continue_content(
&content, "", 1024,
)
.unwrap();
self.content.content.push_str(&response);
}
});
});
});
if let Some(response) = response {
if response.response.changed() || ctx_menu {
self.content.saved = false;
}
}
});
});
+78
View File
@@ -0,0 +1,78 @@
use std::io::Read;
use chrono::NaiveDate;
use egui_extras::DatePickerButton;
use serde::{Deserialize, Serialize};
use crate::PROJECT_FOLDER;
#[derive(Serialize, Deserialize)]
pub struct ProjectContext {
date: NaiveDate,
project_name: String,
project_author: String,
project_description: String,
}
impl ProjectContext {
pub fn new() -> Self {
Self::default()
}
pub fn load() -> Self {
let path = PROJECT_FOLDER.join("context.json");
if let Ok(mut file) = std::fs::File::open(path) {
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
if let Ok(proj) = serde_json::from_str(&contents) {
return proj;
}
}
Self::default()
}
pub fn save(&self) {
let path = PROJECT_FOLDER.join("context.json");
let content = serde_json::to_string_pretty(self).unwrap();
std::fs::write(path, content).unwrap();
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
// table
egui::Grid::new("context_editor")
.striped(true)
.num_columns(2)
.show(ui, |ui| {
ui.label("Project Name");
ui.text_edit_singleline(&mut self.project_name);
ui.end_row();
ui.label("Project Author");
ui.text_edit_singleline(&mut self.project_author);
ui.end_row();
ui.label("Project Description");
ui.text_edit_singleline(&mut self.project_description);
ui.end_row();
ui.label("Date");
ui.add(DatePickerButton::new(&mut self.date));
ui.end_row();
});
}
}
impl Default for ProjectContext {
fn default() -> Self {
Self {
date: chrono::Local::now().naive_local().into(),
project_name: "New Project".to_string(),
project_author: "Your Name".to_string(),
project_description: "Description of your project".to_string(),
}
}
}
+1
View File
@@ -1,5 +1,6 @@
pub mod asset_editor;
pub mod content_editor;
pub mod context_editor;
pub mod note_editor;
pub mod object_editor;
pub mod tags;
+45 -61
View File
@@ -1,23 +1,22 @@
use core::f32;
use std::path::Path;
use chrono::NaiveDate;
use egui::{CollapsingHeader, Response, RichText, Sense, TextEdit, Ui, UiBuilder, vec2};
use egui::{CollapsingHeader, RichText, Sense, TextEdit, Ui, UiBuilder, vec2};
use serde::{Deserialize, Serialize};
use crate::{
PROJECT_FOLDER, RightPanelContent,
editors::{
tags::Tag,
template_editor::{FieldDefinition, FieldType, FieldValue, Template},
template_editor::{FieldValue, Template},
},
util,
};
pub type ObjectId = String;
#[derive(Debug, Serialize, Deserialize)]
pub struct ObjectInstance {
// template info
pub id: String,
pub id: ObjectId,
pub template_id: String,
// instance info
@@ -67,7 +66,7 @@ impl ObjectInstance {
let mut fields = std::collections::HashMap::new();
for field in &template.fields {
fields.insert(field.name.clone(), FieldValue::default());
fields.insert(field.name.clone(), FieldValue::from_type(&field.field_type));
}
Self {
@@ -106,7 +105,7 @@ impl ObjectInstance {
ui: &mut Ui,
template: &Template,
right_panel: &mut Option<RightPanelContent>,
objects: &mut Vec<ObjectInstance>,
objects: &mut [ObjectInstance],
) {
let _ = right_panel;
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
@@ -193,13 +192,7 @@ impl ObjectInstance {
ui.separator();
Self::render_field(
field_def,
field_value,
ui,
&mut self.saved,
objects,
);
Self::render_field(field_value, ui, &mut self.saved, objects);
ui.separator();
});
@@ -210,27 +203,25 @@ impl ObjectInstance {
}
fn render_field(
field_def: &FieldDefinition,
field_value: &mut FieldValue,
ui: &mut egui::Ui,
saved: &mut bool,
objects: &mut Vec<ObjectInstance>,
objects: &mut [ObjectInstance],
) {
match field_def.field_type {
FieldType::SingleLine => {
if TextEdit::singleline(&mut field_value.value)
match field_value {
FieldValue::SingleLine(value) => {
if TextEdit::singleline(value)
.desired_width(f32::INFINITY)
.frame(false)
.show(ui)
.response
.changed()
{
field_value.modified = true;
*saved = false;
}
}
FieldType::MultiLine => {
if TextEdit::multiline(&mut field_value.value)
FieldValue::MultiLine(value) => {
if TextEdit::multiline(value)
.desired_width(f32::INFINITY)
.desired_rows(5)
.frame(false)
@@ -238,51 +229,39 @@ impl ObjectInstance {
.response
.changed()
{
field_value.modified = true;
*saved = false;
}
}
FieldType::Date => {
let date_str = &field_value.value;
let mut date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Local::now().date_naive());
let response = ui.add(egui_extras::DatePickerButton::new(&mut date));
FieldValue::Date(value) => {
let response = ui.add(egui_extras::DatePickerButton::new(value));
if response.changed() {
field_value.value = date.format("%Y-%m-%d").to_string();
field_value.modified = true;
*saved = false;
}
}
FieldType::Number => {
let mut num = field_value.value.parse::<f64>().unwrap_or(0.0);
let response = ui.add(egui::DragValue::new(&mut num).speed(0.1));
FieldValue::Number(value) => {
let response = ui.add(egui::DragValue::new(value).speed(0.1));
if response.changed() {
field_value.value = num.to_string();
field_value.modified = true;
*saved = false;
}
}
FieldType::Image => {
FieldValue::Image(value) => {
ui.scope_builder(UiBuilder::new().sense(Sense::HOVER), |ui| {
let id = ui.make_persistent_id("is_hovered");
let should_show = field_value.value.is_empty()
let should_show = value.is_empty()
|| ui.response().hovered()
|| ui.memory(|mem| mem.data.get_temp(id).unwrap_or(false));
|| 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(&mut field_value.value)
.hint_text("Path to image")
let response = TextEdit::singleline(value)
.hint_text("Asset name (ignore file extension)")
.desired_width(f32::INFINITY)
.frame(false)
.show(ui)
.response;
if response.changed() {
field_value.modified = true;
*saved = false;
}
@@ -292,10 +271,10 @@ impl ObjectInstance {
}
// If we have a valid path, try to display a preview
if !field_value.value.is_empty() {
if let Ok(bytes) = std::fs::read(&field_value.value) {
let path = PROJECT_FOLDER.join(&field_value.value);
if !value.is_empty() {
let path = PROJECT_FOLDER.join("assets").join(&value);
if let Ok(bytes) = std::fs::read(&path) {
let image_source = egui::ImageSource::Bytes {
uri: std::borrow::Cow::Owned(path.to_str().unwrap().to_string()),
bytes: bytes.into(),
@@ -307,10 +286,12 @@ impl ObjectInstance {
}
});
}
FieldType::Link => ObjectInstance::selector_ui(field_value, objects, ui, saved),
FieldType::Links => {
if ui.text_edit_singleline(&mut field_value.value).changed() {
field_value.modified = true;
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;
}
}
@@ -318,13 +299,13 @@ impl ObjectInstance {
}
fn selector_ui(
field_value: &mut FieldValue,
objects: &mut Vec<ObjectInstance>,
selected: &mut ObjectId,
objects: &mut [ObjectInstance],
ui: &mut egui::Ui,
saved: &mut bool,
) {
if !field_value.value.is_empty() {
if let Ok(object) = ObjectInstance::load(&field_value.value) {
if !selected.is_empty() {
if let Ok(object) = ObjectInstance::load(selected) {
ui.strong(&object.name);
}
}
@@ -334,7 +315,7 @@ impl ObjectInstance {
let ctx = ui.ctx();
let mut object_selection: usize =
ctx.memory_mut(|mem| *mem.data.get_temp_mut_or_default::<usize>(id));
ctx.memory(|mem| mem.data.get_temp::<usize>(id).unwrap_or(0));
if objects.is_empty() {
ui.label("No objects available");
@@ -348,15 +329,18 @@ impl ObjectInstance {
});
}
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() {
field_value.value = objects[object_selection].id.clone();
field_value.modified = true;
*selected = objects[object_selection].id.clone();
*saved = false;
}
if ui.button("Remove").clicked() {
field_value.value.clear();
field_value.modified = true;
*selected = String::new();
*saved = false;
}
});
+56 -9
View File
@@ -1,5 +1,6 @@
use core::fmt;
use chrono::NaiveDate;
use egui::ScrollArea;
use serde::{Deserialize, Serialize};
@@ -16,7 +17,7 @@ pub enum FieldType {
MultiLine,
Date,
Number,
Link,
Link { template_id: Option<String> },
Links,
}
@@ -34,17 +35,54 @@ impl FieldType {
FieldType::MultiLine,
FieldType::Date,
FieldType::Number,
FieldType::Link,
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>,
}
@@ -73,6 +111,9 @@ pub struct Template {
#[serde(skip)]
pub new_field_description: String,
#[serde(skip)]
pub new_field_on_preview: bool,
}
impl fmt::Debug for Template {
@@ -105,6 +146,7 @@ impl Clone for Template {
new_field_type: FieldType::default(),
new_field_required: false,
new_field_description: "".to_string(),
new_field_on_preview: false,
}
}
}
@@ -123,6 +165,7 @@ impl Default for Template {
new_field_type: FieldType::default(),
new_field_required: false,
new_field_description: "".to_string(),
new_field_on_preview: false,
}
}
}
@@ -325,6 +368,12 @@ impl Template {
}
ui.end_row();
ui.label("On Preview:");
if ui.checkbox(&mut field.on_preview, "").clicked() {
self.saved = false;
}
ui.end_row();
ui.label("Description:");
if ui
.text_edit_singleline(
@@ -377,6 +426,10 @@ impl Template {
ui.checkbox(&mut self.new_field_required, "");
ui.end_row();
ui.label("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.end_row();
@@ -385,6 +438,7 @@ impl Template {
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
@@ -404,10 +458,3 @@ impl Template {
});
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FieldValue {
pub value: String,
#[serde(skip)]
pub modified: bool,
}
+239 -155
View File
@@ -1,3 +1,5 @@
use walkdir::{DirEntry, WalkDir};
use crate::{
PROJECT_FOLDER, RightPanelContent,
content_editor::MainEditor,
@@ -14,7 +16,6 @@ pub struct Explorer {
notes: Vec<Note>,
documents: Vec<MainEditor>,
tags: Vec<Tag>,
assets: Vec<Asset>,
}
impl Explorer {
@@ -25,7 +26,6 @@ impl Explorer {
notes: Vec::new(),
documents: Vec::new(),
tags: Vec::new(),
assets: Vec::new(),
}
}
@@ -43,161 +43,122 @@ impl Explorer {
self.load_objects().expect("Failed to load objects");
self.load_notes().expect("Failed to load notes");
self.load_documents().expect("Failed to load documents");
self.load_assets().expect("Failed to load assets");
self.load_tags().expect("Failed to load tags");
ui.vertical(|ui| {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("templates"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Templates");
if ui.button("+").clicked() {
*to_load = Some(RightPanelContent::template(Some(Template::default())));
}
});
})
.body(|ui| {
for template in &self.templates {
let id = ui.make_persistent_id(template.name.clone());
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
id,
true,
)
.show_header(ui, |ui| {
// load the template
if ui.selectable_label(false, template.name.clone()).clicked() {
*to_load = Some(RightPanelContent::template(Some(template.clone())));
}
self.render_templates(ui, to_load);
self.render_notes(ui, to_load);
self.render_doc_root(ui, load_doc);
self.render_tags(ui, to_load);
self.render_assets(ui, to_load);
});
}
// create a new object based on this template
if ui.button("+").clicked() {
*to_load = Some(RightPanelContent::object(Some(ObjectInstance::new(
template,
))));
}
})
.body(|ui| {
for object in &self.objects {
if object.template_id == 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())));
}
});
}
}
});
fn render_templates(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("templates"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Templates");
if ui.button("+").clicked() {
*to_load = Some(RightPanelContent::template(Some(Template::default())));
}
});
})
.body(|ui| {
for template in &self.templates {
let id = ui.make_persistent_id(template.name.clone());
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
id,
true,
)
.show_header(ui, |ui| {
// load the template
if ui.selectable_label(false, template.name.clone()).clicked() {
*to_load = Some(RightPanelContent::template(Some(template.clone())));
}
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("notes"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Notes");
// create a new object based on this template
if ui.button("+").clicked() {
*to_load = Some(RightPanelContent::note(Some(Note::default())));
*to_load = Some(RightPanelContent::object(Some(ObjectInstance::new(
template,
))));
}
})
.body(|ui| {
for object in &self.objects {
if object.template_id == 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())));
}
});
}
}
});
})
.body(|ui| {
for note in &self.notes {
ui.horizontal(|ui| {
ui.add_space(10.0);
}
});
}
// load the note
if ui.selectable_label(false, &note.name).clicked() {
*to_load = Some(RightPanelContent::note(Some(note.clone())));
}
});
fn render_notes(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("notes"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Notes");
if ui.button("+").clicked() {
*to_load = Some(RightPanelContent::note(Some(Note::default())));
}
});
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("projects"),
true,
)
.show_header(ui, |ui| {
})
.body(|ui| {
for note in &self.notes {
ui.horizontal(|ui| {
ui.label("Projects");
if ui.button("+").clicked() {
*load_doc = Some(MainEditor::open(ContentSection::new()));
ui.add_space(10.0);
// load the note
if ui.selectable_label(false, &note.name).clicked() {
*to_load = Some(RightPanelContent::note(Some(note.clone())));
}
});
})
.body(|ui| {
// Convert MainEditor vec to ContentSection vec
let content_sections: Vec<ContentSection> = self
.documents
.iter()
.map(|doc| doc.content.clone())
.collect();
}
});
}
Self::render_document_tree(ui, &content_sections, None, load_doc);
});
self.tags = Tag::load_all();
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("tags"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Tags");
if ui.button("+").clicked() {
*to_load = Some(RightPanelContent::Tag(Tag::default()));
}
});
})
.body(|ui| {
for tag in &mut self.tags {
ui.horizontal(|ui| {
ui.add_space(10.0);
// load the tag
if tag.list_ui(ui).clicked() {
*to_load = Some(RightPanelContent::Tag(tag.clone()));
}
});
fn render_doc_root(&self, ui: &mut egui::Ui, load_doc: &mut Option<MainEditor>) {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("projects"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Projects");
if ui.button("+").clicked() {
*load_doc = Some(MainEditor::open(ContentSection::new()));
}
});
})
.body(|ui| {
// Convert MainEditor vec to ContentSection vec
let content_sections: Vec<ContentSection> = self
.documents
.iter()
.map(|doc| doc.content.clone())
.collect();
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("assets"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Assets");
});
})
.body(|ui| {
for asset in &mut self.assets {
ui.horizontal(|ui| {
ui.add_space(10.0);
// load the asset
if ui.selectable_label(false, &asset.name).clicked() {
*to_load = Some(RightPanelContent::Asset(Box::new(asset.clone())));
}
});
}
});
Self::render_doc_branch(ui, &content_sections, None, load_doc);
});
}
@@ -209,7 +170,7 @@ impl Explorer {
///
/// `load_doc` is a mutable reference to a `MainEditor`. When a document is clicked, it
/// is loaded into the `MainEditor` and returned as `Some`.
fn render_document_tree(
fn render_doc_branch(
ui: &mut egui::Ui,
documents: &[ContentSection],
parent_id: Option<&str>,
@@ -243,15 +204,135 @@ impl Explorer {
})
.body(|ui| {
// recursive call to render the next level of documents
Self::render_document_tree(ui, documents, Some(&doc.id), load_doc);
Self::render_doc_branch(ui, documents, Some(&doc.id), load_doc);
});
}
}
fn render_tags(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("tags"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Tags");
if ui.button("+").clicked() {
*to_load = Some(RightPanelContent::Tag(Tag::default()));
}
});
})
.body(|ui| {
for tag in &mut self.tags {
ui.horizontal(|ui| {
ui.add_space(10.0);
// load the tag
if tag.list_ui(ui).clicked() {
*to_load = Some(RightPanelContent::Tag(tag.clone()));
}
});
}
});
}
fn render_assets(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("assets"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Assets");
});
})
.body(|ui| {
let mut entries: Vec<_> = WalkDir::new(PROJECT_FOLDER.join("assets"))
.min_depth(1)
.max_depth(1) // Only immediate children
.sort_by(|a, b| {
// Directories first, then files
let a_is_dir = a.file_type().is_dir();
let b_is_dir = b.file_type().is_dir();
if a_is_dir == b_is_dir {
a.file_name().cmp(b.file_name())
} else if a_is_dir {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
})
.into_iter()
.filter_map(Result::ok)
.collect();
for entry in entries {
self.render_entry(ui, to_load, &entry);
}
});
}
fn render_entry(
&mut self,
ui: &mut egui::Ui,
to_load: &mut Option<RightPanelContent>,
entry: &DirEntry,
) {
let file_type = entry.file_type();
let is_dir = file_type.is_dir();
let file_name = entry.file_name().to_string_lossy();
let path = entry.path();
if is_dir {
let entries: Vec<_> = WalkDir::new(path)
.min_depth(1)
.max_depth(1)
.sort_by(|a, b| a.file_name().cmp(b.file_name()))
.into_iter()
.filter_map(Result::ok)
.collect();
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id(&file_name),
false,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label(file_name);
let clicked = ui.button("+").on_hover_text("Add new item").clicked();
});
})
.body(|ui| {
// recursive call to render the next level of documents
for entry in entries {
self.render_entry(ui, to_load, &entry);
}
});
} else {
// Handle file
if ui
.selectable_label(false, format!("📄 {file_name}"))
.clicked()
{
// use asset::load to get the file at the path
let asset_path = path.strip_prefix(PROJECT_FOLDER.join("assets")).unwrap();
let asset = Asset::open(asset_path.to_string_lossy().to_string());
*to_load = Some(RightPanelContent::Asset(Box::new(asset)));
}
}
}
// load templates from the templates folder
fn load_templates(&mut self) -> std::io::Result<()> {
let templates_folder = PROJECT_FOLDER.join("templates");
if !templates_folder.exists() {
std::fs::create_dir_all(&templates_folder)?;
}
let mut templates = Vec::new();
for entry in std::fs::read_dir(PROJECT_FOLDER.join("templates")).unwrap() {
for entry in std::fs::read_dir(&templates_folder).unwrap() {
let path = entry.unwrap().path();
match Template::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(t) => templates.push(t),
@@ -265,8 +346,12 @@ impl Explorer {
// load objects from the objects folder
fn load_objects(&mut self) -> std::io::Result<()> {
let objects_folder = PROJECT_FOLDER.join("objects");
if !objects_folder.exists() {
std::fs::create_dir_all(&objects_folder)?;
}
let mut objects = Vec::new();
for entry in std::fs::read_dir(PROJECT_FOLDER.join("objects")).unwrap() {
for entry in std::fs::read_dir(&objects_folder).unwrap() {
let path = entry.unwrap().path();
match ObjectInstance::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(o) => objects.push(o),
@@ -280,9 +365,13 @@ impl Explorer {
// load notes from the notes folder
fn load_notes(&mut self) -> std::io::Result<()> {
let notes_folder = PROJECT_FOLDER.join("notes");
if !notes_folder.exists() {
std::fs::create_dir_all(&notes_folder)?;
}
let mut notes = Vec::new();
for entry in std::fs::read_dir(PROJECT_FOLDER.join("notes")).unwrap() {
for entry in std::fs::read_dir(&notes_folder).unwrap() {
let path = entry.unwrap().path();
match Note::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(note) => notes.push(note),
@@ -297,9 +386,13 @@ impl Explorer {
// load documents from the documents folder
fn load_documents(&mut self) -> std::io::Result<()> {
let documents_folder = PROJECT_FOLDER.join("documents");
if !documents_folder.exists() {
std::fs::create_dir_all(&documents_folder)?;
}
let mut documents = Vec::new();
for entry in std::fs::read_dir(PROJECT_FOLDER.join("documents")).unwrap() {
for entry in std::fs::read_dir(&documents_folder).unwrap() {
let path = entry.unwrap().path();
match ContentSection::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(document) => documents.push(MainEditor::open(document)),
@@ -312,17 +405,8 @@ impl Explorer {
Ok(())
}
fn load_assets(&mut self) -> std::io::Result<()> {
let mut assets = Vec::new();
for entry in std::fs::read_dir(PROJECT_FOLDER.join("assets")).unwrap() {
let path = entry.unwrap().path();
assets.push(Asset::open(
path.file_stem().unwrap().to_str().unwrap().to_string(),
));
}
self.assets = assets;
fn load_tags(&mut self) -> std::io::Result<()> {
self.tags = Tag::load_all();
Ok(())
}
}
+79
View File
@@ -0,0 +1,79 @@
use serde::{Deserialize, Serialize};
pub fn continue_content(
context: &str,
instruction: &str,
max_tokens: usize,
) -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::new();
let messages = vec![
Message {
role: "system".to_string(),
content: "
Please generate content that is a direct continuation of the given text.
Your response should be a logical next step in the content and should not repeat any of the text from the instruction or the content.
Do not generate any text that is not a direct continuation of the content.
if extra instructions are provided, follow them exactly, otherwise continue the text in a logical way.
".to_string(),
},
Message {
role: "user".to_string(),
content: context.to_string(),
},
Message {
role: "user".to_string(),
content: format!("Instructions: {instruction}"),
},
];
let request = ChatRequest {
messages,
temperature: 0.7,
};
let response = client
.post("http://localhost:1234/v1/chat/completions")
.json(&request)
.send()?;
if !response.status().is_success() {
return Err(format!("Request failed: {}", response.text()?).into());
}
let response: ChatResponse = response.json()?;
if let Some(choice) = response.choices.into_iter().next() {
Ok(choice.message.content)
} else {
Err("No response from model".into())
}
}
pub fn ai_enabled() -> bool {
let client = reqwest::blocking::Client::new();
client.get("http://localhost:1234/v1/models").send().is_ok()
}
// Simple request structure
#[derive(Serialize)]
struct ChatRequest {
messages: Vec<Message>,
temperature: f32,
}
#[derive(Serialize, Deserialize, Debug)]
struct Message {
role: String,
content: String,
}
#[derive(Deserialize, Debug)]
struct ChatResponse {
choices: Vec<Choice>,
}
#[derive(Deserialize, Debug)]
struct Choice {
message: Message,
}
+1
View File
@@ -0,0 +1 @@
pub mod content_llm;
+19 -7
View File
@@ -4,14 +4,15 @@ use egui::ScrollArea;
mod editors;
mod explorer;
mod llm_integration;
mod scene;
mod util;
use crate::{
editors::{
asset_editor::Asset, content_editor, note_editor, object_editor::ObjectInstance, tags::Tag,
template_editor::Template,
asset_editor::Asset, content_editor, context_editor::ProjectContext, note_editor,
object_editor::ObjectInstance, tags::Tag, template_editor::Template,
},
explorer::Explorer,
};
@@ -39,6 +40,7 @@ pub struct Interface {
editor: content_editor::MainEditor,
scene: scene::EditorScene,
explorer: Explorer,
project: ProjectContext,
}
impl eframe::App for Interface {
@@ -84,6 +86,7 @@ impl Interface {
editor: content_editor::MainEditor::new(),
scene: scene::EditorScene::new(),
explorer: Explorer::new(),
project: ProjectContext::load(),
}
}
@@ -183,28 +186,31 @@ impl Interface {
});
}
// render main content area
fn render_main_content(&mut self, ctx: &egui::Context) {
self.editor.ui(ctx);
self.scene.ui(ctx);
self.scene.ui(ctx, &mut self.explorer.objects());
}
// 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((2.0, egui::Color32::from_rgb(255, 255, 255)));
egui::Stroke::from((1.0, egui::Color32::from_rgb(255, 255, 255)));
visuals.widgets.inactive.bg_stroke =
egui::Stroke::from((2.0, egui::Color32::from_rgb(60, 60, 60)));
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 = 2.0;
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!(
@@ -233,3 +239,9 @@ impl Default for Interface {
Self::new()
}
}
impl Drop for Interface {
fn drop(&mut self) {
self.project.save();
}
}
+125 -5
View File
@@ -1,3 +1,13 @@
use egui::{RichText, vec2};
use crate::{
PROJECT_FOLDER,
editors::{
object_editor::ObjectInstance,
template_editor::{FieldType, FieldValue, Template},
},
};
pub struct EditorScene {
rect: egui::Rect,
}
@@ -9,17 +19,127 @@ impl EditorScene {
}
}
pub fn ui(&mut self, ctx: &egui::Context) {
pub fn ui(&mut self, ctx: &egui::Context, objects: &mut [ObjectInstance]) {
egui::CentralPanel::default()
.frame(egui::Frame::NONE)
.show(ctx, |ui| {
egui::Scene::default()
.zoom_range(0.1..=10.0)
.show(ui, &mut self.rect, |ui| {
egui::Resize::default().auto_sized().show(ui, |ui| {
ui.group(|ui| {
ui.label("Scene");
});
ui.horizontal_wrapped(|ui| {
ui.set_max_width(5000.0);
// Group objects by their template_id
use std::collections::HashMap;
let mut objects_by_template: HashMap<String, Vec<&ObjectInstance>> =
HashMap::new();
for obj in objects {
objects_by_template
.entry(obj.template_id.clone())
.or_default()
.push(obj);
}
// For each template with objects, create cards
for (template_id, template_objects) in objects_by_template {
// Try to load the template to get field definitions
if let Ok(mut template) = Template::load(&template_id) {
for obj in template_objects {
// Create a card for each object
egui::Frame::group(ui.style())
.fill(egui::Color32::from_rgba_premultiplied(
30, 30, 30, 200,
))
.corner_radius(4.0)
.show(ui, |ui| {
ui.vertical(|ui| {
ui.set_max_width(512.0);
ui.set_min_width(512.0);
// Object name as header
ui.heading(RichText::new(&obj.name).strong());
// Show fields with on_preview = true
template.fields.sort_by_key(|field| field.field_type != FieldType::Image);
for field_def in &template.fields {
if field_def.on_preview {
if let Some(field_value) =
obj.fields.get(&field_def.name)
{
ui.separator();
match field_value {
FieldValue::SingleLine(
text,
) => {
ui.strong(&field_def.name);
ui.label(text);
}
FieldValue::MultiLine(
text,
) => {
ui.strong(&field_def.name);
ui.label(text);
}
FieldValue::Number(n) => {
ui.strong(&field_def.name);
ui.label(n.to_string());
}
FieldValue::Date(date) => {
ui.strong(&field_def.name);
ui.label(
date.format(
"%Y-%m-%d",
)
.to_string(),
);
}
FieldValue::Image(value) => {
if !value.is_empty() {
let path = PROJECT_FOLDER.join("assets").join(value);
if let Ok(bytes) = std::fs::read(&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).fit_to_exact_size(vec2(512.0, 512.0)),
);
}
}
}
FieldValue::Link(
target_id,
) => {
ui.strong(&field_def.name);
ui.label(format!(
"{target_id}"
));
}
FieldValue::Links(
links,
) => {
ui.strong(&field_def.name);
ui.label(format!(
"{} links",
links.len()
));
}
}
}
}
}
});
});
// Add some spacing between cards
ui.add_space(8.0);
}
}
}
});
});
});