mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-05-08 22:07:32 +08:00
Compare commits
6 Commits
91554c3f79
...
f5290f3d2a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5290f3d2a | ||
|
|
5813e7dd77 | ||
|
|
ac37d07e74 | ||
|
|
429e7f495d | ||
|
|
339f89220a | ||
|
|
440ff0c342 |
2
.github/deployment/node/Dockerfile
vendored
2
.github/deployment/node/Dockerfile
vendored
@@ -1,4 +1,4 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
# syntax=docker/dockerfile:1.23
|
||||
|
||||
FROM node:22-bookworm-slim AS assets
|
||||
WORKDIR /app
|
||||
|
||||
2
.github/workflows/release-mobile.yml
vendored
2
.github/workflows/release-mobile.yml
vendored
@@ -182,7 +182,7 @@ jobs:
|
||||
run: yarn workspace @affine/android cap sync
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
- name: Auth gcloud
|
||||
id: auth
|
||||
uses: google-github-actions/auth@v2
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,4 +12,4 @@ npmPublishAccess: public
|
||||
|
||||
npmRegistryServer: "https://registry.npmjs.org"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.13.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.14.1.cjs
|
||||
|
||||
1085
Cargo.lock
generated
1085
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
54
Cargo.toml
54
Cargo.toml
@@ -30,15 +30,15 @@ resolver = "3"
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
core-foundation = "0.10"
|
||||
coreaudio-rs = "0.12"
|
||||
cpal = "0.15"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
coreaudio-rs = "0.14"
|
||||
cpal = "0.17"
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
criterion2 = { version = "3", default-features = false }
|
||||
crossbeam-channel = "0.5"
|
||||
dispatch2 = "0.3"
|
||||
docx-parser = { git = "https://github.com/toeverything/docx-parser", rev = "380beea" }
|
||||
dotenvy = "0.15"
|
||||
file-format = { version = "0.28", features = ["reader"] }
|
||||
file-format = { version = "0.29", features = ["reader"] }
|
||||
homedir = "0.3"
|
||||
image = { version = "0.25.9", default-features = false, features = [
|
||||
"bmp",
|
||||
@@ -57,13 +57,13 @@ resolver = "3"
|
||||
llm_runtime = { version = "0.2", default-features = false }
|
||||
log = "0.4"
|
||||
loom = { version = "0.7", features = ["checkpoint"] }
|
||||
lru = "0.16"
|
||||
lru = "0.18"
|
||||
matroska = "0.30"
|
||||
memory-indexer = "0.3.1"
|
||||
mermaid-rs-renderer = { git = "https://github.com/toeverything/mermaid-rs-renderer", rev = "fba9097", default-features = false }
|
||||
mimalloc = "0.1"
|
||||
mp4parse = "0.17"
|
||||
nanoid = "0.4"
|
||||
nanoid = "0.5"
|
||||
napi = { version = "3.7.0", features = [
|
||||
"async",
|
||||
"chrono_date",
|
||||
@@ -83,22 +83,22 @@ resolver = "3"
|
||||
parking_lot = "0.12"
|
||||
path-ext = "0.1.2"
|
||||
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
phf = { version = "0.13", features = ["macros"] }
|
||||
proptest = "1.3"
|
||||
proptest-derive = "0.5"
|
||||
proptest-derive = "0.8"
|
||||
pulldown-cmark = "0.13"
|
||||
rand = "0.9"
|
||||
rand_chacha = "0.9"
|
||||
rand_distr = "0.5"
|
||||
rand = "0.10"
|
||||
rand_chacha = "0.10"
|
||||
rand_distr = "0.6"
|
||||
rayon = "1.10"
|
||||
readability = { version = "0.3.0", default-features = false }
|
||||
regex = "1.10"
|
||||
rubato = "0.16"
|
||||
schemars = "0.8"
|
||||
screencapturekit = "0.3"
|
||||
schemars = "0.9"
|
||||
screencapturekit = "0.4"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sha3 = "0.10"
|
||||
sha3 = "0.11"
|
||||
smol_str = "0.3"
|
||||
sqlx = { version = "0.8", default-features = false, features = [
|
||||
"chrono",
|
||||
@@ -108,23 +108,23 @@ resolver = "3"
|
||||
"sqlite",
|
||||
"tls-rustls",
|
||||
] }
|
||||
strum_macros = "0.27.0"
|
||||
strum_macros = "0.28.0"
|
||||
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
|
||||
text-splitter = "0.27"
|
||||
text-splitter = "0.30"
|
||||
thiserror = "2"
|
||||
tiktoken-rs = "0.7"
|
||||
tiktoken-rs = "0.11"
|
||||
tokio = "1.45"
|
||||
tree-sitter = { version = "0.25" }
|
||||
tree-sitter = { version = "0.26" }
|
||||
tree-sitter-c = { version = "0.24" }
|
||||
tree-sitter-c-sharp = { version = "0.23" }
|
||||
tree-sitter-cpp = { version = "0.23" }
|
||||
tree-sitter-go = { version = "0.23" }
|
||||
tree-sitter-go = { version = "0.25" }
|
||||
tree-sitter-java = { version = "0.23" }
|
||||
tree-sitter-javascript = { version = "0.23" }
|
||||
tree-sitter-javascript = { version = "0.25" }
|
||||
tree-sitter-kotlin-ng = { version = "1.1" }
|
||||
tree-sitter-python = { version = "0.23" }
|
||||
tree-sitter-python = { version = "0.25" }
|
||||
tree-sitter-rust = { version = "0.24" }
|
||||
tree-sitter-scala = { version = "0.24" }
|
||||
tree-sitter-scala = { version = "0.26" }
|
||||
tree-sitter-typescript = { version = "0.23" }
|
||||
typst = "0.14.2"
|
||||
typst-as-lib = { version = "0.15.4", default-features = false, features = [
|
||||
@@ -134,11 +134,11 @@ resolver = "3"
|
||||
"ureq",
|
||||
] }
|
||||
typst-svg = "0.14.2"
|
||||
uniffi = "0.29"
|
||||
uniffi = "0.31"
|
||||
url = { version = "2.5" }
|
||||
uuid = "1.8"
|
||||
v_htmlescape = "0.15"
|
||||
windows = { version = "0.61", features = [
|
||||
v_htmlescape = "0.17"
|
||||
windows = { version = "0.62", features = [
|
||||
"Win32_Devices_FunctionDiscovery",
|
||||
"Win32_Foundation",
|
||||
"Win32_Media_Audio",
|
||||
@@ -150,10 +150,10 @@ resolver = "3"
|
||||
"Win32_System_Variant",
|
||||
"Win32_UI_Shell_PropertiesSystem",
|
||||
] }
|
||||
windows-core = { version = "0.61" }
|
||||
windows-core = { version = "0.62" }
|
||||
y-octo = { path = "./packages/common/y-octo/core" }
|
||||
y-sync = { version = "0.4" }
|
||||
yrs = "0.23.0"
|
||||
yrs = "0.26.0"
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"playwright": "=1.58.2",
|
||||
"playwright": "=1.59.1",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -24,8 +24,8 @@ const styles = css`
|
||||
font-size: var(--affine-font-sm);
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
color: var(--affine-white);
|
||||
background: var(--affine-tooltip);
|
||||
color: var(--affine-v2-tooltips-foreground, var(--affine-white));
|
||||
background: var(--affine-v2-tooltips-background, var(--affine-tooltip));
|
||||
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
@@ -40,6 +40,9 @@ const styles = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const TOOLTIP_ARROW_COLOR =
|
||||
'var(--affine-v2-tooltips-background, var(--affine-tooltip))';
|
||||
|
||||
// See http://apps.eky.hk/css-triangle-generator/
|
||||
const TRIANGLE_HEIGHT = 6;
|
||||
const triangleMap = {
|
||||
@@ -47,25 +50,25 @@ const triangleMap = {
|
||||
bottom: '-6px',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '6px 5px 0 5px',
|
||||
borderColor: 'var(--affine-tooltip) transparent transparent transparent',
|
||||
borderColor: `${TOOLTIP_ARROW_COLOR} transparent transparent transparent`,
|
||||
},
|
||||
right: {
|
||||
left: '-6px',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '5px 6px 5px 0',
|
||||
borderColor: 'transparent var(--affine-tooltip) transparent transparent',
|
||||
borderColor: `transparent ${TOOLTIP_ARROW_COLOR} transparent transparent`,
|
||||
},
|
||||
bottom: {
|
||||
top: '-6px',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '0 5px 6px 5px',
|
||||
borderColor: 'transparent transparent var(--affine-tooltip) transparent',
|
||||
borderColor: `transparent transparent ${TOOLTIP_ARROW_COLOR} transparent`,
|
||||
},
|
||||
left: {
|
||||
right: '-6px',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '5px 0 5px 6px',
|
||||
borderColor: 'transparent transparent transparent var(--affine-tooltip)',
|
||||
borderColor: `transparent transparent transparent ${TOOLTIP_ARROW_COLOR}`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"playwright": "=1.58.2",
|
||||
"playwright": "=1.59.1",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -320,9 +320,21 @@ export const htmlMarkElementToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const dataColor =
|
||||
typeof ast.properties?.dataColor === 'string'
|
||||
? ast.properties.dataColor
|
||||
: '';
|
||||
const colorName =
|
||||
dataColor &&
|
||||
/^(red|orange|yellow|green|teal|blue|purple|grey)$/.test(dataColor)
|
||||
? dataColor
|
||||
: 'yellow';
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes };
|
||||
delta.attributes = {
|
||||
...delta.attributes,
|
||||
background: `var(--affine-text-highlight-${colorName})`,
|
||||
};
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"micromark-extension-gfm-table": "^2.1.0",
|
||||
"micromark-extension-gfm-task-list-item": "^2.1.0",
|
||||
"micromark-util-combine-extensions": "^2.0.0",
|
||||
"pdfmake": "^0.2.20",
|
||||
"pdfmake": "^0.3.0",
|
||||
"quick-lru": "^7.3.0",
|
||||
"rehype-parse": "^9.0.0",
|
||||
"rehype-stringify": "^10.0.0",
|
||||
@@ -72,7 +72,7 @@
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/pdfmake": "^0.2.12",
|
||||
"@types/pdfmake": "^0.3.0",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"version": "0.26.3"
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"fflate": "^0.8.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.23",
|
||||
"mammoth": "^1.11.0",
|
||||
|
||||
531
blocksuite/affine/widgets/linked-doc/src/transformers/bear.ts
Normal file
531
blocksuite/affine/widgets/linked-doc/src/transformers/bear.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
import {
|
||||
defaultImageProxyMiddleware,
|
||||
docLinkBaseURLMiddleware,
|
||||
fileNameMiddleware,
|
||||
filePathMiddleware,
|
||||
MarkdownAdapter,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { Container } from '@blocksuite/global/di';
|
||||
import { sha } from '@blocksuite/global/utils';
|
||||
import type { ExtensionType, Schema, Workspace } from '@blocksuite/store';
|
||||
import { extMimeMap, Transformer } from '@blocksuite/store';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
import { createCollectionDocCRUD } from './markdown.js';
|
||||
|
||||
/** Recursive tree node representing a tag-based folder hierarchy. */
|
||||
type FolderHierarchy = {
|
||||
name: string;
|
||||
path: string;
|
||||
children: Map<string, FolderHierarchy>;
|
||||
pageId?: string;
|
||||
parentPath?: string;
|
||||
};
|
||||
|
||||
type BearImportOptions = {
|
||||
collection: Workspace;
|
||||
schema: Schema;
|
||||
imported: Blob;
|
||||
extensions: ExtensionType[];
|
||||
};
|
||||
|
||||
type BearImportResult = {
|
||||
docIds: string[];
|
||||
tags: Map<string, string[]>;
|
||||
folderHierarchy: FolderHierarchy;
|
||||
};
|
||||
|
||||
type BundleEntry = {
|
||||
bundlePath: string;
|
||||
markdownPath: string | null;
|
||||
infoJsonPath: string | null;
|
||||
assetPaths: string[];
|
||||
};
|
||||
|
||||
/** Create a DI provider from the given extensions. */
|
||||
function getProvider(extensions: ExtensionType[]) {
|
||||
const container = new Container();
|
||||
extensions.forEach(ext => {
|
||||
ext.setup(container);
|
||||
});
|
||||
return container.provider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Bear tags from the trailing footer of a markdown document.
|
||||
* Bear places tags (e.g. `#tag`, `#multi word tag#`, `#nested/tag`) at the end
|
||||
* of notes. This scans from the bottom up, collecting tag-only lines (up to 5)
|
||||
* and returns the deduplicated tags plus the content with those lines removed.
|
||||
*/
|
||||
function parseBearTags(markdown: string): {
|
||||
tags: string[];
|
||||
content: string;
|
||||
} {
|
||||
const lines = markdown.split('\n');
|
||||
|
||||
const codeFenceState: boolean[] = [];
|
||||
let inCodeBlock = false;
|
||||
for (const line of lines) {
|
||||
if (line.trimStart().startsWith('```')) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
}
|
||||
codeFenceState.push(inCodeBlock);
|
||||
}
|
||||
|
||||
const tags: string[] = [];
|
||||
const tagLineIndices = new Set<number>();
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
if (codeFenceState[i]) break;
|
||||
|
||||
const lineTags = extractTagsFromLine(line);
|
||||
if (lineTags.length > 0) {
|
||||
for (const tag of lineTags) {
|
||||
tags.push(tag);
|
||||
}
|
||||
tagLineIndices.add(i);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
if (tagLineIndices.size >= 5) break;
|
||||
}
|
||||
|
||||
const filteredLines = lines.filter((_, i) => !tagLineIndices.has(i));
|
||||
while (
|
||||
filteredLines.length > 0 &&
|
||||
filteredLines[filteredLines.length - 1].trim() === ''
|
||||
) {
|
||||
filteredLines.pop();
|
||||
}
|
||||
|
||||
return {
|
||||
tags: deduplicateTags(tags),
|
||||
content: filteredLines.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Bear tags from a single line. Supports open tags (`#tag`),
|
||||
* closed tags (`#multi word tag#`), and nested tags (`#parent/child`).
|
||||
* Returns an empty array if the line contains non-tag content.
|
||||
*/
|
||||
function extractTagsFromLine(line: string): string[] {
|
||||
const tags: string[] = [];
|
||||
let remaining = line;
|
||||
|
||||
while (remaining.length > 0) {
|
||||
remaining = remaining.trimStart();
|
||||
if (!remaining) break;
|
||||
|
||||
if (remaining.startsWith('[')) return [];
|
||||
|
||||
if (remaining.startsWith('#')) {
|
||||
if (remaining.length > 1 && remaining[1] === ' ') return [];
|
||||
if (remaining.length > 2 && remaining[1] === '#') return [];
|
||||
|
||||
const closedMatch = remaining.match(/^#([^#\n]+)#/);
|
||||
if (closedMatch) {
|
||||
const tagValue = closedMatch[1].trim();
|
||||
if (tagValue) {
|
||||
tags.push(tagValue);
|
||||
remaining = remaining.slice(closedMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const openMatch = remaining.match(
|
||||
/^#([\p{L}\p{N}_][\p{L}\p{N}_/-]*)(.*)$/u
|
||||
);
|
||||
if (openMatch) {
|
||||
const tagValue = openMatch[1];
|
||||
const after = openMatch[2].trim();
|
||||
if (tagValue) {
|
||||
tags.push(tagValue);
|
||||
remaining = after;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduplicate tags case-insensitively while preserving the original
|
||||
* capitalization of the first occurrence of each tag.
|
||||
*/
|
||||
function deduplicateTags(tags: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const tag of tags) {
|
||||
const normalized = tag.toLowerCase();
|
||||
if (!seen.has(normalized)) {
|
||||
seen.add(normalized);
|
||||
result.push(tag);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a nested folder hierarchy from Bear tags.
|
||||
* Tags like `parent/child` create nested folders. Documents are attached
|
||||
* as leaf nodes under their tag's folder using `__doc__` prefixed keys.
|
||||
*/
|
||||
function buildFolderHierarchyFromTags(
|
||||
tagDocMap: Map<string, string[]>
|
||||
): FolderHierarchy {
|
||||
const root: FolderHierarchy = {
|
||||
name: '',
|
||||
path: '',
|
||||
children: new Map(),
|
||||
};
|
||||
|
||||
for (const [tag, docIds] of tagDocMap) {
|
||||
const parts = tag.split('/');
|
||||
let current = root;
|
||||
let currentPath = '';
|
||||
|
||||
for (const part of parts) {
|
||||
const parentPath = currentPath;
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
|
||||
if (!current.children.has(part)) {
|
||||
current.children.set(part, {
|
||||
name: part,
|
||||
path: currentPath,
|
||||
parentPath: parentPath || undefined,
|
||||
children: new Map(),
|
||||
});
|
||||
}
|
||||
current = current.children.get(part)!;
|
||||
}
|
||||
|
||||
for (const docId of docIds) {
|
||||
const docNodeKey = `__doc__${docId}`;
|
||||
if (!current.children.has(docNodeKey)) {
|
||||
current.children.set(docNodeKey, {
|
||||
name: docNodeKey,
|
||||
path: `${current.path}/${docNodeKey}`,
|
||||
parentPath: current.path,
|
||||
children: new Map(),
|
||||
pageId: docId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
const GFM_CALLOUT_MAP: Record<string, string> = {
|
||||
IMPORTANT: '\u26A0',
|
||||
NOTE: '\uD83D\uDCDD',
|
||||
WARNING: '\u26A0',
|
||||
TIP: '\uD83D\uDCA1',
|
||||
CAUTION: '\uD83D\uDD34',
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert GFM-style callouts (`> [!NOTE]`, `> [!WARNING]`, etc.) to
|
||||
* emoji-based callouts that AFFiNE's remark-callout plugin understands.
|
||||
* Skips content inside fenced code blocks.
|
||||
*/
|
||||
function convertGfmCallouts(markdown: string): string {
|
||||
const lines = markdown.split('\n');
|
||||
let inCodeBlock = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].trimStart().startsWith('```')) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
continue;
|
||||
}
|
||||
if (!inCodeBlock) {
|
||||
lines[i] = lines[i].replace(
|
||||
/^(>\s*)\[!(\w+)\]/,
|
||||
(_match, prefix: string, type: string) => {
|
||||
const emoji = GFM_CALLOUT_MAP[type.toUpperCase()];
|
||||
return emoji ? `${prefix}[!${emoji}]` : _match;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const HIGHLIGHT_COLOR_MAP: Record<string, string> = {
|
||||
'\uD83D\uDFE2': 'green',
|
||||
'\uD83D\uDD35': 'blue',
|
||||
'\uD83D\uDFE3': 'purple',
|
||||
'\uD83D\uDD34': 'red',
|
||||
'\uD83D\uDFE1': 'yellow',
|
||||
'\uD83D\uDFE0': 'orange',
|
||||
};
|
||||
|
||||
/** Escape HTML special characters to prevent markup injection. */
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Bear `==highlight==` syntax to `<mark>` HTML elements.
|
||||
* Supports colored highlights via leading color emoji (e.g. `==🟢green text==`).
|
||||
* Skips content inside fenced code blocks.
|
||||
*/
|
||||
function convertHighlights(markdown: string): string {
|
||||
const lines = markdown.split('\n');
|
||||
let inCodeBlock = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].trimStart().startsWith('```')) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
continue;
|
||||
}
|
||||
if (!inCodeBlock) {
|
||||
lines[i] = lines[i].replace(
|
||||
/==(\S(?:[^=]|=[^=])*?)==/g,
|
||||
(_match, content: string) => {
|
||||
const firstChar = String.fromCodePoint(content.codePointAt(0)!);
|
||||
const color = HIGHLIGHT_COLOR_MAP[firstChar];
|
||||
if (color) {
|
||||
const text = content.slice(firstChar.length);
|
||||
return `<mark data-color="${color}">${escapeHtml(text)}</mark>`;
|
||||
}
|
||||
return `<mark>${escapeHtml(content)}</mark>`;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** Extract the document title from the first `# heading` or fall back to the bundle name. */
|
||||
function extractTitle(markdown: string, bundleName: string): string {
|
||||
const lines = markdown.split('\n');
|
||||
let inCodeBlock = false;
|
||||
for (const line of lines) {
|
||||
if (line.trimStart().startsWith('```')) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
continue;
|
||||
}
|
||||
if (inCodeBlock) continue;
|
||||
const match = line.match(/^#\s+(.+)/);
|
||||
if (match) {
|
||||
const title = match[1].trim();
|
||||
if (title) return title;
|
||||
}
|
||||
}
|
||||
return bundleName.replace(/\.textbundle$/i, '') || 'Untitled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a Bear .bear2bk backup file.
|
||||
* Uses JSZip for lazy/streaming decompression to handle large backups.
|
||||
*/
|
||||
async function importBearBackup({
|
||||
collection,
|
||||
schema,
|
||||
imported,
|
||||
extensions,
|
||||
}: BearImportOptions): Promise<BearImportResult> {
|
||||
const provider = getProvider(extensions);
|
||||
|
||||
// JSZip reads the zip directory without decompressing all entries
|
||||
const zip = await JSZip.loadAsync(imported);
|
||||
|
||||
// Scan entries and group by textbundle
|
||||
const bundleMap = new Map<string, BundleEntry>();
|
||||
|
||||
zip.forEach((path, _entry) => {
|
||||
if (path.includes('__MACOSX') || path.includes('.DS_Store')) return;
|
||||
|
||||
const tbMatch = path.match(/^(.+?\.textbundle)\/(.*)/i);
|
||||
if (!tbMatch) return;
|
||||
|
||||
const bundlePath = tbMatch[1];
|
||||
const innerPath = tbMatch[2];
|
||||
|
||||
if (!bundleMap.has(bundlePath)) {
|
||||
bundleMap.set(bundlePath, {
|
||||
bundlePath,
|
||||
markdownPath: null,
|
||||
infoJsonPath: null,
|
||||
assetPaths: [],
|
||||
});
|
||||
}
|
||||
const bundle = bundleMap.get(bundlePath)!;
|
||||
|
||||
if (innerPath === 'text.md' || innerPath === 'text.txt') {
|
||||
bundle.markdownPath = path;
|
||||
} else if (innerPath === 'info.json') {
|
||||
bundle.infoJsonPath = path;
|
||||
} else if (innerPath.startsWith('assets/') && innerPath !== 'assets/') {
|
||||
bundle.assetPaths.push(path);
|
||||
}
|
||||
});
|
||||
|
||||
// Read info.json for all bundles to filter out trashed notes
|
||||
// (info.json is tiny, safe to read all at once)
|
||||
const validBundles: Array<{
|
||||
entry: BundleEntry;
|
||||
bearMeta: Record<string, unknown> | undefined;
|
||||
}> = [];
|
||||
|
||||
for (const entry of bundleMap.values()) {
|
||||
if (!entry.markdownPath) continue;
|
||||
|
||||
let info: Record<string, unknown> = {};
|
||||
if (entry.infoJsonPath) {
|
||||
try {
|
||||
const text = await zip.file(entry.infoJsonPath)!.async('string');
|
||||
info = JSON.parse(text);
|
||||
} catch {
|
||||
// Invalid JSON
|
||||
}
|
||||
}
|
||||
|
||||
const bearMeta = info['net.shinyfrog.bear'] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
if (bearMeta?.trashed === 1) continue;
|
||||
|
||||
validBundles.push({ entry, bearMeta });
|
||||
}
|
||||
|
||||
if (validBundles.length === 0) {
|
||||
throw new Error(
|
||||
'No valid Bear textbundles found in the archive. Please select a .bear2bk backup file.'
|
||||
);
|
||||
}
|
||||
|
||||
const docIds: string[] = [];
|
||||
const tagDocMap = new Map<string, string[]>();
|
||||
|
||||
// Process bundles sequentially to limit memory.
|
||||
// Each bundle is wrapped in try/catch so one bad note does not abort the
|
||||
// entire import after earlier notes have already been written.
|
||||
for (const { entry, bearMeta } of validBundles) {
|
||||
try {
|
||||
// Read markdown (decompress on demand)
|
||||
const rawMarkdown = await zip.file(entry.markdownPath!)!.async('string');
|
||||
if (!rawMarkdown.trim()) continue;
|
||||
|
||||
const { tags, content: cleanedMarkdown } = parseBearTags(rawMarkdown);
|
||||
const bundleDirName =
|
||||
entry.bundlePath.split('/').findLast(Boolean) ?? 'Untitled';
|
||||
const title = extractTitle(cleanedMarkdown, bundleDirName);
|
||||
const markdown = convertHighlights(
|
||||
convertGfmCallouts(
|
||||
cleanedMarkdown.replace(/<!--\s*\{[^}]*\}\s*-->/g, '')
|
||||
)
|
||||
);
|
||||
|
||||
// Read assets on demand (decompress only this bundle's assets)
|
||||
const pendingAssets = new Map<string, File>();
|
||||
const pendingPathBlobIdMap = new Map<string, string>();
|
||||
|
||||
for (const assetFullPath of entry.assetPaths) {
|
||||
try {
|
||||
const data = await zip.file(assetFullPath)!.async('arraybuffer');
|
||||
const tbMatch = assetFullPath.match(/^.+?\.textbundle\/(.*)/i);
|
||||
const assetRelPath = tbMatch ? tbMatch[1] : assetFullPath;
|
||||
const ext = assetRelPath.split('.').at(-1) ?? '';
|
||||
const mime = extMimeMap.get(ext.toLowerCase()) ?? '';
|
||||
const key = await sha(data);
|
||||
// Map both the full zip path and the relative path (assets/...)
|
||||
pendingPathBlobIdMap.set(assetFullPath, key);
|
||||
pendingPathBlobIdMap.set(assetRelPath, key);
|
||||
try {
|
||||
const decodedRel = decodeURIComponent(assetRelPath);
|
||||
if (decodedRel !== assetRelPath) {
|
||||
pendingPathBlobIdMap.set(decodedRel, key);
|
||||
}
|
||||
const decodedFull = decodeURIComponent(assetFullPath);
|
||||
if (decodedFull !== assetFullPath) {
|
||||
pendingPathBlobIdMap.set(decodedFull, key);
|
||||
}
|
||||
} catch {
|
||||
// Invalid URI encoding
|
||||
}
|
||||
const fileName = assetRelPath.split('/').pop() ?? '';
|
||||
pendingAssets.set(key, new File([data], fileName, { type: mime }));
|
||||
} catch {
|
||||
// Failed to read asset, skip
|
||||
}
|
||||
}
|
||||
|
||||
const fullPath = `${entry.bundlePath}/text.md`;
|
||||
const job = new Transformer({
|
||||
schema,
|
||||
blobCRUD: collection.blobSync,
|
||||
docCRUD: createCollectionDocCRUD(collection),
|
||||
middlewares: [
|
||||
defaultImageProxyMiddleware,
|
||||
fileNameMiddleware(title),
|
||||
filePathMiddleware(fullPath),
|
||||
docLinkBaseURLMiddleware(collection.id),
|
||||
],
|
||||
});
|
||||
|
||||
const assets = job.assets;
|
||||
const pathBlobIdMap = job.assetsManager.getPathBlobIdMap();
|
||||
for (const [p, key] of pendingPathBlobIdMap.entries()) {
|
||||
pathBlobIdMap.set(p, key);
|
||||
}
|
||||
for (const [key, file] of pendingAssets.entries()) {
|
||||
assets.set(key, file);
|
||||
}
|
||||
|
||||
const mdAdapter = new MarkdownAdapter(job, provider);
|
||||
const doc = await mdAdapter.toDoc({
|
||||
file: markdown,
|
||||
assets: job.assetsManager,
|
||||
});
|
||||
|
||||
if (doc) {
|
||||
docIds.push(doc.id);
|
||||
|
||||
const metaPatch: Record<string, unknown> = {};
|
||||
if (bearMeta?.creationDate) {
|
||||
const ts = Date.parse(String(bearMeta.creationDate));
|
||||
if (!isNaN(ts)) metaPatch.createDate = ts;
|
||||
}
|
||||
if (bearMeta?.modificationDate) {
|
||||
const ts = Date.parse(String(bearMeta.modificationDate));
|
||||
if (!isNaN(ts)) metaPatch.updatedDate = ts;
|
||||
}
|
||||
if (Object.keys(metaPatch).length) {
|
||||
collection.meta.setDocMeta(doc.id, metaPatch);
|
||||
}
|
||||
|
||||
for (const tag of tags) {
|
||||
if (!tagDocMap.has(tag)) {
|
||||
tagDocMap.set(tag, []);
|
||||
}
|
||||
tagDocMap.get(tag)!.push(doc.id);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to import bundle: ${entry.bundlePath}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
const folderHierarchy = buildFolderHierarchyFromTags(tagDocMap);
|
||||
return { docIds, tags: tagDocMap, folderHierarchy };
|
||||
}
|
||||
|
||||
/** Public API for importing Bear .bear2bk backup archives. */
|
||||
export const BearTransformer = {
|
||||
importBearBackup,
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export { BearTransformer } from './bear.js';
|
||||
export { DocxTransformer } from './docx.js';
|
||||
export { HtmlTransformer } from './html.js';
|
||||
export { MarkdownTransformer } from './markdown.js';
|
||||
|
||||
@@ -462,12 +462,23 @@ async function importMarkdownToDoc({
|
||||
* @param options.imported The zip file as a Blob
|
||||
* @returns A Promise that resolves to an array of IDs of the newly created docs
|
||||
*/
|
||||
type FolderHierarchy = {
|
||||
name: string;
|
||||
path: string;
|
||||
children: Map<string, FolderHierarchy>;
|
||||
pageId?: string;
|
||||
parentPath?: string;
|
||||
};
|
||||
|
||||
async function importMarkdownZip({
|
||||
collection,
|
||||
schema,
|
||||
imported,
|
||||
extensions,
|
||||
}: ImportMarkdownZipOptions) {
|
||||
}: ImportMarkdownZipOptions): Promise<{
|
||||
docIds: string[];
|
||||
folderHierarchy?: FolderHierarchy;
|
||||
}> {
|
||||
const provider = getProvider(extensions);
|
||||
const unzip = new Unzip();
|
||||
await unzip.load(imported);
|
||||
@@ -476,6 +487,7 @@ async function importMarkdownZip({
|
||||
const pendingAssets: AssetMap = new Map();
|
||||
const pendingPathBlobIdMap: PathBlobIdMap = new Map();
|
||||
const markdownBlobs: ImportedFileEntry[] = [];
|
||||
const docPathMap: Array<{ fullPath: string; docId: string }> = [];
|
||||
|
||||
// Iterate over all files in the zip
|
||||
for (const { path, content: blob } of unzip) {
|
||||
@@ -527,10 +539,94 @@ async function importMarkdownZip({
|
||||
if (doc) {
|
||||
applyMetaPatch(collection, doc.id, meta);
|
||||
docIds.push(doc.id);
|
||||
docPathMap.push({ fullPath, docId: doc.id });
|
||||
}
|
||||
})
|
||||
);
|
||||
return docIds;
|
||||
|
||||
// Build folder hierarchy from zip paths
|
||||
const folderHierarchy = buildMarkdownZipFolderHierarchy(docPathMap);
|
||||
|
||||
return { docIds, folderHierarchy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a tree of {@link FolderHierarchy} nodes from the zip paths of
|
||||
* imported markdown files. Returns `undefined` when every entry sits at
|
||||
* the same level (no real subfolder structure). A common root directory
|
||||
* shared by all entries is stripped automatically so that the resulting
|
||||
* hierarchy starts one level deeper.
|
||||
*/
|
||||
function buildMarkdownZipFolderHierarchy(
|
||||
entries: Array<{ fullPath: string; docId: string }>
|
||||
): FolderHierarchy | undefined {
|
||||
if (entries.length === 0) return undefined;
|
||||
|
||||
// Check if any entries have folder structure
|
||||
const hasSubfolders = entries.some(e => {
|
||||
const parts = e.fullPath.split('/').filter(Boolean);
|
||||
// More than just "root/file.md" -- need at least one real subfolder
|
||||
return parts.length > 2;
|
||||
});
|
||||
if (!hasSubfolders) {
|
||||
// All files are at the same level, no folder hierarchy needed
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const root: FolderHierarchy = {
|
||||
name: '',
|
||||
path: '',
|
||||
children: new Map(),
|
||||
};
|
||||
|
||||
// Check once whether all entries share a common root directory
|
||||
const candidateRoot = entries[0]?.fullPath.split('/').find(Boolean);
|
||||
const skipRoot =
|
||||
!!candidateRoot &&
|
||||
entries.every(e => e.fullPath.startsWith(candidateRoot + '/'));
|
||||
|
||||
for (const { fullPath, docId } of entries) {
|
||||
const parts = fullPath.split('/').filter(Boolean);
|
||||
const fileName = parts.pop(); // Remove filename
|
||||
if (!fileName) continue;
|
||||
|
||||
let folderParts = skipRoot ? parts.slice(1) : parts;
|
||||
|
||||
if (folderParts.length === 0) {
|
||||
// Root-level file, no folder needed
|
||||
continue;
|
||||
}
|
||||
|
||||
let current = root;
|
||||
let currentPath = '';
|
||||
|
||||
for (const folderName of folderParts) {
|
||||
const parentPath = currentPath;
|
||||
currentPath = currentPath ? `${currentPath}/${folderName}` : folderName;
|
||||
|
||||
if (!current.children.has(folderName)) {
|
||||
current.children.set(folderName, {
|
||||
name: folderName,
|
||||
path: currentPath,
|
||||
parentPath: parentPath || undefined,
|
||||
children: new Map(),
|
||||
});
|
||||
}
|
||||
current = current.children.get(folderName)!;
|
||||
}
|
||||
|
||||
// Add the doc as a leaf
|
||||
const docNodeKey = `__doc__${docId}`;
|
||||
current.children.set(docNodeKey, {
|
||||
name: docNodeKey,
|
||||
path: `${current.path}/${docNodeKey}`,
|
||||
parentPath: current.path,
|
||||
children: new Map(),
|
||||
pageId: docId,
|
||||
});
|
||||
}
|
||||
|
||||
return root.children.size > 0 ? root : undefined;
|
||||
}
|
||||
|
||||
export const MarkdownTransformer = {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"playwright": "=1.58.2",
|
||||
"playwright": "=1.59.1",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"devDependencies": {
|
||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"playwright": "=1.58.2",
|
||||
"playwright": "=1.59.1",
|
||||
"vite": "^7.2.7",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"vitest": "^4.0.18"
|
||||
|
||||
@@ -436,7 +436,7 @@ export class StarterDebugMenu extends ShadowlessElement {
|
||||
try {
|
||||
const file = await openSingleFileWith('Zip');
|
||||
if (!file) return;
|
||||
const result = await MarkdownTransformer.importMarkdownZip({
|
||||
const { docIds } = await MarkdownTransformer.importMarkdownZip({
|
||||
collection: this.collection,
|
||||
schema: this.editor.doc.schema,
|
||||
imported: file,
|
||||
@@ -445,7 +445,7 @@ export class StarterDebugMenu extends ShadowlessElement {
|
||||
if (!this.editor.host) return;
|
||||
toast(
|
||||
this.editor.host,
|
||||
`Successfully imported ${result.length} markdown files.`
|
||||
`Successfully imported ${docIds.length} markdown files.`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Import markdown zip files failed:', error);
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"@magic-works/i18n-codegen": "^0.6.1",
|
||||
"@playwright/test": "=1.58.2",
|
||||
"@playwright/test": "=1.59.1",
|
||||
"@smarttools/eslint-plugin-rxjs": "^1.0.8",
|
||||
"@taplo/cli": "^0.7.0",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
@@ -93,7 +93,7 @@
|
||||
"vite": "^7.2.7",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"packageManager": "yarn@4.13.0",
|
||||
"packageManager": "yarn@4.14.1",
|
||||
"resolutions": {
|
||||
"array-buffer-byte-length": "npm:@nolyfill/array-buffer-byte-length@^1",
|
||||
"array-includes": "npm:@nolyfill/array-includes@^1",
|
||||
@@ -167,7 +167,7 @@
|
||||
"typedarray": "npm:@nolyfill/typedarray@^1",
|
||||
"macos-alias": "npm:@napi-rs/macos-alias@0.0.4",
|
||||
"fs-xattr": "npm:@napi-rs/xattr@latest",
|
||||
"ioredis": "5.8.2",
|
||||
"ioredis": "5.10.1",
|
||||
"decode-named-character-reference@npm:^1.0.0": "patch:decode-named-character-reference@npm%3A1.0.2#~/.yarn/patches/decode-named-character-reference-npm-1.0.2-db17a755fd.patch",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "patch:@atlaskit/pragmatic-drag-and-drop@npm%3A1.4.0#~/.yarn/patches/@atlaskit-pragmatic-drag-and-drop-npm-1.4.0-75c45f52d3.patch",
|
||||
"yjs": "patch:yjs@npm%3A13.6.21#~/.yarn/patches/yjs-npm-13.6.21-c9f1f3397c.patch"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"build:debug": "napi build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.5.0",
|
||||
"@napi-rs/cli": "3.6.2",
|
||||
"tiktoken": "^1.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"@queuedash/api": "^3.16.0",
|
||||
"@react-email/components": "^0.5.7",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"bullmq": "5.53.0",
|
||||
"bullmq": "5.76.6",
|
||||
"commander": "^13.1.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cross-env": "^10.1.0",
|
||||
@@ -81,7 +81,7 @@
|
||||
"graphql-scalars": "^1.24.0",
|
||||
"graphql-upload": "^17.0.0",
|
||||
"html-validate": "^9.0.0",
|
||||
"htmlrewriter": "^0.0.12",
|
||||
"htmlrewriter": "^0.0.13",
|
||||
"http-errors": "^2.0.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"is-mobile": "^5.0.0",
|
||||
@@ -97,7 +97,7 @@
|
||||
"piscina": "^5.1.4",
|
||||
"prisma": "^6.6.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react-dom": "19.2.6",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"semver": "^7.7.4",
|
||||
|
||||
379
packages/common/y-octo/utils/fuzz/Cargo.lock
generated
379
packages/common/y-octo/utils/fuzz/Cargo.lock
generated
@@ -74,6 +74,12 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.1"
|
||||
@@ -168,6 +174,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.38"
|
||||
@@ -224,6 +241,15 @@ dependencies = [
|
||||
"loom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -238,7 +264,7 @@ checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
@@ -255,6 +281,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.0"
|
||||
@@ -286,6 +318,12 @@ dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.4"
|
||||
@@ -320,22 +358,69 @@ checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 5.2.0",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
@@ -374,6 +459,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lib0"
|
||||
version = "0.16.10"
|
||||
@@ -393,9 +484,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.9"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75"
|
||||
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
@@ -455,11 +546,11 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "nanoid"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
||||
checksum = "8628de41fe064cc3f0cf07f3d299ee3e73521adaff72278731d5c8cae3797873"
|
||||
dependencies = [
|
||||
"rand 0.8.6",
|
||||
"rand 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -530,29 +621,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"phf_shared",
|
||||
"rand 0.8.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.3"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
@@ -563,9 +655,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
@@ -585,6 +677,16 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
@@ -610,15 +712,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
@@ -631,13 +728,14 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.4",
|
||||
"chacha20",
|
||||
"getrandom 0.4.2",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -651,12 +749,13 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
name = "rand_chacha"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"ppv-lite86",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -669,13 +768,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_distr"
|
||||
version = "0.5.1"
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rand_distr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand 0.9.4",
|
||||
"rand 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -729,19 +834,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -814,9 +935,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -928,6 +1049,12 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
@@ -961,6 +1088,24 @@ dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.57.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.51.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
@@ -1018,6 +1163,40 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
@@ -1155,6 +1334,32 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
@@ -1164,6 +1369,74 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "y-octo"
|
||||
version = "0.0.2"
|
||||
@@ -1177,8 +1450,8 @@ dependencies = [
|
||||
"nanoid",
|
||||
"nom",
|
||||
"ordered-float",
|
||||
"rand 0.9.4",
|
||||
"rand_chacha 0.9.0",
|
||||
"rand 0.10.1",
|
||||
"rand_chacha 0.10.0",
|
||||
"rand_distr",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1192,8 +1465,8 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"lib0",
|
||||
"libfuzzer-sys",
|
||||
"rand 0.9.4",
|
||||
"rand_chacha 0.9.0",
|
||||
"rand 0.10.1",
|
||||
"rand_chacha 0.10.0",
|
||||
"y-octo",
|
||||
"y-octo-utils",
|
||||
"yrs",
|
||||
@@ -1207,17 +1480,17 @@ dependencies = [
|
||||
"clap",
|
||||
"lib0",
|
||||
"phf",
|
||||
"rand 0.9.4",
|
||||
"rand_chacha 0.9.0",
|
||||
"rand 0.10.1",
|
||||
"rand_chacha 0.10.0",
|
||||
"y-octo",
|
||||
"yrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yrs"
|
||||
version = "0.23.1"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a7cab84724ae7f361a8c92465f5160922cbb941a499e1a8cacd103351ab9c78"
|
||||
checksum = "89512f2d869f9947e1c58d57ef86c8f4ca1b1e8ccf24d6e1ff8c7cdbd67d54df"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-lock",
|
||||
@@ -1228,7 +1501,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"smallstr",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -10,9 +10,9 @@ version = "0.0.0"
|
||||
[dependencies]
|
||||
lib0 = "=0.16.10"
|
||||
libfuzzer-sys = "0.4"
|
||||
rand = "0.9"
|
||||
rand_chacha = "0.9"
|
||||
yrs = "=0.23.1"
|
||||
rand = "0.10"
|
||||
rand_chacha = "0.10"
|
||||
yrs = "=0.26.0"
|
||||
|
||||
y-octo-utils = { path = "..", features = ["fuzz"] }
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"embla-carousel-react": "^8.5.1",
|
||||
"input-otp": "^1.4.1",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lucide-react": "^0.508.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^19.2.1",
|
||||
"react-day-picker": "^9.4.3",
|
||||
|
||||
@@ -9,7 +9,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.10.0'
|
||||
classpath 'com.android.tools.build:gradle:8.13.2'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
[versions]
|
||||
android-gradle-plugin = "8.10.0"
|
||||
androidx-activity-compose = "1.10.1"
|
||||
androidx-appcompat = "1.7.0"
|
||||
androidx-browser = "1.8.0"
|
||||
androidx-compose-bom = "2025.05.00"
|
||||
android-gradle-plugin = "8.13.2"
|
||||
androidx-activity-compose = "1.13.0"
|
||||
androidx-appcompat = "1.7.1"
|
||||
androidx-browser = "1.10.0"
|
||||
androidx-compose-bom = "2025.12.01"
|
||||
androidx-coordinatorlayout = "1.3.0"
|
||||
androidx-core-ktx = "1.16.0"
|
||||
androidx-core-splashscreen = "1.0.1"
|
||||
androidx-datastore-preferences = "1.2.0-alpha02"
|
||||
androidx-espresso-core = "3.6.1"
|
||||
androidx-junit = "1.2.1"
|
||||
androidx-lifecycle-compose = "2.9.0"
|
||||
androidx-core-ktx = "1.18.0"
|
||||
androidx-core-splashscreen = "1.2.0"
|
||||
androidx-datastore-preferences = "1.2.1"
|
||||
androidx-espresso-core = "3.7.0"
|
||||
androidx-junit = "1.3.0"
|
||||
androidx-lifecycle-compose = "2.10.0"
|
||||
androidx-material3 = "1.3.1"
|
||||
androidx-navigation = "2.9.0"
|
||||
apollo = "4.4.2"
|
||||
apollo-kotlin-adapters = "0.0.6"
|
||||
androidx-navigation = "2.9.8"
|
||||
apollo = "4.4.3"
|
||||
apollo-kotlin-adapters = "0.7.0"
|
||||
# @keep
|
||||
compileSdk = "36"
|
||||
firebase-bom = "33.13.0"
|
||||
firebase-crashlytics = "3.0.3"
|
||||
google-services = "4.4.2"
|
||||
gradle-versions = "0.52.0"
|
||||
hilt = "2.56.2"
|
||||
hilt-ext = "1.2.0"
|
||||
jna = "5.17.0"
|
||||
firebase-bom = "33.16.0"
|
||||
firebase-crashlytics = "3.0.7"
|
||||
google-services = "4.4.4"
|
||||
gradle-versions = "0.54.0"
|
||||
hilt = "2.59.2"
|
||||
hilt-ext = "1.3.0"
|
||||
jna = "5.18.1"
|
||||
junit = "4.13.2"
|
||||
kotlin = "2.1.20"
|
||||
kotlin = "2.3.21"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
kotlinx-datetime = "0.6.2"
|
||||
kotlinx-serialization-json = "1.8.1"
|
||||
ksp = "2.1.20-2.0.1"
|
||||
kotlinx-datetime = "0.7.1-0.6.x-compat"
|
||||
kotlinx-serialization-json = "1.11.0"
|
||||
ksp = "2.3.7"
|
||||
# @keep
|
||||
minSdk = "23"
|
||||
mozilla-rust-android = "0.9.6"
|
||||
okhttp-bom = "5.0.0-alpha.14"
|
||||
richtext = "1.0.0-alpha02"
|
||||
okhttp-bom = "5.3.2"
|
||||
richtext = "1.0.0-alpha04"
|
||||
# @keep
|
||||
targetSdk = "35"
|
||||
timber = "5.0.1"
|
||||
version-catalog-update = "1.0.0"
|
||||
version-catalog-update = "1.1.0"
|
||||
|
||||
[libraries]
|
||||
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-squirrel-startup": "1.0.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild": "^0.28.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "^11.0.0",
|
||||
"lodash-es": "^4.17.23",
|
||||
|
||||
@@ -21,7 +21,7 @@ end
|
||||
target 'AFFiNE' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
pod 'CryptoSwift', '~> 1.8.3'
|
||||
pod 'CryptoSwift', '~> 1.10.0'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"emojibase-data": "^16.0.3",
|
||||
"foxact": "^0.2.49",
|
||||
"foxact": "^0.3.0",
|
||||
"jotai": "^2.10.3",
|
||||
"lit": "^3.2.1",
|
||||
"lodash-es": "^4.17.23",
|
||||
@@ -60,7 +60,7 @@
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react-dom": "19.2.6",
|
||||
"react-paginate": "^8.3.0",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"react-transition-state": "^2.2.0",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@affine/reader": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/track": "workspace:*",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@blocksuite/affine": "workspace:*",
|
||||
"@blocksuite/affine-block-root": "workspace:*",
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
@@ -58,7 +59,7 @@
|
||||
"eventemitter2": "^6.4.9",
|
||||
"file-type": "^21.0.0",
|
||||
"filesize": "^10.1.6",
|
||||
"foxact": "^0.2.49",
|
||||
"foxact": "^0.3.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"graphql": "^16.9.0",
|
||||
@@ -67,7 +68,7 @@
|
||||
"image-blob-reduce": "^4.1.0",
|
||||
"is-svg": "^6.1.0",
|
||||
"jotai": "^2.10.3",
|
||||
"jotai-scope": "^0.7.2",
|
||||
"jotai-scope": "^0.10.0",
|
||||
"katex": "^0.16.27",
|
||||
"lit": "^3.2.1",
|
||||
"lodash-es": "^4.17.23",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { toast } from '@affine/component';
|
||||
import type { TagMeta } from '@affine/core/components/page-list';
|
||||
import type { CollectionMeta } from '@affine/core/modules/collection';
|
||||
import track, { type EventArgs } from '@affine/track';
|
||||
@@ -22,6 +21,7 @@ import { property, query, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { SearchMenuConfig } from '../ai-chat-add-context';
|
||||
import { addFilesToChat } from './attachment-utils';
|
||||
import type { ChatChip, DocDisplayConfig } from './type';
|
||||
|
||||
enum AddPopoverMode {
|
||||
@@ -172,23 +172,10 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
this.abortController.abort();
|
||||
const images = files.filter(file => file.type.startsWith('image/'));
|
||||
if (images.length > 0) {
|
||||
this.addImages(images);
|
||||
}
|
||||
|
||||
const others = files.filter(file => !file.type.startsWith('image/'));
|
||||
const addChipPromises = others.map(async file => {
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
toast(`${file.name} is too large, please upload a file less than 50MB`);
|
||||
return;
|
||||
}
|
||||
await this.addChip({
|
||||
file,
|
||||
state: 'processing',
|
||||
});
|
||||
await addFilesToChat(files, {
|
||||
addImages: this.addImages,
|
||||
addChip: this.addChip,
|
||||
});
|
||||
await Promise.all(addChipPromises);
|
||||
this._track('file');
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { toast } from '@affine/component';
|
||||
|
||||
import type { ChatChip } from './type';
|
||||
|
||||
const MAX_ATTACHMENT_SIZE = 50 * 1024 * 1024;
|
||||
|
||||
export interface AttachmentHandlers {
|
||||
addImages: (images: File[]) => void;
|
||||
addChip: (chip: ChatChip, silent?: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export async function addFilesToChat(
|
||||
files: File[],
|
||||
{ addImages, addChip }: AttachmentHandlers
|
||||
): Promise<void> {
|
||||
if (!files.length) return;
|
||||
|
||||
const images = files.filter(file => file.type.startsWith('image/'));
|
||||
if (images.length > 0) {
|
||||
addImages(images);
|
||||
}
|
||||
|
||||
const others = files.filter(file => !file.type.startsWith('image/'));
|
||||
await Promise.all(
|
||||
others.map(async file => {
|
||||
if (file.size > MAX_ATTACHMENT_SIZE) {
|
||||
toast(`${file.name} is too large, please upload a file less than 50MB`);
|
||||
return;
|
||||
}
|
||||
await addChip({
|
||||
file,
|
||||
state: 'processing',
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './attachment-utils';
|
||||
export * from './type';
|
||||
export * from './utils';
|
||||
|
||||
@@ -9,6 +9,9 @@ import type {
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import track, { type EventArgs } from '@affine/track';
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
@@ -26,6 +29,7 @@ import { reportResponse } from '../../utils/action-reporter';
|
||||
import { readBlobAsURL } from '../../utils/image';
|
||||
import { mergeStreamObjects } from '../../utils/stream-objects';
|
||||
import type { SearchMenuConfig } from '../ai-chat-add-context';
|
||||
import { addFilesToChat } from '../ai-chat-chips/attachment-utils';
|
||||
import type { ChatChip, DocDisplayConfig } from '../ai-chat-chips/type';
|
||||
import { isDocChip } from '../ai-chat-chips/utils';
|
||||
import {
|
||||
@@ -257,6 +261,31 @@ export class AIChatInput extends SignalWatcher(
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.chat-panel-input[data-drag-over='true'] {
|
||||
--input-border-width: 1px;
|
||||
--input-border-color: var(--affine-v2-layer-insideBorder-primaryBorder);
|
||||
background-color: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||
}
|
||||
|
||||
.chat-panel-input-drop-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${unsafeCSSVarV2('icon/activated')};
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--affine-v2-layer-background-primary) 92%,
|
||||
transparent
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.chat-panel-send {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -326,6 +355,16 @@ export class AIChatInput extends SignalWatcher(
|
||||
@state()
|
||||
accessor focused = false;
|
||||
|
||||
@state()
|
||||
accessor isDragOver = false;
|
||||
|
||||
@query('.chat-panel-input')
|
||||
accessor chatPanelInput!: HTMLDivElement;
|
||||
|
||||
private _dragEnterCounter = 0;
|
||||
|
||||
private _internalDropCleanup: (() => void) | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor chatContextValue!: AIChatInputContext;
|
||||
|
||||
@@ -434,6 +473,18 @@ export class AIChatInput extends SignalWatcher(
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
if (this.isConnected && !this._internalDropCleanup) {
|
||||
this._setupInternalDropTarget();
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
window.addEventListener('dragleave', this._handleWindowDragLeave);
|
||||
window.addEventListener('drop', this._resetDragState);
|
||||
window.addEventListener('dragend', this._resetDragState);
|
||||
}
|
||||
|
||||
protected override firstUpdated(changedProperties: PropertyValues): void {
|
||||
@@ -449,6 +500,57 @@ export class AIChatInput extends SignalWatcher(
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._internalDropCleanup?.();
|
||||
this._internalDropCleanup = null;
|
||||
window.removeEventListener('dragleave', this._handleWindowDragLeave);
|
||||
window.removeEventListener('drop', this._resetDragState);
|
||||
window.removeEventListener('dragend', this._resetDragState);
|
||||
}
|
||||
|
||||
private _trackDragDrop(method: EventArgs['addEmbeddingDoc']['method']) {
|
||||
const page = this.independentMode
|
||||
? track.$.intelligence
|
||||
: track.$.chatPanel;
|
||||
page.chatPanelInput.addEmbeddingDoc({
|
||||
control: 'dragDrop',
|
||||
method,
|
||||
});
|
||||
}
|
||||
|
||||
private _setupInternalDropTarget() {
|
||||
const el = this.chatPanelInput;
|
||||
if (!el) return;
|
||||
const dropTargetCleanup = dropTargetForElements({
|
||||
element: el,
|
||||
canDrop: ({ source }) => {
|
||||
const entity = (source.data as { entity?: { type?: string } }).entity;
|
||||
return entity?.type === 'doc';
|
||||
},
|
||||
onDragEnter: () => {
|
||||
this.isDragOver = true;
|
||||
},
|
||||
onDragLeave: () => {
|
||||
this.isDragOver = false;
|
||||
},
|
||||
onDrop: ({ source }) => {
|
||||
this.isDragOver = false;
|
||||
const entity = (
|
||||
source.data as { entity?: { type?: string; id?: string } }
|
||||
).entity;
|
||||
if (entity?.type === 'doc' && entity.id) {
|
||||
this.addChip({
|
||||
docId: entity.id,
|
||||
state: 'processing',
|
||||
}).catch(console.error);
|
||||
this._trackDragDrop('doc');
|
||||
}
|
||||
},
|
||||
});
|
||||
this._internalDropCleanup = combine(dropTargetCleanup);
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
const { images, status } = this.chatContextValue;
|
||||
const hasImages = images.length > 0;
|
||||
@@ -458,11 +560,19 @@ export class AIChatInput extends SignalWatcher(
|
||||
class="chat-panel-input"
|
||||
data-independent-mode=${this.independentMode}
|
||||
data-if-focused=${this.focused}
|
||||
data-drag-over=${this.isDragOver}
|
||||
style=${styleMap({
|
||||
maxHeight: `${maxHeight}px !important`,
|
||||
})}
|
||||
@pointerdown=${this._handlePointerDown}
|
||||
@dragenter=${this._handleDragEnter}
|
||||
@dragover=${this._handleDragOver}
|
||||
@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
|
||||
@@ -611,6 +721,66 @@ export class AIChatInput extends SignalWatcher(
|
||||
}
|
||||
};
|
||||
|
||||
private _dragHasFiles(event: DragEvent) {
|
||||
return Array.from(event.dataTransfer?.types ?? []).includes('Files');
|
||||
}
|
||||
|
||||
private readonly _handleDragEnter = (event: DragEvent) => {
|
||||
if (!this._dragHasFiles(event)) return;
|
||||
event.preventDefault();
|
||||
this._dragEnterCounter += 1;
|
||||
this.isDragOver = true;
|
||||
};
|
||||
|
||||
private readonly _handleDragOver = (event: DragEvent) => {
|
||||
if (!this._dragHasFiles(event)) return;
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _handleDragLeave = (event: DragEvent) => {
|
||||
if (!this._dragHasFiles(event)) return;
|
||||
this._dragEnterCounter = Math.max(0, this._dragEnterCounter - 1);
|
||||
if (this._dragEnterCounter === 0) {
|
||||
this.isDragOver = false;
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _resetDragState = () => {
|
||||
if (this._dragEnterCounter === 0 && !this.isDragOver) return;
|
||||
this._dragEnterCounter = 0;
|
||||
this.isDragOver = false;
|
||||
};
|
||||
|
||||
// Covers the cases where the drag session ends without dragleave/drop firing
|
||||
// on the input (Esc-cancel, release outside window, drop on another element).
|
||||
private readonly _handleWindowDragLeave = (event: DragEvent) => {
|
||||
if (event.relatedTarget === null) this._resetDragState();
|
||||
};
|
||||
|
||||
private readonly _handleDrop = async (event: DragEvent) => {
|
||||
if (!this._dragHasFiles(event)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this._dragEnterCounter = 0;
|
||||
this.isDragOver = false;
|
||||
|
||||
const files = Array.from(event.dataTransfer?.files ?? []);
|
||||
if (!files.length) return;
|
||||
|
||||
try {
|
||||
await addFilesToChat(files, {
|
||||
addImages: this.addImages,
|
||||
addChip: this.addChip,
|
||||
});
|
||||
this._trackDragDrop('file');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _handleAbort = () => {
|
||||
this.chatContextValue.abortController?.abort();
|
||||
this.updateContext({ status: 'success' });
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import { CloseIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, type PropertyValues } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
const DEFAULT_TAB_TITLE = 'New chat';
|
||||
const TITLE_MAX_LENGTH = 28;
|
||||
|
||||
function truncate(text: string): string {
|
||||
if (text.length <= TITLE_MAX_LENGTH) return text;
|
||||
return `${text.slice(0, TITLE_MAX_LENGTH).trimEnd()}…`;
|
||||
}
|
||||
|
||||
function deriveTabTitle(session: CopilotChatHistoryFragment): string {
|
||||
const explicit = session.title?.trim();
|
||||
if (explicit) return truncate(explicit);
|
||||
const firstUserMessage = session.messages?.find(m => m.role === 'user');
|
||||
const raw = firstUserMessage?.content?.trim();
|
||||
if (!raw) return DEFAULT_TAB_TITLE;
|
||||
const newlineIdx = raw.indexOf('\n');
|
||||
return truncate(newlineIdx === -1 ? raw : raw.slice(0, newlineIdx));
|
||||
}
|
||||
|
||||
export class AIChatTabs extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor sessions: CopilotChatHistoryFragment[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor activeSessionId: string | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onSelectTab!: (sessionId: string) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onCloseTab!: (sessionId: string) => void;
|
||||
|
||||
static override styles = css`
|
||||
ai-chat-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ai-chat-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tabs-scroll {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.tabs-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
max-width: 180px;
|
||||
height: 26px;
|
||||
padding: 0 6px 0 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: ${unsafeCSSVarV2('text/secondary')};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background-color: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||
color: ${unsafeCSSVarV2('text/primary')};
|
||||
}
|
||||
|
||||
.tab[data-active='true'] {
|
||||
background-color: ${unsafeCSSVarV2('layer/background/secondary')};
|
||||
color: ${unsafeCSSVarV2('text/primary')};
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.tab-close:hover {
|
||||
opacity: 1;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||
}
|
||||
.tab-close svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
if (!this.sessions.length) return html``;
|
||||
return html`
|
||||
<div class="ai-chat-tabs" data-testid="ai-chat-tabs">
|
||||
<div class="tabs-scroll" @wheel=${this._handleWheel}>
|
||||
${repeat(
|
||||
this.sessions,
|
||||
session => session.sessionId,
|
||||
session => this._renderTab(session)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private readonly _handleWheel = (e: WheelEvent) => {
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
if (el.scrollWidth <= el.clientWidth) return;
|
||||
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
||||
e.preventDefault();
|
||||
el.scrollLeft += e.deltaY;
|
||||
}
|
||||
};
|
||||
|
||||
private _renderTab(session: CopilotChatHistoryFragment) {
|
||||
const active = session.sessionId === this.activeSessionId;
|
||||
const title = deriveTabTitle(session);
|
||||
return html`
|
||||
<div
|
||||
class="tab"
|
||||
data-active=${active}
|
||||
data-session-id=${session.sessionId}
|
||||
data-testid="ai-chat-tab"
|
||||
title=${title}
|
||||
@click=${() => this._handleSelect(session.sessionId)}
|
||||
>
|
||||
<span class="tab-title">${title}</span>
|
||||
<button
|
||||
class="tab-close"
|
||||
data-testid="ai-chat-tab-close"
|
||||
aria-label="Close tab"
|
||||
@click=${(e: Event) => this._handleClose(e, session.sessionId)}
|
||||
>
|
||||
${CloseIcon()}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private readonly _handleSelect = (sessionId: string) => {
|
||||
if (sessionId === this.activeSessionId) return;
|
||||
this.onSelectTab(sessionId);
|
||||
};
|
||||
|
||||
private readonly _handleClose = (e: Event, sessionId: string) => {
|
||||
e.stopPropagation();
|
||||
this.onCloseTab(sessionId);
|
||||
};
|
||||
|
||||
override updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (
|
||||
(changedProps.has('activeSessionId') || changedProps.has('sessions')) &&
|
||||
this.activeSessionId
|
||||
) {
|
||||
const activeTab = this.renderRoot.querySelector(
|
||||
`[data-session-id="${this.activeSessionId}"]`
|
||||
);
|
||||
activeTab?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ai-chat-tabs': AIChatTabs;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import type { NotificationService } from '@blocksuite/affine/shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
HistoryIcon,
|
||||
PinedIcon,
|
||||
PinIcon,
|
||||
PlusIcon,
|
||||
@@ -120,8 +120,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
||||
<div
|
||||
class="chat-toolbar-icon history-button"
|
||||
@click=${this.toggleHistoryMenu}
|
||||
data-testid="ai-panel-chat-history"
|
||||
>
|
||||
${ArrowDownSmallIcon()}
|
||||
${HistoryIcon()}
|
||||
<affine-tooltip>Chat History</affine-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './ai-chat-tabs';
|
||||
export * from './ai-chat-toolbar';
|
||||
export * from './ai-session-history';
|
||||
export * from './configure-ai-chat-toolbar';
|
||||
|
||||
@@ -27,7 +27,11 @@ import { AIChatInput } from '../components/ai-chat-input';
|
||||
import { AIChatEmbeddingStatusTooltip } from '../components/ai-chat-input/embedding-status-tooltip';
|
||||
import { ChatInputPreference } from '../components/ai-chat-input/preference-popup';
|
||||
import { AIChatMessages } from '../components/ai-chat-messages/ai-chat-messages';
|
||||
import { AIChatToolbar, AISessionHistory } from '../components/ai-chat-toolbar';
|
||||
import {
|
||||
AIChatTabs,
|
||||
AIChatToolbar,
|
||||
AISessionHistory,
|
||||
} from '../components/ai-chat-toolbar';
|
||||
import { AIHistoryClear } from '../components/ai-history-clear';
|
||||
import { AssistantAvatar } from '../components/ai-message-content/assistant-avatar';
|
||||
import { ChatActionList } from '../components/chat-action-list';
|
||||
@@ -53,6 +57,7 @@ const appElements = {
|
||||
'action-text': ActionText,
|
||||
'ai-loading': AILoading,
|
||||
'ai-chat-content': AIChatContent,
|
||||
'ai-chat-tabs': AIChatTabs,
|
||||
'ai-chat-toolbar': AIChatToolbar,
|
||||
'ai-session-history': AISessionHistory,
|
||||
'ai-chat-messages': AIChatMessages,
|
||||
|
||||
@@ -75,6 +75,7 @@ export const appEffectElementTags = [
|
||||
'action-text',
|
||||
'ai-loading',
|
||||
'ai-chat-content',
|
||||
'ai-chat-tabs',
|
||||
'ai-chat-toolbar',
|
||||
'ai-session-history',
|
||||
'ai-chat-messages',
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { ExplorerIconService } from '@affine/core/modules/explorer-icon/services/explorer-icon';
|
||||
import { OrganizeService } from '@affine/core/modules/organize';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
import {
|
||||
getAFFiNEWorkspaceSchema,
|
||||
@@ -27,6 +28,7 @@ import track from '@affine/track';
|
||||
import { openDirectory, openFilesWith } from '@blocksuite/affine/shared/utils';
|
||||
import type { Workspace } from '@blocksuite/affine/store';
|
||||
import {
|
||||
BearTransformer,
|
||||
DocxTransformer,
|
||||
HtmlTransformer,
|
||||
MarkdownTransformer,
|
||||
@@ -188,11 +190,49 @@ function createFolderStructure(
|
||||
return { folderId: rootFolderId, docLinks };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the folder tree described by {@link folderHierarchy} via
|
||||
* {@link OrganizeService} and links every document into its folder.
|
||||
* Returns the root folder ID on success, or `undefined` if the
|
||||
* hierarchy is empty or an error occurs.
|
||||
*
|
||||
* When {@link explorerIconService} is provided, document icons from the
|
||||
* hierarchy (e.g. Notion page emojis) are applied. Callers that do not
|
||||
* need icon support can omit it safely.
|
||||
*/
|
||||
function applyFolderHierarchy(
|
||||
organizeService: OrganizeService,
|
||||
folderHierarchy: FolderHierarchy,
|
||||
explorerIconService?: ExplorerIconService
|
||||
): string | undefined {
|
||||
if (folderHierarchy.children.size === 0) return undefined;
|
||||
try {
|
||||
const { folderId, docLinks } = createFolderStructure(
|
||||
organizeService,
|
||||
folderHierarchy,
|
||||
null,
|
||||
explorerIconService
|
||||
);
|
||||
for (const { folderId, docId } of docLinks) {
|
||||
const folder = organizeService.folderTree.folderNode$(folderId).value;
|
||||
if (folder) {
|
||||
const index = folder.indexAt('after');
|
||||
folder.createLink('doc', docId, index);
|
||||
}
|
||||
}
|
||||
return folderId || undefined;
|
||||
} catch (error) {
|
||||
logger.warn('Failed to create folder structure:', error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
type ImportType =
|
||||
| 'markdown'
|
||||
| 'markdownZip'
|
||||
| 'notion'
|
||||
| 'obsidian'
|
||||
| 'bear'
|
||||
| 'snapshot'
|
||||
| 'html'
|
||||
| 'docx'
|
||||
@@ -218,7 +258,8 @@ type ImportConfig = {
|
||||
files: File[],
|
||||
handleImportAffineFile: () => Promise<WorkspaceMetadata | undefined>,
|
||||
organizeService?: OrganizeService,
|
||||
explorerIconService?: ExplorerIconService
|
||||
explorerIconService?: ExplorerIconService,
|
||||
tagService?: TagService
|
||||
) => Promise<ImportResult>;
|
||||
};
|
||||
|
||||
@@ -290,6 +331,19 @@ const importOptions = [
|
||||
testId: 'editor-option-menu-import-obsidian',
|
||||
type: 'obsidian' as ImportType,
|
||||
},
|
||||
{
|
||||
key: 'bear',
|
||||
label: 'com.affine.import.bear',
|
||||
prefixIcon: (
|
||||
<FileIcon color={cssVarV2('icon/primary')} width={20} height={20} />
|
||||
),
|
||||
suffixIcon: (
|
||||
<HelpIcon color={cssVarV2('icon/primary')} width={20} height={20} />
|
||||
),
|
||||
suffixTooltip: 'com.affine.import.bear.tooltip',
|
||||
testId: 'editor-option-menu-import-bear',
|
||||
type: 'bear' as ImportType,
|
||||
},
|
||||
{
|
||||
key: 'docx',
|
||||
label: 'com.affine.import.docx',
|
||||
@@ -365,21 +419,29 @@ const importConfigs: Record<ImportType, ImportConfig> = {
|
||||
docCollection,
|
||||
files,
|
||||
_handleImportAffineFile,
|
||||
_organizeService,
|
||||
organizeService,
|
||||
_explorerIconService
|
||||
) => {
|
||||
const file = files.length === 1 ? files[0] : null;
|
||||
if (!file) {
|
||||
throw new Error('Expected a single zip file for markdownZip import');
|
||||
}
|
||||
const docIds = await MarkdownTransformer.importMarkdownZip({
|
||||
collection: docCollection,
|
||||
schema: getAFFiNEWorkspaceSchema(),
|
||||
imported: file,
|
||||
extensions: getStoreManager().config.init().value.get('store'),
|
||||
});
|
||||
const { docIds, folderHierarchy } =
|
||||
await MarkdownTransformer.importMarkdownZip({
|
||||
collection: docCollection,
|
||||
schema: getAFFiNEWorkspaceSchema(),
|
||||
imported: file,
|
||||
extensions: getStoreManager().config.init().value.get('store'),
|
||||
});
|
||||
|
||||
const rootFolderId =
|
||||
folderHierarchy && organizeService
|
||||
? applyFolderHierarchy(organizeService, folderHierarchy)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
docIds,
|
||||
rootFolderId,
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -431,37 +493,14 @@ const importConfigs: Record<ImportType, ImportConfig> = {
|
||||
extensions: getStoreManager().config.init().value.get('store'),
|
||||
});
|
||||
|
||||
let rootFolderId: string | undefined;
|
||||
|
||||
// Create folder structure if hierarchy exists and OrganizeService is available
|
||||
if (
|
||||
folderHierarchy &&
|
||||
organizeService &&
|
||||
folderHierarchy.children.size > 0
|
||||
) {
|
||||
try {
|
||||
const { folderId, docLinks } = createFolderStructure(
|
||||
organizeService,
|
||||
folderHierarchy,
|
||||
null,
|
||||
explorerIconService
|
||||
);
|
||||
rootFolderId = folderId || undefined;
|
||||
|
||||
// Create links for all documents to their respective folders
|
||||
for (const { folderId, docId } of docLinks) {
|
||||
const folder =
|
||||
organizeService.folderTree.folderNode$(folderId).value;
|
||||
if (folder) {
|
||||
const index = folder.indexAt('after');
|
||||
folder.createLink('doc', docId, index);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Failed to create folder structure:', error);
|
||||
// Continue with import even if folder creation fails
|
||||
}
|
||||
}
|
||||
const rootFolderId =
|
||||
folderHierarchy && organizeService
|
||||
? applyFolderHierarchy(
|
||||
organizeService,
|
||||
folderHierarchy,
|
||||
explorerIconService
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
docIds: pageIds,
|
||||
@@ -501,6 +540,114 @@ const importConfigs: Record<ImportType, ImportConfig> = {
|
||||
return { docIds };
|
||||
},
|
||||
},
|
||||
bear: {
|
||||
fileOptions: { acceptType: 'Zip', multiple: false },
|
||||
importFunction: async (
|
||||
docCollection,
|
||||
files,
|
||||
_handleImportAffineFile,
|
||||
organizeService,
|
||||
_explorerIconService,
|
||||
tagService
|
||||
) => {
|
||||
const file = files.length === 1 ? files[0] : null;
|
||||
if (!file) {
|
||||
throw new Error('Expected a single .bear2bk file for Bear import');
|
||||
}
|
||||
let docIds: string[];
|
||||
let tags: Map<string, string[]>;
|
||||
let folderHierarchy: FolderHierarchy;
|
||||
try {
|
||||
const result = await BearTransformer.importBearBackup({
|
||||
collection: docCollection,
|
||||
schema: getAFFiNEWorkspaceSchema(),
|
||||
imported: file,
|
||||
extensions: getStoreManager().config.init().value.get('store'),
|
||||
});
|
||||
docIds = result.docIds;
|
||||
tags = result.tags;
|
||||
folderHierarchy = result.folderHierarchy;
|
||||
} catch (err) {
|
||||
logger.error('Bear import failed:', err);
|
||||
throw err instanceof Error
|
||||
? err
|
||||
: new Error(String(err) || 'Bear import failed');
|
||||
}
|
||||
|
||||
// Create AFFiNE tags from Bear tags
|
||||
if (tagService && tags.size > 0) {
|
||||
try {
|
||||
// Get existing tags for deduplication
|
||||
const existingTags = tagService.tagList.tags$.value;
|
||||
const existingTagMap = new Map<string, string>(); // lowercase name → tag id
|
||||
for (const tag of existingTags) {
|
||||
const name = tag.value$.value.toLowerCase();
|
||||
existingTagMap.set(name, tag.id);
|
||||
}
|
||||
|
||||
// Consolidate tags by root segment (e.g., "privat/bike" → "privat").
|
||||
// Keyed by lowercase root for case-insensitive dedup, but the
|
||||
// original capitalization of the first occurrence is preserved
|
||||
// so new AFFiNE tags are created with the user's casing.
|
||||
const rootTagDocMap = new Map<
|
||||
string,
|
||||
{ displayName: string; docs: Set<string> }
|
||||
>();
|
||||
for (const [tagName, tagDocIds] of tags) {
|
||||
const originalRoot = tagName.split('/')[0];
|
||||
const key = originalRoot.toLowerCase();
|
||||
let entry = rootTagDocMap.get(key);
|
||||
if (!entry) {
|
||||
entry = { displayName: originalRoot, docs: new Set<string>() };
|
||||
rootTagDocMap.set(key, entry);
|
||||
}
|
||||
for (const docId of tagDocIds) {
|
||||
entry.docs.add(docId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [
|
||||
rootTagKey,
|
||||
{ displayName, docs: docIdSet },
|
||||
] of rootTagDocMap) {
|
||||
// Check if tag already exists (case-insensitive)
|
||||
let tagId = existingTagMap.get(rootTagKey);
|
||||
if (!tagId) {
|
||||
const newTag = tagService.tagList.createTag(
|
||||
displayName,
|
||||
tagService.randomTagColor()
|
||||
);
|
||||
tagId = newTag.id;
|
||||
existingTagMap.set(rootTagKey, tagId);
|
||||
}
|
||||
|
||||
// Assign tag to each doc
|
||||
for (const docId of docIdSet) {
|
||||
const doc = docCollection.getDoc(docId);
|
||||
const currentTags = doc?.meta?.tags ?? [];
|
||||
if (!currentTags.includes(tagId)) {
|
||||
docCollection.meta.setDocMeta(docId, {
|
||||
tags: [...currentTags, tagId],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Failed to create Bear tags:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const rootFolderId =
|
||||
folderHierarchy && organizeService
|
||||
? applyFolderHierarchy(organizeService, folderHierarchy)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
docIds,
|
||||
rootFolderId,
|
||||
};
|
||||
},
|
||||
},
|
||||
docx: {
|
||||
fileOptions: { acceptType: 'Docx', multiple: false },
|
||||
importFunction: async (docCollection, file) => {
|
||||
@@ -735,6 +882,7 @@ export const ImportDialog = ({
|
||||
const docCollection = workspace.docCollection;
|
||||
const organizeService = useService(OrganizeService);
|
||||
const explorerIconService = useService(ExplorerIconService);
|
||||
const tagService = useService(TagService);
|
||||
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
@@ -824,7 +972,8 @@ export const ImportDialog = ({
|
||||
files,
|
||||
handleImportAffineFile,
|
||||
organizeService,
|
||||
explorerIconService
|
||||
explorerIconService,
|
||||
tagService
|
||||
);
|
||||
|
||||
setImportResult({
|
||||
@@ -863,6 +1012,7 @@ export const ImportDialog = ({
|
||||
explorerIconService,
|
||||
handleImportAffineFile,
|
||||
organizeService,
|
||||
tagService,
|
||||
t,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1,5 +1,86 @@
|
||||
import { WorkspaceLocalState } from '@affine/core/modules/workspace';
|
||||
import type { I18nInstance } from '@affine/i18n';
|
||||
import type { NotificationService } from '@blocksuite/affine/shared/services';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
const AI_CHAT_OPEN_TABS_KEY = 'aiChatOpenTabs';
|
||||
|
||||
// Pass `null` for `loadSession` to defer hydration until a real loader is ready.
|
||||
export function useAIChatOpenTabs<T extends { sessionId: string }>(
|
||||
loadSession: ((sessionId: string) => Promise<T | null | undefined>) | null
|
||||
): {
|
||||
openTabs: T[];
|
||||
setOpenTabs: Dispatch<SetStateAction<T[]>>;
|
||||
} {
|
||||
const workspaceLocalState = useService(WorkspaceLocalState);
|
||||
const [openTabs, setOpenTabsState] = useState<T[]>([]);
|
||||
// Ref so persist gate isn't subject to React state-batch ordering.
|
||||
const hydratedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loadSession) return;
|
||||
hydratedRef.current = false;
|
||||
setOpenTabsState([]);
|
||||
|
||||
const ids = workspaceLocalState.get<string[]>(AI_CHAT_OPEN_TABS_KEY) ?? [];
|
||||
if (!ids.length) {
|
||||
hydratedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
Promise.all(ids.map(id => loadSession(id).catch(() => null)))
|
||||
.then(results => {
|
||||
if (cancelled) return;
|
||||
const valid = (results as (T | null | undefined)[]).filter(
|
||||
(entry): entry is T => !!entry && !!entry.sessionId
|
||||
);
|
||||
if (valid.length) setOpenTabsState(valid);
|
||||
hydratedRef.current = true;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
if (!cancelled) hydratedRef.current = true;
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [loadSession, workspaceLocalState]);
|
||||
|
||||
const setOpenTabs = useCallback<Dispatch<SetStateAction<T[]>>>(
|
||||
updater => {
|
||||
setOpenTabsState(prev => {
|
||||
const next =
|
||||
typeof updater === 'function'
|
||||
? (updater as (p: T[]) => T[])(prev)
|
||||
: updater;
|
||||
if (hydratedRef.current) {
|
||||
if (next.length) {
|
||||
workspaceLocalState.set(
|
||||
AI_CHAT_OPEN_TABS_KEY,
|
||||
next.map(tab => tab.sessionId)
|
||||
);
|
||||
} else {
|
||||
workspaceLocalState.del(AI_CHAT_OPEN_TABS_KEY);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[workspaceLocalState]
|
||||
);
|
||||
|
||||
return { openTabs, setOpenTabs };
|
||||
}
|
||||
|
||||
export type SessionDeleteCleanupFn = (
|
||||
session: BlockSuitePresets.AIRecentSession
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const chatTabsContainer = style({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const chatRoot = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
});
|
||||
|
||||
export const chatHeader = style({
|
||||
@@ -10,4 +20,6 @@ export const chatHeader = style({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
gap: 12,
|
||||
borderBottom: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import type { ChatStatus } from '@affine/core/blocksuite/ai/components/ai-chat-messages';
|
||||
import type { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
|
||||
import {
|
||||
AIChatTabs,
|
||||
configureAIChatToolbar,
|
||||
getOrCreateAIChatToolbar,
|
||||
} from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
|
||||
@@ -49,7 +50,10 @@ import { useFramework, useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { createSessionDeleteHandler } from '../chat-panel-utils';
|
||||
import {
|
||||
createSessionDeleteHandler,
|
||||
useAIChatOpenTabs,
|
||||
} from '../chat-panel-utils';
|
||||
import * as styles from './index.css';
|
||||
|
||||
type CopilotSession = Awaited<ReturnType<CopilotClient['getSession']>>;
|
||||
@@ -93,6 +97,7 @@ export const Component = () => {
|
||||
const [isHeaderProvided, setIsHeaderProvided] = useState(false);
|
||||
const [chatContent, setChatContent] = useState<AIChatContent | null>(null);
|
||||
const [chatTool, setChatTool] = useState<AIChatToolbar | null>(null);
|
||||
const [chatTabs, setChatTabs] = useState<AIChatTabs | null>(null);
|
||||
const [currentSession, setCurrentSession] = useState<CopilotSession | null>(
|
||||
null
|
||||
);
|
||||
@@ -102,12 +107,19 @@ export const Component = () => {
|
||||
const hasRestoredPinnedSessionRef = useRef(false);
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||
const chatToolContainerRef = useRef<HTMLDivElement>(null);
|
||||
const chatTabsContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const widthSignalRef = useRef<Signal<number>>(signal(0));
|
||||
const client = useCopilotClient();
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
const workspaceId = useService(WorkspaceService).workspace.id;
|
||||
|
||||
const loadSession = useCallback(
|
||||
(sessionId: string) => client.getSession(workspaceId, sessionId),
|
||||
[client, workspaceId]
|
||||
);
|
||||
const { openTabs, setOpenTabs } = useAIChatOpenTabs(loadSession);
|
||||
|
||||
useEffect(() => {
|
||||
hasRestoredPinnedSessionRef.current = false;
|
||||
}, [workspaceId]);
|
||||
@@ -192,6 +204,11 @@ export const Component = () => {
|
||||
setIsOpeningSession(true);
|
||||
try {
|
||||
const session = await client.getSession(workspaceId, sessionId);
|
||||
if (!session) {
|
||||
// Drop stale tab if session no longer exists.
|
||||
setOpenTabs(prev => prev.filter(tab => tab.sessionId !== sessionId));
|
||||
return;
|
||||
}
|
||||
setCurrentSession(session);
|
||||
reMountChatContent();
|
||||
chatTool?.closeHistoryMenu();
|
||||
@@ -207,10 +224,31 @@ export const Component = () => {
|
||||
currentSession?.sessionId,
|
||||
isOpeningSession,
|
||||
reMountChatContent,
|
||||
setOpenTabs,
|
||||
workspaceId,
|
||||
]
|
||||
);
|
||||
|
||||
const closeTab = useCallback(
|
||||
(sessionId: string) => {
|
||||
let fallback: NonNullable<CopilotSession> | undefined;
|
||||
setOpenTabs(prev => {
|
||||
const idx = prev.findIndex(tab => tab.sessionId === sessionId);
|
||||
if (idx === -1) return prev;
|
||||
const next = prev.filter(tab => tab.sessionId !== sessionId);
|
||||
fallback = next[idx] ?? next[idx - 1] ?? next[0];
|
||||
return next;
|
||||
});
|
||||
if (currentSession?.sessionId !== sessionId) return;
|
||||
if (fallback) {
|
||||
onOpenSession(fallback.sessionId).catch(console.error);
|
||||
} else {
|
||||
createFreshSession().catch(console.error);
|
||||
}
|
||||
},
|
||||
[createFreshSession, currentSession?.sessionId, onOpenSession, setOpenTabs]
|
||||
);
|
||||
|
||||
const onContextChange = useCallback((context: Partial<ChatContextValue>) => {
|
||||
setStatus(context.status ?? 'idle');
|
||||
}, []);
|
||||
@@ -399,6 +437,40 @@ export const Component = () => {
|
||||
return () => sub.unsubscribe();
|
||||
}, [framework, mockStd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentSession?.sessionId) return;
|
||||
setOpenTabs(prev => {
|
||||
const existing = prev.findIndex(
|
||||
tab => tab.sessionId === currentSession.sessionId
|
||||
);
|
||||
if (existing !== -1) {
|
||||
if (prev[existing] === currentSession) return prev;
|
||||
const next = prev.slice();
|
||||
next[existing] = currentSession;
|
||||
return next;
|
||||
}
|
||||
return [...prev, currentSession];
|
||||
});
|
||||
}, [currentSession, setOpenTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatTabsContainerRef.current) return;
|
||||
let tabs = chatTabs;
|
||||
if (!tabs) {
|
||||
tabs = new AIChatTabs();
|
||||
chatTabsContainerRef.current.append(tabs);
|
||||
setChatTabs(tabs);
|
||||
}
|
||||
tabs.sessions = openTabs;
|
||||
tabs.activeSessionId = currentSession?.sessionId;
|
||||
tabs.onSelectTab = (sessionId: string) => {
|
||||
onOpenSession(sessionId).catch(console.error);
|
||||
};
|
||||
tabs.onCloseTab = (sessionId: string) => {
|
||||
closeTab(sessionId);
|
||||
};
|
||||
}, [chatTabs, closeTab, currentSession?.sessionId, onOpenSession, openTabs]);
|
||||
|
||||
// restore pinned session
|
||||
useEffect(() => {
|
||||
if (hasRestoredPinnedSessionRef.current || currentSession) return;
|
||||
@@ -462,6 +534,10 @@ export const Component = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onChatTabsContainerRef = useCallback((node: HTMLDivElement | null) => {
|
||||
chatTabsContainerRef.current = node;
|
||||
}, []);
|
||||
|
||||
// observe chat container width and provide to ai-chat-content
|
||||
useEffect(() => {
|
||||
if (!isBodyProvided || !chatContainerRef.current) return;
|
||||
@@ -476,7 +552,10 @@ export const Component = () => {
|
||||
<ViewIcon icon="ai" />
|
||||
<ViewHeader>
|
||||
<div className={styles.chatHeader}>
|
||||
<div />
|
||||
<div
|
||||
className={styles.chatTabsContainer}
|
||||
ref={onChatTabsContainerRef}
|
||||
/>
|
||||
<div ref={onChatToolContainerRef} />
|
||||
</div>
|
||||
</ViewHeader>
|
||||
|
||||
@@ -20,11 +20,13 @@ export const header = style({
|
||||
position: 'relative',
|
||||
padding: '8px var(--h-padding, 16px)',
|
||||
width: '100%',
|
||||
height: '36px',
|
||||
minHeight: '36px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
zIndex: 1,
|
||||
borderBottom: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
});
|
||||
|
||||
export const title = style({
|
||||
@@ -82,6 +84,14 @@ export const loadingIcon = style({
|
||||
color: 'var(--affine-icon-secondary)',
|
||||
});
|
||||
|
||||
export const tabsContainer = style({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
globalStyle(`${playground} svg`, {
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import type { ChatStatus } from '@affine/core/blocksuite/ai/components/ai-chat-messages';
|
||||
import type { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
|
||||
import {
|
||||
AIChatTabs,
|
||||
configureAIChatToolbar,
|
||||
getOrCreateAIChatToolbar,
|
||||
} from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
|
||||
@@ -45,7 +46,10 @@ import { useFramework, useService } from '@toeverything/infra';
|
||||
import { html } from 'lit';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { createSessionDeleteHandler } from '../../chat-panel-utils';
|
||||
import {
|
||||
createSessionDeleteHandler,
|
||||
useAIChatOpenTabs,
|
||||
} from '../../chat-panel-utils';
|
||||
import * as styles from './chat.css';
|
||||
import {
|
||||
getChatContentKey,
|
||||
@@ -93,11 +97,12 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
||||
|
||||
const [chatContent, setChatContent] = useState<AIChatContent | null>(null);
|
||||
const [chatToolbar, setChatToolbar] = useState<AIChatToolbar | null>(null);
|
||||
const [chatTabs, setChatTabs] = useState<AIChatTabs | null>(null);
|
||||
const [isBodyProvided, setIsBodyProvided] = useState(false);
|
||||
const [isHeaderProvided, setIsHeaderProvided] = useState(false);
|
||||
|
||||
const chatContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const chatToolbarContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const chatTabsContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const contentKeyRef = useRef<string | null>(null);
|
||||
const prevSessionIdRef = useRef<string | null>(null);
|
||||
const prevSessionDocIdRef = useRef<string | null>(null);
|
||||
@@ -107,6 +112,36 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
||||
|
||||
const doc = editor?.doc;
|
||||
const host = editor?.host;
|
||||
const workspaceId = doc?.workspace.id;
|
||||
|
||||
const [sessionServiceReady, setSessionServiceReady] = useState(
|
||||
() => !!AIProvider.session
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionServiceReady) return;
|
||||
if (AIProvider.session) {
|
||||
setSessionServiceReady(true);
|
||||
return;
|
||||
}
|
||||
const sub = AIProvider.slots.sessionReady.subscribe(ready => {
|
||||
if (ready) setSessionServiceReady(true);
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
}, [sessionServiceReady]);
|
||||
|
||||
const loadSession = useMemo(() => {
|
||||
if (!sessionServiceReady || !workspaceId) return null;
|
||||
const sessionService = AIProvider.session;
|
||||
if (!sessionService) return null;
|
||||
return async (
|
||||
sessionId: string
|
||||
): Promise<CopilotChatHistoryFragment | null | undefined> =>
|
||||
sessionService.getSession(workspaceId, sessionId);
|
||||
}, [sessionServiceReady, workspaceId]);
|
||||
|
||||
const { openTabs, setOpenTabs } =
|
||||
useAIChatOpenTabs<CopilotChatHistoryFragment>(loadSession);
|
||||
|
||||
const appSidebarConfig = useMemo<AppSidebarConfig>(() => {
|
||||
return {
|
||||
@@ -237,13 +272,18 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
||||
sessionId
|
||||
);
|
||||
if (requestSeq !== sessionLoadSeqRef.current) return;
|
||||
setSession(nextSession ?? null);
|
||||
setHasPinned(!!nextSession?.pinned);
|
||||
if (!nextSession) {
|
||||
// Drop stale tab if session no longer exists.
|
||||
setOpenTabs(prev => prev.filter(tab => tab.sessionId !== sessionId));
|
||||
return;
|
||||
}
|
||||
setSession(nextSession);
|
||||
setHasPinned(!!nextSession.pinned);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
[doc, session?.sessionId]
|
||||
[doc, session?.sessionId, setOpenTabs]
|
||||
);
|
||||
|
||||
const openDoc = useCallback(
|
||||
@@ -291,6 +331,26 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
||||
[newSession, notificationService, session?.sessionId, t]
|
||||
);
|
||||
|
||||
const closeTab = useCallback(
|
||||
(sessionId: string) => {
|
||||
let fallback: CopilotChatHistoryFragment | undefined;
|
||||
setOpenTabs(prev => {
|
||||
const idx = prev.findIndex(tab => tab.sessionId === sessionId);
|
||||
if (idx === -1) return prev;
|
||||
const next = prev.filter(tab => tab.sessionId !== sessionId);
|
||||
fallback = next[idx] ?? next[idx - 1] ?? next[0];
|
||||
return next;
|
||||
});
|
||||
if (session?.sessionId !== sessionId) return;
|
||||
if (fallback) {
|
||||
openSession(fallback.sessionId).catch(console.error);
|
||||
} else {
|
||||
newSession().catch(console.error);
|
||||
}
|
||||
},
|
||||
[newSession, openSession, session?.sessionId, setOpenTabs]
|
||||
);
|
||||
|
||||
const togglePin = useCallback(async () => {
|
||||
const pinned = !session?.pinned;
|
||||
setHasPinned(true);
|
||||
@@ -347,7 +407,27 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
||||
chatToolbar.remove();
|
||||
setChatToolbar(null);
|
||||
}
|
||||
}, [chatContent, chatToolbar, session]);
|
||||
if (chatTabs) {
|
||||
chatTabs.remove();
|
||||
setChatTabs(null);
|
||||
}
|
||||
}, [chatContent, chatTabs, chatToolbar, session]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.sessionId) return;
|
||||
setOpenTabs(prev => {
|
||||
const existing = prev.findIndex(
|
||||
tab => tab.sessionId === session.sessionId
|
||||
);
|
||||
if (existing !== -1) {
|
||||
if (prev[existing] === session) return prev;
|
||||
const next = prev.slice();
|
||||
next[existing] = session;
|
||||
return next;
|
||||
}
|
||||
return [...prev, session];
|
||||
});
|
||||
}, [session, setOpenTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
@@ -553,6 +633,30 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
||||
togglePin,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatTabsContainerRef.current || !doc) {
|
||||
return;
|
||||
}
|
||||
if (session === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tabs = chatTabs;
|
||||
if (!tabs) {
|
||||
tabs = new AIChatTabs();
|
||||
chatTabsContainerRef.current.append(tabs);
|
||||
setChatTabs(tabs);
|
||||
}
|
||||
tabs.sessions = openTabs;
|
||||
tabs.activeSessionId = session?.sessionId;
|
||||
tabs.onSelectTab = (sessionId: string) => {
|
||||
openSession(sessionId).catch(console.error);
|
||||
};
|
||||
tabs.onCloseTab = (sessionId: string) => {
|
||||
closeTab(sessionId);
|
||||
};
|
||||
}, [chatTabs, closeTab, doc, openSession, openTabs, session]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor?.host || !chatContent) {
|
||||
return;
|
||||
@@ -654,6 +758,10 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
||||
chatToolbarContainerRef.current = node;
|
||||
}, []);
|
||||
|
||||
const onChatTabsContainerRef = useCallback((node: HTMLDivElement | null) => {
|
||||
chatTabsContainerRef.current = node;
|
||||
}, []);
|
||||
|
||||
const isEmbedding =
|
||||
embeddingProgress[1] > 0 && embeddingProgress[0] < embeddingProgress[1];
|
||||
const [done, total] = embeddingProgress;
|
||||
@@ -690,6 +798,10 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
||||
<CenterPeekIcon />
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={styles.tabsContainer}
|
||||
ref={onChatTabsContainerRef}
|
||||
/>
|
||||
<div ref={onChatToolContainerRef} />
|
||||
</div>
|
||||
<div className={styles.content} ref={onChatContainerRef} />
|
||||
|
||||
@@ -3,7 +3,13 @@ import type { IndexerPreferOptions, IndexerSyncState } from '@affine/nbstore';
|
||||
import type { ReferenceParams } from '@blocksuite/affine/model';
|
||||
import { fromPromise, LiveData, Service } from '@toeverything/infra';
|
||||
import { isEmpty, omit } from 'lodash-es';
|
||||
import { map, type Observable, of, switchMap } from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
type Observable,
|
||||
of,
|
||||
switchMap,
|
||||
} from 'rxjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { normalizeSearchText } from '../../../utils/normalize-search-text';
|
||||
@@ -234,6 +240,20 @@ export class DocsSearchService extends Service {
|
||||
})
|
||||
.filter(ref => !!ref);
|
||||
});
|
||||
}),
|
||||
// Only propagate downstream when the actual set of linked docs
|
||||
// changes (a link was added or removed). Without this guard,
|
||||
// every re-index triggered by typing emits a new array (same
|
||||
// docs, arbitrary search-engine order) and the navigation panel
|
||||
// visibly reorders on every keystroke.
|
||||
//
|
||||
// Note: this compares docId sets, not order. A stable, meaningful
|
||||
// sort order (e.g. document appearance order) requires block
|
||||
// position data from the indexer and is tracked separately.
|
||||
distinctUntilChanged((prev, curr) => {
|
||||
if (prev.length !== curr.length) return false;
|
||||
const currIds = new Set(curr.map(r => r.docId));
|
||||
return prev.every(r => currIds.has(r.docId));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ const ToggleButton = ({
|
||||
className={className}
|
||||
data-show={show}
|
||||
data-testid="right-sidebar-toggle"
|
||||
tooltip="Open sidebar"
|
||||
>
|
||||
<RightSidebarIcon />
|
||||
</IconButton>
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { RightSidebarIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import * as styles from './sidebar-header.css';
|
||||
|
||||
const RightSidebarOpenIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="none"
|
||||
style={{ userSelect: 'none', flexShrink: 0 }}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M15.25 6h3.25a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5h-3.25zm-1.5 0H5.5a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h8.25zM3.5 6.5a2 2 0 0 1 2-2h13a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill="#1E96EB"
|
||||
d="M15.25 6h3.25a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5h-3.25z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export type HeaderProps = {
|
||||
onToggle?: () => void;
|
||||
children?: React.ReactNode;
|
||||
@@ -26,8 +48,13 @@ function Container({
|
||||
|
||||
const ToggleButton = ({ onToggle }: { onToggle?: () => void }) => {
|
||||
return (
|
||||
<IconButton size="24" onClick={onToggle} data-testid="right-sidebar-close">
|
||||
<RightSidebarIcon />
|
||||
<IconButton
|
||||
size="24"
|
||||
onClick={onToggle}
|
||||
data-testid="right-sidebar-close"
|
||||
tooltip="Close sidebar"
|
||||
>
|
||||
<RightSidebarOpenIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"hi": 1,
|
||||
"it": 94,
|
||||
"ja": 93,
|
||||
"ko": 94,
|
||||
"ko": 93,
|
||||
"nb-NO": 46,
|
||||
"pl": 94,
|
||||
"pt-BR": 93,
|
||||
@@ -21,6 +21,6 @@
|
||||
"sv-SE": 93,
|
||||
"uk": 93,
|
||||
"ur": 2,
|
||||
"zh-Hans": 98,
|
||||
"zh-Hans": 97,
|
||||
"zh-Hant": 93
|
||||
}
|
||||
|
||||
@@ -2462,6 +2462,14 @@ export function useAFFiNEI18N(): {
|
||||
* `AFFiNE workspace data`
|
||||
*/
|
||||
["com.affine.import.affine-workspace-data"](): string;
|
||||
/**
|
||||
* `Bear (.bear2bk) (Experimental)`
|
||||
*/
|
||||
["com.affine.import.bear"](): string;
|
||||
/**
|
||||
* `Import your Bear note backup. Tags will be converted to AFFiNE tags and folders.`
|
||||
*/
|
||||
["com.affine.import.bear.tooltip"](): string;
|
||||
/**
|
||||
* `Docx`
|
||||
*/
|
||||
@@ -2495,7 +2503,7 @@ export function useAFFiNEI18N(): {
|
||||
*/
|
||||
["com.affine.import.modal.tip"](): string;
|
||||
/**
|
||||
* `Notion`
|
||||
* `Notion (Experimental)`
|
||||
*/
|
||||
["com.affine.import.notion"](): string;
|
||||
/**
|
||||
@@ -2503,7 +2511,7 @@ export function useAFFiNEI18N(): {
|
||||
*/
|
||||
["com.affine.import.notion.tooltip"](): string;
|
||||
/**
|
||||
* `Obsidian Vault`
|
||||
* `Obsidian Vault (Experimental)`
|
||||
*/
|
||||
["com.affine.import.obsidian"](): string;
|
||||
/**
|
||||
|
||||
@@ -614,6 +614,8 @@
|
||||
"com.affine.import-clipper.dialog.errorLoad": "Failed to load content, please try again.",
|
||||
"com.affine.import_file": "Support Markdown/Notion",
|
||||
"com.affine.import.affine-workspace-data": "AFFiNE workspace data",
|
||||
"com.affine.import.bear": "Bear (.bear2bk) (Experimental)",
|
||||
"com.affine.import.bear.tooltip": "Import your Bear note backup. Tags will be converted to AFFiNE tags and folders.",
|
||||
"com.affine.import.docx": "Docx",
|
||||
"com.affine.import.docx.tooltip": "Import your .docx file.",
|
||||
"com.affine.import.html-files": "HTML",
|
||||
@@ -622,9 +624,9 @@
|
||||
"com.affine.import.markdown-with-media-files": "Markdown with media files (.zip)",
|
||||
"com.affine.import.markdown-with-media-files.tooltip": "Please upload a markdown zip file with attachments, experimental function, there may be data loss.",
|
||||
"com.affine.import.modal.tip": "If you'd like to request support for additional file types, feel free to let us know on",
|
||||
"com.affine.import.notion": "Notion",
|
||||
"com.affine.import.notion": "Notion (Experimental)",
|
||||
"com.affine.import.notion.tooltip": "Import your Notion data. Supported import formats: HTML with subpages.",
|
||||
"com.affine.import.obsidian": "Obsidian Vault",
|
||||
"com.affine.import.obsidian": "Obsidian Vault (Experimental)",
|
||||
"com.affine.import.obsidian.tooltip": "Import an Obsidian vault. Select a folder to import all notes, images, and assets with wikilinks resolved.",
|
||||
"com.affine.import.snapshot": "Snapshot",
|
||||
"com.affine.import.snapshot.tooltip": "Import your AFFiNE workspace and page snapshot file.",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.5.0",
|
||||
"@napi-rs/cli": "3.6.2",
|
||||
"@napi-rs/whisper": "^0.0.4",
|
||||
"@types/node": "^22.0.0",
|
||||
"ava": "^7.0.0",
|
||||
|
||||
@@ -721,7 +721,7 @@ export type EventArgs = {
|
||||
dragStart: { type: string };
|
||||
addEmbeddingDoc: {
|
||||
type?: 'page' | 'edgeless';
|
||||
control: 'addButton' | 'atMenu';
|
||||
control: 'addButton' | 'atMenu' | 'dragDrop';
|
||||
method: 'doc' | 'cur-doc' | 'file' | 'tags' | 'collections' | 'suggestion';
|
||||
};
|
||||
openAttachmentInFullscreen: AttachmentEventArgs;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@playwright/test": "=1.58.2"
|
||||
"@playwright/test": "=1.59.1"
|
||||
},
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@playwright/test": "=1.58.2"
|
||||
"@playwright/test": "=1.59.1"
|
||||
},
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@playwright/test": "=1.58.2"
|
||||
"@playwright/test": "=1.59.1"
|
||||
},
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@affine/electron-api": "workspace:*",
|
||||
"@playwright/test": "=1.58.2",
|
||||
"@playwright/test": "=1.59.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"playwright": "=1.58.2"
|
||||
"playwright": "=1.59.1"
|
||||
},
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@playwright/test": "=1.58.2"
|
||||
"@playwright/test": "=1.59.1"
|
||||
},
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@playwright/test": "=1.58.2"
|
||||
"@playwright/test": "=1.59.1"
|
||||
},
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@blocksuite/affine": "workspace:*",
|
||||
"@blocksuite/integration-test": "workspace:*",
|
||||
"@playwright/test": "=1.58.2",
|
||||
"@playwright/test": "=1.59.1",
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"json-stable-stringify": "^1.2.1"
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"@affine-tools/utils": "workspace:*",
|
||||
"@blocksuite/affine": "workspace:*",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@playwright/test": "=1.58.2",
|
||||
"@playwright/test": "=1.59.1",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"express": "^5.1.0",
|
||||
"http-proxy-middleware": "^3.0.5"
|
||||
|
||||
Reference in New Issue
Block a user