feat: live auto-updating dashboard in /watch with top stealth and new models
This commit is contained in:
parent
7425895931
commit
bd3c655a80
4 changed files with 274 additions and 22 deletions
|
|
@ -94,6 +94,14 @@ impl Model {
|
||||||
}
|
}
|
||||||
self.pricing.values().all(|value| is_decimal_zero(value))
|
self.pricing.values().all(|value| is_decimal_zero(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_stealth(&self) -> bool {
|
||||||
|
let id_lower = self.id.to_lowercase();
|
||||||
|
let name_lower = self.name.to_lowercase();
|
||||||
|
id_lower.starts_with("stealth/")
|
||||||
|
|| id_lower.contains("stealth")
|
||||||
|
|| name_lower.contains("stealth")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn validate(catalog: &BTreeMap<String, Model>) -> Result<(), Error> {
|
pub(crate) fn validate(catalog: &BTreeMap<String, Model>) -> Result<(), Error> {
|
||||||
|
|
@ -253,6 +261,16 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stealth_detection_test() {
|
||||||
|
assert!(model("stealth/space-bunny", &[]).is_stealth());
|
||||||
|
assert!(model("vendor/model-stealth-preview", &[]).is_stealth());
|
||||||
|
let mut named = model("vendor/xyz", &[]);
|
||||||
|
named.name = "Stealth Model V1".into();
|
||||||
|
assert!(named.is_stealth());
|
||||||
|
assert!(!model("meta-llama/llama-3", &[]).is_stealth());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn changes_detects_new_and_paid_to_free() {
|
fn changes_detects_new_and_paid_to_free() {
|
||||||
let prev = map(vec![
|
let prev = map(vec![
|
||||||
|
|
|
||||||
111
src/main.rs
111
src/main.rs
|
|
@ -339,8 +339,12 @@ fn patch_subscription(
|
||||||
min_context: 0,
|
min_context: 0,
|
||||||
ping_enabled: false,
|
ping_enabled: false,
|
||||||
compact: false,
|
compact: false,
|
||||||
|
message_id: None,
|
||||||
});
|
});
|
||||||
sub.channel_id = channel_id;
|
if sub.channel_id != channel_id {
|
||||||
|
sub.channel_id = channel_id;
|
||||||
|
sub.message_id = None;
|
||||||
|
}
|
||||||
if patch.clear_role {
|
if patch.clear_role {
|
||||||
sub.role_id = None;
|
sub.role_id = None;
|
||||||
sub.ping_enabled = false;
|
sub.ping_enabled = false;
|
||||||
|
|
@ -490,7 +494,7 @@ async fn watch(
|
||||||
}
|
}
|
||||||
let roles = guild.roles(ctx.serenity_context()).await?;
|
let roles = guild.roles(ctx.serenity_context()).await?;
|
||||||
let mut guard = ctx.data().store.lock().await;
|
let mut guard = ctx.data().store.lock().await;
|
||||||
let sub = match patch_subscription(
|
let mut sub = match patch_subscription(
|
||||||
guard.state.subscriptions.get(&guild.get()),
|
guard.state.subscriptions.get(&guild.get()),
|
||||||
channel.id.get(),
|
channel.id.get(),
|
||||||
&patch,
|
&patch,
|
||||||
|
|
@ -514,8 +518,40 @@ async fn watch(
|
||||||
drop(guard);
|
drop(guard);
|
||||||
return reply(ctx, embed("Invalid role").description("The configured role is missing. Set ping_role to a valid role or @everyone, or use ping_enabled:false or clear_role:true.")).await;
|
return reply(ctx, embed("Invalid role").description("The configured role is missing. Set ping_role to a valid role or @everyone, or use ping_enabled:false or clear_role:true.")).await;
|
||||||
}
|
}
|
||||||
let result = embed("Alerts configured here")
|
|
||||||
.description(WATCH_HELP)
|
// Post or update the public live dashboard in the channel
|
||||||
|
let dashboard_embed = presentation::live_dashboard(&guard.state, &sub, ctx.data().poll_seconds);
|
||||||
|
let mut updated_msg_id = None;
|
||||||
|
if let Some(existing_msg_id) = sub.message_id
|
||||||
|
&& channel
|
||||||
|
.id
|
||||||
|
.edit_message(
|
||||||
|
ctx.serenity_context(),
|
||||||
|
serenity::MessageId::new(existing_msg_id),
|
||||||
|
serenity::EditMessage::new().embed(dashboard_embed.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
updated_msg_id = Some(existing_msg_id);
|
||||||
|
}
|
||||||
|
if updated_msg_id.is_none()
|
||||||
|
&& let Ok(sent) = channel
|
||||||
|
.id
|
||||||
|
.send_message(
|
||||||
|
ctx.serenity_context(),
|
||||||
|
serenity::CreateMessage::new()
|
||||||
|
.embed(dashboard_embed)
|
||||||
|
.allowed_mentions(no_mentions()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
updated_msg_id = Some(sent.id.get());
|
||||||
|
}
|
||||||
|
sub.message_id = updated_msg_id;
|
||||||
|
|
||||||
|
let result = embed("Live Radar Dashboard & Alerts Configured")
|
||||||
|
.description("Posted the live auto-updating dashboard in this channel. It will automatically refresh at every poll interval.\n\n".to_owned() + WATCH_HELP)
|
||||||
.field(
|
.field(
|
||||||
"Complete settings",
|
"Complete settings",
|
||||||
settings(&sub, guild.get(), ctx.data().poll_seconds),
|
settings(&sub, guild.get(), ctx.data().poll_seconds),
|
||||||
|
|
@ -787,7 +823,11 @@ fn acknowledge(state: &mut State, guild: u64, snapshot: &Subscription, count: us
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn deliver(store: &Mutex<Store>, http: &serenity::Http) -> Result<(), Error> {
|
async fn deliver(
|
||||||
|
store: &Mutex<Store>,
|
||||||
|
http: &serenity::Http,
|
||||||
|
poll_seconds: u64,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let guilds: Vec<_> = store
|
let guilds: Vec<_> = store
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
|
|
@ -797,26 +837,53 @@ async fn deliver(store: &Mutex<Store>, http: &serenity::Http) -> Result<(), Erro
|
||||||
.copied()
|
.copied()
|
||||||
.collect();
|
.collect();
|
||||||
for guild in guilds {
|
for guild in guilds {
|
||||||
let snapshot = store.lock().await.state.subscriptions.get(&guild).cloned();
|
let (snapshot, state) = {
|
||||||
|
let guard = store.lock().await;
|
||||||
|
(
|
||||||
|
guard.state.subscriptions.get(&guild).cloned(),
|
||||||
|
guard.state.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
let Some(sub) = snapshot else {
|
let Some(sub) = snapshot else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if sub.pending.is_empty() {
|
|
||||||
continue;
|
let channel = serenity::ChannelId::new(sub.channel_id);
|
||||||
|
|
||||||
|
// 1. Auto-update the live dashboard embed if message_id exists
|
||||||
|
if let Some(msg_id) = sub.message_id {
|
||||||
|
let dashboard = presentation::live_dashboard(&state, &sub, poll_seconds);
|
||||||
|
let edit_res = channel
|
||||||
|
.edit_message(
|
||||||
|
http,
|
||||||
|
serenity::MessageId::new(msg_id),
|
||||||
|
serenity::EditMessage::new().embed(dashboard),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if edit_res.is_err() {
|
||||||
|
tracing::info!(
|
||||||
|
guild,
|
||||||
|
"Live dashboard message edit failed (may have been deleted or moved)"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let count = sub.pending.len().min(8);
|
|
||||||
if serenity::ChannelId::new(sub.channel_id)
|
// 2. Deliver new model drop notification role pings if any are pending
|
||||||
.send_message(http, notification_message(&sub, guild, count))
|
if !sub.pending.is_empty() {
|
||||||
.await
|
let count = sub.pending.len().min(8);
|
||||||
.is_err()
|
if channel
|
||||||
{
|
.send_message(http, notification_message(&sub, guild, count))
|
||||||
tracing::warn!(guild, "Alert delivery failed; retained for next poll");
|
.await
|
||||||
continue;
|
.is_err()
|
||||||
}
|
{
|
||||||
let mut guard = store.lock().await;
|
tracing::warn!(guild, "Alert delivery failed; retained for next poll");
|
||||||
let mut next = guard.state.clone();
|
continue;
|
||||||
if acknowledge(&mut next, guild, &sub, count) {
|
}
|
||||||
guard.commit(next).await?;
|
let mut guard = store.lock().await;
|
||||||
|
let mut next = guard.state.clone();
|
||||||
|
if acknowledge(&mut next, guild, &sub, count) {
|
||||||
|
guard.commit(next).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -973,7 +1040,7 @@ async fn main() -> Result<(), Error> {
|
||||||
if let Err(error) = refresh(&poll_store, &http_client).await {
|
if let Err(error) = refresh(&poll_store, &http_client).await {
|
||||||
tracing::warn!(%error, "Catalog refresh failed; preserving previous snapshot");
|
tracing::warn!(%error, "Catalog refresh failed; preserving previous snapshot");
|
||||||
}
|
}
|
||||||
if let Err(error) = deliver(&poll_store, &http).await {
|
if let Err(error) = deliver(&poll_store, &http, poll_seconds).await {
|
||||||
tracing::warn!(%error, "Saving delivery progress failed; alerts may retry");
|
tracing::warn!(%error, "Saving delivery progress failed; alerts may retry");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,112 @@ pub fn snapshot(state: &State, options: &Browse, poll_seconds: u64) -> serenity:
|
||||||
.field("Poll interval", format!("{formatted_poll} seconds · run /status again to refresh this snapshot"), false)
|
.field("Poll interval", format!("{formatted_poll} seconds · run /status again to refresh this snapshot"), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn live_dashboard(
|
||||||
|
state: &State,
|
||||||
|
sub: &crate::store::Subscription,
|
||||||
|
poll_seconds: u64,
|
||||||
|
) -> serenity::CreateEmbed {
|
||||||
|
let freshness = state
|
||||||
|
.last_success
|
||||||
|
.map(|time| {
|
||||||
|
format!(
|
||||||
|
"Synced <t:{time}:R> (<t:{time}:f>). Auto-updates every {}s.",
|
||||||
|
format_number(poll_seconds)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "Waiting for initial catalog sync...".into());
|
||||||
|
|
||||||
|
let mut result = embed("Model Radar · Live Dashboard").description(freshness);
|
||||||
|
|
||||||
|
// 1. 🔥 New & Recently Dropped Models (at the top)
|
||||||
|
let recent_drops: Vec<_> = sub
|
||||||
|
.pending
|
||||||
|
.iter()
|
||||||
|
.take(if sub.compact { 6 } else { 3 })
|
||||||
|
.map(|event| {
|
||||||
|
format!(
|
||||||
|
"**{}** · {}",
|
||||||
|
if event.kind == "paid_to_free" {
|
||||||
|
"Now free"
|
||||||
|
} else {
|
||||||
|
"New in catalog"
|
||||||
|
},
|
||||||
|
model_line(&event.model, sub.compact)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !recent_drops.is_empty() {
|
||||||
|
let text_block = recent_drops.join("\n\n");
|
||||||
|
result = result.field("🔥 New & Recent Drops", text(&text_block, 1024), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 🕵️ Stealth & Experimental Models (First page section)
|
||||||
|
let stealth_models: Vec<_> = state
|
||||||
|
.catalog
|
||||||
|
.values()
|
||||||
|
.filter(|m| m.is_stealth() && (!sub.free_only || m.is_free()))
|
||||||
|
.take(if sub.compact { 5 } else { 3 })
|
||||||
|
.map(|m| model_line(m, sub.compact))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let stealth_text = if stealth_models.is_empty() {
|
||||||
|
"No stealth models currently active in catalog.".to_string()
|
||||||
|
} else {
|
||||||
|
stealth_models.join("\n\n")
|
||||||
|
};
|
||||||
|
result = result.field(
|
||||||
|
"🕵️ Stealth & Experimental Models",
|
||||||
|
text(&stealth_text, 1024),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. ✨ Active Models Snapshot
|
||||||
|
let active_models: Vec<_> = state
|
||||||
|
.catalog
|
||||||
|
.values()
|
||||||
|
.filter(|m| sub.matches(m))
|
||||||
|
.take(if sub.compact { 6 } else { 4 })
|
||||||
|
.map(|m| model_line(m, sub.compact))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let active_text = if active_models.is_empty() {
|
||||||
|
"No matching models found with current filters.".to_string()
|
||||||
|
} else {
|
||||||
|
active_models.join("\n\n")
|
||||||
|
};
|
||||||
|
result = result.field("✨ Active Models Snapshot", text(&active_text, 1024), false);
|
||||||
|
|
||||||
|
// 4. Overview & Filters
|
||||||
|
let total_count = format_number(state.catalog.len());
|
||||||
|
let free_count = format_number(state.catalog.values().filter(|m| m.is_free()).count());
|
||||||
|
let stealth_count = format_number(state.catalog.values().filter(|m| m.is_stealth()).count());
|
||||||
|
result = result.field(
|
||||||
|
"Overview",
|
||||||
|
format!("{total_count} total · {free_count} free · {stealth_count} stealth"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
let scope_text = if sub.free_only {
|
||||||
|
"Free only"
|
||||||
|
} else {
|
||||||
|
"All models"
|
||||||
|
};
|
||||||
|
let search_desc = if sub.search.is_empty() {
|
||||||
|
"None"
|
||||||
|
} else {
|
||||||
|
&sub.search
|
||||||
|
};
|
||||||
|
let min_ctx = format_number(sub.min_context);
|
||||||
|
result = result.field(
|
||||||
|
"Active Filters",
|
||||||
|
format!("Scope: {scope_text}\nSearch: {search_desc}\nMin Context: {min_ctx} tokens"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
fn format_price_usd(val: f64) -> String {
|
fn format_price_usd(val: f64) -> String {
|
||||||
if val == 0.0 {
|
if val == 0.0 {
|
||||||
return "$0.00".into();
|
return "$0.00".into();
|
||||||
|
|
@ -498,4 +604,61 @@ pub mod tests {
|
||||||
"tiered pricing"
|
"tiered pricing"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn live_dashboard_includes_stealth_and_new_models() {
|
||||||
|
let state = State {
|
||||||
|
catalog: [
|
||||||
|
model("stealth/space-bunny", true, 32000),
|
||||||
|
model("qwen/qwen-2.5", true, 64000),
|
||||||
|
model("meta-llama/llama-3", false, 8000),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| (m.id.clone(), m))
|
||||||
|
.collect(),
|
||||||
|
last_success: Some(12345),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let sub = crate::store::Subscription {
|
||||||
|
channel_id: 1,
|
||||||
|
role_id: None,
|
||||||
|
free_only: true,
|
||||||
|
search: String::new(),
|
||||||
|
min_context: 0,
|
||||||
|
ping_enabled: false,
|
||||||
|
compact: false,
|
||||||
|
message_id: None,
|
||||||
|
pending: vec![crate::catalog::Event {
|
||||||
|
model: model("qwen/qwen-2.5", true, 64000),
|
||||||
|
kind: "paid_to_free".into(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let embed = live_dashboard(&state, &sub, 600);
|
||||||
|
let value = serde_json::to_value(&embed).unwrap();
|
||||||
|
assert_embed_bounds(&value);
|
||||||
|
|
||||||
|
// Verify "🔥 New & Recent Drops" at top
|
||||||
|
assert_eq!(value["fields"][0]["name"], "🔥 New & Recent Drops");
|
||||||
|
assert!(
|
||||||
|
value["fields"][0]["value"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
.contains("Now free")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify "🕵️ Stealth & Experimental Models" section
|
||||||
|
assert_eq!(
|
||||||
|
value["fields"][1]["name"],
|
||||||
|
"🕵️ Stealth & Experimental Models"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
value["fields"][1]["value"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
.contains("stealth/space")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify "✨ Active Models Snapshot"
|
||||||
|
assert_eq!(value["fields"][2]["name"], "✨ Active Models Snapshot");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ pub struct Subscription {
|
||||||
pub ping_enabled: bool,
|
pub ping_enabled: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub compact: bool,
|
pub compact: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub message_id: Option<u64>,
|
||||||
pub pending: Vec<Event>,
|
pub pending: Vec<Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,6 +185,7 @@ mod tests {
|
||||||
min_context: 8192,
|
min_context: 8192,
|
||||||
ping_enabled: false,
|
ping_enabled: false,
|
||||||
compact: true,
|
compact: true,
|
||||||
|
message_id: None,
|
||||||
pending: vec![Event {
|
pending: vec![Event {
|
||||||
model,
|
model,
|
||||||
kind: "new".into(),
|
kind: "new".into(),
|
||||||
|
|
@ -199,6 +202,7 @@ mod tests {
|
||||||
min_context: 0,
|
min_context: 0,
|
||||||
ping_enabled: true,
|
ping_enabled: true,
|
||||||
compact: false,
|
compact: false,
|
||||||
|
message_id: None,
|
||||||
pending: Vec::new(),
|
pending: Vec::new(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue