feat: add project-aware agent workspace
This commit is contained in:
@@ -1,25 +1,35 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ClipboardList, ReceiptText, RefreshCw, X } from "lucide-react";
|
||||
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
|
||||
import { Activity, ClipboardList, RadioTower, ReceiptText, RefreshCw, X } from "lucide-react";
|
||||
import type { WorkbenchAccess } from "../api/request-context";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { cn } from "@/shared/ui/cn";
|
||||
import { Disclosure } from "@/shared/ui/disclosure";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs";
|
||||
import { ManagedTestResources } from "./managed-test-resources";
|
||||
import { ScadaSystemSection } from "./scada-system-section";
|
||||
import { WorkbenchNotice } from "../components/workbench-panel-ui";
|
||||
import {
|
||||
PeripheralSampleClient,
|
||||
type PeripheralContext,
|
||||
type PeripheralTestDataRecord,
|
||||
type PeripheralWorkOrder
|
||||
} from "./client";
|
||||
|
||||
const DISCLAIMER = "仅用于验证外围工单接口与同步过程,不代表真实现场处置";
|
||||
|
||||
export function PeripheralSamplePanel({ open, onClose, getAccessToken, onAgentTrigger }: {
|
||||
export function PeripheralSamplePanel({
|
||||
open,
|
||||
onClose,
|
||||
getAccessToken,
|
||||
onAgentTrigger,
|
||||
onScadaAnomalyTrigger
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
getAccessToken?: AccessTokenProvider;
|
||||
getAccessToken?: WorkbenchAccess;
|
||||
onAgentTrigger?: (workOrder: PeripheralWorkOrder) => Promise<void>;
|
||||
onScadaAnomalyTrigger?: (record: PeripheralTestDataRecord) => Promise<void>;
|
||||
}) {
|
||||
const clientRef = useRef(new PeripheralSampleClient(getAccessToken));
|
||||
const client = clientRef.current;
|
||||
@@ -28,6 +38,8 @@ export function PeripheralSamplePanel({ open, onClose, getAccessToken, onAgentTr
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [receiptRequestId, setReceiptRequestId] = useState("");
|
||||
const [refreshGeneration, setRefreshGeneration] = useState(0);
|
||||
const [scadaRefreshGeneration, setScadaRefreshGeneration] = useState(0);
|
||||
const [activeSystem, setActiveSystem] = useState<"work-orders" | "scada">("work-orders");
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!client.configured) return;
|
||||
@@ -51,84 +63,119 @@ export function PeripheralSamplePanel({ open, onClose, getAccessToken, onAgentTr
|
||||
|
||||
return (
|
||||
<aside
|
||||
aria-label="外围工单测试"
|
||||
aria-label="外围系统"
|
||||
className="acrylic-panel absolute bottom-3 right-3 top-16 z-[61] flex w-[min(680px,calc(100%-1.5rem))] flex-col overflow-hidden overscroll-contain rounded-2xl border text-slate-900 shadow-[0_16px_44px_rgba(15,23,42,0.18)] max-sm:bg-[var(--surface-reading)]"
|
||||
>
|
||||
<header className="flex min-h-16 shrink-0 items-center gap-3 border-b border-slate-200/80 px-4 sm:px-5">
|
||||
<span className="grid h-8 w-8 place-items-center rounded-lg bg-cyan-700 text-white">
|
||||
<ClipboardList size={17} />
|
||||
<RadioTower size={17} />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-sm font-semibold">外围工单测试</h2>
|
||||
<p className="text-xs text-slate-500">创建、更新过程、回执核对与事件上报</p>
|
||||
<h2 className="text-sm font-semibold">外围系统</h2>
|
||||
<p className="text-xs text-slate-500">工单协同与 SCADA 最新数据</p>
|
||||
</div>
|
||||
<Button size="icon" variant="ghost" aria-label="刷新外围工单" onClick={() => void refresh()} disabled={busy}>
|
||||
<RefreshCw size={16} className={cn(busy && "animate-spin")} />
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label="刷新当前外围系统"
|
||||
onClick={() => {
|
||||
if (activeSystem === "work-orders") void refresh();
|
||||
else setScadaRefreshGeneration((current) => current + 1);
|
||||
}}
|
||||
disabled={activeSystem === "work-orders" && busy}
|
||||
>
|
||||
<RefreshCw size={16} className={cn(activeSystem === "work-orders" && busy && "animate-spin")} />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" aria-label="关闭外围工单测试" onClick={onClose}>
|
||||
<Button size="icon" variant="ghost" aria-label="关闭外围系统" onClick={onClose}>
|
||||
<X size={17} />
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-3 sm:p-4">
|
||||
<div className="mb-3 grid gap-2">
|
||||
<WorkbenchNotice tone="info" title="合成工单环境">{DISCLAIMER}</WorkbenchNotice>
|
||||
{!client.configured ? (
|
||||
<WorkbenchNotice tone="warning">
|
||||
未配置 TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL,当前不会连接外围工单服务。
|
||||
</WorkbenchNotice>
|
||||
) : null}
|
||||
{error ? <WorkbenchNotice role="alert" tone="danger">{error}</WorkbenchNotice> : null}
|
||||
{client.configured && !context ? (
|
||||
<WorkbenchNotice role="status" tone="neutral">正在核验项目授权与工单接口。</WorkbenchNotice>
|
||||
) : null}
|
||||
<Tabs
|
||||
value={activeSystem}
|
||||
onValueChange={(value) => setActiveSystem(value as "work-orders" | "scada")}
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
>
|
||||
<div className="shrink-0 border-b border-[var(--surface-divider)] px-3 py-2 sm:px-4">
|
||||
<TabsList aria-label="外围系统类型" className="grid w-full grid-cols-2 bg-[var(--surface-control)]">
|
||||
<TabsTrigger value="work-orders"><ClipboardList size={14} />工单系统</TabsTrigger>
|
||||
<TabsTrigger value="scada"><Activity size={14} />SCADA 系统</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{context ? (
|
||||
<div className="space-y-3">
|
||||
<ManagedTestResources
|
||||
client={client}
|
||||
canWrite={context.can_write}
|
||||
capabilities={context.managed_resources}
|
||||
refreshGeneration={refreshGeneration}
|
||||
onAgentTrigger={onAgentTrigger}
|
||||
/>
|
||||
<Disclosure
|
||||
label={<span className="inline-flex items-center gap-2"><ReceiptText size={14} />按 request_id 找回提交结果</span>}
|
||||
className="rounded-[14px] border border-slate-200/90 bg-[var(--surface-reading)] px-3.5 shadow-[0_1px_2px_rgba(15,23,42,0.06)]"
|
||||
>
|
||||
<p className="pb-2 text-xs leading-5 text-slate-500">
|
||||
结果未知时只查询原请求,不会重复创建或更新工单。
|
||||
</p>
|
||||
<div className="flex gap-2 pb-3">
|
||||
<Input
|
||||
aria-label="提交请求号"
|
||||
value={receiptRequestId}
|
||||
onChange={(event) => setReceiptRequestId(event.target.value.trim())}
|
||||
placeholder="request_id"
|
||||
className="h-10 min-w-0 flex-1 rounded-lg border-[var(--surface-divider)] bg-[var(--surface-reading)] font-mono text-xs md:text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!receiptRequestId || busy}
|
||||
onClick={async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await client.getReceipt(receiptRequestId);
|
||||
setError("回执已确认,请刷新工单列表查看最新状态。");
|
||||
} catch (nextError) {
|
||||
setError(errorMessage(nextError));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}}
|
||||
>查询</Button>
|
||||
</div>
|
||||
</Disclosure>
|
||||
<TabsContent value="work-orders" className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-3 sm:p-4">
|
||||
<div className="mb-3 grid gap-2">
|
||||
<WorkbenchNotice tone="info" title="合成工单环境">{DISCLAIMER}</WorkbenchNotice>
|
||||
{!client.configured ? (
|
||||
<WorkbenchNotice tone="warning">
|
||||
未配置 TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL,当前不会连接外围工单服务。
|
||||
</WorkbenchNotice>
|
||||
) : null}
|
||||
{error ? <WorkbenchNotice role="alert" tone="danger">{error}</WorkbenchNotice> : null}
|
||||
{client.configured && !context ? (
|
||||
<WorkbenchNotice role="status" tone="neutral">正在核验项目授权与工单接口。</WorkbenchNotice>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{context ? (
|
||||
<div className="space-y-3">
|
||||
<ManagedTestResources
|
||||
client={client}
|
||||
canWrite={context.can_write}
|
||||
capabilities={context.managed_resources}
|
||||
refreshGeneration={refreshGeneration}
|
||||
onAgentTrigger={onAgentTrigger}
|
||||
/>
|
||||
<Disclosure
|
||||
label={<span className="inline-flex items-center gap-2"><ReceiptText size={14} />按 request_id 找回提交结果</span>}
|
||||
className="rounded-[14px] border border-slate-200/90 bg-[var(--surface-reading)] px-3.5 shadow-[0_1px_2px_rgba(15,23,42,0.06)]"
|
||||
>
|
||||
<p className="pb-2 text-xs leading-5 text-slate-500">
|
||||
结果未知时只查询原请求,不会重复创建或更新工单。
|
||||
</p>
|
||||
<div className="flex gap-2 pb-3">
|
||||
<Input
|
||||
aria-label="提交请求号"
|
||||
value={receiptRequestId}
|
||||
onChange={(event) => setReceiptRequestId(event.target.value.trim())}
|
||||
placeholder="request_id"
|
||||
className="h-10 min-w-0 flex-1 rounded-lg border-[var(--surface-divider)] bg-[var(--surface-reading)] font-mono text-xs md:text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!receiptRequestId || busy}
|
||||
onClick={async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await client.getReceipt(receiptRequestId);
|
||||
setError("回执已确认,请刷新工单列表查看最新状态。");
|
||||
} catch (nextError) {
|
||||
setError(errorMessage(nextError));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}}
|
||||
>查询</Button>
|
||||
</div>
|
||||
</Disclosure>
|
||||
</div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="scada" className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-3 sm:p-4">
|
||||
<ScadaSystemSection
|
||||
active={activeSystem === "scada"}
|
||||
getAccessToken={getAccessToken}
|
||||
refreshGeneration={scadaRefreshGeneration}
|
||||
peripheralClient={client}
|
||||
canSimulate={Boolean(
|
||||
context?.can_write && context.managed_resources.test_data === "available"
|
||||
)}
|
||||
onAgentTrigger={onScadaAnomalyTrigger}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user