@inkeep/agents-manage-ui 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{.env.sample → .env.example} +6 -1
- package/.turbo/turbo-build.log +27 -33
- package/.turbo/turbo-test.log +43 -10
- package/LICENSE.md +49 -0
- package/package.json +16 -16
- package/src/app/api/signoz/conversations/[conversationId]/route.ts +43 -4
- package/src/app/api/signoz/route.ts +8 -6
- package/src/components/api-keys/form/api-key-form.tsx +2 -0
- package/src/components/api-keys/form/validation.ts +1 -1
- package/src/components/artifact-components/form/artifact-component-form.tsx +5 -0
- package/src/components/artifact-components/form/validation.ts +3 -3
- package/src/components/credentials/views/credential-form-validation.ts +1 -1
- package/src/components/credentials/views/credential-form.tsx +2 -0
- package/src/components/credentials/views/edit-credential-form.tsx +1 -1
- package/src/components/credentials/views/generic-auth-form.tsx +1 -1
- package/src/components/data-components/form/data-component-form.tsx +10 -1
- package/src/components/data-components/form/validation.ts +3 -2
- package/src/components/form/expandable-field.tsx +6 -1
- package/src/components/form/form-field-wrapper.tsx +3 -1
- package/src/components/form/generic-combo-box.tsx +3 -1
- package/src/components/form/generic-input.tsx +9 -1
- package/src/components/form/generic-select.tsx +3 -1
- package/src/components/form/generic-textarea.tsx +3 -1
- package/src/components/form/json-schema-input.tsx +9 -1
- package/src/components/graph/playground/chat-widget.tsx +30 -1
- package/src/components/graph/playground/playground.tsx +7 -1
- package/src/components/graph/sidepane/metadata/metadata-editor.tsx +38 -38
- package/src/components/graph/sidepane/nodes/agent-node-editor.tsx +22 -8
- package/src/components/graph/sidepane/nodes/expandable-text-area.tsx +3 -1
- package/src/components/graph/sidepane/nodes/form-fields.tsx +10 -1
- package/src/components/mcp-servers/form/mcp-server-form.tsx +8 -1
- package/src/components/mcp-servers/form/validation.ts +1 -1
- package/src/components/projects/edit-project-dialog.tsx +1 -1
- package/src/components/projects/form/project-form.tsx +3 -0
- package/src/components/projects/form/validation.ts +14 -10
- package/src/components/projects/new-project-dialog.tsx +1 -1
- package/src/components/traces/timeline/timeline-wrapper.tsx +117 -28
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/form.tsx +11 -4
- package/src/hooks/use-chat-activities-polling.ts +44 -11
- package/src/lib/api/signoz-stats.ts +958 -304
- package/src/lib/types/graph-full.ts +1 -1
- package/src/lib/validation.ts +1 -1
|
@@ -12,9 +12,11 @@ import { TOOL_TYPES } from '@/components/traces/timeline/types';
|
|
|
12
12
|
import { renderPanelContent } from '@/components/traces/timeline/render-panel-content';
|
|
13
13
|
import { StickToBottom } from 'use-stick-to-bottom';
|
|
14
14
|
import { ResizableHandle, ResizablePanel } from '@/components/ui/resizable';
|
|
15
|
-
import { Loader2, ChevronDown, ChevronUp } from 'lucide-react';
|
|
15
|
+
import { Loader2, ChevronDown, ChevronUp, RefreshCw, AlertTriangle } from 'lucide-react';
|
|
16
16
|
import { ConversationTracesLink } from '@/components/traces/signoz-link';
|
|
17
17
|
import { Button } from '@/components/ui/button';
|
|
18
|
+
import { Alert, AlertTitle } from '@/components/ui/alert';
|
|
19
|
+
import { toast } from 'sonner';
|
|
18
20
|
|
|
19
21
|
function panelTitle(selected: SelectedPanel) {
|
|
20
22
|
switch (selected.type) {
|
|
@@ -49,9 +51,42 @@ interface TimelineWrapperProps {
|
|
|
49
51
|
conversation?: ConversationDetail | null;
|
|
50
52
|
enableAutoScroll?: boolean;
|
|
51
53
|
isPolling?: boolean;
|
|
54
|
+
error?: string | null;
|
|
55
|
+
retryConnection?: () => void;
|
|
56
|
+
refreshOnce?: () => Promise<{ hasNewActivity: boolean }>;
|
|
57
|
+
showConversationTracesLink?: boolean;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
function EmptyTimeline({
|
|
60
|
+
function EmptyTimeline({
|
|
61
|
+
isPolling,
|
|
62
|
+
error,
|
|
63
|
+
retryConnection,
|
|
64
|
+
}: {
|
|
65
|
+
isPolling: boolean;
|
|
66
|
+
error?: string | null;
|
|
67
|
+
retryConnection?: () => void;
|
|
68
|
+
}) {
|
|
69
|
+
if (error) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex flex-col gap-4 h-full justify-center items-center px-6">
|
|
72
|
+
<Alert variant="destructive" className="max-w-md">
|
|
73
|
+
<AlertTriangle className="h-4 w-4" />
|
|
74
|
+
<AlertTitle>{error}</AlertTitle>
|
|
75
|
+
</Alert>
|
|
76
|
+
{retryConnection && (
|
|
77
|
+
<Button
|
|
78
|
+
variant="outline"
|
|
79
|
+
size="sm"
|
|
80
|
+
onClick={retryConnection}
|
|
81
|
+
className="flex items-center gap-2"
|
|
82
|
+
>
|
|
83
|
+
<RefreshCw className="h-4 w-4" />
|
|
84
|
+
Retry Connection
|
|
85
|
+
</Button>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
55
90
|
return (
|
|
56
91
|
<div className="flex flex-col gap-2 h-full justify-center items-center">
|
|
57
92
|
{isPolling ? (
|
|
@@ -72,13 +107,19 @@ export function TimelineWrapper({
|
|
|
72
107
|
conversation,
|
|
73
108
|
enableAutoScroll = false,
|
|
74
109
|
isPolling = false,
|
|
110
|
+
error,
|
|
111
|
+
retryConnection,
|
|
112
|
+
refreshOnce,
|
|
113
|
+
showConversationTracesLink = false,
|
|
75
114
|
}: TimelineWrapperProps) {
|
|
76
115
|
const [selected, setSelected] = useState<SelectedPanel | null>(null);
|
|
77
116
|
const [panelVisible, setPanelVisible] = useState(false);
|
|
78
|
-
|
|
117
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
118
|
+
|
|
79
119
|
// State for collapsible AI messages
|
|
80
120
|
const [collapsedAiMessages, setCollapsedAiMessages] = useState<Set<string>>(new Set());
|
|
81
|
-
const [aiMessagesGloballyCollapsed, setAiMessagesGloballyCollapsed] =
|
|
121
|
+
const [aiMessagesGloballyCollapsed, setAiMessagesGloballyCollapsed] =
|
|
122
|
+
useState<boolean>(enableAutoScroll);
|
|
82
123
|
|
|
83
124
|
useEffect(() => {
|
|
84
125
|
if (selected) {
|
|
@@ -117,12 +158,13 @@ export function TimelineWrapper({
|
|
|
117
158
|
// Initialize AI messages based on view type when activities change
|
|
118
159
|
useEffect(() => {
|
|
119
160
|
const aiMessageIds = sortedActivities
|
|
120
|
-
.filter(
|
|
121
|
-
activity
|
|
122
|
-
|
|
161
|
+
.filter(
|
|
162
|
+
(activity) =>
|
|
163
|
+
activity.type === ACTIVITY_TYPES.AI_ASSISTANT_MESSAGE ||
|
|
164
|
+
activity.type === ACTIVITY_TYPES.AI_MODEL_STREAMED_TEXT
|
|
123
165
|
)
|
|
124
|
-
.map(activity => activity.id);
|
|
125
|
-
|
|
166
|
+
.map((activity) => activity.id);
|
|
167
|
+
|
|
126
168
|
if (enableAutoScroll) {
|
|
127
169
|
// Live trace view: default collapsed
|
|
128
170
|
setCollapsedAiMessages(new Set(aiMessageIds));
|
|
@@ -133,7 +175,7 @@ export function TimelineWrapper({
|
|
|
133
175
|
setAiMessagesGloballyCollapsed(false);
|
|
134
176
|
}
|
|
135
177
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
136
|
-
}, [sortedActivities.length, enableAutoScroll]); //
|
|
178
|
+
}, [sortedActivities.length, enableAutoScroll]); // Run when activities count or view type changes
|
|
137
179
|
|
|
138
180
|
// Functions to handle expand/collapse all
|
|
139
181
|
const expandAllAiMessages = () => {
|
|
@@ -143,11 +185,12 @@ export function TimelineWrapper({
|
|
|
143
185
|
|
|
144
186
|
const collapseAllAiMessages = () => {
|
|
145
187
|
const aiMessageIds = sortedActivities
|
|
146
|
-
.filter(
|
|
147
|
-
activity
|
|
148
|
-
|
|
188
|
+
.filter(
|
|
189
|
+
(activity) =>
|
|
190
|
+
activity.type === ACTIVITY_TYPES.AI_ASSISTANT_MESSAGE ||
|
|
191
|
+
activity.type === ACTIVITY_TYPES.AI_MODEL_STREAMED_TEXT
|
|
149
192
|
)
|
|
150
|
-
.map(activity => activity.id);
|
|
193
|
+
.map((activity) => activity.id);
|
|
151
194
|
setCollapsedAiMessages(new Set(aiMessageIds));
|
|
152
195
|
setAiMessagesGloballyCollapsed(true);
|
|
153
196
|
};
|
|
@@ -160,15 +203,16 @@ export function TimelineWrapper({
|
|
|
160
203
|
newCollapsed.add(activityId);
|
|
161
204
|
}
|
|
162
205
|
setCollapsedAiMessages(newCollapsed);
|
|
163
|
-
|
|
206
|
+
|
|
164
207
|
// Update global state based on current state
|
|
165
208
|
const aiMessageIds = sortedActivities
|
|
166
|
-
.filter(
|
|
167
|
-
activity
|
|
168
|
-
|
|
209
|
+
.filter(
|
|
210
|
+
(activity) =>
|
|
211
|
+
activity.type === ACTIVITY_TYPES.AI_ASSISTANT_MESSAGE ||
|
|
212
|
+
activity.type === ACTIVITY_TYPES.AI_MODEL_STREAMED_TEXT
|
|
169
213
|
)
|
|
170
|
-
.map(activity => activity.id);
|
|
171
|
-
const allCollapsed = aiMessageIds.every(id => newCollapsed.has(id));
|
|
214
|
+
.map((activity) => activity.id);
|
|
215
|
+
const allCollapsed = aiMessageIds.every((id) => newCollapsed.has(id));
|
|
172
216
|
setAiMessagesGloballyCollapsed(allCollapsed);
|
|
173
217
|
};
|
|
174
218
|
|
|
@@ -183,7 +227,8 @@ export function TimelineWrapper({
|
|
|
183
227
|
);
|
|
184
228
|
|
|
185
229
|
const determinePanelType = (a: ActivityItem): Exclude<PanelType, 'mcp_tool_error'> => {
|
|
186
|
-
if (a.type === ACTIVITY_TYPES.TOOL_CALL && a.toolType === TOOL_TYPES.TRANSFER)
|
|
230
|
+
if (a.type === ACTIVITY_TYPES.TOOL_CALL && a.toolType === TOOL_TYPES.TRANSFER)
|
|
231
|
+
return 'transfer';
|
|
187
232
|
if (a.type === ACTIVITY_TYPES.TOOL_CALL && a.toolName?.includes('delegate'))
|
|
188
233
|
return 'delegation';
|
|
189
234
|
if (
|
|
@@ -196,6 +241,21 @@ export function TimelineWrapper({
|
|
|
196
241
|
return a.type;
|
|
197
242
|
};
|
|
198
243
|
|
|
244
|
+
const handleRefresh = async () => {
|
|
245
|
+
if (!refreshOnce || isRefreshing) return;
|
|
246
|
+
setIsRefreshing(true);
|
|
247
|
+
try {
|
|
248
|
+
const result = await refreshOnce();
|
|
249
|
+
if (!result.hasNewActivity) {
|
|
250
|
+
toast.info('No new activity found');
|
|
251
|
+
}
|
|
252
|
+
setIsRefreshing(false);
|
|
253
|
+
} catch {
|
|
254
|
+
toast.error('Failed to refresh activities');
|
|
255
|
+
setIsRefreshing(false);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
199
259
|
return (
|
|
200
260
|
<>
|
|
201
261
|
<ResizablePanel order={2}>
|
|
@@ -205,17 +265,24 @@ export function TimelineWrapper({
|
|
|
205
265
|
<div className="text-foreground text-md font-medium">Activity Timeline</div>
|
|
206
266
|
<div className="flex items-center gap-2">
|
|
207
267
|
{/* Expand/Collapse AI Messages Buttons */}
|
|
208
|
-
{sortedActivities.some(
|
|
209
|
-
activity
|
|
210
|
-
|
|
268
|
+
{sortedActivities.some(
|
|
269
|
+
(activity) =>
|
|
270
|
+
activity.type === ACTIVITY_TYPES.AI_ASSISTANT_MESSAGE ||
|
|
271
|
+
activity.type === ACTIVITY_TYPES.AI_MODEL_STREAMED_TEXT
|
|
211
272
|
) && (
|
|
212
273
|
<div className="flex items-center gap-1">
|
|
213
274
|
<Button
|
|
214
275
|
variant="ghost"
|
|
215
276
|
size="sm"
|
|
216
|
-
onClick={
|
|
277
|
+
onClick={
|
|
278
|
+
aiMessagesGloballyCollapsed ? expandAllAiMessages : collapseAllAiMessages
|
|
279
|
+
}
|
|
217
280
|
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
|
|
218
|
-
title={
|
|
281
|
+
title={
|
|
282
|
+
aiMessagesGloballyCollapsed
|
|
283
|
+
? 'Expand all AI messages'
|
|
284
|
+
: 'Collapse all AI messages'
|
|
285
|
+
}
|
|
219
286
|
>
|
|
220
287
|
{aiMessagesGloballyCollapsed ? (
|
|
221
288
|
<>
|
|
@@ -231,7 +298,7 @@ export function TimelineWrapper({
|
|
|
231
298
|
</Button>
|
|
232
299
|
</div>
|
|
233
300
|
)}
|
|
234
|
-
{conversation?.conversationId && (
|
|
301
|
+
{showConversationTracesLink && conversation?.conversationId && (
|
|
235
302
|
<ConversationTracesLink conversationId={conversation.conversationId} />
|
|
236
303
|
)}
|
|
237
304
|
</div>
|
|
@@ -239,7 +306,11 @@ export function TimelineWrapper({
|
|
|
239
306
|
</div>
|
|
240
307
|
<div className="p-0 flex-1 min-h-0">
|
|
241
308
|
{sortedActivities.length === 0 ? (
|
|
242
|
-
<EmptyTimeline
|
|
309
|
+
<EmptyTimeline
|
|
310
|
+
isPolling={isPolling}
|
|
311
|
+
error={error}
|
|
312
|
+
retryConnection={retryConnection}
|
|
313
|
+
/>
|
|
243
314
|
) : enableAutoScroll ? (
|
|
244
315
|
<StickToBottom
|
|
245
316
|
className="h-full [&>div]:overflow-y-auto [&>div]:scrollbar-thin [&>div]:scrollbar-thumb-muted-foreground/30 [&>div]:scrollbar-track-transparent dark:[&>div]:scrollbar-thumb-muted-foreground/50"
|
|
@@ -255,6 +326,24 @@ export function TimelineWrapper({
|
|
|
255
326
|
collapsedAiMessages={collapsedAiMessages}
|
|
256
327
|
onToggleAiMessageCollapse={toggleAiMessageCollapse}
|
|
257
328
|
/>
|
|
329
|
+
{!isPolling && sortedActivities.length > 0 && !error && refreshOnce && (
|
|
330
|
+
<div className="flex justify-center items-center z-10">
|
|
331
|
+
<Button
|
|
332
|
+
variant="outline"
|
|
333
|
+
size="sm"
|
|
334
|
+
onClick={handleRefresh}
|
|
335
|
+
disabled={isRefreshing}
|
|
336
|
+
className=" text-xs bg-background/80 backdrop-blur-sm hover:bg-background/90 transition-all duration-200 opacity-70 hover:opacity-100"
|
|
337
|
+
>
|
|
338
|
+
{isRefreshing ? (
|
|
339
|
+
<Loader2 className="h-3 w-3 mr-1.5 animate-spin" />
|
|
340
|
+
) : (
|
|
341
|
+
<RefreshCw className="h-3 w-3 mr-1.5" />
|
|
342
|
+
)}
|
|
343
|
+
{isRefreshing ? 'Refreshing...' : 'Refresh'}
|
|
344
|
+
</Button>
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
258
347
|
</StickToBottom.Content>
|
|
259
348
|
</StickToBottom>
|
|
260
349
|
) : (
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const alertVariants = cva(
|
|
7
|
+
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-card text-card-foreground",
|
|
12
|
+
destructive:
|
|
13
|
+
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: "default",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
function Alert({
|
|
23
|
+
className,
|
|
24
|
+
variant,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
data-slot="alert"
|
|
30
|
+
role="alert"
|
|
31
|
+
className={cn(alertVariants({ variant }), className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
data-slot="alert-title"
|
|
41
|
+
className={cn(
|
|
42
|
+
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function AlertDescription({
|
|
51
|
+
className,
|
|
52
|
+
...props
|
|
53
|
+
}: React.ComponentProps<"div">) {
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
data-slot="alert-description"
|
|
57
|
+
className={cn(
|
|
58
|
+
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { Alert, AlertTitle, AlertDescription }
|
|
@@ -78,17 +78,24 @@ function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function FormLabel({
|
|
81
|
+
function FormLabel({
|
|
82
|
+
className,
|
|
83
|
+
isRequired,
|
|
84
|
+
children,
|
|
85
|
+
...props
|
|
86
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root> & { isRequired?: boolean }) {
|
|
82
87
|
const { error, formItemId } = useFormField();
|
|
83
|
-
|
|
84
88
|
return (
|
|
85
89
|
<Label
|
|
86
90
|
data-slot="form-label"
|
|
87
91
|
data-error={!!error}
|
|
88
|
-
className={cn('data-[error=true]:text-destructive', className)}
|
|
92
|
+
className={cn('data-[error=true]:text-destructive gap-1', className)}
|
|
89
93
|
htmlFor={formItemId}
|
|
90
94
|
{...props}
|
|
91
|
-
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
{isRequired && <span className="text-red-500">*</span>}
|
|
98
|
+
</Label>
|
|
92
99
|
);
|
|
93
100
|
}
|
|
94
101
|
|
|
@@ -12,6 +12,8 @@ interface UseChatActivitiesPollingReturn {
|
|
|
12
12
|
error: string | null;
|
|
13
13
|
startPolling: () => void;
|
|
14
14
|
stopPolling: () => void;
|
|
15
|
+
retryConnection: () => void;
|
|
16
|
+
refreshOnce: () => Promise<{ hasNewActivity: boolean }>;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export const useChatActivitiesPolling = ({
|
|
@@ -27,15 +29,10 @@ export const useChatActivitiesPolling = ({
|
|
|
27
29
|
const isComponentMountedRef = useRef(true);
|
|
28
30
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
29
31
|
|
|
30
|
-
const fetchChatActivities = useCallback(async () => {
|
|
32
|
+
const fetchChatActivities = useCallback(async (): Promise<ConversationDetail | null> => {
|
|
31
33
|
try {
|
|
32
34
|
setError(null);
|
|
33
35
|
|
|
34
|
-
// Cancel any previous request
|
|
35
|
-
if (abortControllerRef.current) {
|
|
36
|
-
abortControllerRef.current.abort();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
36
|
// Create new abort controller for this request
|
|
40
37
|
abortControllerRef.current = new AbortController();
|
|
41
38
|
const currentConversationId = conversationId; // Capture current ID
|
|
@@ -46,7 +43,9 @@ export const useChatActivitiesPolling = ({
|
|
|
46
43
|
|
|
47
44
|
if (!response.ok) {
|
|
48
45
|
// If conversation doesn't exist yet, that's fine - just return
|
|
49
|
-
if (response.status === 404)
|
|
46
|
+
if (response.status === 404) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
50
49
|
throw new Error('Failed to fetch chat activities');
|
|
51
50
|
}
|
|
52
51
|
|
|
@@ -61,16 +60,29 @@ export const useChatActivitiesPolling = ({
|
|
|
61
60
|
setLastActivityCount(newCount);
|
|
62
61
|
}
|
|
63
62
|
}
|
|
63
|
+
|
|
64
|
+
return data;
|
|
64
65
|
} catch (err) {
|
|
65
66
|
// Don't log abort errors as they are expected when cancelling requests
|
|
66
67
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
67
|
-
return;
|
|
68
|
+
return null;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
if (isComponentMountedRef.current) {
|
|
71
|
-
console.warn('Error fetching chat activities:', err);
|
|
72
72
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
73
|
+
// Stop polling on error to prevent repeated failed requests
|
|
74
|
+
setIsPolling(false);
|
|
75
|
+
if (pollingIntervalRef.current) {
|
|
76
|
+
clearInterval(pollingIntervalRef.current);
|
|
77
|
+
pollingIntervalRef.current = null;
|
|
78
|
+
}
|
|
79
|
+
// Cancel any pending requests
|
|
80
|
+
if (abortControllerRef.current) {
|
|
81
|
+
abortControllerRef.current.abort();
|
|
82
|
+
abortControllerRef.current = null;
|
|
83
|
+
}
|
|
73
84
|
}
|
|
85
|
+
throw err;
|
|
74
86
|
}
|
|
75
87
|
}, [conversationId, lastActivityCount]);
|
|
76
88
|
|
|
@@ -81,11 +93,15 @@ export const useChatActivitiesPolling = ({
|
|
|
81
93
|
setIsPolling(true);
|
|
82
94
|
|
|
83
95
|
// Initial fetch
|
|
84
|
-
fetchChatActivities()
|
|
96
|
+
fetchChatActivities().catch(() => {
|
|
97
|
+
// Error handling is already done in fetchChatActivities
|
|
98
|
+
});
|
|
85
99
|
|
|
86
100
|
// Set up polling interval
|
|
87
101
|
pollingIntervalRef.current = setInterval(() => {
|
|
88
|
-
fetchChatActivities()
|
|
102
|
+
fetchChatActivities().catch(() => {
|
|
103
|
+
// Error handling is already done in fetchChatActivities
|
|
104
|
+
});
|
|
89
105
|
}, pollingInterval);
|
|
90
106
|
}, [fetchChatActivities, pollingInterval]);
|
|
91
107
|
|
|
@@ -103,6 +119,21 @@ export const useChatActivitiesPolling = ({
|
|
|
103
119
|
}
|
|
104
120
|
}, []);
|
|
105
121
|
|
|
122
|
+
// Retry connection - clears error and restarts polling
|
|
123
|
+
const retryConnection = useCallback(() => {
|
|
124
|
+
setError(null);
|
|
125
|
+
stopPolling();
|
|
126
|
+
startPolling();
|
|
127
|
+
}, [startPolling, stopPolling]);
|
|
128
|
+
|
|
129
|
+
// Refresh once - makes a single request without starting polling
|
|
130
|
+
const refreshOnce = useCallback(async (): Promise<{ hasNewActivity: boolean }> => {
|
|
131
|
+
const currentCount = chatActivities?.activities?.length || 0;
|
|
132
|
+
const data = await fetchChatActivities();
|
|
133
|
+
const newCount = data?.activities?.length || 0;
|
|
134
|
+
return { hasNewActivity: newCount > currentCount };
|
|
135
|
+
}, [fetchChatActivities, chatActivities?.activities?.length]);
|
|
136
|
+
|
|
106
137
|
// Cleanup on unmount
|
|
107
138
|
useEffect(() => {
|
|
108
139
|
isComponentMountedRef.current = true;
|
|
@@ -141,5 +172,7 @@ export const useChatActivitiesPolling = ({
|
|
|
141
172
|
error,
|
|
142
173
|
startPolling,
|
|
143
174
|
stopPolling,
|
|
175
|
+
retryConnection,
|
|
176
|
+
refreshOnce,
|
|
144
177
|
};
|
|
145
178
|
};
|