Recherche plein texte dans le contenu des schémas

- /api/search cherche dans les noms et dans le texte des schémas (textes et
  libellés, noms de cadres, liens) ; tous les mots requis, casse et accents
  ignorés, correspondances sur le nom en premier.
- Index en mémoire côté serveur : texte extrait à la première recherche, relu
  seulement si le fichier change (date ou taille), rien d'écrit sur le volume.
- L'explorateur affiche l'extrait correspondant sous le nom.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6J7vKwXx7uopxKhWfFJpm
This commit is contained in:
claude BotandClaude Opus 5.5 committed 2026-10-07 18:04:21 +02:00
1 parent 9f9ec3b3a4
commit c78df092c1
8 files changed
+329 -45

No files matched your search

+5 -3
View File
@@ -10,6 +10,7 @@ import {
stripExt,
withExt,
type Entry,
type Hit,
} from "./api";
import {
CloseIcon,
@@ -84,7 +85,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
const [dir, setDir] = useState(() => (current ? dirname(current) : ""));
const [entries, setEntries] = useState<Entry[]>([]);
const [query, setQuery] = useState("");
const [results, setResults] = useState<Entry[] | null>(null);
const [results, setResults] = useState<Hit[] | null>(null);
const [view, setView] = useState<View>(initialView);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [anchor, setAnchor] = useState<string | null>(null); // origine d'une sélection Maj+clic
@@ -436,7 +437,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
</div>
);
const item = (e: Entry) => {
const item = (e: Hit) => {
const isSel = selected.has(e.path);
const cls = [
"item",
@@ -472,6 +473,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
<span className="label">
{e.isDir ? e.name : stripExt(e.name)}
{results && <small className="muted where"> {dirname(e.path) || "/"}</small>}
{e.snippet && <small className="snippet">{e.snippet}</small>}
</span>
</button>
{!e.isDir && view === "list" && <span className="meta muted">{fmtDate(e.modTime)}</span>}
@@ -551,7 +553,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
<SearchIcon />
<input
type="search"
placeholder="Rechercher…"
placeholder="Rechercher (noms, textes, IP…)"
value={query}
onChange={(ev) => setQuery(ev.target.value)}
/>
+6 -1
View File
@@ -8,6 +8,11 @@ export interface Entry {
modTime: string;
}
/** Résultat de recherche ; snippet = extrait du contenu qui correspond. */
export interface Hit extends Entry {
snippet?: string;
}
export interface MoveResult {
from: string;
to?: string;
@@ -72,7 +77,7 @@ export const api = {
list: async (dir: string): Promise<Entry[]> =>
(await request(`/api/list?path=${q(dir)}`)).json(),
search: async (text: string): Promise<Entry[]> =>
search: async (text: string): Promise<Hit[]> =>
(await request(`/api/search?q=${q(text)}`)).json(),
read: async (path: string): Promise<{ text: string; etag: string }> => {
+13
View File
@@ -1127,3 +1127,16 @@ li.active > .row {
display: none;
}
}
/* Extrait du contenu qui correspond à la recherche. */
.row .label .snippet {
display: block;
overflow: hidden;
text-overflow: ellipsis;
font-size: 11.5px;
color: var(--ex-on-primary-container);
opacity: 0.8;
}
.entries.grid .snippet {
text-align: center;
}