Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions changelogs/CHANGELOG-reading-companion-threads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Reading Companion — Non-Modal, Draggable, Multi-Thread Annotation Q&A

- The annotation "Ask AI" thread panel is now **non-modal** — no full-screen backdrop, so the document stays fully readable and scrollable while a conversation is open
- **Multiple threads open in parallel** — each annotation gets its own panel; opening a second no longer closes the first. New panels cascade (28px offset) and a click brings a panel to the front
- **Draggable** — float panels move by their header (new grip handle); a single shared drag controller avoids per-panel listener accumulation
- **Right-side Threads dock** — a panel can dock into a right rail that the document column reflows away from (mirrors the AI-panel reflow), so you can read in the main column and ask in the rail in parallel. Dock shows a live thread count; collapses when empty
- **Dock ↔ float** — any thread toggles between a floating window and a docked card via a header button
- Per-conversation state (streaming, web-search toggle, attachments, model) is now stored **per panel** instead of in module globals, so parallel threads don't clobber each other

---

## Summary

The annotation Q&A panel was a single, anchored modal: it blocked the text behind it, only one could be open at a time, and it couldn't be moved — fighting the reader's task during dense research. This reworks it into a "reading companion": non-modal floating panels you can drag, several at once, plus an optional right-side dock the document reflows around so reading and asking happen in parallel.

---

## 1. Per-Panel State (multi-thread foundation)
**Files:** `js/ai-tags.js`
**What:** Replaced the module-level singletons (`activeThreadPanel`, `threadPanelTagData`, `threadPanelStreaming`, `threadSearchEnabled`, `threadAttachments`) with state stored on each panel element (`panel._tagData`, `_streaming`, `_search`, `_attachments`, `_docked`) and an `openPanels` registry keyed by tag id. `sendThreadMessage(textarea, panel)` and the toolbar handlers now operate on the specific panel.
**Impact:** Many annotation conversations can stream concurrently without interfering. (Note: they share the globally-selected AI model — switching a panel's model switches the active model, since the local inference backend runs one model at a time.)

## 2. Non-Modal Floating Panels + Drag
**Files:** `js/ai-tags.js`, `css/ai-tags.css`
**What:** Removed the `createOverlay()` modal backdrop from the Q&A path (kept only for the legacy non-Q&A info popup). Added a header drag grip, cascade positioning, z-index bring-to-front, and a single shared `mousemove`/`mouseup` drag controller (bound once, not per panel) with an idempotent `makeDraggable` guard.
**Impact:** The document stays readable; panels can be repositioned out of the way; opening a second annotation keeps the first.

## 3. Right-Side Threads Dock
**Files:** `js/ai-tags.js`, `css/ai-tags.css`
**What:** A lazily-created `aside.ai-tag-thread-dock` holds docked panels as stacked cards. `body.ai-tag-dock-open .app-container { width: calc(100% - var(--ai-tag-dock-width)) }` reflows the document so the dock never overlaps text. Header dock-toggle moves a panel between floating and docked; the dock shows a live count and collapses/hides when empty. Full-width on mobile (no reflow).
**Impact:** Read in the main column, ask in the rail — true parallel reading and questioning.

---

## Testing

- Verified live in the browser preview: two annotations → two parallel panels (0 modal overlays); cascade + z-index bring-to-front; header drag moves a panel; dock toggles in/out and reflows the document; close cleans up and restores width.
- Vite build clean; smoke suite 22/22 pass.
- ESLint: no new errors on `ai-tags.js` (the pre-existing `no-useless-assignment` warnings are untouched code).
119 changes: 118 additions & 1 deletion css/ai-tags.css
Original file line number Diff line number Diff line change
Expand Up @@ -511,13 +511,123 @@ body.ai-tags-hidden .ai-tag-pill-anchor {
color: #a78bfa;
}

/* --- Thread Panel Overlay (captures clicks outside) --- */
/* --- Thread Panel Overlay (legacy info popup only — Q&A threads are non-modal) --- */
.ai-tag-thread-overlay {
position: fixed;
inset: 0;
z-index: 10000;
}

/* --- Drag grip + dock toggle in the panel header --- */
.ai-tag-thread-grip {
cursor: grab;
color: #6e7681;
font-size: 13px;
padding: 0 4px 0 0;
display: flex;
align-items: center;
user-select: none;
}
.ai-tag-thread-panel--dragging,
.ai-tag-thread-panel--dragging .ai-tag-thread-grip {
cursor: grabbing;
}
.ai-tag-thread-panel--dragging {
animation: none;
opacity: 0.96;
}
.ai-tag-thread-dock {
background: none;
border: none;
color: #8b949e;
font-size: 13px;
cursor: pointer;
padding: 2px 4px;
margin-right: 2px;
border-radius: 6px;
}
.ai-tag-thread-dock:hover { color: #c9d1d9; background: rgba(110, 118, 129, 0.15); }

/* --- Right-side Threads dock (Stage 2) --- */
:root { --ai-tag-dock-width: 320px; }

.ai-tag-thread-dock {
position: fixed;
top: 0;
right: 0;
width: var(--ai-tag-dock-width);
height: 100vh;
z-index: 9000;
display: flex;
flex-direction: column;
background: rgba(13, 17, 23, 0.98);
border-left: 1px solid rgba(99, 110, 123, 0.3);
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.35);
}
[data-theme="light"] .ai-tag-thread-dock {
background: rgba(250, 251, 252, 0.99);
border-left-color: rgba(0, 0, 0, 0.1);
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.1);
}
.ai-tag-dock-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid rgba(99, 110, 123, 0.2);
flex-shrink: 0;
}
.ai-tag-dock-title { font-size: 13px; font-weight: 500; color: #c9d1d9; }
[data-theme="light"] .ai-tag-dock-title { color: #24292f; }
.ai-tag-dock-count {
display: inline-block;
min-width: 16px;
text-align: center;
font-size: 11px;
background: rgba(139, 92, 246, 0.25);
color: #a78bfa;
border-radius: 8px;
padding: 0 5px;
margin-left: 4px;
}
.ai-tag-dock-collapse {
background: none;
border: none;
color: #8b949e;
cursor: pointer;
font-size: 14px;
padding: 2px 6px;
border-radius: 6px;
}
.ai-tag-dock-collapse:hover { color: #c9d1d9; background: rgba(110, 118, 129, 0.15); }
.ai-tag-dock-body {
flex: 1;
overflow-y: auto;
padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}

/* Docked panels lose their fixed positioning and float-window chrome —
they become stacked cards inside the rail. */
.ai-tag-thread-dock .ai-tag-thread-panel {
position: static !important;
width: 100% !important;
max-height: 420px;
z-index: auto !important;
box-shadow: none;
backdrop-filter: none;
animation: none;
border: 1px solid rgba(99, 110, 123, 0.25);
}
.ai-tag-thread-dock .ai-tag-thread-grip { display: none; }

/* Reflow the document so the dock never overlaps the text (mirrors ai-panel-active) */
body.ai-tag-dock-open .app-container {
width: calc(100% - var(--ai-tag-dock-width));
}

/* --- Responsive: mobile --- */
@media (max-width: 640px) {
.ai-tag-thread-panel {
Expand All @@ -537,4 +647,11 @@ body.ai-tags-hidden .ai-tag-pill-anchor {
.ai-tag-prompt-card {
width: 90vw;
}
/* Dock takes the full width on phones; don't reflow the (already narrow) doc. */
.ai-tag-thread-dock { width: 100vw; }
body.ai-tag-dock-open .app-container { width: 100%; }
.ai-tag-thread-dock .ai-tag-thread-panel {
width: 100% !important;
max-height: none;
}
}
Loading
Loading