ui fixes
This commit is contained in:
+65
-17
@@ -27,6 +27,7 @@ import { ReviewPage } from "./components/ReviewPage.tsx";
|
|||||||
import { ConfirmDialog, LoadingScreen } from "./components/ui.tsx";
|
import { ConfirmDialog, LoadingScreen } from "./components/ui.tsx";
|
||||||
import { Toasts } from "./components/Toasts.tsx";
|
import { Toasts } from "./components/Toasts.tsx";
|
||||||
import { pushToast } from "./lib/toasts.ts";
|
import { pushToast } from "./lib/toasts.ts";
|
||||||
|
import { clearFailedEntry, getFailedEntry, getSavedModel, pickModel, setFailedEntry, setSavedModel } from "./lib/localPrefs.ts";
|
||||||
import { msUntilNextSolarSwitch, resolveThemeDark } from "./lib/solarTheme.ts";
|
import { msUntilNextSolarSwitch, resolveThemeDark } from "./lib/solarTheme.ts";
|
||||||
import { useT } from "./lib/i18n.ts";
|
import { useT } from "./lib/i18n.ts";
|
||||||
|
|
||||||
@@ -126,6 +127,14 @@ export function App() {
|
|||||||
// its conversation, and how many times this prompt was already retried.
|
// its conversation, and how many times this prompt was already retried.
|
||||||
const [failed, setFailed] = useState<{ index: number; convId: string } | null>(null);
|
const [failed, setFailed] = useState<{ index: number; convId: string } | null>(null);
|
||||||
const [retryCount, setRetryCount] = useState(0);
|
const [retryCount, setRetryCount] = useState(0);
|
||||||
|
// Conversation currently receiving a streamed reply (null when idle).
|
||||||
|
// Streaming / thinking / sending UI is rendered ONLY there — every other
|
||||||
|
// conversation stays clean and selectable while a reply lands.
|
||||||
|
const [sendingFor, setSendingFor] = useState<string | null>(null);
|
||||||
|
function setFlight(id: string | null): void {
|
||||||
|
sendingForRef.current = id;
|
||||||
|
setSendingFor(id);
|
||||||
|
}
|
||||||
const { t } = useT();
|
const { t } = useT();
|
||||||
|
|
||||||
// Public routes: no auth needed. IDs are constant for the page lifetime.
|
// Public routes: no auth needed. IDs are constant for the page lifetime.
|
||||||
@@ -199,7 +208,7 @@ export function App() {
|
|||||||
setReportsEnabled(t.reportsEnabled ?? true);
|
setReportsEnabled(t.reportsEnabled ?? true);
|
||||||
setUsernameChangeEnabled(t.usernameChangeEnabled ?? true);
|
setUsernameChangeEnabled(t.usernameChangeEnabled ?? true);
|
||||||
setModels(m.models);
|
setModels(m.models);
|
||||||
if (m.models.length > 0) setModel(m.models[0]!.id);
|
if (m.models.length > 0) setModel(pickModel(m.models, getSavedModel(res.user.username)));
|
||||||
setConvs(c.conversations);
|
setConvs(c.conversations);
|
||||||
setConvsReady(true);
|
setConvsReady(true);
|
||||||
// Deep link: /chat/<id> opens that conversation directly.
|
// Deep link: /chat/<id> opens that conversation directly.
|
||||||
@@ -290,9 +299,24 @@ export function App() {
|
|||||||
// stream lands — the server list is still empty mid-flight, so a refetch
|
// stream lands — the server list is still empty mid-flight, so a refetch
|
||||||
// here would blank the fresh exchange (displayed as a recreated chat).
|
// here would blank the fresh exchange (displayed as a recreated chat).
|
||||||
if (sendingForRef.current === activeId) return;
|
if (sendingForRef.current === activeId) return;
|
||||||
|
const id = activeId;
|
||||||
api
|
api
|
||||||
.getConv(activeId)
|
.getConv(id)
|
||||||
.then((res) => setMessages(res.messages))
|
.then((res) => {
|
||||||
|
if (activeIdRef.current !== id || sendingForRef.current === id) return;
|
||||||
|
setMessages(res.messages);
|
||||||
|
// A model error is persisted browser-side (never server-side, so
|
||||||
|
// history stays clean): re-create the trailing error bubble + retry
|
||||||
|
// state after a refresh. Stale entries (conversation moved on) drop.
|
||||||
|
const entry = getFailedEntry(id);
|
||||||
|
if (entry && entry.index === res.messages.length) {
|
||||||
|
setMessages([...res.messages, { role: "assistant", content: t("attach.chatError") }]);
|
||||||
|
setFailed({ index: entry.index, convId: id });
|
||||||
|
setRetryCount(entry.retryCount);
|
||||||
|
} else if (entry) {
|
||||||
|
clearFailedEntry(id);
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => setMessages([]));
|
.catch(() => setMessages([]));
|
||||||
}, [activeId]);
|
}, [activeId]);
|
||||||
|
|
||||||
@@ -327,7 +351,7 @@ export function App() {
|
|||||||
.models()
|
.models()
|
||||||
.then((m) => {
|
.then((m) => {
|
||||||
setModels(m.models);
|
setModels(m.models);
|
||||||
if (m.models.length > 0) setModel(m.models[0]!.id);
|
if (m.models.length > 0) setModel(pickModel(m.models, getSavedModel(u.username)));
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
api
|
api
|
||||||
@@ -380,7 +404,14 @@ export function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function send(text: string) {
|
async function send(text: string) {
|
||||||
if (sending || !model || archivedConv) return;
|
if (sending || !model || archivedConv) {
|
||||||
|
// Single flight: another conversation is streaming — say so instead
|
||||||
|
// of silently swallowing the submit.
|
||||||
|
if (sending && sendingForRef.current && sendingForRef.current !== activeIdRef.current) {
|
||||||
|
pushToast(t("chat.busyOther"), { icon: "model" });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
let convId = activeId;
|
let convId = activeId;
|
||||||
if (!convId) {
|
if (!convId) {
|
||||||
try {
|
try {
|
||||||
@@ -390,16 +421,16 @@ export function App() {
|
|||||||
// render): the URL is still bare /chat while the selection already
|
// render): the URL is still bare /chat while the selection already
|
||||||
// points at the new id, and sendingForRef shields both until the
|
// points at the new id, and sendingForRef shields both until the
|
||||||
// stream lands. Then refresh the list and move the URL to /chat/<id>.
|
// stream lands. Then refresh the list and move the URL to /chat/<id>.
|
||||||
sendingForRef.current = convId;
|
setFlight(convId);
|
||||||
setActiveId(convId);
|
setActiveId(convId);
|
||||||
await refreshConvs(convId);
|
await refreshConvs(convId);
|
||||||
navigate(`/chat/${convId}`);
|
navigate(`/chat/${convId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sendingForRef.current = null;
|
setFlight(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sendingForRef.current = convId;
|
setFlight(convId);
|
||||||
}
|
}
|
||||||
const userMsg: ChatMessage = { role: "user", content: text };
|
const userMsg: ChatMessage = { role: "user", content: text };
|
||||||
// Attachments travel as reviewed text blocks appended to the message.
|
// Attachments travel as reviewed text blocks appended to the message.
|
||||||
@@ -412,7 +443,11 @@ export function App() {
|
|||||||
);
|
);
|
||||||
if (blocks.length > 0)
|
if (blocks.length > 0)
|
||||||
userMsg.content = [text, ...blocks].filter(Boolean).join("\n\n");
|
userMsg.content = [text, ...blocks].filter(Boolean).join("\n\n");
|
||||||
const history = [...messages, userMsg];
|
// A trailing error bubble is display-only: never send the fake error
|
||||||
|
// text to the model, whether it was just created or restored.
|
||||||
|
const cleanBase =
|
||||||
|
failed && failed.convId === convId ? messages.filter((_, i) => i !== failed.index) : messages;
|
||||||
|
const history = [...cleanBase, userMsg];
|
||||||
setMessages(history);
|
setMessages(history);
|
||||||
setAttachments([]);
|
setAttachments([]);
|
||||||
setFailed(null);
|
setFailed(null);
|
||||||
@@ -428,6 +463,7 @@ export function App() {
|
|||||||
setMessages([...history, { role: "assistant", content: full }]);
|
setMessages([...history, { role: "assistant", content: full }]);
|
||||||
}
|
}
|
||||||
setStreaming("");
|
setStreaming("");
|
||||||
|
clearFailedEntry(convId!);
|
||||||
// Explicit select: never let the post-stream refresh drift the
|
// Explicit select: never let the post-stream refresh drift the
|
||||||
// selection (or the URL) away from this conversation.
|
// selection (or the URL) away from this conversation.
|
||||||
await refreshConvs(convId!);
|
await refreshConvs(convId!);
|
||||||
@@ -440,9 +476,12 @@ export function App() {
|
|||||||
]);
|
]);
|
||||||
setFailed({ index: history.length, convId: convId! });
|
setFailed({ index: history.length, convId: convId! });
|
||||||
}
|
}
|
||||||
|
// Persist the failure browser-side so the bubble + retry survive a
|
||||||
|
// refresh (nothing fake is written server-side).
|
||||||
|
setFailedEntry(convId!, { index: history.length, retryCount: 0, at: Date.now() });
|
||||||
setStreaming("");
|
setStreaming("");
|
||||||
} finally {
|
} finally {
|
||||||
if (sendingForRef.current === convId) sendingForRef.current = null;
|
if (sendingForRef.current === convId) setFlight(null);
|
||||||
setSending(false);
|
setSending(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -456,10 +495,11 @@ export function App() {
|
|||||||
if (sending || !failed || retryCount >= MAX_RETRIES || !model || archivedConv) return;
|
if (sending || !failed || retryCount >= MAX_RETRIES || !model || archivedConv) return;
|
||||||
const history = messages.slice(0, failed.index);
|
const history = messages.slice(0, failed.index);
|
||||||
const convId = failed.convId;
|
const convId = failed.convId;
|
||||||
sendingForRef.current = convId;
|
const nextCount = retryCount + 1;
|
||||||
|
setFlight(convId);
|
||||||
setMessages(history);
|
setMessages(history);
|
||||||
setFailed(null);
|
setFailed(null);
|
||||||
setRetryCount((c) => c + 1);
|
setRetryCount(nextCount);
|
||||||
setSending(true);
|
setSending(true);
|
||||||
setStreaming("");
|
setStreaming("");
|
||||||
try {
|
try {
|
||||||
@@ -468,6 +508,7 @@ export function App() {
|
|||||||
setMessages([...history, { role: "assistant", content: full }]);
|
setMessages([...history, { role: "assistant", content: full }]);
|
||||||
}
|
}
|
||||||
setRetryCount(0);
|
setRetryCount(0);
|
||||||
|
clearFailedEntry(convId);
|
||||||
await refreshConvs(convId);
|
await refreshConvs(convId);
|
||||||
if (activeIdRef.current === convId) navigate(`/chat/${convId}`);
|
if (activeIdRef.current === convId) navigate(`/chat/${convId}`);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -478,9 +519,10 @@ export function App() {
|
|||||||
]);
|
]);
|
||||||
setFailed({ index: history.length, convId });
|
setFailed({ index: history.length, convId });
|
||||||
}
|
}
|
||||||
|
setFailedEntry(convId, { index: history.length, retryCount: nextCount, at: Date.now() });
|
||||||
setStreaming("");
|
setStreaming("");
|
||||||
} finally {
|
} finally {
|
||||||
if (sendingForRef.current === convId) sendingForRef.current = null;
|
if (sendingForRef.current === convId) setFlight(null);
|
||||||
setSending(false);
|
setSending(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -552,6 +594,7 @@ export function App() {
|
|||||||
async function removeConv(c: Conversation) {
|
async function removeConv(c: Conversation) {
|
||||||
try {
|
try {
|
||||||
await api.deleteConv(c.id);
|
await api.deleteConv(c.id);
|
||||||
|
clearFailedEntry(c.id);
|
||||||
if (activeId === c.id) {
|
if (activeId === c.id) {
|
||||||
setActiveId(null);
|
setActiveId(null);
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
@@ -637,7 +680,9 @@ export function App() {
|
|||||||
const m = await api.models();
|
const m = await api.models();
|
||||||
setModels(m.models);
|
setModels(m.models);
|
||||||
setModel((cur) =>
|
setModel((cur) =>
|
||||||
cur && m.models.some((x) => x.id === cur) ? cur : (m.models[0]?.id ?? ""),
|
cur && m.models.some((x) => x.id === cur)
|
||||||
|
? cur
|
||||||
|
: pickModel(m.models, user ? getSavedModel(user.username) : null),
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore (session errors reload the page via api layer)
|
// ignore (session errors reload the page via api layer)
|
||||||
@@ -715,12 +760,15 @@ export function App() {
|
|||||||
user={user}
|
user={user}
|
||||||
conv={active}
|
conv={active}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
streaming={streaming}
|
// Streaming / thinking / sending UI lives only in the conversation
|
||||||
sending={sending}
|
// being generated for — anywhere else renders clean and stays usable.
|
||||||
|
streaming={sendingFor === activeId ? streaming : ""}
|
||||||
|
sending={sending && sendingFor === activeId}
|
||||||
models={models}
|
models={models}
|
||||||
model={model}
|
model={model}
|
||||||
onModelChange={(id) => {
|
onModelChange={(id) => {
|
||||||
setModel(id);
|
setModel(id);
|
||||||
|
setSavedModel(user.username, id);
|
||||||
pushToast(t("toast.modelChanged", { label: models.find((m) => m.id === id)?.label ?? id }), {
|
pushToast(t("toast.modelChanged", { label: models.find((m) => m.id === id)?.label ?? id }), {
|
||||||
icon: "model",
|
icon: "model",
|
||||||
tag: "model",
|
tag: "model",
|
||||||
@@ -741,7 +789,7 @@ export function App() {
|
|||||||
setAttachments((prev) => prev.filter((a) => a.id !== id))
|
setAttachments((prev) => prev.filter((a) => a.id !== id))
|
||||||
}
|
}
|
||||||
onPickFile={handlePickFile}
|
onPickFile={handlePickFile}
|
||||||
failedIndex={failed?.index ?? null}
|
failedIndex={failed && failed.convId === activeId ? failed.index : null}
|
||||||
retryCount={retryCount}
|
retryCount={retryCount}
|
||||||
maxRetries={MAX_RETRIES}
|
maxRetries={MAX_RETRIES}
|
||||||
onRetry={readOnly ? undefined : retryFailed}
|
onRetry={readOnly ? undefined : retryFailed}
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ const en = {
|
|||||||
"chat.greet": "How can I help, {name}?",
|
"chat.greet": "How can I help, {name}?",
|
||||||
"chat.greetSub": "Pick a model, then type below. Markdown is supported.",
|
"chat.greetSub": "Pick a model, then type below. Markdown is supported.",
|
||||||
"chat.thinking": "Thinking…",
|
"chat.thinking": "Thinking…",
|
||||||
|
"chat.busyOther": "A reply is still generating in another chat — please wait.",
|
||||||
"chat.placeholder": "Message KisAssistant…",
|
"chat.placeholder": "Message KisAssistant…",
|
||||||
"chat.send": "Send",
|
"chat.send": "Send",
|
||||||
"chat.attachTip": "Attach a file",
|
"chat.attachTip": "Attach a file",
|
||||||
@@ -690,6 +691,7 @@ const es: Dict = {
|
|||||||
"chat.greet": "¿En qué te ayudo, {name}?",
|
"chat.greet": "¿En qué te ayudo, {name}?",
|
||||||
"chat.greetSub": "Elige un modelo y escribe abajo. Markdown soportado.",
|
"chat.greetSub": "Elige un modelo y escribe abajo. Markdown soportado.",
|
||||||
"chat.thinking": "Pensando…",
|
"chat.thinking": "Pensando…",
|
||||||
|
"chat.busyOther": "A reply is still generating in another chat — please wait.",
|
||||||
"chat.placeholder": "Escribe a KisAssistant…",
|
"chat.placeholder": "Escribe a KisAssistant…",
|
||||||
"chat.send": "Enviar",
|
"chat.send": "Enviar",
|
||||||
"chat.attachTip": "Adjuntar un archivo",
|
"chat.attachTip": "Adjuntar un archivo",
|
||||||
@@ -1262,6 +1264,7 @@ const fr: Dict = {
|
|||||||
"chat.greetSub":
|
"chat.greetSub":
|
||||||
"Choisissez un modèle, puis écrivez ci-dessous. Markdown pris en charge.",
|
"Choisissez un modèle, puis écrivez ci-dessous. Markdown pris en charge.",
|
||||||
"chat.thinking": "Réflexion…",
|
"chat.thinking": "Réflexion…",
|
||||||
|
"chat.busyOther": "A reply is still generating in another chat — please wait.",
|
||||||
"chat.placeholder": "Écrivez à KisAssistant…",
|
"chat.placeholder": "Écrivez à KisAssistant…",
|
||||||
"chat.send": "Envoyer",
|
"chat.send": "Envoyer",
|
||||||
"chat.attachTip": "Joindre un fichier",
|
"chat.attachTip": "Joindre un fichier",
|
||||||
@@ -1832,6 +1835,7 @@ const ru: Dict = {
|
|||||||
"chat.greet": "Чем помочь, {name}?",
|
"chat.greet": "Чем помочь, {name}?",
|
||||||
"chat.greetSub": "Выберите модель и пишите ниже. Поддерживается Markdown.",
|
"chat.greetSub": "Выберите модель и пишите ниже. Поддерживается Markdown.",
|
||||||
"chat.thinking": "Думаю…",
|
"chat.thinking": "Думаю…",
|
||||||
|
"chat.busyOther": "A reply is still generating in another chat — please wait.",
|
||||||
"chat.placeholder": "Сообщение KisAssistant…",
|
"chat.placeholder": "Сообщение KisAssistant…",
|
||||||
"chat.send": "Отправить",
|
"chat.send": "Отправить",
|
||||||
"chat.attachTip": "Прикрепить файл",
|
"chat.attachTip": "Прикрепить файл",
|
||||||
@@ -2390,6 +2394,7 @@ const it: Dict = {
|
|||||||
"chat.greet": "Come posso aiutare, {name}?",
|
"chat.greet": "Come posso aiutare, {name}?",
|
||||||
"chat.greetSub": "Scegli un modello e scrivi qui sotto. Markdown supportato.",
|
"chat.greetSub": "Scegli un modello e scrivi qui sotto. Markdown supportato.",
|
||||||
"chat.thinking": "Penso…",
|
"chat.thinking": "Penso…",
|
||||||
|
"chat.busyOther": "A reply is still generating in another chat — please wait.",
|
||||||
"chat.placeholder": "Scrivi a KisAssistant…",
|
"chat.placeholder": "Scrivi a KisAssistant…",
|
||||||
"chat.send": "Invia",
|
"chat.send": "Invia",
|
||||||
"chat.attachTip": "Allega un file",
|
"chat.attachTip": "Allega un file",
|
||||||
@@ -2954,6 +2959,7 @@ const pt: Dict = {
|
|||||||
"chat.greet": "Como posso ajudar, {name}?",
|
"chat.greet": "Como posso ajudar, {name}?",
|
||||||
"chat.greetSub": "Escolhe um modelo e escreve abaixo. Markdown suportado.",
|
"chat.greetSub": "Escolhe um modelo e escreve abaixo. Markdown suportado.",
|
||||||
"chat.thinking": "A pensar…",
|
"chat.thinking": "A pensar…",
|
||||||
|
"chat.busyOther": "A reply is still generating in another chat — please wait.",
|
||||||
"chat.placeholder": "Escreve ao KisAssistant…",
|
"chat.placeholder": "Escreve ao KisAssistant…",
|
||||||
"chat.send": "Enviar",
|
"chat.send": "Enviar",
|
||||||
"chat.attachTip": "Anexar ficheiro",
|
"chat.attachTip": "Anexar ficheiro",
|
||||||
|
|||||||
@@ -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 ?? "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Il y as plusieurs bug d'UX bien chiant:
|
||||||
|
Quand un modèle as une erreur: Le modèle le dit, je refresh, le message d'errur à disparue -> Rendre ces message perisstant
|
||||||
|
Quand j'envoie un message dans une conversation , l'IA think: je vais sur une autre, sont overly aussi overide une autre conversation: bien segmenter sa
|
||||||
|
Le modèles choisis n'est pas sauvegarder, rend l'user choix perssistant
|
||||||
|
|
||||||
|
|
||||||
|
____
|
||||||
|
|
||||||
|
Pouvoir ajouter d'autre node ollama dans l'admin center. modale, composant, gestion de priorité avec des "poids" comme un firewall
|
||||||
|
Gestions des rate-limites pour recovery password...
|
||||||
|
admin center: gérer les url pages
|
||||||
|
|
||||||
|
exemple: ouverture catégorie modèles: <host>/admin/models
|
||||||
|
fait un timeout sur les requete envoyez au ollama , configurable dans l'admin center.
|
||||||
|
Faire un catalog des modèles ollama via internet /hugging face compatible ollama.
|
||||||
|
voir les détails des nodes ollama (charge cpu, mémoire, vram..., sur le temps (1h,6h,12h,24h,7w,14w,1m))
|
||||||
|
leur composants,...
|
||||||
Reference in New Issue
Block a user