mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-26 14:58:55 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
+147
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user