Merge master into ishan/fix-activity-clerkprovider
This commit is contained in:
@@ -77,16 +77,12 @@ const humanizeAction = (value: string) =>
|
||||
value
|
||||
.split(".")
|
||||
.map((part) =>
|
||||
part
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
part.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()),
|
||||
)
|
||||
.join(" · ");
|
||||
|
||||
const formatStatusLabel = (status: string) =>
|
||||
status
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
status.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
|
||||
const statusDotClass = (status: string) => {
|
||||
if (status === "approved") return "bg-emerald-500";
|
||||
@@ -108,17 +104,15 @@ type TooltipValue = number | string | Array<number | string>;
|
||||
const formatRubricTooltipValue = (
|
||||
value?: TooltipValue,
|
||||
name?: TooltipValue,
|
||||
item?:
|
||||
| {
|
||||
color?: string | null;
|
||||
payload?: {
|
||||
name?: string;
|
||||
fill?: string;
|
||||
percent?: number;
|
||||
percentLabel?: string;
|
||||
};
|
||||
}
|
||||
| null,
|
||||
item?: {
|
||||
color?: string | null;
|
||||
payload?: {
|
||||
name?: string;
|
||||
fill?: string;
|
||||
percent?: number;
|
||||
percentLabel?: string;
|
||||
};
|
||||
} | null,
|
||||
) => {
|
||||
const payload = item?.payload;
|
||||
const label =
|
||||
@@ -223,19 +217,19 @@ export function BoardApprovalsPanel({
|
||||
|
||||
const approvals = useMemo(() => {
|
||||
const raw = usingExternal
|
||||
? externalApprovals ?? []
|
||||
? (externalApprovals ?? [])
|
||||
: approvalsQuery.data?.status === 200
|
||||
? approvalsQuery.data.data.items ?? []
|
||||
? (approvalsQuery.data.data.items ?? [])
|
||||
: [];
|
||||
return raw.map(normalizeApproval);
|
||||
}, [approvalsQuery.data, externalApprovals, usingExternal]);
|
||||
|
||||
const loadingState = usingExternal
|
||||
? externalLoading ?? false
|
||||
? (externalLoading ?? false)
|
||||
: approvalsQuery.isLoading;
|
||||
const errorState = usingExternal
|
||||
? externalError ?? null
|
||||
: error ?? approvalsQuery.error?.message ?? null;
|
||||
? (externalError ?? null)
|
||||
: (error ?? approvalsQuery.error?.message ?? null);
|
||||
|
||||
const handleDecision = useCallback(
|
||||
(approvalId: string, status: "approved" | "rejected") => {
|
||||
@@ -244,9 +238,9 @@ export function BoardApprovalsPanel({
|
||||
.filter((item) => item.status === "pending")
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(apiDatetimeToMs(b.created_at) ?? 0) - (apiDatetimeToMs(a.created_at) ?? 0),
|
||||
)[0]
|
||||
?.id;
|
||||
(apiDatetimeToMs(b.created_at) ?? 0) -
|
||||
(apiDatetimeToMs(a.created_at) ?? 0),
|
||||
)[0]?.id;
|
||||
if (pendingNext) {
|
||||
setSelectedId(pendingNext);
|
||||
}
|
||||
@@ -311,17 +305,17 @@ export function BoardApprovalsPanel({
|
||||
return bTime - aTime;
|
||||
});
|
||||
const pending = sortByTime(
|
||||
approvals.filter((item) => item.status === "pending")
|
||||
approvals.filter((item) => item.status === "pending"),
|
||||
);
|
||||
const resolved = sortByTime(
|
||||
approvals.filter((item) => item.status !== "pending")
|
||||
approvals.filter((item) => item.status !== "pending"),
|
||||
);
|
||||
return { pending, resolved };
|
||||
}, [approvals]);
|
||||
|
||||
const orderedApprovals = useMemo(
|
||||
() => [...sortedApprovals.pending, ...sortedApprovals.resolved],
|
||||
[sortedApprovals.pending, sortedApprovals.resolved]
|
||||
[sortedApprovals.pending, sortedApprovals.resolved],
|
||||
);
|
||||
|
||||
const effectiveSelectedId = useMemo(() => {
|
||||
@@ -344,7 +338,6 @@ export function BoardApprovalsPanel({
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-6", scrollable && "h-full")}>
|
||||
|
||||
{errorState ? (
|
||||
<div className="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||
{errorState}
|
||||
@@ -358,13 +351,13 @@ export function BoardApprovalsPanel({
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-6 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]",
|
||||
scrollable && "h-full"
|
||||
scrollable && "h-full",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden rounded-xl border border-slate-200 bg-white",
|
||||
scrollable && "flex min-h-0 flex-col"
|
||||
scrollable && "flex min-h-0 flex-col",
|
||||
)}
|
||||
>
|
||||
<div className="border-b border-slate-200 bg-slate-50 px-4 py-3">
|
||||
@@ -378,7 +371,7 @@ export function BoardApprovalsPanel({
|
||||
<div
|
||||
className={cn(
|
||||
"divide-y divide-slate-100",
|
||||
scrollable && "min-h-0 overflow-y-auto"
|
||||
scrollable && "min-h-0 overflow-y-auto",
|
||||
)}
|
||||
>
|
||||
{orderedApprovals.map((approval) => {
|
||||
@@ -386,10 +379,10 @@ export function BoardApprovalsPanel({
|
||||
const isSelected = effectiveSelectedId === approval.id;
|
||||
const isPending = approval.status === "pending";
|
||||
const titleRow = summary.rows.find(
|
||||
(row) => row.label.toLowerCase() === "title"
|
||||
(row) => row.label.toLowerCase() === "title",
|
||||
);
|
||||
const fallbackRow = summary.rows.find(
|
||||
(row) => row.label.toLowerCase() !== "title"
|
||||
(row) => row.label.toLowerCase() !== "title",
|
||||
);
|
||||
const primaryLabel =
|
||||
titleRow?.value ?? fallbackRow?.value ?? "Untitled";
|
||||
@@ -400,9 +393,8 @@ export function BoardApprovalsPanel({
|
||||
onClick={() => setSelectedId(approval.id)}
|
||||
className={cn(
|
||||
"w-full px-4 py-4 text-left transition hover:bg-slate-50",
|
||||
isSelected &&
|
||||
"bg-amber-50 border-l-2 border-amber-500",
|
||||
!isPending && "opacity-60"
|
||||
isSelected && "bg-amber-50 border-l-2 border-amber-500",
|
||||
!isPending && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
@@ -412,7 +404,7 @@ export function BoardApprovalsPanel({
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-[3px] px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.2em]",
|
||||
statusBadgeClass(approval.status)
|
||||
statusBadgeClass(approval.status),
|
||||
)}
|
||||
>
|
||||
{formatStatusLabel(approval.status)}
|
||||
@@ -434,7 +426,7 @@ export function BoardApprovalsPanel({
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden rounded-xl border border-slate-200 bg-white",
|
||||
scrollable && "flex min-h-0 flex-col"
|
||||
scrollable && "flex min-h-0 flex-col",
|
||||
)}
|
||||
>
|
||||
<div className="border-b border-slate-200 bg-slate-50 px-4 py-3">
|
||||
@@ -452,7 +444,7 @@ export function BoardApprovalsPanel({
|
||||
(() => {
|
||||
const summary = approvalSummary(selectedApproval);
|
||||
const titleRow = summary.rows.find(
|
||||
(row) => row.label.toLowerCase() === "title"
|
||||
(row) => row.label.toLowerCase() === "title",
|
||||
);
|
||||
const titleText = titleRow?.value?.trim() ?? "";
|
||||
const descriptionText = summary.description?.trim() ?? "";
|
||||
@@ -465,7 +457,7 @@ export function BoardApprovalsPanel({
|
||||
return true;
|
||||
});
|
||||
const rubricEntries = Object.entries(
|
||||
selectedApproval.rubric_scores ?? {}
|
||||
selectedApproval.rubric_scores ?? {},
|
||||
).map(([key, value]) => ({
|
||||
label: key
|
||||
.replace(/_/g, " ")
|
||||
@@ -478,7 +470,8 @@ export function BoardApprovalsPanel({
|
||||
);
|
||||
const hasRubric = rubricEntries.length > 0 && rubricTotal > 0;
|
||||
const rubricChartData = rubricEntries.map((entry, index) => {
|
||||
const percent = rubricTotal > 0 ? (entry.value / rubricTotal) * 100 : 0;
|
||||
const percent =
|
||||
rubricTotal > 0 ? (entry.value / rubricTotal) * 100 : 0;
|
||||
return {
|
||||
key: entry.label.toLowerCase().replace(/[^a-z0-9]+/g, "_"),
|
||||
name: entry.label,
|
||||
@@ -507,14 +500,15 @@ export function BoardApprovalsPanel({
|
||||
{humanizeAction(selectedApproval.action_type)}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Requested {formatTimestamp(selectedApproval.created_at)}
|
||||
Requested{" "}
|
||||
{formatTimestamp(selectedApproval.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-md px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em]",
|
||||
confidenceBadgeClass(selectedApproval.confidence)
|
||||
confidenceBadgeClass(selectedApproval.confidence),
|
||||
)}
|
||||
>
|
||||
{selectedApproval.confidence}% confidence
|
||||
@@ -552,7 +546,7 @@ export function BoardApprovalsPanel({
|
||||
<span
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full",
|
||||
statusDotClass(selectedApproval.status)
|
||||
statusDotClass(selectedApproval.status),
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
@@ -684,7 +678,6 @@ export function BoardApprovalsPanel({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
|
||||
@@ -56,7 +56,10 @@ function BoardChatComposerImpl({
|
||||
disabled={isSending}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={() => void send()} disabled={isSending || !value.trim()}>
|
||||
<Button
|
||||
onClick={() => void send()}
|
||||
disabled={isSending || !value.trim()}
|
||||
>
|
||||
{isSending ? "Sending…" : "Send"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -95,8 +95,14 @@ export function BoardGoalPanel({
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted">
|
||||
Objective
|
||||
</p>
|
||||
<p className={cn("text-sm", board?.objective ? "text-strong" : "text-muted")}>
|
||||
{board?.objective || (isGoalBoard ? "No objective yet." : "Not required.")}
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm",
|
||||
board?.objective ? "text-strong" : "text-muted",
|
||||
)}
|
||||
>
|
||||
{board?.objective ||
|
||||
(isGoalBoard ? "No objective yet." : "Not required.")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -122,7 +128,9 @@ export function BoardGoalPanel({
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted">
|
||||
Target date
|
||||
</p>
|
||||
<p className="text-sm text-strong">{formatTargetDate(board?.target_date)}</p>
|
||||
<p className="text-sm text-strong">
|
||||
{formatTargetDate(board?.target_date)}
|
||||
</p>
|
||||
</div>
|
||||
{onStartOnboarding || onEdit ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
@@ -255,9 +255,7 @@ export function BoardOnboardingChat({
|
||||
setSelectedOptions([]);
|
||||
setAwaitingAssistantFingerprint(fingerprintBefore);
|
||||
setAwaitingKind("answer");
|
||||
setLastSubmittedAnswer(
|
||||
freeText ? `${value}: ${freeText}` : value,
|
||||
);
|
||||
setLastSubmittedAnswer(freeText ? `${value}: ${freeText}` : value);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to submit answer.",
|
||||
@@ -497,14 +495,14 @@ export function BoardOnboardingChat({
|
||||
{extraContextOpen ? "Hide" : "Add"}
|
||||
</Button>
|
||||
</div>
|
||||
{extraContextOpen ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
<Textarea
|
||||
ref={extraContextRef}
|
||||
className="min-h-[84px]"
|
||||
placeholder="Anything else the agent should know before you confirm? (constraints, context, preferences, links, etc.)"
|
||||
value={extraContext}
|
||||
onChange={(event) => setExtraContext(event.target.value)}
|
||||
{extraContextOpen ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
<Textarea
|
||||
ref={extraContextRef}
|
||||
className="min-h-[84px]"
|
||||
placeholder="Anything else the agent should know before you confirm? (constraints, context, preferences, links, etc.)"
|
||||
value={extraContext}
|
||||
onChange={(event) => setExtraContext(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
if (event.nativeEvent.isComposing) return;
|
||||
@@ -521,9 +519,15 @@ export function BoardOnboardingChat({
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => void submitExtraContext()}
|
||||
disabled={loading || isAwaitingAgent || !extraContext.trim()}
|
||||
disabled={
|
||||
loading || isAwaitingAgent || !extraContext.trim()
|
||||
}
|
||||
>
|
||||
{loading ? "Sending..." : isAwaitingAgent ? "Waiting..." : "Send context"}
|
||||
{loading
|
||||
? "Sending..."
|
||||
: isAwaitingAgent
|
||||
? "Waiting..."
|
||||
: "Send context"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
@@ -538,7 +542,11 @@ export function BoardOnboardingChat({
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={confirmGoal} disabled={loading || isAwaitingAgent} type="button">
|
||||
<Button
|
||||
onClick={confirmGoal}
|
||||
disabled={loading || isAwaitingAgent}
|
||||
type="button"
|
||||
>
|
||||
Confirm goal
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { memo, type HTMLAttributes } from "react";
|
||||
|
||||
import ReactMarkdown, { type Components } from "react-markdown";
|
||||
import remarkBreaks from "remark-breaks";
|
||||
@@ -8,6 +8,60 @@ import remarkGfm from "remark-gfm";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type MarkdownCodeProps = HTMLAttributes<HTMLElement> & {
|
||||
node?: unknown;
|
||||
inline?: boolean;
|
||||
};
|
||||
|
||||
const MARKDOWN_CODE_COMPONENTS: Components = {
|
||||
pre: ({ node: _node, className, ...props }) => (
|
||||
<pre
|
||||
className={cn(
|
||||
"my-3 overflow-x-auto rounded-lg bg-slate-950 p-3 text-xs leading-relaxed text-slate-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
code: (rawProps) => {
|
||||
// react-markdown passes `inline`, but the public `Components` typing doesn't
|
||||
// currently include it, so we pluck it safely here without leaking it to DOM.
|
||||
const {
|
||||
node: _node,
|
||||
inline,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
} = rawProps as MarkdownCodeProps;
|
||||
const codeText = Array.isArray(children)
|
||||
? children.join("")
|
||||
: String(children ?? "");
|
||||
const isInline =
|
||||
typeof inline === "boolean" ? inline : !codeText.includes("\n");
|
||||
|
||||
if (isInline) {
|
||||
return (
|
||||
<code
|
||||
className={cn(
|
||||
"rounded bg-slate-100 px-1 py-0.5 font-mono text-[0.85em] text-slate-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
// For fenced blocks, the parent <pre> handles the box styling.
|
||||
return (
|
||||
<code className={cn("font-mono", className)} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const MARKDOWN_TABLE_COMPONENTS: Components = {
|
||||
table: ({ node: _node, className, ...props }) => (
|
||||
<div className="my-3 overflow-x-auto">
|
||||
@@ -42,6 +96,7 @@ const MARKDOWN_TABLE_COMPONENTS: Components = {
|
||||
|
||||
const MARKDOWN_COMPONENTS_BASIC: Components = {
|
||||
...MARKDOWN_TABLE_COMPONENTS,
|
||||
...MARKDOWN_CODE_COMPONENTS,
|
||||
p: ({ node: _node, className, ...props }) => (
|
||||
<p className={cn("mb-2 last:mb-0", className)} {...props} />
|
||||
),
|
||||
@@ -73,21 +128,6 @@ const MARKDOWN_COMPONENTS_DESCRIPTION: Components = {
|
||||
h3: ({ node: _node, className, ...props }) => (
|
||||
<h3 className={cn("mb-2 text-sm font-semibold", className)} {...props} />
|
||||
),
|
||||
code: ({ node: _node, className, ...props }) => (
|
||||
<code
|
||||
className={cn("rounded bg-slate-100 px-1 py-0.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
pre: ({ node: _node, className, ...props }) => (
|
||||
<pre
|
||||
className={cn(
|
||||
"overflow-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const MARKDOWN_REMARK_PLUGINS_BASIC = [remarkGfm];
|
||||
@@ -119,4 +159,3 @@ export const Markdown = memo(function Markdown({
|
||||
});
|
||||
|
||||
Markdown.displayName = "Markdown";
|
||||
|
||||
|
||||
@@ -36,14 +36,15 @@ export function TaskCard({
|
||||
onDragEnd,
|
||||
}: TaskCardProps) {
|
||||
const hasPendingApproval = approvalsPendingCount > 0;
|
||||
const needsLeadReview = status === "review" && !isBlocked && !hasPendingApproval;
|
||||
const needsLeadReview =
|
||||
status === "review" && !isBlocked && !hasPendingApproval;
|
||||
const leftBarClassName = isBlocked
|
||||
? "bg-rose-400"
|
||||
: hasPendingApproval
|
||||
? "bg-amber-400"
|
||||
: needsLeadReview
|
||||
? "bg-indigo-400"
|
||||
: null;
|
||||
: null;
|
||||
const priorityBadge = (value?: string) => {
|
||||
if (!value) return null;
|
||||
const normalized = value.toLowerCase();
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Activity, BarChart3, Bot, LayoutGrid, Network } from "lucide-react";
|
||||
import {
|
||||
Activity,
|
||||
BarChart3,
|
||||
Bot,
|
||||
Folder,
|
||||
LayoutGrid,
|
||||
Network,
|
||||
} from "lucide-react";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
import {
|
||||
@@ -13,14 +20,16 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
export function DashboardSidebar() {
|
||||
const pathname = usePathname();
|
||||
const healthQuery = useHealthzHealthzGet<healthzHealthzGetResponse, ApiError>({
|
||||
query: {
|
||||
refetchInterval: 30_000,
|
||||
refetchOnMount: "always",
|
||||
retry: false,
|
||||
const healthQuery = useHealthzHealthzGet<healthzHealthzGetResponse, ApiError>(
|
||||
{
|
||||
query: {
|
||||
refetchInterval: 30_000,
|
||||
refetchOnMount: "always",
|
||||
retry: false,
|
||||
},
|
||||
request: { cache: "no-store" },
|
||||
},
|
||||
request: { cache: "no-store" },
|
||||
});
|
||||
);
|
||||
|
||||
const okValue = healthQuery.data?.data?.ok;
|
||||
const systemStatus: "unknown" | "operational" | "degraded" =
|
||||
@@ -51,7 +60,7 @@ export function DashboardSidebar() {
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname === "/dashboard"
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
@@ -63,19 +72,31 @@ export function DashboardSidebar() {
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname.startsWith("/gateways")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<Network className="h-4 w-4" />
|
||||
Gateways
|
||||
</Link>
|
||||
<Link
|
||||
href="/board-groups"
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname.startsWith("/board-groups")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<Folder className="h-4 w-4" />
|
||||
Board groups
|
||||
</Link>
|
||||
<Link
|
||||
href="/boards"
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname.startsWith("/boards")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
@@ -87,7 +108,7 @@ export function DashboardSidebar() {
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname.startsWith("/activity")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<Activity className="h-4 w-4" />
|
||||
@@ -99,7 +120,7 @@ export function DashboardSidebar() {
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname.startsWith("/agents")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<Bot className="h-4 w-4" />
|
||||
@@ -114,7 +135,7 @@ export function DashboardSidebar() {
|
||||
"h-2 w-2 rounded-full",
|
||||
systemStatus === "operational" && "bg-emerald-500",
|
||||
systemStatus === "degraded" && "bg-rose-500",
|
||||
systemStatus === "unknown" && "bg-slate-300"
|
||||
systemStatus === "unknown" && "bg-slate-300",
|
||||
)}
|
||||
/>
|
||||
{statusLabel}
|
||||
|
||||
@@ -2,10 +2,21 @@
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, isClerkEnabled } from "@/auth/clerk";
|
||||
import {
|
||||
SignInButton,
|
||||
SignedIn,
|
||||
SignedOut,
|
||||
isClerkEnabled,
|
||||
} from "@/auth/clerk";
|
||||
|
||||
const ArrowIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M6 12L10 8L6 4"
|
||||
stroke="currentColor"
|
||||
@@ -81,16 +92,14 @@ export function LandingHero() {
|
||||
</div>
|
||||
|
||||
<div className="hero-features">
|
||||
{[
|
||||
"Agent-First Operations",
|
||||
"Approval Queues",
|
||||
"Live Signals",
|
||||
].map((label) => (
|
||||
<div key={label} className="hero-feature">
|
||||
<div className="feature-icon">✓</div>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
))}
|
||||
{["Agent-First Operations", "Approval Queues", "Live Signals"].map(
|
||||
(label) => (
|
||||
<div key={label} className="hero-feature">
|
||||
<div className="feature-icon">✓</div>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -104,7 +113,9 @@ export function LandingHero() {
|
||||
</div>
|
||||
<div className="surface-subtitle">
|
||||
<h3>Ship work without losing the thread.</h3>
|
||||
<p>Tasks, approvals, and agent status stay synced across the board.</p>
|
||||
<p>
|
||||
Tasks, approvals, and agent status stay synced across the board.
|
||||
</p>
|
||||
</div>
|
||||
<div className="metrics-row">
|
||||
{[
|
||||
@@ -266,4 +277,3 @@ export function LandingHero() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { TaskCard } from "@/components/molecules/TaskCard";
|
||||
import { parseApiDatetime } from "@/lib/datetime";
|
||||
@@ -140,6 +147,7 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const cardRefsSnapshot = cardRefs.current;
|
||||
if (animationRafRef.current !== null) {
|
||||
window.cancelAnimationFrame(animationRafRef.current);
|
||||
animationRafRef.current = null;
|
||||
@@ -149,7 +157,7 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
cleanupTimeoutRef.current = null;
|
||||
}
|
||||
for (const taskId of animatedTaskIdsRef.current) {
|
||||
const element = cardRefs.current.get(taskId);
|
||||
const element = cardRefsSnapshot.get(taskId);
|
||||
if (!element) continue;
|
||||
element.style.transform = "";
|
||||
element.style.transition = "";
|
||||
@@ -182,7 +190,7 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
const dx = prev.left - next.left;
|
||||
const dy = prev.top - next.top;
|
||||
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) continue;
|
||||
const element = cardRefs.current.get(taskId);
|
||||
const element = cardRefsSnapshot.get(taskId);
|
||||
if (!element) continue;
|
||||
moved.push({ taskId, element, dx, dy });
|
||||
}
|
||||
@@ -229,7 +237,7 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
cleanupTimeoutRef.current = null;
|
||||
}
|
||||
for (const taskId of animatedTaskIdsRef.current) {
|
||||
const element = cardRefs.current.get(taskId);
|
||||
const element = cardRefsSnapshot.get(taskId);
|
||||
if (!element) continue;
|
||||
element.style.transform = "";
|
||||
element.style.transition = "";
|
||||
@@ -302,10 +310,10 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
};
|
||||
|
||||
const handleDragLeave = (status: TaskStatus) => () => {
|
||||
if (activeColumn === status) {
|
||||
setActiveColumn(null);
|
||||
}
|
||||
};
|
||||
if (activeColumn === status) {
|
||||
setActiveColumn(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -343,9 +351,14 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
? columnTasks.filter((task) => {
|
||||
if (reviewBucket === "blocked") return Boolean(task.is_blocked);
|
||||
if (reviewBucket === "approval_needed")
|
||||
return (task.approvals_pending_count ?? 0) > 0 && !task.is_blocked;
|
||||
return (
|
||||
(task.approvals_pending_count ?? 0) > 0 && !task.is_blocked
|
||||
);
|
||||
if (reviewBucket === "waiting_lead")
|
||||
return !task.is_blocked && (task.approvals_pending_count ?? 0) === 0;
|
||||
return (
|
||||
!task.is_blocked &&
|
||||
(task.approvals_pending_count ?? 0) === 0
|
||||
);
|
||||
return true;
|
||||
})
|
||||
: columnTasks;
|
||||
@@ -393,7 +406,11 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
label: "Lead review",
|
||||
count: reviewCounts.waiting_lead,
|
||||
},
|
||||
{ key: "blocked", label: "Blocked", count: reviewCounts.blocked },
|
||||
{
|
||||
key: "blocked",
|
||||
label: "Blocked",
|
||||
count: reviewCounts.blocked,
|
||||
},
|
||||
] as const
|
||||
).map((option) => (
|
||||
<button
|
||||
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
Trello,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function UserMenu({ className }: { className?: string }) {
|
||||
|
||||
@@ -12,5 +12,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return <ClerkProvider publishableKey={publishableKey}>{children}</ClerkProvider>;
|
||||
return (
|
||||
<ClerkProvider publishableKey={publishableKey}>{children}</ClerkProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
retry: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, isClerkEnabled } from "@/auth/clerk";
|
||||
import {
|
||||
SignInButton,
|
||||
SignedIn,
|
||||
SignedOut,
|
||||
isClerkEnabled,
|
||||
} from "@/auth/clerk";
|
||||
|
||||
import { UserMenu } from "@/components/organisms/UserMenu";
|
||||
|
||||
|
||||
@@ -11,23 +11,22 @@ const badgeVariants = cva(
|
||||
default: "bg-[color:var(--surface-muted)] text-strong",
|
||||
outline:
|
||||
"border border-[color:var(--border-strong)] text-[color:var(--text-muted)]",
|
||||
accent: "bg-[color:var(--accent-soft)] text-[color:var(--accent-strong)]",
|
||||
success:
|
||||
"bg-[color:rgba(15,118,110,0.14)] text-[color:var(--success)]",
|
||||
warning:
|
||||
"bg-[color:rgba(180,83,9,0.15)] text-[color:var(--warning)]",
|
||||
danger:
|
||||
"bg-[color:rgba(180,35,24,0.15)] text-[color:var(--danger)]",
|
||||
accent:
|
||||
"bg-[color:var(--accent-soft)] text-[color:var(--accent-strong)]",
|
||||
success: "bg-[color:rgba(15,118,110,0.14)] text-[color:var(--success)]",
|
||||
warning: "bg-[color:rgba(180,83,9,0.15)] text-[color:var(--warning)]",
|
||||
danger: "bg-[color:rgba(180,35,24,0.15)] text-[color:var(--danger)]",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
extends
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
|
||||
@@ -16,7 +16,8 @@ const buttonVariants = cva(
|
||||
"border border-[color:var(--border)] bg-[color:var(--surface)] text-strong hover:border-[color:var(--accent)] hover:text-[color:var(--accent)]",
|
||||
outline:
|
||||
"border border-[color:var(--border-strong)] bg-transparent text-strong hover:border-[color:var(--accent)] hover:text-[color:var(--accent)]",
|
||||
ghost: "bg-transparent text-strong hover:bg-[color:var(--surface-strong)]",
|
||||
ghost:
|
||||
"bg-transparent text-strong hover:bg-[color:var(--surface-strong)]",
|
||||
},
|
||||
size: {
|
||||
sm: "h-9 px-4",
|
||||
@@ -28,11 +29,12 @@ const buttonVariants = cva(
|
||||
variant: "primary",
|
||||
size: "md",
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
extends
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
@@ -42,7 +44,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
),
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
|
||||
@@ -2,15 +2,16 @@ import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-2xl surface-card", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-2xl surface-card", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
|
||||
@@ -18,7 +18,7 @@ const DialogOverlay = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-slate-950/40 backdrop-blur-[2px] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"w-full max-w-2xl max-h-[calc(100vh-2rem)] overscroll-contain overflow-y-auto rounded-3xl border border-[color:var(--border)] bg-[color:var(--surface)] p-6 shadow-lush focus:outline-none supports-[height:100dvh]:max-h-[calc(100dvh-2rem)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -4,7 +4,11 @@ import * as React from "react";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
|
||||
@@ -2,19 +2,20 @@ import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
||||
({ className, type, ...props }, ref) => (
|
||||
<input
|
||||
ref={ref}
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-4 text-sm text-strong shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
const Input = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
React.InputHTMLAttributes<HTMLInputElement>
|
||||
>(({ className, type, ...props }, ref) => (
|
||||
<input
|
||||
ref={ref}
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-4 text-sm text-strong shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
|
||||
@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex h-11 w-full cursor-pointer items-center justify-between rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-4 text-sm text-strong shadow-sm focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)] focus:ring-offset-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -37,7 +37,10 @@ const SelectScrollUpButton = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-pointer items-center justify-center py-1", className)}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
@@ -51,7 +54,10 @@ const SelectScrollDownButton = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-pointer items-center justify-center py-1", className)}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
@@ -97,7 +103,10 @@ const SelectLabel = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold text-strong", className)}
|
||||
className={cn(
|
||||
"py-1.5 pl-8 pr-2 text-sm font-semibold text-strong",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -15,7 +15,7 @@ const TabsList = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-full border border-[color:var(--border)] bg-[color:var(--surface)] p-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -30,7 +30,7 @@ const TabsTrigger = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-2 text-xs font-semibold text-muted transition data-[state=active]:bg-[color:var(--accent)] data-[state=active]:text-white",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,7 @@ const Textarea = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"min-h-[120px] w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-4 py-3 text-sm text-strong shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@ const TooltipContent = React.forwardRef<
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"rounded-lg bg-slate-900 px-3 py-2 text-xs font-semibold text-white shadow-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user