@roj-ai/debug 0.0.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/dist/components/debug/DebugContext.d.ts +10 -0
- package/dist/components/debug/DebugNavigation.d.ts +29 -0
- package/dist/components/debug/DebugShell.d.ts +18 -0
- package/dist/components/debug/LLMCallDetail.d.ts +7 -0
- package/dist/components/debug/TimelineDetailInspector.d.ts +6 -0
- package/dist/components/debug/communication/CommunicationDiagram.d.ts +9 -0
- package/dist/components/debug/communication/DiagramHeader.d.ts +7 -0
- package/dist/components/debug/communication/ParticipantLane.d.ts +7 -0
- package/dist/components/debug/communication/TimeAxis.d.ts +9 -0
- package/dist/components/debug/communication/elements/IdleGap.d.ts +9 -0
- package/dist/components/debug/communication/elements/LLMBlock.d.ts +9 -0
- package/dist/components/debug/communication/elements/MessageArrow.d.ts +10 -0
- package/dist/components/debug/communication/elements/ToolBlock.d.ts +9 -0
- package/dist/components/debug/communication/hooks/useDiagramData.d.ts +12 -0
- package/dist/components/debug/communication/hooks/useTimeCompression.d.ts +7 -0
- package/dist/components/debug/communication/hooks/useZoomPan.d.ts +11 -0
- package/dist/components/debug/communication/popovers/ElementPopover.d.ts +8 -0
- package/dist/components/debug/communication/types.d.ts +136 -0
- package/dist/components/debug/index.d.ts +11 -0
- package/dist/components/debug/pages/AgentDetailPage.d.ts +3 -0
- package/dist/components/debug/pages/AgentsPage.d.ts +1 -0
- package/dist/components/debug/pages/CommunicationPage.d.ts +1 -0
- package/dist/components/debug/pages/DashboardPage.d.ts +1 -0
- package/dist/components/debug/pages/EventsPage.d.ts +1 -0
- package/dist/components/debug/pages/FilesPage.d.ts +1 -0
- package/dist/components/debug/pages/LLMCallPage.d.ts +1 -0
- package/dist/components/debug/pages/LLMCallsPage.d.ts +1 -0
- package/dist/components/debug/pages/LogsPage.d.ts +1 -0
- package/dist/components/debug/pages/MailboxPage.d.ts +1 -0
- package/dist/components/debug/pages/ServicesPage.d.ts +1 -0
- package/dist/components/debug/pages/TimelinePage.d.ts +1 -0
- package/dist/components/debug/pages/UserChatPage.d.ts +1 -0
- package/dist/components/debug/pages/index.d.ts +13 -0
- package/dist/index.d.ts +9 -0
- package/dist/lib/domain-utils.d.ts +7 -0
- package/dist/providers/EventPollingProvider.d.ts +27 -0
- package/dist/stores/event-store.d.ts +93 -0
- package/dist/utils/format.d.ts +1 -0
- package/package.json +43 -0
- package/src/components/debug/DebugContext.tsx +18 -0
- package/src/components/debug/DebugNavigation.tsx +55 -0
- package/src/components/debug/DebugShell.tsx +321 -0
- package/src/components/debug/LLMCallDetail.tsx +740 -0
- package/src/components/debug/TimelineDetailInspector.tsx +204 -0
- package/src/components/debug/communication/CommunicationDiagram.tsx +260 -0
- package/src/components/debug/communication/DiagramHeader.tsx +113 -0
- package/src/components/debug/communication/ParticipantLane.tsx +60 -0
- package/src/components/debug/communication/TimeAxis.tsx +106 -0
- package/src/components/debug/communication/elements/IdleGap.tsx +90 -0
- package/src/components/debug/communication/elements/LLMBlock.tsx +107 -0
- package/src/components/debug/communication/elements/MessageArrow.tsx +119 -0
- package/src/components/debug/communication/elements/ToolBlock.tsx +99 -0
- package/src/components/debug/communication/hooks/useDiagramData.ts +294 -0
- package/src/components/debug/communication/hooks/useTimeCompression.ts +140 -0
- package/src/components/debug/communication/hooks/useZoomPan.ts +87 -0
- package/src/components/debug/communication/popovers/ElementPopover.tsx +158 -0
- package/src/components/debug/communication/types.ts +180 -0
- package/src/components/debug/index.ts +37 -0
- package/src/components/debug/pages/AgentDetailPage.tsx +1295 -0
- package/src/components/debug/pages/AgentsPage.tsx +297 -0
- package/src/components/debug/pages/CommunicationPage.tsx +89 -0
- package/src/components/debug/pages/DashboardPage.tsx +1504 -0
- package/src/components/debug/pages/EventsPage.tsx +276 -0
- package/src/components/debug/pages/FilesPage.tsx +366 -0
- package/src/components/debug/pages/LLMCallPage.tsx +32 -0
- package/src/components/debug/pages/LLMCallsPage.tsx +473 -0
- package/src/components/debug/pages/LogsPage.tsx +199 -0
- package/src/components/debug/pages/MailboxPage.tsx +232 -0
- package/src/components/debug/pages/ServicesPage.tsx +193 -0
- package/src/components/debug/pages/TimelinePage.tsx +569 -0
- package/src/components/debug/pages/UserChatPage.tsx +250 -0
- package/src/components/debug/pages/index.ts +13 -0
- package/src/index.ts +55 -0
- package/src/lib/domain-utils.ts +12 -0
- package/src/providers/EventPollingProvider.tsx +60 -0
- package/src/stores/event-store.ts +497 -0
- package/src/utils/format.ts +8 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { TimeSegment } from './types'
|
|
2
|
+
import { LAYOUT } from './types'
|
|
3
|
+
|
|
4
|
+
interface TimeAxisProps {
|
|
5
|
+
segments: TimeSegment[]
|
|
6
|
+
sessionStartTime: number
|
|
7
|
+
timestampToY: (timestamp: number) => number
|
|
8
|
+
totalHeight: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function TimeAxis({ segments, sessionStartTime, timestampToY, totalHeight }: TimeAxisProps) {
|
|
12
|
+
// Generate time markers every ~80px on active segments
|
|
13
|
+
const markers: Array<{ timestamp: number; y: number; label: string }> = []
|
|
14
|
+
|
|
15
|
+
for (const segment of segments) {
|
|
16
|
+
if (segment.type === 'idle') continue
|
|
17
|
+
|
|
18
|
+
const interval = calculateInterval(segment.actualDuration)
|
|
19
|
+
let current = segment.startTime
|
|
20
|
+
|
|
21
|
+
while (current <= segment.endTime) {
|
|
22
|
+
const y = timestampToY(current)
|
|
23
|
+
// Only add if not too close to previous marker
|
|
24
|
+
const lastMarker = markers[markers.length - 1]
|
|
25
|
+
if (!lastMarker || y - lastMarker.y > 40) {
|
|
26
|
+
markers.push({
|
|
27
|
+
timestamp: current,
|
|
28
|
+
y,
|
|
29
|
+
label: formatTime(current, sessionStartTime),
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
current += interval
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<g className="time-axis">
|
|
38
|
+
{/* Vertical axis line */}
|
|
39
|
+
<line
|
|
40
|
+
x1={LAYOUT.timeAxisWidth - 1}
|
|
41
|
+
y1={0}
|
|
42
|
+
x2={LAYOUT.timeAxisWidth - 1}
|
|
43
|
+
y2={totalHeight}
|
|
44
|
+
className="stroke-slate-100"
|
|
45
|
+
strokeWidth={1}
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
{/* Time markers */}
|
|
49
|
+
{markers.map((marker, idx) => (
|
|
50
|
+
<g key={idx}>
|
|
51
|
+
{/* Tick mark */}
|
|
52
|
+
<line
|
|
53
|
+
x1={LAYOUT.timeAxisWidth - 6}
|
|
54
|
+
y1={marker.y}
|
|
55
|
+
x2={LAYOUT.timeAxisWidth - 1}
|
|
56
|
+
y2={marker.y}
|
|
57
|
+
className="stroke-slate-200"
|
|
58
|
+
strokeWidth={1}
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
{/* Time label */}
|
|
62
|
+
<text
|
|
63
|
+
x={LAYOUT.timeAxisWidth - 10}
|
|
64
|
+
y={marker.y}
|
|
65
|
+
textAnchor="end"
|
|
66
|
+
dominantBaseline="middle"
|
|
67
|
+
className="text-[9px] text-slate-400 fill-current font-mono"
|
|
68
|
+
>
|
|
69
|
+
{marker.label}
|
|
70
|
+
</text>
|
|
71
|
+
|
|
72
|
+
{/* Subtle horizontal guide line */}
|
|
73
|
+
<line
|
|
74
|
+
x1={LAYOUT.timeAxisWidth}
|
|
75
|
+
y1={marker.y}
|
|
76
|
+
x2={LAYOUT.timeAxisWidth + 2000}
|
|
77
|
+
y2={marker.y}
|
|
78
|
+
className="stroke-slate-50"
|
|
79
|
+
strokeWidth={1}
|
|
80
|
+
/>
|
|
81
|
+
</g>
|
|
82
|
+
))}
|
|
83
|
+
</g>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function calculateInterval(durationMs: number): number {
|
|
88
|
+
// Choose interval based on duration to get ~5-10 markers per segment
|
|
89
|
+
if (durationMs < 10000) return 2000 // Every 2s for < 10s
|
|
90
|
+
if (durationMs < 60000) return 10000 // Every 10s for < 1min
|
|
91
|
+
if (durationMs < 300000) return 30000 // Every 30s for < 5min
|
|
92
|
+
if (durationMs < 3600000) return 60000 // Every 1min for < 1h
|
|
93
|
+
return 300000 // Every 5min for > 1h
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatTime(timestamp: number, sessionStartTime: number): string {
|
|
97
|
+
const elapsed = timestamp - sessionStartTime
|
|
98
|
+
const seconds = Math.floor(elapsed / 1000)
|
|
99
|
+
const minutes = Math.floor(seconds / 60)
|
|
100
|
+
const hours = Math.floor(minutes / 60)
|
|
101
|
+
|
|
102
|
+
if (hours > 0) {
|
|
103
|
+
return `${hours}:${String(minutes % 60).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`
|
|
104
|
+
}
|
|
105
|
+
return `${minutes}:${String(seconds % 60).padStart(2, '0')}`
|
|
106
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { DiagramParticipant, TimeSegment } from '../types'
|
|
2
|
+
import { COLORS, LAYOUT } from '../types'
|
|
3
|
+
|
|
4
|
+
interface IdleGapProps {
|
|
5
|
+
segment: TimeSegment
|
|
6
|
+
participants: DiagramParticipant[]
|
|
7
|
+
yPosition: number
|
|
8
|
+
formatDuration: (ms: number) => string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function IdleGap({ segment, participants, yPosition, formatDuration }: IdleGapProps) {
|
|
12
|
+
const totalWidth = LAYOUT.timeAxisWidth + participants.length * (LAYOUT.participantWidth + LAYOUT.participantGap)
|
|
13
|
+
const height = segment.displayHeight
|
|
14
|
+
const centerX = (LAYOUT.timeAxisWidth + totalWidth) / 2
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<g className="idle-gap">
|
|
18
|
+
{/* Background fill with gradient effect */}
|
|
19
|
+
<rect
|
|
20
|
+
x={LAYOUT.timeAxisWidth}
|
|
21
|
+
y={yPosition + 2}
|
|
22
|
+
width={totalWidth - LAYOUT.timeAxisWidth}
|
|
23
|
+
height={height - 4}
|
|
24
|
+
className="fill-slate-50"
|
|
25
|
+
rx={2}
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
{/* Top dashed line */}
|
|
29
|
+
<line
|
|
30
|
+
x1={LAYOUT.timeAxisWidth}
|
|
31
|
+
y1={yPosition}
|
|
32
|
+
x2={totalWidth}
|
|
33
|
+
y2={yPosition}
|
|
34
|
+
className={COLORS.idle.stroke}
|
|
35
|
+
strokeWidth={1}
|
|
36
|
+
strokeDasharray="4 4"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
{/* Bottom dashed line */}
|
|
40
|
+
<line
|
|
41
|
+
x1={LAYOUT.timeAxisWidth}
|
|
42
|
+
y1={yPosition + height}
|
|
43
|
+
x2={totalWidth}
|
|
44
|
+
y2={yPosition + height}
|
|
45
|
+
className={COLORS.idle.stroke}
|
|
46
|
+
strokeWidth={1}
|
|
47
|
+
strokeDasharray="4 4"
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
{/* Duration label in center */}
|
|
51
|
+
<g transform={`translate(${centerX}, ${yPosition + height / 2})`}>
|
|
52
|
+
<rect
|
|
53
|
+
x={-36}
|
|
54
|
+
y={-10}
|
|
55
|
+
width={72}
|
|
56
|
+
height={20}
|
|
57
|
+
rx={10}
|
|
58
|
+
className="fill-white stroke-slate-200"
|
|
59
|
+
strokeWidth={1}
|
|
60
|
+
/>
|
|
61
|
+
<text
|
|
62
|
+
x={0}
|
|
63
|
+
y={0}
|
|
64
|
+
textAnchor="middle"
|
|
65
|
+
dominantBaseline="middle"
|
|
66
|
+
className={`text-[10px] ${COLORS.idle.text} fill-current`}
|
|
67
|
+
>
|
|
68
|
+
{formatDuration(segment.actualDuration)}
|
|
69
|
+
</text>
|
|
70
|
+
</g>
|
|
71
|
+
|
|
72
|
+
{/* Vertical lane continuations (subtle dots) */}
|
|
73
|
+
{participants.map((participant) => {
|
|
74
|
+
const laneX = LAYOUT.timeAxisWidth + participant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.participantWidth / 2
|
|
75
|
+
return (
|
|
76
|
+
<line
|
|
77
|
+
key={participant.id}
|
|
78
|
+
x1={laneX}
|
|
79
|
+
y1={yPosition + 4}
|
|
80
|
+
x2={laneX}
|
|
81
|
+
y2={yPosition + height - 4}
|
|
82
|
+
className={COLORS.idle.stroke}
|
|
83
|
+
strokeWidth={1}
|
|
84
|
+
strokeDasharray="2 6"
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
})}
|
|
88
|
+
</g>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useDebugNavigate } from '../../DebugNavigation'
|
|
2
|
+
import type { DiagramLLMBlock, DiagramParticipant } from '../types'
|
|
3
|
+
import { COLORS, LAYOUT } from '../types'
|
|
4
|
+
|
|
5
|
+
interface LLMBlockProps {
|
|
6
|
+
block: DiagramLLMBlock
|
|
7
|
+
participants: DiagramParticipant[]
|
|
8
|
+
onHover?: (block: DiagramLLMBlock | null, x: number, y: number) => void
|
|
9
|
+
onClick?: (block: DiagramLLMBlock) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function LLMBlock({ block, participants, onHover, onClick }: LLMBlockProps) {
|
|
13
|
+
const navigate = useDebugNavigate()
|
|
14
|
+
const participant = participants.find((p) => p.id === block.participantId)
|
|
15
|
+
|
|
16
|
+
if (!participant) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const laneX = LAYOUT.timeAxisWidth + participant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.participantWidth / 2
|
|
21
|
+
const x = laneX - LAYOUT.blockWidth / 2
|
|
22
|
+
const y = block.yStart
|
|
23
|
+
const height = Math.max(LAYOUT.blockMinHeight, block.yEnd - block.yStart)
|
|
24
|
+
|
|
25
|
+
const isRunning = block.status === 'running'
|
|
26
|
+
const isError = block.status === 'error'
|
|
27
|
+
|
|
28
|
+
const content = (
|
|
29
|
+
<g
|
|
30
|
+
className="cursor-pointer group"
|
|
31
|
+
onMouseEnter={(e) => onHover?.(block, e.clientX, e.clientY)}
|
|
32
|
+
onMouseLeave={() => onHover?.(null, 0, 0)}
|
|
33
|
+
onClick={() => onClick?.(block)}
|
|
34
|
+
>
|
|
35
|
+
{/* Block rectangle */}
|
|
36
|
+
<rect
|
|
37
|
+
x={x}
|
|
38
|
+
y={y}
|
|
39
|
+
width={LAYOUT.blockWidth}
|
|
40
|
+
height={height}
|
|
41
|
+
rx={4}
|
|
42
|
+
ry={4}
|
|
43
|
+
className={`
|
|
44
|
+
${isError ? COLORS.error.fill : isRunning ? COLORS.llm.fillRunning : COLORS.llm.fill}
|
|
45
|
+
${isError ? COLORS.error.stroke : COLORS.llm.stroke}
|
|
46
|
+
stroke-1
|
|
47
|
+
group-hover:stroke-violet-400
|
|
48
|
+
transition-colors
|
|
49
|
+
${isRunning ? 'animate-pulse' : ''}
|
|
50
|
+
`}
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
{/* Icon */}
|
|
54
|
+
<g transform={`translate(${x + 6}, ${y + height / 2 - 5})`}>
|
|
55
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" className={isError ? COLORS.error.text : COLORS.llm.text}>
|
|
56
|
+
<path
|
|
57
|
+
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
58
|
+
stroke="currentColor"
|
|
59
|
+
strokeWidth="2"
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
strokeLinejoin="round"
|
|
62
|
+
/>
|
|
63
|
+
</svg>
|
|
64
|
+
</g>
|
|
65
|
+
|
|
66
|
+
{/* Label */}
|
|
67
|
+
<text
|
|
68
|
+
x={x + 20}
|
|
69
|
+
y={y + height / 2}
|
|
70
|
+
dominantBaseline="middle"
|
|
71
|
+
className={`text-[10px] font-medium ${isError ? COLORS.error.text : COLORS.llm.text} fill-current`}
|
|
72
|
+
>
|
|
73
|
+
LLM
|
|
74
|
+
</text>
|
|
75
|
+
|
|
76
|
+
{/* Status indicators */}
|
|
77
|
+
{isRunning && (
|
|
78
|
+
<circle
|
|
79
|
+
cx={x + LAYOUT.blockWidth - 8}
|
|
80
|
+
cy={y + 8}
|
|
81
|
+
r={3}
|
|
82
|
+
className="fill-violet-500 animate-pulse"
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{isError && (
|
|
87
|
+
<circle
|
|
88
|
+
cx={x + LAYOUT.blockWidth - 8}
|
|
89
|
+
cy={y + 8}
|
|
90
|
+
r={3}
|
|
91
|
+
className="fill-red-500"
|
|
92
|
+
/>
|
|
93
|
+
)}
|
|
94
|
+
</g>
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
// Wrap in clickable group if we have an LLM call ID
|
|
98
|
+
if (block.llmCallId) {
|
|
99
|
+
return (
|
|
100
|
+
<g style={{ cursor: 'pointer' }} onClick={() => navigate(`llm-calls/${block.llmCallId}`)}>
|
|
101
|
+
{content}
|
|
102
|
+
</g>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return content
|
|
107
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { DiagramMessage, DiagramParticipant } from '../types'
|
|
2
|
+
import { LAYOUT } from '../types'
|
|
3
|
+
|
|
4
|
+
interface MessageArrowProps {
|
|
5
|
+
message: DiagramMessage
|
|
6
|
+
participants: DiagramParticipant[]
|
|
7
|
+
showLabel?: boolean
|
|
8
|
+
onHover?: (message: DiagramMessage | null, x: number, y: number) => void
|
|
9
|
+
onClick?: (message: DiagramMessage) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function MessageArrow({ message, participants, showLabel = false, onHover, onClick }: MessageArrowProps) {
|
|
13
|
+
const fromParticipant = participants.find((p) => p.id === message.fromId)
|
|
14
|
+
const toParticipant = participants.find((p) => p.id === message.toId)
|
|
15
|
+
|
|
16
|
+
if (!fromParticipant || !toParticipant) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const fromX = LAYOUT.timeAxisWidth + fromParticipant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.participantWidth / 2
|
|
21
|
+
const toX = LAYOUT.timeAxisWidth + toParticipant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.participantWidth / 2
|
|
22
|
+
|
|
23
|
+
const y = message.yPosition
|
|
24
|
+
const isLeftToRight = fromX < toX
|
|
25
|
+
|
|
26
|
+
// Arrow head size
|
|
27
|
+
const arrowSize = 6
|
|
28
|
+
const arrowDirection = isLeftToRight ? 1 : -1
|
|
29
|
+
|
|
30
|
+
// Calculate label position
|
|
31
|
+
const midX = (fromX + toX) / 2
|
|
32
|
+
const labelMaxWidth = Math.abs(toX - fromX) - 20
|
|
33
|
+
|
|
34
|
+
// Truncate content for label
|
|
35
|
+
const truncatedContent = message.content.length > 50
|
|
36
|
+
? message.content.slice(0, 50) + '…'
|
|
37
|
+
: message.content
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<g
|
|
41
|
+
className="cursor-pointer group"
|
|
42
|
+
onMouseEnter={(e) => onHover?.(message, e.clientX, e.clientY)}
|
|
43
|
+
onMouseLeave={() => onHover?.(null, 0, 0)}
|
|
44
|
+
onClick={() => onClick?.(message)}
|
|
45
|
+
>
|
|
46
|
+
{/* Invisible wider hit area for easier hovering */}
|
|
47
|
+
<line
|
|
48
|
+
x1={fromX}
|
|
49
|
+
y1={y}
|
|
50
|
+
x2={toX}
|
|
51
|
+
y2={y}
|
|
52
|
+
stroke="transparent"
|
|
53
|
+
strokeWidth={16}
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
{/* Arrow line */}
|
|
57
|
+
<line
|
|
58
|
+
x1={fromX}
|
|
59
|
+
y1={y}
|
|
60
|
+
x2={toX - arrowDirection * arrowSize}
|
|
61
|
+
y2={y}
|
|
62
|
+
className="stroke-blue-300 group-hover:stroke-blue-500 transition-colors"
|
|
63
|
+
strokeWidth={1.5}
|
|
64
|
+
/>
|
|
65
|
+
|
|
66
|
+
{/* Arrow head */}
|
|
67
|
+
<polygon
|
|
68
|
+
points={`
|
|
69
|
+
${toX},${y}
|
|
70
|
+
${toX - arrowDirection * arrowSize},${y - arrowSize / 2}
|
|
71
|
+
${toX - arrowDirection * arrowSize},${y + arrowSize / 2}
|
|
72
|
+
`}
|
|
73
|
+
className="fill-blue-300 group-hover:fill-blue-500 transition-colors"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
{/* Small dot at start */}
|
|
77
|
+
<circle
|
|
78
|
+
cx={fromX}
|
|
79
|
+
cy={y}
|
|
80
|
+
r={2.5}
|
|
81
|
+
className="fill-blue-300 group-hover:fill-blue-500 transition-colors"
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
{/* Label (optional) */}
|
|
85
|
+
{showLabel && labelMaxWidth > 40 && (
|
|
86
|
+
<g>
|
|
87
|
+
{/* Label background */}
|
|
88
|
+
<rect
|
|
89
|
+
x={midX - labelMaxWidth / 2}
|
|
90
|
+
y={y - 18}
|
|
91
|
+
width={labelMaxWidth}
|
|
92
|
+
height={14}
|
|
93
|
+
rx={3}
|
|
94
|
+
className="fill-white/90 stroke-blue-200 group-hover:stroke-blue-300"
|
|
95
|
+
strokeWidth={0.5}
|
|
96
|
+
/>
|
|
97
|
+
{/* Label text */}
|
|
98
|
+
<text
|
|
99
|
+
x={midX}
|
|
100
|
+
y={y - 9}
|
|
101
|
+
textAnchor="middle"
|
|
102
|
+
dominantBaseline="middle"
|
|
103
|
+
className="text-[9px] fill-slate-600 group-hover:fill-slate-800"
|
|
104
|
+
style={{
|
|
105
|
+
fontSize: '9px',
|
|
106
|
+
clipPath: `inset(0 0 0 0)`,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<tspan>
|
|
110
|
+
{truncatedContent.length > labelMaxWidth / 5
|
|
111
|
+
? truncatedContent.slice(0, Math.floor(labelMaxWidth / 5)) + '…'
|
|
112
|
+
: truncatedContent}
|
|
113
|
+
</tspan>
|
|
114
|
+
</text>
|
|
115
|
+
</g>
|
|
116
|
+
)}
|
|
117
|
+
</g>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { DiagramParticipant, DiagramToolBlock } from '../types'
|
|
2
|
+
import { COLORS, LAYOUT } from '../types'
|
|
3
|
+
|
|
4
|
+
interface ToolBlockProps {
|
|
5
|
+
block: DiagramToolBlock
|
|
6
|
+
participants: DiagramParticipant[]
|
|
7
|
+
onHover?: (block: DiagramToolBlock | null, x: number, y: number) => void
|
|
8
|
+
onClick?: (block: DiagramToolBlock) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ToolBlock({ block, participants, onHover, onClick }: ToolBlockProps) {
|
|
12
|
+
const participant = participants.find((p) => p.id === block.participantId)
|
|
13
|
+
|
|
14
|
+
if (!participant) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const laneX = LAYOUT.timeAxisWidth + participant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.participantWidth / 2
|
|
19
|
+
const x = laneX - LAYOUT.blockWidth / 2
|
|
20
|
+
const y = block.yStart
|
|
21
|
+
const height = Math.max(LAYOUT.blockMinHeight - 4, block.yEnd - block.yStart)
|
|
22
|
+
|
|
23
|
+
const isRunning = block.status === 'running'
|
|
24
|
+
const isError = block.status === 'error'
|
|
25
|
+
|
|
26
|
+
// Truncate tool name
|
|
27
|
+
const toolName = block.toolName.length > 8
|
|
28
|
+
? block.toolName.slice(0, 8) + '…'
|
|
29
|
+
: block.toolName
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<g
|
|
33
|
+
className="cursor-pointer group"
|
|
34
|
+
onMouseEnter={(e) => onHover?.(block, e.clientX, e.clientY)}
|
|
35
|
+
onMouseLeave={() => onHover?.(null, 0, 0)}
|
|
36
|
+
onClick={() => onClick?.(block)}
|
|
37
|
+
>
|
|
38
|
+
{/* Block rectangle */}
|
|
39
|
+
<rect
|
|
40
|
+
x={x}
|
|
41
|
+
y={y}
|
|
42
|
+
width={LAYOUT.blockWidth}
|
|
43
|
+
height={height}
|
|
44
|
+
rx={4}
|
|
45
|
+
ry={4}
|
|
46
|
+
className={`
|
|
47
|
+
${isError ? COLORS.error.fill : isRunning ? COLORS.tool.fillRunning : COLORS.tool.fill}
|
|
48
|
+
${isError ? COLORS.error.stroke : COLORS.tool.stroke}
|
|
49
|
+
stroke-1
|
|
50
|
+
group-hover:stroke-teal-400
|
|
51
|
+
transition-colors
|
|
52
|
+
${isRunning ? 'animate-pulse' : ''}
|
|
53
|
+
`}
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
{/* Tool icon */}
|
|
57
|
+
<g transform={`translate(${x + 6}, ${y + height / 2 - 5})`}>
|
|
58
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" className={isError ? COLORS.error.text : COLORS.tool.text}>
|
|
59
|
+
<path
|
|
60
|
+
d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"
|
|
61
|
+
stroke="currentColor"
|
|
62
|
+
strokeWidth="2"
|
|
63
|
+
strokeLinecap="round"
|
|
64
|
+
strokeLinejoin="round"
|
|
65
|
+
/>
|
|
66
|
+
</svg>
|
|
67
|
+
</g>
|
|
68
|
+
|
|
69
|
+
{/* Tool name */}
|
|
70
|
+
<text
|
|
71
|
+
x={x + 20}
|
|
72
|
+
y={y + height / 2}
|
|
73
|
+
dominantBaseline="middle"
|
|
74
|
+
className={`text-[9px] font-mono ${isError ? COLORS.error.text : COLORS.tool.text} fill-current`}
|
|
75
|
+
>
|
|
76
|
+
{toolName}
|
|
77
|
+
</text>
|
|
78
|
+
|
|
79
|
+
{/* Status indicators */}
|
|
80
|
+
{isRunning && (
|
|
81
|
+
<circle
|
|
82
|
+
cx={x + LAYOUT.blockWidth - 8}
|
|
83
|
+
cy={y + 8}
|
|
84
|
+
r={3}
|
|
85
|
+
className="fill-teal-500 animate-pulse"
|
|
86
|
+
/>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{isError && (
|
|
90
|
+
<circle
|
|
91
|
+
cx={x + LAYOUT.blockWidth - 8}
|
|
92
|
+
cy={y + 8}
|
|
93
|
+
r={3}
|
|
94
|
+
className="fill-red-500"
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
</g>
|
|
98
|
+
)
|
|
99
|
+
}
|