diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx
index 780a3b2..2d6ca6f 100644
--- a/frontend/src/components/Chat.tsx
+++ b/frontend/src/components/Chat.tsx
@@ -1,15 +1,28 @@
import { useEffect, useRef, useState } from "react";
-import { Archive, ArchiveRestore, BookOpen, Bot, Cpu, FileText, Menu, PanelLeftOpen, Paperclip, ScanText, SendHorizontal, Share2, User as UserIcon, X } from "lucide-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 { AssistantAvatar, Avatar, FlowerMark, IconButton, Picker, type PickerGroup, Spinner } from "./ui.tsx";
+import { api, 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 { cn } from "../lib/cn.ts";
import { useFeatures } from "../lib/features.ts";
-import { useT } from "../lib/i18n.ts";
+import { useT, type StringKey } from "../lib/i18n.ts";
-function MessageBubble({ msg, authorAvatar, authorName }: { msg: ChatMessage; authorAvatar?: string; authorName: string }) {
+const REPORT_REASONS: ReportReason[] = ["copyright", "gore", "falseinfo", "bug"];
+
+function MessageBubble({ msg, index, authorAvatar, authorName, onReport }: { msg: ChatMessage; index: number; authorAvatar?: string; authorName: string; onReport?: (index: number) => void }) {
const { t } = useT();
const isUser = msg.role === "user";
+ const [copied, setCopied] = useState(false);
+ function copy() {
+ navigator.clipboard.writeText(msg.content).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ }).catch(() => {});
+ }
+ function searchWeb() {
+ window.open(`https://duckduckgo.com/?q=${encodeURIComponent(msg.content.slice(0, 400))}`, "_blank", "noopener,noreferrer");
+ }
return (
{isUser
@@ -29,11 +42,117 @@ function MessageBubble({ msg, authorAvatar, authorName }: { msg: ChatMessage; au
)}
+ {!isUser && onReport && (
+
+
+ {copied ? : }
+
+
+
+
+ onReport(index)}>
+
+
+
+ )}
);
}
+function ReportDialog({ open, onClose, conversationId, messageIndex, content, model }: {
+ open: boolean;
+ onClose: () => void;
+ conversationId: string | null;
+ messageIndex: number;
+ content: string;
+ model: string;
+}) {
+ const { t } = useT();
+ const [reason, setReason] = useState("");
+ const [details, setDetails] = useState("");
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState("");
+ const [sent, setSent] = useState(false);
+
+ useEffect(() => {
+ if (open) {
+ setReason("");
+ setDetails("");
+ setError("");
+ setSent(false);
+ }
+ }, [open, messageIndex]);
+
+ async function submit(e: React.FormEvent) {
+ e.preventDefault();
+ if (!reason || busy) return;
+ setBusy(true);
+ setError("");
+ try {
+ await api.createReport({
+ conversationId: conversationId ?? undefined,
+ messageIndex,
+ reason: reason as ReportReason,
+ details: details.trim(),
+ content,
+ model,
+ });
+ setSent(true);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : t("report.failed"));
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ return (
+