Integrate SearXNG news search, Ollama-powered article summarization with follow-up chat, and a dashboard sidebar showing LLM status, trending keywords, and recent search history. Sidebar yields to a split-view article detail panel when a card is selected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
113 lines
4.0 KiB
Rust
113 lines
4.0 KiB
Rust
use dioxus::prelude::*;
|
|
|
|
use crate::infrastructure::ollama::{get_ollama_status, OllamaStatus};
|
|
|
|
/// Right sidebar for the dashboard, showing Ollama status, trending topics,
|
|
/// and recent search history.
|
|
///
|
|
/// Appears when no article card is selected. Disappears when the user opens
|
|
/// the article detail split view.
|
|
///
|
|
/// # Props
|
|
///
|
|
/// * `ollama_url` - Ollama instance URL for status polling
|
|
/// * `trending` - Trending topic keywords extracted from recent news headlines
|
|
/// * `recent_searches` - Recent search topics stored in localStorage
|
|
/// * `on_topic_click` - Fires when a trending or recent topic is clicked
|
|
#[component]
|
|
pub fn DashboardSidebar(
|
|
ollama_url: String,
|
|
trending: Vec<String>,
|
|
recent_searches: Vec<String>,
|
|
on_topic_click: EventHandler<String>,
|
|
) -> Element {
|
|
// Fetch Ollama status once on mount.
|
|
// use_resource with no signal dependencies runs exactly once and
|
|
// won't re-fire on parent re-renders (unlike use_effect).
|
|
let url = ollama_url.clone();
|
|
let status_resource = use_resource(move || {
|
|
let u = url.clone();
|
|
async move {
|
|
get_ollama_status(u).await.unwrap_or(OllamaStatus {
|
|
online: false,
|
|
models: Vec::new(),
|
|
})
|
|
}
|
|
});
|
|
|
|
let current_status: OllamaStatus =
|
|
status_resource
|
|
.read()
|
|
.as_ref()
|
|
.cloned()
|
|
.unwrap_or(OllamaStatus {
|
|
online: false,
|
|
models: Vec::new(),
|
|
});
|
|
|
|
rsx! {
|
|
aside { class: "dashboard-sidebar",
|
|
|
|
// -- Ollama Status Section --
|
|
div { class: "sidebar-section",
|
|
h4 { class: "sidebar-section-title", "Ollama Status" }
|
|
div { class: "sidebar-status-row",
|
|
span { class: if current_status.online { "sidebar-status-dot sidebar-status-dot--online" } else { "sidebar-status-dot sidebar-status-dot--offline" } }
|
|
span { class: "sidebar-status-label",
|
|
if current_status.online {
|
|
"Online"
|
|
} else {
|
|
"Offline"
|
|
}
|
|
}
|
|
}
|
|
if !current_status.models.is_empty() {
|
|
div { class: "sidebar-model-list",
|
|
for model in current_status.models.iter() {
|
|
span { class: "sidebar-model-tag", "{model}" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- Trending Topics Section --
|
|
if !trending.is_empty() {
|
|
div { class: "sidebar-section",
|
|
h4 { class: "sidebar-section-title", "Trending" }
|
|
for topic in trending.iter() {
|
|
{
|
|
let t = topic.clone();
|
|
rsx! {
|
|
button {
|
|
class: "sidebar-topic-link",
|
|
onclick: move |_| on_topic_click.call(t.clone()),
|
|
"{topic}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- Recent Searches Section --
|
|
if !recent_searches.is_empty() {
|
|
div { class: "sidebar-section",
|
|
h4 { class: "sidebar-section-title", "Recent Searches" }
|
|
for search in recent_searches.iter() {
|
|
{
|
|
let s = search.clone();
|
|
rsx! {
|
|
button {
|
|
class: "sidebar-topic-link",
|
|
onclick: move |_| on_topic_click.call(s.clone()),
|
|
"{search}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|