mirror of
https://github.com/oarkflow/mq.git
synced 2025-10-05 07:57:00 +08:00
update
This commit is contained in:
@@ -5,10 +5,13 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oarkflow/json"
|
||||
|
||||
"github.com/oarkflow/jet"
|
||||
"github.com/oarkflow/mq"
|
||||
"github.com/oarkflow/mq/consts"
|
||||
"github.com/oarkflow/mq/dag"
|
||||
)
|
||||
|
||||
@@ -28,75 +31,52 @@ func loginSubDAG() *dag.DAG {
|
||||
return login
|
||||
}
|
||||
|
||||
// phoneProcessingSubDAG creates a sub-DAG for processing phone numbers
|
||||
func phoneProcessingSubDAG() *dag.DAG {
|
||||
phone := dag.NewDAG("Phone Processing Sub DAG", "phone-processing-sub-dag", func(taskID string, result mq.Result) {
|
||||
fmt.Printf("Phone Processing Sub DAG Final result for task %s: %s\n", taskID, string(result.Payload))
|
||||
}, mq.WithSyncMode(true))
|
||||
|
||||
phone.
|
||||
AddNode(dag.Function, "Parse Phone Numbers", "parse-phones", &ParsePhoneNumbers{}).
|
||||
AddNode(dag.Function, "Phone Loop", "phone-loop", &PhoneLoop{}).
|
||||
AddNode(dag.Function, "Validate Phone", "validate-phone", &ValidatePhone{}).
|
||||
AddNode(dag.Function, "Send Welcome SMS", "send-welcome", &SendWelcomeSMS{}).
|
||||
AddNode(dag.Function, "Collect Valid Phones", "collect-valid", &CollectValidPhones{}).
|
||||
AddNode(dag.Function, "Collect Invalid Phones", "collect-invalid", &CollectInvalidPhones{}).
|
||||
AddEdge(dag.Simple, "Parse to Loop", "parse-phones", "phone-loop").
|
||||
AddEdge(dag.Iterator, "Loop over phones", "phone-loop", "validate-phone").
|
||||
AddCondition("validate-phone", map[string]string{"valid": "send-welcome", "invalid": "collect-invalid"}).
|
||||
AddEdge(dag.Simple, "Welcome to Collect", "send-welcome", "collect-valid").
|
||||
AddEdge(dag.Simple, "Invalid to Collect", "collect-invalid", "collect-valid").
|
||||
AddEdge(dag.Simple, "Loop to Collect", "phone-loop", "collect-valid")
|
||||
|
||||
return phone
|
||||
}
|
||||
|
||||
func main() {
|
||||
flow := dag.NewDAG("Complex Phone Processing DAG with Pages", "complex-phone-dag", func(taskID string, result mq.Result) {
|
||||
fmt.Printf("Complex DAG Final result for task %s: %s\n", taskID, string(result.Payload))
|
||||
})
|
||||
flow.ConfigureMemoryStorage()
|
||||
|
||||
// Main nodes
|
||||
flow.AddNode(dag.Function, "Initialize", "init", &Initialize{}, true)
|
||||
flow.AddDAGNode(dag.Function, "Login Process", "login", loginSubDAG())
|
||||
flow.AddNode(dag.Function, "Upload Phone Data", "upload-page", &UploadPhoneDataPage{})
|
||||
flow.AddDAGNode(dag.Function, "Process Phones", "process-phones", phoneProcessingSubDAG())
|
||||
// Main nodes - Login process as individual nodes (not sub-DAG) for proper page serving
|
||||
flow.AddNode(dag.Page, "Initialize", "init", &Initialize{}, true)
|
||||
flow.AddNode(dag.Page, "Login Page", "login-page", &LoginPage{})
|
||||
flow.AddNode(dag.Function, "Verify Credentials", "verify-credentials", &VerifyCredentials{})
|
||||
flow.AddNode(dag.Function, "Generate Token", "generate-token", &GenerateToken{})
|
||||
flow.AddNode(dag.Page, "Upload Phone Data", "upload-page", &UploadPhoneDataPage{})
|
||||
flow.AddNode(dag.Function, "Parse Phone Numbers", "parse-phones", &ParsePhoneNumbers{})
|
||||
flow.AddNode(dag.Function, "Phone Loop", "phone-loop", &PhoneLoop{})
|
||||
flow.AddNode(dag.Function, "Validate Phone", "validate-phone", &ValidatePhone{})
|
||||
flow.AddNode(dag.Function, "Send Welcome SMS", "send-welcome", &SendWelcomeSMS{})
|
||||
flow.AddNode(dag.Function, "Collect Valid Phones", "collect-valid", &CollectValidPhones{})
|
||||
flow.AddNode(dag.Function, "Collect Invalid Phones", "collect-invalid", &CollectInvalidPhones{})
|
||||
flow.AddNode(dag.Function, "Generate Report", "generate-report", &GenerateReport{})
|
||||
flow.AddNode(dag.Function, "Send Summary Email", "send-summary", &SendSummaryEmail{})
|
||||
flow.AddNode(dag.Function, "Final Cleanup", "cleanup", &FinalCleanup{})
|
||||
|
||||
// Edges
|
||||
flow.AddEdge(dag.Simple, "Init to Login", "init", "login")
|
||||
flow.AddEdge(dag.Simple, "Login to Upload", "login", "upload-page")
|
||||
flow.AddEdge(dag.Simple, "Upload to Process", "upload-page", "process-phones")
|
||||
flow.AddEdge(dag.Simple, "Process to Report", "process-phones", "generate-report")
|
||||
// Edges - Connect login flow individually
|
||||
flow.AddEdge(dag.Simple, "Init to Login", "init", "login-page")
|
||||
flow.AddEdge(dag.Simple, "Login to Verify", "login-page", "verify-credentials")
|
||||
flow.AddEdge(dag.Simple, "Verify to Token", "verify-credentials", "generate-token")
|
||||
flow.AddEdge(dag.Simple, "Token to Upload", "generate-token", "upload-page")
|
||||
flow.AddEdge(dag.Simple, "Upload to Parse", "upload-page", "parse-phones")
|
||||
flow.AddEdge(dag.Simple, "Parse to Loop", "parse-phones", "phone-loop")
|
||||
flow.AddEdge(dag.Iterator, "Loop over phones", "phone-loop", "validate-phone")
|
||||
flow.AddCondition("validate-phone", map[string]string{"valid": "send-welcome", "invalid": "collect-invalid"})
|
||||
flow.AddEdge(dag.Simple, "Welcome to Collect", "send-welcome", "collect-valid")
|
||||
flow.AddEdge(dag.Simple, "Invalid to Collect", "collect-invalid", "collect-valid")
|
||||
flow.AddEdge(dag.Simple, "Loop to Report", "phone-loop", "generate-report")
|
||||
flow.AddEdge(dag.Simple, "Report to Summary", "generate-report", "send-summary")
|
||||
flow.AddEdge(dag.Simple, "Summary to Cleanup", "send-summary", "cleanup")
|
||||
|
||||
// Sample data for testing
|
||||
data := map[string]interface{}{
|
||||
"user_id": "user123",
|
||||
"session_data": map[string]interface{}{
|
||||
"authenticated": false,
|
||||
},
|
||||
"phone_data": map[string]interface{}{
|
||||
"format": "csv",
|
||||
"content": "name,phone\nJohn Doe,+1234567890\nJane Smith,+1987654321\nBob Johnson,invalid-phone\nAlice Brown,+1555123456",
|
||||
},
|
||||
}
|
||||
// Check for DAG errors
|
||||
// if flow.Error != nil {
|
||||
// fmt.Printf("DAG Error: %v\n", flow.Error)
|
||||
// panic(flow.Error)
|
||||
// }
|
||||
|
||||
jsonData, _ := json.Marshal(data)
|
||||
if flow.Error != nil {
|
||||
panic(flow.Error)
|
||||
}
|
||||
|
||||
rs := flow.Process(context.Background(), jsonData)
|
||||
if rs.Error != nil {
|
||||
panic(rs.Error)
|
||||
}
|
||||
fmt.Println("Complex Phone DAG Status:", rs.Status, "Topic:", rs.Topic)
|
||||
fmt.Println("Final Payload:", string(rs.Payload))
|
||||
fmt.Println("Starting Complex Phone Processing DAG server on http://0.0.0.0:8080")
|
||||
fmt.Println("Navigate to the URL to access the login page")
|
||||
flow.Start(context.Background(), ":8080")
|
||||
}
|
||||
|
||||
// Task implementations
|
||||
@@ -108,12 +88,151 @@ type Initialize struct {
|
||||
func (p *Initialize) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(task.Payload, &data); err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("Initialize Error: %s", err.Error()), Ctx: ctx}
|
||||
data = make(map[string]interface{})
|
||||
}
|
||||
data["initialized"] = true
|
||||
data["timestamp"] = "2025-09-19T12:00:00Z"
|
||||
updatedPayload, _ := json.Marshal(data)
|
||||
return mq.Result{Payload: updatedPayload, Ctx: ctx}
|
||||
|
||||
// Add sample phone data for testing
|
||||
sampleCSV := `name,phone
|
||||
John Doe,+1234567890
|
||||
Jane Smith,0987654321
|
||||
Bob Johnson,1555123456
|
||||
Alice Brown,invalid-phone
|
||||
Charlie Wilson,+441234567890`
|
||||
|
||||
data["phone_data"] = map[string]interface{}{
|
||||
"content": sampleCSV,
|
||||
"format": "csv",
|
||||
"source": "sample_data",
|
||||
"created_at": "2025-09-19T12:00:00Z",
|
||||
}
|
||||
|
||||
// Generate a task ID for this workflow instance
|
||||
taskID := "workflow-" + fmt.Sprintf("%d", time.Now().Unix())
|
||||
|
||||
// Since this is a page node, show a welcome page that auto-redirects to login
|
||||
htmlContent := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="3;url=/process">
|
||||
<title>Phone Processing System</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.welcome {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 40px;
|
||||
border-radius: 15px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
.welcome h1 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
.welcome p {
|
||||
margin-bottom: 30px;
|
||||
font-size: 1.1em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.features {
|
||||
margin-top: 30px;
|
||||
text-align: left;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.features h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #fff;
|
||||
}
|
||||
.features ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.features li {
|
||||
margin-bottom: 8px;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}
|
||||
.features li:before {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #4CAF50;
|
||||
}
|
||||
.countdown {
|
||||
margin-top: 20px;
|
||||
font-size: 1.2em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="welcome">
|
||||
<h1>📱 Phone Processing System</h1>
|
||||
<p>Welcome to our advanced phone number processing workflow</p>
|
||||
|
||||
<div class="features">
|
||||
<h3>Features:</h3>
|
||||
<ul>
|
||||
<li>CSV/JSON file upload support</li>
|
||||
<li>Phone number validation and formatting</li>
|
||||
<li>Automated welcome SMS sending</li>
|
||||
<li>Invalid number filtering</li>
|
||||
<li>Comprehensive processing reports</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="countdown">
|
||||
<p>Initializing workflow...</p>
|
||||
<p>Task ID: ` + taskID + `</p>
|
||||
<p>Redirecting to login page in <span id="countdown">3</span> seconds...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let countdown = 3;
|
||||
const countdownElement = document.getElementById('countdown');
|
||||
const interval = setInterval(() => {
|
||||
countdown--;
|
||||
countdownElement.textContent = countdown;
|
||||
if (countdown <= 0) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
|
||||
rs, err := parser.ParseTemplate(htmlContent, map[string]any{})
|
||||
if err != nil {
|
||||
return mq.Result{Error: err, Ctx: ctx}
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
|
||||
resultData := map[string]any{
|
||||
"html_content": rs,
|
||||
"step": "initialize",
|
||||
"data": data,
|
||||
}
|
||||
|
||||
resultPayload, _ := json.Marshal(resultData)
|
||||
return mq.Result{Payload: resultPayload, Ctx: ctx}
|
||||
}
|
||||
|
||||
type LoginPage struct {
|
||||
@@ -121,21 +240,177 @@ type LoginPage struct {
|
||||
}
|
||||
|
||||
func (p *LoginPage) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
// Check if this is a form submission
|
||||
var inputData map[string]interface{}
|
||||
if len(task.Payload) > 0 {
|
||||
if err := json.Unmarshal(task.Payload, &inputData); err == nil {
|
||||
// Check if we have form data (username/password)
|
||||
if formData, ok := inputData["form"].(map[string]interface{}); ok {
|
||||
// This is a form submission, pass it through for verification
|
||||
credentials := map[string]interface{}{
|
||||
"username": formData["username"],
|
||||
"password": formData["password"],
|
||||
}
|
||||
inputData["credentials"] = credentials
|
||||
updatedPayload, _ := json.Marshal(inputData)
|
||||
return mq.Result{Payload: updatedPayload, Ctx: ctx}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, show the form
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(task.Payload, &data); err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("LoginPage Error: %s", err.Error()), Ctx: ctx}
|
||||
data = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Simulate user input from page
|
||||
data["credentials"] = map[string]interface{}{
|
||||
"username": "admin",
|
||||
"password": "password123",
|
||||
// HTML content for login page
|
||||
htmlContent := `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Phone Processing System - Login</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
data["login_attempted"] = true
|
||||
.login-container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.login-header h1 {
|
||||
color: #333;
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.login-header p {
|
||||
color: #666;
|
||||
margin: 0.5rem 0 0 0;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.login-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.login-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.status-message {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.status-error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>📱 Phone Processing System</h1>
|
||||
<p>Please login to continue</p>
|
||||
</div>
|
||||
<form method="post" action="/process" id="loginForm">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required placeholder="Enter your username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required placeholder="Enter your password">
|
||||
</div>
|
||||
<button type="submit" class="login-btn">Login</button>
|
||||
</form>
|
||||
<div id="statusMessage"></div>
|
||||
</div>
|
||||
|
||||
updatedPayload, _ := json.Marshal(data)
|
||||
<script>
|
||||
// Form will submit naturally to the action URL
|
||||
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||
// Optional: Add loading state
|
||||
const btn = e.target.querySelector('.login-btn');
|
||||
btn.textContent = 'Logging in...';
|
||||
btn.disabled = true;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
|
||||
rs, err := parser.ParseTemplate(htmlContent, map[string]any{})
|
||||
if err != nil {
|
||||
return mq.Result{Error: err, Ctx: ctx}
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
|
||||
resultData := map[string]any{
|
||||
"html_content": rs,
|
||||
"step": "login",
|
||||
"data": data,
|
||||
}
|
||||
|
||||
resultPayload, _ := json.Marshal(resultData)
|
||||
return mq.Result{
|
||||
Payload: updatedPayload,
|
||||
Payload: resultPayload,
|
||||
Ctx: ctx,
|
||||
}
|
||||
}
|
||||
@@ -195,22 +470,293 @@ type UploadPhoneDataPage struct {
|
||||
}
|
||||
|
||||
func (p *UploadPhoneDataPage) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
// Check if this is a form submission
|
||||
var inputData map[string]interface{}
|
||||
if len(task.Payload) > 0 {
|
||||
if err := json.Unmarshal(task.Payload, &inputData); err == nil {
|
||||
// Check if we have form data (phone_data)
|
||||
if formData, ok := inputData["form"].(map[string]interface{}); ok {
|
||||
// This is a form submission, pass it through for processing
|
||||
if phoneData, exists := formData["phone_data"]; exists && phoneData != "" {
|
||||
inputData["phone_data"] = map[string]interface{}{
|
||||
"content": phoneData.(string),
|
||||
"format": "csv",
|
||||
"source": "user_input",
|
||||
"created_at": "2025-09-19T12:00:00Z",
|
||||
}
|
||||
}
|
||||
updatedPayload, _ := json.Marshal(inputData)
|
||||
return mq.Result{Payload: updatedPayload, Ctx: ctx}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, show the form
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(task.Payload, &data); err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("UploadPhoneDataPage Error: %s", err.Error()), Ctx: ctx}
|
||||
data = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Simulate user interaction - in a real scenario, this would be user input
|
||||
// The phone data is already in the payload from initialization
|
||||
data["upload_completed"] = true
|
||||
data["uploaded_at"] = "2025-09-19T12:05:00Z"
|
||||
data["user_interaction"] = map[string]interface{}{
|
||||
"confirmed_upload": true,
|
||||
"upload_method": "file_upload",
|
||||
// HTML content for upload page
|
||||
htmlContent := `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Phone Processing System - Upload Data</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.upload-container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
.upload-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.upload-header h1 {
|
||||
color: #333;
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.upload-header p {
|
||||
color: #666;
|
||||
margin: 0.5rem 0 0 0;
|
||||
}
|
||||
.upload-area {
|
||||
border: 2px dashed #667eea;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: border-color 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.upload-area:hover {
|
||||
border-color: #764ba2;
|
||||
}
|
||||
.upload-area.dragover {
|
||||
border-color: #28a745;
|
||||
background: #f8fff9;
|
||||
}
|
||||
.upload-icon {
|
||||
font-size: 3rem;
|
||||
color: #667eea;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.upload-text {
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.file-info {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 1rem;
|
||||
display: none;
|
||||
}
|
||||
.file-info.show {
|
||||
display: block;
|
||||
}
|
||||
.file-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.file-size {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.upload-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.upload-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.upload-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.upload-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e9ecef;
|
||||
border-radius: 4px;
|
||||
margin-top: 1rem;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
}
|
||||
.progress-bar.show {
|
||||
display: block;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.status-message {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
display: none;
|
||||
}
|
||||
.status-message.show {
|
||||
display: block;
|
||||
}
|
||||
.status-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.status-error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="upload-container">
|
||||
<div class="upload-header">
|
||||
<h1>📤 Upload Phone Data</h1>
|
||||
<p>Upload your CSV file containing phone numbers for processing</p>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/process" id="uploadForm" enctype="multipart/form-data">
|
||||
<div class="upload-area" id="uploadArea">
|
||||
<div class="upload-icon">📁</div>
|
||||
<div class="upload-text">Drag & drop your CSV file here or click to browse</div>
|
||||
<div style="color: #999; font-size: 0.9rem; margin-top: 0.5rem;">Supported format: CSV with name,phone columns</div>
|
||||
<input type="file" id="fileInput" name="file" accept=".csv,.json" style="display: none;">
|
||||
</div>
|
||||
|
||||
<div style="margin: 20px 0; text-align: center; color: #666;">OR</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phoneData" style="color: #333; font-weight: bold;">Paste CSV/JSON Data:</label>
|
||||
<textarea id="phoneData" name="phone_data" rows="8" placeholder="name,phone John Doe,+1234567890 Jane Smith,0987654321 Or paste JSON array..." style="width: 100%; padding: 10px; border: 2px solid #e1e5e9; border-radius: 5px; font-family: monospace; resize: vertical;">name,phone
|
||||
John Doe,+1234567890
|
||||
Jane Smith,0987654321
|
||||
Bob Johnson,1555123456
|
||||
Alice Brown,invalid-phone
|
||||
Charlie Wilson,+441234567890</textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="upload-btn" id="uploadBtn">Upload & Process</button> <div class="progress-bar" id="progressBar">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
|
||||
<div class="status-message" id="statusMessage"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const phoneDataTextarea = document.getElementById('phoneData');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
const uploadForm = document.getElementById('uploadForm');
|
||||
|
||||
// Upload area click handler
|
||||
uploadArea.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
// File input change handler
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
// Clear textarea if file is selected
|
||||
phoneDataTextarea.value = '';
|
||||
phoneDataTextarea.disabled = true;
|
||||
} else {
|
||||
phoneDataTextarea.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Textarea input handler
|
||||
phoneDataTextarea.addEventListener('input', () => {
|
||||
if (phoneDataTextarea.value.trim()) {
|
||||
// Clear file input if textarea has content
|
||||
fileInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission handler
|
||||
uploadForm.addEventListener('submit', (e) => {
|
||||
uploadBtn.textContent = 'Processing...';
|
||||
uploadBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Drag and drop handlers
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.add('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', () => {
|
||||
uploadArea.classList.remove('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('dragover');
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
fileInput.files = files;
|
||||
fileInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
|
||||
rs, err := parser.ParseTemplate(htmlContent, map[string]any{})
|
||||
if err != nil {
|
||||
return mq.Result{Error: err, Ctx: ctx}
|
||||
}
|
||||
|
||||
updatedPayload, _ := json.Marshal(data)
|
||||
return mq.Result{Payload: updatedPayload, Ctx: ctx}
|
||||
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
|
||||
resultData := map[string]any{
|
||||
"html_content": rs,
|
||||
"step": "upload",
|
||||
"data": data,
|
||||
}
|
||||
|
||||
resultPayload, _ := json.Marshal(resultData)
|
||||
return mq.Result{
|
||||
Payload: resultPayload,
|
||||
Ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
type ParsePhoneNumbers struct {
|
||||
|
720
examples/form.go
720
examples/form.go
@@ -3,10 +3,14 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oarkflow/json"
|
||||
|
||||
"github.com/oarkflow/mq/dag"
|
||||
"github.com/oarkflow/mq/utils"
|
||||
|
||||
"github.com/oarkflow/jet"
|
||||
|
||||
@@ -15,146 +19,690 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
flow := dag.NewDAG("Multi-Step Form", "multi-step-form", func(taskID string, result mq.Result) {
|
||||
fmt.Printf("Final result for task %s: %s\n", taskID, string(result.Payload))
|
||||
flow := dag.NewDAG("SMS Sender", "sms-sender", func(taskID string, result mq.Result) {
|
||||
fmt.Printf("SMS workflow completed for task %s: %s\n", taskID, string(utils.RemoveRecursiveFromJSON(result.Payload, "html_content")))
|
||||
})
|
||||
flow.AddNode(dag.Page, "Form Step1", "FormStep1", &FormStep1{})
|
||||
flow.AddNode(dag.Page, "Form Step2", "FormStep2", &FormStep2{})
|
||||
flow.AddNode(dag.Page, "Form Result", "FormResult", &FormResult{})
|
||||
|
||||
// Define edges
|
||||
flow.AddEdge(dag.Simple, "Form Step1", "FormStep1", "FormStep2")
|
||||
flow.AddEdge(dag.Simple, "Form Step2", "FormStep2", "FormResult")
|
||||
// Add SMS workflow nodes
|
||||
// Note: Page nodes have no timeout by default, allowing users unlimited time for form input
|
||||
flow.AddNode(dag.Page, "SMS Form", "SMSForm", &SMSFormNode{})
|
||||
flow.AddNode(dag.Function, "Validate Input", "ValidateInput", &ValidateInputNode{})
|
||||
flow.AddNode(dag.Function, "Send SMS", "SendSMS", &SendSMSNode{})
|
||||
flow.AddNode(dag.Page, "SMS Result", "SMSResult", &SMSResultNode{})
|
||||
flow.AddNode(dag.Page, "Error Page", "ErrorPage", &ErrorPageNode{})
|
||||
|
||||
// Define edges for SMS workflow
|
||||
flow.AddEdge(dag.Simple, "Form to Validation", "SMSForm", "ValidateInput")
|
||||
flow.AddCondition("ValidateInput", map[string]string{"valid": "SendSMS", "invalid": "ErrorPage"})
|
||||
flow.AddCondition("SendSMS", map[string]string{"sent": "SMSResult", "failed": "ErrorPage"})
|
||||
|
||||
// Start the flow
|
||||
if flow.Error != nil {
|
||||
panic(flow.Error)
|
||||
}
|
||||
flow.Start(context.Background(), "0.0.0.0:8082")
|
||||
|
||||
fmt.Println("Starting SMS DAG server on http://0.0.0.0:8083")
|
||||
fmt.Println("Navigate to the URL to access the SMS form")
|
||||
flow.Start(context.Background(), "0.0.0.0:8083")
|
||||
}
|
||||
|
||||
type FormStep1 struct {
|
||||
// SMSFormNode - Initial form to collect SMS data
|
||||
type SMSFormNode struct {
|
||||
dag.Operation
|
||||
}
|
||||
|
||||
func (p *FormStep1) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
bt := []byte(`
|
||||
func (s *SMSFormNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
// Check if this is a form submission
|
||||
var inputData map[string]any
|
||||
if task.Payload != nil && len(task.Payload) > 0 {
|
||||
if err := json.Unmarshal(task.Payload, &inputData); err == nil {
|
||||
// If we have valid input data, pass it through for validation
|
||||
return mq.Result{Payload: task.Payload, Ctx: ctx}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, show the form
|
||||
htmlTemplate := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>SMS Sender</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
.form-container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
|
||||
}
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
input::placeholder, textarea::placeholder {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
textarea {
|
||||
height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
.char-count {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
button {
|
||||
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
|
||||
color: white;
|
||||
padding: 15px 30px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
transition: transform 0.2s;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.info {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<form method="post" action="/process?task_id={{task_id}}&next=true">
|
||||
<label>Name:</label>
|
||||
<input type="text" name="name" required>
|
||||
<label>Age:</label>
|
||||
<input type="number" name="age" required>
|
||||
<button type="submit">Next</button>
|
||||
</form>
|
||||
</body
|
||||
</html
|
||||
<div class="form-container">
|
||||
<h1>📱 SMS Sender</h1>
|
||||
<div class="info">
|
||||
<p>Send SMS messages through our secure DAG workflow</p>
|
||||
</div>
|
||||
<form method="post" action="/process?task_id={{task_id}}&next=true">
|
||||
<div class="form-group">
|
||||
<label for="phone">📞 Phone Number:</label>
|
||||
<input type="tel" id="phone" name="phone"
|
||||
placeholder="+1234567890 or 1234567890"
|
||||
required>
|
||||
<div class="info" style="margin-top: 5px; font-size: 12px;">
|
||||
Supports US format: +1234567890 or 1234567890
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="message">💬 Message:</label>
|
||||
<textarea id="message" name="message"
|
||||
placeholder="Enter your message here..."
|
||||
maxlength="160"
|
||||
required
|
||||
oninput="updateCharCount()"></textarea>
|
||||
<div class="char-count" id="charCount">0/160 characters</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sender_name">👤 Sender Name (Optional):</label>
|
||||
<input type="text" id="sender_name" name="sender_name"
|
||||
placeholder="Your name or organization"
|
||||
maxlength="50">
|
||||
</div>
|
||||
|
||||
<button type="submit">🚀 Send SMS</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function updateCharCount() {
|
||||
const messageInput = document.getElementById('message');
|
||||
const charCount = document.getElementById('charCount');
|
||||
const count = messageInput.value.length;
|
||||
charCount.textContent = count + '/160 characters';
|
||||
|
||||
if (count > 140) {
|
||||
charCount.style.color = '#FFB6B6';
|
||||
} else {
|
||||
charCount.style.color = 'rgba(255, 255, 255, 0.8)';
|
||||
}
|
||||
}
|
||||
|
||||
// Format phone number as user types
|
||||
document.getElementById('phone').addEventListener('input', function(e) {
|
||||
let value = e.target.value.replace(/\D/g, '');
|
||||
if (value.length > 0 && !value.startsWith('1') && value.length === 10) {
|
||||
value = '1' + value;
|
||||
}
|
||||
if (value.length > 11) {
|
||||
value = value.substring(0, 11);
|
||||
}
|
||||
e.target.value = value;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
`)
|
||||
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
|
||||
rs, err := parser.ParseTemplate(string(bt), map[string]any{
|
||||
rs, err := parser.ParseTemplate(htmlTemplate, map[string]any{
|
||||
"task_id": ctx.Value("task_id"),
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("FormStep1", string(task.Payload))
|
||||
return mq.Result{Error: err, Ctx: ctx}
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
|
||||
data := map[string]any{
|
||||
"html_content": rs,
|
||||
"step": "form",
|
||||
}
|
||||
bt, _ = json.Marshal(data)
|
||||
bt, _ := json.Marshal(data)
|
||||
return mq.Result{Payload: bt, Ctx: ctx}
|
||||
}
|
||||
|
||||
type FormStep2 struct {
|
||||
// ValidateInputNode - Validates phone number and message
|
||||
type ValidateInputNode struct {
|
||||
dag.Operation
|
||||
}
|
||||
|
||||
func (p *FormStep2) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
// Parse input from Step 1
|
||||
func (v *ValidateInputNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var inputData map[string]any
|
||||
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
|
||||
return mq.Result{
|
||||
Error: fmt.Errorf("invalid input data: %v", err),
|
||||
Ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// Extract form data
|
||||
phone, _ := inputData["phone"].(string)
|
||||
message, _ := inputData["message"].(string)
|
||||
senderName, _ := inputData["sender_name"].(string)
|
||||
|
||||
// Validate phone number
|
||||
if phone == "" {
|
||||
inputData["validation_error"] = "Phone number is required"
|
||||
inputData["error_field"] = "phone"
|
||||
bt, _ := json.Marshal(inputData)
|
||||
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
|
||||
}
|
||||
|
||||
// Clean and validate phone number format
|
||||
cleanPhone := regexp.MustCompile(`\D`).ReplaceAllString(phone, "")
|
||||
|
||||
// Check for valid US phone number (10 or 11 digits)
|
||||
if len(cleanPhone) == 10 {
|
||||
cleanPhone = "1" + cleanPhone // Add country code
|
||||
} else if len(cleanPhone) != 11 || !strings.HasPrefix(cleanPhone, "1") {
|
||||
inputData["validation_error"] = "Invalid phone number format. Please use US format: +1234567890 or 1234567890"
|
||||
inputData["error_field"] = "phone"
|
||||
bt, _ := json.Marshal(inputData)
|
||||
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
|
||||
}
|
||||
|
||||
// Validate message
|
||||
if message == "" {
|
||||
inputData["validation_error"] = "Message is required"
|
||||
inputData["error_field"] = "message"
|
||||
bt, _ := json.Marshal(inputData)
|
||||
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
|
||||
}
|
||||
|
||||
if len(message) > 160 {
|
||||
inputData["validation_error"] = "Message too long. Maximum 160 characters allowed"
|
||||
inputData["error_field"] = "message"
|
||||
bt, _ := json.Marshal(inputData)
|
||||
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
|
||||
}
|
||||
|
||||
// Check for potentially harmful content
|
||||
forbiddenWords := []string{"spam", "scam", "fraud", "hack"}
|
||||
messageLower := strings.ToLower(message)
|
||||
for _, word := range forbiddenWords {
|
||||
if strings.Contains(messageLower, word) {
|
||||
inputData["validation_error"] = "Message contains prohibited content"
|
||||
inputData["error_field"] = "message"
|
||||
bt, _ := json.Marshal(inputData)
|
||||
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
|
||||
}
|
||||
}
|
||||
|
||||
// All validations passed
|
||||
validatedData := map[string]any{
|
||||
"phone": cleanPhone,
|
||||
"message": message,
|
||||
"sender_name": senderName,
|
||||
"validated_at": time.Now().Format("2006-01-02 15:04:05"),
|
||||
"validation_status": "success",
|
||||
"formatted_phone": formatPhoneForDisplay(cleanPhone),
|
||||
"char_count": len(message),
|
||||
}
|
||||
|
||||
bt, _ := json.Marshal(validatedData)
|
||||
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "valid"}
|
||||
}
|
||||
|
||||
// SendSMSNode - Simulates sending SMS
|
||||
type SendSMSNode struct {
|
||||
dag.Operation
|
||||
}
|
||||
|
||||
func (s *SendSMSNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var inputData map[string]any
|
||||
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
|
||||
return mq.Result{Error: err, Ctx: ctx}
|
||||
}
|
||||
// Determine dynamic content
|
||||
isEligible := inputData["age"] == "18"
|
||||
inputData["show_voting_controls"] = isEligible
|
||||
|
||||
bt := []byte(`
|
||||
<html>
|
||||
phone, _ := inputData["phone"].(string)
|
||||
message, _ := inputData["message"].(string)
|
||||
senderName, _ := inputData["sender_name"].(string)
|
||||
|
||||
<body>
|
||||
<form method="post" action="/process?task_id={{task_id}}&next=true">
|
||||
{{ if show_voting_controls }}
|
||||
<label>Do you want to register to vote?</label>
|
||||
<input type="checkbox" name="register_vote">
|
||||
<button type="submit">Next</button>
|
||||
{{ else }}
|
||||
<p>You are not eligible to vote.</p>
|
||||
{{ end }}
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
|
||||
inputData["task_id"] = ctx.Value("task_id")
|
||||
rs, err := parser.ParseTemplate(string(bt), inputData)
|
||||
if err != nil {
|
||||
fmt.Println("FormStep2", inputData)
|
||||
return mq.Result{Error: err, Ctx: ctx}
|
||||
// Simulate SMS sending delay
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Simulate occasional failures for demo purposes
|
||||
timestamp := time.Now()
|
||||
success := timestamp.Second()%10 != 0 // 90% success rate
|
||||
|
||||
if !success {
|
||||
errorData := map[string]any{
|
||||
"phone": phone,
|
||||
"message": message,
|
||||
"sender_name": senderName,
|
||||
"sms_status": "failed",
|
||||
"error_message": "SMS gateway temporarily unavailable. Please try again.",
|
||||
"sent_at": timestamp.Format("2006-01-02 15:04:05"),
|
||||
"retry_suggested": true,
|
||||
}
|
||||
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
|
||||
inputData["html_content"] = rs
|
||||
bt, _ = json.Marshal(inputData)
|
||||
return mq.Result{Payload: bt, Ctx: ctx}
|
||||
bt, _ := json.Marshal(errorData)
|
||||
return mq.Result{
|
||||
Payload: bt,
|
||||
Ctx: ctx,
|
||||
ConditionStatus: "failed",
|
||||
}
|
||||
}
|
||||
|
||||
// Generate mock SMS ID and response
|
||||
smsID := fmt.Sprintf("SMS_%d_%s", timestamp.Unix(), phone[len(phone)-4:])
|
||||
|
||||
resultData := map[string]any{
|
||||
"phone": phone,
|
||||
"formatted_phone": formatPhoneForDisplay(phone),
|
||||
"message": message,
|
||||
"sender_name": senderName,
|
||||
"sms_status": "sent",
|
||||
"sms_id": smsID,
|
||||
"sent_at": timestamp.Format("2006-01-02 15:04:05"),
|
||||
"delivery_estimate": "1-2 minutes",
|
||||
"cost_estimate": "$0.02",
|
||||
"gateway": "MockSMS Gateway",
|
||||
"char_count": len(message),
|
||||
}
|
||||
|
||||
fmt.Printf("📱 SMS sent successfully! ID: %s, Phone: %s\n", smsID, formatPhoneForDisplay(phone))
|
||||
|
||||
bt, _ := json.Marshal(resultData)
|
||||
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "sent"}
|
||||
}
|
||||
|
||||
type FormResult struct {
|
||||
// SMSResultNode - Shows successful SMS result
|
||||
type SMSResultNode struct {
|
||||
dag.Operation
|
||||
}
|
||||
|
||||
func (p *FormResult) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
// Load HTML template for results
|
||||
bt := []byte(`
|
||||
<html>
|
||||
|
||||
<body>
|
||||
<h1>Form Summary</h1>
|
||||
<p>Name: {{ name }}</p>
|
||||
<p>Age: {{ age }}</p>
|
||||
{{ if register_vote }}
|
||||
<p>You have registered to vote!</p>
|
||||
{{ else }}
|
||||
<p>You did not register to vote.</p>
|
||||
{{ end }}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
`)
|
||||
func (r *SMSResultNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var inputData map[string]any
|
||||
if task.Payload != nil {
|
||||
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
|
||||
return mq.Result{Error: err, Ctx: ctx}
|
||||
}
|
||||
|
||||
htmlTemplate := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SMS Sent Successfully</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
||||
color: white;
|
||||
}
|
||||
if inputData != nil {
|
||||
if isEligible, ok := inputData["register_vote"].(string); ok {
|
||||
inputData["register_vote"] = isEligible
|
||||
} else {
|
||||
inputData["register_vote"] = false
|
||||
.result-container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
text-align: center;
|
||||
}
|
||||
.success-icon {
|
||||
font-size: 60px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 30px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
}
|
||||
.info-item {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.info-value {
|
||||
font-size: 16px;
|
||||
}
|
||||
.message-preview {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.btn {
|
||||
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
|
||||
color: white;
|
||||
padding: 12px 25px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0 10px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.status-badge {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="result-container">
|
||||
<div class="success-icon">✅</div>
|
||||
<h1>SMS Sent Successfully!</h1>
|
||||
|
||||
<div class="status-badge">{{sms_status}}</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">📱 Phone Number</div>
|
||||
<div class="info-value">{{formatted_phone}}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">🆔 SMS ID</div>
|
||||
<div class="info-value">{{sms_id}}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">⏰ Sent At</div>
|
||||
<div class="info-value">{{sent_at}}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">🚚 Delivery</div>
|
||||
<div class="info-value">{{delivery_estimate}}</div>
|
||||
</div>
|
||||
{{if sender_name}}
|
||||
<div class="info-item">
|
||||
<div class="info-label">👤 Sender</div>
|
||||
<div class="info-value">{{sender_name}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="info-item">
|
||||
<div class="info-label">💰 Cost</div>
|
||||
<div class="info-value">{{cost_estimate}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-preview">
|
||||
<div class="info-label">💬 Message Sent ({{char_count}} chars):</div>
|
||||
<div class="info-value" style="margin-top: 10px; font-style: italic;">
|
||||
"{{message}}"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a href="/" class="btn">📱 Send Another SMS</a>
|
||||
<a href="/api/metrics" class="btn">📊 View Metrics</a>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; font-size: 12px; opacity: 0.7;">
|
||||
Gateway: {{gateway}} | Task completed in DAG workflow
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
|
||||
rs, err := parser.ParseTemplate(string(bt), inputData)
|
||||
rs, err := parser.ParseTemplate(htmlTemplate, inputData)
|
||||
if err != nil {
|
||||
return mq.Result{Error: err, Ctx: ctx}
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
|
||||
inputData["html_content"] = rs
|
||||
bt, _ = json.Marshal(inputData)
|
||||
finalData := map[string]any{
|
||||
"html_content": rs,
|
||||
"result": inputData,
|
||||
"step": "success",
|
||||
}
|
||||
bt, _ := json.Marshal(finalData)
|
||||
return mq.Result{Payload: bt, Ctx: ctx}
|
||||
}
|
||||
|
||||
// ErrorPageNode - Shows validation or sending errors
|
||||
type ErrorPageNode struct {
|
||||
dag.Operation
|
||||
}
|
||||
|
||||
func (e *ErrorPageNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var inputData map[string]any
|
||||
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
|
||||
return mq.Result{Error: err, Ctx: ctx}
|
||||
}
|
||||
|
||||
// Determine error type and message
|
||||
errorMessage, _ := inputData["validation_error"].(string)
|
||||
errorField, _ := inputData["error_field"].(string)
|
||||
smsError, _ := inputData["error_message"].(string)
|
||||
|
||||
if errorMessage == "" && smsError != "" {
|
||||
errorMessage = smsError
|
||||
errorField = "sms_sending"
|
||||
}
|
||||
if errorMessage == "" {
|
||||
errorMessage = "An unknown error occurred"
|
||||
}
|
||||
|
||||
htmlTemplate := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SMS Error</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF5722 100%);
|
||||
color: white;
|
||||
}
|
||||
.error-container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
text-align: center;
|
||||
}
|
||||
.error-icon {
|
||||
font-size: 60px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 30px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
.error-message {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
font-size: 16px;
|
||||
border-left: 4px solid #FFB6B6;
|
||||
}
|
||||
.error-details {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.btn {
|
||||
background: linear-gradient(45deg, #4ECDC4, #44A08D);
|
||||
color: white;
|
||||
padding: 12px 25px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0 10px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.retry-btn {
|
||||
background: linear-gradient(45deg, #FFA726, #FF9800);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<div class="error-icon">❌</div>
|
||||
<h1>SMS Error</h1>
|
||||
|
||||
<div class="error-message">
|
||||
{{error_message}}
|
||||
</div>
|
||||
|
||||
{{if error_field}}
|
||||
<div class="error-details">
|
||||
<strong>Error Field:</strong> {{error_field}}<br>
|
||||
<strong>Action Required:</strong> Please correct the highlighted field and try again.
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if retry_suggested}}
|
||||
<div class="error-details">
|
||||
<strong>⚠️ Temporary Issue:</strong> This appears to be a temporary gateway issue.
|
||||
Please try sending your SMS again in a few moments.
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="actions">
|
||||
<a href="/" class="btn retry-btn">🔄 Try Again</a>
|
||||
<a href="/api/status" class="btn">📊 Check Status</a>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; font-size: 12px; opacity: 0.7;">
|
||||
DAG Error Handler | SMS Workflow Failed
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
|
||||
templateData := map[string]any{
|
||||
"error_message": errorMessage,
|
||||
"error_field": errorField,
|
||||
"retry_suggested": inputData["retry_suggested"],
|
||||
}
|
||||
|
||||
rs, err := parser.ParseTemplate(htmlTemplate, templateData)
|
||||
if err != nil {
|
||||
return mq.Result{Error: err, Ctx: ctx}
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
|
||||
finalData := map[string]any{
|
||||
"html_content": rs,
|
||||
"error_data": inputData,
|
||||
"step": "error",
|
||||
}
|
||||
bt, _ := json.Marshal(finalData)
|
||||
return mq.Result{Payload: bt, Ctx: ctx}
|
||||
}
|
||||
|
||||
// Helper function to format phone number for display
|
||||
func formatPhoneForDisplay(phone string) string {
|
||||
if len(phone) == 11 && strings.HasPrefix(phone, "1") {
|
||||
// Format as +1 (XXX) XXX-XXXX
|
||||
return fmt.Sprintf("+1 (%s) %s-%s",
|
||||
phone[1:4],
|
||||
phone[4:7],
|
||||
phone[7:11])
|
||||
}
|
||||
return phone
|
||||
}
|
||||
|
Reference in New Issue
Block a user