diff --git a/packages/frontend/core/src/blocksuite/ai/actions/types.ts b/packages/frontend/core/src/blocksuite/ai/actions/types.ts index 01a87e61fb..c7d07df4ad 100644 --- a/packages/frontend/core/src/blocksuite/ai/actions/types.ts +++ b/packages/frontend/core/src/blocksuite/ai/actions/types.ts @@ -134,6 +134,7 @@ declare global { sessionId?: string; isRootSession?: boolean; mustSearch?: boolean; + reasoning?: boolean; contexts?: { docs: AIDocContextOption[]; files: AIFileContextOption[]; diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts index f9e59769e0..ad1105a94e 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts @@ -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=${{ diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts index 089accdc09..1536cdae3e 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts @@ -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} diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts index 29a3a6de62..7783648f05 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts @@ -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`
` : nothing} + ${images.length < MaximumImageCount ? html`
${ImageIcon()}
` @@ -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) { diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/type.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/type.ts index 6291c9dc6f..2e20d30a33 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/type.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/type.ts @@ -9,6 +9,11 @@ export interface AINetworkSearchConfig { setEnabled: (state: boolean) => void; } +export interface AIReasoningConfig { + enabled: Signal; + setEnabled: (state: boolean) => void; +} + // TODO: remove this type export type AIChatInputContext = { messages: HistoryMessage[]; diff --git a/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts b/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts index 30e624f8dc..5c1b8bf52b 100644 --- a/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts +++ b/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts @@ -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} >
`; } @@ -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``; }; diff --git a/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts b/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts index 5bbc602b31..ddb6c0b41b 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts +++ b/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts @@ -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 - ).toString(); + const queryString = this.paramsToQueryString({ + messageId, + seed, + }); + if (queryString) { + url += `?${queryString}`; } return this.eventSource(url); } + + paramsToQueryString(params: Record) { + 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(); + } } diff --git a/packages/frontend/core/src/blocksuite/ai/provider/request.ts b/packages/frontend/core/src/blocksuite/ai/provider/request.ts index 764d479c9b..8a64e00696 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/request.ts +++ b/packages/frontend/core/src/blocksuite/ai/provider/request.ts @@ -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, }); })(), ]); diff --git a/packages/frontend/core/src/components/hooks/affine/use-ai-chat-config.ts b/packages/frontend/core/src/components/hooks/affine/use-ai-chat-config.ts index a91eda7463..556cc55c90 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-ai-chat-config.ts +++ b/packages/frontend/core/src/components/hooks/affine/use-ai-chat-config.ts @@ -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, }; diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx index b3bcacabdb..d2da110d79 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx @@ -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
; diff --git a/packages/frontend/core/src/modules/ai-button/index.ts b/packages/frontend/core/src/modules/ai-button/index.ts index 2eace60e8e..576892059a 100644 --- a/packages/frontend/core/src/modules/ai-button/index.ts +++ b/packages/frontend/core/src/modules/ai-button/index.ts @@ -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]); +} diff --git a/packages/frontend/core/src/modules/ai-button/services/reasoning.ts b/packages/frontend/core/src/modules/ai-button/services/reasoning.ts new file mode 100644 index 0000000000..8df7e8b150 --- /dev/null +++ b/packages/frontend/core/src/modules/ai-button/services/reasoning.ts @@ -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( + this._enabled$, + undefined + ); + this.enabled = enabled; + this.disposables.push(enabledCleanup); + } + + enabled: Signal; + + private readonly _enabled$ = LiveData.from( + this.globalStateService.globalState.watch(AI_REASONING_KEY), + undefined + ); + + setEnabled = (enabled: boolean) => { + this.globalStateService.globalState.set(AI_REASONING_KEY, enabled); + }; +} diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts index 888366faeb..dac1223389 100644 --- a/packages/frontend/core/src/modules/index.ts +++ b/packages/frontend/core/src/modules/index.ts @@ -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); diff --git a/packages/frontend/core/src/modules/peek-view/view/ai-chat-block-peek-view/index.tsx b/packages/frontend/core/src/modules/peek-view/view/ai-chat-block-peek-view/index.tsx index 6c917a6537..667289b27e 100644 --- a/packages/frontend/core/src/modules/peek-view/view/ai-chat-block-peek-view/index.tsx +++ b/packages/frontend/core/src/modules/peek-view/view/ai-chat-block-peek-view/index.tsx @@ -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, + ]); };