summaryrefslogtreecommitdiff
path: root/frontend/src/components/document/nodes/ContractBlockComponent.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-04-28 17:35:08 +0100
committerGitHub <noreply@github.com>2026-04-28 17:35:08 +0100
commitd513f93c84ae985738e0f696fcb72fa1153046ef (patch)
treed169fa48ce93f1e204a80b60ca9295772bc2fa63 /frontend/src/components/document/nodes/ContractBlockComponent.tsx
parent5aa3fafb4acfa89c7d04e84abf7861607733e8ce (diff)
downloadsoryu-d513f93c84ae985738e0f696fcb72fa1153046ef.tar.gz
soryu-d513f93c84ae985738e0f696fcb72fa1153046ef.zip
feat: document UI with contract blocks, expandable logs, and interaction controls (#97)
* feat: soryu-co/soryu - makima: Rename tasks to contracts in directive API and types * feat: soryu-co/soryu - makima: Add contract interaction panel with comment and interrupt * feat: soryu-co/soryu - makima: Build expandable contract log feed in StepsDiagram * feat: soryu-co/soryu - makima: Rename tasks to contracts throughout document UI and add contract block support * feat: soryu-co/soryu - makima: Add comment and interrupt controls to expanded step log feed * feat: soryu-co/soryu - makima: Audit and fix Document UI feature flag visibility and missing implementations * feat: soryu-co/soryu - makima: Add expandable step rows with live log feed in StepsDiagram * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Integrate all document UI components and final polish
Diffstat (limited to 'frontend/src/components/document/nodes/ContractBlockComponent.tsx')
-rw-r--r--frontend/src/components/document/nodes/ContractBlockComponent.tsx117
1 files changed, 117 insertions, 0 deletions
diff --git a/frontend/src/components/document/nodes/ContractBlockComponent.tsx b/frontend/src/components/document/nodes/ContractBlockComponent.tsx
new file mode 100644
index 0000000..0d9a25a
--- /dev/null
+++ b/frontend/src/components/document/nodes/ContractBlockComponent.tsx
@@ -0,0 +1,117 @@
+import React, { useEffect, useState } from 'react';
+import './ContractBlock.css';
+
+interface ContractBlockComponentProps {
+ contractId: string;
+ contractName: string;
+}
+
+interface ContractInfo {
+ id: string;
+ name: string;
+ status: string;
+ phase: string;
+ contract_type: string;
+}
+
+const PHASE_COLORS: Record<string, string> = {
+ planning: '#3b82f6',
+ execution: '#f59e0b',
+ review: '#8b5cf6',
+ completed: '#10b981',
+ failed: '#ef4444',
+};
+
+const STATUS_COLORS: Record<string, string> = {
+ active: '#10b981',
+ running: '#10b981',
+ idle: '#f59e0b',
+ paused: '#f59e0b',
+ completed: '#10b981',
+ failed: '#ef4444',
+ archived: '#6b7280',
+};
+
+export function ContractBlockComponent({ contractId, contractName }: ContractBlockComponentProps) {
+ const [contract, setContract] = useState<ContractInfo | null>(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ async function fetchContract() {
+ try {
+ const response = await fetch(`/api/v1/contracts/${contractId}`);
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ const data = await response.json();
+ if (!cancelled) {
+ setContract(data.contract || data);
+ setError(null);
+ }
+ } catch (err) {
+ if (!cancelled) {
+ setError(err instanceof Error ? err.message : 'Failed to load');
+ }
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ }
+
+ fetchContract();
+ return () => { cancelled = true; };
+ }, [contractId]);
+
+ if (loading) {
+ return (
+ <div className="contract-block" contentEditable={false}>
+ <div className="contract-block-loading">
+ <div className="contract-block-spinner" />
+ <span>Loading contract...</span>
+ </div>
+ </div>
+ );
+ }
+
+ if (error) {
+ return (
+ <div className="contract-block contract-block--error" contentEditable={false}>
+ <div className="contract-block-header">
+ <span className="contract-block-icon">&#x1F4E6;</span>
+ <span className="contract-block-name">{contractName}</span>
+ </div>
+ <div className="contract-block-error-msg">Unable to load: {error}</div>
+ </div>
+ );
+ }
+
+ const phase = contract?.phase?.toLowerCase() || 'unknown';
+ const status = contract?.status?.toLowerCase() || 'unknown';
+ const phaseColor = PHASE_COLORS[phase] || '#6b7280';
+ const statusColor = STATUS_COLORS[status] || '#6b7280';
+
+ return (
+ <div className="contract-block" contentEditable={false}>
+ <div className="contract-block-header">
+ <span className="contract-block-icon">&#x1F4E6;</span>
+ <span className="contract-block-name">{contract?.name || contractName}</span>
+ <span
+ className="contract-block-phase-badge"
+ style={{ backgroundColor: phaseColor + '20', color: phaseColor }}
+ >
+ {phase}
+ </span>
+ <span
+ className="contract-block-status-dot"
+ style={{ backgroundColor: statusColor }}
+ title={status}
+ />
+ </div>
+ {contract?.contract_type && (
+ <div className="contract-block-meta">
+ <span className="contract-block-type">{contract.contract_type}</span>
+ </div>
+ )}
+ </div>
+ );
+}