update web app cookbook

This commit is contained in:
zachary62 2025-05-26 19:16:17 -04:00
parent aaf69731ee
commit 92ccbea299
17 changed files with 660 additions and 297 deletions

View File

@ -87,8 +87,9 @@ From there, it's easy to implement popular design patterns like ([Multi-](https:
| [Code Generator](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-code-generator) | ★☆☆ <sup>*Beginner*</sup> | Generate test cases, implement solutions, and iteratively improve code | | [Code Generator](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-code-generator) | ★☆☆ <sup>*Beginner*</sup> | Generate test cases, implement solutions, and iteratively improve code |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ <sup>*Beginner*</sup> | Agent using Model Context Protocol for numerical operations | | [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ <sup>*Beginner*</sup> | Agent using Model Context Protocol for numerical operations |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ <sup>*Beginner*</sup> | Agent wrapped with A2A protocol for inter-agent communication | | [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ <sup>*Beginner*</sup> | Agent wrapped with A2A protocol for inter-agent communication |
| [Streamlit HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-streamlit-hitl) | ★☆☆ <sup>*Beginner*</sup> | Streamlit app for human-in-the-loop image generation | | [Streamlit FSM](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-streamlit-fsm) | ★☆☆ <sup>*Beginner*</sup> | Streamlit app with finite state machine for HITL image generation |
| [FastAPI HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-fastapi-hitl) | ★☆☆ <sup>*Beginner*</sup> | FastAPI app for async human review loop with SSE | | [FastAPI WebSocket](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-fastapi-websocket) | ★☆☆ <sup>*Beginner*</sup> | Real-time chat interface with streaming LLM responses via WebSocket |
| [FastAPI Background](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-fastapi-background) | ★☆☆ <sup>*Beginner*</sup> | FastAPI app with background jobs and real-time progress via SSE |
| [Voice Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-voice-chat) | ★☆☆ <sup>*Beginner*</sup> | An interactive voice chat application with VAD, STT, LLM, and TTS. | | [Voice Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-voice-chat) | ★☆☆ <sup>*Beginner*</sup> | An interactive voice chat application with VAD, STT, LLM, and TTS. |
</div> </div>

View File

@ -1,74 +1,81 @@
# PocketFlow FastAPI Background Job # PocketFlow FastAPI Background Jobs with Real-time Progress
A minimal example of running PocketFlow workflows as background jobs with real-time progress updates via Server-Sent Events (SSE). A web application demonstrating PocketFlow workflows running as FastAPI background jobs with real-time progress updates via Server-Sent Events (SSE).
<p align="center">
<img
src="./assets/banner.png" width="800"
/>
</p>
## Features ## Features
- Start article generation jobs via REST API - **Modern Web UI**: Clean interface with real-time progress visualization
- Real-time granular progress updates via SSE (shows progress for each section) - **Background Processing**: Non-blocking article generation using FastAPI BackgroundTasks
- Background processing with FastAPI - **Server-Sent Events**: Real-time progress streaming without polling
- Simple three-step workflow: Outline → Content → Style - **Granular Progress**: Section-by-section updates during content generation
- Web interface for easy job submission and monitoring - **PocketFlow Integration**: Three-node workflow (Outline → Content → Style)
## Getting Started ## How to Run
1. Install dependencies: 1. Install Dependencies:
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
2. Set your OpenAI API key: 2. Set your OpenAI API key:
```bash ```bash
export OPENAI_API_KEY=your_api_key_here export OPENAI_API_KEY=your_api_key_here
```
3. Run the FastAPI Server:
```bash
python main.py
```
4. Access the Web UI:
Open your browser and navigate to `http://localhost:8000`.
5. Use the Application:
- Enter an article topic or click suggested topics
- Click "Generate Article" to start background processing
- Watch real-time progress updates with step indicators
- Copy the final article when complete
## How It Works
The application uses PocketFlow to define a three-step article generation workflow. FastAPI handles web requests and manages real-time SSE communication for progress updates.
**PocketFlow Workflow:**
```mermaid
flowchart LR
A[Generate Outline] --> B[Write Content]
B --> C[Apply Style]
``` ```
3. Run the server: 1. **`GenerateOutline`**: Creates structured outline with up to 3 sections
```bash 2. **`WriteContent` (BatchNode)**: Writes content for each section individually, sending progress updates
python main.py 3. **`ApplyStyle`**: Polishes the article with conversational tone
```
## Usage **FastAPI & SSE Integration:**
### Web Interface (Recommended) - The `/start-job` endpoint creates a unique job, initializes an SSE queue, and schedules the workflow using `BackgroundTasks`
- Nodes send progress updates to the job-specific `sse_queue` during execution
- The `/progress/{job_id}` endpoint streams real-time updates to the client via Server-Sent Events
- The web UI displays progress with animated bars, step indicators, and detailed status messages
1. Open your browser and go to `http://localhost:8000` **Progress Updates:**
2. Enter an article topic (e.g., "AI Safety", "Climate Change") - 33%: Outline generation complete
3. Click "Generate Article" - 33-66%: Content writing (individual section updates)
4. You'll be redirected to a progress page showing real-time updates - 66-100%: Style application
5. The final article will appear when generation is complete - 100%: Article ready
### API Usage
#### Start a Job
```bash
curl -X POST "http://localhost:8000/start-job" -d "topic=AI Safety" -H "Content-Type: application/x-www-form-urlencoded"
```
Response:
```json
{"job_id": "123e4567-e89b-12d3-a456-426614174000", "topic": "AI Safety", "status": "started"}
```
#### Monitor Progress
```bash
curl "http://localhost:8000/progress/123e4567-e89b-12d3-a456-426614174000"
```
SSE Stream:
```
data: {"step": "outline", "progress": 33, "data": {"sections": ["Introduction", "Challenges", "Solutions"]}}
data: {"step": "content", "progress": 44, "data": {"section": "Introduction", "completed_sections": 1, "total_sections": 3}}
data: {"step": "content", "progress": 55, "data": {"section": "Challenges", "completed_sections": 2, "total_sections": 3}}
data: {"step": "content", "progress": 66, "data": {"section": "Solutions", "completed_sections": 3, "total_sections": 3}}
data: {"step": "content", "progress": 66, "data": {"draft_length": 1234, "status": "complete"}}
data: {"step": "complete", "progress": 100, "data": {"final_article": "..."}}
```
## Files ## Files
- `main.py` - FastAPI app with background jobs and SSE - [`main.py`](./main.py): FastAPI application with background jobs and SSE endpoints
- `flow.py` - PocketFlow workflow definition - [`flow.py`](./flow.py): PocketFlow workflow definition connecting the three nodes
- `nodes.py` - Workflow nodes (Outline, Content, Style) - [`nodes.py`](./nodes.py): Workflow nodes (GenerateOutline, WriteContent BatchNode, ApplyStyle)
- `utils/call_llm.py` - LLM utility function - [`utils/call_llm.py`](./utils/call_llm.py): OpenAI LLM utility function
- `static/index.html` - Main page for starting jobs - [`static/index.html`](./static/index.html): Modern job submission form with topic suggestions
- `static/progress.html` - Progress monitoring page with real-time updates - [`static/progress.html`](./static/progress.html): Real-time progress monitoring with animations

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

View File

@ -18,8 +18,8 @@ active_jobs = {}
def run_article_workflow(job_id: str, topic: str): def run_article_workflow(job_id: str, topic: str):
"""Run the article workflow in background""" """Run the article workflow in background"""
try: try:
# Create shared store with SSE queue # Get the pre-created queue from active_jobs
sse_queue = asyncio.Queue() sse_queue = active_jobs[job_id]
shared = { shared = {
"topic": topic, "topic": topic,
"sse_queue": sse_queue, "sse_queue": sse_queue,
@ -28,9 +28,6 @@ def run_article_workflow(job_id: str, topic: str):
"final_article": "" "final_article": ""
} }
# Store the queue for SSE access
active_jobs[job_id] = sse_queue
# Run the workflow # Run the workflow
flow = create_article_flow() flow = create_article_flow()
flow.run(shared) flow.run(shared)
@ -46,6 +43,10 @@ async def start_job(background_tasks: BackgroundTasks, topic: str = Form(...)):
"""Start a new article generation job""" """Start a new article generation job"""
job_id = str(uuid.uuid4()) job_id = str(uuid.uuid4())
# Create SSE queue and register job immediately
sse_queue = asyncio.Queue()
active_jobs[job_id] = sse_queue
# Start background task # Start background task
background_tasks.add_task(run_article_workflow, job_id, topic) background_tasks.add_task(run_article_workflow, job_id, topic)
@ -62,6 +63,9 @@ async def get_progress(job_id: str):
sse_queue = active_jobs[job_id] sse_queue = active_jobs[job_id]
# Send initial connection confirmation
yield f"data: {json.dumps({'step': 'connected', 'progress': 0, 'data': {'message': 'Connected to job progress'}})}\n\n"
try: try:
while True: while True:
# Wait for next progress update # Wait for next progress update

View File

@ -3,120 +3,214 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PocketFlow Article Generator</title> <title>Article Generator</title>
<style> <style>
body { * {
font-family: Arial, sans-serif; margin: 0;
max-width: 600px; padding: 0;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
input[type="text"] {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box; box-sizing: border-box;
} }
input[type="text"]:focus {
border-color: #4CAF50; body {
outline: none; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
} }
button {
background-color: #4CAF50; .container {
color: white; background: white;
padding: 12px 30px; border-radius: 20px;
border: none; padding: 40px;
border-radius: 5px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
cursor: pointer; max-width: 500px;
font-size: 16px;
width: 100%; width: 100%;
}
button:hover {
background-color: #45a049;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.loading {
text-align: center; text-align: center;
color: #666; }
margin-top: 20px;
.logo {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 10px;
}
.subtitle {
color: #6b7280;
font-size: 1.1rem;
margin-bottom: 40px;
font-weight: 400;
}
.form-group {
margin-bottom: 30px;
text-align: left;
}
label {
display: block;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
font-size: 0.95rem;
}
input[type="text"] {
width: 100%;
padding: 16px 20px;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 1rem;
transition: all 0.3s ease;
background: #f9fafb;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.submit-btn {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 12px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
}
.submit-btn:active {
transform: translateY(0);
}
.example-topics {
margin-top: 30px;
padding-top: 30px;
border-top: 1px solid #e5e7eb;
}
.example-topics h3 {
color: #6b7280;
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 15px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.topic-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.topic-tag {
background: #f3f4f6;
color: #6b7280;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.topic-tag:hover {
background: #e5e7eb;
color: #374151;
}
@media (max-width: 480px) {
.container {
padding: 30px 20px;
}
.logo {
font-size: 2rem;
}
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>🚀 Article Generator</h1> <div class="logo">✨ Article AI</div>
<form id="jobForm"> <p class="subtitle">Generate engaging articles with AI assistance</p>
<form id="articleForm" action="/start-job" method="post">
<div class="form-group"> <div class="form-group">
<label for="topic">Article Topic:</label> <label for="topic">What would you like to write about?</label>
<input type="text" id="topic" name="topic" placeholder="e.g., AI Safety, Climate Change, Space Exploration" required> <input type="text" id="topic" name="topic" placeholder="Enter your topic here..." required>
</div> </div>
<button type="submit" id="submitBtn">Generate Article</button>
<button type="submit" class="submit-btn">Generate Article</button>
</form> </form>
<div id="loading" class="loading" style="display: none;">
Starting your article generation... <div class="example-topics">
<h3>Popular Topics</h3>
<div class="topic-tags">
<span class="topic-tag" onclick="setTopic('AI Safety')">AI Safety</span>
<span class="topic-tag" onclick="setTopic('Climate Change')">Climate Change</span>
<span class="topic-tag" onclick="setTopic('Space Exploration')">Space Exploration</span>
<span class="topic-tag" onclick="setTopic('Renewable Energy')">Renewable Energy</span>
<span class="topic-tag" onclick="setTopic('Mental Health')">Mental Health</span>
<span class="topic-tag" onclick="setTopic('Future of Work')">Future of Work</span>
</div>
</div> </div>
</div> </div>
<script> <script>
document.getElementById('jobForm').addEventListener('submit', async function(e) { function setTopic(topic) {
document.getElementById('topic').value = topic;
}
document.getElementById('articleForm').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
const topic = document.getElementById('topic').value.trim(); const submitBtn = document.querySelector('.submit-btn');
if (!topic) return; const originalText = submitBtn.textContent;
// Show loading state // Show loading state
document.getElementById('submitBtn').disabled = true; submitBtn.textContent = 'Starting...';
document.getElementById('loading').style.display = 'block'; submitBtn.disabled = true;
try { try {
const formData = new FormData(this);
const response = await fetch('/start-job', { const response = await fetch('/start-job', {
method: 'POST', method: 'POST',
headers: { body: formData
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `topic=${encodeURIComponent(topic)}`
}); });
const result = await response.json(); const result = await response.json();
if (result.job_id) { if (response.ok) {
// Redirect to progress page // Redirect to progress page
window.location.href = `/progress.html?job_id=${result.job_id}&topic=${encodeURIComponent(topic)}`; window.location.href = `/progress.html?job_id=${result.job_id}&topic=${encodeURIComponent(result.topic)}`;
} else { } else {
alert('Failed to start job'); throw new Error('Failed to start job');
document.getElementById('submitBtn').disabled = false;
document.getElementById('loading').style.display = 'none';
} }
} catch (error) { } catch (error) {
alert('Error starting job: ' + error.message); alert('Error starting job: ' + error.message);
document.getElementById('submitBtn').disabled = false; submitBtn.textContent = originalText;
document.getElementById('loading').style.display = 'none'; submitBtn.disabled = false;
} }
}); });
</script> </script>

View File

@ -3,220 +3,478 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Article Generation Progress</title> <title>Generating Article...</title>
<style> <style>
body { * {
font-family: Arial, sans-serif; margin: 0;
max-width: 800px; padding: 0;
margin: 20px auto; box-sizing: border-box;
padding: 20px;
background-color: #f5f5f5;
} }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container { .container {
background: white; background: white;
padding: 30px; border-radius: 20px;
border-radius: 10px; padding: 40px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
} max-width: 600px;
h1 { width: 100%;
color: #333;
text-align: center; text-align: center;
}
.logo {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 10px;
}
.topic-title {
color: #374151;
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 30px; margin-bottom: 30px;
} }
.topic {
text-align: center;
color: #666;
margin-bottom: 30px;
font-style: italic;
}
.progress-container { .progress-container {
margin-bottom: 30px; margin: 30px 0;
} }
.progress-bar { .progress-bar {
width: 100%; width: 100%;
height: 20px; height: 8px;
background-color: #f0f0f0; background: #f3f4f6;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
margin-bottom: 20px;
} }
.progress-fill { .progress-fill {
height: 100%; height: 100%;
background-color: #4CAF50; background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 10px;
width: 0%; width: 0%;
transition: width 0.3s ease; transition: width 0.5s ease;
position: relative;
} }
.progress-text {
text-align: center; .progress-fill::after {
margin-top: 10px;
font-weight: bold;
color: #333;
}
.step-info {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #4CAF50;
}
.article-result {
background-color: #f8f9fa;
padding: 20px;
border-radius: 5px;
margin-top: 20px;
white-space: pre-wrap;
line-height: 1.6;
}
.error {
background-color: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 5px;
border-left: 4px solid #f44336;
}
.back-button {
background-color: #2196F3;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
display: inline-block;
margin-top: 20px;
}
.back-button:hover {
background-color: #1976D2;
}
.loading-dots {
display: inline-block;
}
.loading-dots:after {
content: ''; content: '';
animation: dots 1.5s steps(5, end) infinite; position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: shimmer 2s infinite;
} }
@keyframes dots {
0%, 20% { content: ''; } @keyframes shimmer {
40% { content: '.'; } 0% { transform: translateX(-100%); }
60% { content: '..'; } 100% { transform: translateX(100%); }
80%, 100% { content: '...'; } }
.progress-text {
color: #6b7280;
font-size: 1rem;
font-weight: 500;
margin-bottom: 10px;
}
.progress-percentage {
color: #374151;
font-size: 2rem;
font-weight: 700;
margin-bottom: 20px;
}
.status-card {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
margin: 20px 0;
text-align: left;
}
.status-title {
color: #374151;
font-weight: 600;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.status-content {
color: #6b7280;
line-height: 1.5;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #e5e7eb;
border-top: 2px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.step-indicator {
display: flex;
justify-content: space-between;
margin: 30px 0;
position: relative;
}
.step-indicator::before {
content: '';
position: absolute;
top: 15px;
left: 15px;
right: 15px;
height: 2px;
background: #e5e7eb;
z-index: 1;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
}
.step-circle {
width: 30px;
height: 30px;
border-radius: 50%;
background: #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.8rem;
margin-bottom: 8px;
transition: all 0.3s ease;
}
.step-circle.active {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.step-circle.completed {
background: #10b981;
color: white;
}
.step-label {
font-size: 0.8rem;
color: #6b7280;
text-align: center;
max-width: 80px;
}
.result-section {
display: none;
text-align: left;
margin-top: 30px;
}
.result-section.show {
display: block;
}
.article-content {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 25px;
line-height: 1.6;
color: #374151;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
.action-buttons {
display: flex;
gap: 15px;
margin-top: 20px;
justify-content: center;
}
.btn {
padding: 12px 24px;
border-radius: 10px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
cursor: pointer;
border: none;
font-size: 0.95rem;
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #f3f4f6;
color: #374151;
border: 1px solid #e5e7eb;
}
.btn-secondary:hover {
background: #e5e7eb;
}
.error-message {
background: #fef2f2;
border: 1px solid #fecaca;
color: #dc2626;
padding: 15px;
border-radius: 10px;
margin: 20px 0;
}
@media (max-width: 480px) {
.container {
padding: 30px 20px;
}
.step-indicator {
margin: 20px 0;
}
.step-label {
font-size: 0.7rem;
max-width: 60px;
}
.action-buttons {
flex-direction: column;
}
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>📝 Generating Your Article</h1> <div class="logo">✨ Article AI</div>
<div class="topic" id="topicDisplay"></div> <div class="topic-title" id="topicTitle">Generating your article...</div>
<div class="progress-container"> <div class="progress-container">
<div class="progress-percentage" id="progressPercentage">0%</div>
<div class="progress-bar"> <div class="progress-bar">
<div class="progress-fill" id="progressFill"></div> <div class="progress-fill" id="progressFill"></div>
</div> </div>
<div class="progress-text" id="progressText">Starting<span class="loading-dots"></span></div> <div class="progress-text" id="progressText">Initializing...</div>
</div> </div>
<div id="stepInfo" class="step-info" style="display: none;"></div> <div class="step-indicator">
<div id="errorInfo" class="error" style="display: none;"></div> <div class="step">
<div id="articleResult" class="article-result" style="display: none;"></div> <div class="step-circle" id="step1">1</div>
<div class="step-label">Outline</div>
</div>
<div class="step">
<div class="step-circle" id="step2">2</div>
<div class="step-label">Content</div>
</div>
<div class="step">
<div class="step-circle" id="step3">3</div>
<div class="step-label">Style</div>
</div>
</div>
<a href="/" class="back-button">← Generate Another Article</a> <div class="status-card" id="statusCard">
<div class="status-title" id="statusTitle">
<span class="spinner"></span>
Getting started...
</div>
<div class="status-content" id="statusContent">
Preparing to generate your article. This may take a few moments.
</div>
</div>
<div class="result-section" id="resultSection">
<h3 style="margin-bottom: 15px; color: #374151;">Your Article is Ready! 🎉</h3>
<div class="article-content" id="articleContent"></div>
<div class="action-buttons">
<button class="btn btn-primary" onclick="copyToClipboard()">Copy Article</button>
<a href="/" class="btn btn-secondary">Generate Another</a>
</div>
</div>
<div class="error-message" id="errorMessage" style="display: none;"></div>
</div> </div>
<script> <script>
// Get job_id and topic from URL parameters
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const jobId = urlParams.get('job_id'); const jobId = urlParams.get('job_id');
const topic = urlParams.get('topic'); const topic = urlParams.get('topic');
if (!jobId) { if (topic) {
document.getElementById('errorInfo').style.display = 'block'; document.getElementById('topicTitle').textContent = `"${topic}"`;
document.getElementById('errorInfo').textContent = 'No job ID provided';
} else {
document.getElementById('topicDisplay').textContent = `Topic: ${topic || 'Unknown'}`;
startProgressMonitoring(jobId);
} }
function startProgressMonitoring(jobId) { if (!jobId) {
showError('No job ID provided');
} else {
connectToProgress();
}
function connectToProgress() {
const eventSource = new EventSource(`/progress/${jobId}`); const eventSource = new EventSource(`/progress/${jobId}`);
eventSource.onmessage = function(event) { eventSource.onmessage = function(event) {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
handleProgressUpdate(data);
if (data.error) {
showError(data.error);
eventSource.close();
return;
}
if (data.heartbeat) {
return; // Ignore heartbeat messages
}
updateProgress(data);
if (data.step === 'complete') {
showFinalResult(data.data.final_article);
eventSource.close();
}
} catch (error) { } catch (error) {
console.error('Error parsing SSE data:', error); console.error('Error parsing SSE data:', error);
} }
}; };
eventSource.onerror = function(event) { eventSource.onerror = function(error) {
console.error('SSE connection error:', event); console.error('SSE connection error:', error);
showError('Connection lost. Please refresh the page.'); showError('Connection lost. Please refresh the page.');
eventSource.close(); eventSource.close();
}; };
} }
function updateProgress(data) { function handleProgressUpdate(data) {
const progressFill = document.getElementById('progressFill'); if (data.error) {
const progressText = document.getElementById('progressText'); showError(data.error);
const stepInfo = document.getElementById('stepInfo'); return;
}
// Update progress bar if (data.heartbeat) {
progressFill.style.width = data.progress + '%'; return; // Ignore heartbeat messages
}
const progress = data.progress || 0;
updateProgress(progress);
// Update progress text and step info
switch (data.step) { switch (data.step) {
case 'outline': case 'connected':
progressText.textContent = 'Creating outline... (33%)'; updateStatus('🔗 Connected', 'Successfully connected to the article generation process.');
stepInfo.style.display = 'block';
stepInfo.innerHTML = `<strong>Step 1:</strong> Generated outline with sections: ${data.data.sections.join(', ')}`;
break; break;
case 'content':
if (data.data.section) { case 'outline':
// Individual section progress updateStepIndicator(1);
progressText.textContent = `Writing content... (${data.progress}%)`; if (data.data && data.data.sections) {
stepInfo.innerHTML = `<strong>Step 2:</strong> Completed section "${data.data.section}" (${data.data.completed_sections}/${data.data.total_sections})`; updateStatus('📝 Creating Outline', `Generated outline with ${data.data.sections.length} sections`);
} else { } else {
// Final content completion updateStatus('📝 Creating Outline', 'Generating article structure and main points...');
progressText.textContent = 'Writing content... (66%)';
stepInfo.innerHTML = `<strong>Step 2:</strong> Generated ${data.data.draft_length} characters of content`;
} }
break; break;
case 'content':
updateStepIndicator(2);
if (data.data && data.data.section) {
updateStatus('✍️ Writing Content',
`Writing section: "${data.data.section}" (${data.data.completed_sections}/${data.data.total_sections})`);
} else {
updateStatus('✍️ Writing Content', 'Creating detailed content for each section...');
}
break;
case 'style':
updateStepIndicator(3);
updateStatus('🎨 Applying Style', 'Polishing the article with engaging, conversational tone...');
break;
case 'complete': case 'complete':
progressText.textContent = 'Complete! (100%)'; updateStepIndicator(3, true);
stepInfo.innerHTML = `<strong>Step 3:</strong> Applied conversational styling - Article ready!`; updateProgress(100);
updateStatus('✅ Complete!', 'Your article has been generated successfully.');
if (data.data && data.data.final_article) {
showResult(data.data.final_article);
}
break; break;
} }
} }
function showFinalResult(article) { function updateProgress(percentage) {
const resultDiv = document.getElementById('articleResult'); document.getElementById('progressPercentage').textContent = `${percentage}%`;
resultDiv.style.display = 'block'; document.getElementById('progressFill').style.width = `${percentage}%`;
resultDiv.textContent = article;
} }
function showError(errorMessage) { function updateStatus(title, content) {
const errorDiv = document.getElementById('errorInfo'); document.getElementById('statusTitle').innerHTML = `<span class="spinner"></span> ${title}`;
errorDiv.style.display = 'block'; document.getElementById('statusContent').textContent = content;
errorDiv.textContent = `Error: ${errorMessage}`; }
const progressText = document.getElementById('progressText'); function updateStepIndicator(step, completed = false) {
progressText.textContent = 'Failed'; // Reset all steps
for (let i = 1; i <= 3; i++) {
const stepElement = document.getElementById(`step${i}`);
stepElement.className = 'step-circle';
if (i < step) {
stepElement.classList.add('completed');
stepElement.innerHTML = '✓';
} else if (i === step) {
stepElement.classList.add(completed ? 'completed' : 'active');
stepElement.innerHTML = completed ? '✓' : i;
} else {
stepElement.innerHTML = i;
}
}
}
function showResult(article) {
document.getElementById('statusCard').style.display = 'none';
document.getElementById('articleContent').textContent = article;
document.getElementById('resultSection').classList.add('show');
}
function showError(message) {
document.getElementById('errorMessage').textContent = message;
document.getElementById('errorMessage').style.display = 'block';
document.getElementById('statusCard').style.display = 'none';
}
function copyToClipboard() {
const article = document.getElementById('articleContent').textContent;
navigator.clipboard.writeText(article).then(() => {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
btn.style.background = '#10b981';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '';
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
alert('Failed to copy to clipboard');
});
} }
</script> </script>
</body> </body>

View File

@ -1 +0,0 @@
# Utils package for FastAPI Background Job Interface

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB