mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-16 13:57:02 +08:00
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 {
|
||||
Bound,
|
||||
@@ -69,14 +68,15 @@ export type ChatAction = {
|
||||
|
||||
export async function queryHistoryMessages(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
forkSessionId: string
|
||||
forkSessionId: string,
|
||||
docId?: string
|
||||
) {
|
||||
// Get fork session messages
|
||||
const histories = await AIProvider.histories?.chats(workspaceId, docId, {
|
||||
sessionId: forkSessionId,
|
||||
messageOrder: ChatHistoryOrder.asc,
|
||||
});
|
||||
const histories = await AIProvider.histories?.chats(
|
||||
workspaceId,
|
||||
forkSessionId,
|
||||
docId
|
||||
);
|
||||
|
||||
if (!histories || !histories.length) {
|
||||
return [];
|
||||
@@ -117,8 +117,8 @@ export async function constructRootChatBlockMessages(
|
||||
const userInfo = await AIProvider.userInfo;
|
||||
const forkMessages = (await queryHistoryMessages(
|
||||
doc.workspace.id,
|
||||
doc.id,
|
||||
forkSessionId
|
||||
forkSessionId,
|
||||
doc.id
|
||||
)) as ChatMessage[];
|
||||
return constructUserInfoWithMessages(forkMessages, userInfo);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type {
|
||||
ChatHistoryOrder,
|
||||
ContextMatchedDocChunk,
|
||||
ContextMatchedFileChunk,
|
||||
ContextWorkspaceEmbeddingStatus,
|
||||
@@ -400,15 +399,12 @@ declare global {
|
||||
// non chat histories
|
||||
actions: (
|
||||
workspaceId: string,
|
||||
docId?: string
|
||||
docId: string
|
||||
) => Promise<AIHistory[] | undefined>;
|
||||
chats: (
|
||||
workspaceId: string,
|
||||
docId?: string,
|
||||
options?: {
|
||||
sessionId?: string;
|
||||
messageOrder?: ChatHistoryOrder;
|
||||
}
|
||||
sessionId: string,
|
||||
docId?: string
|
||||
) => Promise<AIHistory[] | undefined>;
|
||||
cleanup: (
|
||||
workspaceId: string,
|
||||
|
||||
@@ -49,12 +49,12 @@ export class AIChatBlockComponent extends BlockComponent<AIChatBlockModel> {
|
||||
|
||||
return html`<div class="affine-ai-chat-block-container">
|
||||
<div class="ai-chat-messages-container">
|
||||
<ai-chat-messages
|
||||
<ai-chat-block-messages
|
||||
.host=${this.host}
|
||||
.messages=${messages}
|
||||
.textRendererOptions=${this._textRendererOptions}
|
||||
.withMask=${true}
|
||||
></ai-chat-messages>
|
||||
></ai-chat-block-messages>
|
||||
</div>
|
||||
<div class="ai-chat-block-button">
|
||||
${ChatWithAIIcon} <span>AI chat block</span>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '../../../components/ai-chat-messages';
|
||||
import { UserInfoTemplate } from './user-info';
|
||||
|
||||
export class AIChatMessage extends LitElement {
|
||||
export class AIChatBlockMessage extends LitElement {
|
||||
static override styles = css`
|
||||
.ai-chat-message {
|
||||
display: flex;
|
||||
@@ -99,7 +99,7 @@ export class AIChatMessage extends LitElement {
|
||||
accessor textRendererOptions: TextRendererOptions = {};
|
||||
}
|
||||
|
||||
export class AIChatMessages extends LitElement {
|
||||
export class AIChatBlockMessages extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
width: 100%;
|
||||
@@ -123,11 +123,11 @@ export class AIChatMessages extends LitElement {
|
||||
message => message.id || message.createdAt,
|
||||
message => {
|
||||
return html`
|
||||
<ai-chat-message
|
||||
<ai-chat-block-message
|
||||
.host=${this.host}
|
||||
.textRendererOptions=${this.textRendererOptions}
|
||||
.message=${message}
|
||||
></ai-chat-message>
|
||||
></ai-chat-block-message>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
@@ -146,7 +146,7 @@ export class AIChatMessages extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ai-chat-message': AIChatMessage;
|
||||
'ai-chat-messages': AIChatMessages;
|
||||
'ai-chat-block-message': AIChatBlockMessage;
|
||||
'ai-chat-block-messages': AIChatBlockMessages;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import './chat-panel-messages';
|
||||
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
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 { css, html, nothing, type PropertyValues } from 'lit';
|
||||
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 { throttle } from 'lodash-es';
|
||||
|
||||
import type {
|
||||
DocDisplayConfig,
|
||||
@@ -25,31 +22,9 @@ import type {
|
||||
AIPlaygroundConfig,
|
||||
AIReasoningConfig,
|
||||
} from '../components/ai-chat-input';
|
||||
import {
|
||||
type ChatAction,
|
||||
type ChatMessage,
|
||||
type HistoryMessage,
|
||||
} from '../components/ai-chat-messages';
|
||||
import { createPlaygroundModal } from '../components/playground/modal';
|
||||
import { AIProvider } from '../provider';
|
||||
import { extractSelectedContent } from '../utils/extract';
|
||||
import {
|
||||
getSelectedImagesAsBlobs,
|
||||
getSelectedTextContent,
|
||||
} from '../utils/selection-utils';
|
||||
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(
|
||||
WithDisposable(ShadowlessElement)
|
||||
@@ -58,24 +33,10 @@ export class ChatPanel extends SignalWatcher(
|
||||
chat-panel {
|
||||
width: 100%;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.chat-panel-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-panel-title-text {
|
||||
font-size: 14px;
|
||||
@@ -83,153 +44,40 @@ export class ChatPanel extends SignalWatcher(
|
||||
color: var(--affine-text-secondary-color);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--affine-text-secondary-color);
|
||||
.chat-panel-playground {
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
margin-left: 8px;
|
||||
margin-right: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-panel-playground:hover svg {
|
||||
color: ${unsafeCSSVarV2('icon/activated')};
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
margin-left: 8px;
|
||||
margin-right: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-panel-playground:hover svg {
|
||||
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 })
|
||||
accessor host!: EditorHost;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor doc!: Store;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor reasoningConfig!: AIReasoningConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor playgroundConfig!: AIPlaygroundConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor appSidebarConfig!: AppSidebarConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor reasoningConfig!: AIReasoningConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor searchMenuConfig!: SearchMenuConfig;
|
||||
|
||||
@@ -246,56 +94,74 @@ export class ChatPanel extends SignalWatcher(
|
||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||
|
||||
@state()
|
||||
accessor isLoading = false;
|
||||
|
||||
@state()
|
||||
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
||||
accessor session: CopilotSessionType | null | undefined;
|
||||
|
||||
@state()
|
||||
accessor embeddingProgress: [number, number] = [0, 0];
|
||||
|
||||
@state()
|
||||
accessor session: CopilotSessionType | undefined = undefined;
|
||||
private isSidebarOpen: Signal<boolean | undefined> = signal(false);
|
||||
|
||||
private _isInitialized = false;
|
||||
private sidebarWidth: Signal<number | undefined> = signal(undefined);
|
||||
|
||||
private _isSidebarOpen: Signal<boolean | undefined> = signal(false);
|
||||
|
||||
private _sidebarWidth: Signal<number | undefined> = signal(undefined);
|
||||
|
||||
private readonly _scrollToEnd = () => {
|
||||
if (!this._wheelTriggered) {
|
||||
this._chatMessagesRef.value?.scrollToEnd();
|
||||
private readonly initSession = async () => {
|
||||
if (this.session) {
|
||||
return this.session;
|
||||
}
|
||||
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 {
|
||||
if (!this._isSidebarOpen.value) return;
|
||||
if (this.isLoading) return;
|
||||
const userId = (await AIProvider.userInfo)?.id;
|
||||
if (!userId) return;
|
||||
|
||||
this.isLoading = true;
|
||||
await this._updateHistory();
|
||||
this.isLoading = false;
|
||||
this._isInitialized = true;
|
||||
if (!this.isSidebarOpen.value) {
|
||||
return;
|
||||
}
|
||||
await this.initSession();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _resetPanel = () => {
|
||||
private readonly resetPanel = () => {
|
||||
this.session = undefined;
|
||||
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
||||
this.isLoading = false;
|
||||
this._isInitialized = false;
|
||||
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`
|
||||
<playground-content
|
||||
.host=${this.host}
|
||||
@@ -314,48 +180,10 @@ export class ChatPanel extends SignalWatcher(
|
||||
createPlaygroundModal(playgroundContent, 'AI Playground');
|
||||
};
|
||||
|
||||
protected override willUpdate(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('doc')) {
|
||||
this._resetPanel();
|
||||
requestAnimationFrame(async () => {
|
||||
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);
|
||||
protected override updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has('doc')) {
|
||||
this.resetPanel();
|
||||
this.initPanel().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,135 +191,80 @@ export class ChatPanel extends SignalWatcher(
|
||||
super.connectedCallback();
|
||||
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(
|
||||
AIProvider.slots.userInfo.subscribe(() => {
|
||||
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);
|
||||
}
|
||||
this.resetPanel();
|
||||
this.initPanel().catch(console.error);
|
||||
})
|
||||
);
|
||||
|
||||
const isOpen = this.appSidebarConfig.isOpen();
|
||||
this._isSidebarOpen = isOpen.signal;
|
||||
this.isSidebarOpen = isOpen.signal;
|
||||
this._disposables.add(isOpen.cleanup);
|
||||
|
||||
const width = this.appSidebarConfig.getWidth();
|
||||
this._sidebarWidth = width.signal;
|
||||
this.sidebarWidth = width.signal;
|
||||
this._disposables.add(width.cleanup);
|
||||
|
||||
this._disposables.add(
|
||||
this._isSidebarOpen.subscribe(isOpen => {
|
||||
if (isOpen && !this._isInitialized) {
|
||||
this._initPanel().catch(console.error);
|
||||
this.isSidebarOpen.subscribe(() => {
|
||||
if (this.session === undefined) {
|
||||
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() {
|
||||
const width = this._sidebarWidth.value || 0;
|
||||
const isInitialized = this.session !== undefined;
|
||||
if (!isInitialized) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const width = this.sidebarWidth.value || 0;
|
||||
const style = styleMap({
|
||||
padding: width > 540 ? '8px 24px 0 24px' : '8px 12px 0 12px',
|
||||
});
|
||||
const [done, total] = this.embeddingProgress;
|
||||
const isEmbedding = total > 0 && done < total;
|
||||
const title = html`
|
||||
<div class="chat-panel-title-text">
|
||||
${isEmbedding
|
||||
? html`<span data-testid="chat-panel-embedding-progress"
|
||||
>Embedding ${done}/${total}</span
|
||||
>`
|
||||
: 'AFFiNE AI'}
|
||||
</div>
|
||||
${this.playgroundConfig.visible.value
|
||||
? html`
|
||||
<div class="chat-panel-playground" @click=${this.openPlayground}>
|
||||
${CenterPeekIcon()}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
|
||||
return html`<div class="chat-panel-container" style=${style}>
|
||||
<div class="chat-panel-title">
|
||||
<div class="chat-panel-title-text">
|
||||
${isEmbedding
|
||||
? html`<span data-testid="chat-panel-embedding-progress"
|
||||
>Embedding ${done}/${total}</span
|
||||
>`
|
||||
: 'AFFiNE AI'}
|
||||
</div>
|
||||
${this.playgroundConfig.visible.value
|
||||
? html`
|
||||
<div class="chat-panel-playground" @click=${this._openPlayground}>
|
||||
${CenterPeekIcon()}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ai-history-clear
|
||||
${keyed(
|
||||
this.doc.id,
|
||||
html`<ai-chat-content
|
||||
.chatTitle=${title}
|
||||
.host=${this.host}
|
||||
.doc=${this.doc}
|
||||
.getSessionId=${this._getSessionId}
|
||||
.onHistoryCleared=${this._updateHistory}
|
||||
.chatContextValue=${this.chatContextValue}
|
||||
></ai-history-clear>
|
||||
</div>
|
||||
<chat-panel-messages
|
||||
${ref(this._chatMessagesRef)}
|
||||
.chatContextValue=${this.chatContextValue}
|
||||
.getSessionId=${this._getSessionId}
|
||||
.createSessionId=${this._createSessionId}
|
||||
.updateContext=${this.updateContext}
|
||||
.host=${this.host}
|
||||
.isLoading=${this.isLoading}
|
||||
.extensions=${this.extensions}
|
||||
.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}
|
||||
.trackOptions=${{
|
||||
where: 'chat-panel',
|
||||
control: 'chat-send',
|
||||
}}
|
||||
.panelWidth=${this._sidebarWidth}
|
||||
></ai-chat-composer>
|
||||
.session=${this.session}
|
||||
.createSession=${this.createSession}
|
||||
.workspaceId=${this.doc.workspace.id}
|
||||
.docId=${this.doc.id}
|
||||
.networkSearchConfig=${this.networkSearchConfig}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.updateEmbeddingProgress=${this.updateEmbeddingProgress}
|
||||
.width=${this.sidebarWidth}
|
||||
></ai-chat-content>`
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { CopilotSessionType } from '@affine/graphql';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
@@ -53,7 +54,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor getSessionId!: () => Promise<string | undefined>;
|
||||
accessor session!: CopilotSessionType | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor retry!: () => void;
|
||||
@@ -62,7 +63,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
accessor testId = 'chat-message-assistant';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor panelWidth!: Signal<number | undefined>;
|
||||
accessor width: Signal<number | undefined> | undefined;
|
||||
|
||||
get state() {
|
||||
const { isLast, status } = this;
|
||||
@@ -117,7 +118,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
.answer=${answer}
|
||||
.host=${this.host}
|
||||
.state=${this.state}
|
||||
.width=${this.panelWidth}
|
||||
.width=${this.width}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
></chat-content-stream-objects>`;
|
||||
@@ -134,7 +135,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
}
|
||||
|
||||
private renderEditorActions() {
|
||||
const { item, isLast, status } = this;
|
||||
const { item, isLast, status, host, session } = this;
|
||||
|
||||
if (!isChatMessage(item) || item.role !== 'assistant') return nothing;
|
||||
|
||||
@@ -146,7 +147,6 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
)
|
||||
return nothing;
|
||||
|
||||
const { host } = this;
|
||||
const { content, streamObjects, id: messageId } = item;
|
||||
const markdown = streamObjects?.length
|
||||
? mergeStreamContent(streamObjects)
|
||||
@@ -159,10 +159,10 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
return html`
|
||||
<chat-copy-more
|
||||
.host=${host}
|
||||
.session=${session}
|
||||
.actions=${actions}
|
||||
.content=${markdown}
|
||||
.isLast=${isLast}
|
||||
.getSessionId=${this.getSessionId}
|
||||
.messageId=${messageId}
|
||||
.withMargin=${true}
|
||||
.retry=${() => this.retry()}
|
||||
@@ -171,8 +171,8 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
? html`<chat-action-list
|
||||
.actions=${actions}
|
||||
.host=${host}
|
||||
.session=${session}
|
||||
.content=${markdown}
|
||||
.getSessionId=${this.getSessionId}
|
||||
.messageId=${messageId ?? undefined}
|
||||
.withMargin=${true}
|
||||
></chat-action-list>`
|
||||
|
||||
@@ -12,9 +12,7 @@ import type {
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import { type Signal, signal } from '@preact/signals-core';
|
||||
import { css, html, type PropertyValues } from 'lit';
|
||||
import { css, html } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
|
||||
import { AIProvider } from '../../provider';
|
||||
@@ -56,16 +54,13 @@ export class AIChatComposer extends SignalWatcher(
|
||||
accessor host!: EditorHost;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor doc!: Store;
|
||||
accessor workspaceId!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor session!: CopilotSessionType | undefined;
|
||||
accessor session!: CopilotSessionType | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor getSessionId!: () => Promise<string | undefined>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor createSessionId!: () => Promise<string | undefined>;
|
||||
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor chatContextValue!: AIChatInputContext;
|
||||
@@ -73,9 +68,6 @@ export class AIChatComposer extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor isVisible: Signal<boolean | undefined> = signal(false);
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor updateEmbeddingProgress!: (
|
||||
count: Record<ContextEmbedStatus, number>
|
||||
@@ -102,9 +94,6 @@ export class AIChatComposer extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor portalContainer: HTMLElement | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor panelWidth: Signal<number | undefined> = signal(undefined);
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||
|
||||
@@ -114,10 +103,6 @@ export class AIChatComposer extends SignalWatcher(
|
||||
@state()
|
||||
accessor embeddingCompleted = false;
|
||||
|
||||
private _isInitialized = false;
|
||||
|
||||
private _isLoading = false;
|
||||
|
||||
private _contextId: string | undefined = undefined;
|
||||
|
||||
private _pollAbortController: AbortController | null = null;
|
||||
@@ -141,9 +126,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
.host=${this.host}
|
||||
.chips=${this.chips}
|
||||
.session=${this.session}
|
||||
.getSessionId=${this.getSessionId}
|
||||
.createSessionId=${this.createSessionId}
|
||||
.getContextId=${this._getContextId}
|
||||
.createSession=${this.createSession}
|
||||
.chatContextValue=${this.chatContextValue}
|
||||
.updateContext=${this.updateContext}
|
||||
.networkSearchConfig=${this.networkSearchConfig}
|
||||
@@ -151,7 +134,6 @@ export class AIChatComposer extends SignalWatcher(
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.onChatSuccess=${this.onChatSuccess}
|
||||
.trackOptions=${this.trackOptions}
|
||||
.panelWidth=${this.panelWidth}
|
||||
.addImages=${this.addImages}
|
||||
></ai-chat-input>
|
||||
<div class="chat-panel-footer">
|
||||
@@ -173,25 +155,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
|
||||
override 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._disposables.add(
|
||||
this.isVisible.subscribe(isVisible => {
|
||||
if (isVisible && !this._isInitialized) {
|
||||
this._initComposer().catch(console.error);
|
||||
}
|
||||
if (!isVisible) {
|
||||
this._abortPoll();
|
||||
this._abortPollEmbeddingStatus();
|
||||
}
|
||||
})
|
||||
);
|
||||
this._initComposer().catch(console.error);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
@@ -200,25 +164,16 @@ export class AIChatComposer extends SignalWatcher(
|
||||
this._abortPollEmbeddingStatus();
|
||||
}
|
||||
|
||||
protected override willUpdate(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('doc')) {
|
||||
this._resetComposer();
|
||||
requestAnimationFrame(async () => {
|
||||
await this._initComposer();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private readonly _getContextId = async () => {
|
||||
if (this._contextId) {
|
||||
return this._contextId;
|
||||
}
|
||||
|
||||
const sessionId = await this.getSessionId();
|
||||
const sessionId = this.session?.id;
|
||||
if (!sessionId) return;
|
||||
|
||||
const contextId = await AIProvider.context?.getContextId(
|
||||
this.doc.workspace.id,
|
||||
this.workspaceId,
|
||||
sessionId
|
||||
);
|
||||
this._contextId = contextId;
|
||||
@@ -230,11 +185,11 @@ export class AIChatComposer extends SignalWatcher(
|
||||
return this._contextId;
|
||||
}
|
||||
|
||||
const sessionId = await this.createSessionId();
|
||||
const sessionId = (await this.createSession())?.id;
|
||||
if (!sessionId) return;
|
||||
|
||||
this._contextId = await AIProvider.context?.createContext(
|
||||
this.doc.workspace.id,
|
||||
this.workspaceId,
|
||||
sessionId
|
||||
);
|
||||
return this._contextId;
|
||||
@@ -242,7 +197,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
|
||||
private readonly _initChips = async () => {
|
||||
// context not initialized
|
||||
const sessionId = await this.getSessionId();
|
||||
const sessionId = this.session?.id;
|
||||
const contextId = await this._getContextId();
|
||||
if (!sessionId || !contextId) {
|
||||
return;
|
||||
@@ -255,7 +210,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
tags = [],
|
||||
collections = [],
|
||||
} = (await AIProvider.context?.getContextDocsAndFiles(
|
||||
this.doc.workspace.id,
|
||||
this.workspaceId,
|
||||
sessionId,
|
||||
contextId
|
||||
)) || {};
|
||||
@@ -319,7 +274,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
};
|
||||
|
||||
private readonly _pollContextDocsAndFiles = async () => {
|
||||
const sessionId = await this.getSessionId();
|
||||
const sessionId = this.session?.id;
|
||||
const contextId = await this._getContextId();
|
||||
if (!sessionId || !contextId || !AIProvider.context) {
|
||||
return;
|
||||
@@ -330,7 +285,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
}
|
||||
this._pollAbortController = new AbortController();
|
||||
await AIProvider.context.pollContextDocsAndFiles(
|
||||
this.doc.workspace.id,
|
||||
this.workspaceId,
|
||||
sessionId,
|
||||
contextId,
|
||||
this._onPoll,
|
||||
@@ -436,13 +391,9 @@ export class AIChatComposer extends SignalWatcher(
|
||||
};
|
||||
|
||||
private readonly _initComposer = async () => {
|
||||
if (!this.isVisible.value) return;
|
||||
if (this._isLoading) return;
|
||||
|
||||
const userId = (await AIProvider.userInfo)?.id;
|
||||
if (!userId) return;
|
||||
if (!userId || !this.session) return;
|
||||
|
||||
this._isLoading = true;
|
||||
await this._initChips();
|
||||
const needPoll = this.chips.some(
|
||||
chip =>
|
||||
@@ -452,16 +403,5 @@ export class AIChatComposer extends SignalWatcher(
|
||||
await this._pollContextDocsAndFiles();
|
||||
}
|
||||
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 {
|
||||
ChatStatus,
|
||||
HistoryMessage,
|
||||
} from '../components/ai-chat-messages';
|
||||
import type { AIError } from '../provider';
|
||||
import type { AIError } from '../../provider';
|
||||
import type { ChatStatus, HistoryMessage } from '../ai-chat-messages';
|
||||
|
||||
export type ChatContextValue = {
|
||||
// history messages of the chat
|
||||
@@ -6,14 +6,13 @@ import { openFilesWith } from '@blocksuite/affine/shared/utils';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import { ArrowUpBigIcon, CloseIcon, ImageIcon } from '@blocksuite/icons/lit';
|
||||
import { type Signal, signal } from '@preact/signals-core';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
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 { readBlobAsURL } from '../../utils/image';
|
||||
import { mergeStreamObjects } from '../../utils/stream-objects';
|
||||
@@ -285,7 +284,7 @@ export class AIChatInput extends SignalWatcher(
|
||||
accessor host!: EditorHost;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor session!: CopilotSessionType | undefined;
|
||||
accessor session!: CopilotSessionType | null | undefined;
|
||||
|
||||
@query('image-preview-grid')
|
||||
accessor imagePreviewGrid: HTMLDivElement | null = null;
|
||||
@@ -309,13 +308,7 @@ export class AIChatInput extends SignalWatcher(
|
||||
accessor chips: ChatChip[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor getSessionId!: () => Promise<string | undefined>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor createSessionId!: () => Promise<string | undefined>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor getContextId!: () => Promise<string | undefined>;
|
||||
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
||||
@@ -341,9 +334,6 @@ export class AIChatInput extends SignalWatcher(
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId = 'chat-panel-input-container';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor panelWidth: Signal<number | undefined> = signal(undefined);
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor addImages!: (images: File[]) => void;
|
||||
|
||||
@@ -366,9 +356,15 @@ export class AIChatInput extends SignalWatcher(
|
||||
super.connectedCallback();
|
||||
this._disposables.add(
|
||||
AIProvider.slots.requestSendWithChat.subscribe(
|
||||
({ input, context, host }) => {
|
||||
(params: AISendParams | null) => {
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
const { input, context, host } = params;
|
||||
if (this.host === host) {
|
||||
context && this.updateContext(context);
|
||||
if (context) {
|
||||
this.updateContext(context);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.send(input).catch(console.error);
|
||||
}, 0);
|
||||
@@ -591,7 +587,7 @@ export class AIChatInput extends SignalWatcher(
|
||||
// optimistic update messages
|
||||
await this._preUpdateMessages(userInput, attachments);
|
||||
|
||||
const sessionId = await this.createSessionId();
|
||||
const sessionId = (await this.createSession())?.id;
|
||||
let contexts = await this._getMatchedContexts();
|
||||
if (abortController.signal.aborted) {
|
||||
return;
|
||||
@@ -678,11 +674,13 @@ export class AIChatInput extends SignalWatcher(
|
||||
};
|
||||
|
||||
private readonly _postUpdateMessages = async () => {
|
||||
const sessionId = this.session?.id;
|
||||
if (!sessionId || !AIProvider.histories) return;
|
||||
|
||||
const { messages } = this.chatContextValue;
|
||||
const last = messages[messages.length - 1] as ChatMessage;
|
||||
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.id,
|
||||
{ sessionId }
|
||||
|
||||
@@ -50,7 +50,7 @@ export class ChatInputPreference extends SignalWatcher(
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor session!: CopilotSessionType | undefined;
|
||||
accessor session!: CopilotSessionType | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onModelChange: ((modelId: string) => void) | undefined;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CopilotSessionType } from '@affine/graphql';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import {
|
||||
DocModeProvider,
|
||||
@@ -13,25 +14,21 @@ import { property, query, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
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 {
|
||||
AINetworkSearchConfig,
|
||||
AIReasoningConfig,
|
||||
} from '../components/ai-chat-input';
|
||||
import {
|
||||
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';
|
||||
} from '../ai-chat-input';
|
||||
import { isChatAction, isChatMessage, StreamObjectSchema } from './type';
|
||||
|
||||
export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
chat-panel-messages {
|
||||
ai-chat-messages {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -147,16 +144,16 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
accessor host!: EditorHost;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor isLoading!: boolean;
|
||||
accessor isHistoryLoading!: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor chatContextValue!: ChatContextValue;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor getSessionId!: () => Promise<string | undefined>;
|
||||
accessor session!: CopilotSessionType | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor createSessionId!: () => Promise<string | undefined>;
|
||||
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
||||
@@ -174,7 +171,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
accessor reasoningConfig!: AIReasoningConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor panelWidth!: Signal<number | undefined>;
|
||||
accessor width: Signal<number | undefined> | undefined;
|
||||
|
||||
@query('.chat-panel-messages-container')
|
||||
accessor messagesContainer: HTMLDivElement | null = null;
|
||||
@@ -202,7 +199,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
}
|
||||
|
||||
private _renderAIOnboarding() {
|
||||
return this.isLoading ||
|
||||
return this.isHistoryLoading ||
|
||||
!this.host?.store.get(FeatureFlagService).getFlag('enable_ai_onboarding')
|
||||
? nothing
|
||||
: html`<div class="onboarding-wrapper" data-testid="ai-onboarding">
|
||||
@@ -241,7 +238,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
protected override render() {
|
||||
const { messages, status, error } = this.chatContextValue;
|
||||
const { isLoading } = this;
|
||||
const { isHistoryLoading } = this;
|
||||
const filteredItems = messages.filter(item => {
|
||||
return (
|
||||
isChatMessage(item) ||
|
||||
@@ -268,12 +265,15 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
data-testid="chat-panel-messages-placeholder"
|
||||
>
|
||||
${AffineIcon(
|
||||
isLoading
|
||||
isHistoryLoading
|
||||
? 'var(--affine-icon-secondary)'
|
||||
: 'var(--affine-primary-color)'
|
||||
)}
|
||||
<div class="messages-placeholder-title" data-loading=${isLoading}>
|
||||
${this.isLoading
|
||||
<div
|
||||
class="messages-placeholder-title"
|
||||
data-loading=${isHistoryLoading}
|
||||
>
|
||||
${this.isHistoryLoading
|
||||
? html`<span data-testid="chat-panel-loading-state"
|
||||
>AFFiNE AI is loading history...</span
|
||||
>`
|
||||
@@ -295,15 +295,15 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
} else if (isChatMessage(item) && item.role === 'assistant') {
|
||||
return html`<chat-message-assistant
|
||||
.host=${this.host}
|
||||
.session=${this.session}
|
||||
.item=${item}
|
||||
.isLast=${isLast}
|
||||
.status=${isLast ? status : 'idle'}
|
||||
.error=${isLast ? error : null}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.getSessionId=${this.getSessionId}
|
||||
.retry=${() => this.retry()}
|
||||
.panelWidth=${this.panelWidth}
|
||||
.width=${this.width}
|
||||
></chat-message-assistant>`;
|
||||
} else if (isChatAction(item)) {
|
||||
return html`<chat-message-action
|
||||
@@ -365,7 +365,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
}
|
||||
|
||||
protected override updated(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('isLoading')) {
|
||||
if (_changedProperties.has('isHistoryLoading')) {
|
||||
this.canScrollDown = false;
|
||||
}
|
||||
}
|
||||
@@ -382,7 +382,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
retry = async () => {
|
||||
try {
|
||||
const sessionId = await this.createSessionId();
|
||||
const sessionId = (await this.createSession())?.id;
|
||||
if (!sessionId) 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';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CopilotSessionType } from '@affine/graphql';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { NotificationProvider } from '@blocksuite/affine/shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
@@ -7,15 +8,15 @@ import type { Store } from '@blocksuite/affine/store';
|
||||
import { css, html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { ChatContextValue } from '../../chat-panel/chat-context';
|
||||
import { AIProvider } from '../../provider';
|
||||
import type { ChatContextValue } from '../ai-chat-content';
|
||||
|
||||
export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor chatContextValue!: ChatContextValue;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor getSessionId!: () => Promise<string | undefined>;
|
||||
accessor session!: CopilotSessionType | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor host!: EditorHost;
|
||||
@@ -41,15 +42,16 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
|
||||
return (
|
||||
this.chatContextValue.status === 'loading' ||
|
||||
this.chatContextValue.status === 'transmitting' ||
|
||||
!this.chatContextValue.messages.length
|
||||
!this.chatContextValue.messages.length ||
|
||||
!this.session
|
||||
);
|
||||
}
|
||||
|
||||
private readonly _cleanupHistories = async () => {
|
||||
if (this._isHistoryClearDisabled) {
|
||||
if (this._isHistoryClearDisabled || !this.session) {
|
||||
return;
|
||||
}
|
||||
const sessionId = await this.getSessionId();
|
||||
const sessionId = this.session.id;
|
||||
const notification = this.host.std.getOptional(NotificationProvider);
|
||||
if (!notification) return;
|
||||
try {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CopilotSessionType } from '@affine/graphql';
|
||||
import type { ImageSelection } from '@blocksuite/affine/shared/selection';
|
||||
import { NotificationProvider } from '@blocksuite/affine/shared/services';
|
||||
import type {
|
||||
@@ -81,7 +82,7 @@ export class ChatActionList extends LitElement {
|
||||
accessor content: string = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor getSessionId!: () => Promise<string | undefined>;
|
||||
accessor session!: CopilotSessionType | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor messageId: string | undefined = undefined;
|
||||
@@ -138,7 +139,7 @@ export class ChatActionList extends LitElement {
|
||||
blocks: this._currentBlockSelections,
|
||||
images: this._currentImageSelections,
|
||||
};
|
||||
const sessionId = await this.getSessionId();
|
||||
const sessionId = this.session?.id;
|
||||
const success = await action.handler(
|
||||
host,
|
||||
content,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CopilotSessionType } from '@affine/graphql';
|
||||
import { Tooltip } from '@blocksuite/affine/components/toolbar';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { noop } from '@blocksuite/affine/global/utils';
|
||||
@@ -110,10 +111,10 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
|
||||
accessor actions: ChatAction[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor content!: string;
|
||||
accessor session!: CopilotSessionType | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor getSessionId!: () => Promise<string | undefined>;
|
||||
accessor content!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor messageId: string | undefined = undefined;
|
||||
@@ -221,7 +222,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
|
||||
};
|
||||
return html`<div
|
||||
@click=${async () => {
|
||||
const sessionId = await this.getSessionId();
|
||||
const sessionId = this.session?.id;
|
||||
const success = await action.handler(
|
||||
host,
|
||||
content,
|
||||
|
||||
@@ -6,23 +6,22 @@ import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import type { ExtensionType, Store } from '@blocksuite/affine/store';
|
||||
import { DeleteIcon, NewPageIcon } from '@blocksuite/icons/lit';
|
||||
import { type Signal, signal } from '@preact/signals-core';
|
||||
import { css, html, type PropertyValues } 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 { 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 type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
|
||||
import type { ChatContextValue } from '../ai-chat-content';
|
||||
import type {
|
||||
AINetworkSearchConfig,
|
||||
AIPlaygroundConfig,
|
||||
AIReasoningConfig,
|
||||
} from '../ai-chat-input';
|
||||
import {
|
||||
type AIChatMessages,
|
||||
type ChatAction,
|
||||
type ChatMessage,
|
||||
type HistoryMessage,
|
||||
@@ -74,7 +73,7 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
}
|
||||
}
|
||||
|
||||
chat-panel-messages {
|
||||
ai-chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
@@ -129,6 +128,9 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor doc!: Store;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor session!: CopilotSessionType | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||
|
||||
@@ -153,9 +155,6 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor session: CopilotSessionType | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor addChat!: () => Promise<void>;
|
||||
|
||||
@@ -168,10 +167,8 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
@state()
|
||||
accessor embeddingProgress: [number, number] = [0, 0];
|
||||
|
||||
private readonly _isVisible: Signal<boolean | undefined> = signal(true);
|
||||
|
||||
private readonly _chatMessagesRef: Ref<ChatPanelMessages> =
|
||||
createRef<ChatPanelMessages>();
|
||||
private readonly _chatMessagesRef: Ref<AIChatMessages> =
|
||||
createRef<AIChatMessages>();
|
||||
|
||||
// request counter to track the latest request
|
||||
private _updateHistoryCounter = 0;
|
||||
@@ -185,22 +182,29 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
this.isLoading = false;
|
||||
};
|
||||
|
||||
private readonly _getSessionId = async () => {
|
||||
return this.session?.id;
|
||||
};
|
||||
|
||||
private readonly _createSessionId = async () => {
|
||||
return this.session?.id;
|
||||
private readonly _createSession = async () => {
|
||||
return this.session;
|
||||
};
|
||||
|
||||
private readonly _updateHistory = async () => {
|
||||
const { doc } = this;
|
||||
if (!AIProvider.histories) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRequest = ++this._updateHistoryCounter;
|
||||
|
||||
const sessionId = this.session?.id;
|
||||
const [histories, actions] = await Promise.all([
|
||||
AIProvider.histories?.chats(doc.workspace.id, doc.id),
|
||||
AIProvider.histories?.actions(doc.workspace.id, doc.id),
|
||||
sessionId
|
||||
? 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
|
||||
@@ -211,12 +215,8 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
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);
|
||||
}
|
||||
const chatMessages = (histories?.[0]?.messages || []) as ChatMessage[];
|
||||
messages.push(...chatMessages);
|
||||
|
||||
this.chatContextValue = {
|
||||
...this.chatContextValue,
|
||||
@@ -289,35 +289,33 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
<ai-history-clear
|
||||
.host=${this.host}
|
||||
.doc=${this.doc}
|
||||
.getSessionId=${this._getSessionId}
|
||||
.session=${this.session}
|
||||
.onHistoryCleared=${this._updateHistory}
|
||||
.chatContextValue=${this.chatContextValue}
|
||||
></ai-history-clear>
|
||||
<div class="chat-panel-delete">${DeleteIcon()}</div>
|
||||
</div>
|
||||
<chat-panel-messages
|
||||
<ai-chat-messages
|
||||
${ref(this._chatMessagesRef)}
|
||||
.chatContextValue=${this.chatContextValue}
|
||||
.getSessionId=${this._getSessionId}
|
||||
.createSessionId=${this._createSessionId}
|
||||
.updateContext=${this.updateContext}
|
||||
.host=${this.host}
|
||||
.isLoading=${this.isLoading}
|
||||
.isHistoryLoading=${this.isLoading}
|
||||
.chatContextValue=${this.chatContextValue}
|
||||
.session=${this.session}
|
||||
.createSession=${this._createSession}
|
||||
.updateContext=${this.updateContext}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.networkSearchConfig=${this.networkSearchConfig}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
></chat-panel-messages>
|
||||
></ai-chat-messages>
|
||||
<ai-chat-composer
|
||||
.host=${this.host}
|
||||
.doc=${this.doc}
|
||||
.workspaceId=${this.doc.workspace.id}
|
||||
.session=${this.session}
|
||||
.getSessionId=${this._getSessionId}
|
||||
.createSessionId=${this._createSessionId}
|
||||
.createSession=${this._createSession}
|
||||
.chatContextValue=${this.chatContextValue}
|
||||
.updateContext=${this.updateContext}
|
||||
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
||||
.isVisible=${this._isVisible}
|
||||
.networkSearchConfig=${this.networkSearchConfig}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.playgroundConfig=${this.playgroundConfig}
|
||||
|
||||
@@ -327,6 +327,7 @@ export class PlaygroundContent extends SignalWatcher(
|
||||
<playground-chat
|
||||
.host=${this.host}
|
||||
.doc=${this.doc}
|
||||
.session=${session}
|
||||
.networkSearchConfig=${this.networkSearchConfig}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.playgroundConfig=${this.playgroundConfig}
|
||||
@@ -335,7 +336,6 @@ export class PlaygroundContent extends SignalWatcher(
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.session=${session}
|
||||
.addChat=${this.addChat}
|
||||
></playground-chat>
|
||||
</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 { LitTranscriptionBlock } from './blocks/ai-chat-block/ai-transcription-block';
|
||||
import {
|
||||
AIChatMessage,
|
||||
AIChatMessages,
|
||||
AIChatBlockMessage,
|
||||
AIChatBlockMessages,
|
||||
} from './blocks/ai-chat-block/components/ai-chat-messages';
|
||||
import {
|
||||
ChatImage,
|
||||
@@ -20,7 +20,6 @@ import { ActionMindmap } from './chat-panel/actions/mindmap';
|
||||
import { ActionSlides } from './chat-panel/actions/slides';
|
||||
import { ActionText } from './chat-panel/actions/text';
|
||||
import { AILoading } from './chat-panel/ai-loading';
|
||||
import { ChatPanelMessages } from './chat-panel/chat-panel-messages';
|
||||
import { ChatMessageAction } from './chat-panel/message/action';
|
||||
import { ChatMessageAssistant } from './chat-panel/message/assistant';
|
||||
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 { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
|
||||
import { AIChatComposer } from './components/ai-chat-composer';
|
||||
import { AIChatContent } from './components/ai-chat-content';
|
||||
import { AIChatInput } from './components/ai-chat-input';
|
||||
import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embedding-status-tooltip';
|
||||
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 { effects as componentAiItemEffects } from './components/ai-item';
|
||||
import { AssistantAvatar } from './components/ai-message-content/assistant-avatar';
|
||||
@@ -104,7 +105,8 @@ export function registerAIEffects() {
|
||||
customElements.define('action-slides', ActionSlides);
|
||||
customElements.define('action-text', ActionText);
|
||||
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('ai-chat-input', AIChatInput);
|
||||
customElements.define(
|
||||
@@ -135,8 +137,8 @@ export function registerAIEffects() {
|
||||
EdgelessAIChatBlockComponent
|
||||
);
|
||||
customElements.define('affine-ai-chat', AIChatBlockComponent);
|
||||
customElements.define('ai-chat-message', AIChatMessage);
|
||||
customElements.define('ai-chat-messages', AIChatMessages);
|
||||
customElements.define('ai-chat-block-message', AIChatBlockMessage);
|
||||
customElements.define('ai-chat-block-messages', AIChatBlockMessages);
|
||||
customElements.define(
|
||||
'ai-scrollable-text-renderer',
|
||||
AIScrollableTextRenderer
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { ContextEmbedStatus } from '@affine/graphql';
|
||||
import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
|
||||
import {
|
||||
CanvasElementType,
|
||||
EdgelessCRUDIdentifier,
|
||||
@@ -12,9 +12,7 @@ import {
|
||||
DocModeProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import type { Signal } from '@blocksuite/affine/shared/utils';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { html, LitElement, nothing, type PropertyValues } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
@@ -94,10 +92,6 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
|
||||
private _forkBlockId: string | undefined = undefined;
|
||||
|
||||
private _forkSessionId: string | undefined = undefined;
|
||||
|
||||
accessor isComposerVisible: Signal<boolean | undefined> = signal(true);
|
||||
|
||||
private readonly _deserializeHistoryChatMessages = (
|
||||
historyMessagesString: string
|
||||
) => {
|
||||
@@ -117,14 +111,14 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
|
||||
private readonly _constructBranchChatBlockMessages = async (
|
||||
rootWorkspaceId: string,
|
||||
rootDocId: string,
|
||||
forkSessionId: string
|
||||
forkSessionId: string,
|
||||
docId?: string
|
||||
) => {
|
||||
const currentUserInfo = await AIProvider.userInfo;
|
||||
const forkMessages = (await queryHistoryMessages(
|
||||
rootWorkspaceId,
|
||||
rootDocId,
|
||||
forkSessionId
|
||||
forkSessionId,
|
||||
docId
|
||||
)) as ChatMessage[];
|
||||
const forkLength = forkMessages.length;
|
||||
const historyLength = this._historyMessages.length;
|
||||
@@ -163,18 +157,20 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
messages: [],
|
||||
});
|
||||
this._forkBlockId = undefined;
|
||||
this._forkSessionId = undefined;
|
||||
};
|
||||
|
||||
private readonly _getSessionId = async () => {
|
||||
return this._forkSessionId ?? this._sessionId;
|
||||
private readonly initSession = async () => {
|
||||
const session = await AIProvider.session?.getSession(
|
||||
this.rootWorkspaceId,
|
||||
this._sessionId
|
||||
);
|
||||
this.session = session ?? null;
|
||||
};
|
||||
|
||||
private readonly _createSessionId = async () => {
|
||||
if (this._forkSessionId) {
|
||||
return this._forkSessionId;
|
||||
private readonly createForkSession = async () => {
|
||||
if (this.forkSession) {
|
||||
return this.forkSession;
|
||||
}
|
||||
|
||||
const lastMessage = this._historyMessages.at(-1);
|
||||
if (!lastMessage) return;
|
||||
|
||||
@@ -185,8 +181,14 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
sessionId: this._sessionId,
|
||||
latestMessageId: lastMessage.id,
|
||||
});
|
||||
this._forkSessionId = forkSessionId;
|
||||
return this._forkSessionId;
|
||||
if (forkSessionId) {
|
||||
const session = await AIProvider.session?.getSession(
|
||||
this.rootWorkspaceId,
|
||||
forkSessionId
|
||||
);
|
||||
this.forkSession = session ?? null;
|
||||
}
|
||||
return this.forkSession;
|
||||
};
|
||||
|
||||
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 (!this._forkSessionId || !this.chatContext.messages.length) {
|
||||
const forkSessionId = this.forkSession?.id;
|
||||
if (!forkSessionId || !this.chatContext.messages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -230,8 +233,8 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
const { rootWorkspaceId, rootDocId } = this;
|
||||
const messages = await this._constructBranchChatBlockMessages(
|
||||
rootWorkspaceId,
|
||||
rootDocId,
|
||||
this._forkSessionId
|
||||
forkSessionId,
|
||||
rootDocId
|
||||
);
|
||||
if (!messages.length) {
|
||||
return;
|
||||
@@ -245,7 +248,7 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
{
|
||||
xywh: bound.serialize(),
|
||||
messages: JSON.stringify(messages),
|
||||
sessionId: this._forkSessionId,
|
||||
sessionId: forkSessionId,
|
||||
rootWorkspaceId: rootWorkspaceId,
|
||||
rootDocId: rootDocId,
|
||||
},
|
||||
@@ -280,7 +283,8 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
* Update the current chat messages with the new message
|
||||
*/
|
||||
updateChatBlockMessages = async () => {
|
||||
if (!this._forkBlockId || !this._forkSessionId) {
|
||||
const forkSessionId = this.forkSession?.id;
|
||||
if (!this._forkBlockId || !forkSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -292,8 +296,8 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
const { rootWorkspaceId, rootDocId } = this;
|
||||
const messages = await this._constructBranchChatBlockMessages(
|
||||
rootWorkspaceId,
|
||||
rootDocId,
|
||||
this._forkSessionId
|
||||
forkSessionId,
|
||||
rootDocId
|
||||
);
|
||||
if (!messages.length) {
|
||||
return;
|
||||
@@ -355,8 +359,8 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
*/
|
||||
retry = async () => {
|
||||
try {
|
||||
const { _forkBlockId, _forkSessionId } = this;
|
||||
if (!_forkBlockId || !_forkSessionId) return;
|
||||
const forkSessionId = this.forkSession?.id;
|
||||
if (!this._forkBlockId || !forkSessionId) return;
|
||||
if (!AIProvider.actions.chat) return;
|
||||
|
||||
const abortController = new AbortController();
|
||||
@@ -376,7 +380,7 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
|
||||
const { store } = this.host;
|
||||
const stream = await AIProvider.actions.chat({
|
||||
sessionId: _forkSessionId,
|
||||
sessionId: forkSessionId,
|
||||
retry: true,
|
||||
docId: store.id,
|
||||
workspaceId: store.workspace.id,
|
||||
@@ -461,20 +465,20 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
}
|
||||
|
||||
return html`<div class=${messageClasses}>
|
||||
<ai-chat-message
|
||||
<ai-chat-block-message
|
||||
.host=${host}
|
||||
.state=${messageState}
|
||||
.message=${message}
|
||||
.textRendererOptions=${this._textRendererOptions}
|
||||
></ai-chat-message>
|
||||
></ai-chat-block-message>
|
||||
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
|
||||
${shouldRenderCopyMore
|
||||
? html` <chat-copy-more
|
||||
.host=${host}
|
||||
.session=${this.forkSession}
|
||||
.actions=${actions}
|
||||
.content=${markdown}
|
||||
.isLast=${isLastReply}
|
||||
.getSessionId=${this._getSessionId}
|
||||
.messageId=${message.id ?? undefined}
|
||||
.retry=${() => this.retry()}
|
||||
></chat-copy-more>`
|
||||
@@ -482,9 +486,9 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
${shouldRenderActions
|
||||
? html`<chat-action-list
|
||||
.host=${host}
|
||||
.session=${this.forkSession}
|
||||
.actions=${actions}
|
||||
.content=${markdown}
|
||||
.getSessionId=${this._getSessionId}
|
||||
.messageId=${message.id ?? undefined}
|
||||
.layoutDirection=${'horizontal'}
|
||||
></chat-action-list>`
|
||||
@@ -496,6 +500,7 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.initSession().catch(console.error);
|
||||
const extensions = this.host.std
|
||||
.get(ViewExtensionManagerIdentifier)
|
||||
.get('preview-page');
|
||||
@@ -507,8 +512,8 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
this._historyMessages = this._deserializeHistoryChatMessages(
|
||||
this.historyMessagesString
|
||||
);
|
||||
const { rootWorkspaceId, rootDocId, _sessionId } = this;
|
||||
queryHistoryMessages(rootWorkspaceId, rootDocId, _sessionId)
|
||||
const { rootWorkspaceId, _sessionId } = this;
|
||||
queryHistoryMessages(rootWorkspaceId, _sessionId)
|
||||
.then(messages => {
|
||||
this._historyMessages = this._historyMessages.map((message, idx) => {
|
||||
return {
|
||||
@@ -566,17 +571,17 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
<ai-history-clear
|
||||
.host=${this.host}
|
||||
.doc=${this.host.store}
|
||||
.getSessionId=${this._getSessionId}
|
||||
.session=${this.forkSession}
|
||||
.onHistoryCleared=${this._onHistoryCleared}
|
||||
.chatContextValue=${chatContext}
|
||||
></ai-history-clear>
|
||||
</div>
|
||||
<div class="ai-chat-messages-container">
|
||||
<ai-chat-messages
|
||||
<ai-chat-block-messages
|
||||
.host=${host}
|
||||
.messages=${_historyMessages}
|
||||
.textRendererOptions=${_textRendererOptions}
|
||||
></ai-chat-messages>
|
||||
></ai-chat-block-messages>
|
||||
<date-time .date=${latestMessageCreatedAt}></date-time>
|
||||
<div class="new-chat-messages-container">
|
||||
${this.CurrentMessages(currentChatMessages)}
|
||||
@@ -584,12 +589,11 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
</div>
|
||||
<ai-chat-composer
|
||||
.host=${host}
|
||||
.doc=${this.host.store}
|
||||
.getSessionId=${this._getSessionId}
|
||||
.createSessionId=${this._createSessionId}
|
||||
.workspaceId=${this.rootWorkspaceId}
|
||||
.session=${this.forkSession ?? this.session}
|
||||
.createSession=${this.createForkSession}
|
||||
.chatContextValue=${chatContext}
|
||||
.updateContext=${updateContext}
|
||||
.isVisible=${this.isComposerVisible}
|
||||
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
||||
.networkSearchConfig=${networkSearchConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
@@ -647,6 +651,12 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
|
||||
@state()
|
||||
accessor embeddingProgress: [number, number] = [0, 0];
|
||||
|
||||
@state()
|
||||
accessor session: CopilotSessionType | null | undefined;
|
||||
|
||||
@state()
|
||||
accessor forkSession: CopilotSessionType | null | undefined;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
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 {
|
||||
PaymentRequiredError,
|
||||
RequestTimeoutError,
|
||||
@@ -133,8 +133,8 @@ export class AIProvider {
|
||||
// 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?
|
||||
/* eslint-disable rxjs/finnish */
|
||||
requestOpenWithChat: new Subject<AIChatParams>(),
|
||||
requestSendWithChat: new Subject<AISendParams>(),
|
||||
requestOpenWithChat: new BehaviorSubject<AIChatParams | null>(null),
|
||||
requestSendWithChat: new BehaviorSubject<AISendParams | null>(null),
|
||||
requestInsertTemplate: new Subject<{
|
||||
template: string;
|
||||
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 { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import {
|
||||
type ChatHistoryOrder,
|
||||
ContextCategories,
|
||||
type ContextWorkspaceEmbeddingStatus,
|
||||
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', {
|
||||
actions: async (
|
||||
workspaceId: string,
|
||||
docId?: string
|
||||
docId: string
|
||||
): Promise<BlockSuitePresets.AIHistory[]> => {
|
||||
// @ts-expect-error - 'action' is missing in server impl
|
||||
return (
|
||||
@@ -754,14 +753,15 @@ Could you make a new website based on these notes and send back just the html fi
|
||||
},
|
||||
chats: async (
|
||||
workspaceId: string,
|
||||
docId?: string,
|
||||
options?: {
|
||||
sessionId?: string;
|
||||
messageOrder?: ChatHistoryOrder;
|
||||
}
|
||||
sessionId: string,
|
||||
docId?: string
|
||||
): Promise<BlockSuitePresets.AIHistory[]> => {
|
||||
// @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 (
|
||||
workspaceId: string,
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import type { BlockModel, Store } 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 {
|
||||
getSelectedImagesAsBlobs,
|
||||
getSelectedTextContent,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Scrollable } from '@affine/component';
|
||||
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 type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
|
||||
import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer';
|
||||
@@ -119,7 +119,10 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
|
||||
useEffect(() => {
|
||||
const disposables: Subscription[] = [];
|
||||
const openHandler = () => {
|
||||
const openHandler = (params: AIChatParams | null) => {
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
workbench.openSidebar();
|
||||
view.activeSidebarTab('chat');
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Scrollable } from '@affine/component';
|
||||
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 { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer';
|
||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||
@@ -127,7 +127,10 @@ function DocPeekPreviewEditor({
|
||||
|
||||
useEffect(() => {
|
||||
const disposables: Subscription[] = [];
|
||||
const openHandler = () => {
|
||||
const openHandler = (params: AIChatParams | null) => {
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
if (doc) {
|
||||
workbench.openDoc(doc.id);
|
||||
peekView.close();
|
||||
|
||||
Reference in New Issue
Block a user