Skip to content

Commit 3ad3093

Browse files
committed
Display the agent defintion in well-formated typescript, with syntax coloring
1 parent 99cc2aa commit 3ad3093

File tree

3 files changed

+186
-10
lines changed

3 files changed

+186
-10
lines changed

bun.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/src/app/publishers/[id]/agents/[agentId]/[version]/page.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Badge } from '@/components/ui/badge'
99
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
1010
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
1111
import { BackButton } from '@/components/ui/back-button'
12-
import { JsonViewer } from '@/components/agent/json-viewer'
12+
import { TypeScriptViewer } from '@/components/agent/typescript-viewer'
1313
import { EnhancedCopyButton } from '@/components/ui/enhanced-copy-button'
1414
import { cn } from '@/lib/utils'
1515
import { AgentUsageMetrics } from './agent-usage-metrics'
@@ -126,11 +126,12 @@ const AgentDetailPage = async ({ params }: AgentDetailPageProps) => {
126126

127127
return (
128128
<div className="container mx-auto py-6 px-4">
129-
<div className="max-w-4xl mx-auto"> {/* Navigation */}
129+
<div className="max-w-4xl mx-auto">
130+
{' '}
131+
{/* Navigation */}
130132
<div className="mb-6">
131133
<BackButton />
132134
</div>
133-
134135
{/* Agent Header */}
135136
<Card className="mb-6">
136137
<CardHeader>
@@ -202,14 +203,12 @@ const AgentDetailPage = async ({ params }: AgentDetailPageProps) => {
202203
</div>
203204
</CardHeader>
204205
</Card>
205-
206206
{/* Usage Metrics */}
207207
<AgentUsageMetrics
208208
publisherId={params.id}
209209
agentId={params.agentId}
210210
version={params.version}
211211
/>
212-
213212
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
214213
{/* Version Navigation */}
215214
<div className="lg:col-span-1">
@@ -264,17 +263,17 @@ const AgentDetailPage = async ({ params }: AgentDetailPageProps) => {
264263
</Card>
265264
</div>
266265

267-
{/* Agent Configuration */}
266+
{/* Agent Definition */}
268267
<div className="lg:col-span-3">
269268
<Card>
270269
<CardHeader>
271-
<CardTitle className="text-lg">Agent Configuration</CardTitle>
270+
<CardTitle className="text-lg">Agent Definition</CardTitle>
272271
<p className="text-sm text-muted-foreground">
273-
Complete agent data in JSON format
272+
Complete agent data in TypeScript format
274273
</p>
275274
</CardHeader>
276275
<CardContent>
277-
<JsonViewer data={agentData} />
276+
<TypeScriptViewer data={agentData} />
278277
</CardContent>
279278
</Card>
280279
</div>
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { Check, Copy } from 'lucide-react'
5+
import { Highlight, themes } from 'prism-react-renderer'
6+
7+
import { Button } from '@/components/ui/button'
8+
9+
interface TypeScriptViewerProps {
10+
data: any
11+
className?: string
12+
}
13+
14+
function formatAsTypeScript(obj: any, indent: number = 0): string {
15+
const spaces = ' '.repeat(indent)
16+
const nextSpaces = ' '.repeat(indent + 1)
17+
18+
if (obj === null) return 'null'
19+
if (obj === undefined) return 'undefined'
20+
if (typeof obj === 'string') {
21+
return JSON.stringify(obj)
22+
}
23+
if (typeof obj === 'number' || typeof obj === 'boolean') {
24+
return String(obj)
25+
}
26+
if (Array.isArray(obj)) {
27+
if (obj.length === 0) return '[]'
28+
const items = obj.map(
29+
(item) => `${nextSpaces}${formatAsTypeScript(item, indent + 1)}`
30+
)
31+
return `[\n${items.join(',\n')}\n${spaces}]`
32+
}
33+
if (typeof obj === 'object') {
34+
const entries = Object.entries(obj)
35+
if (entries.length === 0) return '{}'
36+
37+
// Define preferred field order
38+
const fieldOrder = [
39+
'id',
40+
'displayName',
41+
'publisher',
42+
'version',
43+
'model',
44+
'reasoningOptions',
45+
'toolNames',
46+
'spawnableAgents',
47+
'inputSchema',
48+
'includeMessageHistory',
49+
'outputMode',
50+
'outputSchema',
51+
'spawnerPrompt',
52+
'systemPrompt',
53+
'instructionsPrompt',
54+
'stepPrompt',
55+
'handleSteps',
56+
]
57+
58+
// Sort entries by preferred order, keeping unknown fields in original order
59+
const sortedEntries = entries.sort(([keyA], [keyB]) => {
60+
const indexA = fieldOrder.indexOf(keyA)
61+
const indexB = fieldOrder.indexOf(keyB)
62+
63+
// If both keys are in fieldOrder, sort by their order
64+
if (indexA !== -1 && indexB !== -1) {
65+
return indexA - indexB
66+
}
67+
// If only keyA is in fieldOrder, it comes first
68+
if (indexA !== -1 && indexB === -1) {
69+
return -1
70+
}
71+
// If only keyB is in fieldOrder, it comes first
72+
if (indexA === -1 && indexB !== -1) {
73+
return 1
74+
}
75+
// If neither key is in fieldOrder, maintain original order
76+
return 0
77+
})
78+
79+
const formattedEntries = sortedEntries.map(([key, value]) => {
80+
// Special handling for handleSteps - don't put quotes around the function
81+
if (key === 'handleSteps' && typeof value === 'string') {
82+
return `${nextSpaces}${key}: ${value}`
83+
}
84+
85+
// Special handling for prompt properties with newlines
86+
if (
87+
[
88+
'spawnerPrompt',
89+
'systemPrompt',
90+
'instructionsPrompt',
91+
'stepPrompt',
92+
].includes(key) &&
93+
typeof value === 'string'
94+
) {
95+
const cleanedValue = value.replace(/\\n/g, '\n').replace(/`/g, '\\`')
96+
return `${nextSpaces}${key}: \`${cleanedValue}\``
97+
}
98+
99+
return `${nextSpaces}${key}: ${formatAsTypeScript(value, indent + 1)}`
100+
})
101+
102+
return `{\n${formattedEntries.join(',\n')}\n${spaces}}`
103+
}
104+
105+
return String(obj)
106+
}
107+
108+
export function TypeScriptViewer({
109+
data,
110+
className = '',
111+
}: TypeScriptViewerProps) {
112+
const [copied, setCopied] = useState(false)
113+
114+
const formattedTypeScript = `const agentDefinition = ${formatAsTypeScript(data)}`
115+
116+
const handleCopy = async () => {
117+
try {
118+
await navigator.clipboard.writeText(formattedTypeScript)
119+
setCopied(true)
120+
setTimeout(() => setCopied(false), 2000)
121+
} catch (err) {
122+
console.error('Failed to copy:', err)
123+
}
124+
}
125+
126+
return (
127+
<div className={`relative ${className}`}>
128+
<Highlight
129+
theme={{
130+
...themes.vsDark,
131+
plain: { ...themes.vsDark.plain, backgroundColor: 'transparent' },
132+
}}
133+
code={formattedTypeScript}
134+
language="typescript"
135+
>
136+
{({
137+
className: highlightClassName,
138+
style,
139+
tokens,
140+
getLineProps,
141+
getTokenProps,
142+
}) => (
143+
<pre
144+
className={`${highlightClassName} bg-muted p-4 rounded-lg overflow-x-auto text-sm max-h-[600px] overflow-y-auto`}
145+
style={{ ...style, backgroundColor: 'transparent' }}
146+
>
147+
{tokens.map((line, i) => (
148+
<div key={i} {...getLineProps({ line })}>
149+
{line.map((token, key) => (
150+
<span key={key} {...getTokenProps({ token, key })} />
151+
))}
152+
</div>
153+
))}
154+
</pre>
155+
)}
156+
</Highlight>
157+
<Button
158+
variant="outline"
159+
size="sm"
160+
className="absolute top-2 right-2"
161+
onClick={handleCopy}
162+
>
163+
{copied ? (
164+
<>
165+
<Check className="h-4 w-4 mr-1" />
166+
Copied
167+
</>
168+
) : (
169+
<>
170+
<Copy className="h-4 w-4 mr-1" />
171+
Copy
172+
</>
173+
)}
174+
</Button>
175+
</div>
176+
)
177+
}

0 commit comments

Comments
 (0)