mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat(core): support ai network search (#9357)
### What Changed?
- Add `PerplexityProvider` in backend.
- Update session prompt name if user toggle network search mode in chat panel.
- Add experimental flag for AI network search feature.
- Add unit tests and e2e tests.
Search results are streamed and appear word for word:
<div class='graphite__hidden'>
<div>🎥 Video uploaded on Graphite:</div>
<a href="https://app.graphite.dev/media/video/sJGviKxfE3Ap685cl5bj/56f6ec7b-4b21-405f-9612-43e083f6fb84.mov">
<img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/sJGviKxfE3Ap685cl5bj/56f6ec7b-4b21-405f-9612-43e083f6fb84.mov">
</a>
</div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/56f6ec7b-4b21-405f-9612-43e083f6fb84.mov">录屏2024-12-27 18.58.40.mov</video>
Click the little globe icon to manually turn on/off Internet search:
<div class='graphite__hidden'>
<div>🎥 Video uploaded on Graphite:</div>
<a href="https://app.graphite.dev/media/video/sJGviKxfE3Ap685cl5bj/778f1406-bf29-498e-a90d-7dad813392d1.mov">
<img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/sJGviKxfE3Ap685cl5bj/778f1406-bf29-498e-a90d-7dad813392d1.mov">
</a>
</div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/778f1406-bf29-498e-a90d-7dad813392d1.mov">录屏2024-12-27 19.01.16.mov</video>
When there is an image, it will automatically switch to the openai model:
<div class='graphite__hidden'>
<div>🎥 Video uploaded on Graphite:</div>
<a href="https://app.graphite.dev/media/video/sJGviKxfE3Ap685cl5bj/56431d8e-75e1-4d84-ab4a-b6636042cc6a.mov">
<img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/sJGviKxfE3Ap685cl5bj/56431d8e-75e1-4d84-ab4a-b6636042cc6a.mov">
</a>
</div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/56431d8e-75e1-4d84-ab4a-b6636042cc6a.mov">录屏2024-12-27 19.02.13.mov</video>
This commit is contained in:
@@ -63,7 +63,7 @@ export class AffineAIPanelWidget extends WidgetComponent {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
padding: 8px 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.ai-panel-container:not(:has(ai-panel-generating)) {
|
||||
@@ -474,6 +474,7 @@ export class AffineAIPanelWidget extends WidgetComponent {
|
||||
.onBlur=${this.discard}
|
||||
.onFinish=${this._inputFinish}
|
||||
.onInput=${this.onInput}
|
||||
.networkSearchConfig=${config.networkSearchConfig}
|
||||
></ai-panel-input>`,
|
||||
],
|
||||
[
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { AIStarIcon } from '@blocksuite/affine-components/icons';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { stopPropagation } from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { SendIcon } from '@blocksuite/icons/lit';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import { PublishIcon, SendIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
|
||||
export class AIPanelInput extends WithDisposable(LitElement) {
|
||||
import type { AINetworkSearchConfig } from '../../type';
|
||||
|
||||
export class AIPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
width: 100%;
|
||||
@@ -20,8 +23,9 @@ export class AIPanelInput extends WithDisposable(LitElement) {
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
.star {
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -66,22 +70,36 @@ export class AIPanelInput extends WithDisposable(LitElement) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
gap: 10px;
|
||||
gap: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--affine-black-10, rgba(0, 0, 0, 0.1));
|
||||
|
||||
background: ${unsafeCSSVarV2('icon/disable')};
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--affine-pure-white, #fff);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: ${unsafeCSSVarV2('button/pureWhiteText')};
|
||||
}
|
||||
}
|
||||
.arrow[data-active] {
|
||||
background: var(--affine-brand-color, #1e96eb);
|
||||
background: ${unsafeCSSVarV2('icon/activated')};
|
||||
}
|
||||
.arrow[data-active]:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.network {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
}
|
||||
.network[data-active='true'] svg {
|
||||
color: ${unsafeCSSVarV2('icon/activated')};
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _onInput = () => {
|
||||
@@ -101,12 +119,14 @@ export class AIPanelInput extends WithDisposable(LitElement) {
|
||||
|
||||
private readonly _onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
|
||||
e.preventDefault();
|
||||
this._sendToAI();
|
||||
this._sendToAI(e);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _sendToAI = () => {
|
||||
private readonly _sendToAI = (e: MouseEvent | KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const value = this.textarea.value.trim();
|
||||
if (value.length === 0) return;
|
||||
|
||||
@@ -114,9 +134,17 @@ export class AIPanelInput extends WithDisposable(LitElement) {
|
||||
this.remove();
|
||||
};
|
||||
|
||||
private readonly _toggleNetworkSearch = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const enable = this.networkSearchConfig.enabled.value;
|
||||
this.networkSearchConfig.setEnabled(!enable);
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`<div class="root">
|
||||
<div class="icon">${AIStarIcon}</div>
|
||||
<div class="star">${AIStarIcon}</div>
|
||||
<div class="textarea-container">
|
||||
<textarea
|
||||
placeholder="What are your thoughts?"
|
||||
@@ -131,6 +159,21 @@ export class AIPanelInput extends WithDisposable(LitElement) {
|
||||
@paste=${stopPropagation}
|
||||
@keyup=${stopPropagation}
|
||||
></textarea>
|
||||
${this.networkSearchConfig.visible.value
|
||||
? html`
|
||||
<div
|
||||
class="network"
|
||||
data-active=${!!this.networkSearchConfig.enabled.value}
|
||||
@click=${this._toggleNetworkSearch}
|
||||
@pointerdown=${stopPropagation}
|
||||
>
|
||||
${PublishIcon()}
|
||||
<affine-tooltip .offset=${12}
|
||||
>Toggle Network Search</affine-tooltip
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class="arrow"
|
||||
@click=${this._sendToAI}
|
||||
@@ -157,6 +200,9 @@ export class AIPanelInput extends WithDisposable(LitElement) {
|
||||
@state()
|
||||
private accessor _hasContent = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onFinish: ((input: string) => void) | undefined = undefined;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
AIError,
|
||||
AIItemGroupConfig,
|
||||
} from '@blocksuite/affine-components/ai-item';
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
import type { nothing, TemplateResult } from 'lit';
|
||||
|
||||
export interface CopyConfig {
|
||||
@@ -28,6 +29,12 @@ export interface AIPanelGeneratingConfig {
|
||||
stages?: string[];
|
||||
}
|
||||
|
||||
export interface AINetworkSearchConfig {
|
||||
visible: Signal<boolean | undefined>;
|
||||
enabled: Signal<boolean | undefined>;
|
||||
setEnabled: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export interface AffineAIPanelWidgetConfig {
|
||||
answerRenderer: (
|
||||
answer: string,
|
||||
@@ -44,10 +51,10 @@ export interface AffineAIPanelWidgetConfig {
|
||||
finishStateConfig: AIPanelAnswerConfig;
|
||||
generatingStateConfig: AIPanelGeneratingConfig;
|
||||
errorStateConfig: AIPanelErrorConfig;
|
||||
networkSearchConfig: AINetworkSearchConfig;
|
||||
hideCallback?: () => void;
|
||||
discardCallback?: () => void;
|
||||
inputCallback?: (input: string) => void;
|
||||
|
||||
copy?: CopyConfig;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user