mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
theme updates and better chat config components
This commit is contained in:
@@ -4,7 +4,7 @@ import { Typography } from '@mui/material';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import { useDarkMode } from './utilities/useDarkMode';
|
||||
import { createTheme, ThemeProvider, StyledEngineProvider } from '@mui/material/styles';
|
||||
import { GlobalStyles } from '../themes/GlobalStyles';
|
||||
import { GlobalStyles, ThemeVariables } from '../themes/GlobalStyles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import {FailedRefresh, mePreferences, meState, operatorSettingDefaults} from '../cache';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
@@ -939,7 +939,8 @@ export function App(props) {
|
||||
return (
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyles theme={theme} />
|
||||
<ThemeVariables theme={theme} />
|
||||
<GlobalStyles />
|
||||
<CssBaseline />
|
||||
<div style={{width: '100%', height: '100%', display: "flex", position: "relative",}}>
|
||||
<MythicLoadingState compact title="Loading Preferences" description="Fetching user preferences." sx={{color: "inherit"}} />
|
||||
@@ -958,7 +959,8 @@ export function App(props) {
|
||||
return (
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyles theme={theme} />
|
||||
<ThemeVariables theme={theme} />
|
||||
<GlobalStyles />
|
||||
<CssBaseline />
|
||||
<MeContext.Provider value={me}>
|
||||
<Tooltip id={"my-tooltip"} style={{zIndex: 100000, wordBreak: "break-word", maxWidth: "80%", whiteSpace: "pre-wrap"}}/>
|
||||
@@ -981,7 +983,7 @@ export function App(props) {
|
||||
/>
|
||||
}
|
||||
{me.loggedIn && me.user !== undefined && me.user !== null &&
|
||||
<TopAppBarVertical me={me} toggleTheme={themeToggler} />
|
||||
<TopAppBarVertical me={me} toggleTheme={themeToggler} themeMode={themeMode} />
|
||||
}
|
||||
<div style={{
|
||||
maxHeight: '100%',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import CameraAltTwoToneIcon from '@mui/icons-material/CameraAltTwoTone';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import MuiDrawer from '@mui/material/Drawer';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
@@ -90,36 +89,36 @@ const classes = {
|
||||
listSubHeader: `${PREFIX}-listSubHeader`,
|
||||
};
|
||||
|
||||
const openedMixin = (theme) => ({
|
||||
const openedMixin = () => ({
|
||||
width: drawerWidth,
|
||||
overflowX: 'hidden',
|
||||
borderRadius: "0 !important",
|
||||
border: "0px !important",
|
||||
background: `${theme.navigation.background} !important`,
|
||||
backgroundColor: `${theme.navigation.backgroundColor} !important`,
|
||||
background: "var(--mythic-nav-background) !important",
|
||||
backgroundColor: "var(--mythic-nav-background-color) !important",
|
||||
});
|
||||
const closedMixin = (theme) => ({
|
||||
const closedMixin = () => ({
|
||||
overflowX: 'hidden',
|
||||
width: "60px",
|
||||
borderRadius: "0 !important",
|
||||
border: "0px !important",
|
||||
background: `${theme.navigation.background} !important`,
|
||||
backgroundColor: `${theme.navigation.backgroundColor} !important`,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
background: "var(--mythic-nav-background) !important",
|
||||
backgroundColor: "var(--mythic-nav-background-color) !important",
|
||||
'@media (min-width: 600px)': {
|
||||
width: "60px",
|
||||
borderRadius: "0 !important",
|
||||
border: "0px !important",
|
||||
},
|
||||
});
|
||||
const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })(
|
||||
({ theme }) => ({
|
||||
() => ({
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
boxSizing: 'border-box',
|
||||
background: `${theme.navigation.background} !important`,
|
||||
backgroundColor: `${theme.navigation.backgroundColor} !important`,
|
||||
color: theme.navigation.text,
|
||||
background: "var(--mythic-nav-background) !important",
|
||||
backgroundColor: "var(--mythic-nav-background-color) !important",
|
||||
color: "var(--mythic-nav-text)",
|
||||
'& .MuiDrawer-paper': {
|
||||
border: "0 !important",
|
||||
borderRight: "0 !important",
|
||||
@@ -137,18 +136,18 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
|
||||
width: "auto",
|
||||
},
|
||||
[`& .${classes.listSubHeader}:hover`]: {
|
||||
color: `${theme.navigation.text} !important`,
|
||||
backgroundColor: `${theme.navigation.hover} !important`,
|
||||
color: "var(--mythic-nav-text) !important",
|
||||
backgroundColor: "var(--mythic-nav-hover) !important",
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
props: ({ open }) => open,
|
||||
style: {
|
||||
...openedMixin(theme),
|
||||
...openedMixin(),
|
||||
'& .MuiDrawer-paper': {
|
||||
...openedMixin(theme),
|
||||
background: `${theme.navigation.background} !important`,
|
||||
backgroundColor: `${theme.navigation.backgroundColor} !important`,
|
||||
...openedMixin(),
|
||||
background: "var(--mythic-nav-background) !important",
|
||||
backgroundColor: "var(--mythic-nav-background-color) !important",
|
||||
border: "0 !important",
|
||||
borderRight: "0 !important",
|
||||
borderRadius: "0 !important",
|
||||
@@ -164,11 +163,11 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
|
||||
{
|
||||
props: ({ open }) => !open,
|
||||
style: {
|
||||
...closedMixin(theme),
|
||||
...closedMixin(),
|
||||
'& .MuiDrawer-paper': {
|
||||
...closedMixin(theme),
|
||||
background: `${theme.navigation.background} !important`,
|
||||
backgroundColor: `${theme.navigation.backgroundColor} !important`,
|
||||
...closedMixin(),
|
||||
background: "var(--mythic-nav-background) !important",
|
||||
backgroundColor: "var(--mythic-nav-background-color) !important",
|
||||
border: "0 !important",
|
||||
borderRight: "0 !important",
|
||||
borderRadius: "0 !important",
|
||||
@@ -196,15 +195,15 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
|
||||
}),
|
||||
);
|
||||
export const StyledListItem = styled(ListItem)(
|
||||
({ theme }) => ({
|
||||
() => ({
|
||||
minHeight: "34px",
|
||||
paddingTop: "4px",
|
||||
paddingLeft: "8px",
|
||||
paddingRight: "8px",
|
||||
margin: "2px 4px",
|
||||
paddingBottom: "4px",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
color: theme.navigation.text,
|
||||
borderRadius: "var(--mythic-radius)",
|
||||
color: "var(--mythic-nav-text)",
|
||||
backgroundColor: "transparent !important",
|
||||
boxSizing: "border-box",
|
||||
maxWidth: "calc(100% - 8px)",
|
||||
@@ -214,23 +213,23 @@ export const StyledListItem = styled(ListItem)(
|
||||
minWidth: 0,
|
||||
},
|
||||
"& .MuiListItemText-primary": {
|
||||
fontSize: theme.typography.pxToRem(12.5),
|
||||
fontSize: "0.78125rem",
|
||||
fontWeight: 600,
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: `${theme.navigation.hover} !important`,
|
||||
backgroundColor: "var(--mythic-nav-hover) !important",
|
||||
},
|
||||
}),
|
||||
);
|
||||
export const StyledListItemIcon = styled(ListItemIcon)(
|
||||
({ theme }) => ({
|
||||
() => ({
|
||||
paddingTop:0,
|
||||
marginTop: 0,
|
||||
paddingBottom: 0,
|
||||
minWidth: "36px",
|
||||
width: "36px",
|
||||
justifyContent: "center",
|
||||
color: theme.navigation.icon,
|
||||
color: "var(--mythic-nav-icon)",
|
||||
backgroundColor: "transparent !important",
|
||||
overflow: "visible",
|
||||
}),
|
||||
@@ -318,12 +317,11 @@ const getUnreadChatCount = (channels, readState) => {
|
||||
};
|
||||
|
||||
const Dashboard = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new' key={"home"} >
|
||||
<StyledListItemIcon >
|
||||
<MythicStyledTooltip title={"Operation Dashboard"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<SpaceDashboardTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement" />
|
||||
<SpaceDashboardTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon" />
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Operation Dashboard"} />
|
||||
@@ -331,12 +329,11 @@ const Dashboard = () => {
|
||||
)
|
||||
}
|
||||
const ActiveCallbacks = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/callbacks' key={"callbacks"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Active Callbacks"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<PhoneCallbackIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<PhoneCallbackIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
</StyledListItemIcon>
|
||||
@@ -345,12 +342,11 @@ const ActiveCallbacks = () => {
|
||||
)
|
||||
}
|
||||
const Payloads = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/payloads' key={"payloads"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Payloads"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<FontAwesomeIcon style={{color: theme.navBarTextIconColor}} icon={faBiohazard} size="lg"/>
|
||||
<FontAwesomeIcon className="mythic-navigation-icon" icon={faBiohazard} size="lg"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Payloads"} />
|
||||
@@ -358,13 +354,12 @@ const Payloads = () => {
|
||||
)
|
||||
}
|
||||
const SearchCallbacks = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=callbacks&searchField=Host&search=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Callbacks"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<PhoneCallbackIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<ManageSearchIcon style={{color: theme.navBarTextIconColor, marginLeft: "-8px", marginTop: "7px", borderRadius: "5px"}} fontSize={"small"} />
|
||||
<PhoneCallbackIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
<ManageSearchIcon className="mythic-navigation-icon" style={{marginLeft: "-8px", marginTop: "7px", borderRadius: "5px"}} fontSize={"small"} />
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Callbacks"} />
|
||||
@@ -372,12 +367,11 @@ const SearchCallbacks = () => {
|
||||
)
|
||||
}
|
||||
const SearchTasks = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=tasks&searchField=Command+and+Parameters&search=&taskStatus=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Tasks"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<AssignmentIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<AssignmentIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Tasks"} />
|
||||
@@ -385,13 +379,12 @@ const SearchTasks = () => {
|
||||
)
|
||||
}
|
||||
const SearchPayloads = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=payloads&searchField=Filename&search=&taskStatus=&c2=All+C2&payloadtype=All+Payload+Types'>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Payloads"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<FontAwesomeIcon style={{color: theme.navBarTextIconColor}} size={"lg"} icon={faBiohazard} />
|
||||
<ManageSearchIcon style={{color: theme.navBarTextIconColor, marginLeft: "-8px", marginTop: "7px", borderRadius: "5px"}} fontSize={"small"} />
|
||||
<FontAwesomeIcon className="mythic-navigation-icon" size={"lg"} icon={faBiohazard} />
|
||||
<ManageSearchIcon className="mythic-navigation-icon" style={{marginLeft: "-8px", marginTop: "7px", borderRadius: "5px"}} fontSize={"small"} />
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Payloads"} />
|
||||
@@ -399,12 +392,11 @@ const SearchPayloads = () => {
|
||||
)
|
||||
}
|
||||
const SearchFiles = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?searchField=Filename&tab=files&location=Downloads&host=&search=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Files"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<AttachmentIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<AttachmentIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Files"} />
|
||||
@@ -412,12 +404,11 @@ const SearchFiles = () => {
|
||||
)
|
||||
}
|
||||
const SearchScreenshots = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?searchField=Filename&tab=files&location=Screenshots' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Screenshots"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<CameraAltTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<CameraAltTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Screenshots"} />
|
||||
@@ -425,12 +416,11 @@ const SearchScreenshots = () => {
|
||||
)
|
||||
}
|
||||
const SearchCredentials = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?t?searchField=Account&tab=credentials&search='>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Credentials"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<VpnKeyIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement" />
|
||||
<VpnKeyIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon" />
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Credentials"} />
|
||||
@@ -438,12 +428,11 @@ const SearchCredentials = () => {
|
||||
)
|
||||
}
|
||||
const SearchKeylogs = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=keylogs&searchField=Host&search='>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Keylogs"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<KeyboardIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<KeyboardIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Keylogs"} />
|
||||
@@ -451,12 +440,11 @@ const SearchKeylogs = () => {
|
||||
)
|
||||
}
|
||||
const SearchArtifacts = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=artifacts&searchField=Host&search=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Artifacts"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<FingerprintIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<FingerprintIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Artifacts"} />
|
||||
@@ -464,12 +452,11 @@ const SearchArtifacts = () => {
|
||||
)
|
||||
}
|
||||
const SearchTokens = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=tokens&searchField=Host&search=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Tokens"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<ConfirmationNumberIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<ConfirmationNumberIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Tokens"} />
|
||||
@@ -477,12 +464,11 @@ const SearchTokens = () => {
|
||||
)
|
||||
}
|
||||
const SearchProxies = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=proxies'>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Proxies"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<FontAwesomeIcon style={{color: theme.navBarTextIconColor}} size={"lg"} icon={faSocks} />
|
||||
<FontAwesomeIcon className="mythic-navigation-icon" size={"lg"} icon={faSocks} />
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Proxies"} />
|
||||
@@ -490,12 +476,11 @@ const SearchProxies = () => {
|
||||
)
|
||||
}
|
||||
const SearchProcesses = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=processes&searchField=Name&search=&host=' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Processes"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<AccountTreeIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<AccountTreeIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Processes"} />
|
||||
@@ -503,13 +488,12 @@ const SearchProcesses = () => {
|
||||
)
|
||||
}
|
||||
const SearchTags = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/search?tab=tags&searchField=TagType&search=&host='>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Search Tags"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<LocalOfferTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<ManageSearchIcon style={{color: theme.navBarTextIconColor, marginLeft: "-8px", marginTop: "7px", borderRadius: "5px"}} fontSize={"small"} />
|
||||
<LocalOfferTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
<ManageSearchIcon className="mythic-navigation-icon" style={{marginLeft: "-8px", marginTop: "7px", borderRadius: "5px"}} fontSize={"small"} />
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Search Tags"} />
|
||||
@@ -517,12 +501,11 @@ const SearchTags = () => {
|
||||
)
|
||||
}
|
||||
const Mitre = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/mitre' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"MITRE ATT&CK"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<TableChartTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<TableChartTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"MITRE ATT&CK"} />
|
||||
@@ -530,12 +513,11 @@ const Mitre = () => {
|
||||
)
|
||||
}
|
||||
const Reporting = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/reporting' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Reporting"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<SportsScoreIcon style={{color: theme.navBarTextIconColor}} size={"medium"} />
|
||||
<SportsScoreIcon className="mythic-navigation-icon" size={"medium"} />
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Reporting"} />
|
||||
@@ -543,12 +525,11 @@ const Reporting = () => {
|
||||
)
|
||||
}
|
||||
const Tags = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/tagtypes' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Tags"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<LocalOfferTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<LocalOfferTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Tags"} />
|
||||
@@ -556,12 +537,11 @@ const Tags = () => {
|
||||
)
|
||||
}
|
||||
const Eventing = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/eventing' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Eventing"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<PlayCircleFilledTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<PlayCircleFilledTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Eventing"} />
|
||||
@@ -569,7 +549,6 @@ const Eventing = () => {
|
||||
)
|
||||
}
|
||||
const Chat = ({me}) => {
|
||||
const theme = useTheme();
|
||||
const streamStart = React.useRef(getSkewedNow().toISOString());
|
||||
const [channels, setChannels] = React.useState([]);
|
||||
const [readState, setReadState] = React.useState({});
|
||||
@@ -634,7 +613,7 @@ const Chat = ({me}) => {
|
||||
invisible={!error && unreadCount === 0}
|
||||
max={99}
|
||||
>
|
||||
<ForumTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<ForumTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</Badge>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
@@ -667,12 +646,11 @@ const GraphQL = () => {
|
||||
)
|
||||
}
|
||||
const CreatePayload = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/createpayload' key={"createpayload"} state={{from: 'TopAppBar'}}>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Create Payload"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<FontAwesomeIcon style={{color: theme.navBarTextIconColor}} size={"lg"} icon={faBiohazard} />
|
||||
<FontAwesomeIcon className="mythic-navigation-icon" size={"lg"} icon={faBiohazard} />
|
||||
<AddCircleIcon color={"success"} style={{marginLeft: "-8px", marginTop: "7px", backgroundColor: "white", borderRadius: "10px"}} fontSize={"small"} />
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
@@ -681,12 +659,11 @@ const CreatePayload = () => {
|
||||
)
|
||||
}
|
||||
const CreateWrapper = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/createwrapper' key={"createwrapper"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Create Wrapper"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<PostAddIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<PostAddIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
<AddCircleIcon color={"success"} style={{marginLeft: "-8px", marginTop: "7px", backgroundColor: "white", borderRadius: "10px"}} fontSize={"small"} />
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
@@ -695,12 +672,11 @@ const CreateWrapper = () => {
|
||||
)
|
||||
}
|
||||
const PayloadTypesAndC2 = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/payloadtypes' key={"payloadtypes"}>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Installed Services"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<HeadsetTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<HeadsetTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Installed Services"} />
|
||||
@@ -708,12 +684,11 @@ const PayloadTypesAndC2 = () => {
|
||||
)
|
||||
}
|
||||
const Operations = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/operations' key={"modifyoperations"}>
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Modify Operations"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<EditIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<EditIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Modify Operations"} />
|
||||
@@ -721,12 +696,11 @@ const Operations = () => {
|
||||
)
|
||||
}
|
||||
const BrowserScripts = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/browserscripts' key={"browserscripts"} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Browser Scripts"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<CodeOffIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<CodeOffIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"BrowserScripts"} />
|
||||
@@ -866,7 +840,6 @@ const TopAppBarVerticalAdjustShortcutsDialog = ({onClose, onSave, sideShortcuts}
|
||||
}
|
||||
|
||||
export function TopAppBarVertical(props) {
|
||||
const theme = useTheme();
|
||||
const me = props.me;
|
||||
const navigate = useNavigate();
|
||||
const initialNavBarOpen = GetMythicSetting({setting_name: 'navBarOpen', default_value: operatorSettingDefaults.navBarOpen});
|
||||
@@ -976,8 +949,7 @@ export function TopAppBarVertical(props) {
|
||||
<>
|
||||
{me?.user?.current_operation_id ? (<EventFeedNotifications me={me} />) : null }
|
||||
<Drawer anchor="left" variant="permanent" open={menuOpen} onClose={handleDrawerClose}
|
||||
PaperProps={{sx: {border: "0 !important", borderRight: "0 !important", borderRadius: "0 !important", boxShadow: "none !important"}}}
|
||||
style={{background: theme.navigation.background, backgroundColor: theme.navigation.backgroundColor}}>
|
||||
PaperProps={{sx: {border: "0 !important", borderRight: "0 !important", borderRadius: "0 !important", boxShadow: "none !important"}}}>
|
||||
<List style={{paddingTop: 0, marginTop: 0, height: "100%", display: "flex", flexDirection: "column",
|
||||
backgroundColor: "transparent !important",
|
||||
border: "0 !important", borderRadius: 0}}>
|
||||
@@ -990,37 +962,37 @@ export function TopAppBarVertical(props) {
|
||||
paddingLeft: "8px",
|
||||
paddingRight: "8px",
|
||||
paddingBottom: "5px",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderRadius: "var(--mythic-radius)",
|
||||
}}>
|
||||
<StyledListItemIcon>
|
||||
<img src={ReactLogo} onClick={()=>navigate('/new')} width={"35px"} height={"35px"} alt="Mythic" style={{cursor: "pointer"}}/>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText style={{margin: 0}} primary={
|
||||
<>
|
||||
<Typography style={{ fontSize: 12, color: theme.navigation.muted, display: "inline-block",
|
||||
<Typography className="mythic-navigation-muted-text" style={{ fontSize: 12, display: "inline-block",
|
||||
marginLeft: "1rem",
|
||||
lineHeight: 1.35}}>
|
||||
<b>Mythic:</b> v{serverVersion}<br/>
|
||||
<b>UI:</b> v{mythicUIVersion}<br/>
|
||||
</Typography>
|
||||
<IconButton onClick={props.toggleTheme} size="small" style={{float:"right", display: menuOpen ? "" : "none", color: theme.navigation.icon}} >
|
||||
{theme.palette.mode === 'light' &&
|
||||
<DarkModeTwoToneIcon style={{color: theme.navigation.icon}} fontSize={"medium"} className="mythicElement" />
|
||||
<IconButton onClick={props.toggleTheme} size="small" style={{float:"right", display: menuOpen ? "" : "none"}} >
|
||||
{props.themeMode === 'light' &&
|
||||
<DarkModeTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon" />
|
||||
}
|
||||
{theme.palette.mode === 'dark' &&
|
||||
<LightModeTwoToneIcon style={{color: theme.palette.warning.main}} fontSize={"medium"} className="mythicElement" />
|
||||
{props.themeMode === 'dark' &&
|
||||
<LightModeTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-warning-icon" />
|
||||
}
|
||||
</IconButton>
|
||||
</>
|
||||
} />
|
||||
</ListItem>
|
||||
<StyledListItem className={classes.listSubHeader} onClick={toggleDrawerOpen} style={{height: "32px"}} >
|
||||
<StyledListItemIcon ><MenuIcon style={{color: theme.navBarTextIconColor}} onClick={toggleDrawerOpen} fontSize={"medium"} className="mythicElement" /></StyledListItemIcon>
|
||||
<StyledListItemIcon ><MenuIcon onClick={toggleDrawerOpen} fontSize={"medium"} className="mythicElement mythic-navigation-icon" /></StyledListItemIcon>
|
||||
<ListItemText primary={
|
||||
<>
|
||||
<MythicStyledTooltip title={"Edit Shortcuts"} tooltipStyle={{float: menuOpen ? 'right' : '', margin: 0, padding: 0}}>
|
||||
<Button onClick={openEditShortcuts} style={{color: theme.navBarTextColor}}>
|
||||
<EditIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"}/> Edit
|
||||
<Button onClick={openEditShortcuts} className="mythic-navigation-action-text">
|
||||
<EditIcon className="mythic-navigation-icon" fontSize={"medium"}/> Edit
|
||||
</Button>
|
||||
|
||||
</MythicStyledTooltip>
|
||||
@@ -1039,7 +1011,7 @@ export function TopAppBarVertical(props) {
|
||||
<StyledListItem className={classes.listSubHeader} style={{display: me?.user?.current_operation_id === 0 ? "" : "none"}}>
|
||||
<ListItemText primary={
|
||||
<>
|
||||
<Link style={{display: "inline-flex", alignItems: "center", paddingRight: "10px", color: theme.palette.error.main,
|
||||
<Link style={{display: "inline-flex", alignItems: "center", paddingRight: "10px", color: "var(--mythic-error-main)",
|
||||
fontWeight: "bold",}} to="/new/operations">
|
||||
{"CLICK TO SET OPERATION!"}
|
||||
</Link>
|
||||
@@ -1047,19 +1019,19 @@ export function TopAppBarVertical(props) {
|
||||
|
||||
} />
|
||||
</StyledListItem>
|
||||
<Divider style={{borderColor: theme.navigation.muted, margin: "4px 8px"}} />
|
||||
<Divider style={{borderColor: "var(--mythic-nav-muted)", margin: "4px 8px"}} />
|
||||
<div style={{flexGrow: 1, overflowY: "auto", overflowX: "hidden"}}>
|
||||
{getShortcuts({shortcuts: sideShortcuts})}
|
||||
<Divider style={{borderColor: theme.navigation.muted, margin: "4px 8px"}} />
|
||||
<Divider style={{borderColor: "var(--mythic-nav-muted)", margin: "4px 8px"}} />
|
||||
<StyledListItem className={classes.listSubHeader} onClick={handleToggleExtra}>
|
||||
<StyledListItemIcon>
|
||||
<MoreHorizIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} />
|
||||
<MoreHorizIcon className="mythic-navigation-icon" fontSize={"medium"} />
|
||||
</StyledListItemIcon>
|
||||
<ListItemText>Extra Shortcuts</ListItemText>
|
||||
{openExtra ? <ExpandLess /> : <ExpandMore />}
|
||||
</StyledListItem>
|
||||
{openExtra && getExtraShortcuts()}
|
||||
<Divider style={{borderColor: theme.navigation.muted, margin: "4px 8px"}} />
|
||||
<Divider style={{borderColor: "var(--mythic-nav-muted)", margin: "4px 8px"}} />
|
||||
<div className={classes.listSubHeader} style={{ flexGrow: 1}}></div>
|
||||
</div>
|
||||
<TopBarRightShortcutsVertical me={me} menuOpen={menuOpen} serverName={serverName} />
|
||||
@@ -1070,7 +1042,6 @@ export function TopAppBarVertical(props) {
|
||||
}
|
||||
|
||||
function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
|
||||
const theme = useTheme();
|
||||
const documentationRef = React.useRef(null);
|
||||
const [documentationOpen, setDocumentationOpen] = React.useState(false);
|
||||
const settingsRef = React.useRef(null);
|
||||
@@ -1233,7 +1204,7 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
|
||||
<StyledListItem className={classes.listSubHeader} onClick={() => setOpenFeedbackForm(true)} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Submit feedback via Webhook"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<ThumbDownTwoTone style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement" />
|
||||
<ThumbDownTwoTone fontSize={"medium"} className="mythicElement mythic-navigation-icon" />
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Send Feedback"} />
|
||||
@@ -1249,11 +1220,11 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
|
||||
<StyledListItem className={classes.listSubHeader} onClick={handleDocumentationMenu} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"Documentation Links"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<HelpTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
<HelpTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon"/>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Help"} />
|
||||
<KeyboardArrowDownIcon style={{color: theme.navBarTextIconColor, display: menuOpen ? "" : "none", flex: "0 0 auto", marginLeft: "auto"}} />
|
||||
<KeyboardArrowDownIcon className="mythic-navigation-icon" style={{display: menuOpen ? "" : "none", flex: "0 0 auto", marginLeft: "auto"}} />
|
||||
</StyledListItem>
|
||||
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/EventFeed' >
|
||||
@@ -1268,11 +1239,11 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
|
||||
<StyledListItem className={classes.listSubHeader} onClick={handleSettingsMenu} >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={"User Settings"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<ManageAccountsTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement" />
|
||||
<ManageAccountsTwoToneIcon fontSize={"medium"} className="mythicElement mythic-navigation-icon" />
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Settings"} />
|
||||
<KeyboardArrowDownIcon style={{color: theme.navBarTextIconColor, display: menuOpen ? "" : "none", flex: "0 0 auto", marginLeft: "auto"}} />
|
||||
<KeyboardArrowDownIcon className="mythic-navigation-icon" style={{display: menuOpen ? "" : "none", flex: "0 0 auto", marginLeft: "auto"}} />
|
||||
</StyledListItem>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -43,9 +43,10 @@ import UnarchiveIcon from '@mui/icons-material/Unarchive';
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {MythicConfirmDialog} from "../../MythicComponents/MythicConfirmDialog";
|
||||
import {MeContext} from "../../App";
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {getSkewedNow, toLocalTime} from "../../utilities/Time";
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
|
||||
const CHAT_MESSAGE_LIMIT = 250;
|
||||
const CHAT_REQUEST_LIMIT = 50;
|
||||
@@ -63,6 +64,7 @@ fragment ChatChannelFields on chat_channel {
|
||||
last_message_id
|
||||
chat_container_id
|
||||
chat_model
|
||||
ai_metadata
|
||||
updated_at
|
||||
chat_container {
|
||||
id
|
||||
@@ -340,11 +342,123 @@ const allowedLinkSchemes = ["http:", "https:", "mailto:"];
|
||||
|
||||
const isGeneralChatChannel = (channel) => channel?.channel_type === "standard" && channel?.slug === "general";
|
||||
|
||||
const formatTimestamp = (timestamp, viewUTCTime) => {
|
||||
if(!timestamp){ return ""; }
|
||||
return toLocalTime(timestamp, viewUTCTime);
|
||||
const CHAT_SEARCH_SNIPPET_LENGTH = 260;
|
||||
const CHAT_SEARCH_SNIPPET_CONTEXT = 90;
|
||||
const timestampHasTimeZone = (timestampText) => /(?:[zZ]|[+-]\d{2}:?\d{2})$/.test(timestampText);
|
||||
const utcWeekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
const utcMonths = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
|
||||
const padTimePart = (value) => String(value).padStart(2, "0");
|
||||
|
||||
const parseChatTimestamp = (timestamp) => {
|
||||
if(!timestamp){ return null; }
|
||||
if(timestamp instanceof Date){
|
||||
return Number.isNaN(timestamp.getTime()) ? null : timestamp;
|
||||
}
|
||||
if(typeof timestamp === "number"){
|
||||
const parsedNumber = new Date(timestamp);
|
||||
return Number.isNaN(parsedNumber.getTime()) ? null : parsedNumber;
|
||||
}
|
||||
const timestampText = String(timestamp).trim();
|
||||
if(timestampText === ""){ return null; }
|
||||
const normalizedTimestamp = timestampHasTimeZone(timestampText) ? timestampText : `${timestampText}Z`;
|
||||
const parsedTimestamp = new Date(normalizedTimestamp);
|
||||
return Number.isNaN(parsedTimestamp.getTime()) ? null : parsedTimestamp;
|
||||
};
|
||||
|
||||
const formatUTCTimestamp = (date) => (
|
||||
`${utcWeekdays[date.getUTCDay()]} ${utcMonths[date.getUTCMonth()]} ${padTimePart(date.getUTCDate())} ` +
|
||||
`${date.getUTCFullYear()} ${padTimePart(date.getUTCHours())}:${padTimePart(date.getUTCMinutes())}:${padTimePart(date.getUTCSeconds())} UTC`
|
||||
);
|
||||
|
||||
const formatTimestamp = (timestamp, viewUTCTime) => {
|
||||
const parsedTimestamp = parseChatTimestamp(timestamp);
|
||||
if(!parsedTimestamp){ return ""; }
|
||||
if(viewUTCTime){
|
||||
return formatUTCTimestamp(parsedTimestamp);
|
||||
}
|
||||
return `${parsedTimestamp.toDateString()} ${parsedTimestamp.toLocaleString(["en-us"], {hour12: true, hour: "2-digit", minute: "2-digit"})}`;
|
||||
};
|
||||
|
||||
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
const getSearchTerms = (query) => {
|
||||
const trimmedQuery = (query || "").trim();
|
||||
if(trimmedQuery === ""){
|
||||
return [];
|
||||
}
|
||||
const uniqueTerms = new Set();
|
||||
return [trimmedQuery, ...trimmedQuery.split(/\s+/)]
|
||||
.map((term) => term.replace(/^[^\w]+|[^\w]+$/g, ""))
|
||||
.filter((term) => term.length > 1 || trimmedQuery.length === 1)
|
||||
.filter((term) => {
|
||||
const normalizedTerm = term.toLocaleLowerCase();
|
||||
if(uniqueTerms.has(normalizedTerm)){
|
||||
return false;
|
||||
}
|
||||
uniqueTerms.add(normalizedTerm);
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => b.length - a.length);
|
||||
};
|
||||
|
||||
const buildSearchSnippetParts = (message, query) => {
|
||||
const messageText = String(message || "");
|
||||
if(messageText === ""){
|
||||
return [];
|
||||
}
|
||||
const searchTerms = getSearchTerms(query);
|
||||
const lowerMessageText = messageText.toLocaleLowerCase();
|
||||
const firstMatch = searchTerms.reduce((bestMatch, term) => {
|
||||
const index = lowerMessageText.indexOf(term.toLocaleLowerCase());
|
||||
if(index === -1){
|
||||
return bestMatch;
|
||||
}
|
||||
if(!bestMatch || index < bestMatch.index){
|
||||
return {index, term};
|
||||
}
|
||||
return bestMatch;
|
||||
}, null);
|
||||
let snippetStart = 0;
|
||||
let snippetEnd = Math.min(messageText.length, CHAT_SEARCH_SNIPPET_LENGTH);
|
||||
if(firstMatch){
|
||||
snippetStart = Math.max(0, firstMatch.index - CHAT_SEARCH_SNIPPET_CONTEXT);
|
||||
snippetEnd = Math.min(messageText.length, snippetStart + CHAT_SEARCH_SNIPPET_LENGTH);
|
||||
snippetStart = Math.max(0, snippetEnd - CHAT_SEARCH_SNIPPET_LENGTH);
|
||||
}
|
||||
const hasLeadingText = snippetStart > 0;
|
||||
const hasTrailingText = snippetEnd < messageText.length;
|
||||
const snippetText = `${hasLeadingText ? "..." : ""}${messageText.slice(snippetStart, snippetEnd).replace(/\s+/g, " ").trim()}${hasTrailingText ? "..." : ""}`;
|
||||
if(searchTerms.length === 0){
|
||||
return [{text: snippetText, highlight: false}];
|
||||
}
|
||||
const highlightExpression = new RegExp(searchTerms.map(escapeRegExp).join("|"), "gi");
|
||||
const parts = [];
|
||||
let lastIndex = 0;
|
||||
snippetText.replace(highlightExpression, (match, offset) => {
|
||||
if(offset > lastIndex){
|
||||
parts.push({text: snippetText.slice(lastIndex, offset), highlight: false});
|
||||
}
|
||||
parts.push({text: match, highlight: true});
|
||||
lastIndex = offset + match.length;
|
||||
return match;
|
||||
});
|
||||
if(lastIndex < snippetText.length){
|
||||
parts.push({text: snippetText.slice(lastIndex), highlight: false});
|
||||
}
|
||||
return parts;
|
||||
};
|
||||
|
||||
const renderSearchSnippet = (message, query) => (
|
||||
buildSearchSnippetParts(message, query).map((part, index) => (
|
||||
part.highlight ? (
|
||||
<mark className="mythic-chat-search-highlight" key={`highlight-${index}`}>{part.text}</mark>
|
||||
) : (
|
||||
<React.Fragment key={`text-${index}`}>{part.text}</React.Fragment>
|
||||
)
|
||||
))
|
||||
);
|
||||
|
||||
const timestampValue = (timestamp) => {
|
||||
if(!timestamp){ return 0; }
|
||||
const value = new Date(timestamp).getTime();
|
||||
@@ -524,6 +638,173 @@ const parseChatContainerModels = (container) => {
|
||||
}).filter((model) => model.name);
|
||||
};
|
||||
|
||||
const parseJSONLikeObject = (value) => {
|
||||
if(!value){
|
||||
return {};
|
||||
}
|
||||
if(typeof value === "object" && !Array.isArray(value)){
|
||||
return value;
|
||||
}
|
||||
if(typeof value === "string"){
|
||||
try{
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
||||
}catch(error){
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const getChannelAIMetadata = (channel) => parseJSONLikeObject(channel?.ai_metadata);
|
||||
|
||||
const getChannelAIConfig = (channel) => {
|
||||
const metadata = getChannelAIMetadata(channel);
|
||||
return parseJSONLikeObject(metadata.config || metadata.configuration);
|
||||
};
|
||||
|
||||
const modelForChannel = (channel, chatContainers) => {
|
||||
if(!channel || channel.channel_type !== "ai"){
|
||||
return null;
|
||||
}
|
||||
const container = chatContainers.find((item) => item.id === channel.chat_container_id) || channel.chat_container;
|
||||
return parseChatContainerModels(container).find((model) => model.name === channel.chat_model) || null;
|
||||
};
|
||||
|
||||
const getModelConfigOptions = (model) => {
|
||||
const metadata = model?.metadata || {};
|
||||
const rawOptions = metadata.configuration_options ||
|
||||
metadata.config_options ||
|
||||
metadata?.configuration?.options ||
|
||||
metadata?.config?.options ||
|
||||
[];
|
||||
if(!Array.isArray(rawOptions)){
|
||||
return [];
|
||||
}
|
||||
return rawOptions.map((option) => {
|
||||
const name = option.name || option.key || option.Name || option.Key || "";
|
||||
const choices = Array.isArray(option.choices || option.Choices) ? (option.choices || option.Choices).map((choice) => {
|
||||
if(choice && typeof choice === "object"){
|
||||
const value = choice.value ?? choice.Value ?? choice.name ?? choice.Name ?? choice.label ?? choice.Label ?? "";
|
||||
return {
|
||||
value,
|
||||
label: `${choice.label ?? choice.Label ?? value}`,
|
||||
description: choice.description || choice.Description || "",
|
||||
};
|
||||
}
|
||||
return {value: choice, label: `${choice}`, description: ""};
|
||||
}) : [];
|
||||
const type = `${option.type || option.Type || (choices.length > 0 ? "choice" : "string")}`.toLowerCase();
|
||||
return {
|
||||
name: `${name}`,
|
||||
displayName: option.display_name || option.displayName || option.DisplayName || option.label || option.Label || `${name}`,
|
||||
description: option.description || option.Description || "",
|
||||
type: choices.length > 0 ? "choice" : type,
|
||||
required: Boolean(option.required || option.Required),
|
||||
defaultValue: option.default_value ?? option.defaultValue ?? option.DefaultValue ?? option.default ?? option.Default ?? "",
|
||||
choices,
|
||||
};
|
||||
}).filter((option) => option.name);
|
||||
};
|
||||
|
||||
const configValueForField = (value) => {
|
||||
if(value === undefined || value === null){
|
||||
return "";
|
||||
}
|
||||
return `${value}`;
|
||||
};
|
||||
|
||||
const buildDefaultConfigValues = (options, existing = {}) => {
|
||||
return options.reduce((prev, option) => {
|
||||
const existingValue = existing[option.name];
|
||||
prev[option.name] = configValueForField(existingValue !== undefined ? existingValue : option.defaultValue);
|
||||
return prev;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const normalizeConfigForSubmit = (values, options) => {
|
||||
return options.reduce((prev, option) => {
|
||||
const rawValue = values[option.name];
|
||||
if(rawValue === undefined || rawValue === null || `${rawValue}`.trim() === ""){
|
||||
return prev;
|
||||
}
|
||||
if(option.type === "number"){
|
||||
const numberValue = Number(rawValue);
|
||||
if(!Number.isNaN(numberValue)){
|
||||
prev[option.name] = numberValue;
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
prev[option.name] = rawValue;
|
||||
return prev;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const configHasMissingRequiredValues = (values, options) => options.some((option) => (
|
||||
option.required && `${values[option.name] ?? ""}`.trim() === ""
|
||||
));
|
||||
|
||||
const applyConfigToMetadata = (metadata, config) => ({
|
||||
...parseJSONLikeObject(metadata),
|
||||
config,
|
||||
});
|
||||
|
||||
const ChatConfigurationFields = ({options, values, setValues}) => {
|
||||
if(options.length === 0){
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Box sx={{display: "flex", flexDirection: "column", gap: 1.25}}>
|
||||
<Typography variant="subtitle2">AI Configuration</Typography>
|
||||
{options.map((option) => {
|
||||
if(option.type === "choice"){
|
||||
return (
|
||||
<FormControl size="small" fullWidth key={option.name}>
|
||||
<InputLabel>{option.displayName}</InputLabel>
|
||||
<Select
|
||||
label={option.displayName}
|
||||
value={configValueForField(values[option.name])}
|
||||
onChange={(e) => setValues((prev) => ({...prev, [option.name]: e.target.value}))}
|
||||
>
|
||||
{option.choices.map((choice) => (
|
||||
<MenuItem value={configValueForField(choice.value)} key={`${option.name}-${choice.value}`}>
|
||||
<Box sx={{display: "flex", flexDirection: "column", py: 0.25}}>
|
||||
<Typography variant="body2">{choice.label}</Typography>
|
||||
{choice.description &&
|
||||
<Typography variant="caption" color="text.secondary" sx={{whiteSpace: "normal"}}>
|
||||
{choice.description}
|
||||
</Typography>
|
||||
}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{option.description &&
|
||||
<Typography variant="caption" color="text.secondary" sx={{mt: 0.5}}>
|
||||
{option.description}
|
||||
</Typography>
|
||||
}
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TextField
|
||||
key={option.name}
|
||||
fullWidth
|
||||
size="small"
|
||||
type={option.type === "number" ? "number" : "text"}
|
||||
label={option.displayName}
|
||||
required={option.required}
|
||||
value={configValueForField(values[option.name])}
|
||||
helperText={option.description}
|
||||
onChange={(e) => setValues((prev) => ({...prev, [option.name]: e.target.value}))}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatEmptyState = ({icon, title, detail}) => (
|
||||
<Box className="mythic-chat-empty-state">
|
||||
{icon}
|
||||
@@ -676,6 +957,7 @@ const ChatCreateDialog = ({open, onClose, onCreate, chatContainers}) => {
|
||||
const [channelType, setChannelType] = React.useState("standard");
|
||||
const [containerID, setContainerID] = React.useState("");
|
||||
const [model, setModel] = React.useState("");
|
||||
const [configValues, setConfigValues] = React.useState({});
|
||||
const [locked, setLocked] = React.useState(true);
|
||||
React.useEffect(() => {
|
||||
if(open){
|
||||
@@ -684,6 +966,7 @@ const ChatCreateDialog = ({open, onClose, onCreate, chatContainers}) => {
|
||||
setChannelType("standard");
|
||||
setContainerID("");
|
||||
setModel("");
|
||||
setConfigValues({});
|
||||
setLocked(true);
|
||||
}
|
||||
}, [open]);
|
||||
@@ -693,6 +976,10 @@ const ChatCreateDialog = ({open, onClose, onCreate, chatContainers}) => {
|
||||
const selectedContainerModels = React.useMemo(() => (
|
||||
parseChatContainerModels(selectedContainer)
|
||||
), [selectedContainer]);
|
||||
const selectedModel = React.useMemo(() => (
|
||||
selectedContainerModels.find((containerModel) => containerModel.name === model) || null
|
||||
), [selectedContainerModels, model]);
|
||||
const configOptions = React.useMemo(() => getModelConfigOptions(selectedModel), [selectedModel]);
|
||||
React.useEffect(() => {
|
||||
if(channelType !== "ai" || !containerID){
|
||||
return;
|
||||
@@ -705,12 +992,20 @@ const ChatCreateDialog = ({open, onClose, onCreate, chatContainers}) => {
|
||||
setModel(selectedContainerModels[0].name);
|
||||
}
|
||||
}, [channelType, containerID, model, selectedContainerModels]);
|
||||
React.useEffect(() => {
|
||||
if(channelType === "ai" && model){
|
||||
setConfigValues(buildDefaultConfigValues(configOptions));
|
||||
} else {
|
||||
setConfigValues({});
|
||||
}
|
||||
}, [channelType, containerID, model, configOptions]);
|
||||
const changeChannelType = (event) => {
|
||||
const nextType = event.target.value;
|
||||
setChannelType(nextType);
|
||||
if(nextType !== "ai"){
|
||||
setContainerID("");
|
||||
setModel("");
|
||||
setConfigValues({});
|
||||
}
|
||||
};
|
||||
const changeContainer = (event) => {
|
||||
@@ -719,10 +1014,12 @@ const ChatCreateDialog = ({open, onClose, onCreate, chatContainers}) => {
|
||||
const nextModels = parseChatContainerModels(nextContainer);
|
||||
setContainerID(nextContainerID);
|
||||
setModel(nextModels.length === 1 ? nextModels[0].name : "");
|
||||
setConfigValues({});
|
||||
};
|
||||
const createDisabled = name.trim() === "" ||
|
||||
(channelType === "ai" && (!containerID || selectedContainerModels.length === 0 || model === ""));
|
||||
(channelType === "ai" && (!containerID || selectedContainerModels.length === 0 || model === "" || configHasMissingRequiredValues(configValues, configOptions)));
|
||||
const submit = () => {
|
||||
const aiConfig = channelType === "ai" ? normalizeConfigForSubmit(configValues, configOptions) : {};
|
||||
onCreate({
|
||||
name,
|
||||
description,
|
||||
@@ -730,7 +1027,7 @@ const ChatCreateDialog = ({open, onClose, onCreate, chatContainers}) => {
|
||||
chat_container_id: channelType === "ai" ? Number(containerID) : null,
|
||||
chat_model: channelType === "ai" ? model : "",
|
||||
locked: channelType === "ai" ? locked : false,
|
||||
ai_metadata: {},
|
||||
ai_metadata: channelType === "ai" ? applyConfigToMetadata({}, aiConfig) : {},
|
||||
});
|
||||
};
|
||||
return (
|
||||
@@ -796,6 +1093,9 @@ const ChatCreateDialog = ({open, onClose, onCreate, chatContainers}) => {
|
||||
)}
|
||||
</>
|
||||
}
|
||||
{configOptions.length > 0 &&
|
||||
<ChatConfigurationFields options={configOptions} values={configValues} setValues={setConfigValues} />
|
||||
}
|
||||
<Box sx={{border: `1px solid ${alpha(theme.palette.info.main, 0.22)}`, borderRadius: 1, p: 1.25, backgroundColor: alpha(theme.palette.info.main, theme.palette.mode === "dark" ? 0.12 : 0.07)}}>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={locked} onChange={(e) => setLocked(e.target.checked)} />}
|
||||
@@ -818,59 +1118,95 @@ const ChatCreateDialog = ({open, onClose, onCreate, chatContainers}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ChatSearchDialog = ({open, onClose, onSearch, searchText, setSearchText, results, onSelectResult, viewUTCTime}) => (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Search Chat</DialogTitle>
|
||||
<DialogContent className="mythic-chat-dialog-content" sx={{display: "flex", flexDirection: "column", gap: 1.75, pt: "20px !important", px: 3}}>
|
||||
<Box sx={{display: "flex", gap: 1, alignItems: "flex-start"}}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label="Search"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if(e.key === "Enter"){
|
||||
onSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button variant="contained" startIcon={<SearchIcon />} onClick={onSearch}>Search</Button>
|
||||
</Box>
|
||||
<Box className="mythic-chat-search-results">
|
||||
{(results || []).map((result) => (
|
||||
<button
|
||||
type="button"
|
||||
className="mythic-chat-search-result"
|
||||
key={result.id}
|
||||
onClick={() => onSelectResult(result)}
|
||||
>
|
||||
<span className="mythic-chat-search-channel">
|
||||
{result.channel_type === "ai" ? <SmartToyTwoToneIcon fontSize="small" /> : <ForumTwoToneIcon fontSize="small" />}
|
||||
{result.channel_type === "ai" ? result.channel_name : `#${result.channel_name}`}
|
||||
</span>
|
||||
<span className="mythic-chat-search-message">{result.message}</span>
|
||||
<span className="mythic-chat-search-meta">{result.sender_display_name} · {formatTimestamp(result.created_at, viewUTCTime)}</span>
|
||||
</button>
|
||||
))}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
const ChatSearchDialog = ({open, onClose, onSearch, searchText, setSearchText, searchQuery, results, loading, hasSearched, onSelectResult, viewUTCTime}) => {
|
||||
const trimmedSearchText = searchText.trim();
|
||||
const searchResults = results || [];
|
||||
const highlightQuery = searchQuery || trimmedSearchText;
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth PaperProps={{className: "mythic-chat-search-dialog"}}>
|
||||
<DialogTitle>Search Chat</DialogTitle>
|
||||
<DialogContent className="mythic-chat-dialog-content mythic-chat-search-content" sx={{display: "flex", flexDirection: "column", gap: 1.75, pt: "20px !important", px: 3}}>
|
||||
<Box className="mythic-chat-search-form">
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
size="small"
|
||||
label="Search"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if(e.key === "Enter"){
|
||||
onSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button variant="contained" startIcon={<SearchIcon />} disabled={trimmedSearchText === "" || loading} onClick={onSearch}>
|
||||
{loading ? "Searching" : "Search"}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box className="mythic-chat-search-results">
|
||||
{loading && <Box className="mythic-chat-search-empty">Searching...</Box>}
|
||||
{!loading && hasSearched && searchResults.length === 0 && <Box className="mythic-chat-search-empty">No matches</Box>}
|
||||
{!loading && searchResults.map((result) => (
|
||||
<button
|
||||
type="button"
|
||||
className="mythic-chat-search-result"
|
||||
key={result.id}
|
||||
onClick={() => onSelectResult(result)}
|
||||
>
|
||||
<span className="mythic-chat-search-result-header">
|
||||
<span className="mythic-chat-search-channel">
|
||||
{result.channel_type === "ai" ? <SmartToyTwoToneIcon fontSize="small" /> : <ForumTwoToneIcon fontSize="small" />}
|
||||
<span>{result.channel_type === "ai" ? result.channel_name : `#${result.channel_name}`}</span>
|
||||
</span>
|
||||
<span className="mythic-chat-search-meta">{result.sender_display_name} · {formatTimestamp(result.created_at, viewUTCTime)}</span>
|
||||
</span>
|
||||
<span className="mythic-chat-search-message">{renderSearchSnippet(result.message, highlightQuery)}</span>
|
||||
</button>
|
||||
))}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatEditChannelDialog = ({open, channel, onClose, onSave}) => {
|
||||
const ChatEditChannelDialog = ({open, channel, onClose, onSave, chatContainers = []}) => {
|
||||
const [name, setName] = React.useState("");
|
||||
const [description, setDescription] = React.useState("");
|
||||
const [chatModel, setChatModel] = React.useState("");
|
||||
const [configValues, setConfigValues] = React.useState({});
|
||||
const isGeneralChannel = isGeneralChatChannel(channel);
|
||||
const isAIChannel = channel?.channel_type === "ai";
|
||||
const containerModels = React.useMemo(() => {
|
||||
if(!isAIChannel){
|
||||
return [];
|
||||
}
|
||||
const container = chatContainers.find((item) => item.id === channel.chat_container_id) || channel.chat_container;
|
||||
return parseChatContainerModels(container);
|
||||
}, [channel, chatContainers, isAIChannel]);
|
||||
const selectedModel = React.useMemo(() => (
|
||||
containerModels.find((containerModel) => containerModel.name === chatModel) || (chatModel ? null : modelForChannel(channel, chatContainers))
|
||||
), [channel, chatContainers, chatModel, containerModels]);
|
||||
const configOptions = React.useMemo(() => getModelConfigOptions(selectedModel), [selectedModel]);
|
||||
React.useEffect(() => {
|
||||
if(open && channel){
|
||||
setName(channel.name || "");
|
||||
setDescription(channel.description || "");
|
||||
setChatModel(channel.chat_model || "");
|
||||
const initialModel = modelForChannel(channel, chatContainers);
|
||||
setConfigValues(buildDefaultConfigValues(getModelConfigOptions(initialModel), getChannelAIConfig(channel)));
|
||||
}
|
||||
}, [open, channel]);
|
||||
}, [open, channel, chatContainers]);
|
||||
const changeModel = (event) => {
|
||||
const nextModelName = event.target.value;
|
||||
const nextModel = containerModels.find((containerModel) => containerModel.name === nextModelName) || null;
|
||||
const nextOptions = getModelConfigOptions(nextModel);
|
||||
setChatModel(nextModelName);
|
||||
setConfigValues((prev) => buildDefaultConfigValues(nextOptions, prev));
|
||||
};
|
||||
const submit = () => {
|
||||
if(!channel){
|
||||
return;
|
||||
@@ -882,11 +1218,20 @@ const ChatEditChannelDialog = ({open, channel, onClose, onSave}) => {
|
||||
if(!isGeneralChatChannel(channel)){
|
||||
update.name = name.trim();
|
||||
}
|
||||
if(isAIChannel){
|
||||
update.chat_model = chatModel;
|
||||
update.ai_metadata = applyConfigToMetadata(
|
||||
getChannelAIMetadata(channel),
|
||||
normalizeConfigForSubmit(configValues, configOptions),
|
||||
);
|
||||
}
|
||||
onSave(update);
|
||||
};
|
||||
const saveDisabled = (!isGeneralChannel && name.trim() === "") ||
|
||||
(isAIChannel && configHasMissingRequiredValues(configValues, configOptions));
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Edit Channel</DialogTitle>
|
||||
<DialogTitle>{isAIChannel ? "Edit AI Chat" : "Edit Channel"}</DialogTitle>
|
||||
<DialogContent className="mythic-chat-dialog-content" sx={{display: "flex", flexDirection: "column", gap: 1.75, pt: "20px !important", px: 3}}>
|
||||
<TextField
|
||||
autoFocus={!isGeneralChannel}
|
||||
@@ -907,10 +1252,40 @@ const ChatEditChannelDialog = ({open, channel, onClose, onSave}) => {
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
{isAIChannel &&
|
||||
<>
|
||||
<FormControl size="small" fullWidth>
|
||||
<InputLabel>Model</InputLabel>
|
||||
<Select
|
||||
label="Model"
|
||||
value={chatModel}
|
||||
onChange={changeModel}
|
||||
renderValue={(selected) => selected}
|
||||
>
|
||||
{containerModels.length === 0 &&
|
||||
<MenuItem value={chatModel} disabled>{chatModel || "No models reported"}</MenuItem>
|
||||
}
|
||||
{containerModels.map((containerModel) => (
|
||||
<MenuItem value={containerModel.name} key={`${channel?.id}-${containerModel.name}`}>
|
||||
<Box sx={{display: "flex", flexDirection: "column", py: 0.5, minWidth: 0}}>
|
||||
<Typography variant="body2">{containerModel.name}</Typography>
|
||||
{containerModel.description &&
|
||||
<Typography variant="caption" color="text.secondary" sx={{whiteSpace: "normal"}}>
|
||||
{containerModel.description}
|
||||
</Typography>
|
||||
}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<ChatConfigurationFields options={configOptions} values={configValues} setValues={setConfigValues} />
|
||||
</>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={submit} variant="contained" disabled={!isGeneralChannel && name.trim() === ""}>
|
||||
<Button onClick={submit} variant="contained" disabled={saveDisabled}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -988,8 +1363,10 @@ export function Chat({me}) {
|
||||
const [createOpen, setCreateOpen] = React.useState(false);
|
||||
const [editChannelOpen, setEditChannelOpen] = React.useState(false);
|
||||
const [systemMessageOpen, setSystemMessageOpen] = React.useState(false);
|
||||
const [archiveTarget, setArchiveTarget] = React.useState(null);
|
||||
const [searchOpen, setSearchOpen] = React.useState(false);
|
||||
const [searchText, setSearchText] = React.useState("");
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const [editingID, setEditingID] = React.useState(null);
|
||||
const [editText, setEditText] = React.useState("");
|
||||
const messagesEndRef = React.useRef(null);
|
||||
@@ -1225,7 +1602,7 @@ export function Chat({me}) {
|
||||
onError: (error) => snackActions.error(error.message),
|
||||
});
|
||||
const [markRead] = useMutation(MARK_READ);
|
||||
const [runSearch, {data: searchData}] = useLazyQuery(CHAT_SEARCH, {
|
||||
const [runSearch, {data: searchData, loading: searchLoading}] = useLazyQuery(CHAT_SEARCH, {
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
if(data.chatSearch.status !== "success"){
|
||||
@@ -1305,8 +1682,18 @@ export function Chat({me}) {
|
||||
const composerDisabled = disabledReason !== "";
|
||||
const onCreateChannel = (variables) => createChannel({variables});
|
||||
const toggleArchive = () => {
|
||||
if(selectedChannel && !isGeneralChatChannel(selectedChannel)){
|
||||
updateChannel({variables: {channel_id: selectedChannel.id, archived: !selectedChannel.archived}});
|
||||
if(!selectedChannel || isGeneralChatChannel(selectedChannel)){
|
||||
return;
|
||||
}
|
||||
if(selectedChannel.archived){
|
||||
updateChannel({variables: {channel_id: selectedChannel.id, archived: false}});
|
||||
} else {
|
||||
setArchiveTarget(selectedChannel);
|
||||
}
|
||||
};
|
||||
const confirmArchiveChannel = () => {
|
||||
if(archiveTarget){
|
||||
updateChannel({variables: {channel_id: archiveTarget.id, archived: true}});
|
||||
}
|
||||
};
|
||||
const toggleLock = () => {
|
||||
@@ -1331,8 +1718,10 @@ export function Chat({me}) {
|
||||
}
|
||||
};
|
||||
const runChatSearch = () => {
|
||||
if(searchText.trim()){
|
||||
runSearch({variables: {query: searchText.trim(), limit: 50}});
|
||||
const trimmedSearchText = searchText.trim();
|
||||
if(trimmedSearchText){
|
||||
setSearchQuery(trimmedSearchText);
|
||||
runSearch({variables: {query: trimmedSearchText, limit: 50}});
|
||||
}
|
||||
};
|
||||
const selectSearchResult = (result) => {
|
||||
@@ -1559,6 +1948,7 @@ export function Chat({me}) {
|
||||
<ChatEditChannelDialog
|
||||
open={editChannelOpen}
|
||||
channel={selectedChannel}
|
||||
chatContainers={chatContainers}
|
||||
onClose={() => setEditChannelOpen(false)}
|
||||
onSave={saveChannelDetails}
|
||||
/>
|
||||
@@ -1569,13 +1959,27 @@ export function Chat({me}) {
|
||||
onClose={() => setSystemMessageOpen(false)}
|
||||
onSend={submitSystemMessage}
|
||||
/>
|
||||
{archiveTarget &&
|
||||
<MythicConfirmDialog
|
||||
open={Boolean(archiveTarget)}
|
||||
title="Archive Channel?"
|
||||
dialogText={`Archive ${channelDisplayName(archiveTarget)}? This hides the channel from the default chat list until archived channels are shown.`}
|
||||
acceptText="Archive"
|
||||
acceptColor="warning"
|
||||
onClose={() => setArchiveTarget(null)}
|
||||
onSubmit={confirmArchiveChannel}
|
||||
/>
|
||||
}
|
||||
<ChatSearchDialog
|
||||
open={searchOpen}
|
||||
onClose={() => setSearchOpen(false)}
|
||||
onSearch={runChatSearch}
|
||||
searchText={searchText}
|
||||
setSearchText={setSearchText}
|
||||
searchQuery={searchQuery}
|
||||
results={searchData?.chatSearch?.results || []}
|
||||
loading={searchLoading}
|
||||
hasSearched={searchQuery !== ""}
|
||||
onSelectResult={selectSearchResult}
|
||||
viewUTCTime={currentMe?.user?.view_utc_time}
|
||||
/>
|
||||
|
||||
+2423
-1898
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@ type ChatContainerRequestMessage struct {
|
||||
ResponseMessageID int `json:"response_message_id" mapstructure:"response_message_id"`
|
||||
Model string `json:"model" mapstructure:"model"`
|
||||
Prompt string `json:"prompt" mapstructure:"prompt"`
|
||||
Config map[string]interface{} `json:"config" mapstructure:"config"`
|
||||
Context []ChatContainerContextMessage `json:"context" mapstructure:"context"`
|
||||
Secrets map[string]interface{} `json:"secrets" mapstructure:"secrets"`
|
||||
}
|
||||
|
||||
@@ -776,6 +776,7 @@ func createAIChatMessage(c *gin.Context, operatorOperation *databaseStructs.Oper
|
||||
chatRespondError(c, err.Error())
|
||||
return
|
||||
}
|
||||
chatConfig := getChatChannelConfig(channel)
|
||||
contextIDs := make([]int, len(contextMessages))
|
||||
for i := range contextMessages {
|
||||
contextIDs[i] = contextMessages[i].ID
|
||||
@@ -786,6 +787,7 @@ func createAIChatMessage(c *gin.Context, operatorOperation *databaseStructs.Oper
|
||||
"response_message_id": responseMessageID,
|
||||
"retry_of_id": retryOf,
|
||||
"context_message_limit": chatContextMessageLimit,
|
||||
"config": chatConfig,
|
||||
})
|
||||
_, _ = database.DB.Exec(`UPDATE chat_request
|
||||
SET context_snapshot=$3::jsonb
|
||||
@@ -801,6 +803,7 @@ func createAIChatMessage(c *gin.Context, operatorOperation *databaseStructs.Oper
|
||||
ResponseMessageID: responseMessageID,
|
||||
Model: channel.ChatModel,
|
||||
Prompt: message,
|
||||
Config: chatConfig,
|
||||
Context: contextMessages,
|
||||
Secrets: rabbitmq.GetSecrets(operatorOperation.CurrentOperator.ID, 0),
|
||||
}, authContext)
|
||||
@@ -961,6 +964,16 @@ func chatJSONText(input interface{}) databaseStructs.MythicJSONText {
|
||||
return rabbitmq.GetMythicJSONTextFromStruct(input)
|
||||
}
|
||||
|
||||
func getChatChannelConfig(channel databaseStructs.ChatChannel) map[string]interface{} {
|
||||
metadata := channel.AIMetadata.StructValue()
|
||||
for _, key := range []string{"config", "configuration"} {
|
||||
if config, ok := metadata[key].(map[string]interface{}); ok {
|
||||
return config
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
func getChatContainer(containerID int) (databaseStructs.ConsumingContainer, error) {
|
||||
container := databaseStructs.ConsumingContainer{}
|
||||
err := database.DB.Get(&container, `SELECT *
|
||||
|
||||
Reference in New Issue
Block a user