mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(core): add reasoning icon button (#11941)
Close [AI-58](https://linear.app/affine-design/issue/AI-58). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a toggleable AI reasoning feature in the chat interface, allowing users to enable or disable advanced reasoning during AI chat interactions. - Added a new reasoning button to the chat input for quick access and control. - **Enhancements** - Improved chat configuration options to include reasoning settings, providing more flexibility for AI responses. - Streamlined network search and image upload interactions for a smoother user experience. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -134,6 +134,7 @@ declare global {
|
||||
sessionId?: string;
|
||||
isRootSession?: boolean;
|
||||
mustSearch?: boolean;
|
||||
reasoning?: boolean;
|
||||
contexts?: {
|
||||
docs: AIDocContextOption[];
|
||||
files: AIFileContextOption[];
|
||||
|
||||
@@ -17,7 +17,10 @@ import type {
|
||||
DocDisplayConfig,
|
||||
SearchMenuConfig,
|
||||
} from '../components/ai-chat-chips';
|
||||
import type { AINetworkSearchConfig } from '../components/ai-chat-input';
|
||||
import type {
|
||||
AINetworkSearchConfig,
|
||||
AIReasoningConfig,
|
||||
} from '../components/ai-chat-input';
|
||||
import { type HistoryMessage } from '../components/ai-chat-messages';
|
||||
import { AIProvider } from '../provider';
|
||||
import { extractSelectedContent } from '../utils/extract';
|
||||
@@ -197,6 +200,9 @@ export class ChatPanel extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor reasoningConfig!: AIReasoningConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor appSidebarConfig!: AppSidebarConfig;
|
||||
|
||||
@@ -415,6 +421,7 @@ export class ChatPanel extends SignalWatcher(
|
||||
.onHistoryCleared=${this._updateHistory}
|
||||
.isVisible=${this._isSidebarOpen}
|
||||
.networkSearchConfig=${this.networkSearchConfig}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.trackOptions=${{
|
||||
|
||||
@@ -28,6 +28,7 @@ import { isCollectionChip, isDocChip, isTagChip } from '../ai-chat-chips';
|
||||
import type {
|
||||
AIChatInputContext,
|
||||
AINetworkSearchConfig,
|
||||
AIReasoningConfig,
|
||||
} from '../ai-chat-input';
|
||||
|
||||
export class AIChatComposer extends SignalWatcher(
|
||||
@@ -81,6 +82,9 @@ export class AIChatComposer extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor reasoningConfig!: AIReasoningConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor searchMenuConfig!: SearchMenuConfig;
|
||||
|
||||
@@ -125,6 +129,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
.chatContextValue=${this.chatContextValue}
|
||||
.updateContext=${this.updateContext}
|
||||
.networkSearchConfig=${this.networkSearchConfig}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.cleanupHistories=${this._cleanupHistories}
|
||||
.onChatSuccess=${this.onChatSuccess}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CloseIcon,
|
||||
ImageIcon,
|
||||
PublishIcon,
|
||||
ThinkingIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
@@ -30,7 +31,11 @@ import {
|
||||
isTagChip,
|
||||
} from '../ai-chat-chips/utils';
|
||||
import type { ChatMessage } from '../ai-chat-messages';
|
||||
import type { AIChatInputContext, AINetworkSearchConfig } from './type';
|
||||
import type {
|
||||
AIChatInputContext,
|
||||
AINetworkSearchConfig,
|
||||
AIReasoningConfig,
|
||||
} from './type';
|
||||
|
||||
const MaximumImageCount = 32;
|
||||
|
||||
@@ -241,6 +246,9 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
@property({ attribute: false })
|
||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor reasoningConfig!: AIReasoningConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayConfig!: DocDisplayConfig;
|
||||
|
||||
@@ -259,16 +267,12 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
private get _isNetworkActive() {
|
||||
return (
|
||||
!!this.networkSearchConfig.visible.value &&
|
||||
!!this.networkSearchConfig.enabled.value &&
|
||||
!this._isNetworkDisabled
|
||||
!!this.networkSearchConfig.enabled.value
|
||||
);
|
||||
}
|
||||
|
||||
private get _isNetworkDisabled() {
|
||||
return (
|
||||
!!this.chatContextValue.images.length ||
|
||||
!!this.chips.filter(chip => chip.state === 'finished').length
|
||||
);
|
||||
private get _isReasoningActive() {
|
||||
return !!this.reasoningConfig.enabled.value;
|
||||
}
|
||||
|
||||
private get _isClearDisabled() {
|
||||
@@ -299,7 +303,6 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
const { images, status } = this.chatContextValue;
|
||||
const hasImages = images.length > 0;
|
||||
const maxHeight = hasImages ? 272 + 2 : 200 + 2;
|
||||
const uploadDisabled = this._isNetworkActive;
|
||||
return html` <div
|
||||
class="chat-panel-input"
|
||||
data-if-focused=${this.focused}
|
||||
@@ -364,7 +367,6 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
<div
|
||||
class="chat-network-search"
|
||||
data-testid="chat-network-search"
|
||||
aria-disabled=${this._isNetworkDisabled}
|
||||
data-active=${this._isNetworkActive}
|
||||
@click=${this._toggleNetworkSearch}
|
||||
@pointerdown=${stopPropagation}
|
||||
@@ -373,12 +375,20 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class="chat-network-search"
|
||||
data-testid="chat-reasoning"
|
||||
data-active=${this._isReasoningActive}
|
||||
@click=${this._toggleReasoning}
|
||||
@pointerdown=${stopPropagation}
|
||||
>
|
||||
${ThinkingIcon()}
|
||||
</div>
|
||||
${images.length < MaximumImageCount
|
||||
? html`<div
|
||||
data-testid="chat-panel-input-image-upload"
|
||||
class="image-upload"
|
||||
aria-disabled=${uploadDisabled}
|
||||
@click=${uploadDisabled ? undefined : this._uploadImageFiles}
|
||||
@click=${this._uploadImageFiles}
|
||||
>
|
||||
${ImageIcon()}
|
||||
</div>`
|
||||
@@ -457,13 +467,18 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this._isNetworkDisabled) {
|
||||
return;
|
||||
}
|
||||
const enable = this.networkSearchConfig.enabled.value;
|
||||
this.networkSearchConfig.setEnabled(!enable);
|
||||
};
|
||||
|
||||
private readonly _toggleReasoning = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const enable = this.reasoningConfig.enabled.value;
|
||||
this.reasoningConfig.setEnabled(!enable);
|
||||
};
|
||||
|
||||
private _addImages(images: File[]) {
|
||||
const oldImages = this.chatContextValue.images;
|
||||
this.updateContext({
|
||||
@@ -545,6 +560,7 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
where: this.trackOptions.where,
|
||||
control: this.trackOptions.control,
|
||||
mustSearch: this._isNetworkActive,
|
||||
reasoning: this._isReasoningActive,
|
||||
});
|
||||
|
||||
for await (const text of stream) {
|
||||
|
||||
@@ -9,6 +9,11 @@ export interface AINetworkSearchConfig {
|
||||
setEnabled: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export interface AIReasoningConfig {
|
||||
enabled: Signal<boolean | undefined>;
|
||||
setEnabled: (state: boolean) => void;
|
||||
}
|
||||
|
||||
// TODO: remove this type
|
||||
export type AIChatInputContext = {
|
||||
messages: HistoryMessage[];
|
||||
|
||||
@@ -28,7 +28,10 @@ import type {
|
||||
DocDisplayConfig,
|
||||
SearchMenuConfig,
|
||||
} from '../components/ai-chat-chips';
|
||||
import type { AINetworkSearchConfig } from '../components/ai-chat-input';
|
||||
import type {
|
||||
AINetworkSearchConfig,
|
||||
AIReasoningConfig,
|
||||
} from '../components/ai-chat-input';
|
||||
import type { ChatMessage } from '../components/ai-chat-messages';
|
||||
import { ChatMessagesSchema } from '../components/ai-chat-messages';
|
||||
import type { TextRendererOptions } from '../components/text-renderer';
|
||||
@@ -519,6 +522,7 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
control: 'chat-send',
|
||||
}}
|
||||
.portalContainer=${this.parentElement}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
></ai-chat-composer>
|
||||
</div> `;
|
||||
}
|
||||
@@ -535,6 +539,9 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
@property({ attribute: false })
|
||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor reasoningConfig!: AIReasoningConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayConfig!: DocDisplayConfig;
|
||||
|
||||
@@ -568,7 +575,8 @@ export const AIChatBlockPeekViewTemplate = (
|
||||
host: EditorHost,
|
||||
docDisplayConfig: DocDisplayConfig,
|
||||
searchMenuConfig: SearchMenuConfig,
|
||||
networkSearchConfig: AINetworkSearchConfig
|
||||
networkSearchConfig: AINetworkSearchConfig,
|
||||
reasoningConfig: AIReasoningConfig
|
||||
) => {
|
||||
return html`<ai-chat-block-peek-view
|
||||
.blockModel=${blockModel}
|
||||
@@ -576,5 +584,6 @@ export const AIChatBlockPeekViewTemplate = (
|
||||
.networkSearchConfig=${networkSearchConfig}
|
||||
.docDisplayConfig=${docDisplayConfig}
|
||||
.searchMenuConfig=${searchMenuConfig}
|
||||
.reasoningConfig=${reasoningConfig}
|
||||
></ai-chat-block-peek-view>`;
|
||||
};
|
||||
|
||||
@@ -350,15 +350,21 @@ export class CopilotClient {
|
||||
async chatText({
|
||||
sessionId,
|
||||
messageId,
|
||||
reasoning,
|
||||
signal,
|
||||
}: {
|
||||
sessionId: string;
|
||||
messageId?: string;
|
||||
reasoning?: boolean;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
let url = `/api/copilot/chat/${sessionId}`;
|
||||
if (messageId) {
|
||||
url += `?messageId=${encodeURIComponent(messageId)}`;
|
||||
const queryString = this.paramsToQueryString({
|
||||
messageId,
|
||||
reasoning,
|
||||
});
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
}
|
||||
const response = await this.fetcher(url.toString(), { signal });
|
||||
return response.text();
|
||||
@@ -369,15 +375,21 @@ export class CopilotClient {
|
||||
{
|
||||
sessionId,
|
||||
messageId,
|
||||
reasoning,
|
||||
}: {
|
||||
sessionId: string;
|
||||
messageId?: string;
|
||||
reasoning?: boolean;
|
||||
},
|
||||
endpoint = 'stream'
|
||||
) {
|
||||
let url = `/api/copilot/chat/${sessionId}/${endpoint}`;
|
||||
if (messageId) {
|
||||
url += `?messageId=${encodeURIComponent(messageId)}`;
|
||||
const queryString = this.paramsToQueryString({
|
||||
messageId,
|
||||
reasoning,
|
||||
});
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
}
|
||||
return this.eventSource(url);
|
||||
}
|
||||
@@ -390,17 +402,27 @@ export class CopilotClient {
|
||||
endpoint = 'images'
|
||||
) {
|
||||
let url = `/api/copilot/chat/${sessionId}/${endpoint}`;
|
||||
|
||||
if (messageId || seed) {
|
||||
url += '?';
|
||||
url += new URLSearchParams(
|
||||
Object.fromEntries(
|
||||
Object.entries({ messageId, seed }).filter(
|
||||
([_, v]) => v !== undefined
|
||||
)
|
||||
) as Record<string, string>
|
||||
).toString();
|
||||
const queryString = this.paramsToQueryString({
|
||||
messageId,
|
||||
seed,
|
||||
});
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
}
|
||||
return this.eventSource(url);
|
||||
}
|
||||
|
||||
paramsToQueryString(params: Record<string, string | boolean | undefined>) {
|
||||
const queryString = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (typeof value === 'boolean') {
|
||||
if (value) {
|
||||
queryString.append(key, 'true');
|
||||
}
|
||||
} else if (typeof value === 'string') {
|
||||
queryString.append(key, value);
|
||||
}
|
||||
});
|
||||
return queryString.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export type TextToTextOptions = {
|
||||
workflow?: boolean;
|
||||
isRootSession?: boolean;
|
||||
postfix?: (text: string) => string;
|
||||
reasoning?: boolean;
|
||||
};
|
||||
|
||||
export type ToImageOptions = TextToTextOptions & {
|
||||
@@ -113,6 +114,7 @@ export function textToText({
|
||||
retry = false,
|
||||
workflow = false,
|
||||
postfix,
|
||||
reasoning,
|
||||
}: TextToTextOptions) {
|
||||
let messageId: string | undefined;
|
||||
|
||||
@@ -132,6 +134,7 @@ export function textToText({
|
||||
{
|
||||
sessionId,
|
||||
messageId,
|
||||
reasoning,
|
||||
},
|
||||
workflow ? 'workflow' : undefined
|
||||
);
|
||||
@@ -191,6 +194,7 @@ export function textToText({
|
||||
return client.chatText({
|
||||
sessionId,
|
||||
messageId,
|
||||
reasoning,
|
||||
});
|
||||
})(),
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// packages/frontend/core/src/blocksuite/ai/hooks/useChatPanelConfig.ts
|
||||
import { AINetworkSearchService } from '@affine/core/modules/ai-button/services/network-search';
|
||||
import { AIReasoningService } from '@affine/core/modules/ai-button/services/reasoning';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
@@ -19,6 +20,7 @@ export function useAIChatConfig() {
|
||||
const framework = useFramework();
|
||||
|
||||
const searchService = framework.get(AINetworkSearchService);
|
||||
const reasoningService = framework.get(AIReasoningService);
|
||||
const docDisplayMetaService = framework.get(DocDisplayMetaService);
|
||||
const workspaceService = framework.get(WorkspaceService);
|
||||
const searchMenuService = framework.get(SearchMenuService);
|
||||
@@ -33,6 +35,11 @@ export function useAIChatConfig() {
|
||||
setEnabled: searchService.setEnabled,
|
||||
};
|
||||
|
||||
const reasoningConfig = {
|
||||
enabled: reasoningService.enabled,
|
||||
setEnabled: reasoningService.setEnabled,
|
||||
};
|
||||
|
||||
const docDisplayConfig = {
|
||||
getIcon: (docId: string) => {
|
||||
return docDisplayMetaService.icon$(docId, { type: 'lit' }).value;
|
||||
@@ -114,6 +121,7 @@ export function useAIChatConfig() {
|
||||
|
||||
return {
|
||||
networkSearchConfig,
|
||||
reasoningConfig,
|
||||
docDisplayConfig,
|
||||
searchMenuConfig,
|
||||
};
|
||||
|
||||
@@ -41,8 +41,12 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||
}
|
||||
}, [onLoad, ref]);
|
||||
|
||||
const { docDisplayConfig, searchMenuConfig, networkSearchConfig } =
|
||||
useAIChatConfig();
|
||||
const {
|
||||
docDisplayConfig,
|
||||
searchMenuConfig,
|
||||
networkSearchConfig,
|
||||
reasoningConfig,
|
||||
} = useAIChatConfig();
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !editor.host) return;
|
||||
@@ -67,6 +71,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||
chatPanelRef.current.docDisplayConfig = docDisplayConfig;
|
||||
chatPanelRef.current.searchMenuConfig = searchMenuConfig;
|
||||
chatPanelRef.current.networkSearchConfig = networkSearchConfig;
|
||||
chatPanelRef.current.reasoningConfig = reasoningConfig;
|
||||
chatPanelRef.current.extensions = editor.host.std
|
||||
.get(ViewExtensionManagerIdentifier)
|
||||
.get('preview-page');
|
||||
@@ -98,6 +103,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||
framework,
|
||||
networkSearchConfig,
|
||||
searchMenuConfig,
|
||||
reasoningConfig,
|
||||
]);
|
||||
|
||||
return <div className={styles.root} ref={containerRef} />;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { GlobalStateService } from '../storage';
|
||||
import { AIButtonProvider } from './provider/ai-button';
|
||||
import { AIButtonService } from './services/ai-button';
|
||||
import { AINetworkSearchService } from './services/network-search';
|
||||
import { AIReasoningService } from './services/reasoning';
|
||||
|
||||
export const configureAIButtonModule = (framework: Framework) => {
|
||||
framework.service(AIButtonService, container => {
|
||||
@@ -21,3 +22,7 @@ export function configureAINetworkSearchModule(framework: Framework) {
|
||||
FeatureFlagService,
|
||||
]);
|
||||
}
|
||||
|
||||
export function configureAIReasoningModule(framework: Framework) {
|
||||
framework.service(AIReasoningService, [GlobalStateService]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
createSignalFromObservable,
|
||||
type Signal,
|
||||
} from '@blocksuite/affine/shared/utils';
|
||||
import { LiveData, Service } from '@toeverything/infra';
|
||||
|
||||
import type { GlobalStateService } from '../../storage';
|
||||
|
||||
const AI_REASONING_KEY = 'AIReasoning';
|
||||
|
||||
export class AIReasoningService extends Service {
|
||||
constructor(private readonly globalStateService: GlobalStateService) {
|
||||
super();
|
||||
|
||||
const { signal: enabled, cleanup: enabledCleanup } =
|
||||
createSignalFromObservable<boolean | undefined>(
|
||||
this._enabled$,
|
||||
undefined
|
||||
);
|
||||
this.enabled = enabled;
|
||||
this.disposables.push(enabledCleanup);
|
||||
}
|
||||
|
||||
enabled: Signal<boolean | undefined>;
|
||||
|
||||
private readonly _enabled$ = LiveData.from(
|
||||
this.globalStateService.globalState.watch<boolean>(AI_REASONING_KEY),
|
||||
undefined
|
||||
);
|
||||
|
||||
setEnabled = (enabled: boolean) => {
|
||||
this.globalStateService.globalState.set(AI_REASONING_KEY, enabled);
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { type Framework } from '@toeverything/infra';
|
||||
import {
|
||||
configureAIButtonModule,
|
||||
configureAINetworkSearchModule,
|
||||
configureAIReasoningModule,
|
||||
} from './ai-button';
|
||||
import { configureAppSidebarModule } from './app-sidebar';
|
||||
import { configAtMenuConfigModule } from './at-menu-config';
|
||||
@@ -101,6 +102,7 @@ export function configureCommonModules(framework: Framework) {
|
||||
configureDndModule(framework);
|
||||
configureCommonGlobalStorageImpls(framework);
|
||||
configureAINetworkSearchModule(framework);
|
||||
configureAIReasoningModule(framework);
|
||||
configureAIButtonModule(framework);
|
||||
configureTemplateDocModule(framework);
|
||||
configureBlobManagementModule(framework);
|
||||
|
||||
@@ -14,16 +14,29 @@ export const AIChatBlockPeekView = ({
|
||||
model,
|
||||
host,
|
||||
}: AIChatBlockPeekViewProps) => {
|
||||
const { docDisplayConfig, searchMenuConfig, networkSearchConfig } =
|
||||
useAIChatConfig();
|
||||
const {
|
||||
docDisplayConfig,
|
||||
searchMenuConfig,
|
||||
networkSearchConfig,
|
||||
reasoningConfig,
|
||||
} = useAIChatConfig();
|
||||
|
||||
return useMemo(() => {
|
||||
const template = AIChatBlockPeekViewTemplate(
|
||||
model,
|
||||
host,
|
||||
docDisplayConfig,
|
||||
searchMenuConfig,
|
||||
networkSearchConfig
|
||||
networkSearchConfig,
|
||||
reasoningConfig
|
||||
);
|
||||
return toReactNode(template);
|
||||
}, [model, host, docDisplayConfig, searchMenuConfig, networkSearchConfig]);
|
||||
}, [
|
||||
model,
|
||||
host,
|
||||
docDisplayConfig,
|
||||
searchMenuConfig,
|
||||
networkSearchConfig,
|
||||
reasoningConfig,
|
||||
]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user