diff --git a/frontend/src/components/CustomIcons.tsx b/frontend/src/components/CustomIcons.tsx
new file mode 100644
index 0000000..da8c53e
--- /dev/null
+++ b/frontend/src/components/CustomIcons.tsx
@@ -0,0 +1,96 @@
+// 简化的图标组件 - 使用内联SVG
+// 这些是为了演示暂停/继续/回溯功能所需要的基本图标
+
+export function PauseCircleIcon({ size = 24 }: { size?: number }) {
+ return (
+
+ );
+}
+
+export function PlayCircleIcon({ size = 24 }: { size?: number }) {
+ return (
+
+ );
+}
+
+export function HistoryIcon({ size = 24 }: { size?: number }) {
+ return (
+
+ );
+}
+
+export function CodeIcon({ size = 24 }: { size?: number }) {
+ return (
+
+ );
+}
+
+export function EditIcon({ size = 24 }: { size?: number }) {
+ return (
+
+ );
+}
+
+export function RefreshIcon({ size = 24 }: { size?: number }) {
+ return (
+
+ );
+}
+
+export function InfoIcon({ size = 24 }: { size?: number }) {
+ return (
+
+ );
+}
+
+export function XIcon({ size = 24 }: { size?: number }) {
+ return (
+
+ );
+}
+
+export function CheckCircleIcon({ size = 24 }: { size?: number }) {
+ return (
+
+ );
+}
+
+export function PanelChevronIcon({ direction = "left", size = 16 }: { direction?: "left" | "right"; size?: number }) {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/InteractiveControls.tsx b/frontend/src/components/InteractiveControls.tsx
new file mode 100644
index 0000000..e2db8ca
--- /dev/null
+++ b/frontend/src/components/InteractiveControls.tsx
@@ -0,0 +1,175 @@
+import { useState, useEffect } from "react";
+import {
+ HistoryIcon,
+ CodeIcon,
+ EditIcon,
+ InfoIcon,
+ CheckCircleIcon,
+ PauseCircleIcon,
+ XIcon,
+} from "./CustomIcons";
+
+/**
+ * Workflow Controls - provides quick access to useful actions when execution is paused
+ */
+export function InteractiveControls({
+ status,
+ onRewind,
+ onEditCode,
+ onEditRequirements,
+}: {
+ status: string;
+ onRewind?: () => void;
+ onEditCode?: () => void;
+ onEditRequirements?: () => void;
+}) {
+ const isPaused = status === "PAUSED";
+ const isFinished = status === "PASSED" || status === "FAILED" || status === "INTERRUPTED";
+
+ if (!isPaused && !isFinished) return null;
+
+ return (
+
+
+
Workflow Actions
+
+
+
+ {isPaused && (
+
+
+
Execution is paused. What would you like to do?
+
+ )}
+
+
+
+ {isPaused && (
+ <>
+
+
+
+
+
+ >
+ )}
+
+ {isFinished && (
+
+ )}
+
+
+ );
+}
+
+/**
+ * Confirmation dialog for important actions
+ */
+export function ConfirmDialog({
+ title,
+ message,
+ confirmText,
+ cancelText,
+ onConfirm,
+ onCancel,
+ isOpen,
+ type = "info",
+}: {
+ title: string;
+ message: string;
+ confirmText: string;
+ cancelText: string;
+ onConfirm: () => void;
+ onCancel: () => void;
+ isOpen: boolean;
+ type?: "info" | "warning" | "danger";
+}) {
+ if (!isOpen) return null;
+
+ return (
+
+
e.stopPropagation()}>
+
{title}
+
{message}
+
+
+
+
+
+
+
+ );
+}
+
+/**
+ * Simple toast notifications
+ */
+export function Toast({
+ message,
+ type = "info",
+ isVisible,
+ onClose,
+}: {
+ message: string;
+ type?: "info" | "success" | "error" | "warning";
+ isVisible: boolean;
+ onClose: () => void;
+}) {
+ useEffect(() => {
+ if (isVisible) {
+ const timer = setTimeout(onClose, 4000);
+ return () => clearTimeout(timer);
+ }
+ }, [isVisible, onClose]);
+
+ if (!isVisible) return null;
+
+ return (
+
+ {type === "success" &&
}
+ {type === "error" &&
}
+ {type === "warning" &&
}
+ {type === "info" &&
}
+
{message}
+
+
+ );
+}
diff --git a/frontend/src/pages/PlaygroundSubmissionDetailPage.tsx b/frontend/src/pages/PlaygroundSubmissionDetailPage.tsx
index e3dfd5f..4353796 100644
--- a/frontend/src/pages/PlaygroundSubmissionDetailPage.tsx
+++ b/frontend/src/pages/PlaygroundSubmissionDetailPage.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import { DownOutlined, MinusOutlined, PlusOutlined, RadarChartOutlined, UpOutlined } from "@ant-design/icons";
import type { FocusEvent as ReactFocusEvent, MouseEvent as ReactMouseEvent, ReactNode } from "react";
import { createPortal } from "react-dom";
@@ -9,7 +9,9 @@ import SubmissionFactoryCanvas, { type SubmissionFactoryCanvasHandle } from "../
import RequirementNodeDetailContent from "../components/requirements/RequirementNodeDetailContent";
import SubmissionResultCard from "../components/submissions/SubmissionResultCard";
import SubmissionStepList from "../components/submissions/SubmissionStepList";
+import { InteractiveControls, ConfirmDialog, Toast } from "../components/InteractiveControls";
import { ApiError, api } from "../lib/api";
+import "../styles/interactive-controls.css";
import {
cloneRequirementTree,
findNodeById,
@@ -1394,27 +1396,44 @@ function CommitHistoryPanel({
{orderedCommits.map((commit) => {
const isCommitSelected = selectedCommitOid === commit.oid;
+ const canRewind = onRewind && commit.node_id && commit.phase;
return (
-
+
+
+
);
})}
@@ -1541,6 +1560,19 @@ export default function PlaygroundSubmissionDetailPage() {
const [editableDetailExpanded, setEditableDetailExpanded] = useState(false);
const [savingEditableTask, setSavingEditableTask] = useState(false);
const [editableReloadToken, setEditableReloadToken] = useState(0);
+ const [isPausing, setIsPausing] = useState(false);
+ const [isResuming, setIsResuming] = useState(false);
+ const [showConfirmDialog, setShowConfirmDialog] = useState(false);
+ const [confirmDialogData, setConfirmDialogData] = useState<{
+ title: string;
+ message: string;
+ confirmText: string;
+ cancelText: string;
+ type: "info" | "warning" | "danger";
+ onConfirm: () => void;
+ } | null>(null);
+ const [toastVisible, setToastVisible] = useState(false);
+ const [toastData, setToastData] = useState<{ message: string; type: "info" | "success" | "error" | "warning" } | null>(null);
const taskType = normalizeTaskType(requirement?.category ?? rawTaskType);
const executionPaused = submission?.status === "PAUSED";
const executionPausePending = submission?.status === "PAUSE_REQUESTED";
@@ -1590,6 +1622,41 @@ export default function PlaygroundSubmissionDetailPage() {
error: error.message,
});
+ const showToast = (message: string, type: "info" | "success" | "error" | "warning" = "info") => {
+ setToastData({ message, type });
+ setToastVisible(true);
+ };
+
+ const hideToast = () => {
+ setToastVisible(false);
+ setTimeout(() => setToastData(null), 300);
+ };
+
+ const showConfirm = (
+ title: string,
+ message: string,
+ confirmText: string,
+ cancelText: string,
+ type: "info" | "warning" | "danger",
+ onConfirm: () => void
+ ) => {
+ setConfirmDialogData({ title, message, confirmText, cancelText, type, onConfirm });
+ setShowConfirmDialog(true);
+ };
+
+ const handleConfirmCancel = () => {
+ setShowConfirmDialog(false);
+ setTimeout(() => setConfirmDialogData(null), 300);
+ };
+
+ const handleConfirmOk = () => {
+ if (confirmDialogData) {
+ confirmDialogData.onConfirm();
+ }
+ setShowConfirmDialog(false);
+ setTimeout(() => setConfirmDialogData(null), 300);
+ };
+
const loadPreviewStatus = async (silent = false) => {
if (!silent) {
setPreviewLoading(true);
@@ -2135,8 +2202,17 @@ export default function PlaygroundSubmissionDetailPage() {
if (!submissionId) {
return;
}
- editableTreeDirtyRef.current = false;
- setSubmission(await api.pauseSubmission(submissionId));
+ setIsPausing(true);
+ try {
+ editableTreeDirtyRef.current = false;
+ const result = await api.pauseSubmission(submissionId);
+ setSubmission(result);
+ showToast("Execution paused successfully", "success");
+ } catch (error) {
+ showToast("Failed to pause execution", "error");
+ } finally {
+ setIsPausing(false);
+ }
};
const persistEditableTask = async () => {
@@ -2166,6 +2242,7 @@ export default function PlaygroundSubmissionDetailPage() {
if (!submissionId) {
return;
}
+
if (submission?.can_manual_edit) {
try {
const preview = await api.getManualEditCommitPreview(submissionId);
@@ -2173,40 +2250,66 @@ export default function PlaygroundSubmissionDetailPage() {
const scope = preview.node_id && preview.phase
? `${preview.node_id} (${preview.phase})`
: "the current paused step";
- const confirmed = window.confirm(
- `Resume will create one manual commit for ${scope} before continuing.`,
+
+ // Use our nice confirm dialog instead of window.confirm
+ showConfirm(
+ "Confirm Resume",
+ `Resume will create one manual commit for ${scope} before continuing. Do you want to proceed?`,
+ "Yes, Continue",
+ "Cancel",
+ "warning",
+ async () => {
+ await doResume();
+ }
);
- if (!confirmed) {
- return;
- }
+ return;
}
} catch (error) {
const fallbackMessage = error instanceof Error ? error.message : "Failed to prepare resume preview.";
console.error("Failed to load manual edit commit preview", error);
- const confirmed = window.confirm(
- `${fallbackMessage}\n\nResume will still try to continue from the paused workspace. Continue?`,
+
+ showConfirm(
+ "Resume Anyway?",
+ `${fallbackMessage}\n\nResume will still try to continue from the paused workspace. Do you want to proceed?`,
+ "Yes, Continue",
+ "Cancel",
+ "info",
+ async () => {
+ await doResume();
+ }
);
- if (!confirmed) {
- return;
- }
+ return;
}
}
- if (submission?.status === "PAUSED" && editableTreeDirtyRef.current) {
- setSavingEditableTask(true);
- try {
- await persistEditableTask();
- } finally {
- setSavingEditableTask(false);
+
+ await doResume();
+ };
+
+ const doResume = async () => {
+ setIsResuming(true);
+ try {
+ if (submission?.status === "PAUSED" && editableTreeDirtyRef.current) {
+ setSavingEditableTask(true);
+ try {
+ await persistEditableTask();
+ } finally {
+ setSavingEditableTask(false);
+ }
}
- }
- const nextSubmission = await api.resumeSubmission(submissionId);
- setSubmission(nextSubmission);
- if (nextSubmission.status !== "PAUSED") {
- editableTreeDirtyRef.current = false;
- setEditableTask(null);
- setEditableTree(null);
- setEditableNodeId("ROOT");
- setEditableDetailExpanded(false);
+ const nextSubmission = await api.resumeSubmission(submissionId);
+ setSubmission(nextSubmission);
+ if (nextSubmission.status !== "PAUSED") {
+ editableTreeDirtyRef.current = false;
+ setEditableTask(null);
+ setEditableTree(null);
+ setEditableNodeId("ROOT");
+ setEditableDetailExpanded(false);
+ }
+ showToast("Execution resumed successfully", "success");
+ } catch (error) {
+ showToast("Failed to resume execution", "error");
+ } finally {
+ setIsResuming(false);
}
};
@@ -2214,24 +2317,76 @@ export default function PlaygroundSubmissionDetailPage() {
if (!submissionId) {
return;
}
- const nextSubmission = await api.rewindSubmission(submissionId, { commit_oid: commit.oid });
- const targetNodeId = commit.node_id ?? "ROOT";
- editableTreeDirtyRef.current = false;
- visualEventCountRef.current = 0;
- setSubmission(nextSubmission);
- setTraceabilityNodeId(targetNodeId);
- setEditableTask(null);
- setEditableTree(null);
+
+ showConfirm(
+ "Rewind to This Commit?",
+ `Rewind this submission to commit ${commit.short_oid}? Execution will pause and later commits in the workspace history will be discarded.`,
+ "Rewind",
+ "Cancel",
+ "danger",
+ async () => {
+ try {
+ const nextSubmission = await api.rewindSubmission(submissionId, { commit_oid: commit.oid });
+ const targetNodeId = commit.node_id ?? "ROOT";
+ editableTreeDirtyRef.current = false;
+ visualEventCountRef.current = 0;
+ setSubmission(nextSubmission);
+ setTraceabilityNodeId(targetNodeId);
+ setEditableTask(null);
+ setEditableTree(null);
+ setActiveTab("canvas");
+ setSelectedCommitOid(commit.oid);
+ setSelectedNodeId(targetNodeId);
+ setEditableNodeId(targetNodeId);
+ setEditableReloadToken((current) => current + 1);
+ setFocusNodeId(targetNodeId);
+ setPulseNodeId(null);
+ if (useQuickStartSubmission) {
+ quickStart.setSelectedNode(targetNodeId);
+ }
+ showToast("Rewound to previous commit successfully", "success");
+ } catch (error) {
+ showToast("Failed to rewind submission", "error");
+ }
+ }
+ );
+ };
+
+ const handleRewindClick = () => {
+ // Switch to history tab
+ setPreviewTab("history");
+ // Expand the sidebar if necessary
+ if (sidebarMinimized) {
+ setSidebarMinimized(false);
+ }
+ showToast("Select a commit to rewind to from the history panel", "info");
+ };
+
+ const handleEditCodeClick = () => {
+ // Switch to file tab and open the first file
+ setActiveTab("file");
+ if (sidebarMinimized) {
+ setSidebarMinimized(false);
+ }
+ setSidebarTab("status");
+ showToast("You can now edit code and tests from the workspace panel", "info");
+ };
+
+ const handleEditRequirementsClick = () => {
+ // Activate edit mode for requirements
+ if (submission?.status !== "PAUSED") {
+ showToast("Execution must be paused to edit requirements", "warning");
+ return;
+ }
+ // Select the root node and show edit panel
+ setSelectedNodeId("ROOT");
+ setEditableNodeId("ROOT");
setActiveTab("canvas");
- setSelectedCommitOid(commit.oid);
- setSelectedNodeId(targetNodeId);
- setEditableNodeId(targetNodeId);
- setEditableReloadToken((current) => current + 1);
- setFocusNodeId(targetNodeId);
- setPulseNodeId(null);
- if (useQuickStartSubmission) {
- quickStart.setSelectedNode(targetNodeId);
+ setDetailExpanded(true);
+ if (sidebarMinimized) {
+ setSidebarMinimized(false);
}
+ showToast("You can now edit requirements from the canvas", "info");
};
const handleSaveFix = async () => {
@@ -2430,6 +2585,14 @@ export default function PlaygroundSubmissionDetailPage() {
+ {/* Interactive Controls Panel */}
+
+
+
+ {showConfirmDialog && confirmDialogData && (
+
+ )}
+
+ {toastData && (
+
+ )}
);
}
diff --git a/frontend/src/styles/interactive-controls.css b/frontend/src/styles/interactive-controls.css
new file mode 100644
index 0000000..b218aae
--- /dev/null
+++ b/frontend/src/styles/interactive-controls.css
@@ -0,0 +1,287 @@
+/* Interactive Controls Panel - Clean and Simple */
+
+.interactive-controls-panel {
+ background: var(--bg-elevated);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 16px;
+}
+
+.control-header {
+ margin-bottom: 12px;
+}
+
+.control-header h3 {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.control-tips {
+ margin-bottom: 12px;
+}
+
+.control-tip-item {
+ display: flex;
+ gap: 8px;
+ align-items: flex-start;
+ padding: 10px 12px;
+ border-radius: 6px;
+ background: rgba(59, 130, 246, 0.1);
+ color: #3b82f6;
+}
+
+.control-tip-item p {
+ margin: 0;
+ font-size: 13px;
+ line-height: 1.4;
+}
+
+.control-buttons-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.control-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--text);
+ border-radius: 6px;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.control-button:hover {
+ background: var(--bg-hover);
+ border-color: var(--border-hover);
+ transform: translateY(-1px);
+}
+
+/* Confirm Dialog */
+
+.confirm-dialog-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2000;
+}
+
+.confirm-dialog {
+ background: white;
+ padding: 24px;
+ border-radius: 12px;
+ max-width: 400px;
+ width: 90%;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
+ text-align: center;
+}
+
+.confirm-dialog h3 {
+ margin: 0 0 12px 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2937;
+}
+
+.confirm-dialog p {
+ margin: 0 0 24px 0;
+ color: #6b7280;
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.confirm-actions {
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+}
+
+.confirm-button {
+ padding: 10px 20px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ border: none;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+.confirm-button.secondary {
+ background: #f3f4f6;
+ color: #374151;
+}
+
+.confirm-button.secondary:hover {
+ background: #e5e7eb;
+}
+
+.confirm-button.info {
+ background: #3b82f6;
+ color: white;
+}
+
+.confirm-button.warning {
+ background: #f59e0b;
+ color: white;
+}
+
+.confirm-button.danger {
+ background: #dc2626;
+ color: white;
+}
+
+/* Toast Notification */
+
+.toast-notification {
+ position: fixed;
+ bottom: 24px;
+ right: 24px;
+ background: white;
+ padding: 14px 18px;
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.12);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ min-width: 280px;
+ max-width: 400px;
+ z-index: 3000;
+ border-left: 4px solid #3b82f6;
+}
+
+.toast-notification.success {
+ border-left-color: #10b981;
+}
+
+.toast-notification.error {
+ border-left-color: #ef4444;
+}
+
+.toast-notification.warning {
+ border-left-color: #f59e0b;
+}
+
+.toast-notification span {
+ flex: 1;
+ font-size: 14px;
+ color: #1f2937;
+ line-height: 1.4;
+}
+
+.toast-close {
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ color: #9ca3af;
+ padding: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: color 0.15s;
+}
+
+.toast-close:hover {
+ color: #4b5563;
+}
+
+/* Responsive */
+
+@media (max-width: 768px) {
+ .toast-notification {
+ left: 12px;
+ right: 12px;
+ bottom: 12px;
+ }
+}
+
+/* Commit History Rewind Button */
+
+.commit-history-item-wrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.commit-history-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ text-align: left;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ transition: background 0.15s ease;
+}
+
+.commit-history-item:hover {
+ background: var(--bg-hover);
+}
+
+.commit-history-item.commit-active {
+ background: var(--bg-elevated);
+ border-left: 3px solid var(--accent-color, #3b82f6);
+}
+
+.commit-history-left {
+ flex: 1;
+ min-width: 0;
+}
+
+.commit-history-main {
+ display: block;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 4px;
+}
+
+.commit-history-secondary {
+ display: block;
+ font-size: 12px;
+ color: var(--text-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.commit-rewind-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 6px 12px;
+ background: #fef3c7;
+ color: #92400e;
+ border: 1px solid #fcd34d;
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ white-space: nowrap;
+}
+
+.commit-rewind-button:hover {
+ background: #fde68a;
+ transform: scale(1.02);
+ box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
+}