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:
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import './ai-chat-composer-tip';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ContextEmbedStatus,
|
ContextEmbedStatus,
|
||||||
CopilotContextDoc,
|
CopilotContextDoc,
|
||||||
@@ -9,7 +11,6 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
|||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
import type { Store } from '@blocksuite/affine/store';
|
import type { Store } from '@blocksuite/affine/store';
|
||||||
import { InformationIcon } from '@blocksuite/icons/lit';
|
|
||||||
import { type Signal, signal } from '@preact/signals-core';
|
import { type Signal, signal } from '@preact/signals-core';
|
||||||
import { css, html, type PropertyValues } from 'lit';
|
import { css, html, type PropertyValues } from 'lit';
|
||||||
import { property, state } from 'lit/decorators.js';
|
import { property, state } from 'lit/decorators.js';
|
||||||
@@ -46,12 +47,6 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-misleading-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -153,8 +148,12 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
.addImages=${this.addImages}
|
.addImages=${this.addImages}
|
||||||
></ai-chat-input>
|
></ai-chat-input>
|
||||||
<div class="chat-panel-footer">
|
<div class="chat-panel-footer">
|
||||||
<div class="ai-misleading-info">${InformationIcon()} AI outputs can be misleading or wrong</div>
|
<ai-chat-composer-tip
|
||||||
<ai-chat-embedding-status-tooltip .host=${this.host} />
|
.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>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { SignalWatcher } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher } from '@blocksuite/affine/global/lit';
|
||||||
import { unsafeCSSVar } from '@blocksuite/affine/shared/theme';
|
import { unsafeCSSVar } from '@blocksuite/affine/shared/theme';
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
import { InformationIcon } from '@blocksuite/icons/lit';
|
|
||||||
import { css, html, LitElement } from 'lit';
|
import { css, html, LitElement } from 'lit';
|
||||||
import { property, state } from 'lit/decorators.js';
|
import { property, state } from 'lit/decorators.js';
|
||||||
import { debounce, noop } from 'lodash-es';
|
import { debounce, noop } from 'lodash-es';
|
||||||
@@ -16,6 +15,13 @@ export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.embedding-status-text {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
.check-status {
|
.check-status {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -74,7 +80,9 @@ export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) {
|
|||||||
class="embedding-status"
|
class="embedding-status"
|
||||||
data-testid="ai-chat-embedding-status-tooltip"
|
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
|
<span
|
||||||
class="check-status"
|
class="check-status"
|
||||||
data-testid="ai-chat-embedding-status-tooltip-check"
|
data-testid="ai-chat-embedding-status-tooltip-check"
|
||||||
|
|||||||
Reference in New Issue
Block a user