Compare commits

..

No commits in common. "1a1994ca1785c95ecc34810cc57c29e6925688f5" and "ea246384bf122d687547306b54c3fed3396a88b7" have entirely different histories.

16 changed files with 42 additions and 176 deletions

View File

@ -8,6 +8,8 @@ class ReviewMetadata(BaseModel):
description="A short title characterizing what took place and where, under 10 words."
)
scene: str = Field(
min_length=120,
max_length=600,
description="A chronological narrative of what happens from start to finish.",
)
shortSummary: str = Field(

View File

@ -151,18 +151,6 @@ Each line represents a detection state, not necessarily unique individuals. The
if "other_concerns" in schema.get("required", []):
schema["required"].remove("other_concerns")
# Length hints injected into the schema as suggestions to the model
# (enforced by grammar-based providers like llama.cpp) but kept off the
# Pydantic model so a non-compliant response does not fail validation.
length_hints = {
"scene": {"minLength": 120, "maxLength": 600},
"shortSummary": {"minLength": 70, "maxLength": 100},
}
for field, hints in length_hints.items():
prop = schema.get("properties", {}).get(field)
if prop is not None:
prop.update(hints)
# OpenAI strict mode requires additionalProperties: false on all objects
schema["additionalProperties"] = False

View File

@ -1,114 +1,4 @@
import { test, expect } from "../fixtures/frigate-test";
import {
expectBodyInteractive,
waitForBodyInteractive,
} from "../helpers/overlay-interaction";
test.describe("Export Page - Delete race @high", () => {
// Empirical guard for radix-ui/primitives#3445: when a modal DropdownMenu
// opens an AlertDialog and the AlertDialog's confirm action causes the
// parent's optimistic cache update to unmount the card, we want to know
// whether the deduped react-dismissable-layer (1.1.11) handles the
// pointer-events stack cleanup or whether `modal={false}` is still
// required on the DropdownMenu. The classic "canonical" pattern, distinct
// from the FaceSelectionDialog auto-unmount race already covered by
// face-library.spec.ts.
test("deleting an export via dropdown→alert→confirm leaves body interactive", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
const initialExports = [
{
id: "export-race-001",
camera: "front_door",
name: "Race - Test Export",
date: 1775490731.3863528,
video_path: "/exports/export-race-001.mp4",
thumb_path: "/exports/export-race-001-thumb.jpg",
in_progress: false,
export_case_id: null,
},
];
let deleted = false;
await frigateApp.installDefaults({
exports: initialExports,
});
// Flip /api/export to empty after the delete POST is observed so the
// page's SWR mutate sees the export gone.
await frigateApp.page.route("**/api/export**", async (route) => {
const payload = deleted ? [] : initialExports;
await route.fulfill({ json: payload });
});
await frigateApp.page.route("**/api/exports/delete", async (route) => {
deleted = true;
const delayMs = Number(
(globalThis as { process?: { env?: Record<string, string> } }).process
?.env?.DELETE_DELAY_MS ?? "100",
);
if (delayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
await route.fulfill({ json: { success: true } });
});
await frigateApp.goto("/export");
await expect(frigateApp.page.getByText("Race - Test Export")).toBeVisible({
timeout: 5_000,
});
// Open the kebab menu on the export card. The kebab uses the
// (misleading) aria-label "Edit name" from ExportCard's source — it
// wraps the FiMoreVertical icon. There is exactly one such button on
// the page once we have a single export rendered.
const kebab = frigateApp.page
.getByRole("button", { name: /edit name/i })
.first();
await expect(kebab).toBeVisible({ timeout: 5_000 });
await kebab.click();
const menu = frigateApp.page
.locator('[role="menu"], [data-radix-menu-content]')
.first();
await expect(menu).toBeVisible({ timeout: 3_000 });
// Delete Export
await menu
.getByRole("menuitem", { name: /delete export/i })
.first()
.click();
// AlertDialog at page level. The confirm button's accessible name is
// "Delete Export" (its aria-label), the visible text is just "Delete".
const confirm = frigateApp.page.getByRole("alertdialog");
await expect(confirm).toBeVisible({ timeout: 3_000 });
await confirm
.getByRole("button", { name: /^delete export$/i })
.first()
.click();
// The card optimistically disappears, the dialog closes, and body
// pointer-events must come unstuck.
await expect(
frigateApp.page.getByText("Race - Test Export"),
).not.toBeVisible({ timeout: 5_000 });
await waitForBodyInteractive(frigateApp.page, 5_000);
await expectBodyInteractive(frigateApp.page);
// Sanity: another page-level button still responds.
const newCase = frigateApp.page.getByRole("button", { name: /new case/i });
await expect(newCase).toBeVisible({ timeout: 3_000 });
await newCase.click();
await expect(
frigateApp.page.getByRole("dialog").filter({ hasText: /create case/i }),
).toBeVisible({ timeout: 3_000 });
});
});
test.describe("Export Page - Overview @high", () => {
test("renders uncategorized exports and case cards from mock data", async ({

View File

@ -472,9 +472,7 @@ test.describe("FaceSelectionDialog @high", () => {
await groupedImage.locator("xpath=..").click();
const dialog = frigateApp.page
.getByRole("dialog")
.filter({
has: frigateApp.page.locator('img[src*="clips/faces/train/"]'),
})
.filter({ has: frigateApp.page.locator('img[src*="clips/faces/train/"]') })
.first();
await expect(dialog).toBeVisible({ timeout: 5_000 });

View File

@ -257,7 +257,6 @@
"export": "Export",
"actions": "Actions",
"uiPlayground": "UI Playground",
"features": "Features",
"faceLibrary": "Face Library",
"classification": "Classification",
"chat": "Chat",

View File

@ -161,13 +161,13 @@ export function AnimatedEventCard({
<TooltipTrigger asChild>
<Button
className={cn(
"absolute left-2 top-1 z-40 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 transition-opacity",
"absolute left-2 top-1 z-40 transition-opacity",
threatLevel === ThreatLevel.SECURITY_CONCERN &&
"pointer-events-auto opacity-100",
"pointer-events-auto bg-severity_alert opacity-100 hover:bg-severity_alert",
threatLevel === ThreatLevel.NEEDS_REVIEW &&
"pointer-events-auto opacity-100",
"pointer-events-auto bg-severity_detection opacity-100 hover:bg-severity_detection",
threatLevel === ThreatLevel.NORMAL &&
"pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100",
"pointer-events-none bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 opacity-0 group-hover:pointer-events-auto group-hover:opacity-100",
)}
size="xs"
aria-label={t("markAsReviewed")}

View File

@ -266,7 +266,7 @@ export function ExportCard({
)}
{!exportedRecording.in_progress && !selectionMode && (
<div className="absolute bottom-2 right-3 z-40">
<DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger>
<BlurredIconButton
aria-label={t("tooltip.editName")}

View File

@ -275,7 +275,7 @@ export default function ReviewCard({
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<ContextMenu key={event.id}>
<ContextMenu key={event.id} modal={false}>
<ContextMenuTrigger asChild>{content}</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>

View File

@ -14,6 +14,7 @@ import Step3ChooseExamples, {
Step3FormData,
} from "./wizard/Step3ChooseExamples";
import { cn } from "@/lib/utils";
import { isDesktop } from "react-device-detect";
import axios from "axios";
const OBJECT_STEPS = [
@ -152,9 +153,13 @@ export default function ClassificationModelWizardDialog({
>
<DialogContent
className={cn(
"scrollbar-container max-h-[90%] overflow-y-auto",
wizardState.currentStep == 0 && "xl:max-h-[80%]",
wizardState.currentStep > 0 && "md:max-w-[70%] xl:max-h-[80%]",
"",
isDesktop &&
wizardState.currentStep == 0 &&
"max-h-[90%] overflow-y-auto xl:max-h-[80%]",
isDesktop &&
wizardState.currentStep > 0 &&
"max-h-[90%] max-w-[70%] overflow-y-auto xl:max-h-[80%]",
)}
onInteractOutside={(e) => {
e.preventDefault();

View File

@ -6,7 +6,6 @@ import {
LuLifeBuoy,
LuList,
LuLogOut,
LuMessageSquare,
LuMoon,
LuSquarePen,
LuScanFace,
@ -483,25 +482,21 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
</Link>
</>
)}
</DropdownMenuGroup>
{isMobile && isAdmin && (
<>
<DropdownMenuLabel className="mt-1">
{t("menu.features")}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup className="flex flex-col">
{config?.face_recognition.enabled && (
<Link to="/faces">
<MenuItem
className="flex w-full items-center p-2 text-sm"
aria-label={t("menu.faceLibrary")}
>
<LuScanFace className="mr-2 size-4" />
<span>{t("menu.faceLibrary")}</span>
</MenuItem>
</Link>
)}
{isAdmin && isMobile && config?.face_recognition.enabled && (
<>
<Link to="/faces">
<MenuItem
className="flex w-full items-center p-2 text-sm"
aria-label={t("menu.faceLibrary")}
>
<LuScanFace className="mr-2 size-4" />
<span>{t("menu.faceLibrary")}</span>
</MenuItem>
</Link>
</>
)}
{isAdmin && isMobile && (
<>
<Link to="/classification">
<MenuItem
className="flex w-full items-center p-2 text-sm"
@ -511,20 +506,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
<span>{t("menu.classification")}</span>
</MenuItem>
</Link>
{config?.genai?.model !== "none" && (
<Link to="/chat">
<MenuItem
className="flex w-full items-center p-2 text-sm"
aria-label={t("menu.chat")}
>
<LuMessageSquare className="mr-2 size-4" />
<span>{t("menu.chat")}</span>
</MenuItem>
</Link>
)}
</DropdownMenuGroup>
</>
)}
</>
)}
</DropdownMenuGroup>
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
{t("menu.appearance")}
</DropdownMenuLabel>

View File

@ -272,7 +272,7 @@ export default function LiveContextMenu({
return (
<div className={cn("w-full", className)}>
<ContextMenu key={camera} onOpenChange={handleOpenChange}>
<ContextMenu key={camera} modal={false} onOpenChange={handleOpenChange}>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent>
<div className="flex flex-col items-start gap-1 py-1 pl-2">

View File

@ -258,13 +258,13 @@ export default function SearchResultActions({
</AlertDialogContent>
</AlertDialog>
{isContextMenu ? (
<ContextMenu>
<ContextMenu modal={false}>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent>{menuItems}</ContextMenuContent>
</ContextMenu>
) : (
<>
<DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<BlurredIconButton aria-label={t("itemMenu.more.aria")}>
<FiMoreVertical className="size-5" />

View File

@ -22,7 +22,7 @@ export default function ActionsDropdown({
const { t } = useTranslation(["components/dialog", "views/replay", "common"]);
return (
<DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
className="flex items-center gap-2"

View File

@ -594,7 +594,7 @@ function LibrarySelector({
forbiddenErrorMessage={t("description.nameCannotContainHash")}
/>
<DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button className="flex justify-between smart-capitalize">
{pageTitle}

View File

@ -342,7 +342,7 @@ function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
{config.name}
</div>
<div className="absolute bottom-2 right-2 z-40">
<DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<BlurredIconButton>
<FiMoreVertical className="size-5 text-white" />

View File

@ -698,7 +698,7 @@ function LibrarySelector({
regexErrorMessage={t("description.invalidName")}
/>
<DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button className="flex justify-between smart-capitalize">
{pageTitle}