SchemaManager.html•41.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Schema Manager</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.2/ace.js"></script>
<style>
/* --- Base Styles --- */
:root {
--primary: #2563eb;
--primary-dark: #1d4ed8;
--primary-light: #60a5fa;
--success: #22c55e;
--danger: #ef4444;
--warning: #f59e0b;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.6;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
color: var(--gray-800);
background-color: var(--gray-50);
}
/* --- Typography --- */
h1 {
margin-bottom: 2rem;
color: var(--gray-900);
font-size: 2rem;
font-weight: 600;
}
h2 {
color: var(--gray-800);
font-size: 1.5rem;
margin-bottom: 1rem;
}
h3 {
color: var(--gray-700);
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
/* --- Layout Components --- */
.container {
background: white;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 2rem;
margin-bottom: 2rem;
}
.folder-section {
margin-bottom: 2rem;
padding: 1rem;
background: white;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 1rem;
}
.folder-path {
flex: 1;
font-family: monospace;
padding: 0.5rem;
background: var(--gray-50);
border: 1px solid var(--gray-300);
border-radius: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* --- Tabs --- */
.tabs {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
border-bottom: 2px solid var(--gray-200);
padding-bottom: 0.5rem;
}
.tab {
padding: 0.5rem 1rem;
border: none;
background: none;
cursor: pointer;
font-size: 1rem;
color: var(--gray-600);
border-bottom: 2px solid transparent;
margin-bottom: -0.5rem;
transition: all 0.2s ease;
}
.tab:hover {
color: var(--primary);
}
.tab.active {
color: var(--primary);
border-bottom: 2px solid var(--primary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* --- Forms --- */
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--gray-700);
}
input[type="text"],
select,
textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--gray-300);
border-radius: 0.25rem;
font-size: 1rem;
transition: border-color 0.2s ease;
}
input[type="text"]:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
/* --- Properties Section --- */
.properties-container {
border: 1px solid var(--gray-300);
padding: 1rem;
border-radius: 0.25rem;
margin-bottom: 1rem;
background: white;
}
.property {
border: 1px solid var(--gray-200);
padding: 1rem;
margin-bottom: 1rem;
border-radius: 0.25rem;
background: var(--gray-50);
display: flex;
flex-direction: column;
gap: 1rem;
}
.property-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
/* --- Buttons --- */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 1rem;
background: var(--primary);
color: white;
transition: all 0.2s ease;
}
.btn:hover {
background: var(--primary-dark);
}
.btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.3);
}
.btn-danger {
background: var(--danger);
}
.btn-danger:hover {
background: #dc2626;
}
/* --- Editor --- */
.editor-container {
height: 500px;
border: 1px solid var(--gray-300);
border-radius: 0.25rem;
margin-bottom: 1rem;
overflow: hidden;
}
#jsonEditor {
height: 100%;
font-size: 14px;
}
/* --- Checkboxes --- */
.checkbox-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: normal;
cursor: pointer;
}
/* --- Toast Notifications --- */
#toastContainer {
position: fixed;
top: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 1000;
}
.toast {
min-width: 250px;
padding: 1rem 1.5rem;
border-radius: 0.25rem;
color: white;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
opacity: 0;
transform: translateX(100%);
animation: slideIn 0.5s forwards, fadeOut 0.5s forwards 2.5s;
}
.toast-success {
background-color: var(--success);
}
.toast-error {
background-color: var(--danger);
}
@keyframes slideIn {
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeOut {
to {
opacity: 0;
transform: translateX(100%);
}
}
/* --- Schema Select --- */
#loadSchemaSelect {
margin-bottom: 1rem;
width: 100%;
padding: 0.5rem;
border: 1px solid var(--gray-300);
border-radius: 0.25rem;
font-size: 1rem;
}
/* --- Responsive Design --- */
@media (max-width: 768px) {
body {
padding: 1rem;
}
.folder-section {
flex-direction: column;
align-items: stretch;
}
.property-header {
flex-direction: column;
gap: 0.5rem;
}
.property {
grid-template-columns: 1fr;
}
}
/* --- New Styles --- */
/* Add new styles for locked fields */
.locked-field {
background-color: var(--gray-100);
cursor: not-allowed;
}
.locked-field:disabled {
color: var(--gray-700);
}
/* Add style for the additional properties checkbox */
.global-settings {
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--gray-100);
border-radius: 0.25rem;
}
/* --- Enum Fields --- */
.enum-fields {
display: none;
}
/* --- Updated Property Layout --- */
.property-row {
display: flex;
gap: 1rem;
align-items: center;
}
.property-row.large {
flex-wrap: wrap;
}
.property-row.large .form-group {
flex: 2;
}
.property-row.small {
flex: 1;
}
.property-row .form-group {
flex: 1;
}
.conditional-input {
flex: 2;
}
/* Adjust Description field to be twice as wide */
.description-input {
flex: 2;
}
</style>
</head>
<body>
<h1>Schema Manager</h1>
<div class="folder-section">
<button class="btn" id="selectDirectoryBtn">Select Schemas Folder</button>
<div class="folder-path" id="folderPath">No folder selected</div>
</div>
<!-- Toast Notifications Container -->
<div id="toastContainer"></div>
<div class="tabs">
<button class="tab active" data-tab="create">Create Schema</button>
<button class="tab" data-tab="viewRaw">View/Edit Schema (raw)</button>
</div>
<div id="createSchema" class="tab-content active">
<div class="form-group">
<label for="schemaName">Schema Name:</label>
<input type="text" id="schemaName" placeholder="Identifier for the schema and node type within the memory">
</div>
<div class="form-group">
<label for="schemaDescription">Schema Description:</label>
<input type="text" id="schemaDescription"
placeholder="Description for the add_<name> tool, providing context for the AI.">
</div>
<div class="global-settings">
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="allowAdditionalProps" checked>
Allow Additional Properties
</label>
</div>
</div>
<div class="form-group">
<label>Properties:</label>
<div id="propertiesContainer" class="properties-container">
<!-- Properties will be added here -->
</div>
<button class="btn" id="addPropertyBtn">Add Property</button>
</div>
<div style="display: flex; gap: 1rem;">
<button class="btn" id="viewRawSchemaBtn">View in Raw Format</button>
<button class="btn btn-danger" id="resetFormBtn">Reset</button>
</div>
</div>
<div id="viewRawSchema" class="tab-content">
<select id="loadSchemaSelect">
<option value="">Select a schema to load...</option>
</select>
<div class="editor-container">
<div id="jsonEditor"></div>
</div>
<div class="button-group" style="display: flex; gap: 1rem;">
<button class="btn" id="saveSchemaBtn">Save Schema</button>
<button class="btn" id="editSchemaBtn">Edit</button>
<button class="btn btn-danger" id="deleteSchemaBtn">Delete Schema</button>
</div>
</div>
<script>
// --- Constants and State ---
const DB_NAME = 'SchemaManagerDB';
const STORE_NAME = 'directory';
let db = null;
let dirHandle = null;
let currentSchemaHandle = null;
// --- Ace Editor Initialization ---
const editor = ace.edit("jsonEditor");
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/json");
editor.setOptions({
fontSize: "14px",
showPrintMargin: false,
showGutter: true,
highlightActiveLine: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true
});
// --- Utility Functions ---
function showToast(message, type = 'success', duration = 3000) {
const toastContainer = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.classList.add('toast', type === 'success' ? 'toast-success' : 'toast-error');
toast.textContent = message;
toastContainer.appendChild(toast);
setTimeout(() => toast.remove(), duration + 1000);
}
// --- IndexedDB Operations ---
const idb = {
initDB: async () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};
request.onsuccess = (event) => {
db = event.target.result;
resolve();
};
request.onerror = (event) => reject(event.target.error);
});
},
saveDirectoryHandle: async (handle) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.put(handle, 'schemasDirectory');
request.onsuccess = () => resolve();
request.onerror = (event) => reject(event.target.error);
});
},
getDirectoryHandleFromDB: async () => {
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get('schemasDirectory');
request.onsuccess = async (event) => {
const handle = event.target.result;
if (handle) {
const permission = await handle.queryPermission({mode: 'readwrite'});
if (permission === 'granted') {
resolve(handle);
return;
}
}
resolve(null);
};
request.onerror = (event) => reject(event.target.error);
});
},
removeDirectoryHandleFromDB: async () => {
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.delete('schemasDirectory');
request.onsuccess = () => resolve();
request.onerror = (event) => reject(event.target.error);
});
}
};
// --- File System Operations ---
const fileSystem = {
selectDirectory: async () => {
try {
let options = {id: 'schemas-directory', mode: 'readwrite'};
const storedHandle = await idb.getDirectoryHandleFromDB();
options.startIn = storedHandle || 'documents';
dirHandle = await window.showDirectoryPicker(options);
if (storedHandle && dirHandle.name !== storedHandle.name) {
await idb.removeDirectoryHandleFromDB();
}
document.getElementById('folderPath').textContent = dirHandle.name;
await ui.loadSchemas();
showToast('Folder selected successfully!', 'success');
await idb.saveDirectoryHandle(dirHandle);
} catch (error) {
if (error.name !== 'AbortError') {
showToast('Error selecting folder: ' + error.message, 'error');
}
}
},
loadSchemaFile: async (filename) => {
try {
const fileHandle = await dirHandle.getFileHandle(filename);
currentSchemaHandle = fileHandle;
const file = await fileHandle.getFile();
const content = await file.text();
const formattedContent = JSON.stringify(JSON.parse(content), null, 2);
editor.setValue(formattedContent);
editor.clearSelection();
showToast('Schema loaded successfully!', 'success');
} catch (error) {
showToast('Error loading schema: ' + error.message, 'error');
}
},
saveSchema: async () => {
if (!dirHandle) {
showToast('Please select a schemas folder first', 'error');
return;
}
try {
const schemaContent = editor.getValue();
const schema = JSON.parse(schemaContent);
if (!schema.name) throw new Error('Schema must have a name property');
if (!schema.properties) throw new Error('Schema must have properties defined');
// Check for schema name uniqueness before saving
const isSchemaNameUnique = await schemaValidator.isSchemaNameUnique(schema.name);
if (!isSchemaNameUnique) {
showToast(`A schema with the name "${schema.name}" already exists. Please choose a unique name.`, 'error');
return;
}
const fileName = `${schema.name.replace('add_', '')}.schema.json`;
let fileHandle;
try {
fileHandle = await dirHandle.getFileHandle(fileName);
if (!confirm(`Schema ${fileName} already exists. Do you want to overwrite it?`)) return;
} catch {
fileHandle = await dirHandle.getFileHandle(fileName, {create: true});
}
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(schema, null, 2));
await writable.close();
currentSchemaHandle = fileHandle;
await ui.loadSchemas();
showToast('Schema saved successfully!', 'success');
} catch (error) {
showToast('Error saving schema: ' + error.message, 'error');
}
},
deleteSchema: async () => {
if (!currentSchemaHandle) {
showToast('Please select a schema to delete', 'error');
return;
}
if (!confirm(`Are you sure you want to delete ${currentSchemaHandle.name}? This action cannot be undone.`)) return;
try {
await dirHandle.removeEntry(currentSchemaHandle.name);
editor.setValue('');
currentSchemaHandle = null;
await ui.loadSchemas();
showToast('Schema deleted successfully!', 'success');
} catch (error) {
showToast('Error deleting schema: ' + error.message, 'error');
}
}
};
// --- Schema Generation and Validation ---
const schemaGenerator = {
generateSchema: () => {
const name = document.getElementById('schemaName').value;
const description = document.getElementById('schemaDescription').value;
const allowAdditionalProps = document.getElementById('allowAdditionalProps').checked;
if (!name) {
showToast('Schema name is required', 'error');
return;
}
// Real-time validation for schema name uniqueness
const isSchemaNameUnique = schemaValidator.isSchemaNameUnique(name, dirHandle);
if (!isSchemaNameUnique) {
showToast('Schema name must be unique', 'error');
return;
}
const properties = {};
document.querySelectorAll('.property').forEach(prop => {
if (prop.id === 'property-name-property') return;
const propName = prop.querySelector('.property-name').value;
if (!propName) return;
const propDesc = prop.querySelector('.property-description').value;
const propType = prop.querySelector('.property-type').value;
const required = prop.querySelector('.property-required').checked;
const isEnum = prop.querySelector('.property-enum').checked;
const enumValues = isEnum ? prop.querySelector('.enum-values-input')?.value
.split(',')
.map(v => v.trim())
.filter(v => v) : null;
const isRelationship = prop.querySelector('.property-relationship').checked;
const edgeType = isRelationship ? prop.querySelector('.edge-type').value : null;
const property = {
type: propType,
description: propDesc,
required: required
};
if (isEnum && enumValues) {
property.enum = enumValues;
}
if (isRelationship) {
property.relationship = {
edgeType: edgeType,
description: propDesc
};
}
properties[propName] = property;
});
const nameProp = document.querySelector('#property-name-property');
if (nameProp) {
const propName = nameProp.querySelector('.property-name').value;
const propDesc = nameProp.querySelector('.property-description').value;
const propType = nameProp.querySelector('.property-type').value;
const required = nameProp.querySelector('.property-required').checked;
properties[propName] = {
type: propType,
description: propDesc,
required: required
};
}
const schema = {
name: `add_${name}`,
description: description,
properties: properties,
additionalProperties: allowAdditionalProps
};
editor.setValue(JSON.stringify(schema, null, 2));
ui.switchTab('viewRaw');
}
};
const schemaValidator = {
isSchemaNameUnique: async (schemaName) => {
if (!dirHandle) return true;
const fullSchemaName = `add_${schemaName}.schema.json`;
const simplifiedSchemaName = `${schemaName}.schema.json`
try {
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file' && (entry.name === fullSchemaName || entry.name === simplifiedSchemaName)) {
return false;
}
}
return true; // No matching file found
} catch (error) {
console.error('Error checking for schema name uniqueness:', error);
return false; // Assume not unique in case of error
}
}
};
// --- UI Management ---
const ui = {
switchTab: (tabName) => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.querySelector(`.tab[data-tab="${tabName}"]`).classList.add('active');
document.getElementById(`${tabName}Schema`).classList.add('active');
},
loadSchemas: async () => {
if (!dirHandle) return;
const select = document.getElementById('loadSchemaSelect');
select.innerHTML = '<option value="">Select a schema to load...</option>';
try {
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file' && entry.name.endsWith('.schema.json')) {
const option = document.createElement('option');
option.value = entry.name;
option.textContent = entry.name;
select.appendChild(option);
}
}
} catch (error) {
showToast('Error loading schemas: ' + error.message, 'error');
}
},
resetForm: () => {
if (!confirm('Are you sure you want to reset the form? All unsaved changes will be lost.')) return;
document.getElementById('schemaName').value = '';
document.getElementById('schemaDescription').value = '';
document.getElementById('allowAdditionalProps').checked = true;
const propertiesContainer = document.getElementById('propertiesContainer');
propertiesContainer.innerHTML = '';
propertiesContainer.appendChild(ui.createNamePropertyElement());
ui.addProperty();
showToast('Form has been reset to default state.', 'success');
},
addProperty: () => {
const container = document.getElementById('propertiesContainer');
const newProperty = ui.createPropertyElement();
container.appendChild(newProperty);
},
removeProperty: (button) => {
const property = button.closest('.property');
property.remove();
},
toggleEnumFields: (checkbox) => {
const property = checkbox.closest('.property');
const enumInput = property.querySelector('.enum-values-input');
enumInput.style.display = checkbox.checked ? 'block' : 'none';
},
toggleRelationshipFields: (checkbox) => {
const property = checkbox.closest('.property');
const relationshipInput = property.querySelector('.edge-type');
relationshipInput.style.display = checkbox.checked ? 'block' : 'none';
},
createPropertyElement: () => {
const propertyTemplate = `
<div class="property">
<div class="property-header">
<h3>Property</h3>
<button class="btn btn-danger" onclick="ui.removeProperty(this)">Remove</button>
</div>
<div class="property-row large">
<div class="form-group">
<label>Name:</label>
<input type="text" class="property-name" placeholder="Property" oninput="schemaValidator.validatePropertyName(this)">
<div class="validation-message"></div>
</div>
<div class="form-group description-input">
<label>Description:</label>
<input type="text" class="property-description" placeholder="Property description">
</div>
<div class="form-group">
<label>Type:</label>
<select class="property-type" onchange="ui.handlePropertyTypeChange(this)">
<option value="string">String</option>
<option value="array">Array</option>
</select>
</div>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" class="property-required">
Required
</label>
</div>
</div>
<div class="property-row small">
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" class="property-enum" onchange="ui.toggleEnumFields(this)">
Is Enum
</label>
<input type="text" class="enum-values-input conditional-input" placeholder="e.g., active, inactive, pending" style="display: none;">
</div>
</div>
<div class="property-row small">
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" class="property-relationship" onchange="ui.toggleRelationshipFields(this)">
Is Relationship
</label>
<input type="text" class="edge-type conditional-input" placeholder="e.g., owns, belongs_to" style="display: none;">
</div>
</div>
</div>
`;
const template = document.createElement('template');
template.innerHTML = propertyTemplate.trim();
return template.content.firstChild;
},
createNamePropertyElement: () => {
const namePropertyTemplate = `
<div class="property" id="property-name-property">
<div class="property-header">
<h3>Name Property</h3>
</div>
<div class="property-row large">
<div class="form-group">
<label>Name:</label>
<input type="text" class="property-name" value="name" disabled>
</div>
<div class="form-group description-input">
<label>Description:</label>
<input type="text" class="property-description" placeholder="Property description">
</div>
<div class="form-group">
<label>Type:</label>
<select class="property-type" disabled>
<option value="string" selected>String</option>
</select>
</div>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" class="property-required" checked disabled>
Required
</label>
</div>
</div>
<div class="property-row small">
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" class="property-enum" disabled>
Is Enum
</label>
<input type="text" class="enum-values-input conditional-input" placeholder="e.g., active, inactive, pending" style="display: none;" disabled>
</div>
</div>
<div class="property-row small">
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" class="property-relationship" disabled>
Is Relationship
</label>
<input type="text" class="edge-type conditional-input" placeholder="e.g., owns, belongs_to" style="display: none;" disabled>
</div>
</div>
</div>
`;
const template = document.createElement('template');
template.innerHTML = namePropertyTemplate.trim();
return template.content.firstChild;
},
handlePropertyTypeChange: (select) => {
// No change here
},
loadSchemaIntoCreateTab: async (filename) => {
try {
const fileHandle = await dirHandle.getFileHandle(filename);
const file = await fileHandle.getFile();
const content = await file.text();
const schema = JSON.parse(content);
// Check for parsing errors
if (typeof schema !== 'object' || schema === null) {
throw new Error('Invalid schema format. The schema should be a JSON object.');
}
// Populate schemaName and schemaDescription
document.getElementById('schemaName').value = schema.name.replace(/^add_/, '') || '';
document.getElementById('schemaDescription').value = schema.description || '';
// Clear existing properties except the Name Property
const propertiesContainer = document.getElementById('propertiesContainer');
propertiesContainer.innerHTML = '';
propertiesContainer.appendChild(ui.createNamePropertyElement());
// Populate the Description of the Name Property
const namePropertyDescriptionInput = document.querySelector('#property-name-property .property-description');
if (namePropertyDescriptionInput && schema.properties && schema.properties.name) {
namePropertyDescriptionInput.value = schema.properties.name.description || '';
}
// Add properties from schema
if (schema.properties) {
for (const [propName, propDetails] of Object.entries(schema.properties)) {
if (propName === 'name') continue; // Skip the Name Property
const propertyElement = ui.generatePropertyHTML(propName, propDetails);
propertiesContainer.appendChild(propertyElement);
}
}
// Switch to the Create Schema tab
ui.switchTab('create');
showToast('Schema loaded into Create Schema tab for editing.', 'success');
} catch (error) {
showToast('Error editing schema: ' + error.message, 'error');
}
},
generatePropertyHTML: (propName, propDetails) => {
const propertyElement = ui.createPropertyElement();
// Set values for the dynamically created property
const nameInput = propertyElement.querySelector('.property-name');
const descriptionInput = propertyElement.querySelector('.property-description');
const typeSelect = propertyElement.querySelector('.property-type');
const requiredCheckbox = propertyElement.querySelector('.property-required');
const enumCheckbox = propertyElement.querySelector('.property-enum');
const enumInput = propertyElement.querySelector('.enum-values-input');
const relationshipCheckbox = propertyElement.querySelector('.property-relationship');
const edgeTypeInput = propertyElement.querySelector('.edge-type');
nameInput.value = propName;
descriptionInput.value = propDetails.description || '';
typeSelect.value = propDetails.type || 'string';
requiredCheckbox.checked = propDetails.required || false;
if (propDetails.enum && Array.isArray(propDetails.enum)) {
enumCheckbox.checked = true;
enumInput.value = propDetails.enum.join(', ');
enumInput.style.display = 'block';
}
if (propDetails.relationship && propDetails.relationship.edgeType) {
relationshipCheckbox.checked = true;
edgeTypeInput.value = propDetails.relationship.edgeType;
edgeTypeInput.style.display = 'block';
}
return propertyElement;
},
editSchema: () => {
const select = document.getElementById('loadSchemaSelect');
const selectedFile = select.value;
if (!selectedFile) {
showToast('Please select a schema to edit.', 'error');
return;
}
ui.loadSchemaIntoCreateTab(selectedFile);
}
};
// --- Event Handlers ---
function setupEventListeners() {
document.getElementById('selectDirectoryBtn').addEventListener('click', fileSystem.selectDirectory);
document.getElementById('addPropertyBtn').addEventListener('click', ui.addProperty);
document.getElementById('viewRawSchemaBtn').addEventListener('click', schemaGenerator.generateSchema);
document.getElementById('resetFormBtn').addEventListener('click', ui.resetForm);
document.getElementById('saveSchemaBtn').addEventListener('click', fileSystem.saveSchema);
document.getElementById('deleteSchemaBtn').addEventListener('click', fileSystem.deleteSchema);
document.getElementById('editSchemaBtn').addEventListener('click', ui.editSchema);
document.getElementById('schemaName').addEventListener('input', async (event) => {
const schemaName = event.target.value;
const isUnique = await schemaValidator.isSchemaNameUnique(schemaName);
if (!isUnique) {
showToast('Schema name must be unique', 'error');
}
});
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => ui.switchTab(tab.dataset.tab));
});
document.getElementById('loadSchemaSelect').addEventListener('change', (e) => {
if (e.target.value) {
fileSystem.loadSchemaFile(e.target.value);
}
});
}
// --- Initialization ---
async function initialize() {
try {
await idb.initDB();
} catch (error) {
showToast('Error initializing database: ' + error.message, 'error');
return;
}
if (!('showDirectoryPicker' in window)) {
showToast('Your browser does not support the File System Access API. Please use a modern browser like Chrome or Edge.', 'error', 0);
return;
}
try {
const storedHandle = await idb.getDirectoryHandleFromDB();
if (storedHandle) {
const permission = await storedHandle.queryPermission({mode: 'readwrite'});
if (permission === 'granted') {
dirHandle = storedHandle;
document.getElementById('folderPath').textContent = dirHandle.name;
await ui.loadSchemas();
showToast('Loaded previously selected folder.', 'success');
} else {
const requestPermission = await storedHandle.requestPermission({mode: 'readwrite'});
if (requestPermission === 'granted') {
dirHandle = storedHandle;
document.getElementById('folderPath').textContent = dirHandle.name;
await ui.loadSchemas();
showToast('Loaded previously selected folder.', 'success');
} else {
await idb.removeDirectoryHandleFromDB();
showToast('Permission to access the previously selected folder was denied.', 'error');
}
}
}
} catch (error) {
showToast('Error accessing stored folder: ' + error.message, 'error');
}
const container = document.getElementById('propertiesContainer');
container.innerHTML = '';
container.appendChild(ui.createNamePropertyElement());
ui.addProperty();
setupEventListeners();
}
// --- Start the application ---
initialize();
</script>
</body>
</html>