feat(core): ai input scrolling carousel tips (#12540)

### TL;DR

feat: scrolling carousel for ai input tips

> CLOSE BS-3537
This commit is contained in:
yoyoyohamapi
2025-05-27 07:29:15 +00:00
parent f4cba7d6ee
commit 1837c1fe84
3 changed files with 148 additions and 11 deletions

View File

@@ -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<this>) {
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`
<div class="tip-list">
${this.tips.map(
tip => html`<div class="tip">${InformationIcon()}${tip}</div>`
)}
</div>
`;
}
}

View File

@@ -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}
></ai-chat-input>
<div class="chat-panel-footer">
<div class="ai-misleading-info">${InformationIcon()} AI outputs can be misleading or wrong</div>
<ai-chat-embedding-status-tooltip .host=${this.host} />
<ai-chat-composer-tip
.tips=${[
html`<span>AI outputs can be misleading or wrong</span>`,
html`<ai-chat-embedding-status-tooltip .host=${this.host} />`,
]}
></ai-chat-composer-tip>
</div>
</div>`;
}

View File

@@ -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.
<span class="embedding-status-text">
Better results after embedding finished.
</span>
<span
class="check-status"
data-testid="ai-chat-embedding-status-tooltip-check"