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 —
|
login page (wishlist). Only switchable on while public registration is off —
|
||||||
the two are mutually exclusive (API answers 409 otherwise).
|
the two are mutually exclusive (API answers 409 otherwise).
|
||||||
- `tools_ocr_enabled` (default on): admin kill-switch for the OCR tool.
|
- `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
|
- Flags live in the `settings` SQLite table, edited via
|
||||||
`GET/PATCH /api/admin/settings` (admin Bearer only).
|
`GET/PATCH /api/admin/settings` (admin Bearer only).
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export function App() {
|
|||||||
const [filePreview, setFilePreview] = useState<FilePreview | null>(null);
|
const [filePreview, setFilePreview] = useState<FilePreview | null>(null);
|
||||||
const [previewBusy, setPreviewBusy] = useState(false);
|
const [previewBusy, setPreviewBusy] = useState(false);
|
||||||
const [ocrAvailable, setOcrAvailable] = useState(false);
|
const [ocrAvailable, setOcrAvailable] = useState(false);
|
||||||
|
const [reportsEnabled, setReportsEnabled] = useState(true);
|
||||||
// Model-error retry: which trailing message is the error bubble (if any),
|
// Model-error retry: which trailing message is the error bubble (if any),
|
||||||
// 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);
|
||||||
@@ -183,9 +184,11 @@ export function App() {
|
|||||||
.tools()
|
.tools()
|
||||||
.catch(() => ({
|
.catch(() => ({
|
||||||
tools: [] as { name: string; available: boolean }[],
|
tools: [] as { name: string; available: boolean }[],
|
||||||
|
reportsEnabled: true,
|
||||||
})),
|
})),
|
||||||
]);
|
]);
|
||||||
setOcrAvailable(t.tools.some((x) => x.name === "ocr" && x.available));
|
setOcrAvailable(t.tools.some((x) => x.name === "ocr" && x.available));
|
||||||
|
setReportsEnabled(t.reportsEnabled ?? true);
|
||||||
setModels(m.models);
|
setModels(m.models);
|
||||||
if (m.models.length > 0) setModel(m.models[0]!.id);
|
if (m.models.length > 0) setModel(m.models[0]!.id);
|
||||||
setConvs(c.conversations);
|
setConvs(c.conversations);
|
||||||
@@ -306,9 +309,10 @@ export function App() {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
api
|
api
|
||||||
.tools()
|
.tools()
|
||||||
.then((t) =>
|
.then((t) => {
|
||||||
setOcrAvailable(t.tools.some((x) => x.name === "ocr" && x.available)),
|
setOcrAvailable(t.tools.some((x) => x.name === "ocr" && x.available));
|
||||||
)
|
setReportsEnabled(t.reportsEnabled ?? true);
|
||||||
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
// Land directly on the latest conversation's own URL.
|
// Land directly on the latest conversation's own URL.
|
||||||
refreshConvs().then((id) => {
|
refreshConvs().then((id) => {
|
||||||
@@ -681,6 +685,7 @@ export function App() {
|
|||||||
attachments={attachments}
|
attachments={attachments}
|
||||||
attachError={attachError}
|
attachError={attachError}
|
||||||
ocrAvailable={ocrAvailable}
|
ocrAvailable={ocrAvailable}
|
||||||
|
reportsEnabled={reportsEnabled}
|
||||||
onRemoveAttachment={(id) =>
|
onRemoveAttachment={(id) =>
|
||||||
setAttachments((prev) => prev.filter((a) => a.id !== id))
|
setAttachments((prev) => prev.filter((a) => a.id !== id))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function AdminCenter() {
|
|||||||
const { t } = useT();
|
const { t } = useT();
|
||||||
const [allowed, setAllowed] = useState<boolean | null>(null);
|
const [allowed, setAllowed] = useState<boolean | null>(null);
|
||||||
const [tab, setTab] = useState<Tab>("accounts");
|
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 [error, setError] = useState("");
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [openAccessCount, setOpenAccessCount] = useState(0);
|
const [openAccessCount, setOpenAccessCount] = useState(0);
|
||||||
@@ -100,7 +100,7 @@ export function AdminCenter() {
|
|||||||
});
|
});
|
||||||
}, [allowed]);
|
}, [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);
|
setSaving(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -244,6 +244,13 @@ export function AdminCenter() {
|
|||||||
</div>
|
</div>
|
||||||
<Switch label={t("center.ocrTitle")} checked={settings?.ocrEnabled ?? false} onChange={(v) => toggle({ ocrEnabled: v })} />
|
<Switch label={t("center.ocrTitle")} checked={settings?.ocrEnabled ?? false} onChange={(v) => toggle({ ocrEnabled: v })} />
|
||||||
</div>
|
</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>}
|
{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>}
|
{!settings && !error && <p className="flex items-center gap-2 text-sm opacity-60"><Spinner size={14} /> {t("common.loading")}</p>}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useT, type StringKey } from "../lib/i18n.ts";
|
|||||||
|
|
||||||
const REPORT_REASONS: ReportReason[] = ["copyright", "gore", "falseinfo", "bug"];
|
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 { t } = useT();
|
||||||
const isUser = msg.role === "user";
|
const isUser = msg.role === "user";
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
@@ -53,7 +53,11 @@ function MessageBubble({ msg, index, authorAvatar, authorName, onReport, isFaile
|
|||||||
<IconButton title={t("msg.search")} onClick={searchWeb}>
|
<IconButton title={t("msg.search")} onClick={searchWeb}>
|
||||||
<Search size={14} />
|
<Search size={14} />
|
||||||
</IconButton>
|
</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} />
|
<Flag size={14} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</>
|
</>
|
||||||
@@ -194,6 +198,7 @@ export function Chat({
|
|||||||
attachments,
|
attachments,
|
||||||
attachError,
|
attachError,
|
||||||
ocrAvailable,
|
ocrAvailable,
|
||||||
|
reportsEnabled = true,
|
||||||
onRemoveAttachment,
|
onRemoveAttachment,
|
||||||
onPickFile,
|
onPickFile,
|
||||||
failedIndex,
|
failedIndex,
|
||||||
@@ -220,6 +225,7 @@ export function Chat({
|
|||||||
attachments: Attachment[];
|
attachments: Attachment[];
|
||||||
attachError: string | null;
|
attachError: string | null;
|
||||||
ocrAvailable: boolean;
|
ocrAvailable: boolean;
|
||||||
|
reportsEnabled?: boolean;
|
||||||
onRemoveAttachment: (id: string) => void;
|
onRemoveAttachment: (id: string) => void;
|
||||||
onPickFile: (kind: "ocr" | "text", file: File) => void;
|
onPickFile: (kind: "ocr" | "text", file: File) => void;
|
||||||
/** Trailing model-error bubble index (retry arrow), if any. */
|
/** Trailing model-error bubble index (retry arrow), if any. */
|
||||||
@@ -323,7 +329,7 @@ export function Chat({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{messages.filter((m) => m.role !== "system").map((m, i) => (
|
{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 !== "" && (
|
{streaming !== "" && (
|
||||||
<div className="flex gap-2 sm:gap-3">
|
<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 }) =>
|
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) }),
|
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"),
|
adminGetSettings: () => req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean; reportsEnabled: boolean } }>("/api/admin/settings"),
|
||||||
adminPatchSettings: (patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean }) =>
|
adminPatchSettings: (patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean; reportsEnabled?: boolean }) =>
|
||||||
req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean } }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(patch) }),
|
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. */
|
/** Admin: SMTP credentials viewer (password included, blur it by default) + connectivity. */
|
||||||
adminMailStatus: () =>
|
adminMailStatus: () =>
|
||||||
@@ -328,7 +328,7 @@ export const api = {
|
|||||||
deleteProvider: (provider: string) =>
|
deleteProvider: (provider: string) =>
|
||||||
req<{ providers: UserProvider[] }>(`/api/me/providers/${provider}`, { method: "DELETE" }),
|
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). */
|
/** 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 }> => {
|
ocrImage: async (file: File): Promise<{ text: string; truncated: boolean; chars: number }> => {
|
||||||
|
|||||||
@@ -339,6 +339,9 @@ const en = {
|
|||||||
"center.ocrTitle": "OCR tool",
|
"center.ocrTitle": "OCR tool",
|
||||||
"center.ocrDesc":
|
"center.ocrDesc":
|
||||||
"Server-side image transcription for attachments. Requires the tesseract binary.",
|
"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.mailTitle": "Key recovery emails",
|
||||||
"center.mailChecking": "Checking…",
|
"center.mailChecking": "Checking…",
|
||||||
"center.mailFrom": "Enabled — sent from {from}",
|
"center.mailFrom": "Enabled — sent from {from}",
|
||||||
@@ -479,6 +482,7 @@ const en = {
|
|||||||
"msg.copy": "Copy message",
|
"msg.copy": "Copy message",
|
||||||
"msg.search": "Search the web",
|
"msg.search": "Search the web",
|
||||||
"msg.report": "Report response",
|
"msg.report": "Report response",
|
||||||
|
"msg.reportDisabled": "Report disabled",
|
||||||
"msg.retry": "Retry",
|
"msg.retry": "Retry",
|
||||||
|
|
||||||
"report.title": "Report this response",
|
"report.title": "Report this response",
|
||||||
@@ -900,6 +904,9 @@ const es: Dict = {
|
|||||||
"center.ocrTitle": "Herramienta OCR",
|
"center.ocrTitle": "Herramienta OCR",
|
||||||
"center.ocrDesc":
|
"center.ocrDesc":
|
||||||
"Transcripción de imágenes en el servidor. Requiere tesseract.",
|
"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.mailTitle": "Emails de recuperación",
|
||||||
"center.mailChecking": "Verificando…",
|
"center.mailChecking": "Verificando…",
|
||||||
"center.mailFrom": "Activado — enviados desde {from}",
|
"center.mailFrom": "Activado — enviados desde {from}",
|
||||||
@@ -1039,6 +1046,7 @@ const es: Dict = {
|
|||||||
"msg.copy": "Copiar mensaje",
|
"msg.copy": "Copiar mensaje",
|
||||||
"msg.search": "Buscar en la web",
|
"msg.search": "Buscar en la web",
|
||||||
"msg.report": "Denunciar respuesta",
|
"msg.report": "Denunciar respuesta",
|
||||||
|
"msg.reportDisabled": "Denuncias desactivadas",
|
||||||
"msg.retry": "Reintentar",
|
"msg.retry": "Reintentar",
|
||||||
"report.title": "Denunciar esta respuesta",
|
"report.title": "Denunciar esta respuesta",
|
||||||
"report.reason": "Motivo",
|
"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.",
|
"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.ocrTitle": "Outil OCR",
|
||||||
"center.ocrDesc": "Transcription d'images côté serveur. Requiert tesseract.",
|
"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.mailTitle": "Emails de récupération",
|
||||||
"center.mailChecking": "Vérification…",
|
"center.mailChecking": "Vérification…",
|
||||||
"center.mailFrom": "Activé — envoyés depuis {from}",
|
"center.mailFrom": "Activé — envoyés depuis {from}",
|
||||||
@@ -1594,6 +1605,7 @@ const fr: Dict = {
|
|||||||
"msg.copy": "Copier le message",
|
"msg.copy": "Copier le message",
|
||||||
"msg.search": "Rechercher sur le web",
|
"msg.search": "Rechercher sur le web",
|
||||||
"msg.report": "Signaler la réponse",
|
"msg.report": "Signaler la réponse",
|
||||||
|
"msg.reportDisabled": "Signalements désactivés",
|
||||||
"msg.retry": "Réessayer",
|
"msg.retry": "Réessayer",
|
||||||
"report.title": "Signaler cette réponse",
|
"report.title": "Signaler cette réponse",
|
||||||
"report.reason": "Motif",
|
"report.reason": "Motif",
|
||||||
@@ -2001,6 +2013,9 @@ const ru: Dict = {
|
|||||||
"Кнопка доступа на входе. Гости просят имя; разбор во вкладке.",
|
"Кнопка доступа на входе. Гости просят имя; разбор во вкладке.",
|
||||||
"center.ocrTitle": "Инструмент OCR",
|
"center.ocrTitle": "Инструмент OCR",
|
||||||
"center.ocrDesc": "Распознавание картинок на сервере. Нужен tesseract.",
|
"center.ocrDesc": "Распознавание картинок на сервере. Нужен tesseract.",
|
||||||
|
"center.reportsTitle": "Жалобы на сообщения",
|
||||||
|
"center.reportsDesc":
|
||||||
|
"Кнопка-флажок под ответами. Выкл = серый флажок с подсказкой, новые жалобы отклоняются.",
|
||||||
"center.mailTitle": "Письма восстановления",
|
"center.mailTitle": "Письма восстановления",
|
||||||
"center.mailChecking": "Проверяем…",
|
"center.mailChecking": "Проверяем…",
|
||||||
"center.mailFrom": "Включено — шлём от {from}",
|
"center.mailFrom": "Включено — шлём от {from}",
|
||||||
@@ -2137,6 +2152,7 @@ const ru: Dict = {
|
|||||||
"msg.copy": "Скопировать сообщение",
|
"msg.copy": "Скопировать сообщение",
|
||||||
"msg.search": "Искать в интернете",
|
"msg.search": "Искать в интернете",
|
||||||
"msg.report": "Пожаловаться на ответ",
|
"msg.report": "Пожаловаться на ответ",
|
||||||
|
"msg.reportDisabled": "Жалобы отключены",
|
||||||
"msg.retry": "Повторить",
|
"msg.retry": "Повторить",
|
||||||
"report.title": "Пожаловаться на этот ответ",
|
"report.title": "Пожаловаться на этот ответ",
|
||||||
"report.reason": "Причина",
|
"report.reason": "Причина",
|
||||||
@@ -2547,6 +2563,9 @@ const it: Dict = {
|
|||||||
"Mostra Richiedi accesso al login. Valuta nella scheda Accesso.",
|
"Mostra Richiedi accesso al login. Valuta nella scheda Accesso.",
|
||||||
"center.ocrTitle": "Strumento OCR",
|
"center.ocrTitle": "Strumento OCR",
|
||||||
"center.ocrDesc": "Trascrizione immagini sul server. Serve tesseract.",
|
"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.mailTitle": "Email di recupero",
|
||||||
"center.mailChecking": "Verifica…",
|
"center.mailChecking": "Verifica…",
|
||||||
"center.mailFrom": "Attivo — inviate da {from}",
|
"center.mailFrom": "Attivo — inviate da {from}",
|
||||||
@@ -2685,6 +2704,7 @@ const it: Dict = {
|
|||||||
"msg.copy": "Copia messaggio",
|
"msg.copy": "Copia messaggio",
|
||||||
"msg.search": "Cerca nel web",
|
"msg.search": "Cerca nel web",
|
||||||
"msg.report": "Segnala risposta",
|
"msg.report": "Segnala risposta",
|
||||||
|
"msg.reportDisabled": "Segnalazioni disabilitate",
|
||||||
"msg.retry": "Riprova",
|
"msg.retry": "Riprova",
|
||||||
"report.title": "Segnala questa risposta",
|
"report.title": "Segnala questa risposta",
|
||||||
"report.reason": "Motivo",
|
"report.reason": "Motivo",
|
||||||
@@ -3088,6 +3108,9 @@ const pt: Dict = {
|
|||||||
"center.accDescOff": "Mostra Pedir acesso no login. Triagem na aba Acesso.",
|
"center.accDescOff": "Mostra Pedir acesso no login. Triagem na aba Acesso.",
|
||||||
"center.ocrTitle": "Ferramenta OCR",
|
"center.ocrTitle": "Ferramenta OCR",
|
||||||
"center.ocrDesc": "Transcrição no servidor. Precisa tesseract.",
|
"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.mailTitle": "Emails de recuperação",
|
||||||
"center.mailChecking": "A verificar…",
|
"center.mailChecking": "A verificar…",
|
||||||
"center.mailFrom": "Ativo — enviados de {from}",
|
"center.mailFrom": "Ativo — enviados de {from}",
|
||||||
@@ -3224,6 +3247,7 @@ const pt: Dict = {
|
|||||||
"msg.copy": "Copiar mensagem",
|
"msg.copy": "Copiar mensagem",
|
||||||
"msg.search": "Pesquisar na web",
|
"msg.search": "Pesquisar na web",
|
||||||
"msg.report": "Denunciar resposta",
|
"msg.report": "Denunciar resposta",
|
||||||
|
"msg.reportDisabled": "Denúncias desativadas",
|
||||||
"msg.retry": "Tentar novamente",
|
"msg.retry": "Tentar novamente",
|
||||||
"report.title": "Denunciar esta resposta",
|
"report.title": "Denunciar esta resposta",
|
||||||
"report.reason": "Motivo",
|
"report.reason": "Motivo",
|
||||||
|
|||||||
@@ -389,6 +389,11 @@ export function isOcrToolEnabled(): boolean {
|
|||||||
return getSetting("tools_ocr_enabled", "1") === "1";
|
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 ---
|
// --- public shares ---
|
||||||
|
|
||||||
export function ensureShare(convId: string): string {
|
export function ensureShare(convId: string): string {
|
||||||
|
|||||||
+9
-1
@@ -33,6 +33,7 @@ import {
|
|||||||
getUserById,
|
getUserById,
|
||||||
getUserByUsername,
|
getUserByUsername,
|
||||||
isRegistrationEnabled,
|
isRegistrationEnabled,
|
||||||
|
isReportsEnabled,
|
||||||
listConversations,
|
listConversations,
|
||||||
listArchivedConversations,
|
listArchivedConversations,
|
||||||
listUsers,
|
listUsers,
|
||||||
@@ -831,6 +832,7 @@ const server = Bun.serve<{ ticketId: string | null; isAdmin: boolean }>({
|
|||||||
registrationEnabled: isRegistrationEnabled(),
|
registrationEnabled: isRegistrationEnabled(),
|
||||||
accessRequestEnabled: getSetting("registration_request_enabled", "1") === "1",
|
accessRequestEnabled: getSetting("registration_request_enabled", "1") === "1",
|
||||||
ocrEnabled: getSetting("tools_ocr_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);
|
if (typeof body.ocrEnabled !== "boolean") return json({ error: "ocrEnabled must be boolean" }, 400);
|
||||||
setSetting("tools_ocr_enabled", body.ocrEnabled ? "1" : "0");
|
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({
|
return json({
|
||||||
settings: {
|
settings: {
|
||||||
registrationEnabled: isRegistrationEnabled(),
|
registrationEnabled: isRegistrationEnabled(),
|
||||||
accessRequestEnabled: getSetting("registration_request_enabled", "1") === "1",
|
accessRequestEnabled: getSetting("registration_request_enabled", "1") === "1",
|
||||||
ocrEnabled: getSetting("tools_ocr_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) ---
|
// --- content reports (flagged AI responses) ---
|
||||||
if (path === "/api/reports" && req.method === "POST") {
|
if (path === "/api/reports" && req.method === "POST") {
|
||||||
|
if (!isReportsEnabled()) return json({ error: "reports are disabled" }, 403);
|
||||||
const { ok, body } = await readJson(req);
|
const { ok, body } = await readJson(req);
|
||||||
if (!ok) return json({ error: "invalid JSON" }, 400);
|
if (!ok) return json({ error: "invalid JSON" }, 400);
|
||||||
const reason = body.reason as ReportReason | undefined;
|
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) ---
|
// --- platform tools (uploads always go through tools) ---
|
||||||
if (path === "/api/tools" && req.method === "GET") {
|
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") {
|
if (path === "/api/tools/ocr" && req.method === "POST") {
|
||||||
|
|||||||
Reference in New Issue
Block a user