prompt_objects 0.4.0 → 0.6.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 +33 -0
- data/CLAUDE.md +113 -44
- data/README.md +140 -14
- data/frontend/index.html +5 -1
- data/frontend/src/App.tsx +72 -79
- data/frontend/src/canvas/CanvasView.tsx +5 -5
- data/frontend/src/canvas/constants.ts +31 -31
- data/frontend/src/canvas/inspector/InspectorPanel.tsx +4 -4
- data/frontend/src/canvas/inspector/POInspector.tsx +35 -35
- data/frontend/src/canvas/inspector/ToolCallInspector.tsx +13 -13
- data/frontend/src/canvas/nodes/PONode.ts +2 -2
- data/frontend/src/components/ContextMenu.tsx +5 -4
- data/frontend/src/components/EnvDataPane.tsx +69 -0
- data/frontend/src/components/Inspector.tsx +263 -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 +70 -0
- data/frontend/src/index.css +27 -10
- data/frontend/src/store/index.ts +36 -0
- data/frontend/src/types/index.ts +13 -0
- data/frontend/tailwind.config.js +28 -9
- data/lib/prompt_objects/capability.rb +23 -1
- data/lib/prompt_objects/connectors/mcp.rb +2 -16
- data/lib/prompt_objects/environment.rb +15 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +1 -6
- data/lib/prompt_objects/prompt_object.rb +239 -7
- data/lib/prompt_objects/server/api/routes.rb +16 -48
- data/lib/prompt_objects/server/app.rb +14 -0
- data/lib/prompt_objects/server/public/assets/{index-xvyeb-5Z.js → index-DEPawnfZ.js} +206 -206
- data/lib/prompt_objects/server/public/assets/index-oMrRce1m.css +1 -0
- data/lib/prompt_objects/server/public/index.html +7 -3
- data/lib/prompt_objects/server/websocket_handler.rb +41 -98
- data/lib/prompt_objects/server.rb +6 -62
- data/lib/prompt_objects/session/store.rb +176 -4
- data/lib/prompt_objects/universal/delete_env_data.rb +70 -0
- data/lib/prompt_objects/universal/get_env_data.rb +64 -0
- data/lib/prompt_objects/universal/list_env_data.rb +61 -0
- data/lib/prompt_objects/universal/store_env_data.rb +87 -0
- data/lib/prompt_objects/universal/update_env_data.rb +88 -0
- data/lib/prompt_objects.rb +6 -1
- data/prompt_objects.gemspec +1 -1
- data/templates/arc-agi-1/objects/observer.md +4 -0
- data/templates/arc-agi-1/objects/solver.md +10 -1
- data/templates/arc-agi-1/objects/verifier.md +4 -0
- 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
- data/tools/thread-explorer.html +27 -0
- metadata +18 -16
- data/Gemfile.lock +0 -233
- data/IMPLEMENTATION_PLAN.md +0 -1073
- data/design-doc-v2.md +0 -1232
- data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
- data/frontend/src/components/ChatPanel.tsx +0 -296
- data/frontend/src/components/Dashboard.tsx +0 -83
- data/frontend/src/components/Header.tsx +0 -153
- 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-6y64NXFy.css +0 -1
|
@@ -5,28 +5,28 @@ interface NotificationPanelProps {
|
|
|
5
5
|
respondToNotification: (id: string, response: string) => void
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function NotificationPanel({
|
|
9
|
-
respondToNotification,
|
|
10
|
-
}: NotificationPanelProps) {
|
|
8
|
+
export function NotificationPanel({ respondToNotification }: NotificationPanelProps) {
|
|
11
9
|
const { notifications, selectPO } = useStore()
|
|
12
10
|
|
|
13
11
|
if (notifications.length === 0) return null
|
|
14
12
|
|
|
15
13
|
return (
|
|
16
|
-
<div className="fixed bottom-4 right-4 w-96 max-h-[60vh] overflow-auto bg-po-surface border border-po-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
<div className="fixed bottom-4 right-4 w-96 max-h-[60vh] overflow-auto bg-po-surface-2 border border-po-warning/30 rounded-lg shadow-2xl z-50">
|
|
15
|
+
{/* Header */}
|
|
16
|
+
<div className="sticky top-0 bg-po-surface-2 border-b border-po-border px-3 py-2 flex items-center gap-2">
|
|
17
|
+
<div className="w-2 h-2 rounded-full bg-po-warning animate-pulse" />
|
|
18
|
+
<span className="text-xs font-medium text-po-text-primary flex-1">
|
|
19
|
+
Pending Requests ({notifications.length})
|
|
20
|
+
</span>
|
|
21
21
|
</div>
|
|
22
|
+
|
|
23
|
+
{/* Notifications */}
|
|
22
24
|
<div className="p-2 space-y-2">
|
|
23
25
|
{notifications.map((notification) => (
|
|
24
26
|
<NotificationCard
|
|
25
27
|
key={notification.id}
|
|
26
28
|
notification={notification}
|
|
27
|
-
onRespond={(response) =>
|
|
28
|
-
respondToNotification(notification.id, response)
|
|
29
|
-
}
|
|
29
|
+
onRespond={(response) => respondToNotification(notification.id, response)}
|
|
30
30
|
onViewPO={() => selectPO(notification.po_name)}
|
|
31
31
|
/>
|
|
32
32
|
))}
|
|
@@ -47,18 +47,10 @@ interface NotificationCardProps {
|
|
|
47
47
|
onViewPO: () => void
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function NotificationCard({
|
|
51
|
-
notification,
|
|
52
|
-
onRespond,
|
|
53
|
-
onViewPO,
|
|
54
|
-
}: NotificationCardProps) {
|
|
50
|
+
function NotificationCard({ notification, onRespond, onViewPO }: NotificationCardProps) {
|
|
55
51
|
const [customInput, setCustomInput] = useState('')
|
|
56
52
|
const [showCustom, setShowCustom] = useState(false)
|
|
57
53
|
|
|
58
|
-
const handleOptionClick = (option: string) => {
|
|
59
|
-
onRespond(option)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
54
|
const handleCustomSubmit = () => {
|
|
63
55
|
if (customInput.trim()) {
|
|
64
56
|
onRespond(customInput.trim())
|
|
@@ -68,28 +60,31 @@ function NotificationCard({
|
|
|
68
60
|
}
|
|
69
61
|
|
|
70
62
|
return (
|
|
71
|
-
<div className="bg-po-
|
|
63
|
+
<div className="bg-po-surface border border-po-border rounded-lg p-3">
|
|
64
|
+
{/* Header: type badge + PO name */}
|
|
72
65
|
<div className="flex items-center gap-2 mb-2">
|
|
73
|
-
<span className="text-
|
|
66
|
+
<span className="text-2xs font-mono bg-po-warning text-po-bg px-1.5 py-0.5 rounded font-bold">
|
|
74
67
|
{notification.type}
|
|
75
68
|
</span>
|
|
76
69
|
<button
|
|
77
70
|
onClick={onViewPO}
|
|
78
|
-
className="text-xs text-po-accent hover:underline"
|
|
71
|
+
className="text-xs font-mono text-po-accent hover:underline"
|
|
79
72
|
>
|
|
80
73
|
{notification.po_name}
|
|
81
74
|
</button>
|
|
82
75
|
</div>
|
|
83
76
|
|
|
84
|
-
|
|
77
|
+
{/* Message */}
|
|
78
|
+
<p className="text-xs text-po-text-primary mb-3">{notification.message}</p>
|
|
85
79
|
|
|
80
|
+
{/* Quick response options */}
|
|
86
81
|
{notification.options.length > 0 && (
|
|
87
|
-
<div className="flex flex-wrap gap-
|
|
82
|
+
<div className="flex flex-wrap gap-1.5 mb-2">
|
|
88
83
|
{notification.options.map((option, index) => (
|
|
89
84
|
<button
|
|
90
85
|
key={index}
|
|
91
|
-
onClick={() =>
|
|
92
|
-
className="px-
|
|
86
|
+
onClick={() => onRespond(option)}
|
|
87
|
+
className="px-2.5 py-1 text-xs bg-po-surface-2 border border-po-border rounded hover:border-po-accent hover:text-po-accent transition-colors duration-150 text-po-text-secondary"
|
|
93
88
|
>
|
|
94
89
|
{option}
|
|
95
90
|
</button>
|
|
@@ -97,34 +92,35 @@ function NotificationCard({
|
|
|
97
92
|
</div>
|
|
98
93
|
)}
|
|
99
94
|
|
|
95
|
+
{/* Custom response */}
|
|
100
96
|
{showCustom ? (
|
|
101
|
-
<div className="flex gap-
|
|
97
|
+
<div className="flex gap-1.5">
|
|
102
98
|
<input
|
|
103
99
|
type="text"
|
|
104
100
|
value={customInput}
|
|
105
101
|
onChange={(e) => setCustomInput(e.target.value)}
|
|
106
102
|
placeholder="Custom response..."
|
|
107
|
-
className="flex-1 bg-po-surface border border-po-border rounded px-2 py-1 text-
|
|
103
|
+
className="flex-1 bg-po-surface-2 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"
|
|
108
104
|
onKeyDown={(e) => e.key === 'Enter' && handleCustomSubmit()}
|
|
109
105
|
autoFocus
|
|
110
106
|
/>
|
|
111
107
|
<button
|
|
112
108
|
onClick={handleCustomSubmit}
|
|
113
|
-
className="px-2 py-1 text-
|
|
109
|
+
className="px-2 py-1 text-xs bg-po-accent text-po-bg rounded font-medium hover:bg-po-accent-muted transition-colors duration-150"
|
|
114
110
|
>
|
|
115
111
|
Send
|
|
116
112
|
</button>
|
|
117
113
|
<button
|
|
118
114
|
onClick={() => setShowCustom(false)}
|
|
119
|
-
className="
|
|
115
|
+
className="text-po-text-ghost hover:text-po-text-secondary transition-colors duration-150"
|
|
120
116
|
>
|
|
121
|
-
|
|
117
|
+
{'\u2715'}
|
|
122
118
|
</button>
|
|
123
119
|
</div>
|
|
124
120
|
) : (
|
|
125
121
|
<button
|
|
126
122
|
onClick={() => setShowCustom(true)}
|
|
127
|
-
className="text-
|
|
123
|
+
className="text-2xs text-po-text-ghost hover:text-po-text-secondary transition-colors duration-150"
|
|
128
124
|
>
|
|
129
125
|
+ Custom response
|
|
130
126
|
</button>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { usePromptObjects, useStore, usePONotifications } from '../store'
|
|
2
|
+
import type { PromptObject } from '../types'
|
|
3
|
+
|
|
4
|
+
export function ObjectList() {
|
|
5
|
+
const promptObjects = usePromptObjects()
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<aside className="h-full bg-po-surface border-r border-po-border flex flex-col overflow-hidden">
|
|
9
|
+
{/* Header */}
|
|
10
|
+
<div className="px-3 py-2 border-b border-po-border">
|
|
11
|
+
<span className="text-2xs font-medium text-po-text-ghost uppercase tracking-wider">
|
|
12
|
+
Objects ({promptObjects.length})
|
|
13
|
+
</span>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
{/* List */}
|
|
17
|
+
<div className="flex-1 overflow-auto py-1">
|
|
18
|
+
{promptObjects.length === 0 ? (
|
|
19
|
+
<div className="px-3 py-4 text-2xs text-po-text-ghost text-center">
|
|
20
|
+
Waiting for connection...
|
|
21
|
+
</div>
|
|
22
|
+
) : (
|
|
23
|
+
promptObjects.map((po) => (
|
|
24
|
+
<ObjectItem key={po.name} po={po} />
|
|
25
|
+
))
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
</aside>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ObjectItem({ po }: { po: PromptObject }) {
|
|
33
|
+
const { selectPO, selectedPO } = useStore()
|
|
34
|
+
const notifications = usePONotifications(po.name)
|
|
35
|
+
const isSelected = selectedPO === po.name
|
|
36
|
+
|
|
37
|
+
const isActive = po.status !== 'idle'
|
|
38
|
+
|
|
39
|
+
const statusDot = {
|
|
40
|
+
idle: 'bg-po-status-idle',
|
|
41
|
+
thinking: 'bg-po-status-active',
|
|
42
|
+
calling_tool: 'bg-po-status-calling',
|
|
43
|
+
}[po.status] || 'bg-po-status-idle'
|
|
44
|
+
|
|
45
|
+
const statusGlow = {
|
|
46
|
+
idle: '',
|
|
47
|
+
thinking: 'shadow-[0_0_5px_rgba(212,149,42,0.6)]',
|
|
48
|
+
calling_tool: 'shadow-[0_0_5px_rgba(59,154,110,0.6)]',
|
|
49
|
+
}[po.status] || ''
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<button
|
|
53
|
+
onClick={() => selectPO(po.name)}
|
|
54
|
+
className={`w-full text-left h-7 px-3 flex items-center gap-2 transition-colors duration-150 ${
|
|
55
|
+
isSelected
|
|
56
|
+
? 'bg-po-accent-wash border-l-2 border-po-accent'
|
|
57
|
+
: 'border-l-2 border-transparent hover:bg-po-surface-2'
|
|
58
|
+
}`}
|
|
59
|
+
>
|
|
60
|
+
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${statusDot} ${statusGlow} ${isActive ? 'animate-pulse' : ''}`} />
|
|
61
|
+
<span className={`flex-1 truncate font-mono text-xs ${
|
|
62
|
+
isSelected ? 'text-po-text-primary' : 'text-po-text-secondary'
|
|
63
|
+
}`}>
|
|
64
|
+
{po.name}
|
|
65
|
+
</span>
|
|
66
|
+
{po.delegated_by && (
|
|
67
|
+
<span className="text-2xs text-po-status-delegated truncate max-w-[60px]">
|
|
68
|
+
{po.delegated_by}
|
|
69
|
+
</span>
|
|
70
|
+
)}
|
|
71
|
+
{notifications.length > 0 && (
|
|
72
|
+
<span className="text-2xs font-mono bg-po-warning text-po-bg px-1 rounded font-bold flex-shrink-0">
|
|
73
|
+
{notifications.length}
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
</button>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
interface PaneSlotProps {
|
|
2
|
+
label: string
|
|
3
|
+
collapsed: boolean
|
|
4
|
+
onToggle: () => void
|
|
5
|
+
height: number
|
|
6
|
+
resizeHandle?: React.ReactNode
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PaneSlot({ label, collapsed, onToggle, height, resizeHandle, children }: PaneSlotProps) {
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<div
|
|
14
|
+
className="h-7 bg-po-surface-2 border-b border-po-border flex items-center px-3 cursor-pointer hover:bg-po-surface-3 transition-colors duration-150 flex-shrink-0 select-none"
|
|
15
|
+
onClick={onToggle}
|
|
16
|
+
>
|
|
17
|
+
<span className="text-2xs font-mono text-po-text-secondary flex-1">{label}</span>
|
|
18
|
+
<span className="text-xs text-po-text-ghost">{collapsed ? '▼' : '▲'}</span>
|
|
19
|
+
</div>
|
|
20
|
+
{!collapsed && (
|
|
21
|
+
<>
|
|
22
|
+
<div className="flex-shrink-0 overflow-hidden" style={{ height }}>
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
{resizeHandle}
|
|
26
|
+
</>
|
|
27
|
+
)}
|
|
28
|
+
</>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
2
|
+
import type { PromptObject, CapabilityInfo } from '../types'
|
|
3
|
+
|
|
4
|
+
interface SourcePaneProps {
|
|
5
|
+
po: PromptObject
|
|
6
|
+
selectedCapability: CapabilityInfo | null
|
|
7
|
+
onSave?: (prompt: string) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function SourcePane({ po, selectedCapability, onSave }: SourcePaneProps) {
|
|
11
|
+
const prompt = po.prompt || ''
|
|
12
|
+
const config = po.config || {}
|
|
13
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
14
|
+
const [editedPrompt, setEditedPrompt] = useState(prompt)
|
|
15
|
+
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved')
|
|
16
|
+
const saveTimeoutRef = useRef<number | null>(null)
|
|
17
|
+
|
|
18
|
+
// Sync editedPrompt when po.prompt changes from server
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!isEditing) {
|
|
21
|
+
setEditedPrompt(prompt)
|
|
22
|
+
}
|
|
23
|
+
}, [prompt, isEditing])
|
|
24
|
+
|
|
25
|
+
// Debounced auto-save
|
|
26
|
+
const debouncedSave = useCallback((newPrompt: string) => {
|
|
27
|
+
if (saveTimeoutRef.current) {
|
|
28
|
+
clearTimeout(saveTimeoutRef.current)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setSaveStatus('unsaved')
|
|
32
|
+
|
|
33
|
+
saveTimeoutRef.current = window.setTimeout(() => {
|
|
34
|
+
if (onSave && newPrompt !== prompt) {
|
|
35
|
+
setSaveStatus('saving')
|
|
36
|
+
onSave(newPrompt)
|
|
37
|
+
setTimeout(() => setSaveStatus('saved'), 500)
|
|
38
|
+
} else {
|
|
39
|
+
setSaveStatus('saved')
|
|
40
|
+
}
|
|
41
|
+
}, 1000)
|
|
42
|
+
}, [onSave, prompt])
|
|
43
|
+
|
|
44
|
+
const handlePromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
45
|
+
const newPrompt = e.target.value
|
|
46
|
+
setEditedPrompt(newPrompt)
|
|
47
|
+
debouncedSave(newPrompt)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handleToggleEdit = () => {
|
|
51
|
+
if (isEditing) {
|
|
52
|
+
// Save on exit
|
|
53
|
+
if (saveTimeoutRef.current) {
|
|
54
|
+
clearTimeout(saveTimeoutRef.current)
|
|
55
|
+
}
|
|
56
|
+
if (editedPrompt !== prompt && onSave) {
|
|
57
|
+
onSave(editedPrompt)
|
|
58
|
+
}
|
|
59
|
+
setIsEditing(false)
|
|
60
|
+
setSaveStatus('saved')
|
|
61
|
+
} else {
|
|
62
|
+
setEditedPrompt(prompt)
|
|
63
|
+
setIsEditing(true)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Cleanup timeout on unmount
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
return () => {
|
|
70
|
+
if (saveTimeoutRef.current) {
|
|
71
|
+
clearTimeout(saveTimeoutRef.current)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}, [])
|
|
75
|
+
|
|
76
|
+
// Capability detail view
|
|
77
|
+
if (selectedCapability) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex-1 overflow-auto bg-po-bg">
|
|
80
|
+
<div className="px-3 py-2 border-b border-po-border bg-po-surface">
|
|
81
|
+
<span className="font-mono text-xs text-po-accent">{selectedCapability.name}</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="p-3 space-y-3">
|
|
84
|
+
<p className="text-xs text-po-text-secondary">{selectedCapability.description}</p>
|
|
85
|
+
{selectedCapability.parameters && (
|
|
86
|
+
<ParametersView parameters={selectedCapability.parameters} />
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Prompt source view
|
|
94
|
+
return (
|
|
95
|
+
<div className="flex-1 overflow-auto bg-po-bg flex flex-col">
|
|
96
|
+
{/* Header with edit toggle and save status */}
|
|
97
|
+
<div className="px-3 py-1.5 border-b border-po-border bg-po-surface flex items-center gap-2 flex-shrink-0">
|
|
98
|
+
<span className="text-2xs text-po-text-ghost uppercase tracking-wider flex-1">Source</span>
|
|
99
|
+
|
|
100
|
+
{isEditing && (
|
|
101
|
+
<span className="flex items-center gap-1">
|
|
102
|
+
<span className={`w-1.5 h-1.5 rounded-full ${
|
|
103
|
+
saveStatus === 'saved' ? 'bg-po-success' :
|
|
104
|
+
saveStatus === 'saving' ? 'bg-po-accent animate-pulse' :
|
|
105
|
+
'bg-po-text-ghost'
|
|
106
|
+
}`} />
|
|
107
|
+
<span className={`text-2xs ${
|
|
108
|
+
saveStatus === 'saved' ? 'text-po-success' :
|
|
109
|
+
saveStatus === 'saving' ? 'text-po-accent' :
|
|
110
|
+
'text-po-text-ghost'
|
|
111
|
+
}`}>
|
|
112
|
+
{saveStatus === 'saved' ? 'saved' : saveStatus === 'saving' ? 'saving' : 'unsaved'}
|
|
113
|
+
</span>
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
<button
|
|
118
|
+
onClick={handleToggleEdit}
|
|
119
|
+
className={`text-2xs px-1.5 py-0.5 rounded transition-colors duration-150 ${
|
|
120
|
+
isEditing
|
|
121
|
+
? 'bg-po-accent text-po-bg'
|
|
122
|
+
: 'text-po-text-tertiary hover:text-po-text-primary hover:bg-po-surface-2'
|
|
123
|
+
}`}
|
|
124
|
+
>
|
|
125
|
+
{isEditing ? 'Done' : 'Edit'}
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{/* Config (collapsed) */}
|
|
130
|
+
{Object.keys(config).length > 0 && (
|
|
131
|
+
<details className="border-b border-po-border">
|
|
132
|
+
<summary className="px-3 py-1 text-2xs text-po-text-ghost cursor-pointer hover:text-po-text-secondary transition-colors duration-150">
|
|
133
|
+
Frontmatter
|
|
134
|
+
</summary>
|
|
135
|
+
<pre className="px-3 pb-2 text-2xs text-po-text-tertiary font-mono whitespace-pre-wrap">
|
|
136
|
+
{JSON.stringify(config, null, 2)}
|
|
137
|
+
</pre>
|
|
138
|
+
</details>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{/* Prompt content */}
|
|
142
|
+
{isEditing ? (
|
|
143
|
+
<textarea
|
|
144
|
+
value={editedPrompt}
|
|
145
|
+
onChange={handlePromptChange}
|
|
146
|
+
className="flex-1 w-full p-3 bg-transparent text-po-text-primary font-mono text-xs resize-none focus:outline-none"
|
|
147
|
+
placeholder="Enter prompt markdown..."
|
|
148
|
+
spellCheck={false}
|
|
149
|
+
/>
|
|
150
|
+
) : (
|
|
151
|
+
<pre className="flex-1 p-3 text-xs text-po-text-secondary font-mono whitespace-pre-wrap overflow-auto">
|
|
152
|
+
{prompt || '(empty)'}
|
|
153
|
+
</pre>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ParametersView({ parameters }: { parameters: Record<string, unknown> }) {
|
|
160
|
+
const properties = (parameters.properties as Record<string, unknown>) || {}
|
|
161
|
+
const required = (parameters.required as string[]) || []
|
|
162
|
+
|
|
163
|
+
const propertyNames = Object.keys(properties)
|
|
164
|
+
if (propertyNames.length === 0) return null
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div>
|
|
168
|
+
<div className="text-2xs text-po-text-ghost uppercase tracking-wider mb-1.5">Parameters</div>
|
|
169
|
+
<div className="space-y-1.5">
|
|
170
|
+
{propertyNames.map((propName) => {
|
|
171
|
+
const prop = properties[propName] as Record<string, unknown>
|
|
172
|
+
const isRequired = required.includes(propName)
|
|
173
|
+
const propType = prop.type ? String(prop.type) : null
|
|
174
|
+
const propDescription = prop.description ? String(prop.description) : null
|
|
175
|
+
const propEnum = prop.enum as string[] | undefined
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div key={propName} className="bg-po-surface rounded p-2">
|
|
179
|
+
<div className="flex items-center gap-1.5">
|
|
180
|
+
<span className="font-mono text-2xs text-po-accent">{propName}</span>
|
|
181
|
+
{propType && <span className="text-2xs text-po-text-ghost">({propType})</span>}
|
|
182
|
+
{isRequired && <span className="text-2xs text-po-error">req</span>}
|
|
183
|
+
</div>
|
|
184
|
+
{propDescription && (
|
|
185
|
+
<p className="text-2xs text-po-text-tertiary mt-0.5">{propDescription}</p>
|
|
186
|
+
)}
|
|
187
|
+
{propEnum && propEnum.length > 0 && (
|
|
188
|
+
<div className="mt-1 flex flex-wrap gap-1">
|
|
189
|
+
{propEnum.map((val) => (
|
|
190
|
+
<span key={val} className="text-2xs bg-po-surface-2 px-1 py-0.5 rounded text-po-text-ghost">
|
|
191
|
+
{val}
|
|
192
|
+
</span>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
})}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useStore, useNotificationCount } from '../store'
|
|
2
|
+
import { ModelSelector } from './ModelSelector'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
switchLLM: (provider: string, model?: string) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function SystemBar({ switchLLM }: Props) {
|
|
9
|
+
const { connected, environment, toggleBus, busOpen, currentView, setCurrentView } =
|
|
10
|
+
useStore()
|
|
11
|
+
const notificationCount = useNotificationCount()
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<header className="h-8 bg-po-surface-2 border-b border-po-border flex items-center px-3 gap-3 flex-shrink-0">
|
|
15
|
+
{/* Logo / Environment */}
|
|
16
|
+
<div className="flex items-center gap-1.5 text-xs">
|
|
17
|
+
<span className="font-mono text-po-text-primary font-medium">PromptObjects</span>
|
|
18
|
+
{environment && (
|
|
19
|
+
<>
|
|
20
|
+
<span className="text-po-text-ghost">/</span>
|
|
21
|
+
<span className="text-po-text-secondary">{environment.name}</span>
|
|
22
|
+
</>
|
|
23
|
+
)}
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div className="flex-1" />
|
|
27
|
+
|
|
28
|
+
{/* Connection dot */}
|
|
29
|
+
<div
|
|
30
|
+
className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
|
|
31
|
+
connected ? 'bg-po-success' : 'bg-po-error animate-pulse'
|
|
32
|
+
}`}
|
|
33
|
+
title={connected ? 'Connected' : 'Disconnected'}
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
{/* Model selector */}
|
|
37
|
+
<ModelSelector switchLLM={switchLLM} />
|
|
38
|
+
|
|
39
|
+
{/* Notification count */}
|
|
40
|
+
{notificationCount > 0 && (
|
|
41
|
+
<span
|
|
42
|
+
className="text-2xs font-mono bg-po-warning text-po-bg px-1.5 py-0.5 rounded font-bold"
|
|
43
|
+
title={`${notificationCount} pending requests`}
|
|
44
|
+
>
|
|
45
|
+
{notificationCount}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
{/* Canvas toggle */}
|
|
50
|
+
<button
|
|
51
|
+
onClick={() => setCurrentView(currentView === 'canvas' ? 'dashboard' : 'canvas')}
|
|
52
|
+
className={`text-xs px-2 py-0.5 rounded transition-colors duration-150 ${
|
|
53
|
+
currentView === 'canvas'
|
|
54
|
+
? 'bg-po-accent text-po-bg font-medium'
|
|
55
|
+
: 'text-po-text-secondary hover:text-po-text-primary hover:bg-po-surface-3'
|
|
56
|
+
}`}
|
|
57
|
+
>
|
|
58
|
+
Canvas
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
{/* Transcript toggle */}
|
|
62
|
+
<button
|
|
63
|
+
onClick={toggleBus}
|
|
64
|
+
className={`text-xs px-2 py-0.5 rounded transition-colors duration-150 ${
|
|
65
|
+
busOpen
|
|
66
|
+
? 'bg-po-accent text-po-bg font-medium'
|
|
67
|
+
: 'text-po-text-secondary hover:text-po-text-primary hover:bg-po-surface-3'
|
|
68
|
+
}`}
|
|
69
|
+
>
|
|
70
|
+
Transcript
|
|
71
|
+
</button>
|
|
72
|
+
</header>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useRef, useEffect, useState } from 'react'
|
|
2
|
+
import { useStore } from '../store'
|
|
3
|
+
|
|
4
|
+
export function Transcript() {
|
|
5
|
+
const { busMessages, toggleBus } = useStore()
|
|
6
|
+
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
10
|
+
}, [busMessages])
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="h-full flex flex-col border-t border-po-border">
|
|
14
|
+
{/* Header */}
|
|
15
|
+
<div className="h-6 bg-po-surface-2 border-b border-po-border flex items-center px-3 flex-shrink-0">
|
|
16
|
+
<span className="text-2xs font-medium text-po-text-ghost uppercase tracking-wider flex-1">
|
|
17
|
+
Transcript
|
|
18
|
+
</span>
|
|
19
|
+
<button
|
|
20
|
+
onClick={toggleBus}
|
|
21
|
+
className="text-2xs text-po-text-ghost hover:text-po-text-secondary transition-colors duration-150"
|
|
22
|
+
>
|
|
23
|
+
{'\u2715'}
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{/* Messages */}
|
|
28
|
+
<div className="flex-1 overflow-auto px-3 py-1 font-mono">
|
|
29
|
+
{busMessages.length === 0 ? (
|
|
30
|
+
<div className="text-2xs text-po-text-ghost text-center py-2">
|
|
31
|
+
No messages
|
|
32
|
+
</div>
|
|
33
|
+
) : (
|
|
34
|
+
busMessages.map((msg, index) => (
|
|
35
|
+
<TranscriptRow key={index} msg={msg} />
|
|
36
|
+
))
|
|
37
|
+
)}
|
|
38
|
+
<div ref={messagesEndRef} />
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function TranscriptRow({ msg }: { msg: { from: string; to: string; content: string | Record<string, unknown>; summary?: string; timestamp: string } }) {
|
|
45
|
+
const [expanded, setExpanded] = useState(false)
|
|
46
|
+
const fullText = msg.summary || (typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content, null, 2))
|
|
47
|
+
const isLong = fullText.length > 120
|
|
48
|
+
const truncated = isLong ? fullText.slice(0, 120) + '...' : fullText
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div>
|
|
52
|
+
<div
|
|
53
|
+
onClick={() => isLong && setExpanded(!expanded)}
|
|
54
|
+
className={`text-2xs leading-relaxed flex items-baseline gap-1.5 ${isLong ? 'cursor-pointer hover:bg-po-surface-2' : ''} ${expanded ? 'bg-po-surface-2' : ''}`}
|
|
55
|
+
>
|
|
56
|
+
<span className="text-po-text-ghost flex-shrink-0 whitespace-nowrap">
|
|
57
|
+
{new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false })}
|
|
58
|
+
</span>
|
|
59
|
+
<span className="text-po-accent flex-shrink-0">{msg.from}</span>
|
|
60
|
+
<span className="text-po-text-ghost">{'\u2192'}</span>
|
|
61
|
+
<span className="text-po-status-calling flex-shrink-0">{msg.to}</span>
|
|
62
|
+
{!expanded && (
|
|
63
|
+
<span className="text-po-text-secondary truncate">{truncated}</span>
|
|
64
|
+
)}
|
|
65
|
+
{isLong && (
|
|
66
|
+
<span className="text-po-text-ghost flex-shrink-0 ml-auto">{expanded ? '\u25BC' : '\u25B8'}</span>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
{expanded && (
|
|
70
|
+
<pre className="text-2xs text-po-text-secondary whitespace-pre-wrap break-all pl-[4.5rem] pb-1.5 bg-po-surface-2 rounded-b mb-0.5">
|
|
71
|
+
{fullText}
|
|
72
|
+
</pre>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|