Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session
from typing import List
import boto3
Expand All @@ -9,6 +10,7 @@
from app.db.database import get_db, engine, Base
from app.models import models
from app.schemas import schemas
import os

# Create tables
Base.metadata.create_all(bind=engine)
Expand All @@ -17,6 +19,21 @@
security = HTTPBearer()
settings = get_settings()

# Mount static files for Web UI
static_dir = os.path.join(os.path.dirname(__file__), "static")
if os.path.exists(static_dir):
app.mount("/static", StaticFiles(directory=static_dir), name="static")

# Serve index.html at root
@app.get("/")
def serve_index():
"""Serve the Web UI."""
index_path = os.path.join(static_dir, "index.html")
if os.path.exists(index_path):
from fastapi.responses import FileResponse
return FileResponse(index_path)
return {"message": "Agent Suite API", "docs": "/docs"}


def get_inbox_by_api_key(api_key: str, db: Session):
return db.query(models.Inbox).filter(
Expand Down
271 changes: 271 additions & 0 deletions app/static/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
// Agent Suite - Web UI JavaScript

const API_BASE = window.location.origin;

// State
let apiKey = localStorage.getItem('agent_suite_api_key') || '';
let messages = [];

// DOM Elements
const messageListEl = document.getElementById('messageList');
const messageDetailEl = document.getElementById('messageDetail');
const apiKeyModal = document.getElementById('apiKeyModal');
const composeModal = document.getElementById('composeModal');
const emailAddressEl = document.getElementById('emailAddress');

// Initialize
document.addEventListener('DOMContentLoaded', () => {
if (!apiKey) {
showApiKeyModal();
} else {
loadMessages();
loadInboxInfo();
}
setupEventListeners();
});

function setupEventListeners() {
// API Key Modal
document.getElementById('saveApiKey').addEventListener('click', saveApiKey);
document.getElementById('cancelApiKey').addEventListener('click', () => {
if (!apiKey) {
showError('API key is required');
}
hideModal(apiKeyModal);
});

// Settings Button
document.getElementById('settingsBtn').addEventListener('click', () => {
document.getElementById('apiKeyInput').value = apiKey;
showModal(apiKeyModal);
});

// Compose Button
document.getElementById('composeBtn').addEventListener('click', () => {
showModal(composeModal);
});

// Compose Form
document.getElementById('composeForm').addEventListener('submit', sendEmail);
document.getElementById('cancelCompose').addEventListener('click', () => {
hideModal(composeModal);
});

// Back Button
document.getElementById('backBtn').addEventListener('click', () => {
messageDetailEl.classList.add('hidden');
messageListEl.classList.remove('hidden');
});
}

function getHeaders() {
return {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
};
}

async function loadInboxInfo() {
try {
const response = await fetch(`${API_BASE}/v1/inboxes/me`, {
headers: getHeaders()
});

if (response.status === 401) {
showApiKeyModal();
return;
}

const data = await response.json();
emailAddressEl.textContent = data.email_address;
} catch (error) {
console.error('Failed to load inbox info:', error);
}
}

async function loadMessages() {
messageListEl.innerHTML = '<div class="loading">Loading messages...</div>';

try {
const response = await fetch(`${API_BASE}/v1/inboxes/me/messages`, {
headers: getHeaders()
});

if (response.status === 401) {
showApiKeyModal();
return;
}

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

const data = await response.json();
messages = data.messages;
renderMessages(messages);
} catch (error) {
showError(`Failed to load messages: ${error.message}`);
}
}

function renderMessages(messageList) {
if (!messageList || messageList.length === 0) {
messageListEl.innerHTML = `
<div class="empty-state">
<h3>📭 No messages yet</h3>
<p>Your inbox is empty. Send a message to get started!</p>
</div>
`;
return;
}

messageListEl.innerHTML = messageList.map(msg => `
<div class="message-item ${msg.is_read ? '' : 'unread'}" data-id="${msg.id}">
<div class="subject">${escapeHtml(msg.subject || '(No Subject)')}</div>
<div class="preview">${escapeHtml(msg.body_text || '').substring(0, 100)}</div>
<div class="meta">
<span>${escapeHtml(msg.sender)}</span>
<span>${formatDate(msg.received_at)}</span>
</div>
</div>
`).join('');

// Add click handlers
document.querySelectorAll('.message-item').forEach(item => {
item.addEventListener('click', () => {
const msgId = item.dataset.id;
showMessageDetail(msgId);
});
});
}

async function showMessageDetail(msgId) {
const msg = messages.find(m => m.id === msgId);
if (!msg) return;

// Mark as read
if (!msg.is_read) {
msg.is_read = true;
// Optionally call API to mark as read
}

document.getElementById('detailSubject').textContent = msg.subject || '(No Subject)';
document.getElementById('detailFrom').textContent = msg.sender;
document.getElementById('detailTo').textContent = msg.recipient;
document.getElementById('detailDate').textContent = formatDate(msg.received_at);
document.getElementById('detailBody').textContent = msg.body_text || '(No content)';

messageListEl.classList.add('hidden');
messageDetailEl.classList.remove('hidden');
}

async function sendEmail(e) {
e.preventDefault();

const to = document.getElementById('toInput').value;
const subject = document.getElementById('subjectInput').value;
const body = document.getElementById('bodyInput').value;

const submitBtn = e.target.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = 'Sending...';

try {
const response = await fetch(`${API_BASE}/v1/inboxes/me/send`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ to, subject, body })
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to send');
}

const result = await response.json();

// Show success and close modal
hideModal(composeModal);
e.target.reset();

// Show success message
const successEl = document.createElement('div');
successEl.className = 'success';
successEl.textContent = `✓ Message sent! Message ID: ${result.message_id}`;
messageListEl.insertBefore(successEl, messageListEl.firstChild);

// Refresh messages
loadMessages();

} catch (error) {
showError(`Failed to send: ${error.message}`);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Send';
}
}

function saveApiKey() {
const input = document.getElementById('apiKeyInput').value.trim();
if (!input) {
showError('Please enter a valid API key');
return;
}

apiKey = input;
localStorage.setItem('agent_suite_api_key', apiKey);
hideModal(apiKeyModal);
loadMessages();
loadInboxInfo();
}

function showApiKeyModal() {
document.getElementById('apiKeyInput').value = apiKey;
showModal(apiKeyModal);
}

function showModal(modal) {
modal.classList.remove('hidden');
}

function hideModal(modal) {
modal.classList.add('hidden');
}

function showError(message) {
const errorEl = document.createElement('div');
errorEl.className = 'error';
errorEl.textContent = message;
messageListEl.insertBefore(errorEl, messageListEl.firstChild);

// Auto-remove after 5 seconds
setTimeout(() => errorEl.remove(), 5000);
}

function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const now = new Date();
const diff = now - date;

// Less than 24 hours
if (diff < 86400000) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}

// Less than 7 days
if (diff < 604800000) {
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return days[date.getDay()] + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}

// Older
return date.toLocaleDateString();
}
84 changes: 84 additions & 0 deletions app/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent Suite - Inbox</title>
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<div class="app-container">
<header>
<h1>📧 Agent Suite</h1>
<div class="header-actions">
<button id="composeBtn" class="btn btn-primary">+ Compose</button>
<button id="settingsBtn" class="btn btn-secondary">⚙️ API Key</button>
</div>
</header>

<main>
<div class="inbox-header">
<h2>Inbox</h2>
<span id="emailAddress" class="email-badge">-</span>
</div>

<div id="messageList" class="message-list">
<div class="loading">Loading messages...</div>
</div>

<div id="messageDetail" class="message-detail hidden">
<button id="backBtn" class="btn btn-secondary">← Back</button>
<div class="detail-header">
<h3 id="detailSubject"></h3>
<span id="detailDate" class="date"></span>
</div>
<div class="detail-meta">
<div><strong>From:</strong> <span id="detailFrom"></span></div>
<div><strong>To:</strong> <span id="detailTo"></span></div>
</div>
<div id="detailBody" class="detail-body"></div>
</div>
</main>
</div>

<!-- API Key Modal -->
<div id="apiKeyModal" class="modal hidden">
<div class="modal-content">
<h3>API Key Required</h3>
<p>Enter your API key to access your inbox:</p>
<input type="text" id="apiKeyInput" placeholder="as_xxxxxxxxxxxxx" />
<div class="modal-actions">
<button id="saveApiKey" class="btn btn-primary">Save</button>
<button id="cancelApiKey" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>

<!-- Compose Modal -->
<div id="composeModal" class="modal hidden">
<div class="modal-content">
<h3>New Message</h3>
<form id="composeForm">
<div class="form-group">
<label for="toInput">To:</label>
<input type="email" id="toInput" required placeholder="[email protected]" />
</div>
<div class="form-group">
<label for="subjectInput">Subject:</label>
<input type="text" id="subjectInput" required placeholder="Subject" />
</div>
<div class="form-group">
<label for="bodyInput">Body:</label>
<textarea id="bodyInput" rows="8" required placeholder="Write your message..."></textarea>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Send</button>
<button type="button" id="cancelCompose" class="btn btn-secondary">Cancel</button>
</div>
</form>
</div>
</div>

<script src="/static/app.js"></script>
</body>
</html>
Loading