Skip to content

Commit

Permalink
v
Browse files Browse the repository at this point in the history
  • Loading branch information
julianbenegas committed May 16, 2024
1 parent defbe89 commit f4c4b75
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 67 deletions.
1 change: 1 addition & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dull-paws-arrive",
"giant-insects-jump",
"long-roses-juggle",
"seven-peas-lick",
"tame-yaks-decide",
"thick-balloons-repair",
"three-turkeys-grow",
Expand Down
5 changes: 5 additions & 0 deletions .changeset/seven-peas-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"basehub": patch
---

add \_getFieldHighlight helper
6 changes: 6 additions & 0 deletions packages/basehub/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# basehub

## 4.0.16-canary.16

### Patch Changes

- add \_getFieldHighlight helper

## 4.0.16-canary.15

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/basehub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "basehub",
"description": "The first AI-native content hub.",
"author": "JB <[email protected]>",
"version": "4.0.16-canary.15",
"version": "4.0.16-canary.16",
"license": "MIT",
"repository": "basehub-ai/basehub",
"bugs": "https://github.com/basehub-ai/basehub/issues",
Expand Down
240 changes: 185 additions & 55 deletions packages/basehub/src/react/search/primitive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ export type Hit<Doc = Record<string, unknown>> = {
highlights: Array<Highlight>;
curated: boolean;
_getField: (fieldPath: string) => unknown;
_getFieldHighlight: (
fieldPath: string,
fallbackFieldPaths?: string[]
) => ReturnType<typeof _getFieldHighlightImpl>;
};

export type SearchResult<Doc = Record<string, unknown>> = {
Expand Down Expand Up @@ -271,16 +275,33 @@ export const useSearch = <

const _key = getHitKey(hit);

return {
const _getField = (fieldPath: string) => {
return get(document, fieldPath) as unknown;
};

const fullHit: Hit<Document> = {
_key,
curated: hit.curated ?? false,
document,
highlight: highlightRecord,
highlights,
_getField: (fieldPath: string) => {
return get(document, fieldPath) as unknown;
},
_getField,
_getFieldHighlight: () => null,
};

fullHit._getFieldHighlight = (
fieldPath: string,
fallbackFieldPaths?: string[]
) => {
return _getFieldHighlightImpl({
fieldPath,
fallbackFieldPaths,
includeFallback: true,
hit: fullHit,
});
};

return fullHit;
}) ?? [],
};

Expand Down Expand Up @@ -362,12 +383,18 @@ export const useSearch = <
if (!raw) return;

return (JSON.parse(raw) as Hit<Document>[]).map((hit) => {
return {
...hit,
_getField: (fieldPath: string) => {
return get(hit.document, fieldPath) as unknown;
},
hit._getField = (fieldPath: string) => {
return get(hit.document, fieldPath) as unknown;
};
hit._getFieldHighlight = (fieldPath, fallbackFieldPaths) => {
return _getFieldHighlightImpl({
fieldPath,
fallbackFieldPaths,
includeFallback: true,
hit: hit,
});
};
return hit;
});
},
};
Expand Down Expand Up @@ -794,9 +821,11 @@ const HitItem = React.forwardRef<

const HitSnippet = ({
fieldPath,
fallbackFieldPaths,
components,
}: {
fieldPath: string;
fallbackFieldPaths?: string[];
components?: {
container?: ({
children,
Expand All @@ -808,6 +837,83 @@ const HitSnippet = ({
};
}) => {
const { hit } = useHitContext();

const res = hit._getFieldHighlight(fieldPath, fallbackFieldPaths);
if (!res) return null;

const snippet = res.snippet ?? "";

const matches = [
...snippet.matchAll(/(.*?)<mark>(.*?)<\/mark>(.*?)(?=(?:<mark>|$))/gm),
];

const Container = components?.container ?? "div";
const Text = components?.text ?? "span";
const Mark = components?.mark ?? "mark";

return (
<Container>
{matches.length > 0 ? (
matches.map((match, i) => {
const data = {
beforeMark: match[1] ?? "",
insideMark: match[2] ?? "",
afterMark: match[3] ?? "",
};

return (
<React.Fragment key={i}>
<Text>{data.beforeMark}</Text>
<Mark data-highlight>{data.insideMark}</Mark>
<Text>{data.afterMark}</Text>
</React.Fragment>
);
})
) : (
<Text>{snippet}</Text>
)}
</Container>
);
};

/* -------------------------------------------------------------------------------------------------
* Hit Utils
* -----------------------------------------------------------------------------------------------*/

type HighlightedField =
| undefined
| {
_type: "rich-text-section";
_content: string;
_id?: string;
_level?: number;
}
| {
_type: "text";
_content: string;
}
| {
_type: "unknown";
_content: unknown;
};

function _getFieldHighlightImpl({
hit,
fieldPath,
fallbackFieldPaths,
includeFallback,
}: {
hit: Hit;
fieldPath: string;
fallbackFieldPaths?: string[];
includeFallback?: boolean;
}): null | {
highlightedField: HighlightedField;
snippet: string | undefined;
snippetByExactMatch: string | undefined;
snippetByPrefix: string | undefined;
fallbackSnippet: string | undefined;
} {
const field = hit._getField(fieldPath);
if (!field) return null;

Expand All @@ -819,25 +925,55 @@ const HitSnippet = ({

const prefix = fieldPath.endsWith(".") ? fieldPath : fieldPath + ".";

hit.highlights.forEach((highlight) => {
if (!snippetByExactMatch && highlight.fieldPath === fieldPath) {
snippetByExactMatch = highlight.snippet;
let highlightedField: HighlightedField = undefined;

hit.highlights.forEach((h) => {
if (h.fieldPath !== fieldPath && h.fieldPath.startsWith(prefix) === false) {
return;
}
if (!snippetByPrefix && highlight.fieldPath.startsWith(prefix)) {
snippetByPrefix = highlight.snippet;
if (!highlightedField) {
const adjustedPath = isRichText
? h.fieldPath.split(".").slice(0, 2).join(".")
: h.fieldPath;
const fieldData = hit._getField(adjustedPath);
if (!fieldData) return;
else if (typeof fieldData === "string") {
highlightedField = {
_type: "text",
_content: fieldData,
};
} else if (
typeof fieldData === "object" &&
"_type" in fieldData &&
fieldData._type === "rich-text-section"
) {
highlightedField = fieldData as HighlightedField;
} else {
highlightedField = {
_type: "unknown",
_content: fieldData,
};
}
}

if (!snippetByExactMatch && h.fieldPath === fieldPath) {
snippetByExactMatch = h.snippet;
}
if (!snippetByPrefix && h.fieldPath.startsWith(prefix)) {
snippetByPrefix = h.snippet;
}
});

// get first piece of text we find under `field`
function getFallbackString(
current: unknown,
opts: {
isRichText: boolean;
}
opts: { isRichText: boolean }
): string | undefined {
if (typeof current === "string") return current;

if (current === null || current === undefined) return undefined;
if (current === null || current === undefined) {
return undefined;
}

if (Array.isArray(current)) {
const found = current
Expand All @@ -847,52 +983,46 @@ const HitSnippet = ({
} else if (typeof current === "object") {
const found = Object.entries(current)
.map(([key, value]) => {
if (opts.isRichText && key !== "_content") return undefined;
if (opts.isRichText && key !== "_content") {
return undefined;
}
return getFallbackString(value, opts);
})
.find((v) => v);
return found;
}
}

const snippet =
snippetByExactMatch ||
snippetByPrefix ||
getFallbackString(field, { isRichText }) ||
"";

const matches = [
...snippet.matchAll(/(.*?)<mark>(.*?)<\/mark>(.*?)(?=(?:<mark>|$))/gm),
];

const Container = components?.container ?? "div";
const Text = components?.text ?? "span";
const Mark = components?.mark ?? "mark";
let fallbackSnippet;
let snippet: string | undefined =
snippetByExactMatch || snippetByPrefix || undefined;

if (snippet === undefined && fallbackFieldPaths && fallbackFieldPaths[0]) {
const [fallbackFieldPath, ...rest] = fallbackFieldPaths;
const fallbackResult = _getFieldHighlightImpl({
hit,
fieldPath: fallbackFieldPath,
fallbackFieldPaths: rest,
includeFallback: false,
});
snippet = fallbackResult?.snippet;
fallbackSnippet = fallbackResult?.snippet;
}

return (
<Container>
{matches.length > 0 ? (
matches.map((match, i) => {
const data = {
beforeMark: match[1] ?? "",
insideMark: match[2] ?? "",
afterMark: match[3] ?? "",
};
if (snippet === undefined && includeFallback) {
// if snippet is still undefined after trying all fallbacks, we'll fallback to the first piece of text we find in the field
fallbackSnippet = getFallbackString(field, { isRichText });
snippet = fallbackSnippet;
}

return (
<React.Fragment key={i}>
<Text>{data.beforeMark}</Text>
<Mark data-highlight>{data.insideMark}</Mark>
<Text>{data.afterMark}</Text>
</React.Fragment>
);
})
) : (
<Text>{snippet}</Text>
)}
</Container>
);
};
return {
highlightedField,
snippet,
snippetByExactMatch: snippetByExactMatch as string | undefined,
snippetByPrefix: snippetByPrefix as string | undefined,
fallbackSnippet,
};
}

export const SearchBox = {
Root,
Expand Down
7 changes: 7 additions & 0 deletions playground/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# playground

## 0.0.86-canary.16

### Patch Changes

- Updated dependencies
- [email protected]

## 0.0.86-canary.15

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "playground",
"private": true,
"version": "0.0.86-canary.15",
"version": "0.0.86-canary.16",
"scripts": {
"dev": "basehub dev & next dev",
"build": "next build",
Expand Down
Loading

0 comments on commit f4c4b75

Please sign in to comment.