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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { OfflineState } from "@/components/ui/offline-state";
|
import { OfflineState } from "@/components/ui/offline-state";
|
||||||
import { HelpTip } from "@/components/ui/help-tip";
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { usePageRefresh } from "@/hooks";
|
import { usePageRefresh } from "@/hooks";
|
||||||
import {
|
import {
|
||||||
Bell,
|
Bell,
|
||||||
@@ -114,20 +119,26 @@ function NotificationCard({
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="font-medium">{notification.subject}</span>
|
<span className="font-medium">{notification.subject}</span>
|
||||||
<Badge
|
<HelpTip label="How urgently the sender flagged this — doesn't change what you need to do, just how prominently it's shown.">
|
||||||
className={priorityColors[notification.priority] + " text-xs"}
|
<Badge
|
||||||
>
|
className={priorityColors[notification.priority] + " text-xs"}
|
||||||
{notification.priority}
|
>
|
||||||
</Badge>
|
{notification.priority}
|
||||||
{!notification.is_read && (
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
New
|
|
||||||
</Badge>
|
</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 && (
|
{notification.requires_ack && !notification.is_acknowledged && (
|
||||||
<Badge variant="destructive" className="text-xs">
|
<HelpTip label="A formal signal from a PM/Board role — needs an explicit acknowledgement, tracked separately from read status.">
|
||||||
Needs Ack
|
<Badge variant="destructive" className="text-xs">
|
||||||
</Badge>
|
Needs Ack
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
{notification.related_task_id && (
|
{notification.related_task_id && (
|
||||||
<Link
|
<Link
|
||||||
@@ -329,16 +340,51 @@ function NotificationsPageContent() {
|
|||||||
) : (
|
) : (
|
||||||
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="all">All</TabsTrigger>
|
{/* TooltipTrigger's asChild Slot merge injects its own data-state
|
||||||
<TabsTrigger value="unread">
|
onto the trigger, colliding with Radix Tabs' own literal
|
||||||
Unread {data && data.unread_count > 0 && `(${data.unread_count})`}
|
data-state (same trap as task-tabs.tsx) — reassert the real
|
||||||
</TabsTrigger>
|
selection state explicitly so the active-tab highlight
|
||||||
<TabsTrigger value="pending">
|
survives the merge. */}
|
||||||
Pending{" "}
|
<Tooltip>
|
||||||
{data &&
|
<TooltipTrigger asChild>
|
||||||
data.pending_ack_count > 0 &&
|
<TabsTrigger
|
||||||
`(${data.pending_ack_count})`}
|
value="all"
|
||||||
</TabsTrigger>
|
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>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value={activeTab} className="mt-4 space-y-3">
|
<TabsContent value={activeTab} className="mt-4 space-y-3">
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ export default function SettingsPage() {
|
|||||||
<Separator />
|
<Separator />
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<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">
|
<p className="text-sm text-muted-foreground">
|
||||||
Show icons only in the sidebar
|
Show icons only in the sidebar
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ interface DeliverySectionProps {
|
|||||||
function DeliverySection({ delivery }: DeliverySectionProps) {
|
function DeliverySection({ delivery }: DeliverySectionProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<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">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||||
<DeliveryMetric
|
<DeliveryMetric
|
||||||
label="In flight"
|
label="In flight"
|
||||||
@@ -147,14 +149,18 @@ function SpendSection({ spend }: SpendSectionProps) {
|
|||||||
<SectionLabel>Spend</SectionLabel>
|
<SectionLabel>Spend</SectionLabel>
|
||||||
<div className="rounded-lg border p-3 space-y-1.5">
|
<div className="rounded-lg border p-3 space-y-1.5">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<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">
|
<span className="font-medium tabular-nums">
|
||||||
${spend_30d_usd.toFixed(2)}
|
${spend_30d_usd.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{projected_monthly_usd !== null && (
|
{projected_monthly_usd !== null && (
|
||||||
<div className="flex items-center justify-between text-sm">
|
<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">
|
<span className="font-medium tabular-nums">
|
||||||
${projected_monthly_usd.toFixed(2)}
|
${projected_monthly_usd.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -101,16 +101,20 @@ function ObjectivesEditor({
|
|||||||
<span className="text-xs font-medium text-muted-foreground">
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
Objective #{rowIdx + 1}
|
Objective #{rowIdx + 1}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<HelpTip label="Deletes this row locally — nothing changes until you click Save charter">
|
||||||
type="button"
|
<span className="inline-block">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
type="button"
|
||||||
disabled={disabled}
|
variant="ghost"
|
||||||
onClick={() => removeRow(rowIdx)}
|
size="sm"
|
||||||
className="h-6 px-2 text-xs text-destructive hover:text-destructive"
|
disabled={disabled}
|
||||||
>
|
onClick={() => removeRow(rowIdx)}
|
||||||
Remove
|
className="h-6 px-2 text-xs text-destructive hover:text-destructive"
|
||||||
</Button>
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
{keys.map((key) => (
|
{keys.map((key) => (
|
||||||
<div key={key} className="space-y-1">
|
<div key={key} className="space-y-1">
|
||||||
@@ -132,15 +136,19 @@ function ObjectivesEditor({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<HelpTip label="Adds a blank row using the same field keys as the first objective">
|
||||||
type="button"
|
<span className="inline-block">
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
type="button"
|
||||||
disabled={disabled}
|
variant="outline"
|
||||||
onClick={addRow}
|
size="sm"
|
||||||
>
|
disabled={disabled}
|
||||||
+ Add objective
|
onClick={addRow}
|
||||||
</Button>
|
>
|
||||||
|
+ Add objective
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -297,7 +305,9 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
|
|||||||
<CardContent className="space-y-5">
|
<CardContent className="space-y-5">
|
||||||
{/* North star */}
|
{/* North star */}
|
||||||
<div className="space-y-2">
|
<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
|
<Textarea
|
||||||
id="north-star"
|
id="north-star"
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -325,7 +335,9 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
|
|||||||
|
|
||||||
{/* Constraints */}
|
{/* Constraints */}
|
||||||
<div className="space-y-2">
|
<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
|
<Textarea
|
||||||
id="constraints"
|
id="constraints"
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -361,10 +373,14 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Save */}
|
{/* Save */}
|
||||||
<Button onClick={handleSave} disabled={saving}>
|
<HelpTip label="Saves north star, brand voice, objectives, constraints, and operating policy in one update">
|
||||||
<Save className="h-4 w-4 mr-2" />
|
<span className="inline-block">
|
||||||
{saving ? "Saving…" : "Save charter"}
|
<Button onClick={handleSave} disabled={saving}>
|
||||||
</Button>
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
{saving ? "Saving…" : "Save charter"}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
|
|
||||||
{/* Metadata */}
|
{/* Metadata */}
|
||||||
<div className="border-t pt-4 space-y-1 text-xs text-muted-foreground">
|
<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 && (
|
{goals.updated_by && (
|
||||||
<p>
|
<p>
|
||||||
Updated by:{" "}
|
Updated by:{" "}
|
||||||
<span className="font-medium text-foreground font-mono">
|
<HelpTip label="The agent or CEO identity that last saved this charter">
|
||||||
{goals.updated_by}
|
<span className="font-medium text-foreground font-mono">
|
||||||
</span>
|
{goals.updated_by}
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -89,9 +89,12 @@ function PitchCard({ pitch, onApprove, onReject, busy }: PitchCardProps) {
|
|||||||
{pitch.target_cells.length > 0 && (
|
{pitch.target_cells.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1 mt-1">
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
{pitch.target_cells.map((c) => (
|
{pitch.target_cells.map((c) => (
|
||||||
<Badge key={c} variant="outline">
|
<HelpTip
|
||||||
{c}
|
key={c}
|
||||||
</Badge>
|
label="A cell this pitch would route delivery work to if approved"
|
||||||
|
>
|
||||||
|
<Badge variant="outline">{c}</Badge>
|
||||||
|
</HelpTip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -111,21 +114,29 @@ function PitchCard({ pitch, onApprove, onReject, busy }: PitchCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
{proposed ? (
|
{proposed ? (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<HelpTip label="Auto-provisions a new product + workspace for the target cells listed above">
|
||||||
size="sm"
|
<span className="inline-block">
|
||||||
disabled={busy}
|
<Button
|
||||||
onClick={() => setApproveOpen(true)}
|
size="sm"
|
||||||
>
|
disabled={busy}
|
||||||
<Check className="mr-1 h-4 w-4" /> Approve & provision
|
onClick={() => setApproveOpen(true)}
|
||||||
</Button>
|
>
|
||||||
<Button
|
<Check className="mr-1 h-4 w-4" /> Approve & provision
|
||||||
size="sm"
|
</Button>
|
||||||
variant="outline"
|
</span>
|
||||||
disabled={busy}
|
</HelpTip>
|
||||||
onClick={() => setRejectOpen(true)}
|
<HelpTip label="Rejects without creating anything — recorded in the decision log below">
|
||||||
>
|
<span className="inline-block">
|
||||||
<X className="mr-1 h-4 w-4" /> Reject
|
<Button
|
||||||
</Button>
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => setRejectOpen(true)}
|
||||||
|
>
|
||||||
|
<X className="mr-1 h-4 w-4" /> Reject
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
pitch.decision_notes && (
|
pitch.decision_notes && (
|
||||||
|
|||||||
@@ -67,21 +67,29 @@ function DirectiveCard({
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-2 pt-1">
|
<div className="flex gap-2 pt-1">
|
||||||
<Button
|
<HelpTip label="Executes this directive as drafted (relay a message, update the charter, approve a pitch, or control a task)">
|
||||||
size="sm"
|
<span className="inline-block">
|
||||||
disabled={busy}
|
<Button
|
||||||
onClick={() => onConfirm(directive.id)}
|
size="sm"
|
||||||
>
|
disabled={busy}
|
||||||
<Check className="mr-1 h-4 w-4" /> Confirm
|
onClick={() => onConfirm(directive.id)}
|
||||||
</Button>
|
>
|
||||||
<Button
|
<Check className="mr-1 h-4 w-4" /> Confirm
|
||||||
size="sm"
|
</Button>
|
||||||
variant="outline"
|
</span>
|
||||||
disabled={busy}
|
</HelpTip>
|
||||||
onClick={() => setRejectOpen(true)}
|
<HelpTip label="Discards the directive — the Secretary must re-propose it to try again">
|
||||||
>
|
<span className="inline-block">
|
||||||
<X className="mr-1 h-4 w-4" /> Reject
|
<Button
|
||||||
</Button>
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => setRejectOpen(true)}
|
||||||
|
>
|
||||||
|
<X className="mr-1 h-4 w-4" /> Reject
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -244,9 +252,11 @@ export function SecretaryTab() {
|
|||||||
<CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
|
<CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
|
||||||
<CardTitle>Chat</CardTitle>
|
<CardTitle>Chat</CardTitle>
|
||||||
{sessionId && (
|
{sessionId && (
|
||||||
<Button variant="outline" size="sm" onClick={() => void stop()}>
|
<HelpTip label="Closes this chat — your next message starts a fresh Secretary session">
|
||||||
End session
|
<Button variant="outline" size="sm" onClick={() => void stop()}>
|
||||||
</Button>
|
End session
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-1 flex-col gap-4">
|
<CardContent className="flex flex-1 flex-col gap-4">
|
||||||
@@ -284,18 +294,22 @@ export function SecretaryTab() {
|
|||||||
</Button>
|
</Button>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<HelpTip label="Opens a new Secretary chat session — reads live company state, not a scripted bot">
|
||||||
onClick={() => void handleStart()}
|
<span className="inline-block">
|
||||||
disabled={starting}
|
<Button
|
||||||
className="h-11 shrink-0 px-6"
|
onClick={() => void handleStart()}
|
||||||
aria-label={starting ? "Starting…" : undefined}
|
disabled={starting}
|
||||||
>
|
className="h-11 shrink-0 px-6"
|
||||||
{starting ? (
|
aria-label={starting ? "Starting…" : undefined}
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
>
|
||||||
) : (
|
{starting ? (
|
||||||
"Start"
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
)}
|
) : (
|
||||||
</Button>
|
"Start"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ const FORBIDDABLE_KINDS: DefinitionKind[] = [
|
|||||||
"component",
|
"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 {
|
function actionToast(verb: string, result: ConventionsActionResult): void {
|
||||||
if (result.created && result.pr_number != null) {
|
if (result.created && result.pr_number != null) {
|
||||||
toast.success(
|
toast.success(
|
||||||
@@ -189,50 +198,59 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
|||||||
className="space-y-2 rounded-md border border-border p-3"
|
className="space-y-2 rounded-md border border-border p-3"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<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
|
<Input
|
||||||
value={module.path}
|
value={module.purpose}
|
||||||
placeholder="path/to/module"
|
placeholder="what this module is for"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateModule(index, { path: e.target.value })
|
updateModule(index, { purpose: e.target.value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
</HelpTip>
|
||||||
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 })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{FORBIDDABLE_KINDS.map((kind) => (
|
{FORBIDDABLE_KINDS.map((kind) => (
|
||||||
<Badge
|
<HelpTip key={kind} label={KIND_HINTS[kind]}>
|
||||||
key={kind}
|
<Badge
|
||||||
variant={
|
variant={
|
||||||
module.forbidden.includes(kind)
|
module.forbidden.includes(kind)
|
||||||
? "destructive"
|
? "destructive"
|
||||||
: "outline"
|
: "outline"
|
||||||
}
|
}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => toggleForbidden(index, kind)}
|
onClick={() => toggleForbidden(index, kind)}
|
||||||
>
|
>
|
||||||
no {kind}
|
no {kind}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={addModule}>
|
<HelpTip label="Adds a blank module row to constrain another directory">
|
||||||
Add module
|
<Button variant="outline" size="sm" onClick={addModule}>
|
||||||
</Button>
|
Add module
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -253,9 +271,17 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
|||||||
>
|
>
|
||||||
<span className="text-sm">{rule.name.replace(/_/g, " ")}</span>
|
<span className="text-sm">{rule.name.replace(/_/g, " ")}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="w-10 text-right text-xs text-muted-foreground">
|
<HelpTip
|
||||||
{rule.level}
|
label={
|
||||||
</span>
|
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
|
<Switch
|
||||||
checked={rule.level === "block"}
|
checked={rule.level === "block"}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
@@ -285,34 +311,50 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
|||||||
className="space-y-2 rounded-md border border-border p-3"
|
className="space-y-2 rounded-md border border-border p-3"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<HelpTip label="Repo-relative path or glob this waiver exempts from the rule on the right">
|
||||||
value={waiver.path}
|
<Input
|
||||||
placeholder="path/to/file.py"
|
value={waiver.path}
|
||||||
onChange={(e) => updateWaiver(index, { path: e.target.value })}
|
placeholder="path/to/file.py"
|
||||||
/>
|
onChange={(e) =>
|
||||||
<Input
|
updateWaiver(index, { path: e.target.value })
|
||||||
value={waiver.rule}
|
}
|
||||||
placeholder="rule name"
|
/>
|
||||||
onChange={(e) => updateWaiver(index, { rule: e.target.value })}
|
</HelpTip>
|
||||||
/>
|
<HelpTip label="Must match a rule name above exactly for the waiver to apply">
|
||||||
<Button
|
<Input
|
||||||
variant="ghost"
|
value={waiver.rule}
|
||||||
size="sm"
|
placeholder="rule name"
|
||||||
onClick={() => removeWaiver(index)}
|
onChange={(e) =>
|
||||||
>
|
updateWaiver(index, { rule: e.target.value })
|
||||||
Remove
|
}
|
||||||
</Button>
|
/>
|
||||||
|
</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>
|
</div>
|
||||||
<Input
|
<HelpTip label="Accountability note — committed with the waiver and reviewed in the PR, never a silent suppression">
|
||||||
value={waiver.reason}
|
<Input
|
||||||
placeholder="why this is waived"
|
value={waiver.reason}
|
||||||
onChange={(e) => updateWaiver(index, { reason: e.target.value })}
|
placeholder="why this is waived"
|
||||||
/>
|
onChange={(e) =>
|
||||||
|
updateWaiver(index, { reason: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button variant="outline" size="sm" onClick={addWaiver}>
|
<HelpTip label="Adds a blank waiver row">
|
||||||
Add waiver
|
<Button variant="outline" size="sm" onClick={addWaiver}>
|
||||||
</Button>
|
Add waiver
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -332,11 +374,13 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
|||||||
className="space-y-2 rounded-md border border-border p-3"
|
className="space-y-2 rounded-md border border-border p-3"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<HelpTip label="A short, unique identifier for this rule (shown as its badge in Recent violations)">
|
||||||
value={rule.id}
|
<Input
|
||||||
placeholder="rule-id"
|
value={rule.id}
|
||||||
onChange={(e) => updateCustom(index, { id: e.target.value })}
|
placeholder="rule-id"
|
||||||
/>
|
onChange={(e) => updateCustom(index, { id: e.target.value })}
|
||||||
|
/>
|
||||||
|
</HelpTip>
|
||||||
<HelpTip
|
<HelpTip
|
||||||
label={
|
label={
|
||||||
rule.level === "block"
|
rule.level === "block"
|
||||||
@@ -354,29 +398,41 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
|||||||
updateCustom(index, { level: checked ? "block" : "warn" })
|
updateCustom(index, { level: checked ? "block" : "warn" })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<HelpTip label="Deletes this custom rule row locally — nothing changes until Save to repo">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
onClick={() => removeCustom(index)}
|
size="sm"
|
||||||
>
|
onClick={() => removeCustom(index)}
|
||||||
Remove
|
>
|
||||||
</Button>
|
Remove
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<HelpTip label="Regex tested against each changed file's source text — a bad pattern is skipped, never fails the gate">
|
||||||
value={rule.pattern}
|
<Input
|
||||||
placeholder="regex pattern"
|
value={rule.pattern}
|
||||||
onChange={(e) => updateCustom(index, { pattern: e.target.value })}
|
placeholder="regex pattern"
|
||||||
/>
|
onChange={(e) =>
|
||||||
<Input
|
updateCustom(index, { pattern: e.target.value })
|
||||||
value={rule.message}
|
}
|
||||||
placeholder="message shown when it matches"
|
/>
|
||||||
onChange={(e) => updateCustom(index, { message: 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>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button variant="outline" size="sm" onClick={addCustom}>
|
<HelpTip label="Adds a blank custom regex rule">
|
||||||
Add custom rule
|
<Button variant="outline" size="sm" onClick={addCustom}>
|
||||||
</Button>
|
Add custom rule
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -144,9 +144,14 @@ function UnifiedHistoryRow({ row }: { row: UnifiedRow }) {
|
|||||||
{PLATFORM_LABELS[platform] ?? platform}: {id}
|
{PLATFORM_LABELS[platform] ?? platform}: {id}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<span key={platform} className="text-muted-foreground">
|
<HelpTip
|
||||||
{PLATFORM_LABELS[platform] ?? platform}: {id}
|
key={platform}
|
||||||
</span>
|
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>
|
</div>
|
||||||
@@ -211,7 +216,9 @@ export function SocialHistorySection({ className }: { className?: string }) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="w-full justify-between"
|
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
|
<ChevronDown
|
||||||
className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`}
|
className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -76,9 +76,11 @@ function PipelineRow({ item }: { item: VideoPipelineItem }) {
|
|||||||
prefetch={false}
|
prefetch={false}
|
||||||
className="ml-auto"
|
className="ml-auto"
|
||||||
>
|
>
|
||||||
<Button variant="outline" size="sm">
|
<HelpTip label="Open the task to approve or reject the rendered video">
|
||||||
Review
|
<Button variant="outline" size="sm">
|
||||||
</Button>
|
Review
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{canRerender && (
|
{canRerender && (
|
||||||
@@ -110,7 +112,9 @@ export function VideoPipelineStrip({ className }: { className?: string }) {
|
|||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Film className="h-5 w-5" />
|
<Film className="h-5 w-5" />
|
||||||
Video Pipeline
|
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>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Every video in flight — authoring, review, or rendering — before it
|
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-4">
|
||||||
<div className="space-y-2">
|
<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
|
<ProjectSelector
|
||||||
value={effectiveProjectId}
|
value={effectiveProjectId}
|
||||||
onChange={setProjectId}
|
onChange={setProjectId}
|
||||||
@@ -493,7 +495,9 @@ function RequestVideoDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<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
|
<Input
|
||||||
id="video-request-occasion"
|
id="video-request-occasion"
|
||||||
placeholder="e.g. v0.19.0 launch, Founder's Day..."
|
placeholder="e.g. v0.19.0 launch, Founder's Day..."
|
||||||
@@ -502,7 +506,9 @@ function RequestVideoDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<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
|
<Textarea
|
||||||
id="video-request-brief"
|
id="video-request-brief"
|
||||||
placeholder="What should this video cover?"
|
placeholder="What should this video cover?"
|
||||||
@@ -512,7 +518,9 @@ function RequestVideoDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<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">
|
<div className="flex gap-4">
|
||||||
{REQUEST_PLATFORMS.map((platform) => (
|
{REQUEST_PLATFORMS.map((platform) => (
|
||||||
<div key={platform} className="flex items-center gap-2">
|
<div key={platform} className="flex items-center gap-2">
|
||||||
@@ -521,12 +529,20 @@ function RequestVideoDialog({
|
|||||||
checked={platforms.includes(platform)}
|
checked={platforms.includes(platform)}
|
||||||
onCheckedChange={() => togglePlatform(platform)}
|
onCheckedChange={() => togglePlatform(platform)}
|
||||||
/>
|
/>
|
||||||
<Label
|
<HelpTip
|
||||||
htmlFor={`video-request-${platform}`}
|
label={
|
||||||
className="text-sm font-normal"
|
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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -654,10 +670,12 @@ export function VideoPostQueue({ className }: { className?: string }) {
|
|||||||
if (isLoading) return null;
|
if (isLoading) return null;
|
||||||
|
|
||||||
const requestButton = (
|
const requestButton = (
|
||||||
<Button variant="outline" size="sm" onClick={() => setRequestOpen(true)}>
|
<HelpTip label="Opens a dialog to brief a UX/UI dev on an on-demand video, independent of a release or spotlight">
|
||||||
<Sparkles className="mr-1 h-4 w-4" />
|
<Button variant="outline" size="sm" onClick={() => setRequestOpen(true)}>
|
||||||
Request a video
|
<Sparkles className="mr-1 h-4 w-4" />
|
||||||
</Button>
|
Request a video
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!posts || posts.length === 0) {
|
if (!posts || posts.length === 0) {
|
||||||
@@ -696,7 +714,9 @@ export function VideoPostQueue({ className }: { className?: string }) {
|
|||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Film className="h-5 w-5" />
|
<Film className="h-5 w-5" />
|
||||||
Video Post Queue
|
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>
|
</CardTitle>
|
||||||
<CardAction>{requestButton}</CardAction>
|
<CardAction>{requestButton}</CardAction>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@@ -727,7 +747,9 @@ export function VideoPostQueue({ className }: { className?: string }) {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-2">
|
<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
|
<Textarea
|
||||||
id="video-reject-reason"
|
id="video-reject-reason"
|
||||||
placeholder="e.g. off-brand tone, wrong occasion..."
|
placeholder="e.g. off-brand tone, wrong occasion..."
|
||||||
|
|||||||
@@ -109,17 +109,23 @@ function XPostRow({
|
|||||||
</span>
|
</span>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
{post.release_version && (
|
{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 && (
|
{post.mention && (
|
||||||
<Badge variant="secondary" className="max-w-56 truncate">
|
<HelpTip label="The X mention this draft replies to">
|
||||||
re: {post.mention.text}
|
<Badge variant="secondary" className="max-w-56 truncate">
|
||||||
</Badge>
|
re: {post.mention.text}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
{post.feature && (
|
{post.feature && (
|
||||||
<Badge variant="outline" className="max-w-56 truncate">
|
<HelpTip label="The shipped feature this spotlight covers">
|
||||||
feature: {post.feature.title}
|
<Badge variant="outline" className="max-w-56 truncate">
|
||||||
</Badge>
|
feature: {post.feature.title}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -266,7 +272,9 @@ export function XPostQueue({ className }: { className?: string }) {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Rocket className="h-5 w-5" />X Post Queue
|
<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>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Drafted release announcements, feature spotlights, and mention
|
Drafted release announcements, feature spotlights, and mention
|
||||||
@@ -297,7 +305,9 @@ export function XPostQueue({ className }: { className?: string }) {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-2">
|
<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
|
<Textarea
|
||||||
id="x-reject-reason"
|
id="x-reject-reason"
|
||||||
placeholder="e.g. tone doesn't match our voice; not worth a public reply..."
|
placeholder="e.g. tone doesn't match our voice; not worth a public reply..."
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const categoryConfig: Record<
|
const categoryConfig: Record<
|
||||||
@@ -135,9 +136,11 @@ export function KBCategoryNav({
|
|||||||
<div className="flex-1 min-w-0 text-left">
|
<div className="flex-1 min-w-0 text-left">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-medium text-sm">{config.label}</span>
|
<span className="font-medium text-sm">{config.label}</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<HelpTip label="Document count — one document can split into several chunks at embedding time, so the chunk total (Index Stats) usually runs higher.">
|
||||||
{count.toLocaleString()} docs
|
<span className="text-xs text-muted-foreground w-fit">
|
||||||
</span>
|
{count.toLocaleString()} docs
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground truncate">
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
{config.description}
|
{config.description}
|
||||||
|
|||||||
@@ -86,14 +86,16 @@ function KBCategoryViewInner({ category }: { category: KBIndexType }) {
|
|||||||
{doc.source}
|
{doc.source}
|
||||||
</p>
|
</p>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
|
<HelpTip label={new Date(doc.indexed_at).toLocaleString()}>
|
||||||
<Clock className="h-3 w-3" />
|
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground w-fit">
|
||||||
<span>
|
<Clock className="h-3 w-3" />
|
||||||
{formatDistanceToNow(new Date(doc.indexed_at), {
|
<span>
|
||||||
addSuffix: true,
|
{formatDistanceToNow(new Date(doc.indexed_at), {
|
||||||
})}
|
addSuffix: true,
|
||||||
</span>
|
})}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -85,14 +85,16 @@ export function KBFilters({ selectedTypes, onTypesChange }: KBFiltersProps) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium">Filter by type</span>
|
<span className="text-sm font-medium">Filter by type</span>
|
||||||
{selectedTypes.length > 0 && (
|
{selectedTypes.length > 0 && (
|
||||||
<Button
|
<HelpTip label="Reset — search every index type again instead of just these">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
onClick={() => onTypesChange([])}
|
size="sm"
|
||||||
className="h-auto py-0 px-1 text-xs text-muted-foreground hover:text-foreground"
|
onClick={() => onTypesChange([])}
|
||||||
>
|
className="h-auto py-0 px-1 text-xs text-muted-foreground hover:text-foreground"
|
||||||
Clear
|
>
|
||||||
</Button>
|
Clear
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -75,19 +75,25 @@ export function KBResultCard({ result, onClick }: KBResultCardProps) {
|
|||||||
{result.metadata && Object.keys(result.metadata).length > 0 && (
|
{result.metadata && Object.keys(result.metadata).length > 0 && (
|
||||||
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
||||||
{typeof result.metadata.language === "string" && (
|
{typeof result.metadata.language === "string" && (
|
||||||
<span className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
<HelpTip label="Programming language this chunk was tagged with when indexed">
|
||||||
{result.metadata.language}
|
<span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
|
||||||
</span>
|
{result.metadata.language}
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
{typeof result.metadata.agent === "string" && (
|
{typeof result.metadata.agent === "string" && (
|
||||||
<span className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
<HelpTip label="Agent who authored this content">
|
||||||
@{result.metadata.agent}
|
<span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
|
||||||
</span>
|
@{result.metadata.agent}
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
{typeof result.metadata.section === "string" && (
|
{typeof result.metadata.section === "string" && (
|
||||||
<span className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
<HelpTip label="Section of the source document this chunk was extracted from">
|
||||||
{result.metadata.section}
|
<span className="text-xs bg-muted px-1.5 py-0.5 rounded w-fit">
|
||||||
</span>
|
{result.metadata.section}
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -142,9 +142,11 @@ export function KBStatsCard({ stats, isLoading }: KBStatsCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{latestUpdated && (
|
{latestUpdated && (
|
||||||
<p className="text-xs text-muted-foreground pt-1">
|
<HelpTip label={new Date(latestUpdated).toLocaleString()}>
|
||||||
Updated {formatDistanceToNow(new Date(latestUpdated))} ago
|
<p className="text-xs text-muted-foreground pt-1 w-fit">
|
||||||
</p>
|
Updated {formatDistanceToNow(new Date(latestUpdated))} ago
|
||||||
|
</p>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
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 { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import {
|
import {
|
||||||
@@ -326,26 +331,60 @@ function KnowledgeBaseBrowserContent() {
|
|||||||
onValueChange={(v) => handleTabChange(v as TabValue)}
|
onValueChange={(v) => handleTabChange(v as TabValue)}
|
||||||
>
|
>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="search" className="gap-2">
|
{(
|
||||||
<Search className="h-4 w-4" />
|
[
|
||||||
Search
|
{
|
||||||
</TabsTrigger>
|
value: "search",
|
||||||
<TabsTrigger value="ask" className="gap-2">
|
icon: Search,
|
||||||
<Sparkles className="h-4 w-4" />
|
label: "Search",
|
||||||
Ask AI
|
hint: "Semantic search across indexed content, filterable by category",
|
||||||
</TabsTrigger>
|
},
|
||||||
<TabsTrigger value="mentor" className="gap-2">
|
{
|
||||||
<Brain className="h-4 w-4" />
|
value: "ask",
|
||||||
Mentor
|
icon: Sparkles,
|
||||||
</TabsTrigger>
|
label: "Ask AI",
|
||||||
<TabsTrigger value="browse" className="gap-2">
|
hint: "One-shot Q&A: retrieves matching chunks (RAG) and generates a cited answer",
|
||||||
<FolderTree className="h-4 w-4" />
|
},
|
||||||
Browse
|
{
|
||||||
</TabsTrigger>
|
value: "mentor",
|
||||||
<TabsTrigger value="admin" className="gap-2">
|
icon: Brain,
|
||||||
<Settings className="h-4 w-4" />
|
label: "Mentor",
|
||||||
Admin
|
hint: "Multi-turn chat that also factors in your role and your own journal entries",
|
||||||
</TabsTrigger>
|
},
|
||||||
|
{
|
||||||
|
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>
|
</TabsList>
|
||||||
|
|
||||||
{/* Search Tab */}
|
{/* Search Tab */}
|
||||||
@@ -460,17 +499,27 @@ function KnowledgeBaseBrowserContent() {
|
|||||||
<XCircle className="h-8 w-8 text-red-600" />
|
<XCircle className="h-8 w-8 text-red-600" />
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">
|
<HelpTip label="Healthy requires all three subsystems below — embedding, LLM, and vector store — to each respond OK">
|
||||||
{health.healthy ? "Healthy" : "Unhealthy"}
|
<p className="font-medium w-fit">
|
||||||
</p>
|
{health.healthy ? "Healthy" : "Unhealthy"}
|
||||||
<p className="text-sm text-muted-foreground">
|
</p>
|
||||||
Embedding: {health.embedding_status}
|
</HelpTip>
|
||||||
</p>
|
<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>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
|
<div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
|
||||||
<div>LLM: {health.llm_status}</div>
|
<HelpTip label="Local model that generates RAG and Mentor answers from retrieved context">
|
||||||
<div>Vector: {health.vector_store_status}</div>
|
<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>
|
</div>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
@@ -507,10 +556,12 @@ function KnowledgeBaseBrowserContent() {
|
|||||||
<span>Summary</span>
|
<span>Summary</span>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button size="sm" variant="destructive">
|
<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">
|
||||||
<RefreshCw className="h-3 w-3 mr-1" />
|
<Button size="sm" variant="destructive">
|
||||||
Reindex All
|
<RefreshCw className="h-3 w-3 mr-1" />
|
||||||
</Button>
|
Reindex All
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -538,10 +589,14 @@ function KnowledgeBaseBrowserContent() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<HelpTip label="Sum of documents indexed across every category below">
|
||||||
<p className="text-2xl font-bold">{totalDocs}</p>
|
<div className="w-fit">
|
||||||
<p className="text-xs text-muted-foreground">Documents</p>
|
<p className="text-2xl font-bold">{totalDocs}</p>
|
||||||
</div>
|
<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">
|
<HelpTip label="A chunk is a segment of a document split for embedding — one document can produce many chunks">
|
||||||
<div className="w-fit">
|
<div className="w-fit">
|
||||||
<p className="text-2xl font-bold">{totalChunks}</p>
|
<p className="text-2xl font-bold">{totalChunks}</p>
|
||||||
@@ -667,14 +722,16 @@ function KnowledgeBaseBrowserContent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-4 text-sm mb-2">
|
<div className="grid grid-cols-3 gap-4 text-sm mb-2">
|
||||||
<div>
|
<HelpTip label="Document count for this category — see Chunks for the post-embedding split count">
|
||||||
<span className="text-muted-foreground">
|
<div className="w-fit">
|
||||||
Documents:
|
<span className="text-muted-foreground">
|
||||||
</span>{" "}
|
Documents:
|
||||||
<span className="font-medium">
|
</span>{" "}
|
||||||
{index.document_count}
|
<span className="font-medium">
|
||||||
</span>
|
{index.document_count}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
</HelpTip>
|
||||||
<HelpTip label="A chunk is a segment of a document split for embedding — one document can produce many chunks">
|
<HelpTip label="A chunk is a segment of a document split for embedding — one document can produce many chunks">
|
||||||
<div className="w-fit">
|
<div className="w-fit">
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
@@ -685,25 +742,37 @@ function KnowledgeBaseBrowserContent() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<div>
|
<HelpTip
|
||||||
<span className="text-muted-foreground">
|
label={
|
||||||
Updated:
|
index.last_updated
|
||||||
</span>{" "}
|
? new Date(
|
||||||
<span className="font-medium">
|
index.last_updated,
|
||||||
{index.last_updated
|
).toLocaleString()
|
||||||
? formatDistanceToNow(
|
: "This index has never been populated"
|
||||||
new Date(index.last_updated),
|
}
|
||||||
) + " ago"
|
>
|
||||||
: "Never"}
|
<div className="w-fit">
|
||||||
</span>
|
<span className="text-muted-foreground">
|
||||||
</div>
|
Updated:
|
||||||
|
</span>{" "}
|
||||||
|
<span className="font-medium">
|
||||||
|
{index.last_updated
|
||||||
|
? formatDistanceToNow(
|
||||||
|
new Date(index.last_updated),
|
||||||
|
) + " ago"
|
||||||
|
: "Never"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex justify-between text-xs text-muted-foreground">
|
<HelpTip label="Share of the fleet's total chunk count this category holds — not literal disk bytes">
|
||||||
<span>Storage usage</span>
|
<div className="flex justify-between text-xs text-muted-foreground">
|
||||||
<span>{percentage}%</span>
|
<span>Storage usage</span>
|
||||||
</div>
|
<span>{percentage}%</span>
|
||||||
|
</div>
|
||||||
|
</HelpTip>
|
||||||
<Progress value={percentage} className="h-1" />
|
<Progress value={percentage} className="h-1" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -149,28 +149,32 @@ export function MentorAnswerDisplay({
|
|||||||
<Brain className="h-5 w-5 text-primary shrink-0" />
|
<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">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
|
||||||
{response.agent_role && (
|
{response.agent_role && (
|
||||||
<span className="flex items-center gap-1.5">
|
<HelpTip label="Detected from your agent identity — shapes which standards, journals, and playbooks the mentor draws on">
|
||||||
<User className="h-3.5 w-3.5 text-muted-foreground" />
|
<span className="flex items-center gap-1.5 w-fit">
|
||||||
<span className="text-muted-foreground">Role:</span>
|
<User className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
<Badge variant="secondary" className="font-medium">
|
<span className="text-muted-foreground">Role:</span>
|
||||||
{response.agent_role}
|
<Badge variant="secondary" className="font-medium">
|
||||||
</Badge>
|
{response.agent_role}
|
||||||
{response.agent_team && (
|
</Badge>
|
||||||
<span className="text-muted-foreground">
|
{response.agent_team && (
|
||||||
({response.agent_team})
|
<span className="text-muted-foreground">
|
||||||
</span>
|
({response.agent_team})
|
||||||
)}
|
</span>
|
||||||
</span>
|
)}
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
{response.journal_entries_used !== undefined &&
|
{response.journal_entries_used !== undefined &&
|
||||||
response.journal_entries_used > 0 && (
|
response.journal_entries_used > 0 && (
|
||||||
<span className="flex items-center gap-1.5">
|
<HelpTip label="Entries from your own journal only — never another agent's">
|
||||||
<BookMarked className="h-3.5 w-3.5 text-muted-foreground" />
|
<span className="flex items-center gap-1.5 w-fit">
|
||||||
<span className="text-muted-foreground">
|
<BookMarked className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
{response.journal_entries_used} personal journal
|
<span className="text-muted-foreground">
|
||||||
{response.journal_entries_used !== 1 ? "s" : ""} used
|
{response.journal_entries_used} personal journal
|
||||||
|
{response.journal_entries_used !== 1 ? "s" : ""} used
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,9 +186,11 @@ export function MentorAnswerDisplay({
|
|||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
<Brain className="h-4 w-4 text-primary" />
|
<Brain className="h-4 w-4 text-primary" />
|
||||||
Mentor Response
|
Mentor Response
|
||||||
<span className="text-xs text-muted-foreground font-normal ml-auto">
|
<HelpTip label="Knowledge-base chunks retrieved and handed to the model as context for this answer">
|
||||||
{response.sources.length} sources used
|
<span className="text-xs text-muted-foreground font-normal ml-auto w-fit">
|
||||||
</span>
|
{response.sources.length} sources used
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -204,16 +210,17 @@ export function MentorAnswerDisplay({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{response.suggested_followups.map((followup, index) => (
|
{response.suggested_followups.map((followup, index) => (
|
||||||
<Button
|
<HelpTip key={index} label="Ask this next, in the same conversation">
|
||||||
key={index}
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-xs h-auto py-1.5 px-3"
|
className="text-xs h-auto py-1.5 px-3"
|
||||||
onClick={() => onFollowUp?.(followup)}
|
onClick={() => onFollowUp?.(followup)}
|
||||||
>
|
>
|
||||||
<MessageCircleQuestion className="h-3 w-3 mr-1.5" />
|
<MessageCircleQuestion className="h-3 w-3 mr-1.5" />
|
||||||
{followup}
|
{followup}
|
||||||
</Button>
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import {
|
import {
|
||||||
@@ -177,15 +178,19 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
|
|||||||
<Brain className="h-5 w-5 text-primary" />
|
<Brain className="h-5 w-5 text-primary" />
|
||||||
<span className="font-medium">Mentor Chat</span>
|
<span className="font-medium">Mentor Chat</span>
|
||||||
{conversationId && (
|
{conversationId && (
|
||||||
<Badge variant="outline" className="text-xs">
|
<HelpTip label="Follow-up questions reuse this conversation's history for context">
|
||||||
Conversation active
|
<Badge variant="outline" className="text-xs w-fit">
|
||||||
</Badge>
|
Conversation active
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" onClick={handleNewChat}>
|
<HelpTip label="Clears this thread — the mentor loses this conversation's context">
|
||||||
<RotateCcw className="h-4 w-4 mr-1" />
|
<Button variant="ghost" size="sm" onClick={handleNewChat}>
|
||||||
New Chat
|
<RotateCcw className="h-4 w-4 mr-1" />
|
||||||
</Button>
|
New Chat
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
@@ -241,19 +246,21 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
|
|||||||
{/* Sources toggle */}
|
{/* Sources toggle */}
|
||||||
{msg.sources && msg.sources.length > 0 && (
|
{msg.sources && msg.sources.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<HelpTip label="The knowledge-base chunks retrieved and handed to the model as context for this answer">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
className="text-xs"
|
size="sm"
|
||||||
onClick={() =>
|
className="text-xs"
|
||||||
setExpandedSources(
|
onClick={() =>
|
||||||
expandedSources === idx ? null : idx,
|
setExpandedSources(
|
||||||
)
|
expandedSources === idx ? null : idx,
|
||||||
}
|
)
|
||||||
>
|
}
|
||||||
{expandedSources === idx ? "Hide" : "Show"}{" "}
|
>
|
||||||
{msg.sources.length} sources
|
{expandedSources === idx ? "Hide" : "Show"}{" "}
|
||||||
</Button>
|
{msg.sources.length} sources
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
{expandedSources === idx && (
|
{expandedSources === idx && (
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
{msg.sources.map((source, sidx) => (
|
{msg.sources.map((source, sidx) => (
|
||||||
@@ -278,16 +285,20 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
|
|||||||
{msg.followups && msg.followups.length > 0 && (
|
{msg.followups && msg.followups.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{msg.followups.map((followup, fidx) => (
|
{msg.followups.map((followup, fidx) => (
|
||||||
<Button
|
<HelpTip
|
||||||
key={fidx}
|
key={fidx}
|
||||||
variant="outline"
|
label="Ask this next, in the same conversation"
|
||||||
size="sm"
|
|
||||||
className="text-xs h-auto py-1.5"
|
|
||||||
onClick={() => handleFollowUp(followup)}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
>
|
||||||
{followup}
|
<Button
|
||||||
</Button>
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs h-auto py-1.5"
|
||||||
|
onClick={() => handleFollowUp(followup)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{followup}
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import {
|
import {
|
||||||
Send,
|
Send,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -60,18 +61,33 @@ export function MentorQueryInput({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<Badge variant="outline" className="text-xs gap-1 bg-background">
|
<HelpTip label="Answers factor in your agent role and team">
|
||||||
<User className="h-3 w-3" />
|
<Badge
|
||||||
Role-aware
|
variant="outline"
|
||||||
</Badge>
|
className="text-xs gap-1 bg-background w-fit"
|
||||||
<Badge variant="outline" className="text-xs gap-1 bg-background">
|
>
|
||||||
<BookMarked className="h-3 w-3" />
|
<User className="h-3 w-3" />
|
||||||
Journals
|
Role-aware
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="text-xs gap-1 bg-background">
|
</HelpTip>
|
||||||
<MessageCircle className="h-3 w-3" />
|
<HelpTip label="Searches your own journal entries — not other agents'">
|
||||||
Follow-ups
|
<Badge
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { RAGQueryResponse } from "@/types";
|
|||||||
import { RAGCitationCard } from "./rag-citation-card";
|
import { RAGCitationCard } from "./rag-citation-card";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
@@ -120,9 +121,11 @@ export function RAGAnswerDisplay({
|
|||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
<Bot className="h-4 w-4 text-primary" />
|
<Bot className="h-4 w-4 text-primary" />
|
||||||
AI Answer
|
AI Answer
|
||||||
<span className="text-xs text-muted-foreground font-normal ml-auto">
|
<HelpTip label="Knowledge-base chunks retrieved by hybrid search and handed to the model as context for this answer">
|
||||||
{response.context_used} sources used
|
<span className="text-xs text-muted-foreground font-normal ml-auto w-fit">
|
||||||
</span>
|
{response.context_used} sources used
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<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">
|
<span className="font-medium text-sm line-clamp-1 flex-1 min-w-0">
|
||||||
{notification.subject}
|
{notification.subject}
|
||||||
</span>
|
</span>
|
||||||
<Badge variant="outline" className="text-[10px] shrink-0">
|
<HelpTip label="How urgently the sender flagged this — doesn't change what you need to do, just how prominently it's shown.">
|
||||||
{notification.priority}
|
<Badge variant="outline" className="text-[10px] shrink-0">
|
||||||
</Badge>
|
{notification.priority}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-1.5 mt-1.5">
|
<div className="flex flex-wrap items-center gap-1.5 mt-1.5">
|
||||||
{!notification.is_read && (
|
{!notification.is_read && (
|
||||||
<Badge variant="secondary" className="text-[10px]">
|
<HelpTip label="Hasn't been marked read yet.">
|
||||||
New
|
<Badge variant="secondary" className="text-[10px]">
|
||||||
</Badge>
|
New
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
{needsAck && (
|
{needsAck && (
|
||||||
<Badge variant="destructive" className="text-[10px]">
|
<HelpTip label="A formal signal from a PM/Board role — needs an explicit acknowledgement, tracked separately from read status.">
|
||||||
Needs Ack
|
<Badge variant="destructive" className="text-[10px]">
|
||||||
</Badge>
|
Needs Ack
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
<div className="ml-auto flex items-center gap-1">
|
<div className="ml-auto flex items-center gap-1">
|
||||||
{!notification.is_read && (
|
{!notification.is_read && (
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Team, type ProductCellMapping } from "@/types";
|
import { Team, type ProductCellMapping } from "@/types";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
const cells: { value: Team; label: string }[] = [
|
const cells: { value: Team; label: string }[] = [
|
||||||
{ value: Team.BACKEND, label: "Backend" },
|
{ value: Team.BACKEND, label: "Backend" },
|
||||||
@@ -104,12 +105,14 @@ export function CreateProductDialog() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<HelpTip label="Groups per-cell projects (e.g. a SaaS app + its OSS core) into one deliverable for MegaTask and board routing.">
|
||||||
<Button>
|
<DialogTrigger asChild>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Button>
|
||||||
New Product
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
</Button>
|
New Product
|
||||||
</DialogTrigger>
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
</HelpTip>
|
||||||
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -121,7 +124,9 @@ export function CreateProductDialog() {
|
|||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
value={name}
|
value={name}
|
||||||
@@ -132,7 +137,9 @@ export function CreateProductDialog() {
|
|||||||
|
|
||||||
{/* Slug */}
|
{/* Slug */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="slug"
|
id="slug"
|
||||||
value={slug}
|
value={slug}
|
||||||
@@ -147,7 +154,9 @@ export function CreateProductDialog() {
|
|||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
value={description}
|
value={description}
|
||||||
@@ -158,15 +167,19 @@ export function CreateProductDialog() {
|
|||||||
|
|
||||||
{/* Cell -> Project mapping */}
|
{/* Cell -> Project mapping */}
|
||||||
<div className="grid gap-3">
|
<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) => (
|
{cells.map((cell) => (
|
||||||
<div key={cell.value} className="grid gap-2">
|
<div key={cell.value} className="grid gap-2">
|
||||||
<Label
|
<HelpTip label={`Which project the ${cell.label} cell works on for this product.`}>
|
||||||
htmlFor={`cell-${cell.value}`}
|
<Label
|
||||||
className="text-sm text-muted-foreground"
|
htmlFor={`cell-${cell.value}`}
|
||||||
>
|
className="text-sm text-muted-foreground"
|
||||||
{cell.label}
|
>
|
||||||
</Label>
|
{cell.label}
|
||||||
|
</Label>
|
||||||
|
</HelpTip>
|
||||||
<Select
|
<Select
|
||||||
value={cellMapping[cell.value] ?? ""}
|
value={cellMapping[cell.value] ?? ""}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
type ProductCellMapping,
|
type ProductCellMapping,
|
||||||
type ProductUpdate,
|
type ProductUpdate,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
const cells: { value: Team; label: string }[] = [
|
const cells: { value: Team; label: string }[] = [
|
||||||
{ value: Team.BACKEND, label: "Backend" },
|
{ value: Team.BACKEND, label: "Backend" },
|
||||||
@@ -120,7 +121,9 @@ function EditProductForm({
|
|||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
{/* Slug (read-only) */}
|
{/* Slug (read-only) */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="slug"
|
id="slug"
|
||||||
value={product.slug}
|
value={product.slug}
|
||||||
@@ -131,7 +134,9 @@ function EditProductForm({
|
|||||||
|
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
value={name}
|
value={name}
|
||||||
@@ -142,7 +147,9 @@ function EditProductForm({
|
|||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
value={description}
|
value={description}
|
||||||
@@ -153,15 +160,19 @@ function EditProductForm({
|
|||||||
|
|
||||||
{/* Cell -> Project mapping */}
|
{/* Cell -> Project mapping */}
|
||||||
<div className="grid gap-3">
|
<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) => (
|
{cells.map((cell) => (
|
||||||
<div key={cell.value} className="grid gap-2">
|
<div key={cell.value} className="grid gap-2">
|
||||||
<Label
|
<HelpTip label={`Which project the ${cell.label} cell works on for this product.`}>
|
||||||
htmlFor={`cell-${cell.value}`}
|
<Label
|
||||||
className="text-sm text-muted-foreground"
|
htmlFor={`cell-${cell.value}`}
|
||||||
>
|
className="text-sm text-muted-foreground"
|
||||||
{cell.label}
|
>
|
||||||
</Label>
|
{cell.label}
|
||||||
|
</Label>
|
||||||
|
</HelpTip>
|
||||||
<Select
|
<Select
|
||||||
value={cellMapping[cell.value] ?? ""}
|
value={cellMapping[cell.value] ?? ""}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
|
|||||||
@@ -126,9 +126,21 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Product</TableHead>
|
<TableHead>
|
||||||
<TableHead>Cells</TableHead>
|
<HelpTip label="Name and slug; click either to open the edit dialog.">
|
||||||
<TableHead>Progress</TableHead>
|
<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>
|
<TableHead className="w-[100px]">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -144,9 +156,11 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
|
|||||||
>
|
>
|
||||||
{product.name}
|
{product.name}
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-xs text-muted-foreground font-mono">
|
<HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API.">
|
||||||
{product.slug}
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
</p>
|
{product.slug}
|
||||||
|
</p>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -188,9 +202,11 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
|
|||||||
>
|
>
|
||||||
{product.name}
|
{product.name}
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-xs text-muted-foreground font-mono">
|
<HelpTip label="Identifier for this cell-to-project grouping, used to reference it across the panel and API.">
|
||||||
{product.slug}
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
</p>
|
{product.slug}
|
||||||
|
</p>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
<HelpTip label="Edit product name, description, and cell-project mapping">
|
<HelpTip label="Edit product name, description, and cell-project mapping">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -121,12 +121,14 @@ export function CreateProjectDialog() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<HelpTip label="Registers a git repository — sets cell ownership, branch/environment ladder, and CI/CD gate commands for agents to work on it.">
|
||||||
<Button>
|
<DialogTrigger asChild>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Button>
|
||||||
New Project
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
</Button>
|
New Project
|
||||||
</DialogTrigger>
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
</HelpTip>
|
||||||
<DialogContent className="sm:max-w-[525px]">
|
<DialogContent className="sm:max-w-[525px]">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -138,7 +140,9 @@ export function CreateProjectDialog() {
|
|||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
@@ -149,7 +153,9 @@ export function CreateProjectDialog() {
|
|||||||
|
|
||||||
{/* Slug */}
|
{/* Slug */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="slug"
|
id="slug"
|
||||||
value={formData.slug}
|
value={formData.slug}
|
||||||
@@ -166,7 +172,9 @@ export function CreateProjectDialog() {
|
|||||||
|
|
||||||
{/* Git URL */}
|
{/* Git URL */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="git_url"
|
id="git_url"
|
||||||
value={formData.git_url}
|
value={formData.git_url}
|
||||||
@@ -205,7 +213,9 @@ export function CreateProjectDialog() {
|
|||||||
|
|
||||||
{/* Assigned Cell */}
|
{/* Assigned Cell */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Select
|
||||||
value={formData.assigned_cell}
|
value={formData.assigned_cell}
|
||||||
onValueChange={(value: Team) =>
|
onValueChange={(value: Team) =>
|
||||||
@@ -227,7 +237,9 @@ export function CreateProjectDialog() {
|
|||||||
|
|
||||||
{/* Default Branch */}
|
{/* Default Branch */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="default_branch"
|
id="default_branch"
|
||||||
value={formData.default_branch}
|
value={formData.default_branch}
|
||||||
@@ -248,20 +260,24 @@ export function CreateProjectDialog() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Advanced Options Toggle */}
|
{/* Advanced Options Toggle */}
|
||||||
<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.">
|
||||||
type="button"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
className="justify-start px-0 text-muted-foreground"
|
variant="ghost"
|
||||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
className="justify-start px-0 text-muted-foreground"
|
||||||
>
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||||
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
|
>
|
||||||
</Button>
|
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
|
|
||||||
{showAdvanced && (
|
{showAdvanced && (
|
||||||
<>
|
<>
|
||||||
{/* CI/CD Commands */}
|
{/* CI/CD Commands */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="test_command"
|
id="test_command"
|
||||||
value={formData.test_command || ""}
|
value={formData.test_command || ""}
|
||||||
@@ -273,7 +289,9 @@ export function CreateProjectDialog() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="lint_command"
|
id="lint_command"
|
||||||
value={formData.lint_command || ""}
|
value={formData.lint_command || ""}
|
||||||
@@ -285,7 +303,9 @@ export function CreateProjectDialog() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="format_command"
|
id="format_command"
|
||||||
value={formData.format_command || ""}
|
value={formData.format_command || ""}
|
||||||
@@ -300,7 +320,9 @@ export function CreateProjectDialog() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="typecheck_command"
|
id="typecheck_command"
|
||||||
value={formData.typecheck_command || ""}
|
value={formData.typecheck_command || ""}
|
||||||
@@ -315,7 +337,9 @@ export function CreateProjectDialog() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="build_command"
|
id="build_command"
|
||||||
value={formData.build_command || ""}
|
value={formData.build_command || ""}
|
||||||
@@ -330,7 +354,9 @@ export function CreateProjectDialog() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="quality_command"
|
id="quality_command"
|
||||||
value={formData.quality_command || ""}
|
value={formData.quality_command || ""}
|
||||||
|
|||||||
@@ -271,7 +271,9 @@ function EditProjectForm({
|
|||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
{/* Slug (read-only) */}
|
{/* Slug (read-only) */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="slug"
|
id="slug"
|
||||||
value={project.slug}
|
value={project.slug}
|
||||||
@@ -282,7 +284,9 @@ function EditProjectForm({
|
|||||||
|
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
value={name}
|
value={name}
|
||||||
@@ -293,7 +297,9 @@ function EditProjectForm({
|
|||||||
|
|
||||||
{/* Git URL */}
|
{/* Git URL */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="git_url"
|
id="git_url"
|
||||||
value={gitUrl}
|
value={gitUrl}
|
||||||
@@ -348,9 +354,17 @@ function EditProjectForm({
|
|||||||
|
|
||||||
{!clearToken && (
|
{!clearToken && (
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="git_token" className="text-sm">
|
<HelpTip
|
||||||
{project.has_git_token ? "Replace token" : "Set token"}
|
label={
|
||||||
</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
|
<Input
|
||||||
id="git_token"
|
id="git_token"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -368,7 +382,9 @@ function EditProjectForm({
|
|||||||
|
|
||||||
{/* Assigned Cell */}
|
{/* Assigned Cell */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Select
|
||||||
value={assignedCell}
|
value={assignedCell}
|
||||||
onValueChange={(value: Team) => setAssignedCell(value)}
|
onValueChange={(value: Team) => setAssignedCell(value)}
|
||||||
@@ -388,7 +404,9 @@ function EditProjectForm({
|
|||||||
|
|
||||||
{/* Default Branch */}
|
{/* Default Branch */}
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="default_branch"
|
id="default_branch"
|
||||||
value={defaultBranch}
|
value={defaultBranch}
|
||||||
@@ -419,19 +437,23 @@ function EditProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Advanced Options Toggle */}
|
{/* Advanced Options Toggle */}
|
||||||
<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.">
|
||||||
type="button"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
className="justify-start px-0 text-muted-foreground"
|
variant="ghost"
|
||||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
className="justify-start px-0 text-muted-foreground"
|
||||||
>
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||||
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
|
>
|
||||||
</Button>
|
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
|
|
||||||
{showAdvanced && (
|
{showAdvanced && (
|
||||||
<>
|
<>
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="test_command"
|
id="test_command"
|
||||||
value={testCommand}
|
value={testCommand}
|
||||||
@@ -441,7 +463,9 @@ function EditProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="lint_command"
|
id="lint_command"
|
||||||
value={lintCommand}
|
value={lintCommand}
|
||||||
@@ -451,7 +475,9 @@ function EditProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="format_command"
|
id="format_command"
|
||||||
value={formatCommand}
|
value={formatCommand}
|
||||||
@@ -461,7 +487,9 @@ function EditProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="typecheck_command"
|
id="typecheck_command"
|
||||||
value={typecheckCommand}
|
value={typecheckCommand}
|
||||||
@@ -471,7 +499,9 @@ function EditProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="build_command"
|
id="build_command"
|
||||||
value={buildCommand}
|
value={buildCommand}
|
||||||
@@ -481,7 +511,9 @@ function EditProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="quality_command"
|
id="quality_command"
|
||||||
value={qualityCommand}
|
value={qualityCommand}
|
||||||
@@ -497,14 +529,16 @@ function EditProjectForm({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Autonomous Maintenance Toggle */}
|
{/* Autonomous Maintenance Toggle */}
|
||||||
<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.">
|
||||||
type="button"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
className="justify-start px-0 text-muted-foreground"
|
variant="ghost"
|
||||||
onClick={() => setShowAutonomy(!showAutonomy)}
|
className="justify-start px-0 text-muted-foreground"
|
||||||
>
|
onClick={() => setShowAutonomy(!showAutonomy)}
|
||||||
{showAutonomy ? "Hide" : "Show"} Autonomous Maintenance
|
>
|
||||||
</Button>
|
{showAutonomy ? "Hide" : "Show"} Autonomous Maintenance
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
|
|
||||||
{showAutonomy && (
|
{showAutonomy && (
|
||||||
<>
|
<>
|
||||||
@@ -522,7 +556,9 @@ function EditProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<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
|
<Input
|
||||||
id="ci_watch_workflow"
|
id="ci_watch_workflow"
|
||||||
value={ciWatchWorkflow}
|
value={ciWatchWorkflow}
|
||||||
@@ -549,9 +585,11 @@ function EditProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="dep_update_command">
|
<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.">
|
||||||
Dependency-Update Command
|
<Label htmlFor="dep_update_command">
|
||||||
</Label>
|
Dependency-Update Command
|
||||||
|
</Label>
|
||||||
|
</HelpTip>
|
||||||
<Input
|
<Input
|
||||||
id="dep_update_command"
|
id="dep_update_command"
|
||||||
value={depUpdateCommand}
|
value={depUpdateCommand}
|
||||||
@@ -565,9 +603,11 @@ function EditProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="dep_update_paths">
|
<HelpTip label="Which lockfile paths the dry-run diffs to detect a change; leave blank to auto-infer uv.lock / pnpm-lock.yaml.">
|
||||||
Dependency-Update Lockfile Paths
|
<Label htmlFor="dep_update_paths">
|
||||||
</Label>
|
Dependency-Update Lockfile Paths
|
||||||
|
</Label>
|
||||||
|
</HelpTip>
|
||||||
<Input
|
<Input
|
||||||
id="dep_update_paths"
|
id="dep_update_paths"
|
||||||
value={depUpdatePaths}
|
value={depUpdatePaths}
|
||||||
|
|||||||
@@ -98,52 +98,83 @@ export function EnvironmentLadderEditor({
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
{/* span-wrap: disabled Button has pointer-events-none,
|
||||||
type="button"
|
so the tooltip needs a hoverable wrapper to fire
|
||||||
variant="ghost"
|
when isFirst disables the button. */}
|
||||||
size="icon"
|
<span className="inline-block">
|
||||||
className="h-6 w-6"
|
<Button
|
||||||
disabled={isFirst}
|
type="button"
|
||||||
onClick={() => handleMove(index, -1)}
|
variant="ghost"
|
||||||
aria-label="Move earlier in the flow"
|
size="icon"
|
||||||
>
|
className="h-6 w-6"
|
||||||
<ArrowUp className="h-3.5 w-3.5" />
|
disabled={isFirst}
|
||||||
</Button>
|
onClick={() => handleMove(index, -1)}
|
||||||
|
aria-label="Move earlier in the flow"
|
||||||
|
>
|
||||||
|
<ArrowUp className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
</TooltipTrigger>
|
</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>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<span className="inline-block">
|
||||||
type="button"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="h-6 w-6"
|
size="icon"
|
||||||
disabled={isLast}
|
className="h-6 w-6"
|
||||||
onClick={() => handleMove(index, 1)}
|
disabled={isLast}
|
||||||
aria-label="Move later in the flow"
|
onClick={() => handleMove(index, 1)}
|
||||||
>
|
aria-label="Move later in the flow"
|
||||||
<ArrowDown className="h-3.5 w-3.5" />
|
>
|
||||||
</Button>
|
<ArrowDown className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
</TooltipTrigger>
|
</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>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<span className="w-20 text-center text-[10px] font-medium uppercase text-muted-foreground">
|
<HelpTip
|
||||||
{role}
|
label={
|
||||||
</span>
|
role
|
||||||
<Input
|
? `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)"}.`
|
||||||
value={rung.name}
|
: "An intermediate rung — merged into from the rung above, promoted to the rung below."
|
||||||
onChange={(e) => handleUpdate(index, "name", e.target.value)}
|
}
|
||||||
placeholder="Name (e.g. dev, qa, stag)"
|
>
|
||||||
className="flex-1 h-8"
|
<span className="w-20 text-center text-[10px] font-medium uppercase text-muted-foreground">
|
||||||
/>
|
{role}
|
||||||
<Input
|
</span>
|
||||||
value={rung.branch}
|
</HelpTip>
|
||||||
onChange={(e) => handleUpdate(index, "branch", e.target.value)}
|
<HelpTip label="Display label for this rung (e.g. 'dev', 'qa') — shown in the panel only, not matched against branch names.">
|
||||||
placeholder="Branch (e.g. dev, master)"
|
<span className="flex-1">
|
||||||
className="flex-1 h-8"
|
<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>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -166,10 +197,12 @@ export function EnvironmentLadderEditor({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button type="button" variant="outline" size="sm" onClick={handleAdd}>
|
<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.">
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Button type="button" variant="outline" size="sm" onClick={handleAdd}>
|
||||||
Add rung
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
</Button>
|
Add rung
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Top to bottom is the promotion flow: <strong>PRs land</strong> on the
|
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 { Badge } from "@/components/ui/badge";
|
||||||
import { Search, Filter, X } from "lucide-react";
|
import { Search, Filter, X } from "lucide-react";
|
||||||
import { Team } from "@/types";
|
import { Team } from "@/types";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface ProjectFiltersProps {
|
interface ProjectFiltersProps {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
@@ -57,7 +58,9 @@ export function ProjectFilters({
|
|||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative flex-1 max-w-sm">
|
<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
|
<Input
|
||||||
placeholder="Search projects..."
|
placeholder="Search projects..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
@@ -68,17 +71,19 @@ export function ProjectFilters({
|
|||||||
|
|
||||||
{/* Cell Filter */}
|
{/* Cell Filter */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<HelpTip label="Show only projects assigned to the checked cells (any match).">
|
||||||
<Button variant="outline" className="gap-2">
|
<DropdownMenuTrigger asChild>
|
||||||
<Filter className="h-4 w-4" />
|
<Button variant="outline" className="gap-2">
|
||||||
Cell
|
<Filter className="h-4 w-4" />
|
||||||
{cellFilter.length > 0 && (
|
Cell
|
||||||
<Badge variant="secondary" className="ml-1">
|
{cellFilter.length > 0 && (
|
||||||
{cellFilter.length}
|
<Badge variant="secondary" className="ml-1">
|
||||||
</Badge>
|
{cellFilter.length}
|
||||||
)}
|
</Badge>
|
||||||
</Button>
|
)}
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</HelpTip>
|
||||||
<DropdownMenuContent align="start" className="w-48">
|
<DropdownMenuContent align="start" className="w-48">
|
||||||
<DropdownMenuLabel>Filter by Cell</DropdownMenuLabel>
|
<DropdownMenuLabel>Filter by Cell</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -95,20 +100,24 @@ export function ProjectFilters({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
{/* Show Inactive Toggle */}
|
{/* Show Inactive Toggle */}
|
||||||
<Button
|
<HelpTip label="Include inactive projects — hidden from the default list and skipped as the fallback for idle-agent spawns.">
|
||||||
variant={showInactive ? "default" : "outline"}
|
<Button
|
||||||
onClick={() => onShowInactiveChange(!showInactive)}
|
variant={showInactive ? "default" : "outline"}
|
||||||
className="gap-2"
|
onClick={() => onShowInactiveChange(!showInactive)}
|
||||||
>
|
className="gap-2"
|
||||||
{showInactive ? "Showing Inactive" : "Show Inactive"}
|
>
|
||||||
</Button>
|
{showInactive ? "Showing Inactive" : "Show Inactive"}
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
|
|
||||||
{/* Clear Filters */}
|
{/* Clear Filters */}
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<Button variant="ghost" onClick={clearFilters} className="gap-2">
|
<HelpTip label="Resets search, cell filter, and Show Inactive back to defaults.">
|
||||||
<X className="h-4 w-4" />
|
<Button variant="ghost" onClick={clearFilters} className="gap-2">
|
||||||
Clear
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
Clear
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { FolderGit2, X } from "lucide-react";
|
import { FolderGit2, X } from "lucide-react";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface ProjectSelectorProps {
|
interface ProjectSelectorProps {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
@@ -110,10 +111,12 @@ export function ProjectSelector({
|
|||||||
<FolderGit2 className="h-4 w-4" />
|
<FolderGit2 className="h-4 w-4" />
|
||||||
<span>{selectedProject.name}</span>
|
<span>{selectedProject.name}</span>
|
||||||
{selectedProject.assigned_cell && (
|
{selectedProject.assigned_cell && (
|
||||||
<Badge variant="outline" className="text-xs">
|
<HelpTip label="The cell that owns this project — only its agents can claim tasks against it.">
|
||||||
{TEAM_LABELS[selectedProject.assigned_cell] ||
|
<Badge variant="outline" className="text-xs">
|
||||||
selectedProject.assigned_cell}
|
{TEAM_LABELS[selectedProject.assigned_cell] ||
|
||||||
</Badge>
|
selectedProject.assigned_cell}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -180,11 +180,31 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Project</TableHead>
|
<TableHead>
|
||||||
<TableHead>Cell</TableHead>
|
<HelpTip label="Name and slug; click either to open the edit dialog.">
|
||||||
<TableHead>Tasks</TableHead>
|
<span>Project</span>
|
||||||
<TableHead>Token</TableHead>
|
</HelpTip>
|
||||||
<TableHead>Status</TableHead>
|
</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>
|
<TableHead className="w-[100px]">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -200,9 +220,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
|
|||||||
>
|
>
|
||||||
{project.name}
|
{project.name}
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-xs text-muted-foreground font-mono">
|
<HelpTip label="Composes each agent's workspace clone path and every branch name for this project.">
|
||||||
{project.slug}
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
</p>
|
{project.slug}
|
||||||
|
</p>
|
||||||
|
</HelpTip>
|
||||||
{project.ci_watch_enabled && (
|
{project.ci_watch_enabled && (
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<CiWatchBadge enabled />
|
<CiWatchBadge enabled />
|
||||||
@@ -211,9 +233,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge className={teamColors[project.assigned_cell]}>
|
<HelpTip label="Only this cell's agents can claim tasks on this project.">
|
||||||
{teamLabels[project.assigned_cell]}
|
<Badge className={teamColors[project.assigned_cell]}>
|
||||||
</Badge>
|
{teamLabels[project.assigned_cell]}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<TasksCell counts={project.task_counts} />
|
<TasksCell counts={project.task_counts} />
|
||||||
@@ -271,9 +295,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
|
|||||||
>
|
>
|
||||||
{project.name}
|
{project.name}
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-xs text-muted-foreground font-mono">
|
<HelpTip label="Composes each agent's workspace clone path and every branch name for this project.">
|
||||||
{project.slug}
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
</p>
|
{project.slug}
|
||||||
|
</p>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
<HelpTip label="Edit project settings and CI/CD commands">
|
<HelpTip label="Edit project settings and CI/CD commands">
|
||||||
@@ -306,9 +332,11 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-3 divide-y">
|
<div className="mt-3 divide-y">
|
||||||
<ResponsiveTableCardRow label="Cell">
|
<ResponsiveTableCardRow label="Cell">
|
||||||
<Badge className={teamColors[project.assigned_cell]}>
|
<HelpTip label="Only this cell's agents can claim tasks on this project.">
|
||||||
{teamLabels[project.assigned_cell]}
|
<Badge className={teamColors[project.assigned_cell]}>
|
||||||
</Badge>
|
{teamLabels[project.assigned_cell]}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
</ResponsiveTableCardRow>
|
</ResponsiveTableCardRow>
|
||||||
<ResponsiveTableCardRow label="Tasks">
|
<ResponsiveTableCardRow label="Tasks">
|
||||||
<TasksCell counts={project.task_counts} />
|
<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 { useRateLimitSync } from "@/hooks/use-rate-limit-sync";
|
||||||
import { useRateLimitWebSocket } from "@/hooks/use-rate-limit-websocket";
|
import { useRateLimitWebSocket } from "@/hooks/use-rate-limit-websocket";
|
||||||
import type { RateLimitEntry } from "@/types/rate-limits";
|
import type { RateLimitEntry } from "@/types/rate-limits";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Countdown row for a single rate-limited provider
|
// Countdown row for a single rate-limited provider
|
||||||
@@ -37,18 +38,26 @@ function RateLimitRow({ entry }: { entry: RateLimitEntry }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 px-4 py-2 bg-amber-50 border-b border-amber-300 last:border-b-0">
|
<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" />
|
<AlertTriangle className="h-4 w-4 text-amber-600 shrink-0" />
|
||||||
<span className="text-sm font-medium text-amber-900">
|
<HelpTip label="This AI provider hit a rate limit or a persistent overload; every other provider keeps working normally.">
|
||||||
{entry.provider}
|
<span className="text-sm font-medium text-amber-900">
|
||||||
</span>
|
{entry.provider}
|
||||||
{agentCount > 0 && (
|
|
||||||
<span className="text-sm text-amber-700">
|
|
||||||
{agentCount} agent{agentCount !== 1 ? "s" : ""} affected
|
|
||||||
</span>
|
</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>
|
<HelpTip label="Estimated time until RoboCo retries this provider, based on the retry-after it reported.">
|
||||||
<span className="text-sm text-amber-800 font-medium ml-auto">
|
<span className="text-sm text-amber-700">{secondsLeft}s</span>
|
||||||
operations paused — resuming automatically
|
</HelpTip>
|
||||||
</span>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -527,6 +527,7 @@ export function AIRoutingCard() {
|
|||||||
active={currentMode === "self_hosted"}
|
active={currentMode === "self_hosted"}
|
||||||
onClick={flipToSelfHosted}
|
onClick={flipToSelfHosted}
|
||||||
disabled={applyMode.isPending || !isSelfHostedConnected}
|
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
|
<ModeButton
|
||||||
icon={<Cpu className="h-4 w-4" />}
|
icon={<Cpu className="h-4 w-4" />}
|
||||||
@@ -561,9 +562,11 @@ export function AIRoutingCard() {
|
|||||||
<>
|
<>
|
||||||
<Separator />
|
<Separator />
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<Label className="text-sm font-medium">
|
<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.">
|
||||||
Self-Hosted default model
|
<Label className="text-sm font-medium">
|
||||||
</Label>
|
Self-Hosted default model
|
||||||
|
</Label>
|
||||||
|
</HelpTip>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Choose which discovered model all agents should use in
|
Choose which discovered model all agents should use in
|
||||||
self-hosted mode.
|
self-hosted mode.
|
||||||
@@ -782,6 +785,7 @@ function ModeButton({
|
|||||||
disabled,
|
disabled,
|
||||||
onClick,
|
onClick,
|
||||||
highlight,
|
highlight,
|
||||||
|
labelHint,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -790,6 +794,8 @@ function ModeButton({
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onClick: () => void | Promise<void>;
|
onClick: () => void | Promise<void>;
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
|
/** Extra hover context on the label, e.g. a gotcha the always-visible description doesn't cover. */
|
||||||
|
labelHint?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -804,11 +810,15 @@ function ModeButton({
|
|||||||
>
|
>
|
||||||
<div className="flex w-full items-center gap-2 text-sm font-medium">
|
<div className="flex w-full items-center gap-2 text-sm font-medium">
|
||||||
{icon}
|
{icon}
|
||||||
<span>{label}</span>
|
<HelpTip label={labelHint}>
|
||||||
|
<span>{label}</span>
|
||||||
|
</HelpTip>
|
||||||
{active ? (
|
{active ? (
|
||||||
<Badge className="ml-auto bg-primary/15 text-primary border-0 text-xs">
|
<HelpTip label="Currently applied fleet-wide — every agent spawns on this provider until the mode changes again.">
|
||||||
active
|
<Badge className="ml-auto bg-primary/15 text-primary border-0 text-xs">
|
||||||
</Badge>
|
active
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-muted-foreground font-normal">
|
<p className="mt-1 text-xs text-muted-foreground font-normal">
|
||||||
|
|||||||
@@ -289,7 +289,13 @@ export function FeatureFlagsCard() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="w-full justify-between px-2 text-muted-foreground"
|
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 ? (
|
{xCredsOpen ? (
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
) : (
|
) : (
|
||||||
@@ -314,7 +320,9 @@ export function FeatureFlagsCard() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="w-full justify-between px-2 text-muted-foreground"
|
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 ? (
|
{tiktokCredsOpen ? (
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
) : (
|
) : (
|
||||||
@@ -339,7 +347,9 @@ export function FeatureFlagsCard() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="w-full justify-between px-2 text-muted-foreground"
|
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 ? (
|
{telegramCredsOpen ? (
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -145,16 +145,22 @@ export function SelfHostedSection({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Server className="h-4 w-4 text-muted-foreground" />
|
<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 && (
|
{testResult?.ok === true && (
|
||||||
<Badge className="bg-emerald-500/10 text-emerald-600 border-0">
|
<HelpTip label="The last Test Connection call succeeded — the model list below reflects this endpoint.">
|
||||||
<CheckCircle2 className="h-3 w-3" /> connected
|
<Badge className="bg-emerald-500/10 text-emerald-600 border-0">
|
||||||
</Badge>
|
<CheckCircle2 className="h-3 w-3" /> connected
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
{testResult?.ok === false && (
|
{testResult?.ok === false && (
|
||||||
<Badge className="bg-red-500/10 text-red-600 border-0">
|
<HelpTip label="The last Test Connection call failed — see the error detail below.">
|
||||||
<XCircle className="h-3 w-3" /> error
|
<Badge className="bg-red-500/10 text-red-600 border-0">
|
||||||
</Badge>
|
<XCircle className="h-3 w-3" /> error
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -216,9 +222,11 @@ export function SelfHostedSection({
|
|||||||
</Button>
|
</Button>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleSave} disabled={saveConfig.isPending}>
|
<HelpTip label="Saves the Base URL above together with any token typed here, in one combined config — not just this token.">
|
||||||
{saveConfig.isPending ? "Saving…" : "Save"}
|
<Button onClick={handleSave} disabled={saveConfig.isPending}>
|
||||||
</Button>
|
{saveConfig.isPending ? "Saving…" : "Save"}
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
{config?.has_token && (
|
{config?.has_token && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
@@ -336,24 +344,26 @@ export function SelfHostedSection({
|
|||||||
</span>
|
</span>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<HelpTip label="Re-queries the endpoint's model list only — doesn't re-run the connection test above.">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
onClick={handleRefresh}
|
size="sm"
|
||||||
disabled={refreshModels.isPending}
|
onClick={handleRefresh}
|
||||||
>
|
disabled={refreshModels.isPending}
|
||||||
{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 animate-spin" />
|
||||||
</>
|
Refreshing…
|
||||||
) : (
|
</>
|
||||||
<>
|
) : (
|
||||||
<RefreshCw className="mr-1.5 h-3 w-3" />
|
<>
|
||||||
Refresh Models
|
<RefreshCw className="mr-1.5 h-3 w-3" />
|
||||||
</>
|
Refresh Models
|
||||||
)}
|
</>
|
||||||
</Button>
|
)}
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y rounded-md border">
|
<div className="divide-y rounded-md border">
|
||||||
{models.map((m) => (
|
{models.map((m) => (
|
||||||
|
|||||||
@@ -69,19 +69,22 @@ export function TelegramCredentialsForm() {
|
|||||||
|
|
||||||
<div className="flex items-center gap-2 rounded-md border p-3">
|
<div className="flex items-center gap-2 rounded-md border p-3">
|
||||||
{status?.has_credentials ? (
|
{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" />
|
<Key className="h-4 w-4 text-green-500" />
|
||||||
<span className="text-sm text-green-600 dark:text-green-400">
|
</HelpTip>
|
||||||
Credentials are set
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<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" />
|
<KeyRound className="h-4 w-4 text-amber-500" />
|
||||||
<span className="text-sm text-amber-600 dark:text-amber-400">
|
</HelpTip>
|
||||||
{isLoading ? "Checking..." : "No credentials configured"}
|
)}
|
||||||
</span>
|
{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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -88,19 +88,22 @@ export function TikTokCredentialsForm() {
|
|||||||
|
|
||||||
<div className="flex items-center gap-2 rounded-md border p-3">
|
<div className="flex items-center gap-2 rounded-md border p-3">
|
||||||
{status?.has_credentials ? (
|
{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" />
|
<Key className="h-4 w-4 text-green-500" />
|
||||||
<span className="text-sm text-green-600 dark:text-green-400">
|
</HelpTip>
|
||||||
Credentials are set
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<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" />
|
<KeyRound className="h-4 w-4 text-amber-500" />
|
||||||
<span className="text-sm text-amber-600 dark:text-amber-400">
|
</HelpTip>
|
||||||
{isLoading ? "Checking..." : "No credentials configured"}
|
)}
|
||||||
</span>
|
{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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -86,19 +86,22 @@ export function XCredentialsForm() {
|
|||||||
|
|
||||||
<div className="flex items-center gap-2 rounded-md border p-3">
|
<div className="flex items-center gap-2 rounded-md border p-3">
|
||||||
{status?.has_credentials ? (
|
{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" />
|
<Key className="h-4 w-4 text-green-500" />
|
||||||
<span className="text-sm text-green-600 dark:text-green-400">
|
</HelpTip>
|
||||||
Credentials are set
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<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" />
|
<KeyRound className="h-4 w-4 text-amber-500" />
|
||||||
<span className="text-sm text-amber-600 dark:text-amber-400">
|
</HelpTip>
|
||||||
{isLoading ? "Checking..." : "No credentials configured"}
|
)}
|
||||||
</span>
|
{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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user