4
This commit is contained in:
parent
05382ed1be
commit
47fbf3afb9
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>,
|
||||
);
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
@apply pt-12 xl:pl-72 h-full;
|
||||
@apply pt-12 xl:pl-64 h-full;
|
||||
}
|
||||
|
||||
#app {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user