Generative User Interfaces for Intelligent Web Applications
A full-stack framework for building AI-powered, profile-aware, dynamically generated UI components
Overview β’ Quick Start β’ Components β’ Custom Components β’ Theming β’ Segment Cache β’ Guarantees β’ Auth & Profiles β’ Streaming β’ Uplift β’ API Reference β’ Architecture
GenUI System is a comprehensive framework for building Generative User Interfaces: dynamic, AI-driven UI components that adapt to user profiles, behavior, and context. The system combines a React frontend framework with a Python backend to deliver personalized content in real-time.
|
|
GenUI Studio is the companion web app for building with the framework β a single SPA (studio/, React + Vite) with two tools. Run it locally with cd studio && npm run dev.
Configure the entire --genui-* token dictionary in real time and watch every real framework component (not mockups) update live: hero banners, tabs, pricing, stats, testimonials, bento, charts, and both with-image / text-only variants. Toggle light/dark, tune radius scale, blur, spacing, accent, brand surfaces, heading weight, and font. Export the result as a GenUITheme object, CSS variables, JSON, or copy a shareable link that encodes the theme in the URL.
Manage the RAG knowledge base that feeds the AI: connect to your backend (URL + admin key, stored only in the browser session), drag-and-drop documents (PDF, DOCX, HTML, TXT, MD, images), browse the indexed knowledge base with chunk counts, and test retrieval queries to see exactly which passages the AI would surface, with similarity scores.
Note: the Content Studio requires a reachable backend and an admin key, so for now it runs locally only (
npm run dev). On the public GitHub Pages build it shows an "available locally" notice. A hosted version arrives with proper user auth on the roadmap.
Five steps from zero to a personalized zone on your page. Prerequisites: Python 3.10+, Node 18+, Docker (for Qdrant/Redis), and an OpenAI API key (or Anthropic/Gemini β see step 3).
git clone https://github.com/thevladdo/genui-framework.git
cd genui-framework/backend
# Starts Qdrant (vector store for RAG) and Redis (render cache + profiles).
# Both are optional β without them the backend falls back to in-memory
# storage, fine for a first try, lost on restart.
docker-compose up -d# Still in genui-framework/backend
pip install -r requirements.txt
# For development (running the test suite):
pip install -r requirements-dev.txtcp .env.example .envOpen .env and set one thing to start β your LLM key:
LLM_PROVIDER=openai # openai | anthropic | gemini
OPENAI_API_KEY=sk-... # the only required valueEverything else has sensible defaults. The values you'll likely touch later:
# Cache shared across processes (docker-compose already runs Redis)
REDIS_URL=redis://localhost:6379/0
# Production: API keys ("key:tenant"). WITHOUT THESE THE API IS OPEN (dev only!)
CLIENT_API_KEYS=pk_live_abc:myapp # browser-side key
ADMIN_API_KEYS=sk_live_xyz:myapp # server-to-server key
# Measure personalization uplift (10% of users see the generic version)
HOLDOUT_PERCENT=10
# Other providers instead of OpenAI:
# LLM_PROVIDER=anthropic + ANTHROPIC_API_KEY=... (pip install anthropic)
# LLM_PROVIDER=gemini + GOOGLE_API_KEY=... (no extra package)uvicorn api.main:app --reload --port 8000Verify it's alive:
curl http://localhost:8000/health
# -> {"status": "healthy", ..., "qdrant_connected": true}
# "degraded" just means Qdrant isn't running β zones still work, without RAG.Optional sanity check β render a zone from the terminal:
curl -X POST http://localhost:8000/api/v1/zone/render \
-H "Content-Type: application/json" \
-d '{"zone_id": "test", "base_prompt": "Show three example cards about space exploration"}'You should get JSON with components and a meta.cache block. Run it twice: the second call returns "status": "fresh" β that's the cache working (no LLM call, no cost).
β οΈ The npm package is not yet published. Install locally vianpm link:
cd ../frontend
npm install
npm run build
npm link
# In YOUR app's directory:
npm link genui-frameworkIn your app's entry file (e.g. main.tsx):
import "genui-framework/dist/styles.css";Then drop a zone anywhere:
import { GenUIZone } from "genui-framework";
<GenUIZone
apiUrl="http://localhost:8000"
zoneId="homepage-recommendations"
basePrompt="Show recommended articles"
preferredComponentType="bento"
maxItems={6}
debug // shows reasoning, segment, cache status β remove in production
/>;Open the page: you'll see a loading skeleton, then the generated cards. The debug panel underneath tells you why you're seeing what you're seeing.
cd backend
python3 -m unittest discover -s tests # or: pytest tests/The GenUIZone component automatically fetches personalized content from the backend based on:
- User Profile: Stored preferences, interests, demographics
- Behavior Data: Click patterns, scroll depth, navigation history
- Developer Prompts: Base prompts + context prompts for fine control
- Pinned Content: Guaranteed content that always displays
import { GenUIZone } from "genui-framework";
<GenUIZone
apiUrl="http://localhost:8000"
zoneId="homepage-recommendations"
basePrompt="Show recommended articles"
preferredComponentType="bento"
maxItems={6}
/>;interface GenUIZoneProps {
// === Required ===
apiUrl: string; // Backend API URL
zoneId: string; // Unique zone identifier
// === Auth ===
apiKey?: string; // Client API key (X-API-Key); required when CLIENT_API_KEYS is configured
// === Prompt Engineering ===
basePrompt?: string; // What the zone should display
contextPrompt?: string; // Additional context for AI (page location, user segment, etc.)
// === Content Control ===
pinnedContent?: PinnedContent[]; // Content that MUST be displayed (enforced server-side)
customComponents?: GenUICustomComponentDef[]; // Your design-system components (name + JSON schema)
preferredComponentType?: "bento" | "chart" | "text" | "buttons" | string; // built-in or custom name
maxItems?: number; // Max items to generate (default: 6)
// === User Context ===
userId?: string; // Stable user ID: enables server-side profile, holdout & audit trail
currentPage?: string; // Current page path
pageMetadata?: Record<string, unknown>; // Custom page context (page-level, not per-user!)
// === Behavior ===
loadOnMount?: boolean; // Auto-load on mount (default: true)
refreshInterval?: number; // Auto-refresh in ms (0 = disabled)
cacheStrategy?: "segment" | "live"; // 'segment' (default): per-segment cached renders; 'live': always call the LLM
streaming?: boolean; // Progressive render via SSE (components appear as generated)
trackEvents?: boolean; // Auto impression/click events for uplift measurement (default: true)
// === Theming ===
theme?: GenUITheme; // Theme overrides
className?: string; // CSS class
style?: React.CSSProperties; // Inline styles
// === Custom Render States ===
loadingComponent?: React.ReactNode;
errorComponent?: React.ReactNode | ((error: Error) => React.ReactNode);
emptyComponent?: React.ReactNode; // Shown when AI returns empty
showLoadingSkeleton?: boolean;
// === Callbacks ===
onRender?: (components: GenUIComponent[]) => void;
onError?: (error: Error) => void;
// === Debug ===
debug?: boolean; // Shows reasoning, confidence, profile factors
}Pinned content ensures certain items always appear in the zone, regardless of what the AI generates. The AI will include these items alongside its personalized selections.
interface PinnedContent {
type: "link" | "article" | "document" | "custom";
title: string;
url?: string;
description?: string;
id?: string;
metadata?: Record<string, unknown>;
}<GenUIZone
zoneId="news-feed"
apiUrl="http://localhost:8000"
pinnedContent={[
{
type: "article",
title: "Sustainability Report 2024",
url: "/reports/sustainability-2024",
description: "Our commitment to the environment",
metadata: { category: "sustainability", sponsor: true },
},
{
type: "link",
title: "Investor Relations",
url: "/investors",
description: "Financial information and reports",
},
]}
preferredComponentType="bento"
maxItems={6} // AI will fill remaining slots with personalized content
/>Use contextPrompt to give the AI detailed instructions about the zone's purpose, available content, and selection criteria.
const articlesContext = useMemo(() => {
return articles
.map(
(a, i) =>
`ID ${i}: "${a.title}" (Link: ${a.link}, Img: ${a.src}, Tag: ${a.tag[0]})`,
)
.join("; ");
}, [articles]);
const contextPrompt = `
You are an intelligent content curator for a corporate website.
AVAILABLE CONTENT (Use ONLY these items):
[${articlesContext}]
SELECTION RULES:
1. Select ${maxItems} items that best match the user's profile and interests.
2. If user has interest in "sustainability", prioritize content tagged with that topic.
3. If user role is "investor", prioritize financial and business content.
4. For new users with no profile, show a diverse mix.
OUTPUT REQUIREMENTS:
- Return a 'bento' component with cards.
- Each card MUST use the exact image, title, badge, and link from the input list.
- Do NOT invent new content.
`;
<GenUIZone
zoneId="homepage-for-you"
apiUrl="http://localhost:8000"
basePrompt="Display personalized article recommendations"
contextPrompt={contextPrompt}
preferredComponentType="bento"
maxItems={6}
/>;Pass pageMetadata to give the AI awareness of the current page context:
<GenUIZone
zoneId="related-content"
apiUrl="http://localhost:8000"
currentPage="/products/electric-cars"
pageMetadata={{
pageType: "product",
productCategory: "transportation",
productId: "ETR-500",
userSegment: "business",
region: "europe",
}}
basePrompt="Show related products and content"
/>When the AI returns empty results (e.g., backend unavailable, no matching content), use emptyComponent and errorComponent to display fallback content:
import { GenUIZone, BentoComponent, GenUISection } from "genui-framework";
const fallbackBentoData = {
cards: articles.slice(0, 6).map((a) => ({
title: a.title,
description: a.tag?.[0] || "",
link: a.link || "#",
image: a.src,
badge: a.tag?.[0],
})),
columns: 3,
};
const FallbackBento = () => (
<GenUISection className="genui-layout-complex">
<BentoComponent data={fallbackBentoData} />
</GenUISection>
);
<GenUIZone
zoneId="recommendations"
apiUrl="http://localhost:8000"
emptyComponent={<FallbackBento />}
errorComponent={() => <FallbackBento />}
/>;For chat-based interactions with automatic behavior tracking and profile learning:
import { useGenUI } from "genui-framework";
function ChatBot() {
const {
query, // Send message to AI
isLoading, // Loading state
error, // Last error
profile, // Current user profile
updateProfile, // Manual profile update
clearProfile, // Reset profile
history, // Conversation history
clearHistory, // Clear conversation
behaviorTracker, // Access behavior tracker
trackInteraction, // Track custom events
trackNavigation, // Track page navigation
} = useGenUI({
apiUrl: "http://localhost:8000",
userId: getUserId(),
enablePersistence: true,
enableBehaviorTracking: true,
behaviorTrackingOptions: {
trackClicks: true,
trackScroll: true,
trackPageVisits: true,
trackHover: true,
hoverThreshold: 500, // ms before hover counts
scrollDebounce: 100, // ms debounce
maxEventsPerType: 100, // Memory limit
enableHeatmapZones: true,
},
onProfileUpdate: (profile) => console.log("Profile updated:", profile),
onError: (error) => console.error("GenUI error:", error),
});
const handleSend = async (message: string) => {
try {
const response = await query(message);
// response.text - AI text response
// response.components - Generated UI components
// response.sources - Source citations
// response.suggestedActions - Follow-up suggestions
// response.profileUpdates - Profile learning data
// response.meta - Confidence, sentiment, interaction type
} catch (err) {
// Handle error
}
};
return <ChatUI onSend={handleSend} history={history} loading={isLoading} />;
}For low-level zone control when you need more customization:
import { useZone } from "genui-framework";
const {
components, // Rendered GenUI components
isLoading, // Loading state
error, // Error state
meta, // Render metadata
pinnedContentIncluded, // Which pinned items were included
render, // Manually trigger render
refresh, // Force re-render (clears first)
} = useZone({
apiUrl: "http://localhost:8000",
zoneId: "my-zone",
basePrompt: "Show content",
loadOnMount: true,
refreshInterval: 30000, // Auto-refresh every 30s
});
// Access metadata
console.log(meta?.confidence); // 0.87
console.log(meta?.reasoning); // "Selected based on user interests..."
console.log(meta?.profileFactors); // ["interests.technology", "demographic.role"]
console.log(meta?.personalizationApplied); // true
console.log(meta?.renderId); // "a1b2c3d4e5f6" β identity of the generated variant
console.log(meta?.cache); // { status: "fresh", segment: "role=developer|eng=high", ageSeconds: 42 }
console.log(meta?.experiment); // { arm: "personalized", holdoutPercent: 10 } β when holdout is onA premium card grid with hover animations and responsive layouts:
import { BentoComponent } from "genui-framework";
<BentoComponent
data={{
cards: [
{
title: "Feature One",
description: "Optional description text",
image: "/images/feature1.jpg",
badge: "New", // Top-left badge
link: "/features/one",
action: {
// Optional action button
label: "Learn More",
url: "/features/one",
},
},
// ... more cards
],
columns: 3, // 2, 3, or 4
gap: 16, // Gap in pixels
}}
/>;8 premium button variants with animated arrows:
import { ButtonsComponent } from "genui-framework";
<ButtonsComponent
data={{
buttons: [
{
label: "Get Started",
url: "/start",
style: "shine", // Animated gradient sweep
showArrow: true, // Arrow shows on all buttons by default
arrowPlacement: "right", // "left" or "right"
size: "lg", // "sm" | "md" | "lg"
borderRadius: "8px", // Custom override
backgroundColor: "#3b82f6", // Custom override
textColor: "#ffffff", // Custom override
},
{
label: "Learn More",
style: "outline",
showArrow: false, // Explicitly hide arrow
},
{
label: "Contact",
style: "gooey", // Blob morph on hover
},
{
label: "Explore",
style: "ringHover", // Ring outline on hover
},
{
label: "Details",
style: "expandIcon", // Arrow reveals on hover
},
],
direction: "horizontal", // or "vertical"
align: "center", // "start" | "center" | "end"
gap: 12, // Custom gap in pixels
}}
/>;| Variant | Description |
|---|---|
primary |
Solid accent color with brightness hover |
secondary |
Semi-transparent with backdrop blur |
outline |
Transparent with border, fills on hover |
ghost |
Minimal, text only |
shine |
Animated gradient that sweeps across |
gooey |
Blob morphing effect on hover |
expandIcon |
Arrow icon reveals on hover |
ringHover |
Ring outline appears on hover |
import { ChartComponent } from "genui-framework";
<ChartComponent
data={{
chartType: "bar", // "bar" | "line" | "pie" | "area" | "donut"
title: "Monthly Sales",
data: [
{ label: "Jan", value: 100, color: "#3b82f6" },
{ label: "Feb", value: 150 },
{ label: "Mar", value: 200 },
],
xAxis: "Month",
yAxis: "Sales ($)",
showLegend: true,
showGrid: true,
height: 300,
}}
/>;import { TextComponent } from "genui-framework";
<TextComponent
data={{
content: "This is **markdown** supported text with _emphasis_.",
style: "normal", // "normal" | "emphasis" | "note" | "heading"
}}
/>;Seven section-level components for editorial, e-commerce, insurance, SaaS and corporate portals β same token system, same validation pipeline, all image-optional by design: every image-bearing variant declares layout: "with-image" | "text-only" (or a hero variant), the backend schema enforces coherence (with-image without an image_url is rejected), and the text-only shape is a designed alternative (accent gradients, emphasized typography), never a card with a hole.
| Type | Use case | Image-optional |
|---|---|---|
tabs_feature |
plan comparison, SaaS highlights, product categories | per-tab content.layout |
steps_section |
onboarding, how-it-works, purchase flow (autoplay + progress) | section layout |
stats_banner |
numeric metrics ("10M users") β populate from RAG facts | text-only by design |
testimonial_carousel |
quotes with avatar β initials fallback | avatar optional |
pricing_cards |
plan grid; variant: "detailed" adds a comparison table |
text-only by design |
content_grid |
blog/news cards | per-item layout |
hero_banner |
hero: split (requires image) Β· centered Β· minimal |
variant chain fallback |
{
"type": "hero_banner",
"data": {
"variant": "centered",
"headline": "Coverage that adapts",
"subheadline": "Personalized in real time.",
"primary_cta": { "label": "Get a quote", "url": "/quote" }
}
}New components consume level-2 semantic tokens β rebrand by overriding just these: --genui-surface-1/2/3, --genui-border-subtle/strong, --genui-text-primary/secondary/tertiary/on-accent, --genui-radius-sm/md/lg/full, --genui-shadow-sm/md/lg. Dark is the default; switch any subtree with [data-theme="light"] (or re-assert [data-theme="dark"] when nesting).
The four built-in types cover generic zones, but the real power is letting the LLM generate your components. Registration has two halves:
// 1. Render side: name -> React component
import { registerGenUIComponent } from "genui-framework";
registerGenUIComponent("hero_banner", ({ data }) => (
<HeroBanner
headline={data.headline}
subtitle={data.subtitle}
ctaLabel={data.cta_label}
ctaUrl={data.cta_url}
/>
));
// 2. Generation side: name -> JSON Schema + description (per zone)
<GenUIZone
zoneId="homepage-hero"
apiUrl="..."
preferredComponentType="hero_banner"
customComponents={[
{
name: "hero_banner",
description:
"Full-width hero with headline, subtitle and one CTA. Use as the first component of landing zones.",
dataSchema: {
type: "object",
required: ["headline"],
properties: {
headline: { type: "string", maxLength: 80 },
subtitle: { type: "string" },
cta_label: { type: "string" },
cta_url: { type: "string" },
},
},
},
]}
/>;What the framework guarantees for custom components:
- The JSON Schema is shown to the LLM (name, description, schema, optional
example), so the model knows when and how to use the component. - Generated data is validated against the schema server-side (jsonschema); invalid components are dropped and reported in
meta.sanitization. - The URL whitelist applies recursively: URL-named fields (
url,link,href,src,image,*_url, β¦), absolute URLs and markdown links anywhere in the payload are checked; dangerous schemes are always stripped. - Custom definitions are part of the zone cache key: changing a schema invalidates cached renders automatically.
- The registered React component receives
dataexactly as validated (no key renaming).
Backend embedders can register types globally instead of per request:
from schemas import register_component_type
register_component_type(
"hero_banner",
data_schema={...},
description="Full-width hero with headline and CTA",
)Custom names: 2-32 chars, lowercase
[a-z0-9_-], starting with a letter. Built-in names cannot be overridden.
interface GenUITheme {
borderRadius?: string; // Default: '30px'
primaryColor?: string; // Default: '#fafafa'
secondaryColor?: string; // Default: '#b2b2b2'
backgroundColor?: string; // Default: 'transparent'
textColor?: string;
accentColor?: string; // Used for buttons, highlights
fontFamily?: string;
fontSize?: string;
}Two equivalent ways β pass theme directly to the zone, or wrap a group of zones in a GenUISection:
import { GenUISection, GenUIZone } from 'genui-framework';
const theme = {
borderRadius: '16px',
accentColor: '#3b82f6',
primaryColor: '#1e1e1e',
textColor: '#ffffff',
fontFamily: "'Inter', sans-serif",
};
// Per zone:
<GenUIZone theme={theme} apiUrl="..." zoneId="..." />
// Or shared across several zones:
<GenUISection theme={theme}>
<GenUIZone apiUrl="..." zoneId="hero" />
<GenUIZone apiUrl="..." zoneId="footer" />
</GenUISection>Only the properties you set are emitted; everything else inherits β from an enclosing GenUISection, then from the framework defaults in genui.css (a dark glassmorphism theme). Sections nest cleanly: an inner zone without a theme inherits the outer section's, it does not reset to defaults.
Dark by default. Out of the box the components render on a dark glass theme (light text, dark cards). On a light page background, set
primaryColor/textColorto suit β or override the CSS variables below globally.
The framework's defaults live in :root (override them globally to retheme everything):
:root {
--genui-border-radius: 24px;
--genui-primary-color: #0a0a0c;
--genui-secondary-color: #6b7280;
--genui-accent-color: #3b82f6;
--genui-text-primary: #ffffff;
--genui-text-secondary: rgba(255, 255, 255, 0.8);
--genui-glass-blur: 12px;
--genui-glass-border: 1px solid rgba(255, 255, 255, 0.1);
}By default, zone renders are not generated per user per request. Users are collapsed into a small number of deterministic segments (role, top interests, browsing style, engagement), and each (zone config, segment) pair is rendered once and cached with stale-while-revalidate semantics:
| Cache state | Behavior |
|---|---|
fresh (age β€ ZONE_CACHE_FRESH_TTL) |
Served from cache, no LLM call |
stale (age β€ ZONE_CACHE_STALE_TTL) |
Served instantly from cache, re-rendered in background (single-flight) |
| miss | Rendered live (cold start), then cached for the whole segment |
Anonymous users with no profile signals share a single anon segment β typically the most-hit cache entry. Changing any zone configuration (prompts, pinned content, constraints) automatically invalidates its cache entries.
Use Redis for a shared, persistent cache across processes (REDIS_URL=redis://localhost:6379/0, included in docker-compose.yml); without it, an in-memory fallback is used. The cache always fails open: a cache outage degrades to live rendering.
For genuinely dynamic zones, opt out per zone:
<GenUIZone zoneId="live-dashboard" apiUrl="..." cacheStrategy="live" />Render known archetypes offline (deploy hook, cron) so live traffic only sees cache hits:
POST /api/v1/zone/warmup
Content-Type: application/json
{
"zones": [
{ "zone_id": "homepage-for-you", "base_prompt": "...", "user_profile": null },
{
"zone_id": "homepage-for-you",
"base_prompt": "...",
"user_profile": {
"preferences": { "role": { "value": "developer", "confidence": 1.0 } },
"interests": { "ai": { "value": true, "confidence": 1.0 } }
}
}
]
}Each response's meta.cache reports status (fresh | stale | miss | bypass), the segment key, and age_seconds β visible in the debug panel of GenUIZone. Cache stats are exposed at GET /api/v1/zone/cache/stats.
What reaches the frontend is guaranteed by the system, not by prompt obedience:
- Provider-native structured output β the ZoneAgent constrains generation with
response_format(JSON schema derived from the component schemas, falling back to JSON mode). - Schema validation β every generated component is validated against Pydantic schemas (
backend/schemas/) server-side. Invalid components are dropped individually and reported inmeta.sanitization.dropped_components; one malformed component never breaks the zone. - URL whitelist (hard rule) β a generated URL survives only if it existed in the input: pinned content, developer prompts, RAG documents, or page context. Invented links/images are stripped (
meta.sanitization.removed_urls), buttons left without a valid URL are dropped, markdown links collapse to plain text. Dangerous schemes (javascript:,data:, β¦) are always blocked, even with the whitelist disabled (URL_WHITELIST_ENABLED=false). - Pinned content enforcement β pinned items are verified on the actual output (by URL/title) after generation; missing ones are appended automatically.
pinned_content_includedis computed, not model-claimed. - Frontend defense in depth β rendered
href/srcpass throughsanitizeUrl()regardless of origin.
Because URLs must exist in the input, enumerate your content in
contextPrompt(orpinnedContent/ RAG) β content the model cannot reference, it cannot link.
Two key classes, configured as comma-separated key or key:tenant entries:
CLIENT_API_KEYS=pk_live_abc123:acme,pk_live_def456:globex # shipped to the browser
ADMIN_API_KEYS=sk_live_xyz789:acme # server-to-server only- Client keys identify the calling app/tenant, gate rate limits, and scope cached renders and stored profiles per tenant. Pass them via the
apiKeyprop (sent asX-API-Key;Authorization: Beareralso works). - Admin keys protect
/documents*,/zone/warmup, and/zone/cache/stats. - No keys configured = open API (dev mode, logged loudly). Always configure keys in production.
- Rate limiting:
RATE_LIMIT_PER_MINUTEper client key (default 120,0disables).
<GenUIZone
apiUrl="..."
apiKey="pk_live_abc123"
userId={user.id}
zoneId="home"
/>When userId is provided, the server-side profile store (Redis, or in-memory in dev) is authoritative:
- An existing server profile overrides the client-supplied one.
- With no server profile yet, the client (IndexedDB) copy seeds the store β IndexedDB is thereby demoted to a cache.
- Agent-extracted profile updates are merged server-side (higher confidence wins) on every
/query. - Endpoints:
GET /api/v1/profile/{user_id},POST /api/v1/profile/sync, andDELETE /api/v1/profile/{user_id}(GDPR erasure, audit-logged). - Retention:
PROFILE_TTL_SECONDS(e.g.7776000= auto-expire after 90 days of inactivity).
Every zone render, query, profile sync, and profile deletion emits an append-only JSON event (AUDIT_LOG_PATH file, or the genui.audit logger): tenant, user, zone, segment, cache state, and the exact titles/links displayed. In regulated sectors this answers "why did user X see content Y on date Z?". API keys appear only as fingerprints, never raw.
{
"ts": "2026-06-10T10:30:00+0000",
"event": "zone_render",
"tenant": "acme",
"user_id": "u42",
"zone_id": "homepage-for-you",
"cache": { "status": "fresh", "segment": "role=developer|eng=high" },
"shown_titles": ["API Docs", "Case Study"],
"shown_links": ["/docs/api", "/cases/1"]
}With streaming enabled, components appear one by one as the model generates them, instead of waiting for the full response:
<GenUIZone zoneId="live-feed" apiUrl="..." cacheStrategy="live" streaming />Under the hood the zone consumes POST /api/v1/zone/render/stream (Server-Sent Events): each component event is already validated and URL-sanitized before being emitted; the final complete event carries the authoritative response (including pinned-content enforcement) and replaces the streamed state. Cache hits stream their components in a single burst, so streaming is most useful for cacheStrategy="live" zones. Holdout, audit log, and caching behave exactly like the non-streaming endpoint.
The library can be imported and rendered in server environments (Next.js, Remix, Astro): CSS is shipped as a separate file (no style injection at import time), IndexedDB persistence degrades to a no-op without a browser, and the BehaviorTracker won't attach listeners without a DOM. Zone fetches run in effects, so server-rendered markup shows your loadingComponent/skeleton and hydrates normally. A first-class SSR adapter (zone data fetched server-side) is on the roadmap as a separate package.
Personalization is only worth its cost if it beats your static page. The framework closes the loop natively:
With trackEvents (default true), every GenUIZone:
- emits an impression when the zone enters the viewport (once per generated variant), and
- captures clicks on any link inside the zone (title + URL),
sending them to POST /api/v1/events tagged with the variant identity (render_id), the experiment arm, and the segment. Custom events (e.g. conversions) can be sent with sendGenUIEvents().
HOLDOUT_PERCENT=10 # 10% of identified users get the generic render
HOLDOUT_SALT=genui-exp-1 # change to start a new experiment (reshuffles arms)Assignment is a sticky hash of user_id: the same user always lands in the same arm, across sessions and servers. Control users are served the non-personalized render (profile and behavior stripped β they share the generic cached variant); anonymous users are excluded (arm: "none") since without a stable identity the comparison would be contaminated. The arm is exposed in meta.experiment.arm, so the frontend can also choose to render its own static fallback for control users.
GET /api/v1/events/stats?zone_id=homepage-for-you (admin key){
"zone_id": "homepage-for-you",
"arms": {
"personalized": { "impression": 5400, "click": 540, "ctr": 0.1 },
"control": { "impression": 600, "click": 30, "ctr": 0.05 }
},
"uplift_percent": 100.0,
"significance": {
"method": "two-proportion z-test (two-tailed)",
"z_score": 3.94,
"p_value": 0.00008,
"significant_95": true,
"sample_warning": false
},
"holdout_percent": 10
}uplift_percent is the headline number; significance tells you whether to believe it β a two-proportion z-test between arms (significant_95: true means p < 0.05; sample_warning flags arms under 100 impressions, where any conclusion is preliminary). Raw events also land in the audit log for offline slicing (per segment, per item, per time window).
Set TRACING_ENABLED=true for OpenTelemetry tracing: FastAPI requests, genui.zone.render spans (zone, tenant, segment, cache status, experiment arm) and genui.llm.* spans (provider, model). Point OTLP_ENDPOINT at a collector (Jaeger, Grafana Tempo, ...) or omit it for console output.
The framework automatically tracks user behavior and sends it to the backend for personalization:
| Event Type | What's Tracked |
|---|---|
| Clicks | Element ID, type, page, coordinates |
| Scrolls | Depth percentage, direction, velocity |
| Hovers | Element ID, duration, timeout threshold |
| Navigation | Page path, title, timestamp |
| Zone Views | When GenUI zones enter viewport |
const { trackInteraction, trackNavigation } = useGenUI({ ... });
// Track custom element interaction
<button
onClick={() => {
trackInteraction('cta-signup', 'button', 'click', {
source: 'header',
campaign: 'summer-sale'
});
}}
>
Sign Up
</button>
// Track SPA navigation
function navigateTo(path: string) {
trackNavigation(path, document.title);
router.push(path);
}The knowledge base feeds the AI real content to curate (and its URLs feed the whitelist). Every operation is scoped to the tenant of the API key: tenant A can never retrieve, list, or delete tenant B's documents. Documents indexed before tenant isolation belong to the default tenant. All endpoints require an admin key.
| Endpoint | What it does |
|---|---|
POST /api/v1/documents/upload |
Upload a file (PDF, DOCX, HTML, TXT, MD β max 10 MB, multipart): text extracted server-side, semantically chunked, indexed. Images (PNG/JPG/WEBP/TIFF) too with a capable extractor backend |
POST /api/v1/documents |
Upload raw text (JSON: content + metadata) |
GET /api/v1/documents |
List the tenant's documents with chunk counts |
POST /api/v1/documents/search |
Preview what the AI would retrieve for a query (passages + similarity scores) β content debugging |
DELETE /api/v1/documents/{source} |
Delete a document (tenant-scoped, audit-logged) |
GET /api/v1/documents/stats |
Collection stats incl. the tenant's chunk count |
# Upload a PDF (url becomes linkable by the AI via the whitelist)
curl -X POST http://localhost:8000/api/v1/documents/upload \
-H "X-API-Key: sk_live_xyz789" \
-F "file=@./sustainability-report.pdf" \
-F "title=Sustainability Report 2026" \
-F "url=/reports/sustainability-2026"
# What would the AI see for this query?
curl -X POST http://localhost:8000/api/v1/documents/search \
-H "X-API-Key: sk_live_xyz789" -H "Content-Type: application/json" \
-d '{"query": "renewable energy initiatives", "top_k": 5}'EXTRACTOR_BACKEND=local # default: pypdf/docx/bs4 β zero dependencies, data stays in-house
# EXTRACTOR_BACKEND=docling # local upgrade, no GPU: better tables/layout + images (pip install docling)
# EXTRACTOR_BACKEND=glmocr # state-of-the-art incl. scanned docs (pip install glmocr)
# GLMOCR_BASE_URL=... # self-hosted GLM-OCR (vLLM/Ollama, ~2-4GB VRAM): data stays in-house
# GLMOCR_API_KEY=... # Z.ai cloud API: documents LEAVE your infra β opt-in consciouslyRouting is per-format: plain text always decodes locally; a backend only handles the formats it excels at (Docling: PDF/DOCX/HTML/images; GLM-OCR: PDF/images) and everything else falls through to the local parsers. Runtime failures of a backend fall back to local with a warning; a configured backend with a missing package fails loudly (501) β that's a deployment mistake, not something to hide. The audit log records which extractor produced each document.
Notes: embeddings always use OpenAI (
text-embedding-3-small), soOPENAI_API_KEYis required for RAG even whenLLM_PROVIDER=anthropic. Scanned PDFs needdoclingorglmocrβ the local backend cannot OCR.
POST /api/v1/query
Content-Type: application/json
{
"query": "What products do you recommend?",
"user_profile": {
"preferences": { "role": { "value": "investor", "confidence": 0.9 } },
"interests": { "sustainability": { "value": true, "confidence": 0.8 } },
"demographic": { "region": { "value": "europe", "confidence": 0.7 } }
},
"conversation_history": [
{ "role": "user", "content": "Hello" },
{ "role": "assistant", "content": "Hi! How can I help?" }
],
"behavior_data": {
"clickCount": 15,
"maxScrollDepth": 85,
"userType": "deep_reader",
"navigationPath": ["/", "/products", "/products/trains"]
}
}Response:
{
"text": "Based on your interest in sustainability, I recommend...",
"components": [
{
"type": "bento",
"data": { "cards": [...], "columns": 3 }
}
],
"sources": [
{ "title": "Sustainability Report", "url": "/reports/sustainability" }
],
"suggested_actions": ["View all products", "Contact sales"],
"profile_updates": {
"should_update": true,
"updates": [
{ "field": "interests.products", "value": "trains", "confidence": 0.75 }
]
},
"meta": {
"confidence": 0.92,
"interaction_type": "question",
"topics": ["products", "recommendations"],
"sentiment": "positive"
}
}POST /api/v1/zone/render
Content-Type: application/json
{
"zone_id": "homepage-recommendations",
"base_prompt": "Show recommended articles for the user",
"context_prompt": "User is on the homepage, interested in technology and sustainability",
"pinned_content": [
{ "type": "article", "title": "Annual Report", "url": "/reports/annual" }
],
"preferred_component_type": "bento",
"max_items": 6,
"user_profile": { ... },
"behavior_data": { ... },
"current_page": "/",
"page_metadata": { "section": "hero", "campaign": "summer-2024" },
"cache_strategy": "segment"
}Response:
{
"zone_id": "homepage-recommendations",
"components": [
{
"type": "bento",
"data": {
"cards": [
{ "title": "Annual Report", "link": "/reports/annual", ... },
{ "title": "Green Initiative", "link": "/sustainability", ... }
],
"columns": 3
}
}
],
"pinned_content_included": ["/reports/annual"],
"personalization_applied": true,
"meta": {
"confidence": 0.87,
"reasoning": "Selected sustainability and tech content based on user profile",
"profile_factors": ["interests.sustainability", "interests.technology"],
"cache": {
"status": "fresh",
"strategy": "segment",
"segment": "int=sustainability+technology",
"age_seconds": 42.3
}
},
"rendered_at": "2024-01-15T10:30:00Z"
}genui-framework/
βββ backend/ # Python FastAPI backend
β βββ agents/ # AI agent implementations
β β βββ zone_agent.py # Zone rendering (validation, URL guard,
β β β # pinned enforcement, streaming)
β β βββ response_agent.py # Chat response generation (datapizza)
β β βββ profile_agent.py # Profile learning & extraction
β β βββ behave_agent.py # Behavior analysis
β β βββ orchestrator.py # Multi-agent coordination (chat)
β βββ api/ # REST API endpoints
β β βββ main.py # FastAPI app, query/documents/profile endpoints
β β βββ zone_router.py # Zone render + stream + warmup + cache stats
β β βββ events_router.py # UI event ingestion + uplift stats
β β βββ deps.py # Shared service singletons
β βββ auth/ # API keys, tenants, FastAPI dependencies
β βββ llm/ # Provider abstraction (OpenAI / Anthropic / Gemini)
β βββ schemas/ # Component schemas (Pydantic) + custom type registry
β βββ segmentation/ # Deterministic profile -> segment mapping
β βββ profiles/ # Server-side profile store + merge logic
β βββ experiments/ # Holdout arm assignment
β βββ metrics/ # Impression/click counters + z-test significance
β βββ rag/ # Qdrant vector store + chunking
β βββ utils/ # zone_cache (SWR), url_guard, audit, rate_limit,
β β # json_stream (SSE parser), tracing
β βββ config/settings.py # All env-driven configuration
β βββ tests/ # 136 unit tests (unittest/pytest compatible)
β βββ docker-compose.yml # Qdrant + Redis
β
βββ frontend/ # React component library (npm package)
βββ src/
β βββ components/ # GenUIZone, GenUISection, Bento/Buttons/Chart/Text,
β β # ComponentRenderer (with custom-component fallback)
β βββ hooks/ # useZone (cache/streaming/events), useGenUI (chat)
β βββ registry.ts # registerGenUIComponent (custom design systems)
β βββ styles/genui.css # Glassmorphism theme, animations
β βββ types/ # TypeScript definitions
β βββ utils/ # indexeddb (SSR-safe), behaviorTracker,
β # sanitizeUrl, genuiEvents, sse
βββ dist/ # Built output (+ styles.css)
βββ rollup.config.js
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FRONTEND (React) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββ β
β β GenUIZone β β useGenUI β β BehaviorTracker β β
β β (zones) β β (chat) β β (analytics) β β
β ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββββ¬βββββββββ β
β β β β β
β β βββββββββββββββ΄ββββββββββββββ β β
β β β IndexedDB β β β
β β β - User Profile ββββββββ β
β β β - Conversation History β β
β β βββββββββββββββ¬ββββββββββββββ β
β β β β
β ββββββββββ¬ββββββββββ β
β β β
β βΌ β
β HTTP POST /api/v1/zone/render or /api/v1/query β
β { zone_id, prompts, user_profile, behavior_data, pinned_content } β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BACKEND (FastAPI) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββ β
β β Router β β
β β zone_router β β
β ββββββββ¬ββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β AGENT SYSTEM β β
β β β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β β ZoneAgent β βResponseAgent β β ProfileAgent β β β
β β β (zone render)β β (chat) β β (learning) β β β
β β ββββββββ¬ββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β β β β
β β βΌ β β
β β ββββββββββββββββ ββββββββββββββββ β β
β β β RAG System β β LLM API β β β
β β β (Qdrant) β β (OpenAI) β β β
β β ββββββββββββββββ ββββββββββββββββ β β
β β β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
JSON Response: { components, meta, ... }
- Auth & rate limit β the API key resolves the tenant; client keys are rate-limited.
- Profile resolution β with a
user_id, the server-side profile overrides the client copy (or gets seeded by it). - Holdout assignment β with
HOLDOUT_PERCENTset, a sticky hash sends X% of users to the control arm (signals stripped). - Segmentation β profile + behavior collapse into a deterministic segment key (
role=developer|int=ai|eng=high). - Cache lookup β fresh hit: served, no LLM. Stale: served + refreshed in background. Miss: continue.
- Generation β provider-agnostic LLM call with structured output (prompt includes profile, RAG content, pinned items, custom component schemas).
- Guarantees β per-component schema validation, URL whitelist, pinned-content enforcement.
- Cache write + audit β the render is cached for the whole segment and audit-logged (what was shown, to whom, why).
| Agent | File | Purpose |
|---|---|---|
| ZoneAgent | zone_agent.py |
Zone rendering: prompts + profile + RAG β validated, sanitized components (provider-agnostic, streaming-capable) |
| ResponseAgent | response_agent.py |
Chat responses with optional UI components (datapizza, RAG tools) |
| ProfileAgent | profile_agent.py |
Extracts user preferences from conversations |
| BehaveAgent | behave_agent.py |
Analyzes behavior patterns for UI adjustments |
| Orchestrator | orchestrator.py |
Runs Response/Profile/Behave agents in parallel for /query |
| Module | Purpose | Key Exports |
|---|---|---|
| components/ | React UI components | GenUIZone, BentoComponent, ButtonsComponent, ChartComponent, TextComponent, ComponentRenderer, GenUISection |
| hooks/ | React hooks for state & API | useGenUI, useZone |
| types/ | TypeScript definitions | GenUITheme, BentoCard, ButtonDef, ButtonVariant, UserProfile, GenUIResponse, etc. |
| utils/ | Utilities | BehaviorTracker, profile/history persistence functions |
| styles/ | CSS | Glassmorphism theme, animations, responsive layouts |
GenUI's chat pipeline (ResponseAgent, ProfileAgent, BehaveAgent) is built on top of datapizza-ai, a Python framework for building reliable Gen AI solutions without overhead. The ZoneAgent uses its own provider abstraction (backend/llm/) for structured output and streaming.
- Integration with AI Providers: Seamlessly connect with OpenAI, Google VertexAI, Anthropic, Mistral, and more
- Complex workflows, minimal code: Design, automate, and scale powerful agent workflows without boilerplate
- Retrieval-Augmented Generation (RAG): Built-in support for Qdrant, Milvus vector stores
- Up to 40% less debugging time: Trace and log every LLM/tool call with inputs/outputs
- MCP Support: Model Context Protocol integration for advanced tool usage
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
GenUI System Intelligent interfaces that adapt to every user
Built with β€οΈ for the personalized web



