feat(core): remove chat-panel component's dependency on doc (#12975)
Close [AI-259](https://linear.app/affine-design/issue/AI-259) Close [AI-243](https://linear.app/affine-design/issue/AI-243) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a unified AI chat content component to manage and display chat interactions. * Added new chat block message components for improved chat message rendering. * **Refactor** * Simplified and unified session management across all AI chat components, now passing full session objects instead of session IDs. * Updated component and property names for clarity and consistency (e.g., chat message and block message components). * Consolidated chat history and actions retrieval for a more streamlined chat experience. * Removed redundant session ID getters and replaced them with direct session object usage. * Streamlined chat panel and composer components by removing internal message and context state management. * **Bug Fixes** * Improved handling of chat session state and loading, reducing redundant state properties. * Enhanced event handling to prevent errors when chat parameters are missing. * **Tests** * Removed outdated chat clearing test cases to align with new chat state management. * **Chores** * Updated import paths and reorganized module exports for better maintainability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
import { ChatHistoryOrder } from '@affine/graphql';
|
|
||||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine/blocks/surface';
|
import { EdgelessCRUDIdentifier } from '@blocksuite/affine/blocks/surface';
|
||||||
import {
|
import {
|
||||||
Bound,
|
Bound,
|
||||||
@@ -69,14 +68,15 @@ export type ChatAction = {
|
|||||||
|
|
||||||
export async function queryHistoryMessages(
|
export async function queryHistoryMessages(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
docId: string,
|
forkSessionId: string,
|
||||||
forkSessionId: string
|
docId?: string
|
||||||
) {
|
) {
|
||||||
// Get fork session messages
|
// Get fork session messages
|
||||||
const histories = await AIProvider.histories?.chats(workspaceId, docId, {
|
const histories = await AIProvider.histories?.chats(
|
||||||
sessionId: forkSessionId,
|
workspaceId,
|
||||||
messageOrder: ChatHistoryOrder.asc,
|
forkSessionId,
|
||||||
});
|
docId
|
||||||
|
);
|
||||||
|
|
||||||
if (!histories || !histories.length) {
|
if (!histories || !histories.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -117,8 +117,8 @@ export async function constructRootChatBlockMessages(
|
|||||||
const userInfo = await AIProvider.userInfo;
|
const userInfo = await AIProvider.userInfo;
|
||||||
const forkMessages = (await queryHistoryMessages(
|
const forkMessages = (await queryHistoryMessages(
|
||||||
doc.workspace.id,
|
doc.workspace.id,
|
||||||
doc.id,
|
forkSessionId,
|
||||||
forkSessionId
|
doc.id
|
||||||
)) as ChatMessage[];
|
)) as ChatMessage[];
|
||||||
return constructUserInfoWithMessages(forkMessages, userInfo);
|
return constructUserInfoWithMessages(forkMessages, userInfo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type {
|
import type {
|
||||||
ChatHistoryOrder,
|
|
||||||
ContextMatchedDocChunk,
|
ContextMatchedDocChunk,
|
||||||
ContextMatchedFileChunk,
|
ContextMatchedFileChunk,
|
||||||
ContextWorkspaceEmbeddingStatus,
|
ContextWorkspaceEmbeddingStatus,
|
||||||
@@ -400,15 +399,12 @@ declare global {
|
|||||||
// non chat histories
|
// non chat histories
|
||||||
actions: (
|
actions: (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
docId?: string
|
docId: string
|
||||||
) => Promise<AIHistory[] | undefined>;
|
) => Promise<AIHistory[] | undefined>;
|
||||||
chats: (
|
chats: (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
docId?: string,
|
sessionId: string,
|
||||||
options?: {
|
docId?: string
|
||||||
sessionId?: string;
|
|
||||||
messageOrder?: ChatHistoryOrder;
|
|
||||||
}
|
|
||||||
) => Promise<AIHistory[] | undefined>;
|
) => Promise<AIHistory[] | undefined>;
|
||||||
cleanup: (
|
cleanup: (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ export class AIChatBlockComponent extends BlockComponent<AIChatBlockModel> {
|
|||||||
|
|
||||||
return html`<div class="affine-ai-chat-block-container">
|
return html`<div class="affine-ai-chat-block-container">
|
||||||
<div class="ai-chat-messages-container">
|
<div class="ai-chat-messages-container">
|
||||||
<ai-chat-messages
|
<ai-chat-block-messages
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.messages=${messages}
|
.messages=${messages}
|
||||||
.textRendererOptions=${this._textRendererOptions}
|
.textRendererOptions=${this._textRendererOptions}
|
||||||
.withMask=${true}
|
.withMask=${true}
|
||||||
></ai-chat-messages>
|
></ai-chat-block-messages>
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-chat-block-button">
|
<div class="ai-chat-block-button">
|
||||||
${ChatWithAIIcon} <span>AI chat block</span>
|
${ChatWithAIIcon} <span>AI chat block</span>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from '../../../components/ai-chat-messages';
|
} from '../../../components/ai-chat-messages';
|
||||||
import { UserInfoTemplate } from './user-info';
|
import { UserInfoTemplate } from './user-info';
|
||||||
|
|
||||||
export class AIChatMessage extends LitElement {
|
export class AIChatBlockMessage extends LitElement {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
.ai-chat-message {
|
.ai-chat-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -99,7 +99,7 @@ export class AIChatMessage extends LitElement {
|
|||||||
accessor textRendererOptions: TextRendererOptions = {};
|
accessor textRendererOptions: TextRendererOptions = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AIChatMessages extends LitElement {
|
export class AIChatBlockMessages extends LitElement {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
:host {
|
:host {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -123,11 +123,11 @@ export class AIChatMessages extends LitElement {
|
|||||||
message => message.id || message.createdAt,
|
message => message.id || message.createdAt,
|
||||||
message => {
|
message => {
|
||||||
return html`
|
return html`
|
||||||
<ai-chat-message
|
<ai-chat-block-message
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.textRendererOptions=${this.textRendererOptions}
|
.textRendererOptions=${this.textRendererOptions}
|
||||||
.message=${message}
|
.message=${message}
|
||||||
></ai-chat-message>
|
></ai-chat-block-message>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
@@ -146,7 +146,7 @@ export class AIChatMessages extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'ai-chat-message': AIChatMessage;
|
'ai-chat-block-message': AIChatBlockMessage;
|
||||||
'ai-chat-messages': AIChatMessages;
|
'ai-chat-block-messages': AIChatBlockMessages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import './chat-panel-messages';
|
|
||||||
|
|
||||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
|
import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
|
||||||
@@ -12,9 +10,8 @@ import { CenterPeekIcon } from '@blocksuite/icons/lit';
|
|||||||
import { type Signal, signal } from '@preact/signals-core';
|
import { type Signal, signal } from '@preact/signals-core';
|
||||||
import { css, html, nothing, type PropertyValues } from 'lit';
|
import { css, html, nothing, type PropertyValues } from 'lit';
|
||||||
import { property, state } from 'lit/decorators.js';
|
import { property, state } from 'lit/decorators.js';
|
||||||
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
|
import { keyed } from 'lit/directives/keyed.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import { throttle } from 'lodash-es';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
DocDisplayConfig,
|
DocDisplayConfig,
|
||||||
@@ -25,31 +22,9 @@ import type {
|
|||||||
AIPlaygroundConfig,
|
AIPlaygroundConfig,
|
||||||
AIReasoningConfig,
|
AIReasoningConfig,
|
||||||
} from '../components/ai-chat-input';
|
} from '../components/ai-chat-input';
|
||||||
import {
|
|
||||||
type ChatAction,
|
|
||||||
type ChatMessage,
|
|
||||||
type HistoryMessage,
|
|
||||||
} from '../components/ai-chat-messages';
|
|
||||||
import { createPlaygroundModal } from '../components/playground/modal';
|
import { createPlaygroundModal } from '../components/playground/modal';
|
||||||
import { AIProvider } from '../provider';
|
import { AIProvider } from '../provider';
|
||||||
import { extractSelectedContent } from '../utils/extract';
|
|
||||||
import {
|
|
||||||
getSelectedImagesAsBlobs,
|
|
||||||
getSelectedTextContent,
|
|
||||||
} from '../utils/selection-utils';
|
|
||||||
import type { AppSidebarConfig } from './chat-config';
|
import type { AppSidebarConfig } from './chat-config';
|
||||||
import type { ChatContextValue } from './chat-context';
|
|
||||||
import type { ChatPanelMessages } from './chat-panel-messages';
|
|
||||||
|
|
||||||
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
|
|
||||||
quote: '',
|
|
||||||
images: [],
|
|
||||||
abortController: null,
|
|
||||||
messages: [],
|
|
||||||
status: 'idle',
|
|
||||||
error: null,
|
|
||||||
markdown: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ChatPanel extends SignalWatcher(
|
export class ChatPanel extends SignalWatcher(
|
||||||
WithDisposable(ShadowlessElement)
|
WithDisposable(ShadowlessElement)
|
||||||
@@ -58,61 +33,17 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
chat-panel {
|
chat-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
|
||||||
|
|
||||||
.chat-panel-container {
|
.chat-panel-container {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-panel-title {
|
|
||||||
background: var(--affine-background-primary-color);
|
|
||||||
position: relative;
|
|
||||||
padding: 8px 0px;
|
|
||||||
width: 100%;
|
|
||||||
height: 36px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.chat-panel-title-text {
|
.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
color: var(--affine-text-secondary-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chat-panel-messages {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-panel-hints {
|
|
||||||
margin: 0 4px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--affine-border-color);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-panel-hints :first-child {
|
|
||||||
color: var(--affine-text-primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-panel-hints :nth-child(2) {
|
|
||||||
color: var(--affine-text-secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-panel-playground {
|
.chat-panel-playground {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
@@ -126,110 +57,27 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
.chat-panel-playground:hover svg {
|
.chat-panel-playground:hover svg {
|
||||||
color: ${unsafeCSSVarV2('icon/activated')};
|
color: ${unsafeCSSVarV2('icon/activated')};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
private readonly _chatMessagesRef: Ref<ChatPanelMessages> =
|
|
||||||
createRef<ChatPanelMessages>();
|
|
||||||
|
|
||||||
// request counter to track the latest request
|
|
||||||
private _updateHistoryCounter = 0;
|
|
||||||
|
|
||||||
private _wheelTriggered = false;
|
|
||||||
|
|
||||||
private readonly _updateHistory = async () => {
|
|
||||||
const { doc } = this;
|
|
||||||
|
|
||||||
const currentRequest = ++this._updateHistoryCounter;
|
|
||||||
|
|
||||||
const [histories, actions] = await Promise.all([
|
|
||||||
AIProvider.histories?.chats(doc.workspace.id, doc.id),
|
|
||||||
AIProvider.histories?.actions(doc.workspace.id, doc.id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Check if this is still the latest request
|
|
||||||
if (currentRequest !== this._updateHistoryCounter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatActions = (actions || []) as ChatAction[];
|
|
||||||
const messages: HistoryMessage[] = chatActions;
|
|
||||||
|
|
||||||
const sessionId = await this._getSessionId();
|
|
||||||
const history = histories?.find(history => history.sessionId === sessionId);
|
|
||||||
if (history) {
|
|
||||||
const chatMessages = (history.messages || []) as ChatMessage[];
|
|
||||||
messages.push(...chatMessages);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chatContextValue = {
|
|
||||||
...this.chatContextValue,
|
|
||||||
messages: messages.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
this._scrollToEnd();
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _updateEmbeddingProgress = (
|
|
||||||
count: Record<ContextEmbedStatus, number>
|
|
||||||
) => {
|
|
||||||
const total = count.finished + count.processing + count.failed;
|
|
||||||
this.embeddingProgress = [count.finished, total];
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _getSessionId = async () => {
|
|
||||||
if (this.session) {
|
|
||||||
return this.session.id;
|
|
||||||
}
|
|
||||||
const sessions = (
|
|
||||||
(await AIProvider.session?.getSessions(
|
|
||||||
this.doc.workspace.id,
|
|
||||||
this.doc.id,
|
|
||||||
{ action: false }
|
|
||||||
)) || []
|
|
||||||
).filter(session => !session.parentSessionId);
|
|
||||||
this.session = sessions.at(-1);
|
|
||||||
return this.session?.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _createSessionId = async () => {
|
|
||||||
if (this.session) {
|
|
||||||
return this.session.id;
|
|
||||||
}
|
|
||||||
const sessionId = await AIProvider.session?.createSession({
|
|
||||||
docId: this.doc.id,
|
|
||||||
workspaceId: this.doc.workspace.id,
|
|
||||||
promptName: 'Chat With AFFiNE AI',
|
|
||||||
});
|
|
||||||
if (sessionId) {
|
|
||||||
this.session = await AIProvider.session?.getSession(
|
|
||||||
this.doc.workspace.id,
|
|
||||||
sessionId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return sessionId;
|
|
||||||
};
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host!: EditorHost;
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor doc!: Store;
|
accessor doc!: Store;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor reasoningConfig!: AIReasoningConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor playgroundConfig!: AIPlaygroundConfig;
|
accessor playgroundConfig!: AIPlaygroundConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor appSidebarConfig!: AppSidebarConfig;
|
accessor appSidebarConfig!: AppSidebarConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor reasoningConfig!: AIReasoningConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor searchMenuConfig!: SearchMenuConfig;
|
accessor searchMenuConfig!: SearchMenuConfig;
|
||||||
|
|
||||||
@@ -246,56 +94,74 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor isLoading = false;
|
accessor session: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor embeddingProgress: [number, number] = [0, 0];
|
accessor embeddingProgress: [number, number] = [0, 0];
|
||||||
|
|
||||||
@state()
|
private isSidebarOpen: Signal<boolean | undefined> = signal(false);
|
||||||
accessor session: CopilotSessionType | undefined = undefined;
|
|
||||||
|
|
||||||
private _isInitialized = false;
|
private sidebarWidth: Signal<number | undefined> = signal(undefined);
|
||||||
|
|
||||||
private _isSidebarOpen: Signal<boolean | undefined> = signal(false);
|
private readonly initSession = async () => {
|
||||||
|
if (this.session) {
|
||||||
private _sidebarWidth: Signal<number | undefined> = signal(undefined);
|
return this.session;
|
||||||
|
|
||||||
private readonly _scrollToEnd = () => {
|
|
||||||
if (!this._wheelTriggered) {
|
|
||||||
this._chatMessagesRef.value?.scrollToEnd();
|
|
||||||
}
|
}
|
||||||
|
const sessions = (
|
||||||
|
(await AIProvider.session?.getSessions(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
this.doc.id,
|
||||||
|
{ action: false }
|
||||||
|
)) || []
|
||||||
|
).filter(session => !session.parentSessionId);
|
||||||
|
const session = sessions.at(-1);
|
||||||
|
this.session = session ?? null;
|
||||||
|
return session;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _throttledScrollToEnd = throttle(this._scrollToEnd, 600);
|
private readonly createSession = async () => {
|
||||||
|
if (this.session) {
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
const sessionId = await AIProvider.session?.createSession({
|
||||||
|
docId: this.doc.id,
|
||||||
|
workspaceId: this.doc.workspace.id,
|
||||||
|
promptName: 'Chat With AFFiNE AI',
|
||||||
|
});
|
||||||
|
if (sessionId) {
|
||||||
|
const session = await AIProvider.session?.getSession(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
sessionId
|
||||||
|
);
|
||||||
|
this.session = session ?? null;
|
||||||
|
}
|
||||||
|
return this.session;
|
||||||
|
};
|
||||||
|
|
||||||
private readonly _initPanel = async () => {
|
private readonly initPanel = async () => {
|
||||||
try {
|
try {
|
||||||
if (!this._isSidebarOpen.value) return;
|
if (!this.isSidebarOpen.value) {
|
||||||
if (this.isLoading) return;
|
return;
|
||||||
const userId = (await AIProvider.userInfo)?.id;
|
}
|
||||||
if (!userId) return;
|
await this.initSession();
|
||||||
|
|
||||||
this.isLoading = true;
|
|
||||||
await this._updateHistory();
|
|
||||||
this.isLoading = false;
|
|
||||||
this._isInitialized = true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _resetPanel = () => {
|
private readonly resetPanel = () => {
|
||||||
this.session = undefined;
|
this.session = undefined;
|
||||||
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
|
||||||
this.isLoading = false;
|
|
||||||
this._isInitialized = false;
|
|
||||||
this.embeddingProgress = [0, 0];
|
this.embeddingProgress = [0, 0];
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _openPlayground = () => {
|
private readonly updateEmbeddingProgress = (
|
||||||
|
count: Record<ContextEmbedStatus, number>
|
||||||
|
) => {
|
||||||
|
const total = count.finished + count.processing + count.failed;
|
||||||
|
this.embeddingProgress = [count.finished, total];
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly openPlayground = () => {
|
||||||
const playgroundContent = html`
|
const playgroundContent = html`
|
||||||
<playground-content
|
<playground-content
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
@@ -314,48 +180,10 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
createPlaygroundModal(playgroundContent, 'AI Playground');
|
createPlaygroundModal(playgroundContent, 'AI Playground');
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override willUpdate(_changedProperties: PropertyValues) {
|
protected override updated(changedProperties: PropertyValues) {
|
||||||
if (_changedProperties.has('doc')) {
|
if (changedProperties.has('doc')) {
|
||||||
this._resetPanel();
|
this.resetPanel();
|
||||||
requestAnimationFrame(async () => {
|
this.initPanel().catch(console.error);
|
||||||
await this._initPanel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override updated(_changedProperties: PropertyValues) {
|
|
||||||
if (this.chatContextValue.status === 'loading') {
|
|
||||||
// reset the wheel triggered flag when the status is loading
|
|
||||||
this._wheelTriggered = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
_changedProperties.has('chatContextValue') &&
|
|
||||||
(this.chatContextValue.status === 'loading' ||
|
|
||||||
this.chatContextValue.status === 'error' ||
|
|
||||||
this.chatContextValue.status === 'success')
|
|
||||||
) {
|
|
||||||
setTimeout(this._scrollToEnd, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
_changedProperties.has('chatContextValue') &&
|
|
||||||
this.chatContextValue.status === 'transmitting'
|
|
||||||
) {
|
|
||||||
this._throttledScrollToEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override firstUpdated(): void {
|
|
||||||
const chatMessages = this._chatMessagesRef.value;
|
|
||||||
if (chatMessages) {
|
|
||||||
chatMessages.updateComplete
|
|
||||||
.then(() => {
|
|
||||||
chatMessages.getScrollContainer()?.addEventListener('wheel', () => {
|
|
||||||
this._wheelTriggered = true;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,77 +191,43 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (!this.doc) throw new Error('doc is required');
|
if (!this.doc) throw new Error('doc is required');
|
||||||
|
|
||||||
this._disposables.add(
|
|
||||||
AIProvider.slots.actions.subscribe(({ event }) => {
|
|
||||||
const { status } = this.chatContextValue;
|
|
||||||
if (
|
|
||||||
event === 'finished' &&
|
|
||||||
(status === 'idle' || status === 'success')
|
|
||||||
) {
|
|
||||||
this._updateHistory().catch(console.error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this._disposables.add(
|
this._disposables.add(
|
||||||
AIProvider.slots.userInfo.subscribe(() => {
|
AIProvider.slots.userInfo.subscribe(() => {
|
||||||
this._initPanel().catch(console.error);
|
this.resetPanel();
|
||||||
})
|
this.initPanel().catch(console.error);
|
||||||
);
|
|
||||||
this._disposables.add(
|
|
||||||
AIProvider.slots.requestOpenWithChat.subscribe(({ host }) => {
|
|
||||||
if (this.host === host) {
|
|
||||||
extractSelectedContent(host)
|
|
||||||
.then(context => {
|
|
||||||
if (!context) return;
|
|
||||||
this.updateContext(context);
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const isOpen = this.appSidebarConfig.isOpen();
|
const isOpen = this.appSidebarConfig.isOpen();
|
||||||
this._isSidebarOpen = isOpen.signal;
|
this.isSidebarOpen = isOpen.signal;
|
||||||
this._disposables.add(isOpen.cleanup);
|
this._disposables.add(isOpen.cleanup);
|
||||||
|
|
||||||
const width = this.appSidebarConfig.getWidth();
|
const width = this.appSidebarConfig.getWidth();
|
||||||
this._sidebarWidth = width.signal;
|
this.sidebarWidth = width.signal;
|
||||||
this._disposables.add(width.cleanup);
|
this._disposables.add(width.cleanup);
|
||||||
|
|
||||||
this._disposables.add(
|
this._disposables.add(
|
||||||
this._isSidebarOpen.subscribe(isOpen => {
|
this.isSidebarOpen.subscribe(() => {
|
||||||
if (isOpen && !this._isInitialized) {
|
if (this.session === undefined) {
|
||||||
this._initPanel().catch(console.error);
|
this.initPanel().catch(console.error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateContext = (context: Partial<ChatContextValue>) => {
|
|
||||||
this.chatContextValue = { ...this.chatContextValue, ...context };
|
|
||||||
};
|
|
||||||
|
|
||||||
continueInChat = async () => {
|
|
||||||
const text = await getSelectedTextContent(this.host, 'plain-text');
|
|
||||||
const markdown = await getSelectedTextContent(this.host, 'markdown');
|
|
||||||
const images = await getSelectedImagesAsBlobs(this.host);
|
|
||||||
this.updateContext({
|
|
||||||
quote: text,
|
|
||||||
markdown,
|
|
||||||
images,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const width = this._sidebarWidth.value || 0;
|
const isInitialized = this.session !== undefined;
|
||||||
|
if (!isInitialized) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = this.sidebarWidth.value || 0;
|
||||||
const style = styleMap({
|
const style = styleMap({
|
||||||
padding: width > 540 ? '8px 24px 0 24px' : '8px 12px 0 12px',
|
padding: width > 540 ? '8px 24px 0 24px' : '8px 12px 0 12px',
|
||||||
});
|
});
|
||||||
const [done, total] = this.embeddingProgress;
|
const [done, total] = this.embeddingProgress;
|
||||||
const isEmbedding = total > 0 && done < total;
|
const isEmbedding = total > 0 && done < total;
|
||||||
|
const title = html`
|
||||||
return html`<div class="chat-panel-container" style=${style}>
|
|
||||||
<div class="chat-panel-title">
|
|
||||||
<div class="chat-panel-title-text">
|
<div class="chat-panel-title-text">
|
||||||
${isEmbedding
|
${isEmbedding
|
||||||
? html`<span data-testid="chat-panel-embedding-progress"
|
? html`<span data-testid="chat-panel-embedding-progress"
|
||||||
@@ -443,55 +237,34 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
</div>
|
</div>
|
||||||
${this.playgroundConfig.visible.value
|
${this.playgroundConfig.visible.value
|
||||||
? html`
|
? html`
|
||||||
<div class="chat-panel-playground" @click=${this._openPlayground}>
|
<div class="chat-panel-playground" @click=${this.openPlayground}>
|
||||||
${CenterPeekIcon()}
|
${CenterPeekIcon()}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<ai-history-clear
|
`;
|
||||||
|
|
||||||
|
return html`<div class="chat-panel-container" style=${style}>
|
||||||
|
${keyed(
|
||||||
|
this.doc.id,
|
||||||
|
html`<ai-chat-content
|
||||||
|
.chatTitle=${title}
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.doc=${this.doc}
|
.session=${this.session}
|
||||||
.getSessionId=${this._getSessionId}
|
.createSession=${this.createSession}
|
||||||
.onHistoryCleared=${this._updateHistory}
|
.workspaceId=${this.doc.workspace.id}
|
||||||
.chatContextValue=${this.chatContextValue}
|
.docId=${this.doc.id}
|
||||||
></ai-history-clear>
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
</div>
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
<chat-panel-messages
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
${ref(this._chatMessagesRef)}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.chatContextValue=${this.chatContextValue}
|
|
||||||
.getSessionId=${this._getSessionId}
|
|
||||||
.createSessionId=${this._createSessionId}
|
|
||||||
.updateContext=${this.updateContext}
|
|
||||||
.host=${this.host}
|
|
||||||
.isLoading=${this.isLoading}
|
|
||||||
.extensions=${this.extensions}
|
.extensions=${this.extensions}
|
||||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
|
||||||
.reasoningConfig=${this.reasoningConfig}
|
|
||||||
.panelWidth=${this._sidebarWidth}
|
|
||||||
></chat-panel-messages>
|
|
||||||
<ai-chat-composer
|
|
||||||
.host=${this.host}
|
|
||||||
.doc=${this.doc}
|
|
||||||
.session=${this.session}
|
|
||||||
.getSessionId=${this._getSessionId}
|
|
||||||
.createSessionId=${this._createSessionId}
|
|
||||||
.chatContextValue=${this.chatContextValue}
|
|
||||||
.updateContext=${this.updateContext}
|
|
||||||
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
|
||||||
.isVisible=${this._isSidebarOpen}
|
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
|
||||||
.reasoningConfig=${this.reasoningConfig}
|
|
||||||
.playgroundConfig=${this.playgroundConfig}
|
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
|
||||||
.searchMenuConfig=${this.searchMenuConfig}
|
|
||||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||||
.trackOptions=${{
|
.updateEmbeddingProgress=${this.updateEmbeddingProgress}
|
||||||
where: 'chat-panel',
|
.width=${this.sidebarWidth}
|
||||||
control: 'chat-send',
|
></ai-chat-content>`
|
||||||
}}
|
)}
|
||||||
.panelWidth=${this._sidebarWidth}
|
|
||||||
></ai-chat-composer>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
|
import type { CopilotSessionType } from '@affine/graphql';
|
||||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
|
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
@@ -53,7 +54,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
|||||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getSessionId!: () => Promise<string | undefined>;
|
accessor session!: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor retry!: () => void;
|
accessor retry!: () => void;
|
||||||
@@ -62,7 +63,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
|||||||
accessor testId = 'chat-message-assistant';
|
accessor testId = 'chat-message-assistant';
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor panelWidth!: Signal<number | undefined>;
|
accessor width: Signal<number | undefined> | undefined;
|
||||||
|
|
||||||
get state() {
|
get state() {
|
||||||
const { isLast, status } = this;
|
const { isLast, status } = this;
|
||||||
@@ -117,7 +118,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
|||||||
.answer=${answer}
|
.answer=${answer}
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.state=${this.state}
|
.state=${this.state}
|
||||||
.width=${this.panelWidth}
|
.width=${this.width}
|
||||||
.extensions=${this.extensions}
|
.extensions=${this.extensions}
|
||||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||||
></chat-content-stream-objects>`;
|
></chat-content-stream-objects>`;
|
||||||
@@ -134,7 +135,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderEditorActions() {
|
private renderEditorActions() {
|
||||||
const { item, isLast, status } = this;
|
const { item, isLast, status, host, session } = this;
|
||||||
|
|
||||||
if (!isChatMessage(item) || item.role !== 'assistant') return nothing;
|
if (!isChatMessage(item) || item.role !== 'assistant') return nothing;
|
||||||
|
|
||||||
@@ -146,7 +147,6 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
|||||||
)
|
)
|
||||||
return nothing;
|
return nothing;
|
||||||
|
|
||||||
const { host } = this;
|
|
||||||
const { content, streamObjects, id: messageId } = item;
|
const { content, streamObjects, id: messageId } = item;
|
||||||
const markdown = streamObjects?.length
|
const markdown = streamObjects?.length
|
||||||
? mergeStreamContent(streamObjects)
|
? mergeStreamContent(streamObjects)
|
||||||
@@ -159,10 +159,10 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
|||||||
return html`
|
return html`
|
||||||
<chat-copy-more
|
<chat-copy-more
|
||||||
.host=${host}
|
.host=${host}
|
||||||
|
.session=${session}
|
||||||
.actions=${actions}
|
.actions=${actions}
|
||||||
.content=${markdown}
|
.content=${markdown}
|
||||||
.isLast=${isLast}
|
.isLast=${isLast}
|
||||||
.getSessionId=${this.getSessionId}
|
|
||||||
.messageId=${messageId}
|
.messageId=${messageId}
|
||||||
.withMargin=${true}
|
.withMargin=${true}
|
||||||
.retry=${() => this.retry()}
|
.retry=${() => this.retry()}
|
||||||
@@ -171,8 +171,8 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
|||||||
? html`<chat-action-list
|
? html`<chat-action-list
|
||||||
.actions=${actions}
|
.actions=${actions}
|
||||||
.host=${host}
|
.host=${host}
|
||||||
|
.session=${session}
|
||||||
.content=${markdown}
|
.content=${markdown}
|
||||||
.getSessionId=${this.getSessionId}
|
|
||||||
.messageId=${messageId ?? undefined}
|
.messageId=${messageId ?? undefined}
|
||||||
.withMargin=${true}
|
.withMargin=${true}
|
||||||
></chat-action-list>`
|
></chat-action-list>`
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ import type {
|
|||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
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 { Store } from '@blocksuite/affine/store';
|
import { css, html } from 'lit';
|
||||||
import { type Signal, signal } from '@preact/signals-core';
|
|
||||||
import { css, html, type PropertyValues } from 'lit';
|
|
||||||
import { property, state } from 'lit/decorators.js';
|
import { property, state } from 'lit/decorators.js';
|
||||||
|
|
||||||
import { AIProvider } from '../../provider';
|
import { AIProvider } from '../../provider';
|
||||||
@@ -56,16 +54,13 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
accessor host!: EditorHost;
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor doc!: Store;
|
accessor workspaceId!: string;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor session!: CopilotSessionType | undefined;
|
accessor session!: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getSessionId!: () => Promise<string | undefined>;
|
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor createSessionId!: () => Promise<string | undefined>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor chatContextValue!: AIChatInputContext;
|
accessor chatContextValue!: AIChatInputContext;
|
||||||
@@ -73,9 +68,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 isVisible: Signal<boolean | undefined> = signal(false);
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateEmbeddingProgress!: (
|
accessor updateEmbeddingProgress!: (
|
||||||
count: Record<ContextEmbedStatus, number>
|
count: Record<ContextEmbedStatus, number>
|
||||||
@@ -102,9 +94,6 @@ 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 panelWidth: Signal<number | undefined> = signal(undefined);
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||||
|
|
||||||
@@ -114,10 +103,6 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
@state()
|
@state()
|
||||||
accessor embeddingCompleted = false;
|
accessor embeddingCompleted = false;
|
||||||
|
|
||||||
private _isInitialized = false;
|
|
||||||
|
|
||||||
private _isLoading = false;
|
|
||||||
|
|
||||||
private _contextId: string | undefined = undefined;
|
private _contextId: string | undefined = undefined;
|
||||||
|
|
||||||
private _pollAbortController: AbortController | null = null;
|
private _pollAbortController: AbortController | null = null;
|
||||||
@@ -141,9 +126,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.chips=${this.chips}
|
.chips=${this.chips}
|
||||||
.session=${this.session}
|
.session=${this.session}
|
||||||
.getSessionId=${this.getSessionId}
|
.createSession=${this.createSession}
|
||||||
.createSessionId=${this.createSessionId}
|
|
||||||
.getContextId=${this._getContextId}
|
|
||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
.updateContext=${this.updateContext}
|
.updateContext=${this.updateContext}
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
@@ -151,7 +134,6 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.onChatSuccess=${this.onChatSuccess}
|
.onChatSuccess=${this.onChatSuccess}
|
||||||
.trackOptions=${this.trackOptions}
|
.trackOptions=${this.trackOptions}
|
||||||
.panelWidth=${this.panelWidth}
|
|
||||||
.addImages=${this.addImages}
|
.addImages=${this.addImages}
|
||||||
></ai-chat-input>
|
></ai-chat-input>
|
||||||
<div class="chat-panel-footer">
|
<div class="chat-panel-footer">
|
||||||
@@ -173,25 +155,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (!this.doc) throw new Error('doc is required');
|
|
||||||
|
|
||||||
this._disposables.add(
|
|
||||||
AIProvider.slots.userInfo.subscribe(() => {
|
|
||||||
this._initComposer().catch(console.error);
|
this._initComposer().catch(console.error);
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this._disposables.add(
|
|
||||||
this.isVisible.subscribe(isVisible => {
|
|
||||||
if (isVisible && !this._isInitialized) {
|
|
||||||
this._initComposer().catch(console.error);
|
|
||||||
}
|
|
||||||
if (!isVisible) {
|
|
||||||
this._abortPoll();
|
|
||||||
this._abortPollEmbeddingStatus();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override disconnectedCallback() {
|
override disconnectedCallback() {
|
||||||
@@ -200,25 +164,16 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
this._abortPollEmbeddingStatus();
|
this._abortPollEmbeddingStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override willUpdate(_changedProperties: PropertyValues) {
|
|
||||||
if (_changedProperties.has('doc')) {
|
|
||||||
this._resetComposer();
|
|
||||||
requestAnimationFrame(async () => {
|
|
||||||
await this._initComposer();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly _getContextId = async () => {
|
private readonly _getContextId = async () => {
|
||||||
if (this._contextId) {
|
if (this._contextId) {
|
||||||
return this._contextId;
|
return this._contextId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = await this.getSessionId();
|
const sessionId = this.session?.id;
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
|
|
||||||
const contextId = await AIProvider.context?.getContextId(
|
const contextId = await AIProvider.context?.getContextId(
|
||||||
this.doc.workspace.id,
|
this.workspaceId,
|
||||||
sessionId
|
sessionId
|
||||||
);
|
);
|
||||||
this._contextId = contextId;
|
this._contextId = contextId;
|
||||||
@@ -230,11 +185,11 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
return this._contextId;
|
return this._contextId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = await this.createSessionId();
|
const sessionId = (await this.createSession())?.id;
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
|
|
||||||
this._contextId = await AIProvider.context?.createContext(
|
this._contextId = await AIProvider.context?.createContext(
|
||||||
this.doc.workspace.id,
|
this.workspaceId,
|
||||||
sessionId
|
sessionId
|
||||||
);
|
);
|
||||||
return this._contextId;
|
return this._contextId;
|
||||||
@@ -242,7 +197,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
|
|
||||||
private readonly _initChips = async () => {
|
private readonly _initChips = async () => {
|
||||||
// context not initialized
|
// context not initialized
|
||||||
const sessionId = await this.getSessionId();
|
const sessionId = this.session?.id;
|
||||||
const contextId = await this._getContextId();
|
const contextId = await this._getContextId();
|
||||||
if (!sessionId || !contextId) {
|
if (!sessionId || !contextId) {
|
||||||
return;
|
return;
|
||||||
@@ -255,7 +210,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
tags = [],
|
tags = [],
|
||||||
collections = [],
|
collections = [],
|
||||||
} = (await AIProvider.context?.getContextDocsAndFiles(
|
} = (await AIProvider.context?.getContextDocsAndFiles(
|
||||||
this.doc.workspace.id,
|
this.workspaceId,
|
||||||
sessionId,
|
sessionId,
|
||||||
contextId
|
contextId
|
||||||
)) || {};
|
)) || {};
|
||||||
@@ -319,7 +274,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly _pollContextDocsAndFiles = async () => {
|
private readonly _pollContextDocsAndFiles = async () => {
|
||||||
const sessionId = await this.getSessionId();
|
const sessionId = this.session?.id;
|
||||||
const contextId = await this._getContextId();
|
const contextId = await this._getContextId();
|
||||||
if (!sessionId || !contextId || !AIProvider.context) {
|
if (!sessionId || !contextId || !AIProvider.context) {
|
||||||
return;
|
return;
|
||||||
@@ -330,7 +285,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
this._pollAbortController = new AbortController();
|
this._pollAbortController = new AbortController();
|
||||||
await AIProvider.context.pollContextDocsAndFiles(
|
await AIProvider.context.pollContextDocsAndFiles(
|
||||||
this.doc.workspace.id,
|
this.workspaceId,
|
||||||
sessionId,
|
sessionId,
|
||||||
contextId,
|
contextId,
|
||||||
this._onPoll,
|
this._onPoll,
|
||||||
@@ -436,13 +391,9 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly _initComposer = async () => {
|
private readonly _initComposer = async () => {
|
||||||
if (!this.isVisible.value) return;
|
|
||||||
if (this._isLoading) return;
|
|
||||||
|
|
||||||
const userId = (await AIProvider.userInfo)?.id;
|
const userId = (await AIProvider.userInfo)?.id;
|
||||||
if (!userId) return;
|
if (!userId || !this.session) return;
|
||||||
|
|
||||||
this._isLoading = true;
|
|
||||||
await this._initChips();
|
await this._initChips();
|
||||||
const needPoll = this.chips.some(
|
const needPoll = this.chips.some(
|
||||||
chip =>
|
chip =>
|
||||||
@@ -452,16 +403,5 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
await this._pollContextDocsAndFiles();
|
await this._pollContextDocsAndFiles();
|
||||||
}
|
}
|
||||||
await this._pollEmbeddingStatus();
|
await this._pollEmbeddingStatus();
|
||||||
this._isLoading = false;
|
|
||||||
this._isInitialized = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _resetComposer = () => {
|
|
||||||
this._abortPoll();
|
|
||||||
this._abortPollEmbeddingStatus();
|
|
||||||
this.chips = [];
|
|
||||||
this._contextId = undefined;
|
|
||||||
this._isLoading = false;
|
|
||||||
this._isInitialized = false;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
|
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
|
import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
|
||||||
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
|
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||||
|
import { type Signal } from '@preact/signals-core';
|
||||||
|
import { css, html, type PropertyValues, type TemplateResult } from 'lit';
|
||||||
|
import { property, state } from 'lit/decorators.js';
|
||||||
|
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
|
||||||
|
import { throttle } from 'lodash-es';
|
||||||
|
|
||||||
|
import { type AIChatParams, AIProvider } from '../../provider/ai-provider';
|
||||||
|
import { extractSelectedContent } from '../../utils/extract';
|
||||||
|
import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
|
||||||
|
import type {
|
||||||
|
AINetworkSearchConfig,
|
||||||
|
AIReasoningConfig,
|
||||||
|
} from '../ai-chat-input';
|
||||||
|
import {
|
||||||
|
type AIChatMessages,
|
||||||
|
type ChatAction,
|
||||||
|
type ChatMessage,
|
||||||
|
type HistoryMessage,
|
||||||
|
isChatMessage,
|
||||||
|
} from '../ai-chat-messages';
|
||||||
|
import type { ChatContextValue } from './type';
|
||||||
|
|
||||||
|
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
|
||||||
|
quote: '',
|
||||||
|
images: [],
|
||||||
|
abortController: null,
|
||||||
|
messages: [],
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
markdown: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AIChatContent extends SignalWatcher(
|
||||||
|
WithDisposable(ShadowlessElement)
|
||||||
|
) {
|
||||||
|
static override styles = css`
|
||||||
|
ai-chat-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.ai-chat-title {
|
||||||
|
background: var(--affine-background-primary-color);
|
||||||
|
position: relative;
|
||||||
|
padding: 8px 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: var(--affine-text-secondary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ai-chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor chatTitle!: TemplateResult<1>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor session!: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor workspaceId!: string;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor docId: string | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor reasoningConfig!: AIReasoningConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor searchMenuConfig!: SearchMenuConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor docDisplayConfig!: DocDisplayConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor extensions!: ExtensionType[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor updateEmbeddingProgress!: (
|
||||||
|
count: Record<ContextEmbedStatus, number>
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor width: Signal<number | undefined> | undefined;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor isHistoryLoading = false;
|
||||||
|
|
||||||
|
private readonly chatMessagesRef: Ref<AIChatMessages> =
|
||||||
|
createRef<AIChatMessages>();
|
||||||
|
|
||||||
|
// request counter to track the latest request
|
||||||
|
private updateHistoryCounter = 0;
|
||||||
|
|
||||||
|
private wheelTriggered = false;
|
||||||
|
|
||||||
|
private readonly updateHistory = async () => {
|
||||||
|
const currentRequest = ++this.updateHistoryCounter;
|
||||||
|
if (!AIProvider.histories) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = this.session?.id;
|
||||||
|
const [histories, actions] = await Promise.all([
|
||||||
|
sessionId
|
||||||
|
? AIProvider.histories.chats(this.workspaceId, sessionId, this.docId)
|
||||||
|
: Promise.resolve([]),
|
||||||
|
this.docId
|
||||||
|
? AIProvider.histories.actions(this.workspaceId, this.docId)
|
||||||
|
: Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if this is still the latest request
|
||||||
|
if (currentRequest !== this.updateHistoryCounter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages: HistoryMessage[] = this.chatContextValue.messages.slice();
|
||||||
|
|
||||||
|
const chatActions = (actions || []) as ChatAction[];
|
||||||
|
messages.push(...chatActions);
|
||||||
|
|
||||||
|
const chatMessages = (histories?.[0]?.messages || []) as ChatMessage[];
|
||||||
|
messages.push(...chatMessages);
|
||||||
|
|
||||||
|
this.updateContext({
|
||||||
|
messages: messages.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scrollToEnd();
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly updateActions = async () => {
|
||||||
|
if (!this.docId || !AIProvider.histories) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const actions = await AIProvider.histories.actions(
|
||||||
|
this.workspaceId,
|
||||||
|
this.docId
|
||||||
|
);
|
||||||
|
if (actions && actions.length) {
|
||||||
|
const chatMessages = this.chatContextValue.messages.filter(message =>
|
||||||
|
isChatMessage(message)
|
||||||
|
);
|
||||||
|
const chatActions = actions as ChatAction[];
|
||||||
|
const messages: HistoryMessage[] = [...chatMessages, ...chatActions];
|
||||||
|
this.updateContext({
|
||||||
|
messages: messages.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly updateContext = (context: Partial<ChatContextValue>) => {
|
||||||
|
this.chatContextValue = { ...this.chatContextValue, ...context };
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly scrollToEnd = () => {
|
||||||
|
if (!this.wheelTriggered) {
|
||||||
|
this.chatMessagesRef.value?.scrollToEnd();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _throttledScrollToEnd = throttle(this.scrollToEnd, 600);
|
||||||
|
|
||||||
|
private readonly initChatContent = async () => {
|
||||||
|
this.isHistoryLoading = true;
|
||||||
|
await this.updateHistory();
|
||||||
|
this.isHistoryLoading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override firstUpdated(): void {
|
||||||
|
const chatMessages = this.chatMessagesRef.value;
|
||||||
|
if (chatMessages) {
|
||||||
|
chatMessages.updateComplete
|
||||||
|
.then(() => {
|
||||||
|
chatMessages.getScrollContainer()?.addEventListener('wheel', () => {
|
||||||
|
this.wheelTriggered = true;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override updated(changedProperties: PropertyValues) {
|
||||||
|
if (this.chatContextValue.status === 'loading') {
|
||||||
|
// reset the wheel triggered flag when the status is loading
|
||||||
|
this.wheelTriggered = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
changedProperties.has('chatContextValue') &&
|
||||||
|
(this.chatContextValue.status === 'loading' ||
|
||||||
|
this.chatContextValue.status === 'error' ||
|
||||||
|
this.chatContextValue.status === 'success')
|
||||||
|
) {
|
||||||
|
setTimeout(this.scrollToEnd, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
changedProperties.has('chatContextValue') &&
|
||||||
|
this.chatContextValue.status === 'transmitting'
|
||||||
|
) {
|
||||||
|
this._throttledScrollToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.initChatContent().catch(console.error);
|
||||||
|
|
||||||
|
this._disposables.add(
|
||||||
|
AIProvider.slots.actions.subscribe(({ event }) => {
|
||||||
|
const { status } = this.chatContextValue;
|
||||||
|
if (
|
||||||
|
event === 'finished' &&
|
||||||
|
(status === 'idle' || status === 'success')
|
||||||
|
) {
|
||||||
|
this.updateActions().catch(console.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this._disposables.add(
|
||||||
|
AIProvider.slots.requestOpenWithChat.subscribe(
|
||||||
|
(params: AIChatParams | null) => {
|
||||||
|
if (!params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.host === params.host) {
|
||||||
|
extractSelectedContent(params.host)
|
||||||
|
.then(context => {
|
||||||
|
if (!context) return;
|
||||||
|
this.updateContext(context);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html` <div class="ai-chat-title">${this.chatTitle}</div>
|
||||||
|
<ai-chat-messages
|
||||||
|
${ref(this.chatMessagesRef)}
|
||||||
|
.host=${this.host}
|
||||||
|
.session=${this.session}
|
||||||
|
.createSession=${this.createSession}
|
||||||
|
.chatContextValue=${this.chatContextValue}
|
||||||
|
.updateContext=${this.updateContext}
|
||||||
|
.isHistoryLoading=${this.isHistoryLoading}
|
||||||
|
.extensions=${this.extensions}
|
||||||
|
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||||
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
|
.width=${this.width}
|
||||||
|
></ai-chat-messages>
|
||||||
|
<ai-chat-composer
|
||||||
|
.host=${this.host}
|
||||||
|
.workspaceId=${this.workspaceId}
|
||||||
|
.session=${this.session}
|
||||||
|
.createSession=${this.createSession}
|
||||||
|
.chatContextValue=${this.chatContextValue}
|
||||||
|
.updateContext=${this.updateContext}
|
||||||
|
.updateEmbeddingProgress=${this.updateEmbeddingProgress}
|
||||||
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
|
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||||
|
.trackOptions=${{
|
||||||
|
where: 'chat-panel',
|
||||||
|
control: 'chat-send',
|
||||||
|
}}
|
||||||
|
></ai-chat-composer>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './ai-chat-content';
|
||||||
|
export * from './type';
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import type {
|
import type { AIError } from '../../provider';
|
||||||
ChatStatus,
|
import type { ChatStatus, HistoryMessage } from '../ai-chat-messages';
|
||||||
HistoryMessage,
|
|
||||||
} from '../components/ai-chat-messages';
|
|
||||||
import type { AIError } from '../provider';
|
|
||||||
|
|
||||||
export type ChatContextValue = {
|
export type ChatContextValue = {
|
||||||
// history messages of the chat
|
// history messages of the chat
|
||||||
@@ -6,14 +6,13 @@ import { openFilesWith } from '@blocksuite/affine/shared/utils';
|
|||||||
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 { ArrowUpBigIcon, CloseIcon, ImageIcon } from '@blocksuite/icons/lit';
|
import { ArrowUpBigIcon, CloseIcon, ImageIcon } from '@blocksuite/icons/lit';
|
||||||
import { type Signal, signal } from '@preact/signals-core';
|
|
||||||
import { css, html, nothing } from 'lit';
|
import { css, html, 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';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import { ChatAbortIcon } from '../../_common/icons';
|
import { ChatAbortIcon } from '../../_common/icons';
|
||||||
import { type AIError, AIProvider } from '../../provider';
|
import { type AIError, AIProvider, type AISendParams } from '../../provider';
|
||||||
import { reportResponse } from '../../utils/action-reporter';
|
import { reportResponse } from '../../utils/action-reporter';
|
||||||
import { readBlobAsURL } from '../../utils/image';
|
import { readBlobAsURL } from '../../utils/image';
|
||||||
import { mergeStreamObjects } from '../../utils/stream-objects';
|
import { mergeStreamObjects } from '../../utils/stream-objects';
|
||||||
@@ -285,7 +284,7 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
accessor host!: EditorHost;
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor session!: CopilotSessionType | undefined;
|
accessor session!: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
@query('image-preview-grid')
|
@query('image-preview-grid')
|
||||||
accessor imagePreviewGrid: HTMLDivElement | null = null;
|
accessor imagePreviewGrid: HTMLDivElement | null = null;
|
||||||
@@ -309,13 +308,7 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
accessor chips: ChatChip[] = [];
|
accessor chips: ChatChip[] = [];
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getSessionId!: () => Promise<string | undefined>;
|
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor createSessionId!: () => Promise<string | undefined>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor getContextId!: () => Promise<string | undefined>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
||||||
@@ -341,9 +334,6 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
@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 panelWidth: Signal<number | undefined> = signal(undefined);
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor addImages!: (images: File[]) => void;
|
accessor addImages!: (images: File[]) => void;
|
||||||
|
|
||||||
@@ -366,9 +356,15 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._disposables.add(
|
this._disposables.add(
|
||||||
AIProvider.slots.requestSendWithChat.subscribe(
|
AIProvider.slots.requestSendWithChat.subscribe(
|
||||||
({ input, context, host }) => {
|
(params: AISendParams | null) => {
|
||||||
|
if (!params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { input, context, host } = params;
|
||||||
if (this.host === host) {
|
if (this.host === host) {
|
||||||
context && this.updateContext(context);
|
if (context) {
|
||||||
|
this.updateContext(context);
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.send(input).catch(console.error);
|
this.send(input).catch(console.error);
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -591,7 +587,7 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
// optimistic update messages
|
// optimistic update messages
|
||||||
await this._preUpdateMessages(userInput, attachments);
|
await this._preUpdateMessages(userInput, attachments);
|
||||||
|
|
||||||
const sessionId = await this.createSessionId();
|
const sessionId = (await this.createSession())?.id;
|
||||||
let contexts = await this._getMatchedContexts();
|
let contexts = await this._getMatchedContexts();
|
||||||
if (abortController.signal.aborted) {
|
if (abortController.signal.aborted) {
|
||||||
return;
|
return;
|
||||||
@@ -678,11 +674,13 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly _postUpdateMessages = async () => {
|
private readonly _postUpdateMessages = async () => {
|
||||||
|
const sessionId = this.session?.id;
|
||||||
|
if (!sessionId || !AIProvider.histories) return;
|
||||||
|
|
||||||
const { messages } = this.chatContextValue;
|
const { messages } = this.chatContextValue;
|
||||||
const last = messages[messages.length - 1] as ChatMessage;
|
const last = messages[messages.length - 1] as ChatMessage;
|
||||||
if (!last.id) {
|
if (!last.id) {
|
||||||
const sessionId = await this.getSessionId();
|
const historyIds = await AIProvider.histories.ids(
|
||||||
const historyIds = await AIProvider.histories?.ids(
|
|
||||||
this.host.store.workspace.id,
|
this.host.store.workspace.id,
|
||||||
this.host.store.id,
|
this.host.store.id,
|
||||||
{ sessionId }
|
{ sessionId }
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export class ChatInputPreference extends SignalWatcher(
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor session!: CopilotSessionType | undefined;
|
accessor session!: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onModelChange: ((modelId: string) => void) | undefined;
|
accessor onModelChange: ((modelId: string) => void) | undefined;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { CopilotSessionType } from '@affine/graphql';
|
||||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import {
|
import {
|
||||||
DocModeProvider,
|
DocModeProvider,
|
||||||
@@ -13,25 +14,21 @@ import { property, query, state } from 'lit/decorators.js';
|
|||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
|
|
||||||
import { AffineIcon } from '../_common/icons';
|
import { AffineIcon } from '../../_common/icons';
|
||||||
|
import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const';
|
||||||
|
import { AIPreloadConfig } from '../../chat-panel/preload-config';
|
||||||
|
import { type AIError, AIProvider, UnauthorizedError } from '../../provider';
|
||||||
|
import { mergeStreamObjects } from '../../utils/stream-objects';
|
||||||
|
import { type ChatContextValue } from '../ai-chat-content/type';
|
||||||
import type {
|
import type {
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
AIReasoningConfig,
|
AIReasoningConfig,
|
||||||
} from '../components/ai-chat-input';
|
} from '../ai-chat-input';
|
||||||
import {
|
import { isChatAction, isChatMessage, StreamObjectSchema } from './type';
|
||||||
isChatAction,
|
|
||||||
isChatMessage,
|
|
||||||
StreamObjectSchema,
|
|
||||||
} from '../components/ai-chat-messages';
|
|
||||||
import { type AIError, AIProvider, UnauthorizedError } from '../provider';
|
|
||||||
import { mergeStreamObjects } from '../utils/stream-objects';
|
|
||||||
import { type ChatContextValue } from './chat-context';
|
|
||||||
import { HISTORY_IMAGE_ACTIONS } from './const';
|
|
||||||
import { AIPreloadConfig } from './preload-config';
|
|
||||||
|
|
||||||
export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
chat-panel-messages {
|
ai-chat-messages {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,16 +144,16 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
accessor host!: EditorHost;
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor isLoading!: boolean;
|
accessor isHistoryLoading!: boolean;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor chatContextValue!: ChatContextValue;
|
accessor chatContextValue!: ChatContextValue;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getSessionId!: () => Promise<string | undefined>;
|
accessor session!: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor createSessionId!: () => Promise<string | undefined>;
|
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
||||||
@@ -174,7 +171,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
accessor reasoningConfig!: AIReasoningConfig;
|
accessor reasoningConfig!: AIReasoningConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor panelWidth!: Signal<number | undefined>;
|
accessor width: Signal<number | undefined> | undefined;
|
||||||
|
|
||||||
@query('.chat-panel-messages-container')
|
@query('.chat-panel-messages-container')
|
||||||
accessor messagesContainer: HTMLDivElement | null = null;
|
accessor messagesContainer: HTMLDivElement | null = null;
|
||||||
@@ -202,7 +199,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderAIOnboarding() {
|
private _renderAIOnboarding() {
|
||||||
return this.isLoading ||
|
return this.isHistoryLoading ||
|
||||||
!this.host?.store.get(FeatureFlagService).getFlag('enable_ai_onboarding')
|
!this.host?.store.get(FeatureFlagService).getFlag('enable_ai_onboarding')
|
||||||
? nothing
|
? nothing
|
||||||
: html`<div class="onboarding-wrapper" data-testid="ai-onboarding">
|
: html`<div class="onboarding-wrapper" data-testid="ai-onboarding">
|
||||||
@@ -241,7 +238,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
const { messages, status, error } = this.chatContextValue;
|
const { messages, status, error } = this.chatContextValue;
|
||||||
const { isLoading } = this;
|
const { isHistoryLoading } = this;
|
||||||
const filteredItems = messages.filter(item => {
|
const filteredItems = messages.filter(item => {
|
||||||
return (
|
return (
|
||||||
isChatMessage(item) ||
|
isChatMessage(item) ||
|
||||||
@@ -268,12 +265,15 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
data-testid="chat-panel-messages-placeholder"
|
data-testid="chat-panel-messages-placeholder"
|
||||||
>
|
>
|
||||||
${AffineIcon(
|
${AffineIcon(
|
||||||
isLoading
|
isHistoryLoading
|
||||||
? 'var(--affine-icon-secondary)'
|
? 'var(--affine-icon-secondary)'
|
||||||
: 'var(--affine-primary-color)'
|
: 'var(--affine-primary-color)'
|
||||||
)}
|
)}
|
||||||
<div class="messages-placeholder-title" data-loading=${isLoading}>
|
<div
|
||||||
${this.isLoading
|
class="messages-placeholder-title"
|
||||||
|
data-loading=${isHistoryLoading}
|
||||||
|
>
|
||||||
|
${this.isHistoryLoading
|
||||||
? html`<span data-testid="chat-panel-loading-state"
|
? html`<span data-testid="chat-panel-loading-state"
|
||||||
>AFFiNE AI is loading history...</span
|
>AFFiNE AI is loading history...</span
|
||||||
>`
|
>`
|
||||||
@@ -295,15 +295,15 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
} else if (isChatMessage(item) && item.role === 'assistant') {
|
} else if (isChatMessage(item) && item.role === 'assistant') {
|
||||||
return html`<chat-message-assistant
|
return html`<chat-message-assistant
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
|
.session=${this.session}
|
||||||
.item=${item}
|
.item=${item}
|
||||||
.isLast=${isLast}
|
.isLast=${isLast}
|
||||||
.status=${isLast ? status : 'idle'}
|
.status=${isLast ? status : 'idle'}
|
||||||
.error=${isLast ? error : null}
|
.error=${isLast ? error : null}
|
||||||
.extensions=${this.extensions}
|
.extensions=${this.extensions}
|
||||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||||
.getSessionId=${this.getSessionId}
|
|
||||||
.retry=${() => this.retry()}
|
.retry=${() => this.retry()}
|
||||||
.panelWidth=${this.panelWidth}
|
.width=${this.width}
|
||||||
></chat-message-assistant>`;
|
></chat-message-assistant>`;
|
||||||
} else if (isChatAction(item)) {
|
} else if (isChatAction(item)) {
|
||||||
return html`<chat-message-action
|
return html`<chat-message-action
|
||||||
@@ -365,7 +365,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override updated(_changedProperties: PropertyValues) {
|
protected override updated(_changedProperties: PropertyValues) {
|
||||||
if (_changedProperties.has('isLoading')) {
|
if (_changedProperties.has('isHistoryLoading')) {
|
||||||
this.canScrollDown = false;
|
this.canScrollDown = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,7 +382,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
|
|
||||||
retry = async () => {
|
retry = async () => {
|
||||||
try {
|
try {
|
||||||
const sessionId = await this.createSessionId();
|
const sessionId = (await this.createSession())?.id;
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
if (!AIProvider.actions.chat) return;
|
if (!AIProvider.actions.chat) return;
|
||||||
|
|
||||||
@@ -448,9 +448,3 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'chat-panel-messages': ChatPanelMessages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from './ai-chat-messages';
|
||||||
export * from './type';
|
export * from './type';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { CopilotSessionType } from '@affine/graphql';
|
||||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { NotificationProvider } from '@blocksuite/affine/shared/services';
|
import { NotificationProvider } from '@blocksuite/affine/shared/services';
|
||||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||||
@@ -7,15 +8,15 @@ import type { Store } from '@blocksuite/affine/store';
|
|||||||
import { css, html } from 'lit';
|
import { css, html } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
import type { ChatContextValue } from '../../chat-panel/chat-context';
|
|
||||||
import { AIProvider } from '../../provider';
|
import { AIProvider } from '../../provider';
|
||||||
|
import type { ChatContextValue } from '../ai-chat-content';
|
||||||
|
|
||||||
export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
|
export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor chatContextValue!: ChatContextValue;
|
accessor chatContextValue!: ChatContextValue;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getSessionId!: () => Promise<string | undefined>;
|
accessor session!: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host!: EditorHost;
|
accessor host!: EditorHost;
|
||||||
@@ -41,15 +42,16 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
|
|||||||
return (
|
return (
|
||||||
this.chatContextValue.status === 'loading' ||
|
this.chatContextValue.status === 'loading' ||
|
||||||
this.chatContextValue.status === 'transmitting' ||
|
this.chatContextValue.status === 'transmitting' ||
|
||||||
!this.chatContextValue.messages.length
|
!this.chatContextValue.messages.length ||
|
||||||
|
!this.session
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _cleanupHistories = async () => {
|
private readonly _cleanupHistories = async () => {
|
||||||
if (this._isHistoryClearDisabled) {
|
if (this._isHistoryClearDisabled || !this.session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sessionId = await this.getSessionId();
|
const sessionId = this.session.id;
|
||||||
const notification = this.host.std.getOptional(NotificationProvider);
|
const notification = this.host.std.getOptional(NotificationProvider);
|
||||||
if (!notification) return;
|
if (!notification) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { CopilotSessionType } from '@affine/graphql';
|
||||||
import type { ImageSelection } from '@blocksuite/affine/shared/selection';
|
import type { ImageSelection } from '@blocksuite/affine/shared/selection';
|
||||||
import { NotificationProvider } from '@blocksuite/affine/shared/services';
|
import { NotificationProvider } from '@blocksuite/affine/shared/services';
|
||||||
import type {
|
import type {
|
||||||
@@ -81,7 +82,7 @@ export class ChatActionList extends LitElement {
|
|||||||
accessor content: string = '';
|
accessor content: string = '';
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getSessionId!: () => Promise<string | undefined>;
|
accessor session!: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor messageId: string | undefined = undefined;
|
accessor messageId: string | undefined = undefined;
|
||||||
@@ -138,7 +139,7 @@ export class ChatActionList extends LitElement {
|
|||||||
blocks: this._currentBlockSelections,
|
blocks: this._currentBlockSelections,
|
||||||
images: this._currentImageSelections,
|
images: this._currentImageSelections,
|
||||||
};
|
};
|
||||||
const sessionId = await this.getSessionId();
|
const sessionId = this.session?.id;
|
||||||
const success = await action.handler(
|
const success = await action.handler(
|
||||||
host,
|
host,
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { CopilotSessionType } from '@affine/graphql';
|
||||||
import { Tooltip } from '@blocksuite/affine/components/toolbar';
|
import { Tooltip } from '@blocksuite/affine/components/toolbar';
|
||||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { noop } from '@blocksuite/affine/global/utils';
|
import { noop } from '@blocksuite/affine/global/utils';
|
||||||
@@ -110,10 +111,10 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
|
|||||||
accessor actions: ChatAction[] = [];
|
accessor actions: ChatAction[] = [];
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor content!: string;
|
accessor session!: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getSessionId!: () => Promise<string | undefined>;
|
accessor content!: string;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor messageId: string | undefined = undefined;
|
accessor messageId: string | undefined = undefined;
|
||||||
@@ -221,7 +222,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
|
|||||||
};
|
};
|
||||||
return html`<div
|
return html`<div
|
||||||
@click=${async () => {
|
@click=${async () => {
|
||||||
const sessionId = await this.getSessionId();
|
const sessionId = this.session?.id;
|
||||||
const success = await action.handler(
|
const success = await action.handler(
|
||||||
host,
|
host,
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -6,23 +6,22 @@ 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 { DeleteIcon, NewPageIcon } from '@blocksuite/icons/lit';
|
import { DeleteIcon, NewPageIcon } from '@blocksuite/icons/lit';
|
||||||
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';
|
||||||
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { throttle } from 'lodash-es';
|
import { throttle } from 'lodash-es';
|
||||||
|
|
||||||
import type { AppSidebarConfig } from '../../chat-panel/chat-config';
|
import type { AppSidebarConfig } from '../../chat-panel/chat-config';
|
||||||
import type { ChatContextValue } from '../../chat-panel/chat-context';
|
|
||||||
import type { ChatPanelMessages } from '../../chat-panel/chat-panel-messages';
|
|
||||||
import { AIProvider } from '../../provider';
|
import { AIProvider } from '../../provider';
|
||||||
import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
|
import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
|
||||||
|
import type { ChatContextValue } from '../ai-chat-content';
|
||||||
import type {
|
import type {
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
AIPlaygroundConfig,
|
AIPlaygroundConfig,
|
||||||
AIReasoningConfig,
|
AIReasoningConfig,
|
||||||
} from '../ai-chat-input';
|
} from '../ai-chat-input';
|
||||||
import {
|
import {
|
||||||
|
type AIChatMessages,
|
||||||
type ChatAction,
|
type ChatAction,
|
||||||
type ChatMessage,
|
type ChatMessage,
|
||||||
type HistoryMessage,
|
type HistoryMessage,
|
||||||
@@ -74,7 +73,7 @@ export class PlaygroundChat extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chat-panel-messages {
|
ai-chat-messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
@@ -129,6 +128,9 @@ export class PlaygroundChat extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor doc!: Store;
|
accessor doc!: Store;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor session!: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||||
|
|
||||||
@@ -153,9 +155,6 @@ export class PlaygroundChat extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor session: CopilotSessionType | undefined = undefined;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor addChat!: () => Promise<void>;
|
accessor addChat!: () => Promise<void>;
|
||||||
|
|
||||||
@@ -168,10 +167,8 @@ export class PlaygroundChat extends SignalWatcher(
|
|||||||
@state()
|
@state()
|
||||||
accessor embeddingProgress: [number, number] = [0, 0];
|
accessor embeddingProgress: [number, number] = [0, 0];
|
||||||
|
|
||||||
private readonly _isVisible: Signal<boolean | undefined> = signal(true);
|
private readonly _chatMessagesRef: Ref<AIChatMessages> =
|
||||||
|
createRef<AIChatMessages>();
|
||||||
private readonly _chatMessagesRef: Ref<ChatPanelMessages> =
|
|
||||||
createRef<ChatPanelMessages>();
|
|
||||||
|
|
||||||
// request counter to track the latest request
|
// request counter to track the latest request
|
||||||
private _updateHistoryCounter = 0;
|
private _updateHistoryCounter = 0;
|
||||||
@@ -185,22 +182,29 @@ export class PlaygroundChat extends SignalWatcher(
|
|||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _getSessionId = async () => {
|
private readonly _createSession = async () => {
|
||||||
return this.session?.id;
|
return this.session;
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _createSessionId = async () => {
|
|
||||||
return this.session?.id;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _updateHistory = async () => {
|
private readonly _updateHistory = async () => {
|
||||||
const { doc } = this;
|
if (!AIProvider.histories) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentRequest = ++this._updateHistoryCounter;
|
const currentRequest = ++this._updateHistoryCounter;
|
||||||
|
|
||||||
|
const sessionId = this.session?.id;
|
||||||
const [histories, actions] = await Promise.all([
|
const [histories, actions] = await Promise.all([
|
||||||
AIProvider.histories?.chats(doc.workspace.id, doc.id),
|
sessionId
|
||||||
AIProvider.histories?.actions(doc.workspace.id, doc.id),
|
? AIProvider.histories.chats(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
sessionId,
|
||||||
|
this.doc.id
|
||||||
|
)
|
||||||
|
: Promise.resolve([]),
|
||||||
|
this.doc.id
|
||||||
|
? AIProvider.histories.actions(this.doc.workspace.id, this.doc.id)
|
||||||
|
: Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if this is still the latest request
|
// Check if this is still the latest request
|
||||||
@@ -211,12 +215,8 @@ export class PlaygroundChat extends SignalWatcher(
|
|||||||
const chatActions = (actions || []) as ChatAction[];
|
const chatActions = (actions || []) as ChatAction[];
|
||||||
const messages: HistoryMessage[] = chatActions;
|
const messages: HistoryMessage[] = chatActions;
|
||||||
|
|
||||||
const sessionId = await this._getSessionId();
|
const chatMessages = (histories?.[0]?.messages || []) as ChatMessage[];
|
||||||
const history = histories?.find(history => history.sessionId === sessionId);
|
|
||||||
if (history) {
|
|
||||||
const chatMessages = (history.messages || []) as ChatMessage[];
|
|
||||||
messages.push(...chatMessages);
|
messages.push(...chatMessages);
|
||||||
}
|
|
||||||
|
|
||||||
this.chatContextValue = {
|
this.chatContextValue = {
|
||||||
...this.chatContextValue,
|
...this.chatContextValue,
|
||||||
@@ -289,35 +289,33 @@ export class PlaygroundChat extends SignalWatcher(
|
|||||||
<ai-history-clear
|
<ai-history-clear
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.doc=${this.doc}
|
.doc=${this.doc}
|
||||||
.getSessionId=${this._getSessionId}
|
.session=${this.session}
|
||||||
.onHistoryCleared=${this._updateHistory}
|
.onHistoryCleared=${this._updateHistory}
|
||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
></ai-history-clear>
|
></ai-history-clear>
|
||||||
<div class="chat-panel-delete">${DeleteIcon()}</div>
|
<div class="chat-panel-delete">${DeleteIcon()}</div>
|
||||||
</div>
|
</div>
|
||||||
<chat-panel-messages
|
<ai-chat-messages
|
||||||
${ref(this._chatMessagesRef)}
|
${ref(this._chatMessagesRef)}
|
||||||
.chatContextValue=${this.chatContextValue}
|
|
||||||
.getSessionId=${this._getSessionId}
|
|
||||||
.createSessionId=${this._createSessionId}
|
|
||||||
.updateContext=${this.updateContext}
|
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.isLoading=${this.isLoading}
|
.isHistoryLoading=${this.isLoading}
|
||||||
|
.chatContextValue=${this.chatContextValue}
|
||||||
|
.session=${this.session}
|
||||||
|
.createSession=${this._createSession}
|
||||||
|
.updateContext=${this.updateContext}
|
||||||
.extensions=${this.extensions}
|
.extensions=${this.extensions}
|
||||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
.reasoningConfig=${this.reasoningConfig}
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
></chat-panel-messages>
|
></ai-chat-messages>
|
||||||
<ai-chat-composer
|
<ai-chat-composer
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.doc=${this.doc}
|
.workspaceId=${this.doc.workspace.id}
|
||||||
.session=${this.session}
|
.session=${this.session}
|
||||||
.getSessionId=${this._getSessionId}
|
.createSession=${this._createSession}
|
||||||
.createSessionId=${this._createSessionId}
|
|
||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
.updateContext=${this.updateContext}
|
.updateContext=${this.updateContext}
|
||||||
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
||||||
.isVisible=${this._isVisible}
|
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
.reasoningConfig=${this.reasoningConfig}
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
.playgroundConfig=${this.playgroundConfig}
|
.playgroundConfig=${this.playgroundConfig}
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ export class PlaygroundContent extends SignalWatcher(
|
|||||||
<playground-chat
|
<playground-chat
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.doc=${this.doc}
|
.doc=${this.doc}
|
||||||
|
.session=${session}
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
.reasoningConfig=${this.reasoningConfig}
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
.playgroundConfig=${this.playgroundConfig}
|
.playgroundConfig=${this.playgroundConfig}
|
||||||
@@ -335,7 +336,6 @@ export class PlaygroundContent extends SignalWatcher(
|
|||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.extensions=${this.extensions}
|
.extensions=${this.extensions}
|
||||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||||
.session=${session}
|
|
||||||
.addChat=${this.addChat}
|
.addChat=${this.addChat}
|
||||||
></playground-chat>
|
></playground-chat>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { AIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-block';
|
|||||||
import { EdgelessAIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-edgeless-block';
|
import { EdgelessAIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-edgeless-block';
|
||||||
import { LitTranscriptionBlock } from './blocks/ai-chat-block/ai-transcription-block';
|
import { LitTranscriptionBlock } from './blocks/ai-chat-block/ai-transcription-block';
|
||||||
import {
|
import {
|
||||||
AIChatMessage,
|
AIChatBlockMessage,
|
||||||
AIChatMessages,
|
AIChatBlockMessages,
|
||||||
} from './blocks/ai-chat-block/components/ai-chat-messages';
|
} from './blocks/ai-chat-block/components/ai-chat-messages';
|
||||||
import {
|
import {
|
||||||
ChatImage,
|
ChatImage,
|
||||||
@@ -20,7 +20,6 @@ import { ActionMindmap } from './chat-panel/actions/mindmap';
|
|||||||
import { ActionSlides } from './chat-panel/actions/slides';
|
import { ActionSlides } from './chat-panel/actions/slides';
|
||||||
import { ActionText } from './chat-panel/actions/text';
|
import { ActionText } from './chat-panel/actions/text';
|
||||||
import { AILoading } from './chat-panel/ai-loading';
|
import { AILoading } from './chat-panel/ai-loading';
|
||||||
import { ChatPanelMessages } from './chat-panel/chat-panel-messages';
|
|
||||||
import { ChatMessageAction } from './chat-panel/message/action';
|
import { ChatMessageAction } from './chat-panel/message/action';
|
||||||
import { ChatMessageAssistant } from './chat-panel/message/assistant';
|
import { ChatMessageAssistant } from './chat-panel/message/assistant';
|
||||||
import { ChatMessageUser } from './chat-panel/message/user';
|
import { ChatMessageUser } from './chat-panel/message/user';
|
||||||
@@ -33,9 +32,11 @@ 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 { AIChatContent } from './components/ai-chat-content';
|
||||||
import { AIChatInput } from './components/ai-chat-input';
|
import { AIChatInput } from './components/ai-chat-input';
|
||||||
import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embedding-status-tooltip';
|
import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embedding-status-tooltip';
|
||||||
import { ChatInputPreference } from './components/ai-chat-input/preference-popup';
|
import { ChatInputPreference } from './components/ai-chat-input/preference-popup';
|
||||||
|
import { AIChatMessages } from './components/ai-chat-messages/ai-chat-messages';
|
||||||
import { AIHistoryClear } from './components/ai-history-clear';
|
import { AIHistoryClear } from './components/ai-history-clear';
|
||||||
import { effects as componentAiItemEffects } from './components/ai-item';
|
import { effects as componentAiItemEffects } from './components/ai-item';
|
||||||
import { AssistantAvatar } from './components/ai-message-content/assistant-avatar';
|
import { AssistantAvatar } from './components/ai-message-content/assistant-avatar';
|
||||||
@@ -104,7 +105,8 @@ export function registerAIEffects() {
|
|||||||
customElements.define('action-slides', ActionSlides);
|
customElements.define('action-slides', ActionSlides);
|
||||||
customElements.define('action-text', ActionText);
|
customElements.define('action-text', ActionText);
|
||||||
customElements.define('ai-loading', AILoading);
|
customElements.define('ai-loading', AILoading);
|
||||||
customElements.define('chat-panel-messages', ChatPanelMessages);
|
customElements.define('ai-chat-content', AIChatContent);
|
||||||
|
customElements.define('ai-chat-messages', AIChatMessages);
|
||||||
customElements.define('chat-panel', ChatPanel);
|
customElements.define('chat-panel', ChatPanel);
|
||||||
customElements.define('ai-chat-input', AIChatInput);
|
customElements.define('ai-chat-input', AIChatInput);
|
||||||
customElements.define(
|
customElements.define(
|
||||||
@@ -135,8 +137,8 @@ export function registerAIEffects() {
|
|||||||
EdgelessAIChatBlockComponent
|
EdgelessAIChatBlockComponent
|
||||||
);
|
);
|
||||||
customElements.define('affine-ai-chat', AIChatBlockComponent);
|
customElements.define('affine-ai-chat', AIChatBlockComponent);
|
||||||
customElements.define('ai-chat-message', AIChatMessage);
|
customElements.define('ai-chat-block-message', AIChatBlockMessage);
|
||||||
customElements.define('ai-chat-messages', AIChatMessages);
|
customElements.define('ai-chat-block-messages', AIChatBlockMessages);
|
||||||
customElements.define(
|
customElements.define(
|
||||||
'ai-scrollable-text-renderer',
|
'ai-scrollable-text-renderer',
|
||||||
AIScrollableTextRenderer
|
AIScrollableTextRenderer
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import type { ContextEmbedStatus } from '@affine/graphql';
|
import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
|
||||||
import {
|
import {
|
||||||
CanvasElementType,
|
CanvasElementType,
|
||||||
EdgelessCRUDIdentifier,
|
EdgelessCRUDIdentifier,
|
||||||
@@ -12,9 +12,7 @@ import {
|
|||||||
DocModeProvider,
|
DocModeProvider,
|
||||||
TelemetryProvider,
|
TelemetryProvider,
|
||||||
} from '@blocksuite/affine/shared/services';
|
} from '@blocksuite/affine/shared/services';
|
||||||
import type { Signal } from '@blocksuite/affine/shared/utils';
|
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
import { signal } from '@preact/signals-core';
|
|
||||||
import { html, LitElement, nothing, type PropertyValues } from 'lit';
|
import { html, LitElement, nothing, type PropertyValues } from 'lit';
|
||||||
import { property, query, state } from 'lit/decorators.js';
|
import { property, query, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
@@ -94,10 +92,6 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
|
|
||||||
private _forkBlockId: string | undefined = undefined;
|
private _forkBlockId: string | undefined = undefined;
|
||||||
|
|
||||||
private _forkSessionId: string | undefined = undefined;
|
|
||||||
|
|
||||||
accessor isComposerVisible: Signal<boolean | undefined> = signal(true);
|
|
||||||
|
|
||||||
private readonly _deserializeHistoryChatMessages = (
|
private readonly _deserializeHistoryChatMessages = (
|
||||||
historyMessagesString: string
|
historyMessagesString: string
|
||||||
) => {
|
) => {
|
||||||
@@ -117,14 +111,14 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
|
|
||||||
private readonly _constructBranchChatBlockMessages = async (
|
private readonly _constructBranchChatBlockMessages = async (
|
||||||
rootWorkspaceId: string,
|
rootWorkspaceId: string,
|
||||||
rootDocId: string,
|
forkSessionId: string,
|
||||||
forkSessionId: string
|
docId?: string
|
||||||
) => {
|
) => {
|
||||||
const currentUserInfo = await AIProvider.userInfo;
|
const currentUserInfo = await AIProvider.userInfo;
|
||||||
const forkMessages = (await queryHistoryMessages(
|
const forkMessages = (await queryHistoryMessages(
|
||||||
rootWorkspaceId,
|
rootWorkspaceId,
|
||||||
rootDocId,
|
forkSessionId,
|
||||||
forkSessionId
|
docId
|
||||||
)) as ChatMessage[];
|
)) as ChatMessage[];
|
||||||
const forkLength = forkMessages.length;
|
const forkLength = forkMessages.length;
|
||||||
const historyLength = this._historyMessages.length;
|
const historyLength = this._historyMessages.length;
|
||||||
@@ -163,18 +157,20 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
messages: [],
|
messages: [],
|
||||||
});
|
});
|
||||||
this._forkBlockId = undefined;
|
this._forkBlockId = undefined;
|
||||||
this._forkSessionId = undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _getSessionId = async () => {
|
private readonly initSession = async () => {
|
||||||
return this._forkSessionId ?? this._sessionId;
|
const session = await AIProvider.session?.getSession(
|
||||||
|
this.rootWorkspaceId,
|
||||||
|
this._sessionId
|
||||||
|
);
|
||||||
|
this.session = session ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _createSessionId = async () => {
|
private readonly createForkSession = async () => {
|
||||||
if (this._forkSessionId) {
|
if (this.forkSession) {
|
||||||
return this._forkSessionId;
|
return this.forkSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastMessage = this._historyMessages.at(-1);
|
const lastMessage = this._historyMessages.at(-1);
|
||||||
if (!lastMessage) return;
|
if (!lastMessage) return;
|
||||||
|
|
||||||
@@ -185,8 +181,14 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
sessionId: this._sessionId,
|
sessionId: this._sessionId,
|
||||||
latestMessageId: lastMessage.id,
|
latestMessageId: lastMessage.id,
|
||||||
});
|
});
|
||||||
this._forkSessionId = forkSessionId;
|
if (forkSessionId) {
|
||||||
return this._forkSessionId;
|
const session = await AIProvider.session?.getSession(
|
||||||
|
this.rootWorkspaceId,
|
||||||
|
forkSessionId
|
||||||
|
);
|
||||||
|
this.forkSession = session ?? null;
|
||||||
|
}
|
||||||
|
return this.forkSession;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _onChatSuccess = async () => {
|
private readonly _onChatSuccess = async () => {
|
||||||
@@ -213,7 +215,8 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If there is no session id or chat messages, do not create a new chat block
|
// If there is no session id or chat messages, do not create a new chat block
|
||||||
if (!this._forkSessionId || !this.chatContext.messages.length) {
|
const forkSessionId = this.forkSession?.id;
|
||||||
|
if (!forkSessionId || !this.chatContext.messages.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,8 +233,8 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
const { rootWorkspaceId, rootDocId } = this;
|
const { rootWorkspaceId, rootDocId } = this;
|
||||||
const messages = await this._constructBranchChatBlockMessages(
|
const messages = await this._constructBranchChatBlockMessages(
|
||||||
rootWorkspaceId,
|
rootWorkspaceId,
|
||||||
rootDocId,
|
forkSessionId,
|
||||||
this._forkSessionId
|
rootDocId
|
||||||
);
|
);
|
||||||
if (!messages.length) {
|
if (!messages.length) {
|
||||||
return;
|
return;
|
||||||
@@ -245,7 +248,7 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
{
|
{
|
||||||
xywh: bound.serialize(),
|
xywh: bound.serialize(),
|
||||||
messages: JSON.stringify(messages),
|
messages: JSON.stringify(messages),
|
||||||
sessionId: this._forkSessionId,
|
sessionId: forkSessionId,
|
||||||
rootWorkspaceId: rootWorkspaceId,
|
rootWorkspaceId: rootWorkspaceId,
|
||||||
rootDocId: rootDocId,
|
rootDocId: rootDocId,
|
||||||
},
|
},
|
||||||
@@ -280,7 +283,8 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
* Update the current chat messages with the new message
|
* Update the current chat messages with the new message
|
||||||
*/
|
*/
|
||||||
updateChatBlockMessages = async () => {
|
updateChatBlockMessages = async () => {
|
||||||
if (!this._forkBlockId || !this._forkSessionId) {
|
const forkSessionId = this.forkSession?.id;
|
||||||
|
if (!this._forkBlockId || !forkSessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,8 +296,8 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
const { rootWorkspaceId, rootDocId } = this;
|
const { rootWorkspaceId, rootDocId } = this;
|
||||||
const messages = await this._constructBranchChatBlockMessages(
|
const messages = await this._constructBranchChatBlockMessages(
|
||||||
rootWorkspaceId,
|
rootWorkspaceId,
|
||||||
rootDocId,
|
forkSessionId,
|
||||||
this._forkSessionId
|
rootDocId
|
||||||
);
|
);
|
||||||
if (!messages.length) {
|
if (!messages.length) {
|
||||||
return;
|
return;
|
||||||
@@ -355,8 +359,8 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
*/
|
*/
|
||||||
retry = async () => {
|
retry = async () => {
|
||||||
try {
|
try {
|
||||||
const { _forkBlockId, _forkSessionId } = this;
|
const forkSessionId = this.forkSession?.id;
|
||||||
if (!_forkBlockId || !_forkSessionId) return;
|
if (!this._forkBlockId || !forkSessionId) return;
|
||||||
if (!AIProvider.actions.chat) return;
|
if (!AIProvider.actions.chat) return;
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -376,7 +380,7 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
|
|
||||||
const { store } = this.host;
|
const { store } = this.host;
|
||||||
const stream = await AIProvider.actions.chat({
|
const stream = await AIProvider.actions.chat({
|
||||||
sessionId: _forkSessionId,
|
sessionId: forkSessionId,
|
||||||
retry: true,
|
retry: true,
|
||||||
docId: store.id,
|
docId: store.id,
|
||||||
workspaceId: store.workspace.id,
|
workspaceId: store.workspace.id,
|
||||||
@@ -461,20 +465,20 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`<div class=${messageClasses}>
|
return html`<div class=${messageClasses}>
|
||||||
<ai-chat-message
|
<ai-chat-block-message
|
||||||
.host=${host}
|
.host=${host}
|
||||||
.state=${messageState}
|
.state=${messageState}
|
||||||
.message=${message}
|
.message=${message}
|
||||||
.textRendererOptions=${this._textRendererOptions}
|
.textRendererOptions=${this._textRendererOptions}
|
||||||
></ai-chat-message>
|
></ai-chat-block-message>
|
||||||
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
|
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
|
||||||
${shouldRenderCopyMore
|
${shouldRenderCopyMore
|
||||||
? html` <chat-copy-more
|
? html` <chat-copy-more
|
||||||
.host=${host}
|
.host=${host}
|
||||||
|
.session=${this.forkSession}
|
||||||
.actions=${actions}
|
.actions=${actions}
|
||||||
.content=${markdown}
|
.content=${markdown}
|
||||||
.isLast=${isLastReply}
|
.isLast=${isLastReply}
|
||||||
.getSessionId=${this._getSessionId}
|
|
||||||
.messageId=${message.id ?? undefined}
|
.messageId=${message.id ?? undefined}
|
||||||
.retry=${() => this.retry()}
|
.retry=${() => this.retry()}
|
||||||
></chat-copy-more>`
|
></chat-copy-more>`
|
||||||
@@ -482,9 +486,9 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
${shouldRenderActions
|
${shouldRenderActions
|
||||||
? html`<chat-action-list
|
? html`<chat-action-list
|
||||||
.host=${host}
|
.host=${host}
|
||||||
|
.session=${this.forkSession}
|
||||||
.actions=${actions}
|
.actions=${actions}
|
||||||
.content=${markdown}
|
.content=${markdown}
|
||||||
.getSessionId=${this._getSessionId}
|
|
||||||
.messageId=${message.id ?? undefined}
|
.messageId=${message.id ?? undefined}
|
||||||
.layoutDirection=${'horizontal'}
|
.layoutDirection=${'horizontal'}
|
||||||
></chat-action-list>`
|
></chat-action-list>`
|
||||||
@@ -496,6 +500,7 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
this.initSession().catch(console.error);
|
||||||
const extensions = this.host.std
|
const extensions = this.host.std
|
||||||
.get(ViewExtensionManagerIdentifier)
|
.get(ViewExtensionManagerIdentifier)
|
||||||
.get('preview-page');
|
.get('preview-page');
|
||||||
@@ -507,8 +512,8 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
this._historyMessages = this._deserializeHistoryChatMessages(
|
this._historyMessages = this._deserializeHistoryChatMessages(
|
||||||
this.historyMessagesString
|
this.historyMessagesString
|
||||||
);
|
);
|
||||||
const { rootWorkspaceId, rootDocId, _sessionId } = this;
|
const { rootWorkspaceId, _sessionId } = this;
|
||||||
queryHistoryMessages(rootWorkspaceId, rootDocId, _sessionId)
|
queryHistoryMessages(rootWorkspaceId, _sessionId)
|
||||||
.then(messages => {
|
.then(messages => {
|
||||||
this._historyMessages = this._historyMessages.map((message, idx) => {
|
this._historyMessages = this._historyMessages.map((message, idx) => {
|
||||||
return {
|
return {
|
||||||
@@ -566,17 +571,17 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
<ai-history-clear
|
<ai-history-clear
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.doc=${this.host.store}
|
.doc=${this.host.store}
|
||||||
.getSessionId=${this._getSessionId}
|
.session=${this.forkSession}
|
||||||
.onHistoryCleared=${this._onHistoryCleared}
|
.onHistoryCleared=${this._onHistoryCleared}
|
||||||
.chatContextValue=${chatContext}
|
.chatContextValue=${chatContext}
|
||||||
></ai-history-clear>
|
></ai-history-clear>
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-chat-messages-container">
|
<div class="ai-chat-messages-container">
|
||||||
<ai-chat-messages
|
<ai-chat-block-messages
|
||||||
.host=${host}
|
.host=${host}
|
||||||
.messages=${_historyMessages}
|
.messages=${_historyMessages}
|
||||||
.textRendererOptions=${_textRendererOptions}
|
.textRendererOptions=${_textRendererOptions}
|
||||||
></ai-chat-messages>
|
></ai-chat-block-messages>
|
||||||
<date-time .date=${latestMessageCreatedAt}></date-time>
|
<date-time .date=${latestMessageCreatedAt}></date-time>
|
||||||
<div class="new-chat-messages-container">
|
<div class="new-chat-messages-container">
|
||||||
${this.CurrentMessages(currentChatMessages)}
|
${this.CurrentMessages(currentChatMessages)}
|
||||||
@@ -584,12 +589,11 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<ai-chat-composer
|
<ai-chat-composer
|
||||||
.host=${host}
|
.host=${host}
|
||||||
.doc=${this.host.store}
|
.workspaceId=${this.rootWorkspaceId}
|
||||||
.getSessionId=${this._getSessionId}
|
.session=${this.forkSession ?? this.session}
|
||||||
.createSessionId=${this._createSessionId}
|
.createSession=${this.createForkSession}
|
||||||
.chatContextValue=${chatContext}
|
.chatContextValue=${chatContext}
|
||||||
.updateContext=${updateContext}
|
.updateContext=${updateContext}
|
||||||
.isVisible=${this.isComposerVisible}
|
|
||||||
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
||||||
.networkSearchConfig=${networkSearchConfig}
|
.networkSearchConfig=${networkSearchConfig}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
@@ -647,6 +651,12 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor embeddingProgress: [number, number] = [0, 0];
|
accessor embeddingProgress: [number, number] = [0, 0];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor session: CopilotSessionType | null | undefined;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor forkSession: CopilotSessionType | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
import { captureException } from '@sentry/react';
|
import { captureException } from '@sentry/react';
|
||||||
import { Subject } from 'rxjs';
|
import { BehaviorSubject, Subject } from 'rxjs';
|
||||||
|
|
||||||
import type { ChatContextValue } from '../chat-panel/chat-context';
|
import type { ChatContextValue } from '../components/ai-chat-content';
|
||||||
import {
|
import {
|
||||||
PaymentRequiredError,
|
PaymentRequiredError,
|
||||||
RequestTimeoutError,
|
RequestTimeoutError,
|
||||||
@@ -133,8 +133,8 @@ export class AIProvider {
|
|||||||
// use case: when user selects "continue in chat" in an ask ai result panel
|
// use case: when user selects "continue in chat" in an ask ai result panel
|
||||||
// do we need to pass the context to the chat panel?
|
// do we need to pass the context to the chat panel?
|
||||||
/* eslint-disable rxjs/finnish */
|
/* eslint-disable rxjs/finnish */
|
||||||
requestOpenWithChat: new Subject<AIChatParams>(),
|
requestOpenWithChat: new BehaviorSubject<AIChatParams | null>(null),
|
||||||
requestSendWithChat: new Subject<AISendParams>(),
|
requestSendWithChat: new BehaviorSubject<AISendParams | null>(null),
|
||||||
requestInsertTemplate: new Subject<{
|
requestInsertTemplate: new Subject<{
|
||||||
template: string;
|
template: string;
|
||||||
mode: 'page' | 'edgeless';
|
mode: 'page' | 'edgeless';
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onb
|
|||||||
import type { AuthAccountInfo, AuthService } from '@affine/core/modules/cloud';
|
import type { AuthAccountInfo, AuthService } from '@affine/core/modules/cloud';
|
||||||
import type { GlobalDialogService } from '@affine/core/modules/dialogs';
|
import type { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import {
|
import {
|
||||||
type ChatHistoryOrder,
|
|
||||||
ContextCategories,
|
ContextCategories,
|
||||||
type ContextWorkspaceEmbeddingStatus,
|
type ContextWorkspaceEmbeddingStatus,
|
||||||
type getCopilotHistoriesQuery,
|
type getCopilotHistoriesQuery,
|
||||||
@@ -742,7 +741,7 @@ Could you make a new website based on these notes and send back just the html fi
|
|||||||
AIProvider.provide('histories', {
|
AIProvider.provide('histories', {
|
||||||
actions: async (
|
actions: async (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
docId?: string
|
docId: string
|
||||||
): Promise<BlockSuitePresets.AIHistory[]> => {
|
): Promise<BlockSuitePresets.AIHistory[]> => {
|
||||||
// @ts-expect-error - 'action' is missing in server impl
|
// @ts-expect-error - 'action' is missing in server impl
|
||||||
return (
|
return (
|
||||||
@@ -754,14 +753,15 @@ Could you make a new website based on these notes and send back just the html fi
|
|||||||
},
|
},
|
||||||
chats: async (
|
chats: async (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
docId?: string,
|
sessionId: string,
|
||||||
options?: {
|
docId?: string
|
||||||
sessionId?: string;
|
|
||||||
messageOrder?: ChatHistoryOrder;
|
|
||||||
}
|
|
||||||
): Promise<BlockSuitePresets.AIHistory[]> => {
|
): Promise<BlockSuitePresets.AIHistory[]> => {
|
||||||
// @ts-expect-error - 'action' is missing in server impl
|
// @ts-expect-error - 'action' is missing in server impl
|
||||||
return (await client.getHistories(workspaceId, docId, options)) ?? [];
|
return (
|
||||||
|
(await client.getHistories(workspaceId, docId, {
|
||||||
|
sessionId,
|
||||||
|
})) ?? []
|
||||||
|
);
|
||||||
},
|
},
|
||||||
cleanup: async (
|
cleanup: async (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import type { EditorHost } from '@blocksuite/affine/std';
|
|||||||
import type { BlockModel, Store } from '@blocksuite/affine/store';
|
import type { BlockModel, Store } from '@blocksuite/affine/store';
|
||||||
import { Slice, toDraftModel } from '@blocksuite/affine/store';
|
import { Slice, toDraftModel } from '@blocksuite/affine/store';
|
||||||
|
|
||||||
import type { ChatContextValue } from '../chat-panel/chat-context';
|
import type { ChatContextValue } from '../components/ai-chat-content';
|
||||||
import {
|
import {
|
||||||
getSelectedImagesAsBlobs,
|
getSelectedImagesAsBlobs,
|
||||||
getSelectedTextContent,
|
getSelectedTextContent,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Scrollable } from '@affine/component';
|
import { Scrollable } from '@affine/component';
|
||||||
import { PageDetailLoading } from '@affine/component/page-detail-skeleton';
|
import { PageDetailLoading } from '@affine/component/page-detail-skeleton';
|
||||||
import type { ChatPanel } from '@affine/core/blocksuite/ai';
|
import type { AIChatParams, ChatPanel } from '@affine/core/blocksuite/ai';
|
||||||
import { AIProvider } from '@affine/core/blocksuite/ai';
|
import { AIProvider } from '@affine/core/blocksuite/ai';
|
||||||
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
|
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
|
||||||
import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer';
|
import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer';
|
||||||
@@ -119,7 +119,10 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const disposables: Subscription[] = [];
|
const disposables: Subscription[] = [];
|
||||||
const openHandler = () => {
|
const openHandler = (params: AIChatParams | null) => {
|
||||||
|
if (!params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
workbench.openSidebar();
|
workbench.openSidebar();
|
||||||
view.activeSidebarTab('chat');
|
view.activeSidebarTab('chat');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Scrollable } from '@affine/component';
|
import { Scrollable } from '@affine/component';
|
||||||
import { PageDetailLoading } from '@affine/component/page-detail-skeleton';
|
import { PageDetailLoading } from '@affine/component/page-detail-skeleton';
|
||||||
import { AIProvider } from '@affine/core/blocksuite/ai';
|
import { type AIChatParams, AIProvider } from '@affine/core/blocksuite/ai';
|
||||||
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
|
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
|
||||||
import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer';
|
import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer';
|
||||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||||
@@ -127,7 +127,10 @@ function DocPeekPreviewEditor({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const disposables: Subscription[] = [];
|
const disposables: Subscription[] = [];
|
||||||
const openHandler = () => {
|
const openHandler = (params: AIChatParams | null) => {
|
||||||
|
if (!params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (doc) {
|
if (doc) {
|
||||||
workbench.openDoc(doc.id);
|
workbench.openDoc(doc.id);
|
||||||
peekView.close();
|
peekView.close();
|
||||||
|
|||||||
@@ -343,25 +343,6 @@ test.describe('AIBasic/Chat', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should support clearing chat', async ({
|
|
||||||
loggedInPage: page,
|
|
||||||
utils,
|
|
||||||
}) => {
|
|
||||||
await utils.chatPanel.openChatPanel(page);
|
|
||||||
await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.');
|
|
||||||
await utils.chatPanel.waitForHistory(page, [
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: 'Hello. Answer in 50 words.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'assistant',
|
|
||||||
status: 'success',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
await utils.chatPanel.clearChat(page);
|
|
||||||
await utils.chatPanel.waitForHistory(page, []);
|
|
||||||
});
|
|
||||||
test('should support copying answer', async ({
|
test('should support copying answer', async ({
|
||||||
loggedInPage: page,
|
loggedInPage: page,
|
||||||
utils,
|
utils,
|
||||||
|
|||||||
@@ -460,9 +460,6 @@ test.describe('AISettings/Embedding', () => {
|
|||||||
|
|
||||||
await utils.settings.closeSettingsPanel(page);
|
await utils.settings.closeSettingsPanel(page);
|
||||||
|
|
||||||
// Clear history
|
|
||||||
await utils.chatPanel.clearChat(page);
|
|
||||||
|
|
||||||
// Ignored docs should not be used for embedding
|
// Ignored docs should not be used for embedding
|
||||||
await utils.chatPanel.makeChat(
|
await utils.chatPanel.makeChat(
|
||||||
page,
|
page,
|
||||||
|
|||||||
Reference in New Issue
Block a user