OrbitPilot chat widget — streaming, movable, mobile & voice
What it is
OrbitPilot is now a global floating chat widget for logged-in users: a 🪐 button in the corner opens a panel you can use from any page. The panel is non-blocking (keep browsing while it's open), draggable, collapsible, and it remembers its place and your open conversation as you move around the site.
New abilities
- Streaming answers — the reply appears word-by-word instead of after a wait.
- Stop — halt the AI mid-answer. Stopping saves whatever was written so far and does not use up an ask from your monthly allowance.
- Copy / Download the whole chat — buttons at the top and bottom of the panel (download is a .txt transcript).
- Copy a single message — hover any bubble for a copy icon.
- Maximize the window, browse your chat history, and start a New conversation.
- Voice input — tap the microphone and talk; your words are typed for you.
Guests (not logged in) keep the original home-page chat pop-up.
On your phone (mobile)
- The panel docks to the bottom of the screen and never spills past the edges, in any orientation — rotating the phone re-fits it automatically.
- It respects the notch and the home-indicator (safe areas), and uses the live screen height so the browser's address bar can't clip the Send button.
- After you speak and send, the box is emptied for your next question — tap the microphone again to talk for the next turn.
For developers
Streaming requires an async view under ASGI (a sync generator buffers before the
first byte). orbitpilot.views.orbitpilot_ask_stream is an async def returning
StreamingHttpResponse over an async generator that emits Server-Sent Events
(meta → token… → done). All ORM/gating runs in two SYNC helpers
(_prepare_ask_stream / _finalize_ask_stream) via
sync_to_async(thread_sensitive=True). Stop = the client aborts the
fetch → the async generator is cancelled → the partial answer is saved and the quota row is NOT
written. Frontend lives in templates/_orbitpilot_widget.html +
static/css/orbitpilot-widget.css + static/js/orbitpilot-widget.js, included
in base.html for authed users only.
Mobile responsiveness (Workstream E): the whole widget is
box-sizing: border-box so the 1px border counts inside the width (the old iPhone
overflow was width:100vw + border). The @media (max-width:575px) rule drops
100vw for left/right: env(safe-area-inset-*) + width:auto, sizes
with dvh (with a vh fallback), and pads the composer by
env(safe-area-inset-bottom). In JS, a saved desktop drag position is not applied
on a mobile viewport, and a resize/orientationchange listener re-clamps the
position via applyPos(). Voice-clear: the speech module keeps a finalized
saved buffer separate from the input; send() now calls a shared
resetVoice() (stop recognition, clear the silence timer, blank saved) right
where it clears input.value, so the next turn starts empty.
Chat font size + mobile Send fix (2026-07-27)
- A− / A+ in the widget header shrink/grow the chat text. Your choice is remembered
(saved in the browser via
localStoragekeyop_widget_font) across pages and sessions. Size is clamped to 12–22px. - Send button on phones: the composer row now keeps the Send (and mic) buttons visible next to the text box on narrow screens — previously the text box could push Send off the right edge.
For developers: markup templates/_orbitpilot_widget.html, behavior
static/js/orbitpilot-widget.js (applyFont()/bumpFont(), restored on init), styling
static/css/orbitpilot-widget.css. The buttons bump a --op-chat-font CSS var read by the
message body + input; the composer uses min-width:0 on the textarea and flex:0 0 auto on
the buttons so Send never gets shoved off-screen.
Hamburger menu + full-page mode (2026-07-27)
The chat widget header now has a ☰ menu and a ⤢ full-page button:
- Your instructions — standing notes the AI always considers (like ChatGPT custom
instructions). Saved to
OrbitPilotSettings.custom_user_promptviaorbitpilot_custom_instructions; plan-gated by thecustom_promptfeature (a locked message + upgrade link appears if the plan doesn't allow it; the server also enforces it with a 403). - Downloads — now clearly two options: “This conversation only”
(
orbitpilot_conversation_download) vs “All my chat history” (neworbitpilot_history_download). The toolbar button is relabelled “⭳ This chat”. - Memory → Rebuild my memory now — a full rebuild
(
orbitpilot_rebuild_memory) behind a confirm cost-warning (it re-reads everything and re-embeds, which uses AI credits); it won't stack a rebuild while one is already running.
Full-page mode (⤢) opens /orbitpilot/chat/ in a new tab, a ChatGPT/Gemini-style
full-screen chat that reuses the SAME widget (via body class op-fullpage) — so switching between the
small widget and the full page keeps the same conversation (shared op_widget_conv localStorage +
a storage event listener). Also: A− / A+ change the chat font (remembered), and on a
phone the Send button stays visible next to the mic.
For developers: markup templates/_orbitpilot_widget.html, behavior
static/js/orbitpilot-widget.js, styling static/css/orbitpilot-widget.css; full-page view
orbitpilot.views.orbitpilot_chat → orbitpilot/templates/orbitpilot/chat_full.html.
Quick update vs Full rebuild (soft/hard memory, 2026-07-27)
The widget's 🧠 Memory menu now offers two rebuilds:
- ⚡ Quick update (soft) — re-reads and re-embeds only the items you've changed since the last build; unchanged items keep their existing embeddings, removed items are dropped. Much cheaper, because the OpenAI embedding cost scales with the delta, not the whole corpus.
- 🔄 Full rebuild (hard) — re-reads and re-embeds everything from scratch (uses more AI credits). Use it only if something looks wrong.
For developers: run_memory_build(user, settings, build, incremental=True) diffs the new
chunks against the stored ones by the (source_key, source_id, chunk_index, content_hash) key
before calling embed_chunks, so only new/changed chunks are embedded; stale rows are
deleted in the same atomic block; unchanged rows are left untouched. The mode is carried on
OrbitPilotMemoryBuild.incremental (migration orbitpilot/0025) and set by
orbitpilot_rebuild_memory from mode=soft|hard. If incremental is omitted,
run_memory_build reads it from the build record. The full/hard path keeps the exact prior atomic
delete-all + re-embed behaviour.
Chat-driven fixes (2026-07-28)
Analysing real (PII-redacted) OrbitPilot conversations surfaced four fixes:
- Broken AI links (
your-app-link): the model invented a domain when handed relative paths from a stale booklet. Now (a) a System Health check warns ifSITE_URLis localhost in production, and (b)orbitpilot.services.heal_links()rewrites any placeholder/localhost link base to the real site host in every answer (stream/blocking/guest) — so links work even before a rebuild. The permanent fix (setSITE_URL+ rebuild) is on the Admin Setup Checklist. - “List my Winks” now works:
pipeline/retrieve.py add_list_intent_sources()injects ALL of a source's chunks when the query is an enumerate request ("list/show/all my winks/tasks/events"), and the workspace snapshot samples Wink titles — so the AI lists actual items, not just the count. - Clickable next-actions: actions are now
{label, url}. A real navigation link renders as a clickable button (opens in a new tab); otherwise it's a pre-fill chip. Fixed across the widget, the guest home modal (which previously dropped actions), and the full-page result. Inline answer links also open in a new tab now. - Prompt polish: the core answer prompt stops over-repeating the user's headline goal and handles "what changed since yesterday" gracefully (it works from the current snapshot, not day-over-day history).
For developers: orbitpilot/services.py (heal_links, request_base,
split_answer_actions now dict-returning, guest next_actions layer),
pipeline/retrieve.py (add_list_intent_sources), adminapp/diagnostics.py
(_check_site_url), widget JS + home.html (action render + DOMPurify target=_blank hook),
aihub/prompt_registry.py (core + next_actions defaults; refreshed on untouched seeded Defaults via
aihub/0007).