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>
This commit is contained in:
Adarsh Singh
2026-05-07 01:34:43 +05:30
committed by GitHub
parent eb9cc22502
commit 440ff0c342
20 changed files with 781 additions and 40 deletions
@@ -1,4 +1,3 @@
import { toast } from '@affine/component';
import type { TagMeta } from '@affine/core/components/page-list';
import type { CollectionMeta } from '@affine/core/modules/collection';
import track, { type EventArgs } from '@affine/track';
@@ -22,6 +21,7 @@ import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import type { SearchMenuConfig } from '../ai-chat-add-context';
import { addFilesToChat } from './attachment-utils';
import type { ChatChip, DocDisplayConfig } from './type';
enum AddPopoverMode {
@@ -172,23 +172,10 @@ export class ChatPanelAddPopover extends SignalWatcher(
if (!files || files.length === 0) return;
this.abortController.abort();
const images = files.filter(file => file.type.startsWith('image/'));
if (images.length > 0) {
this.addImages(images);
}
const others = files.filter(file => !file.type.startsWith('image/'));
const addChipPromises = others.map(async file => {
if (file.size > 50 * 1024 * 1024) {
toast(`${file.name} is too large, please upload a file less than 50MB`);
return;
}
await this.addChip({
file,
state: 'processing',
});
await addFilesToChat(files, {
addImages: this.addImages,
addChip: this.addChip,
});
await Promise.all(addChipPromises);
this._track('file');
};
@@ -0,0 +1,36 @@
import { toast } from '@affine/component';
import type { ChatChip } from './type';
const MAX_ATTACHMENT_SIZE = 50 * 1024 * 1024;
export interface AttachmentHandlers {
addImages: (images: File[]) => void;
addChip: (chip: ChatChip, silent?: boolean) => Promise<void>;
}
export async function addFilesToChat(
files: File[],
{ addImages, addChip }: AttachmentHandlers
): Promise<void> {
if (!files.length) return;
const images = files.filter(file => file.type.startsWith('image/'));
if (images.length > 0) {
addImages(images);
}
const others = files.filter(file => !file.type.startsWith('image/'));
await Promise.all(
others.map(async file => {
if (file.size > MAX_ATTACHMENT_SIZE) {
toast(`${file.name} is too large, please upload a file less than 50MB`);
return;
}
await addChip({
file,
state: 'processing',
});
})
);
}
@@ -1,2 +1,3 @@
export * from './attachment-utils';
export * from './type';
export * from './utils';
@@ -9,6 +9,9 @@ import type {
} from '@affine/core/modules/cloud';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import track, { type EventArgs } from '@affine/track';
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import type { EditorHost } from '@blocksuite/affine/std';
@@ -26,6 +29,7 @@ import { reportResponse } from '../../utils/action-reporter';
import { readBlobAsURL } from '../../utils/image';
import { mergeStreamObjects } from '../../utils/stream-objects';
import type { SearchMenuConfig } from '../ai-chat-add-context';
import { addFilesToChat } from '../ai-chat-chips/attachment-utils';
import type { ChatChip, DocDisplayConfig } from '../ai-chat-chips/type';
import { isDocChip } from '../ai-chat-chips/utils';
import {
@@ -257,6 +261,31 @@ export class AIChatInput extends SignalWatcher(
user-select: none;
}
.chat-panel-input[data-drag-over='true'] {
--input-border-width: 1px;
--input-border-color: var(--affine-v2-layer-insideBorder-primaryBorder);
background-color: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
}
.chat-panel-input-drop-overlay {
position: absolute;
inset: 0;
pointer-events: none;
border-radius: inherit;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 500;
color: ${unsafeCSSVarV2('icon/activated')};
background-color: color-mix(
in srgb,
var(--affine-v2-layer-background-primary) 92%,
transparent
);
z-index: 1;
}
.chat-panel-send {
display: flex;
justify-content: center;
@@ -326,6 +355,16 @@ export class AIChatInput extends SignalWatcher(
@state()
accessor focused = false;
@state()
accessor isDragOver = false;
@query('.chat-panel-input')
accessor chatPanelInput!: HTMLDivElement;
private _dragEnterCounter = 0;
private _internalDropCleanup: (() => void) | null = null;
@property({ attribute: false })
accessor chatContextValue!: AIChatInputContext;
@@ -434,6 +473,18 @@ export class AIChatInput extends SignalWatcher(
}
})
);
this.updateComplete
.then(() => {
if (this.isConnected && !this._internalDropCleanup) {
this._setupInternalDropTarget();
}
})
.catch(console.error);
window.addEventListener('dragleave', this._handleWindowDragLeave);
window.addEventListener('drop', this._resetDragState);
window.addEventListener('dragend', this._resetDragState);
}
protected override firstUpdated(changedProperties: PropertyValues): void {
@@ -449,6 +500,57 @@ export class AIChatInput extends SignalWatcher(
}
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this._internalDropCleanup?.();
this._internalDropCleanup = null;
window.removeEventListener('dragleave', this._handleWindowDragLeave);
window.removeEventListener('drop', this._resetDragState);
window.removeEventListener('dragend', this._resetDragState);
}
private _trackDragDrop(method: EventArgs['addEmbeddingDoc']['method']) {
const page = this.independentMode
? track.$.intelligence
: track.$.chatPanel;
page.chatPanelInput.addEmbeddingDoc({
control: 'dragDrop',
method,
});
}
private _setupInternalDropTarget() {
const el = this.chatPanelInput;
if (!el) return;
const dropTargetCleanup = dropTargetForElements({
element: el,
canDrop: ({ source }) => {
const entity = (source.data as { entity?: { type?: string } }).entity;
return entity?.type === 'doc';
},
onDragEnter: () => {
this.isDragOver = true;
},
onDragLeave: () => {
this.isDragOver = false;
},
onDrop: ({ source }) => {
this.isDragOver = false;
const entity = (
source.data as { entity?: { type?: string; id?: string } }
).entity;
if (entity?.type === 'doc' && entity.id) {
this.addChip({
docId: entity.id,
state: 'processing',
}).catch(console.error);
this._trackDragDrop('doc');
}
},
});
this._internalDropCleanup = combine(dropTargetCleanup);
}
protected override render() {
const { images, status } = this.chatContextValue;
const hasImages = images.length > 0;
@@ -458,11 +560,19 @@ export class AIChatInput extends SignalWatcher(
class="chat-panel-input"
data-independent-mode=${this.independentMode}
data-if-focused=${this.focused}
data-drag-over=${this.isDragOver}
style=${styleMap({
maxHeight: `${maxHeight}px !important`,
})}
@pointerdown=${this._handlePointerDown}
@dragenter=${this._handleDragEnter}
@dragover=${this._handleDragOver}
@dragleave=${this._handleDragLeave}
@drop=${this._handleDrop}
>
${this.isDragOver
? html`<div class="chat-panel-input-drop-overlay">Drop to attach</div>`
: nothing}
${hasImages
? html`
<image-preview-grid
@@ -611,6 +721,66 @@ export class AIChatInput extends SignalWatcher(
}
};
private _dragHasFiles(event: DragEvent) {
return Array.from(event.dataTransfer?.types ?? []).includes('Files');
}
private readonly _handleDragEnter = (event: DragEvent) => {
if (!this._dragHasFiles(event)) return;
event.preventDefault();
this._dragEnterCounter += 1;
this.isDragOver = true;
};
private readonly _handleDragOver = (event: DragEvent) => {
if (!this._dragHasFiles(event)) return;
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
}
};
private readonly _handleDragLeave = (event: DragEvent) => {
if (!this._dragHasFiles(event)) return;
this._dragEnterCounter = Math.max(0, this._dragEnterCounter - 1);
if (this._dragEnterCounter === 0) {
this.isDragOver = false;
}
};
private readonly _resetDragState = () => {
if (this._dragEnterCounter === 0 && !this.isDragOver) return;
this._dragEnterCounter = 0;
this.isDragOver = false;
};
// Covers the cases where the drag session ends without dragleave/drop firing
// on the input (Esc-cancel, release outside window, drop on another element).
private readonly _handleWindowDragLeave = (event: DragEvent) => {
if (event.relatedTarget === null) this._resetDragState();
};
private readonly _handleDrop = async (event: DragEvent) => {
if (!this._dragHasFiles(event)) return;
event.preventDefault();
event.stopPropagation();
this._dragEnterCounter = 0;
this.isDragOver = false;
const files = Array.from(event.dataTransfer?.files ?? []);
if (!files.length) return;
try {
await addFilesToChat(files, {
addImages: this.addImages,
addChip: this.addChip,
});
this._trackDragDrop('file');
} catch (error) {
console.error(error);
}
};
private readonly _handleAbort = () => {
this.chatContextValue.abortController?.abort();
this.updateContext({ status: 'success' });
@@ -0,0 +1,212 @@
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { ShadowlessElement } from '@blocksuite/affine/std';
import { CloseIcon } from '@blocksuite/icons/lit';
import { css, html, type PropertyValues } from 'lit';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
const DEFAULT_TAB_TITLE = 'New chat';
const TITLE_MAX_LENGTH = 28;
function truncate(text: string): string {
if (text.length <= TITLE_MAX_LENGTH) return text;
return `${text.slice(0, TITLE_MAX_LENGTH).trimEnd()}`;
}
function deriveTabTitle(session: CopilotChatHistoryFragment): string {
const explicit = session.title?.trim();
if (explicit) return truncate(explicit);
const firstUserMessage = session.messages?.find(m => m.role === 'user');
const raw = firstUserMessage?.content?.trim();
if (!raw) return DEFAULT_TAB_TITLE;
const newlineIdx = raw.indexOf('\n');
return truncate(newlineIdx === -1 ? raw : raw.slice(0, newlineIdx));
}
export class AIChatTabs extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor sessions: CopilotChatHistoryFragment[] = [];
@property({ attribute: false })
accessor activeSessionId: string | undefined;
@property({ attribute: false })
accessor onSelectTab!: (sessionId: string) => void;
@property({ attribute: false })
accessor onCloseTab!: (sessionId: string) => void;
static override styles = css`
ai-chat-tabs {
display: flex;
align-items: center;
width: 100%;
min-width: 0;
height: 100%;
overflow: hidden;
}
.ai-chat-tabs {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
min-width: 0;
height: 100%;
}
.tabs-scroll {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 0;
overflow-x: auto;
scrollbar-width: none;
}
.tabs-scroll::-webkit-scrollbar {
display: none;
}
.tab {
display: inline-flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
max-width: 180px;
height: 26px;
padding: 0 6px 0 10px;
border-radius: 6px;
cursor: pointer;
color: ${unsafeCSSVarV2('text/secondary')};
font-size: 12px;
font-weight: 500;
user-select: none;
transition: background-color 0.15s ease;
}
.tab:hover {
background-color: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
color: ${unsafeCSSVarV2('text/primary')};
}
.tab[data-active='true'] {
background-color: ${unsafeCSSVarV2('layer/background/secondary')};
color: ${unsafeCSSVarV2('text/primary')};
}
.tab-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.tab-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border: none;
padding: 0;
border-radius: 3px;
background: transparent;
color: inherit;
cursor: pointer;
opacity: 0.6;
}
.tab-close:hover {
opacity: 1;
background-color: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
}
.tab-close svg {
width: 12px;
height: 12px;
}
`;
override render() {
if (!this.sessions.length) return html``;
return html`
<div class="ai-chat-tabs" data-testid="ai-chat-tabs">
<div class="tabs-scroll" @wheel=${this._handleWheel}>
${repeat(
this.sessions,
session => session.sessionId,
session => this._renderTab(session)
)}
</div>
</div>
`;
}
private readonly _handleWheel = (e: WheelEvent) => {
const el = e.currentTarget as HTMLElement;
if (el.scrollWidth <= el.clientWidth) return;
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
e.preventDefault();
el.scrollLeft += e.deltaY;
}
};
private _renderTab(session: CopilotChatHistoryFragment) {
const active = session.sessionId === this.activeSessionId;
const title = deriveTabTitle(session);
return html`
<div
class="tab"
data-active=${active}
data-session-id=${session.sessionId}
data-testid="ai-chat-tab"
title=${title}
@click=${() => this._handleSelect(session.sessionId)}
>
<span class="tab-title">${title}</span>
<button
class="tab-close"
data-testid="ai-chat-tab-close"
aria-label="Close tab"
@click=${(e: Event) => this._handleClose(e, session.sessionId)}
>
${CloseIcon()}
</button>
</div>
`;
}
private readonly _handleSelect = (sessionId: string) => {
if (sessionId === this.activeSessionId) return;
this.onSelectTab(sessionId);
};
private readonly _handleClose = (e: Event, sessionId: string) => {
e.stopPropagation();
this.onCloseTab(sessionId);
};
override updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (
(changedProps.has('activeSessionId') || changedProps.has('sessions')) &&
this.activeSessionId
) {
const activeTab = this.renderRoot.querySelector(
`[data-session-id="${this.activeSessionId}"]`
);
activeTab?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
});
}
}
}
declare global {
interface HTMLElementTagNameMap {
'ai-chat-tabs': AIChatTabs;
}
}
@@ -5,7 +5,7 @@ import type { NotificationService } from '@blocksuite/affine/shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { ShadowlessElement } from '@blocksuite/affine/std';
import {
ArrowDownSmallIcon,
HistoryIcon,
PinedIcon,
PinIcon,
PlusIcon,
@@ -120,8 +120,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
<div
class="chat-toolbar-icon history-button"
@click=${this.toggleHistoryMenu}
data-testid="ai-panel-chat-history"
>
${ArrowDownSmallIcon()}
${HistoryIcon()}
<affine-tooltip>Chat History</affine-tooltip>
</div>
</div>
@@ -1,3 +1,4 @@
export * from './ai-chat-tabs';
export * from './ai-chat-toolbar';
export * from './ai-session-history';
export * from './configure-ai-chat-toolbar';
@@ -27,7 +27,11 @@ import { AIChatInput } from '../components/ai-chat-input';
import { AIChatEmbeddingStatusTooltip } from '../components/ai-chat-input/embedding-status-tooltip';
import { ChatInputPreference } from '../components/ai-chat-input/preference-popup';
import { AIChatMessages } from '../components/ai-chat-messages/ai-chat-messages';
import { AIChatToolbar, AISessionHistory } from '../components/ai-chat-toolbar';
import {
AIChatTabs,
AIChatToolbar,
AISessionHistory,
} from '../components/ai-chat-toolbar';
import { AIHistoryClear } from '../components/ai-history-clear';
import { AssistantAvatar } from '../components/ai-message-content/assistant-avatar';
import { ChatActionList } from '../components/chat-action-list';
@@ -53,6 +57,7 @@ const appElements = {
'action-text': ActionText,
'ai-loading': AILoading,
'ai-chat-content': AIChatContent,
'ai-chat-tabs': AIChatTabs,
'ai-chat-toolbar': AIChatToolbar,
'ai-session-history': AISessionHistory,
'ai-chat-messages': AIChatMessages,
@@ -75,6 +75,7 @@ export const appEffectElementTags = [
'action-text',
'ai-loading',
'ai-chat-content',
'ai-chat-tabs',
'ai-chat-toolbar',
'ai-session-history',
'ai-chat-messages',