From fad0f7007a94f08c858fbffdde78607b3f98ff4d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 21:33:26 +0000 Subject: [PATCH] storybook: model Jev task routing on the OpenRouter decisions API Jev (typesafe/jev-1.13) answers typed choice questions with probabilities and does not generate text. Split suggestions accordingly: Jev routes type, assignee, project, and work mode; a separate text model drafts the title. The prototype now builds the real decisions request, simulates responses in the documented shape, shows per-option probabilities in chip menus, and replaces the invented reasons with Jev's confidence and token cost. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SWn6etZjfGz6p8NWYkCQve --- .../jev-task-creation/JevTaskComposer.tsx | 414 +++++++++++------- .../prototypes/jev-task-creation/README.md | 90 ++-- .../jev-task-creation/jev-classifier.ts | 332 +++++++++----- .../stories/jev-task-creation.stories.tsx | 38 +- 4 files changed, 564 insertions(+), 310 deletions(-) diff --git a/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx b/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx index dd7c6d3dd55..2a9e098dc64 100644 --- a/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx +++ b/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx @@ -9,41 +9,50 @@ import { cn } from "@/lib/utils"; import { workModeMetaFor, workModeMetaList } from "@/lib/work-mode-meta"; import { JEV_AGENTS, + JEV_CONFIDENCE_THRESHOLD, JEV_MIN_PROMPT_LENGTH, + JEV_MODEL, JEV_NAME, + JEV_NO_PROJECT, JEV_PROJECTS, JEV_TASK_TYPES, - classifyTaskPrompt, - type JevSuggestion, + buildJevDecisionRequest, + draftTaskTitle, + routeTaskWithJev, + type JevField, + type JevRouting, type JevTaskType, } from "./jev-classifier"; export type JevTaskComposerProps = { /** - * `suggest-first`: Jev fills title and properties while you type; you review and create. + * `suggest-first`: Jev routes and a title is drafted while you type; you review and create. * `instant`: like starting a Claude Code session. Create right away; the task opens as - * "Untitled" and Jev names, classifies, and assigns it a moment later. + * "Untitled" and is named and routed a moment later. */ flow?: "suggest-first" | "instant"; initialPrompt?: string; - /** Simulated Jev latency in milliseconds. */ + /** Simulated Jev decision latency. Jev is a fast decision model. */ latencyMs?: number; - /** Simulate Jev being unavailable; the composer falls back to manual fields. */ + /** Simulated latency of the separate text model that drafts the title. */ + titleLatencyMs?: number; + /** Simulate Jev being unavailable; routing falls back to manual fields. */ jevUnavailable?: boolean; - /** Pre-set fields as if the user already chose them. Jev never overwrites these. */ + /** Pre-set the assignee as if the user already chose it. Jev never overwrites it. */ presetAssigneeId?: string; /** Types `initialPrompt` in character by character to show suggestions arriving. */ typeOnMount?: boolean; mobile?: boolean; }; -type Field = "title" | "type" | "assigneeId" | "projectId" | "workMode"; -type Overrides = Partial<{ title: string; type: JevTaskType; assigneeId: string; projectId: string | null; workMode: IssueWorkMode }>; -type JevStatus = "idle" | "thinking" | "ready" | "error"; -type CreatedTask = { identifier: string; prompt: string }; +type Overrides = Partial<{ title: string; type: JevTaskType; assignee: string; project: string | null; workMode: IssueWorkMode }>; +type OverrideField = keyof Overrides; +type Status = "idle" | "thinking" | "ready" | "error"; const TYPING_DEBOUNCE_MS = 650; +const percent = (value: number | undefined) => (value === undefined ? "" : `${Math.round(value * 100)}%`); + /** Types a newly suggested title in, like a session title arriving. Unchanged titles don't retype. */ function useTypewriter(target: string | null) { const [shown, setShown] = useState(""); @@ -68,23 +77,27 @@ function useTypewriter(target: string | null) { export function JevTaskComposer({ flow = "suggest-first", initialPrompt = "", - latencyMs = 900, + latencyMs = 350, + titleLatencyMs = 1200, jevUnavailable = false, presetAssigneeId, typeOnMount = false, mobile = false, }: JevTaskComposerProps) { const [prompt, setPrompt] = useState(typeOnMount ? "" : initialPrompt); - const [status, setStatus] = useState("idle"); - const [suggestion, setSuggestion] = useState(null); - const [overrides, setOverrides] = useState(presetAssigneeId ? { assigneeId: presetAssigneeId } : {}); + const [routingStatus, setRoutingStatus] = useState("idle"); + const [routing, setRouting] = useState(null); + const [titleStatus, setTitleStatus] = useState("idle"); + const [suggestedTitle, setSuggestedTitle] = useState(null); + const [overrides, setOverrides] = useState(presetAssigneeId ? { assignee: presetAssigneeId } : {}); const [editingTitle, setEditingTitle] = useState(false); - const [created, setCreated] = useState(null); - const [whyOpen, setWhyOpen] = useState(false); + const [createdIdentifier, setCreatedIdentifier] = useState(null); + const [createdPrompt, setCreatedPrompt] = useState(""); + const [detailsOpen, setDetailsOpen] = useState(false); const abortRef = useRef(null); - const lastClassifiedRef = useRef(""); + const lastRequestedRef = useRef(""); - // Autoplay: type the scenario prompt so reviewers see Jev react mid-sentence. + // Autoplay: type the scenario prompt so reviewers see suggestions react mid-sentence. useEffect(() => { if (!typeOnMount || !initialPrompt) return; let index = 0; @@ -96,52 +109,56 @@ export function JevTaskComposer({ return () => window.clearInterval(timer); }, [typeOnMount, initialPrompt]); - const runJev = useCallback((text: string) => { + const suggest = useCallback((text: string) => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; - lastClassifiedRef.current = text; - setStatus("thinking"); - classifyTaskPrompt(text, { latencyMs, fail: jevUnavailable, signal: controller.signal }) - .then((result) => { - setSuggestion(result); - setStatus("ready"); - }) - .catch((error: unknown) => { - if (error instanceof DOMException && error.name === "AbortError") return; - setStatus("error"); - }); - }, [latencyMs, jevUnavailable]); + lastRequestedRef.current = text; + const ignoreAbort = (error: unknown) => error instanceof DOMException && error.name === "AbortError"; - // Suggest-first: re-classify after the user pauses typing. + setRoutingStatus("thinking"); + routeTaskWithJev(text, { latencyMs, fail: jevUnavailable, signal: controller.signal }) + .then((result) => { setRouting(result); setRoutingStatus("ready"); }) + .catch((error: unknown) => { if (!ignoreAbort(error)) setRoutingStatus("error"); }); + + setTitleStatus("thinking"); + draftTaskTitle(text, { latencyMs: titleLatencyMs, signal: controller.signal }) + .then((title) => { setSuggestedTitle(title); setTitleStatus("ready"); }) + .catch((error: unknown) => { if (!ignoreAbort(error)) setTitleStatus("error"); }); + }, [latencyMs, titleLatencyMs, jevUnavailable]); + + // Suggest-first: re-run after the user pauses typing. useEffect(() => { - if (flow !== "suggest-first" || created) return; + if (flow !== "suggest-first" || createdIdentifier) return; const text = prompt.trim(); if (text.length < JEV_MIN_PROMPT_LENGTH) { abortRef.current?.abort(); - setStatus("idle"); - setSuggestion(null); + lastRequestedRef.current = ""; + setRoutingStatus("idle"); + setTitleStatus("idle"); + setRouting(null); + setSuggestedTitle(null); return; } - if (text === lastClassifiedRef.current) return; - const timer = window.setTimeout(() => runJev(text), TYPING_DEBOUNCE_MS); + if (text === lastRequestedRef.current) return; + const timer = window.setTimeout(() => suggest(text), TYPING_DEBOUNCE_MS); return () => window.clearTimeout(timer); - }, [prompt, flow, created, runJev]); + }, [prompt, flow, createdIdentifier, suggest]); useEffect(() => () => abortRef.current?.abort(), []); const effective = useMemo(() => ({ - title: overrides.title ?? suggestion?.title ?? "", - type: overrides.type ?? suggestion?.type ?? null, - assigneeId: overrides.assigneeId ?? suggestion?.assigneeId ?? null, - projectId: overrides.projectId !== undefined ? overrides.projectId : suggestion?.projectId ?? null, - workMode: overrides.workMode ?? suggestion?.workMode ?? "standard", - }), [overrides, suggestion]); + title: overrides.title ?? suggestedTitle ?? "", + type: overrides.type ?? routing?.type ?? null, + assignee: overrides.assignee ?? routing?.assigneeId ?? null, + project: overrides.project !== undefined ? overrides.project : routing?.projectId ?? null, + workMode: overrides.workMode ?? routing?.workMode ?? "standard", + }), [overrides, routing, suggestedTitle]); - const isSuggested = (field: Field) => suggestion !== null && overrides[field] === undefined; - const setField = (field: K, value: Overrides[K]) => + const fromJev = (field: Exclude) => routing !== null && overrides[field] === undefined; + const setField = (field: K, value: Overrides[K]) => setOverrides((current) => ({ ...current, [field]: value })); - const resetField = (field: Field) => + const resetField = (field: OverrideField) => setOverrides((current) => { const next = { ...current }; delete next[field]; @@ -151,23 +168,29 @@ export function JevTaskComposer({ const canCreate = prompt.trim().length > 0; const create = () => { if (!canCreate) return; - setCreated({ identifier: "PAP-412", prompt: prompt.trim() }); - // Instant flow: Jev runs after creation, like a session title arriving. - if (flow === "instant" || status === "idle" || status === "thinking") runJev(prompt.trim()); + const text = prompt.trim(); + setCreatedIdentifier("PAP-412"); + setCreatedPrompt(text); + // Creating never waits. Anything not yet suggested for this exact prompt runs now. + if (lastRequestedRef.current !== text || routingStatus === "idle") suggest(text); }; const startOver = () => { abortRef.current?.abort(); - lastClassifiedRef.current = ""; + lastRequestedRef.current = ""; setPrompt(""); - setSuggestion(null); + setRouting(null); + setSuggestedTitle(null); setOverrides({}); - setStatus("idle"); - setCreated(null); - setWhyOpen(false); + setRoutingStatus("idle"); + setTitleStatus("idle"); + setCreatedIdentifier(null); + setDetailsOpen(false); }; - const animatedTitle = useTypewriter(isSuggested("title") ? effective.title : null); - const headerTitle = isSuggested("title") ? animatedTitle : effective.title; + const titleSuggested = suggestedTitle !== null && overrides.title === undefined; + const animatedTitle = useTypewriter(titleSuggested ? suggestedTitle : null); + const created = createdIdentifier !== null; + const busy = routingStatus === "thinking" || titleStatus === "thinking"; return (
@@ -178,21 +201,23 @@ export function JevTaskComposer({ )} > setEditingTitle(true)} onCommit={(value) => { setEditingTitle(false); if (value.trim() && value.trim() !== effective.title) setField("title", value.trim()); }} - onReset={overrides.title !== undefined && suggestion ? () => resetField("title") : undefined} + onReset={overrides.title !== undefined && suggestedTitle ? () => resetField("title") : undefined} /> {created ? ( - +
+

{createdPrompt}

+
) : (