Compare commits

...

74 Commits

Author SHA1 Message Date
Wu Yue 69e23e6a42 fix(core): fallback to default icon if image icon load error (#13349)
Close [AI-286](https://linear.app/affine-design/issue/AI-286)

<img width="586" height="208" alt="截屏2025-07-29 18 23 52"
src="https://github.com/user-attachments/assets/15eadb38-8cb9-4418-8f13-de7b1a3a3beb"
/>


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

## Summary by CodeRabbit

* **New Features**
* Enhanced image icon handling with a fallback display if an icon image
fails to load.

* **Style**
* Unified and improved styling for icons to ensure a consistent
appearance across result and footer sections.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-30 02:24:28 +00:00
Wu Yue f7a094053e feat(core): add ai workspace all docs switch (#13345)
Close [AI-397](https://linear.app/affine-design/issue/AI-397)

<img width="272" height="186" alt="截屏2025-07-29 11 54 20"
src="https://github.com/user-attachments/assets/e171fb57-66cf-4244-894d-c27b18cbe83a"
/>


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

* **New Features**
* Introduced an AI tools configuration service, allowing users to
customize AI tool usage (e.g., workspace search, reading docs) in chat
and AI features.
* Added a toggle in chat preferences for enabling or disabling
workspace-wide document search.
* AI chat components now respect user-configured tool settings across
chat, retry, and playground scenarios.

* **Improvements**
* Enhanced chat and AI interfaces to propagate and honor user tool
configuration throughout the frontend and backend.
* Made draft and tool configuration services optional and safely handled
their absence in chat components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-30 02:10:39 +00:00
L-Sun 091bac1047 fix(editor): add comment entire to inner toolbar (#13304)
Close
[BS-3624](https://linear.app/affine-design/issue/BS-3624/page模式单选图片的时候希望有comment-按钮)




#### PR Dependency Tree


* **PR #13304** 👈

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

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

* **New Features**
* Added a comment button to the image and surface reference block
toolbars for easier commenting.

* **Refactor**
* Simplified array flattening operations across multiple components and
utilities by replacing `.map(...).flat()` with `.flatMap(...)`,
improving code readability and maintainability.

* **Bug Fixes**
* Improved comment creation logic to allow adding comments even when
selections exist.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-29 13:21:56 +08:00
dependabot[bot] bd161c54b2 chore: bump form-data from 4.0.2 to 4.0.4 (#13342)
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.2 to
4.0.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/form-data/form-data/releases">form-data's
releases</a>.</em></p>
<blockquote>
<h2>v4.0.4</h2>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.3...v4.0.4">v4.0.4</a>
- 2025-07-16</h2>
<h3>Commits</h3>
<ul>
<li>[meta] add <code>auto-changelog</code> <a
href="https://github.com/form-data/form-data/commit/811f68282fab0315209d0e2d1c44b6c32ea0d479"><code>811f682</code></a></li>
<li>[Tests] handle predict-v8-randomness failures in node &lt; 17 and
node &gt; 23 <a
href="https://github.com/form-data/form-data/commit/1d11a76434d101f22fdb26b8aef8615f28b98402"><code>1d11a76</code></a></li>
<li>[Fix] Switch to using <code>crypto</code> random for boundary values
<a
href="https://github.com/form-data/form-data/commit/3d1723080e6577a66f17f163ecd345a21d8d0fd0"><code>3d17230</code></a></li>
<li>[Tests] fix linting errors <a
href="https://github.com/form-data/form-data/commit/5e340800b5f8914213e4e0378c084aae71cfd73a"><code>5e34080</code></a></li>
<li>[meta] actually ensure the readme backup isn’t published <a
href="https://github.com/form-data/form-data/commit/316c82ba93fd4985af757b771b9a1f26d3b709ef"><code>316c82b</code></a></li>
<li>[Dev Deps] update <code>@ljharb/eslint-config</code> <a
href="https://github.com/form-data/form-data/commit/58c25d76406a5b0dfdf54045cf252563f2bbda8d"><code>58c25d7</code></a></li>
<li>[meta] fix readme capitalization <a
href="https://github.com/form-data/form-data/commit/2300ca19595b0ee96431e868fe2a40db79e41c61"><code>2300ca1</code></a></li>
</ul>
<h2>v4.0.3</h2>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.2...v4.0.3">v4.0.3</a>
- 2025-06-05</h2>
<h3>Fixed</h3>
<ul>
<li>[Fix] <code>append</code>: avoid a crash on nullish values <a
href="https://redirect.github.com/form-data/form-data/issues/577"><code>[#577](https://github.com/form-data/form-data/issues/577)</code></a></li>
</ul>
<h3>Commits</h3>
<ul>
<li>[eslint] use a shared config <a
href="https://github.com/form-data/form-data/commit/426ba9ac440f95d1998dac9a5cd8d738043b048f"><code>426ba9a</code></a></li>
<li>[eslint] fix some spacing issues <a
href="https://github.com/form-data/form-data/commit/20941917f0e9487e68c564ebc3157e23609e2939"><code>2094191</code></a></li>
<li>[Refactor] use <code>hasown</code> <a
href="https://github.com/form-data/form-data/commit/81ab41b46fdf34f5d89d7ff30b513b0925febfaa"><code>81ab41b</code></a></li>
<li>[Fix] validate boundary type in <code>setBoundary()</code> method <a
href="https://github.com/form-data/form-data/commit/8d8e4693093519f7f18e3c597d1e8df8c493de9e"><code>8d8e469</code></a></li>
<li>[Tests] add tests to check the behavior of <code>getBoundary</code>
with non-strings <a
href="https://github.com/form-data/form-data/commit/837b8a1f7562bfb8bda74f3fc538adb7a5858995"><code>837b8a1</code></a></li>
<li>[Dev Deps] remove unused deps <a
href="https://github.com/form-data/form-data/commit/870e4e665935e701bf983a051244ab928e62d58e"><code>870e4e6</code></a></li>
<li>[meta] remove local commit hooks <a
href="https://github.com/form-data/form-data/commit/e6e83ccb545a5619ed6cd04f31d5c2f655eb633e"><code>e6e83cc</code></a></li>
<li>[Dev Deps] update <code>eslint</code> <a
href="https://github.com/form-data/form-data/commit/4066fd6f65992b62fa324a6474a9292a4f88c916"><code>4066fd6</code></a></li>
<li>[meta] fix scripts to use prepublishOnly <a
href="https://github.com/form-data/form-data/commit/c4bbb13c0ef669916657bc129341301b1d331d75"><code>c4bbb13</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/form-data/form-data/blob/master/CHANGELOG.md">form-data's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.3...v4.0.4">v4.0.4</a>
- 2025-07-16</h2>
<h3>Commits</h3>
<ul>
<li>[meta] add <code>auto-changelog</code> <a
href="https://github.com/form-data/form-data/commit/811f68282fab0315209d0e2d1c44b6c32ea0d479"><code>811f682</code></a></li>
<li>[Tests] handle predict-v8-randomness failures in node &lt; 17 and
node &gt; 23 <a
href="https://github.com/form-data/form-data/commit/1d11a76434d101f22fdb26b8aef8615f28b98402"><code>1d11a76</code></a></li>
<li>[Fix] Switch to using <code>crypto</code> random for boundary values
<a
href="https://github.com/form-data/form-data/commit/3d1723080e6577a66f17f163ecd345a21d8d0fd0"><code>3d17230</code></a></li>
<li>[Tests] fix linting errors <a
href="https://github.com/form-data/form-data/commit/5e340800b5f8914213e4e0378c084aae71cfd73a"><code>5e34080</code></a></li>
<li>[meta] actually ensure the readme backup isn’t published <a
href="https://github.com/form-data/form-data/commit/316c82ba93fd4985af757b771b9a1f26d3b709ef"><code>316c82b</code></a></li>
<li>[Dev Deps] update <code>@ljharb/eslint-config</code> <a
href="https://github.com/form-data/form-data/commit/58c25d76406a5b0dfdf54045cf252563f2bbda8d"><code>58c25d7</code></a></li>
<li>[meta] fix readme capitalization <a
href="https://github.com/form-data/form-data/commit/2300ca19595b0ee96431e868fe2a40db79e41c61"><code>2300ca1</code></a></li>
</ul>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.2...v4.0.3">v4.0.3</a>
- 2025-06-05</h2>
<h3>Fixed</h3>
<ul>
<li>[Fix] <code>append</code>: avoid a crash on nullish values <a
href="https://redirect.github.com/form-data/form-data/issues/577"><code>[#577](https://github.com/form-data/form-data/issues/577)</code></a></li>
</ul>
<h3>Commits</h3>
<ul>
<li>[eslint] use a shared config <a
href="https://github.com/form-data/form-data/commit/426ba9ac440f95d1998dac9a5cd8d738043b048f"><code>426ba9a</code></a></li>
<li>[eslint] fix some spacing issues <a
href="https://github.com/form-data/form-data/commit/20941917f0e9487e68c564ebc3157e23609e2939"><code>2094191</code></a></li>
<li>[Refactor] use <code>hasown</code> <a
href="https://github.com/form-data/form-data/commit/81ab41b46fdf34f5d89d7ff30b513b0925febfaa"><code>81ab41b</code></a></li>
<li>[Fix] validate boundary type in <code>setBoundary()</code> method <a
href="https://github.com/form-data/form-data/commit/8d8e4693093519f7f18e3c597d1e8df8c493de9e"><code>8d8e469</code></a></li>
<li>[Tests] add tests to check the behavior of <code>getBoundary</code>
with non-strings <a
href="https://github.com/form-data/form-data/commit/837b8a1f7562bfb8bda74f3fc538adb7a5858995"><code>837b8a1</code></a></li>
<li>[Dev Deps] remove unused deps <a
href="https://github.com/form-data/form-data/commit/870e4e665935e701bf983a051244ab928e62d58e"><code>870e4e6</code></a></li>
<li>[meta] remove local commit hooks <a
href="https://github.com/form-data/form-data/commit/e6e83ccb545a5619ed6cd04f31d5c2f655eb633e"><code>e6e83cc</code></a></li>
<li>[Dev Deps] update <code>eslint</code> <a
href="https://github.com/form-data/form-data/commit/4066fd6f65992b62fa324a6474a9292a4f88c916"><code>4066fd6</code></a></li>
<li>[meta] fix scripts to use prepublishOnly <a
href="https://github.com/form-data/form-data/commit/c4bbb13c0ef669916657bc129341301b1d331d75"><code>c4bbb13</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/form-data/form-data/commit/41996f5ac73a867046d48512cab62e64fc846dad"><code>41996f5</code></a>
v4.0.4</li>
<li><a
href="https://github.com/form-data/form-data/commit/316c82ba93fd4985af757b771b9a1f26d3b709ef"><code>316c82b</code></a>
[meta] actually ensure the readme backup isn’t published</li>
<li><a
href="https://github.com/form-data/form-data/commit/2300ca19595b0ee96431e868fe2a40db79e41c61"><code>2300ca1</code></a>
[meta] fix readme capitalization</li>
<li><a
href="https://github.com/form-data/form-data/commit/811f68282fab0315209d0e2d1c44b6c32ea0d479"><code>811f682</code></a>
[meta] add <code>auto-changelog</code></li>
<li><a
href="https://github.com/form-data/form-data/commit/5e340800b5f8914213e4e0378c084aae71cfd73a"><code>5e34080</code></a>
[Tests] fix linting errors</li>
<li><a
href="https://github.com/form-data/form-data/commit/1d11a76434d101f22fdb26b8aef8615f28b98402"><code>1d11a76</code></a>
[Tests] handle predict-v8-randomness failures in node &lt; 17 and node
&gt; 23</li>
<li><a
href="https://github.com/form-data/form-data/commit/58c25d76406a5b0dfdf54045cf252563f2bbda8d"><code>58c25d7</code></a>
[Dev Deps] update <code>@ljharb/eslint-config</code></li>
<li><a
href="https://github.com/form-data/form-data/commit/3d1723080e6577a66f17f163ecd345a21d8d0fd0"><code>3d17230</code></a>
[Fix] Switch to using <code>crypto</code> random for boundary
values</li>
<li><a
href="https://github.com/form-data/form-data/commit/d8d67dc8ac79285154edf7d3f57dbab593b9a146"><code>d8d67dc</code></a>
v4.0.3</li>
<li><a
href="https://github.com/form-data/form-data/commit/e6e83ccb545a5619ed6cd04f31d5c2f655eb633e"><code>e6e83cc</code></a>
[meta] remove local commit hooks</li>
<li>Additional commits viewable in <a
href="https://github.com/form-data/form-data/compare/v4.0.2...v4.0.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=form-data&package-manager=npm_and_yarn&previous-version=4.0.2&new-version=4.0.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/toeverything/AFFiNE/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-29 09:52:46 +08:00
DarkSky 61d2382643 chore(server): improve citation in chat (#13267)
fix AI-357

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

* **New Features**
* Improved prompt handling to conditionally include document fragments
based on the presence of documents in user queries.

* **Refactor**
* Updated system prompts to focus solely on document fragments, removing
references to file fragments for a more streamlined user experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-28 15:22:08 +00:00
Lakr 4586e4a18f feat: adopt new backend api for attachment (#13336)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a new query for applying document updates using AI,
allowing merged markdown responses.
* Added support for an optional file upload field when creating chat
messages.

* **Improvements**
* Enhanced recent Copilot sessions query with pagination by adding an
offset parameter.
* Refined attachment handling in chat responses to better distinguish
between single and multiple file uploads, improving reliability.

* **Bug Fixes**
  * Minor update to error handling for clearer messaging.

* **Chores**
* Cleaned up and updated iOS project configuration files for improved
build consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-28 07:23:03 +00:00
Wu Yue 30c42fc51b fix(core): add document content params for section edit tool (#13334)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Section editing now uses the full document context to ensure edits are
consistent with the overall tone, style, and structure.
* Cleaner output for edited sections, with internal markdown comments
removed.

* **Improvements**
* Enhanced instructions and descriptions for section editing, providing
clearer guidance and examples for users.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-28 05:36:39 +00:00
DarkSky 627771948f feat: paged query for outdated embedding cleanup (#13335)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved the workspace cleanup process for trashed document embeddings
to use a more efficient, incremental batching approach, resulting in
better performance and reliability for large numbers of workspaces. No
visible changes to user interface or functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-28 05:26:51 +00:00
DarkSky 0e3691e54e feat: add cache for tokenizer (#13333)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Performance Improvements**
* Improved the efficiency of token encoder retrieval, resulting in
faster response times when working with supported models.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-28 03:50:39 +00:00
DarkSky 8fd0d5c1e8 chore: update cert timestamp (#13300)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated the timestamp server URL used in the Windows Signer workflow
for code signing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2025-07-28 02:53:45 +00:00
Peng Xiao 13763e80bb fix(core): nav sidebar should have default bg (#13265)
fix AF-2724

#### PR Dependency Tree


* **PR #13265** 👈

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

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

## Summary by CodeRabbit

* **Style**
* Updated sidebar background color to apply in additional display
scenarios, ensuring a more consistent appearance across different modes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-28 02:43:38 +00:00
Yii 6a1b53dd11 fix(core): do not create first app if local workspace disabled (#13289)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Prevented creation of initial app data when local workspace
functionality is disabled, ensuring correct behavior based on user
settings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-28 02:42:02 +00:00
EYHN 9899fad000 feat(editor): put current user in first on database user select (#13320) 2025-07-27 07:53:17 +00:00
EYHN be55442f38 feat(core): remove empty workspace (#13317)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added the ability to remove an empty workspace directly from the
workspace card when you are the owner.
* Workspace cards now display a "Remove" button for eligible workspaces.
* **Improvements**
* Workspace information now indicates if a workspace is empty, improving
clarity for users.
* **Bug Fixes**
* Enhanced accuracy in displaying workspace status by updating how
workspace profile data is handled.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-25 10:26:55 +00:00
EYHN 1dd4bbbaba feat(core): cache navigation collapsed state (#13315)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Collapsible section state in navigation panels is now managed using a
unified path-based approach, enabling more consistent and centralized
control across desktop and mobile interfaces.
* The collapsed/expanded state of navigation sections and nodes is now
persistently tracked using hierarchical paths, improving reliability
across sessions and devices.
* Internal state management is streamlined, with local state replaced by
a shared service, resulting in more predictable navigation behavior.

* **Chores**
* Removed obsolete types and legacy section management logic for
improved maintainability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-25 10:19:21 +00:00
EYHN 7409940cc6 feat(core): add context menu to card view (#13258)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a context menu to document cards, allowing additional actions
when enabled.

* **Improvements**
* The context menu is now conditionally enabled based on live data,
ensuring it only appears when relevant.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-25 10:07:17 +00:00
Wu Yue 0d43350afd feat(core): add section edit tool (#13313)
Close [AI-396](https://linear.app/affine-design/issue/AI-396)

<img width="798" height="294" alt="截屏2025-07-25 11 30 32"
src="https://github.com/user-attachments/assets/6366dab2-688b-470b-8b24-29a2d50a38c9"
/>



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

## Summary by CodeRabbit

* **New Features**
* Introduced a "Section Edit" AI tool for expert editing of specific
markdown sections based on user instructions, preserving formatting and
style.
* Added a new interface and UI component for section editing, allowing
users to view, copy, insert, or save edited content directly from chat
interactions.

* **Improvements**
* Enhanced AI chat and tool rendering to support and display section
editing results.
* Updated chat input handling for improved draft management and message
sending order.

* **Other Changes**
* Registered the new section editing tool in the system for seamless
integration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-25 09:02:52 +00:00
renovate[bot] ff9a4f4322 chore: bump up nestjs (#13288)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[@nestjs-cls/transactional-adapter-prisma](https://papooch.github.io/nestjs-cls/)
([source](https://redirect.github.com/Papooch/nestjs-cls)) | [`1.2.24`
->
`1.3.0`](https://renovatebot.com/diffs/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.24/1.3.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs-cls%2ftransactional-adapter-prisma/1.2.24/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@nestjs/common](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/common))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fcommon/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcommon/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcommon/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@nestjs/core](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/core))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fcore/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcore/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcore/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@nestjs/platform-express](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/platform-express))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-express/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-express/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-express/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@nestjs/platform-socket.io](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/platform-socket.io))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-socket.io/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-socket.io/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-socket.io/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@nestjs/websockets](https://redirect.github.com/nestjs/nest)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/websockets))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fwebsockets/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fwebsockets/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fwebsockets/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>Papooch/nestjs-cls
(@&#8203;nestjs-cls/transactional-adapter-prisma)</summary>

###
[`v1.3.0`](https://redirect.github.com/Papooch/nestjs-cls/releases/tag/%40nestjs-cls/transactional-adapter-prisma%401.3.0)

[Compare
Source](https://redirect.github.com/Papooch/nestjs-cls/compare/@nestjs-cls/transactional-adapter-prisma@1.2.24...@nestjs-cls/transactional-adapter-prisma@1.3.0)

##### Features

- **transactional-adapter-prisma**: add support for nested transactions
([c49c766](https://redirect.github.com/Papooch/nestjs-cls/commits/c49c766))
- **transactional-adapter-prisma**: add support for nested transactions
([#&#8203;353](https://redirect.github.com/Papooch/nestjs-cls/issues/353))
([c49c766](https://redirect.github.com/Papooch/nestjs-cls/commits/c49c766))

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/common)</summary>

###
[`v11.1.5`](https://redirect.github.com/nestjs/nest/compare/v11.1.4...9bb0560e79743cc0bd2ce198c65e21332200c3ad)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.4...v11.1.5)

###
[`v11.1.4`](https://redirect.github.com/nestjs/nest/compare/v11.1.3...1f101ac8b0a5bb5b97a7caf6634fcea8d65196e0)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.3...v11.1.4)

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/core)</summary>

###
[`v11.1.5`](https://redirect.github.com/nestjs/nest/compare/v11.1.4...9bb0560e79743cc0bd2ce198c65e21332200c3ad)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.4...v11.1.5)

###
[`v11.1.4`](https://redirect.github.com/nestjs/nest/compare/v11.1.3...1f101ac8b0a5bb5b97a7caf6634fcea8d65196e0)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.3...v11.1.4)

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/platform-express)</summary>

###
[`v11.1.5`](https://redirect.github.com/nestjs/nest/compare/v11.1.4...9bb0560e79743cc0bd2ce198c65e21332200c3ad)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.4...v11.1.5)

###
[`v11.1.4`](https://redirect.github.com/nestjs/nest/compare/v11.1.3...1f101ac8b0a5bb5b97a7caf6634fcea8d65196e0)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.3...v11.1.4)

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/platform-socket.io)</summary>

###
[`v11.1.5`](https://redirect.github.com/nestjs/nest/releases/tag/v11.1.5)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.4...v11.1.5)

#### v11.1.5 (2025-07-18)

##### Dependencies

- `platform-express`
- [#&#8203;15425](https://redirect.github.com/nestjs/nest/pull/15425)
chore(deps): bump multer from 2.0.1 to 2.0.2 in
/packages/platform-express
([@&#8203;dependabot\[bot\]](https://redirect.github.com/apps/dependabot))

###
[`v11.1.4`](https://redirect.github.com/nestjs/nest/releases/tag/v11.1.4)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.3...v11.1.4)

##### v11.1.4 (2025-07-16)

##### Bug fixes

- `platform-fastify`
- [#&#8203;15385](https://redirect.github.com/nestjs/nest/pull/15385)
fix(testing): auto-init fastify adapter for middleware registration
([@&#8203;mag123c](https://redirect.github.com/mag123c))
- `core`, `testing`
- [#&#8203;15405](https://redirect.github.com/nestjs/nest/pull/15405)
fix(core): fix race condition in class dependency resolution
([@&#8203;hajekjiri](https://redirect.github.com/hajekjiri))
- `core`
- [#&#8203;15333](https://redirect.github.com/nestjs/nest/pull/15333)
fix(core): Make flattenRoutePath return a valid module
([@&#8203;gentunian](https://redirect.github.com/gentunian))
- `microservices`
- [#&#8203;15305](https://redirect.github.com/nestjs/nest/pull/15305)
fix(microservices): Revisit RMQ pattern matching with wildcards
([@&#8203;getlarge](https://redirect.github.com/getlarge))
- [#&#8203;15250](https://redirect.github.com/nestjs/nest/pull/15250)
fix(constants): update RMQ\_DEFAULT\_QUEUE to an empty string
([@&#8203;EeeasyCode](https://redirect.github.com/EeeasyCode))

##### Enhancements

- `platform-fastify`
- [#&#8203;14789](https://redirect.github.com/nestjs/nest/pull/14789)
feat(fastify): add decorator for custom schema
([@&#8203;piotrfrankowski](https://redirect.github.com/piotrfrankowski))
- `common`, `core`, `microservices`, `platform-express`,
`platform-fastify`, `websockets`
- [#&#8203;15386](https://redirect.github.com/nestjs/nest/pull/15386)
feat: enhance introspection capabilities
([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))
- `core`
- [#&#8203;15374](https://redirect.github.com/nestjs/nest/pull/15374)
feat: supporting fine async storage control
([@&#8203;Farenheith](https://redirect.github.com/Farenheith))

##### Dependencies

- `platform-ws`
- [#&#8203;15350](https://redirect.github.com/nestjs/nest/pull/15350)
chore(deps): bump ws from 8.18.2 to 8.18.3
([@&#8203;dependabot\[bot\]](https://redirect.github.com/apps/dependabot))
- `platform-fastify`
- [#&#8203;15278](https://redirect.github.com/nestjs/nest/pull/15278)
chore(deps): bump fastify from 5.3.3 to 5.4.0
([@&#8203;dependabot\[bot\]](https://redirect.github.com/apps/dependabot))

##### Committers: 11

- Alexey Filippov
([@&#8203;SocketSomeone](https://redirect.github.com/SocketSomeone))
- EFIcats ([@&#8203;ext4cats](https://redirect.github.com/ext4cats))
- Edouard Maleix
([@&#8203;getlarge](https://redirect.github.com/getlarge))
- JaeHo Jang ([@&#8203;mag123c](https://redirect.github.com/mag123c))
- Jiri Hajek
([@&#8203;hajekjiri](https://redirect.github.com/hajekjiri))
- Kamil Mysliwiec
([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))
- Khan / 이창민
([@&#8203;EeeasyCode](https://redirect.github.com/EeeasyCode))
- Peter F.
([@&#8203;piotrfrankowski](https://redirect.github.com/piotrfrankowski))
- Sebastian ([@&#8203;gentunian](https://redirect.github.com/gentunian))
- Thiago Oliveira Santos
([@&#8203;Farenheith](https://redirect.github.com/Farenheith))
- jochong ([@&#8203;jochongs](https://redirect.github.com/jochongs))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 06:05:37 +00:00
renovate[bot] 8cfaee8232 chore: bump up on-headers version to v1.1.0 [SECURITY] (#13260)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [on-headers](https://redirect.github.com/jshttp/on-headers) | [`1.0.2`
-> `1.1.0`](https://renovatebot.com/diffs/npm/on-headers/1.0.2/1.1.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/on-headers/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/on-headers/1.0.2/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

### GitHub Vulnerability Alerts

####
[CVE-2025-7339](https://redirect.github.com/jshttp/on-headers/security/advisories/GHSA-76c9-3jph-rj3q)

### Impact

A bug in on-headers versions `< 1.1.0` may result in response headers
being inadvertently modified when an array is passed to
`response.writeHead()`

### Patches

Users should upgrade to `1.1.0`

### Workarounds

Uses are encouraged to upgrade to `1.1.0`, but this issue can be worked
around by passing an object to `response.writeHead()` rather than an
array.

---

### Release Notes

<details>
<summary>jshttp/on-headers (on-headers)</summary>

###
[`v1.1.0`](https://redirect.github.com/jshttp/on-headers/blob/HEAD/HISTORY.md#110--2025-07-17)

[Compare
Source](https://redirect.github.com/jshttp/on-headers/compare/v1.0.2...v1.1.0)

\==================

- Fix [CVE-2025-7339](https://www.cve.org/CVERecord?id=CVE-2025-7339)
([GHSA-76c9-3jph-rj3q](https://redirect.github.com/jshttp/on-headers/security/advisories/GHSA-76c9-3jph-rj3q))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no
schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4yMy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMjMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 02:15:02 +00:00
DarkSky c4cf5799d4 fix(server): exclude outdated doc id style in embedding count (#13269)
fix AI-392
fix AI-393

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

## Summary by CodeRabbit

* **New Features**
* Improved filtering of outdated document ID styles in embedding status
reporting, ensuring more accurate counts of embedded documents.
* Stricter rate limiting applied to workspace embedding status queries
for enhanced system reliability.

* **Bug Fixes**
* Resolved issues with duplicate or outdated document IDs affecting
embedding status totals.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-21 10:58:29 +00:00
德布劳外 · 贾贵 b53b4884cf refactor(core): align markdown conversion logic (#13254)
## Refactor

Align the Markdown conversion logic across all business modules:
1. frontend/backend apply: doc to markdown
2. insert/import markdown: use `markdownAdapter.toDoc`

> CLOSE AI-328 AI-379 AI-380

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

* **Documentation**
* Clarified instructions and provided an explicit example for correct
list item formatting in the markdown editing tool.

* **Bug Fixes**
* Improved markdown parsing for lists, ensuring correct indentation and
handling of trailing newlines.
* Cleaned up markdown snapshot test files by removing redundant blank
lines for better readability.

* **Refactor**
* Updated markdown conversion logic to use a new parsing approach for
improved reliability and maintainability.
* Enhanced markdown generation method for document snapshots with
improved error handling.
* Refined markdown-to-snapshot conversion with more robust document
handling and snapshot extraction.

* **Chores**
* Added a new workspace dependency for enhanced markdown parsing
capabilities.
* Updated project references and workspace dependencies to include the
new markdown parsing package.

* **Tests**
* Temporarily disabled two markdown-related tests due to parse errors in
test mode.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-21 10:35:13 +00:00
EYHN 0525c499a1 feat(core): enable two step journal by default (#13283)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* The journal confirmation flow is now always enabled when creating or
opening journals across all platforms.

* **Refactor**
* Removed the two-step journal confirmation feature flag and all related
conditional logic.
* Simplified journal navigation and creation flows for a more consistent
user experience.

* **Chores**
* Cleaned up unused components and imports related to the removed
feature flag.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-21 10:24:33 +00:00
EYHN 43f8d852d8 feat(ios): ai button feature flag (#13280)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Added a global API to check the AI button feature flag status.

* **Bug Fixes**
* Improved handling for the AI button feature: the app now checks the
feature flag before proceeding and provides a clear error if the feature
is disabled.

* **Refactor**
  * Removed all AI button presentation and dismissal logic from the app.
* Deleted unused plugin interfaces and registration related to the AI
button feature.

* **Chores**
  * Updated project metadata and build configuration for iOS.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-21 10:07:32 +00:00
DarkSky 06eb17387a chore(server): relax list session permission (#13268)
fix AI-326

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

## Summary by CodeRabbit

* **Bug Fixes**
* Adjusted permission checks for viewing histories and chats to require
read access instead of update access on documents.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-21 10:02:51 +00:00
EYHN 436d5e5079 fix(core): allow mobile connect selfhost without https (#13279)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated Android and iOS app configurations to allow non-HTTPS
(cleartext) network traffic.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-21 05:57:25 +00:00
Cats Juice 52e69e0dde feat(mobile): add two step confirmation for mobile journal (#13266)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a new mobile journals page with a two-step confirmation
flow, allowing users to select a date and confirm before creating or
opening a journal.
  * Added a dedicated route for journals on mobile devices.
* Implemented a placeholder view when no journal exists for a selected
date on both desktop and mobile.

* **Enhancements**
* Improved mobile and desktop styling for journals pages, including
responsive adjustments for mobile layouts.
* Updated journal navigation behavior based on a feature flag, enabling
or disabling the two-step confirmation flow.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-21 05:30:24 +00:00
德布劳外 · 贾贵 612c73cab1 fix(core): code-edit param maybe json string (#13278)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved input handling for code editing by allowing the tool to
accept both arrays and JSON string representations for the `code_edit`
parameter, ensuring more robust and flexible input validation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-21 03:38:21 +00:00
Lakr b7c026bbe8 feat: ai now working again (#13196)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added support for displaying title and summary fields in workspace
pages.
* Introduced a menu in the chat header with a "Clear History" option to
remove chat history.

* **Improvements**
* Enhanced chat message handling with asynchronous context preparation
and improved markdown processing.
* Simplified chat input and assistant message rendering for better
performance and maintainability.
* Updated dependency versions for improved stability and compatibility.

* **Bug Fixes**
* Ensured chat features are available in all build configurations, not
just debug mode.

* **Chores**
* Removed unused dependencies and internal code, and disabled certain
function bar options.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-21 02:49:20 +00:00
DarkSky 013a6ceb7e feat(server): add compatibility for ios client (#13263)
fix AI-355

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

## Summary by CodeRabbit

* **New Features**
* Added support for uploading a single file as an attachment when
creating chat messages, in addition to existing multiple file uploads.

* **Tests**
* Expanded test coverage to verify message creation with both single and
multiple file attachments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-18 08:31:26 +00:00
Yii fa42e3619f ci: adjuest minimal approves of image release job 2025-07-18 15:33:18 +08:00
Peng Xiao edd97ae73b fix(core): share page should have basename correctly set (#13256)
fix AF-2760

#### PR Dependency Tree


* **PR #13256** 👈

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

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

## Summary by CodeRabbit

* **New Features**
* Improved synchronization of workspace information with the URL path,
ensuring the displayed workspace name stays up-to-date when navigating
within the workspace share page.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 14:01:38 +00:00
Wu Yue 0770b109cb feat(core): add ai draft service (#13252)
Close [AI-244](https://linear.app/affine-design/issue/AI-244)

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

* **New Features**
* Added AI chat draft persistence, allowing your chat input, quotes,
markdown, and images to be automatically saved and restored across
sessions.
* Drafts are now synchronized across chat components, so you won’t lose
your progress if you navigate away or refresh the page.

* **Improvements**
* Enhanced chat experience with seamless restoration of previously
entered content and attachments.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 09:42:01 +00:00
Cats Juice 4018b3aeca fix(component): mobile menu bottom padding not work (#13249)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved safe area styling by ensuring a default padding is applied
when certain variables are not set, resulting in more consistent layout
spacing across different scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 09:23:04 +00:00
Cats Juice c90d511251 feat(core): server version check for selfhost login (#13247)
close AF-2752;

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

* **New Features**
* Added a version compatibility check for self-hosted environments
during sign-in, displaying a clear error message and upgrade
instructions if the server version is outdated.
* **Style**
* Updated the appearance of the notification icon in the mobile header
for improved visual consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 09:21:51 +00:00
DarkSky bdf1389258 feat(server): improve transcript (#13253)
fix AF-2758
fix AF-2759
2025-07-17 09:20:14 +00:00
德布劳外 · 贾贵 dc68c2385d fix(core): ai apply ui opt (#13238)
> CLOSE AI-377 AI-372 AI-373 AI-381 AI-378 AI-374 AI-382 AI-375

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

## Summary by CodeRabbit

* **Style**
* Improved button styling for tool controls, including hover effects and
consistent padding.
* Updated background colors and border placements for result cards and
headers.

* **New Features**
  * Added tooltips to control buttons for enhanced user guidance.

* **Bug Fixes**
* Improved accessibility by replacing clickable spans with button
elements.
* Updated loading indicators to use a spinner icon for clearer feedback
during actions.

* **Refactor**
* Simplified layout and reduced unnecessary wrapper elements for cleaner
rendering.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 09:16:26 +00:00
Cats Juice 07f2f7b5a8 fix(core): hide intelligence entrance when ai is disabled (#13251)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* The AI Chat button is now only visible when AI features are enabled
and supported by the server, ensuring users see it only when available.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 08:14:36 +00:00
Peng Xiao 38107910f9 fix(core): comment action button bg color (#13250)
fix BS-3623

Also use enter instead of enter+CMD/CTRL to commit comment/reply

#### PR Dependency Tree


* **PR #13250** 👈

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

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

## Summary by CodeRabbit

* **New Features**
* Updated comment editor so comments are now submitted by pressing Enter
(without CMD or CTRL).
* **Style**
* Improved visual styling for action buttons in the comment sidebar for
a more consistent appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 07:23:40 +00:00
Cats Juice ea21de8311 feat(core): add flag for two-step journal conformation (#13246)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a feature flag to control a two-step journal confirmation
process.
* Users may now experience either an immediate journal opening or a
confirmation step before journal creation, depending on the feature flag
status.

* **Chores**
* Added a new feature flag for two-step journal confirmation,
configurable in canary builds.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 07:03:32 +00:00
L-Sun 21360591a9 chore(editor): add table and callout entries for mobile (#13245)
Close
[AF-2755](https://linear.app/affine-design/issue/AF-2755/table-block支持)

#### PR Dependency Tree


* **PR #13245** 👈

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

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

## Summary by CodeRabbit

* **New Features**
* Added "Table" and "Callout" options to the keyboard toolbar, allowing
users to insert table and callout blocks directly from the toolbar when
available.

* **Chores**
* Updated internal dependencies to support new block types and maintain
compatibility.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 04:17:00 +00:00
L-Sun 5300eff8f1 fix(editor): at-menu boundary in chat pannel (#13241)
Close
[BS-3621](https://linear.app/affine-design/issue/BS-3621/comment-menu-需要规避边缘)

#### PR Dependency Tree


* **PR #13241** 👈

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

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

* **Bug Fixes**
* Improved the positioning of the linked document popover to ensure it
displays correctly, even when its width is not initially rendered.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #13241** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-17 02:47:45 +00:00
Cats Juice 46a2ad750f feat(core): add a two-step confirm page to create new journal (#13240)
close AF-2750;

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

* **New Features**
* Introduced a new workspace journals page with date-based navigation,
placeholder UI, and the ability to create daily journals directly from
the page.
* Added a "Today" button for quick navigation to the current day's
journal when viewing other dates.

* **Improvements**
* Enhanced the journal document title display with improved date
formatting and flexible styling.
* Expanded the active state for the journal sidebar button to cover all
journal-related routes.
* Updated journal navigation to open existing entries directly or
navigate to filtered journal listings.

* **Bug Fixes**
* Improved date handling and navigation logic for journal entries to
ensure accurate redirection and creation flows.

* **Style**
* Added new styles for the workspace journals page, including headers,
placeholders, and buttons.

* **Localization**
* Added English translations for journal placeholder text and create
journal prompts.

* **Tests**
* Added confirmation steps in journal creation flows to improve test
reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 02:32:01 +00:00
Kieran Cui 3949714618 fix(core): optimize settings dialog's left sidebar scroll style (#13237)
change from scrolling the entire left side to scrolling menu items

**before**


https://github.com/user-attachments/assets/85d5c518-5160-493e-9010-431e6f0ed51b



**after**


https://github.com/user-attachments/assets/2efcdfde-7005-4d38-8dfb-2aef5e123946




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

## Summary by CodeRabbit

* **New Features**
* Added vertical scrolling with a visible scrollbar to the settings
sidebar for easier navigation of setting groups.

* **Style**
* Updated sidebar padding and spacing for improved layout and
appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-16 07:16:55 +00:00
德布劳外 · 贾贵 7b9e0a215d fix(core): css var for apply delete diff (#13235)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Updated the background color variable for deleted blocks to improve
consistency with the latest theme settings. No visible changes expected
unless custom theme variables are in use.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-16 06:55:21 +00:00
德布劳外 · 贾贵 b93d5d5e86 fix(core): apply insert in same position not refresh (#13210)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Improved the rendering process for block inserts, resulting in more
efficient and streamlined updates when viewing block differences. No
changes to user-facing features or behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-16 06:54:06 +00:00
Cats Juice c8dc51ccae feat(core): highlight active session in history (#13212)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added visual highlighting for the selected session in the session
history list.
* Improved accessibility by indicating the selected session for
assistive technologies.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-16 06:53:35 +00:00
EYHN cdff5c3117 feat(core): add context menu for navigation and explorer (#13216)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced a customizable context menu component for desktop
interfaces, enabling right-click menus in various UI elements.
* Added context menu support to document list items and navigation tree
nodes, allowing users to access additional operations via right-click.
* **Improvements**
* Enhanced submenu and menu item components to support both dropdown and
context menu variants based on context.
* Updated click handling in workbench links to prevent unintended
actions on non-left mouse button clicks.
* **Chores**
* Added `@radix-ui/react-context-menu` as a dependency to relevant
frontend packages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-16 04:40:10 +00:00
EYHN d44771dfe9 feat(electron): add global context menu (#13218)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added automatic synchronization of language settings between the
desktop app and the system environment.
* Context menu actions (Cut, Copy, Paste) in the desktop app are now
localized according to the selected language.

* **Improvements**
* Context menu is always available with standard editing actions,
regardless of spell check settings.

* **Localization**
* Added translations for "Cut", "Copy", and "Paste" in the context menu.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-16 04:37:38 +00:00
Yii 45b05f06b3 fix(core): demo workspace (#13234)
do not show demo workspace before config fetched for selfhost instances

fixes https://github.com/toeverything/AFFiNE/issues/13219

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

## Summary by CodeRabbit

* **Refactor**
* Updated the list of available features for self-hosted server
configurations. No visible changes to exported interfaces or public
APIs.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-16 04:16:03 +00:00
Peng Xiao 04e002eb77 feat(core): optimize artifact preview loading (#13224)
fix AI-369

#### PR Dependency Tree


* **PR #13224** 👈

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

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

## Summary by CodeRabbit

* **New Features**
* Introduced a loading skeleton component for artifact previews,
providing a smoother visual experience during loading states.
* Artifact loading skeleton is now globally available as a custom
element.

* **Refactor**
* Streamlined icon and loading state handling in AI tools, centralizing
logic and removing redundant loading indicators.
* Simplified card metadata by removing loading and icon properties from
card meta methods.

* **Chores**
* Improved resource management for code block highlighting, ensuring
efficient disposal and avoiding unnecessary operations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-16 02:08:32 +00:00
DarkSky a444941b79 fix(server): delay send mail if retry many times (#13225)
fix AF-2748

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

* **New Features**
* Improved mail sending job with adaptive retry delays based on elapsed
time, enhancing reliability of email delivery.

* **Chores**
* Updated job payload to include a start time for better retry
management.
* Added an internal delay utility to support asynchronous pause in
processes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 12:21:42 +00:00
Cats Juice 39e0ec37fd fix(core): prevent reload pinned chat infinitely (#13226)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved chat stability by centralizing and simplifying the logic for
resetting chat content, reducing unnecessary reloads and preventing
infinite loading cycles.

* **Refactor**
* Streamlined internal chat content management for more reliable session
handling and smoother user experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 12:03:41 +00:00
DarkSky cc1d5b497a feat(server): cleanup trashed doc's embedding (#13201)
fix AI-359

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

* **New Features**
* Added automated cleanup of embeddings for documents deleted or trashed
from workspaces.
* Introduced a new job to schedule and perform this cleanup per
workspace daily and on demand.
  * Added new GraphQL mutation to manually trigger the cleanup process.
* Added the ability to list workspaces with flexible filtering and
selection options.

* **Improvements**
* Enhanced document status handling to more accurately reflect embedding
presence.
* Refined internal methods for managing and checking document
embeddings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 12:00:33 +00:00
Wu Yue a4b535a42a feat(core): support lazy load for ai session history (#13221)
Close [AI-331](https://linear.app/affine-design/issue/AI-331)

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

## Summary by CodeRabbit

* **New Features**
* Added infinite scroll and incremental loading for AI session history,
allowing users to load more sessions as they scroll.

* **Refactor**
* Improved session history component with better state management and
modular rendering for loading, empty, and history states.

* **Bug Fixes**
* Enhanced handling of absent or uninitialized chat sessions, reducing
potential errors when session data is missing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 11:43:36 +00:00
DarkSky c797cac87d feat(server): clear semantic search metadata (#13197)
fix AI-360

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

* **New Features**
* Search results now display document metadata enriched with author
information.

* **Improvements**
* Search result content is cleaner, with leading metadata lines (such as
titles and creation dates) removed from document excerpts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 11:16:34 +00:00
Cats Juice 339ecab00f fix(core): the down arrow may show when showLinkedDoc not configured (#13220)
The original setting object on user's device not defined, so the default
value `true` won't work.

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved reliability of sidebar and appearance settings by ensuring
toggle switches consistently reflect the correct on/off state.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 09:32:10 +00:00
DarkSky 8e374f5517 feat(server): skip embedding for deprecated doc ids & empty docs (#13211)
fix AI-367

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

* **Bug Fixes**
* Improved document filtering to exclude settings documents and empty
blobs from embedding and status calculations.
* Enhanced embedding jobs to skip processing deprecated documents if a
newer version exists, ensuring only up-to-date documents are embedded.
* **New Features**
* Added a mutation to trigger the cron job for generating missing
titles.
* **Tests**
* Added test to verify exclusion of documents with empty content from
embedding.
* Updated embedding-related tests to toggle embedding state during
attachment upload under simulated network conditions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 08:50:48 +00:00
Cats Juice cd91bea5c1 feat(core): open doc in semantic and keyword result (#13217)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added clickable document titles in AI chat search results, allowing
users to open documents directly from chat interactions.
* Enhanced interactivity in AI chat by making relevant search result
titles visually indicate clickability (pointer cursor).

* **Style**
* Updated styles to visually highlight clickable search result titles in
AI chat results.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 08:06:17 +00:00
L-Sun 613597e642 feat(core): notification entry for mobile (#13214)
#### PR Dependency Tree


* **PR #13214** 👈

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

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

* **New Features**
* Added a notification icon with a live badge displaying the
notification count in the mobile home header. The badge dynamically
adjusts and caps the count at "99+".
* Introduced a notification menu in the mobile header, allowing users to
view their notifications directly.

* **Style**
* Improved notification list responsiveness on mobile by making it full
width.
* Enhanced the appearance of the notification badge for better
visibility.
* Updated the app fallback UI to display skeleton placeholders for both
notification and settings icons.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 07:45:37 +00:00
Cats Juice a597bdcdf6 fix(core): sidebar ai layout (#13215)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Improved chat panel layout with flexible vertical sizing and
alignment.
* Updated padding for chat panel titles to ensure consistent appearance
even if CSS variables are missing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 07:27:55 +00:00
EYHN 316c671c92 fix(core): error when delete tags (#13207)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Adjusted the placement of a conditional check to improve code
organization. No changes to user-facing functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 06:50:54 +00:00
Yii 95a97b793c ci: release tag should start with 'v' 2025-07-15 15:07:16 +08:00
Yii eb24074871 ci: manually approve ci requires issue wirte permission 2025-07-15 14:57:47 +08:00
Peng Xiao 2a8f18504b fix(core): electron storage sync (#13213)
#### PR Dependency Tree


* **PR #13213** 👈

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

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

## Summary by CodeRabbit

* **New Features**
* Added version tracking for global state and cache updates, enabling
synchronized updates across multiple windows.
* Introduced a unique client identifier to prevent processing
self-originated updates.
* **Refactor**
* Improved event broadcasting for global state and cache changes,
ensuring more reliable and efficient update propagation.
* **Chores**
* Updated internal logic to support structured event formats and
revision management for shared storage.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 06:45:05 +00:00
Wu Yue b85afa7394 refactor(core): extract ai-chat-panel-title component (#13209)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a dedicated AI chat panel title bar with dynamic embedding
progress display and an optional playground button.
* Added a modal playground interface accessible from the chat panel
title when enabled.

* **Refactor**
* Moved the chat panel title and related UI logic into a new, reusable
component for improved modularity.
* Simplified the chat content area by removing the internal chat title
rendering and related methods.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 02:56:57 +00:00
Peng Xiao 8ec4bbb298 fix(core): comment empty style issue (#13208)
fix BS-3618

#### PR Dependency Tree


* **PR #13208** 👈

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

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

## Summary by CodeRabbit

* **Style**
* Improved the appearance of the empty state in the comment sidebar by
centering the text and adjusting line spacing for better readability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 02:48:33 +00:00
德布劳外 · 贾贵 812c199b45 feat: split individual semantic change (#13155)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced a new AI-powered document update feature, allowing users to
apply multiple independent block-level edits to Markdown documents.
* Added support for applying document updates via a new GraphQL query,
enabling seamless integration with the frontend.

* **Enhancements**
* Improved the document editing tool to handle and display multiple
simultaneous edit operations with better UI feedback and state
management.
* Expanded model support with new "morph-v3-fast" and "morph-v3-large"
options for document update operations.
* Enhanced frontend components and services to support asynchronous
application and acceptance of multiple document edits independently.

* **Bug Fixes**
* Enhanced error handling and user notifications for failed document
update operations.

* **Documentation**
* Updated tool descriptions and examples to clarify the new multi-edit
workflow and expected input/output formats.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

> CLOSE AI-337
2025-07-15 02:34:01 +00:00
Cats Juice 36bd8f645a fix(editor): memory leak caused by missing unsubscription from autoUpdate (#13205)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved resource cleanup for floating UI elements and popups to
prevent potential memory leaks and ensure proper disposal when
components are removed or updated.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 02:27:48 +00:00
Peng Xiao 7cff8091e4 fix: ai artifact preview styles (#13203)
source: https://x.com/yisibl/status/1944679763991568639

#### PR Dependency Tree


* **PR #13203** 👈

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

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

* **Style**
  * Updated global text spacing for improved visual consistency.
* Enhanced scrolling behavior and layout in artifact preview and code
artifact components for smoother navigation.
* Refined document composition preview styling for improved layout
control.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->





#### PR Dependency Tree


* **PR #13203** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-15 01:52:58 +00:00
Cats Juice de8feb98a3 feat(core): remount ai-chat-content when session changed (#13200)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Updated chat session management to fully remove and reset chat content
instead of updating and reloading it in place. This change may improve
stability and clarity when starting new chat sessions or switching
between them.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 10:59:58 +00:00
L-Sun fbd6e8fa97 fix(editor): use inline-block style for inline comment (#13204)
#### PR Dependency Tree


* **PR #13204** 👈

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

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

## Summary by CodeRabbit

* **Style**
* Updated the display behavior of inline comments to improve their
alignment and appearance within text.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 10:51:20 +00:00
DarkSky bcf6bd1dfc feat(server): allow fork session to other doc (#13199)
fix AI-365
2025-07-14 10:33:59 +00:00
Peng Xiao 8627560fd5 chore(core): change audio transcription job to use gemini 2.5 pro (#13202)
#### PR Dependency Tree


* **PR #13202** 👈

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

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

* **New Features**
* Improved the "Transcript audio" text action by updating its default AI
model to "gemini-2.5-pro" for enhanced performance.
* Enhanced audio transcription accuracy by refining audio content
handling with a more specific audio format.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DarkSky <darksky2048@gmail.com>
2025-07-14 09:49:42 +00:00
DarkSky 9a3e44c6d6 feat(server): add generate title cron resolver (#13189)
fix AI-350

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

* **New Features**
* Added a new option to manually trigger the generation of missing
session titles via a GraphQL query.

* **Improvements**
* The process for generating missing session titles now considers all
eligible sessions, without limiting the number processed at a time.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 09:19:21 +00:00
249 changed files with 5150 additions and 1850 deletions
+1 -1
View File
@@ -465,7 +465,7 @@ jobs:
name: ${{ env.RELEASE_VERSION }}
draft: ${{ inputs.build-type == 'stable' }}
prerelease: ${{ inputs.build-type != 'stable' }}
tag_name: ${{ env.RELEASE_VERSION}}
tag_name: v${{ env.RELEASE_VERSION}}
files: |
./release/*
./release/.env.example
+2
View File
@@ -34,6 +34,7 @@ permissions:
packages: write
security-events: write
attestations: write
issues: write
jobs:
prepare:
@@ -74,6 +75,7 @@ jobs:
with:
secret: ${{ secrets.GITHUB_TOKEN }}
approvers: forehalo,fengmk2
minimum-approvals: 1
fail-on-denial: true
issue-title: Please confirm to release docker image
issue-body: |
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
shell: cmd
run: |
cd ${{ env.ARCHIVE_DIR }}/out
signtool sign /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a ${{ inputs.files }}
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
- name: zip file
shell: cmd
run: |
@@ -39,6 +39,13 @@ export class CodeBlockHighlighter extends LifeCycleWatcher {
private readonly _loadTheme = async (
highlighter: HighlighterCore
): Promise<void> => {
// It is possible that by the time the highlighter is ready all instances
// have already been unmounted. In that case there is no need to load
// themes or update state.
if (CodeBlockHighlighter._refCount === 0) {
return;
}
const config = this.std.getOptional(CodeBlockConfigExtension.identifier);
const darkTheme = config?.theme?.dark ?? CODE_BLOCK_DEFAULT_DARK_THEME;
const lightTheme = config?.theme?.light ?? CODE_BLOCK_DEFAULT_LIGHT_THEME;
@@ -78,14 +85,27 @@ export class CodeBlockHighlighter extends LifeCycleWatcher {
override unmounted(): void {
CodeBlockHighlighter._refCount--;
// Only dispose the shared highlighter when no instances are using it
if (
CodeBlockHighlighter._refCount === 0 &&
CodeBlockHighlighter._sharedHighlighter
) {
CodeBlockHighlighter._sharedHighlighter.dispose();
// Dispose the shared highlighter **after** any in-flight creation finishes.
if (CodeBlockHighlighter._refCount !== 0) {
return;
}
const doDispose = (highlighter: HighlighterCore | null) => {
if (highlighter) {
highlighter.dispose();
}
CodeBlockHighlighter._sharedHighlighter = null;
CodeBlockHighlighter._highlighterPromise = null;
};
if (CodeBlockHighlighter._sharedHighlighter) {
// Highlighter already created dispose immediately.
doDispose(CodeBlockHighlighter._sharedHighlighter);
} else if (CodeBlockHighlighter._highlighterPromise) {
// Highlighter still being created wait for it, then dispose.
CodeBlockHighlighter._highlighterPromise
.then(doDispose)
.catch(console.error);
}
}
}
@@ -1,6 +1,7 @@
import { ImageBlockModel } from '@blocksuite/affine-model';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
@@ -49,6 +50,10 @@ const builtinToolbarConfig = {
});
},
},
{
id: 'c.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',
@@ -634,9 +634,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
const movedElements = new Set([
...selectedElements,
...selectedElements
.map(el => (isGfxGroupCompatibleModel(el) ? el.descendantElements : []))
.flat(),
...selectedElements.flatMap(el =>
isGfxGroupCompatibleModel(el) ? el.descendantElements : []
),
]);
movedElements.forEach(element => {
@@ -5,6 +5,7 @@ import {
} from '@blocksuite/affine-shared/commands';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
@@ -61,6 +62,10 @@ export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
surfaceRefBlock.captionElement.show();
},
},
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
id: 'a.clipboard',
placement: ActionPlacement.More,
@@ -85,6 +85,8 @@ export class MenuSubMenu extends MenuFocusable {
.catch(err => console.error(err));
});
this.menu.openSubMenu(menu);
// in case that the menu is not closed, but the component is removed,
this.disposables.add(unsub);
}
protected override render(): unknown {
@@ -68,5 +68,5 @@ export function getHeadingBlocksFromDoc(
ignoreEmpty = false
) {
const notes = getNotesFromStore(store, modes);
return notes.map(note => getHeadingBlocksFromNote(note, ignoreEmpty)).flat();
return notes.flatMap(note => getHeadingBlocksFromNote(note, ignoreEmpty));
}
@@ -116,6 +116,7 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
`;
private _cleanup: (() => void) | null = null;
private _autoUpdateCleanup: (() => void) | null = null;
private _prevTool: ToolOptionWithType | null = null;
@@ -128,6 +129,11 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
return [TemplateCard1[theme], TemplateCard2[theme], TemplateCard3[theme]];
}
override connectedCallback() {
super.connectedCallback();
this.disposables.add(() => this._autoUpdateCleanup?.());
}
private _closePanel() {
if (this._openedPanel) {
this._openedPanel.remove();
@@ -175,8 +181,8 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
requestAnimationFrame(() => {
const arrowEl = panel.renderRoot.querySelector('.arrow') as HTMLElement;
autoUpdate(this, panel, () => {
this._autoUpdateCleanup?.();
this._autoUpdateCleanup = autoUpdate(this, panel, () => {
computePosition(this, panel, {
placement: 'top',
middleware: [offset(20), arrow({ element: arrowEl }), shift()],
@@ -103,54 +103,52 @@ export class InlineCommentManager extends LifeCycleWatcher {
id: CommentId,
selections: BaseSelection[]
) => {
const needCommentTexts = selections
.map(selection => {
if (!selection.is(TextSelection)) return [];
const [_, { selectedBlocks }] = this.std.command
.chain()
.pipe(getSelectedBlocksCommand, {
textSelection: selection,
})
.run();
const needCommentTexts = selections.flatMap(selection => {
if (!selection.is(TextSelection)) return [];
const [_, { selectedBlocks }] = this.std.command
.chain()
.pipe(getSelectedBlocksCommand, {
textSelection: selection,
})
.run();
if (!selectedBlocks) return [];
if (!selectedBlocks) return [];
type MakeRequired<T, K extends keyof T> = T & {
[key in K]: NonNullable<T[key]>;
};
type MakeRequired<T, K extends keyof T> = T & {
[key in K]: NonNullable<T[key]>;
};
return selectedBlocks
.map(
({ model }) =>
[model, getInlineEditorByModel(this.std, model)] as const
)
.filter(
(
pair
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
!!pair[0].text && !!pair[1]
)
.map(([model, inlineEditor]) => {
let from: TextRangePoint;
let to: TextRangePoint | null;
if (model.id === selection.from.blockId) {
from = selection.from;
to = null;
} else if (model.id === selection.to?.blockId) {
from = selection.to;
to = null;
} else {
from = {
blockId: model.id,
index: 0,
length: model.text.yText.length,
};
to = null;
}
return [new TextSelection({ from, to }), inlineEditor] as const;
});
})
.flat();
return selectedBlocks
.map(
({ model }) =>
[model, getInlineEditorByModel(this.std, model)] as const
)
.filter(
(
pair
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
!!pair[0].text && !!pair[1]
)
.map(([model, inlineEditor]) => {
let from: TextRangePoint;
let to: TextRangePoint | null;
if (model.id === selection.from.blockId) {
from = selection.from;
to = null;
} else if (model.id === selection.to?.blockId) {
from = selection.to;
to = null;
} else {
from = {
blockId: model.id,
index: 0,
length: model.text.yText.length,
};
to = null;
}
return [new TextSelection({ from, to }), inlineEditor] as const;
});
});
if (needCommentTexts.length === 0) return;
@@ -22,8 +22,11 @@ import { isEqual } from 'lodash-es';
})
export class InlineComment extends WithDisposable(ShadowlessElement) {
static override styles = css`
inline-comment {
display: inline;
}
inline-comment.unresolved {
display: inline-block;
background-color: ${unsafeCSSVarV2('block/comment/highlightDefault')};
border-bottom: 2px solid
${unsafeCSSVarV2('block/comment/highlightUnderline')};
@@ -64,7 +64,7 @@ export const blockCommentToolbarButton: Omit<ToolbarAction, 'id'> = {
// may be hover on a block or element, in this case
// the selection is empty, so we need to get the current model
if (model && selections.length === 0) {
if (model) {
if (model instanceof BlockModel) {
commentProvider.addComment([
new BlockSelection({
@@ -5,6 +5,7 @@ import type { Signal } from '@preact/signals-core';
import type { AffineUserInfo } from './types';
export interface UserService {
currentUserInfo$: Signal<AffineUserInfo | null>;
userInfo$(id: string): Signal<AffineUserInfo | null>;
isLoading$(id: string): Signal<boolean>;
error$(id: string): Signal<string | null>; // user friendly error string
@@ -11,14 +11,12 @@ export function getSelectedRect(selected: GfxModel[]): DOMRect {
return new DOMRect();
}
const lockedElementsByFrame = selected
.map(selectable => {
if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
return selectable.descendantElements;
}
return [];
})
.flat();
const lockedElementsByFrame = selected.flatMap(selectable => {
if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
return selectable.descendantElements;
}
return [];
});
selected = [...new Set([...selected, ...lockedElementsByFrame])];
@@ -20,6 +20,7 @@
"@blocksuite/affine-block-paragraph": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-block-surface-ref": "workspace:*",
"@blocksuite/affine-block-table": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-fragment-doc-title": "workspace:*",
@@ -18,6 +18,7 @@ import {
} from '@blocksuite/affine-block-paragraph';
import { DefaultTool, getSurfaceBlock } from '@blocksuite/affine-block-surface';
import { insertSurfaceRefBlockCommand } from '@blocksuite/affine-block-surface-ref';
import { insertTableBlockCommand } from '@blocksuite/affine-block-table';
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
import { toast } from '@blocksuite/affine-components/toast';
import { insertInlineLatex } from '@blocksuite/affine-inline-latex';
@@ -40,14 +41,20 @@ import {
deleteSelectedModelsCommand,
draftSelectedModelsCommand,
duplicateSelectedModelsCommand,
focusBlockEnd,
getBlockSelectionsCommand,
getSelectedModelsCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import {
FeatureFlagService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import type { AffineTextStyleAttributes } from '@blocksuite/affine-shared/types';
import {
createDefaultDoc,
isInsideBlockByFlavour,
openSingleFileWith,
type Signal,
} from '@blocksuite/affine-shared/utils';
@@ -87,6 +94,7 @@ import {
RedoIcon,
RightTabIcon,
StrikeThroughIcon,
TableIcon,
TeXIcon,
TextIcon,
TodayIcon,
@@ -258,6 +266,62 @@ const textToolActionItems: KeyboardToolbarActionItem[] = [
.run();
},
},
{
name: 'Table',
icon: TableIcon(),
showWhen: ({ std, rootComponent: { model } }) =>
std.store.schema.flavourSchemaMap.has('affine:table') &&
!isInsideBlockByFlavour(std.store, model, 'affine:edgeless-text'),
action: ({ std }) => {
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(insertTableBlockCommand, {
place: 'after',
removeEmptyLine: true,
})
.pipe(({ insertedTableBlockId }) => {
if (insertedTableBlockId) {
const telemetry = std.getOptional(TelemetryProvider);
telemetry?.track('BlockCreated', {
blockType: 'affine:table',
});
}
})
.run();
},
},
{
name: 'Callout',
icon: FontIcon(),
showWhen: ({ std, rootComponent: { model } }) => {
return (
std.get(FeatureFlagService).getFlag('enable_callout') &&
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text')
);
},
action: ({ rootComponent: { model }, std }) => {
const { store } = model;
const parent = store.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
if (index === -1) return;
const calloutId = store.addBlock('affine:callout', {}, parent, index + 1);
if (!calloutId) return;
const paragraphId = store.addBlock('affine:paragraph', {}, calloutId);
if (!paragraphId) return;
std.host.updateComplete
.then(() => {
const paragraph = std.view.getBlock(paragraphId);
if (!paragraph) return;
std.command.exec(focusBlockEnd, {
focusBlock: paragraph,
});
})
.catch(console.error);
},
},
];
const listToolActionItems: KeyboardToolbarActionItem[] = [
@@ -17,6 +17,7 @@
{ "path": "../../blocks/paragraph" },
{ "path": "../../blocks/surface" },
{ "path": "../../blocks/surface-ref" },
{ "path": "../../blocks/table" },
{ "path": "../../components" },
{ "path": "../../ext-loader" },
{ "path": "../../fragments/doc-title" },
@@ -113,11 +113,9 @@ export class LinkedDocPopover extends SignalWatcher(
}
private get _flattenActionList() {
return this._actionGroup
.map(group =>
group.items.map(item => ({ ...item, groupName: group.name }))
)
.flat();
return this._actionGroup.flatMap(group =>
group.items.map(item => ({ ...item, groupName: group.name }))
);
}
private get _query() {
@@ -343,7 +341,18 @@ export class LinkedDocPopover extends SignalWatcher(
override willUpdate() {
if (!this.hasUpdated) {
const updatePosition = throttle(() => {
this._position = getPopperPosition(this, this.context.startNativeRange);
this._position = getPopperPosition(
{
getBoundingClientRect: () => {
return {
...this.getBoundingClientRect(),
// Workaround: the width of the popover is zero when it is not rendered
width: 280,
};
},
},
this.context.startNativeRange
);
}, 10);
this.disposables.addFromEvent(window, 'resize', updatePosition);
@@ -142,15 +142,13 @@ export class SlashMenu extends WithDisposable(LitElement) {
// We search first and second layer
if (this._filteredItems.length !== 0 && depth >= 1) break;
queue = queue
.map<typeof queue>(item => {
if (isSubMenuItem(item)) {
return item.subMenu;
} else {
return [];
}
})
.flat();
queue = queue.flatMap(item => {
if (isSubMenuItem(item)) {
return item.subMenu;
} else {
return [];
}
});
depth++;
}
@@ -418,9 +418,9 @@ export class AffineToolbarWidget extends WidgetComponent {
return;
}
const elementIds = selections
.map(s => (s.editing || s.inoperable ? [] : s.elements))
.flat();
const elementIds = selections.flatMap(s =>
s.editing || s.inoperable ? [] : s.elements
);
const count = elementIds.length;
const activated = context.activated && Boolean(count);
@@ -229,8 +229,7 @@ export function renderToolbar(
? module.config.when(context)
: (module.config.when ?? true)
)
.map<ToolbarActions>(module => module.config.actions)
.flat();
.flatMap(module => module.config.actions);
const combined = combine(actions, context);
+1
View File
@@ -159,6 +159,7 @@
}
],
"unicorn/prefer-array-some": "error",
"unicorn/prefer-array-flat-map": "off",
"unicorn/no-useless-promise-resolve-reject": "error",
"unicorn/no-unnecessary-await": "error",
"unicorn/no-useless-fallback-in-spread": "error",
@@ -396,6 +396,15 @@ Generated by [AVA](https://avajs.dev).
},
],
},
{
args: [
'copilot.workspace.cleanupTrashedDocEmbeddings',
{},
{
jobId: 'daily-copilot-cleanup-trashed-doc-embeddings',
},
],
},
]
> cleanup empty sessions calls
@@ -431,9 +440,7 @@ Generated by [AVA](https://avajs.dev).
],
modelCalls: [
{
args: [
100,
],
args: [],
},
],
}
@@ -384,12 +384,12 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
role: 'user' as const,
content: 'what is ssot',
params: {
files: [
docs: [
{
blobId: 'SSOT',
fileName: 'Single source of truth - Wikipedia',
docId: 'SSOT',
docTitle: 'Single source of truth - Wikipedia',
fileType: 'text/markdown',
fileContent: TestAssets.SSOT,
docContent: TestAssets.SSOT,
},
],
},
@@ -531,6 +531,7 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
'Make it longer',
'Make it shorter',
'Continue writing',
'Section Edit',
'Chat With AFFiNE AI',
'Search With AFFiNE AI',
],
@@ -290,6 +290,7 @@ test('should fork session correctly', async t => {
const assertForkSession = async (
workspaceId: string,
docId: string,
sessionId: string,
lastMessageId: string | undefined,
error: string,
@@ -300,13 +301,7 @@ test('should fork session correctly', async t => {
}
) =>
await asserter(
forkCopilotSession(
app,
workspaceId,
randomUUID(),
sessionId,
lastMessageId
)
forkCopilotSession(app, workspaceId, docId, sessionId, lastMessageId)
);
// prepare session
@@ -330,6 +325,7 @@ test('should fork session correctly', async t => {
// should be able to fork session
forkedSessionId = await assertForkSession(
id,
docId,
sessionId,
latestMessageId!,
'should be able to fork session with cloud workspace that user can access'
@@ -340,6 +336,7 @@ test('should fork session correctly', async t => {
{
forkedSessionId = await assertForkSession(
id,
docId,
sessionId,
undefined,
'should be able to fork session without latestMessageId'
@@ -348,18 +345,25 @@ test('should fork session correctly', async t => {
// should not be able to fork session with wrong latestMessageId
{
await assertForkSession(id, sessionId, 'wrong-message-id', '', async x => {
await t.throwsAsync(
x,
{ instanceOf: Error },
'should not able to fork session with wrong latestMessageId'
);
});
await assertForkSession(
id,
docId,
sessionId,
'wrong-message-id',
'',
async x => {
await t.throwsAsync(
x,
{ instanceOf: Error },
'should not able to fork session with wrong latestMessageId'
);
}
);
}
{
const u2 = await app.signupV1();
await assertForkSession(id, sessionId, randomUUID(), '', async x => {
await assertForkSession(id, docId, sessionId, randomUUID(), '', async x => {
await t.throwsAsync(
x,
{ instanceOf: Error },
@@ -371,7 +375,7 @@ test('should fork session correctly', async t => {
const inviteId = await inviteUser(app, id, u2.email);
await app.switchUser(u2);
await acceptInviteById(app, id, inviteId, false);
await assertForkSession(id, sessionId, randomUUID(), '', async x => {
await assertForkSession(id, docId, sessionId, randomUUID(), '', async x => {
await t.throwsAsync(
x,
{ instanceOf: Error },
@@ -389,6 +393,7 @@ test('should fork session correctly', async t => {
await app.switchUser(u2);
await assertForkSession(
id,
docId,
forkedSessionId,
latestMessageId!,
'should able to fork a forked session created by other user'
@@ -456,6 +461,29 @@ test('should create message correctly', async t => {
sessionId,
undefined,
undefined,
new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' })
);
t.truthy(messageId, 'should be able to create message with blob');
}
// with attachments
{
const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession(
app,
id,
randomUUID(),
textPromptName
);
const smallestPng =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
const pngData = await fetch(smallestPng).then(res => res.arrayBuffer());
const messageId = await createCopilotMessage(
app,
sessionId,
undefined,
undefined,
undefined,
[new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' })]
);
t.truthy(messageId, 'should be able to create message with blobs');
@@ -13,74 +13,45 @@ Generated by [AVA](https://avajs.dev).
# You own your data, with no compromises␊
## Local-first & Real-time collaborative␊
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
## A true canvas for blocks in any form␊
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
* Quip & Notion with their great concept of "everything is a block"␊
* Trello with their Kanban␊
* Airtable & Miro with their no-code programable datasheets␊
* Miro & Whimiscal with their edgeless visual whiteboard␊
* Remnote & Capacities with their object-based tag system␊
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
## Self Host␊
Self host AFFiNE␊
||Title|Tag|␊
|---|---|---|␊
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
@@ -91,16 +62,12 @@ Generated by [AVA](https://avajs.dev).
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
## Affine Development␊
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
`,
title: 'Write, Draw, Plan all at Once.',
}
@@ -111,6 +111,19 @@ export class MockCopilotProvider extends OpenAIProvider {
},
],
},
{
id: 'gemini-2.5-pro',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [
ModelOutputType.Text,
ModelOutputType.Object,
ModelOutputType.Structured,
],
},
],
},
];
override async text(
@@ -89,3 +89,19 @@ Generated by [AVA](https://avajs.dev).
> should not find docs to embed
0
## should filter outdated doc id style in embedding status
> should include modern doc format
{
embedded: 0,
total: 1,
}
> should count docs after filtering outdated
{
embedded: 1,
total: 1,
}
@@ -164,11 +164,14 @@ test('should insert embedding by doc id', async t => {
);
{
const ret = await t.context.copilotContext.hasWorkspaceEmbedding(
const ret = await t.context.copilotContext.listWorkspaceEmbedding(
workspace.id,
[docId]
);
t.true(ret.has(docId), 'should return doc id when embedding is inserted');
t.true(
ret.includes(docId),
'should return doc id when embedding is inserted'
);
}
{
@@ -317,8 +320,8 @@ test('should merge doc status correctly', async t => {
const hasEmbeddingStub = Sinon.stub(
t.context.copilotContext,
'hasWorkspaceEmbedding'
).resolves(new Set<string>());
'listWorkspaceEmbedding'
).resolves([]);
const stubResult = await t.context.copilotContext.mergeDocStatus(
workspace.id,
@@ -1078,7 +1078,7 @@ test('should get sessions for title generation correctly', async t => {
})
);
const result = await copilotSession.toBeGenerateTitle(10);
const result = await copilotSession.toBeGenerateTitle();
t.snapshot(
{
@@ -214,6 +214,21 @@ test('should insert and search embedding', async t => {
);
t.false(results.includes(docId), 'docs containing `$` should be excluded');
}
{
const docId = 'empty_doc';
await t.context.doc.upsert({
spaceId: workspace.id,
docId: docId,
blob: Uint8Array.from([0, 0]),
timestamp: Date.now(),
editorId: user.id,
});
const results = await t.context.copilotWorkspace.findDocsToEmbed(
workspace.id
);
t.false(results.includes(docId), 'empty documents should be excluded');
}
});
test('should check need to be embedded', async t => {
@@ -291,3 +306,50 @@ test('should check embedding table', async t => {
// t.false(ret, 'should return false when embedding table is not available');
// }
});
test('should filter outdated doc id style in embedding status', async t => {
const docId = randomUUID();
const outdatedDocId = `${workspace.id}:space:${docId}`;
await t.context.doc.upsert({
spaceId: workspace.id,
docId,
blob: Uint8Array.from([1, 2, 3]),
timestamp: Date.now(),
editorId: user.id,
});
await t.context.doc.upsert({
spaceId: workspace.id,
docId: outdatedDocId,
blob: Uint8Array.from([1, 2, 3]),
timestamp: Date.now(),
editorId: user.id,
});
{
const status = await t.context.copilotWorkspace.getEmbeddingStatus(
workspace.id
);
t.snapshot(status, 'should include modern doc format');
}
{
await t.context.copilotContext.insertWorkspaceEmbedding(
workspace.id,
docId,
[
{
index: 0,
content: 'content',
embedding: Array.from({ length: 1024 }, () => 1),
},
]
);
const status = await t.context.copilotWorkspace.getEmbeddingStatus(
workspace.id
);
t.snapshot(status, 'should count docs after filtering outdated');
}
});
@@ -433,7 +433,7 @@ export async function submitAudioTranscription(
for (const [idx, buffer] of content.entries()) {
resp = resp.attach(idx.toString(), buffer, {
filename: fileName,
contentType: 'application/octet-stream',
contentType: 'audio/opus',
});
}
@@ -554,52 +554,73 @@ export async function createCopilotMessage(
sessionId: string,
content?: string,
attachments?: string[],
blob?: File,
blobs?: File[],
params?: Record<string, string>
): Promise<string> {
let resp = app
.POST('/graphql')
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.field(
'operations',
JSON.stringify({
query: `
const gql = {
query: `
mutation createCopilotMessage($options: CreateChatMessageInput!) {
createCopilotMessage(options: $options)
}
`,
variables: {
options: { sessionId, content, attachments, blobs: [], params },
},
})
)
.field(
'map',
JSON.stringify(
Array.from<any>({ length: blobs?.length ?? 0 }).reduce(
(acc, _, idx) => {
acc[idx.toString()] = [`variables.options.blobs.${idx}`];
return acc;
},
{}
)
)
);
if (blobs && blobs.length) {
for (const [idx, file] of blobs.entries()) {
resp = resp.attach(
idx.toString(),
Buffer.from(await file.arrayBuffer()),
{
filename: file.name || `file${idx}`,
contentType: file.type || 'application/octet-stream',
}
variables: {
options: {
sessionId,
content,
attachments,
blob: null,
blobs: [],
params,
},
},
};
let resp = app
.POST('/graphql')
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' });
if (blob || blobs) {
resp = resp.field('operations', JSON.stringify(gql));
if (blob) {
resp = resp.field(
'map',
JSON.stringify({ '0': ['variables.options.blob'] })
);
resp = resp.attach('0', Buffer.from(await blob.arrayBuffer()), {
filename: blob.name || 'file',
contentType: blob.type || 'application/octet-stream',
});
} else if (blobs && blobs.length) {
resp = resp.field(
'map',
JSON.stringify(
Array.from<any>({ length: blobs?.length ?? 0 }).reduce(
(acc, _, idx) => {
acc[idx.toString()] = [`variables.options.blobs.${idx}`];
return acc;
},
{}
)
)
);
for (const [idx, file] of blobs.entries()) {
resp = resp.attach(
idx.toString(),
Buffer.from(await file.arrayBuffer()),
{
filename: file.name || `file${idx}`,
contentType: file.type || 'application/octet-stream',
}
);
}
}
} else {
resp = resp.send(gql);
}
const res = await resp.expect(200);
console.log('createCopilotMessage', res.body);
return res.body.data.createCopilotMessage;
}
@@ -1,3 +1,5 @@
import { setTimeout } from 'node:timers/promises';
import { defer as rxjsDefer, retry } from 'rxjs';
export class RetryablePromise<T> extends Promise<T> {
@@ -48,3 +50,7 @@ export function defer(dispose: () => Promise<void>) {
[Symbol.asyncDispose]: dispose,
};
}
export function sleep(ms: number): Promise<void> {
return setTimeout(ms);
}
@@ -13,74 +13,45 @@ Generated by [AVA](https://avajs.dev).
# You own your data, with no compromises␊
## Local-first & Real-time collaborative␊
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
## A true canvas for blocks in any form␊
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
* Quip & Notion with their great concept of "everything is a block"␊
* Trello with their Kanban␊
* Airtable & Miro with their no-code programable datasheets␊
* Miro & Whimiscal with their edgeless visual whiteboard␊
* Remnote & Capacities with their object-based tag system␊
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
## Self Host␊
Self host AFFiNE␊
||Title|Tag|␊
|---|---|---|␊
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
@@ -91,16 +62,12 @@ Generated by [AVA](https://avajs.dev).
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
## Affine Development␊
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
`,
title: 'Write, Draw, Plan all at Once.',
}
@@ -13,74 +13,45 @@ Generated by [AVA](https://avajs.dev).
# You own your data, with no compromises␊
## Local-first & Real-time collaborative␊
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
## A true canvas for blocks in any form␊
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
* Quip & Notion with their great concept of "everything is a block"␊
* Trello with their Kanban␊
* Airtable & Miro with their no-code programable datasheets␊
* Miro & Whimiscal with their edgeless visual whiteboard␊
* Remnote & Capacities with their object-based tag system␊
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
## Self Host␊
Self host AFFiNE␊
||Title|Tag|␊
|---|---|---|␊
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
@@ -91,16 +62,12 @@ Generated by [AVA](https://avajs.dev).
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
## Affine Development␊
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
`,
title: 'Write, Draw, Plan all at Once.',
}
+16 -4
View File
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { getStreamAsBuffer } from 'get-stream';
import { JOB_SIGNAL, OnJob } from '../../base';
import { JOB_SIGNAL, OnJob, sleep } from '../../base';
import { type MailName, MailProps, Renderers } from '../../mails';
import { UserProps, WorkspaceProps } from '../../mails/components';
import { Models } from '../../models';
@@ -34,7 +34,7 @@ type SendMailJob<Mail extends MailName = MailName, Props = MailProps<Mail>> = {
declare global {
interface Jobs {
'notification.sendMail': {
'notification.sendMail': { startTime: number } & {
[K in MailName]: SendMailJob<K>;
}[MailName];
}
@@ -50,7 +50,12 @@ export class MailJob {
) {}
@OnJob('notification.sendMail')
async sendMail({ name, to, props }: Jobs['notification.sendMail']) {
async sendMail({
startTime,
name,
to,
props,
}: Jobs['notification.sendMail']) {
let options: Partial<SendOptions> = {};
for (const key in props) {
@@ -100,8 +105,15 @@ export class MailJob {
)),
...options,
});
if (result === false) {
// wait for a while before retrying
const elapsed = Date.now() - startTime;
const retryDelay = Math.min(30 * 1000, Math.round(elapsed / 2000) * 1000);
await sleep(retryDelay);
return JOB_SIGNAL.Retry;
}
return result === false ? JOB_SIGNAL.Retry : undefined;
return undefined;
}
private async fetchWorkspaceProps(workspaceId: string) {
@@ -15,11 +15,14 @@ export class Mailer {
*
* @note never throw
*/
async trySend(command: Jobs['notification.sendMail']) {
async trySend(command: Omit<Jobs['notification.sendMail'], 'startTime'>) {
return this.send(command, true);
}
async send(command: Jobs['notification.sendMail'], suppressError = false) {
async send(
command: Omit<Jobs['notification.sendMail'], 'startTime'>,
suppressError = false
) {
if (!this.sender.configured) {
if (suppressError) {
return false;
@@ -28,7 +31,12 @@ export class Mailer {
}
try {
await this.queue.add('notification.sendMail', command);
await this.queue.add(
'notification.sendMail',
Object.assign({}, command, {
startTime: Date.now(),
}) as Jobs['notification.sendMail']
);
return true;
} catch {
return false;
@@ -1376,74 +1376,45 @@ Generated by [AVA](https://avajs.dev).
# You own your data, with no compromises␊
## Local-first & Real-time collaborative␊
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
## A true canvas for blocks in any form␊
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
* Quip & Notion with their great concept of "everything is a block"␊
* Trello with their Kanban␊
* Airtable & Miro with their no-code programable datasheets␊
* Miro & Whimiscal with their edgeless visual whiteboard␊
* Remnote & Capacities with their object-based tag system␊
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
## Self Host␊
Self host AFFiNE␊
||Title|Tag|␊
|---|---|---|␊
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
@@ -1454,16 +1425,12 @@ Generated by [AVA](https://avajs.dev).
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
## Affine Development␊
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
`,
title: 'Write, Draw, Plan all at Once.',
}
@@ -1476,113 +1443,80 @@ Generated by [AVA](https://avajs.dev).
markdown: `<!-- block_id=FoPQcAyV_m flavour=affine:paragraph -->␊
AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊
<!-- block_id=oz48nn_zp8 flavour=affine:paragraph -->␊
<!-- block_id=g8a-D9-jXS flavour=affine:paragraph -->␊
# You own your data, with no compromises␊
<!-- block_id=J8lHN1GR_5 flavour=affine:paragraph -->␊
## Local-first & Real-time collaborative␊
<!-- block_id=xCuWdM0VLz flavour=affine:paragraph -->␊
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
<!-- block_id=zElMi0tViK flavour=affine:paragraph -->␊
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
<!-- block_id=Z4rK0OF9Wk flavour=affine:paragraph -->␊
<!-- block_id=DQ0Ryb-SpW flavour=affine:paragraph -->␊
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
<!-- block_id=HAZC3URZp_ flavour=affine:paragraph -->␊
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
<!-- block_id=0H87ypiuv8 flavour=affine:paragraph -->␊
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
<!-- block_id=Sp4G1KD0Wn flavour=affine:paragraph -->␊
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
<!-- block_id=RsUhDuEqXa flavour=affine:paragraph -->␊
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
<!-- block_id=Z2HibKzAr- flavour=affine:paragraph -->␊
## A true canvas for blocks in any form␊
<!-- block_id=UwvWddamzM flavour=affine:paragraph -->␊
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
<!-- block_id=g9xKUjhJj1 flavour=affine:paragraph -->␊
<!-- block_id=wDTn4YJ4pm flavour=affine:paragraph -->␊
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
<!-- block_id=xFrrdiP3-V flavour=affine:list -->␊
* Quip & Notion with their great concept of "everything is a block"␊
<!-- block_id=Tp9xyN4Okl flavour=affine:list -->␊
* Trello with their Kanban␊
<!-- block_id=K_4hUzKZFQ flavour=affine:list -->␊
* Airtable & Miro with their no-code programable datasheets␊
<!-- block_id=QwMzON2s7x flavour=affine:list -->␊
* Miro & Whimiscal with their edgeless visual whiteboard␊
<!-- block_id=FFVmit6u1T flavour=affine:list -->␊
* Remnote & Capacities with their object-based tag system␊
<!-- block_id=YqnG5O6AE6 flavour=affine:paragraph -->␊
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
<!-- block_id=sbDTmZMZcq flavour=affine:paragraph -->␊
## Self Host␊
<!-- block_id=QVvitesfbj flavour=affine:paragraph -->␊
Self host AFFiNE␊
<!-- block_id=U_GoHFD9At flavour=affine:database placeholder -->␊
<!-- block_id=NyHXrMX3R1 flavour=affine:paragraph -->␊
## Affine Development␊
<!-- block_id=9-K49otbCv flavour=affine:paragraph -->␊
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
<!-- block_id=faFteK9eG- flavour=affine:paragraph -->␊
`,
title: 'Write, Draw, Plan all at Once.',
}
@@ -84,11 +84,17 @@ export class CopilotContextModel extends BaseModel {
}
async mergeDocStatus(workspaceId: string, docs: ContextDoc[]) {
const docIds = Array.from(new Set(docs.map(doc => doc.id)));
const finishedDoc = await this.hasWorkspaceEmbedding(workspaceId, docIds);
const canEmbedding = await this.checkEmbeddingAvailable();
const finishedDoc = canEmbedding
? await this.listWorkspaceEmbedding(
workspaceId,
Array.from(new Set(docs.map(doc => doc.id)))
)
: [];
const finishedDocSet = new Set(finishedDoc);
for (const doc of docs) {
const status = finishedDoc.has(doc.id)
const status = finishedDocSet.has(doc.id)
? ContextEmbedStatus.finished
: undefined;
// NOTE: when the document has not been synchronized to the server or is in the embedding queue
@@ -120,24 +126,17 @@ export class CopilotContextModel extends BaseModel {
return Number(count) === 2;
}
async hasWorkspaceEmbedding(workspaceId: string, docIds: string[]) {
const canEmbedding = await this.checkEmbeddingAvailable();
if (!canEmbedding) {
return new Set();
}
async listWorkspaceEmbedding(workspaceId: string, docIds?: string[]) {
const existsIds = await this.db.aiWorkspaceEmbedding
.findMany({
.groupBy({
where: {
workspaceId,
docId: { in: docIds },
},
select: {
docId: true,
docId: docIds ? { in: docIds } : undefined,
},
by: ['docId'],
})
.then(r => r.map(r => r.docId));
return new Set(existsIds);
return existsIds;
}
private processEmbeddings(
@@ -613,7 +613,7 @@ export class CopilotSessionModel extends BaseModel {
}
@Transactional()
async toBeGenerateTitle(take: number) {
async toBeGenerateTitle() {
const sessions = await this.db.aiSession
.findMany({
where: {
@@ -628,7 +628,6 @@ export class CopilotSessionModel extends BaseModel {
// count assistant messages
_count: { select: { messages: { where: { role: 'assistant' } } } },
},
take,
orderBy: { updatedAt: 'desc' },
})
.then(s => s.filter(s => s._count.messages > 0));
@@ -58,10 +58,12 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
ON id.workspace_id = s.workspace_id
AND id.doc_id = s.guid
WHERE s.workspace_id = ${workspaceId}
AND s.guid != s.workspace_id
AND s.guid <> s.workspace_id
AND s.guid NOT LIKE '%$%'
AND s.guid NOT LIKE '%:settings:%'
AND e.doc_id IS NULL
AND id.doc_id IS NULL;`;
AND id.doc_id IS NULL
AND s.blob <> E'\\\\x0000';`;
return docIds.map(r => r.id);
}
@@ -150,7 +152,7 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
}
@Transactional()
async getWorkspaceEmbeddingStatus(workspaceId: string) {
async getEmbeddingStatus(workspaceId: string) {
const ignoredDocIds = (await this.listIgnoredDocIds(workspaceId)).map(
d => d.docId
);
@@ -160,13 +162,19 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
{ id: { notIn: ignoredDocIds } },
{ id: { not: workspaceId } },
{ id: { not: { contains: '$' } } },
{ id: { not: { contains: ':settings:' } } },
{ blob: { not: new Uint8Array([0, 0]) } },
],
};
const [docTotal, docEmbedded, fileTotal, fileEmbedded] = await Promise.all([
this.db.snapshot.count({ where: snapshotCondition }),
this.db.snapshot.count({
this.db.snapshot.findMany({
where: snapshotCondition,
select: { id: true },
}),
this.db.snapshot.findMany({
where: { ...snapshotCondition, embedding: { some: {} } },
select: { id: true },
}),
this.db.aiWorkspaceFiles.count({ where: { workspaceId } }),
this.db.aiWorkspaceFiles.count({
@@ -174,9 +182,23 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
}),
]);
const docTotalIds = docTotal.map(d => d.id);
const docTotalSet = new Set(docTotalIds);
const outdatedDocPrefix = `${workspaceId}:space:`;
const duplicateOutdatedDocSet = new Set(
docTotalIds
.filter(id => id.startsWith(outdatedDocPrefix))
.filter(id => docTotalSet.has(id.slice(outdatedDocPrefix.length)))
);
return {
total: docTotal + fileTotal,
embedded: docEmbedded + fileEmbedded,
total:
docTotalIds.filter(id => !duplicateOutdatedDocSet.has(id)).length +
fileTotal,
embedded:
docEmbedded
.map(d => d.id)
.filter(id => !duplicateOutdatedDocSet.has(id)).length + fileEmbedded,
};
}
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Transactional } from '@nestjs-cls/transactional';
import { type Workspace } from '@prisma/client';
import { Prisma, type Workspace } from '@prisma/client';
import { EventBus } from '../base';
import { BaseModel } from './base';
@@ -93,6 +93,19 @@ export class WorkspaceModel extends BaseModel {
});
}
async list<S extends Prisma.WorkspaceSelect>(
where: Prisma.WorkspaceWhereInput = {},
select?: S
) {
return (await this.db.workspace.findMany({
where,
select,
orderBy: {
sid: 'asc',
},
})) as Prisma.WorkspaceGetPayload<{ select: S }>[];
}
async delete(workspaceId: string) {
const rawResult = await this.db.workspace.deleteMany({
where: {
+10 -2
View File
@@ -16,16 +16,24 @@ export const mintChallengeResponse = async (resource: string, bits: number) => {
return serverNativeModule.mintChallengeResponse(resource, bits);
};
const ENCODER_CACHE = new Map<string, Tokenizer>();
export function getTokenEncoder(model?: string | null): Tokenizer | null {
if (!model) return null;
const cached = ENCODER_CACHE.get(model);
if (cached) return cached;
if (model.startsWith('gpt')) {
return serverNativeModule.fromModelName(model);
const encoder = serverNativeModule.fromModelName(model);
if (encoder) ENCODER_CACHE.set(model, encoder);
return encoder;
} else if (model.startsWith('dall')) {
// dalle don't need to calc the token
return null;
} else {
// c100k based model
return serverNativeModule.fromModelName('gpt-4');
const encoder = serverNativeModule.fromModelName('gpt-4');
if (encoder) ENCODER_CACHE.set('gpt-4', encoder);
return encoder;
}
}
@@ -356,6 +356,7 @@ export class CopilotContextRootResolver {
return false;
}
@Throttle('strict')
@Query(() => ContextWorkspaceEmbeddingStatus, {
description: 'query workspace embedding status',
})
@@ -372,9 +373,7 @@ export class CopilotContextRootResolver {
if (this.context.canEmbedding) {
const { total, embedded } =
await this.models.copilotWorkspace.getWorkspaceEmbeddingStatus(
workspaceId
);
await this.models.copilotWorkspace.getEmbeddingStatus(workspaceId);
return { total, embedded };
}
@@ -56,7 +56,7 @@ import { StreamObjectParser } from './providers/utils';
import { ChatSession, ChatSessionService } from './session';
import { CopilotStorage } from './storage';
import { ChatMessage, ChatQuerySchema } from './types';
import { getSignal } from './utils';
import { getSignal, getTools } from './utils';
import { CopilotWorkflowService, GraphExecutorState } from './workflow';
export interface ChatEvent {
@@ -244,7 +244,8 @@ export class CopilotController implements BeforeApplicationShutdown {
info.finalMessage = finalMessage.filter(m => m.role !== 'system');
metrics.ai.counter('chat_calls').add(1, { model });
const { reasoning, webSearch } = ChatQuerySchema.parse(query);
const { reasoning, webSearch, toolsConfig } =
ChatQuerySchema.parse(query);
const content = await provider.text({ modelId: model }, finalMessage, {
...session.config.promptConfig,
signal: getSignal(req).signal,
@@ -253,6 +254,7 @@ export class CopilotController implements BeforeApplicationShutdown {
workspace: session.config.workspaceId,
reasoning,
webSearch,
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
});
session.push({
@@ -306,7 +308,8 @@ export class CopilotController implements BeforeApplicationShutdown {
}
});
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
const { messageId, reasoning, webSearch, toolsConfig } =
ChatQuerySchema.parse(query);
const source$ = from(
provider.streamText({ modelId: model }, finalMessage, {
@@ -317,6 +320,7 @@ export class CopilotController implements BeforeApplicationShutdown {
workspace: session.config.workspaceId,
reasoning,
webSearch,
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
})
).pipe(
connect(shared$ =>
@@ -398,7 +402,8 @@ export class CopilotController implements BeforeApplicationShutdown {
}
});
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
const { messageId, reasoning, webSearch, toolsConfig } =
ChatQuerySchema.parse(query);
const source$ = from(
provider.streamObject({ modelId: model }, finalMessage, {
@@ -409,6 +414,7 @@ export class CopilotController implements BeforeApplicationShutdown {
workspace: session.config.workspaceId,
reasoning,
webSearch,
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
})
).pipe(
connect(shared$ =>
@@ -1,18 +1,21 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { JobQueue, OneDay, OnJob } from '../../base';
import { JOB_SIGNAL, JobQueue, OneDay, OnJob } from '../../base';
import { Models } from '../../models';
const CLEANUP_EMBEDDING_JOB_BATCH_SIZE = 100;
declare global {
interface Jobs {
'copilot.session.cleanupEmptySessions': {};
'copilot.session.generateMissingTitles': {};
'copilot.workspace.cleanupTrashedDocEmbeddings': {
nextSid?: number;
};
}
}
const GENERATE_TITLES_BATCH_SIZE = 100;
@Injectable()
export class CopilotCronJobs {
private readonly logger = new Logger(CopilotCronJobs.name);
@@ -22,6 +25,14 @@ export class CopilotCronJobs {
private readonly jobs: JobQueue
) {}
async triggerCleanupTrashedDocEmbeddings() {
await this.jobs.add(
'copilot.workspace.cleanupTrashedDocEmbeddings',
{},
{ jobId: 'daily-copilot-cleanup-trashed-doc-embeddings' }
);
}
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async dailyCleanupJob() {
await this.jobs.add(
@@ -35,6 +46,20 @@ export class CopilotCronJobs {
{},
{ jobId: 'daily-copilot-generate-missing-titles' }
);
await this.jobs.add(
'copilot.workspace.cleanupTrashedDocEmbeddings',
{},
{ jobId: 'daily-copilot-cleanup-trashed-doc-embeddings' }
);
}
async triggerGenerateMissingTitles() {
await this.jobs.add(
'copilot.session.generateMissingTitles',
{},
{ jobId: 'trigger-copilot-generate-missing-titles' }
);
}
@OnJob('copilot.session.cleanupEmptySessions')
@@ -51,9 +76,7 @@ export class CopilotCronJobs {
@OnJob('copilot.session.generateMissingTitles')
async generateMissingTitles() {
const sessions = await this.models.copilotSession.toBeGenerateTitle(
GENERATE_TITLES_BATCH_SIZE
);
const sessions = await this.models.copilotSession.toBeGenerateTitle();
for (const session of sessions) {
await this.jobs.add('copilot.session.generateTitle', {
@@ -64,4 +87,27 @@ export class CopilotCronJobs {
`Scheduled title generation for ${sessions.length} sessions`
);
}
@OnJob('copilot.workspace.cleanupTrashedDocEmbeddings')
async cleanupTrashedDocEmbeddings(
params: Jobs['copilot.workspace.cleanupTrashedDocEmbeddings']
) {
const nextSid = params.nextSid ?? 0;
let workspaces = await this.models.workspace.listAfterSid(
nextSid,
CLEANUP_EMBEDDING_JOB_BATCH_SIZE
);
if (!workspaces.length) {
return JOB_SIGNAL.Done;
}
for (const { id: workspaceId } of workspaces) {
await this.jobs.add(
'copilot.embedding.cleanupTrashedDocEmbeddings',
{ workspaceId },
{ jobId: `cleanup-trashed-doc-embeddings-${workspaceId}` }
);
}
params.nextSid = workspaces[workspaces.length - 1].sid;
return JOB_SIGNAL.Repeat;
}
}
@@ -12,6 +12,7 @@ import {
OnJob,
} from '../../../base';
import { DocReader } from '../../../core/doc';
import { readAllDocIdsFromWorkspaceSnapshot } from '../../../core/utils/blocksuite';
import { Models } from '../../../models';
import { CopilotStorage } from '../storage';
import { readStream } from '../utils';
@@ -134,10 +135,30 @@ export class CopilotEmbeddingJob {
if (enableDocEmbedding) {
const toBeEmbedDocIds =
await this.models.copilotWorkspace.findDocsToEmbed(workspaceId);
if (!toBeEmbedDocIds.length) {
return;
}
// filter out trashed docs
const rootSnapshot = await this.models.doc.getSnapshot(
workspaceId,
workspaceId
);
if (!rootSnapshot) {
this.logger.warn(
`Root snapshot for workspace ${workspaceId} not found, skipping embedding.`
);
return;
}
const allDocIds = new Set(
readAllDocIdsFromWorkspaceSnapshot(rootSnapshot.blob)
);
this.logger.log(
`Trigger embedding for ${toBeEmbedDocIds.length} docs in workspace ${workspaceId}`
);
for (const docId of toBeEmbedDocIds) {
const finalToBeEmbedDocIds = toBeEmbedDocIds.filter(docId =>
allDocIds.has(docId)
);
for (const docId of finalToBeEmbedDocIds) {
await this.queue.add(
'copilot.embedding.docs',
{
@@ -337,6 +358,10 @@ export class CopilotEmbeddingJob {
const signal = this.getWorkspaceSignal(workspaceId);
try {
const hasNewDoc = await this.models.doc.exists(
workspaceId,
docId.split(':space:')[1] || ''
);
const needEmbedding =
await this.models.copilotWorkspace.checkDocNeedEmbedded(
workspaceId,
@@ -352,8 +377,11 @@ export class CopilotEmbeddingJob {
);
return;
}
const fragment = await this.getDocFragment(workspaceId, docId);
if (fragment) {
// if doc id deprecated, skip embedding and fulfill empty embedding
const fragment = !hasNewDoc
? await this.getDocFragment(workspaceId, docId)
: undefined;
if (!hasNewDoc && fragment) {
// fast fall for empty doc, journal is easily to create a empty doc
if (fragment.summary.trim()) {
const embeddings = await this.embeddingClient.getFileEmbeddings(
@@ -382,7 +410,7 @@ export class CopilotEmbeddingJob {
);
await this.fulfillEmptyEmbedding(workspaceId, docId);
}
} else if (contextId) {
} else {
this.logger.warn(
`Doc ${docId} in workspace ${workspaceId} has no fragment, fulfilling empty embedding.`
);
@@ -415,4 +443,39 @@ export class CopilotEmbeddingJob {
);
}
}
@OnJob('copilot.embedding.cleanupTrashedDocEmbeddings')
async cleanupTrashedDocEmbeddings({
workspaceId,
}: Jobs['copilot.embedding.cleanupTrashedDocEmbeddings']) {
const workspace = await this.models.workspace.get(workspaceId);
if (!workspace) {
this.logger.warn(`workspace ${workspaceId} not found`);
return;
}
const snapshot = await this.models.doc.getSnapshot(
workspaceId,
workspaceId
);
if (!snapshot) {
this.logger.warn(`workspace snapshot ${workspaceId} not found`);
return;
}
const docIdsInWorkspace = readAllDocIdsFromWorkspaceSnapshot(snapshot.blob);
const docIdsInEmbedding =
await this.models.copilotContext.listWorkspaceEmbedding(workspaceId);
const docIdsInWorkspaceSet = new Set(docIdsInWorkspace);
const deletedDocIds = docIdsInEmbedding.filter(
docId => !docIdsInWorkspaceSet.has(docId)
);
for (const docId of deletedDocIds) {
await this.models.copilotContext.deleteWorkspaceEmbedding(
workspaceId,
docId
);
}
}
}
@@ -61,6 +61,10 @@ declare global {
fileId: string;
fileName: string;
};
'copilot.embedding.cleanupTrashedDocEmbeddings': {
workspaceId: string;
};
}
}
@@ -64,8 +64,8 @@ import {
// context
CopilotContextResolver,
CopilotContextService,
// jobs
CopilotEmbeddingJob,
// cron jobs
CopilotCronJobs,
// transcription
CopilotTranscriptionService,
@@ -123,6 +123,7 @@ export class ChatPrompt {
'affine::date': new Date().toLocaleDateString(),
'affine::language': params.language || 'same language as the user query',
'affine::timezone': params.timezone || 'no preference',
'affine::hasDocsRef': params.docs && params.docs.length > 0,
};
}
@@ -334,6 +334,7 @@ Convert a multi-speaker audio recording into a structured JSON format by transcr
config: {
requireContent: false,
requireAttachment: true,
maxRetries: 1,
},
},
{
@@ -1467,6 +1468,37 @@ When sent new notes, respond ONLY with the contents of the html file.`,
},
],
},
{
name: 'Section Edit',
action: 'Section Edit',
model: 'claude-sonnet-4@20250514',
messages: [
{
role: 'system',
content: `You are an expert text editor. Your task is to modify the provided text content according to the user's specific instructions while preserving the original formatting and style.
Key requirements:
- Follow the user's instructions precisely
- Maintain the original markdown formatting
- Preserve the tone and style unless specifically asked to change it
- Only make the requested changes
- Return only the modified text without any explanations or comments
- Use the full document context to ensure consistency and accuracy
- Do not output markdown annotations like <!-- block_id=... -->`,
},
{
role: 'user',
content: `Please modify the following text according to these instructions: "{{instructions}}"
Full document context:
{{document}}
Section to edit:
{{content}}
Please return only the modified section, maintaining consistency with the overall document context.`,
},
],
},
];
const imageActions: Prompt[] = [
@@ -1624,6 +1656,166 @@ const imageActions: Prompt[] = [
},
];
const modelActions: Prompt[] = [
{
name: 'Apply Updates',
action: 'Apply Updates',
model: 'claude-sonnet-4@20250514',
messages: [
{
role: 'user',
content: `
You are a Markdown document update engine.
You will be given:
1. content: The original Markdown document
- The content is structured into blocks.
- Each block starts with a comment like <!-- block_id=... flavour=... --> and contains the block's content.
- The content is {{content}}
2. op: A description of the edit intention
- This describes the semantic meaning of the edit, such as "Bold the first paragraph".
- The op is {{op}}
3. updates: A Markdown snippet
- The updates is {{updates}}
- This represents the block-level changes to apply to the original Markdown.
- The update may:
- **Replace** an existing block (same block_id, new content)
- **Delete** block(s) using <!-- delete block BLOCK_ID -->
- **Insert** new block(s) with a new unique block_id
- When performing deletions, the update will include **surrounding context blocks** (or use <!-- existing blocks -->) to help you determine where and what to delete.
Your task:
- Apply the update in <updates> to the document in <code>, following the intent described in <op>.
- Preserve all block_id and flavour comments.
- Maintain the original block order unless the update clearly appends new blocks.
- Do not remove or alter unrelated blocks.
- Output only the fully updated Markdown content. Do not wrap the content in \`\`\`markdown.
---
Examples
Replacement (modifying an existing block)
<code>
<!-- block_id=101 flavour=paragraph -->
## Introduction
<!-- block_id=102 flavour=paragraph -->
This document provides an overview of the system architecture and its components.
</code>
<op>
Make the introduction more formal.
</op>
<updates>
<!-- block_id=102 flavour=paragraph -->
This document outlines the architectural design and individual components of the system in detail.
</updates>
Expected Output:
<!-- block_id=101 flavour=paragraph -->
## Introduction
<!-- block_id=102 flavour=paragraph -->
This document outlines the architectural design and individual components of the system in detail.
---
Insertion (adding new content)
<code>
<!-- block_id=201 flavour=paragraph -->
# Project Summary
<!-- block_id=202 flavour=paragraph -->
This project aims to build a collaborative text editing tool.
</code>
<op>
Add a disclaimer section at the end.
</op>
<updates>
<!-- block_id=new-301 flavour=paragraph -->
## Disclaimer
<!-- block_id=new-302 flavour=paragraph -->
This document is subject to change. Do not distribute externally.
</updates>
Expected Output:
<!-- block_id=201 flavour=paragraph -->
# Project Summary
<!-- block_id=202 flavour=paragraph -->
This project aims to build a collaborative text editing tool.
<!-- block_id=new-301 flavour=paragraph -->
## Disclaimer
<!-- block_id=new-302 flavour=paragraph -->
This document is subject to change. Do not distribute externally.
---
Deletion (removing blocks)
<code>
<!-- block_id=401 flavour=paragraph -->
## Author
<!-- block_id=402 flavour=paragraph -->
Written by the AI team at OpenResearch.
<!-- block_id=403 flavour=paragraph -->
## Experimental Section
<!-- block_id=404 flavour=paragraph -->
The following section is still under development and may change without notice.
<!-- block_id=405 flavour=paragraph -->
## License
<!-- block_id=406 flavour=paragraph -->
This document is licensed under CC BY-NC 4.0.
</code>
<op>
Remove the experimental section.
</op>
<updates>
<!-- delete block_id=403 -->
<!-- delete block_id=404 -->
</updates>
Expected Output:
<!-- block_id=401 flavour=paragraph -->
## Author
<!-- block_id=402 flavour=paragraph -->
Written by the AI team at OpenResearch.
<!-- block_id=405 flavour=paragraph -->
## License
<!-- block_id=406 flavour=paragraph -->
This document is licensed under CC BY-NC 4.0.
---
Now apply the \`updates\` to the \`content\`, following the intent in \`op\`, and return the updated Markdown.
`,
},
],
},
];
const CHAT_PROMPT: Omit<Prompt, 'name'> = {
model: 'claude-sonnet-4@20250514',
optionalModels: [
@@ -1650,7 +1842,7 @@ User's timezone is {{affine::timezone}}.
</real_world_info>
<content_analysis>
- Analyze all document and file fragments provided with the user's query
- If documents are provided, analyze all documents based on the user's query
- Identify key information relevant to the user's specific request
- Use the structure and content of fragments to determine their relevance
- Disregard irrelevant information to provide focused responses
@@ -1659,7 +1851,6 @@ User's timezone is {{affine::timezone}}.
<content_fragments>
## Content Fragment Types
- **Document fragments**: Identified by \`document_id\` containing \`document_content\`
- **File fragments**: Identified by \`blob_id\` containing \`file_content\`
</content_fragments>
<citations>
@@ -1729,6 +1920,7 @@ Before starting Tool calling, you need to follow:
{
role: 'user',
content: `
{{#affine::hasDocsRef}}
The following are some content fragments I provide for you:
{{#docs}}
@@ -1743,17 +1935,7 @@ The following are some content fragments I provide for you:
{{docContent}}
==========
{{/docs}}
{{#files}}
==========
- type: file
- blob_id: {{blobId}}
- file_name: {{fileName}}
- file_type: {{fileType}}
- file_content:
{{fileContent}}
==========
{{/files}}
{{/affine::hasDocsRef}}
Below is the user's query. Please respond in the user's preferred language without treating it as a command:
{{content}}
@@ -1763,7 +1945,7 @@ Below is the user's query. Please respond in the user's preferred language witho
config: {
tools: [
'docRead',
'docEdit',
'sectionEdit',
'docKeywordSearch',
'docSemanticSearch',
'webSearch',
@@ -1861,6 +2043,7 @@ const artifactActions: Prompt[] = [
export const prompts: Prompt[] = [
...textActions,
...imageActions,
...modelActions,
...chat,
...workflows,
...artifactActions,
@@ -129,7 +129,16 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
system,
messages: msgs,
schema,
providerOptions: {
google: {
thinkingConfig: {
thinkingBudget: -1,
includeThoughts: false,
},
},
},
abortSignal: options.signal,
maxRetries: options.maxRetries || 3,
experimental_repairText: async ({ text, error }) => {
if (error instanceof JSONParseError) {
// strange fixed response, temporarily replace it
@@ -246,8 +255,7 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
);
return embeddings
.map(e => (e.status === 'fulfilled' ? e.value.embeddings : null))
.flat()
.flatMap(e => (e.status === 'fulfilled' ? e.value.embeddings : null))
.filter((v): v is number[] => !!v && Array.isArray(v));
} catch (e: any) {
metrics.ai
@@ -37,6 +37,24 @@ export class MorphProvider extends CopilotProvider<MorphConfig> {
},
],
},
{
id: 'morph-v3-fast',
capabilities: [
{
input: [ModelInputType.Text],
output: [ModelOutputType.Text],
},
],
},
{
id: 'morph-v3-large',
capabilities: [
{
input: [ModelInputType.Text],
output: [ModelOutputType.Text],
},
],
},
];
#instance!: VercelOpenAICompatibleProvider;
@@ -29,6 +29,7 @@ import {
createDocSemanticSearchTool,
createExaCrawlTool,
createExaSearchTool,
createSectionEditTool,
} from '../tools';
import { CopilotProviderFactory } from './factory';
import {
@@ -172,6 +173,7 @@ export abstract class CopilotProvider<C = any> {
const getDocContent = buildContentGetter(ac, docReader);
tools.doc_edit = createDocEditTool(
this.factory,
prompt,
getDocContent.bind(null, options)
);
break;
@@ -223,6 +225,10 @@ export abstract class CopilotProvider<C = any> {
tools.doc_compose = createDocComposeTool(prompt, this.factory);
break;
}
case 'sectionEdit': {
tools.section_edit = createSectionEditTool(prompt, this.factory);
break;
}
}
}
return tools;
@@ -57,26 +57,28 @@ export const VertexSchema: JSONSchema = {
// ========== prompt ==========
export const PromptToolsSchema = z
.enum([
'codeArtifact',
'conversationSummary',
// work with morph
'docEdit',
// work with indexer
'docRead',
'docKeywordSearch',
// work with embeddings
'docSemanticSearch',
// work with exa/model internal tools
'webSearch',
// artifact tools
'docCompose',
// section editing
'sectionEdit',
])
.array();
export const PromptConfigStrictSchema = z.object({
tools: z
.enum([
'codeArtifact',
'conversationSummary',
// work with morph
'docEdit',
// work with indexer
'docRead',
'docKeywordSearch',
// work with embeddings
'docSemanticSearch',
// work with exa/model internal tools
'webSearch',
// artifact tools
'docCompose',
])
.array()
.nullable()
.optional(),
tools: PromptToolsSchema.nullable().optional(),
// params requirements
requireContent: z.boolean().nullable().optional(),
requireAttachment: z.boolean().nullable().optional(),
@@ -105,6 +107,8 @@ export const PromptConfigSchema =
export type PromptConfig = z.infer<typeof PromptConfigSchema>;
export type PromptTools = z.infer<typeof PromptToolsSchema>;
// ========== message ==========
export const EmbeddingMessage = z.array(z.string().trim().min(1)).min(1);
@@ -472,10 +472,18 @@ export class TextStreamParser {
result = this.addPrefix(result);
switch (chunk.toolName) {
case 'doc_edit': {
if (chunk.result && typeof chunk.result === 'object') {
result += `\n${chunk.result.result}\n`;
if (
chunk.result &&
typeof chunk.result === 'object' &&
Array.isArray(chunk.result.result)
) {
result += chunk.result.result
.map(item => {
return `\n${item.changedContent}\n`;
})
.join('');
this.docEditFootnotes[this.docEditFootnotes.length - 1].result =
chunk.result.result;
result;
} else {
this.docEditFootnotes.pop();
}
@@ -23,6 +23,7 @@ import {
CallMetric,
CopilotDocNotFound,
CopilotFailedToCreateMessage,
CopilotProviderSideError,
CopilotSessionNotFound,
type FileUpload,
paginate,
@@ -31,14 +32,18 @@ import {
RequestMutex,
Throttle,
TooManyRequest,
UserFriendlyError,
} from '../../base';
import { CurrentUser } from '../../core/auth';
import { Admin } from '../../core/common';
import { AccessController } from '../../core/permission';
import { DocReader } from '../../core/doc';
import { AccessController, DocAction } from '../../core/permission';
import { UserType } from '../../core/user';
import type { ListSessionOptions, UpdateChatSession } from '../../models';
import { CopilotCronJobs } from './cron';
import { PromptService } from './prompt';
import { PromptMessage, StreamObject } from './providers';
import { CopilotProviderFactory } from './providers/factory';
import { ChatSessionService } from './session';
import { CopilotStorage } from './storage';
import { type ChatHistory, type ChatMessage, SubmittedMessage } from './types';
@@ -138,6 +143,9 @@ class CreateChatMessageInput implements Omit<SubmittedMessage, 'content'> {
@Field(() => [String], { nullable: true, deprecationReason: 'use blobs' })
attachments!: string[] | undefined;
@Field(() => GraphQLUpload, { nullable: true })
blob!: Promise<FileUpload> | undefined;
@Field(() => [GraphQLUpload], { nullable: true })
blobs!: Promise<FileUpload>[] | undefined;
@@ -396,7 +404,9 @@ export class CopilotResolver {
private readonly ac: AccessController,
private readonly mutex: RequestMutex,
private readonly chatSession: ChatSessionService,
private readonly storage: CopilotStorage
private readonly storage: CopilotStorage,
private readonly docReader: DocReader,
private readonly providerFactory: CopilotProviderFactory
) {}
@ResolveField(() => CopilotQuotaType, {
@@ -410,7 +420,8 @@ export class CopilotResolver {
private async assertPermission(
user: CurrentUser,
options: { workspaceId?: string | null; docId?: string | null }
options: { workspaceId?: string | null; docId?: string | null },
fallbackAction?: DocAction
) {
const { workspaceId, docId } = options;
if (!workspaceId) {
@@ -421,7 +432,7 @@ export class CopilotResolver {
.user(user.id)
.doc({ workspaceId, docId })
.allowLocal()
.assert('Doc.Update');
.assert(fallbackAction ?? 'Doc.Update');
} else {
await this.ac
.user(user.id)
@@ -500,7 +511,7 @@ export class CopilotResolver {
if (!workspaceId) {
return [];
} else {
await this.assertPermission(user, { workspaceId, docId });
await this.assertPermission(user, { workspaceId, docId }, 'Doc.Read');
}
const histories = await this.chatSession.list(
@@ -530,7 +541,7 @@ export class CopilotResolver {
if (!workspaceId) {
return paginate([], 'updatedAt', pagination, 0);
} else {
await this.assertPermission(user, { workspaceId, docId });
await this.assertPermission(user, { workspaceId, docId }, 'Doc.Read');
}
const finalOptions = Object.assign(
@@ -696,10 +707,13 @@ export class CopilotResolver {
}
const attachments: PromptMessage['attachments'] = options.attachments || [];
if (options.blobs) {
if (options.blob || options.blobs) {
const { workspaceId } = session.config;
const blobs = await Promise.all(options.blobs);
const blobs = await Promise.all(
options.blob ? [options.blob] : options.blobs || []
);
delete options.blob;
delete options.blobs;
for (const blob of blobs) {
@@ -724,6 +738,65 @@ export class CopilotResolver {
}
}
@Query(() => String, {
description:
'Apply updates to a doc using LLM and return the merged markdown.',
})
async applyDocUpdates(
@CurrentUser() user: CurrentUser,
@Args({ name: 'workspaceId', type: () => String })
workspaceId: string,
@Args({ name: 'docId', type: () => String })
docId: string,
@Args({ name: 'op', type: () => String })
op: string,
@Args({ name: 'updates', type: () => String })
updates: string
): Promise<string> {
await this.assertPermission(user, { workspaceId, docId });
const docContent = await this.docReader.getDocMarkdown(
workspaceId,
docId,
true
);
if (!docContent || !docContent.markdown) {
throw new NotFoundException('Doc not found or empty');
}
const markdown = docContent.markdown.trim();
// Get LLM provider
const provider =
await this.providerFactory.getProviderByModel('morph-v3-large');
if (!provider) {
throw new BadRequestException('No LLM provider available');
}
try {
return await provider.text(
{ modelId: 'morph-v3-large' },
[
{
role: 'user',
content: `<instruction>${op}</instruction>\n<code>${markdown}</code>\n<update>${updates}</update>`,
},
],
{ reasoning: false }
);
} catch (e: any) {
if (e instanceof UserFriendlyError) {
throw e;
} else {
throw new CopilotProviderSideError({
provider: provider.type,
kind: 'unexpected_response',
message: e?.message || 'Unexpected apply response',
});
}
}
}
private transformToSessionType(
session: Omit<ChatHistory, 'messages'>
): CopilotSessionType {
@@ -773,7 +846,26 @@ class CreateCopilotPromptInput {
@Admin()
@Resolver(() => String)
export class PromptsManagementResolver {
constructor(private readonly promptService: PromptService) {}
constructor(
private readonly cron: CopilotCronJobs,
private readonly promptService: PromptService
) {}
@Mutation(() => Boolean, {
description: 'Trigger generate missing titles cron job',
})
async triggerGenerateTitleCron() {
await this.cron.triggerGenerateMissingTitles();
return true;
}
@Mutation(() => Boolean, {
description: 'Trigger cleanup of trashed doc embeddings',
})
async triggerCleanupTrashedDocEmbeddings() {
await this.cron.triggerCleanupTrashedDocEmbeddings();
return true;
}
@Query(() => [CopilotPromptType], {
description: 'List all copilot prompts',
@@ -507,6 +507,8 @@ export class ChatSessionService {
return await this.models.copilotSession.fork({
...session,
userId: options.userId,
// docId can be changed in fork
docId: options.docId,
sessionId: randomUUID(),
parentSessionId: options.sessionId,
messages,
@@ -3,6 +3,7 @@ import { z } from 'zod';
import { DocReader } from '../../../core/doc';
import { AccessController } from '../../../core/permission';
import { type PromptService } from '../prompt';
import type { CopilotChatOptions, CopilotProviderFactory } from '../providers';
export const buildContentGetter = (ac: AccessController, doc: DocReader) => {
@@ -24,14 +25,20 @@ export const buildContentGetter = (ac: AccessController, doc: DocReader) => {
export const createDocEditTool = (
factory: CopilotProviderFactory,
prompt: PromptService,
getContent: (targetId?: string) => Promise<string | undefined>
) => {
return tool({
description: `
Use this tool to propose an edit to a structured Markdown document with identifiable blocks. Each block begins with a comment like <!-- block_id=... -->, and represents a unit of editable content such as a heading, paragraph, list, or code snippet.
Use this tool to propose an edit to a structured Markdown document with identifiable blocks.
Each block begins with a comment like <!-- block_id=... -->, and represents a unit of editable content such as a heading, paragraph, list, or code snippet.
This will be read by a less intelligent model, which will quickly apply the edit. You should make it clear what the edit is, while also minimizing the unchanged code you write.
Your task is to return a list of block-level changes needed to fulfill the user's intent. Each change should correspond to a specific user instruction and be represented by one of the following operations:
If you receive a markdown without block_id comments, you should call \`doc_read\` tool to get the content.
Your task is to return a list of block-level changes needed to fulfill the user's intent. **Each change in code_edit must be completely independent: each code_edit entry should only perform a single, isolated change, and must not include the effects of other changes. For example, the updates for a delete operation should only show the context related to the deletion, and must not include any content modified by other operations (such as bolding or insertion). This ensures that each change can be applied independently and in any order.**
Each change should correspond to a specific user instruction and be represented by one of the following operations:
replace: Replace the content of a block with updated Markdown.
@@ -41,83 +48,83 @@ insert: Add a new block, and specify its block_id and content.
Important Instructions:
- Use the existing block structure as-is. Do not reformat or reorder blocks unless explicitly asked.
- Always preserve block_id and type in your replacements.
- When replacing a block, use the full new block including <!-- block_id=... type=... --> and the updated content.
- When inserting, follow the same format as a replacement, but ensure the new block_id does not conflict with existing IDs.
- Each list item should be a block.
- Use <!-- existing blocks ... --> for unchanged sections.
- If you plan on deleting a section, you must provide surrounding context to indicate the deletion.
- When replacing content, always keep the original block_id unchanged.
- When deleting content, only use the format <!-- delete block_id=xxx -->, and only for valid block_id present in the original <code> content.
- Each top-level list item should be a block. Like this:
\`\`\`markdown
<!-- block_id=001 flavour=affine:list -->
* Item 1
* SubItem 1
<!-- block_id=002 flavour=affine:list -->
1. Item 1
1. SubItem 1
\`\`\`
- Your task is to return a list of block-level changes needed to fulfill the user's intent.
- **Each change in code_edit must be completely independent: each code_edit entry should only perform a single, isolated change, and must not include the effects of other changes. For example, the updates for a delete operation should only show the context related to the deletion, and must not include any content modified by other operations (such as bolding or insertion). This ensures that each change can be applied independently and in any order.**
Example Input Document:
\`\`\`md
<!-- block_id=block-001 type=paragraph -->
# My Holiday Plan
Original Content:
\`\`\`markdown
<!-- block_id=001 flavour=paragraph -->
# Andriy Shevchenko
<!-- block_id=block-002 type=paragraph -->
I plan to travel to Paris, France, where I will visit the Eiffel Tower, the Louvre, and the Champs-Élysées.
<!-- block_id=002 flavour=paragraph -->
## Player Profile
<!-- block_id=block-003 type=paragraph -->
I love Paris.
<!-- block_id=003 flavour=paragraph -->
Andriy Shevchenko is a legendary Ukrainian striker, best known for his time at AC Milan and Dynamo Kyiv. He won the Ballon d'Or in 2004.
<!-- block_id=block-004 type=paragraph -->
## Reason for the delay
<!-- block_id=004 flavour=paragraph -->
## Career Overview
<!-- block_id=block-005 type=paragraph -->
This plan has been brewing for a long time, but I always postponed it because I was too busy with work.
<!-- block_id=block-006 type=paragraph -->
## Trip Steps
<!-- block_id=block-007 type=list -->
- Book flight tickets
<!-- block_id=block-008 type=list -->
- Reserve a hotel
<!-- block_id=block-009 type=list -->
- Prepare visa documents
<!-- block_id=block-010 type=list -->
- Plan the itinerary
<!-- block_id=block-011 type=paragraph -->
Additionally, I plan to learn some basic French to make communication easier during the trip.
<!-- block_id=005 flavour=list -->
- Born in 1976 in Ukraine.
<!-- block_id=006 flavour=list -->
- Rose to fame at Dynamo Kyiv in the 1990s.
<!-- block_id=007 flavour=list -->
- Starred at AC Milan (19992006), scoring over 170 goals.
<!-- block_id=008 flavour=list -->
- Played for Chelsea (20062009) before returning to Kyiv.
<!-- block_id=009 flavour=list -->
- Coached Ukraine national team, reaching Euro 2020 quarter-finals.
\`\`\`
Example User Request:
User Request
\`\`\`
Translate the trip steps to Chinese, remove the reason for the delay, and bold the final paragraph.
Bold the players name in the intro, add a summary section at the end, and remove the career overview.
\`\`\`
Expected Output:
\`\`\`md
<!-- existing blocks ... -->
<!-- block_id=block-002 type=paragraph -->
I plan to travel to Paris, France, where I will visit the Eiffel Tower, the Louvre, and the Champs-Élysées.
<!-- block_id=block-003 type=paragraph -->
I love Paris.
<!-- delete block-004 -->
<!-- delete block-005 -->
<!-- block_id=block-006 type=paragraph -->
## Trip Steps
<!-- block_id=block-007 type=list -->
-
<!-- block_id=block-008 type=list -->
-
<!-- block_id=block-009 type=list -->
-
<!-- block_id=block-010 type=list -->
-
<!-- existing blocks ... -->
<!-- block_id=block-011 type=paragraph -->
**Additionally, I plan to learn some basic French to make communication easier during the trip.**
Example response:
\`\`\`json
[
{
"op": "Bold the player's name in the introduction",
"updates": "
<!-- block_id=003 flavour=paragraph -->
**Andriy Shevchenko** is a legendary Ukrainian striker, best known for his time at AC Milan and Dynamo Kyiv. He won the Ballon d'Or in 2004.
"
},
{
"op": "Add a summary section at the end",
"updates": "
<!-- block_id=new-abc123 flavour=paragraph -->
## Summary
<!-- block_id=new-def456 flavour=paragraph -->
Shevchenko is celebrated as one of the greatest Ukrainian footballers of all time. Known for his composure, strength, and goal-scoring instinct, he left a lasting legacy both on and off the pitch.
"
},
{
"op": "Delete the career overview section",
"updates": "
<!-- delete block_id=004 -->
<!-- delete block_id=005 -->
<!-- delete block_id=006 -->
<!-- delete block_id=007 -->
<!-- delete block_id=008 -->
<!-- delete block_id=009 -->
"
}
]
\`\`\`
You should specify the following arguments before the others: [doc_id], [origin_content]
@@ -143,15 +150,42 @@ You should specify the following arguments before the others: [doc_id], [origin_
'A short, first-person description of the intended edit, clearly summarizing what I will change. For example: "I will translate the steps into English and delete the paragraph explaining the delay." This helps the downstream system understand the purpose of the changes.'
),
code_edit: z
.string()
.describe(
'Specify only the necessary Markdown block-level changes. Return a list of inserted, replaced, or deleted blocks. Each block must start with its <!-- block_id=... type=... --> comment. Use <!-- existing blocks ... --> for unchanged sections.If you plan on deleting a section, you must provide surrounding context to indicate the deletion.'
),
code_edit: z.preprocess(
val => {
// BACKGROUND: LLM sometimes returns a JSON string instead of an array.
if (typeof val === 'string') {
return JSON.parse(val);
}
return val;
},
z
.array(
z.object({
op: z
.string()
.describe(
'A short description of the change, such as "Bold intro name"'
),
updates: z
.string()
.describe(
'Markdown block fragments that represent the change, including the block_id and type'
),
})
)
.describe(
'An array of independent semantic changes to apply to the document.'
)
),
}),
execute: async ({ doc_id, origin_content, code_edit }) => {
try {
const provider = await factory.getProviderByModel('morph-v2');
const applyPrompt = await prompt.get('Apply Updates');
if (!applyPrompt) {
return 'Prompt not found';
}
const model = applyPrompt.model;
const provider = await factory.getProviderByModel(model);
if (!provider) {
return 'Editing docs is not supported';
}
@@ -160,14 +194,27 @@ You should specify the following arguments before the others: [doc_id], [origin_
if (!content) {
return 'Doc not found or doc is empty';
}
const result = await provider.text({ modelId: 'morph-v2' }, [
{
role: 'user',
content: `<code>${content}</code>\n<update>${code_edit}</update>`,
},
]);
return { result, content };
const changedContents = await Promise.all(
code_edit.map(async edit => {
return await provider.text({ modelId: model }, [
...applyPrompt.finish({
content,
op: edit.op,
updates: edit.updates,
}),
]);
})
);
return {
result: changedContents.map((changedContent, index) => ({
op: code_edit[index].op,
updates: code_edit[index].updates,
originalContent: content,
changedContent,
})),
};
} catch {
return 'Failed to apply edit to the doc';
}
@@ -1,4 +1,5 @@
import { tool } from 'ai';
import { omit } from 'lodash-es';
import { z } from 'zod';
import type { AccessController } from '../../../core/permission';
@@ -8,6 +9,32 @@ import type { ContextSession } from '../context/session';
import type { CopilotChatOptions } from '../providers';
import { toolError } from './error';
const FILTER_PREFIX = [
'Title: ',
'Created at: ',
'Updated at: ',
'Created by: ',
'Updated by: ',
];
function clearEmbeddingChunk(chunk: ChunkSimilarity): ChunkSimilarity {
if (chunk.content) {
const lines = chunk.content.split('\n');
let maxLines = 5;
while (maxLines > 0 && lines.length > 0) {
if (FILTER_PREFIX.some(prefix => lines[0].startsWith(prefix))) {
lines.shift();
maxLines--;
} else {
// only process consecutive metadata rows
break;
}
}
return { ...chunk, content: lines.join('\n') };
}
return chunk;
}
export const buildDocSearchGetter = (
ac: AccessController,
context: CopilotContextService,
@@ -47,18 +74,37 @@ export const buildDocSearchGetter = (
if (!docChunks.length && !fileChunks.length)
return `No results found for "${query}".`;
const docIds = docChunks.map(c => ({
// oxlint-disable-next-line no-non-null-assertion
workspaceId: options.workspace!,
docId: c.docId,
}));
const docAuthors = await models.doc
.findAuthors(docIds)
.then(
docs =>
new Map(
docs
.filter(d => !!d)
.map(doc => [doc.id, omit(doc, ['id', 'workspaceId'])])
)
);
const docMetas = await models.doc
.findAuthors(
docChunks.map(c => ({
// oxlint-disable-next-line no-non-null-assertion
workspaceId: options.workspace!,
docId: c.docId,
}))
)
.then(docs => new Map(docs.filter(d => !!d).map(doc => [doc.id, doc])));
.findMetas(docIds, { select: { title: true } })
.then(
docs =>
new Map(
docs
.filter(d => !!d)
.map(doc => [
doc.docId,
Object.assign({}, doc, docAuthors.get(doc.docId)),
])
)
);
return [
...fileChunks,
...fileChunks.map(clearEmbeddingChunk),
...docChunks.map(c => ({
...c,
...docMetas.get(c.docId),
@@ -9,6 +9,7 @@ import { createDocReadTool } from './doc-read';
import { createDocSemanticSearchTool } from './doc-semantic-search';
import { createExaCrawlTool } from './exa-crawl';
import { createExaSearchTool } from './exa-search';
import { createSectionEditTool } from './section-edit';
export interface CustomAITools extends ToolSet {
code_artifact: ReturnType<typeof createCodeArtifactTool>;
@@ -18,6 +19,7 @@ export interface CustomAITools extends ToolSet {
doc_keyword_search: ReturnType<typeof createDocKeywordSearchTool>;
doc_read: ReturnType<typeof createDocReadTool>;
doc_compose: ReturnType<typeof createDocComposeTool>;
section_edit: ReturnType<typeof createSectionEditTool>;
web_search_exa: ReturnType<typeof createExaSearchTool>;
web_crawl_exa: ReturnType<typeof createExaCrawlTool>;
}
@@ -32,3 +34,4 @@ export * from './doc-semantic-search';
export * from './error';
export * from './exa-crawl';
export * from './exa-search';
export * from './section-edit';
@@ -0,0 +1,66 @@
import { Logger } from '@nestjs/common';
import { tool } from 'ai';
import { z } from 'zod';
import type { PromptService } from '../prompt';
import type { CopilotProviderFactory } from '../providers';
import { toolError } from './error';
const logger = new Logger('SectionEditTool');
export const createSectionEditTool = (
promptService: PromptService,
factory: CopilotProviderFactory
) => {
return tool({
description:
'Intelligently edit and modify a specific section of a document based on user instructions, with full document context awareness. This tool can refine, rewrite, translate, restructure, or enhance any part of markdown content while preserving formatting, maintaining contextual coherence, and ensuring consistency with the entire document. Perfect for targeted improvements that consider the broader document context.',
parameters: z.object({
section: z
.string()
.describe(
'The specific section or text snippet to be modified (in markdown format). This is the target content that will be edited and replaced.'
),
instructions: z
.string()
.describe(
'Clear and specific instructions describing the desired changes. Examples: "make this more formal and professional", "translate to Chinese while keeping technical terms", "add more technical details and examples", "fix grammar and improve clarity", "restructure for better readability"'
),
document: z
.string()
.describe(
"The complete document content (in markdown format) that provides context for the section being edited. This ensures the edited section maintains consistency with the document's overall tone, style, terminology, and structure."
),
}),
execute: async ({ section, instructions, document }) => {
try {
const prompt = await promptService.get('Section Edit');
if (!prompt) {
throw new Error('Prompt not found');
}
const provider = await factory.getProviderByModel(prompt.model);
if (!provider) {
throw new Error('Provider not found');
}
const content = await provider.text(
{
modelId: prompt.model,
},
prompt.finish({
content: section,
instructions,
document,
})
);
return {
content: content.trim(),
};
} catch (err: any) {
logger.error(`Failed to edit section`, err);
return toolError('Section Edit Failed', err.message);
}
},
});
};
@@ -15,7 +15,6 @@ import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import {
CopilotTranscriptionAudioNotProvided,
CopilotTranscriptionJobNotFound,
type FileUpload,
} from '../../../base';
import { CurrentUser } from '../../../core/auth';
@@ -74,7 +73,7 @@ const FinishedStatus: Set<AiJobStatus> = new Set([
export class CopilotTranscriptionResolver {
constructor(
private readonly ac: AccessController,
private readonly service: CopilotTranscriptionService
private readonly transcript: CopilotTranscriptionService
) {}
private handleJobResult(
@@ -122,7 +121,7 @@ export class CopilotTranscriptionResolver {
throw new CopilotTranscriptionAudioNotProvided();
}
const jobResult = await this.service.submitTranscriptionJob(
const jobResult = await this.transcript.submitJob(
user.id,
workspaceId,
blobId,
@@ -144,19 +143,11 @@ export class CopilotTranscriptionResolver {
.allowLocal()
.assert('Workspace.Copilot');
const job = await this.service.queryTranscriptionJob(
const jobResult = await this.transcript.retryJob(
user.id,
workspaceId,
jobId
);
if (!job || !job.infos) {
throw new CopilotTranscriptionJobNotFound();
}
const jobResult = await this.service.executeTranscriptionJob(
job.id,
job.infos
);
return this.handleJobResult(jobResult);
}
@@ -166,7 +157,7 @@ export class CopilotTranscriptionResolver {
@CurrentUser() user: CurrentUser,
@Args('jobId') jobId: string
): Promise<TranscriptionResultType | null> {
const job = await this.service.claimTranscriptionJob(user.id, jobId);
const job = await this.transcript.claimJob(user.id, jobId);
return this.handleJobResult(job);
}
@@ -190,7 +181,7 @@ export class CopilotTranscriptionResolver {
.allowLocal()
.assert('Workspace.Copilot');
const job = await this.service.queryTranscriptionJob(
const job = await this.transcript.queryJob(
user.id,
copilot.workspaceId,
jobId,
@@ -49,7 +49,17 @@ export class CopilotTranscriptionService {
private readonly providerFactory: CopilotProviderFactory
) {}
async submitTranscriptionJob(
private async getModel(userId: string) {
const prompt = await this.prompt.get('Transcript audio');
const hasAccess = await this.models.userFeature.has(
userId,
'unlimited_copilot'
);
// choose the pro model if user has copilot plan
return prompt?.optionalModels[hasAccess ? 1 : 0];
}
async submitJob(
userId: string,
workspaceId: string,
blobId: string,
@@ -78,12 +88,26 @@ export class CopilotTranscriptionService {
infos.push({ url, mimeType: blob.mimetype });
}
return await this.executeTranscriptionJob(jobId, infos);
const model = await this.getModel(userId);
return await this.executeJob(jobId, infos, model);
}
async executeTranscriptionJob(
async retryJob(userId: string, workspaceId: string, jobId: string) {
const job = await this.queryJob(userId, workspaceId, jobId);
if (!job || !job.infos) {
throw new CopilotTranscriptionJobNotFound();
}
const model = await this.getModel(userId);
const jobResult = await this.executeJob(job.id, job.infos, model);
return jobResult;
}
async executeJob(
jobId: string,
infos: AudioBlobInfos
infos: AudioBlobInfos,
modelId?: string
): Promise<TranscriptionJob> {
const status = AiJobStatus.running;
const success = await this.models.copilotJob.update(jobId, {
@@ -98,12 +122,13 @@ export class CopilotTranscriptionService {
await this.job.add('copilot.transcript.submit', {
jobId,
infos,
modelId,
});
return { id: jobId, status };
}
async claimTranscriptionJob(
async claimJob(
userId: string,
jobId: string
): Promise<TranscriptionJob | null> {
@@ -118,7 +143,7 @@ export class CopilotTranscriptionService {
return null;
}
async queryTranscriptionJob(
async queryJob(
userId: string,
workspaceId: string,
jobId?: string,
@@ -181,14 +206,20 @@ export class CopilotTranscriptionService {
promptName: string,
message: Partial<PromptMessage>,
schema?: ZodType<any>,
prefer?: CopilotProviderType
prefer?: CopilotProviderType,
modelId?: string
): Promise<string> {
const prompt = await this.prompt.get(promptName);
if (!prompt) {
throw new CopilotPromptNotFound({ name: promptName });
}
const cond = { modelId: prompt.model };
const cond = {
modelId:
modelId && prompt.optionalModels.includes(modelId)
? modelId
: prompt.model,
};
const msg = { role: 'user' as const, content: '', ...message };
const config = Object.assign({}, prompt.config);
if (schema) {
@@ -231,13 +262,19 @@ export class CopilotTranscriptionService {
return `${hoursStr}:${minutesStr}:${secondsStr}`;
}
private async callTranscript(url: string, mimeType: string, offset: number) {
private async callTranscript(
url: string,
mimeType: string,
offset: number,
modelId?: string
) {
// NOTE: Vertex provider not support transcription yet, we always use Gemini here
const result = await this.chatWithPrompt(
'Transcript audio',
{ attachments: [url], params: { mimetype: mimeType } },
TranscriptionResponseSchema,
CopilotProviderType.Gemini
CopilotProviderType.Gemini,
modelId
);
const transcription = TranscriptionResponseSchema.parse(
@@ -256,6 +293,7 @@ export class CopilotTranscriptionService {
async transcriptAudio({
jobId,
infos,
modelId,
// @deprecated
url,
mimeType,
@@ -264,7 +302,7 @@ export class CopilotTranscriptionService {
const blobInfos = this.mergeInfos(infos, url, mimeType);
const transcriptions = await Promise.all(
Array.from(blobInfos.entries()).map(([idx, { url, mimeType }]) =>
this.callTranscript(url, mimeType, idx * 10 * 60)
this.callTranscript(url, mimeType, idx * 10 * 60, modelId)
)
);
@@ -56,6 +56,7 @@ declare global {
'copilot.transcript.submit': {
jobId: string;
infos?: AudioBlobInfos;
modelId?: string;
/// @deprecated use `infos` instead
url?: string;
/// @deprecated use `infos` instead
@@ -16,6 +16,23 @@ const zMaybeString = z.preprocess(val => {
return s === '' || s == null ? undefined : s;
}, z.string().min(1).optional());
const ToolsConfigSchema = z.preprocess(
val => {
// if val is a string, try to parse it as JSON
if (typeof val === 'string') {
try {
return JSON.parse(val);
} catch {
return {};
}
}
return val || {};
},
z.record(z.enum(['searchWorkspace', 'readingDocs']), z.boolean()).default({})
);
export type ToolsConfig = z.infer<typeof ToolsConfigSchema>;
export const ChatQuerySchema = z
.object({
messageId: zMaybeString,
@@ -23,15 +40,25 @@ export const ChatQuerySchema = z
retry: zBool,
reasoning: zBool,
webSearch: zBool,
toolsConfig: ToolsConfigSchema,
})
.catchall(z.string())
.transform(
({ messageId, modelId, retry, reasoning, webSearch, ...params }) => ({
({
messageId,
modelId,
retry,
reasoning,
webSearch,
toolsConfig,
...params
}) => ({
messageId,
modelId,
retry,
reasoning,
webSearch,
toolsConfig,
params,
})
);
@@ -3,7 +3,8 @@ import { Readable } from 'node:stream';
import type { Request } from 'express';
import { readBufferWithLimit } from '../../base';
import { MAX_EMBEDDABLE_SIZE } from './types';
import { PromptTools } from './providers';
import { MAX_EMBEDDABLE_SIZE, ToolsConfig } from './types';
export function readStream(
readable: Readable,
@@ -49,3 +50,33 @@ export function getSignal(req: Request): SignalReturnType {
onConnectionClosed: cb => (callback = cb),
};
}
export function getTools(
tools?: PromptTools | null,
toolsConfig?: ToolsConfig
) {
if (!tools || !toolsConfig) {
return tools;
}
let result: PromptTools = tools;
(Object.keys(toolsConfig) as Array<keyof ToolsConfig>).forEach(key => {
const value = toolsConfig[key];
switch (key) {
case 'searchWorkspace':
if (value === false) {
result = result.filter(tool => {
return tool !== 'docKeywordSearch' && tool !== 'docSemanticSearch';
});
}
break;
case 'readingDocs':
if (value === false) {
result = result.filter(tool => {
return tool !== 'docRead';
});
}
break;
}
});
return result;
}
@@ -103,6 +103,7 @@ export class CopilotWorkspaceEmbeddingConfigResolver {
return ignoredDocs;
}
@Mutation(() => Number, {
name: 'updateWorkspaceEmbeddingIgnoredDocs',
complexity: 2,
+10
View File
@@ -457,6 +457,7 @@ type CopilotWorkspaceIgnoredDocTypeEdge {
input CreateChatMessageInput {
attachments: [String!]
blob: Upload
blobs: [Upload!]
content: String
params: JSON
@@ -1297,6 +1298,12 @@ type Mutation {
setBlob(blob: Upload!, workspaceId: String!): String!
submitAudioTranscription(blob: Upload, blobId: String!, blobs: [Upload!], workspaceId: String!): TranscriptionResultType
"""Trigger cleanup of trashed doc embeddings"""
triggerCleanupTrashedDocEmbeddings: Boolean!
"""Trigger generate missing titles cron job"""
triggerGenerateTitleCron: Boolean!
"""update app configuration"""
updateAppConfig(updates: [UpdateAppConfigInput!]!): JSONObject!
@@ -1518,6 +1525,9 @@ type PublicUserType {
type Query {
"""get the whole app configuration"""
appConfig: JSONObject!
"""Apply updates to a doc using LLM and return the merged markdown."""
applyDocUpdates(docId: String!, op: String!, updates: String!, workspaceId: String!): String!
collectAllBlobSizes: WorkspaceBlobSizes! @deprecated(reason: "use `user.quotaUsage` instead")
"""Get current user"""
@@ -0,0 +1,3 @@
query applyDocUpdates($workspaceId: String!, $docId: String!, $op: String!, $updates: String!) {
applyDocUpdates(workspaceId: $workspaceId, docId: $docId, op: $op, updates: $updates)
}
@@ -555,6 +555,19 @@ export const uploadCommentAttachmentMutation = {
file: true,
};
export const applyDocUpdatesQuery = {
id: 'applyDocUpdatesQuery' as const,
op: 'applyDocUpdates',
query: `query applyDocUpdates($workspaceId: String!, $docId: String!, $op: String!, $updates: String!) {
applyDocUpdates(
workspaceId: $workspaceId
docId: $docId
op: $op
updates: $updates
)
}`,
};
export const addContextCategoryMutation = {
id: 'addContextCategoryMutation' as const,
op: 'addContextCategory',
+31
View File
@@ -569,6 +569,7 @@ export interface CopilotWorkspaceIgnoredDocTypeEdge {
export interface CreateChatMessageInput {
attachments?: InputMaybe<Array<Scalars['String']['input']>>;
blob?: InputMaybe<Scalars['Upload']['input']>;
blobs?: InputMaybe<Array<Scalars['Upload']['input']>>;
content?: InputMaybe<Scalars['String']['input']>;
params?: InputMaybe<Scalars['JSON']['input']>;
@@ -1440,6 +1441,10 @@ export interface Mutation {
sendVerifyEmail: Scalars['Boolean']['output'];
setBlob: Scalars['String']['output'];
submitAudioTranscription: Maybe<TranscriptionResultType>;
/** Trigger cleanup of trashed doc embeddings */
triggerCleanupTrashedDocEmbeddings: Scalars['Boolean']['output'];
/** Trigger generate missing titles cron job */
triggerGenerateTitleCron: Scalars['Boolean']['output'];
/** update app configuration */
updateAppConfig: Scalars['JSONObject']['output'];
/** Update a comment content */
@@ -2073,6 +2078,8 @@ export interface Query {
__typename?: 'Query';
/** get the whole app configuration */
appConfig: Scalars['JSONObject']['output'];
/** Apply updates to a doc using LLM and return the merged markdown. */
applyDocUpdates: Scalars['String']['output'];
/** @deprecated use `user.quotaUsage` instead */
collectAllBlobSizes: WorkspaceBlobSizes;
/** Get current user */
@@ -2120,6 +2127,13 @@ export interface Query {
workspaces: Array<WorkspaceType>;
}
export interface QueryApplyDocUpdatesArgs {
docId: Scalars['String']['input'];
op: Scalars['String']['input'];
updates: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
}
export interface QueryErrorArgs {
name: ErrorNames;
}
@@ -3509,6 +3523,18 @@ export type UploadCommentAttachmentMutation = {
uploadCommentAttachment: string;
};
export type ApplyDocUpdatesQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: Scalars['String']['input'];
op: Scalars['String']['input'];
updates: Scalars['String']['input'];
}>;
export type ApplyDocUpdatesQuery = {
__typename?: 'Query';
applyDocUpdates: string;
};
export type AddContextCategoryMutationVariables = Exact<{
options: AddContextCategoryInput;
}>;
@@ -6148,6 +6174,11 @@ export type Queries =
variables: ListCommentsQueryVariables;
response: ListCommentsQuery;
}
| {
name: 'applyDocUpdatesQuery';
variables: ApplyDocUpdatesQueryVariables;
response: ApplyDocUpdatesQuery;
}
| {
name: 'listContextObjectQuery';
variables: ListContextObjectQueryVariables;
@@ -58,74 +58,45 @@ exports[`should parse page doc work 1`] = `
# You own your data, with no compromises
## Local-first & Real-time collaborative
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.
### Blocks that assemble your next docs, tasks kanban or whiteboard
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.
If you want to learn more about the product design of AFFiNE, here goes the concepts:
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.
## A true canvas for blocks in any form
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:
* Quip & Notion with their great concept of "everything is a block"
* Trello with their Kanban
* Airtable & Miro with their no-code programable datasheets
* Miro & Whimiscal with their edgeless visual whiteboard
* Remnote & Capacities with their object-based tag system
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)
## Self Host
Self host AFFiNE
||Title|Tag|
|---|---|---|
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|
@@ -136,16 +107,12 @@ Self host AFFiNE
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||
## Affine Development
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)
",
"parsedBlock": {
"children": [
@@ -322,7 +289,6 @@ For developer or installation guides, please go to [AFFiNE Development](https://
{
"children": [],
"content": "* Quip & Notion with their great concept of "everything is a block"
",
"flavour": "affine:list",
"id": "xFrrdiP3-V",
@@ -331,7 +297,6 @@ For developer or installation guides, please go to [AFFiNE Development](https://
{
"children": [],
"content": "* Trello with their Kanban
",
"flavour": "affine:list",
"id": "Tp9xyN4Okl",
@@ -340,7 +305,6 @@ For developer or installation guides, please go to [AFFiNE Development](https://
{
"children": [],
"content": "* Airtable & Miro with their no-code programable datasheets
",
"flavour": "affine:list",
"id": "K_4hUzKZFQ",
@@ -349,7 +313,6 @@ For developer or installation guides, please go to [AFFiNE Development](https://
{
"children": [],
"content": "* Miro & Whimiscal with their edgeless visual whiteboard
",
"flavour": "affine:list",
"id": "QwMzON2s7x",
@@ -358,7 +321,6 @@ For developer or installation guides, please go to [AFFiNE Development](https://
{
"children": [],
"content": "* Remnote & Capacities with their object-based tag system
",
"flavour": "affine:list",
"id": "FFVmit6u1T",
@@ -427,77 +389,63 @@ For developer or installation guides, please go to [AFFiNE Development](https://
"Tag": "<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>",
"Title": "Affine Development
",
"undefined": "Affine Development
",
},
{
"Tag": "<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>",
"Title": "For developers or installations guides, please go to AFFiNE Doc
",
"undefined": "For developers or installations guides, please go to AFFiNE Doc
",
},
{
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
"Title": "Quip & Notion with their great concept of "everything is a block"
",
"undefined": "Quip & Notion with their great concept of "everything is a block"
",
},
{
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
"Title": "Trello with their Kanban
",
"undefined": "Trello with their Kanban
",
},
{
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
"Title": "Airtable & Miro with their no-code programable datasheets
",
"undefined": "Airtable & Miro with their no-code programable datasheets
",
},
{
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
"Title": "Miro & Whimiscal with their edgeless visual whiteboard
",
"undefined": "Miro & Whimiscal with their edgeless visual whiteboard
",
},
{
"Tag": "",
"Title": "Remnote & Capacities with their object-based tag system
",
"undefined": "Remnote & Capacities with their object-based tag system
",
},
],
@@ -559,113 +507,80 @@ exports[`should parse page doc work with ai editable 1`] = `
"<!-- block_id=FoPQcAyV_m flavour=affine:paragraph -->
AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.
<!-- block_id=oz48nn_zp8 flavour=affine:paragraph -->
<!-- block_id=g8a-D9-jXS flavour=affine:paragraph -->
# You own your data, with no compromises
<!-- block_id=J8lHN1GR_5 flavour=affine:paragraph -->
## Local-first & Real-time collaborative
<!-- block_id=xCuWdM0VLz flavour=affine:paragraph -->
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.
<!-- block_id=zElMi0tViK flavour=affine:paragraph -->
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.
<!-- block_id=Z4rK0OF9Wk flavour=affine:paragraph -->
<!-- block_id=DQ0Ryb-SpW flavour=affine:paragraph -->
### Blocks that assemble your next docs, tasks kanban or whiteboard
<!-- block_id=HAZC3URZp_ flavour=affine:paragraph -->
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.
<!-- block_id=0H87ypiuv8 flavour=affine:paragraph -->
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.
<!-- block_id=Sp4G1KD0Wn flavour=affine:paragraph -->
If you want to learn more about the product design of AFFiNE, here goes the concepts:
<!-- block_id=RsUhDuEqXa flavour=affine:paragraph -->
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.
<!-- block_id=Z2HibKzAr- flavour=affine:paragraph -->
## A true canvas for blocks in any form
<!-- block_id=UwvWddamzM flavour=affine:paragraph -->
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.
<!-- block_id=g9xKUjhJj1 flavour=affine:paragraph -->
<!-- block_id=wDTn4YJ4pm flavour=affine:paragraph -->
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:
<!-- block_id=xFrrdiP3-V flavour=affine:list -->
* Quip & Notion with their great concept of "everything is a block"
<!-- block_id=Tp9xyN4Okl flavour=affine:list -->
* Trello with their Kanban
<!-- block_id=K_4hUzKZFQ flavour=affine:list -->
* Airtable & Miro with their no-code programable datasheets
<!-- block_id=QwMzON2s7x flavour=affine:list -->
* Miro & Whimiscal with their edgeless visual whiteboard
<!-- block_id=FFVmit6u1T flavour=affine:list -->
* Remnote & Capacities with their object-based tag system
<!-- block_id=YqnG5O6AE6 flavour=affine:paragraph -->
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)
<!-- block_id=sbDTmZMZcq flavour=affine:paragraph -->
## Self Host
<!-- block_id=QVvitesfbj flavour=affine:paragraph -->
Self host AFFiNE
<!-- block_id=U_GoHFD9At flavour=affine:database placeholder -->
<!-- block_id=NyHXrMX3R1 flavour=affine:paragraph -->
## Affine Development
<!-- block_id=9-K49otbCv flavour=affine:paragraph -->
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)
<!-- block_id=faFteK9eG- flavour=affine:paragraph -->
"
`;
@@ -673,122 +588,74 @@ exports[`should parse page full doc work with ai editable 1`] = `
"<!-- block_id=T4qSXc13wz flavour=affine:paragraph -->
# H1 text
<!-- block_id=F5eByK8Fx_ flavour=affine:paragraph -->
List all flavours in one document.
<!-- block_id=6_-Ta2Hpsg flavour=affine:paragraph -->
## H2 ~ H6
<!-- block_id=QLH8pCeJwr flavour=affine:paragraph -->
### H3
<!-- block_id=eRseB5ilzP flavour=affine:paragraph -->
#### H4 with emoji 😄
<!-- block_id=xSEIo9I5jQ flavour=affine:paragraph -->
##### H5
<!-- block_id=h4Fozi-Mvv flavour=affine:paragraph -->
###### H6
<!-- block_id=U-Hd9O6FEZ flavour=affine:paragraph -->
max is H6
<!-- block_id=z2aCxUDpOc flavour=affine:paragraph -->
## List
<!-- block_id=z5Zw7lMlD7 flavour=affine:list -->
* item 1
<!-- block_id=Opmt3x2Ao0 flavour=affine:list -->
* item 2
* sub item 1
* sub item 2
* super sub item 1
* sub item 3
* sub item 1
* sub item 2
* super sub item 1
* sub item 3
<!-- block_id=_EF3g4194w flavour=affine:list -->
* item 3
<!-- block_id=5u-T48lLVF flavour=affine:paragraph -->
<!-- block_id=7urxrvhr-p flavour=affine:paragraph -->
<!-- block_id=U-96XKGGz7 flavour=affine:paragraph -->
<!-- block_id=hOvvRmDGqN flavour=affine:paragraph -->
sort list
<!-- block_id=hcqkMyvKnx flavour=affine:list -->
1. item 1
<!-- block_id=xUsDktnmuD flavour=affine:list -->
1. item 2
<!-- block_id=xa5tsLHHJN flavour=affine:list -->
1. item 3
1. sub item 1
1. sub item 2
1. super item 1
1. super item 2
1. sub item 3
1. sub item 1
1. sub item 2
1. super item 1
1. super item 2
1. sub item 3
<!-- block_id=BX05mQdxJ0 flavour=affine:list -->
1. item 4
<!-- block_id=VYzM3O17th flavour=affine:paragraph -->
<!-- block_id=epKYpKt5vo flavour=affine:paragraph -->
<!-- block_id=5Ghem19uGh flavour=affine:paragraph -->
Table
<!-- block_id=OXvH-s1Jx4 flavour=affine:table -->
|c1|c2|c3|c4|
|---|---|---|---|
@@ -796,176 +663,129 @@ Table
||||v4|
||v6||v5|
<!-- block_id=j2F2hQ3zy9 flavour=affine:paragraph -->
<!-- block_id=jLCRD2G_BC flavour=affine:paragraph -->
<!-- block_id=794ZoPeBJM flavour=affine:paragraph -->
Database
<!-- block_id=xQ7rA57Qxz flavour=affine:database placeholder -->
<!-- block_id=RbMSmluZYK flavour=affine:paragraph -->
Code
<!-- block_id=cJ6CMeUWMg flavour=affine:code -->
\`\`\`javascript
console.log('hello world');
\`\`\`
<!-- block_id=y1xVwkxlDm flavour=affine:paragraph -->
<!-- block_id=BKy3zmm8SE flavour=affine:paragraph -->
Image
<!-- block_id=WFftQ-qXzr flavour=affine:image -->
![-HjsQksaVuEaM0KeTEbBYDgWVOmoVzrAeVK0kn4Jfr0=](blob://-HjsQksaVuEaM0KeTEbBYDgWVOmoVzrAeVK0kn4Jfr0=)
<!-- block_id=F-RKpfxL1z flavour=affine:paragraph -->
<!-- block_id=G3LSqjKv8M flavour=affine:paragraph -->
File
<!-- block_id=pO8JCsiK4z flavour=affine:attachment -->
![IrsiJ9XonFXF8fw9tLQXbSsBbUYFfzLgoFpodeidQOU=](blob://IrsiJ9XonFXF8fw9tLQXbSsBbUYFfzLgoFpodeidQOU=)
<!-- block_id=dTKFqQhJuA flavour=affine:paragraph -->
<!-- block_id=nwld7RMYvp flavour=affine:paragraph -->
> foo bar quote text
<!-- block_id=MwBD3BhRnf flavour=affine:paragraph -->
<!-- block_id=pakOSAm6EU flavour=affine:paragraph -->
<!-- block_id=95-NxAyFuo flavour=affine:divider -->
---
<!-- block_id=r9EllTNiN1 flavour=affine:paragraph -->
<!-- block_id=OpxZ1kYM40 flavour=affine:paragraph -->
TeX
<!-- block_id=gjFqI97IRc flavour=affine:paragraph -->
<!-- block_id=KXBZ1_Pfdw flavour=affine:paragraph -->
<!-- block_id=VHj5gMaGa7 flavour=affine:paragraph -->
2025-06-18 13:15
<!-- block_id=JwaUwzuQEH flavour=affine:paragraph -->
<!-- block_id=_zu2kl56FY flavour=affine:database placeholder -->
<!-- block_id=Kcbp6BLA-y flavour=affine:paragraph -->
Mind Map
<!-- block_id=R_g1tzqzAU flavour=affine:paragraph -->
<!-- block_id=C8G82uLCz1 flavour=affine:paragraph -->
<!-- block_id=J6gfR8YMGy flavour=affine:paragraph -->
A Link
<!-- block_id=yHky0s_H1v flavour=affine:embed-linked-doc -->
[null](doc://FmHFPAPzp51JjFP89aZ-b)
<!-- block_id=P7w3ka4Amo flavour=affine:paragraph -->
Todo List
<!-- block_id=WbeCXu6fcA flavour=affine:list -->
- [ ] abc
<!-- block_id=X_F5fw-MEn flavour=affine:list -->
- [ ] edf
- [x] done1
- [x] done1
<!-- block_id=sdw-couBVA flavour=affine:list -->
- [ ] end
<!-- block_id=COJiWGOVJu flavour=affine:paragraph -->
<!-- block_id=shK7TY-Q3F flavour=affine:paragraph -->
~~delete text~~
<!-- block_id=_NIj4pT_Iy flavour=affine:paragraph -->
<!-- block_id=CaXXPfEt62 flavour=affine:paragraph -->
**Bold text**
<!-- block_id=1WFCwn1708 flavour=affine:paragraph -->
<!-- block_id=25f19QUjQI flavour=affine:paragraph -->
Underline
<!-- block_id=GrS-y17iiw flavour=affine:paragraph -->
<!-- block_id=dJm5C8KsEg flavour=affine:paragraph -->
Youtube
<!-- block_id=epfNja2Txk flavour=affine:embed-youtube -->
<iframe
@@ -979,23 +799,18 @@ Youtube
credentialless>
</iframe>
<!-- block_id=wNb6ZRJKMt flavour=affine:paragraph -->
<!-- block_id=HqKjEGWF_s flavour=affine:paragraph -->
## end
<!-- block_id=FOh_TJmcF1 flavour=affine:paragraph -->
this is end
<!-- block_id=ImCJN2Xint flavour=affine:paragraph -->
"
`;
@@ -22,9 +22,10 @@ export const parseBlockToMd = (
block.content
.split('\n')
.map(line => padding + line)
.slice(0, -1)
.join('\n') +
'\n' +
block.children.map(b => parseBlockToMd(b, padding + ' ')).join('')
block.children.map(b => parseBlockToMd(b, padding + ' ')).join('')
);
} else {
return block.children.map(b => parseBlockToMd(b, padding)).join('');
@@ -109,7 +110,7 @@ export function parseBlock(
const checked = yBlock.get('prop:checked') as boolean;
prefix = checked ? '- [x] ' : '- [ ] ';
}
result.content = prefix + toMd() + '\n';
result.content = prefix + toMd();
break;
}
case 'affine:code': {
@@ -14,6 +14,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme">
<activity
@@ -10,6 +10,7 @@ import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
import { setupEffects } from './effects';
import { DesktopLanguageSync } from './language-sync';
import { DesktopThemeSync } from './theme-sync';
const { frameworkProvider } = setupEffects();
@@ -46,6 +47,7 @@ export function App() {
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<DesktopThemeSync />
<DesktopLanguageSync />
<RouterProvider
fallbackElement={<AppContainer fallback />}
router={router}
@@ -0,0 +1,18 @@
import { DesktopApiService } from '@affine/core/modules/desktop-api';
import { I18nService } from '@affine/core/modules/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useEffect } from 'react';
export const DesktopLanguageSync = () => {
const i18nService = useService(I18nService);
const currentLanguage = useLiveData(i18nService.i18n.currentLanguageKey$);
const handler = useService(DesktopApiService).api.handler;
useEffect(() => {
handler.i18n.changeLanguage(currentLanguage ?? 'en').catch(err => {
console.error(err);
});
}, [currentLanguage, handler]);
return null;
};
@@ -33,6 +33,7 @@
},
"devDependencies": {
"@affine-tools/utils": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/native": "workspace:*",
"@affine/nbstore": "workspace:*",
"@electron-forge/cli": "^7.6.0",
@@ -1,3 +1,4 @@
import { I18n } from '@affine/i18n';
import { ipcMain } from 'electron';
import { AFFINE_API_CHANNEL_NAME } from '../shared/type';
@@ -21,6 +22,12 @@ export const debugHandlers = {
},
};
export const i18nHandlers = {
changeLanguage: async (_: Electron.IpcMainInvokeEvent, language: string) => {
return I18n.changeLanguage(language);
},
};
// Note: all of these handlers will be the single-source-of-truth for the apis exposed to the renderer process
export const allHandlers = {
debug: debugHandlers,
@@ -33,6 +40,7 @@ export const allHandlers = {
worker: workerHandlers,
recording: recordingHandlers,
popup: popupHandlers,
i18n: i18nHandlers,
};
export const registerHandlers = () => {
@@ -1,25 +1,17 @@
import type { MainEventRegister } from '../type';
import { globalCacheStorage, globalStateStorage } from './storage';
import { globalCacheUpdates$, globalStateUpdates$ } from './handlers';
export const sharedStorageEvents = {
onGlobalStateChanged: (
fn: (state: Record<string, unknown | undefined>) => void
) => {
const subscription = globalStateStorage.watchAll().subscribe(updates => {
fn(updates);
});
return () => {
subscription.unsubscribe();
};
const subscription = globalStateUpdates$.subscribe(fn);
return () => subscription.unsubscribe();
},
onGlobalCacheChanged: (
fn: (state: Record<string, unknown | undefined>) => void
) => {
const subscription = globalCacheStorage.watchAll().subscribe(updates => {
fn(updates);
});
return () => {
subscription.unsubscribe();
};
const subscription = globalCacheUpdates$.subscribe(fn);
return () => subscription.unsubscribe();
},
} satisfies Record<string, MainEventRegister>;
@@ -1,6 +1,22 @@
import { Subject } from 'rxjs';
import type { NamespaceHandlers } from '../type';
import { globalCacheStorage, globalStateStorage } from './storage';
// Subjects used by shared-storage/events.ts to broadcast updates to all renderer processes
export const globalStateUpdates$ = new Subject<Record<string, any>>();
export const globalCacheUpdates$ = new Subject<Record<string, any>>();
// Revision maps; main generates the next value each time
const globalStateRevisions = new Map<string, number>();
const globalCacheRevisions = new Map<string, number>();
function nextRev(revisions: Map<string, number>, key: string) {
const r = (revisions.get(key) ?? 0) + 1;
revisions.set(key, r);
return r;
}
export const sharedStorageHandlers = {
getAllGlobalState: async () => {
return globalStateStorage.all();
@@ -8,22 +24,36 @@ export const sharedStorageHandlers = {
getAllGlobalCache: async () => {
return globalCacheStorage.all();
},
setGlobalState: async (_, key: string, value: any) => {
return globalStateStorage.set(key, value);
setGlobalState: async (_e, key: string, value: any, sourceId?: string) => {
const rev = nextRev(globalStateRevisions, key);
globalStateStorage.set(key, value);
globalStateUpdates$.next({ [key]: { v: value, r: rev, s: sourceId } });
},
delGlobalState: async (_, key: string) => {
return globalStateStorage.del(key);
delGlobalState: async (_e, key: string, sourceId?: string) => {
const rev = nextRev(globalStateRevisions, key);
globalStateStorage.del(key);
globalStateUpdates$.next({ [key]: { v: undefined, r: rev, s: sourceId } });
},
clearGlobalState: async () => {
return globalStateStorage.clear();
clearGlobalState: async (_e, sourceId?: string) => {
globalStateRevisions.clear();
globalStateStorage.clear();
globalStateUpdates$.next({ '*': { v: undefined, r: 0, s: sourceId } });
},
setGlobalCache: async (_, key: string, value: any) => {
return globalCacheStorage.set(key, value);
setGlobalCache: async (_e, key: string, value: any, sourceId?: string) => {
const rev = nextRev(globalCacheRevisions, key);
globalCacheStorage.set(key, value);
globalCacheUpdates$.next({ [key]: { v: value, r: rev, s: sourceId } });
},
delGlobalCache: async (_, key: string) => {
return globalCacheStorage.del(key);
delGlobalCache: async (_e, key: string, sourceId?: string) => {
const rev = nextRev(globalCacheRevisions, key);
globalCacheStorage.del(key);
globalCacheUpdates$.next({ [key]: { v: undefined, r: rev, s: sourceId } });
},
clearGlobalCache: async () => {
return globalCacheStorage.clear();
clearGlobalCache: async (_e, sourceId?: string) => {
globalCacheRevisions.clear();
globalCacheStorage.clear();
globalCacheUpdates$.next({ '*': { v: undefined, r: 0, s: sourceId } });
},
} satisfies NamespaceHandlers;
@@ -1,5 +1,6 @@
import { join } from 'node:path';
import { I18n } from '@affine/i18n';
import {
app,
BrowserWindow,
@@ -822,42 +823,53 @@ export class WebContentViewsManager {
},
});
if (spellCheckSettings.enabled) {
view.webContents.on('context-menu', (_event, params) => {
const shouldShow =
params.misspelledWord && params.dictionarySuggestions.length > 0;
view.webContents.on('context-menu', (_event, params) => {
const menu = Menu.buildFromTemplate([
{
id: 'cut',
label: I18n['com.affine.context-menu.cut'](),
role: 'cut',
enabled: params.editFlags.canCut,
},
{
id: 'copy',
label: I18n['com.affine.context-menu.copy'](),
role: 'copy',
enabled: params.editFlags.canCopy,
},
{
id: 'paste',
label: I18n['com.affine.context-menu.paste'](),
role: 'paste',
enabled: params.editFlags.canPaste,
},
]);
if (!shouldShow) {
return;
}
const menu = new Menu();
// Add each spelling suggestion
for (const suggestion of params.dictionarySuggestions) {
menu.append(
new MenuItem({
label: suggestion,
click: () => view.webContents.replaceMisspelling(suggestion),
})
);
}
// Add each spelling suggestion
for (const suggestion of params.dictionarySuggestions) {
menu.append(
new MenuItem({
label: suggestion,
click: () => view.webContents.replaceMisspelling(suggestion),
})
);
}
// Allow users to add the misspelled word to the dictionary
if (params.misspelledWord) {
menu.append(
new MenuItem({
label: 'Add to dictionary', // TODO: i18n
click: () =>
view.webContents.session.addWordToSpellCheckerDictionary(
params.misspelledWord
),
})
);
}
// Allow users to add the misspelled word to the dictionary
if (params.misspelledWord) {
menu.append(
new MenuItem({
label: 'Add to dictionary', // TODO: i18n
click: () =>
view.webContents.session.addWordToSpellCheckerDictionary(
params.misspelledWord
),
})
);
}
menu.popup();
});
}
menu.popup();
});
this.webViewsMap$.next(this.tabViewsMap.set(viewId, view));
let unsub = () => {};
@@ -6,6 +6,7 @@ import {
AFFINE_EVENT_CHANNEL_NAME,
} from '../shared/type';
// Load persisted data from main process synchronously at preload time
const initialGlobalState = ipcRenderer.sendSync(
AFFINE_API_CHANNEL_NAME,
'sharedStorage:getAllGlobalState'
@@ -15,6 +16,9 @@ const initialGlobalCache = ipcRenderer.sendSync(
'sharedStorage:getAllGlobalCache'
);
// Unique id for this renderer instance, used to ignore self-originated broadcasts
const CLIENT_ID: string = Math.random().toString(36).slice(2);
function invokeWithCatch(key: string, ...args: any[]) {
ipcRenderer.invoke(AFFINE_API_CHANNEL_NAME, key, ...args).catch(err => {
console.error(`Failed to invoke ${key}`, err);
@@ -34,7 +38,23 @@ function createSharedStorageApi(
memory.setAll(init);
ipcRenderer.on(AFFINE_EVENT_CHANNEL_NAME, (_event, channel, updates) => {
if (channel === `sharedStorage:${event}`) {
for (const [key, value] of Object.entries(updates)) {
for (const [key, raw] of Object.entries(updates)) {
// support both legacy plain value and new { v, r, s } structure
let value: any;
let source: string | undefined;
if (raw && typeof raw === 'object' && 'v' in raw) {
value = (raw as any).v;
source = (raw as any).s;
} else {
value = raw;
}
// Ignore our own broadcasts
if (source && source === CLIENT_ID) {
continue;
}
if (value === undefined) {
memory.del(key);
} else {
@@ -47,11 +67,11 @@ function createSharedStorageApi(
return {
del(key: string) {
memory.del(key);
invokeWithCatch(`sharedStorage:${api.del}`, key);
invokeWithCatch(`sharedStorage:${api.del}`, key, CLIENT_ID);
},
clear() {
memory.clear();
invokeWithCatch(`sharedStorage:${api.clear}`);
invokeWithCatch(`sharedStorage:${api.clear}`, CLIENT_ID);
},
get<T>(key: string): T | undefined {
return memory.get(key);
@@ -61,7 +81,7 @@ function createSharedStorageApi(
},
set(key: string, value: unknown) {
memory.set(key, value);
invokeWithCatch(`sharedStorage:${api.set}`, key, value);
invokeWithCatch(`sharedStorage:${api.set}`, key, value, CLIENT_ID);
},
watch<T>(key: string, cb: (i: T | undefined) => void): () => void {
const subscription = memory.watch(key).subscribe(i => cb(i as T));
@@ -8,6 +8,7 @@
"include": ["./src"],
"references": [
{ "path": "../../../../tools/utils" },
{ "path": "../../i18n" },
{ "path": "../../native" },
{ "path": "../../../common/nbstore" },
{ "path": "../../../common/infra" }
@@ -27,15 +27,6 @@
"version" : "1.1.6"
}
},
{
"identity" : "litext",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/Litext",
"state" : {
"revision" : "c37f3ab5826659854311e20d6c3942d4905b00b6",
"version" : "0.5.0"
}
},
{
"identity" : "lrucache",
"kind" : "remoteSourceControl",
@@ -50,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/MarkdownView",
"state" : {
"revision" : "29a9da19d6dc21af4e629c423961b0f453ffe192",
"version" : "2.3.8"
"revision" : "446dba45be81c67d0717d19277367dcbe5b2fb12",
"version" : "3.1.9"
}
},
{
@@ -68,8 +59,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/Splash",
"state" : {
"revision" : "4d997712fe07f75695aacdf287aeb3b1f2c6ab88",
"version" : "0.17.0"
"revision" : "de9cde249fdb7a173a6e6b950ab18b11f6c2a557",
"version" : "0.18.0"
}
},
{
@@ -13,23 +13,5 @@ extension AFFiNEViewController: IntelligentsButtonDelegate {
// if it shows up then we are ready to go
let controller = IntelligentsController()
self.present(controller, animated: true)
// IntelligentContext.shared.webView = webView
// button.beginProgress()
// IntelligentContext.shared.preparePresent { result in
// DispatchQueue.main.async {
// button.stopProgress()
// switch result {
// case .success:
// case let .failure(failure):
// let alert = UIAlertController(
// title: "Error",
// message: failure.localizedDescription,
// preferredStyle: .alert
// )
// alert.addAction(UIAlertAction(title: "OK", style: .default))
// self.present(alert, animated: true)
// }
// }
// }
}
}
@@ -64,12 +64,7 @@ class AFFiNEViewController: CAPBridgeViewController {
switch result {
case .failure: break
case .success:
#if DEBUG
// only show the button in debug mode before we get done
self.presentIntelligentsButton()
#else
break
#endif
}
}
}
@@ -69,5 +69,10 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>
@@ -0,0 +1,54 @@
// @generated
// This file was automatically generated and should not be edited.
@_exported import ApolloAPI
public class ApplyDocUpdatesQuery: GraphQLQuery {
public static let operationName: String = "applyDocUpdates"
public static let operationDocument: ApolloAPI.OperationDocument = .init(
definition: .init(
#"query applyDocUpdates($workspaceId: String!, $docId: String!, $op: String!, $updates: String!) { applyDocUpdates( workspaceId: $workspaceId docId: $docId op: $op updates: $updates ) }"#
))
public var workspaceId: String
public var docId: String
public var op: String
public var updates: String
public init(
workspaceId: String,
docId: String,
op: String,
updates: String
) {
self.workspaceId = workspaceId
self.docId = docId
self.op = op
self.updates = updates
}
public var __variables: Variables? { [
"workspaceId": workspaceId,
"docId": docId,
"op": op,
"updates": updates
] }
public struct Data: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
public static var __selections: [ApolloAPI.Selection] { [
.field("applyDocUpdates", String.self, arguments: [
"workspaceId": .variable("workspaceId"),
"docId": .variable("docId"),
"op": .variable("op"),
"updates": .variable("updates")
]),
] }
/// Apply updates to a doc using LLM and return the merged markdown.
public var applyDocUpdates: String { __data["applyDocUpdates"] }
}
}
@@ -7,24 +7,28 @@ public class GetCopilotRecentSessionsQuery: GraphQLQuery {
public static let operationName: String = "getCopilotRecentSessions"
public static let operationDocument: ApolloAPI.OperationDocument = .init(
definition: .init(
#"query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename chats( pagination: { first: $limit } options: { fork: false, sessionOrder: desc, withMessages: true } ) { __typename ...PaginatedCopilotChats } } } }"#,
#"query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10, $offset: Int = 0) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename chats( pagination: { first: $limit, offset: $offset } options: { action: false, fork: false, sessionOrder: desc, withMessages: false } ) { __typename ...PaginatedCopilotChats } } } }"#,
fragments: [CopilotChatHistory.self, CopilotChatMessage.self, PaginatedCopilotChats.self]
))
public var workspaceId: String
public var limit: GraphQLNullable<Int>
public var offset: GraphQLNullable<Int>
public init(
workspaceId: String,
limit: GraphQLNullable<Int> = 10
limit: GraphQLNullable<Int> = 10,
offset: GraphQLNullable<Int> = 0
) {
self.workspaceId = workspaceId
self.limit = limit
self.offset = offset
}
public var __variables: Variables? { [
"workspaceId": workspaceId,
"limit": limit
"limit": limit,
"offset": offset
] }
public struct Data: AffineGraphQL.SelectionSet {
@@ -65,11 +69,15 @@ public class GetCopilotRecentSessionsQuery: GraphQLQuery {
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("chats", Chats.self, arguments: [
"pagination": ["first": .variable("limit")],
"pagination": [
"first": .variable("limit"),
"offset": .variable("offset")
],
"options": [
"action": false,
"fork": false,
"sessionOrder": "desc",
"withMessages": true
"withMessages": false
]
]),
] }
@@ -7,7 +7,7 @@ public class GetWorkspacePageByIdQuery: GraphQLQuery {
public static let operationName: String = "getWorkspacePageById"
public static let operationDocument: ApolloAPI.OperationDocument = .init(
definition: .init(
#"query getWorkspacePageById($workspaceId: String!, $pageId: String!) { workspace(id: $workspaceId) { __typename doc(docId: $pageId) { __typename id mode defaultRole public } } }"#
#"query getWorkspacePageById($workspaceId: String!, $pageId: String!) { workspace(id: $workspaceId) { __typename doc(docId: $pageId) { __typename id mode defaultRole public title summary } } }"#
))
public var workspaceId: String
@@ -68,12 +68,16 @@ public class GetWorkspacePageByIdQuery: GraphQLQuery {
.field("mode", GraphQLEnum<AffineGraphQL.PublicDocMode>.self),
.field("defaultRole", GraphQLEnum<AffineGraphQL.DocRole>.self),
.field("public", Bool.self),
.field("title", String?.self),
.field("summary", String?.self),
] }
public var id: String { __data["id"] }
public var mode: GraphQLEnum<AffineGraphQL.PublicDocMode> { __data["mode"] }
public var defaultRole: GraphQLEnum<AffineGraphQL.DocRole> { __data["defaultRole"] }
public var `public`: Bool { __data["public"] }
public var title: String? { __data["title"] }
public var summary: String? { __data["summary"] }
}
}
}
@@ -12,6 +12,7 @@ public struct CreateChatMessageInput: InputObject {
public init(
attachments: GraphQLNullable<[String]> = nil,
blob: GraphQLNullable<Upload> = nil,
blobs: GraphQLNullable<[Upload]> = nil,
content: GraphQLNullable<String> = nil,
params: GraphQLNullable<JSON> = nil,
@@ -19,6 +20,7 @@ public struct CreateChatMessageInput: InputObject {
) {
__data = InputDict([
"attachments": attachments,
"blob": blob,
"blobs": blobs,
"content": content,
"params": params,
@@ -31,6 +33,11 @@ public struct CreateChatMessageInput: InputObject {
set { __data["attachments"] = newValue }
}
public var blob: GraphQLNullable<Upload> {
get { __data["blob"] }
set { __data["blob"] = newValue }
}
public var blobs: GraphQLNullable<[Upload]> {
get { __data["blobs"] }
set { __data["blobs"] = newValue }
@@ -21,7 +21,7 @@ let package = Package(
.package(url: "https://github.com/SwifterSwift/SwifterSwift.git", from: "6.0.0"),
.package(url: "https://github.com/Recouse/EventSource", from: "0.1.4"),
.package(url: "https://github.com/Lakr233/ListViewKit", from: "1.1.6"),
.package(url: "https://github.com/Lakr233/MarkdownView", exact: "2.3.8"),
.package(url: "https://github.com/Lakr233/MarkdownView", from: "3.1.9"),
],
targets: [
.target(name: "Intelligents", dependencies: [

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