<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>๐ Tech Stack Advisor v2.0</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script>
console.log('Tech Stack Advisor v2.0 - BYOK Enabled');
// Initialize Mermaid
if (typeof mermaid !== 'undefined') {
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose'
});
console.log('Mermaid initialized');
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
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;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
position: relative;
}
.user-info {
position: absolute;
top: 20px;
right: 30px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
.user-info-row {
display: flex;
align-items: center;
gap: 15px;
}
.user-info-text {
font-size: 14px;
opacity: 0.9;
}
.user-stats {
display: flex;
gap: 15px;
font-size: 12px;
opacity: 0.85;
}
.user-stat {
display: flex;
align-items: center;
gap: 5px;
}
.logout-btn {
padding: 8px 20px;
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.logout-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.login-link {
padding: 8px 20px;
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s;
display: inline-block;
}
.login-link:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.subtitle {
opacity: 0.9;
font-size: 1.1em;
}
.input-section {
padding: 30px;
background: #f8f9fa;
}
.input-group {
display: flex;
gap: 10px;
max-width: 800px;
margin: 0 auto;
}
#queryInput {
flex: 1;
padding: 15px;
border: 2px solid #ddd;
border-radius: 10px;
font-size: 16px;
transition: border-color 0.3s;
}
#queryInput:focus {
outline: none;
border-color: #667eea;
}
button {
padding: 15px 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading {
text-align: center;
padding: 40px;
display: none;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.results {
padding: 30px;
display: none;
}
.tabs {
display: flex;
border-bottom: 2px solid #ddd;
margin-bottom: 30px;
overflow-x: auto;
}
.tab {
padding: 15px 25px;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s;
white-space: nowrap;
}
.tab:hover {
background: #f8f9fa;
}
.tab.active {
border-bottom-color: #667eea;
color: #667eea;
font-weight: 600;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.card h3 {
color: #667eea;
margin-bottom: 15px;
}
.expandable {
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 15px;
overflow: hidden;
}
.expandable-header {
padding: 15px;
background: #fff;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
}
.expandable-header:hover {
background: #f8f9fa;
}
.expandable-content {
padding: 15px;
background: #fff;
display: none;
max-height: 600px; /* Set a max height to enable scrolling */
overflow-y: auto; /* Enable vertical scrolling */
}
/* Custom scrollbar styling for better visibility */
.expandable-content::-webkit-scrollbar {
width: 12px;
}
.expandable-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
}
.expandable-content::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
}
.expandable-content::-webkit-scrollbar-thumb:hover {
background: #555;
}
.reference-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
padding: 20px;
margin: 20px 0;
color: white;
}
.reference-section h3 {
margin-top: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
}
.reference-content {
display: none;
background: white;
color: #333;
padding: 20px;
border-radius: 8px;
margin-top: 15px;
}
.reference-content.show {
display: block;
}
.reference-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.reference-table th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px;
text-align: left;
font-weight: 600;
}
.reference-table td {
padding: 10px 12px;
border-bottom: 1px solid #e5e7eb;
}
.reference-table tr:hover {
background: #f9fafb;
}
.reference-table tr:last-child td {
border-bottom: none;
}
.formula-box {
background: #f3f4f6;
border-left: 4px solid #667eea;
padding: 15px;
margin: 15px 0;
border-radius: 4px;
}
.formula-box code {
background: #e5e7eb;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
.note-box {
background: #fef3c7;
border-left: 4px solid #f59e0b;
padding: 12px;
margin: 10px 0;
border-radius: 4px;
font-size: 0.9em;
}
.info-box {
background: linear-gradient(135deg, #e0f2fe 0%, #dbeafe 100%);
border: 2px solid #3b82f6;
border-radius: 12px;
padding: 25px;
margin: 20px 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.info-box h3 {
color: #1e40af;
margin-top: 0;
margin-bottom: 15px;
font-size: 1.3em;
display: flex;
align-items: center;
gap: 10px;
}
.info-box p {
color: #1e3a8a;
line-height: 1.6;
margin-bottom: 15px;
}
.agent-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-top: 20px;
}
.agent-card {
background: white;
border-left: 4px solid #3b82f6;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.agent-card h4 {
color: #1e40af;
margin-top: 0;
margin-bottom: 8px;
font-size: 1.1em;
}
.agent-card p {
color: #475569;
font-size: 0.9em;
margin: 5px 0;
line-height: 1.5;
}
.api-key-section {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border: 2px solid #f59e0b;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
}
.api-key-section h4 {
margin: 0 0 10px 0;
color: #92400e;
font-size: 1em;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
}
.api-key-content {
display: none;
margin-top: 10px;
}
.api-key-content.show {
display: block;
}
.radio-option {
margin: 10px 0;
padding: 10px;
background: white;
border-radius: 4px;
border: 2px solid transparent;
cursor: pointer;
transition: border-color 0.2s;
}
.radio-option:hover {
border-color: #f59e0b;
}
.radio-option.selected {
border-color: #f59e0b;
background: #fffbeb;
}
.radio-option input[type="radio"] {
margin-right: 10px;
}
.api-key-input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
font-family: 'Courier New', monospace;
margin-top: 10px;
}
.budget-info {
font-size: 0.85em;
color: #92400e;
margin-top: 5px;
padding-left: 28px;
}
.api-key-benefits {
font-size: 0.85em;
color: #065f46;
margin-top: 5px;
padding-left: 28px;
}
.expandable.open .expandable-content {
display: block;
}
.expandable .arrow {
transition: transform 0.3s;
}
.expandable.open .arrow {
transform: rotate(90deg);
}
.pros-cons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-top: 10px;
}
.pros, .cons {
padding: 10px;
}
.pros h4 {
color: #28a745;
margin-bottom: 10px;
}
.cons h4 {
color: #dc3545;
margin-bottom: 10px;
}
.pros ul, .cons ul {
list-style: none;
padding-left: 0;
}
.pros li::before {
content: "โ ";
color: #28a745;
font-weight: bold;
margin-right: 5px;
}
.cons li::before {
content: "โ ";
color: #dc3545;
font-weight: bold;
margin-right: 5px;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 20px;
border-radius: 10px;
margin: 20px;
display: none;
}
pre {
background: #f4f4f4;
padding: 15px;
border-radius: 8px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
max-height: none; /* Ensure no height limit */
}
.badge {
display: inline-block;
padding: 5px 10px;
border-radius: 5px;
font-size: 0.85em;
font-weight: 600;
margin-right: 5px;
margin-bottom: 5px;
}
.badge-success {
background: #d4edda;
color: #155724;
}
.badge-warning {
background: #fff3cd;
color: #856404;
}
.badge-danger {
background: #f8d7da;
color: #721c24;
}
.badge-info {
background: #d1ecf1;
color: #0c5460;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.metric-card {
background: white;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #667eea;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.metric-label {
font-size: 0.85em;
color: #666;
margin-bottom: 5px;
}
.metric-value {
font-size: 1.5em;
font-weight: 700;
color: #333;
}
.context-section {
background: #f8f9fa;
padding: 25px;
margin-bottom: 20px;
border-radius: 10px;
}
.context-section h3 {
color: #667eea;
margin-bottom: 15px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: white;
border-radius: 8px;
overflow: hidden;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #667eea;
color: white;
font-weight: 600;
}
tr:hover {
background: #f8f9fa;
}
.chart-container {
position: relative;
height: 400px;
margin: 20px 0;
background: white;
padding: 20px;
border-radius: 8px;
}
.threat-section {
margin: 15px 0;
}
.threat-high {
border-left: 4px solid #dc3545;
}
.threat-medium {
border-left: 4px solid #ffc107;
}
.threat-low {
border-left: 4px solid #28a745;
}
.threat-list {
padding: 15px;
background: white;
border-radius: 8px;
margin-bottom: 10px;
}
.scale-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin: 15px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.scale-metric {
text-align: center;
}
.scale-metric-label {
font-size: 0.8em;
color: #666;
margin-bottom: 5px;
}
.scale-metric-value {
font-size: 1.2em;
font-weight: 600;
color: #667eea;
}
.status-bar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.status-section {
display: flex;
gap: 30px;
align-items: center;
flex-wrap: wrap;
}
.status-item {
color: white;
display: flex;
flex-direction: column;
gap: 3px;
}
.status-label {
font-size: 0.7em;
opacity: 0.8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-value {
font-size: 1.1em;
font-weight: 600;
}
.status-healthy {
color: #4ade80;
}
.status-offline {
color: #f87171;
}
@media (max-width: 768px) {
.status-bar {
flex-direction: column;
align-items: flex-start;
}
}
.footer {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
margin-top: 30px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.footer p {
margin: 8px 0;
opacity: 0.9;
}
.footer a {
color: #fff;
text-decoration: underline;
opacity: 0.95;
}
.footer a:hover {
opacity: 1;
}
.copyright {
font-size: 0.95em;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.diagram-container {
background: white;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow-x: auto;
}
.diagram-container h4 {
color: #667eea;
margin-bottom: 15px;
}
.mermaid {
text-align: center;
}
/* Chat Modal Styles */
.chat-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
justify-content: center;
align-items: center;
}
.chat-modal.active {
display: flex;
}
.chat-container {
background: white;
border-radius: 20px;
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.chat-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 20px 20px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h3 {
margin: 0;
font-size: 1.3em;
}
.chat-progress {
background: rgba(255,255,255,0.3);
height: 8px;
border-radius: 4px;
margin-top: 10px;
overflow: hidden;
}
.chat-progress-bar {
background: #4ade80;
height: 100%;
transition: width 0.3s;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
min-height: 200px;
max-height: 400px;
}
.chat-message {
margin-bottom: 15px;
display: flex;
gap: 10px;
animation: slideIn 0.3s;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.chat-message.user {
justify-content: flex-end;
}
.chat-message .avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.chat-message.assistant .avatar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.chat-message.user .avatar {
background: #e2e8f0;
}
.chat-bubble {
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
word-wrap: break-word;
}
.chat-message.assistant .chat-bubble {
background: #f1f5f9;
color: #1e293b;
}
.chat-message.user .chat-bubble {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.chat-input-area {
padding: 20px;
border-top: 1px solid #e2e8f0;
display: flex;
gap: 10px;
}
.chat-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 24px;
font-size: 15px;
outline: none;
transition: border-color 0.3s;
}
.chat-input:focus {
border-color: #667eea;
}
.chat-send-btn {
padding: 12px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 24px;
cursor: pointer;
font-weight: 600;
transition: transform 0.2s;
}
.chat-send-btn:hover:not(:disabled) {
transform: scale(1.05);
}
.chat-send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.chat-cancel-btn {
padding: 12px 24px;
background: white;
color: #dc3545;
border: 2px solid #dc3545;
border-radius: 24px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.chat-cancel-btn:hover {
background: #dc3545;
color: white;
transform: scale(1.05);
}
.chat-loading {
display: flex;
gap: 5px;
padding: 10px;
}
.chat-loading span {
width: 8px;
height: 8px;
background: #94a3b8;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.chat-loading span:nth-child(1) { animation-delay: -0.32s; }
.chat-loading span:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
/* Choice Button Styles */
.chat-choices {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
padding: 0 10px;
}
.choice-btn {
flex: 0 1 calc(50% - 5px);
padding: 12px 16px;
background: white;
border: 2px solid #e2e8f0;
border-radius: 12px;
font-size: 14px;
font-weight: 500;
color: #1e293b;
cursor: pointer;
transition: all 0.2s;
text-align: left;
}
.choice-btn:hover {
border-color: #667eea;
background: #f8f9ff;
transform: translateY(-2px);
}
.choice-btn.selected {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
color: white;
}
.choice-btn:active {
transform: translateY(0);
}
.choice-btn.wide {
flex: 1 1 100%;
}
.choice-done-btn {
width: 100%;
padding: 12px 24px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
border: none;
border-radius: 12px;
font-weight: 600;
cursor: pointer;
margin-top: 10px;
transition: all 0.2s;
}
.choice-done-btn:hover {
transform: translateY(-2px);
}
.choice-done-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="user-info" id="userInfo" style="display: none;">
<div class="user-info-row">
<span class="user-info-text" id="userEmail"></span>
<button class="logout-btn" onclick="handleLogout()">Logout</button>
</div>
<div class="user-stats" id="userStats">
<div class="user-stat">
<span>๐ Queries: <strong id="userQueries">0</strong></span>
</div>
<div class="user-stat" id="userCostStat">
<span>๐ฐ Cost: $<strong id="userCost">0.00</strong></span>
</div>
<div class="user-stat" id="userBudgetStat" style="display: none;">
<span>๐ณ Budget Left: $<strong id="userBudget">0.00</strong></span>
</div>
</div>
</div>
<div class="user-info" id="loginInfo">
<a href="/login.html" class="login-link">Login</a>
</div>
<h1>๐ Tech Stack Advisor</h1>
<p class="subtitle">AI-Powered Multi-Agent System for Intelligent Tech Stack Recommendations</p>
</header>
<!-- System Status Bar -->
<div class="status-bar">
<div class="status-section">
<div class="status-item">
<span class="status-label">Status</span>
<span class="status-value" id="healthStatus">โณ Loading...</span>
</div>
<div class="status-item">
<span class="status-label">Agents</span>
<span class="status-value" id="agentsLoaded">-</span>
</div>
<div class="status-item">
<span class="status-label">Uptime</span>
<span class="status-value" id="uptime">-</span>
</div>
</div>
<div class="status-section">
<div class="status-item">
<span class="status-label">Total Requests</span>
<span class="status-value" id="totalRequests">-</span>
</div>
<div class="status-item">
<span class="status-label">Daily Queries</span>
<span class="status-value" id="dailyQueries">-</span>
</div>
<div class="status-item">
<span class="status-label">Daily Cost</span>
<span class="status-value" id="dailyCost">-</span>
</div>
<div class="status-item">
<span class="status-label">Budget Left</span>
<span class="status-value" id="budgetLeft">-</span>
</div>
</div>
</div>
<div class="input-section">
<div class="input-group">
<input
type="text"
id="queryInput"
placeholder="Describe your project (e.g., 'Building a real-time chat app')..."
onkeypress="if(event.key === 'Enter') getRecommendation()"
/>
</div>
<div style="display: flex; gap: 10px; margin-top: 10px; align-items: center;">
<label style="font-weight: 500; min-width: 180px;">Daily Active Users (DAU):</label>
<input
type="number"
id="dauInput"
placeholder="e.g., 100"
min="1"
style="flex: 1; max-width: 150px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"
/>
<select
id="dauUnit"
style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; min-width: 120px;"
>
<option value="1">Users</option>
<option value="1000" selected>Thousands (K)</option>
<option value="1000000">Millions (M)</option>
</select>
</div>
<!-- API Key Section -->
<div class="api-key-section">
<h4 onclick="toggleApiKeySection()">
<span>๐ API Key Settings (Optional)</span>
<span id="apiKeyToggle">โผ</span>
</h4>
<div class="api-key-content" id="apiKeyContent">
<div class="radio-option selected" id="sharedBudgetOption" onclick="selectApiKeyOption('shared')">
<input type="radio" name="apiKeyChoice" id="sharedRadio" value="shared" checked>
<label for="sharedRadio" style="cursor: pointer;">Use shared demo budget</label>
<div class="budget-info" id="budgetInfo">
Budget remaining today: <span id="apiKeyBudgetLeft">$-</span><br>
Queries remaining: <span id="apiKeyQueriesLeft">-</span>
</div>
</div>
<div class="radio-option" id="ownKeyOption" onclick="selectApiKeyOption('own')">
<input type="radio" name="apiKeyChoice" id="ownRadio" value="own">
<label for="ownRadio" style="cursor: pointer;">Use my own Anthropic API key</label>
<div class="api-key-benefits">
โ Unlimited queries<br>
โ No rate limits<br>
โ Your key is never stored on my servers
</div>
<input
type="password"
id="userApiKey"
class="api-key-input"
placeholder="sk-ant-api03-..."
disabled
style="display: none;"
>
<div style="font-size: 0.85em; color: #6b7280; margin-top: 5px; padding-left: 28px; display: none;" id="apiKeyHelpText">
Get your API key at: <a href="https://console.anthropic.com/settings/keys" target="_blank" style="color: #2563eb;">console.anthropic.com</a>
</div>
</div>
</div>
</div>
<button onclick="getRecommendation()" id="submitBtn" style="margin-top: 15px; width: 100%;">
Get Recommendations
</button>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>My AI agents are analyzing your requirements...</p>
</div>
<div class="error" id="error"></div>
<div class="results" id="results">
<!-- Parsed Context Section -->
<div class="context-section" id="parsedContext" style="display: none;">
<h3>๐ Project Context</h3>
<div class="metrics-grid" id="contextMetrics"></div>
</div>
<!-- Download Section -->
<div class="input-section" id="downloadSection" style="display: none;">
<h3 style="margin-bottom: 15px;">๐ฅ Download Recommendations</h3>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button onclick="downloadMarkdown()" style="flex: 1; min-width: 150px;">
๐ Markdown (.md)
</button>
<button onclick="downloadJSON()" style="flex: 1; min-width: 150px;">
๐พ JSON (.json)
</button>
<button onclick="downloadText()" style="flex: 1; min-width: 150px;">
๐ Plain Text (.txt)
</button>
</div>
</div>
<div class="tabs">
<div class="tab active" onclick="showTab('database')">๐ Database</div>
<div class="tab" onclick="showTab('infrastructure')">๐๏ธ Infrastructure</div>
<div class="tab" onclick="showTab('cost')">๐ฐ Cost Analysis</div>
<div class="tab" onclick="showTab('security')">๐ Security</div>
</div>
<div id="database" class="tab-content active"></div>
<div id="infrastructure" class="tab-content"></div>
<div id="cost" class="tab-content"></div>
<div id="security" class="tab-content"></div>
</div>
<!-- What You'll Get Section -->
<div class="info-box">
<h3>
<span>๐ก</span>
<span>What You'll Receive</span>
</h3>
<p>
My AI-powered multi-agent system will analyze your project requirements and provide comprehensive,
production-ready recommendations across four critical domains. Each specialized agent examines
your needs from different perspectives to deliver actionable insights.
</p>
<div class="agent-grid">
<div class="agent-card">
<h4>๐ Database Agent</h4>
<p><strong>Analyzes:</strong> Data volume, query patterns, consistency requirements</p>
<p><strong>Provides:</strong> Database recommendations (PostgreSQL, MongoDB, etc.), schema design strategies, scaling approaches (sharding, replication), caching strategies, and connection pooling guidance</p>
</div>
<div class="agent-card">
<h4>๐๏ธ Infrastructure Agent</h4>
<p><strong>Analyzes:</strong> Traffic patterns, scalability needs, deployment complexity</p>
<p><strong>Provides:</strong> Cloud provider recommendations (AWS, GCP, Azure, Railway), architecture patterns (microservices, monolith, serverless), compute resources, deployment strategies, load balancing, and CDN setup</p>
</div>
<div class="agent-card">
<h4>๐ฐ Cost Agent</h4>
<p><strong>Analyzes:</strong> Infrastructure requirements, budget constraints, growth projections</p>
<p><strong>Provides:</strong> Detailed cost breakdowns across providers, monthly/annual estimates, cost optimization strategies (reserved instances, spot instances), scaling cost projections (2x, 5x, 10x), and budget management recommendations</p>
</div>
<div class="agent-card">
<h4>๐ Security Agent</h4>
<p><strong>Analyzes:</strong> Data sensitivity, compliance requirements, threat landscape</p>
<p><strong>Provides:</strong> Threat modeling (STRIDE analysis), security controls for each threat category, compliance guidance (GDPR, HIPAA, PCI-DSS, SOC2), encryption strategies, access control recommendations, and security monitoring setup</p>
</div>
</div>
</div>
<!-- Reference Guide Section -->
<div class="reference-section">
<h3 onclick="toggleReference()">
<span>๐ Scale & Cost Reference Guide</span>
<span id="referenceToggle">โผ</span>
</h3>
<div class="reference-content" id="referenceContent">
<h4>๐ฏ Infrastructure Scale Tiers</h4>
<table class="reference-table">
<thead>
<tr>
<th>Tier</th>
<th>DAU Threshold</th>
<th>RPS Threshold</th>
<th>Compute</th>
<th>Deployment</th>
<th>Architecture</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>STARTER</strong></td>
<td>< 1,000</td>
<td>< 10</td>
<td>1-2 small instances (2 vCPU, 4GB RAM)</td>
<td>Single region, simple setup</td>
<td>Monolith</td>
</tr>
<tr>
<td><strong>GROWTH</strong></td>
<td>โฅ 1,000</td>
<td>โฅ 10</td>
<td>3-5 medium instances (4 vCPU, 8GB RAM)</td>
<td>Single region with load balancing</td>
<td>Monolith or modular</td>
</tr>
<tr>
<td><strong>SCALE</strong></td>
<td>โฅ 50,000</td>
<td>โฅ 100</td>
<td>10-20 instances with auto-scaling</td>
<td>Multi-AZ, caching layer</td>
<td>Microservices or hybrid</td>
</tr>
<tr>
<td><strong>ENTERPRISE</strong></td>
<td>โฅ 500,000</td>
<td>โฅ 1,000</td>
<td>50+ instances, kubernetes cluster</td>
<td>Multi-region, global CDN</td>
<td>Microservices with mesh</td>
</tr>
</tbody>
</table>
<div class="note-box">
<strong>Note:</strong> Your application is assigned to a tier if it meets <strong>EITHER</strong> the DAU <strong>OR</strong> RPS threshold (whichever is higher).
</div>
<h4>๐พ Database Scale Tiers</h4>
<table class="reference-table">
<thead>
<tr>
<th>Tier</th>
<th>DAU Threshold</th>
<th>QPS Threshold</th>
<th>Recommendation</th>
<th>Features</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>SMALL</strong></td>
<td>< 10,000</td>
<td>< 100</td>
<td>Single instance with read replicas</td>
<td>Basic setup, good for startups</td>
</tr>
<tr>
<td><strong>MEDIUM</strong></td>
<td>โฅ 10,000</td>
<td>โฅ 100</td>
<td>Primary + read replicas, caching layer</td>
<td>Load balancing, improved performance</td>
</tr>
<tr>
<td><strong>LARGE</strong></td>
<td>โฅ 100,000</td>
<td>โฅ 1,000</td>
<td>Sharded setup, dedicated caching</td>
<td>High availability, distributed data</td>
</tr>
<tr>
<td><strong>ENTERPRISE</strong></td>
<td>โฅ 1,000,000</td>
<td>โฅ 10,000</td>
<td>Multi-region, auto-sharding, CDN</td>
<td>Global scale, maximum performance</td>
</tr>
</tbody>
</table>
<div class="note-box">
<strong>Note:</strong> Database tier is assigned based on <strong>EITHER</strong> the DAU <strong>OR</strong> QPS threshold (whichever is higher).
</div>
<h4>๐ข QPS Calculation Formula</h4>
<div class="formula-box">
<p><strong>Formula:</strong> <code>QPS = max(10, DAU ร 0.015 / 60)</code></p>
<p><strong>Assumptions:</strong></p>
<ul>
<li>1.5% of daily active users are concurrent at peak times</li>
<li>Each concurrent user makes ~10 requests per minute</li>
<li>Minimum QPS is set to 10 for small applications</li>
</ul>
<p><strong>Example:</strong> For 1,000,000 DAU โ <code>QPS = 1,000,000 ร 0.015 / 60 = 250 QPS</code></p>
</div>
<h4>๐ฐ Cost Estimation Heuristics</h4>
<p><strong>Infrastructure Requirements by DAU:</strong></p>
<table class="reference-table">
<thead>
<tr>
<th>DAU Range</th>
<th>Instances</th>
<th>Instance Type</th>
<th>Storage (GB)</th>
<th>Bandwidth (GB/month)</th>
</tr>
</thead>
<tbody>
<tr>
<td>< 10,000</td>
<td>2</td>
<td>Small</td>
<td>50</td>
<td>100</td>
</tr>
<tr>
<td>10,000 - 99,999</td>
<td>4</td>
<td>Medium</td>
<td>200</td>
<td>500</td>
</tr>
<tr>
<td>100,000 - 499,999</td>
<td>10</td>
<td>Large</td>
<td>500</td>
<td>2,000</td>
</tr>
<tr>
<td>โฅ 500,000</td>
<td>20</td>
<td>XLarge</td>
<td>1,000</td>
<td>5,000</td>
</tr>
</tbody>
</table>
<h4>๐ต Provider Pricing (Monthly USD)</h4>
<table class="reference-table">
<thead>
<tr>
<th>Component</th>
<th>AWS</th>
<th>GCP</th>
<th>Azure</th>
<th>Railway</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Small instance</strong></td>
<td>$20</td>
<td>$18</td>
<td>$21</td>
<td>$10</td>
</tr>
<tr>
<td><strong>Medium instance</strong></td>
<td>$35</td>
<td>$32</td>
<td>$38</td>
<td>$20</td>
</tr>
<tr>
<td><strong>Large instance</strong></td>
<td>$70</td>
<td>$65</td>
<td>$75</td>
<td>$40</td>
</tr>
<tr>
<td><strong>XLarge instance</strong></td>
<td>$140</td>
<td>$130</td>
<td>$145</td>
<td>$80</td>
</tr>
<tr>
<td><strong>Storage (per GB)</strong></td>
<td>$0.10</td>
<td>$0.09</td>
<td>$0.11</td>
<td>$0.05</td>
</tr>
<tr>
<td><strong>Bandwidth (per GB)</strong></td>
<td>$0.09</td>
<td>$0.08</td>
<td>$0.10</td>
<td>$0.05</td>
</tr>
<tr>
<td><strong>Managed Database</strong></td>
<td>$50</td>
<td>$45</td>
<td>$55</td>
<td>$10</td>
</tr>
<tr>
<td><strong>Load Balancer</strong></td>
<td>$20</td>
<td>$18</td>
<td>$22</td>
<td>Free</td>
</tr>
<tr>
<td><strong>NAT Gateway</strong></td>
<td>$35</td>
<td>$30</td>
<td>$35</td>
<td>Free</td>
</tr>
</tbody>
</table>
<h4>๐ ๏ธ Additional Service Costs (Monthly USD)</h4>
<table class="reference-table">
<thead>
<tr>
<th>Service</th>
<th>Cost</th>
<th>Enabled When</th>
</tr>
</thead>
<tbody>
<tr>
<td>CDN (CloudFlare/CloudFront)</td>
<td>$20</td>
<td>DAU > 10,000</td>
</tr>
<tr>
<td>Cache (Redis/Memcached)</td>
<td>$15</td>
<td>DAU > 10,000</td>
</tr>
<tr>
<td>Monitoring (Basic)</td>
<td>Free</td>
<td>Always</td>
</tr>
<tr>
<td>Monitoring (Standard)</td>
<td>$50</td>
<td>DAU > 50,000</td>
</tr>
<tr>
<td>Monitoring (Premium)</td>
<td>$150</td>
<td>Optional</td>
</tr>
<tr>
<td>Backups</td>
<td>$10</td>
<td>Always</td>
</tr>
<tr>
<td>Logging</td>
<td>$10</td>
<td>Always</td>
</tr>
<tr>
<td>Secrets Management</td>
<td>$5</td>
<td>Always</td>
</tr>
<tr>
<td>DNS</td>
<td>$1</td>
<td>Always</td>
</tr>
</tbody>
</table>
<div class="formula-box">
<p><strong>Total Monthly Cost Formula:</strong></p>
<code>
Total = (Instances ร Instance Price) + (Storage ร $per_GB) + (Bandwidth ร $per_GB) +
Database + Load Balancer + NAT Gateway + Service Costs
</code>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<p><strong>Need Help or Have Feedback?</strong></p>
<p>If something is not working as expected or you'd like to share your feedback, please email me at</p>
<p><a href="mailto:ranjana.rajendran@gmail.com">ranjana.rajendran@gmail.com</a></p>
<div class="copyright">
<p>© 2025 Ranjana Rajendran. All rights reserved.</p>
</div>
</div>
</div>
<script>
let currentData = null;
// Fetch and update system status
async function updateSystemStatus() {
try {
// Fetch health
const healthResponse = await fetch('/health');
const health = await healthResponse.json();
const healthStatusEl = document.getElementById('healthStatus');
if (health.status === 'healthy') {
healthStatusEl.textContent = 'โ
Healthy';
healthStatusEl.className = 'status-value status-healthy';
} else {
healthStatusEl.textContent = 'โ Offline';
healthStatusEl.className = 'status-value status-offline';
}
document.getElementById('agentsLoaded').textContent = health.agents_loaded || '-';
const uptimeSeconds = health.uptime_seconds || 0;
const uptimeMinutes = Math.floor(uptimeSeconds / 60);
const uptimeHours = Math.floor(uptimeMinutes / 60);
const uptimeDays = Math.floor(uptimeHours / 24);
let uptimeStr;
if (uptimeDays > 0) {
uptimeStr = `${uptimeDays}d ${uptimeHours % 24}h`;
} else if (uptimeHours > 0) {
uptimeStr = `${uptimeHours}h ${uptimeMinutes % 60}m`;
} else if (uptimeMinutes > 0) {
uptimeStr = `${uptimeMinutes}m`;
} else {
uptimeStr = `${Math.floor(uptimeSeconds)}s`;
}
document.getElementById('uptime').textContent = uptimeStr;
// Fetch metrics
const metricsResponse = await fetch('/metrics');
const metrics = await metricsResponse.json();
document.getElementById('totalRequests').textContent = metrics.total_requests?.toLocaleString() || '-';
document.getElementById('dailyQueries').textContent = metrics.daily_queries?.toLocaleString() || '-';
document.getElementById('dailyCost').textContent = metrics.daily_cost_usd ? `$${metrics.daily_cost_usd.toFixed(4)}` : '-';
document.getElementById('budgetLeft').textContent = metrics.budget_remaining_usd ? `$${metrics.budget_remaining_usd.toFixed(2)}` : '-';
// Update API key section budget info
document.getElementById('apiKeyBudgetLeft').textContent = metrics.budget_remaining_usd ? `$${metrics.budget_remaining_usd.toFixed(2)}` : '$-';
const queriesRemaining = metrics.daily_queries !== undefined ? `${metrics.daily_queries}/50` : '-';
document.getElementById('apiKeyQueriesLeft').textContent = queriesRemaining;
} catch (error) {
console.error('Error fetching system status:', error);
document.getElementById('healthStatus').textContent = 'โ Error';
document.getElementById('healthStatus').className = 'status-value status-offline';
}
}
// Update status on page load
updateSystemStatus();
// Auto-refresh status every 30 seconds
setInterval(updateSystemStatus, 30000);
// Toggle reference guide
function toggleReference() {
const content = document.getElementById('referenceContent');
const toggle = document.getElementById('referenceToggle');
if (content.classList.contains('show')) {
content.classList.remove('show');
toggle.textContent = 'โผ';
} else {
content.classList.add('show');
toggle.textContent = 'โฒ';
}
}
// Toggle API key section
function toggleApiKeySection() {
const content = document.getElementById('apiKeyContent');
const toggle = document.getElementById('apiKeyToggle');
if (content.classList.contains('show')) {
content.classList.remove('show');
toggle.textContent = 'โผ';
} else {
content.classList.add('show');
toggle.textContent = 'โฒ';
}
}
// Select API key option (shared budget or own key)
function selectApiKeyOption(option) {
const sharedRadio = document.getElementById('sharedRadio');
const ownRadio = document.getElementById('ownRadio');
const sharedOption = document.getElementById('sharedBudgetOption');
const ownOption = document.getElementById('ownKeyOption');
const apiKeyInput = document.getElementById('userApiKey');
const apiKeyHelpText = document.getElementById('apiKeyHelpText');
if (option === 'shared') {
sharedRadio.checked = true;
ownRadio.checked = false;
sharedOption.classList.add('selected');
ownOption.classList.remove('selected');
apiKeyInput.style.display = 'none';
apiKeyInput.disabled = true;
apiKeyHelpText.style.display = 'none';
} else {
sharedRadio.checked = false;
ownRadio.checked = true;
sharedOption.classList.remove('selected');
ownOption.classList.add('selected');
apiKeyInput.style.display = 'block';
apiKeyInput.disabled = false;
apiKeyHelpText.style.display = 'block';
}
}
async function getRecommendation() {
console.log('getRecommendation() called - v3.0 with conversation');
const query = document.getElementById('queryInput').value.trim();
if (!query) {
alert('Please enter a project description');
return;
}
// Start conversation flow
await startConversation();
}
async function getRecommendationOriginal() {
console.log('getRecommendation() called - v2.0');
const query = document.getElementById('queryInput').value.trim();
console.log('Query:', query);
if (!query) {
alert('Please enter a project description');
return;
}
// Get DAU value
const dauInput = document.getElementById('dauInput').value;
const dauUnit = parseInt(document.getElementById('dauUnit').value);
const dau = dauInput ? parseInt(dauInput) * dauUnit : null;
// Get API key if user chose to use their own
const ownRadio = document.getElementById('ownRadio');
const userApiKey = document.getElementById('userApiKey').value.trim();
// Show loading
console.log('Setting loading display to block');
document.getElementById('loading').style.display = 'block';
document.getElementById('results').style.display = 'none';
document.getElementById('error').style.display = 'none';
document.getElementById('submitBtn').disabled = true;
console.log('Loading spinner should be visible now');
try {
// Build request body
const requestBody = { query };
if (dau) {
requestBody.dau = dau;
}
// Add API key if user chose to use their own key
if (ownRadio.checked && userApiKey) {
requestBody.api_key = userApiKey;
}
console.log('Sending request to /recommend with body:', requestBody);
const response = await fetch('/recommend', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
console.log('Received response, status:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Response data received, status:', data.status);
currentData = data;
if (data.status === 'error') {
throw new Error(data.error || 'Unknown error occurred');
}
console.log('Calling displayResults()');
displayResults(data);
} catch (error) {
console.error('Error:', error);
const errorEl = document.getElementById('error');
errorEl.textContent = `Error: ${error.message}`;
errorEl.style.display = 'block';
} finally {
document.getElementById('loading').style.display = 'none';
document.getElementById('submitBtn').disabled = false;
}
}
function showTab(tabName) {
// Update tab buttons
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(tabName).classList.add('active');
}
function displayResults(data) {
document.getElementById('results').style.display = 'block';
document.getElementById('downloadSection').style.display = 'block';
// Display parsed context if available
if (data.parsed_context) {
displayParsedContext(data.parsed_context);
}
const recs = data.recommendations;
// Display Database recommendations
if (recs.database) {
displayDatabase(recs.database);
}
// Display Infrastructure recommendations
if (recs.infrastructure) {
displayInfrastructure(recs.infrastructure);
}
// Display Cost analysis
if (recs.cost) {
displayCost(recs.cost);
}
// Display Security recommendations
if (recs.security) {
displaySecurity(recs.security);
}
}
// Download functions
function generateFilename(query) {
// Take first 50 chars of query and make it filesystem-safe
let filename = query
.substring(0, 50)
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
.replace(/\s+/g, '_') // Replace spaces with underscores
.replace(/_+/g, '_') // Replace multiple underscores with single
.replace(/^_|_$/g, ''); // Remove leading/trailing underscores
// If filename is empty or too short, use a default
if (filename.length < 3) {
filename = 'tech_stack_recommendations';
}
return filename;
}
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function downloadJSON() {
if (!currentData) {
alert('No recommendations to download');
return;
}
const json = JSON.stringify(currentData, null, 2);
const filename = generateFilename(currentData.query) + '.json';
downloadFile(json, filename, 'application/json');
}
// Store current diagram code for downloads
let currentDiagramCode = '';
async function generateArchitectureDiagram() {
if (!currentData || !currentData.recommendations || !currentData.recommendations.infrastructure) {
alert('No infrastructure recommendations available');
return;
}
const btn = document.getElementById('generateDiagramBtn');
const diagramSection = document.getElementById('diagramSection');
const diagramContent = document.getElementById('diagramContent');
try {
// Show loading state
btn.disabled = true;
btn.textContent = 'โณ Generating Diagram...';
diagramContent.innerHTML = '<p style="text-align: center; color: #667eea;">Generating architecture diagram...</p>';
diagramSection.style.display = 'block';
// Get infrastructure data
const infra = currentData.recommendations.infrastructure;
const scaleTier = infra.scale_info?.tier || 'STARTER';
// Get API key if user provided one
const ownRadio = document.getElementById('ownRadio');
const userApiKey = document.getElementById('userApiKey').value.trim();
// Build request body
const requestBody = {
user_query: currentData.query,
recommendations: infra.recommendations || '',
scale_tier: scaleTier
};
// Add API key if user chose to use their own key
if (ownRadio.checked && userApiKey) {
requestBody.api_key = userApiKey;
}
console.log('Requesting diagram generation...');
const response = await fetch('/generate-diagram', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Diagram response:', data);
if (data.status === 'success' && data.diagram) {
// Extract Mermaid code from response
const diagramMatch = data.diagram.match(/```mermaid\n([\s\S]*?)```/);
if (diagramMatch) {
currentDiagramCode = diagramMatch[1].trim();
} else {
// Try without markdown code block
currentDiagramCode = data.diagram.trim();
}
console.log('Diagram code:', currentDiagramCode.substring(0, 100) + '...');
// Create unique ID for this diagram
const diagramId = `diagram-${Date.now()}`;
diagramContent.innerHTML = `<div class="mermaid" id="${diagramId}">${currentDiagramCode}</div>`;
// Render the diagram
if (typeof mermaid !== 'undefined') {
mermaid.run({ nodes: [document.getElementById(diagramId)] });
console.log('Diagram rendered successfully');
} else {
console.error('Mermaid library not available');
diagramContent.innerHTML = '<p style="color: red;">Error: Mermaid library not loaded</p>';
}
btn.textContent = 'โ
Diagram Generated';
setTimeout(() => {
btn.textContent = '๐๏ธ Generate Architecture Diagram';
btn.disabled = false;
}, 2000);
} else {
throw new Error('No diagram returned from API');
}
} catch (error) {
console.error('Diagram generation error:', error);
diagramContent.innerHTML = `<p style="color: red; text-align: center;">Error generating diagram: ${error.message}</p>`;
btn.textContent = 'โ Generation Failed';
setTimeout(() => {
btn.textContent = '๐๏ธ Generate Architecture Diagram';
btn.disabled = false;
}, 2000);
}
}
async function downloadDiagramAsSVG() {
if (!currentDiagramCode) {
alert('Please generate a diagram first');
return;
}
try {
const diagramElement = document.querySelector('#diagramContent .mermaid');
if (!diagramElement) {
throw new Error('Diagram not found');
}
// Get the SVG element created by Mermaid
const svgElement = diagramElement.querySelector('svg');
if (!svgElement) {
throw new Error('SVG not found in diagram');
}
// Clone and get SVG source
const svgClone = svgElement.cloneNode(true);
const svgSource = new XMLSerializer().serializeToString(svgClone);
// Create blob and download
const blob = new Blob([svgSource], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = generateFilename(currentData.query) + '_architecture.svg';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('SVG downloaded successfully');
} catch (error) {
console.error('SVG download error:', error);
alert('Error downloading SVG: ' + error.message);
}
}
function downloadDiagramAsMermaid() {
if (!currentDiagramCode) {
alert('Please generate a diagram first');
return;
}
const filename = generateFilename(currentData.query) + '_architecture.mmd';
downloadFile(currentDiagramCode, filename, 'text/plain');
console.log('Mermaid code downloaded successfully');
}
function downloadMarkdown() {
if (!currentData) {
alert('No recommendations to download');
return;
}
let markdown = `# Tech Stack Recommendations\n\n`;
markdown += `**Query:** ${currentData.query}\n\n`;
markdown += `**Generated:** ${new Date().toLocaleString()}\n\n`;
markdown += `---\n\n`;
// Parsed Context
if (currentData.parsed_context) {
const ctx = currentData.parsed_context;
markdown += `## ๐ Project Context\n\n`;
if (ctx.dau) markdown += `- **Daily Active Users:** ${ctx.dau.toLocaleString()}\n`;
if (ctx.qps) markdown += `- **Queries per Second:** ${ctx.qps}\n`;
if (ctx.data_type) markdown += `- **Data Type:** ${ctx.data_type}\n`;
if (ctx.workload_type) markdown += `- **Workload Type:** ${ctx.workload_type}\n`;
if (ctx.data_sensitivity) markdown += `- **Data Sensitivity:** ${ctx.data_sensitivity}\n`;
if (ctx.compliance_requirements && ctx.compliance_requirements.length > 0) {
markdown += `- **Compliance:** ${ctx.compliance_requirements.join(', ')}\n`;
}
markdown += `\n---\n\n`;
}
const recs = currentData.recommendations;
// Database - Recommendations text first
if (recs.database) {
markdown += `## ๐ Database Recommendations\n\n`;
if (recs.database.recommendations) {
markdown += `${recs.database.recommendations}\n\n`;
}
if (recs.database.scale_info) {
markdown += `### Scale Information\n\n`;
markdown += `\`\`\`json\n${JSON.stringify(recs.database.scale_info, null, 2)}\n\`\`\`\n\n`;
}
}
// Infrastructure - Recommendations text first
if (recs.infrastructure) {
markdown += `## ๐๏ธ Infrastructure Recommendations\n\n`;
if (recs.infrastructure.recommendations) {
markdown += `${recs.infrastructure.recommendations}\n\n`;
}
if (recs.infrastructure.scale_info) {
markdown += `### Scale Information\n\n`;
markdown += `\`\`\`json\n${JSON.stringify(recs.infrastructure.scale_info, null, 2)}\n\`\`\`\n\n`;
}
}
// Cost - Recommendations text first
if (recs.cost) {
markdown += `## ๐ฐ Cost Analysis\n\n`;
if (recs.cost.recommendations) {
markdown += `${recs.cost.recommendations}\n\n`;
}
if (recs.cost.cost_comparisons && recs.cost.cost_comparisons.length > 0) {
markdown += `### Provider Comparison\n\n`;
markdown += `| Provider | Monthly | Annual |\n`;
markdown += `|----------|---------|--------|\n`;
recs.cost.cost_comparisons.forEach(comp => {
markdown += `| ${comp.provider.toUpperCase()} | $${comp.cloud_costs.monthly_total} | $${comp.cloud_costs.annual_total} |\n`;
});
markdown += `\n`;
}
}
// Security - Recommendations text first
if (recs.security) {
markdown += `## ๐ Security Assessment\n\n`;
if (recs.security.recommendations) {
markdown += `${recs.security.recommendations}\n\n`;
}
if (recs.security.threat_assessment && recs.security.threat_assessment.threats) {
const threats = recs.security.threat_assessment.threats;
if (threats.high_risk && threats.high_risk.length > 0) {
markdown += `### ๐ด High Risk Threats\n\n`;
threats.high_risk.forEach(t => markdown += `- ${t}\n`);
markdown += `\n`;
}
if (threats.medium_risk && threats.medium_risk.length > 0) {
markdown += `### ๐ก Medium Risk Threats\n\n`;
threats.medium_risk.forEach(t => markdown += `- ${t}\n`);
markdown += `\n`;
}
if (threats.low_risk && threats.low_risk.length > 0) {
markdown += `### ๐ข Low Risk Threats\n\n`;
threats.low_risk.forEach(t => markdown += `- ${t}\n`);
markdown += `\n`;
}
}
if (recs.security.compliance_requirements) {
markdown += `### Compliance Requirements\n\n`;
markdown += `\`\`\`json\n${JSON.stringify(recs.security.compliance_requirements, null, 2)}\n\`\`\`\n\n`;
}
}
markdown += `---\n\n`;
markdown += `*Generated by Tech Stack Advisor*\n`;
const filename = generateFilename(currentData.query) + '.md';
downloadFile(markdown, filename, 'text/markdown');
}
function downloadText() {
if (!currentData) {
alert('No recommendations to download');
return;
}
// Convert to plain text (similar to markdown but without formatting)
let text = `TECH STACK RECOMMENDATIONS\n`;
text += `${'='.repeat(50)}\n\n`;
text += `Query: ${currentData.query}\n`;
text += `Generated: ${new Date().toLocaleString()}\n\n`;
text += `${'='.repeat(50)}\n\n`;
// Parsed Context
if (currentData.parsed_context) {
const ctx = currentData.parsed_context;
text += `PROJECT CONTEXT\n`;
text += `${'-'.repeat(50)}\n`;
if (ctx.dau) text += `Daily Active Users: ${ctx.dau.toLocaleString()}\n`;
if (ctx.qps) text += `Queries per Second: ${ctx.qps}\n`;
if (ctx.data_type) text += `Data Type: ${ctx.data_type}\n`;
if (ctx.workload_type) text += `Workload Type: ${ctx.workload_type}\n`;
if (ctx.data_sensitivity) text += `Data Sensitivity: ${ctx.data_sensitivity}\n`;
if (ctx.compliance_requirements && ctx.compliance_requirements.length > 0) {
text += `Compliance: ${ctx.compliance_requirements.join(', ')}\n`;
}
text += `\n`;
}
const recs = currentData.recommendations;
// Add each section - Human-readable recommendations first
if (recs.database) {
text += `DATABASE RECOMMENDATIONS\n`;
text += `${'-'.repeat(50)}\n`;
if (recs.database.recommendations) {
text += `${recs.database.recommendations}\n\n`;
}
if (recs.database.scale_info) {
text += `Scale Information:\n${JSON.stringify(recs.database.scale_info, null, 2)}\n\n`;
}
}
if (recs.infrastructure) {
text += `INFRASTRUCTURE RECOMMENDATIONS\n`;
text += `${'-'.repeat(50)}\n`;
if (recs.infrastructure.recommendations) {
text += `${recs.infrastructure.recommendations}\n\n`;
}
if (recs.infrastructure.scale_info) {
text += `Scale Information:\n${JSON.stringify(recs.infrastructure.scale_info, null, 2)}\n\n`;
}
}
if (recs.cost) {
text += `COST ANALYSIS\n`;
text += `${'-'.repeat(50)}\n`;
if (recs.cost.recommendations) {
text += `${recs.cost.recommendations}\n\n`;
}
if (recs.cost.cost_comparisons && recs.cost.cost_comparisons.length > 0) {
text += `Provider Comparison:\n`;
recs.cost.cost_comparisons.forEach(comp => {
text += ` ${comp.provider.toUpperCase()}: $${comp.cloud_costs.monthly_total}/month, $${comp.cloud_costs.annual_total}/year\n`;
});
text += `\n`;
}
}
if (recs.security) {
text += `SECURITY ASSESSMENT\n`;
text += `${'-'.repeat(50)}\n`;
if (recs.security.recommendations) {
text += `${recs.security.recommendations}\n\n`;
}
if (recs.security.threat_assessment && recs.security.threat_assessment.threats) {
const threats = recs.security.threat_assessment.threats;
if (threats.high_risk && threats.high_risk.length > 0) {
text += `High Risk Threats:\n`;
threats.high_risk.forEach(t => text += ` - ${t}\n`);
text += `\n`;
}
if (threats.medium_risk && threats.medium_risk.length > 0) {
text += `Medium Risk Threats:\n`;
threats.medium_risk.forEach(t => text += ` - ${t}\n`);
text += `\n`;
}
if (threats.low_risk && threats.low_risk.length > 0) {
text += `Low Risk Threats:\n`;
threats.low_risk.forEach(t => text += ` - ${t}\n`);
text += `\n`;
}
}
}
text += `${'='.repeat(50)}\n`;
text += `Generated by Tech Stack Advisor\n`;
const filename = generateFilename(currentData.query) + '.txt';
downloadFile(text, filename, 'text/plain');
}
function displayParsedContext(context) {
const contextSection = document.getElementById('parsedContext');
const contextMetrics = document.getElementById('contextMetrics');
let html = '';
// Display metrics
if (context.dau !== undefined) {
html += `
<div class="metric-card">
<div class="metric-label">Daily Active Users</div>
<div class="metric-value">${context.dau.toLocaleString()}</div>
</div>
`;
}
if (context.qps !== undefined) {
html += `
<div class="metric-card">
<div class="metric-label">Queries per Second</div>
<div class="metric-value">${context.qps}</div>
<div style="font-size: 0.75em; color: #666; margin-top: 5px; font-style: italic;">
Calculated: ${context.dau ? context.dau.toLocaleString() : 'N/A'} DAU ร 1.5% concurrent ร 10 req/min รท 60 sec
</div>
</div>
`;
}
if (context.data_type) {
html += `
<div class="metric-card">
<div class="metric-label">Data Type</div>
<div class="metric-value" style="font-size: 1.2em;">${context.data_type}</div>
</div>
`;
}
if (context.workload_type) {
html += `
<div class="metric-card">
<div class="metric-label">Workload Type</div>
<div class="metric-value" style="font-size: 1.2em;">${context.workload_type}</div>
</div>
`;
}
if (context.data_sensitivity) {
html += `
<div class="metric-card">
<div class="metric-label">Data Sensitivity</div>
<div class="metric-value" style="font-size: 1.2em;">${context.data_sensitivity}</div>
</div>
`;
}
if (context.compliance_requirements && context.compliance_requirements.length > 0) {
html += `
<div class="metric-card">
<div class="metric-label">Compliance Requirements</div>
<div class="metric-value" style="font-size: 1em;">${context.compliance_requirements.join(', ')}</div>
</div>
`;
}
contextMetrics.innerHTML = html;
contextSection.style.display = html ? 'block' : 'none';
}
function displayDatabase(db) {
let html = '<div class="card"><h3>Database Recommendations</h3>';
// Scale info metrics
if (db.scale_info) {
html += '<div class="scale-metrics">';
if (db.scale_info.tier) {
html += `
<div class="scale-metric">
<div class="scale-metric-label">Scale Tier</div>
<div class="scale-metric-value">${db.scale_info.tier.toUpperCase()}</div>
</div>
`;
}
if (db.scale_info.cache_recommended !== undefined) {
html += `
<div class="scale-metric">
<div class="scale-metric-label">Cache Recommended</div>
<div class="scale-metric-value">${db.scale_info.cache_recommended ? 'โ
Yes' : 'โ No'}</div>
</div>
`;
}
if (db.scale_info.estimated_connections) {
html += `
<div class="scale-metric">
<div class="scale-metric-label">Estimated Connections</div>
<div class="scale-metric-value">${db.scale_info.estimated_connections}</div>
</div>
`;
}
html += '</div>';
}
// Database options
if (db.raw_knowledge && db.raw_knowledge.results) {
html += '<h4>Database Options Analysis:</h4>';
const databases = Object.entries(db.raw_knowledge.results).slice(0, 4);
databases.forEach(([name, info]) => {
html += `
<div class="expandable">
<div class="expandable-header" onclick="toggleExpandable(this)">
<span><strong>${name.toUpperCase()}</strong> - ${info.description || ''}</span>
<span class="arrow">โถ</span>
</div>
<div class="expandable-content">
<p><strong>Best For:</strong> ${info.best_for || 'N/A'}</p>
<div class="pros-cons">
<div class="pros">
<h4>Pros</h4>
<ul>
${(info.pros || []).map(pro => `<li>${pro}</li>`).join('')}
</ul>
</div>
<div class="cons">
<h4>Cons</h4>
<ul>
${(info.cons || []).map(con => `<li>${con}</li>`).join('')}
</ul>
</div>
</div>
</div>
</div>
`;
});
}
// Comprehensive detailed analysis
if (db.recommendations) {
html += `
<div class="expandable open">
<div class="expandable-header" onclick="toggleExpandable(this)">
<span><strong>Comprehensive Detailed Analysis</strong></span>
<span class="arrow">โถ</span>
</div>
<div class="expandable-content">
<div style="white-space: pre-wrap; line-height: 1.6; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">${typeof db.recommendations === 'string' ? db.recommendations : JSON.stringify(db.recommendations, null, 2)}</div>
</div>
</div>
`;
}
html += '</div>';
document.getElementById('database').innerHTML = html;
}
function displayInfrastructure(infra) {
let html = '<div class="card"><h3>Infrastructure Recommendations</h3>';
// Scale info metrics
if (infra.scale_info) {
html += '<div class="scale-metrics">';
if (infra.scale_info.tier) {
html += `
<div class="scale-metric">
<div class="scale-metric-label">Scale Tier</div>
<div class="scale-metric-value">${infra.scale_info.tier.toUpperCase()}</div>
</div>
`;
}
if (infra.scale_info.load_balancer_needed !== undefined) {
html += `
<div class="scale-metric">
<div class="scale-metric-label">Load Balancer</div>
<div class="scale-metric-value">${infra.scale_info.load_balancer_needed ? 'โ
Needed' : 'โ Not Needed'}</div>
</div>
`;
}
if (infra.scale_info.cdn_recommended !== undefined) {
html += `
<div class="scale-metric">
<div class="scale-metric-label">CDN Recommended</div>
<div class="scale-metric-value">${infra.scale_info.cdn_recommended ? 'โ
Yes' : 'โ No'}</div>
</div>
`;
}
html += '</div>';
}
// Add button to generate architecture diagram
html += `
<div style="margin: 20px 0; text-align: center;">
<button onclick="generateArchitectureDiagram()"
id="generateDiagramBtn"
class="button"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
font-size: 16px;
border-radius: 8px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);">
๐๏ธ Generate Architecture Diagram
</button>
</div>
<div id="diagramSection" style="display: none;">
<div class="diagram-container">
<h4>๐๏ธ Architecture Diagram</h4>
<div id="diagramContent"></div>
<div style="margin-top: 15px; text-align: center;">
<button onclick="downloadDiagramAsSVG()"
class="button"
style="background: #48bb78; color: white; border: none; padding: 8px 16px; margin: 5px; border-radius: 6px; cursor: pointer;">
๐ฅ Download as SVG
</button>
<button onclick="downloadDiagramAsMermaid()"
class="button"
style="background: #4299e1; color: white; border: none; padding: 8px 16px; margin: 5px; border-radius: 6px; cursor: pointer;">
๐ฅ Download Mermaid Code
</button>
</div>
</div>
</div>
`;
// Architecture patterns
if (infra.raw_knowledge && infra.raw_knowledge.patterns) {
html += '<h4>Architecture Patterns:</h4>';
Object.entries(infra.raw_knowledge.patterns).forEach(([name, pattern]) => {
html += `
<div class="expandable">
<div class="expandable-header" onclick="toggleExpandable(this)">
<span><strong>${name.toUpperCase()}</strong> - ${pattern.description || ''}</span>
<span class="arrow">โถ</span>
</div>
<div class="expandable-content">
<p><strong>Best For:</strong> ${pattern.best_for || 'N/A'}</p>
<p><strong>Complexity:</strong> ${pattern.complexity || 'N/A'}</p>
<div class="pros-cons">
<div class="pros">
<h4>Pros</h4>
<ul>
${(pattern.pros || []).map(pro => `<li>${pro}</li>`).join('')}
</ul>
</div>
<div class="cons">
<h4>Cons</h4>
<ul>
${(pattern.cons || []).map(con => `<li>${con}</li>`).join('')}
</ul>
</div>
</div>
</div>
</div>
`;
});
}
// Comprehensive infrastructure analysis
if (infra.recommendations) {
html += `
<div class="expandable open">
<div class="expandable-header" onclick="toggleExpandable(this)">
<span><strong>Comprehensive Infrastructure Analysis</strong></span>
<span class="arrow">โถ</span>
</div>
<div class="expandable-content">
<div style="white-space: pre-wrap; line-height: 1.6; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">${typeof infra.recommendations === 'string' ? infra.recommendations : JSON.stringify(infra.recommendations, null, 2)}</div>
</div>
</div>
`;
}
html += '</div>';
document.getElementById('infrastructure').innerHTML = html;
// Render Mermaid diagrams
if (typeof mermaid !== 'undefined') {
console.log('Rendering Mermaid diagrams...');
setTimeout(() => {
mermaid.run({
querySelector: '.mermaid'
}).then(() => {
console.log('Mermaid diagrams rendered successfully');
}).catch((err) => {
console.error('Mermaid render error:', err);
});
}, 100);
} else {
console.error('Mermaid library not loaded');
}
}
function displayCost(cost) {
let html = '<div class="card"><h3>Cost Analysis</h3>';
// Configuration metrics
if (cost.configuration) {
html += '<div class="scale-metrics">';
Object.entries(cost.configuration).forEach(([key, value]) => {
const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
html += `
<div class="scale-metric">
<div class="scale-metric-label">${label}</div>
<div class="scale-metric-value">${value}</div>
</div>
`;
});
html += '</div>';
}
// Cost comparison table
if (cost.cost_comparisons && cost.cost_comparisons.length > 0) {
html += '<h4>Provider Cost Comparison:</h4>';
html += '<table><thead><tr>';
html += '<th>Provider</th><th>Monthly</th><th>Annual</th><th>Compute</th><th>Storage</th><th>Database</th><th>Bandwidth</th>';
html += '</tr></thead><tbody>';
cost.cost_comparisons.forEach(comp => {
const breakdown = comp.cloud_costs.breakdown || {};
html += '<tr>';
html += `<td><strong>${comp.provider.toUpperCase()}</strong></td>`;
html += `<td>$${comp.cloud_costs.monthly_total || comp.monthly_cost}</td>`;
html += `<td>$${comp.cloud_costs.annual_total || (comp.monthly_cost * 12)}</td>`;
html += `<td>$${breakdown.compute || 'N/A'}</td>`;
html += `<td>$${breakdown.storage || 'N/A'}</td>`;
html += `<td>$${breakdown.database || 'N/A'}</td>`;
html += `<td>$${breakdown.bandwidth || 'N/A'}</td>`;
html += '</tr>';
});
html += '</tbody></table>';
// Chart
html += '<div class="chart-container"><canvas id="costChart"></canvas></div>';
}
// Detailed provider comparison
if (cost.cost_comparisons && cost.cost_comparisons.length > 0) {
html += '<h4>Detailed Provider Analysis:</h4>';
cost.cost_comparisons.forEach(comp => {
html += `
<div class="expandable">
<div class="expandable-header" onclick="toggleExpandable(this)">
<span><strong>${comp.provider.toUpperCase()}</strong> - $${comp.monthly_cost}/month</span>
<span class="arrow">โถ</span>
</div>
<div class="expandable-content">
<p><strong>Monthly Total:</strong> $${comp.cloud_costs.monthly_total}</p>
<p><strong>Annual Total:</strong> $${comp.cloud_costs.annual_total}</p>
<h5>Cost Breakdown:</h5>
<ul>
${Object.entries(comp.cloud_costs.breakdown).map(([key, value]) =>
`<li><strong>${key}:</strong> $${value}</li>`
).join('')}
</ul>
</div>
</div>
`;
});
}
// Comprehensive cost analysis
if (cost.recommendations) {
html += `
<div class="expandable open">
<div class="expandable-header" onclick="toggleExpandable(this)">
<span><strong>Detailed Cost Analysis & Optimization</strong></span>
<span class="arrow">โถ</span>
</div>
<div class="expandable-content">
<div style="white-space: pre-wrap; line-height: 1.6; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">${typeof cost.recommendations === 'string' ? cost.recommendations : JSON.stringify(cost.recommendations, null, 2)}</div>
</div>
</div>
`;
}
html += '</div>';
document.getElementById('cost').innerHTML = html;
// Render chart if cost comparisons exist
if (cost.cost_comparisons && cost.cost_comparisons.length > 0) {
setTimeout(() => renderCostChart(cost.cost_comparisons), 100);
}
}
function renderCostChart(costComparisons) {
const ctx = document.getElementById('costChart');
if (!ctx) return;
const providers = costComparisons.map(c => c.provider.toUpperCase());
const monthlyCosts = costComparisons.map(c => c.cloud_costs.monthly_total || c.monthly_cost);
new Chart(ctx, {
type: 'bar',
data: {
labels: providers,
datasets: [{
label: 'Monthly Cost ($)',
data: monthlyCosts,
backgroundColor: [
'rgba(102, 126, 234, 0.8)',
'rgba(118, 75, 162, 0.8)',
'rgba(237, 100, 166, 0.8)',
],
borderColor: [
'rgba(102, 126, 234, 1)',
'rgba(118, 75, 162, 1)',
'rgba(237, 100, 166, 1)',
],
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top',
},
title: {
display: true,
text: 'Monthly Cost Comparison by Provider'
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Cost (USD)'
}
}
}
}
});
}
function displaySecurity(security) {
let html = '<div class="card"><h3>Security Assessment</h3>';
// Threat assessment overview
if (security.threat_assessment) {
const threat = security.threat_assessment;
html += `
<div class="scale-metrics">
<div class="scale-metric">
<div class="scale-metric-label">Architecture</div>
<div class="scale-metric-value">${threat.architecture || 'N/A'}</div>
</div>
<div class="scale-metric">
<div class="scale-metric-label">Data Sensitivity</div>
<div class="scale-metric-value">${threat.data_sensitivity || 'N/A'}</div>
</div>
<div class="scale-metric">
<div class="scale-metric-label">Priority</div>
<div class="scale-metric-value">${(threat.priority || 'N/A').toUpperCase()}</div>
</div>
</div>
`;
// Threat severity breakdown
if (threat.threats) {
html += '<h4>Threat Assessment by Severity:</h4>';
// High risk threats
if (threat.threats.high_risk && threat.threats.high_risk.length > 0) {
html += `
<div class="threat-list threat-high">
<h5 style="color: #dc3545;">๐ด High Risk Threats</h5>
<ul>
${threat.threats.high_risk.map(t => `<li><span class="badge badge-danger">${t}</span></li>`).join('')}
</ul>
</div>
`;
}
// Medium risk threats
if (threat.threats.medium_risk && threat.threats.medium_risk.length > 0) {
html += `
<div class="threat-list threat-medium">
<h5 style="color: #ffc107;">๐ก Medium Risk Threats</h5>
<ul>
${threat.threats.medium_risk.map(t => `<li><span class="badge badge-warning">${t}</span></li>`).join('')}
</ul>
</div>
`;
}
// Low risk threats
if (threat.threats.low_risk && threat.threats.low_risk.length > 0) {
html += `
<div class="threat-list threat-low">
<h5 style="color: #28a745;">๐ข Low Risk Threats</h5>
<ul>
${threat.threats.low_risk.map(t => `<li><span class="badge badge-success">${t}</span></li>`).join('')}
</ul>
</div>
`;
}
}
}
// Security checklist
if (security.security_checklist && security.security_checklist.checklist) {
html += '<h4>Security Checklist:</h4>';
Object.entries(security.security_checklist.checklist).forEach(([category, items]) => {
html += `
<div class="expandable">
<div class="expandable-header" onclick="toggleExpandable(this)">
<span><strong>${category.toUpperCase().replace('_', ' ')}</strong></span>
<span class="arrow">โถ</span>
</div>
<div class="expandable-content">
${items.critical ? `
<h5>Critical:</h5>
<ul>
${items.critical.map(item => `<li>${item}</li>`).join('')}
</ul>
` : ''}
${items.recommended ? `
<h5>Recommended:</h5>
<ul>
${items.recommended.map(item => `<li>${item}</li>`).join('')}
</ul>
` : ''}
</div>
</div>
`;
});
}
// Compliance frameworks
if (security.security_checklist && security.security_checklist.compliance_frameworks) {
html += '<h4>Compliance Frameworks:</h4>';
Object.entries(security.security_checklist.compliance_frameworks).forEach(([framework, requirements]) => {
html += `
<div class="expandable">
<div class="expandable-header" onclick="toggleExpandable(this)">
<span><strong>${framework.toUpperCase()}</strong></span>
<span class="arrow">โถ</span>
</div>
<div class="expandable-content">
<ul>
${requirements.map(req => `<li>${req}</li>`).join('')}
</ul>
</div>
</div>
`;
});
}
// Comprehensive security recommendations
if (security.recommendations) {
html += `
<div class="expandable open">
<div class="expandable-header" onclick="toggleExpandable(this)">
<span><strong>Comprehensive Security Recommendations</strong></span>
<span class="arrow">โถ</span>
</div>
<div class="expandable-content">
<div style="white-space: pre-wrap; line-height: 1.6; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">${typeof security.recommendations === 'string' ? security.recommendations : JSON.stringify(security.recommendations, null, 2)}</div>
</div>
</div>
`;
}
html += '</div>';
document.getElementById('security').innerHTML = html;
}
function toggleExpandable(header) {
const expandable = header.parentElement;
expandable.classList.toggle('open');
}
// Conversation state
let conversationState = {
sessionId: null,
messages: [],
isActive: false
};
// Track selected choices for multi-select
let selectedChoices = [];
let isMultiSelect = false;
// Handle choice button click
function selectChoice(choice, allowMultiple = false) {
const choicesDiv = document.getElementById('chatChoices');
if (!choicesDiv) return;
// Check if this is a multi-select question (has "Other" option or multiple feature-like options)
const buttons = choicesDiv.querySelectorAll('.choice-btn:not(.wide)');
isMultiSelect = allowMultiple || buttons.length > 4; // More than 4 options usually means multi-select
if (isMultiSelect) {
// Toggle selection
const clickedBtn = Array.from(choicesDiv.querySelectorAll('.choice-btn'))
.find(btn => btn.textContent === choice);
if (clickedBtn) {
if (clickedBtn.classList.contains('selected')) {
clickedBtn.classList.remove('selected');
selectedChoices = selectedChoices.filter(c => c !== choice);
} else {
// If it's a "None"/"Skip" type option, deselect others
if (choice.toLowerCase().includes('none') ||
choice.toLowerCase().includes('skip') ||
choice.toLowerCase().includes('not sure') ||
choice.toLowerCase().includes('no preference')) {
// Deselect all others
choicesDiv.querySelectorAll('.choice-btn').forEach(btn => {
btn.classList.remove('selected');
});
selectedChoices = [choice];
clickedBtn.classList.add('selected');
} else {
// Normal selection
clickedBtn.classList.add('selected');
selectedChoices.push(choice);
// Remove "None" if it was selected
choicesDiv.querySelectorAll('.choice-btn.wide').forEach(btn => {
btn.classList.remove('selected');
});
selectedChoices = selectedChoices.filter(c =>
!c.toLowerCase().includes('none') &&
!c.toLowerCase().includes('skip') &&
!c.toLowerCase().includes('not sure'));
}
}
}
// Add/update "Done" button
let doneBtn = document.getElementById('choiceDoneBtn');
if (!doneBtn && selectedChoices.length > 0) {
doneBtn = document.createElement('button');
doneBtn.id = 'choiceDoneBtn';
doneBtn.className = 'choice-done-btn';
doneBtn.textContent = `Continue with ${selectedChoices.length} selected`;
doneBtn.onclick = submitMultipleChoices;
choicesDiv.appendChild(doneBtn);
} else if (doneBtn) {
if (selectedChoices.length > 0) {
doneBtn.textContent = `Continue with ${selectedChoices.length} selected`;
doneBtn.disabled = false;
} else {
doneBtn.remove();
}
}
} else {
// Single select - submit immediately
choicesDiv.remove();
const input = document.getElementById('chatInput');
input.value = choice;
sendChatMessage();
}
}
// Submit multiple selected choices
function submitMultipleChoices() {
const choicesDiv = document.getElementById('chatChoices');
if (choicesDiv) {
choicesDiv.remove();
}
// Set input value with comma-separated choices
const input = document.getElementById('chatInput');
input.value = selectedChoices.join(', ');
// Reset for next question
selectedChoices = [];
isMultiSelect = false;
sendChatMessage();
}
// Start conversation flow
async function startConversation() {
const query = document.getElementById('queryInput').value.trim();
const dauInput = document.getElementById('dauInput').value;
const dauUnit = parseInt(document.getElementById('dauUnit').value);
const dau = dauInput ? parseInt(dauInput) * dauUnit : null;
// Build initial message with context from form
let initialMessage = query;
if (dau) {
initialMessage += ` with ${dau.toLocaleString()} daily active users`;
}
try {
const response = await fetch('/conversation/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ initial_message: initialMessage })
});
const data = await response.json();
conversationState.sessionId = data.session_id;
conversationState.isActive = true;
// Show chat modal
document.getElementById('chatModal').classList.add('active');
// Add initial messages
addChatMessage('user', initialMessage);
addChatMessage('assistant', data.question, data.options || null);
// Update progress
updateChatProgress(data.completion_percentage);
// Focus input
document.getElementById('chatInput').focus();
} catch (error) {
console.error('Failed to start conversation:', error);
// Fall back to direct recommendation
await getRecommendationDirect();
}
}
// Cancel dialogue and return to main page
function cancelDialogue() {
// Close the chat modal
document.getElementById('chatModal').classList.remove('active');
// Reset conversation state
conversationState.isActive = false;
conversationState.sessionId = null;
// Clear chat messages
document.getElementById('chatMessages').innerHTML = '';
// Clear input
document.getElementById('chatInput').value = '';
// Re-enable buttons
document.getElementById('chatInput').disabled = false;
document.getElementById('chatSendBtn').disabled = false;
// Reset progress bar
updateChatProgress(0);
console.log('Chat dialogue cancelled by user');
}
// Send chat message
async function sendChatMessage() {
const input = document.getElementById('chatInput');
const message = input.value.trim();
if (!message || !conversationState.isActive) return;
// Disable input
input.disabled = true;
document.getElementById('chatSendBtn').disabled = true;
// Add user message
addChatMessage('user', message);
input.value = '';
// Show loading
const loadingDiv = document.createElement('div');
loadingDiv.className = 'chat-message assistant';
loadingDiv.id = 'chatLoading';
loadingDiv.innerHTML = `
<div class="avatar">๐ค</div>
<div class="chat-loading">
<span></span><span></span><span></span>
</div>
`;
document.getElementById('chatMessages').appendChild(loadingDiv);
scrollChatToBottom();
try {
const response = await fetch('/conversation/message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: conversationState.sessionId,
message: message
})
});
const data = await response.json();
// Remove loading
document.getElementById('chatLoading')?.remove();
// Update progress
updateChatProgress(data.completion_percentage);
// Check if ready for recommendation
if (data.ready_for_recommendation) {
addChatMessage('assistant', 'Perfect! I have all the information I need. Generating your recommendations...');
setTimeout(async () => {
// Close chat modal
document.getElementById('chatModal').classList.remove('active');
// Generate recommendation using extracted context
await generateRecommendationFromContext(data.extracted_context);
}, 1500);
} else {
// Add next question
addChatMessage('assistant', data.question, data.options || null);
// Re-enable input
input.disabled = false;
document.getElementById('chatSendBtn').disabled = false;
input.focus();
}
} catch (error) {
console.error('Failed to send message:', error);
document.getElementById('chatLoading')?.remove();
addChatMessage('assistant', 'Sorry, something went wrong. Generating recommendations with current information...');
setTimeout(() => {
document.getElementById('chatModal').classList.remove('active');
getRecommendationDirect();
}, 1500);
}
}
// Add chat message to UI
function addChatMessage(role, content, options = null) {
const messagesDiv = document.getElementById('chatMessages');
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message ${role}`;
const avatar = role === 'assistant' ? '๐ค' : '๐ค';
let messageHTML = `
<div class="avatar">${avatar}</div>
<div class="chat-bubble">${content}</div>
`;
messageDiv.innerHTML = messageHTML;
messagesDiv.appendChild(messageDiv);
// Add choice buttons if provided
if (options && options.length > 0) {
const choicesDiv = document.createElement('div');
choicesDiv.className = 'chat-choices';
choicesDiv.id = 'chatChoices';
options.forEach(option => {
const btn = document.createElement('button');
btn.className = 'choice-btn';
btn.textContent = option;
btn.onclick = () => selectChoice(option);
// Make last option (usually "None"/"Skip") full width
if (option.toLowerCase().includes('none') ||
option.toLowerCase().includes('skip') ||
option.toLowerCase().includes('not sure') ||
option.toLowerCase().includes('no preference')) {
btn.classList.add('wide');
}
choicesDiv.appendChild(btn);
});
messagesDiv.appendChild(choicesDiv);
}
scrollChatToBottom();
}
// Update progress bar
function updateChatProgress(percentage) {
document.getElementById('chatProgressBar').style.width = percentage + '%';
}
// Scroll chat to bottom
function scrollChatToBottom() {
const messagesDiv = document.getElementById('chatMessages');
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// Generate recommendation from extracted context
async function generateRecommendationFromContext(context) {
console.log('Generating recommendation from context:', context);
// Show loading
document.getElementById('loading').style.display = 'block';
document.getElementById('results').style.display = 'none';
document.getElementById('error').style.display = 'none';
document.getElementById('submitBtn').disabled = true;
// Build query from context
let query = document.getElementById('queryInput').value.trim();
// Build request
const requestBody = { query };
// Add DAU if available in context
if (context.dau) {
requestBody.dau = context.dau;
}
// Add API key if user provided one
const ownRadio = document.getElementById('ownRadio');
const userApiKey = document.getElementById('userApiKey').value.trim();
if (ownRadio.checked && userApiKey) {
requestBody.api_key = userApiKey;
}
try {
const response = await fetch('/recommend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
const data = await response.json();
currentData = data;
if (data.status === 'error') {
throw new Error(data.error || 'Unknown error occurred');
}
displayResults(data);
} catch (error) {
console.error('Error:', error);
const errorEl = document.getElementById('error');
errorEl.textContent = `Error: ${error.message}`;
errorEl.style.display = 'block';
} finally {
document.getElementById('loading').style.display = 'none';
document.getElementById('submitBtn').disabled = false;
}
}
// Direct recommendation (fallback)
async function getRecommendationDirect() {
// This is the original function renamed
return getRecommendationOriginal();
}
</script>
<!-- Chat Modal -->
<div id="chatModal" class="chat-modal">
<div class="chat-container">
<div class="chat-header">
<div style="flex: 1;">
<h3>Let's gather some more details</h3>
<div class="chat-progress">
<div id="chatProgressBar" class="chat-progress-bar" style="width: 0%"></div>
</div>
</div>
</div>
<div id="chatMessages" class="chat-messages">
<!-- Messages will be added here dynamically -->
</div>
<div class="chat-input-area">
<input type="text" id="chatInput" class="chat-input" placeholder="Type your answer..."
onkeypress="if(event.key === 'Enter') sendChatMessage()">
<button onclick="cancelDialogue()" id="chatCancelBtn" class="chat-cancel-btn">Cancel</button>
<button onclick="sendChatMessage()" id="chatSendBtn" class="chat-send-btn">Send</button>
</div>
</div>
</div>
<!-- Authentication Script -->
<script src="/auth.js"></script>
<script>
// Global state to track if user has custom API key
let hasCustomApiKey = false;
// Initialize user info on page load
async function initializeUserInfo() {
console.log('=== INITIALIZATION START ===');
console.log('1. Auth.js loaded:', typeof isAuthenticated);
console.log('2. getToken function:', typeof getToken);
// Only show truncated token for security
const token = typeof getToken === 'function' ? getToken() : null;
const tokenPreview = token ? `${token.substring(0, 10)}...${token.substring(token.length - 10)}` : 'No token';
console.log('3. Token preview:', tokenPreview);
console.log('4. isAuthenticated result:', typeof isAuthenticated === 'function' ? isAuthenticated() : 'isAuthenticated not found');
console.log('5. localStorage check:', localStorage.getItem('tech_stack_advisor_token') ? 'Token exists in localStorage' : 'No token in localStorage');
if (typeof isAuthenticated === 'function' && isAuthenticated()) {
try {
console.log('Fetching user info...');
const user = await fetchUserInfo();
console.log('User info received:', user);
if (user) {
// Update user email
document.getElementById('userEmail').textContent = user.email;
// Update user statistics
document.getElementById('userQueries').textContent = user.total_queries || 0;
document.getElementById('userCost').textContent = (user.total_cost_usd || 0).toFixed(4);
// Fetch metrics to calculate budget remaining
try {
const metricsResponse = await fetch('/metrics');
const metrics = await metricsResponse.json();
console.log('Metrics received:', metrics);
// Show budget only if user is using the platform's API key (not BYOK)
// We assume BYOK if the user has 0 cost tracked
hasCustomApiKey = (user.total_queries > 0 && user.total_cost_usd === 0);
if (!hasCustomApiKey && metrics.budget_remaining_usd !== undefined) {
document.getElementById('userBudget').textContent = metrics.budget_remaining_usd.toFixed(2);
document.getElementById('userBudgetStat').style.display = 'flex';
} else {
document.getElementById('userBudgetStat').style.display = 'none';
}
} catch (metricsError) {
console.error('Error fetching metrics:', metricsError);
}
// Show user info, hide login
document.getElementById('userInfo').style.display = 'flex';
document.getElementById('loginInfo').style.display = 'none';
console.log('User info displayed');
} else {
// No user info - show login
console.log('No user info, showing login');
document.getElementById('userInfo').style.display = 'none';
document.getElementById('loginInfo').style.display = 'flex';
}
} catch (error) {
console.error('Error fetching user info:', error);
document.getElementById('userInfo').style.display = 'none';
document.getElementById('loginInfo').style.display = 'flex';
}
} else {
// Not authenticated - show login
console.log('Not authenticated, showing login');
document.getElementById('userInfo').style.display = 'none';
document.getElementById('loginInfo').style.display = 'flex';
}
}
// Logout handler
async function handleLogout() {
console.log('Logging out...');
if (typeof logout === 'function') {
await logout();
} else {
console.error('logout function not found');
}
}
// Update stats after recommendation
async function updateUserStats() {
if (typeof isAuthenticated === 'function' && isAuthenticated()) {
try {
const user = await fetchUserInfo();
if (user) {
document.getElementById('userQueries').textContent = user.total_queries || 0;
document.getElementById('userCost').textContent = (user.total_cost_usd || 0).toFixed(4);
// Update budget if not using custom API key
if (!hasCustomApiKey) {
const metricsResponse = await fetch('/metrics');
const metrics = await metricsResponse.json();
if (metrics.budget_remaining_usd !== undefined) {
document.getElementById('userBudget').textContent = metrics.budget_remaining_usd.toFixed(2);
}
}
}
} catch (error) {
console.error('Error updating user stats:', error);
}
}
}
// Wait for DOM to be ready, then initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeUserInfo);
} else {
// DOM is already ready, initialize immediately
initializeUserInfo();
}
</script>
</body>
</html>