diff --git a/frontend/src/components/HighlightedValueInSearch.jsx b/frontend/src/components/HighlightedValueInSearch.jsx
new file mode 100644
index 00000000..f707a6fc
--- /dev/null
+++ b/frontend/src/components/HighlightedValueInSearch.jsx
@@ -0,0 +1,257 @@
+import React, { useEffect, useRef, useCallback, useMemo } from 'react';
+import ReactJson from 'react-json-view-ssr';
+import CodeMirror from '@uiw/react-codemirror';
+import { python } from '@codemirror/lang-python';
+import { vscodeDark } from '@uiw/codemirror-theme-vscode';
+
+const HighlightedValueInSearch = ({ value, searchTerm, theme }) => {
+ const containerRef = useRef(null);
+ const preRef = useRef(null);
+ const scrollTimeoutRef = useRef(null);
+
+ const isJson = useCallback((str) => {
+ if (typeof str === 'object' && str !== null) return true;
+ if (typeof str !== 'string') return false;
+
+ const trimmed = str.trim();
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return false;
+
+ try {
+ JSON.parse(trimmed);
+ return true;
+ } catch {
+ return false;
+ }
+ }, []);
+
+ const getJsonValue = useCallback((val) => {
+ try {
+ return typeof val === 'object' ? val : JSON.parse(val.trim());
+ } catch {
+ return null;
+ }
+ }, []);
+
+ const highlightInJson = useCallback((jsonStr, term) => {
+ if (!term) return jsonStr;
+
+ try {
+ const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const regex = new RegExp(`(${escapedTerm})`, 'gi');
+ const highlightId = `highlight-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+
+ return jsonStr.replace(regex, `$1`);
+ } catch {
+ return jsonStr;
+ }
+ }, []);
+
+ const isPythonCode = useCallback((str) => {
+ if (typeof str !== 'string') return false;
+
+ const pythonPatterns = [
+ 'import ', 'from ', 'def ', 'class ', 'if __name__',
+ 'print(', 'return ', 'elif ', 'except:', 'try:', 'with ',
+ 'lambda ', 'yield ', 'async def', 'await '
+ ];
+
+ const trimmed = str.trim();
+ return pythonPatterns.some(pattern => trimmed.includes(pattern)) &&
+ (trimmed.includes('def') || trimmed.includes('class'));
+ }, []);
+
+ useEffect(() => {
+ if (!searchTerm || !containerRef.current || !preRef.current) return;
+
+ if (scrollTimeoutRef.current) {
+ clearTimeout(scrollTimeoutRef.current);
+ }
+
+ scrollTimeoutRef.current = setTimeout(() => {
+ try {
+ const container = containerRef.current;
+ const firstHighlight = preRef.current?.querySelector('mark');
+
+ if (firstHighlight && container) {
+ const containerRect = container.getBoundingClientRect();
+ const highlightRect = firstHighlight.getBoundingClientRect();
+ const scrollTop = highlightRect.top - containerRect.top + container.scrollTop - (container.clientHeight / 2);
+
+ container.scrollTo({ top: Math.max(0, scrollTop), behavior: 'smooth' });
+ }
+ } catch (error) {
+ console.warn('Auto-scroll failed:', error);
+ }
+ }, 100);
+
+ return () => {
+ if (scrollTimeoutRef.current) {
+ clearTimeout(scrollTimeoutRef.current);
+ }
+ };
+ }, [searchTerm, value]);
+
+ const RegularText = useCallback(({ value: textValue, searchTerm: term }) => {
+ if (!textValue) return No content;
+
+ const stringValue = String(textValue);
+
+ // Check if it's Python code
+ if (isPythonCode(stringValue)) {
+ const lines = stringValue.split('\n');
+ const isNodeView = lines[0]?.startsWith('Node:');
+ const nodeTitle = isNodeView ? lines[0] : null;
+ const codeContent = isNodeView ? lines.slice(1).join('\n') : stringValue;
+
+ return (
+
+ {nodeTitle && (
+
+ {nodeTitle}
+
+ )}
+
+
+
+
+
+ );
+ }
+
+ if (!term) {
+ return (
+
+ {stringValue}
+
+ );
+ }
+
+ try {
+ const parts = stringValue.split(term);
+ return (
+
+ {parts.map((part, index) => (
+
+ {part}
+ {index < parts.length - 1 && (
+
+ {term}
+
+ )}
+
+ ))}
+
+ );
+ } catch {
+ return {stringValue};
+ }
+ }, [isPythonCode]);
+
+ if (value == null) {
+ return null;
+ }
+
+ try {
+ if (isJson(value)) {
+ const jsonValue = getJsonValue(value);
+
+ if (jsonValue === null) {
+ return ;
+ }
+
+ const jsonString = JSON.stringify(jsonValue, null, 2);
+ const hasMatch = searchTerm && jsonString.toLowerCase().includes(searchTerm.toLowerCase());
+
+ return (
+
+ {hasMatch ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ return ;
+
+ } catch (error) {
+ console.error('HighlightedValueInSearch error:', error);
+ return ;
+ }
+};
+
+export default React.memo(HighlightedValueInSearch);
\ No newline at end of file
diff --git a/frontend/src/components/WorkflowGenerationModal.jsx b/frontend/src/components/WorkflowGenerationModal.jsx
new file mode 100644
index 00000000..156057ed
--- /dev/null
+++ b/frontend/src/components/WorkflowGenerationModal.jsx
@@ -0,0 +1,268 @@
+import React, { useEffect, useState } from "react";
+
+import { getTheme } from '../theme.jsx';
+import { toast } from "react-toastify"
+
+import {
+ Button,
+ Dialog,
+ IconButton,
+ Typography,
+ CircularProgress,
+ Tooltip,
+ TextareaAutosize,
+ TextField,
+ ButtonGroup,
+} from "@mui/material";
+
+import {
+ Close as CloseIcon,
+ DragIndicator as DragIndicatorIcon,
+ Send as SendIcon,
+} from '@mui/icons-material';
+
+const WorkflowGenerationModal = (props) => {
+
+ const {
+ open = false,
+ supportEmail = "support@shuffler.io",
+ isMobile = false,
+ theme = null,
+ workflow={},
+ setWorkflow = () => {},
+ saveWorkflow = () => {},
+ setWorkflowGenerationModalOpen = () => {},
+ isCloud = false,
+ globalUrl = "",
+ } = props;
+
+ const [isFocused, setIsFocused] = useState(false);
+ const [workflowDescription, setWorkflowDescription] = useState("");
+ const [isAiEditing, setIsAiEditing] = React.useState(false);
+ const [backupWorkflow, setBackupWorkflow] = React.useState(null);
+
+ const currentTheme = theme || getTheme("dark");
+ const hasBackup = backupWorkflow !== null && backupWorkflow !== undefined
+
+
+ const handleKeyDown = (event) => {
+
+ if (open === false) {
+ return
+ }
+
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
+ event.preventDefault()
+ const tryItButton = document.getElementById("try-it-button")
+ if (tryItButton !== undefined && tryItButton !== null) {
+ tryItButton.click()
+ } else {
+ const keepChangesButton = document.getElementById("keep-changes-button")
+ if (keepChangesButton !== undefined && keepChangesButton !== null) {
+ keepChangesButton.click()
+ }
+ }
+ }
+
+ // Ctrl+Z
+ if ((event.metaKey || event.ctrlKey) && event.key === 'z') {
+ event.preventDefault()
+ const discardChangesButton = document.getElementById("discard-changes-button")
+ if (discardChangesButton !== undefined && discardChangesButton !== null) {
+ discardChangesButton.click()
+ }
+ }
+
+ // Escape
+ if (event.key === 'Escape') {
+ setWorkflowDescription("");
+ setIsAiEditing(false);
+ setWorkflowGenerationModalOpen(false);
+ }
+}
+
+ useEffect(() => {
+ document.addEventListener("keydown", handleKeyDown)
+ return () => document.removeEventListener("keydown", handleKeyDown)
+ }, [handleKeyDown])
+
+ const discardAiWorkflow = () => {
+ if (backupWorkflow === null || backupWorkflow === undefined) {
+ toast("No backup workflow to discard to.");
+ return;
+ }
+
+ // Deep copy to avoid reference issues and reset state
+ const restored = JSON.parse(JSON.stringify(backupWorkflow));
+ setWorkflow(restored);
+ setBackupWorkflow(null);
+ setWorkflowDescription("");
+ setIsAiEditing(false); // Reset loading state
+ setWorkflowGenerationModalOpen(false);
+
+ toast.success("Changes discarded and previous workflow restored.");
+ };
+
+ const editAIWorkflow = () => {
+ setIsAiEditing(true);
+ setBackupWorkflow(null);
+
+ var envToSend = isCloud ? "Cloud" : "Shuffle"
+ for (var actionkey in workflow?.actions) {
+ envToSend = workflow?.actions[actionkey]?.environment
+ break
+ }
+
+ const data = {
+ query: workflowDescription,
+ workflow_id: workflow?.id,
+ environment: envToSend,
+ }
+
+ fetch(`${globalUrl}/api/v2/workflows/edit/llm`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify(data),
+ credentials: "include",
+ })
+ .then((response) => {
+ return response.json().then((json) => {
+ if (response.status !== 200) {
+ toast.error(json.message || "Unexpected response. Please contact support@shuffler.io if this persists.");
+ setIsAiEditing(false);
+ return null;
+ }
+
+ if (json.success === true && typeof json.message === "string") {
+ toast(json.message);
+ setIsAiEditing(false);
+ return null;
+ }
+
+ if (json.success === false) {
+ toast(json.message || "Operation failed");
+ setIsAiEditing(false);
+ return null;
+ }
+
+ if (!json || Object.keys(json).length === 0) {
+ toast("AI edit failed: empty response");
+ setIsAiEditing(false);
+ return null;
+ }
+
+ toast.success("Workflow load done. Choose what to do.");
+ setBackupWorkflow(JSON.parse(JSON.stringify(workflow)));
+ setWorkflow(json);
+ setIsAiEditing(false);
+ return json;
+ });
+ })
+ .catch((error) => {
+ console.error("AI Workflow Edit Error:", error);
+ toast.error(`Failed to load LLM response due to: ${error.message || error}`);
+ setIsAiEditing(false);
+ });
+ };
+
+ const handleEdit = () => {
+ if (workflowDescription.trim() === "") {
+ return
+ }
+
+ editAIWorkflow()
+ };
+
+ const handleDiscard = () => {
+ setWorkflowDescription("");
+
+ }
+
+ const handleKeep = () => {
+ // Keep the AI-provided workflow: do not restore the backup.
+ // Clear the stored backup and reset modal state.
+ saveWorkflow(workflow);
+ setBackupWorkflow(null)
+ setWorkflowDescription("")
+ setIsAiEditing(false)
+ setWorkflowGenerationModalOpen(false);
+ };
+
+ if (open === false) {
+ if (hasBackup) {
+ }
+
+ return null
+ }
+
+ return (
+
+
+
+ {hasBackup && !isAiEditing ?
+
+
+
+
+
+ :
+
+ setWorkflowDescription(e.target.value)}
+ disabled={isAiEditing}
+ onFocus={() => setIsFocused(true)}
+ onBlur={() => setIsFocused(false)}
+ fullWidth
+
+ InputProps={{
+ endAdornment: (
+
+ )
+ }}
+ />
+ }
+
+
+ AI Edits require you to manually review and accept changes.
You can discard unwanted edits. Uses your configured LLM or shuffler.io AI credits. Alpha feature.
+
+
+
+ );
+};
+
+export default WorkflowGenerationModal;
\ No newline at end of file