All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 32s
Move draft management (export DOCX, download, upload revised version, mark final) and chair feedback into a new "טיוטות והערות" tab on the case detail page. Remove the standalone /feedback page and its nav link since feedback is now case-scoped. Also fix /api/admin/skills 500 error when Paperclip DB is unreachable by adding a connection timeout and graceful fallback to disk-only skills. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
3.9 KiB
TypeScript
136 lines
3.9 KiB
TypeScript
/**
|
||
* Chair feedback hooks — recording and managing Dafna's feedback on drafts.
|
||
*/
|
||
|
||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { apiRequest } from "./client";
|
||
|
||
export type FeedbackCategory =
|
||
| "missing_content"
|
||
| "wrong_tone"
|
||
| "wrong_structure"
|
||
| "factual_error"
|
||
| "style"
|
||
| "other";
|
||
|
||
export type ChairFeedback = {
|
||
id: string;
|
||
case_id: string | null;
|
||
case_number: string;
|
||
block_id: string;
|
||
category: FeedbackCategory;
|
||
feedback_text: string;
|
||
lesson_extracted: string;
|
||
resolved: boolean;
|
||
applied_to: string[];
|
||
created_at: string | null;
|
||
};
|
||
|
||
export type CreateFeedbackInput = {
|
||
case_number?: string;
|
||
block_id?: string;
|
||
feedback_text: string;
|
||
category?: FeedbackCategory;
|
||
lesson_extracted?: string;
|
||
};
|
||
|
||
const feedbackKeys = {
|
||
all: ["feedback"] as const,
|
||
list: (filters: { category?: string; unresolved_only?: boolean }) =>
|
||
[...feedbackKeys.all, "list", filters] as const,
|
||
};
|
||
|
||
export function useFeedbackList(filters: {
|
||
category?: string;
|
||
unresolved_only?: boolean;
|
||
} = {}) {
|
||
const params = new URLSearchParams();
|
||
if (filters.category) params.set("category", filters.category);
|
||
if (filters.unresolved_only) params.set("unresolved_only", "true");
|
||
const qs = params.toString();
|
||
|
||
return useQuery({
|
||
queryKey: feedbackKeys.list(filters),
|
||
queryFn: ({ signal }) =>
|
||
apiRequest<ChairFeedback[]>(`/api/feedback${qs ? `?${qs}` : ""}`, { signal }),
|
||
});
|
||
}
|
||
|
||
/** Feedback filtered by case number */
|
||
export function useCaseFeedback(caseNumber: string | undefined) {
|
||
const params = caseNumber ? `?case_number=${caseNumber}` : "";
|
||
return useQuery({
|
||
queryKey: [...feedbackKeys.all, "case", caseNumber ?? ""] as const,
|
||
queryFn: ({ signal }) =>
|
||
apiRequest<ChairFeedback[]>(`/api/feedback${params}`, { signal }),
|
||
enabled: Boolean(caseNumber),
|
||
staleTime: 5_000,
|
||
refetchInterval: 5_000,
|
||
});
|
||
}
|
||
|
||
export function useCreateFeedback() {
|
||
const qc = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: (data: CreateFeedbackInput) =>
|
||
apiRequest<{ id: string; status: string }>("/api/feedback/json", {
|
||
method: "POST",
|
||
body: data,
|
||
}),
|
||
onSuccess: () => {
|
||
qc.invalidateQueries({ queryKey: feedbackKeys.all });
|
||
},
|
||
});
|
||
}
|
||
|
||
export function useResolveFeedback() {
|
||
const qc = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: ({
|
||
feedbackId,
|
||
applied_to,
|
||
}: {
|
||
feedbackId: string;
|
||
applied_to: string[];
|
||
}) =>
|
||
apiRequest<{ status: string }>(
|
||
`/api/feedback/${feedbackId}/resolve`,
|
||
{ method: "PATCH", body: { applied_to } },
|
||
),
|
||
onSuccess: () => {
|
||
qc.invalidateQueries({ queryKey: feedbackKeys.all });
|
||
},
|
||
});
|
||
}
|
||
|
||
/** Hebrew labels for feedback categories */
|
||
export const CATEGORY_LABELS: Record<FeedbackCategory, string> = {
|
||
missing_content: "תוכן חסר",
|
||
wrong_tone: "טון שגוי",
|
||
wrong_structure: "מבנה שגוי",
|
||
factual_error: "שגיאה עובדתית",
|
||
style: "סגנון",
|
||
other: "אחר",
|
||
};
|
||
|
||
/** Tailwind color classes per category */
|
||
export const CATEGORY_COLORS: Record<FeedbackCategory, string> = {
|
||
missing_content: "bg-amber-100 text-amber-800 border-amber-200",
|
||
wrong_tone: "bg-purple-100 text-purple-800 border-purple-200",
|
||
wrong_structure: "bg-blue-100 text-blue-800 border-blue-200",
|
||
factual_error: "bg-red-100 text-red-800 border-red-200",
|
||
style: "bg-emerald-100 text-emerald-800 border-emerald-200",
|
||
other: "bg-gray-100 text-gray-800 border-gray-200",
|
||
};
|
||
|
||
/** Block ID labels */
|
||
export const BLOCK_LABELS: Record<string, string> = {
|
||
"block-he": "ה — פתיחה",
|
||
"block-vav": "ו — רקע עובדתי",
|
||
"block-zayin": "ז — טענות הצדדים",
|
||
"block-chet": "ח — הליכים",
|
||
"block-tet": "ט — תכניות חלות",
|
||
"block-yod": "י — דיון והכרעה",
|
||
"block-yod-alef": "יא — סיכום",
|
||
};
|