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:
akumatus
2025-01-09 04:00:58 +00:00
parent 4f10457815
commit 58ce86533e
49 changed files with 1274 additions and 169 deletions

View File

@@ -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>`,
],
[

View File

@@ -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;

View File

@@ -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;
}