Compare commits

..

1 Commits

Author SHA1 Message Date
DarkSky
52f4461fff chore: bump deps 2026-03-11 15:50:07 +08:00
22 changed files with 1487 additions and 2325 deletions

View File

@@ -63,7 +63,7 @@
"groupName": "opentelemetry",
"matchPackageNames": [
"/^@opentelemetry/",
"/^@google-cloud/opentelemetry-/"
"/^@google-cloud\/opentelemetry-/"
]
}
],
@@ -79,7 +79,7 @@
"customManagers": [
{
"customType": "regex",
"managerFilePatterns": ["/^rust-toolchain\\.toml?$/"],
"fileMatch": ["^rust-toolchain\\.toml?$"],
"matchStrings": [
"channel\\s*=\\s*\"(?<currentValue>\\d+\\.\\d+(\\.\\d+)?)\""
],

View File

@@ -17,14 +17,7 @@ export async function printToPdf(
return new Promise<void>((resolve, reject) => {
const iframe = document.createElement('iframe');
document.body.append(iframe);
// Use a hidden but rendering-enabled state instead of display: none
Object.assign(iframe.style, {
visibility: 'hidden',
position: 'absolute',
width: '0',
height: '0',
border: 'none',
});
iframe.style.display = 'none';
iframe.srcdoc = '<!DOCTYPE html>';
iframe.onload = async () => {
if (!iframe.contentWindow) {
@@ -35,44 +28,6 @@ export async function printToPdf(
reject(new Error('Root element not defined, unable to print pdf'));
return;
}
const doc = iframe.contentWindow.document;
doc.write(`<!DOCTYPE html><html><head><style>@media print {
html, body {
height: initial !important;
overflow: initial !important;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
color: #000 !important;
background: #fff !important;
color-scheme: light !important;
}
::-webkit-scrollbar {
display: none;
}
:root, body {
--affine-text-primary: #000 !important;
--affine-text-secondary: #111 !important;
--affine-text-tertiary: #333 !important;
--affine-background-primary: #fff !important;
--affine-background-secondary: #fff !important;
--affine-background-tertiary: #fff !important;
}
body, [data-theme='dark'] {
color: #000 !important;
background: #fff !important;
}
body * {
color: #000 !important;
-webkit-text-fill-color: #000 !important;
}
:root {
--affine-note-shadow-box: none !important;
--affine-note-shadow-sticker: none !important;
}
}</style></head><body></body></html>`);
doc.close();
iframe.contentWindow.document
.write(`<!DOCTYPE html><html><head><style>@media print {
html, body {
@@ -94,9 +49,6 @@ export async function printToPdf(
--affine-background-primary: #fff !important;
--affine-background-secondary: #fff !important;
--affine-background-tertiary: #fff !important;
--affine-background-code-block: #f5f5f5 !important;
--affine-quote-color: #e3e3e3 !important;
--affine-border-color: #e3e3e3 !important;
}
body, [data-theme='dark'] {
color: #000 !important;
@@ -116,7 +68,7 @@ export async function printToPdf(
for (const element of document.styleSheets) {
try {
for (const cssRule of element.cssRules) {
const target = doc.styleSheets[0];
const target = iframe.contentWindow.document.styleSheets[0];
target.insertRule(cssRule.cssText, target.cssRules.length);
}
} catch (e) {
@@ -131,33 +83,12 @@ export async function printToPdf(
}
}
// Recursive function to find all canvases, including those in shadow roots
const findAllCanvases = (root: Node): HTMLCanvasElement[] => {
const canvases: HTMLCanvasElement[] = [];
const traverse = (node: Node) => {
if (node instanceof HTMLCanvasElement) {
canvases.push(node);
}
if (node instanceof HTMLElement || node instanceof ShadowRoot) {
node.childNodes.forEach(traverse);
}
if (node instanceof HTMLElement && node.shadowRoot) {
traverse(node.shadowRoot);
}
};
traverse(root);
return canvases;
};
// convert all canvas to image
const canvasImgObjectUrlMap = new Map<string, string>();
const allCanvas = findAllCanvases(rootElement);
const allCanvas = rootElement.getElementsByTagName('canvas');
let canvasKey = 1;
const canvasToKeyMap = new Map<HTMLCanvasElement, string>();
for (const canvas of allCanvas) {
const key = canvasKey.toString();
canvasToKeyMap.set(canvas, key);
canvas.dataset['printToPdfCanvasKey'] = canvasKey.toString();
canvasKey++;
const canvasImgObjectUrl = await new Promise<Blob | null>(resolve => {
try {
@@ -172,42 +103,20 @@ export async function printToPdf(
);
continue;
}
canvasImgObjectUrlMap.set(key, URL.createObjectURL(canvasImgObjectUrl));
canvasImgObjectUrlMap.set(
canvas.dataset['printToPdfCanvasKey'],
URL.createObjectURL(canvasImgObjectUrl)
);
}
// Recursive deep clone that flattens Shadow DOM into Light DOM
const deepCloneWithShadows = (node: Node): Node => {
const clone = doc.importNode(node, false);
if (
clone instanceof HTMLCanvasElement &&
node instanceof HTMLCanvasElement
) {
const key = canvasToKeyMap.get(node);
if (key) {
clone.dataset['printToPdfCanvasKey'] = key;
}
}
const appendChildren = (source: Node) => {
source.childNodes.forEach(child => {
(clone as Element).append(deepCloneWithShadows(child));
});
};
if (node instanceof HTMLElement && node.shadowRoot) {
appendChildren(node.shadowRoot);
}
appendChildren(node);
return clone;
};
const importedRoot = deepCloneWithShadows(rootElement) as HTMLDivElement;
const importedRoot = iframe.contentWindow.document.importNode(
rootElement,
true
) as HTMLDivElement;
// force light theme in print iframe
doc.documentElement.dataset.theme = 'light';
doc.body.dataset.theme = 'light';
iframe.contentWindow.document.documentElement.dataset.theme = 'light';
iframe.contentWindow.document.body.dataset.theme = 'light';
importedRoot.dataset.theme = 'light';
// draw saved canvas image to canvas
@@ -226,67 +135,17 @@ export async function printToPdf(
}
}
// Remove lazy loading from all images and force reload
const allImages = importedRoot.querySelectorAll('img');
allImages.forEach(img => {
img.removeAttribute('loading');
const src = img.getAttribute('src');
if (src) img.setAttribute('src', src);
});
// append to iframe
doc.body.append(importedRoot);
// append to iframe and print
iframe.contentWindow.document.body.append(importedRoot);
await options.beforeprint?.(iframe);
// Robust image waiting logic
const waitForImages = async (container: HTMLElement) => {
const images: HTMLImageElement[] = [];
const view = container.ownerDocument.defaultView;
if (!view) return;
const findImages = (root: Node) => {
if (root instanceof view.HTMLImageElement) {
images.push(root);
}
if (
root instanceof view.HTMLElement ||
root instanceof view.ShadowRoot
) {
root.childNodes.forEach(findImages);
}
if (root instanceof view.HTMLElement && root.shadowRoot) {
findImages(root.shadowRoot);
}
};
findImages(container);
await Promise.all(
images.map(img => {
if (img.complete) {
if (img.naturalWidth === 0) {
console.warn('Image failed to load:', img.src);
}
return Promise.resolve();
}
return new Promise(resolve => {
img.onload = resolve;
img.onerror = resolve;
});
})
);
};
await waitForImages(importedRoot);
// browser may take some time to load font or other resources
await (doc.fonts?.ready ??
new Promise<void>(resolve => {
setTimeout(() => {
resolve();
}, 1000);
}));
// browser may take some time to load font
await new Promise<void>(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
iframe.contentWindow.onafterprint = async () => {
iframe.remove();

View File

@@ -31,13 +31,13 @@
"@google-cloud/opentelemetry-resource-util": "^3.0.0",
"@nestjs-cls/transactional": "^3.2.0",
"@nestjs-cls/transactional-adapter-prisma": "^1.3.4",
"@nestjs/apollo": "^13.0.4",
"@nestjs/apollo": "^13.2.4",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/common": "^11.0.21",
"@nestjs/core": "^11.1.14",
"@nestjs/graphql": "^13.0.4",
"@nestjs/platform-express": "^11.1.14",
"@nestjs/platform-socket.io": "^11.1.14",
"@nestjs/common": "^11.1.16",
"@nestjs/core": "^11.1.16",
"@nestjs/graphql": "^13.2.4",
"@nestjs/platform-express": "^11.1.16",
"@nestjs/platform-socket.io": "^11.1.16",
"@nestjs/schedule": "^6.1.1",
"@nestjs/throttler": "^6.5.0",
"@nestjs/websockets": "^11.1.14",

View File

@@ -1,35 +1,12 @@
import test from 'ava';
import { z } from 'zod';
import type { DocReader } from '../../core/doc';
import type { AccessController } from '../../core/permission';
import type { Models } from '../../models';
import { NativeLlmRequest, NativeLlmStreamEvent } from '../../native';
import {
ToolCallAccumulator,
ToolCallLoop,
ToolSchemaExtractor,
} from '../../plugins/copilot/providers/loop';
import {
buildBlobContentGetter,
createBlobReadTool,
} from '../../plugins/copilot/tools/blob-read';
import {
buildDocKeywordSearchGetter,
createDocKeywordSearchTool,
} from '../../plugins/copilot/tools/doc-keyword-search';
import {
buildDocContentGetter,
createDocReadTool,
} from '../../plugins/copilot/tools/doc-read';
import {
buildDocSearchGetter,
createDocSemanticSearchTool,
} from '../../plugins/copilot/tools/doc-semantic-search';
import {
DOCUMENT_SYNC_PENDING_MESSAGE,
LOCAL_WORKSPACE_SYNC_REQUIRED_MESSAGE,
} from '../../plugins/copilot/tools/doc-sync';
test('ToolCallAccumulator should merge deltas and complete tool call', t => {
const accumulator = new ToolCallAccumulator();
@@ -309,210 +286,3 @@ test('ToolCallLoop should surface invalid JSON as tool error without executing',
is_error: true,
});
});
test('doc_read should return specific sync errors for unavailable docs', async t => {
const cases = [
{
name: 'local workspace without cloud sync',
workspace: null,
authors: null,
markdown: null,
expected: {
type: 'error',
name: 'Workspace Sync Required',
message: LOCAL_WORKSPACE_SYNC_REQUIRED_MESSAGE,
},
docReaderCalled: false,
},
{
name: 'cloud workspace document not synced to server yet',
workspace: { id: 'ws-1' },
authors: null,
markdown: null,
expected: {
type: 'error',
name: 'Document Sync Pending',
message: DOCUMENT_SYNC_PENDING_MESSAGE('doc-1'),
},
docReaderCalled: false,
},
{
name: 'cloud workspace document markdown not ready yet',
workspace: { id: 'ws-1' },
authors: {
createdAt: new Date('2026-01-01T00:00:00.000Z'),
updatedAt: new Date('2026-01-01T00:00:00.000Z'),
createdByUser: null,
updatedByUser: null,
},
markdown: null,
expected: {
type: 'error',
name: 'Document Sync Pending',
message: DOCUMENT_SYNC_PENDING_MESSAGE('doc-1'),
},
docReaderCalled: true,
},
] as const;
const ac = {
user: () => ({
workspace: () => ({ doc: () => ({ can: async () => true }) }),
}),
} as unknown as AccessController;
for (const testCase of cases) {
let docReaderCalled = false;
const docReader = {
getDocMarkdown: async () => {
docReaderCalled = true;
return testCase.markdown;
},
} as unknown as DocReader;
const models = {
workspace: {
get: async () => testCase.workspace,
},
doc: {
getAuthors: async () => testCase.authors,
},
} as unknown as Models;
const getDoc = buildDocContentGetter(ac, docReader, models);
const tool = createDocReadTool(
getDoc.bind(null, {
user: 'user-1',
workspace: 'workspace-1',
})
);
const result = await tool.execute?.({ doc_id: 'doc-1' }, {});
t.is(docReaderCalled, testCase.docReaderCalled, testCase.name);
t.deepEqual(result, testCase.expected, testCase.name);
}
});
test('document search tools should return sync error for local workspace', async t => {
const ac = {
user: () => ({
workspace: () => ({
can: async () => true,
docs: async () => [],
}),
}),
} as unknown as AccessController;
const models = {
workspace: {
get: async () => null,
},
} as unknown as Models;
let keywordSearchCalled = false;
const indexerService = {
searchDocsByKeyword: async () => {
keywordSearchCalled = true;
return [];
},
} as unknown as Parameters<typeof buildDocKeywordSearchGetter>[1];
let semanticSearchCalled = false;
const contextService = {
matchWorkspaceAll: async () => {
semanticSearchCalled = true;
return [];
},
} as unknown as Parameters<typeof buildDocSearchGetter>[1];
const keywordTool = createDocKeywordSearchTool(
buildDocKeywordSearchGetter(ac, indexerService, models).bind(null, {
user: 'user-1',
workspace: 'workspace-1',
})
);
const semanticTool = createDocSemanticSearchTool(
buildDocSearchGetter(ac, contextService, null, models).bind(null, {
user: 'user-1',
workspace: 'workspace-1',
})
);
const keywordResult = await keywordTool.execute?.({ query: 'hello' }, {});
const semanticResult = await semanticTool.execute?.({ query: 'hello' }, {});
t.false(keywordSearchCalled);
t.false(semanticSearchCalled);
t.deepEqual(keywordResult, {
type: 'error',
name: 'Workspace Sync Required',
message: LOCAL_WORKSPACE_SYNC_REQUIRED_MESSAGE,
});
t.deepEqual(semanticResult, {
type: 'error',
name: 'Workspace Sync Required',
message: LOCAL_WORKSPACE_SYNC_REQUIRED_MESSAGE,
});
});
test('doc_semantic_search should return empty array when nothing matches', async t => {
const ac = {
user: () => ({
workspace: () => ({
can: async () => true,
docs: async () => [],
}),
}),
} as unknown as AccessController;
const models = {
workspace: {
get: async () => ({ id: 'workspace-1' }),
},
} as unknown as Models;
const contextService = {
matchWorkspaceAll: async () => [],
} as unknown as Parameters<typeof buildDocSearchGetter>[1];
const semanticTool = createDocSemanticSearchTool(
buildDocSearchGetter(ac, contextService, null, models).bind(null, {
user: 'user-1',
workspace: 'workspace-1',
})
);
const result = await semanticTool.execute?.({ query: 'hello' }, {});
t.deepEqual(result, []);
});
test('blob_read should return explicit error when attachment context is missing', async t => {
const ac = {
user: () => ({
workspace: () => ({
allowLocal: () => ({
can: async () => true,
}),
}),
}),
} as unknown as AccessController;
const blobTool = createBlobReadTool(
buildBlobContentGetter(ac, null).bind(null, {
user: 'user-1',
workspace: 'workspace-1',
})
);
const result = await blobTool.execute?.({ blob_id: 'blob-1' }, {});
t.deepEqual(result, {
type: 'error',
name: 'Blob Read Failed',
message:
'Missing workspace, user, blob id, or copilot context for blob_read.',
});
});

View File

@@ -470,8 +470,7 @@ export abstract class CopilotProvider<C = any> {
});
const searchDocs = buildDocKeywordSearchGetter(
ac,
indexerService,
models
indexerService
);
tools.doc_keyword_search = createDocKeywordSearchTool(
searchDocs.bind(null, options)

View File

@@ -18,10 +18,7 @@ export const buildBlobContentGetter = (
chunk?: number
) => {
if (!options?.user || !options?.workspace || !blobId || !context) {
return toolError(
'Blob Read Failed',
'Missing workspace, user, blob id, or copilot context for blob_read.'
);
return;
}
const canAccess = await ac
.user(options.user)
@@ -32,10 +29,7 @@ export const buildBlobContentGetter = (
logger.warn(
`User ${options.user} does not have access workspace ${options.workspace}`
);
return toolError(
'Blob Read Failed',
'You do not have permission to access this workspace attachment.'
);
return;
}
const contextFile = context.files.find(
@@ -48,12 +42,7 @@ export const buildBlobContentGetter = (
context.getBlobContent(canonicalBlobId, chunk),
]);
const content = file?.trim() || blob?.trim();
if (!content) {
return toolError(
'Blob Read Failed',
`Attachment ${canonicalBlobId} is not available for reading in the current copilot context.`
);
}
if (!content) return;
const info = contextFile
? { fileName: contextFile.name, fileType: contextFile.mimeType }
: {};
@@ -64,7 +53,10 @@ export const buildBlobContentGetter = (
};
export const createBlobReadTool = (
getBlobContent: (targetId?: string, chunk?: number) => Promise<object>
getBlobContent: (
targetId?: string,
chunk?: number
) => Promise<object | undefined>
) => {
return defineTool({
description:
@@ -81,10 +73,13 @@ export const createBlobReadTool = (
execute: async ({ blob_id, chunk }) => {
try {
const blob = await getBlobContent(blob_id, chunk);
if (!blob) {
return;
}
return { ...blob };
} catch (err: any) {
logger.error(`Failed to read the blob ${blob_id} in context`, err);
return toolError('Blob Read Failed', err.message ?? String(err));
return toolError('Blob Read Failed', err.message);
}
},
});

View File

@@ -1,43 +1,27 @@
import { z } from 'zod';
import type { AccessController } from '../../../core/permission';
import type { Models } from '../../../models';
import type { IndexerService, SearchDoc } from '../../indexer';
import { workspaceSyncRequiredError } from './doc-sync';
import { toolError } from './error';
import { defineTool } from './tool';
import type { CopilotChatOptions } from './types';
export const buildDocKeywordSearchGetter = (
ac: AccessController,
indexerService: IndexerService,
models: Models
indexerService: IndexerService
) => {
const searchDocs = async (options: CopilotChatOptions, query?: string) => {
const queryTrimmed = query?.trim();
if (!options || !queryTrimmed || !options.user || !options.workspace) {
return toolError(
'Doc Keyword Search Failed',
'Missing workspace, user, or query for doc_keyword_search.'
);
}
const workspace = await models.workspace.get(options.workspace);
if (!workspace) {
return workspaceSyncRequiredError();
if (!options || !query?.trim() || !options.user || !options.workspace) {
return undefined;
}
const canAccess = await ac
.user(options.user)
.workspace(options.workspace)
.can('Workspace.Read');
if (!canAccess) {
return toolError(
'Doc Keyword Search Failed',
'You do not have permission to access this workspace.'
);
}
if (!canAccess) return undefined;
const docs = await indexerService.searchDocsByKeyword(
options.workspace,
queryTrimmed
query
);
// filter current user readable docs
@@ -45,15 +29,13 @@ export const buildDocKeywordSearchGetter = (
.user(options.user)
.workspace(options.workspace)
.docs(docs, 'Doc.Read');
return readableDocs ?? [];
return readableDocs;
};
return searchDocs;
};
export const createDocKeywordSearchTool = (
searchDocs: (
query: string
) => Promise<SearchDoc[] | ReturnType<typeof toolError>>
searchDocs: (query: string) => Promise<SearchDoc[] | undefined>
) => {
return defineTool({
description:
@@ -68,8 +50,8 @@ export const createDocKeywordSearchTool = (
execute: async ({ query }) => {
try {
const docs = await searchDocs(query);
if (!Array.isArray(docs)) {
return docs;
if (!docs) {
return;
}
return docs.map(doc => ({
docId: doc.docId,

View File

@@ -3,20 +3,13 @@ import { z } from 'zod';
import { DocReader } from '../../../core/doc';
import { AccessController } from '../../../core/permission';
import { Models } from '../../../models';
import {
documentSyncPendingError,
workspaceSyncRequiredError,
} from './doc-sync';
import { type ToolError, toolError } from './error';
import { Models, publicUserSelect } from '../../../models';
import { toolError } from './error';
import { defineTool } from './tool';
import type { CopilotChatOptions } from './types';
const logger = new Logger('DocReadTool');
const isToolError = (result: ToolError | object): result is ToolError =>
'type' in result && result.type === 'error';
export const buildDocContentGetter = (
ac: AccessController,
docReader: DocReader,
@@ -24,17 +17,8 @@ export const buildDocContentGetter = (
) => {
const getDoc = async (options: CopilotChatOptions, docId?: string) => {
if (!options?.user || !options?.workspace || !docId) {
return toolError(
'Doc Read Failed',
'Missing workspace, user, or document id for doc_read.'
);
return;
}
const workspace = await models.workspace.get(options.workspace);
if (!workspace) {
return workspaceSyncRequiredError();
}
const canAccess = await ac
.user(options.user)
.workspace(options.workspace)
@@ -44,15 +28,23 @@ export const buildDocContentGetter = (
logger.warn(
`User ${options.user} does not have access to doc ${docId} in workspace ${options.workspace}`
);
return toolError(
'Doc Read Failed',
`You do not have permission to read document ${docId} in this workspace.`
);
return;
}
const docMeta = await models.doc.getAuthors(options.workspace, docId);
const docMeta = await models.doc.getSnapshot(options.workspace, docId, {
select: {
createdAt: true,
updatedAt: true,
createdByUser: {
select: publicUserSelect,
},
updatedByUser: {
select: publicUserSelect,
},
},
});
if (!docMeta) {
return documentSyncPendingError(docId);
return;
}
const content = await docReader.getDocMarkdown(
@@ -61,7 +53,7 @@ export const buildDocContentGetter = (
true
);
if (!content) {
return documentSyncPendingError(docId);
return;
}
return {
@@ -77,12 +69,8 @@ export const buildDocContentGetter = (
return getDoc;
};
type DocReadToolResult = Awaited<
ReturnType<ReturnType<typeof buildDocContentGetter>>
>;
export const createDocReadTool = (
getDoc: (targetId?: string) => Promise<DocReadToolResult>
getDoc: (targetId?: string) => Promise<object | undefined>
) => {
return defineTool({
description:
@@ -93,10 +81,13 @@ export const createDocReadTool = (
execute: async ({ doc_id }) => {
try {
const doc = await getDoc(doc_id);
return isToolError(doc) ? doc : { ...doc };
if (!doc) {
return;
}
return { ...doc };
} catch (err: any) {
logger.error(`Failed to read the doc ${doc_id}`, err);
return toolError('Doc Read Failed', err.message ?? String(err));
return toolError('Doc Read Failed', err.message);
}
},
});

View File

@@ -7,7 +7,6 @@ import {
clearEmbeddingChunk,
type Models,
} from '../../../models';
import { workspaceSyncRequiredError } from './doc-sync';
import { toolError } from './error';
import { defineTool } from './tool';
import type {
@@ -28,24 +27,14 @@ export const buildDocSearchGetter = (
signal?: AbortSignal
) => {
if (!options || !query?.trim() || !options.user || !options.workspace) {
return toolError(
'Doc Semantic Search Failed',
'Missing workspace, user, or query for doc_semantic_search.'
);
}
const workspace = await models.workspace.get(options.workspace);
if (!workspace) {
return workspaceSyncRequiredError();
return `Invalid search parameters.`;
}
const canAccess = await ac
.user(options.user)
.workspace(options.workspace)
.can('Workspace.Read');
if (!canAccess)
return toolError(
'Doc Semantic Search Failed',
'You do not have permission to access this workspace.'
);
return 'You do not have permission to access this workspace.';
const [chunks, contextChunks] = await Promise.all([
context.matchWorkspaceAll(options.workspace, query, 10, signal),
docContext?.matchFiles(query, 10, signal) ?? [],
@@ -64,7 +53,7 @@ export const buildDocSearchGetter = (
fileChunks.push(...contextChunks);
}
if (!blobChunks.length && !docChunks.length && !fileChunks.length) {
return [];
return `No results found for "${query}".`;
}
const docIds = docChunks.map(c => ({
@@ -112,7 +101,7 @@ export const createDocSemanticSearchTool = (
searchDocs: (
query: string,
signal?: AbortSignal
) => Promise<ChunkSimilarity[] | ReturnType<typeof toolError>>
) => Promise<ChunkSimilarity[] | string | undefined>
) => {
return defineTool({
description:

View File

@@ -1,13 +0,0 @@
import { toolError } from './error';
export const LOCAL_WORKSPACE_SYNC_REQUIRED_MESSAGE =
'This workspace is local-only and does not have AFFiNE Cloud sync enabled yet. Ask the user to enable workspace sync, then try again.';
export const DOCUMENT_SYNC_PENDING_MESSAGE = (docId: string) =>
`Document ${docId} is not available on AFFiNE Cloud yet. Ask the user to wait for workspace sync to finish, then try again.`;
export const workspaceSyncRequiredError = () =>
toolError('Workspace Sync Required', LOCAL_WORKSPACE_SYNC_REQUIRED_MESSAGE);
export const documentSyncPendingError = (docId: string) =>
toolError('Document Sync Pending', DOCUMENT_SYNC_PENDING_MESSAGE(docId));

View File

@@ -12,10 +12,10 @@
},
"sideEffects": false,
"devDependencies": {
"@graphql-codegen/add": "^5.0.3",
"@graphql-codegen/cli": "^5.0.7",
"@graphql-codegen/typescript": "^4.1.6",
"@graphql-codegen/typescript-operations": "^4.6.1",
"@graphql-codegen/add": "^6.0.0",
"@graphql-codegen/cli": "^6.1.3",
"@graphql-codegen/typescript": "^5.0.9",
"@graphql-codegen/typescript-operations": "^5.0.9",
"@types/lodash-es": "^4.17.12",
"prettier": "^3.7.4",
"vitest": "^4.0.18"

View File

@@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/ListViewKit",
"state" : {
"revision" : "07f7adfa0629f8647991e3c148b7d3e060fe2917",
"version" : "1.2.0"
"revision" : "5dea05a52a6c2c7bb013a5925c517d6e32940605",
"version" : "1.1.8"
}
},
{
@@ -59,8 +59,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/MarkdownView",
"state" : {
"revision" : "37f97345a108e95f66b6671c317b43063c7f2de1",
"version" : "3.8.2"
"revision" : "8b8c1eecd251051c5ec2bdd5f31a2243efd9be6c",
"version" : "3.6.2"
}
},
{

View File

@@ -21,8 +21,8 @@ let package = Package(
.package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.7.1"),
.package(url: "https://github.com/SwifterSwift/SwifterSwift.git", from: "6.2.0"),
.package(url: "https://github.com/Recouse/EventSource.git", from: "0.1.7"),
.package(url: "https://github.com/Lakr233/ListViewKit.git", from: "1.2.0"),
.package(url: "https://github.com/Lakr233/MarkdownView.git", from: "3.8.2"),
.package(url: "https://github.com/Lakr233/ListViewKit.git", from: "1.1.8"),
.package(url: "https://github.com/Lakr233/MarkdownView.git", from: "3.6.3"),
],
targets: [
.target(name: "Intelligents", dependencies: [

View File

@@ -7,8 +7,6 @@ import { html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import type { ToolResult } from './tool-result-card';
import { getToolErrorDisplayName, isToolError } from './tool-result-utils';
import type { ToolError } from './type';
interface DocKeywordSearchToolCall {
type: 'tool-call';
@@ -22,7 +20,10 @@ interface DocKeywordSearchToolResult {
toolCallId: string;
toolName: string;
args: { query: string };
result: Array<{ title: string; docId: string }> | ToolError | null;
result: Array<{
title: string;
docId: string;
}>;
}
export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
@@ -50,23 +51,9 @@ export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
if (this.data.type !== 'tool-result') {
return nothing;
}
const result = this.data.result;
if (!result || isToolError(result)) {
return html`<tool-call-failed
.name=${getToolErrorDisplayName(
isToolError(result) ? result : null,
'Document search failed',
{
'Workspace Sync Required':
'Enable workspace sync to search documents',
}
)}
.icon=${SearchIcon()}
></tool-call-failed>`;
}
let results: ToolResult[] = [];
try {
results = result.map(item => ({
results = this.data.result.map(item => ({
title: item.title,
icon: PageIcon(),
onClick: () => {
@@ -82,7 +69,7 @@ export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
console.error('Failed to parse result', err);
}
return html`<tool-result-card
.name=${`Found ${result.length} pages for "${this.data.args.query}"`}
.name=${`Found ${this.data.result.length} pages for "${this.data.args.query}"`}
.icon=${SearchIcon()}
.width=${this.width}
.results=${results}

View File

@@ -6,9 +6,6 @@ import type { Signal } from '@preact/signals-core';
import { html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { getToolErrorDisplayName, isToolError } from './tool-result-utils';
import type { ToolError } from './type';
interface DocReadToolCall {
type: 'tool-call';
toolCallId: string;
@@ -21,24 +18,14 @@ interface DocReadToolResult {
toolCallId: string;
toolName: string;
args: { doc_id: string };
result:
| {
/** Old result may not have docId */
docId?: string;
title: string;
markdown: string;
}
| ToolError
| null;
result: {
/** Old result may not have docId */
docId?: string;
title: string;
markdown: string;
};
}
const getFailedName = (result: ToolError | null) => {
return getToolErrorDisplayName(result, 'Document read failed', {
'Workspace Sync Required': 'Enable workspace sync to read this document',
'Document Sync Pending': 'Wait for document sync to finish',
});
};
export class DocReadResult extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor data!: DocReadToolCall | DocReadToolResult;
@@ -62,25 +49,18 @@ export class DocReadResult extends WithDisposable(ShadowlessElement) {
if (this.data.type !== 'tool-result') {
return nothing;
}
const result = this.data.result;
if (!result || isToolError(result)) {
return html`<tool-call-failed
.name=${getFailedName(isToolError(result) ? result : null)}
.icon=${ViewIcon()}
></tool-call-failed>`;
}
// TODO: better markdown rendering
return html`<tool-result-card
.name=${`Read "${result.title}"`}
.name=${`Read "${this.data.result.title}"`}
.icon=${ViewIcon()}
.width=${this.width}
.results=${[
{
title: result.title,
title: this.data.result.title,
icon: PageIcon(),
content: result.markdown,
content: this.data.result.markdown,
onClick: () => {
const docId = result.docId;
const docId = (this.data as DocReadToolResult).result.docId;
if (!docId) {
return;
}

View File

@@ -7,8 +7,6 @@ import { html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import type { DocDisplayConfig } from '../ai-chat-chips';
import { getToolErrorDisplayName, isToolError } from './tool-result-utils';
import type { ToolError } from './type';
interface DocSemanticSearchToolCall {
type: 'tool-call';
@@ -22,7 +20,10 @@ interface DocSemanticSearchToolResult {
toolCallId: string;
toolName: string;
args: { query: string };
result: Array<{ content: string; docId: string }> | ToolError | null;
result: Array<{
content: string;
docId: string;
}>;
}
function parseResultContent(content: string) {
@@ -81,25 +82,11 @@ export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
if (this.data.type !== 'tool-result') {
return nothing;
}
const result = this.data.result;
if (!result || isToolError(result)) {
return html`<tool-call-failed
.name=${getToolErrorDisplayName(
isToolError(result) ? result : null,
'Semantic search failed',
{
'Workspace Sync Required':
'Enable workspace sync to search documents',
}
)}
.icon=${AiEmbeddingIcon()}
></tool-call-failed>`;
}
return html`<tool-result-card
.name=${`Found semantically related pages for "${this.data.args.query}"`}
.icon=${AiEmbeddingIcon()}
.width=${this.width}
.results=${result
.results=${this.data.result
.map(result => ({
...parseResultContent(result.content),
title: this.docDisplayService.getTitle(result.docId),

View File

@@ -1,16 +0,0 @@
import type { ToolError } from './type';
export const isToolError = (result: unknown): result is ToolError =>
!!result &&
typeof result === 'object' &&
'type' in result &&
(result as ToolError).type === 'error';
export const getToolErrorDisplayName = (
result: ToolError | null,
fallback: string,
overrides: Record<string, string> = {}
) => {
if (!result) return fallback;
return overrides[result.name] ?? result.name;
};

View File

@@ -54,22 +54,13 @@ export class I18n extends Entity {
constructor(private readonly cache: GlobalCache) {
super();
this.i18n.on('languageChanged', (language: Language) => {
this.applyDocumentLanguage(language);
document.documentElement.lang = language;
this.cache.set('i18n_lng', language);
});
}
init() {
const language = this.currentLanguageKey$.value ?? 'en';
this.applyDocumentLanguage(language);
this.changeLanguage(language);
}
private applyDocumentLanguage(language: Language) {
document.documentElement.lang = language;
document.documentElement.dir = SUPPORTED_LANGUAGES[language]?.rtl
? 'rtl'
: 'ltr';
this.changeLanguage(this.currentLanguageKey$.value ?? 'en');
}
changeLanguage = effect(

File diff suppressed because it is too large Load Diff

View File

@@ -32,7 +32,6 @@ export const SUPPORTED_LANGUAGES: Record<
name: string;
originalName: string;
flagEmoji: string;
rtl?: boolean;
resource:
| LanguageResource
| (() => Promise<{ default: Partial<LanguageResource> }>);
@@ -150,21 +149,18 @@ export const SUPPORTED_LANGUAGES: Record<
name: 'Urdu',
originalName: 'اردو',
flagEmoji: '🇵🇰',
rtl: true,
resource: () => import('./ur.json'),
},
ar: {
name: 'Arabic',
originalName: 'العربية',
flagEmoji: '🇸🇦',
rtl: true,
resource: () => import('./ar.json'),
},
fa: {
name: 'Persian',
originalName: 'فارسی',
flagEmoji: '🇮🇷',
rtl: true,
resource: () => import('./fa.json'),
},
uk: {

View File

@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" dir="ltr">
<html lang="en">
<head>
<meta charset="utf-8" />
<meta

1203
yarn.lock

File diff suppressed because it is too large Load Diff