main
ts 106 lines 2.89 KB
Raw
1 import type { FlaskBaseResponse } from "@/types/flask.d"
2 import type { TalonInvestigateRequest, TalonJobData, TalonTemplate } from "@/types/talon.d"
3 import { useAuthStore } from "@/stores/auth"
4 import { HttpClient } from "../httpClient"
5
6 export default {
7 investigate(payload: TalonInvestigateRequest) {
8 return HttpClient.post<FlaskBaseResponse & { data?: Record<string, unknown> }>(`/talon/investigate`, payload)
9 },
10 getStatus() {
11 return HttpClient.get<FlaskBaseResponse & { data?: Record<string, unknown> }>(`/talon/status`)
12 },
13 getJob(alertId: number) {
14 return HttpClient.get<FlaskBaseResponse & { data?: TalonJobData }>(`/talon/jobs/${alertId}`)
15 },
16 /**
17 * List the prompt templates available in NanoClaw's CoPilot group.
18 * Used by the replay picker in the review UI — metadata only, no bodies.
19 */
20 getTemplates() {
21 return HttpClient.get<FlaskBaseResponse & { templates: TalonTemplate[] }>(`/talon/templates`)
22 },
23 /**
24 * Stream a message to Talon via SSE.
25 *
26 * NanoClaw sends events in the format:
27 * data: {"type":"text","content":"...the markdown..."}\n\n
28 * data: {"type":"done"}\n\n
29 */
30 async streamMessage(
31 message: string,
32 onChunk: (text: string) => void,
33 onDone: () => void,
34 onError: (error: string) => void,
35 signal?: AbortSignal
36 ) {
37 const store = useAuthStore()
38 const baseUrl = "/api"
39
40 try {
41 const response = await fetch(`${baseUrl}/talon/message`, {
42 method: "POST",
43 headers: {
44 "Content-Type": "application/json",
45 ...(store.userToken ? { Authorization: `Bearer ${store.userToken}` } : {})
46 },
47 body: JSON.stringify({ message, sender: "copilot" }),
48 signal
49 })
50
51 if (!response.ok) {
52 onError(`Request failed with status ${response.status}`)
53 return
54 }
55
56 const reader = response.body?.getReader()
57 if (!reader) {
58 onError("No response body")
59 return
60 }
61
62 const decoder = new TextDecoder()
63 let buffer = ""
64
65 while (true) {
66 const { done, value } = await reader.read()
67 if (done) break
68
69 buffer += decoder.decode(value, { stream: true })
70
71 // SSE events are separated by \n\n
72 const events = buffer.split("\n\n")
73 // Last element is incomplete — keep in buffer
74 buffer = events.pop() || ""
75
76 for (const event of events) {
77 for (const line of event.split("\n")) {
78 if (!line.startsWith("data: ")) continue
79 const data = line.slice(6)
80 try {
81 const parsed = JSON.parse(data)
82 if (parsed.type === "done") {
83 onDone()
84 return
85 }
86 if (parsed.type === "text" && parsed.content) {
87 onChunk(parsed.content)
88 }
89 if (parsed.error) {
90 onError(parsed.error)
91 return
92 }
93 } catch {
94 // Not JSON — ignore
95 }
96 }
97 }
98 }
99
100 onDone()
101 } catch (err: unknown) {
102 if (err instanceof DOMException && err.name === "AbortError") return
103 onError(err instanceof Error ? err.message : "An error occurred")
104 }
105 }
106 }