<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TechForge Manufacturing - Smart Quoting System</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* TechForge Manufacturing Brand Colors */
--primary-blue: #003d82;
--primary-orange: #ff6b35;
--dark-blue: #002347;
--light-blue: #0066cc;
--accent-yellow: #ffc107;
--gray-dark: #2c3e50;
--gray-medium: #34495e;
--gray-light: #ecf0f1;
--white: #ffffff;
--success: #27ae60;
--warning: #f39c12;
--danger: #e74c3c;
}
body {
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, var(--dark-blue) 0%, var(--primary-blue) 100%);
min-height: 100vh;
padding: 0;
}
/* Top Brand Bar */
.brand-bar {
background: var(--dark-blue);
padding: 15px 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
display: flex;
justify-content: space-between;
align-items: center;
}
.brand-logo {
display: flex;
align-items: center;
gap: 15px;
}
.logo-icon {
width: 50px;
height: 50px;
background: var(--primary-orange);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 24px;
color: white;
box-shadow: 0 4px 8px rgba(255, 107, 53, 0.3);
}
.brand-text h1 {
color: var(--white);
font-size: 28px;
font-weight: 700;
margin: 0;
letter-spacing: -0.5px;
}
.brand-text p {
color: var(--primary-orange);
font-size: 13px;
margin: 0;
font-weight: 500;
letter-spacing: 1px;
text-transform: uppercase;
}
.brand-tagline {
color: var(--gray-light);
font-size: 14px;
font-style: italic;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
background: var(--white);
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
border-left: 5px solid var(--primary-orange);
}
header h2 {
color: var(--primary-blue);
font-size: 28px;
margin-bottom: 10px;
}
.subtitle {
color: var(--gray-medium);
font-size: 16px;
}
.nav-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.nav-tab {
background: var(--white);
border: 2px solid var(--primary-blue);
padding: 15px 30px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
color: var(--primary-blue);
}
.nav-tab:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,61,130,0.2);
background: var(--light-blue);
color: white;
border-color: var(--light-blue);
}
.nav-tab.active {
background: var(--primary-orange);
color: white;
border-color: var(--primary-orange);
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
}
.content-panel {
display: none;
}
.content-panel.active {
display: block;
}
.card {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
margin-bottom: 20px;
border-left: 5px solid var(--primary-blue);
}
.card h2 {
color: var(--primary-blue);
margin-bottom: 20px;
font-size: 24px;
display: flex;
align-items: center;
gap: 10px;
}
.card h2::before {
content: "⚙";
color: var(--primary-orange);
font-size: 28px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: var(--gray-dark);
font-weight: 600;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
input[type="text"],
input[type="email"],
input[type="number"],
textarea {
width: 100%;
padding: 12px 15px;
border: 2px solid var(--gray-light);
border-radius: 8px;
font-size: 14px;
transition: all 0.3s;
font-family: inherit;
}
input:focus,
textarea:focus {
outline: none;
border-color: var(--primary-orange);
box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.1);
}
textarea {
min-height: 120px;
resize: vertical;
}
.btn {
background: var(--primary-orange);
color: white;
border: none;
padding: 14px 35px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 4px 8px rgba(255, 107, 53, 0.3);
}
.btn:hover {
background: #ff5722;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(255, 107, 53, 0.4);
}
.btn:disabled {
background: var(--gray-light);
color: var(--gray-medium);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-secondary {
background: var(--primary-blue);
box-shadow: 0 4px 8px rgba(0, 61, 130, 0.3);
}
.btn-secondary:hover {
background: var(--light-blue);
box-shadow: 0 6px 12px rgba(0, 61, 130, 0.4);
}
.results {
margin-top: 30px;
}
.alert {
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 5px solid;
font-weight: 500;
}
.alert-success {
background: #d4edda;
color: #155724;
border-left-color: var(--success);
}
.alert-warning {
background: #fff3cd;
color: #856404;
border-left-color: var(--warning);
}
.alert-danger {
background: #f8d7da;
color: #721c24;
border-left-color: var(--danger);
}
.alert-info {
background: #d1ecf1;
color: #0c5460;
border-left-color: var(--light-blue);
}
.match-card {
background: var(--gray-light);
border: 2px solid #ddd;
border-left: 5px solid var(--primary-orange);
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s;
}
.match-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transform: translateX(5px);
}
.match-score {
display: inline-block;
padding: 6px 16px;
border-radius: 20px;
font-weight: bold;
font-size: 14px;
}
.score-high {
background: var(--success);
color: white;
}
.score-medium {
background: var(--warning);
color: white;
}
.score-low {
background: var(--danger);
color: white;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.stat-card {
background: var(--white);
border: 3px solid var(--primary-blue);
color: var(--primary-blue);
padding: 30px;
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: all 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0,61,130,0.2);
border-color: var(--primary-orange);
}
.stat-value {
font-size: 48px;
font-weight: bold;
margin-bottom: 10px;
color: var(--primary-orange);
}
.stat-label {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--primary-blue);
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid var(--gray-light);
border-top: 3px solid var(--primary-orange);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.quote-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.quote-table th,
.quote-table td {
padding: 15px;
text-align: left;
border-bottom: 2px solid var(--gray-light);
}
.quote-table th {
background: var(--primary-blue);
font-weight: 600;
color: white;
text-transform: uppercase;
font-size: 13px;
letter-spacing: 0.5px;
}
.quote-table tr:hover {
background: rgba(0, 61, 130, 0.05);
}
.badge {
display: inline-block;
padding: 5px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-success {
background: var(--success);
color: white;
}
.badge-draft {
background: var(--warning);
color: white;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.cost-breakdown {
background: var(--gray-light);
padding: 20px;
border-radius: 8px;
margin-top: 15px;
border-left: 4px solid var(--primary-orange);
}
.cost-item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #ddd;
font-size: 15px;
}
.cost-item:last-child {
border-bottom: none;
font-weight: bold;
font-size: 20px;
margin-top: 10px;
padding-top: 15px;
border-top: 3px solid var(--primary-blue);
color: var(--primary-blue);
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--gray-medium);
}
.footer {
background: var(--dark-blue);
color: var(--gray-light);
padding: 20px;
text-align: center;
margin-top: 40px;
border-radius: 12px;
}
.footer p {
margin: 5px 0;
font-size: 14px;
}
/* Industrial accent elements */
h3 {
color: var(--primary-blue);
border-bottom: 3px solid var(--primary-orange);
padding-bottom: 10px;
margin-bottom: 20px;
margin-top: 30px;
}
@media (max-width: 768px) {
.two-col {
grid-template-columns: 1fr;
}
.nav-tabs {
flex-direction: column;
}
.brand-bar {
flex-direction: column;
gap: 15px;
text-align: center;
}
.stat-value {
font-size: 36px;
}
}
</style>
</head>
<body>
<!-- Brand Header -->
<div class="brand-bar">
<div class="brand-logo">
<div class="logo-icon">TF</div>
<div class="brand-text">
<h1>TechForge Manufacturing</h1>
<p>Precision Engineering Solutions</p>
</div>
</div>
<div class="brand-tagline">
"Forging Innovation Since 1985"
</div>
</div>
<div class="container">
<header>
<h2>Smart Quoting System</h2>
<p class="subtitle">AI-powered quote generation with historical data intelligence</p>
</header>
<div class="nav-tabs">
<button class="nav-tab active" onclick="switchTab('new-quote')">New Quote</button>
<button class="nav-tab" onclick="switchTab('history')">Quote History</button>
<button class="nav-tab" onclick="switchTab('dashboard')">Dashboard</button>
</div>
<!-- New Quote Panel -->
<div id="new-quote" class="content-panel active">
<div class="card">
<h2>Create New Quote</h2>
<form id="rfpForm" onsubmit="submitRFP(event)">
<div class="form-group">
<label for="rfpText">RFP Description *</label>
<textarea
id="rfpText"
name="rfpText"
required
placeholder="Example: We need 200 pieces of 6061-T6 aluminum brackets, CNC machined with anodized finish. Tolerance +/-0.005. Delivery needed by March 2025."></textarea>
<small style="color: #666;">Include material, quantity, processes, tolerances, and any special requirements</small>
</div>
<div class="two-col">
<div class="form-group">
<label for="quantity">Quantity *</label>
<input type="number" id="quantity" name="quantity" required min="1" placeholder="200">
</div>
<div class="form-group">
<label for="customerName">Customer Name</label>
<input type="text" id="customerName" name="customerName" placeholder="Acme Manufacturing">
</div>
</div>
<div class="two-col">
<div class="form-group">
<label for="contactEmail">Contact Email</label>
<input type="email" id="contactEmail" name="contactEmail" placeholder="buyer@example.com">
</div>
<div class="form-group">
<label for="dueDate">Due Date</label>
<input type="text" id="dueDate" name="dueDate" placeholder="2025-03-15">
</div>
</div>
<button type="submit" class="btn" id="submitBtn">
Generate Quote
</button>
<button type="button" class="btn btn-secondary" onclick="clearForm()" style="margin-left: 10px;">
Clear Form
</button>
</form>
<div id="results" class="results" style="display: none;"></div>
</div>
</div>
<!-- Historical Quotes Panel -->
<div id="history" class="content-panel">
<div class="card">
<h2>Historical Quote Database</h2>
<button class="btn" onclick="loadHistoricalQuotes()" style="margin-bottom: 20px;">
Refresh Data
</button>
<div id="historicalQuotesTable"></div>
</div>
</div>
<!-- Dashboard Panel -->
<div id="dashboard" class="content-panel">
<div class="card">
<h2>System Overview</h2>
<div class="grid" id="dashboardStats">
<div class="stat-card">
<div class="stat-value" id="totalQuotes">--</div>
<div class="stat-label">Total Quotes</div>
</div>
<div class="stat-card">
<div class="stat-value" id="avgLeadTime">--</div>
<div class="stat-label">Avg Lead Time (Days)</div>
</div>
<div class="stat-card">
<div class="stat-value" id="avgCost">--</div>
<div class="stat-label">Avg Cost Per Unit</div>
</div>
</div>
</div>
<div class="card">
<h2>Quick Actions</h2>
<button class="btn" onclick="loadSampleRFQ()" style="margin-right: 10px;">Load Sample RFQ</button>
<button class="btn btn-secondary" onclick="window.location.reload()">Refresh Page</button>
</div>
<div class="card">
<h2>System Status</h2>
<div id="systemStatus">Checking...</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<p><strong>TechForge Manufacturing</strong> | 1985 Industrial Way, Manufacturing City, USA</p>
<p>Phone: (555) 123-4567 | Email: quotes@techforge.example.com</p>
<p>© 2024 TechForge Manufacturing. All rights reserved.</p>
</div>
</div>
<script>
const API_BASE = window.location.origin;
// Tab switching
function switchTab(tabName) {
document.querySelectorAll('.content-panel').forEach(panel => {
panel.classList.remove('active');
});
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.classList.remove('active');
});
document.getElementById(tabName).classList.add('active');
event.target.classList.add('active');
if (tabName === 'history') {
loadHistoricalQuotes();
} else if (tabName === 'dashboard') {
loadDashboard();
}
}
// Submit RFP Form
async function submitRFP(event) {
event.preventDefault();
const submitBtn = document.getElementById('submitBtn');
const resultsDiv = document.getElementById('results');
submitBtn.disabled = true;
submitBtn.innerHTML = 'Processing...<span class="loading"></span>';
resultsDiv.style.display = 'none';
const formData = {
rfp: {
rawText: document.getElementById('rfpText').value,
qty: parseInt(document.getElementById('quantity').value),
customerName: document.getElementById('customerName').value || undefined,
contactEmail: document.getElementById('contactEmail').value || undefined,
dueDate: document.getElementById('dueDate').value || undefined
}
};
try {
const response = await fetch(`${API_BASE}/mcp/invoke/evaluateRfpAndDraftQuote`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await response.json();
if (data.ok) {
displayResults(data.result);
} else {
showError(data.error || 'Failed to process RFP');
}
} catch (error) {
showError('Failed to connect to server. Is it running?');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = 'Generate Quote';
}
}
// Display results
function displayResults(result) {
const { parsedRfp, matches, estimate, doc } = result;
const resultsDiv = document.getElementById('results');
const confidenceClass = estimate.confidence === 'high' ? 'success' :
estimate.confidence === 'medium' ? 'warning' : 'danger';
let html = `
<div class="alert alert-${confidenceClass}">
<strong>Confidence: ${estimate.confidence.toUpperCase()}</strong><br>
${getConfidenceMessage(estimate.confidence)}
</div>
<h3>Parsed RFP</h3>
<div style="background: #ecf0f1; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #ff6b35;">
<p><strong>Material:</strong> ${parsedRfp.material || 'Not specified'}</p>
<p><strong>Processes:</strong> ${parsedRfp.processes?.join(', ') || 'Not specified'}</p>
<p><strong>Quantity:</strong> ${parsedRfp.qty}</p>
<p><strong>Tolerances:</strong> ${parsedRfp.tolerances || 'Not specified'}</p>
</div>
<h3>Similar Historical Quotes (${matches.length} found)</h3>
`;
if (matches.length > 0) {
matches.slice(0, 3).forEach(match => {
const scorePercent = (match.score * 100).toFixed(1);
const scoreClass = match.score >= 0.85 ? 'high' : match.score >= 0.70 ? 'medium' : 'low';
html += `
<div class="match-card">
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<strong style="color: var(--primary-blue);">${match.quote.id}</strong>
<span class="match-score score-${scoreClass}">${scorePercent}% Match</span>
</div>
<p><strong>Customer:</strong> ${match.quote.customerName || 'N/A'}</p>
<p><strong>Cost:</strong> $${match.quote.costPerUnit.toFixed(2)}/unit | Lead: ${match.quote.leadDays} days</p>
<p><strong>Matching:</strong> ${match.matchingFields.join(', ')}</p>
<p style="color: #666; font-size: 14px; margin-top: 5px;">${match.notes}</p>
</div>
`;
});
} else {
html += `<p style="color: #666;">No similar historical quotes found. This appears to be a new type of request.</p>`;
}
html += `
<h3>Cost Estimate</h3>
<div class="cost-breakdown">
<div class="cost-item">
<span>Material Cost</span>
<span>$${estimate.breakdown.materialCost.toFixed(2)}</span>
</div>
<div class="cost-item">
<span>Processing Cost</span>
<span>$${estimate.breakdown.processCost.toFixed(2)}</span>
</div>
<div class="cost-item">
<span>Tooling (per unit)</span>
<span>$${estimate.breakdown.toolingPerUnit.toFixed(2)}</span>
</div>
<div class="cost-item">
<span>Overhead</span>
<span>$${estimate.breakdown.overhead.toFixed(2)}</span>
</div>
<div class="cost-item">
<span>Price Per Unit</span>
<span>$${estimate.pricePerUnit.toFixed(2)}</span>
</div>
<div class="cost-item">
<span>Total Price (${parsedRfp.qty} units)</span>
<span>$${estimate.totalPrice.toFixed(2)}</span>
</div>
</div>
<h3 style="margin-top: 30px;">Quote Details</h3>
<div style="background: #ecf0f1; padding: 15px; border-radius: 8px; border-left: 4px solid #003d82;">
<p><strong>Quote ID:</strong> ${doc.quoteId}</p>
<p><strong>Lead Time:</strong> ${estimate.leadDays} days</p>
<p><strong>Status:</strong> <span class="badge badge-draft">DRAFT</span></p>
<p><strong>Payment Terms:</strong> ${doc.terms}</p>
</div>
`;
resultsDiv.innerHTML = html;
resultsDiv.style.display = 'block';
resultsDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function getConfidenceMessage(confidence) {
if (confidence === 'high') {
return 'This estimate is based on very similar past work. The pricing should be reliable.';
} else if (confidence === 'medium') {
return 'This estimate is based on similar work, but some adjustments may be needed. Review before sending.';
} else {
return 'This is a new type of work with limited historical data. Engineering review is strongly recommended.';
}
}
// Load historical quotes
async function loadHistoricalQuotes() {
const tableDiv = document.getElementById('historicalQuotesTable');
tableDiv.innerHTML = '<p>Loading...</p>';
try {
const response = await fetch(`${API_BASE}/mcp/utility/historicalQuotes`);
const data = await response.json();
if (data.ok && data.quotes.length > 0) {
let html = `
<table class="quote-table">
<thead>
<tr>
<th>Quote ID</th>
<th>Customer</th>
<th>Material</th>
<th>Processes</th>
<th>Qty Range</th>
<th>Cost/Unit</th>
<th>Lead Days</th>
<th>Status</th>
</tr>
</thead>
<tbody>
`;
data.quotes.forEach(quote => {
html += `
<tr>
<td><strong>${quote.id}</strong></td>
<td>${quote.customerName || 'N/A'}</td>
<td>${quote.normalized.material}</td>
<td>${quote.normalized.processes.join(', ')}</td>
<td>${quote.normalized.qtyRange[0]}-${quote.normalized.qtyRange[1]}</td>
<td>$${quote.costPerUnit.toFixed(2)}</td>
<td>${quote.leadDays}${quote.actualLeadDays ? ` (actual: ${quote.actualLeadDays})` : ''}</td>
<td><span class="badge ${quote.approved ? 'badge-success' : 'badge-draft'}">${quote.approved ? 'Approved' : 'Draft'}</span></td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
tableDiv.innerHTML = html;
} else {
tableDiv.innerHTML = '<div class="empty-state"><p>No historical quotes found.</p></div>';
}
} catch (error) {
tableDiv.innerHTML = '<div class="alert alert-danger">Failed to load historical quotes</div>';
}
}
// Load dashboard
async function loadDashboard() {
try {
const response = await fetch(`${API_BASE}/mcp/utility/historicalQuotes`);
const data = await response.json();
if (data.ok && data.quotes.length > 0) {
const quotes = data.quotes;
document.getElementById('totalQuotes').textContent = quotes.length;
const avgLead = quotes.reduce((sum, q) => sum + q.leadDays, 0) / quotes.length;
document.getElementById('avgLeadTime').textContent = avgLead.toFixed(1);
const avgCost = quotes.reduce((sum, q) => sum + q.costPerUnit, 0) / quotes.length;
document.getElementById('avgCost').textContent = '$' + avgCost.toFixed(2);
}
// Check system status
const healthResponse = await fetch(`${API_BASE}/health`);
const healthData = await healthResponse.json();
document.getElementById('systemStatus').innerHTML = `
<div class="alert alert-success">
<strong>✓ System Online</strong><br>
Server is running and ready to process quotes.
</div>
`;
} catch (error) {
document.getElementById('systemStatus').innerHTML = `
<div class="alert alert-danger">
<strong>✗ System Offline</strong><br>
Cannot connect to server. Please start the server first.
</div>
`;
}
}
// Helper functions
function showError(message) {
const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML = `<div class="alert alert-danger"><strong>Error:</strong> ${message}</div>`;
resultsDiv.style.display = 'block';
}
function clearForm() {
document.getElementById('rfpForm').reset();
document.getElementById('results').style.display = 'none';
}
function loadSampleRFQ() {
document.getElementById('rfpText').value = 'We need 200 pieces of 6061-T6 aluminum brackets, CNC machined with anodized finish. Tolerance +/-0.005. Delivery needed by March 2025.';
document.getElementById('quantity').value = '200';
document.getElementById('customerName').value = 'Sample Customer';
document.getElementById('contactEmail').value = 'buyer@example.com';
document.getElementById('dueDate').value = '2025-03-15';
switchTab('new-quote');
document.querySelector('.nav-tab').click();
}
// Initialize dashboard on page load
window.addEventListener('load', () => {
loadDashboard();
});
</script>
</body>
</html>