chore: migrate oxlint & oxfmt (#15464)

#### PR Dependency Tree


* **PR #15464** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Replaced the project’s formatting and linting workflow with Oxfmt and
Oxlint.
* Added shared formatting and linting configuration, editor integration,
and updated automated checks.
* Updated generated files, scripts, and lint guidance to use the new
tooling.

* **Style**
* Reformatted templates, source code, examples, and configuration files
for consistent readability.
  * No user-facing functionality or rendering behavior changed.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-08-10 23:51:28 +08:00
committed by GitHub
parent 749c83cd8e
commit 0c7b20dc18
292 changed files with 3372 additions and 2866 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/* oxlint-disable */
declare const _default: typeof import('./index')
export default _default
+1 -1
View File
@@ -1,5 +1,5 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/* oxlint-disable */
declare const _default: typeof import('./index')
export default _default
+1 -1
View File
@@ -1,4 +1,4 @@
/* eslint-disable */
/* oxlint-disable */
import '../src/prelude';
import '../src/app.module';
@@ -261,7 +261,7 @@ export class AuthGuard implements CanActivate, OnModuleInit {
private getVersionRange(versionRange: string): semver.Range | null {
if (this.cachedVersionRange.has(versionRange)) {
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
return this.cachedVersionRange.get(versionRange)!;
}
@@ -132,7 +132,7 @@ export class AuthService implements OnApplicationBootstrap {
// fallback to the first valid session if user provided userId is invalid
if (!userSession) {
// checked
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
userSession = sessions.at(-1)!;
}
@@ -24,7 +24,7 @@ export class Lock {
private release: () => void = () => {};
async acquire() {
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
let release: () => void = null!;
const nextLock = new Promise<void>(resolve => {
release = resolve;
@@ -35,7 +35,7 @@ export class DocID {
return this.variant === DocVariant.Workspace
? this.workspace
: // sub is always truthy when variant is not workspace
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
this.sub!;
}
@@ -165,7 +165,7 @@ function fetchContent(
if (Array.isArray(content.props.children)) {
return content.props.children.map((child, i) => {
/* oxlint-disable-next-line eslint-plugin-react/no-array-index-key */
/* oxlint-disable-next-line react/no-array-index-key */
return <Row key={i}>{child}</Row>;
});
}
@@ -351,7 +351,7 @@ export class CopilotTranscriptionResolver {
user.id,
workspaceId,
blobId,
// oxlint-disable-next-line @typescript-eslint/await-thenable
// oxlint-disable-next-line typescript/await-thenable
await Promise.all(allBlobs),
input ?? undefined
);
+1 -1
View File
@@ -1,6 +1,6 @@
hooks:
afterOneFileWrite:
- prettier --write
- oxfmt --write
config:
strict: true
maybeValue: T | null
+1 -1
View File
@@ -16,7 +16,7 @@
"@graphql-codegen/typescript": "^5.0.9",
"@graphql-codegen/typescript-operations": "^5.0.9",
"@types/lodash-es": "^4.17.12",
"prettier": "^3.7.4",
"oxfmt": "0.63.0",
"vitest": "^4.1.8"
},
"scripts": {
@@ -64,7 +64,7 @@ export abstract class FrameworkProvider {
createEntity = <
T extends Entity<any>,
Props extends T extends Component<infer P> ? P : never,
Props extends (T extends Component<infer P> ? P : never),
>(
identifier: GeneralIdentifier<T>,
...[props]: Props extends Record<string, never> ? [] : [Props]
@@ -78,7 +78,7 @@ export abstract class FrameworkProvider {
createScope = <
T extends Scope<any>,
Props extends T extends Component<infer P> ? P : never,
Props extends (T extends Component<infer P> ? P : never),
>(
root: GeneralIdentifier<T>,
...[props]: Props extends Record<string, never> ? [] : [Props]
@@ -514,7 +514,7 @@ export class LiveData<T = unknown>
const subscription = this.subscribe(v => {
if (predicate(v)) {
resolve(v as any);
// oxlint-disable-next-line @typescript-eslint/no-floating-promises
// oxlint-disable-next-line typescript/no-floating-promises
Promise.resolve().then(() => {
subscription.unsubscribe();
});
@@ -552,7 +552,7 @@ export class LiveData<T = unknown>
throw this.poisonedError;
}
this.ops$.next('watch');
// oxlint-disable-next-line @typescript-eslint/no-floating-promises -- never throw
// oxlint-disable-next-line typescript/no-floating-promises -- never throw
Promise.resolve().then(() => {
this.ops$.next('unwatch');
});
+24 -16
View File
@@ -27,25 +27,29 @@ type Typeof<F extends FieldSchemaBuilder> =
F extends FieldSchemaBuilder<infer Type> ? Type : never;
type RequiredFields<T extends TableSchemaBuilder> = {
[K in TableDefinedFieldNames<T> as T[K] extends FieldSchemaBuilder<
any,
infer Optional
>
? Optional extends false
? K
[
K in TableDefinedFieldNames<T> as T[K] extends FieldSchemaBuilder<
any,
infer Optional
>
? Optional extends false
? K
: never
: never
: never]: Typeof<T[K]>;
]: Typeof<T[K]>;
};
type OptionalFields<T extends TableSchemaBuilder> = {
[K in TableDefinedFieldNames<T> as T[K] extends FieldSchemaBuilder<
any,
infer Optional
>
? Optional extends true
? K
[
K in TableDefinedFieldNames<T> as T[K] extends FieldSchemaBuilder<
any,
infer Optional
>
? Optional extends true
? K
: never
: never
: never]?: Typeof<T[K]> | null;
]?: Typeof<T[K]> | null;
};
type PrimaryKeyField<T extends TableSchemaBuilder> = {
@@ -104,7 +108,9 @@ export type UpdateEntityInput<T extends TableSchemaBuilder> = Pretty<
MaybeDocumentEntityWrapper<
T,
{
[key in NonPrimaryKeyFieldNames<T>]?: key extends keyof TableDefinedEntity<T>
[
key in NonPrimaryKeyFieldNames<T>
]?: key extends keyof TableDefinedEntity<T>
? TableDefinedEntity<T>[key]
: never;
}
@@ -115,7 +121,9 @@ export type FindEntityInput<T extends TableSchemaBuilder> = Pretty<
MaybeDocumentEntityWrapper<
T,
{
[key in TableDefinedFieldNames<T>]?: key extends keyof TableDefinedEntity<T>
[
key in TableDefinedFieldNames<T>
]?: key extends keyof TableDefinedEntity<T>
?
| TableDefinedEntity<T>[key]
| { not: TableDefinedEntity<T>[key] | null }
+14 -3
View File
@@ -30,14 +30,21 @@ import type { ConnectionStatus } from '@affine/nbstore';
import { IndexedDBDocStorage } from '@affine/nbstore/idb';
import { SqliteBlobStorage } from '@affine/nbstore/sqlite';
const storage = new SpaceStorage([new IndexedDBDocStorage({}), new SqliteBlobStorage({})]);
const storage = new SpaceStorage([
new IndexedDBDocStorage({}),
new SqliteBlobStorage({}),
]);
await storage.connect();
storage.on('connection', ({ storage, status, error }) => {
ui.show(storage, status, error);
});
await storage.get('doc').pushDocUpdate({ docId: 'my-first-doc', bin: new Uint8Array(), editor: 'me' });
await storage.get('doc').pushDocUpdate({
docId: 'my-first-doc',
bin: new Uint8Array(),
editor: 'me',
});
await storage.tryGet('blob')?.get('img');
```
@@ -61,7 +68,11 @@ client.ob$('connection', ({ storage, status, error }) => {
ui.show(storage, status, error);
});
await client.call('pushDocUpdate', { docId: 'my-first-doc', bin: new Uint8Array(), editor: 'me' });
await client.call('pushDocUpdate', {
docId: 'my-first-doc',
bin: new Uint8Array(),
editor: 'me',
});
// call unregistered op will leads to Error
// Error { message: 'Handler for operation [listHistory] is not registered.' }
@@ -52,7 +52,7 @@ export class NGramTokenizer implements Tokenizer {
tokenize(text: string): Token[] {
const splitted: Token[] = [];
for (let i = 0; i < text.length; ) {
for (let i = 0; i < text.length;) {
const nextBreak = Graphemer.nextBreak(text, i);
const c = text.substring(i, nextBreak);
@@ -119,7 +119,7 @@ export class GeneralTokenizer implements Tokenizer {
let end = 0;
let lang: string | null = null;
for (let i = 0; i < text.length; ) {
for (let i = 0; i < text.length;) {
const nextBreak = Graphemer.nextBreak(text, i);
const c = text.substring(i, nextBreak);
@@ -23,7 +23,7 @@ export class IndexedDBLocker implements Locker {
async lock(domain: string, resource: string) {
const key = `${domain}:${resource}`;
// eslint-disable-next-line no-constant-condition
// oxlint-disable-next-line no-constant-condition
while (true) {
const trx = this.db.transaction('locks', 'readwrite');
const record = await trx.store.get(key);
+1 -1
View File
@@ -26,7 +26,7 @@ export class Lock {
private release: () => void = () => {};
async acquire() {
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
let release: () => void = null!;
const nextLock = new Promise<void>(resolve => {
release = resolve;
@@ -190,7 +190,7 @@ export class BlobSyncImpl implements BlobSync {
): Promise<void> {
return Promise.race([
Promise.all(
// oxlint-disable-next-line @typescript-eslint/await-thenable
// oxlint-disable-next-line typescript/await-thenable
peerId
? [this.fullDownloadPeer(peerId)]
: this.peers.map(p => this.fullDownloadPeer(p.peerId))
@@ -94,7 +94,7 @@ export class BlobSyncPeer {
Math.pow(2, attempts - 1) * backoffRetry.delay,
backoffRetry.maxDelay
);
// oxlint-disable-next-line @typescript-eslint/no-misused-promises
// oxlint-disable-next-line typescript/no-misused-promises
setTimeout(attempt, waitTime);
} else {
// reach the max retry times, resolve the promise with false
@@ -107,7 +107,7 @@ export class BlobSyncPeer {
}
};
// oxlint-disable-next-line @typescript-eslint/no-floating-promises
// oxlint-disable-next-line typescript/no-floating-promises
attempt();
})
.catch(error => {
@@ -1,4 +1,4 @@
// eslint-disable
// oxlint-disable
// @ts-nocheck
import { Node } from './utils/node';
import { encodeLink } from './utils/url';
@@ -1,4 +1,4 @@
// eslint-disable
// oxlint-disable
// @ts-nocheck
import { Node } from './utils/node';
@@ -1,4 +1,4 @@
// eslint-disable
// oxlint-disable
// @ts-nocheck
let id = 0;
@@ -68,7 +68,7 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
setColumnFilters([]);
}, [resetFiltersDeps]);
// eslint-disable-next-line react-hooks/incompatible-library
// oxlint-disable-next-line react-hooks-js/incompatible-library
const table = useReactTable({
data,
columns,
@@ -38,7 +38,7 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
// eslint-disable-next-line react/no-unknown-property
// oxlint-disable-next-line react/no-unknown-property
<div className="flex items-center px-3 border-b" cmdk-input-wrapper="">
<Search className="w-4 h-4 mr-2 opacity-50 shrink-0" />
<CommandPrimitive.Input
+1 -1
View File
@@ -5,5 +5,5 @@ import { createRoot } from 'react-dom/client';
import { App } from './app';
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
createRoot(document.getElementById('app')!).render(<App />);
@@ -323,7 +323,7 @@ export const useExportUsers = () => {
});
dataToCopy.push(row);
});
// oxlint-disable-next-line @typescript-eslint/no-floating-promises
// oxlint-disable-next-line typescript/no-floating-promises
navigator.clipboard.writeText(JSON.stringify(dataToCopy, null, 2));
callback?.();
},
@@ -18,7 +18,7 @@ function main() {
}
function mountApp() {
// oxlint-disable-next-line typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
const root = document.getElementById('app')!;
createRoot(root).render(
<StrictMode>
@@ -98,7 +98,7 @@ export const config = (): BuildOptions => {
JSON.stringify(val) ??
String(
val
) /* JSON.stringify(undefined) == undefined, but we need 'undefined' */;
); /* JSON.stringify(undefined) == undefined, but we need 'undefined' */
return def;
},
{} as Record<string, string>
@@ -39,7 +39,7 @@ async function getWorkspaceDB(spaceType: SpaceType, id: string) {
process.on('beforeExit', cleanup);
}
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
return db!;
}
@@ -65,7 +65,7 @@ if (overrideSession) {
app.setPath('sessionData', userDataPath);
}
// oxlint-disable-next-line @typescript-eslint/no-var-requires
// oxlint-disable-next-line typescript/no-var-requires
if (require('electron-squirrel-startup')) app.quit();
if (process.env.SKIP_ONBOARDING) {
@@ -1,4 +1,4 @@
// eslint-disable no-var-requires
// oxlint-disable typescript/no-var-requires
// Should not load @affine/native for unsupported platforms
@@ -132,7 +132,6 @@ function createWorkspaceRef() {
parentId?: string
) => string;
getBlock: (id: string) => MockBlockRecord;
// eslint-disable-next-line rxjs/finnish
getBlock$: (id: string) => MockBlockRecord;
};
const attachments: Array<{
@@ -180,7 +179,6 @@ function createWorkspaceRef() {
addBlock: (...args: Parameters<typeof blockSuiteDoc.addBlock>) =>
blockSuiteDoc.addBlock(...args),
getBlock: (blockId: string) => blockSuiteDoc.getBlock(blockId),
// eslint-disable-next-line rxjs/finnish
getBlock$: (blockId: string) => blockSuiteDoc.getBlock(blockId),
},
};
+1 -1
View File
@@ -87,7 +87,7 @@ Follow the guidelines in `AGENTS.md`:
### Testing and Quality
- TypeScript strict mode enabled
- ESLint/Prettier configuration from workspace root
- Oxlint/Oxfmt configuration from workspace root
- No specific test commands in this package (tests likely in workspace root)
# Swift Code Style Guidelines
+1 -1
View File
@@ -13,7 +13,7 @@ import { NbStoreNativeDBApis } from './plugins/nbstore';
bindNativeDBApis(NbStoreNativeDBApis);
function mountApp() {
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
const root = document.getElementById('app')!;
createRoot(root).render(
<StrictMode>
+1 -1
View File
@@ -7,7 +7,7 @@ import { createRoot } from 'react-dom/client';
import { App } from './app';
function mountApp() {
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
const root = document.getElementById('app')!;
createRoot(root).render(
<StrictMode>
@@ -216,7 +216,7 @@ export const OnboardingPage = ({
],
};
// oxlint-disable-next-line @typescript-eslint/no-floating-promises
// oxlint-disable-next-line typescript/no-floating-promises
fetch('/api/worker/questionnaire', {
method: 'POST',
body: JSON.stringify(answer),
@@ -12,7 +12,6 @@ import {
useState,
} from 'react';
import { createPortal } from 'react-dom';
const UniReactNode = memo(
function UniReactNode(props: {
ele: HTMLElement;
@@ -45,7 +45,7 @@ const HeaderLayout = memo(function HeaderLayout({
const isRight = index === length - 1;
return (
<div
// eslint-disable-next-line react/no-array-index-key
// oxlint-disable-next-line react/no-array-index-key
key={index}
data-length={length}
data-is-left={isLeft}
@@ -136,7 +136,7 @@ export const YearPicker = memo(function YearPicker(
<div className={styles.decadeViewBody}>
{matrix.map((row, i) => {
return (
// eslint-disable-next-line react/no-array-index-key
// oxlint-disable-next-line react/no-array-index-key
<div key={i} className={styles.decadeViewRow}>
{row.map(year => {
const isDisabled =
@@ -48,10 +48,12 @@ export type toExternalData<D extends DNDData> = (
args: DraggableGetFeedbackArgs,
data?: DraggableGet<D['draggable']>
) => {
[Key in
| 'text/uri-list'
| 'text/plain'
| 'text/html'
| 'Files'
| (string & {})]?: string;
[
Key in
| 'text/uri-list'
| 'text/plain'
| 'text/html'
| 'Files'
| (string & {})
]?: string;
};
@@ -1,4 +1,3 @@
/* eslint-disable rxjs/finnish */
import { computed, signal } from '@preact/signals-core';
import { describe, expect, test, vi } from 'vitest';
@@ -1,4 +1,4 @@
// eslint-disable-next-line import-x/no-extraneous-dependencies
// oxlint-disable-next-line import-x-js/no-extraneous-dependencies
import { PanTool } from '@blocksuite/affine-gfx-pointer';
import { on } from '@blocksuite/affine-shared/utils';
import type { PointerEventState } from '@blocksuite/std';
@@ -51,7 +51,7 @@ export const imageProcessingTypes = [
] as const;
declare global {
// oxlint-disable-next-line @typescript-eslint/no-namespace
// oxlint-disable-next-line typescript/no-namespace
namespace BlockSuitePresets {
type TrackerControl =
| 'format-bar'
@@ -59,9 +59,11 @@ export class AIChatBlockMessage extends LitElement {
<div class="ai-chat-content">
<chat-images .attachments=${attachments}></chat-images>
<div class=${messageClasses}>
${streamObjects?.length
? this.renderStreamObjects(streamObjects)
: this.renderRichText(content)}
${
streamObjects?.length
? this.renderStreamObjects(streamObjects)
: this.renderRichText(content)
}
</div>
</div>
</div>
@@ -75,8 +77,9 @@ export class AIChatBlockMessage extends LitElement {
.host=${this.host}
.state=${this.state}
.extensions=${this.textRendererOptions.extensions}
.affineFeatureFlagService=${this.textRendererOptions
.affineFeatureFlagService}
.affineFeatureFlagService=${
this.textRendererOptions.affineFeatureFlagService
}
.notificationService=${notificationService}
.independentMode=${false}
.theme=${this.host.std.get(ThemeProvider).app$}
@@ -88,8 +91,9 @@ export class AIChatBlockMessage extends LitElement {
.text=${text}
.state=${this.state}
.extensions=${this.textRendererOptions.extensions}
.affineFeatureFlagService=${this.textRendererOptions
.affineFeatureFlagService}
.affineFeatureFlagService=${
this.textRendererOptions.affineFeatureFlagService
}
.theme=${this.host.std.get(ThemeProvider).app$}
></chat-content-rich-text>`;
}
@@ -65,14 +65,16 @@ export class UserInfo extends LitElement {
override render() {
return html`<div class="user-info-container">
<div class="user-avatar-container">
${this.avatarIcon
? this.avatarIcon
: this.avatarUrl && !this.avatarLoadedFailed
? html`<img
.src=${this.avatarUrl}
@error=${this._handleAvatarLoadError}
/>`
: html`<span class="default-avatar"></span>`}
${
this.avatarIcon
? this.avatarIcon
: this.avatarUrl && !this.avatarLoadedFailed
? html`<img
.src=${this.avatarUrl}
@error=${this._handleAvatarLoadError}
/>`
: html`<span class="default-avatar"></span>`
}
</div>
<span class="user-name">${this.userName}</span>
</div>`;
@@ -152,35 +152,43 @@ export class ActionWrapper extends WithDisposable(LitElement) {
<div>${this.promptShow ? ArrowDownIcon() : ArrowUpIcon()}</div>
</div>
</div>
${this.promptShow
? html`
<div class="answer-prompt" data-testid="answer-prompt">
<div class="subtitle">Answer</div>
${HISTORY_IMAGE_ACTIONS.includes(item.action)
? images &&
html`<chat-content-images
.images=${images}
data-testid="generated-image"
></chat-content-images>`
: nothing}
${answer
? createTextRenderer({
customHeading: true,
testId: 'chat-message-action-answer',
theme: this.host.std.get(ThemeProvider).app$,
})(answer)
: nothing}
${originalText
? html`<div class="subtitle prompt">Prompt</div>
${createTextRenderer({
customHeading: true,
testId: 'chat-message-action-prompt',
theme: this.host.std.get(ThemeProvider).app$,
})(item.messages[0].content + originalText)}`
: nothing}
</div>
`
: nothing} `;
${
this.promptShow
? html`
<div class="answer-prompt" data-testid="answer-prompt">
<div class="subtitle">Answer</div>
${
HISTORY_IMAGE_ACTIONS.includes(item.action)
? images &&
html`<chat-content-images
.images=${images}
data-testid="generated-image"
></chat-content-images>`
: nothing
}
${
answer
? createTextRenderer({
customHeading: true,
testId: 'chat-message-action-answer',
theme: this.host.std.get(ThemeProvider).app$,
})(answer)
: nothing
}
${
originalText
? html`<div class="subtitle prompt">Prompt</div>
${createTextRenderer({
customHeading: true,
testId: 'chat-message-action-prompt',
theme: this.host.std.get(ThemeProvider).app$,
})(item.messages[0].content + originalText)}`
: nothing
}
</div>
`
: nothing
} `;
}
}
@@ -23,12 +23,14 @@ export class ActionImageToText extends WithDisposable(ShadowlessElement) {
marginBottom: '12px',
})}
>
${answer
? html`<chat-content-images
data-testid="original-images"
.images=${answer}
></chat-content-images>`
: nothing}
${
answer
? html`<chat-content-images
data-testid="original-images"
.images=${answer}
></chat-content-images>`
: nothing
}
</div>
</action-wrapper>`;
}
@@ -23,12 +23,14 @@ export class ActionImage extends WithDisposable(ShadowlessElement) {
return html`<action-wrapper .host=${this.host} .item=${this.item}>
<div style=${styleMap({ marginBottom: '12px' })}>
${images
? html`<chat-content-images
.images=${images}
data-testid="original-image"
></chat-content-images>`
: nothing}
${
images
? html`<chat-content-images
.images=${images}
data-testid="original-image"
></chat-content-images>`
: nothing
}
</div>
</action-wrapper>`;
}
@@ -61,8 +61,10 @@ export class ChatMessageAction extends WithDisposable(ShadowlessElement) {
return html`<action-text
.item=${item}
.host=${host}
.isCode=${item.action === 'Explain this code' ||
item.action === 'Check code error'}
.isCode=${
item.action === 'Explain this code' ||
item.action === 'Check code error'
}
></action-text>`;
}
}
@@ -111,9 +111,11 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
return html`<div class="user-info">
<chat-assistant-avatar .status=${this.status}></chat-assistant-avatar>
${isWithDocs
? html`<span class="message-info">with your docs</span>`
: nothing}
${
isWithDocs
? html`<span class="message-info">with your docs</span>`
: nothing
}
</div>`;
}
@@ -124,12 +126,16 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
return html`
${this.renderImages()}
${streamObjects?.length
? this.renderStreamObjects(streamObjects)
: this.renderRichText(content)}
${shouldRenderError
? AIChatErrorRenderer(error, host, () => this.retry())
: nothing}
${
streamObjects?.length
? this.renderStreamObjects(streamObjects)
: this.renderRichText(content)
}
${
shouldRenderError
? AIChatErrorRenderer(error, host, () => this.retry())
: nothing
}
${this.renderEditorActions()}
`;
}
@@ -209,17 +215,19 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
.retry=${() => this.retry()}
.notificationService=${this.notificationService}
></chat-copy-more>
${isLast && showActions
? html`<chat-action-list
.actions=${actions}
.host=${host}
.session=${session}
.content=${markdown}
.messageId=${messageId ?? undefined}
.withMargin=${true}
.notificationService=${this.notificationService}
></chat-action-list>`
: nothing}
${
isLast && showActions
? html`<chat-action-list
.actions=${actions}
.host=${host}
.session=${session}
.content=${markdown}
.messageId=${messageId ?? undefined}
.withMargin=${true}
.notificationService=${this.notificationService}
></chat-action-list>`
: nothing
}
`;
}
@@ -61,12 +61,14 @@ export class ChatMessageUser extends WithDisposable(ShadowlessElement) {
});
return html`
${item.attachments
? html`<chat-content-images
class="chat-content-images"
.images=${item.attachments}
></chat-content-images>`
: nothing}
${
item.attachments
? html`<chat-content-images
class="chat-content-images"
.images=${item.attachments}
></chat-content-images>`
: nothing
}
<div
class="text-content-wrapper"
data-test-id="chat-content-user-text"
@@ -74,15 +76,17 @@ export class ChatMessageUser extends WithDisposable(ShadowlessElement) {
>
<chat-content-pure-text .text=${item.content}></chat-content-pure-text>
</div>
${showReceipt
? html`<div class="scope-receipt" data-testid="chat-scope-receipt">
${selectorNames?.join(', ')} ·
${I18n['com.affine.ai.chat-panel.scope.sources']({
count: String(resolvedCount),
})}
· ${new Date(receipt.resolvedAt).toLocaleString()}
</div>`
: nothing}
${
showReceipt
? html`<div class="scope-receipt" data-testid="chat-scope-receipt">
${selectorNames?.join(', ')} ·
${I18n['com.affine.ai.chat-panel.scope.sources']({
count: String(resolvedCount),
})}
· ${new Date(receipt.resolvedAt).toLocaleString()}
</div>`
: nothing
}
`;
}
@@ -64,13 +64,15 @@ export class AIChatAddContext extends SignalWatcher(
@click=${this.toggleAddDocMenu}
>
${PlusIcon()}
${disabled
? html`<affine-tooltip>
${I18n[
'com.affine.ai.chat-panel.local-workspace-context-unavailable'
]()}
</affine-tooltip>`
: null}
${
disabled
? html`<affine-tooltip>
${I18n[
'com.affine.ai.chat-panel.local-workspace-context-unavailable'
]()}
</affine-tooltip>`
: null
}
</div>
`;
}
@@ -351,12 +351,16 @@ export class ChatPanelAddPopover extends SignalWatcher(
const items = resolveSignal(group.items);
const menuGroup = html`<div class="menu-group">
${items.length > 0
? this._renderMenuItems(items, startIndex)
: (group.noResult?.() ?? this._renderNoResult())}
${idx < groups.length - 1
? (group.divider?.() ?? this._renderDivider())
: ''}
${
items.length > 0
? this._renderMenuItems(items, startIndex)
: (group.noResult?.() ?? this._renderNoResult())
}
${
idx < groups.length - 1
? (group.divider?.() ?? this._renderDivider())
: ''
}
</div>`;
startIndex += items.length;
return menuGroup;
@@ -194,19 +194,23 @@ export class ChatPanelChips extends SignalWatcher(
return null;
}
)}
${moreCandidates && !isCollapsed
? html`<div
class="more-candidate-button"
@click=${this._toggleMoreCandidatesMenu}
>
${MoreVerticalIcon()}
</div>`
: nothing}
${isCollapsed
? html`<div class="collapse-button" @click=${this.toggleCollapse}>
+${allChips.length - 1}
</div>`
: nothing}
${
moreCandidates && !isCollapsed
? html`<div
class="more-candidate-button"
@click=${this._toggleMoreCandidatesMenu}
>
${MoreVerticalIcon()}
</div>`
: nothing
}
${
isCollapsed
? html`<div class="collapse-button" @click=${this.toggleCollapse}>
+${allChips.length - 1}
</div>`
: nothing
}
</div>`;
}
@@ -114,15 +114,17 @@ export class ChatPanelChip extends SignalWatcher(
</span>
<affine-tooltip>${this.tooltip}</affine-tooltip>
</div>
${isCandidate
? html`${PlusIcon()}`
: this.closeable
? html`
<div class="chip-card-close" @click=${this.onChipDelete}>
${CloseIcon()}
</div>
`
: ''}
${
isCandidate
? html`${PlusIcon()}`
: this.closeable
? html`
<div class="chip-card-close" @click=${this.onChipDelete}>
${CloseIcon()}
</div>
`
: ''
}
</div>
`;
}
@@ -242,9 +242,13 @@ export class ChatPanelSplitView extends SignalWatcher(
<div class="ai-chat-panel-split-view-divider">
<div class="ai-chat-panel-split-view-divider-handle"></div>
</div>
${this.open || this.isTransitioning
? html` <div class="ai-chat-panel-split-view-right">${this.right}</div>`
: nothing}
${
this.open || this.isTransitioning
? html` <div class="ai-chat-panel-split-view-right">
${this.right}
</div>`
: nothing
}
</div>`;
}
}
@@ -574,37 +574,45 @@ export class AIChatInput extends SignalWatcher(
@dragleave=${this._handleDragLeave}
@drop=${this._handleDrop}
>
${this.isDragOver
? html`<div class="chat-panel-input-drop-overlay">Drop to attach</div>`
: nothing}
${hasImages
? html`
<image-preview-grid
.images=${images}
.onImageRemove=${this._handleImageRemove}
></image-preview-grid>
`
: nothing}
${this.chatContextValue.quote
? html`<div
class="chat-selection-quote"
data-testid="chat-selection-quote"
>
${repeat(
getFirstTwoLines(this.chatContextValue.quote),
line => line,
line => html`<div>${line}</div>`
)}
<div
class="chat-quote-close"
@click=${() => {
this.updateContext({ quote: '', markdown: '' });
}}
${
this.isDragOver
? html`<div class="chat-panel-input-drop-overlay">
Drop to attach
</div>`
: nothing
}
${
hasImages
? html`
<image-preview-grid
.images=${images}
.onImageRemove=${this._handleImageRemove}
></image-preview-grid>
`
: nothing
}
${
this.chatContextValue.quote
? html`<div
class="chat-selection-quote"
data-testid="chat-selection-quote"
>
${CloseIcon()}
</div>
</div>`
: nothing}
${repeat(
getFirstTwoLines(this.chatContextValue.quote),
line => line,
line => html`<div>${line}</div>`
)}
<div
class="chat-quote-close"
@click=${() => {
this.updateContext({ quote: '', markdown: '' });
}}
>
${CloseIcon()}
</div>
</div>`
: nothing
}
<textarea
rows="1"
placeholder="What are your thoughts?"
@@ -646,22 +654,24 @@ export class AIChatInput extends SignalWatcher(
.subscriptionService=${this.subscriptionService}
.onAISubscribe=${this.onAISubscribe}
></chat-input-preference>
${status === 'transmitting' || status === 'loading'
? html`<button
class="chat-panel-stop"
@click=${this._handleAbort}
data-testid="chat-panel-stop"
>
${ChatAbortIcon}
</button>`
: html`<button
@click="${this._onTextareaSend}"
class="chat-panel-send"
aria-disabled=${this.isSendDisabled}
data-testid="chat-panel-send"
>
${ArrowUpBigIcon()}
</button>`}
${
status === 'transmitting' || status === 'loading'
? html`<button
class="chat-panel-stop"
@click=${this._handleAbort}
data-testid="chat-panel-stop"
>
${ChatAbortIcon}
</button>`
: html`<button
@click="${this._onTextareaSend}"
class="chat-panel-send"
aria-disabled=${this.isSendDisabled}
data-testid="chat-panel-send"
>
${ArrowUpBigIcon()}
</button>`
}
</div>
</div>`;
}
@@ -139,9 +139,9 @@ export class ChatInputPreference extends SignalWatcher(
name: 'Auto',
prefix: html`
<div class="ai-model-prefix">
${this.aiModelService.modelId.value
? undefined
: DoneIcon()}
${
this.aiModelService.modelId.value ? undefined : DoneIcon()
}
</div>
`,
select: () => this.aiModelService.resetModel(),
@@ -154,9 +154,11 @@ export class ChatInputPreference extends SignalWatcher(
`,
prefix: html`
<div class="ai-model-prefix">
${model.id === this.aiModelService.modelId.value
? DoneIcon()
: undefined}
${
model.id === this.aiModelService.modelId.value
? DoneIcon()
: undefined
}
</div>
`,
postfix: html`
@@ -337,77 +337,83 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
})}
data-testid="chat-panel-messages-container"
>
${filteredItems.length === 0
? html`<div
class="messages-placeholder"
data-testid="chat-panel-messages-placeholder"
>
${AffineIcon(
isHistoryLoading
? 'var(--affine-icon-secondary)'
: 'var(--affine-primary-color)'
)}
<div
class="messages-placeholder-title"
data-loading=${isHistoryLoading}
${
filteredItems.length === 0
? html`<div
class="messages-placeholder"
data-testid="chat-panel-messages-placeholder"
>
${this.isHistoryLoading
? html`<span data-testid="chat-panel-loading-state"
>AFFiNE AI is loading history...</span
>`
: html`<span data-testid="chat-panel-empty-state"
>What can I help you with?</span
>`}
</div>
${this.independentMode ? nothing : this._renderAIOnboarding()}
</div> `
: repeat(
filteredItems,
(item, index) => this._getMessageKey(item, index),
(item, index) => {
const isLast = index === filteredItems.length - 1;
if (isChatMessage(item) && item.role === 'user') {
return html`<chat-message-user
.item=${item}
></chat-message-user>`;
} else if (isChatMessage(item) && item.role === 'assistant') {
return html`<chat-message-assistant
.host=${this.host}
.session=${this.session}
.item=${item}
.isLast=${isLast}
.status=${isLast ? status : 'idle'}
.error=${isLast ? error : null}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.retry=${() => this.retry()}
.width=${this.width}
.independentMode=${this.independentMode}
.docDisplayService=${this.docDisplayService}
.peekViewService=${this.peekViewService}
.onOpenDoc=${this.onOpenDoc}
></chat-message-assistant>`;
} else if (isChatAction(item) && this.host) {
return html`<chat-message-action
.host=${this.host}
.item=${item}
></chat-message-action>`;
${AffineIcon(
isHistoryLoading
? 'var(--affine-icon-secondary)'
: 'var(--affine-primary-color)'
)}
<div
class="messages-placeholder-title"
data-loading=${isHistoryLoading}
>
${
this.isHistoryLoading
? html`<span data-testid="chat-panel-loading-state"
>AFFiNE AI is loading history...</span
>`
: html`<span data-testid="chat-panel-empty-state"
>What can I help you with?</span
>`
}
</div>
${this.independentMode ? nothing : this._renderAIOnboarding()}
</div> `
: repeat(
filteredItems,
(item, index) => this._getMessageKey(item, index),
(item, index) => {
const isLast = index === filteredItems.length - 1;
if (isChatMessage(item) && item.role === 'user') {
return html`<chat-message-user
.item=${item}
></chat-message-user>`;
} else if (isChatMessage(item) && item.role === 'assistant') {
return html`<chat-message-assistant
.host=${this.host}
.session=${this.session}
.item=${item}
.isLast=${isLast}
.status=${isLast ? status : 'idle'}
.error=${isLast ? error : null}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.retry=${() => this.retry()}
.width=${this.width}
.independentMode=${this.independentMode}
.docDisplayService=${this.docDisplayService}
.peekViewService=${this.peekViewService}
.onOpenDoc=${this.onOpenDoc}
></chat-message-assistant>`;
} else if (isChatAction(item) && this.host) {
return html`<chat-message-action
.host=${this.host}
.item=${item}
></chat-message-action>`;
}
return nothing;
}
return nothing;
}
)}
)
}
</div>
${showDownIndicator && filteredItems.length > 0
? html`<div
data-testid="chat-panel-scroll-down-indicator"
class="down-indicator"
@click=${this._onDownIndicatorClick}
>
${ArrowDownIcon()}
</div>`
: nothing}
${
showDownIndicator && filteredItems.length > 0
? html`<div
data-testid="chat-panel-scroll-down-indicator"
class="down-indicator"
@click=${this._onDownIndicatorClick}
>
${ArrowDownIcon()}
</div>`
: nothing
}
`;
}
@@ -93,16 +93,18 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
const pinned = this.session?.pinned;
return html`
<div class="ai-chat-toolbar">
${this.canCreateNewSession
? html` <div
class="chat-toolbar-icon"
@click=${this.onPlusClick}
data-testid="ai-panel-new-chat"
>
${PlusIcon()}
<affine-tooltip>New Chat</affine-tooltip>
</div>`
: null}
${
this.canCreateNewSession
? html` <div
class="chat-toolbar-icon"
@click=${this.onPlusClick}
data-testid="ai-panel-new-chat"
>
${PlusIcon()}
<affine-tooltip>New Chat</affine-tooltip>
</div>`
: null
}
<div
class="chat-toolbar-icon"
@click=${this.onPinClick}
@@ -301,9 +301,11 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
Click to open this chat
</affine-tooltip>
</div>
${session.docId
? this.renderSessionDoc(session.docId, session.sessionId)
: nothing}
${
session.docId
? this.renderSessionDoc(session.docId, session.sessionId)
: nothing
}
<div
class="ai-session-item-delete"
@click=${(e: MouseEvent) => {
@@ -115,11 +115,13 @@ export class AIItemList extends WithDisposable(LitElement) {
const theme = this.host.std.get(ThemeProvider).app$.value;
return html`${repeat(this.groups, group => {
return html`
${group.name
? html`<div class="group-name">
${group.name.toLocaleUpperCase()}
</div>`
: nothing}
${
group.name
? html`<div class="group-name">
${group.name.toLocaleUpperCase()}
</div>`
: nothing
}
${repeat(
group.items,
item => item.name,
@@ -40,13 +40,15 @@ export class AIItem extends WithDisposable(LitElement) {
>
<span class="item-icon">${item.icon}</span>
<div class="item-name">
${item.name}${item.beta
? html`<div class="item-beta">(Beta)</div>`
: nothing}
${item.name}${
item.beta ? html`<div class="item-beta">(Beta)</div>` : nothing
}
</div>
${item.subItem
? html`<span class="arrow-right-icon">${ArrowRightIcon}</span>`
: html`<span class="enter-icon">${EnterIcon}</span>`}
${
item.subItem
? html`<span class="arrow-right-icon">${ArrowRightIcon}</span>`
: html`<span class="enter-icon">${EnterIcon}</span>`
}
</div>`;
}
@@ -25,9 +25,11 @@ export class AssistantAvatar extends ShadowlessElement {
`;
protected override render() {
return html`${this.status === 'transmitting'
? AIStarIconWithAnimation
: AffineAvatarIcon}
return html`${
this.status === 'transmitting'
? AIStarIconWithAnimation
: AffineAvatarIcon
}
AFFiNE AI`;
}
}
@@ -129,7 +129,7 @@ type StreamGroup =
function groupStreamObjects(answer: StreamObject[]): StreamGroup[] {
const groups: StreamGroup[] = [];
for (let index = 0; index < answer.length; ) {
for (let index = 0; index < answer.length;) {
if (!isToolObject(answer[index])) {
groups.push({ type: 'item', item: answer[index] });
index += 1;
@@ -351,9 +351,11 @@ export class ChatContentStreamObjects extends WithDisposable(
}
if (!result || result.error || result.type === 'error') {
return html`<tool-call-failed
.name=${result
? frontendReadError(result)
: I18n['com.affine.ai.chat-panel.tool.live.failed']()}
.name=${
result
? frontendReadError(result)
: I18n['com.affine.ai.chat-panel.tool.live.failed']()
}
.icon=${ViewIcon()}
></tool-call-failed>`;
}
@@ -392,9 +394,11 @@ export class ChatContentStreamObjects extends WithDisposable(
const result = object(streamObject.result);
if (!result || result.error || result.type === 'error') {
return html`<tool-call-failed
.name=${result
? frontendReadError(result)
: I18n['com.affine.ai.chat-panel.tool.canvas.failed']()}
.name=${
result
? frontendReadError(result)
: I18n['com.affine.ai.chat-panel.tool.canvas.failed']()
}
.icon=${ViewIcon()}
></tool-call-failed>`;
}
@@ -152,9 +152,13 @@ export abstract class ArtifactTool<
</div>
</div>
</div>
${banner
? html`<div class="affine-embed-linked-doc-banner">${banner}</div>`
: nothing}
${
banner
? html`<div class="affine-embed-linked-doc-banner">
${banner}
</div>`
: nothing
}
</div>
</div>
`;
@@ -486,14 +486,18 @@ export class CodeArtifactTool extends ArtifactTool<
const { html: htmlContent } = result as { html: string };
return html`<div class="code-artifact-preview">
${this.mode === 'preview'
? html`<affine-html-preview .html=${htmlContent}></affine-html-preview>`
: html`<code-highlighter
.std=${this.std}
.code=${htmlContent}
.language=${'html'}
.showLineNumbers=${true}
></code-highlighter>`}
${
this.mode === 'preview'
? html`<affine-html-preview
.html=${htmlContent}
></affine-html-preview>`
: html`<code-highlighter
.std=${this.std}
.code=${htmlContent}
.language=${'html'}
.showLineNumbers=${true}
></code-highlighter>`
}
</div>`;
}
@@ -120,17 +120,19 @@ export class DocComposeTool extends ArtifactTool<
return html`<div class="doc-compose-result-preview">
<div class="doc-compose-result-preview-title">${title}</div>
${successResult
? html`<text-renderer
.answer=${successResult.markdown}
.schema=${this.std?.store.schema}
.options=${{
customHeading: true,
extensions: getCustomPageEditorBlockSpecs(),
theme: this.theme,
}}
></text-renderer>`
: html``}
${
successResult
? html`<text-renderer
.answer=${successResult.markdown}
.schema=${this.std?.store.schema}
.options=${{
customHeading: true,
extensions: getCustomPageEditorBlockSpecs(),
theme: this.theme,
}}
></text-renderer>`
: html``
}
</div>`;
}
@@ -355,9 +355,9 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) {
<div class="doc-edit-tool-result-wrapper">
<div class="doc-edit-tool-result-title">${op}</div>
<div
class="doc-edit-tool-result-card ${this.isCollapsed
? 'collapsed'
: ''}"
class="doc-edit-tool-result-card ${
this.isCollapsed ? 'collapsed' : ''
}"
>
<div class="doc-edit-tool-result-card-header">
<div class="doc-edit-tool-result-card-header-title">
@@ -173,50 +173,54 @@ export class SectionEditTool extends WithDisposable(ShadowlessElement) {
${CopyIcon()}
<affine-tooltip>Copy</affine-tooltip>
</div>
${this.independentMode
? nothing
: html`<div
class="edit-button"
@click=${async () => {
if (!this.host) return;
if (this.host.std.store.readonly$.value) {
this.notificationService.notify({
title: 'Cannot insert in read-only mode',
accent: 'error',
onClose: () => {},
});
return;
}
if (isInsidePageEditor(this.host)) {
await PAGE_INSERT.handler(
this.host,
result.content,
this.selection
);
} else {
await EDGELESS_INSERT.handler(
this.host,
result.content,
this.selection
);
}
}}
>
${InsertBleowIcon()}
<affine-tooltip>Insert below</affine-tooltip>
</div>`}
${this.independentMode
? nothing
: html`<div
class="edit-button"
@click=${async () => {
if (!this.host) return;
SAVE_AS_DOC.handler(this.host, result.content);
}}
>
${LinkedPageIcon()}
<affine-tooltip>Create new doc</affine-tooltip>
</div>`}
${
this.independentMode
? nothing
: html`<div
class="edit-button"
@click=${async () => {
if (!this.host) return;
if (this.host.std.store.readonly$.value) {
this.notificationService.notify({
title: 'Cannot insert in read-only mode',
accent: 'error',
onClose: () => {},
});
return;
}
if (isInsidePageEditor(this.host)) {
await PAGE_INSERT.handler(
this.host,
result.content,
this.selection
);
} else {
await EDGELESS_INSERT.handler(
this.host,
result.content,
this.selection
);
}
}}
>
${InsertBleowIcon()}
<affine-tooltip>Insert below</affine-tooltip>
</div>`
}
${
this.independentMode
? nothing
: html`<div
class="edit-button"
@click=${async () => {
if (!this.host) return;
SAVE_AS_DOC.handler(this.host, result.content);
}}
>
${LinkedPageIcon()}
<affine-tooltip>Create new doc</affine-tooltip>
</div>`
}
</div>
</div>
<chat-content-rich-text
@@ -260,9 +260,11 @@ export class ToolResultCard extends SignalWatcher(
<div class="ai-tool-header" @click=${this.toggleCard}>
<div class="ai-icon">${this.icon}</div>
<div class="ai-tool-name">${this.name}</div>
${this.isCollapsed
? this.renderFooterIcons()
: html` <div class="ai-icon">${ToggleDownIcon()}</div> `}
${
this.isCollapsed
? this.renderFooterIcons()
: html` <div class="ai-icon">${ToggleDownIcon()}</div> `
}
</div>
<div class="ai-tool-results" data-collapsed=${this.isCollapsed}>
<div class="ai-tool-result-collapse-wrapper">
@@ -285,11 +287,13 @@ export class ToolResultCard extends SignalWatcher(
${this.renderIcon(result.icon)}
</div>
</div>
${result.content
? html`<div class="result-content">
${result.content}
</div>`
: nothing}
${
result.content
? html`<div class="result-content">
${result.content}
</div>`
: nothing
}
</a>
`
)}
@@ -178,74 +178,82 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
}
</style>
<div class="copy-more">
${content
? html`<div
class="button copy"
@click=${async () => {
const success = await copyText(content);
if (success) {
this._notifySuccess('Copied to clipboard');
}
}}
data-testid="action-copy-button"
>
${CopyIcon({ width: '20px', height: '20px' })}
<affine-tooltip>Copy</affine-tooltip>
</div>`
: nothing}
${isLast
? html`<div
class="button retry"
@click=${() => this.retry()}
data-testid="action-retry-button"
>
${ResetIcon({ width: '20px', height: '20px' })}
<affine-tooltip .autoShift=${true}>Retry</affine-tooltip>
</div>`
: nothing}
${showMoreIcon && host
? html`<div
class="button more"
data-testid="action-more-button"
@click=${this._toggle}
>
${MoreHorizontalIcon({ width: '20px', height: '20px' })}
</div> `
: nothing}
${
content
? html`<div
class="button copy"
@click=${async () => {
const success = await copyText(content);
if (success) {
this._notifySuccess('Copied to clipboard');
}
}}
data-testid="action-copy-button"
>
${CopyIcon({ width: '20px', height: '20px' })}
<affine-tooltip>Copy</affine-tooltip>
</div>`
: nothing
}
${
isLast
? html`<div
class="button retry"
@click=${() => this.retry()}
data-testid="action-retry-button"
>
${ResetIcon({ width: '20px', height: '20px' })}
<affine-tooltip .autoShift=${true}>Retry</affine-tooltip>
</div>`
: nothing
}
${
showMoreIcon && host
? html`<div
class="button more"
data-testid="action-more-button"
@click=${this._toggle}
>
${MoreHorizontalIcon({ width: '20px', height: '20px' })}
</div> `
: nothing
}
</div>
<div class="more-menu">
${this._showMoreMenu && host
? repeat(
actions.filter(action => action.showWhen(host)),
action => action.title,
action => {
const currentSelections = {
text: this._currentTextSelection,
blocks: this._currentBlockSelections,
};
return html`<div
@click=${async () => {
const sessionId = this.session?.sessionId;
const success = await action.handler(
host,
content,
currentSelections,
sessionId,
messageId
);
${
this._showMoreMenu && host
? repeat(
actions.filter(action => action.showWhen(host)),
action => action.title,
action => {
const currentSelections = {
text: this._currentTextSelection,
blocks: this._currentBlockSelections,
};
return html`<div
@click=${async () => {
const sessionId = this.session?.sessionId;
const success = await action.handler(
host,
content,
currentSelections,
sessionId,
messageId
);
if (success) {
this._notifySuccess(action.toast);
}
}}
>
${action.icon}
<div>${action.title}</div>
</div>`;
}
)
: nothing}
if (success) {
this._notifySuccess(action.toast);
}
}}
>
${action.icon}
<div>${action.title}</div>
</div>`;
}
)
: nothing
}
</div>`;
}
}
@@ -331,11 +331,13 @@ export class PlaygroundChat extends SignalWatcher(
return html`<div class="chat-panel-container">
<div class="chat-panel-title">
<div class="chat-panel-title-text">
${isSynchronizing
? html`<span data-testid="chat-panel-embedding-progress"
>Synchronizing sources</span
>`
: 'AFFiNE AI'}
${
isSynchronizing
? html`<span data-testid="chat-panel-embedding-progress"
>Synchronizing sources</span
>`
: 'AFFiNE AI'
}
</div>
<div class="chat-panel-add" @click=${this.addChat}>
${NewPageIcon()}
@@ -321,11 +321,11 @@ export class PlaygroundContent extends SignalWatcher(
}
};
// oxlint-disable-next-line @typescript-eslint/no-misused-promises
// oxlint-disable-next-line typescript/no-misused-promises
button.addEventListener('click', handleSendClick);
this._disposables.add(() => {
// oxlint-disable-next-line @typescript-eslint/no-misused-promises
// oxlint-disable-next-line typescript/no-misused-promises
button.removeEventListener('click', handleSendClick);
});
}
@@ -383,8 +383,9 @@ export class PlaygroundContent extends SignalWatcher(
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.aiModelService=${this.aiModelService}
.affineWorkspaceDialogService=${this
.affineWorkspaceDialogService}
.affineWorkspaceDialogService=${
this.affineWorkspaceDialogService
}
.subscriptionService=${this.subscriptionService}
.addChat=${this.addChat}
></playground-chat>
@@ -128,46 +128,56 @@ export class AIErrorWrapper extends SignalWatcher(WithDisposable(LitElement)) {
<div class="icon">${InformationIcon()}</div>
<div class="text-container">
<div>${this.text}</div>
${this.showDetailPanel
? html`<div class="detail-container">
<div
class="detail-title"
@click=${() =>
(this._showDetailContent.value =
!this._showDetailContent.value)}
>
<span>Show detail</span>
<span
class="toggle ${this._showDetailContent.value
? 'down'
: 'up'}"
${
this.showDetailPanel
? html`<div class="detail-container">
<div
class="detail-title"
@click=${() =>
(this._showDetailContent.value =
!this._showDetailContent.value)}
>
${ToggleDownIcon({ width: '16px', height: '16px' })}
</span>
</div>
${this._showDetailContent.value
? html`<div class="detail-content">${this.errorMessage}</div>`
: nothing}
</div>`
: nothing}
<span>Show detail</span>
<span
class="toggle ${
this._showDetailContent.value ? 'down' : 'up'
}"
>
${ToggleDownIcon({ width: '16px', height: '16px' })}
</span>
</div>
${
this._showDetailContent.value
? html`<div class="detail-content">
${this.errorMessage}
</div>`
: nothing
}
</div>`
: nothing
}
</div>
</div>
${this.showAction
? html`<div class="action">
<span
class="action-button"
@click=${this.onClick}
data-testid="ai-error-action-button"
>
${this.actionText}
${this.actionTooltip
? html`<affine-tooltip tip-position="top">
${this.actionTooltip}
</affine-tooltip>`
: nothing}
</span>
</div>`
: nothing}
${
this.showAction
? html`<div class="action">
<span
class="action-button"
@click=${this.onClick}
data-testid="ai-error-action-button"
>
${this.actionText}
${
this.actionTooltip
? html`<affine-tooltip tip-position="top">
${this.actionTooltip}
</affine-tooltip>`
: nothing
}
</span>
</div>`
: nothing
}
</div>`;
}
@@ -41,9 +41,9 @@ export class AIAnswerWrapper extends LitElement {
protected override render() {
return html`<style>
:host {
height: ${this.options?.height
? this.options?.height + 'px'
: '100%'};
height: ${
this.options?.height ? this.options?.height + 'px' : '100%'
};
}
</style>
<slot></slot> `;
@@ -184,23 +184,25 @@ export class MiniMindmapPreview extends WithDisposable(LitElement) {
}).render()}
</div>
${this.templateShow
? html` <div class="select-template-title">Select template</div>
<div class="template">
${repeat(
mindmapStyles,
([style]) => style,
([style, icon]) => {
return html`<div
class=${`template-item ${curStyle === style ? 'active' : ''}`}
@click=${() => this._switchStyle(style as MindmapStyle)}
>
${icon}
</div>`;
}
)}
</div>`
: nothing}
${
this.templateShow
? html` <div class="select-template-title">Select template</div>
<div class="template">
${repeat(
mindmapStyles,
([style]) => style,
([style, icon]) => {
return html`<div
class=${`template-item ${curStyle === style ? 'active' : ''}`}
@click=${() => this._switchStyle(style as MindmapStyle)}
>
${icon}
</div>`;
}
)}
</div>`
: nothing
}
</div>`;
}
@@ -5,7 +5,6 @@ import { Subject } from 'rxjs';
export class MindmapService extends BlockService {
static override readonly flavour = RootBlockSchema.model.flavour;
// eslint-disable-next-line rxjs/finnish
requestCenter = new Subject<void>();
center() {
@@ -1,4 +1,4 @@
/* oxlint-disable @typescript-eslint/no-non-null-assertion */
/* oxlint-disable typescript/no-non-null-assertion */
import {
CanvasRenderer,
type SurfaceBlockModel,
@@ -433,29 +433,33 @@ export class AIChatBlockPeekView extends LitElement {
.textRendererOptions=${this._textRendererOptions}
></ai-chat-block-message>
${shouldRenderError ? AIChatErrorRenderer(error, host) : nothing}
${shouldRenderCopyMore
? html` <chat-copy-more
.host=${host}
.session=${this.forkSession}
.actions=${actions}
.content=${markdown}
.isLast=${isLastReply}
.messageId=${message.id ?? undefined}
.retry=${() => this.retry()}
.notificationService=${notificationService}
></chat-copy-more>`
: nothing}
${shouldRenderActions
? html`<chat-action-list
.host=${host}
.session=${this.forkSession}
.actions=${actions}
.content=${markdown}
.messageId=${message.id ?? undefined}
.layoutDirection=${'horizontal'}
.notificationService=${notificationService}
></chat-action-list>`
: nothing}
${
shouldRenderCopyMore
? html` <chat-copy-more
.host=${host}
.session=${this.forkSession}
.actions=${actions}
.content=${markdown}
.isLast=${isLastReply}
.messageId=${message.id ?? undefined}
.retry=${() => this.retry()}
.notificationService=${notificationService}
></chat-copy-more>`
: nothing
}
${
shouldRenderActions
? html`<chat-action-list
.host=${host}
.session=${this.forkSession}
.actions=${actions}
.content=${markdown}
.messageId=${message.id ?? undefined}
.layoutDirection=${'horizontal'}
.notificationService=${notificationService}
></chat-action-list>`
: nothing
}
</div>`;
}
)}`;
@@ -4,7 +4,6 @@ import { BehaviorSubject, Subject } from 'rxjs';
import type { AIChatParams, AISendParams, AIUserInfo } from './ai-provider';
export const AIAppEvents = {
/* eslint-disable rxjs/finnish */
requestOpenWithChat: new BehaviorSubject<AIChatParams | null>(null),
requestSendWithChat: new BehaviorSubject<AISendParams | null>(null),
requestInsertTemplate: new Subject<{
@@ -15,5 +14,4 @@ export const AIAppEvents = {
requestUpgradePlan: new Subject<{ host?: EditorHost | null }>(),
userInfo: new BehaviorSubject<AIUserInfo | null>(null),
previewPanelOpenChange: new Subject<boolean>(),
/* eslint-enable rxjs/finnish */
};
@@ -1,4 +1,4 @@
/* oxlint-disable @typescript-eslint/no-non-null-assertion */
/* oxlint-disable typescript/no-non-null-assertion */
import { DefaultTool } from '@blocksuite/affine/blocks/surface';
import { IS_MAC } from '@blocksuite/affine/global/env';
import {
@@ -25,7 +25,6 @@ export class CopilotTool extends BaseTool {
private _dragging = false;
// eslint-disable-next-line rxjs/finnish
draggingAreaUpdated = new Subject<boolean | void>();
dragLastPoint: [number, number] = [0, 0];
@@ -529,18 +529,22 @@ export class AffineAIPanelWidget extends WidgetComponent {
[
'generating',
() => html`
${this.answer
? html`
<ai-panel-answer
.finish=${false}
.config=${config.finishStateConfig}
.host=${this.host}
>
${this.answer &&
config.answerRenderer(this.answer, this.state)}
</ai-panel-answer>
`
: nothing}
${
this.answer
? html`
<ai-panel-answer
.finish=${false}
.config=${config.finishStateConfig}
.host=${this.host}
>
${
this.answer &&
config.answerRenderer(this.answer, this.state)
}
</ai-panel-answer>
`
: nothing
}
<ai-panel-generating
.config=${config.generatingStateConfig}
.theme=${theme}
@@ -71,29 +71,33 @@ export class AIFinishTip extends WithDisposable(LitElement) {
return html`<div class="finish-tip">
${WarningIcon}
<div class="text">AI outputs can be misleading or wrong</div>
${this.copy?.allowed
? html`<div class="right">
${this.copied
? html`<div class="copied" data-testid="answer-copied">
${AIDoneIcon}
</div>`
: html`<div
class="copy"
data-testid="answer-copy-button"
@click=${async () => {
this.copied = !!(await this.copy?.onCopy());
if (this.copied) {
this.host.std
.getOptional(NotificationProvider)
?.toast('Copied to clipboard');
}
}}
>
${CopyIcon}
<affine-tooltip>Copy</affine-tooltip>
</div>`}
</div>`
: nothing}
${
this.copy?.allowed
? html`<div class="right">
${
this.copied
? html`<div class="copied" data-testid="answer-copied">
${AIDoneIcon}
</div>`
: html`<div
class="copy"
data-testid="answer-copy-button"
@click=${async () => {
this.copied = !!(await this.copy?.onCopy());
if (this.copied) {
this.host.std
.getOptional(NotificationProvider)
?.toast('Copied to clipboard');
}
}}
>
${CopyIcon}
<affine-tooltip>Copy</affine-tooltip>
</div>`
}
</div>`
: nothing
}
</div>`;
}
@@ -93,9 +93,11 @@ export class GeneratingPlaceholder extends WithDisposable(LitElement) {
height: ${this.height}px;
}
</style>
${this.showHeader
? html`<div class="generating-header">Answer</div>`
: nothing}
${
this.showHeader
? html`<div class="generating-header">Answer</div>`
: nothing
}
<div class="generating-body">
<div class="generating-icon">${LoadingIcon()}</div>
<div class="loading-progress">
@@ -90,48 +90,58 @@ export class AIPanelAnswer extends WithDisposable(LitElement) {
<slot></slot>
</div>
</div>
${this.finish
? html`
<ai-finish-tip
.copy=${this.copy}
.host=${this.host}
></ai-finish-tip>
${responseGroup.length > 0
? html`
<ai-panel-divider></ai-panel-divider>
${responseGroup.map(
(group, index) => html`
${index !== 0
? html`<ai-panel-divider></ai-panel-divider>`
: nothing}
<div
class="response-list-container"
data-testid=${group.testId}
>
${
this.finish
? html`
<ai-finish-tip
.copy=${this.copy}
.host=${this.host}
></ai-finish-tip>
${
responseGroup.length > 0
? html`
<ai-panel-divider></ai-panel-divider>
${responseGroup.map(
(group, index) => html`
${
index !== 0
? html`<ai-panel-divider></ai-panel-divider>`
: nothing
}
<div
class="response-list-container"
data-testid=${group.testId}
>
<ai-item-list
.host=${this.host}
.groups=${[group]}
></ai-item-list>
</div>
`
)}
`
: nothing
}
${
responseGroup.length > 0 && this.config.actions.length > 0
? html`<ai-panel-divider></ai-panel-divider>`
: nothing
}
${
this.config.actions.length > 0
? html`
<div class="action-list-container">
<ai-item-list
.host=${this.host}
.groups=${[group]}
.groups=${this.config.actions}
></ai-item-list>
</div>
`
)}
`
: nothing}
${responseGroup.length > 0 && this.config.actions.length > 0
? html`<ai-panel-divider></ai-panel-divider>`
: nothing}
${this.config.actions.length > 0
? html`
<div class="action-list-container">
<ai-item-list
.host=${this.host}
.groups=${this.config.actions}
></ai-item-list>
</div>
`
: nothing}
`
: nothing}
: nothing
}
`
: nothing
}
`;
}
@@ -214,30 +214,36 @@ export class AIPanelError extends WithDisposable(LitElement) {
</div>
${errorTemplate}
</div>
${this.withAnswer
? html`<ai-finish-tip
.copy=${this.copy}
.host=${this.host}
></ai-finish-tip>`
: nothing}
${responseGroup.length > 0
? html`
<ai-panel-divider></ai-panel-divider>
${responseGroup.map(
(group, index) => html`
${index !== 0
? html`<ai-panel-divider></ai-panel-divider>`
: nothing}
<div class="response-list-container">
<ai-item-list
.host=${this.host}
.groups=${[group]}
></ai-item-list>
</div>
`
)}
`
: nothing}
${
this.withAnswer
? html`<ai-finish-tip
.copy=${this.copy}
.host=${this.host}
></ai-finish-tip>`
: nothing
}
${
responseGroup.length > 0
? html`
<ai-panel-divider></ai-panel-divider>
${responseGroup.map(
(group, index) => html`
${
index !== 0
? html`<ai-panel-divider></ai-panel-divider>`
: nothing
}
<div class="response-list-container">
<ai-item-list
.host=${this.host}
.groups=${[group]}
></ai-item-list>
</div>
`
)}
`
: nothing
}
`;
}
@@ -76,14 +76,16 @@ export class AIPanelGenerating extends WithDisposable(LitElement) {
height = 300,
} = this.config;
return html`
${stages && stages.length > 0
? html`<generating-placeholder
.height=${height}
.loadingProgress=${this.loadingProgress}
.stages=${stages}
.showHeader=${!this.withAnswer}
></generating-placeholder>`
: nothing}
${
stages && stages.length > 0
? html`<generating-placeholder
.height=${height}
.loadingProgress=${this.loadingProgress}
.stages=${stages}
.showHeader=${!this.withAnswer}
></generating-placeholder>`
: nothing
}
<div class="generating-tip" data-testid="ai-generating">
<div class="left">${generatingIcon}</div>
<div class="text">AI is generating...</div>
@@ -239,9 +239,11 @@ export class AIPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
@pointerdown=${stopPropagation}
>
${SendIcon()}
${this._hasContent
? html`<affine-tooltip .offsetY=${12}>Send to AI</affine-tooltip>`
: nothing}
${
this._hasContent
? html`<affine-tooltip .offsetY=${12}>Send to AI</affine-tooltip>`
: nothing
}
</div>
</div>
</div>`;
@@ -280,7 +280,7 @@ const BlockSuiteEditorImpl = ({
export const BlockSuiteEditor = (props: EditorProps) => {
const [isLoading, setIsLoading] = useState(true);
const [longerLoading, setLongerLoading] = useState(false);
// eslint-disable-next-line react-hooks/purity
// oxlint-disable-next-line react-hooks-js/purity
const [loadStartTime] = useState(Date.now());
const workspaceService = useService(WorkspaceService);
@@ -362,7 +362,7 @@ const FileCellComponent: ForwardRefRenderFunction<
> = (props, ref): ReactNode => {
const peekView = useService(PeekViewService);
const manager = useMemo(
() => new FileCellManager(props, peekView), // eslint-disable-line react-hooks/preserve-manual-memoization
() => new FileCellManager(props, peekView), // oxlint-disable-line react-hooks-js/preserve-manual-memoization
[] // oxlint-disable-line react/exhaustive-deps
);
@@ -261,7 +261,7 @@ export const MultiMemberSelect: React.FC<MemberManagerOptions> = props => {
const inputRef = useRef<HTMLInputElement>(null);
const memberListRef = useRef<HTMLDivElement>(null);
const memberManager = useMemo(
() => new MemberManager(props), // eslint-disable-line react-hooks/preserve-manual-memoization
() => new MemberManager(props), // oxlint-disable-line react-hooks-js/preserve-manual-memoization
[] // oxlint-disable-line react/exhaustive-deps
);
@@ -70,7 +70,7 @@ const MemberCellComponent: ForwardRefRenderFunction<
CellRenderProps<{}, MemberCellRawValueType, MemberCellJsonValueType>
> = (props, ref): ReactNode => {
const manager = useMemo(
() => new MemberManager(props), // eslint-disable-line react-hooks/preserve-manual-memoization
() => new MemberManager(props), // oxlint-disable-line react-hooks-js/preserve-manual-memoization
[] // oxlint-disable-line react/exhaustive-deps
);
@@ -3,19 +3,15 @@ import { UserListServiceExtension } from '@blocksuite/affine/shared/services';
export function patchUserListExtensions(memberSearch: MemberSearchService) {
return UserListServiceExtension({
// eslint-disable-next-line rxjs/finnish
hasMore$: memberSearch.hasMore$.signal,
loadMore() {
memberSearch.loadMore();
},
// eslint-disable-next-line rxjs/finnish
isLoading$: memberSearch.isLoading$.signal,
// eslint-disable-next-line rxjs/finnish
searchText$: memberSearch.searchText$.signal,
search(keyword) {
memberSearch.search(keyword);
},
// eslint-disable-next-line rxjs/finnish
users$: memberSearch.result$.map(users =>
users.map(u => ({
id: u.id,
@@ -13,7 +13,6 @@ export function patchUserExtensions(
authService: AuthService
) {
return UserServiceExtension({
// eslint-disable-next-line rxjs/finnish
currentUserInfo$: authService.session.account$.map(account => {
if (!account) {
return null;
@@ -25,15 +24,12 @@ export function patchUserExtensions(
removed: false,
} as AffineUserInfo;
}).signal,
// eslint-disable-next-line rxjs/finnish
userInfo$(id) {
return publicUserService.publicUser$(id).signal;
},
// eslint-disable-next-line rxjs/finnish
isLoading$(id) {
return publicUserService.isLoading$(id).signal;
},
// eslint-disable-next-line rxjs/finnish
error$(id) {
return publicUserService.error$(id).selector(error => {
if (error) {
@@ -479,46 +479,50 @@ export class MermaidPreview extends SignalWatcher(
display: this.state === 'finish' ? undefined : 'none',
})}
>
${this.state === 'finish'
? html`
<div
class="mermaid-preview-svg"
style=${styleMap({
transform: `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`,
})}
>
${this.svgContent
? html`<div .innerHTML=${this.svgContent}></div>`
: nothing}
</div>
<div class="mermaid-controls">
<button
class="mermaid-control-button"
@click=${this._zoomIn}
title="Zoom in"
${
this.state === 'finish'
? html`
<div
class="mermaid-preview-svg"
style=${styleMap({
transform: `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`,
})}
>
+
</button>
<button
class="mermaid-control-button"
@click=${this._zoomOut}
title="Zoom out"
>
</button>
<button
class="mermaid-control-button"
@click=${this._resetTransform}
title="Reset view"
>
</button>
</div>
<div class="mermaid-scale-info">
${Math.round(this.scale * 100)}%
</div>
`
: nothing}
${
this.svgContent
? html`<div .innerHTML=${this.svgContent}></div>`
: nothing
}
</div>
<div class="mermaid-controls">
<button
class="mermaid-control-button"
@click=${this._zoomIn}
title="Zoom in"
>
+
</button>
<button
class="mermaid-control-button"
@click=${this._zoomOut}
title="Zoom out"
>
</button>
<button
class="mermaid-control-button"
@click=${this._resetTransform}
title="Reset view"
>
</button>
</div>
<div class="mermaid-scale-info">
${Math.round(this.scale * 100)}%
</div>
`
: nothing
}
</div>
</div>
`;
@@ -194,8 +194,7 @@ export class TypstPreview extends SignalWatcher(
tabindex="0"
aria-label="Typst error message"
>
${this.errorMessage}</pre
>
${this.errorMessage}</pre>
<div class="typst-copy-row">
<button class="typst-copy-button" @click=${this._copyError}>
${this._copyButtonLabel}
@@ -257,9 +256,11 @@ ${this.errorMessage}</pre
transform: `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`,
})}
>
${this.svgContent
? html`<div .innerHTML=${this.svgContent}></div>`
: nothing}
${
this.svgContent
? html`<div .innerHTML=${this.svgContent}></div>`
: nothing
}
</div>
`
: nothing;
@@ -18,7 +18,6 @@ export function getEditorConfigExtension(
return [
EditorSettingExtension({
// eslint-disable-next-line rxjs/finnish
setting$: editorSettingService.editorSetting.settingSignal,
set: (k, v) => editorSettingService.editorSetting.set(k, v),
}),
@@ -24,16 +24,12 @@ export function KeyboardToolbarExtension(
private readonly _disposables = new DisposableGroup();
// eslint-disable-next-line rxjs/finnish
readonly visible$ = signal(false);
// eslint-disable-next-line rxjs/finnish
readonly height$ = signal(0);
// eslint-disable-next-line rxjs/finnish
readonly staticHeight$ = signal(0);
// eslint-disable-next-line rxjs/finnish
readonly appTabSafeArea$ = signal(`calc(${globalVars.appTabSafeArea})`);
static override setup(di: Container) {

Some files were not shown because too many files have changed in this diff Show More