<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QUILLSQL</title>
<style>
:root {
--bg: #040607;
--surface: #05090b;
--surface-lite: #0b1014;
--border: #131c22;
--text: #d1f0ff;
--text-muted: #6c8593;
--accent: #27f79a;
--accent-soft: rgba(39, 247, 154, 0.16);
--accent-strong: rgba(39, 247, 154, 0.7);
--warning: #f7c948;
--error: #ff7b72;
--success: #2ee6c5;
--scanline: rgba(39, 247, 154, 0.045);
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
height: 100%;
background: radial-gradient(circle at 50% -20%, rgba(39, 247, 154, 0.22), transparent 60%), var(--bg);
color: var(--text);
font-family: "JetBrains Mono", "Fira Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace;
letter-spacing: 0.02em;
-webkit-font-smoothing: antialiased;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background: linear-gradient(var(--scanline) 1px, transparent 1px);
background-size: 100% 2px;
mix-blend-mode: screen;
opacity: 0.65;
}
body::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background: radial-gradient(circle at 20% 20%, rgba(39, 247, 154, 0.18), transparent 50%),
radial-gradient(circle at 80% 80%, rgba(39, 120, 247, 0.12), transparent 55%);
opacity: 0.6;
mix-blend-mode: color-dodge;
}
a {
color: var(--accent);
}
.terminal {
position: relative;
max-width: 960px;
height: 88vh;
margin: 3vh auto;
background: linear-gradient(140deg, rgba(39, 247, 154, 0.07), transparent 70%), var(--surface);
border: 1px solid var(--border);
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(39, 247, 154, 0.05);
display: flex;
flex-direction: column;
overflow: hidden;
}
.bezel {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: rgba(4, 12, 14, 0.94);
border-bottom: 1px solid rgba(39, 247, 154, 0.08);
box-shadow: inset 0 -1px 0 rgba(39, 247, 154, 0.08);
}
.bezel-links {
margin-left: auto;
display: inline-flex;
gap: 8px;
align-items: center;
}
.bezel-link {
padding: 4px 10px;
border: 1px solid rgba(39, 247, 154, 0.24);
border-radius: 6px;
color: var(--accent);
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
text-decoration: none;
transition: background 0.15s ease, color 0.15s ease, border 0.15s ease;
background: transparent;
cursor: pointer;
}
.bezel-link:hover {
background: rgba(39, 247, 154, 0.12);
border-color: rgba(39, 247, 154, 0.5);
}
.led-strip {
display: inline-flex;
gap: 6px;
}
.led {
width: 11px;
height: 11px;
border-radius: 50%;
background: rgba(39, 247, 154, 0.25);
box-shadow: 0 0 4px rgba(39, 247, 154, 0.65);
}
.led.warn {
background: rgba(247, 201, 72, 0.35);
box-shadow: 0 0 4px rgba(247, 201, 72, 0.65);
}
.led.error {
background: rgba(255, 123, 114, 0.35);
box-shadow: 0 0 4px rgba(255, 123, 114, 0.65);
}
.bezel .title {
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 11px;
color: var(--text-muted);
}
.status {
margin-left: 16px;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--accent);
}
.status[data-state="busy"] {
color: var(--warning);
}
.status[data-state="error"] {
color: var(--error);
}
.screen {
display: flex;
flex-direction: column;
position: relative;
flex: 1;
padding: 0;
min-height: 0;
}
.screen::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(180deg, rgba(0, 0, 0, 0.12) 0px, rgba(0, 0, 0, 0.12) 1px, transparent 1px, transparent 3px);
mix-blend-mode: multiply;
opacity: 0.45;
}
.screen::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.08), transparent 55%);
opacity: 0.3;
mix-blend-mode: screen;
}
.overlay {
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(rgba(39, 247, 154, 0.1), transparent), linear-gradient(90deg, rgba(39, 247, 154, 0.05), transparent 60%);
mix-blend-mode: soft-light;
opacity: 0.35;
}
.content {
position: relative;
flex: 1;
display: grid;
grid-template-rows: auto 1fr auto auto;
gap: 12px;
padding: 16px 22px 12px 22px;
min-height: 0;
z-index: 1;
}
pre.logo {
margin: 0;
margin-bottom: 14px;
color: var(--accent);
font-size: 12px;
text-shadow: 0 0 8px rgba(39, 247, 154, 0.45);
}
#log {
overflow-y: auto;
padding-right: 8px;
display: flex;
flex-direction: column;
gap: 10px;
min-height: 0;
}
#log::-webkit-scrollbar {
width: 8px;
}
#log::-webkit-scrollbar-thumb {
background: rgba(39, 247, 154, 0.12);
border-radius: 8px;
}
.line {
white-space: pre-wrap;
word-break: break-word;
font-size: 13px;
}
.line.prompt {
color: var(--accent);
}
.line.error {
color: var(--error);
}
.line.success {
color: var(--success);
}
.line.muted {
color: var(--text-muted);
}
.line .prompt-text {
color: var(--accent);
margin-right: 8px;
}
pre.block {
margin: 0;
background: rgba(7, 12, 14, 0.74);
border: 1px solid rgba(39, 247, 154, 0.18);
padding: 12px 14px;
border-radius: 6px;
font-size: 13px;
line-height: 1.6;
color: var(--text);
backdrop-filter: blur(4px);
box-shadow: 0 0 0 1px rgba(39, 247, 154, 0.05) inset;
white-space: pre-wrap;
word-break: break-word;
}
form.input-form {
position: relative;
display: flex;
align-items: flex-start;
gap: 10px;
margin-top: 16px;
padding: 12px 14px;
border: 1px solid rgba(39, 247, 154, 0.12);
border-radius: 8px;
background: rgba(5, 11, 14, 0.78);
box-shadow: inset 0 0 0 1px rgba(39, 247, 154, 0.05), 0 12px 32px rgba(0, 0, 0, 0.45);
}
form.input-form:focus-within {
border-color: rgba(39, 247, 154, 0.34);
box-shadow: inset 0 0 0 1px rgba(39, 247, 154, 0.18), 0 0 16px rgba(39, 247, 154, 0.18);
}
.prompt-label {
color: var(--accent);
padding-top: 4px;
}
#cmd {
flex: 1;
min-height: 22px;
max-height: 160px;
background: transparent;
border: none;
outline: none;
resize: none;
color: var(--text);
font: inherit;
line-height: 1.6;
padding: 0;
caret-color: var(--accent);
}
#cmd::placeholder {
color: rgba(108, 133, 147, 0.55);
}
.hint {
font-size: 11px;
color: var(--text-muted);
margin-top: 6px;
}
.badge {
display: inline-block;
padding: 2px 6px;
border: 1px solid rgba(39, 247, 154, 0.24);
border-radius: 4px;
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
}
@media (max-width: 720px) {
.terminal {
margin: 0;
height: 100vh;
border-radius: 0;
}
.bezel {
flex-wrap: wrap;
gap: 8px;
}
form.input-form {
flex-direction: column;
}
.prompt-label {
padding-top: 0;
}
}
</style>
</head>
<body>
<div class="terminal">
<div class="bezel">
<div class="led-strip">
<span class="led error"></span>
<span class="led warn"></span>
<span class="led"></span>
</div>
<span class="title">QUILLSQL TERMINAL</span>
<div class="bezel-links">
<a href="https://feichai0017.github.io/QuillSQL/" target="_blank" class="bezel-link">DOCS</a>
<button class="bezel-link" id="openGithub">GITHUB</button>
<button class="bezel-link" id="openProfile">PROFILE</button>
</div>
<span id="status" class="status" data-state="ready">READY</span>
</div>
<div class="screen">
<div class="overlay"></div>
<div class="content">
<pre class="logo" id="logo"></pre>
<div id="log" class="log"></div>
<form id="input-form" class="input-form" autocomplete="off">
<span class="prompt-label">quill@tty:~$</span>
<textarea id="cmd" rows="1" placeholder="type help for usage · Shift+Enter to add newline"></textarea>
</form>
<div class="hint">Commands: help · github · profile · clear · examples · example
<name> · history · Any other input executes SQL.</div>
</div>
</div>
</div>
<script>
const statusEl = document.getElementById('status');
const logEl = document.getElementById('log');
const cmdEl = document.getElementById('cmd');
const formEl = document.getElementById('input-form');
const logoEl = document.getElementById('logo');
const openGithubBtn = document.getElementById('openGithub');
const openProfileBtn = document.getElementById('openProfile');
const STORAGE_KEY = 'quillsql:apiBase';
const DEFAULT_API_BASE = '/api/sql';
const asciiLogo = [
' ██████╗ ██╗ ██╗██╗██╗ ██╗ ███████╗ ██████╗ ██╗ ',
' ██╔═══██╗██║ ██║██║██║ ██║ ██╔════╝██╔═══██╗██║ ',
' ██║ ██║██║ ██║██║██║ ██║ ███████╗██║ ██║██║ ',
' ██║▄▄ ██║██║ ██║██║██║ ██║ ╚════██║██║▄▄ ██║██║ ',
' ╚██████╔╝╚██████╔╝██║███████╗███████╗███████║╚██████╔╝███████╗ ',
' ╚══▀▀═╝ ╚═════╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚══▀▀═╝ ╚══════╝ ',
'-----------------------------------------------------------------',
' QuillSQL • Interactive Terminal • `help` for usage '
].join('\n');
logoEl.textContent = asciiLogo;
let examples = {};
(async function loadExamples() {
try {
const response = await fetch('/api/examples');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
examples = await response.json();
appendLine('✓ SQL examples loaded from server.', 'success');
} catch (e) {
appendLine(`❌ Failed to load SQL examples: ${e}`, 'error');
console.error('Failed to load examples:', e);
}
})();
function showGithub() {
appendLine('GitHub repository', 'muted');
appendBlock([
'Project: QuillSQL',
`URL : ${GITHUB_URL}`,
'Stars : give it a ⭐ if you find it useful!',
'',
'Tip: run `git clone` with the URL above to explore locally.'
].join('\n'));
}
function showProfile() {
appendLine('Maintainer profile', 'muted');
appendBlock([
'Handle : feichai0017',
`URL : ${PROFILE_URL}`,
'Bio : Systems & database tinkerer. Find more projects on GitHub.'
].join('\n'));
}
function handleCommand(input) {
const trimmed = input.trim();
if (!trimmed) {
appendLine('(empty)', 'muted');
return;
}
if (trimmed === 'help') {
printHelp();
return;
}
if (trimmed === 'github') {
showGithub();
return;
}
if (trimmed === 'profile') {
showProfile();
return;
}
if (trimmed === 'clear') {
logEl.innerHTML = '';
appendLine('Session cleared.', 'muted');
return;
}
if (trimmed === 'history') {
if (history.length === 0) {
appendLine('No history yet.', 'muted');
} else {
appendBlock(history.map((cmd, idx) => `${String(idx + 1).padStart(2, '0')}: ${cmd}`).join('\n'));
}
return;
}
if (trimmed === 'examples') {
showExamples();
return;
}
if (trimmed.startsWith('example ')) {
const key = trimmed.split(/\s+/)[1];
if (!examples[key]) {
appendLine(`Unknown example: ${key}`, 'error');
} else {
const sqlText = examples[key];
cmdEl.value = sqlText;
cmdEl.dispatchEvent(new Event('input'));
appendLine(`Loaded example '${key}' into prompt.`, 'success');
runSQL(sqlText);
}
return;
}
runSQL(input);
}
formEl.addEventListener('submit', (event) => {
event.preventDefault();
const value = cmdEl.value;
const normalized = value.replace(/\s+$/g, '');
appendPrompt(normalized || value);
if (value.trim()) {
history.push(value.trim());
historyIndex = history.length;
handleCommand(value);
} else {
appendLine('(empty)', 'muted');
}
cmdEl.value = '';
cmdEl.style.height = '22px';
});
cmdEl.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
formEl.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
} else if (event.key === 'ArrowUp') {
event.preventDefault();
if (history.length === 0) return;
historyIndex = Math.max(0, historyIndex - 1);
cmdEl.value = history[historyIndex] || '';
cmdEl.dispatchEvent(new Event('input'));
} else if (event.key === 'ArrowDown') {
event.preventDefault();
if (history.length === 0) return;
historyIndex = Math.min(history.length, historyIndex + 1);
cmdEl.value = history[historyIndex] || '';
cmdEl.dispatchEvent(new Event('input'));
}
});
cmdEl.addEventListener('input', () => {
cmdEl.style.height = '22px';
cmdEl.style.height = `${cmdEl.scrollHeight}px`;
});
openGithubBtn.addEventListener('click', () => {
showGithub();
});
openProfileBtn.addEventListener('click', () => {
showProfile();
});
appendLine('Welcome to QUILLSQL Terminal. Type `help` to get started.', 'muted');
appendLine(`Current SQL endpoint: ${getApiBase()}`, 'muted');
cmdEl.focus();
</script>
</body>
</html>