feat: add doc copilot context api (#10103)
### What Changed? - Add graphql APIs. - Provide context and session service in `AIProvider`. - Rename the state from `embedding` to `processing`. - Reafctor front-end session create, update and save logic. Persist the document selected by the user: [录屏2025-02-08 11.04.40.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/sJGviKxfE3Ap685cl5bj/195a85f2-43c4-4e49-88d9-6b5fc4f235ca.mov" />](https://app.graphite.dev/media/video/sJGviKxfE3Ap685cl5bj/195a85f2-43c4-4e49-88d9-6b5fc4f235ca.mov)
This commit is contained in:
@@ -36,12 +36,12 @@ class AddContextDocInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
class RemoveContextFileInput {
|
class RemoveContextDocInput {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
contextId!: string;
|
contextId!: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
fileId!: string;
|
docId!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType('CopilotContext')
|
@ObjectType('CopilotContext')
|
||||||
@@ -227,8 +227,8 @@ export class CopilotContextResolver {
|
|||||||
})
|
})
|
||||||
@CallMetric('ai', 'context_doc_remove')
|
@CallMetric('ai', 'context_doc_remove')
|
||||||
async removeContextDoc(
|
async removeContextDoc(
|
||||||
@Args({ name: 'options', type: () => RemoveContextFileInput })
|
@Args({ name: 'options', type: () => RemoveContextDocInput })
|
||||||
options: RemoveContextFileInput
|
options: RemoveContextDocInput
|
||||||
) {
|
) {
|
||||||
const lockFlag = `${COPILOT_LOCKER}:context:${options.contextId}`;
|
const lockFlag = `${COPILOT_LOCKER}:context:${options.contextId}`;
|
||||||
await using lock = await this.mutex.acquire(lockFlag);
|
await using lock = await this.mutex.acquire(lockFlag);
|
||||||
@@ -238,7 +238,7 @@ export class CopilotContextResolver {
|
|||||||
const session = await this.context.get(options.contextId);
|
const session = await this.context.get(options.contextId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await session.removeDocRecord(options.fileId);
|
return await session.removeDocRecord(options.docId);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw new CopilotFailedToModifyContext({
|
throw new CopilotFailedToModifyContext({
|
||||||
contextId: options.contextId,
|
contextId: options.contextId,
|
||||||
|
|||||||
@@ -713,7 +713,7 @@ type Mutation {
|
|||||||
removeAvatar: RemoveAvatar!
|
removeAvatar: RemoveAvatar!
|
||||||
|
|
||||||
"""remove a doc from context"""
|
"""remove a doc from context"""
|
||||||
removeContextDoc(options: RemoveContextFileInput!): Boolean!
|
removeContextDoc(options: RemoveContextDocInput!): Boolean!
|
||||||
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean!
|
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean!
|
||||||
resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType!
|
resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType!
|
||||||
revoke(userId: String!, workspaceId: String!): Boolean!
|
revoke(userId: String!, workspaceId: String!): Boolean!
|
||||||
@@ -891,9 +891,9 @@ type RemoveAvatar {
|
|||||||
success: Boolean!
|
success: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
input RemoveContextFileInput {
|
input RemoveContextDocInput {
|
||||||
contextId: String!
|
contextId: String!
|
||||||
fileId: String!
|
docId: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input RevokeDocUserRoleInput {
|
input RevokeDocUserRoleInput {
|
||||||
|
|||||||
@@ -236,9 +236,14 @@ export function handleInlineAskAIAction(
|
|||||||
host.selection.set([selection]);
|
host.selection.set([selection]);
|
||||||
|
|
||||||
selectAboveBlocks(host)
|
selectAboveBlocks(host)
|
||||||
.then(context => {
|
.then(async context => {
|
||||||
assertExists(AIProvider.actions.chat);
|
if (!AIProvider.session || !AIProvider.actions.chat) return;
|
||||||
|
const sessionId = await AIProvider.session.createSession(
|
||||||
|
host.doc.workspace.id,
|
||||||
|
host.doc.id
|
||||||
|
);
|
||||||
const stream = AIProvider.actions.chat({
|
const stream = AIProvider.actions.chat({
|
||||||
|
sessionId,
|
||||||
input: `${context}\n${input}`,
|
input: `${context}\n${input}`,
|
||||||
stream: true,
|
stream: true,
|
||||||
host,
|
host,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { getCopilotHistoriesQuery, RequestOptions } from '@affine/graphql';
|
import type {
|
||||||
|
CopilotContextDoc,
|
||||||
|
CopilotContextFile,
|
||||||
|
getCopilotHistoriesQuery,
|
||||||
|
RequestOptions,
|
||||||
|
} from '@affine/graphql';
|
||||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||||
import type { GfxModel } from '@blocksuite/affine/block-std/gfx';
|
import type { GfxModel } from '@blocksuite/affine/block-std/gfx';
|
||||||
import type { BlockModel } from '@blocksuite/affine/store';
|
import type { BlockModel } from '@blocksuite/affine/store';
|
||||||
@@ -105,10 +110,9 @@ declare global {
|
|||||||
T['stream'] extends true ? TextStream : Promise<string>;
|
T['stream'] extends true ? TextStream : Promise<string>;
|
||||||
|
|
||||||
interface ChatOptions extends AITextActionOptions {
|
interface ChatOptions extends AITextActionOptions {
|
||||||
// related documents
|
|
||||||
docs?: DocContext[];
|
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
isRootSession?: boolean;
|
isRootSession?: boolean;
|
||||||
|
docs?: DocContext[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TranslateOptions extends AITextActionOptions {
|
interface TranslateOptions extends AITextActionOptions {
|
||||||
@@ -232,6 +236,40 @@ declare global {
|
|||||||
): AIActionTextResponse<T>;
|
): AIActionTextResponse<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AIContextService {
|
||||||
|
createContext: (
|
||||||
|
workspaceId: string,
|
||||||
|
sessionId: string
|
||||||
|
) => Promise<string>;
|
||||||
|
getContextId: (
|
||||||
|
workspaceId: string,
|
||||||
|
sessionId: string
|
||||||
|
) => Promise<string | undefined>;
|
||||||
|
addContextDoc: (options: {
|
||||||
|
contextId: string;
|
||||||
|
docId: string;
|
||||||
|
}) => Promise<Array<{ id: string }>>;
|
||||||
|
removeContextDoc: (options: {
|
||||||
|
contextId: string;
|
||||||
|
docId: string;
|
||||||
|
}) => Promise<boolean>;
|
||||||
|
addContextFile: (options: {
|
||||||
|
contextId: string;
|
||||||
|
fileId: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
removeContextFile: (options: {
|
||||||
|
contextId: string;
|
||||||
|
fileId: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
getContextDocsAndFiles: (
|
||||||
|
workspaceId: string,
|
||||||
|
sessionId: string,
|
||||||
|
contextId: string
|
||||||
|
) => Promise<
|
||||||
|
{ docs: CopilotContextDoc[]; files: CopilotContextFile[] } | undefined
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(@Peng): should be refactored to get rid of implement details (like messages, action, role, etc.)
|
// TODO(@Peng): should be refactored to get rid of implement details (like messages, action, role, etc.)
|
||||||
interface AIHistory {
|
interface AIHistory {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -256,6 +294,15 @@ declare global {
|
|||||||
>[];
|
>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface AISessionService {
|
||||||
|
createSession: (
|
||||||
|
workspaceId: string,
|
||||||
|
docId: string,
|
||||||
|
promptName?: string
|
||||||
|
) => Promise<string>;
|
||||||
|
updateSession: (sessionId: string, promptName: string) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
interface AIHistoryService {
|
interface AIHistoryService {
|
||||||
// non chat histories
|
// non chat histories
|
||||||
actions: (
|
actions: (
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export type ChatContextValue = {
|
|||||||
// chips of workspace doc or user uploaded file
|
// chips of workspace doc or user uploaded file
|
||||||
chips: ChatChip[];
|
chips: ChatChip[];
|
||||||
abortController: AbortController | null;
|
abortController: AbortController | null;
|
||||||
chatSessionId: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChatBlockMessage = ChatMessage & {
|
export type ChatBlockMessage = ChatMessage & {
|
||||||
@@ -55,20 +54,14 @@ export type ChatBlockMessage = ChatMessage & {
|
|||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChipState =
|
export type ChipState = 'candidate' | 'processing' | 'success' | 'failed';
|
||||||
| 'candidate'
|
|
||||||
| 'uploading'
|
|
||||||
| 'embedding'
|
|
||||||
| 'success'
|
|
||||||
| 'failed';
|
|
||||||
|
|
||||||
export interface BaseChip {
|
export interface BaseChip {
|
||||||
/**
|
/**
|
||||||
* candidate: the chip is a candidate for the chat
|
* candidate: the chip is a candidate for the chat
|
||||||
* uploading: the chip is uploading
|
* processing: the chip is processing
|
||||||
* embedding: the chip is embedding
|
* success: the chip is successfully processed
|
||||||
* success: the chip is successfully embedded
|
* failed: the chip is failed to process
|
||||||
* failed: the chip is failed to embed
|
|
||||||
*/
|
*/
|
||||||
state: ChipState;
|
state: ChipState;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { css, html } from 'lit';
|
|||||||
import { property, query } from 'lit/decorators.js';
|
import { property, query } from 'lit/decorators.js';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
|
|
||||||
|
import { AIProvider } from '../provider';
|
||||||
import type { DocDisplayConfig, DocSearchMenuConfig } from './chat-config';
|
import type { DocDisplayConfig, DocSearchMenuConfig } from './chat-config';
|
||||||
import type { BaseChip, ChatChip, ChatContextValue } from './chat-context';
|
import type { BaseChip, ChatChip, ChatContextValue } from './chat-context';
|
||||||
import { getChipKey, isDocChip, isFileChip } from './components/utils';
|
import { getChipKey, isDocChip, isFileChip } from './components/utils';
|
||||||
@@ -45,6 +46,9 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor chatContextValue!: ChatContextValue;
|
accessor chatContextValue!: ChatContextValue;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor chatContextId!: string | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
||||||
|
|
||||||
@@ -69,6 +73,7 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
if (isDocChip(chip)) {
|
if (isDocChip(chip)) {
|
||||||
return html`<chat-panel-doc-chip
|
return html`<chat-panel-doc-chip
|
||||||
.chip=${chip}
|
.chip=${chip}
|
||||||
|
.addChip=${this._addChip}
|
||||||
.updateChip=${this._updateChip}
|
.updateChip=${this._updateChip}
|
||||||
.removeChip=${this._removeChip}
|
.removeChip=${this._removeChip}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
@@ -120,11 +125,12 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _addChip = (chip: ChatChip) => {
|
private readonly _addChip = async (chip: ChatChip) => {
|
||||||
if (
|
if (
|
||||||
this.chatContextValue.chips.length === 1 &&
|
this.chatContextValue.chips.length === 1 &&
|
||||||
this.chatContextValue.chips[0].state === 'candidate'
|
this.chatContextValue.chips[0].state === 'candidate'
|
||||||
) {
|
) {
|
||||||
|
await this._addToContext(chip);
|
||||||
this.updateContext({
|
this.updateContext({
|
||||||
chips: [chip],
|
chips: [chip],
|
||||||
});
|
});
|
||||||
@@ -132,12 +138,16 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
}
|
}
|
||||||
// remove the chip if it already exists
|
// remove the chip if it already exists
|
||||||
const chips = this.chatContextValue.chips.filter(item => {
|
const chips = this.chatContextValue.chips.filter(item => {
|
||||||
if (isDocChip(item)) {
|
if (isDocChip(chip)) {
|
||||||
return !isDocChip(chip) || item.docId !== chip.docId;
|
return !isDocChip(item) || item.docId !== chip.docId;
|
||||||
} else {
|
} else {
|
||||||
return !isFileChip(chip) || item.fileId !== chip.fileId;
|
return !isFileChip(item) || item.fileId !== chip.fileId;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (chips.length < this.chatContextValue.chips.length) {
|
||||||
|
await this._removeFromContext(chip);
|
||||||
|
}
|
||||||
|
await this._addToContext(chip);
|
||||||
this.updateContext({
|
this.updateContext({
|
||||||
chips: [...chips, chip],
|
chips: [...chips, chip],
|
||||||
});
|
});
|
||||||
@@ -167,15 +177,55 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _removeChip = (chip: ChatChip) => {
|
private readonly _removeChip = async (chip: ChatChip) => {
|
||||||
this.updateContext({
|
if (isDocChip(chip)) {
|
||||||
chips: this.chatContextValue.chips.filter(item => {
|
await this._removeFromContext(chip);
|
||||||
if (isDocChip(item)) {
|
this.updateContext({
|
||||||
return !isDocChip(chip) || item.docId !== chip.docId;
|
chips: this.chatContextValue.chips.filter(item => {
|
||||||
} else {
|
return !isDocChip(item) || item.docId !== chip.docId;
|
||||||
return !isFileChip(chip) || item.fileId !== chip.fileId;
|
}),
|
||||||
}
|
});
|
||||||
}),
|
} else {
|
||||||
});
|
await this._removeFromContext(chip);
|
||||||
|
this.updateContext({
|
||||||
|
chips: this.chatContextValue.chips.filter(item => {
|
||||||
|
return !isFileChip(item) || item.fileId !== chip.fileId;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _addToContext = async (chip: ChatChip) => {
|
||||||
|
if (!AIProvider.context || !this.chatContextId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isDocChip(chip)) {
|
||||||
|
await AIProvider.context.addContextDoc({
|
||||||
|
contextId: this.chatContextId,
|
||||||
|
docId: chip.docId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await AIProvider.context.addContextFile({
|
||||||
|
contextId: this.chatContextId,
|
||||||
|
fileId: chip.fileId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _removeFromContext = async (chip: ChatChip) => {
|
||||||
|
if (!AIProvider.context || !this.chatContextId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isDocChip(chip)) {
|
||||||
|
await AIProvider.context.removeContextDoc({
|
||||||
|
contextId: this.chatContextId,
|
||||||
|
docId: chip.docId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await AIProvider.context.removeContextFile({
|
||||||
|
contextId: this.chatContextId,
|
||||||
|
fileId: chip.fileId,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,6 +258,9 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor chatContextValue!: ChatContextValue;
|
accessor chatContextValue!: ChatContextValue;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor chatSessionId!: string | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
||||||
|
|
||||||
@@ -267,6 +270,44 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||||
|
|
||||||
|
private _lastPromptName: string | null = null;
|
||||||
|
|
||||||
|
private get _isNetworkActive() {
|
||||||
|
return (
|
||||||
|
!!this.networkSearchConfig.visible.value &&
|
||||||
|
!!this.networkSearchConfig.enabled.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _isNetworkDisabled() {
|
||||||
|
return (
|
||||||
|
!!this.chatContextValue.images.length ||
|
||||||
|
!!this.chatContextValue.chips.filter(chip => chip.state !== 'candidate')
|
||||||
|
.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _promptName() {
|
||||||
|
if (this._isNetworkDisabled) {
|
||||||
|
return 'Chat With AFFiNE AI';
|
||||||
|
}
|
||||||
|
return this._isNetworkActive
|
||||||
|
? 'Search With AFFiNE AI'
|
||||||
|
: 'Chat With AFFiNE AI';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updatePromptName() {
|
||||||
|
if (this._lastPromptName !== this._promptName) {
|
||||||
|
this._lastPromptName = this._promptName;
|
||||||
|
if (this.chatSessionId) {
|
||||||
|
await AIProvider.session?.updateSession(
|
||||||
|
this.chatSessionId,
|
||||||
|
this._promptName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _addImages(images: File[]) {
|
private _addImages(images: File[]) {
|
||||||
const oldImages = this.chatContextValue.images;
|
const oldImages = this.chatContextValue.images;
|
||||||
this.updateContext({
|
this.updateContext({
|
||||||
@@ -363,12 +404,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
const { images, status } = this.chatContextValue;
|
const { images, status } = this.chatContextValue;
|
||||||
const hasImages = images.length > 0;
|
const hasImages = images.length > 0;
|
||||||
const maxHeight = hasImages ? 272 + 2 : 200 + 2;
|
const maxHeight = hasImages ? 272 + 2 : 200 + 2;
|
||||||
const networkDisabled =
|
const uploadDisabled = this._isNetworkActive && !this._isNetworkDisabled;
|
||||||
!!this.chatContextValue.images.length ||
|
|
||||||
!!this.chatContextValue.chips.filter(chip => chip.state !== 'candidate')
|
|
||||||
.length;
|
|
||||||
const networkActive = !!this.networkSearchConfig.enabled.value;
|
|
||||||
const uploadDisabled = networkActive && !networkDisabled;
|
|
||||||
return html`<style>
|
return html`<style>
|
||||||
.chat-panel-input {
|
.chat-panel-input {
|
||||||
border-color: ${this.focused
|
border-color: ${this.focused
|
||||||
@@ -461,9 +497,9 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
<div
|
<div
|
||||||
class="chat-network-search"
|
class="chat-network-search"
|
||||||
data-testid="chat-network-search"
|
data-testid="chat-network-search"
|
||||||
aria-disabled=${networkDisabled}
|
aria-disabled=${this._isNetworkDisabled}
|
||||||
data-active=${networkActive}
|
data-active=${this._isNetworkActive}
|
||||||
@click=${networkDisabled
|
@click=${this._isNetworkDisabled
|
||||||
? undefined
|
? undefined
|
||||||
: this._toggleNetworkSearch}
|
: this._toggleNetworkSearch}
|
||||||
@pointerdown=${stopPropagation}
|
@pointerdown=${stopPropagation}
|
||||||
@@ -520,11 +556,9 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
send = async (text: string) => {
|
send = async (text: string) => {
|
||||||
const { status, markdown, chips } = this.chatContextValue;
|
const { status, markdown, chips } = this.chatContextValue;
|
||||||
if (status === 'loading' || status === 'transmitting') return;
|
if (status === 'loading' || status === 'transmitting') return;
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
const { images } = this.chatContextValue;
|
const { images } = this.chatContextValue;
|
||||||
if (!text) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { doc } = this.host;
|
const { doc } = this.host;
|
||||||
|
|
||||||
this.updateContext({
|
this.updateContext({
|
||||||
@@ -535,6 +569,8 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
markdown: '',
|
markdown: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this._updatePromptName();
|
||||||
|
|
||||||
const attachments = await Promise.all(
|
const attachments = await Promise.all(
|
||||||
images?.map(image => readBlobAsURL(image))
|
images?.map(image => readBlobAsURL(image))
|
||||||
);
|
);
|
||||||
@@ -569,6 +605,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
markdown: chip.markdown?.value || '',
|
markdown: chip.markdown?.value || '',
|
||||||
}));
|
}));
|
||||||
const stream = AIProvider.actions.chat?.({
|
const stream = AIProvider.actions.chat?.({
|
||||||
|
sessionId: this.chatSessionId,
|
||||||
input: userInput,
|
input: userInput,
|
||||||
docs: docs,
|
docs: docs,
|
||||||
docId: doc.id,
|
docId: doc.id,
|
||||||
@@ -594,19 +631,13 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
|
|
||||||
this.updateContext({ status: 'success' });
|
this.updateContext({ status: 'success' });
|
||||||
|
|
||||||
if (!this.chatContextValue.chatSessionId) {
|
|
||||||
this.updateContext({
|
|
||||||
chatSessionId: AIProvider.LAST_ROOT_SESSION_ID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { items } = this.chatContextValue;
|
const { items } = this.chatContextValue;
|
||||||
const last = items[items.length - 1] as ChatMessage;
|
const last = items[items.length - 1] as ChatMessage;
|
||||||
if (!last.id) {
|
if (!last.id) {
|
||||||
const historyIds = await AIProvider.histories?.ids(
|
const historyIds = await AIProvider.histories?.ids(
|
||||||
doc.workspace.id,
|
doc.workspace.id,
|
||||||
doc.id,
|
doc.id,
|
||||||
{ sessionId: this.chatContextValue.chatSessionId }
|
{ sessionId: this.chatSessionId }
|
||||||
);
|
);
|
||||||
if (!historyIds || !historyIds[0]) return;
|
if (!historyIds || !historyIds[0]) return;
|
||||||
last.id = historyIds[0].messages.at(-1)?.id ?? '';
|
last.id = historyIds[0].messages.at(-1)?.id ?? '';
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor chatContextValue!: ChatContextValue;
|
accessor chatContextValue!: ChatContextValue;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor chatSessionId!: string | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
||||||
|
|
||||||
@@ -397,8 +400,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
retry = async () => {
|
retry = async () => {
|
||||||
const { doc } = this.host;
|
const { doc } = this.host;
|
||||||
try {
|
try {
|
||||||
const { chatSessionId } = this.chatContextValue;
|
if (!this.chatSessionId) return;
|
||||||
if (!chatSessionId) return;
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const items = [...this.chatContextValue.items];
|
const items = [...this.chatContextValue.items];
|
||||||
@@ -410,7 +412,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
this.updateContext({ items, status: 'loading', error: null });
|
this.updateContext({ items, status: 'loading', error: null });
|
||||||
|
|
||||||
const stream = AIProvider.actions.chat?.({
|
const stream = AIProvider.actions.chat?.({
|
||||||
sessionId: chatSessionId,
|
sessionId: this.chatSessionId,
|
||||||
retry: true,
|
retry: true,
|
||||||
docId: doc.id,
|
docId: doc.id,
|
||||||
workspaceId: doc.workspace.id,
|
workspaceId: doc.workspace.id,
|
||||||
@@ -441,7 +443,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderEditorActions(item: ChatMessage, isLast: boolean) {
|
renderEditorActions(item: ChatMessage, isLast: boolean) {
|
||||||
const { status, chatSessionId } = this.chatContextValue;
|
const { status } = this.chatContextValue;
|
||||||
|
|
||||||
if (item.role !== 'assistant') return nothing;
|
if (item.role !== 'assistant') return nothing;
|
||||||
|
|
||||||
@@ -465,7 +467,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
.actions=${actions}
|
.actions=${actions}
|
||||||
.content=${content}
|
.content=${content}
|
||||||
.isLast=${isLast}
|
.isLast=${isLast}
|
||||||
.chatSessionId=${chatSessionId ?? undefined}
|
.chatSessionId=${this.chatSessionId}
|
||||||
.messageId=${messageId}
|
.messageId=${messageId}
|
||||||
.withMargin=${true}
|
.withMargin=${true}
|
||||||
.retry=${() => this.retry()}
|
.retry=${() => this.retry()}
|
||||||
@@ -475,7 +477,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
.actions=${actions}
|
.actions=${actions}
|
||||||
.host=${host}
|
.host=${host}
|
||||||
.content=${content}
|
.content=${content}
|
||||||
.chatSessionId=${chatSessionId ?? undefined}
|
.chatSessionId=${this.chatSessionId}
|
||||||
.messageId=${messageId ?? undefined}
|
.messageId=${messageId ?? undefined}
|
||||||
.withMargin=${true}
|
.withMargin=${true}
|
||||||
></chat-action-list>`
|
></chat-action-list>`
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
|||||||
private readonly _addDocChip = (meta: DocMeta) => {
|
private readonly _addDocChip = (meta: DocMeta) => {
|
||||||
this.addChip({
|
this.addChip({
|
||||||
docId: meta.id,
|
docId: meta.id,
|
||||||
state: 'embedding',
|
state: 'processing',
|
||||||
});
|
});
|
||||||
this.abortController.abort();
|
this.abortController.abort();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export class ChatPanelDocChip extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor chip!: DocChip;
|
accessor chip!: DocChip;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor addChip!: (chip: ChatChip) => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateChip!: (chip: ChatChip, options: Partial<BaseChip>) => void;
|
accessor updateChip!: (chip: ChatChip, options: Partial<BaseChip>) => void;
|
||||||
|
|
||||||
@@ -61,9 +64,9 @@ export class ChatPanelDocChip extends SignalWatcher(
|
|||||||
if (
|
if (
|
||||||
changedProperties.has('chip') &&
|
changedProperties.has('chip') &&
|
||||||
changedProperties.get('chip')?.state === 'candidate' &&
|
changedProperties.get('chip')?.state === 'candidate' &&
|
||||||
this.chip.state === 'embedding'
|
this.chip.state === 'processing'
|
||||||
) {
|
) {
|
||||||
this.embedDocChip().catch(console.error);
|
this.processDocChip().catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +77,9 @@ export class ChatPanelDocChip extends SignalWatcher(
|
|||||||
|
|
||||||
private readonly onChipClick = async () => {
|
private readonly onChipClick = async () => {
|
||||||
if (this.chip.state === 'candidate') {
|
if (this.chip.state === 'candidate') {
|
||||||
this.updateChip(this.chip, {
|
this.addChip({
|
||||||
state: 'embedding',
|
...this.chip,
|
||||||
|
state: 'processing',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -86,11 +90,11 @@ export class ChatPanelDocChip extends SignalWatcher(
|
|||||||
|
|
||||||
private readonly autoUpdateChip = () => {
|
private readonly autoUpdateChip = () => {
|
||||||
if (this.chip.state !== 'candidate') {
|
if (this.chip.state !== 'candidate') {
|
||||||
this.embedDocChip().catch(console.error);
|
this.processDocChip().catch(console.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly embedDocChip = async () => {
|
private readonly processDocChip = async () => {
|
||||||
try {
|
try {
|
||||||
const doc = this.docDisplayConfig.getDoc(this.chip.docId);
|
const doc = this.docDisplayConfig.getDoc(this.chip.docId);
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
@@ -111,14 +115,14 @@ export class ChatPanelDocChip extends SignalWatcher(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.updateChip(this.chip, {
|
this.updateChip(this.chip, {
|
||||||
state: 'failed',
|
state: 'failed',
|
||||||
tooltip: e instanceof Error ? e.message : 'Failed to embed document',
|
tooltip: e instanceof Error ? e.message : 'Failed to process document',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const { state, docId } = this.chip;
|
const { state, docId } = this.chip;
|
||||||
const isLoading = state === 'embedding' || state === 'uploading';
|
const isLoading = state === 'processing';
|
||||||
const getIcon = this.docDisplayConfig.getIcon(docId);
|
const getIcon = this.docDisplayConfig.getIcon(docId);
|
||||||
const docIcon = typeof getIcon === 'function' ? getIcon() : getIcon;
|
const docIcon = typeof getIcon === 'function' ? getIcon() : getIcon;
|
||||||
const icon = getChipIcon(state, docIcon);
|
const icon = getChipIcon(state, docIcon);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class ChatPanelFileChip extends SignalWatcher(
|
|||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const { state, fileName, fileType } = this.chip;
|
const { state, fileName, fileType } = this.chip;
|
||||||
const isLoading = state === 'embedding' || state === 'uploading';
|
const isLoading = state === 'processing';
|
||||||
const tooltip = getChipTooltip(state, fileName, this.chip.tooltip);
|
const tooltip = getChipTooltip(state, fileName, this.chip.tooltip);
|
||||||
const fileIcon = getAttachmentFileIcon(fileType);
|
const fileIcon = getAttachmentFileIcon(fileType);
|
||||||
const icon = getChipIcon(state, fileIcon);
|
const icon = getChipIcon(state, fileIcon);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { CopilotContextDoc, CopilotContextFile } from '@affine/graphql';
|
||||||
import { WarningIcon } from '@blocksuite/icons/lit';
|
import { WarningIcon } from '@blocksuite/icons/lit';
|
||||||
import { type TemplateResult } from 'lit';
|
import { type TemplateResult } from 'lit';
|
||||||
|
|
||||||
@@ -15,14 +16,11 @@ export function getChipTooltip(
|
|||||||
if (state === 'candidate') {
|
if (state === 'candidate') {
|
||||||
return 'Click to add doc';
|
return 'Click to add doc';
|
||||||
}
|
}
|
||||||
if (state === 'embedding') {
|
if (state === 'processing') {
|
||||||
return 'Embedding...';
|
return 'Processing...';
|
||||||
}
|
|
||||||
if (state === 'uploading') {
|
|
||||||
return 'Uploading...';
|
|
||||||
}
|
}
|
||||||
if (state === 'failed') {
|
if (state === 'failed') {
|
||||||
return 'Failed to embed';
|
return 'Failed to process';
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
@@ -31,7 +29,7 @@ export function getChipIcon(
|
|||||||
state: ChipState,
|
state: ChipState,
|
||||||
icon: TemplateResult<1>
|
icon: TemplateResult<1>
|
||||||
): TemplateResult<1> {
|
): TemplateResult<1> {
|
||||||
const isLoading = state === 'embedding' || state === 'uploading';
|
const isLoading = state === 'processing';
|
||||||
const isFailed = state === 'failed';
|
const isFailed = state === 'failed';
|
||||||
if (isFailed) {
|
if (isFailed) {
|
||||||
return WarningIcon();
|
return WarningIcon();
|
||||||
@@ -50,6 +48,18 @@ export function isFileChip(chip: ChatChip): chip is FileChip {
|
|||||||
return 'fileId' in chip;
|
return 'fileId' in chip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDocContext(
|
||||||
|
context: CopilotContextDoc | CopilotContextFile
|
||||||
|
): context is CopilotContextDoc {
|
||||||
|
return !('blobId' in context);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFileContext(
|
||||||
|
context: CopilotContextDoc | CopilotContextFile
|
||||||
|
): context is CopilotContextFile {
|
||||||
|
return 'blobId' in context;
|
||||||
|
}
|
||||||
|
|
||||||
export function getChipKey(chip: ChatChip) {
|
export function getChipKey(chip: ChatChip) {
|
||||||
if (isDocChip(chip)) {
|
if (isDocChip(chip)) {
|
||||||
return chip.docId;
|
return chip.docId;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
NotificationProvider,
|
NotificationProvider,
|
||||||
type SpecBuilder,
|
type SpecBuilder,
|
||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
import { debounce, WithDisposable } from '@blocksuite/affine/global/utils';
|
import { WithDisposable } from '@blocksuite/affine/global/utils';
|
||||||
import type { Store } from '@blocksuite/affine/store';
|
import type { Store } from '@blocksuite/affine/store';
|
||||||
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';
|
||||||
@@ -31,8 +31,10 @@ import type {
|
|||||||
ChatContextValue,
|
ChatContextValue,
|
||||||
ChatItem,
|
ChatItem,
|
||||||
DocChip,
|
DocChip,
|
||||||
|
FileChip,
|
||||||
} from './chat-context';
|
} from './chat-context';
|
||||||
import type { ChatPanelMessages } from './chat-panel-messages';
|
import type { ChatPanelMessages } from './chat-panel-messages';
|
||||||
|
import { isDocContext } from './components/utils';
|
||||||
|
|
||||||
export class ChatPanel extends WithDisposable(ShadowlessElement) {
|
export class ChatPanel extends WithDisposable(ShadowlessElement) {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
@@ -113,52 +115,89 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
|
|||||||
private readonly _chatMessages: Ref<ChatPanelMessages> =
|
private readonly _chatMessages: Ref<ChatPanelMessages> =
|
||||||
createRef<ChatPanelMessages>();
|
createRef<ChatPanelMessages>();
|
||||||
|
|
||||||
private _resettingCounter = 0;
|
// request counter to track the latest request
|
||||||
|
private _updateHistoryCounter = 0;
|
||||||
|
|
||||||
private readonly _resetItems = debounce(() => {
|
private readonly _updateHistory = async () => {
|
||||||
const counter = ++this._resettingCounter;
|
const { doc } = this;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
(async () => {
|
|
||||||
const { doc } = this;
|
|
||||||
|
|
||||||
const [histories, actions] = await Promise.all([
|
const currentRequest = ++this._updateHistoryCounter;
|
||||||
AIProvider.histories?.chats(doc.workspace.id, doc.id, { fork: false }),
|
|
||||||
AIProvider.histories?.actions(doc.workspace.id, doc.id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (counter !== this._resettingCounter) return;
|
const [histories, actions] = await Promise.all([
|
||||||
|
AIProvider.histories?.chats(doc.workspace.id, doc.id, { fork: false }),
|
||||||
|
AIProvider.histories?.actions(doc.workspace.id, doc.id),
|
||||||
|
]);
|
||||||
|
|
||||||
const items: ChatItem[] = actions ? [...actions] : [];
|
// Check if this is still the latest request
|
||||||
|
if (currentRequest !== this._updateHistoryCounter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (histories?.at(-1)) {
|
const items: ChatItem[] = actions ? [...actions] : [];
|
||||||
const history = histories.at(-1);
|
|
||||||
if (!history) return;
|
|
||||||
this.chatContextValue.chatSessionId = history.sessionId;
|
|
||||||
items.push(...history.messages);
|
|
||||||
AIProvider.LAST_ROOT_SESSION_ID = history.sessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { chips } = this.chatContextValue;
|
if (histories?.at(-1)) {
|
||||||
const defaultChip: DocChip = {
|
const history = histories.at(-1);
|
||||||
docId: this.doc.id,
|
if (!history) return;
|
||||||
state: 'candidate',
|
items.push(...history.messages);
|
||||||
};
|
AIProvider.LAST_ROOT_SESSION_ID = history.sessionId;
|
||||||
const nextChips =
|
}
|
||||||
items.length === 0 && chips.length === 0 ? [defaultChip] : chips;
|
|
||||||
this.chatContextValue = {
|
|
||||||
...this.chatContextValue,
|
|
||||||
items: items.sort((a, b) => {
|
|
||||||
return (
|
|
||||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
chips: nextChips,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.isLoading = false;
|
this.chatContextValue = {
|
||||||
this._scrollToEnd();
|
...this.chatContextValue,
|
||||||
})().catch(console.error);
|
items: items.sort(
|
||||||
}, 200);
|
(a, b) =>
|
||||||
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
this._scrollToEnd();
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _updateChips = async () => {
|
||||||
|
if (!this._chatSessionId || !this._chatContextId) return;
|
||||||
|
|
||||||
|
const candidateChip: DocChip = {
|
||||||
|
docId: this.doc.id,
|
||||||
|
state: 'candidate',
|
||||||
|
};
|
||||||
|
let chips: (DocChip | FileChip)[] = [];
|
||||||
|
if (this._chatContextId) {
|
||||||
|
const { docs = [], files = [] } =
|
||||||
|
(await AIProvider.context?.getContextDocsAndFiles(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
this._chatSessionId,
|
||||||
|
this._chatContextId
|
||||||
|
)) || {};
|
||||||
|
const list = [...docs, ...files].sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
|
);
|
||||||
|
chips = list.map(item => {
|
||||||
|
let chip: DocChip | FileChip;
|
||||||
|
if (isDocContext(item)) {
|
||||||
|
chip = {
|
||||||
|
docId: item.id,
|
||||||
|
state: 'processing',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
chip = {
|
||||||
|
fileId: item.id,
|
||||||
|
state: item.status === 'finished' ? 'success' : item.status,
|
||||||
|
fileName: item.name,
|
||||||
|
fileType: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return chip;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chatContextValue = {
|
||||||
|
...this.chatContextValue,
|
||||||
|
chips: chips.length === 0 ? [candidateChip] : chips,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host!: EditorHost;
|
accessor host!: EditorHost;
|
||||||
@@ -191,9 +230,12 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
|
|||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
markdown: '',
|
markdown: '',
|
||||||
chatSessionId: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _chatSessionId: string | undefined;
|
||||||
|
|
||||||
|
private _chatContextId: string | undefined;
|
||||||
|
|
||||||
private readonly _scrollToEnd = () => {
|
private readonly _scrollToEnd = () => {
|
||||||
this._chatMessages.value?.scrollToEnd();
|
this._chatMessages.value?.scrollToEnd();
|
||||||
};
|
};
|
||||||
@@ -214,26 +256,40 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
await AIProvider.histories?.cleanup(this.doc.workspace.id, this.doc.id, [
|
await AIProvider.histories?.cleanup(this.doc.workspace.id, this.doc.id, [
|
||||||
this.chatContextValue.chatSessionId ?? '',
|
this._chatSessionId ?? '',
|
||||||
...(
|
...(
|
||||||
this.chatContextValue.items.filter(
|
this.chatContextValue.items.filter(
|
||||||
item => 'sessionId' in item
|
item => 'sessionId' in item
|
||||||
) as ChatAction[]
|
) as ChatAction[]
|
||||||
).map(item => item.sessionId),
|
).map(item => item.sessionId),
|
||||||
]);
|
]);
|
||||||
this.chatContextValue.chatSessionId = null;
|
|
||||||
notification.toast('History cleared');
|
notification.toast('History cleared');
|
||||||
this._resetItems();
|
await this._updateHistory();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly _initPanel = async () => {
|
||||||
|
const userId = (await AIProvider.userInfo)?.id;
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
this._chatSessionId = await AIProvider.session?.createSession(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
this.doc.id
|
||||||
|
);
|
||||||
|
if (this._chatSessionId) {
|
||||||
|
this._chatContextId = await AIProvider.context?.createContext(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
this._chatSessionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this._updateHistory();
|
||||||
|
await this._updateChips();
|
||||||
|
};
|
||||||
|
|
||||||
protected override updated(_changedProperties: PropertyValues) {
|
protected override updated(_changedProperties: PropertyValues) {
|
||||||
if (_changedProperties.has('doc')) {
|
if (_changedProperties.has('doc')) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(async () => {
|
||||||
this.chatContextValue.chatSessionId = null;
|
await this._initPanel();
|
||||||
// TODO get from CopilotContext
|
|
||||||
this.chatContextValue.chips = [];
|
|
||||||
this._resetItems();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,15 +322,13 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
|
|||||||
event === 'finished' &&
|
event === 'finished' &&
|
||||||
(status === 'idle' || status === 'success')
|
(status === 'idle' || status === 'success')
|
||||||
) {
|
) {
|
||||||
this._resetItems();
|
this._updateHistory().catch(console.error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this._disposables.add(
|
this._disposables.add(
|
||||||
AIProvider.slots.userInfo.on(userInfo => {
|
AIProvider.slots.userInfo.on(async () => {
|
||||||
if (userInfo) {
|
await this._initPanel();
|
||||||
this._resetItems();
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this._disposables.add(
|
this._disposables.add(
|
||||||
@@ -318,6 +372,7 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
|
|||||||
<chat-panel-messages
|
<chat-panel-messages
|
||||||
${ref(this._chatMessages)}
|
${ref(this._chatMessages)}
|
||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
|
.chatSessionId=${this._chatSessionId}
|
||||||
.updateContext=${this.updateContext}
|
.updateContext=${this.updateContext}
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.isLoading=${this.isLoading}
|
.isLoading=${this.isLoading}
|
||||||
@@ -326,12 +381,14 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
|
|||||||
<chat-panel-chips
|
<chat-panel-chips
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
|
.chatContextId=${this._chatContextId}
|
||||||
.updateContext=${this.updateContext}
|
.updateContext=${this.updateContext}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.docSearchMenuConfig=${this.docSearchMenuConfig}
|
.docSearchMenuConfig=${this.docSearchMenuConfig}
|
||||||
></chat-panel-chips>
|
></chat-panel-chips>
|
||||||
<chat-panel-input
|
<chat-panel-input
|
||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
|
.chatSessionId=${this._chatSessionId}
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
.updateContext=${this.updateContext}
|
.updateContext=${this.updateContext}
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
|
|||||||
@@ -74,6 +74,14 @@ export class AIProvider {
|
|||||||
return AIProvider.instance.histories;
|
return AIProvider.instance.histories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get session() {
|
||||||
|
return AIProvider.instance.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get context() {
|
||||||
|
return AIProvider.instance.context;
|
||||||
|
}
|
||||||
|
|
||||||
static get actionHistory() {
|
static get actionHistory() {
|
||||||
return AIProvider.instance.actionHistory;
|
return AIProvider.instance.actionHistory;
|
||||||
}
|
}
|
||||||
@@ -100,6 +108,10 @@ export class AIProvider {
|
|||||||
|
|
||||||
private histories: BlockSuitePresets.AIHistoryService | null = null;
|
private histories: BlockSuitePresets.AIHistoryService | null = null;
|
||||||
|
|
||||||
|
private session: BlockSuitePresets.AISessionService | null = null;
|
||||||
|
|
||||||
|
private context: BlockSuitePresets.AIContextService | null = null;
|
||||||
|
|
||||||
private toggleGeneralAIOnboarding: ((value: boolean) => void) | null = null;
|
private toggleGeneralAIOnboarding: ((value: boolean) => void) | null = null;
|
||||||
|
|
||||||
private forkChat:
|
private forkChat:
|
||||||
@@ -259,6 +271,16 @@ export class AIProvider {
|
|||||||
fn: () => AIUserInfo | Promise<AIUserInfo> | null
|
fn: () => AIUserInfo | Promise<AIUserInfo> | null
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
|
static provide(
|
||||||
|
id: 'session',
|
||||||
|
service: BlockSuitePresets.AISessionService
|
||||||
|
): void;
|
||||||
|
|
||||||
|
static provide(
|
||||||
|
id: 'context',
|
||||||
|
service: BlockSuitePresets.AIContextService
|
||||||
|
): void;
|
||||||
|
|
||||||
static provide(
|
static provide(
|
||||||
id: 'histories',
|
id: 'histories',
|
||||||
service: BlockSuitePresets.AIHistoryService
|
service: BlockSuitePresets.AIHistoryService
|
||||||
@@ -292,6 +314,12 @@ export class AIProvider {
|
|||||||
} else if (id === 'histories') {
|
} else if (id === 'histories') {
|
||||||
AIProvider.instance.histories =
|
AIProvider.instance.histories =
|
||||||
action as BlockSuitePresets.AIHistoryService;
|
action as BlockSuitePresets.AIHistoryService;
|
||||||
|
} else if (id === 'session') {
|
||||||
|
AIProvider.instance.session =
|
||||||
|
action as BlockSuitePresets.AISessionService;
|
||||||
|
} else if (id === 'context') {
|
||||||
|
AIProvider.instance.context =
|
||||||
|
action as BlockSuitePresets.AIContextService;
|
||||||
} else if (id === 'photoEngine') {
|
} else if (id === 'photoEngine') {
|
||||||
AIProvider.instance.photoEngine =
|
AIProvider.instance.photoEngine =
|
||||||
action as BlockSuitePresets.AIPhotoEngineService;
|
action as BlockSuitePresets.AIPhotoEngineService;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
|
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
|
||||||
import {
|
import {
|
||||||
|
addContextDocMutation,
|
||||||
cleanupCopilotSessionMutation,
|
cleanupCopilotSessionMutation,
|
||||||
|
createCopilotContextMutation,
|
||||||
createCopilotMessageMutation,
|
createCopilotMessageMutation,
|
||||||
createCopilotSessionMutation,
|
createCopilotSessionMutation,
|
||||||
forkCopilotSessionMutation,
|
forkCopilotSessionMutation,
|
||||||
@@ -9,8 +11,11 @@ import {
|
|||||||
getCopilotSessionsQuery,
|
getCopilotSessionsQuery,
|
||||||
GraphQLError,
|
GraphQLError,
|
||||||
type GraphQLQuery,
|
type GraphQLQuery,
|
||||||
|
listContextDocsAndFilesQuery,
|
||||||
|
listContextQuery,
|
||||||
type QueryOptions,
|
type QueryOptions,
|
||||||
type QueryResponse,
|
type QueryResponse,
|
||||||
|
removeContextDocMutation,
|
||||||
type RequestOptions,
|
type RequestOptions,
|
||||||
updateCopilotSessionMutation,
|
updateCopilotSessionMutation,
|
||||||
UserFriendlyError,
|
UserFriendlyError,
|
||||||
@@ -209,6 +214,74 @@ export class CopilotClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createContext(workspaceId: string, sessionId: string) {
|
||||||
|
const res = await this.gql({
|
||||||
|
query: createCopilotContextMutation,
|
||||||
|
variables: {
|
||||||
|
workspaceId,
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return res.createCopilotContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContextId(workspaceId: string, sessionId: string) {
|
||||||
|
const res = await this.gql({
|
||||||
|
query: listContextQuery,
|
||||||
|
variables: {
|
||||||
|
workspaceId,
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return res.currentUser?.copilot?.contexts?.[0]?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addContextDoc(options: OptionsField<typeof addContextDocMutation>) {
|
||||||
|
const res = await this.gql({
|
||||||
|
query: addContextDocMutation,
|
||||||
|
variables: {
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return res.addContextDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeContextDoc(
|
||||||
|
options: OptionsField<typeof removeContextDocMutation>
|
||||||
|
) {
|
||||||
|
const res = await this.gql({
|
||||||
|
query: removeContextDocMutation,
|
||||||
|
variables: {
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return res.removeContextDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addContextFile() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeContextFile() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContextDocsAndFiles(
|
||||||
|
workspaceId: string,
|
||||||
|
sessionId: string,
|
||||||
|
contextId: string
|
||||||
|
) {
|
||||||
|
const res = await this.gql({
|
||||||
|
query: listContextDocsAndFilesQuery,
|
||||||
|
variables: {
|
||||||
|
workspaceId,
|
||||||
|
sessionId,
|
||||||
|
contextId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return res.currentUser?.copilot?.contexts?.[0];
|
||||||
|
}
|
||||||
|
|
||||||
async chatText({
|
async chatText({
|
||||||
sessionId,
|
sessionId,
|
||||||
messageId,
|
messageId,
|
||||||
|
|||||||
@@ -31,22 +31,29 @@ export type ToImageOptions = TextToTextOptions & {
|
|||||||
seed?: string;
|
seed?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createChatSession({
|
export async function createChatSession({
|
||||||
client,
|
client,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
docId,
|
docId,
|
||||||
promptName,
|
promptName = 'Chat With AFFiNE AI',
|
||||||
}: {
|
}: {
|
||||||
client: CopilotClient;
|
client: CopilotClient;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
docId: string;
|
docId: string;
|
||||||
promptName: string;
|
promptName?: string;
|
||||||
}) {
|
}) {
|
||||||
return client.createSession({
|
const sessionId = await client.createSession({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
docId,
|
docId,
|
||||||
promptName,
|
promptName,
|
||||||
});
|
});
|
||||||
|
// always update the prompt name
|
||||||
|
await updateChatSession({
|
||||||
|
sessionId,
|
||||||
|
client,
|
||||||
|
promptName,
|
||||||
|
});
|
||||||
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateChatSession({
|
export function updateChatSession({
|
||||||
@@ -119,7 +126,8 @@ async function createSessionMessage({
|
|||||||
}
|
}
|
||||||
const hasAttachments = attachments && attachments.length > 0;
|
const hasAttachments = attachments && attachments.length > 0;
|
||||||
const sessionId = await (providedSessionId ??
|
const sessionId = await (providedSessionId ??
|
||||||
client.createSession({
|
createChatSession({
|
||||||
|
client,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
docId,
|
docId,
|
||||||
promptName: promptName as string,
|
promptName: promptName as string,
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
||||||
import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onboarding/apis';
|
import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onboarding/apis';
|
||||||
import type { AINetworkSearchService } from '@affine/core/modules/ai-button/services/network-search';
|
|
||||||
import type { GlobalDialogService } from '@affine/core/modules/dialogs';
|
import type { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import {
|
import {
|
||||||
type getCopilotHistoriesQuery,
|
type getCopilotHistoriesQuery,
|
||||||
type RequestOptions,
|
type RequestOptions,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { UnauthorizedError } from '@blocksuite/affine/blocks';
|
|
||||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -39,80 +37,19 @@ const processTypeToPromptName = new Map(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// a single workspace should have only a single chat session
|
|
||||||
// user-id:workspace-id:doc-id -> chat session id
|
|
||||||
const chatSessions = new Map<
|
|
||||||
string,
|
|
||||||
{ getSessionId: Promise<string>; promptName: string }
|
|
||||||
>();
|
|
||||||
|
|
||||||
export function setupAIProvider(
|
export function setupAIProvider(
|
||||||
client: CopilotClient,
|
client: CopilotClient,
|
||||||
globalDialogService: GlobalDialogService,
|
globalDialogService: GlobalDialogService
|
||||||
networkSearchService: AINetworkSearchService
|
|
||||||
) {
|
) {
|
||||||
function getChatPrompt(options: BlockSuitePresets.ChatOptions) {
|
|
||||||
const { attachments, docs } = options;
|
|
||||||
if (attachments?.length || docs?.length) {
|
|
||||||
return 'Chat With AFFiNE AI';
|
|
||||||
}
|
|
||||||
const { enabled, visible } = networkSearchService;
|
|
||||||
return visible.value && enabled.value
|
|
||||||
? 'Search With AFFiNE AI'
|
|
||||||
: 'Chat With AFFiNE AI';
|
|
||||||
}
|
|
||||||
async function getChatSessionId(options: BlockSuitePresets.ChatOptions) {
|
|
||||||
const userId = (await AIProvider.userInfo)?.id;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new UnauthorizedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { workspaceId, docId } = options;
|
|
||||||
const storeKey = `${userId}:${workspaceId}:${docId}`;
|
|
||||||
const promptName = getChatPrompt(options);
|
|
||||||
if (!chatSessions.has(storeKey)) {
|
|
||||||
chatSessions.set(storeKey, {
|
|
||||||
getSessionId: createChatSession({
|
|
||||||
client,
|
|
||||||
workspaceId,
|
|
||||||
docId,
|
|
||||||
promptName,
|
|
||||||
}).then(sessionId => {
|
|
||||||
return updateChatSession({
|
|
||||||
sessionId,
|
|
||||||
client,
|
|
||||||
promptName,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
promptName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
/* oxlint-disable @typescript-eslint/no-non-null-assertion */
|
|
||||||
const { getSessionId, promptName: prevName } =
|
|
||||||
chatSessions.get(storeKey)!;
|
|
||||||
const sessionId = await getSessionId;
|
|
||||||
//update prompt name
|
|
||||||
if (prevName !== promptName) {
|
|
||||||
await updateChatSession({
|
|
||||||
sessionId,
|
|
||||||
client,
|
|
||||||
promptName,
|
|
||||||
});
|
|
||||||
chatSessions.set(storeKey, { getSessionId, promptName });
|
|
||||||
}
|
|
||||||
return sessionId;
|
|
||||||
} catch (err) {
|
|
||||||
// do not cache the error
|
|
||||||
chatSessions.delete(storeKey);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//#region actions
|
//#region actions
|
||||||
AIProvider.provide('chat', options => {
|
AIProvider.provide('chat', options => {
|
||||||
const sessionId = options.sessionId ?? getChatSessionId(options);
|
const sessionId =
|
||||||
|
options.sessionId ??
|
||||||
|
createChatSession({
|
||||||
|
client,
|
||||||
|
workspaceId: options.workspaceId,
|
||||||
|
docId: options.docId,
|
||||||
|
});
|
||||||
const { input, docs, ...rest } = options;
|
const { input, docs, ...rest } = options;
|
||||||
const params = docs?.length
|
const params = docs?.length
|
||||||
? {
|
? {
|
||||||
@@ -473,6 +410,56 @@ Could you make a new website based on these notes and send back just the html fi
|
|||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
AIProvider.provide('session', {
|
||||||
|
createSession: async (
|
||||||
|
workspaceId: string,
|
||||||
|
docId: string,
|
||||||
|
promptName?: string
|
||||||
|
) => {
|
||||||
|
return createChatSession({
|
||||||
|
client,
|
||||||
|
workspaceId,
|
||||||
|
docId,
|
||||||
|
promptName,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateSession: async (sessionId: string, promptName: string) => {
|
||||||
|
return updateChatSession({
|
||||||
|
client,
|
||||||
|
sessionId,
|
||||||
|
promptName,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
AIProvider.provide('context', {
|
||||||
|
createContext: async (workspaceId: string, sessionId: string) => {
|
||||||
|
return client.createContext(workspaceId, sessionId);
|
||||||
|
},
|
||||||
|
getContextId: async (workspaceId: string, sessionId: string) => {
|
||||||
|
return client.getContextId(workspaceId, sessionId);
|
||||||
|
},
|
||||||
|
addContextDoc: async (options: { contextId: string; docId: string }) => {
|
||||||
|
return client.addContextDoc(options);
|
||||||
|
},
|
||||||
|
removeContextDoc: async (options: { contextId: string; docId: string }) => {
|
||||||
|
return client.removeContextDoc(options);
|
||||||
|
},
|
||||||
|
addContextFile: async () => {
|
||||||
|
return client.addContextFile();
|
||||||
|
},
|
||||||
|
removeContextFile: async () => {
|
||||||
|
return client.removeContextFile();
|
||||||
|
},
|
||||||
|
getContextDocsAndFiles: async (
|
||||||
|
workspaceId: string,
|
||||||
|
sessionId: string,
|
||||||
|
contextId: string
|
||||||
|
) => {
|
||||||
|
return client.getContextDocsAndFiles(workspaceId, sessionId, contextId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
AIProvider.provide('histories', {
|
AIProvider.provide('histories', {
|
||||||
actions: async (
|
actions: async (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { SyncAwareness } from '@affine/core/components/affine/awareness';
|
|||||||
import { useRegisterFindInPageCommands } from '@affine/core/components/hooks/affine/use-register-find-in-page-commands';
|
import { useRegisterFindInPageCommands } from '@affine/core/components/hooks/affine/use-register-find-in-page-commands';
|
||||||
import { useRegisterWorkspaceCommands } from '@affine/core/components/hooks/use-register-workspace-commands';
|
import { useRegisterWorkspaceCommands } from '@affine/core/components/hooks/use-register-workspace-commands';
|
||||||
import { OverCapacityNotification } from '@affine/core/components/over-capacity';
|
import { OverCapacityNotification } from '@affine/core/components/over-capacity';
|
||||||
import { AINetworkSearchService } from '@affine/core/modules/ai-button/services/network-search';
|
|
||||||
import {
|
import {
|
||||||
EventSourceService,
|
EventSourceService,
|
||||||
FetchService,
|
FetchService,
|
||||||
@@ -144,7 +143,6 @@ export const WorkspaceSideEffects = () => {
|
|||||||
const graphqlService = useService(GraphQLService);
|
const graphqlService = useService(GraphQLService);
|
||||||
const eventSourceService = useService(EventSourceService);
|
const eventSourceService = useService(EventSourceService);
|
||||||
const fetchService = useService(FetchService);
|
const fetchService = useService(FetchService);
|
||||||
const networkSearchService = useService(AINetworkSearchService);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const dispose = setupAIProvider(
|
const dispose = setupAIProvider(
|
||||||
@@ -153,8 +151,7 @@ export const WorkspaceSideEffects = () => {
|
|||||||
fetchService.fetch,
|
fetchService.fetch,
|
||||||
eventSourceService.eventSource
|
eventSourceService.eventSource
|
||||||
),
|
),
|
||||||
globalDialogService,
|
globalDialogService
|
||||||
networkSearchService
|
|
||||||
);
|
);
|
||||||
return () => {
|
return () => {
|
||||||
dispose();
|
dispose();
|
||||||
@@ -164,7 +161,6 @@ export const WorkspaceSideEffects = () => {
|
|||||||
fetchService,
|
fetchService,
|
||||||
workspaceDialogService,
|
workspaceDialogService,
|
||||||
graphqlService,
|
graphqlService,
|
||||||
networkSearchService,
|
|
||||||
globalDialogService,
|
globalDialogService,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -193,11 +193,31 @@ export const removeContextDocMutation = {
|
|||||||
definitionName: 'removeContextDoc',
|
definitionName: 'removeContextDoc',
|
||||||
containsFile: false,
|
containsFile: false,
|
||||||
query: `
|
query: `
|
||||||
mutation removeContextDoc($options: RemoveContextFileInput!) {
|
mutation removeContextDoc($options: RemoveContextDocInput!) {
|
||||||
removeContextDoc(options: $options)
|
removeContextDoc(options: $options)
|
||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const listContextDocsAndFilesQuery = {
|
||||||
|
id: 'listContextDocsAndFilesQuery' as const,
|
||||||
|
operationName: 'listContextDocsAndFiles',
|
||||||
|
definitionName: 'currentUser',
|
||||||
|
containsFile: false,
|
||||||
|
query: `
|
||||||
|
query listContextDocsAndFiles($workspaceId: String!, $sessionId: String!, $contextId: String!) {
|
||||||
|
currentUser {
|
||||||
|
copilot(workspaceId: $workspaceId) {
|
||||||
|
contexts(sessionId: $sessionId, contextId: $contextId) {
|
||||||
|
docs {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const listContextQuery = {
|
export const listContextQuery = {
|
||||||
id: 'listContextQuery' as const,
|
id: 'listContextQuery' as const,
|
||||||
operationName: 'listContext',
|
operationName: 'listContext',
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ export interface AddContextDocInput {
|
|||||||
docId: Scalars['String']['input'];
|
docId: Scalars['String']['input'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RemoveContextDocInput {
|
||||||
|
contextId: Scalars['String']['input'];
|
||||||
|
docId: Scalars['String']['input'];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AlreadyInSpaceDataType {
|
export interface AlreadyInSpaceDataType {
|
||||||
__typename?: 'AlreadyInSpaceDataType';
|
__typename?: 'AlreadyInSpaceDataType';
|
||||||
spaceId: Scalars['String']['output'];
|
spaceId: Scalars['String']['output'];
|
||||||
@@ -1013,7 +1018,7 @@ export interface MutationReleaseDeletedBlobsArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MutationRemoveContextDocArgs {
|
export interface MutationRemoveContextDocArgs {
|
||||||
options: RemoveContextFileInput;
|
options: RemoveContextDocInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MutationRemoveWorkspaceFeatureArgs {
|
export interface MutationRemoveWorkspaceFeatureArgs {
|
||||||
@@ -1310,11 +1315,6 @@ export interface RemoveAvatar {
|
|||||||
success: Scalars['Boolean']['output'];
|
success: Scalars['Boolean']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoveContextFileInput {
|
|
||||||
contextId: Scalars['String']['input'];
|
|
||||||
fileId: Scalars['String']['input'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RevokeDocUserRoleInput {
|
export interface RevokeDocUserRoleInput {
|
||||||
docId: Scalars['String']['input'];
|
docId: Scalars['String']['input'];
|
||||||
userId: Scalars['String']['input'];
|
userId: Scalars['String']['input'];
|
||||||
@@ -1952,28 +1952,56 @@ export type AddContextDocMutationVariables = Exact<{
|
|||||||
options: AddContextDocInput;
|
options: AddContextDocInput;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type RemoveContextDocMutationVariables = Exact<{
|
||||||
|
options: RemoveContextDocInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type AddContextDocMutation = {
|
export type AddContextDocMutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
addContextDoc: Array<{
|
addContextDoc: Array<{
|
||||||
__typename?: 'CopilotContextListItem';
|
__typename?: 'CopilotContextListItem';
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: number;
|
|
||||||
name: string | null;
|
|
||||||
chunkSize: number | null;
|
|
||||||
status: ContextFileStatus | null;
|
|
||||||
blobId: string | null;
|
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RemoveContextDocMutationVariables = Exact<{
|
|
||||||
options: RemoveContextFileInput;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type RemoveContextDocMutation = {
|
export type RemoveContextDocMutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
removeContextDoc: boolean;
|
removeContextDoc: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ListContextDocsAndFilesQueryVariables = Exact<{
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
|
sessionId: Scalars['String']['input'];
|
||||||
|
contextId: Scalars['String']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type ListContextDocsAndFilesQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
currentUser: {
|
||||||
|
__typename?: 'UserType';
|
||||||
|
copilot: {
|
||||||
|
__typename?: 'Copilot';
|
||||||
|
contexts: Array<{
|
||||||
|
__typename?: 'CopilotContext';
|
||||||
|
docs: Array<{
|
||||||
|
__typename?: 'CopilotContextDoc';
|
||||||
|
id: string;
|
||||||
|
createdAt: number;
|
||||||
|
}>;
|
||||||
|
files: Array<{
|
||||||
|
__typename?: 'CopilotContextFile';
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
blobId: string;
|
||||||
|
chunkSize: number;
|
||||||
|
status: ContextFileStatus;
|
||||||
|
createdAt: number;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ListContextQueryVariables = Exact<{
|
export type ListContextQueryVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
sessionId: Scalars['String']['input'];
|
sessionId: Scalars['String']['input'];
|
||||||
@@ -3372,6 +3400,11 @@ export type Queries =
|
|||||||
variables: ListBlobsQueryVariables;
|
variables: ListBlobsQueryVariables;
|
||||||
response: ListBlobsQuery;
|
response: ListBlobsQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'listContextDocsAndFilesQuery';
|
||||||
|
variables: ListContextDocsAndFilesQueryVariables;
|
||||||
|
response: ListContextDocsAndFilesQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'listContextQuery';
|
name: 'listContextQuery';
|
||||||
variables: ListContextQueryVariables;
|
variables: ListContextQueryVariables;
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ import {
|
|||||||
getBlockSuiteEditorTitle,
|
getBlockSuiteEditorTitle,
|
||||||
waitForEditorLoad,
|
waitForEditorLoad,
|
||||||
} from '@affine-test/kit/utils/page-logic';
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
import {
|
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
|
||||||
clickSideBarAllPageButton,
|
|
||||||
clickSideBarUseAvatar,
|
|
||||||
} from '@affine-test/kit/utils/sidebar';
|
|
||||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||||
import { expect, type Page } from '@playwright/test';
|
import { expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
@@ -394,20 +391,12 @@ test.describe('chat panel', () => {
|
|||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
await createLocalWorkspace({ name: 'test' }, page);
|
await createLocalWorkspace({ name: 'test' }, page);
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
await clickSideBarUseAvatar(page);
|
|
||||||
await page.getByTestId('workspace-modal-account-settings-option').click();
|
|
||||||
await page.getByTestId('experimental-features-trigger').click();
|
|
||||||
await page
|
|
||||||
.getByTestId('experimental-prompt')
|
|
||||||
.getByTestId('affine-checkbox')
|
|
||||||
.click();
|
|
||||||
await page.getByTestId('experimental-confirm-button').click();
|
|
||||||
await page.getByTestId('enable_ai_network_search').click();
|
|
||||||
await page.getByTestId('modal-close-button').click();
|
|
||||||
await openChat(page);
|
await openChat(page);
|
||||||
await page.getByTestId('chat-network-search').click();
|
await page.getByTestId('chat-network-search').click();
|
||||||
await typeChatSequentially(page, 'What is the weather in Shanghai today?');
|
await typeChatSequentially(page, 'What is the weather in Shanghai today?');
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
let history = await collectChat(page);
|
let history = await collectChat(page);
|
||||||
expect(history[0]).toEqual({
|
expect(history[0]).toEqual({
|
||||||
name: 'You',
|
name: 'You',
|
||||||
@@ -423,6 +412,7 @@ test.describe('chat panel', () => {
|
|||||||
await page.getByTestId('chat-network-search').click();
|
await page.getByTestId('chat-network-search').click();
|
||||||
await typeChatSequentially(page, 'What is the weather in Shanghai today?');
|
await typeChatSequentially(page, 'What is the weather in Shanghai today?');
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
history = await collectChat(page);
|
history = await collectChat(page);
|
||||||
expect(history[0]).toEqual({
|
expect(history[0]).toEqual({
|
||||||
name: 'You',
|
name: 'You',
|
||||||
@@ -770,11 +760,13 @@ test.describe('chat with doc', () => {
|
|||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
||||||
expect(await chip.getAttribute('data-state')).toBe('candidate');
|
expect(await chip.getAttribute('data-state')).toBe('candidate');
|
||||||
await chip.click();
|
await chip.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
||||||
expect(await chip.getAttribute('data-state')).toBe('success');
|
expect(await chip.getAttribute('data-state')).toBe('success');
|
||||||
|
|
||||||
await typeChatSequentially(page, 'What is AFFiNE AI?');
|
await typeChatSequentially(page, 'What is AFFiNE AI?');
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
const history = await collectChat(page);
|
const history = await collectChat(page);
|
||||||
expect(history[0]).toEqual({
|
expect(history[0]).toEqual({
|
||||||
name: 'You',
|
name: 'You',
|
||||||
@@ -786,5 +778,13 @@ test.describe('chat with doc', () => {
|
|||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
await clearChat(page);
|
await clearChat(page);
|
||||||
expect((await collectChat(page)).length).toBe(0);
|
expect((await collectChat(page)).length).toBe(0);
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await openChat(page);
|
||||||
|
expect(await chipTitle.textContent()).toBe('AFFiNE AI');
|
||||||
|
const chip2 = await page.getByTestId('chat-panel-chip');
|
||||||
|
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
||||||
|
expect(await chip2.getAttribute('data-state')).toBe('success');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user