mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(panel): dense tooltip pass — projects, products, social, KB, business, settings, notifications (178 tips)
Density bar: every dialog field, column header, filter, badge, and status pill explains itself — grounded in the backend (ladder normalization, rate-limit park/probe, self-hosted fallback, reindex scope). Disabled buttons span-wrap so tips fire; tabs re-assert data-state.
This commit is contained in:
@@ -17,6 +17,11 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { OfflineState } from "@/components/ui/offline-state";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { usePageRefresh } from "@/hooks";
|
||||
import {
|
||||
Bell,
|
||||
@@ -114,20 +119,26 @@ function NotificationCard({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-medium">{notification.subject}</span>
|
||||
<Badge
|
||||
className={priorityColors[notification.priority] + " text-xs"}
|
||||
>
|
||||
{notification.priority}
|
||||
</Badge>
|
||||
{!notification.is_read && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
New
|
||||
<HelpTip label="How urgently the sender flagged this — doesn't change what you need to do, just how prominently it's shown.">
|
||||
<Badge
|
||||
className={priorityColors[notification.priority] + " text-xs"}
|
||||
>
|
||||
{notification.priority}
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
{!notification.is_read && (
|
||||
<HelpTip label="Hasn't been marked read yet.">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
New
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
{notification.requires_ack && !notification.is_acknowledged && (
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
Needs Ack
|
||||
</Badge>
|
||||
<HelpTip label="A formal signal from a PM/Board role — needs an explicit acknowledgement, tracked separately from read status.">
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
Needs Ack
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
{notification.related_task_id && (
|
||||
<Link
|
||||
@@ -329,16 +340,51 @@ function NotificationsPageContent() {
|
||||
) : (
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">All</TabsTrigger>
|
||||
<TabsTrigger value="unread">
|
||||
Unread {data && data.unread_count > 0 && `(${data.unread_count})`}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pending">
|
||||
Pending{" "}
|
||||
{data &&
|
||||
data.pending_ack_count > 0 &&
|
||||
`(${data.pending_ack_count})`}
|
||||
</TabsTrigger>
|
||||
{/* TooltipTrigger's asChild Slot merge injects its own data-state
|
||||
onto the trigger, colliding with Radix Tabs' own literal
|
||||
data-state (same trap as task-tabs.tsx) — reassert the real
|
||||
selection state explicitly so the active-tab highlight
|
||||
survives the merge. */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<TabsTrigger
|
||||
value="all"
|
||||
data-state={activeTab === "all" ? "active" : "inactive"}
|
||||
>
|
||||
All
|
||||
</TabsTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Every notification you've received, read or not
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<TabsTrigger
|
||||
value="unread"
|
||||
data-state={activeTab === "unread" ? "active" : "inactive"}
|
||||
>
|
||||
Unread {data && data.unread_count > 0 && `(${data.unread_count})`}
|
||||
</TabsTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Notifications you haven't opened yet</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<TabsTrigger
|
||||
value="pending"
|
||||
data-state={activeTab === "pending" ? "active" : "inactive"}
|
||||
>
|
||||
Pending{" "}
|
||||
{data &&
|
||||
data.pending_ack_count > 0 &&
|
||||
`(${data.pending_ack_count})`}
|
||||
</TabsTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Notifications still waiting on your acknowledgement
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={activeTab} className="mt-4 space-y-3">
|
||||
|
||||
@@ -121,7 +121,9 @@ export default function SettingsPage() {
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>Collapsed Sidebar</Label>
|
||||
<HelpTip label="Saved to this browser only — doesn't sync across devices.">
|
||||
<Label>Collapsed Sidebar</Label>
|
||||
</HelpTip>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Show icons only in the sidebar
|
||||
</p>
|
||||
|
||||
@@ -96,7 +96,9 @@ interface DeliverySectionProps {
|
||||
function DeliverySection({ delivery }: DeliverySectionProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<SectionLabel>Delivery</SectionLabel>
|
||||
<HelpTip label="Live task counts across the pipeline — only 'Done (30 d)' looks backward">
|
||||
<SectionLabel>Delivery</SectionLabel>
|
||||
</HelpTip>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<DeliveryMetric
|
||||
label="In flight"
|
||||
@@ -147,14 +149,18 @@ function SpendSection({ spend }: SpendSectionProps) {
|
||||
<SectionLabel>Spend</SectionLabel>
|
||||
<div className="rounded-lg border p-3 space-y-1.5">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">30-day spend</span>
|
||||
<HelpTip label="Total agent-session cost over the trailing 30 days (Anthropic priced; local/Ollama models cost $0)">
|
||||
<span className="text-muted-foreground">30-day spend</span>
|
||||
</HelpTip>
|
||||
<span className="font-medium tabular-nums">
|
||||
${spend_30d_usd.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
{projected_monthly_usd !== null && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Projected monthly</span>
|
||||
<HelpTip label="The 30-day spend extrapolated to a full month">
|
||||
<span className="text-muted-foreground">Projected monthly</span>
|
||||
</HelpTip>
|
||||
<span className="font-medium tabular-nums">
|
||||
${projected_monthly_usd.toFixed(2)}
|
||||
</span>
|
||||
|
||||
@@ -101,16 +101,20 @@ function ObjectivesEditor({
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
Objective #{rowIdx + 1}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() => removeRow(rowIdx)}
|
||||
className="h-6 px-2 text-xs text-destructive hover:text-destructive"
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<HelpTip label="Deletes this row locally — nothing changes until you click Save charter">
|
||||
<span className="inline-block">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() => removeRow(rowIdx)}
|
||||
className="h-6 px-2 text-xs text-destructive hover:text-destructive"
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
{keys.map((key) => (
|
||||
<div key={key} className="space-y-1">
|
||||
@@ -132,15 +136,19 @@ function ObjectivesEditor({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={addRow}
|
||||
>
|
||||
+ Add objective
|
||||
</Button>
|
||||
<HelpTip label="Adds a blank row using the same field keys as the first objective">
|
||||
<span className="inline-block">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={addRow}
|
||||
>
|
||||
+ Add objective
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -297,7 +305,9 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
|
||||
<CardContent className="space-y-5">
|
||||
{/* North star */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="north-star">North star</Label>
|
||||
<HelpTip label="The long-term vision every agent's briefing includes — keep it to a sentence or two">
|
||||
<Label htmlFor="north-star">North star</Label>
|
||||
</HelpTip>
|
||||
<Textarea
|
||||
id="north-star"
|
||||
rows={3}
|
||||
@@ -325,7 +335,9 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
|
||||
|
||||
{/* Constraints */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="constraints">Constraints (one per line)</Label>
|
||||
<HelpTip label="Hard rules injected into every agent's briefing (e.g. licensing, no external data egress)">
|
||||
<Label htmlFor="constraints">Constraints (one per line)</Label>
|
||||
</HelpTip>
|
||||
<Textarea
|
||||
id="constraints"
|
||||
rows={3}
|
||||
@@ -361,10 +373,14 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
|
||||
</div>
|
||||
|
||||
{/* Save */}
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{saving ? "Saving…" : "Save charter"}
|
||||
</Button>
|
||||
<HelpTip label="Saves north star, brand voice, objectives, constraints, and operating policy in one update">
|
||||
<span className="inline-block">
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{saving ? "Saving…" : "Save charter"}
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="border-t pt-4 space-y-1 text-xs text-muted-foreground">
|
||||
@@ -377,9 +393,11 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
|
||||
{goals.updated_by && (
|
||||
<p>
|
||||
Updated by:{" "}
|
||||
<span className="font-medium text-foreground font-mono">
|
||||
{goals.updated_by}
|
||||
</span>
|
||||
<HelpTip label="The agent or CEO identity that last saved this charter">
|
||||
<span className="font-medium text-foreground font-mono">
|
||||
{goals.updated_by}
|
||||
</span>
|
||||
</HelpTip>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -89,9 +89,12 @@ function PitchCard({ pitch, onApprove, onReject, busy }: PitchCardProps) {
|
||||
{pitch.target_cells.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{pitch.target_cells.map((c) => (
|
||||
<Badge key={c} variant="outline">
|
||||
{c}
|
||||
</Badge>
|
||||
<HelpTip
|
||||
key={c}
|
||||
label="A cell this pitch would route delivery work to if approved"
|
||||
>
|
||||
<Badge variant="outline">{c}</Badge>
|
||||
</HelpTip>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -111,21 +114,29 @@ function PitchCard({ pitch, onApprove, onReject, busy }: PitchCardProps) {
|
||||
</div>
|
||||
{proposed ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => setApproveOpen(true)}
|
||||
>
|
||||
<Check className="mr-1 h-4 w-4" /> Approve & provision
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={busy}
|
||||
onClick={() => setRejectOpen(true)}
|
||||
>
|
||||
<X className="mr-1 h-4 w-4" /> Reject
|
||||
</Button>
|
||||
<HelpTip label="Auto-provisions a new product + workspace for the target cells listed above">
|
||||
<span className="inline-block">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => setApproveOpen(true)}
|
||||
>
|
||||
<Check className="mr-1 h-4 w-4" /> Approve & provision
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
<HelpTip label="Rejects without creating anything — recorded in the decision log below">
|
||||
<span className="inline-block">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={busy}
|
||||
onClick={() => setRejectOpen(true)}
|
||||
>
|
||||
<X className="mr-1 h-4 w-4" /> Reject
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
) : (
|
||||
pitch.decision_notes && (
|
||||
|
||||
@@ -67,21 +67,29 @@ function DirectiveCard({
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-1">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => onConfirm(directive.id)}
|
||||
>
|
||||
<Check className="mr-1 h-4 w-4" /> Confirm
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={busy}
|
||||
onClick={() => setRejectOpen(true)}
|
||||
>
|
||||
<X className="mr-1 h-4 w-4" /> Reject
|
||||
</Button>
|
||||
<HelpTip label="Executes this directive as drafted (relay a message, update the charter, approve a pitch, or control a task)">
|
||||
<span className="inline-block">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => onConfirm(directive.id)}
|
||||
>
|
||||
<Check className="mr-1 h-4 w-4" /> Confirm
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
<HelpTip label="Discards the directive — the Secretary must re-propose it to try again">
|
||||
<span className="inline-block">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={busy}
|
||||
onClick={() => setRejectOpen(true)}
|
||||
>
|
||||
<X className="mr-1 h-4 w-4" /> Reject
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -244,9 +252,11 @@ export function SecretaryTab() {
|
||||
<CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
|
||||
<CardTitle>Chat</CardTitle>
|
||||
{sessionId && (
|
||||
<Button variant="outline" size="sm" onClick={() => void stop()}>
|
||||
End session
|
||||
</Button>
|
||||
<HelpTip label="Closes this chat — your next message starts a fresh Secretary session">
|
||||
<Button variant="outline" size="sm" onClick={() => void stop()}>
|
||||
End session
|
||||
</Button>
|
||||
</HelpTip>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col gap-4">
|
||||
@@ -284,18 +294,22 @@ export function SecretaryTab() {
|
||||
</Button>
|
||||
</HelpTip>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => void handleStart()}
|
||||
disabled={starting}
|
||||
className="h-11 shrink-0 px-6"
|
||||
aria-label={starting ? "Starting…" : undefined}
|
||||
>
|
||||
{starting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Start"
|
||||
)}
|
||||
</Button>
|
||||
<HelpTip label="Opens a new Secretary chat session — reads live company state, not a scripted bot">
|
||||
<span className="inline-block">
|
||||
<Button
|
||||
onClick={() => void handleStart()}
|
||||
disabled={starting}
|
||||
className="h-11 shrink-0 px-6"
|
||||
aria-label={starting ? "Starting…" : undefined}
|
||||
>
|
||||
{starting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Start"
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -34,6 +34,15 @@ const FORBIDDABLE_KINDS: DefinitionKind[] = [
|
||||
"component",
|
||||
];
|
||||
|
||||
const KIND_HINTS: Record<DefinitionKind, string> = {
|
||||
model: "Pydantic / ORM data models",
|
||||
route: "FastAPI route handlers",
|
||||
helper: "Standalone top-level functions — placement here only warns, never blocks",
|
||||
business_logic: "Service-layer logic (the classes services/ owns)",
|
||||
component: "Frontend components (.tsx)",
|
||||
other: "Anything the classifier can't place in the kinds above",
|
||||
};
|
||||
|
||||
function actionToast(verb: string, result: ConventionsActionResult): void {
|
||||
if (result.created && result.pr_number != null) {
|
||||
toast.success(
|
||||
@@ -189,50 +198,59 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
className="space-y-2 rounded-md border border-border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpTip label="Repo-relative directory this rule applies to (e.g. roboco/services)">
|
||||
<Input
|
||||
value={module.path}
|
||||
placeholder="path/to/module"
|
||||
onChange={(e) =>
|
||||
updateModule(index, { path: e.target.value })
|
||||
}
|
||||
/>
|
||||
</HelpTip>
|
||||
<HelpTip label="Deletes this module row locally — nothing changes until Save to repo">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeModule(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</div>
|
||||
<HelpTip label="One-line description shown in the ambient 'Architectural Standard' prompt block every agent sees">
|
||||
<Input
|
||||
value={module.path}
|
||||
placeholder="path/to/module"
|
||||
value={module.purpose}
|
||||
placeholder="what this module is for"
|
||||
onChange={(e) =>
|
||||
updateModule(index, { path: e.target.value })
|
||||
updateModule(index, { purpose: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeModule(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={module.purpose}
|
||||
placeholder="what this module is for"
|
||||
onChange={(e) =>
|
||||
updateModule(index, { purpose: e.target.value })
|
||||
}
|
||||
/>
|
||||
</HelpTip>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{FORBIDDABLE_KINDS.map((kind) => (
|
||||
<Badge
|
||||
key={kind}
|
||||
variant={
|
||||
module.forbidden.includes(kind)
|
||||
? "destructive"
|
||||
: "outline"
|
||||
}
|
||||
className="cursor-pointer"
|
||||
onClick={() => toggleForbidden(index, kind)}
|
||||
>
|
||||
no {kind}
|
||||
</Badge>
|
||||
<HelpTip key={kind} label={KIND_HINTS[kind]}>
|
||||
<Badge
|
||||
variant={
|
||||
module.forbidden.includes(kind)
|
||||
? "destructive"
|
||||
: "outline"
|
||||
}
|
||||
className="cursor-pointer"
|
||||
onClick={() => toggleForbidden(index, kind)}
|
||||
>
|
||||
no {kind}
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={addModule}>
|
||||
Add module
|
||||
</Button>
|
||||
<HelpTip label="Adds a blank module row to constrain another directory">
|
||||
<Button variant="outline" size="sm" onClick={addModule}>
|
||||
Add module
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -253,9 +271,17 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
>
|
||||
<span className="text-sm">{rule.name.replace(/_/g, " ")}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-10 text-right text-xs text-muted-foreground">
|
||||
{rule.level}
|
||||
</span>
|
||||
<HelpTip
|
||||
label={
|
||||
rule.level === "block"
|
||||
? "Block: refuses i_am_done / pr_pass with the offending file:line"
|
||||
: "Warn: informational only, never blocks the gate"
|
||||
}
|
||||
>
|
||||
<span className="w-10 text-right text-xs text-muted-foreground">
|
||||
{rule.level}
|
||||
</span>
|
||||
</HelpTip>
|
||||
<Switch
|
||||
checked={rule.level === "block"}
|
||||
onCheckedChange={(checked) =>
|
||||
@@ -285,34 +311,50 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
className="space-y-2 rounded-md border border-border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={waiver.path}
|
||||
placeholder="path/to/file.py"
|
||||
onChange={(e) => updateWaiver(index, { path: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
value={waiver.rule}
|
||||
placeholder="rule name"
|
||||
onChange={(e) => updateWaiver(index, { rule: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeWaiver(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<HelpTip label="Repo-relative path or glob this waiver exempts from the rule on the right">
|
||||
<Input
|
||||
value={waiver.path}
|
||||
placeholder="path/to/file.py"
|
||||
onChange={(e) =>
|
||||
updateWaiver(index, { path: e.target.value })
|
||||
}
|
||||
/>
|
||||
</HelpTip>
|
||||
<HelpTip label="Must match a rule name above exactly for the waiver to apply">
|
||||
<Input
|
||||
value={waiver.rule}
|
||||
placeholder="rule name"
|
||||
onChange={(e) =>
|
||||
updateWaiver(index, { rule: e.target.value })
|
||||
}
|
||||
/>
|
||||
</HelpTip>
|
||||
<HelpTip label="Deletes this waiver row locally — nothing changes until Save to repo">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeWaiver(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</div>
|
||||
<Input
|
||||
value={waiver.reason}
|
||||
placeholder="why this is waived"
|
||||
onChange={(e) => updateWaiver(index, { reason: e.target.value })}
|
||||
/>
|
||||
<HelpTip label="Accountability note — committed with the waiver and reviewed in the PR, never a silent suppression">
|
||||
<Input
|
||||
value={waiver.reason}
|
||||
placeholder="why this is waived"
|
||||
onChange={(e) =>
|
||||
updateWaiver(index, { reason: e.target.value })
|
||||
}
|
||||
/>
|
||||
</HelpTip>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addWaiver}>
|
||||
Add waiver
|
||||
</Button>
|
||||
<HelpTip label="Adds a blank waiver row">
|
||||
<Button variant="outline" size="sm" onClick={addWaiver}>
|
||||
Add waiver
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -332,11 +374,13 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
className="space-y-2 rounded-md border border-border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={rule.id}
|
||||
placeholder="rule-id"
|
||||
onChange={(e) => updateCustom(index, { id: e.target.value })}
|
||||
/>
|
||||
<HelpTip label="A short, unique identifier for this rule (shown as its badge in Recent violations)">
|
||||
<Input
|
||||
value={rule.id}
|
||||
placeholder="rule-id"
|
||||
onChange={(e) => updateCustom(index, { id: e.target.value })}
|
||||
/>
|
||||
</HelpTip>
|
||||
<HelpTip
|
||||
label={
|
||||
rule.level === "block"
|
||||
@@ -354,29 +398,41 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
updateCustom(index, { level: checked ? "block" : "warn" })
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeCustom(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<HelpTip label="Deletes this custom rule row locally — nothing changes until Save to repo">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeCustom(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</div>
|
||||
<Input
|
||||
value={rule.pattern}
|
||||
placeholder="regex pattern"
|
||||
onChange={(e) => updateCustom(index, { pattern: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
value={rule.message}
|
||||
placeholder="message shown when it matches"
|
||||
onChange={(e) => updateCustom(index, { message: e.target.value })}
|
||||
/>
|
||||
<HelpTip label="Regex tested against each changed file's source text — a bad pattern is skipped, never fails the gate">
|
||||
<Input
|
||||
value={rule.pattern}
|
||||
placeholder="regex pattern"
|
||||
onChange={(e) =>
|
||||
updateCustom(index, { pattern: e.target.value })
|
||||
}
|
||||
/>
|
||||
</HelpTip>
|
||||
<HelpTip label="Shown next to a match in Recent violations below">
|
||||
<Input
|
||||
value={rule.message}
|
||||
placeholder="message shown when it matches"
|
||||
onChange={(e) =>
|
||||
updateCustom(index, { message: e.target.value })
|
||||
}
|
||||
/>
|
||||
</HelpTip>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addCustom}>
|
||||
Add custom rule
|
||||
</Button>
|
||||
<HelpTip label="Adds a blank custom regex rule">
|
||||
<Button variant="outline" size="sm" onClick={addCustom}>
|
||||
Add custom rule
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -144,9 +144,14 @@ function UnifiedHistoryRow({ row }: { row: UnifiedRow }) {
|
||||
{PLATFORM_LABELS[platform] ?? platform}: {id}
|
||||
</a>
|
||||
) : (
|
||||
<span key={platform} className="text-muted-foreground">
|
||||
{PLATFORM_LABELS[platform] ?? platform}: {id}
|
||||
</span>
|
||||
<HelpTip
|
||||
key={platform}
|
||||
label="TikTok's inbox-upload API returns no public URL — this is the platform-side post id"
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
{PLATFORM_LABELS[platform] ?? platform}: {id}
|
||||
</span>
|
||||
</HelpTip>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
@@ -211,7 +216,9 @@ export function SocialHistorySection({ className }: { className?: string }) {
|
||||
size="sm"
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<span>{open ? "Hide" : "Show"} history</span>
|
||||
<HelpTip label="Fetches both sources once, on first expand — up to 50 rows each">
|
||||
<span>{open ? "Hide" : "Show"} history</span>
|
||||
</HelpTip>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`}
|
||||
/>
|
||||
|
||||
@@ -76,9 +76,11 @@ function PipelineRow({ item }: { item: VideoPipelineItem }) {
|
||||
prefetch={false}
|
||||
className="ml-auto"
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
Review
|
||||
</Button>
|
||||
<HelpTip label="Open the task to approve or reject the rendered video">
|
||||
<Button variant="outline" size="sm">
|
||||
Review
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</Link>
|
||||
)}
|
||||
{canRerender && (
|
||||
@@ -110,7 +112,9 @@ export function VideoPipelineStrip({ className }: { className?: string }) {
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Film className="h-5 w-5" />
|
||||
Video Pipeline
|
||||
<Badge variant="secondary">{items.length}</Badge>
|
||||
<HelpTip label="Videos currently authoring, in review, or rendering — not yet in the queue below">
|
||||
<Badge variant="secondary">{items.length}</Badge>
|
||||
</HelpTip>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Every video in flight — authoring, review, or rendering — before it
|
||||
|
||||
@@ -483,7 +483,9 @@ function RequestVideoDialog({
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Project</Label>
|
||||
<HelpTip label="Only projects with the video engine enabled (Projects → Edit) can be picked">
|
||||
<Label>Project</Label>
|
||||
</HelpTip>
|
||||
<ProjectSelector
|
||||
value={effectiveProjectId}
|
||||
onChange={setProjectId}
|
||||
@@ -493,7 +495,9 @@ function RequestVideoDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="video-request-occasion">Occasion</Label>
|
||||
<HelpTip label="Short label for the trigger event — shown as a badge throughout the pipeline, queue, and history">
|
||||
<Label htmlFor="video-request-occasion">Occasion</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="video-request-occasion"
|
||||
placeholder="e.g. v0.19.0 launch, Founder's Day..."
|
||||
@@ -502,7 +506,9 @@ function RequestVideoDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="video-request-brief">Brief</Label>
|
||||
<HelpTip label="What the video should communicate — read by the assigned UX/UI dev when building the composition">
|
||||
<Label htmlFor="video-request-brief">Brief</Label>
|
||||
</HelpTip>
|
||||
<Textarea
|
||||
id="video-request-brief"
|
||||
placeholder="What should this video cover?"
|
||||
@@ -512,7 +518,9 @@ function RequestVideoDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Platforms</Label>
|
||||
<HelpTip label="Where the rendered clip can post once you approve it — editable again before approving">
|
||||
<Label>Platforms</Label>
|
||||
</HelpTip>
|
||||
<div className="flex gap-4">
|
||||
{REQUEST_PLATFORMS.map((platform) => (
|
||||
<div key={platform} className="flex items-center gap-2">
|
||||
@@ -521,12 +529,20 @@ function RequestVideoDialog({
|
||||
checked={platforms.includes(platform)}
|
||||
onCheckedChange={() => togglePlatform(platform)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`video-request-${platform}`}
|
||||
className="text-sm font-normal"
|
||||
<HelpTip
|
||||
label={
|
||||
platform === "x"
|
||||
? "Posts via X's OAuth 1.0a API, 280-char caption"
|
||||
: "Posts via TikTok's inbox-upload OAuth2 API, 2200-char caption"
|
||||
}
|
||||
>
|
||||
{PLATFORM_LABELS[platform]}
|
||||
</Label>
|
||||
<Label
|
||||
htmlFor={`video-request-${platform}`}
|
||||
className="text-sm font-normal"
|
||||
>
|
||||
{PLATFORM_LABELS[platform]}
|
||||
</Label>
|
||||
</HelpTip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -654,10 +670,12 @@ export function VideoPostQueue({ className }: { className?: string }) {
|
||||
if (isLoading) return null;
|
||||
|
||||
const requestButton = (
|
||||
<Button variant="outline" size="sm" onClick={() => setRequestOpen(true)}>
|
||||
<Sparkles className="mr-1 h-4 w-4" />
|
||||
Request a video
|
||||
</Button>
|
||||
<HelpTip label="Opens a dialog to brief a UX/UI dev on an on-demand video, independent of a release or spotlight">
|
||||
<Button variant="outline" size="sm" onClick={() => setRequestOpen(true)}>
|
||||
<Sparkles className="mr-1 h-4 w-4" />
|
||||
Request a video
|
||||
</Button>
|
||||
</HelpTip>
|
||||
);
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
@@ -696,7 +714,9 @@ export function VideoPostQueue({ className }: { className?: string }) {
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Film className="h-5 w-5" />
|
||||
Video Post Queue
|
||||
<Badge variant="secondary">{posts.length}</Badge>
|
||||
<HelpTip label="Rendered clips waiting for your preview, caption edits, approve, or reject">
|
||||
<Badge variant="secondary">{posts.length}</Badge>
|
||||
</HelpTip>
|
||||
</CardTitle>
|
||||
<CardAction>{requestButton}</CardAction>
|
||||
<CardDescription>
|
||||
@@ -727,7 +747,9 @@ export function VideoPostQueue({ className }: { className?: string }) {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="video-reject-reason">Reason</Label>
|
||||
<HelpTip label="Stored on the draft and shown in History next to this rejection">
|
||||
<Label htmlFor="video-reject-reason">Reason</Label>
|
||||
</HelpTip>
|
||||
<Textarea
|
||||
id="video-reject-reason"
|
||||
placeholder="e.g. off-brand tone, wrong occasion..."
|
||||
|
||||
@@ -109,17 +109,23 @@ function XPostRow({
|
||||
</span>
|
||||
</HelpTip>
|
||||
{post.release_version && (
|
||||
<Badge variant="outline">v{post.release_version}</Badge>
|
||||
<HelpTip label="The release this post announces">
|
||||
<Badge variant="outline">v{post.release_version}</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
{post.mention && (
|
||||
<Badge variant="secondary" className="max-w-56 truncate">
|
||||
re: {post.mention.text}
|
||||
</Badge>
|
||||
<HelpTip label="The X mention this draft replies to">
|
||||
<Badge variant="secondary" className="max-w-56 truncate">
|
||||
re: {post.mention.text}
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
{post.feature && (
|
||||
<Badge variant="outline" className="max-w-56 truncate">
|
||||
feature: {post.feature.title}
|
||||
</Badge>
|
||||
<HelpTip label="The shipped feature this spotlight covers">
|
||||
<Badge variant="outline" className="max-w-56 truncate">
|
||||
feature: {post.feature.title}
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -266,7 +272,9 @@ export function XPostQueue({ className }: { className?: string }) {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Rocket className="h-5 w-5" />X Post Queue
|
||||
<Badge variant="secondary">{posts.length}</Badge>
|
||||
<HelpTip label="Drafts waiting for your edit, approve, or reject">
|
||||
<Badge variant="secondary">{posts.length}</Badge>
|
||||
</HelpTip>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Drafted release announcements, feature spotlights, and mention
|
||||
@@ -297,7 +305,9 @@ export function XPostQueue({ className }: { className?: string }) {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="x-reject-reason">Reason</Label>
|
||||
<HelpTip label="Stored on the draft and shown in History next to this rejection">
|
||||
<Label htmlFor="x-reject-reason">Reason</Label>
|
||||
</HelpTip>
|
||||
<Textarea
|
||||
id="x-reject-reason"
|
||||
placeholder="e.g. tone doesn't match our voice; not worth a public reply..."
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const categoryConfig: Record<
|
||||
@@ -135,9 +136,11 @@ export function KBCategoryNav({
|
||||
<div className="flex-1 min-w-0 text-left">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-sm">{config.label}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{count.toLocaleString()} docs
|
||||
</span>
|
||||
<HelpTip label="Document count — one document can split into several chunks at embedding time, so the chunk total (Index Stats) usually runs higher.">
|
||||
<span className="text-xs text-muted-foreground w-fit">
|
||||
{count.toLocaleString()} docs
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{config.description}
|
||||
|
||||
@@ -86,14 +86,16 @@ function KBCategoryViewInner({ category }: { category: KBIndexType }) {
|
||||
{doc.source}
|
||||
</p>
|
||||
</HelpTip>
|
||||
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>
|
||||
{formatDistanceToNow(new Date(doc.indexed_at), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<HelpTip label={new Date(doc.indexed_at).toLocaleString()}>
|
||||
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground w-fit">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>
|
||||
{formatDistanceToNow(new Date(doc.indexed_at), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -85,14 +85,16 @@ export function KBFilters({ selectedTypes, onTypesChange }: KBFiltersProps) {
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Filter by type</span>
|
||||
{selectedTypes.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onTypesChange([])}
|
||||
className="h-auto py-0 px-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<HelpTip label="Reset — search every index type again instead of just these">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onTypesChange([])}
|
||||
className="h-auto py-0 px-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</HelpTip>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -75,19 +75,25 @@ export function KBResultCard({ result, onClick }: KBResultCardProps) {
|
||||
{result.metadata && Object.keys(result.metadata).length > 0 && (
|
||||
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
||||
{typeof result.metadata.language === "string" && (
|
||||
<span className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||
{result.metadata.language}
|
||||
</span>
|
||||
<HelpTip label="Programming language this chunk was tagged with when indexed">
|
||||
<span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
|
||||
{result.metadata.language}
|
||||
</span>
|
||||
</HelpTip>
|
||||
)}
|
||||
{typeof result.metadata.agent === "string" && (
|
||||
<span className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||
@{result.metadata.agent}
|
||||
</span>
|
||||
<HelpTip label="Agent who authored this content">
|
||||
<span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
|
||||
@{result.metadata.agent}
|
||||
</span>
|
||||
</HelpTip>
|
||||
)}
|
||||
{typeof result.metadata.section === "string" && (
|
||||
<span className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||
{result.metadata.section}
|
||||
</span>
|
||||
<HelpTip label="Section of the source document this chunk was extracted from">
|
||||
<span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
|
||||
{result.metadata.section}
|
||||
</span>
|
||||
</HelpTip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -142,9 +142,11 @@ export function KBStatsCard({ stats, isLoading }: KBStatsCardProps) {
|
||||
</div>
|
||||
</div>
|
||||
{latestUpdated && (
|
||||
<p className="text-xs text-muted-foreground pt-1">
|
||||
Updated {formatDistanceToNow(new Date(latestUpdated))} ago
|
||||
</p>
|
||||
<HelpTip label={new Date(latestUpdated).toLocaleString()}>
|
||||
<p className="text-xs text-muted-foreground pt-1 w-fit">
|
||||
Updated {formatDistanceToNow(new Date(latestUpdated))} ago
|
||||
</p>
|
||||
</HelpTip>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -17,6 +17,11 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
@@ -326,26 +331,60 @@ function KnowledgeBaseBrowserContent() {
|
||||
onValueChange={(v) => handleTabChange(v as TabValue)}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="search" className="gap-2">
|
||||
<Search className="h-4 w-4" />
|
||||
Search
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ask" className="gap-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Ask AI
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mentor" className="gap-2">
|
||||
<Brain className="h-4 w-4" />
|
||||
Mentor
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="browse" className="gap-2">
|
||||
<FolderTree className="h-4 w-4" />
|
||||
Browse
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="admin" className="gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
Admin
|
||||
</TabsTrigger>
|
||||
{(
|
||||
[
|
||||
{
|
||||
value: "search",
|
||||
icon: Search,
|
||||
label: "Search",
|
||||
hint: "Semantic search across indexed content, filterable by category",
|
||||
},
|
||||
{
|
||||
value: "ask",
|
||||
icon: Sparkles,
|
||||
label: "Ask AI",
|
||||
hint: "One-shot Q&A: retrieves matching chunks (RAG) and generates a cited answer",
|
||||
},
|
||||
{
|
||||
value: "mentor",
|
||||
icon: Brain,
|
||||
label: "Mentor",
|
||||
hint: "Multi-turn chat that also factors in your role and your own journal entries",
|
||||
},
|
||||
{
|
||||
value: "browse",
|
||||
icon: FolderTree,
|
||||
label: "Browse",
|
||||
hint: "Page through every indexed document by category, no query needed",
|
||||
},
|
||||
{
|
||||
value: "admin",
|
||||
icon: Settings,
|
||||
label: "Admin",
|
||||
hint: "Index health, storage stats, and manual reindex/refresh/delete controls",
|
||||
},
|
||||
] as const
|
||||
).map((tab) => (
|
||||
<Tooltip key={tab.value}>
|
||||
<TooltipTrigger asChild>
|
||||
{/* TooltipTrigger's asChild Slot merge injects its own
|
||||
data-state onto this trigger, and Radix Tabs' internal
|
||||
render spreads incoming props after its own literal
|
||||
data-state — so the tooltip's value silently wins and the
|
||||
data-[state=active] highlight never fires. Re-assert the
|
||||
real selection state explicitly so it survives the merge. */}
|
||||
<TabsTrigger
|
||||
value={tab.value}
|
||||
data-state={tab.value === activeTab ? "active" : "inactive"}
|
||||
className="gap-2"
|
||||
>
|
||||
<tab.icon className="h-4 w-4" />
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tab.hint}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{/* Search Tab */}
|
||||
@@ -460,17 +499,27 @@ function KnowledgeBaseBrowserContent() {
|
||||
<XCircle className="h-8 w-8 text-red-600" />
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{health.healthy ? "Healthy" : "Unhealthy"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Embedding: {health.embedding_status}
|
||||
</p>
|
||||
<HelpTip label="Healthy requires all three subsystems below — embedding, LLM, and vector store — to each respond OK">
|
||||
<p className="font-medium w-fit">
|
||||
{health.healthy ? "Healthy" : "Unhealthy"}
|
||||
</p>
|
||||
</HelpTip>
|
||||
<HelpTip label="Ollama-served embedding model (qwen3-embedding:0.6b) that turns text into vectors for search">
|
||||
<p className="text-sm text-muted-foreground w-fit">
|
||||
Embedding: {health.embedding_status}
|
||||
</p>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
|
||||
<div>LLM: {health.llm_status}</div>
|
||||
<div>Vector: {health.vector_store_status}</div>
|
||||
<HelpTip label="Local model that generates RAG and Mentor answers from retrieved context">
|
||||
<div className="w-fit">LLM: {health.llm_status}</div>
|
||||
</HelpTip>
|
||||
<HelpTip label="PostgreSQL + pgvector store holding every embedded chunk for similarity search">
|
||||
<div className="w-fit">
|
||||
Vector: {health.vector_store_status}
|
||||
</div>
|
||||
</HelpTip>
|
||||
</div>
|
||||
{(
|
||||
[
|
||||
@@ -507,10 +556,12 @@ function KnowledgeBaseBrowserContent() {
|
||||
<span>Summary</span>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="sm" variant="destructive">
|
||||
<RefreshCw className="h-3 w-3 mr-1" />
|
||||
Reindex All
|
||||
</Button>
|
||||
<HelpTip label="Rebuilds the Documentation index from the repo's docs/ tree — the other categories (journals, conversations, etc.) are populated live by agent activity, not by this button">
|
||||
<Button size="sm" variant="destructive">
|
||||
<RefreshCw className="h-3 w-3 mr-1" />
|
||||
Reindex All
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -538,10 +589,14 @@ function KnowledgeBaseBrowserContent() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{totalDocs}</p>
|
||||
<p className="text-xs text-muted-foreground">Documents</p>
|
||||
</div>
|
||||
<HelpTip label="Sum of documents indexed across every category below">
|
||||
<div className="w-fit">
|
||||
<p className="text-2xl font-bold">{totalDocs}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Documents
|
||||
</p>
|
||||
</div>
|
||||
</HelpTip>
|
||||
<HelpTip label="A chunk is a segment of a document split for embedding — one document can produce many chunks">
|
||||
<div className="w-fit">
|
||||
<p className="text-2xl font-bold">{totalChunks}</p>
|
||||
@@ -667,14 +722,16 @@ function KnowledgeBaseBrowserContent() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 text-sm mb-2">
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
Documents:
|
||||
</span>{" "}
|
||||
<span className="font-medium">
|
||||
{index.document_count}
|
||||
</span>
|
||||
</div>
|
||||
<HelpTip label="Document count for this category — see Chunks for the post-embedding split count">
|
||||
<div className="w-fit">
|
||||
<span className="text-muted-foreground">
|
||||
Documents:
|
||||
</span>{" "}
|
||||
<span className="font-medium">
|
||||
{index.document_count}
|
||||
</span>
|
||||
</div>
|
||||
</HelpTip>
|
||||
<HelpTip label="A chunk is a segment of a document split for embedding — one document can produce many chunks">
|
||||
<div className="w-fit">
|
||||
<span className="text-muted-foreground">
|
||||
@@ -685,25 +742,37 @@ function KnowledgeBaseBrowserContent() {
|
||||
</span>
|
||||
</div>
|
||||
</HelpTip>
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
Updated:
|
||||
</span>{" "}
|
||||
<span className="font-medium">
|
||||
{index.last_updated
|
||||
? formatDistanceToNow(
|
||||
new Date(index.last_updated),
|
||||
) + " ago"
|
||||
: "Never"}
|
||||
</span>
|
||||
</div>
|
||||
<HelpTip
|
||||
label={
|
||||
index.last_updated
|
||||
? new Date(
|
||||
index.last_updated,
|
||||
).toLocaleString()
|
||||
: "This index has never been populated"
|
||||
}
|
||||
>
|
||||
<div className="w-fit">
|
||||
<span className="text-muted-foreground">
|
||||
Updated:
|
||||
</span>{" "}
|
||||
<span className="font-medium">
|
||||
{index.last_updated
|
||||
? formatDistanceToNow(
|
||||
new Date(index.last_updated),
|
||||
) + " ago"
|
||||
: "Never"}
|
||||
</span>
|
||||
</div>
|
||||
</HelpTip>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Storage usage</span>
|
||||
<span>{percentage}%</span>
|
||||
</div>
|
||||
<HelpTip label="Share of the fleet's total chunk count this category holds — not literal disk bytes">
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Storage usage</span>
|
||||
<span>{percentage}%</span>
|
||||
</div>
|
||||
</HelpTip>
|
||||
<Progress value={percentage} className="h-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -149,28 +149,32 @@ export function MentorAnswerDisplay({
|
||||
<Brain className="h-5 w-5 text-primary shrink-0" />
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
|
||||
{response.agent_role && (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<User className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Role:</span>
|
||||
<Badge variant="secondary" className="font-medium">
|
||||
{response.agent_role}
|
||||
</Badge>
|
||||
{response.agent_team && (
|
||||
<span className="text-muted-foreground">
|
||||
({response.agent_team})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<HelpTip label="Detected from your agent identity — shapes which standards, journals, and playbooks the mentor draws on">
|
||||
<span className="flex items-center gap-1.5 w-fit">
|
||||
<User className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Role:</span>
|
||||
<Badge variant="secondary" className="font-medium">
|
||||
{response.agent_role}
|
||||
</Badge>
|
||||
{response.agent_team && (
|
||||
<span className="text-muted-foreground">
|
||||
({response.agent_team})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</HelpTip>
|
||||
)}
|
||||
{response.journal_entries_used !== undefined &&
|
||||
response.journal_entries_used > 0 && (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<BookMarked className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
{response.journal_entries_used} personal journal
|
||||
{response.journal_entries_used !== 1 ? "s" : ""} used
|
||||
<HelpTip label="Entries from your own journal only — never another agent's">
|
||||
<span className="flex items-center gap-1.5 w-fit">
|
||||
<BookMarked className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
{response.journal_entries_used} personal journal
|
||||
{response.journal_entries_used !== 1 ? "s" : ""} used
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</HelpTip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,9 +186,11 @@ export function MentorAnswerDisplay({
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<Brain className="h-4 w-4 text-primary" />
|
||||
Mentor Response
|
||||
<span className="text-xs text-muted-foreground font-normal ml-auto">
|
||||
{response.sources.length} sources used
|
||||
</span>
|
||||
<HelpTip label="Knowledge-base chunks retrieved and handed to the model as context for this answer">
|
||||
<span className="text-xs text-muted-foreground font-normal ml-auto w-fit">
|
||||
{response.sources.length} sources used
|
||||
</span>
|
||||
</HelpTip>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -204,16 +210,17 @@ export function MentorAnswerDisplay({
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{response.suggested_followups.map((followup, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-auto py-1.5 px-3"
|
||||
onClick={() => onFollowUp?.(followup)}
|
||||
>
|
||||
<MessageCircleQuestion className="h-3 w-3 mr-1.5" />
|
||||
{followup}
|
||||
</Button>
|
||||
<HelpTip key={index} label="Ask this next, in the same conversation">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-auto py-1.5 px-3"
|
||||
onClick={() => onFollowUp?.(followup)}
|
||||
>
|
||||
<MessageCircleQuestion className="h-3 w-3 mr-1.5" />
|
||||
{followup}
|
||||
</Button>
|
||||
</HelpTip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import {
|
||||
@@ -177,15 +178,19 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
|
||||
<Brain className="h-5 w-5 text-primary" />
|
||||
<span className="font-medium">Mentor Chat</span>
|
||||
{conversationId && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Conversation active
|
||||
</Badge>
|
||||
<HelpTip label="Follow-up questions reuse this conversation's history for context">
|
||||
<Badge variant="outline" className="text-xs w-fit">
|
||||
Conversation active
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={handleNewChat}>
|
||||
<RotateCcw className="h-4 w-4 mr-1" />
|
||||
New Chat
|
||||
</Button>
|
||||
<HelpTip label="Clears this thread — the mentor loses this conversation's context">
|
||||
<Button variant="ghost" size="sm" onClick={handleNewChat}>
|
||||
<RotateCcw className="h-4 w-4 mr-1" />
|
||||
New Chat
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
@@ -241,19 +246,21 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
|
||||
{/* Sources toggle */}
|
||||
{msg.sources && msg.sources.length > 0 && (
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
onClick={() =>
|
||||
setExpandedSources(
|
||||
expandedSources === idx ? null : idx,
|
||||
)
|
||||
}
|
||||
>
|
||||
{expandedSources === idx ? "Hide" : "Show"}{" "}
|
||||
{msg.sources.length} sources
|
||||
</Button>
|
||||
<HelpTip label="The knowledge-base chunks retrieved and handed to the model as context for this answer">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
onClick={() =>
|
||||
setExpandedSources(
|
||||
expandedSources === idx ? null : idx,
|
||||
)
|
||||
}
|
||||
>
|
||||
{expandedSources === idx ? "Hide" : "Show"}{" "}
|
||||
{msg.sources.length} sources
|
||||
</Button>
|
||||
</HelpTip>
|
||||
{expandedSources === idx && (
|
||||
<div className="mt-2 space-y-2">
|
||||
{msg.sources.map((source, sidx) => (
|
||||
@@ -278,16 +285,20 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
|
||||
{msg.followups && msg.followups.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{msg.followups.map((followup, fidx) => (
|
||||
<Button
|
||||
<HelpTip
|
||||
key={fidx}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-auto py-1.5"
|
||||
onClick={() => handleFollowUp(followup)}
|
||||
disabled={isLoading}
|
||||
label="Ask this next, in the same conversation"
|
||||
>
|
||||
{followup}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-auto py-1.5"
|
||||
onClick={() => handleFollowUp(followup)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{followup}
|
||||
</Button>
|
||||
</HelpTip>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
import {
|
||||
Send,
|
||||
Loader2,
|
||||
@@ -60,18 +61,33 @@ export function MentorQueryInput({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<Badge variant="outline" className="text-xs gap-1 bg-background">
|
||||
<User className="h-3 w-3" />
|
||||
Role-aware
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs gap-1 bg-background">
|
||||
<BookMarked className="h-3 w-3" />
|
||||
Journals
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs gap-1 bg-background">
|
||||
<MessageCircle className="h-3 w-3" />
|
||||
Follow-ups
|
||||
</Badge>
|
||||
<HelpTip label="Answers factor in your agent role and team">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs gap-1 bg-background w-fit"
|
||||
>
|
||||
<User className="h-3 w-3" />
|
||||
Role-aware
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
<HelpTip label="Searches your own journal entries — not other agents'">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs gap-1 bg-background w-fit"
|
||||
>
|
||||
<BookMarked className="h-3 w-3" />
|
||||
Journals
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
<HelpTip label="Multi-turn — reuses this conversation's context for related questions">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs gap-1 bg-background w-fit"
|
||||
>
|
||||
<MessageCircle className="h-3 w-3" />
|
||||
Follow-ups
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { RAGQueryResponse } from "@/types";
|
||||
import { RAGCitationCard } from "./rag-citation-card";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
import {
|
||||
Bot,
|
||||
BookOpen,
|
||||
@@ -120,9 +121,11 @@ export function RAGAnswerDisplay({
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<Bot className="h-4 w-4 text-primary" />
|
||||
AI Answer
|
||||
<span className="text-xs text-muted-foreground font-normal ml-auto">
|
||||
{response.context_used} sources used
|
||||
</span>
|
||||
<HelpTip label="Knowledge-base chunks retrieved by hybrid search and handed to the model as context for this answer">
|
||||
<span className="text-xs text-muted-foreground font-normal ml-auto w-fit">
|
||||
{response.context_used} sources used
|
||||
</span>
|
||||
</HelpTip>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -178,20 +178,26 @@ function BellRow({ notification, onMarkRead, onAcknowledge }: BellRowProps) {
|
||||
<span className="font-medium text-sm line-clamp-1 flex-1 min-w-0">
|
||||
{notification.subject}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-[10px] shrink-0">
|
||||
{notification.priority}
|
||||
</Badge>
|
||||
<HelpTip label="How urgently the sender flagged this — doesn't change what you need to do, just how prominently it's shown.">
|
||||
<Badge variant="outline" className="text-[10px] shrink-0">
|
||||
{notification.priority}
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1.5 mt-1.5">
|
||||
{!notification.is_read && (
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
New
|
||||
</Badge>
|
||||
<HelpTip label="Hasn't been marked read yet.">
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
New
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
{needsAck && (
|
||||
<Badge variant="destructive" className="text-[10px]">
|
||||
Needs Ack
|
||||
</Badge>
|
||||
<HelpTip label="A formal signal from a PM/Board role — needs an explicit acknowledgement, tracked separately from read status.">
|
||||
<Badge variant="destructive" className="text-[10px]">
|
||||
Needs Ack
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
{!notification.is_read && (
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { Plus } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Team, type ProductCellMapping } from "@/types";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
|
||||
const cells: { value: Team; label: string }[] = [
|
||||
{ value: Team.BACKEND, label: "Backend" },
|
||||
@@ -104,12 +105,14 @@ export function CreateProductDialog() {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
New Product
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<HelpTip label="Groups per-cell projects (e.g. a SaaS app + its OSS core) into one deliverable for MegaTask and board routing.">
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
New Product
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</HelpTip>
|
||||
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
@@ -121,7 +124,9 @@ export function CreateProductDialog() {
|
||||
<div className="grid gap-4 py-4">
|
||||
{/* Name */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Product Name *</Label>
|
||||
<HelpTip label="Display name shown across the panel; renaming it never touches the slug already generated.">
|
||||
<Label htmlFor="name">Product Name *</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
@@ -132,7 +137,9 @@ export function CreateProductDialog() {
|
||||
|
||||
{/* Slug */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="slug">Slug *</Label>
|
||||
<HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API.">
|
||||
<Label htmlFor="slug">Slug *</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="slug"
|
||||
value={slug}
|
||||
@@ -147,7 +154,9 @@ export function CreateProductDialog() {
|
||||
|
||||
{/* Description */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<HelpTip label="Optional context shown in the panel only — not surfaced to agents or used in task routing.">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
</HelpTip>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
@@ -158,15 +167,19 @@ export function CreateProductDialog() {
|
||||
|
||||
{/* Cell -> Project mapping */}
|
||||
<div className="grid gap-3">
|
||||
<Label>Cell Project Mapping</Label>
|
||||
<HelpTip label="Routes each cell's work for this product to its own repo; a cell left unmapped does no work for this product.">
|
||||
<Label>Cell Project Mapping</Label>
|
||||
</HelpTip>
|
||||
{cells.map((cell) => (
|
||||
<div key={cell.value} className="grid gap-2">
|
||||
<Label
|
||||
htmlFor={`cell-${cell.value}`}
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{cell.label}
|
||||
</Label>
|
||||
<HelpTip label={`Which project the ${cell.label} cell works on for this product.`}>
|
||||
<Label
|
||||
htmlFor={`cell-${cell.value}`}
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{cell.label}
|
||||
</Label>
|
||||
</HelpTip>
|
||||
<Select
|
||||
value={cellMapping[cell.value] ?? ""}
|
||||
onValueChange={(value) =>
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
type ProductCellMapping,
|
||||
type ProductUpdate,
|
||||
} from "@/types";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
|
||||
const cells: { value: Team; label: string }[] = [
|
||||
{ value: Team.BACKEND, label: "Backend" },
|
||||
@@ -120,7 +121,9 @@ function EditProductForm({
|
||||
<div className="grid gap-4 py-4">
|
||||
{/* Slug (read-only) */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="slug">Slug</Label>
|
||||
<HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API. Set at creation, fixed here.">
|
||||
<Label htmlFor="slug">Slug</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="slug"
|
||||
value={product.slug}
|
||||
@@ -131,7 +134,9 @@ function EditProductForm({
|
||||
|
||||
{/* Name */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Product Name *</Label>
|
||||
<HelpTip label="Display name shown across the panel; renaming it never touches the slug.">
|
||||
<Label htmlFor="name">Product Name *</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
@@ -142,7 +147,9 @@ function EditProductForm({
|
||||
|
||||
{/* Description */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<HelpTip label="Optional context shown in the panel only — not surfaced to agents or used in task routing.">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
</HelpTip>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
@@ -153,15 +160,19 @@ function EditProductForm({
|
||||
|
||||
{/* Cell -> Project mapping */}
|
||||
<div className="grid gap-3">
|
||||
<Label>Cell Project Mapping</Label>
|
||||
<HelpTip label="Routes each cell's work for this product to its own repo; a cell left unmapped does no work for this product.">
|
||||
<Label>Cell Project Mapping</Label>
|
||||
</HelpTip>
|
||||
{cells.map((cell) => (
|
||||
<div key={cell.value} className="grid gap-2">
|
||||
<Label
|
||||
htmlFor={`cell-${cell.value}`}
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{cell.label}
|
||||
</Label>
|
||||
<HelpTip label={`Which project the ${cell.label} cell works on for this product.`}>
|
||||
<Label
|
||||
htmlFor={`cell-${cell.value}`}
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{cell.label}
|
||||
</Label>
|
||||
</HelpTip>
|
||||
<Select
|
||||
value={cellMapping[cell.value] ?? ""}
|
||||
onValueChange={(value) =>
|
||||
|
||||
@@ -126,9 +126,21 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Product</TableHead>
|
||||
<TableHead>Cells</TableHead>
|
||||
<TableHead>Progress</TableHead>
|
||||
<TableHead>
|
||||
<HelpTip label="Name and slug; click either to open the edit dialog.">
|
||||
<span>Product</span>
|
||||
</HelpTip>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<HelpTip label="Which project each cell (Backend/Frontend/UX-UI) works on for this product.">
|
||||
<span>Cells</span>
|
||||
</HelpTip>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<HelpTip label="Done / active / blocked counts across all mapped projects' tasks.">
|
||||
<span>Progress</span>
|
||||
</HelpTip>
|
||||
</TableHead>
|
||||
<TableHead className="w-[100px]">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -144,9 +156,11 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
|
||||
>
|
||||
{product.name}
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{product.slug}
|
||||
</p>
|
||||
<HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API.">
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{product.slug}
|
||||
</p>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -188,9 +202,11 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
|
||||
>
|
||||
{product.name}
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{product.slug}
|
||||
</p>
|
||||
<HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API.">
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{product.slug}
|
||||
</p>
|
||||
</HelpTip>
|
||||
</div>
|
||||
<HelpTip label="Edit product name, description, and cell-project mapping">
|
||||
<Button
|
||||
|
||||
@@ -121,12 +121,14 @@ export function CreateProjectDialog() {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
New Project
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<HelpTip label="Registers a git repository — sets cell ownership, branch/environment ladder, and CI/CD gate commands for agents to work on it.">
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
New Project
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</HelpTip>
|
||||
<DialogContent className="sm:max-w-[525px]">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
@@ -138,7 +140,9 @@ export function CreateProjectDialog() {
|
||||
<div className="grid gap-4 py-4">
|
||||
{/* Name */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Project Name *</Label>
|
||||
<HelpTip label="Display name shown across the panel and CEO approval queues; changing it later never touches the slug or workspace path already derived from it.">
|
||||
<Label htmlFor="name">Project Name *</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
@@ -149,7 +153,9 @@ export function CreateProjectDialog() {
|
||||
|
||||
{/* Slug */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="slug">Slug *</Label>
|
||||
<HelpTip label="Immutable after creation — composes each agent's workspace clone path (/data/workspaces/<slug>/<cell>/<agent>) and appears in every branch name for this project.">
|
||||
<Label htmlFor="slug">Slug *</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="slug"
|
||||
value={formData.slug}
|
||||
@@ -166,7 +172,9 @@ export function CreateProjectDialog() {
|
||||
|
||||
{/* Git URL */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="git_url">Git URL *</Label>
|
||||
<HelpTip label="Cloned into each assigned agent's workspace on first access; use HTTPS so the encrypted token below can authenticate clone, push, and PR operations.">
|
||||
<Label htmlFor="git_url">Git URL *</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="git_url"
|
||||
value={formData.git_url}
|
||||
@@ -205,7 +213,9 @@ export function CreateProjectDialog() {
|
||||
|
||||
{/* Assigned Cell */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="assigned_cell">Assigned Cell *</Label>
|
||||
<HelpTip label="Which cell owns this project — only that cell's agents can claim its tasks (enforced server-side, not just a UI filter).">
|
||||
<Label htmlFor="assigned_cell">Assigned Cell *</Label>
|
||||
</HelpTip>
|
||||
<Select
|
||||
value={formData.assigned_cell}
|
||||
onValueChange={(value: Team) =>
|
||||
@@ -227,7 +237,9 @@ export function CreateProjectDialog() {
|
||||
|
||||
{/* Default Branch */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="default_branch">Default Branch</Label>
|
||||
<HelpTip label="Used as both head and prod when no environment ladder is set below (a degenerate single-rung ladder) — the PR review gate diffs against it and releases cut from it.">
|
||||
<Label htmlFor="default_branch">Default Branch</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="default_branch"
|
||||
value={formData.default_branch}
|
||||
@@ -248,20 +260,24 @@ export function CreateProjectDialog() {
|
||||
/>
|
||||
|
||||
{/* Advanced Options Toggle */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="justify-start px-0 text-muted-foreground"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
>
|
||||
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
|
||||
</Button>
|
||||
<HelpTip label="Test/Format/Build are reference-only today; Lint + Typecheck (or Quality Gate below, which replaces both) run automatically at the dev's pre-submit gate.">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="justify-start px-0 text-muted-foreground"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
>
|
||||
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
|
||||
</Button>
|
||||
</HelpTip>
|
||||
|
||||
{showAdvanced && (
|
||||
<>
|
||||
{/* CI/CD Commands */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="test_command">Test Command</Label>
|
||||
<HelpTip label="Reference only — not yet wired into any automated gate or CI run by RoboCo itself.">
|
||||
<Label htmlFor="test_command">Test Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="test_command"
|
||||
value={formData.test_command || ""}
|
||||
@@ -273,7 +289,9 @@ export function CreateProjectDialog() {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="lint_command">Lint Command</Label>
|
||||
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Typecheck — unless Quality Gate Command below is set, which replaces both.">
|
||||
<Label htmlFor="lint_command">Lint Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="lint_command"
|
||||
value={formData.lint_command || ""}
|
||||
@@ -285,7 +303,9 @@ export function CreateProjectDialog() {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="format_command">Format Command</Label>
|
||||
<HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
|
||||
<Label htmlFor="format_command">Format Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="format_command"
|
||||
value={formData.format_command || ""}
|
||||
@@ -300,7 +320,9 @@ export function CreateProjectDialog() {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="typecheck_command">Typecheck Command</Label>
|
||||
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Lint — unless Quality Gate Command below is set, which replaces both.">
|
||||
<Label htmlFor="typecheck_command">Typecheck Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="typecheck_command"
|
||||
value={formData.typecheck_command || ""}
|
||||
@@ -315,7 +337,9 @@ export function CreateProjectDialog() {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="build_command">Build Command</Label>
|
||||
<HelpTip label="Reference only — not run automatically; the slow build/test suite is left to CI.">
|
||||
<Label htmlFor="build_command">Build Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="build_command"
|
||||
value={formData.build_command || ""}
|
||||
@@ -330,7 +354,9 @@ export function CreateProjectDialog() {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="quality_command">Quality Gate Command</Label>
|
||||
<HelpTip label="When set, replaces the Lint + Typecheck pair as the dev's complete pre-submit gate command.">
|
||||
<Label htmlFor="quality_command">Quality Gate Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="quality_command"
|
||||
value={formData.quality_command || ""}
|
||||
|
||||
@@ -271,7 +271,9 @@ function EditProjectForm({
|
||||
<div className="grid gap-4 py-4">
|
||||
{/* Slug (read-only) */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="slug">Slug</Label>
|
||||
<HelpTip label="Immutable — composes each agent's workspace clone path and appears in every branch name for this project. Set at creation, fixed here.">
|
||||
<Label htmlFor="slug">Slug</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="slug"
|
||||
value={project.slug}
|
||||
@@ -282,7 +284,9 @@ function EditProjectForm({
|
||||
|
||||
{/* Name */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Project Name *</Label>
|
||||
<HelpTip label="Display name shown across the panel and CEO approval queues; renaming it never touches the slug or workspace path.">
|
||||
<Label htmlFor="name">Project Name *</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
@@ -293,7 +297,9 @@ function EditProjectForm({
|
||||
|
||||
{/* Git URL */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="git_url">Git URL *</Label>
|
||||
<HelpTip label="Cloned into each assigned agent's workspace; use HTTPS so the token below can authenticate clone, push, and PR operations.">
|
||||
<Label htmlFor="git_url">Git URL *</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="git_url"
|
||||
value={gitUrl}
|
||||
@@ -348,9 +354,17 @@ function EditProjectForm({
|
||||
|
||||
{!clearToken && (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="git_token" className="text-sm">
|
||||
{project.has_git_token ? "Replace token" : "Set token"}
|
||||
</Label>
|
||||
<HelpTip
|
||||
label={
|
||||
project.has_git_token
|
||||
? "Overwrites the current token immediately on save; the previous token is discarded and cannot be recovered."
|
||||
: "Required for HTTPS clone/push/PR operations if the repo is private; stored Fernet-encrypted and never re-displayed once saved."
|
||||
}
|
||||
>
|
||||
<Label htmlFor="git_token" className="text-sm">
|
||||
{project.has_git_token ? "Replace token" : "Set token"}
|
||||
</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="git_token"
|
||||
type="password"
|
||||
@@ -368,7 +382,9 @@ function EditProjectForm({
|
||||
|
||||
{/* Assigned Cell */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="assigned_cell">Assigned Cell *</Label>
|
||||
<HelpTip label="Which cell owns this project — only that cell's agents can claim its tasks (enforced server-side, not just a UI filter).">
|
||||
<Label htmlFor="assigned_cell">Assigned Cell *</Label>
|
||||
</HelpTip>
|
||||
<Select
|
||||
value={assignedCell}
|
||||
onValueChange={(value: Team) => setAssignedCell(value)}
|
||||
@@ -388,7 +404,9 @@ function EditProjectForm({
|
||||
|
||||
{/* Default Branch */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="default_branch">Default Branch</Label>
|
||||
<HelpTip label="Used as both head and prod when no environment ladder is set below (a degenerate single-rung ladder) — the PR review gate diffs against it and releases cut from it.">
|
||||
<Label htmlFor="default_branch">Default Branch</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="default_branch"
|
||||
value={defaultBranch}
|
||||
@@ -419,19 +437,23 @@ function EditProjectForm({
|
||||
</div>
|
||||
|
||||
{/* Advanced Options Toggle */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="justify-start px-0 text-muted-foreground"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
>
|
||||
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
|
||||
</Button>
|
||||
<HelpTip label="Test/Format/Build are reference-only today; Lint + Typecheck (or Quality Gate below, which replaces both) run automatically at the dev's pre-submit gate.">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="justify-start px-0 text-muted-foreground"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
>
|
||||
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
|
||||
</Button>
|
||||
</HelpTip>
|
||||
|
||||
{showAdvanced && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="test_command">Test Command</Label>
|
||||
<HelpTip label="Reference only — not yet wired into any automated gate or CI run by RoboCo itself.">
|
||||
<Label htmlFor="test_command">Test Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="test_command"
|
||||
value={testCommand}
|
||||
@@ -441,7 +463,9 @@ function EditProjectForm({
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="lint_command">Lint Command</Label>
|
||||
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Typecheck — unless Quality Gate Command below is set, which replaces both.">
|
||||
<Label htmlFor="lint_command">Lint Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="lint_command"
|
||||
value={lintCommand}
|
||||
@@ -451,7 +475,9 @@ function EditProjectForm({
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="format_command">Format Command</Label>
|
||||
<HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
|
||||
<Label htmlFor="format_command">Format Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="format_command"
|
||||
value={formatCommand}
|
||||
@@ -461,7 +487,9 @@ function EditProjectForm({
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="typecheck_command">Typecheck Command</Label>
|
||||
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Lint — unless Quality Gate Command below is set, which replaces both.">
|
||||
<Label htmlFor="typecheck_command">Typecheck Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="typecheck_command"
|
||||
value={typecheckCommand}
|
||||
@@ -471,7 +499,9 @@ function EditProjectForm({
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="build_command">Build Command</Label>
|
||||
<HelpTip label="Reference only — not run automatically; the slow build/test suite is left to CI.">
|
||||
<Label htmlFor="build_command">Build Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="build_command"
|
||||
value={buildCommand}
|
||||
@@ -481,7 +511,9 @@ function EditProjectForm({
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="quality_command">Quality Gate Command</Label>
|
||||
<HelpTip label="When set, replaces the Lint + Typecheck pair as the dev's complete pre-submit gate command.">
|
||||
<Label htmlFor="quality_command">Quality Gate Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="quality_command"
|
||||
value={qualityCommand}
|
||||
@@ -497,14 +529,16 @@ function EditProjectForm({
|
||||
)}
|
||||
|
||||
{/* Autonomous Maintenance Toggle */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="justify-start px-0 text-muted-foreground"
|
||||
onClick={() => setShowAutonomy(!showAutonomy)}
|
||||
>
|
||||
{showAutonomy ? "Hide" : "Show"} Autonomous Maintenance
|
||||
</Button>
|
||||
<HelpTip label="CI-watch, video engine, dependency-update bot, and sandbox DB/Redis/Mongo opt-ins — each also needs its own fleet-wide flag armed to actually run.">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="justify-start px-0 text-muted-foreground"
|
||||
onClick={() => setShowAutonomy(!showAutonomy)}
|
||||
>
|
||||
{showAutonomy ? "Hide" : "Show"} Autonomous Maintenance
|
||||
</Button>
|
||||
</HelpTip>
|
||||
|
||||
{showAutonomy && (
|
||||
<>
|
||||
@@ -522,7 +556,9 @@ function EditProjectForm({
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="ci_watch_workflow">CI-watch Workflow</Label>
|
||||
<HelpTip label="Scopes CI-watch to one workflow's runs so a green run elsewhere can't mask a red one here; leave blank to fall back to the fleet default (ci.yml).">
|
||||
<Label htmlFor="ci_watch_workflow">CI-watch Workflow</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="ci_watch_workflow"
|
||||
value={ciWatchWorkflow}
|
||||
@@ -549,9 +585,11 @@ function EditProjectForm({
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="dep_update_command">
|
||||
Dependency-Update Command
|
||||
</Label>
|
||||
<HelpTip label="Dry-run only — the weekly bot runs this in a throwaway clone to detect a lockfile diff; nothing is committed until it opens a task that rides the normal PR-review flow.">
|
||||
<Label htmlFor="dep_update_command">
|
||||
Dependency-Update Command
|
||||
</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="dep_update_command"
|
||||
value={depUpdateCommand}
|
||||
@@ -565,9 +603,11 @@ function EditProjectForm({
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="dep_update_paths">
|
||||
Dependency-Update Lockfile Paths
|
||||
</Label>
|
||||
<HelpTip label="Which lockfile paths the dry-run diffs to detect a change; leave blank to auto-infer uv.lock / pnpm-lock.yaml.">
|
||||
<Label htmlFor="dep_update_paths">
|
||||
Dependency-Update Lockfile Paths
|
||||
</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="dep_update_paths"
|
||||
value={depUpdatePaths}
|
||||
|
||||
@@ -98,52 +98,83 @@ export function EnvironmentLadderEditor({
|
||||
<div className="flex flex-col">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
disabled={isFirst}
|
||||
onClick={() => handleMove(index, -1)}
|
||||
aria-label="Move earlier in the flow"
|
||||
>
|
||||
<ArrowUp className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{/* span-wrap: disabled Button has pointer-events-none,
|
||||
so the tooltip needs a hoverable wrapper to fire
|
||||
when isFirst disables the button. */}
|
||||
<span className="inline-block">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
disabled={isFirst}
|
||||
onClick={() => handleMove(index, -1)}
|
||||
aria-label="Move earlier in the flow"
|
||||
>
|
||||
<ArrowUp className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Move earlier in the flow</TooltipContent>
|
||||
<TooltipContent>
|
||||
{isFirst
|
||||
? "Already first — lands PRs, nothing to promote from."
|
||||
: "Move earlier in the flow"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
disabled={isLast}
|
||||
onClick={() => handleMove(index, 1)}
|
||||
aria-label="Move later in the flow"
|
||||
>
|
||||
<ArrowDown className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<span className="inline-block">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
disabled={isLast}
|
||||
onClick={() => handleMove(index, 1)}
|
||||
aria-label="Move later in the flow"
|
||||
>
|
||||
<ArrowDown className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Move later in the flow</TooltipContent>
|
||||
<TooltipContent>
|
||||
{isLast
|
||||
? "Already last — the release target, nothing further to promote to."
|
||||
: "Move later in the flow"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span className="w-20 text-center text-[10px] font-medium uppercase text-muted-foreground">
|
||||
{role}
|
||||
</span>
|
||||
<Input
|
||||
value={rung.name}
|
||||
onChange={(e) => handleUpdate(index, "name", e.target.value)}
|
||||
placeholder="Name (e.g. dev, qa, stag)"
|
||||
className="flex-1 h-8"
|
||||
/>
|
||||
<Input
|
||||
value={rung.branch}
|
||||
onChange={(e) => handleUpdate(index, "branch", e.target.value)}
|
||||
placeholder="Branch (e.g. dev, master)"
|
||||
className="flex-1 h-8"
|
||||
/>
|
||||
<HelpTip
|
||||
label={
|
||||
role
|
||||
? `This rung ${role === "PRs land" ? "is where PRs from this project land" : role === "release" ? "is where releases are cut and tagged" : "both receives PRs and is the release target (single-rung ladder)"}.`
|
||||
: "An intermediate rung — merged into from the rung above, promoted to the rung below."
|
||||
}
|
||||
>
|
||||
<span className="w-20 text-center text-[10px] font-medium uppercase text-muted-foreground">
|
||||
{role}
|
||||
</span>
|
||||
</HelpTip>
|
||||
<HelpTip label="Display label for this rung (e.g. 'dev', 'qa') — shown in the panel only, not matched against branch names.">
|
||||
<span className="flex-1">
|
||||
<Input
|
||||
value={rung.name}
|
||||
onChange={(e) => handleUpdate(index, "name", e.target.value)}
|
||||
placeholder="Name (e.g. dev, qa, stag)"
|
||||
className="h-8"
|
||||
/>
|
||||
</span>
|
||||
</HelpTip>
|
||||
<HelpTip label="The real git branch this rung maps to. Must be unique across rungs; saving rejects an empty or duplicate branch.">
|
||||
<span className="flex-1">
|
||||
<Input
|
||||
value={rung.branch}
|
||||
onChange={(e) => handleUpdate(index, "branch", e.target.value)}
|
||||
placeholder="Branch (e.g. dev, master)"
|
||||
className="h-8"
|
||||
/>
|
||||
</span>
|
||||
</HelpTip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -166,10 +197,12 @@ export function EnvironmentLadderEditor({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="button" variant="outline" size="sm" onClick={handleAdd}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add rung
|
||||
</Button>
|
||||
<HelpTip label="Appends a blank rung at the bottom of the ladder (the new release target) — fill in its name/branch, then reorder with the arrows.">
|
||||
<Button type="button" variant="outline" size="sm" onClick={handleAdd}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add rung
|
||||
</Button>
|
||||
</HelpTip>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Top to bottom is the promotion flow: <strong>PRs land</strong> on the
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Search, Filter, X } from "lucide-react";
|
||||
import { Team } from "@/types";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
|
||||
interface ProjectFiltersProps {
|
||||
searchQuery: string;
|
||||
@@ -57,7 +58,9 @@ export function ProjectFilters({
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<HelpTip label="Filters the list below by project name (client-side, case-insensitive substring match).">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
</HelpTip>
|
||||
<Input
|
||||
placeholder="Search projects..."
|
||||
value={searchQuery}
|
||||
@@ -68,17 +71,19 @@ export function ProjectFilters({
|
||||
|
||||
{/* Cell Filter */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
<Filter className="h-4 w-4" />
|
||||
Cell
|
||||
{cellFilter.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{cellFilter.length}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<HelpTip label="Show only projects assigned to the checked cells (any match).">
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
<Filter className="h-4 w-4" />
|
||||
Cell
|
||||
{cellFilter.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{cellFilter.length}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</HelpTip>
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
<DropdownMenuLabel>Filter by Cell</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -95,20 +100,24 @@ export function ProjectFilters({
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Show Inactive Toggle */}
|
||||
<Button
|
||||
variant={showInactive ? "default" : "outline"}
|
||||
onClick={() => onShowInactiveChange(!showInactive)}
|
||||
className="gap-2"
|
||||
>
|
||||
{showInactive ? "Showing Inactive" : "Show Inactive"}
|
||||
</Button>
|
||||
<HelpTip label="Include inactive projects — hidden from the default list and skipped as the fallback for idle-agent spawns.">
|
||||
<Button
|
||||
variant={showInactive ? "default" : "outline"}
|
||||
onClick={() => onShowInactiveChange(!showInactive)}
|
||||
className="gap-2"
|
||||
>
|
||||
{showInactive ? "Showing Inactive" : "Show Inactive"}
|
||||
</Button>
|
||||
</HelpTip>
|
||||
|
||||
{/* Clear Filters */}
|
||||
{hasActiveFilters && (
|
||||
<Button variant="ghost" onClick={clearFilters} className="gap-2">
|
||||
<X className="h-4 w-4" />
|
||||
Clear
|
||||
</Button>
|
||||
<HelpTip label="Resets search, cell filter, and Show Inactive back to defaults.">
|
||||
<Button variant="ghost" onClick={clearFilters} className="gap-2">
|
||||
<X className="h-4 w-4" />
|
||||
Clear
|
||||
</Button>
|
||||
</HelpTip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { FolderGit2, X } from "lucide-react";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
|
||||
interface ProjectSelectorProps {
|
||||
value: string | null;
|
||||
@@ -110,10 +111,12 @@ export function ProjectSelector({
|
||||
<FolderGit2 className="h-4 w-4" />
|
||||
<span>{selectedProject.name}</span>
|
||||
{selectedProject.assigned_cell && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{TEAM_LABELS[selectedProject.assigned_cell] ||
|
||||
selectedProject.assigned_cell}
|
||||
</Badge>
|
||||
<HelpTip label="The cell that owns this project — only its agents can claim tasks against it.">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{TEAM_LABELS[selectedProject.assigned_cell] ||
|
||||
selectedProject.assigned_cell}
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -180,11 +180,31 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Project</TableHead>
|
||||
<TableHead>Cell</TableHead>
|
||||
<TableHead>Tasks</TableHead>
|
||||
<TableHead>Token</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>
|
||||
<HelpTip label="Name and slug; click either to open the edit dialog.">
|
||||
<span>Project</span>
|
||||
</HelpTip>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<HelpTip label="The team that owns this project — only that cell's agents can claim its tasks.">
|
||||
<span>Cell</span>
|
||||
</HelpTip>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<HelpTip label="Done / active / blocked counts across all of this project's tasks.">
|
||||
<span>Tasks</span>
|
||||
</HelpTip>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<HelpTip label="Whether a GitHub PAT is configured for clone/push/PR operations.">
|
||||
<span>Token</span>
|
||||
</HelpTip>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<HelpTip label="Inactive projects are hidden from the default list and skipped as the fallback for idle-agent spawns.">
|
||||
<span>Status</span>
|
||||
</HelpTip>
|
||||
</TableHead>
|
||||
<TableHead className="w-[100px]">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -200,9 +220,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
|
||||
>
|
||||
{project.name}
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{project.slug}
|
||||
</p>
|
||||
<HelpTip label="Composes each agent's workspace clone path and every branch name for this project.">
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{project.slug}
|
||||
</p>
|
||||
</HelpTip>
|
||||
{project.ci_watch_enabled && (
|
||||
<div className="mt-1">
|
||||
<CiWatchBadge enabled />
|
||||
@@ -211,9 +233,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={teamColors[project.assigned_cell]}>
|
||||
{teamLabels[project.assigned_cell]}
|
||||
</Badge>
|
||||
<HelpTip label="Only this cell's agents can claim tasks on this project.">
|
||||
<Badge className={teamColors[project.assigned_cell]}>
|
||||
{teamLabels[project.assigned_cell]}
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TasksCell counts={project.task_counts} />
|
||||
@@ -271,9 +295,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
|
||||
>
|
||||
{project.name}
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{project.slug}
|
||||
</p>
|
||||
<HelpTip label="Composes each agent's workspace clone path and every branch name for this project.">
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{project.slug}
|
||||
</p>
|
||||
</HelpTip>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<HelpTip label="Edit project settings and CI/CD commands">
|
||||
@@ -306,9 +332,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
|
||||
</div>
|
||||
<div className="mt-3 divide-y">
|
||||
<ResponsiveTableCardRow label="Cell">
|
||||
<Badge className={teamColors[project.assigned_cell]}>
|
||||
{teamLabels[project.assigned_cell]}
|
||||
</Badge>
|
||||
<HelpTip label="Only this cell's agents can claim tasks on this project.">
|
||||
<Badge className={teamColors[project.assigned_cell]}>
|
||||
{teamLabels[project.assigned_cell]}
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
</ResponsiveTableCardRow>
|
||||
<ResponsiveTableCardRow label="Tasks">
|
||||
<TasksCell counts={project.task_counts} />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useRateLimitStore } from "@/store/rate-limit-store";
|
||||
import { useRateLimitSync } from "@/hooks/use-rate-limit-sync";
|
||||
import { useRateLimitWebSocket } from "@/hooks/use-rate-limit-websocket";
|
||||
import type { RateLimitEntry } from "@/types/rate-limits";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
|
||||
// =============================================================================
|
||||
// Countdown row for a single rate-limited provider
|
||||
@@ -37,18 +38,26 @@ function RateLimitRow({ entry }: { entry: RateLimitEntry }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-2 bg-amber-50 border-b border-amber-300 last:border-b-0">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 shrink-0" />
|
||||
<span className="text-sm font-medium text-amber-900">
|
||||
{entry.provider}
|
||||
</span>
|
||||
{agentCount > 0 && (
|
||||
<span className="text-sm text-amber-700">
|
||||
{agentCount} agent{agentCount !== 1 ? "s" : ""} affected
|
||||
<HelpTip label="This AI provider hit a rate limit or a persistent overload; every other provider keeps working normally.">
|
||||
<span className="text-sm font-medium text-amber-900">
|
||||
{entry.provider}
|
||||
</span>
|
||||
</HelpTip>
|
||||
{agentCount > 0 && (
|
||||
<HelpTip label="Agents that were mid-task on this provider when the limit hit; each resumes automatically once it lifts — no work is lost.">
|
||||
<span className="text-sm text-amber-700">
|
||||
{agentCount} agent{agentCount !== 1 ? "s" : ""} affected
|
||||
</span>
|
||||
</HelpTip>
|
||||
)}
|
||||
<span className="text-sm text-amber-700">{secondsLeft}s</span>
|
||||
<span className="text-sm text-amber-800 font-medium ml-auto">
|
||||
operations paused — resuming automatically
|
||||
</span>
|
||||
<HelpTip label="Estimated time until RoboCo retries this provider, based on the retry-after it reported.">
|
||||
<span className="text-sm text-amber-700">{secondsLeft}s</span>
|
||||
</HelpTip>
|
||||
<HelpTip label="New spawns for this provider are queued, not dropped — a background probe-and-resume loop revives parked agents as soon as it's confirmed clear.">
|
||||
<span className="text-sm text-amber-800 font-medium ml-auto">
|
||||
operations paused — resuming automatically
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -527,6 +527,7 @@ export function AIRoutingCard() {
|
||||
active={currentMode === "self_hosted"}
|
||||
onClick={flipToSelfHosted}
|
||||
disabled={applyMode.isPending || !isSelfHostedConnected}
|
||||
labelHint="Unlike Ollama/Grok, self-hosted has no built-in fallback model — pick one in the picker below or the switch fails server-side."
|
||||
/>
|
||||
<ModeButton
|
||||
icon={<Cpu className="h-4 w-4" />}
|
||||
@@ -561,9 +562,11 @@ export function AIRoutingCard() {
|
||||
<>
|
||||
<Separator />
|
||||
<section className="space-y-2">
|
||||
<Label className="text-sm font-medium">
|
||||
Self-Hosted default model
|
||||
</Label>
|
||||
<HelpTip label="Self-hosted has no automatic fallback — clearing this back to '(use server default)' will fail the next time Self-Hosted mode is applied.">
|
||||
<Label className="text-sm font-medium">
|
||||
Self-Hosted default model
|
||||
</Label>
|
||||
</HelpTip>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose which discovered model all agents should use in
|
||||
self-hosted mode.
|
||||
@@ -782,6 +785,7 @@ function ModeButton({
|
||||
disabled,
|
||||
onClick,
|
||||
highlight,
|
||||
labelHint,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
@@ -790,6 +794,8 @@ function ModeButton({
|
||||
disabled: boolean;
|
||||
onClick: () => void | Promise<void>;
|
||||
highlight?: boolean;
|
||||
/** Extra hover context on the label, e.g. a gotcha the always-visible description doesn't cover. */
|
||||
labelHint?: string;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
@@ -804,11 +810,15 @@ function ModeButton({
|
||||
>
|
||||
<div className="flex w-full items-center gap-2 text-sm font-medium">
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
<HelpTip label={labelHint}>
|
||||
<span>{label}</span>
|
||||
</HelpTip>
|
||||
{active ? (
|
||||
<Badge className="ml-auto bg-primary/15 text-primary border-0 text-xs">
|
||||
active
|
||||
</Badge>
|
||||
<HelpTip label="Currently applied fleet-wide — every agent spawns on this provider until the mode changes again.">
|
||||
<Badge className="ml-auto bg-primary/15 text-primary border-0 text-xs">
|
||||
active
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground font-normal">
|
||||
|
||||
@@ -289,7 +289,13 @@ export function FeatureFlagsCard() {
|
||||
size="sm"
|
||||
className="w-full justify-between px-2 text-muted-foreground"
|
||||
>
|
||||
<span className="text-sm">X (Twitter) credentials</span>
|
||||
{/* Tip goes on the inner span, not the
|
||||
CollapsibleTrigger asChild Button — wrapping the
|
||||
trigger itself would clobber its open/closed
|
||||
data-state (same trap as Switch/TabsTrigger). */}
|
||||
<HelpTip label="Only takes effect once x_engine_enabled above is on.">
|
||||
<span className="text-sm">X (Twitter) credentials</span>
|
||||
</HelpTip>
|
||||
{xCredsOpen ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
@@ -314,7 +320,9 @@ export function FeatureFlagsCard() {
|
||||
size="sm"
|
||||
className="w-full justify-between px-2 text-muted-foreground"
|
||||
>
|
||||
<span className="text-sm">TikTok credentials</span>
|
||||
<HelpTip label="Only used for the TikTok distribution leg — needs video_engine_enabled above on, and video only posts once you approve a rendered draft.">
|
||||
<span className="text-sm">TikTok credentials</span>
|
||||
</HelpTip>
|
||||
{tiktokCredsOpen ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
@@ -339,7 +347,9 @@ export function FeatureFlagsCard() {
|
||||
size="sm"
|
||||
className="w-full justify-between px-2 text-muted-foreground"
|
||||
>
|
||||
<span className="text-sm">Telegram credentials</span>
|
||||
<HelpTip label="Only takes effect once telegram_enabled above is on — in-app notifications keep working either way.">
|
||||
<span className="text-sm">Telegram credentials</span>
|
||||
</HelpTip>
|
||||
{telegramCredsOpen ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
|
||||
@@ -145,16 +145,22 @@ export function SelfHostedSection({
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
<Label className="text-sm font-medium">Self-Hosted LLM</Label>
|
||||
<HelpTip label="Routes agents to any locally-run OpenAI-compatible endpoint (Ollama, vLLM, LM Studio) instead of a cloud provider.">
|
||||
<Label className="text-sm font-medium">Self-Hosted LLM</Label>
|
||||
</HelpTip>
|
||||
{testResult?.ok === true && (
|
||||
<Badge className="bg-emerald-500/10 text-emerald-600 border-0">
|
||||
<CheckCircle2 className="h-3 w-3" /> connected
|
||||
</Badge>
|
||||
<HelpTip label="The last Test Connection call succeeded — the model list below reflects this endpoint.">
|
||||
<Badge className="bg-emerald-500/10 text-emerald-600 border-0">
|
||||
<CheckCircle2 className="h-3 w-3" /> connected
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
{testResult?.ok === false && (
|
||||
<Badge className="bg-red-500/10 text-red-600 border-0">
|
||||
<XCircle className="h-3 w-3" /> error
|
||||
</Badge>
|
||||
<HelpTip label="The last Test Connection call failed — see the error detail below.">
|
||||
<Badge className="bg-red-500/10 text-red-600 border-0">
|
||||
<XCircle className="h-3 w-3" /> error
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -216,9 +222,11 @@ export function SelfHostedSection({
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</div>
|
||||
<Button onClick={handleSave} disabled={saveConfig.isPending}>
|
||||
{saveConfig.isPending ? "Saving…" : "Save"}
|
||||
</Button>
|
||||
<HelpTip label="Saves the Base URL above together with any token typed here, in one combined config — not just this token.">
|
||||
<Button onClick={handleSave} disabled={saveConfig.isPending}>
|
||||
{saveConfig.isPending ? "Saving…" : "Save"}
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</div>
|
||||
{config?.has_token && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -336,24 +344,26 @@ export function SelfHostedSection({
|
||||
</span>
|
||||
</HelpTip>
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshModels.isPending}
|
||||
>
|
||||
{refreshModels.isPending ? (
|
||||
<>
|
||||
<RefreshCw className="mr-1.5 h-3 w-3 animate-spin" />
|
||||
Refreshing…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="mr-1.5 h-3 w-3" />
|
||||
Refresh Models
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<HelpTip label="Re-queries the endpoint's model list only — doesn't re-run the connection test above.">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshModels.isPending}
|
||||
>
|
||||
{refreshModels.isPending ? (
|
||||
<>
|
||||
<RefreshCw className="mr-1.5 h-3 w-3 animate-spin" />
|
||||
Refreshing…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="mr-1.5 h-3 w-3" />
|
||||
Refresh Models
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</div>
|
||||
<div className="divide-y rounded-md border">
|
||||
{models.map((m) => (
|
||||
|
||||
@@ -69,19 +69,22 @@ export function TelegramCredentialsForm() {
|
||||
|
||||
<div className="flex items-center gap-2 rounded-md border p-3">
|
||||
{status?.has_credentials ? (
|
||||
<>
|
||||
<HelpTip label="Bot token + chat id are both stored — CEO notifications also DM via Telegram alongside the in-app bell.">
|
||||
<Key className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
Credentials are set
|
||||
</span>
|
||||
</>
|
||||
</HelpTip>
|
||||
) : (
|
||||
<>
|
||||
<HelpTip label="Missing — notifications stay in-app only; nothing sends over Telegram until both fields below are set.">
|
||||
<KeyRound className="h-4 w-4 text-amber-500" />
|
||||
<span className="text-sm text-amber-600 dark:text-amber-400">
|
||||
{isLoading ? "Checking..." : "No credentials configured"}
|
||||
</span>
|
||||
</>
|
||||
</HelpTip>
|
||||
)}
|
||||
{status?.has_credentials ? (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
Credentials are set
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-amber-600 dark:text-amber-400">
|
||||
{isLoading ? "Checking..." : "No credentials configured"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -88,19 +88,22 @@ export function TikTokCredentialsForm() {
|
||||
|
||||
<div className="flex items-center gap-2 rounded-md border p-3">
|
||||
{status?.has_credentials ? (
|
||||
<>
|
||||
<HelpTip label="All 4 OAuth2 secrets are stored — approved video drafts can post to TikTok's inbox-upload.">
|
||||
<Key className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
Credentials are set
|
||||
</span>
|
||||
</>
|
||||
</HelpTip>
|
||||
) : (
|
||||
<>
|
||||
<HelpTip label="Missing at least one secret — video drafts still render and queue but can't post to TikTok until all 4 are set.">
|
||||
<KeyRound className="h-4 w-4 text-amber-500" />
|
||||
<span className="text-sm text-amber-600 dark:text-amber-400">
|
||||
{isLoading ? "Checking..." : "No credentials configured"}
|
||||
</span>
|
||||
</>
|
||||
</HelpTip>
|
||||
)}
|
||||
{status?.has_credentials ? (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
Credentials are set
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-amber-600 dark:text-amber-400">
|
||||
{isLoading ? "Checking..." : "No credentials configured"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -86,19 +86,22 @@ export function XCredentialsForm() {
|
||||
|
||||
<div className="flex items-center gap-2 rounded-md border p-3">
|
||||
{status?.has_credentials ? (
|
||||
<>
|
||||
<HelpTip label="All 4 secrets are stored — approved posts in the X queue can actually publish.">
|
||||
<Key className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
Credentials are set
|
||||
</span>
|
||||
</>
|
||||
</HelpTip>
|
||||
) : (
|
||||
<>
|
||||
<HelpTip label="Missing at least one secret — drafts still queue for review but can't post until all 4 are set.">
|
||||
<KeyRound className="h-4 w-4 text-amber-500" />
|
||||
<span className="text-sm text-amber-600 dark:text-amber-400">
|
||||
{isLoading ? "Checking..." : "No credentials configured"}
|
||||
</span>
|
||||
</>
|
||||
</HelpTip>
|
||||
)}
|
||||
{status?.has_credentials ? (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
Credentials are set
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-amber-600 dark:text-amber-400">
|
||||
{isLoading ? "Checking..." : "No credentials configured"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user