mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
Compare commits
21 Commits
v0.21.0-ca
...
v0.21.0-ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2aa3084ec | ||
|
|
00c5f48a7d | ||
|
|
1306a9733b | ||
|
|
7c41ddb789 | ||
|
|
57ec22ec2e | ||
|
|
a91193c921 | ||
|
|
7477ba6d37 | ||
|
|
9f939d823e | ||
|
|
61b3f82bfe | ||
|
|
a94bef6738 | ||
|
|
dffb89c388 | ||
|
|
ac815142b3 | ||
|
|
ee66545ac9 | ||
|
|
fcc2ec9d66 | ||
|
|
317d3e7ea6 | ||
|
|
a709ed2ef1 | ||
|
|
1b93d3d8d2 | ||
|
|
efab5d4270 | ||
|
|
64c7fb1d66 | ||
|
|
387f7211bf | ||
|
|
ebee11f573 |
File diff suppressed because one or more lines are too long
@@ -12,4 +12,4 @@ npmPublishAccess: public
|
||||
|
||||
npmPublishRegistry: "https://registry.npmjs.org"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.7.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.8.0.cjs
|
||||
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -2501,9 +2501,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.1"
|
||||
version = "1.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
|
||||
checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
|
||||
@@ -11,10 +11,10 @@ import { property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { ERROR_CARD_DEFAULT_HEIGHT } from '../consts';
|
||||
import type { EmbedIframeStatusCardOptions } from '../types';
|
||||
|
||||
const LINK_EDIT_POPUP_OFFSET = 12;
|
||||
const ERROR_CARD_DEFAULT_HEIGHT = 114;
|
||||
|
||||
export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
@@ -24,7 +24,7 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
}
|
||||
|
||||
.affine-embed-iframe-error-card {
|
||||
container: affine-embed-iframe-error-card / inline-size;
|
||||
container: affine-embed-iframe-error-card / size;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
@@ -41,7 +41,6 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1 0 0;
|
||||
|
||||
.error-title {
|
||||
display: flex;
|
||||
@@ -64,6 +63,9 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 22px; /* 157.143% */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,12 +121,6 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container affine-embed-iframe-error-card (width < 480px) {
|
||||
.error-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.affine-embed-iframe-error-card.horizontal {
|
||||
@@ -133,12 +129,19 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
|
||||
.error-content {
|
||||
align-items: flex-start;
|
||||
flex: 1 0 0;
|
||||
|
||||
.error-message {
|
||||
height: 40px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@container affine-embed-iframe-error-card (width < 480px) {
|
||||
.error-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.affine-embed-iframe-error-card.vertical {
|
||||
@@ -155,6 +158,18 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
svg {
|
||||
transform: scale(1.6) translateY(-14px);
|
||||
}
|
||||
}
|
||||
|
||||
@container affine-embed-iframe-error-card (height < 300px) or (width < 300px) {
|
||||
.error-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -216,10 +231,10 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
<div class=${cardClasses} style=${cardStyle}>
|
||||
<div class="error-content">
|
||||
<div class="error-title">
|
||||
<div class="error-icon">
|
||||
<span class="error-icon">
|
||||
${InformationIcon({ width: '16px', height: '16px' })}
|
||||
</div>
|
||||
<div class="error-title-text">This link couldn’t be loaded.</div>
|
||||
</span>
|
||||
<span class="error-title-text">This link couldn’t be loaded.</span>
|
||||
</div>
|
||||
<div class="error-message">
|
||||
${this.error?.message || 'Failed to load embedded content'}
|
||||
@@ -244,8 +259,7 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-banner">
|
||||
<!-- TODO: add error banner icon -->
|
||||
<div class="icon-box"></div>
|
||||
<div class="icon-box">${EmbedIframeErrorIcon}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -280,3 +294,25 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
height: ERROR_CARD_DEFAULT_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
export const EmbedIframeErrorIcon = html`<svg
|
||||
width="204"
|
||||
height="102"
|
||||
viewBox="0 0 204 102"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_2676_106795)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M94.6838 8.45092L106.173 31.9276L84.6593 57.0514L90.5888 64.9202C88.6083 64.6092 86.5089 65.0701 84.7813 66.3719L78.4802 71.1202C75.0967 73.6698 74.4207 78.4796 76.9704 81.8631C79.5201 85.2467 84.3299 85.9227 87.7134 83.373L89.4487 82.0654C90.3714 81.37 90.5558 80.0582 89.8604 79.1354C89.1651 78.2127 87.8533 78.0283 86.9305 78.7237L85.1952 80.0313C83.6573 81.1902 81.471 80.883 80.3121 79.345C79.1531 77.807 79.4604 75.6208 80.9984 74.4618L87.2995 69.7136C88.8375 68.5547 91.0237 68.8619 92.1827 70.3999C92.8645 71.3047 94.1389 71.4996 95.0582 70.8513L95.8982 71.966L94.6469 72.9089C93.109 74.0679 90.9227 73.7606 89.7638 72.2227C89.0684 71.2999 87.7566 71.1155 86.8339 71.8109C85.9111 72.5062 85.7267 73.818 86.4221 74.7408C88.9718 78.1243 93.7816 78.8003 97.1651 76.2506L98.4164 75.3077L99.8156 77.1646L86.8434 102.707L89.291 114.735L42.1397 108.108L56.3354 7.10072C56.6429 4.91308 58.6655 3.38889 60.8532 3.69634L94.6838 8.45092ZM122.987 12.4287L119.974 33.8672L95.4607 58.4925C98.7006 56.8928 102.722 57.7678 104.976 60.7594C107.526 64.1429 106.85 68.9527 103.466 71.5024L102.718 72.0665L105.949 78.0266L92.2105 103.461L92.9872 115.254L147.108 122.86L161.304 21.8531C161.611 19.6654 160.087 17.6428 157.899 17.3353L122.987 12.4287ZM100.701 68.3471L100.948 68.1607C102.486 67.0018 102.793 64.8155 101.634 63.2775C100.625 61.9381 98.8364 61.5321 97.3755 62.2152L100.701 68.3471ZM88.8231 36.502C84.6277 35.9124 80.7486 38.8354 80.159 43.0308L79.1885 49.9367C79.0277 51.0809 79.8249 52.1388 80.9691 52.2996C82.1133 52.4604 83.1712 51.6632 83.332 50.519L84.3025 43.6132C84.5705 41.7062 86.3337 40.3775 88.2407 40.6455L95.1466 41.6161C96.2908 41.7769 97.3487 40.9797 97.5095 39.8355C97.6703 38.6913 96.8731 37.6334 95.7289 37.4726L88.8231 36.502ZM115.065 40.1901C113.921 40.0293 112.863 40.8265 112.702 41.9707C112.542 43.1149 113.339 44.1728 114.483 44.3336L121.389 45.3042C123.296 45.5722 124.625 47.3354 124.357 49.2424L123.386 56.1483C123.225 57.2925 124.022 58.3504 125.167 58.5112C126.311 58.672 127.369 57.8748 127.529 56.7306L128.5 49.8247C129.09 45.6293 126.167 41.7503 121.971 41.1607L115.065 40.1901ZM123.031 73.7041C124.176 73.8649 124.973 74.9228 124.812 76.067L123.841 82.9728C123.252 87.1682 119.373 90.0913 115.177 89.5017L106.89 88.337C105.746 88.1762 104.949 87.1183 105.11 85.9741C105.27 84.8299 106.328 84.0327 107.473 84.1935L115.76 85.3582C117.667 85.6262 119.43 84.2975 119.698 82.3905L120.668 75.4847C120.829 74.3405 121.887 73.5433 123.031 73.7041Z"
|
||||
fill="#E6E6E6"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2676_106795">
|
||||
<rect width="204" height="102" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>`;
|
||||
|
||||
@@ -3,16 +3,22 @@ import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { EmbedIcon } from '@blocksuite/icons/lit';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, LitElement, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { IDLE_CARD_DEFAULT_HEIGHT } from '../consts';
|
||||
import type { EmbedIframeStatusCardOptions } from '../types';
|
||||
|
||||
export class EmbedIframeIdleCard extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.affine-embed-iframe-idle-card {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
container: affine-embed-iframe-idle-card / size;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -23,8 +29,6 @@ export class EmbedIframeIdleCard extends WithDisposable(LitElement) {
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: ${unsafeCSSVarV2('icon/secondary')};
|
||||
@@ -48,18 +52,81 @@ export class EmbedIframeIdleCard extends WithDisposable(LitElement) {
|
||||
.affine-embed-iframe-idle-card:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.affine-embed-iframe-idle-card.horizontal {
|
||||
flex-direction: row;
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.affine-embed-iframe-idle-card.vertical {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
gap: 12px;
|
||||
|
||||
.icon {
|
||||
width: 176px;
|
||||
height: 112px;
|
||||
overflow-y: hidden;
|
||||
|
||||
svg {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
transform: rotate(12deg) translateY(18%);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@container affine-embed-iframe-idle-card (height < 180px) {
|
||||
.icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
const { layout, width, height } = this.options;
|
||||
const cardClasses = classMap({
|
||||
'affine-embed-iframe-idle-card': true,
|
||||
horizontal: layout === 'horizontal',
|
||||
vertical: layout === 'vertical',
|
||||
});
|
||||
|
||||
const cardWidth = width ? `${width}px` : '100%';
|
||||
const cardHeight = height ? `${height}px` : '100%';
|
||||
const cardStyle = styleMap({
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
});
|
||||
|
||||
return html`
|
||||
<div class="affine-embed-iframe-idle-card">
|
||||
<span class="icon">
|
||||
${EmbedIcon({ width: '24px', height: '24px' })}
|
||||
</span>
|
||||
<div class=${cardClasses} style=${cardStyle}>
|
||||
<span class="icon"> ${EmbedIcon()} </span>
|
||||
<span class="text">
|
||||
Embed anything (Google Drive, Google Docs, Spotify, Miro…)
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor options: EmbedIframeStatusCardOptions = {
|
||||
layout: 'horizontal',
|
||||
height: IDLE_CARD_DEFAULT_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
|
||||
this.disposables.addFromEvent(this, 'cut', stopPropagation);
|
||||
this.disposables.addFromEvent(this, 'copy', stopPropagation);
|
||||
this.disposables.addFromEvent(this, 'paste', stopPropagation);
|
||||
this.disposables.addFromEvent(this, 'pointerdown', stopPropagation);
|
||||
}
|
||||
|
||||
get store() {
|
||||
|
||||
@@ -8,10 +8,9 @@ import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { getEmbedCardIcons } from '../../common/utils';
|
||||
import { LOADING_CARD_DEFAULT_HEIGHT } from '../consts';
|
||||
import type { EmbedIframeStatusCardOptions } from '../types';
|
||||
|
||||
const LOADING_CARD_DEFAULT_HEIGHT = 114;
|
||||
|
||||
export class EmbedIframeLoadingCard extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
@@ -20,7 +19,7 @@ export class EmbedIframeLoadingCard extends LitElement {
|
||||
}
|
||||
|
||||
.affine-embed-iframe-loading-card {
|
||||
container: affine-embed-iframe-loading-card / inline-size;
|
||||
container: affine-embed-iframe-loading-card / size;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
@@ -147,6 +146,12 @@ export class EmbedIframeLoadingCard extends LitElement {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container affine-embed-iframe-loading-card (height < 240px) {
|
||||
.loading-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -6,3 +6,7 @@ export const DEFAULT_IFRAME_HEIGHT = 152;
|
||||
export const DEFAULT_IFRAME_WIDTH = '100%';
|
||||
|
||||
export const LINK_CREATE_POPUP_OFFSET = 4;
|
||||
|
||||
export const IDLE_CARD_DEFAULT_HEIGHT = 48;
|
||||
export const LOADING_CARD_DEFAULT_HEIGHT = 114;
|
||||
export const ERROR_CARD_DEFAULT_HEIGHT = 114;
|
||||
|
||||
@@ -34,7 +34,10 @@ import {
|
||||
DEFAULT_IFRAME_HEIGHT,
|
||||
DEFAULT_IFRAME_WIDTH,
|
||||
EMBED_IFRAME_DEFAULT_CONTAINER_BORDER_RADIUS,
|
||||
ERROR_CARD_DEFAULT_HEIGHT,
|
||||
IDLE_CARD_DEFAULT_HEIGHT,
|
||||
LINK_CREATE_POPUP_OFFSET,
|
||||
LOADING_CARD_DEFAULT_HEIGHT,
|
||||
} from './consts.js';
|
||||
import { embedIframeBlockStyles } from './style.js';
|
||||
import type { EmbedIframeStatusCardOptions } from './types.js';
|
||||
@@ -109,10 +112,23 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
return flag ?? false;
|
||||
}
|
||||
|
||||
get _horizontalCardHeight(): number {
|
||||
switch (this.status$.value) {
|
||||
case 'idle':
|
||||
return IDLE_CARD_DEFAULT_HEIGHT;
|
||||
case 'loading':
|
||||
return LOADING_CARD_DEFAULT_HEIGHT;
|
||||
case 'error':
|
||||
return ERROR_CARD_DEFAULT_HEIGHT;
|
||||
default:
|
||||
return LOADING_CARD_DEFAULT_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
get _statusCardOptions(): EmbedIframeStatusCardOptions {
|
||||
return this.inSurface
|
||||
? { layout: 'vertical' }
|
||||
: { layout: 'horizontal', height: 114 };
|
||||
: { layout: 'horizontal', height: this._horizontalCardHeight };
|
||||
}
|
||||
|
||||
open = () => {
|
||||
@@ -257,19 +273,21 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
};
|
||||
|
||||
protected _handleClick = () => {
|
||||
// We don't need to select the block when the block is in the surface
|
||||
if (this.inSurface) {
|
||||
return;
|
||||
}
|
||||
|
||||
// when the block is in idle status and the url is not set, clear the selection
|
||||
// and show the link input popup
|
||||
if (this.isIdle$.value && !this.model.props.url) {
|
||||
this.selectionManager.clear(['block']);
|
||||
// when the block is in the surface, clear the surface selection
|
||||
// otherwise, clear the block selection
|
||||
this.selectionManager.clear([this.inSurface ? 'surface' : 'block']);
|
||||
this.toggleLinkInputPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't need to select the block when the block is in the surface
|
||||
if (this.inSurface) {
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, select the block
|
||||
this._selectBlock();
|
||||
};
|
||||
@@ -311,7 +329,9 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
|
||||
private readonly _renderContent = () => {
|
||||
if (this.isIdle$.value) {
|
||||
return html`<embed-iframe-idle-card></embed-iframe-idle-card>`;
|
||||
return html`<embed-iframe-idle-card
|
||||
.options=${this._statusCardOptions}
|
||||
></embed-iframe-idle-card>`;
|
||||
}
|
||||
|
||||
if (this.isLoading$.value) {
|
||||
@@ -356,6 +376,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
})
|
||||
);
|
||||
|
||||
// if the iframe url is not set, refresh the data to get the iframe url
|
||||
if (!this.model.props.iframeUrl) {
|
||||
this.doc.withoutTransact(() => {
|
||||
this.refreshData().catch(console.error);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
|
||||
"@blocksuite/affine-inline-preset": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-rich-text": "workspace:*",
|
||||
@@ -34,7 +35,8 @@
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effects": "./src/effects.ts"
|
||||
"./effects": "./src/effects.ts",
|
||||
"./turbo-painter": "./src/turbo/list-painter.worker.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@@ -3,3 +3,5 @@ export * from './commands';
|
||||
export { correctNumberedListsOrderToPrev } from './commands/utils';
|
||||
export * from './list-block.js';
|
||||
export * from './list-spec.js';
|
||||
export * from './turbo/list-layout-handler';
|
||||
export * from './turbo/list-painter.worker';
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { Rect } from '@blocksuite/affine-gfx-turbo-renderer';
|
||||
import {
|
||||
BlockLayoutHandlerExtension,
|
||||
BlockLayoutHandlersIdentifier,
|
||||
getSentenceRects,
|
||||
segmentSentences,
|
||||
} from '@blocksuite/affine-gfx-turbo-renderer';
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
import type { GfxBlockComponent } from '@blocksuite/std';
|
||||
import { clientToModelCoord } from '@blocksuite/std/gfx';
|
||||
|
||||
import type { ListLayout } from './list-painter.worker';
|
||||
|
||||
export class ListLayoutHandlerExtension extends BlockLayoutHandlerExtension<ListLayout> {
|
||||
readonly blockType = 'affine:list';
|
||||
|
||||
static override setup(di: Container) {
|
||||
di.addImpl(
|
||||
BlockLayoutHandlersIdentifier('list'),
|
||||
ListLayoutHandlerExtension
|
||||
);
|
||||
}
|
||||
|
||||
queryLayout(component: GfxBlockComponent): ListLayout | null {
|
||||
// Select all list items within this list block
|
||||
const listItemSelector =
|
||||
'.affine-list-block-container .affine-list-rich-text-wrapper [data-v-text="true"]';
|
||||
const listItemNodes = component.querySelectorAll(listItemSelector);
|
||||
|
||||
if (listItemNodes.length === 0) return null;
|
||||
|
||||
const viewportRecord = component.gfx.viewport.deserializeRecord(
|
||||
component.dataset.viewportState
|
||||
);
|
||||
|
||||
if (!viewportRecord) return null;
|
||||
|
||||
const { zoom, viewScale } = viewportRecord;
|
||||
const list: ListLayout = {
|
||||
type: 'affine:list',
|
||||
items: [],
|
||||
};
|
||||
|
||||
listItemNodes.forEach(listItemNode => {
|
||||
const listItemWrapper = listItemNode.closest(
|
||||
'.affine-list-rich-text-wrapper'
|
||||
);
|
||||
if (!listItemWrapper) return;
|
||||
|
||||
// Determine list item type based on class
|
||||
let itemType: 'bulleted' | 'numbered' | 'todo' | 'toggle' = 'bulleted';
|
||||
let checked = false;
|
||||
let collapsed = false;
|
||||
let prefix = '';
|
||||
|
||||
if (listItemWrapper.classList.contains('affine-list--checked')) {
|
||||
checked = true;
|
||||
}
|
||||
|
||||
const parentListBlock = listItemWrapper.closest(
|
||||
'.affine-list-block-container'
|
||||
)?.parentElement;
|
||||
if (parentListBlock) {
|
||||
if (parentListBlock.dataset.listType === 'numbered') {
|
||||
itemType = 'numbered';
|
||||
const orderVal = parentListBlock.dataset.listOrder;
|
||||
if (orderVal) {
|
||||
prefix = orderVal + '.';
|
||||
}
|
||||
} else if (parentListBlock.dataset.listType === 'todo') {
|
||||
itemType = 'todo';
|
||||
} else if (parentListBlock.dataset.listType === 'toggle') {
|
||||
itemType = 'toggle';
|
||||
collapsed = parentListBlock.dataset.collapsed === 'true';
|
||||
} else {
|
||||
itemType = 'bulleted';
|
||||
}
|
||||
}
|
||||
|
||||
const computedStyle = window.getComputedStyle(listItemNode);
|
||||
const fontSizeStr = computedStyle.fontSize;
|
||||
const fontSize = parseInt(fontSizeStr);
|
||||
|
||||
const sentences = segmentSentences(listItemNode.textContent || '');
|
||||
const sentenceLayouts = sentences.map(sentence => {
|
||||
const sentenceRects = getSentenceRects(listItemNode, sentence);
|
||||
return {
|
||||
text: sentence,
|
||||
rects: sentenceRects.map(({ text, rect }) => {
|
||||
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
|
||||
rect.x,
|
||||
rect.y,
|
||||
]);
|
||||
return {
|
||||
text,
|
||||
rect: {
|
||||
x: modelX,
|
||||
y: modelY,
|
||||
w: rect.w / zoom / viewScale,
|
||||
h: rect.h / zoom / viewScale,
|
||||
},
|
||||
};
|
||||
}),
|
||||
fontSize,
|
||||
type: itemType,
|
||||
prefix,
|
||||
checked,
|
||||
collapsed,
|
||||
};
|
||||
});
|
||||
|
||||
list.items.push(...sentenceLayouts);
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
calculateBound(layout: ListLayout) {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
layout.items.forEach(item => {
|
||||
item.rects.forEach(r => {
|
||||
minX = Math.min(minX, r.rect.x);
|
||||
minY = Math.min(minY, r.rect.y);
|
||||
maxX = Math.max(maxX, r.rect.x + r.rect.w);
|
||||
maxY = Math.max(maxY, r.rect.y + r.rect.h);
|
||||
});
|
||||
});
|
||||
|
||||
const rect: Rect = {
|
||||
x: minX,
|
||||
y: minY,
|
||||
w: maxX - minX,
|
||||
h: maxY - minY,
|
||||
};
|
||||
|
||||
return {
|
||||
rect,
|
||||
subRects: layout.items.flatMap(s => s.rects.map(r => r.rect)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import type {
|
||||
BlockLayout,
|
||||
BlockLayoutPainter,
|
||||
TextRect,
|
||||
WorkerToHostMessage,
|
||||
} from '@blocksuite/affine-gfx-turbo-renderer';
|
||||
import {
|
||||
BlockLayoutPainterExtension,
|
||||
getBaseline,
|
||||
} from '@blocksuite/affine-gfx-turbo-renderer/painter';
|
||||
|
||||
interface ListItemLayout {
|
||||
text: string;
|
||||
rects: TextRect[];
|
||||
fontSize: number;
|
||||
type: 'bulleted' | 'numbered' | 'todo' | 'toggle';
|
||||
prefix?: string;
|
||||
checked?: boolean;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
export interface ListLayout extends BlockLayout {
|
||||
type: 'affine:list';
|
||||
items: ListItemLayout[];
|
||||
}
|
||||
|
||||
const debugListBorder = false;
|
||||
|
||||
function isListLayout(layout: BlockLayout): layout is ListLayout {
|
||||
return layout.type === 'affine:list';
|
||||
}
|
||||
|
||||
class ListLayoutPainter implements BlockLayoutPainter {
|
||||
private static readonly supportFontFace =
|
||||
typeof FontFace !== 'undefined' &&
|
||||
typeof self !== 'undefined' &&
|
||||
'fonts' in self;
|
||||
|
||||
static readonly font = ListLayoutPainter.supportFontFace
|
||||
? new FontFace(
|
||||
'Inter',
|
||||
`url(https://fonts.gstatic.com/s/inter/v18/UcCo3FwrK3iLTcviYwYZ8UA3.woff2)`
|
||||
)
|
||||
: null;
|
||||
|
||||
static fontLoaded = !ListLayoutPainter.supportFontFace;
|
||||
|
||||
static {
|
||||
if (ListLayoutPainter.supportFontFace && ListLayoutPainter.font) {
|
||||
// @ts-expect-error worker fonts API
|
||||
self.fonts.add(ListLayoutPainter.font);
|
||||
|
||||
ListLayoutPainter.font
|
||||
.load()
|
||||
.then(() => {
|
||||
ListLayoutPainter.fontLoaded = true;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load Inter font:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
paint(
|
||||
ctx: OffscreenCanvasRenderingContext2D,
|
||||
layout: BlockLayout,
|
||||
layoutBaseX: number,
|
||||
layoutBaseY: number
|
||||
): void {
|
||||
if (!ListLayoutPainter.fontLoaded) {
|
||||
const message: WorkerToHostMessage = {
|
||||
type: 'paintError',
|
||||
error: 'Font not loaded',
|
||||
blockType: 'affine:list',
|
||||
};
|
||||
self.postMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isListLayout(layout)) return;
|
||||
|
||||
const renderedPositions = new Set<string>();
|
||||
|
||||
layout.items.forEach(item => {
|
||||
const fontSize = item.fontSize;
|
||||
const baselineY = getBaseline(fontSize);
|
||||
|
||||
ctx.font = `${fontSize}px Inter`;
|
||||
ctx.strokeStyle = 'yellow';
|
||||
// Render the text content
|
||||
item.rects.forEach(textRect => {
|
||||
const x = textRect.rect.x - layoutBaseX;
|
||||
const y = textRect.rect.y - layoutBaseY;
|
||||
|
||||
const posKey = `${x},${y}`;
|
||||
// Only render if we haven't rendered at this position before
|
||||
if (renderedPositions.has(posKey)) return;
|
||||
|
||||
if (debugListBorder) {
|
||||
ctx.strokeRect(x, y, textRect.rect.w, textRect.rect.h);
|
||||
}
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillText(textRect.text, x, y + baselineY);
|
||||
|
||||
renderedPositions.add(posKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const ListLayoutPainterExtension = BlockLayoutPainterExtension(
|
||||
'affine:list',
|
||||
ListLayoutPainter
|
||||
);
|
||||
@@ -8,6 +8,7 @@
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../gfx/turbo-renderer" },
|
||||
{ "path": "../../inlines/preset" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../rich-text" },
|
||||
|
||||
@@ -3,5 +3,5 @@ export * from './commands';
|
||||
export * from './paragraph-block.js';
|
||||
export * from './paragraph-block-config.js';
|
||||
export * from './paragraph-spec.js';
|
||||
export * from './turbo/paragraph-layout-provider.js';
|
||||
export * from './turbo/paragraph-painter.worker.js';
|
||||
export * from './turbo/paragraph-layout-handler';
|
||||
export * from './turbo/paragraph-painter.worker';
|
||||
|
||||
@@ -15,8 +15,10 @@ export class ParagraphLayoutHandlerExtension extends BlockLayoutHandlerExtension
|
||||
readonly blockType = 'affine:paragraph';
|
||||
|
||||
static override setup(di: Container) {
|
||||
const layoutHandler = new ParagraphLayoutHandlerExtension();
|
||||
di.addImpl(BlockLayoutHandlersIdentifier, layoutHandler);
|
||||
di.addImpl(
|
||||
BlockLayoutHandlersIdentifier('paragraph'),
|
||||
ParagraphLayoutHandlerExtension
|
||||
);
|
||||
}
|
||||
|
||||
queryLayout(component: GfxBlockComponent): ParagraphLayout | null {
|
||||
@@ -4,7 +4,10 @@ import type {
|
||||
TextRect,
|
||||
WorkerToHostMessage,
|
||||
} from '@blocksuite/affine-gfx-turbo-renderer';
|
||||
import { BlockLayoutPainterExtension } from '@blocksuite/affine-gfx-turbo-renderer/painter';
|
||||
import {
|
||||
BlockLayoutPainterExtension,
|
||||
getBaseline,
|
||||
} from '@blocksuite/affine-gfx-turbo-renderer/painter';
|
||||
|
||||
interface SentenceLayout {
|
||||
text: string;
|
||||
@@ -17,25 +20,8 @@ export interface ParagraphLayout extends BlockLayout {
|
||||
sentences: SentenceLayout[];
|
||||
}
|
||||
|
||||
const meta = {
|
||||
emSize: 2048,
|
||||
hHeadAscent: 1984,
|
||||
hHeadDescent: -494,
|
||||
};
|
||||
|
||||
const debugSentenceBorder = false;
|
||||
|
||||
function getBaseline(fontSize: number) {
|
||||
const lineHeight = 1.2 * fontSize;
|
||||
|
||||
const A = fontSize * (meta.hHeadAscent / meta.emSize); // ascent
|
||||
const D = fontSize * (meta.hHeadDescent / meta.emSize); // descent
|
||||
const AD = A + Math.abs(D); // ascent + descent
|
||||
const L = lineHeight - AD; // leading
|
||||
const y = A + L / 2;
|
||||
return y;
|
||||
}
|
||||
|
||||
function isParagraphLayout(layout: BlockLayout): layout is ParagraphLayout {
|
||||
return layout.type === 'affine:paragraph';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { FileDropExtension } from '@blocksuite/affine-components/drop-indicator';
|
||||
import { ConnectorElementView } from '@blocksuite/affine-gfx-connector';
|
||||
import { GroupElementView } from '@blocksuite/affine-gfx-group';
|
||||
import { MindMapView } from '@blocksuite/affine-gfx-mindmap';
|
||||
import { ShapeElementView } from '@blocksuite/affine-gfx-shape';
|
||||
import { TextElementView } from '@blocksuite/affine-gfx-text';
|
||||
import { NoteBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DNDAPIExtension,
|
||||
@@ -27,6 +32,19 @@ import {
|
||||
viewportOverlayWidget,
|
||||
} from './widgets';
|
||||
|
||||
/**
|
||||
* Why do we add these extensions into CommonSpecs?
|
||||
* Because in some cases we need to create edgeless elements in page mode.
|
||||
* And these view may contain some logic when elements initialize.
|
||||
*/
|
||||
const EdgelessElementViews = [
|
||||
ConnectorElementView,
|
||||
MindMapView,
|
||||
GroupElementView,
|
||||
TextElementView,
|
||||
ShapeElementView,
|
||||
];
|
||||
|
||||
export const CommonSpecs: ExtensionType[] = [
|
||||
FlavourExtension('affine:page'),
|
||||
DocModeService,
|
||||
@@ -38,6 +56,7 @@ export const CommonSpecs: ExtensionType[] = [
|
||||
ToolbarRegistryExtension,
|
||||
...RootBlockAdapterExtensions,
|
||||
...clipboardConfigs,
|
||||
...EdgelessElementViews,
|
||||
|
||||
modalWidget,
|
||||
innerModalWidget,
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
import { ConnectorElementView } from '@blocksuite/affine-gfx-connector';
|
||||
import { GroupElementView } from '@blocksuite/affine-gfx-group';
|
||||
import { MindMapView } from '@blocksuite/affine-gfx-mindmap';
|
||||
import { ShapeElementView } from '@blocksuite/affine-gfx-shape';
|
||||
import { TextElementView } from '@blocksuite/affine-gfx-text';
|
||||
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
|
||||
import { autoConnectWidget } from '@blocksuite/affine-widget-edgeless-auto-connect';
|
||||
import { edgelessToolbarWidget } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
@@ -90,20 +85,11 @@ const EdgelessClipboardConfigs: ExtensionType[] = [
|
||||
EdgelessClipboardEmbedSyncedDocConfig,
|
||||
];
|
||||
|
||||
export const gfxElementViews = [
|
||||
ConnectorElementView,
|
||||
MindMapView,
|
||||
GroupElementView,
|
||||
TextElementView,
|
||||
ShapeElementView,
|
||||
];
|
||||
|
||||
const EdgelessCommonExtension: ExtensionType[] = [
|
||||
CommonSpecs,
|
||||
ToolController,
|
||||
EdgelessRootService,
|
||||
ViewportElementExtension('.affine-edgeless-viewport'),
|
||||
...gfxElementViews,
|
||||
...quickTools,
|
||||
...seniorTools,
|
||||
...EdgelessClipboardConfigs,
|
||||
|
||||
@@ -105,3 +105,20 @@ export class ViewportLayoutPainter {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const meta = {
|
||||
emSize: 2048,
|
||||
hHeadAscent: 1984,
|
||||
hHeadDescent: -494,
|
||||
};
|
||||
|
||||
export function getBaseline(fontSize: number) {
|
||||
const lineHeight = 1.2 * fontSize;
|
||||
|
||||
const A = fontSize * (meta.hHeadAscent / meta.emSize); // ascent
|
||||
const D = fontSize * (meta.hHeadDescent / meta.emSize); // descent
|
||||
const AD = A + Math.abs(D); // ascent + descent
|
||||
const L = lineHeight - AD; // leading
|
||||
const y = A + L / 2;
|
||||
return y;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-block-callout": "workspace:*",
|
||||
"@blocksuite/affine-block-embed": "workspace:*",
|
||||
"@blocksuite/affine-block-list": "workspace:*",
|
||||
"@blocksuite/affine-block-note": "workspace:*",
|
||||
"@blocksuite/affine-block-paragraph": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import {
|
||||
EmbedIcon,
|
||||
FrameIcon,
|
||||
ImageIcon,
|
||||
PageIcon,
|
||||
ShapeIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
const BLOCK_PREVIEW_ICON_MAP: Record<
|
||||
string,
|
||||
{
|
||||
icon: typeof ShapeIcon;
|
||||
name: string;
|
||||
}
|
||||
> = {
|
||||
shape: {
|
||||
icon: ShapeIcon,
|
||||
name: 'Edgeless shape',
|
||||
},
|
||||
'affine:image': {
|
||||
icon: ImageIcon,
|
||||
name: 'Image block',
|
||||
},
|
||||
'affine:note': {
|
||||
icon: PageIcon,
|
||||
name: 'Note block',
|
||||
},
|
||||
'affine:frame': {
|
||||
icon: FrameIcon,
|
||||
name: 'Frame block',
|
||||
},
|
||||
'affine:embed-': {
|
||||
icon: EmbedIcon,
|
||||
name: 'Embed block',
|
||||
},
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-dnd-preview-element': EdgelessDndPreviewElement;
|
||||
}
|
||||
}
|
||||
|
||||
export const EDGELESS_DND_PREVIEW_ELEMENT = 'edgeless-dnd-preview-element';
|
||||
|
||||
export class EdgelessDndPreviewElement extends LitElement {
|
||||
static override styles = css`
|
||||
.edgeless-dnd-preview-container {
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
width: 264px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.edgeless-dnd-preview-block {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
||||
width: 234px;
|
||||
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
|
||||
border-radius: 8px;
|
||||
background-color: ${unsafeCSSVarV2(
|
||||
'layer/background/overlayPanel',
|
||||
'#FBFBFC'
|
||||
)};
|
||||
|
||||
padding: 8px 20px;
|
||||
gap: 8px;
|
||||
|
||||
transform-origin: center;
|
||||
|
||||
font-family: var(--affine-font-family);
|
||||
box-shadow: 0px 0px 0px 0.5px #e3e3e4 inset;
|
||||
}
|
||||
|
||||
.edgeless-dnd-preview-block > svg {
|
||||
color: ${unsafeCSSVarV2('icon/primary', '#77757D')};
|
||||
}
|
||||
|
||||
.edgeless-dnd-preview-block > .text {
|
||||
color: ${unsafeCSSVarV2('text/primary', '#121212')};
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor elementTypes: {
|
||||
type: string;
|
||||
}[] = [];
|
||||
|
||||
private _getPreviewIcon(type: string) {
|
||||
if (BLOCK_PREVIEW_ICON_MAP[type]) {
|
||||
return BLOCK_PREVIEW_ICON_MAP[type];
|
||||
}
|
||||
|
||||
if (type.startsWith('affine:embed-')) {
|
||||
return BLOCK_PREVIEW_ICON_MAP['affine:embed-'];
|
||||
}
|
||||
|
||||
return {
|
||||
icon: ShapeIcon,
|
||||
name: 'Edgeless content',
|
||||
};
|
||||
}
|
||||
|
||||
override render() {
|
||||
const blocks = repeat(this.elementTypes.slice(0, 3), ({ type }, index) => {
|
||||
const { icon, name } = this._getPreviewIcon(type);
|
||||
|
||||
return html`<div
|
||||
class="edgeless-dnd-preview-block"
|
||||
style=${styleMap({
|
||||
transform: `rotate(${index * -2}deg)`,
|
||||
zIndex: 3 - index,
|
||||
})}
|
||||
>
|
||||
${icon({ width: '24px', height: '24px' })}
|
||||
<span class="text">${name}</span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
return html`<div class="edgeless-dnd-preview-container">${blocks}</div>`;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
import {
|
||||
EDGELESS_DND_PREVIEW_ELEMENT,
|
||||
EdgelessDndPreviewElement,
|
||||
} from './components/edgeless-preview/preview';
|
||||
import { AFFINE_DRAG_HANDLE_WIDGET } from './consts';
|
||||
import { AffineDragHandleWidget } from './drag-handle';
|
||||
|
||||
export function effects() {
|
||||
customElements.define(AFFINE_DRAG_HANDLE_WIDGET, AffineDragHandleWidget);
|
||||
customElements.define(
|
||||
EDGELESS_DND_PREVIEW_ELEMENT,
|
||||
EdgelessDndPreviewElement
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
|
||||
import { RootBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DocModeExtension,
|
||||
DocModeProvider,
|
||||
EditorSettingExtension,
|
||||
EditorSettingProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { matchModels, SpecProvider } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BlockComponent,
|
||||
BlockStdScope,
|
||||
BlockViewIdentifier,
|
||||
LifeCycleWatcher,
|
||||
} from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import { SpecProvider } from '@blocksuite/affine-shared/utils';
|
||||
import { BlockStdScope, BlockViewIdentifier } from '@blocksuite/std';
|
||||
import type {
|
||||
BlockModel,
|
||||
BlockViewType,
|
||||
@@ -24,14 +16,11 @@ import type {
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { EdgelessDndPreviewElement } from '../components/edgeless-preview/preview.js';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
import { getSnapshotRect } from '../utils.js';
|
||||
|
||||
export class PreviewHelper {
|
||||
private readonly _calculateQuery = (
|
||||
selectedIds: string[],
|
||||
mode: 'block' | 'gfx'
|
||||
): Query => {
|
||||
private readonly _calculateQuery = (selectedIds: string[]): Query => {
|
||||
const ids: Array<{ id: string; viewType: BlockViewType }> = selectedIds.map(
|
||||
id => ({
|
||||
id,
|
||||
@@ -58,22 +47,10 @@ export class PreviewHelper {
|
||||
}
|
||||
|
||||
const children = model.children ?? [];
|
||||
if (
|
||||
mode === 'gfx' &&
|
||||
matchModels(model, [RootBlockModel, SurfaceBlockModel])
|
||||
) {
|
||||
children.forEach(child => {
|
||||
if (selectedIds.includes(child.id)) {
|
||||
ids.push({ viewType: 'display', id: child.id });
|
||||
addChildren(child.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
children.forEach(child => {
|
||||
ids.push({ viewType: 'display', id: child.id });
|
||||
addChildren(child.id);
|
||||
});
|
||||
}
|
||||
children.forEach(child => {
|
||||
ids.push({ viewType: 'display', id: child.id });
|
||||
addChildren(child.id);
|
||||
});
|
||||
};
|
||||
selectedIds.forEach(addChildren);
|
||||
|
||||
@@ -83,28 +60,16 @@ export class PreviewHelper {
|
||||
};
|
||||
};
|
||||
|
||||
getPreviewStd = (
|
||||
blockIds: string[],
|
||||
snapshot: SliceSnapshot,
|
||||
mode: 'block' | 'gfx'
|
||||
) => {
|
||||
getPreviewStd = (blockIds: string[]) => {
|
||||
const widget = this.widget;
|
||||
const std = widget.std;
|
||||
const sourceGfx = std.get(GfxControllerIdentifier);
|
||||
const isEdgeless = mode === 'gfx';
|
||||
blockIds = blockIds.slice();
|
||||
|
||||
if (isEdgeless) {
|
||||
blockIds.push(sourceGfx.surface!.id, std.store.root!.id);
|
||||
}
|
||||
|
||||
const docModeService = std.get(DocModeProvider);
|
||||
const editorSetting = std.get(EditorSettingProvider).peek();
|
||||
const query = this._calculateQuery(blockIds as string[], mode);
|
||||
const query = this._calculateQuery(blockIds as string[]);
|
||||
const store = widget.doc.doc.getStore({ query });
|
||||
const previewSpec = SpecProvider._.getSpec(
|
||||
isEdgeless ? 'preview:edgeless' : 'preview:page'
|
||||
);
|
||||
const previewSpec = SpecProvider._.getSpec('preview:page');
|
||||
const settingSignal = signal({ ...editorSetting });
|
||||
const extensions = [
|
||||
DocModeExtension(docModeService),
|
||||
@@ -134,35 +99,6 @@ export class PreviewHelper {
|
||||
} as ExtensionType,
|
||||
];
|
||||
|
||||
if (isEdgeless) {
|
||||
class PreviewViewportInitializer extends LifeCycleWatcher {
|
||||
static override key = 'preview-viewport-initializer';
|
||||
|
||||
override mounted(): void {
|
||||
const rect = getSnapshotRect(snapshot);
|
||||
if (!rect) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.std.view.viewUpdated.subscribe(payload => {
|
||||
if (payload.type !== 'block') return;
|
||||
|
||||
if (payload.view.model.flavour === 'affine:page') {
|
||||
const gfx = this.std.get(GfxControllerIdentifier);
|
||||
|
||||
(
|
||||
payload.view as BlockComponent & { overrideBackground: string }
|
||||
).overrideBackground = 'transparent';
|
||||
|
||||
gfx.viewport.setViewportByBound(rect);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extensions.push(PreviewViewportInitializer);
|
||||
}
|
||||
|
||||
previewSpec.extend(extensions);
|
||||
|
||||
settingSignal.value = {
|
||||
@@ -177,50 +113,92 @@ export class PreviewHelper {
|
||||
|
||||
let width: number = 500;
|
||||
let height;
|
||||
let scale = 1;
|
||||
|
||||
if (isEdgeless) {
|
||||
const rect = getSnapshotRect(snapshot);
|
||||
if (rect) {
|
||||
width = rect.w;
|
||||
height = rect.h;
|
||||
} else {
|
||||
height = 500;
|
||||
}
|
||||
scale = sourceGfx.viewport.zoom;
|
||||
} else {
|
||||
const noteBlock = this.widget.host.querySelector('affine-note');
|
||||
width = noteBlock?.offsetWidth ?? noteBlock?.clientWidth ?? 500;
|
||||
}
|
||||
const noteBlock = this.widget.host.querySelector('affine-note');
|
||||
width = noteBlock?.offsetWidth ?? noteBlock?.clientWidth ?? 500;
|
||||
|
||||
return {
|
||||
scale,
|
||||
previewStd,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
};
|
||||
|
||||
private _extractBlockTypes(snapshot: SliceSnapshot) {
|
||||
const blockTypes: {
|
||||
type: string;
|
||||
}[] = [];
|
||||
|
||||
snapshot.content.forEach(block => {
|
||||
if (block.flavour === 'affine:surface') {
|
||||
Object.values(
|
||||
block.props.elements as Record<string, { id: string; type: string }>
|
||||
).forEach(elem => {
|
||||
blockTypes.push({
|
||||
type: elem.type,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
blockTypes.push({
|
||||
type: block.flavour,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return blockTypes;
|
||||
}
|
||||
|
||||
getPreviewElement = (options: {
|
||||
blockIds: string[];
|
||||
snapshot: SliceSnapshot;
|
||||
mode: 'block' | 'gfx';
|
||||
}) => {
|
||||
const { blockIds, snapshot, mode } = options;
|
||||
|
||||
if (mode === 'block') {
|
||||
const { previewStd, width, height } = this.getPreviewStd(blockIds);
|
||||
const previewTemplate = previewStd.render();
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
element: previewTemplate,
|
||||
};
|
||||
} else {
|
||||
const blockTypes = this._extractBlockTypes(snapshot);
|
||||
|
||||
const edgelessPreview = new EdgelessDndPreviewElement();
|
||||
edgelessPreview.elementTypes = blockTypes;
|
||||
|
||||
return {
|
||||
left: 12,
|
||||
top: 12,
|
||||
element: edgelessPreview,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
renderDragPreview = (options: {
|
||||
blockIds: string[];
|
||||
snapshot: SliceSnapshot;
|
||||
container: HTMLElement;
|
||||
mode: 'block' | 'gfx';
|
||||
}): void => {
|
||||
const { blockIds, snapshot, container, mode } = options;
|
||||
const { previewStd, width, height, scale } = this.getPreviewStd(
|
||||
blockIds,
|
||||
snapshot,
|
||||
mode
|
||||
);
|
||||
const previewTemplate = previewStd.render();
|
||||
}): { x: number; y: number } => {
|
||||
const { container } = options;
|
||||
const { width, height, element, left, top } =
|
||||
this.getPreviewElement(options);
|
||||
|
||||
container.style.transform = `scale(${scale})`;
|
||||
container.style.width = `${width}px`;
|
||||
if (height) {
|
||||
container.style.height = `${height}px`;
|
||||
}
|
||||
container.append(previewTemplate);
|
||||
container.style.position = 'absolute';
|
||||
container.style.left = left ? `${left}px` : '';
|
||||
container.style.top = top ? `${top}px` : '';
|
||||
container.style.width = width ? `${width}px` : '';
|
||||
container.style.height = height ? `${height}px` : '';
|
||||
container.append(element);
|
||||
|
||||
return {
|
||||
x: left ?? 0,
|
||||
y: top ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE,
|
||||
EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
|
||||
import { DropIndicator } from '@blocksuite/affine-components/drop-indicator';
|
||||
import {
|
||||
@@ -511,6 +515,7 @@ export class DragEventWatcher {
|
||||
this._mergeSnapshotToCurDoc(snapshot, point).catch(console.error);
|
||||
} else {
|
||||
this._dropAsGfxBlock(snapshot, point);
|
||||
this.widget.selectionHelper.selection.clear(['block']);
|
||||
}
|
||||
} else {
|
||||
this._onPageDrop(dropBlock, dragPayload, dropPayload, point);
|
||||
@@ -1052,7 +1057,10 @@ export class DragEventWatcher {
|
||||
Bound.deserialize(block.props.xywh as SerializedXYWH) ??
|
||||
new Bound(0, 0, 0, 0);
|
||||
|
||||
if (
|
||||
if (block.flavour === 'affine:embed-iframe') {
|
||||
blockBound.w = EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE;
|
||||
blockBound.h = EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE;
|
||||
} else if (
|
||||
block.flavour === 'affine:attachment' ||
|
||||
block.flavour === 'affine:bookmark' ||
|
||||
block.flavour.startsWith('affine:embed-')
|
||||
@@ -1132,17 +1140,22 @@ export class DragEventWatcher {
|
||||
this._dropToModel(surfaceSnapshot, this.gfx.surface!.id)
|
||||
.then(slices => {
|
||||
slices?.content.forEach((block, idx) => {
|
||||
if (
|
||||
block.id === content[idx].id &&
|
||||
(block.flavour === 'affine:image' ||
|
||||
if (block.id === content[idx].id) {
|
||||
if (block.flavour === 'affine:embed-iframe') {
|
||||
store.updateBlock(block.id, {
|
||||
xywh: content[idx].props.xywh,
|
||||
});
|
||||
} else if (
|
||||
block.flavour === 'affine:image' ||
|
||||
block.flavour === 'affine:attachment' ||
|
||||
block.flavour === 'affine:bookmark' ||
|
||||
block.flavour.startsWith('affine:embed-'))
|
||||
) {
|
||||
store.updateBlock(block.id, {
|
||||
xywh: content[idx].props.xywh,
|
||||
style: content[idx].props.style,
|
||||
});
|
||||
block.flavour.startsWith('affine:embed-')
|
||||
) {
|
||||
store.updateBlock(block.id, {
|
||||
xywh: content[idx].props.xywh,
|
||||
style: content[idx].props.style,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -1160,7 +1173,11 @@ export class DragEventWatcher {
|
||||
this._dropToModel(pageSnapshot, this.widget.doc.root!.id)
|
||||
.then(slices => {
|
||||
slices?.content.forEach((block, idx) => {
|
||||
if (
|
||||
if (block.flavour === 'affine:embed-iframe') {
|
||||
store.updateBlock(block.id, {
|
||||
xywh: content[idx].props.xywh,
|
||||
});
|
||||
} else if (
|
||||
block.flavour === 'affine:attachment' ||
|
||||
block.flavour.startsWith('affine:embed-')
|
||||
) {
|
||||
@@ -1403,14 +1420,14 @@ export class DragEventWatcher {
|
||||
|
||||
const { snapshot, fromMode } = source.data.bsEntity;
|
||||
|
||||
this.previewHelper.renderDragPreview({
|
||||
const offset = this.previewHelper.renderDragPreview({
|
||||
blockIds: source.data?.bsEntity?.modelIds,
|
||||
snapshot,
|
||||
container,
|
||||
mode: fromMode ?? 'block',
|
||||
});
|
||||
|
||||
setOffset({ x: 0, y: 0 });
|
||||
setOffset(offset);
|
||||
},
|
||||
setDragData: () => {
|
||||
const { fromMode, snapshot } = this._getDraggedSnapshot();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../../blocks/block-callout" },
|
||||
{ "path": "../../blocks/block-embed" },
|
||||
{ "path": "../../blocks/block-list" },
|
||||
{ "path": "../../blocks/block-note" },
|
||||
{ "path": "../../blocks/block-paragraph" },
|
||||
|
||||
@@ -144,7 +144,7 @@ export class Viewport {
|
||||
const newCenterX = initialTopLeftX + width / (2 * this.zoom);
|
||||
const newCenterY = initialTopLeftY + height / (2 * this.zoom);
|
||||
|
||||
this.setCenter(newCenterX, newCenterY);
|
||||
this.setCenter(newCenterX, newCenterY, false);
|
||||
this._width = width;
|
||||
this._height = height;
|
||||
this._left = left;
|
||||
@@ -362,7 +362,7 @@ export class Viewport {
|
||||
* @param centerY The new y coordinate of the center of the viewport.
|
||||
* @param forceUpdate Whether to force complete any pending resize operations before setting the viewport.
|
||||
*/
|
||||
setCenter(centerX: number, centerY: number, forceUpdate = false) {
|
||||
setCenter(centerX: number, centerY: number, forceUpdate = true) {
|
||||
if (forceUpdate && this._isResizing) {
|
||||
this._forceCompleteResize();
|
||||
}
|
||||
@@ -405,7 +405,7 @@ export class Viewport {
|
||||
newZoom: number,
|
||||
newCenter = Vec.toVec(this.center),
|
||||
smooth = false,
|
||||
forceUpdate = smooth
|
||||
forceUpdate = true
|
||||
) {
|
||||
// Force complete any pending resize operations if forceUpdate is true
|
||||
if (forceUpdate && this._isResizing) {
|
||||
@@ -445,7 +445,7 @@ export class Viewport {
|
||||
bound: Bound,
|
||||
padding: [number, number, number, number] = [0, 0, 0, 0],
|
||||
smooth = false,
|
||||
forceUpdate = smooth
|
||||
forceUpdate = true
|
||||
) {
|
||||
let [pt, pr, pb, pl] = padding;
|
||||
|
||||
@@ -511,7 +511,7 @@ export class Viewport {
|
||||
zoom: number,
|
||||
focusPoint?: IPoint,
|
||||
wheel = false,
|
||||
forceUpdate = false
|
||||
forceUpdate = true
|
||||
) {
|
||||
if (forceUpdate && this._isResizing) {
|
||||
this._forceCompleteResize();
|
||||
|
||||
@@ -71,13 +71,13 @@
|
||||
"electron": "^35.0.0",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-import-resolver-typescript": "^4.0.0",
|
||||
"eslint-plugin-import-x": "^4.5.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-sonarjs": "^3.0.1",
|
||||
"eslint-plugin-unicorn": "^57.0.0",
|
||||
"eslint-plugin-unicorn": "^58.0.0",
|
||||
"happy-dom": "^17.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.2.11",
|
||||
@@ -92,7 +92,7 @@
|
||||
"vite": "^6.0.3",
|
||||
"vitest": "3.0.9"
|
||||
},
|
||||
"packageManager": "yarn@4.7.0",
|
||||
"packageManager": "yarn@4.8.0",
|
||||
"resolutions": {
|
||||
"array-buffer-byte-length": "npm:@nolyfill/array-buffer-byte-length@^1",
|
||||
"array-includes": "npm:@nolyfill/array-includes@^1",
|
||||
|
||||
@@ -26,24 +26,24 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/google": "^1.1.19",
|
||||
"@apollo/server": "^4.11.2",
|
||||
"@apollo/server": "^4.11.3",
|
||||
"@aws-sdk/client-s3": "^3.709.0",
|
||||
"@fal-ai/serverless-client": "^0.15.0",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.20.0",
|
||||
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.4.1",
|
||||
"@google-cloud/opentelemetry-resource-util": "^2.4.0",
|
||||
"@nestjs-cls/transactional": "^2.4.4",
|
||||
"@nestjs-cls/transactional-adapter-prisma": "^1.2.7",
|
||||
"@nestjs/apollo": "^12.2.2",
|
||||
"@nestjs/bullmq": "^10.2.3",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
"@nestjs/graphql": "^12.2.2",
|
||||
"@nestjs/platform-express": "^10.4.15",
|
||||
"@nestjs/platform-socket.io": "^10.4.15",
|
||||
"@nestjs/schedule": "^4.1.2",
|
||||
"@nestjs/throttler": "6.4.0",
|
||||
"@nestjs/websockets": "^10.4.15",
|
||||
"@nestjs-cls/transactional": "^2.6.1",
|
||||
"@nestjs-cls/transactional-adapter-prisma": "^1.2.19",
|
||||
"@nestjs/apollo": "^13.0.4",
|
||||
"@nestjs/bullmq": "^11.0.2",
|
||||
"@nestjs/common": "^11.0.12",
|
||||
"@nestjs/core": "^11.0.12",
|
||||
"@nestjs/graphql": "^13.0.4",
|
||||
"@nestjs/platform-express": "^11.0.12",
|
||||
"@nestjs/platform-socket.io": "^11.0.12",
|
||||
"@nestjs/schedule": "^5.0.1",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"@nestjs/websockets": "^11.0.12",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@node-rs/crc32": "^1.10.6",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
@@ -73,7 +73,7 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"eventsource-parser": "^3.0.0",
|
||||
"express": "^4.21.2",
|
||||
"express": "^5.0.1",
|
||||
"fast-xml-parser": "^5.0.0",
|
||||
"get-stream": "^9.0.1",
|
||||
"graphql": "^16.9.0",
|
||||
@@ -120,7 +120,8 @@
|
||||
"@faker-js/faker": "^9.6.0",
|
||||
"@nestjs/testing": "patch:@nestjs/testing@npm%3A10.4.15#~/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch",
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/express-serve-static-core": "^5.0.6",
|
||||
"@types/graphql-upload": "^17.0.0",
|
||||
"@types/http-errors": "^2.0.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
|
||||
@@ -37,7 +37,7 @@ export type ConfigDescriptor<T> = {
|
||||
type ConfigDefineDescriptor<T> = {
|
||||
desc: string;
|
||||
default: T;
|
||||
validate?: (value: T) => boolean;
|
||||
validate?: (value: T) => z.SafeParseReturnType<T, T>;
|
||||
shape?: z.ZodType<T>;
|
||||
env?: string | [string, EnvConfigType];
|
||||
link?: string;
|
||||
@@ -158,7 +158,7 @@ function standardizeDescriptor<T>(
|
||||
default: desc.default,
|
||||
type,
|
||||
validate: (value: T) => {
|
||||
return shape.safeParse(value);
|
||||
return desc.validate ? desc.validate(value) : shape.safeParse(value);
|
||||
},
|
||||
env,
|
||||
link: desc.link,
|
||||
@@ -257,7 +257,15 @@ export function getDefaultConfig(): AppConfigSchema {
|
||||
const { success, error } = desc.validate(defaultValue);
|
||||
|
||||
if (!success) {
|
||||
throw error;
|
||||
throw new Error(
|
||||
error.issues
|
||||
.map(issue => {
|
||||
return `Invalid config for module [${module}] with key [${key}]
|
||||
Value: ${JSON.stringify(defaultValue)}
|
||||
Error: ${issue.message}`;
|
||||
})
|
||||
.join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
set(modulizedConfig, key, defaultValue);
|
||||
|
||||
@@ -23,7 +23,7 @@ defineModuleConfig('redis', {
|
||||
desc: 'The database index of redis server to be used(Must be less than 10).',
|
||||
default: 0,
|
||||
env: ['REDIS_SERVER_DATABASE', 'integer'],
|
||||
validate: val => val >= 0 && val < 10,
|
||||
shape: z.number().int().nonnegative().max(10),
|
||||
},
|
||||
host: {
|
||||
desc: 'The host of the redis server.',
|
||||
|
||||
@@ -9,12 +9,12 @@ export interface ServerFlags {
|
||||
declare global {
|
||||
interface AppConfigSchema {
|
||||
server: {
|
||||
externalUrl: string;
|
||||
externalUrl?: string;
|
||||
https: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
path: string;
|
||||
name: string | undefined;
|
||||
name?: string;
|
||||
};
|
||||
flags: ServerFlags;
|
||||
}
|
||||
@@ -29,9 +29,16 @@ defineModuleConfig('server', {
|
||||
desc: `Base url of AFFiNE server, used for generating external urls.
|
||||
Default to be \`[server.protocol]://[server.host][:server.port]\` if not specified.
|
||||
`,
|
||||
default: 'http://localhost:3010',
|
||||
default: '',
|
||||
env: 'AFFINE_SERVER_EXTERNAL_URL',
|
||||
shape: z.string().url(),
|
||||
validate: val => {
|
||||
// allow to be nullable and empty string
|
||||
if (!val) {
|
||||
return { success: true, data: val };
|
||||
}
|
||||
|
||||
return z.string().url().safeParse(val);
|
||||
},
|
||||
},
|
||||
https: {
|
||||
desc: 'Whether the server is hosted on a ssl enabled domain (https://).',
|
||||
|
||||
@@ -60,7 +60,7 @@ export class DocRendererController {
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('/*')
|
||||
@Get('/*path')
|
||||
async render(@Req() req: Request, @Res() res: Response) {
|
||||
const assets: HtmlAssets =
|
||||
env.namespaces.canary &&
|
||||
|
||||
@@ -62,7 +62,7 @@ export class StaticFilesResolver implements OnModuleInit {
|
||||
|
||||
// fallback all unknown routes
|
||||
app.get(
|
||||
[basePath + '/admin', basePath + '/admin/*'],
|
||||
[basePath + '/admin', basePath + '/admin/*path'],
|
||||
this.check.use,
|
||||
(_req, res) => {
|
||||
res.sendFile(
|
||||
@@ -101,11 +101,13 @@ export class StaticFilesResolver implements OnModuleInit {
|
||||
redirect: false,
|
||||
index: false,
|
||||
fallthrough: true,
|
||||
immutable: true,
|
||||
dotfiles: 'ignore',
|
||||
})
|
||||
);
|
||||
|
||||
// fallback all unknown routes
|
||||
app.get([basePath, basePath + '/*'], this.check.use, (req, res) => {
|
||||
app.get([basePath, basePath + '/*path'], this.check.use, (req, res) => {
|
||||
const mobile =
|
||||
env.namespaces.canary &&
|
||||
isMobile({
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Text } from '@blocksuite/affine/store';
|
||||
import type { BlobEngine } from '@blocksuite/affine/sync';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { getCurrentWorkspace } from './utils';
|
||||
import { getCurrentWorkspace, isAiEnabled } from './utils';
|
||||
|
||||
const logger = new DebugLogger('electron-renderer:recording');
|
||||
|
||||
@@ -44,6 +44,7 @@ export function setupRecordingEvents(frameworkProvider: FrameworkProvider) {
|
||||
frameworkProvider.get(EditorSettingService);
|
||||
const docsService = workspace.scope.get(DocsService);
|
||||
const editorSetting = editorSettingService.editorSetting;
|
||||
const aiEnabled = isAiEnabled(frameworkProvider);
|
||||
|
||||
const timestamp = i18nTime(status.startTime, {
|
||||
absolute: {
|
||||
@@ -96,7 +97,10 @@ export function setupRecordingEvents(frameworkProvider: FrameworkProvider) {
|
||||
MeetingSettingsService
|
||||
);
|
||||
|
||||
if (!meetingSettingsService.settings.autoTranscription) {
|
||||
if (
|
||||
!meetingSettingsService.settings.autoTranscription ||
|
||||
!aiEnabled
|
||||
) {
|
||||
// auto transcription is disabled,
|
||||
// so we don't need to transcribe the recording by default
|
||||
return;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ServersService } from '@affine/core/modules/cloud';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { WorkspacesService } from '@affine/core/modules/workspace';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
@@ -21,3 +23,23 @@ export function getCurrentWorkspace(frameworkProvider: FrameworkProvider) {
|
||||
[Symbol.dispose]: dispose,
|
||||
};
|
||||
}
|
||||
|
||||
export function getCurrentServerService(frameworkProvider: FrameworkProvider) {
|
||||
const currentServerId = frameworkProvider
|
||||
.get(GlobalContextService)
|
||||
.globalContext.serverId.get();
|
||||
const serversService = frameworkProvider.get(ServersService);
|
||||
const serverRef = currentServerId
|
||||
? serversService.servers$.value.find(
|
||||
server => server.id === currentServerId
|
||||
)
|
||||
: null;
|
||||
return serverRef;
|
||||
}
|
||||
|
||||
export function isAiEnabled(frameworkProvider: FrameworkProvider) {
|
||||
const featureFlagService = frameworkProvider.get(FeatureFlagService);
|
||||
const serverService = getCurrentServerService(frameworkProvider);
|
||||
const aiConfig = serverService?.features$.value.copilot;
|
||||
return featureFlagService.flags.enable_ai.$ && aiConfig;
|
||||
}
|
||||
|
||||
@@ -389,6 +389,7 @@ function getAllApps(): TappableAppInfo[] {
|
||||
(v): v is TappableAppInfo =>
|
||||
v !== null &&
|
||||
!v.bundleIdentifier.startsWith('com.apple') &&
|
||||
!v.bundleIdentifier.startsWith('pro.affine') &&
|
||||
v.processId !== process.pid
|
||||
);
|
||||
return filteredApps;
|
||||
@@ -516,14 +517,17 @@ export function startRecording(
|
||||
appGroup: normalizeAppGroupInfo(appGroup),
|
||||
});
|
||||
|
||||
if (state?.status === 'recording') {
|
||||
// set a timeout to stop the recording after MAX_DURATION_FOR_TRANSCRIPTION
|
||||
setTimeout(() => {
|
||||
// set a timeout to stop the recording after MAX_DURATION_FOR_TRANSCRIPTION
|
||||
setTimeout(() => {
|
||||
if (
|
||||
state?.status === 'recording' &&
|
||||
state.id === recordingStatus$.value?.id
|
||||
) {
|
||||
stopRecording(state.id).catch(err => {
|
||||
logger.error('failed to stop recording', err);
|
||||
});
|
||||
}, MAX_DURATION_FOR_TRANSCRIPTION);
|
||||
}
|
||||
}
|
||||
}, MAX_DURATION_FOR_TRANSCRIPTION);
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -548,30 +552,48 @@ export async function stopRecording(id: number) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recordingStatus = recordingStateMachine.dispatch({
|
||||
type: 'STOP_RECORDING',
|
||||
id,
|
||||
filepath: String(recording.file.path),
|
||||
sampleRate: recording.stream.sampleRate,
|
||||
numberOfChannels: recording.stream.channels,
|
||||
});
|
||||
|
||||
if (!recordingStatus) {
|
||||
logger.error('No recording status to stop');
|
||||
return;
|
||||
}
|
||||
|
||||
const { file } = recording;
|
||||
file.end();
|
||||
|
||||
// Wait for file to finish writing
|
||||
await new Promise<void>(resolve => {
|
||||
file.on('finish', () => {
|
||||
resolve();
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
file.on('finish', () => {
|
||||
// check if the file is empty
|
||||
const stats = fs.statSync(file.path);
|
||||
if (stats.size === 0) {
|
||||
logger.error(`Recording ${id} is empty`);
|
||||
reject(new Error('Recording is empty'));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
const recordingStatus = recordingStateMachine.dispatch({
|
||||
type: 'STOP_RECORDING',
|
||||
id,
|
||||
filepath: String(recording.file.path),
|
||||
sampleRate: recording.stream.sampleRate,
|
||||
numberOfChannels: recording.stream.channels,
|
||||
});
|
||||
});
|
||||
|
||||
return serializeRecordingStatus(recordingStatus);
|
||||
if (!recordingStatus) {
|
||||
logger.error('No recording status to stop');
|
||||
return;
|
||||
}
|
||||
return serializeRecordingStatus(recordingStatus);
|
||||
} catch (error: unknown) {
|
||||
logger.error('Failed to stop recording', error);
|
||||
const recordingStatus = recordingStateMachine.dispatch({
|
||||
type: 'CREATE_BLOCK_FAILED',
|
||||
id,
|
||||
error: error instanceof Error ? error : undefined,
|
||||
});
|
||||
if (!recordingStatus) {
|
||||
logger.error('No recording status to stop');
|
||||
return;
|
||||
}
|
||||
return serializeRecordingStatus(recordingStatus);
|
||||
}
|
||||
}
|
||||
|
||||
export async function readyRecording(id: number, buffer: Buffer) {
|
||||
|
||||
@@ -14,7 +14,7 @@ let package = Package(
|
||||
.library(name: "AffineGraphQL", targets: ["AffineGraphQL"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apollographql/apollo-ios", exact: "1.18.0"),
|
||||
.package(url: "https://github.com/apollographql/apollo-ios", exact: "1.19.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
||||
@@ -16,7 +16,7 @@ let package = Package(
|
||||
dependencies: [
|
||||
.package(path: "../AffineGraphQL"),
|
||||
.package(path: "../MarkdownView"),
|
||||
.package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.18.0"),
|
||||
.package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.19.0"),
|
||||
.package(url: "https://github.com/LaunchDarkly/swift-eventsource.git", from: "3.3.0"),
|
||||
.package(url: "https://github.com/apple/swift-collections", from: "1.1.4"),
|
||||
.package(url: "https://github.com/Lakr233/ChidoriMenu", from: "2.4.3"),
|
||||
|
||||
@@ -14,7 +14,7 @@ let package = Package(
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/JohnSundell/Splash", from: "0.16.0"),
|
||||
.package(url: "https://github.com/swiftlang/swift-cmark", from: "0.4.0"),
|
||||
.package(url: "https://github.com/swiftlang/swift-cmark", from: "0.5.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "MarkdownView", dependencies: [
|
||||
|
||||
@@ -83,7 +83,7 @@ export const controlButton = style({
|
||||
justifyContent: 'center',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||
color: cssVarV2('text/primary'),
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
|
||||
export const controls = style({
|
||||
|
||||
@@ -2,6 +2,10 @@ import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
display: 'inline-flex',
|
||||
height: '1em',
|
||||
width: '1em',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
// replace primary colors to cssVarV2('icon/primary')
|
||||
@@ -28,14 +32,14 @@ const backgroundSecondaryColors = [
|
||||
globalStyle(
|
||||
`${root} :is(${iconPrimaryColors.map(color => `path[fill="${color}"]`).join(',')})`,
|
||||
{
|
||||
fill: cssVarV2('icon/primary'),
|
||||
fill: 'currentColor',
|
||||
}
|
||||
);
|
||||
|
||||
globalStyle(
|
||||
`${root} :is(${iconPrimaryColors.map(color => `path[stroke="${color}"]`).join(',')})`,
|
||||
{
|
||||
stroke: cssVarV2('icon/primary'),
|
||||
stroke: 'currentColor',
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
export const translateSubItem: AISubItemConfig[] = translateLangs.map(lang => {
|
||||
return {
|
||||
type: lang,
|
||||
testId: `action-translate-${lang}`,
|
||||
handler: actionToHandler('translate', AIStarIconWithAnimation, { lang }),
|
||||
};
|
||||
});
|
||||
@@ -56,6 +57,7 @@ export const translateSubItem: AISubItemConfig[] = translateLangs.map(lang => {
|
||||
export const toneSubItem: AISubItemConfig[] = textTones.map(tone => {
|
||||
return {
|
||||
type: tone,
|
||||
testId: `action-change-tone-${tone.toLowerCase()}`,
|
||||
handler: actionToHandler('changeTone', AIStarIconWithAnimation, { tone }),
|
||||
};
|
||||
});
|
||||
@@ -66,6 +68,7 @@ export function createImageFilterSubItem(
|
||||
return imageFilterStyles.map(style => {
|
||||
return {
|
||||
type: style,
|
||||
testId: `action-image-filter-${style.toLowerCase().replace(' ', '-')}`,
|
||||
handler: actionToHandler(
|
||||
'filterImage',
|
||||
AIImageIconWithAnimation,
|
||||
@@ -84,6 +87,7 @@ export function createImageProcessingSubItem(
|
||||
return imageProcessingTypes.map(type => {
|
||||
return {
|
||||
type,
|
||||
testId: `action-image-processing-${type.toLowerCase().replace(' ', '-')}`,
|
||||
handler: actionToHandler(
|
||||
'processImage',
|
||||
AIImageIconWithAnimation,
|
||||
@@ -146,36 +150,42 @@ const EditAIGroup: AIItemGroupConfig = {
|
||||
items: [
|
||||
{
|
||||
name: 'Translate to',
|
||||
testId: 'action-translate',
|
||||
icon: LanguageIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
subItem: translateSubItem,
|
||||
},
|
||||
{
|
||||
name: 'Change tone to',
|
||||
testId: 'action-change-tone',
|
||||
icon: ToneIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
subItem: toneSubItem,
|
||||
},
|
||||
{
|
||||
name: 'Improve writing',
|
||||
testId: 'action-improve-writing',
|
||||
icon: ImproveWritingIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('improveWriting', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Make it longer',
|
||||
testId: 'action-make-it-longer',
|
||||
icon: LongerIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('makeLonger', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Make it shorter',
|
||||
testId: 'action-make-it-shorter',
|
||||
icon: ShorterIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('makeShorter', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Continue writing',
|
||||
testId: 'action-continue-writing',
|
||||
icon: PenIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('continueWriting', AIPenIconWithAnimation),
|
||||
@@ -188,30 +198,35 @@ const DraftAIGroup: AIItemGroupConfig = {
|
||||
items: [
|
||||
{
|
||||
name: 'Write an article about this',
|
||||
testId: 'action-write-article',
|
||||
icon: PenIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('writeArticle', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Write a tweet about this',
|
||||
testId: 'action-write-twitter-post',
|
||||
icon: PenIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('writeTwitterPost', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Write a poem about this',
|
||||
testId: 'action-write-poem',
|
||||
icon: PenIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('writePoem', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Write a blog post about this',
|
||||
testId: 'action-write-blog-post',
|
||||
icon: PenIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('writeBlogPost', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Brainstorm ideas about this',
|
||||
testId: 'action-brainstorm',
|
||||
icon: PenIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('brainstorm', AIPenIconWithAnimation),
|
||||
@@ -224,36 +239,42 @@ const ReviewWIthAIGroup: AIItemGroupConfig = {
|
||||
items: [
|
||||
{
|
||||
name: 'Fix spelling',
|
||||
testId: 'action-fix-spelling',
|
||||
icon: DoneIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('fixSpelling', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Fix grammar',
|
||||
testId: 'action-fix-grammar',
|
||||
icon: DoneIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('improveGrammar', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Explain this image',
|
||||
testId: 'action-explain-image',
|
||||
icon: PenIcon(),
|
||||
showWhen: imageBlockShowWhen,
|
||||
handler: actionToHandler('explainImage', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Explain this code',
|
||||
testId: 'action-explain-code',
|
||||
icon: ExplainIcon(),
|
||||
showWhen: codeBlockShowWhen,
|
||||
handler: actionToHandler('explainCode', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Check code error',
|
||||
testId: 'action-check-code-error',
|
||||
icon: ExplainIcon(),
|
||||
showWhen: codeBlockShowWhen,
|
||||
handler: actionToHandler('checkCodeErrors', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Explain selection',
|
||||
testId: 'action-explain-selection',
|
||||
icon: SelectionIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('explain', AIStarIconWithAnimation),
|
||||
@@ -266,12 +287,14 @@ const GenerateWithAIGroup: AIItemGroupConfig = {
|
||||
items: [
|
||||
{
|
||||
name: 'Summarize',
|
||||
testId: 'action-summarize',
|
||||
icon: PenIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('summary', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Generate headings',
|
||||
testId: 'action-generate-headings',
|
||||
icon: PenIcon(),
|
||||
beta: true,
|
||||
handler: actionToHandler('createHeadings', AIPenIconWithAnimation),
|
||||
@@ -293,24 +316,28 @@ const GenerateWithAIGroup: AIItemGroupConfig = {
|
||||
},
|
||||
{
|
||||
name: 'Generate an image',
|
||||
testId: 'action-generate-image',
|
||||
icon: ImageIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('createImage', AIImageIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Generate outline',
|
||||
testId: 'action-generate-outline',
|
||||
icon: PenIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('writeOutline', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Brainstorm ideas with mind map',
|
||||
testId: 'action-brainstorm-mindmap',
|
||||
icon: MindmapIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('brainstormMindmap', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Generate presentation',
|
||||
testId: 'action-generate-presentation',
|
||||
icon: PresentationIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('createSlides', AIPresentationIconWithAnimation),
|
||||
@@ -318,6 +345,7 @@ const GenerateWithAIGroup: AIItemGroupConfig = {
|
||||
},
|
||||
{
|
||||
name: 'Make it real',
|
||||
testId: 'action-make-it-real',
|
||||
icon: MakeItRealIcon(),
|
||||
beta: true,
|
||||
showWhen: textBlockShowWhen,
|
||||
@@ -325,6 +353,7 @@ const GenerateWithAIGroup: AIItemGroupConfig = {
|
||||
},
|
||||
{
|
||||
name: 'Find actions',
|
||||
testId: 'action-find-actions',
|
||||
icon: SearchIcon(),
|
||||
showWhen: textBlockShowWhen,
|
||||
handler: actionToHandler('findActions', AIStarIconWithAnimation),
|
||||
@@ -338,6 +367,7 @@ const OthersAIGroup: AIItemGroupConfig = {
|
||||
items: [
|
||||
{
|
||||
name: 'Continue with AI',
|
||||
testId: 'action-continue-with-ai',
|
||||
icon: CommentIcon(),
|
||||
handler: host => {
|
||||
const panel = getAIPanelWidget(host);
|
||||
@@ -366,6 +396,7 @@ export function buildAIImageItemGroups(): AIItemGroupConfig[] {
|
||||
items: [
|
||||
{
|
||||
name: 'Explain this image',
|
||||
testId: 'action-explain-image',
|
||||
icon: ImageIcon(),
|
||||
showWhen: () => true,
|
||||
handler: actionToHandler(
|
||||
@@ -382,6 +413,7 @@ export function buildAIImageItemGroups(): AIItemGroupConfig[] {
|
||||
items: [
|
||||
{
|
||||
name: 'Generate an image',
|
||||
testId: 'action-generate-image',
|
||||
icon: ImageIcon(),
|
||||
showWhen: () => true,
|
||||
handler: actionToHandler(
|
||||
@@ -393,6 +425,7 @@ export function buildAIImageItemGroups(): AIItemGroupConfig[] {
|
||||
},
|
||||
{
|
||||
name: 'Image processing',
|
||||
testId: 'action-image-processing',
|
||||
icon: ImageIcon(),
|
||||
showWhen: () => true,
|
||||
subItem: createImageProcessingSubItem(blockActionTrackerOptions),
|
||||
@@ -401,6 +434,7 @@ export function buildAIImageItemGroups(): AIItemGroupConfig[] {
|
||||
},
|
||||
{
|
||||
name: 'AI image filter',
|
||||
testId: 'action-ai-image-filter',
|
||||
icon: ImproveWritingIcon(),
|
||||
showWhen: () => true,
|
||||
subItem: createImageFilterSubItem(blockActionTrackerOptions),
|
||||
@@ -409,6 +443,7 @@ export function buildAIImageItemGroups(): AIItemGroupConfig[] {
|
||||
},
|
||||
{
|
||||
name: 'Generate a caption',
|
||||
testId: 'action-generate-caption',
|
||||
icon: PenIcon(),
|
||||
showWhen: () => true,
|
||||
beta: true,
|
||||
@@ -432,6 +467,7 @@ export function buildAICodeItemGroups(): AIItemGroupConfig[] {
|
||||
items: [
|
||||
{
|
||||
name: 'Explain this code',
|
||||
testId: 'action-explain-code',
|
||||
icon: ExplainIcon(),
|
||||
showWhen: () => true,
|
||||
handler: actionToHandler(
|
||||
@@ -443,6 +479,7 @@ export function buildAICodeItemGroups(): AIItemGroupConfig[] {
|
||||
},
|
||||
{
|
||||
name: 'Check code error',
|
||||
testId: 'action-check-code-error',
|
||||
icon: ExplainIcon(),
|
||||
showWhen: () => true,
|
||||
handler: actionToHandler(
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
buildFinishConfig,
|
||||
buildGeneratingConfig,
|
||||
} from '../ai-panel';
|
||||
import type { AIError, AIItemGroupConfig } from '../components/ai-item/types';
|
||||
import { AIProvider } from '../provider';
|
||||
import { type AIItemGroupConfig } from '../components/ai-item/types';
|
||||
import { type AIError, AIProvider } from '../provider';
|
||||
import { reportResponse } from '../utils/action-reporter';
|
||||
import { getAIPanelWidget } from '../utils/ai-widgets';
|
||||
import { AIContext } from '../utils/context';
|
||||
|
||||
@@ -20,8 +20,7 @@ import type { TemplateResult } from 'lit';
|
||||
|
||||
import { getContentFromSlice } from '../../utils';
|
||||
import { AIChatBlockModel } from '../blocks';
|
||||
import type { AIError } from '../components/ai-item/types';
|
||||
import { AIProvider } from '../provider';
|
||||
import { type AIError, AIProvider } from '../provider';
|
||||
import { reportResponse } from '../utils/action-reporter';
|
||||
import { getAIPanelWidget } from '../utils/ai-widgets';
|
||||
import { AIContext } from '../utils/context';
|
||||
@@ -462,7 +461,6 @@ export function noteBlockOrTextShowWhen(
|
||||
host: EditorHost
|
||||
) {
|
||||
const selected = getCopilotSelectedElems(host);
|
||||
|
||||
return selected.some(
|
||||
el =>
|
||||
el instanceof NoteBlockModel ||
|
||||
|
||||
@@ -85,6 +85,7 @@ export function discard(
|
||||
return {
|
||||
name: 'Discard',
|
||||
icon: DeleteIcon(),
|
||||
testId: 'answer-discard',
|
||||
showWhen: () => !!panel.answer,
|
||||
handler: () => {
|
||||
panel.discard();
|
||||
@@ -96,6 +97,7 @@ export function retry(panel: AffineAIPanelWidget): AIItemConfig {
|
||||
return {
|
||||
name: 'Retry',
|
||||
icon: ResetIcon(),
|
||||
testId: 'answer-retry',
|
||||
handler: () => {
|
||||
reportResponse('result:retry');
|
||||
panel.generate();
|
||||
@@ -123,6 +125,7 @@ export function createInsertItems<T extends keyof BlockSuitePresets.AIActions>(
|
||||
icon: html`<div style=${styleMap({ height: '20px', width: '20px' })}>
|
||||
${LightLoadingIcon}
|
||||
</div>`,
|
||||
testId: 'answer-insert-below-loading',
|
||||
showWhen: () => {
|
||||
const panel = getAIPanelWidget(host);
|
||||
const data = ctx.get();
|
||||
@@ -137,6 +140,8 @@ export function createInsertItems<T extends keyof BlockSuitePresets.AIActions>(
|
||||
{
|
||||
name: buttonText,
|
||||
icon: InsertBelowIcon(),
|
||||
testId:
|
||||
buttonText === 'Replace' ? 'answer-replace' : `answer-insert-below`,
|
||||
showWhen: () => {
|
||||
const panel = getAIPanelWidget(host);
|
||||
const data = ctx.get();
|
||||
@@ -191,6 +196,7 @@ export function asCaption<T extends keyof BlockSuitePresets.AIActions>(
|
||||
return {
|
||||
name: 'Use as caption',
|
||||
icon: PenIcon(),
|
||||
testId: 'answer-use-as-caption',
|
||||
showWhen: () => {
|
||||
const panel = getAIPanelWidget(host);
|
||||
return id === 'generateCaption' && !!panel.answer;
|
||||
@@ -553,9 +559,11 @@ export function actionToResponse<T extends keyof BlockSuitePresets.AIActions>(
|
||||
responses: [
|
||||
{
|
||||
name: 'Response',
|
||||
testId: 'answer-responses',
|
||||
items: [
|
||||
{
|
||||
name: 'Continue in chat',
|
||||
testId: 'answer-continue-in-chat',
|
||||
icon: ChatWithAiIcon({}),
|
||||
handler: () => {
|
||||
reportResponse('result:continue-in-chat');
|
||||
|
||||
@@ -53,6 +53,7 @@ function asCaption<T extends keyof BlockSuitePresets.AIActions>(
|
||||
return {
|
||||
name: 'Use as caption',
|
||||
icon: PenIcon(),
|
||||
testId: 'answer-use-as-caption',
|
||||
showWhen: () => {
|
||||
const panel = getAIPanelWidget(host);
|
||||
return id === 'generateCaption' && !!panel.answer;
|
||||
@@ -79,6 +80,7 @@ function createNewNote(host: EditorHost): AIItemConfig {
|
||||
return {
|
||||
name: 'Create new note',
|
||||
icon: PageIcon(),
|
||||
testId: 'answer-create-new-note',
|
||||
showWhen: () => {
|
||||
const panel = getAIPanelWidget(host);
|
||||
return !!panel.answer && isInsideEdgelessEditor(host);
|
||||
@@ -147,9 +149,11 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
|
||||
return [
|
||||
{
|
||||
name: 'Response',
|
||||
testId: 'answer-responses',
|
||||
items: [
|
||||
{
|
||||
name: 'Insert below',
|
||||
testId: 'answer-insert-below',
|
||||
icon: InsertBelowIcon(),
|
||||
showWhen: () =>
|
||||
!!panel.answer && (!id || !INSERT_ABOVE_ACTIONS.includes(id)),
|
||||
@@ -161,6 +165,7 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
|
||||
},
|
||||
{
|
||||
name: 'Insert above',
|
||||
testId: 'answer-insert-above',
|
||||
icon: InsertTopIcon(),
|
||||
showWhen: () =>
|
||||
!!panel.answer && !!id && INSERT_ABOVE_ACTIONS.includes(id),
|
||||
@@ -173,6 +178,7 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
|
||||
asCaption(host, id),
|
||||
{
|
||||
name: 'Replace selection',
|
||||
testId: 'answer-replace',
|
||||
icon: ReplaceIcon(),
|
||||
showWhen: () =>
|
||||
!!panel.answer && !EXCLUDING_REPLACE_ACTIONS.includes(id),
|
||||
@@ -187,10 +193,12 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
testId: 'answer-common-responses',
|
||||
items: [
|
||||
{
|
||||
name: 'Continue in chat',
|
||||
icon: ChatWithAiIcon(),
|
||||
testId: 'answer-continue-in-chat',
|
||||
handler: () => {
|
||||
reportResponse('result:continue-in-chat');
|
||||
AIProvider.slots.requestOpenWithChat.next({ host });
|
||||
@@ -200,6 +208,7 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
|
||||
{
|
||||
name: 'Regenerate',
|
||||
icon: ResetIcon(),
|
||||
testId: 'answer-regenerate',
|
||||
handler: () => {
|
||||
reportResponse('result:retry');
|
||||
panel.generate();
|
||||
@@ -208,6 +217,7 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
|
||||
{
|
||||
name: 'Discard',
|
||||
icon: DeleteIcon(),
|
||||
testId: 'answer-discard',
|
||||
handler: () => {
|
||||
panel.discard();
|
||||
},
|
||||
@@ -225,6 +235,7 @@ export function buildErrorResponseConfig(panel: AffineAIPanelWidget) {
|
||||
{
|
||||
name: 'Retry',
|
||||
icon: ResetIcon(),
|
||||
testId: 'error-retry',
|
||||
showWhen: () => true,
|
||||
handler: () => {
|
||||
reportResponse('result:retry');
|
||||
@@ -234,6 +245,7 @@ export function buildErrorResponseConfig(panel: AffineAIPanelWidget) {
|
||||
{
|
||||
name: 'Discard',
|
||||
icon: DeleteIcon(),
|
||||
testId: 'error-discard',
|
||||
showWhen: () => !!panel.answer,
|
||||
handler: () => {
|
||||
panel.discard();
|
||||
|
||||
@@ -142,6 +142,7 @@ export class ActionWrapper extends WithDisposable(LitElement) {
|
||||
<slot></slot>
|
||||
<div
|
||||
class="action-name"
|
||||
data-testid="action-name"
|
||||
@click=${() => (this.promptShow = !this.promptShow)}
|
||||
>
|
||||
${icons[item.action] ? icons[item.action] : DoneIcon()}
|
||||
@@ -152,22 +153,27 @@ export class ActionWrapper extends WithDisposable(LitElement) {
|
||||
</div>
|
||||
${this.promptShow
|
||||
? html`
|
||||
<div class="answer-prompt">
|
||||
<div class="answer-prompt" data-testid="answer-prompt">
|
||||
<div class="subtitle">Answer</div>
|
||||
${HISTORY_IMAGE_ACTIONS.includes(item.action)
|
||||
? images &&
|
||||
html`<chat-content-images
|
||||
.images=${images}
|
||||
data-testid="generated-image"
|
||||
></chat-content-images>`
|
||||
: nothing}
|
||||
${answer
|
||||
? createTextRenderer(this.host, { customHeading: true })(answer)
|
||||
? createTextRenderer(this.host, {
|
||||
customHeading: true,
|
||||
testId: 'chat-message-action-answer',
|
||||
})(answer)
|
||||
: nothing}
|
||||
${originalText
|
||||
? html`<div class="subtitle prompt">Prompt</div>
|
||||
${createTextRenderer(this.host, { customHeading: true })(
|
||||
item.messages[0].content + originalText
|
||||
)}`
|
||||
${createTextRenderer(this.host, {
|
||||
customHeading: true,
|
||||
testId: 'chat-message-action-prompt',
|
||||
})(item.messages[0].content + originalText)}`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import './action-wrapper';
|
||||
import '../content/images';
|
||||
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
@@ -27,7 +24,10 @@ export class ActionImageToText extends WithDisposable(ShadowlessElement) {
|
||||
})}
|
||||
>
|
||||
${answer
|
||||
? html`<chat-content-images .images=${answer}></chat-content-images>`
|
||||
? html`<chat-content-images
|
||||
data-testid="original-images"
|
||||
.images=${answer}
|
||||
></chat-content-images>`
|
||||
: nothing}
|
||||
</div>
|
||||
</action-wrapper>`;
|
||||
|
||||
@@ -17,13 +17,19 @@ export class ActionImage extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor host!: EditorHost;
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'action-image';
|
||||
|
||||
protected override render() {
|
||||
const images = this.item.messages[0].attachments;
|
||||
|
||||
return html`<action-wrapper .host=${this.host} .item=${this.item}>
|
||||
<div style=${styleMap({ marginBottom: '12px' })}>
|
||||
${images
|
||||
? html`<chat-content-images .images=${images}></chat-content-images>`
|
||||
? html`<chat-content-images
|
||||
.images=${images}
|
||||
data-testid="original-image"
|
||||
></chat-content-images>`
|
||||
: nothing}
|
||||
</div>
|
||||
</action-wrapper>`;
|
||||
|
||||
@@ -54,6 +54,7 @@ export class ActionText extends WithDisposable(LitElement) {
|
||||
border: isCode ? 'none' : '1px solid var(--affine-border-color)',
|
||||
})}
|
||||
class="original-text"
|
||||
data-testid="original-text"
|
||||
>
|
||||
${createTextRenderer(this.host, {
|
||||
customHeading: true,
|
||||
|
||||
@@ -47,6 +47,9 @@ export class AILoading extends WithDisposable(LitElement) {
|
||||
@property({ attribute: false })
|
||||
accessor stopGenerating!: () => void;
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'ai-loading';
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="generating-tip">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
|
||||
import type { AIError } from '../components/ai-item/types';
|
||||
import type { AIError } from '../provider';
|
||||
|
||||
export type ChatMessage = {
|
||||
id: string;
|
||||
|
||||
@@ -43,6 +43,7 @@ export class ChatPanelChips extends SignalWatcher(
|
||||
.chips-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -4px 0 -4px;
|
||||
}
|
||||
.add-button,
|
||||
.collapse-button,
|
||||
@@ -99,6 +100,9 @@ export class ChatPanelChips extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor searchMenuConfig!: SearchMenuConfig;
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'chat-panel-chips';
|
||||
|
||||
@query('.add-button')
|
||||
accessor addButton!: HTMLDivElement;
|
||||
|
||||
@@ -137,7 +141,11 @@ export class ChatPanelChips extends SignalWatcher(
|
||||
const chips = isCollapsed ? allChips.slice(0, 1) : allChips;
|
||||
|
||||
return html`<div class="chips-wrapper">
|
||||
<div class="add-button" @click=${this._toggleAddDocMenu}>
|
||||
<div
|
||||
class="add-button"
|
||||
data-testid="chat-panel-with-button"
|
||||
@click=${this._toggleAddDocMenu}
|
||||
>
|
||||
${PlusIcon()}
|
||||
</div>
|
||||
${repeat(
|
||||
|
||||
@@ -14,8 +14,7 @@ import { property, query, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { ChatAbortIcon, ChatSendIcon } from '../_common/icons';
|
||||
import type { AIError } from '../components/ai-item/types';
|
||||
import { AIProvider } from '../provider';
|
||||
import { type AIError, AIProvider } from '../provider';
|
||||
import { reportResponse } from '../utils/action-reporter';
|
||||
import { readBlobAsURL } from '../utils/image';
|
||||
import type { AINetworkSearchConfig, DocDisplayConfig } from './chat-config';
|
||||
@@ -220,6 +219,9 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayConfig!: DocDisplayConfig;
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'chat-panel-input-container';
|
||||
|
||||
private get _isNetworkActive() {
|
||||
return (
|
||||
!!this.networkSearchConfig.visible.value &&
|
||||
@@ -335,7 +337,10 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
`
|
||||
: nothing}
|
||||
${this.chatContextValue.quote
|
||||
? html`<div class="chat-selection-quote">
|
||||
? html`<div
|
||||
class="chat-selection-quote"
|
||||
data-testid="chat-selection-quote"
|
||||
>
|
||||
${repeat(
|
||||
getFirstTwoLines(this.chatContextValue.quote),
|
||||
line => line,
|
||||
@@ -420,6 +425,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
: nothing}
|
||||
${images.length < MaximumImageCount
|
||||
? html`<div
|
||||
data-testid="chat-panel-input-image-upload"
|
||||
class="image-upload"
|
||||
aria-disabled=${uploadDisabled}
|
||||
@click=${uploadDisabled ? undefined : this._uploadImageFiles}
|
||||
@@ -434,6 +440,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
this.updateContext({ status: 'success' });
|
||||
reportResponse('aborted:stop');
|
||||
}}
|
||||
data-testid="chat-panel-stop"
|
||||
>
|
||||
${ChatAbortIcon}
|
||||
</div>`
|
||||
|
||||
@@ -14,8 +14,7 @@ import { repeat } from 'lit/directives/repeat.js';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
import { AffineIcon } from '../_common/icons';
|
||||
import { type AIError, UnauthorizedError } from '../components/ai-item/types';
|
||||
import { AIProvider } from '../provider';
|
||||
import { type AIError, AIProvider, UnauthorizedError } from '../provider';
|
||||
import {
|
||||
type ChatContextValue,
|
||||
type ChatMessage,
|
||||
@@ -31,7 +30,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-panel-messages {
|
||||
.chat-panel-messages-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
@@ -157,9 +156,16 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor previewSpecBuilder!: SpecBuilder;
|
||||
|
||||
@query('.chat-panel-messages')
|
||||
@query('.chat-panel-messages-container')
|
||||
accessor messagesContainer: HTMLDivElement | null = null;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
attribute: 'data-testid',
|
||||
reflect: true,
|
||||
})
|
||||
accessor testId = 'chat-panel-messages';
|
||||
|
||||
getScrollContainer(): HTMLDivElement | null {
|
||||
return this.messagesContainer;
|
||||
}
|
||||
@@ -168,12 +174,13 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
return this.isLoading ||
|
||||
!this.host?.doc.get(FeatureFlagService).getFlag('enable_ai_onboarding')
|
||||
? nothing
|
||||
: html`<div class="onboarding-wrapper">
|
||||
: html`<div class="onboarding-wrapper" data-testid="ai-onboarding">
|
||||
${repeat(
|
||||
AIPreloadConfig,
|
||||
config => config.text,
|
||||
config => {
|
||||
return html`<div
|
||||
data-testid=${config.testId}
|
||||
@click=${() => config.handler()}
|
||||
class="onboarding-item"
|
||||
>
|
||||
@@ -220,7 +227,8 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="chat-panel-messages"
|
||||
class="chat-panel-messages-container"
|
||||
data-testid="chat-panel-messages-container"
|
||||
@scroll=${() => this._debouncedOnScroll()}
|
||||
>
|
||||
${filteredItems.length === 0
|
||||
@@ -232,8 +240,12 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
)}
|
||||
<div class="messages-placeholder-title" data-loading=${isLoading}>
|
||||
${this.isLoading
|
||||
? 'AFFiNE AI is loading history...'
|
||||
: 'What can I help you with?'}
|
||||
? html`<span data-testid="chat-panel-loading-state"
|
||||
>AFFiNE AI is loading history...</span
|
||||
>`
|
||||
: html`<span data-testid="chat-panel-empty-state"
|
||||
>What can I help you with?</span
|
||||
>`}
|
||||
</div>
|
||||
${this._renderAIOnboarding()}
|
||||
</div> `
|
||||
@@ -268,7 +280,11 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
)}
|
||||
</div>
|
||||
${showDownIndicator && filteredItems.length > 0
|
||||
? html`<div class="down-indicator" @click=${this._onDownIndicatorClick}>
|
||||
? html`<div
|
||||
data-testid="chat-panel-scroll-down-indicator"
|
||||
class="down-indicator"
|
||||
@click=${this._onDownIndicatorClick}
|
||||
>
|
||||
${ArrowDownIcon()}
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
@@ -44,6 +44,8 @@ export type MenuItem = {
|
||||
name: string | TemplateResult<1>;
|
||||
icon: TemplateResult<1>;
|
||||
action: MenuAction;
|
||||
suffix?: string | TemplateResult<1>;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
export type MenuAction = () => Promise<void> | void;
|
||||
@@ -140,6 +142,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
{
|
||||
key: 'tags',
|
||||
name: 'Tags',
|
||||
testId: 'ai-chat-with-tags',
|
||||
icon: TagsIcon(),
|
||||
action: () => {
|
||||
this._toggleMode(AddPopoverMode.Tags);
|
||||
@@ -148,6 +151,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
{
|
||||
key: 'collections',
|
||||
name: 'Collections',
|
||||
testId: 'ai-chat-with-collections',
|
||||
icon: CollectionsIcon(),
|
||||
action: () => {
|
||||
this._toggleMode(AddPopoverMode.Collections);
|
||||
@@ -176,6 +180,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
{
|
||||
key: 'files',
|
||||
name: 'Upload files (pdf, txt, csv)',
|
||||
testId: 'ai-chat-with-files',
|
||||
icon: UploadIcon(),
|
||||
action: this._addFileChip,
|
||||
},
|
||||
@@ -330,13 +335,14 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
${repeat(
|
||||
items,
|
||||
item => item.key,
|
||||
({ key, name, icon, action }, idx) => {
|
||||
({ key, name, icon, action, testId }, idx) => {
|
||||
const curIdx = startIndex + idx;
|
||||
return html`<icon-button
|
||||
width="280px"
|
||||
height="30px"
|
||||
data-id=${key}
|
||||
data-index=${curIdx}
|
||||
data-testid=${testId}
|
||||
.text=${name}
|
||||
hover=${this._activatedIndex === curIdx}
|
||||
@click=${() => action()?.catch(console.error)}
|
||||
|
||||
@@ -37,6 +37,9 @@ export class ChatContentPureText extends ShadowlessElement {
|
||||
@property({ attribute: false })
|
||||
accessor text: string = '';
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'chat-content-pure-text';
|
||||
|
||||
protected override render() {
|
||||
return this.text.length > 0
|
||||
? html`<div class="chat-content-pure-text">${this.text}</div>`
|
||||
|
||||
@@ -16,6 +16,9 @@ export class ChatMessageAction extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor item!: ChatAction;
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'chat-message-action';
|
||||
|
||||
renderHeader() {
|
||||
return html`
|
||||
<div class="user-info">
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
EdgelessEditorActions,
|
||||
PageEditorActions,
|
||||
} from '../../_common/chat-actions-handle';
|
||||
import { type AIError } from '../../components/ai-item/types';
|
||||
import { AIChatErrorRenderer } from '../../messages/error';
|
||||
import { type AIError } from '../../provider';
|
||||
import { type ChatMessage, isChatMessage } from '../chat-context';
|
||||
|
||||
export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
@@ -34,7 +34,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor isLast: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ attribute: 'data-status', reflect: true })
|
||||
accessor status: string = 'idle';
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -49,6 +49,9 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor retry!: () => void;
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'chat-message-assistant';
|
||||
|
||||
renderHeader() {
|
||||
const isWithDocs =
|
||||
'content' in this.item &&
|
||||
|
||||
@@ -31,6 +31,9 @@ export class ChatMessageUser extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor item!: ChatMessage;
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'chat-message-user';
|
||||
|
||||
renderContent() {
|
||||
const { item } = this;
|
||||
|
||||
@@ -41,7 +44,7 @@ export class ChatMessageUser extends WithDisposable(ShadowlessElement) {
|
||||
.images=${item.attachments}
|
||||
></chat-content-images>`
|
||||
: nothing}
|
||||
<div class="text-content-wrapper">
|
||||
<div class="text-content-wrapper" data-test-id="chat-content-user-text">
|
||||
<chat-content-pure-text .text=${item.content}></chat-content-pure-text>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -17,6 +17,7 @@ export const AIPreloadConfig = [
|
||||
{
|
||||
icon: LanguageIcon(),
|
||||
text: 'Read a foreign language article with AI',
|
||||
testId: 'read-foreign-language-article-with-ai',
|
||||
handler: () => {
|
||||
AIProvider.slots.requestInsertTemplate.next({
|
||||
template: readAforeign,
|
||||
@@ -27,6 +28,7 @@ export const AIPreloadConfig = [
|
||||
{
|
||||
icon: MindmapIcon(),
|
||||
text: 'Tidy an article with AI MindMap Action',
|
||||
testId: 'tidy-an-article-with-ai-mindmap-action',
|
||||
handler: () => {
|
||||
AIProvider.slots.requestInsertTemplate.next({
|
||||
template: TidyMindMapV3,
|
||||
@@ -37,6 +39,7 @@ export const AIPreloadConfig = [
|
||||
{
|
||||
icon: ImageIcon(),
|
||||
text: 'Add illustrations to the article',
|
||||
testId: 'add-illustrations-to-the-article',
|
||||
handler: () => {
|
||||
AIProvider.slots.requestInsertTemplate.next({
|
||||
template: redHat,
|
||||
@@ -47,6 +50,7 @@ export const AIPreloadConfig = [
|
||||
{
|
||||
icon: PenIcon(),
|
||||
text: 'Complete writing with AI',
|
||||
testId: 'complete-writing-with-ai',
|
||||
handler: () => {
|
||||
AIProvider.slots.requestInsertTemplate.next({
|
||||
template: completeWritingWithAI,
|
||||
@@ -57,6 +61,7 @@ export const AIPreloadConfig = [
|
||||
{
|
||||
icon: SendIcon(),
|
||||
text: 'Freely communicate with AI',
|
||||
testId: 'freely-communicate-with-ai',
|
||||
handler: () => {
|
||||
AIProvider.slots.requestInsertTemplate.next({
|
||||
template: freelyCommunicateWithAI,
|
||||
|
||||
@@ -87,6 +87,7 @@ export class AIItemList extends WithDisposable(LitElement) {
|
||||
|
||||
createLitPortal({
|
||||
template: html`<ai-sub-item-list
|
||||
data-testid=${item.testId ? item.testId + '-menu' : ''}
|
||||
.item=${item}
|
||||
.host=${this.host}
|
||||
.onClick=${this.onClick}
|
||||
@@ -141,6 +142,9 @@ export class AIItemList extends WithDisposable(LitElement) {
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onClick: (() => void) | undefined = undefined;
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'ai-item-list';
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -23,8 +23,10 @@ export class AIItem extends WithDisposable(LitElement) {
|
||||
override render() {
|
||||
const { item } = this;
|
||||
const className = item.name.split(' ').join('-').toLocaleLowerCase();
|
||||
const testId = item.testId;
|
||||
|
||||
return html`<div
|
||||
data-testid=${testId}
|
||||
class="menu-item ${className}"
|
||||
@pointerdown=${(e: MouseEvent) => e.stopPropagation()}
|
||||
@click=${() => {
|
||||
|
||||
@@ -70,6 +70,7 @@ export class AISubItemList extends WithDisposable(LitElement) {
|
||||
subItem => subItem.type,
|
||||
subItem =>
|
||||
html`<div
|
||||
data-testid=${subItem.testId}
|
||||
class="menu-item"
|
||||
@click=${() => this._handleClick(subItem)}
|
||||
>
|
||||
|
||||
@@ -4,11 +4,13 @@ import type { TemplateResult } from 'lit';
|
||||
|
||||
export interface AIItemGroupConfig {
|
||||
name?: string;
|
||||
testId?: string;
|
||||
items: AIItemConfig[];
|
||||
}
|
||||
|
||||
export interface AIItemConfig {
|
||||
name: string;
|
||||
testId: string;
|
||||
icon: TemplateResult | (() => HTMLElement);
|
||||
showWhen?: (
|
||||
chain: Chain<InitCommandCtx>,
|
||||
@@ -23,46 +25,6 @@ export interface AIItemConfig {
|
||||
|
||||
export interface AISubItemConfig {
|
||||
type: string;
|
||||
testId?: string;
|
||||
handler?: (host: EditorHost) => void;
|
||||
}
|
||||
|
||||
abstract class BaseAIError extends Error {
|
||||
abstract readonly type: AIErrorType;
|
||||
}
|
||||
|
||||
export enum AIErrorType {
|
||||
GeneralNetworkError = 'GeneralNetworkError',
|
||||
PaymentRequired = 'PaymentRequired',
|
||||
Unauthorized = 'Unauthorized',
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends BaseAIError {
|
||||
readonly type = AIErrorType.Unauthorized;
|
||||
|
||||
constructor() {
|
||||
super('Unauthorized');
|
||||
}
|
||||
}
|
||||
|
||||
// user has used up the quota
|
||||
export class PaymentRequiredError extends BaseAIError {
|
||||
readonly type = AIErrorType.PaymentRequired;
|
||||
|
||||
constructor() {
|
||||
super('Payment required');
|
||||
}
|
||||
}
|
||||
|
||||
// general 500x error
|
||||
export class GeneralNetworkError extends BaseAIError {
|
||||
readonly type = AIErrorType.GeneralNetworkError;
|
||||
|
||||
constructor(message: string = 'Network error') {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export type AIError =
|
||||
| UnauthorizedError
|
||||
| PaymentRequiredError
|
||||
| GeneralNetworkError;
|
||||
|
||||
@@ -131,6 +131,7 @@ export class AskAIButton extends WithDisposable(LitElement) {
|
||||
});
|
||||
return html`<div
|
||||
class="ask-ai-button"
|
||||
data-testid="ask-ai-button"
|
||||
style=${buttonStyles}
|
||||
${toggleType === 'hover' ? ref(this._whenHover.setReference) : nothing}
|
||||
@click=${this._toggleAIPanel}
|
||||
|
||||
@@ -110,7 +110,11 @@ export class AskAIToolbarButton extends WithDisposable(LitElement) {
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`<div class="ask-ai-button" @click=${this._onClick}>
|
||||
return html`<div
|
||||
class="ask-ai-button"
|
||||
data-testid="ask-ai-button"
|
||||
@click=${this._onClick}
|
||||
>
|
||||
<ask-ai-icon .size=${'middle'}></ask-ai-icon>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ export class ChatActionList extends LitElement {
|
||||
@property({ attribute: false })
|
||||
accessor withMargin = false;
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'chat-action-list';
|
||||
|
||||
override render() {
|
||||
const { actions } = this;
|
||||
if (!actions.length) {
|
||||
|
||||
@@ -127,6 +127,9 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
|
||||
@property({ attribute: false })
|
||||
accessor retry = () => {};
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'chat-actions';
|
||||
|
||||
private _toggle() {
|
||||
this._morePopper?.toggle();
|
||||
}
|
||||
@@ -197,7 +200,11 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
|
||||
: nothing}
|
||||
${isLast
|
||||
? nothing
|
||||
: html`<div class="button more" @click=${this._toggle}>
|
||||
: html`<div
|
||||
class="button more"
|
||||
data-testid="action-more-button"
|
||||
@click=${this._toggle}
|
||||
>
|
||||
${MoreHorizontalIcon({ width: '20px', height: '20px' })}
|
||||
</div> `}
|
||||
</div>
|
||||
|
||||
@@ -84,6 +84,7 @@ export type TextRendererOptions = {
|
||||
customHeading?: boolean;
|
||||
extensions?: ExtensionType[];
|
||||
additionalMiddlewares?: TransformerMiddleware[];
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
export const CustomPageEditorBlockSpecs: ExtensionType[] = [
|
||||
@@ -290,13 +291,13 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const { customHeading } = this.options;
|
||||
const { customHeading, testId } = this.options;
|
||||
const classes = classMap({
|
||||
'text-renderer-container': true,
|
||||
'custom-heading': !!customHeading,
|
||||
});
|
||||
return html`
|
||||
<div class=${classes}>
|
||||
<div class=${classes} data-testid=${testId}>
|
||||
${keyed(
|
||||
this._doc,
|
||||
html`<div class="ai-answer-text-editor affine-page-viewport">
|
||||
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
const translateSubItem = translateLangs.map(lang => {
|
||||
return {
|
||||
type: lang,
|
||||
testId: `action-translate-${lang}`,
|
||||
handler: actionToHandler('translate', AIStarIconWithAnimation, { lang }),
|
||||
};
|
||||
});
|
||||
@@ -68,6 +69,7 @@ const translateSubItem = translateLangs.map(lang => {
|
||||
const toneSubItem = textTones.map(tone => {
|
||||
return {
|
||||
type: tone,
|
||||
testId: `action-change-tone-${tone.toLowerCase()}`,
|
||||
handler: actionToHandler('changeTone', AIStarIconWithAnimation, { tone }),
|
||||
};
|
||||
});
|
||||
@@ -75,6 +77,7 @@ const toneSubItem = textTones.map(tone => {
|
||||
export const imageFilterSubItem = imageFilterStyles.map(style => {
|
||||
return {
|
||||
type: style,
|
||||
testId: `action-image-filter-${style.toLowerCase().replace(' ', '-')}`,
|
||||
handler: actionToHandler(
|
||||
'filterImage',
|
||||
AIImageIconWithAnimation,
|
||||
@@ -89,6 +92,7 @@ export const imageFilterSubItem = imageFilterStyles.map(style => {
|
||||
export const imageProcessingSubItem = imageProcessingTypes.map(type => {
|
||||
return {
|
||||
type,
|
||||
testId: `action-image-processing-${type.toLowerCase().replace(' ', '-')}`,
|
||||
handler: actionToHandler(
|
||||
'processImage',
|
||||
AIImageIconWithAnimation,
|
||||
@@ -105,6 +109,7 @@ const othersGroup: AIItemGroupConfig = {
|
||||
items: [
|
||||
{
|
||||
name: 'Continue with AI',
|
||||
testId: 'action-continue-with-ai',
|
||||
icon: CommentIcon({ width: '20px', height: '20px' }),
|
||||
showWhen: () => true,
|
||||
handler: host => {
|
||||
@@ -125,18 +130,21 @@ const editGroup: AIItemGroupConfig = {
|
||||
items: [
|
||||
{
|
||||
name: 'Translate to',
|
||||
testId: 'action-translate',
|
||||
icon: LanguageIcon(),
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
subItem: translateSubItem,
|
||||
},
|
||||
{
|
||||
name: 'Change tone to',
|
||||
testId: 'action-change-tone',
|
||||
icon: ToneIcon(),
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
subItem: toneSubItem,
|
||||
},
|
||||
{
|
||||
name: 'Improve writing',
|
||||
testId: 'action-improve-writing',
|
||||
icon: ImproveWritingIcon(),
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('improveWriting', AIStarIconWithAnimation),
|
||||
@@ -144,18 +152,21 @@ const editGroup: AIItemGroupConfig = {
|
||||
|
||||
{
|
||||
name: 'Make it longer',
|
||||
testId: 'action-make-it-longer',
|
||||
icon: LongerIcon(),
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('makeLonger', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Make it shorter',
|
||||
testId: 'action-make-it-shorter',
|
||||
icon: ShorterIcon(),
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('makeShorter', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Continue writing',
|
||||
testId: 'action-continue-writing',
|
||||
icon: PenIcon(),
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('continueWriting', AIPenIconWithAnimation),
|
||||
@@ -168,30 +179,35 @@ const draftGroup: AIItemGroupConfig = {
|
||||
items: [
|
||||
{
|
||||
name: 'Write an article about this',
|
||||
testId: 'action-write-article',
|
||||
icon: PenIcon(),
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('writeArticle', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Write a tweet about this',
|
||||
testId: 'action-write-twitter-post',
|
||||
icon: PenIcon(),
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('writeTwitterPost', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Write a poem about this',
|
||||
testId: 'action-write-poem',
|
||||
icon: PenIcon(),
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('writePoem', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Write a blog post about this',
|
||||
testId: 'action-write-blog-post',
|
||||
icon: PenIcon(),
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('writeBlogPost', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Brainstorm ideas about this',
|
||||
testId: 'action-brainstorm',
|
||||
icon: PenIcon(),
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('brainstorm', AIPenIconWithAnimation),
|
||||
@@ -205,18 +221,21 @@ const reviewGroup: AIItemGroupConfig = {
|
||||
{
|
||||
name: 'Fix spelling',
|
||||
icon: PenIcon(),
|
||||
testId: 'action-fix-spelling',
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('fixSpelling', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Fix grammar',
|
||||
icon: PenIcon(),
|
||||
testId: 'action-fix-grammar',
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('improveGrammar', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Explain this image',
|
||||
icon: PenIcon(),
|
||||
testId: 'action-explain-image',
|
||||
showWhen: imageOnlyShowWhen,
|
||||
handler: actionToHandler(
|
||||
'explainImage',
|
||||
@@ -228,18 +247,21 @@ const reviewGroup: AIItemGroupConfig = {
|
||||
{
|
||||
name: 'Explain this code',
|
||||
icon: ExplainIcon(),
|
||||
testId: 'action-explain-code',
|
||||
showWhen: noteWithCodeBlockShowWen,
|
||||
handler: actionToHandler('explainCode', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Check code error',
|
||||
icon: ExplainIcon(),
|
||||
testId: 'action-check-code-error',
|
||||
showWhen: noteWithCodeBlockShowWen,
|
||||
handler: actionToHandler('checkCodeErrors', AIStarIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Explain selection',
|
||||
icon: SelectionIcon({ width: '20px', height: '20px' }),
|
||||
testId: 'action-explain-selection',
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('explain', AIStarIconWithAnimation),
|
||||
},
|
||||
@@ -252,19 +274,22 @@ const generateGroup: AIItemGroupConfig = {
|
||||
{
|
||||
name: 'Summarize',
|
||||
icon: PenIcon(),
|
||||
testId: 'action-summarize',
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('summary', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Generate headings',
|
||||
icon: PenIcon(),
|
||||
handler: actionToHandler('createHeadings', AIPenIconWithAnimation),
|
||||
testId: 'action-generate-headings',
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('createHeadings', AIPenIconWithAnimation),
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
name: 'Generate an image',
|
||||
icon: ImageIcon(),
|
||||
testId: 'action-generate-image',
|
||||
showWhen: notAllAIChatBlockShowWhen,
|
||||
handler: actionToHandler(
|
||||
'createImage',
|
||||
@@ -339,12 +364,14 @@ const generateGroup: AIItemGroupConfig = {
|
||||
{
|
||||
name: 'Generate outline',
|
||||
icon: PenIcon(),
|
||||
testId: 'action-generate-outline',
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('writeOutline', AIPenIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Expand from this mind map node',
|
||||
icon: MindmapNodeIcon(),
|
||||
testId: 'action-expand-mindmap-node',
|
||||
showWhen: mindmapChildShowWhen,
|
||||
handler: actionToHandler(
|
||||
'expandMindmap',
|
||||
@@ -370,12 +397,14 @@ const generateGroup: AIItemGroupConfig = {
|
||||
{
|
||||
name: 'Brainstorm ideas with mind map',
|
||||
icon: MindmapIcon(),
|
||||
testId: 'action-brainstorm-mindmap',
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('brainstormMindmap', AIMindMapIconWithAnimation),
|
||||
},
|
||||
{
|
||||
name: 'Regenerate mind map',
|
||||
icon: MindmapIcon(),
|
||||
testId: 'action-regenerate-mindmap',
|
||||
showWhen: mindmapRootShowWhen,
|
||||
handler: actionToHandler(
|
||||
'brainstormMindmap',
|
||||
@@ -388,6 +417,7 @@ const generateGroup: AIItemGroupConfig = {
|
||||
{
|
||||
name: 'Generate presentation',
|
||||
icon: PresentationIcon(),
|
||||
testId: 'action-generate-presentation',
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('createSlides', AIPresentationIconWithAnimation),
|
||||
beta: true,
|
||||
@@ -395,6 +425,7 @@ const generateGroup: AIItemGroupConfig = {
|
||||
{
|
||||
name: 'Make it real',
|
||||
icon: MakeItRealIcon({ width: '20px', height: '20px' }),
|
||||
testId: 'action-make-it-real',
|
||||
beta: true,
|
||||
showWhen: notAllAIChatBlockShowWhen,
|
||||
handler: actionToHandler(
|
||||
@@ -476,6 +507,7 @@ const generateGroup: AIItemGroupConfig = {
|
||||
{
|
||||
name: 'AI image filter',
|
||||
icon: PenIcon(),
|
||||
testId: 'action-ai-image-filter',
|
||||
showWhen: imageOnlyShowWhen,
|
||||
subItem: imageFilterSubItem,
|
||||
subItemOffset: [12, -4],
|
||||
@@ -484,6 +516,7 @@ const generateGroup: AIItemGroupConfig = {
|
||||
{
|
||||
name: 'Image processing',
|
||||
icon: ImageIcon(),
|
||||
testId: 'action-image-processing',
|
||||
showWhen: imageOnlyShowWhen,
|
||||
subItem: imageProcessingSubItem,
|
||||
subItemOffset: [12, -6],
|
||||
@@ -492,6 +525,7 @@ const generateGroup: AIItemGroupConfig = {
|
||||
{
|
||||
name: 'Generate a caption',
|
||||
icon: PenIcon(),
|
||||
testId: 'action-generate-caption',
|
||||
showWhen: imageOnlyShowWhen,
|
||||
beta: true,
|
||||
handler: actionToHandler(
|
||||
@@ -504,6 +538,7 @@ const generateGroup: AIItemGroupConfig = {
|
||||
{
|
||||
name: 'Find actions',
|
||||
icon: SearchIcon(),
|
||||
testId: 'action-find-actions',
|
||||
showWhen: noteBlockOrTextShowWhen,
|
||||
handler: actionToHandler('findActions', AIStarIconWithAnimation),
|
||||
beta: true,
|
||||
|
||||
@@ -10,10 +10,10 @@ import { property } from 'lit/decorators.js';
|
||||
|
||||
import {
|
||||
type AIError,
|
||||
AIProvider,
|
||||
PaymentRequiredError,
|
||||
UnauthorizedError,
|
||||
} from '../components/ai-item/types';
|
||||
import { AIProvider } from '../provider';
|
||||
} from '../provider';
|
||||
|
||||
export class AIErrorWrapper extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
static override styles = css`
|
||||
@@ -181,6 +181,9 @@ export class AIErrorWrapper extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor showDetailPanel: boolean = false;
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'ai-error';
|
||||
}
|
||||
|
||||
const PaymentRequiredErrorRenderer = (host: EditorHost) => html`
|
||||
|
||||
@@ -107,7 +107,7 @@ export const createImageRenderer: (
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
<div class="ai-answer-image">
|
||||
<div class="ai-answer-image" data-testid="ai-answer-image">
|
||||
<img src=${answer}></img>
|
||||
</div>`;
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ import {
|
||||
PROMPT_NAME_AFFINE_AI,
|
||||
PROMPT_NAME_NETWORK_SEARCH,
|
||||
} from '../chat-panel/const';
|
||||
import type { AIError } from '../components/ai-item/types';
|
||||
import { AIProvider } from '../provider';
|
||||
import { type AIError, AIProvider } from '../provider';
|
||||
import { reportResponse } from '../utils/action-reporter';
|
||||
import { readBlobAsURL } from '../utils/image';
|
||||
import { stopPropagation } from '../utils/selection-utils';
|
||||
@@ -188,12 +187,21 @@ export class ChatBlockInput extends SignalWatcher(LitElement) {
|
||||
`
|
||||
: nothing}
|
||||
${images.length < MaximumImageCount
|
||||
? html`<div class="image-upload" @click=${this._handleImageUpload}>
|
||||
? html`<div
|
||||
data-testid="chat-block-input-image-upload"
|
||||
class="image-upload"
|
||||
@click=${this._handleImageUpload}
|
||||
>
|
||||
${ImageIcon()}
|
||||
</div>`
|
||||
: nothing}
|
||||
${status === 'transmitting'
|
||||
? html`<div @click=${this._handleAbort}>${ChatAbortIcon}</div>`
|
||||
? html`<div
|
||||
@click=${this._handleAbort}
|
||||
data-testid="chat-panel-peek-view-stop"
|
||||
>
|
||||
${ChatAbortIcon}
|
||||
</div>`
|
||||
: html`<div
|
||||
@click=${this._onTextareaSend}
|
||||
class="chat-panel-send"
|
||||
|
||||
@@ -28,10 +28,9 @@ import {
|
||||
ChatMessagesSchema,
|
||||
} from '../blocks';
|
||||
import type { AINetworkSearchConfig } from '../chat-panel/chat-config';
|
||||
import type { AIError } from '../components/ai-item/types';
|
||||
import type { TextRendererOptions } from '../components/text-renderer';
|
||||
import { AIChatErrorRenderer } from '../messages/error';
|
||||
import { AIProvider } from '../provider';
|
||||
import { type AIError, AIProvider } from '../provider';
|
||||
import { PeekViewStyles } from './styles';
|
||||
import type { ChatContext } from './types';
|
||||
import { calcChildBound } from './utils';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ChatMessage } from '../blocks';
|
||||
import type { AIError } from '../components/ai-item/types';
|
||||
import type { AIError } from '../provider';
|
||||
|
||||
export type ChatStatus =
|
||||
| 'success'
|
||||
|
||||
@@ -5,8 +5,9 @@ import { Subject } from 'rxjs';
|
||||
import type { ChatContextValue } from '../chat-panel/chat-context';
|
||||
import {
|
||||
PaymentRequiredError,
|
||||
RequestTimeoutError,
|
||||
UnauthorizedError,
|
||||
} from '../components/ai-item/types';
|
||||
} from './error';
|
||||
|
||||
export interface AIUserInfo {
|
||||
id: string;
|
||||
@@ -36,6 +37,7 @@ export type ActionEventType =
|
||||
| 'aborted:login-required'
|
||||
| 'aborted:server-error'
|
||||
| 'aborted:stop'
|
||||
| 'aborted:timeout'
|
||||
| 'result:insert'
|
||||
| 'result:replace'
|
||||
| 'result:use-as-caption'
|
||||
@@ -199,7 +201,13 @@ export class AIProvider {
|
||||
options,
|
||||
event: 'error',
|
||||
});
|
||||
if (err instanceof PaymentRequiredError) {
|
||||
if (err instanceof RequestTimeoutError) {
|
||||
slots.actions.next({
|
||||
action: id,
|
||||
options,
|
||||
event: 'aborted:timeout',
|
||||
});
|
||||
} else if (err instanceof PaymentRequiredError) {
|
||||
slots.actions.next({
|
||||
action: id,
|
||||
options,
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
import {
|
||||
GeneralNetworkError,
|
||||
PaymentRequiredError,
|
||||
UnauthorizedError,
|
||||
} from '@affine/core/blocksuite/ai/components/ai-item/types';
|
||||
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
|
||||
import type { UserFriendlyError } from '@affine/error';
|
||||
import {
|
||||
@@ -31,6 +26,12 @@ import {
|
||||
} from '@affine/graphql';
|
||||
import { getCurrentStore } from '@toeverything/infra';
|
||||
|
||||
import {
|
||||
GeneralNetworkError,
|
||||
PaymentRequiredError,
|
||||
UnauthorizedError,
|
||||
} from './error';
|
||||
|
||||
type OptionsField<T extends GraphQLQuery> =
|
||||
RequestOptions<T>['variables'] extends { options: infer U } ? U : never;
|
||||
|
||||
|
||||
51
packages/frontend/core/src/blocksuite/ai/provider/error.ts
Normal file
51
packages/frontend/core/src/blocksuite/ai/provider/error.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
abstract class BaseAIError extends Error {
|
||||
abstract readonly type: AIErrorType;
|
||||
}
|
||||
|
||||
export enum AIErrorType {
|
||||
GeneralNetworkError = 'GeneralNetworkError',
|
||||
PaymentRequired = 'PaymentRequired',
|
||||
Unauthorized = 'Unauthorized',
|
||||
RequestTimeout = 'RequestTimeout',
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends BaseAIError {
|
||||
readonly type = AIErrorType.Unauthorized;
|
||||
|
||||
constructor() {
|
||||
super('Unauthorized');
|
||||
}
|
||||
}
|
||||
|
||||
// user has used up the quota
|
||||
export class PaymentRequiredError extends BaseAIError {
|
||||
readonly type = AIErrorType.PaymentRequired;
|
||||
|
||||
constructor() {
|
||||
super('Payment required');
|
||||
}
|
||||
}
|
||||
|
||||
// general 500x error
|
||||
export class GeneralNetworkError extends BaseAIError {
|
||||
readonly type = AIErrorType.GeneralNetworkError;
|
||||
|
||||
constructor(message: string = 'Network error') {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
// request timeout
|
||||
export class RequestTimeoutError extends BaseAIError {
|
||||
readonly type = AIErrorType.RequestTimeout;
|
||||
|
||||
constructor(message: string = 'Request timeout') {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export type AIError =
|
||||
| UnauthorizedError
|
||||
| PaymentRequiredError
|
||||
| GeneralNetworkError
|
||||
| RequestTimeoutError;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { handleError } from './copilot-client';
|
||||
import { RequestTimeoutError } from './error';
|
||||
|
||||
export function delay(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
@@ -85,7 +86,7 @@ export function toTextStream(
|
||||
messagePromise,
|
||||
delay(timeout).then(() => {
|
||||
if (!signal?.aborted) {
|
||||
throw new Error('Timeout');
|
||||
throw new RequestTimeoutError();
|
||||
}
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './ai-provider';
|
||||
export * from './copilot-client';
|
||||
export * from './error';
|
||||
export * from './setup-provider';
|
||||
|
||||
@@ -39,6 +39,7 @@ type AIActionEventProperties = {
|
||||
| 'policy wall'
|
||||
| 'server error'
|
||||
| 'login required'
|
||||
| 'request timeout'
|
||||
| 'insert'
|
||||
| 'replace'
|
||||
| 'use as caption'
|
||||
@@ -193,6 +194,8 @@ function inferControl(
|
||||
return 'server error';
|
||||
} else if (event.event === 'aborted:login-required') {
|
||||
return 'login required';
|
||||
} else if (event.event === 'aborted:timeout') {
|
||||
return 'request timeout';
|
||||
} else if (event.options.control === 'chat-send') {
|
||||
return 'AI chat send button';
|
||||
} else if (event.options.control === 'block-action-bar') {
|
||||
|
||||
@@ -31,10 +31,9 @@ import { property, query } from 'lit/decorators.js';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import type { AIError } from '../../components/ai-item/types.js';
|
||||
import { type AIError } from '../../provider';
|
||||
import type { AIPanelGenerating } from './components/index.js';
|
||||
import type { AffineAIPanelState, AffineAIPanelWidgetConfig } from './type.js';
|
||||
|
||||
export const AFFINE_AI_PANEL_WIDGET = 'affine-ai-panel-widget';
|
||||
|
||||
export class AffineAIPanelWidget extends WidgetComponent {
|
||||
@@ -517,7 +516,12 @@ export class AffineAIPanelWidget extends WidgetComponent {
|
||||
],
|
||||
]);
|
||||
|
||||
return html`<div class="ai-panel-container">${mainTemplate}</div>`;
|
||||
return html`<div
|
||||
class="ai-panel-container"
|
||||
data-testid="ai-panel-container"
|
||||
>
|
||||
${mainTemplate}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected override willUpdate(changed: PropertyValues): void {
|
||||
|
||||
@@ -74,9 +74,12 @@ export class AIFinishTip extends WithDisposable(LitElement) {
|
||||
${this.copy?.allowed
|
||||
? html`<div class="right">
|
||||
${this.copied
|
||||
? html`<div class="copied">${AIDoneIcon}</div>`
|
||||
? html`<div class="copied" data-testid="answer-copied">
|
||||
${AIDoneIcon}
|
||||
</div>`
|
||||
: html`<div
|
||||
class="copy"
|
||||
data-testid="answer-copy-button"
|
||||
@click=${async () => {
|
||||
this.copied = !!(await this.copy?.onCopy());
|
||||
if (this.copied) {
|
||||
|
||||
@@ -86,7 +86,7 @@ export class AIPanelAnswer extends WithDisposable(LitElement) {
|
||||
return html`
|
||||
<div class="answer">
|
||||
<div class="answer-head">Answer</div>
|
||||
<div class="answer-body">
|
||||
<div class="answer-body" data-testid="answer-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,7 +104,10 @@ export class AIPanelAnswer extends WithDisposable(LitElement) {
|
||||
${index !== 0
|
||||
? html`<ai-panel-divider></ai-panel-divider>`
|
||||
: nothing}
|
||||
<div class="response-list-container">
|
||||
<div
|
||||
class="response-list-container"
|
||||
data-testid=${group.testId}
|
||||
>
|
||||
<ai-item-list
|
||||
.host=${this.host}
|
||||
.groups=${[group]}
|
||||
@@ -143,6 +146,9 @@ export class AIPanelAnswer extends WithDisposable(LitElement) {
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor host!: EditorHost;
|
||||
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'ai-penel-answer';
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -5,10 +5,8 @@ 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 '../../../../components/ai-item/types.js';
|
||||
import { type AIItemGroupConfig } from '../../../../components/ai-item/types.js';
|
||||
import { AIErrorType } from '../../../../provider';
|
||||
import type { AIPanelErrorConfig, CopyConfig } from '../../type.js';
|
||||
import { filterAIItemGroup } from '../../utils.js';
|
||||
|
||||
@@ -210,7 +208,7 @@ export class AIPanelError extends WithDisposable(LitElement) {
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="error">
|
||||
<div class="error" data-testid="ai-error">
|
||||
<div class="answer-tip">
|
||||
<div class="answer-label">Answer</div>
|
||||
<slot></slot>
|
||||
|
||||
@@ -85,10 +85,10 @@ export class AIPanelGenerating extends WithDisposable(LitElement) {
|
||||
.showHeader=${!this.withAnswer}
|
||||
></generating-placeholder>`
|
||||
: nothing}
|
||||
<div class="generating-tip">
|
||||
<div class="generating-tip" data-testid="ai-generating">
|
||||
<div class="left">${generatingIcon}</div>
|
||||
<div class="text">AI is generating...</div>
|
||||
<div @click=${this.stopGenerating} class="right">
|
||||
<div @click=${this.stopGenerating} class="right" data-testid="ai-stop">
|
||||
<span class="stop-icon">${AIStopIcon}</span>
|
||||
<span class="esc-label">ESC</span>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
import type { nothing, TemplateResult } from 'lit';
|
||||
|
||||
import type {
|
||||
AIError,
|
||||
AIItemGroupConfig,
|
||||
} from '../../components/ai-item/types';
|
||||
import type { AIItemGroupConfig } from '../../components/ai-item/types';
|
||||
import type { AIError } from '../../provider';
|
||||
|
||||
export interface CopyConfig {
|
||||
allowed: boolean;
|
||||
|
||||
@@ -62,6 +62,7 @@ export class EdgelessCopilotToolbarEntry extends WithDisposable(LitElement) {
|
||||
return html`<edgeless-tool-icon-button
|
||||
aria-label="Ask AI"
|
||||
class="copilot-icon-button"
|
||||
data-testid="ask-ai-button"
|
||||
@click=${this._onClick}
|
||||
>
|
||||
${AIStarIcon} <span class="label medium">Ask AI</span>
|
||||
|
||||
@@ -90,7 +90,7 @@ export function patchForEdgelessNoteConfig(
|
||||
]);
|
||||
|
||||
const center = Vec.sub(edgelessTitleAnchor, pageTitleAnchor);
|
||||
gfx.viewport.setCenter(center[0], center[1], true);
|
||||
gfx.viewport.setCenter(center[0], center[1]);
|
||||
gfx.viewport.smoothZoom(0.65, undefined, 15);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { ListLayoutPainterExtension } from '@blocksuite/affine/blocks/list';
|
||||
import { ParagraphLayoutPainterExtension } from '@blocksuite/affine/blocks/paragraph';
|
||||
import { ViewportLayoutPainter } from '@blocksuite/affine/gfx/turbo-renderer';
|
||||
|
||||
new ViewportLayoutPainter([ParagraphLayoutPainterExtension]);
|
||||
new ViewportLayoutPainter([
|
||||
ParagraphLayoutPainterExtension,
|
||||
ListLayoutPainterExtension,
|
||||
]);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ListLayoutHandlerExtension } from '@blocksuite/affine/blocks/list';
|
||||
import { ParagraphLayoutHandlerExtension } from '@blocksuite/affine/blocks/paragraph';
|
||||
import {
|
||||
TurboRendererConfigFactory,
|
||||
@@ -20,6 +21,7 @@ function createPainterWorker() {
|
||||
export function patchTurboRendererExtension() {
|
||||
return [
|
||||
ParagraphLayoutHandlerExtension,
|
||||
ListLayoutHandlerExtension,
|
||||
TurboRendererConfigFactory({
|
||||
options: {
|
||||
zoomThreshold: 1,
|
||||
|
||||
@@ -47,8 +47,10 @@ export const IntegrationCardHeader = ({
|
||||
className,
|
||||
icon,
|
||||
onSettingClick,
|
||||
showSetting = true,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLHeadElement> & {
|
||||
showSetting?: boolean;
|
||||
onSettingClick?: () => void;
|
||||
icon?: ReactNode;
|
||||
}) => {
|
||||
@@ -56,7 +58,7 @@ export const IntegrationCardHeader = ({
|
||||
<header className={clsx(cardHeader, className)} {...props}>
|
||||
<IntegrationCardIcon>{icon}</IntegrationCardIcon>
|
||||
<div className={spaceX} />
|
||||
<IntegrationSettingIcon onClick={onSettingClick} />
|
||||
{showSetting ? <IntegrationSettingIcon onClick={onSettingClick} /> : null}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ export const ReadwiseIntegration = () => {
|
||||
<IntegrationCardHeader
|
||||
icon={<IntegrationTypeIcon type="readwise" />}
|
||||
onSettingClick={handleOpenSetting}
|
||||
showSetting={!!token}
|
||||
/>
|
||||
<IntegrationCardContent
|
||||
title={t['com.affine.integration.readwise.name']()}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import * as styles from './style.css';
|
||||
|
||||
const clipperInput$ = new LiveData<ClipperInput | null>(null);
|
||||
const port$ = new LiveData<MessagePort | null>(null);
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
if (
|
||||
@@ -28,6 +29,10 @@ window.addEventListener('message', event => {
|
||||
event.data.type === 'affine-clipper:import'
|
||||
) {
|
||||
clipperInput$.value = event.data.payload;
|
||||
|
||||
if (event.ports.length > 0) {
|
||||
port$.value = event.ports[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -90,6 +95,17 @@ export const Component = () => {
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSuccess = useCallback(() => {
|
||||
const arg = { type: 'affine-clipper:import:success' };
|
||||
const port = port$.value;
|
||||
if (port) {
|
||||
port.postMessage(arg);
|
||||
} else {
|
||||
window.postMessage(arg);
|
||||
}
|
||||
window.close();
|
||||
}, []);
|
||||
|
||||
const handleImportToSelectedWorkspace = useAsyncCallback(async () => {
|
||||
if (clipperInputSnapshot && selectedWorkspace) {
|
||||
setImporting(true);
|
||||
@@ -98,17 +114,19 @@ export const Component = () => {
|
||||
selectedWorkspace,
|
||||
clipperInputSnapshot
|
||||
);
|
||||
window.postMessage({
|
||||
type: 'affine-clipper:import:success',
|
||||
});
|
||||
window.close();
|
||||
handleSuccess();
|
||||
} catch (err) {
|
||||
setImportingError(err);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
}
|
||||
}, [clipperInputSnapshot, importClipperService, selectedWorkspace]);
|
||||
}, [
|
||||
clipperInputSnapshot,
|
||||
handleSuccess,
|
||||
importClipperService,
|
||||
selectedWorkspace,
|
||||
]);
|
||||
|
||||
const handleImportToNewWorkspace = useAsyncCallback(async () => {
|
||||
if (!clipperInputSnapshot) {
|
||||
@@ -121,16 +139,13 @@ export const Component = () => {
|
||||
'Workspace',
|
||||
clipperInputSnapshot
|
||||
);
|
||||
window.postMessage({
|
||||
type: 'affine-clipper:import:success',
|
||||
});
|
||||
window.close();
|
||||
handleSuccess();
|
||||
} catch (err) {
|
||||
setImportingError(err);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
}, [clipperInputSnapshot, importClipperService]);
|
||||
}, [clipperInputSnapshot, handleSuccess, importClipperService]);
|
||||
|
||||
const handleClickSignIn = useCallback(() => {
|
||||
window.open(
|
||||
|
||||
@@ -53,6 +53,11 @@ export class MeetingSettingsService extends Service {
|
||||
return;
|
||||
}
|
||||
|
||||
this.globalStateService.globalState.set(MEETING_SETTINGS_KEY, {
|
||||
...this.settings$.value,
|
||||
enabled,
|
||||
});
|
||||
|
||||
// when the user enable the recording feature the first time,
|
||||
// the app may prompt the user to allow the recording feature by MacOS.
|
||||
// when the user allows the recording feature, the app may be required to restart.
|
||||
@@ -77,12 +82,6 @@ export class MeetingSettingsService extends Service {
|
||||
// if the user disabled the recording feature, we need to setup the recording feature
|
||||
await this.desktopApiService?.handler.recording.disableRecordingFeature();
|
||||
}
|
||||
|
||||
// Only update the state after successful feature setup/disable
|
||||
this.globalStateService.globalState.set(MEETING_SETTINGS_KEY, {
|
||||
...this.settings$.value,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
setRecordingSavingMode(mode: MeetingSettingsSchema['recordingSavingMode']) {
|
||||
|
||||
@@ -58,8 +58,7 @@ function fitViewport(
|
||||
viewport.setViewportByBound(
|
||||
Bound.deserialize(newViewport.xywh),
|
||||
newViewport.padding,
|
||||
false,
|
||||
true
|
||||
false
|
||||
);
|
||||
} else {
|
||||
gfx.fitToScreen({
|
||||
|
||||
@@ -43,17 +43,17 @@ export class UserDBEngine extends Entity<{
|
||||
doc: {
|
||||
name: this.DocStorageType.identifier,
|
||||
opts: {
|
||||
id: `${serverService.server.id}:` + this.userId,
|
||||
flavour: serverService.server.id,
|
||||
type: 'userspace',
|
||||
flavour: serverService.server.id,
|
||||
id: this.userId,
|
||||
},
|
||||
},
|
||||
docSync: {
|
||||
name: this.DocSyncStorageType.identifier,
|
||||
opts: {
|
||||
id: `${serverService.server.id}:` + this.userId,
|
||||
type: 'userspace',
|
||||
flavour: serverService.server.id,
|
||||
id: this.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const sidebarContainerInner = style({
|
||||
@@ -12,11 +13,11 @@ export const sidebarContainerInner = style({
|
||||
selectors: {
|
||||
['[data-client-border=true] &']: {
|
||||
borderRadius: 6,
|
||||
border: `0.5px solid ${cssVar('borderColor')}`,
|
||||
border: `0.5px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||
},
|
||||
['[data-client-border=true][data-is-floating="true"] &']: {
|
||||
boxShadow: cssVar('shadow3'),
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
border: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -31,7 +32,7 @@ export const sidebarBodyTarget = style({
|
||||
});
|
||||
|
||||
export const borderTop = style({
|
||||
borderTop: `0.5px solid ${cssVar('borderColor')}`,
|
||||
borderTop: `0.5px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||
});
|
||||
|
||||
export const sidebarBodyNoSelection = style({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user