Compare commits

...

11 Commits

Author SHA1 Message Date
pengx17
bb606ac3e5 fix(core): audio job submittion timeout too short (#11918)
fix AF-2556
2025-04-24 17:07:49 +08:00
pengx17
851111e1e4 feat(core): add actions to transcription block (#11896) 2025-04-24 17:05:55 +08:00
pengx17
9982e0ea45 fix(core): sidebar audio player seek position issue (#11844)
fix AF-2541
2025-04-24 17:05:44 +08:00
pengx17
58f7a6166c fix(electron): use askForMeetingPermission for asking microphone permission (#11792) 2025-04-24 17:05:33 +08:00
pengx17
07d7a62071 fix(electron): sometimes pops up failed to save dialog (#11925)
fix AF-2557
2025-04-24 17:05:08 +08:00
Peng Xiao
ab3f056927 fix(core): remove image proxy in onboarding snapshots (#11954) 2025-04-24 17:04:32 +08:00
pengx17
61e3364717 fix(core): should not limit the number of docs of at menu (#11889)
fix AF-2544
2025-04-24 17:03:52 +08:00
yoyoyohamapi
e32d6b9347 fix(core): action items in the ai response are not optimized for dark mode (#11839)
### TL;DR

* Fix action items in the AI response are not optimized for dark mode.
* Fix answer content in the AI response are not optimized for edgeless theme.

![截屏2025-04-21 14.26.41.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/MyktQ6Qwc7H6TiRCFoYN/9c991df4-36b1-4969-ac0d-3c582edb1120.png)

[uploading 截屏2025-04-21 14.30.00.png...]

> CLOSE BS-3249
2025-04-24 16:48:09 +08:00
EYHN
075a2e9f99 fix(nbstore): fix indexer cache not working (#11922) 2025-04-24 16:43:04 +08:00
EYHN
8b486b4833 fix(core): subscribe search not unsubscribe (#11929) 2025-04-24 16:42:53 +08:00
darkskygit
21b7f02b0f fix(server): empty mimetype attachments fallback (#11869) 2025-04-23 15:57:12 +08:00
29 changed files with 309 additions and 116 deletions

View File

@@ -249,6 +249,7 @@ export class LinkedDocPopover extends SignalWatcher(
override disconnectedCallback() { override disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
this._menusItemsEffectCleanup(); this._menusItemsEffectCleanup();
this._updateLinkedDocGroupAbortController?.abort();
} }
override render() { override render() {

View File

@@ -385,6 +385,45 @@ test('should create message correctly', async t => {
t.truthy(messageId, 'should be able to create message with valid session'); t.truthy(messageId, 'should be able to create message with valid session');
} }
{
// with attachment url
{
const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession(
app,
id,
randomUUID(),
promptName
);
const messageId = await createCopilotMessage(app, sessionId, undefined, [
'http://example.com/cat.jpg',
]);
t.truthy(messageId, 'should be able to create message with url link');
}
// with attachment
{
const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession(
app,
id,
randomUUID(),
promptName
);
const smallestPng =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
const pngData = await fetch(smallestPng).then(res => res.arrayBuffer());
const messageId = await createCopilotMessage(
app,
sessionId,
undefined,
undefined,
[new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' })]
);
t.truthy(messageId, 'should be able to create message with blobs');
}
}
{ {
await t.throwsAsync( await t.throwsAsync(
createCopilotMessage(app, randomUUID()), createCopilotMessage(app, randomUUID()),

View File

@@ -490,19 +490,53 @@ export async function createCopilotMessage(
sessionId: string, sessionId: string,
content?: string, content?: string,
attachments?: string[], attachments?: string[],
blobs?: ArrayBuffer[], blobs?: File[],
params?: Record<string, string> params?: Record<string, string>
): Promise<string> { ): Promise<string> {
const res = await app.gql( let resp = app
` .POST('/graphql')
mutation createCopilotMessage($options: CreateChatMessageInput!) { .set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
createCopilotMessage(options: $options) .field(
'operations',
JSON.stringify({
query: `
mutation createCopilotMessage($options: CreateChatMessageInput!) {
createCopilotMessage(options: $options)
}
`,
variables: {
options: { sessionId, content, attachments, blobs: [], params },
},
})
)
.field(
'map',
JSON.stringify(
Array.from<any>({ length: blobs?.length ?? 0 }).reduce(
(acc, _, idx) => {
acc[idx.toString()] = [`variables.options.blobs.${idx}`];
return acc;
},
{}
)
)
);
if (blobs && blobs.length) {
for (const [idx, file] of blobs.entries()) {
resp = resp.attach(
idx.toString(),
Buffer.from(await file.arrayBuffer()),
{
filename: file.name || `file${idx}`,
contentType: file.type || 'application/octet-stream',
}
);
} }
`, }
{ options: { sessionId, content, attachments, blobs, params } }
);
return res.createCopilotMessage; const res = await resp.expect(200);
return res.body.data.createCopilotMessage;
} }
export async function chatWithText( export async function chatWithText(

View File

@@ -138,7 +138,15 @@ export class FalProvider
); );
return { return {
model_name: options.modelName || undefined, model_name: options.modelName || undefined,
image_url: attachments?.[0], image_url: attachments
?.map(v =>
typeof v === 'string'
? v
: v.mimeType.startsWith('image/')
? v.attachment
: undefined
)
.filter(v => !!v)[0],
prompt: content.trim(), prompt: content.trim(),
loras: lora.length ? lora : undefined, loras: lora.length ? lora : undefined,
controlnets: controlnets.length ? controlnets : undefined, controlnets: controlnets.length ? controlnets : undefined,

View File

@@ -51,7 +51,15 @@ export const ChatMessageRole = Object.values(AiPromptRole) as [
export const PureMessageSchema = z.object({ export const PureMessageSchema = z.object({
content: z.string(), content: z.string(),
attachments: z.array(z.string()).optional().nullable(), attachments: z
.array(
z.union([
z.string(),
z.object({ attachment: z.string(), mimeType: z.string() }),
])
)
.optional()
.nullable(),
params: z.record(z.any()).optional().nullable(), params: z.record(z.any()).optional().nullable(),
}); });

View File

@@ -35,15 +35,26 @@ const FORMAT_INFER_MAP: Record<string, string> = {
flv: 'video/flv', flv: 'video/flv',
}; };
function inferMimeType(url: string) { async function inferMimeType(url: string) {
if (url.startsWith('data:')) { if (url.startsWith('data:')) {
return url.split(';')[0].split(':')[1]; return url.split(';')[0].split(':')[1];
} }
const extension = url.split('.').pop(); const pathname = new URL(url).pathname;
const extension = pathname.split('.').pop();
if (extension) { if (extension) {
return FORMAT_INFER_MAP[extension]; const ext = FORMAT_INFER_MAP[extension];
if (ext) {
return ext;
}
const mimeType = await fetch(url, {
method: 'HEAD',
redirect: 'follow',
}).then(res => res.headers.get('Content-Type'));
if (mimeType) {
return mimeType;
}
} }
return undefined; return 'application/octet-stream';
} }
export async function chatToGPTMessage( export async function chatToGPTMessage(
@@ -66,19 +77,24 @@ export async function chatToGPTMessage(
contents.push({ type: 'text', text: content }); contents.push({ type: 'text', text: content });
} }
for (const url of attachments) { for (let attachment of attachments) {
if (SIMPLE_IMAGE_URL_REGEX.test(url)) { let mimeType: string;
const mimeType = if (typeof attachment === 'string') {
typeof mimetype === 'string' ? mimetype : inferMimeType(url); mimeType =
if (mimeType) { typeof mimetype === 'string'
if (mimeType.startsWith('image/')) { ? mimetype
contents.push({ type: 'image', image: url, mimeType }); : await inferMimeType(attachment);
} else { } else {
const data = url.startsWith('data:') ({ attachment, mimeType } = attachment);
? await fetch(url).then(r => r.arrayBuffer()) }
: new URL(url); if (SIMPLE_IMAGE_URL_REGEX.test(attachment)) {
contents.push({ type: 'file' as const, data, mimeType }); if (mimeType.startsWith('image/')) {
} contents.push({ type: 'image', image: attachment, mimeType });
} else {
const data = attachment.startsWith('data:')
? await fetch(attachment).then(r => r.arrayBuffer())
: new URL(attachment);
contents.push({ type: 'file' as const, data, mimeType });
} }
} }
} }

View File

@@ -34,6 +34,7 @@ import { Admin } from '../../core/common';
import { AccessController } from '../../core/permission'; import { AccessController } from '../../core/permission';
import { UserType } from '../../core/user'; import { UserType } from '../../core/user';
import { PromptService } from './prompt'; import { PromptService } from './prompt';
import { PromptMessage } from './providers';
import { ChatSessionService } from './session'; import { ChatSessionService } from './session';
import { CopilotStorage } from './storage'; import { CopilotStorage } from './storage';
import { import {
@@ -113,7 +114,7 @@ class CreateChatMessageInput implements Omit<SubmittedMessage, 'content'> {
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
content!: string | undefined; content!: string | undefined;
@Field(() => [String], { nullable: true }) @Field(() => [String], { nullable: true, deprecationReason: 'use blobs' })
attachments!: string[] | undefined; attachments!: string[] | undefined;
@Field(() => [GraphQLUpload], { nullable: true }) @Field(() => [GraphQLUpload], { nullable: true })
@@ -527,8 +528,8 @@ export class CopilotResolver {
throw new BadRequestException('Session not found'); throw new BadRequestException('Session not found');
} }
const attachments: PromptMessage['attachments'] = options.attachments || [];
if (options.blobs) { if (options.blobs) {
options.attachments = options.attachments || [];
const { workspaceId } = session.config; const { workspaceId } = session.config;
const blobs = await Promise.all(options.blobs); const blobs = await Promise.all(options.blobs);
@@ -539,18 +540,18 @@ export class CopilotResolver {
const filename = createHash('sha256') const filename = createHash('sha256')
.update(uploaded.buffer) .update(uploaded.buffer)
.digest('base64url'); .digest('base64url');
const link = await this.storage.put( const attachment = await this.storage.put(
user.id, user.id,
workspaceId, workspaceId,
filename, filename,
uploaded.buffer uploaded.buffer
); );
options.attachments.push(link); attachments.push({ attachment, mimeType: blob.mimetype });
} }
} }
try { try {
return await this.chatSession.createMessage(options); return await this.chatSession.createMessage({ ...options, attachments });
} catch (e: any) { } catch (e: any) {
throw new CopilotFailedToCreateMessage(e.message); throw new CopilotFailedToCreateMessage(e.message);
} }

View File

@@ -166,7 +166,11 @@ export class ChatSession implements AsyncDisposable {
firstMessage.attachments || [], firstMessage.attachments || [],
] ]
.flat() .flat()
.filter(v => !!v?.trim()); .filter(v =>
typeof v === 'string'
? !!v.trim()
: v && v.attachment.trim() && v.mimeType
);
return finished; return finished;
} }
@@ -553,7 +557,12 @@ export class ChatSessionService {
action: prompt.action || null, action: prompt.action || null,
tokens: tokenCost, tokens: tokenCost,
createdAt, createdAt,
messages: preload.concat(ret.data), messages: preload.concat(ret.data).map(m => ({
...m,
attachments: m.attachments
?.map(a => (typeof a === 'string' ? a : a.attachment))
.filter(a => !!a),
})),
}; };
} else { } else {
this.logger.error( this.logger.error(

View File

@@ -265,40 +265,47 @@ export class DataStruct {
if (cached) { if (cached) {
return cached; return cached;
} }
using _ = await this.measure(`query[${query.type}]`);
if (query.type === 'match') { const result = await (async () => {
const iidx = this.invertedIndex.get(table)?.get(query.field as string); using _ = await this.measure(`query[${query.type}]`);
if (!iidx) { if (query.type === 'match') {
return new Match(); const iidx = this.invertedIndex.get(table)?.get(query.field as string);
if (!iidx) {
return new Match();
}
return await iidx.match(trx, query.match);
} else if (query.type === 'boolean') {
const weights = [];
for (const q of query.queries) {
weights.push(await this.queryRaw(trx, table, q, cache));
}
if (query.occur === 'must') {
return weights.reduce((acc, w) => acc.and(w));
} else if (query.occur === 'must_not') {
const total = weights.reduce((acc, w) => acc.and(w));
return (await this.matchAll(trx, table)).exclude(total);
} else if (query.occur === 'should') {
return weights.reduce((acc, w) => acc.or(w));
}
} else if (query.type === 'all') {
return await this.matchAll(trx, table);
} else if (query.type === 'boost') {
return (await this.queryRaw(trx, table, query.query, cache)).boost(
query.boost
);
} else if (query.type === 'exists') {
const iidx = this.invertedIndex.get(table)?.get(query.field as string);
if (!iidx) {
return new Match();
}
return await iidx.all(trx);
} }
return await iidx.match(trx, query.match); throw new Error(`Query type '${query.type}' not supported`);
} else if (query.type === 'boolean') { })();
const weights = [];
for (const q of query.queries) { cache.set(query, result);
weights.push(await this.queryRaw(trx, table, q, cache));
} return result;
if (query.occur === 'must') {
return weights.reduce((acc, w) => acc.and(w));
} else if (query.occur === 'must_not') {
const total = weights.reduce((acc, w) => acc.and(w));
return (await this.matchAll(trx, table)).exclude(total);
} else if (query.occur === 'should') {
return weights.reduce((acc, w) => acc.or(w));
}
} else if (query.type === 'all') {
return await this.matchAll(trx, table);
} else if (query.type === 'boost') {
return (await this.queryRaw(trx, table, query.query, cache)).boost(
query.boost
);
} else if (query.type === 'exists') {
const iidx = this.invertedIndex.get(table)?.get(query.field as string);
if (!iidx) {
return new Match();
}
return await iidx.all(trx);
}
throw new Error(`Query type '${query.type}' not supported`);
} }
async clear(trx: DataStructRWTransaction) { async clear(trx: DataStructRWTransaction) {

View File

@@ -225,17 +225,26 @@ export class FullTextInvertedIndex implements InvertedIndex {
)?.value ?? 0; )?.value ?? 0;
for (const token of queryTokens) { for (const token of queryTokens) {
const key = InvertedIndexKey.forString(this.fieldKey, token.term); const key = InvertedIndexKey.forString(this.fieldKey, token.term);
const objs = await trx const objs = [
.objectStore('invertedIndex') // match exact
.index('key') await trx
.getAll( .objectStore('invertedIndex')
IDBKeyRange.bound( .index('key')
[this.table, key.buffer()], .get([this.table, key.buffer()]),
[this.table, key.add1().buffer()], // match prefix
false, ...(await trx
true .objectStore('invertedIndex')
) .index('key')
); .getAll(
IDBKeyRange.bound(
[this.table, key.buffer()],
[this.table, key.add1().buffer()],
true,
true
),
5000 // get maximum 5000 items for prefix match
)),
];
const submatched: { const submatched: {
nid: number; nid: number;
score: number; score: number;
@@ -245,6 +254,9 @@ export class FullTextInvertedIndex implements InvertedIndex {
}; };
}[] = []; }[] = [];
for (const obj of objs) { for (const obj of objs) {
if (!obj) {
continue;
}
const key = InvertedIndexKey.fromBuffer(obj.key); const key = InvertedIndexKey.fromBuffer(obj.key);
const originTokenTerm = key.asString(); const originTokenTerm = key.asString();
const matchLength = token.term.length; const matchLength = token.term.length;

View File

@@ -531,6 +531,7 @@ export function startRecording(
// set a timeout to stop the recording after MAX_DURATION_FOR_TRANSCRIPTION // set a timeout to stop the recording after MAX_DURATION_FOR_TRANSCRIPTION
setTimeout(() => { setTimeout(() => {
const state = recordingStateMachine.status$.value;
if ( if (
state?.status === 'recording' && state?.status === 'recording' &&
state.id === recordingStatus$.value?.id state.id === recordingStatus$.value?.id
@@ -780,6 +781,13 @@ export const checkMeetingPermissions = () => {
) as Record<(typeof mediaTypes)[number], boolean>; ) as Record<(typeof mediaTypes)[number], boolean>;
}; };
export const askForMeetingPermission = async (type: 'microphone') => {
if (!isMacOS()) {
return false;
}
return systemPreferences.askForMediaAccess(type);
};
export const checkCanRecordMeeting = () => { export const checkCanRecordMeeting = () => {
const features = checkMeetingPermissions(); const features = checkMeetingPermissions();
return ( return (

View File

@@ -9,6 +9,7 @@ import { shell } from 'electron';
import { isMacOS } from '../../shared/utils'; import { isMacOS } from '../../shared/utils';
import type { NamespaceHandlers } from '../type'; import type { NamespaceHandlers } from '../type';
import { import {
askForMeetingPermission,
checkMeetingPermissions, checkMeetingPermissions,
checkRecordingAvailable, checkRecordingAvailable,
disableRecordingFeature, disableRecordingFeature,
@@ -76,6 +77,9 @@ export const recordingHandlers = {
checkMeetingPermissions: async () => { checkMeetingPermissions: async () => {
return checkMeetingPermissions(); return checkMeetingPermissions();
}, },
askForMeetingPermission: async (_, type: 'microphone') => {
return askForMeetingPermission(type);
},
showRecordingPermissionSetting: async (_, type: 'screen' | 'microphone') => { showRecordingPermissionSetting: async (_, type: 'screen' | 'microphone') => {
const urlMap = { const urlMap = {
screen: 'Privacy_ScreenCapture', screen: 'Privacy_ScreenCapture',

View File

@@ -260,6 +260,8 @@ export class ChatPanelAddPopover extends SignalWatcher(
@query('.search-input') @query('.search-input')
accessor searchInput!: HTMLInputElement; accessor searchInput!: HTMLInputElement;
private _menuGroupAbortController = new AbortController();
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this._updateSearchGroup(); this._updateSearchGroup();
@@ -273,6 +275,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
override disconnectedCallback() { override disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
document.removeEventListener('keydown', this._handleKeyDown); document.removeEventListener('keydown', this._handleKeyDown);
this._menuGroupAbortController.abort();
} }
override render() { override render() {
@@ -385,13 +388,15 @@ export class ChatPanelAddPopover extends SignalWatcher(
} }
private _updateSearchGroup() { private _updateSearchGroup() {
this._menuGroupAbortController.abort();
this._menuGroupAbortController = new AbortController();
switch (this._mode) { switch (this._mode) {
case AddPopoverMode.Tags: { case AddPopoverMode.Tags: {
this._searchGroups = [ this._searchGroups = [
this.searchMenuConfig.getTagMenuGroup( this.searchMenuConfig.getTagMenuGroup(
this._query, this._query,
this._addTagChip, this._addTagChip,
this.abortController.signal this._menuGroupAbortController.signal
), ),
]; ];
break; break;
@@ -401,7 +406,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
this.searchMenuConfig.getCollectionMenuGroup( this.searchMenuConfig.getCollectionMenuGroup(
this._query, this._query,
this._addCollectionChip, this._addCollectionChip,
this.abortController.signal this._menuGroupAbortController.signal
), ),
]; ];
break; break;
@@ -410,7 +415,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
const docGroup = this.searchMenuConfig.getDocMenuGroup( const docGroup = this.searchMenuConfig.getDocMenuGroup(
this._query, this._query,
this._addDocChip, this._addDocChip,
this.abortController.signal this._menuGroupAbortController.signal
); );
if (!this._query) { if (!this._query) {
this._searchGroups = [docGroup]; this._searchGroups = [docGroup];
@@ -418,12 +423,12 @@ export class ChatPanelAddPopover extends SignalWatcher(
const tagGroup = this.searchMenuConfig.getTagMenuGroup( const tagGroup = this.searchMenuConfig.getTagMenuGroup(
this._query, this._query,
this._addTagChip, this._addTagChip,
this.abortController.signal this._menuGroupAbortController.signal
); );
const collectionGroup = this.searchMenuConfig.getCollectionMenuGroup( const collectionGroup = this.searchMenuConfig.getCollectionMenuGroup(
this._query, this._query,
this._addCollectionChip, this._addCollectionChip,
this.abortController.signal this._menuGroupAbortController.signal
); );
const nothing = html``; const nothing = html``;
this._searchGroups = [ this._searchGroups = [

View File

@@ -1,6 +1,6 @@
import { createLitPortal } from '@blocksuite/affine/components/portal'; import { createLitPortal } from '@blocksuite/affine/components/portal';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { ColorScheme } from '@blocksuite/affine/model'; import { ThemeProvider } from '@blocksuite/affine/shared/services';
import { import {
EditorHost, EditorHost,
PropTypes, PropTypes,
@@ -112,6 +112,7 @@ export class AIItemList extends WithDisposable(LitElement) {
} }
override render() { override render() {
const theme = this.host.std.get(ThemeProvider).app$.value;
return html`${repeat(this.groups, group => { return html`${repeat(this.groups, group => {
return html` return html`
${group.name ${group.name
@@ -124,7 +125,7 @@ export class AIItemList extends WithDisposable(LitElement) {
item => item.name, item => item.name,
item => item =>
html`<ai-item html`<ai-item
.theme=${this.theme} .theme=${theme}
.onClick=${this.onClick} .onClick=${this.onClick}
.item=${item} .item=${item}
.host=${this.host} .host=${this.host}
@@ -147,9 +148,6 @@ export class AIItemList extends WithDisposable(LitElement) {
@property({ attribute: 'data-testid', reflect: true }) @property({ attribute: 'data-testid', reflect: true })
accessor testId = 'ai-item-list'; accessor testId = 'ai-item-list';
@property({ attribute: false })
accessor theme: ColorScheme = ColorScheme.Light;
} }
declare global { declare global {

View File

@@ -86,10 +86,8 @@ export class AskAIPanel extends WithDisposable(LitElement) {
const style = styleMap({ const style = styleMap({
minWidth: `${this.minWidth}px`, minWidth: `${this.minWidth}px`,
}); });
const appTheme = this.host.std.get(ThemeProvider).app$.value;
return html`<div class="ask-ai-panel" style=${style}> return html`<div class="ask-ai-panel" style=${style}>
<ai-item-list <ai-item-list
.theme=${appTheme}
.host=${this.host} .host=${this.host}
.groups=${this._actionGroups} .groups=${this._actionGroups}
.onClick=${this.onItemClick} .onClick=${this.onItemClick}

View File

@@ -8,7 +8,11 @@ import { PageEditorBlockSpecs } from '@blocksuite/affine/extensions';
import { Container, type ServiceProvider } from '@blocksuite/affine/global/di'; import { Container, type ServiceProvider } from '@blocksuite/affine/global/di';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { codeBlockWrapMiddleware } from '@blocksuite/affine/shared/adapters'; import { codeBlockWrapMiddleware } from '@blocksuite/affine/shared/adapters';
import { LinkPreviewerService } from '@blocksuite/affine/shared/services'; import {
LinkPreviewerService,
ThemeProvider,
} from '@blocksuite/affine/shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { import {
BlockStdScope, BlockStdScope,
BlockViewIdentifier, BlockViewIdentifier,
@@ -22,7 +26,11 @@ import type {
Store, Store,
TransformerMiddleware, TransformerMiddleware,
} from '@blocksuite/affine/store'; } from '@blocksuite/affine/store';
import { css, html, nothing, type PropertyValues } from 'lit'; import {
darkCssVariablesV2,
lightCssVariablesV2,
} from '@toeverything/theme/v2';
import { css, html, nothing, type PropertyValues, unsafeCSS } from 'lit';
import { property, query } from 'lit/decorators.js'; import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { keyed } from 'lit/directives/keyed.js'; import { keyed } from 'lit/directives/keyed.js';
@@ -109,7 +117,7 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
padding: 0; padding: 0;
margin: 0; margin: 0;
line-height: var(--affine-line-height); line-height: var(--affine-line-height);
color: var(--affine-text-primary-color); color: ${unsafeCSSVarV2('text/primary')};
font-weight: 400; font-weight: 400;
} }
@@ -168,6 +176,18 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
} }
} }
.text-renderer-container[data-app-theme='dark'] {
.ai-answer-text-editor .affine-page-root-block-container {
color: ${unsafeCSS(darkCssVariablesV2['--affine-v2-text-primary'])};
}
}
.text-renderer-container[data-app-theme='light'] {
.ai-answer-text-editor .affine-page-root-block-container {
color: ${unsafeCSS(lightCssVariablesV2['--affine-v2-text-primary'])};
}
}
${customHeadingStyles} ${customHeadingStyles}
`; `;
@@ -288,8 +308,9 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
'text-renderer-container': true, 'text-renderer-container': true,
'custom-heading': !!customHeading, 'custom-heading': !!customHeading,
}); });
const theme = this.host?.std.get(ThemeProvider).app$.value;
return html` return html`
<div class=${classes} data-testid=${testId}> <div class=${classes} data-testid=${testId} data-app-theme=${theme}>
${keyed( ${keyed(
this._doc, this._doc,
html`<div class="ai-answer-text-editor affine-page-viewport"> html`<div class="ai-answer-text-editor affine-page-viewport">

View File

@@ -30,9 +30,3 @@ export const notesButtonIcon = style({
export const error = style({ export const error = style({
color: cssVarV2('aI/errorText'), color: cssVarV2('aI/errorText'),
}); });
export const publicUserLabel = style({
fontSize: cssVar('fontXs'),
fontWeight: 500,
userSelect: 'none',
});

View File

@@ -37,12 +37,12 @@ export function patchQuickSearchService(framework: FrameworkProvider) {
searchResult = await new Promise((resolve, reject) => searchResult = await new Promise((resolve, reject) =>
framework.get(QuickSearchService).quickSearch.show( framework.get(QuickSearchService).quickSearch.show(
[ [
framework.get(RecentDocsQuickSearchSession), framework.createEntity(RecentDocsQuickSearchSession),
framework.get(CreationQuickSearchSession), framework.createEntity(CreationQuickSearchSession),
framework.get(DocsQuickSearchSession), framework.createEntity(DocsQuickSearchSession),
framework.get(LinksQuickSearchSession), framework.createEntity(LinksQuickSearchSession),
framework.get(ExternalLinksQuickSearchSession), framework.createEntity(ExternalLinksQuickSearchSession),
framework.get(JournalsQuickSearchSession), framework.createEntity(JournalsQuickSearchSession),
], ],
result => { result => {
if (result === null) { if (result === null) {

View File

@@ -210,7 +210,13 @@ export const MeetingsSettings = () => {
const handleOpenMicrophoneRecordingPermissionSetting = const handleOpenMicrophoneRecordingPermissionSetting =
useAsyncCallback(async () => { useAsyncCallback(async () => {
await meetingSettingsService.showRecordingPermissionSetting('microphone'); const result =
await meetingSettingsService.askForMeetingPermission('microphone');
if (!result) {
await meetingSettingsService.showRecordingPermissionSetting(
'microphone'
);
}
}, [meetingSettingsService]); }, [meetingSettingsService]);
const handleOpenSavedRecordings = useAsyncCallback(async () => { const handleOpenSavedRecordings = useAsyncCallback(async () => {

View File

@@ -2,7 +2,7 @@ import { style } from '@vanilla-extract/css';
export const publicUserLabel = style({ export const publicUserLabel = style({
fontSize: 'inherit', fontSize: 'inherit',
display: 'flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
}); });

View File

@@ -242,7 +242,21 @@ export class AudioAttachmentBlock extends Entity<AttachmentBlockModel> {
); );
}; };
const fillActions = async (actions: TranscriptionResult['actions']) => {
if (!actions) {
return;
}
const calloutId = addCalloutBlock('🎯', 'Todo');
await insertFromMarkdown(
undefined,
actions ?? '',
this.props.doc,
calloutId,
1
);
};
fillTranscription(result.segments); fillTranscription(result.segments);
await fillSummary(result.summary); await fillSummary(result.summary);
await fillActions(result.actions);
}; };
} }

View File

@@ -43,6 +43,7 @@ export class AudioTranscriptionJobStore extends Entity<{
} }
const files = await this.props.getAudioFiles(); const files = await this.props.getAudioFiles();
const response = await graphqlService.gql({ const response = await graphqlService.gql({
timeout: 600_000, // default 15s is too short for audio transcription
query: submitAudioTranscriptionMutation, query: submitAudioTranscriptionMutation,
variables: { variables: {
workspaceId: this.currentWorkspaceId, workspaceId: this.currentWorkspaceId,

View File

@@ -7,4 +7,5 @@ export interface TranscriptionResult {
end: string; end: string;
transcription: string; transcription: string;
}[]; }[];
actions?: string;
} }

View File

@@ -188,8 +188,7 @@ export class AudioMediaManagerService extends Service {
if (!stats || !currentState) { if (!stats || !currentState) {
return; return;
} }
const seekOffset = const seekOffset = currentState.seekOffset;
currentState.seekOffset + (Date.now() - currentState.updateTime) / 1000;
this.globalMediaState.updatePlaybackState({ this.globalMediaState.updatePlaybackState({
state: 'playing', state: 'playing',
// rewind to the beginning if the seek offset is greater than the duration // rewind to the beginning if the seek offset is greater than the duration
@@ -207,7 +206,9 @@ export class AudioMediaManagerService extends Service {
this.globalMediaState.updatePlaybackState({ this.globalMediaState.updatePlaybackState({
state: 'paused', state: 'paused',
seekOffset: (Date.now() - state.updateTime) / 1000 + state.seekOffset, seekOffset:
((Date.now() - state.updateTime) / 1000) * (state.playbackRate || 1.0) +
state.seekOffset,
updateTime: Date.now(), updateTime: Date.now(),
}); });
} }

View File

@@ -114,6 +114,12 @@ export class MeetingSettingsService extends Service {
); );
} }
async askForMeetingPermission(type: 'microphone') {
return this.desktopApiService?.handler.recording.askForMeetingPermission(
type
);
}
setRecordingMode = (mode: MeetingSettingsSchema['recordingMode']) => { setRecordingMode = (mode: MeetingSettingsSchema['recordingMode']) => {
const currentMode = this.settings.recordingMode; const currentMode = this.settings.recordingMode;

View File

@@ -116,4 +116,8 @@ export class DocsQuickSearchSession
setQuery(query: string) { setQuery(query: string) {
this.query$.next(query); this.query$.next(query);
} }
override dispose(): void {
this.query.unsubscribe();
}
} }

View File

@@ -152,9 +152,6 @@ export class SearchMenuService extends Service {
}, },
{ {
fields: ['docId', 'title'], fields: ['docId', 'title'],
pagination: {
limit: 1,
},
highlights: [ highlights: [
{ {
field: 'title', field: 'title',

View File

@@ -54,6 +54,6 @@ test('can add text property', async ({ page }) => {
await page.getByTestId('mobile-menu-back-button').last().click(); await page.getByTestId('mobile-menu-back-button').last().click();
await expect(page.getByTestId('mobile-menu-back-button')).toContainText( await expect(page.getByTestId('mobile-menu-back-button')).toContainText(
'How to use folder and Tags' 'Getting Started'
); );
}); });