feat: chat unifié, GestureCatalog, Copilot, Léa UI, extraction données, vérification replay
Refonte majeure du système Agent Chat et ajout de nombreux modules : - Chat unifié : suppression du dual Workflows/Agent Libre, tout passe par /api/chat avec résolution en 3 niveaux (workflow → geste → "montre-moi") - GestureCatalog : 38 raccourcis clavier universels Windows avec matching sémantique, substitution automatique dans les replays, et endpoint /api/gestures - Mode Copilot : exécution pas-à-pas des workflows avec validation humaine via WebSocket (approve/skip/abort) avant chaque action - Léa UI (agent_v0/lea_ui/) : interface PyQt5 pour Windows avec overlay transparent pour feedback visuel pendant le replay - Data Extraction (core/extraction/) : moteur d'extraction visuelle de données (OCR + VLM → SQLite), avec schémas YAML et export CSV/Excel - ReplayVerifier (agent_v0/server_v1/) : vérification post-action par comparaison de screenshots, avec logique de retry (max 3) - IntentParser durci : meilleur fallback regex, type GREETING, patterns améliorés - Dashboard : nouvelles pages gestures, streaming, extractions - Tests : 63 tests GestureCatalog, 47 tests extraction, corrections tests existants - Dépréciation : /api/agent/plan et /api/agent/execute retournent HTTP 410, suppression du code hardcodé _plan_to_replay_actions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ import CaptureLibrary from './components/CaptureLibrary';
|
||||
import SelfHealingDialog from './components/SelfHealingDialog';
|
||||
import ConfidenceDashboard from './components/ConfidenceDashboard';
|
||||
import WorkflowValidation from './components/WorkflowValidation';
|
||||
import ReviewModal from './components/ReviewModal';
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
step: StepNode,
|
||||
@@ -48,6 +49,8 @@ function App() {
|
||||
const [variables, setVariables] = useState<Variable[]>([]);
|
||||
const [runtimeVariables, setRuntimeVariables] = useState<Record<string, unknown>>({});
|
||||
const [showWorkflowManager, setShowWorkflowManager] = useState(false);
|
||||
const [showReviewModal, setShowReviewModal] = useState(false);
|
||||
const [pendingReviewCount, setPendingReviewCount] = useState(0);
|
||||
const [currentCapture, setCurrentCapture] = useState<Capture | null>(null);
|
||||
|
||||
// React Flow instance pour screenToFlowPosition
|
||||
@@ -70,6 +73,11 @@ function App() {
|
||||
state.workflow?.steps || [],
|
||||
state.workflow?.id
|
||||
);
|
||||
// Compter les workflows en attente de review
|
||||
const pending = (state.workflows_list || []).filter(
|
||||
(wf) => wf.review_status === 'pending_review' || wf.review_status === 'needs_edit'
|
||||
).length;
|
||||
setPendingReviewCount(pending);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
}
|
||||
@@ -409,6 +417,17 @@ function App() {
|
||||
onOpenManager={() => setShowWorkflowManager(true)}
|
||||
onRename={handleRenameWorkflow}
|
||||
/>
|
||||
<button
|
||||
className="review-header-btn"
|
||||
onClick={() => setShowReviewModal(true)}
|
||||
title="Workflows en attente de validation"
|
||||
>
|
||||
<span className="review-header-icon">📋</span>
|
||||
<span className="review-header-text">Review</span>
|
||||
{pendingReviewCount > 0 && (
|
||||
<span className="review-header-count">{pendingReviewCount}</span>
|
||||
)}
|
||||
</button>
|
||||
<WorkflowValidation
|
||||
workflowId={appState?.session.active_workflow_id}
|
||||
/>
|
||||
@@ -526,6 +545,18 @@ function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Review Modal */}
|
||||
{showReviewModal && (
|
||||
<ReviewModal
|
||||
onClose={() => setShowReviewModal(false)}
|
||||
onOpenWorkflow={(id) => {
|
||||
handleSelectWorkflow(id);
|
||||
setShowReviewModal(false);
|
||||
}}
|
||||
onRefresh={loadState}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Self-Healing Dialog */}
|
||||
<SelfHealingDialog
|
||||
isOpen={showSelfHealing}
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import * as api from '../services/api';
|
||||
import type { Step, ReviewStatus } from '../types';
|
||||
|
||||
interface ReviewWorkflow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
step_count: number;
|
||||
source: string;
|
||||
review_status: ReviewStatus;
|
||||
review_feedback: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ReviewInfo {
|
||||
source: string;
|
||||
review_status: ReviewStatus;
|
||||
review_feedback: string | null;
|
||||
reviewed_at: string | null;
|
||||
step_count: number;
|
||||
steps_with_anchors: number;
|
||||
steps_without_anchors: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onOpenWorkflow: (id: string) => void;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
type ViewMode = 'list' | 'detail';
|
||||
|
||||
export default function ReviewModal({ onClose, onOpenWorkflow, onRefresh }: Props) {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [pendingWorkflows, setPendingWorkflows] = useState<ReviewWorkflow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Detail view state
|
||||
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(null);
|
||||
const [reviewInfo, setReviewInfo] = useState<ReviewInfo | null>(null);
|
||||
const [workflowSteps, setWorkflowSteps] = useState<Step[]>([]);
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitResult, setSubmitResult] = useState<{ status: string; message: string } | null>(null);
|
||||
|
||||
// Charger les workflows en attente
|
||||
useEffect(() => {
|
||||
loadPendingWorkflows();
|
||||
}, []);
|
||||
|
||||
const loadPendingWorkflows = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await api.getPendingReview();
|
||||
setPendingWorkflows(data.workflows);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadReviewDetail = async (workflowId: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSubmitResult(null);
|
||||
setFeedback('');
|
||||
try {
|
||||
const data = await api.getReviewData(workflowId);
|
||||
setSelectedWorkflowId(workflowId);
|
||||
setReviewInfo(data.review_info);
|
||||
setWorkflowSteps(data.workflow.steps || []);
|
||||
setViewMode('detail');
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitReview = async (status: 'approved' | 'rejected' | 'needs_edit') => {
|
||||
if (!selectedWorkflowId) return;
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await api.submitReview(selectedWorkflowId, status, feedback);
|
||||
setSubmitResult({ status: result.review_status, message: result.message });
|
||||
|
||||
// Si needs_edit, proposer d'ouvrir dans le VWB
|
||||
if (status === 'needs_edit') {
|
||||
// Laisser l'utilisateur voir le message puis ouvrir
|
||||
}
|
||||
|
||||
// Rafraichir la liste
|
||||
onRefresh();
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackToList = () => {
|
||||
setViewMode('list');
|
||||
setSelectedWorkflowId(null);
|
||||
setReviewInfo(null);
|
||||
setWorkflowSteps([]);
|
||||
setSubmitResult(null);
|
||||
setFeedback('');
|
||||
loadPendingWorkflows();
|
||||
};
|
||||
|
||||
const handleOpenInEditor = () => {
|
||||
if (selectedWorkflowId) {
|
||||
onOpenWorkflow(selectedWorkflowId);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const selectedWf = pendingWorkflows.find(w => w.id === selectedWorkflowId);
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="review-modal" onClick={e => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
<div className="modal-header">
|
||||
<div className="review-modal-title">
|
||||
{viewMode === 'list' ? (
|
||||
<h2>Workflows en attente de validation</h2>
|
||||
) : (
|
||||
<>
|
||||
<button className="back-btn" onClick={handleBackToList}>
|
||||
←
|
||||
</button>
|
||||
<h2>Review : {selectedWf?.name || '...'}</h2>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<button className="close-btn" onClick={onClose}>×</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
{error && (
|
||||
<div className="review-error">
|
||||
{error}
|
||||
<button onClick={() => setError(null)}>×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="review-loading">Chargement...</div>
|
||||
)}
|
||||
|
||||
{/* === MODE LISTE === */}
|
||||
{viewMode === 'list' && !loading && (
|
||||
<>
|
||||
{pendingWorkflows.length === 0 ? (
|
||||
<div className="review-empty">
|
||||
<div className="review-empty-icon">✓</div>
|
||||
<p>Aucun workflow en attente de validation</p>
|
||||
<p className="review-empty-sub">
|
||||
Les workflows importes depuis le streaming apparaitront ici.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="review-list">
|
||||
{pendingWorkflows.map(wf => (
|
||||
<div key={wf.id} className="review-list-item" onClick={() => loadReviewDetail(wf.id)}>
|
||||
<div className="review-item-header">
|
||||
<span className="review-item-name">{wf.name}</span>
|
||||
<span className={`review-badge ${wf.review_status}`}>
|
||||
{wf.review_status === 'pending_review' ? 'En attente' : 'A modifier'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="review-item-meta">
|
||||
<span>{wf.step_count} etape{wf.step_count > 1 ? 's' : ''}</span>
|
||||
<span className="review-item-sep">•</span>
|
||||
<span>Importe le {new Date(wf.created_at).toLocaleDateString('fr-FR')}</span>
|
||||
</div>
|
||||
{wf.description && (
|
||||
<div className="review-item-desc">{wf.description}</div>
|
||||
)}
|
||||
{wf.review_feedback && (
|
||||
<div className="review-item-feedback">
|
||||
Feedback: {wf.review_feedback}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* === MODE DETAIL === */}
|
||||
{viewMode === 'detail' && !loading && reviewInfo && (
|
||||
<div className="review-detail">
|
||||
{/* Info du workflow */}
|
||||
<div className="review-detail-info">
|
||||
<div className="review-info-grid">
|
||||
<div className="review-info-item">
|
||||
<span className="review-info-label">Source</span>
|
||||
<span className="review-info-value">
|
||||
{reviewInfo.source === 'graph_to_visual_converter' ? 'Streaming / Apprentissage auto' : reviewInfo.source}
|
||||
</span>
|
||||
</div>
|
||||
<div className="review-info-item">
|
||||
<span className="review-info-label">Etapes</span>
|
||||
<span className="review-info-value">{reviewInfo.step_count}</span>
|
||||
</div>
|
||||
<div className="review-info-item">
|
||||
<span className="review-info-label">Avec ancre visuelle</span>
|
||||
<span className="review-info-value">{reviewInfo.steps_with_anchors}</span>
|
||||
</div>
|
||||
<div className="review-info-item">
|
||||
<span className="review-info-label">Sans ancre visuelle</span>
|
||||
<span className="review-info-value review-info-warning">
|
||||
{reviewInfo.steps_without_anchors}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Liste des etapes step-by-step */}
|
||||
<div className="review-steps">
|
||||
<h3>Etapes du workflow</h3>
|
||||
{workflowSteps.length === 0 ? (
|
||||
<p className="review-steps-empty">Aucune etape</p>
|
||||
) : (
|
||||
<div className="review-steps-list">
|
||||
{workflowSteps.map((step, idx) => (
|
||||
<div key={step.id} className="review-step-item">
|
||||
<div className="review-step-number">{idx + 1}</div>
|
||||
<div className="review-step-content">
|
||||
<div className="review-step-header">
|
||||
<span className="review-step-type">{step.action_type}</span>
|
||||
<span className="review-step-label">{step.label}</span>
|
||||
</div>
|
||||
{step.parameters && Object.keys(step.parameters).length > 0 && (
|
||||
<div className="review-step-params">
|
||||
{Object.entries(step.parameters).map(([key, value]) => (
|
||||
<span key={key} className="review-step-param">
|
||||
{key}: {String(value)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{step.anchor ? (
|
||||
<div className="review-step-anchor">
|
||||
{step.anchor.thumbnail_url && (
|
||||
<img
|
||||
src={step.anchor.thumbnail_url}
|
||||
alt="Ancre visuelle"
|
||||
className="review-step-thumbnail"
|
||||
/>
|
||||
)}
|
||||
<span className="review-step-anchor-ok">Ancre visuelle configuree</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="review-step-no-anchor">
|
||||
Pas d'ancre visuelle
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Zone de decision */}
|
||||
{!submitResult && (
|
||||
<div className="review-decision">
|
||||
<h3>Decision</h3>
|
||||
<div className="review-feedback-field">
|
||||
<label>Commentaire (optionnel)</label>
|
||||
<textarea
|
||||
value={feedback}
|
||||
onChange={(e) => setFeedback(e.target.value)}
|
||||
placeholder="Raison de la decision, suggestions..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="review-actions">
|
||||
<button
|
||||
className="review-btn approve"
|
||||
onClick={() => handleSubmitReview('approved')}
|
||||
disabled={submitting}
|
||||
>
|
||||
Approuver
|
||||
</button>
|
||||
<button
|
||||
className="review-btn edit"
|
||||
onClick={() => handleSubmitReview('needs_edit')}
|
||||
disabled={submitting}
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
<button
|
||||
className="review-btn reject"
|
||||
onClick={() => handleSubmitReview('rejected')}
|
||||
disabled={submitting}
|
||||
>
|
||||
Rejeter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Resultat de la review */}
|
||||
{submitResult && (
|
||||
<div className={`review-result ${submitResult.status}`}>
|
||||
<div className="review-result-icon">
|
||||
{submitResult.status === 'approved' && '\u2705'}
|
||||
{submitResult.status === 'rejected' && '\u274C'}
|
||||
{submitResult.status === 'needs_edit' && '\u270F\uFE0F'}
|
||||
</div>
|
||||
<div className="review-result-message">{submitResult.message}</div>
|
||||
{submitResult.status === 'needs_edit' && (
|
||||
<button className="review-btn-open-editor" onClick={handleOpenInEditor}>
|
||||
Ouvrir dans l'editeur
|
||||
</button>
|
||||
)}
|
||||
<button className="review-btn-back" onClick={handleBackToList}>
|
||||
Retour a la liste
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -128,6 +128,15 @@ export default function WorkflowManagerModal({
|
||||
<div className="item-main">
|
||||
<span className="item-name">{wf.name}</span>
|
||||
{wf.id === activeWorkflowId && <span className="active-badge">actif</span>}
|
||||
{wf.review_status === 'pending_review' && (
|
||||
<span className="review-badge-small pending_review">A valider</span>
|
||||
)}
|
||||
{wf.review_status === 'needs_edit' && (
|
||||
<span className="review-badge-small needs_edit">A modifier</span>
|
||||
)}
|
||||
{wf.review_status === 'approved' && (
|
||||
<span className="review-badge-small approved">Approuve</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="item-info">
|
||||
<span>{wf.step_count} étapes</span>
|
||||
|
||||
@@ -147,7 +147,19 @@ export default function WorkflowSelector({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span className="item-name">{wf.name}</span>
|
||||
<span className="item-name">
|
||||
{wf.name}
|
||||
{wf.review_status === 'pending_review' && (
|
||||
<span className="review-badge-inline pending_review" title="En attente de validation">
|
||||
● A valider
|
||||
</span>
|
||||
)}
|
||||
{wf.review_status === 'needs_edit' && (
|
||||
<span className="review-badge-inline needs_edit" title="Modification requise">
|
||||
● A modifier
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="item-meta">
|
||||
{wf.step_count} étapes
|
||||
{wf.tags && wf.tags.length > 0 && (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* API Client - Toutes les interactions avec le backend
|
||||
*/
|
||||
|
||||
import type { AppState, Workflow, Step, Execution, Capture, ActionType, ExecutionMode } from '../types';
|
||||
import type { AppState, Workflow, Step, Execution, Capture, ActionType, ExecutionMode, ReviewStatus } from '../types';
|
||||
|
||||
const API_BASE = '/api/v3';
|
||||
|
||||
@@ -210,3 +210,53 @@ export async function exportWorkflowForTraining(workflowId: string): Promise<{
|
||||
}> {
|
||||
return request('POST', `/workflow/${workflowId}/export-training`);
|
||||
}
|
||||
|
||||
// Review/Validation
|
||||
export async function getPendingReview(): Promise<{
|
||||
workflows: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
step_count: number;
|
||||
source: string;
|
||||
review_status: ReviewStatus;
|
||||
review_feedback: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>;
|
||||
total: number;
|
||||
}> {
|
||||
return request('GET', '/workflows/pending-review');
|
||||
}
|
||||
|
||||
export async function getReviewData(workflowId: string): Promise<{
|
||||
workflow: Workflow & {
|
||||
source: string;
|
||||
review_status: ReviewStatus;
|
||||
review_feedback: string | null;
|
||||
reviewed_at: string | null;
|
||||
};
|
||||
review_info: {
|
||||
source: string;
|
||||
review_status: ReviewStatus;
|
||||
review_feedback: string | null;
|
||||
reviewed_at: string | null;
|
||||
step_count: number;
|
||||
steps_with_anchors: number;
|
||||
steps_without_anchors: number;
|
||||
};
|
||||
}> {
|
||||
return request('GET', `/workflow/${workflowId}/review`);
|
||||
}
|
||||
|
||||
export async function submitReview(
|
||||
workflowId: string,
|
||||
status: 'approved' | 'rejected' | 'needs_edit',
|
||||
feedback?: string
|
||||
): Promise<{
|
||||
workflow_id: string;
|
||||
review_status: string;
|
||||
message: string;
|
||||
}> {
|
||||
return request('POST', `/workflow/${workflowId}/review`, { status, feedback });
|
||||
}
|
||||
|
||||
@@ -3294,10 +3294,19 @@ body {
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.validation-modal .modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.validation-loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
@@ -3433,3 +3442,592 @@ body {
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Review / Validation Mode
|
||||
=========================================== */
|
||||
|
||||
/* Header button */
|
||||
.review-header-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.review-header-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.review-header-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.review-header-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.review-header-count {
|
||||
background: var(--warning);
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 10px;
|
||||
min-width: 18px;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Inline badge in workflow selector */
|
||||
.review-badge-inline {
|
||||
display: inline-block;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
margin-left: 0.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.review-badge-inline.pending_review {
|
||||
background: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.review-badge-inline.needs_edit {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
/* Small badge in WorkflowManagerModal */
|
||||
.review-badge-small {
|
||||
display: inline-block;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 3px;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.review-badge-small.pending_review {
|
||||
background: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.review-badge-small.needs_edit {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.review-badge-small.approved {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
/* Review Modal */
|
||||
.review-modal {
|
||||
background: var(--bg-paper);
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.review-modal .modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-sidebar);
|
||||
}
|
||||
|
||||
.review-modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.review-modal-title h2 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: var(--bg-sidebar);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.review-modal .modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Loading & Error */
|
||||
.review-loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.review-error {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #ffebee;
|
||||
color: var(--error);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.review-error button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--error);
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.review-empty {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.review-empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.review-empty p {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.review-empty-sub {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-disabled);
|
||||
}
|
||||
|
||||
/* List view */
|
||||
.review-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.review-list-item {
|
||||
padding: 1rem 1.25rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.review-list-item:hover {
|
||||
border-color: var(--primary-light);
|
||||
background: #f5f9ff;
|
||||
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.1);
|
||||
}
|
||||
|
||||
.review-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.review-item-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.review-badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.review-badge.pending_review {
|
||||
background: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.review-badge.needs_edit {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.review-item-meta {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.review-item-sep {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.review-item-desc {
|
||||
margin-top: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.review-item-feedback {
|
||||
margin-top: 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--primary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Detail view */
|
||||
.review-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.review-detail-info {
|
||||
background: var(--bg-sidebar);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.review-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.review-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.review-info-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.review-info-value {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.review-info-warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
/* Steps list in review */
|
||||
.review-steps h3 {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.review-steps-empty {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.review-steps-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.review-step-item {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-paper);
|
||||
}
|
||||
|
||||
.review-step-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.review-step-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.review-step-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.review-step-type {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 0.1rem 0.4rem;
|
||||
background: #e8eaf6;
|
||||
color: #3f51b5;
|
||||
border-radius: 3px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.review-step-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.review-step-params {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.review-step-param {
|
||||
font-size: 0.7rem;
|
||||
background: var(--bg-sidebar);
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.review-step-anchor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.review-step-thumbnail {
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.review-step-anchor-ok {
|
||||
font-size: 0.75rem;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.review-step-no-anchor {
|
||||
margin-top: 0.3rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--warning);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Decision zone */
|
||||
.review-decision {
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
|
||||
.review-decision h3 {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.review-feedback-field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.review-feedback-field label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.review-feedback-field textarea {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.review-feedback-field textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.15);
|
||||
}
|
||||
|
||||
.review-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.review-btn {
|
||||
flex: 1;
|
||||
padding: 0.65rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.review-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.review-btn.approve {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.review-btn.approve:hover:not(:disabled) {
|
||||
background: #388e3c;
|
||||
}
|
||||
|
||||
.review-btn.edit {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.review-btn.edit:hover:not(:disabled) {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.review-btn.reject {
|
||||
background: var(--error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.review-btn.reject:hover:not(:disabled) {
|
||||
background: #c62828;
|
||||
}
|
||||
|
||||
/* Review result */
|
||||
.review-result {
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.review-result.approved {
|
||||
background: #e8f5e9;
|
||||
border-color: #a5d6a7;
|
||||
}
|
||||
|
||||
.review-result.rejected {
|
||||
background: #ffebee;
|
||||
border-color: #ef9a9a;
|
||||
}
|
||||
|
||||
.review-result.needs_edit {
|
||||
background: #e3f2fd;
|
||||
border-color: #90caf9;
|
||||
}
|
||||
|
||||
.review-result-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.review-result-message {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.review-btn-open-editor {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.review-btn-open-editor:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.review-btn-back {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
margin-left: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.review-btn-back:hover {
|
||||
background: var(--bg-sidebar);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -143,6 +143,8 @@ export interface Workflow {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export type ReviewStatus = 'pending_review' | 'approved' | 'rejected' | 'needs_edit' | null;
|
||||
|
||||
export interface WorkflowSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -150,6 +152,8 @@ export interface WorkflowSummary {
|
||||
tags?: string[];
|
||||
description?: string;
|
||||
trigger_examples?: string[];
|
||||
source?: string;
|
||||
review_status?: ReviewStatus;
|
||||
}
|
||||
|
||||
export interface Execution {
|
||||
|
||||
Reference in New Issue
Block a user