Files
roboco/panel/src/components/company-goals/company-goals-card.tsx
T
Renn F da17c49f2d feat(marketing): HoM feature-spotlight X drafts + brand-voice charter (v0.18.0 B)
The Head of Marketing now markets features, not just releases: a default-off
x_feature_spotlight loop periodically spawns the HoM to investigate what shipped
(CHANGELOG, feature flags, docs/map, KB) and draft ONE held marketing post via
propose_feature_spotlight, reviewed in the X post queue.

- New x_feature source (distinct from x_post, fixing panel mislabeling) + a
  panel Feature-spotlight branch.
- brand_voice column on company_goals (migration 061, single head) as the
  CEO-editable voice source, surfaced in Settings and injected into the HoM
  briefing; a VOICE GUIDE baseline in head-marketing.md.
- propose_feature_spotlight verb (HoM-only), mirroring propose_roadmap.

Gated by x_feature_spotlight_enabled (default off; flag-off dormancy proven).
Also fixed two real bugs found mid-build: company_goals API schemas dropped
brand_voice on GET/PUT; the live charter UI is goals-tab.tsx, not the unmounted
company-goals-card.tsx. Full suite green (2935); migration single-head verified.
2026-07-04 07:34:40 +02:00

184 lines
6.0 KiB
TypeScript

"use client";
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
companyGoalsApi,
type CompanyGoalsUpdate,
} from "@/lib/api/company-goals";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Target, Save } from "lucide-react";
import { toast } from "sonner";
function parseError(e: unknown): string {
return e instanceof Error ? e.message : "parse error";
}
export function CompanyGoalsCard() {
const queryClient = useQueryClient();
// null = "show the server value"; deriving the displayed value avoids syncing
// query state into local state with an effect (react-hooks/set-state-in-effect).
const [northStar, setNorthStar] = useState<string | null>(null);
const [constraints, setConstraints] = useState<string | null>(null);
const [objectives, setObjectives] = useState<string | null>(null);
const [policy, setPolicy] = useState<string | null>(null);
const [brandVoice, setBrandVoice] = useState<string | null>(null);
const { data: goals, isLoading } = useQuery({
queryKey: ["company-goals"],
queryFn: companyGoalsApi.get,
});
const northStarVal = northStar ?? goals?.north_star ?? "";
const constraintsVal = constraints ?? (goals?.constraints ?? []).join("\n");
const objectivesVal =
objectives ?? JSON.stringify(goals?.objectives ?? [], null, 2);
const policyVal =
policy ?? JSON.stringify(goals?.operating_policy ?? {}, null, 2);
const brandVoiceVal = brandVoice ?? goals?.brand_voice ?? "";
const saveMutation = useMutation({
mutationFn: (update: CompanyGoalsUpdate) => companyGoalsApi.update(update),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["company-goals"] });
setNorthStar(null);
setConstraints(null);
setObjectives(null);
setPolicy(null);
setBrandVoice(null);
toast.success("Company charter updated");
},
onError: (error) => {
toast.error(`Failed to save: ${parseError(error)}`);
},
});
const handleSave = () => {
let parsedObjectives: Record<string, unknown>[];
let parsedPolicy: Record<string, unknown>;
try {
parsedObjectives = JSON.parse(objectivesVal);
if (!Array.isArray(parsedObjectives)) {
throw new Error("must be a JSON array");
}
} catch (e) {
toast.error(`Objectives: invalid JSON — ${parseError(e)}`);
return;
}
try {
parsedPolicy = JSON.parse(policyVal);
if (
typeof parsedPolicy !== "object" ||
parsedPolicy === null ||
Array.isArray(parsedPolicy)
) {
throw new Error("must be a JSON object");
}
} catch (e) {
toast.error(`Operating policy: invalid JSON — ${parseError(e)}`);
return;
}
saveMutation.mutate({
north_star: northStarVal,
objectives: parsedObjectives,
constraints: constraintsVal
.split("\n")
.map((c) => c.trim())
.filter(Boolean),
operating_policy: parsedPolicy,
brand_voice: brandVoiceVal,
});
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Target className="h-5 w-5" />
Company Charter
</CardTitle>
<CardDescription>
The CEO-owned north star, objectives, constraints, operating policy,
and brand voice. Injected into every agent&apos;s briefing so all
work stays goal-aware.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="north-star">North star</Label>
<Textarea
id="north-star"
rows={3}
value={northStarVal}
disabled={isLoading}
onChange={(e) => setNorthStar(e.target.value)}
placeholder="The long-term vision in one or two sentences..."
/>
</div>
<div className="space-y-2">
<Label htmlFor="brand-voice">Brand voice</Label>
<Textarea
id="brand-voice"
rows={3}
value={brandVoiceVal}
disabled={isLoading}
onChange={(e) => setBrandVoice(e.target.value)}
placeholder="Sample posts or a style description for the Head of Marketing's drafts..."
/>
</div>
<div className="space-y-2">
<Label htmlFor="constraints">Constraints (one per line)</Label>
<Textarea
id="constraints"
rows={3}
value={constraintsVal}
disabled={isLoading}
onChange={(e) => setConstraints(e.target.value)}
placeholder={"AGPL only\nNo external data egress"}
/>
</div>
<div className="space-y-2">
<Label htmlFor="objectives">Objectives (JSON array)</Label>
<Textarea
id="objectives"
rows={6}
value={objectivesVal}
disabled={isLoading}
onChange={(e) => setObjectives(e.target.value)}
className="font-mono text-xs"
placeholder='[{"metric": "NPS", "target": 50, "status": "active"}]'
/>
</div>
<div className="space-y-2">
<Label htmlFor="policy">Operating policy (JSON object)</Label>
<Textarea
id="policy"
rows={6}
value={policyVal}
disabled={isLoading}
onChange={(e) => setPolicy(e.target.value)}
className="font-mono text-xs"
placeholder='{"autonomy_level": "assisted", "monthly_budget_cap": 500}'
/>
</div>
<Button
onClick={handleSave}
disabled={saveMutation.isPending || isLoading}
>
<Save className="h-4 w-4 mr-2" />
{saveMutation.isPending ? "Saving..." : "Save charter"}
</Button>
</CardContent>
</Card>
);
}