mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
feat(core): adjust the layout, style, and structure of the AI chat input (#12828)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for image uploads in the chat panel, including upload limits and user feedback when limits are exceeded. - Introduced a unified chat input preference menu for selecting AI models, toggling extended thinking, and enabling web search. - Menu buttons and menus now support test identifiers for improved testing. - **Improvements** - Updated chat input UI with enhanced styling, consolidated controls, and simplified feature toggling. - Improved layout and spacing for chat chips and image preview grids. - Chat abort icon now adapts to the current color theme. - **Refactor** - Replaced the separate AI model selection component with the new chat input preference menu. - Streamlined imports and custom element registrations for chat input preferences. - **Tests** - Enhanced test utilities to support the new chat input preference menu interactions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -155,7 +155,7 @@ export const ChatAbortIcon = html`<svg
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M19.0833 11.9993C19.0833 15.9114 15.912 19.0827 12 19.0827C8.08798 19.0827 4.91667 15.9114 4.91667 11.9993C4.91667 8.08733 8.08798 4.91602 12 4.91602C15.912 4.91602 19.0833 8.08733 19.0833 11.9993ZM20.3333 11.9993C20.3333 16.6017 16.6024 20.3327 12 20.3327C7.39763 20.3327 3.66667 16.6017 3.66667 11.9993C3.66667 7.39698 7.39763 3.66602 12 3.66602C16.6024 3.66602 20.3333 7.39698 20.3333 11.9993ZM10.3333 8.66602C9.41286 8.66602 8.66667 9.41221 8.66667 10.3327V13.666C8.66667 14.5865 9.41286 15.3327 10.3333 15.3327H13.6667C14.5871 15.3327 15.3333 14.5865 15.3333 13.666V10.3327C15.3333 9.41221 14.5871 8.66602 13.6667 8.66602H10.3333Z"
|
||||
fill="#1E96EB"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import {
|
||||
CollectionsIcon,
|
||||
ImageIcon,
|
||||
MoreHorizontalIcon,
|
||||
SearchIcon,
|
||||
TagsIcon,
|
||||
@@ -20,6 +21,7 @@ import { css, html, type TemplateResult } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { MAX_IMAGE_COUNT } from '../ai-chat-input';
|
||||
import type { ChatChip, DocDisplayConfig, SearchMenuConfig } from './type';
|
||||
|
||||
enum AddPopoverMode {
|
||||
@@ -182,9 +184,28 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
this.abortController.abort();
|
||||
};
|
||||
|
||||
private readonly _addImageChip = async () => {
|
||||
if (this.isImageUploadDisabled) return;
|
||||
|
||||
const images = await openFilesWith('Images');
|
||||
if (!images) return;
|
||||
if (this.uploadImageCount + images.length > MAX_IMAGE_COUNT) {
|
||||
toast(`You can only upload up to ${MAX_IMAGE_COUNT} images`);
|
||||
return;
|
||||
}
|
||||
this.addImages(images);
|
||||
};
|
||||
|
||||
private readonly uploadGroup: MenuGroup = {
|
||||
name: 'Upload',
|
||||
items: [
|
||||
{
|
||||
key: 'images',
|
||||
name: 'Upload images',
|
||||
testId: 'ai-chat-with-images',
|
||||
icon: ImageIcon(),
|
||||
action: this._addImageChip,
|
||||
},
|
||||
{
|
||||
key: 'files',
|
||||
name: 'Upload files (pdf, txt, csv)',
|
||||
@@ -267,6 +288,12 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
@property({ attribute: 'data-testid', reflect: true })
|
||||
accessor testId: string = 'ai-search-input';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor isImageUploadDisabled!: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor uploadImageCount!: number;
|
||||
|
||||
@query('.search-input')
|
||||
accessor searchInput!: HTMLInputElement;
|
||||
|
||||
|
||||
@@ -41,23 +41,27 @@ export class ChatPanelChips extends SignalWatcher(
|
||||
static override styles = css`
|
||||
.chips-wrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -4px 0 -4px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
.add-button,
|
||||
.collapse-button,
|
||||
.more-candidate-button {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
border-radius: 4px;
|
||||
margin: 4px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
.add-button:hover,
|
||||
.collapse-button:hover,
|
||||
|
||||
@@ -16,7 +16,6 @@ export class ChatPanelChip extends SignalWatcher(
|
||||
height: 24px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 4px;
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { stopPropagation } from '@affine/core/utils';
|
||||
import type { CopilotSessionType } from '@affine/graphql';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import { openFilesWith } from '@blocksuite/affine/shared/utils';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import {
|
||||
CloseIcon,
|
||||
ImageIcon,
|
||||
PublishIcon,
|
||||
ThinkingIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { ArrowUpBigIcon, CloseIcon, ImageIcon } from '@blocksuite/icons/lit';
|
||||
import { type Signal, signal } from '@preact/signals-core';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { ChatAbortIcon, ChatSendIcon } from '../../_common/icons';
|
||||
import { ChatAbortIcon } from '../../_common/icons';
|
||||
import { type AIError, AIProvider } from '../../provider';
|
||||
import { reportResponse } from '../../utils/action-reporter';
|
||||
import { readBlobAsURL } from '../../utils/image';
|
||||
@@ -45,20 +39,42 @@ export class AIChatInput extends SignalWatcher(
|
||||
:host {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .chat-panel-input {
|
||||
box-shadow:
|
||||
var(--border-shadow),
|
||||
0px 0px 0px 0px rgba(28, 158, 228, 0),
|
||||
0px 0px 0px 2px transparent;
|
||||
}
|
||||
[data-theme='light'] .chat-panel-input {
|
||||
box-shadow:
|
||||
var(--border-shadow),
|
||||
0px 0px 0px 3px transparent,
|
||||
0px 2px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
[data-theme='dark'] .chat-panel-input[data-if-focused='true'] {
|
||||
box-shadow:
|
||||
var(--border-shadow),
|
||||
0px 0px 0px 3px rgba(28, 158, 228, 0.3),
|
||||
0px 2px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chat-panel-input {
|
||||
--input-border-width: 0.5px;
|
||||
--input-border-color: var(--affine-v2-layer-insideBorder-border);
|
||||
--border-shadow: 0px 0px 0px var(--input-border-width)
|
||||
var(--input-border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
margin-top: 12px;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
padding: 8px 6px 6px 8px;
|
||||
min-height: 94px;
|
||||
box-sizing: border-box;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--affine-border-color);
|
||||
transition: box-shadow 0.23s ease;
|
||||
background-color: var(--affine-v2-input-background);
|
||||
|
||||
.chat-selection-quote {
|
||||
padding: 4px 0px 8px 0px;
|
||||
@@ -207,7 +223,7 @@ export class AIChatInput extends SignalWatcher(
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-family: var(--affine-font-family);
|
||||
color: var(--affine-placeholder-color);
|
||||
color: var(--affine-v2-text-placeholder);
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
@@ -216,8 +232,8 @@ export class AIChatInput extends SignalWatcher(
|
||||
}
|
||||
|
||||
.chat-panel-input[data-if-focused='true'] {
|
||||
border-color: var(--affine-primary-color);
|
||||
box-shadow: var(--affine-active-shadow);
|
||||
--input-border-width: 1px;
|
||||
--input-border-color: var(--affine-v2-layer-insideBorder-primaryBorder);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -225,16 +241,32 @@ export class AIChatInput extends SignalWatcher(
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-panel-send svg rect {
|
||||
fill: var(--affine-primary-color);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
background: var(--affine-v2-icon-activated);
|
||||
color: var(--affine-v2-layer-pureWhite);
|
||||
}
|
||||
.chat-panel-send[aria-disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
background: var(--affine-v2-button-disable);
|
||||
}
|
||||
.chat-panel-send[aria-disabled='true'] svg rect {
|
||||
fill: var(--affine-text-disable-color);
|
||||
.chat-panel-stop {
|
||||
cursor: pointer;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
font-size: 24px;
|
||||
color: var(--affine-v2-icon-activated);
|
||||
}
|
||||
.chat-input-footer-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -342,7 +374,6 @@ export class AIChatInput extends SignalWatcher(
|
||||
const { images, status } = this.chatContextValue;
|
||||
const hasImages = images.length > 0;
|
||||
const maxHeight = hasImages ? 272 + 2 : 200 + 2;
|
||||
const showLabel = this.panelWidth.value && this.panelWidth.value > 400;
|
||||
|
||||
return html` <div
|
||||
class="chat-panel-input"
|
||||
@@ -404,62 +435,34 @@ export class AIChatInput extends SignalWatcher(
|
||||
${ImageIcon()}
|
||||
<affine-tooltip>Upload</affine-tooltip>
|
||||
</div>
|
||||
${this.modelSwitchConfig?.visible.value
|
||||
? html`
|
||||
<ai-chat-models
|
||||
class="chat-input-icon"
|
||||
.modelId=${this.modelId}
|
||||
.session=${this.session}
|
||||
.onModelChange=${this._handleModelChange}
|
||||
></ai-chat-models>
|
||||
`
|
||||
: nothing}
|
||||
${this.networkSearchConfig.visible.value
|
||||
? html`
|
||||
<div
|
||||
class="chat-input-icon"
|
||||
data-testid="chat-network-search"
|
||||
data-active=${this._isNetworkActive}
|
||||
@click=${this._toggleNetworkSearch}
|
||||
@pointerdown=${stopPropagation}
|
||||
>
|
||||
${PublishIcon()}
|
||||
${!showLabel
|
||||
? html`<affine-tooltip>Search</affine-tooltip>`
|
||||
: nothing}
|
||||
${showLabel
|
||||
? html`<span class="chat-input-icon-label">Search</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class="chat-input-icon"
|
||||
data-testid="chat-reasoning"
|
||||
data-active=${this._isReasoningActive}
|
||||
@click=${this._toggleReasoning}
|
||||
@pointerdown=${stopPropagation}
|
||||
>
|
||||
${ThinkingIcon()}
|
||||
${!showLabel
|
||||
? html`<affine-tooltip>Reason</affine-tooltip>`
|
||||
: nothing}
|
||||
${showLabel
|
||||
? html`<span class="chat-input-icon-label">Reason</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="chat-input-footer-spacer"></div>
|
||||
<chat-input-preference
|
||||
.modelSwitchConfig=${this.modelSwitchConfig}
|
||||
.session=${this.session}
|
||||
.onModelChange=${this._handleModelChange}
|
||||
.modelId=${this.modelId}
|
||||
.extendedThinking=${this._isReasoningActive}
|
||||
.onExtendedThinkingChange=${this._toggleReasoning}
|
||||
.networkSearchVisible=${!!this.networkSearchConfig.visible.value}
|
||||
.isNetworkActive=${this._isNetworkActive}
|
||||
.onNetworkActiveChange=${this._toggleNetworkSearch}
|
||||
></chat-input-preference>
|
||||
${status === 'transmitting' || status === 'loading'
|
||||
? html`<div @click=${this._handleAbort} data-testid="chat-panel-stop">
|
||||
? html`<button
|
||||
class="chat-panel-stop"
|
||||
@click=${this._handleAbort}
|
||||
data-testid="chat-panel-stop"
|
||||
>
|
||||
${ChatAbortIcon}
|
||||
</div>`
|
||||
: html`<div
|
||||
</button>`
|
||||
: html`<button
|
||||
@click="${this._onTextareaSend}"
|
||||
class="chat-panel-send"
|
||||
aria-disabled=${this.isInputEmpty}
|
||||
data-testid="chat-panel-send"
|
||||
>
|
||||
${ChatSendIcon}
|
||||
</div>`}
|
||||
${ArrowUpBigIcon()}
|
||||
</button>`}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -512,20 +515,12 @@ export class AIChatInput extends SignalWatcher(
|
||||
reportResponse('aborted:stop');
|
||||
};
|
||||
|
||||
private readonly _toggleNetworkSearch = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const enable = this.networkSearchConfig.enabled.value;
|
||||
this.networkSearchConfig.setEnabled(!enable);
|
||||
private readonly _toggleNetworkSearch = (isNetworkActive: boolean) => {
|
||||
this.networkSearchConfig.setEnabled(isNetworkActive);
|
||||
};
|
||||
|
||||
private readonly _toggleReasoning = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const enable = this.reasoningConfig.enabled.value;
|
||||
this.reasoningConfig.setEnabled(!enable);
|
||||
private readonly _toggleReasoning = (extendedThinking: boolean) => {
|
||||
this.reasoningConfig.setEnabled(extendedThinking);
|
||||
};
|
||||
|
||||
private readonly _handleImageRemove = (index: number) => {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import type { CopilotSessionType } from '@affine/graphql';
|
||||
import {
|
||||
menu,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine/components/context-menu';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import {
|
||||
AiOutlineIcon,
|
||||
ArrowDownSmallIcon,
|
||||
ThinkingIcon,
|
||||
WebIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { css, html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { AIModelSwitchConfig } from './type';
|
||||
|
||||
export class ChatInputPreference extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.chat-input-preference-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px 4px;
|
||||
color: var(--affine-v2-icon-primary);
|
||||
transition: all 0.23s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.chat-input-preference-trigger:hover {
|
||||
background-color: var(--affine-v2-layer-background-hoverOverlay);
|
||||
}
|
||||
.chat-input-preference-trigger-label {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 500;
|
||||
padding: 0px 4px;
|
||||
}
|
||||
.chat-input-preference-trigger-icon {
|
||||
font-size: 20px;
|
||||
line-height: 0;
|
||||
}
|
||||
.preference-action {
|
||||
white-space: nowrap;
|
||||
min-width: 220px;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor session!: CopilotSessionType | undefined;
|
||||
|
||||
// --------- model props start ---------
|
||||
@property({ attribute: false })
|
||||
accessor modelSwitchConfig: AIModelSwitchConfig | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onModelChange: ((modelId: string) => void) | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor modelId: string | undefined = undefined;
|
||||
// --------- model props end ---------
|
||||
|
||||
// --------- extended thinking props start ---------
|
||||
@property({ attribute: false })
|
||||
accessor extendedThinking: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onExtendedThinkingChange:
|
||||
| ((extendedThinking: boolean) => void)
|
||||
| undefined;
|
||||
// --------- extended thinking props end ---------
|
||||
|
||||
// --------- search props start ---------
|
||||
@property({ attribute: false })
|
||||
accessor networkSearchVisible: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor isNetworkActive: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onNetworkActiveChange:
|
||||
| ((isNetworkActive: boolean) => void)
|
||||
| undefined;
|
||||
// --------- search props end ---------
|
||||
|
||||
private readonly _onModelChange = (modelId: string) => {
|
||||
this.onModelChange?.(modelId);
|
||||
};
|
||||
|
||||
openPreference(e: Event) {
|
||||
const element = e.currentTarget;
|
||||
if (!(element instanceof HTMLElement)) return;
|
||||
const modelItems = [];
|
||||
const searchItems = [];
|
||||
|
||||
// model switch
|
||||
if (this.modelSwitchConfig?.visible.value) {
|
||||
modelItems.push(
|
||||
menu.subMenu({
|
||||
name: 'Model',
|
||||
prefix: AiOutlineIcon(),
|
||||
options: {
|
||||
items: (this.session?.optionalModels ?? []).map(modelId => {
|
||||
return menu.action({
|
||||
name: modelId,
|
||||
select: () => this._onModelChange(modelId),
|
||||
});
|
||||
}),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
modelItems.push(
|
||||
menu.toggleSwitch({
|
||||
name: 'Extended Thinking',
|
||||
prefix: ThinkingIcon(),
|
||||
on: this.extendedThinking,
|
||||
onChange: (value: boolean) => this.onExtendedThinkingChange?.(value),
|
||||
class: { 'preference-action': true },
|
||||
})
|
||||
);
|
||||
|
||||
if (this.networkSearchVisible) {
|
||||
searchItems.push(
|
||||
menu.toggleSwitch({
|
||||
name: 'Web Search',
|
||||
prefix: WebIcon(),
|
||||
on: this.isNetworkActive,
|
||||
onChange: (value: boolean) => this.onNetworkActiveChange?.(value),
|
||||
class: { 'preference-action': true },
|
||||
testId: 'chat-network-search',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
popMenu(popupTargetFromElement(element), {
|
||||
options: {
|
||||
items: [
|
||||
menu.group({
|
||||
items: [...modelItems],
|
||||
}),
|
||||
menu.group({
|
||||
items: [...searchItems],
|
||||
}),
|
||||
],
|
||||
testId: 'chat-input-preference',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<button
|
||||
@click=${this.openPreference}
|
||||
data-testid="chat-input-preference-trigger"
|
||||
class="chat-input-preference-trigger"
|
||||
>
|
||||
<span class="chat-input-preference-trigger-label">
|
||||
${this.modelId || this.session?.model}
|
||||
</span>
|
||||
<span class="chat-input-preference-trigger-icon">
|
||||
${ArrowDownSmallIcon()}
|
||||
</span>
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import type { CopilotSessionType } from '@affine/graphql';
|
||||
import { createLitPortal } from '@blocksuite/affine/components/portal';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
export class AIChatModels extends WithDisposable(ShadowlessElement) {
|
||||
@property()
|
||||
accessor modelId: string | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onModelChange: ((modelId: string) => void) | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor session!: CopilotSessionType | undefined;
|
||||
|
||||
@query('.ai-chat-models')
|
||||
accessor modelsButton!: HTMLDivElement;
|
||||
|
||||
private _abortController: AbortController | null = null;
|
||||
|
||||
static override styles = css`
|
||||
ai-chat-models {
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _onItemClick = (modelId: string) => {
|
||||
this.onModelChange?.(modelId);
|
||||
this._abortController?.abort();
|
||||
this._abortController = null;
|
||||
};
|
||||
|
||||
private readonly _toggleSwitchModelMenu = () => {
|
||||
if (this._abortController) {
|
||||
this._abortController.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
this._abortController = new AbortController();
|
||||
this._abortController.signal.addEventListener('abort', () => {
|
||||
this._abortController = null;
|
||||
});
|
||||
|
||||
createLitPortal({
|
||||
template: html` <style>
|
||||
.ai-model-list {
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
border-radius: 4px;
|
||||
background: ${unsafeCSSVarV2('layer/background/overlayPanel')};
|
||||
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
|
||||
padding: 8px;
|
||||
}
|
||||
.ai-model-item {
|
||||
font-size: 13px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ai-model-item:hover {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
</style>
|
||||
<div class="ai-model-list">
|
||||
${repeat(
|
||||
this.session?.optionalModels ?? [],
|
||||
modelId => modelId,
|
||||
modelId => {
|
||||
return html`<div
|
||||
class="ai-model-item"
|
||||
@click=${() => this._onItemClick(modelId)}
|
||||
>
|
||||
${modelId}
|
||||
</div>`;
|
||||
}
|
||||
)}
|
||||
</div>`,
|
||||
portalStyles: {
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
},
|
||||
container: document.body,
|
||||
computePosition: {
|
||||
referenceElement: this.modelsButton,
|
||||
placement: 'top-start',
|
||||
middleware: [offset({ crossAxis: -30, mainAxis: 8 }), flip()],
|
||||
autoUpdate: { animationFrame: true },
|
||||
},
|
||||
abortController: this._abortController,
|
||||
closeOnClickAway: true,
|
||||
});
|
||||
};
|
||||
|
||||
override render() {
|
||||
if (!this.session) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="ai-chat-models"
|
||||
@click=${this._toggleSwitchModelMenu}
|
||||
data-testid="ai-chat-models"
|
||||
>
|
||||
${this.modelId || this.session.model}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './ai-chat-models';
|
||||
@@ -18,16 +18,15 @@ export class ImagePreviewGrid extends LitElement {
|
||||
.images-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
@@ -39,7 +39,7 @@ import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
|
||||
import { AIChatComposer } from './components/ai-chat-composer';
|
||||
import { AIChatInput } from './components/ai-chat-input';
|
||||
import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embedding-status-tooltip';
|
||||
import { AIChatModels } from './components/ai-chat-models/ai-chat-models';
|
||||
import { ChatInputPreference } from './components/ai-chat-input/preference-popup';
|
||||
import { AIHistoryClear } from './components/ai-history-clear';
|
||||
import { effects as componentAiItemEffects } from './components/ai-item';
|
||||
import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
|
||||
@@ -109,6 +109,7 @@ export function registerAIEffects() {
|
||||
customElements.define('chat-panel-chips', ChatPanelChips);
|
||||
customElements.define('ai-history-clear', AIHistoryClear);
|
||||
customElements.define('chat-panel-add-popover', ChatPanelAddPopover);
|
||||
customElements.define('chat-input-preference', ChatInputPreference);
|
||||
customElements.define(
|
||||
'chat-panel-candidates-popover',
|
||||
ChatPanelCandidatesPopover
|
||||
@@ -118,7 +119,6 @@ export function registerAIEffects() {
|
||||
customElements.define('chat-panel-tag-chip', ChatPanelTagChip);
|
||||
customElements.define('chat-panel-collection-chip', ChatPanelCollectionChip);
|
||||
customElements.define('chat-panel-chip', ChatPanelChip);
|
||||
customElements.define('ai-chat-models', AIChatModels);
|
||||
customElements.define('ai-error-wrapper', AIErrorWrapper);
|
||||
customElements.define('ai-slides-renderer', AISlidesRenderer);
|
||||
customElements.define('ai-answer-wrapper', AIAnswerWrapper);
|
||||
|
||||
Reference in New Issue
Block a user