Skip to content

Commit 80fe68b

Browse files
committed
feat(web): edit project master-instructions + default model (#12)
Add a project-settings dialog (gear in the chat header) wired to the existing PATCH /v1/projects/:id via updateProject — lets users edit masterInstructions and defaultModel post-creation (backend already supported both; no UI existed).
1 parent 01339c3 commit 80fe68b

1 file changed

Lines changed: 106 additions & 2 deletions

File tree

apps/web/src/components/chat/chat-panel.tsx

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import {
77
ErrorResponseSchema,
88
type ProjectSummary,
99
} from "@cheatcode/types";
10+
import { ModalShell } from "@cheatcode/ui";
1011
import { useAuth } from "@clerk/nextjs";
1112
import { useMutation, useQueryClient } from "@tanstack/react-query";
1213
import { DefaultChatTransport } from "ai";
13-
import { useCallback, useDeferredValue, useEffect, useMemo, useRef } from "react";
14+
import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
1415
import { toast } from "sonner";
1516
import { MessageList } from "@/components/chat/message-list";
1617
import { PromptComposer } from "@/components/chat/prompt-composer";
1718
import { StreamReconnectBanner } from "@/components/chat/stream-reconnect-banner";
18-
import { Monitor } from "@/components/ui/icons";
19+
import { Monitor, SlidersHorizontal } from "@/components/ui/icons";
1920
import { agentModelRequestValue } from "@/lib/agent-models";
2021
import { cancelRun, getThread, updateProject } from "@/lib/api/project-thread";
2122
import { useAppStore } from "@/lib/store/app-store";
@@ -284,6 +285,7 @@ function ChatContextRow({
284285
}) {
285286
const projectName = project?.name?.trim() || "new project";
286287
const titleText = title?.trim() || "New task";
288+
const [settingsOpen, setSettingsOpen] = useState(false);
287289

288290
return (
289291
<header className="flex h-[54px] shrink-0 items-center gap-3 px-4 pt-3 text-[#1b1b1b]">
@@ -292,11 +294,113 @@ function ChatContextRow({
292294
<span className="truncate">{projectName}</span>
293295
</span>
294296
<h1 className="min-w-0 flex-1 truncate font-semibold text-[15px]">{titleText}</h1>
297+
{project ? (
298+
<button
299+
aria-label="Project settings"
300+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[#8a8a8a] transition-colors hover:bg-[#f7f7f7] hover:text-[#1b1b1b]"
301+
onClick={() => setSettingsOpen(true)}
302+
type="button"
303+
>
304+
<SlidersHorizontal aria-hidden="true" className="h-4 w-4" />
305+
</button>
306+
) : null}
295307
<span className="shrink-0 text-[#8a8a8a] text-[13px]">$0.00</span>
308+
{project ? (
309+
<ProjectSettingsDialog
310+
onClose={() => setSettingsOpen(false)}
311+
open={settingsOpen}
312+
project={project}
313+
/>
314+
) : null}
296315
</header>
297316
);
298317
}
299318

319+
function ProjectSettingsDialog({
320+
onClose,
321+
open,
322+
project,
323+
}: {
324+
onClose: () => void;
325+
open: boolean;
326+
project: ProjectSummary;
327+
}) {
328+
const { getToken } = useAuth();
329+
const queryClient = useQueryClient();
330+
const [instructions, setInstructions] = useState(project.masterInstructions ?? "");
331+
const [model, setModel] = useState(project.defaultModel ?? "");
332+
333+
const mutation = useMutation({
334+
mutationFn: () =>
335+
updateProject(getToken, project.id, {
336+
defaultModel: model.trim() || null,
337+
masterInstructions: instructions.trim() || null,
338+
}),
339+
onError: (error) =>
340+
toast.error(error instanceof Error ? error.message : "Could not save project settings"),
341+
onSuccess: () => {
342+
toast.success("Project settings saved");
343+
void queryClient.invalidateQueries({ queryKey: ["projects", project.id] });
344+
onClose();
345+
},
346+
});
347+
348+
return (
349+
<ModalShell
350+
ariaLabel="Project settings"
351+
className="m-auto w-full max-w-lg"
352+
onClose={onClose}
353+
open={open}
354+
>
355+
<form
356+
className="flex flex-col gap-4 p-5 text-[#1b1b1b]"
357+
onSubmit={(event) => {
358+
event.preventDefault();
359+
if (!mutation.isPending) {
360+
mutation.mutate();
361+
}
362+
}}
363+
>
364+
<h2 className="font-semibold text-[18px]">Project settings</h2>
365+
<label className="flex flex-col gap-1.5">
366+
<span className="font-medium text-[#5f5f5f] text-[12px]">Master instructions</span>
367+
<textarea
368+
className="min-h-[120px] w-full resize-y rounded-lg border border-[#ececec] bg-white px-3 py-2 text-[#1b1b1b] text-[14px] outline-none focus:border-[#1b1b1b]/40"
369+
onChange={(event) => setInstructions(event.target.value)}
370+
placeholder="Persistent guidance applied to every run in this project."
371+
value={instructions}
372+
/>
373+
</label>
374+
<label className="flex flex-col gap-1.5">
375+
<span className="font-medium text-[#5f5f5f] text-[12px]">Default model</span>
376+
<input
377+
className="h-9 w-full rounded-lg border border-[#ececec] bg-white px-3 text-[#1b1b1b] text-[14px] outline-none focus:border-[#1b1b1b]/40"
378+
onChange={(event) => setModel(event.target.value)}
379+
placeholder="e.g. claude-sonnet-4-6 (leave blank for Auto)"
380+
value={model}
381+
/>
382+
</label>
383+
<div className="flex justify-end gap-2">
384+
<button
385+
className="rounded-full border border-[#ececec] px-4 py-1.5 font-medium text-[13px] hover:bg-[#f7f7f7]"
386+
onClick={onClose}
387+
type="button"
388+
>
389+
Cancel
390+
</button>
391+
<button
392+
className="rounded-full bg-[#1b1b1b] px-4 py-1.5 font-medium text-[13px] text-white hover:bg-black disabled:opacity-50"
393+
disabled={mutation.isPending}
394+
type="submit"
395+
>
396+
{mutation.isPending ? "Saving…" : "Save"}
397+
</button>
398+
</div>
399+
</form>
400+
</ModalShell>
401+
);
402+
}
403+
300404
function effectiveBudgetCap(
301405
threadBudgetCapUsd: null | number | undefined,
302406
project: ProjectSummary | null,

0 commit comments

Comments
 (0)