From 1837c1fe84b337d361595f7c77feba8f991bb7c7 Mon Sep 17 00:00:00 2001 From: yoyoyohamapi <8338436+yoyoyohamapi@users.noreply.github.com> Date: Tue, 27 May 2025 07:29:15 +0000 Subject: [PATCH] feat(core): ai input scrolling carousel tips (#12540) ### TL;DR feat: scrolling carousel for ai input tips > CLOSE BS-3537 --- .../ai-chat-composer/ai-chat-composer-tip.ts | 130 ++++++++++++++++++ .../ai-chat-composer/ai-chat-composer.ts | 17 ++- .../ai-chat-input/embedding-status-tooltip.ts | 12 +- 3 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer-tip.ts diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer-tip.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer-tip.ts new file mode 100644 index 000000000..8a31ffc45 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer-tip.ts @@ -0,0 +1,130 @@ +import { InformationIcon } from '@blocksuite/icons/lit'; +import type { PropertyValues, TemplateResult } from 'lit'; +import { css, html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +const TIP_HEIGHT = 24; +@customElement('ai-chat-composer-tip') +export class AIChatComposerTip extends LitElement { + static override styles = css` + :host { + display: block; + min-height: 24px; + position: relative; + height: 24px; + overflow: hidden; + } + .tip-list { + display: flex; + flex-direction: column; + transition: margin-top 0.5s ease-in-out; + will-change: margin-top; + } + .tip { + width: 100%; + height: ${TIP_HEIGHT}px; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 4px; + } + `; + + @property({ attribute: false }) + accessor tips: TemplateResult[] = []; + + private readonly _interval = 5000; + private readonly _animDuration = 500; + private _tipIntervalId: number | null = null; + private _tipListElement: HTMLDivElement | null = null; + + override connectedCallback() { + super.connectedCallback(); + this._startAutoScroll(); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + this._stopAutoScroll(); + if (this._tipListElement) { + this._tipListElement.removeEventListener( + 'mouseenter', + this._onMouseEnter + ); + this._tipListElement.removeEventListener( + 'mouseleave', + this._onMouseLeave + ); + } + } + + protected override firstUpdated() { + this._tipListElement = this.renderRoot.querySelector('.tip-list'); + if (this._tipListElement) { + this._tipListElement.addEventListener('mouseenter', this._onMouseEnter); + this._tipListElement.addEventListener('mouseleave', this._onMouseLeave); + } + } + + protected override willUpdate(changed: PropertyValues) { + if (changed.has('tips')) { + this._stopAutoScroll(); + this._startAutoScroll(); + } + } + + private _startAutoScroll() { + this._stopAutoScroll(); + this._tipIntervalId = window.setInterval(() => { + this._scrollToNext(); + }, this._interval); + } + + private _stopAutoScroll() { + if (this._tipIntervalId) { + clearInterval(this._tipIntervalId); + this._tipIntervalId = null; + } + } + + private _scrollToNext() { + if (this.tips.length <= 1 || !this._tipListElement) return; + + const list = this._tipListElement; + const firstItem = list.firstElementChild as HTMLElement; + + if (!firstItem) return; + + // Set transition effect, smoothly move up by one item height + list.style.transition = 'margin-top ' + this._animDuration + 'ms'; + list.style.marginTop = '-' + TIP_HEIGHT + 'px'; + + // After the animation ends: reorder the list and reset the position + setTimeout(function () { + list.style.transition = 'none'; // Immediately disable transition to reset position instantly without animation + list.append(firstItem); // Move the original first item to the bottom to achieve cyclic order + list.style.marginTop = '0'; // Reset the list position to the initial state + }, this._animDuration); + } + + private readonly _onMouseEnter = (e: MouseEvent) => { + e.stopPropagation(); + this._stopAutoScroll(); + }; + + private readonly _onMouseLeave = (e: MouseEvent) => { + e.stopPropagation(); + this._startAutoScroll(); + }; + + override render() { + return html` +
+ ${this.tips.map( + tip => html`
${InformationIcon()}${tip}
` + )} +
+ `; + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts index b8d106a19..837d8c778 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts @@ -1,3 +1,5 @@ +import './ai-chat-composer-tip'; + import type { ContextEmbedStatus, CopilotContextDoc, @@ -9,7 +11,6 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import type { EditorHost } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std'; import type { Store } from '@blocksuite/affine/store'; -import { InformationIcon } from '@blocksuite/icons/lit'; import { type Signal, signal } from '@preact/signals-core'; import { css, html, type PropertyValues } from 'lit'; import { property, state } from 'lit/decorators.js'; @@ -46,12 +47,6 @@ export class AIChatComposer extends SignalWatcher( font-size: 12px; user-select: none; } - - .ai-misleading-info { - display: flex; - align-items: center; - gap: 4px; - } `; @property({ attribute: false }) @@ -153,8 +148,12 @@ export class AIChatComposer extends SignalWatcher( .addImages=${this.addImages} > `; } diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/embedding-status-tooltip.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/embedding-status-tooltip.ts index 1e992e3d7..a5e1529cb 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/embedding-status-tooltip.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/embedding-status-tooltip.ts @@ -1,7 +1,6 @@ import { SignalWatcher } from '@blocksuite/affine/global/lit'; import { unsafeCSSVar } from '@blocksuite/affine/shared/theme'; import type { EditorHost } from '@blocksuite/affine/std'; -import { InformationIcon } from '@blocksuite/icons/lit'; import { css, html, LitElement } from 'lit'; import { property, state } from 'lit/decorators.js'; import { debounce, noop } from 'lodash-es'; @@ -16,6 +15,13 @@ export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) { gap: 4px; user-select: none; } + .embedding-status-text { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 500px; + } .check-status { padding: 4px; cursor: pointer; @@ -74,7 +80,9 @@ export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) { class="embedding-status" data-testid="ai-chat-embedding-status-tooltip" > - ${InformationIcon()} Better results after embedding finished. + + Better results after embedding finished. +