Design pass (all themes + chat UI), model picker, reduce-motion toggle
- Aero: blue gradients back on the active segment and every primary
(Review due, the card's Study button, the composer's send button); a
frosted backing behind the page heading and the All decks label; a
bottom-weighted rail tint; chat header/composer/welcome panels; calmer
orbs; stronger card veil + a stronger blur bake.
- Dark/Light: a dedicated --track token makes progress bars read (the
first attempt shifted --surface-sunken and the pixel scan caught the
sheet-contrast regression); brighter dark secondary text; darker light
segmented labels and difficulty tints; stronger light card border and
shadow.
- Chat UI: friendly model chip ("Gemma 4 31B") with the raw name in a
tooltip, larger chips, plain centered disclaimer, vertically centered
welcome panel, distinct chat-plus icon for new chat.
- Chat models: every chat-capable model from the server's /api/tags is
selectable (favourites first, search field when long, live-list
validation in the route); default CHAT_MODEL_DEFAULT or gemma4:31b.
- Reduce motion: Settings toggle (default off) that parks the scene,
collapses CSS animations and drives framer-motion's calm mode.
- Folders hint, "New" badge on fresh decks.
Contrast: 0 findings, axe clean, mobile 126/126, chat 102/102, wizard
95/95, regression 44/44, i18n 13/13, native 33/33.
Version 0.4.6, SW -> freezmo-v71.
This commit is contained in:
parent
b70b0e83cf
commit
6635b5d405
29 changed files with 468 additions and 90 deletions
27
lib/utils.ts
27
lib/utils.ts
|
|
@ -57,3 +57,30 @@ export function parseJsonLoose(text: string): unknown {
|
|||
export function truncate(value: string, max: number): string {
|
||||
return value.length > max ? `${value.slice(0, max - 1)}…` : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* A friendlier label for a model name: "gemma4:31b-cloud" → "Gemma 4 31B",
|
||||
* "gpt-oss:120b" → "GPT OSS 120B". Chips show this; the raw name lives in a
|
||||
* tooltip (and the picker keeps the technical names).
|
||||
*/
|
||||
export function prettyModelName(name: string): string {
|
||||
const base = name.replace(/:cloud$/i, "").replace(/-cloud$/i, "");
|
||||
const [family, ...tagParts] = base.split(":");
|
||||
const words = family
|
||||
.replace(/[-_.]+/g, " ")
|
||||
.replace(/([a-z])(\d)/gi, "$1 $2")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
const prettyFamily = words
|
||||
.map((word) =>
|
||||
/^[a-z]{1,3}$/i.test(word) && word === word.toLowerCase()
|
||||
? word.toUpperCase()
|
||||
: word.charAt(0).toUpperCase() + word.slice(1),
|
||||
)
|
||||
.join(" ");
|
||||
const tag = tagParts
|
||||
.join(" ")
|
||||
.replace(/^(\d+)([a-z])$/i, (_match, size: string, unit: string) => `${size}${unit.toUpperCase()}`);
|
||||
return [prettyFamily, tag].filter(Boolean).join(" ");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue