Skip to content

Commit 4a71bb3

Browse files
committed
Add custom UI
1 parent dd25ab6 commit 4a71bb3

6 files changed

Lines changed: 680 additions & 0 deletions

File tree

agentex-ui/app/custom/page.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { connection } from 'next/server';
2+
3+
import { CustomPageRoot } from '@/components/custom/custom-page-root';
4+
import { AgentexProvider } from '@/components/providers';
5+
6+
export default async function CustomPage() {
7+
await connection();
8+
9+
const sgpAppURL = process.env.NEXT_PUBLIC_SGP_APP_URL ?? '';
10+
const agentexAPIBaseURL =
11+
process.env.NEXT_PUBLIC_AGENTEX_API_BASE_URL ?? 'http://localhost:5003';
12+
13+
if (!agentexAPIBaseURL) {
14+
return (
15+
<div role="alert">
16+
<p>Missing some configs</p>
17+
<pre>{JSON.stringify({ sgpAppURL, agentexAPIBaseURL }, null, 2)}</pre>
18+
</div>
19+
);
20+
}
21+
22+
return (
23+
<AgentexProvider
24+
sgpAppURL={sgpAppURL}
25+
agentexAPIBaseURL={agentexAPIBaseURL}
26+
>
27+
<CustomPageRoot />
28+
</AgentexProvider>
29+
);
30+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
'use client';
2+
3+
import { useCallback, useEffect } from 'react';
4+
5+
import { RotateCcw } from 'lucide-react';
6+
import { useForm } from 'react-hook-form';
7+
8+
import { TagInput } from '@/components/custom/tag-input';
9+
import { Button } from '@/components/ui/button';
10+
import {
11+
Form,
12+
FormControl,
13+
FormField,
14+
FormItem,
15+
FormLabel,
16+
} from '@/components/ui/form';
17+
import {
18+
Select,
19+
SelectContent,
20+
SelectItem,
21+
SelectTrigger,
22+
SelectValue,
23+
} from '@/components/ui/select';
24+
import { Textarea } from '@/components/ui/textarea';
25+
26+
export type GoldenAgentConfig = {
27+
harness: string;
28+
model: string;
29+
system_prompt: string;
30+
allowed_tools: string[];
31+
};
32+
33+
const SUPPORTED_HARNESSES = [
34+
'sandbox-claude',
35+
'claude-code',
36+
'codex',
37+
'agentex',
38+
] as const;
39+
40+
const DEFAULT_MODEL_BY_HARNESS: Record<string, string> = {
41+
'sandbox-claude': 'claude-opus-4-6',
42+
'claude-code': 'claude-opus-4-6',
43+
codex: 'gpt-5.4',
44+
agentex: 'gpt-5.4',
45+
};
46+
47+
export const DEFAULT_CONFIG: GoldenAgentConfig = {
48+
harness: 'sandbox-claude',
49+
model: 'claude-opus-4-6',
50+
system_prompt:
51+
"You are a developer for Scale AI's SGP team. Your primary sources of truth are scaleapi/packages/egp-api-backend and scaleapi/packages/egp-annotation. Use these packages to do the development you need to do. Make sure to read the READMEs and CLAUDE.mds in those directories to get the context you need to address any issues. Your main goal is to take in the information from the prompt, use your sources of truth to come up with a course of action to address it, and then make a PR with the solution.",
52+
allowed_tools: [
53+
'Read',
54+
'Write',
55+
'Edit',
56+
'Glob',
57+
'Grep',
58+
'Bash',
59+
'WebSearch',
60+
'WebFetch',
61+
'List',
62+
],
63+
};
64+
65+
type ConfigPanelProps = {
66+
disabled: boolean;
67+
onConfigChange: (config: GoldenAgentConfig) => void;
68+
onReset: () => void;
69+
};
70+
71+
export function ConfigPanel({
72+
disabled,
73+
onConfigChange,
74+
onReset,
75+
}: ConfigPanelProps) {
76+
const form = useForm<GoldenAgentConfig>({
77+
defaultValues: DEFAULT_CONFIG,
78+
});
79+
80+
const harness = form.watch('harness');
81+
82+
useEffect(() => {
83+
const subscription = form.watch(values => {
84+
onConfigChange(values as GoldenAgentConfig);
85+
});
86+
return () => subscription.unsubscribe();
87+
}, [form, onConfigChange]);
88+
89+
const handleHarnessChange = useCallback(
90+
(value: string) => {
91+
form.setValue('harness', value);
92+
form.setValue('model', DEFAULT_MODEL_BY_HARNESS[value] ?? '');
93+
},
94+
[form]
95+
);
96+
97+
const handleReset = useCallback(() => {
98+
form.reset(DEFAULT_CONFIG);
99+
onReset();
100+
}, [form, onReset]);
101+
102+
return (
103+
<div className="flex h-full flex-col">
104+
<div className="border-b px-4 py-3">
105+
<div className="flex items-center justify-between">
106+
<h2 className="text-sm font-semibold">Configuration</h2>
107+
{disabled && (
108+
<Button
109+
variant="ghost"
110+
size="sm"
111+
onClick={handleReset}
112+
className="gap-1.5 text-xs"
113+
>
114+
<RotateCcw className="size-3" />
115+
New Chat
116+
</Button>
117+
)}
118+
</div>
119+
<p className="text-muted-foreground mt-0.5 text-xs">golden-agent</p>
120+
</div>
121+
122+
<Form {...form}>
123+
<form className="flex-1 space-y-4 overflow-y-auto p-4">
124+
<FormField
125+
control={form.control}
126+
name="harness"
127+
render={({ field }) => (
128+
<FormItem>
129+
<FormLabel className="text-xs">Harness</FormLabel>
130+
<Select
131+
value={field.value}
132+
onValueChange={handleHarnessChange}
133+
disabled={disabled}
134+
>
135+
<FormControl>
136+
<SelectTrigger className="w-full">
137+
<SelectValue />
138+
</SelectTrigger>
139+
</FormControl>
140+
<SelectContent>
141+
{SUPPORTED_HARNESSES.map(h => (
142+
<SelectItem key={h} value={h}>
143+
{h}
144+
</SelectItem>
145+
))}
146+
</SelectContent>
147+
</Select>
148+
</FormItem>
149+
)}
150+
/>
151+
152+
<FormField
153+
control={form.control}
154+
name="model"
155+
render={({ field }) => (
156+
<FormItem>
157+
<FormLabel className="text-xs">Model</FormLabel>
158+
<FormControl>
159+
<input
160+
type="text"
161+
value={field.value}
162+
onChange={field.onChange}
163+
disabled={disabled}
164+
placeholder={
165+
DEFAULT_MODEL_BY_HARNESS[harness] ?? 'Enter model name'
166+
}
167+
className="border-input placeholder:text-muted-foreground flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:ring-[1px] focus-visible:ring-[#756BA2] disabled:cursor-not-allowed disabled:opacity-50"
168+
/>
169+
</FormControl>
170+
</FormItem>
171+
)}
172+
/>
173+
174+
<FormField
175+
control={form.control}
176+
name="system_prompt"
177+
render={({ field }) => (
178+
<FormItem>
179+
<FormLabel className="text-xs">System Prompt</FormLabel>
180+
<FormControl>
181+
<Textarea
182+
value={field.value}
183+
onChange={field.onChange}
184+
disabled={disabled}
185+
placeholder="Enter system instructions..."
186+
className="min-h-32 resize-y"
187+
/>
188+
</FormControl>
189+
</FormItem>
190+
)}
191+
/>
192+
193+
<FormField
194+
control={form.control}
195+
name="allowed_tools"
196+
render={({ field }) => (
197+
<FormItem>
198+
<FormLabel className="text-xs">Allowed Tools</FormLabel>
199+
<FormControl>
200+
<TagInput
201+
value={field.value}
202+
onChange={field.onChange}
203+
disabled={disabled}
204+
placeholder="Add tool name and press Enter"
205+
/>
206+
</FormControl>
207+
</FormItem>
208+
)}
209+
/>
210+
</form>
211+
</Form>
212+
</div>
213+
);
214+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use client';
2+
3+
import { useRef } from 'react';
4+
5+
import { motion } from 'framer-motion';
6+
7+
import { useAgentexClient } from '@/components/providers';
8+
import { TaskMessages } from '@/components/task-messages/task-messages';
9+
import { CopyButton } from '@/components/ui/copy-button';
10+
import { useTaskSubscription } from '@/hooks/use-task-subscription';
11+
12+
const GOLDEN_AGENT_NAME = 'golden-agent';
13+
14+
type CustomChatPanelProps = {
15+
taskId: string | null;
16+
};
17+
18+
export function CustomChatPanel({ taskId }: CustomChatPanelProps) {
19+
const { agentexClient } = useAgentexClient();
20+
const headerRef = useRef<HTMLDivElement>(null);
21+
const scrollContainerRef = useRef<HTMLDivElement>(null);
22+
23+
useTaskSubscription({
24+
agentexClient,
25+
taskId: taskId ?? '',
26+
agentName: GOLDEN_AGENT_NAME,
27+
enabled: !!taskId,
28+
});
29+
30+
if (!taskId) {
31+
return (
32+
<div className="flex flex-1 items-center justify-center">
33+
<p className="text-muted-foreground text-sm">
34+
Configure the agent and send a message to start.
35+
</p>
36+
</div>
37+
);
38+
}
39+
40+
return (
41+
<motion.div
42+
key={taskId}
43+
ref={scrollContainerRef}
44+
className="relative flex-1 flex-col overflow-x-hidden overflow-y-auto"
45+
initial={{ opacity: 0 }}
46+
animate={{ opacity: 1 }}
47+
transition={{ duration: 0.25, ease: 'easeInOut' }}
48+
>
49+
<div
50+
ref={headerRef}
51+
className="bg-background/80 sticky top-0 z-10 flex items-center gap-2 border-b px-4 py-2 backdrop-blur-sm"
52+
>
53+
<span className="text-muted-foreground text-xs font-medium">
54+
golden-agent
55+
</span>
56+
<span className="text-muted-foreground text-xs">|</span>
57+
<span className="text-muted-foreground font-mono text-xs">
58+
{taskId.slice(0, 8)}...
59+
</span>
60+
<CopyButton content={taskId} tooltip="Copy task ID" />
61+
</div>
62+
63+
<div className="flex w-full flex-col items-center px-4 sm:px-6 md:px-8">
64+
<div className="w-full max-w-3xl">
65+
<TaskMessages
66+
taskId={taskId}
67+
headerRef={headerRef}
68+
scrollContainerRef={scrollContainerRef}
69+
agentNameOverride={GOLDEN_AGENT_NAME}
70+
/>
71+
</div>
72+
</div>
73+
</motion.div>
74+
);
75+
}

0 commit comments

Comments
 (0)