Skip to content

Commit 04c1970

Browse files
committed
Style Markdown
1 parent e690a0e commit 04c1970

File tree

19 files changed

+1853
-110
lines changed

19 files changed

+1853
-110
lines changed

package-lock.json

Lines changed: 1395 additions & 78 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
"react-helmet": "^6.1.0",
142142
"react-hook-form": "^7.48.2",
143143
"react-lottie": "^1.2.10",
144+
"react-markdown": "^10.1.0",
144145
"react-product-fruits": "^2.2.61",
145146
"react-redux": "^9.2.0",
146147
"react-router": "^7.5.2",

src/components/Agentic/IncidentDetails/AdditionalInfo/RelatedIssues/index.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useGetIncidentQuery } from "../../../../../redux/services/digma";
1010
import type { GenericIncidentIssue } from "../../../../../redux/services/types";
1111
import { getIdeLauncherLinkForSpan } from "../../../../../utils/getIdeLauncherLinkForSpan";
1212
import { getInsightTypeInfo } from "../../../../../utils/getInsightTypeInfo";
13+
import { roundTo } from "../../../../../utils/roundTo";
1314
import { Tooltip } from "../../../../common/v3/Tooltip";
1415
import {
1516
getTagType,
@@ -59,7 +60,10 @@ export const RelatedIssues = () => {
5960
);
6061

6162
const issues = useMemo(
62-
() => data?.related_issues ?? [],
63+
() =>
64+
[...(data?.related_issues ?? [])].sort(
65+
(a, b) => b.criticality - a.criticality
66+
),
6367
[data?.related_issues]
6468
);
6569

@@ -78,8 +82,8 @@ export const RelatedIssues = () => {
7882
const label = isInsightIncidentIssue(issue)
7983
? insightTypeInfo?.label
8084
: isErrorIncidentIssue(issue)
81-
? issue.issue_type
82-
: undefined;
85+
? issue.issue_type
86+
: undefined;
8387

8488
return (
8589
<s.IssueInfoContainer>
@@ -91,7 +95,11 @@ export const RelatedIssues = () => {
9195
)}
9296
<Tooltip title={label}>
9397
{issue.span_uid ? (
94-
<s.Link href={getIdeLauncherLinkForSpan(issue.span_uid)}>
98+
<s.Link
99+
href={getIdeLauncherLinkForSpan(issue.span_uid)}
100+
target={"_blank"}
101+
rel={"noopener noreferrer"}
102+
>
95103
{label}
96104
</s.Link>
97105
) : (
@@ -110,12 +118,13 @@ export const RelatedIssues = () => {
110118
},
111119
cell: (info) => {
112120
const issue = info.getValue();
121+
const tagTitle = `${roundTo(issue.criticality * 100, 0)}%`;
113122
const tagLabel = getValueLabel(issue.criticality);
114123
const tagType = getTagType(tagLabel);
115124

116125
return (
117126
<s.CriticalityTag
118-
title={issue.criticality}
127+
title={tagTitle}
119128
type={tagType}
120129
content={<s.CriticalityLabel>{tagLabel}</s.CriticalityLabel>}
121130
/>

src/components/Agentic/IncidentDetails/AdditionalInfo/RelatedIssues/styles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const Link = styled(CommonLink)`
3535
export const CriticalityTag = styled(Tag)`
3636
width: 68px;
3737
max-width: 68px;
38+
height: 24px;
3839
`;
3940

4041
export const CriticalityLabel = styled.span`
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useState } from "react";
2+
import { ChevronIcon } from "../../../../common/icons/16px/ChevronIcon";
3+
import { Direction } from "../../../../common/icons/types";
4+
import * as s from "./styles";
5+
import type { AccordionProps } from "./types";
6+
7+
export const Accordion = ({ summary, content }: AccordionProps) => {
8+
const [isOpen, setIsOpen] = useState(false);
9+
10+
const handleSummaryClick = () => {
11+
setIsOpen((prev) => !prev);
12+
};
13+
14+
return (
15+
<s.Container>
16+
<s.Summary onClick={handleSummaryClick}>
17+
<s.IconContainer>
18+
<ChevronIcon
19+
color={"currentColor"}
20+
size={16}
21+
direction={isOpen ? Direction.Right : Direction.Down}
22+
/>
23+
</s.IconContainer>
24+
{summary}
25+
</s.Summary>
26+
{isOpen && <s.Content>{content}</s.Content>}
27+
</s.Container>
28+
);
29+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import styled from "styled-components";
2+
3+
export const Container = styled.div`
4+
border: 1px solid ${({ theme }) => theme.colors.v3.stroke.primary};
5+
border-radius: 8px;
6+
flex-shrink: 0;
7+
overflow: hidden;
8+
`;
9+
10+
export const Summary = styled.div`
11+
background-color: ${({ theme }) => theme.colors.v3.surface.secondary};
12+
color: ${({ theme }) => theme.colors.v3.text.primary};
13+
padding: 8px;
14+
cursor: pointer;
15+
display: flex;
16+
gap: 8px;
17+
align-items: center;
18+
`;
19+
20+
export const IconContainer = styled.div`
21+
display: flex;
22+
flex-shrink: 0;
23+
`;
24+
25+
export const Content = styled.div`
26+
padding: 8px;
27+
`;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { ReactNode } from "react";
2+
3+
export interface AccordionProps {
4+
summary: ReactNode;
5+
content: ReactNode;
6+
}
Lines changed: 173 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,205 @@
1+
import { useEffect, useMemo, useRef, useState } from "react";
12
import { Fragment } from "react/jsx-runtime";
23
import { useAgenticSelector } from "../../../../containers/Agentic/hooks";
3-
import { useGetIncidentAgentEventsQuery } from "../../../../redux/services/digma";
4+
import {
5+
useGetIncidentAgentEventsQuery,
6+
useGetIncidentAgentsQuery
7+
} from "../../../../redux/services/digma";
8+
import type { GetIncidentAgentEventsResponse } from "../../../../redux/services/types";
9+
import { isBoolean } from "../../../../typeGuards/isBoolean";
10+
import { isNumber } from "../../../../typeGuards/isNumber";
11+
import { isUndefined } from "../../../../typeGuards/isUndefined";
412
import { ThreeCirclesSpinner } from "../../../common/ThreeCirclesSpinner";
13+
import { TypingMarkdown } from "../TypingMarkdown";
14+
import { Accordion } from "./Accordion";
515
import * as s from "./styles";
616

717
const REFRESH_INTERVAL = 10 * 1000; // in milliseconds
18+
const AUTO_SCROLL_THRESHOLD = 10; // in pixels
19+
const TYPING_SPEED = 3; // in milliseconds per character
820

9-
const getMessage = (message: string) => {
21+
const convertToMarkdown = (text: string) => {
1022
try {
11-
const parsedMessage: unknown = JSON.parse(message);
12-
return JSON.stringify(parsedMessage, null, 2);
23+
// First try to parse as JSON
24+
const parsedJSON = JSON.parse(text) as unknown;
25+
const formattedJSON = JSON.stringify(parsedJSON, null, 2);
26+
return `\`\`\`json\n${formattedJSON}\n\`\`\``;
1327
} catch {
14-
return message;
28+
// If JSON parsing fails, check if it looks like structured data
29+
const trimmed = text.trim();
30+
31+
// Check for Python list/object representation patterns
32+
if (
33+
(trimmed.startsWith("[") && trimmed.endsWith("]")) ||
34+
(trimmed.startsWith("(") && trimmed.endsWith(")")) ||
35+
(trimmed.includes("(") && trimmed.includes("=")) // Constructor-like syntax
36+
) {
37+
return `\`\`\`python\n${text}\n\`\`\``;
38+
}
39+
40+
return text;
1541
}
1642
};
1743

1844
export const AgentEvents = () => {
1945
const incidentId = useAgenticSelector((state) => state.incidents.incidentId);
2046
const agentId = useAgenticSelector((state) => state.incidents.agentId);
47+
const [initialAgentRunning, setInitialAgentRunning] = useState<boolean>();
48+
const [eventsVisibleCount, setEventsVisibleCount] = useState<number>();
49+
const [data, setData] = useState<GetIncidentAgentEventsResponse>();
50+
const containerRef = useRef<HTMLDivElement | null>(null);
51+
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
52+
const scrollHeightRef = useRef<number>(0);
2153

22-
const { data, isLoading } = useGetIncidentAgentEventsQuery(
54+
const { data: agentsData } = useGetIncidentAgentsQuery(
55+
{ id: incidentId ?? "" },
56+
{
57+
pollingInterval: REFRESH_INTERVAL,
58+
skip: !incidentId
59+
}
60+
);
61+
62+
const { data: agentEventsData } = useGetIncidentAgentEventsQuery(
2363
{ incidentId: incidentId ?? "", agentId: agentId ?? "" },
2464
{
2565
pollingInterval: REFRESH_INTERVAL,
26-
skip: !incidentId || !agentId
66+
skip: !incidentId || !agentId || !isBoolean(initialAgentRunning)
2767
}
2868
);
2969

30-
if (isLoading) {
31-
return <ThreeCirclesSpinner />;
32-
}
70+
const handleMarkdownTypingComplete = (i: number) => () => {
71+
const events = data ?? [];
72+
const tokenEventsIndexes = events.reduce((acc, event, index) => {
73+
if (event.type === "token") {
74+
acc.push(index);
75+
}
76+
return acc;
77+
}, [] as number[]);
78+
79+
const nextTokenEventIndex = tokenEventsIndexes.find((el) => el > i);
80+
81+
if (isNumber(nextTokenEventIndex) && nextTokenEventIndex >= 0) {
82+
setEventsVisibleCount(nextTokenEventIndex + 1);
83+
} else {
84+
setEventsVisibleCount(events.length);
85+
}
86+
};
87+
88+
const handleContainerScroll = () => {
89+
const isAtBottom = () => {
90+
if (!containerRef.current) {
91+
return false;
92+
}
93+
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
94+
return scrollHeight - scrollTop <= clientHeight + AUTO_SCROLL_THRESHOLD;
95+
};
96+
97+
if (!containerRef.current) {
98+
return;
99+
}
100+
101+
setShouldAutoScroll(isAtBottom());
102+
};
103+
104+
const scrollToBottom = () => {
105+
if (containerRef.current) {
106+
containerRef.current.scrollTop = containerRef.current.scrollHeight;
107+
}
108+
};
109+
110+
// Handle scroll height changes and auto-scroll
111+
useEffect(() => {
112+
const element = containerRef.current;
113+
if (!element) {
114+
return;
115+
}
116+
117+
const checkScrollHeight = () => {
118+
const currentScrollHeight = element.scrollHeight;
119+
120+
// Only auto-scroll if height has grown and auto-scroll is enabled
121+
if (currentScrollHeight > scrollHeightRef.current && shouldAutoScroll) {
122+
scrollToBottom();
123+
}
124+
125+
scrollHeightRef.current = currentScrollHeight;
126+
};
127+
128+
const mutationObserver = new MutationObserver(() => {
129+
// Use RAF to ensure DOM is updated before measuring
130+
requestAnimationFrame(checkScrollHeight);
131+
});
132+
133+
mutationObserver.observe(element, {
134+
childList: true,
135+
subtree: true,
136+
attributes: true,
137+
characterData: true
138+
});
139+
140+
// Initial setup
141+
scrollHeightRef.current = element.scrollHeight;
142+
scrollToBottom();
143+
144+
return () => {
145+
mutationObserver.disconnect();
146+
};
147+
}, [shouldAutoScroll]);
148+
149+
useEffect(() => {
150+
setData(agentEventsData);
151+
}, [agentEventsData]);
152+
153+
// Set agent initial running state
154+
useEffect(() => {
155+
if (!isBoolean(initialAgentRunning)) {
156+
const agent = agentsData?.agents.find((x) => x.name === agentId);
157+
setInitialAgentRunning(agent?.running);
158+
}
159+
}, [agentsData, agentId, initialAgentRunning]);
160+
161+
// Set initial visible count based on agent initial running state
162+
useEffect(() => {
163+
if (
164+
isBoolean(initialAgentRunning) &&
165+
isUndefined(eventsVisibleCount) &&
166+
data
167+
) {
168+
const initialCount = initialAgentRunning ? 1 : data.length;
169+
setEventsVisibleCount(initialCount);
170+
}
171+
}, [initialAgentRunning, eventsVisibleCount, data]);
172+
173+
const visibleEvents = useMemo(
174+
() => data?.slice(0, eventsVisibleCount) ?? [],
175+
[data, eventsVisibleCount]
176+
);
33177

34178
return (
35-
<s.Container>
36-
{data?.map((x, i) => (
37-
<Fragment key={i}>
179+
<s.Container ref={containerRef} onScroll={handleContainerScroll}>
180+
{visibleEvents.map((x, i) => (
181+
<Fragment key={x.type + i}>
38182
{x.type === "tool" ? (
39-
<details key={i}>
40-
<summary>
41-
{x.mcp_name} {x.tool_name}
42-
</summary>
43-
<s.Message>{getMessage(x.message)}</s.Message>
44-
</details>
183+
<Accordion
184+
summary={`${x.tool_name} (${[x.mcp_name, "MCP tool"]
185+
.filter(Boolean)
186+
.join(" ")})`}
187+
content={<TypingMarkdown text={convertToMarkdown(x.message)} />}
188+
/>
45189
) : (
46-
<s.Message>{x.message}</s.Message>
190+
<TypingMarkdown
191+
text={x.message}
192+
onComplete={
193+
initialAgentRunning
194+
? handleMarkdownTypingComplete(i)
195+
: undefined
196+
}
197+
speed={initialAgentRunning ? TYPING_SPEED : undefined}
198+
/>
47199
)}
48200
</Fragment>
49201
))}
202+
{initialAgentRunning && <ThreeCirclesSpinner />}
50203
</s.Container>
51204
);
52205
};

src/components/Agentic/IncidentDetails/AgentEvents/styles.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,24 @@ export const Container = styled.div`
44
display: flex;
55
flex-direction: column;
66
overflow: auto;
7+
gap: 8px;
78
`;
89

9-
export const Message = styled.pre`
10-
white-space: pre-wrap;
11-
word-break: break-word;
10+
export const ToolContainer = styled.details`
11+
border: 1px solid ${({ theme }) => theme.colors.v3.stroke.primary};
12+
border-radius: 8px;
13+
overflow: hidden;
14+
flex-shrink: 0;
15+
`;
16+
17+
export const ToolSummary = styled.summary`
18+
background-color: ${({ theme }) => theme.colors.v3.surface.secondary};
19+
padding: 12px 16px;
20+
font-weight: 600;
21+
color: ${({ theme }) => theme.colors.v3.text.primary};
22+
cursor: pointer;
23+
`;
24+
25+
export const ToolContent = styled.div`
26+
padding: 16px;
1227
`;

0 commit comments

Comments
 (0)