forensic-pathways/tools-yaml-editor.html
2025-08-08 13:05:40 +02:00

1907 lines
78 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tools.yaml Editor</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
<style>
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--text-primary: #212529;
--text-secondary: #6c757d;
--border-color: #dee2e6;
--accent-color: #0d6efd;
--success-color: #198754;
--warning-color: #fd7e14;
--danger-color: #dc3545;
--shadow: 0 2px 4px rgba(0,0,0,0.1);
}
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #404040;
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--border-color: #404040;
--accent-color: #4a9eff;
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--shadow: 0 2px 4px rgba(0,0,0,0.3);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.5;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: var(--bg-secondary);
border-radius: 8px;
box-shadow: var(--shadow);
}
.controls {
display: flex;
gap: 15px;
align-items: center;
}
.search-section {
display: grid;
grid-template-columns: 1fr auto;
gap: 20px;
margin-bottom: 20px;
padding: 20px;
background: var(--bg-secondary);
border-radius: 8px;
box-shadow: var(--shadow);
}
.search-filters {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr;
gap: 15px;
align-items: end;
}
.bulk-actions {
display: flex;
gap: 10px;
}
.main-content {
display: grid;
grid-template-columns: 1fr 400px;
gap: 20px;
}
.tools-section {
background: var(--bg-secondary);
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow);
}
.edit-section {
background: var(--bg-secondary);
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow);
position: sticky;
top: 20px;
max-height: calc(100vh - 40px);
overflow-y: auto;
}
.tools-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.tools-list {
max-height: 70vh;
overflow-y: auto;
}
.tool-item {
padding: 15px;
border: 1px solid var(--border-color);
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.2s;
background: var(--bg-primary);
}
.tool-item:hover {
background: var(--bg-tertiary);
}
.tool-item.selected {
border-color: var(--accent-color);
background: var(--bg-tertiary);
}
.tool-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.tool-name {
font-weight: 600;
color: var(--accent-color);
margin-bottom: 4px;
}
.tool-meta {
display: flex;
gap: 10px;
font-size: 0.85em;
color: var(--text-secondary);
}
.tool-description {
font-size: 0.9em;
margin-top: 8px;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 8px;
}
.tag {
background: var(--accent-color);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75em;
}
.form-group {
margin-bottom: 15px;
}
.form-label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-input, .form-select, .form-textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.form-checkbox {
margin-right: 8px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background: var(--accent-color);
color: white;
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-success {
background: var(--success-color);
color: white;
}
.btn-warning {
background: var(--warning-color);
color: white;
}
.btn-danger {
background: var(--danger-color);
color: white;
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.autocomplete-container {
position: relative;
}
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-top: none;
border-radius: 0 0 4px 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.autocomplete-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--border-color);
}
.autocomplete-item:hover,
.autocomplete-item.selected {
background: var(--bg-tertiary);
}
.autocomplete-item:last-child {
border-bottom: none;
}
.status-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9em;
}
.theme-toggle {
background: none;
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: 4px;
cursor: pointer;
padding: 6px 10px;
}
.validation-errors {
background: var(--danger-color);
color: white;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
display: none;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.stat-item {
text-align: center;
padding: 10px;
background: var(--bg-tertiary);
border-radius: 4px;
}
.stat-number {
font-size: 1.5em;
font-weight: bold;
color: var(--accent-color);
}
.bulk-panel {
display: none;
background: var(--bg-tertiary);
padding: 15px;
border-radius: 6px;
margin-top: 15px;
}
.bulk-panel.active {
display: block;
}
.tag-input-container {
position: relative;
}
.tag-input {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 36px;
padding: 4px 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
cursor: text;
}
.tag-input-item {
background: var(--accent-color);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
display: flex;
align-items: center;
gap: 4px;
}
.tag-input-item .remove {
cursor: pointer;
font-weight: bold;
}
.tag-input input {
border: none;
background: none;
outline: none;
color: var(--text-primary);
min-width: 100px;
flex: 1;
}
@media (max-width: 1200px) {
.main-content {
grid-template-columns: 1fr;
}
.edit-section {
position: static;
max-height: none;
}
.search-filters {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body data-theme="dark">
<div class="container">
<header class="header">
<h1>🔧 Tools.yaml Editor</h1>
<div class="controls">
<button class="theme-toggle" onclick="toggleTheme()">☀️</button>
<input type="file" id="importFile" accept=".yaml,.yml" style="display: none;" onchange="importYAML(event)">
<button class="btn btn-secondary" onclick="document.getElementById('importFile').click()">Import YAML</button>
<button class="btn btn-primary" onclick="exportYAML()">Export YAML</button>
<button class="btn btn-success" onclick="addNewTool()">Add Tool</button>
</div>
</header>
<section class="search-section">
<div class="search-filters">
<div class="form-group">
<label class="form-label">Search Tools</label>
<div class="autocomplete-container">
<input type="text" id="searchInput" class="form-input" placeholder="Search by name, description, tags..." oninput="filterTools()">
<div class="autocomplete-dropdown" id="searchAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Type</label>
<select id="typeFilter" class="form-select" onchange="filterTools()">
<option value="">All Types</option>
<option value="software">Software</option>
<option value="method">Method</option>
<option value="concept">Concept</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Skill Level</label>
<select id="skillFilter" class="form-select" onchange="filterTools()">
<option value="">All Levels</option>
<option value="novice">Novice</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
<option value="expert">Expert</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Domain</label>
<select id="domainFilter" class="form-select" onchange="filterTools()">
<option value="">All Domains</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Knowledgebase</label>
<select id="knowledgebaseFilter" class="form-select" onchange="filterTools()">
<option value="">All</option>
<option value="true">Has KB</option>
<option value="false">No KB</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Validation</label>
<select id="validationFilter" class="form-select" onchange="filterTools()">
<option value="">All</option>
<option value="valid">Valid Only</option>
<option value="invalid">Has Issues</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Sort by</label>
<select id="sortBy" class="form-select" onchange="sortTools()">
<option value="name">Name</option>
<option value="type">Type</option>
<option value="skillLevel">Skill Level</option>
<option value="domains">Domain</option>
</select>
</div>
</div>
<div class="bulk-actions">
<button class="btn btn-warning btn-sm" onclick="validateAllTools()">Validate All</button>
<button class="btn btn-danger btn-sm" onclick="fixMissingUrls()">Fix Missing URLs</button>
<button class="btn btn-secondary btn-sm" onclick="clearAllFilters()">Clear Filters</button>
<button class="btn btn-secondary btn-sm" onclick="toggleBulkPanel()">Bulk Actions</button>
<button class="btn btn-secondary btn-sm" onclick="showStats()">Statistics</button>
</div>
</section>
<div class="bulk-panel" id="bulkPanel">
<h3>Bulk Operations</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 10px;">
<div>
<button class="btn btn-warning btn-sm" onclick="bulkRemoveTags()">Remove Tags</button>
<input type="text" id="bulkRemoveTagsInput" placeholder="Tags to remove (comma-separated)" class="form-input" style="margin-top: 5px; font-size: 12px;">
</div>
<div>
<button class="btn btn-warning btn-sm" onclick="bulkRenameTags()">Rename Tags</button>
<input type="text" id="bulkRenameFrom" placeholder="From tag" class="form-input" style="margin-top: 5px; font-size: 12px;">
<input type="text" id="bulkRenameTo" placeholder="To tag" class="form-input" style="margin-top: 5px; font-size: 12px;">
</div>
<div>
<button class="btn btn-success btn-sm" onclick="bulkAssignRelatedConcepts()">Assign Related Concepts</button>
<input type="text" id="bulkConceptsInput" placeholder="Concepts (comma-separated)" class="form-input" style="margin-top: 5px; font-size: 12px;">
</div>
<div>
<button class="btn btn-success btn-sm" onclick="bulkAssignRelatedSoftware()">Assign Related Software</button>
<input type="text" id="bulkSoftwareInput" placeholder="Software (comma-separated)" class="form-input" style="margin-top: 5px; font-size: 12px;">
</div>
</div>
</div>
<div class="main-content">
<section class="tools-section">
<div class="tools-header">
<h2>Tools (<span id="toolCount">0</span>)</h2>
<div style="display: flex; gap: 10px; align-items: center;">
<div id="validationSummary" style="display: none; padding: 5px 10px; background: var(--warning-color); color: white; border-radius: 4px; font-size: 0.9em;">
<span id="validationCount">0</span> tools need fixing
</div>
<button class="btn btn-danger btn-sm" onclick="clearSelection()">Clear Selection</button>
</div>
</div>
<div class="stats" id="statsContainer" style="display: none;"></div>
<div class="validation-errors" id="validationErrors"></div>
<div class="tools-list" id="toolsList"></div>
</section>
<section class="edit-section">
<h2 id="editTitle">Tool Editor</h2>
<form id="toolForm">
<div class="form-group">
<label class="form-label">Name *</label>
<input type="text" id="name" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">Type *</label>
<select id="type" class="form-select" required>
<option value="">Select type...</option>
<option value="software">Software</option>
<option value="method">Method</option>
<option value="concept">Concept</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Icon</label>
<input type="text" id="icon" class="form-input" placeholder="🔧">
</div>
<div class="form-group">
<label class="form-label">Description *</label>
<textarea id="description" class="form-textarea" required placeholder="Detailed description of the tool..."></textarea>
</div>
<div class="form-group">
<label class="form-label">URL *</label>
<input type="url" id="url" class="form-input" required placeholder="https://example.com">
</div>
<div class="form-group">
<label class="form-label">Project URL</label>
<input type="url" id="projectUrl" class="form-input" placeholder="https://project.example.com">
</div>
<div class="form-group">
<label class="form-label">Skill Level *</label>
<select id="skillLevel" class="form-select" required>
<option value="">Select level...</option>
<option value="novice">Novice</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
<option value="expert">Expert</option>
</select>
</div>
<div class="form-group">
<label class="form-label">License</label>
<input type="text" id="license" class="form-input" placeholder="MIT, GPL-3.0, Proprietary...">
</div>
<div class="form-group">
<label class="form-label">Access Type</label>
<select id="accessType" class="form-select">
<option value="">Select access type...</option>
<option value="download">Download</option>
<option value="web">Web</option>
<option value="api">API</option>
<option value="cli">CLI</option>
<option value="commercial">Commercial</option>
<option value="server-based">Server-based</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Domains</label>
<div class="tag-input-container">
<div class="tag-input" id="domainsInput" onclick="focusTagInput('domainsInput')">
<input type="text" placeholder="Type domain..." onkeydown="handleTagInput(event, 'domains')" oninput="showAutocomplete(event, 'domains')">
</div>
<div class="autocomplete-dropdown" id="domainsAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Phases</label>
<div class="tag-input-container">
<div class="tag-input" id="phasesInput" onclick="focusTagInput('phasesInput')">
<input type="text" placeholder="Type phase..." onkeydown="handleTagInput(event, 'phases')" oninput="showAutocomplete(event, 'phases')">
</div>
<div class="autocomplete-dropdown" id="phasesAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Platforms</label>
<div class="tag-input-container">
<div class="tag-input" id="platformsInput" onclick="focusTagInput('platformsInput')">
<input type="text" placeholder="Type platform..." onkeydown="handleTagInput(event, 'platforms')" oninput="showAutocomplete(event, 'platforms')">
</div>
<div class="autocomplete-dropdown" id="platformsAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Tags</label>
<div class="tag-input-container">
<div class="tag-input" id="tagsInput" onclick="focusTagInput('tagsInput')">
<input type="text" placeholder="Type tag..." onkeydown="handleTagInput(event, 'tags')" oninput="showAutocomplete(event, 'tags')">
</div>
<div class="autocomplete-dropdown" id="tagsAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Related Concepts</label>
<div class="tag-input-container">
<div class="tag-input" id="related_conceptsInput" onclick="focusTagInput('related_conceptsInput')">
<input type="text" placeholder="Type concept..." onkeydown="handleTagInput(event, 'related_concepts')" oninput="showAutocomplete(event, 'related_concepts')">
</div>
<div class="autocomplete-dropdown" id="related_conceptsAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Related Software</label>
<div class="tag-input-container">
<div class="tag-input" id="related_softwareInput" onclick="focusTagInput('related_softwareInput')">
<input type="text" placeholder="Type software..." onkeydown="handleTagInput(event, 'related_software')" oninput="showAutocomplete(event, 'related_software')">
</div>
<div class="autocomplete-dropdown" id="related_softwareAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="knowledgebase" class="form-checkbox">
Has Knowledgebase
</label>
</div>
<div class="form-group" style="display: flex; gap: 10px; margin-top: 20px;">
<button type="button" class="btn btn-primary" onclick="saveTool()">Save Tool</button>
<button type="button" class="btn btn-secondary" onclick="clearForm()">Clear</button>
<button type="button" class="btn btn-danger" onclick="deleteTool()" id="deleteBtn" style="display: none;">Delete</button>
</div>
</form>
</section>
</div>
</div>
<div class="status-bar">
<div id="statusText">Ready</div>
<div style="font-size: 0.8em; color: var(--text-secondary);">
Tools.yaml Editor v1.0 |
Shortcuts: Ctrl+S (Save), Ctrl+N (New), Ctrl+E (Export), Alt+V (Validate), Ctrl+F (Search), Esc (Clear)
</div>
</div>
<script>
// Global data storage
let toolsData = {
tools: [],
domains: [],
phases: [],
'domain-agnostic-software': [],
scenarios: [],
skill_levels: {}
};
let filteredTools = [];
let selectedTool = null;
let editingIndex = -1;
// Initialize the application
function init() {
// Load theme preference
const savedTheme = localStorage.getItem('tools-editor-theme') || 'dark';
document.body.setAttribute('data-theme', savedTheme);
const themeToggle = document.querySelector('.theme-toggle');
themeToggle.textContent = savedTheme === 'light' ? '🌙' : '☀️';
// Load sample domains and phases if none exist
if (toolsData.domains.length === 0) {
toolsData.domains = [
{ id: 'incident-response', name: 'Incident Response' },
{ id: 'static-investigations', name: 'Static Investigations' },
{ id: 'malware-analysis', name: 'Malware Analysis' },
{ id: 'network-forensics', name: 'Network Forensics' },
{ id: 'mobile-forensics', name: 'Mobile Forensics' },
{ id: 'cloud-forensics', name: 'Cloud Forensics' }
];
}
if (toolsData.phases.length === 0) {
toolsData.phases = [
{ id: 'data-collection', name: 'Data Collection' },
{ id: 'examination', name: 'Examination' },
{ id: 'analysis', name: 'Analysis' },
{ id: 'reporting', name: 'Reporting' }
];
}
populateFilters();
renderTools();
updateStatus('Application initialized');
}
// Theme management
function toggleTheme() {
const body = document.body;
const currentTheme = body.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
body.setAttribute('data-theme', newTheme);
// Save theme preference
localStorage.setItem('tools-editor-theme', newTheme);
const themeToggle = document.querySelector('.theme-toggle');
themeToggle.textContent = newTheme === 'light' ? '🌙' : '☀️';
updateStatus(`Switched to ${newTheme} theme`);
}
// Status updates
function updateStatus(message) {
document.getElementById('statusText').textContent = message;
}
// Populate filter dropdowns
function populateFilters() {
const domainFilter = document.getElementById('domainFilter');
domainFilter.innerHTML = '<option value="">All Domains</option>';
toolsData.domains.forEach(domain => {
const option = document.createElement('option');
option.value = domain.id;
option.textContent = domain.name;
domainFilter.appendChild(option);
});
}
// Get all unique values for autocomplete
function getUniqueValues(field) {
const values = new Set();
toolsData.tools.forEach(tool => {
if (tool[field]) {
if (Array.isArray(tool[field])) {
tool[field].forEach(value => values.add(value));
} else {
values.add(tool[field]);
}
}
});
// Add predefined values
if (field === 'domains') {
toolsData.domains.forEach(domain => values.add(domain.id));
} else if (field === 'phases') {
toolsData.phases.forEach(phase => values.add(phase.id));
} else if (field === 'platforms') {
['Windows', 'macOS', 'Linux', 'Web', 'Mobile', 'Cross-platform'].forEach(p => values.add(p));
} else if (field === 'related_concepts') {
toolsData.tools.filter(t => t.type === 'concept').forEach(t => values.add(t.name));
} else if (field === 'related_software') {
toolsData.tools.filter(t => t.type === 'software').forEach(t => values.add(t.name));
}
return Array.from(values).sort();
}
// Tag input handling
function focusTagInput(containerId) {
const input = document.querySelector(`#${containerId} input`);
input.focus();
}
function handleTagInput(event, field) {
const dropdown = document.getElementById(`${field}Autocomplete`);
const items = dropdown.querySelectorAll('.autocomplete-item');
let selectedIndex = Array.from(items).findIndex(item => item.classList.contains('selected'));
if (event.key === 'ArrowDown') {
event.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
updateAutocompleteSelection(items, selectedIndex);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, 0);
updateAutocompleteSelection(items, selectedIndex);
} else if (event.key === 'Enter') {
event.preventDefault();
if (selectedIndex >= 0 && items[selectedIndex]) {
const value = items[selectedIndex].textContent;
addTag(field, value);
event.target.value = '';
hideAutocomplete(field);
} else {
const value = event.target.value.trim();
if (value) {
addTag(field, value);
event.target.value = '';
hideAutocomplete(field);
}
}
} else if (event.key === ',' || event.key === 'Tab') {
event.preventDefault();
const input = event.target;
const value = input.value.trim();
if (value) {
addTag(field, value);
input.value = '';
hideAutocomplete(field);
}
} else if (event.key === 'Backspace' && event.target.value === '') {
removeLastTag(field);
}
}
function updateAutocompleteSelection(items, selectedIndex) {
items.forEach((item, index) => {
item.classList.toggle('selected', index === selectedIndex);
});
}
function addTag(field, value) {
const container = document.getElementById(`${field}Input`);
const input = container.querySelector('input');
// Check if tag already exists
const existingTags = Array.from(container.querySelectorAll('.tag-input-item')).map(item =>
item.textContent.replace('×', '').trim()
);
if (existingTags.includes(value)) return;
const tagElement = document.createElement('div');
tagElement.className = 'tag-input-item';
tagElement.innerHTML = `${value} <span class="remove" onclick="removeTag('${field}', this)">×</span>`;
container.insertBefore(tagElement, input);
}
function removeTag(field, element) {
element.parentElement.remove();
}
function removeLastTag(field) {
const container = document.getElementById(`${field}Input`);
const tags = container.querySelectorAll('.tag-input-item');
if (tags.length > 0) {
tags[tags.length - 1].remove();
}
}
function getTagValues(field) {
const container = document.getElementById(`${field}Input`);
return Array.from(container.querySelectorAll('.tag-input-item')).map(item =>
item.textContent.replace('×', '').trim()
);
}
function setTagValues(field, values) {
const container = document.getElementById(`${field}Input`);
// Clear existing tags
container.querySelectorAll('.tag-input-item').forEach(item => item.remove());
// Add new tags
if (values && Array.isArray(values)) {
values.forEach(value => addTag(field, value));
}
}
// Autocomplete functionality
function showAutocomplete(event, field) {
const input = event.target;
const value = input.value.toLowerCase();
const dropdown = document.getElementById(`${field}Autocomplete`);
if (value.length < 1) {
hideAutocomplete(field);
return;
}
const suggestions = getUniqueValues(field).filter(item =>
item.toLowerCase().includes(value)
);
if (suggestions.length === 0) {
hideAutocomplete(field);
return;
}
dropdown.innerHTML = '';
suggestions.slice(0, 10).forEach((suggestion, index) => {
const item = document.createElement('div');
item.className = 'autocomplete-item';
if (index === 0) item.classList.add('selected'); // Select first item by default
item.textContent = suggestion;
item.onclick = () => {
addTag(field, suggestion);
input.value = '';
hideAutocomplete(field);
};
dropdown.appendChild(item);
});
dropdown.style.display = 'block';
}
function hideAutocomplete(field) {
document.getElementById(`${field}Autocomplete`).style.display = 'none';
}
// Tool management
function addNewTool() {
clearForm();
editingIndex = -1;
document.getElementById('editTitle').textContent = 'Add New Tool';
document.getElementById('deleteBtn').style.display = 'none';
updateStatus('Creating new tool');
}
function editTool(index) {
const tool = filteredTools[index];
editingIndex = toolsData.tools.indexOf(tool);
// Populate form
document.getElementById('name').value = tool.name || '';
document.getElementById('type').value = tool.type || '';
document.getElementById('icon').value = tool.icon || '';
document.getElementById('description').value = tool.description || '';
document.getElementById('url').value = tool.url || '';
document.getElementById('projectUrl').value = tool.projectUrl || '';
document.getElementById('skillLevel').value = tool.skillLevel || '';
document.getElementById('license').value = tool.license || '';
document.getElementById('accessType').value = tool.accessType || '';
document.getElementById('knowledgebase').checked = tool.knowledgebase || false;
// Set tag values
setTagValues('domains', tool.domains);
setTagValues('phases', tool.phases);
setTagValues('platforms', tool.platforms);
setTagValues('tags', tool.tags);
setTagValues('related_concepts', tool.related_concepts);
setTagValues('related_software', tool.related_software);
document.getElementById('editTitle').textContent = `Edit: ${tool.name}`;
document.getElementById('deleteBtn').style.display = 'inline-block';
updateStatus(`Editing ${tool.name}`);
}
function saveTool() {
const form = document.getElementById('toolForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const tool = {
name: document.getElementById('name').value.trim(),
type: document.getElementById('type').value,
description: document.getElementById('description').value.trim(),
url: document.getElementById('url').value.trim(),
skillLevel: document.getElementById('skillLevel').value,
domains: getTagValues('domains'),
phases: getTagValues('phases'),
platforms: getTagValues('platforms'),
tags: getTagValues('tags'),
related_concepts: getTagValues('related_concepts'),
related_software: getTagValues('related_software')
};
// Add optional fields only if they have values
const icon = document.getElementById('icon').value.trim();
if (icon) tool.icon = icon;
const projectUrl = document.getElementById('projectUrl').value.trim();
if (projectUrl) tool.projectUrl = projectUrl;
const license = document.getElementById('license').value.trim();
if (license) tool.license = license;
const accessType = document.getElementById('accessType').value;
if (accessType) tool.accessType = accessType;
if (document.getElementById('knowledgebase').checked) {
tool.knowledgebase = true;
}
// Remove empty arrays to keep YAML clean
Object.keys(tool).forEach(key => {
if (Array.isArray(tool[key]) && tool[key].length === 0) {
delete tool[key];
}
});
// Validate tool
const errors = validateTool(tool);
if (errors.length > 0) {
showValidationErrors(errors);
return;
}
hideValidationErrors();
const isNewTool = editingIndex < 0;
const toolName = tool.name;
if (editingIndex >= 0) {
// Update existing tool
toolsData.tools[editingIndex] = tool;
// Show immediate feedback
updateStatus(`✅ Updated ${toolName}`);
// Add temporary success indicator to the form
showSaveSuccess('Tool updated successfully!');
} else {
// Add new tool
toolsData.tools.push(tool);
// Show immediate feedback
updateStatus(`✅ Added ${toolName}`);
// Add temporary success indicator to the form
showSaveSuccess('Tool added successfully!');
}
// Refresh the view immediately - this will update filtered results
filterTools(); // This ensures tools disappear from "invalid" filter if fixed
// Clear form after successful save
clearForm();
// If we were viewing validation errors, update the validation summary
setTimeout(() => {
const validationFilter = document.getElementById('validationFilter').value;
if (validationFilter === 'invalid') {
// Re-run validation check to update counts
const remainingErrors = [];
toolsData.tools.forEach((t, index) => {
const toolErrors = validateTool(t, index);
remainingErrors.push(...toolErrors);
});
if (remainingErrors.length === 0) {
// No more invalid tools - show success message
hideValidationErrors();
updateStatus('🎉 All validation errors fixed!');
// Optionally clear the validation filter
document.getElementById('validationFilter').value = '';
filterTools();
} else {
// Update validation summary
const validationSummary = document.getElementById('validationSummary');
const validationCount = document.getElementById('validationCount');
if (validationSummary && validationCount) {
validationCount.textContent = toolsData.tools.filter(t => validateToolBasic(t).length > 0).length;
validationSummary.style.display = validationCount.textContent === '0' ? 'none' : 'block';
}
}
}
}, 100);
}
// Helper function to show save success feedback
function showSaveSuccess(message) {
// Create or update success indicator
let successDiv = document.getElementById('saveSuccess');
if (!successDiv) {
successDiv = document.createElement('div');
successDiv.id = 'saveSuccess';
successDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: var(--success-color);
color: white;
padding: 12px 20px;
border-radius: 6px;
box-shadow: var(--shadow);
z-index: 1000;
opacity: 0;
transform: translateY(-20px);
transition: all 0.3s ease;
`;
document.body.appendChild(successDiv);
}
successDiv.textContent = message;
// Animate in
requestAnimationFrame(() => {
successDiv.style.opacity = '1';
successDiv.style.transform = 'translateY(0)';
});
// Animate out after 3 seconds
setTimeout(() => {
successDiv.style.opacity = '0';
successDiv.style.transform = 'translateY(-20px)';
setTimeout(() => {
if (successDiv.parentNode) {
successDiv.parentNode.removeChild(successDiv);
}
}, 300);
}, 3000);
}
function deleteTool() {
if (editingIndex >= 0 && confirm('Are you sure you want to delete this tool?')) {
const toolName = toolsData.tools[editingIndex].name;
toolsData.tools.splice(editingIndex, 1);
updateStatus(`Deleted ${toolName}`);
renderTools();
clearForm();
}
}
function clearForm() {
document.getElementById('toolForm').reset();
// Clear tag inputs
['domains', 'phases', 'platforms', 'tags', 'related_concepts', 'related_software'].forEach(field => {
setTagValues(field, []);
});
editingIndex = -1;
document.getElementById('editTitle').textContent = 'Tool Editor';
document.getElementById('deleteBtn').style.display = 'none';
}
// Basic validation for rendering (no duplicate checks)
function validateToolBasic(tool) {
const errors = [];
// Required fields validation
if (!tool.name || typeof tool.name !== 'string' || tool.name.trim() === '') {
errors.push('Name is required and must be a non-empty string');
}
if (!tool.type || !['software', 'method', 'concept'].includes(tool.type)) {
errors.push('Type is required and must be one of: software, method, concept');
}
if (!tool.description || typeof tool.description !== 'string' || tool.description.trim() === '') {
errors.push('Description is required and must be a non-empty string');
}
if (!tool.url || typeof tool.url !== 'string' || tool.url.trim() === '') {
errors.push('URL is required and must be a non-empty string');
}
if (!tool.skillLevel || !['novice', 'beginner', 'intermediate', 'advanced', 'expert'].includes(tool.skillLevel)) {
errors.push('Skill level is required and must be one of: novice, beginner, intermediate, advanced, expert');
}
// URL format validation
if (tool.url && tool.url.trim() !== '') {
try {
new URL(tool.url);
} catch {
errors.push('URL must be a valid URL format');
}
}
// Project URL validation (if provided)
if (tool.projectUrl && tool.projectUrl.trim() !== '') {
try {
new URL(tool.projectUrl);
} catch {
errors.push('Project URL must be a valid URL format');
}
}
// Array fields validation
const arrayFields = ['domains', 'phases', 'platforms', 'tags', 'related_concepts', 'related_software'];
arrayFields.forEach(field => {
if (tool[field] !== undefined && tool[field] !== null && !Array.isArray(tool[field])) {
errors.push(`${field} must be an array if provided`);
}
});
// String fields validation (optional but must be strings if provided)
const stringFields = ['icon', 'license', 'accessType'];
stringFields.forEach(field => {
if (tool[field] !== undefined && tool[field] !== null && typeof tool[field] !== 'string') {
errors.push(`${field} must be a string if provided`);
}
});
// Boolean field validation
if (tool.knowledgebase !== undefined && tool.knowledgebase !== null && typeof tool.knowledgebase !== 'boolean') {
errors.push('knowledgebase must be a boolean if provided');
}
return errors;
}
// Full validation for form submission and bulk validation (includes duplicate checks)
function validateTool(tool, index = null) {
const prefix = index !== null ? `Tool ${index + 1} (${tool.name || 'Unnamed'}): ` : '';
const errors = validateToolBasic(tool).map(error => prefix + error);
// Check for duplicate names (only for single tool validation)
if (index === null) {
const existingIndex = toolsData.tools.findIndex(t => t.name === tool.name);
if (existingIndex >= 0 && existingIndex !== editingIndex) {
errors.push(prefix + 'Tool name already exists');
}
}
return errors;
}
// Update the renderTools function to use basic validation
// Find this line in the renderTools function:
// And replace it with:
// const hasValidationIssues = validateToolBasic(tool).length > 0;
function fixMissingUrls() {
const toolsWithMissingUrls = toolsData.tools.filter(tool =>
!tool.url || typeof tool.url !== 'string' || tool.url.trim() === ''
);
if (toolsWithMissingUrls.length === 0) {
updateStatus('✅ No tools with missing URLs found');
return;
}
const confirmMessage = `Found ${toolsWithMissingUrls.length} tools with missing URLs:\n\n${toolsWithMissingUrls.map(t => `${t.name || 'Unnamed'}`).join('\n')}\n\nFix by adding placeholder URLs?`;
if (confirm(confirmMessage)) {
let fixedCount = 0;
toolsData.tools.forEach(tool => {
if (!tool.url || typeof tool.url !== 'string' || tool.url.trim() === '') {
tool.url = 'https://example.com'; // Placeholder URL
fixedCount++;
}
});
updateStatus(`🔧 Fixed ${fixedCount} tools with missing URLs (added placeholder URLs)`);
renderTools();
// Show validation results
setTimeout(() => validateAllTools(), 500);
}
}
function validateAllTools() {
const allErrors = [];
toolsData.tools.forEach((tool, index) => {
const toolErrors = validateTool(tool, index);
allErrors.push(...toolErrors);
});
// Check for duplicate names across all tools
const nameMap = new Map();
toolsData.tools.forEach((tool, index) => {
if (tool.name) {
if (nameMap.has(tool.name)) {
allErrors.push(`Duplicate name "${tool.name}" found at indices ${nameMap.get(tool.name)} and ${index}`);
} else {
nameMap.set(tool.name, index);
}
}
});
if (allErrors.length === 0) {
updateStatus('✅ All tools are valid');
hideValidationErrors();
} else {
updateStatus(`❌ Found ${allErrors.length} validation errors`);
showValidationErrors(allErrors);
// Scroll to errors
document.getElementById('validationErrors').scrollIntoView({ behavior: 'smooth' });
}
return allErrors.length === 0;
}
function showValidationErrors(errors) {
const errorDiv = document.getElementById('validationErrors');
if (Array.isArray(errors) && errors.length > 10) {
// Show first 10 errors and a summary for large error lists
const firstTen = errors.slice(0, 10);
const remaining = errors.length - 10;
errorDiv.innerHTML = `
<strong>Validation Errors (showing first 10 of ${errors.length}):</strong><br>
${firstTen.map(error => `${error}`).join('<br>')}
<br><strong>... and ${remaining} more errors</strong>
`;
} else {
errorDiv.innerHTML = `
<strong>Validation Errors:</strong><br>
${(Array.isArray(errors) ? errors : [errors]).map(error => `${error}`).join('<br>')}
`;
}
errorDiv.style.display = 'block';
}
function hideValidationErrors() {
document.getElementById('validationErrors').style.display = 'none';
}
// Filtering and sorting
function filterTools() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const typeFilter = document.getElementById('typeFilter').value;
const skillFilter = document.getElementById('skillFilter').value;
const domainFilter = document.getElementById('domainFilter').value;
const knowledgebaseFilter = document.getElementById('knowledgebaseFilter').value;
const validationFilter = document.getElementById('validationFilter').value;
filteredTools = toolsData.tools.filter(tool => {
const matchesSearch = !searchTerm ||
tool.name.toLowerCase().includes(searchTerm) ||
tool.description.toLowerCase().includes(searchTerm) ||
(tool.tags && tool.tags.some(tag => tag.toLowerCase().includes(searchTerm)));
const matchesType = !typeFilter || tool.type === typeFilter;
const matchesSkill = !skillFilter || tool.skillLevel === skillFilter;
const matchesDomain = !domainFilter || (tool.domains && tool.domains.includes(domainFilter));
const matchesKnowledgebase = !knowledgebaseFilter ||
(knowledgebaseFilter === 'true' && tool.knowledgebase) ||
(knowledgebaseFilter === 'false' && !tool.knowledgebase);
// Validation filter
let matchesValidation = true;
if (validationFilter) {
const hasValidationIssues = validateToolBasic(tool).length > 0;
if (validationFilter === 'valid') {
matchesValidation = !hasValidationIssues;
} else if (validationFilter === 'invalid') {
matchesValidation = hasValidationIssues;
}
}
return matchesSearch && matchesType && matchesSkill && matchesDomain && matchesKnowledgebase && matchesValidation;
});
sortTools();
}
function showInvalidToolsFilter(errorCount) {
// Set validation filter to show only invalid tools
document.getElementById('validationFilter').value = 'invalid';
filterTools();
// Show helpful message
showValidationErrors([
`⚠️ Import completed with ${errorCount} validation errors.`,
'',
'The filter has been set to show only tools with validation issues.',
'Please fix these errors before exporting to ensure compatibility with the web application.',
'',
'Click on each tool marked with ⚠️ to edit and resolve the issues.'
]);
updateStatus(`⚠️ Import completed - ${errorCount} tools need fixing`);
}
function clearAllFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('typeFilter').value = '';
document.getElementById('skillFilter').value = '';
document.getElementById('domainFilter').value = '';
document.getElementById('knowledgebaseFilter').value = '';
document.getElementById('validationFilter').value = '';
document.getElementById('sortBy').value = 'name';
filterTools();
updateStatus('All filters cleared');
}
function sortTools() {
const sortBy = document.getElementById('sortBy').value;
filteredTools.sort((a, b) => {
let aVal = a[sortBy] || '';
let bVal = b[sortBy] || '';
if (Array.isArray(aVal)) aVal = aVal.join(', ');
if (Array.isArray(bVal)) bVal = bVal.join(', ');
return aVal.toString().localeCompare(bVal.toString());
});
renderTools();
}
// Rendering
function renderTools() {
const container = document.getElementById('toolsList');
const countElement = document.getElementById('toolCount');
if (!filteredTools.length) {
countElement.textContent = 0;
container.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-secondary);">No tools found</div>';
return;
}
countElement.textContent = filteredTools.length;
if (filteredTools.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-secondary);">No tools found</div>';
return;
}
container.innerHTML = filteredTools.map((tool, index) => {
// Quick validation check for visual indicator
const hasValidationIssues = validateToolBasic(tool).length > 0;
return `
<div class="tool-item ${selectedTool === tool ? 'selected' : ''}" onclick="selectTool(${index})">
<div class="tool-header">
<div>
<div class="tool-name">
${hasValidationIssues ? '⚠️ ' : ''}
${tool.icon || '🔧'} ${tool.name || 'Unnamed Tool'}
${tool.knowledgebase ? ' 📖' : ''}
${tool.projectUrl ? ' 🌐' : ''}
</div>
<div class="tool-meta">
<span>${tool.type || 'Unknown'}</span>
<span>${tool.skillLevel || 'Unknown'}</span>
${tool.license ? `<span>${tool.license}</span>` : ''}
${tool.accessType ? `<span>${tool.accessType}</span>` : ''}
${hasValidationIssues ? '<span style="color: var(--danger-color);">⚠️ Issues</span>' : ''}
</div>
</div>
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); editTool(${index})">Edit</button>
</div>
<div class="tool-description">${tool.description || 'No description provided'}</div>
${tool.domains && tool.domains.length ? `
<div style="margin-top: 8px; font-size: 0.85em; color: var(--text-secondary);">
<strong>Domains:</strong> ${tool.domains.slice(0, 3).join(', ')}${tool.domains.length > 3 ? '...' : ''}
</div>
` : ''}
${tool.tags ? `
<div class="tags">
${tool.tags.slice(0, 5).map(tag => `<span class="tag">${tag}</span>`).join('')}
${tool.tags.length > 5 ? `<span class="tag">+${tool.tags.length - 5}</span>` : ''}
</div>
` : ''}
</div>
`}).join('');
}
function selectTool(index) {
selectedTool = filteredTools[index];
renderTools();
}
function clearSelection() {
selectedTool = null;
renderTools();
}
// Statistics
function showStats() {
const stats = calculateStats();
const container = document.getElementById('statsContainer');
container.innerHTML = Object.entries(stats).map(([key, value]) => `
<div class="stat-item">
<div class="stat-number">${value}</div>
<div>${key}</div>
</div>
`).join('');
container.style.display = container.style.display === 'none' ? 'grid' : 'none';
}
function calculateStats() {
const stats = {
'Total Tools': toolsData.tools.length,
'Software': toolsData.tools.filter(t => t.type === 'software').length,
'Methods': toolsData.tools.filter(t => t.type === 'method').length,
'Concepts': toolsData.tools.filter(t => t.type === 'concept').length,
'With KB': toolsData.tools.filter(t => t.knowledgebase).length,
'Open Source': toolsData.tools.filter(t => t.license && t.license !== 'Proprietary').length
};
return stats;
}
// Bulk operations
function toggleBulkPanel() {
const panel = document.getElementById('bulkPanel');
panel.classList.toggle('active');
}
function bulkRemoveTags() {
const tags = document.getElementById('bulkRemoveTagsInput').value
.split(',').map(t => t.trim()).filter(t => t);
if (tags.length === 0) {
updateStatus('Please enter tags to remove');
return;
}
if (!confirm(`Remove tags "${tags.join(', ')}" from all tools?`)) return;
let count = 0;
toolsData.tools.forEach(tool => {
if (tool.tags) {
const before = tool.tags.length;
tool.tags = tool.tags.filter(tag => !tags.includes(tag));
if (tool.tags.length < before) count++;
}
});
updateStatus(`Removed tags from ${count} tools`);
document.getElementById('bulkRemoveTagsInput').value = '';
renderTools();
}
function bulkRenameTags() {
const fromTag = document.getElementById('bulkRenameFrom').value.trim();
const toTag = document.getElementById('bulkRenameTo').value.trim();
if (!fromTag || !toTag) {
updateStatus('Please enter both "from" and "to" tags');
return;
}
if (!confirm(`Rename tag "${fromTag}" to "${toTag}" in all tools?`)) return;
let count = 0;
toolsData.tools.forEach(tool => {
if (tool.tags && tool.tags.includes(fromTag)) {
const index = tool.tags.indexOf(fromTag);
tool.tags[index] = toTag;
count++;
}
});
updateStatus(`Renamed tag in ${count} tools`);
document.getElementById('bulkRenameFrom').value = '';
document.getElementById('bulkRenameTo').value = '';
renderTools();
}
function bulkAssignRelatedConcepts() {
const concepts = document.getElementById('bulkConceptsInput').value
.split(',').map(t => t.trim()).filter(t => t);
if (concepts.length === 0) {
updateStatus('Please enter concepts to assign');
return;
}
if (!confirm(`Assign concepts "${concepts.join(', ')}" to all tools?`)) return;
const selectedTools = getSelectedTools();
selectedTools.forEach(tool => {
if (!tool.related_concepts) tool.related_concepts = [];
concepts.forEach(concept => {
if (!tool.related_concepts.includes(concept)) {
tool.related_concepts.push(concept);
}
});
});
updateStatus(`Assigned concepts to ${selectedTools.length} tools`);
document.getElementById('bulkConceptsInput').value = '';
renderTools();
}
function bulkAssignRelatedSoftware() {
const software = document.getElementById('bulkSoftwareInput').value
.split(',').map(t => t.trim()).filter(t => t);
if (software.length === 0) {
updateStatus('Please enter software to assign');
return;
}
if (!confirm(`Assign software "${software.join(', ')}" to all tools?`)) return;
const selectedTools = getSelectedTools();
selectedTools.forEach(tool => {
if (!tool.related_software) tool.related_software = [];
software.forEach(sw => {
if (!tool.related_software.includes(sw)) {
tool.related_software.push(sw);
}
});
});
updateStatus(`Assigned software to ${selectedTools.length} tools`);
document.getElementById('bulkSoftwareInput').value = '';
renderTools();
}
function getSelectedTools() {
// For now, return all tools. Could be enhanced to support multi-selection
return toolsData.tools;
}
// YAML operations
function importYAML(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const yamlContent = e.target.result;
const parsed = jsyaml.load(yamlContent);
if (!parsed || !parsed.tools || !Array.isArray(parsed.tools)) {
alert('Invalid YAML format: missing or invalid tools array');
return;
}
// Show import summary and confirm
const importSummary = `
Import Summary:
${parsed.tools.length} tools
${parsed.tools.filter(t => t.type === 'software').length} software
${parsed.tools.filter(t => t.type === 'method').length} methods
${parsed.tools.filter(t => t.type === 'concept').length} concepts
${parsed.tools.filter(t => t.knowledgebase).length} with knowledgebase
This will replace all current data. Continue?`;
if (!confirm(importSummary)) {
return;
}
// Import the data
toolsData = parsed;
// Ensure required structure exists
if (!toolsData.domains) toolsData.domains = [];
if (!toolsData.phases) toolsData.phases = [];
if (!toolsData['domain-agnostic-software']) toolsData['domain-agnostic-software'] = [];
if (!toolsData.scenarios) toolsData.scenarios = [];
if (!toolsData.skill_levels) toolsData.skill_levels = {};
populateFilters();
// Check for validation errors after import
updateStatus('Validating imported data...');
const validationErrors = [];
toolsData.tools.forEach((tool, index) => {
const toolErrors = validateTool(tool, index);
validationErrors.push(...toolErrors);
});
// Check for duplicate names
const nameMap = new Map();
toolsData.tools.forEach((tool, index) => {
if (tool.name) {
if (nameMap.has(tool.name)) {
validationErrors.push(`Duplicate name "${tool.name}" found at indices ${nameMap.get(tool.name)} and ${index}`);
} else {
nameMap.set(tool.name, index);
}
}
});
if (validationErrors.length > 0) {
// Show tools with validation issues
showInvalidToolsFilter(validationErrors.length);
} else {
// Everything is valid
hideValidationErrors();
renderTools();
updateStatus(`✅ Successfully imported ${toolsData.tools.length} tools - all valid`);
}
} catch (error) {
alert('Error parsing YAML: ' + error.message);
updateStatus('❌ YAML parsing failed');
}
};
reader.readAsText(file);
}
function exportYAML() {
// Check for validation errors before export
const allErrors = [];
toolsData.tools.forEach((tool, index) => {
const toolErrors = validateTool(tool, index);
allErrors.push(...toolErrors);
});
if (allErrors.length > 0) {
const confirmExport = confirm(`⚠️ Warning: Found ${allErrors.length} validation errors in the database.\n\nExporting this data may cause issues when importing into the web application.\n\nWould you like to:\n• Click OK to export anyway\n• Click Cancel to fix errors first (recommended)`);
if (!confirmExport) {
// Set filter to show invalid tools and highlight the issues
showInvalidToolsFilter(allErrors.length);
return;
}
}
try {
// Process tools to ensure proper description formatting
const processedData = {
...toolsData,
tools: toolsData.tools.map(tool => {
const processedTool = { ...tool };
// Format description with folded style for multiline
if (processedTool.description && processedTool.description.length > 60) {
// Keep the description as is, jsyaml will handle the formatting
}
// Clean up empty arrays and null values
Object.keys(processedTool).forEach(key => {
if (Array.isArray(processedTool[key]) && processedTool[key].length === 0) {
delete processedTool[key];
}
if (processedTool[key] === null || processedTool[key] === undefined || processedTool[key] === '') {
delete processedTool[key];
}
});
return processedTool;
})
};
const yamlContent = jsyaml.dump(processedData, {
indent: 2,
lineWidth: 80,
noRefs: true,
sortKeys: false,
styles: {
'!!str': 'fold' // Use folded style for long strings
},
replacer: (key, value) => {
// Handle description field specially
if (key === 'description' && typeof value === 'string' && value.length > 60) {
return value;
}
return value;
}
});
// Post-process to ensure descriptions use >- format
const lines = yamlContent.split('\n');
const processedLines = [];
let inDescription = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim().startsWith('description:')) {
if (line.includes('"') || line.includes("'")) {
// Replace quoted description with folded style
const indent = line.match(/^(\s*)/)[1];
processedLines.push(`${indent}description: >-`);
// Extract the description content
let descContent = line.substring(line.indexOf(':') + 1).trim();
if (descContent.startsWith('"') || descContent.startsWith("'")) {
descContent = descContent.slice(1, -1);
}
// Split and reformat the description
const words = descContent.split(' ');
let currentLine = '';
const descIndent = indent + ' ';
words.forEach(word => {
if (currentLine.length + word.length > 76) {
if (currentLine) {
processedLines.push(descIndent + currentLine.trim());
}
currentLine = word + ' ';
} else {
currentLine += word + ' ';
}
});
if (currentLine.trim()) {
processedLines.push(descIndent + currentLine.trim());
}
} else {
processedLines.push(line);
}
} else {
processedLines.push(line);
}
}
const finalYaml = processedLines.join('\n');
const blob = new Blob([finalYaml], { type: 'text/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'tools.yaml';
a.click();
URL.revokeObjectURL(url);
updateStatus(allErrors.length > 0 ?
`⚠️ YAML exported with ${allErrors.length} validation errors` :
'✅ YAML exported successfully'
);
} catch (error) {
alert('Error exporting YAML: ' + error.message);
}
}
// Initialize on load
document.addEventListener('DOMContentLoaded', init);
// Keyboard shortcuts
document.addEventListener('keydown', function(event) {
// Ctrl/Cmd + S to save
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
saveTool();
}
// Ctrl/Cmd + N to add new tool
if ((event.ctrlKey || event.metaKey) && event.key === 'n') {
event.preventDefault();
addNewTool();
}
// Ctrl/Cmd + E to export
if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
event.preventDefault();
exportYAML();
}
// Alt + V to validate all (changed from Ctrl+V to avoid paste conflict)
if (event.altKey && event.key === 'v') {
event.preventDefault();
validateAllTools();
}
// Escape to clear selection/form
if (event.key === 'Escape') {
clearSelection();
clearForm();
}
// Ctrl/Cmd + F to focus search
if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
event.preventDefault();
document.getElementById('searchInput').focus();
}
});
// Hide autocomplete when clicking outside
document.addEventListener('click', function(event) {
if (!event.target.closest('.autocomplete-container')) {
document.querySelectorAll('.autocomplete-dropdown').forEach(dropdown => {
dropdown.style.display = 'none';
});
}
});
</script>
</body>
</html>