feat(developer): replace agents iframe with informational landing and live agent table
Some checks failed
CI / Format (push) Successful in 3s
CI / Clippy (push) Failing after 1m45s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / E2E Tests (push) Has been skipped
CI / Deploy (push) Has been skipped

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 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-02-25 18:37:33 +01:00
parent 40afc88317
commit 91a4b6ab34
9 changed files with 667 additions and 15 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -3375,3 +3375,250 @@ h6 {
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;
}
}

View File

@@ -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 <langgraph_url>/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<Vec<AgentEntry>, 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<LangGraphAssistant> = 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)
}

View File

@@ -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;

View File

@@ -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::<Signal<Locale>>();
@@ -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")}
}
}
}
}
}
}