This commit is contained in:
2026-01-06 21:17:10 +01:00
parent 85d19cbaad
commit 54050b543a
3 changed files with 352 additions and 183 deletions

View File

@@ -1,53 +1,108 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en" class="dark"> <html lang="en" class="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stupid LLM Editor</title> <title>Kreatyw</title>
<link rel="stylesheet" href="/ui/style.css"> <link rel="stylesheet" href="/ui/style.css" />
</head> </head>
<body> <body>
<div class="container"> <div class="app-layout">
<header class="header"> <aside class="sidebar">
<h1 class="header-title">Stupid LLM Editor</h1> <div class="sidebar-header">
<p class="header-subtitle">AI Pair-Programmer for Polish Literature</p> <div class="brand-wrapper">
</header> <svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="white"
class="sparkle-icon"
>
<path
d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"
/>
</svg>
<h1 class="brand">Kreatyw</h1>
</div>
</div>
<div class="card controls"> <nav class="settings">
<div class="control-grid"> <div class="setting-item">
<div class="control-group"> <div class="setting-label">
<label for="n-gram">Complexity (N)</label> <label for="n-gram">Complexity (N)</label>
<select id="n-gram" class="input-base"> <span class="value-badge"
>N=<span id="n-val">4</span></span
>
</div>
<select id="n-gram" class="select-input">
<option value="2">2 (Bigram)</option> <option value="2">2 (Bigram)</option>
<option value="3" selected>3 (Trigram)</option> <option value="3">3 (Trigram)</option>
<option value="4">4 (Tetragram)</option> <option value="4" selected>4 (Tetragram)</option>
<option value="5">5 (Pentagram)</option> <option value="5">5 (Pentagram)</option>
</select> </select>
</div> </div>
<div class="control-group">
<label for="temperature">Creativity (Temp): <span id="temp-val">0.7</span></label> <div class="setting-item">
<input type="range" id="temperature" min="0.1" max="2.0" step="0.1" value="0.7"> <div class="setting-label">
</div> <label for="temperature">Creativity (Temp)</label>
<div class="control-group"> <span class="value-badge" id="temp-val">1.6</span>
<label for="length">Length (Words): <span id="length-val">5</span></label>
<input type="range" id="length" min="1" max="20" step="1" value="5">
</div>
</div>
<div class="generate-action">
<button id="generate-more-btn" class="btn btn-primary">Generate Paragraph</button>
</div> </div>
<input
type="range"
id="temperature"
min="0.1"
max="2.0"
step="0.1"
value="1.6"
class="slider"
/>
</div> </div>
<div class="card editor-wrapper"> <div class="setting-item">
<div id="suggestion-overlay"></div> <div class="setting-label">
<textarea id="editor" rows="1" spellcheck="false" autofocus placeholder="Start typing... Press Tab to autocomplete."></textarea> <label for="length">Token Length</label>
<span class="value-badge" id="length-val">5</span>
</div>
<input
type="range"
id="length"
min="1"
max="50"
step="1"
value="5"
class="slider"
/>
</div> </div>
<footer class="status-bar"> <div class="action-area">
<span>Status:</span> <button id="generate-more-btn" class="btn btn-primary">
Generate Paragraph
</button>
</div>
</nav>
<div class="sidebar-footer">
<div class="status-indicator">
<span class="status-dot"></span>
<span id="status">Idle</span> <span id="status">Idle</span>
</footer> </div>
</div>
</aside>
<main class="editor-main">
<div class="editor-container">
<div class="editor-viewport card">
<div id="suggestion-overlay"></div>
<textarea
id="editor"
spellcheck="false"
autofocus
placeholder="Start writing something poetic..."
></textarea>
</div>
</div>
</main>
</div> </div>
<script src="/ui/script.js"></script> <script src="/ui/script.js"></script>
</body> </body>

View File

@@ -1,10 +1,13 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const editor = document.getElementById('editor'); const editor = document.getElementById('editor');
const suggestionOverlay = document.getElementById('suggestion-overlay'); const suggestionOverlay = document.getElementById('suggestion-overlay');
const status = document.getElementById('status'); const status = document.getElementById('status');
const statusIndicator = document.querySelector('.status-indicator');
// Controls // Controls
const nGramSelect = document.getElementById('n-gram'); const nGramSelect = document.getElementById('n-gram');
const nValDisplay = document.getElementById('n-val');
const tempInput = document.getElementById('temperature'); const tempInput = document.getElementById('temperature');
const tempValDisplay = document.getElementById('temp-val'); const tempValDisplay = document.getElementById('temp-val');
const lengthInput = document.getElementById('length'); const lengthInput = document.getElementById('length');
@@ -15,16 +18,20 @@ document.addEventListener('DOMContentLoaded', () => {
let isFetching = false; let isFetching = false;
let debounceTimer; let debounceTimer;
const autoResize = () => { // --- UI Logic ---
editor.style.height = 'auto';
suggestionOverlay.style.height = 'auto'; const updateUI = () => {
const newHeight = Math.max(360, editor.scrollHeight); nValDisplay.textContent = nGramSelect.value;
editor.style.height = newHeight + 'px'; tempValDisplay.textContent = tempInput.value;
suggestionOverlay.style.height = newHeight + 'px'; lengthValDisplay.textContent = lengthInput.value;
}; };
tempInput.addEventListener('input', () => { tempValDisplay.textContent = tempInput.value; }); tempInput.addEventListener('input', updateUI);
lengthInput.addEventListener('input', () => { lengthValDisplay.textContent = lengthInput.value; }); lengthInput.addEventListener('input', updateUI);
nGramSelect.addEventListener('change', () => {
updateUI();
triggerUpdate();
});
const triggerUpdate = () => { const triggerUpdate = () => {
currentSuggestion = ''; currentSuggestion = '';
@@ -33,16 +40,17 @@ document.addEventListener('DOMContentLoaded', () => {
if (prompt.trim().length > 0) fetchPrediction(prompt); if (prompt.trim().length > 0) fetchPrediction(prompt);
}; };
nGramSelect.addEventListener('change', triggerUpdate);
tempInput.addEventListener('change', triggerUpdate); tempInput.addEventListener('change', triggerUpdate);
lengthInput.addEventListener('change', triggerUpdate); lengthInput.addEventListener('change', triggerUpdate);
// --- Core Functions ---
const fetchPrediction = async (prompt, customLength = null) => { const fetchPrediction = async (prompt, customLength = null) => {
if (isFetching) return; if (isFetching) return;
isFetching = true; isFetching = true;
status.textContent = 'Thinking...'; status.textContent = 'Thinking...';
status.classList.add('fetching'); statusIndicator.classList.add('fetching');
const n = parseInt(nGramSelect.value); const n = parseInt(nGramSelect.value);
const temperature = parseFloat(tempInput.value); const temperature = parseFloat(tempInput.value);
@@ -72,7 +80,7 @@ document.addEventListener('DOMContentLoaded', () => {
} finally { } finally {
isFetching = false; isFetching = false;
status.textContent = 'Idle'; status.textContent = 'Idle';
status.classList.remove('fetching'); statusIndicator.classList.remove('fetching');
} }
}; };
@@ -88,15 +96,18 @@ document.addEventListener('DOMContentLoaded', () => {
editor.value += space + text; editor.value += space + text;
currentSuggestion = ''; currentSuggestion = '';
updateSuggestion(); updateSuggestion();
autoResize();
window.scrollTo(0, document.body.scrollHeight); // Ensure the editor scrolls with content
editor.scrollTop = editor.scrollHeight;
}; };
// --- Event Handlers ---
editor.addEventListener('input', () => { editor.addEventListener('input', () => {
autoResize();
clearTimeout(debounceTimer); clearTimeout(debounceTimer);
currentSuggestion = ''; currentSuggestion = '';
updateSuggestion(); updateSuggestion();
const prompt = editor.value; const prompt = editor.value;
if (prompt.trim().length === 0) return; if (prompt.trim().length === 0) return;
debounceTimer = setTimeout(() => fetchPrediction(prompt), 300); debounceTimer = setTimeout(() => fetchPrediction(prompt), 300);
@@ -114,5 +125,11 @@ document.addEventListener('DOMContentLoaded', () => {
fetchPrediction(editor.value, 50); fetchPrediction(editor.value, 50);
}); });
autoResize(); // Sync scroll
editor.addEventListener('scroll', () => {
suggestionOverlay.scrollTop = editor.scrollTop;
});
// Initialize UI badges
updateUI();
}); });

View File

@@ -1,168 +1,265 @@
:root { :root {
--background: hsl(0 0% 3.9%); --background: #09090b;
--foreground: hsl(0 0% 98%); --foreground: #fafafa;
--card: hsl(0 0% 12%); --card: #09090b;
--card-foreground: hsl(0 0% 98%); --card-foreground: #fafafa;
--popover: hsl(0 0% 3.9%); --primary: #fafafa;
--popover-foreground: hsl(0 0% 98%); --primary-foreground: #18181b;
--primary: hsl(0 0% 98%); --secondary: #27272a;
--primary-foreground: hsl(0 0% 9%); --secondary-foreground: #fafafa;
--secondary: hsl(0 0% 14.9%); --muted: #27272a;
--secondary-foreground: hsl(0 0% 98%); --muted-foreground: #a1a1aa;
--muted: hsl(0 0% 14.9%); --accent: #27272a;
--muted-foreground: hsl(0 0% 63.9%); --accent-foreground: #fafafa;
--accent: hsl(0 0% 14.9%); --border: #27272a;
--accent-foreground: hsl(0 0% 98%); --input: #27272a;
--border: hsl(0 0% 14.9%); --ring: #d4d4d8;
--input: hsl(0 0% 14.9%);
--ring: hsl(0 0% 83.1%);
--radius: 0.5rem; --radius: 0.5rem;
} }
* {
box-sizing: border-box;
}
body { body {
background-color: var(--background); background-color: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0; margin: 0;
padding: 2rem; height: 100vh;
overflow: hidden;
} }
.container { .app-layout {
max-width: 800px;
margin: 0 auto;
display: flex; display: flex;
flex-direction: column; height: 100vh;
gap: 1.5rem; width: 100vw;
} }
.header { /* Sidebar */
text-align: center; .sidebar {
} width: 300px;
.header-title {
font-size: 2rem;
font-weight: 700;
letter-spacing: -0.02em;
margin: 0;
}
.header-subtitle {
color: var(--muted-foreground);
font-size: 1rem;
margin-top: 0.25rem;
}
.card {
background-color: var(--card); background-color: var(--card);
border: 1px solid var(--border); border-right: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
}
.controls {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; padding: 1.5rem;
} flex-shrink: 0;
.control-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
} }
.control-group { .sidebar-header {
margin-bottom: 2.5rem;
}
.brand-wrapper {
display: flex;
align-items: center;
gap: 0.6rem;
}
.brand {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
letter-spacing: -0.02em;
}
.brand-sub {
font-size: 0.75rem;
color: var(--muted-foreground);
margin: 0.2rem 0 0 0;
}
.settings {
display: flex;
flex-direction: column;
gap: 2rem;
flex-grow: 1;
}
.setting-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
} }
.setting-label {
display: flex;
justify-content: space-between;
align-items: center;
}
label { label {
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
}
.input-base {
background-color: var(--background);
border: 1px solid var(--border);
border-radius: calc(var(--radius) - 2px);
color: var(--foreground); color: var(--foreground);
padding: 0.5rem 0.75rem;
height: 2.5rem;
}
select.input-base {
-webkit-appearance: none;
appearance: none;
padding-right: 2rem;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='hsl(0 0% 63.9%)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3e%3cpath d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 0.5rem center;
background-size: 1em 1em;
} }
.generate-action { .value-badge {
border-top: 1px solid var(--border); font-family: monospace;
padding-top: 1.5rem; font-size: 0.75rem;
text-align: right; background: var(--secondary);
padding: 2px 6px;
border-radius: 4px;
color: var(--muted-foreground);
} }
/* Styled Inputs */
.select-input {
background-color: var(--background);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--foreground);
padding: 0.5rem;
font-size: 0.875rem;
outline: none;
cursor: pointer;
transition: border-color 0.2s;
}
.select-input:focus {
border-color: var(--ring);
}
/* Custom Slider Styling */
.slider {
-webkit-appearance: none;
width: 100%;
height: 4px;
background: var(--secondary);
border-radius: 2px;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
background: var(--primary);
border-radius: 50%;
cursor: pointer;
border: 2px solid var(--background);
box-shadow: 0 0 0 1px var(--border);
}
.slider::-moz-range-thumb {
width: 16px;
height: 16px;
background: var(--primary);
border-radius: 50%;
cursor: pointer;
border: 2px solid var(--background);
box-shadow: 0 0 0 1px var(--border);
}
/* Button */
.btn { .btn {
display: inline-flex; width: 100%;
align-items: center; padding: 0.6rem;
justify-content: center; border-radius: var(--radius);
border-radius: calc(var(--radius) - 2px);
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
padding: 0.5rem 1rem;
transition: all 0.2s;
cursor: pointer; cursor: pointer;
transition: opacity 0.2s;
border: none; border: none;
} }
.btn-primary { .btn-primary {
background-color: var(--primary); background-color: var(--primary);
color: var(--primary-foreground); color: var(--primary-foreground);
} }
.btn-primary:hover { .btn-primary:hover {
background-color: hsl(0 0% 98% / 0.9); opacity: 0.9;
} }
.editor-wrapper { /* Sidebar Footer */
position: relative; .sidebar-footer {
padding: 0; padding-top: 1rem;
border-top: 1px solid var(--border);
} }
.status-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--muted-foreground);
}
.status-dot {
width: 6px;
height: 6px;
background-color: #22c55e;
border-radius: 50%;
}
.fetching .status-dot {
background-color: #eab308;
box-shadow: 0 0 8px #eab308;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.4; }
100% { opacity: 1; }
}
/* Main Editor Area */
.editor-main {
flex-grow: 1;
padding: 2rem;
display: flex;
justify-content: center;
background-color: var(--background);
}
.editor-container {
width: 100%;
max-width: 800px;
height: 100%;
}
.editor-viewport {
position: relative;
height: 100%;
border: 1px solid var(--border);
border-radius: var(--radius);
background-color: var(--card);
overflow-y: auto;
}
#editor, #suggestion-overlay { #editor, #suggestion-overlay {
width: 100%;
height: 100%;
padding: 2rem;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 1.1rem;
line-height: 1.8;
border: none;
background: transparent;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
box-sizing: border-box;
}
#editor {
position: relative;
z-index: 2;
color: var(--foreground);
outline: none;
resize: none;
}
#suggestion-overlay {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
font-family: 'SF Mono', 'Fira Code', 'Courier New', monospace;
font-size: 1rem;
line-height: 1.7;
background-color: transparent;
border: none;
overflow: hidden;
resize: none;
padding: 1.5rem;
min-height: 300px;
}
#editor {
z-index: 2;
color: var(--foreground);
}
#editor:focus { outline: none; }
#editor::placeholder { color: var(--muted-foreground); }
#suggestion-overlay {
z-index: 1; z-index: 1;
color: var(--muted-foreground); color: var(--muted-foreground);
pointer-events: none; pointer-events: none;
} opacity: 0.5;
.status-bar {
text-align: center;
font-size: 0.8rem;
color: var(--muted-foreground);
display: flex;
gap: 0.5rem;
justify-content: center;
}
#status.fetching {
color: var(--foreground);
} }