From 47fbf3afb999672ffd47781a1f94e84d3b43d82a Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 14 May 2026 19:14:10 +0000 Subject: [PATCH] 4 --- frontend/src/components/AsideMenu.tsx | 2 +- frontend/src/components/AsideMenuItem.tsx | 50 +-- frontend/src/components/AsideMenuLayer.tsx | 28 +- frontend/src/components/AsideMenuList.tsx | 9 +- .../src/components/Workspace/ChatMarkdown.tsx | 20 +- .../components/Workspace/WorkspaceShell.tsx | 205 +++++---- frontend/src/css/_app.css | 2 +- frontend/src/layouts/Authenticated.tsx | 49 +- frontend/src/pages/_app.tsx | 3 + frontend/src/pages/agents/agents-list.tsx | 248 +++++----- .../conversations/conversations-list.tsx | 219 ++++----- frontend/src/pages/messages/messages-list.tsx | 229 ++++------ .../pages/permissions/permissions-list.tsx | 207 +++------ frontend/src/pages/profile.tsx | 424 ++++++++++++------ frontend/src/pages/roles/roles-list.tsx | 218 ++++----- .../pages/usage_events/usage_events-list.tsx | 240 ++++------ frontend/src/pages/users/users-list.tsx | 253 ++++++----- 17 files changed, 1180 insertions(+), 1226 deletions(-) diff --git a/frontend/src/components/AsideMenu.tsx b/frontend/src/components/AsideMenu.tsx index 11e58a3..0e878ea 100644 --- a/frontend/src/components/AsideMenu.tsx +++ b/frontend/src/components/AsideMenu.tsx @@ -19,7 +19,7 @@ export default function AsideMenu({ <> { const [isLinkActive, setIsLinkActive] = useState(false) const [isDropdownActive, setIsDropdownActive] = useState(false) - const asideMenuItemStyle = useAppSelector((state) => state.style.asideMenuItemStyle) - const asideMenuDropdownStyle = useAppSelector((state) => state.style.asideMenuDropdownStyle) - const asideMenuItemActiveStyle = useAppSelector((state) => state.style.asideMenuItemActiveStyle) - const borders = useAppSelector((state) => state.style.borders); - const activeLinkColor = useAppSelector( - (state) => state.style.activeLinkColor, - ); - const activeClassAddon = !item.color && isLinkActive ? asideMenuItemActiveStyle : '' - const { asPath, isReady } = useRouter() useEffect(() => { @@ -40,42 +29,45 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { } }, [item.href, isReady, asPath]) + const isActiveItem = isLinkActive || (Boolean(item.menu) && isDropdownActive) + const asideMenuItemInnerContents = ( <> {item.icon && ( - + )} {item.label} {item.menu && ( )} ) const componentClass = [ - 'flex items-start gap-3 cursor-pointer py-1.5', - isDropdownList ? 'px-4 text-sm' : '', - item.color - ? getButtonColor(item.color, false, true) - : `${asideMenuItemStyle}`, - isLinkActive - ? `text-black ${activeLinkColor} dark:text-white dark:bg-dark-800` - : '', + 'flex cursor-pointer items-start gap-3 rounded-[8px] border transition-colors', + isDropdownList ? 'px-3 py-2 text-[14px] font-medium' : 'px-3 py-2.5 text-[15px] font-medium', + isActiveItem + ? 'border-slate-200 bg-white dark:border-dark-600 dark:bg-dark-800' + : 'border-transparent bg-transparent hover:border-slate-200 hover:bg-slate-50 dark:hover:border-dark-700 dark:hover:bg-dark-800/60', ].join(' '); return ( -
  • - {item.withDevider &&
    } +
  • + {item.withDevider &&
    } {item.href && ( {asideMenuItemInnerContents} @@ -88,11 +80,11 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { )} {item.menu && ( )}
  • diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx index d46649f..5b39d36 100644 --- a/frontend/src/components/AsideMenuLayer.tsx +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -1,10 +1,9 @@ import React from 'react' -import { mdiLogout, mdiClose } from '@mdi/js' +import { mdiClose } from '@mdi/js' import BaseIcon from './BaseIcon' import AsideMenuList from './AsideMenuList' import { MenuAsideItem } from '../interfaces' import { useAppSelector } from '../stores/hooks' -import Link from 'next/link'; type Props = { @@ -14,9 +13,6 @@ type Props = { } export default function AsideMenuLayer({ menu, className = '', ...props }: Props) { - const corners = useAppSelector((state) => state.style.corners); - const asideStyle = useAppSelector((state) => state.style.asideStyle) - const asideBrandStyle = useAppSelector((state) => state.style.asideBrandStyle) const asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle) const darkMode = useAppSelector((state) => state.style.darkMode) @@ -29,25 +25,25 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props return ( diff --git a/frontend/src/components/AsideMenuList.tsx b/frontend/src/components/AsideMenuList.tsx index 9e33ea1..0eda604 100644 --- a/frontend/src/components/AsideMenuList.tsx +++ b/frontend/src/components/AsideMenuList.tsx @@ -17,17 +17,16 @@ export default function AsideMenuList({ menu, isDropdownList = false, className return (
      - {menu.map((item, index) => { - + {menu.map((item) => { if (!hasPermission(currentUser, item.permissions)) return null; - + return ( -
      + -
      + ) })}
    diff --git a/frontend/src/components/Workspace/ChatMarkdown.tsx b/frontend/src/components/Workspace/ChatMarkdown.tsx index dc57d3b..23dd938 100644 --- a/frontend/src/components/Workspace/ChatMarkdown.tsx +++ b/frontend/src/components/Workspace/ChatMarkdown.tsx @@ -63,7 +63,7 @@ const renderInline = (text: string, keyPrefix: string) => return ( {token.value} @@ -90,7 +90,7 @@ const renderInline = (text: string, keyPrefix: string) => return ( }); const headingClassNames: Record = { - h1: 'text-2xl font-semibold tracking-[-0.03em] text-slate-900 dark:text-white', - h2: 'text-xl font-semibold tracking-[-0.02em] text-slate-900 dark:text-white', - h3: 'text-lg font-semibold text-slate-900 dark:text-white', + h1: 'text-[1.35rem] font-semibold tracking-[-0.03em] text-slate-900 dark:text-white', + h2: 'text-[1.15rem] font-semibold tracking-[-0.02em] text-slate-900 dark:text-white', + h3: 'text-[1rem] font-semibold text-slate-900 dark:text-white', }; const isSpecialBlock = (line: string) => { @@ -147,12 +147,12 @@ export default function ChatMarkdown({ content, className = '' }: ChatMarkdownPr } blocks.push( -
    +
    {language || 'code'} {codeLines.length} lines
    -
    +          
                 {codeLines.join('\n')}
               
    , @@ -203,7 +203,7 @@ export default function ChatMarkdown({ content, className = '' }: ChatMarkdownPr blocks.push(
    {renderInline(quoteLines.join(' '), `quote-${index}`)}
    , @@ -223,7 +223,7 @@ export default function ChatMarkdown({ content, className = '' }: ChatMarkdownPr } blocks.push( -
      +
        {items.map((item, itemIndex) => (
      • {renderInline(item, `list-${index}-${itemIndex}`)} @@ -241,7 +241,7 @@ export default function ChatMarkdown({ content, className = '' }: ChatMarkdownPr } blocks.push( -

        +

        {renderInline(paragraphLines.join(' '), `paragraph-${index}`)}

        , ); diff --git a/frontend/src/components/Workspace/WorkspaceShell.tsx b/frontend/src/components/Workspace/WorkspaceShell.tsx index e2f6c14..620605a 100644 --- a/frontend/src/components/Workspace/WorkspaceShell.tsx +++ b/frontend/src/components/Workspace/WorkspaceShell.tsx @@ -15,6 +15,7 @@ import axios from 'axios'; import Link from 'next/link'; import { useRouter } from 'next/router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { toast } from 'react-toastify'; import BaseIcon from '../BaseIcon'; import { hasPermission } from '../../helpers/userPermissions'; import { useAppSelector } from '../../stores/hooks'; @@ -196,12 +197,24 @@ const formatMessageTime = (value?: string) => { return ''; } + const date = new Date(value); + const now = new Date(); + const isSameDay = + date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate(); + + if (isSameDay) { + return new Intl.DateTimeFormat('en-US', { + hour: 'numeric', + minute: '2-digit', + }).format(date); + } + return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', - hour: 'numeric', - minute: '2-digit', - }).format(new Date(value)); + }).format(date); }; const getErrorMessage = (error: unknown, fallback: string) => { @@ -222,6 +235,30 @@ const getErrorMessage = (error: unknown, fallback: string) => { return fallback; }; +function getAvatarUrl(avatar: any) { + if (!Array.isArray(avatar) || !avatar.length) { + return ''; + } + + return avatar[0]?.publicUrl || ''; +} + +function getUserInitial(currentUser: any) { + const firstName = currentUser?.firstName || ''; + const lastName = currentUser?.lastName || ''; + const initials = `${firstName.slice(0, 1)}${lastName.slice(0, 1)}`.trim(); + + if (initials) { + return initials.toUpperCase(); + } + + if (currentUser?.email) { + return currentUser.email.slice(0, 1).toUpperCase(); + } + + return 'U'; +} + const toConversationSummary = (conversation: ConversationDetail): ConversationSummary => ({ id: conversation.id, title: conversation.title, @@ -245,6 +282,8 @@ function TypingIndicator() { } type MessageBubbleProps = { + currentUserAvatarUrl: string; + currentUserInitial: string; currentUserName: string; message: WorkspaceMessage; streamingText: string; @@ -252,6 +291,8 @@ type MessageBubbleProps = { }; function MessageBubble({ + currentUserAvatarUrl, + currentUserInitial, currentUserName, message, streamingText, @@ -279,10 +320,10 @@ function MessageBubble({ : 'border border-slate-200 bg-white text-slate-900 shadow-sm'; const avatarLabel = isUser ? currentUserName : 'AI'; const metaClassName = isUser - ? 'text-[11px] uppercase tracking-[0.18em] text-white/70' + ? 'text-[9px] uppercase tracking-[0.12em] text-white/65' : isFailed - ? 'text-[11px] uppercase tracking-[0.18em] text-red-500' - : 'text-[11px] uppercase tracking-[0.18em] text-slate-400'; + ? 'text-[9px] uppercase tracking-[0.12em] text-red-500' + : 'text-[9px] uppercase tracking-[0.12em] text-slate-400'; return (
        @@ -296,7 +337,7 @@ function MessageBubble({ {isUser ? currentUserName : 'Assistant'} {statusLabel && ( )} {displayTime && ( - + {displayTime} )} @@ -317,14 +358,14 @@ function MessageBubble({ {message.pending ? ( ) : isStreaming ? ( -
        {streamingText}
        +
        {streamingText}
        ) : ( {isUser && ( -
        - {avatarLabel.slice(0, 1)} -
        + currentUserAvatarUrl ? ( +
        + {currentUserName} +
        + ) : ( +
        + {currentUserInitial} +
        + ) )}
        ); @@ -411,9 +462,18 @@ export default function WorkspaceShell() { const [streamingText, setStreamingText] = useState(''); const [hasBootstrapped, setHasBootstrapped] = useState(false); + const showSuccessToast = useCallback((message: string) => { + toast(message, { + position: 'bottom-center', + type: 'success', + }); + }, []); + const currentConversationId = typeof router.query.conversationId === 'string' ? router.query.conversationId : null; const currentUserName = currentUser?.firstName || currentUser?.email || 'You'; + const currentUserAvatarUrl = getAvatarUrl(currentUser?.avatar); + const currentUserInitial = getUserInitial(currentUser); const canAccessAdmin = hasPermission(currentUser, 'READ_USERS'); const activeConversations = useMemo( @@ -620,17 +680,14 @@ export default function WorkspaceShell() { }); applyConversationPayload(data.conversation); setIsEditingTitle(false); - setNotice({ - type: 'success', - message: 'Conversation renamed.', - }); + showSuccessToast('Conversation renamed.'); } catch (error) { setNotice({ type: 'error', message: getErrorMessage(error, 'Failed to rename the conversation.'), }); } - }, [activeConversation, applyConversationPayload, draftTitle]); + }, [activeConversation, applyConversationPayload, draftTitle, showSuccessToast]); const handleArchiveConversation = useCallback(async () => { if (!activeConversation) { @@ -647,20 +704,18 @@ export default function WorkspaceShell() { if (nextStatus === 'archived') { setShowArchived(true); } - setNotice({ - type: 'success', - message: - nextStatus === 'archived' - ? 'Conversation archived. It is now listed under Archived chats.' - : 'Conversation restored to Recent chats.', - }); + showSuccessToast( + nextStatus === 'archived' + ? 'Conversation archived. It is now listed under Archived chats.' + : 'Conversation restored to Recent chats.', + ); } catch (error) { setNotice({ type: 'error', message: getErrorMessage(error, 'Failed to update the conversation status.'), }); } - }, [activeConversation, applyConversationPayload]); + }, [activeConversation, applyConversationPayload, showSuccessToast]); const handleDeleteConversation = useCallback(async () => { if (!activeConversation) { @@ -677,10 +732,7 @@ export default function WorkspaceShell() { try { await axios.delete(`/workspace/conversations/${activeConversation.id}`); removeConversation(activeConversation.id); - setNotice({ - type: 'success', - message: 'Conversation deleted.', - }); + showSuccessToast('Conversation deleted.'); if (nextConversation) { await router.replace(`/workspace/${nextConversation.id}`); @@ -693,7 +745,7 @@ export default function WorkspaceShell() { message: getErrorMessage(error, 'Failed to delete the conversation.'), }); } - }, [activeConversation, conversations, removeConversation, router]); + }, [activeConversation, conversations, removeConversation, router, showSuccessToast]); const handleAgentChange = useCallback( async (nextAgentId: string) => { @@ -900,14 +952,15 @@ export default function WorkspaceShell() { .find((message) => message.role === 'assistant')?.id || null; return ( -
        -
        +
        +
        -
        +
        {activeConversation && !isEditingTitle && ( <> -