prompt_objects 0.1.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 +7 -0
- data/CLAUDE.md +108 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +231 -0
- data/IMPLEMENTATION_PLAN.md +1073 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/Rakefile +27 -0
- data/design-doc-v2.md +1232 -0
- data/exe/prompt_objects +572 -0
- data/exe/prompt_objects_mcp +34 -0
- data/frontend/.gitignore +3 -0
- data/frontend/index.html +13 -0
- data/frontend/package-lock.json +4417 -0
- data/frontend/package.json +32 -0
- data/frontend/postcss.config.js +6 -0
- data/frontend/src/App.tsx +95 -0
- data/frontend/src/components/CapabilitiesPanel.tsx +44 -0
- data/frontend/src/components/ChatPanel.tsx +251 -0
- data/frontend/src/components/Dashboard.tsx +83 -0
- data/frontend/src/components/Header.tsx +141 -0
- data/frontend/src/components/MarkdownMessage.tsx +153 -0
- data/frontend/src/components/MessageBus.tsx +55 -0
- data/frontend/src/components/ModelSelector.tsx +112 -0
- data/frontend/src/components/NotificationPanel.tsx +134 -0
- data/frontend/src/components/POCard.tsx +56 -0
- data/frontend/src/components/PODetail.tsx +117 -0
- data/frontend/src/components/PromptPanel.tsx +51 -0
- data/frontend/src/components/SessionsPanel.tsx +174 -0
- data/frontend/src/components/ThreadsSidebar.tsx +119 -0
- data/frontend/src/components/index.ts +11 -0
- data/frontend/src/hooks/useWebSocket.ts +363 -0
- data/frontend/src/index.css +37 -0
- data/frontend/src/main.tsx +10 -0
- data/frontend/src/store/index.ts +246 -0
- data/frontend/src/types/index.ts +146 -0
- data/frontend/tailwind.config.js +25 -0
- data/frontend/tsconfig.json +30 -0
- data/frontend/vite.config.ts +29 -0
- data/lib/prompt_objects/capability.rb +46 -0
- data/lib/prompt_objects/cli.rb +431 -0
- data/lib/prompt_objects/connectors/base.rb +73 -0
- data/lib/prompt_objects/connectors/mcp.rb +524 -0
- data/lib/prompt_objects/environment/exporter.rb +83 -0
- data/lib/prompt_objects/environment/git.rb +118 -0
- data/lib/prompt_objects/environment/importer.rb +159 -0
- data/lib/prompt_objects/environment/manager.rb +401 -0
- data/lib/prompt_objects/environment/manifest.rb +218 -0
- data/lib/prompt_objects/environment.rb +283 -0
- data/lib/prompt_objects/human_queue.rb +144 -0
- data/lib/prompt_objects/llm/anthropic_adapter.rb +137 -0
- data/lib/prompt_objects/llm/factory.rb +84 -0
- data/lib/prompt_objects/llm/gemini_adapter.rb +209 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +104 -0
- data/lib/prompt_objects/llm/response.rb +61 -0
- data/lib/prompt_objects/loader.rb +32 -0
- data/lib/prompt_objects/mcp/server.rb +167 -0
- data/lib/prompt_objects/mcp/tools/get_conversation.rb +60 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +54 -0
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +73 -0
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +37 -0
- data/lib/prompt_objects/mcp/tools/respond_to_request.rb +68 -0
- data/lib/prompt_objects/mcp/tools/send_message.rb +71 -0
- data/lib/prompt_objects/message_bus.rb +97 -0
- data/lib/prompt_objects/primitive.rb +13 -0
- data/lib/prompt_objects/primitives/http_get.rb +72 -0
- data/lib/prompt_objects/primitives/list_files.rb +95 -0
- data/lib/prompt_objects/primitives/read_file.rb +81 -0
- data/lib/prompt_objects/primitives/write_file.rb +73 -0
- data/lib/prompt_objects/prompt_object.rb +415 -0
- data/lib/prompt_objects/registry.rb +88 -0
- data/lib/prompt_objects/server/api/routes.rb +297 -0
- data/lib/prompt_objects/server/app.rb +174 -0
- data/lib/prompt_objects/server/file_watcher.rb +113 -0
- data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +77 -0
- data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +1 -0
- data/lib/prompt_objects/server/public/index.html +14 -0
- data/lib/prompt_objects/server/websocket_handler.rb +619 -0
- data/lib/prompt_objects/server.rb +166 -0
- data/lib/prompt_objects/session/store.rb +826 -0
- data/lib/prompt_objects/universal/add_capability.rb +74 -0
- data/lib/prompt_objects/universal/add_primitive.rb +113 -0
- data/lib/prompt_objects/universal/ask_human.rb +109 -0
- data/lib/prompt_objects/universal/create_capability.rb +219 -0
- data/lib/prompt_objects/universal/create_primitive.rb +170 -0
- data/lib/prompt_objects/universal/list_capabilities.rb +55 -0
- data/lib/prompt_objects/universal/list_primitives.rb +145 -0
- data/lib/prompt_objects/universal/modify_primitive.rb +180 -0
- data/lib/prompt_objects/universal/request_primitive.rb +287 -0
- data/lib/prompt_objects/universal/think.rb +41 -0
- data/lib/prompt_objects/universal/verify_primitive.rb +173 -0
- data/lib/prompt_objects.rb +62 -0
- data/objects/coordinator.md +48 -0
- data/objects/greeter.md +30 -0
- data/objects/reader.md +33 -0
- data/prompt_objects.gemspec +50 -0
- data/templates/basic/.gitignore +2 -0
- data/templates/basic/manifest.yml +7 -0
- data/templates/basic/objects/basic.md +32 -0
- data/templates/developer/.gitignore +5 -0
- data/templates/developer/manifest.yml +17 -0
- data/templates/developer/objects/code_reviewer.md +33 -0
- data/templates/developer/objects/coordinator.md +39 -0
- data/templates/developer/objects/debugger.md +35 -0
- data/templates/empty/.gitignore +5 -0
- data/templates/empty/manifest.yml +14 -0
- data/templates/empty/objects/.gitkeep +0 -0
- data/templates/empty/objects/assistant.md +41 -0
- data/templates/minimal/.gitignore +5 -0
- data/templates/minimal/manifest.yml +7 -0
- data/templates/minimal/objects/assistant.md +41 -0
- data/templates/writer/.gitignore +5 -0
- data/templates/writer/manifest.yml +17 -0
- data/templates/writer/objects/coordinator.md +33 -0
- data/templates/writer/objects/editor.md +33 -0
- data/templates/writer/objects/researcher.md +34 -0
- metadata +343 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prompt-objects-frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@types/react-syntax-highlighter": "^15.5.13",
|
|
14
|
+
"clsx": "^2.1.1",
|
|
15
|
+
"react": "^18.3.1",
|
|
16
|
+
"react-dom": "^18.3.1",
|
|
17
|
+
"react-markdown": "^10.1.0",
|
|
18
|
+
"react-syntax-highlighter": "^16.1.0",
|
|
19
|
+
"remark-gfm": "^4.0.1",
|
|
20
|
+
"zustand": "^5.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/react": "^18.3.12",
|
|
24
|
+
"@types/react-dom": "^18.3.1",
|
|
25
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
26
|
+
"autoprefixer": "^10.4.20",
|
|
27
|
+
"postcss": "^8.4.49",
|
|
28
|
+
"tailwindcss": "^3.4.16",
|
|
29
|
+
"typescript": "^5.6.3",
|
|
30
|
+
"vite": "^6.0.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useWebSocket } from './hooks/useWebSocket'
|
|
3
|
+
import { useStore, useSelectedPO } from './store'
|
|
4
|
+
import { Header } from './components/Header'
|
|
5
|
+
import { Dashboard } from './components/Dashboard'
|
|
6
|
+
import { PODetail } from './components/PODetail'
|
|
7
|
+
import { MessageBus } from './components/MessageBus'
|
|
8
|
+
import { NotificationPanel } from './components/NotificationPanel'
|
|
9
|
+
import { ThreadsSidebar } from './components/ThreadsSidebar'
|
|
10
|
+
|
|
11
|
+
export default function App() {
|
|
12
|
+
const { sendMessage, respondToNotification, createSession, switchSession, switchLLM, createThread } =
|
|
13
|
+
useWebSocket()
|
|
14
|
+
const { selectedPO, busOpen, notifications } = useStore()
|
|
15
|
+
const selectedPOData = useSelectedPO()
|
|
16
|
+
const [splitView, setSplitView] = useState(true) // Default to split view
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="h-screen flex flex-col bg-po-bg">
|
|
20
|
+
<Header switchLLM={switchLLM} />
|
|
21
|
+
|
|
22
|
+
<div className="flex-1 flex overflow-hidden">
|
|
23
|
+
{/* Split view: Dashboard sidebar on left when PO selected */}
|
|
24
|
+
{splitView && selectedPO && (
|
|
25
|
+
<>
|
|
26
|
+
{/* PO List */}
|
|
27
|
+
<aside className="w-56 border-r border-po-border bg-po-surface overflow-hidden flex flex-col">
|
|
28
|
+
<div className="p-3 border-b border-po-border flex items-center justify-between">
|
|
29
|
+
<h2 className="text-sm font-medium text-gray-400">Prompt Objects</h2>
|
|
30
|
+
<button
|
|
31
|
+
onClick={() => setSplitView(false)}
|
|
32
|
+
className="text-xs text-gray-500 hover:text-white"
|
|
33
|
+
title="Hide sidebar"
|
|
34
|
+
>
|
|
35
|
+
✕
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="flex-1 overflow-auto">
|
|
39
|
+
<Dashboard compact />
|
|
40
|
+
</div>
|
|
41
|
+
</aside>
|
|
42
|
+
|
|
43
|
+
{/* Threads List for selected PO */}
|
|
44
|
+
{selectedPOData && (
|
|
45
|
+
<aside className="w-56 border-r border-po-border bg-po-bg overflow-hidden">
|
|
46
|
+
<ThreadsSidebar
|
|
47
|
+
po={selectedPOData}
|
|
48
|
+
switchSession={switchSession}
|
|
49
|
+
createThread={createThread}
|
|
50
|
+
/>
|
|
51
|
+
</aside>
|
|
52
|
+
)}
|
|
53
|
+
</>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{/* Main content */}
|
|
57
|
+
<main className="flex-1 overflow-hidden flex flex-col">
|
|
58
|
+
{/* Show expand button when sidebar is hidden */}
|
|
59
|
+
{!splitView && selectedPO && (
|
|
60
|
+
<button
|
|
61
|
+
onClick={() => setSplitView(true)}
|
|
62
|
+
className="absolute left-2 top-16 z-10 bg-po-surface border border-po-border rounded px-2 py-1 text-xs text-gray-400 hover:text-white hover:border-po-accent transition-colors"
|
|
63
|
+
title="Show dashboard sidebar"
|
|
64
|
+
>
|
|
65
|
+
☰ POs
|
|
66
|
+
</button>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{selectedPO ? (
|
|
70
|
+
<PODetail
|
|
71
|
+
sendMessage={sendMessage}
|
|
72
|
+
createSession={createSession}
|
|
73
|
+
switchSession={switchSession}
|
|
74
|
+
createThread={createThread}
|
|
75
|
+
/>
|
|
76
|
+
) : (
|
|
77
|
+
<Dashboard />
|
|
78
|
+
)}
|
|
79
|
+
</main>
|
|
80
|
+
|
|
81
|
+
{/* Message Bus sidebar */}
|
|
82
|
+
{busOpen && (
|
|
83
|
+
<aside className="w-80 border-l border-po-border bg-po-surface overflow-hidden">
|
|
84
|
+
<MessageBus />
|
|
85
|
+
</aside>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Notification panel */}
|
|
90
|
+
{notifications.length > 0 && (
|
|
91
|
+
<NotificationPanel respondToNotification={respondToNotification} />
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PromptObject } from '../types'
|
|
2
|
+
|
|
3
|
+
interface CapabilitiesPanelProps {
|
|
4
|
+
po: PromptObject
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function CapabilitiesPanel({ po }: CapabilitiesPanelProps) {
|
|
8
|
+
const capabilities = po.capabilities || []
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="h-full overflow-auto p-4">
|
|
12
|
+
<h3 className="text-lg font-medium text-white mb-4">Capabilities</h3>
|
|
13
|
+
|
|
14
|
+
{capabilities.length === 0 ? (
|
|
15
|
+
<div className="text-gray-500 text-center py-8">
|
|
16
|
+
No capabilities defined.
|
|
17
|
+
</div>
|
|
18
|
+
) : (
|
|
19
|
+
<div className="space-y-2">
|
|
20
|
+
{capabilities.map((cap, index) => (
|
|
21
|
+
<div
|
|
22
|
+
key={index}
|
|
23
|
+
className="bg-po-surface border border-po-border rounded-lg p-3"
|
|
24
|
+
>
|
|
25
|
+
<div className="font-mono text-sm text-po-accent">{cap}</div>
|
|
26
|
+
</div>
|
|
27
|
+
))}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
|
|
31
|
+
<div className="mt-6 p-4 bg-po-bg rounded-lg border border-po-border">
|
|
32
|
+
<h4 className="text-sm font-medium text-gray-400 mb-2">
|
|
33
|
+
Universal Capabilities
|
|
34
|
+
</h4>
|
|
35
|
+
<p className="text-xs text-gray-500">
|
|
36
|
+
All Prompt Objects automatically have access to universal capabilities
|
|
37
|
+
like <code className="text-po-accent">ask_human</code>,{' '}
|
|
38
|
+
<code className="text-po-accent">think</code>, and{' '}
|
|
39
|
+
<code className="text-po-accent">request_capability</code>.
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import { useStore } from '../store'
|
|
3
|
+
import { MarkdownMessage } from './MarkdownMessage'
|
|
4
|
+
import type { PromptObject, Message, ToolCall } from '../types'
|
|
5
|
+
|
|
6
|
+
interface ChatPanelProps {
|
|
7
|
+
po: PromptObject
|
|
8
|
+
sendMessage: (target: string, content: string, newThread?: boolean) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ChatPanel({ po, sendMessage }: ChatPanelProps) {
|
|
12
|
+
const [input, setInput] = useState('')
|
|
13
|
+
const [continueThread, setContinueThread] = useState(false)
|
|
14
|
+
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
15
|
+
const { streamingContent } = useStore()
|
|
16
|
+
|
|
17
|
+
const messages = po.current_session?.messages || []
|
|
18
|
+
const streaming = streamingContent[po.name]
|
|
19
|
+
const hasMessages = messages.length > 0
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
23
|
+
}, [messages, streaming])
|
|
24
|
+
|
|
25
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
26
|
+
e.preventDefault()
|
|
27
|
+
if (!input.trim()) return
|
|
28
|
+
|
|
29
|
+
const content = input.trim()
|
|
30
|
+
|
|
31
|
+
// Determine if we should create a new thread
|
|
32
|
+
const shouldCreateNewThread = !continueThread && hasMessages
|
|
33
|
+
|
|
34
|
+
// Send message to server - it will handle thread creation + message in one operation
|
|
35
|
+
// Server sends immediate session_updated with user message for instant feedback
|
|
36
|
+
sendMessage(po.name, content, shouldCreateNewThread)
|
|
37
|
+
setInput('')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="h-full flex flex-col">
|
|
42
|
+
{/* Messages */}
|
|
43
|
+
<div className="flex-1 overflow-auto p-4 space-y-4">
|
|
44
|
+
{messages.length === 0 && !streaming && (
|
|
45
|
+
<div className="h-full flex items-center justify-center text-gray-500">
|
|
46
|
+
<div className="text-center">
|
|
47
|
+
<div className="text-2xl mb-2">💬</div>
|
|
48
|
+
<div>Start a conversation with {po.name}</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
{messages.map((message, index) => (
|
|
54
|
+
<MessageBubble key={index} message={message} />
|
|
55
|
+
))}
|
|
56
|
+
|
|
57
|
+
{/* Streaming content */}
|
|
58
|
+
{streaming && (
|
|
59
|
+
<div className="flex gap-3">
|
|
60
|
+
<div className="w-8 h-8 rounded-full bg-po-accent flex items-center justify-center text-white text-sm font-medium flex-shrink-0">
|
|
61
|
+
AI
|
|
62
|
+
</div>
|
|
63
|
+
<div className="flex-1 bg-po-surface rounded-lg p-3 text-gray-200">
|
|
64
|
+
<MarkdownMessage content={streaming} />
|
|
65
|
+
<span className="inline-block w-2 h-4 bg-po-accent animate-pulse ml-1" />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
<div ref={messagesEndRef} />
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Input */}
|
|
74
|
+
<form onSubmit={handleSubmit} className="border-t border-po-border p-4">
|
|
75
|
+
<div className="flex gap-3">
|
|
76
|
+
<input
|
|
77
|
+
type="text"
|
|
78
|
+
value={input}
|
|
79
|
+
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={po.status !== 'idle'}
|
|
83
|
+
/>
|
|
84
|
+
<button
|
|
85
|
+
type="submit"
|
|
86
|
+
disabled={!input.trim() || po.status !== 'idle'}
|
|
87
|
+
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
|
+
>
|
|
89
|
+
Send
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Thread toggle - only show when there are existing messages */}
|
|
94
|
+
{hasMessages && (
|
|
95
|
+
<div className="flex items-center gap-3 mt-2 text-xs">
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={() => setContinueThread(false)}
|
|
99
|
+
className={`px-2 py-1 rounded transition-colors ${
|
|
100
|
+
!continueThread
|
|
101
|
+
? 'bg-po-accent/20 text-po-accent border border-po-accent'
|
|
102
|
+
: 'text-gray-400 hover:text-white'
|
|
103
|
+
}`}
|
|
104
|
+
>
|
|
105
|
+
New thread
|
|
106
|
+
</button>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => setContinueThread(true)}
|
|
110
|
+
className={`px-2 py-1 rounded transition-colors ${
|
|
111
|
+
continueThread
|
|
112
|
+
? 'bg-po-accent/20 text-po-accent border border-po-accent'
|
|
113
|
+
: 'text-gray-400 hover:text-white'
|
|
114
|
+
}`}
|
|
115
|
+
>
|
|
116
|
+
Continue thread
|
|
117
|
+
</button>
|
|
118
|
+
<span className="text-gray-500">
|
|
119
|
+
{continueThread
|
|
120
|
+
? 'Will add to current conversation'
|
|
121
|
+
: 'Will start a fresh conversation'}
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</form>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function MessageBubble({ message }: { message: Message }) {
|
|
131
|
+
const isUser = message.role === 'user'
|
|
132
|
+
const isAssistant = message.role === 'assistant'
|
|
133
|
+
const isTool = message.role === 'tool'
|
|
134
|
+
|
|
135
|
+
if (isTool) {
|
|
136
|
+
return (
|
|
137
|
+
<div className="text-xs text-gray-500 bg-po-bg rounded p-2 font-mono">
|
|
138
|
+
<div className="text-gray-400 mb-1">Tool Result:</div>
|
|
139
|
+
<div className="whitespace-pre-wrap break-all">
|
|
140
|
+
{message.content?.slice(0, 500)}
|
|
141
|
+
{message.content && message.content.length > 500 && '...'}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : ''}`}>
|
|
149
|
+
<div
|
|
150
|
+
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium flex-shrink-0 ${
|
|
151
|
+
isUser ? 'bg-po-border text-white' : 'bg-po-accent text-white'
|
|
152
|
+
}`}
|
|
153
|
+
>
|
|
154
|
+
{isUser ? 'You' : 'AI'}
|
|
155
|
+
</div>
|
|
156
|
+
<div
|
|
157
|
+
className={`flex-1 max-w-[80%] rounded-lg p-3 ${
|
|
158
|
+
isUser
|
|
159
|
+
? 'bg-po-accent text-white'
|
|
160
|
+
: 'bg-po-surface text-gray-200'
|
|
161
|
+
}`}
|
|
162
|
+
>
|
|
163
|
+
{message.content && (
|
|
164
|
+
isAssistant ? (
|
|
165
|
+
<MarkdownMessage content={message.content} />
|
|
166
|
+
) : (
|
|
167
|
+
<div className="whitespace-pre-wrap">{message.content}</div>
|
|
168
|
+
)
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* Tool calls */}
|
|
172
|
+
{isAssistant && message.tool_calls && message.tool_calls.length > 0 && (
|
|
173
|
+
<div className="mt-2 space-y-2">
|
|
174
|
+
{message.tool_calls.map((tc) => (
|
|
175
|
+
<ToolCallDisplay key={tc.id} toolCall={tc} />
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function ToolCallDisplay({ toolCall }: { toolCall: ToolCall }) {
|
|
185
|
+
const [expanded, setExpanded] = useState(false)
|
|
186
|
+
const { notifications } = useStore()
|
|
187
|
+
|
|
188
|
+
// Special display for ask_human
|
|
189
|
+
if (toolCall.name === 'ask_human') {
|
|
190
|
+
const question = toolCall.arguments.question as string
|
|
191
|
+
const options = toolCall.arguments.options as string[] | undefined
|
|
192
|
+
|
|
193
|
+
// Check if there's a pending notification for this (by matching question)
|
|
194
|
+
const isPending = notifications.some((n) => n.message === question)
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div className="bg-po-warning/10 border border-po-warning/30 rounded-lg p-3">
|
|
198
|
+
<div className="flex items-center gap-2 mb-2">
|
|
199
|
+
<span className="text-po-warning text-sm font-medium">
|
|
200
|
+
Waiting for human input
|
|
201
|
+
</span>
|
|
202
|
+
{isPending ? (
|
|
203
|
+
<span className="text-xs bg-po-warning text-black px-2 py-0.5 rounded animate-pulse">
|
|
204
|
+
PENDING
|
|
205
|
+
</span>
|
|
206
|
+
) : (
|
|
207
|
+
<span className="text-xs bg-green-600 text-white px-2 py-0.5 rounded">
|
|
208
|
+
RESOLVED
|
|
209
|
+
</span>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
<p className="text-gray-200 text-sm mb-2">{question}</p>
|
|
213
|
+
{options && options.length > 0 && (
|
|
214
|
+
<div className="flex flex-wrap gap-2">
|
|
215
|
+
{options.map((opt, i) => (
|
|
216
|
+
<span
|
|
217
|
+
key={i}
|
|
218
|
+
className="text-xs bg-po-bg px-2 py-1 rounded text-gray-400"
|
|
219
|
+
>
|
|
220
|
+
{opt}
|
|
221
|
+
</span>
|
|
222
|
+
))}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Default display for other tool calls - expandable
|
|
230
|
+
return (
|
|
231
|
+
<div className="text-xs bg-po-bg/50 rounded overflow-hidden">
|
|
232
|
+
<button
|
|
233
|
+
onClick={() => setExpanded(!expanded)}
|
|
234
|
+
className="w-full px-2 py-1 text-left font-mono flex items-center gap-1 hover:bg-po-bg/70 transition-colors"
|
|
235
|
+
>
|
|
236
|
+
<span className="text-gray-500">{expanded ? '▼' : '▶'}</span>
|
|
237
|
+
<span className="text-po-accent">{toolCall.name}</span>
|
|
238
|
+
<span className="text-gray-500">
|
|
239
|
+
({Object.keys(toolCall.arguments).length} args)
|
|
240
|
+
</span>
|
|
241
|
+
</button>
|
|
242
|
+
{expanded && (
|
|
243
|
+
<div className="px-2 pb-2 border-t border-po-border/50">
|
|
244
|
+
<pre className="text-gray-400 whitespace-pre-wrap break-all mt-1 text-[10px] leading-relaxed">
|
|
245
|
+
{JSON.stringify(toolCall.arguments, null, 2)}
|
|
246
|
+
</pre>
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { usePromptObjects, useStore, usePONotifications } from '../store'
|
|
2
|
+
import { POCard } from './POCard'
|
|
3
|
+
import type { PromptObject } from '../types'
|
|
4
|
+
|
|
5
|
+
interface DashboardProps {
|
|
6
|
+
compact?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Dashboard({ compact = false }: DashboardProps) {
|
|
10
|
+
const promptObjects = usePromptObjects()
|
|
11
|
+
|
|
12
|
+
if (promptObjects.length === 0) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="h-full flex items-center justify-center text-gray-500">
|
|
15
|
+
<div className="text-center">
|
|
16
|
+
<div className="text-4xl mb-4">🔮</div>
|
|
17
|
+
<div className="text-lg">No Prompt Objects loaded</div>
|
|
18
|
+
<div className="text-sm mt-2">
|
|
19
|
+
Waiting for environment to connect...
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Compact mode: simple list for sidebar
|
|
27
|
+
if (compact) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="p-2 space-y-1">
|
|
30
|
+
{promptObjects.map((po) => (
|
|
31
|
+
<CompactPOItem key={po.name} po={po} />
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Full dashboard view
|
|
38
|
+
return (
|
|
39
|
+
<div className="h-full overflow-auto p-6">
|
|
40
|
+
<h1 className="text-2xl font-semibold text-white mb-6">
|
|
41
|
+
Prompt Objects
|
|
42
|
+
</h1>
|
|
43
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
44
|
+
{promptObjects.map((po) => (
|
|
45
|
+
<POCard key={po.name} po={po} />
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function CompactPOItem({ po }: { po: PromptObject }) {
|
|
53
|
+
const { selectPO, selectedPO } = useStore()
|
|
54
|
+
const notifications = usePONotifications(po.name)
|
|
55
|
+
const isSelected = selectedPO === po.name
|
|
56
|
+
|
|
57
|
+
const statusColors = {
|
|
58
|
+
idle: 'bg-gray-500',
|
|
59
|
+
thinking: 'bg-po-accent animate-pulse',
|
|
60
|
+
calling_tool: 'bg-po-warning animate-pulse',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => selectPO(po.name)}
|
|
66
|
+
className={`w-full text-left px-3 py-2 rounded transition-colors flex items-center gap-2 ${
|
|
67
|
+
isSelected
|
|
68
|
+
? 'bg-po-accent/20 border border-po-accent'
|
|
69
|
+
: 'hover:bg-po-bg border border-transparent'
|
|
70
|
+
}`}
|
|
71
|
+
>
|
|
72
|
+
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${statusColors[po.status]}`} />
|
|
73
|
+
<span className={`flex-1 truncate text-sm ${isSelected ? 'text-white' : 'text-gray-300'}`}>
|
|
74
|
+
{po.name}
|
|
75
|
+
</span>
|
|
76
|
+
{notifications.length > 0 && (
|
|
77
|
+
<span className="bg-po-warning text-black text-xs font-bold px-1.5 py-0.5 rounded-full">
|
|
78
|
+
{notifications.length}
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
81
|
+
</button>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { useStore, useNotificationCount } from '../store'
|
|
3
|
+
import { ModelSelector } from './ModelSelector'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
switchLLM: (provider: string, model?: string) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Header({ switchLLM }: Props) {
|
|
10
|
+
const { connected, environment, selectedPO, selectPO, toggleBus, busOpen, notifications } =
|
|
11
|
+
useStore()
|
|
12
|
+
const notificationCount = useNotificationCount()
|
|
13
|
+
const [showNotifications, setShowNotifications] = useState(false)
|
|
14
|
+
const [animate, setAnimate] = useState(false)
|
|
15
|
+
const prevCount = useRef(notificationCount)
|
|
16
|
+
|
|
17
|
+
// Animate badge when count increases
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (notificationCount > prevCount.current) {
|
|
20
|
+
setAnimate(true)
|
|
21
|
+
const timer = setTimeout(() => setAnimate(false), 500)
|
|
22
|
+
return () => clearTimeout(timer)
|
|
23
|
+
}
|
|
24
|
+
prevCount.current = notificationCount
|
|
25
|
+
}, [notificationCount])
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<header className="h-14 bg-po-surface border-b border-po-border flex items-center px-4 gap-4">
|
|
29
|
+
{/* Logo / Title */}
|
|
30
|
+
<button
|
|
31
|
+
onClick={() => selectPO(null)}
|
|
32
|
+
className="text-lg font-semibold text-white hover:text-po-accent transition-colors"
|
|
33
|
+
>
|
|
34
|
+
PromptObjects
|
|
35
|
+
</button>
|
|
36
|
+
|
|
37
|
+
{/* Environment info */}
|
|
38
|
+
{environment && (
|
|
39
|
+
<div className="text-sm text-gray-400">
|
|
40
|
+
<span className="text-gray-500">/</span>
|
|
41
|
+
<span className="ml-2">{environment.name}</span>
|
|
42
|
+
<span className="ml-3 text-gray-500">
|
|
43
|
+
{environment.po_count} POs, {environment.primitive_count} primitives
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{/* Breadcrumb for selected PO */}
|
|
49
|
+
{selectedPO && (
|
|
50
|
+
<div className="text-sm text-gray-400">
|
|
51
|
+
<span className="text-gray-500">/</span>
|
|
52
|
+
<span className="ml-2 text-po-accent">{selectedPO}</span>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
<div className="flex-1" />
|
|
57
|
+
|
|
58
|
+
{/* Model selector */}
|
|
59
|
+
<ModelSelector switchLLM={switchLLM} />
|
|
60
|
+
|
|
61
|
+
{/* Connection status */}
|
|
62
|
+
<div className="flex items-center gap-2 text-sm">
|
|
63
|
+
<div
|
|
64
|
+
className={`w-2 h-2 rounded-full ${
|
|
65
|
+
connected ? 'bg-green-500' : 'bg-red-500'
|
|
66
|
+
}`}
|
|
67
|
+
/>
|
|
68
|
+
<span className="text-gray-400">
|
|
69
|
+
{connected ? 'Connected' : 'Disconnected'}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Notification bell with badge */}
|
|
74
|
+
<div className="relative">
|
|
75
|
+
<button
|
|
76
|
+
onClick={() => setShowNotifications(!showNotifications)}
|
|
77
|
+
className={`relative p-2 rounded transition-colors ${
|
|
78
|
+
notificationCount > 0
|
|
79
|
+
? 'text-po-warning hover:bg-po-warning/20'
|
|
80
|
+
: 'text-gray-400 hover:text-white hover:bg-po-border'
|
|
81
|
+
}`}
|
|
82
|
+
title={notificationCount > 0 ? `${notificationCount} pending requests` : 'No notifications'}
|
|
83
|
+
>
|
|
84
|
+
{/* Bell icon */}
|
|
85
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
86
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
|
87
|
+
</svg>
|
|
88
|
+
{/* Badge */}
|
|
89
|
+
{notificationCount > 0 && (
|
|
90
|
+
<span
|
|
91
|
+
className={`absolute -top-1 -right-1 bg-po-warning text-black text-xs font-bold w-5 h-5 flex items-center justify-center rounded-full ${
|
|
92
|
+
animate ? 'animate-bounce' : ''
|
|
93
|
+
}`}
|
|
94
|
+
>
|
|
95
|
+
{notificationCount}
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
98
|
+
</button>
|
|
99
|
+
|
|
100
|
+
{/* Dropdown with notification summaries */}
|
|
101
|
+
{showNotifications && notifications.length > 0 && (
|
|
102
|
+
<div className="absolute right-0 top-full mt-2 w-80 bg-po-surface border border-po-border rounded-lg shadow-xl z-50">
|
|
103
|
+
<div className="p-3 border-b border-po-border">
|
|
104
|
+
<h3 className="font-medium text-white">Pending Requests</h3>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="max-h-64 overflow-auto">
|
|
107
|
+
{notifications.map((n) => (
|
|
108
|
+
<div key={n.id} className="p-3 border-b border-po-border last:border-0 hover:bg-po-bg">
|
|
109
|
+
<div className="flex items-center gap-2 mb-1">
|
|
110
|
+
<span className="text-xs bg-po-warning text-black px-1.5 py-0.5 rounded font-medium">
|
|
111
|
+
{n.type}
|
|
112
|
+
</span>
|
|
113
|
+
<span className="text-xs text-po-accent">{n.po_name}</span>
|
|
114
|
+
</div>
|
|
115
|
+
<p className="text-sm text-gray-300 line-clamp-2">{n.message}</p>
|
|
116
|
+
</div>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
<div className="p-2 border-t border-po-border">
|
|
120
|
+
<p className="text-xs text-gray-500 text-center">
|
|
121
|
+
Respond in the notification panel below
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Message Bus toggle */}
|
|
129
|
+
<button
|
|
130
|
+
onClick={toggleBus}
|
|
131
|
+
className={`px-3 py-1.5 text-sm rounded transition-colors ${
|
|
132
|
+
busOpen
|
|
133
|
+
? 'bg-po-accent text-white'
|
|
134
|
+
: 'bg-po-border text-gray-300 hover:bg-po-accent/50'
|
|
135
|
+
}`}
|
|
136
|
+
>
|
|
137
|
+
Bus
|
|
138
|
+
</button>
|
|
139
|
+
</header>
|
|
140
|
+
)
|
|
141
|
+
}
|