diff --git a/opus_orchestrator/cli.py b/opus_orchestrator/cli.py index 8699979..243ff03 100644 --- a/opus_orchestrator/cli.py +++ b/opus_orchestrator/cli.py @@ -315,6 +315,26 @@ Examples: help="Enable auto-reload on code changes", ) + # ------------------------------------------------------------------------- + # UI COMMAND (Web Interface) + # ------------------------------------------------------------------------- + ui_parser = subparsers.add_parser( + "ui", + help="Start web UI only (no API)", + description="Start the novice-friendly web interface", + ) + ui_parser.add_argument( + "--host", + default="0.0.0.0", + help="Host to bind to (default: 0.0.0.0)", + ) + ui_parser.add_argument( + "--port", "-p", + type=int, + default=8080, + help="Port to bind to (default: 8080)", + ) + # ------------------------------------------------------------------------- # INGEST COMMAND (GitHub) # ------------------------------------------------------------------------- @@ -771,6 +791,29 @@ async def run_serve(args: argparse.Namespace) -> int: return 0 +async def run_ui(args: argparse.Namespace) -> int: + """Start the web UI only.""" + print(f"\nšŸŽØ Starting Opus Web UI...") + print(f" Host: {args.host}") + print(f" Port: {args.port}") + print(f" UI: http://{args.host}:{args.port}/\n") + + try: + from opus_orchestrator.server import create_app + import uvicorn + + app = create_app(include_ui=True) + config = uvicorn.Config(app, host=args.host, port=args.port, log_level="info") + server = uvicorn.Server(config) + await server.serve() + except ImportError as e: + print(f"Error: {e}") + print("Run `pip install fastapi uvicorn` to enable web UI") + return 1 + + return 0 + + def run_ingest(args: argparse.Namespace) -> int: """Ingest content from GitHub.""" from opus_orchestrator import OpusOrchestrator @@ -1035,6 +1078,7 @@ async def main_async(args: argparse.Namespace) -> int: commands = { "generate": run_generate, "serve": run_serve, + "ui": run_ui, "ingest": run_ingest, "ingest-s3": run_s3_ingest, "ingest-local": run_local_ingest, @@ -1045,7 +1089,7 @@ async def main_async(args: argparse.Namespace) -> int: } if args.command in commands: - if args.command in ["generate", "serve"]: + if args.command in ["generate", "serve", "ui"]: return await commands[args.command](args) else: return commands[args.command](args) diff --git a/opus_orchestrator/server.py b/opus_orchestrator/server.py index 8bca18b..42e06c1 100644 --- a/opus_orchestrator/server.py +++ b/opus_orchestrator/server.py @@ -97,7 +97,7 @@ async def lifespan(app: FastAPI): # CREATE APP # ============================================================================= -def create_app() -> FastAPI: +def create_app(include_ui: bool = True) -> FastAPI: """Create and configure the FastAPI application.""" app = FastAPI( title="Opus Orchestrator API", @@ -110,6 +110,7 @@ def create_app() -> FastAPI: - **AutoGen Critique**: Multi-agent debate for editorial feedback - **PydanticAI Validation**: Structured output validation - **GitHub Ingestion**: Pull content from repositories +- **S3 Upload**: Upload manuscripts to S3-compatible storage ## Quick Start @@ -126,6 +127,13 @@ curl -X POST "http://localhost:8000/ingest" \\ -H "Content-Type: application/json" \\ -d '{"repo": "owner/my-book-notes"}' ``` + +3. Upload to S3: +```bash +curl -X POST "http://localhost:8000/upload/s3" \\ + -H "Content-Type: application/json" \\ + -d '{"content": "# My Manuscript", "bucket": "my-bucket", "key": "output/story.md"}' +``` """, version="0.2.0", lifespan=lifespan, @@ -134,6 +142,11 @@ curl -X POST "http://localhost:8000/ingest" \\ openapi_url="/openapi.json", ) + # Add web UI if requested + if include_ui: + from opus_orchestrator.web_ui import create_web_ui + create_web_ui(app) + return app @@ -261,6 +274,84 @@ async def ingest(request: IngestRequest): raise HTTPException(status_code=500, detail=str(e)) +# ============================================================================= +# UPLOAD ENDPOINTS +# ============================================================================= + +class UploadResponse(BaseModel): + """Response from file upload.""" + filename: str + content: str + size: int + status: str + + +class S3UploadRequest(BaseModel): + """Request to upload content to S3.""" + content: str + bucket: str + key: str + endpoint_url: Optional[str] = None + + +class S3UploadResponse(BaseModel): + """Response from S3 upload.""" + bucket: str + key: str + url: str + status: str + + +@app.post("/upload", response_model=UploadResponse, tags=["upload"]) +async def upload_file(file: UploadFile = File(...)): + """Upload a file for processing.""" + try: + content = await file.read() + text_content = content.decode("utf-8") + + return UploadResponse( + filename=file.filename, + content=text_content, + size=len(content), + status="success", + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/upload/s3", response_model=S3UploadResponse, tags=["upload"]) +async def upload_to_s3(request: S3UploadRequest): + """Upload content to S3-compatible storage.""" + try: + from opus_orchestrator import S3Ingestor + + # Create S3 ingestor + s3 = S3Ingestor(endpoint_url=request.endpoint_url) + + # Upload using boto3 directly + s3.s3_client.put_object( + Bucket=request.bucket, + Key=request.key, + Body=request.content.encode("utf-8"), + ContentType="text/markdown", + ) + + # Build URL + if request.endpoint_url: + url = f"{request.endpoint_url}/{request.bucket}/{request.key}" + else: + url = f"s3://{request.bucket}/{request.key}" + + return S3UploadResponse( + bucket=request.bucket, + key=request.key, + url=url, + status="success", + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + # ============================================================================= # SERVER RUNNER # ============================================================================= diff --git a/opus_orchestrator/web_ui.py b/opus_orchestrator/web_ui.py new file mode 100644 index 0000000..71a3f3d --- /dev/null +++ b/opus_orchestrator/web_ui.py @@ -0,0 +1,675 @@ +"""Web UI for Opus Orchestrator. + +A simple, novice-friendly web interface for generating manuscripts. +""" + +import os +import asyncio +from pathlib import Path +from typing import Optional + +from fastapi import FastAPI, Request, UploadFile, File, Form, HTTPException +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from dotenv import load_dotenv + +load_dotenv("/home/solaria/.openclaw/workspace/opus-orchestrator-ai/.env") + + +# HTML Template for the UI +WEB_UI_TEMPLATE = """ + + + + + + Opus Orchestrator - AI Book Generator + + + +
+
+

šŸ“š Opus Orchestrator

+

AI-Powered Book Generation

+
+
+
šŸŽÆ
+
7 Story Frameworks
+
+
+
šŸ¤–
+
AI Critique
+
+
+
ā˜ļø
+
Cloud Storage
+
+
+
+ +
+

Generate Your Manuscript

+ +
+ +
+ + + + +
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+ +
+ +

šŸ“‚ Drag & drop files here
or click to browse

+
+
+
+
+ + +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ + +
+ + +
+ + + + + + +
+
+ + +
+
+

Initializing...

+
+ + +
+

Your Manuscript

+
+

+            
+            
+        
+ + +
+ + + + +""" + + +def create_web_ui(app: FastAPI) -> None: + """Create and mount the web UI.""" + from fastapi.responses import HTMLResponse + + @app.get("/", response_class=HTMLResponse) + async def index(): + """Serve the main UI page.""" + return WEB_UI_TEMPLATE + + @app.get("/ui", response_class=HTMLResponse) + async def ui(): + """Alias for /""" + return WEB_UI_TEMPLATE