diff --git a/app/src/features/crews/CrewColorField.tsx b/app/src/features/crews/CrewColorField.tsx new file mode 100644 index 00000000..93b5ee70 --- /dev/null +++ b/app/src/features/crews/CrewColorField.tsx @@ -0,0 +1,46 @@ +import { Check } from "lucide-react"; +import { useT } from "../../i18n"; +import { AVATAR_PALETTE } from "../../lib/protocol.gen"; + +/** Swatches for a crew's color, and a way to have none. */ +export function CrewColorField({ + value, + onChange, +}: { + value: string | null; + onChange(color: string | null): void; +}) { + const words = useT().crews.dialog; + const same = (a: string | null, b: string | null) => a?.toLowerCase() === b?.toLowerCase(); + return ( +
+ {words.color} +
+ + {AVATAR_PALETTE.map((swatch) => { + const on = same(value, swatch); + return ( + + ); + })} +
+
+ ); +} diff --git a/app/src/features/crews/CrewDialog.tsx b/app/src/features/crews/CrewDialog.tsx index a6436cfa..bed8205a 100644 --- a/app/src/features/crews/CrewDialog.tsx +++ b/app/src/features/crews/CrewDialog.tsx @@ -9,21 +9,37 @@ import { SelectField, TextArea, TextField } from "../../ui/Field"; import { notifyError } from "../../ui/toast"; import { useCatalog } from "../catalog/useCatalog"; import { addTemplateBots } from "./addTemplateBots"; +import { CrewColorField } from "./CrewColorField"; import type { CrewTemplate } from "./crewTemplates"; import { PickedTemplate, TemplatePicker } from "./TemplatePicker"; import { WorkFolderField } from "./WorkFolderField"; const MODELS: BotModel[] = ["default", "fable", "opus", "sonnet", "haiku"]; -/** Creates a crew with its chief, or renames `crew`. */ -export function CrewDialog({ crew, onClose }: { crew?: Crew; onClose(): void }) { +/** + * Creates a crew with its chief, or renames `crew`. `start` opens a new crew + * already on that template, as when the owner taps an idea on the welcome + * screen. + */ +export function CrewDialog({ + crew, + start, + onClose, +}: { + crew?: Crew; + start?: CrewTemplate | undefined; + onClose(): void; +}) { const t = useT(); const api = useApi(); const putCrew = useApp((state) => state.putCrew); const selectCrew = useApp((state) => state.selectCrew); - const [name, setName] = useState(crew?.name ?? ""); + const [name, setName] = useState( + crew?.name ?? (start ? t.crewTemplates.items[start.id].name : ""), + ); + const [color, setColor] = useState(crew?.color ?? null); const [folder, setFolder] = useState(null); - const [goal, setGoal] = useState(""); + const [goal, setGoal] = useState(start ? t.crewTemplates.items[start.id].goal : ""); const [chiefModel, setChiefModel] = useState("default"); const selectBot = useApp((state) => state.selectBot); const putBot = useApp((state) => state.putBot); @@ -32,7 +48,7 @@ export function CrewDialog({ crew, onClose }: { crew?: Crew; onClose(): void }) // A new crew starts from a template (spec 29.1) when the catalog loaded: // `undefined` is the list of templates, `null` an empty crew. const { roles, failed: catalogFailed } = useCatalog(!crew); - const [template, setTemplate] = useState(undefined); + const [template, setTemplate] = useState(start); const loading = !crew && roles === null && !catalogFailed; const choosing = !crew && template === undefined && roles !== null; @@ -49,7 +65,7 @@ export function CrewDialog({ crew, onClose }: { crew?: Crew; onClose(): void }) setError(null); try { const d = t.crews.dialog; - const saved = crew + let saved = crew ? await api.call("crews.rename", { crewId: crew.id, name }) : await api.call("crews.create", { name, @@ -62,6 +78,9 @@ export function CrewDialog({ crew, onClose }: { crew?: Crew; onClose(): void }) ...(chiefModel !== "default" && { model: chiefModel }), }, }); + if (crew && color !== crew.color) { + saved = await api.call("crews.setColor", { crewId: crew.id, color }); + } putCrew(saved); if (!crew) { selectCrew(saved.id); @@ -134,6 +153,7 @@ export function CrewDialog({ crew, onClose }: { crew?: Crew; onClose(): void }) autoFocus required /> + {crew && } {!crew && ( <>