From 9418a89ae9e56b1f2ea9dde68eca3bb9d6f01a72 Mon Sep 17 00:00:00 2001 From: akumatus Date: Mon, 17 Feb 2025 03:38:07 +0000 Subject: [PATCH] feat(core): auto collapse ai chips (#10209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../presets/ai/chat-panel/chat-context.ts | 8 ++++ .../presets/ai/chat-panel/chat-panel-chips.ts | 44 ++++++++++++++++--- .../ai/chat-panel/chat-panel-messages.ts | 17 ++++--- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-context.ts b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-context.ts index c93d03efe1..8a2a955c67 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-context.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-context.ts @@ -18,6 +18,14 @@ export type 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 = | 'loading' | 'success' diff --git a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-chips.ts b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-chips.ts index 9789ffe6fc..e051505ad5 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-chips.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-chips.ts @@ -6,8 +6,8 @@ import { createLitPortal } from '@blocksuite/affine/blocks'; import { WithDisposable } from '@blocksuite/affine/global/utils'; import { PlusIcon } from '@blocksuite/icons/lit'; import { flip, offset } from '@floating-ui/dom'; -import { css, html } from 'lit'; -import { property, query } from 'lit/decorators.js'; +import { css, html, nothing, type PropertyValues } from 'lit'; +import { property, query, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import { AIProvider } from '../provider'; @@ -21,7 +21,8 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) { display: flex; flex-wrap: wrap; } - .add-button { + .add-button, + .collapse-button { display: flex; align-items: center; justify-content: center; @@ -32,8 +33,10 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) { margin: 4px 0; box-sizing: border-box; cursor: pointer; + font-size: 12px; } - .add-button:hover { + .add-button:hover, + .collapse-button:hover { background-color: var(--affine-hover-color); } `; @@ -61,13 +64,25 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) { @query('.add-button') accessor addButton!: HTMLDivElement; + @state() + accessor isCollapsed = false; + 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`
${PlusIcon()}
${repeat( - this.chatContextValue.chips, + chips, chip => getChipKey(chip), chip => { if (isDocChip(chip)) { @@ -88,9 +103,28 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) { return null; } )} + ${isCollapsed + ? html`
+ +${this.chatContextValue.chips.length - 1} +
` + : nothing}
`; } + 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 = () => { if (this._abortController) { this._abortController.abort(); diff --git a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts index 67e95fb72f..a18c9d2acb 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts @@ -24,7 +24,12 @@ import { import { AffineAvatarIcon, AffineIcon, DownArrowIcon } from '../_common/icons'; import { AIChatErrorRenderer } from '../messages/error'; 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 { AIPreloadConfig } from './preload-config'; @@ -207,7 +212,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { const { isLoading } = this; const filteredItems = items.filter(item => { return ( - 'role' in item || + isChatMessage(item) || item.messages?.length === 3 || (HISTORY_IMAGE_ACTIONS.includes(item.action) && item.messages?.length === 2) @@ -244,7 +249,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { ` : repeat( filteredItems, - item => ('role' in item ? item.id : item.sessionId), + item => (isChatMessage(item) ? item.id : item.sessionId), (item, index) => { const isLast = index === filteredItems.length - 1; return html`
@@ -317,7 +322,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { return AIChatErrorRenderer(host, error); } - if ('role' in item) { + if (isChatMessage(item)) { const state = isLast ? status !== 'loading' && status !== 'transmitting' ? 'finished' @@ -375,8 +380,8 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { } renderAvatar(item: ChatItem) { - const isUser = 'role' in item && item.role === 'user'; - const isAssistant = 'role' in item && item.role === 'assistant'; + const isUser = isChatMessage(item) && item.role === 'user'; + const isAssistant = isChatMessage(item) && item.role === 'assistant'; const isWithDocs = isAssistant && item.content &&