Skip to content

Commit 9263715

Browse files
committed
feat(apps): refactor the Polkadot API Tools into a step-by-step hierarchical interface
1 parent b59e6f4 commit 9263715

File tree

1 file changed

+124
-125
lines changed

1 file changed

+124
-125
lines changed

apps/playground/app/developer/page.tsx

Lines changed: 124 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import { Textarea } from "@/components/ui/textarea"
1010
import { Button } from "@/components/ui/button"
1111
import { ScrollArea } from "@/components/ui/scroll-area"
1212
import { Play, Zap, Terminal } from "lucide-react"
13+
import {
14+
Collapsible,
15+
CollapsibleContent,
16+
CollapsibleTrigger,
17+
} from "@/components/ui/collapsible"
18+
import { ChevronDown } from "lucide-react"
1319

1420
type ToolLike = { name?: string; description?: string; schema?: any; schemaJson?: any; call: (args: any) => Promise<any> }
1521
type EndpointKey = "assets" | "swap" | "bifrost" | "staking"
@@ -39,74 +45,46 @@ export default function DeveloperPage() {
3945
const [selectedMethod, setSelectedMethod] = useState("")
4046
const [toolParams, setToolParams] = useState("{}")
4147
const [toolCalls, setToolCalls] = useState<ToolCall[]>([])
48+
const [formData, setFormData] = useState<Record<string, any>>({})
49+
const [parsedSchema, setParsedSchema] = useState<any>(null)
4250

43-
useEffect(() => {
44-
const init = async () => {
45-
try {
46-
const raw = localStorage.getItem("polkadot-agent-config")
47-
if (!raw) return
48-
const cfg = JSON.parse(raw) as AgentConfigLocal
49-
50-
const [{ PolkadotAgentKit }, { default: zodToJsonSchema }] = await Promise.all([
51-
import("@polkadot-agent-kit/sdk"),
52-
import("zod-to-json-schema"),
53-
])
54-
55-
const kit = new PolkadotAgentKit({
56-
privateKey: cfg.privateKey,
57-
keyType: cfg.keyType,
58-
chains: cfg.chains as any,
59-
})
60-
await kit.initializeApi()
51+
const selectedTool = useMemo(() => {
52+
if (!selectedEndpoint || !selectedMethod || !toolsMap) return null
53+
return toolsMap[selectedEndpoint][selectedMethod]
54+
}, [selectedEndpoint, selectedMethod, toolsMap])
6155

62-
const ep: ToolsMap = {
63-
assets: {
64-
getNativeBalanceTool: kit.getNativeBalanceTool(),
65-
transferNativeTool: kit.transferNativeTool(),
66-
xcmTransferNativeTool: kit.xcmTransferNativeTool(),
67-
},
68-
swap: {
69-
swapTokensTool: kit.swapTokensTool(),
70-
},
71-
bifrost: {
72-
mintVdotTool: kit.mintVdotTool(),
73-
},
74-
staking: {
75-
joinPoolTool: kit.joinPoolTool(),
76-
bondExtraTool: kit.bondExtraTool(),
77-
unbondTool: kit.unbondTool(),
78-
withdrawUnbondedTool: kit.withdrawUnbondedTool(),
79-
claimRewardsTool: kit.claimRewardsTool(),
80-
},
81-
}
56+
const selectedSchemaJson = useMemo(() => {
57+
const sj = (selectedTool as any)?.schemaJson
58+
return sj ? JSON.stringify(sj, null, 2) : ""
59+
}, [selectedTool])
8260

83-
for (const group of Object.values(ep)) {
84-
for (const [name, tool] of Object.entries(group)) {
85-
const anyTool = tool as any
86-
if (anyTool?.schema) {
87-
try {
88-
const fullSchema = zodToJsonSchema(anyTool.schema, name) as any
61+
// This useEffect handles parsing the schema and initializing the form data
62+
// when a tool is selected.
63+
useEffect(() => {
64+
const schemaString = (selectedTool as any)?.schemaJson
65+
if (schemaString && schemaString !== "// Select a method to view schema") {
66+
try {
67+
const schema = JSON.parse(schemaString)
68+
setParsedSchema(schema)
8969

90-
if (fullSchema?.$ref && fullSchema?.definitions) {
91-
const refName = fullSchema.$ref.replace('#/definitions/', '')
92-
anyTool.schemaJson = fullSchema.definitions[refName] || fullSchema
93-
} else {
94-
anyTool.schemaJson = fullSchema
95-
}
96-
} catch {}
97-
}
70+
// Initialize form data with default values from the new schema
71+
const initialFormData: Record<string, any> = {}
72+
if (schema.properties) {
73+
for (const [key, prop] of Object.entries(schema.properties as Record<string, any>)) {
74+
initialFormData[key] = prop.default ?? ""
9875
}
9976
}
100-
101-
setToolsMap(ep)
102-
setAgentReady(true)
77+
setFormData(initialFormData)
10378
} catch (e) {
104-
console.error("Developer init failed:", e)
105-
setAgentReady(false)
79+
console.error("Failed to parse schema:", e)
80+
setParsedSchema(null)
81+
setFormData({})
10682
}
83+
} else {
84+
setParsedSchema(null)
85+
setFormData({})
10786
}
108-
init()
109-
}, [])
87+
}, [selectedTool])
11088

11189
const staticMethods: Record<EndpointKey, string[]> = {
11290
assets: [
@@ -135,24 +113,46 @@ export default function DeveloperPage() {
135113
return dynamic.length ? dynamic : staticMethods[selectedEndpoint]
136114
}, [selectedEndpoint, toolsMap])
137115

138-
const selectedTool = useMemo(() => {
139-
if (!selectedEndpoint || !selectedMethod || !toolsMap) return null
140-
return toolsMap[selectedEndpoint][selectedMethod]
141-
}, [selectedEndpoint, selectedMethod, toolsMap])
116+
// This function will render the correct input field based on the schema property type.
117+
const renderParameterInput = (key: string, prop: any) => {
118+
const handleInputChange = (value: any) => {
119+
setFormData(prev => ({ ...prev, [key]: value }))
120+
}
142121

143-
const selectedSchemaJson = useMemo(() => {
144-
const sj = (selectedTool as any)?.schemaJson
145-
return sj ? JSON.stringify(sj, null, 2) : ""
146-
}, [selectedTool])
122+
// If the property is an enum, render a dropdown select.
123+
if (prop.enum) {
124+
return (
125+
<Select onValueChange={handleInputChange} value={formData[key]}>
126+
<SelectTrigger className="modern-select">
127+
<SelectValue placeholder={`Select ${prop.description || key}...`} />
128+
</SelectTrigger>
129+
<SelectContent>
130+
{prop.enum.map((option: string) => (
131+
<SelectItem key={option} value={option}>{option}</SelectItem>
132+
))}
133+
</SelectContent>
134+
</Select>
135+
)
136+
}
137+
138+
// Render a standard text input for other types.
139+
return (
140+
<Input
141+
placeholder={prop.description || key}
142+
value={formData[key] || ""}
143+
onChange={e => handleInputChange(e.target.value)}
144+
className="modern-input"
145+
/>
146+
)
147+
}
147148

148-
const runTool = async () => {
149+
const runTool = async (params: Record<string, any>) => {
149150
if (!selectedTool) return
150151
const id = String(Date.now())
151-
setToolCalls(prev => [...prev, { id, tool: selectedEndpoint || "", method: selectedMethod, params: toolParams, status: "pending" }])
152+
setToolCalls(prev => [...prev, { id, tool: selectedEndpoint || "", method: selectedMethod, params: JSON.stringify(params, null, 2), status: "pending" }])
152153
try {
153-
const parsed = toolParams ? JSON.parse(toolParams) : {}
154-
console.log("[Developer] Executing:", { endpoint: selectedEndpoint, method: selectedMethod, params: parsed })
155-
const res = await (selectedTool as any).call(parsed)
154+
console.log("[Developer] Executing:", { endpoint: selectedEndpoint, method: selectedMethod, params: params })
155+
const res = await (selectedTool as any).call(params)
156156
console.log("[Developer] Response:", res)
157157
setToolCalls(prev => prev.map(c => c.id === id ? { ...c, status: "success", response: JSON.stringify(res, null, 2) } : c))
158158
} catch (err: any) {
@@ -189,10 +189,11 @@ export default function DeveloperPage() {
189189
Polkadot API Tools
190190
</h3>
191191

192-
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3 sm:gap-4">
193-
<div className="col-span-1">
194-
<label className="text-sm font-semibold mb-3 block modern-text-primary">Select API Endpoint</label>
195-
<Select value={selectedEndpoint} onValueChange={(v) => { setSelectedEndpoint(v as EndpointKey); setSelectedMethod(""); }}>
192+
<div className="space-y-4">
193+
{/* Step 1: Select API Endpoint */}
194+
<div className="space-y-2">
195+
<label className="text-sm font-semibold block modern-text-primary">Select API Endpoint</label>
196+
<Select value={selectedEndpoint} onValueChange={(v) => { setSelectedEndpoint(v as EndpointKey); setSelectedMethod(""); setParsedSchema(null); setFormData({}); }}>
196197
<SelectTrigger className="h-12 modern-select">
197198
<SelectValue placeholder="Choose endpoint..." />
198199
</SelectTrigger>
@@ -202,61 +203,59 @@ export default function DeveloperPage() {
202203
<SelectItem value="bifrost">Bifrost</SelectItem>
203204
<SelectItem value="staking">Staking</SelectItem>
204205
</SelectContent>
205-
</Select>
206-
</div>
207-
208-
<div className="col-span-1">
209-
<label className="text-sm font-semibold mb-3 block modern-text-primary">Method</label>
210-
<Select value={selectedMethod} onValueChange={setSelectedMethod} disabled={!selectedEndpoint}>
211-
<SelectTrigger className="h-12 modern-select">
212-
<SelectValue placeholder="Choose method..." />
213-
</SelectTrigger>
214-
<SelectContent>
215-
{methodOptions.map(m => (
216-
<SelectItem key={m} value={m}>{m}</SelectItem>
217-
))}
218-
</SelectContent>
219206
</Select>
220207
</div>
221208

222-
<div className="col-span-1">
223-
<label className="text-sm font-semibold mb-3 block modern-text-primary">Quick Params</label>
224-
<Input
225-
className="h-12 modern-input"
226-
placeholder='e.g. {"chain":"paseo"}'
227-
onChange={(e) => setToolParams(e.target.value)}
228-
value={toolParams}
229-
/>
230-
</div>
231-
</div>
209+
{/* Step 2: Select Method (appears after endpoint is selected) */}
210+
{selectedEndpoint && (
211+
<div className="space-y-2">
212+
<label className="text-sm font-semibold block modern-text-primary">Method</label>
213+
<Select value={selectedMethod} onValueChange={setSelectedMethod} disabled={!selectedEndpoint}>
214+
<SelectTrigger className="h-12 modern-select">
215+
<SelectValue placeholder="Choose method..." />
216+
</SelectTrigger>
217+
<SelectContent>
218+
{methodOptions.map(m => (
219+
<SelectItem key={m} value={m}>{m}</SelectItem>
220+
))}
221+
</SelectContent>
222+
</Select>
223+
</div>
224+
)}
232225

233-
<div className="mt-3 sm:mt-4 grid grid-cols-1 lg:grid-cols-2 gap-3 sm:gap-4">
234-
<div>
235-
<label className="text-sm font-semibold mb-2 sm:mb-3 block modern-text-primary">Parameters (JSON)</label>
236-
<Textarea
237-
className="font-mono text-xs sm:text-sm modern-input min-h-[100px] sm:min-h-[120px]"
238-
value={toolParams}
239-
onChange={(e) => setToolParams(e.target.value)}
240-
placeholder="{}"
241-
rows={4}
242-
/>
243-
</div>
244-
<div>
245-
<label className="text-sm font-semibold mb-2 sm:mb-3 block modern-text-primary">Schema (readonly)</label>
246-
<pre className="text-xs bg-black/30 p-2 sm:p-3 rounded-lg font-mono overflow-x-auto border border-white/10 min-h-[100px] sm:min-h-[120px] text-white leading-relaxed">
247-
{selectedSchemaJson || "// Select a method to view schema"}
248-
</pre>
226+
{/* Step 3: Parameters (appears after method is selected and schema is parsed) */}
227+
{selectedMethod && parsedSchema?.properties && (
228+
<div className="space-y-4 pt-4 border-t border-white/10">
229+
{Object.entries(parsedSchema.properties).map(([key, prop]: [string, any]) => (
230+
<Collapsible key={key} defaultOpen>
231+
<CollapsibleTrigger className="flex justify-between items-center w-full text-left">
232+
<h4 className="text-md font-semibold flex items-center gap-2">
233+
{key}
234+
<span className="text-xs font-mono text-gray-400">({prop.type})</span>
235+
</h4>
236+
<ChevronDown className="h-4 w-4 transition-transform [&[data-state=open]]:rotate-180" />
237+
</CollapsibleTrigger>
238+
<CollapsibleContent className="pt-2">
239+
<p className="text-xs text-gray-400 mb-2">{prop.description}</p>
240+
{renderParameterInput(key, prop)}
241+
</CollapsibleContent>
242+
</Collapsible>
243+
))}
244+
</div>
245+
)}
246+
247+
{/* Execute Button */}
248+
<div className="pt-4 border-t border-white/10">
249+
<Button
250+
onClick={() => runTool(formData)}
251+
disabled={!agentReady || !selectedEndpoint || !selectedMethod || !parsedSchema}
252+
className="w-full modern-button-primary"
253+
>
254+
<Play className="w-4 h-4 sm:w-5 sm:h-5 mr-2" />
255+
Execute API Call
256+
</Button>
249257
</div>
250258
</div>
251-
252-
<Button
253-
onClick={runTool}
254-
disabled={!agentReady || !selectedEndpoint || !selectedMethod}
255-
className="mt-4 sm:mt-6 px-6 sm:px-8 h-10 sm:h-12 text-sm sm:text-base font-medium modern-button-primary w-full sm:w-auto"
256-
>
257-
<Play className="w-4 h-4 sm:w-5 sm:h-5 mr-2" />
258-
Execute API Call
259-
</Button>
260259
</Card>
261260

262261
<Card className="p-3 sm:p-4 lg:p-6 modern-card">

0 commit comments

Comments
 (0)