File size: 1,783 Bytes
b74c6ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { api, post } from "./client";
import { streamSSE } from "./stream";

export type Navigation = { target_route: string; needs_notebook: boolean; description: string };
export type EscalateReason = "human" | "refund" | "feature";

export type SupportMessage = {
  id: string;
  role: "user" | "assistant";
  content: string;
  cited_docs: string[];
};

export type DonePayload = {
  conversation_id: string;
  citations: string[];
  escalate: boolean;
  escalate_reason: EscalateReason | null;
};

export const supportApi = {
  latest: () => api<{ conversation_id: string | null; messages: SupportMessage[] }>("/api/support/conversations/latest"),
  escalate: (body: { email: string; concern: string; reason: EscalateReason; page_path: string; conversation_id: string | null }) =>
    post<{ ok: true }>("/api/support/escalate", body),
  /** Streams the answer. onToken gets text as it arrives, onDone the labels (escalation) once it is saved. */
  chat: (
    body: { message: string; page_path: string; conversation_id: string | null },
    on: { onToken: (t: string) => void; onAnswerDone?: () => void; onDone: (d: DonePayload) => void; onError: (m: string) => void },
    signal?: AbortSignal,
  ) =>
    streamSSE(
      "/api/support/chat/stream",
      (event, data) => {
        if (event === "token") on.onToken(String(data));
        else if (event === "answer_done") on.onAnswerDone?.();
        else if (event === "done") on.onDone(data as DonePayload);
        else if (event === "error") on.onError(String(data));
      },
      { method: "POST", body: JSON.stringify(body) },
      signal,
    ).catch((err: unknown) => {
      if ((err as { name?: string })?.name !== "AbortError") on.onError("Could not reach the help assistant. Please try again.");
    }),
};