use dioxus::prelude::*; use serde::{Deserialize, Serialize}; /// Article shape sent to the browser — a trimmed view of /// `feedsignal_core::Article`, kept separate so the wire format can evolve /// independently of the storage model. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ArticleView { pub id: String, pub title: String, pub url: String, pub summary: String, pub topics: Vec, pub final_score: Option, } #[component] pub fn App() -> Element { let articles = use_server_future(list_ranked_articles)?; rsx! { style { {include_str!("../assets/app.css")} } main { h1 { "feedsignal" } match articles.read().as_ref() { Some(Ok(articles)) => rsx! { ul { class: "article-list", for article in articles.iter() { ArticleRow { article: article.clone() } } } }, Some(Err(err)) => rsx! { p { class: "error", "Failed to load articles: {err}" } }, None => rsx! { p { "Loading..." } }, } } } } #[component] fn ArticleRow(article: ArticleView) -> Element { let score_pct = article.final_score.map(|s| (s * 100.0).round() as i32); rsx! { li { class: "article-row", a { href: "{article.url}", target: "_blank", "{article.title}" } if let Some(pct) = score_pct { span { class: "score", "{pct}%" } } p { class: "summary", "{article.summary}" } div { class: "topics", for topic in article.topics.iter() { span { class: "topic-tag", "{topic}" } } } button { onclick: move |_| { let id = article.id.clone(); async move { let _ = mark_dismissed(id).await; } }, "Not relevant" } } } } /// Returns articles ranked by `final_score` descending, highest-relevance /// first. Runs on the server (native, has DB access); the `#[server]` /// macro generates the HTTP call the browser/WASM build uses instead. #[server] async fn list_ranked_articles() -> Result, ServerFnError> { let db = crate::server::db().await?; crate::server::list_ranked_articles_impl(db) .await .map_err(|e| ServerFnError::new(e.to_string())) } /// Records an explicit "not relevant" signal, which feeds directly into the /// topic-affinity update (see `feedsignal_core::affinity::apply_feedback`). #[server] async fn mark_dismissed(article_id: String) -> Result<(), ServerFnError> { let db = crate::server::db().await?; crate::server::mark_dismissed_impl(db, article_id) .await .map_err(|e| ServerFnError::new(e.to_string())) }