diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts index 039afb126d..0d6b390513 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts @@ -138,6 +138,8 @@ export class ChatContentStreamObjects extends WithDisposable( .std=${this.host?.std} .data=${streamObject} .width=${this.width} + .theme=${this.theme} + .notificationService=${this.notificationService} > `; case 'doc_edit': diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/artifact-tool.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/artifact-tool.ts index f0af1f4eef..31f895be97 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/artifact-tool.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/artifact-tool.ts @@ -1,7 +1,8 @@ import { LoadingIcon } from '@blocksuite/affine/components/icons'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; -import type { ImageProxyService } from '@blocksuite/affine/shared/adapters'; -import { type BlockStdScope, ShadowlessElement } from '@blocksuite/affine/std'; +import type { ColorScheme } from '@blocksuite/affine/model'; +import { ShadowlessElement } from '@blocksuite/affine/std'; +import { type NotificationService } from '@blocksuite/affine-shared/services'; import type { Signal } from '@preact/signals-core'; import { css, @@ -33,7 +34,7 @@ export abstract class ArtifactTool< } .artifact-tool-card:hover { - background-color: var(--affine-hover-color); + opacity: 0.8; } `; @@ -45,10 +46,10 @@ export abstract class ArtifactTool< accessor width: Signal | undefined; @property({ attribute: false }) - accessor imageProxyService: ImageProxyService | null | undefined; + accessor notificationService!: NotificationService; @property({ attribute: false }) - accessor std: BlockStdScope | undefined; + accessor theme!: Signal; /* -------------------------- Card meta hooks -------------------------- */ @@ -66,12 +67,9 @@ export abstract class ArtifactTool< }; /** Banner shown on the right side of the card (can be undefined). */ - protected abstract getBanner(): - | TemplateResult - | HTMLElement - | string - | null - | undefined; + protected abstract getBanner( + theme: ColorScheme + ): TemplateResult | HTMLElement | string | null | undefined; /** * Provide the main TemplateResult shown in the preview panel. @@ -117,7 +115,7 @@ export abstract class ArtifactTool< }) : icon; - const banner = this.getBanner(); + const banner = this.getBanner(this.theme.value); return html`
+ + + + + + + + + + + + + + + + + + + + + +`; + +const CodeBlockBannerDark = html` + + + + + + + + + + + + + + + + + + + + + + `; + /** * Component to render code artifact tool call/result inside chat. */ -export class CodeArtifactTool extends WithDisposable(ShadowlessElement) { +export class CodeArtifactTool extends ArtifactTool< + CodeArtifactToolCall | CodeArtifactToolResult +> { static override styles = css` - .code-artifact-result { - cursor: pointer; - margin: 8px 0; - } - - .code-artifact-result:hover { - background-color: var(--affine-hover-color); - } - .code-artifact-preview { padding: 0; width: 100%; @@ -299,165 +427,148 @@ export class CodeArtifactTool extends WithDisposable(ShadowlessElement) { } `; - @property({ attribute: false }) - accessor data!: CodeArtifactToolCall | CodeArtifactToolResult; - - @property({ attribute: false }) - accessor width: Signal | undefined; - @property({ attribute: false }) accessor std: BlockStdScope | undefined; @state() private accessor mode: 'preview' | 'code' = 'code'; - private renderToolCall() { - const { args } = this.data as CodeArtifactToolCall; - const name = `Generating HTML artifact "${args.title}"`; - return html``; + /* ---------------- ArtifactTool hooks ---------------- */ + + protected getBanner(theme: ColorScheme) { + return theme === ColorScheme.Dark ? CodeBlockBannerDark : CodeBlockBanner; } - private renderToolResult() { - if (!this.std) return nothing; - if (this.data.type !== 'tool-result') return nothing; - const resultData = this.data as CodeArtifactToolResult; - const result = resultData.result; + protected getCardMeta() { + const loading = this.data.type === 'tool-call'; + return { + title: this.data.args.title, + icon: CodeBlockIcon({ width: '20', height: '20' }), + loading, + className: 'code-artifact-result', + }; + } - if (result && typeof result === 'object' && 'title' in result) { - const { title, html: htmlContent } = result as { - title: string; - html: string; - }; - - const onClick = () => { - const copyHTML = async () => { - if (this.std) { - await navigator.clipboard - .writeText(htmlContent) - .catch(console.error); - toast(this.std.host, 'Copied HTML to clipboard'); - } - }; - - const downloadHTML = () => { - try { - const blob = new Blob([htmlContent], { type: 'text/html' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${title || 'artifact'}.html`; - document.body.append(a); - a.click(); - a.remove(); - URL.revokeObjectURL(url); - } catch (e) { - console.error(e); - } - }; - - const setCodeMode = () => { - if (this.mode !== 'code') { - this.mode = 'code'; - renderPreview(); - } - }; - - const setPreviewMode = () => { - if (this.mode !== 'preview') { - this.mode = 'preview'; - renderPreview(); - } - }; - - const renderPreview = () => { - const controls = html` -
-
- Code -
-
- Preview -
-
-
- - - ${CopyIcon({ width: '20', height: '20' })} - - `; - renderPreviewPanel( - this, - html`
- ${this.mode === 'preview' - ? html`` - : html``} -
`, - controls - ); - }; - - renderPreview(); - }; - - return html` + protected override getPreviewContent() { + if (this.data.type !== 'tool-result' || !this.data.result) { + // loading state + return html`
-
-
-
- ${PageIcon({ width: '20', height: '20' })} -
-
- ${title} -
-
-
+ ${CodeBlockIcon({ width: '24', height: '24' })}
- `; +
`; } - return html``; + const result = this.data.result; + if (typeof result !== 'object' || !('html' in result)) return html``; + + const { html: htmlContent } = result as { html: string }; + + return html`
+ ${this.mode === 'preview' + ? html`` + : html``} +
`; } - protected override render() { - if (this.data.type === 'tool-call') { - return this.renderToolCall(); + protected override getPreviewControls() { + if (this.data.type !== 'tool-result' || !this.std || !this.data.result) { + return undefined; } - if (this.data.type === 'tool-result') { - return this.renderToolResult(); + + const result = this.data.result as { html: string; title: string }; + const htmlContent = result.html; + const title = result.title; + + const copyHTML = async () => { + await navigator.clipboard.writeText(htmlContent).catch(console.error); + this.notificationService.toast('Copied HTML to clipboard'); + }; + + const downloadHTML = () => { + try { + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${title || 'artifact'}.html`; + document.body.append(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } catch (e) { + console.error(e); + } + }; + + const setCodeMode = () => { + if (this.mode !== 'code') { + this.mode = 'code'; + this.refreshPreviewPanel(); + } + }; + + const setPreviewMode = () => { + if (this.mode !== 'preview') { + this.mode = 'preview'; + this.refreshPreviewPanel(); + } + }; + + return html` +
+
+ Code +
+
+ Preview +
+
+
+ + + ${CopyIcon({ width: '20', height: '20' })} + + `; + } + + protected override getErrorTemplate() { + if ( + this.data.type === 'tool-result' && + this.data.result && + (this.data.result as any).type === 'error' + ) { + return html``; } - return nothing; + return null; } } diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-compose.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-compose.ts index 1bee79756c..79c304f733 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-compose.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-compose.ts @@ -2,24 +2,18 @@ import { getStoreManager } from '@affine/core/blocksuite/manager/store'; import { getAFFiNEWorkspaceSchema } from '@affine/core/modules/workspace'; import { getEmbedLinkedDocIcons } from '@blocksuite/affine/blocks/embed-doc'; import { LoadingIcon } from '@blocksuite/affine/components/icons'; -import { toast } from '@blocksuite/affine/components/toast'; -import { WithDisposable } from '@blocksuite/affine/global/lit'; import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference'; import type { ColorScheme } from '@blocksuite/affine/model'; +import { NotificationProvider } from '@blocksuite/affine/shared/services'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; -import { type BlockStdScope, ShadowlessElement } from '@blocksuite/affine/std'; import { MarkdownTransformer } from '@blocksuite/affine/widgets/linked-doc'; -import type { NotificationService } from '@blocksuite/affine-shared/services'; import { CopyIcon, PageIcon, ToolIcon } from '@blocksuite/icons/lit'; -import { type Signal } from '@preact/signals-core'; -import { css, html, nothing, type PropertyValues } from 'lit'; +import type { BlockStdScope } from '@blocksuite/std'; +import { css, html } from 'lit'; import { property } from 'lit/decorators.js'; import { getCustomPageEditorBlockSpecs } from '../text-renderer'; -import { - isPreviewPanelOpen, - renderPreviewPanel, -} from './artifacts-preview-panel'; +import { ArtifactTool } from './artifact-tool'; import type { ToolError } from './type'; interface DocComposeToolCall { @@ -47,17 +41,10 @@ interface DocComposeToolResult { /** * Component to render doc compose tool call/result inside chat. */ -export class DocComposeTool extends WithDisposable(ShadowlessElement) { +export class DocComposeTool extends ArtifactTool< + DocComposeToolCall | DocComposeToolResult +> { static override styles = css` - .doc-compose-result { - cursor: pointer; - margin: 8px 0; - } - - .doc-compose-result:hover { - background-color: var(--affine-hover-color); - } - .doc-compose-result-preview { padding: 24px; height: 100%; @@ -100,33 +87,60 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) { } `; - @property({ attribute: false }) - accessor data!: DocComposeToolCall | DocComposeToolResult; - - @property({ attribute: false }) - accessor width: Signal | undefined; - @property({ attribute: false }) accessor std: BlockStdScope | undefined; - @property({ attribute: false }) - accessor notificationService!: NotificationService; - - @property({ attribute: false }) - accessor theme!: Signal; - - override updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (changedProperties.has('data') && isPreviewPanelOpen(this)) { - this.updatePreviewPanel(); - } + protected getBanner(theme: ColorScheme) { + const { LinkedDocEmptyBanner } = getEmbedLinkedDocIcons( + theme, + 'page', + 'horizontal' + ); + return LinkedDocEmptyBanner; } - private updatePreviewPanel() { + protected getCardMeta() { + const composing = this.data.type === 'tool-call'; + return { + title: this.data.args.title, + icon: PageIcon(), + loading: composing, + className: 'doc-compose-result', + }; + } + + protected override getPreviewContent() { + if (!this.std) return html``; + const std = this.std; + const resultData = this.data; + const title = this.data.args.title; + const result = resultData.type === 'tool-result' ? resultData.result : null; + const successResult = result && 'markdown' in result ? result : null; + + return html`
+
${title}
+ ${successResult + ? html`` + : html`
+ ${LoadingIcon({ + size: '32px', + })} +
`} +
`; + } + + protected override getPreviewControls() { if (!this.std) return; const std = this.std; const resultData = this.data; - const composing = resultData.type === 'tool-call'; const title = this.data.args.title; const result = resultData.type === 'tool-result' ? resultData.result : null; const successResult = result && 'markdown' in result ? result : null; @@ -138,7 +152,7 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) { await navigator.clipboard .writeText(successResult.markdown) .catch(console.error); - toast(std.host, 'Copied markdown to clipboard'); + this.notificationService.toast('Copied markdown to clipboard'); }; const saveAsDoc = async () => { @@ -147,6 +161,7 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) { return; } const workspace = std.store.workspace; + const notificationService = std.get(NotificationProvider); const refNodeSlots = std.getOptional(RefNodeSlotsProvider); const docId = await MarkdownTransformer.importMarkdownToDoc({ collection: workspace, @@ -156,7 +171,7 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) { extensions: getStoreManager().config.init().value.get('store'), }); if (docId) { - const open = await this.notificationService.confirm({ + const open = await notificationService.confirm({ title: 'Open the doc you just created', message: 'Doc saved successfully! Would you like to open it now?', cancelText: 'Cancel', @@ -170,99 +185,43 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) { }); } } else { - toast(std.host, 'Failed to create document'); + this.notificationService.toast('Failed to create document'); } } catch (e) { console.error(e); - toast(std.host, 'Failed to create document'); + this.notificationService.toast('Failed to create document'); } }; - const controls = html` - - - ${CopyIcon({ width: '20', height: '20' })} - - `; - - renderPreviewPanel( - this, - html`
-
${title}
- ${successResult - ? html`` - : html`
- ${LoadingIcon({ - size: '32px', - })} -
`} -
`, - composing ? undefined : controls - ); + return this.data.type === 'tool-call' + ? undefined + : html` + + + ${CopyIcon({ width: '20', height: '20' })} + + `; } - protected override render() { - if (!this.std) return nothing; - const resultData = this.data; - const composing = resultData.type === 'tool-call'; - - const title = this.data.args.title; - + protected override getErrorTemplate() { if ( - resultData.type === 'tool-result' && - resultData.result && - 'type' in resultData.result && - resultData.result.type === 'error' + this.data.type === 'tool-result' && + this.data.result && + 'type' in this.data.result && + (this.data.result as any).type === 'error' ) { - // failed return html``; } - - const { LinkedDocEmptyBanner } = getEmbedLinkedDocIcons( - this.theme.value, - 'page', - 'horizontal' - ); - - return html` -
-
-
-
- ${composing - ? LoadingIcon({ - size: '20px', - }) - : PageIcon()} -
-
- ${title} -
-
-
-
- ${LinkedDocEmptyBanner} -
-
- `; + return null; } }