-
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathusePortStream.ts
More file actions
119 lines (102 loc) · 3.39 KB
/
usePortStream.ts
File metadata and controls
119 lines (102 loc) · 3.39 KB
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { usePort } from "@plasmohq/messaging/hook"
import { useCallback, useEffect, useRef, useState } from "react"
import type {
AiStreamRequest,
AiStreamResponse
} from "~background/ports/aiStream"
import { debugLog } from "~utils/logger"
import type { UsageInfo } from "./useHistorySaver"
interface PortStreamResult {
text: string
usage: UsageInfo | null
}
/**
* Hook for streaming AI text via background port messaging.
* Used in content script context to bypass CORS restrictions.
*
* Always call this hook (hooks can't be conditional),
* but only invoke `startStream` when in content script context.
*/
export function usePortStream() {
const [streamingText, setStreamingText] = useState("")
const [usage, setUsage] = useState<UsageInfo | null>(null)
const port = usePort<AiStreamRequest, AiStreamResponse>("aiStream")
// Use refs to resolve/reject the stream promise from the listener
const resolveRef = useRef<((result: PortStreamResult) => void) | null>(null)
const rejectRef = useRef<((error: Error) => void) | null>(null)
const accumulatedTextRef = useRef("")
const usageRef = useRef<UsageInfo | null>(null)
// Set up the port listener once
useEffect(() => {
const { port: chromePort, disconnect } = port.listen<AiStreamResponse>(
(msg) => {
switch (msg.type) {
case "chunk":
accumulatedTextRef.current += msg.text
setStreamingText((prev) => prev + msg.text)
break
case "usage":
usageRef.current = msg.usage as UsageInfo
setUsage(msg.usage as UsageInfo)
break
case "done":
debugLog(
"Port stream completed, text length:",
accumulatedTextRef.current.length
)
resolveRef.current?.({
text: accumulatedTextRef.current,
usage: usageRef.current
})
resolveRef.current = null
rejectRef.current = null
break
case "error":
debugLog("Port stream error:", msg.message)
rejectRef.current?.(new Error(msg.message))
resolveRef.current = null
rejectRef.current = null
break
}
}
)
// Reject pending promise if the port disconnects (e.g. service worker restart)
const onDisconnect = (port: chrome.runtime.Port) => {
if (resolveRef.current) {
debugLog("Port disconnected while stream in progress")
rejectRef.current?.(new Error("Port disconnected", { cause: port }))
resolveRef.current = null
rejectRef.current = null
}
}
chromePort.onDisconnect.addListener(onDisconnect)
return () => {
disconnect()
chromePort.onDisconnect.removeListener(onDisconnect)
}
}, [port])
const resetStream = useCallback(() => {
setStreamingText("")
setUsage(null)
accumulatedTextRef.current = ""
usageRef.current = null
}, [])
const startStream = useCallback(
(content: string, processedPrompt: string): Promise<PortStreamResult> => {
resetStream()
return new Promise<PortStreamResult>((resolve, reject) => {
resolveRef.current = resolve
rejectRef.current = reject
port.send({ content, processedPrompt })
})
},
[port, resetStream]
)
return {
streamingText,
usage,
startStream,
resetStream
}
}
export default usePortStream