PoC: flat and advanced mode

This commit is contained in:
2025-10-10 18:13:54 +02:00
commit 2b25bc17b3
14 changed files with 2040 additions and 0 deletions

536
static/css/style.css Normal file
View File

@@ -0,0 +1,536 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3a3a3a;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--accent: #4a9eff;
--accent-hover: #3a8eef;
--border: #444;
--success: #4caf50;
--danger: #f44336;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 40px;
}
header h1 {
font-size: 2rem;
font-weight: 300;
letter-spacing: -0.5px;
}
.input-section {
background: var(--bg-secondary);
padding: 30px;
border-radius: 8px;
margin-bottom: 30px;
}
.header-input {
margin-bottom: 30px;
}
.header-input label {
display: block;
margin-bottom: 8px;
font-size: 0.9rem;
color: var(--text-secondary);
}
.header-input input {
width: 100%;
padding: 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-primary);
font-size: 1rem;
transition: border-color 0.2s;
}
.header-input input:focus {
outline: none;
border-color: var(--accent);
}
.arguments-section h2 {
font-size: 1.3rem;
font-weight: 400;
margin-bottom: 15px;
}
.argument-header {
display: grid;
grid-template-columns: 2fr 2fr 3fr 60px;
gap: 10px;
padding: 10px 0;
border-bottom: 2px solid var(--border);
margin-bottom: 15px;
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 500;
}
.argument-row {
display: grid;
grid-template-columns: 2fr 2fr 3fr 60px;
gap: 10px;
margin-bottom: 15px;
align-items: start;
}
.argument-row input {
padding: 10px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-primary);
font-size: 0.95rem;
transition: border-color 0.2s;
}
.argument-row input:focus {
outline: none;
border-color: var(--accent);
}
.argument-row input::placeholder {
color: #666;
}
.remove-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 10px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-size: 1.2rem;
}
.remove-btn:hover {
background: var(--danger);
border-color: var(--danger);
color: white;
}
.generate-btn {
width: 100%;
padding: 15px;
background: var(--accent);
color: white;
border: none;
border-radius: 4px;
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
margin-top: 20px;
}
.generate-btn:hover {
background: var(--accent-hover);
}
.output-section {
background: var(--bg-secondary);
padding: 30px;
border-radius: 8px;
margin-bottom: 30px;
}
.output-section h2 {
font-size: 1.3rem;
font-weight: 400;
margin-bottom: 15px;
}
.code-container {
position: relative;
background: #1f1f1f;
border-radius: 4px;
padding: 20px;
}
.copy-btn {
position: absolute;
top: 10px;
right: 10px;
padding: 8px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-primary);
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.copy-btn:hover {
background: var(--accent);
border-color: var(--accent);
}
.copy-btn.copied {
background: var(--success);
border-color: var(--success);
}
.code-container pre {
margin: 0;
overflow-x: auto;
}
.code-container code {
color: rgb(131, 177, 131);
font-family: 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.5;
}
.help-section {
background: var(--bg-secondary);
padding: 30px;
border-radius: 8px;
}
.help-section h2 {
font-size: 1.3rem;
font-weight: 400;
margin-bottom: 15px;
}
.help-preview {
background: #000;
border-radius: 4px;
padding: 20px;
min-height: 150px;
}
.help-preview pre {
color: #0f0;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
}
/* Mode Toggle */
.mode-toggle {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 30px;
}
.mode-btn {
padding: 10px 30px;
background: var(--bg-tertiary);
border: 2px solid var(--border);
color: var(--text-primary);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.mode-btn.active {
background: var(--accent);
border-color: var(--accent);
}
/* Global Section */
.global-section {
background: var(--bg-secondary);
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 3px solid var(--success);
}
.section-title {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 15px;
}
/* Subcommands */
.subcommands-container {
margin-top: 20px;
}
.subcommand-section {
background: var(--bg-tertiary);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border-left: 3px solid var(--accent);
}
.subcommand-section.collapsed .subcommand-body {
display: none;
}
.subcommand-header {
display: grid;
grid-template-columns: 2fr 3fr auto auto;
gap: 10px;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--border);
}
.subcommand-name-input,
.subcommand-desc-input {
padding: 10px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-primary);
font-size: 1rem;
}
.subcommand-name-input {
font-weight: 600;
font-family: 'Courier New', monospace;
}
.toggle-collapse-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.toggle-collapse-btn::before {
content: '▼';
}
.subcommand-section.collapsed .toggle-collapse-btn::before {
content: '▶';
}
.add-subcommand-btn {
width: 100%;
padding: 15px;
background: transparent;
border: 2px dashed var(--border);
color: var(--text-secondary);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.add-subcommand-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
/* Mode-specific visibility */
.flat-mode .subcommands-container {
display: none;
}
.flat-mode .global-section {
border-left-color: var(--accent);
}
.mode-toggle {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 30px;
}
.mode-btn {
padding: 10px 30px;
background: var(--bg-tertiary);
border: 2px solid var(--border);
color: var(--text-primary);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.mode-btn.active {
background: var(--accent);
border-color: var(--accent);
}
.global-section {
background: var(--bg-secondary);
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 3px solid var(--success);
}
.section-title {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 15px;
color: var(--text-primary);
}
.subcommands-container {
margin-top: 20px;
}
.subcommand-section {
background: var(--bg-tertiary);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border-left: 3px solid var(--accent);
}
.subcommand-section.collapsed .subcommand-body {
display: none;
}
.subcommand-header {
display: grid;
grid-template-columns: 2fr 3fr auto auto;
gap: 10px;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--border);
}
.subcommand-name-input,
.subcommand-desc-input {
padding: 10px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-primary);
font-size: 1rem;
}
.subcommand-name-input {
font-weight: 600;
font-family: 'Courier New', monospace;
}
.toggle-collapse-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-size: 1rem;
}
.toggle-collapse-btn:hover {
background: var(--bg-primary);
}
.subcommand-section.collapsed .toggle-collapse-btn::before {
content: '▶';
}
.subcommand-section:not(.collapsed) .toggle-collapse-btn::before {
content: '▼';
}
.add-subcommand-btn {
width: 100%;
padding: 15px;
background: transparent;
border: 2px dashed var(--border);
color: var(--text-secondary);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-size: 1rem;
}
.add-subcommand-btn:hover {
border-color: var(--accent);
color: var(--accent);
background: rgba(74, 158, 255, 0.05);
}
.subcommand-body {
animation: slideDown 0.2s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.badge {
display: inline-block;
padding: 3px 8px;
background: var(--accent);
color: white;
border-radius: 3px;
font-size: 0.75rem;
font-weight: 600;
margin-left: 10px;
}
.flat-mode .subcommands-container {
display: none;
}
.subcommand-mode .global-section .section-title::after {
content: '(Optional)';
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 400;
margin-left: 10px;
}
@media (max-width: 768px) {
.argument-header,
.argument-row {
grid-template-columns: 1fr;
}
.argument-header span {
display: none;
}
.remove-btn {
justify-self: start;
}
}

234
static/js/app.js Normal file
View File

@@ -0,0 +1,234 @@
class ArgumentBuilder {
constructor() {
this.arguments = [];
this.container = document.getElementById('arguments-container');
this.headerInput = document.getElementById('custom-header');
this.generateBtn = document.getElementById('generate-btn');
this.helpContent = document.getElementById('help-content');
this.outputSection = document.getElementById('output-section');
this.generatedCode = document.getElementById('generated-code');
this.copyBtn = document.getElementById('copy-btn');
this.init();
}
init() {
// Add initial empty row
this.addArgumentRow();
// Event listeners
this.generateBtn.addEventListener('click', () => this.generateScript());
this.headerInput.addEventListener('input', () => this.updateHelpPreview());
this.copyBtn.addEventListener('click', () => this.copyToClipboard());
// Update help preview initially
this.updateHelpPreview();
}
addArgumentRow(params = '', command = '', helpText = '') {
const row = document.createElement('div');
row.className = 'argument-row';
const paramsInput = document.createElement('input');
paramsInput.type = 'text';
paramsInput.placeholder = '-v --verbose';
paramsInput.value = params;
const commandInput = document.createElement('input');
commandInput.type = 'text';
commandInput.placeholder = 'verbose';
commandInput.value = command;
const helpTextInput = document.createElement('input');
helpTextInput.type = 'text';
helpTextInput.placeholder = 'Enable verbose output';
helpTextInput.value = helpText;
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '×';
removeBtn.addEventListener('click', () => {
row.remove();
this.updateHelpPreview();
});
// Add input event listeners for real-time updates
const updatePreview = () => {
this.updateHelpPreview();
// Add new empty row if this is the last row and has content
const rows = this.container.querySelectorAll('.argument-row');
const isLastRow = rows[rows.length - 1] === row;
const hasContent = commandInput.value.trim() !== '';
if (isLastRow && hasContent) {
this.addArgumentRow();
}
};
paramsInput.addEventListener('input', updatePreview);
commandInput.addEventListener('input', updatePreview);
helpTextInput.addEventListener('input', updatePreview);
row.appendChild(paramsInput);
row.appendChild(commandInput);
row.appendChild(helpTextInput);
row.appendChild(removeBtn);
this.container.appendChild(row);
}
getArguments() {
const rows = this.container.querySelectorAll('.argument-row');
const args = [];
rows.forEach(row => {
const inputs = row.querySelectorAll('input');
const params = inputs[0].value.trim();
const command = inputs[1].value.trim();
const helpText = inputs[2].value.trim();
if (command) {
args.push({
params: params || command,
command: command,
helpText: helpText || command
});
}
});
return args;
}
addSubcommand(name = '', description = '') {
const subcommandSection = document.createElement('div');
subcommandSection.className = 'subcommand-section';
const header = this.createSubcommandHeader(name, description);
const argsContainer = this.createArgumentsContainer();
subcommandSection.appendChild(header);
subcommandSection.appendChild(argsContainer);
this.subcommandContainer.appendChild(subcommandSection);
return subcommandSection;
}
updateHelpPreview() {
const header = this.headerInput.value.trim();
const args = this.getArguments();
if (args.length === 0) {
this.helpContent.textContent = 'Add arguments to see the help output...';
return;
}
let helpText = '';
if (header) {
helpText += header + '\n\n';
}
helpText += 'Usage: script.sh [OPTIONS]\n\n';
helpText += 'Options:\n';
args.forEach(arg => {
const params = arg.params || arg.command;
const help = arg.helpText || arg.command;
helpText += ` ${params.padEnd(20)} ${help}\n`;
});
helpText += ` ${'-h --help'.padEnd(20)} Show this help message\n`;
this.helpContent.textContent = helpText;
}
createSubcommandHeader(name, description) {
const header = document.createElement('div');
header.className = 'subcommand-header';
header.innerHTML = `
<input type="text" class="subcommand-name"
placeholder="container" value="${name}">
<input type="text" class="subcommand-desc"
placeholder="Manage containers" value="${description}">
<button class="toggle-btn">▼</button>
<button class="remove-btn">×</button>
`;
return header;
}
getSubcommands() {
const sections = this.subcommandContainer.querySelectorAll('.subcommand-section');
return Array.from(sections).map(section => {
const nameInput = section.querySelector('.subcommand-name');
const descInput = section.querySelector('.subcommand-desc');
const args = this.getArgumentsFromSection(section);
return {
name: nameInput.value.trim(),
description: descInput.value.trim(),
arguments: args
};
}).filter(sc => sc.name);
}
async generateScript() {
const header = this.headerInput.value.trim();
const args = this.getArguments();
if (args.length === 0) {
alert('Please add at least one argument');
return;
}
const payload = {
header: this.headerInput.value.trim(),
arguments: this.getArguments(),
subcommands: this.getSubcommands()
};
try {
const response = await fetch('/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Failed to generate script');
}
const scriptCode = await response.text();
this.generatedCode.textContent = scriptCode;
this.outputSection.style.display = 'block';
// Smooth scroll to output
this.outputSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (error) {
alert('Error generating script: ' + error.message);
}
}
async copyToClipboard() {
const code = this.generatedCode.textContent;
try {
await navigator.clipboard.writeText(code);
this.copyBtn.textContent = 'Copied!';
this.copyBtn.classList.add('copied');
setTimeout(() => {
this.copyBtn.textContent = 'Copy';
this.copyBtn.classList.remove('copied');
}, 2000);
} catch (error) {
alert('Failed to copy to clipboard');
}
}
}
// Initialize the application when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new ArgumentBuilder();
});

234
static/js/v2.js Normal file
View File

@@ -0,0 +1,234 @@
class ArgumentBuilder {
constructor() {
this.arguments = [];
this.container = document.getElementById('arguments-container');
this.headerInput = document.getElementById('custom-header');
this.generateBtn = document.getElementById('generate-btn');
this.helpContent = document.getElementById('help-content');
this.outputSection = document.getElementById('output-section');
this.generatedCode = document.getElementById('generated-code');
this.copyBtn = document.getElementById('copy-btn');
this.init();
}
init() {
// Add initial empty row
this.addArgumentRow();
// Event listeners
this.generateBtn.addEventListener('click', () => this.generateScript());
this.headerInput.addEventListener('input', () => this.updateHelpPreview());
this.copyBtn.addEventListener('click', () => this.copyToClipboard());
// Update help preview initially
this.updateHelpPreview();
}
addArgumentRow(params = '', command = '', helpText = '') {
const row = document.createElement('div');
row.className = 'argument-row';
const paramsInput = document.createElement('input');
paramsInput.type = 'text';
paramsInput.placeholder = '-v --verbose';
paramsInput.value = params;
const commandInput = document.createElement('input');
commandInput.type = 'text';
commandInput.placeholder = 'verbose';
commandInput.value = command;
const helpTextInput = document.createElement('input');
helpTextInput.type = 'text';
helpTextInput.placeholder = 'Enable verbose output';
helpTextInput.value = helpText;
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '×';
removeBtn.addEventListener('click', () => {
row.remove();
this.updateHelpPreview();
});
// Add input event listeners for real-time updates
const updatePreview = () => {
this.updateHelpPreview();
// Add new empty row if this is the last row and has content
const rows = this.container.querySelectorAll('.argument-row');
const isLastRow = rows[rows.length - 1] === row;
const hasContent = commandInput.value.trim() !== '';
if (isLastRow && hasContent) {
this.addArgumentRow();
}
};
paramsInput.addEventListener('input', updatePreview);
commandInput.addEventListener('input', updatePreview);
helpTextInput.addEventListener('input', updatePreview);
row.appendChild(paramsInput);
row.appendChild(commandInput);
row.appendChild(helpTextInput);
row.appendChild(removeBtn);
this.container.appendChild(row);
}
getArguments() {
const rows = this.container.querySelectorAll('.argument-row');
const args = [];
rows.forEach(row => {
const inputs = row.querySelectorAll('input');
const params = inputs[0].value.trim();
const command = inputs[1].value.trim();
const helpText = inputs[2].value.trim();
if (command) {
args.push({
params: params || command,
command: command,
helpText: helpText || command
});
}
});
return args;
}
addSubcommand(name = '', description = '') {
const subcommandSection = document.createElement('div');
subcommandSection.className = 'subcommand-section';
const header = this.createSubcommandHeader(name, description);
const argsContainer = this.createArgumentsContainer();
subcommandSection.appendChild(header);
subcommandSection.appendChild(argsContainer);
this.subcommandContainer.appendChild(subcommandSection);
return subcommandSection;
}
updateHelpPreview() {
const header = this.headerInput.value.trim();
const args = this.getArguments();
if (args.length === 0) {
this.helpContent.textContent = 'Add arguments to see the help output...';
return;
}
let helpText = '';
if (header) {
helpText += header + '\n\n';
}
helpText += 'Usage: script.sh [OPTIONS]\n\n';
helpText += 'Options:\n';
args.forEach(arg => {
const params = arg.params || arg.command;
const help = arg.helpText || arg.command;
helpText += ` ${params.padEnd(20)} ${help}\n`;
});
helpText += ` ${'-h --help'.padEnd(20)} Show this help message\n`;
this.helpContent.textContent = helpText;
}
createSubcommandHeader(name, description) {
const header = document.createElement('div');
header.className = 'subcommand-header';
header.innerHTML = `
<input type="text" class="subcommand-name"
placeholder="container" value="${name}">
<input type="text" class="subcommand-desc"
placeholder="Manage containers" value="${description}">
<button class="toggle-btn">▼</button>
<button class="remove-btn">×</button>
`;
return header;
}
getSubcommands() {
const sections = this.subcommandContainer.querySelectorAll('.subcommand-section');
return Array.from(sections).map(section => {
const nameInput = section.querySelector('.subcommand-name');
const descInput = section.querySelector('.subcommand-desc');
const args = this.getArgumentsFromSection(section);
return {
name: nameInput.value.trim(),
description: descInput.value.trim(),
arguments: args
};
}).filter(sc => sc.name);
}
async generateScript() {
const header = this.headerInput.value.trim();
const args = this.getArguments();
if (args.length === 0) {
alert('Please add at least one argument');
return;
}
const payload = {
header: this.headerInput.value.trim(),
arguments: this.getArguments(),
subcommands: this.getSubcommands()
};
try {
const response = await fetch('/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Failed to generate script');
}
const scriptCode = await response.text();
this.generatedCode.textContent = scriptCode;
this.outputSection.style.display = 'block';
// Smooth scroll to output
this.outputSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (error) {
alert('Error generating script: ' + error.message);
}
}
async copyToClipboard() {
const code = this.generatedCode.textContent;
try {
await navigator.clipboard.writeText(code);
this.copyBtn.textContent = 'Copied!';
this.copyBtn.classList.add('copied');
setTimeout(() => {
this.copyBtn.textContent = 'Copy';
this.copyBtn.classList.remove('copied');
}, 2000);
} catch (error) {
alert('Failed to copy to clipboard');
}
}
}
// Initialize the application when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new ArgumentBuilder();
});