From b91bf7a5f6142dbde34ffedd9f921f4e7e545299 Mon Sep 17 00:00:00 2001 From: OpenAssistant Contributor Date: Sun, 1 Mar 2026 09:14:29 +0800 Subject: [PATCH 1/2] feat: Add 'Hide all Chats' button to chat list Implements #3287 - Add new HideAllChatsButton component with confirmation dialog - Integrate button into ChatListBase component - Add translation strings for hide all functionality - Allow users to hide all visible chats at once - Hidden chats can still be found in 'Visible & hidden' view --- website/public/locales/en/chat.json | 5 + website/src/components/Chat/ChatListBase.tsx | 24 +++- .../components/Chat/HideAllChatsButton.tsx | 115 ++++++++++++++++++ 3 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 website/src/components/Chat/HideAllChatsButton.tsx 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/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 })} + + + + + + + + + + + ); +}; From 8fbf01032b463e7e9a904dd8eb027dede8770bda Mon Sep 17 00:00:00 2001 From: stargazerwh Date: Wed, 4 Mar 2026 10:25:57 +0800 Subject: [PATCH 2/2] feat: add updates section to dashboard - Add UpdatesWidget component to display news/updates - Support RSS feed integration (configurable via env) - Show 'NEW' badge for unread updates - Store read status in local storage - Add loading skeleton and empty state - Add translations for new UI strings Fixes #2606 --- website/public/locales/en/dashboard.json | 4 + .../components/Dashboard/UpdatesWidget.tsx | 160 ++++++++++++++++++ website/src/components/Dashboard/index.ts | 1 + website/src/pages/dashboard.tsx | 3 +- 4 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 website/src/components/Dashboard/UpdatesWidget.tsx diff --git a/website/public/locales/en/dashboard.json b/website/public/locales/en/dashboard.json index ba37c4edba..53b8c2252a 100644 --- a/website/public/locales/en/dashboard.json +++ b/website/public/locales/en/dashboard.json @@ -5,6 +5,10 @@ "label": "Label", "dashboard": "Dashboard", "go": "Go", + "latest_updates": "Latest Updates", + "view_all": "View All", + "new": "NEW", + "no_updates": "No updates available", "welcome_message": { "label": "Welcome, {{username}}!", "contributor": "Contributor", diff --git a/website/src/components/Dashboard/UpdatesWidget.tsx b/website/src/components/Dashboard/UpdatesWidget.tsx new file mode 100644 index 0000000000..0da9e1870c --- /dev/null +++ b/website/src/components/Dashboard/UpdatesWidget.tsx @@ -0,0 +1,160 @@ +import { Card, CardBody, Link, Text, VStack, HStack, Badge, Skeleton } from "@chakra-ui/react"; +import NextLink from "next/link"; +import { useTranslation } from "next-i18next"; +import { useEffect, useState } from "react"; + +interface NewsItem { + id: string; + title: string; + date: string; + url?: string; + isNew?: boolean; +} + +// Default news items (fallback if RSS fetch fails) +const DEFAULT_NEWS: NewsItem[] = [ + { + id: "1", + title: "Welcome to Open Assistant!", + date: new Date().toISOString().split("T")[0], + isNew: true, + }, +]; + +// RSS Feed URL (configurable) +const RSS_FEED_URL = process.env.NEXT_PUBLIC_OA_RSS_FEED || ""; + +export function UpdatesWidget() { + const { t } = useTranslation(["dashboard"]); + const [news, setNews] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchNews = async () => { + try { + // Try to fetch from RSS feed if configured + if (RSS_FEED_URL) { + const response = await fetch(RSS_FEED_URL); + if (response.ok) { + const data = await response.text(); + // Simple RSS parsing (can be enhanced with a proper RSS parser) + const parser = new DOMParser(); + const xml = parser.parseFromString(data, "text/xml"); + const items = xml.querySelectorAll("item"); + + const parsedNews: NewsItem[] = Array.from(items).slice(0, 5).map((item, index) => ({ + id: String(index), + title: item.querySelector("title")?.textContent || "", + date: item.querySelector("pubDate")?.textContent?.split("T")[0] || "", + url: item.querySelector("link")?.textContent || "", + isNew: index === 0, // Mark first item as new + })); + + setNews(parsedNews); + } else { + setNews(DEFAULT_NEWS); + } + } else { + // Use default news if no RSS configured + setNews(DEFAULT_NEWS); + } + } catch (error) { + console.error("Failed to fetch news:", error); + setNews(DEFAULT_NEWS); + } finally { + setIsLoading(false); + } + }; + + fetchNews(); + }, []); + + // Check if news was read (stored in localStorage) + const [readNews, setReadNews] = useState>(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("oa-read-news"); + return saved ? new Set(JSON.parse(saved)) : new Set(); + } + return new Set(); + }); + + const markAsRead = (id: string) => { + const newRead = new Set(readNews); + newRead.add(id); + setReadNews(newRead); + if (typeof window !== "undefined") { + localStorage.setItem("oa-read-news", JSON.stringify([...newRead])); + } + }; + + return ( +
+
+
+ + {t("latest_updates")} + {news.some((n) => n.isNew && !readNews.has(n.id)) && ( + + {t("new")} + + )} + + + + {t("view_all")} -> + + +
+ + + + {isLoading ? ( + <> + + + + + ) : news.length === 0 ? ( + {t("no_updates")} + ) : ( + news.map((item) => ( + markAsRead(item.id)} + as={item.url ? Link : undefined} + href={item.url} + isExternal={!!item.url} + > + + {item.isNew && !readNews.has(item.id) && ( + + {t("new")} + + )} + + {item.title} + + + + {item.date} + + + )) + )} + + + +
+
+ ); +} diff --git a/website/src/components/Dashboard/index.ts b/website/src/components/Dashboard/index.ts index 848583457c..673f9bbca9 100644 --- a/website/src/components/Dashboard/index.ts +++ b/website/src/components/Dashboard/index.ts @@ -1,3 +1,4 @@ export { LeaderboardWidget } from "./LeaderboardWidget"; export { TaskOption } from "./TaskOption"; +export { UpdatesWidget } from "./UpdatesWidget"; export { WelcomeCard } from "./WelcomeCard"; diff --git a/website/src/pages/dashboard.tsx b/website/src/pages/dashboard.tsx index 17282b61d2..ad744c04e5 100644 --- a/website/src/pages/dashboard.tsx +++ b/website/src/pages/dashboard.tsx @@ -3,7 +3,7 @@ import Head from "next/head"; import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; import { useEffect, useMemo } from "react"; -import { LeaderboardWidget, TaskOption, WelcomeCard } from "src/components/Dashboard"; +import { LeaderboardWidget, TaskOption, UpdatesWidget, WelcomeCard } from "src/components/Dashboard"; import { DashboardLayout } from "src/components/Layout"; import { get } from "src/lib/api"; import { AvailableTasks, TaskCategory } from "src/types/Task"; @@ -81,6 +81,7 @@ const Dashboard = () => { )} +