chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -0,0 +1,29 @@
import { WithDisposable } from '@blocksuite/global/utils';
import { css, html, LitElement } from 'lit';
export class AIPanelDivider extends WithDisposable(LitElement) {
static override styles = css`
:host {
display: flex;
flex-direction: column;
align-items: flex-start;
align-self: stretch;
width: 100%;
}
.divider {
height: 0.5px;
background: var(--affine-border-color);
width: 100%;
}
`;
override render() {
return html`<div class="divider"></div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'ai-panel-divider': AIPanelDivider;
}
}
@@ -0,0 +1,107 @@
import {
AIDoneIcon,
CopyIcon,
WarningIcon,
} from '@blocksuite/affine-components/icons';
import { NotificationProvider } from '@blocksuite/affine-shared/services';
import type { EditorHost } from '@blocksuite/block-std';
import { WithDisposable } from '@blocksuite/global/utils';
import { css, html, LitElement, nothing } from 'lit';
import { property, state } from 'lit/decorators.js';
import type { CopyConfig } from '../type.js';
export class AIFinishTip extends WithDisposable(LitElement) {
static override styles = css`
.finish-tip {
display: flex;
box-sizing: border-box;
width: 100%;
height: 22px;
align-items: center;
justify-content: space-between;
padding: 0 12px;
gap: 4px;
color: var(--affine-text-secondary-color);
.text {
display: flex;
align-items: flex-start;
flex: 1 0 0;
/* light/xs */
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
}
.right {
display: flex;
align-items: center;
.copy,
.copied {
display: flex;
width: 20px;
height: 20px;
justify-content: center;
align-items: center;
border-radius: 8px;
user-select: none;
}
.copy:hover {
color: var(--affine-icon-color);
background: var(--affine-hover-color);
cursor: pointer;
}
.copied {
color: var(--affine-brand-color);
}
}
}
`;
override render() {
return html`<div class="finish-tip">
${WarningIcon}
<div class="text">AI outputs can be misleading or wrong</div>
${this.copy?.allowed
? html`<div class="right">
${this.copied
? html`<div class="copied">${AIDoneIcon}</div>`
: html`<div
class="copy"
@click=${async () => {
this.copied = !!(await this.copy?.onCopy());
if (this.copied) {
this.host.std
.getOptional(NotificationProvider)
?.toast('Copied to clipboard');
}
}}
>
${CopyIcon}
<affine-tooltip>Copy</affine-tooltip>
</div>`}
</div>`
: nothing}
</div>`;
}
@state()
accessor copied = false;
@property({ attribute: false })
accessor copy: CopyConfig | undefined = undefined;
@property({ attribute: false })
accessor host!: EditorHost;
}
declare global {
interface HTMLElementTagNameMap {
'ai-finish-tip': AIFinishTip;
}
}
@@ -0,0 +1,147 @@
import {
DarkLoadingIcon,
LightLoadingIcon,
} from '@blocksuite/affine-components/icons';
import { ColorScheme } from '@blocksuite/affine-model';
import { unsafeCSSVar } from '@blocksuite/affine-shared/theme';
import { WithDisposable } from '@blocksuite/global/utils';
import { baseTheme } from '@toeverything/theme';
import {
css,
html,
LitElement,
nothing,
type PropertyValues,
unsafeCSS,
} from 'lit';
import { property } from 'lit/decorators.js';
export class GeneratingPlaceholder extends WithDisposable(LitElement) {
static override styles = css`
:host {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
margin-bottom: 8px;
}
.generating-header {
width: 100%;
font-size: ${unsafeCSSVar('fontXs')};
font-style: normal;
font-weight: 500;
line-height: 20px;
height: 20px;
}
.generating-header,
.loading-progress {
color: ${unsafeCSSVar('textSecondaryColor')};
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
}
.generating-body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 100%;
border-radius: 4px;
border: 2px solid ${unsafeCSSVar('primaryColor')};
background: ${unsafeCSSVar('blue50')};
color: ${unsafeCSSVar('brandColor')};
gap: 4px;
}
.generating-icon {
display: flex;
justify-content: center;
align-items: center;
height: 24px;
}
.generating-icon svg {
scale: 1.5;
}
.loading-progress {
display: flex;
flex-direction: column;
font-style: normal;
font-weight: 400;
text-align: center;
gap: 4px;
}
.loading-text {
font-size: ${unsafeCSSVar('fontBase')};
height: 24px;
line-height: 24px;
}
.loading-stage {
font-size: ${unsafeCSSVar('fontXs')};
height: 20px;
line-height: 20px;
}
`;
protected override render() {
const loadingText = this.stages[this.loadingProgress - 1] || '';
return html`<style>
.generating-body {
height: ${this.height}px;
}
</style>
${this.showHeader
? html`<div class="generating-header">Answer</div>`
: nothing}
<div class="generating-body">
<div class="generating-icon">
${this.theme === ColorScheme.Light
? DarkLoadingIcon
: LightLoadingIcon}
</div>
<div class="loading-progress">
<div class="loading-text">${loadingText}</div>
<div class="loading-stage">
${this.loadingProgress} / ${this.stages.length}
</div>
</div>
</div>`;
}
override willUpdate(changed: PropertyValues) {
if (changed.has('loadingProgress')) {
this.loadingProgress = Math.max(
1,
Math.min(this.loadingProgress, this.stages.length)
);
}
}
@property({ attribute: false })
accessor height: number = 300;
@property({ attribute: false })
accessor loadingProgress!: number;
@property({ attribute: false })
accessor showHeader!: boolean;
@property({ attribute: false })
accessor stages!: string[];
@property({ attribute: false })
accessor theme!: ColorScheme;
}
declare global {
interface HTMLElementTagNameMap {
'generating-placeholder': GeneratingPlaceholder;
}
}
@@ -0,0 +1,2 @@
export * from './divider.js';
export * from './state/index.js';
@@ -0,0 +1,152 @@
import type { EditorHost } from '@blocksuite/block-std';
import { WithDisposable } from '@blocksuite/global/utils';
import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import type { AIPanelAnswerConfig, CopyConfig } from '../../type.js';
import { filterAIItemGroup } from '../../utils.js';
export class AIPanelAnswer extends WithDisposable(LitElement) {
static override styles = css`
:host {
width: 100%;
display: flex;
box-sizing: border-box;
flex-direction: column;
gap: 8px;
padding: 0;
}
.answer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 4px;
align-self: stretch;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
padding: 0 12px;
}
.answer-head {
align-self: stretch;
color: var(--affine-text-secondary-color);
/* light/xsMedium */
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
height: 20px;
}
.answer-body {
align-self: stretch;
color: var(--affine-text-primary-color);
font-feature-settings:
'clig' off,
'liga' off;
/* light/sm */
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
.response-list-container {
display: flex;
flex-direction: column;
gap: 4px;
}
.response-list-container,
.action-list-container {
padding: 0 8px;
user-select: none;
}
/* set item style outside ai-item */
.response-list-container ai-item-list,
.action-list-container ai-item-list {
--item-padding: 4px;
}
.response-list-container ai-item-list {
--item-icon-color: var(--affine-icon-secondary);
--item-icon-hover-color: var(--affine-icon-color);
}
`;
override render() {
const responseGroup = filterAIItemGroup(this.host, this.config.responses);
return html`
<div class="answer">
<div class="answer-head">Answer</div>
<div class="answer-body">
<slot></slot>
</div>
</div>
${this.finish
? html`
<ai-finish-tip
.copy=${this.copy}
.host=${this.host}
></ai-finish-tip>
${responseGroup.length > 0
? html`
<ai-panel-divider></ai-panel-divider>
${responseGroup.map(
(group, index) => html`
${index !== 0
? html`<ai-panel-divider></ai-panel-divider>`
: nothing}
<div class="response-list-container">
<ai-item-list
.host=${this.host}
.groups=${[group]}
></ai-item-list>
</div>
`
)}
`
: nothing}
${responseGroup.length > 0 && this.config.actions.length > 0
? html`<ai-panel-divider></ai-panel-divider>`
: nothing}
${this.config.actions.length > 0
? html`
<div class="action-list-container">
<ai-item-list
.host=${this.host}
.groups=${this.config.actions}
></ai-item-list>
</div>
`
: nothing}
`
: nothing}
`;
}
@property({ attribute: false })
accessor config!: AIPanelAnswerConfig;
@property({ attribute: false })
accessor copy: CopyConfig | undefined = undefined;
@property({ attribute: false })
accessor finish = true;
@property({ attribute: false })
accessor host!: EditorHost;
}
declare global {
interface HTMLElementTagNameMap {
'ai-panel-answer': AIPanelAnswer;
}
}
@@ -0,0 +1,263 @@
import type { EditorHost } from '@blocksuite/block-std';
import { WithDisposable } from '@blocksuite/global/utils';
import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js';
import {
AIErrorType,
type AIItemGroupConfig,
} from '../../../../../_common/components/index.js';
import type { AIPanelErrorConfig, CopyConfig } from '../../type.js';
import { filterAIItemGroup } from '../../utils.js';
export class AIPanelError extends WithDisposable(LitElement) {
static override styles = css`
:host {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
padding: 0;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
}
.error {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
align-self: stretch;
padding: 0px 12px;
gap: 4px;
.answer-tip {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 4px;
align-self: stretch;
.answer-label {
align-self: stretch;
color: var(--affine-text-secondary-color);
/* light/xsMedium */
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
}
}
.error-info {
align-self: stretch;
color: var(--affine-error-color, #eb4335);
font-feature-settings:
'clig' off,
'liga' off;
/* light/sm */
font-size: var(--affine-font-sm);
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
a {
color: inherit;
}
}
.action-button-group {
display: flex;
width: 100%;
gap: 16px;
align-items: center;
justify-content: end;
margin-top: 4px;
}
.action-button {
display: flex;
box-sizing: border-box;
padding: 4px 12px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 8px;
border: 1px solid var(--affine-border-color);
background: var(--affine-white);
color: var(--affine-text-primary-color);
/* light/xsMedium */
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
}
.action-button:hover {
cursor: pointer;
}
.action-button.primary {
border: 1px solid var(--affine-black-10);
background: var(--affine-primary-color);
color: var(--affine-pure-white);
}
.action-button > span {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
}
.action-button:not(.primary):hover {
background: var(--affine-hover-color);
}
}
ai-panel-divider {
margin-top: 4px;
}
.response-list-container {
display: flex;
flex-direction: column;
gap: 4px;
padding: 0 8px;
user-select: none;
}
.response-list-container ai-item-list {
--item-padding: 4px;
--item-icon-color: var(--affine-icon-secondary);
--item-icon-hover-color: var(--affine-icon-color);
}
`;
private _getResponseGroup = () => {
let responseGroup: AIItemGroupConfig[] = [];
const errorType = this.config.error?.type;
if (errorType && errorType !== AIErrorType.GeneralNetworkError) {
return responseGroup;
}
responseGroup = filterAIItemGroup(this.host, this.config.responses);
return responseGroup;
};
override render() {
const responseGroup = this._getResponseGroup();
const errorTemplate = choose(
this.config.error?.type,
[
[
AIErrorType.Unauthorized,
() =>
html` <div class="error-info">
You need to login to AFFiNE Cloud to continue using AFFiNE AI.
</div>
<div class="action-button-group">
<div @click=${this.config.cancel} class="action-button">
<span>Cancel</span>
</div>
<div @click=${this.config.login} class="action-button primary">
<span>login</span>
</div>
</div>`,
],
[
AIErrorType.PaymentRequired,
() =>
html` <div class="error-info">
You've reached the current usage cap for AFFiNE AI. You can
subscribe to AFFiNE AI to continue the AI experience!
</div>
<div class="action-button-group">
<div @click=${this.config.cancel} class="action-button">
<span>Cancel</span>
</div>
<div
@click=${this.config.upgrade}
class="action-button primary"
>
<span>Upgrade</span>
</div>
</div>`,
],
],
// default error handler
() => {
const tip = this.config.error?.message;
const error = tip
? html`<span class="error-tip"
>An error occurred<affine-tooltip
tip-position="bottom-start"
.arrow=${false}
>${tip}</affine-tooltip
></span
>`
: 'An error occurred';
return html`
<style>
.error-tip {
text-decoration: underline;
}
</style>
<div class="error-info">
${error}. Please try again later. If this issue persists, please let
us know at
<a href="mailto:support@toeverything.info">
support@toeverything.info
</a>
</div>
`;
}
);
return html`
<div class="error">
<div class="answer-tip">
<div class="answer-label">Answer</div>
<slot></slot>
</div>
${errorTemplate}
</div>
${this.withAnswer
? html`<ai-finish-tip
.copy=${this.copy}
.host=${this.host}
></ai-finish-tip>`
: nothing}
${responseGroup.length > 0
? html`
<ai-panel-divider></ai-panel-divider>
${responseGroup.map(
(group, index) => html`
${index !== 0
? html`<ai-panel-divider></ai-panel-divider>`
: nothing}
<div class="response-list-container">
<ai-item-list
.host=${this.host}
.groups=${[group]}
></ai-item-list>
</div>
`
)}
`
: nothing}
`;
}
@property({ attribute: false })
accessor config!: AIPanelErrorConfig;
@property({ attribute: false })
accessor copy: CopyConfig | undefined = undefined;
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor withAnswer = false;
}
declare global {
interface HTMLElementTagNameMap {
'ai-panel-error': AIPanelError;
}
}
@@ -0,0 +1,123 @@
import {
AIStarIconWithAnimation,
AIStopIcon,
} from '@blocksuite/affine-components/icons';
import type { ColorScheme } from '@blocksuite/affine-model';
import { WithDisposable } from '@blocksuite/global/utils';
import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import type { AIPanelGeneratingConfig } from '../../type.js';
export class AIPanelGenerating extends WithDisposable(LitElement) {
static override styles = css`
:host {
width: 100%;
padding: 0 12px;
box-sizing: border-box;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
}
.generating-tip {
display: flex;
width: 100%;
height: 22px;
align-items: center;
gap: 8px;
color: var(--affine-brand-color);
.text {
display: flex;
align-items: flex-start;
gap: 10px;
flex: 1 0 0;
/* light/smMedium */
font-size: var(--affine-font-sm);
font-style: normal;
font-weight: 500;
line-height: 22px; /* 157.143% */
}
.left,
.right {
display: flex;
height: 20px;
justify-content: center;
align-items: center;
}
.left {
width: 20px;
}
.right {
gap: 6px;
}
.right:hover {
cursor: pointer;
}
.stop-icon {
height: 20px;
width: 20px;
}
.esc-label {
font-size: var(--affine-font-xs);
font-weight: 500;
line-height: 20px;
}
}
`;
override render() {
const {
generatingIcon = AIStarIconWithAnimation,
stages,
height = 300,
} = this.config;
return html`
${stages && stages.length > 0
? html`<generating-placeholder
.height=${height}
.theme=${this.theme}
.loadingProgress=${this.loadingProgress}
.stages=${stages}
.showHeader=${!this.withAnswer}
></generating-placeholder>`
: nothing}
<div class="generating-tip">
<div class="left">${generatingIcon}</div>
<div class="text">AI is generating...</div>
<div @click=${this.stopGenerating} class="right">
<span class="stop-icon">${AIStopIcon}</span>
<span class="esc-label">ESC</span>
</div>
</div>
`;
}
updateLoadingProgress(progress: number) {
this.loadingProgress = progress;
}
@property({ attribute: false })
accessor config!: AIPanelGeneratingConfig;
@property({ attribute: false })
accessor loadingProgress: number = 1;
@property({ attribute: false })
accessor stopGenerating!: () => void;
@property({ attribute: false })
accessor theme!: ColorScheme;
@property({ attribute: false })
accessor withAnswer!: boolean;
}
declare global {
interface HTMLElementTagNameMap {
'ai-panel-generating': AIPanelGenerating;
}
}
@@ -0,0 +1,4 @@
export * from './answer.js';
export * from './error.js';
export * from './generating.js';
export * from './input.js';
@@ -0,0 +1,174 @@
import { AIStarIcon } from '@blocksuite/affine-components/icons';
import { stopPropagation } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/utils';
import { SendIcon } from '@blocksuite/icons/lit';
import { css, html, LitElement, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';
export class AIPanelInput extends WithDisposable(LitElement) {
static override styles = css`
:host {
width: 100%;
padding: 0 12px;
box-sizing: border-box;
}
.root {
display: flex;
align-items: flex-start;
gap: 8px;
background: var(--affine-background-overlay-panel-color);
}
.icon {
display: flex;
align-items: center;
}
.textarea-container {
display: flex;
align-items: flex-end;
gap: 8px;
flex: 1 0 0;
textarea {
flex: 1 0 0;
border: none;
outline: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
background-color: transparent;
resize: none;
overflow: hidden;
padding: 0px;
color: var(--affine-text-primary-color);
/* light/sm */
font-family: var(--affine-font-family);
font-size: var(--affine-font-sm);
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
textarea::placeholder {
color: var(--affine-placeholder-color);
}
textarea::-moz-placeholder {
color: var(--affine-placeholder-color);
}
}
.arrow {
display: flex;
align-items: center;
padding: 2px;
gap: 10px;
border-radius: 4px;
background: var(--affine-black-10, rgba(0, 0, 0, 0.1));
svg {
width: 16px;
height: 16px;
color: var(--affine-pure-white, #fff);
}
}
.arrow[data-active] {
background: var(--affine-brand-color, #1e96eb);
}
.arrow[data-active]:hover {
cursor: pointer;
}
`;
private _onInput = () => {
this.textarea.style.height = 'auto';
this.textarea.style.height = this.textarea.scrollHeight + 'px';
this.onInput?.(this.textarea.value);
const value = this.textarea.value.trim();
if (value.length > 0) {
this._arrow.dataset.active = '';
this._hasContent = true;
} else {
delete this._arrow.dataset.active;
this._hasContent = false;
}
};
private _onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
e.preventDefault();
this._sendToAI();
}
};
private _sendToAI = () => {
const value = this.textarea.value.trim();
if (value.length === 0) return;
this.onFinish?.(value);
this.remove();
};
override render() {
return html`<div class="root">
<div class="icon">${AIStarIcon}</div>
<div class="textarea-container">
<textarea
placeholder="What are your thoughts?"
rows="1"
@keydown=${this._onKeyDown}
@input=${this._onInput}
@pointerdown=${stopPropagation}
@click=${stopPropagation}
@dblclick=${stopPropagation}
@cut=${stopPropagation}
@copy=${stopPropagation}
@paste=${stopPropagation}
@keyup=${stopPropagation}
></textarea>
<div
class="arrow"
@click=${this._sendToAI}
@pointerdown=${stopPropagation}
>
${SendIcon()}
${this._hasContent
? html`<affine-tooltip .offset=${12}>Send to AI</affine-tooltip>`
: nothing}
</div>
</div>
</div>`;
}
override updated(_changedProperties: Map<PropertyKey, unknown>): void {
const result = super.updated(_changedProperties);
this.textarea.style.height = this.textarea.scrollHeight + 'px';
return result;
}
@query('.arrow')
private accessor _arrow!: HTMLDivElement;
@state()
private accessor _hasContent = false;
@property({ attribute: false })
accessor onFinish: ((input: string) => void) | undefined = undefined;
@property({ attribute: false })
accessor onInput: ((input: string) => void) | undefined = undefined;
@query('textarea')
accessor textarea!: HTMLTextAreaElement;
}
declare global {
interface HTMLElementTagNameMap {
'ai-panel-input': AIPanelInput;
}
}