feat(editor): remove chat-cards and Open AI Chat menu item (#9626)
Support issue [BS-2353](https://linear.app/affine-design/issue/BS-2353). ### What changed? - Remove `Open AI Chat` menu item. - Remove `chat-cards` web component. - Add `extractAll` function for page and edgeless doc full content extract.
This commit is contained in:
@@ -15,7 +15,7 @@ import { property } from 'lit/decorators.js';
|
|||||||
|
|
||||||
import { AIProvider } from '../../provider';
|
import { AIProvider } from '../../provider';
|
||||||
import { getAIPanelWidget } from '../../utils/ai-widgets';
|
import { getAIPanelWidget } from '../../utils/ai-widgets';
|
||||||
import { extractContext } from '../../utils/extract';
|
import { extractSelectedContent } from '../../utils/extract';
|
||||||
|
|
||||||
export class AskAIToolbarButton extends WithDisposable(LitElement) {
|
export class AskAIToolbarButton extends WithDisposable(LitElement) {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
@@ -75,8 +75,7 @@ export class AskAIToolbarButton extends WithDisposable(LitElement) {
|
|||||||
finish('success');
|
finish('success');
|
||||||
const aiPanel = getAIPanelWidget(this.host);
|
const aiPanel = getAIPanelWidget(this.host);
|
||||||
aiPanel.discard();
|
aiPanel.discard();
|
||||||
AIProvider.slots.requestOpenWithChat.emit({ host: this.host });
|
extractSelectedContent(this.host)
|
||||||
extractContext(this.host)
|
|
||||||
.then(context => {
|
.then(context => {
|
||||||
AIProvider.slots.requestSendWithChat.emit({
|
AIProvider.slots.requestSendWithChat.emit({
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
AIPresentationIconWithAnimation,
|
AIPresentationIconWithAnimation,
|
||||||
AISearchIcon,
|
AISearchIcon,
|
||||||
AIStarIconWithAnimation,
|
AIStarIconWithAnimation,
|
||||||
ChatWithAIIcon,
|
|
||||||
CommentIcon,
|
CommentIcon,
|
||||||
ExplainIcon,
|
ExplainIcon,
|
||||||
ImproveWritingIcon,
|
ImproveWritingIcon,
|
||||||
@@ -336,19 +335,6 @@ const OthersAIGroup: AIItemGroupConfig = {
|
|||||||
AIProvider.slots.requestOpenWithChat.emit({
|
AIProvider.slots.requestOpenWithChat.emit({
|
||||||
host,
|
host,
|
||||||
autoSelect: true,
|
autoSelect: true,
|
||||||
appendCard: true,
|
|
||||||
});
|
|
||||||
panel.hide();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Open AI Chat',
|
|
||||||
icon: ChatWithAIIcon,
|
|
||||||
handler: host => {
|
|
||||||
const panel = getAIPanelWidget(host);
|
|
||||||
AIProvider.slots.requestOpenWithChat.emit({
|
|
||||||
host,
|
|
||||||
appendCard: true,
|
|
||||||
});
|
});
|
||||||
panel.hide();
|
panel.hide();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -566,10 +566,7 @@ export function actionToResponse<T extends keyof BlockSuitePresets.AIActions>(
|
|||||||
handler: () => {
|
handler: () => {
|
||||||
reportResponse('result:continue-in-chat');
|
reportResponse('result:continue-in-chat');
|
||||||
const panel = getAIPanelWidget(host);
|
const panel = getAIPanelWidget(host);
|
||||||
AIProvider.slots.requestOpenWithChat.emit({
|
AIProvider.slots.requestOpenWithChat.emit({ host });
|
||||||
host,
|
|
||||||
appendCard: true,
|
|
||||||
});
|
|
||||||
panel.hide();
|
panel.hide();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -191,10 +191,7 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
|
|||||||
icon: ChatWithAIIcon,
|
icon: ChatWithAIIcon,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
reportResponse('result:continue-in-chat');
|
reportResponse('result:continue-in-chat');
|
||||||
AIProvider.slots.requestOpenWithChat.emit({
|
AIProvider.slots.requestOpenWithChat.emit({ host });
|
||||||
host,
|
|
||||||
appendCard: true,
|
|
||||||
});
|
|
||||||
panel.hide();
|
panel.hide();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,560 +0,0 @@
|
|||||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
|
||||||
import {
|
|
||||||
type ImageBlockModel,
|
|
||||||
isInsideEdgelessEditor,
|
|
||||||
type NoteBlockModel,
|
|
||||||
NoteDisplayMode,
|
|
||||||
} from '@blocksuite/affine/blocks';
|
|
||||||
import { WithDisposable } from '@blocksuite/affine/global/utils';
|
|
||||||
import type { BlockModel } from '@blocksuite/affine/store';
|
|
||||||
import { captureException } from '@sentry/react';
|
|
||||||
import {
|
|
||||||
css,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
nothing,
|
|
||||||
type PropertyValues,
|
|
||||||
type TemplateResult,
|
|
||||||
} from 'lit';
|
|
||||||
import { property, state } from 'lit/decorators.js';
|
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CurrentSelectionIcon,
|
|
||||||
DocIcon,
|
|
||||||
SmallImageIcon,
|
|
||||||
} from '../_common/icons';
|
|
||||||
import { type AIChatParams, AIProvider } from '../provider';
|
|
||||||
import {
|
|
||||||
getSelectedImagesAsBlobs,
|
|
||||||
getSelectedTextContent,
|
|
||||||
getTextContentFromBlockModels,
|
|
||||||
selectedToCanvas,
|
|
||||||
} from '../utils/selection-utils';
|
|
||||||
import type { ChatContextValue } from './chat-context';
|
|
||||||
|
|
||||||
const cardsStyles = css`
|
|
||||||
.card-wrapper {
|
|
||||||
width: 90%;
|
|
||||||
max-height: 76px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--affine-border-color);
|
|
||||||
padding: 4px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
height: 22px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--affine-text-primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.second-text {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--affine-text-secondary-color);
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
enum CardType {
|
|
||||||
Text,
|
|
||||||
Image,
|
|
||||||
Block,
|
|
||||||
Doc,
|
|
||||||
}
|
|
||||||
|
|
||||||
type CardBase = {
|
|
||||||
id: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CardText = CardBase & {
|
|
||||||
type: CardType.Text;
|
|
||||||
text: string;
|
|
||||||
markdown: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CardImage = CardBase & {
|
|
||||||
type: CardType.Image;
|
|
||||||
image: File;
|
|
||||||
caption?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CardBlock = CardBase & {
|
|
||||||
type: CardType.Block | CardType.Doc;
|
|
||||||
text?: string;
|
|
||||||
markdown?: string;
|
|
||||||
images?: File[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Card = CardText | CardImage | CardBlock;
|
|
||||||
|
|
||||||
const MAX_CARDS = 3;
|
|
||||||
|
|
||||||
export class ChatCards extends WithDisposable(LitElement) {
|
|
||||||
static override styles = css`
|
|
||||||
:host {
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([data-show]) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
${cardsStyles}
|
|
||||||
`;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor host!: EditorHost;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor temporaryParams: AIChatParams | null = null;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor isEmpty!: boolean;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor cards: Card[] = [];
|
|
||||||
|
|
||||||
private _currentDocId: string | null = null;
|
|
||||||
private _selectedCardId: number = 0;
|
|
||||||
|
|
||||||
static renderText({ text }: CardText) {
|
|
||||||
const lines = text.split('\n');
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="card-wrapper">
|
|
||||||
<div class="card-title">
|
|
||||||
${CurrentSelectionIcon}
|
|
||||||
<div>Start with current selection</div>
|
|
||||||
</div>
|
|
||||||
<div class="second-text">
|
|
||||||
${repeat(
|
|
||||||
lines.slice(0, 2),
|
|
||||||
line => line,
|
|
||||||
line => html`
|
|
||||||
<div
|
|
||||||
style=${styleMap({
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
${line}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static renderImage({ caption, image }: CardImage) {
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
class="card-wrapper"
|
|
||||||
style=${styleMap({
|
|
||||||
display: 'flex',
|
|
||||||
gap: '8px',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style=${styleMap({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
maxWidth: 'calc(100% - 72px)',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div class="card-title">
|
|
||||||
${SmallImageIcon}
|
|
||||||
<div>Start with this Image</div>
|
|
||||||
</div>
|
|
||||||
<div class="second-text">${caption ? caption : 'caption'}</div>
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
style=${styleMap({
|
|
||||||
maxWidth: '72px',
|
|
||||||
maxHeight: '46px',
|
|
||||||
})}
|
|
||||||
src="${URL.createObjectURL(image)}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static renderDoc({ text, images }: CardBlock) {
|
|
||||||
let textTpl = html`you've chosen within the doc`;
|
|
||||||
let imageTpl: TemplateResult<1> | typeof nothing = nothing;
|
|
||||||
let hasImage = false;
|
|
||||||
|
|
||||||
if (text?.length) {
|
|
||||||
const lines = text.split('\n');
|
|
||||||
textTpl = html`${repeat(
|
|
||||||
lines.slice(0, 2),
|
|
||||||
line => line,
|
|
||||||
line => html`
|
|
||||||
<div
|
|
||||||
style=${styleMap({
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
${line}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (images?.length) {
|
|
||||||
hasImage = true;
|
|
||||||
imageTpl = html`
|
|
||||||
<img
|
|
||||||
style=${styleMap({
|
|
||||||
maxWidth: '72px',
|
|
||||||
maxHeight: '46px',
|
|
||||||
})}
|
|
||||||
src="${URL.createObjectURL(images[0])}"
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
class="card-wrapper"
|
|
||||||
style=${styleMap({
|
|
||||||
display: 'flex',
|
|
||||||
gap: '8px',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style=${styleMap({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
maxWidth: hasImage ? 'calc(100% - 72px)' : '100%',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div class="card-title">
|
|
||||||
${DocIcon}
|
|
||||||
<div>Start with this doc</div>
|
|
||||||
</div>
|
|
||||||
<div class="second-text">${textTpl}</div>
|
|
||||||
</div>
|
|
||||||
${imageTpl}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderCard(card: Card) {
|
|
||||||
if (card.type === CardType.Text) {
|
|
||||||
return ChatCards.renderText(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (card.type === CardType.Image) {
|
|
||||||
return ChatCards.renderImage(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (card.type === CardType.Doc) {
|
|
||||||
return ChatCards.renderDoc(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateCards(card: Card) {
|
|
||||||
this.cards.unshift(card);
|
|
||||||
|
|
||||||
if (this.cards.length > MAX_CARDS) {
|
|
||||||
this.cards.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handleDocSelection(card: CardBlock) {
|
|
||||||
const { text, markdown, images } = await this._extractAll();
|
|
||||||
|
|
||||||
card.text = text;
|
|
||||||
card.markdown = markdown;
|
|
||||||
card.images = images;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _selectCard(card: Card) {
|
|
||||||
AIProvider.slots.toggleChatCards.emit({ visible: false });
|
|
||||||
|
|
||||||
this._selectedCardId = card.id;
|
|
||||||
|
|
||||||
switch (card.type) {
|
|
||||||
case CardType.Text: {
|
|
||||||
this.updateContext({
|
|
||||||
quote: card.text,
|
|
||||||
markdown: card.markdown,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CardType.Image: {
|
|
||||||
this.updateContext({
|
|
||||||
images: [card.image],
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CardType.Block: {
|
|
||||||
this.updateContext({
|
|
||||||
quote: card.text,
|
|
||||||
markdown: card.markdown,
|
|
||||||
images: card.images,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case CardType.Doc: {
|
|
||||||
await this._handleDocSelection(card);
|
|
||||||
this.updateContext({
|
|
||||||
quote: card.text,
|
|
||||||
markdown: card.markdown,
|
|
||||||
images: card.images,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _extract() {
|
|
||||||
const text = await getSelectedTextContent(this.host, 'plain-text');
|
|
||||||
const images = await getSelectedImagesAsBlobs(this.host);
|
|
||||||
const hasText = text.length > 0;
|
|
||||||
const hasImages = images.length > 0;
|
|
||||||
|
|
||||||
if (hasText && !hasImages) {
|
|
||||||
const markdown = await getSelectedTextContent(this.host, 'markdown');
|
|
||||||
|
|
||||||
this._updateCards({
|
|
||||||
id: Date.now(),
|
|
||||||
type: CardType.Text,
|
|
||||||
text,
|
|
||||||
markdown,
|
|
||||||
});
|
|
||||||
} else if (!hasText && hasImages && images.length === 1) {
|
|
||||||
const [_, data] = this.host.command
|
|
||||||
.chain()
|
|
||||||
.tryAll(chain => [chain.getImageSelections()])
|
|
||||||
.getSelectedBlocks({
|
|
||||||
types: ['image'],
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
let caption = '';
|
|
||||||
|
|
||||||
if (data.currentImageSelections?.[0]) {
|
|
||||||
caption =
|
|
||||||
(
|
|
||||||
this.host.doc.getBlock(data.currentImageSelections[0].blockId)
|
|
||||||
?.model as ImageBlockModel
|
|
||||||
)?.caption ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateCards({
|
|
||||||
id: Date.now(),
|
|
||||||
type: CardType.Image,
|
|
||||||
image: images[0],
|
|
||||||
caption,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const markdown =
|
|
||||||
(await getSelectedTextContent(this.host, 'markdown')).trim() || '';
|
|
||||||
this._updateCards({
|
|
||||||
id: Date.now(),
|
|
||||||
type: CardType.Block,
|
|
||||||
text,
|
|
||||||
markdown,
|
|
||||||
images,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _extractOnEdgeless(host: EditorHost) {
|
|
||||||
if (!isInsideEdgelessEditor(host)) return;
|
|
||||||
|
|
||||||
const canvas = await selectedToCanvas(host);
|
|
||||||
if (!canvas) return;
|
|
||||||
|
|
||||||
const blob: Blob | null = await new Promise(resolve =>
|
|
||||||
canvas.toBlob(resolve)
|
|
||||||
);
|
|
||||||
if (!blob) return;
|
|
||||||
|
|
||||||
this._updateCards({
|
|
||||||
id: Date.now(),
|
|
||||||
type: CardType.Image,
|
|
||||||
image: new File([blob], 'selected.png'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _extractAll() {
|
|
||||||
const notes = this.host.doc
|
|
||||||
.getBlocksByFlavour('affine:note')
|
|
||||||
.filter(
|
|
||||||
note =>
|
|
||||||
(note.model as NoteBlockModel).displayMode !==
|
|
||||||
NoteDisplayMode.EdgelessOnly
|
|
||||||
)
|
|
||||||
.map(note => note.model as NoteBlockModel);
|
|
||||||
const selectedModels = notes.reduce((acc, note) => {
|
|
||||||
acc.push(...note.children);
|
|
||||||
return acc;
|
|
||||||
}, [] as BlockModel[]);
|
|
||||||
const text = await getTextContentFromBlockModels(
|
|
||||||
this.host,
|
|
||||||
selectedModels,
|
|
||||||
'plain-text'
|
|
||||||
);
|
|
||||||
const markdown = await getTextContentFromBlockModels(
|
|
||||||
this.host,
|
|
||||||
selectedModels,
|
|
||||||
'markdown'
|
|
||||||
);
|
|
||||||
const blobs = await Promise.all(
|
|
||||||
selectedModels.map(async s => {
|
|
||||||
if (s.flavour !== 'affine:image') return null;
|
|
||||||
const sourceId = (s as ImageBlockModel)?.sourceId;
|
|
||||||
if (!sourceId) return null;
|
|
||||||
const blob = await (sourceId
|
|
||||||
? this.host.doc.blobSync.get(sourceId)
|
|
||||||
: null);
|
|
||||||
if (!blob) return null;
|
|
||||||
return new File([blob], sourceId);
|
|
||||||
}) ?? []
|
|
||||||
);
|
|
||||||
const images = blobs.filter((blob): blob is File => !!blob);
|
|
||||||
|
|
||||||
return {
|
|
||||||
text,
|
|
||||||
markdown,
|
|
||||||
images,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _appendCardWithParams({
|
|
||||||
host,
|
|
||||||
mode,
|
|
||||||
autoSelect,
|
|
||||||
}: AIChatParams) {
|
|
||||||
if (mode === 'edgeless') {
|
|
||||||
await this._extractOnEdgeless(host);
|
|
||||||
} else {
|
|
||||||
await this._extract();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!autoSelect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.cards.length > 0) {
|
|
||||||
const card = this.cards[0];
|
|
||||||
if (card.type === CardType.Doc) return;
|
|
||||||
|
|
||||||
await this._selectCard(card);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override willUpdate(changedProperties: PropertyValues) {
|
|
||||||
Promise.resolve()
|
|
||||||
.then(async () => {
|
|
||||||
if (changedProperties.has('temporaryParams') && this.temporaryParams) {
|
|
||||||
const params = this.temporaryParams;
|
|
||||||
await this._appendCardWithParams(params);
|
|
||||||
this.temporaryParams = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedProperties.has('host')) {
|
|
||||||
if (this._currentDocId === this.host.doc.id) return;
|
|
||||||
this._currentDocId = this.host.doc.id;
|
|
||||||
this.cards = [];
|
|
||||||
|
|
||||||
const { text, images } = await this._extractAll();
|
|
||||||
const hasText = text.length > 0;
|
|
||||||
const hasImages = images.length > 0;
|
|
||||||
|
|
||||||
// Currently only supports checking on first load
|
|
||||||
if (hasText || hasImages) {
|
|
||||||
const card: CardBlock = {
|
|
||||||
id: Date.now(),
|
|
||||||
type: CardType.Doc,
|
|
||||||
};
|
|
||||||
if (hasText) {
|
|
||||||
card.text = text;
|
|
||||||
}
|
|
||||||
if (hasImages) {
|
|
||||||
card.images = images;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cards.push(card);
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
captureException(err, {
|
|
||||||
level: 'fatal',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
override connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
|
|
||||||
this._disposables.add(
|
|
||||||
AIProvider.slots.requestOpenWithChat.on(async params => {
|
|
||||||
if (params.appendCard) {
|
|
||||||
await this._appendCardWithParams(params);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this._disposables.add(
|
|
||||||
AIProvider.slots.toggleChatCards.on(({ visible, ok }) => {
|
|
||||||
if (visible && ok && this._selectedCardId > 0) {
|
|
||||||
this.cards = this.cards.filter(
|
|
||||||
card => card.id !== this._selectedCardId
|
|
||||||
);
|
|
||||||
this._selectedCardId = 0;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override render() {
|
|
||||||
if (!this.isEmpty) return nothing;
|
|
||||||
|
|
||||||
return repeat(
|
|
||||||
this.cards,
|
|
||||||
card => card.id,
|
|
||||||
card => html`
|
|
||||||
<div @click=${() => this._selectCard(card)}>
|
|
||||||
${this._renderCard(card)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'chat-cards': ChatCards;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -313,8 +313,6 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
<div
|
<div
|
||||||
class="close-wrapper"
|
class="close-wrapper"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
AIProvider.slots.toggleChatCards.emit({ visible: true });
|
|
||||||
|
|
||||||
if (this.curIndex >= 0 && this.curIndex < images.length) {
|
if (this.curIndex >= 0 && this.curIndex < images.length) {
|
||||||
const newImages = [...images];
|
const newImages = [...images];
|
||||||
newImages.splice(this.curIndex, 1);
|
newImages.splice(this.curIndex, 1);
|
||||||
@@ -356,7 +354,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
if (this.host === host) {
|
if (this.host === host) {
|
||||||
context && this.updateContext(context);
|
context && this.updateContext(context);
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
input && (await this.send(input));
|
await this.send(input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -392,7 +390,6 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
<div
|
<div
|
||||||
class="chat-quote-close"
|
class="chat-quote-close"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
AIProvider.slots.toggleChatCards.emit({ visible: true });
|
|
||||||
this.updateContext({ quote: '', markdown: '' });
|
this.updateContext({ quote: '', markdown: '' });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -40,12 +40,6 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
chat-cards {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-panel-messages-placeholder {
|
.chat-panel-messages-placeholder {
|
||||||
@@ -138,9 +132,6 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
@query('.chat-panel-messages')
|
@query('.chat-panel-messages')
|
||||||
accessor messagesContainer: HTMLDivElement | null = null;
|
accessor messagesContainer: HTMLDivElement | null = null;
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor showChatCards = true;
|
|
||||||
|
|
||||||
private _renderAIOnboarding() {
|
private _renderAIOnboarding() {
|
||||||
return this.isLoading ||
|
return this.isLoading ||
|
||||||
!this.host?.doc.get(FeatureFlagService).getFlag('enable_ai_onboarding')
|
!this.host?.doc.get(FeatureFlagService).getFlag('enable_ai_onboarding')
|
||||||
@@ -251,12 +242,6 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
<chat-cards
|
|
||||||
.updateContext=${this.updateContext}
|
|
||||||
.host=${this.host}
|
|
||||||
.isEmpty=${items.length === 0}
|
|
||||||
?data-show=${this.showChatCards}
|
|
||||||
></chat-cards>
|
|
||||||
</div>
|
</div>
|
||||||
${this.showDownIndicator
|
${this.showDownIndicator
|
||||||
? html`<div class="down-indicator" @click=${this.scrollToEnd}>
|
? html`<div class="down-indicator" @click=${this.scrollToEnd}>
|
||||||
@@ -289,11 +274,6 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
disposables.add(
|
|
||||||
AIProvider.slots.toggleChatCards.on(({ visible }) => {
|
|
||||||
this.showChatCards = visible;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
disposables.add(
|
disposables.add(
|
||||||
this.host.selection.slots.changed.on(() => {
|
this.host.selection.slots.changed.on(() => {
|
||||||
this._selectionValue = this.host.selection.value;
|
this._selectionValue = this.host.selection.value;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { createRef, type Ref, ref } from 'lit/directives/ref.js';
|
|||||||
|
|
||||||
import { AIHelpIcon, SmallHintIcon } from '../_common/icons';
|
import { AIHelpIcon, SmallHintIcon } from '../_common/icons';
|
||||||
import { AIProvider } from '../provider';
|
import { AIProvider } from '../provider';
|
||||||
|
import { extractSelectedContent } from '../utils/extract';
|
||||||
import {
|
import {
|
||||||
getSelectedImagesAsBlobs,
|
getSelectedImagesAsBlobs,
|
||||||
getSelectedTextContent,
|
getSelectedTextContent,
|
||||||
@@ -222,30 +223,34 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (!this.doc) throw new Error('doc is required');
|
if (!this.doc) throw new Error('doc is required');
|
||||||
|
|
||||||
AIProvider.slots.actions.on(({ action, event }) => {
|
this._disposables.add(
|
||||||
const { status } = this.chatContextValue;
|
AIProvider.slots.actions.on(({ action, event }) => {
|
||||||
|
const { status } = this.chatContextValue;
|
||||||
if (
|
if (
|
||||||
action !== 'chat' &&
|
action !== 'chat' &&
|
||||||
event === 'finished' &&
|
event === 'finished' &&
|
||||||
(status === 'idle' || status === 'success')
|
(status === 'idle' || status === 'success')
|
||||||
) {
|
) {
|
||||||
this._resetItems();
|
this._resetItems();
|
||||||
}
|
}
|
||||||
|
})
|
||||||
if (action === 'chat' && event === 'finished') {
|
);
|
||||||
AIProvider.slots.toggleChatCards.emit({
|
this._disposables.add(
|
||||||
visible: true,
|
AIProvider.slots.userInfo.on(userInfo => {
|
||||||
ok: status === 'success',
|
if (userInfo) {
|
||||||
});
|
this._resetItems();
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
AIProvider.slots.userInfo.on(userInfo => {
|
this._disposables.add(
|
||||||
if (userInfo) {
|
AIProvider.slots.requestOpenWithChat.on(async ({ host }) => {
|
||||||
this._resetItems();
|
if (this.host === host) {
|
||||||
}
|
const context = await extractSelectedContent(host);
|
||||||
});
|
if (!context) return;
|
||||||
|
this.updateContext(context);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateContext = (context: Partial<ChatContextValue>) => {
|
updateContext = (context: Partial<ChatContextValue>) => {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
AIPresentationIcon,
|
AIPresentationIcon,
|
||||||
AIPresentationIconWithAnimation,
|
AIPresentationIconWithAnimation,
|
||||||
AISearchIcon,
|
AISearchIcon,
|
||||||
ChatWithAIIcon,
|
|
||||||
CommentIcon,
|
CommentIcon,
|
||||||
ExplainIcon,
|
ExplainIcon,
|
||||||
ImproveWritingIcon,
|
ImproveWritingIcon,
|
||||||
@@ -110,21 +109,6 @@ const othersGroup: AIItemGroupConfig = {
|
|||||||
host,
|
host,
|
||||||
mode: 'edgeless',
|
mode: 'edgeless',
|
||||||
autoSelect: true,
|
autoSelect: true,
|
||||||
appendCard: true,
|
|
||||||
});
|
|
||||||
panel.hide();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Open AI Chat',
|
|
||||||
icon: ChatWithAIIcon,
|
|
||||||
showWhen: () => true,
|
|
||||||
handler: host => {
|
|
||||||
const panel = getAIPanelWidget(host);
|
|
||||||
AIProvider.slots.requestOpenWithChat.emit({
|
|
||||||
host,
|
|
||||||
mode: 'edgeless',
|
|
||||||
appendCard: true,
|
|
||||||
});
|
});
|
||||||
panel.hide();
|
panel.hide();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { html } from 'lit';
|
|||||||
import { AIProvider } from '../../provider';
|
import { AIProvider } from '../../provider';
|
||||||
import { getAIPanelWidget } from '../../utils/ai-widgets';
|
import { getAIPanelWidget } from '../../utils/ai-widgets';
|
||||||
import { getEdgelessCopilotWidget } from '../../utils/edgeless';
|
import { getEdgelessCopilotWidget } from '../../utils/edgeless';
|
||||||
import { extractContext } from '../../utils/extract';
|
import { extractSelectedContent } from '../../utils/extract';
|
||||||
import { edgelessAIGroups } from './actions-config';
|
import { edgelessAIGroups } from './actions-config';
|
||||||
|
|
||||||
noop(EdgelessCopilotToolbarEntry);
|
noop(EdgelessCopilotToolbarEntry);
|
||||||
@@ -48,8 +48,7 @@ export function setupEdgelessElementToolbarAIEntry(
|
|||||||
aiPanel.config.generateAnswer = ({ finish, input }) => {
|
aiPanel.config.generateAnswer = ({ finish, input }) => {
|
||||||
finish('success');
|
finish('success');
|
||||||
aiPanel.discard();
|
aiPanel.discard();
|
||||||
AIProvider.slots.requestOpenWithChat.emit({ host: edgeless.host });
|
extractSelectedContent(edgeless.host)
|
||||||
extractContext(edgeless.host)
|
|
||||||
.then(context => {
|
.then(context => {
|
||||||
AIProvider.slots.requestSendWithChat.emit({
|
AIProvider.slots.requestSendWithChat.emit({
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -20,12 +20,11 @@ export interface AIChatParams {
|
|||||||
mode?: 'page' | 'edgeless';
|
mode?: 'page' | 'edgeless';
|
||||||
// Auto select and append selection to input via `Continue with AI` action.
|
// Auto select and append selection to input via `Continue with AI` action.
|
||||||
autoSelect?: boolean;
|
autoSelect?: boolean;
|
||||||
appendCard?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AISendParams {
|
export interface AISendParams {
|
||||||
host: EditorHost;
|
host: EditorHost;
|
||||||
input?: string;
|
input: string;
|
||||||
context?: Partial<ChatContextValue | null>;
|
context?: Partial<ChatContextValue | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,11 +127,6 @@ export class AIProvider {
|
|||||||
}>(),
|
}>(),
|
||||||
// downstream can emit this slot to notify ai presets that user info has been updated
|
// downstream can emit this slot to notify ai presets that user info has been updated
|
||||||
userInfo: new Slot<AIUserInfo | null>(),
|
userInfo: new Slot<AIUserInfo | null>(),
|
||||||
// add more if needed
|
|
||||||
toggleChatCards: new Slot<{
|
|
||||||
visible: boolean;
|
|
||||||
ok?: boolean;
|
|
||||||
}>(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// track the history of triggered actions (in memory only)
|
// track the history of triggered actions (in memory only)
|
||||||
|
|||||||
@@ -1,29 +1,35 @@
|
|||||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||||
import {
|
import {
|
||||||
DocModeProvider,
|
DocModeProvider,
|
||||||
|
type ImageBlockModel,
|
||||||
isInsideEdgelessEditor,
|
isInsideEdgelessEditor,
|
||||||
|
type NoteBlockModel,
|
||||||
|
NoteDisplayMode,
|
||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
|
import type { BlockModel } from '@blocksuite/affine/store';
|
||||||
|
|
||||||
import type { ChatContextValue } from '../chat-panel/chat-context';
|
import type { ChatContextValue } from '../chat-panel/chat-context';
|
||||||
import {
|
import {
|
||||||
|
allToCanvas,
|
||||||
getSelectedImagesAsBlobs,
|
getSelectedImagesAsBlobs,
|
||||||
getSelectedTextContent,
|
getSelectedTextContent,
|
||||||
|
getTextContentFromBlockModels,
|
||||||
selectedToCanvas,
|
selectedToCanvas,
|
||||||
} from './selection-utils';
|
} from './selection-utils';
|
||||||
|
|
||||||
export async function extractContext(
|
export async function extractSelectedContent(
|
||||||
host: EditorHost
|
host: EditorHost
|
||||||
): Promise<Partial<ChatContextValue> | null> {
|
): Promise<Partial<ChatContextValue> | null> {
|
||||||
const docModeService = host.std.get(DocModeProvider);
|
const docModeService = host.std.get(DocModeProvider);
|
||||||
const mode = docModeService.getEditorMode() || 'page';
|
const mode = docModeService.getEditorMode() || 'page';
|
||||||
if (mode === 'edgeless') {
|
if (mode === 'edgeless') {
|
||||||
return await extractOnEdgeless(host);
|
return await extractEdgelessSelected(host);
|
||||||
} else {
|
} else {
|
||||||
return await extractOnPage(host);
|
return await extractPageSelected(host);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function extractOnEdgeless(
|
async function extractEdgelessSelected(
|
||||||
host: EditorHost
|
host: EditorHost
|
||||||
): Promise<Partial<ChatContextValue> | null> {
|
): Promise<Partial<ChatContextValue> | null> {
|
||||||
if (!isInsideEdgelessEditor(host)) return null;
|
if (!isInsideEdgelessEditor(host)) return null;
|
||||||
@@ -41,7 +47,7 @@ export async function extractOnEdgeless(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function extractOnPage(
|
async function extractPageSelected(
|
||||||
host: EditorHost
|
host: EditorHost
|
||||||
): Promise<Partial<ChatContextValue> | null> {
|
): Promise<Partial<ChatContextValue> | null> {
|
||||||
const text = await getSelectedTextContent(host, 'plain-text');
|
const text = await getSelectedTextContent(host, 'plain-text');
|
||||||
@@ -76,3 +82,77 @@ export async function extractOnPage(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function extractAllContent(
|
||||||
|
host: EditorHost
|
||||||
|
): Promise<Partial<ChatContextValue> | null> {
|
||||||
|
const docModeService = host.std.get(DocModeProvider);
|
||||||
|
const mode = docModeService.getEditorMode() || 'page';
|
||||||
|
if (mode === 'edgeless') {
|
||||||
|
return await extractEdgelessAll(host);
|
||||||
|
} else {
|
||||||
|
return await extractPageAll(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractEdgelessAll(
|
||||||
|
host: EditorHost
|
||||||
|
): Promise<Partial<ChatContextValue> | null> {
|
||||||
|
if (!isInsideEdgelessEditor(host)) return null;
|
||||||
|
|
||||||
|
const canvas = await allToCanvas(host);
|
||||||
|
if (!canvas) return null;
|
||||||
|
|
||||||
|
const blob: Blob | null = await new Promise(resolve =>
|
||||||
|
canvas.toBlob(resolve)
|
||||||
|
);
|
||||||
|
if (!blob) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
images: [new File([blob], `${host.doc.id}.png`)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractPageAll(
|
||||||
|
host: EditorHost
|
||||||
|
): Promise<Partial<ChatContextValue> | null> {
|
||||||
|
const notes = host.doc
|
||||||
|
.getBlocksByFlavour('affine:note')
|
||||||
|
.filter(
|
||||||
|
note =>
|
||||||
|
(note.model as NoteBlockModel).displayMode !==
|
||||||
|
NoteDisplayMode.EdgelessOnly
|
||||||
|
)
|
||||||
|
.map(note => note.model as NoteBlockModel);
|
||||||
|
const blockModels = notes.reduce((acc, note) => {
|
||||||
|
acc.push(...note.children);
|
||||||
|
return acc;
|
||||||
|
}, [] as BlockModel[]);
|
||||||
|
const text = await getTextContentFromBlockModels(
|
||||||
|
host,
|
||||||
|
blockModels,
|
||||||
|
'plain-text'
|
||||||
|
);
|
||||||
|
const markdown = await getTextContentFromBlockModels(
|
||||||
|
host,
|
||||||
|
blockModels,
|
||||||
|
'markdown'
|
||||||
|
);
|
||||||
|
const blobs = await Promise.all(
|
||||||
|
blockModels.map(async s => {
|
||||||
|
if (s.flavour !== 'affine:image') return null;
|
||||||
|
const sourceId = (s as ImageBlockModel)?.sourceId;
|
||||||
|
if (!sourceId) return null;
|
||||||
|
const blob = await (sourceId ? host.doc.blobSync.get(sourceId) : null);
|
||||||
|
if (!blob) return null;
|
||||||
|
return new File([blob], sourceId);
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
const images = blobs.filter((blob): blob is File => !!blob);
|
||||||
|
|
||||||
|
return {
|
||||||
|
quote: text,
|
||||||
|
markdown,
|
||||||
|
images,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Slice,
|
Slice,
|
||||||
toDraftModel,
|
toDraftModel,
|
||||||
} from '@blocksuite/affine/store';
|
} from '@blocksuite/affine/store';
|
||||||
|
import type { GfxModel } from '@blocksuite/block-std/gfx';
|
||||||
|
|
||||||
import { getContentFromSlice } from '../../_common';
|
import { getContentFromSlice } from '../../_common';
|
||||||
import { getEdgelessCopilotWidget, getService } from './edgeless';
|
import { getEdgelessCopilotWidget, getService } from './edgeless';
|
||||||
@@ -39,11 +40,22 @@ export function getEdgelessService(editor: EditorHost) {
|
|||||||
throw new Error('Please open switch to edgeless mode');
|
throw new Error('Please open switch to edgeless mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function selectedToCanvas(editor: EditorHost) {
|
export async function selectedToCanvas(host: EditorHost) {
|
||||||
const edgelessRoot = getEdgelessRootFromEditor(editor);
|
const edgelessRoot = getEdgelessRootFromEditor(host);
|
||||||
const { notes, frames, shapes, images } = BlocksUtils.splitElements(
|
return elementsToCanvas(
|
||||||
|
host,
|
||||||
edgelessRoot.service.selection.selectedElements
|
edgelessRoot.service.selection.selectedElements
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function allToCanvas(host: EditorHost) {
|
||||||
|
const edgelessRoot = getEdgelessRootFromEditor(host);
|
||||||
|
return elementsToCanvas(host, edgelessRoot.gfx.gfxElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function elementsToCanvas(host: EditorHost, elements: GfxModel[]) {
|
||||||
|
const edgelessRoot = getEdgelessRootFromEditor(host);
|
||||||
|
const { notes, frames, shapes, images } = BlocksUtils.splitElements(elements);
|
||||||
if (notes.length + frames.length + images.length + shapes.length === 0) {
|
if (notes.length + frames.length + images.length + shapes.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { ActionMindmap } from './ai/chat-panel/actions/mindmap';
|
|||||||
import { ActionSlides } from './ai/chat-panel/actions/slides';
|
import { ActionSlides } from './ai/chat-panel/actions/slides';
|
||||||
import { ActionText } from './ai/chat-panel/actions/text';
|
import { ActionText } from './ai/chat-panel/actions/text';
|
||||||
import { AILoading } from './ai/chat-panel/ai-loading';
|
import { AILoading } from './ai/chat-panel/ai-loading';
|
||||||
import { ChatCards } from './ai/chat-panel/chat-cards';
|
|
||||||
import { ChatPanelInput } from './ai/chat-panel/chat-panel-input';
|
import { ChatPanelInput } from './ai/chat-panel/chat-panel-input';
|
||||||
import { ChatPanelMessages } from './ai/chat-panel/chat-panel-messages';
|
import { ChatPanelMessages } from './ai/chat-panel/chat-panel-messages';
|
||||||
import { AIErrorWrapper } from './ai/messages/error';
|
import { AIErrorWrapper } from './ai/messages/error';
|
||||||
@@ -55,7 +54,6 @@ export function registerBlocksuitePresetsCustomComponents() {
|
|||||||
customElements.define('action-slides', ActionSlides);
|
customElements.define('action-slides', ActionSlides);
|
||||||
customElements.define('action-text', ActionText);
|
customElements.define('action-text', ActionText);
|
||||||
customElements.define('ai-loading', AILoading);
|
customElements.define('ai-loading', AILoading);
|
||||||
customElements.define('chat-cards', ChatCards);
|
|
||||||
customElements.define('chat-panel-input', ChatPanelInput);
|
customElements.define('chat-panel-input', ChatPanelInput);
|
||||||
customElements.define('chat-panel-messages', ChatPanelMessages);
|
customElements.define('chat-panel-messages', ChatPanelMessages);
|
||||||
customElements.define('chat-panel', ChatPanel);
|
customElements.define('chat-panel', ChatPanel);
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ import { ViewService } from '@affine/core/modules/workbench';
|
|||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { isNewTabTrigger } from '@affine/core/utils';
|
import { isNewTabTrigger } from '@affine/core/utils';
|
||||||
import { RefNodeSlotsProvider } from '@blocksuite/affine/blocks';
|
import { RefNodeSlotsProvider } from '@blocksuite/affine/blocks';
|
||||||
import { DisposableGroup } from '@blocksuite/affine/global/utils';
|
import {
|
||||||
|
type Disposable,
|
||||||
|
DisposableGroup,
|
||||||
|
} from '@blocksuite/affine/global/utils';
|
||||||
import { type AffineEditorContainer } from '@blocksuite/affine/presets';
|
import { type AffineEditorContainer } from '@blocksuite/affine/presets';
|
||||||
import {
|
import {
|
||||||
AiIcon,
|
AiIcon,
|
||||||
@@ -111,16 +114,14 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
}, [editorContainer, isActiveView, setActiveBlockSuiteEditor]);
|
}, [editorContainer, isActiveView, setActiveBlockSuiteEditor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const disposable = AIProvider.slots.requestOpenWithChat.on(params => {
|
const disposables: Disposable[] = [];
|
||||||
|
const openHandler = () => {
|
||||||
workbench.openSidebar();
|
workbench.openSidebar();
|
||||||
view.activeSidebarTab('chat');
|
view.activeSidebarTab('chat');
|
||||||
|
};
|
||||||
if (chatPanelRef.current) {
|
disposables.push(AIProvider.slots.requestOpenWithChat.on(openHandler));
|
||||||
const chatCards = chatPanelRef.current.querySelector('chat-cards');
|
disposables.push(AIProvider.slots.requestSendWithChat.on(openHandler));
|
||||||
if (chatCards) chatCards.temporaryParams = params;
|
return () => disposables.forEach(d => d.dispose());
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => disposable.dispose();
|
|
||||||
}, [activeSidebarTab, view, workbench]);
|
}, [activeSidebarTab, view, workbench]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import {
|
|||||||
type EdgelessRootService,
|
type EdgelessRootService,
|
||||||
RefNodeSlotsProvider,
|
RefNodeSlotsProvider,
|
||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
import { Bound, DisposableGroup } from '@blocksuite/affine/global/utils';
|
import {
|
||||||
|
Bound,
|
||||||
|
type Disposable,
|
||||||
|
DisposableGroup,
|
||||||
|
} from '@blocksuite/affine/global/utils';
|
||||||
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
|
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
|
||||||
import {
|
import {
|
||||||
FrameworkScope,
|
FrameworkScope,
|
||||||
@@ -123,17 +127,17 @@ function DocPeekPreviewEditor({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const disposable = AIProvider.slots.requestOpenWithChat.on(() => {
|
const disposables: Disposable[] = [];
|
||||||
|
const openHandler = () => {
|
||||||
if (doc) {
|
if (doc) {
|
||||||
workbench.openDoc(doc.id);
|
workbench.openDoc(doc.id);
|
||||||
peekView.close();
|
peekView.close();
|
||||||
// chat panel open is already handled in <DetailPageImpl />
|
// chat panel open is already handled in <DetailPageImpl />
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
disposable.dispose();
|
|
||||||
};
|
};
|
||||||
|
disposables.push(AIProvider.slots.requestOpenWithChat.on(openHandler));
|
||||||
|
disposables.push(AIProvider.slots.requestSendWithChat.on(openHandler));
|
||||||
|
return () => disposables.forEach(d => d.dispose());
|
||||||
}, [doc, peekView, workbench, workspace.id]);
|
}, [doc, peekView, workbench, workspace.id]);
|
||||||
|
|
||||||
const openOutlinePanel = useCallback(() => {
|
const openOutlinePanel = useCallback(() => {
|
||||||
|
|||||||
@@ -599,20 +599,6 @@ test.describe('chat with block', () => {
|
|||||||
.waitForSelector('chat-panel-input .chat-panel-images')
|
.waitForSelector('chat-panel-input .chat-panel-images')
|
||||||
.then(el => el.waitForElementState('visible'));
|
.then(el => el.waitForElementState('visible'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('open ai chat', async ({ page }) => {
|
|
||||||
await page
|
|
||||||
.waitForSelector('.ai-item-open-ai-chat')
|
|
||||||
.then(i => i.click());
|
|
||||||
const cards = await page.waitForSelector('chat-panel chat-cards');
|
|
||||||
await cards.waitForElementState('visible');
|
|
||||||
const cardTitles = await Promise.all(
|
|
||||||
await cards
|
|
||||||
.$$('.card-wrapper .card-title')
|
|
||||||
.then(els => els.map(async el => await el.innerText()))
|
|
||||||
);
|
|
||||||
expect(cardTitles).toContain('Start with this Image');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(@darkskygit): block by BS-1709, enable this after bug fix
|
// TODO(@darkskygit): block by BS-1709, enable this after bug fix
|
||||||
|
|||||||
Reference in New Issue
Block a user