diff --git a/web-ui/src/app/cases/[caseNumber]/page.tsx b/web-ui/src/app/cases/[caseNumber]/page.tsx index 26f8c15..ef92085 100644 --- a/web-ui/src/app/cases/[caseNumber]/page.tsx +++ b/web-ui/src/app/cases/[caseNumber]/page.tsx @@ -39,14 +39,16 @@ export default function CaseDetailPage({ params: Promise<{ caseNumber: string }>; }) { const { caseNumber } = use(params); - const { data, isPending, error } = useCase(caseNumber); + const { data, isPending, error, refetch } = useCase(caseNumber); const startWorkflow = useStartWorkflow(caseNumber); const canStartWorkflow = data?.status === "new" || data?.status === "documents_ready"; const expectedOutcomeLabel = data?.expected_outcome ? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome : null; - if (error) { + // Only take over the whole page when there is NO data to show. A transient + // 5xx on the 5s background refetch must not blow away an already-loaded page. + if (error && !data) { return (
@@ -54,9 +56,14 @@ export default function CaseDetailPage({

שגיאה בטעינת התיק

{error.message}

- +
+ + +
diff --git a/web-ui/src/components/cases/agent-activity-feed.tsx b/web-ui/src/components/cases/agent-activity-feed.tsx index 394f2bc..87dfd45 100644 --- a/web-ui/src/components/cases/agent-activity-feed.tsx +++ b/web-ui/src/components/cases/agent-activity-feed.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRef, useState, useEffect } from "react"; +import { useRef, useState, useEffect, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; @@ -151,7 +151,7 @@ function CommentCard({ {identifier} )} - + {timeAgo(comment.created_at)} @@ -267,7 +267,7 @@ function AskUserQuestionsForm({
{q.prompt} - {(q.required ?? true) && *} + {(q.required ?? true) && *}
{q.options.map((opt) => { @@ -316,7 +316,7 @@ function AskUserQuestionsForm({ {pending ? ( ) : ( - + )} {interaction.payload.submitLabel || "שלח תשובה"} @@ -397,14 +397,14 @@ function RequestConfirmationForm({ onClick={handleReject} disabled={pending || (requireReason && !reason.trim())} > - + {rejectLabel} @@ -489,7 +489,7 @@ function SuggestTasksForm({ onClick={() => (showReason ? onReject(reason.trim()) : setShowReason(true))} disabled={pending} > - + {showReason ? "אישור דחייה" : "דחייה"} @@ -575,7 +575,7 @@ function InteractionCard({ {identifier} )} - + {timeAgo(interaction.resolved_at ?? interaction.created_at)} @@ -635,13 +635,13 @@ export function AgentActivityFeed({ const [body, setBody] = useState(""); const endRef = useRef(null); - // Build issue_id → identifier map - const issueMap = new Map(); - if (data?.issues) { - for (const iss of data.issues) { - issueMap.set(iss.id, iss.identifier); - } - } + // Build issue_id → identifier map (memoized — the feed refetches every 10s, + // and a fresh Map each render would defeat the child cards' memoization). + const issueMap = useMemo(() => { + const m = new Map(); + for (const iss of data?.issues ?? []) m.set(iss.id, iss.identifier); + return m; + }, [data?.issues]); // Auto-scroll on new comments or interactions const commentCount = data?.comments?.length ?? 0; @@ -669,7 +669,7 @@ export function AgentActivityFeed({ if (isLoading) { return (
- + טוען פעילות סוכנים...
); @@ -777,6 +777,7 @@ export function AgentActivityFeed({ value={body} onChange={(e) => setBody(e.target.value)} placeholder="כתוב הוראה לסוכנים..." + aria-label="הוראה לסוכנים" className="min-h-[60px] resize-none text-sm" dir="rtl" onKeyDown={(e) => { @@ -798,7 +799,7 @@ export function AgentActivityFeed({ {sendComment.isPending ? ( ) : ( - + )} שלח diff --git a/web-ui/src/components/cases/agent-status-widget.tsx b/web-ui/src/components/cases/agent-status-widget.tsx index 499addd..86f4ee8 100644 --- a/web-ui/src/components/cases/agent-status-widget.tsx +++ b/web-ui/src/components/cases/agent-status-widget.tsx @@ -33,7 +33,7 @@ function AgentRow({ agent }: { agent: PaperclipAgentStatus }) { className={`w-2 h-2 rounded-full flex-shrink-0 ${statusDot(agent.status)}`} /> {agent.name} - + {STATUS_LABEL[agent.status] ?? agent.status}
diff --git a/web-ui/src/components/cases/case-edit-dialog.tsx b/web-ui/src/components/cases/case-edit-dialog.tsx index c1e0ae4..2af1207 100644 --- a/web-ui/src/components/cases/case-edit-dialog.tsx +++ b/web-ui/src/components/cases/case-edit-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { toast } from "sonner"; @@ -54,22 +54,26 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) { }, }); - /* Re-sync the form when the underlying case refetches after save */ - useEffect(() => { - if (!open) return; - form.reset({ - title: data.title ?? "", - subject: data.subject ?? "", - hearing_date: data.hearing_date ?? "", - notes: "", - expected_outcome: data.expected_outcome ?? "", - appellants: data.appellants ?? [], - respondents: data.respondents ?? [], - property_address: data.property_address ?? "", - permit_number: data.permit_number ?? "", - proceeding_type: data.proceeding_type ?? "ערר", - }); - }, [open, data, form]); + /* Reset to the latest case values only on the open→true transition. + * Resetting on every `data` change would clobber in-progress edits, because + * useCase refetches every 5s (refetchInterval) while the dialog is open. */ + const handleOpenChange = (next: boolean) => { + if (next) { + form.reset({ + title: data.title ?? "", + subject: data.subject ?? "", + hearing_date: data.hearing_date ?? "", + notes: "", + expected_outcome: data.expected_outcome ?? "", + appellants: data.appellants ?? [], + respondents: data.respondents ?? [], + property_address: data.property_address ?? "", + permit_number: data.permit_number ?? "", + proceeding_type: data.proceeding_type ?? "ערר", + }); + } + setOpen(next); + }; const onSubmit = form.handleSubmit(async (values) => { try { @@ -82,7 +86,7 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) { }); return ( - + סטטוס הריצה — בדף{" "} - + התפעול - + )} @@ -628,7 +629,7 @@ function CitationsSection({ caseNumber }: { caseNumber: string }) {
{data.linked.map((c) => ( - בספרייה - + ))} {data.missing.map((c) => (
(currentStatus ?? ""); + // `null` = untouched → the dropdown tracks the live `currentStatus` (which + // arrives async and changes on the 5s poll / external updates). Only an + // explicit pick overrides it, until save resets back to tracking. + const [picked, setPicked] = useState(null); const mutate = useUpdateCase(caseNumber); + const effective: CaseStatus | "" = picked ?? currentStatus ?? ""; + const canSave = Boolean(effective) && effective !== currentStatus; const handleSave = async () => { - if (!selected || selected === currentStatus) return; + if (!canSave || !effective) return; try { - await mutate.mutateAsync({ status: selected }); - toast.success(`הסטטוס עודכן ל${STATUS_LABELS[selected]}`); + await mutate.mutateAsync({ status: effective }); + toast.success(`הסטטוס עודכן ל${STATUS_LABELS[effective]}`); + setPicked(null); } catch (e) { toast.error(e instanceof Error ? e.message : "שגיאה בעדכון הסטטוס"); } @@ -43,8 +49,8 @@ export function StatusChanger({