feat: added langflow, langfuse and langgraph integrations #17

Merged
sharang merged 8 commits from test/add-unit-test-suite into main 2026-02-25 20:08:49 +00:00
21 changed files with 467 additions and 52 deletions
Showing only changes of commit a24ea984b1 - Show all commits

View File

@@ -66,10 +66,11 @@ STRIPE_WEBHOOK_SECRET=
STRIPE_PUBLISHABLE_KEY= STRIPE_PUBLISHABLE_KEY=
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# LangChain / LangGraph / Langfuse [OPTIONAL] # LangChain / LangGraph / LangFlow / Langfuse [OPTIONAL]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
LANGCHAIN_URL= LANGCHAIN_URL=
LANGGRAPH_URL= LANGGRAPH_URL=
LANGFLOW_URL=
LANGFUSE_URL= LANGFUSE_URL=
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -154,6 +154,9 @@ jobs:
MONGODB_URI: mongodb://root:example@mongo:27017 MONGODB_URI: mongodb://root:example@mongo:27017
MONGODB_DATABASE: certifai MONGODB_DATABASE: certifai
SEARXNG_URL: http://searxng:8080 SEARXNG_URL: http://searxng:8080
LANGGRAPH_URL: ""
LANGFLOW_URL: ""
LANGFUSE_URL: ""
steps: steps:
- name: Checkout - name: Checkout
run: | run: |

View File

@@ -96,7 +96,9 @@
"total_requests": "Anfragen gesamt", "total_requests": "Anfragen gesamt",
"avg_latency": "Durchschn. Latenz", "avg_latency": "Durchschn. Latenz",
"tokens_used": "Verbrauchte Token", "tokens_used": "Verbrauchte Token",
"error_rate": "Fehlerrate" "error_rate": "Fehlerrate",
"not_configured": "Nicht konfiguriert",
"open_new_tab": "In neuem Tab oeffnen"
}, },
"org": { "org": {
"title": "Organisation", "title": "Organisation",

View File

@@ -96,7 +96,9 @@
"total_requests": "Total Requests", "total_requests": "Total Requests",
"avg_latency": "Avg Latency", "avg_latency": "Avg Latency",
"tokens_used": "Tokens Used", "tokens_used": "Tokens Used",
"error_rate": "Error Rate" "error_rate": "Error Rate",
"not_configured": "Not Configured",
"open_new_tab": "Open in New Tab"
}, },
"org": { "org": {
"title": "Organization", "title": "Organization",

View File

@@ -96,7 +96,9 @@
"total_requests": "Total de solicitudes", "total_requests": "Total de solicitudes",
"avg_latency": "Latencia promedio", "avg_latency": "Latencia promedio",
"tokens_used": "Tokens utilizados", "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": { "org": {
"title": "Organizacion", "title": "Organizacion",

View File

@@ -96,7 +96,9 @@
"total_requests": "Requetes totales", "total_requests": "Requetes totales",
"avg_latency": "Latence moyenne", "avg_latency": "Latence moyenne",
"tokens_used": "Tokens utilises", "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": { "org": {
"title": "Organisation", "title": "Organisation",

View File

@@ -96,7 +96,9 @@
"total_requests": "Total de Pedidos", "total_requests": "Total de Pedidos",
"avg_latency": "Latencia Media", "avg_latency": "Latencia Media",
"tokens_used": "Tokens Utilizados", "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": { "org": {
"title": "Organizacao", "title": "Organizacao",

View File

@@ -2591,6 +2591,58 @@ h6 {
border-radius: 20px; 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 ===== */
.analytics-stats-bar { .analytics-stats-bar {
display: flex; display: flex;

View File

@@ -94,5 +94,155 @@ services:
- ./librechat/openidStrategy.js:/app/api/strategies/openidStrategy.js:ro - ./librechat/openidStrategy.js:/app/api/strategies/openidStrategy.js:ro
- librechat-data:/app/data - 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: volumes:
librechat-data: librechat-data:
langgraph-db-data:
langfuse-db-data:
langfuse-clickhouse-data:
langfuse-clickhouse-logs:

View File

@@ -11,23 +11,50 @@ test.describe("Developer section", () => {
await expect(nav.locator("a", { hasText: "Analytics" })).toBeVisible(); 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.goto("/developer/agents");
await page.waitForSelector(".placeholder-page", { timeout: 15_000 }); await page.waitForSelector(".placeholder-page", { timeout: 15_000 });
await expect(page.locator(".placeholder-badge")).toContainText( await expect(page.locator(".placeholder-badge")).toContainText(
"Coming Soon" "Not Configured"
); );
await expect(page.locator("h2")).toContainText("Agent Builder"); 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.goto("/developer/analytics");
await page.waitForSelector(".placeholder-page", { timeout: 15_000 }); 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( 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();
}
});
}); });

View File

@@ -5,7 +5,7 @@ use dioxus_free_icons::Icon;
use crate::components::sidebar::Sidebar; use crate::components::sidebar::Sidebar;
use crate::i18n::{t, tw, Locale}; use crate::i18n::{t, tw, Locale};
use crate::infrastructure::auth_check::check_auth; use crate::infrastructure::auth_check::check_auth;
use crate::models::AuthInfo; use crate::models::{AuthInfo, ServiceUrlsContext};
use crate::Route; use crate::Route;
/// Application shell layout that wraps all authenticated pages. /// Application shell layout that wraps all authenticated pages.
@@ -29,6 +29,16 @@ pub fn AppShell() -> Element {
match auth_snapshot { match auth_snapshot {
Some(Ok(info)) if info.authenticated => { 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 menu_open = *mobile_menu_open.read();
let sidebar_cls = if menu_open { let sidebar_cls = if menu_open {
"sidebar sidebar--open" "sidebar sidebar--open"

View File

@@ -9,6 +9,7 @@ mod page_header;
mod pricing_card; mod pricing_card;
pub mod sidebar; pub mod sidebar;
pub mod sub_nav; pub mod sub_nav;
mod tool_embed;
pub use app_shell::*; pub use app_shell::*;
pub use article_detail::*; pub use article_detail::*;
@@ -20,3 +21,4 @@ pub use news_card::*;
pub use page_header::*; pub use page_header::*;
pub use pricing_card::*; pub use pricing_card::*;
pub use sub_nav::*; pub use sub_nav::*;
pub use tool_embed::*;

View File

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

View File

@@ -27,6 +27,15 @@ pub async fn check_auth() -> Result<AuthInfo, ServerFnError> {
Some(u) => { Some(u) => {
let librechat_url = let librechat_url =
std::env::var("LIBRECHAT_URL").unwrap_or_else(|_| "http://localhost:3080".into()); 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 { Ok(AuthInfo {
authenticated: true, authenticated: true,
sub: u.sub, sub: u.sub,
@@ -34,6 +43,9 @@ pub async fn check_auth() -> Result<AuthInfo, ServerFnError> {
name: u.user.name, name: u.user.name,
avatar_url: u.user.avatar_url, avatar_url: u.user.avatar_url,
librechat_url, librechat_url,
langgraph_url,
langflow_url,
langfuse_url,
}) })
} }
None => Ok(AuthInfo::default()), None => Ok(AuthInfo::default()),

View File

@@ -154,6 +154,8 @@ pub struct ServiceUrls {
pub langchain_url: String, pub langchain_url: String,
/// LangGraph service URL. /// LangGraph service URL.
pub langgraph_url: String, pub langgraph_url: String,
/// LangFlow visual workflow builder URL.
pub langflow_url: String,
/// Langfuse observability URL. /// Langfuse observability URL.
pub langfuse_url: String, pub langfuse_url: String,
/// Vector database URL. /// Vector database URL.
@@ -183,6 +185,7 @@ impl ServiceUrls {
.unwrap_or_else(|_| "http://localhost:8888".into()), .unwrap_or_else(|_| "http://localhost:8888".into()),
langchain_url: optional_env("LANGCHAIN_URL"), langchain_url: optional_env("LANGCHAIN_URL"),
langgraph_url: optional_env("LANGGRAPH_URL"), langgraph_url: optional_env("LANGGRAPH_URL"),
langflow_url: optional_env("LANGFLOW_URL"),
langfuse_url: optional_env("LANGFUSE_URL"), langfuse_url: optional_env("LANGFUSE_URL"),
vectordb_url: optional_env("VECTORDB_URL"), vectordb_url: optional_env("VECTORDB_URL"),
s3_url: optional_env("S3_URL"), s3_url: optional_env("S3_URL"),

View File

@@ -3,6 +3,7 @@ mod developer;
mod news; mod news;
mod organization; mod organization;
mod provider; mod provider;
mod services;
mod user; mod user;
pub use chat::*; pub use chat::*;
@@ -10,4 +11,5 @@ pub use developer::*;
pub use news::*; pub use news::*;
pub use organization::*; pub use organization::*;
pub use provider::*; pub use provider::*;
pub use services::*;
pub use user::*; pub use user::*;

43
src/models/services.rs Normal file
View File

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

View File

@@ -24,6 +24,12 @@ pub struct AuthInfo {
pub avatar_url: String, pub avatar_url: String,
/// LibreChat instance URL for the sidebar chat link /// LibreChat instance URL for the sidebar chat link
pub librechat_url: String, 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. /// Per-user LLM provider configuration stored in MongoDB.
@@ -91,6 +97,9 @@ mod tests {
assert_eq!(info.name, ""); assert_eq!(info.name, "");
assert_eq!(info.avatar_url, ""); assert_eq!(info.avatar_url, "");
assert_eq!(info.librechat_url, ""); assert_eq!(info.librechat_url, "");
assert_eq!(info.langgraph_url, "");
assert_eq!(info.langflow_url, "");
assert_eq!(info.langfuse_url, "");
} }
#[test] #[test]
@@ -102,6 +111,9 @@ mod tests {
name: "Test User".into(), name: "Test User".into(),
avatar_url: "https://example.com/avatar.png".into(), avatar_url: "https://example.com/avatar.png".into(),
librechat_url: "https://chat.example.com".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 json = serde_json::to_string(&info).expect("serialize AuthInfo");
let back: AuthInfo = serde_json::from_str(&json).expect("deserialize AuthInfo"); let back: AuthInfo = serde_json::from_str(&json).expect("deserialize AuthInfo");

View File

@@ -1,27 +1,27 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::ToolEmbed;
use crate::i18n::{t, Locale}; 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. /// When `langgraph_url` is configured, embeds the service in an iframe
/// Will eventually integrate with the LangGraph framework. /// with a pop-out button. Otherwise shows a "Not Configured" placeholder.
#[component] #[component]
pub fn AgentsPage() -> Element { pub fn AgentsPage() -> Element {
let locale = use_context::<Signal<Locale>>(); let locale = use_context::<Signal<Locale>>();
let svc = use_context::<Signal<ServiceUrlsContext>>();
let l = *locale.read(); let l = *locale.read();
let url = svc.read().langgraph_url.clone();
rsx! { rsx! {
section { class: "placeholder-page", ToolEmbed {
div { class: "placeholder-card", url,
div { class: "placeholder-icon", "A" } title: t(l, "developer.agents_title"),
h2 { "{t(l, \"developer.agents_title\")}" } description: t(l, "developer.agents_desc"),
p { class: "placeholder-desc", icon: "A",
"{t(l, \"developer.agents_desc\")}" launch_label: t(l, "developer.launch_agents"),
}
button { class: "btn-primary", disabled: true, "{t(l, \"developer.launch_agents\")}" }
span { class: "placeholder-badge", "{t(l, \"common.coming_soon\")}" }
}
} }
} }
} }

View File

@@ -1,16 +1,20 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::ToolEmbed;
use crate::i18n::{t, Locale}; 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, /// Always shows a stats bar with sample metrics. Below that, when
/// plus a mock stats bar showing sample metrics. /// `langfuse_url` is configured, embeds the service in an iframe
/// with a pop-out button. Otherwise shows a "Not Configured" placeholder.
#[component] #[component]
pub fn AnalyticsPage() -> Element { pub fn AnalyticsPage() -> Element {
let locale = use_context::<Signal<Locale>>(); let locale = use_context::<Signal<Locale>>();
let svc = use_context::<Signal<ServiceUrlsContext>>();
let l = *locale.read(); let l = *locale.read();
let url = svc.read().langfuse_url.clone();
let metrics = mock_metrics(l); let metrics = mock_metrics(l);
@@ -21,21 +25,24 @@ pub fn AnalyticsPage() -> Element {
div { class: "analytics-stat", div { class: "analytics-stat",
span { class: "analytics-stat-value", "{metric.value}" } span { class: "analytics-stat-value", "{metric.value}" }
span { class: "analytics-stat-label", "{metric.label}" } 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}%" "{metric.change_pct:+.1}%"
} }
} }
} }
} }
div { class: "placeholder-card", }
div { class: "placeholder-icon", "L" } ToolEmbed {
h2 { "{t(l, \"developer.analytics_title\")}" } url,
p { class: "placeholder-desc", title: t(l, "developer.analytics_title"),
"{t(l, \"developer.analytics_desc\")}" description: t(l, "developer.analytics_desc"),
} icon: "L",
button { class: "btn-primary", disabled: true, "{t(l, \"developer.launch_analytics\")}" } launch_label: t(l, "developer.launch_analytics"),
span { class: "placeholder-badge", "{t(l, \"common.coming_soon\")}" }
}
} }
} }
} }

View File

@@ -1,27 +1,27 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::ToolEmbed;
use crate::i18n::{t, Locale}; 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. /// When `langflow_url` is configured, embeds the service in an iframe
/// Will eventually integrate with LangFlow for visual flow design. /// with a pop-out button. Otherwise shows a "Not Configured" placeholder.
#[component] #[component]
pub fn FlowPage() -> Element { pub fn FlowPage() -> Element {
let locale = use_context::<Signal<Locale>>(); let locale = use_context::<Signal<Locale>>();
let svc = use_context::<Signal<ServiceUrlsContext>>();
let l = *locale.read(); let l = *locale.read();
let url = svc.read().langflow_url.clone();
rsx! { rsx! {
section { class: "placeholder-page", ToolEmbed {
div { class: "placeholder-card", url,
div { class: "placeholder-icon", "F" } title: t(l, "developer.flow_title"),
h2 { "{t(l, \"developer.flow_title\")}" } description: t(l, "developer.flow_desc"),
p { class: "placeholder-desc", icon: "F",
"{t(l, \"developer.flow_desc\")}" launch_label: t(l, "developer.launch_flow"),
}
button { class: "btn-primary", disabled: true, "{t(l, \"developer.launch_flow\")}" }
span { class: "placeholder-badge", "{t(l, \"common.coming_soon\")}" }
}
} }
} }
} }