Added AI popup again to see how it works :3

This commit is contained in:
Frikky
2025-08-27 00:05:26 +02:00
parent f82c4751d4
commit 249b8dbf2c
4 changed files with 318 additions and 83 deletions
+3 -1
View File
@@ -463,6 +463,7 @@ const EditWorkflow = (props) => {
> >
<span> <span>
<Button <Button
id="ai-generate-button"
disabled={ disabled={
name.length === 0 || name.length === 0 ||
aiGenerateLoading === true || aiGenerateLoading === true ||
@@ -744,6 +745,7 @@ const EditWorkflow = (props) => {
{newWorkflow === true ? {newWorkflow === true ?
<TextField <TextField
id="Workflow-Description"
onBlur={(event) => { onBlur={(event) => {
innerWorkflow.default_return_value = event.target.value innerWorkflow.default_return_value = event.target.value
setInnerWorkflow(innerWorkflow) setInnerWorkflow(innerWorkflow)
@@ -867,7 +869,7 @@ const EditWorkflow = (props) => {
{!uploadedImage ? ( {!uploadedImage ? (
<div <div
style={{ style={{
border: `2px solid rgba(255,255,255,0.3)`, border: `1px solid ${theme.palette.primary.main}`,
borderRadius: 8, borderRadius: 8,
padding: 20, padding: 20,
textAlign: 'center', textAlign: 'center',
+10 -10
View File
@@ -53,14 +53,14 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
: selectedOrganization.sso_config.SSORequired); : selectedOrganization.sso_config.SSORequired);
const [ssoCertificate, setSsoCertificate] = React.useState( const [ssoCertificate, setSsoCertificate] = React.useState(
selectedOrganization.sso_config === undefined selectedOrganization.sso_config === undefined
? "" ? ""
: selectedOrganization.sso_config.sso_certificate === undefined || : selectedOrganization.sso_config.sso_certificate &&
selectedOrganization.sso_config.sso_certificate.length === 0 selectedOrganization.sso_config.sso_certificate.length > 0
? "" ? selectedOrganization.sso_config.sso_certificate
: selectedOrganization.sso_config.sso_certificate : selectedOrganization.sso_config.sso_long_certificate || ""
); );
const [openidClientId, setOpenidClientId] = React.useState( const [openidClientId, setOpenidClientId] = React.useState(
selectedOrganization.sso_config === undefined selectedOrganization.sso_config === undefined
? "" ? ""
@@ -115,8 +115,8 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
setOpenidToken(selectedOrganization?.sso_config?.openid_token) setOpenidToken(selectedOrganization?.sso_config?.openid_token)
} }
if (ssoCertificate !== selectedOrganization?.sso_config?.sso_certificate) { if (ssoCertificate !== selectedOrganization?.sso_config?.sso_certificate || ssoCertificate !== selectedOrganization?.sso_config?.sso_long_certificate) {
setSsoCertificate(selectedOrganization?.sso_config?.sso_certificate) setSsoCertificate(selectedOrganization?.sso_config?.sso_certificate || selectedOrganization?.sso_config?.sso_long_certificate)
} }
if (ssoEntrypoint !== selectedOrganization?.sso_config?.sso_entrypoint) { if (ssoEntrypoint !== selectedOrganization?.sso_config?.sso_entrypoint) {
+1 -2
View File
@@ -934,9 +934,8 @@ const AngularWorkflow = (defaultprops) => {
// Add keyboard shortcut handler in your main component // Add keyboard shortcut handler in your main component
useEffect(() => { useEffect(() => {
const handleKeyPress = (event) => { const handleKeyPress = (event) => {
const hasSupport = userdata?.support === true;
if (hasSupport && ((event.metaKey || event.ctrlKey) && event.key === 'f')) { if (((event.metaKey || event.ctrlKey) && event.key === 'f')) {
// Check if any modal is currently open // Check if any modal is currently open
const isAnyModalOpen = codeEditorModalOpen || executionModalOpen || const isAnyModalOpen = codeEditorModalOpen || executionModalOpen ||
+304 -70
View File
@@ -128,6 +128,18 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b
const svgSize = 24; const svgSize = 24;
const imagesize = 23; const imagesize = 23;
// Session-based modal visibility helper
const AI_ANNOUNCEMENT_SESSION_KEY = "ai_announcement_session";
const getCookie = (name) => {
if (typeof document === "undefined") return "";
const pattern = `; ${document.cookie}`;
const parts = pattern.split(`; ${name}=`);
if (parts.length === 2) {
return decodeURIComponent(parts.pop().split(";").shift() || "");
}
return "";
};
@@ -791,6 +803,7 @@ const Workflows2 = (props) => {
const [firstLoad, setFirstLoad] = React.useState(true); const [firstLoad, setFirstLoad] = React.useState(true);
const [aiAnnouncementModalOpen, setAiAnnouncementModalOpen] = React.useState(false); const [aiAnnouncementModalOpen, setAiAnnouncementModalOpen] = React.useState(false);
const sessionRef = useRef("");
const [showMoreClicked, setShowMoreClicked] = React.useState(false); const [showMoreClicked, setShowMoreClicked] = React.useState(false);
const [usecases, setUsecases] = React.useState([]); const [usecases, setUsecases] = React.useState([]);
const [allUsecases, setAllUsecases] = React.useState({ const [allUsecases, setAllUsecases] = React.useState({
@@ -900,23 +913,129 @@ const Workflows2 = (props) => {
} }
React.useEffect(() => { React.useEffect(() => {
if (!isLoggedIn) return;
const bannerID = "banner_ai_announcement"; const bannerID = "banner_ai_announcement";
const cookieSession = getCookie("__session") || "";
if (isLoggedIn === true && userdata && userdata.tutorials !== undefined && userdata.tutorials !== null) { sessionRef.current = cookieSession;
// Check if user has already dismissed this banner - tutorials is array of objects with 'name' field
const alreadyDismissed = userdata.tutorials.some(tutorial => tutorial.name === bannerID); try {
const storedSession = localStorage.getItem(AI_ANNOUNCEMENT_SESSION_KEY) || "";
if (!alreadyDismissed && !aiAnnouncementModalOpen) {
// Show the banner as the user hasn't seen it yet // If backend says it's dismissed, sync storage and exit
//setAiAnnouncementModalOpen(true); if (userdata && Array.isArray(userdata.tutorials)) {
const alreadyDismissed = userdata.tutorials.some((t) => t?.name === bannerID);
if (alreadyDismissed) {
if (cookieSession && storedSession !== cookieSession) {
localStorage.setItem(AI_ANNOUNCEMENT_SESSION_KEY, cookieSession);
}
return;
} }
} else if (!userdata) {
// Wait for userdata to load
return;
}
// Open only if not dismissed in this session
if (cookieSession && storedSession !== cookieSession) {
setAiAnnouncementModalOpen(true);
}
} catch {
// If storage is unavailable, fallback to a single open per mount
setAiAnnouncementModalOpen((open) => open || true);
} }
}, [isLoggedIn, userdata]); }, [isLoggedIn, userdata]);
const handleCloseAiAnnouncement = React.useCallback(() => {
try {
const cookieSession = sessionRef.current || getCookie("__session") || "";
if (cookieSession) {
localStorage.setItem(AI_ANNOUNCEMENT_SESSION_KEY, cookieSession);
}
} catch {
// ignore storage access issues
}
setAiAnnouncementModalOpen(false);
}, []);
const dismissAiAnnouncement = () => { const dismissAiAnnouncement = () => {
const bannerID = "banner_ai_announcement"; const bannerID = "banner_ai_announcement";
setAiAnnouncementModalOpen(false); handleCloseAiAnnouncement();
// Open Create Workflow modal (EditWorkflow) and temporarily highlight inputs/buttons
try {
setModalOpen(true)
setIsEditing(false)
setNewWorkflowName("")
setNewWorkflowDescription("")
setDefaultReturnValue("")
setEditingWorkflow({})
setNewWorkflowTags([])
setSelectedUsecases([])
// Wait a moment for the drawer to mount, then highlight
setTimeout(() => {
const BORDER = '#4CAF50';
const DURATION = 1500;
const highlightTextField = (inputEl) => {
if (!inputEl) return;
const formControl = inputEl.closest('.MuiFormControl-root') || inputEl.closest('.MuiInputBase-root') || inputEl.parentElement;
const inputRoot = formControl?.querySelector('.MuiOutlinedInput-root') || formControl?.querySelector('.MuiInputBase-root') || formControl;
const notch = formControl?.querySelector('fieldset');
const prevBoxShadow = inputRoot?.style?.boxShadow;
const prevBorderColor = notch?.style?.borderColor;
const prevBorderWidth = notch?.style?.borderWidth;
if (inputRoot) inputRoot.style.boxShadow = '0 0 0 3px rgba(76,175,80,0.45)';
if (notch) {
notch.style.borderColor = BORDER;
notch.style.borderWidth = '2px';
}
setTimeout(() => {
try {
if (inputRoot) inputRoot.style.boxShadow = prevBoxShadow || '';
if (notch) {
notch.style.borderColor = prevBorderColor || '';
notch.style.borderWidth = prevBorderWidth || '';
}
} catch (_) {}
}, DURATION);
};
const highlightButton = (btnEl) => {
if (!btnEl) return;
const prevBoxShadow = btnEl.style.boxShadow;
const prevBorder = btnEl.style.border;
const prevRadius = btnEl.style.borderRadius;
btnEl.style.boxShadow = '0 0 0 3px rgba(76,175,80,0.45)';
btnEl.style.border = '2px solid ' + BORDER;
btnEl.style.borderRadius = '6px';
setTimeout(() => {
try {
btnEl.style.boxShadow = prevBoxShadow || '';
btnEl.style.border = prevBorder || '';
btnEl.style.borderRadius = prevRadius || '';
} catch (_) {}
}, DURATION);
};
const nameEl = document.getElementById('Enter-Workflow-Name');
highlightTextField(nameEl);
const descEl = document.getElementById('Workflow-Description');
highlightTextField(descEl);
const aiBtn = document.getElementById('ai-generate-button');
highlightButton(aiBtn);
}, 250);
} catch (e) {
console.debug('Failed to open and highlight Create Workflow modal:', e);
}
fetch(globalUrl + '/api/v1/users/updateuser', { fetch(globalUrl + '/api/v1/users/updateuser', {
method: 'PUT', method: 'PUT',
@@ -1223,70 +1342,184 @@ const Workflows2 = (props) => {
const aiAnnouncementModal = aiAnnouncementModalOpen ? ( const aiAnnouncementModal = aiAnnouncementModalOpen ? (
<Dialog <Dialog
open={aiAnnouncementModalOpen} open={aiAnnouncementModalOpen}
onClose={() => setAiAnnouncementModalOpen(false)} onClose={handleCloseAiAnnouncement}
PaperProps={{ TransitionComponent={Zoom}
style: { TransitionProps={{ timeout: 300 }}
backgroundColor: "#1a1a1a", PaperProps={{
color: "white", style: {
minWidth: 500, background: theme.palette.DialogStyle.backgroundColor,
maxWidth: 550, minWidth: isMobile ? "90vw" : 780,
padding: 30, maxWidth: isMobile ? "90vw" : 860,
borderRadius: theme?.palette?.DialogStyle?.borderRadius, borderRadius: 8,
border: "1px solid #333", overflow: "hidden",
}, transformOrigin: "center",
},
}}
>
<DialogTitle style={{ position: "relative", padding: 0, margin: 0 }}>
<IconButton
style={{
position: "absolute",
top: 10,
right: 10,
}} }}
onClick={() => handleCloseAiAnnouncement()}
aria-label="Close"
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent
sx={{
height: "380px",
overflow: "hidden",
}}
> >
<DialogTitle style={{ marginBottom: 0, textAlign: "center", position: "relative", paddingBottom: 10 }}> {/* Main two-column layout */}
<IconButton <Box
style={{ sx={{
position: "absolute", display: "flex",
top: -10, flexDirection: { xs: "column", sm: "row" },
right: -15, gap: { xs: 3, sm: 4 },
color: "rgba(255,255,255,0.7)", alignItems: "stretch",
}} p: { xs: 2.5, sm: 3 },
onClick={() => setAiAnnouncementModalOpen(false)} mt: 1,
}}
>
{/* Left: steps image (38%) */}
<Box
sx={{
flex: { xs: "0 0 auto", sm: "0 0 38%" },
maxWidth: { xs: "100%", sm: "38%" },
}}
>
<Box
component="img"
src="/aiGenerateWorkflowSteps.svg"
alt="AI workflow generation steps"
sx={{
width: "100%",
height: {xs: "auto", md: "80%"},
marginLeft: -2,
objectFit: "contain",
borderRadius: "6px",
}}
/>
</Box>
{/* Right: content (62%) */}
<Box
sx={{
flex: { xs: "1 1 auto", sm: "0 0 62%" },
maxWidth: { xs: "100%", sm: "62%" },
display: "flex",
flexDirection: "column",
gap: { xs: 1.5, sm: 2 },
py: 1.2,
}}
>
{/* NEW badge */}
<Box
sx={{
display: "inline-flex",
alignItems: "center",
gap: 1,
py: 0.5,
px: 1.25,
border: "1px solid #2bc07e",
color: "#f85a3e",
background: "transparent",
borderRadius: 999,
width: "fit-content",
fontSize: 10,
fontWeight: 600,
mb: { xs: 0.5, sm: 1 },
}}
>
<AutoAwesomeIcon sx={{ fontSize: 16, color: "#2bc07e" }} />
<Box
component="span"
sx={{
color: "#2bc07e",
}}
> >
<CloseIcon /> NEW
</IconButton> </Box>
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 15 }}> </Box>
<AutoAwesomeIcon style={{ marginRight: 12, color: "#f85a3e", fontSize: 32 }} />
<Typography variant="h5" style={{ color: "rgba(255,255,255,0.9)" }}>
🎉 Introducing AI Workflow Generation!
</Typography>
</div>
</DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.8)", textAlign: "left", paddingTop: 0 }}>
<Typography variant="body1" style={{ marginBottom: 18, fontSize: "15px", lineHeight: 1.5 }}>
🤖 Simply describe what you want your workflow to do, and our AI will automatically generate the workflow for you!
</Typography>
<Typography variant="body1" style={{ marginBottom: 18, fontSize: "15px", lineHeight: 1.5 }}> {/* Title */}
✨ <strong>Quick start:</strong> Click "Create Workflow" → Describe your workflow → "AI Generate" → Done! <Typography
</Typography> sx={{
fontWeight: 700,
lineHeight: 1.25,
fontFamily: theme.typography.fontFamily,
fontSize: { xs: "1.25rem", sm: "1.5rem" },
}}
>
Introducing AI Workflow Generation
</Typography>
<Typography variant="body1" style={{ marginBottom: 25, fontSize: "14px", lineHeight: 1.4, color: "rgba(255,255,255,0.6)" }}> {/* Body text */}
For self-hosted setups: <a href="https://shuffler.io/docs/AI-Features" target="_blank" rel="noopener noreferrer" style={{ color: "#f85a3e", textDecoration: "none" }}>setup docs</a> <Typography
</Typography> variant="body2"
sx={{
lineHeight: 1.7,
fontFamily: theme.typography.fontFamily,
}}
>
Simply describe what you want your workflow to do, and our AI
will automatically generate the workflow for you.
</Typography>
<div style={{ textAlign: "center", marginTop: 20 }}> <Typography
<Button variant="body2"
variant="contained" sx={{ fontFamily: theme.typography.fontFamily }}
onClick={dismissAiAnnouncement} >
style={{ <strong>Quick start:</strong> Create Workflow → Describe → AI
backgroundColor: "#f85a3e", Generate → Done.
color: "white", </Typography>
padding: "10px 25px",
fontSize: "15px", <Typography
textTransform: "none", variant="body2"
borderRadius: "6px" sx={{ fontFamily: theme.typography.fontFamily }}
}} >
> For self-hosted setups, see the{" "}
Got it, let's try it! 🚀 <Box
</Button> component="a"
</div> href="https://shuffler.io/docs/AI"
</DialogContent> target="_blank"
</Dialog> rel="noopener noreferrer"
sx={{ color: "#ff8544", textDecoration: "underline" }}
>
setup docs
</Box>
</Typography>
{/* CTA button */}
<Box sx={{ display: "flex", mt: { xs: 2, sm: 2.5 } }}>
<Button
variant="contained"
color="primary"
onClick={dismissAiAnnouncement}
disableElevation
sx={{
py: 1.1,
px: 2.7,
textTransform: "none",
mt: 3,
borderRadius: "8px",
fontSize: 14,
width: { xs: "100%", sm: "auto" },
}}
>
Let's try it out
</Button>
</Box>
</Box>
</Box>
</DialogContent>
</Dialog>
) : null; ) : null;
const deleteModal = deleteModalOpen ? ( const deleteModal = deleteModalOpen ? (
@@ -4950,6 +5183,7 @@ const Workflows2 = (props) => {
<Tooltip title="Show/Hide Workflow Runs for top workflows" placement="top"> <Tooltip title="Show/Hide Workflow Runs for top workflows" placement="top">
<IconButton <IconButton
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
onClick={() => { onClick={() => {
const newView = !showExecutionStats const newView = !showExecutionStats
@@ -5601,8 +5835,8 @@ const Workflows2 = (props) => {
} }
// Maybe use gridview or something, idk // Maybe use gridview or something, idk
//return <div style={isSafari ? safariStyle : {zoom: 0.7, minHeight: "80vh",}}>{loadedCheck}</div>; return <div style={isSafari ? safariStyle : {zoom: 0.7, minHeight: "80vh",}}>{loadedCheck}</div>;
return <div style={isSafari ? safariStyle : {minHeight: "80vh",}}>{loadedCheck}</div>; // return <div style={isSafari ? safariStyle : {minHeight: "80vh",}}>{loadedCheck}</div>;
}; };