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:
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);
+ });
+}