better-chatbot / src /components /layouts /app-header.tsx
Bot
Initial commit for HF Spaces
05c5ed5
Raw
History Blame Contribute Delete
7.82 kB
"use client";
import { useSidebar } from "ui/sidebar";
import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip";
import {
AudioWaveformIcon,
ChevronDown,
MessageCircleDashed,
PanelLeft,
} from "lucide-react";
import { Button } from "ui/button";
import { Separator } from "ui/separator";
import { useEffect, useMemo } from "react";
import { ThreadDropdown } from "../thread-dropdown";
import { appStore } from "@/app/store";
import { usePathname, useSearchParams } from "next/navigation";
import { useShallow } from "zustand/shallow";
import { getShortcutKeyList, Shortcuts } from "lib/keyboard-shortcuts";
import { useTranslations } from "next-intl";
import { TextShimmer } from "ui/text-shimmer";
import { buildReturnUrl } from "lib/admin/navigation-utils";
import { BackButton } from "@/components/layouts/back-button";
export function AppHeader() {
const t = useTranslations();
const [appStoreMutate] = appStore(useShallow((state) => [state.mutate]));
const { toggleSidebar, open } = useSidebar();
const currentPaths = usePathname();
const searchParams = useSearchParams();
const showActionButtons = useMemo(() => {
if (currentPaths.startsWith("/admin")) {
return false;
}
return true;
}, [currentPaths]);
const componentByPage = useMemo(() => {
if (currentPaths.startsWith("/chat/")) {
return <ThreadDropdownComponent />;
}
if (
currentPaths.startsWith("/admin/users/") &&
currentPaths.split("/").length > 3
) {
const searchPageParams = searchParams.get("searchPageParams");
const returnUrl = buildReturnUrl("/admin/users", searchPageParams || "");
return (
<BackButton
data-testid="admin-users-back-button"
returnUrl={returnUrl}
title={t("Admin.Users.backToUsers")}
/>
);
}
}, [currentPaths, searchParams]);
return (
<header className="sticky top-0 z-50 flex items-center px-3 py-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
aria-label="Toggle Sidebar"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleSidebar();
}}
data-testid="sidebar-toggle"
data-state={open ? "open" : "closed"}
>
<PanelLeft />
</Button>
</TooltipTrigger>
<TooltipContent align="start" side="bottom">
<div className="flex items-center gap-2">
{t("KeyboardShortcuts.toggleSidebar")}
<div className="text-xs text-muted-foreground flex items-center gap-1">
{getShortcutKeyList(Shortcuts.toggleSidebar).map((key) => (
<span
key={key}
className="w-5 h-5 flex items-center justify-center bg-muted rounded "
>
{key}
</span>
))}
</div>
</div>
</TooltipContent>
</Tooltip>
{componentByPage}
<div className="flex-1" />
{showActionButtons && (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
size={"icon"}
variant={"ghost"}
className="bg-secondary/40"
onClick={() => {
appStoreMutate((state) => ({
voiceChat: {
...state.voiceChat,
isOpen: true,
agentId: undefined,
},
}));
}}
>
<AudioWaveformIcon className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent align="end" side="bottom">
<div className="text-xs flex items-center gap-2">
{t("KeyboardShortcuts.toggleVoiceChat")}
<div className="text-xs text-muted-foreground flex items-center gap-1">
{getShortcutKeyList(Shortcuts.toggleVoiceChat).map((key) => (
<span
className="w-5 h-5 flex items-center justify-center bg-muted rounded "
key={key}
>
{key}
</span>
))}
</div>
</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size={"icon"}
variant={"secondary"}
className="bg-secondary/40"
onClick={() => {
appStoreMutate((state) => ({
temporaryChat: {
...state.temporaryChat,
isOpen: !state.temporaryChat.isOpen,
},
}));
}}
>
<MessageCircleDashed className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent align="end" side="bottom">
<div className="text-xs flex items-center gap-2">
{t("KeyboardShortcuts.toggleTemporaryChat")}
<div className="text-xs text-muted-foreground flex items-center gap-1">
{getShortcutKeyList(Shortcuts.toggleTemporaryChat).map(
(key) => (
<span
className="w-5 h-5 flex items-center justify-center bg-muted rounded "
key={key}
>
{key}
</span>
),
)}
</div>
</div>
</TooltipContent>
</Tooltip>
</div>
)}
</header>
);
}
function ThreadDropdownComponent() {
const [threadList, currentThreadId, generatingTitleThreadIds] = appStore(
useShallow((state) => [
state.threadList,
state.currentThreadId,
state.generatingTitleThreadIds,
]),
);
const currentThread = useMemo(() => {
return threadList.find((thread) => thread.id === currentThreadId);
}, [threadList, currentThreadId]);
useEffect(() => {
if (currentThread?.id) {
document.title = currentThread.title || "New Chat";
}
}, [currentThread?.id]);
if (!currentThread) return null;
return (
<div className="items-center gap-1 hidden md:flex">
<div className="w-1 h-4">
<Separator orientation="vertical" />
</div>
<ThreadDropdown
threadId={currentThread.id}
beforeTitle={currentThread.title}
>
<div>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
className="data-[state=open]:bg-input! hover:text-foreground cursor-pointer flex gap-1 items-center px-2 py-1 rounded-md hover:bg-accent"
>
{generatingTitleThreadIds.includes(currentThread.id) ? (
<TextShimmer className="truncate max-w-60 min-w-0 mr-1">
{currentThread.title || "New Chat"}
</TextShimmer>
) : (
<p className="truncate max-w-60 min-w-0 mr-1">
{currentThread.title || "New Chat"}
</p>
)}
<ChevronDown size={14} />
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-[200px] p-4 break-all overflow-y-auto max-h-[200px]">
{currentThread.title || "New Chat"}
</TooltipContent>
</Tooltip>
</div>
</ThreadDropdown>
</div>
);
}