feat(PROJ-44): Snippet + Quellen-Badge in Such-Trefferliste
- Zweite Zeile unter dem Betreff zeigt Manticore-Snippet mit <b>-Highlights, gerendert via dangerouslySetInnerHTML ueber sanitizeSnippet - Quellen-Badge je match_field (Subject, Body, PDF-Anhang, Dateiname, Absender, Empfaenger) als kleines Tailwind-Pill - Nur sichtbar wenn das Backend ein snippet zurueckliefert Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+37
-3
@@ -3,7 +3,8 @@
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { searchEmails, exportMailsZIP, exportEDiscovery, uploadMailFilesUser, getUploadProgressUser, listSavedSearches, createSavedSearch, deleteSavedSearch, type SearchHit, type UploadJob, type SavedSearch } from "@/lib/api";
|
||||
import { searchEmails, exportMailsZIP, exportEDiscovery, uploadMailFilesUser, getUploadProgressUser, listSavedSearches, createSavedSearch, deleteSavedSearch, type SearchHit, type SearchMatchField, type UploadJob, type SavedSearch } from "@/lib/api";
|
||||
import { sanitizeSnippet } from "@/lib/sanitize";
|
||||
import { Navbar } from "@/components/navbar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -51,6 +52,36 @@ function formatBytes(bytes: number): string {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
const MATCH_FIELD_LABEL: Record<SearchMatchField, string> = {
|
||||
subject: "📨 Subject",
|
||||
body: "✉️ Body",
|
||||
attachment_text: "📄 PDF-Anhang",
|
||||
attachment_names: "📎 Dateiname",
|
||||
from_addr: "👤 Absender",
|
||||
to_addr: "📧 Empfänger",
|
||||
};
|
||||
|
||||
function MatchSourceBadge({ field }: { field: SearchMatchField }) {
|
||||
return (
|
||||
<span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground whitespace-nowrap">
|
||||
{MATCH_FIELD_LABEL[field]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function SnippetLine({ hit }: { hit: SearchHit }) {
|
||||
if (!hit.snippet) return null;
|
||||
return (
|
||||
<div className="mt-1 flex items-start gap-2 text-xs text-muted-foreground font-normal">
|
||||
{hit.match_field && <MatchSourceBadge field={hit.match_field} />}
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate [&_b]:font-semibold [&_b]:text-foreground"
|
||||
dangerouslySetInnerHTML={{ __html: sanitizeSnippet(hit.snippet) }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SearchPage() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
@@ -621,12 +652,15 @@ export default function SearchPage() {
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[14rem] truncate text-sm">{hit.from || "-"}</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
<span>{hit.subject || "(kein Betreff)"}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate">{hit.subject || "(kein Betreff)"}</span>
|
||||
{hit.thread_size && hit.thread_size > 1 && (
|
||||
<span className="ml-2 inline-flex items-center rounded-full bg-muted px-1.5 py-0.5 text-xs text-muted-foreground font-normal">
|
||||
<span className="inline-flex items-center rounded-full bg-muted px-1.5 py-0.5 text-xs text-muted-foreground font-normal">
|
||||
{hit.thread_size}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<SnippetLine hit={hit} />
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[12rem] truncate text-sm text-muted-foreground">{hit.to || "-"}</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
|
||||
Reference in New Issue
Block a user