Skip to content

Commit

Permalink
fix: add image renderer (#6811)
Browse files Browse the repository at this point in the history
  • Loading branch information
regischen committed Apr 18, 2024
1 parent 434aa99 commit ba8db96
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 43 deletions.
15 changes: 15 additions & 0 deletions packages/presets/src/ai/_common/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,18 @@ export const ArrowUpIcon = html`<svg
fill-opacity="0.6"
/>
</svg> `;

export const ErrorTipIcon = html`<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.99984 3.16699C5.33046 3.16699 3.1665 5.33095 3.1665 8.00033C3.1665 10.6697 5.33046 12.8337 7.99984 12.8337C10.6692 12.8337 12.8332 10.6697 12.8332 8.00033C12.8332 5.33095 10.6692 3.16699 7.99984 3.16699ZM2.1665 8.00033C2.1665 4.77866 4.77818 2.16699 7.99984 2.16699C11.2215 2.16699 13.8332 4.77866 13.8332 8.00033C13.8332 11.222 11.2215 13.8337 7.99984 13.8337C4.77818 13.8337 2.1665 11.222 2.1665 8.00033ZM7.99984 5.12996C8.27598 5.12996 8.49984 5.35381 8.49984 5.62996V8.00033C8.49984 8.27647 8.27598 8.50033 7.99984 8.50033C7.72369 8.50033 7.49984 8.27647 7.49984 8.00033V5.62996C7.49984 5.35381 7.72369 5.12996 7.99984 5.12996ZM7.49984 10.3707C7.49984 10.0946 7.72369 9.8707 7.99984 9.8707H8.00576C8.28191 9.8707 8.50576 10.0946 8.50576 10.3707C8.50576 10.6468 8.28191 10.8707 8.00576 10.8707H7.99984C7.72369 10.8707 7.49984 10.6468 7.49984 10.3707Z"
fill="#EB4335"
/>
</svg>`;
25 changes: 17 additions & 8 deletions packages/presets/src/ai/chat-panel/actions/action-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ export class ActionWrapper extends WithDisposable(ShadowlessElement) {
gap: 18px;
height: 22px;
margin-bottom: 12px;
}
.action-name div:last-child {
margin-left: auto;
cursor: pointer;
div:last-child {
cursor: pointer;
display: flex;
align-items: center;
flex: 1;
div:last-child svg {
margin-left: auto;
}
}
}
.answer-prompt {
Expand Down Expand Up @@ -56,11 +62,14 @@ export class ActionWrapper extends WithDisposable(ShadowlessElement) {
const originalText = item.messages[1].content;
return html`<style></style>
<slot></slot>
<div class="action-name">
<div
class="action-name"
@click=${() => (this.promptShow = !this.promptShow)}
>
${ActionIcon}
<div>${item.action}</div>
<div @click=${() => (this.promptShow = !this.promptShow)}>
${this.promptShow ? ArrowUpIcon : ArrowDownIcon}
<div>
<div>${item.action}</div>
<div>${this.promptShow ? ArrowUpIcon : ArrowDownIcon}</div>
</div>
</div>
${this.promptShow
Expand Down
66 changes: 66 additions & 0 deletions packages/presets/src/ai/chat-panel/actions/chat-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import './action-wrapper.js';

import type { EditorHost } from '@blocksuite/block-std';
import { ShadowlessElement, WithDisposable } from '@blocksuite/block-std';
import { css, html, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';

import { createTextRenderer } from '../../messages/text.js';
@customElement('chat-text')
export class ChatText extends WithDisposable(ShadowlessElement) {
static override styles = css`
.images-container {
display: flex;
gap: 12px;
}
.image-container {
border-radius: 4px;
width: 155px;
height: 129px;
overflow: hidden;
position: relative;
display: flex;
justify-content: center;
align-items: center;
img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
}
}
`;
@property({ attribute: false })
host!: EditorHost;

@property({ attribute: false })
blobs?: Blob[];

@property({ attribute: false })
text!: string;

protected override render() {
const { blobs, text } = this;
return html`${blobs
? html`<div class="images-container">
${repeat(
blobs,
blob => blob,
blob => {
return html`<div class="image-container">
<img src="${URL.createObjectURL(blob)}" />
</div>`;
}
)}
</div>`
: nothing}${createTextRenderer(this.host)(text)}`;
}
}

declare global {
interface HTMLElementTagNameMap {
'chat-text': ChatText;
}
}
97 changes: 67 additions & 30 deletions packages/presets/src/ai/chat-panel/chat-panel-input.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { EditorHost } from '@blocksuite/block-std';
import { WithDisposable } from '@blocksuite/block-std';
import { openFileOrFiles } from '@blocksuite/blocks';
import { type AIError, openFileOrFiles } from '@blocksuite/blocks';
import { css, html, LitElement } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';

import { ChatSendIcon, CloseIcon, ImageIcon } from '../_common/icons.js';
import { AIProvider } from '../provider.js';
import type { ChatItem, ChatStatus } from './index.js';
import type { ChatItem, ChatMessage, ChatStatus } from './index.js';

const MaximumImageCount = 8;

Expand Down Expand Up @@ -61,11 +61,26 @@ export class ChatPanelInput extends WithDisposable(LitElement) {
gap: 6px;
flex-wrap: wrap;
position: relative;
}
.chat-panel-images img {
border-radius: 4px;
border: 1px solid var(--affine-border-color);
cursor: pointer;
.image-container {
width: 58px;
height: 58px;
border-radius: 4px;
border: 1px solid var(--affine-border-color);
cursor: pointer;
overflow: hidden;
position: relative;
display: flex;
justify-content: center;
align-items: center;
img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
}
}
}
.close-wrapper {
Expand Down Expand Up @@ -107,6 +122,12 @@ export class ChatPanelInput extends WithDisposable(LitElement) {
@property({ attribute: false })
items!: ChatItem[];

@property({ attribute: false })
error?: Error;

@property({ attribute: false })
updateError!: (error: AIError) => void;

@property({ attribute: false })
updateStatus!: (status: ChatStatus) => void;

Expand All @@ -129,33 +150,50 @@ export class ChatPanelInput extends WithDisposable(LitElement) {
focused = false;

send = async () => {
if (this.status !== 'idle' && this.status !== 'success') return;

const text = this.textarea.value;
if (!text) {
return;
}
const { images } = this;
const { doc } = this.host;
this.textarea.value = '';
this.isInputEmpty = true;
this.images = [];
this.updateStatus('loading');
this.addToItems([
{ role: 'user', content: text, createdAt: new Date().toISOString() },
{
role: 'user',
content: text,
createdAt: new Date().toISOString(),
blobs: images ? images : undefined,
},
{ role: 'assistant', content: '', createdAt: new Date().toISOString() },
]);
const res = await AIProvider.actions.chat?.({
input: text,
attachments: this.images,
docId: this.host.doc.id,
workspaceId: this.host.doc.collection.id,
});

if (res) {
const items = [...this.items];
items[items.length - 1] = {
role: 'assistant',
content: res,
createdAt: new Date().toISOString(),
};
this.updateStatus('success');
this.updateItems(items);
try {
const stream = AIProvider.actions.chat?.({
input: text,
docId: doc.id,
attachments: images,
workspaceId: doc.collection.id,
stream: true,
});

if (stream) {
for await (const text of stream) {
this.updateStatus('transmitting');
const items = [...this.items];
const last = items[items.length - 1] as ChatMessage;
last.content += text;
this.updateItems(items);
}

this.updateStatus('success');
}
} catch (error) {
this.updateStatus('error');
this.updateError(error as AIError);
}
};

Expand Down Expand Up @@ -188,7 +226,8 @@ export class ChatPanelInput extends WithDisposable(LitElement) {
this.images,
image => image.name,
(image, index) =>
html`<img
html`<div
class="image-container"
@mouseenter=${(evt: MouseEvent) => {
const ele = evt.target as HTMLImageElement;
const rect = ele.getBoundingClientRect();
Expand All @@ -200,11 +239,9 @@ export class ChatPanelInput extends WithDisposable(LitElement) {
this.closeWrapper.style.left = left + 'px';
this.closeWrapper.style.top = top + 'px';
}}
width="58"
height="58"
src="${URL.createObjectURL(image)}"
alt="${image.name}"
/>`
>
<img src="${URL.createObjectURL(image)}" alt="${image.name}" />
</div>`
)}
<div
class="close-wrapper"
Expand Down
30 changes: 26 additions & 4 deletions packages/presets/src/ai/chat-panel/chat-panel-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import './actions/action-wrapper.js';
import './actions/make-real.js';
import './actions/slides.js';
import './actions/mindmap.js';
import './actions/chat-text.js';

import type { BlockSelection, TextSelection } from '@blocksuite/block-std';
import { type EditorHost } from '@blocksuite/block-std';
import { ShadowlessElement, WithDisposable } from '@blocksuite/block-std';
import { type AIError, PaymentRequiredError } from '@blocksuite/blocks';
import { Text } from '@blocksuite/store';
import { css, html, nothing } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
Expand All @@ -24,7 +26,10 @@ import {
NewBlockIcon,
ReplaceIcon,
} from '../_common/icons.js';
import { createTextRenderer } from '../messages/text.js';
import {
GeneralErrorRenderer,
PaymentRequiredErrorRenderer,
} from '../messages/error.js';
import { AIProvider } from '../provider.js';
import { insertBelow, replace } from '../utils/editor-actions.js';
import type { ChatItem, ChatStatus } from './index.js';
Expand Down Expand Up @@ -132,6 +137,9 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
status!: ChatStatus;

@property({ attribute: false })
error?: AIError;

@query('.chat-panel-messages')
messagesContainer!: HTMLDivElement;

Expand All @@ -150,9 +158,21 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
this.avatarUrl = res?.avatarUrl ?? '';
}

renderItem(item: ChatItem) {
renderItem(item: ChatItem, isLast: boolean) {
if (isLast && this.status === 'error') {
if (this.error instanceof PaymentRequiredError) {
return PaymentRequiredErrorRenderer(this.host);
} else {
return GeneralErrorRenderer(this.error?.message);
}
}

if ('role' in item) {
return createTextRenderer(this.host)(item.content);
return html`<chat-text
.host=${this.host}
.blobs=${item.blobs}
.text=${item.content}
></chat-text>`;
} else {
switch (item.action) {
case 'Create a presentation':
Expand Down Expand Up @@ -364,7 +384,9 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
(item, index) => {
return html`<div class="message">
${this.renderAvatar(item)}
<div class="item-wrapper">${this.renderItem(item)}</div>
<div class="item-wrapper">
${this.renderItem(item, index === items.length - 1)}
</div>
<div class="item-wrapper">
${this.status === 'loading' && index === items.length - 1
? this.renderLoading()
Expand Down
Loading

1 comment on commit ba8db96

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Size Report

Bundles

Entry Size Gzip Brotli
examples/basic 1.38 kB (+71 B) 773 B (+28 B) 663 B (+24 B)

Packages

Name Size Gzip Brotli
blocks 2.41 MB (+1.73 kB) 566 kB (+485 B) 411 kB (+307 B)
editor 84 B 89 B 63 B
store 83 B 88 B 63 B
inline 84 B 88 B 63 B

Please sign in to comment.