Spaces:
Sleeping
Sleeping
harryagasi commited on
Commit ·
587748b
1
Parent(s): e2da2f6
fix: handle failed AI message persistence per updated Go contract
Browse filesBackend now returns 201 with status:"failed" and a reserved all-zero
message_id when an AI message is saved without a valid message_id,
instead of a request error. Surface this as an error bubble and skip
calling traceability with the reserved id.
- docs/API_CONTRACT_BE_GOLANG.md +69 -10
- src/app/components/KnowledgeManagement.tsx +4 -2
- src/app/components/analysis/AnalysisShell.tsx +14 -3
- src/app/components/analysis/MarkdownContent.tsx +3 -3
- src/app/components/analysis/ReportSidebar.tsx +1 -1
- src/app/components/analysis/types.ts +2 -0
- src/services/orchestrationApi.ts +29 -15
docs/API_CONTRACT_BE_GOLANG.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
-
#
|
|
|
|
|
|
|
| 2 |
|
| 3 |
Dokumen ini merangkum endpoint Orchestration Agent Service yang dipakai oleh frontend. Fokus flow:
|
| 4 |
|
|
@@ -802,11 +804,14 @@ Errors:
|
|
| 802 |
|
| 803 |
```json
|
| 804 |
{
|
| 805 |
-
"id": "
|
|
|
|
| 806 |
"analysis_id": "analysis-id",
|
| 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,11 +821,18 @@ Errors:
|
|
| 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`.
|
| 822 |
|
| 823 |
-
Request untuk pertanyaan user:
|
| 824 |
|
| 825 |
```json
|
| 826 |
{
|
|
@@ -829,16 +841,17 @@ Request untuk pertanyaan user:
|
|
| 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 |
```
|
| 840 |
|
| 841 |
-
Success `201`:
|
| 842 |
|
| 843 |
```json
|
| 844 |
{
|
|
@@ -852,13 +865,38 @@ Success `201`:
|
|
| 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 |
|
|
@@ -891,14 +929,26 @@ Success `200`:
|
|
| 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`
|
| 902 |
|
| 903 |
Errors: `400`, `401`, `404`.
|
| 904 |
|
|
@@ -942,7 +992,7 @@ GET /api/v1/data-catalog/{user_id}
|
|
| 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
|
| 946 |
GET /api/v1/analyses/{analysis_id}/messages
|
| 947 |
```
|
| 948 |
|
|
@@ -954,4 +1004,13 @@ GET /api/v1/analyses/{analysis_id}/messages
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Backend Go API Contract
|
| 2 |
+
|
| 3 |
+
**Last updated**: 2026-07-09
|
| 4 |
|
| 5 |
Dokumen ini merangkum endpoint Orchestration Agent Service yang dipakai oleh frontend. Fokus flow:
|
| 6 |
|
|
|
|
| 804 |
|
| 805 |
```json
|
| 806 |
{
|
| 807 |
+
"id": "uuid-dari-db",
|
| 808 |
+
"message_id": "uuid-dari-db",
|
| 809 |
"analysis_id": "analysis-id",
|
| 810 |
"user_id": "user-id",
|
| 811 |
"role": "user",
|
| 812 |
"content": "Apa penyebab revenue turun di Q3?",
|
| 813 |
+
"status": "success",
|
| 814 |
+
"note": "",
|
| 815 |
"created_at": "2026-06-30T08:00:00Z"
|
| 816 |
}
|
| 817 |
```
|
|
|
|
| 821 |
- `user`: pertanyaan atau instruksi dari user
|
| 822 |
- `ai`: jawaban dari AI Agent Service
|
| 823 |
|
| 824 |
+
`status` hanya:
|
| 825 |
+
|
| 826 |
+
- `success`: pesan tersimpan sesuai konten yang dikirim. Untuk `role=user`, `message_id` selalu system-generated (default), berapa pun nilai `message_id` yang dikirim frontend (jika ada) — nilai kiriman itu diabaikan. Untuk `role=ai`, `success` berarti `message_id` yang dikirim valid dan dipakai apa adanya.
|
| 827 |
+
- `failed`: khusus `role=ai` yang dikirim tanpa `message_id` valid (lihat catatan di bawah). `content` diganti pesan fallback hardcoded dan `message_id` diganti default reserved value.
|
| 828 |
+
|
| 829 |
+
`note` hanya terisi (non-empty) ketika `status=failed`, berisi penjelasan singkat penyebab kegagalan. Saat `status=success`, field ini kosong/absent.
|
| 830 |
+
|
| 831 |
### `POST /api/v1/analyses/{id}/messages`
|
| 832 |
|
| 833 |
Merekam tepat satu pesan conversation ke `analyses_messages`.
|
| 834 |
|
| 835 |
+
Request untuk pertanyaan user — **jangan kirim `message_id`**; jika dikirim tetap akan diabaikan sepenuhnya dan diganti dengan nilai default (system-generated):
|
| 836 |
|
| 837 |
```json
|
| 838 |
{
|
|
|
|
| 841 |
}
|
| 842 |
```
|
| 843 |
|
| 844 |
+
Request untuk jawaban AI — **`message_id` wajib diisi untuk `role=ai`**:
|
| 845 |
|
| 846 |
```json
|
| 847 |
{
|
| 848 |
"role": "ai",
|
| 849 |
+
"content": "Revenue turun karena penurunan transaksi enterprise dan kenaikan churn di wilayah barat.",
|
| 850 |
+
"message_id": "uuid-yang-dibuat-frontend-atau-agent-service"
|
| 851 |
}
|
| 852 |
```
|
| 853 |
|
| 854 |
+
Success `201` (role=user, atau role=ai dengan `message_id` valid):
|
| 855 |
|
| 856 |
```json
|
| 857 |
{
|
|
|
|
| 865 |
"user_id": "user-id",
|
| 866 |
"role": "user",
|
| 867 |
"content": "Apa penyebab revenue turun di Q3?",
|
| 868 |
+
"status": "success",
|
| 869 |
"created_at": "2026-06-30T08:00:00Z"
|
| 870 |
}
|
| 871 |
}
|
| 872 |
}
|
| 873 |
```
|
| 874 |
|
| 875 |
+
> `message_id` bernilai sama dengan `id` untuk pesan sukses (baik `role=user` maupun `role=ai` dengan `message_id` valid) — disertakan agar frontend dapat langsung menggunakannya untuk memanggil endpoint observability (`GET /api/v1/observability?analysis_id=...&message_id=...`) tanpa mapping tambahan.
|
| 876 |
+
|
| 877 |
+
**Kasus `role=ai` tanpa `message_id`** (missing atau blank): endpoint **tetap** mengembalikan `201` (bukan error) karena kegagalan ini dianggap outcome data-level, bukan request error. Response menunjukkan `status: "failed"`:
|
| 878 |
+
|
| 879 |
+
```json
|
| 880 |
+
{
|
| 881 |
+
"status": "success",
|
| 882 |
+
"message": "Message created",
|
| 883 |
+
"data": {
|
| 884 |
+
"message": {
|
| 885 |
+
"id": "uuid-dari-db",
|
| 886 |
+
"message_id": "00000000-0000-0000-0000-000000000000",
|
| 887 |
+
"analysis_id": "analysis-id",
|
| 888 |
+
"user_id": "user-id",
|
| 889 |
+
"role": "ai",
|
| 890 |
+
"content": "Maaf, terjadi kesalahan saat memproses respons AI. Silakan coba lagi.",
|
| 891 |
+
"status": "failed",
|
| 892 |
+
"note": "message_id is required for AI-role messages but was not provided",
|
| 893 |
+
"created_at": "2026-06-30T08:01:00Z"
|
| 894 |
+
}
|
| 895 |
+
}
|
| 896 |
+
}
|
| 897 |
+
```
|
| 898 |
+
|
| 899 |
+
`message_id: "00000000-0000-0000-0000-000000000000"` adalah nilai reserved yang dipakai berulang untuk setiap pesan `ai` yang gagal (bukan di-generate baru setiap kali) — jangan gunakan nilai ini untuk memanggil endpoint observability, karena tidak menunjuk ke jawaban AI yang valid. Chat tetap bisa dilanjutkan setelah kegagalan ini; tidak ada retry otomatis dari backend.
|
| 900 |
|
| 901 |
Errors:
|
| 902 |
|
|
|
|
| 929 |
"user_id": "user-id",
|
| 930 |
"role": "ai",
|
| 931 |
"content": "Revenue turun karena penurunan transaksi enterprise.",
|
| 932 |
+
"status": "success",
|
| 933 |
"created_at": "2026-06-30T08:01:00Z"
|
| 934 |
+
},
|
| 935 |
+
{
|
| 936 |
+
"id": "uuid-dari-db-2",
|
| 937 |
+
"message_id": "00000000-0000-0000-0000-000000000000",
|
| 938 |
+
"analysis_id": "analysis-id",
|
| 939 |
+
"user_id": "user-id",
|
| 940 |
+
"role": "ai",
|
| 941 |
+
"content": "Maaf, terjadi kesalahan saat memproses respons AI. Silakan coba lagi.",
|
| 942 |
+
"status": "failed",
|
| 943 |
+
"note": "message_id is required for AI-role messages but was not provided",
|
| 944 |
+
"created_at": "2026-06-30T08:02:00Z"
|
| 945 |
}
|
| 946 |
]
|
| 947 |
}
|
| 948 |
}
|
| 949 |
```
|
| 950 |
|
| 951 |
+
> Setiap item menyertakan `message_id`, `status`, dan (jika `failed`) `note`. Untuk pesan `status=success`, `message_id` sama dengan `id` dan dapat dipakai memanggil `GET /api/v1/observability?analysis_id=...&message_id=...` setelah page refresh. Untuk pesan `status=failed`, gunakan field ini hanya untuk ditampilkan sebagai error bubble di UI — jangan dipakai memanggil observability endpoint.
|
| 952 |
|
| 953 |
Errors: `400`, `401`, `404`.
|
| 954 |
|
|
|
|
| 992 |
POST /api/v1/analyses with business_questions
|
| 993 |
call AI Agent Service outside this service
|
| 994 |
POST /api/v1/analyses/{analysis_id}/messages with role=user and content=user_question
|
| 995 |
+
POST /api/v1/analyses/{analysis_id}/messages with role=ai, content=agent_answer, message_id=agent_message_id
|
| 996 |
GET /api/v1/analyses/{analysis_id}/messages
|
| 997 |
```
|
| 998 |
|
|
|
|
| 1004 |
- Perubahan `data_bind` akan rebuild analysis-level catalog. Frontend juga dapat memanggil endpoint rebuild analysis catalog secara eksplisit jika diperlukan.
|
| 1005 |
- Setelah refresh token sukses, selalu replace refresh token lama dengan refresh token baru.
|
| 1006 |
- Untuk endpoint yang membutuhkan `user_id`, gunakan `data.user.id` dari login dan pastikan sama dengan token aktif.
|
| 1007 |
+
- Untuk binding analysis, pakai source yang sudah berhasil diupload/diproses atau database client yang sudah diingest.
|
| 1008 |
+
- Saat merekam jawaban AI (`role=ai`), selalu sertakan `message_id` dari AI Agent Service. Jika `message_id` tidak dikirim atau kosong, endpoint tetap sukses (`201`) tetapi menyimpan pesan dengan `status: "failed"`, `content` fallback hardcoded, dan `message_id` reserved (`00000000-0000-0000-0000-000000000000`) — frontend harus memeriksa `status` pada response, bukan hanya HTTP status code, untuk mendeteksi kegagalan ini.
|
| 1009 |
+
- Saat merekam pertanyaan user (`role=user`), **jangan** mengirim `message_id` sama sekali — backend selalu mengabaikannya dan memakai nilai default (system-generated) untuk `role=user`, tidak peduli apa yang dikirim frontend.
|
| 1010 |
+
|
| 1011 |
+
|
| 1012 |
+
|
| 1013 |
+
|
| 1014 |
+
|
| 1015 |
+
|
| 1016 |
+
|
src/app/components/KnowledgeManagement.tsx
CHANGED
|
@@ -305,8 +305,10 @@ export default function KnowledgeManagement({
|
|
| 305 |
if (!userId) return;
|
| 306 |
setIngesting(clientId);
|
| 307 |
try {
|
| 308 |
-
|
| 309 |
-
const
|
|
|
|
|
|
|
| 310 |
toast.success(`Ingested ${tableCount} table${tableCount !== 1 ? "s" : ""} successfully`);
|
| 311 |
const now = new Date().toISOString();
|
| 312 |
setIngestTimestamps((prev) => {
|
|
|
|
| 305 |
if (!userId) return;
|
| 306 |
setIngesting(clientId);
|
| 307 |
try {
|
| 308 |
+
await ingestDatabaseClient(clientId, userId);
|
| 309 |
+
const catalog = await getDataCatalog(userId);
|
| 310 |
+
const source = catalog.sources.find((s) => s.source_id === clientId);
|
| 311 |
+
const tableCount = source?.table_count ?? 0;
|
| 312 |
toast.success(`Ingested ${tableCount} table${tableCount !== 1 ? "s" : ""} successfully`);
|
| 313 |
const now = new Date().toISOString();
|
| 314 |
setIngestTimestamps((prev) => {
|
src/app/components/analysis/AnalysisShell.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
| 12 |
getDocuments,
|
| 13 |
listAnalyses,
|
| 14 |
updateAnalysisDataBind,
|
|
|
|
| 15 |
type Analysis,
|
| 16 |
type AnalysisMessage,
|
| 17 |
type DataBindItem,
|
|
@@ -29,13 +30,17 @@ import type { StreamState, UiMessage } from "./types";
|
|
| 29 |
import { cx, makeLocalId } from "./utils";
|
| 30 |
|
| 31 |
function toUiMessage(message: AnalysisMessage): UiMessage {
|
|
|
|
| 32 |
return {
|
| 33 |
id: message.id,
|
| 34 |
role: message.role,
|
| 35 |
content: message.content,
|
| 36 |
created_at: message.created_at,
|
| 37 |
-
status: "complete",
|
|
|
|
| 38 |
messageId: message.message_id ?? undefined,
|
|
|
|
|
|
|
| 39 |
};
|
| 40 |
}
|
| 41 |
|
|
@@ -153,6 +158,7 @@ export function AnalysisShell() {
|
|
| 153 |
message.role === "ai" &&
|
| 154 |
message.status === "complete" &&
|
| 155 |
Boolean(message.messageId) &&
|
|
|
|
| 156 |
!message.traceability &&
|
| 157 |
!message.traceabilityLoading &&
|
| 158 |
!message.traceabilityError
|
|
@@ -229,14 +235,19 @@ export function AnalysisShell() {
|
|
| 229 |
const saved = await createAnalysisMessage(activeAnalysis.id, { role: "ai", content: completedContent, message_id: doneMessageId });
|
| 230 |
traceTargetId = saved.id;
|
| 231 |
const savedUi = toUiMessage(saved);
|
| 232 |
-
|
|
|
|
| 233 |
setMessages((prev) =>
|
| 234 |
prev.map((msg) =>
|
| 235 |
msg.id === aiId
|
| 236 |
-
? { ...savedUi, sources: msg.sources, traceabilityLoading: Boolean(
|
| 237 |
: msg
|
| 238 |
)
|
| 239 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
}
|
| 241 |
|
| 242 |
await fetchTraceability(activeAnalysis.id, resolvedMessageId, traceTargetId);
|
|
|
|
| 12 |
getDocuments,
|
| 13 |
listAnalyses,
|
| 14 |
updateAnalysisDataBind,
|
| 15 |
+
RESERVED_FAILED_MESSAGE_ID,
|
| 16 |
type Analysis,
|
| 17 |
type AnalysisMessage,
|
| 18 |
type DataBindItem,
|
|
|
|
| 30 |
import { cx, makeLocalId } from "./utils";
|
| 31 |
|
| 32 |
function toUiMessage(message: AnalysisMessage): UiMessage {
|
| 33 |
+
const failed = message.status === "failed";
|
| 34 |
return {
|
| 35 |
id: message.id,
|
| 36 |
role: message.role,
|
| 37 |
content: message.content,
|
| 38 |
created_at: message.created_at,
|
| 39 |
+
status: failed ? "error" : "complete",
|
| 40 |
+
statusText: failed ? message.note : undefined,
|
| 41 |
messageId: message.message_id ?? undefined,
|
| 42 |
+
persistStatus: message.status,
|
| 43 |
+
persistNote: message.note,
|
| 44 |
};
|
| 45 |
}
|
| 46 |
|
|
|
|
| 158 |
message.role === "ai" &&
|
| 159 |
message.status === "complete" &&
|
| 160 |
Boolean(message.messageId) &&
|
| 161 |
+
message.messageId !== RESERVED_FAILED_MESSAGE_ID &&
|
| 162 |
!message.traceability &&
|
| 163 |
!message.traceabilityLoading &&
|
| 164 |
!message.traceabilityError
|
|
|
|
| 235 |
const saved = await createAnalysisMessage(activeAnalysis.id, { role: "ai", content: completedContent, message_id: doneMessageId });
|
| 236 |
traceTargetId = saved.id;
|
| 237 |
const savedUi = toUiMessage(saved);
|
| 238 |
+
const persistFailed = saved.status === "failed";
|
| 239 |
+
resolvedMessageId = persistFailed ? undefined : savedUi.messageId ?? doneMessageId;
|
| 240 |
setMessages((prev) =>
|
| 241 |
prev.map((msg) =>
|
| 242 |
msg.id === aiId
|
| 243 |
+
? { ...savedUi, sources: msg.sources, traceabilityLoading: Boolean(resolvedMessageId) }
|
| 244 |
: msg
|
| 245 |
)
|
| 246 |
);
|
| 247 |
+
if (persistFailed) {
|
| 248 |
+
toast.error(saved.note || "Failed to save AI response");
|
| 249 |
+
return;
|
| 250 |
+
}
|
| 251 |
}
|
| 252 |
|
| 253 |
await fetchTraceability(activeAnalysis.id, resolvedMessageId, traceTargetId);
|
src/app/components/analysis/MarkdownContent.tsx
CHANGED
|
@@ -26,8 +26,8 @@ function normalizeReadableMarkdown(content: string) {
|
|
| 26 |
return repairMarkdownMarkers(line)
|
| 27 |
.replace(/([.!?])\s*-\s*(?=[A-Z0-9*])/g, "$1\n\n- ")
|
| 28 |
.replace(/(^|\n)-(?=\*\*|[A-Z0-9])/g, "$1- ")
|
| 29 |
-
.replace(/([,;:])(?=\S)/g, "$1 ")
|
| 30 |
-
.replace(/([.!?])(?=[A-
|
| 31 |
.replace(/[ \t]{2,}/g, " ");
|
| 32 |
});
|
| 33 |
|
|
@@ -79,7 +79,7 @@ export function MarkdownContent({ content }: { content: string }) {
|
|
| 79 |
const readableContent = normalizeReadableMarkdown(content);
|
| 80 |
|
| 81 |
return (
|
| 82 |
-
<div className="agent-markdown max-w-none
|
| 83 |
<ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]} components={markdownComponents}>
|
| 84 |
{readableContent}
|
| 85 |
</ReactMarkdown>
|
|
|
|
| 26 |
return repairMarkdownMarkers(line)
|
| 27 |
.replace(/([.!?])\s*-\s*(?=[A-Z0-9*])/g, "$1\n\n- ")
|
| 28 |
.replace(/(^|\n)-(?=\*\*|[A-Z0-9])/g, "$1- ")
|
| 29 |
+
.replace(/(?<!\d)([,;:])(?=\S)/g, "$1 ")
|
| 30 |
+
.replace(/(?<!\d)([.!?])(?=[A-Z])/g, "$1 ")
|
| 31 |
.replace(/[ \t]{2,}/g, " ");
|
| 32 |
});
|
| 33 |
|
|
|
|
| 79 |
const readableContent = normalizeReadableMarkdown(content);
|
| 80 |
|
| 81 |
return (
|
| 82 |
+
<div className="agent-markdown max-w-none text-sm leading-7 text-slate-800 [overflow-wrap:anywhere]">
|
| 83 |
<ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]} components={markdownComponents}>
|
| 84 |
{readableContent}
|
| 85 |
</ReactMarkdown>
|
src/app/components/analysis/ReportSidebar.tsx
CHANGED
|
@@ -72,7 +72,7 @@ export function ReportSidebar({ analysis, userId, onCollapse }: ReportSidebarPro
|
|
| 72 |
}
|
| 73 |
};
|
| 74 |
loadDetail();
|
| 75 |
-
}, [
|
| 76 |
|
| 77 |
const handleGenerate = async () => {
|
| 78 |
if (!analysis?.id || !userId) return;
|
|
|
|
| 72 |
}
|
| 73 |
};
|
| 74 |
loadDetail();
|
| 75 |
+
}, [selectedVersion]);
|
| 76 |
|
| 77 |
const handleGenerate = async () => {
|
| 78 |
if (!analysis?.id || !userId) return;
|
src/app/components/analysis/types.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface UiMessage {
|
|
| 12 |
statusText?: string;
|
| 13 |
sources?: unknown[];
|
| 14 |
messageId?: string;
|
|
|
|
|
|
|
| 15 |
traceability?: Observability | null;
|
| 16 |
traceabilityLoading?: boolean;
|
| 17 |
traceabilityError?: string | null;
|
|
|
|
| 12 |
statusText?: string;
|
| 13 |
sources?: unknown[];
|
| 14 |
messageId?: string;
|
| 15 |
+
persistStatus?: "success" | "failed";
|
| 16 |
+
persistNote?: string;
|
| 17 |
traceability?: Observability | null;
|
| 18 |
traceabilityLoading?: boolean;
|
| 19 |
traceabilityError?: string | null;
|
src/services/orchestrationApi.ts
CHANGED
|
@@ -185,9 +185,13 @@ export interface AnalysisMessage {
|
|
| 185 |
role: "user" | "ai";
|
| 186 |
content: string;
|
| 187 |
message_id?: string | null;
|
|
|
|
|
|
|
| 188 |
created_at: string;
|
| 189 |
}
|
| 190 |
|
|
|
|
|
|
|
| 191 |
export interface CreateAnalysisPayload {
|
| 192 |
analysis_title: string;
|
| 193 |
objective: string;
|
|
@@ -250,25 +254,35 @@ async function parseError(res: Response): Promise<ApiError> {
|
|
| 250 |
return new ApiError(message, res.status, body);
|
| 251 |
}
|
| 252 |
|
|
|
|
|
|
|
| 253 |
async function refreshWithStoredSession(): Promise<UserSession | null> {
|
| 254 |
-
|
| 255 |
-
if (!current?.refresh_token) return null;
|
| 256 |
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
body: JSON.stringify({ refresh_token: current.refresh_token }),
|
| 261 |
-
});
|
| 262 |
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
-
|
| 269 |
-
const next = sessionFromAuthData(envelope.data, current);
|
| 270 |
-
writeStoredSession(next);
|
| 271 |
-
return next;
|
| 272 |
}
|
| 273 |
|
| 274 |
async function orchestrationRequest<T>(path: string, options: RequestInit = {}, retry = true): Promise<T> {
|
|
|
|
| 185 |
role: "user" | "ai";
|
| 186 |
content: string;
|
| 187 |
message_id?: string | null;
|
| 188 |
+
status?: "success" | "failed";
|
| 189 |
+
note?: string;
|
| 190 |
created_at: string;
|
| 191 |
}
|
| 192 |
|
| 193 |
+
export const RESERVED_FAILED_MESSAGE_ID = "00000000-0000-0000-0000-000000000000";
|
| 194 |
+
|
| 195 |
export interface CreateAnalysisPayload {
|
| 196 |
analysis_title: string;
|
| 197 |
objective: string;
|
|
|
|
| 254 |
return new ApiError(message, res.status, body);
|
| 255 |
}
|
| 256 |
|
| 257 |
+
let _refreshPromise: Promise<UserSession | null> | null = null;
|
| 258 |
+
|
| 259 |
async function refreshWithStoredSession(): Promise<UserSession | null> {
|
| 260 |
+
if (_refreshPromise) return _refreshPromise;
|
|
|
|
| 261 |
|
| 262 |
+
_refreshPromise = (async () => {
|
| 263 |
+
const current = readStoredSession();
|
| 264 |
+
if (!current?.refresh_token) return null;
|
|
|
|
|
|
|
| 265 |
|
| 266 |
+
const res = await fetch(`${ORCHESTRATION_BASE_URL}/api/refresh`, {
|
| 267 |
+
method: "POST",
|
| 268 |
+
headers: { "Content-Type": "application/json" },
|
| 269 |
+
body: JSON.stringify({ refresh_token: current.refresh_token }),
|
| 270 |
+
});
|
| 271 |
+
|
| 272 |
+
if (!res.ok) {
|
| 273 |
+
clearStoredSession();
|
| 274 |
+
return null;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
const envelope = (await res.json()) as RefreshResponse;
|
| 278 |
+
const next = sessionFromAuthData(envelope.data, current);
|
| 279 |
+
writeStoredSession(next);
|
| 280 |
+
return next;
|
| 281 |
+
})().finally(() => {
|
| 282 |
+
_refreshPromise = null;
|
| 283 |
+
});
|
| 284 |
|
| 285 |
+
return _refreshPromise;
|
|
|
|
|
|
|
|
|
|
| 286 |
}
|
| 287 |
|
| 288 |
async function orchestrationRequest<T>(path: string, options: RequestInit = {}, retry = true): Promise<T> {
|