prompt_objects 0.3.1 → 0.5.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/CLAUDE.md +112 -44
- data/Gemfile.lock +31 -29
- data/README.md +5 -0
- data/frontend/index.html +5 -1
- data/frontend/package-lock.json +123 -0
- data/frontend/package.json +4 -0
- data/frontend/src/App.tsx +70 -71
- data/frontend/src/canvas/CanvasView.tsx +113 -0
- data/frontend/src/canvas/ForceLayout.ts +115 -0
- data/frontend/src/canvas/SceneManager.ts +587 -0
- data/frontend/src/canvas/canvasStore.ts +47 -0
- data/frontend/src/canvas/constants.ts +95 -0
- data/frontend/src/canvas/controls/CameraControls.ts +98 -0
- data/frontend/src/canvas/edges/MessageArc.ts +149 -0
- data/frontend/src/canvas/inspector/InspectorPanel.tsx +31 -0
- data/frontend/src/canvas/inspector/POInspector.tsx +262 -0
- data/frontend/src/canvas/inspector/ToolCallInspector.tsx +67 -0
- data/frontend/src/canvas/nodes/PONode.ts +249 -0
- data/frontend/src/canvas/nodes/ToolCallNode.ts +116 -0
- data/frontend/src/canvas/types.ts +24 -0
- data/frontend/src/components/ContextMenu.tsx +5 -4
- data/frontend/src/components/Inspector.tsx +232 -0
- data/frontend/src/components/MarkdownMessage.tsx +22 -20
- data/frontend/src/components/MethodList.tsx +90 -0
- data/frontend/src/components/ModelSelector.tsx +13 -14
- data/frontend/src/components/NotificationPanel.tsx +29 -33
- data/frontend/src/components/ObjectList.tsx +78 -0
- data/frontend/src/components/PaneSlot.tsx +30 -0
- data/frontend/src/components/SourcePane.tsx +202 -0
- data/frontend/src/components/SystemBar.tsx +74 -0
- data/frontend/src/components/Transcript.tsx +76 -0
- data/frontend/src/components/UsagePanel.tsx +27 -27
- data/frontend/src/components/Workspace.tsx +260 -0
- data/frontend/src/components/index.ts +10 -9
- data/frontend/src/hooks/useResize.ts +55 -0
- data/frontend/src/hooks/useWebSocket.ts +274 -189
- data/frontend/src/index.css +69 -3
- data/frontend/src/store/index.ts +23 -0
- data/frontend/src/types/index.ts +5 -0
- data/frontend/tailwind.config.js +28 -9
- data/lib/prompt_objects/capability.rb +23 -1
- data/lib/prompt_objects/connectors/mcp.rb +5 -22
- data/lib/prompt_objects/environment.rb +8 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +1 -2
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +2 -8
- data/lib/prompt_objects/primitives/list_files.rb +1 -2
- data/lib/prompt_objects/prompt_object.rb +150 -6
- data/lib/prompt_objects/server/api/routes.rb +3 -48
- data/lib/prompt_objects/server/app.rb +9 -0
- data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +4345 -0
- data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +1 -0
- data/lib/prompt_objects/server/public/index.html +7 -3
- data/lib/prompt_objects/server/websocket_handler.rb +23 -100
- data/lib/prompt_objects/server.rb +6 -62
- data/prompt_objects.gemspec +1 -1
- data/templates/arc-agi-1/primitives/find_objects.rb +1 -1
- data/templates/arc-agi-1/primitives/grid_diff.rb +2 -2
- data/templates/arc-agi-1/primitives/grid_info.rb +1 -1
- data/templates/arc-agi-1/primitives/grid_transform.rb +1 -1
- data/templates/arc-agi-1/primitives/render_grid.rb +1 -0
- data/templates/arc-agi-1/primitives/test_solution.rb +3 -0
- metadata +26 -14
- data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
- data/frontend/src/components/ChatPanel.tsx +0 -288
- data/frontend/src/components/Dashboard.tsx +0 -83
- data/frontend/src/components/Header.tsx +0 -141
- data/frontend/src/components/MessageBus.tsx +0 -56
- data/frontend/src/components/POCard.tsx +0 -56
- data/frontend/src/components/PODetail.tsx +0 -124
- data/frontend/src/components/PromptPanel.tsx +0 -156
- data/frontend/src/components/SessionsPanel.tsx +0 -174
- data/frontend/src/components/ThreadsSidebar.tsx +0 -163
- data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +0 -1
- data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +0 -77
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Canvas visualization constants
|
|
2
|
+
|
|
3
|
+
// Colors (hex values matching warm po-* palette)
|
|
4
|
+
export const COLORS = {
|
|
5
|
+
// Node colors
|
|
6
|
+
background: 0x1a1918,
|
|
7
|
+
surface: 0x222120,
|
|
8
|
+
border: 0x3d3a37,
|
|
9
|
+
accent: 0xd4952a,
|
|
10
|
+
accentHover: 0xe0a940,
|
|
11
|
+
success: 0x3b9a6e,
|
|
12
|
+
warning: 0xd4952a,
|
|
13
|
+
error: 0xc45c4a,
|
|
14
|
+
|
|
15
|
+
// Status colors
|
|
16
|
+
statusIdle: 0x78726a,
|
|
17
|
+
statusThinking: 0xd4952a,
|
|
18
|
+
statusCallingTool: 0x3b9a6e,
|
|
19
|
+
|
|
20
|
+
// Canvas-specific
|
|
21
|
+
nodeFill: 0x222120,
|
|
22
|
+
nodeGlow: 0xd4952a,
|
|
23
|
+
toolCallFill: 0x3b9a6e,
|
|
24
|
+
arcColor: 0xd4952a,
|
|
25
|
+
particleColor: 0xe0a940,
|
|
26
|
+
gridColor: 0x222120,
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
// CSS color strings (for CSS2DRenderer elements)
|
|
30
|
+
export const CSS_COLORS = {
|
|
31
|
+
accent: '#d4952a',
|
|
32
|
+
accentHover: '#e0a940',
|
|
33
|
+
warning: '#d4952a',
|
|
34
|
+
success: '#3b9a6e',
|
|
35
|
+
error: '#c45c4a',
|
|
36
|
+
textPrimary: '#e8e2da',
|
|
37
|
+
textSecondary: '#a8a29a',
|
|
38
|
+
textMuted: '#78726a',
|
|
39
|
+
surface: '#222120',
|
|
40
|
+
border: '#3d3a37',
|
|
41
|
+
statusIdle: '#78726a',
|
|
42
|
+
statusThinking: '#d4952a',
|
|
43
|
+
statusCallingTool: '#3b9a6e',
|
|
44
|
+
} as const
|
|
45
|
+
|
|
46
|
+
// Node dimensions
|
|
47
|
+
export const NODE = {
|
|
48
|
+
poRadius: 40,
|
|
49
|
+
poSides: 6, // hexagon
|
|
50
|
+
toolCallRadius: 18,
|
|
51
|
+
labelOffsetY: 55,
|
|
52
|
+
badgeOffsetX: 30,
|
|
53
|
+
badgeOffsetY: -30,
|
|
54
|
+
} as const
|
|
55
|
+
|
|
56
|
+
// Camera
|
|
57
|
+
export const CAMERA = {
|
|
58
|
+
zoomMin: 0.1,
|
|
59
|
+
zoomMax: 5,
|
|
60
|
+
zoomSpeed: 0.1,
|
|
61
|
+
fitPadding: 1.3, // 30% padding
|
|
62
|
+
} as const
|
|
63
|
+
|
|
64
|
+
// Animation
|
|
65
|
+
export const ANIMATION = {
|
|
66
|
+
positionLerpFactor: 0.1,
|
|
67
|
+
toolCallFadeInDuration: 0.4,
|
|
68
|
+
toolCallActiveDuration: 8,
|
|
69
|
+
toolCallFadeOutDuration: 1.5,
|
|
70
|
+
arcLifetime: 6,
|
|
71
|
+
arcFadeDuration: 2,
|
|
72
|
+
particleSpeed: 0.4,
|
|
73
|
+
particleCount: 5,
|
|
74
|
+
pulseSpeed: 2,
|
|
75
|
+
} as const
|
|
76
|
+
|
|
77
|
+
// Force simulation
|
|
78
|
+
export const FORCE = {
|
|
79
|
+
chargeStrength: -300,
|
|
80
|
+
centerStrength: 0.05,
|
|
81
|
+
collisionRadius: 60, // radius + 20
|
|
82
|
+
linkDistance: 200,
|
|
83
|
+
alphaDecay: 0.02,
|
|
84
|
+
velocityDecay: 0.4,
|
|
85
|
+
} as const
|
|
86
|
+
|
|
87
|
+
// Bloom
|
|
88
|
+
export const BLOOM = {
|
|
89
|
+
strength: 0.8,
|
|
90
|
+
radius: 0.4,
|
|
91
|
+
threshold: 0.6,
|
|
92
|
+
} as const
|
|
93
|
+
|
|
94
|
+
// Sync throttling
|
|
95
|
+
export const SYNC_THROTTLE_MS = 100
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { CAMERA } from '../constants'
|
|
3
|
+
|
|
4
|
+
export class CameraControls {
|
|
5
|
+
private camera: THREE.OrthographicCamera
|
|
6
|
+
private domElement: HTMLElement
|
|
7
|
+
private isPanning = false
|
|
8
|
+
private panStart = new THREE.Vector2()
|
|
9
|
+
|
|
10
|
+
private onWheel: (e: WheelEvent) => void
|
|
11
|
+
private onPointerDown: (e: PointerEvent) => void
|
|
12
|
+
private onPointerMove: (e: PointerEvent) => void
|
|
13
|
+
private onPointerUp: (e: PointerEvent) => void
|
|
14
|
+
|
|
15
|
+
constructor(camera: THREE.OrthographicCamera, domElement: HTMLElement) {
|
|
16
|
+
this.camera = camera
|
|
17
|
+
this.domElement = domElement
|
|
18
|
+
|
|
19
|
+
this.onWheel = this.handleWheel.bind(this)
|
|
20
|
+
this.onPointerDown = this.handlePointerDown.bind(this)
|
|
21
|
+
this.onPointerMove = this.handlePointerMove.bind(this)
|
|
22
|
+
this.onPointerUp = this.handlePointerUp.bind(this)
|
|
23
|
+
|
|
24
|
+
domElement.addEventListener('wheel', this.onWheel, { passive: false })
|
|
25
|
+
domElement.addEventListener('pointerdown', this.onPointerDown)
|
|
26
|
+
domElement.addEventListener('pointermove', this.onPointerMove)
|
|
27
|
+
domElement.addEventListener('pointerup', this.onPointerUp)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private handleWheel(e: WheelEvent): void {
|
|
31
|
+
e.preventDefault()
|
|
32
|
+
const delta = e.deltaY > 0 ? 1 - CAMERA.zoomSpeed : 1 + CAMERA.zoomSpeed
|
|
33
|
+
const newZoom = this.camera.zoom * delta
|
|
34
|
+
this.camera.zoom = THREE.MathUtils.clamp(newZoom, CAMERA.zoomMin, CAMERA.zoomMax)
|
|
35
|
+
this.camera.updateProjectionMatrix()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private handlePointerDown(e: PointerEvent): void {
|
|
39
|
+
// Shift+left click or middle mouse button for panning
|
|
40
|
+
if ((e.button === 0 && e.shiftKey) || e.button === 1) {
|
|
41
|
+
this.isPanning = true
|
|
42
|
+
this.panStart.set(e.clientX, e.clientY)
|
|
43
|
+
this.domElement.setPointerCapture(e.pointerId)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private handlePointerMove(e: PointerEvent): void {
|
|
48
|
+
if (!this.isPanning) return
|
|
49
|
+
|
|
50
|
+
const dx = e.clientX - this.panStart.x
|
|
51
|
+
const dy = e.clientY - this.panStart.y
|
|
52
|
+
|
|
53
|
+
// Convert screen pixels to world units based on zoom
|
|
54
|
+
const worldDx = -dx / this.camera.zoom
|
|
55
|
+
const worldDy = dy / this.camera.zoom
|
|
56
|
+
|
|
57
|
+
this.camera.position.x += worldDx
|
|
58
|
+
this.camera.position.y += worldDy
|
|
59
|
+
|
|
60
|
+
this.panStart.set(e.clientX, e.clientY)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private handlePointerUp(e: PointerEvent): void {
|
|
64
|
+
if (this.isPanning) {
|
|
65
|
+
this.isPanning = false
|
|
66
|
+
this.domElement.releasePointerCapture(e.pointerId)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fitAll(bounds: THREE.Box3): void {
|
|
71
|
+
if (bounds.isEmpty()) return
|
|
72
|
+
|
|
73
|
+
const center = new THREE.Vector3()
|
|
74
|
+
bounds.getCenter(center)
|
|
75
|
+
const size = new THREE.Vector3()
|
|
76
|
+
bounds.getSize(size)
|
|
77
|
+
|
|
78
|
+
this.camera.position.x = center.x
|
|
79
|
+
this.camera.position.y = center.y
|
|
80
|
+
|
|
81
|
+
const viewWidth = this.camera.right - this.camera.left
|
|
82
|
+
const viewHeight = this.camera.top - this.camera.bottom
|
|
83
|
+
|
|
84
|
+
const scaleX = viewWidth / (size.x * CAMERA.fitPadding)
|
|
85
|
+
const scaleY = viewHeight / (size.y * CAMERA.fitPadding)
|
|
86
|
+
|
|
87
|
+
this.camera.zoom = Math.min(scaleX, scaleY, CAMERA.zoomMax)
|
|
88
|
+
this.camera.zoom = Math.max(this.camera.zoom, CAMERA.zoomMin)
|
|
89
|
+
this.camera.updateProjectionMatrix()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
dispose(): void {
|
|
93
|
+
this.domElement.removeEventListener('wheel', this.onWheel)
|
|
94
|
+
this.domElement.removeEventListener('pointerdown', this.onPointerDown)
|
|
95
|
+
this.domElement.removeEventListener('pointermove', this.onPointerMove)
|
|
96
|
+
this.domElement.removeEventListener('pointerup', this.onPointerUp)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { COLORS, ANIMATION } from '../constants'
|
|
3
|
+
|
|
4
|
+
export class MessageArc {
|
|
5
|
+
readonly id: string
|
|
6
|
+
readonly from: string
|
|
7
|
+
readonly to: string
|
|
8
|
+
readonly group: THREE.Group
|
|
9
|
+
|
|
10
|
+
private curve: THREE.QuadraticBezierCurve3
|
|
11
|
+
private line: THREE.Line
|
|
12
|
+
private lineMaterial: THREE.LineBasicMaterial
|
|
13
|
+
private particles: THREE.Points
|
|
14
|
+
private particleMaterial: THREE.PointsMaterial
|
|
15
|
+
private particlePositions: Float32Array
|
|
16
|
+
private particleTs: number[] // parameter t for each particle on the curve
|
|
17
|
+
|
|
18
|
+
private startPoint = new THREE.Vector3()
|
|
19
|
+
private endPoint = new THREE.Vector3()
|
|
20
|
+
private controlPoint = new THREE.Vector3()
|
|
21
|
+
private age = 0
|
|
22
|
+
private expired = false
|
|
23
|
+
|
|
24
|
+
constructor(id: string, from: string, to: string, startPos: THREE.Vector3, endPos: THREE.Vector3) {
|
|
25
|
+
this.id = id
|
|
26
|
+
this.from = from
|
|
27
|
+
this.to = to
|
|
28
|
+
this.group = new THREE.Group()
|
|
29
|
+
|
|
30
|
+
this.startPoint.copy(startPos)
|
|
31
|
+
this.endPoint.copy(endPos)
|
|
32
|
+
this.computeControlPoint()
|
|
33
|
+
|
|
34
|
+
this.curve = new THREE.QuadraticBezierCurve3(
|
|
35
|
+
this.startPoint,
|
|
36
|
+
this.controlPoint,
|
|
37
|
+
this.endPoint
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// Arc line
|
|
41
|
+
const linePoints = this.curve.getPoints(50)
|
|
42
|
+
const lineGeometry = new THREE.BufferGeometry().setFromPoints(linePoints)
|
|
43
|
+
this.lineMaterial = new THREE.LineBasicMaterial({
|
|
44
|
+
color: COLORS.arcColor,
|
|
45
|
+
transparent: true,
|
|
46
|
+
opacity: 0.4,
|
|
47
|
+
})
|
|
48
|
+
this.line = new THREE.Line(lineGeometry, this.lineMaterial)
|
|
49
|
+
this.group.add(this.line)
|
|
50
|
+
|
|
51
|
+
// Particles traveling along the curve
|
|
52
|
+
const count = ANIMATION.particleCount
|
|
53
|
+
this.particlePositions = new Float32Array(count * 3)
|
|
54
|
+
this.particleTs = []
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < count; i++) {
|
|
57
|
+
this.particleTs.push(i / count)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const particleGeometry = new THREE.BufferGeometry()
|
|
61
|
+
particleGeometry.setAttribute(
|
|
62
|
+
'position',
|
|
63
|
+
new THREE.BufferAttribute(this.particlePositions, 3)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
this.particleMaterial = new THREE.PointsMaterial({
|
|
67
|
+
color: COLORS.particleColor,
|
|
68
|
+
size: 4,
|
|
69
|
+
transparent: true,
|
|
70
|
+
opacity: 0.8,
|
|
71
|
+
sizeAttenuation: false,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
this.particles = new THREE.Points(particleGeometry, this.particleMaterial)
|
|
75
|
+
this.group.add(this.particles)
|
|
76
|
+
|
|
77
|
+
this.updateParticlePositions()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private computeControlPoint(): void {
|
|
81
|
+
// Perpendicular offset at midpoint
|
|
82
|
+
const mid = new THREE.Vector3().addVectors(this.startPoint, this.endPoint).multiplyScalar(0.5)
|
|
83
|
+
const dir = new THREE.Vector3().subVectors(this.endPoint, this.startPoint)
|
|
84
|
+
const dist = dir.length()
|
|
85
|
+
// Perpendicular in 2D: rotate 90 degrees
|
|
86
|
+
const perp = new THREE.Vector3(-dir.y, dir.x, 0).normalize()
|
|
87
|
+
this.controlPoint.copy(mid).addScaledVector(perp, dist * 0.3)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
updateEndpoints(startPos: THREE.Vector3, endPos: THREE.Vector3): void {
|
|
91
|
+
this.startPoint.copy(startPos)
|
|
92
|
+
this.endPoint.copy(endPos)
|
|
93
|
+
this.computeControlPoint()
|
|
94
|
+
|
|
95
|
+
this.curve.v0.copy(this.startPoint)
|
|
96
|
+
this.curve.v1.copy(this.controlPoint)
|
|
97
|
+
this.curve.v2.copy(this.endPoint)
|
|
98
|
+
|
|
99
|
+
// Rebuild line geometry
|
|
100
|
+
const linePoints = this.curve.getPoints(50)
|
|
101
|
+
this.line.geometry.dispose()
|
|
102
|
+
this.line.geometry = new THREE.BufferGeometry().setFromPoints(linePoints)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private updateParticlePositions(): void {
|
|
106
|
+
for (let i = 0; i < this.particleTs.length; i++) {
|
|
107
|
+
const point = this.curve.getPoint(this.particleTs[i])
|
|
108
|
+
this.particlePositions[i * 3] = point.x
|
|
109
|
+
this.particlePositions[i * 3 + 1] = point.y
|
|
110
|
+
this.particlePositions[i * 3 + 2] = point.z
|
|
111
|
+
}
|
|
112
|
+
this.particles.geometry.attributes.position.needsUpdate = true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
update(delta: number): void {
|
|
116
|
+
this.age += delta
|
|
117
|
+
|
|
118
|
+
// Advance particles
|
|
119
|
+
for (let i = 0; i < this.particleTs.length; i++) {
|
|
120
|
+
this.particleTs[i] = (this.particleTs[i] + delta * ANIMATION.particleSpeed) % 1
|
|
121
|
+
}
|
|
122
|
+
this.updateParticlePositions()
|
|
123
|
+
|
|
124
|
+
// Fade out in last 2 seconds of lifetime
|
|
125
|
+
const fadeStart = ANIMATION.arcLifetime - ANIMATION.arcFadeDuration
|
|
126
|
+
if (this.age > fadeStart) {
|
|
127
|
+
const fadeProgress = (this.age - fadeStart) / ANIMATION.arcFadeDuration
|
|
128
|
+
const alpha = Math.max(1 - fadeProgress, 0)
|
|
129
|
+
this.lineMaterial.opacity = 0.4 * alpha
|
|
130
|
+
this.particleMaterial.opacity = 0.8 * alpha
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (this.age >= ANIMATION.arcLifetime) {
|
|
134
|
+
this.expired = true
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
isExpired(): boolean {
|
|
139
|
+
return this.expired
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
dispose(): void {
|
|
143
|
+
this.line.geometry.dispose()
|
|
144
|
+
this.lineMaterial.dispose()
|
|
145
|
+
this.particles.geometry.dispose()
|
|
146
|
+
this.particleMaterial.dispose()
|
|
147
|
+
this.group.parent?.remove(this.group)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useCanvasStore } from '../canvasStore'
|
|
2
|
+
import { POInspector } from './POInspector'
|
|
3
|
+
import { ToolCallInspector } from './ToolCallInspector'
|
|
4
|
+
|
|
5
|
+
export function InspectorPanel() {
|
|
6
|
+
const selectedNode = useCanvasStore((s) => s.selectedNode)
|
|
7
|
+
|
|
8
|
+
if (!selectedNode) return null
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<aside className="w-80 border-l border-po-border bg-po-surface overflow-hidden flex flex-col">
|
|
12
|
+
<div className="h-8 bg-po-surface-2 border-b border-po-border flex items-center px-3">
|
|
13
|
+
<span className="text-2xs font-medium text-po-text-ghost uppercase tracking-wider flex-1">Inspector</span>
|
|
14
|
+
<button
|
|
15
|
+
onClick={() => useCanvasStore.getState().selectNode(null)}
|
|
16
|
+
className="text-2xs text-po-text-ghost hover:text-po-text-secondary transition-colors duration-150"
|
|
17
|
+
title="Close inspector"
|
|
18
|
+
>
|
|
19
|
+
{'\u2715'}
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="flex-1 overflow-auto">
|
|
23
|
+
{selectedNode.type === 'po' ? (
|
|
24
|
+
<POInspector poName={selectedNode.id} />
|
|
25
|
+
) : (
|
|
26
|
+
<ToolCallInspector toolCallId={selectedNode.id} />
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
</aside>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
|
+
import { useStore, usePONotifications } from '../../store'
|
|
3
|
+
import { useWebSocket } from '../../hooks/useWebSocket'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
poName: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const statusColors: Record<string, string> = {
|
|
10
|
+
idle: 'bg-po-status-idle',
|
|
11
|
+
thinking: 'bg-po-status-active animate-pulse',
|
|
12
|
+
calling_tool: 'bg-po-status-calling animate-pulse',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function POInspector({ poName }: Props) {
|
|
16
|
+
const po = useStore((s) => s.promptObjects[poName])
|
|
17
|
+
const notifications = usePONotifications(poName)
|
|
18
|
+
const { updatePrompt, respondToNotification } = useWebSocket()
|
|
19
|
+
const { selectPO, setCurrentView } = useStore()
|
|
20
|
+
|
|
21
|
+
if (!po) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="p-4 text-po-text-ghost text-xs font-mono">
|
|
24
|
+
Prompt Object "{poName}" not found.
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="p-4 space-y-5">
|
|
31
|
+
{/* Header */}
|
|
32
|
+
<div>
|
|
33
|
+
<div className="flex items-center gap-2 mb-1">
|
|
34
|
+
<h3 className="text-sm font-mono font-medium text-po-text-primary">{po.name}</h3>
|
|
35
|
+
<div className={`w-2 h-2 rounded-full ${statusColors[po.status] || statusColors.idle}`} />
|
|
36
|
+
</div>
|
|
37
|
+
<p className="text-xs text-po-text-secondary">{po.description}</p>
|
|
38
|
+
<span className="inline-block mt-1 text-2xs text-po-text-ghost bg-po-surface-2 px-1.5 py-0.5 rounded font-mono">
|
|
39
|
+
{po.status}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Capabilities */}
|
|
44
|
+
<div>
|
|
45
|
+
<h4 className="text-2xs font-medium text-po-text-ghost uppercase tracking-wider mb-2">
|
|
46
|
+
Capabilities ({po.capabilities?.length || 0})
|
|
47
|
+
</h4>
|
|
48
|
+
<div className="space-y-1">
|
|
49
|
+
{(po.capabilities || []).map((cap) => (
|
|
50
|
+
<CapabilityItem key={cap.name} name={cap.name} description={cap.description} />
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Prompt */}
|
|
56
|
+
<PromptSection
|
|
57
|
+
prompt={po.prompt || ''}
|
|
58
|
+
onSave={(prompt) => updatePrompt(poName, prompt)}
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
{/* Sessions */}
|
|
62
|
+
<div>
|
|
63
|
+
<h4 className="text-2xs font-medium text-po-text-ghost uppercase tracking-wider mb-2">
|
|
64
|
+
Sessions ({po.sessions?.length || 0})
|
|
65
|
+
</h4>
|
|
66
|
+
{po.current_session && (
|
|
67
|
+
<div className="text-xs text-po-text-tertiary">
|
|
68
|
+
Current: <span className="text-po-text-secondary font-mono">{po.current_session.id.slice(0, 8)}...</span>
|
|
69
|
+
<span className="ml-2">({po.current_session.messages.length} messages)</span>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Notifications */}
|
|
75
|
+
{notifications.length > 0 && (
|
|
76
|
+
<div>
|
|
77
|
+
<h4 className="text-2xs font-medium text-po-warning uppercase tracking-wider mb-2">
|
|
78
|
+
Pending Requests ({notifications.length})
|
|
79
|
+
</h4>
|
|
80
|
+
<div className="space-y-2">
|
|
81
|
+
{notifications.map((n) => (
|
|
82
|
+
<NotificationCard
|
|
83
|
+
key={n.id}
|
|
84
|
+
notification={n}
|
|
85
|
+
onRespond={(response) => respondToNotification(n.id, response)}
|
|
86
|
+
/>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{/* Link to full detail */}
|
|
93
|
+
<button
|
|
94
|
+
onClick={() => {
|
|
95
|
+
selectPO(poName)
|
|
96
|
+
setCurrentView('dashboard')
|
|
97
|
+
}}
|
|
98
|
+
className="text-xs text-po-accent hover:underline font-mono transition-colors duration-150"
|
|
99
|
+
>
|
|
100
|
+
Open in browser view
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function CapabilityItem({ name, description }: { name: string; description: string }) {
|
|
107
|
+
const [expanded, setExpanded] = useState(false)
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="bg-po-surface-2 border border-po-border rounded overflow-hidden">
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => setExpanded(!expanded)}
|
|
113
|
+
className="w-full px-2.5 py-1.5 flex items-center justify-between hover:bg-po-surface-3 transition-colors duration-150"
|
|
114
|
+
>
|
|
115
|
+
<span className="font-mono text-xs text-po-accent">{name}</span>
|
|
116
|
+
<span className="text-po-text-ghost text-xs">{expanded ? '\u25BC' : '\u25B8'}</span>
|
|
117
|
+
</button>
|
|
118
|
+
{expanded && (
|
|
119
|
+
<div className="px-2.5 py-2 border-t border-po-border bg-po-surface">
|
|
120
|
+
<p className="text-xs text-po-text-secondary">{description}</p>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function PromptSection({ prompt, onSave }: { prompt: string; onSave: (p: string) => void }) {
|
|
128
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
129
|
+
const [edited, setEdited] = useState(prompt)
|
|
130
|
+
const saveTimeoutRef = useRef<number | null>(null)
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!isEditing) setEdited(prompt)
|
|
134
|
+
}, [prompt, isEditing])
|
|
135
|
+
|
|
136
|
+
const debouncedSave = useCallback(
|
|
137
|
+
(value: string) => {
|
|
138
|
+
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
|
|
139
|
+
saveTimeoutRef.current = window.setTimeout(() => {
|
|
140
|
+
if (value !== prompt) onSave(value)
|
|
141
|
+
}, 1000)
|
|
142
|
+
},
|
|
143
|
+
[onSave, prompt]
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
return () => {
|
|
148
|
+
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
|
|
149
|
+
}
|
|
150
|
+
}, [])
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div>
|
|
154
|
+
<div className="flex items-center justify-between mb-2">
|
|
155
|
+
<h4 className="text-2xs font-medium text-po-text-ghost uppercase tracking-wider">Prompt</h4>
|
|
156
|
+
<button
|
|
157
|
+
onClick={() => {
|
|
158
|
+
if (isEditing && edited !== prompt) onSave(edited)
|
|
159
|
+
setIsEditing(!isEditing)
|
|
160
|
+
}}
|
|
161
|
+
className={`text-2xs px-1.5 py-0.5 rounded transition-colors duration-150 ${
|
|
162
|
+
isEditing
|
|
163
|
+
? 'bg-po-accent text-po-bg'
|
|
164
|
+
: 'text-po-text-tertiary hover:text-po-text-primary hover:bg-po-surface-2'
|
|
165
|
+
}`}
|
|
166
|
+
>
|
|
167
|
+
{isEditing ? 'Done' : 'Edit'}
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
{isEditing ? (
|
|
171
|
+
<textarea
|
|
172
|
+
value={edited}
|
|
173
|
+
onChange={(e) => {
|
|
174
|
+
setEdited(e.target.value)
|
|
175
|
+
debouncedSave(e.target.value)
|
|
176
|
+
}}
|
|
177
|
+
className="w-full h-40 bg-po-bg border border-po-border rounded p-2 text-xs text-po-text-primary font-mono resize-none focus:outline-none focus:border-po-accent"
|
|
178
|
+
spellCheck={false}
|
|
179
|
+
/>
|
|
180
|
+
) : (
|
|
181
|
+
<div className="bg-po-surface-2 border border-po-border rounded p-2 max-h-32 overflow-auto">
|
|
182
|
+
<pre className="text-xs text-po-text-secondary font-mono whitespace-pre-wrap">
|
|
183
|
+
{prompt || '(no prompt)'}
|
|
184
|
+
</pre>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function NotificationCard({
|
|
192
|
+
notification,
|
|
193
|
+
onRespond,
|
|
194
|
+
}: {
|
|
195
|
+
notification: { id: string; type: string; message: string; options: string[] }
|
|
196
|
+
onRespond: (response: string) => void
|
|
197
|
+
}) {
|
|
198
|
+
const [customInput, setCustomInput] = useState('')
|
|
199
|
+
const [showCustom, setShowCustom] = useState(false)
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div className="bg-po-surface-2 border border-po-border rounded p-2">
|
|
203
|
+
<span className="text-2xs font-mono bg-po-warning text-po-bg px-1.5 py-0.5 rounded font-bold">
|
|
204
|
+
{notification.type}
|
|
205
|
+
</span>
|
|
206
|
+
<p className="text-xs text-po-text-primary mt-1.5 mb-2">{notification.message}</p>
|
|
207
|
+
|
|
208
|
+
{notification.options.length > 0 && (
|
|
209
|
+
<div className="flex flex-wrap gap-1.5 mb-1.5">
|
|
210
|
+
{notification.options.map((opt, i) => (
|
|
211
|
+
<button
|
|
212
|
+
key={i}
|
|
213
|
+
onClick={() => onRespond(opt)}
|
|
214
|
+
className="px-2 py-0.5 text-xs bg-po-surface border border-po-border rounded hover:border-po-accent hover:text-po-accent transition-colors duration-150 text-po-text-secondary"
|
|
215
|
+
>
|
|
216
|
+
{opt}
|
|
217
|
+
</button>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{showCustom ? (
|
|
223
|
+
<div className="flex gap-1.5 mt-1.5">
|
|
224
|
+
<input
|
|
225
|
+
type="text"
|
|
226
|
+
value={customInput}
|
|
227
|
+
onChange={(e) => setCustomInput(e.target.value)}
|
|
228
|
+
placeholder="Custom response..."
|
|
229
|
+
className="flex-1 bg-po-bg border border-po-border rounded px-2 py-1 text-xs text-po-text-primary placeholder-po-text-ghost focus:outline-none focus:border-po-accent"
|
|
230
|
+
onKeyDown={(e) => {
|
|
231
|
+
if (e.key === 'Enter' && customInput.trim()) {
|
|
232
|
+
onRespond(customInput.trim())
|
|
233
|
+
setCustomInput('')
|
|
234
|
+
setShowCustom(false)
|
|
235
|
+
}
|
|
236
|
+
}}
|
|
237
|
+
autoFocus
|
|
238
|
+
/>
|
|
239
|
+
<button
|
|
240
|
+
onClick={() => {
|
|
241
|
+
if (customInput.trim()) {
|
|
242
|
+
onRespond(customInput.trim())
|
|
243
|
+
setCustomInput('')
|
|
244
|
+
setShowCustom(false)
|
|
245
|
+
}
|
|
246
|
+
}}
|
|
247
|
+
className="px-2 py-1 text-xs bg-po-accent text-po-bg rounded font-medium"
|
|
248
|
+
>
|
|
249
|
+
Send
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
) : (
|
|
253
|
+
<button
|
|
254
|
+
onClick={() => setShowCustom(true)}
|
|
255
|
+
className="text-2xs text-po-text-ghost hover:text-po-text-secondary transition-colors duration-150 mt-1"
|
|
256
|
+
>
|
|
257
|
+
+ Custom
|
|
258
|
+
</button>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useCanvasStore } from '../canvasStore'
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
toolCallId: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const statusColors: Record<string, string> = {
|
|
8
|
+
active: 'text-po-accent',
|
|
9
|
+
completed: 'text-po-success',
|
|
10
|
+
error: 'text-po-error',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ToolCallInspector({ toolCallId }: Props) {
|
|
14
|
+
const toolCall = useCanvasStore((s) => s.activeToolCalls.get(toolCallId))
|
|
15
|
+
|
|
16
|
+
if (!toolCall) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="p-4 text-po-text-ghost text-xs font-mono">
|
|
19
|
+
Tool call not found or has expired.
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const duration = toolCall.completedAt
|
|
25
|
+
? ((toolCall.completedAt - toolCall.startedAt) / 1000).toFixed(1) + 's'
|
|
26
|
+
: ((Date.now() - toolCall.startedAt) / 1000).toFixed(1) + 's (running)'
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="p-4 space-y-4">
|
|
30
|
+
{/* Header */}
|
|
31
|
+
<div>
|
|
32
|
+
<h3 className="text-sm font-mono font-medium text-po-text-primary">{toolCall.toolName}</h3>
|
|
33
|
+
<div className="flex items-center gap-2 mt-1">
|
|
34
|
+
<span className={`text-xs font-mono font-medium ${statusColors[toolCall.status] || ''}`}>
|
|
35
|
+
{toolCall.status}
|
|
36
|
+
</span>
|
|
37
|
+
<span className="text-2xs text-po-text-ghost font-mono">{duration}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="text-xs text-po-text-tertiary mt-1">
|
|
40
|
+
Called by: <span className="text-po-accent font-mono">{toolCall.callerPO}</span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{/* Parameters */}
|
|
45
|
+
<div>
|
|
46
|
+
<h4 className="text-2xs font-medium text-po-text-ghost uppercase tracking-wider mb-2">Parameters</h4>
|
|
47
|
+
<div className="bg-po-surface-2 border border-po-border rounded p-2.5 overflow-auto max-h-48">
|
|
48
|
+
<pre className="text-xs text-po-text-secondary font-mono whitespace-pre-wrap">
|
|
49
|
+
{JSON.stringify(toolCall.params, null, 2)}
|
|
50
|
+
</pre>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Result */}
|
|
55
|
+
{toolCall.result && (
|
|
56
|
+
<div>
|
|
57
|
+
<h4 className="text-2xs font-medium text-po-text-ghost uppercase tracking-wider mb-2">Result</h4>
|
|
58
|
+
<div className="bg-po-surface-2 border border-po-border rounded p-2.5 overflow-auto max-h-64">
|
|
59
|
+
<pre className="text-xs text-po-text-secondary font-mono whitespace-pre-wrap">
|
|
60
|
+
{toolCall.result}
|
|
61
|
+
</pre>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|