diff --git a/Cargo.lock b/Cargo.lock index 693e06d..39352df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3305,6 +3305,7 @@ dependencies = [ "hex", "nostrdb", "notedeck", + "rand 0.9.0", "reqwest", "serde", "serde_json", diff --git a/crates/notedeck_dave/Cargo.toml b/crates/notedeck_dave/Cargo.toml index 42fad61..4f0597f 100644 --- a/crates/notedeck_dave/Cargo.toml +++ b/crates/notedeck_dave/Cargo.toml @@ -16,6 +16,7 @@ serde = { workspace = true } nostrdb = { workspace = true } hex = { workspace = true } time = "0.3.41" +rand = "0.9.0" bytemuck = "1.22.0" futures = "0.3.31" reqwest = "0.12.15" diff --git a/crates/notedeck_dave/src/avatar.rs b/crates/notedeck_dave/src/avatar.rs index 9b01721..d5b9639 100644 --- a/crates/notedeck_dave/src/avatar.rs +++ b/crates/notedeck_dave/src/avatar.rs @@ -1,11 +1,13 @@ use std::num::NonZeroU64; +use crate::vec3::Vec3; use eframe::egui_wgpu::{self, wgpu}; use egui::{Rect, Response}; +use rand::Rng; pub struct DaveAvatar { rotation: Quaternion, - rot_dir: egui::Vec2, + rot_dir: Vec3, } // A simple quaternion implementation @@ -28,13 +30,13 @@ impl Quaternion { } // Create from axis-angle representation - fn from_axis_angle(axis: [f32; 3], angle: f32) -> Self { + fn from_axis_angle(axis: &Vec3, angle: f32) -> Self { let half_angle = angle * 0.5; let s = half_angle.sin(); Self { - x: axis[0] * s, - y: axis[1] * s, - z: axis[2] * s, + x: axis.x * s, + y: axis.y * s, + z: axis.z * s, w: half_angle.cos(), } } @@ -358,7 +360,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { Self { rotation: Quaternion::identity(), - rot_dir: egui::Vec2::ZERO, + rot_dir: Vec3::new(0.0, 0.0, 0.0), } } } @@ -373,6 +375,21 @@ fn apply_friction(val: f32, friction: f32, clamp: f32) -> f32 { } impl DaveAvatar { + pub fn random_nudge(&mut self) { + let mut rng = rand::rng(); + + let nudge = Vec3::new( + rng.random::(), + rng.random::(), + rng.random::(), + ) + .normalize(); + + self.rot_dir.x += nudge.x; + self.rot_dir.y += nudge.y; + self.rot_dir.z += nudge.z; + } + pub fn render(&mut self, rect: Rect, ui: &mut egui::Ui) -> Response { let response = ui.allocate_rect(rect, egui::Sense::drag()); @@ -381,10 +398,10 @@ impl DaveAvatar { // Create rotation quaternions based on drag let dx = response.drag_delta().x; let dy = response.drag_delta().y; - let x_rotation = Quaternion::from_axis_angle([1.0, 0.0, 0.0], dy * 0.01); - let y_rotation = Quaternion::from_axis_angle([0.0, 1.0, 0.0], dx * 0.01); + let x_rotation = Quaternion::from_axis_angle(&Vec3::new(1.0, 0.0, 0.0), dy * 0.01); + let y_rotation = Quaternion::from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), dx * 0.01); - self.rot_dir = egui::Vec2::new(dx, dy); + self.rot_dir = Vec3::new(dx, dy, 0.0); // Apply rotations (order matters) self.rotation = y_rotation.multiply(&x_rotation).multiply(&self.rotation); @@ -394,15 +411,21 @@ impl DaveAvatar { let clamp = 0.1; self.rot_dir.x = apply_friction(self.rot_dir.x, friction, clamp); self.rot_dir.y = apply_friction(self.rot_dir.y, friction, clamp); + self.rot_dir.z = apply_friction(self.rot_dir.y, friction, clamp); // we only need to render if we're still spinning - if self.rot_dir.x > clamp || self.rot_dir.y > clamp { + if self.rot_dir.x > clamp || self.rot_dir.y > clamp || self.rot_dir.z > clamp { let x_rotation = - Quaternion::from_axis_angle([1.0, 0.0, 0.0], self.rot_dir.y * 0.01); + Quaternion::from_axis_angle(&Vec3::new(1.0, 0.0, 0.0), self.rot_dir.y * 0.01); let y_rotation = - Quaternion::from_axis_angle([0.0, 1.0, 0.0], self.rot_dir.x * 0.01); + Quaternion::from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), self.rot_dir.x * 0.01); + let z_rotation = + Quaternion::from_axis_angle(&Vec3::new(0.0, 0.0, 1.0), self.rot_dir.z * 0.01); - self.rotation = y_rotation.multiply(&x_rotation).multiply(&self.rotation); + self.rotation = y_rotation + .multiply(&x_rotation) + .multiply(&z_rotation) + .multiply(&self.rotation); ui.ctx().request_repaint(); } diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs index 246ee7a..8da5f24 100644 --- a/crates/notedeck_dave/src/lib.rs +++ b/crates/notedeck_dave/src/lib.rs @@ -26,6 +26,7 @@ use egui::{Rect, Vec2}; use egui_wgpu::RenderState; mod avatar; +mod vec3; #[derive(Debug, Clone)] pub enum Message { @@ -331,8 +332,12 @@ impl Dave { } fn render(&mut self, app_ctx: &AppContext, ui: &mut egui::Ui) { + let mut should_send = false; if let Some(recvr) = &self.incoming_tokens { while let Ok(res) = recvr.try_recv() { + if let Some(avatar) = &mut self.avatar { + avatar.random_nudge(); + } match res { DaveResponse::Token(token) => match self.chat.last_mut() { Some(Message::Assistant(msg)) => *msg = msg.clone() + &token, @@ -357,31 +362,44 @@ impl Dave { } } } + + should_send = true; } } } } // Scroll area for chat messages - egui::Frame::new().inner_margin(10.0).show(ui, |ui| { - egui::ScrollArea::vertical() - .stick_to_bottom(true) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.vertical(|ui| { - self.render_chat(ui); + egui::Frame::new() + .outer_margin(egui::Margin { + top: 100, + ..Default::default() + }) + .inner_margin(10.0) + .show(ui, |ui| { + egui::ScrollArea::vertical() + .stick_to_bottom(true) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.vertical(|ui| { + self.render_chat(ui); - self.inputbox(app_ctx, ui); - }) - }); - }); + self.inputbox(app_ctx, ui); + }) + }); + }); if let Some(avatar) = &mut self.avatar { - let avatar_size = Vec2::splat(200.0); - let pos = Vec2::splat(100.0).to_pos2(); + let avatar_size = Vec2::splat(100.0); + let pos = Vec2::splat(10.0).to_pos2(); let pos = Rect::from_min_max(pos, pos + avatar_size); avatar.render(pos, ui); } + + // send again + if should_send { + self.send_user_message(app_ctx, ui.ctx()); + } } fn render_chat(&self, ui: &mut egui::Ui) { @@ -441,7 +459,7 @@ impl Dave { egui::Frame::new() .inner_margin(10.0) .corner_radius(10.0) - .fill(ui.visuals().extreme_bg_color) + .fill(ui.visuals().widgets.inactive.weak_bg_fill) .show(ui, |ui| { ui.label(msg); }) diff --git a/crates/notedeck_dave/src/vec3.rs b/crates/notedeck_dave/src/vec3.rs new file mode 100644 index 0000000..5fb2d52 --- /dev/null +++ b/crates/notedeck_dave/src/vec3.rs @@ -0,0 +1,38 @@ +#[derive(Debug, Clone, Copy)] +pub struct Vec3 { + pub x: f32, + pub y: f32, + pub z: f32, +} + +impl Vec3 { + pub fn new(x: f32, y: f32, z: f32) -> Self { + Vec3 { x, y, z } + } + + pub fn squared_length(&self) -> f32 { + self.x * self.x + self.y * self.y + self.z * self.z + } + + pub fn length(&self) -> f32 { + self.squared_length().sqrt() + } + + pub fn normalize(&self) -> Vec3 { + let len = self.length(); + if len != 0.0 { + Vec3 { + x: self.x / len, + y: self.y / len, + z: self.z / len, + } + } else { + // Return zero vector if length is zero to avoid NaNs + Vec3 { + x: 0.0, + y: 0.0, + z: 0.0, + } + } + } +}