feat(vwb-v3): Architecture Thin Client fonctionnelle
API = Source de vérité unique (SQLite + Flask) - Backend: API v3 avec session, workflow, capture, execute - Frontend: Vanilla TypeScript, pas de state local - Contrats stricts pour les actions RPA - Drag & drop pour réorganiser les étapes - Insertion d'étapes entre deux existantes - Bibliothèque de captures (sessionStorage) - Exécution avec coordonnées statiques (pyautogui) Fonctionne mais fragile (coordonnées fixes, pas de détection visuelle) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
96
visual_workflow_builder/frontend_v3/index.html
Normal file
96
visual_workflow_builder/frontend_v3/index.html
Normal file
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VWB v3 - Thin Client</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Visual Workflow Builder v3</h1>
|
||||
<span class="badge">Thin Client - API = Verite</span>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Colonne gauche: Workflows -->
|
||||
<aside class="sidebar">
|
||||
<section class="panel">
|
||||
<h2>Workflows</h2>
|
||||
<form id="create-workflow-form">
|
||||
<input type="text" placeholder="Nouveau workflow..." required />
|
||||
<button type="submit">+</button>
|
||||
</form>
|
||||
<div id="workflow-list"></div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<!-- Zone centrale: Canvas/Etapes -->
|
||||
<section class="main-content">
|
||||
<div class="toolbar">
|
||||
<h3 id="workflow-name">Aucun workflow</h3>
|
||||
<div id="action-buttons"></div>
|
||||
</div>
|
||||
|
||||
<!-- Zone scrollable pour les étapes -->
|
||||
<div class="steps-container">
|
||||
<h4>Étapes du workflow</h4>
|
||||
<div id="steps-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Zone de capture (colonne séparée) -->
|
||||
<section class="capture-section">
|
||||
<div class="capture-zone">
|
||||
<div class="capture-header">
|
||||
<h4>Capture d'écran</h4>
|
||||
<button id="btn-toggle-capture" class="toggle-btn">Réduire</button>
|
||||
</div>
|
||||
<div id="capture-content">
|
||||
<div class="capture-controls">
|
||||
<button id="btn-capture" class="capture-btn">Capturer</button>
|
||||
<div class="timer-control">
|
||||
<select id="capture-delay">
|
||||
<option value="0">Immédiat</option>
|
||||
<option value="3">3s</option>
|
||||
<option value="5">5s</option>
|
||||
<option value="10">10s</option>
|
||||
</select>
|
||||
<button id="btn-capture-timer" class="capture-btn secondary">Avec délai</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="timer-countdown" class="timer-countdown hidden"></div>
|
||||
<div id="capture-preview"></div>
|
||||
<button id="btn-fullscreen" class="fullscreen-btn hidden">Ouvrir en plein écran</button>
|
||||
|
||||
<!-- Bibliothèque de captures -->
|
||||
<div class="capture-library">
|
||||
<h5>Bibliothèque <span id="library-count">(0)</span></h5>
|
||||
<div id="capture-library-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Colonne droite: Propriétés et Execution -->
|
||||
<aside class="sidebar right">
|
||||
<section class="panel">
|
||||
<h2>Étape sélectionnée</h2>
|
||||
<div id="selected-step">
|
||||
<p class="empty">Sélectionnez une étape</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Exécution</h2>
|
||||
<div id="execution-status">
|
||||
<p>Prêt à exécuter</p>
|
||||
<button id="btn-start" class="primary">Démarrer</button>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
944
visual_workflow_builder/frontend_v3/package-lock.json
generated
Normal file
944
visual_workflow_builder/frontend_v3/package-lock.json
generated
Normal file
@@ -0,0 +1,944 @@
|
||||
{
|
||||
"name": "vwb-thin-client",
|
||||
"version": "3.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vwb-thin-client",
|
||||
"version": "3.0.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz",
|
||||
"integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz",
|
||||
"integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz",
|
||||
"integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz",
|
||||
"integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz",
|
||||
"integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz",
|
||||
"integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz",
|
||||
"integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz",
|
||||
"integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz",
|
||||
"integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz",
|
||||
"integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz",
|
||||
"integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz",
|
||||
"integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz",
|
||||
"integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz",
|
||||
"integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz",
|
||||
"integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz",
|
||||
"integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz",
|
||||
"integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz",
|
||||
"integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz",
|
||||
"integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz",
|
||||
"integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz",
|
||||
"integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz",
|
||||
"integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz",
|
||||
"integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz",
|
||||
"integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz",
|
||||
"integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.56.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz",
|
||||
"integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.56.0",
|
||||
"@rollup/rollup-android-arm64": "4.56.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.56.0",
|
||||
"@rollup/rollup-darwin-x64": "4.56.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.56.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.56.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.56.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.56.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.56.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.56.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.56.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.56.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.56.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.56.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.56.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.56.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.56.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.56.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.56.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.56.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.56.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.56.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.56.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.56.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.56.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
visual_workflow_builder/frontend_v3/package.json
Normal file
15
visual_workflow_builder/frontend_v3/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "vwb-thin-client",
|
||||
"version": "3.0.0",
|
||||
"description": "Visual Workflow Builder - Thin Client (API = Verite)",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3001",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
228
visual_workflow_builder/frontend_v3/src/api.ts
Normal file
228
visual_workflow_builder/frontend_v3/src/api.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* API Client VWB v3 - Thin Client
|
||||
* Toutes les interactions avec le backend passent par ce module.
|
||||
* L'API est la SOURCE DE VERITE UNIQUE.
|
||||
*/
|
||||
|
||||
import type { AppState, Workflow, Step, Execution, Capture } from './types';
|
||||
|
||||
const API_BASE = '/api/v3';
|
||||
|
||||
async function request<T>(
|
||||
method: string,
|
||||
endpoint: string,
|
||||
body?: unknown
|
||||
): Promise<T> {
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, options);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Erreur API');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Session
|
||||
// ============================================================
|
||||
|
||||
export async function getState(): Promise<AppState> {
|
||||
const data = await request<{
|
||||
success: boolean;
|
||||
session: AppState['session'];
|
||||
workflow: AppState['workflow'];
|
||||
execution: AppState['execution'];
|
||||
workflows_list: AppState['workflows_list'];
|
||||
}>('GET', '/session/state');
|
||||
|
||||
return {
|
||||
session: data.session,
|
||||
workflow: data.workflow,
|
||||
execution: data.execution,
|
||||
workflows_list: data.workflows_list
|
||||
};
|
||||
}
|
||||
|
||||
export async function selectWorkflow(workflowId: string): Promise<{
|
||||
session: AppState['session'];
|
||||
workflow: Workflow;
|
||||
}> {
|
||||
return request('POST', `/session/select-workflow/${workflowId}`);
|
||||
}
|
||||
|
||||
export async function selectStep(stepId: string): Promise<{
|
||||
session: AppState['session'];
|
||||
step: Step;
|
||||
}> {
|
||||
return request('POST', `/session/select-step/${stepId}`);
|
||||
}
|
||||
|
||||
export async function clearSession(): Promise<{ session: AppState['session'] }> {
|
||||
return request('POST', '/session/clear');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Workflow CRUD
|
||||
// ============================================================
|
||||
|
||||
export async function createWorkflow(
|
||||
name: string,
|
||||
description?: string
|
||||
): Promise<{ workflow: Workflow; session: AppState['session'] }> {
|
||||
return request('POST', '/workflow/create', { name, description });
|
||||
}
|
||||
|
||||
export async function getWorkflow(workflowId: string): Promise<{ workflow: Workflow }> {
|
||||
return request('GET', `/workflow/${workflowId}`);
|
||||
}
|
||||
|
||||
export async function deleteWorkflow(workflowId: string): Promise<{
|
||||
deleted_id: string;
|
||||
session: AppState['session'];
|
||||
}> {
|
||||
return request('DELETE', `/workflow/${workflowId}`);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Steps
|
||||
// ============================================================
|
||||
|
||||
export async function addStep(
|
||||
workflowId: string,
|
||||
actionType: string,
|
||||
options?: {
|
||||
position?: { x: number; y: number };
|
||||
parameters?: Record<string, unknown>;
|
||||
label?: string;
|
||||
insertAfter?: string; // ID de l'étape après laquelle insérer
|
||||
}
|
||||
): Promise<{
|
||||
workflow: Workflow;
|
||||
step: Step;
|
||||
needs_anchor: boolean;
|
||||
session: AppState['session'];
|
||||
}> {
|
||||
return request('POST', `/workflow/${workflowId}/step`, {
|
||||
action_type: actionType,
|
||||
position: options?.position,
|
||||
parameters: options?.parameters,
|
||||
label: options?.label,
|
||||
insert_after: options?.insertAfter
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateStep(
|
||||
workflowId: string,
|
||||
stepId: string,
|
||||
updates: {
|
||||
action_type?: string;
|
||||
position?: { x: number; y: number };
|
||||
parameters?: Record<string, unknown>;
|
||||
label?: string;
|
||||
anchor_id?: string | null;
|
||||
}
|
||||
): Promise<{ workflow: Workflow; step: Step }> {
|
||||
return request('PUT', `/workflow/${workflowId}/step/${stepId}`, updates);
|
||||
}
|
||||
|
||||
export async function deleteStep(
|
||||
workflowId: string,
|
||||
stepId: string
|
||||
): Promise<{ workflow: Workflow; session: AppState['session'] }> {
|
||||
return request('DELETE', `/workflow/${workflowId}/step/${stepId}`);
|
||||
}
|
||||
|
||||
export async function reorderSteps(
|
||||
workflowId: string,
|
||||
stepIds: string[]
|
||||
): Promise<{ workflow: Workflow }> {
|
||||
return request('POST', `/workflow/${workflowId}/reorder`, { step_ids: stepIds });
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Capture
|
||||
// ============================================================
|
||||
|
||||
export async function captureScreen(): Promise<{ capture: Capture }> {
|
||||
return request('POST', '/capture/screen');
|
||||
}
|
||||
|
||||
export async function selectAnchor(
|
||||
stepId: string,
|
||||
bbox: { x: number; y: number; width: number; height: number },
|
||||
description?: string,
|
||||
screenshotBase64?: string // Capture optionnelle
|
||||
): Promise<{
|
||||
workflow: Workflow;
|
||||
step: Step;
|
||||
anchor: import('./types').VisualAnchor;
|
||||
}> {
|
||||
return request('POST', '/capture/select', {
|
||||
step_id: stepId,
|
||||
bbox,
|
||||
description,
|
||||
screenshot_base64: screenshotBase64
|
||||
});
|
||||
}
|
||||
|
||||
export function getAnchorImageUrl(anchorId: string): string {
|
||||
return `${API_BASE}/anchor/${anchorId}/image`;
|
||||
}
|
||||
|
||||
export function getAnchorThumbnailUrl(anchorId: string): string {
|
||||
return `${API_BASE}/anchor/${anchorId}/thumbnail`;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Execution
|
||||
// ============================================================
|
||||
|
||||
export async function startExecution(workflowId?: string): Promise<{
|
||||
execution: Execution;
|
||||
session: AppState['session'];
|
||||
}> {
|
||||
return request('POST', '/execute/start', workflowId ? { workflow_id: workflowId } : {});
|
||||
}
|
||||
|
||||
export async function pauseExecution(): Promise<{ execution: Execution }> {
|
||||
return request('POST', '/execute/pause');
|
||||
}
|
||||
|
||||
export async function resumeExecution(): Promise<{ execution: Execution }> {
|
||||
return request('POST', '/execute/resume');
|
||||
}
|
||||
|
||||
export async function stopExecution(): Promise<{
|
||||
execution: Execution;
|
||||
session: AppState['session'];
|
||||
}> {
|
||||
return request('POST', '/execute/stop');
|
||||
}
|
||||
|
||||
export async function getExecutionStatus(): Promise<{
|
||||
is_running: boolean;
|
||||
is_paused: boolean;
|
||||
execution: Execution | null;
|
||||
session: AppState['session'];
|
||||
}> {
|
||||
return request('GET', '/execute/status');
|
||||
}
|
||||
|
||||
export async function getExecutionHistory(workflowId?: string): Promise<{
|
||||
executions: Execution[];
|
||||
}> {
|
||||
const query = workflowId ? `?workflow_id=${workflowId}` : '';
|
||||
return request('GET', `/execute/history${query}`);
|
||||
}
|
||||
232
visual_workflow_builder/frontend_v3/src/main.ts
Normal file
232
visual_workflow_builder/frontend_v3/src/main.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* VWB v3 - Thin Client
|
||||
* Point d'entrée principal
|
||||
*
|
||||
* PRINCIPE: L'API est la SOURCE DE VERITE UNIQUE
|
||||
* Ce frontend NE FAIT QU'AFFICHER ce que l'API retourne.
|
||||
*/
|
||||
|
||||
import * as api from './api';
|
||||
import * as ui from './ui';
|
||||
import type { AppState, ActionType, Capture } from './types';
|
||||
|
||||
// État local minimal (juste pour le polling et la capture en cours)
|
||||
let pollingInterval: number | null = null;
|
||||
let currentCapture: Capture | null = null;
|
||||
|
||||
async function loadState(): Promise<void> {
|
||||
try {
|
||||
const state = await api.getState();
|
||||
ui.render(state);
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async function init(): Promise<void> {
|
||||
// Initialiser l'UI avec les callbacks
|
||||
ui.initUI({
|
||||
// Workflows
|
||||
onCreateWorkflow: async (name) => {
|
||||
try {
|
||||
await api.createWorkflow(name);
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
onSelectWorkflow: async (id) => {
|
||||
try {
|
||||
await api.selectWorkflow(id);
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
onDeleteWorkflow: async (id) => {
|
||||
try {
|
||||
await api.deleteWorkflow(id);
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
// Steps
|
||||
onAddStep: async (actionType: ActionType, insertAfter?: string) => {
|
||||
try {
|
||||
const state = await api.getState();
|
||||
if (!state.session.active_workflow_id) {
|
||||
ui.showError('Aucun workflow actif');
|
||||
return;
|
||||
}
|
||||
await api.addStep(state.session.active_workflow_id, actionType, {
|
||||
insertAfter
|
||||
});
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
onSelectStep: async (id) => {
|
||||
try {
|
||||
await api.selectStep(id);
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
onDeleteStep: async (id) => {
|
||||
try {
|
||||
const state = await api.getState();
|
||||
if (!state.session.active_workflow_id) return;
|
||||
await api.deleteStep(state.session.active_workflow_id, id);
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
onReorderSteps: async (stepIds) => {
|
||||
console.log('📝 onReorderSteps appelé avec:', stepIds);
|
||||
try {
|
||||
const state = await api.getState();
|
||||
console.log('État récupéré, active_workflow_id:', state.session.active_workflow_id);
|
||||
if (!state.session.active_workflow_id) {
|
||||
console.log('Pas de workflow actif, abandon');
|
||||
return;
|
||||
}
|
||||
console.log('Appel API reorderSteps...');
|
||||
await api.reorderSteps(state.session.active_workflow_id, stepIds);
|
||||
console.log('API reorderSteps terminé, rechargement état...');
|
||||
await loadState();
|
||||
console.log('État rechargé');
|
||||
} catch (error) {
|
||||
console.error('Erreur dans onReorderSteps:', error);
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
onUpdateStepParams: async (id, params) => {
|
||||
try {
|
||||
const state = await api.getState();
|
||||
if (!state.session.active_workflow_id) return;
|
||||
|
||||
// Traiter le cas des touches clavier
|
||||
if ('keys' in params && typeof params.keys === 'string') {
|
||||
params.keys = (params.keys as string).split('+').map(k => k.trim());
|
||||
}
|
||||
|
||||
await api.updateStep(state.session.active_workflow_id, id, { parameters: params });
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
// Capture
|
||||
onCaptureScreen: async () => {
|
||||
try {
|
||||
const result = await api.captureScreen();
|
||||
currentCapture = result.capture;
|
||||
ui.renderCapture(currentCapture);
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
onSelectAnchor: async (bbox, screenshotBase64) => {
|
||||
try {
|
||||
const state = await api.getState();
|
||||
if (!state.session.selected_step_id) {
|
||||
ui.showError('Sélectionnez une étape d\'abord');
|
||||
return;
|
||||
}
|
||||
await api.selectAnchor(state.session.selected_step_id, bbox, undefined, screenshotBase64);
|
||||
await loadState();
|
||||
ui.showInfo('Ancre créée avec succès');
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
// Execution
|
||||
onStartExecution: async () => {
|
||||
try {
|
||||
await api.startExecution();
|
||||
startPolling();
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
onPauseExecution: async () => {
|
||||
try {
|
||||
await api.pauseExecution();
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
onResumeExecution: async () => {
|
||||
try {
|
||||
await api.resumeExecution();
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
onStopExecution: async () => {
|
||||
try {
|
||||
await api.stopExecution();
|
||||
stopPolling();
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
ui.showError((error as Error).message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Charger l'état initial
|
||||
await loadState();
|
||||
}
|
||||
|
||||
function startPolling(): void {
|
||||
if (pollingInterval) return;
|
||||
pollingInterval = window.setInterval(async () => {
|
||||
try {
|
||||
const status = await api.getExecutionStatus();
|
||||
ui.render({
|
||||
session: status.session,
|
||||
workflow: null, // On garde le workflow actuel
|
||||
execution: status.execution,
|
||||
workflows_list: []
|
||||
} as AppState);
|
||||
|
||||
// Arrêter le polling si l'exécution est terminée
|
||||
if (!status.is_running) {
|
||||
stopPolling();
|
||||
await loadState(); // Recharger l'état complet
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Polling error:', error);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function stopPolling(): void {
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
pollingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Démarrer l'application
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
125
visual_workflow_builder/frontend_v3/src/types.ts
Normal file
125
visual_workflow_builder/frontend_v3/src/types.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Types VWB v3 - Thin Client
|
||||
* Ces types reflètent EXACTEMENT ce que l'API retourne.
|
||||
* L'API est la source de vérité unique.
|
||||
*/
|
||||
|
||||
export interface Session {
|
||||
active_workflow_id: string | null;
|
||||
selected_step_id: string | null;
|
||||
active_execution_id: string | null;
|
||||
has_capture: boolean;
|
||||
}
|
||||
|
||||
export interface BoundingBox {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface VisualAnchor {
|
||||
id: string;
|
||||
image_url: string | null;
|
||||
thumbnail_url: string | null;
|
||||
bounding_box: BoundingBox | null;
|
||||
screen_resolution: { width: number; height: number } | null;
|
||||
description: string | null;
|
||||
confidence_threshold: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Step {
|
||||
id: string;
|
||||
workflow_id: string;
|
||||
action_type: string; // SOURCE DE VERITE UNIQUE
|
||||
order: number;
|
||||
position: { x: number; y: number };
|
||||
parameters: Record<string, unknown>;
|
||||
anchor_id: string | null;
|
||||
anchor: VisualAnchor | null;
|
||||
label: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Workflow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
steps: Step[];
|
||||
step_count: number;
|
||||
}
|
||||
|
||||
export interface WorkflowSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
step_count: number;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ExecutionStepResult {
|
||||
step_id: string;
|
||||
status: 'pending' | 'running' | 'success' | 'error' | 'skipped';
|
||||
started_at: string | null;
|
||||
ended_at: string | null;
|
||||
duration_ms: number | null;
|
||||
error_message: string | null;
|
||||
evidence_url: string | null;
|
||||
output: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface Execution {
|
||||
id: string;
|
||||
workflow_id: string;
|
||||
status: 'pending' | 'running' | 'paused' | 'completed' | 'error' | 'cancelled';
|
||||
started_at: string | null;
|
||||
ended_at: string | null;
|
||||
current_step_index: number;
|
||||
total_steps: number;
|
||||
completed_steps: number;
|
||||
failed_steps: number;
|
||||
error_message: string | null;
|
||||
progress: number;
|
||||
step_results: ExecutionStepResult[];
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
session: Session;
|
||||
workflow: Workflow | null;
|
||||
execution: Execution | null;
|
||||
workflows_list: WorkflowSummary[];
|
||||
}
|
||||
|
||||
export interface Capture {
|
||||
screenshot_base64: string;
|
||||
width: number;
|
||||
height: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// Types d'actions VWB supportés
|
||||
export type ActionType =
|
||||
| 'click_anchor'
|
||||
| 'double_click_anchor'
|
||||
| 'right_click_anchor'
|
||||
| 'type_text'
|
||||
| 'wait_for_anchor'
|
||||
| 'keyboard_shortcut'
|
||||
| 'scroll_to_anchor'
|
||||
| 'extract_text'
|
||||
| 'screenshot_evidence';
|
||||
|
||||
export const ACTION_LABELS: Record<ActionType, string> = {
|
||||
'click_anchor': 'Clic sur ancre',
|
||||
'double_click_anchor': 'Double-clic sur ancre',
|
||||
'right_click_anchor': 'Clic droit sur ancre',
|
||||
'type_text': 'Saisir texte',
|
||||
'wait_for_anchor': 'Attendre ancre',
|
||||
'keyboard_shortcut': 'Raccourci clavier',
|
||||
'scroll_to_anchor': 'Défiler vers ancre',
|
||||
'extract_text': 'Extraire texte',
|
||||
'screenshot_evidence': 'Capture preuve'
|
||||
};
|
||||
872
visual_workflow_builder/frontend_v3/src/ui.ts
Normal file
872
visual_workflow_builder/frontend_v3/src/ui.ts
Normal file
@@ -0,0 +1,872 @@
|
||||
/**
|
||||
* UI VWB v3 - Thin Client
|
||||
* Ce module AFFICHE ce que l'API retourne.
|
||||
* Pas de logique complexe, pas d'état local.
|
||||
*/
|
||||
|
||||
import type { AppState, Step, Workflow, Execution, Capture, ActionType } from './types';
|
||||
import { ACTION_LABELS } from './types';
|
||||
|
||||
// Éléments DOM
|
||||
let $workflowList: HTMLElement;
|
||||
let $workflowName: HTMLElement;
|
||||
let $stepsList: HTMLElement;
|
||||
let $selectedStep: HTMLElement;
|
||||
let $executionStatus: HTMLElement;
|
||||
let $capturePreview: HTMLElement;
|
||||
let $actionButtons: HTMLElement;
|
||||
let $timerCountdown: HTMLElement;
|
||||
let $libraryList: HTMLElement;
|
||||
let $libraryCount: HTMLElement;
|
||||
|
||||
// Bibliothèque de captures (en mémoire session)
|
||||
let captureLibrary: Array<{ id: string; capture: import('./types').Capture; timestamp: Date }> = [];
|
||||
|
||||
// Callbacks pour les actions utilisateur
|
||||
export type UICallbacks = {
|
||||
onCreateWorkflow: (name: string) => void;
|
||||
onSelectWorkflow: (id: string) => void;
|
||||
onDeleteWorkflow: (id: string) => void;
|
||||
onAddStep: (actionType: ActionType, insertAfter?: string) => void;
|
||||
onSelectStep: (id: string) => void;
|
||||
onDeleteStep: (id: string) => void;
|
||||
onUpdateStepParams: (id: string, params: Record<string, unknown>) => void;
|
||||
onReorderSteps: (stepIds: string[]) => void;
|
||||
onCaptureScreen: () => void;
|
||||
onSelectAnchor: (bbox: { x: number; y: number; width: number; height: number }, screenshotBase64?: string) => void;
|
||||
onStartExecution: () => void;
|
||||
onPauseExecution: () => void;
|
||||
onResumeExecution: () => void;
|
||||
onStopExecution: () => void;
|
||||
};
|
||||
|
||||
// Variable pour stocker l'ID de l'étape après laquelle insérer
|
||||
let insertAfterStepId: string | null = null;
|
||||
|
||||
let callbacks: UICallbacks;
|
||||
|
||||
export function initUI(cb: UICallbacks): void {
|
||||
callbacks = cb;
|
||||
|
||||
// Récupérer les éléments DOM
|
||||
$workflowList = document.getElementById('workflow-list')!;
|
||||
$workflowName = document.getElementById('workflow-name')!;
|
||||
$stepsList = document.getElementById('steps-list')!;
|
||||
$selectedStep = document.getElementById('selected-step')!;
|
||||
$executionStatus = document.getElementById('execution-status')!;
|
||||
$capturePreview = document.getElementById('capture-preview')!;
|
||||
$actionButtons = document.getElementById('action-buttons')!;
|
||||
$timerCountdown = document.getElementById('timer-countdown')!;
|
||||
$libraryList = document.getElementById('capture-library-list')!;
|
||||
$libraryCount = document.getElementById('library-count')!;
|
||||
|
||||
// Boutons d'action
|
||||
setupActionButtons();
|
||||
|
||||
// Formulaire nouveau workflow
|
||||
const createForm = document.getElementById('create-workflow-form') as HTMLFormElement;
|
||||
createForm?.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const input = createForm.querySelector('input') as HTMLInputElement;
|
||||
if (input.value.trim()) {
|
||||
callbacks.onCreateWorkflow(input.value.trim());
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Boutons execution
|
||||
document.getElementById('btn-start')?.addEventListener('click', () => callbacks.onStartExecution());
|
||||
document.getElementById('btn-pause')?.addEventListener('click', () => callbacks.onPauseExecution());
|
||||
document.getElementById('btn-resume')?.addEventListener('click', () => callbacks.onResumeExecution());
|
||||
document.getElementById('btn-stop')?.addEventListener('click', () => callbacks.onStopExecution());
|
||||
|
||||
// Capture immédiate
|
||||
document.getElementById('btn-capture')?.addEventListener('click', () => callbacks.onCaptureScreen());
|
||||
|
||||
// Capture avec délai
|
||||
document.getElementById('btn-capture-timer')?.addEventListener('click', () => {
|
||||
const delay = parseInt((document.getElementById('capture-delay') as HTMLSelectElement).value);
|
||||
startCaptureTimer(delay);
|
||||
});
|
||||
|
||||
// Bouton plein écran
|
||||
document.getElementById('btn-fullscreen')?.addEventListener('click', openFullscreenModal);
|
||||
|
||||
// Bouton toggle capture
|
||||
const $toggleBtn = document.getElementById('btn-toggle-capture');
|
||||
const $captureContent = document.getElementById('capture-content');
|
||||
$toggleBtn?.addEventListener('click', () => {
|
||||
const isCollapsed = $captureContent?.classList.toggle('collapsed');
|
||||
if ($toggleBtn) {
|
||||
$toggleBtn.textContent = isCollapsed ? 'Afficher' : 'Réduire';
|
||||
}
|
||||
});
|
||||
|
||||
// Créer la modal plein écran
|
||||
createFullscreenModal();
|
||||
|
||||
// Charger la bibliothèque depuis sessionStorage
|
||||
loadLibraryFromStorage();
|
||||
}
|
||||
|
||||
function startCaptureTimer(seconds: number): void {
|
||||
if (seconds === 0) {
|
||||
callbacks.onCaptureScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
let remaining = seconds;
|
||||
$timerCountdown.classList.remove('hidden');
|
||||
$timerCountdown.textContent = String(remaining);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
remaining--;
|
||||
if (remaining > 0) {
|
||||
$timerCountdown.textContent = String(remaining);
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
$timerCountdown.classList.add('hidden');
|
||||
callbacks.onCaptureScreen();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function loadLibraryFromStorage(): void {
|
||||
try {
|
||||
const stored = sessionStorage.getItem('captureLibrary');
|
||||
if (stored) {
|
||||
captureLibrary = JSON.parse(stored);
|
||||
renderLibrary();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur chargement bibliothèque:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function saveLibraryToStorage(): void {
|
||||
try {
|
||||
sessionStorage.setItem('captureLibrary', JSON.stringify(captureLibrary));
|
||||
} catch (e) {
|
||||
console.error('Erreur sauvegarde bibliothèque:', e);
|
||||
}
|
||||
}
|
||||
|
||||
export function addToLibrary(capture: import('./types').Capture): void {
|
||||
const id = `cap_${Date.now()}`;
|
||||
captureLibrary.unshift({ id, capture, timestamp: new Date() });
|
||||
// Garder max 20 captures
|
||||
if (captureLibrary.length > 20) {
|
||||
captureLibrary = captureLibrary.slice(0, 20);
|
||||
}
|
||||
saveLibraryToStorage();
|
||||
renderLibrary();
|
||||
}
|
||||
|
||||
function renderLibrary(): void {
|
||||
$libraryCount.textContent = `(${captureLibrary.length})`;
|
||||
|
||||
if (captureLibrary.length === 0) {
|
||||
$libraryList.innerHTML = '<p class="empty">Aucune capture</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$libraryList.innerHTML = captureLibrary.map(item => `
|
||||
<div class="library-item" data-id="${item.id}">
|
||||
<img src="data:image/png;base64,${item.capture.screenshot_base64}" alt="Capture" />
|
||||
<button class="delete-lib" data-delete="${item.id}">×</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Événements
|
||||
$libraryList.querySelectorAll('.library-item').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.classList.contains('delete-lib')) {
|
||||
const id = target.dataset.delete!;
|
||||
captureLibrary = captureLibrary.filter(c => c.id !== id);
|
||||
saveLibraryToStorage();
|
||||
renderLibrary();
|
||||
} else {
|
||||
const id = (el as HTMLElement).dataset.id!;
|
||||
const item = captureLibrary.find(c => c.id === id);
|
||||
if (item) {
|
||||
renderCapture(item.capture, false); // false = ne pas ré-ajouter à la bibliothèque
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Modal plein écran
|
||||
let $fullscreenModal: HTMLElement;
|
||||
let currentFullscreenCapture: import('./types').Capture | null = null;
|
||||
|
||||
function createFullscreenModal(): void {
|
||||
$fullscreenModal = document.createElement('div');
|
||||
$fullscreenModal.className = 'fullscreen-modal hidden';
|
||||
$fullscreenModal.innerHTML = `
|
||||
<button class="close-btn">Fermer (Echap)</button>
|
||||
<div class="modal-content">
|
||||
<img id="fullscreen-image" src="" alt="Capture plein écran" />
|
||||
<div id="fullscreen-overlay"></div>
|
||||
</div>
|
||||
<p class="instructions">Dessinez un rectangle pour sélectionner l'ancre visuelle</p>
|
||||
`;
|
||||
document.body.appendChild($fullscreenModal);
|
||||
|
||||
// Fermer avec le bouton ou Echap
|
||||
$fullscreenModal.querySelector('.close-btn')?.addEventListener('click', closeFullscreenModal);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !$fullscreenModal.classList.contains('hidden')) {
|
||||
closeFullscreenModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openFullscreenModal(): void {
|
||||
if (!currentFullscreenCapture) return;
|
||||
|
||||
const img = $fullscreenModal.querySelector('#fullscreen-image') as HTMLImageElement;
|
||||
img.src = `data:image/png;base64,${currentFullscreenCapture.screenshot_base64}`;
|
||||
|
||||
$fullscreenModal.classList.remove('hidden');
|
||||
|
||||
// Setup selection tool pour le mode plein écran
|
||||
setTimeout(() => setupFullscreenSelectionTool(), 100);
|
||||
}
|
||||
|
||||
function closeFullscreenModal(): void {
|
||||
$fullscreenModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
function setupFullscreenSelectionTool(): void {
|
||||
const img = $fullscreenModal.querySelector('#fullscreen-image') as HTMLImageElement;
|
||||
const overlay = $fullscreenModal.querySelector('#fullscreen-overlay') as HTMLElement;
|
||||
|
||||
if (!img || !overlay) return;
|
||||
|
||||
let isSelecting = false;
|
||||
let startX = 0, startY = 0;
|
||||
let imgRect: DOMRect;
|
||||
|
||||
// Supprimer les anciens listeners
|
||||
const newImg = img.cloneNode(true) as HTMLImageElement;
|
||||
img.parentNode?.replaceChild(newImg, img);
|
||||
|
||||
newImg.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
imgRect = newImg.getBoundingClientRect();
|
||||
startX = e.clientX - imgRect.left;
|
||||
startY = e.clientY - imgRect.top;
|
||||
isSelecting = true;
|
||||
|
||||
overlay.style.display = 'block';
|
||||
overlay.style.left = startX + 'px';
|
||||
overlay.style.top = startY + 'px';
|
||||
overlay.style.width = '0';
|
||||
overlay.style.height = '0';
|
||||
});
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (!isSelecting) return;
|
||||
|
||||
const currentX = e.clientX - imgRect.left;
|
||||
const currentY = e.clientY - imgRect.top;
|
||||
|
||||
const width = currentX - startX;
|
||||
const height = currentY - startY;
|
||||
|
||||
overlay.style.left = (width < 0 ? currentX : startX) + 'px';
|
||||
overlay.style.top = (height < 0 ? currentY : startY) + 'px';
|
||||
overlay.style.width = Math.abs(width) + 'px';
|
||||
overlay.style.height = Math.abs(height) + 'px';
|
||||
};
|
||||
|
||||
const onMouseUp = (e: MouseEvent) => {
|
||||
if (!isSelecting) return;
|
||||
isSelecting = false;
|
||||
|
||||
const endX = e.clientX - imgRect.left;
|
||||
const endY = e.clientY - imgRect.top;
|
||||
|
||||
const scaleX = newImg.naturalWidth / newImg.width;
|
||||
const scaleY = newImg.naturalHeight / newImg.height;
|
||||
|
||||
const bbox = {
|
||||
x: Math.round(Math.min(startX, endX) * scaleX),
|
||||
y: Math.round(Math.min(startY, endY) * scaleY),
|
||||
width: Math.round(Math.abs(endX - startX) * scaleX),
|
||||
height: Math.round(Math.abs(endY - startY) * scaleY)
|
||||
};
|
||||
|
||||
if (bbox.width > 10 && bbox.height > 10) {
|
||||
closeFullscreenModal();
|
||||
// Envoyer la capture courante avec la sélection
|
||||
const screenshotBase64 = currentFullscreenCapture?.screenshot_base64;
|
||||
callbacks.onSelectAnchor(bbox, screenshotBase64);
|
||||
} else {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
const AVAILABLE_ACTIONS: ActionType[] = [
|
||||
'click_anchor',
|
||||
'double_click_anchor',
|
||||
'type_text',
|
||||
'wait_for_anchor',
|
||||
'keyboard_shortcut'
|
||||
];
|
||||
|
||||
function setupActionButtons(): void {
|
||||
$actionButtons.innerHTML = AVAILABLE_ACTIONS.map(action => `
|
||||
<button class="action-btn" data-action="${action}">
|
||||
${ACTION_LABELS[action]}
|
||||
</button>
|
||||
`).join('');
|
||||
|
||||
$actionButtons.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const action = target.dataset.action as ActionType;
|
||||
if (action) {
|
||||
callbacks.onAddStep(action);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Menu contextuel pour insérer une étape
|
||||
let $insertMenu: HTMLElement | null = null;
|
||||
|
||||
function showInsertStepMenu(anchorEl: HTMLElement, insertAfterId: string): void {
|
||||
// Supprimer le menu existant
|
||||
hideInsertStepMenu();
|
||||
|
||||
// Créer le menu
|
||||
$insertMenu = document.createElement('div');
|
||||
$insertMenu.className = 'insert-menu';
|
||||
$insertMenu.innerHTML = `
|
||||
<div class="insert-menu-header">Insérer après l'étape:</div>
|
||||
${AVAILABLE_ACTIONS.map(action => `
|
||||
<button class="insert-menu-item" data-action="${action}">
|
||||
${ACTION_LABELS[action]}
|
||||
</button>
|
||||
`).join('')}
|
||||
`;
|
||||
|
||||
// Positionner le menu
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
$insertMenu.style.position = 'fixed';
|
||||
$insertMenu.style.left = `${rect.left}px`;
|
||||
$insertMenu.style.top = `${rect.bottom + 5}px`;
|
||||
|
||||
document.body.appendChild($insertMenu);
|
||||
|
||||
// Event listeners
|
||||
$insertMenu.querySelectorAll('.insert-menu-item').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const action = (btn as HTMLElement).dataset.action as ActionType;
|
||||
callbacks.onAddStep(action, insertAfterId);
|
||||
hideInsertStepMenu();
|
||||
});
|
||||
});
|
||||
|
||||
// Fermer si clic ailleurs
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function handleOutsideClick(e: MouseEvent): void {
|
||||
if ($insertMenu && !$insertMenu.contains(e.target as Node)) {
|
||||
hideInsertStepMenu();
|
||||
}
|
||||
}
|
||||
|
||||
function hideInsertStepMenu(): void {
|
||||
if ($insertMenu) {
|
||||
$insertMenu.remove();
|
||||
$insertMenu = null;
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
}
|
||||
}
|
||||
|
||||
export function render(state: AppState): void {
|
||||
renderWorkflowList(state);
|
||||
renderWorkflow(state.workflow);
|
||||
renderSelectedStep(state);
|
||||
renderExecution(state.execution);
|
||||
}
|
||||
|
||||
function renderWorkflowList(state: AppState): void {
|
||||
if (state.workflows_list.length === 0) {
|
||||
$workflowList.innerHTML = '<p class="empty">Aucun workflow</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$workflowList.innerHTML = state.workflows_list.map(wf => `
|
||||
<div class="workflow-item ${state.session.active_workflow_id === wf.id ? 'active' : ''}"
|
||||
data-id="${wf.id}">
|
||||
<span class="name">${escapeHtml(wf.name)}</span>
|
||||
<span class="count">${wf.step_count} étapes</span>
|
||||
<button class="delete-btn" data-delete="${wf.id}" title="Supprimer">×</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Event listeners
|
||||
$workflowList.querySelectorAll('.workflow-item').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.classList.contains('delete-btn')) {
|
||||
const id = target.dataset.delete!;
|
||||
if (confirm('Supprimer ce workflow ?')) {
|
||||
callbacks.onDeleteWorkflow(id);
|
||||
}
|
||||
} else {
|
||||
const id = (el as HTMLElement).dataset.id!;
|
||||
callbacks.onSelectWorkflow(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Variables pour le drag & drop
|
||||
let draggedStepId: string | null = null;
|
||||
let currentWorkflowSteps: string[] = [];
|
||||
|
||||
function renderWorkflow(workflow: Workflow | null): void {
|
||||
if (!workflow) {
|
||||
$workflowName.textContent = 'Aucun workflow sélectionné';
|
||||
$stepsList.innerHTML = '<p class="empty">Sélectionnez ou créez un workflow</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$workflowName.textContent = workflow.name;
|
||||
|
||||
if (workflow.steps.length === 0) {
|
||||
$stepsList.innerHTML = '<p class="empty">Ajoutez des étapes avec les boutons ci-dessus</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Stocker l'ordre actuel des étapes
|
||||
currentWorkflowSteps = workflow.steps.map(s => s.id);
|
||||
|
||||
// Générer HTML avec drag & drop
|
||||
let html = '';
|
||||
workflow.steps.forEach((step, index) => {
|
||||
html += `
|
||||
<div class="step-item" data-id="${step.id}" draggable="true">
|
||||
<span class="drag-handle" title="Glisser pour réorganiser">⋮⋮</span>
|
||||
<span class="order">${index + 1}</span>
|
||||
<span class="type">${ACTION_LABELS[step.action_type as ActionType] || step.action_type}</span>
|
||||
<span class="label">${escapeHtml(step.label)}</span>
|
||||
${step.anchor ? '<span class="anchor-badge">Ancre</span>' : ''}
|
||||
<button class="delete-btn" data-delete="${step.id}" title="Supprimer">×</button>
|
||||
</div>
|
||||
<div class="insert-step-btn" data-insert-after="${step.id}" title="Insérer une étape ici">
|
||||
<span class="insert-icon">+</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$stepsList.innerHTML = html;
|
||||
|
||||
// Event listeners pour les étapes (clic et drag & drop)
|
||||
$stepsList.querySelectorAll('.step-item').forEach(el => {
|
||||
const stepEl = el as HTMLElement;
|
||||
|
||||
// Clic pour sélectionner ou supprimer
|
||||
stepEl.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.classList.contains('delete-btn')) {
|
||||
const id = target.dataset.delete!;
|
||||
callbacks.onDeleteStep(id);
|
||||
} else if (!target.classList.contains('drag-handle')) {
|
||||
const id = stepEl.dataset.id!;
|
||||
callbacks.onSelectStep(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Drag & Drop
|
||||
stepEl.addEventListener('dragstart', (e) => {
|
||||
draggedStepId = stepEl.dataset.id!;
|
||||
stepEl.classList.add('dragging');
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
});
|
||||
|
||||
stepEl.addEventListener('dragend', () => {
|
||||
stepEl.classList.remove('dragging');
|
||||
draggedStepId = null;
|
||||
// Supprimer tous les indicateurs
|
||||
$stepsList.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
||||
});
|
||||
|
||||
stepEl.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
if (!draggedStepId || draggedStepId === stepEl.dataset.id) return;
|
||||
|
||||
// Supprimer les autres indicateurs
|
||||
$stepsList.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
||||
|
||||
// Ajouter l'indicateur sur cet élément
|
||||
stepEl.classList.add('drag-over');
|
||||
});
|
||||
|
||||
stepEl.addEventListener('dragleave', () => {
|
||||
stepEl.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
stepEl.addEventListener('drop', (e) => {
|
||||
console.log('🎯 DROP EVENT TRIGGERED on', stepEl.dataset.id);
|
||||
e.preventDefault();
|
||||
stepEl.classList.remove('drag-over');
|
||||
|
||||
if (!draggedStepId || draggedStepId === stepEl.dataset.id) {
|
||||
console.log('Drop ignoré: draggedStepId=', draggedStepId, 'targetId=', stepEl.dataset.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = stepEl.dataset.id!;
|
||||
|
||||
console.log('Drop event: draggedStepId=', draggedStepId, 'targetId=', targetId);
|
||||
console.log('currentWorkflowSteps avant:', [...currentWorkflowSteps]);
|
||||
|
||||
// Calculer le nouvel ordre
|
||||
const newOrder = [...currentWorkflowSteps];
|
||||
const draggedIndex = newOrder.indexOf(draggedStepId);
|
||||
const targetIndex = newOrder.indexOf(targetId);
|
||||
|
||||
console.log('draggedIndex=', draggedIndex, 'targetIndex=', targetIndex);
|
||||
|
||||
if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) {
|
||||
// Retirer l'élément glissé
|
||||
newOrder.splice(draggedIndex, 1);
|
||||
// L'insérer à la position cible
|
||||
// Quand on déplace vers le bas: on veut aller APRÈS l'élément cible
|
||||
// Quand on déplace vers le haut: on veut prendre la place de l'élément cible
|
||||
// Dans les deux cas après suppression, targetIndex est la bonne position
|
||||
newOrder.splice(targetIndex, 0, draggedStepId);
|
||||
|
||||
console.log('newOrder après réorganisation:', newOrder);
|
||||
|
||||
// Appeler le callback pour réordonner
|
||||
try {
|
||||
console.log('Appel de callbacks.onReorderSteps...');
|
||||
callbacks.onReorderSteps(newOrder);
|
||||
console.log('Callback appelé avec succès');
|
||||
} catch (err) {
|
||||
console.error('Erreur lors du callback onReorderSteps:', err);
|
||||
}
|
||||
} else {
|
||||
console.log('Pas de changement nécessaire');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Event listeners pour les boutons d'insertion
|
||||
$stepsList.querySelectorAll('.insert-step-btn').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const insertAfter = (el as HTMLElement).dataset.insertAfter!;
|
||||
showInsertStepMenu(el as HTMLElement, insertAfter);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderSelectedStep(state: AppState): void {
|
||||
const selectedId = state.session.selected_step_id;
|
||||
|
||||
// Highlight selected step
|
||||
$stepsList.querySelectorAll('.step-item').forEach(el => {
|
||||
el.classList.toggle('selected', (el as HTMLElement).dataset.id === selectedId);
|
||||
});
|
||||
|
||||
if (!selectedId || !state.workflow) {
|
||||
$selectedStep.innerHTML = '<p class="empty">Sélectionnez une étape</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const step = state.workflow.steps.find(s => s.id === selectedId);
|
||||
if (!step) {
|
||||
$selectedStep.innerHTML = '<p class="empty">Étape non trouvée</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$selectedStep.innerHTML = `
|
||||
<div class="step-details">
|
||||
<h4>${ACTION_LABELS[step.action_type as ActionType] || step.action_type}</h4>
|
||||
<p><strong>ID:</strong> ${step.id}</p>
|
||||
<p><strong>Type:</strong> ${step.action_type}</p>
|
||||
${renderStepParams(step)}
|
||||
${step.anchor ? renderAnchor(step) : renderNeedAnchor(step)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event listener pour les paramètres
|
||||
const paramsForm = $selectedStep.querySelector('.params-form');
|
||||
paramsForm?.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
const params: Record<string, unknown> = {};
|
||||
formData.forEach((value, key) => {
|
||||
params[key] = value;
|
||||
});
|
||||
callbacks.onUpdateStepParams(step.id, params);
|
||||
});
|
||||
}
|
||||
|
||||
function renderStepParams(step: Step): string {
|
||||
const actionType = step.action_type as ActionType;
|
||||
|
||||
if (actionType === 'type_text') {
|
||||
return `
|
||||
<form class="params-form">
|
||||
<label>
|
||||
Texte à saisir:
|
||||
<input type="text" name="text" value="${escapeHtml(String(step.parameters.text || ''))}" />
|
||||
</label>
|
||||
<button type="submit">Mettre à jour</button>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
if (actionType === 'wait_for_anchor') {
|
||||
return `
|
||||
<form class="params-form">
|
||||
<label>
|
||||
Timeout (ms):
|
||||
<input type="number" name="timeout_ms" value="${step.parameters.timeout_ms || 5000}" />
|
||||
</label>
|
||||
<button type="submit">Mettre à jour</button>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
if (actionType === 'keyboard_shortcut') {
|
||||
return `
|
||||
<form class="params-form">
|
||||
<label>
|
||||
Touches (séparées par +):
|
||||
<input type="text" name="keys" value="${escapeHtml((step.parameters.keys as string[] || []).join('+'))}" />
|
||||
</label>
|
||||
<button type="submit">Mettre à jour</button>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
return '<p class="no-params">Pas de paramètres supplémentaires</p>';
|
||||
}
|
||||
|
||||
function renderAnchor(step: Step): string {
|
||||
const anchor = step.anchor!;
|
||||
return `
|
||||
<div class="anchor-info">
|
||||
<h5>Ancre visuelle</h5>
|
||||
${anchor.thumbnail_url ? `<img src="${anchor.thumbnail_url}" alt="Ancre" class="anchor-thumb" />` : ''}
|
||||
<p>${anchor.description || 'Aucune description'}</p>
|
||||
<p class="coords">Position: (${anchor.bounding_box?.x}, ${anchor.bounding_box?.y})</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderNeedAnchor(step: Step): string {
|
||||
const needsAnchor = ['click_anchor', 'double_click_anchor', 'right_click_anchor', 'wait_for_anchor', 'scroll_to_anchor']
|
||||
.includes(step.action_type);
|
||||
|
||||
if (!needsAnchor) return '';
|
||||
|
||||
return `
|
||||
<div class="need-anchor">
|
||||
<p class="warning">Cette étape nécessite une ancre visuelle</p>
|
||||
<button id="btn-select-anchor">Capturer et sélectionner</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderExecution(execution: Execution | null): void {
|
||||
if (!execution) {
|
||||
$executionStatus.innerHTML = `
|
||||
<p>Prêt à exécuter</p>
|
||||
<div class="exec-buttons">
|
||||
<button id="btn-start" class="primary">Démarrer</button>
|
||||
</div>
|
||||
`;
|
||||
// Attacher l'événement au nouveau bouton
|
||||
document.getElementById('btn-start')?.addEventListener('click', () => {
|
||||
console.log('Démarrer cliqué');
|
||||
callbacks.onStartExecution();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
'pending': 'En attente',
|
||||
'running': 'En cours',
|
||||
'paused': 'En pause',
|
||||
'completed': 'Terminé',
|
||||
'error': 'Erreur',
|
||||
'cancelled': 'Annulé'
|
||||
};
|
||||
|
||||
$executionStatus.innerHTML = `
|
||||
<div class="exec-info">
|
||||
<p class="status status-${execution.status}">${statusLabels[execution.status]}</p>
|
||||
<div class="progress-bar">
|
||||
<div class="progress" style="width: ${execution.progress}%"></div>
|
||||
</div>
|
||||
<p class="progress-text">${execution.completed_steps}/${execution.total_steps} étapes</p>
|
||||
${execution.error_message ? `<p class="error">${escapeHtml(execution.error_message)}</p>` : ''}
|
||||
</div>
|
||||
<div class="exec-buttons">
|
||||
${execution.status === 'running' ? '<button id="btn-pause">Pause</button>' : ''}
|
||||
${execution.status === 'paused' ? '<button id="btn-resume">Reprendre</button>' : ''}
|
||||
${['running', 'paused'].includes(execution.status) ? '<button id="btn-stop" class="danger">Arrêter</button>' : ''}
|
||||
${['completed', 'error', 'cancelled'].includes(execution.status) ? '<button id="btn-start" class="primary">Relancer</button>' : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Re-attacher les événements
|
||||
document.getElementById('btn-start')?.addEventListener('click', () => callbacks.onStartExecution());
|
||||
document.getElementById('btn-pause')?.addEventListener('click', () => callbacks.onPauseExecution());
|
||||
document.getElementById('btn-resume')?.addEventListener('click', () => callbacks.onResumeExecution());
|
||||
document.getElementById('btn-stop')?.addEventListener('click', () => callbacks.onStopExecution());
|
||||
}
|
||||
|
||||
export function renderCapture(capture: Capture, addToLib: boolean = true): void {
|
||||
currentFullscreenCapture = capture;
|
||||
|
||||
$capturePreview.innerHTML = `
|
||||
<div class="capture-container">
|
||||
<img src="data:image/png;base64,${capture.screenshot_base64}"
|
||||
alt="Capture d'écran"
|
||||
id="capture-image" />
|
||||
<div id="selection-overlay"></div>
|
||||
</div>
|
||||
<p class="capture-info">${capture.width}x${capture.height} - Dessinez un rectangle ou utilisez le mode plein écran</p>
|
||||
`;
|
||||
|
||||
// Afficher le bouton plein écran
|
||||
const $btnFullscreen = document.getElementById('btn-fullscreen');
|
||||
if ($btnFullscreen) {
|
||||
$btnFullscreen.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Ajouter à la bibliothèque (sauf si on charge depuis la bibliothèque)
|
||||
if (addToLib) {
|
||||
addToLibrary(capture);
|
||||
}
|
||||
|
||||
// Attendre que l'image soit chargée
|
||||
const img = document.getElementById('capture-image') as HTMLImageElement;
|
||||
if (img) {
|
||||
img.onload = () => {
|
||||
console.log('Image chargée, setup selection tool');
|
||||
setupSelectionTool();
|
||||
};
|
||||
// Si déjà chargée (cache)
|
||||
if (img.complete) {
|
||||
console.log('Image déjà en cache');
|
||||
setupSelectionTool();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupSelectionTool(): void {
|
||||
const img = document.getElementById('capture-image') as HTMLImageElement;
|
||||
const overlay = document.getElementById('selection-overlay')!;
|
||||
const container = img?.parentElement;
|
||||
|
||||
if (!img || !overlay || !container) {
|
||||
console.error('Selection tool: éléments manquants', { img: !!img, overlay: !!overlay, container: !!container });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Selection tool initialisé');
|
||||
|
||||
let isSelecting = false;
|
||||
let startX = 0, startY = 0;
|
||||
let imgRect: DOMRect;
|
||||
|
||||
// Mousedown sur l'image pour démarrer la sélection
|
||||
img.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
imgRect = img.getBoundingClientRect();
|
||||
startX = e.clientX - imgRect.left;
|
||||
startY = e.clientY - imgRect.top;
|
||||
isSelecting = true;
|
||||
|
||||
overlay.style.display = 'block';
|
||||
overlay.style.left = startX + 'px';
|
||||
overlay.style.top = startY + 'px';
|
||||
overlay.style.width = '0';
|
||||
overlay.style.height = '0';
|
||||
|
||||
console.log('Sélection démarrée à', startX, startY);
|
||||
});
|
||||
|
||||
// Mousemove sur le document pour suivre même en dehors de l'image
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isSelecting) return;
|
||||
|
||||
const currentX = e.clientX - imgRect.left;
|
||||
const currentY = e.clientY - imgRect.top;
|
||||
|
||||
const width = currentX - startX;
|
||||
const height = currentY - startY;
|
||||
|
||||
overlay.style.left = (width < 0 ? currentX : startX) + 'px';
|
||||
overlay.style.top = (height < 0 ? currentY : startY) + 'px';
|
||||
overlay.style.width = Math.abs(width) + 'px';
|
||||
overlay.style.height = Math.abs(height) + 'px';
|
||||
});
|
||||
|
||||
// Mouseup sur le document pour terminer
|
||||
document.addEventListener('mouseup', (e) => {
|
||||
if (!isSelecting) return;
|
||||
isSelecting = false;
|
||||
|
||||
const endX = e.clientX - imgRect.left;
|
||||
const endY = e.clientY - imgRect.top;
|
||||
|
||||
// Calculer les coordonnées réelles (ratio image affichée / taille réelle)
|
||||
const scaleX = img.naturalWidth / img.width;
|
||||
const scaleY = img.naturalHeight / img.height;
|
||||
|
||||
const bbox = {
|
||||
x: Math.round(Math.min(startX, endX) * scaleX),
|
||||
y: Math.round(Math.min(startY, endY) * scaleY),
|
||||
width: Math.round(Math.abs(endX - startX) * scaleX),
|
||||
height: Math.round(Math.abs(endY - startY) * scaleY)
|
||||
};
|
||||
|
||||
console.log('Sélection terminée, bbox:', bbox);
|
||||
|
||||
if (bbox.width > 10 && bbox.height > 10) {
|
||||
console.log('Envoi de la sélection au backend...');
|
||||
// Envoyer la capture courante avec la sélection
|
||||
const screenshotBase64 = currentFullscreenCapture?.screenshot_base64;
|
||||
callbacks.onSelectAnchor(bbox, screenshotBase64);
|
||||
} else {
|
||||
console.log('Sélection trop petite, ignorée');
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
export function showError(message: string): void {
|
||||
alert(`Erreur: ${message}`);
|
||||
}
|
||||
|
||||
export function showInfo(message: string): void {
|
||||
console.log(`Info: ${message}`);
|
||||
}
|
||||
830
visual_workflow_builder/frontend_v3/styles.css
Normal file
830
visual_workflow_builder/frontend_v3/styles.css
Normal file
@@ -0,0 +1,830 @@
|
||||
/* VWB v3 - Thin Client Styles */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #eee;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #16213e;
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
border-bottom: 1px solid #0f3460;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: #e94560;
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr 320px 300px;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
height: calc(100vh - 60px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid #0f3460;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Formulaire création workflow */
|
||||
#create-workflow-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#create-workflow-form input {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #0f3460;
|
||||
border-radius: 4px;
|
||||
background: #1a1a2e;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
#create-workflow-form button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #e94560;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Liste workflows */
|
||||
#workflow-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.workflow-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: #1a1a2e;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.workflow-item:hover {
|
||||
background: #0f3460;
|
||||
}
|
||||
|
||||
.workflow-item.active {
|
||||
background: #0f3460;
|
||||
border-left: 3px solid #e94560;
|
||||
}
|
||||
|
||||
.workflow-item .name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workflow-item .count {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.workflow-item .delete-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.workflow-item .delete-btn:hover {
|
||||
color: #e94560;
|
||||
}
|
||||
|
||||
/* Zone centrale */
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Section capture */
|
||||
.capture-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.capture-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.capture-header h4 {
|
||||
margin: 0;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #0f3460;
|
||||
color: #94a3b8;
|
||||
border: 1px solid #1a5276;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: #1a5276;
|
||||
}
|
||||
|
||||
#capture-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#capture-content.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background: #16213e;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #0f3460;
|
||||
}
|
||||
|
||||
.toolbar h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#action-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #0f3460;
|
||||
color: white;
|
||||
border: 1px solid #1a5276;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #1a5276;
|
||||
border-color: #2471a3;
|
||||
}
|
||||
|
||||
/* Liste des étapes */
|
||||
.steps-container {
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid #0f3460;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.steps-container h4 {
|
||||
margin-bottom: 1rem;
|
||||
color: #94a3b8;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #16213e;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#steps-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: #1a1a2e;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.step-item:hover {
|
||||
background: #0f3460;
|
||||
}
|
||||
|
||||
.step-item.selected {
|
||||
border-color: #e94560;
|
||||
background: #0f3460;
|
||||
}
|
||||
|
||||
.step-item .order {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #0f3460;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-item .type {
|
||||
font-weight: 500;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.step-item .label {
|
||||
flex: 1;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.step-item .anchor-badge {
|
||||
font-size: 0.625rem;
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
/* Drag & Drop */
|
||||
.step-item .drag-handle {
|
||||
cursor: grab;
|
||||
color: #64748b;
|
||||
font-size: 1rem;
|
||||
padding: 0 0.25rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.step-item .drag-handle:hover {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.step-item .drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.step-item.dragging {
|
||||
opacity: 0.5;
|
||||
border: 2px dashed #3498db;
|
||||
}
|
||||
|
||||
.step-item.drag-over {
|
||||
border-top: 3px solid #e94560;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.step-item[draggable="true"] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Bouton d'insertion entre les étapes */
|
||||
.insert-step-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 20px;
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.insert-step-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.insert-step-btn .insert-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.insert-step-btn:hover .insert-icon {
|
||||
background: #2980b9;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Menu contextuel d'insertion */
|
||||
.insert-menu {
|
||||
background: #16213e;
|
||||
border: 1px solid #0f3460;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 100;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.insert-menu-header {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.insert-menu-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #eee;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.insert-menu-item:hover {
|
||||
background: #0f3460;
|
||||
}
|
||||
|
||||
/* Zone capture */
|
||||
.capture-zone {
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid #0f3460;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.capture-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.capture-btn:hover {
|
||||
background: #2ecc71;
|
||||
}
|
||||
|
||||
#capture-preview {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
#capture-preview .capture-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#capture-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 250px;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
cursor: crosshair;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
#selection-overlay {
|
||||
position: absolute;
|
||||
border: 2px dashed #e94560;
|
||||
background: rgba(233, 69, 96, 0.3);
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.capture-info {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.capture-hint {
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.capture-zone h4 {
|
||||
margin-bottom: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.capture-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.timer-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.timer-control label {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.timer-control select {
|
||||
padding: 0.5rem;
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #0f3460;
|
||||
border-radius: 4px;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.capture-btn.secondary {
|
||||
background: #3498db;
|
||||
}
|
||||
|
||||
.capture-btn.secondary:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.timer-countdown {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: #e94560;
|
||||
padding: 2rem;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.timer-countdown.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.fullscreen-btn {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #0f3460;
|
||||
color: white;
|
||||
border: 1px solid #1a5276;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fullscreen-btn:hover {
|
||||
background: #1a5276;
|
||||
}
|
||||
|
||||
.fullscreen-btn.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Bibliothèque de captures */
|
||||
.capture-library {
|
||||
background: #1a1a2e;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem;
|
||||
margin-top: 0.75rem;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.capture-library h5 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#library-count {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
#capture-library-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.library-item {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.library-item:hover {
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.library-item.selected {
|
||||
border-color: #e94560;
|
||||
}
|
||||
|
||||
.library-item img {
|
||||
width: 60px;
|
||||
height: 45px;
|
||||
object-fit: cover;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.library-item .delete-lib {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.library-item:hover .delete-lib {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Modal plein écran */
|
||||
.fullscreen-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fullscreen-modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fullscreen-modal .close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.fullscreen-modal .modal-content {
|
||||
position: relative;
|
||||
max-width: 95vw;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.fullscreen-modal img {
|
||||
max-width: 100%;
|
||||
max-height: 85vh;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.fullscreen-modal #fullscreen-overlay {
|
||||
position: absolute;
|
||||
border: 3px dashed #e94560;
|
||||
background: rgba(233, 69, 96, 0.3);
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fullscreen-modal .instructions {
|
||||
color: #94a3b8;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Panneau étape sélectionnée */
|
||||
#selected-step {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.step-details h4 {
|
||||
margin-bottom: 0.75rem;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.step-details p {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.params-form {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.params-form label {
|
||||
display: block;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.params-form input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #0f3460;
|
||||
border-radius: 4px;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.params-form button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.anchor-info {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: #1a1a2e;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.anchor-info h5 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.anchor-thumb {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.need-anchor {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(231, 76, 60, 0.2);
|
||||
border: 1px solid #e74c3c;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #e74c3c;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Panneau exécution */
|
||||
#execution-status {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.exec-info {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.status-running { color: #3498db; }
|
||||
.status-paused { color: #f39c12; }
|
||||
.status-completed { color: #27ae60; }
|
||||
.status-error { color: #e74c3c; }
|
||||
.status-cancelled { color: #95a5a6; }
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: #1a1a2e;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background: #3498db;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.exec-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.exec-buttons button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.exec-buttons .primary {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.exec-buttons .danger {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Utilitaires */
|
||||
.empty {
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
14
visual_workflow_builder/frontend_v3/tsconfig.json
Normal file
14
visual_workflow_builder/frontend_v3/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
13
visual_workflow_builder/frontend_v3/vite.config.ts
Normal file
13
visual_workflow_builder/frontend_v3/vite.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3001,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5001',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user