PromptDrawer.tsx 2.6 KB

12345678910111213141516171819202122232425262728293031
  1. "use client";
  2. import { useEffect, useRef } from "react";
  3. import { X } from "lucide-react";
  4. import type { PromptProjection } from "@/lib/types";
  5. import { RichText } from "@/components/ui/RichText";
  6. export function PromptDrawer({ prompt, loading, error, onClose }: { prompt?: PromptProjection; loading: boolean; error?: string; onClose: () => void }) {
  7. const close = useRef<HTMLButtonElement>(null);
  8. const dialog = useRef<HTMLElement>(null);
  9. useEffect(() => { close.current?.focus(); }, []);
  10. useEffect(() => { const handler = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [onClose]);
  11. const trapFocus = (event: React.KeyboardEvent<HTMLElement>) => {
  12. if (event.key !== "Tab") return;
  13. const focusable = [...(dialog.current?.querySelectorAll<HTMLElement>('button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])') || [])];
  14. if (!focusable.length) return;
  15. const first = focusable[0]; const last = focusable[focusable.length - 1];
  16. if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
  17. else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
  18. };
  19. return <aside ref={dialog} className="promptDrawer" role="dialog" aria-modal="true" aria-label="Agent 任务与提示词" onKeyDown={trapFocus} onWheelCapture={(event) => event.stopPropagation()}>
  20. <header><div><span>Agent 提示词</span><h2>{prompt?.actor.label || "Agent"}</h2></div><button ref={close} className="iconButton" type="button" aria-label="关闭提示词" onClick={onClose}><X size={18} /></button></header>
  21. <div className="promptDrawerBody">
  22. {loading ? <div className="skeletonLines"><i /><i /><i /></div> : null}
  23. {error ? <section className="inspectorPanel isMissing"><h3>暂时无法读取</h3><p>{error}</p></section> : null}
  24. {prompt?.notices.map((notice) => <section className="inspectorPanel promptAccuracy" key={notice.code}><h3>准确性说明</h3><p>{notice.message}</p></section>)}
  25. {prompt?.exactTask ? <section className="inspectorPanel"><h3>本次真实任务</h3><RichText value={prompt.exactTask.content} /></section> : null}
  26. {prompt?.systemPrompt ? <section className="inspectorPanel currentPrompt"><h3>当前提示词</h3><p className="promptMeta">{prompt.systemPrompt.source === "current-db" ? "当前数据库版本" : "当前代码文件"}{prompt.systemPrompt.version ? ` · v${prompt.systemPrompt.version}` : ""}</p><RichText value={prompt.systemPrompt.content} /></section> : null}
  27. </div>
  28. </aside>;
  29. }