@inkeep/agents-manage-ui 0.1.1 → 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.
Files changed (43) hide show
  1. package/{.env.sample → .env.example} +6 -1
  2. package/.turbo/turbo-build.log +27 -33
  3. package/.turbo/turbo-test.log +43 -10
  4. package/LICENSE.md +22 -17
  5. package/package.json +5 -5
  6. package/src/app/api/signoz/conversations/[conversationId]/route.ts +43 -4
  7. package/src/app/api/signoz/route.ts +8 -6
  8. package/src/components/api-keys/form/api-key-form.tsx +2 -0
  9. package/src/components/api-keys/form/validation.ts +1 -1
  10. package/src/components/artifact-components/form/artifact-component-form.tsx +5 -0
  11. package/src/components/artifact-components/form/validation.ts +3 -3
  12. package/src/components/credentials/views/credential-form-validation.ts +1 -1
  13. package/src/components/credentials/views/credential-form.tsx +2 -0
  14. package/src/components/credentials/views/edit-credential-form.tsx +1 -1
  15. package/src/components/credentials/views/generic-auth-form.tsx +1 -1
  16. package/src/components/data-components/form/data-component-form.tsx +10 -1
  17. package/src/components/data-components/form/validation.ts +3 -2
  18. package/src/components/form/expandable-field.tsx +6 -1
  19. package/src/components/form/form-field-wrapper.tsx +3 -1
  20. package/src/components/form/generic-combo-box.tsx +3 -1
  21. package/src/components/form/generic-input.tsx +9 -1
  22. package/src/components/form/generic-select.tsx +3 -1
  23. package/src/components/form/generic-textarea.tsx +3 -1
  24. package/src/components/form/json-schema-input.tsx +9 -1
  25. package/src/components/graph/playground/chat-widget.tsx +30 -1
  26. package/src/components/graph/playground/playground.tsx +7 -1
  27. package/src/components/graph/sidepane/metadata/metadata-editor.tsx +38 -38
  28. package/src/components/graph/sidepane/nodes/agent-node-editor.tsx +22 -8
  29. package/src/components/graph/sidepane/nodes/expandable-text-area.tsx +3 -1
  30. package/src/components/graph/sidepane/nodes/form-fields.tsx +10 -1
  31. package/src/components/mcp-servers/form/mcp-server-form.tsx +8 -1
  32. package/src/components/mcp-servers/form/validation.ts +1 -1
  33. package/src/components/projects/edit-project-dialog.tsx +1 -1
  34. package/src/components/projects/form/project-form.tsx +3 -0
  35. package/src/components/projects/form/validation.ts +14 -10
  36. package/src/components/projects/new-project-dialog.tsx +1 -1
  37. package/src/components/traces/timeline/timeline-wrapper.tsx +117 -28
  38. package/src/components/ui/alert.tsx +66 -0
  39. package/src/components/ui/form.tsx +11 -4
  40. package/src/hooks/use-chat-activities-polling.ts +44 -11
  41. package/src/lib/api/signoz-stats.ts +958 -304
  42. package/src/lib/types/graph-full.ts +1 -1
  43. 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({ isPolling }: { isPolling: boolean }) {
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] = useState<boolean>(enableAutoScroll);
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(activity =>
121
- activity.type === ACTIVITY_TYPES.AI_ASSISTANT_MESSAGE ||
122
- activity.type === ACTIVITY_TYPES.AI_MODEL_STREAMED_TEXT
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]); // Run when activities count or view type changes
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(activity =>
147
- activity.type === ACTIVITY_TYPES.AI_ASSISTANT_MESSAGE ||
148
- activity.type === ACTIVITY_TYPES.AI_MODEL_STREAMED_TEXT
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(activity =>
167
- activity.type === ACTIVITY_TYPES.AI_ASSISTANT_MESSAGE ||
168
- activity.type === ACTIVITY_TYPES.AI_MODEL_STREAMED_TEXT
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) return '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(activity =>
209
- activity.type === ACTIVITY_TYPES.AI_ASSISTANT_MESSAGE ||
210
- activity.type === ACTIVITY_TYPES.AI_MODEL_STREAMED_TEXT
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={aiMessagesGloballyCollapsed ? expandAllAiMessages : collapseAllAiMessages}
277
+ onClick={
278
+ aiMessagesGloballyCollapsed ? expandAllAiMessages : collapseAllAiMessages
279
+ }
217
280
  className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
218
- title={aiMessagesGloballyCollapsed ? "Expand all AI messages" : "Collapse all AI messages"}
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 isPolling={isPolling} />
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({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
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) return;
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
  };