From 91a4b6ab3452aab07702f1b8a53c28766323d567 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Wed, 25 Feb 2026 18:37:33 +0100 Subject: [PATCH] feat(developer): replace agents iframe with informational landing and live agent table LangGraph is API-only with no web UI, so the ToolEmbed iframe pattern doesn't work. Replace it with an informational landing page featuring a hero section, connection status indicator, quick-start card grid linking to docs/GitHub/examples, and a live table of registered agents fetched from the LangGraph POST /assistants/search endpoint. Co-Authored-By: Claude Opus 4.6 --- assets/i18n/de.json | 22 ++- assets/i18n/en.json | 22 ++- assets/i18n/es.json | 22 ++- assets/i18n/fr.json | 22 ++- assets/i18n/pt.json | 22 ++- assets/main.css | 247 ++++++++++++++++++++++++++++++++ src/infrastructure/langgraph.rs | 107 ++++++++++++++ src/infrastructure/mod.rs | 1 + src/pages/developer/agents.rs | 217 ++++++++++++++++++++++++++-- 9 files changed, 667 insertions(+), 15 deletions(-) create mode 100644 src/infrastructure/langgraph.rs diff --git a/assets/i18n/de.json b/assets/i18n/de.json index 46dc084..b0b1029 100644 --- a/assets/i18n/de.json +++ b/assets/i18n/de.json @@ -98,7 +98,27 @@ "tokens_used": "Verbrauchte Token", "error_rate": "Fehlerrate", "not_configured": "Nicht konfiguriert", - "open_new_tab": "In neuem Tab oeffnen" + "open_new_tab": "In neuem Tab oeffnen", + "agents_status_connected": "Verbunden", + "agents_status_not_connected": "Nicht verbunden", + "agents_config_hint": "Setzen Sie LANGGRAPH_URL in .env, um eine Verbindung herzustellen", + "agents_quick_start": "Schnellstart", + "agents_docs": "Dokumentation", + "agents_docs_desc": "Offizielle LangGraph-Dokumentation und API-Anleitungen.", + "agents_getting_started": "Erste Schritte", + "agents_getting_started_desc": "Schritt-fuer-Schritt-Anleitung zum Erstellen Ihres ersten Agenten.", + "agents_github": "GitHub", + "agents_github_desc": "Quellcode, Issues und Community-Beitraege.", + "agents_examples": "Beispiele", + "agents_examples_desc": "Einsatzbereite Vorlagen und Beispielprojekte fuer Agenten.", + "agents_api_ref": "API-Referenz", + "agents_api_ref_desc": "Lokale Swagger-Dokumentation fuer Ihre LangGraph-Instanz.", + "agents_running_title": "Laufende Agenten", + "agents_none": "Keine Agenten registriert. Stellen Sie einen Assistenten in LangGraph bereit, um ihn hier zu sehen.", + "agents_col_name": "Name", + "agents_col_id": "ID", + "agents_col_description": "Beschreibung", + "agents_col_status": "Status" }, "org": { "title": "Organisation", diff --git a/assets/i18n/en.json b/assets/i18n/en.json index c4e84cf..a6a3070 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -98,7 +98,27 @@ "tokens_used": "Tokens Used", "error_rate": "Error Rate", "not_configured": "Not Configured", - "open_new_tab": "Open in New Tab" + "open_new_tab": "Open in New Tab", + "agents_status_connected": "Connected", + "agents_status_not_connected": "Not Connected", + "agents_config_hint": "Set LANGGRAPH_URL in .env to connect", + "agents_quick_start": "Quick Start", + "agents_docs": "Documentation", + "agents_docs_desc": "Official LangGraph documentation and API guides.", + "agents_getting_started": "Getting Started", + "agents_getting_started_desc": "Step-by-step tutorial to build your first agent.", + "agents_github": "GitHub", + "agents_github_desc": "Source code, issues, and community contributions.", + "agents_examples": "Examples", + "agents_examples_desc": "Ready-to-use templates and example agent projects.", + "agents_api_ref": "API Reference", + "agents_api_ref_desc": "Local Swagger docs for your LangGraph instance.", + "agents_running_title": "Running Agents", + "agents_none": "No agents registered. Deploy an assistant to LangGraph to see it here.", + "agents_col_name": "Name", + "agents_col_id": "ID", + "agents_col_description": "Description", + "agents_col_status": "Status" }, "org": { "title": "Organization", diff --git a/assets/i18n/es.json b/assets/i18n/es.json index 51c98ce..9017b39 100644 --- a/assets/i18n/es.json +++ b/assets/i18n/es.json @@ -98,7 +98,27 @@ "tokens_used": "Tokens utilizados", "error_rate": "Tasa de errores", "not_configured": "No configurado", - "open_new_tab": "Abrir en nueva pestana" + "open_new_tab": "Abrir en nueva pestana", + "agents_status_connected": "Conectado", + "agents_status_not_connected": "No conectado", + "agents_config_hint": "Configure LANGGRAPH_URL en .env para conectar", + "agents_quick_start": "Inicio rapido", + "agents_docs": "Documentacion", + "agents_docs_desc": "Documentacion oficial de LangGraph y guias de API.", + "agents_getting_started": "Primeros pasos", + "agents_getting_started_desc": "Tutorial paso a paso para crear su primer agente.", + "agents_github": "GitHub", + "agents_github_desc": "Codigo fuente, issues y contribuciones de la comunidad.", + "agents_examples": "Ejemplos", + "agents_examples_desc": "Plantillas y proyectos de agentes listos para usar.", + "agents_api_ref": "Referencia API", + "agents_api_ref_desc": "Documentacion Swagger local para su instancia de LangGraph.", + "agents_running_title": "Agentes en ejecucion", + "agents_none": "No hay agentes registrados. Despliegue un asistente en LangGraph para verlo aqui.", + "agents_col_name": "Nombre", + "agents_col_id": "ID", + "agents_col_description": "Descripcion", + "agents_col_status": "Estado" }, "org": { "title": "Organizacion", diff --git a/assets/i18n/fr.json b/assets/i18n/fr.json index 1d3845b..eb5e41b 100644 --- a/assets/i18n/fr.json +++ b/assets/i18n/fr.json @@ -98,7 +98,27 @@ "tokens_used": "Tokens utilises", "error_rate": "Taux d'erreur", "not_configured": "Non configure", - "open_new_tab": "Ouvrir dans un nouvel onglet" + "open_new_tab": "Ouvrir dans un nouvel onglet", + "agents_status_connected": "Connecte", + "agents_status_not_connected": "Non connecte", + "agents_config_hint": "Definissez LANGGRAPH_URL dans .env pour vous connecter", + "agents_quick_start": "Demarrage rapide", + "agents_docs": "Documentation", + "agents_docs_desc": "Documentation officielle de LangGraph et guides API.", + "agents_getting_started": "Premiers pas", + "agents_getting_started_desc": "Tutoriel etape par etape pour creer votre premier agent.", + "agents_github": "GitHub", + "agents_github_desc": "Code source, issues et contributions de la communaute.", + "agents_examples": "Exemples", + "agents_examples_desc": "Modeles et projets d'agents prets a l'emploi.", + "agents_api_ref": "Reference API", + "agents_api_ref_desc": "Documentation Swagger locale pour votre instance LangGraph.", + "agents_running_title": "Agents en cours", + "agents_none": "Aucun agent enregistre. Deployez un assistant dans LangGraph pour le voir ici.", + "agents_col_name": "Nom", + "agents_col_id": "ID", + "agents_col_description": "Description", + "agents_col_status": "Statut" }, "org": { "title": "Organisation", diff --git a/assets/i18n/pt.json b/assets/i18n/pt.json index fe0cb2a..c83d8a5 100644 --- a/assets/i18n/pt.json +++ b/assets/i18n/pt.json @@ -98,7 +98,27 @@ "tokens_used": "Tokens Utilizados", "error_rate": "Taxa de Erros", "not_configured": "Nao configurado", - "open_new_tab": "Abrir em novo separador" + "open_new_tab": "Abrir em novo separador", + "agents_status_connected": "Conectado", + "agents_status_not_connected": "Nao conectado", + "agents_config_hint": "Defina LANGGRAPH_URL no .env para conectar", + "agents_quick_start": "Inicio rapido", + "agents_docs": "Documentacao", + "agents_docs_desc": "Documentacao oficial do LangGraph e guias de API.", + "agents_getting_started": "Primeiros passos", + "agents_getting_started_desc": "Tutorial passo a passo para criar o seu primeiro agente.", + "agents_github": "GitHub", + "agents_github_desc": "Codigo fonte, issues e contribuicoes da comunidade.", + "agents_examples": "Exemplos", + "agents_examples_desc": "Modelos e projetos de agentes prontos a usar.", + "agents_api_ref": "Referencia API", + "agents_api_ref_desc": "Documentacao Swagger local para a sua instancia LangGraph.", + "agents_running_title": "Agentes em execucao", + "agents_none": "Nenhum agente registado. Implemente um assistente no LangGraph para o ver aqui.", + "agents_col_name": "Nome", + "agents_col_id": "ID", + "agents_col_description": "Descricao", + "agents_col_status": "Estado" }, "org": { "title": "Organizacao", diff --git a/assets/main.css b/assets/main.css index c551e78..fe26bcb 100644 --- a/assets/main.css +++ b/assets/main.css @@ -3374,4 +3374,251 @@ h6 { .feature-card { padding: 20px 16px; } +} + +/* ===== Agents Page ===== */ +.agents-page { + display: flex; + flex-direction: column; + padding: 32px; + gap: 32px; +} + +.agents-hero { + max-width: 720px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.agents-hero-row { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; +} + +.agents-hero-icon { + width: 48px; + height: 48px; + min-width: 48px; + background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); + color: var(--avatar-text); + border-radius: 12px; + font-size: 24px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; +} + +.agents-hero-title { + font-family: 'Space Grotesk', sans-serif; + font-size: 28px; + font-weight: 700; + color: var(--text-heading); + margin: 0; +} + +.agents-hero-desc { + font-size: 15px; + color: var(--text-muted); + line-height: 1.6; + max-width: 600px; + margin: 0; +} + +.agents-status { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; +} + +.agents-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + flex-shrink: 0; +} + +.agents-status-dot--on { + background-color: #22c55e; +} + +.agents-status-dot--off { + background-color: var(--text-faint); +} + +.agents-status-url { + font-family: 'JetBrains Mono', 'Fira Code', monospace; + color: var(--accent); + font-size: 13px; +} + +.agents-status-hint { + font-size: 13px; + color: var(--text-faint); + font-style: italic; +} + +.agents-section-title { + font-size: 18px; + font-weight: 600; + color: var(--text-heading); + margin: 0 0 12px 0; +} + +.agents-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.agents-card { + display: block; + text-decoration: none; + background-color: var(--bg-card); + border: 1px solid var(--border-primary); + border-radius: 12px; + padding: 24px; + transition: border-color 0.2s, transform 0.2s; + cursor: pointer; +} + +.agents-card:hover { + border-color: var(--accent); + transform: translateY(-2px); +} + +.agents-card-icon { + width: 36px; + height: 36px; + min-width: 36px; + background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); + color: var(--avatar-text); + border-radius: 8px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; +} + +.agents-card-title { + font-size: 16px; + font-weight: 600; + color: var(--text-heading); + margin: 12px 0 4px; +} + +.agents-card-desc { + font-size: 13px; + color: var(--text-muted); + line-height: 1.5; +} + +.agents-card--disabled { + opacity: 0.4; + pointer-events: none; + cursor: default; +} + +/* -- Agents table -- */ +.agents-table-section { + max-width: 960px; +} + +.agents-table-wrap { + overflow-x: auto; +} + +.agents-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} + +.agents-table thead th { + text-align: left; + font-size: 12px; + font-weight: 600; + color: var(--text-faint); + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 8px 12px; + border-bottom: 1px solid var(--border-secondary); +} + +.agents-table tbody td { + padding: 10px 12px; + border-bottom: 1px solid var(--border-primary); + color: var(--text-primary); + vertical-align: middle; +} + +.agents-table tbody tr:hover { + background-color: var(--bg-surface); +} + +.agents-cell-name { + font-weight: 600; + color: var(--text-heading); + white-space: nowrap; +} + +.agents-cell-id { + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 12px; + color: var(--text-muted); +} + +.agents-cell-desc { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-muted); +} + +.agents-cell-none { + color: var(--text-faint); +} + +.agents-badge { + display: inline-block; + font-size: 12px; + font-weight: 600; + padding: 2px 10px; + border-radius: 9999px; +} + +.agents-badge--active { + background-color: rgba(34, 197, 94, 0.15); + color: #22c55e; +} + +.agents-table-loading, +.agents-table-empty { + font-size: 14px; + color: var(--text-faint); + font-style: italic; + padding: 16px 0; +} + +@media (max-width: 768px) { + .agents-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 480px) { + .agents-page { + padding: 20px 16px; + } + + .agents-grid { + grid-template-columns: 1fr; + } } \ No newline at end of file diff --git a/src/infrastructure/langgraph.rs b/src/infrastructure/langgraph.rs new file mode 100644 index 0000000..3d6147d --- /dev/null +++ b/src/infrastructure/langgraph.rs @@ -0,0 +1,107 @@ +use dioxus::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::models::AgentEntry; + +/// Raw assistant object returned by the LangGraph `POST /assistants/search` +/// endpoint. Only the fields we display are deserialized; unknown keys are +/// silently ignored thanks to serde defaults. +#[cfg(feature = "server")] +#[derive(Deserialize)] +struct LangGraphAssistant { + assistant_id: String, + #[serde(default)] + name: String, + #[serde(default)] + graph_id: String, + #[serde(default)] + metadata: serde_json::Value, +} + +/// Fetch the list of assistants (agents) from a LangGraph instance. +/// +/// Calls `POST /assistants/search` with an empty body to +/// retrieve every registered assistant. Each result is mapped to the +/// frontend-friendly `AgentEntry` model. +/// +/// # Returns +/// +/// A vector of `AgentEntry` structs. Returns an empty vector when the +/// LangGraph URL is not configured or the service is unreachable. +/// +/// # Errors +/// +/// Returns `ServerFnError` on network or deserialization failures that +/// indicate a misconfigured (but present) LangGraph instance. +#[server(endpoint = "list-langgraph-agents")] +pub async fn list_langgraph_agents() -> Result, ServerFnError> { + let state: crate::infrastructure::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + + let base_url = state.services.langgraph_url.clone(); + if base_url.is_empty() { + return Ok(Vec::new()); + } + + let url = format!("{}/assistants/search", base_url.trim_end_matches('/')); + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .map_err(|e| ServerFnError::new(format!("HTTP client error: {e}")))?; + + // LangGraph expects a POST with a JSON body (empty object = no filters). + let resp = match client + .post(&url) + .header("content-type", "application/json") + .body("{}") + .send() + .await + { + Ok(r) if r.status().is_success() => r, + Ok(r) => { + let status = r.status(); + let body = r.text().await.unwrap_or_default(); + tracing::error!("LangGraph returned {status}: {body}"); + return Ok(Vec::new()); + } + Err(e) => { + tracing::error!("LangGraph request failed: {e}"); + return Ok(Vec::new()); + } + }; + + let assistants: Vec = resp + .json() + .await + .map_err(|e| ServerFnError::new(format!("Failed to parse LangGraph response: {e}")))?; + + let entries = assistants + .into_iter() + .map(|a| { + // Use the assistant name if present, otherwise fall back to graph_id. + let name = if a.name.is_empty() { + a.graph_id.clone() + } else { + a.name + }; + + // Extract a description from metadata if available. + let description = a + .metadata + .get("description") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + AgentEntry { + id: a.assistant_id, + name, + description, + status: "active".to_string(), + } + }) + .collect(); + + Ok(entries) +} diff --git a/src/infrastructure/mod.rs b/src/infrastructure/mod.rs index 8a96c2f..c18bf52 100644 --- a/src/infrastructure/mod.rs +++ b/src/infrastructure/mod.rs @@ -2,6 +2,7 @@ // the #[server] macro generates client stubs for the web target) pub mod auth_check; pub mod chat; +pub mod langgraph; pub mod llm; pub mod ollama; pub mod searxng; diff --git a/src/pages/developer/agents.rs b/src/pages/developer/agents.rs index 0cf4653..8717a71 100644 --- a/src/pages/developer/agents.rs +++ b/src/pages/developer/agents.rs @@ -1,13 +1,14 @@ use dioxus::prelude::*; -use crate::components::ToolEmbed; use crate::i18n::{t, Locale}; use crate::models::ServiceUrlsContext; -/// Agents page embedding the LangGraph agent builder. +/// Agents informational landing page for LangGraph. /// -/// When `langgraph_url` is configured, embeds the service in an iframe -/// with a pop-out button. Otherwise shows a "Not Configured" placeholder. +/// Since LangGraph is API-only (no web UI), this page displays a hero section +/// explaining its role, a connection status indicator, a card grid linking +/// to documentation, and a live table of registered agents fetched from the +/// LangGraph assistants API. #[component] pub fn AgentsPage() -> Element { let locale = use_context::>(); @@ -15,13 +16,209 @@ pub fn AgentsPage() -> Element { let l = *locale.read(); let url = svc.read().langgraph_url.clone(); + // Derive whether a LangGraph URL is configured + let connected = !url.is_empty(); + // Build the API reference URL from the configured base, falling back to "#" + let api_ref_href = if connected { + format!("{}/docs", url) + } else { + "#".to_string() + }; + + // Fetch agents from LangGraph when connected + let agents_resource = use_resource(move || async move { + match crate::infrastructure::langgraph::list_langgraph_agents().await { + Ok(agents) => agents, + Err(e) => { + tracing::error!("Failed to fetch agents: {e}"); + Vec::new() + } + } + }); + rsx! { - ToolEmbed { - url, - title: t(l, "developer.agents_title"), - description: t(l, "developer.agents_desc"), - icon: "A", - launch_label: t(l, "developer.launch_agents"), + div { class: "agents-page", + // -- Hero section -- + div { class: "agents-hero", + div { class: "agents-hero-row", + div { class: "agents-hero-icon placeholder-icon", "A" } + h2 { class: "agents-hero-title", + {t(l, "developer.agents_title")} + } + } + p { class: "agents-hero-desc", + {t(l, "developer.agents_desc")} + } + + // -- Connection status -- + if connected { + div { class: "agents-status", + span { + class: "agents-status-dot agents-status-dot--on", + } + span { {t(l, "developer.agents_status_connected")} } + code { class: "agents-status-url", {url.clone()} } + } + } else { + div { class: "agents-status", + span { + class: "agents-status-dot agents-status-dot--off", + } + span { {t(l, "developer.agents_status_not_connected")} } + span { class: "agents-status-hint", + {t(l, "developer.agents_config_hint")} + } + } + } + } + + // -- Running Agents table -- + div { class: "agents-table-section", + h3 { class: "agents-section-title", + {t(l, "developer.agents_running_title")} + } + + match agents_resource.read().as_ref() { + None => { + rsx! { + p { class: "agents-table-loading", + {t(l, "common.loading")} + } + } + } + Some(agents) if agents.is_empty() => { + rsx! { + p { class: "agents-table-empty", + {t(l, "developer.agents_none")} + } + } + } + Some(agents) => { + rsx! { + div { class: "agents-table-wrap", + table { class: "agents-table", + thead { + tr { + th { {t(l, "developer.agents_col_name")} } + th { {t(l, "developer.agents_col_id")} } + th { {t(l, "developer.agents_col_description")} } + th { {t(l, "developer.agents_col_status")} } + } + } + tbody { + for agent in agents.iter() { + tr { key: "{agent.id}", + td { class: "agents-cell-name", + {agent.name.clone()} + } + td { + code { class: "agents-cell-id", + {agent.id.clone()} + } + } + td { class: "agents-cell-desc", + if agent.description.is_empty() { + span { class: "agents-cell-none", "--" } + } else { + {agent.description.clone()} + } + } + td { + span { class: "agents-badge agents-badge--active", + {agent.status.clone()} + } + } + } + } + } + } + } + } + } + } + } + + // -- Quick Start card grid -- + h3 { class: "agents-section-title", + {t(l, "developer.agents_quick_start")} + } + + div { class: "agents-grid", + // Documentation + a { + class: "agents-card", + href: "https://langchain-ai.github.io/langgraph/", + target: "_blank", + rel: "noopener noreferrer", + div { class: "agents-card-icon placeholder-icon", "D" } + div { class: "agents-card-title", + {t(l, "developer.agents_docs")} + } + div { class: "agents-card-desc", + {t(l, "developer.agents_docs_desc")} + } + } + + // Getting Started + a { + class: "agents-card", + href: "https://langchain-ai.github.io/langgraph/tutorials/introduction/", + target: "_blank", + rel: "noopener noreferrer", + div { class: "agents-card-icon placeholder-icon", "G" } + div { class: "agents-card-title", + {t(l, "developer.agents_getting_started")} + } + div { class: "agents-card-desc", + {t(l, "developer.agents_getting_started_desc")} + } + } + + // GitHub + a { + class: "agents-card", + href: "https://github.com/langchain-ai/langgraph", + target: "_blank", + rel: "noopener noreferrer", + div { class: "agents-card-icon placeholder-icon", "H" } + div { class: "agents-card-title", + {t(l, "developer.agents_github")} + } + div { class: "agents-card-desc", + {t(l, "developer.agents_github_desc")} + } + } + + // Examples + a { + class: "agents-card", + href: "https://github.com/langchain-ai/langgraph/tree/main/examples", + target: "_blank", + rel: "noopener noreferrer", + div { class: "agents-card-icon placeholder-icon", "E" } + div { class: "agents-card-title", + {t(l, "developer.agents_examples")} + } + div { class: "agents-card-desc", + {t(l, "developer.agents_examples_desc")} + } + } + + // API Reference (disabled when URL is empty) + a { + class: if connected { "agents-card" } else { "agents-card agents-card--disabled" }, + href: "{api_ref_href}", + target: "_blank", + rel: "noopener noreferrer", + div { class: "agents-card-icon placeholder-icon", "R" } + div { class: "agents-card-title", + {t(l, "developer.agents_api_ref")} + } + div { class: "agents-card-desc", + {t(l, "developer.agents_api_ref_desc")} + } + } + } } } }