<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GATEPAS Prompt Library Template</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://unpkg.com/lucide-react@0.292.0/dist/lucide-react.js"></script>
<style>
:root {
--header-bg: #1e3a8a; /* dark blue */
--header-text: #ffffff;
--status-production: #dcfce7; /* green */
--status-testing: #fef9c3; /* yellow */
--status-draft: #e0f2fe; /* light blue */
--status-deprecated: #e5e7eb;/* gray */
--review-alert: #fecaca; /* red */
}
.tab-active {
border-bottom: 2px solid var(--header-bg);
color: var(--header-bg);
font-weight: 600;
}
#prompt-library-table th {
background-color: var(--header-bg);
color: var(--header-text);
text-align: center;
padding: 8px;
border: 1px solid #ddd;
position: sticky;
top: 0;
z-index: 10;
}
#prompt-library-table td {
border: 1px solid #ddd;
padding: 6px 8px;
white-space: nowrap;
}
#prompt-library-table td.wrap {
white-space: normal;
}
.status-Production { background-color: var(--status-production); }
.status-Testing { background-color: var(--status-testing); }
.status-Draft { background-color: var(--status-draft); }
.status-Deprecated { background-color: var(--status-deprecated); }
.review-needed { background-color: var(--review-alert); }
.high-performer { color: #15803d; font-weight: bold; }
.modal {
transition: opacity 0.25s ease;
}
</style>
</head>
<body class="bg-gray-100 font-sans">
<div class="container mx-auto p-4">
<div class="bg-white rounded-lg shadow-lg p-6">
<header class="flex justify-between items-center mb-4">
<h1 class="text-2xl font-bold text-gray-800">GATEPAS Prompt Library</h1>
<div class="flex items-center space-x-2">
<label for="file-upload" class="cursor-pointer bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors text-sm font-semibold flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>
Upload .xlsx
</label>
<input id="file-upload" type="file" class="hidden" accept=".xlsx, .xls">
<button id="download-btn" class="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 transition-colors text-sm font-semibold flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
Download Template
</button>
</div>
</header>
<!-- Tabs -->
<div class="border-b border-gray-200">
<nav class="-mb-px flex space-x-6" id="tabs">
<button data-tab="Prompt_Library" class="py-3 px-1 border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none tab-active">Prompt Library</button>
<button data-tab="Analytics" class="py-3 px-1 border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none">Analytics</button>
<button data-tab="Dropdown_Lists" class="py-3 px-1 border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none">Dropdown Lists</button>
<button data-tab="Archive" class="py-3 px-1 border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none">Archive</button>
<button data-tab="Instructions" class="py-3 px-1 border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none">Instructions</button>
</nav>
</div>
<!-- Tab Content -->
<div id="tab-content" class="mt-6">
<!-- Prompt Library -->
<div id="Prompt_Library" class="tab-pane">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center space-x-3">
<button id="add-prompt-btn" class="bg-blue-800 text-white px-4 py-2 rounded-md hover:bg-blue-900 transition-colors text-sm font-semibold flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><line x1="12" x2="12" y1="5" y2="19"/><line x1="5" x2="19" y1="12" y2="12"/></svg>
Add New Prompt
</button>
<div>
<label for="filter-view" class="text-sm font-medium text-gray-700 mr-2">Filter Views:</label>
<select id="filter-view" class="rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<option value="all">All Prompts</option>
<option value="my_active">My Active Prompts</option>
<option value="needs_review">Needs Review</option>
<option value="top_performers">Top Performers</option>
</select>
</div>
</div>
<div class="relative">
<input type="text" id="search-input" placeholder="Search library..." class="pl-8 pr-4 py-2 border rounded-md w-64 focus:outline-none focus:ring-2 focus:ring-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
</div>
</div>
<div class="overflow-x-auto h-[60vh] relative">
<table id="prompt-library-table" class="w-full text-sm">
<thead>
<tr id="table-headers"></tr>
</thead>
<tbody id="table-body"></tbody>
</table>
</div>
</div>
<!-- Analytics -->
<div id="Analytics" class="tab-pane hidden">
<h2 class="text-xl font-bold mb-4 text-gray-700">Prompt Library Analytics</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="bg-gray-50 p-4 rounded-lg shadow">
<h3 class="font-semibold text-gray-600">Total Prompts</h3>
<p id="total-prompts" class="text-3xl font-bold text-blue-800">0</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg shadow">
<h3 class="font-semibold text-gray-600">Production Ready</h3>
<p id="production-ready" class="text-3xl font-bold text-green-600">0</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg shadow">
<h3 class="font-semibold text-gray-600">Avg. Success Rate</h3>
<p id="avg-success" class="text-3xl font-bold text-yellow-600">0%</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg shadow">
<h3 class="font-semibold text-gray-600">Total Hours Saved</h3>
<p id="hours-saved" class="text-3xl font-bold text-indigo-600">0</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="bg-white p-4 rounded-lg shadow">
<h3 class="font-semibold text-center mb-2">Prompts by Status</h3>
<canvas id="status-chart"></canvas>
</div>
<div class="bg-white p-4 rounded-lg shadow">
<h3 class="font-semibold text-center mb-2">Prompts by Practice Area</h3>
<canvas id="practice-area-chart"></canvas>
</div>
<div class="bg-white p-4 rounded-lg shadow">
<h3 class="font-semibold text-center mb-2">Prompts by Document Type</h3>
<canvas id="doc-type-chart"></canvas>
</div>
</div>
</div>
<!-- Dropdown Lists -->
<div id="Dropdown_Lists" class="tab-pane hidden">
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6" id="dropdown-lists-container"></div>
</div>
<!-- Archive -->
<div id="Archive" class="tab-pane hidden">
<p class="text-gray-600">Archived prompts will be stored here. This functionality can be expanded to move prompts from the main library to this sheet.</p>
</div>
<!-- Instructions -->
<div id="Instructions" class="tab-pane hidden prose max-w-none">
<h2 class="font-bold text-lg">HOW TO USE THE GATEPAS PROMPT LIBRARY</h2>
<h3 class="font-semibold">1. FINDING PROMPTS:</h3>
<ul>
<li>Use the search bar at the top right of the library view.</li>
<li>Use the "Filter views" dropdown for pre-configured searches.</li>
<li>Sort by any column by clicking its header.</li>
</ul>
<h3 class="font-semibold">2. ADDING NEW PROMPTS:</h3>
<ul>
<li>Click the "Add New Prompt" button.</li>
<li>Fill in the required fields in the form that appears.</li>
<li>The Status will be set to "Draft" initially.</li>
</ul>
<h3 class="font-semibold">3. SCALE GUIDELINES (1-10):</h3>
<ul>
<li>5-6 is neutral/standard.</li>
<li>Higher numbers indicate more of that quality (e.g., 10 is max Urgency).</li>
</ul>
<h3 class="font-semibold">4. WORKFLOW:</h3>
<p>Draft → Testing (after ~3 uses) → Production → Archive</p>
<hr class="my-6">
<h2 class="font-bold text-lg">WHY A WEB APP (vs. Google Sheets)?</h2>
<h3 class="font-semibold">Collaboration Features</h3>
<ul>
<li><strong>Centralized Data:</strong> Everyone accesses the same, single source of truth.</li>
<li><strong>User Roles (Future):</strong> Permissions can be set for different user types.</li>
</ul>
<h3 class="font-semibold">Automation & Integration</h3>
<ul>
<li><strong>API Access:</strong> Can be integrated with other legal tech tools.</li>
<li><strong>Custom Logic:</strong> Features like automatic ID generation are built-in and reliable.</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Modal for Adding/Editing Prompts -->
<div id="prompt-modal" class="modal fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden">
<div class="relative top-10 mx-auto p-5 border w-full max-w-4xl shadow-lg rounded-md bg-white">
<div class="flex justify-between items-center pb-3">
<p class="text-2xl font-bold" id="modal-title">Add New Prompt</p>
<button id="close-modal-btn" class="cursor-pointer z-50">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<form id="prompt-form" class="space-y-4 max-h-[75vh] overflow-y-auto pr-2">
<!-- Form fields will be generated by JS -->
</form>
<div class="flex justify-end pt-4">
<button id="save-prompt-btn" class="px-4 py-2 bg-blue-600 text-white text-base font-medium rounded-md w-full sm:w-auto hover:bg-blue-700">Save Prompt</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- DATA & STATE MANAGEMENT ---
const headers = [
'Prompt_ID', 'Version', 'Status', 'Prompt_Name', 'Practice_Area', 'Document_Type', 'Jurisdiction', 'Complexity',
'Experience_Level', 'Formality', 'Assertiveness', 'Emotional_Temp', 'Urgency', 'Success_Probability',
'Risk_Tolerance', 'Strategic_Importance', 'Audience_Sophistication', 'Audience_Patience', 'Audience_Predisposition',
'Scope_Breadth', 'Scope_Depth', 'Full_Prompt', 'Guardrails', 'Required_Checkpoints', 'Known_Issues', 'Times_Used',
'Success_Rate', 'Avg_Time_Saved', 'User_Rating', 'Author', 'Owner', 'Created_Date', 'Modified_Date',
'Last_Tested', 'Next_Review', 'Notes'
];
const dropdownData = {
'Status': ['Draft', 'Testing', 'Production', 'Deprecated'],
'Practice_Area': ['Criminal Defense', 'Civil Litigation', 'Corporate', 'Real Estate', 'Family Law', 'Immigration', 'Tax', 'Employment', 'IP'],
'Document_Type': ['Motion', 'Letter', 'Contract', 'Discovery', 'Brief', 'Memo', 'Agreement', 'Pleading', 'Appeal'],
'Complexity': ['Green', 'Yellow', 'Red'],
'Jurisdiction': ['Federal', 'State', 'E.D. La.', 'M.D. La.', 'W.D. La.', 'La. State Court', 'Fifth Circuit']
};
let promptData = [
{ Prompt_ID: 'CIVI-MOTI-001', Version: 1.0, Status: 'Production', Prompt_Name: 'Initial Motion to Compel', Practice_Area: 'Civil Litigation', Document_Type: 'Motion', Jurisdiction: 'Federal', Complexity: 'Yellow', Experience_Level: 5, Formality: 8, Assertiveness: 7, Emotional_Temp: 4, Urgency: 6, Success_Probability: 7, Risk_Tolerance: 4, Strategic_Importance: 8, Audience_Sophistication: 9, Audience_Patience: 3, Audience_Predisposition: 2, Scope_Breadth: 6, Scope_Depth: 7, Full_Prompt: 'Draft a motion to compel discovery responses from Opposing Counsel in the matter of [Case Name], Case No. [Case #]. The motion should address the overdue responses for Interrogatories and Requests for Production served on [Date].', Guardrails: 'Adhere to local rules for formatting. Do not include sanctions request at this stage.', Required_Checkpoints: 'Verify service date of original discovery. Confirm meet-and-confer attempts.', Known_Issues: 'Opposing counsel is notoriously slow to respond.', Times_Used: 15, Success_Rate: 0.9, Avg_Time_Saved: 30, User_Rating: 4.5, Author: 'user@example.com', Owner: 'user@example.com', Created_Date: '2023-01-15', Modified_Date: '2023-10-20', Last_Tested: '2023-10-18', Next_Review: '2024-04-20', Notes: 'Highly effective for standard discovery disputes.' },
{ Prompt_ID: 'CORP-AGRE-002', Version: 0.5, Status: 'Draft', Prompt_Name: 'Standard NDA for Contractors', Practice_Area: 'Corporate', Document_Type: 'Agreement', Jurisdiction: 'State', Complexity: 'Green', Experience_Level: 3, Formality: 9, Assertiveness: 5, Emotional_Temp: 5, Urgency: 3, Success_Probability: 9, Risk_Tolerance: 2, Strategic_Importance: 5, Audience_Sophistication: 5, Audience_Patience: 7, Audience_Predisposition: 5, Scope_Breadth: 4, Scope_Depth: 4, Full_Prompt: 'Generate a standard, unilateral Non-Disclosure Agreement for an independent contractor providing services to our company. Include clauses for confidentiality, return of information, and term of agreement (2 years).', Guardrails: 'Use boilerplate language. No unusual clauses.', Required_Checkpoints: 'Define "Confidential Information" broadly.', Known_Issues: '', Times_Used: 0, Success_Rate: 0, Avg_Time_Saved: 15, User_Rating: 0, Author: 'admin@example.com', Owner: 'admin@example.com', Created_Date: '2023-11-01', Modified_Date: '2023-11-05', Last_Tested: '', Next_Review: '2024-05-01', Notes: 'Awaiting legal review.' },
{ Prompt_ID: 'CRIM-APPE-003', Version: 2.1, Status: 'Testing', Prompt_Name: 'Appellate Brief Opening Statement', Practice_Area: 'Criminal Defense', Document_Type: 'Appeal', Jurisdiction: 'Fifth Circuit', Complexity: 'Red', Experience_Level: 9, Formality: 10, Assertiveness: 9, Emotional_Temp: 6, Urgency: 8, Success_Probability: 4, Risk_Tolerance: 8, Strategic_Importance: 10, Audience_Sophistication: 10, Audience_Patience: 2, Audience_Predisposition: 1, Scope_Breadth: 8, Scope_Depth: 9, Full_Prompt: 'Craft the opening "Statement of the Case" for an appellate brief concerning the wrongful admission of evidence in [Case Name]. Focus on the violation of the Fourth Amendment.', Guardrails: 'Strictly adhere to Fifth Circuit formatting rules. Maintain a respectful but firm tone.', Required_Checkpoints: 'Cite the trial record accurately for every factual assertion.', Known_Issues: 'The panel is known to be hostile to this type of appeal.', Times_Used: 2, Success_Rate: 0.5, Avg_Time_Saved: 120, User_Rating: 4, Author: 'user@example.com', Owner: 'admin@example.com', Created_Date: '2022-05-30', Modified_Date: '2023-09-15', Last_Tested: '2023-09-14', Next_Review: '2023-12-15', Notes: 'Requires careful wording.' }
];
let charts = {};
let currentSort = { column: null, direction: 'asc' };
// --- UI ELEMENTS ---
const tabs = document.getElementById('tabs');
const tabContent = document.getElementById('tab-content');
const tableHeaders = document.getElementById('table-headers');
const tableBody = document.getElementById('table-body');
const modal = document.getElementById('prompt-modal');
const promptForm = document.getElementById('prompt-form');
// --- INITIALIZATION ---
const init = () => {
setupEventListeners();
renderAll();
};
// --- EVENT LISTENERS ---
const setupEventListeners = () => {
tabs.addEventListener('click', handleTabClick);
document.getElementById('add-prompt-btn').addEventListener('click', () => openModal());
document.getElementById('close-modal-btn').addEventListener('click', closeModal);
document.getElementById('save-prompt-btn').addEventListener('click', handleSavePrompt);
document.getElementById('download-btn').addEventListener('click', handleDownload);
document.getElementById('file-upload').addEventListener('change', handleUpload);
document.getElementById('filter-view').addEventListener('change', renderPromptLibrary);
document.getElementById('search-input').addEventListener('input', renderPromptLibrary);
};
// --- RENDER FUNCTIONS ---
const renderAll = () => {
renderHeaders();
renderDropdownLists();
renderPromptLibrary();
renderAnalytics();
};
const renderHeaders = () => {
tableHeaders.innerHTML = `<tr>${headers.map(h => `<th class="cursor-pointer" data-column="${h}">${h.replace(/_/g, ' ')}</th>`).join('')}</tr>`;
tableHeaders.querySelectorAll('th').forEach(th => th.addEventListener('click', (e) => handleSort(e.currentTarget.dataset.column)));
};
const renderPromptLibrary = () => {
const filteredData = getFilteredData();
tableBody.innerHTML = filteredData.map(prompt => {
const isReviewNeeded = prompt.Next_Review && new Date(prompt.Next_Review) <= new Date();
const isHighPerformer = prompt.Times_Used >= 10;
return `<tr class="hover:bg-gray-50">
${headers.map(header => {
let value = prompt[header] || '';
let className = '';
if (header === 'Status') className += ` status-${value}`;
if (header === 'Next_Review' && isReviewNeeded) className += ' review-needed';
if (header === 'Times_Used' && isHighPerformer) className += ' high-performer';
if (['Full_Prompt', 'Guardrails', 'Required_Checkpoints', 'Known_Issues', 'Notes'].includes(header)) {
className += ' wrap';
}
// Formatting
if (header === 'Success_Rate' && typeof value === 'number') value = `${(value * 100).toFixed(0)}%`;
if (header === 'Version' && typeof value === 'number') value = value.toFixed(1);
return `<td class="${className}" title="${value}">${value}</td>`;
}).join('')}
</tr>`;
}).join('');
};
const renderDropdownLists = () => {
const container = document.getElementById('dropdown-lists-container');
container.innerHTML = Object.entries(dropdownData).map(([title, items]) => `
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="font-semibold text-gray-800 border-b pb-2 mb-2">${title.replace(/_/g, ' ')}</h3>
<ul class="space-y-1">${items.map(item => `<li class="text-sm text-gray-600">${item}</li>`).join('')}</ul>
</div>
`).join('');
};
const renderAnalytics = () => {
const total = promptData.length;
const production = promptData.filter(p => p.Status === 'Production').length;
const avgSuccess = promptData.length > 0 ? promptData.reduce((acc, p) => acc + (p.Success_Rate || 0), 0) / total : 0;
const hoursSaved = promptData.reduce((acc, p) => acc + (p.Avg_Time_Saved || 0), 0) / 60;
document.getElementById('total-prompts').textContent = total;
document.getElementById('production-ready').textContent = production;
document.getElementById('avg-success').textContent = `${(avgSuccess * 100).toFixed(1)}%`;
document.getElementById('hours-saved').textContent = hoursSaved.toFixed(1);
createOrUpdateChart('status-chart', 'pie', getChartData('Status'));
createOrUpdateChart('practice-area-chart', 'doughnut', getChartData('Practice_Area'));
createOrUpdateChart('doc-type-chart', 'pie', getChartData('Document_Type'));
};
// --- CHARTING ---
const getChartData = (field) => {
const counts = promptData.reduce((acc, p) => {
const key = p[field] || 'N/A';
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
return {
labels: Object.keys(counts),
datasets: [{
data: Object.values(counts),
backgroundColor: ['#3b82f6', '#16a34a', '#f97316', '#6b7280', '#ef4444', '#8b5cf6', '#d946ef'],
}]
};
};
const createOrUpdateChart = (canvasId, type, data) => {
const ctx = document.getElementById(canvasId).getContext('2d');
if (charts[canvasId]) {
charts[canvasId].data = data;
charts[canvasId].update();
} else {
charts[canvasId] = new Chart(ctx, { type, data, options: { responsive: true, maintainAspectRatio: true } });
}
};
// --- DATA HANDLING & FILTERING ---
const getFilteredData = () => {
let data = [...promptData];
const filter = document.getElementById('filter-view').value;
const search = document.getElementById('search-input').value.toLowerCase();
const today = new Date().toISOString().split('T')[0];
// Apply named filter views
if (filter === 'my_active') {
data = data.filter(p => (p.Status === 'Production' || p.Status === 'Testing') && p.Owner === 'user@example.com');
} else if (filter === 'needs_review') {
data = data.filter(p => p.Next_Review && p.Next_Review <= today);
} else if (filter === 'top_performers') {
data = data.filter(p => p.Success_Rate >= 0.85).sort((a, b) => (b.Times_Used || 0) - (a.Times_Used || 0));
}
// Apply search
if (search) {
data = data.filter(p => Object.values(p).some(val => String(val).toLowerCase().includes(search)));
}
// Apply sort
if (currentSort.column) {
data.sort((a, b) => {
const valA = a[currentSort.column] || '';
const valB = b[currentSort.column] || '';
if (valA < valB) return currentSort.direction === 'asc' ? -1 : 1;
if (valA > valB) return currentSort.direction === 'asc' ? 1 : -1;
return 0;
});
}
return data;
};
const handleSort = (column) => {
if (currentSort.column === column) {
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
currentSort.column = column;
currentSort.direction = 'asc';
}
renderPromptLibrary();
};
// --- MODAL & FORM HANDLING ---
const openModal = (promptId = null) => {
// For simplicity, this implementation only supports adding new prompts.
// An edit functionality would find the prompt by ID and populate the form.
document.getElementById('modal-title').textContent = 'Add New Prompt';
promptForm.innerHTML = createFormFields();
modal.classList.remove('hidden');
};
const closeModal = () => modal.classList.add('hidden');
const createFormFields = (prompt = {}) => {
return headers.map(h => {
const value = prompt[h] || '';
const label = h.replace(/_/g, ' ');
if (dropdownData[h]) {
return `<div><label class="block text-sm font-medium text-gray-700">${label}</label><select name="${h}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">${dropdownData[h].map(opt => `<option value="${opt}" ${opt === value ? 'selected' : ''}>${opt}</option>`).join('')}</select></div>`;
}
if (h.includes('Date') || h.includes('Review') || h.includes('Tested')) {
return `<div><label class="block text-sm font-medium text-gray-700">${label}</label><input type="date" name="${h}" value="${value}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm"></div>`;
}
if (['Full_Prompt', 'Guardrails', 'Required_Checkpoints', 'Known_Issues', 'Notes'].includes(h)) {
return `<div><label class="block text-sm font-medium text-gray-700">${label}</label><textarea name="${h}" rows="3" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">${value}</textarea></div>`;
}
if (typeof value === 'number' || h.includes('Level') || h.includes('Rate') || h.includes('Saved')) {
return `<div><label class="block text-sm font-medium text-gray-700">${label}</label><input type="number" name="${h}" value="${value}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm"></div>`;
}
return `<div><label class="block text-sm font-medium text-gray-700">${label}</label><input type="text" name="${h}" value="${value}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" ${h === 'Prompt_ID' ? 'disabled' : ''}></div>`;
}).join('');
};
const handleSavePrompt = () => {
const formData = new FormData(promptForm);
const newPrompt = {};
for (let [key, value] of formData.entries()) {
// Convert numbers back to number type
if (!isNaN(value) && value !== '') {
const numHeaders = ['Version', 'Experience_Level', 'Formality', 'Assertiveness', 'Emotional_Temp', 'Urgency', 'Success_Probability', 'Risk_Tolerance', 'Strategic_Importance', 'Audience_Sophistication', 'Audience_Patience', 'Audience_Predisposition', 'Scope_Breadth', 'Scope_Depth', 'Times_Used', 'Success_Rate', 'Avg_Time_Saved', 'User_Rating'];
if(numHeaders.includes(key)) value = parseFloat(value);
}
newPrompt[key] = value;
}
// App Script Emulation
const practice = newPrompt.Practice_Area || 'GEN';
const docType = newPrompt.Document_Type || 'DOC';
const prefix = practice.substring(0,4).toUpperCase();
const type = docType.substring(0,4).toUpperCase();
const count = String(promptData.length + 1).padStart(3, '0');
newPrompt.Prompt_ID = `${prefix}-${type}-${count}`;
newPrompt.Version = 0.1;
newPrompt.Status = 'Draft';
const today = new Date().toISOString().split('T')[0];
newPrompt.Created_Date = today;
newPrompt.Modified_Date = today;
newPrompt.Author = 'user@example.com'; // Hardcoded for demo
newPrompt.Owner = 'user@example.com'; // Hardcoded for demo
promptData.push(newPrompt);
closeModal();
renderAll();
};
// --- FILE I/O ---
const handleDownload = () => {
const wb = XLSX.utils.book_new();
// 1. Prompt_Library Sheet
const ws_library = XLSX.utils.json_to_sheet(promptData, { header: headers });
XLSX.utils.book_append_sheet(wb, ws_library, 'Prompt_Library');
// 2. Dropdown_Lists Sheet
const dropdownArray = [];
const maxLen = Math.max(...Object.values(dropdownData).map(arr => arr.length));
const dropdownHeaders = Object.keys(dropdownData);
dropdownArray.push(dropdownHeaders);
for(let i=0; i < maxLen; i++) {
const row = [];
dropdownHeaders.forEach(h => row.push(dropdownData[h][i] || ''));
dropdownArray.push(row);
}
const ws_dropdowns = XLSX.utils.aoa_to_sheet(dropdownArray);
XLSX.utils.book_append_sheet(wb, ws_dropdowns, 'Dropdown_Lists');
// 3. Analytics Sheet
const analyticsData = [
['PROMPT LIBRARY ANALYTICS'],
[],
[`Total Prompts:`, {f: `COUNTA(Prompt_Library!C:C)-1`}],
[`Production Ready:`, {f: `COUNTIF(Prompt_Library!C:C,"Production")`}],
[`Avg Success Rate:`, {f: `AVERAGE(Prompt_Library!AA:AA)`, z: '0.0%'}],
[`Total Hours Saved:`, {f: `SUM(Prompt_Library!AB:AB)/60`, z: '0.0'}],
];
const ws_analytics = XLSX.utils.aoa_to_sheet(analyticsData);
XLSX.utils.book_append_sheet(wb, ws_analytics, 'Analytics');
// 4. Other Sheets
const ws_archive = XLSX.utils.aoa_to_sheet([headers]);
XLSX.utils.book_append_sheet(wb, ws_archive, 'Archive');
const instructionsText = document.getElementById('Instructions').innerText;
const ws_instructions = XLSX.utils.aoa_to_sheet([[instructionsText]]);
XLSX.utils.book_append_sheet(wb, ws_instructions, 'Instructions');
XLSX.writeFile(wb, "GATEPAS_Prompt_Library.xlsx");
};
const handleUpload = (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, {type: 'array', cellDates:true});
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet);
// Basic data validation
if (jsonData.length > 0 && headers.every(h => Object.keys(jsonData[0]).includes(h))) {
promptData = jsonData.map(row => {
// Ensure dates are formatted correctly
headers.forEach(h => {
if (h.includes('Date') || h.includes('Review') || h.includes('Tested')) {
if (row[h] instanceof Date) {
row[h] = row[h].toISOString().split('T')[0];
}
}
});
return row;
});
renderAll();
alert('File uploaded and data loaded successfully!');
} else {
alert('Error: The uploaded file does not have the correct headers.');
}
};
reader.readAsArrayBuffer(file);
};
// --- TABS ---
const handleTabClick = (e) => {
if (e.target.tagName !== 'BUTTON') return;
document.querySelectorAll('#tabs button').forEach(b => b.classList.remove('tab-active'));
e.target.classList.add('tab-active');
document.querySelectorAll('.tab-pane').forEach(p => p.classList.add('hidden'));
document.getElementById(e.target.dataset.tab).classList.remove('hidden');
};
// --- START THE APP ---
init();
});
</script>
</body>
</html>