storybook: rebuild Jev task composer as a token-only product modal

Rebuild the prototype on the shipped NewIssueDialog chrome: breadcrumb
header that becomes the task name, a borderless auto-growing prompt, a
single [Mode] For [Owner] in [Project] routing row in the shipped compact
control style, a quiet Jev confidence popover, InlineBanner errors, and a
secondary-left, primary-right footer with in-place results instead of a
toast. Motion uses tc-enter-marker and the motion tokens.

Add a per-task estimated cost table covering live Jev routing calls and
the single Claude Haiku 4.5 title call, built from a real Messages API
request body, with a per-1,000-tasks estimate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SWn6etZjfGz6p8NWYkCQve
This commit is contained in:
Claude committed 2026-09-24 22:32:47 +00:00
1 parent e38fb7be45
commit 1dd388e641
4 files changed
+697 -487

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -67,6 +67,46 @@ Two models do the work, because they are good at different things:
- Starting never waits. Whatever Jev has suggested is used, and anything still
pending finishes after.
## Modal design
The composer is the shipped `NewIssueDialog` chrome, rebuilt for prompt-first
creation with product tokens only (no bracket values, hex, or raw durations):
- **Header:** the `PAP ›` breadcrumb. After starting, it becomes `PAP-412 ›` and
the title types in; click it to rename.
- **Body:** the prompt is the hero: borderless, auto-growing, with Enter to start
and Shift+Enter for a new line. Under it, one routing sentence in the shipped
dialog's shape: `[Mode] For [Owner] in [Project]`, using the shipped compact
control style and work-mode chip colors.
- **Jev's presence:** a quiet sparkle and confidence number at the end of the
routing row. It pulses while Jev re-reads and opens the confidence details on
click, instead of an always-visible disclosure.
- **Motion:** changed values fade in with the app's `tc-enter-marker` class and
take a brief accent fill timed by `--motion-duration-slow` and
`--motion-ease-standard`. Both collapse under reduced motion.
- **Footer:** Cancel left, the ↵ / ⇧↵ hint, and Start task right. After starting:
Create another left and Open task right, with the result shown in place rather
than as a toast (DESIGN.md contextual feedback).
- **Errors:** the shipped `InlineBanner` with a Try again action.
- `presentation="inline"` renders the same surface in the page flow for review
pages; the default is the real `Dialog`.
## Estimated cost per task
Every task shows an **Estimated cost** table:
- **Jev · live routing:** every call made while typing, plus a final call if
typing got ahead of routing. Input tokens only, at $0.042 per million; output
tokens are free.
- **Haiku 4.5 · title:** the single title call made on start. `buildTitleRequest`
is the real Messages API body for `claude-haiku-4-5`: a short system
instruction, the prompt, and `max_tokens: 64`. $1 per million input tokens and
$5 per million output tokens.
- **Total**, with a per-1,000-tasks estimate.
Token counts are estimated at about four characters per token. In production,
read `usage` from each response instead.
## Two flows to compare
- **Suggest-first** (stories 01–11c): review the live suggestions, then create.
@@ -19,6 +19,19 @@ import type { IssueWorkMode } from "@paperclipai/shared";
export const JEV_NAME = "Jev";
export const JEV_MODEL = "typesafe/jev-1.13";
/** Jev bills input tokens only; output tokens are free (OpenRouter Jev classification cookbook). */
export const JEV_INPUT_USD_PER_MTOK = 0.042;
/** Title model: Claude Haiku 4.5, Anthropic first-party API pricing. */
export const TITLE_MODEL = "claude-haiku-4-5";
export const TITLE_INPUT_USD_PER_MTOK = 1;
export const TITLE_OUTPUT_USD_PER_MTOK = 5;
export type ModelUsage = { input_tokens: number; output_tokens: number; cost: number };
/** Rough token estimate for the prototype: about four characters per token. Production reads `usage` from each response. */
export function estimateTokens(text: string): number {
return Math.max(1, Math.ceil(text.length / 4));
}
/** Below this assignee confidence, the task goes to the fallback owner (see `fallbackAssigneeId`). */
export const JEV_CONFIDENCE_THRESHOLD = 0.6;
/** Prompts need a little substance before routing starts. */
@@ -44,7 +57,7 @@ export type JevChoiceAnswer = { type: "choice"; choice: string; confidence: numb
export type JevDecisionResponse = {
model: string;
answers: Record<string, JevChoiceAnswer>;
usage: { input_tokens: number; output_tokens: number; cost: number };
usage: ModelUsage;
};
// ---------------------------------------------------------------------------
@@ -298,8 +311,7 @@ export function simulateJevDecisions(request: JevDecisionRequest): JevDecisionRe
const isInvestigation = /^(investigate|research|find out|figure out|look into|compare|evaluate)\b/.test(text);
const modeScores = { standard: 1, planning: isLarge ? 3 : 0, ask: isQuestion ? 3 : isInvestigation ? 1.6 : 0 };
// Rough estimate: about four characters per token.
const inputTokens = Math.round(JSON.stringify(request).length / 4);
const inputTokens = estimateTokens(JSON.stringify(request));
return {
model: request.model,
answers: {
@@ -307,7 +319,7 @@ export function simulateJevDecisions(request: JevDecisionRequest): JevDecisionRe
assignee: choiceAnswer(assigneeProbabilities),
},
// Output tokens are free on Jev; $0.042 per million input tokens.
usage: { input_tokens: inputTokens, output_tokens: 0, cost: (inputTokens * 0.042) / 1_000_000 },
usage: { input_tokens: inputTokens, output_tokens: 0, cost: (inputTokens * JEV_INPUT_USD_PER_MTOK) / 1_000_000 },
};
}
@@ -437,6 +449,29 @@ export function draftTitle(prompt: string): string {
return text.charAt(0).toUpperCase() + text.slice(1);
}
export function draftTaskTitle(prompt: string, options: LatencyOptions = {}): Promise<string> {
return delay(() => draftTitle(prompt), options, "Couldn't draft a title.");
const TITLE_SYSTEM =
"You name tasks for an operator dashboard. Reply with only the title: 3 to 8 words, imperative, sentence case, no quotes or trailing punctuation.";
/** The Messages API request a server would send to Claude Haiku 4.5 when the task is started. */
export function buildTitleRequest(prompt: string) {
return {
model: TITLE_MODEL,
max_tokens: 64,
system: TITLE_SYSTEM,
messages: [{ role: "user" as const, content: prompt }],
};
}
export type TitleDraft = { title: string; usage: ModelUsage };
export function draftTaskTitle(prompt: string, options: LatencyOptions = {}): Promise<TitleDraft> {
return delay(() => {
const title = draftTitle(prompt);
const request = buildTitleRequest(prompt);
// System + user text plus a small allowance for message framing.
const inputTokens = estimateTokens(request.system) + estimateTokens(prompt) + 8;
const outputTokens = estimateTokens(title) + 1;
const cost = (inputTokens * TITLE_INPUT_USD_PER_MTOK + outputTokens * TITLE_OUTPUT_USD_PER_MTOK) / 1_000_000;
return { title, usage: { input_tokens: inputTokens, output_tokens: outputTokens, cost } };
}, options, "Couldn't draft a title.");
}
@@ -122,6 +122,6 @@ export const SlowJev: Story = {
export const Light: Story = { name: "16 · Light theme", args: { initialPrompt: PROMPTS.bug }, globals: { theme: "light" } };
export const Mobile: Story = {
name: "17 · Mobile",
args: { initialPrompt: PROMPTS.design, mobile: true },
args: { initialPrompt: PROMPTS.design },
globals: { viewport: { value: "mobile1", isRotated: false } },
};