mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 03:33:06 +08:00
feat(core): update ai add context button ui (#13172)
Close [AI-301](https://linear.app/affine-design/issue/AI-301) <img width="571" height="204" alt="截屏2025-07-11 17 33 01" src="https://github.com/user-attachments/assets/3b7ed81f-1137-4c01-8fe2-9fe5ebf2adf3" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a new component for adding context (images, documents, tags, collections) to AI chat via a plus button and popover menu. * Added notification feedback for duplicate chip additions and image upload limits. * Chips panel now supports collapsing and expanding for improved UI control. * **Improvements** * Refactored chip management for better error handling, feedback, and external control. * Streamlined image and document uploads through a unified menu-driven interface. * Enhanced chip management methods with clearer naming and robust synchronization. * Updated chat input to delegate image upload and context additions to the new add-context component. * **Bug Fixes** * Improved cancellation and cleanup of ongoing chip addition operations to prevent conflicts. * **Tests** * Updated end-to-end tests to reflect the new menu-driven image upload workflow and removed legacy checks. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: fengmk2 <fengmk2@gmail.com>
This commit is contained in:
@@ -20,10 +20,8 @@ import { property, state } from 'lit/decorators.js';
|
|||||||
import { keyed } from 'lit/directives/keyed.js';
|
import { keyed } from 'lit/directives/keyed.js';
|
||||||
|
|
||||||
import { AffineIcon } from '../_common/icons';
|
import { AffineIcon } from '../_common/icons';
|
||||||
import type {
|
import type { SearchMenuConfig } from '../components/ai-chat-add-context';
|
||||||
DocDisplayConfig,
|
import type { DocDisplayConfig } from '../components/ai-chat-chips';
|
||||||
SearchMenuConfig,
|
|
||||||
} from '../components/ai-chat-chips';
|
|
||||||
import type { ChatContextValue } from '../components/ai-chat-content';
|
import type { ChatContextValue } from '../components/ai-chat-content';
|
||||||
import type {
|
import type {
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
@@ -385,6 +383,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||||
.affineThemeService=${this.affineThemeService}
|
.affineThemeService=${this.affineThemeService}
|
||||||
.notificationService=${this.notificationService}
|
.notificationService=${this.notificationService}
|
||||||
|
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||||
></playground-content>
|
></playground-content>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
import { createLitPortal } from '@blocksuite/affine/components/portal';
|
||||||
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
|
import { PlusIcon } from '@blocksuite/icons/lit';
|
||||||
|
import { flip, offset } from '@floating-ui/dom';
|
||||||
|
import { css, html } from 'lit';
|
||||||
|
import { property, query } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
import type { ChatChip, DocDisplayConfig } from '../ai-chat-chips';
|
||||||
|
import type { SearchMenuConfig } from './type';
|
||||||
|
|
||||||
|
export class AIChatAddContext extends SignalWatcher(
|
||||||
|
WithDisposable(ShadowlessElement)
|
||||||
|
) {
|
||||||
|
static override styles = css`
|
||||||
|
.ai-chat-add-context {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor addChip!: (chip: ChatChip) => Promise<void>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor addImages!: (images: File[]) => void;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor docDisplayConfig!: DocDisplayConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor searchMenuConfig!: SearchMenuConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor portalContainer: HTMLElement | null = null;
|
||||||
|
|
||||||
|
@query('.ai-chat-add-context')
|
||||||
|
accessor addButton!: HTMLDivElement;
|
||||||
|
|
||||||
|
private abortController: AbortController | null = null;
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="ai-chat-add-context"
|
||||||
|
data-testid="chat-panel-with-button"
|
||||||
|
@click=${this.toggleAddDocMenu}
|
||||||
|
>
|
||||||
|
${PlusIcon()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly toggleAddDocMenu = () => {
|
||||||
|
if (this.abortController) {
|
||||||
|
this.abortController.abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.abortController = new AbortController();
|
||||||
|
this.abortController.signal.addEventListener('abort', () => {
|
||||||
|
this.abortController = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
createLitPortal({
|
||||||
|
template: html`
|
||||||
|
<chat-panel-add-popover
|
||||||
|
.addChip=${this.addChip}
|
||||||
|
.addImages=${this.addImages}
|
||||||
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
|
.abortController=${this.abortController}
|
||||||
|
></chat-panel-add-popover>
|
||||||
|
`,
|
||||||
|
portalStyles: {
|
||||||
|
zIndex: 'var(--affine-z-index-popover)',
|
||||||
|
},
|
||||||
|
container: this.portalContainer ?? document.body,
|
||||||
|
computePosition: {
|
||||||
|
referenceElement: this.addButton,
|
||||||
|
placement: 'top-start',
|
||||||
|
middleware: [offset({ crossAxis: -30, mainAxis: 8 }), flip()],
|
||||||
|
autoUpdate: { animationFrame: true },
|
||||||
|
},
|
||||||
|
abortController: this.abortController,
|
||||||
|
closeOnClickAway: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './ai-chat-add-context';
|
||||||
|
export * from './type';
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type {
|
||||||
|
SearchCollectionMenuAction,
|
||||||
|
SearchDocMenuAction,
|
||||||
|
SearchTagMenuAction,
|
||||||
|
} from '@affine/core/modules/search-menu/services';
|
||||||
|
import type { LinkedMenuGroup } from '@blocksuite/affine/widgets/linked-doc';
|
||||||
|
|
||||||
|
export interface SearchMenuConfig {
|
||||||
|
getDocMenuGroup: (
|
||||||
|
query: string,
|
||||||
|
action: SearchDocMenuAction,
|
||||||
|
abortSignal: AbortSignal
|
||||||
|
) => LinkedMenuGroup;
|
||||||
|
getTagMenuGroup: (
|
||||||
|
query: string,
|
||||||
|
action: SearchTagMenuAction,
|
||||||
|
abortSignal: AbortSignal
|
||||||
|
) => LinkedMenuGroup;
|
||||||
|
getCollectionMenuGroup: (
|
||||||
|
query: string,
|
||||||
|
action: SearchCollectionMenuAction,
|
||||||
|
abortSignal: AbortSignal
|
||||||
|
) => LinkedMenuGroup;
|
||||||
|
}
|
||||||
@@ -21,8 +21,8 @@ import { css, html, type TemplateResult } from 'lit';
|
|||||||
import { property, query, state } from 'lit/decorators.js';
|
import { property, query, state } from 'lit/decorators.js';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
|
|
||||||
import { MAX_IMAGE_COUNT } from '../ai-chat-input';
|
import type { SearchMenuConfig } from '../ai-chat-add-context';
|
||||||
import type { ChatChip, DocDisplayConfig, SearchMenuConfig } from './type';
|
import type { ChatChip, DocDisplayConfig } from './type';
|
||||||
|
|
||||||
enum AddPopoverMode {
|
enum AddPopoverMode {
|
||||||
Default = 'default',
|
Default = 'default',
|
||||||
@@ -165,35 +165,31 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
|||||||
const files = await openFilesWith();
|
const files = await openFilesWith();
|
||||||
if (!files || files.length === 0) return;
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
this.abortController.abort();
|
||||||
const images = files.filter(file => file.type.startsWith('image/'));
|
const images = files.filter(file => file.type.startsWith('image/'));
|
||||||
if (images.length > 0) {
|
if (images.length > 0) {
|
||||||
this.addImages(images);
|
this.addImages(images);
|
||||||
}
|
}
|
||||||
|
|
||||||
const others = files.filter(file => !file.type.startsWith('image/'));
|
const others = files.filter(file => !file.type.startsWith('image/'));
|
||||||
for (const file of others) {
|
const addChipPromises = others.map(async file => {
|
||||||
if (file.size > 50 * 1024 * 1024) {
|
if (file.size > 50 * 1024 * 1024) {
|
||||||
toast(`${file.name} is too large, please upload a file less than 50MB`);
|
toast(`${file.name} is too large, please upload a file less than 50MB`);
|
||||||
} else {
|
return;
|
||||||
await this.addChip({
|
|
||||||
file,
|
|
||||||
state: 'processing',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
await this.addChip({
|
||||||
|
file,
|
||||||
|
state: 'processing',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await Promise.all(addChipPromises);
|
||||||
this._track('file');
|
this._track('file');
|
||||||
this.abortController.abort();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _addImageChip = async () => {
|
private readonly _addImageChip = async () => {
|
||||||
if (this.isImageUploadDisabled) return;
|
|
||||||
|
|
||||||
const images = await openFilesWith('Images');
|
const images = await openFilesWith('Images');
|
||||||
if (!images) return;
|
if (!images) return;
|
||||||
if (this.uploadImageCount + images.length > MAX_IMAGE_COUNT) {
|
this.abortController.abort();
|
||||||
toast(`You can only upload up to ${MAX_IMAGE_COUNT} images`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.addImages(images);
|
this.addImages(images);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,9 +285,6 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
|||||||
@property({ attribute: 'data-testid', reflect: true })
|
@property({ attribute: 'data-testid', reflect: true })
|
||||||
accessor testId: string = 'ai-search-input';
|
accessor testId: string = 'ai-search-input';
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor isImageUploadDisabled!: boolean;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor uploadImageCount!: number;
|
accessor uploadImageCount!: number;
|
||||||
|
|
||||||
@@ -498,31 +491,31 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly _addDocChip = async (meta: DocMeta) => {
|
private readonly _addDocChip = async (meta: DocMeta) => {
|
||||||
|
this.abortController.abort();
|
||||||
await this.addChip({
|
await this.addChip({
|
||||||
docId: meta.id,
|
docId: meta.id,
|
||||||
state: 'processing',
|
state: 'processing',
|
||||||
});
|
});
|
||||||
const mode = this.docDisplayConfig.getDocPrimaryMode(meta.id);
|
const mode = this.docDisplayConfig.getDocPrimaryMode(meta.id);
|
||||||
this._track('doc', mode);
|
this._track('doc', mode);
|
||||||
this.abortController.abort();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _addTagChip = async (tag: TagMeta) => {
|
private readonly _addTagChip = async (tag: TagMeta) => {
|
||||||
|
this.abortController.abort();
|
||||||
await this.addChip({
|
await this.addChip({
|
||||||
tagId: tag.id,
|
tagId: tag.id,
|
||||||
state: 'processing',
|
state: 'processing',
|
||||||
});
|
});
|
||||||
this._track('tags');
|
this._track('tags');
|
||||||
this.abortController.abort();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _addCollectionChip = async (collection: CollectionMeta) => {
|
private readonly _addCollectionChip = async (collection: CollectionMeta) => {
|
||||||
|
this.abortController.abort();
|
||||||
await this.addChip({
|
await this.addChip({
|
||||||
collectionId: collection.id,
|
collectionId: collection.id,
|
||||||
state: 'processing',
|
state: 'processing',
|
||||||
});
|
});
|
||||||
this._track('collections');
|
this._track('collections');
|
||||||
this.abortController.abort();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _handleKeyDown = (event: KeyboardEvent) => {
|
private readonly _handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
|||||||
+56
-345
@@ -3,7 +3,7 @@ import { createLitPortal } from '@blocksuite/affine/components/portal';
|
|||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
import { MoreVerticalIcon, PlusIcon } from '@blocksuite/icons/lit';
|
import { MoreVerticalIcon } from '@blocksuite/icons/lit';
|
||||||
import { flip, offset } from '@floating-ui/dom';
|
import { flip, offset } from '@floating-ui/dom';
|
||||||
import { computed, type Signal, signal } from '@preact/signals-core';
|
import { computed, type Signal, signal } from '@preact/signals-core';
|
||||||
import { css, html, nothing, type PropertyValues } from 'lit';
|
import { css, html, nothing, type PropertyValues } from 'lit';
|
||||||
@@ -11,16 +11,7 @@ import { property, query, state } from 'lit/decorators.js';
|
|||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
import { AIProvider } from '../../provider';
|
import type { ChatChip, DocChip, DocDisplayConfig, FileChip } from './type';
|
||||||
import type {
|
|
||||||
ChatChip,
|
|
||||||
CollectionChip,
|
|
||||||
DocChip,
|
|
||||||
DocDisplayConfig,
|
|
||||||
FileChip,
|
|
||||||
SearchMenuConfig,
|
|
||||||
TagChip,
|
|
||||||
} from './type';
|
|
||||||
import {
|
import {
|
||||||
estimateTokenCount,
|
estimateTokenCount,
|
||||||
getChipKey,
|
getChipKey,
|
||||||
@@ -39,44 +30,46 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
WithDisposable(ShadowlessElement)
|
WithDisposable(ShadowlessElement)
|
||||||
) {
|
) {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
.chips-wrapper {
|
.ai-chat-panel-chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
}
|
|
||||||
.add-button,
|
.collapse-button,
|
||||||
.collapse-button,
|
.more-candidate-button {
|
||||||
.more-candidate-button {
|
display: flex;
|
||||||
display: flex;
|
flex-shrink: 0;
|
||||||
flex-shrink: 0;
|
flex-grow: 0;
|
||||||
flex-grow: 0;
|
align-items: center;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
justify-content: center;
|
width: 24px;
|
||||||
width: 24px;
|
height: 24px;
|
||||||
height: 24px;
|
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
border-radius: 4px;
|
||||||
border-radius: 4px;
|
box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
cursor: pointer;
|
||||||
cursor: pointer;
|
font-size: 12px;
|
||||||
font-size: 12px;
|
color: ${unsafeCSSVarV2('icon/primary')};
|
||||||
color: ${unsafeCSSVarV2('icon/primary')};
|
}
|
||||||
}
|
|
||||||
.add-button:hover,
|
.collapse-button:hover,
|
||||||
.collapse-button:hover,
|
.more-candidate-button:hover {
|
||||||
.more-candidate-button:hover {
|
background-color: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||||
background-color: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
}
|
||||||
}
|
|
||||||
.more-candidate-button {
|
.more-candidate-button {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
border-color: ${unsafeCSSVarV2('icon/tertiary')};
|
border-color: ${unsafeCSSVarV2('icon/tertiary')};
|
||||||
background: ${unsafeCSSVarV2('layer/background/secondary')};
|
background: ${unsafeCSSVarV2('layer/background/secondary')};
|
||||||
color: ${unsafeCSSVarV2('icon/secondary')};
|
color: ${unsafeCSSVarV2('icon/secondary')};
|
||||||
}
|
}
|
||||||
.more-candidate-button svg {
|
|
||||||
color: ${unsafeCSSVarV2('icon/secondary')};
|
.more-candidate-button svg {
|
||||||
|
color: ${unsafeCSSVarV2('icon/secondary')};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -86,38 +79,35 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
accessor chips!: ChatChip[];
|
accessor chips!: ChatChip[];
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor createContextId!: () => Promise<string | undefined>;
|
accessor isCollapsed!: boolean;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateChips!: (chips: ChatChip[]) => void;
|
accessor addChip!: (chip: ChatChip) => Promise<void>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor addImages!: (images: File[]) => void;
|
accessor updateChip!: (
|
||||||
|
chip: ChatChip,
|
||||||
|
options: Partial<DocChip | FileChip>
|
||||||
|
) => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor pollContextDocsAndFiles!: () => void;
|
accessor removeChip!: (chip: ChatChip) => Promise<void>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor toggleCollapse!: () => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor docDisplayConfig!: DocDisplayConfig;
|
accessor docDisplayConfig!: DocDisplayConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor searchMenuConfig!: SearchMenuConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor portalContainer: HTMLElement | null = null;
|
accessor portalContainer: HTMLElement | null = null;
|
||||||
|
|
||||||
@property({ attribute: 'data-testid', reflect: true })
|
@property({ attribute: 'data-testid', reflect: true })
|
||||||
accessor testId = 'chat-panel-chips';
|
accessor testId = 'chat-panel-chips';
|
||||||
|
|
||||||
@query('.add-button')
|
|
||||||
accessor addButton!: HTMLDivElement;
|
|
||||||
|
|
||||||
@query('.more-candidate-button')
|
@query('.more-candidate-button')
|
||||||
accessor moreCandidateButton!: HTMLDivElement;
|
accessor moreCandidateButton!: HTMLDivElement;
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor isCollapsed = false;
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor referenceDocs: Signal<
|
accessor referenceDocs: Signal<
|
||||||
Array<{
|
Array<{
|
||||||
@@ -144,14 +134,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
const isCollapsed = this.isCollapsed && allChips.length > 1;
|
const isCollapsed = this.isCollapsed && allChips.length > 1;
|
||||||
const chips = isCollapsed ? allChips.slice(0, 1) : allChips;
|
const chips = isCollapsed ? allChips.slice(0, 1) : allChips;
|
||||||
|
|
||||||
return html`<div class="chips-wrapper">
|
return html`<div class="ai-chat-panel-chips">
|
||||||
<div
|
|
||||||
class="add-button"
|
|
||||||
data-testid="chat-panel-with-button"
|
|
||||||
@click=${this._toggleAddDocMenu}
|
|
||||||
>
|
|
||||||
${PlusIcon()}
|
|
||||||
</div>
|
|
||||||
${repeat(
|
${repeat(
|
||||||
chips,
|
chips,
|
||||||
chip => getChipKey(chip),
|
chip => getChipKey(chip),
|
||||||
@@ -159,9 +142,9 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
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}
|
.addChip=${this.addChip}
|
||||||
.updateChip=${this._updateChip}
|
.updateChip=${this.updateChip}
|
||||||
.removeChip=${this._removeChip}
|
.removeChip=${this.removeChip}
|
||||||
.checkTokenLimit=${this._checkTokenLimit}
|
.checkTokenLimit=${this._checkTokenLimit}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
></chat-panel-doc-chip>`;
|
></chat-panel-doc-chip>`;
|
||||||
@@ -169,7 +152,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
if (isFileChip(chip)) {
|
if (isFileChip(chip)) {
|
||||||
return html`<chat-panel-file-chip
|
return html`<chat-panel-file-chip
|
||||||
.chip=${chip}
|
.chip=${chip}
|
||||||
.removeChip=${this._removeChip}
|
.removeChip=${this.removeChip}
|
||||||
></chat-panel-file-chip>`;
|
></chat-panel-file-chip>`;
|
||||||
}
|
}
|
||||||
if (isTagChip(chip)) {
|
if (isTagChip(chip)) {
|
||||||
@@ -180,7 +163,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
return html`<chat-panel-tag-chip
|
return html`<chat-panel-tag-chip
|
||||||
.chip=${chip}
|
.chip=${chip}
|
||||||
.tag=${tag}
|
.tag=${tag}
|
||||||
.removeChip=${this._removeChip}
|
.removeChip=${this.removeChip}
|
||||||
></chat-panel-tag-chip>`;
|
></chat-panel-tag-chip>`;
|
||||||
}
|
}
|
||||||
if (isCollectionChip(chip)) {
|
if (isCollectionChip(chip)) {
|
||||||
@@ -193,7 +176,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
return html`<chat-panel-collection-chip
|
return html`<chat-panel-collection-chip
|
||||||
.chip=${chip}
|
.chip=${chip}
|
||||||
.collection=${collection}
|
.collection=${collection}
|
||||||
.removeChip=${this._removeChip}
|
.removeChip=${this.removeChip}
|
||||||
></chat-panel-collection-chip>`;
|
></chat-panel-collection-chip>`;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -208,7 +191,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${isCollapsed
|
${isCollapsed
|
||||||
? html`<div class="collapse-button" @click=${this._toggleCollapse}>
|
? html`<div class="collapse-button" @click=${this.toggleCollapse}>
|
||||||
+${allChips.length - 1}
|
+${allChips.length - 1}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
@@ -227,14 +210,6 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override updated(_changedProperties: PropertyValues): void {
|
protected override updated(_changedProperties: PropertyValues): void {
|
||||||
if (
|
|
||||||
_changedProperties.has('chatContextValue') &&
|
|
||||||
_changedProperties.get('chatContextValue')?.status === 'loading' &&
|
|
||||||
this.isCollapsed === false
|
|
||||||
) {
|
|
||||||
this.isCollapsed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_changedProperties.has('chips')) {
|
if (_changedProperties.has('chips')) {
|
||||||
this._updateReferenceDocs();
|
this._updateReferenceDocs();
|
||||||
}
|
}
|
||||||
@@ -245,46 +220,6 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
this._cleanup?.();
|
this._cleanup?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _toggleCollapse = () => {
|
|
||||||
this.isCollapsed = !this.isCollapsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _toggleAddDocMenu = () => {
|
|
||||||
if (this._abortController) {
|
|
||||||
this._abortController.abort();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._abortController = new AbortController();
|
|
||||||
this._abortController.signal.addEventListener('abort', () => {
|
|
||||||
this._abortController = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
createLitPortal({
|
|
||||||
template: html`
|
|
||||||
<chat-panel-add-popover
|
|
||||||
.addChip=${this._addChip}
|
|
||||||
.addImages=${this.addImages}
|
|
||||||
.searchMenuConfig=${this.searchMenuConfig}
|
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
|
||||||
.abortController=${this._abortController}
|
|
||||||
></chat-panel-add-popover>
|
|
||||||
`,
|
|
||||||
portalStyles: {
|
|
||||||
zIndex: 'var(--affine-z-index-popover)',
|
|
||||||
},
|
|
||||||
container: this.portalContainer ?? document.body,
|
|
||||||
computePosition: {
|
|
||||||
referenceElement: this.addButton,
|
|
||||||
placement: 'top-start',
|
|
||||||
middleware: [offset({ crossAxis: -30, mainAxis: 8 }), flip()],
|
|
||||||
autoUpdate: { animationFrame: true },
|
|
||||||
},
|
|
||||||
abortController: this._abortController,
|
|
||||||
closeOnClickAway: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _toggleMoreCandidatesMenu = () => {
|
private readonly _toggleMoreCandidatesMenu = () => {
|
||||||
if (this._abortController) {
|
if (this._abortController) {
|
||||||
this._abortController.abort();
|
this._abortController.abort();
|
||||||
@@ -303,7 +238,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
createLitPortal({
|
createLitPortal({
|
||||||
template: html`
|
template: html`
|
||||||
<chat-panel-candidates-popover
|
<chat-panel-candidates-popover
|
||||||
.addChip=${this._addChip}
|
.addChip=${this.addChip}
|
||||||
.referenceDocs=${referenceDocs}
|
.referenceDocs=${referenceDocs}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.abortController=${this._abortController}
|
.abortController=${this._abortController}
|
||||||
@@ -324,190 +259,6 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _addChip = async (chip: ChatChip) => {
|
|
||||||
this.isCollapsed = false;
|
|
||||||
// remove the chip if it already exists
|
|
||||||
const chips = this._omitChip(this.chips, chip);
|
|
||||||
this.updateChips([...chips, chip]);
|
|
||||||
if (chips.length < this.chips.length) {
|
|
||||||
await this._removeFromContext(chip);
|
|
||||||
}
|
|
||||||
await this._addToContext(chip);
|
|
||||||
this.pollContextDocsAndFiles();
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _updateChip = (
|
|
||||||
chip: ChatChip,
|
|
||||||
options: Partial<DocChip | FileChip>
|
|
||||||
) => {
|
|
||||||
const index = this._findChipIndex(this.chips, chip);
|
|
||||||
if (index === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextChip: ChatChip = {
|
|
||||||
...chip,
|
|
||||||
...options,
|
|
||||||
};
|
|
||||||
this.updateChips([
|
|
||||||
...this.chips.slice(0, index),
|
|
||||||
nextChip,
|
|
||||||
...this.chips.slice(index + 1),
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _removeChip = async (chip: ChatChip) => {
|
|
||||||
const chips = this._omitChip(this.chips, chip);
|
|
||||||
this.updateChips(chips);
|
|
||||||
if (chips.length < this.chips.length) {
|
|
||||||
await this._removeFromContext(chip);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _addToContext = async (chip: ChatChip) => {
|
|
||||||
if (isDocChip(chip)) {
|
|
||||||
return await this._addDocToContext(chip);
|
|
||||||
}
|
|
||||||
if (isFileChip(chip)) {
|
|
||||||
return await this._addFileToContext(chip);
|
|
||||||
}
|
|
||||||
if (isTagChip(chip)) {
|
|
||||||
return await this._addTagToContext(chip);
|
|
||||||
}
|
|
||||||
if (isCollectionChip(chip)) {
|
|
||||||
return await this._addCollectionToContext(chip);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _addDocToContext = async (chip: DocChip) => {
|
|
||||||
try {
|
|
||||||
const contextId = await this.createContextId();
|
|
||||||
if (!contextId || !AIProvider.context) {
|
|
||||||
throw new Error('Context not found');
|
|
||||||
}
|
|
||||||
await AIProvider.context.addContextDoc({
|
|
||||||
contextId,
|
|
||||||
docId: chip.docId,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
this._updateChip(chip, {
|
|
||||||
state: 'failed',
|
|
||||||
tooltip: e instanceof Error ? e.message : 'Add context doc error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _addFileToContext = async (chip: FileChip) => {
|
|
||||||
try {
|
|
||||||
const contextId = await this.createContextId();
|
|
||||||
if (!contextId || !AIProvider.context) {
|
|
||||||
throw new Error('Context not found');
|
|
||||||
}
|
|
||||||
const contextFile = await AIProvider.context.addContextFile(chip.file, {
|
|
||||||
contextId,
|
|
||||||
});
|
|
||||||
this._updateChip(chip, {
|
|
||||||
state: contextFile.status,
|
|
||||||
blobId: contextFile.blobId,
|
|
||||||
fileId: contextFile.id,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
this._updateChip(chip, {
|
|
||||||
state: 'failed',
|
|
||||||
tooltip: e instanceof Error ? e.message : 'Add context file error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _addTagToContext = async (chip: TagChip) => {
|
|
||||||
try {
|
|
||||||
const contextId = await this.createContextId();
|
|
||||||
if (!contextId || !AIProvider.context) {
|
|
||||||
throw new Error('Context not found');
|
|
||||||
}
|
|
||||||
// TODO: server side docIds calculation
|
|
||||||
const docIds = this.docDisplayConfig.getTagPageIds(chip.tagId);
|
|
||||||
await AIProvider.context.addContextTag({
|
|
||||||
contextId,
|
|
||||||
tagId: chip.tagId,
|
|
||||||
docIds,
|
|
||||||
});
|
|
||||||
this._updateChip(chip, {
|
|
||||||
state: 'finished',
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
this._updateChip(chip, {
|
|
||||||
state: 'failed',
|
|
||||||
tooltip: e instanceof Error ? e.message : 'Add context tag error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _addCollectionToContext = async (chip: CollectionChip) => {
|
|
||||||
try {
|
|
||||||
const contextId = await this.createContextId();
|
|
||||||
if (!contextId || !AIProvider.context) {
|
|
||||||
throw new Error('Context not found');
|
|
||||||
}
|
|
||||||
// TODO: server side docIds calculation
|
|
||||||
const docIds = this.docDisplayConfig.getCollectionPageIds(
|
|
||||||
chip.collectionId
|
|
||||||
);
|
|
||||||
await AIProvider.context.addContextCollection({
|
|
||||||
contextId,
|
|
||||||
collectionId: chip.collectionId,
|
|
||||||
docIds,
|
|
||||||
});
|
|
||||||
this._updateChip(chip, {
|
|
||||||
state: 'finished',
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
this._updateChip(chip, {
|
|
||||||
state: 'failed',
|
|
||||||
tooltip:
|
|
||||||
e instanceof Error ? e.message : 'Add context collection error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _removeFromContext = async (
|
|
||||||
chip: ChatChip
|
|
||||||
): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const contextId = await this.createContextId();
|
|
||||||
if (!contextId || !AIProvider.context) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (isDocChip(chip)) {
|
|
||||||
return await AIProvider.context.removeContextDoc({
|
|
||||||
contextId,
|
|
||||||
docId: chip.docId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (isFileChip(chip) && chip.fileId) {
|
|
||||||
return await AIProvider.context.removeContextFile({
|
|
||||||
contextId,
|
|
||||||
fileId: chip.fileId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (isTagChip(chip)) {
|
|
||||||
return await AIProvider.context.removeContextTag({
|
|
||||||
contextId,
|
|
||||||
tagId: chip.tagId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (isCollectionChip(chip)) {
|
|
||||||
return await AIProvider.context.removeContextCollection({
|
|
||||||
contextId,
|
|
||||||
collectionId: chip.collectionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _checkTokenLimit = (
|
private readonly _checkTokenLimit = (
|
||||||
newChip: DocChip,
|
newChip: DocChip,
|
||||||
newTokenCount: number
|
newTokenCount: number
|
||||||
@@ -544,44 +295,4 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
this.referenceDocs = signal;
|
this.referenceDocs = signal;
|
||||||
this._cleanup = cleanup;
|
this._cleanup = cleanup;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _omitChip = (chips: ChatChip[], chip: ChatChip) => {
|
|
||||||
return chips.filter(item => {
|
|
||||||
if (isDocChip(chip)) {
|
|
||||||
return !isDocChip(item) || item.docId !== chip.docId;
|
|
||||||
}
|
|
||||||
if (isFileChip(chip)) {
|
|
||||||
return !isFileChip(item) || item.file !== chip.file;
|
|
||||||
}
|
|
||||||
if (isTagChip(chip)) {
|
|
||||||
return !isTagChip(item) || item.tagId !== chip.tagId;
|
|
||||||
}
|
|
||||||
if (isCollectionChip(chip)) {
|
|
||||||
return (
|
|
||||||
!isCollectionChip(item) || item.collectionId !== chip.collectionId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _findChipIndex = (chips: ChatChip[], chip: ChatChip) => {
|
|
||||||
return chips.findIndex(item => {
|
|
||||||
if (isDocChip(chip)) {
|
|
||||||
return isDocChip(item) && item.docId === chip.docId;
|
|
||||||
}
|
|
||||||
if (isFileChip(chip)) {
|
|
||||||
return isFileChip(item) && item.file === chip.file;
|
|
||||||
}
|
|
||||||
if (isTagChip(chip)) {
|
|
||||||
return isTagChip(item) && item.tagId === chip.tagId;
|
|
||||||
}
|
|
||||||
if (isCollectionChip(chip)) {
|
|
||||||
return (
|
|
||||||
isCollectionChip(item) && item.collectionId === chip.collectionId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import type { TagMeta } from '@affine/core/components/page-list';
|
import type { TagMeta } from '@affine/core/components/page-list';
|
||||||
import type {
|
|
||||||
SearchCollectionMenuAction,
|
|
||||||
SearchDocMenuAction,
|
|
||||||
SearchTagMenuAction,
|
|
||||||
} from '@affine/core/modules/search-menu/services';
|
|
||||||
import type { DocMeta, Store } from '@blocksuite/affine/store';
|
import type { DocMeta, Store } from '@blocksuite/affine/store';
|
||||||
import type { LinkedMenuGroup } from '@blocksuite/affine/widgets/linked-doc';
|
|
||||||
import type { Signal } from '@preact/signals-core';
|
import type { Signal } from '@preact/signals-core';
|
||||||
|
|
||||||
export type ChipState = 'candidate' | 'processing' | 'finished' | 'failed';
|
export type ChipState = 'candidate' | 'processing' | 'finished' | 'failed';
|
||||||
@@ -75,21 +69,3 @@ export interface DocDisplayConfig {
|
|||||||
};
|
};
|
||||||
getCollectionPageIds: (collectionId: string) => string[];
|
getCollectionPageIds: (collectionId: string) => string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchMenuConfig {
|
|
||||||
getDocMenuGroup: (
|
|
||||||
query: string,
|
|
||||||
action: SearchDocMenuAction,
|
|
||||||
abortSignal: AbortSignal
|
|
||||||
) => LinkedMenuGroup;
|
|
||||||
getTagMenuGroup: (
|
|
||||||
query: string,
|
|
||||||
action: SearchTagMenuAction,
|
|
||||||
abortSignal: AbortSignal
|
|
||||||
) => LinkedMenuGroup;
|
|
||||||
getCollectionMenuGroup: (
|
|
||||||
query: string,
|
|
||||||
action: SearchCollectionMenuAction,
|
|
||||||
abortSignal: AbortSignal
|
|
||||||
) => LinkedMenuGroup;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -78,6 +78,42 @@ export function getChipKey(chip: ChatChip) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function omitChip(chips: ChatChip[], chip: ChatChip) {
|
||||||
|
return chips.filter(item => {
|
||||||
|
if (isDocChip(chip)) {
|
||||||
|
return !isDocChip(item) || item.docId !== chip.docId;
|
||||||
|
}
|
||||||
|
if (isFileChip(chip)) {
|
||||||
|
return !isFileChip(item) || item.file !== chip.file;
|
||||||
|
}
|
||||||
|
if (isTagChip(chip)) {
|
||||||
|
return !isTagChip(item) || item.tagId !== chip.tagId;
|
||||||
|
}
|
||||||
|
if (isCollectionChip(chip)) {
|
||||||
|
return !isCollectionChip(item) || item.collectionId !== chip.collectionId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findChipIndex(chips: ChatChip[], chip: ChatChip) {
|
||||||
|
return chips.findIndex(item => {
|
||||||
|
if (isDocChip(chip)) {
|
||||||
|
return isDocChip(item) && item.docId === chip.docId;
|
||||||
|
}
|
||||||
|
if (isFileChip(chip)) {
|
||||||
|
return isFileChip(item) && item.file === chip.file;
|
||||||
|
}
|
||||||
|
if (isTagChip(chip)) {
|
||||||
|
return isTagChip(item) && item.tagId === chip.tagId;
|
||||||
|
}
|
||||||
|
if (isCollectionChip(chip)) {
|
||||||
|
return isCollectionChip(item) && item.collectionId === chip.collectionId;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function estimateTokenCount(text: string): number {
|
export function estimateTokenCount(text: string): number {
|
||||||
const chinese = text.match(/[\u4e00-\u9fa5]/g)?.length || 0;
|
const chinese = text.match(/[\u4e00-\u9fa5]/g)?.length || 0;
|
||||||
const english = text.replace(/[\u4e00-\u9fa5]/g, '');
|
const english = text.replace(/[\u4e00-\u9fa5]/g, '');
|
||||||
|
|||||||
+238
-17
@@ -12,20 +12,28 @@ import type {
|
|||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
import { css, html } from 'lit';
|
import type { NotificationService } from '@blocksuite/affine-shared/services';
|
||||||
|
import { css, html, type PropertyValues } from 'lit';
|
||||||
import { property, state } from 'lit/decorators.js';
|
import { property, state } from 'lit/decorators.js';
|
||||||
|
|
||||||
import { AIProvider } from '../../provider';
|
import { AIProvider } from '../../provider';
|
||||||
|
import type { SearchMenuConfig } from '../ai-chat-add-context';
|
||||||
import type {
|
import type {
|
||||||
ChatChip,
|
ChatChip,
|
||||||
CollectionChip,
|
CollectionChip,
|
||||||
DocChip,
|
DocChip,
|
||||||
DocDisplayConfig,
|
DocDisplayConfig,
|
||||||
FileChip,
|
FileChip,
|
||||||
SearchMenuConfig,
|
|
||||||
TagChip,
|
TagChip,
|
||||||
} from '../ai-chat-chips';
|
} from '../ai-chat-chips';
|
||||||
import { isCollectionChip, isDocChip, isTagChip } from '../ai-chat-chips';
|
import {
|
||||||
|
findChipIndex,
|
||||||
|
isCollectionChip,
|
||||||
|
isDocChip,
|
||||||
|
isFileChip,
|
||||||
|
isTagChip,
|
||||||
|
omitChip,
|
||||||
|
} from '../ai-chat-chips';
|
||||||
import type {
|
import type {
|
||||||
AIChatInputContext,
|
AIChatInputContext,
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
@@ -105,9 +113,15 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor notificationService!: NotificationService;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor chips: ChatChip[] = [];
|
accessor chips: ChatChip[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor isChipsCollapsed = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor embeddingCompleted = false;
|
accessor embeddingCompleted = false;
|
||||||
|
|
||||||
@@ -121,11 +135,12 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
return html`
|
return html`
|
||||||
<chat-panel-chips
|
<chat-panel-chips
|
||||||
.chips=${this.chips}
|
.chips=${this.chips}
|
||||||
.createContextId=${this._createContextId}
|
.isCollapsed=${this.isChipsCollapsed}
|
||||||
.updateChips=${this.updateChips}
|
.addChip=${this.addChip}
|
||||||
.pollContextDocsAndFiles=${this._pollContextDocsAndFiles}
|
.updateChip=${this.updateChip}
|
||||||
|
.removeChip=${this.removeChip}
|
||||||
|
.toggleCollapse=${this.toggleChipsCollapse}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.searchMenuConfig=${this.searchMenuConfig}
|
|
||||||
.portalContainer=${this.portalContainer}
|
.portalContainer=${this.portalContainer}
|
||||||
.addImages=${this.addImages}
|
.addImages=${this.addImages}
|
||||||
></chat-panel-chips>
|
></chat-panel-chips>
|
||||||
@@ -136,15 +151,18 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
.docId=${this.docId}
|
.docId=${this.docId}
|
||||||
.session=${this.session}
|
.session=${this.session}
|
||||||
.chips=${this.chips}
|
.chips=${this.chips}
|
||||||
|
.addChip=${this.addChip}
|
||||||
|
.addImages=${this.addImages}
|
||||||
.createSession=${this.createSession}
|
.createSession=${this.createSession}
|
||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
.updateContext=${this.updateContext}
|
.updateContext=${this.updateContext}
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
.reasoningConfig=${this.reasoningConfig}
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
|
.portalContainer=${this.portalContainer}
|
||||||
.onChatSuccess=${this.onChatSuccess}
|
.onChatSuccess=${this.onChatSuccess}
|
||||||
.trackOptions=${this.trackOptions}
|
.trackOptions=${this.trackOptions}
|
||||||
.addImages=${this.addImages}
|
|
||||||
></ai-chat-input>
|
></ai-chat-input>
|
||||||
<div class="chat-panel-footer">
|
<div class="chat-panel-footer">
|
||||||
<ai-chat-composer-tip
|
<ai-chat-composer-tip
|
||||||
@@ -165,7 +183,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._initComposer().catch(console.error);
|
this.initComposer().catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
override disconnectedCallback() {
|
override disconnectedCallback() {
|
||||||
@@ -174,6 +192,17 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
this._abortPollEmbeddingStatus();
|
this._abortPollEmbeddingStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||||
|
if (
|
||||||
|
changedProperties.has('chatContextValue') &&
|
||||||
|
changedProperties.get('chatContextValue')?.status !== 'loading' &&
|
||||||
|
this.chatContextValue.status === 'loading' &&
|
||||||
|
this.isChipsCollapsed === false
|
||||||
|
) {
|
||||||
|
this.isChipsCollapsed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly _getContextId = async () => {
|
private readonly _getContextId = async () => {
|
||||||
if (this._contextId) {
|
if (this._contextId) {
|
||||||
return this._contextId;
|
return this._contextId;
|
||||||
@@ -190,7 +219,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
return this._contextId;
|
return this._contextId;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _createContextId = async () => {
|
private readonly createContextId = async () => {
|
||||||
if (this._contextId) {
|
if (this._contextId) {
|
||||||
return this._contextId;
|
return this._contextId;
|
||||||
}
|
}
|
||||||
@@ -205,7 +234,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
return this._contextId;
|
return this._contextId;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _initChips = async () => {
|
private readonly initChips = async () => {
|
||||||
// context not initialized
|
// context not initialized
|
||||||
const sessionId = this.session?.sessionId;
|
const sessionId = this.session?.sessionId;
|
||||||
const contextId = await this._getContextId();
|
const contextId = await this._getContextId();
|
||||||
@@ -275,14 +304,206 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
this.chips = chips;
|
this.chips = chips;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly updateChip = (
|
||||||
|
chip: ChatChip,
|
||||||
|
options: Partial<DocChip | FileChip>
|
||||||
|
) => {
|
||||||
|
const index = findChipIndex(this.chips, chip);
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextChip: ChatChip = {
|
||||||
|
...chip,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
this.updateChips([
|
||||||
|
...this.chips.slice(0, index),
|
||||||
|
nextChip,
|
||||||
|
...this.chips.slice(index + 1),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly addChip = async (chip: ChatChip) => {
|
||||||
|
this.isChipsCollapsed = false;
|
||||||
|
// if already exists
|
||||||
|
const index = findChipIndex(this.chips, chip);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.notificationService.toast('chip already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updateChips([...this.chips, chip]);
|
||||||
|
await this.addToContext(chip);
|
||||||
|
await this.pollContextDocsAndFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly removeChip = async (chip: ChatChip) => {
|
||||||
|
const chips = omitChip(this.chips, chip);
|
||||||
|
this.updateChips(chips);
|
||||||
|
await this.removeFromContext(chip);
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly addToContext = async (chip: ChatChip) => {
|
||||||
|
if (isDocChip(chip)) {
|
||||||
|
return await this.addDocToContext(chip);
|
||||||
|
}
|
||||||
|
if (isFileChip(chip)) {
|
||||||
|
return await this.addFileToContext(chip);
|
||||||
|
}
|
||||||
|
if (isTagChip(chip)) {
|
||||||
|
return await this.addTagToContext(chip);
|
||||||
|
}
|
||||||
|
if (isCollectionChip(chip)) {
|
||||||
|
return await this.addCollectionToContext(chip);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly addDocToContext = async (chip: DocChip) => {
|
||||||
|
try {
|
||||||
|
const contextId = await this.createContextId();
|
||||||
|
if (!contextId || !AIProvider.context) {
|
||||||
|
throw new Error('Context not found');
|
||||||
|
}
|
||||||
|
await AIProvider.context.addContextDoc({
|
||||||
|
contextId,
|
||||||
|
docId: chip.docId,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.updateChip(chip, {
|
||||||
|
state: 'failed',
|
||||||
|
tooltip: e instanceof Error ? e.message : 'Add context doc error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly addFileToContext = async (chip: FileChip) => {
|
||||||
|
try {
|
||||||
|
const contextId = await this.createContextId();
|
||||||
|
if (!contextId || !AIProvider.context) {
|
||||||
|
throw new Error('Context not found');
|
||||||
|
}
|
||||||
|
const contextFile = await AIProvider.context.addContextFile(chip.file, {
|
||||||
|
contextId,
|
||||||
|
});
|
||||||
|
this.updateChip(chip, {
|
||||||
|
state: contextFile.status,
|
||||||
|
blobId: contextFile.blobId,
|
||||||
|
fileId: contextFile.id,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.updateChip(chip, {
|
||||||
|
state: 'failed',
|
||||||
|
tooltip: e instanceof Error ? e.message : 'Add context file error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly addTagToContext = async (chip: TagChip) => {
|
||||||
|
try {
|
||||||
|
const contextId = await this.createContextId();
|
||||||
|
if (!contextId || !AIProvider.context) {
|
||||||
|
throw new Error('Context not found');
|
||||||
|
}
|
||||||
|
// TODO: server side docIds calculation
|
||||||
|
const docIds = this.docDisplayConfig.getTagPageIds(chip.tagId);
|
||||||
|
await AIProvider.context.addContextTag({
|
||||||
|
contextId,
|
||||||
|
tagId: chip.tagId,
|
||||||
|
docIds,
|
||||||
|
});
|
||||||
|
this.updateChip(chip, {
|
||||||
|
state: 'finished',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.updateChip(chip, {
|
||||||
|
state: 'failed',
|
||||||
|
tooltip: e instanceof Error ? e.message : 'Add context tag error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly addCollectionToContext = async (chip: CollectionChip) => {
|
||||||
|
try {
|
||||||
|
const contextId = await this.createContextId();
|
||||||
|
if (!contextId || !AIProvider.context) {
|
||||||
|
throw new Error('Context not found');
|
||||||
|
}
|
||||||
|
// TODO: server side docIds calculation
|
||||||
|
const docIds = this.docDisplayConfig.getCollectionPageIds(
|
||||||
|
chip.collectionId
|
||||||
|
);
|
||||||
|
await AIProvider.context.addContextCollection({
|
||||||
|
contextId,
|
||||||
|
collectionId: chip.collectionId,
|
||||||
|
docIds,
|
||||||
|
});
|
||||||
|
this.updateChip(chip, {
|
||||||
|
state: 'finished',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.updateChip(chip, {
|
||||||
|
state: 'failed',
|
||||||
|
tooltip:
|
||||||
|
e instanceof Error ? e.message : 'Add context collection error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly removeFromContext = async (
|
||||||
|
chip: ChatChip
|
||||||
|
): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const contextId = await this.createContextId();
|
||||||
|
if (!contextId || !AIProvider.context) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isDocChip(chip)) {
|
||||||
|
return await AIProvider.context.removeContextDoc({
|
||||||
|
contextId,
|
||||||
|
docId: chip.docId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isFileChip(chip) && chip.fileId) {
|
||||||
|
return await AIProvider.context.removeContextFile({
|
||||||
|
contextId,
|
||||||
|
fileId: chip.fileId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isTagChip(chip)) {
|
||||||
|
return await AIProvider.context.removeContextTag({
|
||||||
|
contextId,
|
||||||
|
tagId: chip.tagId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isCollectionChip(chip)) {
|
||||||
|
return await AIProvider.context.removeContextCollection({
|
||||||
|
contextId,
|
||||||
|
collectionId: chip.collectionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly toggleChipsCollapse = () => {
|
||||||
|
this.isChipsCollapsed = !this.isChipsCollapsed;
|
||||||
|
};
|
||||||
|
|
||||||
private readonly addImages = (images: File[]) => {
|
private readonly addImages = (images: File[]) => {
|
||||||
const oldImages = this.chatContextValue.images;
|
const oldImages = this.chatContextValue.images;
|
||||||
|
if (oldImages.length + images.length > MAX_IMAGE_COUNT) {
|
||||||
|
this.notificationService.toast(
|
||||||
|
`You can only upload up to ${MAX_IMAGE_COUNT} images`
|
||||||
|
);
|
||||||
|
}
|
||||||
this.updateContext({
|
this.updateContext({
|
||||||
images: [...oldImages, ...images].slice(0, MAX_IMAGE_COUNT),
|
images: [...oldImages, ...images].slice(0, MAX_IMAGE_COUNT),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _pollContextDocsAndFiles = async () => {
|
private readonly pollContextDocsAndFiles = async () => {
|
||||||
const sessionId = this.session?.sessionId;
|
const sessionId = this.session?.sessionId;
|
||||||
const contextId = await this._getContextId();
|
const contextId = await this._getContextId();
|
||||||
if (!sessionId || !contextId || !AIProvider.context) {
|
if (!sessionId || !contextId || !AIProvider.context) {
|
||||||
@@ -302,7 +523,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _pollEmbeddingStatus = async () => {
|
private readonly pollEmbeddingStatus = async () => {
|
||||||
if (this._pollEmbeddingStatusAbortController) {
|
if (this._pollEmbeddingStatusAbortController) {
|
||||||
this._pollEmbeddingStatusAbortController.abort();
|
this._pollEmbeddingStatusAbortController.abort();
|
||||||
}
|
}
|
||||||
@@ -398,18 +619,18 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
this._pollEmbeddingStatusAbortController = null;
|
this._pollEmbeddingStatusAbortController = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _initComposer = async () => {
|
private readonly initComposer = async () => {
|
||||||
const userId = (await AIProvider.userInfo)?.id;
|
const userId = (await AIProvider.userInfo)?.id;
|
||||||
if (!userId || !this.session) return;
|
if (!userId || !this.session) return;
|
||||||
|
|
||||||
await this._initChips();
|
await this.initChips();
|
||||||
const needPoll = this.chips.some(
|
const needPoll = this.chips.some(
|
||||||
chip =>
|
chip =>
|
||||||
chip.state === 'processing' || isTagChip(chip) || isCollectionChip(chip)
|
chip.state === 'processing' || isTagChip(chip) || isCollectionChip(chip)
|
||||||
);
|
);
|
||||||
if (needPoll) {
|
if (needPoll) {
|
||||||
await this._pollContextDocsAndFiles();
|
await this.pollContextDocsAndFiles();
|
||||||
}
|
}
|
||||||
await this._pollEmbeddingStatus();
|
await this.pollEmbeddingStatus();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -25,7 +25,8 @@ import { styleMap } from 'lit/directives/style-map.js';
|
|||||||
import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const';
|
import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const';
|
||||||
import { type AIChatParams, AIProvider } from '../../provider/ai-provider';
|
import { type AIChatParams, AIProvider } from '../../provider/ai-provider';
|
||||||
import { extractSelectedContent } from '../../utils/extract';
|
import { extractSelectedContent } from '../../utils/extract';
|
||||||
import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
|
import type { SearchMenuConfig } from '../ai-chat-add-context';
|
||||||
|
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||||
import type {
|
import type {
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
AIReasoningConfig,
|
AIReasoningConfig,
|
||||||
@@ -443,6 +444,7 @@ export class AIChatContent extends SignalWatcher(
|
|||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.searchMenuConfig=${this.searchMenuConfig}
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||||
|
.notificationService=${this.notificationService}
|
||||||
.trackOptions=${{
|
.trackOptions=${{
|
||||||
where: 'chat-panel',
|
where: 'chat-panel',
|
||||||
control: 'chat-send',
|
control: 'chat-send',
|
||||||
|
|||||||
+20
-29
@@ -1,11 +1,9 @@
|
|||||||
import { toast } from '@affine/component';
|
|
||||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||||
import { openFilesWith } from '@blocksuite/affine/shared/utils';
|
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
import { ArrowUpBigIcon, CloseIcon, ImageIcon } from '@blocksuite/icons/lit';
|
import { ArrowUpBigIcon, CloseIcon } from '@blocksuite/icons/lit';
|
||||||
import { css, html, nothing } from 'lit';
|
import { css, html, nothing } from 'lit';
|
||||||
import { property, query, state } from 'lit/decorators.js';
|
import { property, query, state } from 'lit/decorators.js';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
@@ -16,6 +14,7 @@ import { type AIError, AIProvider, type AISendParams } from '../../provider';
|
|||||||
import { reportResponse } from '../../utils/action-reporter';
|
import { reportResponse } from '../../utils/action-reporter';
|
||||||
import { readBlobAsURL } from '../../utils/image';
|
import { readBlobAsURL } from '../../utils/image';
|
||||||
import { mergeStreamObjects } from '../../utils/stream-objects';
|
import { mergeStreamObjects } from '../../utils/stream-objects';
|
||||||
|
import type { SearchMenuConfig } from '../ai-chat-add-context';
|
||||||
import type { ChatChip, DocDisplayConfig } from '../ai-chat-chips/type';
|
import type { ChatChip, DocDisplayConfig } from '../ai-chat-chips/type';
|
||||||
import { isDocChip } from '../ai-chat-chips/utils';
|
import { isDocChip } from '../ai-chat-chips/utils';
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +22,6 @@ import {
|
|||||||
isChatMessage,
|
isChatMessage,
|
||||||
StreamObjectSchema,
|
StreamObjectSchema,
|
||||||
} from '../ai-chat-messages';
|
} from '../ai-chat-messages';
|
||||||
import { MAX_IMAGE_COUNT } from './const';
|
|
||||||
import type {
|
import type {
|
||||||
AIChatInputContext,
|
AIChatInputContext,
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
@@ -335,6 +333,12 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor addImages!: (images: File[]) => void;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor addChip!: (chip: ChatChip) => Promise<void>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||||
|
|
||||||
@@ -344,6 +348,9 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor docDisplayConfig!: DocDisplayConfig;
|
accessor docDisplayConfig!: DocDisplayConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor searchMenuConfig!: SearchMenuConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor isRootSession: boolean = true;
|
accessor isRootSession: boolean = true;
|
||||||
|
|
||||||
@@ -357,7 +364,7 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
accessor testId = 'chat-panel-input-container';
|
accessor testId = 'chat-panel-input-container';
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor addImages!: (images: File[]) => void;
|
accessor portalContainer: HTMLElement | null = null;
|
||||||
|
|
||||||
private get _isNetworkActive() {
|
private get _isNetworkActive() {
|
||||||
return (
|
return (
|
||||||
@@ -370,10 +377,6 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
return !!this.reasoningConfig.enabled.value;
|
return !!this.reasoningConfig.enabled.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _isImageUploadDisabled() {
|
|
||||||
return this.chatContextValue.images.length >= MAX_IMAGE_COUNT;
|
|
||||||
}
|
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._disposables.add(
|
this._disposables.add(
|
||||||
@@ -453,14 +456,14 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
data-testid="chat-panel-input"
|
data-testid="chat-panel-input"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="chat-panel-input-actions">
|
<div class="chat-panel-input-actions">
|
||||||
<div
|
<div class="chat-input-icon">
|
||||||
class="chat-input-icon"
|
<ai-chat-add-context
|
||||||
data-testid="chat-panel-input-image-upload"
|
.addChip=${this.addChip}
|
||||||
aria-disabled=${this._isImageUploadDisabled}
|
.addImages=${this.addImages}
|
||||||
@click=${this._uploadImageFiles}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
>
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
${ImageIcon()}
|
.portalContainer=${this.portalContainer}
|
||||||
<affine-tooltip>Upload</affine-tooltip>
|
></ai-chat-add-context>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-input-footer-spacer"></div>
|
<div class="chat-input-footer-spacer"></div>
|
||||||
<chat-input-preference
|
<chat-input-preference
|
||||||
@@ -555,18 +558,6 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
this.updateContext({ images: newImages });
|
this.updateContext({ images: newImages });
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _uploadImageFiles = async (_e: MouseEvent) => {
|
|
||||||
if (this._isImageUploadDisabled) return;
|
|
||||||
|
|
||||||
const images = await openFilesWith('Images');
|
|
||||||
if (!images) return;
|
|
||||||
if (this.chatContextValue.images.length + images.length > MAX_IMAGE_COUNT) {
|
|
||||||
toast(`You can only upload up to ${MAX_IMAGE_COUNT} images`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.addImages(images);
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _onTextareaSend = async (e: MouseEvent | KeyboardEvent) => {
|
private readonly _onTextareaSend = async (e: MouseEvent | KeyboardEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||||
import type {
|
import type {
|
||||||
@@ -19,7 +20,8 @@ import { throttle } from 'lodash-es';
|
|||||||
import type { AppSidebarConfig } from '../../chat-panel/chat-config';
|
import type { AppSidebarConfig } from '../../chat-panel/chat-config';
|
||||||
import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const';
|
import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const';
|
||||||
import { AIProvider } from '../../provider';
|
import { AIProvider } from '../../provider';
|
||||||
import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
|
import type { SearchMenuConfig } from '../ai-chat-add-context';
|
||||||
|
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||||
import type { ChatContextValue } from '../ai-chat-content';
|
import type { ChatContextValue } from '../ai-chat-content';
|
||||||
import type {
|
import type {
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
@@ -165,6 +167,9 @@ export class PlaygroundChat extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor affineThemeService!: AppThemeService;
|
accessor affineThemeService!: AppThemeService;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor notificationService!: NotificationService;
|
accessor notificationService!: NotificationService;
|
||||||
|
|
||||||
@@ -351,6 +356,8 @@ export class PlaygroundChat extends SignalWatcher(
|
|||||||
.playgroundConfig=${this.playgroundConfig}
|
.playgroundConfig=${this.playgroundConfig}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.searchMenuConfig=${this.searchMenuConfig}
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
|
.notificationService=${this.notificationService}
|
||||||
|
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||||
></ai-chat-composer>
|
></ai-chat-composer>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { repeat } from 'lit/directives/repeat.js';
|
|||||||
|
|
||||||
import type { AppSidebarConfig } from '../../chat-panel/chat-config';
|
import type { AppSidebarConfig } from '../../chat-panel/chat-config';
|
||||||
import { AIProvider } from '../../provider';
|
import { AIProvider } from '../../provider';
|
||||||
import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
|
import type { SearchMenuConfig } from '../ai-chat-add-context';
|
||||||
|
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||||
import type {
|
import type {
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
AIPlaygroundConfig,
|
AIPlaygroundConfig,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { ChatMessageAction } from './chat-panel/message/action';
|
|||||||
import { ChatMessageAssistant } from './chat-panel/message/assistant';
|
import { ChatMessageAssistant } from './chat-panel/message/assistant';
|
||||||
import { ChatMessageUser } from './chat-panel/message/user';
|
import { ChatMessageUser } from './chat-panel/message/user';
|
||||||
import { ChatPanelSplitView } from './chat-panel/split-view';
|
import { ChatPanelSplitView } from './chat-panel/split-view';
|
||||||
|
import { AIChatAddContext } from './components/ai-chat-add-context';
|
||||||
import { ChatPanelAddPopover } from './components/ai-chat-chips/add-popover';
|
import { ChatPanelAddPopover } from './components/ai-chat-chips/add-popover';
|
||||||
import { ChatPanelCandidatesPopover } from './components/ai-chat-chips/candidates-popover';
|
import { ChatPanelCandidatesPopover } from './components/ai-chat-chips/candidates-popover';
|
||||||
import { ChatPanelChips } from './components/ai-chat-chips/chat-panel-chips';
|
import { ChatPanelChips } from './components/ai-chat-chips/chat-panel-chips';
|
||||||
@@ -138,6 +139,7 @@ export function registerAIEffects() {
|
|||||||
customElements.define('ai-chat-messages', AIChatMessages);
|
customElements.define('ai-chat-messages', AIChatMessages);
|
||||||
customElements.define('chat-panel', ChatPanel);
|
customElements.define('chat-panel', ChatPanel);
|
||||||
customElements.define('ai-chat-input', AIChatInput);
|
customElements.define('ai-chat-input', AIChatInput);
|
||||||
|
customElements.define('ai-chat-add-context', AIChatAddContext);
|
||||||
customElements.define(
|
customElements.define(
|
||||||
'ai-chat-embedding-status-tooltip',
|
'ai-chat-embedding-status-tooltip',
|
||||||
AIChatEmbeddingStatusTooltip
|
AIChatEmbeddingStatusTooltip
|
||||||
|
|||||||
@@ -29,10 +29,8 @@ import {
|
|||||||
queryHistoryMessages,
|
queryHistoryMessages,
|
||||||
} from '../_common/chat-actions-handle';
|
} from '../_common/chat-actions-handle';
|
||||||
import { type AIChatBlockModel } from '../blocks';
|
import { type AIChatBlockModel } from '../blocks';
|
||||||
import type {
|
import type { SearchMenuConfig } from '../components/ai-chat-add-context';
|
||||||
DocDisplayConfig,
|
import type { DocDisplayConfig } from '../components/ai-chat-chips';
|
||||||
SearchMenuConfig,
|
|
||||||
} from '../components/ai-chat-chips';
|
|
||||||
import type {
|
import type {
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
AIReasoningConfig,
|
AIReasoningConfig,
|
||||||
@@ -609,6 +607,7 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.searchMenuConfig=${this.searchMenuConfig}
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||||
|
.notificationService=${notificationService}
|
||||||
.onChatSuccess=${this._onChatSuccess}
|
.onChatSuccess=${this._onChatSuccess}
|
||||||
.trackOptions=${{
|
.trackOptions=${{
|
||||||
where: 'ai-chat-block',
|
where: 'ai-chat-block',
|
||||||
|
|||||||
@@ -260,8 +260,12 @@ export class ChatPanelUtils {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||||
// Open file upload dialog
|
const withButton = page.getByTestId('chat-panel-with-button');
|
||||||
await page.getByTestId('chat-panel-input-image-upload').click();
|
await withButton.hover();
|
||||||
|
await withButton.click({ delay: 200 });
|
||||||
|
const withMenu = page.getByTestId('ai-add-popover');
|
||||||
|
await withMenu.waitFor({ state: 'visible' });
|
||||||
|
await withMenu.getByTestId('ai-chat-with-images').click();
|
||||||
|
|
||||||
const fileChooser = await fileChooserPromise;
|
const fileChooser = await fileChooserPromise;
|
||||||
await fileChooser.setFiles(images);
|
await fileChooser.setFiles(images);
|
||||||
@@ -369,10 +373,4 @@ export class ChatPanelUtils {
|
|||||||
const networkSearch = await page.getByTestId('chat-network-search');
|
const networkSearch = await page.getByTestId('chat-network-search');
|
||||||
return (await networkSearch.getAttribute('aria-disabled')) === 'false';
|
return (await networkSearch.getAttribute('aria-disabled')) === 'false';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async isImageUploadEnabled(page: Page) {
|
|
||||||
const imageUpload = await page.getByTestId('chat-panel-input-image-upload');
|
|
||||||
const disabled = await imageUpload.getAttribute('data-disabled');
|
|
||||||
return disabled === 'false';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user