Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions website/public/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions website/public/locales/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 21 additions & 3 deletions website/src/components/Chat/ChatListBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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) => (
Expand Down Expand Up @@ -107,9 +122,12 @@ export const ChatListBase = memo(function ChatListBase({
>
{t("create_chat")}
</CreateChatButton>
{allowViews && (
<ChatViewSelection w={["full", "auto"]} onChange={(e) => setView(e.target.value as ChatListViewSelection)} />
)}
<Flex gap="2" alignItems="center">
{allowViews && (
<ChatViewSelection w={["full", "auto"]} onChange={(e) => setView(e.target.value as ChatListViewSelection)} />
)}
<HideAllChatsButton chatIds={chatIds} onHideAll={handleHideAllChats} />
</Flex>
</Flex>
{noScrollbar ? (
content
Expand Down
115 changes: 115 additions & 0 deletions website/src/components/Chat/HideAllChatsButton.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement>(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 (
<>
<Tooltip label={t("chat:hide_all_chats")}>
<IconButton
icon={<EyeOff size="16px" />}
aria-label={t("chat:hide_all_chats")}
onClick={onOpen}
variant="ghost"
size="sm"
/>
</Tooltip>

<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{t("chat:hide_all_chats")}
</AlertDialogHeader>

<AlertDialogBody>
{t("chat:hide_all_confirmation", { count: chatIds.length })}
</AlertDialogBody>

<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose} isDisabled={isHiding}>
{t("common:cancel")}
</Button>
<Button
colorScheme="blue"
onClick={handleHideAll}
ml={3}
isLoading={isHiding}
loadingText={t("chat:hiding")}
>
{t("common:confirm")}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
);
};
160 changes: 160 additions & 0 deletions website/src/components/Dashboard/UpdatesWidget.tsx
Original file line number Diff line number Diff line change
@@ -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<NewsItem[]>([]);
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<Set<string>>(() => {
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 (
<main className="h-fit col-span-3">
<div className="flex flex-col gap-4">
<div className="flex items-end justify-between">
<HStack>
<Text className="text-2xl font-bold">{t("latest_updates")}</Text>
{news.some((n) => n.isNew && !readNews.has(n.id)) && (
<Badge colorScheme="red" variant="solid">
{t("new")}
</Badge>
)}
</HStack>
<Link as={NextLink} href="/updates" _hover={{ textDecoration: "none" }}>
<Text color="blue.400" className="text-sm font-bold">
{t("view_all")} -&gt;
</Text>
</Link>
</div>
<Card>
<CardBody>
<VStack align="stretch" spacing={3}>
{isLoading ? (
<>
<Skeleton height="20px" />
<Skeleton height="20px" />
<Skeleton height="20px" />
</>
) : news.length === 0 ? (
<Text color="gray.500">{t("no_updates")}</Text>
) : (
news.map((item) => (
<HStack
key={item.id}
justify="space-between"
p={2}
borderRadius="md"
bg={readNews.has(item.id) ? undefined : "blue.50"}
_dark={{ bg: readNews.has(item.id) ? undefined : "blue.900" }}
cursor="pointer"
onClick={() => markAsRead(item.id)}
as={item.url ? Link : undefined}
href={item.url}
isExternal={!!item.url}
>
<HStack>
{item.isNew && !readNews.has(item.id) && (
<Badge colorScheme="red" size="sm">
{t("new")}
</Badge>
)}
<Text
fontWeight={readNews.has(item.id) ? "normal" : "semibold"}
color={readNews.has(item.id) ? "gray.600" : undefined}
_dark={{ color: readNews.has(item.id) ? "gray.400" : undefined }}
>
{item.title}
</Text>
</HStack>
<Text fontSize="sm" color="gray.500">
{item.date}
</Text>
</HStack>
))
)}
</VStack>
</CardBody>
</Card>
</div>
</main>
);
}
1 change: 1 addition & 0 deletions website/src/components/Dashboard/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { LeaderboardWidget } from "./LeaderboardWidget";
export { TaskOption } from "./TaskOption";
export { UpdatesWidget } from "./UpdatesWidget";
export { WelcomeCard } from "./WelcomeCard";
3 changes: 2 additions & 1 deletion website/src/pages/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -81,6 +81,7 @@ const Dashboard = () => {
)}

<TaskOption content={availableTaskTypes} />
<UpdatesWidget />
<Card>
<CardBody>
<XPBar />
Expand Down
Loading