1907 lines
78 KiB
HTML
1907 lines
78 KiB
HTML
<!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> |