Skip to content

Commit 63726a8

Browse files
authored
feat: Improve autocorrelation error handling (#933)
1 parent cda68e8 commit 63726a8

File tree

11 files changed

+221
-112
lines changed

11 files changed

+221
-112
lines changed

src/handlers/ai/preload.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { ipcRenderer } from 'electron'
22

33
import {
4+
AbortStreamChatRequest,
45
AiHandler,
5-
StreamChatRequest,
66
StreamChatChunk,
77
StreamChatEnd,
8-
StreamChatError,
9-
AbortStreamChatRequest,
8+
StreamChatRequest,
109
TokenUsage,
1110
} from './types'
1211

@@ -43,20 +42,6 @@ export function streamChat(request: StreamChatRequest) {
4342
return () => ipcRenderer.removeListener(AiHandler.StreamChatEnd, handler)
4443
},
4544

46-
onError: (callback: (error: string) => void) => {
47-
const handler = (
48-
_event: Electron.IpcRendererEvent,
49-
data: StreamChatError
50-
) => {
51-
if (data.id === request.id) {
52-
callback(data.error)
53-
}
54-
}
55-
ipcRenderer.on(AiHandler.StreamChatError, handler)
56-
return () =>
57-
ipcRenderer.removeListener(AiHandler.StreamChatError, handler)
58-
},
59-
6045
abort: () => {
6146
const abortRequest: AbortStreamChatRequest = { id: request.id }
6247
ipcRenderer.send(AiHandler.AbortStreamChat, abortRequest)

src/handlers/ai/streamMessages.ts

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,25 @@
11
import { StreamTextResult, ToolSet } from 'ai'
2-
import log from 'electron-log/main'
32

4-
import {
5-
AiHandler,
6-
StreamChatChunk,
7-
StreamChatEnd,
8-
StreamChatError,
9-
} from './types'
3+
import { AiHandler } from './types'
104

115
export async function streamMessages<Tools extends ToolSet, PARTIAL_OUTPUT>(
126
webContents: Electron.WebContents,
137
response: StreamTextResult<Tools, PARTIAL_OUTPUT>,
148
requestId: string
159
) {
16-
try {
17-
const uiStream = response.toUIMessageStream({
18-
onError: (error) => {
19-
// Throw tools errors, without this they get silenced
20-
throw error
21-
},
22-
})
23-
24-
// Process the stream and send chunks via IPC
25-
const reader = uiStream.getReader()
26-
let done = false
27-
28-
while (!done) {
29-
const result = await reader.read()
30-
done = result.done || false
31-
32-
if (done) {
33-
const usageData = await response.usage
34-
35-
webContents.send(AiHandler.StreamChatEnd, {
36-
id: requestId,
37-
usage: usageData,
38-
} satisfies StreamChatEnd)
39-
break
40-
}
10+
const stream = response.toUIMessageStream({})
4111

42-
// Send chunk event
43-
webContents.send(AiHandler.StreamChatChunk, {
44-
id: requestId,
45-
chunk: result.value,
46-
} satisfies StreamChatChunk)
47-
}
48-
} catch (error) {
49-
log.error('Error in handleStreamChat:', error)
50-
51-
// Send error event
52-
webContents.send(AiHandler.StreamChatError, {
12+
for await (const part of stream) {
13+
webContents.send(AiHandler.StreamChatChunk, {
5314
id: requestId,
54-
error: error instanceof Error ? error.message : 'Unknown error',
55-
} satisfies StreamChatError)
15+
chunk: part,
16+
})
5617
}
18+
19+
const usageData = await response.usage
20+
21+
webContents.send(AiHandler.StreamChatEnd, {
22+
id: requestId,
23+
usage: usageData,
24+
})
5725
}

src/handlers/ai/types.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export enum AiHandler {
44
StreamChat = 'ai:streamChat',
55
StreamChatChunk = 'ai:streamChatChunk',
66
StreamChatEnd = 'ai:streamChatEnd',
7-
StreamChatError = 'ai:streamChatError',
87
AbortStreamChat = 'ai:abortStreamChat',
98
}
109

@@ -29,11 +28,6 @@ export interface StreamChatEnd {
2928
usage?: TokenUsage
3029
}
3130

32-
export interface StreamChatError {
33-
id: string
34-
error: string
35-
}
36-
3731
export interface AbortStreamChatRequest {
3832
id: string
3933
}

src/hooks/useSettings.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useMutation, useQuery } from '@tanstack/react-query'
2+
import { useEffect, useRef } from 'react'
23

34
import { queryClient } from '@/utils/query'
45

@@ -32,3 +33,19 @@ export function useBrowserCheck() {
3233
queryFn: window.studio.ui.detectBrowser,
3334
})
3435
}
36+
37+
export function useSettingsChanged(onChanged: () => void) {
38+
const { data } = useSettings()
39+
const prevDataRef = useRef(data)
40+
41+
useEffect(() => {
42+
if (!data) {
43+
return
44+
}
45+
46+
if (prevDataRef.current !== data) {
47+
onChanged()
48+
prevDataRef.current = data
49+
}
50+
}, [data, onChanged])
51+
}

src/views/Generator/AutoCorrelation/AutoCorrelation.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
44
import { useListenProxyData } from '@/hooks/useListenProxyData'
55
import { useGeneratorStore } from '@/store/generator'
66

7+
import { ErrorMessage } from './ErrorMessage'
78
import { IntroductionMessage } from './IntroductionMessage'
89
import { SuggestedRules } from './SuggestedRules'
910
import { TokenUsageIndicator } from './TokenUsageIndicator'
@@ -34,7 +35,9 @@ export function AutoCorrelation({
3435
correlationStatus,
3536
outcomeReason,
3637
tokenUsage,
38+
error,
3739
stop,
40+
restart,
3841
} = useGenerateRules({
3942
clearValidation: clearValidation,
4043
})
@@ -71,6 +74,10 @@ export function AutoCorrelation({
7174
return <IntroductionMessage onStart={start} />
7275
}
7376

77+
if (error) {
78+
return <ErrorMessage error={error} onRetry={restart} />
79+
}
80+
7481
return (
7582
<Flex
7683
css={{
@@ -90,7 +97,6 @@ export function AutoCorrelation({
9097
isLoading={isLoading}
9198
onCheckRules={setCheckedRuleIds}
9299
checkedRuleIds={checkedRuleIds}
93-
correlationStatus={correlationStatus}
94100
/>
95101
{outcomeReason !== '' && (
96102
<Box px="3" pt="2">
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { Button, Flex, Text } from '@radix-ui/themes'
2+
import { ExternalLink, KeyIcon, RefreshCw } from 'lucide-react'
3+
4+
import grotCrashed from '@/assets/grot-crashed.svg'
5+
import { useSettingsChanged } from '@/hooks/useSettings'
6+
import { useStudioUIStore } from '@/store/ui'
7+
8+
interface AutoCorrelationErrorProps {
9+
error: Error
10+
onRetry: () => void
11+
}
12+
13+
export function ErrorMessage({ error, onRetry }: AutoCorrelationErrorProps) {
14+
const errorMessage = error.message.toLowerCase()
15+
const openSettingsDialog = useStudioUIStore(
16+
(state) => state.openSettingsDialog
17+
)
18+
19+
useSettingsChanged(() => {
20+
onRetry()
21+
})
22+
23+
const retryButton = (
24+
<Button onClick={onRetry}>
25+
<RefreshCw />
26+
Retry
27+
</Button>
28+
)
29+
30+
const openSettingsButton = (
31+
<Button onClick={() => openSettingsDialog('ai')}>
32+
<KeyIcon />
33+
Settings
34+
</Button>
35+
)
36+
37+
const reportIssueButton = (
38+
<Button onClick={() => window.studio.ui.reportIssue()} variant="outline">
39+
<ExternalLink />
40+
Report issue
41+
</Button>
42+
)
43+
44+
if (errorMessage.includes('incorrect api key')) {
45+
return (
46+
<MessageContent
47+
title="Incorrect API key"
48+
message="The OpenAI API key is incorrect or has been revoked. Check your API key in settings."
49+
>
50+
{openSettingsButton}
51+
</MessageContent>
52+
)
53+
}
54+
55+
if (errorMessage.includes('context window')) {
56+
return (
57+
<MessageContent
58+
title="Token usage limit exceeded"
59+
message="The recording exceeds the token limit. Try reducing the number of allowed hosts in your recording or work with a smaller recording."
60+
/>
61+
)
62+
}
63+
64+
if (errorMessage.includes('rate limit')) {
65+
return (
66+
<MessageContent
67+
title="Too many requests"
68+
message="You have exceeded the API rate limit. Wait a moment and try again."
69+
>
70+
{retryButton}
71+
</MessageContent>
72+
)
73+
}
74+
75+
return (
76+
<MessageContent
77+
title="Something went wrong"
78+
message="An unexpected error occurred during auto-correlation. Click retry to try again or report an issue if problem persists."
79+
>
80+
{retryButton}
81+
{reportIssueButton}
82+
</MessageContent>
83+
)
84+
}
85+
86+
function MessageContent({
87+
title,
88+
message,
89+
children,
90+
}: {
91+
title: string
92+
message: React.ReactNode
93+
children?: React.ReactNode
94+
}) {
95+
return (
96+
<Flex
97+
direction="column"
98+
align="center"
99+
gap="6"
100+
justify="center"
101+
height="100%"
102+
p="6"
103+
>
104+
<img
105+
css={{
106+
maxWidth: '200px',
107+
transform: 'scaleX(-1)',
108+
}}
109+
src={grotCrashed}
110+
aria-label="Error illustration"
111+
/>
112+
113+
<Flex direction="column" align="center" gap="3" maxWidth="400px">
114+
<Text size="4" weight="medium" align="center">
115+
{title}
116+
</Text>
117+
118+
<Text size="2" color="gray" align="center">
119+
{message}
120+
</Text>
121+
122+
<Flex gap="3" mt="4">
123+
{children}
124+
</Flex>
125+
</Flex>
126+
</Flex>
127+
)
128+
}

src/views/Generator/AutoCorrelation/SuggestedRules.tsx

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,18 @@ import { fadeIn } from '@/utils/animations'
77
import { TestRuleInlineContent } from '../TestRuleContainer/TestRule/TestRuleInlineContent'
88
import { TestRuleTypeBadge } from '../TestRuleContainer/TestRule/TestRuleTypeBadge'
99

10-
import { CorrelationStatus } from './types'
11-
1210
interface SuggestedRulesProps {
1311
suggestedRules: CorrelationRule[]
1412
isLoading: boolean
1513
checkedRuleIds: string[]
1614
onCheckRules: (ruleIds: string[]) => void
17-
correlationStatus: CorrelationStatus
1815
}
1916

2017
export function SuggestedRules({
2118
suggestedRules,
2219
isLoading,
2320
checkedRuleIds,
2421
onCheckRules,
25-
correlationStatus,
2622
}: SuggestedRulesProps) {
2723
const allChecked =
2824
suggestedRules.length > 0 && checkedRuleIds.length === suggestedRules.length
@@ -35,22 +31,18 @@ export function SuggestedRules({
3531
}
3632
}
3733

38-
if (
39-
['not-started', 'correlation-not-needed', 'error'].includes(
40-
correlationStatus
41-
)
42-
) {
43-
return null
44-
}
45-
46-
if (suggestedRules.length === 0) {
34+
if (isLoading && suggestedRules.length === 0) {
4735
return (
4836
<Flex height="100%" align="center" justify="center">
4937
<Text color="gray">Your rules will appear here</Text>
5038
</Flex>
5139
)
5240
}
5341

42+
if (!isLoading && suggestedRules.length === 0) {
43+
return null
44+
}
45+
5446
return (
5547
<Box p="3">
5648
<Label mb="2">

0 commit comments

Comments
 (0)