Layout fix

This commit is contained in:
2026-01-07 10:39:32 +01:00
parent e625447222
commit b5d5195f8f
4 changed files with 186 additions and 178 deletions

View File

@@ -1,3 +0,0 @@
TODO:
- Fix the suggestion text not scrolling
- Add a /api page

25
api.py
View File

@@ -1,13 +1,14 @@
import os import os
import uvicorn
from fastapi import FastAPI, Body
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
import sys import sys
import uvicorn
from fastapi import Body, FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
# Import core LLM logic # Import core LLM logic
from llm import load_or_train_model, generate_text, SOURCES_DIR from llm import SOURCES_DIR, generate_text, load_or_train_model
# --- Configuration --- # --- Configuration ---
# Models to pre-load on startup # Models to pre-load on startup
@@ -18,6 +19,7 @@ UI_DIR = "ui"
# Cache for loaded models: {n: model} # Cache for loaded models: {n: model}
MODEL_CACHE = {} MODEL_CACHE = {}
# --- Pydantic Models --- # --- Pydantic Models ---
class PredictRequest(BaseModel): class PredictRequest(BaseModel):
prompt: str prompt: str
@@ -25,12 +27,15 @@ class PredictRequest(BaseModel):
n: int = 3 n: int = 3
length: int = 5 length: int = 5
class PredictResponse(BaseModel): class PredictResponse(BaseModel):
prediction: str prediction: str
# --- FastAPI App --- # --- FastAPI App ---
app = FastAPI() app = FastAPI()
def get_model_for_n(n: int): def get_model_for_n(n: int):
""" """
Retrieves the model for a specific N from cache, or loads/trains it. Retrieves the model for a specific N from cache, or loads/trains it.
@@ -44,6 +49,7 @@ def get_model_for_n(n: int):
MODEL_CACHE[n] = model MODEL_CACHE[n] = model
return model return model
@app.on_event("startup") @app.on_event("startup")
def startup_event(): def startup_event():
""" """
@@ -54,6 +60,7 @@ def startup_event():
get_model_for_n(n) get_model_for_n(n)
print(f"Models for N={PRELOAD_N_GRAMS} loaded. Server is ready.") print(f"Models for N={PRELOAD_N_GRAMS} loaded. Server is ready.")
@app.post("/api/predict", response_model=PredictResponse) @app.post("/api/predict", response_model=PredictResponse)
async def predict(request: PredictRequest): async def predict(request: PredictRequest):
""" """
@@ -71,22 +78,26 @@ async def predict(request: PredictRequest):
model, model,
start_prompt=request.prompt, start_prompt=request.prompt,
length=length, length=length,
temperature=request.temperature temperature=request.temperature,
) )
return PredictResponse(prediction=prediction) return PredictResponse(prediction=prediction)
# --- Static Files and Root --- # --- Static Files and Root ---
app.mount("/ui", StaticFiles(directory=UI_DIR), name="ui") app.mount("/ui", StaticFiles(directory=UI_DIR), name="ui")
@app.get("/") @app.get("/")
async def read_root(): async def read_root():
return FileResponse(os.path.join(UI_DIR, "index.html")) return FileResponse(os.path.join(UI_DIR, "index.html"))
def run(): def run():
# Read port from environment variable, default to 8000 # Read port from environment variable, default to 8000
port = int(os.environ.get("PORT", 8000)) port = int(os.environ.get("PORT", 8000))
uvicorn.run(app, host="0.0.0.0", port=port) uvicorn.run(app, host="0.0.0.0", port=port)
if __name__ == "__main__": if __name__ == "__main__":
run() run()

View File

@@ -1,23 +1,22 @@
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");
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 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");
const lengthValDisplay = document.getElementById('length-val'); const lengthValDisplay = document.getElementById("length-val");
const generateBtn = document.getElementById('generate-more-btn'); const generateBtn = document.getElementById("generate-more-btn");
const sidebarToggle = document.getElementById('sidebar-toggle'); const sidebarToggle = document.getElementById("sidebar-toggle");
const sidebar = document.getElementById('sidebar'); const sidebar = document.getElementById("sidebar");
const acceptSuggestionBtn = document.getElementById('accept-suggestion-btn'); const acceptSuggestionBtn = document.getElementById("accept-suggestion-btn");
let currentSuggestion = ''; let currentSuggestion = "";
let isFetching = false; let isFetching = false;
let debounceTimer; let debounceTimer;
@@ -29,37 +28,35 @@ document.addEventListener('DOMContentLoaded', () => {
lengthValDisplay.textContent = lengthInput.value; lengthValDisplay.textContent = lengthInput.value;
}; };
sidebarToggle.addEventListener('click', () => { sidebarToggle.addEventListener("click", () => {
sidebar.classList.toggle('open'); sidebar.classList.toggle("open");
}); });
const closeSidebarOnMobile = () => { const closeSidebarOnMobile = () => {
if (window.innerWidth <= 768) { if (window.innerWidth <= 768) {
sidebar.classList.remove('open'); sidebar.classList.remove("open");
} }
}; };
tempInput.addEventListener('input', updateUI); tempInput.addEventListener("input", updateUI);
lengthInput.addEventListener('input', updateUI); lengthInput.addEventListener("input", updateUI);
nGramSelect.addEventListener('change', () => { nGramSelect.addEventListener("change", () => {
updateUI(); updateUI();
triggerUpdate(); triggerUpdate();
}); });
const triggerUpdate = () => { const triggerUpdate = () => {
currentSuggestion = ''; currentSuggestion = "";
updateSuggestion(); updateSuggestion();
const prompt = editor.value; const prompt = editor.value;
if (prompt.trim().length > 0) fetchPrediction(prompt); if (prompt.trim().length > 0) fetchPrediction(prompt);
}; };
tempInput.addEventListener('change', () => { tempInput.addEventListener("change", () => {
triggerUpdate(); triggerUpdate();
// Optional: close sidebar on change if on mobile
// closeSidebarOnMobile();
}); });
lengthInput.addEventListener('change', () => { lengthInput.addEventListener("change", () => {
triggerUpdate(); triggerUpdate();
}); });
@@ -69,59 +66,59 @@ document.addEventListener('DOMContentLoaded', () => {
if (isFetching) return; if (isFetching) return;
isFetching = true; isFetching = true;
status.textContent = 'Thinking...'; status.textContent = "Thinking...";
statusIndicator.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);
const length = customLength || parseInt(lengthInput.value); const length = customLength || parseInt(lengthInput.value);
try { try {
const response = await fetch('/api/predict', { const response = await fetch("/api/predict", {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt, n, temperature, length }), body: JSON.stringify({ prompt, n, temperature, length }),
}); });
if (!response.ok) throw new Error('Network response failed'); if (!response.ok) throw new Error("Network response failed");
const data = await response.json(); const data = await response.json();
if (customLength) { if (customLength) {
insertText(data.prediction || ''); insertText(data.prediction || "");
} else { } else {
currentSuggestion = data.prediction || ''; currentSuggestion = data.prediction || "";
updateSuggestion(); updateSuggestion();
} }
} catch (error) { } catch (error) {
console.error('Prediction failed:', error); console.error("Prediction failed:", error);
status.textContent = 'Error'; status.textContent = "Error";
} finally { } finally {
isFetching = false; isFetching = false;
status.textContent = 'Idle'; status.textContent = "Idle";
statusIndicator.classList.remove('fetching'); statusIndicator.classList.remove("fetching");
} }
}; };
const updateSuggestion = () => { const updateSuggestion = () => {
const editorText = editor.value; const editorText = editor.value;
const space = (editorText.length > 0 && !/\s$/.test(editorText)) ? ' ' : ''; const space = editorText.length > 0 && !/\s$/.test(editorText) ? " " : "";
suggestionOverlay.textContent = editorText + space + currentSuggestion; suggestionOverlay.textContent = editorText + space + currentSuggestion;
// Show/hide accept button // Show/hide accept button
if (currentSuggestion) { if (currentSuggestion) {
acceptSuggestionBtn.classList.add('visible'); acceptSuggestionBtn.classList.add("visible");
} else { } else {
acceptSuggestionBtn.classList.remove('visible'); acceptSuggestionBtn.classList.remove("visible");
} }
}; };
const insertText = (text) => { const insertText = (text) => {
if (!text) return; if (!text) return;
const space = (editor.value.length > 0 && !/\s$/.test(editor.value)) ? ' ' : ''; const space =
editor.value.length > 0 && !/\s$/.test(editor.value) ? " " : "";
editor.value += space + text; editor.value += space + text;
currentSuggestion = ''; currentSuggestion = "";
updateSuggestion(); updateSuggestion();
// Ensure the editor scrolls with content // Ensure the editor scrolls with content
@@ -130,9 +127,9 @@ document.addEventListener('DOMContentLoaded', () => {
// --- Event Handlers --- // --- Event Handlers ---
editor.addEventListener('input', () => { editor.addEventListener("input", () => {
clearTimeout(debounceTimer); clearTimeout(debounceTimer);
currentSuggestion = ''; currentSuggestion = "";
updateSuggestion(); updateSuggestion();
const prompt = editor.value; const prompt = editor.value;
@@ -140,15 +137,15 @@ document.addEventListener('DOMContentLoaded', () => {
debounceTimer = setTimeout(() => fetchPrediction(prompt), 300); debounceTimer = setTimeout(() => fetchPrediction(prompt), 300);
}); });
editor.addEventListener('keydown', (e) => { editor.addEventListener("keydown", (e) => {
if (e.key === 'Tab' && currentSuggestion) { if (e.key === "Tab" && currentSuggestion) {
e.preventDefault(); e.preventDefault();
insertText(currentSuggestion); insertText(currentSuggestion);
fetchPrediction(editor.value); fetchPrediction(editor.value);
} }
}); });
acceptSuggestionBtn.addEventListener('click', () => { acceptSuggestionBtn.addEventListener("click", () => {
if (currentSuggestion) { if (currentSuggestion) {
insertText(currentSuggestion); insertText(currentSuggestion);
fetchPrediction(editor.value); fetchPrediction(editor.value);
@@ -156,14 +153,14 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
generateBtn.addEventListener('click', () => { generateBtn.addEventListener("click", () => {
fetchPrediction(editor.value, 50); fetchPrediction(editor.value, 50);
closeSidebarOnMobile(); closeSidebarOnMobile();
}); });
// Sync scroll // Sync scroll - FIX: Use transform instead of scrollTop
editor.addEventListener('scroll', () => { editor.addEventListener("scroll", () => {
suggestionOverlay.scrollTop = editor.scrollTop; suggestionOverlay.style.transform = `translateY(-${editor.scrollTop}px)`;
}); });
// Initialize UI badges // Initialize UI badges

View File

@@ -369,7 +369,7 @@ label {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
background-color: var(--card); background-color: var(--card);
overflow-y: auto; overflow: hidden;
} }
#editor, #editor,
@@ -402,6 +402,7 @@ label {
color: var(--foreground); color: var(--foreground);
outline: none; outline: none;
resize: none; resize: none;
overflow-y: auto;
} }
#suggestion-overlay { #suggestion-overlay {
@@ -412,4 +413,6 @@ label {
color: var(--muted-foreground); color: var(--muted-foreground);
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
overflow: hidden;
transition: transform 0.05s linear;
} }