The two stage indicators in the compose page's "השלמה והעברה" rail
("הרץ למידת-קול" / "הרץ אימות-הלכות") were static placeholder text
translated from mockup 03 and never wired to data — they always read
"ממתין להעלאת הסופי", even after the final was uploaded and both
pipelines completed. (The real status was already shown correctly by
LearningStatusBadges on the case page.)
Wire them to useCaseLearningStatus (/api/cases/{n}/learning-status) — the
same source the drafts-panel badges use (G2). The trailing text is now
derived: "ממתין להעלאת הסופי" until the final is uploaded, then the live
state ("✓ הושלם · 12 לקחים הופקו · 12 הוצעו לאישור" / "✓ הושלם · חולצו 48
· 33 אושרו · 15 נדחו", or running/queued/failed).
Data-binding bugfix only — identical markup/classes (mockup-03 layout
preserved), so no visual redesign; within the design-gate bugfix exception
(chair-approved approach). tsc + eslint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
507 lines
22 KiB
TypeScript
507 lines
22 KiB
TypeScript
"use client";
|
||
|
||
import { use, useRef, useState } from "react";
|
||
import Link from "next/link";
|
||
import { FileText } from "lucide-react";
|
||
import { AppShell } from "@/components/app-shell";
|
||
import { Card, CardContent } from "@/components/ui/card";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
import { SubsectionCard } from "@/components/compose/subsection-card";
|
||
import { PrecedentsSection } from "@/components/compose/precedents-section";
|
||
import { CitationVerificationPanel } from "@/components/compose/citation-verification-panel";
|
||
import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel";
|
||
import { Markdown } from "@/components/ui/markdown";
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||
import { useCase, type CaseStatus } from "@/lib/api/cases";
|
||
import {
|
||
useCaseLearningStatus,
|
||
type CaseLearningStatus,
|
||
} from "@/lib/api/learning";
|
||
import { useResearchAnalysis } from "@/lib/api/research";
|
||
import { useCasePrecedents } from "@/lib/api/precedents";
|
||
import { APPEAL_SUBTYPES } from "@/lib/practice-area";
|
||
import { DOC_TYPE_LABELS, type DocType } from "@/lib/doc-types";
|
||
|
||
// ── Case-status → Hebrew label + tone (mockup 03 status chip) ────────────────
|
||
const STATUS_CHIP: Record<string, { label: string; cls: string }> = {
|
||
new: { label: "חדש", cls: "bg-rule-soft text-ink-muted border-rule" },
|
||
processing: { label: "בעיבוד", cls: "bg-info-bg text-info border-info/30" },
|
||
documents_ready: { label: "מסמכים מוכנים", cls: "bg-info-bg text-info border-info/30" },
|
||
outcome_set: { label: "תוצאה נקבעה", cls: "bg-info-bg text-info border-info/30" },
|
||
direction_approved: { label: "כיוון אושר", cls: "bg-info-bg text-info border-info/30" },
|
||
qa_review: { label: "בדיקת-איכות", cls: "bg-gold-wash text-gold-deep border-gold/40" },
|
||
drafted: { label: "טיוטה", cls: "bg-gold-wash text-gold-deep border-gold/40" },
|
||
exported: { label: "יוצא", cls: "bg-success-bg text-success border-success/40" },
|
||
reviewed: { label: "נסקר", cls: "bg-success-bg text-success border-success/40" },
|
||
final: { label: "סופי", cls: "bg-success-bg text-success border-success/40" },
|
||
};
|
||
|
||
function StatusChip({ status }: { status?: CaseStatus }) {
|
||
const c = (status && STATUS_CHIP[status]) || {
|
||
label: "בעריכה",
|
||
cls: "bg-info-bg text-info border-info/30",
|
||
};
|
||
return (
|
||
<span
|
||
className={`rounded-full text-[0.78rem] font-semibold px-3 py-0.5 border ${c.cls}`}
|
||
>
|
||
{c.label}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function subtypeLabel(subtype?: string | null): string | null {
|
||
if (!subtype) return null;
|
||
return APPEAL_SUBTYPES.find((s) => s.value === subtype)?.label ?? null;
|
||
}
|
||
|
||
// ── Staged-pipeline indicator text (mockup 03) — derived from the live
|
||
// learning-status, same source as the drafts-panel LearningStatusBadges. ──────
|
||
function voiceLearningText(s?: CaseLearningStatus): string {
|
||
if (!s?.final_uploaded) return "ממתין להעלאת הסופי";
|
||
const v = s.voice_learning;
|
||
if (v.outcome === "succeeded") {
|
||
const bits = [`${v.lessons_count} לקחים הופקו`];
|
||
if (v.lessons_proposed > 0) bits.push(`${v.lessons_proposed} הוצעו לאישור`);
|
||
return `✓ הושלם · ${bits.join(" · ")}`;
|
||
}
|
||
if (v.outcome === "failed") return v.error ? `✗ נכשל — ${v.error}` : "✗ נכשל";
|
||
return "ממתין להרצה";
|
||
}
|
||
|
||
function halachaExtractionText(s?: CaseLearningStatus): string {
|
||
if (!s?.final_uploaded) return "ממתין להעלאת הסופי";
|
||
const h = s.halacha_extraction;
|
||
if (!h.enrolled_in_corpus)
|
||
return h.not_enrolled_reason ?? "לא נכנס לקורפוס-הפסיקה";
|
||
switch (h.status) {
|
||
case "completed":
|
||
return `✓ הושלם · חולצו ${h.halachot_count} · ${h.approved} אושרו · ${h.rejected} נדחו`;
|
||
case "processing":
|
||
return "רץ עכשיו…";
|
||
case "pending":
|
||
case "busy":
|
||
return "בתור";
|
||
case "partial":
|
||
return `חלקי · חולצו ${h.halachot_count}`;
|
||
case "failed":
|
||
case "extraction_failed":
|
||
return "✗ נכשל";
|
||
case "no_chunks":
|
||
return "אין טקסט לחילוץ";
|
||
default:
|
||
return "ממתין להרצה";
|
||
}
|
||
}
|
||
|
||
function ProseSection({ title, content }: { title: string; content?: string }) {
|
||
if (!content?.trim()) return null;
|
||
return (
|
||
<section className="space-y-2">
|
||
<h3 className="text-[0.78rem] uppercase tracking-[0.08em] text-gold-deep font-semibold">
|
||
{title}
|
||
</h3>
|
||
<Markdown content={content.trim()} />
|
||
</section>
|
||
);
|
||
}
|
||
|
||
// ── "השלמה והעברה" rail card — DOCX export, upload, download (all real) ──────
|
||
function FinishRail({
|
||
caseNumber,
|
||
hasAnalysis,
|
||
onUploaded,
|
||
}: {
|
||
caseNumber: string;
|
||
hasAnalysis: boolean;
|
||
onUploaded: () => void;
|
||
}) {
|
||
const fileRef = useRef<HTMLInputElement>(null);
|
||
const [uploading, setUploading] = useState(false);
|
||
const [uploadMsg, setUploadMsg] = useState<{ ok: boolean; text: string } | null>(null);
|
||
const learning = useCaseLearningStatus(caseNumber);
|
||
|
||
async function handleUpload(file: File) {
|
||
setUploading(true);
|
||
setUploadMsg(null);
|
||
try {
|
||
const form = new FormData();
|
||
form.append("file", file);
|
||
const res = await fetch(`/api/cases/${caseNumber}/research/analysis/upload`, {
|
||
method: "PUT",
|
||
body: form,
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) {
|
||
setUploadMsg({ ok: false, text: data.detail || "שגיאה בהעלאה" });
|
||
return;
|
||
}
|
||
setUploadMsg({
|
||
ok: true,
|
||
text: `הקובץ הועלה — ${data.sections.threshold_claims} טענות סף, ${data.sections.issues} סוגיות`,
|
||
});
|
||
onUploaded();
|
||
} catch {
|
||
setUploadMsg({ ok: false, text: "שגיאת רשת" });
|
||
} finally {
|
||
setUploading(false);
|
||
if (fileRef.current) fileRef.current.value = "";
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Card className="bg-surface border-rule shadow-sm">
|
||
<CardContent className="px-4 py-4">
|
||
<h3 className="text-navy text-[0.9rem] font-semibold mb-3">השלמה והעברה</h3>
|
||
|
||
<input
|
||
ref={fileRef}
|
||
type="file"
|
||
accept=".md"
|
||
className="hidden"
|
||
onChange={(e) => {
|
||
const f = e.target.files?.[0];
|
||
if (f) handleUpload(f);
|
||
}}
|
||
/>
|
||
|
||
<div className="space-y-2">
|
||
{hasAnalysis && (
|
||
<Button
|
||
variant="outline"
|
||
className="w-full justify-center"
|
||
onClick={() => {
|
||
const a = document.createElement("a");
|
||
a.href = `/api/cases/${caseNumber}/research/analysis/export-docx`;
|
||
a.click();
|
||
}}
|
||
>
|
||
ייצוא DOCX
|
||
</Button>
|
||
)}
|
||
<Button
|
||
className="w-full justify-center bg-gold text-white hover:bg-gold-deep"
|
||
disabled={uploading}
|
||
onClick={() => fileRef.current?.click()}
|
||
>
|
||
{uploading ? "מעלה…" : "העלאת ניתוח מעודכן"}
|
||
</Button>
|
||
{hasAnalysis && (
|
||
<Button
|
||
variant="outline"
|
||
className="w-full justify-center"
|
||
onClick={() => {
|
||
const a = document.createElement("a");
|
||
a.href = `/api/cases/${caseNumber}/research/analysis/download`;
|
||
a.download = `analysis-${caseNumber}.md`;
|
||
a.click();
|
||
}}
|
||
>
|
||
הורד ניתוח (MD)
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{uploadMsg && (
|
||
<p className={`text-xs mt-2 ${uploadMsg.ok ? "text-success" : "text-danger"}`}>
|
||
{uploadMsg.text}
|
||
</p>
|
||
)}
|
||
|
||
{/* mockup 03: stage indicators — informational pointers, not actions */}
|
||
<div className="mt-3 space-y-0">
|
||
<div className="text-[0.78rem] text-ink-muted pt-2 border-t border-rule-soft">
|
||
<b className="text-navy">הרץ למידת-קול</b> — {voiceLearningText(learning.data)}
|
||
</div>
|
||
<div className="text-[0.78rem] text-ink-muted pt-2 mt-2 border-t border-rule-soft">
|
||
<b className="text-navy">הרץ אימות-הלכות</b> — {halachaExtractionText(learning.data)}
|
||
</div>
|
||
</div>
|
||
|
||
<Button asChild variant="ghost" className="w-full justify-center mt-3 text-ink-muted">
|
||
<Link href={`/cases/${caseNumber}`}>חזרה לתיק</Link>
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
export default function ComposePage({
|
||
params,
|
||
}: {
|
||
params: Promise<{ caseNumber: string }>;
|
||
}) {
|
||
const { caseNumber } = use(params);
|
||
const caseQuery = useCase(caseNumber);
|
||
const analysis = useResearchAnalysis(caseNumber);
|
||
const precedentsQuery = useCasePrecedents(caseNumber);
|
||
|
||
/* Partition the flat list into scopes so each child renders its own slice
|
||
* without re-fetching. Done once at the page level. */
|
||
const allPrecedents = precedentsQuery.data ?? [];
|
||
const caseLevelPrecedents = allPrecedents.filter((p) => p.section_id === null);
|
||
const precedentsBySection = new Map<string, typeof allPrecedents>();
|
||
for (const p of allPrecedents) {
|
||
if (p.section_id) {
|
||
const existing = precedentsBySection.get(p.section_id) ?? [];
|
||
existing.push(p);
|
||
precedentsBySection.set(p.section_id, existing);
|
||
}
|
||
}
|
||
const practiceArea = caseQuery.data?.practice_area ?? null;
|
||
const subtype = subtypeLabel(caseQuery.data?.appeal_subtype);
|
||
const parties = (() => {
|
||
const c = caseQuery.data;
|
||
if (!c) return null;
|
||
const app = c.appellants?.length ? c.appellants.join(", ") : null;
|
||
const resp = c.respondents?.length ? c.respondents.join(", ") : null;
|
||
const out: string[] = [];
|
||
if (app) out.push(`עוררים: ${app}`);
|
||
if (resp) out.push(`משיבה: ${resp}`);
|
||
return out.length ? out.join(" · ") : c.title || null;
|
||
})();
|
||
const documents = caseQuery.data?.documents ?? [];
|
||
|
||
const isNotFound =
|
||
analysis.error instanceof Error &&
|
||
/404|לא נמצא|טרם בוצע/.test(analysis.error.message);
|
||
|
||
return (
|
||
<AppShell>
|
||
{/* ── Case header band (mockup 03) — parchment strip, full-bleed to the
|
||
AppShell <main> edges (which pads px-10 py-10) ── */}
|
||
<div className="-mx-10 -mt-10 mb-6 border-b border-rule bg-parchment px-10 py-5">
|
||
<nav className="text-[0.78rem] text-ink-muted flex items-center gap-2 mb-2">
|
||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||
<span aria-hidden>·</span>
|
||
<Link href={`/cases/${caseNumber}`} className="hover:text-gold-deep">
|
||
ערר {caseNumber}
|
||
</Link>
|
||
<span aria-hidden>·</span>
|
||
<span className="text-navy">עורך החלטה</span>
|
||
</nav>
|
||
<Link
|
||
href={`/cases/${caseNumber}`}
|
||
className="inline-flex items-center gap-1.5 mb-3 rounded-md border border-rule bg-surface px-3 py-1.5 text-[0.82rem] font-semibold text-gold-deep hover:bg-gold-wash hover:text-navy transition-colors"
|
||
>
|
||
<span aria-hidden>→</span> חזרה לדף התיק
|
||
</Link>
|
||
<div className="flex items-center gap-3 flex-wrap">
|
||
<h1 className="text-navy text-2xl font-bold mb-0">ערר {caseNumber}</h1>
|
||
<StatusChip status={caseQuery.data?.status} />
|
||
{subtype && (
|
||
<span className="rounded-full text-[0.78rem] font-semibold px-3 py-0.5 border border-rule bg-gold-wash text-gold-deep">
|
||
{subtype}
|
||
</span>
|
||
)}
|
||
{/* INV-G10: source-of-truth pill — the blocks are the canonical text */}
|
||
<span className="ms-auto rounded-lg text-[0.8rem] font-semibold px-3.5 py-1.5 border border-gold bg-gold-wash text-gold-deep">
|
||
מקור-אמת: בלוקים
|
||
</span>
|
||
</div>
|
||
{parties && <p className="text-ink-soft text-sm mt-2">{parties}</p>}
|
||
</div>
|
||
|
||
{caseQuery.isPending ? (
|
||
<Card className="bg-surface border-rule shadow-sm">
|
||
<CardContent className="px-6 py-5 space-y-3">
|
||
<Skeleton className="h-6 w-48" />
|
||
<Skeleton className="h-4 w-96" />
|
||
<Skeleton className="h-4 w-80" />
|
||
<Skeleton className="h-32 w-full" />
|
||
</CardContent>
|
||
</Card>
|
||
) : (
|
||
/* ── Two-column workspace: tabbed main editor + 320px side rail ──── */
|
||
<div className="grid gap-6 lg:grid-cols-[1fr_320px] items-start">
|
||
{/* MAIN — block editor (default) + chair positions, as tabs (mockup 03) */}
|
||
<div className="min-w-0">
|
||
<Tabs defaultValue="blocks" dir="rtl">
|
||
<TabsList className="bg-rule-soft/60">
|
||
<TabsTrigger value="blocks">עורך הבלוקים</TabsTrigger>
|
||
<TabsTrigger value="positions">עמדות וטענות</TabsTrigger>
|
||
<TabsTrigger value="verify">אימות פסיקה</TabsTrigger>
|
||
</TabsList>
|
||
|
||
{/* Tab 1 — the 12-block decision editor (reused DecisionBlocksPanel) */}
|
||
<TabsContent value="blocks" className="mt-5">
|
||
<DecisionBlocksPanel caseNumber={caseNumber} />
|
||
</TabsContent>
|
||
|
||
{/* Tab 3 — citation verification: per-argument support + verify gate (#154) */}
|
||
<TabsContent value="verify" className="mt-5">
|
||
<CitationVerificationPanel caseNumber={caseNumber} />
|
||
</TabsContent>
|
||
|
||
{/* Tab 2 — chair positions on the analyst's threshold-claims + issues */}
|
||
<TabsContent value="positions" className="mt-5">
|
||
{analysis.isPending ? (
|
||
<Card className="bg-surface border-rule shadow-sm">
|
||
<CardContent className="px-6 py-5 space-y-3">
|
||
<Skeleton className="h-6 w-48" />
|
||
<Skeleton className="h-4 w-96" />
|
||
<Skeleton className="h-32 w-full" />
|
||
</CardContent>
|
||
</Card>
|
||
) : isNotFound ? (
|
||
<Card className="bg-surface border-rule shadow-sm">
|
||
<CardContent className="px-6 py-12 text-center space-y-3">
|
||
<div className="text-gold text-3xl" aria-hidden>❦</div>
|
||
<h2 className="text-navy text-lg mb-0">
|
||
טרם בוצע ניתוח משפטי לתיק זה
|
||
</h2>
|
||
<p className="text-ink-muted text-sm max-w-md mx-auto">
|
||
לאחר שקובץ <code>analysis-and-research.md</code> ייווצר, תוכלי
|
||
לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה.
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
) : analysis.error ? (
|
||
<Card className="bg-danger-bg border-danger/40">
|
||
<CardContent className="px-6 py-5 text-center">
|
||
<p className="text-danger">{analysis.error.message}</p>
|
||
</CardContent>
|
||
</Card>
|
||
) : analysis.data ? (
|
||
<div className="space-y-6">
|
||
{/* Threshold claims */}
|
||
{analysis.data.threshold_claims &&
|
||
analysis.data.threshold_claims.length > 0 && (
|
||
<div className="space-y-3">
|
||
<div className="flex items-center gap-2">
|
||
<h2 className="text-navy text-lg font-semibold mb-0">טענות סף</h2>
|
||
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
|
||
{analysis.data.threshold_claims.length}
|
||
</span>
|
||
</div>
|
||
<div className="space-y-2.5">
|
||
{analysis.data.threshold_claims.map((tc) => (
|
||
<SubsectionCard
|
||
key={tc.id}
|
||
caseNumber={caseNumber}
|
||
item={tc}
|
||
precedents={precedentsBySection.get(tc.id) ?? []}
|
||
practiceArea={practiceArea}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Issues */}
|
||
{analysis.data.issues && analysis.data.issues.length > 0 && (
|
||
<div className="space-y-3">
|
||
<div className="flex items-center gap-2">
|
||
<h2 className="text-navy text-lg font-semibold mb-0">סוגיות להכרעה</h2>
|
||
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
|
||
{analysis.data.issues.length}
|
||
</span>
|
||
</div>
|
||
<div className="space-y-2.5">
|
||
{analysis.data.issues.map((iss) => (
|
||
<SubsectionCard
|
||
key={iss.id}
|
||
caseNumber={caseNumber}
|
||
item={iss}
|
||
precedents={precedentsBySection.get(iss.id) ?? []}
|
||
practiceArea={practiceArea}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{!analysis.data.threshold_claims?.length &&
|
||
!analysis.data.issues?.length && (
|
||
<Card className="bg-surface border-rule">
|
||
<CardContent className="px-6 py-10 text-center text-ink-muted">
|
||
לא נמצאו טענות סף או סוגיות בניתוח זה.
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Background prose — supporting context after the decision points */}
|
||
<Card className="bg-surface border-rule shadow-sm">
|
||
<CardContent className="px-6 py-5 space-y-5">
|
||
<h2 className="text-navy text-lg font-semibold mb-0">רקע לניתוח</h2>
|
||
<ProseSection title="צד מיוצג" content={analysis.data.represented_party} />
|
||
<ProseSection title="רקע דיוני" content={analysis.data.procedural_background} />
|
||
<ProseSection title="עובדות מוסכמות" content={analysis.data.agreed_facts} />
|
||
<ProseSection title="עובדות במחלוקת" content={analysis.data.disputed_facts} />
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{analysis.data.conclusions?.trim() && (
|
||
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
||
<CardContent className="px-6 py-5 space-y-3">
|
||
<h2 className="text-gold-deep text-lg font-semibold mb-0">מסקנות</h2>
|
||
<Markdown content={analysis.data.conclusions.trim()} />
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
) : null}
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
|
||
{/* SIDE RAIL — documents · attached precedents · finish-and-transfer */}
|
||
<aside className="space-y-4 lg:sticky lg:top-4">
|
||
{/* מסמכי התיק */}
|
||
<Card className="bg-surface border-rule shadow-sm">
|
||
<CardContent className="px-4 py-4">
|
||
<h3 className="text-navy text-[0.9rem] font-semibold mb-2">מסמכי התיק</h3>
|
||
{documents.length === 0 ? (
|
||
<p className="text-[0.78rem] text-ink-muted">אין מסמכים מצורפים</p>
|
||
) : (
|
||
<ul>
|
||
{documents.map((d) => (
|
||
<li
|
||
key={d.id}
|
||
className="flex items-center gap-2 text-[0.82rem] text-ink-soft py-1.5 border-b border-rule-soft last:border-0"
|
||
>
|
||
<FileText className="w-3.5 h-3.5 text-ink-muted shrink-0" aria-hidden />
|
||
<span className="truncate flex-1" title={d.title}>
|
||
{d.title || "מסמך"}
|
||
</span>
|
||
<span className="rounded bg-rule-soft text-ink-muted text-[0.68rem] px-1.5 py-0.5 shrink-0 whitespace-nowrap">
|
||
{DOC_TYPE_LABELS[d.doc_type as DocType] ?? d.doc_type}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* פסיקה מצורפת (case-level) */}
|
||
<Card className="bg-surface border-rule shadow-sm">
|
||
<CardContent className="px-4 py-4">
|
||
<h3 className="text-navy text-[0.9rem] font-semibold mb-1">פסיקה מצורפת</h3>
|
||
<p className="text-[0.72rem] text-ink-muted mb-3">
|
||
ציטוטים התומכים בעמדה באופן רוחבי — ישולבו בפתיחת בלוק י (דיון).
|
||
</p>
|
||
<PrecedentsSection
|
||
caseNumber={caseNumber}
|
||
sectionId={null}
|
||
precedents={caseLevelPrecedents}
|
||
practiceArea={practiceArea}
|
||
emptyHelperText="עדיין לא צורפה פסיקה כללית לתיק"
|
||
/>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* השלמה והעברה */}
|
||
<FinishRail
|
||
caseNumber={caseNumber}
|
||
hasAnalysis={!!analysis.data}
|
||
onUploaded={() => analysis.refetch()}
|
||
/>
|
||
</aside>
|
||
</div>
|
||
)}
|
||
</AppShell>
|
||
);
|
||
}
|