diff --git a/web/src/app/(main)/function/llm-records/page.tsx b/web/src/app/(main)/function/llm-records/page.tsx index 5f05618..28dbe6a 100644 --- a/web/src/app/(main)/function/llm-records/page.tsx +++ b/web/src/app/(main)/function/llm-records/page.tsx @@ -29,21 +29,9 @@ import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { HttpPacketSyntax } from "@/components/http-code-block"; import { cn } from "@/lib/utils"; import { api } from "@/lib/api"; import type { LLMRecordItem, LLMRecordDetail, LLMTask } from "@/lib/types"; @@ -80,7 +68,12 @@ function tryFormatJSON(s: string): string { function CopyButton({ text }: { text: string }) { const [copied, setCopied] = React.useState(false); const timer = React.useRef | null>(null); - React.useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []); + React.useEffect( + () => () => { + if (timer.current) clearTimeout(timer.current); + }, + [], + ); const disabled = !text; const copy = async () => { @@ -95,7 +88,11 @@ function CopyButton({ text }: { text: string }) { ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); - try { document.execCommand("copy"); } catch { /* 忽略:不支持则静默 */ } + try { + document.execCommand("copy"); + } catch { + /* 忽略:不支持则静默 */ + } document.body.removeChild(ta); } setCopied(true); @@ -151,9 +148,7 @@ export default function LLMRecordsPage() { const reqText = showRaw ? detail?.raw_request && tryFormatJSON(detail.raw_request) : detail?.request_body && tryFormatJSON(detail.request_body); - const respText = showRaw - ? detail?.raw_response - : detail?.response_body && tryFormatJSON(detail.response_body); + const respText = showRaw ? detail?.raw_response : detail?.response_body && tryFormatJSON(detail.response_body); // Per-task delete (task picker + confirm dialog) const [tasks, setTasks] = React.useState([]); @@ -167,9 +162,13 @@ export default function LLMRecordsPage() { let alive = true; api .settings() - .then((s) => { if (alive) setRecEnabled(!!s.llm_record); }) + .then((s) => { + if (alive) setRecEnabled(!!s.llm_record); + }) .catch(() => {}); - return () => { alive = false; }; + return () => { + alive = false; + }; }, []); const toggleRecording = async (on: boolean) => { @@ -201,7 +200,13 @@ export default function LLMRecordsPage() { let alive = true; setLoading(true); api - .llmRecords({ model: model || undefined, session: sessionQ || undefined, task: pickedTask || undefined, page, size }) + .llmRecords({ + model: model || undefined, + session: sessionQ || undefined, + task: pickedTask || undefined, + page, + size, + }) .then((r) => { if (!alive) return; setRecords(r.records ?? []); @@ -211,9 +216,13 @@ export default function LLMRecordsPage() { .finally(() => alive && setLoading(false)); api .llmTasks() - .then((r) => { if (alive) setTasks(r.tasks ?? []); }) + .then((r) => { + if (alive) setTasks(r.tasks ?? []); + }) .catch(() => {}); - return () => { alive = false; }; + return () => { + alive = false; + }; }, [page, size, sessionQ, model, pickedTask, reloadTick]); // Delete every LLM record for the picked task, then refetch. @@ -244,10 +253,16 @@ export default function LLMRecordsPage() { setDetail(null); api .llmRecordDetail(selected.id) - .then((d) => { if (alive) setDetail(d); }) + .then((d) => { + if (alive) setDetail(d); + }) .catch(() => {}) - .finally(() => { if (alive) setDetailLoading(false); }); - return () => { alive = false; }; + .finally(() => { + if (alive) setDetailLoading(false); + }); + return () => { + alive = false; + }; }, [selected]); const totalPages = Math.max(1, Math.ceil(total / size)); @@ -276,12 +291,7 @@ export default function LLMRecordsPage() { className="h-8 pl-8" /> - setModel(e.target.value)} - /> + setModel(e.target.value)} /> setQuery(event.target.value)} + placeholder="搜索报文内容" + aria-label="搜索报文内容" + className="min-w-0 flex-1 bg-transparent font-mono text-xs outline-none placeholder:text-muted-foreground/60" + /> + {query.trim() ? ( + - - {renderLine(line, index)} - - - ))} + {matches} 处命中 + + ) : null} ); diff --git a/web/src/components/repeater/repeater-editor.tsx b/web/src/components/repeater/repeater-editor.tsx index 7f1fc9c..d6f4563 100644 --- a/web/src/components/repeater/repeater-editor.tsx +++ b/web/src/components/repeater/repeater-editor.tsx @@ -154,6 +154,7 @@ export function RepeaterEditor({ tab }: { tab: RepeaterTab }) { onChange={changeRequest} disabled={busy} placeholder="POST /path HTTP/1.1" + syntaxHighlight />
{response?.error ? ( @@ -166,6 +167,7 @@ export function RepeaterEditor({ tab }: { tab: RepeaterTab }) { label="响应" value={response ? buildRawResponse(response) : ""} placeholder="发送后显示原始响应" + syntaxHighlight />
diff --git a/web/src/components/repeater/repeater-text-pane.tsx b/web/src/components/repeater/repeater-text-pane.tsx index 8819074..1bd0342 100644 --- a/web/src/components/repeater/repeater-text-pane.tsx +++ b/web/src/components/repeater/repeater-text-pane.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import { CopyButton } from "@/components/copy-button"; +import { HttpPacketSyntax } from "@/components/http-code-block"; import { Textarea } from "@/components/ui/textarea"; export function RepeaterTextPane({ @@ -12,6 +13,7 @@ export function RepeaterTextPane({ onChange, disabled, placeholder, + syntaxHighlight = false, }: { id: string; label: string; @@ -19,8 +21,10 @@ export function RepeaterTextPane({ onChange?: (value: string) => void; disabled?: boolean; placeholder?: string; + syntaxHighlight?: boolean; }) { const gutter = React.useRef(null); + const syntax = React.useRef(null); const lines = React.useMemo(() => value.split(/\r\n|\r|\n/).length, [value]); const numbers = React.useMemo(() => Array.from({ length: lines }, (_, index) => index + 1).join("\n"), [lines]); return ( @@ -32,7 +36,7 @@ export function RepeaterTextPane({ {lines} 行 -
+
); diff --git a/web/src/components/traffic-evidence-viewer.tsx b/web/src/components/traffic-evidence-viewer.tsx index f614f27..812c473 100644 --- a/web/src/components/traffic-evidence-viewer.tsx +++ b/web/src/components/traffic-evidence-viewer.tsx @@ -13,7 +13,6 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { api } from "@/lib/api"; import type { FindingTrafficDetail, TrafficDetail } from "@/lib/types"; @@ -114,17 +113,16 @@ export function TrafficEvidenceViewer({ {error} ) : detail ? ( - - - 请求 Request - 响应 Response - +
{(["request", "response"] as const).map((side) => ( - +
- - 正文 {detail[side].total.toLocaleString()} 字节{detail[side].truncated ? " · 当前为预览" : ""} - +
+

{side === "request" ? "请求 Request" : "响应 Response"}

+ + 正文 {detail[side].total.toLocaleString()} 字节{detail[side].truncated ? " · 当前为预览" : ""} + +
) : null} - +
))} - +
) : ( )} @@ -193,18 +191,16 @@ export function CapturedTrafficViewer({ id, onClose }: { id: string | null; onCl {error} ) : detail ? ( - - - 请求 Request - 响应 Response - - +
+
+

请求 Request

- - +
+
+

响应 Response

- - +
+
) : ( )}