mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-07-16 16:13:10 +02:00
Add user dropdown seed-able list (#2308)
* add user dropdown seedable list * minor cleanup * fix build issue * minor type update * remove log * quick update to divider logic (squash) * tiny icon updates
This commit is contained in:
@ -1,4 +1,13 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class NavigationItem(BaseModel):
|
||||
link: str
|
||||
icon: str
|
||||
title: str
|
||||
|
||||
|
||||
class EnterpriseSettings(BaseModel):
|
||||
@ -10,6 +19,9 @@ class EnterpriseSettings(BaseModel):
|
||||
use_custom_logo: bool = False
|
||||
use_custom_logotype: bool = False
|
||||
|
||||
# custom navigation
|
||||
custom_nav_items: List[NavigationItem] = Field(default_factory=list)
|
||||
|
||||
# custom Chat components
|
||||
two_lines_for_chat_header: bool | None = None
|
||||
custom_lower_disclaimer_content: str | None = None
|
||||
|
@ -15,11 +15,20 @@ export interface Notification {
|
||||
first_shown: string;
|
||||
}
|
||||
|
||||
export interface NavigationItem {
|
||||
link: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface EnterpriseSettings {
|
||||
application_name: string | null;
|
||||
use_custom_logo: boolean;
|
||||
use_custom_logotype: boolean;
|
||||
|
||||
// custom navigation
|
||||
custom_nav_items: NavigationItem[];
|
||||
|
||||
// custom Chat components
|
||||
custom_lower_disclaimer_content: string | null;
|
||||
custom_header_content: string | null;
|
||||
|
@ -64,8 +64,6 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import GeneratingImageDisplay from "../tools/GeneratingImageDisplay";
|
||||
import RegenerateOption from "../RegenerateOption";
|
||||
import { LlmOverride } from "@/lib/hooks";
|
||||
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
|
||||
import { EmphasizedClickable } from "@/components/BasicClickable";
|
||||
import { ContinueGenerating } from "./ContinueMessage";
|
||||
|
||||
const TOOLS_WITH_CUSTOM_HANDLING = [
|
||||
@ -742,6 +740,7 @@ export const HumanMessage = ({
|
||||
<div className="xl:ml-8">
|
||||
<div className="flex flex-col mr-4">
|
||||
<FileDisplay alignBubble files={files || []} />
|
||||
|
||||
<div className="flex justify-end">
|
||||
<div className="w-full ml-8 flex w-full w-[800px] break-words">
|
||||
{isEditing ? (
|
||||
|
@ -64,6 +64,7 @@ export function WhitelabelingForm() {
|
||||
custom_popup_content: enterpriseSettings?.custom_popup_content || "",
|
||||
custom_lower_disclaimer_content:
|
||||
enterpriseSettings?.custom_lower_disclaimer_content || "",
|
||||
custom_nav_items: enterpriseSettings?.custom_nav_items || [],
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
application_name: Yup.string().nullable(),
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useContext } from "react";
|
||||
import { useState, useRef, useContext, useEffect } from "react";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
@ -15,6 +15,8 @@ import {
|
||||
UsersIcon,
|
||||
} from "./icons/icons";
|
||||
import { pageType } from "@/app/chat/sessionSidebar/types";
|
||||
import { NavigationItem } from "@/app/admin/settings/interfaces";
|
||||
import DynamicFaIcon, { preloadIcons } from "./icons/DynamicFaIcon";
|
||||
|
||||
interface DropdownOptionProps {
|
||||
href?: string;
|
||||
@ -55,6 +57,14 @@ export function UserDropdown({
|
||||
const router = useRouter();
|
||||
|
||||
const combinedSettings = useContext(SettingsContext);
|
||||
const customNavItems: NavigationItem[] =
|
||||
combinedSettings?.enterpriseSettings?.custom_nav_items || [];
|
||||
|
||||
useEffect(() => {
|
||||
const iconNames = customNavItems.map((item) => item.icon);
|
||||
preloadIcons(iconNames);
|
||||
}, [customNavItems]);
|
||||
|
||||
if (!combinedSettings) {
|
||||
return null;
|
||||
}
|
||||
@ -126,6 +136,20 @@ export function UserDropdown({
|
||||
overscroll-contain
|
||||
`}
|
||||
>
|
||||
{customNavItems.map((item, i) => (
|
||||
<DropdownOption
|
||||
key={i}
|
||||
href={item.link}
|
||||
icon={
|
||||
<DynamicFaIcon
|
||||
name={item.icon}
|
||||
className="h-4 w-4 my-auto mr-2"
|
||||
/>
|
||||
}
|
||||
label={item.title}
|
||||
/>
|
||||
))}
|
||||
|
||||
{showAdminPanel ? (
|
||||
<DropdownOption
|
||||
href="/admin/indexing/status"
|
||||
@ -142,9 +166,12 @@ export function UserDropdown({
|
||||
)
|
||||
)}
|
||||
|
||||
{showLogout && (showCuratorPanel || showAdminPanel) && (
|
||||
<div className="border-t border-border my-1" />
|
||||
)}
|
||||
{showLogout &&
|
||||
(showCuratorPanel ||
|
||||
showAdminPanel ||
|
||||
customNavItems.length > 0) && (
|
||||
<div className="border-t border-border my-1" />
|
||||
)}
|
||||
|
||||
{showLogout && (
|
||||
<DropdownOption
|
||||
|
@ -74,8 +74,8 @@ export function AdminSidebar({ collections }: { collections: Collection[] }) {
|
||||
</div>
|
||||
<div className="flex w-full justify-center">
|
||||
<Link href={"/chat"}>
|
||||
<button className="text-sm block w-52 py-2.5 flex px-2 text-left bg-background-200 hover:bg-background-200/80 cursor-pointer rounded">
|
||||
<BackIcon size={20} />
|
||||
<button className="text-sm flex items-center block w-52 py-2.5 flex px-2 text-left bg-background-200 hover:bg-background-200/80 cursor-pointer rounded">
|
||||
<BackIcon className="my-auto" size={18} />
|
||||
<p className="ml-1 break-words line-clamp-2 ellipsis leading-none">
|
||||
Back to{" "}
|
||||
{combinedSettings.enterpriseSettings?.application_name ||
|
||||
|
44
web/src/components/icons/DynamicFaIcon.tsx
Normal file
44
web/src/components/icons/DynamicFaIcon.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { IconBaseProps, IconType } from "react-icons";
|
||||
import { FaQuestion } from "react-icons/fa";
|
||||
|
||||
interface DynamicIconProps extends IconBaseProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Renders a FontAwesome icon dynamically based on the provided name
|
||||
const DynamicFaIcon: React.FC<DynamicIconProps> = ({ name, ...props }) => {
|
||||
const IconComponent = getPreloadedIcon(name);
|
||||
return IconComponent ? (
|
||||
<IconComponent className="h-4 w-4" {...props} />
|
||||
) : (
|
||||
<FaQuestion className="h-4 w-4" {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
// Cache for storing preloaded icons
|
||||
const iconCache: Record<string, IconType> = {};
|
||||
|
||||
// Preloads icons asynchronously and stores them in the cache
|
||||
export async function preloadIcons(iconNames: string[]): Promise<void> {
|
||||
const promises = iconNames.map(async (name) => {
|
||||
try {
|
||||
const iconModule = await import("react-icons/fa");
|
||||
const iconName =
|
||||
`Fa${name.charAt(0).toUpperCase() + name.slice(1)}` as keyof typeof iconModule;
|
||||
iconCache[name] = (iconModule[iconName] as IconType) || FaQuestion;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load icon: ${name}`, error);
|
||||
iconCache[name] = FaQuestion;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
// Retrieves a preloaded icon from the cache
|
||||
export function getPreloadedIcon(name: string): IconType | undefined {
|
||||
return iconCache[name] || FaQuestion;
|
||||
}
|
||||
|
||||
export default DynamicFaIcon;
|
Reference in New Issue
Block a user