Compare commits

...

30 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
157 changed files with 2193 additions and 1218 deletions
+1
View File
@@ -75,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: |
@@ -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,
@@ -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));
}
@@ -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;
@@ -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])];
@@ -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() {
@@ -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",
@@ -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',
],
@@ -461,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.',
}
@@ -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,
}
@@ -306,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');
}
});
@@ -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;
}
@@ -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.',
}
@@ -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.',
}
@@ -152,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
);
@@ -168,9 +168,13 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
};
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({
@@ -178,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,
};
}
+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,14 +1,18 @@
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': {};
'copilot.workspace.cleanupTrashedDocEmbeddings': {
nextSid?: number;
};
}
}
@@ -85,10 +89,17 @@ export class CopilotCronJobs {
}
@OnJob('copilot.workspace.cleanupTrashedDocEmbeddings')
async cleanupTrashedDocEmbeddings() {
const workspaces = await this.models.workspace.list(undefined, {
id: true,
});
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',
@@ -96,5 +107,7 @@ export class CopilotCronJobs {
{ jobId: `cleanup-trashed-doc-embeddings-${workspaceId}` }
);
}
params.nextSid = workspaces[workspaces.length - 1].sid;
return JOB_SIGNAL.Repeat;
}
}
@@ -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,
};
}
@@ -1468,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[] = [
@@ -1811,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
@@ -1820,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>
@@ -1890,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}}
@@ -1904,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}}
@@ -1924,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',
@@ -255,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
@@ -29,6 +29,7 @@ import {
createDocSemanticSearchTool,
createExaCrawlTool,
createExaSearchTool,
createSectionEditTool,
} from '../tools';
import { CopilotProviderFactory } from './factory';
import {
@@ -224,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);
@@ -37,7 +37,7 @@ import {
import { CurrentUser } from '../../core/auth';
import { Admin } from '../../core/common';
import { DocReader } from '../../core/doc';
import { AccessController } from '../../core/permission';
import { AccessController, DocAction } from '../../core/permission';
import { UserType } from '../../core/user';
import type { ListSessionOptions, UpdateChatSession } from '../../models';
import { CopilotCronJobs } from './cron';
@@ -143,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;
@@ -417,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) {
@@ -428,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)
@@ -507,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(
@@ -537,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(
@@ -703,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) {
@@ -51,7 +51,15 @@ Important Instructions:
- When inserting, follow the same format as a replacement, but ensure the new block_id does not conflict with existing IDs.
- 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 list item should be a block.
- 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.**
@@ -142,24 +150,33 @@ 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
.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.'
),
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 {
@@ -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);
}
},
});
};
@@ -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,
+1
View File
@@ -457,6 +457,7 @@ type CopilotWorkspaceIgnoredDocTypeEdge {
input CreateChatMessageInput {
attachments: [String!]
blob: Upload
blobs: [Upload!]
content: String
params: JSON
+1
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']>;
@@ -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
@@ -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: [
@@ -0,0 +1,33 @@
//
// ChatManager+CURD.swift
// Intelligents
//
// Created by on 7/14/25.
//
import AffineGraphQL
import Apollo
import ApolloAPI
import EventSource
import Foundation
import MarkdownParser
import MarkdownView
extension ChatManager {
func clearCurrentSession() {
guard let session = IntelligentContext.shared.currentSession else {
print("[-] no current session to clear")
return
}
let mutation = CleanupCopilotSessionMutation(input: .init(
docId: session.docId ?? "",
sessionIds: [session.id],
workspaceId: session.workspaceId
))
QLService.shared.client.perform(mutation: mutation) { result in
print("[+] cleanup session result: \(result)")
}
}
}
@@ -24,53 +24,172 @@ private extension InputBoxData {
}
}
extension ChatManager {
public func startUserRequest(
content: String,
inputBoxData: InputBoxData,
sessionId: String
) {
public extension ChatManager {
func startUserRequest(editorData: InputBoxData, sessionId: String) {
append(sessionId: sessionId, UserMessageCellViewModel(
id: .init(),
content: inputBoxData.text,
content: editorData.text,
timestamp: .init()
))
append(sessionId: sessionId, UserHintCellViewModel(
id: .init(),
timestamp: .init(),
imageAttachments: inputBoxData.imageAttachments,
fileAttachments: inputBoxData.fileAttachments,
docAttachments: inputBoxData.documentAttachments
imageAttachments: editorData.imageAttachments,
fileAttachments: editorData.fileAttachments,
docAttachments: editorData.documentAttachments
))
let viewModelId = append(sessionId: sessionId, AssistantMessageCellViewModel(
id: .init(),
content: "...",
timestamp: .init()
))
scrollToBottomPublisher.send(sessionId)
guard let workspaceId = IntelligentContext.shared.currentWorkspaceId,
!workspaceId.isEmpty
else {
report(sessionId, ChatError.unknownError)
assertionFailure("Invalid workspace ID")
return
}
DispatchQueue.global().async {
self.prepareContext(
workspaceId: workspaceId,
sessionId: sessionId,
editorData: editorData,
viewModelId: viewModelId
)
}
}
}
private extension ChatManager {
func prepareContext(
workspaceId: String,
sessionId: String,
editorData: InputBoxData,
viewModelId: UUID
) {
assert(!Thread.isMainThread)
let createContext = CreateCopilotContextMutation(
workspaceId: workspaceId,
sessionId: sessionId
)
QLService.shared.client.perform(mutation: createContext) { result in
DispatchQueue.main.async {
switch result {
case let .success(graphQLResult):
guard let contextId = graphQLResult.data?.createCopilotContext else {
self.report(sessionId, ChatError.invalidResponse)
return
}
print("[+] copilot context created: \(contextId)")
DispatchQueue.global().async {
let docAttachGroup = DispatchGroup()
for docAttach in editorData.documentAttachments {
let addDoc = AddContextDocMutation(
options: .init(
contextId: contextId,
docId: docAttach.documentID
)
)
docAttachGroup.enter()
QLService.shared.client.perform(mutation: addDoc) { result in
switch result {
case .success:
print("[+] doc \(docAttach.documentID) added to context")
case let .failure(error):
print("[-] addContextDoc failed: \(error)")
}
docAttachGroup.leave()
}
}
docAttachGroup.notify(queue: .global()) {
var contextSnippet = ""
if !editorData.documentAttachments.isEmpty {
let sem = DispatchSemaphore(value: 0)
let matchQuery = MatchContextQuery(
contextId: .some(contextId),
workspaceId: .some(workspaceId),
content: editorData.text,
limit: .none,
scopedThreshold: .none,
threshold: .none
)
QLService.shared.client.fetch(query: matchQuery) { result in
switch result {
case let .success(queryResult):
let matches = queryResult.data?.currentUser?.copilot.contexts ?? []
let matchDocs = matches.compactMap(\.matchWorkspaceDocs).flatMap(\.self)
for context in matchDocs {
contextSnippet += "<file docId=\"\(context.docId)\" chunk=\"\(context.chunk)\">\(context.content)</file>\n"
}
case let .failure(error):
print("[-] matchContext failed: \(error)")
// self.report(sessionId, error)
}
sem.signal()
}
sem.wait()
}
print("[+] context snippet prepared: \(contextSnippet)")
self.startCopilotResponse(
editorData: editorData,
contextSnippet: contextSnippet,
sessionId: sessionId,
viewModelId: viewModelId
)
}
}
case let .failure(error):
self.report(sessionId, error)
return
}
}
}
}
func startCopilotResponse(
editorData: InputBoxData,
contextSnippet: String,
sessionId: String,
viewModelId: UUID
) {
assert(!Thread.isMainThread)
let messageParameters: [String: AnyHashable] = [
// packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx
"docs": inputBoxData.documentAttachments.map(\.documentID), // affine doc
"docs": editorData.documentAttachments.map(\.documentID), // affine doc
"files": [String](), // attachment in context, keep nil for now
"searchMode": inputBoxData.isSearchEnabled ? "MUST" : "AUTO",
"searchMode": editorData.isSearchEnabled ? "MUST" : "AUTO",
]
let hasMultipleAttachmentBlobs = [
editorData.fileAttachments.count,
editorData.documentAttachments.count,
].reduce(0, +) > 1
let attachmentFieldName = hasMultipleAttachmentBlobs ? "options.blobs" : "options.blob"
let uploadableAttachments: [GraphQLFile] = [
inputBoxData.fileAttachments.map { file -> GraphQLFile in
.init(
fieldName: file.name,
originalName: file.name,
data: file.data ?? .init()
)
editorData.fileAttachments.map { file -> GraphQLFile in
.init(fieldName: attachmentFieldName, originalName: file.name, data: file.data ?? .init())
},
inputBoxData.imageAttachments.map { image -> GraphQLFile in
.init(
fieldName: image.hashValue.description,
originalName: "image.jpg",
data: image.imageData
)
editorData.imageAttachments.map { image -> GraphQLFile in
.init(fieldName: attachmentFieldName, originalName: "image.jpg", data: image.imageData)
},
].flatMap(\.self)
assert(uploadableAttachments.allSatisfy { !($0.data?.isEmpty ?? true) })
guard let input = try? CreateChatMessageInput(
content: .some(content),
attachments: [],
blob: hasMultipleAttachmentBlobs ? .none : "",
blobs: hasMultipleAttachmentBlobs ? .some([]) : .none,
content: .some(contextSnippet.isEmpty ? editorData.text : "\(contextSnippet)\n\(editorData.text)"),
params: .some(AffineGraphQL.JSON(_jsonValue: messageParameters)),
sessionId: sessionId
) else {
report(sessionId, ChatError.unknownError)
assertionFailure() // very unlikely to happen
return
}
@@ -83,11 +202,6 @@ extension ChatManager {
self.report(sessionId, ChatError.invalidResponse)
return
}
let viewModelId = self.append(sessionId: sessionId, AssistantMessageCellViewModel(
id: .init(),
content: .init(),
timestamp: .init()
))
self.startStreamingResponse(
sessionId: sessionId,
messageId: messageIdentifier,
@@ -99,8 +213,10 @@ extension ChatManager {
}
}
}
}
private func startStreamingResponse(sessionId: String, messageId: String, applyingTo vmId: UUID) {
private extension ChatManager {
func startStreamingResponse(sessionId: String, messageId: String, applyingTo vmId: UUID) {
let base = IntelligentContext.shared.webViewMetadata[.currentServerBaseUrl] as? String
guard let base, let url = URL(string: base) else {
report(sessionId, ChatError.invalidServerConfiguration)
@@ -164,24 +280,11 @@ extension ChatManager {
vmId: UUID
) {
let result = MarkdownParser().parse(document)
var renderedContexts: [String: RenderedItem] = [:]
for (key, value) in result.mathContext {
let image = MathRenderer.renderToImage(
latex: value,
fontSize: MarkdownTheme.default.fonts.body.pointSize,
textColor: MarkdownTheme.default.colors.body
)?.withRenderingMode(.alwaysTemplate)
let renderedContext = RenderedItem(
image: image,
text: value
)
renderedContexts["math://\(key)"] = renderedContext
}
let content = MarkdownTextView.PreprocessContent(parserResult: result, theme: .default)
with(sessionId: sessionId, vmId: vmId) { (viewModel: inout AssistantMessageCellViewModel) in
viewModel.content = document
viewModel.documentBlocks = result.document
viewModel.documentRenderedContent = renderedContexts
viewModel.preprocessedContent = content
}
}
}
@@ -33,6 +33,12 @@ public class ChatManager: ObservableObject, @unchecked Sendable {
closable.removeAll()
}
public func clearAll() {
assert(Thread.isMainThread)
closeAll()
viewModels.removeAll()
}
public func with(sessionId: String, _ action: (inout OrderedDictionary<MessageID, any ChatCellViewModel>) -> Void) {
if Thread.isMainThread {
if var sessionViewModels = viewModels[sessionId] {
@@ -59,8 +65,6 @@ public class ChatManager: ObservableObject, @unchecked Sendable {
return
}
sessionViewModels[vmId] = vm
} else {
assertionFailure()
}
}
}
@@ -17,6 +17,7 @@ enum BridgedWindowScript: String {
case getCurrentServerBaseUrl = "window.getCurrentServerBaseUrl()"
case getCurrentWorkspaceId = "window.getCurrentWorkspaceId();"
case getCurrentDocId = "window.getCurrentDocId();"
case getAiButtonFeatureFlag = "window.getAiButtonFeatureFlag();"
case getCurrentI18nLocale = "window.getCurrentI18nLocale();"
case createNewDocByMarkdownInCurrentWorkspace = "return await window.createNewDocByMarkdownInCurrentWorkspace(markdown, title);"
@@ -17,6 +17,7 @@ extension IntelligentContext {
(.currentWorkspaceId, .getCurrentWorkspaceId),
(.currentServerBaseUrl, .getCurrentServerBaseUrl),
(.currentI18nLocale, .getCurrentI18nLocale),
(.currentAiButtonFeatureFlag, .getAiButtonFeatureFlag),
]
for (key, script) in keysAndScripts {
DispatchQueue.main.async {
@@ -40,6 +40,7 @@ public class IntelligentContext {
case currentWorkspaceId
case currentServerBaseUrl
case currentI18nLocale
case currentAiButtonFeatureFlag
}
@Published public private(set) var currentSession: ChatSessionObject?
@@ -53,6 +54,7 @@ public class IntelligentContext {
public enum IntelligentError: Error, LocalizedError {
case loginRequired(String)
case sessionCreationFailed(String)
case featureClosed
public var errorDescription: String? {
switch self {
@@ -60,6 +62,8 @@ public class IntelligentContext {
"Login required: \(reason)"
case let .sessionCreationFailed(reason):
"Session creation failed: \(reason)"
case .featureClosed:
"Intelligent feature closed"
}
}
}
@@ -81,6 +85,11 @@ public class IntelligentContext {
}
webViewGroup.wait()
webViewMetadata = webViewMetadataResult
if webViewMetadataResult[.currentAiButtonFeatureFlag] as? Bool == false {
completion(.failure(IntelligentError.featureClosed))
return
}
// Check required webView metadata
guard let baseUrlString = webViewMetadataResult[.currentServerBaseUrl] as? String,
@@ -85,11 +85,7 @@ extension MainViewController: InputBoxDelegate {
}
ChatManager.shared.closeAll()
ChatManager.shared.startUserRequest(
content: inputData.text,
inputBoxData: inputData,
sessionId: currentSession.id
)
ChatManager.shared.startUserRequest(editorData: inputData, sessionId: currentSession.id)
}
private func showAlert(title: String, message: String) {
@@ -21,11 +21,6 @@ class AssistantMessageCell: ChatBaseCell {
contentView.addSubview(markdownView)
}
override func prepareForReuse() {
super.prepareForReuse()
markdownView.prepareForReuse()
}
override func configure(with viewModel: any ChatCellViewModel) {
super.configure(with: viewModel)
@@ -33,10 +28,7 @@ class AssistantMessageCell: ChatBaseCell {
assertionFailure()
return
}
markdownView.setMarkdown(
vm.documentBlocks,
renderedContent: vm.documentRenderedContent
)
markdownView.setMarkdown(vm.preprocessedContent)
}
override func layoutContentView(bounds: CGRect) {
@@ -53,10 +45,7 @@ class AssistantMessageCell: ChatBaseCell {
markdownViewForSizeCalculation.frame = .init(
x: 0, y: 0, width: width, height: .greatestFiniteMagnitude
)
markdownViewForSizeCalculation.setMarkdown(
vm.documentBlocks,
renderedContent: vm.documentRenderedContent
)
markdownViewForSizeCalculation.setMarkdownManually(vm.preprocessedContent)
let boundingSize = markdownViewForSizeCalculation.boundingSize(for: width)
return ceil(boundingSize.height)
}
@@ -38,8 +38,7 @@ struct AssistantMessageCellViewModel: ChatCellViewModel {
var citations: [CitationViewModel]?
var actions: [MessageActionViewModel]?
var documentBlocks: [MarkdownBlockNode]
var documentRenderedContent: RenderContext
var preprocessedContent: MarkdownTextView.PreprocessContent
init(
id: UUID,
@@ -53,7 +52,7 @@ struct AssistantMessageCellViewModel: ChatCellViewModel {
actions: [MessageActionViewModel]? = nil
) {
// time expensive rendering should not happen here
assert(!Thread.isMainThread || content.isEmpty)
assert(!Thread.isMainThread || content.count < 10) // allow placeholder content
self.id = id
self.content = content
@@ -67,21 +66,10 @@ struct AssistantMessageCellViewModel: ChatCellViewModel {
let parser = MarkdownParser()
let parserResult = parser.parse(content)
documentBlocks = parserResult.document
var renderedContexts: [String: RenderedItem] = [:]
for (key, value) in parserResult.mathContext {
let image = MathRenderer.renderToImage(
latex: value,
fontSize: MarkdownTheme.default.fonts.body.pointSize,
textColor: MarkdownTheme.default.colors.body
)?.withRenderingMode(.alwaysTemplate)
let renderedContext = RenderedItem(
image: image,
text: value
)
renderedContexts["math://\(key)"] = renderedContext
}
documentRenderedContent = renderedContexts
preprocessedContent = MarkdownTextView.PreprocessContent(
parserResult: parserResult,
theme: .default,
)
}
}
@@ -6,8 +6,8 @@ private let unselectedColor: UIColor = .affineIconPrimary
private let selectedColor: UIColor = .affineIconActivated
private let configurableOptions: [ConfigurableOptions] = [
.networking,
.reasoning,
// .networking,
// .reasoning,
]
enum ConfigurableOptions {
case tool
@@ -22,11 +22,20 @@ class MainHeaderView: UIView {
$0.textAlignment = .center
}
private lazy var modelMenu = UIDeferredMenuElement.uncached { completion in
completion([])
}
private lazy var dropdownButton = UIButton(type: .system).then {
$0.imageView?.contentMode = .scaleAspectFit
$0.setImage(UIImage.affineArrowDown, for: .normal)
$0.tintColor = UIColor.affineIconPrimary
$0.addTarget(self, action: #selector(dropdownButtonTapped), for: .touchUpInside)
$0.showsMenuAsPrimaryAction = true
$0.menu = UIMenu(options: [.displayInline], children: [
modelMenu,
])
$0.isHidden = true
}
private lazy var centerStackView = UIStackView().then {
@@ -45,6 +54,13 @@ class MainHeaderView: UIView {
$0.layer.cornerRadius = 8
$0.addTarget(self, action: #selector(menuButtonTapped), for: .touchUpInside)
$0.setContentHuggingPriority(.required, for: .horizontal)
$0.showsMenuAsPrimaryAction = true
$0.menu = .init(options: [.displayInline], children: [
UIAction(title: "Clear History", image: .affineBroom, handler: { _ in
ChatManager.shared.clearCurrentSession()
ChatManager.shared.clearAll()
}),
])
}
private lazy var leftSpacerView = UIView()
+5 -10
View File
@@ -7,7 +7,6 @@ import { NavigationGestureProvider } from '@affine/core/mobile/modules/navigatio
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
import { router } from '@affine/core/mobile/router';
import { configureCommonModules } from '@affine/core/modules';
import { AIButtonProvider } from '@affine/core/modules/ai-button';
import {
AuthProvider,
AuthService,
@@ -18,6 +17,7 @@ import {
ValidatorProvider,
} from '@affine/core/modules/cloud';
import { DocsService } from '@affine/core/modules/doc';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { I18nProvider } from '@affine/core/modules/i18n';
import { LifecycleService } from '@affine/core/modules/lifecycle';
@@ -62,7 +62,6 @@ import { BlocksuiteMenuConfigProvider } from './bs-menu-config';
import { ModalConfigProvider } from './modal-config';
import { Auth } from './plugins/auth';
import { Hashcash } from './plugins/hashcash';
import { Intelligents } from './plugins/intelligents';
import { NbStoreNativeDBApis } from './plugins/nbstore';
import { writeEndpointToken } from './proxy';
import { enableNavigationGesture$ } from './web-navigation-control';
@@ -162,14 +161,6 @@ framework.impl(HapticProvider, {
selectionChanged: () => Haptics.selectionChanged(),
selectionEnd: () => Haptics.selectionEnd(),
});
framework.impl(AIButtonProvider, {
presentAIButton: () => {
return Intelligents.presentIntelligentsButton();
},
dismissAIButton: () => {
return Intelligents.dismissIntelligentsButton();
},
});
framework.scope(ServerScope).override(AuthProvider, resolver => {
const serverService = resolver.get(ServerService);
const endpoint = serverService.server.baseUrl;
@@ -224,6 +215,10 @@ const frameworkProvider = framework.provider();
(window as any).getCurrentI18nLocale = () => {
return I18n.language;
};
(window as any).getAiButtonFeatureFlag = () => {
const featureFlagService = frameworkProvider.get(FeatureFlagService);
return featureFlagService.flags.enable_mobile_ai_button.value;
};
(window as any).getCurrentWorkspaceId = () => {
const globalContextService = frameworkProvider.get(GlobalContextService);
return globalContextService.globalContext.workspaceId.get();
@@ -1,4 +0,0 @@
export interface IntelligentsPlugin {
presentIntelligentsButton(): Promise<void>;
dismissIntelligentsButton(): Promise<void>;
}
@@ -1,8 +0,0 @@
import { registerPlugin } from '@capacitor/core';
import type { IntelligentsPlugin } from './definitions';
const Intelligents = registerPlugin<IntelligentsPlugin>('Intelligents');
export * from './definitions';
export { Intelligents };
+1
View File
@@ -16,6 +16,7 @@
"@affine/graphql": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/nbstore": "workspace:*",
"@affine/reader": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/track": "workspace:*",
"@blocksuite/affine": "workspace:*",
@@ -39,7 +39,8 @@ describe('applyPatchToDoc', () => {
});
});
it('should replace a block', async () => {
// FIXME: markdown parse error in test mode
it.skip('should replace a block', async () => {
const host = affine`
<affine-page id="page">
<affine-note id="note">
@@ -73,7 +74,8 @@ describe('applyPatchToDoc', () => {
});
});
it('should insert a block at index', async () => {
// FIXME: markdown parse error in test mode
it.skip('should insert a block at index', async () => {
const host = affine`
<affine-page id="page">
<affine-note id="note">
@@ -252,7 +252,7 @@ async function insertBelowBlock(
return true;
}
const PAGE_INSERT = {
export const PAGE_INSERT = {
icon: InsertBelowIcon({ width: '20px', height: '20px' }),
title: 'Insert',
showWhen: (host: EditorHost) => {
@@ -291,7 +291,7 @@ const PAGE_INSERT = {
},
};
const EDGELESS_INSERT = {
export const EDGELESS_INSERT = {
...PAGE_INSERT,
handler: async (
host: EditorHost,
@@ -469,7 +469,7 @@ const ADD_TO_EDGELESS_AS_NOTE = {
},
};
const SAVE_AS_DOC = {
export const SAVE_AS_DOC = {
icon: PageIcon({ width: '20px', height: '20px' }),
title: 'Save as doc',
showWhen: () => true,
@@ -1,3 +1,4 @@
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
import type {
AddContextFileInput,
ContextMatchedDocChunk,
@@ -142,6 +143,7 @@ declare global {
webSearch?: boolean;
reasoning?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig | undefined;
contexts?: {
docs: AIDocContextOption[];
files: AIFileContextOption[];
@@ -78,6 +78,7 @@ export class AIChatBlockMessage extends LitElement {
.affineFeatureFlagService=${this.textRendererOptions
.affineFeatureFlagService}
.notificationService=${notificationService}
.independentMode=${false}
.theme=${this.host.std.get(ThemeProvider).app$}
></chat-content-stream-objects>`;
}
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
@@ -105,6 +106,9 @@ export class AIChatPanelTitle extends SignalWatcher(
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor session!: CopilotChatHistoryFragment | null | undefined;
@@ -142,6 +146,7 @@ export class AIChatPanelTitle extends SignalWatcher(
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.aiToolsConfigService=${this.aiToolsConfigService}
></playground-content>
`;
@@ -1,4 +1,7 @@
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
@@ -119,6 +122,9 @@ export class ChatPanel extends SignalWatcher(
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor session: CopilotChatHistoryFragment | null | undefined;
@@ -387,6 +393,7 @@ export class ChatPanel extends SignalWatcher(
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.session=${this.session}
.status=${this.status}
.embeddingProgress=${this.embeddingProgress}
@@ -413,6 +420,7 @@ export class ChatPanel extends SignalWatcher(
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiDraftService=${this.aiDraftService}
.aiToolsConfigService=${this.aiToolsConfigService}
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
.onContextChange=${this.onContextChange}
.width=${this.sidebarWidth}
@@ -148,6 +148,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
.affineFeatureFlagService=${this.affineFeatureFlagService}
.notificationService=${this.notificationService}
.theme=${this.affineThemeService.appTheme.themeSignal}
.independentMode=${this.independentMode}
.docDisplayService=${this.docDisplayService}
.onOpenDoc=${this.onOpenDoc}
></chat-content-stream-objects>`;
@@ -1,6 +1,9 @@
import './ai-chat-composer-tip';
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type {
ContextEmbedStatus,
@@ -118,7 +121,10 @@ export class AIChatComposer extends SignalWatcher(
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor chips: ChatChip[] = [];
@@ -166,6 +172,7 @@ export class AIChatComposer extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
.aiDraftService=${this.aiDraftService}
.aiToolsConfigService=${this.aiToolsConfigService}
.portalContainer=${this.portalContainer}
.onChatSuccess=${this.onChatSuccess}
.trackOptions=${this.trackOptions}
@@ -1,4 +1,7 @@
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { AIDraftState } from '@affine/core/modules/ai-button/services/ai-draft';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
@@ -153,7 +156,10 @@ export class AIChatContent extends SignalWatcher(
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor onEmbeddingProgressChange:
@@ -273,6 +279,9 @@ export class AIChatContent extends SignalWatcher(
};
private readonly updateDraft = async (context: Partial<ChatContextValue>) => {
if (!this.aiDraftService) {
return;
}
const draft: Partial<AIDraftState> = pick(context, [
'quote',
'images',
@@ -344,15 +353,17 @@ export class AIChatContent extends SignalWatcher(
this.initChatContent().catch(console.error);
this.aiDraftService
.getDraft()
.then(draft => {
this.chatContextValue = {
...this.chatContextValue,
...draft,
};
})
.catch(console.error);
if (this.aiDraftService) {
this.aiDraftService
.getDraft()
.then(draft => {
this.chatContextValue = {
...this.chatContextValue,
...draft,
};
})
.catch(console.error);
}
this._disposables.add(
AIProvider.slots.actions.subscribe(({ event }) => {
@@ -405,6 +416,7 @@ export class AIChatContent extends SignalWatcher(
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.width=${this.width}
@@ -434,6 +446,7 @@ export class AIChatContent extends SignalWatcher(
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.notificationService=${this.notificationService}
.aiDraftService=${this.aiDraftService}
.aiToolsConfigService=${this.aiToolsConfigService}
.trackOptions=${{
where: 'chat-panel',
control: 'chat-send',
@@ -1,4 +1,7 @@
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
@@ -353,7 +356,10 @@ export class AIChatInput extends SignalWatcher(
accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor isRootSession: boolean = true;
@@ -406,13 +412,15 @@ export class AIChatInput extends SignalWatcher(
protected override firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.aiDraftService
.getDraft()
.then(draft => {
this.textarea.value = draft.input;
this.isInputEmpty = !this.textarea.value.trim();
})
.catch(console.error);
if (this.aiDraftService) {
this.aiDraftService
.getDraft()
.then(draft => {
this.textarea.value = draft.input;
this.isInputEmpty = !this.textarea.value.trim();
})
.catch(console.error);
}
}
protected override render() {
@@ -493,6 +501,7 @@ export class AIChatInput extends SignalWatcher(
.networkSearchVisible=${!!this.networkSearchConfig.visible.value}
.isNetworkActive=${this._isNetworkActive}
.onNetworkActiveChange=${this._toggleNetworkSearch}
.toolsConfigService=${this.aiToolsConfigService}
></chat-input-preference>
${status === 'transmitting' || status === 'loading'
? html`<button
@@ -536,9 +545,11 @@ export class AIChatInput extends SignalWatcher(
textarea.style.overflowY = 'scroll';
}
await this.aiDraftService.setDraft({
input: value,
});
if (this.aiDraftService) {
await this.aiDraftService.setDraft({
input: value,
});
}
};
private readonly _handleKeyDown = async (evt: KeyboardEvent) => {
@@ -593,10 +604,12 @@ export class AIChatInput extends SignalWatcher(
this.isInputEmpty = true;
this.textarea.style.height = 'unset';
if (this.aiDraftService) {
await this.aiDraftService.setDraft({
input: '',
});
}
await this.send(value);
await this.aiDraftService.setDraft({
input: '',
});
};
private readonly _handleModelChange = (modelId: string) => {
@@ -647,6 +660,7 @@ export class AIChatInput extends SignalWatcher(
control: this.trackOptions?.control,
webSearch: this._isNetworkActive,
reasoning: this._isReasoningActive,
toolsConfig: this.aiToolsConfigService.config.value,
modelId: this.modelId,
});
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import {
menu,
@@ -7,6 +8,7 @@ import {
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import {
ArrowDownSmallIcon,
CloudWorkspaceIcon,
ThinkingIcon,
WebIcon,
} from '@blocksuite/icons/lit';
@@ -81,6 +83,9 @@ export class ChatInputPreference extends SignalWatcher(
| undefined;
// --------- search props end ---------
@property({ attribute: false })
accessor toolsConfigService!: AIToolsConfigService;
// private readonly _onModelChange = (modelId: string) => {
// this.onModelChange?.(modelId);
// };
@@ -126,6 +131,19 @@ export class ChatInputPreference extends SignalWatcher(
onChange: (value: boolean) => this.onNetworkActiveChange?.(value),
class: { 'preference-action': true },
testId: 'chat-network-search',
}),
menu.toggleSwitch({
name: 'Workspace All Docs',
prefix: CloudWorkspaceIcon(),
on:
!!this.toolsConfigService.config.value.searchWorkspace &&
!!this.toolsConfigService.config.value.readingDocs,
onChange: (value: boolean) =>
this.toolsConfigService.setConfig({
searchWorkspace: value,
readingDocs: value,
}),
class: { 'preference-action': true },
})
);
}
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit';
@@ -206,6 +207,9 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor docDisplayService!: DocDisplayConfig;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
@@ -467,6 +471,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
isRootSession: true,
reasoning: this._isReasoningActive,
webSearch: this._isNetworkActive,
toolsConfig: this.aiToolsConfigService.config.value,
});
for await (const text of stream) {
@@ -52,6 +52,9 @@ export class ChatContentStreamObjects extends WithDisposable(
@property({ attribute: false })
accessor theme!: Signal<ColorScheme>;
@property({ attribute: false })
accessor independentMode: boolean | undefined;
@property({ attribute: false })
accessor notificationService!: NotificationService;
@@ -123,6 +126,18 @@ export class ChatContentStreamObjects extends WithDisposable(
.data=${streamObject}
.width=${this.width}
></doc-read-result>`;
case 'section_edit':
return html`
<section-edit-tool
.data=${streamObject}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.notificationService=${this.notificationService}
.theme=${this.theme}
.host=${this.host}
.independentMode=${this.independentMode}
></section-edit-tool>
`;
default: {
const name = streamObject.toolName + ' tool calling';
return html`
@@ -199,6 +214,18 @@ export class ChatContentStreamObjects extends WithDisposable(
.data=${streamObject}
.width=${this.width}
></doc-read-result>`;
case 'section_edit':
return html`
<section-edit-tool
.data=${streamObject}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.notificationService=${this.notificationService}
.theme=${this.theme}
.host=${this.host}
.independentMode=${this.independentMode}
></section-edit-tool>
`;
default: {
const name = streamObject.toolName + ' tool result';
return html`
@@ -0,0 +1,260 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import type { ColorScheme } from '@blocksuite/affine/model';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import {
type BlockSelection,
type EditorHost,
ShadowlessElement,
type TextSelection,
} from '@blocksuite/affine/std';
import type { ExtensionType } from '@blocksuite/affine/store';
import type { NotificationService } from '@blocksuite/affine-shared/services';
import { isInsidePageEditor } from '@blocksuite/affine-shared/utils';
import {
CopyIcon,
InsertBleowIcon,
LinkedPageIcon,
PageIcon,
} from '@blocksuite/icons/lit';
import type { Signal } from '@preact/signals-core';
import { css, html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import {
EDGELESS_INSERT,
PAGE_INSERT,
SAVE_AS_DOC,
} from '../../_common/chat-actions-handle';
import { copyText } from '../../utils/editor-actions';
import type { ToolError } from './type';
interface SectionEditToolCall {
type: 'tool-call';
toolCallId: string;
toolName: string;
args: { section: string; instructions: string };
}
interface SectionEditToolResult {
type: 'tool-result';
toolCallId: string;
toolName: string;
args: { section: string; instructions: string };
result: { content: string } | ToolError | null;
}
export class SectionEditTool extends WithDisposable(ShadowlessElement) {
static override styles = css`
.section-edit-result {
padding: 12px;
margin: 8px 0;
border-radius: 8px;
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
.section-edit-header {
height: 24px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
.section-edit-title {
display: flex;
align-items: center;
gap: 8px;
svg {
width: 24px;
height: 24px;
color: ${unsafeCSSVarV2('icon/primary')};
}
span {
font-size: 14px;
font-weight: 500;
color: ${unsafeCSSVarV2('icon/primary')};
line-height: 24px;
}
}
.section-edit-actions {
display: flex;
align-items: center;
gap: 8px;
.edit-button {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: ${unsafeCSSVarV2(
'layer/background/hoverOverlay'
)};
}
svg {
width: 20px;
height: 20px;
color: ${unsafeCSSVarV2('icon/primary')};
}
}
}
}
}
`;
@property({ attribute: false })
accessor data!: SectionEditToolCall | SectionEditToolResult;
@property({ attribute: false })
accessor extensions!: ExtensionType[];
@property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor theme!: Signal<ColorScheme>;
@property({ attribute: false })
accessor host: EditorHost | null | undefined;
@property({ attribute: false })
accessor independentMode: boolean | undefined;
private get selection() {
const value = this.host?.selection.value ?? [];
return {
text: value.find(v => v.type === 'text') as TextSelection | undefined,
blocks: value.filter(v => v.type === 'block') as BlockSelection[],
};
}
renderToolCall() {
return html`
<tool-call-card
.name=${`Editing: ${this.data.args.instructions}`}
.icon=${PageIcon()}
></tool-call-card>
`;
}
renderToolResult() {
if (this.data.type !== 'tool-result') {
return nothing;
}
const result = this.data.result;
if (result && 'content' in result) {
return html`
<div class="section-edit-result">
<div class="section-edit-header">
<div class="section-edit-title">
${PageIcon()}
<span>Edited Content</span>
</div>
<div class="section-edit-actions">
<div
class="edit-button"
@click=${async () => {
const success = await copyText(result.content);
if (success) {
this.notifySuccess('Copied to clipboard');
}
}}
>
${CopyIcon()}
<affine-tooltip>Copy</affine-tooltip>
</div>
${this.independentMode
? nothing
: html`<div
class="edit-button"
@click=${async () => {
if (!this.host) return;
if (this.host.std.store.readonly$.value) {
this.notificationService.notify({
title: 'Cannot insert in read-only mode',
accent: 'error',
onClose: () => {},
});
return;
}
if (isInsidePageEditor(this.host)) {
await PAGE_INSERT.handler(
this.host,
result.content,
this.selection
);
} else {
await EDGELESS_INSERT.handler(
this.host,
result.content,
this.selection
);
}
}}
>
${InsertBleowIcon()}
<affine-tooltip>Insert below</affine-tooltip>
</div>`}
${this.independentMode
? nothing
: html`<div
class="edit-button"
@click=${async () => {
if (!this.host) return;
SAVE_AS_DOC.handler(this.host, result.content);
}}
>
${LinkedPageIcon()}
<affine-tooltip>Create new doc</affine-tooltip>
</div>`}
</div>
</div>
<chat-content-rich-text
.text=${result.content}
.state=${'finished'}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.theme=${this.theme}
></chat-content-rich-text>
</div>
`;
}
return html`
<tool-call-failed
.name=${'Section edit failed'}
.icon=${PageIcon()}
></tool-call-failed>
`;
}
private readonly notifySuccess = (title: string) => {
this.notificationService.notify({
title: title,
accent: 'success',
onClose: function (): void {},
});
};
protected override render() {
const { data } = this;
if (data.type === 'tool-call') {
return this.renderToolCall();
}
if (data.type === 'tool-result') {
return this.renderToolResult();
}
return nothing;
}
}
@@ -111,30 +111,6 @@ export class ToolResultCard extends SignalWatcher(
flex: 1;
}
.result-icon {
width: 18px;
height: 18px;
&:has(img) {
background-color: ${unsafeCSSVarV2('layer/background/primary')};
border-radius: 100%;
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
}
img {
width: inherit;
height: inherit;
border-radius: 100%;
border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
}
svg {
width: inherit;
height: inherit;
color: ${unsafeCSSVarV2('icon/primary')};
}
}
.result-content {
font-size: 12px;
line-height: 20px;
@@ -147,6 +123,27 @@ export class ToolResultCard extends SignalWatcher(
text-overflow: ellipsis;
}
.result-icon,
.footer-icon {
width: 18px;
height: 18px;
border-radius: 100%;
background-color: ${unsafeCSSVarV2('layer/background/primary')};
img {
width: 18px;
height: 18px;
border-radius: 100%;
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
}
svg {
width: 18px;
height: 18px;
color: ${unsafeCSSVarV2('icon/primary')};
}
}
.footer-icons {
display: flex;
position: relative;
@@ -157,26 +154,6 @@ export class ToolResultCard extends SignalWatcher(
user-select: none;
}
.footer-icon {
width: 18px;
height: 18px;
background-color: ${unsafeCSSVarV2('layer/background/primary')};
border-radius: 100%;
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
img {
width: 18px;
height: 18px;
border-radius: 100%;
}
svg {
width: 18px;
height: 18px;
color: ${unsafeCSSVarV2('icon/primary')};
}
}
.footer-icon:not(:first-child) {
margin-left: -8px;
}
@@ -194,7 +171,7 @@ export class ToolResultCard extends SignalWatcher(
accessor name: string = 'Tool result';
@property({ attribute: false })
accessor icon: TemplateResult<1> | string = ToolIcon();
accessor icon: TemplateResult<1> = ToolIcon();
@property({ attribute: false })
accessor footerIcons: TemplateResult<1>[] | string[] = [];
@@ -214,7 +191,7 @@ export class ToolResultCard extends SignalWatcher(
return html`
<div class="ai-tool-result-wrapper">
<div class="ai-tool-header" @click=${this.toggleCard}>
<div class="ai-icon">${this.renderIcon(this.icon)}</div>
<div class="ai-icon">${this.icon}</div>
<div class="ai-tool-name">${this.name}</div>
${this.isCollapsed
? this.renderFooterIcons()
@@ -284,7 +261,18 @@ export class ToolResultCard extends SignalWatcher(
}
if (typeof icon === 'string') {
return html`<img src=${this.buildUrl(icon)} />`;
return html`<div class="image-icon">
<img
src=${this.buildUrl(icon)}
@error=${(e: Event) => {
const img = e.target as HTMLImageElement;
img.style.display = 'none';
const iconElement = img.nextElementSibling as HTMLDivElement;
iconElement.style.display = 'block';
}}
/>
<div style="display: none;">${this.icon}</div>
</div>`;
}
return html`${icon}`;
}
@@ -47,6 +47,7 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) {
></tool-call-card>
`;
}
renderToolResult() {
if (this.data.type !== 'tool-result') {
return nothing;
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
@@ -173,6 +174,9 @@ export class PlaygroundChat extends SignalWatcher(
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor addChat!: () => Promise<void>;
@@ -338,6 +342,7 @@ export class PlaygroundChat extends SignalWatcher(
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.messages=${this.messages}
@@ -357,6 +362,7 @@ export class PlaygroundChat extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
></ai-chat-composer>
</div>`;
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
@@ -92,6 +93,9 @@ export class PlaygroundContent extends SignalWatcher(
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor sessions: CopilotChatHistoryFragment[] = [];
@@ -347,6 +351,7 @@ export class PlaygroundContent extends SignalWatcher(
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.addChat=${this.addChat}
></playground-chat>
</div>
@@ -62,6 +62,7 @@ import { DocEditTool } from './components/ai-tools/doc-edit';
import { DocKeywordSearchResult } from './components/ai-tools/doc-keyword-search-result';
import { DocReadResult } from './components/ai-tools/doc-read-result';
import { DocSemanticSearchResult } from './components/ai-tools/doc-semantic-search-result';
import { SectionEditTool } from './components/ai-tools/section-edit';
import { ToolCallCard } from './components/ai-tools/tool-call-card';
import { ToolFailedCard } from './components/ai-tools/tool-failed-card';
import { ToolResultCard } from './components/ai-tools/tool-result-card';
@@ -219,6 +220,7 @@ export function registerAIEffects() {
customElements.define('doc-read-result', DocReadResult);
customElements.define('web-crawl-tool', WebCrawlTool);
customElements.define('web-search-tool', WebSearchTool);
customElements.define('section-edit-tool', SectionEditTool);
customElements.define('doc-compose-tool', DocComposeTool);
customElements.define('code-artifact-tool', CodeArtifactTool);
customElements.define('code-highlighter', CodeHighlighter);
@@ -19,7 +19,7 @@ import {
} from '../widgets/ai-panel/ai-panel';
export function AiSlashMenuConfigExtension() {
const AIItems = pageAIGroups.map(group => group.items).flat();
const AIItems = pageAIGroups.flatMap(group => group.items);
const iconWrapper = (icon: AIItemConfig['icon']) => {
return html`<div style="color: var(--affine-primary-color)">
@@ -1,3 +1,7 @@
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type {
@@ -393,6 +397,7 @@ export class AIChatBlockPeekView extends LitElement {
control: 'chat-send',
reasoning: this._isReasoningActive,
webSearch: this._isNetworkActive,
toolsConfig: this.aiToolsConfigService.config.value,
});
for await (const text of stream) {
@@ -608,6 +613,7 @@ export class AIChatBlockPeekView extends LitElement {
.searchMenuConfig=${this.searchMenuConfig}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.notificationService=${notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.onChatSuccess=${this._onChatSuccess}
.trackOptions=${{
where: 'ai-chat-block',
@@ -646,6 +652,12 @@ export class AIChatBlockPeekView extends LitElement {
@property({ attribute: false })
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor _historyMessages: ChatMessage[] = [];
@@ -682,7 +694,9 @@ export const AIChatBlockPeekViewTemplate = (
networkSearchConfig: AINetworkSearchConfig,
reasoningConfig: AIReasoningConfig,
affineFeatureFlagService: FeatureFlagService,
affineWorkspaceDialogService: WorkspaceDialogService
affineWorkspaceDialogService: WorkspaceDialogService,
aiDraftService: AIDraftService,
aiToolsConfigService: AIToolsConfigService
) => {
return html`<ai-chat-block-peek-view
.blockModel=${blockModel}
@@ -693,5 +707,7 @@ export const AIChatBlockPeekViewTemplate = (
.reasoningConfig=${reasoningConfig}
.affineFeatureFlagService=${affineFeatureFlagService}
.affineWorkspaceDialogService=${affineWorkspaceDialogService}
.aiDraftService=${aiDraftService}
.aiToolsConfigService=${aiToolsConfigService}
></ai-chat-block-peek-view>`;
};
@@ -1,4 +1,5 @@
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
import type { UserFriendlyError } from '@affine/error';
import {
addContextCategoryMutation,
@@ -415,6 +416,7 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
signal,
}: {
sessionId: string;
@@ -422,6 +424,7 @@ export class CopilotClient {
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
signal?: AbortSignal;
}) {
let url = `/api/copilot/chat/${sessionId}`;
@@ -430,6 +433,7 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
});
if (queryString) {
url += `?${queryString}`;
@@ -446,12 +450,14 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
}: {
sessionId: string;
messageId?: string;
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
},
endpoint = Endpoint.Stream
) {
@@ -461,6 +467,7 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
});
if (queryString) {
url += `?${queryString}`;
@@ -486,7 +493,9 @@ export class CopilotClient {
return this.eventSource(url);
}
paramsToQueryString(params: Record<string, string | boolean | undefined>) {
paramsToQueryString(
params: Record<string, string | boolean | undefined | Record<string, any>>
) {
const queryString = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (typeof value === 'boolean') {
@@ -495,6 +504,8 @@ export class CopilotClient {
}
} else if (typeof value === 'string') {
queryString.append(key, value);
} else if (typeof value === 'object' && value !== null) {
queryString.append(key, JSON.stringify(value));
}
});
return queryString.toString();
@@ -1,3 +1,4 @@
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
import { partition } from 'lodash-es';
import { AIProvider } from './ai-provider';
@@ -22,6 +23,7 @@ export type TextToTextOptions = {
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
};
export type ToImageOptions = TextToTextOptions & {
@@ -119,6 +121,7 @@ export function textToText({
reasoning,
webSearch,
modelId,
toolsConfig,
}: TextToTextOptions) {
let messageId: string | undefined;
@@ -141,6 +144,7 @@ export function textToText({
reasoning,
webSearch,
modelId,
toolsConfig,
},
endpoint
);
@@ -1,14 +1,10 @@
import { parsePageDoc } from '@affine/reader';
import { LifeCycleWatcher } from '@blocksuite/affine/std';
import { Extension, type Store } from '@blocksuite/affine/store';
import {
BlockMarkdownAdapterMatcherIdentifier,
MarkdownAdapter,
} from '@blocksuite/affine-shared/adapters';
import { type Container, createIdentifier } from '@blocksuite/global/di';
import { LiveData } from '@toeverything/infra';
import type { Subscription } from 'rxjs';
import { blockTagMarkdownAdapterMatcher } from '../adapters/block-tag';
import { applyPatchToDoc } from '../utils/apply-model/apply-patch-to-doc';
import {
generateRenderDiff,
@@ -381,24 +377,25 @@ export class BlockDiffService extends Extension implements BlockDiffProvider {
}
getMarkdownFromDoc = async (doc: Store) => {
const cloned = doc.provider.container.clone();
cloned.addImpl(
BlockMarkdownAdapterMatcherIdentifier,
blockTagMarkdownAdapterMatcher
);
const job = doc.getTransformer();
const snapshot = job.docToSnapshot(doc);
const adapter = new MarkdownAdapter(job, cloned.provider());
const spaceDoc = doc.doc.spaceDoc;
if (!snapshot) {
return 'Failed to get markdown from doc';
throw new Error('Failed to get snapshot');
}
// FIXME: reverse the block matchers to make the block tag adapter the first one
adapter.blockMatchers.reverse();
const markdown = await adapter.fromDocSnapshot({
snapshot,
assets: job.assetsManager,
const parsed = parsePageDoc({
doc: spaceDoc,
workspaceId: doc.workspace.id,
buildBlobUrl: (blobId: string) => {
return `/${doc.workspace.id}/blobs/${blobId}`;
},
buildDocUrl: (docId: string) => {
return `/workspace/${doc.workspace.id}/${docId}`;
},
aiEditable: true,
});
return markdown.file;
return parsed.md;
};
}
@@ -1,6 +1,5 @@
import type { WeekDatePickerHandle } from '@affine/component';
import { WeekDatePicker } from '@affine/component';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import {
JOURNAL_DATE_FORMAT,
JournalService,
@@ -26,11 +25,6 @@ export const JournalWeekDatePicker = ({ page }: JournalWeekDatePickerProps) => {
);
const workbench = useService(WorkbenchService).workbench;
const featureFlagService = useService(FeatureFlagService);
const isTwoStepJournalConfirmationEnabled = useLiveData(
featureFlagService.flags.enable_two_step_journal_confirmation.$
);
useEffect(() => {
if (!journalDate) return;
setDate(journalDate.format(JOURNAL_DATE_FORMAT));
@@ -39,19 +33,14 @@ export const JournalWeekDatePicker = ({ page }: JournalWeekDatePickerProps) => {
const openJournal = useCallback(
(date: string) => {
if (isTwoStepJournalConfirmationEnabled) {
const docs = journalService.journalsByDate$(date).value;
if (docs.length > 0) {
workbench.openDoc(docs[0].id, { at: 'active' });
} else {
workbench.open(`/journals?date=${date}`, { at: 'active' });
}
const docs = journalService.journalsByDate$(date).value;
if (docs.length > 0) {
workbench.openDoc(docs[0].id, { at: 'active' });
} else {
const doc = journalService.ensureJournalByDate(date);
workbench.openDoc(doc.id, { at: 'active' });
workbench.open(`/journals?date=${date}`, { at: 'active' });
}
},
[isTwoStepJournalConfirmationEnabled, journalService, workbench]
[journalService, workbench]
);
return (

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