← All posts Project

Building Draft: A Hotkey-Driven AI Text Refiner for Windows

Building Draft: a local-first Windows text refiner using Electron, React, Rust, and Groq to clean or translate highlighted text via a global hotkey.

Updated July 24, 2026 10 min read
DraftElectronReactTypeScriptRustGroq APIDesktop DevelopmentWindows
TL;DR
  • Draft intercepts a global hotkey, reads selected text, sends it to Groq, and pastes the result back — all in under 300ms.
  • Electron was chosen over Tauri for its mature clipboard and accessibility-tree ecosystem on Windows.
  • The Rust layer handles only what Node.js can't: reading selected text from the foreground window via Windows COM APIs.
  • Voice mode uses OpenAI Whisper via REST — no local model needed.
Contents

    Draft started as a frustration. I was writing a message in one window, a document in another, and jumping between them to polish the same rough text over and over. I wanted something that would just fix it, right there, without breaking my flow.

    So I built Draft — a small Windows utility that sits in your system tray until you need it. Highlight any text in any app, press Ctrl+Shift+D, and Draft surfaces a refined version in under a second. Press the hotkey again and the cleaned text replaces what you had selected. No window switching. No copy-paste dance.

    This post is a technical walkthrough of how it works.

    What Draft does

    Draft has four core modes:

    • Refine — cleans grammar, tightens phrasing, improves clarity
    • Translate — switches between English and Bengali in either direction
    • Reformat — restructures bullet points, paragraphs, or code snippets
    • Voice — dictates directly to the active window via Whisper transcription

    All modes operate on the currently selected text. The user sees the result in Draft’s floating UI, approves it, and the refined text pastes back to wherever they were working.

    Tech stack

    Frontend  →  React 18 + TypeScript (Vite)
    Shell     →  Electron 33
    AI        →  Groq API (Llama 3 70B / Mixtral 8x7B)
    Voice     →  OpenAI Whisper (via REST)
    Native    →  Rust (via Node.js native addon — napi-rs)
    Packaging →  electron-builder → NSIS installer + portable .exe

    I chose Electron over Tauri for one reason: ecosystem. Draft needed to intercept global keyboard events, read the system clipboard, and paste text back to the previously focused window — reliably, across every version of Windows 10 and 11. Electron’s Node.js runtime has mature, well-tested libraries for all of this. The Rust layer handles only what Node cannot: reading selected text from the foreground window’s accessibility tree using Windows COM APIs.

    The global hotkey

    The interaction chain starts with electron-global-shortcut:

    import { globalShortcut, BrowserWindow } from 'electron'
    
    app.on('ready', () => {
      globalShortcut.register('CommandOrControl+Shift+D', () => {
        captureSelectionAndShow()
      })
    })

    When the hotkey fires, we have about 300ms before the user notices any lag. The chain has to complete within that window:

    1. Save the foreground window handle
    2. Read the clipboard content before we clobber it
    3. Send a synthetic Ctrl+C to copy the selection
    4. Read the new clipboard value (the selected text)
    5. Restore the original clipboard
    6. Show the Draft overlay with the captured text

    Steps 1 and 2–5 use different mechanisms. Saving the foreground window is a Rust native call via napi-rsGetForegroundWindow() from the Win32 API. The clipboard read/write uses Electron’s built-in clipboard module.

    async function captureSelectionAndShow() {
      const prevClip = clipboard.readText()
      
      await nativeLib.sendCtrlC()          // Rust: SendInput() with VK_C
      await sleep(80)                       // wait for clipboard to update
      
      const selected = clipboard.readText()
      clipboard.writeText(prevClip)         // restore previous clipboard
      
      if (!selected.trim()) return          // nothing selected
      showOverlay(selected)
    }

    The sleep(80) is a real wait — clipboard propagation through Win32 is not synchronous. 80ms covers every machine I tested on, including older spinning-disk machines.

    Pasting back

    After the user approves the refined text, Draft writes it to the clipboard and sends a synthetic Ctrl+V to the window that was active before the overlay appeared:

    // src/native/paste.rs (napi-rs export)
    #[napi]
    pub fn paste_to_foreground(hwnd: i64, text: String) -> bool {
        unsafe {
            // Restore focus to the original window
            SetForegroundWindow(hwnd as HWND);
            
            // Small delay to let the window re-focus
            std::thread::sleep(Duration::from_millis(60));
            
            // Send Ctrl+V
            send_key_combo(VK_CONTROL, VK_V)
        }
    }

    The hwnd is the window handle saved at the start of the capture chain. This is why we save it immediately — by the time the overlay is shown and the user clicks “Apply”, the foreground window will have changed to Draft’s own window.

    The AI layer

    Draft calls Groq’s inference API rather than running a model locally. Groq’s response times (typically 200–400ms for a short refinement) are fast enough to feel instant compared to the clipboard capture overhead.

    Each mode maps to a system prompt:

    const SYSTEM_PROMPTS = {
      refine: `You are a concise writing assistant. Fix grammar, tighten phrasing, 
               and improve clarity. Return only the refined text, nothing else.`,
    
      translate: `Detect the language and translate. Bengali → English or English → Bengali.
                  Return only the translated text, nothing else.`,
    
      reformat: `Reformat the input for clarity. Use bullet points if it is a list,
                 prose if it is a paragraph, code blocks if it is code.
                 Return only the reformatted text.`,
    }
    
    async function refine(text: string, mode: keyof typeof SYSTEM_PROMPTS) {
      const resp = await groq.chat.completions.create({
        model: 'llama3-70b-8192',
        messages: [
          { role: 'system', content: SYSTEM_PROMPTS[mode] },
          { role: 'user',   content: text },
        ],
        temperature: 0.3,
        max_tokens: 1024,
      })
      return resp.choices[0].message.content ?? text
    }

    Low temperature (0.3) keeps the output deterministic — the model improves the text rather than rewriting it creatively.

    Voice input

    The voice mode records audio from the default microphone and sends it to Whisper:

    async function transcribeVoice(audioBlob: Blob): Promise<string> {
      const form = new FormData()
      form.append('file', audioBlob, 'audio.webm')
      form.append('model', 'whisper-1')
      form.append('language', 'en')
    
      const resp = await fetch('https://api.openai.com/v1/audio/transcriptions', {
        method: 'POST',
        headers: { Authorization: `Bearer ${OPENAI_KEY}` },
        body: form,
      })
      const data = await resp.json()
      return data.text
    }

    The recording uses the Web Audio API via the Electron renderer process — navigator.mediaDevices.getUserMedia({ audio: true }). Electron handles the microphone permission prompt natively on Windows.

    The overlay UI

    Draft’s window is borderless, transparent, and always-on-top. It appears near the cursor and dismisses on Escape or a click outside:

    const win = new BrowserWindow({
      width: 580,
      height: 420,
      frame: false,
      transparent: true,
      alwaysOnTop: true,
      skipTaskbar: true,
      resizable: false,
      show: false,
      webPreferences: {
        contextIsolation: true,
        preload: path.join(__dirname, 'preload.js'),
      },
    })

    skipTaskbar: true keeps Draft out of the Windows taskbar — it lives in the system tray instead, accessible from the tray icon.

    Packaging and distribution

    npm run make

    electron-builder produces:

    • A signed NSIS installer (Draft-Setup-x.x.x.exe) — 42 MB
    • A portable executable (Draft-x.x.x-portable.exe) — 38 MB

    Both run on Windows 10 v1903+ and Windows 11. The Rust addon is bundled as a prebuilt .node file, compiled for x86_64-pc-windows-msvc.

    Auto-updates are handled by electron-updater pointing at a GitHub Releases endpoint. When a new release is published, Draft checks on startup and installs the update silently in the background.

    What I learned

    Clipboard timing is the hardest part. The sleep(80) after Ctrl+C feels wrong — polling would be cleaner — but Windows does not provide a reliable event for “clipboard updated by another process.” I tried AddClipboardFormatListener but it introduced race conditions. The fixed delay works.

    Keep the AI call out of the critical path. The capture chain (hotkey → clipboard → overlay) has to be fast and synchronous. The AI call happens only after the overlay is visible and the user sees the input text. A spinner for 300ms is invisible; a 300ms delay before the overlay appears feels broken.

    System tray UX matters more than I expected. Users who run Draft all day judge the app almost entirely by how unobtrusively it lives in the tray. No notifications, no startup sound, no update prompts during work hours — these are the things people mention first in feedback.


    Draft is live at draftbangla.vercel.app. If you build something similar or have questions about any part of the implementation, reach out.