autocomplete_gradio_textbox / autocomplete_textbox.py
ysharma's picture
ysharma HF Staff
Create autocomplete_textbox.py
39f3819 verified
import gradio as gr
class AutocompleteTextbox(gr.HTML):
"""A textbox with AI-powered autocomplete suggestions, similar to Gmail smart compose."""
def __init__(
self,
value="",
label="Autocomplete Textbox",
placeholder="Start typing...",
**kwargs,
):
html_template = """
<div class="act-container">
<div class="act-settings">
<div class="act-settings-row">
<div class="act-field act-field-token">
<label class="act-label">HF Token</label>
<input type="password" class="act-token" placeholder="hf_..." />
</div>
<div class="act-field act-field-model">
<label class="act-label">Model</label>
<select class="act-model">
<option value="Qwen/Qwen2.5-7B-Instruct">Qwen 2.5 7B</option>
<option value="Qwen/Qwen2.5-Coder-7B-Instruct">Qwen 2.5 Coder 7B</option>
<option value="meta-llama/Llama-3.1-8B-Instruct">Llama 3.1 8B</option>
<option value="mistralai/Mistral-7B-Instruct-v0.3">Mistral 7B</option>
</select>
</div>
<div class="act-status-wrap">
<span class="act-status"></span>
</div>
</div>
</div>
<div class="act-editor-wrap">
<div class="act-backdrop"><span class="act-mirror"></span><span class="act-ghost"></span></div>
<textarea class="act-textarea" rows="12" spellcheck="true"></textarea>
</div>
<div class="act-footer">
<span class="act-hint">Press <kbd>Tab</kbd> to accept &middot; <kbd>Esc</kbd> to dismiss</span>
<span class="act-charcount"></span>
</div>
</div>
"""
css_template = """
.act-container {
border: 1px solid var(--border-color-primary, #d1d5db);
border-radius: 8px;
overflow: hidden;
font-family: var(--font, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
background: var(--background-fill-primary, #ffffff);
}
/* Settings bar */
.act-settings {
padding: 8px 12px;
background: var(--background-fill-secondary, #f9fafb);
border-bottom: 1px solid var(--border-color-primary, #d1d5db);
}
.act-settings-row {
display: flex;
align-items: flex-end;
gap: 12px;
flex-wrap: wrap;
}
.act-field {
display: flex;
flex-direction: column;
gap: 3px;
}
.act-field-token { flex: 1; min-width: 180px; }
.act-field-model { min-width: 160px; }
.act-label {
font-size: 11px;
font-weight: 600;
color: var(--body-text-color-subdued, #6b7280);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.act-token, .act-model {
padding: 6px 10px;
border: 1px solid var(--border-color-primary, #d1d5db);
border-radius: 6px;
font-size: 13px;
background: var(--background-fill-primary, #fff);
color: var(--body-text-color, #1f2937);
outline: none;
transition: border-color 0.15s;
}
.act-token:focus, .act-model:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59,130,246,0.12);
}
.act-status-wrap {
display: flex;
align-items: center;
padding-bottom: 4px;
}
.act-status {
font-size: 12px;
color: var(--body-text-color-subdued, #9ca3af);
white-space: nowrap;
}
.act-status.loading { color: #f59e0b; }
.act-status.ready { color: #10b981; }
.act-status.error { color: #ef4444; }
/* Editor area */
.act-editor-wrap {
position: relative;
margin: 0;
}
.act-backdrop {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
padding: 12px 16px;
font-family: var(--font, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
font-size: 15px;
line-height: 1.7;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
overflow: hidden;
pointer-events: none;
z-index: 0;
box-sizing: border-box;
}
.act-mirror {
color: transparent;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.act-ghost {
color: #9ca3af;
pointer-events: none;
}
.act-textarea {
display: block;
width: 100%;
min-height: 250px;
padding: 12px 16px;
font-family: var(--font, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
font-size: 15px;
line-height: 1.7;
color: var(--body-text-color, #1f2937);
background: transparent;
border: none;
outline: none;
resize: vertical;
position: relative;
z-index: 1;
caret-color: var(--body-text-color, #1f2937);
box-sizing: border-box;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.act-textarea::placeholder {
color: var(--body-text-color-subdued, #9ca3af);
}
/* Footer */
.act-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 12px;
background: var(--background-fill-secondary, #f9fafb);
border-top: 1px solid var(--border-color-primary, #d1d5db);
}
.act-hint {
font-size: 12px;
color: var(--body-text-color-subdued, #9ca3af);
}
.act-hint kbd {
display: inline-block;
padding: 1px 5px;
font-size: 11px;
font-family: var(--font-mono, monospace);
background: var(--background-fill-primary, #fff);
border: 1px solid var(--border-color-primary, #d1d5db);
border-radius: 3px;
box-shadow: 0 1px 0 var(--border-color-primary, #d1d5db);
}
.act-charcount {
font-size: 12px;
color: var(--body-text-color-subdued, #9ca3af);
}
"""
js_on_load = """
(function() {
var textarea = element.querySelector('.act-textarea');
var backdrop = element.querySelector('.act-backdrop');
var mirror = element.querySelector('.act-mirror');
var ghost = element.querySelector('.act-ghost');
var tokenInput = element.querySelector('.act-token');
var modelSelect = element.querySelector('.act-model');
var status = element.querySelector('.act-status');
var charcount = element.querySelector('.act-charcount');
var currentSuggestion = '';
var debounceTimer = null;
var abortController = null;
var DEBOUNCE_MS = 600;
/* Placeholder */
if (props.placeholder) {
textarea.setAttribute('placeholder', props.placeholder);
}
/* Initial content */
if (props.value) {
textarea.value = props.value;
updateMirror();
}
/* --- Mirror: keep backdrop text in sync --- */
function updateMirror() {
/* Escape HTML entities in the textarea value */
var text = textarea.value;
var escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
mirror.innerHTML = escaped;
charcount.textContent = text.length + ' chars';
}
/* --- Suggestion display --- */
function showSuggestion(text) {
currentSuggestion = text;
ghost.textContent = text;
}
function clearSuggestion() {
currentSuggestion = '';
ghost.textContent = '';
}
/* --- Status display --- */
function setStatus(msg, cls) {
status.textContent = msg;
status.className = 'act-status' + (cls ? ' ' + cls : '');
}
/* --- Sync value to Gradio --- */
function syncValue() {
var text = textarea.value;
if (text !== props.value) {
props.value = text;
}
}
/* --- Scroll sync: keep backdrop scrolled with textarea --- */
textarea.addEventListener('scroll', function() {
backdrop.scrollTop = textarea.scrollTop;
backdrop.scrollLeft = textarea.scrollLeft;
});
/* --- API call --- */
function fetchSuggestion() {
var text = textarea.value;
var token = tokenInput.value.trim();
var model = modelSelect.value;
if (!token) {
setStatus('Enter HF token', '');
return;
}
if (!text || text.length < 5) {
clearSuggestion();
return;
}
/* Cancel previous request */
if (abortController) {
abortController.abort();
}
abortController = new AbortController();
setStatus('Thinking...', 'loading');
var lastChunk = text;
if (text.length > 1000) {
lastChunk = text.slice(-1000);
}
var body = JSON.stringify({
model: model,
messages: [
{
role: 'system',
content: 'You are an autocomplete assistant. The user is writing text and you must predict what comes next. Output ONLY the continuation text (1-2 short sentences max). Do not repeat any of the existing text. Do not add quotes or explanations.'
},
{
role: 'user',
content: lastChunk
}
],
max_tokens: 60,
temperature: 0.3,
stream: false
});
fetch('https://router.huggingface.co/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: body,
signal: abortController.signal
})
.then(function(resp) {
if (!resp.ok) {
return resp.json().then(function(data) {
throw new Error(data.error || 'HTTP ' + resp.status);
});
}
return resp.json();
})
.then(function(data) {
var content = data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content;
if (content) {
/* Clean up: remove leading whitespace only if the text doesn't end with space */
var trimmed = content;
if (text.endsWith(' ') || text.endsWith('\\n')) {
trimmed = content.replace(/^\\s+/, '');
} else if (!content.startsWith(' ')) {
trimmed = ' ' + content;
}
/* Remove any repeated text from the end of input */
var overlap = findOverlap(text, trimmed);
if (overlap > 0) {
trimmed = trimmed.slice(overlap);
}
if (trimmed && textarea.value === text) {
showSuggestion(trimmed);
setStatus('Suggestion ready', 'ready');
}
} else {
setStatus('No suggestion', '');
}
})
.catch(function(err) {
if (err.name !== 'AbortError') {
setStatus('Error: ' + err.message.slice(0, 50), 'error');
console.error('Autocomplete error:', err);
}
});
}
/* Find overlap between end of text and start of suggestion */
function findOverlap(text, suggestion) {
var maxCheck = Math.min(text.length, suggestion.length, 50);
for (var i = maxCheck; i > 0; i--) {
if (text.slice(-i) === suggestion.slice(0, i)) {
return i;
}
}
return 0;
}
/* --- Input handler --- */
textarea.addEventListener('input', function() {
updateMirror();
clearSuggestion();
syncValue();
/* Debounce API call */
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(fetchSuggestion, DEBOUNCE_MS);
});
/* --- Keydown: Tab to accept, Esc to dismiss --- */
textarea.addEventListener('keydown', function(e) {
if (e.key === 'Tab' && currentSuggestion) {
e.preventDefault();
textarea.value += currentSuggestion;
clearSuggestion();
updateMirror();
syncValue();
setStatus('Accepted', 'ready');
/* Move cursor to end */
textarea.selectionStart = textarea.value.length;
textarea.selectionEnd = textarea.value.length;
return;
}
if (e.key === 'Escape' && currentSuggestion) {
e.preventDefault();
clearSuggestion();
setStatus('Dismissed', '');
return;
}
});
/* --- Focus/blur --- */
textarea.addEventListener('focus', function() {
if (textarea.value.length >= 5 && !currentSuggestion && tokenInput.value.trim()) {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(fetchSuggestion, DEBOUNCE_MS);
}
});
/* --- Token input: show status --- */
tokenInput.addEventListener('input', function() {
if (tokenInput.value.trim()) {
setStatus('Token set', 'ready');
} else {
setStatus('Enter HF token', '');
}
clearSuggestion();
});
/* --- Initial mirror update --- */
updateMirror();
setStatus('Enter HF token to enable', '');
})();
"""
super().__init__(
value=value,
label=label,
html_template=html_template,
css_template=css_template,
js_on_load=js_on_load,
apply_default_css=False,
placeholder=placeholder,
container=False,
padding=False,
**kwargs,
)
def api_info(self):
return {"type": "string", "description": "Text content of the autocomplete textbox"}
if __name__ == "__main__":
with gr.Blocks(title="Autocomplete Textbox Demo") as demo:
gr.Markdown("# AI Autocomplete Textbox")
gr.Markdown(
"A textbox with AI-powered autocomplete suggestions (like Gmail Smart Compose). "
"Enter your HF token, pick a model, and start typing. "
"Press **Tab** to accept a suggestion, **Esc** to dismiss."
)
editor = AutocompleteTextbox(
value="",
label="",
placeholder="Start typing here... suggestions will appear in gray after you pause.",
)
with gr.Row():
get_text_btn = gr.Button("Get Text", variant="primary")
clear_btn = gr.Button("Clear", variant="secondary")
text_output = gr.Textbox(label="Output", lines=5, interactive=False)
get_text_btn.click(fn=lambda x: x, inputs=editor, outputs=text_output)
clear_btn.click(fn=lambda: "", outputs=editor)
editor.change(fn=lambda x: x, inputs=editor, outputs=text_output)
demo.launch()