added a feature to enable/disable report agentic message repspnse
This commit is contained in:
@@ -71,6 +71,9 @@ connectivity tester: `GET /api/admin/mail`, `POST /api/admin/mail/verify`,
|
||||
login page (wishlist). Only switchable on while public registration is off —
|
||||
the two are mutually exclusive (API answers 409 otherwise).
|
||||
- `tools_ocr_enabled` (default on): admin kill-switch for the OCR tool.
|
||||
- `reports_enabled` (default on): admin kill-switch for message reports —
|
||||
the chat flag button turns grey with a "Report disabled" tooltip and
|
||||
`POST /api/reports` answers 403. Surfaced to clients via `GET /api/tools`.
|
||||
- Flags live in the `settings` SQLite table, edited via
|
||||
`GET/PATCH /api/admin/settings` (admin Bearer only).
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ export function App() {
|
||||
const [filePreview, setFilePreview] = useState<FilePreview | null>(null);
|
||||
const [previewBusy, setPreviewBusy] = useState(false);
|
||||
const [ocrAvailable, setOcrAvailable] = useState(false);
|
||||
const [reportsEnabled, setReportsEnabled] = useState(true);
|
||||
// Model-error retry: which trailing message is the error bubble (if any),
|
||||
// its conversation, and how many times this prompt was already retried.
|
||||
const [failed, setFailed] = useState<{ index: number; convId: string } | null>(null);
|
||||
@@ -183,9 +184,11 @@ export function App() {
|
||||
.tools()
|
||||
.catch(() => ({
|
||||
tools: [] as { name: string; available: boolean }[],
|
||||
reportsEnabled: true,
|
||||
})),
|
||||
]);
|
||||
setOcrAvailable(t.tools.some((x) => x.name === "ocr" && x.available));
|
||||
setReportsEnabled(t.reportsEnabled ?? true);
|
||||
setModels(m.models);
|
||||
if (m.models.length > 0) setModel(m.models[0]!.id);
|
||||
setConvs(c.conversations);
|
||||
@@ -306,9 +309,10 @@ export function App() {
|
||||
.catch(() => {});
|
||||
api
|
||||
.tools()
|
||||
.then((t) =>
|
||||
setOcrAvailable(t.tools.some((x) => x.name === "ocr" && x.available)),
|
||||
)
|
||||
.then((t) => {
|
||||
setOcrAvailable(t.tools.some((x) => x.name === "ocr" && x.available));
|
||||
setReportsEnabled(t.reportsEnabled ?? true);
|
||||
})
|
||||
.catch(() => {});
|
||||
// Land directly on the latest conversation's own URL.
|
||||
refreshConvs().then((id) => {
|
||||
@@ -681,6 +685,7 @@ export function App() {
|
||||
attachments={attachments}
|
||||
attachError={attachError}
|
||||
ocrAvailable={ocrAvailable}
|
||||
reportsEnabled={reportsEnabled}
|
||||
onRemoveAttachment={(id) =>
|
||||
setAttachments((prev) => prev.filter((a) => a.id !== id))
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function AdminCenter() {
|
||||
const { t } = useT();
|
||||
const [allowed, setAllowed] = useState<boolean | null>(null);
|
||||
const [tab, setTab] = useState<Tab>("accounts");
|
||||
const [settings, setSettings] = useState<{ registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean } | null>(null);
|
||||
const [settings, setSettings] = useState<{ registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean; reportsEnabled: boolean } | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [openAccessCount, setOpenAccessCount] = useState(0);
|
||||
@@ -100,7 +100,7 @@ export function AdminCenter() {
|
||||
});
|
||||
}, [allowed]);
|
||||
|
||||
async function toggle(patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean }) {
|
||||
async function toggle(patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean; reportsEnabled?: boolean }) {
|
||||
setSaving(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -244,6 +244,13 @@ export function AdminCenter() {
|
||||
</div>
|
||||
<Switch label={t("center.ocrTitle")} checked={settings?.ocrEnabled ?? false} onChange={(v) => toggle({ ocrEnabled: v })} />
|
||||
</div>
|
||||
<div className="flex items-center gap-4 rounded-3xl border border-stone-200/70 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium">{t("center.reportsTitle")}</p>
|
||||
<p className="text-sm opacity-60">{t("center.reportsDesc")}</p>
|
||||
</div>
|
||||
<Switch label={t("center.reportsTitle")} checked={settings?.reportsEnabled ?? false} onChange={(v) => toggle({ reportsEnabled: v })} />
|
||||
</div>
|
||||
{saving && <p className="flex items-center gap-2 text-sm opacity-60"><Spinner size={14} /> {t("common.saving")}</p>}
|
||||
{!settings && !error && <p className="flex items-center gap-2 text-sm opacity-60"><Spinner size={14} /> {t("common.loading")}</p>}
|
||||
</section>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useT, type StringKey } from "../lib/i18n.ts";
|
||||
|
||||
const REPORT_REASONS: ReportReason[] = ["copyright", "gore", "falseinfo", "bug"];
|
||||
|
||||
function MessageBubble({ msg, index, authorAvatar, authorName, onReport, isFailed, retryCount, maxRetries, retryDisabled, onRetry }: { msg: ChatMessage; index: number; authorAvatar?: string; authorName: string; onReport?: (index: number) => void; isFailed?: boolean; retryCount?: number; maxRetries?: number; retryDisabled?: boolean; onRetry?: () => void }) {
|
||||
function MessageBubble({ msg, index, authorAvatar, authorName, onReport, reportsEnabled = true, isFailed, retryCount, maxRetries, retryDisabled, onRetry }: { msg: ChatMessage; index: number; authorAvatar?: string; authorName: string; onReport?: (index: number) => void; reportsEnabled?: boolean; isFailed?: boolean; retryCount?: number; maxRetries?: number; retryDisabled?: boolean; onRetry?: () => void }) {
|
||||
const { t } = useT();
|
||||
const isUser = msg.role === "user";
|
||||
const [copied, setCopied] = useState(false);
|
||||
@@ -53,7 +53,11 @@ function MessageBubble({ msg, index, authorAvatar, authorName, onReport, isFaile
|
||||
<IconButton title={t("msg.search")} onClick={searchWeb}>
|
||||
<Search size={14} />
|
||||
</IconButton>
|
||||
<IconButton title={t("msg.report")} onClick={() => onReport(index)}>
|
||||
<IconButton
|
||||
title={reportsEnabled ? t("msg.report") : t("msg.reportDisabled")}
|
||||
onClick={() => onReport(index)}
|
||||
disabled={!reportsEnabled}
|
||||
>
|
||||
<Flag size={14} />
|
||||
</IconButton>
|
||||
</>
|
||||
@@ -194,6 +198,7 @@ export function Chat({
|
||||
attachments,
|
||||
attachError,
|
||||
ocrAvailable,
|
||||
reportsEnabled = true,
|
||||
onRemoveAttachment,
|
||||
onPickFile,
|
||||
failedIndex,
|
||||
@@ -220,6 +225,7 @@ export function Chat({
|
||||
attachments: Attachment[];
|
||||
attachError: string | null;
|
||||
ocrAvailable: boolean;
|
||||
reportsEnabled?: boolean;
|
||||
onRemoveAttachment: (id: string) => void;
|
||||
onPickFile: (kind: "ocr" | "text", file: File) => void;
|
||||
/** Trailing model-error bubble index (retry arrow), if any. */
|
||||
@@ -323,7 +329,7 @@ export function Chat({
|
||||
</div>
|
||||
)}
|
||||
{messages.filter((m) => m.role !== "system").map((m, i) => (
|
||||
<MessageBubble key={i} msg={m} index={i} authorAvatar={user.avatarUrl} authorName={user.displayName} onReport={readOnly ? undefined : setReportIndex} isFailed={failedIndex === i} retryCount={retryCount} maxRetries={maxRetries} retryDisabled={sending} onRetry={failedIndex === i && retryCount < maxRetries ? onRetry : undefined} />
|
||||
<MessageBubble key={i} msg={m} index={i} authorAvatar={user.avatarUrl} authorName={user.displayName} onReport={readOnly ? undefined : setReportIndex} reportsEnabled={reportsEnabled} isFailed={failedIndex === i} retryCount={retryCount} maxRetries={maxRetries} retryDisabled={sending} onRetry={failedIndex === i && retryCount < maxRetries ? onRetry : undefined} />
|
||||
))}
|
||||
{streaming !== "" && (
|
||||
<div className="flex gap-2 sm:gap-3">
|
||||
|
||||
@@ -188,9 +188,9 @@ export const api = {
|
||||
adminPatch: (id: string, patch: { displayName?: string; avatarUrl?: string; theme?: string; email?: string }) =>
|
||||
req<{ user: User }>(`/api/admin/users/${id}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
||||
|
||||
adminGetSettings: () => req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean } }>("/api/admin/settings"),
|
||||
adminPatchSettings: (patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean }) =>
|
||||
req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean } }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(patch) }),
|
||||
adminGetSettings: () => req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean; reportsEnabled: boolean } }>("/api/admin/settings"),
|
||||
adminPatchSettings: (patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean; reportsEnabled?: boolean }) =>
|
||||
req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean; reportsEnabled: boolean } }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(patch) }),
|
||||
|
||||
/** Admin: SMTP credentials viewer (password included, blur it by default) + connectivity. */
|
||||
adminMailStatus: () =>
|
||||
@@ -328,7 +328,7 @@ export const api = {
|
||||
deleteProvider: (provider: string) =>
|
||||
req<{ providers: UserProvider[] }>(`/api/me/providers/${provider}`, { method: "DELETE" }),
|
||||
|
||||
tools: () => req<{ tools: { name: string; description: string; available: boolean; reason: string | null }[] }>("/api/tools"),
|
||||
tools: () => req<{ tools: { name: string; description: string; available: boolean; reason: string | null }[]; reportsEnabled: boolean }>("/api/tools"),
|
||||
|
||||
/** Send an image to the platform OCR tool. Returns editable text (never the raw image). */
|
||||
ocrImage: async (file: File): Promise<{ text: string; truncated: boolean; chars: number }> => {
|
||||
|
||||
@@ -339,6 +339,9 @@ const en = {
|
||||
"center.ocrTitle": "OCR tool",
|
||||
"center.ocrDesc":
|
||||
"Server-side image transcription for attachments. Requires the tesseract binary.",
|
||||
"center.reportsTitle": "Message reports",
|
||||
"center.reportsDesc":
|
||||
"Flag button under assistant responses. Off = the flag turns grey with a “Report disabled” tooltip and new reports are rejected.",
|
||||
"center.mailTitle": "Key recovery emails",
|
||||
"center.mailChecking": "Checking…",
|
||||
"center.mailFrom": "Enabled — sent from {from}",
|
||||
@@ -479,6 +482,7 @@ const en = {
|
||||
"msg.copy": "Copy message",
|
||||
"msg.search": "Search the web",
|
||||
"msg.report": "Report response",
|
||||
"msg.reportDisabled": "Report disabled",
|
||||
"msg.retry": "Retry",
|
||||
|
||||
"report.title": "Report this response",
|
||||
@@ -900,6 +904,9 @@ const es: Dict = {
|
||||
"center.ocrTitle": "Herramienta OCR",
|
||||
"center.ocrDesc":
|
||||
"Transcripción de imágenes en el servidor. Requiere tesseract.",
|
||||
"center.reportsTitle": "Denuncias de mensajes",
|
||||
"center.reportsDesc":
|
||||
"Botón de bandera bajo las respuestas. Off = bandera gris con aviso y se rechazan nuevas denuncias.",
|
||||
"center.mailTitle": "Emails de recuperación",
|
||||
"center.mailChecking": "Verificando…",
|
||||
"center.mailFrom": "Activado — enviados desde {from}",
|
||||
@@ -1039,6 +1046,7 @@ const es: Dict = {
|
||||
"msg.copy": "Copiar mensaje",
|
||||
"msg.search": "Buscar en la web",
|
||||
"msg.report": "Denunciar respuesta",
|
||||
"msg.reportDisabled": "Denuncias desactivadas",
|
||||
"msg.retry": "Reintentar",
|
||||
"report.title": "Denunciar esta respuesta",
|
||||
"report.reason": "Motivo",
|
||||
@@ -1455,6 +1463,9 @@ const fr: Dict = {
|
||||
"Affiche Demander l'accès au login. Réservez un pseudo, plaidez votre cas ; tri dans l'onglet Accès.",
|
||||
"center.ocrTitle": "Outil OCR",
|
||||
"center.ocrDesc": "Transcription d'images côté serveur. Requiert tesseract.",
|
||||
"center.reportsTitle": "Signalements de messages",
|
||||
"center.reportsDesc":
|
||||
"Bouton drapeau sous les réponses. Off = drapeau grisé avec infobulle et nouveaux signalements refusés.",
|
||||
"center.mailTitle": "Emails de récupération",
|
||||
"center.mailChecking": "Vérification…",
|
||||
"center.mailFrom": "Activé — envoyés depuis {from}",
|
||||
@@ -1594,6 +1605,7 @@ const fr: Dict = {
|
||||
"msg.copy": "Copier le message",
|
||||
"msg.search": "Rechercher sur le web",
|
||||
"msg.report": "Signaler la réponse",
|
||||
"msg.reportDisabled": "Signalements désactivés",
|
||||
"msg.retry": "Réessayer",
|
||||
"report.title": "Signaler cette réponse",
|
||||
"report.reason": "Motif",
|
||||
@@ -2001,6 +2013,9 @@ const ru: Dict = {
|
||||
"Кнопка доступа на входе. Гости просят имя; разбор во вкладке.",
|
||||
"center.ocrTitle": "Инструмент OCR",
|
||||
"center.ocrDesc": "Распознавание картинок на сервере. Нужен tesseract.",
|
||||
"center.reportsTitle": "Жалобы на сообщения",
|
||||
"center.reportsDesc":
|
||||
"Кнопка-флажок под ответами. Выкл = серый флажок с подсказкой, новые жалобы отклоняются.",
|
||||
"center.mailTitle": "Письма восстановления",
|
||||
"center.mailChecking": "Проверяем…",
|
||||
"center.mailFrom": "Включено — шлём от {from}",
|
||||
@@ -2137,6 +2152,7 @@ const ru: Dict = {
|
||||
"msg.copy": "Скопировать сообщение",
|
||||
"msg.search": "Искать в интернете",
|
||||
"msg.report": "Пожаловаться на ответ",
|
||||
"msg.reportDisabled": "Жалобы отключены",
|
||||
"msg.retry": "Повторить",
|
||||
"report.title": "Пожаловаться на этот ответ",
|
||||
"report.reason": "Причина",
|
||||
@@ -2547,6 +2563,9 @@ const it: Dict = {
|
||||
"Mostra Richiedi accesso al login. Valuta nella scheda Accesso.",
|
||||
"center.ocrTitle": "Strumento OCR",
|
||||
"center.ocrDesc": "Trascrizione immagini sul server. Serve tesseract.",
|
||||
"center.reportsTitle": "Segnalazioni messaggi",
|
||||
"center.reportsDesc":
|
||||
"Bandierina sotto le risposte. Off = bandierina grigia con tooltip e nuove segnalazioni rifiutate.",
|
||||
"center.mailTitle": "Email di recupero",
|
||||
"center.mailChecking": "Verifica…",
|
||||
"center.mailFrom": "Attivo — inviate da {from}",
|
||||
@@ -2685,6 +2704,7 @@ const it: Dict = {
|
||||
"msg.copy": "Copia messaggio",
|
||||
"msg.search": "Cerca nel web",
|
||||
"msg.report": "Segnala risposta",
|
||||
"msg.reportDisabled": "Segnalazioni disabilitate",
|
||||
"msg.retry": "Riprova",
|
||||
"report.title": "Segnala questa risposta",
|
||||
"report.reason": "Motivo",
|
||||
@@ -3088,6 +3108,9 @@ const pt: Dict = {
|
||||
"center.accDescOff": "Mostra Pedir acesso no login. Triagem na aba Acesso.",
|
||||
"center.ocrTitle": "Ferramenta OCR",
|
||||
"center.ocrDesc": "Transcrição no servidor. Precisa tesseract.",
|
||||
"center.reportsTitle": "Denúncias de mensagens",
|
||||
"center.reportsDesc":
|
||||
"Bandeira sob as respostas. Off = bandeira cinzenta com aviso e novas denúncias recusadas.",
|
||||
"center.mailTitle": "Emails de recuperação",
|
||||
"center.mailChecking": "A verificar…",
|
||||
"center.mailFrom": "Ativo — enviados de {from}",
|
||||
@@ -3224,6 +3247,7 @@ const pt: Dict = {
|
||||
"msg.copy": "Copiar mensagem",
|
||||
"msg.search": "Pesquisar na web",
|
||||
"msg.report": "Denunciar resposta",
|
||||
"msg.reportDisabled": "Denúncias desativadas",
|
||||
"msg.retry": "Tentar novamente",
|
||||
"report.title": "Denunciar esta resposta",
|
||||
"report.reason": "Motivo",
|
||||
|
||||
@@ -389,6 +389,11 @@ export function isOcrToolEnabled(): boolean {
|
||||
return getSetting("tools_ocr_enabled", "1") === "1";
|
||||
}
|
||||
|
||||
/** Admin kill-switch for message reports / flag button (default on). */
|
||||
export function isReportsEnabled(): boolean {
|
||||
return getSetting("reports_enabled", "1") === "1";
|
||||
}
|
||||
|
||||
// --- public shares ---
|
||||
|
||||
export function ensureShare(convId: string): string {
|
||||
|
||||
+9
-1
@@ -33,6 +33,7 @@ import {
|
||||
getUserById,
|
||||
getUserByUsername,
|
||||
isRegistrationEnabled,
|
||||
isReportsEnabled,
|
||||
listConversations,
|
||||
listArchivedConversations,
|
||||
listUsers,
|
||||
@@ -831,6 +832,7 @@ const server = Bun.serve<{ ticketId: string | null; isAdmin: boolean }>({
|
||||
registrationEnabled: isRegistrationEnabled(),
|
||||
accessRequestEnabled: getSetting("registration_request_enabled", "1") === "1",
|
||||
ocrEnabled: getSetting("tools_ocr_enabled", "1") === "1",
|
||||
reportsEnabled: isReportsEnabled(),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -858,11 +860,16 @@ const server = Bun.serve<{ ticketId: string | null; isAdmin: boolean }>({
|
||||
if (typeof body.ocrEnabled !== "boolean") return json({ error: "ocrEnabled must be boolean" }, 400);
|
||||
setSetting("tools_ocr_enabled", body.ocrEnabled ? "1" : "0");
|
||||
}
|
||||
if (body.reportsEnabled !== undefined) {
|
||||
if (typeof body.reportsEnabled !== "boolean") return json({ error: "reportsEnabled must be boolean" }, 400);
|
||||
setSetting("reports_enabled", body.reportsEnabled ? "1" : "0");
|
||||
}
|
||||
return json({
|
||||
settings: {
|
||||
registrationEnabled: isRegistrationEnabled(),
|
||||
accessRequestEnabled: getSetting("registration_request_enabled", "1") === "1",
|
||||
ocrEnabled: getSetting("tools_ocr_enabled", "1") === "1",
|
||||
reportsEnabled: isReportsEnabled(),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1010,6 +1017,7 @@ const server = Bun.serve<{ ticketId: string | null; isAdmin: boolean }>({
|
||||
|
||||
// --- content reports (flagged AI responses) ---
|
||||
if (path === "/api/reports" && req.method === "POST") {
|
||||
if (!isReportsEnabled()) return json({ error: "reports are disabled" }, 403);
|
||||
const { ok, body } = await readJson(req);
|
||||
if (!ok) return json({ error: "invalid JSON" }, 400);
|
||||
const reason = body.reason as ReportReason | undefined;
|
||||
@@ -1352,7 +1360,7 @@ const server = Bun.serve<{ ticketId: string | null; isAdmin: boolean }>({
|
||||
|
||||
// --- platform tools (uploads always go through tools) ---
|
||||
if (path === "/api/tools" && req.method === "GET") {
|
||||
return json({ tools: tools.list() });
|
||||
return json({ tools: tools.list(), reportsEnabled: isReportsEnabled() });
|
||||
}
|
||||
|
||||
if (path === "/api/tools/ocr" && req.method === "POST") {
|
||||
|
||||
Reference in New Issue
Block a user