aktueller stand

This commit is contained in:
2026-02-03 12:48:43 +01:00
parent e1ecd1a38c
commit b50594dbfa
77 changed files with 19139 additions and 0 deletions

912
src/tui/app.py Normal file
View File

@@ -0,0 +1,912 @@
"""Main TUI application using Textual."""
import threading
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
from textual.widgets import Header, Footer, Button, Static, Input, Label, TextArea, OptionList, LoadingIndicator, ProgressBar
from textual.widgets.option_list import Option
from textual.binding import Binding
from textual.screen import Screen
from textual.worker import Worker, WorkerState
from loguru import logger
from src.orchestrator import orchestrator
from src.database import db
class WelcomeScreen(Screen):
"""Welcome screen with main menu."""
BINDINGS = [
Binding("q", "quit", "Quit"),
]
def compose(self) -> ComposeResult:
"""Create child widgets."""
yield Header()
yield Container(
Static(
"""
[bold cyan]Multi-Agent AI Workflow[/]
[yellow]Choose an option:[/]
""",
id="welcome_text",
),
Button("🚀 New Customer Setup", id="btn_new_customer", variant="primary"),
Button("🔍 Research Topics", id="btn_research", variant="success"),
Button("✍️ Create Post", id="btn_create_post", variant="success"),
Button("📊 View Status", id="btn_status", variant="default"),
Button("❌ Exit", id="btn_exit", variant="error"),
id="menu_container",
)
yield Footer()
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button presses."""
button_id = event.button.id
if button_id == "btn_new_customer":
self.app.push_screen(NewCustomerScreen())
elif button_id == "btn_research":
self.app.push_screen(ResearchScreen())
elif button_id == "btn_create_post":
self.app.push_screen(CreatePostScreen())
elif button_id == "btn_status":
self.app.push_screen(StatusScreen())
elif button_id == "btn_exit":
self.app.exit()
class NewCustomerScreen(Screen):
"""Screen for setting up a new customer."""
BINDINGS = [
Binding("escape", "app.pop_screen", "Back"),
]
def compose(self) -> ComposeResult:
"""Create child widgets."""
yield Header()
yield ScrollableContainer(
Static("[bold cyan]═══ New Customer Setup ═══[/]\n", id="title"),
# Basic Info Section
Static("[bold yellow]Basic Information[/]"),
Label("Customer Name *:"),
Input(placeholder="Enter customer name", id="input_name"),
Label("LinkedIn URL *:"),
Input(placeholder="https://www.linkedin.com/in/username", id="input_linkedin"),
Label("Company Name:"),
Input(placeholder="Enter company name", id="input_company"),
Label("Email:"),
Input(placeholder="customer@example.com", id="input_email"),
# Persona Section
Static("\n[bold yellow]Persona[/]"),
Label("Describe the customer's persona, expertise, and positioning:"),
TextArea(id="input_persona"),
# Form of Address
Static("\n[bold yellow]Communication Style[/]"),
Label("Form of Address:"),
Input(placeholder="e.g., Duzen (Du/Euch) or Siezen (Sie)", id="input_address"),
# Style Guide
Label("Style Guide:"),
Label("Describe writing style, tone, and guidelines:"),
TextArea(id="input_style_guide"),
# Topic History
Static("\n[bold yellow]Content History[/]"),
Label("Topic History (comma separated):"),
Label("Enter previous topics covered:"),
TextArea(id="input_topic_history"),
# Example Posts
Label("Example Posts (separate with --- on new line):"),
Label("Paste example posts to analyze writing style:"),
TextArea(id="input_example_posts"),
# Actions
Static("\n"),
Horizontal(
Button("Cancel", id="btn_cancel", variant="error"),
Button("Start Setup", id="btn_start", variant="primary"),
id="button_row"
),
# Status/Progress area
Container(
Static("", id="status_message"),
id="status_container"
),
id="form_container",
)
yield Footer()
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button presses."""
if event.button.id == "btn_cancel":
self.app.pop_screen()
elif event.button.id == "btn_start":
self.start_setup()
def start_setup(self) -> None:
"""Start the customer setup process."""
# Get inputs
name = self.query_one("#input_name", Input).value.strip()
linkedin_url = self.query_one("#input_linkedin", Input).value.strip()
company = self.query_one("#input_company", Input).value.strip()
email = self.query_one("#input_email", Input).value.strip()
persona = self.query_one("#input_persona", TextArea).text.strip()
form_of_address = self.query_one("#input_address", Input).value.strip()
style_guide = self.query_one("#input_style_guide", TextArea).text.strip()
topic_history_raw = self.query_one("#input_topic_history", TextArea).text.strip()
example_posts_raw = self.query_one("#input_example_posts", TextArea).text.strip()
status_widget = self.query_one("#status_message", Static)
if not name or not linkedin_url:
status_widget.update("[red]✗ Please fill in required fields (Name and LinkedIn URL)[/]")
return
# Parse topic history
topic_history = [t.strip() for t in topic_history_raw.split(",") if t.strip()]
# Parse example posts
example_posts = [p.strip() for p in example_posts_raw.split("---") if p.strip()]
# Disable buttons during setup
self.query_one("#btn_start", Button).disabled = True
self.query_one("#btn_cancel", Button).disabled = True
# Show progress steps
status_widget.update("[bold cyan]Starting setup process...[/]\n")
customer_data = {
"company_name": company,
"email": email,
"persona": persona,
"form_of_address": form_of_address,
"style_guide": style_guide,
"topic_history": topic_history,
"example_posts": example_posts
}
# Show what's happening
status_widget.update(
"[bold cyan]⏳ Step 1/5: Creating customer record...[/]\n"
"[bold cyan]⏳ Step 2/5: Creating LinkedIn profile...[/]\n"
"[bold cyan]⏳ Step 3/5: Scraping LinkedIn posts...[/]\n"
"[yellow] This may take 1-2 minutes...[/]"
)
# Run setup in background worker
self.run_worker(
self._run_setup_worker(linkedin_url, name, customer_data),
name="setup_worker",
group="setup",
exclusive=True
)
async def _run_setup_worker(self, linkedin_url: str, name: str, customer_data: dict):
"""Worker method to run setup in background."""
return await orchestrator.run_initial_setup(
linkedin_url=linkedin_url,
customer_name=name,
customer_data=customer_data
)
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
"""Handle worker state changes."""
if event.worker.name != "setup_worker":
return
status_widget = self.query_one("#status_message", Static)
if event.state == WorkerState.SUCCESS:
# Worker completed successfully
customer = event.worker.result
status_widget.update(
"[bold green]✓ Step 1/5: Customer record created[/]\n"
"[bold green]✓ Step 2/5: LinkedIn profile created[/]\n"
"[bold green]✓ Step 3/5: LinkedIn posts scraped[/]\n"
"[bold green]✓ Step 4/5: Profile analyzed[/]\n"
"[bold green]✓ Step 5/5: Topics extracted[/]\n\n"
f"[bold cyan]═══ Setup Complete! ═══[/]\n"
f"[green]Customer ID: {customer.id}[/]\n"
f"[green]Name: {customer.name}[/]\n\n"
"[yellow]You can now research topics or create posts.[/]"
)
logger.info(f"Setup completed for customer: {customer.id}")
elif event.state == WorkerState.ERROR:
# Worker failed
error = event.worker.error
logger.exception(f"Setup failed: {error}")
status_widget.update(
f"[bold red]✗ Setup Failed[/]\n\n"
f"[red]Error: {str(error)}[/]\n\n"
f"[yellow]Please check the error and try again.[/]"
)
self.query_one("#btn_start", Button).disabled = False
self.query_one("#btn_cancel", Button).disabled = False
elif event.state == WorkerState.CANCELLED:
# Worker was cancelled
status_widget.update("[yellow]Setup cancelled[/]")
self.query_one("#btn_start", Button).disabled = False
self.query_one("#btn_cancel", Button).disabled = False
class ResearchScreen(Screen):
"""Screen for researching new topics."""
BINDINGS = [
Binding("escape", "app.pop_screen", "Back"),
]
def compose(self) -> ComposeResult:
"""Create child widgets."""
yield Header()
yield Container(
Static("[bold cyan]═══ Research New Topics ═══[/]\n"),
Static("[bold yellow]Select Customer[/]"),
Static("Use arrow keys to navigate, Enter to select", id="help_text"),
OptionList(id="customer_list"),
Static("\n"),
Button("Start Research", id="btn_research", variant="primary"),
Static("\n"),
Container(
Static("", id="progress_status"),
ProgressBar(id="progress_bar", total=100, show_eta=False),
id="progress_container"
),
ScrollableContainer(
Static("", id="research_results"),
id="results_container"
),
id="research_container",
)
yield Footer()
async def on_mount(self) -> None:
"""Load customers when screen mounts."""
# Hide progress container initially
self.query_one("#progress_container").display = False
await self.load_customers()
async def load_customers(self) -> None:
"""Load customer list."""
try:
customers = await db.list_customers()
customer_list = self.query_one("#customer_list", OptionList)
if customers:
for c in customers:
customer_list.add_option(
Option(f"- {c.name} - {c.company_name or 'No Company'}", id=str(c.id))
)
self._customers = {str(c.id): c for c in customers}
else:
self.query_one("#help_text", Static).update(
"[yellow]No customers found. Please create a customer first.[/]"
)
except Exception as e:
logger.error(f"Failed to load customers: {e}")
self.query_one("#help_text", Static).update(f"[red]Error loading customers: {str(e)}[/]")
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
"""Handle customer selection."""
self._selected_customer_id = event.option.id
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button presses."""
if event.button.id == "btn_research":
if hasattr(self, "_selected_customer_id") and self._selected_customer_id:
self.start_research(self._selected_customer_id)
else:
results_widget = self.query_one("#research_results", Static)
results_widget.update("[yellow]Please select a customer first.[/]")
def start_research(self, customer_id: str) -> None:
"""Start research."""
# Clear previous results
self.query_one("#research_results", Static).update("")
# Show progress container
self.query_one("#progress_container").display = True
self.query_one("#progress_bar", ProgressBar).update(progress=0)
self.query_one("#progress_status", Static).update("[bold cyan]Starte Research...[/]")
# Disable button
self.query_one("#btn_research", Button).disabled = True
# Run research in background worker
self.run_worker(
self._run_research_worker(customer_id),
name="research_worker",
group="research",
exclusive=True
)
def _update_research_progress(self, message: str, step: int, total: int) -> None:
"""Update progress - works from both main thread and worker threads."""
def update():
progress_pct = (step / total) * 100
self.query_one("#progress_bar", ProgressBar).update(progress=progress_pct)
self.query_one("#progress_status", Static).update(f"[bold cyan]Step {step}/{total}:[/] {message}")
self.refresh()
# Check if we're on the main thread or a different thread
if self.app._thread_id == threading.get_ident():
# Same thread - schedule update for next tick to allow UI refresh
self.app.call_later(update)
else:
# Different thread - use call_from_thread
self.app.call_from_thread(update)
async def _run_research_worker(self, customer_id: str):
"""Worker method to run research in background."""
from uuid import UUID
return await orchestrator.research_new_topics(
UUID(customer_id),
progress_callback=self._update_research_progress
)
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
"""Handle worker state changes."""
if event.worker.name != "research_worker":
return
results_widget = self.query_one("#research_results", Static)
if event.state == WorkerState.SUCCESS:
# Worker completed successfully
topics = event.worker.result
# Update progress to 100%
self.query_one("#progress_bar", ProgressBar).update(progress=100)
self.query_one("#progress_status", Static).update("[bold green]✓ Abgeschlossen![/]")
# Format results
output = "[bold green]✓ Research Complete![/]\n\n"
output += f"[bold cyan]Found {len(topics)} new topic suggestions:[/]\n\n"
for i, topic in enumerate(topics, 1):
output += f"[bold]{i}. {topic.get('title', 'Unknown')}[/]\n"
output += f" [dim]Category:[/] {topic.get('category', 'N/A')}\n"
fact = topic.get('fact', '')
if fact:
if len(fact) > 200:
fact = fact[:197] + "..."
output += f" [dim]Description:[/] {fact}\n"
output += "\n"
output += "[yellow]Topics saved to research results and ready for post creation.[/]"
results_widget.update(output)
elif event.state == WorkerState.ERROR:
# Worker failed
error = event.worker.error
logger.exception(f"Research failed: {error}")
self.query_one("#progress_status", Static).update("[bold red]✗ Fehler![/]")
results_widget.update(
f"[bold red]✗ Research Failed[/]\n\n"
f"[red]Error: {str(error)}[/]\n\n"
f"[yellow]Please check the error and try again.[/]"
)
elif event.state == WorkerState.CANCELLED:
# Worker was cancelled
results_widget.update("[yellow]Research cancelled[/]")
# Hide progress container after a moment (keep visible briefly to show completion)
# self.query_one("#progress_container").display = False
# Re-enable button
self.query_one("#btn_research", Button).disabled = False
class CreatePostScreen(Screen):
"""Screen for creating posts."""
BINDINGS = [
Binding("escape", "app.pop_screen", "Back"),
]
def compose(self) -> ComposeResult:
"""Create child widgets."""
yield Header()
yield Container(
Static("[bold cyan]═══ Create LinkedIn Post ═══[/]\n"),
# Customer Selection
Static("[bold yellow]1. Select Customer[/]"),
Static("Use arrow keys to navigate, Enter to select", id="help_customer"),
OptionList(id="customer_list"),
# Topic Selection
Static("\n[bold yellow]2. Select Topic[/]"),
Static("Select a customer first to load topics...", id="help_topic"),
OptionList(id="topic_list"),
Static("\n"),
Button("Create Post", id="btn_create", variant="primary"),
Static("\n"),
Container(
Static("", id="progress_status"),
ProgressBar(id="progress_bar", total=100, show_eta=False),
Static("", id="iteration_info"),
id="progress_container"
),
ScrollableContainer(
Static("", id="post_output"),
id="output_container"
),
id="create_container",
)
yield Footer()
async def on_mount(self) -> None:
"""Load data when screen mounts."""
# Hide progress container initially
self.query_one("#progress_container").display = False
await self.load_customers()
async def load_customers(self) -> None:
"""Load customer list."""
try:
customers = await db.list_customers()
customer_list = self.query_one("#customer_list", OptionList)
if customers:
for c in customers:
customer_list.add_option(
Option(f"- {c.name} - {c.company_name or 'No Company'}", id=str(c.id))
)
self._customers = {str(c.id): c for c in customers}
else:
self.query_one("#help_customer", Static).update(
"[yellow]No customers found.[/]"
)
except Exception as e:
logger.exception(f"Failed to load customers: {e}")
self.query_one("#help_customer", Static).update(
f"[red]Error loading customers: {str(e)}[/]"
)
async def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
"""Handle selection from option lists."""
if event.option_list.id == "customer_list":
# Customer selected
self._selected_customer_id = event.option.id
customer_name = self._customers[event.option.id].name
self.query_one("#help_customer", Static).update(
f"[green]✓ Selected: {customer_name}[/]"
)
# Load topics for this customer
await self.load_topics(event.option.id)
elif event.option_list.id == "topic_list":
# Topic selected
self._selected_topic_index = int(event.option.id)
topic = self._topics[self._selected_topic_index]
self.query_one("#help_topic", Static).update(
f"[green]✓ Selected: {topic.get('title', 'Unknown')}[/]"
)
async def load_topics(self, customer_id) -> None:
"""Load ALL topics for customer from ALL research results."""
try:
from uuid import UUID
# Get ALL research results, not just the latest
all_research = await db.get_all_research(UUID(customer_id))
topic_list = self.query_one("#topic_list", OptionList)
topic_list.clear_options()
# Collect all topics from all research results
all_topics = []
for research in all_research:
if research.suggested_topics:
all_topics.extend(research.suggested_topics)
if all_topics:
self._topics = all_topics
for i, t in enumerate(all_topics):
# Show title and category
display_text = f"- {t.get('title', 'Unknown')} [{t.get('category', 'N/A')}]"
topic_list.add_option(Option(display_text, id=str(i)))
self.query_one("#help_topic", Static).update(
f"[cyan]{len(all_topics)} topics available from {len(all_research)} research(es) - select one to continue[/]"
)
else:
self.query_one("#help_topic", Static).update(
"[yellow]No research topics found. Run research first.[/]"
)
except Exception as e:
logger.exception(f"Failed to load topics: {e}")
self.query_one("#help_topic", Static).update(
f"[red]Error loading topics: {str(e)}[/]"
)
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button presses."""
if event.button.id == "btn_create":
if not hasattr(self, "_selected_customer_id") or not self._selected_customer_id:
output_widget = self.query_one("#post_output", Static)
output_widget.update("[yellow]Please select a customer first.[/]")
return
if not hasattr(self, "_selected_topic_index") or self._selected_topic_index is None:
output_widget = self.query_one("#post_output", Static)
output_widget.update("[yellow]Please select a topic first.[/]")
return
from uuid import UUID
topic = self._topics[self._selected_topic_index]
self.create_post(UUID(self._selected_customer_id), topic)
def create_post(self, customer_id, topic) -> None:
"""Create a post."""
output_widget = self.query_one("#post_output", Static)
# Clear previous output
output_widget.update("")
# Show progress container
self.query_one("#progress_container").display = True
self.query_one("#progress_bar", ProgressBar).update(progress=0)
self.query_one("#progress_status", Static).update("[bold cyan]Starte Post-Erstellung...[/]")
self.query_one("#iteration_info", Static).update("")
# Disable button
self.query_one("#btn_create", Button).disabled = True
# Run post creation in background worker
self.run_worker(
self._run_create_post_worker(customer_id, topic),
name="create_post_worker",
group="create_post",
exclusive=True
)
def _update_post_progress(self, message: str, iteration: int, max_iterations: int, score: int = None) -> None:
"""Update progress - works from both main thread and worker threads."""
def update():
# Calculate progress based on iteration
if iteration == 0:
progress_pct = 0
else:
progress_pct = (iteration / max_iterations) * 100
self.query_one("#progress_bar", ProgressBar).update(progress=progress_pct)
self.query_one("#progress_status", Static).update(f"[bold cyan]{message}[/]")
if iteration > 0:
score_text = f" | Score: {score}/100" if score else ""
self.query_one("#iteration_info", Static).update(
f"[dim]Iteration {iteration}/{max_iterations}{score_text}[/]"
)
self.refresh()
# Check if we're on the main thread or a different thread
if self.app._thread_id == threading.get_ident():
# Same thread - schedule update for next tick to allow UI refresh
self.app.call_later(update)
else:
# Different thread - use call_from_thread
self.app.call_from_thread(update)
async def _run_create_post_worker(self, customer_id, topic):
"""Worker method to create post in background."""
return await orchestrator.create_post(
customer_id=customer_id,
topic=topic,
max_iterations=3,
progress_callback=self._update_post_progress
)
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
"""Handle worker state changes."""
if event.worker.name != "create_post_worker":
return
output_widget = self.query_one("#post_output", Static)
if event.state == WorkerState.SUCCESS:
# Worker completed successfully
result = event.worker.result
topic = self._topics[self._selected_topic_index]
# Update progress to 100%
self.query_one("#progress_bar", ProgressBar).update(progress=100)
self.query_one("#progress_status", Static).update("[bold green]✓ Post erstellt![/]")
self.query_one("#iteration_info", Static).update(
f"[green]Final: {result['iterations']} Iterationen | Score: {result['final_score']}/100[/]"
)
# Format output
output = f"[bold green]✓ Post Created Successfully![/]\n\n"
output += f"[bold cyan]═══ Post Details ═══[/]\n"
output += f"[bold]Topic:[/] {topic.get('title', 'Unknown')}\n"
output += f"[bold]Iterations:[/] {result['iterations']}\n"
output += f"[bold]Final Score:[/] {result['final_score']}/100\n"
output += f"[bold]Approved:[/] {'✓ Yes' if result['approved'] else '✗ No (reached max iterations)'}\n\n"
output += f"[bold cyan]═══ Final Post ═══[/]\n\n"
output += f"[white]{result['final_post']}[/]\n\n"
output += f"[bold cyan]═══════════════════[/]\n"
output += f"[yellow]Post saved to database with ID: {result['post_id']}[/]"
output_widget.update(output)
elif event.state == WorkerState.ERROR:
# Worker failed
error = event.worker.error
logger.exception(f"Post creation failed: {error}")
self.query_one("#progress_status", Static).update("[bold red]✗ Fehler![/]")
output_widget.update(
f"[bold red]✗ Post Creation Failed[/]\n\n"
f"[red]Error: {str(error)}[/]\n\n"
f"[yellow]Please check the error and try again.[/]"
)
elif event.state == WorkerState.CANCELLED:
# Worker was cancelled
output_widget.update("[yellow]Post creation cancelled[/]")
# Re-enable button
self.query_one("#btn_create", Button).disabled = False
class StatusScreen(Screen):
"""Screen for viewing customer status."""
BINDINGS = [
Binding("escape", "app.pop_screen", "Back"),
]
def compose(self) -> ComposeResult:
"""Create child widgets."""
yield Header()
yield Container(
Static("[bold cyan]═══ Customer Status ═══[/]\n\n"),
ScrollableContainer(
Static("Loading...", id="status_content"),
id="status_scroll"
),
Static("\n"),
Button("Refresh", id="btn_refresh", variant="primary"),
)
yield Footer()
def on_mount(self) -> None:
"""Load status when screen mounts."""
self.load_status()
def load_status(self) -> None:
"""Load and display status."""
status_widget = self.query_one("#status_content", Static)
status_widget.update("[yellow]Loading customer data...[/]")
# Run status loading in background worker
self.run_worker(
self._run_load_status_worker(),
name="load_status_worker",
group="status",
exclusive=True
)
async def _run_load_status_worker(self):
"""Worker method to load status in background."""
customers = await db.list_customers()
if not customers:
return None
output = ""
for customer in customers:
status = await orchestrator.get_customer_status(customer.id)
output += f"[bold cyan]╔═══ {customer.name} ═══╗[/]\n"
output += f"[bold]Customer ID:[/] {customer.id}\n"
output += f"[bold]LinkedIn:[/] {customer.linkedin_url}\n"
output += f"[bold]Company:[/] {customer.company_name or 'N/A'}\n\n"
output += f"[bold yellow]Status:[/]\n"
output += f" Profile: {'[green]✓ Created[/]' if status['has_profile'] else '[red]✗ Missing[/]'}\n"
output += f" Analysis: {'[green]✓ Complete[/]' if status['has_analysis'] else '[red]✗ Missing[/]'}\n\n"
output += f"[bold yellow]Content:[/]\n"
output += f" LinkedIn Posts: [cyan]{status['posts_count']}[/]\n"
output += f" Extracted Topics: [cyan]{status['topics_count']}[/]\n"
output += f" Generated Posts: [cyan]{status['generated_posts_count']}[/]\n"
output += f"[bold cyan]╚{'' * (len(customer.name) + 8)}╝[/]\n\n"
return output
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
"""Handle worker state changes."""
if event.worker.name != "load_status_worker":
return
status_widget = self.query_one("#status_content", Static)
if event.state == WorkerState.SUCCESS:
# Worker completed successfully
output = event.worker.result
if output is None:
status_widget.update(
"[yellow]No customers found.[/]\n"
"[dim]Create a new customer to get started.[/]"
)
else:
status_widget.update(output)
elif event.state == WorkerState.ERROR:
# Worker failed
error = event.worker.error
logger.exception(f"Failed to load status: {error}")
status_widget.update(
f"[bold red]✗ Error Loading Status[/]\n\n"
f"[red]{str(error)}[/]"
)
elif event.state == WorkerState.CANCELLED:
# Worker was cancelled
status_widget.update("[yellow]Status loading cancelled[/]")
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button presses."""
if event.button.id == "btn_refresh":
self.load_status()
class LinkedInWorkflowApp(App):
"""Main Textual application."""
CSS = """
Screen {
align: center middle;
}
#menu_container {
width: 60;
height: auto;
padding: 2;
border: solid $primary;
background: $surface;
}
#menu_container Button {
width: 100%;
margin: 1;
}
#welcome_text {
text-align: center;
padding: 1;
}
#form_container {
width: 100%;
height: 100%;
padding: 2;
}
#form_container Input, #form_container TextArea {
margin-bottom: 1;
}
#form_container Label {
margin-top: 1;
color: $text;
}
#form_container TextArea {
height: 5;
}
#button_row {
width: 100%;
height: auto;
margin: 1 0;
}
#button_row Button {
margin: 0 1;
}
#status_container, #results_container, #output_container {
min-height: 10;
border: solid $accent;
margin: 1 0;
padding: 1;
}
#status_scroll {
height: 30;
border: solid $accent;
margin-top: 1;
padding: 1;
}
#research_container, #create_container {
width: 90;
height: auto;
padding: 2;
border: solid $primary;
background: $surface;
}
#customer_list, #topic_list {
height: 10;
border: solid $accent;
margin: 1 0;
}
#customer_list > .option-list--option,
#topic_list > .option-list--option {
padding: 1 1;
margin-bottom: 1;
}
#help_text, #help_customer, #help_topic {
color: $text-muted;
margin-bottom: 1;
}
#progress_container {
height: auto;
padding: 1;
margin: 1 0;
border: solid $accent;
background: $surface-darken-1;
}
#progress_bar {
width: 100%;
margin: 1 0;
}
#progress_status {
text-align: center;
margin-bottom: 1;
}
#iteration_info {
text-align: center;
margin-top: 1;
}
#title {
text-align: center;
padding: 1;
}
"""
BINDINGS = [
Binding("q", "quit", "Quit", show=True),
]
def on_mount(self) -> None:
"""Set up the application on mount."""
self.title = "LinkedIn Post Creation System"
self.sub_title = "Multi-Agent AI Workflow"
self.push_screen(WelcomeScreen())
def run_app():
"""Run the TUI application."""
app = LinkedInWorkflowApp()
app.run()