Compare commits

...

38 Commits

Author SHA1 Message Date
fengmk2 b1d7011047 chore(server): use jemalloc to reduce RSS 2025-07-10 11:22:37 +08:00
L-Sun 1fe07410c0 feat(editor): can highlight resolved comment (#13122)
#### PR Dependency Tree


* **PR #13122** 👈

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**
* Inline comments now visually distinguish between unresolved, resolved,
and deleted states.
* Only unresolved inline comments are interactive and highlighted in the
editor.

* **Bug Fixes**
* Improved accuracy in fetching and displaying all comments, including
resolved ones, during initialization.

* **Refactor**
* Enhanced handling of comment resolution and deletion to provide
clearer differentiation in both behavior and appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 03:06:05 +00:00
DarkSky 0f3066f7d0 fix(server): batch size in gemini embedding (#13120)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved reliability of embedding generation for multiple messages,
allowing partial results even if some embeddings fail.
* Enhanced error handling to ensure only valid embeddings are returned.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 15:56:10 +00:00
DarkSky c4c11da976 feat(server): use faster model in ci test (#13038)
fix AI-329
2025-07-09 22:21:30 +08:00
Peng Xiao 38537bf310 fix(core): code block artifact styles (#13116)
fix AI-314

#### PR Dependency Tree


* **PR #13116** 👈

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 theme support for AI artifact tools, with banners and UI
adapting to light or dark mode.
* Enhanced notification handling for user actions like copying or saving
content.

* **Refactor**
* Streamlined the structure of AI artifact tools for better
maintainability and a more consistent user experience.
* Unified and modernized preview and control panels for code and
document compose tools.
* Updated component integrations to consistently pass theme and
notification services.

* **Style**
  * Updated hover effects and visual feedback for artifact tool cards.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #13116** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-09 13:26:06 +00:00
Wu Yue 1f87cd8752 feat(core): add onOpenDoc handler for AFFiNE Intelligence page (#13118)
Close [AI-240](https://linear.app/affine-design/issue/AI-240)

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

## Summary by CodeRabbit

* **New Features**
* Enabled opening specific documents directly from the chat toolbar,
automatically displaying the document in the workbench and focusing the
chat tab in the sidebar.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 12:55:31 +00:00
EYHN f54cb5c296 fix(android): fix android build error (#13117)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Improved chat session and history retrieval with support for paginated
results in the chat interface.

* **Bug Fixes**
* Enhanced reliability when loading large numbers of chat messages and
histories.

* **Refactor**
* Updated chat data handling to align with the latest backend schema and
pagination model.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 12:52:05 +00:00
fengmk2 45c016af8b fix(server): add user id to comment-attachment model (#13113)
close AF-2723



#### PR Dependency Tree


* **PR #13113** 👈

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 attachments now track and display the user who uploaded them.

* **Tests**
* Updated tests to verify that the uploader’s information is correctly
stored and retrieved with comment attachments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 12:16:09 +00:00
Peng Xiao d4c905600b feat(core): support normal attachments (#13112)
fix AF-2722


![image](https://github.com/user-attachments/assets/376a0119-ae8e-4cb4-a31c-2eb6bb56c868)


#### PR Dependency Tree


* **PR #13112** 👈

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**
* Expanded comment editor attachment support to include any file type,
not just images.
* Added file preview and download functionality for non-image
attachments.
* Introduced notifications for attachment upload failures and downloads.
* Added a new AI artifact tool component for enhanced AI tool
integrations.

* **Style**
* Added new styles for generic file previews, including icons, file
info, and delete button.

* **Bug Fixes**
  * Improved error handling and user feedback for attachment uploads.

* **Refactor**
* Unified attachment UI rendering and handling for both images and other
file types.

* **Chores**
* Removed obsolete editor state attribute from comment preview sidebar.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #13112** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-09 11:22:04 +00:00
Peng Xiao f839e5c136 fix(core): should use sonnet 4 for make it real (#13106)
#### PR Dependency Tree


* **PR #13106** 👈

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 code highlighting performance and resource management for
AI-generated code artifacts, resulting in smoother user experience and
more efficient updates.
* **Chores**
* Updated underlying AI model for "Make it real" features, which may
affect AI-generated outputs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 11:10:53 +00:00
L-Sun 39abd1bbb8 fix(editor): can not create surface block comment (#13115)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved comment handling to ensure elements from all selections are
considered, regardless of surface ID.
* Enhanced preview generation for comments to include all relevant
selections without surface-based filtering.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 11:05:03 +00:00
Lakr ecea7bd825 fix: 🚑 compiler issue (#13114) 2025-07-09 10:18:04 +00:00
Wu Yue d10e5ee92f feat(core): completely remove the dependence on EditorHost (#13110)
Close [AI-260](https://linear.app/affine-design/issue/AI-260)

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

* **New Features**
* Added theme support to AI chat and message components, enabling
dynamic theming based on the current app theme.
* Introduced a reactive theme signal to the theme service for improved
theme handling.
* Integrated notification and theme services across various AI chat,
playground, and message components for consistent user experience.

* **Refactor**
* Simplified component APIs by removing dependencies on editor host and
related properties across AI chat, message, and tool components.
* Centralized and streamlined clipboard and markdown conversion
utilities, reducing external dependencies.
* Standardized the interface for context file addition and improved type
usage for better consistency.
* Reworked notification service to a class-based implementation for
improved encapsulation.
* Updated AI chat components to use injected notification and theme
services instead of host-based retrieval.

* **Bug Fixes**
* Improved reliability of copy and notification actions by decoupling
them from editor host dependencies.

* **Chores**
* Updated and cleaned up internal imports and removed unused properties
to enhance maintainability.
  * Added test IDs for sidebar close button to improve test reliability.
  * Updated test prompts in end-to-end tests for consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 10:16:55 +00:00
Cats Juice dace1d1738 fix(core): should show delete permanently for trash page multi-select (#13111)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a confirmation modal before permanently deleting pages from the
trash, ensuring users must confirm before deletion.
* Permanent deletion now displays a toast notification upon completion.

* **Improvements**
* Enhanced deletion actions with callbacks for handling completion,
cancellation, or errors.
* Permanent delete option is now conditionally available based on user
permissions (admin or owner).

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 08:45:26 +00:00
fengmk2 ae74f4ae51 fix(server): should use signed url first (#13109)
#### PR Dependency Tree


* **PR #13109** 👈

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**
* Updated internal handling of comment attachments to improve processing
logic. No visible changes to end-user features or workflows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 08:12:16 +00:00
Peng Xiao 9071c5032d fix(core): should not be able to commit comments when uploading images (#13108)
#### PR Dependency Tree


* **PR #13108** 👈

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**
* The commit button in the comment editor is now properly disabled while
attachments are uploading or when the editor is empty without
attachments, preventing accidental or premature submissions.
* **New Features**
* Attachment delete button now shows a loading state during uploads for
clearer user feedback.
* **Style**
* Updated comment editor attachment button styles for a cleaner and more
consistent appearance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 07:56:34 +00:00
DarkSky 8236ecf486 fix(server): chunk session in migration (#13107)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved the process for updating session records to handle them in
smaller batches, enhancing reliability and performance during data
updates. No changes to user-facing features.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 07:32:02 +00:00
Peng Xiao a50270fc03 fix(core): some ux enhancements on comments (#13105)
fix PD-2688

#### PR Dependency Tree


* **PR #13105** 👈

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

* **New Features**
* Added configurable support for enabling or disabling inline comments.
* Introduced visual indication (strikethrough) for deleted comments in
the comment sidebar.
  * Sidebar now shows when a comment is no longer present in the editor.
* Added a localized placeholder prompt ("What are your thoughts?") in
the comment editor.
* Integrated detailed event tracking for comment actions: create, edit,
delete, and resolve.

* **Improvements**
  * Inline comments are now disabled in shared mode.
* Enhanced synchronization between editor comments and provider state to
remove stale comments.
  * Inline comment features now respect the document’s read-only state.
* Improved mention handling and tracking in comment creation and
editing.
* Comment manager and entities now dynamically track comments present in
the editor.
* Comment configuration updated to enable or disable inline comments
based on settings.

* **Bug Fixes**
  * Prevented comment block creation when in read-only mode.

* **Localization**
  * Added English localization for the comment prompt.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #13105** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-09 04:49:46 +00:00
EYHN ce7fffda08 fix(core): avoid shared page to fetch workspace info (#13104)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved permission handling to correctly identify user roles when the
workspace is in shared mode or has a local flavour, ensuring accurate
permissions are assigned in these scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 03:56:15 +00:00
德布劳外 · 贾贵 3cc33bd40f fix(core): apply model ui (#13084) 2025-07-09 11:55:17 +08:00
fengmk2 ee878e8f27 chore: improve Cloud E2E Test speed (#13103)
Before 11m

![before_wechat_2025-07-09_105625_157](https://github.com/user-attachments/assets/68eb1026-cacd-4d0f-a0b9-e4f76a1df45a)

After 7m

![afterwechat_2025-07-09_110410_383](https://github.com/user-attachments/assets/bc154cfb-88d2-4e25-bfab-bc0ecc0499ce)


#### PR Dependency Tree


* **PR #13103** 👈

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**
* Expanded cloud end-to-end test coverage by increasing test shards from
6 to 10 for improved parallelization.
* Added a new suite of end-to-end tests focused on page sharing,
including scenarios for sharing links, table of contents, edgeless mode,
and image previews.
* Removed several redundant or relocated sharing-related tests,
retaining only the reference link verification in the affected suite.
* Updated test output format to use the "list" reporter for clearer test
results.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 03:32:14 +00:00
fengmk2 95f88c378c fix(server): use new LocalWorkspace ServerFeature instead (#13091)
keep compatibility

close AF-2720



#### PR Dependency Tree


* **PR #13091** 👈

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

* **New Features**
* Added a new `LocalWorkspace` feature flag to server configuration,
enabling more flexible feature management.

* **Deprecations**
* The `allowGuestDemoWorkspace` flag is now deprecated and will be
removed in version 0.25.0. Please use the `features` array for feature
checks instead.

* **Bug Fixes**
* Updated UI and logic throughout the app to rely on the new
`LocalWorkspace` feature flag rather than the deprecated boolean flag.

* **Chores**
* Removed references to `allowGuestDemoWorkspace` from configuration,
queries, and type definitions for improved consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #13091** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-08 15:23:42 +00:00
fengmk2 15db657b1c chore: bump up manticoresearch/manticore to v10 (#12935)
https://github.com/toeverything/AFFiNE/pull/12816



#### PR Dependency Tree


* **PR #12935** 👈

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

* **Chores**
* Updated the default Manticore Search version in development
environment configurations from 9.3.2 to 10.1.0.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 13:42:16 +00:00
Cats Juice e04d407b2f feat(core): show ai-island and navigate to chat page if not available in sidebar (#13085)
close AI-318, AI-317

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

* **New Features**
* Updated the AI chat button label to "AFFiNE Intelligence" and changed
its icon for improved clarity.
* Enhanced the AI chat button's placement in the sidebar for better
accessibility.
* Improved the AI chat button’s visibility and interaction logic based
on current view and sidebar state.
* **Style**
* Adjusted button styles to disable interaction when hidden, enhancing
user experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 13:17:28 +00:00
DarkSky 0bd1f10498 fix(server): session updated at (#13099)
fix AI-325

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

## Summary by CodeRabbit

* **Chores**
* Improved database handling for session update times to ensure more
accurate tracking of session activity.
* Enhanced migration process to better manage and update session
metadata.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 13:16:56 +00:00
Peng Xiao 072fff1460 fix(core): some editor issues (#13096)
fix AI-313, BS-3611

#### PR Dependency Tree


* **PR #13096** 👈

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**
* Improved performance and resource management in code block
highlighting by using a shared highlighter instance across all code
blocks.
* Enhanced the text rendering component with additional reactive
capabilities.

* **Style**
* Updated the comment sidebar header with a new background color for
improved visual consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 10:38:07 +00:00
Cats Juice 81a76634f2 fix(core): long words will overflow in chat panel (#13101) 2025-07-08 10:37:04 +00:00
L-Sun 1d865f16fe feat(editor): comment for edgeless element (#13098)
#### PR Dependency Tree


* **PR #13098** 👈

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 comments on graphical elements, allowing users to
comment on both blocks and graphical elements within surfaces.
* Enhanced comment previews to include graphical elements in selection
summaries.
* Improved editor navigation to focus on commented graphical elements in
addition to blocks and inline texts.

* **Bug Fixes**
* Updated comment highlighting and management to consistently use the
new comment manager across all block and element types.

* **Refactor**
* Renamed and extended the comment manager to handle both block and
element comments.
* Streamlined toolbar configurations by removing outdated comment button
entries and adding a consolidated comment button in the root toolbar.

* **Tests**
* Disabled the mock comment provider integration in the test editor
environment to refine testing setup.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 10:33:09 +00:00
DarkSky e027564d2a fix(server): incorrect abort condition (#13100)
fix AI-308
2025-07-08 10:18:25 +00:00
Wu Yue 3226a0a3fe fix(core): ai tool calling explanation (#13097)
Close [AI-293](https://linear.app/affine-design/issue/AI-293)

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

## Summary by CodeRabbit

* **New Features**
* Tool call cards for website reading now display the specific URL being
accessed.
* Tool call cards for web searches now display the search query being
used.

* **Style**
* Updated tool call instructions to prevent explanations of operations
before execution.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 09:46:13 +00:00
Wu Yue d5c959a83f feat(core): add ai history loading placeholder (#13092)
Close [AI-324](https://linear.app/affine-design/issue/AI-324)

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

* **New Features**
* Added a loading state indicator to the chat panel, displaying a styled
message and icon while history is loading.
* Enhanced the session history component with clear loading and empty
state messages for improved user feedback.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 09:39:53 +00:00
DarkSky d2f016c628 fix(server): get pending embedding docs in event handler (#13095)
maybe fix AI-309
2025-07-08 09:38:16 +00:00
Peng Xiao 839706cf65 feat(core): comment with attachment uploads (#13089)
fix AF-2721, BS-3611

#### PR Dependency Tree


* **PR #13089** 👈

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

* **New Features**
* Added support for image attachments in comments and replies, including
upload, preview, removal, and paste-from-clipboard capabilities.
* Users can add images via file picker or clipboard paste, preview
thumbnails with navigation, and remove images before submitting.
* Commit button activates only when text content or attachments are
present.

* **UI Improvements**
* Enhanced comment editor with a scrollable preview row showing image
thumbnails, delete buttons, and upload status spinners.
* Unified comment and reply components with consistent attachment
support and streamlined action menus.

* **Bug Fixes**
* Fixed minor inconsistencies in editing and deleting comments and
replies.

* **Chores**
* Improved internal handling of comment attachments, upload workflows,
and state management.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->





#### PR Dependency Tree


* **PR #13089** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-08 08:59:24 +00:00
DarkSky 6dac94d90a feat(server): paginated list endpoint (#13026)
fix AI-323
2025-07-08 17:11:58 +08:00
德布劳外 · 贾贵 8c49a45162 fix(core): insert diff not displayed after the expected block (#13086)
> CLOSE AI-319

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

## Summary by CodeRabbit

* **New Features**
* Improved block insertion behavior by specifying the reference block
after which new blocks are inserted.
  
* **Bug Fixes**
* Enhanced accuracy and clarity of block diffing and patch application,
ensuring correct handling of insertions and deletions.

* **Tests**
* Added and updated test cases to verify correct handling of interval
insertions, deletions, and complete block replacements.
* Updated test expectations to include explicit insertion context for
greater consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 08:46:17 +00:00
EYHN f6a45ae20b fix(core): shared mode permission check (#13087)
close CLOUD-191

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved permission handling for shared mode workspaces to prevent
unnecessary permission checks.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 08:23:19 +00:00
Wu Yue afb3907efa fix(core): show actions only if docId equals session.docId (#13080)
Close [AI-240](https://linear.app/affine-design/issue/AI-240)

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

## Summary by CodeRabbit

* **New Features**
* Improved logic for displaying actions in AI chat content, ensuring
actions are shown only when appropriate based on session and document
context.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 07:48:38 +00:00
fengmk2 db79c00ea7 feat(server): support read all notifications (#13083)
close AF-2719



#### PR Dependency Tree


* **PR #13083** 👈

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 the ability to mark all notifications as read with a single
action.
  
* **Bug Fixes**
  * Ensured notifications marked as read are no longer shown as unread.

* **Tests**
* Introduced new tests to verify the functionality of marking all
notifications as read.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 07:19:45 +00:00
245 changed files with 6828 additions and 2637 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ services:
image: redis
indexer:
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.3.2}
image: manticoresearch/manticore:${MANTICORE_VERSION:-10.1.0}
ulimits:
nproc: 65535
nofile:
+1 -1
View File
@@ -12,4 +12,4 @@ DB_DATABASE_NAME=affine
# ELASTIC_PLATFORM=linux/arm64
# manticoresearch
MANTICORE_VERSION=9.3.2
MANTICORE_VERSION=10.1.0
+1 -1
View File
@@ -26,7 +26,7 @@ services:
# https://manual.manticoresearch.com/Starting_the_server/Docker
manticoresearch:
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.3.2}
image: manticoresearch/manticore:${MANTICORE_VERSION:-10.1.0}
ports:
- 9308:9308
ulimits:
+4 -1
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"]
+24 -12
View File
@@ -1064,24 +1064,36 @@ jobs:
fail-fast: false
matrix:
tests:
- name: 'Cloud E2E Test 1/6'
- name: 'Cloud E2E Test 1/10'
shard: 1
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=1/6
- name: 'Cloud E2E Test 2/6'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=1/10
- name: 'Cloud E2E Test 2/10'
shard: 2
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=2/6
- name: 'Cloud E2E Test 3/6'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=2/10
- name: 'Cloud E2E Test 3/10'
shard: 3
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=3/6
- name: 'Cloud E2E Test 4/6'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=3/10
- name: 'Cloud E2E Test 4/10'
shard: 4
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=4/6
- name: 'Cloud E2E Test 5/6'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=4/10
- name: 'Cloud E2E Test 5/10'
shard: 5
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=5/6
- name: 'Cloud E2E Test 6/6'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=5/10
- name: 'Cloud E2E Test 6/10'
shard: 6
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=6/6
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=6/10
- name: 'Cloud E2E Test 7/10'
shard: 7
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=7/10
- name: 'Cloud E2E Test 8/10'
shard: 8
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=8/10
- name: 'Cloud E2E Test 9/10'
shard: 9
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=9/10
- name: 'Cloud E2E Test 10/10'
shard: 10
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=10/10
- name: 'Cloud Desktop E2E Test'
shard: desktop
script: |
@@ -17,7 +17,7 @@ import {
AttachmentBlockStyles,
} from '@blocksuite/affine-model';
import {
BlockCommentManager,
BlockElementCommentManager,
CitationProvider,
DocModeProvider,
FileSizeLimitProvider,
@@ -96,7 +96,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
.getOptional(BlockElementCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
@@ -10,7 +10,6 @@ import {
} from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarAction,
type ToolbarActionGroup,
type ToolbarModuleConfig,
@@ -241,10 +240,6 @@ const builtinToolbarConfig = {
replaceAction,
downloadAction,
captionAction,
{
id: 'f.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',
@@ -8,7 +8,7 @@ import type {
} from '@blocksuite/affine-model';
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import {
BlockCommentManager,
BlockElementCommentManager,
CitationProvider,
DocModeProvider,
LinkPreviewServiceIdentifier,
@@ -132,7 +132,7 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
.getOptional(BlockElementCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
@@ -17,7 +17,6 @@ import {
} from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
blockCommentToolbarButton,
EmbedIframeService,
EmbedOptionProvider,
type LinkEventType,
@@ -289,10 +288,6 @@ const builtinToolbarConfig = {
},
} satisfies ToolbarActionGroup<ToolbarAction>,
captionAction,
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',
@@ -19,8 +19,12 @@ import {
export class CodeBlockHighlighter extends LifeCycleWatcher {
static override key = 'code-block-highlighter';
private _darkThemeKey: string | undefined;
// Singleton highlighter instance
private static _sharedHighlighter: HighlighterCore | null = null;
private static _highlighterPromise: Promise<HighlighterCore> | null = null;
private static _refCount = 0;
private _darkThemeKey: string | undefined;
private _lightThemeKey: string | undefined;
highlighter$: Signal<HighlighterCore | null> = signal(null);
@@ -44,18 +48,45 @@ export class CodeBlockHighlighter extends LifeCycleWatcher {
this.highlighter$.value = highlighter;
};
private static async _getOrCreateHighlighter(): Promise<HighlighterCore> {
if (CodeBlockHighlighter._sharedHighlighter) {
return CodeBlockHighlighter._sharedHighlighter;
}
if (!CodeBlockHighlighter._highlighterPromise) {
CodeBlockHighlighter._highlighterPromise = createHighlighterCore({
engine: createOnigurumaEngine(() => getWasm),
}).then(highlighter => {
CodeBlockHighlighter._sharedHighlighter = highlighter;
return highlighter;
});
}
return CodeBlockHighlighter._highlighterPromise;
}
override mounted(): void {
super.mounted();
createHighlighterCore({
engine: createOnigurumaEngine(() => getWasm),
})
CodeBlockHighlighter._refCount++;
CodeBlockHighlighter._getOrCreateHighlighter()
.then(this._loadTheme)
.catch(console.error);
}
override unmounted(): void {
this.highlighter$.value?.dispose();
CodeBlockHighlighter._refCount--;
// Only dispose the shared highlighter when no instances are using it
if (
CodeBlockHighlighter._refCount === 0 &&
CodeBlockHighlighter._sharedHighlighter
) {
CodeBlockHighlighter._sharedHighlighter.dispose();
CodeBlockHighlighter._sharedHighlighter = null;
CodeBlockHighlighter._highlighterPromise = null;
}
}
}
@@ -6,7 +6,7 @@ import {
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
} from '@blocksuite/affine-shared/consts';
import {
BlockCommentManager,
BlockElementCommentManager,
DocModeProvider,
NotificationProvider,
} from '@blocksuite/affine-shared/services';
@@ -394,7 +394,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
.getOptional(BlockElementCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
@@ -10,7 +10,7 @@ import { toast } from '@blocksuite/affine-components/toast';
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
BlockCommentManager,
BlockElementCommentManager,
CommentProviderIdentifier,
DocModeProvider,
NotificationProvider,
@@ -316,7 +316,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
.getOptional(BlockElementCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
@@ -11,7 +11,6 @@ import {
} from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
blockCommentToolbarButton,
DocDisplayMetaProvider,
EditorSettingProvider,
type LinkEventType,
@@ -306,10 +305,6 @@ const builtinToolbarConfig = {
},
} satisfies ToolbarActionGroup<ToolbarAction>,
captionAction,
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',
@@ -16,7 +16,6 @@ import {
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
blockCommentToolbarButton,
EditorSettingProvider,
type LinkEventType,
type OpenDocMode,
@@ -226,10 +225,6 @@ const builtinToolbarConfig = {
openDocActionGroup,
conversionsActionGroup,
captionAction,
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',
@@ -9,7 +9,7 @@ import {
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import {
BlockCommentManager,
BlockElementCommentManager,
DocModeProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
@@ -65,7 +65,7 @@ export class EmbedBlockComponent<
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
.getOptional(BlockElementCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
@@ -13,7 +13,6 @@ import {
} from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
blockCommentToolbarButton,
EmbedOptionProvider,
type LinkEventType,
type ToolbarAction,
@@ -349,10 +348,6 @@ function createBuiltinToolbarConfigForExternal(
});
},
},
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',
@@ -1,7 +1,6 @@
import { ImageBlockModel } from '@blocksuite/affine-model';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
@@ -50,10 +49,6 @@ const builtinToolbarConfig = {
});
},
},
{
id: 'c.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',
@@ -146,10 +141,6 @@ const builtinSurfaceToolbarConfig = {
});
},
},
{
id: 'c.comment',
...blockCommentToolbarButton,
},
],
when: ctx => ctx.getSurfaceModelsByType(ImageBlockModel).length === 1,
@@ -6,7 +6,7 @@ import { ResourceController } from '@blocksuite/affine-components/resource';
import type { ImageBlockModel } from '@blocksuite/affine-model';
import { ImageSelection } from '@blocksuite/affine-shared/selection';
import {
BlockCommentManager,
BlockElementCommentManager,
ToolbarRegistryIdentifier,
} from '@blocksuite/affine-shared/services';
import { formatSize } from '@blocksuite/affine-shared/utils';
@@ -71,7 +71,7 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
.getOptional(BlockElementCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
@@ -8,7 +8,7 @@ import {
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
} from '@blocksuite/affine-shared/consts';
import {
BlockCommentManager,
BlockElementCommentManager,
CitationProvider,
DocModeProvider,
} from '@blocksuite/affine-shared/services';
@@ -112,7 +112,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
.getOptional(BlockElementCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
@@ -17,6 +17,7 @@ import {
} from '@blocksuite/affine-model';
import {
ActionPlacement,
blockCommentToolbarButton,
type ElementLockEvent,
type ToolbarAction,
type ToolbarContext,
@@ -305,6 +306,12 @@ export const builtinMiscToolbarConfig = {
},
},
{
placement: ActionPlacement.End,
id: 'c.comment',
...blockCommentToolbarButton,
},
// More actions
...moreActions.map(action => ({
...action,
@@ -305,7 +305,10 @@ export class PageRootBlockComponent extends BlockComponent<RootBlockModel> {
);
// make sure there is a block can be focused
if (notes.length === 0 || notes[notes.length - 1].children.length === 0) {
if (
!this.store.readonly$.value &&
(notes.length === 0 || notes[notes.length - 1].children.length === 0)
) {
this.std.command.exec(appendParagraphCommand);
return;
}
@@ -322,7 +325,7 @@ export class PageRootBlockComponent extends BlockComponent<RootBlockModel> {
parseFloat(paddingLeft),
parseFloat(paddingRight)
);
if (!isClickOnBlankArea) {
if (!isClickOnBlankArea && !this.store.readonly$.value) {
const lastBlock = notes[notes.length - 1].lastChild();
if (
!lastBlock ||
@@ -5,7 +5,6 @@ import {
} from '@blocksuite/affine-shared/commands';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
@@ -62,10 +61,6 @@ export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
surfaceRefBlock.captionElement.show();
},
},
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
id: 'a.clipboard',
placement: ActionPlacement.More,
@@ -13,7 +13,7 @@ import {
type SurfaceRefBlockModel,
} from '@blocksuite/affine-model';
import {
BlockCommentManager,
BlockElementCommentManager,
DocModeProvider,
EditPropsStore,
type OpenDocMode,
@@ -145,7 +145,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
get isCommentHighlighted() {
return (
this.std
.getOptional(BlockCommentManager)
.getOptional(BlockElementCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
+2 -2
View File
@@ -9,7 +9,7 @@ import {
} from '@blocksuite/affine-ext-loader';
import {
AutoClearSelectionService,
BlockCommentManager,
BlockElementCommentManager,
CitationService,
DefaultOpenDocExtension,
DNDAPIExtension,
@@ -79,7 +79,7 @@ export class FoundationViewExtension extends ViewExtensionProvider<FoundationVie
LinkPreviewCache,
LinkPreviewService,
CitationService,
BlockCommentManager,
BlockElementCommentManager,
]);
context.register(clipboardConfigs);
if (this.isEdgeless(context.scope)) {
@@ -17,9 +17,9 @@ import {
TextAlign,
type TextStyleProps,
} from '@blocksuite/affine-model';
import type {
ToolbarActions,
ToolbarContext,
import {
type ToolbarActions,
type ToolbarContext,
} from '@blocksuite/affine-shared/services';
import {
getMostCommonResolvedValue,
@@ -1,2 +1,4 @@
export { InlineCommentManager } from './inline-comment-manager';
export * from './inline-spec';
export * from './utils';
export * from './view';
@@ -1,10 +1,11 @@
import { getInlineEditorByModel } from '@blocksuite/affine-rich-text';
import { getSelectedBlocksCommand } from '@blocksuite/affine-shared/commands';
import {
BlockCommentManager,
BlockElementCommentManager,
type CommentId,
CommentProviderIdentifier,
findAllCommentedBlocks,
findAllCommentedElements,
} from '@blocksuite/affine-shared/services';
import type { AffineInlineEditor } from '@blocksuite/affine-shared/types';
import { DisposableGroup } from '@blocksuite/global/disposable';
@@ -42,10 +43,14 @@ export class InlineCommentManager extends LifeCycleWatcher {
this._disposables.add(provider.onCommentAdded(this._handleAddComment));
this._disposables.add(
provider.onCommentDeleted(this._handleDeleteAndResolve)
provider.onCommentDeleted(id =>
this._handleDeleteAndResolve(id, 'delete')
)
);
this._disposables.add(
provider.onCommentResolved(this._handleDeleteAndResolve)
provider.onCommentResolved(id =>
this._handleDeleteAndResolve(id, 'resolve')
)
);
this._disposables.add(
provider.onCommentHighlighted(this._handleHighlightComment)
@@ -63,23 +68,35 @@ export class InlineCommentManager extends LifeCycleWatcher {
const provider = this._provider;
if (!provider) return;
const commentsInProvider = await provider.getComments('unresolved');
const commentsInProvider = await provider.getComments('all');
const commentsInEditor = this.getCommentsInEditor();
// remove comments that are in editor but not in provider
// which means the comment may be removed or resolved in provider side
difference(commentsInEditor, commentsInProvider).forEach(comment => {
this.std
.get(BlockElementCommentManager)
.handleDeleteAndResolve(comment, 'delete');
});
}
getCommentsInEditor() {
const inlineComments = [...findAllCommentedTexts(this.std.store).values()];
const blockComments = findAllCommentedBlocks(this.std.store).flatMap(
block => Object.keys(block.props.comments)
);
const surfaceComments = findAllCommentedElements(this.std.store).flatMap(
element => Object.keys(element.comments)
);
const commentsInEditor = [
...new Set([...inlineComments, ...blockComments]),
...new Set([...inlineComments, ...blockComments, ...surfaceComments]),
];
// remove comments that are in editor but not in provider
// which means the comment may be removed or resolved in provider side
difference(commentsInEditor, commentsInProvider).forEach(comment => {
this._handleDeleteAndResolve(comment);
this.std.get(BlockCommentManager).handleDeleteAndResolve(comment);
});
return commentsInEditor;
}
private readonly _handleAddComment = (
@@ -150,7 +167,10 @@ export class InlineCommentManager extends LifeCycleWatcher {
});
};
private readonly _handleDeleteAndResolve = (id: CommentId) => {
private readonly _handleDeleteAndResolve = (
id: CommentId,
type: 'delete' | 'resolve'
) => {
const commentedTexts = findCommentedTexts(this.std.store, id);
if (commentedTexts.length === 0) return;
@@ -164,7 +184,7 @@ export class InlineCommentManager extends LifeCycleWatcher {
inlineEditor?.formatText(
selection.from,
{
[`comment-${id}`]: null,
[`comment-${id}`]: type === 'delete' ? null : false,
},
{
withoutTransact: true,
@@ -22,7 +22,7 @@ import { isEqual } from 'lodash-es';
})
export class InlineComment extends WithDisposable(ShadowlessElement) {
static override styles = css`
inline-comment {
inline-comment.unresolved {
display: inline-block;
background-color: ${unsafeCSSVarV2('block/comment/highlightDefault')};
border-bottom: 2px solid
@@ -41,6 +41,9 @@ export class InlineComment extends WithDisposable(ShadowlessElement) {
})
accessor commentIds!: string[];
@property({ attribute: false })
accessor unresolved = false;
private _index: number = 0;
@consume({ context: stdContext })
@@ -54,8 +57,10 @@ export class InlineComment extends WithDisposable(ShadowlessElement) {
}
private readonly _handleClick = () => {
this._provider?.highlightComment(this.commentIds[this._index]);
this._index = (this._index + 1) % this.commentIds.length;
if (this.unresolved) {
this._provider?.highlightComment(this.commentIds[this._index]);
this._index = (this._index + 1) % this.commentIds.length;
}
};
private readonly _handleHighlight = (id: CommentId | null) => {
@@ -89,6 +94,13 @@ export class InlineComment extends WithDisposable(ShadowlessElement) {
this.classList.remove('highlighted');
}
}
if (_changedProperties.has('unresolved')) {
if (this.unresolved) {
this.classList.add('unresolved');
} else {
this.classList.remove('unresolved');
}
}
}
override render() {
@@ -21,18 +21,39 @@ export const CommentInlineSpecExtension =
),
match: delta => {
if (!delta.attributes) return false;
const comments = Object.entries(delta.attributes).filter(
([key, value]) => isInlineCommendId(key) && value === true
);
const comments = Object.keys(delta.attributes).filter(isInlineCommendId);
return comments.length > 0;
},
renderer: ({ delta, children }) =>
html`<inline-comment .commentIds=${extractCommentIdFromDelta(delta)}
renderer: ({ delta, children }) => {
if (!delta.attributes) return html`${nothing}`;
const unresolved = Object.entries(delta.attributes).some(
([key, value]) => isInlineCommendId(key) && value === true
);
return html`<inline-comment
.unresolved=${unresolved}
.commentIds=${extractCommentIdFromDelta(delta)}
>${when(
children,
() => html`${children}`,
() => nothing
)}</inline-comment
>`,
>`;
},
wrapper: true,
});
export const NullCommentInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'comment',
schema: dynamicSchema(
isInlineCommendId,
z.boolean().optional().nullable().catch(undefined)
),
match: () => false,
renderer: () => html``,
});
// reuse the same identifier
NullCommentInlineSpecExtension.identifier =
CommentInlineSpecExtension.identifier;
+25 -5
View File
@@ -2,21 +2,41 @@ import {
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader';
import z from 'zod';
import { effects } from './effects';
import { InlineCommentManager } from './inline-comment-manager';
import { CommentInlineSpecExtension } from './inline-spec';
import {
CommentInlineSpecExtension,
NullCommentInlineSpecExtension,
} from './inline-spec';
export class InlineCommentViewExtension extends ViewExtensionProvider {
const optionsSchema = z.object({
enabled: z.boolean().optional().default(true),
});
export class InlineCommentViewExtension extends ViewExtensionProvider<
z.infer<typeof optionsSchema>
> {
override name = 'affine-inline-comment';
override schema = optionsSchema;
override effect(): void {
super.effect();
effects();
}
override setup(context: ViewExtensionContext) {
super.setup(context);
context.register([CommentInlineSpecExtension, InlineCommentManager]);
override setup(
context: ViewExtensionContext,
options?: z.infer<typeof optionsSchema>
) {
super.setup(context, options);
context.register([
options?.enabled
? CommentInlineSpecExtension
: NullCommentInlineSpecExtension,
InlineCommentManager,
]);
}
}
@@ -1,118 +0,0 @@
import { DividerBlockModel } from '@blocksuite/affine-model';
import { DisposableGroup } from '@blocksuite/global/disposable';
import {
BlockSelection,
LifeCycleWatcher,
TextSelection,
} from '@blocksuite/std';
import type { BaseSelection, BlockModel } from '@blocksuite/store';
import { signal } from '@preact/signals-core';
import { getSelectedBlocksCommand } from '../../commands';
import { ImageSelection } from '../../selection';
import { matchModels } from '../../utils';
import { type CommentId, CommentProviderIdentifier } from './comment-provider';
import { findCommentedBlocks } from './utils';
export class BlockCommentManager extends LifeCycleWatcher {
static override key = 'block-comment-manager';
private readonly _highlightedCommentId$ = signal<CommentId | null>(null);
private readonly _disposables = new DisposableGroup();
private get _provider() {
return this.std.getOptional(CommentProviderIdentifier);
}
isBlockCommentHighlighted(
block: BlockModel<{ comments?: Record<CommentId, boolean> }>
) {
const comments = block.props.comments;
if (!comments) return false;
return (
this._highlightedCommentId$.value !== null &&
Object.keys(comments).includes(this._highlightedCommentId$.value)
);
}
override mounted() {
const provider = this._provider;
if (!provider) return;
this._disposables.add(provider.onCommentAdded(this._handleAddComment));
this._disposables.add(
provider.onCommentDeleted(this.handleDeleteAndResolve)
);
this._disposables.add(
provider.onCommentResolved(this.handleDeleteAndResolve)
);
this._disposables.add(
provider.onCommentHighlighted(this._handleHighlightComment)
);
}
override unmounted() {
this._disposables.dispose();
}
private readonly _handleAddComment = (
id: CommentId,
selections: BaseSelection[]
) => {
const blocksFromTextRange = selections
.filter((s): s is TextSelection => s.is(TextSelection))
.map(s => {
const [_, { selectedBlocks }] = this.std.command.exec(
getSelectedBlocksCommand,
{
textSelection: s,
}
);
if (!selectedBlocks) return [];
return selectedBlocks.map(b => b.model).filter(m => !m.text);
});
const needCommentBlocks = [
...blocksFromTextRange.flat(),
...selections
.filter(s => s instanceof BlockSelection || s instanceof ImageSelection)
.map(({ blockId }) => this.std.store.getModelById(blockId))
.filter(
(m): m is BlockModel =>
m !== null && !matchModels(m, [DividerBlockModel])
),
];
if (needCommentBlocks.length === 0) return;
this.std.store.withoutTransact(() => {
needCommentBlocks.forEach(block => {
const comments = (
'comments' in block.props &&
typeof block.props.comments === 'object' &&
block.props.comments !== null
? block.props.comments
: {}
) as Record<CommentId, boolean>;
this.std.store.updateBlock(block, {
comments: { [id]: true, ...comments },
});
});
});
};
readonly handleDeleteAndResolve = (id: CommentId) => {
const commentedBlocks = findCommentedBlocks(this.std.store, id);
this.std.store.withoutTransact(() => {
commentedBlocks.forEach(block => {
delete block.props.comments[id];
});
});
};
private readonly _handleHighlightComment = (id: CommentId | null) => {
this._highlightedCommentId$.value = id;
};
}
@@ -0,0 +1,181 @@
import { DividerBlockModel } from '@blocksuite/affine-model';
import { DisposableGroup } from '@blocksuite/global/disposable';
import {
BlockSelection,
LifeCycleWatcher,
SurfaceSelection,
TextSelection,
} from '@blocksuite/std';
import {
GfxControllerIdentifier,
type GfxModel,
type GfxPrimitiveElementModel,
} from '@blocksuite/std/gfx';
import type { BaseSelection, BlockModel } from '@blocksuite/store';
import { signal } from '@preact/signals-core';
import { getSelectedBlocksCommand } from '../../commands';
import { ImageSelection } from '../../selection';
import { matchModels } from '../../utils';
import { type CommentId, CommentProviderIdentifier } from './comment-provider';
import { findCommentedBlocks, findCommentedElements } from './utils';
export class BlockElementCommentManager extends LifeCycleWatcher {
static override key = 'block-element-comment-manager';
private readonly _highlightedCommentId$ = signal<CommentId | null>(null);
private readonly _disposables = new DisposableGroup();
private get _provider() {
return this.std.getOptional(CommentProviderIdentifier);
}
isBlockCommentHighlighted(
block: BlockModel<{ comments?: Record<CommentId, boolean> }>
) {
const comments = block.props.comments;
if (!comments) return false;
return (
this._highlightedCommentId$.value !== null &&
Object.keys(comments).includes(this._highlightedCommentId$.value)
);
}
isElementCommentHighlighted(element: GfxPrimitiveElementModel) {
const comments = element.comments;
if (!comments) return false;
return (
this._highlightedCommentId$.value !== null &&
Object.keys(comments).includes(this._highlightedCommentId$.value)
);
}
override mounted() {
const provider = this._provider;
if (!provider) return;
this._disposables.add(provider.onCommentAdded(this._handleAddComment));
this._disposables.add(
provider.onCommentDeleted(id => this.handleDeleteAndResolve(id, 'delete'))
);
this._disposables.add(
provider.onCommentResolved(id =>
this.handleDeleteAndResolve(id, 'resolve')
)
);
this._disposables.add(
provider.onCommentHighlighted(this._handleHighlightComment)
);
}
override unmounted() {
this._disposables.dispose();
}
private readonly _handleAddComment = (
id: CommentId,
selections: BaseSelection[]
) => {
// get blocks from text range that some no-text blocks are selected such as image, bookmark, etc.
const noTextBlocksFromTextRange = selections
.filter((s): s is TextSelection => s.is(TextSelection))
.flatMap(s => {
const [_, { selectedBlocks }] = this.std.command.exec(
getSelectedBlocksCommand,
{
textSelection: s,
}
);
if (!selectedBlocks) return [];
return selectedBlocks.map(b => b.model).filter(m => !m.text);
});
const blocksFromBlockSelection = selections
.filter(s => s instanceof BlockSelection || s instanceof ImageSelection)
.map(({ blockId }) => this.std.store.getModelById(blockId))
.filter(
(m): m is BlockModel =>
m !== null && !matchModels(m, [DividerBlockModel])
);
const needCommentBlocks = [
...noTextBlocksFromTextRange,
...blocksFromBlockSelection,
];
if (needCommentBlocks.length !== 0) {
this.std.store.withoutTransact(() => {
needCommentBlocks.forEach(block => {
const comments = (
'comments' in block.props &&
typeof block.props.comments === 'object' &&
block.props.comments !== null
? block.props.comments
: {}
) as Record<CommentId, boolean>;
this.std.store.updateBlock(block, {
comments: { [id]: true, ...comments },
});
});
});
}
const gfx = this.std.get(GfxControllerIdentifier);
const elementsFromSurfaceSelection = selections
.filter(s => s instanceof SurfaceSelection)
.flatMap(({ elements }) => {
return elements
.map(id => gfx.getElementById<GfxModel>(id))
.filter(m => m !== null);
});
if (elementsFromSurfaceSelection.length !== 0) {
this.std.store.withoutTransact(() => {
elementsFromSurfaceSelection.forEach(element => {
const comments =
'comments' in element &&
typeof element.comments === 'object' &&
element.comments !== null
? element.comments
: {};
gfx.updateElement(element, {
comments: { [id]: true, ...comments },
});
});
});
}
};
readonly handleDeleteAndResolve = (
id: CommentId,
type: 'delete' | 'resolve'
) => {
const commentedBlocks = findCommentedBlocks(this.std.store, id);
this.std.store.withoutTransact(() => {
commentedBlocks.forEach(block => {
if (type === 'delete') {
delete block.props.comments[id];
} else {
block.props.comments[id] = false;
}
});
});
const commentedElements = findCommentedElements(this.std.store, id);
this.std.store.withoutTransact(() => {
commentedElements.forEach(element => {
if (type === 'delete') {
delete element.comments[id];
} else {
element.comments[id] = false;
}
});
});
};
private readonly _handleHighlightComment = (id: CommentId | null) => {
this._highlightedCommentId$.value = id;
};
}
@@ -1,3 +1,3 @@
export * from './block-comment-manager';
export * from './block-element-comment-manager';
export * from './comment-provider';
export * from './utils';
@@ -1,6 +1,10 @@
import { CommentIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import type { BlockModel, Store } from '@blocksuite/store';
import { BlockSelection, SurfaceSelection } from '@blocksuite/std';
import type {
GfxPrimitiveElementModel,
SurfaceBlockModel,
} from '@blocksuite/std/gfx';
import { BlockModel, type Store } from '@blocksuite/store';
import type { ToolbarAction } from '../toolbar-service';
import { type CommentId, CommentProviderIdentifier } from './comment-provider';
@@ -22,6 +26,31 @@ export function findCommentedBlocks(store: Store, commentId: CommentId) {
});
}
export function findAllCommentedElements(store: Store) {
type CommentedElement = GfxPrimitiveElementModel & {
comments: Record<CommentId, boolean>;
};
const surface = store.getModelsByFlavour('affine:surface')[0] as
| SurfaceBlockModel
| undefined;
if (!surface) return [];
return surface.elementModels.filter(
(element): element is CommentedElement => {
return (
element.comments !== undefined &&
Object.keys(element.comments).length > 0
);
}
);
}
export function findCommentedElements(store: Store, commentId: CommentId) {
return findAllCommentedElements(store).filter(element => {
return element.comments[commentId];
});
}
export const blockCommentToolbarButton: Omit<ToolbarAction, 'id'> = {
tooltip: 'Comment',
when: ({ std }) => !!std.getOptional(CommentProviderIdentifier),
@@ -29,22 +58,26 @@ export const blockCommentToolbarButton: Omit<ToolbarAction, 'id'> = {
run: ctx => {
const commentProvider = ctx.std.getOptional(CommentProviderIdentifier);
if (!commentProvider) return;
const selections = ctx.selection.value;
const selections = ctx.selection.value;
const model = ctx.getCurrentModel();
if (selections.length > 1) {
// may be hover on a block or element, in this case
// the selection is empty, so we need to get the current model
if (model && selections.length === 0) {
if (model instanceof BlockModel) {
commentProvider.addComment([
new BlockSelection({
blockId: model.id,
}),
]);
} else if (ctx.gfx.surface?.id) {
commentProvider.addComment([
new SurfaceSelection(ctx.gfx.surface.id, [model.id], false),
]);
}
} else if (selections.length > 0) {
commentProvider.addComment(selections);
} else if (model) {
commentProvider.addComment([
new BlockSelection({
blockId: model.id,
}),
]);
} else if (selections.length === 1) {
commentProvider.addComment(selections);
} else {
return;
}
},
};
@@ -40,7 +40,6 @@ export interface NotificationService {
}[];
onClose?: () => void;
}): void;
/**
* Notify with undo action, it is a helper function to notify with undo action.
* And the notification card will be closed when undo action is triggered by shortcut key or other ways.
@@ -55,13 +54,16 @@ export const NotificationProvider = createIdentifier<NotificationService>(
);
export function NotificationExtension(
notificationService: Omit<NotificationService, 'notifyWithUndoAction'>
notificationService: NotificationService
): ExtensionType {
return {
setup: di => {
di.addImpl(NotificationProvider, provider => {
return {
...notificationService,
notify: notificationService.notify,
toast: notificationService.toast,
confirm: notificationService.confirm,
prompt: notificationService.prompt,
notifyWithUndoAction: options => {
notifyWithUndoActionImpl(
provider,
@@ -52,6 +52,7 @@ export type BaseElementProps = {
index: string;
seed: number;
lockedBySelf?: boolean;
comments?: Record<string, boolean>;
};
export type SerializedElement = Record<string, unknown> & {
@@ -60,6 +61,7 @@ export type SerializedElement = Record<string, unknown> & {
id: string;
index: string;
lockedBySelf?: boolean;
comments?: Record<string, boolean>;
props: Record<string, unknown>;
};
export abstract class GfxPrimitiveElementModel<
@@ -372,6 +374,9 @@ export abstract class GfxPrimitiveElementModel<
@field()
accessor seed!: number;
@field()
accessor comments: Record<string, boolean> | undefined = undefined;
}
export abstract class GfxGroupLikeElementModel<
@@ -31,7 +31,7 @@ export interface BlockStdOptions {
extensions: ExtensionType[];
}
const internalExtensions = [
export const internalExtensions = [
ServiceManager,
CommandManager,
UIEventDispatcher,
@@ -33,6 +33,7 @@ export function getTestCommonExtensions(
di.override(DocModeProvider, mockDocModeService(editor));
},
},
// CommentProviderExtension(mockCommentProvider()),
];
}
@@ -1,5 +1,9 @@
-- AlterTable
ALTER TABLE "ai_sessions_metadata" ADD COLUMN "updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE ai_sessions_metadata ADD COLUMN updated_at TIMESTAMPTZ(3);
UPDATE ai_sessions_metadata SET updated_at = created_at;
ALTER TABLE ai_sessions_metadata ALTER COLUMN updated_at SET NOT NULL, ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
-- DropIndex
DROP INDEX IF EXISTS "ai_session_unique_doc_session_idx";
@@ -369,7 +369,7 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
.map(c => JSON.parse(c.citationJson).type)
.filter(type => ['attachment', 'doc'].includes(type)).length ===
0,
'should not have citation'
`should not have citation: ${JSON.stringify(c, null, 2)}`
);
});
},
@@ -1891,7 +1891,7 @@ test('should handle generateSessionTitle correctly under various conditions', as
await session.generateSessionTitle({ sessionId });
if (testCase.expectSnapshot) {
const sessionState = await session.getSession(sessionId);
const sessionState = await session.getSessionInfo(sessionId);
t.snapshot(
{
chatWithPromptCalled: testCase.expectNotCalled
@@ -21,3 +21,13 @@ e2e('should comment feature enabled by default', async t => {
JSON.stringify(serverConfig, null, 2)
);
});
e2e('should enable local workspace feature by default', async t => {
const { serverConfig } = await app.gql({ query: serverConfigQuery });
t.is(
serverConfig.features.includes(ServerFeature.LocalWorkspace),
true,
JSON.stringify(serverConfig, null, 2)
);
});
@@ -8,6 +8,7 @@ import {
notificationCountQuery,
NotificationObjectType,
NotificationType,
readAllNotificationsMutation,
readNotificationMutation,
} from '@affine/graphql';
@@ -677,3 +678,41 @@ e2e('should list and count notifications', async t => {
t.is(result3.currentUser!.notifications.edges.length, 0);
}
});
e2e('should mark all notifications as read', async t => {
const { member, owner, workspace } = await init();
await app.login(owner);
await app.gql({
query: mentionUserMutation,
variables: {
input: {
userId: member.id,
workspaceId: workspace.id,
doc: {
id: 'doc-id-1',
title: 'doc-title-1',
blockId: 'block-id-1',
mode: DocMode.page,
},
},
},
});
await app.login(member);
await app.gql({
query: readAllNotificationsMutation,
});
const result = await app.gql({
query: listNotificationsQuery,
variables: {
pagination: {
first: 10,
offset: 0,
},
},
});
t.is(result.currentUser!.notifications.totalCount, 0);
});
@@ -73,7 +73,8 @@ e2e('should get comment attachment body', async t => {
docId,
key,
'test.txt',
Buffer.from('test')
Buffer.from('test'),
owner.id
);
const res = await app.GET(
@@ -361,7 +361,8 @@ export class CommentResolver {
docId,
key,
attachment.filename ?? key,
buffer
buffer,
me.id
);
return this.commentAttachmentStorage.getUrl(workspaceId, docId, key);
}
@@ -85,6 +85,7 @@ export class ServerConfigResolver {
baseUrl: this.url.requestBaseUrl,
type: env.DEPLOYMENT_TYPE,
features: this.server.features,
// TODO(@fengmk2): remove this field after the feature 0.25.0 is released
allowGuestDemoWorkspace: this.config.flags.allowGuestDemoWorkspace,
};
}
@@ -110,6 +110,13 @@ export class ServerService implements OnApplicationBootstrap {
this.event.emit('config.changed', event);
}
@OnEvent('config.changed')
onConfigChanged(event: Events['config.changed']) {
if ('flags' in event.updates) {
this.onFlagsChanged();
}
}
async revalidateConfig() {
const overrides = await this.loadDbOverrides();
this.configFactory.override(overrides);
@@ -122,6 +129,7 @@ export class ServerService implements OnApplicationBootstrap {
await this.event.emitAsync('config.init', {
config: this.configFactory.config,
});
this.onFlagsChanged();
}
private async loadDbOverrides() {
@@ -134,4 +142,13 @@ export class ServerService implements OnApplicationBootstrap {
return overrides;
}
private onFlagsChanged() {
const flags = this.configFactory.config.flags;
if (flags.allowGuestDemoWorkspace) {
this.enableFeature(ServerFeature.LocalWorkspace);
} else {
this.disableFeature(ServerFeature.LocalWorkspace);
}
}
}
@@ -10,6 +10,7 @@ export enum ServerFeature {
OAuth = 'oauth',
Indexer = 'indexer',
Comment = 'comment',
LocalWorkspace = 'local_workspace',
}
registerEnumType(ServerFeature, {
@@ -42,6 +43,8 @@ export class ServerConfigType {
@Field(() => Boolean, {
description: 'Whether allow guest users to create demo workspaces.',
deprecationReason:
'This field is deprecated, please use `features` instead. Will be removed in 0.25.0',
})
allowGuestDemoWorkspace!: boolean;
}
@@ -100,6 +100,14 @@ export class UserNotificationResolver {
await this.service.markAsRead(me.id, notificationId);
return true;
}
@Mutation(() => Boolean, {
description: 'mark all notifications as read',
})
async readAllNotifications(@CurrentUser() me: UserType) {
await this.service.markAllAsRead(me.id);
return true;
}
}
@Resolver(() => NotificationObjectType)
@@ -399,6 +399,10 @@ export class NotificationService {
}
}
async markAllAsRead(userId: string) {
await this.models.notification.markAllAsRead(userId);
}
/**
* Find notifications by user id, order by createdAt desc
*/
@@ -24,11 +24,12 @@ test.after.always(async () => {
test('should put comment attachment', async t => {
const workspace = await module.create(Mockers.Workspace);
const user = await module.create(Mockers.User);
const docId = randomUUID();
const key = randomUUID();
const blob = Buffer.from('test');
await storage.put(workspace.id, docId, key, 'test.txt', blob);
await storage.put(workspace.id, docId, key, 'test.txt', blob, user.id);
const item = await models.commentAttachment.get(workspace.id, docId, key);
@@ -39,15 +40,17 @@ test('should put comment attachment', async t => {
t.is(item?.mime, 'text/plain');
t.is(item?.size, blob.length);
t.is(item?.name, 'test.txt');
t.is(item?.createdBy, user.id);
});
test('should get comment attachment', async t => {
const workspace = await module.create(Mockers.Workspace);
const user = await module.create(Mockers.User);
const docId = randomUUID();
const key = randomUUID();
const blob = Buffer.from('test');
await storage.put(workspace.id, docId, key, 'test.txt', blob);
await storage.put(workspace.id, docId, key, 'test.txt', blob, user.id);
const item = await storage.get(workspace.id, docId, key);
@@ -62,11 +65,12 @@ test('should get comment attachment', async t => {
test('should get comment attachment with access url', async t => {
const workspace = await module.create(Mockers.Workspace);
const user = await module.create(Mockers.User);
const docId = randomUUID();
const key = randomUUID();
const blob = Buffer.from('test');
await storage.put(workspace.id, docId, key, 'test.txt', blob);
await storage.put(workspace.id, docId, key, 'test.txt', blob, user.id);
const url = storage.getUrl(workspace.id, docId, key);
@@ -79,11 +83,12 @@ test('should get comment attachment with access url', async t => {
test('should delete comment attachment', async t => {
const workspace = await module.create(Mockers.Workspace);
const user = await module.create(Mockers.User);
const docId = randomUUID();
const key = randomUUID();
const blob = Buffer.from('test');
await storage.put(workspace.id, docId, key, 'test.txt', blob);
await storage.put(workspace.id, docId, key, 'test.txt', blob, user.id);
await storage.delete(workspace.id, docId, key);
@@ -94,11 +99,12 @@ test('should delete comment attachment', async t => {
test('should handle comment.attachment.delete event', async t => {
const workspace = await module.create(Mockers.Workspace);
const user = await module.create(Mockers.User);
const docId = randomUUID();
const key = randomUUID();
const blob = Buffer.from('test');
await storage.put(workspace.id, docId, key, 'test.txt', blob);
await storage.put(workspace.id, docId, key, 'test.txt', blob, user.id);
await storage.onCommentAttachmentDelete({
workspaceId: workspace.id,
@@ -113,14 +119,15 @@ test('should handle comment.attachment.delete event', async t => {
test('should handle workspace.deleted event', async t => {
const workspace = await module.create(Mockers.Workspace);
const user = await module.create(Mockers.User);
const docId = randomUUID();
const key1 = randomUUID();
const key2 = randomUUID();
const blob1 = Buffer.from('test');
const blob2 = Buffer.from('test2');
await storage.put(workspace.id, docId, key1, 'test.txt', blob1);
await storage.put(workspace.id, docId, key2, 'test.txt', blob2);
await storage.put(workspace.id, docId, key1, 'test.txt', blob1, user.id);
await storage.put(workspace.id, docId, key2, 'test.txt', blob2, user.id);
const count = module.event.count('comment.attachment.delete');
@@ -59,7 +59,8 @@ export class CommentAttachmentStorage {
docId: string,
key: string,
name: string,
blob: Buffer
blob: Buffer,
userId: string
) {
const meta = autoMetadata(blob);
@@ -75,6 +76,7 @@ export class CommentAttachmentStorage {
name,
mime: meta.contentType ?? 'application/octet-stream',
size: blob.length,
createdBy: userId,
});
}
@@ -195,7 +195,7 @@ export class WorkspacesController {
await this.ac.user(user.id).doc(workspaceId, docId).assert('Doc.Read');
const { body, metadata, redirectUrl } =
await this.commentAttachmentStorage.get(workspaceId, docId, key);
await this.commentAttachmentStorage.get(workspaceId, docId, key, true);
if (redirectUrl) {
return res.redirect(redirectUrl);
@@ -0,0 +1,38 @@
import { PrismaClient } from '@prisma/client';
import { chunk } from 'lodash-es';
type SessionTime = {
sessionId: string;
_max: {
createdAt: Date;
};
};
export class CorrectSessionUpdateTime1751966744168 {
// do the migration
static async up(db: PrismaClient) {
const sessionTime = await db.aiSessionMessage.groupBy({
by: ['sessionId'],
_max: {
createdAt: true,
},
});
for (const s of chunk(sessionTime, 100)) {
const sessions = s.filter((s): s is SessionTime => !!s._max.createdAt);
await db.$transaction(async tx => {
await Promise.all(
sessions.map(s =>
tx.aiSession.update({
where: { id: s.sessionId },
data: { updatedAt: s._max.createdAt },
})
)
);
});
}
}
// revert the migration
static async down(_db: PrismaClient) {}
}
@@ -6,3 +6,4 @@ export * from './1732861452428-migrate-invite-status';
export * from './1733125339942-universal-subscription';
export * from './1738590347632-feature-redundant';
export * from './1745211351719-create-indexer-tables';
export * from './1751966744168-correct-session-update-time';
@@ -13,6 +13,7 @@ test.after.always(async () => {
test('should upsert comment attachment', async t => {
const workspace = await module.create(Mockers.Workspace);
const user = await module.create(Mockers.User);
// add
const item = await models.commentAttachment.upsert({
@@ -22,6 +23,7 @@ test('should upsert comment attachment', async t => {
name: 'test-name',
mime: 'text/plain',
size: 100,
createdBy: user.id,
});
t.is(item.workspaceId, workspace.id);
@@ -30,6 +32,7 @@ test('should upsert comment attachment', async t => {
t.is(item.mime, 'text/plain');
t.is(item.size, 100);
t.truthy(item.createdAt);
t.is(item.createdBy, user.id);
// update
const item2 = await models.commentAttachment.upsert({
@@ -46,6 +49,7 @@ test('should upsert comment attachment', async t => {
t.is(item2.key, 'test-key');
t.is(item2.mime, 'text/html');
t.is(item2.size, 200);
t.is(item2.createdBy, user.id);
// make sure only one blob is created
const items = await models.commentAttachment.list(workspace.id);
@@ -430,3 +430,32 @@ test('should create a comment mention notification', async t => {
t.is(notification.body.commentId, commentId);
t.is(notification.body.replyId, replyId);
});
test('should mark all notifications as read', async t => {
await models.notification.createMention({
userId: user.id,
body: {
workspaceId: workspace.id,
doc: {
id: docId,
title: 'doc-title',
blockId: 'blockId',
mode: DocMode.page,
},
createdByUserId: createdBy.id,
},
});
await models.notification.createInvitation({
userId: user.id,
body: {
workspaceId: workspace.id,
createdByUserId: createdBy.id,
inviteId: randomUUID(),
},
});
await models.notification.markAllAsRead(user.id);
const notifications = await models.notification.findManyByUserId(user.id);
t.is(notifications.length, 0);
});
@@ -32,6 +32,7 @@ export class CommentAttachmentModel extends BaseModel {
name: input.name,
mime: input.mime,
size: input.size,
createdBy: input.createdBy,
},
});
}
@@ -265,26 +265,31 @@ export class CopilotSessionModel extends BaseModel {
userId: true,
workspaceId: true,
docId: true,
pinned: true,
parentSessionId: true,
pinned: true,
title: true,
promptName: true,
tokenCost: true,
createdAt: true,
updatedAt: true,
messages: {
select: {
id: true,
role: true,
content: true,
streamObjects: true,
attachments: true,
streamObjects: true,
params: true,
createdAt: true,
},
orderBy: { createdAt: 'asc' },
},
promptName: true,
});
}
async list(options: ListSessionOptions) {
private getListConditions(
options: ListSessionOptions
): Prisma.AiSessionWhereInput {
const { userId, sessionId, workspaceId, docId, action, fork } = options;
function getNullCond<T>(
@@ -330,8 +335,18 @@ export class CopilotSessionModel extends BaseModel {
});
}
return { OR: conditions };
}
async count(options: ListSessionOptions) {
return await this.db.aiSession.count({
where: this.getListConditions(options),
});
}
async list(options: ListSessionOptions) {
return await this.db.aiSession.findMany({
where: { OR: conditions },
where: this.getListConditions(options),
select: {
id: true,
userId: true,
@@ -351,8 +366,8 @@ export class CopilotSessionModel extends BaseModel {
role: true,
content: true,
attachments: true,
params: true,
streamObjects: true,
params: true,
createdAt: true,
},
orderBy: {
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { Transactional } from '@nestjs-cls/transactional';
import { Prisma } from '@prisma/client';
import { Prisma, PrismaClient } from '@prisma/client';
import { PaginationInput } from '../base';
import { BaseModel } from './base';
@@ -16,6 +16,10 @@ import type {
@Injectable()
export class CopilotWorkspaceConfigModel extends BaseModel {
constructor(private readonly database: PrismaClient) {
super();
}
@Transactional()
private async listIgnoredDocIds(
workspaceId: string,
@@ -41,28 +45,25 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
* @param workspaceId id of the workspace
* @returns docIds
*/
@Transactional()
async findDocsToEmbed(workspaceId: string): Promise<string[]> {
const ignoredDocIds = (await this.listIgnoredDocIds(workspaceId)).map(
d => d.docId
);
// NOTE: for unknown reason, the transaction will timeout if call from event handler
// so we use an independent client here
const docIds = await this.database.$queryRaw<{ id: string }[]>`
SELECT s.guid as id
FROM snapshots AS s
LEFT JOIN ai_workspace_embeddings e
ON e.workspace_id = s.workspace_id
AND e.doc_id = s.guid
LEFT JOIN ai_workspace_ignored_docs id
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 NOT LIKE '%$%'
AND e.doc_id IS NULL
AND id.doc_id IS NULL;`;
const docIds = await this.db.snapshot
.findMany({
where: {
workspaceId,
AND: [
{ id: { notIn: ignoredDocIds } },
{ id: { not: workspaceId } },
{ id: { not: { contains: '$' } } },
],
embedding: { none: {} },
},
select: { id: true },
})
.then(r => r.map(doc => doc.id));
return docIds;
return docIds.map(r => r.id);
}
@Transactional()
@@ -261,6 +261,18 @@ export class NotificationModel extends BaseModel {
});
}
async markAllAsRead(userId: string) {
const { count } = await this.db.notification.updateMany({
where: { userId },
data: {
read: true,
},
});
this.logger.log(
`Marked all notifications as read for user ${userId}, count: ${count}`
);
}
/**
* Find many notifications by user id, exclude read notifications by default
*/
@@ -112,11 +112,14 @@ class ProductionEmbeddingClient extends EmbeddingClient {
);
try {
return ranks.map((score, chunk) => ({
chunk,
targetId: this.getTargetId(embeddings[chunk]),
score,
}));
return ranks.map((score, i) => {
const chunk = embeddings[i];
return {
chunk: chunk.chunk,
targetId: this.getTargetId(chunk),
score: Math.max(score, 1 - (chunk.distance || -Infinity)),
};
});
} catch (error) {
this.logger.error('Failed to parse rerank results', error);
// silent error, will fallback to default sorting in parent method
@@ -148,7 +151,7 @@ class ProductionEmbeddingClient extends EmbeddingClient {
const chunks = sortedEmbeddings.reduce(
(acc, e) => {
const targetId = 'docId' in e ? e.docId : 'fileId' in e ? e.fileId : '';
const targetId = this.getTargetId(e);
const key = `${targetId}:${e.chunk}`;
acc[key] = e;
return acc;
@@ -179,7 +182,10 @@ class ProductionEmbeddingClient extends EmbeddingClient {
.filter(Boolean);
this.logger.verbose(
`ReRank completed: ${highConfidenceChunks.length} high-confidence results found`
`ReRank completed: ${highConfidenceChunks.length} high-confidence results found, total ${sortedEmbeddings.length} embeddings`,
highConfidenceChunks.length !== sortedEmbeddings.length
? JSON.stringify(ranks)
: undefined
);
return highConfidenceChunks.slice(0, topK);
} catch (error) {
@@ -338,7 +338,7 @@ Convert a multi-speaker audio recording into a structured JSON format by transcr
{
name: 'Rerank results',
action: 'Rerank results',
model: 'gpt-4.1-mini',
model: 'gpt-4.1',
messages: [
{
role: 'system',
@@ -1286,7 +1286,7 @@ If there are items in the content that can be used as to-do tasks, please refer
{
name: 'Make it real',
action: 'Make it real',
model: 'gpt-4.1-2025-04-14',
model: 'claude-sonnet-4@20250514',
messages: [
{
role: 'system',
@@ -1327,7 +1327,7 @@ When sent new wireframes, respond ONLY with the contents of the html file.`,
{
name: 'Make it real with text',
action: 'Make it real with text',
model: 'gpt-4.1-2025-04-14',
model: 'claude-sonnet-4@20250514',
messages: [
{
role: 'system',
@@ -1675,8 +1675,9 @@ This sentence contains information from the first source[^1]. This sentence refe
<tool-calling-guidelines>
Before starting Tool calling, you need to follow:
- DO NOT explain what operation you will perform.
- DO NOT embed a tool call mid-sentence.
- When searching for unknown information or keyword, prioritize searching the user's workspace.
- When searching for unknown information, personal information or keyword, prioritize searching the user's workspace rather than the web.
- Depending on the complexity of the question and the information returned by the search tools, you can call different tools multiple times to search.
</tool-calling-guidelines>
@@ -53,8 +53,11 @@ export class PromptService implements OnApplicationBootstrap {
* @returns prompt messages
*/
async get(name: string): Promise<ChatPrompt | null> {
const cached = this.cache.get(name);
if (cached) return cached;
// skip cache in dev mode to ensure the latest prompt is always fetched
if (!env.dev) {
const cached = this.cache.get(name);
if (cached) return cached;
}
const prompt = await this.db.aiPrompt.findUnique({
where: {
@@ -62,6 +62,7 @@ export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
try {
metrics.ai.counter('chat_text_calls').add(1, { model: model.id });
const [system, msgs] = await chatToGPTMessage(messages, true, true);
const modelInstance = this.instance(model.id);
@@ -88,6 +88,12 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
system,
messages: msgs,
abortSignal: options.signal,
providerOptions: {
google: this.getGeminiOptions(options, model.id),
},
tools: await this.getTools(options, model.id),
maxSteps: this.MAX_STEPS,
experimental_continueSteps: true,
});
if (!text) throw new Error('Failed to generate text');
@@ -233,12 +239,16 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
taskType: 'RETRIEVAL_DOCUMENT',
});
const { embeddings } = await embedMany({
model: modelInstance,
values: messages,
});
const embeddings = await Promise.allSettled(
messages.map(m =>
embedMany({ model: modelInstance, values: [m], maxRetries: 3 })
)
);
return embeddings.filter(v => v && Array.isArray(v));
return embeddings
.map(e => (e.status === 'fulfilled' ? e.value.embeddings : null))
.flat()
.filter((v): v is number[] => !!v && Array.isArray(v));
} catch (e: any) {
metrics.ai
.counter('generate_embedding_errors')
@@ -254,16 +264,16 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
) {
const [system, msgs] = await chatToGPTMessage(messages);
const { fullStream } = streamText({
model: this.instance(model.id, {
useSearchGrounding: this.useSearchGrounding(options),
}),
model: this.instance(model.id),
system,
messages: msgs,
abortSignal: options.signal,
maxSteps: this.MAX_STEPS,
providerOptions: {
google: this.getGeminiOptions(options, model.id),
},
tools: await this.getTools(options, model.id),
maxSteps: this.MAX_STEPS,
experimental_continueSteps: true,
});
return fullStream;
}
@@ -282,8 +292,4 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
private isReasoningModel(model: string) {
return model.startsWith('gemini-2.5');
}
private useSearchGrounding(options: CopilotChatOptions) {
return options?.tools?.includes('webSearch');
}
}
@@ -274,9 +274,11 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
override getProviderSpecificTools(
toolName: CopilotChatTools,
model: string
): [string, Tool] | undefined {
): [string, Tool?] | undefined {
if (toolName === 'webSearch' && !this.isReasoningModel(model)) {
return ['web_search_preview', openai.tools.webSearchPreview()];
} else if (toolName === 'docEdit') {
return ['doc_edit', undefined];
}
return;
}
@@ -126,7 +126,7 @@ export abstract class CopilotProvider<C = any> {
protected getProviderSpecificTools(
_toolName: CopilotChatTools,
_model: string
): [string, Tool] | undefined {
): [string, Tool?] | undefined {
return;
}
@@ -143,7 +143,10 @@ export abstract class CopilotProvider<C = any> {
for (const tool of options.tools) {
const toolDef = this.getProviderSpecificTools(tool, model);
if (toolDef) {
tools[toolDef[0]] = toolDef[1];
// allow provider prevent tool creation
if (toolDef[1]) {
tools[toolDef[0]] = toolDef[1];
}
continue;
}
switch (tool) {
@@ -25,6 +25,9 @@ import {
CopilotFailedToCreateMessage,
CopilotSessionNotFound,
type FileUpload,
paginate,
Paginated,
PaginationInput,
RequestMutex,
Throttle,
TooManyRequest,
@@ -38,12 +41,7 @@ import { PromptService } from './prompt';
import { PromptMessage, StreamObject } from './providers';
import { ChatSessionService } from './session';
import { CopilotStorage } from './storage';
import {
type ChatHistory,
type ChatMessage,
type ChatSessionState,
SubmittedMessage,
} from './types';
import { type ChatHistory, type ChatMessage, SubmittedMessage } from './types';
export const COPILOT_LOCKER = 'copilot';
@@ -186,6 +184,9 @@ class QueryChatHistoriesInput
@Field(() => String, { nullable: true })
sessionId: string | undefined;
@Field(() => Boolean, { nullable: true })
withMessages: boolean | undefined;
@Field(() => Boolean, { nullable: true })
withPrompt: boolean | undefined;
}
@@ -239,7 +240,7 @@ class ChatMessageType implements Partial<ChatMessage> {
}
@ObjectType('CopilotHistories')
class CopilotHistoriesType implements Partial<ChatHistory> {
class CopilotHistoriesType implements Omit<ChatHistory, 'userId'> {
@Field(() => String)
sessionId!: string;
@@ -249,8 +250,17 @@ class CopilotHistoriesType implements Partial<ChatHistory> {
@Field(() => String, { nullable: true })
docId!: string | null;
@Field(() => Boolean)
pinned!: boolean;
@Field(() => String, { nullable: true })
parentSessionId!: string | null;
@Field(() => String)
promptName!: string;
@Field(() => String)
model!: string;
@Field(() => [String])
optionalModels!: string[];
@Field(() => String, {
description: 'An mark identifying which view to use to display the session',
@@ -258,6 +268,12 @@ class CopilotHistoriesType implements Partial<ChatHistory> {
})
action!: string | null;
@Field(() => Boolean)
pinned!: boolean;
@Field(() => String, { nullable: true })
title!: string | null;
@Field(() => Number, {
description: 'The number of tokens used in the session',
})
@@ -273,6 +289,11 @@ class CopilotHistoriesType implements Partial<ChatHistory> {
updatedAt!: Date;
}
@ObjectType()
export class PaginatedCopilotHistoriesType extends Paginated(
CopilotHistoriesType
) {}
@ObjectType('CopilotQuota')
class CopilotQuotaType {
@Field(() => SafeIntResolver, { nullable: true })
@@ -421,7 +442,7 @@ export class CopilotResolver {
@Args('sessionId') sessionId: string
): Promise<CopilotSessionType> {
await this.assertPermission(user, copilot);
const session = await this.chatSession.getSession(sessionId);
const session = await this.chatSession.getSessionInfo(sessionId);
if (!session) {
throw new NotFoundException('Session not found');
}
@@ -430,6 +451,7 @@ export class CopilotResolver {
@ResolveField(() => [CopilotSessionType], {
description: 'Get the session list in the workspace',
deprecationReason: 'use `chats` instead',
complexity: 2,
})
async sessions(
@@ -447,11 +469,12 @@ export class CopilotResolver {
Object.assign({}, copilot, { docId: maybeDocId })
);
const sessions = await this.chatSession.listSessions(
Object.assign({}, options, appendOptions)
const sessions = await this.chatSession.list(
Object.assign({}, options, appendOptions),
false
);
if (appendOptions.docId) {
type Session = Omit<ChatSessionState, 'messages'> & { docId: string };
type Session = ChatHistory & { docId: string };
const filtered = sessions.filter((s): s is Session => !!s.docId);
const accessible = await this.ac
.user(user.id)
@@ -463,7 +486,9 @@ export class CopilotResolver {
}
}
@ResolveField(() => [CopilotHistoriesType], {})
@ResolveField(() => [CopilotHistoriesType], {
deprecationReason: 'use `chats` instead',
})
@CallMetric('ai', 'histories')
async histories(
@Parent() copilot: CopilotType,
@@ -478,8 +503,9 @@ export class CopilotResolver {
await this.assertPermission(user, { workspaceId, docId });
}
const histories = await this.chatSession.listHistories(
Object.assign({}, options, { userId: user.id, workspaceId, docId })
const histories = await this.chatSession.list(
Object.assign({}, options, { userId: user.id, workspaceId, docId }),
true
);
return histories.map(h => ({
@@ -491,6 +517,48 @@ export class CopilotResolver {
}));
}
@ResolveField(() => PaginatedCopilotHistoriesType, {})
@CallMetric('ai', 'histories')
async chats(
@Parent() copilot: CopilotType,
@CurrentUser() user: CurrentUser,
@Args('pagination', PaginationInput.decode) pagination: PaginationInput,
@Args('docId', { nullable: true }) docId?: string,
@Args('options', { nullable: true }) options?: QueryChatHistoriesInput
): Promise<PaginatedCopilotHistoriesType> {
const workspaceId = copilot.workspaceId;
if (!workspaceId) {
return paginate([], 'updatedAt', pagination, 0);
} else {
await this.assertPermission(user, { workspaceId, docId });
}
const finalOptions = Object.assign(
{},
options,
{ userId: user.id, workspaceId, docId },
{ skip: pagination.offset, limit: pagination.first }
);
const totalCount = await this.chatSession.count(finalOptions);
const histories = await this.chatSession.list(
finalOptions,
!!options?.withMessages
);
return paginate(
histories.map(h => ({
...h,
// filter out empty messages
messages: h.messages?.filter(
m => m.content || m.attachments?.length
) as ChatMessageType[],
})),
'updatedAt',
pagination,
totalCount
);
}
@Mutation(() => String, {
description: 'Create a chat session',
})
@@ -657,18 +725,9 @@ export class CopilotResolver {
}
private transformToSessionType(
session: Omit<ChatSessionState, 'messages'>
session: Omit<ChatHistory, 'messages'>
): CopilotSessionType {
return {
id: session.sessionId,
parentSessionId: session.parentSessionId,
docId: session.docId,
pinned: session.pinned,
title: session.title,
promptName: session.prompt.name,
model: session.prompt.model,
optionalModels: session.prompt.optionalModels,
};
return { id: session.sessionId, ...session };
}
}
@@ -4,6 +4,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { Transactional } from '@nestjs-cls/transactional';
import { AiPromptRole } from '@prisma/client';
import { pick } from 'lodash-es';
import {
CopilotActionTaken,
@@ -25,7 +26,7 @@ import {
UpdateChatSessionOptions,
} from '../../models';
import { ChatMessageCache } from './message';
import { PromptService } from './prompt';
import { ChatPrompt, PromptService } from './prompt';
import {
CopilotProviderFactory,
ModelOutputType,
@@ -240,6 +241,14 @@ export class ChatSession implements AsyncDisposable {
}
}
type Session = NonNullable<
Awaited<ReturnType<Models['copilotSession']['get']>>
>;
type SessionHistory = ChatHistory & {
prompt: ChatPrompt;
};
@Injectable()
export class ChatSessionService {
private readonly logger = new Logger(ChatSessionService.name);
@@ -253,27 +262,55 @@ export class ChatSessionService {
private readonly prompt: PromptService
) {}
async getSession(sessionId: string): Promise<ChatSessionState | undefined> {
const session = await this.models.copilotSession.get(sessionId);
if (!session) return;
private getMessage(session: Session): ChatMessage[] {
if (!Array.isArray(session.messages) || !session.messages.length) {
return [];
}
const messages = ChatMessageSchema.array().safeParse(session.messages);
if (!messages.success) {
this.logger.error(
`Unexpected message schema: ${JSON.stringify(messages.error)}`
);
return [];
}
return messages.data;
}
private async getHistory(session: Session): Promise<SessionHistory> {
const prompt = await this.prompt.get(session.promptName);
if (!prompt) throw new CopilotPromptNotFound({ name: session.promptName });
const messages = ChatMessageSchema.array().safeParse(session.messages);
return {
...pick(session, [
'userId',
'workspaceId',
'docId',
'parentSessionId',
'pinned',
'title',
'createdAt',
'updatedAt',
]),
sessionId: session.id,
userId: session.userId,
workspaceId: session.workspaceId,
docId: session.docId,
pinned: session.pinned,
title: session.title,
parentSessionId: session.parentSessionId,
tokens: session.tokenCost,
messages: this.getMessage(session),
// prompt info
prompt,
messages: messages.success ? messages.data : [],
action: prompt.action || null,
model: prompt.model,
optionalModels: prompt.optionalModels || null,
promptName: prompt.name,
};
}
async getSessionInfo(sessionId: string): Promise<SessionHistory | undefined> {
const session = await this.models.copilotSession.get(sessionId);
if (!session) return;
return await this.getHistory(session);
}
// revert the latest messages not generate by user
// after revert, we can retry the action
async revertLatestMessage(
@@ -286,116 +323,70 @@ export class ChatSessionService {
);
}
async listSessions(
options: ListSessionOptions
): Promise<Omit<ChatSessionState, 'messages'>[]> {
const sessions = await this.models.copilotSession.list({
...options,
withMessages: false,
});
return Promise.all(
sessions.map(async session => {
const prompt = await this.prompt.get(session.promptName);
if (!prompt)
throw new CopilotPromptNotFound({ name: session.promptName });
return {
sessionId: session.id,
userId: session.userId,
workspaceId: session.workspaceId,
docId: session.docId,
pinned: session.pinned,
title: session.title,
parentSessionId: session.parentSessionId,
prompt,
};
})
);
async count(options: ListSessionOptions): Promise<number> {
return await this.models.copilotSession.count(options);
}
async listHistories(options: ListSessionOptions): Promise<ChatHistory[]> {
const { userId } = options;
async list(
options: ListSessionOptions,
withMessages: boolean
): Promise<ChatHistory[]> {
const { userId: reqUserId } = options;
const sessions = await this.models.copilotSession.list({
...options,
withMessages: true,
withMessages,
});
const histories = await Promise.all(
sessions.map(
async ({
userId: uid,
id,
workspaceId,
docId,
pinned,
title,
promptName,
tokenCost,
messages,
createdAt,
updatedAt,
}) => {
try {
const prompt = await this.prompt.get(promptName);
if (!prompt) {
throw new CopilotPromptNotFound({ name: promptName });
}
sessions.map(async session => {
const { userId, id: sessionId, createdAt } = session;
try {
const { prompt, messages, ...baseHistory } =
await this.getHistory(session);
if (withMessages) {
if (
// filter out the user's session that not match the action option
(uid === userId && !!options?.action !== !!prompt.action) ||
(userId === reqUserId && !!options?.action !== !!prompt.action) ||
// filter out the non chat session from other user
(uid !== userId && !!prompt.action)
(userId !== reqUserId && !!prompt.action)
) {
return undefined;
}
const ret = ChatMessageSchema.array().safeParse(messages);
if (ret.success) {
// render system prompt
const preload = (
options?.withPrompt
? prompt
.finish(ret.data[0]?.params || {}, id)
.filter(({ role }) => role !== 'system')
: []
) as ChatMessage[];
// render system prompt
const preload = (
options?.withPrompt
? prompt
.finish(messages[0]?.params || {}, sessionId)
.filter(({ role }) => role !== 'system')
: []
) as ChatMessage[];
// `createdAt` is required for history sorting in frontend
// let's fake the creating time of prompt messages
preload.forEach((msg, i) => {
msg.createdAt = new Date(
createdAt.getTime() - preload.length - i - 1
);
});
return {
sessionId: id,
workspaceId,
docId,
pinned,
title,
action: prompt.action || null,
tokens: tokenCost,
createdAt,
updatedAt,
messages: preload.concat(ret.data).map(m => ({
...m,
attachments: m.attachments
?.map(a => (typeof a === 'string' ? a : a.attachment))
.filter(a => !!a),
})),
};
} else {
this.logger.error(
`Unexpected message schema: ${JSON.stringify(ret.error)}`
// `createdAt` is required for history sorting in frontend
// let's fake the creating time of prompt messages
preload.forEach((msg, i) => {
msg.createdAt = new Date(
createdAt.getTime() - preload.length - i - 1
);
}
} catch (e) {
this.logger.error('Unexpected error in listHistories', e);
});
return {
...baseHistory,
messages: preload.concat(messages).map(m => ({
...m,
attachments: m.attachments
?.map(a => (typeof a === 'string' ? a : a.attachment))
.filter(a => !!a),
})),
};
} else {
return { ...baseHistory, messages: [] };
}
return undefined;
} catch (e) {
this.logger.error('Unexpected error in list ChatHistories', e);
}
)
return undefined;
})
);
return histories.filter((v): v is NonNullable<typeof v> => !!v);
@@ -461,7 +452,7 @@ export class ChatSessionService {
@Transactional()
async update(options: UpdateChatSession): Promise<string> {
const session = await this.getSession(options.sessionId);
const session = await this.getSessionInfo(options.sessionId);
if (!session) {
throw new CopilotSessionNotFound();
}
@@ -494,14 +485,14 @@ export class ChatSessionService {
@Transactional()
async fork(options: ChatSessionForkOptions): Promise<string> {
const state = await this.getSession(options.sessionId);
if (!state) {
const session = await this.getSessionInfo(options.sessionId);
if (!session) {
throw new CopilotSessionNotFound();
}
let messages = state.messages.map(m => ({ ...m, id: undefined }));
let messages = session.messages.map(m => ({ ...m, id: undefined }));
if (options.latestMessageId) {
const lastMessageIdx = state.messages.findLastIndex(
const lastMessageIdx = session.messages.findLastIndex(
({ id, role }) =>
role === AiPromptRole.assistant && id === options.latestMessageId
);
@@ -514,7 +505,7 @@ export class ChatSessionService {
}
return await this.models.copilotSession.fork({
...state,
...session,
userId: options.userId,
sessionId: randomUUID(),
parentSessionId: options.sessionId,
@@ -544,7 +535,7 @@ export class ChatSessionService {
* @returns
*/
async get(sessionId: string): Promise<ChatSession | null> {
const state = await this.getSession(sessionId);
const state = await this.getSessionInfo(sessionId);
if (state) {
return new ChatSession(this.messageCache, state, async state => {
await this.models.copilotSession.updateMessages(state);
@@ -167,7 +167,7 @@ You should specify the following arguments before the others: [doc_id], [origin_
},
]);
return { result };
return { result, content };
} catch {
return 'Failed to apply edit to the doc';
}
@@ -58,7 +58,7 @@ export const createDocSemanticSearchTool = (
) => {
return tool({
description:
'Retrieve conceptually related passages by performing vector-based semantic similarity search across embedded documents; use this tool only when exact keyword search fails or the user explicitly needs meaning-level matches (e.g., paraphrases, synonyms, broader concepts).',
'Retrieve conceptually related passages by performing vector-based semantic similarity search across embedded documents; use this tool only when exact keyword search fails or the user explicitly needs meaning-level matches (e.g., paraphrases, synonyms, broader concepts, recent documents).',
parameters: z.object({
query: z
.string()
@@ -46,12 +46,19 @@ export type ChatMessage = z.infer<typeof ChatMessageSchema>;
export const ChatHistorySchema = z
.object({
userId: z.string(),
sessionId: z.string(),
workspaceId: z.string(),
docId: z.string().nullable(),
parentSessionId: z.string().nullable(),
pinned: z.boolean(),
title: z.string().nullable(),
action: z.string().nullable(),
model: z.string(),
optionalModels: z.array(z.string()),
promptName: z.string(),
tokens: z.number(),
messages: z.array(ChatMessageSchema),
createdAt: z.date(),
@@ -69,32 +76,26 @@ export type SubmittedMessage = z.infer<typeof SubmittedMessageSchema>;
// ======== Chat Session ========
export interface ChatSessionOptions {
// connect ids
userId: string;
workspaceId: string;
docId: string | null;
promptName: string;
pinned: boolean;
export type ChatSessionOptions = Pick<
ChatHistory,
'userId' | 'workspaceId' | 'docId' | 'promptName' | 'pinned'
> & {
reuseLatestChat?: boolean;
}
};
export interface ChatSessionForkOptions
extends Omit<ChatSessionOptions, 'pinned' | 'promptName'> {
sessionId: string;
export type ChatSessionForkOptions = Pick<
ChatHistory,
'userId' | 'sessionId' | 'workspaceId' | 'docId'
> & {
latestMessageId?: string;
}
};
export interface ChatSessionState
extends Omit<ChatSessionOptions, 'promptName'> {
title: string | null;
// connect ids
sessionId: string;
parentSessionId: string | null;
// states
export type ChatSessionState = Pick<
ChatHistory,
'userId' | 'sessionId' | 'workspaceId' | 'docId' | 'messages'
> & {
prompt: ChatPrompt;
messages: ChatMessage[];
}
};
export type CopilotContextFile = {
id: string; // fileId
@@ -21,16 +21,19 @@ type SignalReturnType = {
export function getSignal(req: Request): SignalReturnType {
const controller = new AbortController();
let isAborted = true;
let hasEnded = false;
let callback: ((isAborted: boolean) => void) | undefined = undefined;
const onSocketEnd = () => {
isAborted = false;
hasEnded = true;
};
const onSocketClose = (hadError: boolean) => {
req.socket.off('end', onSocketEnd);
req.socket.off('close', onSocketClose);
const aborted = hadError || isAborted;
// NOTE: the connection is considered abnormally interrupted:
// 1. there is an error when the socket is closed.
// 2. the connection is closed directly without going through the normal end process (the client disconnects actively).
const aborted = hadError || !hasEnded;
if (aborted) {
controller.abort();
}
+25 -3
View File
@@ -208,10 +208,11 @@ type ContextWorkspaceEmbeddingStatus {
type Copilot {
audioTranscription(blobId: String, jobId: String): TranscriptionResultType
chats(docId: String, options: QueryChatHistoriesInput, pagination: PaginationInput!): PaginatedCopilotHistoriesType!
"""Get the context list of a session"""
contexts(contextId: String, sessionId: String): [CopilotContext!]!
histories(docId: String, options: QueryChatHistoriesInput): [CopilotHistories!]!
histories(docId: String, options: QueryChatHistoriesInput): [CopilotHistories!]! @deprecated(reason: "use `chats` instead")
"""Get the quota of the user in the workspace"""
quota: CopilotQuota!
@@ -220,7 +221,7 @@ type Copilot {
session(sessionId: String!): CopilotSessionType!
"""Get the session list in the workspace"""
sessions(docId: String, options: QueryChatSessionsInput): [CopilotSessionType!]!
sessions(docId: String, options: QueryChatSessionsInput): [CopilotSessionType!]! @deprecated(reason: "use `chats` instead")
workspaceId: ID
}
@@ -313,8 +314,13 @@ type CopilotHistories {
createdAt: DateTime!
docId: String
messages: [ChatMessage!]!
model: String!
optionalModels: [String!]!
parentSessionId: String
pinned: Boolean!
promptName: String!
sessionId: String!
title: String
"""The number of tokens used in the session"""
tokens: Int!
@@ -322,6 +328,11 @@ type CopilotHistories {
workspaceId: String!
}
type CopilotHistoriesTypeEdge {
cursor: String!
node: CopilotHistories!
}
type CopilotInvalidContextDataType {
contextId: String!
}
@@ -1236,6 +1247,9 @@ type Mutation {
"""queue workspace doc embedding"""
queueWorkspaceEmbedding(docId: [String!]!, workspaceId: String!): Boolean!
"""mark all notifications as read"""
readAllNotifications: Boolean!
"""mark notification as read"""
readNotification(id: String!): Boolean!
recoverDoc(guid: String!, timestamp: DateTime!, workspaceId: String!): DateTime!
@@ -1418,6 +1432,12 @@ type PaginatedCommentObjectType {
totalCount: Int!
}
type PaginatedCopilotHistoriesType {
edges: [CopilotHistoriesTypeEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PaginatedCopilotWorkspaceFileType {
edges: [CopilotWorkspaceFileTypeEdge!]!
pageInfo: PageInfo!
@@ -1549,6 +1569,7 @@ input QueryChatHistoriesInput {
sessionId: String
sessionOrder: ChatHistoryOrder
skip: Int
withMessages: Boolean
withPrompt: Boolean
}
@@ -1739,7 +1760,7 @@ enum SearchTable {
type ServerConfigType {
"""Whether allow guest users to create demo workspaces."""
allowGuestDemoWorkspace: Boolean!
allowGuestDemoWorkspace: Boolean! @deprecated(reason: "This field is deprecated, please use `features` instead. Will be removed in 0.25.0")
"""fetch latest available upgradable release of server"""
availableUpgrade: ReleaseVersionType
@@ -1781,6 +1802,7 @@ enum ServerFeature {
Copilot
CopilotEmbedding
Indexer
LocalWorkspace
OAuth
Payment
}
@@ -7,7 +7,6 @@ query adminServerConfig {
baseUrl
name
features
allowGuestDemoWorkspace
type
initialized
credentialsRequirement {
@@ -1,17 +1,29 @@
query getCopilotHistoryIds(
$workspaceId: String!
$pagination: PaginationInput!
$docId: String
$options: QueryChatHistoriesInput
) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(docId: $docId, options: $options) {
sessionId
pinned
messages {
id
role
createdAt
chats(pagination: $pagination, docId: $docId, options: $options) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
sessionId
pinned
messages {
id
role
createdAt
}
}
}
}
}
@@ -1,31 +1,15 @@
#import "./fragments/copilot.gql"
query getCopilotDocSessions(
$workspaceId: String!
$docId: String!
$pagination: PaginationInput!
$options: QueryChatHistoriesInput
) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(docId: $docId, options: $options) {
sessionId
pinned
tokens
action
createdAt
messages {
id
role
content
streamObjects {
type
textDelta
toolCallId
toolName
args
result
}
attachments
createdAt
}
chats(pagination: $pagination, docId: $docId, options: $options) {
...PaginatedCopilotChats
}
}
}
@@ -1,3 +1,5 @@
#import "./fragments/copilot.gql"
query getCopilotPinnedSessions(
$workspaceId: String!
$docId: String
@@ -6,32 +8,12 @@ query getCopilotPinnedSessions(
) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(docId: $docId, options: {
limit: 1,
chats(pagination: { first: 1 }, docId: $docId, options: {
pinned: true,
messageOrder: $messageOrder,
withPrompt: $withPrompt
}) {
sessionId
pinned
tokens
action
createdAt
messages {
id
role
content
streamObjects {
type
textDelta
toolCallId
toolName
args
result
}
attachments
createdAt
}
...PaginatedCopilotChats
}
}
}
@@ -1,30 +1,14 @@
#import "./fragments/copilot.gql"
query getCopilotWorkspaceSessions(
$workspaceId: String!
$pagination: PaginationInput!
$options: QueryChatHistoriesInput
) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(docId: null, options: $options) {
sessionId
pinned
tokens
action
createdAt
messages {
id
role
content
streamObjects {
type
textDelta
toolCallId
toolName
args
result
}
attachments
createdAt
}
chats(pagination: $pagination, docId: null, options: $options) {
...PaginatedCopilotChats
}
}
}
@@ -1,31 +1,15 @@
#import "./fragments/copilot.gql"
query getCopilotHistories(
$workspaceId: String!
$pagination: PaginationInput!
$docId: String
$options: QueryChatHistoriesInput
) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(docId: $docId, options: $options) {
sessionId
pinned
tokens
action
createdAt
messages {
id
role
content
streamObjects {
type
textDelta
toolCallId
toolName
args
result
}
attachments
createdAt
}
chats(pagination: $pagination, docId: $docId, options: $options) {
...PaginatedCopilotChats
}
}
}
@@ -1,34 +1,22 @@
#import "./fragments/copilot.gql"
query getCopilotLatestDocSession(
$workspaceId: String!
$docId: String!
) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(
chats(
pagination: { first: 1 }
docId: $docId
options: {
limit: 1
sessionOrder: desc
action: false
fork: false
withMessages: true
}
) {
sessionId
workspaceId
docId
pinned
action
tokens
createdAt
updatedAt
messages {
id
role
content
attachments
params
createdAt
}
...PaginatedCopilotChats
}
}
}
@@ -1,18 +1,16 @@
#import "./fragments/copilot.gql"
query getCopilotSession(
$workspaceId: String!
$sessionId: String!
) {
currentUser {
copilot(workspaceId: $workspaceId) {
session(sessionId: $sessionId) {
id
parentSessionId
docId
pinned
title
promptName
model
optionalModels
chats(
pagination: { first: 1 }
options: { sessionId: $sessionId }
) {
...PaginatedCopilotChats
}
}
}
@@ -1,23 +1,20 @@
#import "./fragments/copilot.gql"
query getCopilotRecentSessions(
$workspaceId: String!
$limit: Int = 10
) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(
chats(
pagination: { first: $limit }
options: {
limit: $limit
fork: false
sessionOrder: desc
withMessages: true
}
) {
sessionId
workspaceId
docId
pinned
action
tokens
createdAt
updatedAt
...PaginatedCopilotChats
}
}
}
@@ -1,19 +1,15 @@
#import "./fragments/copilot.gql"
query getCopilotSessions(
$workspaceId: String!
$pagination: PaginationInput!
$docId: String
$options: QueryChatSessionsInput
$options: QueryChatHistoriesInput
) {
currentUser {
copilot(workspaceId: $workspaceId) {
sessions(docId: $docId, options: $options) {
id
parentSessionId
docId
pinned
title
promptName
model
optionalModels
chats(pagination: $pagination, docId: $docId, options: $options) {
...PaginatedCopilotChats
}
}
}
@@ -0,0 +1,49 @@
fragment CopilotChatMessage on ChatMessage {
id
role
content
attachments
streamObjects {
type
textDelta
toolCallId
toolName
args
result
}
createdAt
}
fragment CopilotChatHistory on CopilotHistories {
sessionId
workspaceId
docId
parentSessionId
promptName
model
optionalModels
action
pinned
title
tokens
messages {
...CopilotChatMessage
}
createdAt
updatedAt
}
fragment PaginatedCopilotChats on PaginatedCopilotHistoriesType {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
...CopilotChatHistory
}
}
}
+133 -152
View File
@@ -6,6 +6,53 @@ export interface GraphQLQuery {
file?: boolean;
deprecations?: string[];
}
export const copilotChatMessageFragment = `fragment CopilotChatMessage on ChatMessage {
id
role
content
attachments
streamObjects {
type
textDelta
toolCallId
toolName
args
result
}
createdAt
}`;
export const copilotChatHistoryFragment = `fragment CopilotChatHistory on CopilotHistories {
sessionId
workspaceId
docId
parentSessionId
promptName
model
optionalModels
action
pinned
title
tokens
messages {
...CopilotChatMessage
}
createdAt
updatedAt
}`;
export const paginatedCopilotChatsFragment = `fragment PaginatedCopilotChats on PaginatedCopilotHistoriesType {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
...CopilotChatHistory
}
}
}`;
export const credentialsRequirementsFragment = `fragment CredentialsRequirements on CredentialsRequirementType {
password {
...PasswordLimits
@@ -32,7 +79,6 @@ export const adminServerConfigQuery = {
baseUrl
name
features
allowGuestDemoWorkspace
type
initialized
credentialsRequirement {
@@ -762,16 +808,27 @@ export const queueWorkspaceEmbeddingMutation = {
export const getCopilotHistoryIdsQuery = {
id: 'getCopilotHistoryIdsQuery' as const,
op: 'getCopilotHistoryIds',
query: `query getCopilotHistoryIds($workspaceId: String!, $docId: String, $options: QueryChatHistoriesInput) {
query: `query getCopilotHistoryIds($workspaceId: String!, $pagination: PaginationInput!, $docId: String, $options: QueryChatHistoriesInput) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(docId: $docId, options: $options) {
sessionId
pinned
messages {
id
role
createdAt
chats(pagination: $pagination, docId: $docId, options: $options) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
sessionId
pinned
messages {
id
role
createdAt
}
}
}
}
}
@@ -782,34 +839,18 @@ export const getCopilotHistoryIdsQuery = {
export const getCopilotDocSessionsQuery = {
id: 'getCopilotDocSessionsQuery' as const,
op: 'getCopilotDocSessions',
query: `query getCopilotDocSessions($workspaceId: String!, $docId: String!, $options: QueryChatHistoriesInput) {
query: `query getCopilotDocSessions($workspaceId: String!, $docId: String!, $pagination: PaginationInput!, $options: QueryChatHistoriesInput) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(docId: $docId, options: $options) {
sessionId
pinned
tokens
action
createdAt
messages {
id
role
content
streamObjects {
type
textDelta
toolCallId
toolName
args
result
}
attachments
createdAt
}
chats(pagination: $pagination, docId: $docId, options: $options) {
...PaginatedCopilotChats
}
}
}
}`,
}
${copilotChatMessageFragment}
${copilotChatHistoryFragment}
${paginatedCopilotChatsFragment}`,
};
export const getCopilotPinnedSessionsQuery = {
@@ -818,100 +859,53 @@ export const getCopilotPinnedSessionsQuery = {
query: `query getCopilotPinnedSessions($workspaceId: String!, $docId: String, $messageOrder: ChatHistoryOrder, $withPrompt: Boolean) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(
chats(
pagination: {first: 1}
docId: $docId
options: {limit: 1, pinned: true, messageOrder: $messageOrder, withPrompt: $withPrompt}
options: {pinned: true, messageOrder: $messageOrder, withPrompt: $withPrompt}
) {
sessionId
pinned
tokens
action
createdAt
messages {
id
role
content
streamObjects {
type
textDelta
toolCallId
toolName
args
result
}
attachments
createdAt
}
...PaginatedCopilotChats
}
}
}
}`,
}
${copilotChatMessageFragment}
${copilotChatHistoryFragment}
${paginatedCopilotChatsFragment}`,
};
export const getCopilotWorkspaceSessionsQuery = {
id: 'getCopilotWorkspaceSessionsQuery' as const,
op: 'getCopilotWorkspaceSessions',
query: `query getCopilotWorkspaceSessions($workspaceId: String!, $options: QueryChatHistoriesInput) {
query: `query getCopilotWorkspaceSessions($workspaceId: String!, $pagination: PaginationInput!, $options: QueryChatHistoriesInput) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(docId: null, options: $options) {
sessionId
pinned
tokens
action
createdAt
messages {
id
role
content
streamObjects {
type
textDelta
toolCallId
toolName
args
result
}
attachments
createdAt
}
chats(pagination: $pagination, docId: null, options: $options) {
...PaginatedCopilotChats
}
}
}
}`,
}
${copilotChatMessageFragment}
${copilotChatHistoryFragment}
${paginatedCopilotChatsFragment}`,
};
export const getCopilotHistoriesQuery = {
id: 'getCopilotHistoriesQuery' as const,
op: 'getCopilotHistories',
query: `query getCopilotHistories($workspaceId: String!, $docId: String, $options: QueryChatHistoriesInput) {
query: `query getCopilotHistories($workspaceId: String!, $pagination: PaginationInput!, $docId: String, $options: QueryChatHistoriesInput) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(docId: $docId, options: $options) {
sessionId
pinned
tokens
action
createdAt
messages {
id
role
content
streamObjects {
type
textDelta
toolCallId
toolName
args
result
}
attachments
createdAt
}
chats(pagination: $pagination, docId: $docId, options: $options) {
...PaginatedCopilotChats
}
}
}
}`,
}
${copilotChatMessageFragment}
${copilotChatHistoryFragment}
${paginatedCopilotChatsFragment}`,
};
export const submitAudioTranscriptionMutation = {
@@ -1039,30 +1033,19 @@ export const getCopilotLatestDocSessionQuery = {
query: `query getCopilotLatestDocSession($workspaceId: String!, $docId: String!) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(
chats(
pagination: {first: 1}
docId: $docId
options: {limit: 1, sessionOrder: desc, action: false, fork: false}
options: {sessionOrder: desc, action: false, fork: false, withMessages: true}
) {
sessionId
workspaceId
docId
pinned
action
tokens
createdAt
updatedAt
messages {
id
role
content
attachments
params
createdAt
}
...PaginatedCopilotChats
}
}
}
}`,
}
${copilotChatMessageFragment}
${copilotChatHistoryFragment}
${paginatedCopilotChatsFragment}`,
};
export const getCopilotSessionQuery = {
@@ -1071,19 +1054,15 @@ export const getCopilotSessionQuery = {
query: `query getCopilotSession($workspaceId: String!, $sessionId: String!) {
currentUser {
copilot(workspaceId: $workspaceId) {
session(sessionId: $sessionId) {
id
parentSessionId
docId
pinned
title
promptName
model
optionalModels
chats(pagination: {first: 1}, options: {sessionId: $sessionId}) {
...PaginatedCopilotChats
}
}
}
}`,
}
${copilotChatMessageFragment}
${copilotChatHistoryFragment}
${paginatedCopilotChatsFragment}`,
};
export const getCopilotRecentSessionsQuery = {
@@ -1092,19 +1071,18 @@ export const getCopilotRecentSessionsQuery = {
query: `query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10) {
currentUser {
copilot(workspaceId: $workspaceId) {
histories(options: {limit: $limit, sessionOrder: desc}) {
sessionId
workspaceId
docId
pinned
action
tokens
createdAt
updatedAt
chats(
pagination: {first: $limit}
options: {fork: false, sessionOrder: desc, withMessages: true}
) {
...PaginatedCopilotChats
}
}
}
}`,
}
${copilotChatMessageFragment}
${copilotChatHistoryFragment}
${paginatedCopilotChatsFragment}`,
};
export const updateCopilotSessionMutation = {
@@ -1118,22 +1096,18 @@ export const updateCopilotSessionMutation = {
export const getCopilotSessionsQuery = {
id: 'getCopilotSessionsQuery' as const,
op: 'getCopilotSessions',
query: `query getCopilotSessions($workspaceId: String!, $docId: String, $options: QueryChatSessionsInput) {
query: `query getCopilotSessions($workspaceId: String!, $pagination: PaginationInput!, $docId: String, $options: QueryChatHistoriesInput) {
currentUser {
copilot(workspaceId: $workspaceId) {
sessions(docId: $docId, options: $options) {
id
parentSessionId
docId
pinned
title
promptName
model
optionalModels
chats(pagination: $pagination, docId: $docId, options: $options) {
...PaginatedCopilotChats
}
}
}
}`,
}
${copilotChatMessageFragment}
${copilotChatHistoryFragment}
${paginatedCopilotChatsFragment}`,
};
export const addWorkspaceEmbeddingFilesMutation = {
@@ -1989,6 +1963,14 @@ export const quotaQuery = {
deprecations: ["'storageQuota' is deprecated: use `UserQuotaType['usedStorageQuota']` instead"],
};
export const readAllNotificationsMutation = {
id: 'readAllNotificationsMutation' as const,
op: 'readAllNotifications',
query: `mutation readAllNotifications {
readAllNotifications
}`,
};
export const readNotificationMutation = {
id: 'readNotificationMutation' as const,
op: 'readNotification',
@@ -2107,7 +2089,6 @@ export const serverConfigQuery = {
baseUrl
name
features
allowGuestDemoWorkspace
type
initialized
credentialsRequirement {
@@ -0,0 +1,3 @@
mutation readAllNotifications {
readAllNotifications
}
@@ -7,7 +7,6 @@ query serverConfig {
baseUrl
name
features
allowGuestDemoWorkspace
type
initialized
credentialsRequirement {
+552 -162
View File
@@ -245,14 +245,19 @@ export interface ContextWorkspaceEmbeddingStatus {
export interface Copilot {
__typename?: 'Copilot';
audioTranscription: Maybe<TranscriptionResultType>;
chats: PaginatedCopilotHistoriesType;
/** Get the context list of a session */
contexts: Array<CopilotContext>;
/** @deprecated use `chats` instead */
histories: Array<CopilotHistories>;
/** Get the quota of the user in the workspace */
quota: CopilotQuota;
/** Get the session by id */
session: CopilotSessionType;
/** Get the session list in the workspace */
/**
* Get the session list in the workspace
* @deprecated use `chats` instead
*/
sessions: Array<CopilotSessionType>;
workspaceId: Maybe<Scalars['ID']['output']>;
}
@@ -262,6 +267,12 @@ export interface CopilotAudioTranscriptionArgs {
jobId?: InputMaybe<Scalars['String']['input']>;
}
export interface CopilotChatsArgs {
docId?: InputMaybe<Scalars['String']['input']>;
options?: InputMaybe<QueryChatHistoriesInput>;
pagination: PaginationInput;
}
export interface CopilotContextsArgs {
contextId?: InputMaybe<Scalars['String']['input']>;
sessionId?: InputMaybe<Scalars['String']['input']>;
@@ -391,14 +402,25 @@ export interface CopilotHistories {
createdAt: Scalars['DateTime']['output'];
docId: Maybe<Scalars['String']['output']>;
messages: Array<ChatMessage>;
model: Scalars['String']['output'];
optionalModels: Array<Scalars['String']['output']>;
parentSessionId: Maybe<Scalars['String']['output']>;
pinned: Scalars['Boolean']['output'];
promptName: Scalars['String']['output'];
sessionId: Scalars['String']['output'];
title: Maybe<Scalars['String']['output']>;
/** The number of tokens used in the session */
tokens: Scalars['Int']['output'];
updatedAt: Scalars['DateTime']['output'];
workspaceId: Scalars['String']['output'];
}
export interface CopilotHistoriesTypeEdge {
__typename?: 'CopilotHistoriesTypeEdge';
cursor: Scalars['String']['output'];
node: CopilotHistories;
}
export interface CopilotInvalidContextDataType {
__typename?: 'CopilotInvalidContextDataType';
contextId: Scalars['String']['output'];
@@ -1371,6 +1393,8 @@ export interface Mutation {
publishPage: DocType;
/** queue workspace doc embedding */
queueWorkspaceEmbedding: Scalars['Boolean']['output'];
/** mark all notifications as read */
readAllNotifications: Scalars['Boolean']['output'];
/** mark notification as read */
readNotification: Scalars['Boolean']['output'];
recoverDoc: Scalars['DateTime']['output'];
@@ -1952,6 +1976,13 @@ export interface PaginatedCommentObjectType {
totalCount: Scalars['Int']['output'];
}
export interface PaginatedCopilotHistoriesType {
__typename?: 'PaginatedCopilotHistoriesType';
edges: Array<CopilotHistoriesTypeEdge>;
pageInfo: PageInfo;
totalCount: Scalars['Int']['output'];
}
export interface PaginatedCopilotWorkspaceFileType {
__typename?: 'PaginatedCopilotWorkspaceFileType';
edges: Array<CopilotWorkspaceFileTypeEdge>;
@@ -2131,6 +2162,7 @@ export interface QueryChatHistoriesInput {
sessionId?: InputMaybe<Scalars['String']['input']>;
sessionOrder?: InputMaybe<ChatHistoryOrder>;
skip?: InputMaybe<Scalars['Int']['input']>;
withMessages?: InputMaybe<Scalars['Boolean']['input']>;
withPrompt?: InputMaybe<Scalars['Boolean']['input']>;
}
@@ -2317,7 +2349,10 @@ export enum SearchTable {
export interface ServerConfigType {
__typename?: 'ServerConfigType';
/** Whether allow guest users to create demo workspaces. */
/**
* Whether allow guest users to create demo workspaces.
* @deprecated This field is deprecated, please use `features` instead. Will be removed in 0.25.0
*/
allowGuestDemoWorkspace: Scalars['Boolean']['output'];
/** fetch latest available upgradable release of server */
availableUpgrade: Maybe<ReleaseVersionType>;
@@ -2351,6 +2386,7 @@ export enum ServerFeature {
Copilot = 'Copilot',
CopilotEmbedding = 'CopilotEmbedding',
Indexer = 'Indexer',
LocalWorkspace = 'LocalWorkspace',
OAuth = 'OAuth',
Payment = 'Payment',
}
@@ -2932,7 +2968,6 @@ export type AdminServerConfigQuery = {
baseUrl: string;
name: string;
features: Array<ServerFeature>;
allowGuestDemoWorkspace: boolean;
type: ServerDeploymentType;
initialized: boolean;
availableUserFeatures: Array<FeatureType>;
@@ -3755,6 +3790,7 @@ export type QueueWorkspaceEmbeddingMutation = {
export type GetCopilotHistoryIdsQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
pagination: PaginationInput;
docId?: InputMaybe<Scalars['String']['input']>;
options?: InputMaybe<QueryChatHistoriesInput>;
}>;
@@ -3765,17 +3801,31 @@ export type GetCopilotHistoryIdsQuery = {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
histories: Array<{
__typename?: 'CopilotHistories';
sessionId: string;
pinned: boolean;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
createdAt: string;
chats: {
__typename?: 'PaginatedCopilotHistoriesType';
pageInfo: {
__typename?: 'PageInfo';
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
edges: Array<{
__typename?: 'CopilotHistoriesTypeEdge';
cursor: string;
node: {
__typename?: 'CopilotHistories';
sessionId: string;
pinned: boolean;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
createdAt: string;
}>;
};
}>;
}>;
};
};
} | null;
};
@@ -3783,6 +3833,7 @@ export type GetCopilotHistoryIdsQuery = {
export type GetCopilotDocSessionsQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: Scalars['String']['input'];
pagination: PaginationInput;
options?: InputMaybe<QueryChatHistoriesInput>;
}>;
@@ -3792,31 +3843,53 @@ export type GetCopilotDocSessionsQuery = {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
histories: Array<{
__typename?: 'CopilotHistories';
sessionId: string;
pinned: boolean;
tokens: number;
action: string | null;
createdAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
chats: {
__typename?: 'PaginatedCopilotHistoriesType';
pageInfo: {
__typename?: 'PageInfo';
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
edges: Array<{
__typename?: 'CopilotHistoriesTypeEdge';
cursor: string;
node: {
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
parentSessionId: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
action: string | null;
pinned: boolean;
title: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
}>;
};
}>;
}>;
};
};
} | null;
};
@@ -3834,37 +3907,60 @@ export type GetCopilotPinnedSessionsQuery = {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
histories: Array<{
__typename?: 'CopilotHistories';
sessionId: string;
pinned: boolean;
tokens: number;
action: string | null;
createdAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
chats: {
__typename?: 'PaginatedCopilotHistoriesType';
pageInfo: {
__typename?: 'PageInfo';
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
edges: Array<{
__typename?: 'CopilotHistoriesTypeEdge';
cursor: string;
node: {
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
parentSessionId: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
action: string | null;
pinned: boolean;
title: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
}>;
};
}>;
}>;
};
};
} | null;
};
export type GetCopilotWorkspaceSessionsQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
pagination: PaginationInput;
options?: InputMaybe<QueryChatHistoriesInput>;
}>;
@@ -3874,37 +3970,60 @@ export type GetCopilotWorkspaceSessionsQuery = {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
histories: Array<{
__typename?: 'CopilotHistories';
sessionId: string;
pinned: boolean;
tokens: number;
action: string | null;
createdAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
chats: {
__typename?: 'PaginatedCopilotHistoriesType';
pageInfo: {
__typename?: 'PageInfo';
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
edges: Array<{
__typename?: 'CopilotHistoriesTypeEdge';
cursor: string;
node: {
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
parentSessionId: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
action: string | null;
pinned: boolean;
title: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
}>;
};
}>;
}>;
};
};
} | null;
};
export type GetCopilotHistoriesQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
pagination: PaginationInput;
docId?: InputMaybe<Scalars['String']['input']>;
options?: InputMaybe<QueryChatHistoriesInput>;
}>;
@@ -3915,31 +4034,53 @@ export type GetCopilotHistoriesQuery = {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
histories: Array<{
__typename?: 'CopilotHistories';
sessionId: string;
pinned: boolean;
tokens: number;
action: string | null;
createdAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
chats: {
__typename?: 'PaginatedCopilotHistoriesType';
pageInfo: {
__typename?: 'PageInfo';
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
edges: Array<{
__typename?: 'CopilotHistoriesTypeEdge';
cursor: string;
node: {
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
parentSessionId: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
action: string | null;
pinned: boolean;
title: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
}>;
};
}>;
}>;
};
};
} | null;
};
@@ -4093,26 +4234,53 @@ export type GetCopilotLatestDocSessionQuery = {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
histories: Array<{
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
pinned: boolean;
action: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
params: Record<string, string> | null;
createdAt: string;
chats: {
__typename?: 'PaginatedCopilotHistoriesType';
pageInfo: {
__typename?: 'PageInfo';
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
edges: Array<{
__typename?: 'CopilotHistoriesTypeEdge';
cursor: string;
node: {
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
parentSessionId: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
action: string | null;
pinned: boolean;
title: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
}>;
};
}>;
}>;
};
};
} | null;
};
@@ -4128,16 +4296,52 @@ export type GetCopilotSessionQuery = {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
session: {
__typename?: 'CopilotSessionType';
id: string;
parentSessionId: string | null;
docId: string | null;
pinned: boolean;
title: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
chats: {
__typename?: 'PaginatedCopilotHistoriesType';
pageInfo: {
__typename?: 'PageInfo';
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
edges: Array<{
__typename?: 'CopilotHistoriesTypeEdge';
cursor: string;
node: {
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
parentSessionId: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
action: string | null;
pinned: boolean;
title: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
}>;
};
}>;
};
};
} | null;
@@ -4154,17 +4358,53 @@ export type GetCopilotRecentSessionsQuery = {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
histories: Array<{
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
pinned: boolean;
action: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
}>;
chats: {
__typename?: 'PaginatedCopilotHistoriesType';
pageInfo: {
__typename?: 'PageInfo';
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
edges: Array<{
__typename?: 'CopilotHistoriesTypeEdge';
cursor: string;
node: {
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
parentSessionId: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
action: string | null;
pinned: boolean;
title: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
}>;
};
}>;
};
};
} | null;
};
@@ -4180,8 +4420,9 @@ export type UpdateCopilotSessionMutation = {
export type GetCopilotSessionsQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
pagination: PaginationInput;
docId?: InputMaybe<Scalars['String']['input']>;
options?: InputMaybe<QueryChatSessionsInput>;
options?: InputMaybe<QueryChatHistoriesInput>;
}>;
export type GetCopilotSessionsQuery = {
@@ -4190,17 +4431,53 @@ export type GetCopilotSessionsQuery = {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
sessions: Array<{
__typename?: 'CopilotSessionType';
id: string;
parentSessionId: string | null;
docId: string | null;
pinned: boolean;
title: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
}>;
chats: {
__typename?: 'PaginatedCopilotHistoriesType';
pageInfo: {
__typename?: 'PageInfo';
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
edges: Array<{
__typename?: 'CopilotHistoriesTypeEdge';
cursor: string;
node: {
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
parentSessionId: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
action: string | null;
pinned: boolean;
title: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
}>;
};
}>;
};
};
} | null;
};
@@ -4436,6 +4713,106 @@ export type GetDocRolePermissionsQuery = {
};
};
export type CopilotChatMessageFragment = {
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
};
export type CopilotChatHistoryFragment = {
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
parentSessionId: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
action: string | null;
pinned: boolean;
title: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
}>;
};
export type PaginatedCopilotChatsFragment = {
__typename?: 'PaginatedCopilotHistoriesType';
pageInfo: {
__typename?: 'PageInfo';
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
edges: Array<{
__typename?: 'CopilotHistoriesTypeEdge';
cursor: string;
node: {
__typename?: 'CopilotHistories';
sessionId: string;
workspaceId: string;
docId: string | null;
parentSessionId: string | null;
promptName: string;
model: string;
optionalModels: Array<string>;
action: string | null;
pinned: boolean;
title: string | null;
tokens: number;
createdAt: string;
updatedAt: string;
messages: Array<{
__typename?: 'ChatMessage';
id: string | null;
role: string;
content: string;
attachments: Array<string> | null;
createdAt: string;
streamObjects: Array<{
__typename?: 'StreamObject';
type: string;
textDelta: string | null;
toolCallId: string | null;
toolName: string | null;
args: Record<string, string> | null;
result: Record<string, string> | null;
}> | null;
}>;
};
}>;
};
export type CredentialsRequirementsFragment = {
__typename?: 'CredentialsRequirementType';
password: {
@@ -5218,6 +5595,15 @@ export type QuotaQuery = {
} | null;
};
export type ReadAllNotificationsMutationVariables = Exact<{
[key: string]: never;
}>;
export type ReadAllNotificationsMutation = {
__typename?: 'Mutation';
readAllNotifications: boolean;
};
export type ReadNotificationMutationVariables = Exact<{
id: Scalars['String']['input'];
}>;
@@ -5353,7 +5739,6 @@ export type ServerConfigQuery = {
baseUrl: string;
name: string;
features: Array<ServerFeature>;
allowGuestDemoWorkspace: boolean;
type: ServerDeploymentType;
initialized: boolean;
credentialsRequirement: {
@@ -6352,6 +6737,11 @@ export type Mutations =
variables: PublishPageMutationVariables;
response: PublishPageMutation;
}
| {
name: 'readAllNotificationsMutation';
variables: ReadAllNotificationsMutationVariables;
response: ReadAllNotificationsMutation;
}
| {
name: 'readNotificationMutation';
variables: ReadNotificationMutationVariables;
@@ -1,6 +1,8 @@
package app.affine.pro.ai.chat
import com.affine.pro.graphql.GetCopilotHistoriesQuery
import com.affine.pro.graphql.fragment.CopilotChatHistory
import com.affine.pro.graphql.fragment.CopilotChatMessage
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
@@ -51,11 +53,11 @@ data class ChatMessage(
createAt = Clock.System.now(),
)
fun from(message: GetCopilotHistoriesQuery.Message) = ChatMessage(
fun from(message: CopilotChatMessage) = ChatMessage(
id = message.id,
role = Role.fromValue(message.role),
content = message.content,
createAt = message.createdAt,
)
}
}
}
@@ -9,7 +9,8 @@ import com.affine.pro.graphql.GetCopilotHistoryIdsQuery
import com.affine.pro.graphql.GetCopilotSessionsQuery
import com.affine.pro.graphql.type.CreateChatMessageInput
import com.affine.pro.graphql.type.CreateChatSessionInput
import com.affine.pro.graphql.type.QueryChatSessionsInput
import com.affine.pro.graphql.type.PaginationInput
import com.affine.pro.graphql.type.QueryChatHistoriesInput
import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.api.Mutation
import com.apollographql.apollo.api.Optional
@@ -29,12 +30,15 @@ class GraphQLService @Inject constructor() {
GetCopilotSessionsQuery(
workspaceId = workspaceId,
docId = Optional.present(docId),
options = Optional.present(QueryChatSessionsInput(action = Optional.present(false)))
pagination = PaginationInput(
first = Optional.present(100)
),
options = Optional.present(QueryChatHistoriesInput(action = Optional.present(false)))
)
).mapCatching { data ->
data.currentUser?.copilot?.sessions?.find {
data.currentUser?.copilot?.chats?.paginatedCopilotChats?.edges?.map { item -> item.node.copilotChatHistory }?.find {
it.parentSessionId == null
}?.id ?: error(ERROR_NULL_SESSION_ID)
}?.sessionId ?: error(ERROR_NULL_SESSION_ID)
}
suspend fun createCopilotSession(
@@ -60,12 +64,15 @@ class GraphQLService @Inject constructor() {
) = query(
GetCopilotHistoriesQuery(
workspaceId = workspaceId,
pagination = PaginationInput(
first = Optional.present(100)
),
docId = Optional.present(docId),
)
).mapCatching { data ->
data.currentUser?.copilot?.histories?.firstOrNull { history ->
history.sessionId == sessionId
}?.messages ?: emptyList()
data.currentUser?.copilot?.chats?.paginatedCopilotChats?.edges?.map { item -> item.node.copilotChatHistory }?.firstOrNull { history ->
history.sessionId == sessionId
}?.messages?.map { msg -> msg.copilotChatMessage } ?: emptyList()
}
suspend fun getCopilotHistoryIds(
@@ -76,9 +83,12 @@ class GraphQLService @Inject constructor() {
GetCopilotHistoryIdsQuery(
workspaceId = workspaceId,
docId = Optional.present(docId),
pagination = PaginationInput(
first = Optional.present(100)
),
)
).mapCatching { data ->
data.currentUser?.copilot?.histories?.firstOrNull { history ->
data.currentUser?.copilot?.chats?.edges?.map { item -> item.node }?.firstOrNull { history ->
history.sessionId == sessionId
}?.messages ?: emptyList()
}
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
@@ -90,6 +90,8 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
C45499AB2D140B5000E21978 /* NBStore */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = NBStore;
sourceTree = "<group>";
};
@@ -337,13 +339,9 @@
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AFFiNE/Pods-AFFiNE-frameworks.sh\"\n";
@@ -0,0 +1,79 @@
// @generated
// This file was automatically generated and should not be edited.
@_exported import ApolloAPI
public struct CopilotChatHistory: AffineGraphQL.SelectionSet, Fragment {
public static var fragmentDefinition: StaticString {
#"fragment CopilotChatHistory on CopilotHistories { __typename sessionId workspaceId docId parentSessionId promptName model optionalModels action pinned title tokens messages { __typename ...CopilotChatMessage } createdAt updatedAt }"#
}
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.CopilotHistories }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("sessionId", String.self),
.field("workspaceId", String.self),
.field("docId", String?.self),
.field("parentSessionId", String?.self),
.field("promptName", String.self),
.field("model", String.self),
.field("optionalModels", [String].self),
.field("action", String?.self),
.field("pinned", Bool.self),
.field("title", String?.self),
.field("tokens", Int.self),
.field("messages", [Message].self),
.field("createdAt", AffineGraphQL.DateTime.self),
.field("updatedAt", AffineGraphQL.DateTime.self),
] }
public var sessionId: String { __data["sessionId"] }
public var workspaceId: String { __data["workspaceId"] }
public var docId: String? { __data["docId"] }
public var parentSessionId: String? { __data["parentSessionId"] }
public var promptName: String { __data["promptName"] }
public var model: String { __data["model"] }
public var optionalModels: [String] { __data["optionalModels"] }
/// An mark identifying which view to use to display the session
public var action: String? { __data["action"] }
public var pinned: Bool { __data["pinned"] }
public var title: String? { __data["title"] }
/// The number of tokens used in the session
public var tokens: Int { __data["tokens"] }
public var messages: [Message] { __data["messages"] }
public var createdAt: AffineGraphQL.DateTime { __data["createdAt"] }
public var updatedAt: AffineGraphQL.DateTime { __data["updatedAt"] }
/// Message
///
/// Parent Type: `ChatMessage`
public struct Message: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.ChatMessage }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.fragment(CopilotChatMessage.self),
] }
public var id: AffineGraphQL.ID? { __data["id"] }
public var role: String { __data["role"] }
public var content: String { __data["content"] }
public var attachments: [String]? { __data["attachments"] }
public var streamObjects: [StreamObject]? { __data["streamObjects"] }
public var createdAt: AffineGraphQL.DateTime { __data["createdAt"] }
public struct Fragments: FragmentContainer {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public var copilotChatMessage: CopilotChatMessage { _toFragment() }
}
public typealias StreamObject = CopilotChatMessage.StreamObject
}
}
@@ -0,0 +1,57 @@
// @generated
// This file was automatically generated and should not be edited.
@_exported import ApolloAPI
public struct CopilotChatMessage: AffineGraphQL.SelectionSet, Fragment {
public static var fragmentDefinition: StaticString {
#"fragment CopilotChatMessage on ChatMessage { __typename id role content attachments streamObjects { __typename type textDelta toolCallId toolName args result } createdAt }"#
}
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.ChatMessage }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("id", AffineGraphQL.ID?.self),
.field("role", String.self),
.field("content", String.self),
.field("attachments", [String]?.self),
.field("streamObjects", [StreamObject]?.self),
.field("createdAt", AffineGraphQL.DateTime.self),
] }
public var id: AffineGraphQL.ID? { __data["id"] }
public var role: String { __data["role"] }
public var content: String { __data["content"] }
public var attachments: [String]? { __data["attachments"] }
public var streamObjects: [StreamObject]? { __data["streamObjects"] }
public var createdAt: AffineGraphQL.DateTime { __data["createdAt"] }
/// StreamObject
///
/// Parent Type: `StreamObject`
public struct StreamObject: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.StreamObject }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("type", String.self),
.field("textDelta", String?.self),
.field("toolCallId", String?.self),
.field("toolName", String?.self),
.field("args", AffineGraphQL.JSON?.self),
.field("result", AffineGraphQL.JSON?.self),
] }
public var type: String { __data["type"] }
public var textDelta: String? { __data["textDelta"] }
public var toolCallId: String? { __data["toolCallId"] }
public var toolName: String? { __data["toolName"] }
public var args: AffineGraphQL.JSON? { __data["args"] }
public var result: AffineGraphQL.JSON? { __data["result"] }
}
}
@@ -0,0 +1,103 @@
// @generated
// This file was automatically generated and should not be edited.
@_exported import ApolloAPI
public struct PaginatedCopilotChats: AffineGraphQL.SelectionSet, Fragment {
public static var fragmentDefinition: StaticString {
#"fragment PaginatedCopilotChats on PaginatedCopilotHistoriesType { __typename pageInfo { __typename hasNextPage hasPreviousPage startCursor endCursor } edges { __typename cursor node { __typename ...CopilotChatHistory } } }"#
}
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.PaginatedCopilotHistoriesType }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("pageInfo", PageInfo.self),
.field("edges", [Edge].self),
] }
public var pageInfo: PageInfo { __data["pageInfo"] }
public var edges: [Edge] { __data["edges"] }
/// PageInfo
///
/// Parent Type: `PageInfo`
public struct PageInfo: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.PageInfo }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("hasNextPage", Bool.self),
.field("hasPreviousPage", Bool.self),
.field("startCursor", String?.self),
.field("endCursor", String?.self),
] }
public var hasNextPage: Bool { __data["hasNextPage"] }
public var hasPreviousPage: Bool { __data["hasPreviousPage"] }
public var startCursor: String? { __data["startCursor"] }
public var endCursor: String? { __data["endCursor"] }
}
/// Edge
///
/// Parent Type: `CopilotHistoriesTypeEdge`
public struct Edge: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.CopilotHistoriesTypeEdge }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("cursor", String.self),
.field("node", Node.self),
] }
public var cursor: String { __data["cursor"] }
public var node: Node { __data["node"] }
/// Edge.Node
///
/// Parent Type: `CopilotHistories`
public struct Node: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.CopilotHistories }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.fragment(CopilotChatHistory.self),
] }
public var sessionId: String { __data["sessionId"] }
public var workspaceId: String { __data["workspaceId"] }
public var docId: String? { __data["docId"] }
public var parentSessionId: String? { __data["parentSessionId"] }
public var promptName: String { __data["promptName"] }
public var model: String { __data["model"] }
public var optionalModels: [String] { __data["optionalModels"] }
/// An mark identifying which view to use to display the session
public var action: String? { __data["action"] }
public var pinned: Bool { __data["pinned"] }
public var title: String? { __data["title"] }
/// The number of tokens used in the session
public var tokens: Int { __data["tokens"] }
public var messages: [Message] { __data["messages"] }
public var createdAt: AffineGraphQL.DateTime { __data["createdAt"] }
public var updatedAt: AffineGraphQL.DateTime { __data["updatedAt"] }
public struct Fragments: FragmentContainer {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public var copilotChatHistory: CopilotChatHistory { _toFragment() }
}
public typealias Message = CopilotChatHistory.Message
}
}
}
@@ -0,0 +1,136 @@
// @generated
// This file was automatically generated and should not be edited.
@_exported import ApolloAPI
public class CreateCommentMutation: GraphQLMutation {
public static let operationName: String = "createComment"
public static let operationDocument: ApolloAPI.OperationDocument = .init(
definition: .init(
#"mutation createComment($input: CommentCreateInput!) { createComment(input: $input) { __typename id content resolved createdAt updatedAt user { __typename id name avatarUrl } replies { __typename commentId id content createdAt updatedAt user { __typename id name avatarUrl } } } }"#
))
public var input: CommentCreateInput
public init(input: CommentCreateInput) {
self.input = input
}
public var __variables: Variables? { ["input": input] }
public struct Data: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation }
public static var __selections: [ApolloAPI.Selection] { [
.field("createComment", CreateComment.self, arguments: ["input": .variable("input")]),
] }
public var createComment: CreateComment { __data["createComment"] }
/// CreateComment
///
/// Parent Type: `CommentObjectType`
public struct CreateComment: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.CommentObjectType }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("id", AffineGraphQL.ID.self),
.field("content", AffineGraphQL.JSONObject.self),
.field("resolved", Bool.self),
.field("createdAt", AffineGraphQL.DateTime.self),
.field("updatedAt", AffineGraphQL.DateTime.self),
.field("user", User.self),
.field("replies", [Reply].self),
] }
public var id: AffineGraphQL.ID { __data["id"] }
/// The content of the comment
public var content: AffineGraphQL.JSONObject { __data["content"] }
/// Whether the comment is resolved
public var resolved: Bool { __data["resolved"] }
/// The created at time of the comment
public var createdAt: AffineGraphQL.DateTime { __data["createdAt"] }
/// The updated at time of the comment
public var updatedAt: AffineGraphQL.DateTime { __data["updatedAt"] }
/// The user who created the comment
public var user: User { __data["user"] }
/// The replies of the comment
public var replies: [Reply] { __data["replies"] }
/// CreateComment.User
///
/// Parent Type: `PublicUserType`
public struct User: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.PublicUserType }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("id", String.self),
.field("name", String.self),
.field("avatarUrl", String?.self),
] }
public var id: String { __data["id"] }
public var name: String { __data["name"] }
public var avatarUrl: String? { __data["avatarUrl"] }
}
/// CreateComment.Reply
///
/// Parent Type: `ReplyObjectType`
public struct Reply: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.ReplyObjectType }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("commentId", AffineGraphQL.ID.self),
.field("id", AffineGraphQL.ID.self),
.field("content", AffineGraphQL.JSONObject.self),
.field("createdAt", AffineGraphQL.DateTime.self),
.field("updatedAt", AffineGraphQL.DateTime.self),
.field("user", User.self),
] }
public var commentId: AffineGraphQL.ID { __data["commentId"] }
public var id: AffineGraphQL.ID { __data["id"] }
/// The content of the reply
public var content: AffineGraphQL.JSONObject { __data["content"] }
/// The created at time of the reply
public var createdAt: AffineGraphQL.DateTime { __data["createdAt"] }
/// The updated at time of the reply
public var updatedAt: AffineGraphQL.DateTime { __data["updatedAt"] }
/// The user who created the reply
public var user: User { __data["user"] }
/// CreateComment.Reply.User
///
/// Parent Type: `PublicUserType`
public struct User: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.PublicUserType }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("id", String.self),
.field("name", String.self),
.field("avatarUrl", String?.self),
] }
public var id: String { __data["id"] }
public var name: String { __data["name"] }
public var avatarUrl: String? { __data["avatarUrl"] }
}
}
}
}
}
@@ -0,0 +1,82 @@
// @generated
// This file was automatically generated and should not be edited.
@_exported import ApolloAPI
public class CreateReplyMutation: GraphQLMutation {
public static let operationName: String = "createReply"
public static let operationDocument: ApolloAPI.OperationDocument = .init(
definition: .init(
#"mutation createReply($input: ReplyCreateInput!) { createReply(input: $input) { __typename commentId id content createdAt updatedAt user { __typename id name avatarUrl } } }"#
))
public var input: ReplyCreateInput
public init(input: ReplyCreateInput) {
self.input = input
}
public var __variables: Variables? { ["input": input] }
public struct Data: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation }
public static var __selections: [ApolloAPI.Selection] { [
.field("createReply", CreateReply.self, arguments: ["input": .variable("input")]),
] }
public var createReply: CreateReply { __data["createReply"] }
/// CreateReply
///
/// Parent Type: `ReplyObjectType`
public struct CreateReply: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.ReplyObjectType }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("commentId", AffineGraphQL.ID.self),
.field("id", AffineGraphQL.ID.self),
.field("content", AffineGraphQL.JSONObject.self),
.field("createdAt", AffineGraphQL.DateTime.self),
.field("updatedAt", AffineGraphQL.DateTime.self),
.field("user", User.self),
] }
public var commentId: AffineGraphQL.ID { __data["commentId"] }
public var id: AffineGraphQL.ID { __data["id"] }
/// The content of the reply
public var content: AffineGraphQL.JSONObject { __data["content"] }
/// The created at time of the reply
public var createdAt: AffineGraphQL.DateTime { __data["createdAt"] }
/// The updated at time of the reply
public var updatedAt: AffineGraphQL.DateTime { __data["updatedAt"] }
/// The user who created the reply
public var user: User { __data["user"] }
/// CreateReply.User
///
/// Parent Type: `PublicUserType`
public struct User: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.PublicUserType }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("id", String.self),
.field("name", String.self),
.field("avatarUrl", String?.self),
] }
public var id: String { __data["id"] }
public var name: String { __data["name"] }
public var avatarUrl: String? { __data["avatarUrl"] }
}
}
}
}
@@ -0,0 +1,33 @@
// @generated
// This file was automatically generated and should not be edited.
@_exported import ApolloAPI
public class DeleteCommentMutation: GraphQLMutation {
public static let operationName: String = "deleteComment"
public static let operationDocument: ApolloAPI.OperationDocument = .init(
definition: .init(
#"mutation deleteComment($id: String!) { deleteComment(id: $id) }"#
))
public var id: String
public init(id: String) {
self.id = id
}
public var __variables: Variables? { ["id": id] }
public struct Data: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation }
public static var __selections: [ApolloAPI.Selection] { [
.field("deleteComment", Bool.self, arguments: ["id": .variable("id")]),
] }
/// Delete a comment
public var deleteComment: Bool { __data["deleteComment"] }
}
}
@@ -0,0 +1,33 @@
// @generated
// This file was automatically generated and should not be edited.
@_exported import ApolloAPI
public class DeleteReplyMutation: GraphQLMutation {
public static let operationName: String = "deleteReply"
public static let operationDocument: ApolloAPI.OperationDocument = .init(
definition: .init(
#"mutation deleteReply($id: String!) { deleteReply(id: $id) }"#
))
public var id: String
public init(id: String) {
self.id = id
}
public var __variables: Variables? { ["id": id] }
public struct Data: AffineGraphQL.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Mutation }
public static var __selections: [ApolloAPI.Selection] { [
.field("deleteReply", Bool.self, arguments: ["id": .variable("id")]),
] }
/// Delete a reply
public var deleteReply: Bool { __data["deleteReply"] }
}
}

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