From a24ea984b19a043cef9be73a43aa743ff485d888 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Wed, 25 Feb 2026 17:49:56 +0100 Subject: [PATCH] feat(developer): add hybrid iframe integration for developer tools Replace placeholder pages with ToolEmbed component that embeds LangGraph, LangFlow, and Langfuse in iframes when configured, or shows "Not Configured" placeholders when URLs are empty. Add ServiceUrlsContext for passing service URLs through Dioxus context. Add docker-compose services for local development: LangFlow, LangGraph (trial), Langfuse with full dependency stack (Postgres, ClickHouse, Redis, MinIO). Co-Authored-By: Claude Opus 4.6 --- .env.example | 3 +- .gitea/workflows/ci.yml | 3 + assets/i18n/de.json | 4 +- assets/i18n/en.json | 4 +- assets/i18n/es.json | 4 +- assets/i18n/fr.json | 4 +- assets/i18n/pt.json | 4 +- assets/main.css | 52 +++++++++++ docker-compose.yml | 150 +++++++++++++++++++++++++++++++ e2e/developer.spec.ts | 37 ++++++-- src/components/app_shell.rs | 12 ++- src/components/mod.rs | 2 + src/components/tool_embed.rs | 81 +++++++++++++++++ src/infrastructure/auth_check.rs | 12 +++ src/infrastructure/config.rs | 3 + src/models/mod.rs | 2 + src/models/services.rs | 43 +++++++++ src/models/user.rs | 12 +++ src/pages/developer/agents.rs | 26 +++--- src/pages/developer/analytics.rs | 35 +++++--- src/pages/developer/flow.rs | 26 +++--- 21 files changed, 467 insertions(+), 52 deletions(-) create mode 100644 src/components/tool_embed.rs create mode 100644 src/models/services.rs diff --git a/.env.example b/.env.example index 6182d8f..f5a8f34 100644 --- a/.env.example +++ b/.env.example @@ -66,10 +66,11 @@ STRIPE_WEBHOOK_SECRET= STRIPE_PUBLISHABLE_KEY= # --------------------------------------------------------------------------- -# LangChain / LangGraph / Langfuse [OPTIONAL] +# LangChain / LangGraph / LangFlow / Langfuse [OPTIONAL] # --------------------------------------------------------------------------- LANGCHAIN_URL= LANGGRAPH_URL= +LANGFLOW_URL= LANGFUSE_URL= # --------------------------------------------------------------------------- diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 2f47959..08cf528 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -154,6 +154,9 @@ jobs: MONGODB_URI: mongodb://root:example@mongo:27017 MONGODB_DATABASE: certifai SEARXNG_URL: http://searxng:8080 + LANGGRAPH_URL: "" + LANGFLOW_URL: "" + LANGFUSE_URL: "" steps: - name: Checkout run: | diff --git a/assets/i18n/de.json b/assets/i18n/de.json index 515c528..46dc084 100644 --- a/assets/i18n/de.json +++ b/assets/i18n/de.json @@ -96,7 +96,9 @@ "total_requests": "Anfragen gesamt", "avg_latency": "Durchschn. Latenz", "tokens_used": "Verbrauchte Token", - "error_rate": "Fehlerrate" + "error_rate": "Fehlerrate", + "not_configured": "Nicht konfiguriert", + "open_new_tab": "In neuem Tab oeffnen" }, "org": { "title": "Organisation", diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 774f1fa..c4e84cf 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -96,7 +96,9 @@ "total_requests": "Total Requests", "avg_latency": "Avg Latency", "tokens_used": "Tokens Used", - "error_rate": "Error Rate" + "error_rate": "Error Rate", + "not_configured": "Not Configured", + "open_new_tab": "Open in New Tab" }, "org": { "title": "Organization", diff --git a/assets/i18n/es.json b/assets/i18n/es.json index 6a0a4b1..51c98ce 100644 --- a/assets/i18n/es.json +++ b/assets/i18n/es.json @@ -96,7 +96,9 @@ "total_requests": "Total de solicitudes", "avg_latency": "Latencia promedio", "tokens_used": "Tokens utilizados", - "error_rate": "Tasa de errores" + "error_rate": "Tasa de errores", + "not_configured": "No configurado", + "open_new_tab": "Abrir en nueva pestana" }, "org": { "title": "Organizacion", diff --git a/assets/i18n/fr.json b/assets/i18n/fr.json index 9ab76f1..1d3845b 100644 --- a/assets/i18n/fr.json +++ b/assets/i18n/fr.json @@ -96,7 +96,9 @@ "total_requests": "Requetes totales", "avg_latency": "Latence moyenne", "tokens_used": "Tokens utilises", - "error_rate": "Taux d'erreur" + "error_rate": "Taux d'erreur", + "not_configured": "Non configure", + "open_new_tab": "Ouvrir dans un nouvel onglet" }, "org": { "title": "Organisation", diff --git a/assets/i18n/pt.json b/assets/i18n/pt.json index 1d4e7d4..fe0cb2a 100644 --- a/assets/i18n/pt.json +++ b/assets/i18n/pt.json @@ -96,7 +96,9 @@ "total_requests": "Total de Pedidos", "avg_latency": "Latencia Media", "tokens_used": "Tokens Utilizados", - "error_rate": "Taxa de Erros" + "error_rate": "Taxa de Erros", + "not_configured": "Nao configurado", + "open_new_tab": "Abrir em novo separador" }, "org": { "title": "Organizacao", diff --git a/assets/main.css b/assets/main.css index 1ed9d8a..c551e78 100644 --- a/assets/main.css +++ b/assets/main.css @@ -2591,6 +2591,58 @@ h6 { border-radius: 20px; } +/* ===== Tool Embed (iframe integration) ===== */ +.tool-embed { + display: flex; + flex-direction: column; + flex: 1; + height: calc(100vh - 60px); + min-height: 400px; +} + +.tool-embed-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 20px; + background-color: var(--bg-card); + border-bottom: 1px solid var(--border-primary); +} + +.tool-embed-title { + font-family: 'Space Grotesk', sans-serif; + font-size: 16px; + font-weight: 600; + color: var(--text-heading); +} + +.tool-embed-popout-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + font-size: 13px; + font-weight: 500; + color: var(--accent); + background-color: transparent; + border: 1px solid var(--accent); + border-radius: 6px; + text-decoration: none; + cursor: pointer; + transition: background-color 0.15s, color 0.15s; +} + +.tool-embed-popout-btn:hover { + background-color: var(--accent); + color: var(--bg-body); +} + +.tool-embed-iframe { + flex: 1; + width: 100%; + border: none; +} + /* ===== Analytics Stats Bar ===== */ .analytics-stats-bar { display: flex; diff --git a/docker-compose.yml b/docker-compose.yml index 1d8b2ef..4a850ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -94,5 +94,155 @@ services: - ./librechat/openidStrategy.js:/app/api/strategies/openidStrategy.js:ro - librechat-data:/app/data + langflow: + image: langflowai/langflow:latest + container_name: certifai-langflow + restart: unless-stopped + ports: + - "7860:7860" + environment: + LANGFLOW_AUTO_LOGIN: "true" + + langgraph: + image: langchain/langgraph-trial:3.12 + container_name: certifai-langgraph + restart: unless-stopped + depends_on: + langgraph-db: + condition: service_started + langgraph-redis: + condition: service_started + ports: + - "8123:8000" + environment: + DATABASE_URI: postgresql://langgraph:langgraph@langgraph-db:5432/langgraph + REDIS_URI: redis://langgraph-redis:6379 + + langgraph-db: + image: postgres:16 + container_name: certifai-langgraph-db + restart: unless-stopped + environment: + POSTGRES_USER: langgraph + POSTGRES_PASSWORD: langgraph + POSTGRES_DB: langgraph + volumes: + - langgraph-db-data:/var/lib/postgresql/data + + langgraph-redis: + image: redis:7-alpine + container_name: certifai-langgraph-redis + restart: unless-stopped + + langfuse: + image: langfuse/langfuse:3 + container_name: certifai-langfuse + restart: unless-stopped + depends_on: + langfuse-db: + condition: service_healthy + langfuse-clickhouse: + condition: service_healthy + langfuse-redis: + condition: service_healthy + langfuse-minio: + condition: service_healthy + ports: + - "3000:3000" + environment: + DATABASE_URL: postgresql://langfuse:langfuse@langfuse-db:5432/langfuse + NEXTAUTH_URL: http://localhost:3000 + NEXTAUTH_SECRET: certifai-langfuse-dev-secret + SALT: certifai-langfuse-dev-salt + ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" + CLICKHOUSE_URL: http://langfuse-clickhouse:8123 + CLICKHOUSE_MIGRATION_URL: clickhouse://langfuse-clickhouse:9000 + CLICKHOUSE_USER: clickhouse + CLICKHOUSE_PASSWORD: clickhouse + CLICKHOUSE_CLUSTER_ENABLED: "false" + REDIS_HOST: langfuse-redis + REDIS_PORT: "6379" + REDIS_AUTH: langfuse-dev-redis + LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse + LANGFUSE_S3_EVENT_UPLOAD_REGION: auto + LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio + LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: miniosecret + LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://langfuse-minio:9000 + LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true" + LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse + LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto + LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: minio + LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: miniosecret + LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://langfuse-minio:9000 + LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true" + + langfuse-db: + image: postgres:16 + container_name: certifai-langfuse-db + restart: unless-stopped + environment: + POSTGRES_USER: langfuse + POSTGRES_PASSWORD: langfuse + POSTGRES_DB: langfuse + volumes: + - langfuse-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U langfuse"] + interval: 5s + timeout: 5s + retries: 10 + + langfuse-clickhouse: + image: clickhouse/clickhouse-server:latest + container_name: certifai-langfuse-clickhouse + restart: unless-stopped + user: "101:101" + environment: + CLICKHOUSE_DB: default + CLICKHOUSE_USER: clickhouse + CLICKHOUSE_PASSWORD: clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + volumes: + - langfuse-clickhouse-data:/var/lib/clickhouse + - langfuse-clickhouse-logs:/var/log/clickhouse-server + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"] + interval: 5s + timeout: 5s + retries: 10 + + langfuse-redis: + image: redis:7-alpine + container_name: certifai-langfuse-redis + restart: unless-stopped + command: redis-server --requirepass langfuse-dev-redis + healthcheck: + test: ["CMD", "redis-cli", "-a", "langfuse-dev-redis", "ping"] + interval: 5s + timeout: 5s + retries: 10 + + langfuse-minio: + image: cgr.dev/chainguard/minio + container_name: certifai-langfuse-minio + restart: unless-stopped + entrypoint: sh + command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data' + environment: + MINIO_ROOT_USER: minio + MINIO_ROOT_PASSWORD: miniosecret + healthcheck: + test: ["CMD-SHELL", "mc ready local || exit 1"] + interval: 5s + timeout: 5s + retries: 10 + volumes: librechat-data: + langgraph-db-data: + langfuse-db-data: + langfuse-clickhouse-data: + langfuse-clickhouse-logs: diff --git a/e2e/developer.spec.ts b/e2e/developer.spec.ts index 9d84e30..b64d426 100644 --- a/e2e/developer.spec.ts +++ b/e2e/developer.spec.ts @@ -11,23 +11,50 @@ test.describe("Developer section", () => { await expect(nav.locator("a", { hasText: "Analytics" })).toBeVisible(); }); - test("agents page shows Coming Soon badge", async ({ page }) => { + test("agents page shows Not Configured when URL is empty", async ({ + page, + }) => { await page.goto("/developer/agents"); await page.waitForSelector(".placeholder-page", { timeout: 15_000 }); await expect(page.locator(".placeholder-badge")).toContainText( - "Coming Soon" + "Not Configured" ); await expect(page.locator("h2")).toContainText("Agent Builder"); }); - test("analytics page loads via sub-nav", async ({ page }) => { + test("analytics page shows Not Configured when URL is empty", async ({ + page, + }) => { await page.goto("/developer/analytics"); await page.waitForSelector(".placeholder-page", { timeout: 15_000 }); - await expect(page.locator("h2")).toContainText("Analytics"); + await expect( + page.locator("h2", { hasText: "Analytics" }) + ).toBeVisible(); await expect(page.locator(".placeholder-badge")).toContainText( - "Coming Soon" + "Not Configured" ); }); + + test("agents page shows iframe when URL is configured", async ({ + page, + }) => { + // This test only runs meaningfully when LANGGRAPH_URL is set in the + // environment. When empty, the placeholder is shown instead. + await page.goto("/developer/agents"); + await page.waitForTimeout(2000); + + const iframe = page.locator(".tool-embed-iframe"); + const placeholder = page.locator(".placeholder-badge"); + + if (await placeholder.isVisible()) { + await expect(placeholder).toContainText("Not Configured"); + } else { + await expect(iframe).toBeVisible(); + await expect( + page.locator(".tool-embed-popout-btn") + ).toBeVisible(); + } + }); }); diff --git a/src/components/app_shell.rs b/src/components/app_shell.rs index 37e084b..2eb96aa 100644 --- a/src/components/app_shell.rs +++ b/src/components/app_shell.rs @@ -5,7 +5,7 @@ use dioxus_free_icons::Icon; use crate::components::sidebar::Sidebar; use crate::i18n::{t, tw, Locale}; use crate::infrastructure::auth_check::check_auth; -use crate::models::AuthInfo; +use crate::models::{AuthInfo, ServiceUrlsContext}; use crate::Route; /// Application shell layout that wraps all authenticated pages. @@ -29,6 +29,16 @@ pub fn AppShell() -> Element { match auth_snapshot { Some(Ok(info)) if info.authenticated => { + // Provide developer tool URLs as context so child pages + // can read them without prop-drilling through layouts. + use_context_provider(|| { + Signal::new(ServiceUrlsContext { + langgraph_url: info.langgraph_url.clone(), + langflow_url: info.langflow_url.clone(), + langfuse_url: info.langfuse_url.clone(), + }) + }); + let menu_open = *mobile_menu_open.read(); let sidebar_cls = if menu_open { "sidebar sidebar--open" diff --git a/src/components/mod.rs b/src/components/mod.rs index 614a89b..7188c2c 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -9,6 +9,7 @@ mod page_header; mod pricing_card; pub mod sidebar; pub mod sub_nav; +mod tool_embed; pub use app_shell::*; pub use article_detail::*; @@ -20,3 +21,4 @@ pub use news_card::*; pub use page_header::*; pub use pricing_card::*; pub use sub_nav::*; +pub use tool_embed::*; diff --git a/src/components/tool_embed.rs b/src/components/tool_embed.rs new file mode 100644 index 0000000..6bfc119 --- /dev/null +++ b/src/components/tool_embed.rs @@ -0,0 +1,81 @@ +use dioxus::prelude::*; + +use crate::i18n::{t, Locale}; + +/// Properties for the [`ToolEmbed`] component. +/// +/// # Fields +/// +/// * `url` - Service URL; when empty, a "Not Configured" placeholder is shown +/// * `title` - Display title for the tool (e.g. "Agent Builder") +/// * `description` - Description text shown in the placeholder card +/// * `icon` - Single-character icon for the placeholder card +/// * `launch_label` - Label for the disabled button in the placeholder +#[derive(Props, Clone, PartialEq)] +pub struct ToolEmbedProps { + /// Service URL. Empty string means "not configured". + pub url: String, + /// Display title shown in the toolbar / placeholder heading. + pub title: String, + /// Description shown in the "not configured" placeholder. + pub description: String, + /// Single-character icon for the placeholder card. + pub icon: &'static str, + /// Label for the disabled launch button in placeholder mode. + pub launch_label: String, +} + +/// Hybrid iframe / placeholder component for developer tool pages. +/// +/// When `url` is non-empty, renders a toolbar (title + pop-out button) +/// above a full-height iframe embedding the service. When `url` is +/// empty, renders the existing placeholder card with a "Not Configured" +/// badge instead of "Coming Soon". +#[component] +pub fn ToolEmbed(props: ToolEmbedProps) -> Element { + let locale = use_context::>(); + let l = *locale.read(); + + if props.url.is_empty() { + // Not configured -- show placeholder card + rsx! { + section { class: "placeholder-page", + div { class: "placeholder-card", + div { class: "placeholder-icon", "{props.icon}" } + h2 { "{props.title}" } + p { class: "placeholder-desc", "{props.description}" } + button { + class: "btn-primary", + disabled: true, + "{props.launch_label}" + } + span { class: "placeholder-badge", + "{t(l, \"developer.not_configured\")}" + } + } + } + } + } else { + // URL is set -- render toolbar + iframe + let pop_out_url = props.url.clone(); + rsx! { + div { class: "tool-embed", + div { class: "tool-embed-toolbar", + span { class: "tool-embed-title", "{props.title}" } + a { + class: "tool-embed-popout-btn", + href: "{pop_out_url}", + target: "_blank", + rel: "noopener noreferrer", + "{t(l, \"developer.open_new_tab\")}" + } + } + iframe { + class: "tool-embed-iframe", + src: "{props.url}", + title: "{props.title}", + } + } + } + } +} diff --git a/src/infrastructure/auth_check.rs b/src/infrastructure/auth_check.rs index 6bbb8d8..009ec52 100644 --- a/src/infrastructure/auth_check.rs +++ b/src/infrastructure/auth_check.rs @@ -27,6 +27,15 @@ pub async fn check_auth() -> Result { Some(u) => { let librechat_url = std::env::var("LIBRECHAT_URL").unwrap_or_else(|_| "http://localhost:3080".into()); + + // Extract service URLs from server state so the frontend can + // embed developer tools (LangGraph, LangFlow, Langfuse). + let state: crate::infrastructure::server_state::ServerState = + FullstackContext::extract().await?; + let langgraph_url = state.services.langgraph_url.clone(); + let langflow_url = state.services.langflow_url.clone(); + let langfuse_url = state.services.langfuse_url.clone(); + Ok(AuthInfo { authenticated: true, sub: u.sub, @@ -34,6 +43,9 @@ pub async fn check_auth() -> Result { name: u.user.name, avatar_url: u.user.avatar_url, librechat_url, + langgraph_url, + langflow_url, + langfuse_url, }) } None => Ok(AuthInfo::default()), diff --git a/src/infrastructure/config.rs b/src/infrastructure/config.rs index 3ce3ac5..23128fc 100644 --- a/src/infrastructure/config.rs +++ b/src/infrastructure/config.rs @@ -154,6 +154,8 @@ pub struct ServiceUrls { pub langchain_url: String, /// LangGraph service URL. pub langgraph_url: String, + /// LangFlow visual workflow builder URL. + pub langflow_url: String, /// Langfuse observability URL. pub langfuse_url: String, /// Vector database URL. @@ -183,6 +185,7 @@ impl ServiceUrls { .unwrap_or_else(|_| "http://localhost:8888".into()), langchain_url: optional_env("LANGCHAIN_URL"), langgraph_url: optional_env("LANGGRAPH_URL"), + langflow_url: optional_env("LANGFLOW_URL"), langfuse_url: optional_env("LANGFUSE_URL"), vectordb_url: optional_env("VECTORDB_URL"), s3_url: optional_env("S3_URL"), diff --git a/src/models/mod.rs b/src/models/mod.rs index 933d0be..c9f5be8 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -3,6 +3,7 @@ mod developer; mod news; mod organization; mod provider; +mod services; mod user; pub use chat::*; @@ -10,4 +11,5 @@ pub use developer::*; pub use news::*; pub use organization::*; pub use provider::*; +pub use services::*; pub use user::*; diff --git a/src/models/services.rs b/src/models/services.rs new file mode 100644 index 0000000..7d1e599 --- /dev/null +++ b/src/models/services.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; + +/// Frontend-facing URLs for developer tool services. +/// +/// Provided as a context signal in `AppShell` so that developer pages +/// can read the configured URLs without threading props through layouts. +/// An empty string indicates the service is not configured. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct ServiceUrlsContext { + /// LangGraph agent builder URL (empty if not configured) + pub langgraph_url: String, + /// LangFlow visual workflow builder URL (empty if not configured) + pub langflow_url: String, + /// Langfuse observability URL (empty if not configured) + pub langfuse_url: String, +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn default_urls_are_empty() { + let ctx = ServiceUrlsContext::default(); + assert_eq!(ctx.langgraph_url, ""); + assert_eq!(ctx.langflow_url, ""); + assert_eq!(ctx.langfuse_url, ""); + } + + #[test] + fn serde_round_trip() { + let ctx = ServiceUrlsContext { + langgraph_url: "http://localhost:8123".into(), + langflow_url: "http://localhost:7860".into(), + langfuse_url: "http://localhost:3000".into(), + }; + let json = serde_json::to_string(&ctx).expect("serialize ServiceUrlsContext"); + let back: ServiceUrlsContext = + serde_json::from_str(&json).expect("deserialize ServiceUrlsContext"); + assert_eq!(ctx, back); + } +} diff --git a/src/models/user.rs b/src/models/user.rs index 5bbc8f9..cbab583 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -24,6 +24,12 @@ pub struct AuthInfo { pub avatar_url: String, /// LibreChat instance URL for the sidebar chat link pub librechat_url: String, + /// LangGraph agent builder URL (empty if not configured) + pub langgraph_url: String, + /// LangFlow visual workflow builder URL (empty if not configured) + pub langflow_url: String, + /// Langfuse observability URL (empty if not configured) + pub langfuse_url: String, } /// Per-user LLM provider configuration stored in MongoDB. @@ -91,6 +97,9 @@ mod tests { assert_eq!(info.name, ""); assert_eq!(info.avatar_url, ""); assert_eq!(info.librechat_url, ""); + assert_eq!(info.langgraph_url, ""); + assert_eq!(info.langflow_url, ""); + assert_eq!(info.langfuse_url, ""); } #[test] @@ -102,6 +111,9 @@ mod tests { name: "Test User".into(), avatar_url: "https://example.com/avatar.png".into(), librechat_url: "https://chat.example.com".into(), + langgraph_url: "http://localhost:8123".into(), + langflow_url: "http://localhost:7860".into(), + langfuse_url: "http://localhost:3000".into(), }; let json = serde_json::to_string(&info).expect("serialize AuthInfo"); let back: AuthInfo = serde_json::from_str(&json).expect("deserialize AuthInfo"); diff --git a/src/pages/developer/agents.rs b/src/pages/developer/agents.rs index 1396e4b..0cf4653 100644 --- a/src/pages/developer/agents.rs +++ b/src/pages/developer/agents.rs @@ -1,27 +1,27 @@ use dioxus::prelude::*; +use crate::components::ToolEmbed; use crate::i18n::{t, Locale}; +use crate::models::ServiceUrlsContext; -/// Agents page placeholder for the LangGraph agent builder. +/// Agents page embedding the LangGraph agent builder. /// -/// Shows a "Coming Soon" card with a disabled launch button. -/// Will eventually integrate with the LangGraph framework. +/// When `langgraph_url` is configured, embeds the service in an iframe +/// with a pop-out button. Otherwise shows a "Not Configured" placeholder. #[component] pub fn AgentsPage() -> Element { let locale = use_context::>(); + let svc = use_context::>(); let l = *locale.read(); + let url = svc.read().langgraph_url.clone(); rsx! { - section { class: "placeholder-page", - div { class: "placeholder-card", - div { class: "placeholder-icon", "A" } - h2 { "{t(l, \"developer.agents_title\")}" } - p { class: "placeholder-desc", - "{t(l, \"developer.agents_desc\")}" - } - button { class: "btn-primary", disabled: true, "{t(l, \"developer.launch_agents\")}" } - span { class: "placeholder-badge", "{t(l, \"common.coming_soon\")}" } - } + ToolEmbed { + url, + title: t(l, "developer.agents_title"), + description: t(l, "developer.agents_desc"), + icon: "A", + launch_label: t(l, "developer.launch_agents"), } } } diff --git a/src/pages/developer/analytics.rs b/src/pages/developer/analytics.rs index b04883d..bdde149 100644 --- a/src/pages/developer/analytics.rs +++ b/src/pages/developer/analytics.rs @@ -1,16 +1,20 @@ use dioxus::prelude::*; +use crate::components::ToolEmbed; use crate::i18n::{t, Locale}; -use crate::models::AnalyticsMetric; +use crate::models::{AnalyticsMetric, ServiceUrlsContext}; -/// Analytics page placeholder for LangFuse integration. +/// Analytics page embedding Langfuse for observability. /// -/// Shows a "Coming Soon" card with a disabled launch button, -/// plus a mock stats bar showing sample metrics. +/// Always shows a stats bar with sample metrics. Below that, when +/// `langfuse_url` is configured, embeds the service in an iframe +/// with a pop-out button. Otherwise shows a "Not Configured" placeholder. #[component] pub fn AnalyticsPage() -> Element { let locale = use_context::>(); + let svc = use_context::>(); let l = *locale.read(); + let url = svc.read().langfuse_url.clone(); let metrics = mock_metrics(l); @@ -21,21 +25,24 @@ pub fn AnalyticsPage() -> Element { div { class: "analytics-stat", span { class: "analytics-stat-value", "{metric.value}" } span { class: "analytics-stat-label", "{metric.label}" } - span { class: if metric.change_pct >= 0.0 { "analytics-stat-change analytics-stat-change--up" } else { "analytics-stat-change analytics-stat-change--down" }, + span { + class: if metric.change_pct >= 0.0 { + "analytics-stat-change analytics-stat-change--up" + } else { + "analytics-stat-change analytics-stat-change--down" + }, "{metric.change_pct:+.1}%" } } } } - div { class: "placeholder-card", - div { class: "placeholder-icon", "L" } - h2 { "{t(l, \"developer.analytics_title\")}" } - p { class: "placeholder-desc", - "{t(l, \"developer.analytics_desc\")}" - } - button { class: "btn-primary", disabled: true, "{t(l, \"developer.launch_analytics\")}" } - span { class: "placeholder-badge", "{t(l, \"common.coming_soon\")}" } - } + } + ToolEmbed { + url, + title: t(l, "developer.analytics_title"), + description: t(l, "developer.analytics_desc"), + icon: "L", + launch_label: t(l, "developer.launch_analytics"), } } } diff --git a/src/pages/developer/flow.rs b/src/pages/developer/flow.rs index 0f95496..f252116 100644 --- a/src/pages/developer/flow.rs +++ b/src/pages/developer/flow.rs @@ -1,27 +1,27 @@ use dioxus::prelude::*; +use crate::components::ToolEmbed; use crate::i18n::{t, Locale}; +use crate::models::ServiceUrlsContext; -/// Flow page placeholder for the LangFlow visual workflow builder. +/// Flow page embedding the LangFlow visual workflow builder. /// -/// Shows a "Coming Soon" card with a disabled launch button. -/// Will eventually integrate with LangFlow for visual flow design. +/// When `langflow_url` is configured, embeds the service in an iframe +/// with a pop-out button. Otherwise shows a "Not Configured" placeholder. #[component] pub fn FlowPage() -> Element { let locale = use_context::>(); + let svc = use_context::>(); let l = *locale.read(); + let url = svc.read().langflow_url.clone(); rsx! { - section { class: "placeholder-page", - div { class: "placeholder-card", - div { class: "placeholder-icon", "F" } - h2 { "{t(l, \"developer.flow_title\")}" } - p { class: "placeholder-desc", - "{t(l, \"developer.flow_desc\")}" - } - button { class: "btn-primary", disabled: true, "{t(l, \"developer.launch_flow\")}" } - span { class: "placeholder-badge", "{t(l, \"common.coming_soon\")}" } - } + ToolEmbed { + url, + title: t(l, "developer.flow_title"), + description: t(l, "developer.flow_desc"), + icon: "F", + launch_label: t(l, "developer.launch_flow"), } } }