harryagasi commited on
Commit
eb3bd3c
·
1 Parent(s): f3f70dc

fix: improve analysis chat rendering, scroll, and traceability persistence

Browse files

- Fix bold markdown not rendering: reorder repairMarkdownMarkers so
space-adding runs before trim, preventing trim from being undone
- Apply same fix to normalizeAssistantContent before saving to DB
- Add auto-scroll to bottom on send, scroll to top of AI answer on
stream complete
- Tighten markdown spacing (my-3→my-1.5, leading-7→leading-6, space-y-2→space-y-0.5)
- Fix traceability disappearing on page refresh by using message_id
returned from API (now provided by backend) instead of hardcoding
doneMessageId from the agentic stream

.claude/skills/speckit-implement/SKILL.md CHANGED
@@ -49,7 +49,7 @@ You **MUST** consider the user input before proceeding (if not empty).
49
  **Automatic Pre-Hook**: {extension}
50
  Executing: `/{command}`
51
  EXECUTE_COMMAND: {command}
52
-
53
  Wait for the result of the hook command before proceeding to the Outline.
54
  ```
55
  - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
@@ -150,7 +150,7 @@ You **MUST** consider the user input before proceeding (if not empty).
150
 
151
  6. Execute implementation following the task plan:
152
  - **Phase-by-phase execution**: Complete each phase before moving to the next
153
- - **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
154
  - **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
155
  - **File-based coordination**: Tasks affecting the same files must run sequentially
156
  - **Validation checkpoints**: Verify each phase completion before proceeding
 
49
  **Automatic Pre-Hook**: {extension}
50
  Executing: `/{command}`
51
  EXECUTE_COMMAND: {command}
52
+
53
  Wait for the result of the hook command before proceeding to the Outline.
54
  ```
55
  - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
 
150
 
151
  6. Execute implementation following the task plan:
152
  - **Phase-by-phase execution**: Complete each phase before moving to the next
153
+ - **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
154
  - **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
155
  - **File-based coordination**: Tasks affecting the same files must run sequentially
156
  - **Validation checkpoints**: Verify each phase completion before proceeding
.claude/skills/speckit-specify/SKILL.md CHANGED
@@ -144,20 +144,20 @@ Given that feature description, do this:
144
 
145
  ```markdown
146
  # Specification Quality Checklist: [FEATURE NAME]
147
-
148
  **Purpose**: Validate specification completeness and quality before proceeding to planning
149
  **Created**: [DATE]
150
  **Feature**: [Link to spec.md]
151
-
152
  ## Content Quality
153
-
154
  - [ ] No implementation details (languages, frameworks, APIs)
155
  - [ ] Focused on user value and business needs
156
  - [ ] Written for non-technical stakeholders
157
  - [ ] All mandatory sections completed
158
-
159
  ## Requirement Completeness
160
-
161
  - [ ] No [NEEDS CLARIFICATION] markers remain
162
  - [ ] Requirements are testable and unambiguous
163
  - [ ] Success criteria are measurable
@@ -166,16 +166,16 @@ Given that feature description, do this:
166
  - [ ] Edge cases are identified
167
  - [ ] Scope is clearly bounded
168
  - [ ] Dependencies and assumptions identified
169
-
170
  ## Feature Readiness
171
-
172
  - [ ] All functional requirements have clear acceptance criteria
173
  - [ ] User scenarios cover primary flows
174
  - [ ] Feature meets measurable outcomes defined in Success Criteria
175
  - [ ] No implementation details leak into specification
176
-
177
  ## Notes
178
-
179
  - Items marked incomplete require spec updates before `/speckit-clarify` or `/speckit-plan`
180
  ```
181
 
@@ -200,20 +200,20 @@ Given that feature description, do this:
200
 
201
  ```markdown
202
  ## Question [N]: [Topic]
203
-
204
  **Context**: [Quote relevant spec section]
205
-
206
  **What we need to know**: [Specific question from NEEDS CLARIFICATION marker]
207
-
208
  **Suggested Answers**:
209
-
210
  | Option | Answer | Implications |
211
  |--------|--------|--------------|
212
  | A | [First suggested answer] | [What this means for the feature] |
213
  | B | [Second suggested answer] | [What this means for the feature] |
214
  | C | [Third suggested answer] | [What this means for the feature] |
215
  | Custom | Provide your own answer | [Explain how to provide custom input] |
216
-
217
  **Your choice**: _[Wait for user response]_
218
  ```
219
 
 
144
 
145
  ```markdown
146
  # Specification Quality Checklist: [FEATURE NAME]
147
+
148
  **Purpose**: Validate specification completeness and quality before proceeding to planning
149
  **Created**: [DATE]
150
  **Feature**: [Link to spec.md]
151
+
152
  ## Content Quality
153
+
154
  - [ ] No implementation details (languages, frameworks, APIs)
155
  - [ ] Focused on user value and business needs
156
  - [ ] Written for non-technical stakeholders
157
  - [ ] All mandatory sections completed
158
+
159
  ## Requirement Completeness
160
+
161
  - [ ] No [NEEDS CLARIFICATION] markers remain
162
  - [ ] Requirements are testable and unambiguous
163
  - [ ] Success criteria are measurable
 
166
  - [ ] Edge cases are identified
167
  - [ ] Scope is clearly bounded
168
  - [ ] Dependencies and assumptions identified
169
+
170
  ## Feature Readiness
171
+
172
  - [ ] All functional requirements have clear acceptance criteria
173
  - [ ] User scenarios cover primary flows
174
  - [ ] Feature meets measurable outcomes defined in Success Criteria
175
  - [ ] No implementation details leak into specification
176
+
177
  ## Notes
178
+
179
  - Items marked incomplete require spec updates before `/speckit-clarify` or `/speckit-plan`
180
  ```
181
 
 
200
 
201
  ```markdown
202
  ## Question [N]: [Topic]
203
+
204
  **Context**: [Quote relevant spec section]
205
+
206
  **What we need to know**: [Specific question from NEEDS CLARIFICATION marker]
207
+
208
  **Suggested Answers**:
209
+
210
  | Option | Answer | Implications |
211
  |--------|--------|--------------|
212
  | A | [First suggested answer] | [What this means for the feature] |
213
  | B | [Second suggested answer] | [What this means for the feature] |
214
  | C | [Third suggested answer] | [What this means for the feature] |
215
  | Custom | Provide your own answer | [Explain how to provide custom input] |
216
+
217
  **Your choice**: _[Wait for user response]_
218
  ```
219
 
.claude/skills/speckit-tasks/SKILL.md CHANGED
@@ -49,7 +49,7 @@ You **MUST** consider the user input before proceeding (if not empty).
49
  **Automatic Pre-Hook**: {extension}
50
  Executing: `/{command}`
51
  EXECUTE_COMMAND: {command}
52
-
53
  Wait for the result of the hook command before proceeding to the Outline.
54
  ```
55
  - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
@@ -151,7 +151,7 @@ Every task MUST strictly follow this format:
151
  4. **[Story] label**: REQUIRED for user story phase tasks only
152
  - Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md)
153
  - Setup phase: NO story label
154
- - Foundational phase: NO story label
155
  - User Story phases: MUST have story label
156
  - Polish phase: NO story label
157
  5. **Description**: Clear action with exact file path
 
49
  **Automatic Pre-Hook**: {extension}
50
  Executing: `/{command}`
51
  EXECUTE_COMMAND: {command}
52
+
53
  Wait for the result of the hook command before proceeding to the Outline.
54
  ```
55
  - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
 
151
  4. **[Story] label**: REQUIRED for user story phase tasks only
152
  - Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md)
153
  - Setup phase: NO story label
154
+ - Foundational phase: NO story label
155
  - User Story phases: MUST have story label
156
  - Polish phase: NO story label
157
  5. **Description**: Clear action with exact file path
.specify/templates/checklist-template.md CHANGED
@@ -6,16 +6,16 @@
6
 
7
  **Note**: This checklist is generated by the `/speckit-checklist` command based on feature context and requirements.
8
 
9
- <!--
10
  ============================================================================
11
  IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
12
-
13
  The /speckit-checklist command MUST replace these with actual items based on:
14
  - User's specific checklist request
15
  - Feature requirements from spec.md
16
  - Technical context from plan.md
17
  - Implementation details from tasks.md
18
-
19
  DO NOT keep these sample items in the generated checklist file.
20
  ============================================================================
21
  -->
 
6
 
7
  **Note**: This checklist is generated by the `/speckit-checklist` command based on feature context and requirements.
8
 
9
+ <!--
10
  ============================================================================
11
  IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
12
+
13
  The /speckit-checklist command MUST replace these with actual items based on:
14
  - User's specific checklist request
15
  - Feature requirements from spec.md
16
  - Technical context from plan.md
17
  - Implementation details from tasks.md
18
+
19
  DO NOT keep these sample items in the generated checklist file.
20
  ============================================================================
21
  -->
docs/API_CONTRACT_BE_GOLANG.md CHANGED
@@ -106,7 +106,7 @@ Flow yang direkomendasikan:
106
  2. Ambil `data.id` dari response sebagai `analysis_id`.
107
  3. Frontend memanggil AI Agent Service terpisah memakai context analysis, business_questions, dan catalog.
108
  4. Saat user mulai bertanya ke AI Agent Service, rekam pertanyaan dengan `role=user` ke endpoint messages.
109
- 5. Setelah AI Agent Service menjawab, simpan jawaban dengan `role=ai` dan `message_id` dari event `done` Python ke endpoint messages.
110
 
111
  ### 5. Conversation Recording
112
 
@@ -128,7 +128,6 @@ Frontend bertanggung jawab melakukan dua write terpisah:
128
  ```json
129
  {
130
  "role": "ai",
131
- "message_id": "msg_88f1",
132
  "content": "Revenue Q3 turun terutama karena penurunan volume transaksi di segmen enterprise..."
133
  }
134
  ```
@@ -141,7 +140,7 @@ Frontend bertanggung jawab melakukan dua write terpisah:
141
  | `POST` | `/api/login` | Login dan issue token pair |
142
  | `POST` | `/api/refresh` | Rotate refresh token dan issue token pair baru |
143
  | `GET` | `/api/v1/documents/doctypes` | List tipe dokumen yang didukung |
144
- | `POST` | `/api/v1/document/upload` | Upload dokumen ke Azure Blob Storage |
145
  | `POST` | `/api/v1/document/upload-local` | Upload dokumen ke local filesystem untuk benchmark |
146
  | `POST` | `/api/v1/document/process` | Proses dokumen async |
147
  | `GET` | `/api/v1/documents/{user_id}` | List dokumen milik user |
@@ -244,6 +243,8 @@ Errors: `400`, `401`, `403`, `500`.
244
 
245
  Status umum: `uploaded`, `processing`, `processed`, `failed`.
246
 
 
 
247
  ### `GET /api/v1/documents/doctypes`
248
 
249
  Mengambil tipe dokumen yang didukung.
@@ -267,7 +268,7 @@ Success `200`:
267
 
268
  ### `POST /api/v1/document/upload`
269
 
270
- Upload dokumen ke Azure Blob Storage. Maksimum 10 MB. Mendukung `pdf`, `docx`, `txt`, `csv`, dan `xlsx`.
271
 
272
  Content-Type: `multipart/form-data`
273
 
@@ -284,7 +285,7 @@ Errors: `400`, `401`, `403`, `429`, `500`.
284
 
285
  ### `POST /api/v1/document/upload-local`
286
 
287
- Upload file ke filesystem lokal untuk benchmarking. Kontrak form sama dengan upload Azure.
288
 
289
  Success `201`:
290
 
@@ -347,7 +348,7 @@ Errors: `401`, `403`, `500`.
347
 
348
  ### `DELETE /api/v1/document/delete`
349
 
350
- Menghapus dokumen dari storage, embedding/parquet terkait, dan record database.
351
 
352
  Request:
353
 
@@ -548,7 +549,7 @@ Success `200`:
548
  "source_id": "document-or-client-id",
549
  "source_type": "tabular",
550
  "name": "sales.csv",
551
- "location_ref": "blob/path/or/db-ref",
552
  "table_count": 1,
553
  "updated_at": "2026-06-30T08:00:00Z"
554
  }
@@ -806,7 +807,6 @@ Errors:
806
  "user_id": "user-id",
807
  "role": "user",
808
  "content": "Apa penyebab revenue turun di Q3?",
809
- "message_id": null,
810
  "created_at": "2026-06-30T08:00:00Z"
811
  }
812
  ```
@@ -816,8 +816,6 @@ Errors:
816
  - `user`: pertanyaan atau instruksi dari user
817
  - `ai`: jawaban dari AI Agent Service
818
 
819
- `message_id` nullable untuk `role=user`, tetapi wajib untuk `role=ai`. Nilainya harus sama dengan `message_id` dari event `done` Python Agentic Service agar message history dapat dikorelasikan dengan observability.
820
-
821
  ### `POST /api/v1/analyses/{id}/messages`
822
 
823
  Merekam tepat satu pesan conversation ke `analyses_messages`.
@@ -831,12 +829,11 @@ Request untuk pertanyaan user:
831
  }
832
  ```
833
 
834
- Request untuk jawaban AI. `message_id` wajib berasal dari event `done` Python Agentic Service, bukan dibuat oleh frontend:
835
 
836
  ```json
837
  {
838
  "role": "ai",
839
- "message_id": "msg_88f1",
840
  "content": "Revenue turun karena penurunan transaksi enterprise dan kenaikan churn di wilayah barat."
841
  }
842
  ```
@@ -849,21 +846,23 @@ Success `201`:
849
  "message": "Message created",
850
  "data": {
851
  "message": {
852
- "id": "message-id",
 
853
  "analysis_id": "analysis-id",
854
  "user_id": "user-id",
855
  "role": "user",
856
  "content": "Apa penyebab revenue turun di Q3?",
857
- "message_id": null,
858
  "created_at": "2026-06-30T08:00:00Z"
859
  }
860
  }
861
  }
862
  ```
863
 
 
 
864
  Errors:
865
 
866
- - `400`: invalid role/content/message_id, invalid analysis ID
867
  - `401`: token invalid/missing
868
  - `409`: inactive analysis atau message limit tercapai
869
 
@@ -884,11 +883,23 @@ Success `200`:
884
  "status": "success",
885
  "message": "Messages retrieved",
886
  "data": {
887
- "messages": []
 
 
 
 
 
 
 
 
 
 
888
  }
889
  }
890
  ```
891
 
 
 
892
  Errors: `400`, `401`, `404`.
893
 
894
  ## Suggested Frontend Integration Sequence
@@ -931,7 +942,7 @@ GET /api/v1/data-catalog/{user_id}
931
  POST /api/v1/analyses with business_questions
932
  call AI Agent Service outside this service
933
  POST /api/v1/analyses/{analysis_id}/messages with role=user and content=user_question
934
- POST /api/v1/analyses/{analysis_id}/messages with role=ai, content=agent_answer, and message_id=python_done_message_id
935
  GET /api/v1/analyses/{analysis_id}/messages
936
  ```
937
 
@@ -939,16 +950,8 @@ GET /api/v1/analyses/{analysis_id}/messages
939
 
940
  - Jangan mengirim pesan user ke `POST /api/v1/analyses/{id}/messages` dengan ekspektasi service ini akan menjawab. Endpoint ini hanya persistence.
941
  - AI Agent Service adalah service terpisah. Service ini menyimpan metadata analysis, knowledge catalog, data binding, dan history conversation.
942
- - Untuk pesan `role=ai`, simpan `message_id` yang dikembalikan Python pada event `done` agar history conversation dapat dikorelasikan dengan observability Python.
943
  - User-level catalog berisi seluruh knowledge source user; analysis-level catalog hanya berisi source yang ada di `data_bind` analysis.
944
  - Perubahan `data_bind` akan rebuild analysis-level catalog. Frontend juga dapat memanggil endpoint rebuild analysis catalog secara eksplisit jika diperlukan.
945
  - Setelah refresh token sukses, selalu replace refresh token lama dengan refresh token baru.
946
  - Untuk endpoint yang membutuhkan `user_id`, gunakan `data.user.id` dari login dan pastikan sama dengan token aktif.
947
- - Untuk binding analysis, pakai source yang sudah berhasil diupload/diproses atau database client yang sudah diingest.
948
-
949
-
950
-
951
-
952
-
953
-
954
-
 
106
  2. Ambil `data.id` dari response sebagai `analysis_id`.
107
  3. Frontend memanggil AI Agent Service terpisah memakai context analysis, business_questions, dan catalog.
108
  4. Saat user mulai bertanya ke AI Agent Service, rekam pertanyaan dengan `role=user` ke endpoint messages.
109
+ 5. Setelah AI Agent Service menjawab, simpan jawaban dengan `role=ai` ke endpoint messages.
110
 
111
  ### 5. Conversation Recording
112
 
 
128
  ```json
129
  {
130
  "role": "ai",
 
131
  "content": "Revenue Q3 turun terutama karena penurunan volume transaksi di segmen enterprise..."
132
  }
133
  ```
 
140
  | `POST` | `/api/login` | Login dan issue token pair |
141
  | `POST` | `/api/refresh` | Rotate refresh token dan issue token pair baru |
142
  | `GET` | `/api/v1/documents/doctypes` | List tipe dokumen yang didukung |
143
+ | `POST` | `/api/v1/document/upload` | Upload dokumen ke private object storage |
144
  | `POST` | `/api/v1/document/upload-local` | Upload dokumen ke local filesystem untuk benchmark |
145
  | `POST` | `/api/v1/document/process` | Proses dokumen async |
146
  | `GET` | `/api/v1/documents/{user_id}` | List dokumen milik user |
 
243
 
244
  Status umum: `uploaded`, `processing`, `processed`, `failed`.
245
 
246
+ `blob_name` dipertahankan untuk kompatibilitas response lama, tetapi secara kontrak frontend harus dipahami sebagai stored object reference di private object storage, bukan Azure-specific blob URL.
247
+
248
  ### `GET /api/v1/documents/doctypes`
249
 
250
  Mengambil tipe dokumen yang didukung.
 
268
 
269
  ### `POST /api/v1/document/upload`
270
 
271
+ Upload dokumen ke private object storage. Maksimum 10 MB. Mendukung `pdf`, `docx`, `txt`, `csv`, dan `xlsx`.
272
 
273
  Content-Type: `multipart/form-data`
274
 
 
285
 
286
  ### `POST /api/v1/document/upload-local`
287
 
288
+ Upload file ke filesystem lokal untuk benchmarking. Kontrak form sama dengan upload object storage.
289
 
290
  Success `201`:
291
 
 
348
 
349
  ### `DELETE /api/v1/document/delete`
350
 
351
+ Menghapus dokumen dari private object storage, embedding/parquet terkait, dan record database.
352
 
353
  Request:
354
 
 
549
  "source_id": "document-or-client-id",
550
  "source_type": "tabular",
551
  "name": "sales.csv",
552
+ "location_ref": "object-storage/path/or/db-ref",
553
  "table_count": 1,
554
  "updated_at": "2026-06-30T08:00:00Z"
555
  }
 
807
  "user_id": "user-id",
808
  "role": "user",
809
  "content": "Apa penyebab revenue turun di Q3?",
 
810
  "created_at": "2026-06-30T08:00:00Z"
811
  }
812
  ```
 
816
  - `user`: pertanyaan atau instruksi dari user
817
  - `ai`: jawaban dari AI Agent Service
818
 
 
 
819
  ### `POST /api/v1/analyses/{id}/messages`
820
 
821
  Merekam tepat satu pesan conversation ke `analyses_messages`.
 
829
  }
830
  ```
831
 
832
+ Request untuk jawaban AI:
833
 
834
  ```json
835
  {
836
  "role": "ai",
 
837
  "content": "Revenue turun karena penurunan transaksi enterprise dan kenaikan churn di wilayah barat."
838
  }
839
  ```
 
846
  "message": "Message created",
847
  "data": {
848
  "message": {
849
+ "id": "uuid-dari-db",
850
+ "message_id": "uuid-dari-db",
851
  "analysis_id": "analysis-id",
852
  "user_id": "user-id",
853
  "role": "user",
854
  "content": "Apa penyebab revenue turun di Q3?",
 
855
  "created_at": "2026-06-30T08:00:00Z"
856
  }
857
  }
858
  }
859
  ```
860
 
861
+ > `message_id` bernilai sama dengan `id` — disertakan agar frontend dapat langsung menggunakannya untuk memanggil endpoint observability (`GET /api/v1/observability?analysis_id=...&message_id=...`) tanpa mapping tambahan.
862
+
863
  Errors:
864
 
865
+ - `400`: invalid role/content, invalid analysis ID
866
  - `401`: token invalid/missing
867
  - `409`: inactive analysis atau message limit tercapai
868
 
 
883
  "status": "success",
884
  "message": "Messages retrieved",
885
  "data": {
886
+ "messages": [
887
+ {
888
+ "id": "uuid-dari-db",
889
+ "message_id": "uuid-dari-db",
890
+ "analysis_id": "analysis-id",
891
+ "user_id": "user-id",
892
+ "role": "ai",
893
+ "content": "Revenue turun karena penurunan transaksi enterprise.",
894
+ "created_at": "2026-06-30T08:01:00Z"
895
+ }
896
+ ]
897
  }
898
  }
899
  ```
900
 
901
+ > Setiap item menyertakan `message_id` (sama dengan `id`) untuk digunakan frontend pada pemanggilan `GET /api/v1/observability?analysis_id=...&message_id=...` setelah page refresh.
902
+
903
  Errors: `400`, `401`, `404`.
904
 
905
  ## Suggested Frontend Integration Sequence
 
942
  POST /api/v1/analyses with business_questions
943
  call AI Agent Service outside this service
944
  POST /api/v1/analyses/{analysis_id}/messages with role=user and content=user_question
945
+ POST /api/v1/analyses/{analysis_id}/messages with role=ai and content=agent_answer
946
  GET /api/v1/analyses/{analysis_id}/messages
947
  ```
948
 
 
950
 
951
  - Jangan mengirim pesan user ke `POST /api/v1/analyses/{id}/messages` dengan ekspektasi service ini akan menjawab. Endpoint ini hanya persistence.
952
  - AI Agent Service adalah service terpisah. Service ini menyimpan metadata analysis, knowledge catalog, data binding, dan history conversation.
 
953
  - User-level catalog berisi seluruh knowledge source user; analysis-level catalog hanya berisi source yang ada di `data_bind` analysis.
954
  - Perubahan `data_bind` akan rebuild analysis-level catalog. Frontend juga dapat memanggil endpoint rebuild analysis catalog secara eksplisit jika diperlukan.
955
  - Setelah refresh token sukses, selalu replace refresh token lama dengan refresh token baru.
956
  - Untuk endpoint yang membutuhkan `user_id`, gunakan `data.user.id` dari login dan pastikan sama dengan token aktif.
957
+ - Untuk binding analysis, pakai source yang sudah berhasil diupload/diproses atau database client yang sudah diingest.
 
 
 
 
 
 
 
docs/API_CONTRACT_BE_PYTHON.md CHANGED
@@ -98,7 +98,7 @@ event: status
98
  data: Running 3 steps...
99
 
100
  event: chunk
101
- data: Total sales by region last quarter:
102
 
103
  event: chunk
104
  data: Central led at $1.21M (38%), East $0.74M, West $0.55M (down 12% QoQ).
 
98
  data: Running 3 steps...
99
 
100
  event: chunk
101
+ data: Total sales by region last quarter:
102
 
103
  event: chunk
104
  data: Central led at $1.21M (38%), East $0.74M, West $0.55M (down 12% QoQ).
specs/001-unified-analysis-chat/contracts/ENDPOINTS.md CHANGED
@@ -1,8 +1,8 @@
1
- > Superseded planning note: For the 2026-06-30 dual-backend UX plan, use `frontend-dual-backend-contract.md` as the authoritative frontend contract. This file is retained as historical context from the older endpoint analysis.
2
  # Backend API Endpoints Required
3
 
4
- **Feature**: Unified Analysis Chat with Skills
5
- **Date**: 2026-06-15
6
  **Status**: Required for Implementation
7
 
8
  ## Overview
@@ -779,7 +779,7 @@ Based on Success Criteria:
779
 
780
  2. **Message Order**: Ensure messages are returned in chronological order (timestamp ASC) for proper conversation flow.
781
 
782
- 3. **Validation**:
783
  - Validate title length (1-200 chars)
784
  - Validate problem statement length (1-2000 chars)
785
  - Prevent removing all data sources from an analysis
 
1
+ > Superseded planning note: For the 2026-06-30 dual-backend UX plan, use `frontend-dual-backend-contract.md` as the authoritative frontend contract. This file is retained as historical context from the older endpoint analysis.
2
  # Backend API Endpoints Required
3
 
4
+ **Feature**: Unified Analysis Chat with Skills
5
+ **Date**: 2026-06-15
6
  **Status**: Required for Implementation
7
 
8
  ## Overview
 
779
 
780
  2. **Message Order**: Ensure messages are returned in chronological order (timestamp ASC) for proper conversation flow.
781
 
782
+ 3. **Validation**:
783
  - Validate title length (1-200 chars)
784
  - Validate problem statement length (1-2000 chars)
785
  - Prevent removing all data sources from an analysis
specs/001-unified-analysis-chat/contracts/ENDPOINTS_BY_TASK.md CHANGED
@@ -1,4 +1,4 @@
1
- > Superseded planning note: For the 2026-06-30 dual-backend UX plan, use `frontend-dual-backend-contract.md` as the authoritative frontend contract. This file is retained as historical context from the older endpoint analysis.
2
  # Backend Endpoints Needed - Organized by Task
3
 
4
  This document maps frontend implementation tasks to the backend endpoints they depend on.
@@ -273,19 +273,19 @@ updateAnalysis(analysisId, { dataSources: [...] }); // Already needed
273
  ## Summary: All Required Endpoints
274
 
275
  ### MUST HAVE (MVP):
276
- 1. ✅ POST `/api/analyses` - Create analysis
277
- 2. ✅ GET `/api/analyses` - List analyses
278
- 3. ✅ GET `/api/analyses/:id` - Get analysis with messages
279
- 4. ✅ PUT `/api/analyses/:id` - Update analysis
280
- 5. ✅ DELETE `/api/analyses/:id` - Delete analysis
281
- 6. ✅ POST `/api/analyses/:id/messages` - Add message/invoke skill
282
- 7. ✅ GET `/api/analyses/:id/messages` - Get messages (paginated)
283
- 8. ✅ GET `/api/skills` - Get available skills with status
284
- 9. ✅ GET `/api/datasources` - Get available data sources
285
 
286
  ### NICE TO HAVE (Optional):
287
- 10. ⚠️ GET `/api/skills/:skillName` - Get single skill details
288
- 11. ⚠️ WebSocket `/ws/analyses/:id` - Real-time skill status updates
289
 
290
  ---
291
 
@@ -297,7 +297,7 @@ updateAnalysis(analysisId, { dataSources: [...] }); // Already needed
297
  3. **READ**: GET `/api/analyses` with pagination
298
  4. **READ**: GET `/api/analyses/:id` with messages included
299
  5. **UPDATE**: PUT `/api/analyses/:id` with validation (min 1 source)
300
- 6. **DELETE**: DELETE `/api/analyses/:id`
301
  7. **READ**: GET `/api/skills` with status computation
302
  8. **READ**: GET `/api/datasources`
303
 
@@ -315,16 +315,16 @@ updateAnalysis(analysisId, { dataSources: [...] }); // Already needed
315
 
316
  ## Backend Testing Checklist
317
 
318
- - [ ] POST `/api/analyses` with valid data → returns 201 with full object
319
- - [ ] POST `/api/analyses` without data sources → returns 400 "MIN_SOURCES_REQUIRED"
320
- - [ ] GET `/api/analyses` → returns user's analyses only
321
- - [ ] GET `/api/analyses/:id` for other user's analysis → returns 403 Forbidden
322
- - [ ] PUT `/api/analyses/:id` removing all sources → returns 400 "Cannot remove all sources"
323
- - [ ] GET `/api/skills?analysisId=X` → returns status based on analysis state
324
- - [ ] POST `/api/analyses/:id/messages` with unmet precondition → returns 400 "PRECONDITION_UNMET"
325
- - [ ] POST `/api/analyses/:id/messages` with valid skill → returns 201 with message + skillResponse
326
- - [ ] GET `/api/datasources` → returns documents/databases user has access to
327
- - [ ] All endpoints without auth → return 401 Unauthorized
328
  - [ ] Pagination works (limit, offset, hasMore)
329
  - [ ] Error responses include error code, message, timestamp
330
 
 
1
+ > Superseded planning note: For the 2026-06-30 dual-backend UX plan, use `frontend-dual-backend-contract.md` as the authoritative frontend contract. This file is retained as historical context from the older endpoint analysis.
2
  # Backend Endpoints Needed - Organized by Task
3
 
4
  This document maps frontend implementation tasks to the backend endpoints they depend on.
 
273
  ## Summary: All Required Endpoints
274
 
275
  ### MUST HAVE (MVP):
276
+ 1. [OK] POST `/api/analyses` - Create analysis
277
+ 2. [OK] GET `/api/analyses` - List analyses
278
+ 3. [OK] GET `/api/analyses/:id` - Get analysis with messages
279
+ 4. [OK] PUT `/api/analyses/:id` - Update analysis
280
+ 5. [OK] DELETE `/api/analyses/:id` - Delete analysis
281
+ 6. [OK] POST `/api/analyses/:id/messages` - Add message/invoke skill
282
+ 7. [OK] GET `/api/analyses/:id/messages` - Get messages (paginated)
283
+ 8. [OK] GET `/api/skills` - Get available skills with status
284
+ 9. [OK] GET `/api/datasources` - Get available data sources
285
 
286
  ### NICE TO HAVE (Optional):
287
+ 10. [WARN] GET `/api/skills/:skillName` - Get single skill details
288
+ 11. [WARN] WebSocket `/ws/analyses/:id` - Real-time skill status updates
289
 
290
  ---
291
 
 
297
  3. **READ**: GET `/api/analyses` with pagination
298
  4. **READ**: GET `/api/analyses/:id` with messages included
299
  5. **UPDATE**: PUT `/api/analyses/:id` with validation (min 1 source)
300
+ 6. **DELETE**: DELETE `/api/analyses/:id`
301
  7. **READ**: GET `/api/skills` with status computation
302
  8. **READ**: GET `/api/datasources`
303
 
 
315
 
316
  ## Backend Testing Checklist
317
 
318
+ - [ ] POST `/api/analyses` with valid data -> returns 201 with full object
319
+ - [ ] POST `/api/analyses` without data sources -> returns 400 "MIN_SOURCES_REQUIRED"
320
+ - [ ] GET `/api/analyses` -> returns user's analyses only
321
+ - [ ] GET `/api/analyses/:id` for other user's analysis -> returns 403 Forbidden
322
+ - [ ] PUT `/api/analyses/:id` removing all sources -> returns 400 "Cannot remove all sources"
323
+ - [ ] GET `/api/skills?analysisId=X` -> returns status based on analysis state
324
+ - [ ] POST `/api/analyses/:id/messages` with unmet precondition -> returns 400 "PRECONDITION_UNMET"
325
+ - [ ] POST `/api/analyses/:id/messages` with valid skill -> returns 201 with message + skillResponse
326
+ - [ ] GET `/api/datasources` -> returns documents/databases user has access to
327
+ - [ ] All endpoints without auth -> return 401 Unauthorized
328
  - [ ] Pagination works (limit, offset, hasMore)
329
  - [ ] Error responses include error code, message, timestamp
330
 
specs/001-unified-analysis-chat/contracts/ENDPOINTS_SUMMARY.md CHANGED
@@ -1,7 +1,7 @@
1
- > Superseded planning note: For the 2026-06-30 dual-backend UX plan, use `frontend-dual-backend-contract.md` as the authoritative frontend contract. This file is retained as historical context from the older endpoint analysis.
2
  # Backend API Endpoints - Quick Reference
3
 
4
- ## 📋 Endpoint Summary
5
 
6
  | Category | Endpoint | Method | Purpose |
7
  |----------|----------|--------|---------|
@@ -18,7 +18,7 @@
18
 
19
  ---
20
 
21
- ## 🎯 Frontend Dependency by Task
22
 
23
  ### Phase 2 Foundational Tasks Needed:
24
  - **T010**: `analysisService.ts` needs endpoints: POST/GET/PUT/DELETE `/api/analyses`, GET `/api/analyses/:id/messages`
@@ -35,7 +35,7 @@
35
 
36
  ---
37
 
38
- ## 🚀 Critical Endpoints for MVP (User Story 1 Only)
39
 
40
  If delivering MVP with just **US1 (Unified Interface)**:
41
 
@@ -48,7 +48,7 @@ These 4 endpoints are the **absolute minimum** to display a working unified chat
48
 
49
  ---
50
 
51
- ## 🔑 Key Features of Each Endpoint
52
 
53
  ### Skill Status Management
54
  ```
@@ -61,7 +61,7 @@ Response includes:
61
 
62
  **Frontend responsibility**: Display which skills are available and why others are disabled
63
 
64
- **Backend responsibility**:
65
  - Compute `status` based on analysis state and skill preconditions
66
  - Examples:
67
  - `/analyze-*` skills are `unavailable` until `/problem-statement` is called
@@ -80,7 +80,7 @@ PUT /api/analyses/:id
80
  Error if: Attempting to remove all sources
81
  ```
82
 
83
- **Frontend responsibility**:
84
  - Validate form before submission (min 1 source selected)
85
  - Show error if trying to remove last source
86
 
@@ -93,9 +93,9 @@ PUT /api/analyses/:id
93
  ### Skill Invocation with Preconditions
94
  ```
95
  POST /api/analyses/:id/messages
96
- Request: {
97
  content: "/analyze-descriptive",
98
- skillName: "analyze-descriptive"
99
  }
100
 
101
  Response if precondition unmet:
@@ -108,7 +108,7 @@ Response if precondition unmet:
108
  ```
109
 
110
  **Frontend responsibility**:
111
- - Display error message in chat: "❌ /analyze-descriptive requires /problem-statement to be completed first"
112
  - Don't gray out skills (backend handles status via GET /api/skills)
113
 
114
  **Backend responsibility**:
@@ -117,7 +117,7 @@ Response if precondition unmet:
117
 
118
  ---
119
 
120
- ## 📊 Message Structure
121
 
122
  Every message in the system has this structure:
123
 
@@ -142,7 +142,7 @@ Every message in the system has this structure:
142
 
143
  ---
144
 
145
- ## âš¡ Performance Targets
146
 
147
  Per Success Criteria from spec.md:
148
 
@@ -156,7 +156,7 @@ Per Success Criteria from spec.md:
156
 
157
  ---
158
 
159
- ## 🔐 Authentication & Authorization
160
 
161
  - **All endpoints** require authenticated user (JWT token / session)
162
  - **User isolation**: Only return/allow access to user's own analyses
@@ -164,7 +164,7 @@ Per Success Criteria from spec.md:
164
 
165
  ---
166
 
167
- ## 📝 Error Response Format
168
 
169
  All errors follow this format:
170
 
@@ -188,36 +188,36 @@ All errors follow this format:
188
 
189
  ---
190
 
191
- ## 🔄 Data Flow Example: Skill Invocation
192
 
193
  ```
194
  User types: "/analyze-descriptive" in chat
195
- ↓
196
  Frontend: POST /api/analyses/:id/messages
197
  {
198
  content: "/analyze-descriptive",
199
  skillName: "analyze-descriptive"
200
  }
201
- ↓
202
  Backend checks:
203
- 1. Is user authenticated? ✓
204
- 2. Does user own analysis? ✓
205
- 3. Are preconditions met?
206
- - Is /problem-statement called? ✓
207
- - Any other preconditions? ✓
208
  4. Execute skill with analysis context
209
  5. Return result
210
- ↓
211
  Backend response (201 Created):
212
  {
213
- "message": {
214
  user message with skill metadata
215
  },
216
  "skillResponse": {
217
  system message with skill output
218
  }
219
  }
220
- ↓
221
  Frontend displays:
222
  - User message: "/analyze-descriptive" (styled with skill icon)
223
  - System message: "[skill output]" (styled as system response)
@@ -225,7 +225,7 @@ Frontend displays:
225
 
226
  ---
227
 
228
- ## 📋 Checklist for Backend Implementation
229
 
230
  - [ ] Create Analysis table/collection with fields: id, title, problemStatement, dataSources, createdAt, updatedAt, ownerId
231
  - [ ] Create Message table/collection with fields: id, analysisId, content, author, timestamp, isSkillInvocation, skillName, skillStatus
@@ -242,7 +242,7 @@ Frontend displays:
242
 
243
  ---
244
 
245
- ## 📞 Questions for Backend Team
246
 
247
  1. **Skill Execution**: How long do skills take to execute? Should we make skill invocation async with polling/websocket for updates?
248
 
 
1
+ > Superseded planning note: For the 2026-06-30 dual-backend UX plan, use `frontend-dual-backend-contract.md` as the authoritative frontend contract. This file is retained as historical context from the older endpoint analysis.
2
  # Backend API Endpoints - Quick Reference
3
 
4
+ ## Endpoint Summary
5
 
6
  | Category | Endpoint | Method | Purpose |
7
  |----------|----------|--------|---------|
 
18
 
19
  ---
20
 
21
+ ## Frontend Dependency by Task
22
 
23
  ### Phase 2 Foundational Tasks Needed:
24
  - **T010**: `analysisService.ts` needs endpoints: POST/GET/PUT/DELETE `/api/analyses`, GET `/api/analyses/:id/messages`
 
35
 
36
  ---
37
 
38
+ ## Critical Endpoints for MVP (User Story 1 Only)
39
 
40
  If delivering MVP with just **US1 (Unified Interface)**:
41
 
 
48
 
49
  ---
50
 
51
+ ## Key Features of Each Endpoint
52
 
53
  ### Skill Status Management
54
  ```
 
61
 
62
  **Frontend responsibility**: Display which skills are available and why others are disabled
63
 
64
+ **Backend responsibility**:
65
  - Compute `status` based on analysis state and skill preconditions
66
  - Examples:
67
  - `/analyze-*` skills are `unavailable` until `/problem-statement` is called
 
80
  Error if: Attempting to remove all sources
81
  ```
82
 
83
+ **Frontend responsibility**:
84
  - Validate form before submission (min 1 source selected)
85
  - Show error if trying to remove last source
86
 
 
93
  ### Skill Invocation with Preconditions
94
  ```
95
  POST /api/analyses/:id/messages
96
+ Request: {
97
  content: "/analyze-descriptive",
98
+ skillName: "analyze-descriptive"
99
  }
100
 
101
  Response if precondition unmet:
 
108
  ```
109
 
110
  **Frontend responsibility**:
111
+ - Display error message in chat: "[NO] /analyze-descriptive requires /problem-statement to be completed first"
112
  - Don't gray out skills (backend handles status via GET /api/skills)
113
 
114
  **Backend responsibility**:
 
117
 
118
  ---
119
 
120
+ ## Message Structure
121
 
122
  Every message in the system has this structure:
123
 
 
142
 
143
  ---
144
 
145
+ ## Performance Performance Targets
146
 
147
  Per Success Criteria from spec.md:
148
 
 
156
 
157
  ---
158
 
159
+ ## Authentication & Authorization
160
 
161
  - **All endpoints** require authenticated user (JWT token / session)
162
  - **User isolation**: Only return/allow access to user's own analyses
 
164
 
165
  ---
166
 
167
+ ## Error Response Format
168
 
169
  All errors follow this format:
170
 
 
188
 
189
  ---
190
 
191
+ ## Refresh Data Flow Example: Skill Invocation
192
 
193
  ```
194
  User types: "/analyze-descriptive" in chat
195
+ v
196
  Frontend: POST /api/analyses/:id/messages
197
  {
198
  content: "/analyze-descriptive",
199
  skillName: "analyze-descriptive"
200
  }
201
+ v
202
  Backend checks:
203
+ 1. Is user authenticated? [OK]
204
+ 2. Does user own analysis? [OK]
205
+ 3. Are preconditions met?
206
+ - Is /problem-statement called? [OK]
207
+ - Any other preconditions? [OK]
208
  4. Execute skill with analysis context
209
  5. Return result
210
+ v
211
  Backend response (201 Created):
212
  {
213
+ "message": {
214
  user message with skill metadata
215
  },
216
  "skillResponse": {
217
  system message with skill output
218
  }
219
  }
220
+ v
221
  Frontend displays:
222
  - User message: "/analyze-descriptive" (styled with skill icon)
223
  - System message: "[skill output]" (styled as system response)
 
225
 
226
  ---
227
 
228
+ ## Checklist for Backend Implementation
229
 
230
  - [ ] Create Analysis table/collection with fields: id, title, problemStatement, dataSources, createdAt, updatedAt, ownerId
231
  - [ ] Create Message table/collection with fields: id, analysisId, content, author, timestamp, isSkillInvocation, skillName, skillStatus
 
242
 
243
  ---
244
 
245
+ ## Questions for Backend Team
246
 
247
  1. **Skill Execution**: How long do skills take to execute? Should we make skill invocation async with polling/websocket for updates?
248
 
specs/001-unified-analysis-chat/contracts/ENDPOINT_COMPARISON.md CHANGED
@@ -1,118 +1,118 @@
1
- > Superseded planning note: For the 2026-06-30 dual-backend UX plan, use `frontend-dual-backend-contract.md` as the authoritative frontend contract. This file is retained as historical context from the older endpoint analysis.
2
  # Endpoint Comparison: Existing vs Needed
3
 
4
  ---
5
 
6
- ## 📊 Visual Comparison
7
 
8
  ### EXISTING STRUCTURE (Current State)
9
 
10
  ```
11
  Frontend Pages:
12
- ├── Analytics Page (Chat Rooms)
13
- │ ├── GET /api/v1/chat-rooms (List rooms)
14
- │ ├── POST /api/v1/chat-rooms (Create room)
15
- │ ├── GET /api/v1/chat-rooms/:roomId (Get room + messages)
16
- │ ├── DELETE /api/v1/chat-rooms/:roomId (Delete room)
17
- │ └── POST /api/v1/chat/stream (Send message, get streaming response)
18
- │
19
- ├── Knowledge Management Page (Documents + Databases)
20
- │ ├── GET /api/v1/documents/:userId (List documents)
21
- │ ├── POST /api/v1/document/upload (Upload doc)
22
- │ ├── POST /api/v1/document/process (Process doc)
23
- │ ├── DELETE /api/v1/document/delete (Delete doc)
24
- │ ├── GET /api/v1/database-clients/dbtypes
25
- │ ├── POST /api/v1/database-clients (Connect DB)
26
- │ ├── GET /api/v1/database-clients/:userId
27
- │ ├── DELETE /api/v1/database-clients/:id
28
- │ └── POST /api/v1/database-clients/:id/ingest
29
- │
30
- └── Interview Page (Separate)
31
- ├── GET /frameworks
32
- ├── POST /api/v1/interviews/sessions (Create session)
33
- ├── POST /api/v1/interviews/sessions/:id/message
34
- ├── POST /api/v1/interviews/sessions/:id/finish
35
- ├── GET /api/v1/interviews/:roomId/result
36
- └── POST /api/v1/interviews/sessions/:id/stream-message
37
  ```
38
 
39
  ### NEW STRUCTURE (After Unified Analysis Chat)
40
 
41
  ```
42
  Frontend:
43
- ├── Unified Analysis Chat Page
44
- │ ├── POST /api/analyses (Create analysis) [NEW or EXTEND chat-rooms]
45
- │ ├── GET /api/analyses (List analyses) [NEW or EXTEND]
46
- │ ├── GET /api/analyses/:id (Get analysis + messages) [NEW or EXTEND]
47
- │ ├── PUT /api/analyses/:id (Update analysis) [NEW - untuk update datasources]
48
- │ ├── DELETE /api/analyses/:id (Delete analysis) [NEW or EXTEND]
49
- │ │
50
- │ ├── POST /api/analyses/:id/messages (Add message/invoke skill) [NEW - extend chat/stream]
51
- │ ├── GET /api/analyses/:id/messages (List messages paginated) [NEW]
52
- │ │
53
- │ ├── GET /api/skills?analysisId=X (Get available skills) [NEW - skill management]
54
- │ ├── GET /api/skills/:skillName (Get skill detail) [NEW - optional]
55
- │ │
56
- │ └── GET /api/datasources (Get documents + databases) [NEW - aggregate]
57
- │
58
- ├── Knowledge Management (TETAP ADA)
59
- │ └── Same as before
60
- │
61
- └── Interview (TETAP ADA atau DIGABUNG)
62
- └── Same as before or merged
63
  ```
64
 
65
  ---
66
 
67
- ## 🔄 Endpoint Reuse Strategy
68
 
69
  ### Which Endpoints Can Be Reused/Extended?
70
 
71
- #### ✅ REUSABLE (Logic mirip):
72
 
73
  ```
74
- Chat Rooms → Analysis CRUD
75
- ├── GET /api/v1/chat-rooms → GET /api/analyses
76
- │ Same: List user's conversations
77
- │ Diff: Response includes dataSources + problem_statement
78
- │
79
- ├── POST /api/v1/chat-rooms → POST /api/analyses
80
- │ Same: Create new conversation
81
- │ Diff: Require dataSources, problem_statement; no longer optional title
82
- │
83
- ├── GET /api/v1/chat-rooms/:id → GET /api/analyses/:id
84
- │ Same: Get full conversation with messages
85
- │ Diff: Include dataSources, problem_statement, skill metadata in messages
86
- │
87
- └── DELETE /api/v1/chat-rooms/:id → DELETE /api/analyses/:id
88
  Same: Delete conversation
89
  Diff: Same logic
90
  ```
91
 
92
  ```
93
- Chat Message → Analysis Message + Skill Metadata
94
- ├── POST /api/v1/chat/stream → POST /api/analyses/:id/messages
95
- │ Same: Send message to conversation, get response
96
- │ Diff: Add skillName, skillParams; add skill invocation metadata
97
- │ Response includes message + optional skillResponse
98
- │
99
- └── NEW REQUIREMENT: GET /api/analyses/:id/messages
100
  Needed for: Paginated message history when switching analyses
101
  Similar to: GET /api/v1/chat-rooms/:id (but paginated messages)
102
  ```
103
 
104
  ```
105
- Knowledge Base → Data Sources
106
- ├── GET /api/v1/documents → GET /api/datasources (documents type)
107
- │ Same: List available data sources
108
- │ Diff: Filter by type, more metadata
109
- │
110
- └── GET /api/v1/database-clients → GET /api/datasources (database type)
111
  Same: List available data sources
112
  Diff: Aggregate with documents, unified response
113
  ```
114
 
115
- #### ❌ TIDAK BISA REUSE (Logic baru):
116
 
117
  ```
118
  NEW: PUT /api/analyses/:id
@@ -133,12 +133,12 @@ NEW: POST /api/analyses/:id/messages with skill precondition validation
133
 
134
  ---
135
 
136
- ## 📈 Data Structure Comparison
137
 
138
  ### Current: Room vs New: Analysis
139
 
140
  ```typescript
141
- // ❌ EXISTING: Room (from api.ts)
142
  interface Room {
143
  id: string;
144
  title: string;
@@ -156,7 +156,7 @@ interface RoomMessage {
156
  sources?: ChatSource[]; // References to documents used
157
  }
158
 
159
- // ✅ NEW: Analysis (needed for new feature)
160
  interface Analysis {
161
  id: string;
162
  title: string;
@@ -194,7 +194,7 @@ interface DataSource {
194
 
195
  ---
196
 
197
- ## 🔌 Integration Points
198
 
199
  ### How Frontend Will Call New Endpoints
200
 
@@ -240,8 +240,8 @@ const analyses = await api.getAnalyses(limit=50); // NEW
240
 
241
  // On sidebar item click:
242
  const messages = await api.getAnalysisMessages( // NEW
243
- analysisId,
244
- limit=50,
245
  offset=0
246
  );
247
  // Load paginated message history
@@ -249,29 +249,29 @@ const messages = await api.getAnalysisMessages( // NEW
249
 
250
  ---
251
 
252
- ## 📋 Implementation Checklist for Backend
253
 
254
  ### Phase 1: Extend Existing Endpoints (2 days)
255
 
256
  ```
257
- ☐ Extend Room table schema:
258
  - Add: problem_statement TEXT
259
  - Add: data_sources JSONB
260
-
261
- ☐ Extend POST /api/v1/chat-rooms:
262
  - Add: problem_statement, dataSources to request
263
  - Validate: dataSources.length >= 1
264
  - Test: Create analysis with data sources
265
-
266
- ☐ Extend GET /api/v1/chat-rooms:
267
  - Include: problem_statement, dataSources in response
268
  - Test: Verify data sources in response
269
-
270
- ☐ Extend GET /api/v1/chat-rooms/:id:
271
  - Include: problem_statement, dataSources
272
  - Test: Verify full analysis structure
273
-
274
- ☐ Extend PUT /api/v1/chat-rooms/:id → PUT /api/analyses/:id:
275
  - New endpoint: Update analysis
276
  - Allow update: title, problem_statement, data_sources
277
  - Validate: dataSources.length >= 1
@@ -281,37 +281,37 @@ const messages = await api.getAnalysisMessages( // NEW
281
  ### Phase 2: New Endpoints (2 days)
282
 
283
  ```
284
- ☐ Implement GET /api/datasources:
285
  - Aggregate: documents + database clients
286
  - Format: { id, name, type: "document" | "database" }
287
  - Test: Get datasources, verify both types included
288
-
289
- ☐ Implement GET /api/skills:
290
  - Query param: analysisId (required)
291
  - Return: Array of 10 skills with status
292
- - Status logic:
293
- - /analyze-* skills → unavailable until /problem-statement called
294
- - /help, /check-*, /retrieve-* → always available
295
  - Test: Verify status changes based on analysis messages
296
-
297
- ☐ Extend RoomMessage schema:
298
  - Add: is_skill_invocation BOOLEAN
299
  - Add: skill_name VARCHAR(255)
300
  - Add: skill_status VARCHAR(50)
301
-
302
- ☐ Extend POST /api/v1/chat/stream → POST /api/analyses/:id/messages:
303
  - New endpoint or extend existing
304
  - Input: { content, skillName?, skillParams? }
305
  - Validate: Check skill preconditions
306
  - Return error if precondition unmet:
307
  { error: "PRECONDITION_UNMET", precondition: "problem-statement" }
308
- - Execute skill → return both user message + skill response
309
- - Test:
310
- - Send regular message → works
311
- - Send skill with met preconditions → works
312
- - Send skill with unmet preconditions → error 400
313
-
314
- ☐ Implement GET /api/analyses/:id/messages (PAGINATED):
315
  - Query: limit=50, offset=0
316
  - Return: { messages, pagination }
317
  - Test: Large message history, pagination works
@@ -320,26 +320,26 @@ const messages = await api.getAnalysisMessages( // NEW
320
  ### Phase 3: Testing & Integration (1 day)
321
 
322
  ```
323
- ☐ Integration tests:
324
- - Create analysis → Get analysis → Verify structure
325
- - Send message → Get messages → Verify in list
326
- - Invoke skill → Verify response + skill status
327
- - Update datasources → Verify changes persisted
328
-
329
- ☐ Error handling tests:
330
- - Create analysis without dataSources → 400
331
- - Invoke skill without precondition → 400 PRECONDITION_UNMET
332
- - Remove all datasources → 400
333
-
334
- ☐ Frontend integration:
335
- - Frontend calls new endpoints → works
336
- - Skill invocation → displays status correctly
337
- - Datasource selection → lists both documents & databases
338
  ```
339
 
340
  ---
341
 
342
- ## 🎯 Summary Table
343
 
344
  | Endpoint | Existing | Status | Effort |
345
  |----------|----------|--------|--------|
 
1
+ > Superseded planning note: For the 2026-06-30 dual-backend UX plan, use `frontend-dual-backend-contract.md` as the authoritative frontend contract. This file is retained as historical context from the older endpoint analysis.
2
  # Endpoint Comparison: Existing vs Needed
3
 
4
  ---
5
 
6
+ ## Visual Comparison
7
 
8
  ### EXISTING STRUCTURE (Current State)
9
 
10
  ```
11
  Frontend Pages:
12
+ +-- Analytics Page (Chat Rooms)
13
+ | +-- GET /api/v1/chat-rooms (List rooms)
14
+ | +-- POST /api/v1/chat-rooms (Create room)
15
+ | +-- GET /api/v1/chat-rooms/:roomId (Get room + messages)
16
+ | +-- DELETE /api/v1/chat-rooms/:roomId (Delete room)
17
+ | +-- POST /api/v1/chat/stream (Send message, get streaming response)
18
+ |
19
+ +-- Knowledge Management Page (Documents + Databases)
20
+ | +-- GET /api/v1/documents/:userId (List documents)
21
+ | +-- POST /api/v1/document/upload (Upload doc)
22
+ | +-- POST /api/v1/document/process (Process doc)
23
+ | +-- DELETE /api/v1/document/delete (Delete doc)
24
+ | +-- GET /api/v1/database-clients/dbtypes
25
+ | +-- POST /api/v1/database-clients (Connect DB)
26
+ | +-- GET /api/v1/database-clients/:userId
27
+ | +-- DELETE /api/v1/database-clients/:id
28
+ | +-- POST /api/v1/database-clients/:id/ingest
29
+ |
30
+ +-- Interview Page (Separate)
31
+ +-- GET /frameworks
32
+ +-- POST /api/v1/interviews/sessions (Create session)
33
+ +-- POST /api/v1/interviews/sessions/:id/message
34
+ +-- POST /api/v1/interviews/sessions/:id/finish
35
+ +-- GET /api/v1/interviews/:roomId/result
36
+ +-- POST /api/v1/interviews/sessions/:id/stream-message
37
  ```
38
 
39
  ### NEW STRUCTURE (After Unified Analysis Chat)
40
 
41
  ```
42
  Frontend:
43
+ +-- Unified Analysis Chat Page
44
+ | +-- POST /api/analyses (Create analysis) [NEW or EXTEND chat-rooms]
45
+ | +-- GET /api/analyses (List analyses) [NEW or EXTEND]
46
+ | +-- GET /api/analyses/:id (Get analysis + messages) [NEW or EXTEND]
47
+ | +-- PUT /api/analyses/:id (Update analysis) [NEW - untuk update datasources]
48
+ | +-- DELETE /api/analyses/:id (Delete analysis) [NEW or EXTEND]
49
+ | |
50
+ | +-- POST /api/analyses/:id/messages (Add message/invoke skill) [NEW - extend chat/stream]
51
+ | +-- GET /api/analyses/:id/messages (List messages paginated) [NEW]
52
+ | |
53
+ | +-- GET /api/skills?analysisId=X (Get available skills) [NEW - skill management]
54
+ | +-- GET /api/skills/:skillName (Get skill detail) [NEW - optional]
55
+ | |
56
+ | +-- GET /api/datasources (Get documents + databases) [NEW - aggregate]
57
+ |
58
+ +-- Knowledge Management (TETAP ADA)
59
+ | +-- Same as before
60
+ |
61
+ +-- Interview (TETAP ADA atau DIGABUNG)
62
+ +-- Same as before or merged
63
  ```
64
 
65
  ---
66
 
67
+ ## Refresh Endpoint Reuse Strategy
68
 
69
  ### Which Endpoints Can Be Reused/Extended?
70
 
71
+ #### [OK] REUSABLE (Logic mirip):
72
 
73
  ```
74
+ Chat Rooms -> Analysis CRUD
75
+ +-- GET /api/v1/chat-rooms -> GET /api/analyses
76
+ | Same: List user's conversations
77
+ | Diff: Response includes dataSources + problem_statement
78
+ |
79
+ +-- POST /api/v1/chat-rooms -> POST /api/analyses
80
+ | Same: Create new conversation
81
+ | Diff: Require dataSources, problem_statement; no longer optional title
82
+ |
83
+ +-- GET /api/v1/chat-rooms/:id -> GET /api/analyses/:id
84
+ | Same: Get full conversation with messages
85
+ | Diff: Include dataSources, problem_statement, skill metadata in messages
86
+ |
87
+ +-- DELETE /api/v1/chat-rooms/:id -> DELETE /api/analyses/:id
88
  Same: Delete conversation
89
  Diff: Same logic
90
  ```
91
 
92
  ```
93
+ Chat Message -> Analysis Message + Skill Metadata
94
+ +-- POST /api/v1/chat/stream -> POST /api/analyses/:id/messages
95
+ | Same: Send message to conversation, get response
96
+ | Diff: Add skillName, skillParams; add skill invocation metadata
97
+ | Response includes message + optional skillResponse
98
+ |
99
+ +-- NEW REQUIREMENT: GET /api/analyses/:id/messages
100
  Needed for: Paginated message history when switching analyses
101
  Similar to: GET /api/v1/chat-rooms/:id (but paginated messages)
102
  ```
103
 
104
  ```
105
+ Knowledge Base -> Data Sources
106
+ +-- GET /api/v1/documents -> GET /api/datasources (documents type)
107
+ | Same: List available data sources
108
+ | Diff: Filter by type, more metadata
109
+ |
110
+ +-- GET /api/v1/database-clients -> GET /api/datasources (database type)
111
  Same: List available data sources
112
  Diff: Aggregate with documents, unified response
113
  ```
114
 
115
+ #### [NO] TIDAK BISA REUSE (Logic baru):
116
 
117
  ```
118
  NEW: PUT /api/analyses/:id
 
133
 
134
  ---
135
 
136
+ ## Data Structure Comparison
137
 
138
  ### Current: Room vs New: Analysis
139
 
140
  ```typescript
141
+ // [NO] EXISTING: Room (from api.ts)
142
  interface Room {
143
  id: string;
144
  title: string;
 
156
  sources?: ChatSource[]; // References to documents used
157
  }
158
 
159
+ // [OK] NEW: Analysis (needed for new feature)
160
  interface Analysis {
161
  id: string;
162
  title: string;
 
194
 
195
  ---
196
 
197
+ ## Integration Points
198
 
199
  ### How Frontend Will Call New Endpoints
200
 
 
240
 
241
  // On sidebar item click:
242
  const messages = await api.getAnalysisMessages( // NEW
243
+ analysisId,
244
+ limit=50,
245
  offset=0
246
  );
247
  // Load paginated message history
 
249
 
250
  ---
251
 
252
+ ## Implementation Checklist for Backend
253
 
254
  ### Phase 1: Extend Existing Endpoints (2 days)
255
 
256
  ```
257
+ [ ] Extend Room table schema:
258
  - Add: problem_statement TEXT
259
  - Add: data_sources JSONB
260
+
261
+ [ ] Extend POST /api/v1/chat-rooms:
262
  - Add: problem_statement, dataSources to request
263
  - Validate: dataSources.length >= 1
264
  - Test: Create analysis with data sources
265
+
266
+ [ ] Extend GET /api/v1/chat-rooms:
267
  - Include: problem_statement, dataSources in response
268
  - Test: Verify data sources in response
269
+
270
+ [ ] Extend GET /api/v1/chat-rooms/:id:
271
  - Include: problem_statement, dataSources
272
  - Test: Verify full analysis structure
273
+
274
+ [ ] Extend PUT /api/v1/chat-rooms/:id -> PUT /api/analyses/:id:
275
  - New endpoint: Update analysis
276
  - Allow update: title, problem_statement, data_sources
277
  - Validate: dataSources.length >= 1
 
281
  ### Phase 2: New Endpoints (2 days)
282
 
283
  ```
284
+ [ ] Implement GET /api/datasources:
285
  - Aggregate: documents + database clients
286
  - Format: { id, name, type: "document" | "database" }
287
  - Test: Get datasources, verify both types included
288
+
289
+ [ ] Implement GET /api/skills:
290
  - Query param: analysisId (required)
291
  - Return: Array of 10 skills with status
292
+ - Status logic:
293
+ - /analyze-* skills -> unavailable until /problem-statement called
294
+ - /help, /check-*, /retrieve-* -> always available
295
  - Test: Verify status changes based on analysis messages
296
+
297
+ [ ] Extend RoomMessage schema:
298
  - Add: is_skill_invocation BOOLEAN
299
  - Add: skill_name VARCHAR(255)
300
  - Add: skill_status VARCHAR(50)
301
+
302
+ [ ] Extend POST /api/v1/chat/stream -> POST /api/analyses/:id/messages:
303
  - New endpoint or extend existing
304
  - Input: { content, skillName?, skillParams? }
305
  - Validate: Check skill preconditions
306
  - Return error if precondition unmet:
307
  { error: "PRECONDITION_UNMET", precondition: "problem-statement" }
308
+ - Execute skill -> return both user message + skill response
309
+ - Test:
310
+ - Send regular message -> works
311
+ - Send skill with met preconditions -> works
312
+ - Send skill with unmet preconditions -> error 400
313
+
314
+ [ ] Implement GET /api/analyses/:id/messages (PAGINATED):
315
  - Query: limit=50, offset=0
316
  - Return: { messages, pagination }
317
  - Test: Large message history, pagination works
 
320
  ### Phase 3: Testing & Integration (1 day)
321
 
322
  ```
323
+ [ ] Integration tests:
324
+ - Create analysis -> Get analysis -> Verify structure
325
+ - Send message -> Get messages -> Verify in list
326
+ - Invoke skill -> Verify response + skill status
327
+ - Update datasources -> Verify changes persisted
328
+
329
+ [ ] Error handling tests:
330
+ - Create analysis without dataSources -> 400
331
+ - Invoke skill without precondition -> 400 PRECONDITION_UNMET
332
+ - Remove all datasources -> 400
333
+
334
+ [ ] Frontend integration:
335
+ - Frontend calls new endpoints -> works
336
+ - Skill invocation -> displays status correctly
337
+ - Datasource selection -> lists both documents & databases
338
  ```
339
 
340
  ---
341
 
342
+ ## Summary Table
343
 
344
  | Endpoint | Existing | Status | Effort |
345
  |----------|----------|--------|--------|
specs/001-unified-analysis-chat/contracts/EXISTING_ENDPOINTS_ANALYSIS.md CHANGED
@@ -1,39 +1,39 @@
1
- > Superseded planning note: For the 2026-06-30 dual-backend UX plan, use `frontend-dual-backend-contract.md` as the authoritative frontend contract. This file is retained as historical context from the older endpoint analysis.
2
  # Analisis Endpoint API Existing di Frontend
3
 
4
- **Tanggal**: 2026-06-15
5
  **Purpose**: Memahami endpoint mana yang sudah ada vs yang perlu ditambah untuk fitur Unified Analysis Chat
6
 
7
  ---
8
 
9
- ## 📍 Lokasi API Service Files
10
 
11
  ```
12
  src/services/
13
- ├── api.ts # Main API service (Orchestration & Agentic)
14
- └── interviewApi.ts # Interview-specific API
15
  ```
16
 
17
  ---
18
 
19
- ## 🔗 Base URLs yang Digunakan
20
 
21
  ```typescript
22
  // Dari src/env atau environment variables:
23
 
24
  ORCHESTRATION_API_BASE_URL = "https://api-orchestration.example.com"
25
- ↓ Untuk: Chat Rooms, Documents, Database Clients, Data Catalog
26
 
27
- AGENTIC_API_BASE_URL = "https://api-agentic.example.com"
28
- ↓ Untuk: Chat Streaming (Analytics/Chat Page)
29
 
30
  INTERVIEW_BASE_URL = "https://api-interview.example.com"
31
- ↓ Untuk: Interview Page
32
  ```
33
 
34
  ---
35
 
36
- ## 📊 Endpoint Existing - Diorganisir Berdasarkan Feature
37
 
38
  ### 1. AUTHENTICATION (src/services/api.ts)
39
 
@@ -68,13 +68,13 @@ Ini adalah fitur **MIRIP** dengan "Analysis" yang kita butuh, tapi nama/struktur
68
  // Location: src/services/api.ts:140-166
69
  export const getRooms = (userId: string): Promise<Room[]> =>
70
  request<{ status: string; message: string; data: Room[] | null }>(
71
- ORCHESTRATION_BASE_URL,
72
  `/api/v1/chat-rooms?user_id=${userId}`
73
  ).then(res => res.data ?? []);
74
 
75
  export const getRoom = (roomId: string): Promise<RoomDetail> =>
76
  request<{ status: string; message: string; data: RoomDetail }>(
77
- ORCHESTRATION_BASE_URL,
78
  `/api/v1/chat-rooms/${roomId}`
79
  ).then(res => res.data);
80
 
@@ -92,7 +92,7 @@ export const deleteRoom = (roomId: string, userId: string) =>
92
  );
93
  ```
94
 
95
- **🎯 Catatan**: Ini sudah mirip dengan Analysis CRUD yang kita butuh!
96
  - Room = Analysis (conversation)
97
  - RoomMessage = Message (tapi belum punya skill metadata)
98
 
@@ -112,7 +112,7 @@ export const deleteRoom = (roomId: string, userId: string) =>
112
  // Location: src/services/api.ts:170-212
113
  export const getDocuments = (userId: string): Promise<ApiDocument[]> =>
114
  request<{ status: string; message: string; data: ApiDocument[] | null }>(
115
- ORCHESTRATION_BASE_URL,
116
  `/api/v1/documents/${userId}`
117
  ).then(res => res.data ?? []);
118
 
@@ -120,7 +120,7 @@ export const uploadDocument = async (userId: string, file: File) => {
120
  const form = new FormData();
121
  form.append("user_id", userId);
122
  form.append("file", file);
123
-
124
  const res = await fetch(`${ORCHESTRATION_BASE_URL}/api/v1/document/upload`, {
125
  method: "POST",
126
  body: form,
@@ -136,7 +136,7 @@ export const deleteDocument = (userId: string, documentId: string) =>
136
  );
137
  ```
138
 
139
- **🎯 Catatan**: Ini adalah bagian dari "DataSources" yang kita butuh!
140
  - Documents = Tipe data source (document type)
141
  - Kita perlu: `GET /api/datasources` untuk menampilkan opsi pilihan data source dalam dialog
142
 
@@ -156,7 +156,7 @@ export const deleteDocument = (userId: string, documentId: string) =>
156
  // Location: src/services/api.ts:264-295
157
  export const getDatabaseClientTypes = (): Promise<DbTypeInfo[]> =>
158
  request<{ status: string; message: string; data: DbTypeInfo[] | null }>(
159
- ORCHESTRATION_BASE_URL,
160
  "/api/v1/database-clients/dbtypes"
161
  ).then(res => res.data ?? []);
162
 
@@ -167,7 +167,7 @@ export const connectDatabase = (
167
  credentials: Record<string, string | number | boolean>
168
  ): Promise<DatabaseClient> =>
169
  request<{ status: string; message: string; data: DatabaseClient }>(
170
- ORCHESTRATION_BASE_URL,
171
  `/api/v1/database-clients`, {
172
  method: "POST",
173
  body: JSON.stringify({ user_id: userId, name, db_type: dbType, credentials }),
@@ -175,12 +175,12 @@ export const connectDatabase = (
175
 
176
  export const getDatabaseClients = (userId: string): Promise<DatabaseClient[]> =>
177
  request<{ status: string; message: string; data: DatabaseClient[] | null }>(
178
- ORCHESTRATION_BASE_URL,
179
  `/api/v1/database-clients/${userId}`
180
  ).then(res => res.data ?? []);
181
 
182
  export const ingestDatabaseClient = (
183
- clientId: string,
184
  userId: string
185
  ): Promise<IngestResponse> =>
186
  request<{ status: string; message: string; data: IngestResponse }>(
@@ -190,7 +190,7 @@ export const ingestDatabaseClient = (
190
  ).then(res => res.data);
191
  ```
192
 
193
- **🎯 Catatan**: Ini juga bagian dari "DataSources"!
194
  - Database Clients = Type data source (database type)
195
  - Ingest = Process untuk membuat data tersedia
196
 
@@ -241,7 +241,7 @@ export const streamChat = (
241
  });
242
  ```
243
 
244
- **🎯 Catatan**: Ini endpoint untuk mengirim message ke chat!
245
  - Mirip dengan: `POST /api/analyses/:id/messages` yang kita butuh
246
  - Tapi ini hanya untuk plain message, belum ada skill invocation metadata
247
 
@@ -295,13 +295,13 @@ export const streamMessage = (
295
  });
296
  ```
297
 
298
- **🎯 Catatan**: Interview adalah fitur TERSENDIRI
299
  - Kemungkinan: Kita akan **MERGER** interview + analytics menjadi satu Analysis Chat
300
  - Tapi interview API tetap terpisah untuk structured interview flow
301
 
302
  ---
303
 
304
- ## 📈 Ringkasan Endpoint Existing
305
 
306
  ### Total: 22 Endpoint
307
 
@@ -309,65 +309,65 @@ export const streamMessage = (
309
 
310
  | Feature | Count | Status |
311
  |---------|-------|--------|
312
- | Authentication | 1 | ✅ |
313
- | Chat Rooms (Analysis equivalents) | 5 | ✅ |
314
- | Documents (Data Sources) | 5 | ✅ |
315
- | Database Clients (Data Sources) | 4 | ✅ |
316
- | Data Catalog | 2 | ✅ |
317
- | Chat Streaming | 1 | ✅ |
318
- | Interview | 6 | ✅ |
319
 
320
  ---
321
 
322
- ## 🆕 Endpoint BARU yang Dibutuhkan untuk Unified Analysis Chat
323
 
324
  Dari dokumentasi yang sudah saya buat (`ENDPOINTS.md`), inilah endpoint BARU yang perlu ditambah:
325
 
326
  | Endpoint | Status | Catatan |
327
  |----------|--------|---------|
328
- | `POST /api/analyses` | 🆕 NEW | Bisa reuse logika `POST /api/v1/chat-rooms` |
329
- | `GET /api/analyses` | 🆕 NEW | Bisa reuse logika `GET /api/v1/chat-rooms` |
330
- | `GET /api/analyses/:id` | 🆕 NEW | Bisa reuse logika `GET /api/v1/chat-rooms/:id` |
331
- | `PUT /api/analyses/:id` | 🆕 NEW | TIDAK ADA yang mirip sekarang |
332
- | `DELETE /api/analyses/:id` | 🆕 NEW | Bisa reuse logika `DELETE /api/v1/chat-rooms/:id` |
333
- | `POST /api/analyses/:id/messages` | 🆕 NEW | Mirip `POST /api/v1/chat/stream`, tapi dengan skill metadata |
334
- | `GET /api/analyses/:id/messages` | 🆕 NEW | GET messages dengan pagination, TIDAK ADA sekarang |
335
- | `GET /api/skills` | 🆕 NEW | TIDAK ADA - untuk skill discovery |
336
- | `GET /api/datasources` | 🆕 NEW | BISA REUSE dari `/api/v1/documents` + `/api/v1/database-clients` |
337
- | `GET /api/skills/:skillName` | 🆕 NEW | Optional - untuk skill details |
338
 
339
  ---
340
 
341
- ## 🔄 Mapping: Endpoint Existing → Reusable untuk Analysis Chat
342
 
343
  ### BISA LANGSUNG REUSE:
344
 
345
  ```typescript
346
- // Chat Room CRUD → Analysis CRUD
347
  // Struktur sama, tinggal ubah nama endpoint
348
 
349
- ❌ OLD: GET /api/v1/chat-rooms?user_id=X
350
- ✅ NEW: GET /api/analyses (dengan user diambil dari auth)
351
 
352
- ❌ OLD: POST /api/v1/chat-rooms { user_id, title }
353
- ✅ NEW: POST /api/analyses { title, problemStatement, dataSources }
354
 
355
- ❌ OLD: GET /api/v1/chat-rooms/:roomId
356
- ✅ NEW: GET /api/analyses/:id (ubah response structure untuk support dataSources)
357
 
358
- ❌ OLD: DELETE /api/v1/chat-rooms/:roomId?user_id=X
359
- ✅ NEW: DELETE /api/analyses/:id
360
  ```
361
 
362
  ### PERLU DIMODIFIKASI:
363
 
364
  ```typescript
365
- // Chat Message → Analysis Message + Skill Metadata
366
- ❌ OLD: POST /api/v1/chat/stream { user_id, room_id, message }
367
- ✅ NEW: POST /api/analyses/:id/messages {
368
- content,
369
- skillName?,
370
- skillParams?
371
  }
372
 
373
  Perbedaan:
@@ -378,51 +378,51 @@ Perbedaan:
378
  ### PERLU DIBIKIN BARU:
379
 
380
  ```typescript
381
- ✅ NEW: GET /api/analyses/:id/messages (paginated)
382
- ✅ NEW: GET /api/skills?analysisId=X (skill discovery dengan status)
383
- ✅ NEW: GET /api/datasources (list of documents + databases)
384
- ✅ NEW: PUT /api/analyses/:id (update title, problem, datasources)
385
  ```
386
 
387
  ---
388
 
389
- ## 🏗️ Implementation Strategy untuk Backend
390
 
391
  ### Option 1: Reuse Chat Room (Recommended)
392
 
393
  ```
394
  Rename/Extend existing endpoints:
395
- /api/v1/chat-rooms → /api/analyses
396
- ✅ Minimal code change
397
- ✅ Reuse existing DB schema (bisa extend dengan column dataSources)
398
- ✅ Reuse existing pagination, auth logic
399
  ```
400
 
401
  ### Option 2: Separate Analytics Service
402
 
403
  ```
404
  Create new service:
405
- /api/v1/analyses → Completely new implementation
406
- ✅ Clean separation of concerns
407
- ❌ More code duplication
408
- ❌ Maintain 2 similar services
409
  ```
410
 
411
  ### Option 3: Merge Everything
412
 
413
  ```
414
  Consolidate interview + analytics + chat into ONE service:
415
- /api/v1/analysis-sessions → Unified endpoint
416
- ✅ Single source of truth
417
- ❌ Big refactor, breaking changes
418
- ❌ Remove existing interview API
419
  ```
420
 
421
  **Rekomendasi**: **Option 1** - Extend existing chat-rooms dengan skill metadata
422
 
423
  ---
424
 
425
- ## 📋 Database Schema Changes Needed
426
 
427
  **Existing Room table**:
428
  ```sql
@@ -467,7 +467,7 @@ CREATE TABLE analysis_messages (
467
 
468
  ---
469
 
470
- ## 🎯 Skill Execution Logic Perlu Ditambah
471
 
472
  Endpoint existing **TIDAK ADA** untuk:
473
  - Tracking skill status (available vs unavailable)
@@ -499,20 +499,20 @@ export const invokeSkill = (analysisId: string, skillName: string) => {
499
 
500
  ---
501
 
502
- ## 📝 Kesimpulan & Next Steps
503
 
504
  ### Existing State:
505
- ✅ Ada 22 endpoint untuk auth, chat rooms, documents, databases, chat streaming, interview
506
 
507
  ### Untuk Unified Analysis Chat, butuh:
508
- 🆕 Tambah ~9 endpoint baru (atau extend yang existing)
509
  - Sebagian bisa reuse logika existing chat-rooms
510
  - Perlu skill management logic baru
511
  - Perlu message pagination
512
  - Perlu data source aggregation
513
 
514
  ### Recommended Approach:
515
- 1. **Extend** existing `/api/v1/chat-rooms` → `/api/analyses`
516
  - Add `problem_statement`, `data_sources` fields
517
  - Add skill metadata ke messages
518
  2. **Create** new endpoints untuk skills
 
1
+ > Superseded planning note: For the 2026-06-30 dual-backend UX plan, use `frontend-dual-backend-contract.md` as the authoritative frontend contract. This file is retained as historical context from the older endpoint analysis.
2
  # Analisis Endpoint API Existing di Frontend
3
 
4
+ **Tanggal**: 2026-06-15
5
  **Purpose**: Memahami endpoint mana yang sudah ada vs yang perlu ditambah untuk fitur Unified Analysis Chat
6
 
7
  ---
8
 
9
+ ## Lokasi API Service Files
10
 
11
  ```
12
  src/services/
13
+ +-- api.ts # Main API service (Orchestration & Agentic)
14
+ +-- interviewApi.ts # Interview-specific API
15
  ```
16
 
17
  ---
18
 
19
+ ## Base URLs yang Digunakan
20
 
21
  ```typescript
22
  // Dari src/env atau environment variables:
23
 
24
  ORCHESTRATION_API_BASE_URL = "https://api-orchestration.example.com"
25
+ v Untuk: Chat Rooms, Documents, Database Clients, Data Catalog
26
 
27
+ AGENTIC_API_BASE_URL = "https://api-agentic.example.com"
28
+ v Untuk: Chat Streaming (Analytics/Chat Page)
29
 
30
  INTERVIEW_BASE_URL = "https://api-interview.example.com"
31
+ v Untuk: Interview Page
32
  ```
33
 
34
  ---
35
 
36
+ ## Endpoint Existing - Diorganisir Berdasarkan Feature
37
 
38
  ### 1. AUTHENTICATION (src/services/api.ts)
39
 
 
68
  // Location: src/services/api.ts:140-166
69
  export const getRooms = (userId: string): Promise<Room[]> =>
70
  request<{ status: string; message: string; data: Room[] | null }>(
71
+ ORCHESTRATION_BASE_URL,
72
  `/api/v1/chat-rooms?user_id=${userId}`
73
  ).then(res => res.data ?? []);
74
 
75
  export const getRoom = (roomId: string): Promise<RoomDetail> =>
76
  request<{ status: string; message: string; data: RoomDetail }>(
77
+ ORCHESTRATION_BASE_URL,
78
  `/api/v1/chat-rooms/${roomId}`
79
  ).then(res => res.data);
80
 
 
92
  );
93
  ```
94
 
95
+ ** Catatan**: Ini sudah mirip dengan Analysis CRUD yang kita butuh!
96
  - Room = Analysis (conversation)
97
  - RoomMessage = Message (tapi belum punya skill metadata)
98
 
 
112
  // Location: src/services/api.ts:170-212
113
  export const getDocuments = (userId: string): Promise<ApiDocument[]> =>
114
  request<{ status: string; message: string; data: ApiDocument[] | null }>(
115
+ ORCHESTRATION_BASE_URL,
116
  `/api/v1/documents/${userId}`
117
  ).then(res => res.data ?? []);
118
 
 
120
  const form = new FormData();
121
  form.append("user_id", userId);
122
  form.append("file", file);
123
+
124
  const res = await fetch(`${ORCHESTRATION_BASE_URL}/api/v1/document/upload`, {
125
  method: "POST",
126
  body: form,
 
136
  );
137
  ```
138
 
139
+ ** Catatan**: Ini adalah bagian dari "DataSources" yang kita butuh!
140
  - Documents = Tipe data source (document type)
141
  - Kita perlu: `GET /api/datasources` untuk menampilkan opsi pilihan data source dalam dialog
142
 
 
156
  // Location: src/services/api.ts:264-295
157
  export const getDatabaseClientTypes = (): Promise<DbTypeInfo[]> =>
158
  request<{ status: string; message: string; data: DbTypeInfo[] | null }>(
159
+ ORCHESTRATION_BASE_URL,
160
  "/api/v1/database-clients/dbtypes"
161
  ).then(res => res.data ?? []);
162
 
 
167
  credentials: Record<string, string | number | boolean>
168
  ): Promise<DatabaseClient> =>
169
  request<{ status: string; message: string; data: DatabaseClient }>(
170
+ ORCHESTRATION_BASE_URL,
171
  `/api/v1/database-clients`, {
172
  method: "POST",
173
  body: JSON.stringify({ user_id: userId, name, db_type: dbType, credentials }),
 
175
 
176
  export const getDatabaseClients = (userId: string): Promise<DatabaseClient[]> =>
177
  request<{ status: string; message: string; data: DatabaseClient[] | null }>(
178
+ ORCHESTRATION_BASE_URL,
179
  `/api/v1/database-clients/${userId}`
180
  ).then(res => res.data ?? []);
181
 
182
  export const ingestDatabaseClient = (
183
+ clientId: string,
184
  userId: string
185
  ): Promise<IngestResponse> =>
186
  request<{ status: string; message: string; data: IngestResponse }>(
 
190
  ).then(res => res.data);
191
  ```
192
 
193
+ ** Catatan**: Ini juga bagian dari "DataSources"!
194
  - Database Clients = Type data source (database type)
195
  - Ingest = Process untuk membuat data tersedia
196
 
 
241
  });
242
  ```
243
 
244
+ ** Catatan**: Ini endpoint untuk mengirim message ke chat!
245
  - Mirip dengan: `POST /api/analyses/:id/messages` yang kita butuh
246
  - Tapi ini hanya untuk plain message, belum ada skill invocation metadata
247
 
 
295
  });
296
  ```
297
 
298
+ ** Catatan**: Interview adalah fitur TERSENDIRI
299
  - Kemungkinan: Kita akan **MERGER** interview + analytics menjadi satu Analysis Chat
300
  - Tapi interview API tetap terpisah untuk structured interview flow
301
 
302
  ---
303
 
304
+ ## Ringkasan Endpoint Existing
305
 
306
  ### Total: 22 Endpoint
307
 
 
309
 
310
  | Feature | Count | Status |
311
  |---------|-------|--------|
312
+ | Authentication | 1 | [OK] |
313
+ | Chat Rooms (Analysis equivalents) | 5 | [OK] |
314
+ | Documents (Data Sources) | 5 | [OK] |
315
+ | Database Clients (Data Sources) | 4 | [OK] |
316
+ | Data Catalog | 2 | [OK] |
317
+ | Chat Streaming | 1 | [OK] |
318
+ | Interview | 6 | [OK] |
319
 
320
  ---
321
 
322
+ ## Endpoint BARU yang Dibutuhkan untuk Unified Analysis Chat
323
 
324
  Dari dokumentasi yang sudah saya buat (`ENDPOINTS.md`), inilah endpoint BARU yang perlu ditambah:
325
 
326
  | Endpoint | Status | Catatan |
327
  |----------|--------|---------|
328
+ | `POST /api/analyses` | NEW | Bisa reuse logika `POST /api/v1/chat-rooms` |
329
+ | `GET /api/analyses` | NEW | Bisa reuse logika `GET /api/v1/chat-rooms` |
330
+ | `GET /api/analyses/:id` | NEW | Bisa reuse logika `GET /api/v1/chat-rooms/:id` |
331
+ | `PUT /api/analyses/:id` | NEW | TIDAK ADA yang mirip sekarang |
332
+ | `DELETE /api/analyses/:id` | NEW | Bisa reuse logika `DELETE /api/v1/chat-rooms/:id` |
333
+ | `POST /api/analyses/:id/messages` | NEW | Mirip `POST /api/v1/chat/stream`, tapi dengan skill metadata |
334
+ | `GET /api/analyses/:id/messages` | NEW | GET messages dengan pagination, TIDAK ADA sekarang |
335
+ | `GET /api/skills` | NEW | TIDAK ADA - untuk skill discovery |
336
+ | `GET /api/datasources` | NEW | BISA REUSE dari `/api/v1/documents` + `/api/v1/database-clients` |
337
+ | `GET /api/skills/:skillName` | NEW | Optional - untuk skill details |
338
 
339
  ---
340
 
341
+ ## Refresh Mapping: Endpoint Existing -> Reusable untuk Analysis Chat
342
 
343
  ### BISA LANGSUNG REUSE:
344
 
345
  ```typescript
346
+ // Chat Room CRUD -> Analysis CRUD
347
  // Struktur sama, tinggal ubah nama endpoint
348
 
349
+ [NO] OLD: GET /api/v1/chat-rooms?user_id=X
350
+ [OK] NEW: GET /api/analyses (dengan user diambil dari auth)
351
 
352
+ [NO] OLD: POST /api/v1/chat-rooms { user_id, title }
353
+ [OK] NEW: POST /api/analyses { title, problemStatement, dataSources }
354
 
355
+ [NO] OLD: GET /api/v1/chat-rooms/:roomId
356
+ [OK] NEW: GET /api/analyses/:id (ubah response structure untuk support dataSources)
357
 
358
+ [NO] OLD: DELETE /api/v1/chat-rooms/:roomId?user_id=X
359
+ [OK] NEW: DELETE /api/analyses/:id
360
  ```
361
 
362
  ### PERLU DIMODIFIKASI:
363
 
364
  ```typescript
365
+ // Chat Message -> Analysis Message + Skill Metadata
366
+ [NO] OLD: POST /api/v1/chat/stream { user_id, room_id, message }
367
+ [OK] NEW: POST /api/analyses/:id/messages {
368
+ content,
369
+ skillName?,
370
+ skillParams?
371
  }
372
 
373
  Perbedaan:
 
378
  ### PERLU DIBIKIN BARU:
379
 
380
  ```typescript
381
+ [OK] NEW: GET /api/analyses/:id/messages (paginated)
382
+ [OK] NEW: GET /api/skills?analysisId=X (skill discovery dengan status)
383
+ [OK] NEW: GET /api/datasources (list of documents + databases)
384
+ [OK] NEW: PUT /api/analyses/:id (update title, problem, datasources)
385
  ```
386
 
387
  ---
388
 
389
+ ## Implementation Strategy untuk Backend
390
 
391
  ### Option 1: Reuse Chat Room (Recommended)
392
 
393
  ```
394
  Rename/Extend existing endpoints:
395
+ /api/v1/chat-rooms -> /api/analyses
396
+ [OK] Minimal code change
397
+ [OK] Reuse existing DB schema (bisa extend dengan column dataSources)
398
+ [OK] Reuse existing pagination, auth logic
399
  ```
400
 
401
  ### Option 2: Separate Analytics Service
402
 
403
  ```
404
  Create new service:
405
+ /api/v1/analyses -> Completely new implementation
406
+ [OK] Clean separation of concerns
407
+ [NO] More code duplication
408
+ [NO] Maintain 2 similar services
409
  ```
410
 
411
  ### Option 3: Merge Everything
412
 
413
  ```
414
  Consolidate interview + analytics + chat into ONE service:
415
+ /api/v1/analysis-sessions -> Unified endpoint
416
+ [OK] Single source of truth
417
+ [NO] Big refactor, breaking changes
418
+ [NO] Remove existing interview API
419
  ```
420
 
421
  **Rekomendasi**: **Option 1** - Extend existing chat-rooms dengan skill metadata
422
 
423
  ---
424
 
425
+ ## Database Schema Changes Needed
426
 
427
  **Existing Room table**:
428
  ```sql
 
467
 
468
  ---
469
 
470
+ ## Skill Execution Logic Perlu Ditambah
471
 
472
  Endpoint existing **TIDAK ADA** untuk:
473
  - Tracking skill status (available vs unavailable)
 
499
 
500
  ---
501
 
502
+ ## Kesimpulan & Next Steps
503
 
504
  ### Existing State:
505
+ [OK] Ada 22 endpoint untuk auth, chat rooms, documents, databases, chat streaming, interview
506
 
507
  ### Untuk Unified Analysis Chat, butuh:
508
+ Tambah ~9 endpoint baru (atau extend yang existing)
509
  - Sebagian bisa reuse logika existing chat-rooms
510
  - Perlu skill management logic baru
511
  - Perlu message pagination
512
  - Perlu data source aggregation
513
 
514
  ### Recommended Approach:
515
+ 1. **Extend** existing `/api/v1/chat-rooms` -> `/api/analyses`
516
  - Add `problem_statement`, `data_sources` fields
517
  - Add skill metadata ke messages
518
  2. **Create** new endpoints untuk skills
src/app/components/KnowledgeManagement.tsx CHANGED
@@ -75,7 +75,7 @@ export default function KnowledgeManagement({
75
  onClose,
76
  variant = "modal",
77
  }: KnowledgeManagementProps) {
78
- // ── Document state ──────────────────────────────────────────────────────────
79
  const [docTypes, setDocTypes] = useState<DocTypeInfo[]>([]);
80
  const [documents, setDocuments] = useState<ApiDocument[]>([]);
81
  const [loadingDocs, setLoadingDocs] = useState(false);
@@ -85,11 +85,11 @@ export default function KnowledgeManagement({
85
  const [processing, setProcessing] = useState<string | null>(null);
86
  const [deleting, setDeleting] = useState<string | null>(null);
87
 
88
- // ── Navigation state ────────────────────────────────────────────────────────
89
  const [view, setView] = useState<View>("main");
90
  const [selectedDbType, setSelectedDbType] = useState<DbType | null>(null);
91
 
92
- // ── DB type & client state ──────────────────────────────────────────────────
93
  const [dbTypeInfos, setDbTypeInfos] = useState<DbTypeInfo[]>([]);
94
  const [dbClients, setDbClients] = useState<DatabaseClient[]>([]);
95
  const [loadingDbTypes, setLoadingDbTypes] = useState(false);
@@ -104,11 +104,11 @@ export default function KnowledgeManagement({
104
  });
105
  const pollingTimers = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
106
 
107
- // ── Data Catalog state ──────────────────────────────────────────────────────
108
  const [catalogSources, setCatalogSources] = useState<DataCatalogSource[]>([]);
109
  const [loadingCatalog, setLoadingCatalog] = useState(false);
110
 
111
- // ── DB credentials form state ───────────────────────────────────────────────
112
  const [connectionName, setConnectionName] = useState("");
113
  const [dbForm, setDbForm] = useState<Record<string, string | number | boolean>>({});
114
  const [connecting, setConnecting] = useState(false);
@@ -184,7 +184,7 @@ export default function KnowledgeManagement({
184
  onClose();
185
  };
186
 
187
- // ── Document handlers ───────────────────────────────────────────────────────
188
 
189
  const loadDocuments = async (userId: string) => {
190
  setLoadingDocs(true);
@@ -277,7 +277,7 @@ export default function KnowledgeManagement({
277
  setDocuments([]);
278
  };
279
 
280
- // ── DB handlers ─────────────────────────────────────────────────────────────
281
 
282
  const handleDbConnect = async () => {
283
  const userId = getUserId();
@@ -335,7 +335,7 @@ export default function KnowledgeManagement({
335
  }
336
  };
337
 
338
- // ── Helpers ─────────────────────────────────────────────────────────────────
339
 
340
  const formatFileSize = (bytes: number) => {
341
  if (bytes < 1024) return bytes + " B";
@@ -419,7 +419,7 @@ export default function KnowledgeManagement({
419
 
420
  const isPage = variant === "page";
421
 
422
- // ── Header title & back button logic ────────────────────────────────────────
423
 
424
  const selectedDbInfo = dbTypeInfos.find((d) => d.db_type === selectedDbType);
425
 
@@ -441,7 +441,7 @@ export default function KnowledgeManagement({
441
  ? () => setView("main")
442
  : null;
443
 
444
- // ── Render ───────────────────────────────────────────────────────────────────
445
 
446
  return (
447
  <div className={isPage ? "knowledge-surface h-full min-h-screen bg-slate-50" : "knowledge-surface fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4 backdrop-blur-sm"}>
@@ -490,7 +490,7 @@ export default function KnowledgeManagement({
490
  {/* Content */}
491
  <div className={isPage ? "flex-1 overflow-y-auto px-6 py-5" : "flex-1 overflow-y-auto px-5 py-4"}>
492
 
493
- {/* ── VIEW: main ── */}
494
  {view === "main" && (
495
  <div className="space-y-4">
496
 
@@ -509,7 +509,7 @@ export default function KnowledgeManagement({
509
  <div className="text-center">
510
  <p className="text-sm font-medium text-slate-700">
511
  {uploading
512
- ? "Uploading…"
513
  : <>Drop files, or <span className="text-[#FF8F00]">browse</span></>
514
  }
515
  </p>
@@ -557,7 +557,7 @@ export default function KnowledgeManagement({
557
  <div>
558
  <div className="flex items-center justify-between mb-2">
559
  <span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
560
- Databases · {dbClients.length}
561
  </span>
562
  </div>
563
  <div className="space-y-1">
@@ -575,14 +575,14 @@ export default function KnowledgeManagement({
575
  </div>
576
  <div className="flex-1 min-w-0">
577
  <p className="text-sm font-medium text-slate-800 truncate">{client.name}</p>
578
- <p className="text-xs text-slate-400 capitalize">{client.db_type} · {client.status}</p>
579
  </div>
580
  <div className="flex items-center gap-1.5 flex-shrink-0">
581
  <div className="flex flex-col items-end gap-0.5">
582
  <button
583
  onClick={() => handleIngest(client.id)}
584
  disabled={ingesting === client.id || client.status === "inactive"}
585
- title={ingestTimestamps[client.id] ? `Last ingested: ${new Date(ingestTimestamps[client.id]).toLocaleString()}` : "Never ingested — click to ingest schema"}
586
  className={[
587
  "flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg transition disabled:opacity-40 disabled:cursor-not-allowed",
588
  ingestTimestamps[client.id]
@@ -595,7 +595,7 @@ export default function KnowledgeManagement({
595
  ) : (
596
  <Database className="w-3 h-3" />
597
  )}
598
- {ingesting === client.id ? "Ingesting…" : "Ingest"}
599
  </button>
600
  {ingestTimestamps[client.id] && (
601
  <span className="text-[10px] text-slate-300 leading-none">
@@ -676,7 +676,7 @@ export default function KnowledgeManagement({
676
  {doc.filename}
677
  </p>
678
  <p className="text-xs text-slate-400">
679
- {formatFileSize(doc.file_size)} · {formatDate(doc.created_at)}
680
  </p>
681
  </div>
682
  <div className="flex flex-shrink-0 items-center gap-1.5 sm:gap-2">
@@ -702,7 +702,7 @@ export default function KnowledgeManagement({
702
  </div>
703
  )}
704
 
705
- {/* ── VIEW: db-select ── */}
706
  {view === "db-select" && (
707
  <div className="space-y-4">
708
  <p className="text-sm text-slate-400">
@@ -760,7 +760,7 @@ export default function KnowledgeManagement({
760
  </div>
761
  )}
762
 
763
- {/* ── VIEW: db-credentials ── */}
764
  {view === "db-credentials" && (
765
  <div className="space-y-4">
766
 
@@ -859,7 +859,7 @@ export default function KnowledgeManagement({
859
  className="w-full flex items-center justify-center gap-2 bg-[#FF8F00] hover:bg-[#FF6F00] active:bg-[#E65100] text-white py-2.5 rounded-xl text-sm font-medium transition disabled:opacity-40 disabled:cursor-not-allowed"
860
  >
861
  {connecting ? (
862
- <><Loader2 className="w-4 h-4 animate-spin" /> Connecting…</>
863
  ) : (
864
  "Connect"
865
  )}
@@ -867,7 +867,7 @@ export default function KnowledgeManagement({
867
  </div>
868
  )}
869
 
870
- {/* ── VIEW: catalog ── */}
871
  {view === "catalog" && (
872
  <div className="space-y-3">
873
  <button
@@ -897,7 +897,7 @@ export default function KnowledgeManagement({
897
  <p className="text-sm font-medium text-slate-800 truncate">{src.name}</p>
898
  <p className="text-xs text-slate-400">
899
  {src.source_type === "schema" ? "Database" : "Tabular"}
900
- {src.table_count != null ? ` · ${src.table_count} table${src.table_count !== 1 ? "s" : ""}` : ""}
901
  </p>
902
  </div>
903
  </div>
 
75
  onClose,
76
  variant = "modal",
77
  }: KnowledgeManagementProps) {
78
+ // -- Document state ----------------------------------------------------------
79
  const [docTypes, setDocTypes] = useState<DocTypeInfo[]>([]);
80
  const [documents, setDocuments] = useState<ApiDocument[]>([]);
81
  const [loadingDocs, setLoadingDocs] = useState(false);
 
85
  const [processing, setProcessing] = useState<string | null>(null);
86
  const [deleting, setDeleting] = useState<string | null>(null);
87
 
88
+ // -- Navigation state --------------------------------------------------------
89
  const [view, setView] = useState<View>("main");
90
  const [selectedDbType, setSelectedDbType] = useState<DbType | null>(null);
91
 
92
+ // -- DB type & client state --------------------------------------------------
93
  const [dbTypeInfos, setDbTypeInfos] = useState<DbTypeInfo[]>([]);
94
  const [dbClients, setDbClients] = useState<DatabaseClient[]>([]);
95
  const [loadingDbTypes, setLoadingDbTypes] = useState(false);
 
104
  });
105
  const pollingTimers = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
106
 
107
+ // -- Data Catalog state ------------------------------------------------------
108
  const [catalogSources, setCatalogSources] = useState<DataCatalogSource[]>([]);
109
  const [loadingCatalog, setLoadingCatalog] = useState(false);
110
 
111
+ // -- DB credentials form state -----------------------------------------------
112
  const [connectionName, setConnectionName] = useState("");
113
  const [dbForm, setDbForm] = useState<Record<string, string | number | boolean>>({});
114
  const [connecting, setConnecting] = useState(false);
 
184
  onClose();
185
  };
186
 
187
+ // -- Document handlers -------------------------------------------------------
188
 
189
  const loadDocuments = async (userId: string) => {
190
  setLoadingDocs(true);
 
277
  setDocuments([]);
278
  };
279
 
280
+ // -- DB handlers -------------------------------------------------------------
281
 
282
  const handleDbConnect = async () => {
283
  const userId = getUserId();
 
335
  }
336
  };
337
 
338
+ // -- Helpers -----------------------------------------------------------------
339
 
340
  const formatFileSize = (bytes: number) => {
341
  if (bytes < 1024) return bytes + " B";
 
419
 
420
  const isPage = variant === "page";
421
 
422
+ // -- Header title & back button logic ----------------------------------------
423
 
424
  const selectedDbInfo = dbTypeInfos.find((d) => d.db_type === selectedDbType);
425
 
 
441
  ? () => setView("main")
442
  : null;
443
 
444
+ // -- Render -------------------------------------------------------------------
445
 
446
  return (
447
  <div className={isPage ? "knowledge-surface h-full min-h-screen bg-slate-50" : "knowledge-surface fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4 backdrop-blur-sm"}>
 
490
  {/* Content */}
491
  <div className={isPage ? "flex-1 overflow-y-auto px-6 py-5" : "flex-1 overflow-y-auto px-5 py-4"}>
492
 
493
+ {/* -- VIEW: main -- */}
494
  {view === "main" && (
495
  <div className="space-y-4">
496
 
 
509
  <div className="text-center">
510
  <p className="text-sm font-medium text-slate-700">
511
  {uploading
512
+ ? "Uploading..."
513
  : <>Drop files, or <span className="text-[#FF8F00]">browse</span></>
514
  }
515
  </p>
 
557
  <div>
558
  <div className="flex items-center justify-between mb-2">
559
  <span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
560
+ Databases - {dbClients.length}
561
  </span>
562
  </div>
563
  <div className="space-y-1">
 
575
  </div>
576
  <div className="flex-1 min-w-0">
577
  <p className="text-sm font-medium text-slate-800 truncate">{client.name}</p>
578
+ <p className="text-xs text-slate-400 capitalize">{client.db_type} - {client.status}</p>
579
  </div>
580
  <div className="flex items-center gap-1.5 flex-shrink-0">
581
  <div className="flex flex-col items-end gap-0.5">
582
  <button
583
  onClick={() => handleIngest(client.id)}
584
  disabled={ingesting === client.id || client.status === "inactive"}
585
+ title={ingestTimestamps[client.id] ? `Last ingested: ${new Date(ingestTimestamps[client.id]).toLocaleString()}` : "Never ingested -- click to ingest schema"}
586
  className={[
587
  "flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg transition disabled:opacity-40 disabled:cursor-not-allowed",
588
  ingestTimestamps[client.id]
 
595
  ) : (
596
  <Database className="w-3 h-3" />
597
  )}
598
+ {ingesting === client.id ? "Ingesting..." : "Ingest"}
599
  </button>
600
  {ingestTimestamps[client.id] && (
601
  <span className="text-[10px] text-slate-300 leading-none">
 
676
  {doc.filename}
677
  </p>
678
  <p className="text-xs text-slate-400">
679
+ {formatFileSize(doc.file_size)} - {formatDate(doc.created_at)}
680
  </p>
681
  </div>
682
  <div className="flex flex-shrink-0 items-center gap-1.5 sm:gap-2">
 
702
  </div>
703
  )}
704
 
705
+ {/* -- VIEW: db-select -- */}
706
  {view === "db-select" && (
707
  <div className="space-y-4">
708
  <p className="text-sm text-slate-400">
 
760
  </div>
761
  )}
762
 
763
+ {/* -- VIEW: db-credentials -- */}
764
  {view === "db-credentials" && (
765
  <div className="space-y-4">
766
 
 
859
  className="w-full flex items-center justify-center gap-2 bg-[#FF8F00] hover:bg-[#FF6F00] active:bg-[#E65100] text-white py-2.5 rounded-xl text-sm font-medium transition disabled:opacity-40 disabled:cursor-not-allowed"
860
  >
861
  {connecting ? (
862
+ <><Loader2 className="w-4 h-4 animate-spin" /> Connecting...</>
863
  ) : (
864
  "Connect"
865
  )}
 
867
  </div>
868
  )}
869
 
870
+ {/* -- VIEW: catalog -- */}
871
  {view === "catalog" && (
872
  <div className="space-y-3">
873
  <button
 
897
  <p className="text-sm font-medium text-slate-800 truncate">{src.name}</p>
898
  <p className="text-xs text-slate-400">
899
  {src.source_type === "schema" ? "Database" : "Tabular"}
900
+ {src.table_count != null ? ` - ${src.table_count} table${src.table_count !== 1 ? "s" : ""}` : ""}
901
  </p>
902
  </div>
903
  </div>
src/app/components/analysis/AnalysisShell.tsx CHANGED
@@ -43,8 +43,9 @@ const wait = (ms: number) => new Promise((resolve) => window.setTimeout(resolve,
43
 
44
  function normalizeAssistantContent(content: string) {
45
  return content
46
- .replace(/\*\*\s+([^*\n]*?\S)\s*\*\*/g, "**$1**")
47
- .replace(/\*\*([^*\n]*?\S)\s+\*\*/g, "**$1**")
 
48
  .trim();
49
  }
50
 
@@ -61,6 +62,7 @@ export function AnalysisShell() {
61
  const [loadingAnalyses, setLoadingAnalyses] = useState(true);
62
  const [loadingMessages, setLoadingMessages] = useState(false);
63
  const [streamState, setStreamState] = useState<StreamState>("idle");
 
64
  const [newAnalysisOpen, setNewAnalysisOpen] = useState(false);
65
  const [reportCollapsed, setReportCollapsed] = useState(false);
66
  const [staleSources, setStaleSources] = useState<DataBindItem[]>([]);
@@ -189,6 +191,7 @@ export function AnalysisShell() {
189
  },
190
  ]);
191
 
 
192
  setStreamState(kind === "chat" ? "streaming-chat" : "streaming-help");
193
  try {
194
  const onEvent = (event: AgentStreamEvent) => {
@@ -221,6 +224,8 @@ export function AnalysisShell() {
221
 
222
  const completedContent = normalizeAssistantContent(finalContent);
223
  let traceTargetId = aiId;
 
 
224
  setMessages((prev) =>
225
  prev.map((msg) =>
226
  msg.id === aiId
@@ -232,16 +237,18 @@ export function AnalysisShell() {
232
  if (completedContent) {
233
  const saved = await createAnalysisMessage(activeAnalysis.id, { role: "ai", content: completedContent, message_id: doneMessageId });
234
  traceTargetId = saved.id;
 
 
235
  setMessages((prev) =>
236
  prev.map((msg) =>
237
  msg.id === aiId
238
- ? { ...toUiMessage(saved), messageId: doneMessageId, sources: msg.sources, traceabilityLoading: Boolean(doneMessageId) }
239
  : msg
240
  )
241
  );
242
  }
243
 
244
- await fetchTraceability(activeAnalysis.id, doneMessageId, traceTargetId);
245
  } catch (err) {
246
  const message = err instanceof Error ? err.message : "Agent stream failed";
247
  setMessages((prev) => prev.map((msg) => (msg.id === aiId ? { ...msg, status: "error", statusText: message, traceabilityLoading: false } : msg)));
@@ -327,7 +334,7 @@ export function AnalysisShell() {
327
  {loadingMessages ? (
328
  <div className="flex h-full items-center justify-center text-sm text-slate-500">Loading messages</div>
329
  ) : (
330
- <MessageList messages={messages} />
331
  )}
332
  </div>
333
  <ChatInput disabled={!activeAnalysis} streaming={streamState !== "idle"} onSend={handleSend} onHelp={handleHelp} />
 
43
 
44
  function normalizeAssistantContent(content: string) {
45
  return content
46
+ .replace(/([A-Za-z0-9])(?=\*\*)/g, "$1 ")
47
+ .replace(/(\*\*[^*\n]+\*\*)(?=[A-Za-z0-9])/g, "$1 ")
48
+ .replace(/\*\*([^*\n]*?)\*\*/g, (_, inner) => `**${inner.trim()}**`)
49
  .trim();
50
  }
51
 
 
62
  const [loadingAnalyses, setLoadingAnalyses] = useState(true);
63
  const [loadingMessages, setLoadingMessages] = useState(false);
64
  const [streamState, setStreamState] = useState<StreamState>("idle");
65
+ const [scrollTo, setScrollTo] = useState<"bottom" | "last-ai" | null>(null);
66
  const [newAnalysisOpen, setNewAnalysisOpen] = useState(false);
67
  const [reportCollapsed, setReportCollapsed] = useState(false);
68
  const [staleSources, setStaleSources] = useState<DataBindItem[]>([]);
 
191
  },
192
  ]);
193
 
194
+ setScrollTo("bottom");
195
  setStreamState(kind === "chat" ? "streaming-chat" : "streaming-help");
196
  try {
197
  const onEvent = (event: AgentStreamEvent) => {
 
224
 
225
  const completedContent = normalizeAssistantContent(finalContent);
226
  let traceTargetId = aiId;
227
+ let resolvedMessageId = doneMessageId;
228
+ setScrollTo("last-ai");
229
  setMessages((prev) =>
230
  prev.map((msg) =>
231
  msg.id === aiId
 
237
  if (completedContent) {
238
  const saved = await createAnalysisMessage(activeAnalysis.id, { role: "ai", content: completedContent, message_id: doneMessageId });
239
  traceTargetId = saved.id;
240
+ const savedUi = toUiMessage(saved);
241
+ resolvedMessageId = savedUi.messageId ?? doneMessageId;
242
  setMessages((prev) =>
243
  prev.map((msg) =>
244
  msg.id === aiId
245
+ ? { ...savedUi, sources: msg.sources, traceabilityLoading: Boolean(savedUi.messageId) }
246
  : msg
247
  )
248
  );
249
  }
250
 
251
+ await fetchTraceability(activeAnalysis.id, resolvedMessageId, traceTargetId);
252
  } catch (err) {
253
  const message = err instanceof Error ? err.message : "Agent stream failed";
254
  setMessages((prev) => prev.map((msg) => (msg.id === aiId ? { ...msg, status: "error", statusText: message, traceabilityLoading: false } : msg)));
 
334
  {loadingMessages ? (
335
  <div className="flex h-full items-center justify-center text-sm text-slate-500">Loading messages</div>
336
  ) : (
337
+ <MessageList messages={messages} scrollTo={scrollTo} onScrollDone={() => setScrollTo(null)} />
338
  )}
339
  </div>
340
  <ChatInput disabled={!activeAnalysis} streaming={streamState !== "idle"} onSend={handleSend} onHelp={handleHelp} />
src/app/components/analysis/MarkdownContent.tsx CHANGED
@@ -6,10 +6,9 @@ import rehypeKatex from "rehype-katex";
6
 
7
  function repairMarkdownMarkers(line: string) {
8
  return line
9
- .replace(/\*\*\s+([^*\n]*?\S)\s*\*\*/g, "**$1**")
10
- .replace(/\*\*([^*\n]*?\S)\s+\*\*/g, "**$1**")
11
  .replace(/([A-Za-z0-9])(?=\*\*)/g, "$1 ")
12
- .replace(/(\*\*[^*]+\*\*)(?=[A-Za-z0-9])/g, "$1 ");
 
13
  }
14
 
15
  function normalizeReadableMarkdown(content: string) {
@@ -50,14 +49,14 @@ function normalizeReadableMarkdown(content: string) {
50
  }
51
 
52
  const markdownComponents: Components = {
53
- p: ({ children }) => <p className="my-3 leading-7 text-slate-800">{children}</p>,
54
- ul: ({ children }) => <ul className="my-3 list-disc space-y-2 pl-5">{children}</ul>,
55
- ol: ({ children }) => <ol className="my-3 list-decimal space-y-2 pl-5">{children}</ol>,
56
- li: ({ children }) => <li className="pl-1 leading-7 text-slate-800">{children}</li>,
57
  strong: ({ children }) => <strong className="font-semibold text-slate-950">{children}</strong>,
58
- h1: ({ children }) => <h1 className="mb-3 mt-5 text-xl font-semibold leading-7 text-slate-950">{children}</h1>,
59
- h2: ({ children }) => <h2 className="mb-3 mt-5 text-lg font-semibold leading-7 text-slate-950">{children}</h2>,
60
- h3: ({ children }) => <h3 className="mb-2 mt-4 text-base font-semibold leading-6 text-slate-950">{children}</h3>,
61
  a: ({ children, href }) => (
62
  <a href={href} className="font-medium text-emerald-700 underline decoration-emerald-200 underline-offset-4" target="_blank" rel="noreferrer">
63
  {children}
 
6
 
7
  function repairMarkdownMarkers(line: string) {
8
  return line
 
 
9
  .replace(/([A-Za-z0-9])(?=\*\*)/g, "$1 ")
10
+ .replace(/(\*\*[^*\n]+\*\*)(?=[A-Za-z0-9])/g, "$1 ")
11
+ .replace(/\*\*([^*\n]*?)\*\*/g, (_, inner) => `**${inner.trim()}**`);
12
  }
13
 
14
  function normalizeReadableMarkdown(content: string) {
 
49
  }
50
 
51
  const markdownComponents: Components = {
52
+ p: ({ children }) => <p className="my-1.5 leading-6 text-slate-800">{children}</p>,
53
+ ul: ({ children }) => <ul className="my-1.5 list-disc space-y-0.5 pl-5">{children}</ul>,
54
+ ol: ({ children }) => <ol className="my-1.5 list-decimal space-y-0.5 pl-5">{children}</ol>,
55
+ li: ({ children }) => <li className="pl-1 leading-6 text-slate-800">{children}</li>,
56
  strong: ({ children }) => <strong className="font-semibold text-slate-950">{children}</strong>,
57
+ h1: ({ children }) => <h1 className="mb-2 mt-4 text-xl font-semibold leading-6 text-slate-950">{children}</h1>,
58
+ h2: ({ children }) => <h2 className="mb-2 mt-3 text-lg font-semibold leading-6 text-slate-950">{children}</h2>,
59
+ h3: ({ children }) => <h3 className="mb-1.5 mt-3 text-base font-semibold leading-6 text-slate-950">{children}</h3>,
60
  a: ({ children, href }) => (
61
  <a href={href} className="font-medium text-emerald-700 underline decoration-emerald-200 underline-offset-4" target="_blank" rel="noreferrer">
62
  {children}
src/app/components/analysis/MessageList.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import { Bot, User } from "lucide-react";
2
  import type { UiMessage } from "./types";
3
  import { MarkdownContent } from "./MarkdownContent";
@@ -5,7 +6,28 @@ import { MessageTraceability } from "./MessageTraceability";
5
  import { EmptyState } from "./AnalysisStates";
6
  import { cx, formatDateTime } from "./utils";
7
 
8
- export function MessageList({ messages }: { messages: UiMessage[] }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  if (messages.length === 0) {
10
  return (
11
  <div className="flex h-full items-center justify-center p-6">
@@ -14,12 +36,15 @@ export function MessageList({ messages }: { messages: UiMessage[] }) {
14
  );
15
  }
16
 
 
 
17
  return (
18
- <div className="mx-auto flex w-full max-w-5xl flex-col gap-5 px-4 py-6">
19
- {messages.map((message) => {
20
  const isUser = message.role === "user";
 
21
  return (
22
- <article key={message.id} className={cx("flex min-w-0 gap-3", isUser && "justify-end")}>
23
  {!isUser && (
24
  <div className="mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-slate-900 text-white">
25
  <Bot className="h-4 w-4" />
 
1
+ import { useEffect, useRef } from "react";
2
  import { Bot, User } from "lucide-react";
3
  import type { UiMessage } from "./types";
4
  import { MarkdownContent } from "./MarkdownContent";
 
6
  import { EmptyState } from "./AnalysisStates";
7
  import { cx, formatDateTime } from "./utils";
8
 
9
+ export function MessageList({
10
+ messages,
11
+ scrollTo,
12
+ onScrollDone,
13
+ }: {
14
+ messages: UiMessage[];
15
+ scrollTo?: "bottom" | "last-ai" | null;
16
+ onScrollDone?: () => void;
17
+ }) {
18
+ const containerRef = useRef<HTMLDivElement>(null);
19
+ const lastAiRef = useRef<HTMLElement>(null);
20
+
21
+ useEffect(() => {
22
+ if (!scrollTo) return;
23
+ if (scrollTo === "bottom") {
24
+ containerRef.current?.scrollIntoView({ block: "end", behavior: "smooth" });
25
+ } else if (scrollTo === "last-ai" && lastAiRef.current) {
26
+ lastAiRef.current.scrollIntoView({ block: "start", behavior: "smooth" });
27
+ }
28
+ onScrollDone?.();
29
+ }, [scrollTo]);
30
+
31
  if (messages.length === 0) {
32
  return (
33
  <div className="flex h-full items-center justify-center p-6">
 
36
  );
37
  }
38
 
39
+ const lastAiIndex = messages.reduce((last, msg, i) => (msg.role === "ai" ? i : last), -1);
40
+
41
  return (
42
+ <div ref={containerRef} className="mx-auto flex w-full max-w-5xl flex-col gap-5 px-4 py-6">
43
+ {messages.map((message, index) => {
44
  const isUser = message.role === "user";
45
+ const isLastAi = index === lastAiIndex;
46
  return (
47
+ <article ref={isLastAi ? lastAiRef : null} key={message.id} className={cx("flex min-w-0 gap-3", isUser && "justify-end")}>
48
  {!isUser && (
49
  <div className="mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-slate-900 text-white">
50
  <Bot className="h-4 w-4" />