From e69654430aba8fe0c5af5f6801bdc7bddf16398b Mon Sep 17 00:00:00 2001 From: Anais Saraiva Date: Mon, 21 Sep 2026 21:54:03 +0200 Subject: [PATCH] . --- frontend/src/components/Chat.tsx | 32 ++++++++++++++++++---- frontend/src/components/MessageContent.tsx | 20 ++++++++++---- frontend/src/components/ReportsPanel.tsx | 9 ++++-- frontend/src/components/SharePage.tsx | 3 +- frontend/src/lib/api.ts | 5 ++++ 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx index 2d6ca6f..4afd63c 100644 --- a/frontend/src/components/Chat.tsx +++ b/frontend/src/components/Chat.tsx @@ -1,9 +1,10 @@ import { useEffect, useRef, useState } from "react"; import { Archive, ArchiveRestore, BookOpen, Bot, Check, Copy, Cpu, FileText, Flag, Menu, PanelLeftOpen, Paperclip, ScanText, Search, SendHorizontal, Share2, User as UserIcon, X } from "lucide-react"; import type { Attachment, ChatMessage, Conversation, DriverModel, User } from "../lib/types.ts"; -import { api, type ReportReason } from "../lib/api.ts"; +import { api, ApiError, type ReportReason } from "../lib/api.ts"; import { AssistantAvatar, Avatar, Button, Field, FlowerMark, IconButton, Modal, Picker, type PickerGroup, Spinner } from "./ui.tsx"; import { Markdown } from "./Markdown.tsx"; +import { UserMessageContent } from "./MessageContent.tsx"; import { cn } from "../lib/cn.ts"; import { useFeatures } from "../lib/features.ts"; import { useT, type StringKey } from "../lib/i18n.ts"; @@ -34,8 +35,8 @@ function MessageBubble({ msg, index, authorAvatar, authorName, onReport }: { msg {isUser ? t("chat.you") : "KisAssistant"} {isUser ? ( -
- {msg.content} +
+
) : (
@@ -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) }),