feat(core): auto collapse ai chips (#10209)

Support issue [BS-2545](https://linear.app/affine-design/issue/BS-2545).

Automatically collapse the AI chips when starting a new chat.

![截屏2025-02-16 19.07.05.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/eac6f760-3b07-410d-863c-9f15c99df58a.png)
This commit is contained in:
akumatus
2025-02-17 03:38:07 +00:00
parent 85e413f8c8
commit 9418a89ae9
3 changed files with 58 additions and 11 deletions
@@ -18,6 +18,14 @@ export type ChatAction = {
export type ChatItem = ChatMessage | ChatAction; export type ChatItem = ChatMessage | ChatAction;
export function isChatAction(item: ChatItem): item is ChatAction {
return 'action' in item;
}
export function isChatMessage(item: ChatItem): item is ChatMessage {
return 'role' in item;
}
export type ChatStatus = export type ChatStatus =
| 'loading' | 'loading'
| 'success' | 'success'
@@ -6,8 +6,8 @@ import { createLitPortal } from '@blocksuite/affine/blocks';
import { WithDisposable } from '@blocksuite/affine/global/utils'; import { WithDisposable } from '@blocksuite/affine/global/utils';
import { PlusIcon } from '@blocksuite/icons/lit'; import { PlusIcon } from '@blocksuite/icons/lit';
import { flip, offset } from '@floating-ui/dom'; import { flip, offset } from '@floating-ui/dom';
import { css, html } from 'lit'; import { css, html, nothing, type PropertyValues } from 'lit';
import { property, query } from 'lit/decorators.js'; import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { AIProvider } from '../provider'; import { AIProvider } from '../provider';
@@ -21,7 +21,8 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.add-button { .add-button,
.collapse-button {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -32,8 +33,10 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
margin: 4px 0; margin: 4px 0;
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;
font-size: 12px;
} }
.add-button:hover { .add-button:hover,
.collapse-button:hover {
background-color: var(--affine-hover-color); background-color: var(--affine-hover-color);
} }
`; `;
@@ -61,13 +64,25 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
@query('.add-button') @query('.add-button')
accessor addButton!: HTMLDivElement; accessor addButton!: HTMLDivElement;
@state()
accessor isCollapsed = false;
override render() { override render() {
const isCollapsed =
this.isCollapsed &&
this.chatContextValue.chips.filter(c => c.state !== 'candidate').length >
1;
const chips = isCollapsed
? this.chatContextValue.chips.slice(0, 1)
: this.chatContextValue.chips;
return html` <div class="chips-wrapper"> return html` <div class="chips-wrapper">
<div class="add-button" @click=${this._toggleAddDocMenu}> <div class="add-button" @click=${this._toggleAddDocMenu}>
${PlusIcon()} ${PlusIcon()}
</div> </div>
${repeat( ${repeat(
this.chatContextValue.chips, chips,
chip => getChipKey(chip), chip => getChipKey(chip),
chip => { chip => {
if (isDocChip(chip)) { if (isDocChip(chip)) {
@@ -88,9 +103,28 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
return null; return null;
} }
)} )}
${isCollapsed
? html`<div class="collapse-button" @click=${this._toggleCollapse}>
+${this.chatContextValue.chips.length - 1}
</div>`
: nothing}
</div>`; </div>`;
} }
protected override updated(_changedProperties: PropertyValues): void {
if (
_changedProperties.has('chatContextValue') &&
_changedProperties.get('chatContextValue')?.status === 'loading' &&
this.isCollapsed === false
) {
this.isCollapsed = true;
}
}
private readonly _toggleCollapse = () => {
this.isCollapsed = !this.isCollapsed;
};
private readonly _toggleAddDocMenu = () => { private readonly _toggleAddDocMenu = () => {
if (this._abortController) { if (this._abortController) {
this._abortController.abort(); this._abortController.abort();
@@ -24,7 +24,12 @@ import {
import { AffineAvatarIcon, AffineIcon, DownArrowIcon } from '../_common/icons'; import { AffineAvatarIcon, AffineIcon, DownArrowIcon } from '../_common/icons';
import { AIChatErrorRenderer } from '../messages/error'; import { AIChatErrorRenderer } from '../messages/error';
import { AIProvider } from '../provider'; import { AIProvider } from '../provider';
import type { ChatContextValue, ChatItem, ChatMessage } from './chat-context'; import {
type ChatContextValue,
type ChatItem,
type ChatMessage,
isChatMessage,
} from './chat-context';
import { HISTORY_IMAGE_ACTIONS } from './const'; import { HISTORY_IMAGE_ACTIONS } from './const';
import { AIPreloadConfig } from './preload-config'; import { AIPreloadConfig } from './preload-config';
@@ -207,7 +212,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
const { isLoading } = this; const { isLoading } = this;
const filteredItems = items.filter(item => { const filteredItems = items.filter(item => {
return ( return (
'role' in item || isChatMessage(item) ||
item.messages?.length === 3 || item.messages?.length === 3 ||
(HISTORY_IMAGE_ACTIONS.includes(item.action) && (HISTORY_IMAGE_ACTIONS.includes(item.action) &&
item.messages?.length === 2) item.messages?.length === 2)
@@ -244,7 +249,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
</div> ` </div> `
: repeat( : repeat(
filteredItems, filteredItems,
item => ('role' in item ? item.id : item.sessionId), item => (isChatMessage(item) ? item.id : item.sessionId),
(item, index) => { (item, index) => {
const isLast = index === filteredItems.length - 1; const isLast = index === filteredItems.length - 1;
return html`<div class="message"> return html`<div class="message">
@@ -317,7 +322,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
return AIChatErrorRenderer(host, error); return AIChatErrorRenderer(host, error);
} }
if ('role' in item) { if (isChatMessage(item)) {
const state = isLast const state = isLast
? status !== 'loading' && status !== 'transmitting' ? status !== 'loading' && status !== 'transmitting'
? 'finished' ? 'finished'
@@ -375,8 +380,8 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
} }
renderAvatar(item: ChatItem) { renderAvatar(item: ChatItem) {
const isUser = 'role' in item && item.role === 'user'; const isUser = isChatMessage(item) && item.role === 'user';
const isAssistant = 'role' in item && item.role === 'assistant'; const isAssistant = isChatMessage(item) && item.role === 'assistant';
const isWithDocs = const isWithDocs =
isAssistant && isAssistant &&
item.content && item.content &&