This commit is contained in:
Flatlogic Bot 2026-05-14 19:14:10 +00:00
parent 05382ed1be
commit 47fbf3afb9
17 changed files with 1180 additions and 1226 deletions

View File

@ -19,7 +19,7 @@ export default function AsideMenu({
<>
<AsideMenuLayer
menu={props.menu}
className={`${isAsideMobileExpanded ? 'left-0' : '-left-72 lg:left-0'} ${
className={`${isAsideMobileExpanded ? 'left-0' : '-left-64 lg:left-0'} ${
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
}`}
onAsideLgCloseClick={props.onAsideLgClose}

View File

@ -2,10 +2,8 @@ import React, { useEffect, useState } from 'react'
import { mdiMinus, mdiPlus } from '@mdi/js'
import BaseIcon from './BaseIcon'
import Link from 'next/link'
import { getButtonColor } from '../colors'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks'
import { useRouter } from 'next/router'
type Props = {
@ -17,15 +15,6 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
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 && (
<BaseIcon path={item.icon} className={`mt-0.5 flex-none ${activeClassAddon}`} size="18" />
<BaseIcon
path={item.icon}
className={`mt-0.5 flex-none ${isActiveItem ? 'text-slate-900 dark:text-white' : 'text-slate-500 dark:text-slate-300'}`}
size="18"
/>
)}
<span
className={`min-w-0 grow break-words leading-6 ${
item.menu ? '' : 'pr-2'
} ${activeClassAddon}`}
isActiveItem ? 'text-slate-900 dark:text-white' : 'text-slate-700 dark:text-slate-200'
}`}
>
{item.label}
</span>
{item.menu && (
<BaseIcon
path={isDropdownActive ? mdiMinus : mdiPlus}
className={`flex-none ${activeClassAddon}`}
w="w-8"
className={`flex-none ${isActiveItem ? 'text-slate-900 dark:text-white' : 'text-slate-400 dark:text-slate-400'}`}
size="16"
/>
)}
</>
)
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 (
<li className={'px-3 py-1.5'}>
{item.withDevider && <hr className={`${borders} mb-3`} />}
<li className={isDropdownList ? 'py-0.5' : 'py-1'}>
{item.withDevider && <hr className="my-3 border-slate-200 dark:border-dark-700" />}
{item.href && (
<Link href={item.href} target={item.target} className={componentClass}>
{asideMenuItemInnerContents}
@ -88,11 +80,11 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
)}
{item.menu && (
<AsideMenuList
menu={item.menu}
className={`${asideMenuDropdownStyle} ${
isDropdownActive ? 'block dark:bg-slate-800/50' : 'hidden'
className={`mt-2 rounded-[10px] border border-slate-200 bg-slate-50/80 p-2 dark:border-dark-700 dark:bg-dark-800/40 ${
isDropdownActive ? 'block' : 'hidden'
}`}
isDropdownList
menu={item.menu}
/>
)}
</li>

View File

@ -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 (
<aside
id='asideMenu'
className={`${className} zzz lg:py-2 lg:pl-2 w-72 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
className={`${className} fixed top-12 z-40 flex h-[calc(100vh-3rem)] w-64 overflow-hidden px-2 pb-2 transition-position`}
>
<div
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
className="flex flex-1 flex-col overflow-hidden rounded-[10px] border border-slate-200 bg-white shadow-none dark:border-dark-700 dark:bg-dark-900"
>
<div
className={`flex flex-row h-12 items-center justify-between ${asideBrandStyle}`}
className="flex h-11 items-center justify-between border-b border-slate-200 px-4 dark:border-dark-700"
>
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
<b className="font-black">AI Chat Workspace</b>
<div className="min-w-0 flex-1">
<span className="block truncate text-[14px] font-semibold tracking-[-0.02em] text-slate-900 dark:text-white">
AI Chat Workspace
</span>
</div>
<button
className="hidden lg:inline-block xl:hidden p-2.5"
className="hidden rounded-[8px] border border-slate-200 bg-white p-2 text-slate-500 lg:inline-flex xl:hidden dark:border-dark-700 dark:bg-dark-900 dark:text-slate-300"
onClick={handleAsideLgCloseClick}
type="button"
>
<BaseIcon path={mdiClose} />
<BaseIcon path={mdiClose} size={16} />
</button>
</div>
<div
@ -55,7 +51,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
}`}
>
<AsideMenuList menu={menu} />
<AsideMenuList className="px-2 py-2.5" menu={menu} />
</div>
</div>
</aside>

View File

@ -17,17 +17,16 @@ export default function AsideMenuList({ menu, isDropdownList = false, className
return (
<ul className={className}>
{menu.map((item, index) => {
{menu.map((item) => {
if (!hasPermission(currentUser, item.permissions)) return null;
return (
<div key={index}>
<React.Fragment key={item.href || item.label}>
<AsideMenuItem
item={item}
isDropdownList={isDropdownList}
/>
</div>
</React.Fragment>
)
})}
</ul>

View File

@ -63,7 +63,7 @@ const renderInline = (text: string, keyPrefix: string) =>
return (
<code
key={key}
className="rounded-md border border-slate-200 bg-slate-100 px-1.5 py-0.5 font-mono text-[0.95em] text-slate-700 dark:border-white/10 dark:bg-white/10 dark:text-[#CDE4FF]"
className="rounded-[6px] border border-slate-200 bg-slate-100 px-1.5 py-0.5 font-mono text-[0.95em] text-slate-700 dark:border-white/10 dark:bg-white/10 dark:text-[#CDE4FF]"
>
{token.value}
</code>
@ -90,7 +90,7 @@ const renderInline = (text: string, keyPrefix: string) =>
return (
<a
key={key}
className="text-sky-600 underline underline-offset-4 transition hover:text-sky-700 dark:text-[#7DD3FC] dark:hover:text-[#BAE6FD]"
className="text-sky-600 underline underline-offset-4 dark:text-[#7DD3FC]"
href={token.href}
rel="noreferrer"
target="_blank"
@ -104,9 +104,9 @@ const renderInline = (text: string, keyPrefix: string) =>
});
const headingClassNames: Record<string, string> = {
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(
<div key={`code-${index}`} className="overflow-hidden rounded-2xl border border-slate-200 bg-slate-950 dark:border-white/10 dark:bg-[#050816]">
<div key={`code-${index}`} className="overflow-hidden rounded-[10px] border border-slate-200 bg-slate-950 dark:border-white/10 dark:bg-[#050816]">
<div className="flex items-center justify-between border-b border-slate-800 bg-white/5 px-4 py-2 text-xs uppercase tracking-[0.24em] text-slate-400">
<span>{language || 'code'}</span>
<span>{codeLines.length} lines</span>
</div>
<pre className="overflow-x-auto px-4 py-4 text-sm leading-6 text-[#E2E8F0]">
<pre className="overflow-x-auto px-4 py-4 text-[13px] leading-6 text-[#E2E8F0]">
<code>{codeLines.join('\n')}</code>
</pre>
</div>,
@ -203,7 +203,7 @@ export default function ChatMarkdown({ content, className = '' }: ChatMarkdownPr
blocks.push(
<blockquote
key={`quote-${index}`}
className="rounded-2xl border border-sky-200 bg-sky-50 px-4 py-3 text-sm text-sky-900 dark:border-[#38BDF8]/20 dark:bg-[#0F172A] dark:text-[#CFE8FF]"
className="rounded-[10px] border border-sky-200 bg-sky-50 px-4 py-3 text-[13px] text-sky-900 dark:border-[#38BDF8]/20 dark:bg-[#0F172A] dark:text-[#CFE8FF]"
>
{renderInline(quoteLines.join(' '), `quote-${index}`)}
</blockquote>,
@ -223,7 +223,7 @@ export default function ChatMarkdown({ content, className = '' }: ChatMarkdownPr
}
blocks.push(
<ul key={`list-${index}`} className="space-y-2 pl-5 text-slate-100">
<ul key={`list-${index}`} className="space-y-1.5 pl-5 text-[13px] text-slate-700 dark:text-slate-100/95">
{items.map((item, itemIndex) => (
<li key={`item-${index}-${itemIndex}`} className="list-disc">
{renderInline(item, `list-${index}-${itemIndex}`)}
@ -241,7 +241,7 @@ export default function ChatMarkdown({ content, className = '' }: ChatMarkdownPr
}
blocks.push(
<p key={`paragraph-${index}`} className="text-[15px] leading-7 text-slate-700 dark:text-slate-100/95">
<p key={`paragraph-${index}`} className="text-[13px] leading-6 text-slate-700 dark:text-slate-100/95">
{renderInline(paragraphLines.join(' '), `paragraph-${index}`)}
</p>,
);

View File

@ -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 (
<div className={`flex gap-2.5 ${isUser ? 'justify-end' : 'justify-start'}`}>
@ -296,7 +337,7 @@ function MessageBubble({
<span>{isUser ? currentUserName : 'Assistant'}</span>
{statusLabel && (
<span
className={`rounded-md px-2 py-1 ${
className={`rounded-md px-1.5 py-0.5 text-[9px] ${
isUser
? 'bg-white/10 text-white/80'
: isFailed
@ -308,7 +349,7 @@ function MessageBubble({
</span>
)}
{displayTime && (
<span className={isUser ? 'text-white/50' : isFailed ? 'text-red-400' : 'text-slate-400'}>
<span className={isUser ? 'text-white/45' : isFailed ? 'text-red-400' : 'text-slate-400'}>
{displayTime}
</span>
)}
@ -317,14 +358,14 @@ function MessageBubble({
{message.pending ? (
<TypingIndicator />
) : isStreaming ? (
<pre className={`whitespace-pre-wrap font-sans text-[13px] leading-5 ${isUser ? 'text-white' : 'text-slate-700'}`}>{streamingText}</pre>
<pre className={`whitespace-pre-wrap font-sans text-[12px] leading-5 ${isUser ? 'text-white' : 'text-slate-700'}`}>{streamingText}</pre>
) : (
<ChatMarkdown
className={
isUser
? 'text-white [&_a]:text-white [&_a]:decoration-white/60 [&_blockquote]:border-white/20 [&_blockquote]:bg-white/10 [&_blockquote]:text-white [&_code]:border-white/10 [&_code]:bg-white/15 [&_code]:text-white [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_p]:text-white [&_strong]:text-white'
? 'text-white [&_a]:text-white [&_a]:decoration-white/60 [&_blockquote]:border-white/20 [&_blockquote]:bg-white/10 [&_blockquote]:text-white [&_code]:border-white/10 [&_code]:bg-white/15 [&_code]:text-white [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_li]:text-white [&_p]:text-white [&_strong]:text-white [&_ul]:text-white'
: isFailed
? 'text-red-900 [&_a]:text-red-700 [&_blockquote]:border-red-200 [&_blockquote]:bg-red-100/70 [&_blockquote]:text-red-900 [&_code]:border-red-200 [&_code]:bg-red-100 [&_code]:text-red-900 [&_h1]:text-red-900 [&_h2]:text-red-900 [&_h3]:text-red-900 [&_p]:text-red-900 [&_strong]:text-red-900'
? 'text-red-900 [&_a]:text-red-700 [&_blockquote]:border-red-200 [&_blockquote]:bg-red-100/70 [&_blockquote]:text-red-900 [&_code]:border-red-200 [&_code]:bg-red-100 [&_code]:text-red-900 [&_h1]:text-red-900 [&_h2]:text-red-900 [&_h3]:text-red-900 [&_li]:text-red-900 [&_p]:text-red-900 [&_strong]:text-red-900 [&_ul]:text-red-900'
: ''
}
content={rawContent}
@ -332,9 +373,19 @@ function MessageBubble({
)}
</div>
{isUser && (
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-slate-900 bg-slate-900 text-[10px] font-semibold text-white">
{avatarLabel.slice(0, 1)}
</div>
currentUserAvatarUrl ? (
<div className="flex h-7 w-7 shrink-0 overflow-hidden rounded-md border border-slate-200 bg-slate-100">
<img
alt={currentUserName}
className="h-full w-full object-cover"
src={currentUserAvatarUrl}
/>
</div>
) : (
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-slate-900 bg-slate-900 text-[10px] font-semibold text-white">
{currentUserInitial}
</div>
)
)}
</div>
);
@ -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 (
<section
className={`${activeConversation ? 'h-[calc(100vh-3rem)]' : 'min-h-[calc(100vh-3rem)]'} bg-[#f5f5f7] px-0 sm:px-1.5 sm:pb-1.5 sm:pt-1.5`}
>
<div
className={`grid border border-slate-200 bg-white shadow-[0_16px_48px_-40px_rgba(15,23,42,0.18)] sm:rounded-[10px] lg:grid-cols-[220px_minmax(0,1fr)] ${
activeConversation ? 'h-full min-h-0 overflow-hidden' : 'min-h-[calc(100vh-3rem)]'
}`}
<>
<section
className={`${activeConversation ? 'h-[calc(100vh-3rem)]' : 'min-h-[calc(100vh-3rem)]'} bg-[#f5f5f7] px-0 sm:px-1.5 sm:pb-1.5`}
>
<div
className={`grid border border-slate-200 bg-white shadow-[0_16px_48px_-40px_rgba(15,23,42,0.18)] sm:rounded-[10px] lg:grid-cols-[220px_minmax(0,1fr)] ${
activeConversation ? 'h-full min-h-0 overflow-hidden' : 'min-h-[calc(100vh-3rem)]'
}`}
>
<aside
className={`absolute inset-y-0 left-0 z-20 flex w-full max-w-[220px] flex-col border-r border-slate-200 bg-[#fafafa] transition duration-200 lg:static lg:translate-x-0 ${
isSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
@ -1063,13 +1116,13 @@ export default function WorkspaceShell() {
</>
)}
</div>
<div className="flex flex-wrap items-center gap-1.5">
<div className="flex flex-wrap items-end gap-1.5">
{activeConversation && !isEditingTitle && (
<>
<label className="text-[11px] text-slate-600">
<span className="mb-1 block text-[9px] uppercase tracking-[0.12em] text-slate-400">Agent</span>
<label className="flex min-w-[176px] flex-col gap-1 text-[11px] text-slate-600">
<span className="block text-[9px] uppercase tracking-[0.12em] text-slate-400">Agent</span>
<select
className="min-w-[156px] bg-white py-1.5 text-[11px]"
className="h-10 min-w-[176px] bg-white px-3 py-0 text-[12px]"
onChange={(event) => void handleAgentChange(event.target.value)}
value={agentSelectionValue}
>
@ -1081,14 +1134,14 @@ export default function WorkspaceShell() {
</select>
</label>
<button
className="inline-flex h-7 w-7 items-center justify-center rounded-[8px] border border-slate-200 bg-white text-slate-500"
className="inline-flex h-10 w-10 items-center justify-center rounded-[8px] border border-slate-200 bg-white text-slate-500"
onClick={() => setIsEditingTitle(true)}
type="button"
>
<BaseIcon path={mdiPencilOutline} size={16} />
</button>
<button
className="inline-flex items-center gap-1 rounded-[8px] border border-slate-200 bg-white px-2.5 py-1.5 text-[11px] font-medium text-slate-700"
className="inline-flex h-10 items-center gap-1 rounded-[8px] border border-slate-200 bg-white px-3 text-[12px] font-medium text-slate-700"
onClick={() => void handleArchiveConversation()}
type="button"
>
@ -1096,7 +1149,7 @@ export default function WorkspaceShell() {
{activeConversation.status === 'archived' ? 'Restore to recent' : 'Move to archive'}
</button>
<button
className="inline-flex items-center gap-1 rounded-[8px] border border-red-200 bg-white px-2.5 py-1.5 text-[11px] font-medium text-red-600"
className="inline-flex h-10 items-center gap-1 rounded-[8px] border border-red-200 bg-white px-3 text-[12px] font-medium text-red-600"
onClick={() => void handleDeleteConversation()}
type="button"
>
@ -1106,7 +1159,7 @@ export default function WorkspaceShell() {
</>
)}
<button
className="inline-flex items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-2.5 py-1.5 text-[12px] font-medium text-slate-700 lg:hidden"
className="inline-flex h-10 items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-3 text-[12px] font-medium text-slate-700 lg:hidden"
onClick={() => setIsSidebarOpen(true)}
type="button"
>
@ -1114,7 +1167,7 @@ export default function WorkspaceShell() {
History
</button>
<Link
className="inline-flex items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-2.5 py-1.5 text-[12px] font-medium text-slate-700"
className="inline-flex h-10 items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-3 text-[12px] font-medium text-slate-700"
href="/settings"
>
<BaseIcon path={mdiCogOutline} size={16} />
@ -1122,7 +1175,7 @@ export default function WorkspaceShell() {
</Link>
{canAccessAdmin && (
<Link
className="inline-flex items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-2.5 py-1.5 text-[12px] font-medium text-slate-700"
className="inline-flex h-10 items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-3 text-[12px] font-medium text-slate-700"
href="/dashboard"
>
<BaseIcon path={mdiOpenInNew} size={16} />
@ -1182,6 +1235,8 @@ export default function WorkspaceShell() {
return (
<div className="space-y-1.5" key={message.id}>
<MessageBubble
currentUserAvatarUrl={currentUserAvatarUrl}
currentUserInitial={currentUserInitial}
currentUserName={currentUserName}
message={message}
streamingMessageId={streamingMessageId}
@ -1235,20 +1290,6 @@ export default function WorkspaceShell() {
<div className="border-t border-slate-200 bg-white px-3.5 py-2 md:px-4">
<div className="rounded-[8px] border border-slate-200 bg-white p-2.5">
<div className="mb-2 flex flex-col gap-1.5 lg:flex-row lg:items-center lg:justify-between">
<div>
<p className="text-[9px] uppercase tracking-[0.14em] text-slate-400">Composer</p>
<p className="mt-0.5 text-[11px] text-slate-500">
{activeConversation?.status === 'archived'
? 'Sending a message will reopen this archived conversation.'
: 'Use Enter to send and Shift+Enter for a new line.'}
</p>
</div>
<div className="rounded-md border border-slate-200 bg-slate-50 px-2.5 py-1 text-[11px] text-slate-600">
{activeConversation?.agent?.name || emptyStateAgent?.name || 'Workspace assistant'}
</div>
</div>
<div className="grid gap-2 lg:grid-cols-[minmax(0,1fr)_165px]">
<textarea
className="min-h-[62px] w-full resize-none rounded-[10px] border border-slate-200 bg-slate-50 px-3 py-2 text-[13px] leading-5 text-slate-900 outline-none placeholder:text-slate-400 focus:border-slate-900"
@ -1291,9 +1332,6 @@ export default function WorkspaceShell() {
{sending ? 'Sending…' : 'Send message'}
<BaseIcon path={mdiArrowRight} size={18} />
</button>
<p className="text-[10px] leading-4 text-slate-400">
Saved automatically. Enter sends, Shift+Enter adds a new line.
</p>
</div>
</div>
</div>
@ -1301,26 +1339,26 @@ export default function WorkspaceShell() {
</div>
</>
) : (
<div className="bg-[#fcfcfd] px-5 py-6 md:px-7 md:py-7">
<div className="mx-auto flex w-full max-w-4xl flex-col items-start overflow-hidden rounded-[14px] border border-slate-200 bg-white p-5 md:p-6">
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-100 text-slate-600">
<div className="bg-[#fcfcfd] px-4 py-4 md:px-5 md:py-5">
<div className="mx-auto flex w-full max-w-[72rem] flex-col items-start overflow-hidden rounded-[12px] border border-slate-200 bg-white p-4 md:p-5">
<div className="mb-3 flex h-11 w-11 items-center justify-center rounded-[9px] border border-slate-200 bg-slate-100 text-slate-600">
<BaseIcon path={mdiChatOutline} size={22} />
</div>
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
New chat
</p>
<h2 className="mt-2 text-[2rem] font-semibold tracking-[-0.04em] text-slate-900 md:text-[2.25rem]">
<h2 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900 md:text-[1.95rem]">
Start with one clear ask.
</h2>
<p className="mt-3 max-w-2xl text-sm leading-6 text-slate-500">
<p className="mt-2.5 max-w-3xl text-sm leading-6 text-slate-500">
Choose an agent, use a starter tailored to that agent, or type your own prompt
below. The first send creates the conversation and adds it to Recent automatically.
</p>
<div className="mt-5 grid w-full gap-2.5 md:grid-cols-2">
<div className="mt-4 grid w-full gap-2.5 md:grid-cols-2">
{emptyStateConfig.prompts.map((prompt) => (
<button
className="rounded-[10px] border border-slate-200 bg-slate-50 px-4 py-3 text-left text-sm leading-6 text-slate-700"
className="rounded-[10px] border border-slate-200 bg-slate-50 px-4 py-2.5 text-left text-sm leading-6 text-slate-700"
key={prompt}
onClick={() => {
setComposer(prompt);
@ -1333,16 +1371,16 @@ export default function WorkspaceShell() {
))}
</div>
<div className="mt-5 grid w-full gap-4 rounded-[12px] border border-slate-200 bg-slate-50 p-4 lg:grid-cols-[minmax(0,1fr)_220px]">
<div className="mt-4 grid w-full gap-3.5 rounded-[12px] border border-slate-200 bg-slate-50 p-3.5 lg:grid-cols-[minmax(0,1fr)_220px]">
<div className="min-w-0">
<p className="text-[11px] uppercase tracking-[0.24em] text-slate-400">Selected agent</p>
<p className="mt-3 text-base font-medium text-slate-900">
<p className="mt-2.5 text-[15px] font-medium text-slate-900">
{emptyStateAgent?.name || 'Workspace assistant'}
</p>
<p className="mt-1.5 max-w-2xl text-sm leading-6 text-slate-500">
<p className="mt-1 max-w-2xl text-sm leading-6 text-slate-500">
{emptyStateAgent?.description || emptyStateConfig.helper}
</p>
<div className="mt-3 flex flex-wrap gap-2">
<div className="mt-2.5 flex flex-wrap gap-2">
{emptyStateConfig.bestFor.map((item) => (
<span
className="inline-flex rounded-[8px] border border-slate-200 bg-white px-2 py-1 text-[10px] text-slate-600"
@ -1354,7 +1392,7 @@ export default function WorkspaceShell() {
</div>
</div>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2.5">
<label className="text-sm text-slate-600">
<span className="mb-2 block text-[10px] uppercase tracking-[0.24em] text-slate-400">
Start with agent
@ -1372,7 +1410,7 @@ export default function WorkspaceShell() {
</select>
</label>
<button
className="inline-flex items-center justify-center gap-2 rounded-[10px] bg-slate-900 px-4 py-3 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-60"
className="inline-flex items-center justify-center gap-2 rounded-[10px] bg-slate-900 px-4 py-2.5 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-60"
disabled={!composer.trim() || sending || loadingBootstrap}
onClick={() => void handleSendMessage()}
type="button"
@ -1383,13 +1421,13 @@ export default function WorkspaceShell() {
</div>
</div>
<div className="mt-4 w-full rounded-[12px] border border-slate-200 bg-slate-50 p-4">
<div className="mt-3.5 w-full rounded-[12px] border border-slate-200 bg-slate-50 p-3.5">
<p className="mb-2 text-[11px] uppercase tracking-[0.24em] text-slate-400">First message</p>
<p className="mb-3 text-[12px] leading-5 text-slate-500">
<p className="mb-2.5 text-[12px] leading-5 text-slate-500">
Press Enter to start. Use Shift+Enter for a new line.
</p>
<textarea
className="min-h-[120px] w-full resize-none rounded-[10px] border border-slate-200 bg-white px-4 py-3.5 text-[15px] leading-7 text-slate-900 outline-none placeholder:text-slate-400 focus:border-slate-900"
className="min-h-[104px] w-full resize-none rounded-[10px] border border-slate-200 bg-white px-4 py-3 text-[14px] leading-6 text-slate-900 outline-none placeholder:text-slate-400 focus:border-slate-900"
onChange={(event) => setComposer(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
@ -1408,6 +1446,7 @@ export default function WorkspaceShell() {
</div>
</div>
</div>
</section>
</section>
</>
);
}

View File

@ -3,7 +3,7 @@ html {
}
body {
@apply pt-12 xl:pl-72 h-full;
@apply pt-12 xl:pl-64 h-full;
}
#app {

View File

@ -4,7 +4,6 @@ import {
mdiForwardburger,
mdiBackburger,
mdiMenu,
mdiAccountCircleOutline,
mdiChevronDown,
mdiCogOutline,
mdiLogout,
@ -31,6 +30,30 @@ type Props = {
}
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';
}
export default function LayoutAuthenticated({
children,
@ -79,6 +102,8 @@ export default function LayoutAuthenticated({
const [isAsideLgActive, setIsAsideLgActive] = useState(false)
const [isWorkspaceAccountMenuOpen, setIsWorkspaceAccountMenuOpen] = useState(false)
const workspaceAccountMenuButtonRef = useRef(null)
const currentUserAvatarUrl = getAvatarUrl(currentUser?.avatar)
const currentUserInitial = getUserInitial(currentUser)
useEffect(() => {
const handleRouteChangeStart = () => {
@ -97,8 +122,8 @@ export default function LayoutAuthenticated({
}, [router.events, dispatch])
const layoutAsidePadding = isWorkspaceRoute ? '' : 'xl:pl-72'
const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''
const layoutAsidePadding = isWorkspaceRoute ? '' : 'xl:pl-64'
const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-64 lg:ml-0' : ''
return (
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
@ -129,7 +154,7 @@ export default function LayoutAuthenticated({
</>
)}
<Link
className="truncate text-[11px] font-semibold uppercase tracking-[0.28em] text-slate-500 dark:text-slate-300"
className="truncate text-[14px] font-semibold tracking-[-0.02em] text-slate-700 dark:text-slate-100"
href="/workspace"
>
AI Chat Workspace
@ -137,12 +162,24 @@ export default function LayoutAuthenticated({
</div>
<div className="relative">
<button
className="inline-flex items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-2.5 py-1 text-[11px] text-slate-500"
className="inline-flex items-center gap-2 rounded-[8px] border border-slate-200 bg-white px-3 py-1.5 text-[12px] font-medium text-slate-700"
onClick={() => setIsWorkspaceAccountMenuOpen((previous) => !previous)}
ref={workspaceAccountMenuButtonRef}
type="button"
>
<BaseIcon path={mdiAccountCircleOutline} size="18" />
{currentUserAvatarUrl ? (
<span className="flex h-6 w-6 shrink-0 overflow-hidden rounded-full border border-slate-200 bg-slate-100">
<img
alt={currentUser?.email || 'Workspace user'}
className="h-full w-full object-cover"
src={currentUserAvatarUrl}
/>
</span>
) : (
<span className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-slate-100 text-[10px] font-semibold text-slate-600">
{currentUserInitial}
</span>
)}
<span className="hidden max-w-[220px] truncate md:inline">
{currentUser?.email || 'Workspace user'}
</span>

View File

@ -6,6 +6,8 @@ import Head from 'next/head';
import { store } from '../stores/store';
import { Provider } from 'react-redux';
import '../css/main.css';
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from 'react-toastify';
import axios from 'axios';
import { baseURLApi } from '../config';
import { useRouter } from 'next/router';
@ -185,6 +187,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<ErrorBoundary>
<Component {...pageProps} />
</ErrorBoundary>
<ToastContainer position="bottom-center" />
<IntroGuide
steps={steps}
stepsName={stepName}

View File

@ -1,87 +1,88 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import Head from 'next/head';
import Link from 'next/link';
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TableAgents from '../../components/Agents/TableAgents'
import BaseButton from '../../components/BaseButton'
import axios from "axios";
import Link from "next/link";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import CardBoxModal from "../../components/CardBoxModal";
import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/agents/agentsSlice';
import React, { ReactElement, useState } from 'react';
import axios from 'axios';
import CardBox from '../../components/CardBox';
import CardBoxModal from '../../components/CardBoxModal';
import DragDropFilePicker from '../../components/DragDropFilePicker';
import TableAgents from '../../components/Agents/TableAgents';
import SectionMain from '../../components/SectionMain';
import { getPageTitle } from '../../config';
import { hasPermission } from '../../helpers/userPermissions';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { setRefetch, uploadCsv } from '../../stores/agents/agentsSlice';
import {hasPermission} from "../../helpers/userPermissions";
const primaryActionClassName =
'inline-flex items-center justify-center rounded-[8px] bg-slate-900 px-4 py-2 text-sm font-medium text-white';
const secondaryActionClassName =
'inline-flex items-center justify-center rounded-[8px] border border-slate-200 bg-white px-3.5 py-2 text-sm font-medium text-slate-700';
const AgentsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'Name', title: 'name'},{label: 'Description', title: 'description'},{label: 'Model', title: 'model'},{label: 'Systemprompt', title: 'system_prompt'},{label: 'MetadataJSON', title: 'metadata_json'},
{label: 'Maxoutputtokens', title: 'max_output_tokens', number: 'true'},
{label: 'Temperature', title: 'temperature', number: 'true'},
const [filters] = useState([
{ label: 'Name', title: 'name' },
{ label: 'Description', title: 'description' },
{ label: 'Model', title: 'model' },
{ label: 'Systemprompt', title: 'system_prompt' },
{ label: 'MetadataJSON', title: 'metadata_json' },
{ label: 'Maxoutputtokens', title: 'max_output_tokens', number: 'true' },
{ label: 'Temperature', title: 'temperature', number: 'true' },
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_AGENTS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_AGENTS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: filters[0].title,
},
};
const getAgentsCSV = async () => {
const response = await axios({url: '/agents?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'agentsCSV.csv'
link.click()
};
setFilterItems([...filterItems, newItem]);
};
const onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const getAgentsCSV = async () => {
const response = await axios({
url: '/agents?filetype=csv',
method: 'GET',
responseType: 'blob',
});
const type = response.headers['content-type'];
const blob = new Blob([response.data], { type });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'agentsCSV.csv';
link.click();
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
const onModalConfirm = async () => {
if (!csvFile) {
return;
}
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
return (
<>
@ -89,74 +90,77 @@ const AgentsTablesPage = () => {
<title>{getPageTitle('Agents')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Agents" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/agents/agents-new'} color='info' label='New Item'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getAgentsCSV} />
<div className="mb-5 rounded-[10px] border border-slate-200 bg-white px-5 py-5">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="min-w-0">
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
Admin
</p>
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
Agents
</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
Manage assistant presets, models, and prompt behavior used across the workspace.
</p>
</div>
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
<Link className={primaryActionClassName} href="/agents/agents-new">
New agent
</Link>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className="mb-6" hasTable>
</div>
</div>
<div className="mb-4 flex flex-wrap items-center gap-2 rounded-[10px] border border-slate-200 bg-slate-50 px-3.5 py-3">
<button className={secondaryActionClassName} onClick={addFilter} type="button">
Add filter
</button>
<button
className={secondaryActionClassName}
onClick={() => void getAgentsCSV()}
type="button"
>
Download CSV
</button>
{hasCreatePermission && (
<button
className={secondaryActionClassName}
onClick={() => setIsModalActive(true)}
type="button"
>
Upload CSV
</button>
)}
<div className="ml-auto flex items-center">
<div id="delete-rows-button" />
</div>
</div>
<CardBox className="mb-6 rounded-[10px] border border-slate-200 bg-white shadow-none" hasTable>
<TableAgents
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
setFilterItems={setFilterItems}
showGrid={false}
/>
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
buttonColor="info"
buttonLabel="Confirm"
isActive={isModalActive}
onCancel={onModalCancel}
onConfirm={onModalConfirm}
title="Upload CSV"
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
<DragDropFilePicker file={csvFile} formats=".csv" setFile={setCsvFile} />
</CardBoxModal>
</>
)
}
);
};
AgentsTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_AGENTS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission="READ_AGENTS">{page}</LayoutAuthenticated>;
};
export default AgentsTablesPage
export default AgentsTablesPage;

View File

@ -1,95 +1,57 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import Head from 'next/head';
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TableConversations from '../../components/Conversations/TableConversations'
import BaseButton from '../../components/BaseButton'
import axios from "axios";
import Link from "next/link";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import CardBoxModal from "../../components/CardBoxModal";
import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/conversations/conversationsSlice';
import {hasPermission} from "../../helpers/userPermissions";
import React, { ReactElement, useState } from 'react';
import axios from 'axios';
import CardBox from '../../components/CardBox';
import TableConversations from '../../components/Conversations/TableConversations';
import SectionMain from '../../components/SectionMain';
import { getPageTitle } from '../../config';
import LayoutAuthenticated from '../../layouts/Authenticated';
const secondaryActionClassName =
'inline-flex items-center justify-center rounded-[8px] border border-slate-200 bg-white px-3.5 py-2 text-sm font-medium text-slate-700';
const ConversationsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'Title', title: 'title'},{label: 'Summary', title: 'summary'},{label: 'ClientcontextJSON', title: 'client_context_json'},
{label: 'Lastmessageat', title: 'last_message_at', date: 'true'},
{label: 'User', title: 'user'},
{label: 'Agent', title: 'agent'},
{label: 'Status', title: 'status', type: 'enum', options: ['active','archived','deleted']},
const [filters] = useState([
{ label: 'Title', title: 'title' },
{ label: 'Summary', title: 'summary' },
{ label: 'ClientcontextJSON', title: 'client_context_json' },
{ label: 'Lastmessageat', title: 'last_message_at', date: 'true' },
{ label: 'User', title: 'user' },
{ label: 'Agent', title: 'agent' },
{ label: 'Status', title: 'status', type: 'enum', options: ['active', 'archived', 'deleted'] },
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_CONVERSATIONS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: filters[0].title,
},
};
const getConversationsCSV = async () => {
const response = await axios({url: '/conversations?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'conversationsCSV.csv'
link.click()
};
setFilterItems([...filterItems, newItem]);
};
const onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
const getConversationsCSV = async () => {
const response = await axios({
url: '/conversations?filetype=csv',
method: 'GET',
responseType: 'blob',
});
const type = response.headers['content-type'];
const blob = new Blob([response.data], { type });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'conversationsCSV.csv';
link.click();
};
return (
<>
@ -97,74 +59,47 @@ const ConversationsTablesPage = () => {
<title>{getPageTitle('Conversations')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Conversations" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/conversations/conversations-new'} color='info' label='New Item'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getConversationsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className="mb-6" hasTable>
<div className="mb-5 rounded-[10px] border border-slate-200 bg-white px-5 py-5">
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">Admin</p>
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
Conversations
</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
Review active and archived threads, inspect ownership, and follow how the workspace is being used.
</p>
</div>
<div className="mb-4 flex flex-wrap items-center gap-2 rounded-[10px] border border-slate-200 bg-slate-50 px-3.5 py-3">
<button className={secondaryActionClassName} onClick={addFilter} type="button">
Add filter
</button>
<button
className={secondaryActionClassName}
onClick={() => void getConversationsCSV()}
type="button"
>
Download CSV
</button>
<div className="ml-auto flex items-center">
<div id="delete-rows-button" />
</div>
</div>
<CardBox className="mb-6 rounded-[10px] border border-slate-200 bg-white shadow-none" hasTable>
<TableConversations
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
setFilterItems={setFilterItems}
showGrid={false}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</CardBox>
</SectionMain>
</>
)
}
);
};
ConversationsTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_CONVERSATIONS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission="READ_CONVERSATIONS">{page}</LayoutAuthenticated>;
};
export default ConversationsTablesPage
export default ConversationsTablesPage;

View File

@ -1,95 +1,67 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import Head from 'next/head';
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TableMessages from '../../components/Messages/TableMessages'
import BaseButton from '../../components/BaseButton'
import axios from "axios";
import Link from "next/link";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import CardBoxModal from "../../components/CardBoxModal";
import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/messages/messagesSlice';
import {hasPermission} from "../../helpers/userPermissions";
import React, { ReactElement, useState } from 'react';
import axios from 'axios';
import CardBox from '../../components/CardBox';
import TableMessages from '../../components/Messages/TableMessages';
import SectionMain from '../../components/SectionMain';
import { getPageTitle } from '../../config';
import LayoutAuthenticated from '../../layouts/Authenticated';
const secondaryActionClassName =
'inline-flex items-center justify-center rounded-[8px] border border-slate-200 bg-white px-3.5 py-2 text-sm font-medium text-slate-700';
const MessagesTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'Content', title: 'content'},{label: 'Contentmarkdown', title: 'content_markdown'},{label: 'Toolname', title: 'tool_name'},{label: 'ToolcallJSON', title: 'tool_call_json'},{label: 'ToolresultJSON', title: 'tool_result_json'},
{label: 'Sequence', title: 'sequence', number: 'true'},
{label: 'Sentat', title: 'sent_at', date: 'true'},{label: 'Completedat', title: 'completed_at', date: 'true'},
{label: 'Conversation', title: 'conversation'},
{label: 'Authoruser', title: 'author_user'},
{label: 'Role', title: 'role', type: 'enum', options: ['user','assistant','system','tool']},{label: 'Deliverystatus', title: 'delivery_status', type: 'enum', options: ['draft','sent','streaming','completed','failed']},
const [filters] = useState([
{ label: 'Content', title: 'content' },
{ label: 'Contentmarkdown', title: 'content_markdown' },
{ label: 'Toolname', title: 'tool_name' },
{ label: 'ToolcallJSON', title: 'tool_call_json' },
{ label: 'ToolresultJSON', title: 'tool_result_json' },
{ label: 'Sequence', title: 'sequence', number: 'true' },
{ label: 'Sentat', title: 'sent_at', date: 'true' },
{ label: 'Completedat', title: 'completed_at', date: 'true' },
{ label: 'Conversation', title: 'conversation' },
{ label: 'Authoruser', title: 'author_user' },
{ label: 'Role', title: 'role', type: 'enum', options: ['user', 'assistant', 'system', 'tool'] },
{
label: 'Deliverystatus',
title: 'delivery_status',
type: 'enum',
options: ['draft', 'sent', 'streaming', 'completed', 'failed'],
},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_MESSAGES');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: filters[0].title,
},
};
const getMessagesCSV = async () => {
const response = await axios({url: '/messages?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'messagesCSV.csv'
link.click()
};
setFilterItems([...filterItems, newItem]);
};
const onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
const getMessagesCSV = async () => {
const response = await axios({
url: '/messages?filetype=csv',
method: 'GET',
responseType: 'blob',
});
const type = response.headers['content-type'];
const blob = new Blob([response.data], { type });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'messagesCSV.csv';
link.click();
};
return (
<>
@ -97,74 +69,47 @@ const MessagesTablesPage = () => {
<title>{getPageTitle('Messages')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Messages" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/messages/messages-new'} color='info' label='New Item'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getMessagesCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className="mb-6" hasTable>
<div className="mb-5 rounded-[10px] border border-slate-200 bg-white px-5 py-5">
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">Admin</p>
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
Messages
</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
Inspect prompts, responses, delivery states, and the raw message flow inside each conversation.
</p>
</div>
<div className="mb-4 flex flex-wrap items-center gap-2 rounded-[10px] border border-slate-200 bg-slate-50 px-3.5 py-3">
<button className={secondaryActionClassName} onClick={addFilter} type="button">
Add filter
</button>
<button
className={secondaryActionClassName}
onClick={() => void getMessagesCSV()}
type="button"
>
Download CSV
</button>
<div className="ml-auto flex items-center">
<div id="delete-rows-button" />
</div>
</div>
<CardBox className="mb-6 rounded-[10px] border border-slate-200 bg-white shadow-none" hasTable>
<TableMessages
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
setFilterItems={setFilterItems}
showGrid={false}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</CardBox>
</SectionMain>
</>
)
}
);
};
MessagesTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_MESSAGES'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission="READ_MESSAGES">{page}</LayoutAuthenticated>;
};
export default MessagesTablesPage
export default MessagesTablesPage;

View File

@ -1,87 +1,49 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import Head from 'next/head';
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TablePermissions from '../../components/Permissions/TablePermissions'
import BaseButton from '../../components/BaseButton'
import axios from "axios";
import Link from "next/link";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import CardBoxModal from "../../components/CardBoxModal";
import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/permissions/permissionsSlice';
import {hasPermission} from "../../helpers/userPermissions";
import React, { ReactElement, useState } from 'react';
import axios from 'axios';
import CardBox from '../../components/CardBox';
import TablePermissions from '../../components/Permissions/TablePermissions';
import SectionMain from '../../components/SectionMain';
import { getPageTitle } from '../../config';
import LayoutAuthenticated from '../../layouts/Authenticated';
const secondaryActionClassName =
'inline-flex items-center justify-center rounded-[8px] border border-slate-200 bg-white px-3.5 py-2 text-sm font-medium text-slate-700';
const PermissionsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const [filters] = useState([{ label: 'Name', title: 'name' }]);
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'Name', title: 'name'},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PERMISSIONS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: filters[0].title,
},
};
const getPermissionsCSV = async () => {
const response = await axios({url: '/permissions?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'permissionsCSV.csv'
link.click()
};
setFilterItems([...filterItems, newItem]);
};
const onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
const getPermissionsCSV = async () => {
const response = await axios({
url: '/permissions?filetype=csv',
method: 'GET',
responseType: 'blob',
});
const type = response.headers['content-type'];
const blob = new Blob([response.data], { type });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'permissionsCSV.csv';
link.click();
};
return (
<>
@ -89,74 +51,49 @@ const PermissionsTablesPage = () => {
<title>{getPageTitle('Permissions')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Permissions" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/permissions/permissions-new'} color='info' label='New Item'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getPermissionsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className="mb-6" hasTable>
<div className="mb-5 rounded-[10px] border border-slate-200 bg-white px-5 py-5">
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
Access control
</p>
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
Permissions
</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
Inspect the low-level capability map that powers admin access throughout the app.
</p>
</div>
<div className="mb-4 flex flex-wrap items-center gap-2 rounded-[10px] border border-slate-200 bg-slate-50 px-3.5 py-3">
<button className={secondaryActionClassName} onClick={addFilter} type="button">
Add filter
</button>
<button
className={secondaryActionClassName}
onClick={() => void getPermissionsCSV()}
type="button"
>
Download CSV
</button>
<div className="ml-auto flex items-center">
<div id="delete-rows-button" />
</div>
</div>
<CardBox className="mb-6 rounded-[10px] border border-slate-200 bg-white shadow-none" hasTable>
<TablePermissions
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
setFilterItems={setFilterItems}
showGrid={false}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</CardBox>
</SectionMain>
</>
)
}
);
};
PermissionsTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_PERMISSIONS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission="READ_PERMISSIONS">{page}</LayoutAuthenticated>;
};
export default PermissionsTablesPage
export default PermissionsTablesPage;

View File

@ -1,153 +1,325 @@
import {
mdiChartTimelineVariant,
mdiUpload,
mdiAccountCircleOutline,
mdiArrowLeft,
mdiCogOutline,
mdiLockOutline,
mdiPhoneOutline,
mdiUpload,
} from '@mdi/js';
import Head from 'next/head';
import Link from 'next/link';
import React, { ReactElement, useEffect, useState } from 'react';
import { Field, Form, Formik } from 'formik';
import { toast } from 'react-toastify';
import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../layouts/Authenticated';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import { Field, Form, Formik } from 'formik';
import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import BaseButton from '../components/BaseButton';
import BaseIcon from '../components/BaseIcon';
import FormImagePicker from '../components/FormImagePicker';
import { update } from '../stores/users/usersSlice';
import SectionMain from '../components/SectionMain';
import { getPageTitle } from '../config';
import LayoutAuthenticated from '../layouts/Authenticated';
import { findMe } from '../stores/authSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router';
import {findMe} from "../stores/authSlice";
import { update } from '../stores/users/usersSlice';
const inputClassName =
'w-full rounded-[10px] border border-slate-200 bg-white px-3.5 py-2.5 text-[14px] text-slate-900 outline-none placeholder:text-slate-400 focus:border-slate-900';
const labelClassName = 'mb-2 block text-[12px] font-medium text-slate-600';
const actionButtonClassName =
'inline-flex items-center justify-center rounded-[8px] border px-4 py-2 text-sm font-medium';
const initialFormValues = {
firstName: '',
lastName: '',
phoneNumber: '',
email: '',
avatar: [],
password: '',
};
function getAvatarUrl(avatar: any) {
if (!Array.isArray(avatar) || !avatar.length) {
return '';
}
return avatar[0]?.publicUrl || '';
}
function getProfileInitials(currentUser: any, formValues: typeof initialFormValues) {
const firstName = formValues.firstName || currentUser?.firstName || '';
const lastName = formValues.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 ProfilePage = () => {
const { currentUser } = useAppSelector(
(state) => state.auth,
);
const router = useRouter();
const dispatch = useAppDispatch();
const notify = (type, msg) => toast(msg, { type });
const initVals = {
firstName: '',
lastName: '',
phoneNumber: '',
email: '',
avatar: [],
password: ''
};
const [initialValues, setInitialValues] = useState(initVals);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [initialValues, setInitialValues] = useState(initialFormValues);
useEffect(() => {
if (currentUser?.id && typeof currentUser === 'object') {
const newInitialVal = { ...initVals };
useEffect(() => {
if (!currentUser?.id || typeof currentUser !== 'object') {
return;
}
Object.keys(initVals).forEach(
(el) => (newInitialVal[el] = currentUser[el]),
);
setInitialValues({
firstName: currentUser.firstName || '',
lastName: currentUser.lastName || '',
phoneNumber: currentUser.phoneNumber || '',
email: currentUser.email || '',
avatar: currentUser.avatar || [],
password: '',
});
}, [currentUser]);
setInitialValues(newInitialVal);
}
}, [currentUser]);
const handleSubmit = async (data: typeof initialFormValues) => {
await dispatch(update({ id: currentUser.id, data }));
await dispatch(findMe());
toast('Profile updated.', { type: 'success', position: 'bottom-center' });
};
const handleSubmit = async (data) => {
await dispatch(update({ id: currentUser.id, data }));
await dispatch(findMe());
await router.push('/settings');
notify('success', 'Profile was updated!');
};
return (
<>
<Head>
<title>{getPageTitle('Profile')}</title>
</Head>
<SectionMain>
<div className="mx-auto flex w-full max-w-5xl flex-col gap-5">
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-6">
<Link
className="inline-flex items-center gap-2 text-[12px] font-medium text-slate-500"
href="/settings"
>
<BaseIcon path={mdiArrowLeft} size={14} />
Back to settings
</Link>
<p className="mt-4 text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
Profile
</p>
<h1 className="mt-3 text-[2rem] font-semibold tracking-[-0.04em] text-slate-900">
Manage your account details.
</h1>
<p className="mt-3 max-w-2xl text-sm leading-6 text-slate-500">
Keep your name, avatar, contact information, and password current without leaving the workspace.
</p>
</div>
return (
<>
<Head>
<title>{getPageTitle('Profile')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Profile'
main
>
{''}
</SectionTitleLineWithButton>
<CardBox>
{currentUser?.avatar[0]?.publicUrl && <div className={'grid grid-cols-6 gap-4 mb-4'}>
<div className="col-span-1 w-80 h-80 overflow-hidden border-2 rounded-full inline-flex items-center justify-center mb-8">
<img className="w-80 h-80 max-w-full max-h-full object-cover object-center" src={`${currentUser?.avatar[0]?.publicUrl}`} alt="Avatar" />
<Formik
enableReinitialize
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
{({ isSubmitting, resetForm, setFieldValue, values }) => {
const previewUrl = getAvatarUrl(values.avatar);
const initials = getProfileInitials(currentUser, values);
return (
<Form>
<div className="grid gap-5 xl:grid-cols-[280px_minmax(0,1fr)]">
<div className="space-y-5">
<div className="rounded-[12px] border border-slate-200 bg-white px-5 py-5">
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
Account
</p>
<div className="mt-4 flex flex-col items-start gap-4">
<div className="flex h-24 w-24 items-center justify-center overflow-hidden rounded-[12px] border border-slate-200 bg-slate-100 text-[28px] font-semibold text-slate-600">
{previewUrl ? (
<img
alt="Profile avatar"
className="h-full w-full object-cover"
src={previewUrl}
/>
) : (
initials
)}
</div>
<div>
<p className="text-[15px] font-medium text-slate-900">
{values.firstName || values.lastName
? `${values.firstName} ${values.lastName}`.trim()
: 'Workspace user'}
</p>
<p className="mt-1 text-sm text-slate-500">
{values.email || currentUser?.email || 'No email'}
</p>
</div>
</div>
</div>}
<Formik
enableReinitialize
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField>
<Field
label='Avatar'
color='info'
icon={mdiUpload}
path={'users/avatar'}
name='avatar'
id='avatar'
schema={{
size: undefined,
formats: undefined,
}}
component={FormImagePicker}
></Field>
</FormField>
<FormField label='First Name'>
<Field name='firstName' placeholder='First Name' />
</FormField>
<div className="mt-5">
<span className={labelClassName}>Avatar</span>
<div className="flex flex-wrap items-center gap-2">
<Field
color="info"
component={FormImagePicker}
icon={mdiUpload}
id="avatar"
label="Upload image"
name="avatar"
path="users/avatar"
schema={{
size: undefined,
formats: undefined,
}}
/>
{previewUrl && (
<button
className={`${actionButtonClassName} border-slate-200 bg-white text-slate-600`}
onClick={() => {
setFieldValue('avatar', []);
}}
type="button"
>
Remove avatar
</button>
)}
</div>
</div>
</div>
<FormField label='Last Name'>
<Field name='lastName' placeholder='Last Name' />
</FormField>
<div className="rounded-[12px] border border-slate-200 bg-white px-5 py-5">
<div className="flex items-start gap-3">
<div className="mt-0.5 inline-flex h-9 w-9 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
<BaseIcon path={mdiLockOutline} size={18} />
</div>
<div>
<p className="text-[15px] font-medium text-slate-900">Password</p>
<p className="mt-1 text-sm leading-6 text-slate-500">
Leave the password field blank if you do not want to change it.
</p>
</div>
</div>
</div>
</div>
<FormField label='Phone Number'>
<Field name='phoneNumber' placeholder='Phone Number' />
</FormField>
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-6">
<div className="mb-5 flex items-start gap-3">
<div className="mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
<BaseIcon path={mdiAccountCircleOutline} size={20} />
</div>
<div>
<p className="text-[15px] font-medium text-slate-900">Personal details</p>
<p className="mt-1 text-sm leading-6 text-slate-500">
Update the information teammates will see around the workspace.
</p>
</div>
</div>
<FormField label='E-Mail'>
<Field name='email' placeholder='E-Mail' disabled />
</FormField>
<div className="grid gap-4 md:grid-cols-2">
<div>
<label className={labelClassName} htmlFor="firstName">
First name
</label>
<Field className={inputClassName} id="firstName" name="firstName" placeholder="Alex" />
</div>
<div>
<label className={labelClassName} htmlFor="lastName">
Last name
</label>
<Field className={inputClassName} id="lastName" name="lastName" placeholder="Blari" />
</div>
<div>
<label className={labelClassName} htmlFor="phoneNumber">
Phone number
</label>
<div className="relative">
<BaseIcon
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
path={mdiPhoneOutline}
size={16}
/>
<Field
className={`${inputClassName} pl-10`}
id="phoneNumber"
name="phoneNumber"
placeholder="+1 (555) 000-0000"
/>
</div>
</div>
<div>
<label className={labelClassName} htmlFor="email">
Email
</label>
<Field
className={`${inputClassName} cursor-not-allowed bg-slate-50 text-slate-500`}
disabled
id="email"
name="email"
placeholder="name@company.com"
/>
</div>
</div>
<FormField
label="Password"
>
<Field
name="password"
placeholder="password"
/>
</FormField>
<div className="mt-6 rounded-[12px] border border-slate-200 bg-slate-50 px-4 py-4">
<div className="mb-3 flex items-start gap-3">
<div className="mt-0.5 inline-flex h-9 w-9 items-center justify-center rounded-[10px] border border-slate-200 bg-white text-slate-600">
<BaseIcon path={mdiCogOutline} size={18} />
</div>
<div>
<p className="text-[15px] font-medium text-slate-900">Security</p>
<p className="mt-1 text-sm leading-6 text-slate-500">
Set a new password only when you need to rotate credentials.
</p>
</div>
</div>
<div>
<label className={labelClassName} htmlFor="password">
New password
</label>
<Field
className={inputClassName}
id="password"
name="password"
placeholder="Leave blank to keep the current password"
type="password"
/>
</div>
</div>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton
type='reset'
color='danger'
outline
label='Cancel'
onClick={() => router.push('/settings')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
<div className="mt-6 flex flex-wrap items-center gap-3 border-t border-slate-200 pt-5">
<button
className={`${actionButtonClassName} border-slate-900 bg-slate-900 text-white disabled:cursor-not-allowed disabled:opacity-60`}
disabled={isSubmitting}
type="submit"
>
{isSubmitting ? 'Saving...' : 'Save changes'}
</button>
<button
className={`${actionButtonClassName} border-slate-200 bg-white text-slate-700`}
onClick={() => resetForm()}
type="reset"
>
Reset
</button>
<Link
className={`${actionButtonClassName} border-slate-200 bg-white text-slate-700`}
href="/settings"
>
Cancel
</Link>
</div>
</div>
</div>
</Form>
);
}}
</Formik>
</div>
</SectionMain>
</>
);
};
ProfilePage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default ProfilePage;

View File

@ -1,87 +1,61 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import Head from 'next/head';
import Link from 'next/link';
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TableRoles from '../../components/Roles/TableRoles'
import BaseButton from '../../components/BaseButton'
import axios from "axios";
import Link from "next/link";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import CardBoxModal from "../../components/CardBoxModal";
import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/roles/rolesSlice';
import React, { ReactElement, useState } from 'react';
import axios from 'axios';
import CardBox from '../../components/CardBox';
import TableRoles from '../../components/Roles/TableRoles';
import SectionMain from '../../components/SectionMain';
import { getPageTitle } from '../../config';
import { hasPermission } from '../../helpers/userPermissions';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { useAppSelector } from '../../stores/hooks';
import {hasPermission} from "../../helpers/userPermissions";
const primaryActionClassName =
'inline-flex items-center justify-center rounded-[8px] bg-slate-900 px-4 py-2 text-sm font-medium text-white';
const secondaryActionClassName =
'inline-flex items-center justify-center rounded-[8px] border border-slate-200 bg-white px-3.5 py-2 text-sm font-medium text-slate-700';
const RolesTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'Name', title: 'name'},
{label: 'Permissions', title: 'permissions'},
const [filters] = useState([
{ label: 'Name', title: 'name' },
{ label: 'Permissions', title: 'permissions' },
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ROLES');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ROLES');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: filters[0].title,
},
};
const getRolesCSV = async () => {
const response = await axios({url: '/roles?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'rolesCSV.csv'
link.click()
};
setFilterItems([...filterItems, newItem]);
};
const onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
const getRolesCSV = async () => {
const response = await axios({
url: '/roles?filetype=csv',
method: 'GET',
responseType: 'blob',
});
const type = response.headers['content-type'];
const blob = new Blob([response.data], { type });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'rolesCSV.csv';
link.click();
};
return (
<>
@ -89,74 +63,58 @@ const RolesTablesPage = () => {
<title>{getPageTitle('Roles')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Roles" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/roles/roles-new'} color='info' label='New Item'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getRolesCSV} />
<div className="mb-5 rounded-[10px] border border-slate-200 bg-white px-5 py-5">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="min-w-0">
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
Access control
</p>
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
Roles
</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
Group permissions into a few clear access levels and keep role names readable.
</p>
</div>
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
<Link className={primaryActionClassName} href="/roles/roles-new">
New role
</Link>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className="mb-6" hasTable>
</div>
</div>
<div className="mb-4 flex flex-wrap items-center gap-2 rounded-[10px] border border-slate-200 bg-slate-50 px-3.5 py-3">
<button className={secondaryActionClassName} onClick={addFilter} type="button">
Add filter
</button>
<button
className={secondaryActionClassName}
onClick={() => void getRolesCSV()}
type="button"
>
Download CSV
</button>
<div className="ml-auto flex items-center">
<div id="delete-rows-button" />
</div>
</div>
<CardBox className="mb-6 rounded-[10px] border border-slate-200 bg-white shadow-none" hasTable>
<TableRoles
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
setFilterItems={setFilterItems}
showGrid={false}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</CardBox>
</SectionMain>
</>
)
}
);
};
RolesTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_ROLES'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission="READ_ROLES">{page}</LayoutAuthenticated>;
};
export default RolesTablesPage
export default RolesTablesPage;

View File

@ -1,178 +1,116 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import Head from 'next/head';
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TableUsage_events from '../../components/Usage_events/TableUsage_events'
import BaseButton from '../../components/BaseButton'
import axios from "axios";
import Link from "next/link";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import CardBoxModal from "../../components/CardBoxModal";
import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/usage_events/usage_eventsSlice';
import {hasPermission} from "../../helpers/userPermissions";
import React, { ReactElement, useState } from 'react';
import axios from 'axios';
import CardBox from '../../components/CardBox';
import TableUsage_events from '../../components/Usage_events/TableUsage_events';
import SectionMain from '../../components/SectionMain';
import { getPageTitle } from '../../config';
import LayoutAuthenticated from '../../layouts/Authenticated';
const secondaryActionClassName =
'inline-flex items-center justify-center rounded-[8px] border border-slate-200 bg-white px-3.5 py-2 text-sm font-medium text-slate-700';
const Usage_eventsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'Provider', title: 'provider'},{label: 'Model', title: 'model'},{label: 'MetadataJSON', title: 'metadata_json'},
{label: 'Inputtokens', title: 'input_tokens', number: 'true'},{label: 'Outputtokens', title: 'output_tokens', number: 'true'},{label: 'Totaltokens', title: 'total_tokens', number: 'true'},
{label: 'CostUSD', title: 'cost_usd', number: 'true'},
{label: 'Occurredat', title: 'occurred_at', date: 'true'},
{label: 'User', title: 'user'},
{label: 'Conversation', title: 'conversation'},
{label: 'Message', title: 'message'},
{label: 'Agent', title: 'agent'},
{label: 'Eventtype', title: 'event_type', type: 'enum', options: ['message_sent','message_generated','tokens_counted','cost_incurred','attachment_uploaded']},
const [filters] = useState([
{ label: 'Provider', title: 'provider' },
{ label: 'Model', title: 'model' },
{ label: 'MetadataJSON', title: 'metadata_json' },
{ label: 'Inputtokens', title: 'input_tokens', number: 'true' },
{ label: 'Outputtokens', title: 'output_tokens', number: 'true' },
{ label: 'Totaltokens', title: 'total_tokens', number: 'true' },
{ label: 'CostUSD', title: 'cost_usd', number: 'true' },
{ label: 'Occurredat', title: 'occurred_at', date: 'true' },
{ label: 'User', title: 'user' },
{ label: 'Conversation', title: 'conversation' },
{ label: 'Message', title: 'message' },
{ label: 'Agent', title: 'agent' },
{
label: 'Eventtype',
title: 'event_type',
type: 'enum',
options: ['message_sent', 'message_generated', 'tokens_counted', 'cost_incurred', 'attachment_uploaded'],
},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_USAGE_EVENTS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: filters[0].title,
},
};
const getUsage_eventsCSV = async () => {
const response = await axios({url: '/usage_events?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'usage_eventsCSV.csv'
link.click()
};
setFilterItems([...filterItems, newItem]);
};
const onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
const getUsageEventsCSV = async () => {
const response = await axios({
url: '/usage_events?filetype=csv',
method: 'GET',
responseType: 'blob',
});
const type = response.headers['content-type'];
const blob = new Blob([response.data], { type });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'usage_eventsCSV.csv';
link.click();
};
return (
<>
<Head>
<title>{getPageTitle('Usage_events')}</title>
<title>{getPageTitle('Usage events')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Usage_events" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/usage_events/usage_events-new'} color='info' label='New Item'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getUsage_eventsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className="mb-6" hasTable>
<div className="mb-5 rounded-[10px] border border-slate-200 bg-white px-5 py-5">
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">Admin</p>
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
Usage events
</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
Track model activity, token usage, and cost signals without leaving the backoffice.
</p>
</div>
<div className="mb-4 flex flex-wrap items-center gap-2 rounded-[10px] border border-slate-200 bg-slate-50 px-3.5 py-3">
<button className={secondaryActionClassName} onClick={addFilter} type="button">
Add filter
</button>
<button
className={secondaryActionClassName}
onClick={() => void getUsageEventsCSV()}
type="button"
>
Download CSV
</button>
<div className="ml-auto flex items-center">
<div id="delete-rows-button" />
</div>
</div>
<CardBox className="mb-6 rounded-[10px] border border-slate-200 bg-white shadow-none" hasTable>
<TableUsage_events
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
setFilterItems={setFilterItems}
showGrid={false}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</CardBox>
</SectionMain>
</>
)
}
);
};
Usage_eventsTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_USAGE_EVENTS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission="READ_USAGE_EVENTS">{page}</LayoutAuthenticated>;
};
export default Usage_eventsTablesPage
export default Usage_eventsTablesPage;

View File

@ -1,166 +1,165 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import Head from 'next/head';
import Link from 'next/link';
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TableUsers from '../../components/Users/TableUsers'
import BaseButton from '../../components/BaseButton'
import axios from "axios";
import Link from "next/link";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import CardBoxModal from "../../components/CardBoxModal";
import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/users/usersSlice';
import React, { ReactElement, useState } from 'react';
import axios from 'axios';
import CardBox from '../../components/CardBox';
import CardBoxModal from '../../components/CardBoxModal';
import DragDropFilePicker from '../../components/DragDropFilePicker';
import TableUsers from '../../components/Users/TableUsers';
import SectionMain from '../../components/SectionMain';
import { getPageTitle } from '../../config';
import { hasPermission } from '../../helpers/userPermissions';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { setRefetch, uploadCsv } from '../../stores/users/usersSlice';
import {hasPermission} from "../../helpers/userPermissions";
const primaryActionClassName =
'inline-flex items-center justify-center rounded-[8px] bg-slate-900 px-4 py-2 text-sm font-medium text-white';
const secondaryActionClassName =
'inline-flex items-center justify-center rounded-[8px] border border-slate-200 bg-white px-3.5 py-2 text-sm font-medium text-slate-700';
const UsersTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'First Name', title: 'firstName'},{label: 'Last Name', title: 'lastName'},{label: 'Phone Number', title: 'phoneNumber'},{label: 'E-Mail', title: 'email'},
{label: 'App Role', title: 'app_role'},
{label: 'Custom Permissions', title: 'custom_permissions'},
const [filters] = useState([
{ label: 'First Name', title: 'firstName' },
{ label: 'Last Name', title: 'lastName' },
{ label: 'Phone Number', title: 'phoneNumber' },
{ label: 'E-Mail', title: 'email' },
{ label: 'App Role', title: 'app_role' },
{ label: 'Custom Permissions', title: 'custom_permissions' },
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_USERS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_USERS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: filters[0].title,
},
};
const getUsersCSV = async () => {
const response = await axios({url: '/users?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'usersCSV.csv'
link.click()
};
setFilterItems([...filterItems, newItem]);
};
const onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const getUsersCSV = async () => {
const response = await axios({
url: '/users?filetype=csv',
method: 'GET',
responseType: 'blob',
});
const type = response.headers['content-type'];
const blob = new Blob([response.data], { type });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'usersCSV.csv';
link.click();
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
const onModalConfirm = async () => {
if (!csvFile) {
return;
}
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
return (
<>
<Head>
<title>{getPageTitle('Users')}</title>
<title>{getPageTitle('People')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Users" main>
{''}
</SectionTitleLineWithButton>
<CardBox id="usersList" className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/users/users-new'} color='info' label='Add/Invite User'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getUsersCSV} />
<div className="mb-5 rounded-[10px] border border-slate-200 bg-white px-5 py-5">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="min-w-0">
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
Admin
</p>
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
People
</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
Invite teammates, review workspace access, and keep account ownership visible.
</p>
</div>
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
<Link className={primaryActionClassName} href="/users/users-new">
Invite user
</Link>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className="mb-6" hasTable>
</div>
</div>
<div className="mb-4 flex flex-wrap items-center gap-2 rounded-[10px] border border-slate-200 bg-slate-50 px-3.5 py-3">
<button className={secondaryActionClassName} onClick={addFilter} type="button">
Add filter
</button>
<button
className={secondaryActionClassName}
onClick={() => void getUsersCSV()}
type="button"
>
Download CSV
</button>
{hasCreatePermission && (
<button
className={secondaryActionClassName}
onClick={() => setIsModalActive(true)}
type="button"
>
Upload CSV
</button>
)}
<div className="ml-auto flex items-center">
<div id="delete-rows-button" />
</div>
</div>
<CardBox className="mb-6 rounded-[10px] border border-slate-200 bg-white shadow-none" hasTable>
<TableUsers
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
setFilterItems={setFilterItems}
showGrid={false}
/>
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
buttonColor="info"
buttonLabel="Confirm"
isActive={isModalActive}
onCancel={onModalCancel}
onConfirm={onModalConfirm}
title="Upload CSV"
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
<DragDropFilePicker file={csvFile} formats=".csv" setFile={setCsvFile} />
</CardBoxModal>
</>
)
}
);
};
UsersTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_USERS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission="READ_USERS">{page}</LayoutAuthenticated>;
};
export default UsersTablesPage
export default UsersTablesPage;