2026-02-27 11:21:35 +00:00
|
|
|
import * as React from "react";
|
|
|
|
|
import Markdown from "react-markdown";
|
2026-02-28 09:28:27 +00:00
|
|
|
import type { AgentEvent, AgentInfo, AgentStatusValue } from "../api/agents";
|
|
|
|
|
import { agentsApi, subscribeAgentStream } from "../api/agents";
|
2026-02-28 09:38:51 +00:00
|
|
|
import type { TestCaseResult, TestResultsResponse } from "../api/client";
|
2026-02-27 11:21:35 +00:00
|
|
|
import { api } from "../api/client";
|
|
|
|
|
|
|
|
|
|
const { useEffect, useRef, useState } = React;
|
|
|
|
|
|
|
|
|
|
const STAGE_LABELS: Record<string, string> = {
|
|
|
|
|
upcoming: "Upcoming",
|
|
|
|
|
current: "Current",
|
|
|
|
|
qa: "QA",
|
|
|
|
|
merge: "To Merge",
|
|
|
|
|
done: "Done",
|
|
|
|
|
archived: "Archived",
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-28 09:28:27 +00:00
|
|
|
const STATUS_COLORS: Record<AgentStatusValue, string> = {
|
|
|
|
|
running: "#3fb950",
|
|
|
|
|
pending: "#e3b341",
|
|
|
|
|
completed: "#aaa",
|
|
|
|
|
failed: "#f85149",
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-27 11:21:35 +00:00
|
|
|
interface WorkItemDetailPanelProps {
|
|
|
|
|
storyId: string;
|
2026-02-28 09:38:51 +00:00
|
|
|
pipelineVersion: number;
|
2026-02-27 11:21:35 +00:00
|
|
|
onClose: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 09:38:51 +00:00
|
|
|
function TestCaseRow({ tc }: { tc: TestCaseResult }) {
|
|
|
|
|
const isPassing = tc.status === "pass";
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
data-testid={`test-case-${tc.name}`}
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
gap: "2px",
|
|
|
|
|
padding: "4px 0",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
|
|
|
<span
|
|
|
|
|
data-testid={`test-status-${tc.name}`}
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: "0.85em",
|
|
|
|
|
color: isPassing ? "#3fb950" : "#f85149",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{isPassing ? "PASS" : "FAIL"}
|
|
|
|
|
</span>
|
|
|
|
|
<span style={{ fontSize: "0.82em", color: "#ccc" }}>{tc.name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{tc.details && (
|
|
|
|
|
<div
|
|
|
|
|
data-testid={`test-details-${tc.name}`}
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: "0.75em",
|
|
|
|
|
color: "#888",
|
|
|
|
|
paddingLeft: "22px",
|
|
|
|
|
whiteSpace: "pre-wrap",
|
|
|
|
|
wordBreak: "break-word",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{tc.details}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function TestSection({
|
|
|
|
|
title,
|
|
|
|
|
tests,
|
|
|
|
|
testId,
|
|
|
|
|
}: {
|
|
|
|
|
title: string;
|
|
|
|
|
tests: TestCaseResult[];
|
|
|
|
|
testId: string;
|
|
|
|
|
}) {
|
|
|
|
|
const passCount = tests.filter((t) => t.status === "pass").length;
|
|
|
|
|
const failCount = tests.length - passCount;
|
|
|
|
|
return (
|
|
|
|
|
<div data-testid={testId}>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: "0.78em",
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
color: "#aaa",
|
|
|
|
|
marginBottom: "6px",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{title} ({passCount} passed, {failCount} failed)
|
|
|
|
|
</div>
|
|
|
|
|
{tests.length === 0 ? (
|
|
|
|
|
<div style={{ fontSize: "0.75em", color: "#555", fontStyle: "italic" }}>
|
|
|
|
|
No tests recorded
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
tests.map((tc) => <TestCaseRow key={tc.name} tc={tc} />)
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 11:21:35 +00:00
|
|
|
export function WorkItemDetailPanel({
|
|
|
|
|
storyId,
|
2026-02-28 09:38:51 +00:00
|
|
|
pipelineVersion,
|
2026-02-27 11:21:35 +00:00
|
|
|
onClose,
|
|
|
|
|
}: WorkItemDetailPanelProps) {
|
|
|
|
|
const [content, setContent] = useState<string | null>(null);
|
|
|
|
|
const [stage, setStage] = useState<string>("");
|
|
|
|
|
const [name, setName] = useState<string | null>(null);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
2026-02-28 09:28:27 +00:00
|
|
|
const [agentInfo, setAgentInfo] = useState<AgentInfo | null>(null);
|
|
|
|
|
const [agentLog, setAgentLog] = useState<string[]>([]);
|
|
|
|
|
const [agentStatus, setAgentStatus] = useState<AgentStatusValue | null>(null);
|
2026-02-28 09:38:51 +00:00
|
|
|
const [testResults, setTestResults] = useState<TestResultsResponse | null>(
|
|
|
|
|
null,
|
|
|
|
|
);
|
2026-02-27 11:21:35 +00:00
|
|
|
const panelRef = useRef<HTMLDivElement>(null);
|
2026-02-28 09:28:27 +00:00
|
|
|
const cleanupRef = useRef<(() => void) | null>(null);
|
2026-02-27 11:21:35 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
api
|
|
|
|
|
.getWorkItemContent(storyId)
|
|
|
|
|
.then((data) => {
|
|
|
|
|
setContent(data.content);
|
|
|
|
|
setStage(data.stage);
|
|
|
|
|
setName(data.name);
|
|
|
|
|
})
|
|
|
|
|
.catch((err: unknown) => {
|
|
|
|
|
setError(err instanceof Error ? err.message : "Failed to load content");
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
});
|
|
|
|
|
}, [storyId]);
|
|
|
|
|
|
2026-02-28 09:38:51 +00:00
|
|
|
// Fetch test results on mount and when pipeline updates arrive.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
api
|
|
|
|
|
.getTestResults(storyId)
|
|
|
|
|
.then((data) => {
|
|
|
|
|
setTestResults(data);
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
// Silently ignore — test results may not exist yet.
|
|
|
|
|
});
|
|
|
|
|
}, [storyId, pipelineVersion]);
|
|
|
|
|
|
2026-02-28 09:28:27 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
cleanupRef.current?.();
|
|
|
|
|
cleanupRef.current = null;
|
|
|
|
|
setAgentInfo(null);
|
|
|
|
|
setAgentLog([]);
|
|
|
|
|
setAgentStatus(null);
|
|
|
|
|
|
|
|
|
|
agentsApi
|
|
|
|
|
.listAgents()
|
|
|
|
|
.then((agents) => {
|
|
|
|
|
const agent = agents.find((a) => a.story_id === storyId);
|
|
|
|
|
if (!agent) return;
|
|
|
|
|
setAgentInfo(agent);
|
|
|
|
|
setAgentStatus(agent.status);
|
|
|
|
|
|
|
|
|
|
if (agent.status === "running" || agent.status === "pending") {
|
|
|
|
|
const cleanup = subscribeAgentStream(
|
|
|
|
|
storyId,
|
|
|
|
|
agent.agent_name,
|
|
|
|
|
(event: AgentEvent) => {
|
|
|
|
|
switch (event.type) {
|
|
|
|
|
case "status":
|
|
|
|
|
setAgentStatus((event.status as AgentStatusValue) ?? null);
|
|
|
|
|
break;
|
|
|
|
|
case "output":
|
|
|
|
|
setAgentLog((prev) => [...prev, event.text ?? ""]);
|
|
|
|
|
break;
|
|
|
|
|
case "done":
|
|
|
|
|
setAgentStatus("completed");
|
|
|
|
|
break;
|
|
|
|
|
case "error":
|
|
|
|
|
setAgentStatus("failed");
|
|
|
|
|
setAgentLog((prev) => [
|
|
|
|
|
...prev,
|
|
|
|
|
`[ERROR] ${event.message ?? "Unknown error"}`,
|
|
|
|
|
]);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
cleanupRef.current = cleanup;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((err: unknown) => {
|
|
|
|
|
console.error("Failed to load agents:", err);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
cleanupRef.current?.();
|
|
|
|
|
cleanupRef.current = null;
|
|
|
|
|
};
|
|
|
|
|
}, [storyId]);
|
|
|
|
|
|
2026-02-27 11:21:35 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
onClose();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
|
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
|
|
|
}, [onClose]);
|
|
|
|
|
|
|
|
|
|
const stageLabel = STAGE_LABELS[stage] ?? stage;
|
2026-02-28 09:38:51 +00:00
|
|
|
const hasTestResults =
|
|
|
|
|
testResults &&
|
|
|
|
|
(testResults.unit.length > 0 || testResults.integration.length > 0);
|
2026-02-27 11:21:35 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
data-testid="work-item-detail-panel"
|
|
|
|
|
ref={panelRef}
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
height: "100%",
|
|
|
|
|
overflow: "hidden",
|
|
|
|
|
background: "#1a1a1a",
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
border: "1px solid #333",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
justifyContent: "space-between",
|
|
|
|
|
padding: "12px 16px",
|
|
|
|
|
borderBottom: "1px solid #333",
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
gap: "2px",
|
|
|
|
|
minWidth: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
data-testid="detail-panel-title"
|
|
|
|
|
style={{
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
fontSize: "0.95em",
|
|
|
|
|
color: "#ececec",
|
|
|
|
|
overflow: "hidden",
|
|
|
|
|
textOverflow: "ellipsis",
|
|
|
|
|
whiteSpace: "nowrap",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{name ?? storyId}
|
|
|
|
|
</div>
|
|
|
|
|
{stage && (
|
|
|
|
|
<div
|
|
|
|
|
data-testid="detail-panel-stage"
|
|
|
|
|
style={{ fontSize: "0.75em", color: "#888" }}
|
|
|
|
|
>
|
|
|
|
|
{stageLabel}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
data-testid="detail-panel-close"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
style={{
|
|
|
|
|
background: "none",
|
|
|
|
|
border: "1px solid #444",
|
|
|
|
|
borderRadius: "6px",
|
|
|
|
|
color: "#aaa",
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
padding: "4px 10px",
|
|
|
|
|
fontSize: "0.8em",
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Close
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Scrollable content area */}
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
flex: 1,
|
|
|
|
|
overflowY: "auto",
|
|
|
|
|
padding: "16px",
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
gap: "16px",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{loading && (
|
|
|
|
|
<div
|
|
|
|
|
data-testid="detail-panel-loading"
|
|
|
|
|
style={{ color: "#666", fontSize: "0.85em" }}
|
|
|
|
|
>
|
|
|
|
|
Loading...
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{error && (
|
|
|
|
|
<div
|
|
|
|
|
data-testid="detail-panel-error"
|
|
|
|
|
style={{ color: "#ff7b72", fontSize: "0.85em" }}
|
|
|
|
|
>
|
|
|
|
|
{error}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!loading && !error && content !== null && (
|
|
|
|
|
<div
|
|
|
|
|
data-testid="detail-panel-content"
|
|
|
|
|
className="markdown-body"
|
|
|
|
|
style={{ fontSize: "0.9em", lineHeight: 1.6 }}
|
|
|
|
|
>
|
2026-02-28 09:18:50 +00:00
|
|
|
<Markdown
|
|
|
|
|
components={{
|
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: react-markdown requires any for component props
|
|
|
|
|
h1: ({ children }: any) => (
|
|
|
|
|
<h1 style={{ fontSize: "1.2em" }}>{children}</h1>
|
|
|
|
|
),
|
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: react-markdown requires any for component props
|
|
|
|
|
h2: ({ children }: any) => (
|
|
|
|
|
<h2 style={{ fontSize: "1.1em" }}>{children}</h2>
|
|
|
|
|
),
|
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: react-markdown requires any for component props
|
|
|
|
|
h3: ({ children }: any) => (
|
|
|
|
|
<h3 style={{ fontSize: "1em" }}>{children}</h3>
|
|
|
|
|
),
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{content}
|
|
|
|
|
</Markdown>
|
2026-02-27 11:21:35 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-28 09:38:51 +00:00
|
|
|
{/* Test Results section */}
|
|
|
|
|
<div
|
|
|
|
|
data-testid="test-results-section"
|
|
|
|
|
style={{
|
|
|
|
|
border: "1px solid #2a2a2a",
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
padding: "10px 12px",
|
|
|
|
|
background: "#161616",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
fontSize: "0.8em",
|
|
|
|
|
color: "#555",
|
|
|
|
|
marginBottom: "8px",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Test Results
|
|
|
|
|
</div>
|
|
|
|
|
{hasTestResults ? (
|
|
|
|
|
<div
|
|
|
|
|
data-testid="test-results-content"
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
gap: "12px",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<TestSection
|
|
|
|
|
title="Unit Tests"
|
|
|
|
|
tests={testResults.unit}
|
|
|
|
|
testId="test-section-unit"
|
|
|
|
|
/>
|
|
|
|
|
<TestSection
|
|
|
|
|
title="Integration Tests"
|
|
|
|
|
tests={testResults.integration}
|
|
|
|
|
testId="test-section-integration"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div
|
|
|
|
|
data-testid="test-results-empty"
|
|
|
|
|
style={{ fontSize: "0.75em", color: "#444" }}
|
|
|
|
|
>
|
|
|
|
|
No test results recorded
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-27 11:21:35 +00:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
gap: "8px",
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-02-28 09:28:27 +00:00
|
|
|
{/* Agent Logs section */}
|
|
|
|
|
<div
|
|
|
|
|
data-testid={
|
|
|
|
|
agentInfo ? "agent-logs-section" : "placeholder-agent-logs"
|
|
|
|
|
}
|
|
|
|
|
style={{
|
|
|
|
|
border: "1px solid #2a2a2a",
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
padding: "10px 12px",
|
|
|
|
|
background: "#161616",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
justifyContent: "space-between",
|
|
|
|
|
marginBottom: agentInfo ? "6px" : "4px",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
fontSize: "0.8em",
|
|
|
|
|
color: agentInfo ? "#888" : "#555",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Agent Logs
|
|
|
|
|
</div>
|
|
|
|
|
{agentInfo && agentStatus && (
|
|
|
|
|
<div
|
|
|
|
|
data-testid="agent-status-badge"
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: "0.7em",
|
|
|
|
|
color: STATUS_COLORS[agentStatus],
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{agentInfo.agent_name} — {agentStatus}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{agentInfo && agentLog.length > 0 ? (
|
|
|
|
|
<div
|
|
|
|
|
data-testid="agent-log-output"
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: "0.75em",
|
|
|
|
|
fontFamily: "monospace",
|
|
|
|
|
color: "#ccc",
|
|
|
|
|
whiteSpace: "pre-wrap",
|
|
|
|
|
wordBreak: "break-word",
|
|
|
|
|
lineHeight: "1.5",
|
|
|
|
|
maxHeight: "200px",
|
|
|
|
|
overflowY: "auto",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{agentLog.join("")}
|
|
|
|
|
</div>
|
|
|
|
|
) : agentInfo ? (
|
|
|
|
|
<div style={{ fontSize: "0.75em", color: "#444" }}>
|
|
|
|
|
{agentStatus === "running" || agentStatus === "pending"
|
|
|
|
|
? "Waiting for output..."
|
|
|
|
|
: "No output."}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div style={{ fontSize: "0.75em", color: "#444" }}>
|
|
|
|
|
Coming soon
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Placeholder sections for future content */}
|
2026-02-27 11:21:35 +00:00
|
|
|
{(
|
|
|
|
|
[
|
|
|
|
|
{ id: "coverage", label: "Coverage" },
|
|
|
|
|
] as { id: string; label: string }[]
|
|
|
|
|
).map(({ id, label }) => (
|
|
|
|
|
<div
|
|
|
|
|
key={id}
|
|
|
|
|
data-testid={`placeholder-${id}`}
|
|
|
|
|
style={{
|
|
|
|
|
border: "1px solid #2a2a2a",
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
padding: "10px 12px",
|
|
|
|
|
background: "#161616",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
fontSize: "0.8em",
|
|
|
|
|
color: "#555",
|
|
|
|
|
marginBottom: "4px",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ fontSize: "0.75em", color: "#444" }}>
|
|
|
|
|
Coming soon
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|