@@ -60,12 +61,13 @@ function MessageBubble({ msg, index, authorAvatar, authorName, onReport }: { msg
);
}
-function ReportDialog({ open, onClose, conversationId, messageIndex, content, model }: {
+function ReportDialog({ open, onClose, conversationId, messageIndex, content, prompt, model }: {
open: boolean;
onClose: () => void;
conversationId: string | null;
messageIndex: number;
content: string;
+ prompt: string;
model: string;
}) {
const { t } = useT();
@@ -74,6 +76,7 @@ function ReportDialog({ open, onClose, conversationId, messageIndex, content, mo
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [sent, setSent] = useState(false);
+ const [duplicate, setDuplicate] = useState(false);
useEffect(() => {
if (open) {
@@ -81,6 +84,7 @@ function ReportDialog({ open, onClose, conversationId, messageIndex, content, mo
setDetails("");
setError("");
setSent(false);
+ setDuplicate(false);
}
}, [open, messageIndex]);
@@ -96,11 +100,16 @@ function ReportDialog({ open, onClose, conversationId, messageIndex, content, mo
reason: reason as ReportReason,
details: details.trim(),
content,
+ prompt,
model,
});
setSent(true);
} catch (err) {
- setError(err instanceof Error ? err.message : t("report.failed"));
+ if (err instanceof ApiError && err.status === 409) {
+ setDuplicate(true);
+ } else {
+ setError(err instanceof Error ? err.message : t("report.failed"));
+ }
} finally {
setBusy(false);
}
@@ -108,7 +117,16 @@ function ReportDialog({ open, onClose, conversationId, messageIndex, content, mo
return (
- {sent ? (
+ {sent || duplicate ? (
+
+
+ {duplicate ? t("report.alreadyReported") : t("report.sent")}
+
+
+
+
+
+ ) : (
{t("report.sent")}
@@ -429,6 +447,7 @@ export function Chat({
{(() => {
const shown = messages.filter((m) => m.role !== "system");
const target = reportIndex !== null ? shown[reportIndex] : undefined;
+ const prev = reportIndex !== null && reportIndex > 0 ? shown[reportIndex - 1] : undefined;
return (
);
diff --git a/frontend/src/components/MessageContent.tsx b/frontend/src/components/MessageContent.tsx
index 2536cd0..1075580 100644
--- a/frontend/src/components/MessageContent.tsx
+++ b/frontend/src/components/MessageContent.tsx
@@ -16,11 +16,16 @@ import { cn } from "../lib/cn.ts";
*/
const ATTACH_RE = /^(\[.*?\])\n```text\n([\s\S]*?)\n```/gm;
-function AttachmentBlock({ header, body }: { header: string; body: string }) {
+function AttachmentBlock({ header, body, tone }: { header: string; body: string; tone: "accent" | "plain" }) {
const [open, setOpen] = useState(false);
const label = header.startsWith("[") && header.endsWith("]") ? header.slice(1, -1) : header;
return (
-
+
{open && (
-
+
{body}
)}
@@ -43,7 +53,7 @@ function AttachmentBlock({ header, body }: { header: string; body: string }) {
);
}
-export function UserMessageContent({ content }: { content: string }) {
+export function UserMessageContent({ content, tone = "accent" }: { content: string; tone?: "accent" | "plain" }) {
const parts: React.ReactNode[] = [];
let last = 0;
let key = 0;
@@ -51,7 +61,7 @@ export function UserMessageContent({ content }: { content: string }) {
let m: RegExpExecArray | null;
while ((m = ATTACH_RE.exec(content)) !== null) {
if (m.index > last) parts.push({content.slice(last, m.index)});
- parts.push();
+ parts.push();
last = m.index + m[0].length;
}
if (last < content.length) parts.push({content.slice(last)});
diff --git a/frontend/src/components/ReportsPanel.tsx b/frontend/src/components/ReportsPanel.tsx
index 69d1132..929e8b8 100644
--- a/frontend/src/components/ReportsPanel.tsx
+++ b/frontend/src/components/ReportsPanel.tsx
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from "react";
-import { CheckCircle2, Clock, Flag, Inbox, Search, Undo2, XCircle } from "lucide-react";
+import { CheckCircle2, Clock, Eye, EyeOff, Flag, Inbox, Search, Undo2, XCircle } from "lucide-react";
import { api, type Report, type ReportStatus } from "../lib/api.ts";
-import { Button, Field, Input, Spinner } from "./ui.tsx";
+import { Button, ConfirmDialog, Field, Input, Spinner } from "./ui.tsx";
import { useT, type StringKey } from "../lib/i18n.ts";
import { cn } from "../lib/cn.ts";
@@ -118,6 +118,11 @@ export function ReportsPanel() {
{t(`reports.status.${r.status}` as StringKey)}
+ {r.reporter_shadowbanned === 1 && (
+
+ {t("reports.unreliable")}
+
+ )}
{selectedId === r.id && refresh(r.id)} />}
diff --git a/frontend/src/components/SharePage.tsx b/frontend/src/components/SharePage.tsx
index 8fad9ab..173e7ba 100644
--- a/frontend/src/components/SharePage.tsx
+++ b/frontend/src/components/SharePage.tsx
@@ -4,6 +4,7 @@ import { api } from "../lib/api.ts";
import type { SharedChat } from "../lib/types.ts";
import { AssistantAvatar, Avatar, FlowerMark } from "./ui.tsx";
import { Markdown } from "./Markdown.tsx";
+import { UserMessageContent } from "./MessageContent.tsx";
import { useT } from "../lib/i18n.ts";
export function SharePage({ publicId }: { publicId: string }) {
@@ -80,7 +81,7 @@ export function SharePage({ publicId }: { publicId: string }) {
{m.role === "user" ? chat.authorName : "KisAssistant"}
{m.role === "user" ? (
- {m.content}
+
) : (
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 8acb851..86e0c27 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -135,6 +135,11 @@ 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" }),
+ adminShadowban: (id: string, shadowbanned: boolean) =>
+ req<{ shadowbanned: boolean }>(`/api/admin/users/${id}/shadowban`, {
+ method: "POST",
+ body: JSON.stringify({ shadowbanned }),
+ }),
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) }),