notedeck

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

commit 9692b6b9fecb79a08f0c5d3a2d7abc823a02dbc5
parent 5c8fba220c79f26a5a633eaf7d3f8700992362e4
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 22 Apr 2025 10:49:37 -0700

dave: add query rendering, fix author queries

Diffstat:
Mcrates/notedeck_dave/src/lib.rs | 4++--
Mcrates/notedeck_dave/src/tools.rs | 57+++++++++++++++++++++++++++++++++------------------------
Mcrates/notedeck_dave/src/ui/dave.rs | 114++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
3 files changed, 125 insertions(+), 50 deletions(-)

diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -17,8 +17,8 @@ pub use config::ModelConfig; pub use messages::{DaveApiResponse, Message}; pub use quaternion::Quaternion; pub use tools::{ - PartialToolCall, QueryCall, QueryContext, QueryResponse, Tool, ToolCall, ToolCalls, - ToolResponse, ToolResponses, + PartialToolCall, QueryCall, QueryResponse, Tool, ToolCall, ToolCalls, ToolResponse, + ToolResponses, }; pub use ui::{DaveAction, DaveResponse, DaveUi}; pub use vec3::Vec3; diff --git a/crates/notedeck_dave/src/tools.rs b/crates/notedeck_dave/src/tools.rs @@ -1,6 +1,6 @@ use async_openai::types::*; use chrono::DateTime; -use enostr::NoteId; +use enostr::{NoteId, Pubkey}; use nostrdb::{Ndb, Note, NoteKey, Transaction}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -70,7 +70,6 @@ impl PartialToolCall { /// The query response from nostrdb for a given context #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QueryResponse { - context: QueryContext, notes: Vec<u64>, } @@ -286,14 +285,6 @@ impl ToolResponse { } } -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "lowercase")] -pub enum QueryContext { - Home, - Profile, - Any, -} - /// Called by dave when he wants to display notes on the screen #[derive(Debug, Deserialize, Serialize, Clone)] pub struct PresentNotesCall { @@ -342,12 +333,12 @@ impl PresentNotesCall { /// The parsed nostrdb query that dave wants to use to satisfy a request #[derive(Debug, Deserialize, Serialize, Clone)] pub struct QueryCall { - context: Option<QueryContext>, - limit: Option<u64>, - since: Option<u64>, - kind: Option<u64>, - until: Option<u64>, - search: Option<String>, + pub author: Option<Pubkey>, + pub limit: Option<u64>, + pub since: Option<u64>, + pub kind: Option<u64>, + pub until: Option<u64>, + pub search: Option<String>, } fn is_reply(note: Note) -> bool { @@ -379,6 +370,10 @@ impl QueryCall { .custom(|n| !is_reply(n)) .kinds([self.kind.unwrap_or(1)]); + if let Some(author) = &self.author { + filter = filter.authors([author.bytes()]); + } + if let Some(search) = &self.search { filter = filter.search(search); } @@ -398,12 +393,20 @@ impl QueryCall { self.limit.unwrap_or(10) } - pub fn search(&self) -> Option<&str> { - self.search.as_deref() + pub fn author(&self) -> Option<&Pubkey> { + self.author.as_ref() + } + + pub fn since(&self) -> Option<u64> { + self.since + } + + pub fn until(&self) -> Option<u64> { + self.until } - pub fn context(&self) -> QueryContext { - self.context.clone().unwrap_or(QueryContext::Any) + pub fn search(&self) -> Option<&str> { + self.search.as_deref() } pub fn execute(&self, txn: &Transaction, ndb: &Ndb) -> QueryResponse { @@ -414,10 +417,7 @@ impl QueryCall { vec![] } }; - QueryResponse { - context: self.context.clone().unwrap_or(QueryContext::Any), - notes, - } + QueryResponse { notes } } pub fn parse(args: &str) -> Result<ToolCalls, ToolCallError> { @@ -557,6 +557,15 @@ fn query_tool() -> Tool { }, ToolArg { + name: "author", + typ: ArgType::String, + required: false, + default: None, + description: "An author *pubkey* to constrain the query on. Can be used to search for notes from individual users. If unsure what pubkey to u +se, you can query for kind 0 profiles with the search argument.", + }, + + ToolArg { name: "kind", typ: ArgType::Number, required: false, diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -1,11 +1,11 @@ use crate::{ messages::Message, - tools::{PresentNotesCall, QueryCall, QueryContext, ToolCall, ToolCalls, ToolResponse}, + tools::{PresentNotesCall, QueryCall, ToolCall, ToolCalls, ToolResponse}, }; use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers}; -use nostrdb::Transaction; +use nostrdb::{Ndb, Transaction}; use notedeck::{AppContext, NoteContext}; -use notedeck_ui::{icons::search_icon, NoteOptions}; +use notedeck_ui::{icons::search_icon, NoteOptions, ProfilePic}; /// DaveUi holds all of the data it needs to render itself pub struct DaveUi<'a> { @@ -153,21 +153,11 @@ impl<'a> DaveUi<'a> { //ui.label(format!("tool_response: {:?}", tool_response)); } - fn search_call_ui(query_call: &QueryCall, ui: &mut egui::Ui) { + fn search_call_ui(ctx: &mut AppContext, query_call: &QueryCall, ui: &mut egui::Ui) { ui.add(search_icon(16.0, 16.0)); ui.add_space(8.0); - let context = match query_call.context() { - QueryContext::Profile => "profile ", - QueryContext::Any => "", - QueryContext::Home => "home ", - }; - //TODO: fix this to support any query - if let Some(search) = query_call.search() { - ui.label(format!("Querying {context}for '{search}'")); - } else { - ui.label(format!("Querying {:?}", &query_call)); - } + query_call_ui(ctx.img_cache, ctx.ndb, query_call, ui); } /// The ai has asked us to render some notes, so we do that here @@ -215,15 +205,13 @@ impl<'a> DaveUi<'a> { match call.calls() { ToolCalls::PresentNotes(call) => Self::present_notes_ui(ctx, call, ui), ToolCalls::Query(search_call) => { - ui.horizontal(|ui| { - egui::Frame::new() - .inner_margin(10.0) - .corner_radius(10.0) - .fill(ui.visuals().widgets.inactive.weak_bg_fill) - .show(ui, |ui| { - Self::search_call_ui(search_call, ui); - }) - }); + ui.allocate_ui_with_layout( + egui::vec2(ui.available_size().x, 32.0), + Layout::left_to_right(Align::Center), + |ui| { + Self::search_call_ui(ctx, search_call, ui); + }, + ); } } } @@ -303,3 +291,81 @@ fn new_chat_button() -> impl egui::Widget { helper.take_animation_response() } } + +fn query_call_ui(cache: &mut notedeck::Images, ndb: &Ndb, query: &QueryCall, ui: &mut egui::Ui) { + ui.spacing_mut().item_spacing.x = 8.0; + if let Some(pubkey) = query.author() { + let txn = Transaction::new(ndb).unwrap(); + pill_label_ui( + "author", + move |ui| { + ui.add( + ProfilePic::from_profile_or_default( + cache, + ndb.get_profile_by_pubkey(&txn, pubkey.bytes()) + .ok() + .as_ref(), + ) + .size(ProfilePic::small_size() as f32), + ); + }, + ui, + ); + } + + if let Some(limit) = query.limit { + pill_label("limit", &limit.to_string(), ui); + } + + if let Some(since) = query.since { + pill_label("since", &since.to_string(), ui); + } + + if let Some(kind) = query.kind { + pill_label("kind", &kind.to_string(), ui); + } + + if let Some(until) = query.until { + pill_label("until", &until.to_string(), ui); + } + + if let Some(search) = query.search.as_ref() { + pill_label("search", search, ui); + } +} + +fn pill_label(name: &str, value: &str, ui: &mut egui::Ui) { + pill_label_ui( + name, + move |ui| { + ui.label(value); + }, + ui, + ); +} + +fn pill_label_ui(name: &str, mut value: impl FnMut(&mut egui::Ui), ui: &mut egui::Ui) { + egui::Frame::new() + .fill(ui.visuals().noninteractive().bg_fill) + .inner_margin(egui::Margin::same(4)) + .corner_radius(egui::CornerRadius::same(10)) + .stroke(egui::Stroke::new( + 1.0, + ui.visuals().noninteractive().bg_stroke.color, + )) + .show(ui, |ui| { + egui::Frame::new() + .fill(ui.visuals().noninteractive().weak_bg_fill) + .inner_margin(egui::Margin::same(4)) + .corner_radius(egui::CornerRadius::same(10)) + .stroke(egui::Stroke::new( + 1.0, + ui.visuals().noninteractive().bg_stroke.color, + )) + .show(ui, |ui| { + ui.label(name); + }); + + value(ui); + }); +}