mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-23 04:04:27 +08:00
df86d52594bb5ce3dec8a9bfcb45641d93e859c4
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
440ff0c342 |
fix(editor): resolve UX inconsistencies in the AI chat interface (#14850)
# Closes #14189. Fixes the three UX issues reported in the original bug report, plus one small adjacent polish on the right-sidebar toggle that was requested during review. Each concern in the issue is addressed end-to-end, with the same treatment applied to both places the AI chat panel lives: the **sidebar chat panel** (right panel on a doc page) and the **standalone `/chat` page**. --- ## 1. `+` button → persistent multi-session tabs (issue point 1) **Before:** clicking `+` called `createFreshSession()` (standalone) or `newSession()` (sidebar), both of which tore down the current chat content and replaced it in place. There was no way to keep two chats open at once. **After:** a browser/IDE-style tab strip lives above the chat content. Each open session gets its own tab with a close `×`; the active tab is highlighted; `+` now adds a tab rather than replacing the chat. ### Details - New Lit component `ai-chat-tabs` ([packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-tabs.ts](packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-tabs.ts)). - Tab title is derived from `session.title` → first user message → `"New chat"`. - Horizontal scroll when tabs overflow, with a `wheel` handler that converts mouse wheel / trackpad vertical swipe into horizontal scroll (native horizontal trackpad swipes also work natively via `overflow-x: auto`). - Auto `scrollIntoView({ inline: 'nearest' })` on active tab change, so a newly created or newly selected tab slides into view instead of staying hidden behind the toolbar. - Close `×` removes the tab from the strip but leaves the session on the server (matches the existing **Chat history** dropdown semantics — the session is still reachable there). Closing the active tab switches to an adjacent one; closing the last tab starts a fresh session. - Persistence: open session IDs are saved per-workspace in `localStorage` under `ai-chat-open-tabs:{workspaceId}`. On mount, the React pages hydrate those IDs via `AIProvider.session.getSession` / `CopilotClient.getSession` — no new backend or schema work. - Wiring: identical effects on both variants ([chat.tsx (sidebar)](packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx) and [chat/index.tsx (standalone)](packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx)) — hydrate → sync active session into tabs → persist. - The tab strip sits on the same row as the existing toolbar icons (pin / history / `+`), separated by `flex: 1` + `min-width: 0` so the tabs scroll cleanly up to the toolbar boundary. - The `ShadowlessElement` base class injects its static CSS globally, and the `:host` selector does not match in a React-rooted DOM — the component uses tag-selector CSS (`ai-chat-tabs { display: flex; … }`) instead. ## 2. Drag-and-drop attachments (issue point 2) **Before:** the chat input accepted no DnD. Attaching anything required the `+` → file-picker flow. **After:** the chat input accepts OS files via native HTML5 DnD and AFFiNE documents via the repo's existing pragmatic-drag-and-drop infrastructure. ### Details - Native handlers (`dragenter/over/leave/drop`) on [ai-chat-input.ts](packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts) accept OS files: images go into the image preview grid, other files become attachment chips, with the same 50 MB per-file cap as the `+` picker. - Internal AFFiNE document drags from the nav panel land as doc chips, handled via `dropTargetForElements` from `@atlaskit/pragmatic-drag-and-drop` (same library the rest of the app already uses for internal DnD). - A "Drop to attach" overlay appears during drag, reusing the existing focused-border token (`--affine-v2-layer-insideBorder-primaryBorder`) for visual consistency with the focused state. - The image/file routing logic that previously lived inline in `add-popover.ts` was factored into a shared helper [attachment-utils.ts](packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/attachment-utils.ts) (`addFilesToChat`), so the `+` picker and the drop handler stay in lockstep. - Analytics: extended the `addEmbeddingDoc.control` union in [events.ts](packages/frontend/track/src/events.ts) with `'dragDrop'` so drag-originated attachments are distinguishable from button-initiated ones in telemetry. - `@atlaskit/pragmatic-drag-and-drop` is promoted from a transitive dependency (via `@affine/component`) to a direct dependency of `@affine/core` and `yarn.lock` is refreshed accordingly. ## 3. Chat-history tooltip + icon (issue point 3) **Before:** hovering the chat-history button showed a tooltip whose background did not invert for dark theme (`--affine-tooltip` is not theme-aware), and the icon was `ArrowDownSmallIcon` — a chevron that does not convey "history." **After:** the tooltip primitive itself is theme-aware (every tooltip in the app benefits, not just the chat one), and the icon is the semantically-clear `HistoryIcon`. ### Details - [tooltip.ts](blocksuite/affine/components/src/tooltip/tooltip.ts) now uses `var(--affine-v2-tooltips-background, var(--affine-tooltip))` and `var(--affine-v2-tooltips-foreground, var(--affine-white))`. The V2 tokens auto-invert with theme; the old vars remain as fallbacks so components that override via the existing `tooltipStyle` escape hatch continue to work. - Triangle arrow colors updated to use the same V2 token. - [ai-chat-toolbar.ts](packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts): `ArrowDownSmallIcon` → `HistoryIcon`; added `data-testid="ai-panel-chat-history"` for future e2e coverage. ## 4. Right-sidebar toggle: tooltips + open-state icon *(adjacent polish)* Not part of the original issue, but surfaced while testing the tab strip — neither of the two right-sidebar toggle buttons had hover affordance, and both used the same icon regardless of the sidebar's state. - Added `tooltip="Open sidebar"` on the route-container button shown when the sidebar is hidden. - Added `tooltip="Close sidebar"` on the sidebar-header button shown when the sidebar is expanded. - The close button now renders a small inline `RightSidebarOpenIcon` variant: same outline as `RightSidebarIcon`, but with the right panel filled in the AFFiNE accent color to convey the open state. Icon shape change is self-contained — no new icon asset added to `@blocksuite/icons`. --- ## Commits - `2adc0c7` — fix(ai-chat): theme-aware tooltip + semantic chat-history icon *(2 files)* - `bf26974` — feat(ai-chat): drag-and-drop file and doc attachments in chat input *(7 files)* - `fca29c8` — feat(ai-chat): persistent multi-session tab strip *(8 files)* - `7d5dffe` — feat(workbench): tooltips and open-state icon for the right-sidebar toggle *(2 files)* Kept ordered smallest → largest blast radius so the history is easy to bisect. --- ## Test plan Verified locally against a fresh server stack (postgres / redis / mailpit via compose, migrations run) signed in as `dev@affine.pro`, in both `/chat` and the sidebar chat on a doc page, in light and dark themes: - [x] Tooltip: hover the chat-history icon in dark mode → tooltip is dark-on-light; toggle to light mode → tooltip is light-on-dark. Existing tooltips on other surfaces (slash menu, edgeless, linked-doc) still render correctly. - [x] Icon: chat-history button renders the history glyph (clock), not a chevron. - [x] Drag-and-drop (OS file): drop a PDF / PNG / TXT onto the input → overlay shows → chips/images appear; file > 50 MB → rejected silently (same as `+` picker). - [x] Drag-and-drop (internal doc): drag an AFFiNE doc from the nav panel → becomes a doc chip. - [x] Pin-picker, `+` picker, paste-image — all unchanged. - [x] Tab strip: first chat auto-becomes a tab on first message; `+` adds tab; click tab switches chat; `×` removes tab and switches to adjacent; close last tab → new fresh tab spawns. - [x] Reload browser → tab strip rehydrates from localStorage with the same sessions. - [x] Tab overflow: 12+ tabs → horizontal scroll via trackpad vertical swipe, trackpad horizontal swipe, and mouse wheel; active tab auto-scrolls into view on `+` click. - [x] Right-sidebar: hover both toggle buttons → tooltips appear; open the sidebar → close button shows the filled right-panel icon. - [x] `yarn lint:ox` and lint-staged both clean on every commit. Not verified locally (no local model key configured): the assistant actually streams a response. Drop/chip flow is independent of that path. ## Out of scope / follow-ups - No new unit or Playwright tests — the fixes are visually verifiable and reuse existing reducer / state paths. Happy to add tests if reviewers prefer. - `@affine/native` is not required for the web dev stack; I only built `@affine/server-native`. Irrelevant to the PR diff. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Multi-tab chat UI with a tabs component, open/close/switch actions, and per-workspace persistence/restoration. * Drag-and-drop attachments into chat input (files and docs). * **UI/UX** * Tooltip theming moved to v2 variables (includes arrow color). * Sidebar toggle/close buttons now show tooltips. * “Drop to attach” overlay and updated history icon. * **Behavior** * Unified attachment handling with 50MB validation and toast notices. * **Analytics** * Attachment events record drag-and-drop as a control method. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com> |
||
|
|
7046ad7bf4 |
fix(editor): align selection/handle/remote/text overlays with blocks (#14862)
# Closes #14855. ## The bug When an `affine:embed-synced-doc` is placed on an edgeless canvas and resized which sets `model.props.scale` to a value ≠ 1 - the block-selection frame rendered **inside** that embedded editor is drawn offset from the actual block boundary. The reporter hit this in Safari, but the root cause is platform-independent.  ## Root cause `affine-embed-edgeless-synced-doc-block` applies `transform: scale(modelScale)` to its `.affine-embed-synced-doc-container` so the embedded editor visually fits inside its edgeless xywh ([embed-edgeless-synced-doc-block.ts#L48-L58](https://github.com/toeverything/AFFiNE/blob/canary/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/embed-edgeless-synced-doc-block.ts#L48-L58)). The inner `Viewport` exposes that outer scale as `viewScale = boundingClientRect.width / offsetWidth`. PR #14015 and PR #14074 already taught the surface canvas and `GfxBlockComponent.getCSSTransform` to compensate by dividing by `viewScale`. But several selection-related overlays that render inside the same scaled container were **not** updated in those PRs. They either: - read `viewport.toViewCoord(x, y)` - which returns `(x - viewportX) * zoom * viewScale` and drop the result into CSS `left` / `top` inside the scaled container, or - hand-build a `translate(translateX, translateY) scale(zoom)` transform without `viewScale` compensation. The outer CSS `scale(viewScale)` then re-applies the scale, leaving the overlays one factor of `viewScale` away from their blocks. That's exactly the misalignment in the screenshot - the rect's size looks right but its position is offset. ## The fix Mirror the pattern shipped in #14074 everywhere the inner overlays are placed: - position: `(model - viewportX) * zoom / viewScale` - transform scale: `zoom / viewScale` - translate: `translateX / viewScale, translateY / viewScale` This keeps the overlays in the same reference frame as `GfxBlockComponent.getCSSTransform` so they line up with the block they're framing. When `viewScale === 1` (normal edgeless canvas, outside any embed) every `/ viewScale` is a no-op and behaviour is unchanged. ## Why this is safe - When `viewScale === 1` - every existing caller outside `embed-edgeless-synced-doc` - the math reduces to the original expression byte-for-byte. - The fix strictly mirrors the invariant already adopted by `GfxBlockComponent.getCSSTransform` in #14074. It's the same division by `viewScale` applied in the same place. - No public API, type, or DOM structure changed. ## Scope / known limitations - The `Viewport._cachedBoundingClientRect` cache is only invalidated by its own `ResizeObserver` ([viewport.ts#L500-L505](https://github.com/toeverything/AFFiNE/blob/canary/blocksuite/framework/std/src/gfx/viewport.ts#L500-L505)). A CSS-transform change on an ancestor (e.g. the user panning/zooming the outer edgeless canvas) does not fire it, so in theory `viewScale` can go stale between outer-viewport updates. In practice this hasn't come up in repro - the inner viewport's shell is observed and fires whenever layout shifts. If it turns out to matter I'm happy to add a `viewport.onResize()` refresh hook off the existing `GfxViewportInitializer` in a follow-up. - No integration test added - the existing `blocksuite/integration-test/edgeless/` suite has no `embed-synced-doc` harness. Adding one is a larger scope; can follow up if requested. ## Test plan - [x] `yarn typecheck` - passes - [x] `yarn lint:ox` - `0 warnings, 0 errors` - [x] `yarn prettier --write` on the 5 touched files - no changes - [ ] Manual: on canary, create an edgeless canvas, drop an embed-synced-doc, resize with `Shift` held so `model.props.scale` ≠ 1, select any block inside, and verify the blue selection frame sits flush with the block's boundary (confirm on Safari, Chrome, Firefox). - [ ] Regression check: on a normal edgeless canvas (no embed), verify element selection, drag handle, and text/shape inline editors still render correctly (these code paths hit `viewScale === 1` and should be unchanged). ## Related PRs - #14015 - fixed surface canvas at non-1 `viewScale`. - #14074 - fixed `GfxBlockComponent.getCSSTransform` at non-1 `viewScale`. This PR completes that series by covering the selection overlays. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed positioning and scaling of inline text editors, selection rectangles, drag handles, and remote cursors so overlays and editors remain correctly aligned and sized when the viewport uses an additional outer scale/transform during zooming and panning. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |