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:
Dom
2026-03-15 10:02:09 +01:00
parent 74a1cb4e03
commit cf495dd82f
93 changed files with 12463 additions and 1080 deletions

View File

@@ -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">&#128203;</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}

View File

@@ -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}>
&#8592;
</button>
<h2>Review : {selectedWf?.name || '...'}</h2>
</>
)}
</div>
<button className="close-btn" onClick={onClose}>&times;</button>
</div>
<div className="modal-body">
{error && (
<div className="review-error">
{error}
<button onClick={() => setError(null)}>&times;</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">&#10003;</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">&bull;</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>
);
}

View File

@@ -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>

View File

@@ -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">
&#9679; A valider
</span>
)}
{wf.review_status === 'needs_edit' && (
<span className="review-badge-inline needs_edit" title="Modification requise">
&#9679; A modifier
</span>
)}
</span>
<span className="item-meta">
{wf.step_count} étapes
{wf.tags && wf.tags.length > 0 && (

View File

@@ -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 });
}

View File

@@ -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);
}

View File

@@ -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 {