Dash UI Architektur
Die Benutzeroberfläche von MarketingGPT basiert auf Plotly Dash, einem Python-Framework für interaktive Web-Apps. Diese Dokumentation erklärt die Kernarchitektur und Best Practices für die Entwicklung neuer Seiten und Features.
1. Multi-Page Architektur
MarketingGPT nutzt Dash's built-in Multi-Page System, um 15+ verschiedene Funktionalitäten zu organisieren. Jede Seite ist eine eigenständige Python-Datei, die sich selbst registriert.
App-Initialisierung
In marketinggpt/app.py wird die Dash-App mit Multi-Page Support initialisiert:
app = dash.Dash(
external_stylesheets=[dbc.themes.BOOTSTRAP, FONT_AWESOME],
use_pages=True, # Auto-Discovery von Pages
prevent_initial_callbacks="initial_duplicate",
server=server
)
Wichtig: use_pages=True aktiviert Dash's automatische Page-Discovery. Alle Dateien im pages/ Ordner werden automatisch als eigene Seiten registriert.
Neue Page registrieren
Um eine neue Seite hinzuzufügen:
- Erstelle eine neue Datei in
marketinggpt/pages/meine_seite.py - Registriere die Seite mit
dash.register_page() - Definiere das Layout
- (Optional) Registriere Flask Routes für Backend-APIs
Beispiel:
# marketinggpt/pages/meine_seite.py
import dash
from dash import html, callback
from dash.dependencies import Input, Output
# Registriere die Seite — wird automatisch unter /meine_seite geroutet
dash.register_page(__name__)
layout = html.Div([
html.H1("Meine neue Seite"),
html.Button("Klick mich", id="my-button"),
html.Div(id="output"),
])
@callback(
Output("output", "children"),
Input("my-button", "n_clicks")
)
def update_output(n_clicks):
return f"Du hast {n_clicks or 0}x geklickt"
def setup_routes(router):
"""Optional: Backend Flask Routes"""
@router.route('/meine_api', methods=['POST'])
def meine_api():
return {"success": True}, 200
Routing & URL-Prefix
Dash generiert automatisch URLs basierend auf dem Dateinamen:
| Dateiname | URL |
|---|---|
pages/home.py |
/ (Special Case) |
pages/generation.py |
/generation |
pages/editor.py |
/editor |
pages/library_management.py |
/library_management |
Wichtig: Verwende in Links immer den get_prefix() Helper, um URL-Prefix Handling zu berücksichtigen:
from pages.home import get_prefix
href = get_prefix() + "/generation" # Funktioniert auch mit URL-Prefix
Übersicht: Multi-Page Flow
graph LR
A["app.py<br/>use_pages=True"] --> B["Auto-Discovery<br/>pages/ Ordner"]
B --> C["dash.register_page<br/>in jeder Page"]
C --> D["URL Routing<br/>/generation, /editor, ..."]
D --> E["Sidebar Navigation<br/>Dynamic Filtering"]
E --> F["User klickt Link<br/>Page lädt"]
F --> G["Layout + Callbacks<br/>werden ausgeführt"]
2. Role-Based Access Control (RBAC)
MarketingGPT implementiert rollenbasierte Zugriffskontrolle, um nur berechtigte User bestimmte Seiten sehen können.
RBAC Config in app.py
In app.py gibt es ein pages Dictionary, das alle verfügbaren Seiten und ihre Berechtigungen definiert:
pages = {
"home": {"url": "/", "name": "Home", "allowed_roles": ["all"]},
"search_filter": {
"url": "/search_filter",
"name": "Search by Smart Filters",
"allowed_roles": ["admin", "author", "sales", "engineering", "stakeholder"],
},
"discover_with_ai": {
"url": "/discover_with_ai",
"name": "Discover the Database with AI",
"allowed_roles": ["admin"], # Nur Admins
},
# ... weitere Seiten
}
RBAC Enforcement: has_page_access()
Die Funktion has_page_access() in python_functions/energyai_elements.py prüft ob ein User auf eine Seite Zugriff hat:
def has_page_access(page, username):
"""
Prüft ob ein User auf eine Seite zugreifen darf.
- Admins (in ADMIN_USERS) haben Zugriff auf alles
- Normale User: Rollen-Check
"""
if username in ADMIN_USERS:
return True # Admins: All Access
page_roles = page.get("allowed_roles", [])
user_roles = STREAMS["USERS"].get(username, {}).get("roles", [])
return any(role in page_roles for role in user_roles)
Sidebar Filtering
Die Sidebar zeigt nur Seiten an, auf die der User Zugriff hat. In app.py:
visible_pages = [p for p in pages.values() if has_page_access(p, USERNAME)]
Neue Seite mit RBAC hinzufügen
-
Füge Seite zum
pagesDictionary in app.py hinzu:"meine_admin_seite": { "url": "/meine_admin_seite", "name": "Nur für Admins", "allowed_roles": ["admin"], }, -
Erstelle die Page-Datei
marketinggpt/pages/meine_admin_seite.py(siehe Multi-Page Architektur) -
Optional: Sicherheits-Check in Backend (siehe oben)
3. Callbacks & State Management Patterns
Dash verwendet Callbacks um interaktive Verhaltensweisen zu definieren. In MarketingGPT gibt es zwei verschiedene Callback-Patterns, je nachdem wie komplex die UI ist.
Pattern A: Client-Side Callbacks mit iFrames
Wo verwendet: generation.py, translate.py, review.py, paraphrase.py, search_filter.py (~60% der Seiten)
Wann sinnvoll:
- Komplexe, dynamische UIs (Editoren, Drag-Drop, Rich Text)
- Streaming Responses (Echtzeit-Updates)
- Viel JavaScript für Interaktionen
Struktur:
# marketinggpt/pages/generation.py
from dash import html
layout = html.Div([
html.Iframe(
id="generationIframe",
src='flask/generation/generation.html',
style={'minWidth': '100%', 'height': '90vh', 'border': 'none'},
),
])
# Client-Side Callback: Resize iFrame bei Route-Change
this_app.clientside_callback(
"""function(pathname) {
iFrameResize({ log: false }, '#generationIframe');
return window.dash_clientside.no_update;
}""",
Output('generationIframe', 'src'),
Input('url', 'pathname'),
prevent_initial_call=False
)
Backend (Flask Route):
Die eigentliche UI-Logik lebt in statischen HTML/JavaScript Files (flask/generation/generation.html). Diese kommunizieren mit Python via Flask Routes:
# In marketinggpt/pages/generation.py
def setup_routes(router):
@router.route('/get_generation_prompt', methods=['POST'])
def get_generation_prompt():
# Backend: Prompt aufbauen, LLM aufrufen, Streaming starten
connectionID = hashlib.sha256(str(time.time()).encode()).hexdigest()
start_stream_oai(connectionID, prompt, systemMessage, deployment_name)
return jsonify({'CID': connectionID}), 200
@router.route('/poll_stream/<connection_id>', methods=['GET'])
def poll_stream(connection_id):
# Frontend pollt diesen Endpoint für Streaming Updates
if connection_id in STREAMS:
return jsonify({"data": STREAMS[connection_id]}), 200
return jsonify({"data": ""}), 200
Frontend (JavaScript in iFrame):
// flask/generation/generation.html
fetch('/api/get_generation_prompt', {
method: 'POST',
body: JSON.stringify({prompt: userInput})
})
.then(r => r.json())
.then(data => {
const connectionID = data.CID;
// Poll für Streaming Updates
pollStream(connectionID);
});
function pollStream(cid) {
fetch(`/api/poll_stream/${cid}`)
.then(r => r.json())
.then(data => {
document.getElementById('output').textContent += data.data;
if (!data.finished) pollStream(cid);
});
}
Pattern B: Server-Side Callbacks
Wo verwendet: home.py, library_management.py, global_http.py
Wann sinnvoll: - Einfache UIs (Tabellen, Buttons, Forms) - State Management in Python - Keine Streaming-Anforderungen
Basic Callback (Input → Output)
# marketinggpt/pages/home.py
from dash import callback
@callback(
Output("homeContent", "children"),
Input("expanded_tile", "data"),
)
def update_home_content(expanded_id):
# Wird ausgelöst wenn "expanded_tile" sich ändert
return build_home_content(expanded_id)
Callback mit State (Input + State → Output)
@callback(
Output("output", "children"),
Input("submit_button", "n_clicks"),
State("input_field", "value"), # State wird gelesen aber triggert nicht
prevent_initial_call=True,
)
def handle_submit(n_clicks, input_value):
# Wird nur ausgelöst wenn Button geklickt
# input_value wird gelesen aber ändert sich nicht
return f"Du hast eingegeben: {input_value}"
Pattern-Matched Callbacks (dash.ALL)
Für dynamische Component-Listen verwendest du Pattern-Matched Callbacks:
# marketinggpt/pages/home.py
@callback(
Output("expanded_tile", "data"),
[Input({"type": "tilecard-btn", "index": dash.ALL}, "n_clicks")],
[State("expanded_tile", "data")],
prevent_initial_call=True,
)
def expand_tile(n_clicks_list, expanded_id):
ctx = dash.callback_context
if not ctx.triggered:
return expanded_id
# Extrahiere welcher Button geklickt wurde
triggered_id = json.loads(ctx.triggered[0]["prop_id"].split(".")[0])
tile_id = triggered_id["index"]
# Toggle: Wenn gleiche Tile nochmal geklickt, zusammenklappen
return None if expanded_id == tile_id else tile_id
Wie die Buttons erzeugt werden:
def TileCard(tile, expanded_id):
return html.Div([
html.Button(
tile["title"],
id={"type": "tilecard-btn", "index": tile["id"]},
n_clicks=0,
),
# Content wenn expanded
html.Div(...) if expanded_id == tile["id"] else None
])
Multiple Outputs mit allow_duplicate
Manche Callbacks müssen mehrere Outputs aktualisieren:
# marketinggpt/pages/library_management.py
@callback(
Output("library", "data", allow_duplicate=True), # Primary output
[Output(f"FLAG_LIBRARY_{lib}", "data") for lib in LIBRARY_LIST], # Extra outputs
Input("library-saveChanges", "n_clicks"),
State("library", "data"),
prevent_initial_call=True,
)
def update_library(n_clicks, table_data):
# Aktualisiere Haupt-Store und triggere andere Callbacks
new_lib_data = process_library(table_data)
library_flags = [1 for _ in LIBRARY_LIST] # Trigger andere Callbacks
return [new_lib_data] + library_flags
4. State Management
State in Dash ist verteilt über 3 Layer.
Layer 1: dcc.Store (Lokal, Single-Page)
# marketinggpt/app.py
dcc.Store(id="expanded_tile", data=None), # Welche Home-Tile ist expanded?
dcc.Store(id="side_click", data=0), # Ist Sidebar offen oder zugeklappt?
Wann verwenden:
- Einfache, primitive Werte (String, Number, Boolean)
- Nur auf einer Seite benötigt
- Kleine Datenmengen
Beispiel:
@callback(
Output("expanded_tile", "data"),
Input({"type": "tilecard-btn", "index": dash.ALL}, "n_clicks"),
State("expanded_tile", "data"),
)
def expand_tile(n_clicks, expanded_id):
return new_expanded_id # Wird in dcc.Store gespeichert
Layer 2: session_storage (Cross-Page)
# marketinggpt/app.py
dcc.Store(id="session_storage", data={
"document_translate": {},
"document_review": {},
"create_draft": {},
"review_draft": {},
})
Wann verwenden:
- State zwischen mehreren Seiten teilen
- Beispiel: User wählt Dokument auf Search-Seite → will es auf Review-Seite reviewen
- Session-basiert (verschwindet nach Reload)
Beispiel:
# Auf search_filter.py: Speichere ausgewähltes Dokument
@callback(
Output("session_storage", "data"),
Input("select-document-btn", "n_clicks"),
State("session_storage", "data"),
State("document-id", "value"),
)
def select_document(n_clicks, session_data, doc_id):
session_data["document_translate"] = {"id": doc_id}
return session_data
# Auf review.py: Lese ausgewähltes Dokument
@callback(
Output("review-content", "children"),
Input("url", "pathname"), # Wenn auf diese Seite navigiert
State("session_storage", "data"),
)
def load_review(pathname, session_data):
doc = session_data.get("document_review", {})
return display_document(doc)
Layer 3: FLAG_* Stores (Anti-Pattern — nur als Workaround)
# marketinggpt/app.py
dcc.Store(id="FLAG_LIBRARY_MANAGER", data=0),
dcc.Store(id="FLAG_SEARCH_FILTER", data=0),
Wann verwenden: Nur wenn du andere Callbacks zwingen willst sich zu neu-ausführen.
Beispiel (Anti-Pattern):
# Callback A: Aktualisiere Daten
@callback(
Output("data", "data"),
Output("FLAG_LIBRARY", "data"), # Increment to trigger other callbacks
Input("save-btn", "n_clicks"),
)
def save_data(n_clicks):
return new_data, (n_clicks or 0) # Ändere FLAG um andere Callbacks zu triggern
# Callback B: Re-execute wenn FLAG sich ändert
@callback(
Output("table", "children"),
Input("FLAG_LIBRARY", "data"), # Wird triggered wenn FLAG sich ändert
)
def refresh_table(flag):
return load_table_from_database()
5. Navigation & Komponenten
Home Dashboard: Tile-Based Navigation
Die Home-Seite (pages/home.py) nutzt eine Tile-basierte Navigationt, um die Funktionalitäten zu organisieren.
TILE_CONFIG Definition:
# marketinggpt/pages/home.py
TILE_CONFIG = [
{
"id": "search",
"title": "Product & Document Search",
"icon": "fas fa-search",
"allowed_roles": ["admin", "author", "sales", "engineering", "stakeholder"],
"subpages": [
{
"label": "Search by Smart Filters",
"route": "/search_filter",
"color": "#f1c40f",
"allowed_roles": ["admin", "author", "sales", "engineering", "stakeholder"],
},
{
"label": "Search by Document",
"route": "/search_documents",
"color": "#e74c3c",
"allowed_roles": ["admin", "author", "sales", "engineering", "stakeholder"],
},
],
},
# ... weitere Tiles
]
Funktionsweise:
graph TD
A["User öffnet Home"] --> B["TILE_CONFIG laden"]
B --> C["Pro Tile: Subpages nach Role filtern"]
C --> D["TileCard rendern mit Buttons"]
D --> E["User klickt Tile-Button"]
E --> F["Tile expanded/collapsed"]
F --> G["Subpage-Links werden sichtbar"]
G --> H["User klickt Subpage-Link"]
H --> I["Navigiere zu Seite"]
TileCard Component
# marketinggpt/pages/home.py
def TileCard(tile, expanded_id, username):
# Filter: Zeige nur Subpages auf die User Zugriff hat
visible_subpages = [
sp for sp in tile["subpages"]
if has_page_access(sp, username)
]
if not visible_subpages:
return None # Ganze Tile verstecken wenn keine Subpages sichtbar
is_expanded = expanded_id == tile["id"]
return html.Div([
# Tile-Button
html.Button(
[
html.I(className=tile["icon"]),
html.Span(tile["title"]),
],
id={"type": "tilecard-btn", "index": tile["id"]},
className="tile-button",
),
# Subpages (nur wenn expanded)
html.Div([
SubActionButton(sp["label"], sp["route"], sp["color"])
for sp in visible_subpages
], className="subpages") if is_expanded else None,
])
SubActionButton Component
Wiederverwendbarer Component für Subpage-Links mit Farben:
# marketinggpt/pages/home.py
def SubActionButton(label, route, color, btn_id=None):
return html.A([
html.Span(className="subaction-color", style={"background": color}),
html.Span(label),
],
href=get_prefix() + route,
className="subaction-btn",
)
Sidebar Navigation
Die Sidebar zeigt alle verfügbaren Seiten und erlaubt Toggle zwischen Icon-Only und Full-Text View.
Sidebar Toggle Callback app.py:
@app.callback(
[
Output("sidebar", "style"),
Output("page-content", "style"),
],
[Input("sidebar-toggle", "n_clicks")],
[State("side_click", "data")],
)
def toggle_sidebar(n, nclick):
"""Toggle zwischen collapsed (50px) und expanded (200px) Sidebar"""
if n:
if nclick:
# Gerade expanded → collapse
return [SIDEBAR_HIDDEN, CONTENT_STYLE_COLLAPSED]
else:
# Gerade collapsed → expand
return [SIDEBAR_STYLE, CONTENT_STYLE]
return [SIDEBAR_STYLE, CONTENT_STYLE]
Sidebar Aufbau:
# marketinggpt/app.py
sidebar = html.Div([
# Toggle Button
html.Button(
html.I(className="fas fa-bars"),
id="sidebar-toggle",
n_clicks=0,
),
# Navigations-Links
html.Div(
[
html.A(
page["name"],
href=prefix + page["url"],
className="nav-link",
)
for page in visible_pages
],
id="sidebarNames",
),
# Store für Toggle-State
dcc.Store(id="side_click", data=0),
])
Neue Tile hinzufügen
Um eine neue Kategorie auf der Home-Seite hinzuzufügen:
-
Füge Tile zu TILE_CONFIG hinzu pages/home.py:
{ "id": "meine_kategorie", "title": "Meine Kategorie", "icon": "fas fa-star", # Font Awesome Icon "allowed_roles": ["admin", "author"], "subpages": [ { "label": "Subseite 1", "route": "/meine_seite", "color": "#3498db", "allowed_roles": ["admin", "author"], }, ], } -
Erstelle die neue Seite (siehe Multi-Page Architektur oben)
-
Speichern & Testen