prompt_objects 0.3.0 → 0.4.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 +27 -0
- data/Gemfile.lock +31 -29
- data/exe/prompt_objects +161 -0
- data/frontend/package-lock.json +123 -0
- data/frontend/package.json +4 -0
- data/frontend/src/App.tsx +58 -51
- 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/ChatPanel.tsx +13 -5
- data/frontend/src/components/Header.tsx +13 -1
- data/frontend/src/hooks/useWebSocket.ts +246 -189
- data/frontend/src/index.css +48 -0
- data/frontend/src/store/index.ts +19 -0
- data/frontend/src/types/index.ts +3 -0
- data/lib/prompt_objects/connectors/mcp.rb +3 -6
- data/lib/prompt_objects/environment.rb +8 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +1 -2
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +1 -2
- data/lib/prompt_objects/primitives/list_files.rb +1 -2
- data/lib/prompt_objects/prompt_object.rb +25 -7
- data/lib/prompt_objects/server/app.rb +9 -0
- data/lib/prompt_objects/server/public/assets/index-6y64NXFy.css +1 -0
- data/lib/prompt_objects/server/public/assets/index-xvyeb-5Z.js +4345 -0
- data/lib/prompt_objects/server/public/index.html +2 -2
- data/prompt_objects.gemspec +1 -1
- data/tools/thread-explorer.html +1043 -0
- metadata +18 -4
- 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,249 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js'
|
|
3
|
+
import { COLORS, CSS_COLORS, NODE, ANIMATION } from '../constants'
|
|
4
|
+
|
|
5
|
+
const vertexShader = /* glsl */ `
|
|
6
|
+
varying vec2 vUv;
|
|
7
|
+
void main() {
|
|
8
|
+
vUv = uv;
|
|
9
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
10
|
+
}
|
|
11
|
+
`
|
|
12
|
+
|
|
13
|
+
const fragmentShader = /* glsl */ `
|
|
14
|
+
uniform vec3 uColor;
|
|
15
|
+
uniform vec3 uGlowColor;
|
|
16
|
+
uniform float uGlowIntensity;
|
|
17
|
+
uniform float uTime;
|
|
18
|
+
uniform float uHovered;
|
|
19
|
+
uniform float uSelected;
|
|
20
|
+
|
|
21
|
+
varying vec2 vUv;
|
|
22
|
+
|
|
23
|
+
void main() {
|
|
24
|
+
// Distance from center (0 at center, 1 at edge)
|
|
25
|
+
float dist = length(vUv - 0.5) * 2.0;
|
|
26
|
+
|
|
27
|
+
// Base dark fill
|
|
28
|
+
vec3 base = uColor * 0.3;
|
|
29
|
+
|
|
30
|
+
// Edge glow
|
|
31
|
+
float edgeGlow = smoothstep(0.5, 1.0, dist) * uGlowIntensity;
|
|
32
|
+
// Animated pulse
|
|
33
|
+
float pulse = sin(uTime * ${ANIMATION.pulseSpeed.toFixed(1)}) * 0.15 + 0.85;
|
|
34
|
+
edgeGlow *= pulse;
|
|
35
|
+
|
|
36
|
+
vec3 color = mix(base, uGlowColor, edgeGlow);
|
|
37
|
+
|
|
38
|
+
// Hover brightening
|
|
39
|
+
color = mix(color, uGlowColor * 1.2, uHovered * 0.3);
|
|
40
|
+
|
|
41
|
+
// Selection brightening
|
|
42
|
+
color = mix(color, uGlowColor * 1.5, uSelected * 0.4);
|
|
43
|
+
|
|
44
|
+
// Alpha: solid center, slight transparency at edges
|
|
45
|
+
float alpha = smoothstep(1.0, 0.8, dist);
|
|
46
|
+
|
|
47
|
+
gl_FragColor = vec4(color, alpha);
|
|
48
|
+
}
|
|
49
|
+
`
|
|
50
|
+
|
|
51
|
+
type POStatus = 'idle' | 'thinking' | 'calling_tool'
|
|
52
|
+
|
|
53
|
+
const STATUS_COLORS: Record<POStatus, number> = {
|
|
54
|
+
idle: COLORS.statusIdle,
|
|
55
|
+
thinking: COLORS.statusThinking,
|
|
56
|
+
calling_tool: COLORS.statusCallingTool,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const STATUS_CSS_COLORS: Record<POStatus, string> = {
|
|
60
|
+
idle: CSS_COLORS.statusIdle,
|
|
61
|
+
thinking: CSS_COLORS.statusThinking,
|
|
62
|
+
calling_tool: CSS_COLORS.statusCallingTool,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Color for when this PO is being called by another PO (delegation)
|
|
66
|
+
const DELEGATED_COLOR = 0x06b6d4 // cyan-500
|
|
67
|
+
const DELEGATED_CSS_COLOR = '#06b6d4'
|
|
68
|
+
|
|
69
|
+
export class PONode {
|
|
70
|
+
readonly id: string
|
|
71
|
+
readonly group: THREE.Group
|
|
72
|
+
readonly mesh: THREE.Mesh
|
|
73
|
+
|
|
74
|
+
private material: THREE.ShaderMaterial
|
|
75
|
+
private statusRing: THREE.LineLoop
|
|
76
|
+
private statusRingMaterial: THREE.LineBasicMaterial
|
|
77
|
+
private label: CSS2DObject
|
|
78
|
+
private labelEl: HTMLDivElement
|
|
79
|
+
private nameEl: HTMLSpanElement
|
|
80
|
+
private statusEl: HTMLSpanElement
|
|
81
|
+
private badge: CSS2DObject
|
|
82
|
+
private badgeEl: HTMLDivElement
|
|
83
|
+
private badgeCountEl: HTMLSpanElement
|
|
84
|
+
|
|
85
|
+
private targetPosition = new THREE.Vector3()
|
|
86
|
+
|
|
87
|
+
constructor(id: string, name: string) {
|
|
88
|
+
this.id = id
|
|
89
|
+
this.group = new THREE.Group()
|
|
90
|
+
this.group.userData = { type: 'po', id }
|
|
91
|
+
|
|
92
|
+
// Hexagonal geometry
|
|
93
|
+
const geometry = new THREE.CircleGeometry(NODE.poRadius, NODE.poSides)
|
|
94
|
+
|
|
95
|
+
// Shader material
|
|
96
|
+
this.material = new THREE.ShaderMaterial({
|
|
97
|
+
vertexShader,
|
|
98
|
+
fragmentShader,
|
|
99
|
+
uniforms: {
|
|
100
|
+
uColor: { value: new THREE.Color(COLORS.nodeFill) },
|
|
101
|
+
uGlowColor: { value: new THREE.Color(COLORS.nodeGlow) },
|
|
102
|
+
uGlowIntensity: { value: 0.5 },
|
|
103
|
+
uTime: { value: 0 },
|
|
104
|
+
uHovered: { value: 0 },
|
|
105
|
+
uSelected: { value: 0 },
|
|
106
|
+
},
|
|
107
|
+
transparent: true,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
this.mesh = new THREE.Mesh(geometry, this.material)
|
|
111
|
+
this.mesh.userData = { type: 'po', id }
|
|
112
|
+
this.group.add(this.mesh)
|
|
113
|
+
|
|
114
|
+
// Status ring (hex outline)
|
|
115
|
+
const ringGeometry = new THREE.BufferGeometry()
|
|
116
|
+
const ringPoints: THREE.Vector3[] = []
|
|
117
|
+
for (let i = 0; i <= NODE.poSides; i++) {
|
|
118
|
+
const angle = (i / NODE.poSides) * Math.PI * 2 - Math.PI / 2
|
|
119
|
+
ringPoints.push(
|
|
120
|
+
new THREE.Vector3(
|
|
121
|
+
Math.cos(angle) * (NODE.poRadius + 3),
|
|
122
|
+
Math.sin(angle) * (NODE.poRadius + 3),
|
|
123
|
+
0
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
ringGeometry.setFromPoints(ringPoints)
|
|
128
|
+
|
|
129
|
+
this.statusRingMaterial = new THREE.LineBasicMaterial({
|
|
130
|
+
color: STATUS_COLORS.idle,
|
|
131
|
+
transparent: true,
|
|
132
|
+
opacity: 0.6,
|
|
133
|
+
})
|
|
134
|
+
this.statusRing = new THREE.LineLoop(ringGeometry, this.statusRingMaterial)
|
|
135
|
+
this.group.add(this.statusRing)
|
|
136
|
+
|
|
137
|
+
// CSS2D Label (name + status)
|
|
138
|
+
this.labelEl = document.createElement('div')
|
|
139
|
+
this.labelEl.className = 'canvas-node-label'
|
|
140
|
+
|
|
141
|
+
this.nameEl = document.createElement('span')
|
|
142
|
+
this.nameEl.className = 'canvas-node-name'
|
|
143
|
+
this.nameEl.textContent = name
|
|
144
|
+
|
|
145
|
+
this.statusEl = document.createElement('span')
|
|
146
|
+
this.statusEl.className = 'canvas-node-status'
|
|
147
|
+
this.statusEl.textContent = 'idle'
|
|
148
|
+
|
|
149
|
+
this.labelEl.appendChild(this.nameEl)
|
|
150
|
+
this.labelEl.appendChild(this.statusEl)
|
|
151
|
+
|
|
152
|
+
this.label = new CSS2DObject(this.labelEl)
|
|
153
|
+
this.label.position.set(0, -NODE.labelOffsetY, 0)
|
|
154
|
+
this.group.add(this.label)
|
|
155
|
+
|
|
156
|
+
// Notification badge
|
|
157
|
+
this.badgeEl = document.createElement('div')
|
|
158
|
+
this.badgeEl.className = 'canvas-node-badge'
|
|
159
|
+
this.badgeEl.style.display = 'none'
|
|
160
|
+
|
|
161
|
+
this.badgeCountEl = document.createElement('span')
|
|
162
|
+
this.badgeCountEl.textContent = '0'
|
|
163
|
+
this.badgeEl.appendChild(this.badgeCountEl)
|
|
164
|
+
|
|
165
|
+
this.badge = new CSS2DObject(this.badgeEl)
|
|
166
|
+
this.badge.position.set(NODE.badgeOffsetX, -NODE.badgeOffsetY, 0)
|
|
167
|
+
this.group.add(this.badge)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setStatus(status: POStatus): void {
|
|
171
|
+
this.statusRingMaterial.color.setHex(STATUS_COLORS[status])
|
|
172
|
+
|
|
173
|
+
// Update glow intensity based on status
|
|
174
|
+
const intensity = status === 'idle' ? 0.3 : status === 'thinking' ? 0.8 : 0.6
|
|
175
|
+
this.material.uniforms.uGlowIntensity.value = intensity
|
|
176
|
+
|
|
177
|
+
// Update glow color for calling_tool
|
|
178
|
+
if (status === 'calling_tool') {
|
|
179
|
+
this.material.uniforms.uGlowColor.value.setHex(COLORS.statusCallingTool)
|
|
180
|
+
} else {
|
|
181
|
+
this.material.uniforms.uGlowColor.value.setHex(COLORS.nodeGlow)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.statusEl.textContent = status.replace('_', ' ')
|
|
185
|
+
this.statusEl.style.color = STATUS_CSS_COLORS[status]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
setDelegatedBy(callerName: string): void {
|
|
189
|
+
// Override visual to show this PO is working on behalf of another PO.
|
|
190
|
+
// The server doesn't send status updates for delegated POs, so we
|
|
191
|
+
// infer this from the caller's tool_calls.
|
|
192
|
+
this.statusRingMaterial.color.setHex(DELEGATED_COLOR)
|
|
193
|
+
this.statusRingMaterial.opacity = 1.0
|
|
194
|
+
this.material.uniforms.uGlowColor.value.setHex(DELEGATED_COLOR)
|
|
195
|
+
this.material.uniforms.uGlowIntensity.value = 0.7
|
|
196
|
+
this.statusEl.textContent = `called by ${callerName}`
|
|
197
|
+
this.statusEl.style.color = DELEGATED_CSS_COLOR
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
clearDelegated(): void {
|
|
201
|
+
// Restore to whatever the server status is — caller should
|
|
202
|
+
// call setStatus() after this to re-apply server state.
|
|
203
|
+
this.statusRingMaterial.opacity = 0.6
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setNotificationCount(count: number): void {
|
|
207
|
+
if (count > 0) {
|
|
208
|
+
this.badgeEl.style.display = 'flex'
|
|
209
|
+
this.badgeCountEl.textContent = String(count)
|
|
210
|
+
} else {
|
|
211
|
+
this.badgeEl.style.display = 'none'
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
setHovered(hovered: boolean): void {
|
|
216
|
+
this.material.uniforms.uHovered.value = hovered ? 1 : 0
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
setSelected(selected: boolean): void {
|
|
220
|
+
this.material.uniforms.uSelected.value = selected ? 1 : 0
|
|
221
|
+
this.statusRingMaterial.opacity = selected ? 1.0 : 0.6
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
setPosition(x: number, y: number): void {
|
|
225
|
+
this.targetPosition.set(x, y, 0)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
getPosition(): THREE.Vector3 {
|
|
229
|
+
return this.group.position.clone()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
update(_delta: number, elapsed: number): void {
|
|
233
|
+
// Lerp toward target position
|
|
234
|
+
this.group.position.lerp(this.targetPosition, ANIMATION.positionLerpFactor)
|
|
235
|
+
|
|
236
|
+
// Update time uniform for pulse animation
|
|
237
|
+
this.material.uniforms.uTime.value = elapsed
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
dispose(): void {
|
|
241
|
+
this.mesh.geometry.dispose()
|
|
242
|
+
this.material.dispose()
|
|
243
|
+
this.statusRing.geometry.dispose()
|
|
244
|
+
this.statusRingMaterial.dispose()
|
|
245
|
+
this.labelEl.remove()
|
|
246
|
+
this.badgeEl.remove()
|
|
247
|
+
this.group.parent?.remove(this.group)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js'
|
|
3
|
+
import { COLORS, NODE, ANIMATION } from '../constants'
|
|
4
|
+
|
|
5
|
+
type Phase = 'fadein' | 'active' | 'fadeout' | 'expired'
|
|
6
|
+
|
|
7
|
+
export class ToolCallNode {
|
|
8
|
+
readonly id: string
|
|
9
|
+
readonly callerPO: string
|
|
10
|
+
readonly group: THREE.Group
|
|
11
|
+
readonly mesh: THREE.Mesh
|
|
12
|
+
|
|
13
|
+
private material: THREE.MeshBasicMaterial
|
|
14
|
+
private label: CSS2DObject
|
|
15
|
+
private labelEl: HTMLDivElement
|
|
16
|
+
private phase: Phase = 'fadein'
|
|
17
|
+
private phaseTime = 0
|
|
18
|
+
private targetPosition = new THREE.Vector3()
|
|
19
|
+
|
|
20
|
+
constructor(id: string, toolName: string, callerPO: string) {
|
|
21
|
+
this.id = id
|
|
22
|
+
this.callerPO = callerPO
|
|
23
|
+
this.group = new THREE.Group()
|
|
24
|
+
this.group.userData = { type: 'toolcall', id }
|
|
25
|
+
|
|
26
|
+
// Diamond shape (rotated square)
|
|
27
|
+
const r = NODE.toolCallRadius
|
|
28
|
+
const shape = new THREE.Shape()
|
|
29
|
+
shape.moveTo(0, r)
|
|
30
|
+
shape.lineTo(r, 0)
|
|
31
|
+
shape.lineTo(0, -r)
|
|
32
|
+
shape.lineTo(-r, 0)
|
|
33
|
+
shape.closePath()
|
|
34
|
+
|
|
35
|
+
const geometry = new THREE.ShapeGeometry(shape)
|
|
36
|
+
|
|
37
|
+
this.material = new THREE.MeshBasicMaterial({
|
|
38
|
+
color: COLORS.toolCallFill,
|
|
39
|
+
transparent: true,
|
|
40
|
+
opacity: 0,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
this.mesh = new THREE.Mesh(geometry, this.material)
|
|
44
|
+
this.mesh.userData = { type: 'toolcall', id }
|
|
45
|
+
this.group.add(this.mesh)
|
|
46
|
+
|
|
47
|
+
// Label
|
|
48
|
+
this.labelEl = document.createElement('div')
|
|
49
|
+
this.labelEl.className = 'canvas-toolcall-label'
|
|
50
|
+
this.labelEl.textContent = toolName
|
|
51
|
+
|
|
52
|
+
this.label = new CSS2DObject(this.labelEl)
|
|
53
|
+
this.label.position.set(0, -(NODE.toolCallRadius + 12), 0)
|
|
54
|
+
this.group.add(this.label)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setPosition(x: number, y: number): void {
|
|
58
|
+
this.targetPosition.set(x, y, 0)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
triggerFadeOut(): void {
|
|
62
|
+
if (this.phase !== 'expired') {
|
|
63
|
+
this.phase = 'fadeout'
|
|
64
|
+
this.phaseTime = 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isExpired(): boolean {
|
|
69
|
+
return this.phase === 'expired'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
update(delta: number): void {
|
|
73
|
+
this.phaseTime += delta
|
|
74
|
+
this.group.position.lerp(this.targetPosition, ANIMATION.positionLerpFactor)
|
|
75
|
+
|
|
76
|
+
switch (this.phase) {
|
|
77
|
+
case 'fadein':
|
|
78
|
+
this.material.opacity = Math.min(this.phaseTime / ANIMATION.toolCallFadeInDuration, 1)
|
|
79
|
+
if (this.phaseTime >= ANIMATION.toolCallFadeInDuration) {
|
|
80
|
+
this.phase = 'active'
|
|
81
|
+
this.phaseTime = 0
|
|
82
|
+
}
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
case 'active':
|
|
86
|
+
this.material.opacity = 1
|
|
87
|
+
if (this.phaseTime >= ANIMATION.toolCallActiveDuration) {
|
|
88
|
+
this.phase = 'fadeout'
|
|
89
|
+
this.phaseTime = 0
|
|
90
|
+
}
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
case 'fadeout':
|
|
94
|
+
this.material.opacity = Math.max(
|
|
95
|
+
1 - this.phaseTime / ANIMATION.toolCallFadeOutDuration,
|
|
96
|
+
0
|
|
97
|
+
)
|
|
98
|
+
this.labelEl.style.opacity = String(this.material.opacity)
|
|
99
|
+
if (this.phaseTime >= ANIMATION.toolCallFadeOutDuration) {
|
|
100
|
+
this.phase = 'expired'
|
|
101
|
+
}
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
case 'expired':
|
|
105
|
+
this.material.opacity = 0
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
dispose(): void {
|
|
111
|
+
this.mesh.geometry.dispose()
|
|
112
|
+
this.material.dispose()
|
|
113
|
+
this.labelEl.remove()
|
|
114
|
+
this.group.parent?.remove(this.group)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Canvas-specific type definitions
|
|
2
|
+
|
|
3
|
+
export interface CanvasNodeSelection {
|
|
4
|
+
type: 'po' | 'toolcall'
|
|
5
|
+
id: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ActiveToolCall {
|
|
9
|
+
id: string
|
|
10
|
+
toolName: string
|
|
11
|
+
callerPO: string
|
|
12
|
+
params: Record<string, unknown>
|
|
13
|
+
status: 'active' | 'completed' | 'error'
|
|
14
|
+
result?: string
|
|
15
|
+
startedAt: number
|
|
16
|
+
completedAt?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ActiveMessageArc {
|
|
20
|
+
id: string
|
|
21
|
+
from: string
|
|
22
|
+
to: string
|
|
23
|
+
timestamp: number
|
|
24
|
+
}
|
|
@@ -12,11 +12,13 @@ export function ChatPanel({ po, sendMessage }: ChatPanelProps) {
|
|
|
12
12
|
const [input, setInput] = useState('')
|
|
13
13
|
const [continueThread, setContinueThread] = useState(false)
|
|
14
14
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
15
|
-
const { streamingContent } = useStore()
|
|
15
|
+
const { streamingContent, connected } = useStore()
|
|
16
16
|
|
|
17
17
|
const messages = po.current_session?.messages || []
|
|
18
18
|
const streaming = streamingContent[po.name]
|
|
19
19
|
const hasMessages = messages.length > 0
|
|
20
|
+
const isBusy = po.status !== 'idle' && connected
|
|
21
|
+
const canSend = connected && !isBusy && !!input.trim()
|
|
20
22
|
|
|
21
23
|
useEffect(() => {
|
|
22
24
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
@@ -72,18 +74,24 @@ export function ChatPanel({ po, sendMessage }: ChatPanelProps) {
|
|
|
72
74
|
|
|
73
75
|
{/* Input */}
|
|
74
76
|
<form onSubmit={handleSubmit} className="border-t border-po-border p-4">
|
|
77
|
+
{!connected && (
|
|
78
|
+
<div className="mb-2 text-xs text-po-warning flex items-center gap-2">
|
|
79
|
+
<div className="w-2 h-2 rounded-full bg-po-warning animate-pulse" />
|
|
80
|
+
Reconnecting...
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
75
83
|
<div className="flex gap-3">
|
|
76
84
|
<input
|
|
77
85
|
type="text"
|
|
78
86
|
value={input}
|
|
79
87
|
onChange={(e) => setInput(e.target.value)}
|
|
80
|
-
placeholder={`Message ${po.name}...`}
|
|
81
|
-
className="flex-1 bg-po-surface border border-po-border rounded-lg px-4 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-po-accent"
|
|
82
|
-
disabled={
|
|
88
|
+
placeholder={!connected ? 'Disconnected — waiting to reconnect...' : isBusy ? `${po.name} is ${po.status.replace('_', ' ')}...` : `Message ${po.name}...`}
|
|
89
|
+
className="flex-1 bg-po-surface border border-po-border rounded-lg px-4 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-po-accent disabled:opacity-50"
|
|
90
|
+
disabled={isBusy}
|
|
83
91
|
/>
|
|
84
92
|
<button
|
|
85
93
|
type="submit"
|
|
86
|
-
disabled={!
|
|
94
|
+
disabled={!canSend}
|
|
87
95
|
className="px-4 py-2 bg-po-accent text-white rounded-lg font-medium hover:bg-po-accent/80 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
88
96
|
>
|
|
89
97
|
Send
|
|
@@ -7,7 +7,7 @@ interface Props {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function Header({ switchLLM }: Props) {
|
|
10
|
-
const { connected, environment, selectedPO, selectPO, toggleBus, busOpen, notifications } =
|
|
10
|
+
const { connected, environment, selectedPO, selectPO, toggleBus, busOpen, notifications, currentView, setCurrentView } =
|
|
11
11
|
useStore()
|
|
12
12
|
const notificationCount = useNotificationCount()
|
|
13
13
|
const [showNotifications, setShowNotifications] = useState(false)
|
|
@@ -125,6 +125,18 @@ export function Header({ switchLLM }: Props) {
|
|
|
125
125
|
)}
|
|
126
126
|
</div>
|
|
127
127
|
|
|
128
|
+
{/* Canvas toggle */}
|
|
129
|
+
<button
|
|
130
|
+
onClick={() => setCurrentView(currentView === 'canvas' ? 'dashboard' : 'canvas')}
|
|
131
|
+
className={`px-3 py-1.5 text-sm rounded transition-colors ${
|
|
132
|
+
currentView === 'canvas'
|
|
133
|
+
? 'bg-po-accent text-white'
|
|
134
|
+
: 'bg-po-border text-gray-300 hover:bg-po-accent/50'
|
|
135
|
+
}`}
|
|
136
|
+
>
|
|
137
|
+
Canvas
|
|
138
|
+
</button>
|
|
139
|
+
|
|
128
140
|
{/* Message Bus toggle */}
|
|
129
141
|
<button
|
|
130
142
|
onClick={toggleBus}
|