prompt_objects 0.1.0 → 0.3.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 +68 -0
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/exe/prompt_objects +387 -1
- data/frontend/src/App.tsx +12 -3
- data/frontend/src/components/CapabilitiesPanel.tsx +122 -25
- data/frontend/src/components/ChatPanel.tsx +43 -6
- data/frontend/src/components/ContextMenu.tsx +67 -0
- data/frontend/src/components/MessageBus.tsx +4 -3
- data/frontend/src/components/ModelSelector.tsx +5 -1
- data/frontend/src/components/PODetail.tsx +8 -1
- data/frontend/src/components/PromptPanel.tsx +124 -19
- data/frontend/src/components/ThreadsSidebar.tsx +46 -2
- data/frontend/src/components/UsagePanel.tsx +105 -0
- data/frontend/src/hooks/useWebSocket.ts +69 -0
- data/frontend/src/store/index.ts +10 -0
- data/frontend/src/types/index.ts +16 -2
- data/lib/prompt_objects/cli.rb +1 -0
- data/lib/prompt_objects/connectors/mcp.rb +1 -0
- data/lib/prompt_objects/environment.rb +35 -1
- data/lib/prompt_objects/llm/anthropic_adapter.rb +15 -1
- data/lib/prompt_objects/llm/factory.rb +93 -6
- data/lib/prompt_objects/llm/gemini_adapter.rb +13 -1
- data/lib/prompt_objects/llm/openai_adapter.rb +21 -4
- data/lib/prompt_objects/llm/pricing.rb +49 -0
- data/lib/prompt_objects/llm/response.rb +3 -2
- data/lib/prompt_objects/mcp/server.rb +1 -0
- data/lib/prompt_objects/message_bus.rb +27 -8
- data/lib/prompt_objects/prompt_object.rb +15 -3
- data/lib/prompt_objects/server/api/routes.rb +186 -29
- data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +1 -0
- data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +77 -0
- data/lib/prompt_objects/server/public/index.html +2 -2
- data/lib/prompt_objects/server/websocket_handler.rb +160 -12
- data/lib/prompt_objects/server.rb +67 -0
- data/lib/prompt_objects/session/store.rb +399 -4
- data/lib/prompt_objects/universal/add_capability.rb +6 -1
- data/lib/prompt_objects/universal/add_primitive.rb +6 -1
- data/lib/prompt_objects/universal/create_capability.rb +4 -0
- data/lib/prompt_objects/universal/create_primitive.rb +4 -0
- data/lib/prompt_objects/universal/delete_primitive.rb +77 -0
- data/lib/prompt_objects/universal/modify_prompt.rb +164 -0
- data/lib/prompt_objects/universal/remove_capability.rb +73 -0
- data/lib/prompt_objects.rb +5 -1
- data/prompt_objects.gemspec +1 -1
- data/templates/arc-agi-1/manifest.yml +22 -0
- data/templates/arc-agi-1/objects/data_manager.md +42 -0
- data/templates/arc-agi-1/objects/observer.md +100 -0
- data/templates/arc-agi-1/objects/solver.md +118 -0
- data/templates/arc-agi-1/objects/verifier.md +79 -0
- data/templates/arc-agi-1/primitives/check_arc_data.rb +53 -0
- data/templates/arc-agi-1/primitives/find_objects.rb +72 -0
- data/templates/arc-agi-1/primitives/grid_diff.rb +70 -0
- data/templates/arc-agi-1/primitives/grid_info.rb +42 -0
- data/templates/arc-agi-1/primitives/grid_transform.rb +50 -0
- data/templates/arc-agi-1/primitives/load_arc_task.rb +68 -0
- data/templates/arc-agi-1/primitives/render_grid.rb +78 -0
- data/templates/arc-agi-1/primitives/test_solution.rb +131 -0
- metadata +23 -3
- data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +0 -77
- data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import type { PromptObject, CapabilityInfo } from '../types'
|
|
2
3
|
|
|
3
4
|
interface CapabilitiesPanelProps {
|
|
4
5
|
po: PromptObject
|
|
@@ -6,38 +7,134 @@ interface CapabilitiesPanelProps {
|
|
|
6
7
|
|
|
7
8
|
export function CapabilitiesPanel({ po }: CapabilitiesPanelProps) {
|
|
8
9
|
const capabilities = po.capabilities || []
|
|
10
|
+
const universalCapabilities = po.universal_capabilities || []
|
|
9
11
|
|
|
10
12
|
return (
|
|
11
13
|
<div className="h-full overflow-auto p-4">
|
|
12
|
-
|
|
14
|
+
{/* Declared Capabilities */}
|
|
15
|
+
<div className="mb-6">
|
|
16
|
+
<h3 className="text-lg font-medium text-white mb-3">
|
|
17
|
+
Declared Capabilities
|
|
18
|
+
<span className="ml-2 text-sm text-gray-500">({capabilities.length})</span>
|
|
19
|
+
</h3>
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
{capabilities.length === 0 ? (
|
|
22
|
+
<div className="text-gray-500 text-sm py-4 px-3 bg-po-bg rounded-lg border border-po-border">
|
|
23
|
+
No capabilities declared. This PO can only use universal capabilities.
|
|
24
|
+
</div>
|
|
25
|
+
) : (
|
|
26
|
+
<div className="space-y-1">
|
|
27
|
+
{capabilities.map((cap) => (
|
|
28
|
+
<CapabilityItem key={cap.name} capability={cap} accent />
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{/* Universal Capabilities */}
|
|
35
|
+
<div>
|
|
36
|
+
<h3 className="text-lg font-medium text-white mb-3">
|
|
37
|
+
Universal Capabilities
|
|
38
|
+
<span className="ml-2 text-sm text-gray-500">({universalCapabilities.length})</span>
|
|
39
|
+
</h3>
|
|
40
|
+
<p className="text-xs text-gray-500 mb-3">
|
|
41
|
+
Available to all Prompt Objects automatically.
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<div className="space-y-1">
|
|
45
|
+
{universalCapabilities.map((cap) => (
|
|
46
|
+
<CapabilityItem key={cap.name} capability={cap} />
|
|
27
47
|
))}
|
|
28
48
|
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CapabilityItemProps {
|
|
55
|
+
capability: CapabilityInfo
|
|
56
|
+
accent?: boolean // Use accent color for name (for declared caps)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function CapabilityItem({ capability, accent }: CapabilityItemProps) {
|
|
60
|
+
const [expanded, setExpanded] = useState(false)
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="bg-po-bg border border-po-border rounded-lg overflow-hidden">
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => setExpanded(!expanded)}
|
|
66
|
+
className="w-full px-3 py-2 flex items-center justify-between hover:bg-po-surface transition-colors"
|
|
67
|
+
>
|
|
68
|
+
<span className={`font-mono text-sm ${accent ? 'text-po-accent' : 'text-gray-300'}`}>
|
|
69
|
+
{capability.name}
|
|
70
|
+
</span>
|
|
71
|
+
<span className="text-gray-500 text-xs">{expanded ? '▼' : '▶'}</span>
|
|
72
|
+
</button>
|
|
73
|
+
{expanded && (
|
|
74
|
+
<div className="px-3 py-2 border-t border-po-border bg-po-surface space-y-3">
|
|
75
|
+
<p className="text-xs text-gray-400">{capability.description}</p>
|
|
76
|
+
|
|
77
|
+
{capability.parameters && (
|
|
78
|
+
<ParametersDisplay parameters={capability.parameters} />
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
29
81
|
)}
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
30
85
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
86
|
+
interface ParametersDisplayProps {
|
|
87
|
+
parameters: Record<string, unknown>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ParametersDisplay({ parameters }: ParametersDisplayProps) {
|
|
91
|
+
const properties = (parameters.properties as Record<string, unknown>) || {}
|
|
92
|
+
const required = (parameters.required as string[]) || []
|
|
93
|
+
|
|
94
|
+
const propertyNames = Object.keys(properties)
|
|
95
|
+
|
|
96
|
+
if (propertyNames.length === 0) {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div>
|
|
102
|
+
<div className="text-xs text-gray-500 mb-2 font-medium">Parameters</div>
|
|
103
|
+
<div className="space-y-2">
|
|
104
|
+
{propertyNames.map((propName) => {
|
|
105
|
+
const prop = properties[propName] as Record<string, unknown>
|
|
106
|
+
const isRequired = required.includes(propName)
|
|
107
|
+
|
|
108
|
+
const propType = prop.type ? String(prop.type) : null
|
|
109
|
+
const propDescription = prop.description ? String(prop.description) : null
|
|
110
|
+
const propEnum = prop.enum as string[] | undefined
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div key={propName} className="bg-po-bg rounded p-2">
|
|
114
|
+
<div className="flex items-center gap-2">
|
|
115
|
+
<span className="font-mono text-xs text-po-accent">{propName}</span>
|
|
116
|
+
{propType && (
|
|
117
|
+
<span className="text-xs text-gray-600">({propType})</span>
|
|
118
|
+
)}
|
|
119
|
+
{isRequired && (
|
|
120
|
+
<span className="text-xs text-red-400">required</span>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
{propDescription && (
|
|
124
|
+
<p className="text-xs text-gray-500 mt-1">{propDescription}</p>
|
|
125
|
+
)}
|
|
126
|
+
{propEnum && propEnum.length > 0 && (
|
|
127
|
+
<div className="mt-1 flex flex-wrap gap-1">
|
|
128
|
+
{propEnum.map((val) => (
|
|
129
|
+
<span key={val} className="text-xs bg-po-surface px-1.5 py-0.5 rounded text-gray-400">
|
|
130
|
+
{val}
|
|
131
|
+
</span>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
})}
|
|
41
138
|
</div>
|
|
42
139
|
</div>
|
|
43
140
|
)
|
|
@@ -133,13 +133,15 @@ function MessageBubble({ message }: { message: Message }) {
|
|
|
133
133
|
const isTool = message.role === 'tool'
|
|
134
134
|
|
|
135
135
|
if (isTool) {
|
|
136
|
+
// Tool messages contain results from tool calls
|
|
137
|
+
const results = message.results || []
|
|
138
|
+
if (results.length === 0) return null
|
|
139
|
+
|
|
136
140
|
return (
|
|
137
|
-
<div className="
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
{message.content && message.content.length > 500 && '...'}
|
|
142
|
-
</div>
|
|
141
|
+
<div className="space-y-2 ml-11">
|
|
142
|
+
{results.map((result, idx) => (
|
|
143
|
+
<ToolResultDisplay key={result.tool_call_id || idx} result={result} />
|
|
144
|
+
))}
|
|
143
145
|
</div>
|
|
144
146
|
)
|
|
145
147
|
}
|
|
@@ -249,3 +251,38 @@ function ToolCallDisplay({ toolCall }: { toolCall: ToolCall }) {
|
|
|
249
251
|
</div>
|
|
250
252
|
)
|
|
251
253
|
}
|
|
254
|
+
|
|
255
|
+
function ToolResultDisplay({ result }: { result: { tool_call_id: string; name?: string; content: string } }) {
|
|
256
|
+
const [expanded, setExpanded] = useState(false)
|
|
257
|
+
const content = result.content || ''
|
|
258
|
+
const truncatedContent = content.slice(0, 200)
|
|
259
|
+
const isTruncated = content.length > 200
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div className="text-xs bg-po-surface/50 border border-po-border/30 rounded overflow-hidden">
|
|
263
|
+
<button
|
|
264
|
+
onClick={() => setExpanded(!expanded)}
|
|
265
|
+
className="w-full px-2 py-1 text-left font-mono flex items-center gap-1 hover:bg-po-surface transition-colors"
|
|
266
|
+
>
|
|
267
|
+
<span className="text-gray-500">{expanded ? '▼' : '▶'}</span>
|
|
268
|
+
<span className="text-gray-400">↳ Result</span>
|
|
269
|
+
{result.name && <span className="text-po-accent/70">from {result.name}</span>}
|
|
270
|
+
</button>
|
|
271
|
+
{expanded && (
|
|
272
|
+
<div className="px-2 pb-2 border-t border-po-border/30">
|
|
273
|
+
<pre className="text-gray-400 whitespace-pre-wrap break-all mt-1 text-[10px] leading-relaxed">
|
|
274
|
+
{content}
|
|
275
|
+
</pre>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
{!expanded && (
|
|
279
|
+
<div className="px-2 pb-1 text-gray-500">
|
|
280
|
+
<span className="whitespace-pre-wrap break-all">
|
|
281
|
+
{truncatedContent}
|
|
282
|
+
{isTruncated && '...'}
|
|
283
|
+
</span>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
interface ContextMenuItem {
|
|
4
|
+
label: string
|
|
5
|
+
onClick: () => void
|
|
6
|
+
icon?: string
|
|
7
|
+
danger?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ContextMenuProps {
|
|
11
|
+
x: number
|
|
12
|
+
y: number
|
|
13
|
+
items: ContextMenuItem[]
|
|
14
|
+
onClose: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
|
|
18
|
+
const menuRef = useRef<HTMLDivElement>(null)
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
22
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
23
|
+
onClose()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
27
|
+
if (e.key === 'Escape') onClose()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
31
|
+
document.addEventListener('keydown', handleEscape)
|
|
32
|
+
return () => {
|
|
33
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
34
|
+
document.removeEventListener('keydown', handleEscape)
|
|
35
|
+
}
|
|
36
|
+
}, [onClose])
|
|
37
|
+
|
|
38
|
+
// Adjust position to stay within viewport
|
|
39
|
+
const adjustedStyle = {
|
|
40
|
+
top: y,
|
|
41
|
+
left: x,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
ref={menuRef}
|
|
47
|
+
className="fixed z-50 bg-po-surface border border-po-border rounded-lg shadow-xl py-1 min-w-[160px]"
|
|
48
|
+
style={adjustedStyle}
|
|
49
|
+
>
|
|
50
|
+
{items.map((item, idx) => (
|
|
51
|
+
<button
|
|
52
|
+
key={idx}
|
|
53
|
+
onClick={() => {
|
|
54
|
+
item.onClick()
|
|
55
|
+
onClose()
|
|
56
|
+
}}
|
|
57
|
+
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-po-bg transition-colors flex items-center gap-2 ${
|
|
58
|
+
item.danger ? 'text-red-400 hover:text-red-300' : 'text-gray-300 hover:text-white'
|
|
59
|
+
}`}
|
|
60
|
+
>
|
|
61
|
+
{item.icon && <span>{item.icon}</span>}
|
|
62
|
+
{item.label}
|
|
63
|
+
</button>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -41,9 +41,10 @@ export function MessageBus() {
|
|
|
41
41
|
</span>
|
|
42
42
|
</div>
|
|
43
43
|
<div className="text-gray-300 break-words">
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
:
|
|
44
|
+
{(() => {
|
|
45
|
+
const text = msg.summary || (typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content))
|
|
46
|
+
return text.length > 200 ? text.slice(0, 200) + '...' : text
|
|
47
|
+
})()}
|
|
47
48
|
</div>
|
|
48
49
|
</div>
|
|
49
50
|
))
|
|
@@ -33,6 +33,8 @@ export function ModelSelector({ switchLLM }: Props) {
|
|
|
33
33
|
openai: 'OpenAI',
|
|
34
34
|
anthropic: 'Anthropic',
|
|
35
35
|
gemini: 'Gemini',
|
|
36
|
+
ollama: 'Ollama',
|
|
37
|
+
openrouter: 'OpenRouter',
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
return (
|
|
@@ -62,7 +64,9 @@ export function ModelSelector({ switchLLM }: Props) {
|
|
|
62
64
|
<div className="px-3 py-2 bg-po-bg text-xs font-medium text-gray-400 uppercase tracking-wide flex items-center justify-between">
|
|
63
65
|
<span>{providerNames[provider.name] || provider.name}</span>
|
|
64
66
|
{!provider.available && (
|
|
65
|
-
<span className="text-red-400 text-[10px] normal-case">
|
|
67
|
+
<span className="text-red-400 text-[10px] normal-case">
|
|
68
|
+
{provider.name === 'ollama' ? 'Not Running' : 'No API Key'}
|
|
69
|
+
</span>
|
|
66
70
|
)}
|
|
67
71
|
</div>
|
|
68
72
|
{provider.models.map((model) => {
|
|
@@ -9,6 +9,7 @@ interface PODetailProps {
|
|
|
9
9
|
createSession: (target: string, name?: string) => void
|
|
10
10
|
switchSession: (target: string, sessionId: string) => void
|
|
11
11
|
createThread: (target: string, name?: string) => void
|
|
12
|
+
updatePrompt: (target: string, prompt: string) => void
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function PODetail({
|
|
@@ -16,6 +17,7 @@ export function PODetail({
|
|
|
16
17
|
createSession,
|
|
17
18
|
switchSession,
|
|
18
19
|
createThread,
|
|
20
|
+
updatePrompt,
|
|
19
21
|
}: PODetailProps) {
|
|
20
22
|
const { activeTab, setActiveTab, selectPO } = useStore()
|
|
21
23
|
const po = useSelectedPO()
|
|
@@ -95,7 +97,12 @@ export function PODetail({
|
|
|
95
97
|
/>
|
|
96
98
|
)}
|
|
97
99
|
{activeTab === 'capabilities' && <CapabilitiesPanel po={po} />}
|
|
98
|
-
{activeTab === 'prompt' &&
|
|
100
|
+
{activeTab === 'prompt' && (
|
|
101
|
+
<PromptPanel
|
|
102
|
+
po={po}
|
|
103
|
+
onSave={(prompt) => updatePrompt(po.name, prompt)}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
99
106
|
</div>
|
|
100
107
|
</div>
|
|
101
108
|
)
|
|
@@ -1,13 +1,78 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
1
2
|
import type { PromptObject } from '../types'
|
|
2
3
|
import { MarkdownMessage } from './MarkdownMessage'
|
|
3
4
|
|
|
4
5
|
interface PromptPanelProps {
|
|
5
6
|
po: PromptObject
|
|
7
|
+
onSave?: (prompt: string) => void
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
export function PromptPanel({ po }: PromptPanelProps) {
|
|
10
|
+
export function PromptPanel({ po, onSave }: PromptPanelProps) {
|
|
9
11
|
const prompt = po.prompt || ''
|
|
10
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
|
+
// Assume save succeeded - server will broadcast update
|
|
38
|
+
setTimeout(() => setSaveStatus('saved'), 500)
|
|
39
|
+
} else {
|
|
40
|
+
setSaveStatus('saved')
|
|
41
|
+
}
|
|
42
|
+
}, 1000) // 1 second debounce
|
|
43
|
+
}, [onSave, prompt])
|
|
44
|
+
|
|
45
|
+
const handlePromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
46
|
+
const newPrompt = e.target.value
|
|
47
|
+
setEditedPrompt(newPrompt)
|
|
48
|
+
debouncedSave(newPrompt)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleStartEditing = () => {
|
|
52
|
+
setEditedPrompt(prompt)
|
|
53
|
+
setIsEditing(true)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handleStopEditing = () => {
|
|
57
|
+
// Save any pending changes
|
|
58
|
+
if (saveTimeoutRef.current) {
|
|
59
|
+
clearTimeout(saveTimeoutRef.current)
|
|
60
|
+
}
|
|
61
|
+
if (editedPrompt !== prompt && onSave) {
|
|
62
|
+
onSave(editedPrompt)
|
|
63
|
+
}
|
|
64
|
+
setIsEditing(false)
|
|
65
|
+
setSaveStatus('saved')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Cleanup timeout on unmount
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
return () => {
|
|
71
|
+
if (saveTimeoutRef.current) {
|
|
72
|
+
clearTimeout(saveTimeoutRef.current)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}, [])
|
|
11
76
|
|
|
12
77
|
return (
|
|
13
78
|
<div className="h-full overflow-auto p-4">
|
|
@@ -23,29 +88,69 @@ export function PromptPanel({ po }: PromptPanelProps) {
|
|
|
23
88
|
|
|
24
89
|
{/* Prompt/Body */}
|
|
25
90
|
<div>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
91
|
+
<div className="flex items-center justify-between mb-3">
|
|
92
|
+
<h3 className="text-lg font-medium text-white">Prompt</h3>
|
|
93
|
+
<div className="flex items-center gap-3">
|
|
94
|
+
{isEditing && (
|
|
95
|
+
<span className={`text-xs ${
|
|
96
|
+
saveStatus === 'saved' ? 'text-green-400' :
|
|
97
|
+
saveStatus === 'saving' ? 'text-yellow-400' :
|
|
98
|
+
'text-gray-400'
|
|
99
|
+
}`}>
|
|
100
|
+
{saveStatus === 'saved' ? 'Saved' :
|
|
101
|
+
saveStatus === 'saving' ? 'Saving...' :
|
|
102
|
+
'Unsaved changes'}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
<button
|
|
106
|
+
onClick={isEditing ? handleStopEditing : handleStartEditing}
|
|
107
|
+
className={`px-3 py-1 text-sm rounded transition-colors ${
|
|
108
|
+
isEditing
|
|
109
|
+
? 'bg-po-accent text-black hover:bg-po-accent/80'
|
|
110
|
+
: 'bg-po-surface border border-po-border text-gray-300 hover:text-white hover:border-po-accent'
|
|
111
|
+
}`}
|
|
112
|
+
>
|
|
113
|
+
{isEditing ? 'Done' : 'Edit'}
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="bg-po-bg rounded-lg border border-po-border">
|
|
119
|
+
{isEditing ? (
|
|
120
|
+
<textarea
|
|
121
|
+
value={editedPrompt}
|
|
122
|
+
onChange={handlePromptChange}
|
|
123
|
+
className="w-full h-96 p-4 bg-transparent text-gray-200 font-mono text-sm resize-none focus:outline-none focus:ring-1 focus:ring-po-accent rounded-lg"
|
|
124
|
+
placeholder="Enter prompt markdown..."
|
|
125
|
+
spellCheck={false}
|
|
126
|
+
/>
|
|
32
127
|
) : (
|
|
33
|
-
<
|
|
128
|
+
<div className="p-4">
|
|
129
|
+
{prompt ? (
|
|
130
|
+
<div className="prose prose-invert max-w-none">
|
|
131
|
+
<MarkdownMessage content={prompt} />
|
|
132
|
+
</div>
|
|
133
|
+
) : (
|
|
134
|
+
<p className="text-gray-500 italic">No prompt defined. Click Edit to add one.</p>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
34
137
|
)}
|
|
35
138
|
</div>
|
|
36
139
|
</div>
|
|
37
140
|
|
|
38
|
-
{/* Raw source toggle */}
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
141
|
+
{/* Raw source toggle (only in view mode) */}
|
|
142
|
+
{!isEditing && (
|
|
143
|
+
<details className="mt-6">
|
|
144
|
+
<summary className="text-sm text-gray-400 cursor-pointer hover:text-white">
|
|
145
|
+
View raw source
|
|
146
|
+
</summary>
|
|
147
|
+
<div className="mt-2 bg-po-bg rounded-lg border border-po-border p-4">
|
|
148
|
+
<pre className="text-xs text-gray-400 font-mono whitespace-pre-wrap overflow-x-auto">
|
|
149
|
+
{prompt || '(empty)'}
|
|
150
|
+
</pre>
|
|
151
|
+
</div>
|
|
152
|
+
</details>
|
|
153
|
+
)}
|
|
49
154
|
</div>
|
|
50
155
|
)
|
|
51
156
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { useMemo } from 'react'
|
|
1
|
+
import { useMemo, useState } from 'react'
|
|
2
|
+
import { ContextMenu } from './ContextMenu'
|
|
2
3
|
import type { PromptObject, Session, ThreadType } from '../types'
|
|
3
4
|
|
|
4
5
|
interface ThreadsSidebarProps {
|
|
5
6
|
po: PromptObject
|
|
6
7
|
switchSession: (target: string, sessionId: string) => void
|
|
7
8
|
createThread: (target: string) => void
|
|
9
|
+
requestUsage?: (sessionId: string, includeTree?: boolean) => void
|
|
10
|
+
exportThread?: (sessionId: string, format?: string) => void
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
// Build a flat list with depth info
|
|
@@ -59,12 +62,18 @@ function ThreadTypeIcon({ type }: { type: ThreadType }) {
|
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
export function ThreadsSidebar({ po, switchSession, createThread }: ThreadsSidebarProps) {
|
|
65
|
+
export function ThreadsSidebar({ po, switchSession, createThread, requestUsage, exportThread }: ThreadsSidebarProps) {
|
|
63
66
|
const sessions = po.sessions || []
|
|
64
67
|
const currentSessionId = po.current_session?.id
|
|
68
|
+
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; sessionId: string } | null>(null)
|
|
65
69
|
|
|
66
70
|
const threadList = useMemo(() => buildThreadList(sessions), [sessions])
|
|
67
71
|
|
|
72
|
+
const handleContextMenu = (e: React.MouseEvent, sessionId: string) => {
|
|
73
|
+
e.preventDefault()
|
|
74
|
+
setContextMenu({ x: e.clientX, y: e.clientY, sessionId })
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
return (
|
|
69
78
|
<div className="h-full flex flex-col">
|
|
70
79
|
<div className="p-3 border-b border-po-border flex items-center justify-between">
|
|
@@ -88,6 +97,7 @@ export function ThreadsSidebar({ po, switchSession, createThread }: ThreadsSideb
|
|
|
88
97
|
<button
|
|
89
98
|
key={session.id}
|
|
90
99
|
onClick={() => switchSession(po.name, session.id)}
|
|
100
|
+
onContextMenu={(e) => handleContextMenu(e, session.id)}
|
|
91
101
|
className={`w-full text-left p-2 rounded text-xs transition-colors ${
|
|
92
102
|
session.id === currentSessionId
|
|
93
103
|
? 'bg-po-accent/20 border border-po-accent'
|
|
@@ -114,6 +124,40 @@ export function ThreadsSidebar({ po, switchSession, createThread }: ThreadsSideb
|
|
|
114
124
|
))
|
|
115
125
|
)}
|
|
116
126
|
</div>
|
|
127
|
+
|
|
128
|
+
{contextMenu && (
|
|
129
|
+
<ContextMenu
|
|
130
|
+
x={contextMenu.x}
|
|
131
|
+
y={contextMenu.y}
|
|
132
|
+
onClose={() => setContextMenu(null)}
|
|
133
|
+
items={[
|
|
134
|
+
...(requestUsage ? [
|
|
135
|
+
{
|
|
136
|
+
label: 'View Usage',
|
|
137
|
+
icon: '📊',
|
|
138
|
+
onClick: () => requestUsage(contextMenu.sessionId),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
label: 'View Tree Usage',
|
|
142
|
+
icon: '🌳',
|
|
143
|
+
onClick: () => requestUsage(contextMenu.sessionId, true),
|
|
144
|
+
},
|
|
145
|
+
] : []),
|
|
146
|
+
...(exportThread ? [
|
|
147
|
+
{
|
|
148
|
+
label: 'Export as Markdown',
|
|
149
|
+
icon: '📄',
|
|
150
|
+
onClick: () => exportThread(contextMenu.sessionId, 'markdown'),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
label: 'Export as JSON',
|
|
154
|
+
icon: '📋',
|
|
155
|
+
onClick: () => exportThread(contextMenu.sessionId, 'json'),
|
|
156
|
+
},
|
|
157
|
+
] : []),
|
|
158
|
+
]}
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
117
161
|
</div>
|
|
118
162
|
)
|
|
119
163
|
}
|