Spaces:
Sleeping
Sleeping
| """ | |
| Creative Help - A word processor interface for eliciting text completions from the creative-help model. | |
| Users write freely, then type \\help\\ to trigger generation automatically. | |
| Deploy to Hugging Face Spaces: https://huggingface.co/spaces | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| import re | |
| import gradio as gr | |
| import requests | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # --- Configuration --- | |
| MAX_NEW_TOKENS = 50 | |
| TEMPERATURE = 1.0 | |
| def get_api_config(): | |
| """Get API endpoint and token from environment (Space secrets/variables).""" | |
| endpoint_url = os.getenv("HF_ENDPOINT_URL", "").strip() | |
| token = os.getenv("HF_TOKEN", "").strip() | |
| return endpoint_url, token | |
| def call_creative_help(prompt: str, token: str, url: str) -> dict: | |
| """Call the creative-help model API and return the result.""" | |
| headers = {"Authorization": f"Bearer {token}"} if token else {} | |
| payload = { | |
| "inputs": prompt, | |
| "parameters": { | |
| "max_new_tokens": MAX_NEW_TOKENS, | |
| "temperature": TEMPERATURE, | |
| "do_sample": True, | |
| }, | |
| } | |
| logger.info("API request: url=%s prompt=%r parameters=%s", | |
| url, prompt, payload["parameters"]) | |
| response = requests.post(url, headers=headers, json=payload, timeout=60) | |
| response.raise_for_status() | |
| result = response.json() | |
| logger.info("API response: %s", result) | |
| return result | |
| def extract_prompt_and_trigger(text: str) -> tuple[str, bool]: | |
| """ | |
| If text ends with \\help\\ or /help/, return (text_without_trigger, True). | |
| Otherwise return (text, False). | |
| """ | |
| if not text or not text.strip(): | |
| return text, False | |
| # Match \help\ or /help/ at end (with optional trailing whitespace) | |
| # Use greedy (.*) to preserve whitespace (e.g. paragraph breaks) before trigger | |
| for pattern in (r"(.*)\\help\\\s*$", r"(.*)/help/\s*$"): | |
| match = re.search(pattern, text, re.DOTALL) | |
| if match: | |
| return match.group(1), True | |
| return text, False | |
| def generate_completion(prompt: str) -> tuple[str | None, str]: | |
| """ | |
| Call the API and append generated text to the prompt. | |
| prompt is the text before \\help\\ (trigger already extracted). | |
| Returns (updated_text, status_message). Returns (None, error_msg) on API/config errors. | |
| """ | |
| url, token = get_api_config() | |
| if not url: | |
| return None, "⚠️ Configure HF_ENDPOINT_URL or HF_MODEL_ID in Space Settings → Variables." | |
| if not token: | |
| return None, "⚠️ Configure HF_TOKEN in Space Settings → Secrets." | |
| if not prompt.strip(): | |
| return None, "⚠️ Enter some text first to use as a prompt." | |
| try: | |
| result = call_creative_help(prompt, token, url) | |
| except requests.exceptions.RequestException as e: | |
| logger.exception("API request failed: %s", e) | |
| return None, f"❌ API error: {e}" | |
| # Parse response - handler returns [{"generated_text": "..."}] | |
| if isinstance(result, list) and result and isinstance(result[0], dict): | |
| generated = result[0].get("generated_text", "") | |
| elif isinstance(result, dict): | |
| if "error" in result: | |
| return None, f"❌ {result['error']}" | |
| generated = result.get("generated_text", "") | |
| else: | |
| return None, "❌ Unexpected API response format." | |
| if not generated or not isinstance(generated, str): | |
| return None, "❌ No generated text in response." | |
| # Add single space only when prefix doesn't end with newline (avoids " ") | |
| if prompt and prompt[-1].isspace(): | |
| new_text = prompt + generated.lstrip() | |
| else: | |
| new_text = prompt + generated | |
| return new_text, f"✨ Generated: {generated[:80]}{'...' if len(generated) > 80 else ''}" | |
| CURSOR_JS = """ | |
| (function() { | |
| function findTextarea() { | |
| const app = document.querySelector('gradio-app'); | |
| if (app && app.shadowRoot) { | |
| return app.shadowRoot.querySelector('#story-input textarea'); | |
| } | |
| return document.querySelector('#story-input textarea'); | |
| } | |
| function moveCursorToEnd() { | |
| const ta = findTextarea(); | |
| if (ta && !ta.disabled) { | |
| ta.focus(); | |
| const len = ta.value.length; | |
| ta.setSelectionRange(len, len); | |
| } | |
| } | |
| function observeTextarea() { | |
| const ta = findTextarea(); | |
| if (!ta) return false; | |
| const observer = new MutationObserver(function(mutations) { | |
| mutations.forEach(function(m) { | |
| if (m.attributeName === 'disabled' && !ta.disabled) { | |
| setTimeout(moveCursorToEnd, 0); | |
| } | |
| }); | |
| }); | |
| observer.observe(ta, { attributes: true, attributeFilter: ['disabled'] }); | |
| return true; | |
| } | |
| var attempts = 0; | |
| var id = setInterval(function() { | |
| if (observeTextarea() || ++attempts > 50) clearInterval(id); | |
| }, 100); | |
| })(); | |
| """ | |
| def get_model_repo_url() -> str: | |
| return "https://huggingface.co/roemmele/creative-help" | |
| def get_paper_url() -> str: | |
| """Get the research paper URL for the footer link.""" | |
| url = os.getenv("PAPER_URL", "").strip() | |
| if url: | |
| return url | |
| return "https://roemmele.github.io/publications/creative-help-demo.pdf" | |
| APP_EXPLANATION = ( | |
| "Creative Help is a legacy app for AI-based writing assistance. It was developed in 2016 as one of the first demonstrations of the use of a language model for helping people write stories." | |
| ) | |
| CUSTOM_CSS = """ | |
| .header-row { display: flex !important; align-items: center !important; gap: 0.75rem !important; margin-bottom: 0.25rem !important; } | |
| .header-icons { display: flex !important; gap: 0.5rem !important; } | |
| .header-icon { color: #718096 !important; cursor: pointer !important; transition: color 0.2s !important; } | |
| .header-icon:hover { color: #c53030 !important; } | |
| .header-icon svg { display: block !important; } | |
| .header-icon-help { position: relative !important; } | |
| .header-icon-help .help-tooltip { | |
| visibility: hidden; opacity: 0; position: absolute; left: 50%; transform: translateX(-50%); | |
| top: 100%; margin-top: 8px; padding: 10px 14px; background: #2d3748; color: #fff; | |
| font-size: 0.85rem; line-height: 1.4; border-radius: 8px; width: 280px; text-align: left; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); z-index: 100; transition: opacity 0.2s, visibility 0.2s; | |
| } | |
| .header-icon-help:hover .help-tooltip { visibility: visible; opacity: 1; } | |
| .creative-help-title { font-size: 2rem !important; font-weight: 700 !important; color: #c53030 !important; margin: 0 !important; } | |
| .footer { margin-top: 1rem !important; padding-top: 0.75rem !important; border-top: 1px solid #e2e8f0 !important; } | |
| .footer { display: flex !important; flex-wrap: wrap !important; gap: 1rem !important; } | |
| .footer-link { color: #718096 !important; font-size: 0.875rem !important; text-decoration: none !important; display: inline-flex !important; align-items: center !important; gap: 0.35rem !important; } | |
| .footer-link:hover { color: #c53030 !important; } | |
| .instructions { color: #4a5568; font-size: 0.95rem; margin-bottom: 1rem; } | |
| /* Dim textbox while processing - remove border/outline from textarea and container */ | |
| #story-input textarea:disabled { | |
| opacity: 0.25 !important; | |
| background-color: #e2e8f0 !important; | |
| filter: blur(0.5px); | |
| border: none !important; | |
| box-shadow: none !important; | |
| outline: none !important; | |
| } | |
| #story-input:has(textarea:disabled), | |
| #story-input:has(textarea:disabled) * { | |
| border: none !important; | |
| box-shadow: none !important; | |
| outline: none !important; | |
| } | |
| """ | |
| def create_ui(): | |
| """Build the Creative Help Gradio interface.""" | |
| with gr.Blocks( | |
| title="Creative Help", | |
| js=CURSOR_JS, | |
| theme=gr.themes.Soft(primary_hue="red", secondary_hue="slate"), | |
| css=CUSTOM_CSS, | |
| ) as demo: | |
| model_url = get_model_repo_url() | |
| paper_url = get_paper_url() | |
| header_icons = ( | |
| '<span class="header-icon header-icon-help">' | |
| '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" ' | |
| 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' | |
| '<circle cx="12" cy="12" r="10"/>' | |
| '<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>' | |
| '<span class="help-tooltip">' + | |
| APP_EXPLANATION.replace("<", "<").replace( | |
| ">", ">") + '</span></span>' | |
| ) | |
| gr.HTML( | |
| '<div class="header-row">' | |
| '<h1 class="creative-help-title">Creative Help</h1>' | |
| '<div class="header-icons">' + header_icons + '</div>' | |
| '</div>' | |
| '<p class="instructions">Type <code>\\help\\</code> when you want to generate a suggestion.</p>' | |
| ) | |
| with gr.Row(): | |
| textbox = gr.Textbox( | |
| placeholder="Write your story here... Type \\help\\ when you want a suggestion.", | |
| lines=12, | |
| max_lines=20, | |
| elem_id="story-input", | |
| show_label=False, | |
| ) | |
| def on_text_input(text: str): | |
| prompt, had_trigger = extract_prompt_and_trigger(text or "") | |
| if not had_trigger: | |
| # Use gr.skip() if available (Gradio 5+), else no-op update for Gradio 4 | |
| if hasattr(gr, "skip"): | |
| yield gr.skip() | |
| else: | |
| yield gr.update(value=text) | |
| return | |
| # Preserve text (dimmed) while waiting, then replace with result | |
| yield gr.update(value=text, interactive=False) | |
| new_text, _ = generate_completion(prompt) | |
| yield gr.update(value=new_text if new_text is not None else text, interactive=True) | |
| # Use trigger_mode="always_last" so rapid typing only processes the final value. | |
| textbox.input( | |
| fn=on_text_input, | |
| inputs=[textbox], | |
| outputs=[textbox], | |
| trigger_mode="always_last", | |
| ) | |
| # Footer with model repo and paper links | |
| gr.HTML( | |
| f'<div class="footer">' | |
| f'<a href="{model_url}" target="_blank" rel="noopener" class="footer-link">' | |
| f'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" ' | |
| f'fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>' | |
| f'<polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>' | |
| f' Model on Hugging Face</a>' | |
| f'<a href="{paper_url}" target="_blank" rel="noopener" class="footer-link">' | |
| f'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" ' | |
| f'fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>' | |
| f'<polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>' | |
| f' Research paper</a></div>' | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| demo = create_ui() | |
| demo.launch() | |