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
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { authErrorMessage, authErrorKind, getServerConfig, resolveAuth, resolveModelName } from "@/lib/config";
|
||||
import { friendlyErrorMessage, ollamaChatStream, OllamaError } from "@/lib/ollama";
|
||||
import { authErrorMessage, authErrorKind, canonicalModelName, getServerConfig, resolveAuth, resolveModelName } from "@/lib/config";
|
||||
import { friendlyErrorMessage, ollamaChatStream, ollamaListModels, OllamaError } from "@/lib/ollama";
|
||||
import { rejectCrossOrigin, rejectOversizedBody, requestLocale } from "@/lib/http-guard";
|
||||
import { clientKey, rateLimit, rateLimitResponse } from "@/lib/rate-limit";
|
||||
import { acquireModelGate } from "@/lib/model-guard";
|
||||
|
|
@ -98,14 +98,28 @@ export async function POST(request: Request) {
|
|||
|
||||
const { messages, context } = parsed.data;
|
||||
|
||||
// The model must come from the server's allowlist — never from the client.
|
||||
// The model must be one the server actually offers — the configured
|
||||
// favourites, or anything the user's Ollama lists. The picker shows exactly
|
||||
// that list, so nothing selectable is ever rejected; an unknown model still
|
||||
// gets the 400.
|
||||
const allowed = config.chatModels.length > 0 ? config.chatModels : [config.textModel];
|
||||
const model = parsed.data.model ?? allowed[0];
|
||||
if (!allowed.some((entry) => entry.toLowerCase() === model.toLowerCase())) {
|
||||
return Response.json(
|
||||
{ error: translate(locale, "server.chatModelNotAllowed"), kind: "bad_model" },
|
||||
{ status: 400 },
|
||||
);
|
||||
const wanted = canonicalModelName(model);
|
||||
const inFavourites = allowed.some((entry) => canonicalModelName(entry) === wanted);
|
||||
if (!inFavourites) {
|
||||
let listed = false;
|
||||
try {
|
||||
const models = await ollamaListModels(auth.auth, config.healthTimeoutMs);
|
||||
listed = models.some((entry) => canonicalModelName(entry.name) === wanted);
|
||||
} catch {
|
||||
listed = false;
|
||||
}
|
||||
if (!listed) {
|
||||
return Response.json(
|
||||
{ error: translate(locale, "server.chatModelNotAllowed"), kind: "bad_model" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const last = messages[messages.length - 1];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue