Show a desktop notification when the AI TA finishes replying
Notify me when classmates post messages in the forum
Play an alert sound whenever there is a new notification
A multi-user real-time collaborative document system built around Yjs CRDTs, transmitting incremental updates via Socket.IO. Supports snapshot-based version history, line-by-line comments, clipboard image uploads, KaTeX equations, and one-click export to a course RAG knowledge base.
Collaborative tools such as Google Docs are already standard in university teaching, but commercial tools have two pain points: (1) Student work and Learning history are scattered across external platforms, making them impossible to incorporate into research data; (2) they cannot be integrated with in-course RAG, Aida, or Bloom analysers.
Uedu collaborative notes (UeduNote) are built into the platform, enabling teachers and students to complete group discussion drafts, shared notes and project report drafts in Uedu. When finished, they can export to Course RAG with one click, allowing the class AI assistant to understand the document.
Operational Transformation (OT, used by Google Docs) requires a central server to perform transforms when handling complex concurrency, making implementation difficult and edge cases numerous. CRDT (Conflict-free Replicated Data Type) guarantees commutativity through the data structure itself, without relying on a central arbiter, making it suitable for rapid deployment. Yjs is currently the most mature CRDT implementation, with a Python port pycrdt, allowing the backend to operate on the same Y.Doc directly.
The UeduNote backend uses pycrdt (Python implementation of Yjs), and the frontend uses the original Yjs (JavaScript). Both sides share the same Y.Doc binary representation, meaning:
Located in utils/yjs_doc_manager.py. The global singleton yjs_manager is responsible for:
yjs_state BLOB field in MySQLget_update_for_client(state_vector) to generate incremental updatesNamespace: /collab-editor. The event flow follows Yjs's standard three-stage handshake + continuous update:
| Event | Direction | Purpose |
|---|---|---|
join_document | C → S | Join the room, authorisation verification (_collab_socket_room_auth), load Y.Doc |
yjs_sync_step1 | C → S | The client sends its own state vector |
yjs_sync_step1_response | S → C | The server returns the client's missing updates + the server's own state vector |
yjs_sync_step2 | C → S | The client sends back the updates the server is missing |
yjs_update | C ↔ S | Subsequent incremental edits are broadcast bidirectionally |
yjs_awareness | C ↔ S | Remote cursors, selected blocks (y-protocols/awareness) |
yjs_save_version | C → S | Manually save version snapshot |
yjs_full_reset | S → C | Force the client to resynchronise after restoring a previous version |
All socket events use cached authentication via _collab_socket_room_auth() (one DB lookup, stored in the in-memory collab_editor_rooms structure). Subsequent events use an in-memory lookup to determine whether edit, comment or view is allowed. This avoids hitting the DB for every event.
The core schema is defined in sql/collab_editor.sql and sql/collab_editor_yjs.sql:
| Data table | Purpose |
|---|---|
collab_documents | Main document table. UUID, title, content (plain text), yjs_state (BLOB, CRDT binary), owner, permission, word_count |
collab_document_versions | Version snapshot. version_number auto-increments, full content, edit_summary, edited_by |
collab_document_yjs_updates | Incremental update buffer (for future update compaction) |
collab_document_collaborators | Collaborator list. role ∈ {editor, commenter, viewer} |
collab_document_comments | Comments. line_ref (optional line-number anchor), parent_id (threading), is_resolved |
collab_groups、collab_group_members | Group-based collaboration (invitation code mechanism) |
Y.Doc's yjs_state is binary and cannot be directly read by MySQL full-text indexes or AI. Whenever a version is saved or when it is stored due to inactivity, sync extract plain text and write it to the content field for search, export and RAG use.
Uses a 4-level role + two dimensions:
| role | Action |
|---|---|
| admin (owner) | Delete documents, manage collaborators, change permission levels |
| edit | Edit content, upload images, save versions |
| comment | Add / reply to / resolve comments |
| view | Browse content |
In addition to the per-user collab_document_collaborators.collab_role, documents have a document-level permission field, which can be set to editable, commentable, view_only or private. The permissions of Group members and community members are also taken into account.
Permission calculation is centralised in _collab_get_doc_perm(doc_uuid, user_id). The result is stored in the collab_editor_rooms in-memory structure; subsequent socket events use in-memory lookup only, avoiding DB queries on the hot path.
yjs_save_version, immediately writes to collab_documents and adds a collab_document_versions recordyjs_state + content, but does not create version recordsPOST /api/editor/documents/<doc_uuid>/versions/<version_id>/restore behaviour:
yjs_manager.reset_doc() to replace the in-memory versionyjs_full_reset to all connected clients, forcing re-synchronisationEarly UeduNote stored plain text only, with no CRDT. The first time an old document is loaded, an initial Y.Doc is created from content and written back to yjs_state, after which it operates in CRDT mode. This is a lazy migration, so no downtime is required.
Comments are anchored to line numbers (line_ref), rather than CRDT position indexes. The benefits are:
The trade-off is this: after making major additions and deletions inline, the meaning of the “line in question” in the comment may drift. In practice, this is suitable for university teaching scenarios (moderate comment frequency, and a separate conversation can be opened after substantial revisions).
Highlight colour settings are handled by the front end and ultimately written into the Y.Text attribute, synchronised via CRDT together with the text content. Highlight data are not stored separately on the back end.
NAS_UEDU_PATH/collab_editor_images/; on the test machine, local uploads/collab_editor_images/{timestamp}_{uuid}.{ext}, to avoid naming collisionsutils/file_validation.py), to prevent polyglot attackspaste events and uploads image blobs in multipart form/api/editor/documents/<doc_uuid>/images/<filename>, verify view permission; secure_filename() prevents path traversalendpoint: POST /api/editor/documents/<doc_uuid>/export-to-rag. Process:
collab_documents.content.md filerag_documents table (classroom_id, file_type=md, status=completed)After the group completes the project notes, the Instructor can export them to the Course RAG, allowing the class's AI TA to cite this collaboratively organised knowledge. Subsequent conversations between Students and AI are then built on "the understanding co-constructed by classmates", enabling the digital practice of Social Constructivism.
At present, export is one-way (document → RAG). Future plans include support for two-way synchronisation: when RAG content is updated, the original note owner will be notified automatically.
Collaborative note-taking provides fine-grained data for the Sociomics dimension (social interaction). Research questions include:
Traditional Google Docs activity logs have to be reconstructed via third-party tools (such as Draftback), and the content belongs to Google. UeduNote's Y.Doc updates are fully retained on the server, and the edit history at every keystroke level can be replayed, making it an ideal data source for collaborative learning research.
When citing this system, please cite: "UeduNote: Yjs-based real-time collaborative editor with CRDT replay capability (https://uedu.tw)". When analysing edit history, it is recommended to state the Y.Doc update parsing tool used and the time granularity.