mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-26 01:06:23 +02:00
Rename comments files to chat_comments and add mention parser utility
This commit is contained in:
parent
7d43f1fb8f
commit
c793e2d621
6 changed files with 573 additions and 5 deletions
506
docs/comments-implementation-guide.md
Normal file
506
docs/comments-implementation-guide.md
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
# Comments & Mentions Implementation Guide
|
||||
|
||||
> Implementation guide for adding Google Docs-style comments and @mentions to shared chats in SurfSense.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Architecture Decisions](#architecture-decisions)
|
||||
3. [Database Design](#database-design)
|
||||
4. [API Design](#api-design)
|
||||
5. [Permission Model](#permission-model)
|
||||
6. [Business Rules](#business-rules)
|
||||
7. [File Structure](#file-structure)
|
||||
8. [Implementation Phases](#implementation-phases)
|
||||
9. [Testing Checklist](#testing-checklist)
|
||||
10. [Out of Scope](#out-of-scope)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
### Problem Statement
|
||||
|
||||
When team members view a shared AI chat, they cannot discuss specific answers in place. Users currently have to screenshot or copy-paste the output into external tools (Slack/Teams) to ask questions or validate data. This context switching causes friction and fragments organizational knowledge.
|
||||
|
||||
### User Story
|
||||
|
||||
As a user, I want to be able to place and reply to comments on AI responses to let my team know my thoughts without leaving SurfSense.
|
||||
|
||||
### Solution
|
||||
|
||||
Users can place comments on AI chat responses and reply to existing comments, similar to Google Docs, but limited to a single level of nesting per AI response.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
### Threading Model
|
||||
|
||||
```
|
||||
AI Response (message_id: 123)
|
||||
├── Comment A (parent_id: NULL) ← Top-level comment
|
||||
│ ├── Reply A1 (parent_id: A) ← Reply
|
||||
│ ├── Reply A2 (parent_id: A) ← Reply
|
||||
│ └── Reply A3 (parent_id: A) ← Reply
|
||||
├── Comment B (parent_id: NULL) ← Top-level comment
|
||||
│ └── Reply B1 (parent_id: B) ← Reply
|
||||
└── Comment C (parent_id: NULL) ← Top-level comment (no replies)
|
||||
```
|
||||
|
||||
- **Multiple top-level comments** allowed per AI response
|
||||
- **One level of nesting** (replies to comments, but no replies to replies)
|
||||
- API enforces nesting limit, not DB constraint
|
||||
|
||||
### Comment Anchoring
|
||||
|
||||
- Comments attach to **AI responses only** (not user messages)
|
||||
- Comments attach to **entire message** (not text selection)
|
||||
- Validated by checking `message.role == 'assistant'`
|
||||
|
||||
### @Mentions
|
||||
|
||||
- **Storage format:** `@[uuid]` in raw content
|
||||
- **Display format:** `@Display Name` in rendered content
|
||||
- Mentions extracted and stored in separate table for notifications
|
||||
- Only search space members can be mentioned
|
||||
|
||||
### Read/Unread Mentions
|
||||
|
||||
- Simple boolean on mention record
|
||||
- Marked read when user clicks the mention notification
|
||||
- No automatic read-on-view (keep it simple)
|
||||
|
||||
### Cascade Behavior
|
||||
|
||||
- Deleting a comment deletes all its replies (DB CASCADE)
|
||||
- Deleting/regenerating a message deletes its comments (DB CASCADE)
|
||||
|
||||
### Backend-First Architecture
|
||||
|
||||
- All business logic in backend
|
||||
- Frontend is a thin consumer
|
||||
- Backend returns computed fields: `can_edit`, `can_delete`, `is_edited`, `content_rendered`
|
||||
|
||||
---
|
||||
|
||||
## Database Design
|
||||
|
||||
### Table: `chat_comments`
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| `id` | `SERIAL` | PK | Primary key |
|
||||
| `message_id` | `INTEGER` | FK → `new_chat_messages(id)` ON DELETE CASCADE, NOT NULL | Which AI response |
|
||||
| `parent_id` | `INTEGER` | FK → `chat_comments(id)` ON DELETE CASCADE, NULLABLE | NULL = top-level, otherwise = reply |
|
||||
| `author_id` | `UUID` | FK → `user(id)` ON DELETE SET NULL | Who wrote it |
|
||||
| `content` | `TEXT` | NOT NULL | Plain text, may contain `@[uuid]` |
|
||||
| `created_at` | `TIMESTAMPTZ` | NOT NULL, DEFAULT NOW() | Creation time |
|
||||
| `updated_at` | `TIMESTAMPTZ` | NOT NULL, DEFAULT NOW() | Last edit time |
|
||||
|
||||
**Indexes:**
|
||||
- `idx_comments_message_id` on `message_id`
|
||||
- `idx_comments_parent_id` on `parent_id`
|
||||
- `idx_comments_author_id` on `author_id`
|
||||
- `idx_comments_created_at` on `created_at`
|
||||
|
||||
### Table: `chat_comment_mentions`
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| `id` | `SERIAL` | PK | Primary key |
|
||||
| `comment_id` | `INTEGER` | FK → `chat_comments(id)` ON DELETE CASCADE, NOT NULL | Which comment |
|
||||
| `mentioned_user_id` | `UUID` | FK → `user(id)` ON DELETE CASCADE, NOT NULL | Who was mentioned |
|
||||
| `read` | `BOOLEAN` | NOT NULL, DEFAULT FALSE | Has user seen it |
|
||||
| `created_at` | `TIMESTAMPTZ` | NOT NULL, DEFAULT NOW() | When mentioned |
|
||||
|
||||
**Constraints:**
|
||||
- `UNIQUE (comment_id, mentioned_user_id)` - Can't mention same person twice
|
||||
|
||||
**Indexes:**
|
||||
- `idx_mentions_user_unread` on `(mentioned_user_id) WHERE read = FALSE` (partial index)
|
||||
- `idx_mentions_comment_id` on `comment_id`
|
||||
|
||||
### Schema Diagram
|
||||
|
||||
```
|
||||
new_chat_messages (existing)
|
||||
│
|
||||
│ 1:N
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ chat_comments │
|
||||
├──────────────────────────────────┤
|
||||
│ id (PK) │
|
||||
│ message_id (FK) │
|
||||
│ parent_id (FK, self-ref) │
|
||||
│ author_id (FK → user) │
|
||||
│ content │
|
||||
│ created_at │
|
||||
│ updated_at │
|
||||
└──────────────────────────────────┘
|
||||
│
|
||||
│ 1:N
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ chat_comment_mentions │
|
||||
├──────────────────────────────────┤
|
||||
│ id (PK) │
|
||||
│ comment_id (FK) │
|
||||
│ mentioned_user_id (FK → user) │
|
||||
│ read │
|
||||
│ created_at │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Design
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
|--------|----------|---------|
|
||||
| `GET` | `/api/v1/messages/{message_id}/comments` | List comments with replies |
|
||||
| `POST` | `/api/v1/messages/{message_id}/comments` | Create top-level comment |
|
||||
| `POST` | `/api/v1/comments/{comment_id}/replies` | Reply to a comment |
|
||||
| `PUT` | `/api/v1/comments/{comment_id}` | Edit comment (author only) |
|
||||
| `DELETE` | `/api/v1/comments/{comment_id}` | Delete comment + replies |
|
||||
| `GET` | `/api/v1/users/me/mentions` | List user's mentions |
|
||||
| `POST` | `/api/v1/mentions/{mention_id}/read` | Mark mention as read |
|
||||
| `POST` | `/api/v1/users/me/mentions/read-all` | Mark all mentions read |
|
||||
|
||||
### Request Schemas
|
||||
|
||||
**Create/Update Comment:**
|
||||
```json
|
||||
{
|
||||
"content": "string (1-5000 chars)"
|
||||
}
|
||||
```
|
||||
|
||||
### Response Schemas
|
||||
|
||||
**Comment Response:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"message_id": 123,
|
||||
"content": "Is this accurate? @[uuid-here]",
|
||||
"content_rendered": "Is this accurate? @John Doe",
|
||||
"author": {
|
||||
"id": "uuid",
|
||||
"display_name": "Jane Smith",
|
||||
"avatar_url": "https://...",
|
||||
"email": "jane@example.com"
|
||||
},
|
||||
"created_at": "2024-01-15T10:42:00Z",
|
||||
"updated_at": "2024-01-15T10:42:00Z",
|
||||
"is_edited": false,
|
||||
"can_edit": true,
|
||||
"can_delete": true,
|
||||
"reply_count": 2,
|
||||
"replies": [
|
||||
{
|
||||
"id": 2,
|
||||
"content": "Yes, verified",
|
||||
"content_rendered": "Yes, verified",
|
||||
"author": { ... },
|
||||
"created_at": "...",
|
||||
"updated_at": "...",
|
||||
"is_edited": false,
|
||||
"can_edit": false,
|
||||
"can_delete": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Comment List Response:**
|
||||
```json
|
||||
{
|
||||
"comments": [ ... ],
|
||||
"total_count": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Mention Response:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"read": false,
|
||||
"created_at": "2024-01-15T10:42:00Z",
|
||||
"comment": {
|
||||
"id": 5,
|
||||
"content_preview": "Hey, can you check...",
|
||||
"author": {
|
||||
"display_name": "John Doe",
|
||||
"avatar_url": "..."
|
||||
},
|
||||
"created_at": "..."
|
||||
},
|
||||
"context": {
|
||||
"thread_id": 123,
|
||||
"thread_title": "Q3 Analysis",
|
||||
"message_id": 456,
|
||||
"search_space_id": 1,
|
||||
"search_space_name": "Finance Team"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Mention List Response:**
|
||||
```json
|
||||
{
|
||||
"mentions": [ ... ],
|
||||
"unread_count": 3
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permission Model
|
||||
|
||||
### New Permissions
|
||||
|
||||
Add to `Permission` enum in `db.py`:
|
||||
|
||||
| Permission | Value |
|
||||
|------------|-------|
|
||||
| `COMMENTS_CREATE` | `"comments:create"` |
|
||||
| `COMMENTS_READ` | `"comments:read"` |
|
||||
| `COMMENTS_DELETE` | `"comments:delete"` |
|
||||
|
||||
### Default Role Assignments
|
||||
|
||||
| Role | `comments:read` | `comments:create` | `comments:delete` |
|
||||
|------|-----------------|-------------------|-------------------|
|
||||
| Owner | ✅ | ✅ | ✅ |
|
||||
| Admin | ✅ | ✅ | ✅ |
|
||||
| Editor | ✅ | ✅ | ❌ |
|
||||
| Viewer | ✅ | ✅ | ❌ |
|
||||
|
||||
### Authorization Rules
|
||||
|
||||
| Action | Who Can Do It |
|
||||
|--------|---------------|
|
||||
| Read comments | Anyone with `comments:read` |
|
||||
| Create comment | Anyone with `comments:create` |
|
||||
| Create reply | Anyone with `comments:create` |
|
||||
| Edit comment | Author only |
|
||||
| Delete own comment | Author |
|
||||
| Delete any comment | Anyone with `comments:delete` |
|
||||
|
||||
---
|
||||
|
||||
## Business Rules
|
||||
|
||||
### Validation Rules
|
||||
|
||||
1. **Message must be AI response:** `message.role == 'assistant'`
|
||||
2. **Reply parent must be top-level:** `parent.parent_id IS NULL`
|
||||
3. **Content not empty:** 1-5000 characters
|
||||
4. **Mentioned users must be search space members**
|
||||
|
||||
### Computed Fields (Backend Returns)
|
||||
|
||||
| Field | Logic |
|
||||
|-------|-------|
|
||||
| `is_edited` | `updated_at > created_at` |
|
||||
| `can_edit` | `comment.author_id == current_user.id` |
|
||||
| `can_delete` | `author_id == user.id OR has_permission("comments:delete")` |
|
||||
| `content_rendered` | Replace `@[uuid]` with `@Display Name` |
|
||||
| `reply_count` | Count of replies |
|
||||
|
||||
### Mention Processing
|
||||
|
||||
1. On comment create/update, parse `@[uuid]` patterns from content
|
||||
2. Validate each UUID is a member of the search space
|
||||
3. Insert/update records in `chat_comment_mentions`
|
||||
4. Invalid UUIDs: silently ignore (don't create mention record)
|
||||
|
||||
### Error Responses
|
||||
|
||||
| Scenario | HTTP Status | Message |
|
||||
|----------|-------------|---------|
|
||||
| Message not found | 404 | "Message not found" |
|
||||
| Message is not AI response | 400 | "Comments can only be added to AI responses" |
|
||||
| Comment not found | 404 | "Comment not found" |
|
||||
| Cannot reply to reply | 400 | "Cannot reply to a reply" |
|
||||
| Not authorized to edit | 403 | "You can only edit your own comments" |
|
||||
| Not authorized to delete | 403 | "You do not have permission to delete this comment" |
|
||||
| Mention not found | 404 | "Mention not found" |
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
surfsense_backend/app/
|
||||
├── routes/
|
||||
│ └── comments_routes.py # All comment endpoints
|
||||
│
|
||||
├── services/
|
||||
│ └── comments_service.py # Business logic + DB access
|
||||
│
|
||||
├── schemas/
|
||||
│ └── comments.py # Request/response schemas
|
||||
│
|
||||
├── utils/
|
||||
│ └── comments.py # Mention parsing, helpers
|
||||
│
|
||||
├── db.py # Add models (ChatComment, ChatCommentMention)
|
||||
│
|
||||
└── alembic/versions/
|
||||
└── XX_add_comments_tables.py # Migration
|
||||
```
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Foundation (P0)
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| 1.1 | Create `chat_comments` table migration |
|
||||
| 1.2 | Create `chat_comment_mentions` table migration |
|
||||
| 1.3 | Add `ChatComment` model to `db.py` |
|
||||
| 1.4 | Add `ChatCommentMention` model to `db.py` |
|
||||
| 1.5 | Add comment permissions to `Permission` enum |
|
||||
| 1.6 | Update `DEFAULT_ROLE_PERMISSIONS` |
|
||||
|
||||
### Phase 2: Core CRUD (P0)
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| 2.1 | Create Pydantic schemas in `schemas/comments.py` |
|
||||
| 2.2 | Implement `GET /messages/{id}/comments` |
|
||||
| 2.3 | Implement `POST /messages/{id}/comments` |
|
||||
| 2.4 | Implement `POST /comments/{id}/replies` |
|
||||
| 2.5 | Implement `PUT /comments/{id}` |
|
||||
| 2.6 | Implement `DELETE /comments/{id}` |
|
||||
|
||||
### Phase 3: Mentions (P1)
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| 3.1 | Create mention parser in `utils/comments.py` |
|
||||
| 3.2 | Validate mentioned users are search space members |
|
||||
| 3.3 | Insert mentions on comment create/update |
|
||||
| 3.4 | Return `content_rendered` with names resolved |
|
||||
| 3.5 | Implement `GET /users/me/mentions` |
|
||||
| 3.6 | Implement `POST /mentions/{id}/read` |
|
||||
| 3.7 | Implement `POST /users/me/mentions/read-all` |
|
||||
|
||||
### Phase 4: Authorization (P1)
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| 4.1 | Check `comments:read` on list endpoint |
|
||||
| 4.2 | Check `comments:create` on create/reply |
|
||||
| 4.3 | Check author-only on edit |
|
||||
| 4.4 | Check author OR `comments:delete` on delete |
|
||||
| 4.5 | Validate message is AI response |
|
||||
| 4.6 | Validate parent is top-level |
|
||||
|
||||
### Phase 5: Response Enrichment (P1)
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| 5.1 | Return `can_edit` per comment |
|
||||
| 5.2 | Return `can_delete` per comment |
|
||||
| 5.3 | Return `is_edited` |
|
||||
| 5.4 | Return author info (name, avatar, email fallback) |
|
||||
| 5.5 | Return `reply_count` per top-level comment |
|
||||
|
||||
### Phase 6: Edge Cases (P2)
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| 6.1 | Handle deleted user (author_id SET NULL) |
|
||||
| 6.2 | Handle mention of user no longer in search space |
|
||||
| 6.3 | Clear error when replying to deleted comment |
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist (will be done manual)
|
||||
|
||||
### Functional Tests
|
||||
|
||||
- [ ] Create comment on AI response
|
||||
- [ ] Create comment on user message (should fail)
|
||||
- [ ] Reply to comment
|
||||
- [ ] Reply to reply (should fail)
|
||||
- [ ] Edit own comment
|
||||
- [ ] Edit other's comment (should fail)
|
||||
- [ ] Delete own comment (and verify replies deleted)
|
||||
- [ ] Delete other's comment as owner/admin
|
||||
- [ ] Delete other's comment as editor/viewer (should fail)
|
||||
|
||||
### Mention Tests
|
||||
|
||||
- [ ] Mention valid user creates mention record
|
||||
- [ ] Mention invalid UUID is ignored
|
||||
- [ ] Mention non-member is ignored
|
||||
- [ ] `content_rendered` shows display names
|
||||
- [ ] List mentions returns correct data
|
||||
- [ ] Mark mention read updates `read` flag
|
||||
- [ ] Mark all read updates all mentions
|
||||
|
||||
### Edge Case Tests
|
||||
|
||||
- [ ] Comment with 10+ replies scrolls properly (frontend)
|
||||
- [ ] Delete parent comment cascades to replies
|
||||
- [ ] Regenerate AI response deletes comments
|
||||
- [ ] Comment on one-word AI response (frontend min height)
|
||||
- [ ] User A deletes comment while User B replies → clear error
|
||||
|
||||
### Permission Tests
|
||||
|
||||
- [ ] Viewer can read comments
|
||||
- [ ] Viewer can create comments
|
||||
- [ ] Viewer cannot delete other's comments
|
||||
- [ ] Owner can delete any comment
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
The following are explicitly NOT included in v1:
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| Multiple threads per message | Simplicity |
|
||||
| Text selection comments | Complexity |
|
||||
| Rich text (bold/italic/images) | Complexity |
|
||||
| Email/push notifications | Infrastructure |
|
||||
| Emoji reactions | Scope |
|
||||
| Comment on user messages | Focus on AI responses |
|
||||
| Nested replies (> 2 levels) | UX complexity |
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations (Not Now)
|
||||
|
||||
These are documented for future reference but NOT to be implemented now:
|
||||
|
||||
1. **Stale comment detection:** Store `message_content_hash` to detect if AI response changed
|
||||
2. **Real-time sync:** Electric-SQL integration for live updates
|
||||
3. **Notification center:** Dedicated page for all notifications
|
||||
4. **Email digests:** Periodic email summaries of mentions
|
||||
5. **Comment reactions:** Thumbs up, etc.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Existing patterns: `routes/new_chat_routes.py`, `services/connector_service.py`
|
||||
- Permission system: `db.py` (Permission enum, DEFAULT_ROLE_PERMISSIONS)
|
||||
- Schema patterns: `schemas/new_chat.py`
|
||||
|
||||
# Important rules
|
||||
|
||||
- Never do actions in bulk , you will always need my approval before doing something i dod not specifically mention in the prompt
|
||||
- After each item task , we need to commit , for the backend we need to make sure , there are no ruff errors (ruff check, ruff format); for the frontend we need to run pnpm format:fix at web project.
|
||||
- Commits should have minimal message and use --no-verify flag
|
||||
Loading…
Add table
Add a link
Reference in a new issue