Compare commits

...

114 Commits

Author SHA1 Message Date
DarkSky
c4cf5799d4 fix(server): exclude outdated doc id style in embedding count (#13269)
fix AI-392
fix AI-393

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

## Summary by CodeRabbit

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

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

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

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

> CLOSE AI-328 AI-379 AI-380

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

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

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

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

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

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

## Summary by CodeRabbit

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

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

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

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

## Summary by CodeRabbit

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

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

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

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

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

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

## Summary by CodeRabbit

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

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

## Summary by CodeRabbit

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

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

## Summary by CodeRabbit

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

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

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

## Summary by CodeRabbit

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

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

## Summary by CodeRabbit

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

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

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

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

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

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

## Summary by CodeRabbit

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

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

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

#### PR Dependency Tree


* **PR #13256** 👈

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

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

## Summary by CodeRabbit

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

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

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

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

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

## Summary by CodeRabbit

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

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

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

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

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

## Summary by CodeRabbit

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

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

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

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

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

## Summary by CodeRabbit

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

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

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

#### PR Dependency Tree


* **PR #13250** 👈

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

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

## Summary by CodeRabbit

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

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

## Summary by CodeRabbit

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

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

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

#### PR Dependency Tree


* **PR #13245** 👈

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

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

## Summary by CodeRabbit

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

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

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

#### PR Dependency Tree


* **PR #13241** 👈

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

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

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


#### PR Dependency Tree


* **PR #13241** 👈

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

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

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

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

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

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

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

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

**before**


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



**after**


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




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

## Summary by CodeRabbit

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

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

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

## Summary by CodeRabbit

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

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

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

## Summary by CodeRabbit

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

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

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

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

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

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

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

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

## Summary by CodeRabbit

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

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

#### PR Dependency Tree


* **PR #13224** 👈

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

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

## Summary by CodeRabbit

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

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

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

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

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

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

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

## Summary by CodeRabbit

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

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

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

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

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

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

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

## Summary by CodeRabbit

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

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

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

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

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

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

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

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

## Summary by CodeRabbit

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

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

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

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

## Summary by CodeRabbit

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

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

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


* **PR #13214** 👈

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

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

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

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

## Summary by CodeRabbit

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

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

## Summary by CodeRabbit

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

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


* **PR #13213** 👈

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

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

## Summary by CodeRabbit

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

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

## Summary by CodeRabbit

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

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

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

#### PR Dependency Tree


* **PR #13208** 👈

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

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

## Summary by CodeRabbit

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

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

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

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

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

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

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

## Summary by CodeRabbit

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

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

#### PR Dependency Tree


* **PR #13203** 👈

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

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

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





#### PR Dependency Tree


* **PR #13203** 👈

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

## Summary by CodeRabbit

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

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


* **PR #13204** 👈

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

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

## Summary by CodeRabbit

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

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


* **PR #13202** 👈

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

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

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

---------

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

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

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

* **Improvements**
* The process for generating missing session titles now considers all
eligible sessions, without limiting the number processed at a time.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 09:19:21 +00:00
Cats Juice
7b53641a94 fix(core): disable creating linked doc in sidebar when show linked is off (#13191)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* The "add linked page" icon button in the navigation panel is now only
visible if enabled in your app settings.

* **Enhancements**
* The navigation panel dynamically updates the available operations
based on your sidebar settings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 08:02:16 +00:00
Cats Juice
3948b8eada feat(core): display doc title with display-config for semantic result (#13194)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Improved document title display in AI chat semantic search results by
fetching titles from a dedicated service for more accurate and
consistent information.

* **Enhancements**
* Enhanced integration between chat and document display features,
ensuring configuration and services are consistently passed through chat
components for better user experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Wu Yue <akumatus@gmail.com>
2025-07-14 07:58:17 +00:00
Cats Juice
d05bb9992c style(core): adjust sidebar new page button background (#13193)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Updated the background color of the add-page button in the sidebar for
a refreshed appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 07:57:46 +00:00
Wu Yue
b2c09825ac feat(core): do not show AI actions in history (#13198)
Close [AI-351](https://linear.app/affine-design/issue/AI-351)

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

* **Bug Fixes**
* Disabled action updates related to document IDs and sessions in the AI
chat content panel.

* **Tests**
* Skipped all end-to-end tests for the "should show chat history in chat
panel" scenario across various AI action test suites. These tests will
no longer run during automated testing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 07:53:14 +00:00
Wu Yue
65453c31c6 feat(core): ai intelligence track (#13187)
Close [AI-335](https://linear.app/affine-design/issue/AI-335)

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

* **New Features**
* Added support for an "independent mode" and document-specific context
in AI chat components, allowing enhanced context handling and tracking.
* Introduced new tracking options to distinguish between current
document and general document actions in chat interactions.

* **Improvements**
* More flexible property handling for independent mode and document
context across chat-related components for consistent behavior and
tracking.
* Enhanced tracking system to support additional event categories and
methods for more granular analytics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 06:43:06 +00:00
Peng Xiao
d9e8ce802f fix(core): loading spinner color issue (#13192)
#### PR Dependency Tree


* **PR #13192** 👈

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

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

## Summary by CodeRabbit

* **Style**
* Updated the loading icon to ensure consistent appearance by explicitly
setting the fill property to none using an inline style.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 04:54:47 +00:00
DarkSky
d5f63b9e43 fix(server): recent session missing params (#13188)
fix AI-349
2025-07-14 04:17:48 +00:00
Cats Juice
ebefbeefc8 fix(core): prevent creating session every time in chat page (#13190)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved session handling in chat to prevent redundant session
creation and ensure consistent assignment of session functions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 03:57:51 +00:00
Peng Xiao
4d7d8f215f fix(core): artifact panel theme (#13186)
fix AI-340, AI-344

#### PR Dependency Tree


* **PR #13186** 👈

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

## Summary by CodeRabbit

* **Style**
* Updated the artifact preview panel layout by removing a fixed height
constraint for improved flexibility.
* Refined visual styling of linked document blocks with updated shadow
effects for better aesthetics.

* **New Features**
* Enhanced document preview rendering to respect the current theme,
providing a more consistent visual experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #13186** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-14 02:46:28 +00:00
DarkSky
b6187718ea feat(server): add cron job for session cleanup (#13181)
fix AI-338
2025-07-13 13:53:38 +00:00
德布劳外 · 贾贵
3ee82bd9ce test: skip ai chat with multi tags test (#13170)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Temporarily skipped tests related to chatting with tags and specified
documents due to flakiness.
* Improved chat retry test by streamlining status checks for faster
validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-13 13:02:03 +00:00
Cats Juice
3dbdb99435 feat(core): add basic ui for doc search related tool calling (#13176)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced support for document semantic search, keyword search, and
document reading tools in chat AI features.
* Added new interactive cards to display results for document keyword
search, semantic search, and reading operations within chat.
* Automatically restores and displays pinned chat sessions when
revisiting the workspace chat page.

* **Improvements**
* Enhanced the chat interface with new components for richer
document-related AI responses.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-12 02:17:37 +00:00
Cats Juice
0d414d914a fix(core): right sidebar switching not work after switching workspace (#13179)
Due to missing the correct unsubscription, switching workspaces triggers
multiple events. As a result, the sidebar cannot be closed on every
second trigger.
2025-07-11 15:15:16 +00:00
github-actions[bot]
41f338bce0 chore(i18n): sync translations (#13178)
New Crowdin translations by [Crowdin GH
Action](https://github.com/crowdin/github-action)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-07-11 21:41:57 +08:00
Cats Juice
6f87c1ca50 fix(core): hide footer actions for independent ai chat (#13177)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added support for an "independent mode" in assistant chat messages,
which hides editor actions when enabled.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 12:59:02 +00:00
EYHN
33f6496d79 feat(core): show server name when delete account (#13175)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Account deletion prompts and confirmation dialogs now display the
specific server name, providing clearer context when deleting your
account.

* **Localization**
* Updated account deletion messages to explicitly mention the server
name.
* Improved translation keys to support server-specific messaging in all
supported languages.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 12:38:05 +00:00
DarkSky
847ef00a75 feat(server): add doc meta for semantic search (#13174)
fix AI-339
2025-07-11 18:36:21 +08:00
Wu Yue
93f13e9e01 feat(core): update ai add context button ui (#13172)
Close [AI-301](https://linear.app/affine-design/issue/AI-301)

<img width="571" height="204" alt="截屏2025-07-11 17 33 01"
src="https://github.com/user-attachments/assets/3b7ed81f-1137-4c01-8fe2-9fe5ebf2adf3"
/>


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

* **New Features**
* Introduced a new component for adding context (images, documents,
tags, collections) to AI chat via a plus button and popover menu.
* Added notification feedback for duplicate chip additions and image
upload limits.
* Chips panel now supports collapsing and expanding for improved UI
control.

* **Improvements**
* Refactored chip management for better error handling, feedback, and
external control.
* Streamlined image and document uploads through a unified menu-driven
interface.
* Enhanced chip management methods with clearer naming and robust
synchronization.
* Updated chat input to delegate image upload and context additions to
the new add-context component.

* **Bug Fixes**
* Improved cancellation and cleanup of ongoing chip addition operations
to prevent conflicts.

* **Tests**
* Updated end-to-end tests to reflect the new menu-driven image upload
workflow and removed legacy checks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2025-07-11 10:10:41 +00:00
fengmk2
a2b86bc6d2 chore(server): enable schedule module by default (#13173)
#### PR Dependency Tree


* **PR #13173** 👈

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

* **Refactor**
* Simplified internal module management to ensure more consistent
availability of core features. No visible changes to user-facing
functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 09:54:38 +00:00
Peng Xiao
aee7a8839e fix(core): update code artifact tool prompt (#13171)
#### PR Dependency Tree


* **PR #13171** 👈

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

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

* **New Features**
* Introduced a new "Code Artifact" prompt that generates HTML files
styled with Tailwind CSS, following a specific color theme and design
guidelines.

* **Style**
  * Minor formatting improvements for consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 09:52:16 +00:00
EYHN
0e8ffce126 fix(core): avoid infinite sign in with selfhost (#13169)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* The 404 page now reflects user session state across multiple servers,
showing the appropriate user context when multiple accounts are logged
in.

* **Improvements**
* Enhanced user experience on the 404 page by accurately displaying
information based on the first active logged-in account across all
servers.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 09:46:11 +00:00
Peng Xiao
9cda655c9e fix(core): artifact rendering issue in standalone ai chat panel (#13166)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Improved chat component to support document link navigation directly
from chat messages, allowing users to open documents in the workbench
when links are clicked.

* **Refactor**
* Streamlined notification handling and property access in document
composition tools for a cleaner user experience.
* Updated import statements for improved code clarity and
maintainability.
  * Enhanced code artifact tool rendering to ensure consistent theming.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 17:53:04 +08:00
L-Sun
15726bd522 fix(editor): missing viewport selector in editor setting (#13168)
#### PR Dependency Tree


* **PR #13168** 👈

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 CSS class names for the snapshot container to improve
consistency in styling and targeting.


<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 09:06:18 +00:00
Peng Xiao
d65a7494a4 fix(core): some artifact tools styling (#13152)
fix BS-3615, BS-3616, BS-3614

#### PR Dependency Tree


* **PR #13152** 👈

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

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

* **Style**
* Improved consistency of horizontal padding and spacing in AI chat
components.
* Updated chat message containers to enable vertical scrolling and
adjust height behavior.
* Refined artifact tool card appearance with enhanced hover effects,
cursor placement, and updated card structure for a more polished look.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #13152** 👈

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

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2025-07-11 16:48:56 +08:00
fengmk2
0f74e1fa0f fix(server): ignore 409 status error on es delete query (#13162)
close AF-2736



#### PR Dependency Tree


* **PR #13162** 👈

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

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved error handling for Elasticsearch operations by allowing
certain conflict errors to be ignored, resulting in more robust and
tolerant behavior during data deletion processes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 16:30:58 +08:00
Peng Xiao
fef4a9eeb6 fix(core): artifact rendering issue in standalone ai chat panel (#13164)
#### PR Dependency Tree


* **PR #13164** 👈

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

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

## Summary by CodeRabbit

* **New Features**
* Improved integration of workspace context into AI chat, enabling more
responsive interactions when clicking document links within chat
messages.
* Enhanced document opening experience from chat by reacting to link
clicks and providing direct access to related documents.

* **Refactor**
* Streamlined notification handling and workspace context management
within chat-related components for better maintainability and
performance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 08:09:07 +00:00
德布劳外 · 贾贵
58dc53581f fix: hide embedding status tip if embedding completed (#13156)
> CLOSE AI-334

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved the responsiveness of embedding status updates in the AI chat
composer, reducing unnecessary refreshes when the status has not
changed.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 07:31:12 +00:00
德布劳外 · 贾贵
b23f380539 fix(core): remove scroller visiblility test (#13159)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Removed the test verifying the scroll indicator appears when there are
many chat messages.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 07:22:08 +00:00
github-actions[bot]
d29a97f86c chore(i18n): sync translations (#13161)
New Crowdin translations by [Crowdin GH
Action](https://github.com/crowdin/github-action)

---------

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-11 07:19:10 +00:00
github-actions[bot]
0f287f9661 chore(i18n): sync translations (#13160)
New Crowdin translations by [Crowdin GH
Action](https://github.com/crowdin/github-action)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-07-11 14:47:04 +08:00
github-actions[bot]
18f13626cc chore(i18n): sync translations (#13158)
New Crowdin translations by [Crowdin GH
Action](https://github.com/crowdin/github-action)

---------

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-11 14:35:45 +08:00
github-actions[bot]
0eeea5e173 chore(i18n): sync translations (#13157)
New Crowdin translations by [Crowdin GH
Action](https://github.com/crowdin/github-action)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-07-11 14:14:59 +08:00
DarkSky
2052a34d19 chore(server): add detail for error (#13151)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Error messages for unavailable copilot providers now include specific
model IDs for clearer context.
* Added new detailed error messages for embedding generation failures
specifying provider and error details.
* The API and GraphQL schema have been extended with new error types
reflecting these detailed error cases.

* **Bug Fixes**
* Enhanced error handling to detect and report incomplete or missing
embeddings from providers.
* Added safeguards to skip embedding insertions when no embeddings are
provided, preventing unnecessary processing.

* **Documentation**
* Updated localization and translation keys to support dynamic error
messages with model IDs and provider details.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 05:55:10 +00:00
DarkSky
b79439b01d fix(server): sse abort behavior (#13153)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of aborted client connections during streaming,
ensuring that session messages accurately reflect if a request was
aborted.
* Enhanced consistency and reliability across all streaming endpoints
when saving session messages after streaming.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 04:46:55 +00:00
Cats Juice
2dacba9011 feat(core): restore pinned chat for independent chat (#13154)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Improved chat session management by automatically restoring a pinned
chat session when opening the workspace chat.

* **Enhancements**
* Added support for cancelling certain requests, improving
responsiveness and user experience.

* **Style**
* Updated the label "AFFiNE Intelligence" to "Intelligence" in relevant
UI components for a more concise display.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 04:45:59 +00:00
fengmk2
af9c455ee0 feat(server): add process memory usage metrics (#13148)
close CLOUD-235

<img width="2104" height="1200" alt="image"
src="https://github.com/user-attachments/assets/6ea0fd89-ab32-42e3-a675-f00f9e5856ad"
/>



#### PR Dependency Tree


* **PR #13148** 👈

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

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

## Summary by CodeRabbit

* **New Features**
* Introduced a new monitoring service that automatically collects and
logs process memory usage statistics every minute.
* Enhanced system monitoring capabilities by integrating a global
monitoring module.
  * Added support for a new "process" scope in metric tracking.

* **Chores**
* Improved internal module organization by including the monitoring
module in the core functionality set.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 04:15:08 +00:00
Peng Xiao
3d45c7623f test(core): add a simple test for comment (#13150)
#### PR Dependency Tree


* **PR #13150** 👈

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

* **Tests**
* Added a new end-to-end test to verify creating and displaying comments
on selected text within a document.
* Updated test retry logic to limit retries to 1 in local or non-CI
environments, and to 3 in CI environments without COPILOT enabled.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 04:13:46 +00:00
Wu Yue
e0f88451e1 feat(core): render session title in ai session history (#13147)
Close [AI-331](https://linear.app/affine-design/issue/AI-331)

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

## Summary by CodeRabbit

* **Improvements**
* Session history now displays the session title (or "New chat" if
unavailable) instead of the session ID for a clearer user experience.

* **Performance**
* Recent copilot chat session lists now load faster by excluding message
details from the initial query.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 03:42:52 +00:00
Cats Juice
aba0a3d485 fix(core): load chat history content correctly (#13149)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved session handling in chat to prevent potential errors when
reloading sessions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 03:36:56 +00:00
Cats Juice
8b579e3a92 fix(core): ensure new chat when entering chat page (#13146)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added an option to always start a new AI chat session instead of
reusing the latest one.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 02:58:16 +00:00
EYHN
d98b45ca3d feat(core): clear all notifications (#13144)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a "Delete All" option in the notifications list, allowing users
to mark all notifications as read at once.
* Introduced a header with a menu button in the notifications list for
easier access to actions.

* **Style**
* Updated notification list layout with improved structure, including a
header and a scrollable content area.

* **Localization**
* Added a new English localization string for the "Delete all
notifications" action.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 02:50:48 +00:00
fengmk2
fc1104cd68 chore(server): add comment attachment storage metrics (#13143)
close AF-2728



#### PR Dependency Tree


* **PR #13143** 👈

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 metrics tracking for comment attachment uploads, including
recording attachment size and total uploads by MIME type.
* Enhanced logging for attachment uploads with detailed information such
as workspace ID, document ID, file size, and user ID.

* **Chores**
* Expanded internal metric categories to include storage-related
metrics.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 15:43:12 +00:00
Peng Xiao
46901c472c fix(core): empty style for comment (#13142)
fix AF-2735

#### PR Dependency Tree


* **PR #13142** 👈

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**
* Increased padding for empty state elements in the comment sidebar to
improve visual spacing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 13:50:19 +00:00
Wu Yue
9d5c7dd1e9 fix(core): doc reference error in ai answer (#13141)
Close [AI-303](https://linear.app/affine-design/issue/AI-303)

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

## Summary by CodeRabbit

* **New Features**
* Introduced a new AI configuration hook to streamline AI-related
features and integrations.
* Integrated enhanced AI specifications into chat components for
improved AI chat experiences.

* **Refactor**
* Updated chat panels to use the new AI configuration hook, simplifying
extension management and improving maintainability.

* **Chores**
* Improved options handling in the editor view extension for more
flexible configuration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 11:44:30 +00:00
fengmk2
f655e6e8bf feat(server): export title and summary on doc resolver (#13139)
close AF-2732






#### PR Dependency Tree


* **PR #13139** 👈

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 support for a document summary field, allowing documents to
include and display an optional summary alongside the title.

* **Bug Fixes**
* Improved access control when retrieving documents, ensuring proper
permission checks are enforced.

* **Tests**
* Expanded test coverage to verify correct handling of document title
and summary fields, including cases where the summary is absent.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 11:13:19 +00:00
L-Sun
46a9d0f7fe fix(editor): commented heading style (#13140)
Close BS-3613

#### PR Dependency Tree


* **PR #13140** 👈

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 default text styling to inherit font weight, style, and
decoration from parent elements when bold, italic, underline, or strike
attributes are not set. This may result in text more closely matching
its surrounding context.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 11:12:20 +00:00
fengmk2
340aae6476 refactor(server): updates merge delay reduced to 5 seconds (#13138)
close AF-2733



#### PR Dependency Tree


* **PR #13138** 👈

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

* **Refactor**
* Reduced the delay for merging pending document updates from 30 seconds
to 5 seconds, resulting in faster update processing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 09:41:28 +00:00
德布劳外 · 贾贵
6b7d1e91e0 feat(core): apply model tracking (#13128)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added user interaction tracking for document editing and diff review
actions, including accepting, rejecting, applying, and copying changes.
* Introduced tracking for "Accept all" and "Reject all" actions in block
diff views.

* **Chores**
* Enhanced event tracking system with new event types and payloads to
support detailed analytics for editing and review actions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 17:25:05 +08:00
fengmk2
3538c78a8b chore(server): use jemalloc to reduce RSS (#13134)
close CLOUD-237



#### PR Dependency Tree


* **PR #13134** 👈
  * **PR #13079**
    * **PR #13125**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-10 09:03:04 +00:00
Peng Xiao
7d527c7f3a fix(core): cannot download comment files (#13136)
#### PR Dependency Tree


* **PR #13136** 👈

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 timeout of 3 minutes to comment attachment uploads, improving
reliability for long uploads.

* **Refactor**
* Unified the file download process to always use blob conversion,
ensuring consistent behavior for all URLs.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 07:48:17 +00:00
DarkSky
ad5a122391 feat(server): summary tools (#13133)
fix AI-281

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

## Summary by CodeRabbit

* **New Features**
* Added a new AI-powered conversation summary tool that generates
concise summaries of key topics, decisions, and details from
conversations, with options to focus on specific areas and adjust
summary length.
* Introduced a new prompt for conversation summarization, supporting
customizable focus and summary length.

* **Bug Fixes**
* Improved tool handling and error messages for conversation
summarization when required input is missing.

* **Tests**
* Expanded test coverage to include scenarios for the new conversation
summary feature.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 06:47:34 +00:00
Cats Juice
0f9b9789da fix(core): add missing tooltip effect for independent chat (#13127)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a tooltip component, making it available throughout the
application.

* **Refactor**
* Centralized tooltip initialization and registration for improved
consistency.
* Updated imports to use the new tooltip module, streamlining component
usage.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 05:09:48 +00:00
L-Sun
5b027f7986 fix(core): disable comment in local workspace (#13124)
Close AF-2731

#### PR Dependency Tree


* **PR #13124** 👈

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**
* Comment functionality is now available only for cloud workspaces and
is disabled for local or shared modes.

* **Bug Fixes**
* Improved accuracy in enabling comments based on workspace type and
configuration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 05:08:53 +00:00
Wu Yue
fe00293e3e feat(core): disable pin chat while generating AI answers (#13131)
Close [AI-316](https://linear.app/affine-design/issue/AI-316)

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

## Summary by CodeRabbit

* **New Features**
* Chat status is now displayed and updated in the chat panel and
toolbar, allowing users to see when the chat is generating a response.
* The pin button in the chat toolbar is disabled while the chat is
generating a response, preventing pin actions during this time and
providing feedback via a notification if attempted.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 04:25:12 +00:00
Cats Juice
385226083f chore(core): adjust ai page tab name (#13129)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Replaced the localized workspace title with a hardcoded "AFFiNE
Intelligence" label in the chat view.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 04:19:24 +00:00
EYHN
38d8dde6b8 chore(ios): fix ios version (#13130)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added support for specifying a custom iOS App Store version during
release workflows.

* **Chores**
  * Updated the iOS app's marketing version to 0.23.1.
  * Upgraded the iOS workflow runner to macOS 15 and Xcode 16.4.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 04:18:33 +00:00
Peng Xiao
ed6fde550f fix(core): some comment editor ux enhancements (#13126)
fix AF-2726, AF-2729

#### PR Dependency Tree


* **PR #13126** 👈

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 drag-and-drop support for file attachments in the comment
editor.
* Improved user feedback with notifications and toasts when downloading
attachments.

* **Bug Fixes**
  * Enhanced error handling and reporting for attachment downloads.

* **Improvements**
* Optimized file download process for same-origin resources to improve
performance.
* Updated default comment filter to show all comments, not just those
for the current mode.

* **Documentation**
* Updated English localization to provide clearer instructions when no
comments are present.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #13126** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-10 03:58:00 +00:00
Wu Yue
11a9e67bc1 feat(core): remove scrollable-text-renderer's dependency on editor host (#13123)
[AI-260](https://linear.app/affine-design/issue/AI-260)

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

## Summary by CodeRabbit

* **Refactor**
* Improved integration of theme support in AI-generated answer
rendering, allowing the renderer to adapt to theme changes dynamically.
* Simplified component interfaces by removing unnecessary dependencies.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 03:31:52 +00:00
Wu Yue
899585ba7f fix(core): old ai messages not cleared before retrying (#13119)
Close [AI-258](https://linear.app/affine-design/issue/AI-258)

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved the retry behavior in AI chat by ensuring previous streaming
data is properly cleared before retrying, resulting in more accurate and
consistent chat message handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 03:20:30 +00:00
302 changed files with 7241 additions and 2452 deletions

View File

@@ -4,9 +4,15 @@ inputs:
app-version:
description: 'App Version'
required: true
ios-app-version:
description: 'iOS App Store Version (Optional, use App version if empty)'
required: false
type: string
runs:
using: 'composite'
steps:
- name: 'Write Version'
shell: bash
env:
IOS_APP_VERSION: ${{ inputs.ios-app-version }}
run: ./scripts/set-version.sh ${{ inputs.app-version }}

View File

@@ -7,7 +7,10 @@ COPY ./packages/frontend/apps/mobile/dist /app/static/mobile
WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends openssl && \
apt-get install -y --no-install-recommends openssl libjemalloc2 && \
rm -rf /var/lib/apt/lists/*
# Enable jemalloc by preloading the library
ENV LD_PRELOAD=libjemalloc.so.2
CMD ["node", "./dist/main.js"]

View File

@@ -465,7 +465,7 @@ jobs:
name: ${{ env.RELEASE_VERSION }}
draft: ${{ inputs.build-type == 'stable' }}
prerelease: ${{ inputs.build-type != 'stable' }}
tag_name: ${{ env.RELEASE_VERSION}}
tag_name: v${{ env.RELEASE_VERSION}}
files: |
./release/*
./release/.env.example

View File

@@ -12,6 +12,9 @@ on:
build-type:
type: string
required: true
ios-app-version:
type: string
required: false
env:
BUILD_TYPE: ${{ inputs.build-type }}
@@ -78,7 +81,7 @@ jobs:
path: packages/frontend/apps/android/dist
ios:
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || 'blaze/macos-14' }}
runs-on: 'macos-15'
needs:
- build-ios-web
steps:
@@ -87,6 +90,7 @@ jobs:
uses: ./.github/actions/setup-version
with:
app-version: ${{ inputs.app-version }}
ios-app-version: ${{ inputs.ios-app-version }}
- name: 'Update Code Sign Identity'
shell: bash
run: ./packages/frontend/apps/ios/update_code_sign_identity.sh
@@ -106,7 +110,7 @@ jobs:
enableScripts: false
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 16.2
xcode-version: 16.4
- name: Install Swiftformat
run: brew install swiftformat
- name: Cap sync

View File

@@ -21,6 +21,10 @@ on:
required: true
type: boolean
default: false
ios-app-version:
description: 'iOS App Store Version (Optional, use tag version if empty)'
required: false
type: string
permissions:
contents: write
@@ -30,6 +34,7 @@ permissions:
packages: write
security-events: write
attestations: write
issues: write
jobs:
prepare:
@@ -70,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: |
@@ -117,3 +123,4 @@ jobs:
build-type: ${{ needs.prepare.outputs.BUILD_TYPE }}
app-version: ${{ needs.prepare.outputs.APP_VERSION }}
git-short-hash: ${{ needs.prepare.outputs.GIT_SHORT_HASH }}
ios-app-version: ${{ inputs.ios-app-version }}

View File

@@ -266,6 +266,7 @@
"./components/toggle-button": "./src/components/toggle-button.ts",
"./components/toggle-switch": "./src/components/toggle-switch.ts",
"./components/toolbar": "./src/components/toolbar.ts",
"./components/tooltip": "./src/components/tooltip.ts",
"./components/view-dropdown-menu": "./src/components/view-dropdown-menu.ts",
"./components/tooltip-content-with-shortcut": "./src/components/tooltip-content-with-shortcut.ts",
"./components/resource": "./src/components/resource.ts",

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-components/tooltip';

View File

@@ -39,6 +39,13 @@ export class CodeBlockHighlighter extends LifeCycleWatcher {
private readonly _loadTheme = async (
highlighter: HighlighterCore
): Promise<void> => {
// It is possible that by the time the highlighter is ready all instances
// have already been unmounted. In that case there is no need to load
// themes or update state.
if (CodeBlockHighlighter._refCount === 0) {
return;
}
const config = this.std.getOptional(CodeBlockConfigExtension.identifier);
const darkTheme = config?.theme?.dark ?? CODE_BLOCK_DEFAULT_DARK_THEME;
const lightTheme = config?.theme?.light ?? CODE_BLOCK_DEFAULT_LIGHT_THEME;
@@ -78,14 +85,27 @@ export class CodeBlockHighlighter extends LifeCycleWatcher {
override unmounted(): void {
CodeBlockHighlighter._refCount--;
// Only dispose the shared highlighter when no instances are using it
if (
CodeBlockHighlighter._refCount === 0 &&
CodeBlockHighlighter._sharedHighlighter
) {
CodeBlockHighlighter._sharedHighlighter.dispose();
// Dispose the shared highlighter **after** any in-flight creation finishes.
if (CodeBlockHighlighter._refCount !== 0) {
return;
}
const doDispose = (highlighter: HighlighterCore | null) => {
if (highlighter) {
highlighter.dispose();
}
CodeBlockHighlighter._sharedHighlighter = null;
CodeBlockHighlighter._highlighterPromise = null;
};
if (CodeBlockHighlighter._sharedHighlighter) {
// Highlighter already created dispose immediately.
doDispose(CodeBlockHighlighter._sharedHighlighter);
} else if (CodeBlockHighlighter._highlighterPromise) {
// Highlighter still being created wait for it, then dispose.
CodeBlockHighlighter._highlighterPromise
.then(doDispose)
.catch(console.error);
}
}
}

View File

@@ -73,7 +73,8 @@
"./edgeless-line-styles-panel": "./src/edgeless-line-styles-panel/index.ts",
"./edgeless-shape-color-picker": "./src/edgeless-shape-color-picker/index.ts",
"./open-doc-dropdown-menu": "./src/open-doc-dropdown-menu/index.ts",
"./slider": "./src/slider/index.ts"
"./slider": "./src/slider/index.ts",
"./tooltip": "./src/tooltip/index.ts"
},
"files": [
"src",

View File

@@ -85,6 +85,8 @@ export class MenuSubMenu extends MenuFocusable {
.catch(err => console.error(err));
});
this.menu.openSubMenu(menu);
// in case that the menu is not closed, but the component is removed,
this.disposables.add(unsub);
}
protected override render(): unknown {

View File

@@ -18,6 +18,7 @@ export const LoadingIcon = ({
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="none"
style="fill: none;"
>
<style>
.spinner {

View File

@@ -1,3 +1,4 @@
import { effects as tooltipEffects } from '../tooltip/effect.js';
import { EditorIconButton } from './icon-button.js';
import {
EditorMenuAction,
@@ -6,7 +7,6 @@ import {
} from './menu-button.js';
import { EditorToolbarSeparator } from './separator.js';
import { EditorToolbar } from './toolbar.js';
import { Tooltip } from './tooltip.js';
export { EditorChevronDown } from './chevron-down.js';
export { ToolbarMoreMenuConfigExtension } from './config.js';
@@ -20,7 +20,6 @@ export { MenuContext } from './menu-context.js';
export { EditorToolbarSeparator } from './separator.js';
export { darkToolbarStyles, lightToolbarStyles } from './styles.js';
export { EditorToolbar } from './toolbar.js';
export { Tooltip } from './tooltip.js';
export type {
AdvancedMenuItem,
FatMenuItems,
@@ -38,11 +37,12 @@ export {
} from './utils.js';
export function effects() {
tooltipEffects();
customElements.define('editor-toolbar-separator', EditorToolbarSeparator);
customElements.define('editor-toolbar', EditorToolbar);
customElements.define('editor-icon-button', EditorIconButton);
customElements.define('editor-menu-button', EditorMenuButton);
customElements.define('editor-menu-content', EditorMenuContent);
customElements.define('editor-menu-action', EditorMenuAction);
customElements.define('affine-tooltip', Tooltip);
}

View File

@@ -0,0 +1,7 @@
import { Tooltip } from './tooltip.js';
export function effects() {
if (!customElements.get('affine-tooltip')) {
customElements.define('affine-tooltip', Tooltip);
}
}

View File

@@ -0,0 +1,2 @@
export { effects } from './effect.js';
export { Tooltip } from './tooltip.js';

View File

@@ -116,6 +116,7 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
`;
private _cleanup: (() => void) | null = null;
private _autoUpdateCleanup: (() => void) | null = null;
private _prevTool: ToolOptionWithType | null = null;
@@ -128,6 +129,11 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
return [TemplateCard1[theme], TemplateCard2[theme], TemplateCard3[theme]];
}
override connectedCallback() {
super.connectedCallback();
this.disposables.add(() => this._autoUpdateCleanup?.());
}
private _closePanel() {
if (this._openedPanel) {
this._openedPanel.remove();
@@ -175,8 +181,8 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
requestAnimationFrame(() => {
const arrowEl = panel.renderRoot.querySelector('.arrow') as HTMLElement;
autoUpdate(this, panel, () => {
this._autoUpdateCleanup?.();
this._autoUpdateCleanup = autoUpdate(this, panel, () => {
computePosition(this, panel, {
placement: 'top',
middleware: [offset(20), arrow({ element: arrowEl }), shift()],

View File

@@ -22,8 +22,11 @@ import { isEqual } from 'lodash-es';
})
export class InlineComment extends WithDisposable(ShadowlessElement) {
static override styles = css`
inline-comment {
display: inline;
}
inline-comment.unresolved {
display: inline-block;
background-color: ${unsafeCSSVarV2('block/comment/highlightDefault')};
border-bottom: 2px solid
${unsafeCSSVarV2('block/comment/highlightUnderline')};

View File

@@ -20,6 +20,7 @@
"@blocksuite/affine-block-paragraph": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-block-surface-ref": "workspace:*",
"@blocksuite/affine-block-table": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-fragment-doc-title": "workspace:*",

View File

@@ -18,6 +18,7 @@ import {
} from '@blocksuite/affine-block-paragraph';
import { DefaultTool, getSurfaceBlock } from '@blocksuite/affine-block-surface';
import { insertSurfaceRefBlockCommand } from '@blocksuite/affine-block-surface-ref';
import { insertTableBlockCommand } from '@blocksuite/affine-block-table';
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
import { toast } from '@blocksuite/affine-components/toast';
import { insertInlineLatex } from '@blocksuite/affine-inline-latex';
@@ -40,14 +41,20 @@ import {
deleteSelectedModelsCommand,
draftSelectedModelsCommand,
duplicateSelectedModelsCommand,
focusBlockEnd,
getBlockSelectionsCommand,
getSelectedModelsCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import {
FeatureFlagService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import type { AffineTextStyleAttributes } from '@blocksuite/affine-shared/types';
import {
createDefaultDoc,
isInsideBlockByFlavour,
openSingleFileWith,
type Signal,
} from '@blocksuite/affine-shared/utils';
@@ -87,6 +94,7 @@ import {
RedoIcon,
RightTabIcon,
StrikeThroughIcon,
TableIcon,
TeXIcon,
TextIcon,
TodayIcon,
@@ -258,6 +266,62 @@ const textToolActionItems: KeyboardToolbarActionItem[] = [
.run();
},
},
{
name: 'Table',
icon: TableIcon(),
showWhen: ({ std, rootComponent: { model } }) =>
std.store.schema.flavourSchemaMap.has('affine:table') &&
!isInsideBlockByFlavour(std.store, model, 'affine:edgeless-text'),
action: ({ std }) => {
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(insertTableBlockCommand, {
place: 'after',
removeEmptyLine: true,
})
.pipe(({ insertedTableBlockId }) => {
if (insertedTableBlockId) {
const telemetry = std.getOptional(TelemetryProvider);
telemetry?.track('BlockCreated', {
blockType: 'affine:table',
});
}
})
.run();
},
},
{
name: 'Callout',
icon: FontIcon(),
showWhen: ({ std, rootComponent: { model } }) => {
return (
std.get(FeatureFlagService).getFlag('enable_callout') &&
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text')
);
},
action: ({ rootComponent: { model }, std }) => {
const { store } = model;
const parent = store.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
if (index === -1) return;
const calloutId = store.addBlock('affine:callout', {}, parent, index + 1);
if (!calloutId) return;
const paragraphId = store.addBlock('affine:paragraph', {}, calloutId);
if (!paragraphId) return;
std.host.updateComplete
.then(() => {
const paragraph = std.view.getBlock(paragraphId);
if (!paragraph) return;
std.command.exec(focusBlockEnd, {
focusBlock: paragraph,
});
})
.catch(console.error);
},
},
];
const listToolActionItems: KeyboardToolbarActionItem[] = [

View File

@@ -17,6 +17,7 @@
{ "path": "../../blocks/paragraph" },
{ "path": "../../blocks/surface" },
{ "path": "../../blocks/surface-ref" },
{ "path": "../../blocks/table" },
{ "path": "../../components" },
{ "path": "../../ext-loader" },
{ "path": "../../fragments/doc-title" },

View File

@@ -343,7 +343,18 @@ export class LinkedDocPopover extends SignalWatcher(
override willUpdate() {
if (!this.hasUpdated) {
const updatePosition = throttle(() => {
this._position = getPopperPosition(this, this.context.startNativeRange);
this._position = getPopperPosition(
{
getBoundingClientRect: () => {
return {
...this.getBoundingClientRect(),
// Workaround: the width of the popover is zero when it is not rendered
width: 280,
};
},
},
this.context.startNativeRange
);
}, 10);
this.disposables.addFromEvent(window, 'resize', updatePosition);

View File

@@ -30,9 +30,9 @@ function inlineTextStyles(
}
return styleMap({
'font-weight': props.bold ? 'bold' : 'normal',
'font-style': props.italic ? 'italic' : 'normal',
'text-decoration': textDecorations.length > 0 ? textDecorations : 'none',
'font-weight': props.bold ? 'bold' : 'inherit',
'font-style': props.italic ? 'italic' : 'inherit',
'text-decoration': textDecorations.length > 0 ? textDecorations : 'inherit',
...inlineCodeStyle,
});
}

View File

@@ -372,3 +372,75 @@ Generated by [AVA](https://avajs.dev).
[assistant]: Quantum computing uses quantum mechanics principles.`,
promptName: 'Summary as title',
}
## should handle copilot cron jobs correctly
> daily job scheduling calls
[
{
args: [
'copilot.session.cleanupEmptySessions',
{},
{
jobId: 'daily-copilot-cleanup-empty-sessions',
},
],
},
{
args: [
'copilot.session.generateMissingTitles',
{},
{
jobId: 'daily-copilot-generate-missing-titles',
},
],
},
{
args: [
'copilot.workspace.cleanupTrashedDocEmbeddings',
{},
{
jobId: 'daily-copilot-cleanup-trashed-doc-embeddings',
},
],
},
]
> cleanup empty sessions calls
[
{
args: [
'Date',
],
},
]
> title generation calls
{
jobCalls: [
{
args: [
'copilot.session.generateTitle',
{
sessionId: 'session1',
},
],
},
{
args: [
'copilot.session.generateTitle',
{
sessionId: 'session2',
},
],
},
],
modelCalls: [
{
args: [],
},
],
}

View File

@@ -207,6 +207,7 @@ const retry = async (
try {
await callback(t);
} catch (e) {
console.error(`Error during ${action}:`, e);
t.log(`Error during ${action}:`, e);
throw e;
}
@@ -350,10 +351,10 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
params: {
files: [
{
blobId: 'euclidean_distance',
fileName: 'euclidean_distance.rs',
fileType: 'text/rust',
fileContent: TestAssets.Code,
blobId: 'todo_md',
fileName: 'todo.md',
fileType: 'text/markdown',
fileContent: TestAssets.TODO,
},
],
},
@@ -475,6 +476,7 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
},
},
],
config: { model: 'gemini-2.5-pro' },
verifier: (t: ExecutionContext<Tester>, result: string) => {
t.notThrows(() => {
TranscriptionResponseSchema.parse(JSON.parse(result));
@@ -483,6 +485,34 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
type: 'structured' as const,
prefer: CopilotProviderType.Gemini,
},
{
promptName: ['Conversation Summary'],
messages: [
{
role: 'user' as const,
content: '',
params: {
messages: [
{ role: 'user', content: 'what is single source of truth?' },
{ role: 'assistant', content: TestAssets.SSOT },
],
focus: 'technical decisions',
length: 'comprehensive',
},
},
],
verifier: (t: ExecutionContext<Tester>, result: string) => {
assertNotWrappedInCodeBlock(t, result);
const cleared = result.toLowerCase();
t.assert(
cleared.includes('single source of truth') ||
/single.*source/.test(cleared) ||
cleared.includes('ssot'),
'should include original keyword'
);
},
type: 'text' as const,
},
{
promptName: [
'Summary',
@@ -668,11 +698,12 @@ for (const {
t.truthy(provider, 'should have provider');
await retry(`action: ${promptName}`, t, async t => {
const finalConfig = Object.assign({}, prompt.config, config);
const modelId = finalConfig.model || prompt.model;
switch (type) {
case 'text': {
const result = await provider.text(
{ modelId: prompt.model },
{ modelId },
[
...prompt.finish(
messages.reduce(
@@ -691,7 +722,7 @@ for (const {
}
case 'structured': {
const result = await provider.structure(
{ modelId: prompt.model },
{ modelId },
[
...prompt.finish(
messages.reduce(
@@ -710,7 +741,7 @@ for (const {
case 'object': {
const streamObjects: StreamObject[] = [];
for await (const chunk of provider.streamObject(
{ modelId: prompt.model },
{ modelId },
[
...prompt.finish(
messages.reduce(
@@ -742,7 +773,7 @@ for (const {
});
}
const stream = provider.streamImages(
{ modelId: prompt.model },
{ modelId },
[
...prompt.finish(
finalMessage.reduce(

View File

@@ -290,6 +290,7 @@ test('should fork session correctly', async t => {
const assertForkSession = async (
workspaceId: string,
docId: string,
sessionId: string,
lastMessageId: string | undefined,
error: string,
@@ -300,13 +301,7 @@ test('should fork session correctly', async t => {
}
) =>
await asserter(
forkCopilotSession(
app,
workspaceId,
randomUUID(),
sessionId,
lastMessageId
)
forkCopilotSession(app, workspaceId, docId, sessionId, lastMessageId)
);
// prepare session
@@ -330,6 +325,7 @@ test('should fork session correctly', async t => {
// should be able to fork session
forkedSessionId = await assertForkSession(
id,
docId,
sessionId,
latestMessageId!,
'should be able to fork session with cloud workspace that user can access'
@@ -340,6 +336,7 @@ test('should fork session correctly', async t => {
{
forkedSessionId = await assertForkSession(
id,
docId,
sessionId,
undefined,
'should be able to fork session without latestMessageId'
@@ -348,18 +345,25 @@ test('should fork session correctly', async t => {
// should not be able to fork session with wrong latestMessageId
{
await assertForkSession(id, sessionId, 'wrong-message-id', '', async x => {
await t.throwsAsync(
x,
{ instanceOf: Error },
'should not able to fork session with wrong latestMessageId'
);
});
await assertForkSession(
id,
docId,
sessionId,
'wrong-message-id',
'',
async x => {
await t.throwsAsync(
x,
{ instanceOf: Error },
'should not able to fork session with wrong latestMessageId'
);
}
);
}
{
const u2 = await app.signupV1();
await assertForkSession(id, sessionId, randomUUID(), '', async x => {
await assertForkSession(id, docId, sessionId, randomUUID(), '', async x => {
await t.throwsAsync(
x,
{ instanceOf: Error },
@@ -371,7 +375,7 @@ test('should fork session correctly', async t => {
const inviteId = await inviteUser(app, id, u2.email);
await app.switchUser(u2);
await acceptInviteById(app, id, inviteId, false);
await assertForkSession(id, sessionId, randomUUID(), '', async x => {
await assertForkSession(id, docId, sessionId, randomUUID(), '', async x => {
await t.throwsAsync(
x,
{ instanceOf: Error },
@@ -389,6 +393,7 @@ test('should fork session correctly', async t => {
await app.switchUser(u2);
await assertForkSession(
id,
docId,
forkedSessionId,
latestMessageId!,
'should able to fork a forked session created by other user'
@@ -456,6 +461,29 @@ test('should create message correctly', async t => {
sessionId,
undefined,
undefined,
new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' })
);
t.truthy(messageId, 'should be able to create message with blob');
}
// with attachments
{
const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession(
app,
id,
randomUUID(),
textPromptName
);
const smallestPng =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
const pngData = await fetch(smallestPng).then(res => res.arrayBuffer());
const messageId = await createCopilotMessage(
app,
sessionId,
undefined,
undefined,
undefined,
[new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' })]
);
t.truthy(messageId, 'should be able to create message with blobs');

View File

@@ -18,6 +18,7 @@ import {
} from '../models';
import { CopilotModule } from '../plugins/copilot';
import { CopilotContextService } from '../plugins/copilot/context';
import { CopilotCronJobs } from '../plugins/copilot/cron';
import {
CopilotEmbeddingJob,
MockEmbeddingClient,
@@ -77,6 +78,7 @@ type Context = {
jobs: CopilotEmbeddingJob;
storage: CopilotStorage;
workflow: CopilotWorkflowService;
cronJobs: CopilotCronJobs;
executors: {
image: CopilotChatImageExecutor;
text: CopilotChatTextExecutor;
@@ -137,6 +139,7 @@ test.before(async t => {
const jobs = module.get(CopilotEmbeddingJob);
const transcript = module.get(CopilotTranscriptionService);
const workspaceEmbedding = module.get(CopilotWorkspaceService);
const cronJobs = module.get(CopilotCronJobs);
t.context.module = module;
t.context.auth = auth;
@@ -153,6 +156,7 @@ test.before(async t => {
t.context.jobs = jobs;
t.context.transcript = transcript;
t.context.workspaceEmbedding = workspaceEmbedding;
t.context.cronJobs = cronJobs;
t.context.executors = {
image: module.get(CopilotChatImageExecutor),
@@ -1931,3 +1935,71 @@ test('should handle generateSessionTitle correctly under various conditions', as
);
}
});
test('should handle copilot cron jobs correctly', async t => {
const { cronJobs, copilotSession } = t.context;
// mock calls
const mockCleanupResult = { removed: 2, cleaned: 3 };
const mockSessions = [
{ id: 'session1', _count: { messages: 1 } },
{ id: 'session2', _count: { messages: 2 } },
];
const cleanupStub = Sinon.stub(
copilotSession,
'cleanupEmptySessions'
).resolves(mockCleanupResult);
const toBeGenerateStub = Sinon.stub(
copilotSession,
'toBeGenerateTitle'
).resolves(mockSessions);
const jobAddStub = Sinon.stub(cronJobs['jobs'], 'add').resolves();
// daily cleanup job scheduling
{
await cronJobs.dailyCleanupJob();
t.snapshot(
jobAddStub.getCalls().map(call => ({
args: call.args,
})),
'daily job scheduling calls'
);
jobAddStub.reset();
cleanupStub.reset();
toBeGenerateStub.reset();
}
// cleanup empty sessions
{
// mock
cleanupStub.resolves(mockCleanupResult);
toBeGenerateStub.resolves(mockSessions);
await cronJobs.cleanupEmptySessions();
t.snapshot(
cleanupStub.getCalls().map(call => ({
args: call.args.map(arg => (arg instanceof Date ? 'Date' : arg)), // Replace Date with string for stable snapshot
})),
'cleanup empty sessions calls'
);
}
// generate missing titles
await cronJobs.generateMissingTitles();
t.snapshot(
{
modelCalls: toBeGenerateStub.getCalls().map(call => ({
args: call.args,
})),
jobCalls: jobAddStub.getCalls().map(call => ({
args: call.args,
})),
},
'title generation calls'
);
cleanupStub.restore();
toBeGenerateStub.restore();
jobAddStub.restore();
});

View File

@@ -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.',
}

View File

@@ -99,3 +99,56 @@ e2e(
t.is(result2.workspace.doc.public, true);
}
);
e2e('should get doc with title and summary', async t => {
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
const docSnapshot = await app.create(Mockers.DocSnapshot, {
workspaceId: workspace.id,
user: owner,
});
const doc = await app.create(Mockers.DocMeta, {
workspaceId: workspace.id,
docId: docSnapshot.id,
title: 'doc1',
summary: 'summary1',
});
const result = await app.gql({
query: getWorkspacePageByIdQuery,
variables: { workspaceId: workspace.id, pageId: doc.docId },
});
t.is(result.workspace.doc.title, doc.title);
t.is(result.workspace.doc.summary, doc.summary);
});
e2e('should get doc with title and null summary', async t => {
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
const docSnapshot = await app.create(Mockers.DocSnapshot, {
workspaceId: workspace.id,
user: owner,
});
const doc = await app.create(Mockers.DocMeta, {
workspaceId: workspace.id,
docId: docSnapshot.id,
title: 'doc1',
});
const result = await app.gql({
query: getWorkspacePageByIdQuery,
variables: { workspaceId: workspace.id, pageId: doc.docId },
});
t.is(result.workspace.doc.title, doc.title);
t.is(result.workspace.doc.summary, null);
});

View File

@@ -111,6 +111,19 @@ export class MockCopilotProvider extends OpenAIProvider {
},
],
},
{
id: 'gemini-2.5-pro',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [
ModelOutputType.Text,
ModelOutputType.Object,
ModelOutputType.Structured,
],
},
],
},
];
override async text(

View File

@@ -565,3 +565,65 @@ Generated by [AVA](https://avajs.dev).
workspaceSessionExists: true,
},
}
## should cleanup empty sessions correctly
> cleanup empty sessions results
{
cleanupResult: {
cleaned: 0,
removed: 0,
},
remainingSessions: [
{
deleted: false,
pinned: false,
type: 'zeroCost',
},
{
deleted: false,
pinned: false,
type: 'zeroCost',
},
{
deleted: false,
pinned: false,
type: 'noMessages',
},
{
deleted: false,
pinned: false,
type: 'noMessages',
},
{
deleted: false,
pinned: false,
type: 'recent',
},
{
deleted: false,
pinned: false,
type: 'withMessages',
},
],
}
## should get sessions for title generation correctly
> sessions for title generation results
{
onlyValidSessionsReturned: true,
sessions: [
{
assistantMessageCount: 1,
isValid: true,
},
{
assistantMessageCount: 2,
isValid: true,
},
],
total: 2,
}

View File

@@ -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,
}

View File

@@ -164,11 +164,14 @@ test('should insert embedding by doc id', async t => {
);
{
const ret = await t.context.copilotContext.hasWorkspaceEmbedding(
const ret = await t.context.copilotContext.listWorkspaceEmbedding(
workspace.id,
[docId]
);
t.true(ret.has(docId), 'should return doc id when embedding is inserted');
t.true(
ret.includes(docId),
'should return doc id when embedding is inserted'
);
}
{
@@ -317,8 +320,8 @@ test('should merge doc status correctly', async t => {
const hasEmbeddingStub = Sinon.stub(
t.context.copilotContext,
'hasWorkspaceEmbedding'
).resolves(new Set<string>());
'listWorkspaceEmbedding'
).resolves([]);
const stubResult = await t.context.copilotContext.mergeDocStatus(
workspace.id,

View File

@@ -917,3 +917,178 @@ test('should handle fork and session attachment operations', async t => {
'attach and detach operation results'
);
});
test('should cleanup empty sessions correctly', async t => {
const { copilotSession, db } = t.context;
await createTestPrompts(copilotSession, db);
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
// should be deleted
const neverUsedSessionIds: string[] = [randomUUID(), randomUUID()];
await Promise.all(
neverUsedSessionIds.map(async id => {
await createTestSession(t, { sessionId: id });
await db.aiSession.update({
where: { id },
data: { messageCost: 0, updatedAt: oneDayAgo },
});
})
);
// should be marked as deleted
const emptySessionIds: string[] = [randomUUID(), randomUUID()];
await Promise.all(
emptySessionIds.map(async id => {
await createTestSession(t, { sessionId: id });
await db.aiSession.update({
where: { id },
data: { messageCost: 100, updatedAt: oneDayAgo },
});
})
);
// should not be affected
const recentSessionId = randomUUID();
await createTestSession(t, { sessionId: recentSessionId });
await db.aiSession.update({
where: { id: recentSessionId },
data: { messageCost: 0, updatedAt: twoHoursAgo },
});
// Create session with messages (should not be affected)
const sessionWithMsgId = randomUUID();
await createSessionWithMessages(
t,
{ sessionId: sessionWithMsgId },
'test message'
);
const result = await copilotSession.cleanupEmptySessions(oneDayAgo);
const remainingSessions = await db.aiSession.findMany({
where: {
id: {
in: [
...neverUsedSessionIds,
...emptySessionIds,
recentSessionId,
sessionWithMsgId,
],
},
},
select: { id: true, deletedAt: true, pinned: true },
});
t.snapshot(
{
cleanupResult: result,
remainingSessions: remainingSessions.map(s => ({
deleted: !!s.deletedAt,
pinned: s.pinned,
type: neverUsedSessionIds.includes(s.id)
? 'zeroCost'
: emptySessionIds.includes(s.id)
? 'noMessages'
: s.id === recentSessionId
? 'recent'
: 'withMessages',
})),
},
'cleanup empty sessions results'
);
});
test('should get sessions for title generation correctly', async t => {
const { copilotSession, db } = t.context;
await createTestPrompts(copilotSession, db);
// create valid sessions with messages
const sessionIds: string[] = [randomUUID(), randomUUID()];
await Promise.all(
sessionIds.map(async (id, index) => {
await createTestSession(t, { sessionId: id });
await db.aiSession.update({
where: { id },
data: {
updatedAt: new Date(Date.now() - index * 1000),
messages: {
create: Array.from({ length: index + 1 }, (_, i) => ({
role: 'assistant',
content: `assistant message ${i}`,
})),
},
},
});
})
);
// create excluded sessions
const excludedSessions = [
{
reason: 'hasTitle',
setupFn: async (id: string) => {
await createTestSession(t, { sessionId: id });
await db.aiSession.update({
where: { id },
data: { title: 'Existing Title' },
});
},
},
{
reason: 'isDeleted',
setupFn: async (id: string) => {
await createTestSession(t, { sessionId: id });
await db.aiSession.update({
where: { id },
data: { deletedAt: new Date() },
});
},
},
{
reason: 'noMessages',
setupFn: async (id: string) => {
await createTestSession(t, { sessionId: id });
},
},
{
reason: 'isAction',
setupFn: async (id: string) => {
await createTestSession(t, {
sessionId: id,
promptName: TEST_PROMPTS.ACTION,
});
},
},
{
reason: 'noAssistantMessages',
setupFn: async (id: string) => {
await createTestSession(t, { sessionId: id });
await db.aiSessionMessage.create({
data: { sessionId: id, role: 'user', content: 'User message only' },
});
},
},
];
await Promise.all(
excludedSessions.map(async session => {
await session.setupFn(randomUUID());
})
);
const result = await copilotSession.toBeGenerateTitle();
t.snapshot(
{
total: result.length,
sessions: result.map(s => ({
assistantMessageCount: s._count.messages,
isValid: sessionIds.includes(s.id),
})),
onlyValidSessionsReturned: result.every(s => sessionIds.includes(s.id)),
},
'sessions for title generation results'
);
});

View File

@@ -214,6 +214,21 @@ test('should insert and search embedding', async t => {
);
t.false(results.includes(docId), 'docs containing `$` should be excluded');
}
{
const docId = 'empty_doc';
await t.context.doc.upsert({
spaceId: workspace.id,
docId: docId,
blob: Uint8Array.from([0, 0]),
timestamp: Date.now(),
editorId: user.id,
});
const results = await t.context.copilotWorkspace.findDocsToEmbed(
workspace.id
);
t.false(results.includes(docId), 'empty documents should be excluded');
}
});
test('should check need to be embedded', async t => {
@@ -291,3 +306,50 @@ test('should check embedding table', async t => {
// t.false(ret, 'should return false when embedding table is not available');
// }
});
test('should filter outdated doc id style in embedding status', async t => {
const docId = randomUUID();
const outdatedDocId = `${workspace.id}:space:${docId}`;
await t.context.doc.upsert({
spaceId: workspace.id,
docId,
blob: Uint8Array.from([1, 2, 3]),
timestamp: Date.now(),
editorId: user.id,
});
await t.context.doc.upsert({
spaceId: workspace.id,
docId: outdatedDocId,
blob: Uint8Array.from([1, 2, 3]),
timestamp: Date.now(),
editorId: user.id,
});
{
const status = await t.context.copilotWorkspace.getEmbeddingStatus(
workspace.id
);
t.snapshot(status, 'should include modern doc format');
}
{
await t.context.copilotContext.insertWorkspaceEmbedding(
workspace.id,
docId,
[
{
index: 0,
content: 'content',
embedding: Array.from({ length: 1024 }, () => 1),
},
]
);
const status = await t.context.copilotWorkspace.getEmbeddingStatus(
workspace.id
);
t.snapshot(status, 'should count docs after filtering outdated');
}
});

View File

@@ -669,7 +669,10 @@ test('should get doc info', async t => {
};
await t.context.doc.upsert(snapshot);
await t.context.doc.upsertMeta(workspace.id, docId);
await t.context.doc.upsertMeta(workspace.id, docId, {
title: 'test title',
summary: 'test summary',
});
const docInfo = await t.context.doc.getDocInfo(workspace.id, docId);
@@ -679,6 +682,8 @@ test('should get doc info', async t => {
updatedAt: new Date(snapshot.timestamp),
creatorId: user.id,
lastUpdaterId: user.id,
title: 'test title',
summary: 'test summary',
});
});

View File

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

View File

@@ -36,6 +36,7 @@ import { DocRendererModule } from './core/doc-renderer';
import { DocServiceModule } from './core/doc-service';
import { FeatureModule } from './core/features';
import { MailModule } from './core/mail';
import { MonitorModule } from './core/monitor';
import { NotificationModule } from './core/notification';
import { PermissionModule } from './core/permission';
import { QuotaModule } from './core/quota';
@@ -112,6 +113,8 @@ export const FunctionalityModules = [
WebSocketModule,
JobModule.forRoot(),
ModelsModule,
ScheduleModule.forRoot(),
MonitorModule,
];
export class AppModuleBuilder {
@@ -151,12 +154,8 @@ export function buildAppModule(env: Env) {
// basic
.use(...FunctionalityModules)
// enable schedule module on graphql server and doc service
.useIf(
() => env.flavors.graphql || env.flavors.doc,
ScheduleModule.forRoot(),
IndexerModule
)
// enable indexer module on graphql server and doc service
.useIf(() => env.flavors.graphql || env.flavors.doc, IndexerModule)
// auth
.use(UserModule, AuthModule, PermissionModule)

View File

@@ -653,12 +653,19 @@ export const USER_FRIENDLY_ERRORS = {
},
no_copilot_provider_available: {
type: 'internal_server_error',
message: `No copilot provider available.`,
args: { modelId: 'string' },
message: ({ modelId }) => `No copilot provider available: ${modelId}`,
},
copilot_failed_to_generate_text: {
type: 'internal_server_error',
message: `Failed to generate text.`,
},
copilot_failed_to_generate_embedding: {
type: 'internal_server_error',
args: { provider: 'string', message: 'string' },
message: ({ provider, message }) =>
`Failed to generate embedding with ${provider}: ${message}`,
},
copilot_failed_to_create_message: {
type: 'internal_server_error',
message: `Failed to create chat message.`,

View File

@@ -668,10 +668,14 @@ export class CopilotSessionDeleted extends UserFriendlyError {
super('action_forbidden', 'copilot_session_deleted', message);
}
}
@ObjectType()
class NoCopilotProviderAvailableDataType {
@Field() modelId!: string
}
export class NoCopilotProviderAvailable extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'no_copilot_provider_available', message);
constructor(args: NoCopilotProviderAvailableDataType, message?: string | ((args: NoCopilotProviderAvailableDataType) => string)) {
super('internal_server_error', 'no_copilot_provider_available', message, args);
}
}
@@ -680,6 +684,17 @@ export class CopilotFailedToGenerateText extends UserFriendlyError {
super('internal_server_error', 'copilot_failed_to_generate_text', message);
}
}
@ObjectType()
class CopilotFailedToGenerateEmbeddingDataType {
@Field() provider!: string
@Field() message!: string
}
export class CopilotFailedToGenerateEmbedding extends UserFriendlyError {
constructor(args: CopilotFailedToGenerateEmbeddingDataType, message?: string | ((args: CopilotFailedToGenerateEmbeddingDataType) => string)) {
super('internal_server_error', 'copilot_failed_to_generate_embedding', message, args);
}
}
export class CopilotFailedToCreateMessage extends UserFriendlyError {
constructor(message?: string) {
@@ -1179,6 +1194,7 @@ export enum ErrorNames {
COPILOT_SESSION_DELETED,
NO_COPILOT_PROVIDER_AVAILABLE,
COPILOT_FAILED_TO_GENERATE_TEXT,
COPILOT_FAILED_TO_GENERATE_EMBEDDING,
COPILOT_FAILED_TO_CREATE_MESSAGE,
UNSPLASH_IS_NOT_CONFIGURED,
COPILOT_ACTION_TAKEN,
@@ -1239,5 +1255,5 @@ registerEnumType(ErrorNames, {
export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion',
types: () =>
[GraphqlBadRequestDataType, HttpRequestErrorDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidOauthResponseDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderNotSupportedDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, CopilotFailedToMatchGlobalContextDataType, CopilotFailedToAddWorkspaceFileEmbeddingDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseToActivateDataType, InvalidLicenseUpdateParamsDataType, UnsupportedClientVersionDataType, MentionUserDocAccessDeniedDataType, InvalidAppConfigDataType, InvalidAppConfigInputDataType, InvalidSearchProviderRequestDataType, InvalidIndexerInputDataType] as const,
[GraphqlBadRequestDataType, HttpRequestErrorDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidOauthResponseDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, NoCopilotProviderAvailableDataType, CopilotFailedToGenerateEmbeddingDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderNotSupportedDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, CopilotFailedToMatchGlobalContextDataType, CopilotFailedToAddWorkspaceFileEmbeddingDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseToActivateDataType, InvalidLicenseUpdateParamsDataType, UnsupportedClientVersionDataType, MentionUserDocAccessDeniedDataType, InvalidAppConfigDataType, InvalidAppConfigInputDataType, InvalidSearchProviderRequestDataType, InvalidIndexerInputDataType] as const,
});

View File

@@ -59,7 +59,9 @@ export type KnownMetricScopes =
| 'mail'
| 'ai'
| 'event'
| 'queue';
| 'queue'
| 'storage'
| 'process';
const metricCreators: MetricCreators = {
counter(meter: Meter, name: string, opts?: MetricOptions) {

View File

@@ -1,3 +1,5 @@
import { setTimeout } from 'node:timers/promises';
import { defer as rxjsDefer, retry } from 'rxjs';
export class RetryablePromise<T> extends Promise<T> {
@@ -48,3 +50,7 @@ export function defer(dispose: () => Promise<void>) {
[Symbol.asyncDispose]: dispose,
};
}
export function sleep(ms: number): Promise<void> {
return setTimeout(ms);
}

View File

@@ -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.',
}

View File

@@ -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.',
}

View File

@@ -100,7 +100,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
{
// keep it simple to let all update merged in one job
jobId: `doc:merge-pending-updates:${workspaceId}:${docId}`,
delay: 30 * 1000 /* 30s */,
delay: 5 * 1000 /* 5s */,
priority: 100,
}
);

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { getStreamAsBuffer } from 'get-stream';
import { JOB_SIGNAL, OnJob } from '../../base';
import { JOB_SIGNAL, OnJob, sleep } from '../../base';
import { type MailName, MailProps, Renderers } from '../../mails';
import { UserProps, WorkspaceProps } from '../../mails/components';
import { Models } from '../../models';
@@ -34,7 +34,7 @@ type SendMailJob<Mail extends MailName = MailName, Props = MailProps<Mail>> = {
declare global {
interface Jobs {
'notification.sendMail': {
'notification.sendMail': { startTime: number } & {
[K in MailName]: SendMailJob<K>;
}[MailName];
}
@@ -50,7 +50,12 @@ export class MailJob {
) {}
@OnJob('notification.sendMail')
async sendMail({ name, to, props }: Jobs['notification.sendMail']) {
async sendMail({
startTime,
name,
to,
props,
}: Jobs['notification.sendMail']) {
let options: Partial<SendOptions> = {};
for (const key in props) {
@@ -100,8 +105,15 @@ export class MailJob {
)),
...options,
});
if (result === false) {
// wait for a while before retrying
const elapsed = Date.now() - startTime;
const retryDelay = Math.min(30 * 1000, Math.round(elapsed / 2000) * 1000);
await sleep(retryDelay);
return JOB_SIGNAL.Retry;
}
return result === false ? JOB_SIGNAL.Retry : undefined;
return undefined;
}
private async fetchWorkspaceProps(workspaceId: string) {

View File

@@ -15,11 +15,14 @@ export class Mailer {
*
* @note never throw
*/
async trySend(command: Jobs['notification.sendMail']) {
async trySend(command: Omit<Jobs['notification.sendMail'], 'startTime'>) {
return this.send(command, true);
}
async send(command: Jobs['notification.sendMail'], suppressError = false) {
async send(
command: Omit<Jobs['notification.sendMail'], 'startTime'>,
suppressError = false
) {
if (!this.sender.configured) {
if (suppressError) {
return false;
@@ -28,7 +31,12 @@ export class Mailer {
}
try {
await this.queue.add('notification.sendMail', command);
await this.queue.add(
'notification.sendMail',
Object.assign({}, command, {
startTime: Date.now(),
}) as Jobs['notification.sendMail']
);
return true;
} catch {
return false;

View File

@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { MonitorService } from './service';
@Global()
@Module({
providers: [MonitorService],
})
export class MonitorModule {}

View File

@@ -0,0 +1,28 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { metrics } from '../../base';
@Injectable()
export class MonitorService {
protected logger = new Logger(MonitorService.name);
@Cron(CronExpression.EVERY_MINUTE)
async monitor() {
const memoryUsage = process.memoryUsage();
this.logger.log(
`memory usage: rss: ${memoryUsage.rss}, heapTotal: ${memoryUsage.heapTotal}, heapUsed: ${memoryUsage.heapUsed}, external: ${memoryUsage.external}, arrayBuffers: ${memoryUsage.arrayBuffers}`
);
metrics.process.gauge('node_process_rss').record(memoryUsage.rss);
metrics.process
.gauge('node_process_heap_total')
.record(memoryUsage.heapTotal);
metrics.process
.gauge('node_process_heap_used')
.record(memoryUsage.heapUsed);
metrics.process.gauge('node_process_external').record(memoryUsage.external);
metrics.process
.gauge('node_process_array_buffers')
.record(memoryUsage.arrayBuffers);
}
}

View File

@@ -4,6 +4,7 @@ import {
autoMetadata,
Config,
EventBus,
metrics,
OnEvent,
type StorageProvider,
StorageProviderFactory,
@@ -69,15 +70,23 @@ export class CommentAttachmentStorage {
blob,
meta
);
const mime = meta.contentType ?? 'application/octet-stream';
const size = blob.length;
await this.models.commentAttachment.upsert({
workspaceId,
docId,
key,
name,
mime: meta.contentType ?? 'application/octet-stream',
size: blob.length,
mime,
size,
createdBy: userId,
});
metrics.storage.histogram('comment_attachment_size').record(size, { mime });
metrics.storage.counter('comment_attachment_total').add(1, { mime });
this.logger.log(
`uploaded comment attachment ${workspaceId}/${docId}/${key} with size ${size}, mime: ${mime}, name: ${name}, user: ${userId}`
);
}
async get(

View File

@@ -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.',
}

View File

@@ -79,6 +79,9 @@ class DocType {
@Field(() => String, { nullable: true })
title?: string | null;
@Field(() => String, { nullable: true })
summary?: string | null;
}
@InputType()
@@ -250,10 +253,11 @@ export class WorkspaceDocResolver {
deprecationReason: 'use [WorkspaceType.doc] instead',
})
async publicPage(
@CurrentUser() me: CurrentUser,
@Parent() workspace: WorkspaceType,
@Args('pageId') pageId: string
) {
return this.doc(workspace, pageId);
return this.doc(me, workspace, pageId);
}
@ResolveField(() => PaginatedDocType)
@@ -294,11 +298,14 @@ export class WorkspaceDocResolver {
complexity: 2,
})
async doc(
@CurrentUser() me: CurrentUser,
@Parent() workspace: WorkspaceType,
@Args('docId') docId: string
): Promise<DocType> {
const doc = await this.models.doc.getDocInfo(workspace.id, docId);
if (doc) {
// check if doc is readable
await this.ac.user(me.id).doc(workspace.id, docId).assert('Doc.Read');
return doc;
}

View File

@@ -84,11 +84,17 @@ export class CopilotContextModel extends BaseModel {
}
async mergeDocStatus(workspaceId: string, docs: ContextDoc[]) {
const docIds = Array.from(new Set(docs.map(doc => doc.id)));
const finishedDoc = await this.hasWorkspaceEmbedding(workspaceId, docIds);
const canEmbedding = await this.checkEmbeddingAvailable();
const finishedDoc = canEmbedding
? await this.listWorkspaceEmbedding(
workspaceId,
Array.from(new Set(docs.map(doc => doc.id)))
)
: [];
const finishedDocSet = new Set(finishedDoc);
for (const doc of docs) {
const status = finishedDoc.has(doc.id)
const status = finishedDocSet.has(doc.id)
? ContextEmbedStatus.finished
: undefined;
// NOTE: when the document has not been synchronized to the server or is in the embedding queue
@@ -120,24 +126,17 @@ export class CopilotContextModel extends BaseModel {
return Number(count) === 2;
}
async hasWorkspaceEmbedding(workspaceId: string, docIds: string[]) {
const canEmbedding = await this.checkEmbeddingAvailable();
if (!canEmbedding) {
return new Set();
}
async listWorkspaceEmbedding(workspaceId: string, docIds?: string[]) {
const existsIds = await this.db.aiWorkspaceEmbedding
.findMany({
.groupBy({
where: {
workspaceId,
docId: { in: docIds },
},
select: {
docId: true,
docId: docIds ? { in: docIds } : undefined,
},
by: ['docId'],
})
.then(r => r.map(r => r.docId));
return new Set(existsIds);
return existsIds;
}
private processEmbeddings(
@@ -165,6 +164,13 @@ export class CopilotContextModel extends BaseModel {
fileId: string,
embeddings: Embedding[]
) {
if (embeddings.length === 0) {
this.logger.warn(
`No embeddings provided for contextId: ${contextId}, fileId: ${fileId}. Skipping insertion.`
);
return;
}
const values = this.processEmbeddings(contextId, fileId, embeddings);
await this.db.$executeRaw`
@@ -204,6 +210,13 @@ export class CopilotContextModel extends BaseModel {
docId: string,
embeddings: Embedding[]
) {
if (embeddings.length === 0) {
this.logger.warn(
`No embeddings provided for workspaceId: ${workspaceId}, docId: ${docId}. Skipping insertion.`
);
return;
}
const values = this.processEmbeddings(
workspaceId,
docId,

View File

@@ -582,4 +582,56 @@ export class CopilotSessionModel extends BaseModel {
.map(({ messageCost, prompt: { action } }) => (action ? 1 : messageCost))
.reduce((prev, cost) => prev + cost, 0);
}
@Transactional()
async cleanupEmptySessions(earlyThen: Date) {
// delete never used sessions
const { count: removed } = await this.db.aiSession.deleteMany({
where: {
messageCost: 0,
deletedAt: null,
// filter session updated more than 24 hours ago
updatedAt: { lt: earlyThen },
},
});
// mark empty sessions as deleted
const { count: cleaned } = await this.db.aiSession.updateMany({
where: {
deletedAt: null,
messages: { none: {} },
// filter session updated more than 24 hours ago
updatedAt: { lt: earlyThen },
},
data: {
deletedAt: new Date(),
pinned: false,
},
});
return { removed, cleaned };
}
@Transactional()
async toBeGenerateTitle() {
const sessions = await this.db.aiSession
.findMany({
where: {
title: null,
deletedAt: null,
messages: { some: {} },
// only generate titles for non-actions sessions
prompt: { action: null },
},
select: {
id: true,
// count assistant messages
_count: { select: { messages: { where: { role: 'assistant' } } } },
},
orderBy: { updatedAt: 'desc' },
})
.then(s => s.filter(s => s._count.messages > 0));
return sessions;
}
}

View File

@@ -58,10 +58,12 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
ON id.workspace_id = s.workspace_id
AND id.doc_id = s.guid
WHERE s.workspace_id = ${workspaceId}
AND s.guid != s.workspace_id
AND s.guid <> s.workspace_id
AND s.guid NOT LIKE '%$%'
AND s.guid NOT LIKE '%:settings:%'
AND e.doc_id IS NULL
AND id.doc_id IS NULL;`;
AND id.doc_id IS NULL
AND s.blob <> E'\\\\x0000';`;
return docIds.map(r => r.id);
}
@@ -150,7 +152,7 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
}
@Transactional()
async getWorkspaceEmbeddingStatus(workspaceId: string) {
async getEmbeddingStatus(workspaceId: string) {
const ignoredDocIds = (await this.listIgnoredDocIds(workspaceId)).map(
d => d.docId
);
@@ -160,13 +162,19 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
{ id: { notIn: ignoredDocIds } },
{ id: { not: workspaceId } },
{ id: { not: { contains: '$' } } },
{ id: { not: { contains: ':settings:' } } },
{ blob: { not: new Uint8Array([0, 0]) } },
],
};
const [docTotal, docEmbedded, fileTotal, fileEmbedded] = await Promise.all([
this.db.snapshot.count({ where: snapshotCondition }),
this.db.snapshot.count({
this.db.snapshot.findMany({
where: snapshotCondition,
select: { id: true },
}),
this.db.snapshot.findMany({
where: { ...snapshotCondition, embedding: { some: {} } },
select: { id: true },
}),
this.db.aiWorkspaceFiles.count({ where: { workspaceId } }),
this.db.aiWorkspaceFiles.count({
@@ -174,9 +182,23 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
}),
]);
const docTotalIds = docTotal.map(d => d.id);
const docTotalSet = new Set(docTotalIds);
const outdatedDocPrefix = `${workspaceId}:space:`;
const duplicateOutdatedDocSet = new Set(
docTotalIds
.filter(id => id.startsWith(outdatedDocPrefix))
.filter(id => docTotalSet.has(id.slice(outdatedDocPrefix.length)))
);
return {
total: docTotal + fileTotal,
embedded: docEmbedded + fileEmbedded,
total:
docTotalIds.filter(id => !duplicateOutdatedDocSet.has(id)).length +
fileTotal,
embedded:
docEmbedded
.map(d => d.id)
.filter(id => !duplicateOutdatedDocSet.has(id)).length + fileEmbedded,
};
}
@@ -283,6 +305,13 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
fileId: string,
embeddings: Embedding[]
) {
if (embeddings.length === 0) {
this.logger.warn(
`No embeddings provided for workspaceId: ${workspaceId}, fileId: ${fileId}. Skipping insertion.`
);
return;
}
const values = this.processEmbeddings(workspaceId, fileId, embeddings);
await this.db.$executeRaw`
INSERT INTO "ai_workspace_file_embeddings"

View File

@@ -558,6 +558,8 @@ export class DocModel extends BaseModel {
mode: PublicDocMode;
public: boolean;
defaultRole: DocRole;
title: string | null;
summary: string | null;
createdAt: Date;
updatedAt: Date;
creatorId?: string;
@@ -570,6 +572,8 @@ export class DocModel extends BaseModel {
"workspace_pages"."mode" as "mode",
"workspace_pages"."public" as "public",
"workspace_pages"."defaultRole" as "defaultRole",
"workspace_pages"."title" as "title",
"workspace_pages"."summary" as "summary",
"snapshots"."created_at" as "createdAt",
"snapshots"."updated_at" as "updatedAt",
"snapshots"."created_by" as "creatorId",

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Transactional } from '@nestjs-cls/transactional';
import { type Workspace } from '@prisma/client';
import { Prisma, type Workspace } from '@prisma/client';
import { EventBus } from '../base';
import { BaseModel } from './base';
@@ -93,6 +93,19 @@ export class WorkspaceModel extends BaseModel {
});
}
async list<S extends Prisma.WorkspaceSelect>(
where: Prisma.WorkspaceWhereInput = {},
select?: S
) {
return (await this.db.workspace.findMany({
where,
select,
orderBy: {
sid: 'asc',
},
})) as Prisma.WorkspaceGetPayload<{ select: S }>[];
}
async delete(workspaceId: string) {
const rawResult = await this.db.workspace.deleteMany({
where: {

View File

@@ -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 };
}

View File

@@ -125,7 +125,10 @@ export class CopilotContextService implements OnApplicationBootstrap {
async get(id: string): Promise<ContextSession> {
if (!this.embeddingClient) {
throw new NoCopilotProviderAvailable('embedding client not configured');
throw new NoCopilotProviderAvailable(
{ modelId: 'embedding' },
'embedding client not configured'
);
}
const context = await this.getCachedSession(id);

View File

@@ -124,7 +124,7 @@ export class CopilotController implements BeforeApplicationShutdown {
modelId: model,
});
if (!provider) {
throw new NoCopilotProviderAvailable();
throw new NoCopilotProviderAvailable({ modelId: model });
}
return { provider, model, hasAttachment };
@@ -299,6 +299,13 @@ export class CopilotController implements BeforeApplicationShutdown {
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
const { signal, onConnectionClosed } = getSignal(req);
let endBeforePromiseResolve = false;
onConnectionClosed(isAborted => {
if (isAborted) {
endBeforePromiseResolve = true;
}
});
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
const source$ = from(
@@ -322,21 +329,21 @@ export class CopilotController implements BeforeApplicationShutdown {
shared$.pipe(
reduce((acc, chunk) => acc + chunk, ''),
tap(buffer => {
onConnectionClosed(isAborted => {
session.push({
role: 'assistant',
content: isAborted ? '> Request aborted' : buffer,
createdAt: new Date(),
});
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
session.push({
role: 'assistant',
content: endBeforePromiseResolve
? '> Request aborted'
: buffer,
createdAt: new Date(),
});
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}),
ignoreElements()
)
@@ -384,6 +391,13 @@ export class CopilotController implements BeforeApplicationShutdown {
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
const { signal, onConnectionClosed } = getSignal(req);
let endBeforePromiseResolve = false;
onConnectionClosed(isAborted => {
if (isAborted) {
endBeforePromiseResolve = true;
}
});
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
const source$ = from(
@@ -407,25 +421,25 @@ export class CopilotController implements BeforeApplicationShutdown {
shared$.pipe(
reduce((acc, chunk) => acc.concat([chunk]), [] as StreamObject[]),
tap(result => {
onConnectionClosed(isAborted => {
const parser = new StreamObjectParser();
const streamObjects = parser.mergeTextDelta(result);
const content = parser.mergeContent(streamObjects);
session.push({
role: 'assistant',
content: isAborted ? '> Request aborted' : content,
streamObjects: isAborted ? null : streamObjects,
createdAt: new Date(),
});
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
const parser = new StreamObjectParser();
const streamObjects = parser.mergeTextDelta(result);
const content = parser.mergeContent(streamObjects);
session.push({
role: 'assistant',
content: endBeforePromiseResolve
? '> Request aborted'
: content,
streamObjects: endBeforePromiseResolve ? null : streamObjects,
createdAt: new Date(),
});
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}),
ignoreElements()
)
@@ -477,6 +491,13 @@ export class CopilotController implements BeforeApplicationShutdown {
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
const { signal, onConnectionClosed } = getSignal(req);
let endBeforePromiseResolve = false;
onConnectionClosed(isAborted => {
if (isAborted) {
endBeforePromiseResolve = true;
}
});
const source$ = from(
this.workflow.runGraph(params, session.model, {
...session.config.promptConfig,
@@ -526,21 +547,21 @@ export class CopilotController implements BeforeApplicationShutdown {
return acc;
}, ''),
tap(content => {
onConnectionClosed(isAborted => {
session.push({
role: 'assistant',
content: isAborted ? '> Request aborted' : content,
createdAt: new Date(),
});
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
session.push({
role: 'assistant',
content: endBeforePromiseResolve
? '> Request aborted'
: content,
createdAt: new Date(),
});
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}),
ignoreElements()
)
@@ -604,6 +625,13 @@ export class CopilotController implements BeforeApplicationShutdown {
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
const { signal, onConnectionClosed } = getSignal(req);
let endBeforePromiseResolve = false;
onConnectionClosed(isAborted => {
if (isAborted) {
endBeforePromiseResolve = true;
}
});
const source$ = from(
provider.streamImages(
{
@@ -639,22 +667,20 @@ export class CopilotController implements BeforeApplicationShutdown {
shared$.pipe(
reduce((acc, chunk) => acc.concat([chunk]), [] as string[]),
tap(attachments => {
onConnectionClosed(isAborted => {
session.push({
role: 'assistant',
content: isAborted ? '> Request aborted' : '',
attachments: isAborted ? [] : attachments,
createdAt: new Date(),
});
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
session.push({
role: 'assistant',
content: endBeforePromiseResolve ? '> Request aborted' : '',
attachments: endBeforePromiseResolve ? [] : attachments,
createdAt: new Date(),
});
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}),
ignoreElements()
)

View File

@@ -0,0 +1,100 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { JobQueue, OneDay, OnJob } from '../../base';
import { Models } from '../../models';
declare global {
interface Jobs {
'copilot.session.cleanupEmptySessions': {};
'copilot.session.generateMissingTitles': {};
'copilot.workspace.cleanupTrashedDocEmbeddings': {};
}
}
@Injectable()
export class CopilotCronJobs {
private readonly logger = new Logger(CopilotCronJobs.name);
constructor(
private readonly models: Models,
private readonly jobs: JobQueue
) {}
async triggerCleanupTrashedDocEmbeddings() {
await this.jobs.add(
'copilot.workspace.cleanupTrashedDocEmbeddings',
{},
{ jobId: 'daily-copilot-cleanup-trashed-doc-embeddings' }
);
}
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async dailyCleanupJob() {
await this.jobs.add(
'copilot.session.cleanupEmptySessions',
{},
{ jobId: 'daily-copilot-cleanup-empty-sessions' }
);
await this.jobs.add(
'copilot.session.generateMissingTitles',
{},
{ jobId: 'daily-copilot-generate-missing-titles' }
);
await this.jobs.add(
'copilot.workspace.cleanupTrashedDocEmbeddings',
{},
{ jobId: 'daily-copilot-cleanup-trashed-doc-embeddings' }
);
}
async triggerGenerateMissingTitles() {
await this.jobs.add(
'copilot.session.generateMissingTitles',
{},
{ jobId: 'trigger-copilot-generate-missing-titles' }
);
}
@OnJob('copilot.session.cleanupEmptySessions')
async cleanupEmptySessions() {
const { removed, cleaned } =
await this.models.copilotSession.cleanupEmptySessions(
new Date(Date.now() - OneDay)
);
this.logger.log(
`Cleanup completed: ${removed} sessions deleted, ${cleaned} sessions marked as deleted`
);
}
@OnJob('copilot.session.generateMissingTitles')
async generateMissingTitles() {
const sessions = await this.models.copilotSession.toBeGenerateTitle();
for (const session of sessions) {
await this.jobs.add('copilot.session.generateTitle', {
sessionId: session.id,
});
}
this.logger.log(
`Scheduled title generation for ${sessions.length} sessions`
);
}
@OnJob('copilot.workspace.cleanupTrashedDocEmbeddings')
async cleanupTrashedDocEmbeddings() {
const workspaces = await this.models.workspace.list(undefined, {
id: true,
});
for (const { id: workspaceId } of workspaces) {
await this.jobs.add(
'copilot.embedding.cleanupTrashedDocEmbeddings',
{ workspaceId },
{ jobId: `cleanup-trashed-doc-embeddings-${workspaceId}` }
);
}
}
}

View File

@@ -5,6 +5,7 @@ import {
CopilotPromptNotFound,
CopilotProviderNotSupported,
} from '../../../base';
import { CopilotFailedToGenerateEmbedding } from '../../../base/error/errors.gen';
import { ChunkSimilarity, Embedding } from '../../../models';
import { PromptService } from '../prompt';
import {
@@ -74,6 +75,12 @@ class ProductionEmbeddingClient extends EmbeddingClient {
input,
{ dimensions: EMBEDDING_DIMENSIONS }
);
if (embeddings.length !== input.length) {
throw new CopilotFailedToGenerateEmbedding({
provider: provider.type,
message: `Expected ${input.length} embeddings, got ${embeddings.length}`,
});
}
return Array.from(embeddings.entries()).map(([index, embedding]) => ({
index,

View File

@@ -12,6 +12,7 @@ import {
OnJob,
} from '../../../base';
import { DocReader } from '../../../core/doc';
import { readAllDocIdsFromWorkspaceSnapshot } from '../../../core/utils/blocksuite';
import { Models } from '../../../models';
import { CopilotStorage } from '../storage';
import { readStream } from '../utils';
@@ -134,10 +135,30 @@ export class CopilotEmbeddingJob {
if (enableDocEmbedding) {
const toBeEmbedDocIds =
await this.models.copilotWorkspace.findDocsToEmbed(workspaceId);
if (!toBeEmbedDocIds.length) {
return;
}
// filter out trashed docs
const rootSnapshot = await this.models.doc.getSnapshot(
workspaceId,
workspaceId
);
if (!rootSnapshot) {
this.logger.warn(
`Root snapshot for workspace ${workspaceId} not found, skipping embedding.`
);
return;
}
const allDocIds = new Set(
readAllDocIdsFromWorkspaceSnapshot(rootSnapshot.blob)
);
this.logger.log(
`Trigger embedding for ${toBeEmbedDocIds.length} docs in workspace ${workspaceId}`
);
for (const docId of toBeEmbedDocIds) {
const finalToBeEmbedDocIds = toBeEmbedDocIds.filter(docId =>
allDocIds.has(docId)
);
for (const docId of finalToBeEmbedDocIds) {
await this.queue.add(
'copilot.embedding.docs',
{
@@ -337,6 +358,10 @@ export class CopilotEmbeddingJob {
const signal = this.getWorkspaceSignal(workspaceId);
try {
const hasNewDoc = await this.models.doc.exists(
workspaceId,
docId.split(':space:')[1] || ''
);
const needEmbedding =
await this.models.copilotWorkspace.checkDocNeedEmbedded(
workspaceId,
@@ -352,8 +377,11 @@ export class CopilotEmbeddingJob {
);
return;
}
const fragment = await this.getDocFragment(workspaceId, docId);
if (fragment) {
// if doc id deprecated, skip embedding and fulfill empty embedding
const fragment = !hasNewDoc
? await this.getDocFragment(workspaceId, docId)
: undefined;
if (!hasNewDoc && fragment) {
// fast fall for empty doc, journal is easily to create a empty doc
if (fragment.summary.trim()) {
const embeddings = await this.embeddingClient.getFileEmbeddings(
@@ -382,7 +410,7 @@ export class CopilotEmbeddingJob {
);
await this.fulfillEmptyEmbedding(workspaceId, docId);
}
} else if (contextId) {
} else {
this.logger.warn(
`Doc ${docId} in workspace ${workspaceId} has no fragment, fulfilling empty embedding.`
);
@@ -415,4 +443,39 @@ export class CopilotEmbeddingJob {
);
}
}
@OnJob('copilot.embedding.cleanupTrashedDocEmbeddings')
async cleanupTrashedDocEmbeddings({
workspaceId,
}: Jobs['copilot.embedding.cleanupTrashedDocEmbeddings']) {
const workspace = await this.models.workspace.get(workspaceId);
if (!workspace) {
this.logger.warn(`workspace ${workspaceId} not found`);
return;
}
const snapshot = await this.models.doc.getSnapshot(
workspaceId,
workspaceId
);
if (!snapshot) {
this.logger.warn(`workspace snapshot ${workspaceId} not found`);
return;
}
const docIdsInWorkspace = readAllDocIdsFromWorkspaceSnapshot(snapshot.blob);
const docIdsInEmbedding =
await this.models.copilotContext.listWorkspaceEmbedding(workspaceId);
const docIdsInWorkspaceSet = new Set(docIdsInWorkspace);
const deletedDocIds = docIdsInEmbedding.filter(
docId => !docIdsInWorkspaceSet.has(docId)
);
for (const docId of deletedDocIds) {
await this.models.copilotContext.deleteWorkspaceEmbedding(
workspaceId,
docId
);
}
}
}

View File

@@ -61,6 +61,10 @@ declare global {
fileId: string;
fileName: string;
};
'copilot.embedding.cleanupTrashedDocEmbeddings': {
workspaceId: string;
};
}
}

View File

@@ -15,6 +15,7 @@ import {
CopilotContextService,
} from './context';
import { CopilotController } from './controller';
import { CopilotCronJobs } from './cron';
import { CopilotEmbeddingJob } from './embedding';
import { ChatMessageCache } from './message';
import { PromptService } from './prompt';
@@ -63,7 +64,9 @@ import {
// context
CopilotContextResolver,
CopilotContextService,
// jobs
CopilotEmbeddingJob,
CopilotCronJobs,
// transcription
CopilotTranscriptionService,
CopilotTranscriptionResolver,

View File

@@ -304,6 +304,7 @@ const textActions: Prompt[] = [
name: 'Transcript audio',
action: 'Transcript audio',
model: 'gemini-2.5-flash',
optionalModels: ['gemini-2.5-flash', 'gemini-2.5-pro'],
messages: [
{
role: 'system',
@@ -333,6 +334,7 @@ Convert a multi-speaker audio recording into a structured JSON format by transcr
config: {
requireContent: false,
requireAttachment: true,
maxRetries: 1,
},
},
{
@@ -366,6 +368,31 @@ Convert a multi-speaker audio recording into a structured JSON format by transcr
requireAttachment: true,
},
},
{
name: 'Conversation Summary',
action: 'Conversation Summary',
model: 'gpt-4.1-2025-04-14',
messages: [
{
role: 'system',
content: `You are an expert conversation summarizer. Your job is to distill long dialogues into clear, compact summaries that preserve every key decision, fact, and open question. When asked, always:
• Honor any explicit “focus” the user gives you.
• Match the desired length style:
- “brief” → 1-2 sentences
- “detailed” → ≈ 5 sentences or short bullet list
- “comprehensive” → full paragraph(s) covering all salient points.
• Write in neutral, third-person prose and never add new information.
Return only the summary text—no headings, labels, or commentary.`,
},
{
role: 'user',
content: `Summarize the conversation below so it can be carried forward without loss.\n\nFocus: {{focus}}\nDesired length: {{length}}\n\nConversation:\n{{#messages}}\n{{role}}: {{content}}\n{{/messages}}`,
},
],
config: {
requireContent: false,
},
},
{
name: 'Summary',
action: 'Summary',
@@ -1598,6 +1625,166 @@ const imageActions: Prompt[] = [
},
];
const modelActions: Prompt[] = [
{
name: 'Apply Updates',
action: 'Apply Updates',
model: 'claude-sonnet-4@20250514',
messages: [
{
role: 'user',
content: `
You are a Markdown document update engine.
You will be given:
1. content: The original Markdown document
- The content is structured into blocks.
- Each block starts with a comment like <!-- block_id=... flavour=... --> and contains the block's content.
- The content is {{content}}
2. op: A description of the edit intention
- This describes the semantic meaning of the edit, such as "Bold the first paragraph".
- The op is {{op}}
3. updates: A Markdown snippet
- The updates is {{updates}}
- This represents the block-level changes to apply to the original Markdown.
- The update may:
- **Replace** an existing block (same block_id, new content)
- **Delete** block(s) using <!-- delete block BLOCK_ID -->
- **Insert** new block(s) with a new unique block_id
- When performing deletions, the update will include **surrounding context blocks** (or use <!-- existing blocks -->) to help you determine where and what to delete.
Your task:
- Apply the update in <updates> to the document in <code>, following the intent described in <op>.
- Preserve all block_id and flavour comments.
- Maintain the original block order unless the update clearly appends new blocks.
- Do not remove or alter unrelated blocks.
- Output only the fully updated Markdown content. Do not wrap the content in \`\`\`markdown.
---
✍️ Examples
✅ Replacement (modifying an existing block)
<code>
<!-- block_id=101 flavour=paragraph -->
## Introduction
<!-- block_id=102 flavour=paragraph -->
This document provides an overview of the system architecture and its components.
</code>
<op>
Make the introduction more formal.
</op>
<updates>
<!-- block_id=102 flavour=paragraph -->
This document outlines the architectural design and individual components of the system in detail.
</updates>
Expected Output:
<!-- block_id=101 flavour=paragraph -->
## Introduction
<!-- block_id=102 flavour=paragraph -->
This document outlines the architectural design and individual components of the system in detail.
---
Insertion (adding new content)
<code>
<!-- block_id=201 flavour=paragraph -->
# Project Summary
<!-- block_id=202 flavour=paragraph -->
This project aims to build a collaborative text editing tool.
</code>
<op>
Add a disclaimer section at the end.
</op>
<updates>
<!-- block_id=new-301 flavour=paragraph -->
## Disclaimer
<!-- block_id=new-302 flavour=paragraph -->
This document is subject to change. Do not distribute externally.
</updates>
Expected Output:
<!-- block_id=201 flavour=paragraph -->
# Project Summary
<!-- block_id=202 flavour=paragraph -->
This project aims to build a collaborative text editing tool.
<!-- block_id=new-301 flavour=paragraph -->
## Disclaimer
<!-- block_id=new-302 flavour=paragraph -->
This document is subject to change. Do not distribute externally.
---
❌ Deletion (removing blocks)
<code>
<!-- block_id=401 flavour=paragraph -->
## Author
<!-- block_id=402 flavour=paragraph -->
Written by the AI team at OpenResearch.
<!-- block_id=403 flavour=paragraph -->
## Experimental Section
<!-- block_id=404 flavour=paragraph -->
The following section is still under development and may change without notice.
<!-- block_id=405 flavour=paragraph -->
## License
<!-- block_id=406 flavour=paragraph -->
This document is licensed under CC BY-NC 4.0.
</code>
<op>
Remove the experimental section.
</op>
<updates>
<!-- delete block_id=403 -->
<!-- delete block_id=404 -->
</updates>
Expected Output:
<!-- block_id=401 flavour=paragraph -->
## Author
<!-- block_id=402 flavour=paragraph -->
Written by the AI team at OpenResearch.
<!-- block_id=405 flavour=paragraph -->
## License
<!-- block_id=406 flavour=paragraph -->
This document is licensed under CC BY-NC 4.0.
---
Now apply the \`updates\` to the \`content\`, following the intent in \`op\`, and return the updated Markdown.
`,
},
],
},
];
const CHAT_PROMPT: Omit<Prompt, 'name'> = {
model: 'claude-sonnet-4@20250514',
optionalModels: [
@@ -1770,11 +1957,75 @@ const chat: Prompt[] = [
},
];
const artifactActions: Prompt[] = [
{
name: 'Code Artifact',
model: 'claude-sonnet-4@20250514',
messages: [
{
role: 'system',
content: `
When sent new notes, respond ONLY with the contents of the html file.
DO NOT INCLUDE ANY OTHER TEXT, EXPLANATIONS, APOLOGIES, OR INTRODUCTORY/CLOSING PHRASES.
IF USER DOES NOT SPECIFY A STYLE, FOLLOW THE DEFAULT STYLE.
<generate_guide>
- The results should be a single HTML file.
- Use tailwindcss to style the website
- Put any additional CSS styles in a style tag and any JavaScript in a script tag.
- Use unpkg or skypack to import any required dependencies.
- Use Google fonts to pull in any open source fonts you require.
- Use lucide icons for any icons.
- If you have any images, load them from Unsplash or use solid colored rectangles.
</generate_guide>
<DO_NOT_USE_COLORS>
- DO NOT USE ANY COLORS
</DO_NOT_USE_COLORS>
<DO_NOT_USE_GRADIENTS>
- DO NOT USE ANY GRADIENTS
</DO_NOT_USE_GRADIENTS>
<COLOR_THEME>
- --affine-blue-300: #93e2fd
- --affine-blue-400: #60cffa
- --affine-blue-500: #3ab5f7
- --affine-blue-600: #1e96eb
- --affine-blue-700: #1e67af
- --affine-text-primary-color: #121212
- --affine-text-secondary-color: #8e8d91
- --affine-text-disable-color: #a9a9ad
- --affine-background-overlay-panel-color: #fbfbfc
- --affine-background-secondary-color: #f4f4f5
- --affine-background-primary-color: #fff
</COLOR_THEME>
<default_style_guide>
- MUST USE White and Blue(#1e96eb) as the primary color
- KEEP THE DEFAULT STYLE SIMPLE AND CLEAN
- DO NOT USE ANY COMPLEX STYLES
- DO NOT USE ANY GRADIENTS
- USE LESS SHADOWS
- USE RADIUS 4px or 8px for rounded corners
- USE 12px or 16px for padding
- Use the tailwind color gray, zinc, slate, neutral much more.
- Use 0.5px border should be better
</default_style_guide>
`,
},
{
role: 'user',
content: '{{content}}',
},
],
},
];
export const prompts: Prompt[] = [
...textActions,
...imageActions,
...modelActions,
...chat,
...workflows,
...artifactActions,
];
export async function refreshPrompts(db: PrismaClient) {

View File

@@ -129,7 +129,16 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
system,
messages: msgs,
schema,
providerOptions: {
google: {
thinkingConfig: {
thinkingBudget: -1,
includeThoughts: false,
},
},
},
abortSignal: options.signal,
maxRetries: options.maxRetries || 3,
experimental_repairText: async ({ text, error }) => {
if (error instanceof JSONParseError) {
// strange fixed response, temporarily replace it

View File

@@ -37,6 +37,24 @@ export class MorphProvider extends CopilotProvider<MorphConfig> {
},
],
},
{
id: 'morph-v3-fast',
capabilities: [
{
input: [ModelInputType.Text],
output: [ModelOutputType.Text],
},
],
},
{
id: 'morph-v3-large',
capabilities: [
{
input: [ModelInputType.Text],
output: [ModelOutputType.Text],
},
],
},
];
#instance!: VercelOpenAICompatibleProvider;

View File

@@ -21,6 +21,7 @@ import {
buildDocKeywordSearchGetter,
buildDocSearchGetter,
createCodeArtifactTool,
createConversationSummaryTool,
createDocComposeTool,
createDocEditTool,
createDocKeywordSearchTool,
@@ -139,6 +140,11 @@ export abstract class CopilotProvider<C = any> {
if (options?.tools?.length) {
this.logger.debug(`getTools: ${JSON.stringify(options.tools)}`);
const ac = this.moduleRef.get(AccessController, { strict: false });
const docReader = this.moduleRef.get(DocReader, { strict: false });
const models = this.moduleRef.get(Models, { strict: false });
const prompt = this.moduleRef.get(PromptService, {
strict: false,
});
for (const tool of options.tools) {
const toolDef = this.getProviderSpecificTools(tool, model);
@@ -150,11 +156,23 @@ export abstract class CopilotProvider<C = any> {
continue;
}
switch (tool) {
case 'codeArtifact': {
tools.code_artifact = createCodeArtifactTool(prompt, this.factory);
break;
}
case 'conversationSummary': {
tools.conversation_summary = createConversationSummaryTool(
options.session,
prompt,
this.factory
);
break;
}
case 'docEdit': {
const doc = this.moduleRef.get(DocReader, { strict: false });
const getDocContent = buildContentGetter(ac, doc);
const getDocContent = buildContentGetter(ac, docReader);
tools.doc_edit = createDocEditTool(
this.factory,
prompt,
getDocContent.bind(null, options)
);
break;
@@ -163,11 +181,15 @@ export abstract class CopilotProvider<C = any> {
const context = this.moduleRef.get(CopilotContextService, {
strict: false,
});
const docContext = options.session
? await context.getBySessionId(options.session)
: null;
const searchDocs = buildDocSearchGetter(ac, context, docContext);
const searchDocs = buildDocSearchGetter(
ac,
context,
docContext,
models
);
tools.doc_semantic_search = createDocSemanticSearchTool(
searchDocs.bind(null, options)
);
@@ -175,9 +197,6 @@ export abstract class CopilotProvider<C = any> {
}
case 'docKeywordSearch': {
if (this.AFFiNEConfig.indexer.enabled) {
const ac = this.moduleRef.get(AccessController, {
strict: false,
});
const indexerService = this.moduleRef.get(IndexerService, {
strict: false,
});
@@ -192,9 +211,6 @@ export abstract class CopilotProvider<C = any> {
break;
}
case 'docRead': {
const ac = this.moduleRef.get(AccessController, { strict: false });
const models = this.moduleRef.get(Models, { strict: false });
const docReader = this.moduleRef.get(DocReader, { strict: false });
const getDoc = buildDocContentGetter(ac, docReader, models);
tools.doc_read = createDocReadTool(getDoc.bind(null, options));
break;
@@ -205,23 +221,7 @@ export abstract class CopilotProvider<C = any> {
break;
}
case 'docCompose': {
const promptService = this.moduleRef.get(PromptService, {
strict: false,
});
tools.doc_compose = createDocComposeTool(
promptService,
this.factory
);
break;
}
case 'codeArtifact': {
const promptService = this.moduleRef.get(PromptService, {
strict: false,
});
tools.code_artifact = createCodeArtifactTool(
promptService,
this.factory
);
tools.doc_compose = createDocComposeTool(prompt, this.factory);
break;
}
}

View File

@@ -60,6 +60,8 @@ export const VertexSchema: JSONSchema = {
export const PromptConfigStrictSchema = z.object({
tools: z
.enum([
'codeArtifact',
'conversationSummary',
// work with morph
'docEdit',
// work with indexer
@@ -71,7 +73,6 @@ export const PromptConfigStrictSchema = z.object({
'webSearch',
// artifact tools
'docCompose',
'codeArtifact',
])
.array()
.nullable()

View File

@@ -6,20 +6,10 @@ import {
ImagePart,
TextPart,
TextStreamPart,
ToolSet,
} from 'ai';
import { ZodType } from 'zod';
import {
createCodeArtifactTool,
createDocComposeTool,
createDocEditTool,
createDocKeywordSearchTool,
createDocReadTool,
createDocSemanticSearchTool,
createExaCrawlTool,
createExaSearchTool,
} from '../tools';
import { CustomAITools } from '../tools';
import { PromptMessage, StreamObject } from './types';
type ChatMessage = CoreUserMessage | CoreAssistantMessage;
@@ -385,17 +375,6 @@ export class CitationParser {
}
}
export interface CustomAITools extends ToolSet {
doc_edit: ReturnType<typeof createDocEditTool>;
doc_semantic_search: ReturnType<typeof createDocSemanticSearchTool>;
doc_keyword_search: ReturnType<typeof createDocKeywordSearchTool>;
doc_read: ReturnType<typeof createDocReadTool>;
doc_compose: ReturnType<typeof createDocComposeTool>;
web_search_exa: ReturnType<typeof createExaSearchTool>;
web_crawl_exa: ReturnType<typeof createExaCrawlTool>;
code_artifact: ReturnType<typeof createCodeArtifactTool>;
}
type ChunkType = TextStreamPart<CustomAITools>['type'];
export function toError(error: unknown): Error {
@@ -451,6 +430,10 @@ export class TextStreamParser {
);
result = this.addPrefix(result);
switch (chunk.toolName) {
case 'conversation_summary': {
result += `\nSummarizing context\n`;
break;
}
case 'web_search_exa': {
result += `\nSearching the web "${chunk.args.query}"\n`;
break;
@@ -489,10 +472,18 @@ export class TextStreamParser {
result = this.addPrefix(result);
switch (chunk.toolName) {
case 'doc_edit': {
if (chunk.result && typeof chunk.result === 'object') {
result += `\n${chunk.result.result}\n`;
if (
chunk.result &&
typeof chunk.result === 'object' &&
Array.isArray(chunk.result.result)
) {
result += chunk.result.result
.map(item => {
return `\n${item.changedContent}\n`;
})
.join('');
this.docEditFootnotes[this.docEditFootnotes.length - 1].result =
chunk.result.result;
result;
} else {
this.docEditFootnotes.pop();
}

View File

@@ -23,6 +23,7 @@ import {
CallMetric,
CopilotDocNotFound,
CopilotFailedToCreateMessage,
CopilotProviderSideError,
CopilotSessionNotFound,
type FileUpload,
paginate,
@@ -31,14 +32,18 @@ import {
RequestMutex,
Throttle,
TooManyRequest,
UserFriendlyError,
} from '../../base';
import { CurrentUser } from '../../core/auth';
import { Admin } from '../../core/common';
import { AccessController } from '../../core/permission';
import { DocReader } from '../../core/doc';
import { AccessController, DocAction } from '../../core/permission';
import { UserType } from '../../core/user';
import type { ListSessionOptions, UpdateChatSession } from '../../models';
import { CopilotCronJobs } from './cron';
import { PromptService } from './prompt';
import { PromptMessage, StreamObject } from './providers';
import { CopilotProviderFactory } from './providers/factory';
import { ChatSessionService } from './session';
import { CopilotStorage } from './storage';
import { type ChatHistory, type ChatMessage, SubmittedMessage } from './types';
@@ -138,6 +143,9 @@ class CreateChatMessageInput implements Omit<SubmittedMessage, 'content'> {
@Field(() => [String], { nullable: true, deprecationReason: 'use blobs' })
attachments!: string[] | undefined;
@Field(() => GraphQLUpload, { nullable: true })
blob!: Promise<FileUpload> | undefined;
@Field(() => [GraphQLUpload], { nullable: true })
blobs!: Promise<FileUpload>[] | undefined;
@@ -396,7 +404,9 @@ export class CopilotResolver {
private readonly ac: AccessController,
private readonly mutex: RequestMutex,
private readonly chatSession: ChatSessionService,
private readonly storage: CopilotStorage
private readonly storage: CopilotStorage,
private readonly docReader: DocReader,
private readonly providerFactory: CopilotProviderFactory
) {}
@ResolveField(() => CopilotQuotaType, {
@@ -410,7 +420,8 @@ export class CopilotResolver {
private async assertPermission(
user: CurrentUser,
options: { workspaceId?: string | null; docId?: string | null }
options: { workspaceId?: string | null; docId?: string | null },
fallbackAction?: DocAction
) {
const { workspaceId, docId } = options;
if (!workspaceId) {
@@ -421,7 +432,7 @@ export class CopilotResolver {
.user(user.id)
.doc({ workspaceId, docId })
.allowLocal()
.assert('Doc.Update');
.assert(fallbackAction ?? 'Doc.Update');
} else {
await this.ac
.user(user.id)
@@ -500,7 +511,7 @@ export class CopilotResolver {
if (!workspaceId) {
return [];
} else {
await this.assertPermission(user, { workspaceId, docId });
await this.assertPermission(user, { workspaceId, docId }, 'Doc.Read');
}
const histories = await this.chatSession.list(
@@ -530,7 +541,7 @@ export class CopilotResolver {
if (!workspaceId) {
return paginate([], 'updatedAt', pagination, 0);
} else {
await this.assertPermission(user, { workspaceId, docId });
await this.assertPermission(user, { workspaceId, docId }, 'Doc.Read');
}
const finalOptions = Object.assign(
@@ -696,10 +707,13 @@ export class CopilotResolver {
}
const attachments: PromptMessage['attachments'] = options.attachments || [];
if (options.blobs) {
if (options.blob || options.blobs) {
const { workspaceId } = session.config;
const blobs = await Promise.all(options.blobs);
const blobs = await Promise.all(
options.blob ? [options.blob] : options.blobs || []
);
delete options.blob;
delete options.blobs;
for (const blob of blobs) {
@@ -724,6 +738,65 @@ export class CopilotResolver {
}
}
@Query(() => String, {
description:
'Apply updates to a doc using LLM and return the merged markdown.',
})
async applyDocUpdates(
@CurrentUser() user: CurrentUser,
@Args({ name: 'workspaceId', type: () => String })
workspaceId: string,
@Args({ name: 'docId', type: () => String })
docId: string,
@Args({ name: 'op', type: () => String })
op: string,
@Args({ name: 'updates', type: () => String })
updates: string
): Promise<string> {
await this.assertPermission(user, { workspaceId, docId });
const docContent = await this.docReader.getDocMarkdown(
workspaceId,
docId,
true
);
if (!docContent || !docContent.markdown) {
throw new NotFoundException('Doc not found or empty');
}
const markdown = docContent.markdown.trim();
// Get LLM provider
const provider =
await this.providerFactory.getProviderByModel('morph-v3-large');
if (!provider) {
throw new BadRequestException('No LLM provider available');
}
try {
return await provider.text(
{ modelId: 'morph-v3-large' },
[
{
role: 'user',
content: `<instruction>${op}</instruction>\n<code>${markdown}</code>\n<update>${updates}</update>`,
},
],
{ reasoning: false }
);
} catch (e: any) {
if (e instanceof UserFriendlyError) {
throw e;
} else {
throw new CopilotProviderSideError({
provider: provider.type,
kind: 'unexpected_response',
message: e?.message || 'Unexpected apply response',
});
}
}
}
private transformToSessionType(
session: Omit<ChatHistory, 'messages'>
): CopilotSessionType {
@@ -773,7 +846,26 @@ class CreateCopilotPromptInput {
@Admin()
@Resolver(() => String)
export class PromptsManagementResolver {
constructor(private readonly promptService: PromptService) {}
constructor(
private readonly cron: CopilotCronJobs,
private readonly promptService: PromptService
) {}
@Mutation(() => Boolean, {
description: 'Trigger generate missing titles cron job',
})
async triggerGenerateTitleCron() {
await this.cron.triggerGenerateMissingTitles();
return true;
}
@Mutation(() => Boolean, {
description: 'Trigger cleanup of trashed doc embeddings',
})
async triggerCleanupTrashedDocEmbeddings() {
await this.cron.triggerCleanupTrashedDocEmbeddings();
return true;
}
@Query(() => [CopilotPromptType], {
description: 'List all copilot prompts',

View File

@@ -507,6 +507,8 @@ export class ChatSessionService {
return await this.models.copilotSession.fork({
...session,
userId: options.userId,
// docId can be changed in fork
docId: options.docId,
sessionId: randomUUID(),
parentSessionId: options.sessionId,
messages,
@@ -569,7 +571,7 @@ export class ChatSessionService {
});
if (!provider) {
throw new NoCopilotProviderAvailable();
throw new NoCopilotProviderAvailable({ modelId: prompt.model });
}
return provider.text(cond, [...prompt.finish({}), msg], config);

View File

@@ -5,9 +5,7 @@ import { z } from 'zod';
import type { PromptService } from '../prompt';
import type { CopilotProviderFactory } from '../providers';
import { toolError } from './error';
const logger = new Logger('CodeArtifactTool');
/**
* A copilot tool that produces a completely self-contained HTML artifact.
* The returned HTML must include <style> and <script> tags directly so that
@@ -37,23 +35,20 @@ export const createCodeArtifactTool = (
}),
execute: async ({ title, userPrompt }) => {
try {
const prompt = await promptService.get('Make it real with text');
const prompt = await promptService.get('Code Artifact');
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({}), { role: 'user', content: userPrompt }]
prompt.finish({ content: userPrompt })
);
// Remove surrounding ``` or ```html fences if present
let stripped = content.trim();
if (stripped.startsWith('```')) {
@@ -65,7 +60,6 @@ export const createCodeArtifactTool = (
stripped = stripped.slice(0, -3);
}
}
return {
title,
html: stripped,

View File

@@ -0,0 +1,76 @@
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('ConversationSummaryTool');
export const createConversationSummaryTool = (
sessionId: string | undefined,
promptService: PromptService,
factory: CopilotProviderFactory
) => {
return tool({
description:
'Create a concise, AI-generated summary of the conversation so far—capturing key topics, decisions, and critical details. Use this tool whenever the context becomes lengthy to preserve essential information that might otherwise be lost to truncation in future turns.',
parameters: z.object({
focus: z
.string()
.optional()
.describe(
'Optional focus area for the summary (e.g., "technical decisions", "user requirements", "project status")'
),
length: z
.enum(['brief', 'detailed', 'comprehensive'])
.default('detailed')
.describe(
'The desired length of the summary: brief (1-2 sentences), detailed (paragraph), comprehensive (multiple paragraphs)'
),
}),
execute: async ({ focus, length }, { messages }) => {
try {
if (!messages || messages.length === 0) {
return toolError(
'No Conversation Context',
'No messages available to summarize'
);
}
const prompt = await promptService.get('Conversation Summary');
const provider = await factory.getProviderByModel(prompt?.model || '');
if (!prompt || !provider) {
return toolError(
'Prompt Not Found',
'Failed to summarize conversation.'
);
}
const summary = await provider.text(
{ modelId: prompt.model },
prompt.finish({
messages: messages.map(m => ({
...m,
content: m.content.toString(),
})),
focus: focus || 'general',
length,
})
);
return {
focusArea: focus || 'general',
messageCount: messages.length,
summary,
timestamp: new Date().toISOString(),
};
} catch (err: any) {
logger.error(`Failed to summarize conversation (${sessionId})`, err);
return toolError('Conversation Summary Failed', err.message);
}
},
});
};

View File

@@ -3,6 +3,7 @@ import { z } from 'zod';
import { DocReader } from '../../../core/doc';
import { AccessController } from '../../../core/permission';
import { type PromptService } from '../prompt';
import type { CopilotChatOptions, CopilotProviderFactory } from '../providers';
export const buildContentGetter = (ac: AccessController, doc: DocReader) => {
@@ -24,14 +25,20 @@ export const buildContentGetter = (ac: AccessController, doc: DocReader) => {
export const createDocEditTool = (
factory: CopilotProviderFactory,
prompt: PromptService,
getContent: (targetId?: string) => Promise<string | undefined>
) => {
return tool({
description: `
Use this tool to propose an edit to a structured Markdown document with identifiable blocks. Each block begins with a comment like <!-- block_id=... -->, and represents a unit of editable content such as a heading, paragraph, list, or code snippet.
Use this tool to propose an edit to a structured Markdown document with identifiable blocks.
Each block begins with a comment like <!-- block_id=... -->, and represents a unit of editable content such as a heading, paragraph, list, or code snippet.
This will be read by a less intelligent model, which will quickly apply the edit. You should make it clear what the edit is, while also minimizing the unchanged code you write.
Your task is to return a list of block-level changes needed to fulfill the user's intent. Each change should correspond to a specific user instruction and be represented by one of the following operations:
If you receive a markdown without block_id comments, you should call \`doc_read\` tool to get the content.
Your task is to return a list of block-level changes needed to fulfill the user's intent. **Each change in code_edit must be completely independent: each code_edit entry should only perform a single, isolated change, and must not include the effects of other changes. For example, the updates for a delete operation should only show the context related to the deletion, and must not include any content modified by other operations (such as bolding or insertion). This ensures that each change can be applied independently and in any order.**
Each change should correspond to a specific user instruction and be represented by one of the following operations:
replace: Replace the content of a block with updated Markdown.
@@ -41,83 +48,83 @@ insert: Add a new block, and specify its block_id and content.
Important Instructions:
- Use the existing block structure as-is. Do not reformat or reorder blocks unless explicitly asked.
- Always preserve block_id and type in your replacements.
- When replacing a block, use the full new block including <!-- block_id=... type=... --> and the updated content.
- When inserting, follow the same format as a replacement, but ensure the new block_id does not conflict with existing IDs.
- Each list item should be a block.
- Use <!-- existing blocks ... --> for unchanged sections.
- If you plan on deleting a section, you must provide surrounding context to indicate the deletion.
- When replacing content, always keep the original block_id unchanged.
- When deleting content, only use the format <!-- delete block_id=xxx -->, and only for valid block_id present in the original <code> content.
- Each top-level list item should be a block. Like this:
\`\`\`markdown
<!-- block_id=001 flavour=affine:list -->
* Item 1
* SubItem 1
<!-- block_id=002 flavour=affine:list -->
1. Item 1
1. SubItem 1
\`\`\`
- Your task is to return a list of block-level changes needed to fulfill the user's intent.
- **Each change in code_edit must be completely independent: each code_edit entry should only perform a single, isolated change, and must not include the effects of other changes. For example, the updates for a delete operation should only show the context related to the deletion, and must not include any content modified by other operations (such as bolding or insertion). This ensures that each change can be applied independently and in any order.**
Example Input Document:
\`\`\`md
<!-- block_id=block-001 type=paragraph -->
# My Holiday Plan
Original Content:
\`\`\`markdown
<!-- block_id=001 flavour=paragraph -->
# Andriy Shevchenko
<!-- block_id=block-002 type=paragraph -->
I plan to travel to Paris, France, where I will visit the Eiffel Tower, the Louvre, and the Champs-Élysées.
<!-- block_id=002 flavour=paragraph -->
## Player Profile
<!-- block_id=block-003 type=paragraph -->
I love Paris.
<!-- block_id=003 flavour=paragraph -->
Andriy Shevchenko is a legendary Ukrainian striker, best known for his time at AC Milan and Dynamo Kyiv. He won the Ballon d'Or in 2004.
<!-- block_id=block-004 type=paragraph -->
## Reason for the delay
<!-- block_id=004 flavour=paragraph -->
## Career Overview
<!-- block_id=block-005 type=paragraph -->
This plan has been brewing for a long time, but I always postponed it because I was too busy with work.
<!-- block_id=block-006 type=paragraph -->
## Trip Steps
<!-- block_id=block-007 type=list -->
- Book flight tickets
<!-- block_id=block-008 type=list -->
- Reserve a hotel
<!-- block_id=block-009 type=list -->
- Prepare visa documents
<!-- block_id=block-010 type=list -->
- Plan the itinerary
<!-- block_id=block-011 type=paragraph -->
Additionally, I plan to learn some basic French to make communication easier during the trip.
<!-- block_id=005 flavour=list -->
- Born in 1976 in Ukraine.
<!-- block_id=006 flavour=list -->
- Rose to fame at Dynamo Kyiv in the 1990s.
<!-- block_id=007 flavour=list -->
- Starred at AC Milan (19992006), scoring over 170 goals.
<!-- block_id=008 flavour=list -->
- Played for Chelsea (20062009) before returning to Kyiv.
<!-- block_id=009 flavour=list -->
- Coached Ukraine national team, reaching Euro 2020 quarter-finals.
\`\`\`
Example User Request:
User Request
\`\`\`
Translate the trip steps to Chinese, remove the reason for the delay, and bold the final paragraph.
Bold the players name in the intro, add a summary section at the end, and remove the career overview.
\`\`\`
Expected Output:
\`\`\`md
<!-- existing blocks ... -->
<!-- block_id=block-002 type=paragraph -->
I plan to travel to Paris, France, where I will visit the Eiffel Tower, the Louvre, and the Champs-Élysées.
<!-- block_id=block-003 type=paragraph -->
I love Paris.
<!-- delete block-004 -->
<!-- delete block-005 -->
<!-- block_id=block-006 type=paragraph -->
## Trip Steps
<!-- block_id=block-007 type=list -->
- 订机票
<!-- block_id=block-008 type=list -->
- 预定酒店
<!-- block_id=block-009 type=list -->
- 准备签证材料
<!-- block_id=block-010 type=list -->
- 规划行程
<!-- existing blocks ... -->
<!-- block_id=block-011 type=paragraph -->
**Additionally, I plan to learn some basic French to make communication easier during the trip.**
Example response:
\`\`\`json
[
{
"op": "Bold the player's name in the introduction",
"updates": "
<!-- block_id=003 flavour=paragraph -->
**Andriy Shevchenko** is a legendary Ukrainian striker, best known for his time at AC Milan and Dynamo Kyiv. He won the Ballon d'Or in 2004.
"
},
{
"op": "Add a summary section at the end",
"updates": "
<!-- block_id=new-abc123 flavour=paragraph -->
## Summary
<!-- block_id=new-def456 flavour=paragraph -->
Shevchenko is celebrated as one of the greatest Ukrainian footballers of all time. Known for his composure, strength, and goal-scoring instinct, he left a lasting legacy both on and off the pitch.
"
},
{
"op": "Delete the career overview section",
"updates": "
<!-- delete block_id=004 -->
<!-- delete block_id=005 -->
<!-- delete block_id=006 -->
<!-- delete block_id=007 -->
<!-- delete block_id=008 -->
<!-- delete block_id=009 -->
"
}
]
\`\`\`
You should specify the following arguments before the others: [doc_id], [origin_content]
@@ -143,15 +150,42 @@ You should specify the following arguments before the others: [doc_id], [origin_
'A short, first-person description of the intended edit, clearly summarizing what I will change. For example: "I will translate the steps into English and delete the paragraph explaining the delay." This helps the downstream system understand the purpose of the changes.'
),
code_edit: z
.string()
.describe(
'Specify only the necessary Markdown block-level changes. Return a list of inserted, replaced, or deleted blocks. Each block must start with its <!-- block_id=... type=... --> comment. Use <!-- existing blocks ... --> for unchanged sections.If you plan on deleting a section, you must provide surrounding context to indicate the deletion.'
),
code_edit: z.preprocess(
val => {
// BACKGROUND: LLM sometimes returns a JSON string instead of an array.
if (typeof val === 'string') {
return JSON.parse(val);
}
return val;
},
z
.array(
z.object({
op: z
.string()
.describe(
'A short description of the change, such as "Bold intro name"'
),
updates: z
.string()
.describe(
'Markdown block fragments that represent the change, including the block_id and type'
),
})
)
.describe(
'An array of independent semantic changes to apply to the document.'
)
),
}),
execute: async ({ doc_id, origin_content, code_edit }) => {
try {
const provider = await factory.getProviderByModel('morph-v2');
const applyPrompt = await prompt.get('Apply Updates');
if (!applyPrompt) {
return 'Prompt not found';
}
const model = applyPrompt.model;
const provider = await factory.getProviderByModel(model);
if (!provider) {
return 'Editing docs is not supported';
}
@@ -160,14 +194,27 @@ You should specify the following arguments before the others: [doc_id], [origin_
if (!content) {
return 'Doc not found or doc is empty';
}
const result = await provider.text({ modelId: 'morph-v2' }, [
{
role: 'user',
content: `<code>${content}</code>\n<update>${code_edit}</update>`,
},
]);
return { result, content };
const changedContents = await Promise.all(
code_edit.map(async edit => {
return await provider.text({ modelId: model }, [
...applyPrompt.finish({
content,
op: edit.op,
updates: edit.updates,
}),
]);
})
);
return {
result: changedContents.map((changedContent, index) => ({
op: code_edit[index].op,
updates: code_edit[index].updates,
originalContent: content,
changedContent,
})),
};
} catch {
return 'Failed to apply edit to the doc';
}

View File

@@ -1,17 +1,45 @@
import { tool } from 'ai';
import { omit } from 'lodash-es';
import { z } from 'zod';
import type { AccessController } from '../../../core/permission';
import type { ChunkSimilarity } from '../../../models';
import type { ChunkSimilarity, Models } from '../../../models';
import type { CopilotContextService } from '../context';
import type { ContextSession } from '../context/session';
import type { CopilotChatOptions } from '../providers';
import { toolError } from './error';
const FILTER_PREFIX = [
'Title: ',
'Created at: ',
'Updated at: ',
'Created by: ',
'Updated by: ',
];
function clearEmbeddingChunk(chunk: ChunkSimilarity): ChunkSimilarity {
if (chunk.content) {
const lines = chunk.content.split('\n');
let maxLines = 5;
while (maxLines > 0 && lines.length > 0) {
if (FILTER_PREFIX.some(prefix => lines[0].startsWith(prefix))) {
lines.shift();
maxLines--;
} else {
// only process consecutive metadata rows
break;
}
}
return { ...chunk, content: lines.join('\n') };
}
return chunk;
}
export const buildDocSearchGetter = (
ac: AccessController,
context: CopilotContextService,
docContext: ContextSession | null
docContext: ContextSession | null,
models: Models
) => {
const searchDocs = async (
options: CopilotChatOptions,
@@ -45,7 +73,43 @@ export const buildDocSearchGetter = (
}
if (!docChunks.length && !fileChunks.length)
return `No results found for "${query}".`;
return [...fileChunks, ...docChunks];
const docIds = docChunks.map(c => ({
// oxlint-disable-next-line no-non-null-assertion
workspaceId: options.workspace!,
docId: c.docId,
}));
const docAuthors = await models.doc
.findAuthors(docIds)
.then(
docs =>
new Map(
docs
.filter(d => !!d)
.map(doc => [doc.id, omit(doc, ['id', 'workspaceId'])])
)
);
const docMetas = await models.doc
.findMetas(docIds, { select: { title: true } })
.then(
docs =>
new Map(
docs
.filter(d => !!d)
.map(doc => [
doc.docId,
Object.assign({}, doc, docAuthors.get(doc.docId)),
])
)
);
return [
...fileChunks.map(clearEmbeddingChunk),
...docChunks.map(c => ({
...c,
...docMetas.get(c.docId),
})),
] as ChunkSimilarity[];
};
return searchDocs;
};

View File

@@ -1,4 +1,29 @@
import { ToolSet } from 'ai';
import { createCodeArtifactTool } from './code-artifact';
import { createConversationSummaryTool } from './conversation-summary';
import { createDocComposeTool } from './doc-compose';
import { createDocEditTool } from './doc-edit';
import { createDocKeywordSearchTool } from './doc-keyword-search';
import { createDocReadTool } from './doc-read';
import { createDocSemanticSearchTool } from './doc-semantic-search';
import { createExaCrawlTool } from './exa-crawl';
import { createExaSearchTool } from './exa-search';
export interface CustomAITools extends ToolSet {
code_artifact: ReturnType<typeof createCodeArtifactTool>;
conversation_summary: ReturnType<typeof createConversationSummaryTool>;
doc_edit: ReturnType<typeof createDocEditTool>;
doc_semantic_search: ReturnType<typeof createDocSemanticSearchTool>;
doc_keyword_search: ReturnType<typeof createDocKeywordSearchTool>;
doc_read: ReturnType<typeof createDocReadTool>;
doc_compose: ReturnType<typeof createDocComposeTool>;
web_search_exa: ReturnType<typeof createExaSearchTool>;
web_crawl_exa: ReturnType<typeof createExaCrawlTool>;
}
export * from './code-artifact';
export * from './conversation-summary';
export * from './doc-compose';
export * from './doc-edit';
export * from './doc-keyword-search';

View File

@@ -15,7 +15,6 @@ import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import {
CopilotTranscriptionAudioNotProvided,
CopilotTranscriptionJobNotFound,
type FileUpload,
} from '../../../base';
import { CurrentUser } from '../../../core/auth';
@@ -74,7 +73,7 @@ const FinishedStatus: Set<AiJobStatus> = new Set([
export class CopilotTranscriptionResolver {
constructor(
private readonly ac: AccessController,
private readonly service: CopilotTranscriptionService
private readonly transcript: CopilotTranscriptionService
) {}
private handleJobResult(
@@ -122,7 +121,7 @@ export class CopilotTranscriptionResolver {
throw new CopilotTranscriptionAudioNotProvided();
}
const jobResult = await this.service.submitTranscriptionJob(
const jobResult = await this.transcript.submitJob(
user.id,
workspaceId,
blobId,
@@ -144,19 +143,11 @@ export class CopilotTranscriptionResolver {
.allowLocal()
.assert('Workspace.Copilot');
const job = await this.service.queryTranscriptionJob(
const jobResult = await this.transcript.retryJob(
user.id,
workspaceId,
jobId
);
if (!job || !job.infos) {
throw new CopilotTranscriptionJobNotFound();
}
const jobResult = await this.service.executeTranscriptionJob(
job.id,
job.infos
);
return this.handleJobResult(jobResult);
}
@@ -166,7 +157,7 @@ export class CopilotTranscriptionResolver {
@CurrentUser() user: CurrentUser,
@Args('jobId') jobId: string
): Promise<TranscriptionResultType | null> {
const job = await this.service.claimTranscriptionJob(user.id, jobId);
const job = await this.transcript.claimJob(user.id, jobId);
return this.handleJobResult(job);
}
@@ -190,7 +181,7 @@ export class CopilotTranscriptionResolver {
.allowLocal()
.assert('Workspace.Copilot');
const job = await this.service.queryTranscriptionJob(
const job = await this.transcript.queryJob(
user.id,
copilot.workspaceId,
jobId,

View File

@@ -49,7 +49,17 @@ export class CopilotTranscriptionService {
private readonly providerFactory: CopilotProviderFactory
) {}
async submitTranscriptionJob(
private async getModel(userId: string) {
const prompt = await this.prompt.get('Transcript audio');
const hasAccess = await this.models.userFeature.has(
userId,
'unlimited_copilot'
);
// choose the pro model if user has copilot plan
return prompt?.optionalModels[hasAccess ? 1 : 0];
}
async submitJob(
userId: string,
workspaceId: string,
blobId: string,
@@ -78,12 +88,26 @@ export class CopilotTranscriptionService {
infos.push({ url, mimeType: blob.mimetype });
}
return await this.executeTranscriptionJob(jobId, infos);
const model = await this.getModel(userId);
return await this.executeJob(jobId, infos, model);
}
async executeTranscriptionJob(
async retryJob(userId: string, workspaceId: string, jobId: string) {
const job = await this.queryJob(userId, workspaceId, jobId);
if (!job || !job.infos) {
throw new CopilotTranscriptionJobNotFound();
}
const model = await this.getModel(userId);
const jobResult = await this.executeJob(job.id, job.infos, model);
return jobResult;
}
async executeJob(
jobId: string,
infos: AudioBlobInfos
infos: AudioBlobInfos,
modelId?: string
): Promise<TranscriptionJob> {
const status = AiJobStatus.running;
const success = await this.models.copilotJob.update(jobId, {
@@ -98,12 +122,13 @@ export class CopilotTranscriptionService {
await this.job.add('copilot.transcript.submit', {
jobId,
infos,
modelId,
});
return { id: jobId, status };
}
async claimTranscriptionJob(
async claimJob(
userId: string,
jobId: string
): Promise<TranscriptionJob | null> {
@@ -118,7 +143,7 @@ export class CopilotTranscriptionService {
return null;
}
async queryTranscriptionJob(
async queryJob(
userId: string,
workspaceId: string,
jobId?: string,
@@ -171,7 +196,7 @@ export class CopilotTranscriptionService {
);
if (!provider) {
throw new NoCopilotProviderAvailable();
throw new NoCopilotProviderAvailable({ modelId });
}
return provider;
@@ -181,14 +206,20 @@ export class CopilotTranscriptionService {
promptName: string,
message: Partial<PromptMessage>,
schema?: ZodType<any>,
prefer?: CopilotProviderType
prefer?: CopilotProviderType,
modelId?: string
): Promise<string> {
const prompt = await this.prompt.get(promptName);
if (!prompt) {
throw new CopilotPromptNotFound({ name: promptName });
}
const cond = { modelId: prompt.model };
const cond = {
modelId:
modelId && prompt.optionalModels.includes(modelId)
? modelId
: prompt.model,
};
const msg = { role: 'user' as const, content: '', ...message };
const config = Object.assign({}, prompt.config);
if (schema) {
@@ -231,13 +262,19 @@ export class CopilotTranscriptionService {
return `${hoursStr}:${minutesStr}:${secondsStr}`;
}
private async callTranscript(url: string, mimeType: string, offset: number) {
private async callTranscript(
url: string,
mimeType: string,
offset: number,
modelId?: string
) {
// NOTE: Vertex provider not support transcription yet, we always use Gemini here
const result = await this.chatWithPrompt(
'Transcript audio',
{ attachments: [url], params: { mimetype: mimeType } },
TranscriptionResponseSchema,
CopilotProviderType.Gemini
CopilotProviderType.Gemini,
modelId
);
const transcription = TranscriptionResponseSchema.parse(
@@ -256,6 +293,7 @@ export class CopilotTranscriptionService {
async transcriptAudio({
jobId,
infos,
modelId,
// @deprecated
url,
mimeType,
@@ -264,7 +302,7 @@ export class CopilotTranscriptionService {
const blobInfos = this.mergeInfos(infos, url, mimeType);
const transcriptions = await Promise.all(
Array.from(blobInfos.entries()).map(([idx, { url, mimeType }]) =>
this.callTranscript(url, mimeType, idx * 10 * 60)
this.callTranscript(url, mimeType, idx * 10 * 60, modelId)
)
);

View File

@@ -56,6 +56,7 @@ declare global {
'copilot.transcript.submit': {
jobId: string;
infos?: AudioBlobInfos;
modelId?: string;
/// @deprecated use `infos` instead
url?: string;
/// @deprecated use `infos` instead

View File

@@ -103,6 +103,7 @@ export class CopilotWorkspaceEmbeddingConfigResolver {
return ignoredDocs;
}
@Mutation(() => Number, {
name: 'updateWorkspaceEmbeddingIgnoredDocs',
complexity: 2,

View File

@@ -140,10 +140,13 @@ export class ElasticsearchProvider extends SearchProvider {
const result = await this.request(
'POST',
url.toString(),
JSON.stringify({ query })
JSON.stringify({ query }),
'application/json',
// ignore 409 error: version_conflict_engine_exception, version conflict, required seqNo [255898790], primary term [3]. current document has seqNo [256133002] and primary term [3]
[409]
);
this.logger.debug(
`deleted by query ${table} ${JSON.stringify(query)} in ${Date.now() - start}ms, result: ${JSON.stringify(result)}`
`deleted by query ${table} ${JSON.stringify(query)} in ${Date.now() - start}ms, result: ${JSON.stringify(result).substring(0, 500)}`
);
}
@@ -264,7 +267,8 @@ export class ElasticsearchProvider extends SearchProvider {
method: 'POST' | 'PUT',
url: string,
body: string,
contentType = 'application/json'
contentType = 'application/json',
ignoreErrorStatus?: number[]
) {
const headers = {
'Content-Type': contentType,
@@ -280,6 +284,10 @@ export class ElasticsearchProvider extends SearchProvider {
headers,
});
const data = await response.json();
if (ignoreErrorStatus?.includes(response.status)) {
return data;
}
// handle error, status >= 400
// {
// "error": {

View File

@@ -291,6 +291,11 @@ type CopilotFailedToAddWorkspaceFileEmbeddingDataType {
message: String!
}
type CopilotFailedToGenerateEmbeddingDataType {
message: String!
provider: String!
}
type CopilotFailedToMatchContextDataType {
content: String!
contextId: String!
@@ -452,6 +457,7 @@ type CopilotWorkspaceIgnoredDocTypeEdge {
input CreateChatMessageInput {
attachments: [String!]
blob: Upload
blobs: [Upload!]
content: String
params: JSON
@@ -595,6 +601,7 @@ type DocType {
mode: PublicDocMode!
permissions: DocPermissions!
public: Boolean!
summary: String
title: String
updatedAt: DateTime
workspaceId: String!
@@ -615,7 +622,7 @@ type EditorType {
name: String!
}
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToAddWorkspaceFileEmbeddingDataType | CopilotFailedToMatchContextDataType | CopilotFailedToMatchGlobalContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderNotSupportedDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | DocUpdateBlockedDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | HttpRequestErrorDataType | InvalidAppConfigDataType | InvalidAppConfigInputDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidIndexerInputDataType | InvalidLicenseToActivateDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidOauthResponseDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | InvalidSearchProviderRequestDataType | MemberNotFoundInSpaceDataType | MentionUserDocAccessDeniedDataType | MissingOauthQueryParameterDataType | NoMoreSeatDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | ValidationErrorDataType | VersionRejectedDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToAddWorkspaceFileEmbeddingDataType | CopilotFailedToGenerateEmbeddingDataType | CopilotFailedToMatchContextDataType | CopilotFailedToMatchGlobalContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderNotSupportedDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | DocUpdateBlockedDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | HttpRequestErrorDataType | InvalidAppConfigDataType | InvalidAppConfigInputDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidIndexerInputDataType | InvalidLicenseToActivateDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidOauthResponseDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | InvalidSearchProviderRequestDataType | MemberNotFoundInSpaceDataType | MentionUserDocAccessDeniedDataType | MissingOauthQueryParameterDataType | NoCopilotProviderAvailableDataType | NoMoreSeatDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | ValidationErrorDataType | VersionRejectedDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
enum ErrorNames {
ACCESS_DENIED
@@ -644,6 +651,7 @@ enum ErrorNames {
COPILOT_EMBEDDING_UNAVAILABLE
COPILOT_FAILED_TO_ADD_WORKSPACE_FILE_EMBEDDING
COPILOT_FAILED_TO_CREATE_MESSAGE
COPILOT_FAILED_TO_GENERATE_EMBEDDING
COPILOT_FAILED_TO_GENERATE_TEXT
COPILOT_FAILED_TO_MATCH_CONTEXT
COPILOT_FAILED_TO_MATCH_GLOBAL_CONTEXT
@@ -1290,6 +1298,12 @@ type Mutation {
setBlob(blob: Upload!, workspaceId: String!): String!
submitAudioTranscription(blob: Upload, blobId: String!, blobs: [Upload!], workspaceId: String!): TranscriptionResultType
"""Trigger cleanup of trashed doc embeddings"""
triggerCleanupTrashedDocEmbeddings: Boolean!
"""Trigger generate missing titles cron job"""
triggerGenerateTitleCron: Boolean!
"""update app configuration"""
updateAppConfig(updates: [UpdateAppConfigInput!]!): JSONObject!
@@ -1335,6 +1349,10 @@ type Mutation {
verifyEmail(token: String!): Boolean!
}
type NoCopilotProviderAvailableDataType {
modelId: String!
}
type NoMoreSeatDataType {
spaceId: String!
}
@@ -1507,6 +1525,9 @@ type PublicUserType {
type Query {
"""get the whole app configuration"""
appConfig: JSONObject!
"""Apply updates to a doc using LLM and return the merged markdown."""
applyDocUpdates(docId: String!, op: String!, updates: String!, workspaceId: String!): String!
collectAllBlobSizes: WorkspaceBlobSizes! @deprecated(reason: "use `user.quotaUsage` instead")
"""Get current user"""

View File

@@ -82,6 +82,10 @@ export type RequestOptions<Q extends GraphQLQuery> = QueryVariablesOption<Q> & {
* @default 15000
*/
timeout?: number;
/**
* Abort signal
*/
signal?: AbortSignal;
};
export type QueryOptions<Q extends GraphQLQuery> = RequestOptions<Q> & {
@@ -207,6 +211,7 @@ export const gqlFetcherFactory = (
headers,
body: isFormData ? body : JSON.stringify(body),
timeout: options.timeout,
signal: options.signal,
})
).then(async res => {
if (res.headers.get('content-type')?.startsWith('application/json')) {

View File

@@ -0,0 +1,3 @@
query applyDocUpdates($workspaceId: String!, $docId: String!, $op: String!, $updates: String!) {
applyDocUpdates(workspaceId: $workspaceId, docId: $docId, op: $op, updates: $updates)
}

View File

@@ -3,15 +3,17 @@
query getCopilotRecentSessions(
$workspaceId: String!
$limit: Int = 10
$offset: Int = 0
) {
currentUser {
copilot(workspaceId: $workspaceId) {
chats(
pagination: { first: $limit }
pagination: { first: $limit, offset: $offset }
options: {
action: false
fork: false
sessionOrder: desc
withMessages: true
withMessages: false
}
) {
...PaginatedCopilotChats

View File

@@ -5,6 +5,8 @@ query getWorkspacePageById($workspaceId: String!, $pageId: String!) {
mode
defaultRole
public
title
summary
}
}
}

View File

@@ -555,6 +555,19 @@ export const uploadCommentAttachmentMutation = {
file: true,
};
export const applyDocUpdatesQuery = {
id: 'applyDocUpdatesQuery' as const,
op: 'applyDocUpdates',
query: `query applyDocUpdates($workspaceId: String!, $docId: String!, $op: String!, $updates: String!) {
applyDocUpdates(
workspaceId: $workspaceId
docId: $docId
op: $op
updates: $updates
)
}`,
};
export const addContextCategoryMutation = {
id: 'addContextCategoryMutation' as const,
op: 'addContextCategory',
@@ -1068,12 +1081,12 @@ ${paginatedCopilotChatsFragment}`,
export const getCopilotRecentSessionsQuery = {
id: 'getCopilotRecentSessionsQuery' as const,
op: 'getCopilotRecentSessions',
query: `query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10) {
query: `query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10, $offset: Int = 0) {
currentUser {
copilot(workspaceId: $workspaceId) {
chats(
pagination: {first: $limit}
options: {fork: false, sessionOrder: desc, withMessages: true}
pagination: {first: $limit, offset: $offset}
options: {action: false, fork: false, sessionOrder: desc, withMessages: false}
) {
...PaginatedCopilotChats
}
@@ -1584,6 +1597,8 @@ export const getWorkspacePageByIdQuery = {
mode
defaultRole
public
title
summary
}
}
}`,

View File

@@ -375,6 +375,12 @@ export interface CopilotFailedToAddWorkspaceFileEmbeddingDataType {
message: Scalars['String']['output'];
}
export interface CopilotFailedToGenerateEmbeddingDataType {
__typename?: 'CopilotFailedToGenerateEmbeddingDataType';
message: Scalars['String']['output'];
provider: Scalars['String']['output'];
}
export interface CopilotFailedToMatchContextDataType {
__typename?: 'CopilotFailedToMatchContextDataType';
content: Scalars['String']['output'];
@@ -563,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']>;
@@ -703,6 +710,7 @@ export interface DocType {
mode: PublicDocMode;
permissions: DocPermissions;
public: Scalars['Boolean']['output'];
summary: Maybe<Scalars['String']['output']>;
title: Maybe<Scalars['String']['output']>;
updatedAt: Maybe<Scalars['DateTime']['output']>;
workspaceId: Scalars['String']['output'];
@@ -736,6 +744,7 @@ export type ErrorDataUnion =
| CopilotContextFileNotSupportedDataType
| CopilotDocNotFoundDataType
| CopilotFailedToAddWorkspaceFileEmbeddingDataType
| CopilotFailedToGenerateEmbeddingDataType
| CopilotFailedToMatchContextDataType
| CopilotFailedToMatchGlobalContextDataType
| CopilotFailedToModifyContextDataType
@@ -768,6 +777,7 @@ export type ErrorDataUnion =
| MemberNotFoundInSpaceDataType
| MentionUserDocAccessDeniedDataType
| MissingOauthQueryParameterDataType
| NoCopilotProviderAvailableDataType
| NoMoreSeatDataType
| NotInSpaceDataType
| QueryTooLongDataType
@@ -815,6 +825,7 @@ export enum ErrorNames {
COPILOT_EMBEDDING_UNAVAILABLE = 'COPILOT_EMBEDDING_UNAVAILABLE',
COPILOT_FAILED_TO_ADD_WORKSPACE_FILE_EMBEDDING = 'COPILOT_FAILED_TO_ADD_WORKSPACE_FILE_EMBEDDING',
COPILOT_FAILED_TO_CREATE_MESSAGE = 'COPILOT_FAILED_TO_CREATE_MESSAGE',
COPILOT_FAILED_TO_GENERATE_EMBEDDING = 'COPILOT_FAILED_TO_GENERATE_EMBEDDING',
COPILOT_FAILED_TO_GENERATE_TEXT = 'COPILOT_FAILED_TO_GENERATE_TEXT',
COPILOT_FAILED_TO_MATCH_CONTEXT = 'COPILOT_FAILED_TO_MATCH_CONTEXT',
COPILOT_FAILED_TO_MATCH_GLOBAL_CONTEXT = 'COPILOT_FAILED_TO_MATCH_GLOBAL_CONTEXT',
@@ -1430,6 +1441,10 @@ export interface Mutation {
sendVerifyEmail: Scalars['Boolean']['output'];
setBlob: Scalars['String']['output'];
submitAudioTranscription: Maybe<TranscriptionResultType>;
/** Trigger cleanup of trashed doc embeddings */
triggerCleanupTrashedDocEmbeddings: Scalars['Boolean']['output'];
/** Trigger generate missing titles cron job */
triggerGenerateTitleCron: Scalars['Boolean']['output'];
/** update app configuration */
updateAppConfig: Scalars['JSONObject']['output'];
/** Update a comment content */
@@ -1880,6 +1895,11 @@ export interface MutationVerifyEmailArgs {
token: Scalars['String']['input'];
}
export interface NoCopilotProviderAvailableDataType {
__typename?: 'NoCopilotProviderAvailableDataType';
modelId: Scalars['String']['output'];
}
export interface NoMoreSeatDataType {
__typename?: 'NoMoreSeatDataType';
spaceId: Scalars['String']['output'];
@@ -2058,6 +2078,8 @@ export interface Query {
__typename?: 'Query';
/** get the whole app configuration */
appConfig: Scalars['JSONObject']['output'];
/** Apply updates to a doc using LLM and return the merged markdown. */
applyDocUpdates: Scalars['String']['output'];
/** @deprecated use `user.quotaUsage` instead */
collectAllBlobSizes: WorkspaceBlobSizes;
/** Get current user */
@@ -2105,6 +2127,13 @@ export interface Query {
workspaces: Array<WorkspaceType>;
}
export interface QueryApplyDocUpdatesArgs {
docId: Scalars['String']['input'];
op: Scalars['String']['input'];
updates: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
}
export interface QueryErrorArgs {
name: ErrorNames;
}
@@ -3494,6 +3523,18 @@ export type UploadCommentAttachmentMutation = {
uploadCommentAttachment: string;
};
export type ApplyDocUpdatesQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: Scalars['String']['input'];
op: Scalars['String']['input'];
updates: Scalars['String']['input'];
}>;
export type ApplyDocUpdatesQuery = {
__typename?: 'Query';
applyDocUpdates: string;
};
export type AddContextCategoryMutationVariables = Exact<{
options: AddContextCategoryInput;
}>;
@@ -4350,6 +4391,7 @@ export type GetCopilotSessionQuery = {
export type GetCopilotRecentSessionsQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
limit?: InputMaybe<Scalars['Int']['input']>;
offset?: InputMaybe<Scalars['Int']['input']>;
}>;
export type GetCopilotRecentSessionsQuery = {
@@ -5147,6 +5189,8 @@ export type GetWorkspacePageByIdQuery = {
mode: PublicDocMode;
defaultRole: DocRole;
public: boolean;
title: string | null;
summary: string | null;
};
};
};
@@ -6130,6 +6174,11 @@ export type Queries =
variables: ListCommentsQueryVariables;
response: ListCommentsQuery;
}
| {
name: 'applyDocUpdatesQuery';
variables: ApplyDocUpdatesQueryVariables;
response: ApplyDocUpdatesQuery;
}
| {
name: 'listContextObjectQuery';
variables: ListContextObjectQueryVariables;

View File

@@ -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 -->
"
`;

View File

@@ -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': {

View File

@@ -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

View File

@@ -10,6 +10,7 @@ import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
import { setupEffects } from './effects';
import { DesktopLanguageSync } from './language-sync';
import { DesktopThemeSync } from './theme-sync';
const { frameworkProvider } = setupEffects();
@@ -46,6 +47,7 @@ export function App() {
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<DesktopThemeSync />
<DesktopLanguageSync />
<RouterProvider
fallbackElement={<AppContainer fallback />}
router={router}

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