diff --git a/assets/favicon.ico b/assets/favicon.ico deleted file mode 100644 index eed0c09..0000000 Binary files a/assets/favicon.ico and /dev/null differ diff --git a/assets/favicon.svg b/assets/favicon.svg new file mode 100644 index 0000000..ac16408 --- /dev/null +++ b/assets/favicon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/sw.js b/assets/sw.js index f5db1a2..dcaaab7 100644 --- a/assets/sw.js +++ b/assets/sw.js @@ -12,7 +12,7 @@ self.addEventListener("install", (event) => { "/", "/dashboard", "/assets/logo.svg", - "/assets/favicon.ico", + "/assets/favicon.svg", ]) ) ); diff --git a/assets/tailwind.css b/assets/tailwind.css index d30c32c..7177b28 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -1733,6 +1733,9 @@ .text-center { text-align: center; } + .lowercase { + text-transform: lowercase; + } .outline { outline-style: var(--tw-outline-style); outline-width: 1px; diff --git a/src/app.rs b/src/app.rs index 8c982cd..8964acc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -49,7 +49,7 @@ pub enum Route { Login { redirect_url: String }, } -const FAVICON: Asset = asset!("/assets/favicon.ico"); +const FAVICON: Asset = asset!("/assets/favicon.svg"); const MAIN_CSS: Asset = asset!("/assets/main.css"); const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css"); const MANIFEST: Asset = asset!("/assets/manifest.json"); diff --git a/src/infrastructure/llm.rs b/src/infrastructure/llm.rs index 46955c8..5addf44 100644 --- a/src/infrastructure/llm.rs +++ b/src/infrastructure/llm.rs @@ -159,7 +159,7 @@ mod inner { /// # Errors /// /// Returns `ServerFnError` if the Ollama request fails or response parsing fails -#[server(endpoint = "/api/summarize")] +#[post("/api/summarize")] pub async fn summarize_article( snippet: String, article_url: String, @@ -258,7 +258,7 @@ pub struct FollowUpMessage { /// # Errors /// /// Returns `ServerFnError` if the Ollama request fails or response parsing fails -#[server(endpoint = "/api/chat")] +#[post("/api/chat")] pub async fn chat_followup( messages: Vec, ollama_url: String, diff --git a/src/infrastructure/ollama.rs b/src/infrastructure/ollama.rs index 1b60964..5ef0449 100644 --- a/src/infrastructure/ollama.rs +++ b/src/infrastructure/ollama.rs @@ -45,7 +45,7 @@ struct OllamaModel { /// /// Returns `ServerFnError` only on serialization issues; network failures /// are caught and returned as `online: false` -#[server(endpoint = "/api/ollama-status")] +#[post("/api/ollama-status")] pub async fn get_ollama_status(ollama_url: String) -> Result { dotenvy::dotenv().ok(); diff --git a/src/infrastructure/searxng.rs b/src/infrastructure/searxng.rs index d5d2322..67e6274 100644 --- a/src/infrastructure/searxng.rs +++ b/src/infrastructure/searxng.rs @@ -110,7 +110,7 @@ mod inner { /// # Errors /// /// Returns `ServerFnError` if the SearXNG request fails or response parsing fails -#[server(endpoint = "/api/search")] +#[post("/api/search")] pub async fn search_topic(query: String) -> Result, ServerFnError> { dotenvy::dotenv().ok(); use inner::{extract_source, rank_and_deduplicate, SearxngResponse}; @@ -122,23 +122,21 @@ pub async fn search_topic(query: String) -> Result, ServerFnError> // similar to how Perplexity reformulates queries before searching. let enriched_query = format!("{query} latest news"); - // Build URL with query parameters using the url crate's encoder - // to avoid reqwest version conflicts between our dep and dioxus's. - // Key SearXNG params: - // categories=news,general - prioritize news sources + supplement with general - // time_range=month - only recent results (last 30 days) - // language=en - English results - // format=json - machine-readable output - let encoded_query: String = - url::form_urlencoded::byte_serialize(enriched_query.as_bytes()).collect(); - let search_url = format!( - "{searxng_url}/search?q={encoded_query}&format=json&language=en\ - &categories=news,general&time_range=month" - ); + // Use POST with form-encoded body because SearXNG's default config + // sets `method: "POST"` which rejects GET requests with 405. + let search_url = format!("{searxng_url}/search"); + let params = [ + ("q", enriched_query.as_str()), + ("format", "json"), + ("language", "en"), + ("categories", "news,general"), + ("time_range", "month"), + ]; let client = reqwest::Client::new(); let resp = client - .get(&search_url) + .post(&search_url) + .form(¶ms) .send() .await .map_err(|e| ServerFnError::new(format!("SearXNG request failed: {e}")))?; @@ -198,7 +196,7 @@ pub async fn search_topic(query: String) -> Result, ServerFnError> /// # Errors /// /// Returns `ServerFnError` if the SearXNG search request fails -#[server(endpoint = "/api/trending")] +#[get("/api/trending")] pub async fn get_trending_topics() -> Result, ServerFnError> { dotenvy::dotenv().ok(); use inner::SearxngResponse; @@ -207,12 +205,15 @@ pub async fn get_trending_topics() -> Result, ServerFnError> { let searxng_url = std::env::var("SEARXNG_URL").unwrap_or_else(|_| "http://localhost:8888".into()); - let encoded_query: String = - url::form_urlencoded::byte_serialize(b"trending technology AI").collect(); - let search_url = format!( - "{searxng_url}/search?q={encoded_query}&format=json&language=en\ - &categories=news&time_range=week" - ); + // Use POST to match SearXNG's default `method: "POST"` setting + let search_url = format!("{searxng_url}/search"); + let params = [ + ("q", "trending technology AI"), + ("format", "json"), + ("language", "en"), + ("categories", "news"), + ("time_range", "week"), + ]; let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(5)) @@ -220,7 +221,8 @@ pub async fn get_trending_topics() -> Result, ServerFnError> { .map_err(|e| ServerFnError::new(format!("HTTP client error: {e}")))?; let resp = client - .get(&search_url) + .post(&search_url) + .form(¶ms) .send() .await .map_err(|e| ServerFnError::new(format!("SearXNG trending search failed: {e}")))?; diff --git a/src/pages/dashboard.rs b/src/pages/dashboard.rs index f3d3a1b..b6520cb 100644 --- a/src/pages/dashboard.rs +++ b/src/pages/dashboard.rs @@ -329,7 +329,7 @@ pub fn DashboardPage() -> Element { .set( format!( "Article content:\n{snippet}\n\n\ - AI Summary:\n{text}", + AI Summary:\n{text}", ), ); summary.set(Some(text)); @@ -385,9 +385,9 @@ pub fn DashboardPage() -> Element { role: "system".into(), content: format!( "You are a helpful assistant. The user is reading \ - a news article. Use the following context to answer \ - their questions. Do NOT comment on the source, \ - dates, URLs, or formatting.\n\n{ctx}", + a news article. Use the following context to answer \ + their questions. Do NOT comment on the source, \ + dates, URLs, or formatting.\n\n{ctx}", ), }, ];