Skip to content

Commit 65fe8ac

Browse files
committed
Some design tweaks, improvements to playground options, rename unnamed preloaded conversations on first message
1 parent 3174647 commit 65fe8ac

File tree

6 files changed

+319
-64
lines changed

6 files changed

+319
-64
lines changed

apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
ClipboardDocumentIcon,
66
CodeBracketSquareIcon,
77
} from "@heroicons/react/20/solid";
8-
import { Suspense, useState } from "react";
8+
import { Suspense, useEffect, useState } from "react";
99
import { CodeBlock } from "~/components/code/CodeBlock";
1010
import { StreamdownRenderer } from "~/components/code/StreamdownRenderer";
1111
import { Button, LinkButton } from "~/components/primitives/Buttons";
@@ -260,8 +260,14 @@ export function ToolUseRow({ tool }: { tool: ToolUse }) {
260260
...(hasDetails ? (["details"] as const) : []),
261261
];
262262

263-
const defaultTab: ToolTab | null = hasInput ? "input" : null;
264-
const [activeTab, setActiveTab] = useState<ToolTab | null>(defaultTab);
263+
const [activeTab, setActiveTab] = useState<ToolTab | null>(hasInput ? "input" : null);
264+
265+
// Auto-select input tab when input arrives after initial render (e.g. streaming tool calls)
266+
useEffect(() => {
267+
if (hasInput && activeTab === null) {
268+
setActiveTab("input");
269+
}
270+
}, [hasInput]);
265271

266272
function handleTabClick(tab: ToolTab) {
267273
setActiveTab(activeTab === tab ? null : tab);

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.$agentParam/route.tsx

Lines changed: 211 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ import { MainCenteredContainer } from "~/components/layout/AppLayout";
1818
import { Badge } from "~/components/primitives/Badge";
1919
import { Button, LinkButton } from "~/components/primitives/Buttons";
2020
import { CopyButton } from "~/components/primitives/CopyButton";
21+
import { DurationPicker } from "~/components/primitives/DurationPicker";
2122
import { Header3 } from "~/components/primitives/Headers";
23+
import { Hint } from "~/components/primitives/Hint";
24+
import { Input } from "~/components/primitives/Input";
25+
import { InputGroup } from "~/components/primitives/InputGroup";
26+
import { Label } from "~/components/primitives/Label";
2227
import { Paragraph } from "~/components/primitives/Paragraph";
2328
import { Spinner } from "~/components/primitives/Spinner";
2429
import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover";
@@ -46,10 +51,11 @@ import { findProjectBySlug } from "~/models/project.server";
4651
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
4752
import { playgroundPresenter } from "~/presenters/v3/PlaygroundPresenter.server";
4853
import { requireUserId } from "~/services/session.server";
54+
import { RunTagInput } from "~/components/runs/v3/RunTagInput";
4955
import { Select, SelectItem } from "~/components/primitives/Select";
5056
import { EnvironmentParamSchema, v3PlaygroundAgentPath } from "~/utils/pathBuilder";
5157
import { env as serverEnv } from "~/env.server";
52-
import { generateJWT as internal_generateJWT } from "@trigger.dev/core/v3";
58+
import { generateJWT as internal_generateJWT, MachinePresetName } from "@trigger.dev/core/v3";
5359
import { extractJwtSigningSecretKey } from "~/services/realtime/jwtAuth.server";
5460
import { SchemaTabContent } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/SchemaTabContent";
5561
import { AIPayloadTabContent } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent";
@@ -167,9 +173,23 @@ function PlaygroundChat() {
167173
const { agent, apiOrigin, recentConversations, activeConversation } =
168174
useTypedLoaderData<typeof loader>();
169175
const parentData = useRouteLoaderData(PARENT_ROUTE_ID) as
170-
| { agents: Array<{ slug: string }> }
176+
| {
177+
agents: Array<{ slug: string }>;
178+
versions: string[];
179+
regions: Array<{
180+
id: string;
181+
name: string;
182+
description?: string;
183+
isDefault: boolean;
184+
}>;
185+
isDev: boolean;
186+
}
171187
| undefined;
172188
const agents = parentData?.agents ?? [];
189+
const versions = parentData?.versions ?? [];
190+
const regions = parentData?.regions ?? [];
191+
const isDev = parentData?.isDev ?? false;
192+
const defaultRegion = regions.find((r) => r.isDefault);
173193
const navigate = useNavigate();
174194
const organization = useOrganization();
175195
const project = useProject();
@@ -187,7 +207,13 @@ function PlaygroundChat() {
187207
const clientDataJsonRef = useRef(clientDataJson);
188208
clientDataJsonRef.current = clientDataJson;
189209
const [machine, setMachine] = useState<string | undefined>(undefined);
190-
const [tags, setTags] = useState<string>("");
210+
const [tags, setTags] = useState<string[]>([]);
211+
const [maxAttempts, setMaxAttempts] = useState<number | undefined>(undefined);
212+
const [maxDuration, setMaxDuration] = useState<number | undefined>(undefined);
213+
const [version, setVersion] = useState<string | undefined>(undefined);
214+
const [region, setRegion] = useState<string | undefined>(() =>
215+
isDev ? undefined : defaultRegion?.name
216+
);
191217

192218
const actionPath = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/playground/action`;
193219

@@ -200,8 +226,12 @@ function PlaygroundChat() {
200226
formData.set("chatId", chatId);
201227
formData.set("payload", JSON.stringify(params.payload));
202228
formData.set("clientData", clientDataJsonRef.current);
203-
if (tags.trim()) formData.set("tags", tags.trim());
229+
if (tags.length > 0) formData.set("tags", tags.join(","));
204230
if (machine) formData.set("machine", machine);
231+
if (maxAttempts) formData.set("maxAttempts", String(maxAttempts));
232+
if (maxDuration) formData.set("maxDuration", String(maxDuration));
233+
if (version) formData.set("version", version);
234+
if (region) formData.set("region", region);
205235

206236
const response = await fetch(actionPath, { method: "POST", body: formData });
207237
const data = (await response.json()) as {
@@ -221,7 +251,7 @@ function PlaygroundChat() {
221251

222252
return { runId: data.runId, publicAccessToken: data.publicAccessToken };
223253
},
224-
[actionPath, agent.slug, chatId, tags, machine]
254+
[actionPath, agent.slug, chatId, tags, machine, maxAttempts, maxDuration, version, region]
225255
);
226256

227257
// Token renewal via Remix action
@@ -465,7 +495,7 @@ function PlaygroundChat() {
465495
</div>
466496

467497
{/* Messages */}
468-
<div className="flex-1 overflow-y-auto p-4">
498+
<div className="flex-1 overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
469499
{messages.length === 0 ? (
470500
<MainCenteredContainer>
471501
<div className="flex flex-col items-center gap-3 py-16">
@@ -500,7 +530,7 @@ function PlaygroundChat() {
500530
</div>
501531
</MainCenteredContainer>
502532
) : (
503-
<div className="mx-auto max-w-3xl space-y-4">
533+
<div className="mx-auto w-full max-w-4xl space-y-4">
504534
{messages.map((msg) => (
505535
<MessageBubble key={msg.id} message={msg} />
506536
))}
@@ -584,7 +614,7 @@ function PlaygroundChat() {
584614
</div>
585615
</ResizablePanel>
586616
<ResizableHandle id="playground-sidebar-handle" />
587-
<ResizablePanel id="playground-sidebar" default="320px" min="250px" max="500px">
617+
<ResizablePanel id="playground-sidebar" default="420px" min="360px" max="720px">
588618
<PlaygroundSidebar
589619
clientDataJson={clientDataJson}
590620
onClientDataChange={setClientDataJson}
@@ -595,6 +625,17 @@ function PlaygroundChat() {
595625
onMachineChange={setMachine}
596626
tags={tags}
597627
onTagsChange={setTags}
628+
maxAttempts={maxAttempts}
629+
onMaxAttemptsChange={setMaxAttempts}
630+
maxDuration={maxDuration}
631+
onMaxDurationChange={setMaxDuration}
632+
version={version}
633+
onVersionChange={setVersion}
634+
versions={versions}
635+
region={region}
636+
onRegionChange={setRegion}
637+
regions={regions}
638+
isDev={isDev}
598639
session={session}
599640
messageCount={messages.length}
600641
isStreaming={isStreaming}
@@ -811,15 +852,7 @@ function DataPartPopover({ name, data }: { name: string; data: unknown }) {
811852
// Sidebar
812853
// ---------------------------------------------------------------------------
813854

814-
const machinePresets = [
815-
"micro",
816-
"small-1x",
817-
"small-2x",
818-
"medium-1x",
819-
"medium-2x",
820-
"large-1x",
821-
"large-2x",
822-
];
855+
const machinePresets = Object.values(MachinePresetName.enum);
823856

824857
function PlaygroundSidebar({
825858
clientDataJson,
@@ -831,6 +864,17 @@ function PlaygroundSidebar({
831864
onMachineChange,
832865
tags,
833866
onTagsChange,
867+
maxAttempts,
868+
onMaxAttemptsChange,
869+
maxDuration,
870+
onMaxDurationChange,
871+
version,
872+
onVersionChange,
873+
versions,
874+
region,
875+
onRegionChange,
876+
regions,
877+
isDev,
834878
session,
835879
messageCount,
836880
isStreaming,
@@ -843,13 +887,28 @@ function PlaygroundSidebar({
843887
agentSlug: string;
844888
machine: string | undefined;
845889
onMachineChange: (val: string | undefined) => void;
846-
tags: string;
847-
onTagsChange: (val: string) => void;
890+
tags: string[];
891+
onTagsChange: (val: string[]) => void;
892+
maxAttempts: number | undefined;
893+
onMaxAttemptsChange: (val: number | undefined) => void;
894+
maxDuration: number | undefined;
895+
onMaxDurationChange: (val: number | undefined) => void;
896+
version: string | undefined;
897+
onVersionChange: (val: string | undefined) => void;
898+
versions: string[];
899+
region: string | undefined;
900+
onRegionChange: (val: string | undefined) => void;
901+
regions: Array<{ id: string; name: string; description?: string; isDefault: boolean }>;
902+
isDev: boolean;
848903
session: { runId: string; publicAccessToken: string; lastEventId?: string } | undefined;
849904
messageCount: number;
850905
isStreaming: boolean;
851906
status: string;
852907
}) {
908+
const regionItems = regions.map((r) => ({
909+
value: r.name,
910+
label: r.description ? `${r.name}${r.description}` : r.name,
911+
}));
853912
return (
854913
<div className="flex h-full flex-col border-l border-grid-bright">
855914
<ClientTabs
@@ -938,36 +997,142 @@ function PlaygroundSidebar({
938997
value="options"
939998
className="min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
940999
>
941-
<div className="min-w-64 space-y-4 p-3">
942-
<div>
943-
<label className="mb-1 block text-xs font-medium text-text-dimmed">Machine</label>
944-
<select
1000+
<div className="space-y-4 p-3">
1001+
<InputGroup fullWidth>
1002+
<Label variant="small" required={false}>
1003+
Machine
1004+
</Label>
1005+
<Select
9451006
value={machine ?? ""}
946-
onChange={(e) => onMachineChange(e.target.value || undefined)}
947-
className="w-full rounded border border-charcoal-650 bg-charcoal-850 px-2.5 py-1.5 text-xs text-text-bright focus:border-indigo-500 focus:outline-none"
1007+
setValue={(val) =>
1008+
onMachineChange(val && typeof val === "string" ? val : undefined)
1009+
}
1010+
placeholder="Default"
1011+
variant="tertiary/small"
1012+
items={machinePresets}
1013+
filter={(item, search) => item.toLowerCase().includes(search.toLowerCase())}
9481014
>
949-
<option value="">Default</option>
950-
{machinePresets.map((preset) => (
951-
<option key={preset} value={preset}>
952-
{preset}
953-
</option>
954-
))}
955-
</select>
956-
<p className="mt-1 text-[10px] text-text-dimmed">Machine preset for the agent run.</p>
957-
</div>
958-
<div>
959-
<label className="mb-1 block text-xs font-medium text-text-dimmed">Tags</label>
960-
<input
961-
type="text"
962-
value={tags}
963-
onChange={(e) => onTagsChange(e.target.value)}
964-
placeholder="tag1, tag2"
965-
className="w-full rounded border border-charcoal-650 bg-charcoal-850 px-2.5 py-1.5 text-xs text-text-bright placeholder-text-dimmed focus:border-indigo-500 focus:outline-none"
1015+
{(matches) =>
1016+
matches.map((preset) => (
1017+
<SelectItem key={preset} value={preset}>
1018+
{preset}
1019+
</SelectItem>
1020+
))
1021+
}
1022+
</Select>
1023+
<Hint>Overrides the machine preset.</Hint>
1024+
</InputGroup>
1025+
1026+
<InputGroup fullWidth>
1027+
<Label variant="small" required={false}>
1028+
Tags
1029+
</Label>
1030+
<RunTagInput
1031+
tags={tags}
1032+
onTagsChange={onTagsChange}
1033+
variant="small"
1034+
maxTags={3}
1035+
placeholder="Add tag..."
9661036
/>
967-
<p className="mt-1 text-[10px] text-text-dimmed">
968-
Comma-separated tags (max 5 total).
969-
</p>
970-
</div>
1037+
<Hint>Add tags to easily filter runs. 3 max (2 added automatically).</Hint>
1038+
</InputGroup>
1039+
1040+
<InputGroup fullWidth>
1041+
<Label variant="small" required={false}>
1042+
Max attempts
1043+
</Label>
1044+
<Input
1045+
type="number"
1046+
variant="small"
1047+
min={1}
1048+
placeholder="Default"
1049+
value={maxAttempts ?? ""}
1050+
onChange={(e) => {
1051+
const val = e.target.value;
1052+
onMaxAttemptsChange(val ? parseInt(val, 10) : undefined);
1053+
}}
1054+
/>
1055+
<Hint>Retries failed runs up to the specified number of attempts.</Hint>
1056+
</InputGroup>
1057+
1058+
<InputGroup fullWidth>
1059+
<Label variant="small" required={false}>
1060+
Max duration
1061+
</Label>
1062+
<DurationPicker
1063+
value={maxDuration}
1064+
onChange={onMaxDurationChange}
1065+
variant="small"
1066+
/>
1067+
<Hint>Overrides the maximum compute time limit for the run.</Hint>
1068+
</InputGroup>
1069+
1070+
{versions.length > 0 && (
1071+
<InputGroup fullWidth>
1072+
<Label variant="small" required={false}>
1073+
Version
1074+
</Label>
1075+
<Select
1076+
value={version ?? ""}
1077+
setValue={(val) =>
1078+
onVersionChange(val && typeof val === "string" ? val : undefined)
1079+
}
1080+
placeholder="Latest"
1081+
variant="tertiary/small"
1082+
disabled={isDev}
1083+
items={versions}
1084+
filter={(item, search) => item.toLowerCase().includes(search.toLowerCase())}
1085+
>
1086+
{(matches) =>
1087+
matches.map((v, i) => (
1088+
<SelectItem key={v} value={v}>
1089+
{i === 0 ? `${v} (latest)` : v}
1090+
</SelectItem>
1091+
))
1092+
}
1093+
</Select>
1094+
<Hint>
1095+
{isDev
1096+
? "Version is determined by the running dev server."
1097+
: "Lock the run to a specific deployed version."}
1098+
</Hint>
1099+
</InputGroup>
1100+
)}
1101+
1102+
{regionItems.length > 1 && (
1103+
<InputGroup fullWidth>
1104+
<Label variant="small" required={false}>
1105+
Region
1106+
</Label>
1107+
<Select
1108+
value={region ?? ""}
1109+
setValue={(val) =>
1110+
onRegionChange(val && typeof val === "string" ? val : undefined)
1111+
}
1112+
text={(val) => val || undefined}
1113+
placeholder={isDev ? "–" : "Default"}
1114+
variant="tertiary/small"
1115+
disabled={isDev}
1116+
items={regionItems}
1117+
filter={(item, search) =>
1118+
item.label.toLowerCase().includes(search.toLowerCase())
1119+
}
1120+
>
1121+
{(matches) =>
1122+
matches.map((r) => (
1123+
<SelectItem key={r.value} value={r.value}>
1124+
{r.label}
1125+
</SelectItem>
1126+
))
1127+
}
1128+
</Select>
1129+
<Hint>
1130+
{isDev
1131+
? "Region is not applicable in development."
1132+
: "Run the agent in a specific region."}
1133+
</Hint>
1134+
</InputGroup>
1135+
)}
9711136
</div>
9721137
</ClientTabsContent>
9731138

0 commit comments

Comments
 (0)