This commit is contained in:
k
2026-09-24 10:41:57 +02:00
parent 4005ce0631
commit 8e64c0c963
4 changed files with 187 additions and 17 deletions
+6
View File
@@ -109,6 +109,7 @@ const en = {
"chat.greet": "How can I help, {name}?",
"chat.greetSub": "Pick a model, then type below. Markdown is supported.",
"chat.thinking": "Thinking…",
"chat.busyOther": "A reply is still generating in another chat — please wait.",
"chat.placeholder": "Message KisAssistant…",
"chat.send": "Send",
"chat.attachTip": "Attach a file",
@@ -690,6 +691,7 @@ const es: Dict = {
"chat.greet": "¿En qué te ayudo, {name}?",
"chat.greetSub": "Elige un modelo y escribe abajo. Markdown soportado.",
"chat.thinking": "Pensando…",
"chat.busyOther": "A reply is still generating in another chat — please wait.",
"chat.placeholder": "Escribe a KisAssistant…",
"chat.send": "Enviar",
"chat.attachTip": "Adjuntar un archivo",
@@ -1262,6 +1264,7 @@ const fr: Dict = {
"chat.greetSub":
"Choisissez un modèle, puis écrivez ci-dessous. Markdown pris en charge.",
"chat.thinking": "Réflexion…",
"chat.busyOther": "A reply is still generating in another chat — please wait.",
"chat.placeholder": "Écrivez à KisAssistant…",
"chat.send": "Envoyer",
"chat.attachTip": "Joindre un fichier",
@@ -1832,6 +1835,7 @@ const ru: Dict = {
"chat.greet": "Чем помочь, {name}?",
"chat.greetSub": "Выберите модель и пишите ниже. Поддерживается Markdown.",
"chat.thinking": "Думаю…",
"chat.busyOther": "A reply is still generating in another chat — please wait.",
"chat.placeholder": "Сообщение KisAssistant…",
"chat.send": "Отправить",
"chat.attachTip": "Прикрепить файл",
@@ -2390,6 +2394,7 @@ const it: Dict = {
"chat.greet": "Come posso aiutare, {name}?",
"chat.greetSub": "Scegli un modello e scrivi qui sotto. Markdown supportato.",
"chat.thinking": "Penso…",
"chat.busyOther": "A reply is still generating in another chat — please wait.",
"chat.placeholder": "Scrivi a KisAssistant…",
"chat.send": "Invia",
"chat.attachTip": "Allega un file",
@@ -2954,6 +2959,7 @@ const pt: Dict = {
"chat.greet": "Como posso ajudar, {name}?",
"chat.greetSub": "Escolhe um modelo e escreve abaixo. Markdown suportado.",
"chat.thinking": "A pensar…",
"chat.busyOther": "A reply is still generating in another chat — please wait.",
"chat.placeholder": "Escreve ao KisAssistant…",
"chat.send": "Enviar",
"chat.attachTip": "Anexar ficheiro",
+99
View File
@@ -0,0 +1,99 @@
/**
* Browser-local UI persistence (no server round-trip, per browser profile).
*
* - Selected model per account (`ka_model:<username>`): restored on boot when
* still offered, otherwise the first available model wins.
* - Failed model attempts (`ka_failed_v1`): the error bubble + retry state
* survive a refresh. The fake error text is NEVER stored server-side, so
* model history stays clean — the bubble is re-created locally on load and
* filtered out of every outgoing payload.
*/
export interface FailedEntry {
/** Position of the error bubble in the message list (trailing). */
index: number;
retryCount: number;
at: number;
}
const FAILED_KEY = "ka_failed_v1";
const FAILED_MAX_AGE_MS = 30 * 24 * 3600_000;
function readFailedMap(): Record<string, FailedEntry> {
try {
const raw = localStorage.getItem(FAILED_KEY);
if (!raw) return {};
const obj = JSON.parse(raw) as Record<string, unknown>;
const out: Record<string, FailedEntry> = {};
const now = Date.now();
for (const [k, v] of Object.entries(obj ?? {})) {
const e = v as FailedEntry;
if (
v !== null &&
typeof v === "object" &&
Number.isInteger(e.index) &&
e.index >= 0 &&
typeof e.retryCount === "number" &&
typeof e.at === "number" &&
now - e.at < FAILED_MAX_AGE_MS
) {
out[k] = { index: e.index, retryCount: e.retryCount, at: e.at };
}
}
return out;
} catch {
return {};
}
}
function writeFailedMap(map: Record<string, FailedEntry>): void {
try {
localStorage.setItem(FAILED_KEY, JSON.stringify(map));
} catch {
// Private mode / quota — retry simply won't survive a refresh.
}
}
export function getFailedEntry(convId: string): FailedEntry | null {
return readFailedMap()[convId] ?? null;
}
export function setFailedEntry(convId: string, entry: FailedEntry): void {
const map = readFailedMap();
map[convId] = entry;
writeFailedMap(map);
}
export function clearFailedEntry(convId: string): void {
const map = readFailedMap();
if (map[convId] !== undefined) {
delete map[convId];
writeFailedMap(map);
}
}
function modelKey(username: string): string {
return `ka_model:${username.trim().toLowerCase()}`;
}
export function getSavedModel(username: string): string | null {
try {
return localStorage.getItem(modelKey(username));
} catch {
return null;
}
}
export function setSavedModel(username: string, id: string): void {
try {
localStorage.setItem(modelKey(username), id);
} catch {
// ignore — falls back to first model next boot
}
}
/** Saved pick when still offered, otherwise the first available model. */
export function pickModel(available: { id: string }[], saved: string | null): string {
if (saved && available.some((m) => m.id === saved)) return saved;
return available[0]?.id ?? "";
}