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))
|
||||
}
|
||||
|
||||
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> {
|
||||
|
|
@ -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]
|
||||
fn changes_detects_new_and_paid_to_free() {
|
||||
let prev = map(vec![
|
||||
|
|
|
|||
111
src/main.rs
111
src/main.rs
|
|
@ -339,8 +339,12 @@ fn patch_subscription(
|
|||
min_context: 0,
|
||||
ping_enabled: 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 {
|
||||
sub.role_id = None;
|
||||
sub.ping_enabled = false;
|
||||
|
|
@ -490,7 +494,7 @@ async fn watch(
|
|||
}
|
||||
let roles = guild.roles(ctx.serenity_context()).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()),
|
||||
channel.id.get(),
|
||||
&patch,
|
||||
|
|
@ -514,8 +518,40 @@ async fn watch(
|
|||
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;
|
||||
}
|
||||
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(
|
||||
"Complete settings",
|
||||
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
|
||||
.lock()
|
||||
.await
|
||||
|
|
@ -797,26 +837,53 @@ async fn deliver(store: &Mutex<Store>, http: &serenity::Http) -> Result<(), Erro
|
|||
.copied()
|
||||
.collect();
|
||||
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 {
|
||||
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)
|
||||
.send_message(http, notification_message(&sub, guild, count))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!(guild, "Alert delivery failed; retained for next poll");
|
||||
continue;
|
||||
}
|
||||
let mut guard = store.lock().await;
|
||||
let mut next = guard.state.clone();
|
||||
if acknowledge(&mut next, guild, &sub, count) {
|
||||
guard.commit(next).await?;
|
||||
|
||||
// 2. Deliver new model drop notification role pings if any are pending
|
||||
if !sub.pending.is_empty() {
|
||||
let count = sub.pending.len().min(8);
|
||||
if channel
|
||||
.send_message(http, notification_message(&sub, guild, count))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!(guild, "Alert delivery failed; retained for next poll");
|
||||
continue;
|
||||
}
|
||||
let mut guard = store.lock().await;
|
||||
let mut next = guard.state.clone();
|
||||
if acknowledge(&mut next, guild, &sub, count) {
|
||||
guard.commit(next).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -973,7 +1040,7 @@ async fn main() -> Result<(), Error> {
|
|||
if let Err(error) = refresh(&poll_store, &http_client).await {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
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 {
|
||||
if val == 0.0 {
|
||||
return "$0.00".into();
|
||||
|
|
@ -498,4 +604,61 @@ pub mod tests {
|
|||
"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,
|
||||
#[serde(default)]
|
||||
pub compact: bool,
|
||||
#[serde(default)]
|
||||
pub message_id: Option<u64>,
|
||||
pub pending: Vec<Event>,
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +185,7 @@ mod tests {
|
|||
min_context: 8192,
|
||||
ping_enabled: false,
|
||||
compact: true,
|
||||
message_id: None,
|
||||
pending: vec![Event {
|
||||
model,
|
||||
kind: "new".into(),
|
||||
|
|
@ -199,6 +202,7 @@ mod tests {
|
|||
min_context: 0,
|
||||
ping_enabled: true,
|
||||
compact: false,
|
||||
message_id: None,
|
||||
pending: Vec::new(),
|
||||
},
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue