updates
This commit is contained in:
@@ -432,7 +432,7 @@ export function App() {
|
||||
} else {
|
||||
setFlight(convId);
|
||||
}
|
||||
const userMsg: ChatMessage = { role: "user", content: text };
|
||||
const userMsg: ChatMessage = { role: "user", content: text, model };
|
||||
// Attachments travel as reviewed text blocks appended to the message.
|
||||
const blocks = attachments.map(
|
||||
(a) =>
|
||||
@@ -460,7 +460,7 @@ export function App() {
|
||||
// switched chats mid-stream, the reply is already persisted
|
||||
// server-side and the other view reloads it on selection.
|
||||
if (activeIdRef.current === convId) {
|
||||
setMessages([...history, { role: "assistant", content: full }]);
|
||||
setMessages([...history, { role: "assistant", content: full, model }]);
|
||||
}
|
||||
setStreaming("");
|
||||
clearFailedEntry(convId!);
|
||||
@@ -505,7 +505,7 @@ export function App() {
|
||||
try {
|
||||
const full = await streamReply(history, convId);
|
||||
if (activeIdRef.current === convId) {
|
||||
setMessages([...history, { role: "assistant", content: full }]);
|
||||
setMessages([...history, { role: "assistant", content: full, model }]);
|
||||
}
|
||||
setRetryCount(0);
|
||||
clearFailedEntry(convId);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, type FC, type ReactNode } from "react";
|
||||
import { ArrowUpDown, Bot, ChevronLeft, ChevronRight, Download, Filter, HardDrive, KeyRound, Pencil, RefreshCw, Search, Trash2, UserPlus, Users } from "lucide-react";
|
||||
import { ArrowUpDown, Bot, ChevronLeft, ChevronRight, Download, Filter, HardDrive, KeyRound, Mail, Pencil, RefreshCw, Search, Trash2, UserPlus, Users } from "lucide-react";
|
||||
import { api } from "../lib/api.ts";
|
||||
import type { DriverModel, User } from "../lib/types.ts";
|
||||
import { useT, type StringKey } from "../lib/i18n.ts";
|
||||
@@ -190,6 +190,8 @@ export function AdminPanel({ open, onClose, bare }: { open: boolean; onClose: ()
|
||||
const [freshKey, setFreshKey] = useState<{ username: string; key: string } | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<User | null>(null);
|
||||
const [editTarget, setEditTarget] = useState<User | null>(null);
|
||||
const [resetTarget, setResetTarget] = useState<User | null>(null);
|
||||
const [resetSent, setResetSent] = useState<{ username: string; email: string } | null>(null);
|
||||
// Listing: search + sort + filter + pagination (server-side).
|
||||
const [q, setQ] = useState("");
|
||||
const [debouncedQ, setDebouncedQ] = useState("");
|
||||
@@ -270,6 +272,17 @@ export function AdminPanel({ open, onClose, bare }: { open: boolean; onClose: ()
|
||||
}
|
||||
}
|
||||
|
||||
async function sendResetLink(u: User) {
|
||||
setError("");
|
||||
setResetSent(null);
|
||||
try {
|
||||
const res = await api.adminResetLink(u.id);
|
||||
setResetSent({ username: u.username, email: res.email });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("admin.resetFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(u: User) {
|
||||
try {
|
||||
await api.adminDelete(u.id);
|
||||
@@ -303,6 +316,12 @@ export function AdminPanel({ open, onClose, bare }: { open: boolean; onClose: ()
|
||||
</div>
|
||||
)}
|
||||
|
||||
{resetSent && (
|
||||
<div className="mb-4 rounded-2xl border border-accent-500/40 bg-accent-50 p-4 dark:bg-accent-950/30">
|
||||
<p className="flex items-center gap-2 text-sm font-medium"><Mail size={15} /> {t("admin.resetSent", { user: resetSent.username, email: resetSent.email })}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showCreate ? (
|
||||
<div className="mb-5 flex flex-wrap gap-2">
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowCreate(true)}>
|
||||
@@ -372,6 +391,7 @@ export function AdminPanel({ open, onClose, bare }: { open: boolean; onClose: ()
|
||||
<div className="flex shrink-0 gap-1">
|
||||
<button title={t("admin.editProfile")} onClick={() => setEditTarget(u)} className="rounded-full p-2 transition hover:bg-stone-100 dark:hover:bg-zinc-800"><Pencil size={15} /></button>
|
||||
<button title={t("admin.regenKey")} onClick={() => regenerate(u)} className="rounded-full p-2 transition hover:bg-stone-100 dark:hover:bg-zinc-800"><RefreshCw size={15} /></button>
|
||||
<button title={t("admin.resetLink")} onClick={() => setResetTarget(u)} className="rounded-full p-2 transition hover:bg-stone-100 dark:hover:bg-zinc-800"><Mail size={15} /></button>
|
||||
<button title={t("admin.deleteAccount")} onClick={() => setDeleteTarget(u)} className="rounded-full p-2 text-red-600 transition hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950/40"><Trash2 size={15} /></button>
|
||||
</div>
|
||||
)}
|
||||
@@ -410,6 +430,19 @@ export function AdminPanel({ open, onClose, bare }: { open: boolean; onClose: ()
|
||||
onConfirm={() => deleteTarget && remove(deleteTarget)}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={resetTarget !== null}
|
||||
onClose={() => setResetTarget(null)}
|
||||
title={t("admin.resetTitle", { user: resetTarget?.username ?? "" })}
|
||||
message={t("admin.resetMsg", { email: resetTarget?.email || t("admin.noEmail") })}
|
||||
confirmLabel={t("admin.resetConfirm")}
|
||||
onConfirm={() => {
|
||||
const u = resetTarget;
|
||||
setResetTarget(null);
|
||||
if (u) void sendResetLink(u);
|
||||
}}
|
||||
/>
|
||||
|
||||
<EditUserDialog user={editTarget} onClose={() => setEditTarget(null)} onSaved={refresh} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -44,8 +44,16 @@ function MessageBubble({ msg, index, authorAvatar, authorName, onReport, reports
|
||||
<Markdown text={msg.content} />
|
||||
</div>
|
||||
)}
|
||||
{!isUser && (onReport || onToggleSpeak || (isFailed && onRetry)) && (
|
||||
{!isUser && (msg.model || onReport || onToggleSpeak || (isFailed && onRetry)) && (
|
||||
<div className="mt-1.5 flex items-center gap-0.5 opacity-70 transition hover:opacity-100">
|
||||
{msg.model ? (
|
||||
<span
|
||||
title={msg.model}
|
||||
className="mr-1 max-w-40 truncate rounded-full bg-stone-200/70 px-2.5 py-1 font-mono text-[11px] opacity-80 dark:bg-zinc-800"
|
||||
>
|
||||
{msg.model}
|
||||
</span>
|
||||
) : null}
|
||||
{onToggleSpeak && (
|
||||
<IconButton title={speaking ? t("voice.stopSpeak") : t("voice.speak")} onClick={onToggleSpeak}>
|
||||
{speaking ? <VolumeX size={14} className="speaking-orb text-accent-500" /> : <Volume2 size={14} />}
|
||||
@@ -534,7 +542,12 @@ export function Chat({
|
||||
<div className="flex gap-2 sm:gap-3">
|
||||
<AssistantAvatar size={32} dynamic />
|
||||
<div className="min-w-0 max-w-[88%] sm:max-w-[85%]">
|
||||
<div className="mb-1 text-xs opacity-60">KisAssistant</div>
|
||||
<div className="mb-1 flex items-center gap-1.5 text-xs opacity-60">
|
||||
<span>KisAssistant</span>
|
||||
{model ? (
|
||||
<span title={model} className="max-w-40 truncate rounded-full bg-stone-200/70 px-2 py-0.5 font-mono text-[11px] dark:bg-zinc-800">{model}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="rounded-3xl rounded-tl-lg border border-stone-200/70 bg-white px-4 py-3 shadow-sm sm:px-5 sm:py-3.5 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<div className="typing-cursor"><Markdown text={streaming} /></div>
|
||||
</div>
|
||||
|
||||
@@ -79,6 +79,9 @@ export function SharePage({ publicId }: { publicId: string }) {
|
||||
<div className="mb-1 flex items-center gap-1.5 text-xs opacity-60">
|
||||
{m.role === "user" ? <UserIcon size={12} /> : <Bot size={12} />}
|
||||
{m.role === "user" ? chat.authorName : "KisAssistant"}
|
||||
{m.role !== "user" && m.model ? (
|
||||
<span title={m.model} className="max-w-40 truncate rounded-full bg-stone-200/70 px-2 py-0.5 font-mono text-[11px] dark:bg-zinc-800">{m.model}</span>
|
||||
) : null}
|
||||
</div>
|
||||
{m.role === "user" ? (
|
||||
<div className="break-words whitespace-pre-wrap rounded-3xl rounded-tl-lg border border-stone-200/70 bg-white px-5 py-3 text-[15px] dark:border-zinc-800 dark:bg-zinc-900"><UserMessageContent content={m.content} tone="plain" /></div>
|
||||
|
||||
@@ -267,6 +267,7 @@ export const api = {
|
||||
req<{ user: User; key: string }>("/api/admin/users", { method: "POST", body: JSON.stringify(u) }),
|
||||
adminDelete: (id: string) => req<{ ok: boolean }>(`/api/admin/users/${id}`, { method: "DELETE" }),
|
||||
adminRegenerate: (id: string) => req<{ key: string }>(`/api/admin/users/${id}/regenerate`, { method: "POST" }),
|
||||
adminResetLink: (id: string) => req<{ ok: boolean; email: string }>(`/api/admin/users/${id}/reset-link`, { method: "POST" }),
|
||||
adminShadowban: (id: string, shadowbanned: boolean) =>
|
||||
req<{ shadowbanned: boolean }>(`/api/admin/users/${id}/shadowban`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -244,6 +244,12 @@ const en = {
|
||||
"admin.email": "Email (recovery)",
|
||||
"admin.createFailed": "Create failed",
|
||||
"admin.regenFailed": "Regenerate failed",
|
||||
"admin.resetLink": "Email a password reset link",
|
||||
"admin.resetTitle": "Email a reset link to @{user}?",
|
||||
"admin.resetMsg": "A one-time key-recovery link will be sent to {email}.",
|
||||
"admin.resetConfirm": "Send link",
|
||||
"admin.resetSent": "Reset link sent to @{user} ({email}).",
|
||||
"admin.resetFailed": "Could not send the reset link",
|
||||
"admin.deleteFailed": "Delete failed",
|
||||
"admin.loadFailed": "Load failed",
|
||||
"admin.searchPh": "Search accounts…",
|
||||
@@ -957,6 +963,12 @@ const es: Dict = {
|
||||
"admin.email": "Email (recuperación)",
|
||||
"admin.createFailed": "No se pudo crear",
|
||||
"admin.regenFailed": "No se pudo regenerar",
|
||||
"admin.resetLink": "Email a password reset link",
|
||||
"admin.resetTitle": "Email a reset link to @{user}?",
|
||||
"admin.resetMsg": "A one-time key-recovery link will be sent to {email}.",
|
||||
"admin.resetConfirm": "Send link",
|
||||
"admin.resetSent": "Reset link sent to @{user} ({email}).",
|
||||
"admin.resetFailed": "Could not send the reset link",
|
||||
"admin.deleteFailed": "No se pudo eliminar",
|
||||
"admin.loadFailed": "No se pudo cargar",
|
||||
"admin.searchPh": "Buscar cuentas…",
|
||||
@@ -1662,6 +1674,12 @@ const fr: Dict = {
|
||||
"admin.email": "Email (récupération)",
|
||||
"admin.createFailed": "Création impossible",
|
||||
"admin.regenFailed": "Régénération impossible",
|
||||
"admin.resetLink": "Email a password reset link",
|
||||
"admin.resetTitle": "Email a reset link to @{user}?",
|
||||
"admin.resetMsg": "A one-time key-recovery link will be sent to {email}.",
|
||||
"admin.resetConfirm": "Send link",
|
||||
"admin.resetSent": "Reset link sent to @{user} ({email}).",
|
||||
"admin.resetFailed": "Could not send the reset link",
|
||||
"admin.deleteFailed": "Suppression impossible",
|
||||
"admin.loadFailed": "Chargement impossible",
|
||||
"admin.searchPh": "Rechercher des comptes…",
|
||||
@@ -2361,6 +2379,12 @@ const ru: Dict = {
|
||||
"admin.email": "Email (восстановление)",
|
||||
"admin.createFailed": "Не создалось",
|
||||
"admin.regenFailed": "Не сменилось",
|
||||
"admin.resetLink": "Email a password reset link",
|
||||
"admin.resetTitle": "Email a reset link to @{user}?",
|
||||
"admin.resetMsg": "A one-time key-recovery link will be sent to {email}.",
|
||||
"admin.resetConfirm": "Send link",
|
||||
"admin.resetSent": "Reset link sent to @{user} ({email}).",
|
||||
"admin.resetFailed": "Could not send the reset link",
|
||||
"admin.deleteFailed": "Не удалилось",
|
||||
"admin.loadFailed": "Не загрузилось",
|
||||
"admin.searchPh": "Поиск аккаунтов…",
|
||||
@@ -3055,6 +3079,12 @@ const it: Dict = {
|
||||
"admin.email": "Email (recupero)",
|
||||
"admin.createFailed": "Creazione fallita",
|
||||
"admin.regenFailed": "Rigenerazione fallita",
|
||||
"admin.resetLink": "Email a password reset link",
|
||||
"admin.resetTitle": "Email a reset link to @{user}?",
|
||||
"admin.resetMsg": "A one-time key-recovery link will be sent to {email}.",
|
||||
"admin.resetConfirm": "Send link",
|
||||
"admin.resetSent": "Reset link sent to @{user} ({email}).",
|
||||
"admin.resetFailed": "Could not send the reset link",
|
||||
"admin.deleteFailed": "Eliminazione fallita",
|
||||
"admin.loadFailed": "Caricamento fallito",
|
||||
"admin.searchPh": "Cerca account…",
|
||||
@@ -3750,6 +3780,12 @@ const pt: Dict = {
|
||||
"admin.email": "Email (recuperação)",
|
||||
"admin.createFailed": "Falha ao criar",
|
||||
"admin.regenFailed": "Falha ao regenerar",
|
||||
"admin.resetLink": "Email a password reset link",
|
||||
"admin.resetTitle": "Email a reset link to @{user}?",
|
||||
"admin.resetMsg": "A one-time key-recovery link will be sent to {email}.",
|
||||
"admin.resetConfirm": "Send link",
|
||||
"admin.resetSent": "Reset link sent to @{user} ({email}).",
|
||||
"admin.resetFailed": "Could not send the reset link",
|
||||
"admin.deleteFailed": "Falha ao eliminar",
|
||||
"admin.loadFailed": "Falha ao carregar",
|
||||
"admin.searchPh": "Pesquisar contas…",
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface Conversation {
|
||||
export interface ChatMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
/** Fully-qualified model id that produced this message (assistant only, "" = unknown). */
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface DriverModel {
|
||||
|
||||
Reference in New Issue
Block a user