Preview
Showing a document with its highlights — the two views of the highlight data, and why the iframe needs a cookie.
A preview is a cached rendition of a document, prepared by the server so it can be displayed and highlighted in a browser. One call gives you the URL to embed and the highlight data to build a panel from.
const preview = await client.api.preview.get(record.id, query);Concept — the preview cache
The server converts a document once — a PDF, an email, a spreadsheet — into a cached HTML rendition and keeps it
under a cacheId. The preview you show is that rendition, not the original file, which is why the highlights
can be positioned in it.
What comes back
Prop
Type
Two views of the same data, for two jobs. Grouped by category, you build a panel of entity filters. Listed
by location, you step through occurrences — "next mention of Alice" — because each entry carries its start,
length and its positionInCategories.
A preview panel
import { createAtomicClient, sysLang } from "@sinequa/atomic";
import type { PreviewData, Query } from "@sinequa/atomic";
const client = createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
});
await client.initializeConfig();
await client.auth.login();
export async function openPreview(recordId: string, query: Query) {
const preview = await client.api.preview.get(recordId, query);
showDocument(preview);
showCategories(preview);
return preview;
}
function showDocument(preview: PreviewData) {
const frame = document.querySelector("iframe")!;
// The iframe cannot send an Authorization header: it relies on the session
// cookie. A blank preview on a token-authenticated deployment is that.
frame.src = preview.documentCachedContentUrl;
}
function showCategories(preview: PreviewData) {
for (const [name, category] of Object.entries(preview.highlightsPerCategory ?? {})) {
// Labels are multilingual strings; decode before displaying.
const label = sysLang(category.categoryDisplayLabel ?? name, navigator.language);
console.log(`${label}: ${category.values.length} value(s)`);
}
}
/** Occurrences in document order, for "next mention" navigation. */
export function occurrencesOf(preview: PreviewData, value: string) {
return preview.highlightsPerLocation
.filter((location) => location.values.includes(value))
.sort((a, b) => a.start - b.start);
}import { ChangeDetectionStrategy, Component, computed, inject, input, resource } from "@angular/core";
import { sysLang } from "@sinequa/atomic";
import type { Query } from "@sinequa/atomic";
import { SINEQUA } from "./sinequa";
@Component({
selector: "app-preview",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (preview.isLoading()) {
<p>Preparing the preview…</p>
} @else if (preview.value(); as data) {
<h2>{{ data.record.title }}</h2>
<!-- The iframe authenticates with the session cookie, not with a header. -->
<iframe [src]="data.documentCachedContentUrl" title="Document preview"></iframe>
<aside>
@for (category of categories(); track category.name) {
<section>
<h3>{{ category.label }} ({{ category.count }})</h3>
</section>
}
</aside>
}
`,
})
export class PreviewComponent {
private readonly client = inject(SINEQUA);
readonly recordId = input.required<string>();
readonly query = input.required<Query>();
readonly preview = resource({
params: () => ({ id: this.recordId(), query: this.query() }),
loader: ({ params }) => this.client.api.preview.get(params.id, params.query),
});
/** The panel is built from the by-category view; navigation uses by-location. */
readonly categories = computed(() =>
Object.entries(this.preview.value()?.highlightsPerCategory ?? {}).map(([name, category]) => ({
name,
label: sysLang(category.categoryDisplayLabel ?? name, navigator.language),
count: category.values.length,
})),
);
}import { sysLang } from "@sinequa/atomic";
import type { PreviewData, Query } from "@sinequa/atomic";
import { useEffect, useMemo, useState } from "react";
import { ready, sinequa } from "./sinequa";
export function Preview({ recordId, query }: { recordId: string; query: Query }) {
const [preview, setPreview] = useState<PreviewData>();
useEffect(() => {
let active = true;
ready
.then(() => sinequa.api.preview.get(recordId, query))
.then((next) => active && setPreview(next));
return () => {
active = false;
};
}, [recordId, query]);
/** The panel is built from the by-category view; navigation uses by-location. */
const categories = useMemo(
() =>
Object.entries(preview?.highlightsPerCategory ?? {}).map(([name, category]) => ({
name,
label: sysLang(category.categoryDisplayLabel ?? name, navigator.language),
count: category.values.length,
})),
[preview],
);
if (!preview) return <p>Preparing the preview…</p>;
return (
<>
<h2>{preview.record.title}</h2>
{/* The iframe authenticates with the session cookie, not with a header. */}
<iframe src={preview.documentCachedContentUrl} title="Document preview" />
<aside>
{categories.map((category) => (
<section key={category.name}>
<h3>
{category.label} ({category.count})
</h3>
</section>
))}
</aside>
</>
);
}