mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
feat(core): chat-panel buttons ui (#11942)
Close [AI-7](https://linear.app/affine-design/issue/AI-7). 
This commit is contained in:
@@ -5,7 +5,6 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
|||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
import type { ExtensionType, Store } from '@blocksuite/affine/store';
|
import type { ExtensionType, Store } from '@blocksuite/affine/store';
|
||||||
import { HelpIcon } from '@blocksuite/icons/lit';
|
|
||||||
import { type Signal, signal } from '@preact/signals-core';
|
import { type Signal, signal } from '@preact/signals-core';
|
||||||
import { css, html, type PropertyValues } from 'lit';
|
import { css, html, type PropertyValues } from 'lit';
|
||||||
import { property, state } from 'lit/decorators.js';
|
import { property, state } from 'lit/decorators.js';
|
||||||
@@ -68,21 +67,12 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
div:first-child {
|
.chat-panel-title-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--affine-text-secondary-color);
|
color: var(--affine-text-secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
div:last-child {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
@@ -385,20 +375,20 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
|
|
||||||
return html`<div class="chat-panel-container" style=${style}>
|
return html`<div class="chat-panel-container" style=${style}>
|
||||||
<div class="chat-panel-title">
|
<div class="chat-panel-title">
|
||||||
<div>
|
<div class="chat-panel-title-text">
|
||||||
${isEmbedding
|
${isEmbedding
|
||||||
? html`<span data-testid="chat-panel-embedding-progress"
|
? html`<span data-testid="chat-panel-embedding-progress"
|
||||||
>Embedding ${done}/${total}</span
|
>Embedding ${done}/${total}</span
|
||||||
>`
|
>`
|
||||||
: 'AFFiNE AI'}
|
: 'AFFiNE AI'}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<ai-history-clear
|
||||||
@click=${() => {
|
.host=${this.host}
|
||||||
AIProvider.toggleGeneralAIOnboarding?.(true);
|
.doc=${this.doc}
|
||||||
}}
|
.getSessionId=${this._getSessionId}
|
||||||
>
|
.onHistoryCleared=${this._updateHistory}
|
||||||
${HelpIcon()}
|
.chatContextValue=${this.chatContextValue}
|
||||||
</div>
|
></ai-history-clear>
|
||||||
</div>
|
</div>
|
||||||
<chat-panel-messages
|
<chat-panel-messages
|
||||||
${ref(this._chatMessagesRef)}
|
${ref(this._chatMessagesRef)}
|
||||||
@@ -418,7 +408,6 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
.updateContext=${this.updateContext}
|
.updateContext=${this.updateContext}
|
||||||
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
||||||
.onHistoryCleared=${this._updateHistory}
|
|
||||||
.isVisible=${this._isSidebarOpen}
|
.isVisible=${this._isSidebarOpen}
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
.reasoningConfig=${this.reasoningConfig}
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
@@ -428,6 +417,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
where: 'chat-panel',
|
where: 'chat-panel',
|
||||||
control: 'chat-send',
|
control: 'chat-send',
|
||||||
}}
|
}}
|
||||||
|
.sideBarWidth=${this._sidebarWidth}
|
||||||
></ai-chat-composer>
|
></ai-chat-composer>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type {
|
|||||||
CopilotDocType,
|
CopilotDocType,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { NotificationProvider } from '@blocksuite/affine/shared/services';
|
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
import type { Store } from '@blocksuite/affine/store';
|
import type { Store } from '@blocksuite/affine/store';
|
||||||
@@ -65,9 +64,6 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor onHistoryCleared: (() => void) | undefined;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor isVisible: Signal<boolean | undefined> = signal(false);
|
accessor isVisible: Signal<boolean | undefined> = signal(false);
|
||||||
|
|
||||||
@@ -97,6 +93,9 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor portalContainer: HTMLElement | null = null;
|
accessor portalContainer: HTMLElement | null = null;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor sideBarWidth: Signal<number | undefined> = signal(undefined);
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor chips: ChatChip[] = [];
|
accessor chips: ChatChip[] = [];
|
||||||
|
|
||||||
@@ -131,9 +130,9 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
.networkSearchConfig=${this.networkSearchConfig}
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
.reasoningConfig=${this.reasoningConfig}
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.cleanupHistories=${this._cleanupHistories}
|
|
||||||
.onChatSuccess=${this.onChatSuccess}
|
.onChatSuccess=${this.onChatSuccess}
|
||||||
.trackOptions=${this.trackOptions}
|
.trackOptions=${this.trackOptions}
|
||||||
|
.sideBarWidth=${this.sideBarWidth}
|
||||||
></ai-chat-input>
|
></ai-chat-input>
|
||||||
<div class="chat-panel-footer">
|
<div class="chat-panel-footer">
|
||||||
${InformationIcon()}
|
${InformationIcon()}
|
||||||
@@ -354,36 +353,6 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
this._pollAbortController = null;
|
this._pollAbortController = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _cleanupHistories = async () => {
|
|
||||||
const sessionId = await this.getSessionId();
|
|
||||||
const notification = this.host.std.getOptional(NotificationProvider);
|
|
||||||
if (!notification) return;
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
await notification.confirm({
|
|
||||||
title: 'Clear History',
|
|
||||||
message:
|
|
||||||
'Are you sure you want to clear all history? This action will permanently delete all content, including all chat logs and data, and cannot be undone.',
|
|
||||||
confirmText: 'Confirm',
|
|
||||||
cancelText: 'Cancel',
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
const actionIds = this.chatContextValue.messages
|
|
||||||
.filter(item => 'sessionId' in item)
|
|
||||||
.map(item => item.sessionId);
|
|
||||||
await AIProvider.histories?.cleanup(
|
|
||||||
this.doc.workspace.id,
|
|
||||||
this.doc.id,
|
|
||||||
[...(sessionId ? [sessionId] : []), ...(actionIds || [])]
|
|
||||||
);
|
|
||||||
notification.toast('History cleared');
|
|
||||||
this.onHistoryCleared?.();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
notification.toast('Failed to clear history');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _initComposer = async () => {
|
private readonly _initComposer = async () => {
|
||||||
if (!this.isVisible.value) return;
|
if (!this.isVisible.value) return;
|
||||||
if (this._isLoading) return;
|
if (this._isLoading) return;
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
|||||||
import { openFileOrFiles } from '@blocksuite/affine/shared/utils';
|
import { openFileOrFiles } from '@blocksuite/affine/shared/utils';
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
import {
|
import {
|
||||||
BroomIcon,
|
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
PublishIcon,
|
PublishIcon,
|
||||||
ThinkingIcon,
|
ThinkingIcon,
|
||||||
} from '@blocksuite/icons/lit';
|
} from '@blocksuite/icons/lit';
|
||||||
|
import { type Signal, signal } from '@preact/signals-core';
|
||||||
import { css, html, LitElement, nothing } from 'lit';
|
import { css, html, LitElement, nothing } from 'lit';
|
||||||
import { property, query, state } from 'lit/decorators.js';
|
import { property, query, state } from 'lit/decorators.js';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
@@ -118,46 +118,55 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
div {
|
.chat-input-icon {
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
padding: 2px;
|
||||||
|
|
||||||
div:nth-child(2) {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-upload,
|
|
||||||
.chat-history-clear,
|
|
||||||
.chat-network-search {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
color: ${unsafeCSSVarV2('icon/primary')};
|
color: ${unsafeCSSVarV2('icon/primary')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-input-icon-label {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${unsafeCSSVarV2('icon/primary')};
|
||||||
|
margin: 0 4px 0 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-history-clear svg {
|
.chat-input-icon:nth-child(2) {
|
||||||
color: var(--affine-text-secondary-color);
|
margin-left: auto;
|
||||||
}
|
|
||||||
.chat-network-search[data-active='true'] svg {
|
|
||||||
color: ${unsafeCSSVarV2('icon/activated')};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-history-clear[aria-disabled='true'],
|
.chat-input-icon:hover {
|
||||||
.image-upload[aria-disabled='true'],
|
background-color: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||||
.chat-network-search[aria-disabled='true'] {
|
}
|
||||||
|
|
||||||
|
.chat-input-icon[data-active='true'] {
|
||||||
|
background-color: #1e96eb14;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: ${unsafeCSSVarV2('icon/activated')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-icon-label {
|
||||||
|
color: ${unsafeCSSVarV2('icon/activated')};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-icon[aria-disabled='true'] {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
|
||||||
|
|
||||||
.chat-history-clear[aria-disabled='true'] svg,
|
svg {
|
||||||
.image-upload[aria-disabled='true'] svg,
|
color: ${unsafeCSSVarV2('icon/secondary')} !important;
|
||||||
.chat-network-search[aria-disabled='true'] svg {
|
}
|
||||||
color: var(--affine-text-disable-color) !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,9 +249,6 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor cleanupHistories!: () => Promise<void>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||||
|
|
||||||
@@ -264,6 +270,9 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
@property({ attribute: 'data-testid', reflect: true })
|
@property({ attribute: 'data-testid', reflect: true })
|
||||||
accessor testId = 'chat-panel-input-container';
|
accessor testId = 'chat-panel-input-container';
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor sideBarWidth: Signal<number | undefined> = signal(undefined);
|
||||||
|
|
||||||
private get _isNetworkActive() {
|
private get _isNetworkActive() {
|
||||||
return (
|
return (
|
||||||
!!this.networkSearchConfig.visible.value &&
|
!!this.networkSearchConfig.visible.value &&
|
||||||
@@ -275,12 +284,8 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
return !!this.reasoningConfig.enabled.value;
|
return !!this.reasoningConfig.enabled.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _isClearDisabled() {
|
private get _isImageUploadDisabled() {
|
||||||
return (
|
return this.chatContextValue.images.length >= MaximumImageCount;
|
||||||
this.chatContextValue.status === 'loading' ||
|
|
||||||
this.chatContextValue.status === 'transmitting' ||
|
|
||||||
!this.chatContextValue.messages.length
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
@@ -303,6 +308,8 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
const { images, status } = this.chatContextValue;
|
const { images, status } = this.chatContextValue;
|
||||||
const hasImages = images.length > 0;
|
const hasImages = images.length > 0;
|
||||||
const maxHeight = hasImages ? 272 + 2 : 200 + 2;
|
const maxHeight = hasImages ? 272 + 2 : 200 + 2;
|
||||||
|
const showLabel = this.sideBarWidth.value && this.sideBarWidth.value > 400;
|
||||||
|
|
||||||
return html` <div
|
return html` <div
|
||||||
class="chat-panel-input"
|
class="chat-panel-input"
|
||||||
data-if-focused=${this.focused}
|
data-if-focused=${this.focused}
|
||||||
@@ -355,44 +362,48 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
></textarea>
|
></textarea>
|
||||||
<div class="chat-panel-input-actions">
|
<div class="chat-panel-input-actions">
|
||||||
<div
|
<div
|
||||||
class="chat-history-clear"
|
class="chat-input-icon"
|
||||||
aria-disabled=${this._isClearDisabled}
|
data-testid="chat-panel-input-image-upload"
|
||||||
@click=${this._handleClear}
|
aria-disabled=${this._isImageUploadDisabled}
|
||||||
data-testid="chat-panel-clear"
|
@click=${this._uploadImageFiles}
|
||||||
>
|
>
|
||||||
${BroomIcon()}
|
${ImageIcon()}
|
||||||
|
<affine-tooltip>Upload</affine-tooltip>
|
||||||
</div>
|
</div>
|
||||||
${this.networkSearchConfig.visible.value
|
${this.networkSearchConfig.visible.value
|
||||||
? html`
|
? html`
|
||||||
<div
|
<div
|
||||||
class="chat-network-search"
|
class="chat-input-icon"
|
||||||
data-testid="chat-network-search"
|
data-testid="chat-network-search"
|
||||||
data-active=${this._isNetworkActive}
|
data-active=${this._isNetworkActive}
|
||||||
@click=${this._toggleNetworkSearch}
|
@click=${this._toggleNetworkSearch}
|
||||||
@pointerdown=${stopPropagation}
|
@pointerdown=${stopPropagation}
|
||||||
>
|
>
|
||||||
${PublishIcon()}
|
${PublishIcon()}
|
||||||
|
${!showLabel
|
||||||
|
? html`<affine-tooltip>Search</affine-tooltip>`
|
||||||
|
: nothing}
|
||||||
|
${showLabel
|
||||||
|
? html`<span class="chat-input-icon-label">Search</span>`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<div
|
<div
|
||||||
class="chat-network-search"
|
class="chat-input-icon"
|
||||||
data-testid="chat-reasoning"
|
data-testid="chat-reasoning"
|
||||||
data-active=${this._isReasoningActive}
|
data-active=${this._isReasoningActive}
|
||||||
@click=${this._toggleReasoning}
|
@click=${this._toggleReasoning}
|
||||||
@pointerdown=${stopPropagation}
|
@pointerdown=${stopPropagation}
|
||||||
>
|
>
|
||||||
${ThinkingIcon()}
|
${ThinkingIcon()}
|
||||||
|
${!showLabel
|
||||||
|
? html`<affine-tooltip>Reason</affine-tooltip>`
|
||||||
|
: nothing}
|
||||||
|
${showLabel
|
||||||
|
? html`<span class="chat-input-icon-label">Reason</span>`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${images.length < MaximumImageCount
|
|
||||||
? html`<div
|
|
||||||
data-testid="chat-panel-input-image-upload"
|
|
||||||
class="image-upload"
|
|
||||||
@click=${this._uploadImageFiles}
|
|
||||||
>
|
|
||||||
${ImageIcon()}
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
${status === 'transmitting'
|
${status === 'transmitting'
|
||||||
? html`<div @click=${this._handleAbort} data-testid="chat-panel-stop">
|
? html`<div @click=${this._handleAbort} data-testid="chat-panel-stop">
|
||||||
${ChatAbortIcon}
|
${ChatAbortIcon}
|
||||||
@@ -456,13 +467,6 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
reportResponse('aborted:stop');
|
reportResponse('aborted:stop');
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _handleClear = async () => {
|
|
||||||
if (this._isClearDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.cleanupHistories();
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _toggleNetworkSearch = (e: MouseEvent) => {
|
private readonly _toggleNetworkSearch = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -493,6 +497,8 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly _uploadImageFiles = async (_e: MouseEvent) => {
|
private readonly _uploadImageFiles = async (_e: MouseEvent) => {
|
||||||
|
if (this._isImageUploadDisabled) return;
|
||||||
|
|
||||||
const images = await openFileOrFiles({
|
const images = await openFileOrFiles({
|
||||||
acceptType: 'Images',
|
acceptType: 'Images',
|
||||||
multiple: true,
|
multiple: true,
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
|
import { NotificationProvider } from '@blocksuite/affine/shared/services';
|
||||||
|
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||||
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
|
import type { Store } from '@blocksuite/affine/store';
|
||||||
|
import { css, html } from 'lit';
|
||||||
|
import { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
import type { ChatContextValue } from '../../chat-panel/chat-context';
|
||||||
|
import { AIProvider } from '../../provider';
|
||||||
|
|
||||||
|
export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor chatContextValue!: ChatContextValue;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor getSessionId!: () => Promise<string | undefined>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor doc!: Store;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor onHistoryCleared!: () => void;
|
||||||
|
|
||||||
|
static override styles = css`
|
||||||
|
.chat-history-clear {
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${unsafeCSSVarV2('icon/primary')};
|
||||||
|
}
|
||||||
|
.chat-history-clear[aria-disabled='true'] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: ${unsafeCSSVarV2('icon/secondary')};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
private get _isHistoryClearDisabled() {
|
||||||
|
return (
|
||||||
|
this.chatContextValue.status === 'loading' ||
|
||||||
|
this.chatContextValue.status === 'transmitting' ||
|
||||||
|
!this.chatContextValue.messages.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _cleanupHistories = async () => {
|
||||||
|
if (this._isHistoryClearDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sessionId = await this.getSessionId();
|
||||||
|
const notification = this.host.std.getOptional(NotificationProvider);
|
||||||
|
if (!notification) return;
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
await notification.confirm({
|
||||||
|
title: 'Clear History',
|
||||||
|
message:
|
||||||
|
'Are you sure you want to clear all history? This action will permanently delete all content, including all chat logs and data, and cannot be undone.',
|
||||||
|
confirmText: 'Confirm',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
const actionIds = this.chatContextValue.messages
|
||||||
|
.filter(item => 'sessionId' in item)
|
||||||
|
.map(item => item.sessionId);
|
||||||
|
await AIProvider.histories?.cleanup(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
this.doc.id,
|
||||||
|
[...(sessionId ? [sessionId] : []), ...(actionIds || [])]
|
||||||
|
);
|
||||||
|
notification.toast('History cleared');
|
||||||
|
this.onHistoryCleared?.();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
notification.toast('Failed to clear history');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="chat-history-clear"
|
||||||
|
aria-disabled=${this._isHistoryClearDisabled}
|
||||||
|
@click=${this._cleanupHistories}
|
||||||
|
data-testid="chat-panel-clear"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './ai-history-clear';
|
||||||
@@ -37,7 +37,8 @@ import { ChatPanelDocChip } from './components/ai-chat-chips/doc-chip';
|
|||||||
import { ChatPanelFileChip } from './components/ai-chat-chips/file-chip';
|
import { ChatPanelFileChip } from './components/ai-chat-chips/file-chip';
|
||||||
import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
|
import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
|
||||||
import { AIChatComposer } from './components/ai-chat-composer';
|
import { AIChatComposer } from './components/ai-chat-composer';
|
||||||
import { AIChatInput } from './components/ai-chat-input/ai-chat-input';
|
import { AIChatInput } from './components/ai-chat-input';
|
||||||
|
import { AIHistoryClear } from './components/ai-history-clear';
|
||||||
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';
|
||||||
@@ -98,6 +99,7 @@ export function registerAIEffects() {
|
|||||||
customElements.define('ai-chat-input', AIChatInput);
|
customElements.define('ai-chat-input', AIChatInput);
|
||||||
customElements.define('ai-chat-composer', AIChatComposer);
|
customElements.define('ai-chat-composer', AIChatComposer);
|
||||||
customElements.define('chat-panel-chips', ChatPanelChips);
|
customElements.define('chat-panel-chips', ChatPanelChips);
|
||||||
|
customElements.define('ai-history-clear', AIHistoryClear);
|
||||||
customElements.define('chat-panel-add-popover', ChatPanelAddPopover);
|
customElements.define('chat-panel-add-popover', ChatPanelAddPopover);
|
||||||
customElements.define(
|
customElements.define(
|
||||||
'chat-panel-candidates-popover',
|
'chat-panel-candidates-popover',
|
||||||
|
|||||||
@@ -502,6 +502,15 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
<div class="new-chat-messages-container">
|
<div class="new-chat-messages-container">
|
||||||
${this.CurrentMessages(currentChatMessages)}
|
${this.CurrentMessages(currentChatMessages)}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="history-clear-container">
|
||||||
|
<ai-history-clear
|
||||||
|
.host=${this.host}
|
||||||
|
.doc=${this.host.doc}
|
||||||
|
.getSessionId=${this._getSessionId}
|
||||||
|
.onHistoryCleared=${this._onHistoryCleared}
|
||||||
|
.chatContextValue=${chatContext}
|
||||||
|
></ai-history-clear>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ai-chat-composer
|
<ai-chat-composer
|
||||||
.host=${host}
|
.host=${host}
|
||||||
@@ -510,7 +519,6 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
.createSessionId=${this._createSessionId}
|
.createSessionId=${this._createSessionId}
|
||||||
.chatContextValue=${chatContext}
|
.chatContextValue=${chatContext}
|
||||||
.updateContext=${updateContext}
|
.updateContext=${updateContext}
|
||||||
.onHistoryCleared=${this._onHistoryCleared}
|
|
||||||
.isVisible=${this.isComposerVisible}
|
.isVisible=${this.isComposerVisible}
|
||||||
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
||||||
.networkSearchConfig=${networkSearchConfig}
|
.networkSearchConfig=${networkSearchConfig}
|
||||||
|
|||||||
@@ -57,4 +57,11 @@ export const PeekViewStyles = css`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-clear-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
30
tests/affine-cloud-copilot/e2e/chat-with/reasoning.spec.ts
Normal file
30
tests/affine-cloud-copilot/e2e/chat-with/reasoning.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { test } from '../base/base-test';
|
||||||
|
|
||||||
|
test.describe('AIChatWith/Reasoning', () => {
|
||||||
|
test.beforeEach(async ({ loggedInPage: page, utils }) => {
|
||||||
|
await utils.testUtils.setupTestEnvironment(page);
|
||||||
|
await utils.chatPanel.openChatPanel(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should support chat with reasoning', async ({
|
||||||
|
loggedInPage: page,
|
||||||
|
utils,
|
||||||
|
}) => {
|
||||||
|
await utils.chatPanel.enableReasoning(page);
|
||||||
|
await utils.chatPanel.makeChat(
|
||||||
|
page,
|
||||||
|
'How do you measure exactly 4 liters of water using a jug that only holds 3 and 5 liters?'
|
||||||
|
);
|
||||||
|
await utils.chatPanel.waitForHistory(page, [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content:
|
||||||
|
'How do you measure exactly 4 liters of water using a jug that only holds 3 and 5 liters?',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -305,6 +305,20 @@ export class ChatPanelUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async enableReasoning(page: Page) {
|
||||||
|
const reasoning = page.getByTestId('chat-reasoning');
|
||||||
|
if ((await reasoning.getAttribute('data-active')) === 'false') {
|
||||||
|
await reasoning.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async disableReasoning(page: Page) {
|
||||||
|
const reasoning = page.getByTestId('chat-reasoning');
|
||||||
|
if ((await reasoning.getAttribute('data-active')) === 'true') {
|
||||||
|
await reasoning.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static async isNetworkSearchEnabled(page: Page) {
|
public static async isNetworkSearchEnabled(page: Page) {
|
||||||
const networkSearch = await page.getByTestId('chat-network-search');
|
const networkSearch = await page.getByTestId('chat-network-search');
|
||||||
return (await networkSearch.getAttribute('aria-disabled')) === 'false';
|
return (await networkSearch.getAttribute('aria-disabled')) === 'false';
|
||||||
|
|||||||
Reference in New Issue
Block a user