diff --git a/website/public/locales/en/chat.json b/website/public/locales/en/chat.json index 3516b2dd87..c9c08cb8f8 100644 --- a/website/public/locales/en/chat.json +++ b/website/public/locales/en/chat.json @@ -10,6 +10,11 @@ "drafts_generating_notify": "Draft messages are still generating. Please wait.", "edit_plugin": "Edit Plugin", "empty": "Untitled", + "hide_all_chats": "Hide all chats", + "hide_all_confirmation": "Are you sure you want to hide all {{count}} visible chats? They can be found in the 'Visible & hidden' view.", + "hide_all_success": "All chats have been hidden successfully", + "hide_all_error": "Failed to hide some chats", + "hiding": "Hiding...", "input_placeholder": "Ask the assistant anything", "login_message": "To use this feature, you need to login again. Login using one of these providers:", "max_new_tokens": "Max new tokens", diff --git a/website/public/locales/en/tasks.json b/website/public/locales/en/tasks.json index ea19bc062d..b32fdc3560 100644 --- a/website/public/locales/en/tasks.json +++ b/website/public/locales/en/tasks.json @@ -95,5 +95,10 @@ "tab_preview": "Preview", "writing_wrong_langauge_a_b": "You appear to be writing in {{detected_lang}} but this will be submitted as {{submit_lang}}.", "not_rankable": "All answers are factually incorrect and cannot be ranked", + "horizontal_layout": "Horizontal Layout", + "vertical_layout": "Vertical Layout", + "show_full_content": "Show Full Content", + "messages_per_row": "Messages Per Row", + "full_text": "Full Text", "moderator_edit_explain": "Modify the highlighted message while keeping changes to a minimum. This action can cause the conversation to not make sense and cause ratings to no longer be accurate. Please use carefully." } diff --git a/website/src/components/Chat/ChatListBase.tsx b/website/src/components/Chat/ChatListBase.tsx index f23c2d4fbb..a11c8a2fd9 100644 --- a/website/src/components/Chat/ChatListBase.tsx +++ b/website/src/components/Chat/ChatListBase.tsx @@ -9,6 +9,7 @@ import SimpleBar from "simplebar-react"; import { ChatListItem } from "./ChatListItem"; import { ChatViewSelection } from "./ChatViewSelection"; import { CreateChatButton } from "./CreateChatButton"; +import { HideAllChatsButton } from "./HideAllChatsButton"; import { InferencePoweredBy } from "./InferencePoweredBy"; import { ChatListViewSelection, useListChatPagination } from "./useListChatPagination"; @@ -67,6 +68,20 @@ export const ChatListBase = memo(function ChatListBase({ mutateChatResponses(); }, [mutateChatResponses]); + const handleHideAllChats = useCallback(() => { + mutateChatResponses( + (chatResponses) => [ + ...(chatResponses?.map((chatResponse) => ({ + ...chatResponse, + chats: [], + })) || []), + ], + false + ); + }, [mutateChatResponses]); + + const chatIds = chats.map((chat) => chat.id); + const content = ( <> {chats.map((chat) => ( @@ -107,9 +122,12 @@ export const ChatListBase = memo(function ChatListBase({ > {t("create_chat")} - {allowViews && ( - setView(e.target.value as ChatListViewSelection)} /> - )} + + {allowViews && ( + setView(e.target.value as ChatListViewSelection)} /> + )} + + {noScrollbar ? ( content diff --git a/website/src/components/Chat/HideAllChatsButton.tsx b/website/src/components/Chat/HideAllChatsButton.tsx new file mode 100644 index 0000000000..1ee67dbee7 --- /dev/null +++ b/website/src/components/Chat/HideAllChatsButton.tsx @@ -0,0 +1,115 @@ +import { + AlertDialog, + AlertDialogBody, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Button, + IconButton, + Tooltip, + useDisclosure, + useToast, +} from "@chakra-ui/react"; +import { EyeOff } from "lucide-react"; +import { useTranslation } from "next-i18next"; +import { useCallback, useRef, useState } from "react"; +import { API_ROUTES } from "src/lib/routes"; +import useSWRMutation from "swr/mutation"; +import { put } from "src/lib/api"; + +interface HideAllChatsButtonProps { + chatIds: string[]; + onHideAll: () => void; +} + +export const HideAllChatsButton = ({ chatIds, onHideAll }: HideAllChatsButtonProps) => { + const { t } = useTranslation(["chat", "common"]); + const { isOpen, onOpen, onClose } = useDisclosure(); + const cancelRef = useRef(null); + const toast = useToast(); + const [isHiding, setIsHiding] = useState(false); + + const { trigger: triggerHide } = useSWRMutation(API_ROUTES.UPDATE_CHAT(), put); + + const handleHideAll = useCallback(async () => { + if (chatIds.length === 0) { + onClose(); + return; + } + + setIsHiding(true); + try { + // Hide all chats sequentially + for (const chatId of chatIds) { + await triggerHide({ chat_id: chatId, hidden: true }); + } + + toast({ + title: t("chat:hide_all_success"), + status: "success", + duration: 3000, + isClosable: true, + }); + + onHideAll(); + onClose(); + } catch (error) { + toast({ + title: t("chat:hide_all_error"), + status: "error", + duration: 3000, + isClosable: true, + }); + } finally { + setIsHiding(false); + } + }, [chatIds, onHideAll, onClose, toast, t, triggerHide]); + + if (chatIds.length === 0) { + return null; + } + + return ( + <> + + } + aria-label={t("chat:hide_all_chats")} + onClick={onOpen} + variant="ghost" + size="sm" + /> + + + + + + + {t("chat:hide_all_chats")} + + + + {t("chat:hide_all_confirmation", { count: chatIds.length })} + + + + + + + + + + + ); +}; diff --git a/website/src/components/CollapsableText.tsx b/website/src/components/CollapsableText.tsx index 3f6db47771..ed05b11232 100644 --- a/website/src/components/CollapsableText.tsx +++ b/website/src/components/CollapsableText.tsx @@ -1,11 +1,19 @@ import { useBreakpointValue } from "@chakra-ui/react"; -export const CollapsableText = ({ text }: { text: string }) => { +interface CollapsableTextProps { + text: string; + isCollapsed?: boolean; +} + +export const CollapsableText = ({ text, isCollapsed = true }: CollapsableTextProps) => { const maxLength = useBreakpointValue({ base: 220, md: 500, lg: 700, xl: 1000 }); - if (typeof text !== "string" || text.length <= maxLength) { + + // If not collapsed or text is short, show full text + if (!isCollapsed || typeof text !== "string" || text.length <= maxLength) { return <>{text}; - } else { - const visibleText = text.substring(0, maxLength - 3); - return {visibleText}...; } + + // Show collapsed text + const visibleText = text.substring(0, maxLength - 3); + return {visibleText}...; }; diff --git a/website/src/components/Sortable/Sortable.tsx b/website/src/components/Sortable/Sortable.tsx index 35ea9ec779..a7292bf9fb 100644 --- a/website/src/components/Sortable/Sortable.tsx +++ b/website/src/components/Sortable/Sortable.tsx @@ -1,4 +1,6 @@ import { + Box, + Checkbox, Flex, Modal, ModalBody, @@ -6,6 +8,12 @@ import { ModalContent, ModalHeader, ModalOverlay, + Slider, + SliderFilledTrack, + SliderThumb, + SliderTrack, + Switch, + Text, useDisclosure, } from "@chakra-ui/react"; import { @@ -26,6 +34,7 @@ import { verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { lazy, Suspense, useCallback, useEffect, useState } from "react"; +import { useTranslation } from "next-i18next"; import { Message } from "src/types/Conversation"; import { CollapsableText } from "../CollapsableText"; @@ -42,15 +51,47 @@ export interface SortableProps { revealSynthetic?: boolean; } -interface SortableItem { +interface SortableItemType { id: number; originalIndex: number; item: Message; } +// Local storage keys +const STORAGE_KEYS = { + horizontalLayout: "oa-sortable-horizontal-layout", + removeContentLimit: "oa-sortable-remove-content-limit", + messagesPerRow: "oa-sortable-messages-per-row", +}; + export const Sortable = ({ onChange, revealSynthetic, ...props }: SortableProps) => { - const [itemsWithIds, setItemsWithIds] = useState([]); + const [itemsWithIds, setItemsWithIds] = useState([]); const [modalText, setModalText] = useState(null); + + // UI preferences from local storage + const [isHorizontal, setIsHorizontal] = useState(() => { + if (typeof window !== "undefined") { + return localStorage.getItem(STORAGE_KEYS.horizontalLayout) === "true"; + } + return false; + }); + + const [removeContentLimit, setRemoveContentLimit] = useState(() => { + if (typeof window !== "undefined") { + return localStorage.getItem(STORAGE_KEYS.removeContentLimit) === "true"; + } + return false; + }); + + const [messagesPerRow, setMessagesPerRow] = useState(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem(STORAGE_KEYS.messagesPerRow); + return saved ? parseInt(saved, 10) : 3; + } + return 3; + }); + + const { t } = useTranslation("tasks"); useEffect(() => { setItemsWithIds( props.items.map((item, idx) => ({ @@ -61,6 +102,25 @@ export const Sortable = ({ onChange, revealSynthetic, ...props }: SortableProps) ); }, [props.items]); + // Save preferences to local storage + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem(STORAGE_KEYS.horizontalLayout, String(isHorizontal)); + } + }, [isHorizontal]); + + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem(STORAGE_KEYS.removeContentLimit, String(removeContentLimit)); + } + }, [removeContentLimit]); + + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem(STORAGE_KEYS.messagesPerRow, String(messagesPerRow)); + } + }, [messagesPerRow]); + const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, tolerance: 100 }, @@ -90,8 +150,79 @@ export const Sortable = ({ onChange, revealSynthetic, ...props }: SortableProps) [onChange] ); + // Handle horizontal layout toggle + const handleLayoutToggle = useCallback(() => { + setIsHorizontal((prev) => !prev); + }, []); + + // Handle content limit toggle + const handleContentLimitToggle = useCallback(() => { + setRemoveContentLimit((prev) => !prev); + }, []); + + // Handle messages per row change + const handleMessagesPerRowChange = useCallback((value: number) => { + setMessagesPerRow(value); + }, []); + + // Calculate flex wrap style for horizontal layout + const containerStyle = isHorizontal + ? { flexWrap: "wrap" as const, gap: "16px" } + : { flexDirection: "column" as const, gap: "16px" }; + + const itemWidth = isHorizontal ? `${100 / messagesPerRow - 2}%` : "100%"; + return ( <> + {/* Control Panel */} + + + {/* Layout Toggle */} + + + + {isHorizontal ? t("horizontal_layout") : t("vertical_layout")} + + + + {/* Content Limit Toggle */} + + + {t("show_full_content")} + + + {/* Messages Per Row Slider (only in horizontal mode) */} + {isHorizontal && ( + + + {t("messages_per_row")}: {messagesPerRow} + + + + + + + + + )} + + + - + {itemsWithIds.map(({ id, item }, index) => ( - { - setModalText(item.text); - onOpen(); - }} - key={id} - id={id} - index={index} - isEditable={props.isEditable} - isDisabled={!!props.isDisabled} - synthetic={item.synthetic && !!revealSynthetic} - > - - + + + ))} @@ -140,7 +272,7 @@ export const Sortable = ({ onChange, revealSynthetic, ...props }: SortableProps) > - Full Text + {t("full_text")}