refactor(core): reorganize chat-panel components (#10935)
### TL;DR Split complex chat message component into smaller, reusable units with finer granularity > CLOSE AF-2323 AF-2326 ### What Changed * Split messages into components: * `<chat-message-assistant />` * `<chat-message-user />` * `<chat-message-action />` * Split message content into types: * `<chat-content-images />` * `<chat-content-rich-text />` * `<chat-content-pure-text />`
This commit is contained in:
@@ -24,7 +24,6 @@ import { property, state } from 'lit/decorators.js';
|
|||||||
|
|
||||||
import { createTextRenderer } from '../../components/text-renderer';
|
import { createTextRenderer } from '../../components/text-renderer';
|
||||||
import type { ChatAction } from '../chat-context';
|
import type { ChatAction } from '../chat-context';
|
||||||
import { renderImages } from '../components/images';
|
|
||||||
import { HISTORY_IMAGE_ACTIONS } from '../const';
|
import { HISTORY_IMAGE_ACTIONS } from '../const';
|
||||||
|
|
||||||
const icons: Record<string, TemplateResult<1>> = {
|
const icons: Record<string, TemplateResult<1>> = {
|
||||||
@@ -141,7 +140,10 @@ export class ActionWrapper extends WithDisposable(LitElement) {
|
|||||||
<div class="answer-prompt">
|
<div class="answer-prompt">
|
||||||
<div class="subtitle">Answer</div>
|
<div class="subtitle">Answer</div>
|
||||||
${HISTORY_IMAGE_ACTIONS.includes(item.action)
|
${HISTORY_IMAGE_ACTIONS.includes(item.action)
|
||||||
? images && renderImages(images)
|
? images &&
|
||||||
|
html`<chat-content-images
|
||||||
|
.images=${images}
|
||||||
|
></chat-content-images>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${answer
|
${answer
|
||||||
? createTextRenderer(this.host, { customHeading: true })(answer)
|
? createTextRenderer(this.host, { customHeading: true })(answer)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import './action-wrapper';
|
import './action-wrapper';
|
||||||
|
import '../content/images';
|
||||||
|
|
||||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
@@ -8,7 +9,6 @@ import { property } from 'lit/decorators.js';
|
|||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import type { ChatAction } from '../chat-context';
|
import type { ChatAction } from '../chat-context';
|
||||||
import { renderImages } from '../components/images';
|
|
||||||
|
|
||||||
export class ActionImageToText extends WithDisposable(ShadowlessElement) {
|
export class ActionImageToText extends WithDisposable(ShadowlessElement) {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -21,8 +21,14 @@ export class ActionImageToText extends WithDisposable(ShadowlessElement) {
|
|||||||
const answer = this.item.messages[1].attachments;
|
const answer = this.item.messages[1].attachments;
|
||||||
|
|
||||||
return html`<action-wrapper .host=${this.host} .item=${this.item}>
|
return html`<action-wrapper .host=${this.host} .item=${this.item}>
|
||||||
<div style=${styleMap({ marginBottom: '12px' })}>
|
<div
|
||||||
${answer ? renderImages(answer) : nothing}
|
style=${styleMap({
|
||||||
|
marginBottom: '12px',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${answer
|
||||||
|
? html`<chat-content-images .images=${answer}></chat-content-images>`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
</action-wrapper>`;
|
</action-wrapper>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import './action-wrapper';
|
import './action-wrapper';
|
||||||
|
import '../content/images';
|
||||||
|
|
||||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
@@ -8,7 +9,6 @@ import { property } from 'lit/decorators.js';
|
|||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import type { ChatAction } from '../chat-context';
|
import type { ChatAction } from '../chat-context';
|
||||||
import { renderImages } from '../components/images';
|
|
||||||
|
|
||||||
export class ActionImage extends WithDisposable(ShadowlessElement) {
|
export class ActionImage extends WithDisposable(ShadowlessElement) {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -18,11 +18,13 @@ export class ActionImage extends WithDisposable(ShadowlessElement) {
|
|||||||
accessor host!: EditorHost;
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
const answer = this.item.messages[0].attachments;
|
const images = this.item.messages[0].attachments;
|
||||||
|
|
||||||
return html`<action-wrapper .host=${this.host} .item=${this.item}>
|
return html`<action-wrapper .host=${this.host} .item=${this.item}>
|
||||||
<div style=${styleMap({ marginBottom: '12px' })}>
|
<div style=${styleMap({ marginBottom: '12px' })}>
|
||||||
${answer ? renderImages(answer) : nothing}
|
${images
|
||||||
|
? html`<chat-content-images .images=${images}></chat-content-images>`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
</action-wrapper>`;
|
</action-wrapper>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { AIProvider } from '../provider';
|
|||||||
import {
|
import {
|
||||||
type ChatContextValue,
|
type ChatContextValue,
|
||||||
type ChatMessage,
|
type ChatMessage,
|
||||||
|
isChatAction,
|
||||||
isChatMessage,
|
isChatMessage,
|
||||||
} from './chat-context';
|
} from './chat-context';
|
||||||
import { HISTORY_IMAGE_ACTIONS } from './const';
|
import { HISTORY_IMAGE_ACTIONS } from './const';
|
||||||
@@ -245,20 +246,28 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
(_, index) => index,
|
(_, index) => index,
|
||||||
(item, index) => {
|
(item, index) => {
|
||||||
const isLast = index === filteredItems.length - 1;
|
const isLast = index === filteredItems.length - 1;
|
||||||
return isChatMessage(item) && item.role === 'user'
|
if (isChatMessage(item) && item.role === 'user') {
|
||||||
? html`<chat-panel-user-message
|
return html`<chat-message-user
|
||||||
.item=${item}
|
.item=${item}
|
||||||
></chat-panel-user-message>`
|
></chat-message-user>`;
|
||||||
: html`<chat-panel-assistant-message
|
} else if (isChatMessage(item) && item.role === 'assistant') {
|
||||||
|
return html`<chat-message-assistant
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.item=${item}
|
.item=${item}
|
||||||
.isLast=${isLast}
|
.isLast=${isLast}
|
||||||
.status=${status}
|
.status=${isLast ? status : 'idle'}
|
||||||
.error=${error}
|
.error=${isLast ? error : null}
|
||||||
.previewSpecBuilder=${this.previewSpecBuilder}
|
.previewSpecBuilder=${this.previewSpecBuilder}
|
||||||
.getSessionId=${this.getSessionId}
|
.getSessionId=${this.getSessionId}
|
||||||
.retry=${() => this.retry()}
|
.retry=${() => this.retry()}
|
||||||
></chat-panel-assistant-message>`;
|
></chat-message-assistant>`;
|
||||||
|
} else if (isChatAction(item)) {
|
||||||
|
return html`<chat-message-action
|
||||||
|
.host=${this.host}
|
||||||
|
.item=${item}
|
||||||
|
></chat-message-action>`;
|
||||||
|
}
|
||||||
|
return nothing;
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
|
||||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
|
||||||
import { css, html, nothing } from 'lit';
|
|
||||||
import { property } from 'lit/decorators.js';
|
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
|
||||||
|
|
||||||
import { type ChatMessage } from './chat-context';
|
|
||||||
|
|
||||||
export class ChatPanelUserMessage extends WithDisposable(ShadowlessElement) {
|
|
||||||
static override styles = css`
|
|
||||||
.chat-user-message {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
|
|
||||||
.images {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 4px;
|
|
||||||
scrollbar-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.images::-webkit-scrollbar {
|
|
||||||
height: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.images::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--affine-border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.images::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 180px;
|
|
||||||
max-height: 264px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-content {
|
|
||||||
display: inline-block;
|
|
||||||
text-align: left;
|
|
||||||
max-width: 800px;
|
|
||||||
max-height: 500px;
|
|
||||||
overflow-y: auto;
|
|
||||||
background: var(--affine-v2-aI-userTextBackground);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor item!: ChatMessage;
|
|
||||||
|
|
||||||
renderImages(images: string[]) {
|
|
||||||
return images.length > 0
|
|
||||||
? html`<div class="images">
|
|
||||||
${repeat(
|
|
||||||
images,
|
|
||||||
image => image,
|
|
||||||
image => {
|
|
||||||
return html`<img src="${image}" />`;
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>`
|
|
||||||
: nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderText(text: string) {
|
|
||||||
return text.length > 0
|
|
||||||
? html`<div class="text-content">${text}</div>`
|
|
||||||
: nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderContent() {
|
|
||||||
const { item } = this;
|
|
||||||
|
|
||||||
const imagesRendered = item.attachments
|
|
||||||
? this.renderImages(item.attachments)
|
|
||||||
: nothing;
|
|
||||||
return html` ${imagesRendered} ${this.renderText(item.content)} `;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override render() {
|
|
||||||
return html` <div class="chat-user-message">${this.renderContent()}</div> `;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'chat-panel-user-message': ChatPanelUserMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { html } from 'lit';
|
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
|
||||||
|
|
||||||
export const renderImages = (images: string[]) => {
|
|
||||||
return html`<style>
|
|
||||||
.images-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.image-container {
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 70%;
|
|
||||||
max-width: 320px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="images-container">
|
|
||||||
${repeat(
|
|
||||||
images,
|
|
||||||
image => image,
|
|
||||||
image => {
|
|
||||||
return html`<div class="image-container">
|
|
||||||
<img src="${image}" />
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
|
import { AiIcon } from '@blocksuite/icons/lit';
|
||||||
|
import { css, html } from 'lit';
|
||||||
|
|
||||||
|
const AffineAvatarIcon = AiIcon({
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
style: 'color: var(--affine-primary-color)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export class AssistantAvatar extends ShadowlessElement {
|
||||||
|
static override styles = css`
|
||||||
|
.assistant-avatar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
protected override render() {
|
||||||
|
return html`<span class="assistant-avatar"
|
||||||
|
>${AffineAvatarIcon} AFFiNE AI</span
|
||||||
|
>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'chat-assistant-avatar': AssistantAvatar;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
|
import { css, html, nothing } from 'lit';
|
||||||
|
import { property } from 'lit/decorators.js';
|
||||||
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
|
|
||||||
|
export class ChatContentImages extends WithDisposable(ShadowlessElement) {
|
||||||
|
static override styles = css`
|
||||||
|
.chat-content-images-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 4px;
|
||||||
|
scrollbar-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content-images-row::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content-images-row::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--affine-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content-images-row::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content-images-row img {
|
||||||
|
max-width: 180px;
|
||||||
|
max-height: 264px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content-images-column {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content-images-column .image-container {
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 70%;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content-images-column .image-container img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor images: string[] = [];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor layout: 'row' | 'column' = 'row';
|
||||||
|
|
||||||
|
protected override render() {
|
||||||
|
if (this.images.length === 0) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.layout === 'row') {
|
||||||
|
return html`<div class="chat-content-images-row">
|
||||||
|
${repeat(
|
||||||
|
this.images,
|
||||||
|
image => image,
|
||||||
|
image => html`<img src="${image}" />`
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
return html`<div class="chat-content-images-column">
|
||||||
|
${repeat(
|
||||||
|
this.images,
|
||||||
|
image => image,
|
||||||
|
image =>
|
||||||
|
html`<div class="image-container">
|
||||||
|
<img src="${image}" />
|
||||||
|
</div>`
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'chat-content-images': ChatContentImages;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
|
import { css, html, nothing } from 'lit';
|
||||||
|
import { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
export class ChatContentPureText extends ShadowlessElement {
|
||||||
|
static override styles = css`
|
||||||
|
.chat-content-pure-text {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
max-width: 800px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--affine-v2-aI-userTextBackground);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor text: string = '';
|
||||||
|
|
||||||
|
protected override render() {
|
||||||
|
return this.text.length > 0
|
||||||
|
? html`<div class="chat-content-pure-text">${this.text}</div>`
|
||||||
|
: nothing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'chat-content-pure-text': ChatContentPureText;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,16 @@
|
|||||||
import './action-wrapper';
|
|
||||||
|
|
||||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import type { SpecBuilder } from '@blocksuite/affine/shared/utils';
|
import type { SpecBuilder } from '@blocksuite/affine/shared/utils';
|
||||||
import { html, nothing } from 'lit';
|
import { html } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
import { createTextRenderer } from '../../components/text-renderer';
|
import { createTextRenderer } from '../../components/text-renderer';
|
||||||
import { renderImages } from '../components/images';
|
|
||||||
|
|
||||||
export class ChatText extends WithDisposable(ShadowlessElement) {
|
export class ChatContentRichText extends WithDisposable(ShadowlessElement) {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host!: EditorHost;
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor attachments: string[] | undefined = undefined;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor text!: string;
|
accessor text!: string;
|
||||||
|
|
||||||
@@ -27,10 +21,8 @@ export class ChatText extends WithDisposable(ShadowlessElement) {
|
|||||||
accessor previewSpecBuilder!: SpecBuilder;
|
accessor previewSpecBuilder!: SpecBuilder;
|
||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
const { attachments, text, host } = this;
|
const { text, host } = this;
|
||||||
return html`${attachments && attachments.length > 0
|
return html`${createTextRenderer(host, {
|
||||||
? renderImages(attachments)
|
|
||||||
: nothing}${createTextRenderer(host, {
|
|
||||||
customHeading: true,
|
customHeading: true,
|
||||||
extensions: this.previewSpecBuilder.value,
|
extensions: this.previewSpecBuilder.value,
|
||||||
})(text, this.state)}`;
|
})(text, this.state)}`;
|
||||||
@@ -39,6 +31,6 @@ export class ChatText extends WithDisposable(ShadowlessElement) {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'chat-text': ChatText;
|
'chat-content-rich-text': ChatContentRichText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import '../content/assistant-avatar';
|
||||||
|
|
||||||
|
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||||
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
|
import { html } from 'lit';
|
||||||
|
import { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
import { type ChatAction } from '../chat-context';
|
||||||
|
import { HISTORY_IMAGE_ACTIONS } from '../const';
|
||||||
|
|
||||||
|
export class ChatMessageAction extends WithDisposable(ShadowlessElement) {
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor item!: ChatAction;
|
||||||
|
|
||||||
|
renderHeader() {
|
||||||
|
return html`
|
||||||
|
<div class="user-info">
|
||||||
|
<chat-assistant-avatar></chat-assistant-avatar>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
const { host, item } = this;
|
||||||
|
|
||||||
|
switch (item.action) {
|
||||||
|
case 'Create a presentation':
|
||||||
|
return html`<action-slides
|
||||||
|
.host=${host}
|
||||||
|
.item=${item}
|
||||||
|
></action-slides>`;
|
||||||
|
case 'Make it real':
|
||||||
|
return html`<action-make-real
|
||||||
|
.host=${host}
|
||||||
|
.item=${item}
|
||||||
|
></action-make-real>`;
|
||||||
|
case 'Brainstorm mindmap':
|
||||||
|
return html`<action-mindmap
|
||||||
|
.host=${host}
|
||||||
|
.item=${item}
|
||||||
|
></action-mindmap>`;
|
||||||
|
case 'Explain this image':
|
||||||
|
case 'Generate a caption':
|
||||||
|
return html`<action-image-to-text
|
||||||
|
.host=${host}
|
||||||
|
.item=${item}
|
||||||
|
></action-image-to-text>`;
|
||||||
|
default:
|
||||||
|
if (HISTORY_IMAGE_ACTIONS.includes(item.action)) {
|
||||||
|
return html`<action-image
|
||||||
|
.host=${host}
|
||||||
|
.item=${item}
|
||||||
|
></action-image>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<action-text
|
||||||
|
.item=${item}
|
||||||
|
.host=${host}
|
||||||
|
.isCode=${item.action === 'Explain this code' ||
|
||||||
|
item.action === 'Check code error'}
|
||||||
|
></action-text>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override render() {
|
||||||
|
return html` ${this.renderHeader()} ${this.renderContent()} `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'chat-message-action': ChatMessageAction;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,22 @@
|
|||||||
|
import '../content/assistant-avatar';
|
||||||
|
import '../content/rich-text';
|
||||||
|
|
||||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
|
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
|
||||||
import { AiIcon } from '@blocksuite/icons/lit';
|
|
||||||
import { css, html, nothing } from 'lit';
|
import { css, html, nothing } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EdgelessEditorActions,
|
EdgelessEditorActions,
|
||||||
PageEditorActions,
|
PageEditorActions,
|
||||||
} from '../_common/chat-actions-handle';
|
} from '../../_common/chat-actions-handle';
|
||||||
import { type AIError } from '../components/ai-item/types';
|
import { type AIError } from '../../components/ai-item/types';
|
||||||
import { AIChatErrorRenderer } from '../messages/error';
|
import { AIChatErrorRenderer } from '../../messages/error';
|
||||||
import { isChatMessage } from './chat-context';
|
import { type ChatMessage, isChatMessage } from '../chat-context';
|
||||||
import { type ChatItem } from './chat-context';
|
|
||||||
import { HISTORY_IMAGE_ACTIONS } from './const';
|
|
||||||
|
|
||||||
const AffineAvatarIcon = AiIcon({
|
export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||||
width: '20px',
|
|
||||||
height: '20px',
|
|
||||||
style: 'color: var(--affine-primary-color)',
|
|
||||||
});
|
|
||||||
|
|
||||||
export class ChatPanelAssistantMessage extends WithDisposable(
|
|
||||||
ShadowlessElement
|
|
||||||
) {
|
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
.message-info {
|
.message-info {
|
||||||
color: var(--affine-placeholder-color);
|
color: var(--affine-placeholder-color);
|
||||||
@@ -37,7 +29,7 @@ export class ChatPanelAssistantMessage extends WithDisposable(
|
|||||||
accessor host!: EditorHost;
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor item!: ChatItem;
|
accessor item!: ChatMessage;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor isLast: boolean = false;
|
accessor isLast: boolean = false;
|
||||||
@@ -57,18 +49,15 @@ export class ChatPanelAssistantMessage extends WithDisposable(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor retry!: () => void;
|
accessor retry!: () => void;
|
||||||
|
|
||||||
renderAvatar() {
|
renderHeader() {
|
||||||
const isAssistant =
|
|
||||||
isChatMessage(this.item) && this.item.role === 'assistant';
|
|
||||||
const isWithDocs =
|
const isWithDocs =
|
||||||
isAssistant &&
|
|
||||||
'content' in this.item &&
|
'content' in this.item &&
|
||||||
this.item.content &&
|
this.item.content &&
|
||||||
this.item.content.includes('[^') &&
|
this.item.content.includes('[^') &&
|
||||||
/\[\^\d+\]:{"type":"doc","docId":"[^"]+"}/.test(this.item.content);
|
/\[\^\d+\]:{"type":"doc","docId":"[^"]+"}/.test(this.item.content);
|
||||||
|
|
||||||
return html`<div class="user-info">
|
return html`<div class="user-info">
|
||||||
${AffineAvatarIcon} AFFiNE AI
|
<chat-assistant-avatar></chat-assistant-avatar>
|
||||||
${isWithDocs
|
${isWithDocs
|
||||||
? html`<span class="message-info">with your docs</span>`
|
? html`<span class="message-info">with your docs</span>`
|
||||||
: nothing}
|
: nothing}
|
||||||
@@ -82,61 +71,28 @@ export class ChatPanelAssistantMessage extends WithDisposable(
|
|||||||
return html`<ai-loading></ai-loading>`;
|
return html`<ai-loading></ai-loading>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChatMessage(item)) {
|
|
||||||
const state = isLast
|
const state = isLast
|
||||||
? status !== 'loading' && status !== 'transmitting'
|
? status !== 'loading' && status !== 'transmitting'
|
||||||
? 'finished'
|
? 'finished'
|
||||||
: 'generating'
|
: 'generating'
|
||||||
: 'finished';
|
: 'finished';
|
||||||
const shouldRenderError = isLast && status === 'error' && !!error;
|
const shouldRenderError = isLast && status === 'error' && !!error;
|
||||||
return html`<chat-text
|
|
||||||
|
return html`
|
||||||
|
${item.attachments
|
||||||
|
? html`<chat-content-images
|
||||||
|
.images=${item.attachments}
|
||||||
|
></chat-content-images>`
|
||||||
|
: nothing}
|
||||||
|
<chat-content-rich-text
|
||||||
.host=${host}
|
.host=${host}
|
||||||
.attachments=${item.attachments}
|
|
||||||
.text=${item.content}
|
.text=${item.content}
|
||||||
.state=${state}
|
.state=${state}
|
||||||
.previewSpecBuilder=${this.previewSpecBuilder}
|
.previewSpecBuilder=${this.previewSpecBuilder}
|
||||||
></chat-text>
|
></chat-content-rich-text>
|
||||||
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
|
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
|
||||||
${this.renderEditorActions()}`;
|
${this.renderEditorActions()}
|
||||||
} else {
|
`;
|
||||||
switch (item.action) {
|
|
||||||
case 'Create a presentation':
|
|
||||||
return html`<action-slides
|
|
||||||
.host=${host}
|
|
||||||
.item=${item}
|
|
||||||
></action-slides>`;
|
|
||||||
case 'Make it real':
|
|
||||||
return html`<action-make-real
|
|
||||||
.host=${host}
|
|
||||||
.item=${item}
|
|
||||||
></action-make-real>`;
|
|
||||||
case 'Brainstorm mindmap':
|
|
||||||
return html`<action-mindmap
|
|
||||||
.host=${host}
|
|
||||||
.item=${item}
|
|
||||||
></action-mindmap>`;
|
|
||||||
case 'Explain this image':
|
|
||||||
case 'Generate a caption':
|
|
||||||
return html`<action-image-to-text
|
|
||||||
.host=${host}
|
|
||||||
.item=${item}
|
|
||||||
></action-image-to-text>`;
|
|
||||||
default:
|
|
||||||
if (HISTORY_IMAGE_ACTIONS.includes(item.action)) {
|
|
||||||
return html`<action-image
|
|
||||||
.host=${host}
|
|
||||||
.item=${item}
|
|
||||||
></action-image>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`<action-text
|
|
||||||
.item=${item}
|
|
||||||
.host=${host}
|
|
||||||
.isCode=${item.action === 'Explain this code' ||
|
|
||||||
item.action === 'Check code error'}
|
|
||||||
></action-text>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEditorActions() {
|
renderEditorActions() {
|
||||||
@@ -185,7 +141,7 @@ export class ChatPanelAssistantMessage extends WithDisposable(
|
|||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
return html`
|
return html`
|
||||||
${this.renderAvatar()}
|
${this.renderHeader()}
|
||||||
<div class="item-wrapper">${this.renderContent()}</div>
|
<div class="item-wrapper">${this.renderContent()}</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -193,6 +149,6 @@ export class ChatPanelAssistantMessage extends WithDisposable(
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'chat-panel-assistant-message': ChatPanelAssistantMessage;
|
'chat-message-assistant': ChatMessageAssistant;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import '../content/images';
|
||||||
|
import '../content/pure-text';
|
||||||
|
|
||||||
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
|
import { css, html, nothing } from 'lit';
|
||||||
|
import { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
import { type ChatMessage } from '../chat-context';
|
||||||
|
|
||||||
|
export class ChatMessageUser extends WithDisposable(ShadowlessElement) {
|
||||||
|
static override styles = css`
|
||||||
|
.chat-message-user {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor item!: ChatMessage;
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
const { item } = this;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${item.attachments
|
||||||
|
? html`<chat-content-images
|
||||||
|
.images=${item.attachments}
|
||||||
|
></chat-content-images>`
|
||||||
|
: nothing}
|
||||||
|
<chat-content-pure-text .text=${item.content}></chat-content-pure-text>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override render() {
|
||||||
|
return html` <div class="chat-message-user">${this.renderContent()}</div> `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'chat-message-user': ChatMessageUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ import { UserInfo } from './blocks/ai-chat-block/components/user-info';
|
|||||||
import { AIChatBlockSchemaExtension } from './blocks/ai-chat-block/model';
|
import { AIChatBlockSchemaExtension } from './blocks/ai-chat-block/model';
|
||||||
import { ChatPanel } from './chat-panel';
|
import { ChatPanel } from './chat-panel';
|
||||||
import { ActionWrapper } from './chat-panel/actions/action-wrapper';
|
import { ActionWrapper } from './chat-panel/actions/action-wrapper';
|
||||||
import { ChatText } from './chat-panel/actions/chat-text';
|
|
||||||
import { ActionImage } from './chat-panel/actions/image';
|
import { ActionImage } from './chat-panel/actions/image';
|
||||||
import { ActionImageToText } from './chat-panel/actions/image-to-text';
|
import { ActionImageToText } from './chat-panel/actions/image-to-text';
|
||||||
import { ActionMakeReal } from './chat-panel/actions/make-real';
|
import { ActionMakeReal } from './chat-panel/actions/make-real';
|
||||||
@@ -23,17 +22,22 @@ import { ActionMindmap } from './chat-panel/actions/mindmap';
|
|||||||
import { ActionSlides } from './chat-panel/actions/slides';
|
import { ActionSlides } from './chat-panel/actions/slides';
|
||||||
import { ActionText } from './chat-panel/actions/text';
|
import { ActionText } from './chat-panel/actions/text';
|
||||||
import { AILoading } from './chat-panel/ai-loading';
|
import { AILoading } from './chat-panel/ai-loading';
|
||||||
import { ChatPanelAssistantMessage } from './chat-panel/chat-panel-assistant-message';
|
|
||||||
import { ChatPanelChips } from './chat-panel/chat-panel-chips';
|
import { ChatPanelChips } from './chat-panel/chat-panel-chips';
|
||||||
import { ChatPanelInput } from './chat-panel/chat-panel-input';
|
import { ChatPanelInput } from './chat-panel/chat-panel-input';
|
||||||
import { ChatPanelMessages } from './chat-panel/chat-panel-messages';
|
import { ChatPanelMessages } from './chat-panel/chat-panel-messages';
|
||||||
import { ChatPanelUserMessage } from './chat-panel/chat-panel-user-message';
|
|
||||||
import { ChatPanelAddPopover } from './chat-panel/components/add-popover';
|
import { ChatPanelAddPopover } from './chat-panel/components/add-popover';
|
||||||
import { ChatPanelChip } from './chat-panel/components/chip';
|
import { ChatPanelChip } from './chat-panel/components/chip';
|
||||||
import { ChatPanelCollectionChip } from './chat-panel/components/collection-chip';
|
import { ChatPanelCollectionChip } from './chat-panel/components/collection-chip';
|
||||||
import { ChatPanelDocChip } from './chat-panel/components/doc-chip';
|
import { ChatPanelDocChip } from './chat-panel/components/doc-chip';
|
||||||
import { ChatPanelFileChip } from './chat-panel/components/file-chip';
|
import { ChatPanelFileChip } from './chat-panel/components/file-chip';
|
||||||
import { ChatPanelTagChip } from './chat-panel/components/tag-chip';
|
import { ChatPanelTagChip } from './chat-panel/components/tag-chip';
|
||||||
|
import { AssistantAvatar } from './chat-panel/content/assistant-avatar';
|
||||||
|
import { ChatContentImages } from './chat-panel/content/images';
|
||||||
|
import { ChatContentPureText } from './chat-panel/content/pure-text';
|
||||||
|
import { ChatContentRichText } from './chat-panel/content/rich-text';
|
||||||
|
import { ChatMessageAction } from './chat-panel/message/action';
|
||||||
|
import { ChatMessageAssistant } from './chat-panel/message/assistant';
|
||||||
|
import { ChatMessageUser } from './chat-panel/message/user';
|
||||||
import { effects as componentAiItemEffects } from './components/ai-item';
|
import { effects as componentAiItemEffects } from './components/ai-item';
|
||||||
import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
|
import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
|
||||||
import { AskAIButton } from './components/ask-ai-button';
|
import { AskAIButton } from './components/ask-ai-button';
|
||||||
@@ -83,7 +87,6 @@ export function registerAIEffects() {
|
|||||||
customElements.define('chat-copy-more', ChatCopyMore);
|
customElements.define('chat-copy-more', ChatCopyMore);
|
||||||
customElements.define('image-preview-grid', ImagePreviewGrid);
|
customElements.define('image-preview-grid', ImagePreviewGrid);
|
||||||
customElements.define('action-wrapper', ActionWrapper);
|
customElements.define('action-wrapper', ActionWrapper);
|
||||||
customElements.define('chat-text', ChatText);
|
|
||||||
customElements.define('action-image-to-text', ActionImageToText);
|
customElements.define('action-image-to-text', ActionImageToText);
|
||||||
customElements.define('action-image', ActionImage);
|
customElements.define('action-image', ActionImage);
|
||||||
customElements.define('action-make-real', ActionMakeReal);
|
customElements.define('action-make-real', ActionMakeReal);
|
||||||
@@ -91,11 +94,6 @@ export function registerAIEffects() {
|
|||||||
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-panel-assistant-message',
|
|
||||||
ChatPanelAssistantMessage
|
|
||||||
);
|
|
||||||
customElements.define('chat-panel-user-message', ChatPanelUserMessage);
|
|
||||||
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);
|
||||||
@@ -136,6 +134,13 @@ export function registerAIEffects() {
|
|||||||
customElements.define('ai-panel-input', AIPanelInput);
|
customElements.define('ai-panel-input', AIPanelInput);
|
||||||
customElements.define('ai-panel-generating', AIPanelGenerating);
|
customElements.define('ai-panel-generating', AIPanelGenerating);
|
||||||
customElements.define('ai-panel-error', AIPanelError);
|
customElements.define('ai-panel-error', AIPanelError);
|
||||||
|
customElements.define('chat-assistant-avatar', AssistantAvatar);
|
||||||
|
customElements.define('chat-content-images', ChatContentImages);
|
||||||
|
customElements.define('chat-content-pure-text', ChatContentPureText);
|
||||||
|
customElements.define('chat-content-rich-text', ChatContentRichText);
|
||||||
|
customElements.define('chat-message-action', ChatMessageAction);
|
||||||
|
customElements.define('chat-message-assistant', ChatMessageAssistant);
|
||||||
|
customElements.define('chat-message-user', ChatMessageUser);
|
||||||
|
|
||||||
customElements.define(AFFINE_AI_PANEL_WIDGET, AffineAIPanelWidget);
|
customElements.define(AFFINE_AI_PANEL_WIDGET, AffineAIPanelWidget);
|
||||||
customElements.define(AFFINE_EDGELESS_COPILOT_WIDGET, EdgelessCopilotWidget);
|
customElements.define(AFFINE_EDGELESS_COPILOT_WIDGET, EdgelessCopilotWidget);
|
||||||
|
|||||||
@@ -107,22 +107,30 @@ const collectHistory = async (page: Page) => {
|
|||||||
const chatPanel = await page.waitForSelector('.chat-panel-messages');
|
const chatPanel = await page.waitForSelector('.chat-panel-messages');
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
Array.from(
|
Array.from(
|
||||||
await chatPanel.$$('chat-panel-user-message,chat-panel-assistant-message')
|
await chatPanel.$$(
|
||||||
|
'chat-message-user,chat-message-assistant,chat-message-action'
|
||||||
|
)
|
||||||
).map(async m => {
|
).map(async m => {
|
||||||
const isAssistant = await m.evaluate(
|
const isAssistant = await m.evaluate(
|
||||||
el => el.tagName === 'CHAT-PANEL-ASSISTANT-MESSAGE'
|
el => el.tagName.toLocaleLowerCase() === 'chat-message-assistant'
|
||||||
);
|
);
|
||||||
return isAssistant
|
const isChatAction = await m.evaluate(
|
||||||
|
el => el.tagName.toLocaleLowerCase() === 'chat-message-action'
|
||||||
|
);
|
||||||
|
|
||||||
|
return isAssistant || isChatAction
|
||||||
? {
|
? {
|
||||||
name: await m.$('.user-info').then(i => i?.innerText()),
|
name: await m.$('.user-info').then(i => i?.innerText()),
|
||||||
content: await m
|
content: await m
|
||||||
.$('chat-text')
|
.$('chat-content-rich-text')
|
||||||
.then(t => t?.$('editor-host'))
|
.then(t => t?.$('editor-host'))
|
||||||
.then(e => e?.innerText()),
|
.then(e => e?.innerText()),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
name: 'You',
|
name: 'You',
|
||||||
content: await m.$('.text-content').then(i => i?.innerText()),
|
content: await m
|
||||||
|
.$('chat-content-pure-text')
|
||||||
|
.then(i => i?.innerText()),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -136,14 +144,14 @@ const collectChat = async (page: Page) => {
|
|||||||
}
|
}
|
||||||
// wait ai response
|
// wait ai response
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(
|
||||||
'.chat-panel-messages chat-panel-assistant-message chat-copy-more',
|
'.chat-panel-messages chat-message-assistant chat-copy-more',
|
||||||
{
|
{
|
||||||
timeout: ONE_MINUTE,
|
timeout: ONE_MINUTE,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
await page
|
await page
|
||||||
.locator('.chat-panel-messages > chat-panel-assistant-message')
|
.locator('.chat-panel-messages > chat-message-assistant')
|
||||||
.last()
|
.last()
|
||||||
.locator('chat-copy-more');
|
.locator('chat-copy-more');
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
|
|||||||
Reference in New Issue
Block a user