refactor: move chat block to affine (#8368)

[BS-898](https://linear.app/affine-design/issue/BS-898/move-ai-chat-block-to-affine)

Should be merged after https://github.com/toeverything/blocksuite/pull/8420 merged and bumped.
This commit is contained in:
donteatfriedrice
2024-10-16 12:40:30 +00:00
parent 6f541ecf80
commit 11aa6f63b2
58 changed files with 1213 additions and 360 deletions

View File

@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"devDependencies": {
"@blocksuite/affine": "0.17.18",
"@blocksuite/affine": "0.17.19",
"vitest": "2.1.1"
},
"exports": {

View File

@@ -14,7 +14,7 @@
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
"@affine/templates": "workspace:*",
"@blocksuite/affine": "0.17.18",
"@blocksuite/affine": "0.17.19",
"@datastructures-js/binary-search-tree": "^5.3.2",
"foxact": "^0.2.33",
"fractional-indexing": "^3.2.0",

View File

@@ -0,0 +1,48 @@
import { GfxCompatible } from '@blocksuite/affine/block-std/gfx';
import type { SerializedXYWH } from '@blocksuite/affine/global/utils';
import { BlockModel, defineBlockSchema } from '@blocksuite/affine/store';
type AIChatProps = {
xywh: SerializedXYWH;
index: string;
scale: number;
messages: string; // JSON string of ChatMessage[]
sessionId: string; // forked session id
rootWorkspaceId: string; // workspace id of root chat session
rootDocId: string; // doc id of root chat session
};
export const AIChatBlockSchema = defineBlockSchema({
flavour: 'affine:embed-ai-chat',
props: (): AIChatProps => ({
xywh: '[0,0,0,0]',
index: 'a0',
scale: 1,
messages: '',
sessionId: '',
rootWorkspaceId: '',
rootDocId: '',
}),
metadata: {
version: 1,
role: 'content',
children: [],
},
toModel: () => {
return new AIChatBlockModel();
},
});
export class AIChatBlockModel extends GfxCompatible<AIChatProps>(BlockModel) {}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace BlockSuite {
interface EdgelessBlockModelMap {
'affine:embed-ai-chat': AIChatBlockModel;
}
interface BlockModels {
'affine:embed-ai-chat': AIChatBlockModel;
}
}
}

View File

@@ -0,0 +1,2 @@
export const CHAT_BLOCK_WIDTH = 300;
export const CHAT_BLOCK_HEIGHT = 320;

View File

@@ -0,0 +1,3 @@
export * from './ai-chat-model';
export * from './consts';
export * from './types';

View File

@@ -0,0 +1,25 @@
import { z } from 'zod';
// Define the Zod schema
const ChatMessageSchema = z.object({
id: z.string(),
content: z.string(),
role: z.union([z.literal('user'), z.literal('assistant')]),
createdAt: z.string(),
attachments: z.array(z.string()).optional(),
userId: z.string().optional(),
userName: z.string().optional(),
avatarUrl: z.string().optional(),
});
export const ChatMessagesSchema = z.array(ChatMessageSchema);
// Derive the TypeScript type from the Zod schema
export type ChatMessage = z.infer<typeof ChatMessageSchema>;
export type MessageRole = 'user' | 'assistant';
export type MessageUserInfo = {
userId?: string;
userName?: string;
avatarUrl?: string;
};

View File

@@ -0,0 +1 @@
export * from './ai-chat-block';

View File

@@ -1,3 +1,4 @@
export * from './blocks';
export {
migratePages as forceUpgradePages,
migrateGuidCompatibility,

View File

@@ -30,7 +30,7 @@ export function initDocFromProps(doc: Doc, props?: DocProps) {
'affine:page',
props?.page || { title: new Text('') }
);
doc.addBlock('affine:surface', props?.surface || {}, pageBlockId);
doc.addBlock('affine:surface' as never, props?.surface || {}, pageBlockId);
const noteBlockId = doc.addBlock(
'affine:note',
{

View File

@@ -1,5 +1,9 @@
import { Unreachable } from '@affine/env/constant';
import { type DocMode } from '@blocksuite/affine/blocks';
import {
type AffineTextAttributes,
type DocMode,
} from '@blocksuite/affine/blocks';
import type { DeltaInsert } from '@blocksuite/affine/inline';
import { Service } from '../../../framework';
import { type DocProps, initDocFromProps } from '../../../initialization';
@@ -77,7 +81,7 @@ export class DocsService extends Service {
const { doc, release } = this.open(targetDocId);
doc.setPriorityLoad(10);
await doc.waitForSyncReady();
const text = doc.blockSuiteDoc.Text.fromDelta([
const text = new doc.blockSuiteDoc.Text([
{
insert: ' ',
attributes: {
@@ -87,7 +91,7 @@ export class DocsService extends Service {
},
},
},
]);
] as DeltaInsert<AffineTextAttributes>[]);
const [frame] = doc.blockSuiteDoc.getBlocksByFlavour('affine:note');
frame &&
doc.blockSuiteDoc.addBlock(

View File

@@ -1,7 +1,8 @@
import { AffineSchemas } from '@blocksuite/affine/blocks/schemas';
import { AIChatBlockSchema } from '@blocksuite/affine/presets';
import { Schema } from '@blocksuite/affine/store';
import { AIChatBlockSchema } from '../../blocksuite/blocks/ai-chat-block/ai-chat-model';
let _schema: Schema | null = null;
export function getAFFiNEWorkspaceSchema() {
if (!_schema) {

View File

@@ -28,7 +28,7 @@
"@affine/core": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/native": "workspace:*",
"@blocksuite/affine": "0.17.18",
"@blocksuite/affine": "0.17.19",
"@electron-forge/cli": "^7.3.0",
"@electron-forge/core": "^7.3.0",
"@electron-forge/core-utils": "^7.3.0",

View File

@@ -13,7 +13,7 @@
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/i18n": "workspace:*",
"@blocksuite/affine": "0.17.18",
"@blocksuite/affine": "0.17.19",
"@blocksuite/icons": "^2.1.67",
"@sentry/react": "^8.0.0",
"react": "^18.2.0",

View File

@@ -60,7 +60,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@blocksuite/affine": "0.17.18",
"@blocksuite/affine": "0.17.19",
"@blocksuite/icons": "2.1.68",
"@chromatic-com/storybook": "^2.0.0",
"@storybook/addon-essentials": "^8.2.9",

View File

@@ -16,7 +16,7 @@
"@affine/i18n": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/track": "workspace:*",
"@blocksuite/affine": "0.17.18",
"@blocksuite/affine": "0.17.19",
"@blocksuite/icons": "2.1.68",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
@@ -27,6 +27,7 @@
"@floating-ui/dom": "^1.6.5",
"@juggle/resize-observer": "^3.4.0",
"@marsidev/react-turnstile": "^1.0.0",
"@preact/signals-core": "^1.8.0",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-popover": "^1.0.7",

View File

@@ -1,22 +1,22 @@
import { BlockStdScope, type EditorHost } from '@blocksuite/affine/block-std';
import {
type AffineAIPanelState,
type AffineAIPanelWidgetConfig,
import type {
AffineAIPanelState,
AffineAIPanelWidgetConfig,
} from '@blocksuite/affine/blocks';
import {
CodeBlockComponent,
DividerBlockComponent,
ListBlockComponent,
ParagraphBlockComponent,
SpecProvider,
} from '@blocksuite/affine/blocks';
import { WithDisposable } from '@blocksuite/affine/global/utils';
import { BlockViewType, type Doc, type Query } from '@blocksuite/affine/store';
import { css, html, LitElement, type PropertyValues } from 'lit';
import { css, html, LitElement, nothing, type PropertyValues } from 'lit';
import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { keyed } from 'lit/directives/keyed.js';
import { CustomPageEditorBlockSpecs } from '../utils/custom-specs';
import { markDownToDoc } from '../utils/markdown-utils';
const textBlockStyles = css`
@@ -67,12 +67,12 @@ const customHeadingStyles = css`
}
`;
type TextRendererOptions = {
export type TextRendererOptions = {
maxHeight?: number;
customHeading?: boolean;
};
export class AIAnswerText extends WithDisposable(LitElement) {
export class TextRenderer extends WithDisposable(LitElement) {
static override styles = css`
.ai-answer-text-editor.affine-page-viewport {
background: transparent;
@@ -138,43 +138,14 @@ export class AIAnswerText extends WithDisposable(LitElement) {
editor-host * {
box-sizing: border-box;
}
editor-host {
isolation: isolate;
}
}
${textBlockStyles}
${customHeadingStyles}
`;
@query('.ai-answer-text-container')
private accessor _container!: HTMLDivElement;
private _doc!: Doc;
private _answers: string[] = [];
private _timer?: ReturnType<typeof setInterval> | null = null;
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor answer!: string;
@property({ attribute: false })
accessor options!: TextRendererOptions;
@property({ attribute: false })
accessor state: AffineAIPanelState | undefined = undefined;
private _onWheel(e: MouseEvent) {
e.stopPropagation();
if (this.state === 'generating') {
e.preventDefault();
}
}
private readonly _clearTimer = () => {
if (this._timer) {
clearInterval(this._timer);
@@ -182,6 +153,8 @@ export class AIAnswerText extends WithDisposable(LitElement) {
}
};
private _doc: Doc | null = null;
private readonly _query: Query = {
mode: 'strict',
match: [
@@ -195,6 +168,8 @@ export class AIAnswerText extends WithDisposable(LitElement) {
].map(flavour => ({ flavour, viewType: BlockViewType.Display })),
};
private _timer?: ReturnType<typeof setInterval> | null = null;
private readonly _updateDoc = () => {
if (this._answers.length > 0) {
const latestAnswer = this._answers.pop();
@@ -222,13 +197,11 @@ export class AIAnswerText extends WithDisposable(LitElement) {
}
};
override shouldUpdate(changedProperties: PropertyValues) {
if (changedProperties.has('answer')) {
this._answers.push(this.answer);
return false;
private _onWheel(e: MouseEvent) {
e.stopPropagation();
if (this.state === 'generating') {
e.preventDefault();
}
return true;
}
override connectedCallback() {
@@ -246,16 +219,13 @@ export class AIAnswerText extends WithDisposable(LitElement) {
this._clearTimer();
}
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
requestAnimationFrame(() => {
if (!this._container) return;
this._container.scrollTop = this._container.scrollHeight;
});
}
override render() {
if (!this._doc) {
return nothing;
}
const { maxHeight, customHeading } = this.options;
const previewSpec = SpecProvider.getInstance().getSpec('page:preview');
const classes = classMap({
'ai-answer-text-container': true,
'show-scrollbar': !!maxHeight,
@@ -273,18 +243,50 @@ export class AIAnswerText extends WithDisposable(LitElement) {
html`<div class="ai-answer-text-editor affine-page-viewport">
${new BlockStdScope({
doc: this._doc,
extensions: CustomPageEditorBlockSpecs,
extensions: previewSpec.value,
}).render()}
</div>`
)}
</div>
`;
}
override shouldUpdate(changedProperties: PropertyValues) {
if (changedProperties.has('answer')) {
this._answers.push(this.answer);
return false;
}
return true;
}
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
requestAnimationFrame(() => {
if (!this._container) return;
this._container.scrollTop = this._container.scrollHeight;
});
}
@query('.ai-answer-text-container')
private accessor _container!: HTMLDivElement;
@property({ attribute: false })
accessor answer!: string;
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor options!: TextRendererOptions;
@property({ attribute: false })
accessor state: AffineAIPanelState | undefined = undefined;
}
declare global {
interface HTMLElementTagNameMap {
'ai-answer-text': AIAnswerText;
'text-renderer': TextRenderer;
}
}
@@ -293,11 +295,11 @@ export const createTextRenderer: (
options: TextRendererOptions
) => AffineAIPanelWidgetConfig['answerRenderer'] = (host, options) => {
return (answer, state) => {
return html`<ai-answer-text
return html`<text-renderer
.host=${host}
.answer=${answer}
.state=${state}
.options=${options}
></ai-answer-text>`;
></text-renderer>`;
};
};

View File

@@ -0,0 +1,2 @@
export * from './components/text-renderer';
export * from './utils/markdown-utils';

View File

@@ -12,7 +12,8 @@ import {
PlainTextAdapter,
titleMiddleware,
} from '@blocksuite/affine/blocks';
import { assertExists } from '@blocksuite/affine/global/utils';
import { DocCollection, Job } from '@blocksuite/affine/store';
import { assertExists } from '@blocksuite/global/utils';
import type {
BlockModel,
BlockSnapshot,
@@ -20,8 +21,7 @@ import type {
DraftModel,
Slice,
SliceSnapshot,
} from '@blocksuite/affine/store';
import { DocCollection, Job } from '@blocksuite/affine/store';
} from '@blocksuite/store';
const updateSnapshotText = (
point: TextRangePoint,

View File

@@ -24,14 +24,14 @@ import {
getElementsBound,
type SerializedXYWH,
} from '@blocksuite/affine/global/utils';
import { type ChatMessage } from '@blocksuite/affine/presets';
import type { Doc } from '@blocksuite/affine/store';
import type { ChatMessage } from '@toeverything/infra/blocksuite';
import type { TemplateResult } from 'lit';
import { insertFromMarkdown } from '../../_common';
import { AIProvider, type AIUserInfo } from '../provider';
import { reportResponse } from '../utils/action-reporter';
import { insertBelow, replace } from '../utils/editor-actions';
import { insertFromMarkdown } from '../utils/markdown-utils';
import { BlockIcon, CreateIcon, InsertBelowIcon, ReplaceIcon } from './icons';
const { matchFlavours } = BlocksUtils;

View File

@@ -7,6 +7,7 @@ import type {
import { assertExists } from '@blocksuite/affine/global/utils';
import type { TemplateResult } from 'lit';
import { createTextRenderer } from '../../_common';
import {
buildCopyConfig,
buildErrorConfig,
@@ -14,7 +15,6 @@ import {
buildGeneratingConfig,
getAIPanel,
} from '../ai-panel';
import { createTextRenderer } from '../messages/text';
import { AIProvider } from '../provider';
import { reportResponse } from '../utils/action-reporter';
import {

View File

@@ -15,17 +15,17 @@ import {
TextElementModel,
} from '@blocksuite/affine/blocks';
import { assertExists } from '@blocksuite/affine/global/utils';
import { AIChatBlockModel } from '@blocksuite/affine/presets';
import { Slice } from '@blocksuite/affine/store';
import { AIChatBlockModel } from '@toeverything/infra';
import type { TemplateResult } from 'lit';
import { createTextRenderer, getContentFromSlice } from '../../_common';
import { getAIPanel } from '../ai-panel';
import {
createMindmapExecuteRenderer,
createMindmapRenderer,
} from '../messages/mindmap';
import { createSlidesRenderer } from '../messages/slides-renderer';
import { createTextRenderer } from '../messages/text';
import { createIframeRenderer, createImageRenderer } from '../messages/wrapper';
import { AIProvider } from '../provider';
import { reportResponse } from '../utils/action-reporter';
@@ -35,7 +35,6 @@ import {
isMindMapRoot,
} from '../utils/edgeless';
import { copyTextAnswer } from '../utils/editor-actions';
import { getContentFromSlice } from '../utils/markdown-utils';
import {
getCopilotSelectedElems,
getSelectedNoteAnchor,

View File

@@ -28,6 +28,7 @@ import { assertExists, Bound } from '@blocksuite/affine/global/utils';
import { html, type TemplateResult } from 'lit';
import { styleMap } from 'lit/directives/style-map.js';
import { insertFromMarkdown } from '../../_common';
import { AIPenIcon, ChatWithAIIcon } from '../_common/icons';
import { getAIPanel } from '../ai-panel';
import { AIProvider } from '../provider';
@@ -39,7 +40,6 @@ import {
} from '../utils/edgeless';
import { preprocessHtml } from '../utils/html';
import { fetchImageToFile } from '../utils/image';
import { insertFromMarkdown } from '../utils/markdown-utils';
import {
getCopilotSelectedElems,
getEdgelessRootFromEditor,
@@ -313,7 +313,7 @@ const imageHandler = (host: EditorHost) => {
host.doc.transact(() => {
edgelessRoot
.addImages([img], [x, y], true)
.addImages([img], [x, y])
.then(blockIds => {
const imageBlockId = blockIds[0];
const imageBlock = host.doc.getBlock(imageBlockId);

View File

@@ -12,6 +12,7 @@ import {
import { assertExists, Bound } from '@blocksuite/affine/global/utils';
import type { TemplateResult } from 'lit';
import { createTextRenderer, insertFromMarkdown } from '../_common';
import {
AIPenIcon,
AIStarIconWithAnimation,
@@ -24,7 +25,6 @@ import {
RetryIcon,
} from './_common/icons';
import { INSERT_ABOVE_ACTIONS } from './actions/consts';
import { createTextRenderer } from './messages/text';
import { AIProvider } from './provider';
import { reportResponse } from './utils/action-reporter';
import { findNoteBlockModel, getService } from './utils/edgeless';
@@ -34,7 +34,6 @@ import {
insertBelow,
replace,
} from './utils/editor-actions';
import { insertFromMarkdown } from './utils/markdown-utils';
import { getSelections } from './utils/selection-utils';
function getSelection(host: EditorHost) {

View File

@@ -3,6 +3,7 @@ import { WithDisposable } from '@blocksuite/affine/global/utils';
import { css, html, LitElement, nothing, type TemplateResult } from 'lit';
import { property, state } from 'lit/decorators.js';
import { createTextRenderer } from '../../../_common';
import {
ActionIcon,
AIChangeToneIcon,
@@ -22,7 +23,6 @@ import {
ArrowDownIcon,
ArrowUpIcon,
} from '../../_common/icons';
import { createTextRenderer } from '../../messages/text';
import type { ChatAction } from '../chat-context';
import { renderImages } from '../components/images';
import { HISTORY_IMAGE_ACTIONS } from '../const';

View File

@@ -6,7 +6,7 @@ import { WithDisposable } from '@blocksuite/affine/global/utils';
import { html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { createTextRenderer } from '../../messages/text';
import { createTextRenderer } from '../../../_common';
import { renderImages } from '../components/images';
export class ChatText extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })

View File

@@ -6,7 +6,7 @@ import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { createTextRenderer } from '../../messages/text';
import { createTextRenderer } from '../../../_common';
import type { ChatAction } from '../chat-context';
export class ActionText extends WithDisposable(LitElement) {

View File

@@ -1,18 +1,3 @@
import '../messages/slides-renderer';
import './ai-loading';
import '../messages/text';
import './actions/text';
import './actions/action-wrapper';
import './actions/make-real';
import './actions/slides';
import './actions/mindmap';
import './actions/chat-text';
import './actions/image-to-text';
import './actions/image';
import './chat-cards';
import '../_common/components/chat-action-list';
import '../_common/components/copy-more';
import type { BaseSelection, EditorHost } from '@blocksuite/affine/block-std';
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import {

View File

@@ -6,4 +6,3 @@ export * from './entries/index';
export * from './messages/index';
export { AIChatBlockPeekViewTemplate } from './peek-view/chat-block-peek-view';
export * from './provider';
export * from './setup';

View File

@@ -1,2 +1 @@
export * from './text';
export * from './wrapper';

View File

@@ -1,6 +1,6 @@
import type { EditorHost } from '@blocksuite/affine/block-std';
import { type AIError, openFileOrFiles } from '@blocksuite/affine/blocks';
import { type ChatMessage } from '@blocksuite/affine/presets';
import type { ChatMessage } from '@toeverything/infra';
import { css, html, LitElement, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';

View File

@@ -1,8 +1,3 @@
import './chat-block-input';
import './date-time';
import '../_common/components/chat-action-list';
import '../_common/components/copy-more';
import { type EditorHost } from '@blocksuite/affine/block-std';
import {
type AIError,
@@ -17,7 +12,7 @@ import {
type AIChatBlockModel,
type ChatMessage,
ChatMessagesSchema,
} from '@blocksuite/affine/presets';
} from '@toeverything/infra/blocksuite';
import { html, LitElement, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';

View File

@@ -1,5 +1,5 @@
import type { AIError } from '@blocksuite/affine/blocks';
import { type ChatMessage } from '@blocksuite/affine/presets';
import type { ChatMessage } from '@toeverything/infra/blocksuite';
export type ChatStatus =
| 'success'

View File

@@ -4,7 +4,7 @@ import {
type AIChatBlockModel,
CHAT_BLOCK_HEIGHT,
CHAT_BLOCK_WIDTH,
} from '@blocksuite/affine/presets';
} from '@toeverything/infra';
/**
* Calculates the bounding box for a child block

View File

@@ -1,50 +0,0 @@
import { AskAIButton } from './_common/components/ask-ai-button';
import { AskAIPanel } from './_common/components/ask-ai-panel';
import { ChatActionList } from './_common/components/chat-action-list';
import { ChatCopyMore } from './_common/components/copy-more';
import { ChatPanel } from './chat-panel';
import { ActionWrapper } from './chat-panel/actions/action-wrapper';
import { ChatText } from './chat-panel/actions/chat-text';
import { ActionImage } from './chat-panel/actions/image';
import { ActionImageToText } from './chat-panel/actions/image-to-text';
import { ActionMakeReal } from './chat-panel/actions/make-real';
import { ActionMindmap } from './chat-panel/actions/mindmap';
import { ActionSlides } from './chat-panel/actions/slides';
import { ActionText } from './chat-panel/actions/text';
import { AILoading } from './chat-panel/ai-loading';
import { ChatCards } from './chat-panel/chat-cards';
import { ChatPanelInput } from './chat-panel/chat-panel-input';
import { ChatPanelMessages } from './chat-panel/chat-panel-messages';
import { AIAnswerText, AIAnswerWrapper } from './messages';
import { AIErrorWrapper } from './messages/error';
import { AISlidesRenderer } from './messages/slides-renderer';
import { ChatBlockInput } from './peek-view/chat-block-input';
import { AIChatBlockPeekView } from './peek-view/chat-block-peek-view';
import { DateTime } from './peek-view/date-time';
export function registerAICustomComponents() {
customElements.define('ask-ai-button', AskAIButton);
customElements.define('ask-ai-panel', AskAIPanel);
customElements.define('chat-action-list', ChatActionList);
customElements.define('chat-copy-more', ChatCopyMore);
customElements.define('action-wrapper', ActionWrapper);
customElements.define('chat-text', ChatText);
customElements.define('action-image-to-text', ActionImageToText);
customElements.define('action-image', ActionImage);
customElements.define('action-make-real', ActionMakeReal);
customElements.define('action-mindmap', ActionMindmap);
customElements.define('action-slides', ActionSlides);
customElements.define('action-text', ActionText);
customElements.define('ai-loading', AILoading);
customElements.define('chat-cards', ChatCards);
customElements.define('chat-panel-input', ChatPanelInput);
customElements.define('chat-panel-messages', ChatPanelMessages);
customElements.define('chat-panel', ChatPanel);
customElements.define('ai-error-wrapper', AIErrorWrapper);
customElements.define('ai-slides-renderer', AISlidesRenderer);
customElements.define('ai-answer-wrapper', AIAnswerWrapper);
customElements.define('ai-answer-text', AIAnswerText);
customElements.define('chat-block-input', ChatBlockInput);
customElements.define('ai-chat-block-peek-view', AIChatBlockPeekView);
customElements.define('date-time', DateTime);
}

View File

@@ -2,7 +2,7 @@ import type { EditorHost } from '@blocksuite/affine/block-std';
import type { EdgelessRootService } from '@blocksuite/affine/blocks';
import type { BlockSnapshot } from '@blocksuite/affine/store';
import { markdownToSnapshot } from '../utils/markdown-utils';
import { markdownToSnapshot } from '../../_common';
import { getSurfaceElementFromEditor } from '../utils/selection-utils';
import {
basicTheme,

View File

@@ -11,7 +11,7 @@ import {
insertFromMarkdown,
markDownToDoc,
markdownToSnapshot,
} from './markdown-utils';
} from '../../_common';
const getNoteId = (blockElement: BlockComponent) => {
let element = blockElement;

View File

@@ -14,8 +14,8 @@ import {
toDraftModel,
} from '@blocksuite/affine/store';
import { getContentFromSlice } from '../../_common';
import { getEdgelessCopilotWidget, getService } from './edgeless';
import { getContentFromSlice } from './markdown-utils';
export const getRootService = (host: EditorHost) => {
return host.std.getService('affine:page');

View File

@@ -0,0 +1,102 @@
import { html } from 'lit';
export const ChatWithAIIcon = html`<svg
width="21"
height="21"
viewBox="0 0 21 21"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.59593 7.38585C2.75058 6.62196 3.25841 5.16699 4.43763 5.16699H16.3017C17.2852 5.16699 17.8664 6.23248 17.3995 7.06491L11.5806 17.4378C11.0097 18.4556 9.51091 18.2189 9.25406 17.098L7.9249 11.2976L3.59593 7.38585ZM9.20223 11.2755L10.4725 16.8188C10.4742 16.8262 10.4759 16.8301 10.4767 16.8316C10.4777 16.8321 10.4796 16.8329 10.4827 16.8334C10.4839 16.8336 10.4849 16.8337 10.4857 16.8337C10.4869 16.8321 10.4885 16.8297 10.4904 16.8263L15.7266 7.492L9.20223 11.2755ZM15.0887 6.41699H4.43763C4.4362 6.41699 4.43499 6.41703 4.434 6.41709C4.43249 6.41912 4.43033 6.42258 4.42836 6.42784C4.42439 6.43845 4.42506 6.44624 4.42551 6.44838C4.42564 6.44898 4.42571 6.4491 4.42586 6.44937L4.42588 6.44939C4.42593 6.44949 4.42768 6.4527 4.434 6.45841L8.57091 10.1967L15.0887 6.41699Z"
/>
</svg>`;
export const AffineAIIcon = html`<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.2812 5.49104C11.2403 5.13024 10.9353 4.85751 10.5722 4.85714C10.2091 4.85677 9.90345 5.12887 9.86185 5.48959C9.59131 7.83515 8.89003 9.48448 7.75868 10.6158C6.62734 11.7472 4.97801 12.4485 2.63244 12.719C2.27173 12.7606 1.99963 13.0662 2 13.4293C2.00037 13.7924 2.2731 14.0975 2.63389 14.1383C4.94069 14.3996 6.62508 15.1006 7.78328 16.2379C8.93713 17.3709 9.65305 19.0198 9.85994 21.3489C9.89271 21.7178 10.2019 22.0004 10.5722 22C10.9425 21.9996 11.2511 21.7162 11.2831 21.3473C11.4813 19.0565 12.1966 17.3729 13.3562 16.2133C14.5157 15.0537 16.1994 14.3385 18.4902 14.1402C18.8591 14.1083 19.1424 13.7997 19.1429 13.4294C19.1433 13.0591 18.8606 12.7499 18.4918 12.7171C16.1627 12.5102 14.5137 11.7943 13.3807 10.6404C12.2435 9.48222 11.5425 7.79783 11.2812 5.49104Z"
/>
<path
d="M18.9427 2.24651C18.9268 2.1062 18.8082 2.00014 18.667 2C18.5257 1.99986 18.4069 2.10567 18.3907 2.24595C18.2855 3.15811 18.0128 3.79952 17.5728 4.23949C17.1329 4.67946 16.4914 4.95218 15.5793 5.05739C15.439 5.07356 15.3332 5.19241 15.3333 5.33362C15.3335 5.47482 15.4395 5.59345 15.5798 5.60935C16.4769 5.71096 17.132 5.98357 17.5824 6.42584C18.0311 6.86644 18.3095 7.50771 18.39 8.41347C18.4027 8.55691 18.523 8.66683 18.667 8.66667C18.811 8.6665 18.931 8.55632 18.9434 8.41284C19.0205 7.52199 19.2987 6.86723 19.7496 6.41629C20.2006 5.96534 20.8553 5.68719 21.7462 5.61008C21.8896 5.59766 21.9998 5.47765 22 5.33365C22.0002 5.18964 21.8902 5.06939 21.7468 5.05664C20.841 4.97619 20.1998 4.69777 19.7592 4.24905C19.3169 3.79864 19.0443 3.1436 18.9427 2.24651Z"
/>
</svg> `;
export const ImageLoadingFailedIcon = html`<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.1665 3.99984C2.1665 2.98732 2.98732 2.1665 3.99984 2.1665H11.9998C13.0124 2.1665 13.8332 2.98732 13.8332 3.99984V7.33317C13.8332 7.60931 13.6093 7.83317 13.3332 7.83317C13.057 7.83317 12.8332 7.60931 12.8332 7.33317V3.99984C12.8332 3.5396 12.4601 3.1665 11.9998 3.1665H3.99984C3.5396 3.1665 3.1665 3.5396 3.1665 3.99984V9.4594L5.37014 7.25576C6.0861 6.5398 7.2469 6.5398 7.96287 7.25576L8.35339 7.64628C8.54865 7.84155 8.54865 8.15813 8.35339 8.35339C8.15813 8.54865 7.84155 8.54865 7.64628 8.35339L7.25576 7.96287C6.93032 7.63743 6.40268 7.63743 6.07725 7.96287L3.1665 10.8736V11.9998C3.1665 12.4601 3.5396 12.8332 3.99984 12.8332H7.33317C7.60931 12.8332 7.83317 13.057 7.83317 13.3332C7.83317 13.6093 7.60931 13.8332 7.33317 13.8332H3.99984C2.98732 13.8332 2.1665 13.0124 2.1665 11.9998V3.99984Z"
fill="currentColor"
fill-opacity="0.6"
/>
<path
d="M9.99984 5.33317C9.99984 5.70136 9.70136 5.99984 9.33317 5.99984C8.96498 5.99984 8.6665 5.70136 8.6665 5.33317C8.6665 4.96498 8.96498 4.6665 9.33317 4.6665C9.70136 4.6665 9.99984 4.96498 9.99984 5.33317Z"
fill="currentColor"
fill-opacity="0.6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.97962 8.97962C9.17488 8.78435 9.49146 8.78435 9.68672 8.97962L11.3332 10.6261L12.9796 8.97962C13.1749 8.78435 13.4915 8.78435 13.6867 8.97962C13.882 9.17488 13.882 9.49146 13.6867 9.68672L12.0403 11.3332L13.6867 12.9796C13.882 13.1749 13.882 13.4915 13.6867 13.6867C13.4915 13.882 13.1749 13.882 12.9796 13.6867L11.3332 12.0403L9.68672 13.6867C9.49146 13.882 9.17488 13.882 8.97962 13.6867C8.78435 13.4915 8.78435 13.1749 8.97962 12.9796L10.6261 11.3332L8.97962 9.68672C8.78435 9.49146 8.78435 9.17488 8.97962 8.97962Z"
fill="currentColor"
fill-opacity="0.6"
/>
</svg>`;
export const LoadingIcon = html`<svg
width="16"
height="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<style xmlns="http://www.w3.org/2000/svg">
.spinner {
transform-origin: center;
animation: spinner_animate 0.75s infinite linear;
}
@keyframes spinner_animate {
100% {
transform: rotate(360deg);
}
}
</style>
<path
d="M14.6666 8.00004C14.6666 11.6819 11.6818 14.6667 7.99992 14.6667C4.31802 14.6667 1.33325 11.6819 1.33325 8.00004C1.33325 4.31814 4.31802 1.33337 7.99992 1.33337C11.6818 1.33337 14.6666 4.31814 14.6666 8.00004ZM3.30003 8.00004C3.30003 10.5957 5.40424 12.6999 7.99992 12.6999C10.5956 12.6999 12.6998 10.5957 12.6998 8.00004C12.6998 5.40436 10.5956 3.30015 7.99992 3.30015C5.40424 3.30015 3.30003 5.40436 3.30003 8.00004Z"
fill="black"
fill-opacity="0.1"
/>
<path
d="M13.6833 8.00004C14.2263 8.00004 14.674 7.55745 14.5942 7.02026C14.5142 6.48183 14.3684 5.954 14.1591 5.44882C13.8241 4.63998 13.333 3.90505 12.714 3.286C12.0949 2.66694 11.36 2.17588 10.5511 1.84084C10.046 1.63159 9.51812 1.48576 8.9797 1.40576C8.44251 1.32595 7.99992 1.77363 7.99992 2.31671C7.99992 2.85979 8.44486 3.28974 8.9761 3.40253C9.25681 3.46214 9.53214 3.54746 9.79853 3.65781C10.3688 3.894 10.8869 4.2402 11.3233 4.67664C11.7598 5.11307 12.106 5.6312 12.3422 6.20143C12.4525 6.46782 12.5378 6.74315 12.5974 7.02386C12.7102 7.5551 13.1402 8.00004 13.6833 8.00004Z"
fill="#1C9EE4"
class="spinner"
/>
</svg>`;
export const SmallHintIcon = html`<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.00008 3.16699C5.33071 3.16699 3.16675 5.33095 3.16675 8.00033C3.16675 10.6697 5.33071 12.8337 8.00008 12.8337C10.6695 12.8337 12.8334 10.6697 12.8334 8.00033C12.8334 5.33095 10.6695 3.16699 8.00008 3.16699ZM2.16675 8.00033C2.16675 4.77866 4.77842 2.16699 8.00008 2.16699C11.2217 2.16699 13.8334 4.77866 13.8334 8.00033C13.8334 11.222 11.2217 13.8337 8.00008 13.8337C4.77842 13.8337 2.16675 11.222 2.16675 8.00033ZM8.00008 5.12996C8.27622 5.12996 8.50008 5.35381 8.50008 5.62996V8.00033C8.50008 8.27647 8.27622 8.50033 8.00008 8.50033C7.72394 8.50033 7.50008 8.27647 7.50008 8.00033V5.62996C7.50008 5.35381 7.72394 5.12996 8.00008 5.12996ZM7.50008 10.3707C7.50008 10.0946 7.72394 9.8707 8.00008 9.8707H8.00601C8.28215 9.8707 8.50601 10.0946 8.50601 10.3707C8.50601 10.6468 8.28215 10.8707 8.00601 10.8707H8.00008C7.72394 10.8707 7.50008 10.6468 7.50008 10.3707Z"
fill-opacity="0.6"
/>
</svg> `;

View File

@@ -0,0 +1,60 @@
import { BlockComponent } from '@blocksuite/affine/block-std';
import { Peekable } from '@blocksuite/affine/blocks';
import { computed } from '@preact/signals-core';
import {
type AIChatBlockModel,
ChatMessagesSchema,
} from '@toeverything/infra/blocksuite';
import { html } from 'lit';
import { ChatWithAIIcon } from '../_common/icon';
import { AIChatBlockStyles } from './styles';
@Peekable({
enableOn: ({ doc }: AIChatBlockComponent) => !doc.readonly,
})
export class AIChatBlockComponent extends BlockComponent<AIChatBlockModel> {
static override styles = AIChatBlockStyles;
// Deserialize messages from JSON string and verify the type using zod
private readonly _deserializeChatMessages = computed(() => {
const messages = this.model.messages$.value;
try {
const result = ChatMessagesSchema.safeParse(JSON.parse(messages));
if (result.success) {
return result.data;
} else {
return [];
}
} catch {
return [];
}
});
override renderBlock() {
const messages = this._deserializeChatMessages.value.slice(-2);
const textRendererOptions = {
customHeading: true,
};
return html`<div class="affine-ai-chat-block-container">
<div class="ai-chat-messages-container">
<ai-chat-messages
.host=${this.host}
.messages=${messages}
.textRendererOptions=${textRendererOptions}
.withMask=${true}
></ai-chat-messages>
</div>
<div class="ai-chat-block-button">
${ChatWithAIIcon} <span>AI chat block</span>
</div>
</div> `;
}
}
declare global {
interface HTMLElementTagNameMap {
'affine-ai-chat': AIChatBlockComponent;
}
}

View File

@@ -0,0 +1,38 @@
import { toGfxBlockComponent } from '@blocksuite/affine/block-std';
import { Bound } from '@blocksuite/global/utils';
import { html } from 'lit';
import { styleMap } from 'lit/directives/style-map.js';
import { AIChatBlockComponent } from './ai-chat-block';
export class EdgelessAIChatBlockComponent extends toGfxBlockComponent(
AIChatBlockComponent
) {
override renderGfxBlock() {
const bound = Bound.deserialize(this.model.xywh$.value);
const scale = this.model.scale$.value;
const width = bound.w / scale;
const height = bound.h / scale;
const style = {
width: `${width}px`,
height: `${height}px`,
borderRadius: '8px',
transformOrigin: '0 0',
boxShadow: 'var(--affine-shadow-1)',
border: '1px solid var(--affine-border-color)',
transform: `scale(${scale})`,
};
return html`
<div class="edgeless-ai-chat" style=${styleMap(style)}>
${this.renderPageContent()}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'affine-edgeless-ai-chat': EdgelessAIChatBlockComponent;
}
}

View File

@@ -0,0 +1,17 @@
import {
BlockViewExtension,
type ExtensionType,
} from '@blocksuite/affine/block-std';
import { literal } from 'lit/static-html.js';
export const AIChatBlockSpec: ExtensionType[] = [
BlockViewExtension('affine:embed-ai-chat', model => {
const parent = model.doc.getParent(model.id);
if (parent?.flavour === 'affine:surface') {
return literal`affine-edgeless-ai-chat`;
}
return literal`affine-ai-chat`;
}),
];

View File

@@ -0,0 +1,153 @@
import type { EditorHost } from '@blocksuite/block-std';
import type { AffineAIPanelState } from '@blocksuite/blocks';
import type {
ChatMessage,
MessageRole,
MessageUserInfo,
} from '@toeverything/infra/blocksuite';
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import type { TextRendererOptions } from '../../../_common/components/text-renderer';
import { UserInfoTemplate } from './user-info';
export class AIChatMessage extends LitElement {
static override styles = css`
.ai-chat-message {
display: flex;
width: 100%;
flex-direction: column;
gap: 4px;
box-sizing: border-box;
}
.ai-chat-content {
display: block;
width: calc(100% - 34px);
padding-left: 34px;
font-weight: 400;
}
.with-attachments {
margin-top: 8px;
}
`;
override render() {
const {
host,
textRendererOptions,
state,
content,
attachments,
messageRole,
userInfo,
} = this;
const withAttachments = !!attachments && attachments.length > 0;
const messageClasses = classMap({
'with-attachments': withAttachments,
});
return html`
<div class="ai-chat-message">
${UserInfoTemplate(userInfo, messageRole)}
<div class="ai-chat-content">
<chat-images .attachments=${attachments}></chat-images>
<div class=${messageClasses}>
<text-renderer
.host=${host}
.answer=${content}
.options=${textRendererOptions}
.state=${state}
></text-renderer>
</div>
</div>
</div>
`;
}
@property({ attribute: false })
accessor attachments: string[] | undefined = undefined;
@property({ attribute: false })
accessor content: string = '';
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor messageRole: MessageRole | undefined = undefined;
@property({ attribute: false })
accessor state: AffineAIPanelState = 'finished';
@property({ attribute: false })
accessor textRendererOptions: TextRendererOptions = {};
@property({ attribute: false })
accessor userInfo: MessageUserInfo = {};
}
export class AIChatMessages extends LitElement {
static override styles = css`
:host {
width: 100%;
box-sizing: border-box;
}
.ai-chat-messages {
display: flex;
box-sizing: border-box;
width: 100%;
height: 100%;
flex-direction: column;
gap: 24px;
}
`;
override render() {
return html`<div class="ai-chat-messages">
${repeat(
this.messages,
message => message.id,
message => {
const { attachments, role, content } = message;
const userInfo = {
userId: message.userId,
userName: message.userName,
avatarUrl: message.avatarUrl,
};
return html`
<ai-chat-message
.host=${this.host}
.textRendererOptions=${this.textRendererOptions}
.content=${content}
.attachments=${attachments}
.messageRole=${role}
.userInfo=${userInfo}
></ai-chat-message>
`;
}
)}
</div>`;
}
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor messages: ChatMessage[] = [];
@property({ attribute: false })
accessor textRendererOptions: TextRendererOptions = {};
}
declare global {
interface HTMLElementTagNameMap {
'ai-chat-message': AIChatMessage;
'ai-chat-messages': AIChatMessages;
}
}

View File

@@ -0,0 +1,102 @@
import { css, html, LitElement, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js';
import { repeat } from 'lit/directives/repeat.js';
import { ImageLoadingFailedIcon, LoadingIcon } from '../../_common/icon';
export class ChatImage extends LitElement {
static override styles = css`
.image-container {
border-radius: 4px;
overflow: hidden;
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 70%;
max-width: 200px;
max-height: 122px;
img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
}
}
`;
override render() {
return choose(this.status, [
[
'loading',
() =>
html`<image-placeholder
.text=${'Loading image'}
.icon=${LoadingIcon}
></image-placeholder>`,
],
[
'error',
() =>
html`<image-placeholder
.text=${'Image Loading Failed'}
.icon=${ImageLoadingFailedIcon}
></image-placeholder>`,
],
[
'success',
() =>
html`<div class="image-container">
<img src=${this.imageUrl} />
</div>`,
],
]);
}
@property({ attribute: false })
accessor imageUrl!: string;
@property({ attribute: false })
accessor status!: 'loading' | 'error' | 'success';
}
export class ChatImages extends LitElement {
static override styles = css`
.images-container {
display: flex;
width: 100%;
gap: 8px;
flex-wrap: wrap;
}
`;
override render() {
if (!this.attachments || this.attachments.length === 0) {
return nothing;
}
return html`<div class="images-container">
${repeat(
this.attachments,
attachment => attachment,
attachment =>
html`<chat-image
.imageUrl=${attachment}
.status=${'success'}
></chat-image>`
)}
</div>`;
}
@property({ attribute: false })
accessor attachments: string[] | undefined;
}
declare global {
interface HTMLElementTagNameMap {
'chat-image': ChatImage;
'chat-images': ChatImages;
}
}

View File

@@ -0,0 +1,62 @@
import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, type TemplateResult, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
export class ImagePlaceholder extends LitElement {
static override styles = css`
.placeholder-container {
display: flex;
width: 100%;
height: 122px;
padding: 12px;
align-items: flex-start;
border-radius: 8px;
border: 1px solid var(--affine-background-tertiary-color);
background: var(--affine-background-secondary-color);
box-sizing: border-box;
}
.placeholder-title {
display: flex;
gap: 8px;
align-items: center;
color: var(--affine-placeholder-color, #c0bfc1);
text-align: justify;
/* light/smBold */
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
font-size: var(--affine-font-sm);
font-style: normal;
font-weight: 600;
line-height: 22px; /* 157.143% */
height: 22px;
}
.placeholder-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--affine-icon-color);
}
`;
override render() {
return html`<div class="placeholder-container">
<div class="placeholder-title">
<span class="placeholder-icon">${this.icon}</span>
<span>${this.text}</span>
</div>
</div>`;
}
@property({ attribute: false })
accessor icon!: TemplateResult<1>;
@property({ attribute: false })
accessor text!: string;
}
declare global {
interface HTMLElementTagNameMap {
'image-placeholder': ImagePlaceholder;
}
}

View File

@@ -0,0 +1,114 @@
import type { MessageRole, MessageUserInfo } from '@toeverything/infra';
import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, type TemplateResult, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import { AffineAIIcon } from '../../_common/icon';
export class UserInfo extends LitElement {
static override styles = css`
.user-info-container {
display: flex;
width: 100%;
height: 24px;
flex-direction: row;
gap: 10px;
font-weight: 500;
.user-avatar-container {
width: 24px;
height: 24px;
color: var(--affine-brand-color);
display: flex;
justify-content: center;
align-items: center;
}
.default-avatar,
.user-avatar-container img {
width: 100%;
height: 100%;
border-radius: 50%;
}
.user-avatar-container img {
object-fit: cover;
}
.default-avatar,
.avatar-image {
background-color: var(--affine-primary-color);
}
.user-name {
color: var(--affine-text-primary-color);
text-align: justify;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
font-size: var(--affine-font-sm);
font-style: normal;
font-weight: 500;
line-height: 22px;
text-overflow: ellipsis;
}
}
`;
private _handleAvatarLoadError(e: Event) {
const target = e.target as HTMLImageElement;
target.onerror = null;
this.avatarLoadedFailed = true;
}
override render() {
return html`<div class="user-info-container">
<div class="user-avatar-container">
${this.avatarIcon
? this.avatarIcon
: this.avatarUrl && !this.avatarLoadedFailed
? html`<img
.src=${this.avatarUrl}
@error=${this._handleAvatarLoadError}
/>`
: html`<span class="default-avatar"></span>`}
</div>
<span class="user-name">${this.userName}</span>
</div>`;
}
@property({ attribute: false })
accessor avatarIcon: TemplateResult<1> | undefined = undefined;
@property({ attribute: false })
accessor avatarLoadedFailed = false;
@property({ attribute: false })
accessor avatarUrl: string | undefined = undefined;
@property({ attribute: false })
accessor userName!: string;
}
declare global {
interface HTMLElementTagNameMap {
'user-info': UserInfo;
}
}
export function UserInfoTemplate(
userInfo: MessageUserInfo,
messageRole?: MessageRole
) {
const isUser = !!messageRole && messageRole === 'user';
const userInfoTemplate = isUser
? html`<user-info
.userName=${userInfo.userName ?? 'You'}
.avatarUrl=${userInfo.avatarUrl}
></user-info>`
: html`<user-info
.userName=${'AFFiNE AI'}
.avatarIcon=${AffineAIIcon}
></user-info>`;
return userInfoTemplate;
}

View File

@@ -0,0 +1,3 @@
export * from './ai-chat-block.js';
export * from './ai-chat-edgeless-block.js';
export * from './ai-chat-spec.js';

View File

@@ -0,0 +1,54 @@
import { baseTheme } from '@toeverything/theme';
import { css, unsafeCSS } from 'lit';
export const AIChatBlockStyles = css`
.affine-ai-chat-block-container {
display: flex;
flex-direction: column;
align-items: flex-end;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 16px;
background: var(--affine-white);
color: var(--affine-text-primary-color);
line-height: 22px;
font-size: var(--affine-font-sm);
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
border-radius: 8px;
user-select: none;
pointer-events: none;
.ai-chat-messages-container {
display: block;
flex: 1 0 0;
width: 100%;
box-sizing: border-box;
background: linear-gradient(to top, transparent, var(--affine-white));
-webkit-mask-image: linear-gradient(
to bottom,
var(--affine-white) 25%,
transparent
);
mask-image: linear-gradient(
to bottom,
var(--affine-white) 25%,
transparent
);
overflow: hidden;
}
.ai-chat-block-button {
display: flex;
width: 100%;
height: 22px;
flex-direction: row;
align-items: center;
gap: 8px;
font-weight: 600;
svg {
color: var(--affine-icon-color);
}
}
}
`;

View File

@@ -0,0 +1,2 @@
export { AIChatMessages } from './ai-chat-block/components/ai-chat-messages.js';
export * from './ai-chat-block/index.js';

View File

@@ -0,0 +1,74 @@
import { TextRenderer } from './_common/components/text-renderer';
import { AskAIButton } from './ai/_common/components/ask-ai-button';
import { AskAIPanel } from './ai/_common/components/ask-ai-panel';
import { ChatActionList } from './ai/_common/components/chat-action-list';
import { ChatCopyMore } from './ai/_common/components/copy-more';
import { ChatPanel } from './ai/chat-panel';
import { ActionWrapper } from './ai/chat-panel/actions/action-wrapper';
import { ChatText } from './ai/chat-panel/actions/chat-text';
import { ActionImage } from './ai/chat-panel/actions/image';
import { ActionImageToText } from './ai/chat-panel/actions/image-to-text';
import { ActionMakeReal } from './ai/chat-panel/actions/make-real';
import { ActionMindmap } from './ai/chat-panel/actions/mindmap';
import { ActionSlides } from './ai/chat-panel/actions/slides';
import { ActionText } from './ai/chat-panel/actions/text';
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 { ChatPanelMessages } from './ai/chat-panel/chat-panel-messages';
import { AIErrorWrapper } from './ai/messages/error';
import { AISlidesRenderer } from './ai/messages/slides-renderer';
import { AIAnswerWrapper } from './ai/messages/wrapper';
import { ChatBlockInput } from './ai/peek-view/chat-block-input';
import { AIChatBlockPeekView } from './ai/peek-view/chat-block-peek-view';
import { DateTime } from './ai/peek-view/date-time';
import { AIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-block';
import { EdgelessAIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-edgeless-block';
import {
AIChatMessage,
AIChatMessages,
} from './blocks/ai-chat-block/components/ai-chat-messages';
import {
ChatImage,
ChatImages,
} from './blocks/ai-chat-block/components/chat-images';
import { ImagePlaceholder } from './blocks/ai-chat-block/components/image-placeholder';
import { UserInfo } from './blocks/ai-chat-block/components/user-info';
export function registerBlocksuitePresetsCustomComponents() {
customElements.define('ask-ai-button', AskAIButton);
customElements.define('ask-ai-panel', AskAIPanel);
customElements.define('chat-action-list', ChatActionList);
customElements.define('chat-copy-more', ChatCopyMore);
customElements.define('action-wrapper', ActionWrapper);
customElements.define('chat-text', ChatText);
customElements.define('action-image-to-text', ActionImageToText);
customElements.define('action-image', ActionImage);
customElements.define('action-make-real', ActionMakeReal);
customElements.define('action-mindmap', ActionMindmap);
customElements.define('action-slides', ActionSlides);
customElements.define('action-text', ActionText);
customElements.define('ai-loading', AILoading);
customElements.define('chat-cards', ChatCards);
customElements.define('chat-panel-input', ChatPanelInput);
customElements.define('chat-panel-messages', ChatPanelMessages);
customElements.define('chat-panel', ChatPanel);
customElements.define('ai-error-wrapper', AIErrorWrapper);
customElements.define('ai-slides-renderer', AISlidesRenderer);
customElements.define('ai-answer-wrapper', AIAnswerWrapper);
customElements.define('chat-block-input', ChatBlockInput);
customElements.define('ai-chat-block-peek-view', AIChatBlockPeekView);
customElements.define('date-time', DateTime);
customElements.define(
'affine-edgeless-ai-chat',
EdgelessAIChatBlockComponent
);
customElements.define('affine-ai-chat', AIChatBlockComponent);
customElements.define('ai-chat-message', AIChatMessage);
customElements.define('ai-chat-messages', AIChatMessages);
customElements.define('image-placeholder', ImagePlaceholder);
customElements.define('chat-image', ChatImage);
customElements.define('chat-images', ChatImages);
customElements.define('user-info', UserInfo);
customElements.define('text-renderer', TextRenderer);
}

View File

@@ -1,4 +1,4 @@
import { registerAICustomComponents } from '@affine/core/blocksuite/presets/ai';
import { registerBlocksuitePresetsCustomComponents } from '@affine/core/blocksuite/presets/effects';
import { effects as bsEffects } from '@blocksuite/affine/effects';
import { setupAIProvider } from './ai/setup-provider';
@@ -9,6 +9,6 @@ bsEffects();
patchEffects();
setupAIProvider();
edgelessEffects();
registerAICustomComponents();
registerBlocksuitePresetsCustomComponents();
export * from './blocksuite-editor';

View File

@@ -3,6 +3,7 @@ import {
AIImageBlockSpec,
AIParagraphBlockSpec,
} from '@affine/core/blocksuite/presets/ai';
import { AIChatBlockSpec } from '@affine/core/blocksuite/presets/blocks/ai-chat-block';
import type { ExtensionType } from '@blocksuite/affine/block-std';
import {
BookmarkBlockSpec,
@@ -25,7 +26,6 @@ import {
RefNodeSlotsExtension,
RichTextExtensions,
} from '@blocksuite/affine/blocks';
import { AIChatBlockSpec } from '@blocksuite/affine/presets';
import { CustomAttachmentBlockSpec } from './custom/attachment-block';

View File

@@ -11,24 +11,16 @@ import type {
DatabaseBlockModel,
MenuOptions,
} from '@blocksuite/affine/blocks';
import { menu } from '@blocksuite/affine-components/context-menu';
import { LinkIcon } from '@blocksuite/icons/lit';
import type { FrameworkProvider } from '@toeverything/infra';
import type { TemplateResult } from 'lit';
export function createDatabaseOptionsConfig(framework: FrameworkProvider) {
return {
configure: (model: DatabaseBlockModel, options: MenuOptions) => {
const items = options.items;
const copyIndex = items.findIndex(
item => item.type === 'action' && item.name === 'Copy'
);
items.splice(
copyIndex + 1,
0,
createCopyLinkToBlockMenuItem(framework, model)
);
items.splice(2, 0, createCopyLinkToBlockMenuItem(framework, model));
return options;
},
@@ -38,17 +30,10 @@ export function createDatabaseOptionsConfig(framework: FrameworkProvider) {
function createCopyLinkToBlockMenuItem(
framework: FrameworkProvider,
model: DatabaseBlockModel
): {
type: 'action';
name: string;
icon?: TemplateResult<1>;
hide?: () => boolean;
select: () => void;
} {
return {
type: 'action',
) {
return menu.action({
name: 'Copy link to block',
icon: LinkIcon({ width: '20', height: '20' }),
prefix: LinkIcon({ width: '20', height: '20' }),
hide: () => {
const { editor } = framework.get(EditorService);
const mode = editor.mode$.value;
@@ -91,5 +76,5 @@ function createCopyLinkToBlockMenuItem(
})
.catch(console.error);
},
};
});
}

View File

@@ -50,9 +50,9 @@ import {
QuickSearchExtension,
ReferenceNodeConfigExtension,
} from '@blocksuite/affine/blocks';
import { AIChatBlockSchema } from '@blocksuite/affine/presets';
import { type BlockSnapshot, Text } from '@blocksuite/affine/store';
import {
AIChatBlockSchema,
type DocProps,
type DocService,
DocsService,

View File

@@ -1,6 +1,6 @@
import { AIChatBlockSpec } from '@affine/core/blocksuite/presets/blocks/ai-chat-block';
import type { ExtensionType } from '@blocksuite/affine/block-std';
import { SpecProvider } from '@blocksuite/affine/blocks';
import { AIChatBlockSpec } from '@blocksuite/affine/presets';
import { getFontConfigExtension } from './font-extension';

View File

@@ -1,3 +1,5 @@
import type { AffineTextAttributes } from '@blocksuite/affine/blocks';
import type { DeltaInsert } from '@blocksuite/affine/inline';
import type { DocCollection } from '@blocksuite/affine/store';
import { useCallback } from 'react';
@@ -8,7 +10,7 @@ export function useReferenceLinkHelper(docCollection: DocCollection) {
if (!page) {
return;
}
const text = page.Text.fromDelta([
const text = new page.Text([
{
insert: ' ',
attributes: {
@@ -18,7 +20,7 @@ export function useReferenceLinkHelper(docCollection: DocCollection) {
},
},
},
]);
] as DeltaInsert<AffineTextAttributes>[]);
const [frame] = page.getBlockByFlavour('affine:note');
frame && page.addBlock('affine:paragraph', { text }, frame.id);

View File

@@ -8,8 +8,8 @@ import type {
SurfaceRefBlockModel,
} from '@blocksuite/affine/blocks';
import { AffineReference } from '@blocksuite/affine/blocks';
import type { AIChatBlockModel } from '@blocksuite/affine/presets';
import type { BlockModel } from '@blocksuite/affine/store';
import type { AIChatBlockModel } from '@toeverything/infra';
import { Entity, LiveData } from '@toeverything/infra';
import type { TemplateResult } from 'lit';
import { firstValueFrom, map, race } from 'rxjs';