mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 21:07:40 +01:00
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:
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 } },
|
||||
};
|
||||
Reference in new issue
Block a user