mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
feat(lens): show the queue reason and what the worker is doing in the live strip
The progress header and the strip replace "Queued for your worker" with the concrete reason. While waiting, the strip lists the busy worker's investigations; click one to open it.
This commit is contained in:
parent
12738b0d79
commit
a75dab2f41
7 changed files with 114 additions and 9 deletions
|
|
@ -18,18 +18,30 @@ import { durationText } from "../model/format";
|
|||
import { nextCheckStatus } from "../model/status";
|
||||
import { type Lens, type Job } from "../model/types";
|
||||
import { cn } from "@/lib/cva.config";
|
||||
import { QueueReasonText } from "./QueueReasonText";
|
||||
import { useQueueReason, type QueueContext } from "./useQueueReason";
|
||||
|
||||
const steps = ["review runs", "find patterns", "check evidence"];
|
||||
const markers = { done: "✓", active: "▸", todo: "·" };
|
||||
const blocks = 32;
|
||||
const QUEUED_TITLE = { busy: "Waiting in line", no_worker: "Waiting for a worker", starting: "Picking up" } as const;
|
||||
|
||||
function stageState(index: number, current: number): keyof typeof markers {
|
||||
if (index < current) return "done";
|
||||
return index === current ? "active" : "todo";
|
||||
}
|
||||
|
||||
export function InvestigationProgress({ job, onCancel }: { job: Job; onCancel?: () => void }) {
|
||||
export function InvestigationProgress({
|
||||
job,
|
||||
queue,
|
||||
onCancel,
|
||||
}: {
|
||||
job: Job;
|
||||
queue?: QueueContext;
|
||||
onCancel?: () => void;
|
||||
}) {
|
||||
const now = useNow(1000);
|
||||
const reason = useQueueReason(job, queue);
|
||||
const progress = analysisProgress(job);
|
||||
const fraction = analysisFraction(progress);
|
||||
const [samples, setSamples] = useState<ProgressSample[]>([]);
|
||||
|
|
@ -53,7 +65,7 @@ export function InvestigationProgress({ job, onCancel }: { job: Job; onCancel?:
|
|||
<section aria-label="Analysis progress" className="space-y-3 rounded-md border bg-muted/40 px-4 py-3 text-sm">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="truncate" role="status">
|
||||
<span className="font-medium">{progress.title}</span>
|
||||
<span className="font-medium">{reason ? QUEUED_TITLE[reason.kind] : progress.title}</span>
|
||||
{!queued && <span className="text-muted-foreground"> · {progress.detail}</span>}
|
||||
</span>
|
||||
{onCancel && (
|
||||
|
|
@ -117,7 +129,9 @@ export function InvestigationProgress({ job, onCancel }: { job: Job; onCancel?:
|
|||
</ol>
|
||||
<div className="flex flex-wrap gap-x-5 gap-y-1 pt-1 tabular-nums">
|
||||
{queued ? (
|
||||
<span className="text-muted-foreground">{progress.detail}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{reason ? <QueueReasonText reason={reason} onConnect={queue?.onConnect} /> : progress.detail}
|
||||
</span>
|
||||
) : (
|
||||
stats.map(([key, value]) => (
|
||||
<span key={key}>
|
||||
|
|
|
|||
|
|
@ -281,6 +281,12 @@ export function InvestigationsView({
|
|||
connected={connected}
|
||||
results={results}
|
||||
agents={Array.isArray(agents.data) ? agents.data : []}
|
||||
queue={{
|
||||
lenses,
|
||||
workers: query.data?.workers ?? [],
|
||||
onConnect: () => setWorkerSetup(true),
|
||||
onOpenLens: selectLens,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{editing && (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
"use client";
|
||||
|
||||
import { queueReasonText, workerTaskText, type QueueReason } from "../model/status";
|
||||
import type { QueueContext } from "./useQueueReason";
|
||||
|
||||
export function QueueReasonText({ reason, onConnect }: { reason: QueueReason; onConnect?: () => void }) {
|
||||
if (reason.kind !== "no_worker" || !onConnect) return <>{queueReasonText(reason)}</>;
|
||||
return (
|
||||
<>
|
||||
No worker connected. Start one from{" "}
|
||||
<button type="button" onClick={onConnect} className="font-medium text-foreground underline underline-offset-2">
|
||||
Connect worker
|
||||
</button>
|
||||
.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkerTasks({ reason, queue }: { reason: QueueReason; queue?: QueueContext }) {
|
||||
if (reason.kind === "no_worker" || !reason.tasks.length) return null;
|
||||
return (
|
||||
<ol aria-label="What the worker is doing" className="flex flex-col">
|
||||
{reason.tasks.map((task) => (
|
||||
<li key={task.lensId}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!queue}
|
||||
onClick={() => queue?.onOpenLens(task.lensId)}
|
||||
className="w-full truncate rounded px-1.5 py-0.5 text-left tabular-nums hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
{workerTaskText(task)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
|||
import { RunsTab } from "./RunsTab";
|
||||
import { InvestigationProgress } from "../InvestigationProgress";
|
||||
import { LiveRun } from "../live/LiveRun";
|
||||
import { type QueueContext } from "../useQueueReason";
|
||||
import { liveJob } from "../../model/live";
|
||||
import { RunNowDialog } from "./RunNowDialog";
|
||||
import { useState } from "react";
|
||||
|
|
@ -32,6 +33,7 @@ export function InvestigationDetail({
|
|||
connected,
|
||||
results,
|
||||
agents = [],
|
||||
queue,
|
||||
}: {
|
||||
lens: Lens;
|
||||
readOnly: boolean;
|
||||
|
|
@ -43,6 +45,7 @@ export function InvestigationDetail({
|
|||
connected: boolean;
|
||||
results: ReturnType<typeof useInvestigationResults>;
|
||||
agents?: readonly string[];
|
||||
queue?: QueueContext;
|
||||
}) {
|
||||
const [runNow, setRunNow] = useState(false);
|
||||
const {
|
||||
|
|
@ -102,10 +105,11 @@ export function InvestigationDetail({
|
|||
<InvestigationProgress
|
||||
key={active.id}
|
||||
job={active}
|
||||
queue={queue}
|
||||
onCancel={readOnly ? undefined : () => void update((api) => api.cancelRun(lens.id))}
|
||||
/>
|
||||
)}
|
||||
{live && <LiveRun key={live.id} job={live} reviews={live.reviews} name={lens.settings.name} />}
|
||||
{live && <LiveRun key={live.id} job={live} reviews={live.reviews} name={lens.settings.name} queue={queue} />}
|
||||
{runNow && (
|
||||
<RunNowDialog
|
||||
lens={lens}
|
||||
|
|
|
|||
|
|
@ -13,14 +13,28 @@ import {
|
|||
shownCount,
|
||||
stripState,
|
||||
} from "../../model/live";
|
||||
import { queueReasonText } from "../../model/status";
|
||||
import type { Job, Review } from "../../model/types";
|
||||
import { QueueReasonText, WorkerTasks } from "../QueueReasonText";
|
||||
import { useQueueReason, type QueueContext } from "../useQueueReason";
|
||||
import { LiveDrawer } from "./LiveDrawer";
|
||||
import { LiveStrip } from "./LiveStrip";
|
||||
import { useStripOpen } from "./useLivePanels";
|
||||
import { useReviewPlayback } from "./useReviewPlayback";
|
||||
|
||||
export function LiveRun({ job, reviews, name }: { job: Job; reviews: readonly Review[]; name: string }) {
|
||||
export function LiveRun({
|
||||
job,
|
||||
reviews,
|
||||
name,
|
||||
queue,
|
||||
}: {
|
||||
job: Job;
|
||||
reviews: readonly Review[];
|
||||
name: string;
|
||||
queue?: QueueContext;
|
||||
}) {
|
||||
const live = job.status === "queued" || job.status === "running";
|
||||
const reason = useQueueReason(job, queue);
|
||||
const [stripOpen, setStripOpen] = useStripOpen();
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [pinned, setPinned] = useState<string | null>(null);
|
||||
|
|
@ -42,7 +56,15 @@ export function LiveRun({ job, reviews, name }: { job: Job; reviews: readonly Re
|
|||
{stripOpen ? (
|
||||
<LiveStrip
|
||||
model={model}
|
||||
state={stripState(withReviews, model)}
|
||||
state={stripState(withReviews, model, reason ? queueReasonText(reason) : undefined)}
|
||||
waiting={
|
||||
reason && (
|
||||
<>
|
||||
<QueueReasonText reason={reason} onConnect={queue?.onConnect} />
|
||||
<WorkerTasks reason={reason} queue={queue} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
playback={playback}
|
||||
reviewed={reviewed}
|
||||
selected={job.coverage.selected}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { PanelRight, X } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { ProviderLogo } from "@/components/molecules/models/ProviderLogo";
|
||||
import { agoLabel } from "@/components/view_logs/TraceView/lensField";
|
||||
|
|
@ -63,11 +64,13 @@ export function LiveStrip({
|
|||
selected,
|
||||
issues,
|
||||
cost,
|
||||
waiting,
|
||||
onOpen,
|
||||
onClose,
|
||||
}: {
|
||||
model: string;
|
||||
state: StripState;
|
||||
waiting?: ReactNode;
|
||||
playback: Pick<Playback, "played" | "current">;
|
||||
reviewed: number;
|
||||
selected: number;
|
||||
|
|
@ -96,9 +99,9 @@ export function LiveStrip({
|
|||
</p>
|
||||
)}
|
||||
{state.kind === "waiting" && (
|
||||
<p role="status" className="py-0.5">
|
||||
{state.message}
|
||||
</p>
|
||||
<div role="status" className="flex flex-col gap-0.5 py-0.5">
|
||||
{waiting ?? state.message}
|
||||
</div>
|
||||
)}
|
||||
{recent.length > 0 && state.kind !== "waiting" && (
|
||||
<ol aria-label="Recently reviewed traces" className="flex flex-col">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
"use client";
|
||||
|
||||
import { useNow } from "@/hooks/useNow";
|
||||
|
||||
import { queueReason, type QueueReason } from "../model/status";
|
||||
import type { Job, Lens, LensList } from "../model/types";
|
||||
|
||||
export interface QueueContext {
|
||||
lenses: readonly Lens[];
|
||||
workers: readonly LensList["workers"][number][];
|
||||
onConnect: () => void;
|
||||
onOpenLens: (id: string) => void;
|
||||
}
|
||||
|
||||
export function useQueueReason(job: Job, queue: QueueContext | undefined): QueueReason | null {
|
||||
const now = useNow(1000);
|
||||
if (job.status !== "queued" || !queue) return null;
|
||||
return queueReason(job, queue.lenses, queue.workers, now);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue