dave: auto-reply, initial avatar anim

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-03-26 06:34:35 -07:00
parent 80f02d829a
commit 31aae7f315
5 changed files with 108 additions and 27 deletions

1
Cargo.lock generated
View File

@@ -3305,6 +3305,7 @@ dependencies = [
"hex", "hex",
"nostrdb", "nostrdb",
"notedeck", "notedeck",
"rand 0.9.0",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",

View File

@@ -16,6 +16,7 @@ serde = { workspace = true }
nostrdb = { workspace = true } nostrdb = { workspace = true }
hex = { workspace = true } hex = { workspace = true }
time = "0.3.41" time = "0.3.41"
rand = "0.9.0"
bytemuck = "1.22.0" bytemuck = "1.22.0"
futures = "0.3.31" futures = "0.3.31"
reqwest = "0.12.15" reqwest = "0.12.15"

View File

@@ -1,11 +1,13 @@
use std::num::NonZeroU64; use std::num::NonZeroU64;
use crate::vec3::Vec3;
use eframe::egui_wgpu::{self, wgpu}; use eframe::egui_wgpu::{self, wgpu};
use egui::{Rect, Response}; use egui::{Rect, Response};
use rand::Rng;
pub struct DaveAvatar { pub struct DaveAvatar {
rotation: Quaternion, rotation: Quaternion,
rot_dir: egui::Vec2, rot_dir: Vec3,
} }
// A simple quaternion implementation // A simple quaternion implementation
@@ -28,13 +30,13 @@ impl Quaternion {
} }
// Create from axis-angle representation // 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 half_angle = angle * 0.5;
let s = half_angle.sin(); let s = half_angle.sin();
Self { Self {
x: axis[0] * s, x: axis.x * s,
y: axis[1] * s, y: axis.y * s,
z: axis[2] * s, z: axis.z * s,
w: half_angle.cos(), w: half_angle.cos(),
} }
} }
@@ -358,7 +360,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
Self { Self {
rotation: Quaternion::identity(), 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 { 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 { pub fn render(&mut self, rect: Rect, ui: &mut egui::Ui) -> Response {
let response = ui.allocate_rect(rect, egui::Sense::drag()); let response = ui.allocate_rect(rect, egui::Sense::drag());
@@ -381,10 +398,10 @@ impl DaveAvatar {
// Create rotation quaternions based on drag // Create rotation quaternions based on drag
let dx = response.drag_delta().x; let dx = response.drag_delta().x;
let dy = response.drag_delta().y; let dy = response.drag_delta().y;
let x_rotation = Quaternion::from_axis_angle([1.0, 0.0, 0.0], dy * 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([0.0, 1.0, 0.0], dx * 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) // Apply rotations (order matters)
self.rotation = y_rotation.multiply(&x_rotation).multiply(&self.rotation); self.rotation = y_rotation.multiply(&x_rotation).multiply(&self.rotation);
@@ -394,15 +411,21 @@ impl DaveAvatar {
let clamp = 0.1; let clamp = 0.1;
self.rot_dir.x = apply_friction(self.rot_dir.x, friction, clamp); 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.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 // 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 = 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 = 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(); ui.ctx().request_repaint();
} }

View File

@@ -26,6 +26,7 @@ use egui::{Rect, Vec2};
use egui_wgpu::RenderState; use egui_wgpu::RenderState;
mod avatar; mod avatar;
mod vec3;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
@@ -331,8 +332,12 @@ impl Dave {
} }
fn render(&mut self, app_ctx: &AppContext, ui: &mut egui::Ui) { fn render(&mut self, app_ctx: &AppContext, ui: &mut egui::Ui) {
let mut should_send = false;
if let Some(recvr) = &self.incoming_tokens { if let Some(recvr) = &self.incoming_tokens {
while let Ok(res) = recvr.try_recv() { while let Ok(res) = recvr.try_recv() {
if let Some(avatar) = &mut self.avatar {
avatar.random_nudge();
}
match res { match res {
DaveResponse::Token(token) => match self.chat.last_mut() { DaveResponse::Token(token) => match self.chat.last_mut() {
Some(Message::Assistant(msg)) => *msg = msg.clone() + &token, Some(Message::Assistant(msg)) => *msg = msg.clone() + &token,
@@ -357,31 +362,44 @@ impl Dave {
} }
} }
} }
should_send = true;
} }
} }
} }
} }
// Scroll area for chat messages // Scroll area for chat messages
egui::Frame::new().inner_margin(10.0).show(ui, |ui| { egui::Frame::new()
egui::ScrollArea::vertical() .outer_margin(egui::Margin {
.stick_to_bottom(true) top: 100,
.auto_shrink([false; 2]) ..Default::default()
.show(ui, |ui| { })
ui.vertical(|ui| { .inner_margin(10.0)
self.render_chat(ui); .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 { if let Some(avatar) = &mut self.avatar {
let avatar_size = Vec2::splat(200.0); let avatar_size = Vec2::splat(100.0);
let pos = Vec2::splat(100.0).to_pos2(); let pos = Vec2::splat(10.0).to_pos2();
let pos = Rect::from_min_max(pos, pos + avatar_size); let pos = Rect::from_min_max(pos, pos + avatar_size);
avatar.render(pos, ui); 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) { fn render_chat(&self, ui: &mut egui::Ui) {
@@ -441,7 +459,7 @@ impl Dave {
egui::Frame::new() egui::Frame::new()
.inner_margin(10.0) .inner_margin(10.0)
.corner_radius(10.0) .corner_radius(10.0)
.fill(ui.visuals().extreme_bg_color) .fill(ui.visuals().widgets.inactive.weak_bg_fill)
.show(ui, |ui| { .show(ui, |ui| {
ui.label(msg); ui.label(msg);
}) })

View File

@@ -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,
}
}
}
}