diff --git a/autoflow-frontend/src/components/visualization/WorkflowGraph.jsx b/autoflow-frontend/src/components/visualization/WorkflowGraph.jsx
index 1f48b40..937b591 100644
--- a/autoflow-frontend/src/components/visualization/WorkflowGraph.jsx
+++ b/autoflow-frontend/src/components/visualization/WorkflowGraph.jsx
@@ -1,14 +1,22 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
+import { useWorkflowStore } from '../../store/workflowStore';
import ReactFlow, {
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
- useReactFlow
+ useReactFlow,
+ MarkerType
} from 'reactflow';
import 'reactflow/dist/style.css';
import { CustomNode } from './CustomNode';
+import TriggerNode from '../nodes/TriggerNode';
+import ActionNode from '../nodes/ActionNode';
+import ConditionNode from '../nodes/ConditionNode';
+import WhatsAppNode from '../nodes/WhatsAppNode';
+import InventoryNode from '../nodes/InventoryNode';
+import DelayNode from '../nodes/DelayNode';
// Simple ID generator
const generateId = () => `node_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@@ -24,71 +32,156 @@ const initialNodes = [
const initialEdges = [];
+// Custom Auto-Layout Function (Horizontal Directed Graph)
+const getLayoutedElements = (nodes, edges) => {
+ const nodeWidth = 350; // Horizontal spacing between columns
+ const nodeHeight = 150; // Vertical spacing between nodes
+
+ // 1. Calculate Depths (Longest Path) for Columns
+ const depths = {};
+ nodes.forEach(n => depths[n.id] = 0);
+
+ // Run relaxation N times to handle DAGs
+ for (let i = 0; i < nodes.length; i++) {
+ let changed = false;
+ edges.forEach(e => {
+ const sourceDepth = depths[e.source] || 0; // Use source depth
+ // If we have edges, verify structure
+ const targetDepth = depths[e.target] || 0;
+ if (sourceDepth + 1 > targetDepth) {
+ depths[e.target] = sourceDepth + 1;
+ changed = true;
+ }
+ });
+ if (!changed) break; // Optimization
+ }
+
+ // 2. Group by Depth (Columns)
+ const levels = {};
+ Object.entries(depths).forEach(([id, depth]) => {
+ if (!levels[depth]) levels[depth] = [];
+ levels[depth].push(id);
+ });
+
+ // 3. Assign Positions
+ return nodes.map(node => {
+ const depth = depths[node.id] || 0; // Column Index
+ const levelNodes = levels[depth];
+ const indexInLevel = levelNodes.indexOf(node.id);
+
+ // Calculate center offset for Y-axis (Vertical spread within column)
+ const columnHeight = levelNodes.length * nodeHeight;
+ const yOffset = indexInLevel * nodeHeight - (columnHeight / 2);
+
+ return {
+ ...node,
+ position: {
+ x: depth * nodeWidth + 100, // Moves Right (Column)
+ y: yOffset + 300 // Moves Down (Row) + Center Buffer
+ },
+ targetPosition: 'left',
+ sourcePosition: 'right'
+ };
+ });
+};
+
+const defaultEdgeOptions = { type: 'smoothstep', animated: true, style: { stroke: '#555', strokeWidth: 2 } };
+
export const WorkflowGraph = ({ workflowData }) => {
- const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
- const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
+ const nodes = useWorkflowStore(state => state.nodes);
+ const edges = useWorkflowStore(state => state.edges);
+ const setNodes = useWorkflowStore(state => state.setNodes);
+ const setEdges = useWorkflowStore(state => state.setEdges);
+ const onNodesChange = useWorkflowStore(state => state.onNodesChange);
+ const onEdgesChange = useWorkflowStore(state => state.onEdgesChange);
+ const setSelectedNode = useWorkflowStore(state => state.setSelectedNode);
+
const reactFlowInstance = useReactFlow();
const reactFlowWrapper = useRef(null);
// Register custom node types
- const nodeTypes = useMemo(() => ({ custom: CustomNode }), []);
+ const nodeTypes = useMemo(() => ({
+ trigger: TriggerNode,
+ action: ActionNode,
+ condition: ConditionNode,
+ whatsapp: WhatsAppNode,
+ inventory: InventoryNode,
+ delay: DelayNode,
+ custom: CustomNode
+ }), []);
// Handle initial workflow data from AI
useEffect(() => {
- if (workflowData && workflowData.steps) {
- const aiNodes = workflowData.steps.map((step, index) => {
- // FORCE First Node to be the Agent
- if (index === 0) {
- return {
- id: step.id || generateId(),
- type: 'custom',
- position: { x: 50, y: 250 },
- data: { label: 'Your Agent', category: 'AutoFlow' }
- };
- }
+ const incomingNodes = workflowData?.nodes || workflowData?.steps;
- // Subsequent nodes: Position them in a fan/grid to the right
- // Alternating Y positions to show branching
- const yOffset = (index - 1) * 150;
- return {
- id: step.id || generateId(),
- type: 'custom',
- position: { x: 450, y: 100 + yOffset },
- data: {
- label: step.data?.label || "AI Step",
- type: step.type === 'trigger' ? 'trigger' : (step.data?.label?.includes('?') ? 'condition' : 'action')
- },
- };
- });
+ if (incomingNodes && Array.isArray(incomingNodes) && incomingNodes.length > 0) {
- // Connect ALL nodes to the Central Agent (Index 0)
- // Use the specific bottom handles we added
- const agentId = aiNodes[0].id;
- const aiEdges = aiNodes.slice(1).map((node, i) => {
- // Distribute connections across the 3 bottom handles + right handle
- let handleId = 'right';
- if (i === 0) handleId = 'bottom-1';
- else if (i === 1) handleId = 'bottom-2';
- else if (i === 2) handleId = 'bottom-3';
-
- return {
- id: `e${agentId}-${node.id}`,
- source: agentId,
- target: node.id,
- sourceHandle: handleId,
- targetHandle: 'left',
- animated: true,
- type: 'default',
- style: { stroke: '#555', strokeWidth: 2 }
+ // 1. RAW NODES & EDGES GENERATION
+ let rawNodes = incomingNodes.map((node, index) => ({
+ id: node.id || generateId(),
+ type: 'custom',
+ data: {
+ label: node.data?.label || node.label || "Step",
+ type: node.type || node.data?.type || (node.label?.includes('?') ? 'condition' : 'action'),
+ category: index === 0 ? 'AutoFlow' : 'AI'
+ },
+ position: { x: 0, y: 0 } // Will be calculated
+ }));
+
+ let rawEdges = [];
+ const aiIds = rawNodes.map(n => n.id);
+
+ incomingNodes.forEach((node, i) => {
+ const sourceId = node.id || aiIds[i];
+
+ // Helper to add edge
+ const addEdgeToGraph = (targetId, label = '', color = '#555') => {
+ rawEdges.push({
+ id: `e${sourceId}-${targetId}-${Math.random()}`,
+ source: sourceId,
+ target: targetId,
+ type: 'smoothstep', // Better looking edges
+ animated: true,
+ label: label,
+ style: { stroke: color, strokeWidth: 2 },
+ markerEnd: { type: MarkerType.ArrowClosed, color: color },
+ });
};
+
+ // Parsing Logic
+ if (node.next && Array.isArray(node.next)) {
+ node.next.forEach(tid => addEdgeToGraph(tid));
+ }
+ if (node.data?.true_id) addEdgeToGraph(node.data.true_id, 'Yes', 'green');
+ if (node.data?.false_id) addEdgeToGraph(node.data.false_id, 'No', 'red');
+ if (node.data?.outputs) {
+ Object.entries(node.data.outputs).forEach(([k, tid]) => addEdgeToGraph(tid, k));
+ }
});
- setNodes(aiNodes);
- setEdges(aiEdges);
+ // Fallback: Linear
+ if (rawEdges.length === 0 && rawNodes.length > 1) {
+ for (let i = 0; i < rawNodes.length - 1; i++) {
+ rawEdges.push({
+ id: `e-fallback-${rawNodes[i].id}-${rawNodes[i + 1].id}`,
+ source: rawNodes[i].id,
+ target: rawNodes[i + 1].id,
+ type: 'smoothstep',
+ animated: true,
+ style: { stroke: '#999', strokeDasharray: '5,5' }
+ });
+ }
+ }
+
+ // 2. APPLY AUTO-LAYOUT
+ const layoutedNodes = getLayoutedElements(rawNodes, rawEdges);
+
+ setNodes(layoutedNodes);
+ setEdges(rawEdges);
}
}, [workflowData, setNodes, setEdges]);
- const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
+ const onConnect = useCallback((params) => setEdges((eds) => addEdge({ ...params, type: 'smoothstep', animated: true }, eds)), [setEdges]);
const onDragOver = useCallback((event) => {
event.preventDefault();
@@ -98,31 +191,22 @@ export const WorkflowGraph = ({ workflowData }) => {
const onDrop = useCallback(
(event) => {
event.preventDefault();
-
const type = event.dataTransfer.getData('application/reactflow');
- if (typeof type === 'undefined' || !type) {
- return;
- }
+ if (typeof type === 'undefined' || !type) return;
- // Get viewport boundaries to calculate relative position correctly
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
-
const position = reactFlowInstance.project({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
});
- // Adjust for node center (approx half of w:200 h:70)
- position.x -= 100;
- position.y -= 35;
-
const newNode = {
id: generateId(),
type: 'custom',
position,
data: {
label: `${type.charAt(0).toUpperCase() + type.slice(1)}`,
- type: type // 'trigger', 'action', 'condition', or tool ID like 'whatsapp'
+ type: type
},
};
@@ -131,6 +215,11 @@ export const WorkflowGraph = ({ workflowData }) => {
[reactFlowInstance, setNodes]
);
+ const handleSelectionChange = useCallback((params) => {
+ const selected = params.nodes[0] || null;
+ setSelectedNode(selected);
+ }, [setSelectedNode]);
+
return (
{
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
+ onSelectionChange={handleSelectionChange}
nodeTypes={nodeTypes}
fitView
- defaultEdgeOptions={{ type: 'default', animated: true, style: { stroke: '#555', strokeWidth: 2 } }}
+ defaultEdgeOptions={defaultEdgeOptions}
>
diff --git a/autoflow-frontend/src/constants/tools.js b/autoflow-frontend/src/constants/tools.js
index 806d1a3..a23bc71 100644
--- a/autoflow-frontend/src/constants/tools.js
+++ b/autoflow-frontend/src/constants/tools.js
@@ -58,7 +58,7 @@ export const TOOL_CATEGORIES = [
name: 'AI & Intelligence',
tools: [
{ id: 'chatgpt', label: 'ChatGPT (OpenAI)', type: 'action', icon: 'Bot' },
- { id: 'gemini', label: 'Google Gemini', type: 'action', icon: 'Bot' },
+ { id: 'ollama', label: 'Ollama (qwen3:8b)', type: 'action', icon: 'Bot' },
{ id: 'claude', label: 'Claude AI', type: 'action', icon: 'Bot' },
{ id: 'dalle', label: 'DALLยทE Image', type: 'action', icon: 'Bot' },
{ id: 'whisper', label: 'Whisper Audio', type: 'action', icon: 'Bot' },
diff --git a/autoflow-frontend/src/data/tools.json b/autoflow-frontend/src/data/tools.json
new file mode 100644
index 0000000..a47b87f
--- /dev/null
+++ b/autoflow-frontend/src/data/tools.json
@@ -0,0 +1,644 @@
+[
+ {
+ "id": "google_sheets",
+ "name": "Google Sheets",
+ "category": "Productivity",
+ "icon": "Database"
+ },
+ {
+ "id": "slack",
+ "name": "Slack",
+ "category": "Communication",
+ "icon": "MessageSquare"
+ },
+ {
+ "id": "gmail",
+ "name": "Gmail",
+ "category": "Communication",
+ "icon": "Mail"
+ },
+ {
+ "id": "stripe",
+ "name": "Stripe",
+ "category": "Payment",
+ "icon": "CreditCard"
+ },
+ {
+ "id": "salesforce",
+ "name": "Salesforce",
+ "category": "CRM",
+ "icon": "Users"
+ },
+ {
+ "id": "hubspot",
+ "name": "HubSpot",
+ "category": "CRM",
+ "icon": "Users"
+ },
+ {
+ "id": "notion",
+ "name": "Notion",
+ "category": "Productivity",
+ "icon": "FileText"
+ },
+ {
+ "id": "airtable",
+ "name": "Airtable",
+ "category": "Database",
+ "icon": "Database"
+ },
+ {
+ "id": "twilio",
+ "name": "Twilio",
+ "category": "Communication",
+ "icon": "Phone"
+ },
+ {
+ "id": "whatsapp_business",
+ "name": "WhatsApp Business",
+ "category": "Communication",
+ "icon": "MessageCircle"
+ },
+ {
+ "id": "discord",
+ "name": "Discord",
+ "category": "Communication",
+ "icon": "MessageSquare"
+ },
+ {
+ "id": "telegram",
+ "name": "Telegram",
+ "category": "Communication",
+ "icon": "Send"
+ },
+ {
+ "id": "zoom",
+ "name": "Zoom",
+ "category": "Communication",
+ "icon": "Video"
+ },
+ {
+ "id": "microsoft_teams",
+ "name": "Microsoft Teams",
+ "category": "Communication",
+ "icon": "Users"
+ },
+ {
+ "id": "jira",
+ "name": "Jira",
+ "category": "Project Management",
+ "icon": "CheckSquare"
+ },
+ {
+ "id": "trello",
+ "name": "Trello",
+ "category": "Project Management",
+ "icon": "Layout"
+ },
+ {
+ "id": "asana",
+ "name": "Asana",
+ "category": "Project Management",
+ "icon": "CheckCircle"
+ },
+ {
+ "id": "monday",
+ "name": "Monday.com",
+ "category": "Project Management",
+ "icon": "Calendar"
+ },
+ {
+ "id": "clickup",
+ "name": "ClickUp",
+ "category": "Project Management",
+ "icon": "CheckSquare"
+ },
+ {
+ "id": "github",
+ "name": "GitHub",
+ "category": "Development",
+ "icon": "Github"
+ },
+ {
+ "id": "gitlab",
+ "name": "GitLab",
+ "category": "Development",
+ "icon": "GitBranch"
+ },
+ {
+ "id": "bitbucket",
+ "name": "Bitbucket",
+ "category": "Development",
+ "icon": "Code"
+ },
+ {
+ "id": "aws_lambda",
+ "name": "AWS Lambda",
+ "category": "Development",
+ "icon": "Cloud"
+ },
+ {
+ "id": "google_cloud_functions",
+ "name": "Google Cloud Functions",
+ "category": "Development",
+ "icon": "Cloud"
+ },
+ {
+ "id": "azure_functions",
+ "name": "Azure Functions",
+ "category": "Development",
+ "icon": "Cloud"
+ },
+ {
+ "id": "heroku",
+ "name": "Heroku",
+ "category": "Development",
+ "icon": "Cloud"
+ },
+ {
+ "id": "netlify",
+ "name": "Netlify",
+ "category": "Development",
+ "icon": "Globe"
+ },
+ {
+ "id": "vercel",
+ "name": "Vercel",
+ "category": "Development",
+ "icon": "Globe"
+ },
+ {
+ "id": "docker",
+ "name": "Docker",
+ "category": "Development",
+ "icon": "Box"
+ },
+ {
+ "id": "kubernetes",
+ "name": "Kubernetes",
+ "category": "Development",
+ "icon": "Server"
+ },
+ {
+ "id": "mailchimp",
+ "name": "Mailchimp",
+ "category": "Marketing",
+ "icon": "Mail"
+ },
+ {
+ "id": "klaviyo",
+ "name": "Klaviyo",
+ "category": "Marketing",
+ "icon": "Mail"
+ },
+ {
+ "id": "convertkit",
+ "name": "ConvertKit",
+ "category": "Marketing",
+ "icon": "Mail"
+ },
+ {
+ "id": "activecampaign",
+ "name": "ActiveCampaign",
+ "category": "Marketing",
+ "icon": "Mail"
+ },
+ {
+ "id": "sendgrid",
+ "name": "SendGrid",
+ "category": "Marketing",
+ "icon": "Mail"
+ },
+ {
+ "id": "facebook_ads",
+ "name": "Facebook Ads",
+ "category": "Marketing",
+ "icon": "Facebook"
+ },
+ {
+ "id": "google_ads",
+ "name": "Google Ads",
+ "category": "Marketing",
+ "icon": "Search"
+ },
+ {
+ "id": "linkedin_ads",
+ "name": "LinkedIn Ads",
+ "category": "Marketing",
+ "icon": "Linkedin"
+ },
+ {
+ "id": "tiktok_ads",
+ "name": "TikTok Ads",
+ "category": "Marketing",
+ "icon": "Video"
+ },
+ {
+ "id": "shopify",
+ "name": "Shopify",
+ "category": "E-commerce",
+ "icon": "ShoppingCart"
+ },
+ {
+ "id": "woocommerce",
+ "name": "WooCommerce",
+ "category": "E-commerce",
+ "icon": "ShoppingCart"
+ },
+ {
+ "id": "magento",
+ "name": "Magento",
+ "category": "E-commerce",
+ "icon": "ShoppingCart"
+ },
+ {
+ "id": "bigcommerce",
+ "name": "BigCommerce",
+ "category": "E-commerce",
+ "icon": "ShoppingCart"
+ },
+ {
+ "id": "paypal",
+ "name": "PayPal",
+ "category": "Payment",
+ "icon": "DollarSign"
+ },
+ {
+ "id": "square",
+ "name": "Square",
+ "category": "Payment",
+ "icon": "CreditCard"
+ },
+ {
+ "id": "quickbooks",
+ "name": "QuickBooks",
+ "category": "Finance",
+ "icon": "FileText"
+ },
+ {
+ "id": "xero",
+ "name": "Xero",
+ "category": "Finance",
+ "icon": "FileText"
+ },
+ {
+ "id": "mysql",
+ "name": "MySQL",
+ "category": "Database",
+ "icon": "Database"
+ },
+ {
+ "id": "postgresql",
+ "name": "PostgreSQL",
+ "category": "Database",
+ "icon": "Database"
+ },
+ {
+ "id": "mongodb",
+ "name": "MongoDB",
+ "category": "Database",
+ "icon": "Database"
+ },
+ {
+ "id": "redis",
+ "name": "Redis",
+ "category": "Database",
+ "icon": "Database"
+ },
+ {
+ "id": "firebase",
+ "name": "Firebase",
+ "category": "Development",
+ "icon": "Flame"
+ },
+ {
+ "id": "supabase",
+ "name": "Supabase",
+ "category": "Database",
+ "icon": "Database"
+ },
+ {
+ "id": "openai",
+ "name": "OpenAI (GPT-4)",
+ "category": "AI/ML",
+ "icon": "Bot"
+ },
+ {
+ "id": "anthropic",
+ "name": "Anthropic (Claude)",
+ "category": "AI/ML",
+ "icon": "Bot"
+ },
+ {
+ "id": "huggingface",
+ "name": "Hugging Face",
+ "category": "AI/ML",
+ "icon": "Bot"
+ },
+ {
+ "id": "pinecone",
+ "name": "Pinecone",
+ "category": "AI/ML",
+ "icon": "Database"
+ },
+ {
+ "id": "zendesk",
+ "name": "Zendesk",
+ "category": "Support",
+ "icon": "Headphones"
+ },
+ {
+ "id": "intercom",
+ "name": "Intercom",
+ "category": "Support",
+ "icon": "MessageCircle"
+ },
+ {
+ "id": "freshdesk",
+ "name": "Freshdesk",
+ "category": "Support",
+ "icon": "Headphones"
+ },
+ {
+ "id": "calendly",
+ "name": "Calendly",
+ "category": "Productivity",
+ "icon": "Calendar"
+ },
+ {
+ "id": "google_calendar",
+ "name": "Google Calendar",
+ "category": "Productivity",
+ "icon": "Calendar"
+ },
+ {
+ "id": "outlook_calendar",
+ "name": "Outlook Calendar",
+ "category": "Productivity",
+ "icon": "Calendar"
+ },
+ {
+ "id": "dropbox",
+ "name": "Dropbox",
+ "category": "Storage",
+ "icon": "HardDrive"
+ },
+ {
+ "id": "google_drive",
+ "name": "Google Drive",
+ "category": "Storage",
+ "icon": "HardDrive"
+ },
+ {
+ "id": "onedrive",
+ "name": "OneDrive",
+ "category": "Storage",
+ "icon": "HardDrive"
+ },
+ {
+ "id": "box",
+ "name": "Box",
+ "category": "Storage",
+ "icon": "HardDrive"
+ },
+ {
+ "id": "typeform",
+ "name": "Typeform",
+ "category": "Forms",
+ "icon": "FileText"
+ },
+ {
+ "id": "google_forms",
+ "name": "Google Forms",
+ "category": "Forms",
+ "icon": "FileText"
+ },
+ {
+ "id": "jotform",
+ "name": "JotForm",
+ "category": "Forms",
+ "icon": "FileText"
+ },
+ {
+ "id": "survey_monkey",
+ "name": "SurveyMonkey",
+ "category": "Forms",
+ "icon": "FileText"
+ },
+ {
+ "id": "zapier",
+ "name": "Zapier",
+ "category": "Automation",
+ "icon": "Zap"
+ },
+ {
+ "id": "make",
+ "name": "Make (Integromat)",
+ "category": "Automation",
+ "icon": "Zap"
+ },
+ {
+ "id": "n8n",
+ "name": "n8n",
+ "category": "Automation",
+ "icon": "Zap"
+ },
+ {
+ "id": "ifttt",
+ "name": "IFTTT",
+ "category": "Automation",
+ "icon": "Zap"
+ },
+ {
+ "id": "wordpress",
+ "name": "WordPress",
+ "category": "CMS",
+ "icon": "Globe"
+ },
+ {
+ "id": "webflow",
+ "name": "Webflow",
+ "category": "CMS",
+ "icon": "Globe"
+ },
+ {
+ "id": "wix",
+ "name": "Wix",
+ "category": "CMS",
+ "icon": "Globe"
+ },
+ {
+ "id": "squarespace",
+ "name": "Squarespace",
+ "category": "CMS",
+ "icon": "Globe"
+ },
+ {
+ "id": "ghost",
+ "name": "Ghost",
+ "category": "CMS",
+ "icon": "Globe"
+ },
+ {
+ "id": "medium",
+ "name": "Medium",
+ "category": "CMS",
+ "icon": "FileText"
+ },
+ {
+ "id": "reddit",
+ "name": "Reddit",
+ "category": "Social",
+ "icon": "MessageSquare"
+ },
+ {
+ "id": "youtube",
+ "name": "YouTube",
+ "category": "Social",
+ "icon": "Video"
+ },
+ {
+ "id": "twitch",
+ "name": "Twitch",
+ "category": "Social",
+ "icon": "Video"
+ },
+ {
+ "id": "spotify",
+ "name": "Spotify",
+ "category": "Media",
+ "icon": "Headphones"
+ },
+ {
+ "id": "soundcloud",
+ "name": "SoundCloud",
+ "category": "Media",
+ "icon": "Headphones"
+ },
+ {
+ "id": "vimeo",
+ "name": "Vimeo",
+ "category": "Media",
+ "icon": "Video"
+ },
+ {
+ "id": "twilio_sendgrid",
+ "name": "Twilio SendGrid",
+ "category": "Communication",
+ "icon": "Mail"
+ },
+ {
+ "id": "mailgun",
+ "name": "Mailgun",
+ "category": "Communication",
+ "icon": "Mail"
+ },
+ {
+ "id": "postmark",
+ "name": "Postmark",
+ "category": "Communication",
+ "icon": "Mail"
+ },
+ {
+ "id": "algolia",
+ "name": "Algolia",
+ "category": "Search",
+ "icon": "Search"
+ },
+ {
+ "id": "elasticsearch",
+ "name": "Elasticsearch",
+ "category": "Search",
+ "icon": "Search"
+ },
+ {
+ "id": "mixpanel",
+ "name": "Mixpanel",
+ "category": "Analytics",
+ "icon": "BarChart"
+ },
+ {
+ "id": "amplitude",
+ "name": "Amplitude",
+ "category": "Analytics",
+ "icon": "BarChart"
+ },
+ {
+ "id": "segment",
+ "name": "Segment",
+ "category": "Analytics",
+ "icon": "BarChart"
+ },
+ {
+ "id": "google_analytics",
+ "name": "Google Analytics",
+ "category": "Analytics",
+ "icon": "BarChart"
+ },
+ {
+ "id": "datadog",
+ "name": "Datadog",
+ "category": "Monitoring",
+ "icon": "Activity"
+ },
+ {
+ "id": "new_relic",
+ "name": "New Relic",
+ "category": "Monitoring",
+ "icon": "Activity"
+ },
+ {
+ "id": "sentry",
+ "name": "Sentry",
+ "category": "Monitoring",
+ "icon": "AlertTriangle"
+ },
+ {
+ "id": "pagerduty",
+ "name": "PagerDuty",
+ "category": "Monitoring",
+ "icon": "AlertTriangle"
+ },
+ {
+ "id": "grafana",
+ "name": "Grafana",
+ "category": "Monitoring",
+ "icon": "Activity"
+ },
+ {
+ "id": "prometheus",
+ "name": "Prometheus",
+ "category": "Monitoring",
+ "icon": "Activity"
+ },
+ {
+ "id": "cloudflare",
+ "name": "Cloudflare",
+ "category": "Security",
+ "icon": "Shield"
+ },
+ {
+ "id": "auth0",
+ "name": "Auth0",
+ "category": "Security",
+ "icon": "Lock"
+ },
+ {
+ "id": "okta",
+ "name": "Okta",
+ "category": "Security",
+ "icon": "Lock"
+ },
+ {
+ "id": "1password",
+ "name": "1Password",
+ "category": "Security",
+ "icon": "Key"
+ },
+ {
+ "id": "lastpass",
+ "name": "LastPass",
+ "category": "Security",
+ "icon": "Key"
+ }
+]
\ No newline at end of file
diff --git a/autoflow-frontend/src/index.css b/autoflow-frontend/src/index.css
index 55d2777..08cbcd6 100644
--- a/autoflow-frontend/src/index.css
+++ b/autoflow-frontend/src/index.css
@@ -3,7 +3,44 @@
@tailwind utilities;
@layer base {
+ :root {
+ --background: 0 0% 0%;
+ --foreground: 0 0% 100%;
+
+ --card: 0 0% 6%;
+ --card-foreground: 0 0% 100%;
+
+ --popover: 0 0% 6%;
+ --popover-foreground: 0 0% 100%;
+
+ --primary: 0 0% 100%;
+ --primary-foreground: 0 0% 0%;
+
+ --secondary: 0 0% 15%;
+ --secondary-foreground: 0 0% 100%;
+
+ --muted: 0 0% 15%;
+ --muted-foreground: 0 0% 60%;
+
+ --accent: 0 0% 15%;
+ --accent-foreground: 0 0% 100%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 100%;
+
+ --border: 0 0% 15%;
+ --input: 0 0% 15%;
+ --ring: 0 0% 100%;
+
+ --radius: 0.5rem;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
body {
- @apply antialiased text-gray-900 bg-gray-50;
+ @apply bg-background text-foreground;
}
}
diff --git a/autoflow-frontend/src/main.jsx b/autoflow-frontend/src/main.jsx
deleted file mode 100644
index 54b39dd..0000000
--- a/autoflow-frontend/src/main.jsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App.jsx'
-import './index.css'
-
-ReactDOM.createRoot(document.getElementById('root')).render(
-
-
- ,
-)
diff --git a/autoflow-frontend/src/main.tsx b/autoflow-frontend/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/autoflow-frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/autoflow-frontend/src/pages/Builder.jsx b/autoflow-frontend/src/pages/Builder.jsx
deleted file mode 100644
index 0f334ca..0000000
--- a/autoflow-frontend/src/pages/Builder.jsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import { useState } from 'react';
-import { ReactFlowProvider } from 'reactflow';
-import { ChatInterface } from '../components/builder/ChatInterface';
-import { WorkflowGraph } from '../components/visualization/WorkflowGraph';
-import { AIExplanation } from '../components/builder/AIExplanation';
-import { Modal } from '../components/ui/Modal';
-import { TestMode } from '../components/simulation/TestMode';
-import { NodePalette } from '../components/builder/NodePalette';
-import { Play, ToggleLeft, ToggleRight, PenTool } from 'lucide-react';
-import clsx from 'clsx';
-
-const Builder = () => {
- const [workflow, setWorkflow] = useState(null);
- const [isTestOpen, setIsTestOpen] = useState(false);
- const [isCustomMode, setIsCustomMode] = useState(false);
-
- const handleWorkflowGenerated = (data) => {
- console.log("Workflow generated:", data);
- setWorkflow(data);
- };
-
- return (
-
-
-
- {/* Chat / Sidebar */}
-
-
- {/* Visual Canvas with Provider */}
-
-
-
-
- {/* Floating Overlay for AI Explanation */}
-
-
-
- {/* Manual Builder Palette - Only visible in Custom Mode */}
-
-
-
-
-
-
-
- {/* Test Mode Modal */}
-
setIsTestOpen(false)}
- title="Test & Simulation"
- >
-
-
-
- );
-};
-
-export default Builder;
diff --git a/autoflow-frontend/src/pages/Chat.tsx b/autoflow-frontend/src/pages/Chat.tsx
new file mode 100644
index 0000000..e97ed33
--- /dev/null
+++ b/autoflow-frontend/src/pages/Chat.tsx
@@ -0,0 +1,82 @@
+import { Send, Bot, User } from "lucide-react";
+
+export default function Chat() {
+ return (
+
+ {/* Header */}
+
+
+ {/* Chat History Viewport */}
+
+
+ {/* System Greeting */}
+
+
+
+
+
+
AutoFlow Native Agent Just now
+
+ Hello! This is a secure testing environment connected directly to your n8n workflow. Try asking me about shipping policies, product inventory, or anything you've trained me on.
+
+
+
+
+ {/* User Message */}
+
+
+
+
+
+
2 minutes ago You
+
+ Do you offer bulk discounts for B2B distributors on the new FOSS automation tier?
+
+
+
+
+ {/* System Response */}
+
+
+
+
+
+
AutoFlow Native Agent 1 minute ago
+
+ Yes, absolutely! We offer special bulk pricing for MSMEs and B2B distributors ordering more than 10 licenses.
+
+ Would you like me to connect you with our enterprise sales automation flow to process a purchase order automatically?
+
+
+ Yes, connect me
+ View pricing tier list
+
+
+
+
+
+
+ {/* Input Area */}
+
+
+ );
+}
diff --git a/autoflow-frontend/src/pages/Dashboard.jsx b/autoflow-frontend/src/pages/Dashboard.jsx
deleted file mode 100644
index 8bb7ca9..0000000
--- a/autoflow-frontend/src/pages/Dashboard.jsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { Link } from 'react-router-dom';
-import { ROIDashboard } from '../components/dashboard/ROIDashboard';
-import { Plus, Zap, Settings } from 'lucide-react';
-
-const Dashboard = () => {
- return (
-
- {/* Header */}
-
-
-
-
-
-
Welcome back, Sibam ๐
-
Here's how your automation is performing today.
-
-
-
- New Automation
-
-
-
- {/* ROI Dashboard */}
-
-
- {/* Active Automations List */}
-
-
Active Workflows
-
-
- {/* Item 1 */}
-
-
-
-
-
-
-
Product Price Inquiry
-
Triggers on "price", "cost"
-
-
-
-
- Active
-
- 24m ago
-
-
-
- {/* Item 2 */}
-
-
-
-
-
-
-
Order Status Check
-
Triggers on "track", "where is"
-
-
-
-
- Active
-
- 1h ago
-
-
-
-
-
-
- );
-};
-
-// Start Icon for the list
-import { MessageSquare } from 'lucide-react';
-
-export default Dashboard;
diff --git a/autoflow-frontend/src/pages/Dashboard.tsx b/autoflow-frontend/src/pages/Dashboard.tsx
new file mode 100644
index 0000000..b739fdd
--- /dev/null
+++ b/autoflow-frontend/src/pages/Dashboard.tsx
@@ -0,0 +1,94 @@
+import { Search, Bell, Copy } from "lucide-react";
+
+export default function Dashboard() {
+ return (
+
+ {/* Top Header */}
+
+
+ {/* Page Content */}
+
+
+ {/* Metric Cards */}
+
+
+
Total Contacts
+
8,047
+
+15% this week
+
+
+
Active Deals
+
127
+
+8% this week
+
+
+
Messages Handled
+
14,350
+
+22% this week
+
+
+
Deployment Status
+
2 Agents Live
+
WhatsApp: Connected
+
+ Generate New Agent
+
+
+
+
+ {/* Live Agents Section */}
+
+
Your Active Deployments
+
+
+
+
+ Agent Name
+ Platform
+ Status
+ Webhook URL
+
+
+
+
+
+ CS
+ Customer Support
+
+ WhatsApp Business
+ LIVE
+ https://api.autoflow.in/wh/xyz
+
+
+
+ FA
+ FAQ & Catalog Bot
+
+ Telegram
+ TESTING
+ https://api.autoflow.in/wh/abc
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/autoflow-frontend/src/pages/DeployPage.tsx b/autoflow-frontend/src/pages/DeployPage.tsx
new file mode 100644
index 0000000..0844227
--- /dev/null
+++ b/autoflow-frontend/src/pages/DeployPage.tsx
@@ -0,0 +1,273 @@
+// @ts-nocheck
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { Send, CheckCircle, Clock, RefreshCw, Smartphone, ChevronLeft } from 'lucide-react';
+import clsx from 'clsx';
+import { motion } from 'framer-motion';
+import { QRCodeSVG } from 'qrcode.react';
+import { useNavigate } from 'react-router-dom';
+import { workflowApi } from '../services/workflowApi';
+
+export default function DeployPage() {
+ const navigate = useNavigate();
+ const [messages, setMessages] = useState([]);
+ const [qrCode, setQrCode] = useState(null);
+ const [isConnected, setIsConnected] = useState(false);
+ const [isDeploying, setIsDeploying] = useState(true);
+ const [statusText, setStatusText] = useState("Initializing Agent...");
+
+ const deployAgent = async () => {
+ setIsDeploying(true);
+ setStatusText("Linking WhatsApp Bridge...");
+ try {
+ await workflowApi.deploy();
+ // Polling handles the rest
+ } catch (err) {
+ console.error("Deployment failed", err);
+ setStatusText("Deployment initialization failed.");
+ setIsDeploying(false);
+ }
+ };
+
+ useEffect(() => {
+ // Start deployment immediately on entry
+ deployAgent();
+ }, []);
+
+ const isMounted = useRef(true);
+ const isConnectedRef = useRef(false);
+
+ useEffect(() => {
+ return () => { isMounted.current = false; };
+ }, []);
+
+ const checkStatus = useCallback(async () => {
+ try {
+ const data = await workflowApi.getStatus();
+ if (!isMounted.current) return;
+
+ if (data.connected) {
+ setIsConnected(true);
+ isConnectedRef.current = true;
+ setQrCode(null);
+ setIsDeploying(false);
+ setStatusText("Agent Active & Listening");
+
+ setMessages(prev => {
+ if (prev.length === 0) {
+ return [{
+ role: 'bot',
+ text: "๐ Agent Deployed Successfully!\n\nI am now live and responding to messages on this WhatsApp account using your custom workflow logic.",
+ time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+ }];
+ }
+ return prev;
+ });
+ } else if (data.qr) {
+ setQrCode(data.qr);
+ setIsDeploying(false);
+ setStatusText("Scan QR Code to Deploy");
+ } else if (!data.connecting && !data.connected && !data.qr) {
+ setIsDeploying(false);
+ setStatusText("Ready to Deploy");
+ }
+ } catch (err) {
+ if (!isMounted.current) return;
+ console.warn("Status check failed", err.message);
+ setStatusText("Connecting to server...");
+ }
+ }, []);
+
+ useEffect(() => {
+ let timeoutId;
+ const poll = async () => {
+ if (isConnectedRef.current || !isMounted.current) return;
+ await checkStatus();
+ if (!isConnectedRef.current && isMounted.current) {
+ timeoutId = setTimeout(poll, 3000);
+ }
+ };
+ poll();
+ return () => clearTimeout(timeoutId);
+ }, [checkStatus]);
+
+ const handleLogout = async () => {
+ if (!confirm("Are you sure? This will disconnect the current WhatsApp session.")) return;
+ try {
+ await workflowApi.logout();
+ setIsConnected(false);
+ isConnectedRef.current = false;
+ setQrCode(null);
+ setIsDeploying(true);
+ setMessages([]);
+ deployAgent();
+ } catch (err) {
+ console.error("Logout failed", err);
+ }
+ };
+
+ return (
+
+ {/* Animated Background Line */}
+
+
+ {/* Back Button */}
+
navigate('/studio')}
+ className="absolute top-8 left-8 z-50 flex items-center gap-2 px-5 py-2.5 bg-[#252526] hover:bg-[#2d2d2d] rounded-xl shadow-2xl border border-white/5 text-gray-400 hover:text-white transition-all font-bold group"
+ >
+
+ Return to Studio
+
+
+ {/* Left Column: Phone & QR */}
+
+
+
Production Link
+
Final Deployment
+
Scan the secure handshake code below to finalize your real-time automation bridge.
+
+
+
+ {/* Shadow Glow */}
+
+
+ {/* iPhone Frame */}
+
+ {/* Notch */}
+
+
+ {/* Screen */}
+
+ {/* Header */}
+
+
+
๐ค
+
+
AutoFlow LeadGen
+
+ {isConnected ? "โ Authenticated" : "Handshaking..."}
+
+
+
+
+
+ {/* Dynamic Content */}
+
+ {isDeploying && !qrCode && (
+
+ )}
+
+ {qrCode && !isConnected && (
+
+
+
+
+
+ Link Production Device
+
+
+
+ )}
+
+ {isConnected && (
+
+
+ {messages.map((msg, idx) => (
+
+ {msg.text}
+ {msg.time}
+
+ ))}
+
+
+
+ Bridge Synchronized
+
+
+ )}
+
+
+
+
+
+
+ {/* Right Column: Deployment Progress */}
+
+
+
+
+
+
+ Status Cluster
+
+
System Initialization
+
+
+
+
+
+
+
+
+ {isConnected && (
+
+ Reset Production Session
+
+ )}
+
+
+
+ );
+}
+
+const DeploymentStep = ({ active, completed, title, desc }) => (
+
+);
diff --git a/autoflow-frontend/src/pages/Landing.tsx b/autoflow-frontend/src/pages/Landing.tsx
new file mode 100644
index 0000000..64a2d36
--- /dev/null
+++ b/autoflow-frontend/src/pages/Landing.tsx
@@ -0,0 +1,225 @@
+import { ArrowRight, Search, Bell, Settings, Home, BarChart2 } from "lucide-react";
+import { Link } from "react-router-dom";
+
+export default function Landing() {
+ return (
+
+ {/* Navbar */}
+
+
+
+ AutoFlow
+
+
+ Sign in
+
+
+ {/* Dramatic Glow Effect background */}
+
+
+ {/* Hero Content */}
+
+
+
New version of AutoFlow is out!
+
Read more
+
+
+
+ Empower your MSME with instant AI agents
+
+
+
+ Powered by open-source n8n. Type a prompt, scan a QR code, and deploy a live AI catalog and support agent to your favorite messaging app.
+
+
+
+ Get started
+
+
+
+ {/* Dashboard Mockup Base View */}
+
+
+
+ {/* Sidebar Area */}
+
+
+ CRM Studio
+
+
+
+ Dashboard
+
+
+ Flow Settings
+
+
+
+
+ {/* Main Content Area */}
+
+ {/* Header */}
+
+
+ {/* Content Cards */}
+
+
+
Total Contacts
+
8,047
+
+15% from last week
+
+
+
Active Deals
+
127
+
+8% from last week
+
+
+
Messages Handled
+
14,350
+
+22% from last week
+
+
+ {/* Large Activity Section Overlay */}
+
+
+ Agent Activity Stream
+ Last 24 Hours
+
+ {/* Subtle fade to bottom */}
+
+
+
+
+
+
+
+
+ {/* Bento Grid Features Section */}
+
+
+
Everything you need to automate.
+
Built with open-source tools for maximum flexibility and zero vendor lock-in.
+
+
+
+ {/* Card 1: Large */}
+
+
+
+
+
+
Open Source n8n
+
Fully transparent and extensible automation nodes generated instantly behind the scenes. Zero setup, zero vendor lock-in. Build powerful flows visually and securely on your own infrastructure.
+
+
+
+ {/* Card 2 */}
+
+
+
+
+
+
Deep Intent Search
+
Natural language processing to understand exactly what your customers need.
+
+
+
+ {/* Card 3 */}
+
+
+
+
+
+
Instant QR Deploy
+
Scan to test and deploy your agent on any mobile device immediately.
+
+
+
+ {/* Card 4 */}
+
+
+
+
Chat App Support
+
Integrates naturally with your customers' favorite messaging applications.
+
+
+
+ {/* Card 5: Medium spanning 2 columns */}
+
+
+
+
+
+
24/7 AI Catalog
+
Automated responses, product suggestions, and live support around the clock.
+
+
+ Explore capabilities →
+
+
+
+
+
+
+ {/* How It Works Section */}
+
+
+
+
How AutoFlow works
+
No coding or hosting required. We bridge the gap between complex n8n workflows and your daily interactions.
+
+ Read the docs
+
+
+
+
+ {/* Step 1 */}
+
+
01
+
Describe your business
+
Type a simple prompt explaining your inventory or FAQ in plain language. Our system determines the exact automation logic required.
+
+ {/* Step 2 */}
+
+
02
+
Auto-generate n8n nodes
+
AutoFlow instantly spins up a secure, invisible n8n workspace and automatically wires the webhooks, LLM agents, and logic nodes for you instantly.
+
+ {/* Step 3 */}
+
+
03
+
Scan and go live
+
Pull out your phone, scan the generated QR code, and your fully autonomous AI agent is instantly tethered to your messaging app. Zero deployment hassle.
+
+
+
+
+
+ {/* Simple Footer */}
+
+
+ )
+}
diff --git a/autoflow-frontend/src/pages/N8n.tsx b/autoflow-frontend/src/pages/N8n.tsx
new file mode 100644
index 0000000..991a627
--- /dev/null
+++ b/autoflow-frontend/src/pages/N8n.tsx
@@ -0,0 +1,75 @@
+import { Play, Download, Search, Settings } from "lucide-react";
+
+export default function N8n() {
+ return (
+
+ {/* Header */}
+
+
+
+
AutoFlow Lead Generation Webhook
+
+
+
+
+ Export
+
+
+ Execute Workflow
+
+
+
+
+ {/* Canvas Viewport */}
+
+
+
+ {/* Node Mockups */}
+
+
+
+ {/* Webhook Node */}
+
+
+
+
Webhook Lead Ingestion
+
+
+
POST /api/leads
+
Listens constantly for WhatsApp mobile intents
+
+ {/* Output Port */}
+
+
+
+ {/* Connection Line */}
+
+
+ {/* AI Node */}
+
+ {/* Input Port */}
+
+
+
+
+
gpt-4o-mini
+
Classifies intents mapping MSME business data
+
+
+
+
+
+
+
+ );
+}
diff --git a/autoflow-frontend/src/pages/NotFound.jsx b/autoflow-frontend/src/pages/NotFound.jsx
deleted file mode 100644
index 0add7bf..0000000
--- a/autoflow-frontend/src/pages/NotFound.jsx
+++ /dev/null
@@ -1,10 +0,0 @@
-const NotFound = () => {
- return (
-
- );
-};
-
-export default NotFound;
diff --git a/autoflow-frontend/src/pages/Studio.tsx b/autoflow-frontend/src/pages/Studio.tsx
new file mode 100644
index 0000000..5b20a7a
--- /dev/null
+++ b/autoflow-frontend/src/pages/Studio.tsx
@@ -0,0 +1,149 @@
+// @ts-nocheck
+import React, { useState } from 'react';
+import { ReactFlowProvider } from 'reactflow';
+import { WorkflowGraph } from '../components/visualization/WorkflowGraph';
+import { ChatInterface } from '../components/builder/ChatInterface';
+import { NodePalette } from '../components/builder/NodePalette';
+import { Play, Download, Settings, MousePointer2, Move, ChevronLeft, ChevronRight, Smartphone } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+import { Modal } from '../components/ui/Modal';
+import { TestMode } from '../components/simulation/TestMode';
+import 'reactflow/dist/style.css';
+
+export default function Studio() {
+ const [workflowData, setWorkflowData] = useState(null);
+ const [activeTab, setActiveTab] = useState("chat"); // 'chat' | 'settings'
+ const [isLeftOpen, setIsLeftOpen] = useState(true);
+ const [isRightOpen, setIsRightOpen] = useState(true);
+ const [isTestOpen, setIsTestOpen] = useState(false);
+ const navigate = useNavigate();
+
+ const handleWorkflowGenerated = (workflow) => {
+ setWorkflowData(workflow);
+ };
+
+ return (
+
+
+ {/* โโ LEFT PANEL: Toolbox โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */}
+
+
+
+ Toolbox
+
+
+
+ {/* Real functional palette embedded here */}
+
+ {/* If NodePalette has white backgrounds, we might need to modify it later, but here is the logic integration */}
+
+
+
+
+ Drag & Drop to build
+
+
+
+ {/* โโ CENTER PANEL: Canvas โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */}
+
+
+ {/* Toggle Left Panel */}
+ setIsLeftOpen(!isLeftOpen)}
+ className="absolute top-1/2 left-0 -translate-y-1/2 bg-[#252526] border border-l-0 border-black/40 p-1.5 rounded-r-md z-40 text-gray-400 hover:text-white shadow-md transition-colors"
+ >
+ {isLeftOpen ? : }
+
+
+ {/* Toggle Right Panel */}
+ setIsRightOpen(!isRightOpen)}
+ className="absolute top-1/2 right-0 -translate-y-1/2 bg-[#121212] border border-r-0 border-black/40 p-1.5 rounded-l-md z-40 text-gray-400 hover:text-white shadow-[-2px_0_5px_rgba(0,0,0,0.3)] transition-colors"
+ >
+ {isRightOpen ? : }
+
+
+ {/* Header toolbar over canvas */}
+
+
+
AutoFlow Lead Generation
+
Saved
+
+
+
+
setIsTestOpen(true)}
+ className="flex items-center gap-2 px-3 py-1.5 bg-amber-500/10 border border-amber-500/20 hover:bg-amber-500/20 text-amber-500 rounded font-medium text-xs transition-colors animate-pulse"
+ >
+ Test Automation
+
+
+ Export
+
+
navigate('/deploy-agent')}
+ className="flex items-center gap-2 px-3 py-1.5 bg-[#ff6d5a] hover:bg-[#ff6d5a]/90 text-white rounded font-medium text-xs shadow-md transition-colors"
+ >
+ Deploy Agent
+
+
+
+
+ {/* Canvas controls */}
+
+
+ {/* React Flow Canvas */}
+
+
+
+
+
+
+
+
+ {/* โโ RIGHT PANEL: Sandbox & Insights โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */}
+
+ {/* Tabs */}
+
+ setActiveTab('chat')}
+ className={`flex-1 py-4 font-semibold transition-colors ${activeTab === 'chat' ? 'text-amber-500 border-b-2 border-amber-500' : 'text-gray-400 hover:text-white'}`}
+ >
+ AI Insight Builder
+
+ setActiveTab('settings')}
+ className={`flex-1 py-4 font-semibold transition-colors ${activeTab === 'settings' ? 'text-amber-500 border-b-2 border-amber-500' : 'text-gray-400 hover:text-white'}`}
+ >
+ Settings
+
+
+
+ {activeTab === 'chat' && isRightOpen && (
+
+ {/* The existing functional Chat Interface */}
+
+
+ )}
+
+ {activeTab === 'settings' && isRightOpen && (
+
+
Workflow settings and properties will appear here.
+
+ )}
+
+
+ {/* โโ TEST MODAL โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */}
+
setIsTestOpen(false)}
+ title="Testing & Execution Phase"
+ >
+
+
+
+ );
+}
diff --git a/autoflow-frontend/src/services/workflowApi.js b/autoflow-frontend/src/services/workflowApi.js
index 51a3f97..9a3ea82 100644
--- a/autoflow-frontend/src/services/workflowApi.js
+++ b/autoflow-frontend/src/services/workflowApi.js
@@ -1,21 +1,62 @@
-import api from './api';
+import axios from 'axios';
+
+const api = axios.create({
+ baseURL: 'http://localhost:8000/api',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Separate instance for WhatsApp Bridge (Port 3001)
+const whatsappApi = axios.create({
+ baseURL: 'http://localhost:3001',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
export const workflowApi = {
// Generate a workflow from a text description
- generate: async (description) => {
- const response = await api.post('/generate-workflow', { userPrompt: description });
+ generate: async (description, fileContext = null) => {
+ const response = await api.post('/workflow/generate', { nl_input: description });
+ return response.data;
+ },
+
+ // Upload a file for context (inventory)
+ uploadFile: async (file) => {
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('name', file.name); // Required by FastAPI Form data
+ const response = await api.post('/inventory/upload', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ });
return response.data;
},
// Explain a workflow JSON
explain: async (workflow) => {
- const response = await api.post('/explain-workflow', { workflow });
+ const response = await api.post('/workflow/explain', workflow);
return response.data;
},
- // Simulate a message to test the engine
simulate: async (message) => {
- const response = await api.post('/simulate-message', { message });
+ const response = await api.post('/workflow/simulate', { message });
+ return response.data;
+ },
+
+ // WhatsApp Bridge (Using Port 3001 and specific bridge routes)
+ getStatus: async () => {
+ const response = await whatsappApi.get('/status');
+ return response.data;
+ },
+
+ deploy: async () => {
+ const response = await whatsappApi.post('/deploy');
+ return response.data;
+ },
+
+ logout: async () => {
+ const response = await whatsappApi.post('/logout');
return response.data;
},
};
diff --git a/autoflow-frontend/src/store/workflowStore.js b/autoflow-frontend/src/store/workflowStore.js
new file mode 100644
index 0000000..3b1311e
--- /dev/null
+++ b/autoflow-frontend/src/store/workflowStore.js
@@ -0,0 +1,48 @@
+import { create } from 'zustand';
+import { applyNodeChanges, applyEdgeChanges } from 'reactflow';
+
+export const useWorkflowStore = create((set, get) => ({
+ nodes: [
+ {
+ id: '1',
+ position: { x: 100, y: 200 },
+ data: { label: 'Your Agent', category: 'AutoFlow', type: 'trigger' },
+ type: 'custom'
+ }
+ ],
+ edges: [],
+ currentWorkflow: null, // Holds id, name, description, etc.
+ isActive: false,
+ explanation: null,
+ selectedNode: null,
+
+ setNodes: (nodes) => set({ nodes }),
+ setEdges: (edges) => set({ edges }),
+
+ onNodesChange: (changes) => {
+ set({
+ nodes: applyNodeChanges(changes, get().nodes),
+ });
+ },
+ onEdgesChange: (changes) => {
+ set({
+ edges: applyEdgeChanges(changes, get().edges),
+ });
+ },
+
+ setCurrentWorkflow: (workflow) => set({ currentWorkflow: workflow }),
+ setIsActive: (isActive) => set({ isActive }),
+ setExplanation: (explanation) => set({ explanation }),
+ setSelectedNode: (node) => set({ selectedNode: node }),
+
+ updateNodeData: (id, newData) => {
+ set({
+ nodes: get().nodes.map((node) => {
+ if (node.id === id) {
+ return { ...node, data: { ...node.data, ...newData } };
+ }
+ return node;
+ }),
+ });
+ },
+}));
diff --git a/autoflow-frontend/src/styles/landing.css b/autoflow-frontend/src/styles/landing.css
new file mode 100644
index 0000000..d63a47b
--- /dev/null
+++ b/autoflow-frontend/src/styles/landing.css
@@ -0,0 +1,3135 @@
+/* ===== CSS Variables & Design Tokens ===== */
+/* Add this to the very top of src/styles/landing.css */
+
+/* Force dark mode defaults for the landing page wrapper */
+.landing-page {
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ font-family: 'Inter', sans-serif;
+ min-height: 100vh;
+}
+
+/* Ensure global variables are accessible */
+:root {
+ /* Colors - Charcoal + Emerald Scheme */
+ --bg-primary: #0D0D0D;
+ --bg-secondary: #1A1A1A;
+ --color-primary: #10B981;
+ --color-accent: #34D399;
+ --text-primary: #FFFFFF;
+ --text-secondary: #B8B8C8;
+ --text-muted: #6B6B80;
+
+ /* WhatsApp Colors */
+ --wa-green: #075E54;
+ --wa-light-green: #25D366;
+ --wa-received: #202C33;
+ --wa-sent: #005C4B;
+}
+
+/* ... rest of your CSS ... */
+:root {
+ /* Colors - Charcoal + Emerald Scheme */
+ --bg-primary: #0D0D0D;
+ --bg-secondary: #1A1A1A;
+ --color-primary: #10B981;
+ --color-accent: #34D399;
+ --color-highlight: #6EE7B7;
+ --text-primary: #FFFFFF;
+ --text-secondary: #B8B8C8;
+ --text-muted: #6B6B80;
+
+ /* WhatsApp Colors */
+ --wa-green: #075E54;
+ --wa-light-green: #25D366;
+ --wa-bg: #0B141A;
+ --wa-chat-bg: #0B141A;
+ --wa-sent: #005C4B;
+ --wa-received: #202C33;
+
+ /* Typography */
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+
+ /* Spacing */
+ --container-max: 1440px;
+ --section-padding: 120px;
+
+ /* Animation */
+ --transition-fast: 0.2s ease;
+ --transition-medium: 0.4s ease;
+ --transition-slow: 0.6s ease;
+
+ /* Easing Functions */
+ --ease-smooth: cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ --ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ --ease-default: cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* ===== Reset & Base ===== */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ font-size: 16px;
+ scroll-behavior: smooth;
+}
+
+body {
+ font-family: var(--font-family);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ line-height: 1.6;
+ overflow-x: hidden;
+ min-height: 100vh;
+}
+
+/* ===== Background Effects ===== */
+.bg-gradient {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(180deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
+ z-index: -3;
+}
+
+/* Subtle Dot Grid Pattern */
+.bg-gradient::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-image:
+ radial-gradient(circle, rgba(52, 211, 153, 0.15) 1px, transparent 1px);
+ background-size: 40px 40px;
+ opacity: 0.3;
+ z-index: -2;
+}
+
+/* Noise Texture Overlay */
+.bg-gradient::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
+ opacity: 0.03;
+ mix-blend-mode: overlay;
+ z-index: -1;
+}
+
+/* Subtle Grid Pattern (replaces orbs) */
+.bg-glow {
+ display: none;
+ /* Remove orbs */
+}
+
+.bg-glow-1,
+.bg-glow-2 {
+ display: none;
+ /* Remove orbs */
+}
+
+
+/* ===== Hero Section ===== */
+.hero {
+ min-height: 90vh;
+ min-height: max(90vh, 700px);
+ display: flex;
+ align-items: center;
+ padding: 120px 100px;
+ position: relative;
+}
+
+.hero-container {
+ max-width: 1400px;
+ margin: 0 auto;
+ width: 100%;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 80px;
+ align-items: center;
+}
+
+/* ===== Hero Content (Left Side) ===== */
+.hero-content {
+ max-width: 600px;
+}
+
+.hero-headline {
+ font-size: 64px;
+ font-weight: 700;
+ line-height: 1.1;
+ margin-bottom: 24px;
+ letter-spacing: -0.02em;
+}
+
+.gradient-text {
+ background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 50%, var(--color-purple) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ background-size: 200% 200%;
+ animation: gradientShift 4s ease infinite;
+}
+
+@keyframes gradientShift {
+
+ 0%,
+ 100% {
+ background-position: 0% 50%;
+ }
+
+ 50% {
+ background-position: 100% 50%;
+ }
+}
+
+.hero-subheadline {
+ font-size: 22px;
+ font-weight: 400;
+ color: var(--text-secondary);
+ line-height: 1.5;
+ margin-bottom: 40px;
+}
+
+/* ===== CTA Button ===== */
+.cta-container {
+ margin-bottom: 40px;
+}
+
+.cta-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ padding: 18px 40px;
+ font-family: var(--font-family);
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 12px;
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+ transition: all 300ms var(--ease-default);
+ box-shadow: 0 10px 40px rgba(0, 102, 255, 0.3);
+}
+
+.cta-button::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+ transition: left 0.5s ease;
+}
+
+.cta-button:hover {
+ transform: translateY(-2px) scale(1.05);
+ box-shadow: 0 15px 50px rgba(0, 102, 255, 0.5);
+ background: linear-gradient(135deg, #0052CC 0%, var(--color-accent) 100%);
+}
+
+.cta-button:active {
+ transform: translateY(0) scale(0.98);
+ transition: all 100ms var(--ease-default);
+}
+
+.cta-button:hover::before {
+ left: 100%;
+}
+
+.cta-arrow {
+ width: 20px;
+ height: 20px;
+ transition: transform var(--transition-fast);
+}
+
+.cta-button:hover .cta-arrow {
+ transform: translateX(4px);
+}
+
+.cta-subtext {
+ margin-top: 12px;
+ font-size: 14px;
+ color: var(--text-muted);
+}
+
+/* ===== Trust Indicators ===== */
+.trust-indicators {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 24px;
+}
+
+.trust-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ color: var(--text-secondary);
+}
+
+.trust-check {
+ width: 18px;
+ height: 18px;
+ color: var(--wa-light-green);
+}
+
+/* ===== Hero Demo (Right Side) ===== */
+.hero-demo {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 600px;
+}
+
+/* ===== Workflow Visualization ===== */
+.workflow-bg {
+ position: absolute;
+ width: 150%;
+ height: 100%;
+ left: -25%;
+ pointer-events: none;
+ filter: blur(0.5px);
+ opacity: 0.9;
+}
+
+.workflow-node {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ opacity: 0.6;
+ transition: all 0.4s var(--ease-smooth);
+}
+
+.node-content {
+ width: 56px;
+ height: 56px;
+ background: rgba(0, 102, 255, 0.08);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ border: 1px solid rgba(0, 217, 255, 0.2);
+ border-radius: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow:
+ 0 4px 16px rgba(0, 102, 255, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ transition: all 0.4s var(--ease-smooth);
+}
+
+.node-content svg {
+ width: 26px;
+ height: 26px;
+ color: var(--color-accent);
+ opacity: 0.8;
+ transition: all 0.3s ease;
+}
+
+.node-label {
+ font-size: 11px;
+ font-weight: 500;
+ color: var(--text-muted);
+ white-space: nowrap;
+ opacity: 0.7;
+}
+
+/* Node Positions */
+#node-trigger {
+ top: 5%;
+ left: -5%;
+}
+
+#node-intent {
+ top: 25%;
+ left: 0%;
+}
+
+#node-database {
+ top: 60%;
+ left: -5%;
+}
+
+#node-format {
+ top: 80%;
+ left: 10%;
+}
+
+#node-send {
+ top: 75%;
+ right: 0%;
+}
+
+/* Active Node State */
+.workflow-node.active {
+ opacity: 1;
+ animation: nodeActivate 1.5s var(--ease-smooth);
+}
+
+.workflow-node.active .node-content {
+ background: rgba(0, 217, 255, 0.15);
+ border-color: var(--color-accent);
+ box-shadow:
+ 0 0 30px rgba(0, 217, 255, 0.4),
+ 0 4px 16px rgba(0, 102, 255, 0.2),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
+}
+
+.workflow-node.active .node-content svg {
+ color: var(--color-accent);
+ opacity: 1;
+ filter: drop-shadow(0 0 8px rgba(0, 217, 255, 0.6));
+}
+
+@keyframes nodeActivate {
+ 0% {
+ transform: scale(1);
+ }
+
+ 50% {
+ transform: scale(1.15);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+/* Connection Lines */
+.workflow-connections {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ opacity: 0.4;
+ pointer-events: none;
+ /* Ensure lines don't block clicks */
+}
+
+.workflow-line-path {
+ stroke-dashoffset: 100;
+ animation: flowLine 2s linear infinite;
+}
+
+@keyframes flowLine {
+ to {
+ stroke-dashoffset: 0;
+ }
+}
+
+.workflow-line-path.active {
+ opacity: 1;
+ animation: lineActivate 0.8s var(--ease-smooth) forwards;
+}
+
+@keyframes lineActivate {
+ from {
+ stroke-dashoffset: 100;
+ opacity: 0.4;
+ }
+
+ to {
+ stroke-dashoffset: 0;
+ opacity: 1;
+ }
+}
+
+/* ===== Floating Stats Badges ===== */
+.floating-stats {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 2;
+}
+
+.stat-badge {
+ position: absolute;
+ padding: 10px 16px;
+ background: rgba(10, 10, 15, 0.85);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border: 1px solid rgba(0, 217, 255, 0.3);
+ border-radius: 10px;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--text-primary);
+ white-space: nowrap;
+ opacity: 0;
+ box-shadow:
+ 0 8px 24px rgba(0, 0, 0, 0.4),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ animation: badgeFloat 4s ease-in-out infinite;
+}
+
+#stat-response {
+ top: 15%;
+ right: -15%;
+}
+
+#stat-confidence {
+ top: 45%;
+ left: -20%;
+}
+
+#stat-automated {
+ bottom: 20%;
+ right: -10%;
+}
+
+#stat-query {
+ top: 2%;
+ left: -15%;
+}
+
+@keyframes badgeFloat {
+
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+
+ 50% {
+ transform: translateY(-8px);
+ }
+}
+
+.stat-badge.show {
+ animation: badgeAppear 0.5s var(--ease-bounce) forwards;
+}
+
+@keyframes badgeAppear {
+ 0% {
+ opacity: 0;
+ transform: translateY(10px) scale(0.9);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.stat-badge.hide {
+ animation: badgeDisappear 0.4s var(--ease-smooth) forwards;
+}
+
+@keyframes badgeDisappear {
+ 0% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translateY(-10px) scale(0.95);
+ }
+}
+
+/* ===== Phone Mockup ===== */
+.phone-mockup {
+ position: relative;
+ z-index: 3;
+ animation: phoneFloat 4s ease-in-out infinite;
+}
+
+@keyframes phoneFloat {
+
+ 0%,
+ 100% {
+ transform: translateY(0) rotateY(-12deg) rotateX(5deg);
+ }
+
+ 50% {
+ transform: translateY(-15px) rotateY(-12deg) rotateX(5deg);
+ }
+}
+
+.phone-frame {
+ width: 300px;
+ height: 600px;
+ background: #1C1C1E;
+ border-radius: 44px;
+ padding: 12px;
+ position: relative;
+ box-shadow:
+ 0 0 0 2px #2C2C2E,
+ 0 25px 60px rgba(0, 0, 0, 0.5),
+ 0 0 100px rgba(0, 102, 255, 0.2),
+ 0 0 200px rgba(0, 217, 255, 0.1);
+ transform-style: preserve-3d;
+}
+
+.phone-notch {
+ position: absolute;
+ top: 12px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 100px;
+ height: 28px;
+ background: #000;
+ border-radius: 20px;
+ z-index: 10;
+}
+
+.phone-glow {
+ position: absolute;
+ bottom: -40px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 200px;
+ height: 60px;
+ background: radial-gradient(ellipse, rgba(0, 102, 255, 0.3) 0%, transparent 70%);
+ filter: blur(20px);
+}
+
+/* ===== WhatsApp Interface ===== */
+.whatsapp-interface {
+ width: 100%;
+ height: 100%;
+ background: var(--wa-bg);
+ border-radius: 34px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+/* WhatsApp Header */
+.wa-header {
+ background: var(--wa-green);
+ padding: 45px 12px 12px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.wa-back svg {
+ width: 24px;
+ height: 24px;
+ color: white;
+}
+
+.wa-avatar {
+ width: 36px;
+ height: 36px;
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+ font-size: 14px;
+}
+
+.wa-contact {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.wa-name {
+ font-size: 14px;
+ font-weight: 500;
+ color: white;
+}
+
+.wa-status {
+ font-size: 11px;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.wa-actions svg {
+ width: 20px;
+ height: 20px;
+ color: white;
+}
+
+/* WhatsApp Chat */
+.wa-chat {
+ flex: 1;
+ padding: 16px 12px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.02'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
+}
+
+.wa-message {
+ display: flex;
+ opacity: 0;
+ transform: translateY(20px);
+}
+
+.wa-message-received {
+ justify-content: flex-start;
+}
+
+.wa-message-sent {
+ justify-content: flex-end;
+}
+
+.wa-bubble {
+ max-width: 85%;
+ padding: 8px 12px;
+ border-radius: 12px;
+ font-size: 13px;
+ line-height: 1.4;
+ position: relative;
+}
+
+.wa-message-received .wa-bubble {
+ background: var(--wa-received);
+ border-top-left-radius: 4px;
+}
+
+.wa-message-sent .wa-bubble {
+ background: var(--wa-sent);
+ border-top-right-radius: 4px;
+}
+
+.wa-ai-badge {
+ display: inline-block;
+ padding: 2px 6px;
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
+ border-radius: 4px;
+ font-size: 9px;
+ font-weight: 600;
+ margin-right: 6px;
+ vertical-align: middle;
+}
+
+.wa-time {
+ display: block;
+ font-size: 10px;
+ color: rgba(255, 255, 255, 0.5);
+ text-align: right;
+ margin-top: 4px;
+}
+
+/* Typing Indicator */
+.wa-typing {
+ display: flex;
+ justify-content: flex-end;
+ opacity: 0;
+}
+
+.wa-typing .wa-bubble {
+ background: var(--wa-sent);
+ padding: 12px 16px;
+}
+
+.typing-dots {
+ display: flex;
+ gap: 4px;
+}
+
+.typing-dots span {
+ width: 8px;
+ height: 8px;
+ background: rgba(255, 255, 255, 0.6);
+ border-radius: 50%;
+ animation: typingBounce 1.4s ease-in-out infinite;
+}
+
+.typing-dots span:nth-child(2) {
+ animation-delay: 0.2s;
+}
+
+.typing-dots span:nth-child(3) {
+ animation-delay: 0.4s;
+}
+
+@keyframes typingBounce {
+
+ 0%,
+ 60%,
+ 100% {
+ transform: translateY(0);
+ }
+
+ 30% {
+ transform: translateY(-6px);
+ }
+}
+
+@keyframes messageAppear {
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* WhatsApp Input Bar */
+.wa-input-bar {
+ padding: 8px 12px;
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.wa-input-container {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: var(--wa-received);
+ border-radius: 24px;
+ padding: 8px 12px;
+}
+
+.wa-input-container input {
+ flex: 1;
+ background: none;
+ border: none;
+ color: white;
+ font-size: 13px;
+ outline: none;
+}
+
+.wa-input-container input::placeholder {
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.wa-emoji,
+.wa-attach {
+ width: 22px;
+ height: 22px;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.wa-mic {
+ width: 40px;
+ height: 40px;
+ background: var(--wa-light-green);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.wa-mic svg {
+ width: 20px;
+ height: 20px;
+ color: white;
+}
+
+/* ===== Animation Delays removed (Handled by JS) ===== */
+
+/* ===== Responsive Design ===== */
+@media (max-width: 1200px) {
+ .hero {
+ padding: 40px 40px;
+ }
+
+ .hero-headline {
+ font-size: 52px;
+ }
+
+ .hero-container {
+ gap: 60px;
+ }
+}
+
+@media (max-width: 1024px) {
+ .hero-container {
+ grid-template-columns: 1fr;
+ gap: 60px;
+ }
+
+ .hero-content {
+ text-align: center;
+ max-width: 100%;
+ order: 2;
+ }
+
+ .hero-demo {
+ order: 1;
+ min-height: 500px;
+ }
+
+ .trust-indicators {
+ justify-content: center;
+ }
+
+ .cta-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+}
+
+@media (max-width: 768px) {
+ .hero {
+ padding: 30px 20px;
+ }
+
+ .hero-headline {
+ font-size: 40px;
+ }
+
+ .hero-subheadline {
+ font-size: 18px;
+ margin-bottom: 32px;
+ }
+
+ .phone-frame {
+ width: 240px;
+ height: 500px;
+ }
+
+ .hero-demo {
+ min-height: 420px;
+ }
+
+ .phone-mockup {
+ animation: phoneFloatMobile 4s ease-in-out infinite;
+ }
+
+ @keyframes phoneFloatMobile {
+
+ 0%,
+ 100% {
+ transform: translateY(0) rotateY(-5deg) rotateX(3deg);
+ }
+
+ 50% {
+ transform: translateY(-10px) rotateY(-5deg) rotateX(3deg);
+ }
+ }
+
+ .workflow-bg {
+ opacity: 0.5;
+ }
+
+ .trust-indicators {
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .bg-glow-1,
+ .bg-glow-2 {
+ width: 300px;
+ height: 300px;
+ }
+}
+
+@media (max-width: 480px) {
+ .hero-headline {
+ font-size: 32px;
+ }
+
+ .cta-button {
+ padding: 16px 28px;
+ font-size: 16px;
+ }
+
+ .phone-frame {
+ width: 220px;
+ height: 460px;
+ }
+}
+
+/* ===== Comparison Section ===== */
+.comparison-section {
+ min-height: 100vh;
+ padding: 120px 60px;
+ background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
+ position: relative;
+ overflow: hidden;
+}
+
+.comparison-container {
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+.comparison-headline {
+ font-size: 56px;
+ font-weight: 700;
+ text-align: center;
+ margin-bottom: 80px;
+ background: linear-gradient(135deg, var(--text-primary) 0%, var(--text-secondary) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+/* Split Screen Comparison */
+.split-comparison {
+ display: grid;
+ grid-template-columns: 1fr 80px 1fr;
+ gap: 32px;
+ align-items: center;
+ max-width: 1100px;
+ /* Decreased overall width */
+ margin: 0 auto;
+ align-items: start;
+}
+
+.comparison-side {
+ background: rgba(10, 10, 15, 0.4);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ border-radius: 24px;
+ padding: 32px;
+ min-height: 500px;
+}
+
+.chaos-side {
+ border-color: rgba(255, 100, 100, 0.2);
+ background: rgba(30, 10, 10, 0.3);
+}
+
+.solution-side {
+ border-color: rgba(0, 217, 255, 0.2);
+ background: rgba(0, 20, 30, 0.3);
+}
+
+.side-label {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 32px;
+}
+
+.label-icon {
+ width: 24px;
+ height: 24px;
+}
+
+.chaos-side .label-icon {
+ color: #ef4444;
+}
+
+.solution-side .label-icon {
+ color: var(--color-accent);
+}
+
+.label-text {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ letter-spacing: -0.01em;
+}
+
+/* Chaos Container */
+.chaos-container,
+.solution-container {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.notification-stack,
+.resolved-stack {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.notification-item,
+.resolved-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ border-radius: 12px;
+ font-size: 14px;
+}
+
+.notification-item {
+ background: rgba(239, 68, 68, 0.05);
+ border: 1px solid rgba(239, 68, 68, 0.1);
+}
+
+.resolved-item {
+ background: rgba(16, 185, 129, 0.05);
+ border: 1px solid rgba(16, 185, 129, 0.1);
+}
+
+.notif-icon,
+.resolved-icon {
+ width: 20px;
+ height: 20px;
+}
+
+.notif-icon {
+ color: #ef4444;
+}
+
+.resolved-icon {
+ color: var(--color-accent);
+}
+
+.notif-content,
+.resolved-content {
+ flex: 1;
+}
+
+.notif-name,
+.resolved-name {
+ display: block;
+ font-weight: 600;
+ margin-bottom: 2px;
+}
+
+.notif-msg,
+.resolved-msg {
+ color: var(--text-secondary);
+ font-size: 13px;
+}
+
+.notif-time,
+.resolved-time {
+ font-size: 11px;
+ color: var(--text-muted);
+}
+
+.notification-badge {
+ text-align: center;
+ padding: 8px;
+ background: rgba(239, 68, 68, 0.1);
+ color: #ef4444;
+ font-size: 12px;
+ font-weight: 600;
+ border-radius: 8px;
+}
+
+/* Metrics Row */
+.metrics-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+}
+
+.chaos-metric,
+.solution-metric {
+ padding: 16px;
+ border-radius: 16px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.chaos-metric {
+ background: rgba(239, 68, 68, 0.03);
+ border: 1px solid rgba(239, 68, 68, 0.08);
+}
+
+.solution-metric {
+ background: rgba(16, 185, 129, 0.03);
+ border: 1px solid rgba(16, 185, 129, 0.08);
+}
+
+.metric-icon {
+ width: 24px;
+ height: 24px;
+}
+
+.chaos-metric .metric-icon {
+ color: #ef4444;
+}
+
+.solution-metric .metric-icon {
+ color: var(--color-accent);
+}
+
+.metric-value {
+ display: block;
+ font-size: 18px;
+ font-weight: 700;
+}
+
+.metric-label {
+ display: block;
+ font-size: 11px;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+/* Divider */
+.comparison-divider {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ position: relative;
+}
+
+.vs-badge {
+ width: 50px;
+ height: 50px;
+ background: var(--bg-secondary);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ font-weight: 700;
+ color: var(--text-secondary);
+ z-index: 2;
+}
+
+.divider-line {
+ position: absolute;
+ width: 1px;
+ height: 100%;
+ background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.1), transparent);
+ z-index: 1;
+}
+
+/* Happy Customers */
+.happy-customers {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px;
+}
+
+.customer-avatars {
+ display: flex;
+}
+
+.avatar,
+.avatar-plus {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ border: 2px solid var(--bg-primary);
+ background: var(--bg-secondary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ font-weight: 600;
+ margin-right: -8px;
+}
+
+.avatar-plus {
+ background: var(--color-primary);
+ color: white;
+ margin-right: 0;
+}
+
+.happy-text {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+/* Responsive Design */
+@media (max-width: 1024px) {
+ .split-comparison {
+ grid-template-columns: 1fr;
+ gap: 20px;
+ }
+
+ .comparison-divider {
+ height: 40px;
+ }
+
+ .divider-line {
+ width: 100%;
+ height: 1px;
+ }
+
+ .stats-highlight {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 768px) {
+ .comparison-section {
+ padding: 60px 20px;
+ }
+
+ .comparison-headline {
+ font-size: 36px;
+ margin-bottom: 60px;
+ }
+
+ .comparison-side {
+ padding: 24px;
+ min-height: auto;
+ }
+
+ .stat-number {
+ font-size: 36px;
+ }
+
+ .stat-text {
+ font-size: 14px;
+ }
+}
+
+@media (max-width: 1200px) {
+ .hero {
+ padding: 40px 40px;
+ }
+
+ .hero-headline {
+ font-size: 52px;
+ }
+
+ .hero-container {
+ gap: 60px;
+ }
+}
+
+@media (max-width: 1024px) {
+ .hero-container {
+ grid-template-columns: 1fr;
+ gap: 60px;
+ }
+
+ .hero-content {
+ text-align: center;
+ max-width: 100%;
+ order: 2;
+ }
+
+ .hero-demo {
+ order: 1;
+ min-height: 500px;
+ }
+
+ .trust-indicators {
+ justify-content: center;
+ }
+
+ .cta-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+}
+
+@media (max-width: 768px) {
+ .hero {
+ padding: 30px 20px;
+ }
+
+ .hero-headline {
+ font-size: 40px;
+ }
+
+ .hero-subheadline {
+ font-size: 18px;
+ margin-bottom: 32px;
+ }
+
+ .phone-frame {
+ width: 240px;
+ height: 500px;
+ }
+
+ .hero-demo {
+ min-height: 420px;
+ }
+
+ .phone-mockup {
+ animation: phoneFloatMobile 4s ease-in-out infinite;
+ }
+
+ @keyframes phoneFloatMobile {
+
+ 0%,
+ 100% {
+ transform: translateY(0) rotateY(-5deg) rotateX(3deg);
+ }
+
+ 50% {
+ transform: translateY(-10px) rotateY(-5deg) rotateX(3deg);
+ }
+ }
+
+ .workflow-bg {
+ opacity: 0.5;
+ }
+
+ .trust-indicators {
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .bg-glow-1,
+ .bg-glow-2 {
+ width: 300px;
+ height: 300px;
+ }
+}
+
+@media (max-width: 480px) {
+ .hero-headline {
+ font-size: 32px;
+ }
+
+ .cta-button {
+ padding: 16px 28px;
+ font-size: 16px;
+ }
+
+ .phone-frame {
+ width: 220px;
+ height: 460px;
+ }
+}
+
+/* ===== How It Works Section ===== */
+.how-it-works-section {
+ min-height: 100vh;
+ padding: 100px 60px;
+ background: var(--bg-primary);
+}
+
+.how-container {
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+.how-headline {
+ font-size: 56px;
+ font-weight: 700;
+ text-align: center;
+ margin-bottom: 16px;
+}
+
+.how-subheadline {
+ font-size: 20px;
+ text-align: center;
+ color: var(--text-secondary);
+ margin-bottom: 100px;
+}
+
+.how-step {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 80px;
+ align-items: center;
+ margin-bottom: 150px;
+ opacity: 0;
+ transform: translateY(50px);
+ transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+}
+
+.how-step.visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.how-step[data-step="2"] .step-visual {
+ order: -1;
+}
+
+.step-number {
+ font-size: 72px;
+ font-weight: 700;
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ opacity: 0.3;
+}
+
+.step-title {
+ font-size: 40px;
+ font-weight: 700;
+ margin: 16px 0;
+}
+
+.step-description {
+ font-size: 18px;
+ color: var(--text-secondary);
+ line-height: 1.6;
+}
+
+.fake-input-container,
+.workflow-builder,
+.live-demo-container {
+ background: rgba(10, 10, 15, 0.6);
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(0, 217, 255, 0.2);
+ border-radius: 20px;
+ padding: 24px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
+}
+
+.fake-input-body {
+ min-height: 120px;
+ padding: 20px;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 12px;
+ font-size: 18px;
+ margin: 20px 0;
+}
+
+.cursor {
+ color: var(--color-accent);
+ animation: blink 1s step-end infinite;
+}
+
+@keyframes blink {
+ 50% {
+ opacity: 0;
+ }
+}
+
+.fake-submit-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 14px 28px;
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
+ border: none;
+ border-radius: 10px;
+ color: white;
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.3s;
+}
+
+.fake-submit-btn:hover {
+ transform: translateY(-2px);
+}
+
+.builder-canvas {
+ position: relative;
+ height: 350px;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 12px;
+ margin-top: 20px;
+}
+
+.build-node {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ opacity: 0;
+ transform: scale(0);
+}
+
+.how-step.visible .build-node[data-node="1"] {
+ animation: nodeAppear 0.5s ease forwards 0.5s;
+}
+
+.how-step.visible .build-node[data-node="2"] {
+ animation: nodeAppear 0.5s ease forwards 0.8s;
+}
+
+.how-step.visible .build-node[data-node="3"] {
+ animation: nodeAppear 0.5s ease forwards 1.1s;
+}
+
+.how-step.visible .build-node[data-node="4"] {
+ animation: nodeAppear 0.5s ease forwards 1.4s;
+}
+
+.how-step.visible .build-node[data-node="5"] {
+ animation: nodeAppear 0.5s ease forwards 1.7s;
+}
+
+@keyframes nodeAppear {
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.build-node-icon {
+ width: 60px;
+ height: 60px;
+ background: rgba(0, 102, 255, 0.1);
+ border: 1px solid rgba(0, 217, 255, 0.3);
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-accent);
+}
+
+.build-node-icon svg {
+ width: 32px;
+ height: 32px;
+ stroke-width: 1.5;
+}
+
+.build-connections {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+}
+
+.build-line {
+ stroke-dasharray: 100;
+ stroke-dashoffset: 100;
+}
+
+.how-step.visible .build-line[data-line="1"] {
+ animation: lineDraw 0.6s ease forwards 1s;
+}
+
+.how-step.visible .build-line[data-line="2"] {
+ animation: lineDraw 0.6s ease forwards 1.3s;
+}
+
+.how-step.visible .build-line[data-line="3"] {
+ animation: lineDraw 0.6s ease forwards 1.6s;
+}
+
+.how-step.visible .build-line[data-line="4"] {
+ animation: lineDraw 0.6s ease forwards 1.9s;
+}
+
+@keyframes lineDraw {
+ to {
+ stroke-dashoffset: 0;
+ opacity: 1;
+ }
+}
+
+.live-phone {
+ background: #1C1C1E;
+ border-radius: 24px;
+ padding: 16px;
+ margin-top: 20px;
+}
+
+.live-phone-screen {
+ background: #0B141A;
+ border-radius: 16px;
+ overflow: hidden;
+}
+
+.live-wa-header {
+ background: var(--wa-green);
+ padding: 16px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.live-wa-avatar {
+ width: 40px;
+ height: 40px;
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+}
+
+.live-chat-area {
+ padding: 20px;
+ min-height: 300px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.live-message {
+ display: flex;
+ opacity: 0;
+}
+
+.how-step.visible .live-message[data-live-msg="1"] {
+ animation: msgSlideIn 0.4s ease forwards 0.5s;
+}
+
+.how-step.visible .live-message[data-live-msg="2"] {
+ animation: msgSlideIn 0.4s ease forwards 1s;
+}
+
+.how-step.visible .live-message[data-live-msg="3"] {
+ animation: msgSlideIn 0.4s ease forwards 1.8s;
+}
+
+.how-step.visible .live-message[data-live-msg="4"] {
+ animation: msgSlideIn 0.4s ease forwards 2.3s;
+}
+
+@keyframes msgSlideIn {
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.live-msg-in {
+ justify-content: flex-start;
+}
+
+.live-msg-out {
+ justify-content: flex-end;
+}
+
+.live-bubble {
+ max-width: 80%;
+ padding: 10px 14px;
+ border-radius: 12px;
+ font-size: 14px;
+}
+
+.live-msg-in .live-bubble {
+ background: var(--wa-received);
+}
+
+.live-msg-out .live-bubble {
+ background: var(--wa-sent);
+}
+
+.live-ai-tag {
+ display: inline-block;
+ padding: 2px 6px;
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
+ border-radius: 4px;
+ font-size: 9px;
+ font-weight: 700;
+ margin-right: 6px;
+}
+
+.live-counter {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+}
+
+.counter-number {
+ font-size: 32px;
+ font-weight: 700;
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.live-header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 20px;
+}
+
+.live-status {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.live-dot {
+ width: 10px;
+ height: 10px;
+ background: #25D366;
+ border-radius: 50%;
+ animation: pulse 2s ease-in-out infinite;
+}
+
+.progress-bar {
+ height: 4px;
+ background: linear-gradient(90deg, var(--color-primary), var(--color-accent));
+ width: 0%;
+ transition: width 3s ease-out;
+}
+
+.how-step.visible .progress-bar {
+ width: 100%;
+}
+
+@media (max-width: 1024px) {
+ .how-step {
+ grid-template-columns: 1fr;
+ gap: 60px;
+ }
+
+ .how-step[data-step="2"] .step-visual {
+ order: 0;
+ }
+}
+
+/* ===== Features Section ===== */
+.features-section {
+ min-height: 100vh;
+ padding: 100px 60px;
+ background: linear-gradient(180deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
+}
+
+.features-container {
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+.features-headline {
+ font-size: 56px;
+ font-weight: 700;
+ text-align: center;
+ margin-bottom: 60px;
+}
+
+.feature-tabs {
+ display: flex;
+ justify-content: center;
+ gap: 16px;
+ margin-bottom: 60px;
+ flex-wrap: wrap;
+}
+
+.feature-tab {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 16px 28px;
+ background: rgba(10, 10, 15, 0.4);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 12px;
+ color: var(--text-secondary);
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.feature-tab:hover {
+ background: rgba(0, 102, 255, 0.1);
+ border-color: rgba(0, 217, 255, 0.3);
+}
+
+.feature-tab.active {
+ background: linear-gradient(135deg, rgba(0, 102, 255, 0.2), rgba(0, 217, 255, 0.2));
+ border-color: var(--color-accent);
+ color: var(--text-primary);
+}
+
+.tab-icon {
+ font-size: 20px;
+}
+
+.feature-content-wrapper {
+ position: relative;
+ min-height: 500px;
+}
+
+.feature-content {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 80px;
+ align-items: center;
+ position: absolute;
+ width: 100%;
+ opacity: 0;
+ pointer-events: none;
+ transform: scale(0.95);
+ transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+}
+
+.feature-content.active {
+ opacity: 1;
+ pointer-events: all;
+ transform: scale(1);
+ position: relative;
+}
+
+.feature-title {
+ font-size: 40px;
+ font-weight: 700;
+ margin-bottom: 20px;
+}
+
+.feature-description {
+ font-size: 18px;
+ color: var(--text-secondary);
+ line-height: 1.6;
+ margin-bottom: 30px;
+}
+
+.feature-list {
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.feature-list li {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-size: 16px;
+ color: var(--text-secondary);
+}
+
+.feature-list svg {
+ width: 20px;
+ height: 20px;
+ color: var(--wa-light-green);
+ flex-shrink: 0;
+}
+
+.feature-demo {
+ background: rgba(10, 10, 15, 0.6);
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(0, 217, 255, 0.2);
+ border-radius: 20px;
+ padding: 32px;
+ min-height: 450px;
+}
+
+/* Intent Demo */
+.intent-demo {
+ position: relative;
+}
+
+.intent-header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 30px;
+}
+
+.intent-badge {
+ padding: 6px 12px;
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.intent-queries {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-bottom: 30px;
+}
+
+.intent-query {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ opacity: 0;
+ animation: querySlide 0.5s ease forwards;
+}
+
+.intent-query[data-query="1"] {
+ animation-delay: 0.2s;
+}
+
+.intent-query[data-query="2"] {
+ animation-delay: 0.4s;
+}
+
+.intent-query[data-query="3"] {
+ animation-delay: 0.6s;
+}
+
+@keyframes querySlide {
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.query-bubble {
+ padding: 12px 18px;
+ background: rgba(0, 102, 255, 0.1);
+ border: 1px solid rgba(0, 217, 255, 0.2);
+ border-radius: 12px;
+ font-size: 14px;
+}
+
+.intent-result {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 20px;
+ background: rgba(0, 217, 255, 0.1);
+ border: 1px solid var(--color-accent);
+ border-radius: 12px;
+ animation: resultPop 0.5s ease forwards 1s;
+ opacity: 0;
+}
+
+@keyframes resultPop {
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.result-icon {
+ font-size: 32px;
+}
+
+.result-label {
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.result-value {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--color-accent);
+}
+
+.intent-particles {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+}
+
+/* Workflow Demo */
+.workflow-canvas-demo {
+ position: relative;
+ height: 400px;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 12px;
+}
+
+.demo-node {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ animation: demoNodePop 0.5s ease forwards;
+ opacity: 0;
+}
+
+@keyframes demoNodePop {
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.demo-node-icon {
+ width: 60px;
+ height: 60px;
+ background: rgba(0, 102, 255, 0.15);
+ border: 1px solid rgba(0, 217, 255, 0.3);
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28px;
+}
+
+.demo-connections {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+}
+
+/* Use Cases Demo */
+.usecases-demo {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+}
+
+.usecase-card {
+ padding: 24px;
+ background: rgba(0, 102, 255, 0.05);
+ border: 1px solid rgba(0, 217, 255, 0.2);
+ border-radius: 16px;
+ text-align: center;
+ transition: all 0.3s ease;
+ cursor: pointer;
+}
+
+.usecase-card:hover {
+ transform: translateY(-4px);
+ background: rgba(0, 102, 255, 0.1);
+ border-color: var(--color-accent);
+}
+
+.usecase-icon {
+ font-size: 48px;
+ margin-bottom: 12px;
+}
+
+.usecase-title {
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+
+.usecase-stat {
+ font-size: 14px;
+ color: var(--text-muted);
+}
+
+/* Monitoring Demo */
+.monitor-stats {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.monitor-stat {
+ text-align: center;
+ padding: 20px;
+ background: rgba(0, 102, 255, 0.05);
+ border-radius: 12px;
+}
+
+.stat-value {
+ font-size: 36px;
+ font-weight: 700;
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ margin-bottom: 8px;
+}
+
+.monitor-chart {
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 12px;
+ padding: 20px;
+}
+
+@media (max-width: 1024px) {
+ .feature-content {
+ grid-template-columns: 1fr;
+ gap: 40px;
+ }
+
+ .usecases-demo {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* ===== Use Cases Section ===== */
+.usecases-section {
+ min-height: 100vh;
+ padding: 160px 60px;
+ background: var(--bg-primary);
+}
+
+.usecases-container {
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+.usecases-headline {
+ font-size: 56px;
+ font-weight: 700;
+ text-align: center;
+ margin-bottom: 16px;
+}
+
+.usecases-subheadline {
+ font-size: 20px;
+ text-align: center;
+ color: var(--text-secondary);
+ margin-bottom: 80px;
+}
+
+.usecases-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 32px;
+}
+
+/* 3D Flip Card */
+.usecase-card-3d {
+ perspective: 1000px;
+ height: 320px;
+}
+
+.card-inner {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ transition: transform 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ transform-style: preserve-3d;
+}
+
+.usecase-card-3d:hover .card-inner {
+ transform: rotateY(180deg);
+}
+
+.card-front,
+.card-back {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ backface-visibility: hidden;
+ border-radius: 20px;
+ padding: 32px;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Card Front */
+.card-front {
+ background: linear-gradient(135deg, rgba(0, 102, 255, 0.1), rgba(0, 217, 255, 0.05));
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(0, 217, 255, 0.2);
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+}
+
+.card-icon {
+ font-size: 64px;
+ margin-bottom: 20px;
+ transition: transform 0.3s ease;
+}
+
+.usecase-card-3d:hover .card-icon {
+ transform: scale(1.1) rotate(5deg);
+}
+
+.card-title {
+ font-size: 28px;
+ font-weight: 700;
+ margin-bottom: 12px;
+ color: var(--text-primary);
+}
+
+.card-subtitle {
+ font-size: 16px;
+ color: var(--text-secondary);
+ margin-bottom: 24px;
+}
+
+.card-hover-hint {
+ font-size: 14px;
+ color: var(--color-accent);
+ opacity: 0.7;
+ margin-top: auto;
+}
+
+/* Card Back */
+.card-back {
+ background: linear-gradient(135deg, rgba(0, 102, 255, 0.15), rgba(0, 217, 255, 0.1));
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid var(--color-accent);
+ box-shadow: 0 20px 60px rgba(0, 102, 255, 0.4);
+ transform: rotateY(180deg);
+}
+
+.card-back-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--color-accent);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ margin-bottom: 24px;
+}
+
+.card-stats {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 16px;
+}
+
+.stat-change {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ margin-bottom: 8px;
+}
+
+.stat-before {
+ font-size: 16px;
+ color: #FF6B6B;
+ text-decoration: line-through;
+ opacity: 0.7;
+}
+
+.stat-arrow {
+ font-size: 24px;
+ color: var(--color-accent);
+}
+
+.stat-after {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--wa-light-green);
+}
+
+.stat-metric {
+ text-align: center;
+ font-size: 14px;
+ color: var(--text-muted);
+}
+
+.card-highlight {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ padding: 20px;
+ background: rgba(0, 217, 255, 0.1);
+ border-radius: 12px;
+ margin-top: auto;
+}
+
+.highlight-number {
+ font-size: 36px;
+ font-weight: 700;
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.highlight-text {
+ font-size: 14px;
+ color: var(--text-secondary);
+}
+
+/* Hover Effects */
+.usecase-card-3d {
+ transition: transform 0.3s ease;
+}
+
+.usecase-card-3d:hover {
+ transform: translateY(-8px);
+}
+
+/* Responsive */
+@media (max-width: 1024px) {
+ .usecases-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (max-width: 768px) {
+ .usecases-section {
+ padding: 60px 20px;
+ }
+
+ .usecases-headline {
+ font-size: 40px;
+ }
+
+ .usecases-grid {
+ grid-template-columns: 1fr;
+ gap: 24px;
+ }
+
+ .usecase-card-3d {
+ height: 280px;
+ }
+}
+
+/* ===== Final CTA Section ===== */
+.final-cta-section {
+ position: relative;
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ padding: 100px 60px;
+}
+
+/* Animated Gradient Background */
+.cta-gradient-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #000;
+ z-index: 0;
+}
+
+@keyframes gradientFlow {
+
+ 0%,
+ 100% {
+ background-position: 0% 50%;
+ }
+
+ 50% {
+ background-position: 100% 50%;
+ }
+}
+
+/* Floating Particles */
+.cta-particles {
+ display: none;
+ /* User requested removal of bubbles */
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+}
+
+/* Geometric Shapes */
+.cta-shapes {
+ display: none;
+ /* Hide shapes for cleaner look */
+}
+
+.cta-shape {
+ display: none;
+}
+
+
+.shape-1 {
+ width: 400px;
+ height: 400px;
+ background: radial-gradient(circle, rgba(16, 185, 129, 0.4), transparent);
+ top: 10%;
+ left: 10%;
+ animation-delay: 0s;
+}
+
+.shape-2 {
+ width: 300px;
+ height: 300px;
+ background: radial-gradient(circle, rgba(52, 211, 153, 0.3), transparent);
+ top: 60%;
+ right: 15%;
+ animation-delay: 5s;
+}
+
+.shape-3 {
+ width: 350px;
+ height: 350px;
+ background: radial-gradient(circle, rgba(110, 231, 183, 0.3), transparent);
+ bottom: 20%;
+ left: 20%;
+ animation-delay: 10s;
+}
+
+.shape-4 {
+ width: 250px;
+ height: 250px;
+ background: radial-gradient(circle, rgba(16, 185, 129, 0.4), transparent);
+ top: 30%;
+ right: 30%;
+ animation-delay: 15s;
+}
+
+@keyframes shapeFloat {
+
+ 0%,
+ 100% {
+ transform: translate(0, 0) scale(1);
+ }
+
+ 25% {
+ transform: translate(30px, -30px) scale(1.1);
+ }
+
+ 50% {
+ transform: translate(-20px, 20px) scale(0.9);
+ }
+
+ 75% {
+ transform: translate(20px, 30px) scale(1.05);
+ }
+}
+
+/* Content */
+.cta-content {
+ position: relative;
+ z-index: 2;
+ text-align: center;
+ max-width: 800px;
+}
+
+.cta-headline {
+ font-size: 64px;
+ font-weight: 700;
+ line-height: 1.1;
+ margin-bottom: 24px;
+ background: linear-gradient(135deg, var(--text-primary) 0%, var(--color-accent) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.cta-subheadline {
+ font-size: 22px;
+ color: var(--text-secondary);
+ margin-bottom: 48px;
+}
+
+/* Mega Button */
+.cta-mega-button {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ gap: 16px;
+ padding: 24px 56px;
+ font-size: 20px;
+ font-weight: 700;
+ color: white;
+ background: linear-gradient(135deg, #10B981 0%, #34D399 100%);
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-radius: 16px;
+ cursor: pointer;
+ overflow: hidden;
+ transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ box-shadow:
+ 0 20px 60px rgba(16, 185, 129, 0.5),
+ 0 0 0 0 rgba(52, 211, 153, 0.4);
+ animation: buttonPulse 3s ease-in-out infinite;
+}
+
+@keyframes buttonPulse {
+
+ 0%,
+ 100% {
+ box-shadow: 0 10px 40px rgba(255, 255, 255, 0.1);
+ }
+
+ 50% {
+ box-shadow: 0 10px 40px rgba(255, 255, 255, 0.2);
+ }
+}
+
+.cta-mega-button:hover {
+ transform: translateY(-4px) scale(1.02);
+ box-shadow: 0 20px 50px rgba(255, 255, 255, 0.2);
+}
+
+.cta-mega-button:active {
+ transform: translateY(-2px) scale(1.02);
+ transition: all 0.1s ease;
+}
+
+.cta-btn-glow {
+ display: none;
+}
+
+.cta-mega-button:hover .cta-btn-glow {
+ opacity: 1;
+ animation: glowRotate 2s linear infinite;
+}
+
+@keyframes glowRotate {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.cta-btn-text {
+ position: relative;
+ z-index: 1;
+}
+
+.cta-btn-arrow {
+ width: 24px;
+ height: 24px;
+ transition: transform 0.3s ease;
+ color: #000;
+}
+
+.cta-mega-button:hover .cta-btn-arrow {
+ transform: translateX(6px);
+}
+
+/* Trust Badges */
+.cta-trust {
+ display: flex;
+ justify-content: center;
+ gap: 32px;
+ margin-top: 40px;
+ flex-wrap: wrap;
+}
+
+.trust-badge {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ color: var(--text-secondary);
+}
+
+.trust-badge svg {
+ width: 20px;
+ height: 20px;
+ color: #fff;
+ /* White checkmarks */
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .final-cta-section {
+ padding: 60px 20px;
+ }
+
+ .cta-headline {
+ font-size: 40px;
+ }
+
+ .cta-subheadline {
+ font-size: 18px;
+ }
+
+ .cta-mega-button {
+ padding: 20px 40px;
+ font-size: 18px;
+ }
+
+ .cta-trust {
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .cta-shape {
+ filter: blur(40px);
+ }
+}
+
+/* Section Accent Borders */
+section {
+ position: relative;
+}
+
+section::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 80%;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(52, 211, 153, 0.3), transparent);
+}
+
+/* Hero Section - Top Border */
+.hero::before {
+ display: none;
+ /* No border on first section */
+}
+
+/* Accent Corner Decorations */
+.hero::after {
+ content: '';
+ position: absolute;
+ top: 40px;
+ right: 40px;
+ width: 100px;
+ height: 100px;
+ border-top: 2px solid rgba(52, 211, 153, 0.2);
+ border-right: 2px solid rgba(52, 211, 153, 0.2);
+ border-radius: 0 8px 0 0;
+}
+
+.comparison-section::after {
+ content: '';
+ position: absolute;
+ bottom: 40px;
+ left: 40px;
+ width: 100px;
+ height: 100px;
+ border-bottom: 2px solid rgba(52, 211, 153, 0.2);
+ border-left: 2px solid rgba(52, 211, 153, 0.2);
+ border-radius: 0 0 0 8px;
+}
+
+.features-section::after {
+ content: '';
+ position: absolute;
+ top: 40px;
+ left: 40px;
+ width: 80px;
+ height: 80px;
+ border-top: 2px solid rgba(52, 211, 153, 0.15);
+ border-left: 2px solid rgba(52, 211, 153, 0.15);
+ border-radius: 8px 0 0 0;
+}
+
+/* Vertical Accent Lines */
+.how-it-works-section::before {
+ content: '';
+ position: absolute;
+ left: 60px;
+ top: 100px;
+ width: 2px;
+ height: 200px;
+ background: linear-gradient(180deg, transparent, rgba(52, 211, 153, 0.4), transparent);
+}
+
+.usecases-section::before {
+ content: '';
+ position: absolute;
+ right: 60px;
+ top: 100px;
+ width: 2px;
+ height: 200px;
+ background: linear-gradient(180deg, transparent, rgba(52, 211, 153, 0.4), transparent);
+}
+
+/* OVERRIDE - Keep dot grid, hide only section decorations */
+section::before,
+section::after,
+.hero::after,
+.comparison-section::after,
+.features-section::after,
+.how-it-works-section::before,
+.usecases-section::before {
+ display: none !important;
+}
+
+/* ===== RESPONSIVE LAYOUT FIXES FOR LARGE SCREENS ===== */
+
+/* Global max-width container to prevent content from stretching too wide */
+.hero-container,
+.comparison-container,
+.how-container,
+.features-container,
+.usecases-container,
+.cta-content {
+ max-width: 1400px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* Large screen optimizations (1920px+) */
+@media (min-width: 1920px) {
+
+ /* Hero section */
+ .hero-container {
+ max-width: 1600px;
+ }
+
+ .hero-content {
+ max-width: 600px;
+ }
+
+ .hero-demo {
+ max-width: 700px;
+ }
+
+ /* Comparison section */
+ .comparison-container {
+ max-width: 1600px;
+ }
+
+ .split-comparison {
+ max-width: 1100px;
+ /* Keep boxes focused even on large screens */
+ margin-bottom: 240px;
+ }
+
+ /* How It Works */
+ .how-container {
+ max-width: 1600px;
+ }
+
+ .how-step {
+ gap: 100px;
+ }
+
+ /* Features */
+ .features-container {
+ max-width: 1600px;
+ }
+
+ .feature-content {
+ gap: 100px;
+ }
+
+ /* Use Cases */
+ .usecases-container {
+ max-width: 1800px;
+ }
+
+ .usecases-grid {
+ grid-template-columns: repeat(3, minmax(300px, 400px));
+ justify-content: center;
+ gap: 40px;
+ }
+
+ /* CTA */
+ .cta-content {
+ max-width: 1000px;
+ }
+}
+
+/* Ultra-wide screens (2560px+) */
+@media (min-width: 2560px) {
+
+ /* Prevent excessive stretching */
+ .hero-container,
+ .comparison-container,
+ .how-container,
+ .features-container {
+ max-width: 2000px;
+ }
+
+ .usecases-container {
+ max-width: 2200px;
+ }
+
+ /* Increase font sizes slightly for readability */
+ .hero-headline {
+ font-size: 76px;
+ }
+
+ .hero-subheadline {
+ font-size: 26px;
+ }
+
+ .how-headline,
+ .features-headline,
+ .usecases-headline {
+ font-size: 64px;
+ }
+
+}
+
+/* ===== Feature Tab Icons ===== */
+.tab-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-muted);
+ transition: all 0.3s ease;
+}
+
+.tab-icon svg {
+ width: 24px;
+ height: 24px;
+}
+
+.feature-tab:hover .tab-icon,
+.feature-tab.active .tab-icon {
+ color: var(--color-accent);
+
+}
+
+
+/* ===== Demo Icon Styles ===== */
+.result-icon svg,
+.demo-node-icon svg {
+ width: 24px;
+ height: 24px;
+ color: var(--color-accent);
+}
+
+.live-bubble svg.check-icon,
+.live-bubble svg.star-icon {
+ width: 16px;
+ height: 16px;
+ margin-left: 6px;
+ vertical-align: middle;
+ display: inline-block;
+}
+
+.live-bubble svg.check-icon {
+ color: #4ade80;
+ /* Green */
+}
+
+.live-bubble svg.star-icon {
+ color: #fbbf24;
+ /* Yellow/Gold */
+}
+
+/* ===== Footer Styles ===== */
+.main-footer {
+ background: #000;
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+ padding: 80px 60px 40px;
+ margin-top: 0;
+ position: relative;
+ z-index: 10;
+}
+
+.footer-container {
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+.footer-top {
+ display: grid;
+ grid-template-columns: 350px 1fr;
+ gap: 60px;
+ margin-bottom: 60px;
+}
+
+.footer-brand .footer-logo {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 20px;
+}
+
+.footer-brand .logo-icon {
+ font-size: 24px;
+ color: #fff;
+}
+
+.footer-brand .logo-text {
+ font-size: 24px;
+ font-weight: 700;
+ color: #fff;
+ letter-spacing: -0.5px;
+}
+
+.footer-tagline {
+ font-size: 16px;
+ color: var(--text-muted);
+ line-height: 1.6;
+ max-width: 300px;
+}
+
+.footer-links {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 40px;
+}
+
+.footer-column h4 {
+ color: #fff;
+ font-size: 16px;
+ font-weight: 600;
+ margin-bottom: 24px;
+}
+
+.footer-column a {
+ display: block;
+ color: var(--text-muted);
+ text-decoration: none;
+ margin-bottom: 12px;
+ transition: color 0.3s ease;
+ font-size: 15px;
+}
+
+.footer-column a:hover {
+ color: #fff;
+}
+
+.footer-bottom {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 40px;
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.footer-copyright {
+ color: var(--text-muted);
+ font-size: 14px;
+}
+
+.footer-social {
+ display: flex;
+ gap: 20px;
+}
+
+.footer-social a {
+ color: var(--text-muted);
+ transition: color 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.footer-social a:hover {
+ color: #fff;
+}
+
+.footer-social svg {
+ width: 20px;
+ height: 20px;
+}
+
+/* Responsive Footer */
+@media (max-width: 1024px) {
+ .main-footer {
+ padding: 60px 40px 30px;
+ }
+
+ .footer-top {
+ grid-template-columns: 1fr;
+ gap: 40px;
+ }
+
+ .footer-links {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+@media (max-width: 768px) {
+ .main-footer {
+ padding: 40px 24px 24px;
+ }
+
+ .footer-links {
+ grid-template-columns: 1fr;
+ /* Stack columns on mobile */
+ gap: 30px;
+ }
+
+ .footer-bottom {
+ flex-direction: column-reverse;
+ gap: 20px;
+ text-align: center;
+ }
+}
+
+/* ===== Navigation ===== */
+/* ===== Navigation ===== */
+.navbar {
+ position: fixed;
+ top: 24px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 90%;
+ max-width: 1200px;
+ z-index: 1000;
+ padding: 12px 24px;
+ transition: all 0.3s ease;
+
+ /* Glassmorphism */
+ background: rgba(15, 15, 20, 0.6);
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 100px;
+ /* Pill shape */
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
+}
+
+.nav-container {
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.nav-brand {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ text-decoration: none;
+}
+
+.logo-img {
+ height: 28px;
+ /* Slightly smaller for the pill */
+ width: auto;
+ filter: invert(1);
+ mix-blend-mode: screen;
+}
+
+.nav-brand .logo-text {
+ font-size: 20px;
+ font-weight: 700;
+ color: #fff;
+ letter-spacing: -0.5px;
+}
+
+.nav-links {
+ display: flex;
+ align-items: center;
+ gap: 32px;
+}
+
+.nav-link {
+ color: var(--text-muted);
+ text-decoration: none;
+ font-size: 15px;
+ font-weight: 500;
+ transition: color 0.3s ease;
+}
+
+.nav-link:hover {
+ color: #fff;
+}
+
+.nav-btn {
+ background: #fff;
+ border: none;
+ color: #000;
+ padding: 10px 24px;
+ border-radius: 100px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 12px rgba(255, 255, 255, 0.1);
+}
+
+.nav-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(255, 255, 255, 0.2);
+ color: #000;
+}
+
+/* Responsive Nav */
+@media (max-width: 768px) {
+ .nav-container {
+ padding: 0 24px;
+ }
+
+ .nav-links {
+ display: none;
+ /* Hide links on mobile for now */
+ }
+}
+
+/* Add to src/styles/landing.css */
+
+@keyframes float {
+
+ 0%,
+ 100% {
+ transform: translateY(0px) rotateY(-12deg) rotateX(5deg);
+ }
+
+ 50% {
+ transform: translateY(-15px) rotateY(-12deg) rotateX(5deg);
+ }
+}
+
+.animate-float {
+ animation: float 6s ease-in-out infinite;
+}
+
+/* Ensure the gradient text shines */
+.gradient-text {
+ background: linear-gradient(135deg, #34D399 0%, #3B82F6 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
\ No newline at end of file
diff --git a/autoflow-frontend/src/vite-env.d.ts b/autoflow-frontend/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/autoflow-frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/autoflow-frontend/tailwind.config.js b/autoflow-frontend/tailwind.config.js
index 991a6db..d78303c 100644
--- a/autoflow-frontend/tailwind.config.js
+++ b/autoflow-frontend/tailwind.config.js
@@ -1,29 +1,19 @@
/** @type {import('tailwindcss').Config} */
export default {
- content: [
- "./index.html",
- "./src/**/*.{js,ts,jsx,tsx}",
- ],
- theme: {
- extend: {
- fontFamily: {
- sans: ['Inter', 'sans-serif'],
- },
- colors: {
- primary: {
- 50: '#eff6ff',
- 100: '#dbeafe',
- 200: '#bfdbfe',
- 300: '#93c5fd',
- 400: '#60a5fa',
- 500: '#3b82f6', // Brand color
- 600: '#2563eb',
- 700: '#1d4ed8',
- 800: '#1e40af',
- 900: '#1e3a8a',
- },
- },
- },
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ }
},
- plugins: [],
+ },
+ plugins: [],
}
diff --git a/autoflow-frontend/tsconfig.app.json b/autoflow-frontend/tsconfig.app.json
new file mode 100644
index 0000000..f867de0
--- /dev/null
+++ b/autoflow-frontend/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/autoflow-frontend/tsconfig.json b/autoflow-frontend/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/autoflow-frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/autoflow-frontend/tsconfig.node.json b/autoflow-frontend/tsconfig.node.json
new file mode 100644
index 0000000..abcd7f0
--- /dev/null
+++ b/autoflow-frontend/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/autoflow-frontend/vite.config.js b/autoflow-frontend/vite.config.ts
similarity index 81%
rename from autoflow-frontend/vite.config.js
rename to autoflow-frontend/vite.config.ts
index 5a33944..8b0f57b 100644
--- a/autoflow-frontend/vite.config.js
+++ b/autoflow-frontend/vite.config.ts
@@ -1,7 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
-// https://vitejs.dev/config/
+// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..f8fda52
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,6 @@
+FROM python:3.11-slim
+WORKDIR /app
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+COPY . .
+CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/backend/database.py b/backend/database.py
new file mode 100644
index 0000000..3ea45cd
--- /dev/null
+++ b/backend/database.py
@@ -0,0 +1,15 @@
+import os
+from sqlmodel import SQLModel, create_engine, Session
+
+DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./autoflow.db")
+
+# For SQLite, check_same_thread=False is needed in FastAPI
+connect_args = {"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
+engine = create_engine(DATABASE_URL, echo=True, connect_args=connect_args)
+
+def create_db_and_tables():
+ SQLModel.metadata.create_all(engine)
+
+def get_session():
+ with Session(engine) as session:
+ yield session
diff --git a/backend/executor.py b/backend/executor.py
new file mode 100644
index 0000000..6b5de94
--- /dev/null
+++ b/backend/executor.py
@@ -0,0 +1,238 @@
+import os
+import httpx
+import asyncio
+from typing import Dict, Any, List
+from collections import defaultdict
+import re
+from uuid import uuid4
+from datetime import datetime
+
+from sqlmodel import Session
+from models import AuditLog
+
+WHATSAPP_BRIDGE_URL = os.getenv("WHATSAPP_BRIDGE_URL", "http://whatsapp-bridge:3001")
+
+class WorkflowExecutor:
+ """
+ Executes a workflow graph node by node.
+ Starting from the trigger node, traverses edges,
+ evaluates conditions, and fires actions.
+ """
+
+ def __init__(self, db_session: Session):
+ self.db = db_session
+
+ async def execute(self, workflow_id: str, workflow_name: str, workflow_json: dict, trigger_type: str, trigger_data: dict):
+ nodes = {n["id"]: n for n in workflow_json["nodes"]}
+ edges = workflow_json["edges"]
+
+ # Find trigger node (always starts execution)
+ try:
+ trigger_node = next(n for n in workflow_json["nodes"] if n["type"] == "trigger")
+ except StopIteration:
+ await self._log_audit(workflow_id, workflow_name, trigger_type, trigger_data, [], "failed", "No trigger node found")
+ return
+
+ # Build adjacency map
+ next_nodes = defaultdict(list)
+ for edge in edges:
+ next_nodes[edge["source"]].append({
+ "target": edge["target"],
+ "label": edge.get("label") # "true"/"false" for conditions
+ })
+
+ context = {"trigger": trigger_data, "results": {}}
+ actions_taken = []
+
+ try:
+ await self._execute_node(trigger_node, nodes, next_nodes, context, actions_taken)
+ await self._log_audit(workflow_id, workflow_name, trigger_type, trigger_data, actions_taken, "success", None)
+ except Exception as e:
+ await self._log_audit(workflow_id, workflow_name, trigger_type, trigger_data, actions_taken, "failed", str(e))
+
+
+ async def _execute_node(self, node: dict, nodes: dict, next_nodes: dict, context: dict, actions_taken: List[dict]):
+ node_type = node["type"]
+ node_data = node.get("data", {})
+
+ actions_taken.append({
+ "node_id": node["id"],
+ "type": node_type,
+ "label": node_data.get("label", "Unknown"),
+ "executed_at": datetime.utcnow().isoformat()
+ })
+
+ if node_type == "action":
+ result = await self._run_action(node_data, context)
+ context["results"][node["id"]] = result
+
+ elif node_type == "condition":
+ result = await self._evaluate_condition(node_data, context)
+ # Only follow the matching branch (true/false edge)
+ for edge in next_nodes[node["id"]]:
+ if edge["label"] and edge["label"].lower() == str(result).lower():
+ await self._execute_node(nodes[edge["target"]], nodes, next_nodes, context, actions_taken)
+ return
+
+ elif node_type == "whatsapp":
+ await self._send_whatsapp(node_data.get("config", {}), context)
+
+ elif node_type == "delay":
+ config = node_data.get("config", {})
+ wait_time = int(config.get("wait_seconds", 0))
+ await asyncio.sleep(wait_time)
+
+ # Continue to next nodes
+ for edge in next_nodes[node["id"]]:
+ if not edge.get("label"): # unconditional edge
+ await self._execute_node(nodes[edge["target"]], nodes, next_nodes, context, actions_taken)
+
+ async def _run_action(self, node_data: dict, context: dict) -> Any:
+ import os, csv
+ DATA_DIR = "/app/data" if os.environ.get("DATABASE_URL") else "./data"
+
+ action_type = node_data.get("action_type")
+ config = node_data.get("config", {})
+
+ if action_type == "inventory_lookup":
+ # Extract keywords from the original trigger message to search CSV
+ search_query = str(context.get("trigger", {}).get("message", "")).lower()
+
+ try:
+ with open(os.path.join(DATA_DIR, "inventory.csv"), "r", encoding="utf-8") as f:
+ reader = csv.DictReader(f)
+ for row in reader:
+ product = row["product"].lower().strip()
+ if product in search_query:
+ return {
+ "found": True,
+ "product": row["product"],
+ "quantity": int(row["quantity"]),
+ "price": float(row["price"])
+ }
+ except Exception as e:
+ print(f"Inventory lookup failed: {e}")
+ return {"found": False, "quantity": 0, "product": search_query}
+
+ elif action_type == "load_udhaar_list":
+ # Overdue > 7 logic
+ try:
+ with open(os.path.join(DATA_DIR, "udhaar.csv"), "r", encoding="utf-8") as f:
+ reader = csv.DictReader(f)
+ for row in reader:
+ if int(row["days_overdue"]) > 7:
+ # Send reminder
+ message = f"Hi {row['name']}, friendly reminder: โน{row['amount']} is due since {row['date']}. Please pay when convenient. - AutoFlow"
+ await self._send_whatsapp({"to": row["phone"], "message": message}, context)
+ return {"status": "reminders_sent"}
+ except Exception as e:
+ print(f"Udhaar check failed: {e}")
+ return {"status": "failed"}
+
+ elif action_type == "load_customers": # For broadcast
+ try:
+ with open(os.path.join(DATA_DIR, "customers.csv"), "r", encoding="utf-8") as f:
+ reader = csv.DictReader(f)
+ numbers = [row["phone"] for row in reader]
+ message = str(context.get("trigger", {}).get("message", "Promo alert!"))
+ # The Node.js bridge expects an array for broadcast
+ async with httpx.AsyncClient() as client:
+ resp = await client.post(
+ f"{WHATSAPP_BRIDGE_URL}/broadcast",
+ json={"numbers": numbers, "message": message},
+ timeout=60.0
+ )
+ return {"status": "broadcast_sent"}
+ except Exception as e:
+ return {"status": "failed"}
+
+ elif action_type == "log_payment":
+ return {"status": "logged"}
+
+ elif action_type == "send_whatsapp" or action_type == "send_broadcast":
+ await self._send_whatsapp(config, context)
+ return {"status": "sent"}
+
+ return None
+
+ async def _evaluate_condition(self, node_data: dict, context: dict) -> bool:
+ condition_type = node_data.get("condition_type")
+ config = node_data.get("config", {})
+
+ field = config.get("field")
+ operator = config.get("operator")
+ value = config.get("value")
+
+ actual_value = self._resolve_template(f"{{{{{field}}}}}", context)
+
+ # Super basic string evaluation logic for now
+ if operator == "greater_than":
+ try:
+ return float(actual_value) > float(value)
+ except:
+ return False
+ elif operator == "equals":
+ return str(actual_value).lower() == str(value).lower()
+ elif operator == "contains":
+ return str(value).lower() in str(actual_value).lower()
+
+ return False
+
+ async def _send_whatsapp(self, config: dict, context: dict):
+ message_template = config.get("message", "")
+ to_template = config.get("to", "")
+
+ message = self._resolve_template(message_template, context)
+ to_number = self._resolve_template(to_template, context)
+
+ if not to_number:
+ raise Exception("Missing 'to' number for WhatsApp message")
+
+ # Call the nodejs bridge
+ async with httpx.AsyncClient() as client:
+ response = await client.post(
+ f"{WHATSAPP_BRIDGE_URL}/send",
+ json={
+ "to": to_number,
+ "message": message
+ },
+ timeout=10.0
+ )
+ response.raise_for_status()
+
+ def _resolve_template(self, text: str, context: dict) -> str:
+ """
+ Replaces {{trigger.from}} or {{inventory.quantity}} or {{results.node_X.status}} with actual values.
+ """
+ if not text or not isinstance(text, str):
+ return text
+
+ def replacer(match):
+ path = match.group(1).split('.')
+ value = context
+ try:
+ for key in path:
+ # Very simple lookup logic
+ if isinstance(value, dict):
+ value = value.get(key, "")
+ else:
+ return ""
+ return str(value)
+ except Exception:
+ return ""
+
+ return re.sub(r'\{\{([\w\.]+)\}\}', replacer, text)
+
+ async def _log_audit(self, workflow_id: str, workflow_name: str, trigger_type: str, trigger_data: dict, actions_taken: List[dict], result: str, error_message: str = None):
+ log = AuditLog(
+ id=str(uuid4()),
+ workflow_id=workflow_id,
+ workflow_name=workflow_name,
+ trigger_type=trigger_type,
+ trigger_data=trigger_data,
+ actions_taken=actions_taken,
+ result=result,
+ error_message=error_message
+ )
+ self.db.add(log)
+ self.db.commit()
diff --git a/backend/main.py b/backend/main.py
new file mode 100644
index 0000000..e43bda5
--- /dev/null
+++ b/backend/main.py
@@ -0,0 +1,138 @@
+from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks
+from fastapi.middleware.cors import CORSMiddleware
+from contextlib import asynccontextmanager
+from sqlmodel import Session, select
+import asyncio
+
+from database import create_db_and_tables, get_session, engine
+import models
+from schemas import WorkflowGenerateRequest, WorkflowSchema
+from ollama_client import generate_workflow, explain_workflow
+from executor import WorkflowExecutor
+from seeder import seed_database
+
+# Routers
+from routers.inventory import router as inventory_router
+from routers.templates import router as templates_router
+from routers.audit import router as audit_router
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ create_db_and_tables()
+ # Seed DB with initial data
+ with Session(engine) as session:
+ seed_database(session)
+ yield
+
+app = FastAPI(lifespan=lifespan, title="AutoFlow OSS Backend")
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=False,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Register routers
+app.include_router(inventory_router)
+app.include_router(templates_router)
+app.include_router(audit_router)
+
+@app.get("/docs")
+def health():
+ return {"status": "ok"}
+
+@app.post("/api/workflow/generate")
+async def api_generate_workflow(request: WorkflowGenerateRequest):
+ try:
+ # B1. Generate JSON via Ollama
+ workflow_json = await generate_workflow(request.nl_input)
+
+ # B2. Validate JSON against Pydantic constraint immediately
+ # (This will throw ValidationError if Ollama hallucinations don't match the schema)
+ # Using WorkflowSchema as a loose wrapper for the node list
+ validated = WorkflowSchema(name="Generated Workflow", nodes=workflow_json.get("nodes", []), edges=workflow_json.get("edges", []))
+
+ # B2. Generate Plain English Explanation
+ explanation = await explain_workflow(validated.dict())
+
+ return {
+ "success": True,
+ "workflow": validated.dict(),
+ "explanation": explanation
+ }
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/workflow/explain")
+async def api_explain_workflow(workflow: dict):
+ try:
+ explanation = await explain_workflow(workflow)
+ return {"success": True, "explanation": explanation}
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/workflow/simulate")
+async def api_simulate_workflow(payload: dict):
+ try:
+ message = payload.get("message", "")
+ # Mocking the AI detection for simulation
+ return {
+ "success": True,
+ "intent": "Simulated Interaction",
+ "reply": f"Hello! The engine received your test message: '{message}'.\nIn a live environment, this would execute the mapped workflow nodes."
+ }
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/whatsapp/incoming")
+async def api_whatsapp_incoming(payload: dict, background_tasks: BackgroundTasks, db: Session = Depends(get_session)):
+ """
+ B5. Webhook listener from Node.js Whatsapp-Bridge
+ """
+ message_text = payload.get("message", "").lower()
+ from_number = payload.get("from")
+
+ if not message_text or not from_number:
+ return {"status": "ignored"}
+
+ # Query all active workflows
+ active_workflows = db.exec(select(models.Workflow).where(models.Workflow.is_active == True)).all()
+
+ matches = []
+
+ for workflow in active_workflows:
+ # Load the JSON
+ workflow_json = {"nodes": workflow.nodes, "edges": workflow.edges}
+ trigger_node = next((n for n in workflow_json["nodes"] if n["type"] == "trigger"), None)
+
+ if not trigger_node:
+ continue
+
+ config = trigger_node.get("data", {}).get("config", {})
+ keywords = config.get("keywords", [])
+
+ # Basic match check
+ if any(keyword.lower() in message_text for keyword in keywords):
+ executor = WorkflowExecutor(db_session=db)
+
+ trigger_data = {
+ "from": from_number,
+ "message": message_text,
+ "raw": payload
+ }
+
+ # Schedule execution in background
+ background_tasks.add_task(
+ executor.execute,
+ workflow_id=workflow.id,
+ workflow_name=workflow.name,
+ workflow_json=workflow_json,
+ trigger_type="whatsapp_message",
+ trigger_data=trigger_data
+ )
+ matches.append(workflow.name)
+
+ return {"status": "processed", "matches": len(matches), "matched_workflows": matches}
+
diff --git a/backend/models.py b/backend/models.py
new file mode 100644
index 0000000..20157cd
--- /dev/null
+++ b/backend/models.py
@@ -0,0 +1,49 @@
+from sqlmodel import SQLModel, Field
+from sqlalchemy import Column, JSON
+from typing import Optional, Dict, List, Any
+from datetime import datetime
+
+class Workflow(SQLModel, table=True):
+ __tablename__ = "workflows"
+ id: str = Field(primary_key=True)
+ name: str
+ description: Optional[str] = None
+ nl_input: Optional[str] = None
+ nodes: List[Dict[str, Any]] = Field(default_factory=list, sa_column=Column(JSON))
+ edges: List[Dict[str, Any]] = Field(default_factory=list, sa_column=Column(JSON))
+ is_active: bool = Field(default=False)
+ created_at: datetime = Field(default_factory=datetime.utcnow)
+ updated_at: datetime = Field(default_factory=datetime.utcnow)
+
+class Template(SQLModel, table=True):
+ __tablename__ = "templates"
+ id: str = Field(primary_key=True)
+ name: str
+ description: Optional[str] = None
+ category: Optional[str] = None # 'orders', 'payments', 'inventory', 'broadcast'
+ nodes: List[Dict[str, Any]] = Field(default_factory=list, sa_column=Column(JSON))
+ edges: List[Dict[str, Any]] = Field(default_factory=list, sa_column=Column(JSON))
+ use_count: int = Field(default=0)
+ created_at: datetime = Field(default_factory=datetime.utcnow)
+
+class AuditLog(SQLModel, table=True):
+ __tablename__ = "audit_logs"
+ id: str = Field(primary_key=True)
+ workflow_id: Optional[str] = Field(default=None, foreign_key="workflows.id")
+ workflow_name: Optional[str] = None
+ trigger_type: Optional[str] = None # 'whatsapp', 'schedule', 'manual'
+ trigger_data: Optional[Dict[str, Any]] = Field(default=None, sa_column=Column(JSON))
+ actions_taken: Optional[List[Dict[str, Any]]] = Field(default=None, sa_column=Column(JSON))
+ result: Optional[str] = None # 'success', 'failed', 'skipped'
+ error_message: Optional[str] = None
+ executed_at: datetime = Field(default_factory=datetime.utcnow)
+
+class InventorySource(SQLModel, table=True):
+ __tablename__ = "inventory_sources"
+ id: str = Field(primary_key=True)
+ name: str
+ type: Optional[str] = None # 'csv', 'google_sheets'
+ file_path: Optional[str] = None
+ sheet_url: Optional[str] = None
+ last_synced: Optional[datetime] = None
+ created_at: datetime = Field(default_factory=datetime.utcnow)
diff --git a/backend/ollama_client.py b/backend/ollama_client.py
new file mode 100644
index 0000000..c2df0be
--- /dev/null
+++ b/backend/ollama_client.py
@@ -0,0 +1,127 @@
+import os
+import json
+import httpx
+import asyncio
+from typing import Dict, Any
+
+OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434")
+OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen3:1.7b")
+
+SYSTEM_PROMPT = """/no_think
+You are AutoFlow, an AI that converts business automation requests into structured workflow JSON.
+
+You have access to these node types:
+- trigger: whatsapp_message, schedule, manual
+- action: inventory_lookup, send_whatsapp, send_broadcast, log_payment, check_udhaar
+- condition: compare, contains_text, time_check
+- whatsapp: send_whatsapp (outbound message with template variables)
+- delay: wait_seconds, wait_until
+
+Rules:
+1. Always start with exactly ONE trigger node
+2. Always end with at least one action node
+3. Use {{variable}} syntax for dynamic values
+4. Position nodes left-to-right with x incrementing by 200, y centered at 200
+5. Return ONLY valid JSON matching the workflow schema. No explanation, no markdown.
+6. Do NOT wrap in code blocks. Start with { and end with }.
+"""
+
+USER_PROMPT = """
+Business automation request: "{nl_input}"
+
+Generate a complete workflow JSON with nodes and edges.
+Use node positions that make visual sense left-to-right.
+"""
+
+STRICT_PROMPT = """
+You failed to generate valid JSON last time.
+This time, you MUST output ONLY a valid, parseable JSON object.
+Do NOT use markdown code blocks (```json).
+Start your response with { and end with }.
+Do not include any other conversational text.
+
+Business automation request: "{nl_input}"
+"""
+
+EXPLAINER_PROMPT = """
+Given this workflow JSON, explain in simple plain English what this automation does.
+Write it as if explaining to a non-technical shop owner in India.
+Maximum 4 sentences. Start with "This automation..."
+"""
+
+async def generate_workflow(nl_input: str, is_retry: bool = False) -> Dict[str, Any]:
+ prompt = STRICT_PROMPT.format(nl_input=nl_input) if is_retry else USER_PROMPT.format(nl_input=nl_input)
+
+ for attempt in range(3):
+ try:
+ async with httpx.AsyncClient() as client:
+ response = await client.post(
+ f"{OLLAMA_URL}/api/chat",
+ json={
+ "model": OLLAMA_MODEL,
+ "messages": [
+ {"role": "system", "content": SYSTEM_PROMPT},
+ {"role": "user", "content": prompt}
+ ],
+ "stream": False,
+ "options": {
+ "temperature": 0.1 if is_retry else 0.2,
+ "num_predict": 1024
+ }
+ },
+ timeout=300.0 # CPU inference can take 2-3 min for qwen3:8b
+ )
+ response.raise_for_status()
+ raw = response.json().get("message", {}).get("content", "")
+
+ # Clean JSON markdown blocks
+ clean = raw.strip()
+ if clean.startswith("```json"):
+ clean = clean[7:]
+ elif clean.startswith("```"):
+ clean = clean[3:]
+ if clean.endswith("```"):
+ clean = clean[:-3]
+
+ clean = clean.strip()
+ return json.loads(clean)
+
+ except (httpx.RequestError, httpx.HTTPStatusError) as e:
+ if attempt == 2:
+ raise Exception(f"Ollama connection failed after 3 attempts: {str(e)}")
+ await asyncio.sleep(2)
+
+ except json.JSONDecodeError as e:
+ if not is_retry:
+ # Retry once with stricter prompt
+ return await generate_workflow(nl_input, is_retry=True)
+ raise Exception(f"Failed to generate valid JSON: {str(e)}\nRaw output: {raw[:100]}...")
+
+async def explain_workflow(workflow_json: Dict[str, Any]) -> str:
+ for attempt in range(3):
+ try:
+ async with httpx.AsyncClient() as client:
+ response = await client.post(
+ f"{OLLAMA_URL}/api/chat",
+ json={
+ "model": OLLAMA_MODEL,
+ "messages": [
+ {"role": "system", "content": EXPLAINER_PROMPT},
+ {"role": "user", "content": f"Workflow JSON:\n{json.dumps(workflow_json)}"}
+ ],
+ "stream": False,
+ "options": {
+ "temperature": 0.3,
+ "num_predict": 512
+ }
+ },
+ timeout=120.0 # Explanation is shorter, but still needs time on CPU
+ )
+ response.raise_for_status()
+ raw = response.json().get("message", {}).get("content", "")
+ return raw.strip()
+ except (httpx.RequestError, httpx.HTTPStatusError) as e:
+ if attempt == 2:
+ raise Exception(f"Ollama connection failed after 3 attempts: {str(e)}")
+ await asyncio.sleep(2)
+
diff --git a/backend/package.json b/backend/package.json
deleted file mode 100644
index 71bb720..0000000
--- a/backend/package.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "name": "backend",
- "version": "1.0.0",
- "description": "",
- "main": "server.js",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
- "start": "node server.js"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "dependencies": {
- "@google/generative-ai": "^0.24.1",
- "@whiskeysockets/baileys": "^7.0.0-rc.9",
- "cors": "^2.8.6",
- "dotenv": "^17.2.3",
- "express": "^5.2.1",
- "googleapis": "^170.1.0",
- "qrcode-terminal": "^0.12.0"
- }
-}
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 0000000..bafd7e8
--- /dev/null
+++ b/backend/requirements.txt
@@ -0,0 +1,7 @@
+fastapi
+uvicorn
+httpx
+pydantic
+sqlmodel
+python-multipart
+multipart
\ No newline at end of file
diff --git a/backend/routers/audit.py b/backend/routers/audit.py
new file mode 100644
index 0000000..10ab341
--- /dev/null
+++ b/backend/routers/audit.py
@@ -0,0 +1,21 @@
+from fastapi import APIRouter, Depends
+from sqlmodel import Session, select
+from database import get_session
+from models import AuditLog
+
+router = APIRouter(prefix="/api/audit", tags=["Audit"])
+
+@router.get("")
+def list_logs(limit: int = 50, offset: int = 0, db: Session = Depends(get_session)):
+ logs = db.exec(select(AuditLog).order_by(AuditLog.executed_at.desc()).offset(offset).limit(limit)).all()
+ return logs
+
+@router.get("/{workflow_id}")
+def workflow_logs(workflow_id: str, limit: int = 50, db: Session = Depends(get_session)):
+ logs = db.exec(
+ select(AuditLog)
+ .where(AuditLog.workflow_id == workflow_id)
+ .order_by(AuditLog.executed_at.desc())
+ .limit(limit)
+ ).all()
+ return logs
diff --git a/backend/routers/inventory.py b/backend/routers/inventory.py
new file mode 100644
index 0000000..67228b4
--- /dev/null
+++ b/backend/routers/inventory.py
@@ -0,0 +1,59 @@
+import os
+import csv
+from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
+from sqlmodel import Session, select
+from database import get_session
+from models import InventorySource
+import uuid
+
+router = APIRouter(prefix="/api/inventory", tags=["Inventory"])
+
+DATA_DIR = "/app/data" if os.environ.get("DATABASE_URL") else "./data"
+
+@router.post("/upload")
+async def upload_inventory(
+ name: str = Form(...),
+ file: UploadFile = File(...),
+ db: Session = Depends(get_session)
+):
+ try:
+ os.makedirs(DATA_DIR, exist_ok=True)
+ file_path = os.path.join(DATA_DIR, file.filename)
+
+ with open(file_path, "wb") as f:
+ f.write(await file.read())
+
+ source = InventorySource(
+ id=str(uuid.uuid4()),
+ name=name,
+ type="csv",
+ file_path=file_path
+ )
+ db.add(source)
+ db.commit()
+ db.refresh(source)
+ return {"success": True, "source": source}
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@router.get("/list")
+def list_inventory(db: Session = Depends(get_session)):
+ sources = db.exec(select(InventorySource)).all()
+ return sources
+
+@router.get("/{id}/data")
+def get_inventory_data(id: str, db: Session = Depends(get_session)):
+ source = db.get(InventorySource, id)
+ if not source or source.type != "csv":
+ raise HTTPException(status_code=404, detail="Source not found or invalid CSV")
+
+ if not os.path.exists(source.file_path):
+ raise HTTPException(status_code=404, detail="File missing from disk")
+
+ data = []
+ with open(source.file_path, mode="r", encoding="utf-8") as f:
+ reader = csv.DictReader(f)
+ for row in reader:
+ data.append(row)
+
+ return data
diff --git a/backend/routers/templates.py b/backend/routers/templates.py
new file mode 100644
index 0000000..743c3e8
--- /dev/null
+++ b/backend/routers/templates.py
@@ -0,0 +1,35 @@
+import uuid
+from fastapi import APIRouter, Depends, HTTPException
+from sqlmodel import Session, select
+from database import get_session
+from models import Template, Workflow
+
+router = APIRouter(prefix="/api/templates", tags=["Templates"])
+
+@router.get("")
+def list_templates(db: Session = Depends(get_session)):
+ return db.exec(select(Template)).all()
+
+@router.post("/{id}/fork")
+def fork_template(id: str, db: Session = Depends(get_session)):
+ template = db.get(Template, id)
+ if not template:
+ raise HTTPException(status_code=404, detail="Template not found")
+
+ wf_id = str(uuid.uuid4())
+ workflow = Workflow(
+ id=wf_id,
+ name=f"Copy of {template.name}",
+ description=template.description,
+ nodes=template.nodes,
+ edges=template.edges,
+ is_active=False
+ )
+ db.add(workflow)
+
+ template.use_count += 1
+ db.add(template)
+
+ db.commit()
+ db.refresh(workflow)
+ return {"success": True, "workflow": workflow.id}
diff --git a/backend/schemas.py b/backend/schemas.py
new file mode 100644
index 0000000..7757a6c
--- /dev/null
+++ b/backend/schemas.py
@@ -0,0 +1,29 @@
+from pydantic import BaseModel
+from typing import List, Dict, Any, Optional
+
+class NodePosition(BaseModel):
+ x: float
+ y: float
+
+class WorkflowNode(BaseModel):
+ id: str
+ type: str
+ position: NodePosition
+ data: Dict[str, Any]
+
+class WorkflowEdge(BaseModel):
+ id: str
+ source: str
+ target: str
+ label: Optional[str] = None
+
+class WorkflowSchema(BaseModel):
+ id: Optional[str] = None
+ name: str
+ description: Optional[str] = None
+ nl_input: Optional[str] = None
+ nodes: List[WorkflowNode]
+ edges: List[WorkflowEdge]
+
+class WorkflowGenerateRequest(BaseModel):
+ nl_input: str
diff --git a/backend/seeder.py b/backend/seeder.py
new file mode 100644
index 0000000..efe7a0c
--- /dev/null
+++ b/backend/seeder.py
@@ -0,0 +1,90 @@
+import os
+import uuid
+from sqlmodel import Session, select
+from models import Template, Workflow
+
+def seed_database(db: Session):
+ existing_templates = db.exec(select(Template)).all()
+ if not existing_templates:
+ seed_templates(db)
+
+ existing_workflows = db.exec(select(Workflow)).all()
+ if not existing_workflows:
+ seed_demo_workflows(db)
+
+def seed_templates(db: Session):
+ templates = [
+ Template(id=str(uuid.uuid4()), name="Stock Query Auto-Reply", category="inventory", description="Automatically replies to stock queries from WhatsApp based on CSV data."),
+ Template(id=str(uuid.uuid4()), name="Payment Reminder", category="payments", description="Reminds customers to pay overdue balances."),
+ Template(id=str(uuid.uuid4()), name="Customer Broadcast", category="broadcast", description="Send announcements to your customer list."),
+ Template(id=str(uuid.uuid4()), name="Order Confirmation", category="orders", description="Thanks customer for order and updates tracking."),
+ Template(id=str(uuid.uuid4()), name="Udhaar Reminder", category="payments", description="Polite reminder for local udhaar books.")
+ ]
+ for t in templates:
+ db.add(t)
+ db.commit()
+
+def seed_demo_workflows(db: Session):
+ # E1 - Demo Workflow 1
+ w1 = Workflow(
+ id=str(uuid.uuid4()),
+ name="Stock Query Auto-Reply",
+ description="Demo workflow for FOSS Hack checking stock",
+ is_active=True,
+ nodes=[
+ {
+ "id": "trigger_1", "type": "trigger",
+ "position": {"x": 100, "y": 200},
+ "data": {"config": {"keywords": ["stock", "available", "have you got", "do you have", "sugar", "rice"]}}
+ },
+ {
+ "id": "inventory_1", "type": "action",
+ "position": {"x": 350, "y": 200},
+ "data": {"action_type": "inventory_lookup", "config": {"source_id": "default"}}
+ },
+ {
+ "id": "condition_1", "type": "condition",
+ "position": {"x": 600, "y": 200},
+ "data": {"config": {"field": "results.inventory_1.quantity", "operator": "greater_than", "value": "0"}}
+ },
+ {
+ "id": "whatsapp_true", "type": "whatsapp",
+ "position": {"x": 850, "y": 100},
+ "data": {"config": {"to": "{{trigger.from}}", "message": "Yes! {{results.inventory_1.product}} is available. Stock: {{results.inventory_1.quantity}} units."}}
+ },
+ {
+ "id": "whatsapp_false", "type": "whatsapp",
+ "position": {"x": 850, "y": 300},
+ "data": {"config": {"to": "{{trigger.from}}", "message": "Sorry, {{results.inventory_1.product}} is out of stock right now."}}
+ }
+ ],
+ edges=[
+ {"id": "e1", "source": "trigger_1", "target": "inventory_1"},
+ {"id": "e2", "source": "inventory_1", "target": "condition_1"},
+ {"id": "e3", "source": "condition_1", "target": "whatsapp_true", "label": "true"},
+ {"id": "e4", "source": "condition_1", "target": "whatsapp_false", "label": "false"}
+ ]
+ )
+
+ # E2 - Demo Workflow 2 (Simulated via Manual Trigger with Broadcast style action)
+ w2 = Workflow(
+ id=str(uuid.uuid4()),
+ name="Udhaar Payment Reminder",
+ description="Reminds overdue customers",
+ is_active=False,
+ nodes=[
+ {
+ "id": "trigger_2", "type": "trigger", "position": {"x": 100, "y": 200},
+ "data": {"config": {"keywords": []}, "trigger_type": "manual"}
+ },
+ {
+ "id": "action_2", "type": "action", "position": {"x": 400, "y": 200},
+ "data": {"action_type": "load_udhaar_list", "config": {}}
+ }
+ ],
+ edges=[{"id": "e_u1", "source": "trigger_2", "target": "action_2"}]
+ )
+
+ db.add(w1)
+ db.add(w2)
+ db.commit()
diff --git a/backend/server.js b/backend/server.js
deleted file mode 100644
index c968d7e..0000000
--- a/backend/server.js
+++ /dev/null
@@ -1,18 +0,0 @@
-require('dotenv').config();
-const app = require('./src/app');
-const whatsappService = require('./src/services/whatsapp.service');
-
-const PORT = process.env.PORT || 3000;
-
-// Start Server
-app.listen(PORT, async () => {
- console.log(`๐ Server running on port ${PORT}`);
-
- // Connect to WhatsApp after server starts
- try {
- console.log("๐ฑ Connecting to WhatsApp...");
- await whatsappService.connectToWhatsApp();
- } catch (err) {
- console.error("โ WhatsApp Connection Failed:", err);
- }
-});
\ No newline at end of file
diff --git a/backend/src/app.js b/backend/src/app.js
deleted file mode 100644
index 9f5fa9d..0000000
--- a/backend/src/app.js
+++ /dev/null
@@ -1,19 +0,0 @@
-const express = require('express');
-const cors = require('cors');
-const apiRoutes = require('./routes/api.routes');
-
-const app = express();
-
-// Middleware
-app.use(cors()); // Allow frontend to call backend
-app.use(express.json()); // Parse JSON bodies
-
-// Routes
-app.use('/api', apiRoutes);
-
-// Root Endpoint (Check if server is alive)
-app.get('/', (req, res) => {
- res.send('๐ค AutoFlow AI Backend is Running!');
-});
-
-module.exports = app;
\ No newline at end of file
diff --git a/backend/src/config/env.js b/backend/src/config/env.js
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/src/controllers/whatsapp.controller.js b/backend/src/controllers/whatsapp.controller.js
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/src/controllers/workflow.controller.js b/backend/src/controllers/workflow.controller.js
deleted file mode 100644
index 4d9deb1..0000000
--- a/backend/src/controllers/workflow.controller.js
+++ /dev/null
@@ -1,21 +0,0 @@
-const aiService = require('../services/ai.service');
-
-exports.createWorkflow = async (req, res) => {
- try {
- const { userPrompt } = req.body;
- const workflow = await aiService.generateWorkflow(userPrompt);
- res.json({ success: true, workflow });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-};
-
-exports.explainWorkflow = async (req, res) => {
- try {
- const { workflow } = req.body;
- const explanation = await aiService.explainWorkflow(workflow);
- res.json({ success: true, explanation });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-};
\ No newline at end of file
diff --git a/backend/src/routes/api.routes.js b/backend/src/routes/api.routes.js
deleted file mode 100644
index 91200ea..0000000
--- a/backend/src/routes/api.routes.js
+++ /dev/null
@@ -1,29 +0,0 @@
-const express = require('express');
-const router = express.Router();
-const workflowController = require('../controllers/workflow.controller');
-const engineService = require('../services/engine.service'); // Import Engine
-
-// AI Routes
-router.post('/generate-workflow', workflowController.createWorkflow);
-router.post('/explain-workflow', workflowController.explainWorkflow);
-
-// โ
SIMULATION ROUTE (The Test Bridge)
-router.post('/simulate-message', async (req, res) => {
- const { message } = req.body;
- let botReply = "";
-
- // MOCK SOCKET: Pretends to be WhatsApp
- const mockSock = {
- sendMessage: async (sender, content) => {
- botReply = content.text; // Capture the reply
- }
- };
-
- // Run the real engine logic
- await engineService.processMessage(mockSock, "TestUser", message);
-
- // Send the captured reply back to Frontend
- res.json({ success: true, reply: botReply });
-});
-
-module.exports = router;
\ No newline at end of file
diff --git a/backend/src/services/ai.service.js b/backend/src/services/ai.service.js
deleted file mode 100644
index f14d5ab..0000000
--- a/backend/src/services/ai.service.js
+++ /dev/null
@@ -1,74 +0,0 @@
-const { GoogleGenerativeAI } = require("@google/generative-ai");
-require('dotenv').config();
-
-const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
-
-exports.generateWorkflow = async (userPrompt) => {
- console.log("๐ค AI Service: Generating workflow for:", userPrompt);
-
- try {
- const model = genAI.getGenerativeModel({ model: "gemini-flash-latest" });
-
- const prompt = `
- You are an automation builder.
- The user describes a workflow.
-
- Your job: ALWAYS output VALID JSON ONLY.
-
- Format:
- {
- "trigger": "whatsapp_message",
- "steps": [
- { "id": "1", "type": "trigger", "data": { "label": "Message Received" }, "position": { "x": 250, "y": 0 } },
- { "id": "2", "type": "action", "data": { "label": "Analyze Intent" }, "position": { "x": 250, "y": 100 } },
- { "id": "3", "type": "condition", "data": { "label": "Is Product Inquiry?" }, "position": { "x": 250, "y": 200 } },
- { "id": "4", "type": "action", "data": { "label": "Reply with Price" }, "position": { "x": 250, "y": 300 } }
- ]
- }
-
- If the user text is unclear, still guess the most likely workflow.
- NEVER output text outside JSON.
- User said: "${userPrompt}"
- `;
-
- const result = await model.generateContent(prompt);
- const response = await result.response;
- const text = response.text();
-
- console.log("๐ค AI Raw Response:", text);
-
- // Clean up markdown code blocks if Gemini adds them
- const cleanJson = text.replace(/```json/g, "").replace(/```/g, "").trim();
-
- try {
- const parsed = JSON.parse(cleanJson);
- // Ensure "steps" exists for React Flow
- if (!parsed.steps) {
- parsed.steps = [];
- }
- return parsed;
- } catch (parseError) {
- console.error("โ JSON Parse Failed. Raw text:", cleanJson);
- throw new Error("AI returned invalid JSON");
- }
-
- } catch (error) {
- console.error("โ AI Service Error:", error);
- // Fallback workflow so UI doesn't break
- return {
- trigger: "whatsapp_message",
- steps: [
- { id: "1", type: "trigger", data: { label: "Message Received" }, position: { x: 250, y: 0 } },
- { id: "2", type: "action", data: { label: "AI Error - Default Flow" }, position: { x: 250, y: 100 } }
- ]
- };
- }
-};
-
-exports.explainWorkflow = async (workflowJson) => {
- const model = genAI.getGenerativeModel({ model: "gemini-flash-latest" });
- const prompt = `Explain this workflow in simple, human language with emojis:\n${JSON.stringify(workflowJson)}`;
-
- const result = await model.generateContent(prompt);
- return result.response.text();
-};
\ No newline at end of file
diff --git a/backend/src/services/engine.service.js b/backend/src/services/engine.service.js
deleted file mode 100644
index 9bdca99..0000000
--- a/backend/src/services/engine.service.js
+++ /dev/null
@@ -1,85 +0,0 @@
-const sheetsService = require('./sheets.service');
-
-// Default active workflow (In real app, AI generates this)
-let activeWorkflow = {
- trigger: "whatsapp_message",
- steps: [
- { type: "detect_intent" },
- { type: "action_router" },
- { type: "reply" }
- ]
-};
-
-exports.processMessage = async (sock, sender, messageText) => {
- console.log(`โ๏ธ Engine Processing: "${messageText}"`);
-
- const lowerMsg = messageText.toLowerCase();
- let replyText = "";
-
- // 1. HANDLE GREETINGS
- if (lowerMsg === "hi" || lowerMsg === "hello" || lowerMsg === "hy" || lowerMsg === "hey") {
- await sock.sendMessage(sender, { text: "๐ Hi there! I'm AutoFlow AI.\n\nYou can ask:\n- List all products\n- Price of Red Lipstick\n- Track order" });
- return;
- }
-
- // 2. DETECT INTENT (The Brain)
- let intent = "unknown";
- let productQuery = "";
-
- // A. LIST PRODUCTS (New!)
- if (lowerMsg.includes("list") || lowerMsg.includes("products") || lowerMsg.includes("have") || lowerMsg.includes("catalogue")) {
- intent = "list_products";
- }
- // B. BUY / PLACE ORDER (New!)
- else if (lowerMsg.includes("buy") || lowerMsg.includes("need one") || lowerMsg.includes("want") || lowerMsg.includes("book") || lowerMsg === "order one") {
- intent = "place_order";
- }
- // C. TRACK ORDER (Refined)
- // Only triggers if user says "track", "status", or "where is"
- else if (lowerMsg.includes("track") || lowerMsg.includes("status") || lowerMsg.includes("where is")) {
- intent = "order_tracking";
- }
- // D. PRODUCT INQUIRY (Price/Stock)
- else if (lowerMsg.includes("price") || lowerMsg.includes("available") || lowerMsg.includes("hai kya") || lowerMsg.includes("cost") || lowerMsg.includes("stock")) {
- intent = "product_inquiry";
- productQuery = messageText
- .replace(/price|available|hai kya|cost|stock|please|tell|me|the|for|is|of|what|are/gi, "")
- .trim();
- }
-
- // 3. EXECUTE ACTIONS
- if (intent === "list_products") {
- const productList = await sheetsService.getAllProducts();
- replyText = `๐๏ธ *Here is our Product List:*\n\n${productList}\n\nReply with a product name to check stock!`;
- await sock.sendMessage(sender, { text: replyText });
- }
- else if (intent === "place_order") {
- // Simple buying flow
- replyText = "๐ Great choice! To place your order, please reply with your **Full Name and Address**.";
- await sock.sendMessage(sender, { text: replyText });
- }
- else if (intent === "product_inquiry") {
- const data = await sheetsService.lookupProduct(productQuery);
- if (data.found) {
- if (data.stock > 0) {
- replyText = `โ
Yes! ${data.product} is available.\n๐ฐ Price: โน${data.price}\n๐ฆ Stock: ${data.stock} units.\n\nType "Buy now" to order!`;
- } else {
- replyText = `โ Sorry, ${data.product} is currently out of stock.`;
- }
- } else {
- if (productQuery.length > 1) {
- replyText = `๐ค I couldn't find "${productQuery}". Try asking "List products" to see what we have.`;
- } else {
- replyText = "Please type the product name.";
- }
- }
- await sock.sendMessage(sender, { text: replyText });
- }
- else if (intent === "order_tracking") {
- await sock.sendMessage(sender, { text: "๐ฆ To track your order, please send your Order ID (e.g. #ORD-123)." });
- }
- else {
- // Fallback
- await sock.sendMessage(sender, { text: "๐ค I didn't understand. Try:\n- 'List products'\n- 'Price of Red Lipstick'" });
- }
-};
\ No newline at end of file
diff --git a/backend/src/services/sheets.service.js b/backend/src/services/sheets.service.js
deleted file mode 100644
index 3845390..0000000
--- a/backend/src/services/sheets.service.js
+++ /dev/null
@@ -1,24 +0,0 @@
-const mockInventory = [
- { product: "Red Lipstick", price: 299, stock: 15 },
- { product: "Blue Eyeliner", price: 349, stock: 8 },
- { product: "Foundation", price: 599, stock: 0 },
- { product: "Matte Compact", price: 199, stock: 20 }
-];
-
-// Existing lookup function
-exports.lookupProduct = async (productName) => {
- // Simple fuzzy search
- const item = mockInventory.find(p =>
- p.product.toLowerCase().includes(productName.toLowerCase())
- );
-
- if (item) {
- return { found: true, ...item };
- }
- return { found: false };
-};
-
-// NEW: Function to get list of products
-exports.getAllProducts = async () => {
- return mockInventory.map(p => `- ${p.product} (โน${p.price})`).join("\n");
-};
\ No newline at end of file
diff --git a/backend/src/services/whatsapp.service.js b/backend/src/services/whatsapp.service.js
deleted file mode 100644
index 2650dd8..0000000
--- a/backend/src/services/whatsapp.service.js
+++ /dev/null
@@ -1,69 +0,0 @@
-const makeWASocket = require('@whiskeysockets/baileys').default;
-const { useMultiFileAuthState, DisconnectReason } = require('@whiskeysockets/baileys');
-const qrcode = require('qrcode-terminal');
-const engineService = require('./engine.service');
-
-exports.connectToWhatsApp = async () => {
- const { state, saveCreds } = await useMultiFileAuthState('auth_info_baileys');
-
- const sock = makeWASocket({
- auth: state,
- logger: require('pino')({ level: 'silent' })
- });
-
- sock.ev.on('creds.update', saveCreds);
-
- sock.ev.on('connection.update', (update) => {
- const { connection, lastDisconnect, qr } = update;
-
- if (qr) {
- console.log("โโโโโโโโโโโโโโโโโโโโโโโโโโโ");
- console.log("โ SCAN THIS QR WITH WHATSAPP โ");
- console.log("โโโโโโโโโโโโโโโโโโโโโโโโโโโ");
- qrcode.generate(qr, { small: true });
- }
-
- if (connection === 'close') {
- const shouldReconnect = lastDisconnect.error?.output?.statusCode !== DisconnectReason.loggedOut;
- console.log('โ Connection closed. Reconnecting...', shouldReconnect);
- if (shouldReconnect) {
- exports.connectToWhatsApp();
- }
- } else if (connection === 'open') {
- console.log('โ
WhatsApp Connected Successfully!');
- }
- });
-
- // Listen for Messages
- sock.ev.on('messages.upsert', async ({ messages }) => {
- const msg = messages[0];
-
- // Ignore messages sent by yourself
- if (!msg.key.fromMe && msg.message) {
-
- // 1. EXTRACT TEXT SAFELY (The Fix)
- const userMessage =
- msg.message.conversation ||
- msg.message.extendedTextMessage?.text ||
- msg.message.imageMessage?.caption ||
- null;
-
- const sender = msg.key.remoteJid;
-
- // 2. IGNORE EMPTY MESSAGES (Status updates, stickers, etc.)
- if (!userMessage) {
- // console.log(`๐ฉ Ignored non-text message from ${sender}`);
- return;
- }
-
- console.log(`๐ฉ New Message from ${sender}: ${userMessage}`);
-
- // 3. SEND TO ENGINE
- try {
- await engineService.processMessage(sock, sender, userMessage);
- } catch (error) {
- console.error("โ Engine Error:", error);
- }
- }
- });
-};
\ No newline at end of file
diff --git a/backend/test_models.js b/backend/test_models.js
deleted file mode 100644
index f50194e..0000000
--- a/backend/test_models.js
+++ /dev/null
@@ -1,43 +0,0 @@
-const { GoogleGenerativeAI } = require("@google/generative-ai");
-require('dotenv').config();
-
-async function listModels() {
- const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
- try {
- // For listing models, we don't need a specific model instance usually,
- // but the SDK structure usually implies using the model manager if available
- // or just iterating.
- // However, the node SDK doesn't expose listModels directly on the main class easily in all versions.
- // Let's try to just use a known model that usually exists 'embedding-001' or similar to test connection,
- // or try to find the list method.
- // Actually, newer SDKs don't always have a listModels helper without a model manager.
- // Let's try to infer from the error or just try 'gemini-1.0-pro' which is often the strict name.
-
- // Attempting to check via a direct request if SDK doesn't support listModels easily
- // But wait, the error message literally said: "Call ListModels to see the list of available models".
- // This implies the API supports it.
-
- // Let's try a very basic "gemini-1.5-flash-latest" or "gemini-1.0-pro" in a simple generation to see if it works.
- console.group("Testing Models...");
-
- const candidates = ["gemini-1.5-flash", "gemini-pro", "gemini-1.0-pro", "gemini-1.0-pro-001", "gemini-1.5-pro"];
-
- for (const modelName of candidates) {
- process.stdout.write(`Testing ${modelName}... `);
- try {
- const model = genAI.getGenerativeModel({ model: modelName });
- const result = await model.generateContent("Hello");
- console.log("โ
Success!");
- console.log("Response:", result.response.text());
- process.exit(0); // Found a working one
- } catch (e) {
- console.log("โ Failed (" + e.message.split('[')[0] + "...)");
- }
- }
-
- } catch (error) {
- console.error("Error:", error);
- }
-}
-
-listModels();
diff --git a/data/customers.csv b/data/customers.csv
new file mode 100644
index 0000000..67dd2a4
--- /dev/null
+++ b/data/customers.csv
@@ -0,0 +1,4 @@
+name,phone
+Customer A,919999999994
+Customer B,919999999995
+Customer C,919999999996
diff --git a/data/inventory.csv b/data/inventory.csv
new file mode 100644
index 0000000..a828833
--- /dev/null
+++ b/data/inventory.csv
@@ -0,0 +1,11 @@
+product,quantity,price
+rice,50,45.00
+wheat,100,30.00
+sugar,0,40.00
+dal,20,110.00
+oil,15,140.00
+salt,200,20.00
+tea,30,250.00
+coffee,10,550.00
+biscuits,100,10.00
+soap,45,35.00
diff --git a/data/udhaar.csv b/data/udhaar.csv
new file mode 100644
index 0000000..2229596
--- /dev/null
+++ b/data/udhaar.csv
@@ -0,0 +1,4 @@
+name,phone,amount,date,days_overdue
+Raju,919999999991,500,2026-02-15,27
+Sita,919999999992,1200,2026-03-01,13
+Mohan,919999999993,300,2026-03-05,9
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..ff016a4
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,54 @@
+services:
+ frontend:
+ build: ./autoflow-frontend
+ ports: ["3000:3000"]
+ depends_on: [backend]
+ volumes:
+ - ./autoflow-frontend:/app
+ - /app/node_modules
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:3000"]
+ interval: 15s
+ timeout: 5s
+ retries: 5
+
+ backend:
+ build: ./backend
+ ports: ["8000:8000"]
+ environment:
+ - OLLAMA_URL=http://ollama:11434
+ - WHATSAPP_BRIDGE_URL=http://whatsapp-bridge:3001
+ - DATABASE_URL=sqlite:////app/data/autoflow.db
+ depends_on: [ollama, whatsapp-bridge]
+ volumes:
+ - ./backend:/app
+ - ./data:/app/data
+ command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8000/docs"]
+ interval: 15s
+ timeout: 5s
+ retries: 5
+
+ whatsapp-bridge:
+ build: ./whatsapp-bridge
+ ports: ["3001:3001"]
+ volumes:
+ - whatsapp_session:/app/auth_info_baileys
+
+ ollama:
+ image: ollama/ollama
+ ports: ["11434:11434"]
+ volumes:
+ - ollama_data:/root/.ollama
+ entrypoint: >
+ sh -c "ollama serve & sleep 8 && ollama pull qwen3:1.7b && wait"
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
+ interval: 15s
+ timeout: 5s
+ retries: 5
+
+volumes:
+ ollama_data:
+ whatsapp_session:
diff --git a/list_model.py b/list_model.py
deleted file mode 100644
index 410182f..0000000
--- a/list_model.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from google.generativeai import configure, list_models
-
-# 1๏ธโฃ Add your Gemini API key here
-configure(api_key="AIzaSyDcc8gnjCMIAV_z48vYuUwMC9-OSH3Qe04")
-
-# 2๏ธโฃ Fetch & print all models
-print("\n๐ Available Gemini Models:\n")
-
-for model in list_models():
- print(f"- {model.name}")
diff --git a/whatsapp-bridge/Dockerfile b/whatsapp-bridge/Dockerfile
new file mode 100644
index 0000000..5281a53
--- /dev/null
+++ b/whatsapp-bridge/Dockerfile
@@ -0,0 +1,10 @@
+FROM node:20-alpine
+
+RUN apk add --no-cache git
+
+WORKDIR /app
+COPY package.json ./
+RUN npm install
+COPY . .
+EXPOSE 3001
+CMD ["node", "server.js"]
diff --git a/whatsapp-bridge/package.json b/whatsapp-bridge/package.json
new file mode 100644
index 0000000..1b54bfb
--- /dev/null
+++ b/whatsapp-bridge/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "whatsapp-bridge",
+ "version": "1.0.0",
+ "main": "server.js",
+ "dependencies": {
+ "baileys": "^6.7.16",
+ "express": "^4.18.2",
+ "cors": "^2.8.5",
+ "axios": "^1.6.0",
+ "pino": "^9.6.0",
+ "qrcode-terminal": "^0.12.0"
+ }
+}
diff --git a/whatsapp-bridge/server.js b/whatsapp-bridge/server.js
new file mode 100644
index 0000000..1b4f21e
--- /dev/null
+++ b/whatsapp-bridge/server.js
@@ -0,0 +1,274 @@
+const express = require('express');
+const cors = require('cors');
+const axios = require('axios');
+const fs = require('fs');
+const qrcode = require('qrcode-terminal');
+const makeWASocket = require('baileys').default;
+const { useMultiFileAuthState, DisconnectReason } = require('baileys');
+
+const app = express();
+app.use(cors());
+app.use(express.json());
+
+const PORT = 3001;
+const WEBHOOK_URL = process.env.WEBHOOK_URL || 'http://localhost:8000/api/whatsapp/incoming';
+const AUTH_DIR = '/app/auth_info_baileys';
+
+// โโโ Global State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+let sock = null;
+let isConnected = false;
+let isConnecting = false;
+let qrCode = null;
+let reconnectAttempts = 0;
+const MAX_RECONNECT = 10;
+
+// โโโ WhatsApp Connection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+const connectToWhatsApp = async () => {
+ if (isConnected) {
+ console.log('โ ๏ธ WhatsApp already connected.');
+ return { status: 'already_connected' };
+ }
+ if (isConnecting) {
+ console.log('โ ๏ธ WhatsApp connection already in progress.');
+ return { status: 'connecting' };
+ }
+
+ isConnecting = true;
+ qrCode = null;
+
+ // Cleanup previous socket
+ if (sock) {
+ try {
+ sock.ev.removeAllListeners();
+ sock.end();
+ sock = null;
+ } catch (e) {
+ console.error('Cleanup error:', e.message);
+ }
+ }
+
+ try {
+ const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
+
+ sock = makeWASocket({
+ auth: state,
+ printQRInTerminal: false, // We handle QR ourselves
+ logger: require('pino')({ level: 'silent' }),
+ browser: ['AutoFlow', 'Chrome', '1.0.0']
+ });
+
+ // Save credentials
+ sock.ev.on('creds.update', async () => {
+ try {
+ await saveCreds();
+ } catch (e) {
+ // Ignore
+ }
+ });
+
+ // Connection update handler
+ sock.ev.on('connection.update', async (update) => {
+ const { connection, lastDisconnect, qr } = update;
+
+ // โโ QR Code โโโโโโโโโโโโโโโโโโโโ
+ if (qr) {
+ console.log('\n๐ธ โโโโโโโ SCAN THIS QR CODE โโโโโโโ');
+ qrcode.generate(qr, { small: true });
+ console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n');
+ qrCode = qr;
+ reconnectAttempts = 0; // Reset on QR (we're making progress)
+ }
+
+ // โโ Connection Opened โโโโโโโโโโ
+ if (connection === 'open') {
+ console.log('โ
WhatsApp Connected Successfully!');
+ isConnected = true;
+ isConnecting = false;
+ qrCode = null;
+ reconnectAttempts = 0;
+ }
+
+ // โโ Connection Closed โโโโโโโโโโ
+ if (connection === 'close') {
+ isConnected = false;
+
+ const statusCode = lastDisconnect?.error?.output?.statusCode;
+ // 401 = Logged Out
+ // 405 = Not Acceptable / Corrupt Session Data
+ const shouldReconnect = statusCode !== DisconnectReason.loggedOut && statusCode !== 405;
+
+ console.log(`Connection closed. Status: ${statusCode || 'initial'}, Reconnect: ${shouldReconnect}`);
+
+ // Cleanup
+ try { sock?.ev?.removeAllListeners(); } catch (e) { }
+ try { sock?.end(); } catch (e) { }
+ sock = null;
+ isConnecting = false;
+
+ if (!shouldReconnect) {
+ // Logged out โ clear session
+ console.log('โ ๏ธ Logged out. Clearing session...');
+ qrCode = null;
+ reconnectAttempts = 0;
+ try {
+ await new Promise(r => setTimeout(r, 500));
+ if (fs.existsSync(AUTH_DIR)) {
+ const files = fs.readdirSync(AUTH_DIR);
+ for (const file of files) {
+ fs.rmSync(`${AUTH_DIR}/${file}`, { recursive: true, force: true });
+ }
+ }
+ console.log('๐ Session cleared. Call /deploy to reconnect.');
+ } catch (e) {
+ console.error('Failed to delete session:', e.message);
+ }
+ } else if (reconnectAttempts < MAX_RECONNECT) {
+ reconnectAttempts++;
+ const delay = Math.min(3000 * reconnectAttempts, 30000); // Exponential backoff, max 30s
+ console.log(`๐ Reconnecting (${reconnectAttempts}/${MAX_RECONNECT}) in ${delay / 1000}s...`);
+ setTimeout(() => connectToWhatsApp(), delay);
+ } else {
+ console.error(`โ Max reconnect attempts (${MAX_RECONNECT}) reached. Call /deploy to retry.`);
+ reconnectAttempts = 0;
+ }
+ }
+ });
+
+ // โโ Incoming Messages โโโโโโโโโโโโ
+ sock.ev.on('messages.upsert', async ({ messages }) => {
+ const msg = messages[0];
+ if (!msg.message || msg.key.fromMe) return;
+
+ const sender = msg.key.remoteJid;
+ if (sender === 'status@broadcast') return;
+
+ const userMessage =
+ msg.message.conversation ||
+ msg.message.extendedTextMessage?.text ||
+ msg.message.imageMessage?.caption ||
+ null;
+
+ if (!userMessage) return;
+
+ console.log(`๐ฉ Message from ${sender}: ${userMessage}`);
+
+ try {
+ await axios.post(WEBHOOK_URL, {
+ from: sender,
+ message: userMessage,
+ timestamp: Math.floor(Date.now() / 1000)
+ });
+ } catch (error) {
+ console.error('Webhook push failed:', error.message);
+ }
+ });
+
+ return { status: 'initiated' };
+
+ } catch (e) {
+ console.error('โ Connection Failed:', e.message);
+ isConnecting = false;
+ return { error: e.message };
+ }
+};
+
+// โโโ REST API โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+app.get('/status', (req, res) => {
+ res.json({
+ success: true,
+ connected: isConnected,
+ connecting: isConnecting,
+ qr: qrCode,
+ reconnectAttempts
+ });
+});
+
+app.get('/qr', (req, res) => {
+ res.json({ qr: qrCode });
+});
+
+app.post('/deploy', async (req, res) => {
+ reconnectAttempts = 0; // Reset on manual deploy
+ isConnecting = false; // Allow fresh connection
+ const result = await connectToWhatsApp();
+ res.json(result);
+});
+
+app.post('/send', async (req, res) => {
+ const { to, message } = req.body;
+ if (!to || !message) {
+ return res.status(400).json({ error: "Missing 'to' or 'message'" });
+ }
+ if (!isConnected || !sock) {
+ return res.status(503).json({ error: 'WhatsApp not connected' });
+ }
+
+ try {
+ const jid = to.includes('@') ? to : `${to}@s.whatsapp.net`;
+ await sock.sendMessage(jid, { text: message });
+ res.json({ success: true, to: jid });
+ } catch (e) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
+app.post('/broadcast', async (req, res) => {
+ const { numbers, message } = req.body;
+ if (!numbers || !Array.isArray(numbers) || !message) {
+ return res.status(400).json({ error: 'Invalid broadcast payload' });
+ }
+ if (!isConnected || !sock) {
+ return res.status(503).json({ error: 'WhatsApp not connected' });
+ }
+
+ let sent = 0;
+ for (const num of numbers) {
+ try {
+ const jid = num.includes('@') ? num : `${num}@s.whatsapp.net`;
+ await sock.sendMessage(jid, { text: message });
+ sent++;
+ } catch (e) {
+ console.error(`Failed sending to ${num}:`, e.message);
+ }
+ }
+ res.json({ success: true, totalSent: sent });
+});
+
+app.post('/logout', async (req, res) => {
+ try {
+ if (sock) {
+ sock.ev.removeAllListeners();
+ sock.end();
+ sock = null;
+ }
+ } catch (e) {
+ console.error('Error closing socket:', e.message);
+ }
+
+ isConnected = false;
+ isConnecting = false;
+ qrCode = null;
+ reconnectAttempts = 0;
+
+ try {
+ console.log('โ ๏ธ Manual Logout. Clearing session...');
+ if (fs.existsSync(AUTH_DIR)) {
+ const files = fs.readdirSync(AUTH_DIR);
+ for (const file of files) {
+ fs.rmSync(`${AUTH_DIR}/${file}`, { recursive: true, force: true });
+ }
+ }
+ res.json({ success: true });
+ } catch (e) {
+ res.status(500).json({ success: false, error: e.message });
+ }
+});
+
+// โโโ Start โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+app.listen(PORT, () => {
+ console.log(`\n๐ WhatsApp Bridge running on port ${PORT}`);
+ console.log(` Webhook: ${WEBHOOK_URL}`);
+ console.log(` Starting WhatsApp connection...\n`);
+ connectToWhatsApp();
+});