mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
feat(core): add ai model switch ui (#12266)
Close [AI-86](https://linear.app/affine-design/issue/AI-86)  <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced AI model switching in chat, allowing users to select from multiple AI models during conversations. - Added a floating menu for easy AI model selection within the chat interface. - Enabled visibility of the AI model switcher through a new experimental feature flag, configurable in workspace settings (canary builds only). - **Enhancements** - Improved session management in the chat panel for smoother model switching and state handling. - Updated localization to support the new AI model switch feature in settings. - **Bug Fixes** - None. - **Chores** - Registered new components and services to support AI model switching functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1066,6 +1066,7 @@ const chat: Prompt[] = [
|
|||||||
name: 'Chat With AFFiNE AI',
|
name: 'Chat With AFFiNE AI',
|
||||||
model: 'gpt-4.1',
|
model: 'gpt-4.1',
|
||||||
optionalModels: [
|
optionalModels: [
|
||||||
|
'gpt-4.1',
|
||||||
'o3',
|
'o3',
|
||||||
'o4-mini',
|
'o4-mini',
|
||||||
'claude-3-7-sonnet-20250219',
|
'claude-3-7-sonnet-20250219',
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export class AnthropicProvider
|
|||||||
messages: msgs,
|
messages: msgs,
|
||||||
abortSignal: options.signal,
|
abortSignal: options.signal,
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
anthropic: this.getAnthropicOptions(options),
|
anthropic: this.getAnthropicOptions(options, model),
|
||||||
},
|
},
|
||||||
tools: this.getTools(),
|
tools: this.getTools(),
|
||||||
maxSteps: this.MAX_STEPS,
|
maxSteps: this.MAX_STEPS,
|
||||||
@@ -167,7 +167,7 @@ export class AnthropicProvider
|
|||||||
messages: msgs,
|
messages: msgs,
|
||||||
abortSignal: options.signal,
|
abortSignal: options.signal,
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
anthropic: this.getAnthropicOptions(options),
|
anthropic: this.getAnthropicOptions(options, model),
|
||||||
},
|
},
|
||||||
tools: this.getTools(),
|
tools: this.getTools(),
|
||||||
maxSteps: this.MAX_STEPS,
|
maxSteps: this.MAX_STEPS,
|
||||||
@@ -256,9 +256,9 @@ export class AnthropicProvider
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAnthropicOptions(options: CopilotChatOptions) {
|
private getAnthropicOptions(options: CopilotChatOptions, model: string) {
|
||||||
const result: AnthropicProviderOptions = {};
|
const result: AnthropicProviderOptions = {};
|
||||||
if (options?.reasoning) {
|
if (options?.reasoning && this.isThinkingModel(model)) {
|
||||||
result.thinking = {
|
result.thinking = {
|
||||||
type: 'enabled',
|
type: 'enabled',
|
||||||
budgetTokens: 12000,
|
budgetTokens: 12000,
|
||||||
@@ -282,4 +282,8 @@ export class AnthropicProvider
|
|||||||
private markAsCallout(text: string) {
|
private markAsCallout(text: string) {
|
||||||
return text.replaceAll('\n', '\n> ');
|
return text.replaceAll('\n', '\n> ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isThinkingModel(model: string) {
|
||||||
|
return model.startsWith('claude-3-7-sonnet');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,8 +200,7 @@ export class OpenAIProvider
|
|||||||
for (const tool of options.tools) {
|
for (const tool of options.tools) {
|
||||||
switch (tool) {
|
switch (tool) {
|
||||||
case 'webSearch': {
|
case 'webSearch': {
|
||||||
// o series reasoning models
|
if (this.isReasoningModel(model)) {
|
||||||
if (model.startsWith('o')) {
|
|
||||||
tools.web_search_exa = createExaSearchTool(this.AFFiNEConfig);
|
tools.web_search_exa = createExaSearchTool(this.AFFiNEConfig);
|
||||||
tools.web_crawl_exa = createExaCrawlTool(this.AFFiNEConfig);
|
tools.web_crawl_exa = createExaCrawlTool(this.AFFiNEConfig);
|
||||||
} else {
|
} else {
|
||||||
@@ -251,7 +250,7 @@ export class OpenAIProvider
|
|||||||
: await generateText({
|
: await generateText({
|
||||||
...commonParams,
|
...commonParams,
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
openai: this.getOpenAIOptions(options),
|
openai: this.getOpenAIOptions(options, model),
|
||||||
},
|
},
|
||||||
tools: this.getTools(options, model),
|
tools: this.getTools(options, model),
|
||||||
maxSteps: this.MAX_STEPS,
|
maxSteps: this.MAX_STEPS,
|
||||||
@@ -284,7 +283,7 @@ export class OpenAIProvider
|
|||||||
system,
|
system,
|
||||||
messages: msgs,
|
messages: msgs,
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
openai: this.getOpenAIOptions(options),
|
openai: this.getOpenAIOptions(options, model),
|
||||||
},
|
},
|
||||||
tools: tools as OpenAITools,
|
tools: tools as OpenAITools,
|
||||||
maxSteps: this.MAX_STEPS,
|
maxSteps: this.MAX_STEPS,
|
||||||
@@ -454,9 +453,9 @@ export class OpenAIProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOpenAIOptions(options: CopilotChatOptions) {
|
private getOpenAIOptions(options: CopilotChatOptions, model: string) {
|
||||||
const result: OpenAIResponsesProviderOptions = {};
|
const result: OpenAIResponsesProviderOptions = {};
|
||||||
if (options?.reasoning) {
|
if (options?.reasoning && this.isReasoningModel(model)) {
|
||||||
result.reasoningEffort = 'medium';
|
result.reasoningEffort = 'medium';
|
||||||
result.reasoningSummary = 'detailed';
|
result.reasoningSummary = 'detailed';
|
||||||
}
|
}
|
||||||
@@ -481,4 +480,9 @@ export class OpenAIProvider
|
|||||||
private markAsCallout(text: string) {
|
private markAsCallout(text: string) {
|
||||||
return text.replaceAll('\n', '\n> ');
|
return text.replaceAll('\n', '\n> ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isReasoningModel(model: string) {
|
||||||
|
// o series reasoning models
|
||||||
|
return model.startsWith('o');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import './chat-panel-messages';
|
import './chat-panel-messages';
|
||||||
|
|
||||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import type { ContextEmbedStatus } from '@affine/graphql';
|
import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
|
||||||
import { 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';
|
||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
SearchMenuConfig,
|
SearchMenuConfig,
|
||||||
} from '../components/ai-chat-chips';
|
} from '../components/ai-chat-chips';
|
||||||
import type {
|
import type {
|
||||||
|
AIModelSwitchConfig,
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
AIReasoningConfig,
|
AIReasoningConfig,
|
||||||
} from '../components/ai-chat-input';
|
} from '../components/ai-chat-input';
|
||||||
@@ -155,8 +156,8 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly _getSessionId = async () => {
|
private readonly _getSessionId = async () => {
|
||||||
if (this._sessionId) {
|
if (this.session) {
|
||||||
return this._sessionId;
|
return this.session.id;
|
||||||
}
|
}
|
||||||
const sessions = (
|
const sessions = (
|
||||||
(await AIProvider.session?.getSessions(
|
(await AIProvider.session?.getSessions(
|
||||||
@@ -164,22 +165,33 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
this.doc.id,
|
this.doc.id,
|
||||||
{ action: false }
|
{ action: false }
|
||||||
)) || []
|
)) || []
|
||||||
).filter(session => !session.parentSessionId);
|
).filter(session => {
|
||||||
const sessionId = sessions.at(-1)?.id;
|
if (this.parentSessionId) {
|
||||||
this._sessionId = sessionId;
|
return session.parentSessionId === this.parentSessionId;
|
||||||
return this._sessionId;
|
} else {
|
||||||
|
return !session.parentSessionId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.session = sessions.at(-1);
|
||||||
|
return this.session?.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _createSessionId = async () => {
|
private readonly _createSessionId = async () => {
|
||||||
if (this._sessionId) {
|
if (this.session) {
|
||||||
return this._sessionId;
|
return this.session.id;
|
||||||
}
|
}
|
||||||
this._sessionId = await AIProvider.session?.createSession({
|
const sessionId = await AIProvider.session?.createSession({
|
||||||
docId: this.doc.id,
|
docId: this.doc.id,
|
||||||
workspaceId: this.doc.workspace.id,
|
workspaceId: this.doc.workspace.id,
|
||||||
promptName: 'Chat With AFFiNE AI',
|
promptName: 'Chat With AFFiNE AI',
|
||||||
});
|
});
|
||||||
return this._sessionId;
|
if (sessionId) {
|
||||||
|
this.session = await AIProvider.session?.getSession(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
sessionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return sessionId;
|
||||||
};
|
};
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -194,6 +206,9 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor reasoningConfig!: AIReasoningConfig;
|
accessor reasoningConfig!: AIReasoningConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor modelSwitchConfig!: AIModelSwitchConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor appSidebarConfig!: AppSidebarConfig;
|
accessor appSidebarConfig!: AppSidebarConfig;
|
||||||
|
|
||||||
@@ -209,6 +224,9 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor parentSessionId: string | undefined = undefined;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor isLoading = false;
|
accessor isLoading = false;
|
||||||
|
|
||||||
@@ -218,10 +236,10 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
@state()
|
@state()
|
||||||
accessor embeddingProgress: [number, number] = [0, 0];
|
accessor embeddingProgress: [number, number] = [0, 0];
|
||||||
|
|
||||||
private _isInitialized = false;
|
@state()
|
||||||
|
accessor session: CopilotSessionType | undefined = undefined;
|
||||||
|
|
||||||
// always use getSessionId to get the sessionId
|
private _isInitialized = false;
|
||||||
private _sessionId: string | undefined = undefined;
|
|
||||||
|
|
||||||
private _isSidebarOpen: Signal<boolean | undefined> = signal(false);
|
private _isSidebarOpen: Signal<boolean | undefined> = signal(false);
|
||||||
|
|
||||||
@@ -252,7 +270,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly _resetPanel = () => {
|
private readonly _resetPanel = () => {
|
||||||
this._sessionId = undefined;
|
this.session = undefined;
|
||||||
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this._isInitialized = false;
|
this._isInitialized = false;
|
||||||
@@ -408,6 +426,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
<ai-chat-composer
|
<ai-chat-composer
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.doc=${this.doc}
|
.doc=${this.doc}
|
||||||
|
.session=${this.session}
|
||||||
.getSessionId=${this._getSessionId}
|
.getSessionId=${this._getSessionId}
|
||||||
.createSessionId=${this._createSessionId}
|
.createSessionId=${this._createSessionId}
|
||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
@@ -416,6 +435,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
.isVisible=${this._isSidebarOpen}
|
.isVisible=${this._isSidebarOpen}
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
.reasoningConfig=${this.reasoningConfig}
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
|
.modelSwitchConfig=${this.modelSwitchConfig}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.searchMenuConfig=${this.searchMenuConfig}
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
.trackOptions=${{
|
.trackOptions=${{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
CopilotContextDoc,
|
CopilotContextDoc,
|
||||||
CopilotContextFile,
|
CopilotContextFile,
|
||||||
CopilotDocType,
|
CopilotDocType,
|
||||||
|
CopilotSessionType,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
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';
|
||||||
@@ -26,6 +27,7 @@ import type {
|
|||||||
import { isCollectionChip, isDocChip, isTagChip } from '../ai-chat-chips';
|
import { isCollectionChip, isDocChip, isTagChip } from '../ai-chat-chips';
|
||||||
import type {
|
import type {
|
||||||
AIChatInputContext,
|
AIChatInputContext,
|
||||||
|
AIModelSwitchConfig,
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
AIReasoningConfig,
|
AIReasoningConfig,
|
||||||
} from '../ai-chat-input';
|
} from '../ai-chat-input';
|
||||||
@@ -53,6 +55,9 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor doc!: Store;
|
accessor doc!: Store;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor session!: CopilotSessionType | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getSessionId!: () => Promise<string | undefined>;
|
accessor getSessionId!: () => Promise<string | undefined>;
|
||||||
|
|
||||||
@@ -85,6 +90,9 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor searchMenuConfig!: SearchMenuConfig;
|
accessor searchMenuConfig!: SearchMenuConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor modelSwitchConfig!: AIModelSwitchConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onChatSuccess: (() => void) | undefined;
|
accessor onChatSuccess: (() => void) | undefined;
|
||||||
|
|
||||||
@@ -124,6 +132,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
<ai-chat-input
|
<ai-chat-input
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.chips=${this.chips}
|
.chips=${this.chips}
|
||||||
|
.session=${this.session}
|
||||||
.getSessionId=${this.getSessionId}
|
.getSessionId=${this.getSessionId}
|
||||||
.createSessionId=${this.createSessionId}
|
.createSessionId=${this.createSessionId}
|
||||||
.getContextId=${this._getContextId}
|
.getContextId=${this._getContextId}
|
||||||
@@ -131,6 +140,7 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
.updateContext=${this.updateContext}
|
.updateContext=${this.updateContext}
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
.reasoningConfig=${this.reasoningConfig}
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
|
.modelSwitchConfig=${this.modelSwitchConfig}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.onChatSuccess=${this.onChatSuccess}
|
.onChatSuccess=${this.onChatSuccess}
|
||||||
.trackOptions=${this.trackOptions}
|
.trackOptions=${this.trackOptions}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { stopPropagation } from '@affine/core/utils';
|
import { stopPropagation } from '@affine/core/utils';
|
||||||
|
import type { CopilotSessionType } 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 { openFileOrFiles } from '@blocksuite/affine/shared/utils';
|
import { openFileOrFiles } from '@blocksuite/affine/shared/utils';
|
||||||
@@ -34,6 +35,7 @@ import type { ChatMessage } from '../ai-chat-messages';
|
|||||||
import { MAX_IMAGE_COUNT } from './const';
|
import { MAX_IMAGE_COUNT } from './const';
|
||||||
import type {
|
import type {
|
||||||
AIChatInputContext,
|
AIChatInputContext,
|
||||||
|
AIModelSwitchConfig,
|
||||||
AINetworkSearchConfig,
|
AINetworkSearchConfig,
|
||||||
AIReasoningConfig,
|
AIReasoningConfig,
|
||||||
} from './type';
|
} from './type';
|
||||||
@@ -238,6 +240,9 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host!: EditorHost;
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor session!: CopilotSessionType | undefined;
|
||||||
|
|
||||||
@query('image-preview-grid')
|
@query('image-preview-grid')
|
||||||
accessor imagePreviewGrid: HTMLDivElement | null = null;
|
accessor imagePreviewGrid: HTMLDivElement | null = null;
|
||||||
|
|
||||||
@@ -250,6 +255,9 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
@state()
|
@state()
|
||||||
accessor focused = false;
|
accessor focused = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor modelId: string | undefined = undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor chatContextValue!: AIChatInputContext;
|
accessor chatContextValue!: AIChatInputContext;
|
||||||
|
|
||||||
@@ -274,6 +282,9 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor reasoningConfig!: AIReasoningConfig;
|
accessor reasoningConfig!: AIReasoningConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor modelSwitchConfig!: AIModelSwitchConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor docDisplayConfig!: DocDisplayConfig;
|
accessor docDisplayConfig!: DocDisplayConfig;
|
||||||
|
|
||||||
@@ -392,6 +403,16 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
${ImageIcon()}
|
${ImageIcon()}
|
||||||
<affine-tooltip>Upload</affine-tooltip>
|
<affine-tooltip>Upload</affine-tooltip>
|
||||||
</div>
|
</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
|
${this.networkSearchConfig.visible.value
|
||||||
? html`
|
? html`
|
||||||
<div
|
<div
|
||||||
@@ -536,6 +557,10 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
await this.send(value);
|
await this.send(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly _handleModelChange = (modelId: string) => {
|
||||||
|
this.modelId = modelId;
|
||||||
|
};
|
||||||
|
|
||||||
send = async (text: string) => {
|
send = async (text: string) => {
|
||||||
try {
|
try {
|
||||||
const { status, markdown, images } = this.chatContextValue;
|
const { status, markdown, images } = this.chatContextValue;
|
||||||
@@ -582,6 +607,7 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
control: this.trackOptions.control,
|
control: this.trackOptions.control,
|
||||||
webSearch: this._isNetworkActive,
|
webSearch: this._isNetworkActive,
|
||||||
reasoning: this._isReasoningActive,
|
reasoning: this._isReasoningActive,
|
||||||
|
modelId: this.modelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for await (const text of stream) {
|
for await (const text of stream) {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ export interface AIReasoningConfig {
|
|||||||
setEnabled: (state: boolean) => void;
|
setEnabled: (state: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AIModelSwitchConfig {
|
||||||
|
visible: Signal<boolean | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove this type
|
// TODO: remove this type
|
||||||
export type AIChatInputContext = {
|
export type AIChatInputContext = {
|
||||||
messages: HistoryMessage[];
|
messages: HistoryMessage[];
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './ai-chat-models';
|
||||||
@@ -38,6 +38,7 @@ import { ChatPanelFileChip } from './components/ai-chat-chips/file-chip';
|
|||||||
import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
|
import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
|
||||||
import { AIChatComposer } from './components/ai-chat-composer';
|
import { AIChatComposer } from './components/ai-chat-composer';
|
||||||
import { AIChatInput } from './components/ai-chat-input';
|
import { AIChatInput } from './components/ai-chat-input';
|
||||||
|
import { AIChatModels } from './components/ai-chat-models/ai-chat-models';
|
||||||
import { AIHistoryClear } from './components/ai-history-clear';
|
import { AIHistoryClear } from './components/ai-history-clear';
|
||||||
import { effects as componentAiItemEffects } from './components/ai-item';
|
import { effects as componentAiItemEffects } from './components/ai-item';
|
||||||
import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
|
import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
|
||||||
@@ -110,6 +111,7 @@ export function registerAIEffects() {
|
|||||||
customElements.define('chat-panel-tag-chip', ChatPanelTagChip);
|
customElements.define('chat-panel-tag-chip', ChatPanelTagChip);
|
||||||
customElements.define('chat-panel-collection-chip', ChatPanelCollectionChip);
|
customElements.define('chat-panel-collection-chip', ChatPanelCollectionChip);
|
||||||
customElements.define('chat-panel-chip', ChatPanelChip);
|
customElements.define('chat-panel-chip', ChatPanelChip);
|
||||||
|
customElements.define('ai-chat-models', AIChatModels);
|
||||||
customElements.define('ai-error-wrapper', AIErrorWrapper);
|
customElements.define('ai-error-wrapper', AIErrorWrapper);
|
||||||
customElements.define('ai-slides-renderer', AISlidesRenderer);
|
customElements.define('ai-slides-renderer', AISlidesRenderer);
|
||||||
customElements.define('ai-answer-wrapper', AIAnswerWrapper);
|
customElements.define('ai-answer-wrapper', AIAnswerWrapper);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// packages/frontend/core/src/blocksuite/ai/hooks/useChatPanelConfig.ts
|
// packages/frontend/core/src/blocksuite/ai/hooks/useChatPanelConfig.ts
|
||||||
|
import { AIModelSwitchService } from '@affine/core/modules/ai-button/services/model-switch';
|
||||||
import { AINetworkSearchService } from '@affine/core/modules/ai-button/services/network-search';
|
import { AINetworkSearchService } from '@affine/core/modules/ai-button/services/network-search';
|
||||||
import { AIReasoningService } from '@affine/core/modules/ai-button/services/reasoning';
|
import { AIReasoningService } from '@affine/core/modules/ai-button/services/reasoning';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
@@ -21,6 +22,7 @@ export function useAIChatConfig() {
|
|||||||
|
|
||||||
const searchService = framework.get(AINetworkSearchService);
|
const searchService = framework.get(AINetworkSearchService);
|
||||||
const reasoningService = framework.get(AIReasoningService);
|
const reasoningService = framework.get(AIReasoningService);
|
||||||
|
const modelSwitchService = framework.get(AIModelSwitchService);
|
||||||
const docDisplayMetaService = framework.get(DocDisplayMetaService);
|
const docDisplayMetaService = framework.get(DocDisplayMetaService);
|
||||||
const workspaceService = framework.get(WorkspaceService);
|
const workspaceService = framework.get(WorkspaceService);
|
||||||
const searchMenuService = framework.get(SearchMenuService);
|
const searchMenuService = framework.get(SearchMenuService);
|
||||||
@@ -40,6 +42,10 @@ export function useAIChatConfig() {
|
|||||||
setEnabled: reasoningService.setEnabled,
|
setEnabled: reasoningService.setEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const modelSwitchConfig = {
|
||||||
|
visible: modelSwitchService.visible,
|
||||||
|
};
|
||||||
|
|
||||||
const docDisplayConfig = {
|
const docDisplayConfig = {
|
||||||
getIcon: (docId: string) => {
|
getIcon: (docId: string) => {
|
||||||
return docDisplayMetaService.icon$(docId, { type: 'lit' }).value;
|
return docDisplayMetaService.icon$(docId, { type: 'lit' }).value;
|
||||||
@@ -124,5 +130,6 @@ export function useAIChatConfig() {
|
|||||||
reasoningConfig,
|
reasoningConfig,
|
||||||
docDisplayConfig,
|
docDisplayConfig,
|
||||||
searchMenuConfig,
|
searchMenuConfig,
|
||||||
|
modelSwitchConfig,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
|||||||
searchMenuConfig,
|
searchMenuConfig,
|
||||||
networkSearchConfig,
|
networkSearchConfig,
|
||||||
reasoningConfig,
|
reasoningConfig,
|
||||||
|
modelSwitchConfig,
|
||||||
} = useAIChatConfig();
|
} = useAIChatConfig();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,6 +74,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
|||||||
chatPanelRef.current.searchMenuConfig = searchMenuConfig;
|
chatPanelRef.current.searchMenuConfig = searchMenuConfig;
|
||||||
chatPanelRef.current.networkSearchConfig = networkSearchConfig;
|
chatPanelRef.current.networkSearchConfig = networkSearchConfig;
|
||||||
chatPanelRef.current.reasoningConfig = reasoningConfig;
|
chatPanelRef.current.reasoningConfig = reasoningConfig;
|
||||||
|
chatPanelRef.current.modelSwitchConfig = modelSwitchConfig;
|
||||||
chatPanelRef.current.extensions = editor.host.std
|
chatPanelRef.current.extensions = editor.host.std
|
||||||
.get(ViewExtensionManagerIdentifier)
|
.get(ViewExtensionManagerIdentifier)
|
||||||
.get('preview-page');
|
.get('preview-page');
|
||||||
@@ -107,6 +109,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
|||||||
networkSearchConfig,
|
networkSearchConfig,
|
||||||
searchMenuConfig,
|
searchMenuConfig,
|
||||||
reasoningConfig,
|
reasoningConfig,
|
||||||
|
modelSwitchConfig,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <div className={styles.root} ref={containerRef} />;
|
return <div className={styles.root} ref={containerRef} />;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { FeatureFlagService } from '../feature-flag';
|
|||||||
import { GlobalStateService } from '../storage';
|
import { GlobalStateService } from '../storage';
|
||||||
import { AIButtonProvider } from './provider/ai-button';
|
import { AIButtonProvider } from './provider/ai-button';
|
||||||
import { AIButtonService } from './services/ai-button';
|
import { AIButtonService } from './services/ai-button';
|
||||||
|
import { AIModelSwitchService } from './services/model-switch';
|
||||||
import { AINetworkSearchService } from './services/network-search';
|
import { AINetworkSearchService } from './services/network-search';
|
||||||
import { AIReasoningService } from './services/reasoning';
|
import { AIReasoningService } from './services/reasoning';
|
||||||
|
|
||||||
@@ -26,3 +27,7 @@ export function configureAINetworkSearchModule(framework: Framework) {
|
|||||||
export function configureAIReasoningModule(framework: Framework) {
|
export function configureAIReasoningModule(framework: Framework) {
|
||||||
framework.service(AIReasoningService, [GlobalStateService]);
|
framework.service(AIReasoningService, [GlobalStateService]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function configureAIModelSwitchModule(framework: Framework) {
|
||||||
|
framework.service(AIModelSwitchService, [FeatureFlagService]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
createSignalFromObservable,
|
||||||
|
type Signal,
|
||||||
|
} from '@blocksuite/affine/shared/utils';
|
||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { FeatureFlagService } from '../../feature-flag';
|
||||||
|
|
||||||
|
export class AIModelSwitchService extends Service {
|
||||||
|
constructor(private readonly featureFlagService: FeatureFlagService) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const { signal: visible, cleanup: visibleCleanup } =
|
||||||
|
createSignalFromObservable<boolean | undefined>(
|
||||||
|
this._visible$,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
this.visible = visible;
|
||||||
|
this.disposables.push(visibleCleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: Signal<boolean | undefined>;
|
||||||
|
|
||||||
|
private readonly _visible$ =
|
||||||
|
this.featureFlagService.flags.enable_ai_model_switch.$;
|
||||||
|
}
|
||||||
@@ -26,6 +26,15 @@ export const AFFINE_FLAGS = {
|
|||||||
configurable: false,
|
configurable: false,
|
||||||
defaultState: true,
|
defaultState: true,
|
||||||
},
|
},
|
||||||
|
enable_ai_model_switch: {
|
||||||
|
category: 'affine',
|
||||||
|
displayName:
|
||||||
|
'com.affine.settings.workspace.experimental-features.enable-ai-model-switch.name',
|
||||||
|
description:
|
||||||
|
'com.affine.settings.workspace.experimental-features.enable-ai-model-switch.description',
|
||||||
|
configurable: isCanaryBuild,
|
||||||
|
defaultState: isCanaryBuild,
|
||||||
|
},
|
||||||
enable_edgeless_text: {
|
enable_edgeless_text: {
|
||||||
category: 'blocksuite',
|
category: 'blocksuite',
|
||||||
bsFlag: 'enable_edgeless_text',
|
bsFlag: 'enable_edgeless_text',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type Framework } from '@toeverything/infra';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
configureAIButtonModule,
|
configureAIButtonModule,
|
||||||
|
configureAIModelSwitchModule,
|
||||||
configureAINetworkSearchModule,
|
configureAINetworkSearchModule,
|
||||||
configureAIReasoningModule,
|
configureAIReasoningModule,
|
||||||
} from './ai-button';
|
} from './ai-button';
|
||||||
@@ -105,6 +106,7 @@ export function configureCommonModules(framework: Framework) {
|
|||||||
configureCommonGlobalStorageImpls(framework);
|
configureCommonGlobalStorageImpls(framework);
|
||||||
configureAINetworkSearchModule(framework);
|
configureAINetworkSearchModule(framework);
|
||||||
configureAIReasoningModule(framework);
|
configureAIReasoningModule(framework);
|
||||||
|
configureAIModelSwitchModule(framework);
|
||||||
configureAIButtonModule(framework);
|
configureAIButtonModule(framework);
|
||||||
configureTemplateDocModule(framework);
|
configureTemplateDocModule(framework);
|
||||||
configureBlobManagementModule(framework);
|
configureBlobManagementModule(framework);
|
||||||
|
|||||||
@@ -5626,6 +5626,14 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Enable or disable AI Network Search feature.`
|
* `Enable or disable AI Network Search feature.`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.experimental-features.enable-ai-network-search.description"](): string;
|
["com.affine.settings.workspace.experimental-features.enable-ai-network-search.description"](): string;
|
||||||
|
/**
|
||||||
|
* `Enable AI Model Switch`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.experimental-features.enable-ai-model-switch.name"](): string;
|
||||||
|
/**
|
||||||
|
* `Enable or disable AI model switch feature.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.experimental-features.enable-ai-model-switch.description"](): string;
|
||||||
/**
|
/**
|
||||||
* `Database Full Width`
|
* `Database Full Width`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1405,6 +1405,8 @@
|
|||||||
"com.affine.settings.workspace.experimental-features.enable-ai.description": "Enable or disable ALL AI features.",
|
"com.affine.settings.workspace.experimental-features.enable-ai.description": "Enable or disable ALL AI features.",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-ai-network-search.name": "Enable AI Network Search",
|
"com.affine.settings.workspace.experimental-features.enable-ai-network-search.name": "Enable AI Network Search",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-ai-network-search.description": "Enable or disable AI Network Search feature.",
|
"com.affine.settings.workspace.experimental-features.enable-ai-network-search.description": "Enable or disable AI Network Search feature.",
|
||||||
|
"com.affine.settings.workspace.experimental-features.enable-ai-model-switch.name": "Enable AI Model Switch",
|
||||||
|
"com.affine.settings.workspace.experimental-features.enable-ai-model-switch.description": "Enable or disable AI model switch feature.",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-database-full-width.name": "Database Full Width",
|
"com.affine.settings.workspace.experimental-features.enable-database-full-width.name": "Database Full Width",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-database-full-width.description": "The database will be displayed in full-width mode.",
|
"com.affine.settings.workspace.experimental-features.enable-database-full-width.description": "The database will be displayed in full-width mode.",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-database-attachment-note.name": "Database Attachment Note",
|
"com.affine.settings.workspace.experimental-features.enable-database-attachment-note.name": "Database Attachment Note",
|
||||||
|
|||||||
Reference in New Issue
Block a user