notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 31aae7f3152bc8c152f23c2dfaee8376cd3a1e11
parent 80f02d829a767df6841d1f1ddbe18f72a2144d6c
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 26 Mar 2025 06:34:35 -0700

dave: auto-reply, initial avatar anim

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
MCargo.lock | 1+
Mcrates/notedeck_dave/Cargo.toml | 1+
Mcrates/notedeck_dave/src/avatar.rs | 51+++++++++++++++++++++++++++++++++++++--------------
Mcrates/notedeck_dave/src/lib.rs | 48+++++++++++++++++++++++++++++++++---------------
Acrates/notedeck_dave/src/vec3.rs | 38++++++++++++++++++++++++++++++++++++++
5 files changed, 110 insertions(+), 29 deletions(-)

diff --git 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 @@ -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 @@ -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<f32> { 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::<f32>(), + rng.random::<f32>(), + rng.random::<f32>(), + ) + .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); - - self.rotation = y_rotation.multiply(&x_rotation).multiply(&self.rotation); + 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(&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 @@ -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); - - self.inputbox(app_ctx, 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); + }) + }); + }); 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 @@ -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, + } + } + } +}