From b5d5195f8f55d0c6cfbef820c934f12992830285 Mon Sep 17 00:00:00 2001 From: N0VA Date: Wed, 7 Jan 2026 10:39:32 +0100 Subject: [PATCH] Layout fix --- README.md | 3 - api.py | 35 ++++-- ui/script.js | 321 +++++++++++++++++++++++++-------------------------- ui/style.css | 5 +- 4 files changed, 186 insertions(+), 178 deletions(-) diff --git a/README.md b/README.md index c538251..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,3 +0,0 @@ -TODO: -- Fix the suggestion text not scrolling -- Add a /api page \ No newline at end of file diff --git a/api.py b/api.py index f97d135..006dadd 100644 --- a/api.py +++ b/api.py @@ -1,13 +1,14 @@ 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 uvicorn +from fastapi import Body, FastAPI +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + # 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 --- # Models to pre-load on startup @@ -18,6 +19,7 @@ UI_DIR = "ui" # Cache for loaded models: {n: model} MODEL_CACHE = {} + # --- Pydantic Models --- class PredictRequest(BaseModel): prompt: str @@ -25,12 +27,15 @@ class PredictRequest(BaseModel): n: int = 3 length: int = 5 + class PredictResponse(BaseModel): prediction: str + # --- FastAPI App --- app = FastAPI() + def get_model_for_n(n: int): """ Retrieves the model for a specific N from cache, or loads/trains it. @@ -38,12 +43,13 @@ def get_model_for_n(n: int): global MODEL_CACHE if n in MODEL_CACHE: return MODEL_CACHE[n] - + print(f"Loading/Training model for N={n}...") model = load_or_train_model(SOURCES_DIR, n) MODEL_CACHE[n] = model return model + @app.on_event("startup") def startup_event(): """ @@ -54,6 +60,7 @@ def startup_event(): get_model_for_n(n) print(f"Models for N={PRELOAD_N_GRAMS} loaded. Server is ready.") + @app.post("/api/predict", response_model=PredictResponse) async def predict(request: PredictRequest): """ @@ -61,7 +68,7 @@ async def predict(request: PredictRequest): """ n = max(2, min(request.n, 5)) model = get_model_for_n(n) - + if not model: return {"prediction": ""} @@ -70,23 +77,27 @@ async def predict(request: PredictRequest): prediction = generate_text( model, start_prompt=request.prompt, - length=length, - temperature=request.temperature + length=length, + temperature=request.temperature, ) - + return PredictResponse(prediction=prediction) + # --- Static Files and Root --- app.mount("/ui", StaticFiles(directory=UI_DIR), name="ui") + @app.get("/") async def read_root(): return FileResponse(os.path.join(UI_DIR, "index.html")) + def run(): # Read port from environment variable, default to 8000 port = int(os.environ.get("PORT", 8000)) uvicorn.run(app, host="0.0.0.0", port=port) + if __name__ == "__main__": - run() \ No newline at end of file + run() diff --git a/ui/script.js b/ui/script.js index f87171e..846f963 100644 --- a/ui/script.js +++ b/ui/script.js @@ -1,171 +1,168 @@ +document.addEventListener("DOMContentLoaded", () => { + const editor = document.getElementById("editor"); + const suggestionOverlay = document.getElementById("suggestion-overlay"); + const status = document.getElementById("status"); + const statusIndicator = document.querySelector(".status-indicator"); -document.addEventListener('DOMContentLoaded', () => { - const editor = document.getElementById('editor'); - const suggestionOverlay = document.getElementById('suggestion-overlay'); - const status = document.getElementById('status'); - const statusIndicator = document.querySelector('.status-indicator'); - - // Controls - const nGramSelect = document.getElementById('n-gram'); - const nValDisplay = document.getElementById('n-val'); - const tempInput = document.getElementById('temperature'); - const tempValDisplay = document.getElementById('temp-val'); - const lengthInput = document.getElementById('length'); - const lengthValDisplay = document.getElementById('length-val'); - const generateBtn = document.getElementById('generate-more-btn'); - const sidebarToggle = document.getElementById('sidebar-toggle'); - const sidebar = document.getElementById('sidebar'); - const acceptSuggestionBtn = document.getElementById('accept-suggestion-btn'); + // Controls + const nGramSelect = document.getElementById("n-gram"); + const nValDisplay = document.getElementById("n-val"); + const tempInput = document.getElementById("temperature"); + const tempValDisplay = document.getElementById("temp-val"); + const lengthInput = document.getElementById("length"); + const lengthValDisplay = document.getElementById("length-val"); + const generateBtn = document.getElementById("generate-more-btn"); + const sidebarToggle = document.getElementById("sidebar-toggle"); + const sidebar = document.getElementById("sidebar"); + const acceptSuggestionBtn = document.getElementById("accept-suggestion-btn"); - let currentSuggestion = ''; - let isFetching = false; - let debounceTimer; + let currentSuggestion = ""; + let isFetching = false; + let debounceTimer; - // --- UI Logic --- - - const updateUI = () => { - nValDisplay.textContent = nGramSelect.value; - tempValDisplay.textContent = tempInput.value; - lengthValDisplay.textContent = lengthInput.value; - }; + // --- UI Logic --- - sidebarToggle.addEventListener('click', () => { - sidebar.classList.toggle('open'); - }); + const updateUI = () => { + nValDisplay.textContent = nGramSelect.value; + tempValDisplay.textContent = tempInput.value; + lengthValDisplay.textContent = lengthInput.value; + }; - const closeSidebarOnMobile = () => { - if (window.innerWidth <= 768) { - sidebar.classList.remove('open'); - } - }; + sidebarToggle.addEventListener("click", () => { + sidebar.classList.toggle("open"); + }); - tempInput.addEventListener('input', updateUI); - lengthInput.addEventListener('input', updateUI); - nGramSelect.addEventListener('change', () => { - updateUI(); - triggerUpdate(); - }); + const closeSidebarOnMobile = () => { + if (window.innerWidth <= 768) { + sidebar.classList.remove("open"); + } + }; - const triggerUpdate = () => { - currentSuggestion = ''; - updateSuggestion(); - const prompt = editor.value; - if (prompt.trim().length > 0) fetchPrediction(prompt); - }; - - tempInput.addEventListener('change', () => { - triggerUpdate(); - // Optional: close sidebar on change if on mobile - // closeSidebarOnMobile(); - }); - - lengthInput.addEventListener('change', () => { - triggerUpdate(); - }); - - // --- Core Functions --- - - const fetchPrediction = async (prompt, customLength = null) => { - if (isFetching) return; - - isFetching = true; - status.textContent = 'Thinking...'; - statusIndicator.classList.add('fetching'); - - const n = parseInt(nGramSelect.value); - const temperature = parseFloat(tempInput.value); - const length = customLength || parseInt(lengthInput.value); - - try { - const response = await fetch('/api/predict', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ prompt, n, temperature, length }), - }); - - if (!response.ok) throw new Error('Network response failed'); - - const data = await response.json(); - - if (customLength) { - insertText(data.prediction || ''); - } else { - currentSuggestion = data.prediction || ''; - updateSuggestion(); - } - - } catch (error) { - console.error('Prediction failed:', error); - status.textContent = 'Error'; - } finally { - isFetching = false; - status.textContent = 'Idle'; - statusIndicator.classList.remove('fetching'); - } - }; - - const updateSuggestion = () => { - const editorText = editor.value; - const space = (editorText.length > 0 && !/\s$/.test(editorText)) ? ' ' : ''; - suggestionOverlay.textContent = editorText + space + currentSuggestion; - - // Show/hide accept button - if (currentSuggestion) { - acceptSuggestionBtn.classList.add('visible'); - } else { - acceptSuggestionBtn.classList.remove('visible'); - } - }; - - const insertText = (text) => { - if (!text) return; - const space = (editor.value.length > 0 && !/\s$/.test(editor.value)) ? ' ' : ''; - editor.value += space + text; - currentSuggestion = ''; - updateSuggestion(); - - // Ensure the editor scrolls with content - editor.scrollTop = editor.scrollHeight; - }; - - // --- Event Handlers --- - - editor.addEventListener('input', () => { - clearTimeout(debounceTimer); - currentSuggestion = ''; - updateSuggestion(); - - const prompt = editor.value; - if (prompt.trim().length === 0) return; - debounceTimer = setTimeout(() => fetchPrediction(prompt), 300); - }); - - editor.addEventListener('keydown', (e) => { - if (e.key === 'Tab' && currentSuggestion) { - e.preventDefault(); - insertText(currentSuggestion); - fetchPrediction(editor.value); - } - }); - - acceptSuggestionBtn.addEventListener('click', () => { - if (currentSuggestion) { - insertText(currentSuggestion); - fetchPrediction(editor.value); - editor.focus(); - } - }); - - generateBtn.addEventListener('click', () => { - fetchPrediction(editor.value, 50); - closeSidebarOnMobile(); - }); - - // Sync scroll - editor.addEventListener('scroll', () => { - suggestionOverlay.scrollTop = editor.scrollTop; - }); - - // Initialize UI badges + tempInput.addEventListener("input", updateUI); + lengthInput.addEventListener("input", updateUI); + nGramSelect.addEventListener("change", () => { updateUI(); + triggerUpdate(); + }); + + const triggerUpdate = () => { + currentSuggestion = ""; + updateSuggestion(); + const prompt = editor.value; + if (prompt.trim().length > 0) fetchPrediction(prompt); + }; + + tempInput.addEventListener("change", () => { + triggerUpdate(); + }); + + lengthInput.addEventListener("change", () => { + triggerUpdate(); + }); + + // --- Core Functions --- + + const fetchPrediction = async (prompt, customLength = null) => { + if (isFetching) return; + + isFetching = true; + status.textContent = "Thinking..."; + statusIndicator.classList.add("fetching"); + + const n = parseInt(nGramSelect.value); + const temperature = parseFloat(tempInput.value); + const length = customLength || parseInt(lengthInput.value); + + try { + const response = await fetch("/api/predict", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ prompt, n, temperature, length }), + }); + + if (!response.ok) throw new Error("Network response failed"); + + const data = await response.json(); + + if (customLength) { + insertText(data.prediction || ""); + } else { + currentSuggestion = data.prediction || ""; + updateSuggestion(); + } + } catch (error) { + console.error("Prediction failed:", error); + status.textContent = "Error"; + } finally { + isFetching = false; + status.textContent = "Idle"; + statusIndicator.classList.remove("fetching"); + } + }; + + const updateSuggestion = () => { + const editorText = editor.value; + const space = editorText.length > 0 && !/\s$/.test(editorText) ? " " : ""; + suggestionOverlay.textContent = editorText + space + currentSuggestion; + + // Show/hide accept button + if (currentSuggestion) { + acceptSuggestionBtn.classList.add("visible"); + } else { + acceptSuggestionBtn.classList.remove("visible"); + } + }; + + const insertText = (text) => { + if (!text) return; + const space = + editor.value.length > 0 && !/\s$/.test(editor.value) ? " " : ""; + editor.value += space + text; + currentSuggestion = ""; + updateSuggestion(); + + // Ensure the editor scrolls with content + editor.scrollTop = editor.scrollHeight; + }; + + // --- Event Handlers --- + + editor.addEventListener("input", () => { + clearTimeout(debounceTimer); + currentSuggestion = ""; + updateSuggestion(); + + const prompt = editor.value; + if (prompt.trim().length === 0) return; + debounceTimer = setTimeout(() => fetchPrediction(prompt), 300); + }); + + editor.addEventListener("keydown", (e) => { + if (e.key === "Tab" && currentSuggestion) { + e.preventDefault(); + insertText(currentSuggestion); + fetchPrediction(editor.value); + } + }); + + acceptSuggestionBtn.addEventListener("click", () => { + if (currentSuggestion) { + insertText(currentSuggestion); + fetchPrediction(editor.value); + editor.focus(); + } + }); + + generateBtn.addEventListener("click", () => { + fetchPrediction(editor.value, 50); + closeSidebarOnMobile(); + }); + + // Sync scroll - FIX: Use transform instead of scrollTop + editor.addEventListener("scroll", () => { + suggestionOverlay.style.transform = `translateY(-${editor.scrollTop}px)`; + }); + + // Initialize UI badges + updateUI(); }); diff --git a/ui/style.css b/ui/style.css index 42eb162..e2c025b 100644 --- a/ui/style.css +++ b/ui/style.css @@ -369,7 +369,7 @@ label { border: 1px solid var(--border); border-radius: var(--radius); background-color: var(--card); - overflow-y: auto; + overflow: hidden; } #editor, @@ -402,6 +402,7 @@ label { color: var(--foreground); outline: none; resize: none; + overflow-y: auto; } #suggestion-overlay { @@ -412,4 +413,6 @@ label { color: var(--muted-foreground); pointer-events: none; opacity: 0.5; + overflow: hidden; + transition: transform 0.05s linear; }