4
This commit is contained in:
parent
05382ed1be
commit
47fbf3afb9
@ -19,7 +19,7 @@ export default function AsideMenu({
|
|||||||
<>
|
<>
|
||||||
<AsideMenuLayer
|
<AsideMenuLayer
|
||||||
menu={props.menu}
|
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' : ''
|
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
|
||||||
}`}
|
}`}
|
||||||
onAsideLgCloseClick={props.onAsideLgClose}
|
onAsideLgCloseClick={props.onAsideLgClose}
|
||||||
|
|||||||
@ -2,10 +2,8 @@ import React, { useEffect, useState } from 'react'
|
|||||||
import { mdiMinus, mdiPlus } from '@mdi/js'
|
import { mdiMinus, mdiPlus } from '@mdi/js'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { getButtonColor } from '../colors'
|
|
||||||
import AsideMenuList from './AsideMenuList'
|
import AsideMenuList from './AsideMenuList'
|
||||||
import { MenuAsideItem } from '../interfaces'
|
import { MenuAsideItem } from '../interfaces'
|
||||||
import { useAppSelector } from '../stores/hooks'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -17,15 +15,6 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
|
|||||||
const [isLinkActive, setIsLinkActive] = useState(false)
|
const [isLinkActive, setIsLinkActive] = useState(false)
|
||||||
const [isDropdownActive, setIsDropdownActive] = 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()
|
const { asPath, isReady } = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -40,42 +29,45 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
|
|||||||
}
|
}
|
||||||
}, [item.href, isReady, asPath])
|
}, [item.href, isReady, asPath])
|
||||||
|
|
||||||
|
const isActiveItem = isLinkActive || (Boolean(item.menu) && isDropdownActive)
|
||||||
|
|
||||||
const asideMenuItemInnerContents = (
|
const asideMenuItemInnerContents = (
|
||||||
<>
|
<>
|
||||||
{item.icon && (
|
{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
|
<span
|
||||||
className={`min-w-0 grow break-words leading-6 ${
|
className={`min-w-0 grow break-words leading-6 ${
|
||||||
item.menu ? '' : 'pr-2'
|
isActiveItem ? 'text-slate-900 dark:text-white' : 'text-slate-700 dark:text-slate-200'
|
||||||
} ${activeClassAddon}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
{item.menu && (
|
{item.menu && (
|
||||||
<BaseIcon
|
<BaseIcon
|
||||||
path={isDropdownActive ? mdiMinus : mdiPlus}
|
path={isDropdownActive ? mdiMinus : mdiPlus}
|
||||||
className={`flex-none ${activeClassAddon}`}
|
className={`flex-none ${isActiveItem ? 'text-slate-900 dark:text-white' : 'text-slate-400 dark:text-slate-400'}`}
|
||||||
w="w-8"
|
size="16"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
const componentClass = [
|
const componentClass = [
|
||||||
'flex items-start gap-3 cursor-pointer py-1.5',
|
'flex cursor-pointer items-start gap-3 rounded-[8px] border transition-colors',
|
||||||
isDropdownList ? 'px-4 text-sm' : '',
|
isDropdownList ? 'px-3 py-2 text-[14px] font-medium' : 'px-3 py-2.5 text-[15px] font-medium',
|
||||||
item.color
|
isActiveItem
|
||||||
? getButtonColor(item.color, false, true)
|
? 'border-slate-200 bg-white dark:border-dark-600 dark:bg-dark-800'
|
||||||
: `${asideMenuItemStyle}`,
|
: 'border-transparent bg-transparent hover:border-slate-200 hover:bg-slate-50 dark:hover:border-dark-700 dark:hover:bg-dark-800/60',
|
||||||
isLinkActive
|
|
||||||
? `text-black ${activeLinkColor} dark:text-white dark:bg-dark-800`
|
|
||||||
: '',
|
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={'px-3 py-1.5'}>
|
<li className={isDropdownList ? 'py-0.5' : 'py-1'}>
|
||||||
{item.withDevider && <hr className={`${borders} mb-3`} />}
|
{item.withDevider && <hr className="my-3 border-slate-200 dark:border-dark-700" />}
|
||||||
{item.href && (
|
{item.href && (
|
||||||
<Link href={item.href} target={item.target} className={componentClass}>
|
<Link href={item.href} target={item.target} className={componentClass}>
|
||||||
{asideMenuItemInnerContents}
|
{asideMenuItemInnerContents}
|
||||||
@ -88,11 +80,11 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{item.menu && (
|
{item.menu && (
|
||||||
<AsideMenuList
|
<AsideMenuList
|
||||||
menu={item.menu}
|
className={`mt-2 rounded-[10px] border border-slate-200 bg-slate-50/80 p-2 dark:border-dark-700 dark:bg-dark-800/40 ${
|
||||||
className={`${asideMenuDropdownStyle} ${
|
isDropdownActive ? 'block' : 'hidden'
|
||||||
isDropdownActive ? 'block dark:bg-slate-800/50' : 'hidden'
|
|
||||||
}`}
|
}`}
|
||||||
isDropdownList
|
isDropdownList
|
||||||
|
menu={item.menu}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { mdiLogout, mdiClose } from '@mdi/js'
|
import { mdiClose } from '@mdi/js'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
import AsideMenuList from './AsideMenuList'
|
import AsideMenuList from './AsideMenuList'
|
||||||
import { MenuAsideItem } from '../interfaces'
|
import { MenuAsideItem } from '../interfaces'
|
||||||
import { useAppSelector } from '../stores/hooks'
|
import { useAppSelector } from '../stores/hooks'
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -14,9 +13,6 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AsideMenuLayer({ menu, className = '', ...props }: 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 asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle)
|
||||||
const darkMode = useAppSelector((state) => state.style.darkMode)
|
const darkMode = useAppSelector((state) => state.style.darkMode)
|
||||||
|
|
||||||
@ -29,25 +25,25 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
id='asideMenu'
|
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
|
<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
|
<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">
|
<div className="min-w-0 flex-1">
|
||||||
|
<span className="block truncate text-[14px] font-semibold tracking-[-0.02em] text-slate-900 dark:text-white">
|
||||||
<b className="font-black">AI Chat Workspace</b>
|
AI Chat Workspace
|
||||||
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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}
|
onClick={handleAsideLgCloseClick}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<BaseIcon path={mdiClose} />
|
<BaseIcon path={mdiClose} size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -55,7 +51,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
|
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<AsideMenuList menu={menu} />
|
<AsideMenuList className="px-2 py-2.5" menu={menu} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@ -17,17 +17,16 @@ export default function AsideMenuList({ menu, isDropdownList = false, className
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={className}>
|
<ul className={className}>
|
||||||
{menu.map((item, index) => {
|
{menu.map((item) => {
|
||||||
|
|
||||||
if (!hasPermission(currentUser, item.permissions)) return null;
|
if (!hasPermission(currentUser, item.permissions)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index}>
|
<React.Fragment key={item.href || item.label}>
|
||||||
<AsideMenuItem
|
<AsideMenuItem
|
||||||
item={item}
|
item={item}
|
||||||
isDropdownList={isDropdownList}
|
isDropdownList={isDropdownList}
|
||||||
/>
|
/>
|
||||||
</div>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -63,7 +63,7 @@ const renderInline = (text: string, keyPrefix: string) =>
|
|||||||
return (
|
return (
|
||||||
<code
|
<code
|
||||||
key={key}
|
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}
|
{token.value}
|
||||||
</code>
|
</code>
|
||||||
@ -90,7 +90,7 @@ const renderInline = (text: string, keyPrefix: string) =>
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
key={key}
|
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}
|
href={token.href}
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -104,9 +104,9 @@ const renderInline = (text: string, keyPrefix: string) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
const headingClassNames: Record<string, string> = {
|
const headingClassNames: Record<string, string> = {
|
||||||
h1: 'text-2xl font-semibold tracking-[-0.03em] text-slate-900 dark:text-white',
|
h1: 'text-[1.35rem] 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',
|
h2: 'text-[1.15rem] font-semibold tracking-[-0.02em] text-slate-900 dark:text-white',
|
||||||
h3: 'text-lg font-semibold text-slate-900 dark:text-white',
|
h3: 'text-[1rem] font-semibold text-slate-900 dark:text-white',
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSpecialBlock = (line: string) => {
|
const isSpecialBlock = (line: string) => {
|
||||||
@ -147,12 +147,12 @@ export default function ChatMarkdown({ content, className = '' }: ChatMarkdownPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
blocks.push(
|
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">
|
<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>{language || 'code'}</span>
|
||||||
<span>{codeLines.length} lines</span>
|
<span>{codeLines.length} lines</span>
|
||||||
</div>
|
</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>
|
<code>{codeLines.join('\n')}</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>,
|
</div>,
|
||||||
@ -203,7 +203,7 @@ export default function ChatMarkdown({ content, className = '' }: ChatMarkdownPr
|
|||||||
blocks.push(
|
blocks.push(
|
||||||
<blockquote
|
<blockquote
|
||||||
key={`quote-${index}`}
|
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}`)}
|
{renderInline(quoteLines.join(' '), `quote-${index}`)}
|
||||||
</blockquote>,
|
</blockquote>,
|
||||||
@ -223,7 +223,7 @@ export default function ChatMarkdown({ content, className = '' }: ChatMarkdownPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
blocks.push(
|
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) => (
|
{items.map((item, itemIndex) => (
|
||||||
<li key={`item-${index}-${itemIndex}`} className="list-disc">
|
<li key={`item-${index}-${itemIndex}`} className="list-disc">
|
||||||
{renderInline(item, `list-${index}-${itemIndex}`)}
|
{renderInline(item, `list-${index}-${itemIndex}`)}
|
||||||
@ -241,7 +241,7 @@ export default function ChatMarkdown({ content, className = '' }: ChatMarkdownPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
blocks.push(
|
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}`)}
|
{renderInline(paragraphLines.join(' '), `paragraph-${index}`)}
|
||||||
</p>,
|
</p>,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import axios from 'axios';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
import BaseIcon from '../BaseIcon';
|
import BaseIcon from '../BaseIcon';
|
||||||
import { hasPermission } from '../../helpers/userPermissions';
|
import { hasPermission } from '../../helpers/userPermissions';
|
||||||
import { useAppSelector } from '../../stores/hooks';
|
import { useAppSelector } from '../../stores/hooks';
|
||||||
@ -196,12 +197,24 @@ const formatMessageTime = (value?: string) => {
|
|||||||
return '';
|
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', {
|
return new Intl.DateTimeFormat('en-US', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
hour: 'numeric',
|
}).format(date);
|
||||||
minute: '2-digit',
|
|
||||||
}).format(new Date(value));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getErrorMessage = (error: unknown, fallback: string) => {
|
const getErrorMessage = (error: unknown, fallback: string) => {
|
||||||
@ -222,6 +235,30 @@ const getErrorMessage = (error: unknown, fallback: string) => {
|
|||||||
return fallback;
|
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 => ({
|
const toConversationSummary = (conversation: ConversationDetail): ConversationSummary => ({
|
||||||
id: conversation.id,
|
id: conversation.id,
|
||||||
title: conversation.title,
|
title: conversation.title,
|
||||||
@ -245,6 +282,8 @@ function TypingIndicator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MessageBubbleProps = {
|
type MessageBubbleProps = {
|
||||||
|
currentUserAvatarUrl: string;
|
||||||
|
currentUserInitial: string;
|
||||||
currentUserName: string;
|
currentUserName: string;
|
||||||
message: WorkspaceMessage;
|
message: WorkspaceMessage;
|
||||||
streamingText: string;
|
streamingText: string;
|
||||||
@ -252,6 +291,8 @@ type MessageBubbleProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function MessageBubble({
|
function MessageBubble({
|
||||||
|
currentUserAvatarUrl,
|
||||||
|
currentUserInitial,
|
||||||
currentUserName,
|
currentUserName,
|
||||||
message,
|
message,
|
||||||
streamingText,
|
streamingText,
|
||||||
@ -279,10 +320,10 @@ function MessageBubble({
|
|||||||
: 'border border-slate-200 bg-white text-slate-900 shadow-sm';
|
: 'border border-slate-200 bg-white text-slate-900 shadow-sm';
|
||||||
const avatarLabel = isUser ? currentUserName : 'AI';
|
const avatarLabel = isUser ? currentUserName : 'AI';
|
||||||
const metaClassName = isUser
|
const metaClassName = isUser
|
||||||
? 'text-[11px] uppercase tracking-[0.18em] text-white/70'
|
? 'text-[9px] uppercase tracking-[0.12em] text-white/65'
|
||||||
: isFailed
|
: isFailed
|
||||||
? 'text-[11px] uppercase tracking-[0.18em] text-red-500'
|
? 'text-[9px] uppercase tracking-[0.12em] text-red-500'
|
||||||
: 'text-[11px] uppercase tracking-[0.18em] text-slate-400';
|
: 'text-[9px] uppercase tracking-[0.12em] text-slate-400';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex gap-2.5 ${isUser ? 'justify-end' : 'justify-start'}`}>
|
<div className={`flex gap-2.5 ${isUser ? 'justify-end' : 'justify-start'}`}>
|
||||||
@ -296,7 +337,7 @@ function MessageBubble({
|
|||||||
<span>{isUser ? currentUserName : 'Assistant'}</span>
|
<span>{isUser ? currentUserName : 'Assistant'}</span>
|
||||||
{statusLabel && (
|
{statusLabel && (
|
||||||
<span
|
<span
|
||||||
className={`rounded-md px-2 py-1 ${
|
className={`rounded-md px-1.5 py-0.5 text-[9px] ${
|
||||||
isUser
|
isUser
|
||||||
? 'bg-white/10 text-white/80'
|
? 'bg-white/10 text-white/80'
|
||||||
: isFailed
|
: isFailed
|
||||||
@ -308,7 +349,7 @@ function MessageBubble({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{displayTime && (
|
{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}
|
{displayTime}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -317,14 +358,14 @@ function MessageBubble({
|
|||||||
{message.pending ? (
|
{message.pending ? (
|
||||||
<TypingIndicator />
|
<TypingIndicator />
|
||||||
) : isStreaming ? (
|
) : 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
|
<ChatMarkdown
|
||||||
className={
|
className={
|
||||||
isUser
|
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
|
: 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}
|
content={rawContent}
|
||||||
@ -332,9 +373,19 @@ function MessageBubble({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isUser && (
|
{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">
|
currentUserAvatarUrl ? (
|
||||||
{avatarLabel.slice(0, 1)}
|
<div className="flex h-7 w-7 shrink-0 overflow-hidden rounded-md border border-slate-200 bg-slate-100">
|
||||||
</div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
@ -411,9 +462,18 @@ export default function WorkspaceShell() {
|
|||||||
const [streamingText, setStreamingText] = useState('');
|
const [streamingText, setStreamingText] = useState('');
|
||||||
const [hasBootstrapped, setHasBootstrapped] = useState(false);
|
const [hasBootstrapped, setHasBootstrapped] = useState(false);
|
||||||
|
|
||||||
|
const showSuccessToast = useCallback((message: string) => {
|
||||||
|
toast(message, {
|
||||||
|
position: 'bottom-center',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const currentConversationId =
|
const currentConversationId =
|
||||||
typeof router.query.conversationId === 'string' ? router.query.conversationId : null;
|
typeof router.query.conversationId === 'string' ? router.query.conversationId : null;
|
||||||
const currentUserName = currentUser?.firstName || currentUser?.email || 'You';
|
const currentUserName = currentUser?.firstName || currentUser?.email || 'You';
|
||||||
|
const currentUserAvatarUrl = getAvatarUrl(currentUser?.avatar);
|
||||||
|
const currentUserInitial = getUserInitial(currentUser);
|
||||||
const canAccessAdmin = hasPermission(currentUser, 'READ_USERS');
|
const canAccessAdmin = hasPermission(currentUser, 'READ_USERS');
|
||||||
|
|
||||||
const activeConversations = useMemo(
|
const activeConversations = useMemo(
|
||||||
@ -620,17 +680,14 @@ export default function WorkspaceShell() {
|
|||||||
});
|
});
|
||||||
applyConversationPayload(data.conversation);
|
applyConversationPayload(data.conversation);
|
||||||
setIsEditingTitle(false);
|
setIsEditingTitle(false);
|
||||||
setNotice({
|
showSuccessToast('Conversation renamed.');
|
||||||
type: 'success',
|
|
||||||
message: 'Conversation renamed.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNotice({
|
setNotice({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: getErrorMessage(error, 'Failed to rename the conversation.'),
|
message: getErrorMessage(error, 'Failed to rename the conversation.'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [activeConversation, applyConversationPayload, draftTitle]);
|
}, [activeConversation, applyConversationPayload, draftTitle, showSuccessToast]);
|
||||||
|
|
||||||
const handleArchiveConversation = useCallback(async () => {
|
const handleArchiveConversation = useCallback(async () => {
|
||||||
if (!activeConversation) {
|
if (!activeConversation) {
|
||||||
@ -647,20 +704,18 @@ export default function WorkspaceShell() {
|
|||||||
if (nextStatus === 'archived') {
|
if (nextStatus === 'archived') {
|
||||||
setShowArchived(true);
|
setShowArchived(true);
|
||||||
}
|
}
|
||||||
setNotice({
|
showSuccessToast(
|
||||||
type: 'success',
|
nextStatus === 'archived'
|
||||||
message:
|
? 'Conversation archived. It is now listed under Archived chats.'
|
||||||
nextStatus === 'archived'
|
: 'Conversation restored to Recent chats.',
|
||||||
? 'Conversation archived. It is now listed under Archived chats.'
|
);
|
||||||
: 'Conversation restored to Recent chats.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNotice({
|
setNotice({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: getErrorMessage(error, 'Failed to update the conversation status.'),
|
message: getErrorMessage(error, 'Failed to update the conversation status.'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [activeConversation, applyConversationPayload]);
|
}, [activeConversation, applyConversationPayload, showSuccessToast]);
|
||||||
|
|
||||||
const handleDeleteConversation = useCallback(async () => {
|
const handleDeleteConversation = useCallback(async () => {
|
||||||
if (!activeConversation) {
|
if (!activeConversation) {
|
||||||
@ -677,10 +732,7 @@ export default function WorkspaceShell() {
|
|||||||
try {
|
try {
|
||||||
await axios.delete(`/workspace/conversations/${activeConversation.id}`);
|
await axios.delete(`/workspace/conversations/${activeConversation.id}`);
|
||||||
removeConversation(activeConversation.id);
|
removeConversation(activeConversation.id);
|
||||||
setNotice({
|
showSuccessToast('Conversation deleted.');
|
||||||
type: 'success',
|
|
||||||
message: 'Conversation deleted.',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (nextConversation) {
|
if (nextConversation) {
|
||||||
await router.replace(`/workspace/${nextConversation.id}`);
|
await router.replace(`/workspace/${nextConversation.id}`);
|
||||||
@ -693,7 +745,7 @@ export default function WorkspaceShell() {
|
|||||||
message: getErrorMessage(error, 'Failed to delete the conversation.'),
|
message: getErrorMessage(error, 'Failed to delete the conversation.'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [activeConversation, conversations, removeConversation, router]);
|
}, [activeConversation, conversations, removeConversation, router, showSuccessToast]);
|
||||||
|
|
||||||
const handleAgentChange = useCallback(
|
const handleAgentChange = useCallback(
|
||||||
async (nextAgentId: string) => {
|
async (nextAgentId: string) => {
|
||||||
@ -900,14 +952,15 @@ export default function WorkspaceShell() {
|
|||||||
.find((message) => message.role === 'assistant')?.id || null;
|
.find((message) => message.role === 'assistant')?.id || null;
|
||||||
|
|
||||||
return (
|
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`}
|
<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)]'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
|
<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
|
<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 ${
|
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'
|
isSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
|
||||||
@ -1063,13 +1116,13 @@ export default function WorkspaceShell() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-end gap-1.5">
|
||||||
{activeConversation && !isEditingTitle && (
|
{activeConversation && !isEditingTitle && (
|
||||||
<>
|
<>
|
||||||
<label className="text-[11px] text-slate-600">
|
<label className="flex min-w-[176px] flex-col gap-1 text-[11px] text-slate-600">
|
||||||
<span className="mb-1 block text-[9px] uppercase tracking-[0.12em] text-slate-400">Agent</span>
|
<span className="block text-[9px] uppercase tracking-[0.12em] text-slate-400">Agent</span>
|
||||||
<select
|
<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)}
|
onChange={(event) => void handleAgentChange(event.target.value)}
|
||||||
value={agentSelectionValue}
|
value={agentSelectionValue}
|
||||||
>
|
>
|
||||||
@ -1081,14 +1134,14 @@ export default function WorkspaceShell() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<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)}
|
onClick={() => setIsEditingTitle(true)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<BaseIcon path={mdiPencilOutline} size={16} />
|
<BaseIcon path={mdiPencilOutline} size={16} />
|
||||||
</button>
|
</button>
|
||||||
<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()}
|
onClick={() => void handleArchiveConversation()}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -1096,7 +1149,7 @@ export default function WorkspaceShell() {
|
|||||||
{activeConversation.status === 'archived' ? 'Restore to recent' : 'Move to archive'}
|
{activeConversation.status === 'archived' ? 'Restore to recent' : 'Move to archive'}
|
||||||
</button>
|
</button>
|
||||||
<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()}
|
onClick={() => void handleDeleteConversation()}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -1106,7 +1159,7 @@ export default function WorkspaceShell() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button
|
<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)}
|
onClick={() => setIsSidebarOpen(true)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -1114,7 +1167,7 @@ export default function WorkspaceShell() {
|
|||||||
History
|
History
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<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"
|
href="/settings"
|
||||||
>
|
>
|
||||||
<BaseIcon path={mdiCogOutline} size={16} />
|
<BaseIcon path={mdiCogOutline} size={16} />
|
||||||
@ -1122,7 +1175,7 @@ export default function WorkspaceShell() {
|
|||||||
</Link>
|
</Link>
|
||||||
{canAccessAdmin && (
|
{canAccessAdmin && (
|
||||||
<Link
|
<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"
|
href="/dashboard"
|
||||||
>
|
>
|
||||||
<BaseIcon path={mdiOpenInNew} size={16} />
|
<BaseIcon path={mdiOpenInNew} size={16} />
|
||||||
@ -1182,6 +1235,8 @@ export default function WorkspaceShell() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-1.5" key={message.id}>
|
<div className="space-y-1.5" key={message.id}>
|
||||||
<MessageBubble
|
<MessageBubble
|
||||||
|
currentUserAvatarUrl={currentUserAvatarUrl}
|
||||||
|
currentUserInitial={currentUserInitial}
|
||||||
currentUserName={currentUserName}
|
currentUserName={currentUserName}
|
||||||
message={message}
|
message={message}
|
||||||
streamingMessageId={streamingMessageId}
|
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="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="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]">
|
<div className="grid gap-2 lg:grid-cols-[minmax(0,1fr)_165px]">
|
||||||
<textarea
|
<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"
|
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'}
|
{sending ? 'Sending…' : 'Send message'}
|
||||||
<BaseIcon path={mdiArrowRight} size={18} />
|
<BaseIcon path={mdiArrowRight} size={18} />
|
||||||
</button>
|
</button>
|
||||||
<p className="text-[10px] leading-4 text-slate-400">
|
|
||||||
Saved automatically. Enter sends, Shift+Enter adds a new line.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1301,26 +1339,26 @@ export default function WorkspaceShell() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-[#fcfcfd] px-5 py-6 md:px-7 md:py-7">
|
<div className="bg-[#fcfcfd] px-4 py-4 md:px-5 md:py-5">
|
||||||
<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="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-4 flex h-12 w-12 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-100 text-slate-600">
|
<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} />
|
<BaseIcon path={mdiChatOutline} size={22} />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
||||||
New chat
|
New chat
|
||||||
</p>
|
</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.
|
Start with one clear ask.
|
||||||
</h2>
|
</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
|
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.
|
below. The first send creates the conversation and adds it to Recent automatically.
|
||||||
</p>
|
</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) => (
|
{emptyStateConfig.prompts.map((prompt) => (
|
||||||
<button
|
<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}
|
key={prompt}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setComposer(prompt);
|
setComposer(prompt);
|
||||||
@ -1333,16 +1371,16 @@ export default function WorkspaceShell() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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">
|
<div className="min-w-0">
|
||||||
<p className="text-[11px] uppercase tracking-[0.24em] text-slate-400">Selected agent</p>
|
<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'}
|
{emptyStateAgent?.name || 'Workspace assistant'}
|
||||||
</p>
|
</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}
|
{emptyStateAgent?.description || emptyStateConfig.helper}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-2.5 flex flex-wrap gap-2">
|
||||||
{emptyStateConfig.bestFor.map((item) => (
|
{emptyStateConfig.bestFor.map((item) => (
|
||||||
<span
|
<span
|
||||||
className="inline-flex rounded-[8px] border border-slate-200 bg-white px-2 py-1 text-[10px] text-slate-600"
|
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>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-2.5">
|
||||||
<label className="text-sm text-slate-600">
|
<label className="text-sm text-slate-600">
|
||||||
<span className="mb-2 block text-[10px] uppercase tracking-[0.24em] text-slate-400">
|
<span className="mb-2 block text-[10px] uppercase tracking-[0.24em] text-slate-400">
|
||||||
Start with agent
|
Start with agent
|
||||||
@ -1372,7 +1410,7 @@ export default function WorkspaceShell() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<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}
|
disabled={!composer.trim() || sending || loadingBootstrap}
|
||||||
onClick={() => void handleSendMessage()}
|
onClick={() => void handleSendMessage()}
|
||||||
type="button"
|
type="button"
|
||||||
@ -1383,13 +1421,13 @@ export default function WorkspaceShell() {
|
|||||||
</div>
|
</div>
|
||||||
</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-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.
|
Press Enter to start. Use Shift+Enter for a new line.
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<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)}
|
onChange={(event) => setComposer(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter' && !event.shiftKey) {
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
@ -1408,6 +1446,7 @@ export default function WorkspaceShell() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply pt-12 xl:pl-72 h-full;
|
@apply pt-12 xl:pl-64 h-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import {
|
|||||||
mdiForwardburger,
|
mdiForwardburger,
|
||||||
mdiBackburger,
|
mdiBackburger,
|
||||||
mdiMenu,
|
mdiMenu,
|
||||||
mdiAccountCircleOutline,
|
|
||||||
mdiChevronDown,
|
mdiChevronDown,
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
mdiLogout,
|
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({
|
export default function LayoutAuthenticated({
|
||||||
children,
|
children,
|
||||||
|
|
||||||
@ -79,6 +102,8 @@ export default function LayoutAuthenticated({
|
|||||||
const [isAsideLgActive, setIsAsideLgActive] = useState(false)
|
const [isAsideLgActive, setIsAsideLgActive] = useState(false)
|
||||||
const [isWorkspaceAccountMenuOpen, setIsWorkspaceAccountMenuOpen] = useState(false)
|
const [isWorkspaceAccountMenuOpen, setIsWorkspaceAccountMenuOpen] = useState(false)
|
||||||
const workspaceAccountMenuButtonRef = useRef(null)
|
const workspaceAccountMenuButtonRef = useRef(null)
|
||||||
|
const currentUserAvatarUrl = getAvatarUrl(currentUser?.avatar)
|
||||||
|
const currentUserInitial = getUserInitial(currentUser)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChangeStart = () => {
|
const handleRouteChangeStart = () => {
|
||||||
@ -97,8 +122,8 @@ export default function LayoutAuthenticated({
|
|||||||
}, [router.events, dispatch])
|
}, [router.events, dispatch])
|
||||||
|
|
||||||
|
|
||||||
const layoutAsidePadding = isWorkspaceRoute ? '' : 'xl:pl-72'
|
const layoutAsidePadding = isWorkspaceRoute ? '' : 'xl:pl-64'
|
||||||
const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''
|
const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-64 lg:ml-0' : ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
||||||
@ -129,7 +154,7 @@ export default function LayoutAuthenticated({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Link
|
<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"
|
href="/workspace"
|
||||||
>
|
>
|
||||||
AI Chat Workspace
|
AI Chat Workspace
|
||||||
@ -137,12 +162,24 @@ export default function LayoutAuthenticated({
|
|||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<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)}
|
onClick={() => setIsWorkspaceAccountMenuOpen((previous) => !previous)}
|
||||||
ref={workspaceAccountMenuButtonRef}
|
ref={workspaceAccountMenuButtonRef}
|
||||||
type="button"
|
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">
|
<span className="hidden max-w-[220px] truncate md:inline">
|
||||||
{currentUser?.email || 'Workspace user'}
|
{currentUser?.email || 'Workspace user'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import Head from 'next/head';
|
|||||||
import { store } from '../stores/store';
|
import { store } from '../stores/store';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import '../css/main.css';
|
import '../css/main.css';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { baseURLApi } from '../config';
|
import { baseURLApi } from '../config';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@ -185,6 +187,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
<ToastContainer position="bottom-center" />
|
||||||
<IntroGuide
|
<IntroGuide
|
||||||
steps={steps}
|
steps={steps}
|
||||||
stepsName={stepName}
|
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 { uniqueId } from 'lodash';
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useState } from 'react';
|
||||||
import CardBox from '../../components/CardBox'
|
import axios from 'axios';
|
||||||
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 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 AgentsTablesPage = () => {
|
||||||
const [filterItems, setFilterItems] = useState([]);
|
const [filterItems, setFilterItems] = useState([]);
|
||||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||||
const [isModalActive, setIsModalActive] = useState(false);
|
const [isModalActive, setIsModalActive] = useState(false);
|
||||||
const [showTableView, setShowTableView] = useState(false);
|
|
||||||
|
|
||||||
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const [filters] = useState([
|
||||||
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: 'Name', title: 'name' },
|
||||||
{label: 'Maxoutputtokens', title: 'max_output_tokens', number: 'true'},
|
{ label: 'Description', title: 'description' },
|
||||||
{label: 'Temperature', title: 'temperature', number: 'true'},
|
{ 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 hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_AGENTS');
|
||||||
const newItem = {
|
|
||||||
id: uniqueId(),
|
const addFilter = () => {
|
||||||
fields: {
|
const newItem = {
|
||||||
filterValue: '',
|
id: uniqueId(),
|
||||||
filterValueFrom: '',
|
fields: {
|
||||||
filterValueTo: '',
|
filterValue: '',
|
||||||
selectedField: '',
|
filterValueFrom: '',
|
||||||
},
|
filterValueTo: '',
|
||||||
};
|
selectedField: filters[0].title,
|
||||||
newItem.fields.selectedField = filters[0].title;
|
},
|
||||||
setFilterItems([...filterItems, newItem]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAgentsCSV = async () => {
|
setFilterItems([...filterItems, newItem]);
|
||||||
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()
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModalConfirm = async () => {
|
const getAgentsCSV = async () => {
|
||||||
if (!csvFile) return;
|
const response = await axios({
|
||||||
await dispatch(uploadCsv(csvFile));
|
url: '/agents?filetype=csv',
|
||||||
dispatch(setRefetch(true));
|
method: 'GET',
|
||||||
setCsvFile(null);
|
responseType: 'blob',
|
||||||
setIsModalActive(false);
|
});
|
||||||
};
|
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 = () => {
|
const onModalConfirm = async () => {
|
||||||
setCsvFile(null);
|
if (!csvFile) {
|
||||||
setIsModalActive(false);
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
await dispatch(uploadCsv(csvFile));
|
||||||
|
dispatch(setRefetch(true));
|
||||||
|
setCsvFile(null);
|
||||||
|
setIsModalActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onModalCancel = () => {
|
||||||
|
setCsvFile(null);
|
||||||
|
setIsModalActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -89,74 +90,77 @@ const AgentsTablesPage = () => {
|
|||||||
<title>{getPageTitle('Agents')}</title>
|
<title>{getPageTitle('Agents')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Agents" main>
|
<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">
|
||||||
</SectionTitleLineWithButton>
|
<div className="min-w-0">
|
||||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
||||||
|
Admin
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/agents/agents-new'} color='info' label='New Item'/>}
|
</p>
|
||||||
|
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
|
||||||
<BaseButton
|
Agents
|
||||||
className={'mr-3'}
|
</h1>
|
||||||
color='info'
|
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
|
||||||
label='Filter'
|
Manage assistant presets, models, and prompt behavior used across the workspace.
|
||||||
onClick={addFilter}
|
</p>
|
||||||
/>
|
</div>
|
||||||
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getAgentsCSV} />
|
|
||||||
|
|
||||||
{hasCreatePermission && (
|
{hasCreatePermission && (
|
||||||
<BaseButton
|
<Link className={primaryActionClassName} href="/agents/agents-new">
|
||||||
color='info'
|
New agent
|
||||||
label='Upload CSV'
|
</Link>
|
||||||
onClick={() => setIsModalActive(true)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
<div className='md:inline-flex items-center ms-auto'>
|
</div>
|
||||||
<div id='delete-rows-button'></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">
|
||||||
</CardBox>
|
Add filter
|
||||||
|
</button>
|
||||||
<CardBox className="mb-6" hasTable>
|
<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
|
<TableAgents
|
||||||
filterItems={filterItems}
|
filterItems={filterItems}
|
||||||
setFilterItems={setFilterItems}
|
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
setFilterItems={setFilterItems}
|
||||||
showGrid={false}
|
showGrid={false}
|
||||||
/>
|
/>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
<CardBoxModal
|
<CardBoxModal
|
||||||
title='Upload CSV'
|
buttonColor="info"
|
||||||
buttonColor='info'
|
buttonLabel="Confirm"
|
||||||
buttonLabel={'Confirm'}
|
isActive={isModalActive}
|
||||||
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
|
onCancel={onModalCancel}
|
||||||
isActive={isModalActive}
|
onConfirm={onModalConfirm}
|
||||||
onConfirm={onModalConfirm}
|
title="Upload CSV"
|
||||||
onCancel={onModalCancel}
|
|
||||||
>
|
>
|
||||||
<DragDropFilePicker
|
<DragDropFilePicker file={csvFile} formats=".csv" setFile={setCsvFile} />
|
||||||
file={csvFile}
|
|
||||||
setFile={setCsvFile}
|
|
||||||
formats={'.csv'}
|
|
||||||
/>
|
|
||||||
</CardBoxModal>
|
</CardBoxModal>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
AgentsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
AgentsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <LayoutAuthenticated permission="READ_AGENTS">{page}</LayoutAuthenticated>;
|
||||||
<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 { uniqueId } from 'lodash';
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useState } from 'react';
|
||||||
import CardBox from '../../components/CardBox'
|
import axios from 'axios';
|
||||||
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 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 ConversationsTablesPage = () => {
|
||||||
const [filterItems, setFilterItems] = useState([]);
|
const [filterItems, setFilterItems] = useState([]);
|
||||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
|
||||||
const [isModalActive, setIsModalActive] = useState(false);
|
|
||||||
const [showTableView, setShowTableView] = useState(false);
|
|
||||||
|
|
||||||
|
const [filters] = useState([
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
{ label: 'Title', title: 'title' },
|
||||||
|
{ label: 'Summary', title: 'summary' },
|
||||||
|
{ label: 'ClientcontextJSON', title: 'client_context_json' },
|
||||||
const dispatch = useAppDispatch();
|
{ label: 'Lastmessageat', title: 'last_message_at', date: 'true' },
|
||||||
|
{ label: 'User', title: 'user' },
|
||||||
|
{ label: 'Agent', title: 'agent' },
|
||||||
const [filters] = useState([{label: 'Title', title: 'title'},{label: 'Summary', title: 'summary'},{label: 'ClientcontextJSON', title: 'client_context_json'},
|
{ label: 'Status', title: 'status', type: 'enum', options: ['active', 'archived', 'deleted'] },
|
||||||
|
|
||||||
|
|
||||||
{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 addFilter = () => {
|
||||||
const newItem = {
|
const newItem = {
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
fields: {
|
fields: {
|
||||||
filterValue: '',
|
filterValue: '',
|
||||||
filterValueFrom: '',
|
filterValueFrom: '',
|
||||||
filterValueTo: '',
|
filterValueTo: '',
|
||||||
selectedField: '',
|
selectedField: filters[0].title,
|
||||||
},
|
},
|
||||||
};
|
|
||||||
newItem.fields.selectedField = filters[0].title;
|
|
||||||
setFilterItems([...filterItems, newItem]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getConversationsCSV = async () => {
|
setFilterItems([...filterItems, newItem]);
|
||||||
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()
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModalConfirm = async () => {
|
const getConversationsCSV = async () => {
|
||||||
if (!csvFile) return;
|
const response = await axios({
|
||||||
await dispatch(uploadCsv(csvFile));
|
url: '/conversations?filetype=csv',
|
||||||
dispatch(setRefetch(true));
|
method: 'GET',
|
||||||
setCsvFile(null);
|
responseType: 'blob',
|
||||||
setIsModalActive(false);
|
});
|
||||||
};
|
const type = response.headers['content-type'];
|
||||||
|
const blob = new Blob([response.data], { type });
|
||||||
const onModalCancel = () => {
|
const link = document.createElement('a');
|
||||||
setCsvFile(null);
|
link.href = window.URL.createObjectURL(blob);
|
||||||
setIsModalActive(false);
|
link.download = 'conversationsCSV.csv';
|
||||||
};
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -97,74 +59,47 @@ const ConversationsTablesPage = () => {
|
|||||||
<title>{getPageTitle('Conversations')}</title>
|
<title>{getPageTitle('Conversations')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Conversations" main>
|
<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>
|
||||||
</SectionTitleLineWithButton>
|
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
|
||||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
Conversations
|
||||||
|
</h1>
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/conversations/conversations-new'} color='info' label='New Item'/>}
|
<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.
|
||||||
<BaseButton
|
</p>
|
||||||
className={'mr-3'}
|
</div>
|
||||||
color='info'
|
|
||||||
label='Filter'
|
<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">
|
||||||
onClick={addFilter}
|
<button className={secondaryActionClassName} onClick={addFilter} type="button">
|
||||||
/>
|
Add filter
|
||||||
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getConversationsCSV} />
|
</button>
|
||||||
|
<button
|
||||||
{hasCreatePermission && (
|
className={secondaryActionClassName}
|
||||||
<BaseButton
|
onClick={() => void getConversationsCSV()}
|
||||||
color='info'
|
type="button"
|
||||||
label='Upload CSV'
|
>
|
||||||
onClick={() => setIsModalActive(true)}
|
Download CSV
|
||||||
/>
|
</button>
|
||||||
)}
|
<div className="ml-auto flex items-center">
|
||||||
|
<div id="delete-rows-button" />
|
||||||
<div className='md:inline-flex items-center ms-auto'>
|
</div>
|
||||||
<div id='delete-rows-button'></div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<CardBox className="mb-6 rounded-[10px] border border-slate-200 bg-white shadow-none" hasTable>
|
||||||
</CardBox>
|
|
||||||
|
|
||||||
<CardBox className="mb-6" hasTable>
|
|
||||||
<TableConversations
|
<TableConversations
|
||||||
filterItems={filterItems}
|
filterItems={filterItems}
|
||||||
setFilterItems={setFilterItems}
|
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
setFilterItems={setFilterItems}
|
||||||
showGrid={false}
|
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) {
|
ConversationsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <LayoutAuthenticated permission="READ_CONVERSATIONS">{page}</LayoutAuthenticated>;
|
||||||
<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 { uniqueId } from 'lodash';
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useState } from 'react';
|
||||||
import CardBox from '../../components/CardBox'
|
import axios from 'axios';
|
||||||
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 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 MessagesTablesPage = () => {
|
||||||
const [filterItems, setFilterItems] = useState([]);
|
const [filterItems, setFilterItems] = useState([]);
|
||||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
|
||||||
const [isModalActive, setIsModalActive] = useState(false);
|
|
||||||
const [showTableView, setShowTableView] = useState(false);
|
|
||||||
|
|
||||||
|
const [filters] = useState([
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
{ label: 'Content', title: 'content' },
|
||||||
|
{ label: 'Contentmarkdown', title: 'content_markdown' },
|
||||||
|
{ label: 'Toolname', title: 'tool_name' },
|
||||||
const dispatch = useAppDispatch();
|
{ label: 'ToolcallJSON', title: 'tool_call_json' },
|
||||||
|
{ label: 'ToolresultJSON', title: 'tool_result_json' },
|
||||||
|
{ label: 'Sequence', title: 'sequence', number: 'true' },
|
||||||
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: 'Sentat', title: 'sent_at', date: 'true' },
|
||||||
{label: 'Sequence', title: 'sequence', number: 'true'},
|
{ label: 'Completedat', title: 'completed_at', date: 'true' },
|
||||||
|
{ label: 'Conversation', title: 'conversation' },
|
||||||
{label: 'Sentat', title: 'sent_at', date: 'true'},{label: 'Completedat', title: 'completed_at', date: 'true'},
|
{ label: 'Authoruser', title: 'author_user' },
|
||||||
|
{ label: 'Role', title: 'role', type: 'enum', options: ['user', 'assistant', 'system', 'tool'] },
|
||||||
|
{
|
||||||
{label: 'Conversation', title: 'conversation'},
|
label: 'Deliverystatus',
|
||||||
|
title: 'delivery_status',
|
||||||
|
type: 'enum',
|
||||||
|
options: ['draft', 'sent', 'streaming', 'completed', 'failed'],
|
||||||
{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 addFilter = () => {
|
||||||
const newItem = {
|
const newItem = {
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
fields: {
|
fields: {
|
||||||
filterValue: '',
|
filterValue: '',
|
||||||
filterValueFrom: '',
|
filterValueFrom: '',
|
||||||
filterValueTo: '',
|
filterValueTo: '',
|
||||||
selectedField: '',
|
selectedField: filters[0].title,
|
||||||
},
|
},
|
||||||
};
|
|
||||||
newItem.fields.selectedField = filters[0].title;
|
|
||||||
setFilterItems([...filterItems, newItem]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMessagesCSV = async () => {
|
setFilterItems([...filterItems, newItem]);
|
||||||
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()
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModalConfirm = async () => {
|
const getMessagesCSV = async () => {
|
||||||
if (!csvFile) return;
|
const response = await axios({
|
||||||
await dispatch(uploadCsv(csvFile));
|
url: '/messages?filetype=csv',
|
||||||
dispatch(setRefetch(true));
|
method: 'GET',
|
||||||
setCsvFile(null);
|
responseType: 'blob',
|
||||||
setIsModalActive(false);
|
});
|
||||||
};
|
const type = response.headers['content-type'];
|
||||||
|
const blob = new Blob([response.data], { type });
|
||||||
const onModalCancel = () => {
|
const link = document.createElement('a');
|
||||||
setCsvFile(null);
|
link.href = window.URL.createObjectURL(blob);
|
||||||
setIsModalActive(false);
|
link.download = 'messagesCSV.csv';
|
||||||
};
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -97,74 +69,47 @@ const MessagesTablesPage = () => {
|
|||||||
<title>{getPageTitle('Messages')}</title>
|
<title>{getPageTitle('Messages')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Messages" main>
|
<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>
|
||||||
</SectionTitleLineWithButton>
|
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
|
||||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
Messages
|
||||||
|
</h1>
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/messages/messages-new'} color='info' label='New Item'/>}
|
<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.
|
||||||
<BaseButton
|
</p>
|
||||||
className={'mr-3'}
|
</div>
|
||||||
color='info'
|
|
||||||
label='Filter'
|
<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">
|
||||||
onClick={addFilter}
|
<button className={secondaryActionClassName} onClick={addFilter} type="button">
|
||||||
/>
|
Add filter
|
||||||
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getMessagesCSV} />
|
</button>
|
||||||
|
<button
|
||||||
{hasCreatePermission && (
|
className={secondaryActionClassName}
|
||||||
<BaseButton
|
onClick={() => void getMessagesCSV()}
|
||||||
color='info'
|
type="button"
|
||||||
label='Upload CSV'
|
>
|
||||||
onClick={() => setIsModalActive(true)}
|
Download CSV
|
||||||
/>
|
</button>
|
||||||
)}
|
<div className="ml-auto flex items-center">
|
||||||
|
<div id="delete-rows-button" />
|
||||||
<div className='md:inline-flex items-center ms-auto'>
|
</div>
|
||||||
<div id='delete-rows-button'></div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<CardBox className="mb-6 rounded-[10px] border border-slate-200 bg-white shadow-none" hasTable>
|
||||||
</CardBox>
|
|
||||||
|
|
||||||
<CardBox className="mb-6" hasTable>
|
|
||||||
<TableMessages
|
<TableMessages
|
||||||
filterItems={filterItems}
|
filterItems={filterItems}
|
||||||
setFilterItems={setFilterItems}
|
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
setFilterItems={setFilterItems}
|
||||||
showGrid={false}
|
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) {
|
MessagesTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <LayoutAuthenticated permission="READ_MESSAGES">{page}</LayoutAuthenticated>;
|
||||||
<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 { uniqueId } from 'lodash';
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useState } from 'react';
|
||||||
import CardBox from '../../components/CardBox'
|
import axios from 'axios';
|
||||||
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 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 PermissionsTablesPage = () => {
|
||||||
const [filterItems, setFilterItems] = useState([]);
|
const [filterItems, setFilterItems] = useState([]);
|
||||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
|
||||||
const [isModalActive, setIsModalActive] = useState(false);
|
|
||||||
const [showTableView, setShowTableView] = useState(false);
|
|
||||||
|
|
||||||
|
const [filters] = useState([{ label: 'Name', title: 'name' }]);
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
|
||||||
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const addFilter = () => {
|
||||||
|
const newItem = {
|
||||||
|
id: uniqueId(),
|
||||||
const [filters] = useState([{label: 'Name', title: 'name'},
|
fields: {
|
||||||
|
filterValue: '',
|
||||||
|
filterValueFrom: '',
|
||||||
|
filterValueTo: '',
|
||||||
|
selectedField: filters[0].title,
|
||||||
|
},
|
||||||
|
|
||||||
]);
|
|
||||||
|
|
||||||
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 getPermissionsCSV = async () => {
|
setFilterItems([...filterItems, newItem]);
|
||||||
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()
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModalConfirm = async () => {
|
const getPermissionsCSV = async () => {
|
||||||
if (!csvFile) return;
|
const response = await axios({
|
||||||
await dispatch(uploadCsv(csvFile));
|
url: '/permissions?filetype=csv',
|
||||||
dispatch(setRefetch(true));
|
method: 'GET',
|
||||||
setCsvFile(null);
|
responseType: 'blob',
|
||||||
setIsModalActive(false);
|
});
|
||||||
};
|
const type = response.headers['content-type'];
|
||||||
|
const blob = new Blob([response.data], { type });
|
||||||
const onModalCancel = () => {
|
const link = document.createElement('a');
|
||||||
setCsvFile(null);
|
link.href = window.URL.createObjectURL(blob);
|
||||||
setIsModalActive(false);
|
link.download = 'permissionsCSV.csv';
|
||||||
};
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -89,74 +51,49 @@ const PermissionsTablesPage = () => {
|
|||||||
<title>{getPageTitle('Permissions')}</title>
|
<title>{getPageTitle('Permissions')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Permissions" main>
|
<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">
|
||||||
</SectionTitleLineWithButton>
|
Access control
|
||||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
</p>
|
||||||
|
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/permissions/permissions-new'} color='info' label='New Item'/>}
|
Permissions
|
||||||
|
</h1>
|
||||||
<BaseButton
|
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
|
||||||
className={'mr-3'}
|
Inspect the low-level capability map that powers admin access throughout the app.
|
||||||
color='info'
|
</p>
|
||||||
label='Filter'
|
</div>
|
||||||
onClick={addFilter}
|
|
||||||
/>
|
<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">
|
||||||
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getPermissionsCSV} />
|
<button className={secondaryActionClassName} onClick={addFilter} type="button">
|
||||||
|
Add filter
|
||||||
{hasCreatePermission && (
|
</button>
|
||||||
<BaseButton
|
<button
|
||||||
color='info'
|
className={secondaryActionClassName}
|
||||||
label='Upload CSV'
|
onClick={() => void getPermissionsCSV()}
|
||||||
onClick={() => setIsModalActive(true)}
|
type="button"
|
||||||
/>
|
>
|
||||||
)}
|
Download CSV
|
||||||
|
</button>
|
||||||
<div className='md:inline-flex items-center ms-auto'>
|
<div className="ml-auto flex items-center">
|
||||||
<div id='delete-rows-button'></div>
|
<div id="delete-rows-button" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</CardBox>
|
|
||||||
|
<CardBox className="mb-6 rounded-[10px] border border-slate-200 bg-white shadow-none" hasTable>
|
||||||
<CardBox className="mb-6" hasTable>
|
|
||||||
<TablePermissions
|
<TablePermissions
|
||||||
filterItems={filterItems}
|
filterItems={filterItems}
|
||||||
setFilterItems={setFilterItems}
|
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
setFilterItems={setFilterItems}
|
||||||
showGrid={false}
|
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) {
|
PermissionsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <LayoutAuthenticated permission="READ_PERMISSIONS">{page}</LayoutAuthenticated>;
|
||||||
<LayoutAuthenticated
|
};
|
||||||
|
|
||||||
permission={'READ_PERMISSIONS'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</LayoutAuthenticated>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PermissionsTablesPage
|
export default PermissionsTablesPage;
|
||||||
|
|||||||
@ -1,153 +1,325 @@
|
|||||||
import {
|
import {
|
||||||
mdiChartTimelineVariant,
|
mdiAccountCircleOutline,
|
||||||
mdiUpload,
|
mdiArrowLeft,
|
||||||
|
mdiCogOutline,
|
||||||
|
mdiLockOutline,
|
||||||
|
mdiPhoneOutline,
|
||||||
|
mdiUpload,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import Link from 'next/link';
|
||||||
import React, { ReactElement, useEffect, useState } from 'react';
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import CardBox from '../components/CardBox';
|
import BaseIcon from '../components/BaseIcon';
|
||||||
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 FormImagePicker from '../components/FormImagePicker';
|
import FormImagePicker from '../components/FormImagePicker';
|
||||||
|
import SectionMain from '../components/SectionMain';
|
||||||
import { update } from '../stores/users/usersSlice';
|
import { getPageTitle } from '../config';
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
import { findMe } from '../stores/authSlice';
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
import { useRouter } from 'next/router';
|
import { update } from '../stores/users/usersSlice';
|
||||||
import {findMe} from "../stores/authSlice";
|
|
||||||
|
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 ProfilePage = () => {
|
||||||
const { currentUser } = useAppSelector(
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
(state) => state.auth,
|
const dispatch = useAppDispatch();
|
||||||
);
|
const [initialValues, setInitialValues] = useState(initialFormValues);
|
||||||
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);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser?.id && typeof currentUser === 'object') {
|
if (!currentUser?.id || typeof currentUser !== 'object') {
|
||||||
const newInitialVal = { ...initVals };
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Object.keys(initVals).forEach(
|
setInitialValues({
|
||||||
(el) => (newInitialVal[el] = currentUser[el]),
|
firstName: currentUser.firstName || '',
|
||||||
);
|
lastName: currentUser.lastName || '',
|
||||||
|
phoneNumber: currentUser.phoneNumber || '',
|
||||||
|
email: currentUser.email || '',
|
||||||
|
avatar: currentUser.avatar || [],
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
}, [currentUser]);
|
||||||
|
|
||||||
setInitialValues(newInitialVal);
|
const handleSubmit = async (data: typeof initialFormValues) => {
|
||||||
}
|
await dispatch(update({ id: currentUser.id, data }));
|
||||||
}, [currentUser]);
|
await dispatch(findMe());
|
||||||
|
toast('Profile updated.', { type: 'success', position: 'bottom-center' });
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (data) => {
|
return (
|
||||||
await dispatch(update({ id: currentUser.id, data }));
|
<>
|
||||||
await dispatch(findMe());
|
<Head>
|
||||||
await router.push('/settings');
|
<title>{getPageTitle('Profile')}</title>
|
||||||
notify('success', 'Profile was updated!');
|
</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 (
|
<Formik
|
||||||
<>
|
enableReinitialize
|
||||||
<Head>
|
initialValues={initialValues}
|
||||||
<title>{getPageTitle('Profile')}</title>
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
</Head>
|
>
|
||||||
<SectionMain>
|
{({ isSubmitting, resetForm, setFieldValue, values }) => {
|
||||||
<SectionTitleLineWithButton
|
const previewUrl = getAvatarUrl(values.avatar);
|
||||||
icon={mdiChartTimelineVariant}
|
const initials = getProfileInitials(currentUser, values);
|
||||||
title='Profile'
|
|
||||||
main
|
return (
|
||||||
>
|
<Form>
|
||||||
{''}
|
<div className="grid gap-5 xl:grid-cols-[280px_minmax(0,1fr)]">
|
||||||
</SectionTitleLineWithButton>
|
<div className="space-y-5">
|
||||||
<CardBox>
|
<div className="rounded-[12px] border border-slate-200 bg-white px-5 py-5">
|
||||||
{currentUser?.avatar[0]?.publicUrl && <div className={'grid grid-cols-6 gap-4 mb-4'}>
|
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
||||||
<div className="col-span-1 w-80 h-80 overflow-hidden border-2 rounded-full inline-flex items-center justify-center mb-8">
|
Account
|
||||||
<img className="w-80 h-80 max-w-full max-h-full object-cover object-center" src={`${currentUser?.avatar[0]?.publicUrl}`} alt="Avatar" />
|
</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>
|
||||||
</div>}
|
<div className="mt-5">
|
||||||
<Formik
|
<span className={labelClassName}>Avatar</span>
|
||||||
enableReinitialize
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
initialValues={initialValues}
|
<Field
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
color="info"
|
||||||
>
|
component={FormImagePicker}
|
||||||
<Form>
|
icon={mdiUpload}
|
||||||
<FormField>
|
id="avatar"
|
||||||
<Field
|
label="Upload image"
|
||||||
label='Avatar'
|
name="avatar"
|
||||||
color='info'
|
path="users/avatar"
|
||||||
icon={mdiUpload}
|
schema={{
|
||||||
path={'users/avatar'}
|
size: undefined,
|
||||||
name='avatar'
|
formats: undefined,
|
||||||
id='avatar'
|
}}
|
||||||
schema={{
|
/>
|
||||||
size: undefined,
|
{previewUrl && (
|
||||||
formats: undefined,
|
<button
|
||||||
}}
|
className={`${actionButtonClassName} border-slate-200 bg-white text-slate-600`}
|
||||||
component={FormImagePicker}
|
onClick={() => {
|
||||||
></Field>
|
setFieldValue('avatar', []);
|
||||||
</FormField>
|
}}
|
||||||
<FormField label='First Name'>
|
type="button"
|
||||||
<Field name='firstName' placeholder='First Name' />
|
>
|
||||||
</FormField>
|
Remove avatar
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField label='Last Name'>
|
<div className="rounded-[12px] border border-slate-200 bg-white px-5 py-5">
|
||||||
<Field name='lastName' placeholder='Last Name' />
|
<div className="flex items-start gap-3">
|
||||||
</FormField>
|
<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'>
|
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-6">
|
||||||
<Field name='phoneNumber' placeholder='Phone Number' />
|
<div className="mb-5 flex items-start gap-3">
|
||||||
</FormField>
|
<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'>
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<Field name='email' placeholder='E-Mail' disabled />
|
<div>
|
||||||
</FormField>
|
<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
|
<div className="mt-6 rounded-[12px] border border-slate-200 bg-slate-50 px-4 py-4">
|
||||||
label="Password"
|
<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">
|
||||||
<Field
|
<BaseIcon path={mdiCogOutline} size={18} />
|
||||||
name="password"
|
</div>
|
||||||
placeholder="password"
|
<div>
|
||||||
/>
|
<p className="text-[15px] font-medium text-slate-900">Security</p>
|
||||||
</FormField>
|
<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 />
|
<div className="mt-6 flex flex-wrap items-center gap-3 border-t border-slate-200 pt-5">
|
||||||
|
<button
|
||||||
<BaseButtons>
|
className={`${actionButtonClassName} border-slate-900 bg-slate-900 text-white disabled:cursor-not-allowed disabled:opacity-60`}
|
||||||
<BaseButton type='submit' color='info' label='Submit' />
|
disabled={isSubmitting}
|
||||||
<BaseButton type='reset' color='info' outline label='Reset' />
|
type="submit"
|
||||||
<BaseButton
|
>
|
||||||
type='reset'
|
{isSubmitting ? 'Saving...' : 'Save changes'}
|
||||||
color='danger'
|
</button>
|
||||||
outline
|
<button
|
||||||
label='Cancel'
|
className={`${actionButtonClassName} border-slate-200 bg-white text-slate-700`}
|
||||||
onClick={() => router.push('/settings')}
|
onClick={() => resetForm()}
|
||||||
/>
|
type="reset"
|
||||||
</BaseButtons>
|
>
|
||||||
</Form>
|
Reset
|
||||||
</Formik>
|
</button>
|
||||||
</CardBox>
|
<Link
|
||||||
</SectionMain>
|
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) {
|
ProfilePage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProfilePage;
|
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 { uniqueId } from 'lodash';
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useState } from 'react';
|
||||||
import CardBox from '../../components/CardBox'
|
import axios from 'axios';
|
||||||
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 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 RolesTablesPage = () => {
|
||||||
const [filterItems, setFilterItems] = useState([]);
|
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 { 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 hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ROLES');
|
||||||
const newItem = {
|
|
||||||
id: uniqueId(),
|
const addFilter = () => {
|
||||||
fields: {
|
const newItem = {
|
||||||
filterValue: '',
|
id: uniqueId(),
|
||||||
filterValueFrom: '',
|
fields: {
|
||||||
filterValueTo: '',
|
filterValue: '',
|
||||||
selectedField: '',
|
filterValueFrom: '',
|
||||||
},
|
filterValueTo: '',
|
||||||
};
|
selectedField: filters[0].title,
|
||||||
newItem.fields.selectedField = filters[0].title;
|
},
|
||||||
setFilterItems([...filterItems, newItem]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRolesCSV = async () => {
|
setFilterItems([...filterItems, newItem]);
|
||||||
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()
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModalConfirm = async () => {
|
const getRolesCSV = async () => {
|
||||||
if (!csvFile) return;
|
const response = await axios({
|
||||||
await dispatch(uploadCsv(csvFile));
|
url: '/roles?filetype=csv',
|
||||||
dispatch(setRefetch(true));
|
method: 'GET',
|
||||||
setCsvFile(null);
|
responseType: 'blob',
|
||||||
setIsModalActive(false);
|
});
|
||||||
};
|
const type = response.headers['content-type'];
|
||||||
|
const blob = new Blob([response.data], { type });
|
||||||
const onModalCancel = () => {
|
const link = document.createElement('a');
|
||||||
setCsvFile(null);
|
link.href = window.URL.createObjectURL(blob);
|
||||||
setIsModalActive(false);
|
link.download = 'rolesCSV.csv';
|
||||||
};
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -89,74 +63,58 @@ const RolesTablesPage = () => {
|
|||||||
<title>{getPageTitle('Roles')}</title>
|
<title>{getPageTitle('Roles')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Roles" main>
|
<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">
|
||||||
</SectionTitleLineWithButton>
|
<div className="min-w-0">
|
||||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
||||||
|
Access control
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/roles/roles-new'} color='info' label='New Item'/>}
|
</p>
|
||||||
|
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
|
||||||
<BaseButton
|
Roles
|
||||||
className={'mr-3'}
|
</h1>
|
||||||
color='info'
|
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
|
||||||
label='Filter'
|
Group permissions into a few clear access levels and keep role names readable.
|
||||||
onClick={addFilter}
|
</p>
|
||||||
/>
|
</div>
|
||||||
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getRolesCSV} />
|
|
||||||
|
|
||||||
{hasCreatePermission && (
|
{hasCreatePermission && (
|
||||||
<BaseButton
|
<Link className={primaryActionClassName} href="/roles/roles-new">
|
||||||
color='info'
|
New role
|
||||||
label='Upload CSV'
|
</Link>
|
||||||
onClick={() => setIsModalActive(true)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
<div className='md:inline-flex items-center ms-auto'>
|
</div>
|
||||||
<div id='delete-rows-button'></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">
|
||||||
</CardBox>
|
Add filter
|
||||||
|
</button>
|
||||||
<CardBox className="mb-6" hasTable>
|
<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
|
<TableRoles
|
||||||
filterItems={filterItems}
|
filterItems={filterItems}
|
||||||
setFilterItems={setFilterItems}
|
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
setFilterItems={setFilterItems}
|
||||||
showGrid={false}
|
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) {
|
RolesTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <LayoutAuthenticated permission="READ_ROLES">{page}</LayoutAuthenticated>;
|
||||||
<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 { uniqueId } from 'lodash';
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useState } from 'react';
|
||||||
import CardBox from '../../components/CardBox'
|
import axios from 'axios';
|
||||||
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 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 Usage_eventsTablesPage = () => {
|
||||||
const [filterItems, setFilterItems] = useState([]);
|
const [filterItems, setFilterItems] = useState([]);
|
||||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
|
||||||
const [isModalActive, setIsModalActive] = useState(false);
|
|
||||||
const [showTableView, setShowTableView] = useState(false);
|
|
||||||
|
|
||||||
|
const [filters] = useState([
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
{ label: 'Provider', title: 'provider' },
|
||||||
|
{ label: 'Model', title: 'model' },
|
||||||
|
{ label: 'MetadataJSON', title: 'metadata_json' },
|
||||||
const dispatch = useAppDispatch();
|
{ label: 'Inputtokens', title: 'input_tokens', number: 'true' },
|
||||||
|
{ label: 'Outputtokens', title: 'output_tokens', number: 'true' },
|
||||||
|
{ label: 'Totaltokens', title: 'total_tokens', number: 'true' },
|
||||||
const [filters] = useState([{label: 'Provider', title: 'provider'},{label: 'Model', title: 'model'},{label: 'MetadataJSON', title: 'metadata_json'},
|
{ label: 'CostUSD', title: 'cost_usd', number: 'true' },
|
||||||
{label: 'Inputtokens', title: 'input_tokens', number: 'true'},{label: 'Outputtokens', title: 'output_tokens', number: 'true'},{label: 'Totaltokens', title: 'total_tokens', number: 'true'},
|
{ label: 'Occurredat', title: 'occurred_at', date: 'true' },
|
||||||
{label: 'CostUSD', title: 'cost_usd', number: 'true'},
|
{ label: 'User', title: 'user' },
|
||||||
{label: 'Occurredat', title: 'occurred_at', date: 'true'},
|
{ label: 'Conversation', title: 'conversation' },
|
||||||
|
{ label: 'Message', title: 'message' },
|
||||||
|
{ label: 'Agent', title: 'agent' },
|
||||||
{label: 'User', title: 'user'},
|
{
|
||||||
|
label: 'Eventtype',
|
||||||
|
title: 'event_type',
|
||||||
|
type: 'enum',
|
||||||
{label: 'Conversation', title: 'conversation'},
|
options: ['message_sent', 'message_generated', 'tokens_counted', 'cost_incurred', 'attachment_uploaded'],
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
{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 addFilter = () => {
|
||||||
const newItem = {
|
const newItem = {
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
fields: {
|
fields: {
|
||||||
filterValue: '',
|
filterValue: '',
|
||||||
filterValueFrom: '',
|
filterValueFrom: '',
|
||||||
filterValueTo: '',
|
filterValueTo: '',
|
||||||
selectedField: '',
|
selectedField: filters[0].title,
|
||||||
},
|
},
|
||||||
};
|
|
||||||
newItem.fields.selectedField = filters[0].title;
|
|
||||||
setFilterItems([...filterItems, newItem]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUsage_eventsCSV = async () => {
|
setFilterItems([...filterItems, newItem]);
|
||||||
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()
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModalConfirm = async () => {
|
const getUsageEventsCSV = async () => {
|
||||||
if (!csvFile) return;
|
const response = await axios({
|
||||||
await dispatch(uploadCsv(csvFile));
|
url: '/usage_events?filetype=csv',
|
||||||
dispatch(setRefetch(true));
|
method: 'GET',
|
||||||
setCsvFile(null);
|
responseType: 'blob',
|
||||||
setIsModalActive(false);
|
});
|
||||||
};
|
const type = response.headers['content-type'];
|
||||||
|
const blob = new Blob([response.data], { type });
|
||||||
const onModalCancel = () => {
|
const link = document.createElement('a');
|
||||||
setCsvFile(null);
|
link.href = window.URL.createObjectURL(blob);
|
||||||
setIsModalActive(false);
|
link.download = 'usage_eventsCSV.csv';
|
||||||
};
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Usage_events')}</title>
|
<title>{getPageTitle('Usage events')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Usage_events" main>
|
<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>
|
||||||
</SectionTitleLineWithButton>
|
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
|
||||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
Usage events
|
||||||
|
</h1>
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/usage_events/usage_events-new'} color='info' label='New Item'/>}
|
<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.
|
||||||
<BaseButton
|
</p>
|
||||||
className={'mr-3'}
|
</div>
|
||||||
color='info'
|
|
||||||
label='Filter'
|
<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">
|
||||||
onClick={addFilter}
|
<button className={secondaryActionClassName} onClick={addFilter} type="button">
|
||||||
/>
|
Add filter
|
||||||
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getUsage_eventsCSV} />
|
</button>
|
||||||
|
<button
|
||||||
{hasCreatePermission && (
|
className={secondaryActionClassName}
|
||||||
<BaseButton
|
onClick={() => void getUsageEventsCSV()}
|
||||||
color='info'
|
type="button"
|
||||||
label='Upload CSV'
|
>
|
||||||
onClick={() => setIsModalActive(true)}
|
Download CSV
|
||||||
/>
|
</button>
|
||||||
)}
|
<div className="ml-auto flex items-center">
|
||||||
|
<div id="delete-rows-button" />
|
||||||
<div className='md:inline-flex items-center ms-auto'>
|
</div>
|
||||||
<div id='delete-rows-button'></div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<CardBox className="mb-6 rounded-[10px] border border-slate-200 bg-white shadow-none" hasTable>
|
||||||
</CardBox>
|
|
||||||
|
|
||||||
<CardBox className="mb-6" hasTable>
|
|
||||||
<TableUsage_events
|
<TableUsage_events
|
||||||
filterItems={filterItems}
|
filterItems={filterItems}
|
||||||
setFilterItems={setFilterItems}
|
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
setFilterItems={setFilterItems}
|
||||||
showGrid={false}
|
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) {
|
Usage_eventsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <LayoutAuthenticated permission="READ_USAGE_EVENTS">{page}</LayoutAuthenticated>;
|
||||||
<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 { uniqueId } from 'lodash';
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useState } from 'react';
|
||||||
import CardBox from '../../components/CardBox'
|
import axios from 'axios';
|
||||||
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 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 UsersTablesPage = () => {
|
||||||
const [filterItems, setFilterItems] = useState([]);
|
const [filterItems, setFilterItems] = useState([]);
|
||||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||||
const [isModalActive, setIsModalActive] = useState(false);
|
const [isModalActive, setIsModalActive] = useState(false);
|
||||||
const [showTableView, setShowTableView] = useState(false);
|
|
||||||
|
|
||||||
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const [filters] = useState([
|
||||||
const [filters] = useState([{label: 'First Name', title: 'firstName'},{label: 'Last Name', title: 'lastName'},{label: 'Phone Number', title: 'phoneNumber'},{label: 'E-Mail', title: 'email'},
|
{ 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' },
|
||||||
{label: 'App Role', title: 'app_role'},
|
|
||||||
|
|
||||||
|
|
||||||
{label: 'Custom Permissions', title: 'custom_permissions'},
|
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_USERS');
|
|
||||||
|
|
||||||
|
|
||||||
const addFilter = () => {
|
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_USERS');
|
||||||
const newItem = {
|
|
||||||
id: uniqueId(),
|
const addFilter = () => {
|
||||||
fields: {
|
const newItem = {
|
||||||
filterValue: '',
|
id: uniqueId(),
|
||||||
filterValueFrom: '',
|
fields: {
|
||||||
filterValueTo: '',
|
filterValue: '',
|
||||||
selectedField: '',
|
filterValueFrom: '',
|
||||||
},
|
filterValueTo: '',
|
||||||
};
|
selectedField: filters[0].title,
|
||||||
newItem.fields.selectedField = filters[0].title;
|
},
|
||||||
setFilterItems([...filterItems, newItem]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUsersCSV = async () => {
|
setFilterItems([...filterItems, newItem]);
|
||||||
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()
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModalConfirm = async () => {
|
const getUsersCSV = async () => {
|
||||||
if (!csvFile) return;
|
const response = await axios({
|
||||||
await dispatch(uploadCsv(csvFile));
|
url: '/users?filetype=csv',
|
||||||
dispatch(setRefetch(true));
|
method: 'GET',
|
||||||
setCsvFile(null);
|
responseType: 'blob',
|
||||||
setIsModalActive(false);
|
});
|
||||||
};
|
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 = () => {
|
const onModalConfirm = async () => {
|
||||||
setCsvFile(null);
|
if (!csvFile) {
|
||||||
setIsModalActive(false);
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
await dispatch(uploadCsv(csvFile));
|
||||||
|
dispatch(setRefetch(true));
|
||||||
|
setCsvFile(null);
|
||||||
|
setIsModalActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onModalCancel = () => {
|
||||||
|
setCsvFile(null);
|
||||||
|
setIsModalActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Users')}</title>
|
<title>{getPageTitle('People')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Users" main>
|
<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">
|
||||||
</SectionTitleLineWithButton>
|
<div className="min-w-0">
|
||||||
<CardBox id="usersList" className='mb-6' cardBoxClassName='flex flex-wrap'>
|
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
||||||
|
Admin
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/users/users-new'} color='info' label='Add/Invite User'/>}
|
</p>
|
||||||
|
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
|
||||||
<BaseButton
|
People
|
||||||
className={'mr-3'}
|
</h1>
|
||||||
color='info'
|
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
|
||||||
label='Filter'
|
Invite teammates, review workspace access, and keep account ownership visible.
|
||||||
onClick={addFilter}
|
</p>
|
||||||
/>
|
</div>
|
||||||
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getUsersCSV} />
|
|
||||||
|
|
||||||
{hasCreatePermission && (
|
{hasCreatePermission && (
|
||||||
<BaseButton
|
<Link className={primaryActionClassName} href="/users/users-new">
|
||||||
color='info'
|
Invite user
|
||||||
label='Upload CSV'
|
</Link>
|
||||||
onClick={() => setIsModalActive(true)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
<div className='md:inline-flex items-center ms-auto'>
|
</div>
|
||||||
<div id='delete-rows-button'></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">
|
||||||
</CardBox>
|
Add filter
|
||||||
|
</button>
|
||||||
<CardBox className="mb-6" hasTable>
|
<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
|
<TableUsers
|
||||||
filterItems={filterItems}
|
filterItems={filterItems}
|
||||||
setFilterItems={setFilterItems}
|
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
setFilterItems={setFilterItems}
|
||||||
showGrid={false}
|
showGrid={false}
|
||||||
/>
|
/>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
<CardBoxModal
|
<CardBoxModal
|
||||||
title='Upload CSV'
|
buttonColor="info"
|
||||||
buttonColor='info'
|
buttonLabel="Confirm"
|
||||||
buttonLabel={'Confirm'}
|
isActive={isModalActive}
|
||||||
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
|
onCancel={onModalCancel}
|
||||||
isActive={isModalActive}
|
onConfirm={onModalConfirm}
|
||||||
onConfirm={onModalConfirm}
|
title="Upload CSV"
|
||||||
onCancel={onModalCancel}
|
|
||||||
>
|
>
|
||||||
<DragDropFilePicker
|
<DragDropFilePicker file={csvFile} formats=".csv" setFile={setCsvFile} />
|
||||||
file={csvFile}
|
|
||||||
setFile={setCsvFile}
|
|
||||||
formats={'.csv'}
|
|
||||||
/>
|
|
||||||
</CardBoxModal>
|
</CardBoxModal>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
UsersTablesPage.getLayout = function getLayout(page: ReactElement) {
|
UsersTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <LayoutAuthenticated permission="READ_USERS">{page}</LayoutAuthenticated>;
|
||||||
<LayoutAuthenticated
|
};
|
||||||
|
|
||||||
permission={'READ_USERS'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</LayoutAuthenticated>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UsersTablesPage
|
export default UsersTablesPage;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user