+ os.listdir(SAMPLE_IMG_DIR) SAMPLE_IMG_DIR
Path('/home/evylz/AnimalEquality/lv-recipe-chatbot/assets/images/vegan_ingredients')
+diff --git a/README.md b/README.md index e53898ef813e596eeca4255ac0a4ebfd285d6f2e..3d820a6363936af9b345137481c06dbcedd8924c 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,10 @@ license: unknown -This file will become your README and also the index of your -documentation. - ## Install ``` sh -pip install lv_recipe_chatbot +pip install -e '.[dev]' ``` ## How to use @@ -27,15 +24,18 @@ pip install lv_recipe_chatbot ``` python from dotenv import load_dotenv -load_dotenv() +load_dotenv() # or load environment vars with different method -app.launch_demo() +demo = app.create_demo(app.ConversationBot()) +demo.launch() ``` Running on local URL: http://127.0.0.1:7860 To create a public link, set `share=True` in `launch()`. +
+ or ``` sh diff --git a/_proc/00_engineer_prompt.ipynb b/_proc/00_engineer_prompt.ipynb index 0014106ca9efca1eafd9d82d7bb59394459a6266..1ae6a10b0271662e5ba84fab2a2433d7bed8e10c 100644 --- a/_proc/00_engineer_prompt.ipynb +++ b/_proc/00_engineer_prompt.ipynb @@ -57,6 +57,7 @@ } ], "source": [ + "#| eval: false\n", "load_dotenv()" ] }, @@ -190,6 +191,7 @@ } ], "source": [ + "#| eval: false\n", "chat = PromptLayerChatOpenAI(temperature=1, pl_tags=[\"langchain\"], return_pl_id=True)\n", "memory = ConversationBufferMemory(return_messages=True)\n", "chat_msgs = init_prompt.format_prompt(\n", @@ -215,17 +217,6 @@ "result = conversation.predict(input=\"Recommend a different recipe please.\")\n", "print(result)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "language": "python" - }, - "outputs": [], - "source": [ - "test_temp = HumanMessagePromptTemplate.from_template(\"Ingredients: {ingredients}\")" - ] } ], "metadata": { diff --git a/_proc/01_app.ipynb b/_proc/01_app.ipynb index 089884183c7e6277bdc5ac8f3b52706a86bebb1f..bd5a57676f070718a25e1370f4422624e0009076 100644 --- a/_proc/01_app.ipynb +++ b/_proc/01_app.ipynb @@ -30,7 +30,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/app.py#L32){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://gitlab.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/app.py#L32){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### ConversationBot\n", "\n", @@ -41,7 +41,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/app.py#L32){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://gitlab.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/app.py#L32){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### ConversationBot\n", "\n", @@ -71,12 +71,7 @@ { "data": { "text/plain": [ - "['veggie-fridge.jpeg',\n", - " 'veg-groceries-table.jpg',\n", - " 'fridge-splendid.jpg',\n", - " 'neat-veg-groceries.jpg',\n", - " 'veg-groceries-table.jpeg',\n", - " 'Fruits-and-vegetables-one-a-table.jpg']" + "Path('/home/evylz/AnimalEquality/lv-recipe-chatbot/assets/images/vegan_ingredients')" ] }, "execution_count": null, @@ -85,7 +80,8 @@ } ], "source": [ - "os.listdir(SAMPLE_IMG_DIR)" + "os.listdir(SAMPLE_IMG_DIR)\n", + "SAMPLE_IMG_DIR" ] }, { @@ -118,6 +114,7 @@ } ], "source": [ + "#: eval: false\n", "load_dotenv()" ] }, @@ -137,7 +134,9 @@ ] } ], - "source": [] + "source": [ + "#| eval: false" + ] }, { "cell_type": "code", @@ -165,61 +164,10 @@ ] } ], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "language": "python" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['ingredients'], output_parser=None, partial_variables={}, template='I uploaded an image that may contain vegan ingredients.\\nThe description of the image is: `a refrigerator with food inside`.\\nThe extracted ingredients are:\\n```\\ncabbage lettuce onion\\napples\\nrice\\nplant-based milk\\n```\\nI may type some more ingredients below.\\nIngredients: {ingredients}', template_format='f-string', validate_template=True), additional_kwargs={})" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "bot.init_prompt.messages[2]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "language": "python" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['ingredients'], output_parser=None, partial_variables={}, template='Ingredients: {ingredients}', template_format='f-string', validate_template=True), additional_kwargs={})" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "INIT_PROMPT.messages[2]" + "#| eval: false" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "language": "python" - }, - "outputs": [], - "source": [] - }, { "cell_type": "code", "execution_count": 2, @@ -230,7 +178,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/app.py#L116){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://gitlab.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/app.py#L116){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### create_demo\n", "\n", @@ -239,7 +187,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/app.py#L116){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://gitlab.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/app.py#L116){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### create_demo\n", "\n", @@ -296,20 +244,12 @@ } ], "source": [ + "#| eval: false\n", "if \"demo\" in globals():\n", " demo.close()\n", "demo = create_demo(bot)\n", "demo.launch()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "language": "python" - }, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/_proc/02_lchain_tool.ipynb b/_proc/02_lchain_tool.ipynb index 9a401933a1f857cb850b4d9f13d76a0576da7d7e..e9c617e1e12586f27e93d39b2ce33f486224eea7 100644 --- a/_proc/02_lchain_tool.ipynb +++ b/_proc/02_lchain_tool.ipynb @@ -156,6 +156,7 @@ } ], "source": [ + "#| eval: false\n", "agent(\"What is the 3% of of 300 * 30?\")" ] }, @@ -217,6 +218,7 @@ } ], "source": [ + "#| eval: false\n", "params = {\n", " \"q\": \"Vegan pad thai recipes\",\n", " \"location\": \"United States\",\n", @@ -306,7 +308,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/lchain_tool.py#L19){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://gitlab.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/lchain_tool.py#L19){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### RecipeSerpAPIWrapper\n", "\n", @@ -331,7 +333,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/lchain_tool.py#L19){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://gitlab.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/lchain_tool.py#L19){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### RecipeSerpAPIWrapper\n", "\n", @@ -373,6 +375,7 @@ }, "outputs": [], "source": [ + "#| eval: false\n", "params = {\n", " \"location\": \"United States\",\n", " \"hl\": \"en\",\n", @@ -432,6 +435,7 @@ } ], "source": [ + "#| eval: false\n", "vegan_recipes = search.run(\"Vegan fried rice recipes\")\n", "vegan_recipes[0:3]" ] @@ -444,6 +448,7 @@ }, "outputs": [], "source": [ + "#| eval: false\n", "params = {\n", " \"engine\": \"google_images\",\n", " \"q\": \"Vegan pad thai recipes\",\n", @@ -614,6 +619,7 @@ } ], "source": [ + "#| eval: false\n", "for r in results[\"images_results\"][0:5]:\n", " display(r[\"title\"], r[\"link\"], Image(url=r[\"thumbnail\"]))" ] @@ -806,7 +812,7 @@ "outputs": [], "source": [ "@tool\n", - "def vegan_recipe_search_serpapi(text: str) -> str:\n", + "def vegan_recipe_serpapi_search(text: str) -> str:\n", " \"\"\"Returns a JSON/Python list of dictionaries of recipe data with keys in format:\n", " ```\n", " 'title': str,\n", @@ -963,6 +969,7 @@ } ], "source": [ + "#| eval: false\n", "agent.run(\"Search vegan pad thai recipes\")" ] }, @@ -981,6 +988,7 @@ }, "outputs": [], "source": [ + "#| eval: false\n", "search = GoogleSerperAPIWrapper(type=\"search\")\n", "results = search.results(\"Lion\")" ] diff --git a/_proc/03_ingredient_vision.ipynb b/_proc/03_ingredient_vision.ipynb index 18ce32c2fee996013c53a6d740489b0b40580bda..3b0ca2623c472bf5e908c011ecd63f33eae24dc8 100644 --- a/_proc/03_ingredient_vision.ipynb +++ b/_proc/03_ingredient_vision.ipynb @@ -37,20 +37,28 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/ingredient_vision.py#L24){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://gitlab.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/ingredient_vision.py#L24){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### format_image\n", "\n", - "> format_image (image)" + "> format_image (image:str)\n", + "\n", + "| | **Type** | **Details** |\n", + "| -- | -------- | ----------- |\n", + "| image | str | Image file path |" ], "text/plain": [ "---\n", "\n", - "[source](https://github.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/ingredient_vision.py#L24){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://gitlab.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/ingredient_vision.py#L24){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### format_image\n", "\n", - "> format_image (image)" + "> format_image (image:str)\n", + "\n", + "| | **Type** | **Details** |\n", + "| -- | -------- | ----------- |\n", + "| image | str | Image file path |" ] }, "execution_count": 1, @@ -74,7 +82,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/ingredient_vision.py#L36){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://gitlab.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/ingredient_vision.py#L36){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### BlipImageCaptioning\n", "\n", @@ -85,7 +93,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/ingredient_vision.py#L36){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://gitlab.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/ingredient_vision.py#L36){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### BlipImageCaptioning\n", "\n", @@ -108,6 +116,63 @@ { "cell_type": "code", "execution_count": 3, + "metadata": { + "language": "python" + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "---\n", + "\n", + "[source](https://gitlab.com/animalequality/lv-recipe-chatbot/blob/main/lv_recipe_chatbot/ingredient_vision.py#L51){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "### BlipImageCaptioning.inference\n", + "\n", + "> BlipImageCaptioning.inference\n", + "> (image:${missingFields[0]}
field.`,
+ message: `The items being returned for this search do not include all the required fields. Please ensure that your index items include the ${missingFields[0]}
field or use index-fields
in your _quarto.yml
file to specify the field names.`,
+ };
+ } else if (missingFields.length > 1) {
+ const missingFieldList = missingFields
+ .map((field) => {
+ return `${field}
`;
+ })
+ .join(", ");
+
+ throw {
+ name: `Error: Search index is missing the following fields: ${missingFieldList}.`,
+ message: `The items being returned for this search do not include all the required fields. Please ensure that your index items includes the following fields: ${missingFieldList}, or use index-fields
in your _quarto.yml
file to specify the field names.`,
+ };
+ }
+ }
+}
+
+let lastQuery = null;
+function showCopyLink(query, options) {
+ const language = options.language;
+ lastQuery = query;
+ // Insert share icon
+ const inputSuffixEl = window.document.body.querySelector(
+ ".aa-Form .aa-InputWrapperSuffix"
+ );
+
+ if (inputSuffixEl) {
+ let copyButtonEl = window.document.body.querySelector(
+ ".aa-Form .aa-InputWrapperSuffix .aa-CopyButton"
+ );
+
+ if (copyButtonEl === null) {
+ copyButtonEl = window.document.createElement("button");
+ copyButtonEl.setAttribute("class", "aa-CopyButton");
+ copyButtonEl.setAttribute("type", "button");
+ copyButtonEl.setAttribute("title", language["search-copy-link-title"]);
+ copyButtonEl.onmousedown = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ };
+
+ const linkIcon = "bi-clipboard";
+ const checkIcon = "bi-check2";
+
+ const shareIconEl = window.document.createElement("i");
+ shareIconEl.setAttribute("class", `bi ${linkIcon}`);
+ copyButtonEl.appendChild(shareIconEl);
+ inputSuffixEl.prepend(copyButtonEl);
+
+ const clipboard = new window.ClipboardJS(".aa-CopyButton", {
+ text: function (_trigger) {
+ const copyUrl = new URL(window.location);
+ copyUrl.searchParams.set(kQueryArg, lastQuery);
+ copyUrl.searchParams.set(kResultsArg, "1");
+ return copyUrl.toString();
+ },
+ });
+ clipboard.on("success", function (e) {
+ // Focus the input
+
+ // button target
+ const button = e.trigger;
+ const icon = button.querySelector("i.bi");
+
+ // flash "checked"
+ icon.classList.add(checkIcon);
+ icon.classList.remove(linkIcon);
+ setTimeout(function () {
+ icon.classList.remove(checkIcon);
+ icon.classList.add(linkIcon);
+ }, 1000);
+ });
+ }
+
+ // If there is a query, show the link icon
+ if (copyButtonEl) {
+ if (lastQuery && options["copy-button"]) {
+ copyButtonEl.style.display = "flex";
+ } else {
+ copyButtonEl.style.display = "none";
+ }
+ }
+ }
+}
+
+/* Search Index Handling */
+// create the index
+var fuseIndex = undefined;
+async function readSearchData() {
+ // Initialize the search index on demand
+ if (fuseIndex === undefined) {
+ // create fuse index
+ const options = {
+ keys: [
+ { name: "title", weight: 20 },
+ { name: "section", weight: 20 },
+ { name: "text", weight: 10 },
+ ],
+ ignoreLocation: true,
+ threshold: 0.1,
+ };
+ const fuse = new window.Fuse([], options);
+
+ // fetch the main search.json
+ const response = await fetch(offsetURL("search.json"));
+ if (response.status == 200) {
+ return response.json().then(function (searchDocs) {
+ searchDocs.forEach(function (searchDoc) {
+ fuse.add(searchDoc);
+ });
+ fuseIndex = fuse;
+ return fuseIndex;
+ });
+ } else {
+ return Promise.reject(
+ new Error(
+ "Unexpected status from search index request: " + response.status
+ )
+ );
+ }
+ }
+ return fuseIndex;
+}
+
+function inputElement() {
+ return window.document.body.querySelector(".aa-Form .aa-Input");
+}
+
+function focusSearchInput() {
+ setTimeout(() => {
+ const inputEl = inputElement();
+ if (inputEl) {
+ inputEl.focus();
+ }
+ }, 50);
+}
+
+/* Panels */
+const kItemTypeDoc = "document";
+const kItemTypeMore = "document-more";
+const kItemTypeItem = "document-item";
+const kItemTypeError = "error";
+
+function renderItem(
+ item,
+ createElement,
+ state,
+ setActiveItemId,
+ setContext,
+ refresh
+) {
+ switch (item.type) {
+ case kItemTypeDoc:
+ return createDocumentCard(
+ createElement,
+ "file-richtext",
+ item.title,
+ item.section,
+ item.text,
+ item.href
+ );
+ case kItemTypeMore:
+ return createMoreCard(
+ createElement,
+ item,
+ state,
+ setActiveItemId,
+ setContext,
+ refresh
+ );
+ case kItemTypeItem:
+ return createSectionCard(
+ createElement,
+ item.section,
+ item.text,
+ item.href
+ );
+ case kItemTypeError:
+ return createErrorCard(createElement, item.title, item.text);
+ default:
+ return undefined;
+ }
+}
+
+function createDocumentCard(createElement, icon, title, section, text, href) {
+ const iconEl = createElement("i", {
+ class: `bi bi-${icon} search-result-icon`,
+ });
+ const titleEl = createElement("p", { class: "search-result-title" }, title);
+ const titleContainerEl = createElement(
+ "div",
+ { class: "search-result-title-container" },
+ [iconEl, titleEl]
+ );
+
+ const textEls = [];
+ if (section) {
+ const sectionEl = createElement(
+ "p",
+ { class: "search-result-section" },
+ section
+ );
+ textEls.push(sectionEl);
+ }
+ const descEl = createElement("p", {
+ class: "search-result-text",
+ dangerouslySetInnerHTML: {
+ __html: text,
+ },
+ });
+ textEls.push(descEl);
+
+ const textContainerEl = createElement(
+ "div",
+ { class: "search-result-text-container" },
+ textEls
+ );
+
+ const containerEl = createElement(
+ "div",
+ {
+ class: "search-result-container",
+ },
+ [titleContainerEl, textContainerEl]
+ );
+
+ const linkEl = createElement(
+ "a",
+ {
+ href: offsetURL(href),
+ class: "search-result-link",
+ },
+ containerEl
+ );
+
+ const classes = ["search-result-doc", "search-item"];
+ if (!section) {
+ classes.push("document-selectable");
+ }
+
+ return createElement(
+ "div",
+ {
+ class: classes.join(" "),
+ },
+ linkEl
+ );
+}
+
+function createMoreCard(
+ createElement,
+ item,
+ state,
+ setActiveItemId,
+ setContext,
+ refresh
+) {
+ const moreCardEl = createElement(
+ "div",
+ {
+ class: "search-result-more search-item",
+ onClick: (e) => {
+ // Handle expanding the sections by adding the expanded
+ // section to the list of expanded sections
+ toggleExpanded(item, state, setContext, setActiveItemId, refresh);
+ e.stopPropagation();
+ },
+ },
+ item.title
+ );
+
+ return moreCardEl;
+}
+
+function toggleExpanded(item, state, setContext, setActiveItemId, refresh) {
+ const expanded = state.context.expanded || [];
+ if (expanded.includes(item.target)) {
+ setContext({
+ expanded: expanded.filter((target) => target !== item.target),
+ });
+ } else {
+ setContext({ expanded: [...expanded, item.target] });
+ }
+
+ refresh();
+ setActiveItemId(item.__autocomplete_id);
+}
+
+function createSectionCard(createElement, section, text, href) {
+ const sectionEl = createSection(createElement, section, text, href);
+ return createElement(
+ "div",
+ {
+ class: "search-result-doc-section search-item",
+ },
+ sectionEl
+ );
+}
+
+function createSection(createElement, title, text, href) {
+ const descEl = createElement("p", {
+ class: "search-result-text",
+ dangerouslySetInnerHTML: {
+ __html: text,
+ },
+ });
+
+ const titleEl = createElement("p", { class: "search-result-section" }, title);
+ const linkEl = createElement(
+ "a",
+ {
+ href: offsetURL(href),
+ class: "search-result-link",
+ },
+ [titleEl, descEl]
+ );
+ return linkEl;
+}
+
+function createErrorCard(createElement, title, text) {
+ const descEl = createElement("p", {
+ class: "search-error-text",
+ dangerouslySetInnerHTML: {
+ __html: text,
+ },
+ });
+
+ const titleEl = createElement("p", {
+ class: "search-error-title",
+ dangerouslySetInnerHTML: {
+ __html: ` ${title}`,
+ },
+ });
+ const errorEl = createElement("div", { class: "search-error" }, [
+ titleEl,
+ descEl,
+ ]);
+ return errorEl;
+}
+
+function positionPanel(pos) {
+ const panelEl = window.document.querySelector(
+ "#quarto-search-results .aa-Panel"
+ );
+ const inputEl = window.document.querySelector(
+ "#quarto-search .aa-Autocomplete"
+ );
+
+ if (panelEl && inputEl) {
+ panelEl.style.top = `${Math.round(panelEl.offsetTop)}px`;
+ if (pos === "start") {
+ panelEl.style.left = `${Math.round(inputEl.left)}px`;
+ } else {
+ panelEl.style.right = `${Math.round(inputEl.offsetRight)}px`;
+ }
+ }
+}
+
+/* Highlighting */
+// highlighting functions
+function highlightMatch(query, text) {
+ if (text) {
+ const start = text.toLowerCase().indexOf(query.toLowerCase());
+ if (start !== -1) {
+ const startMark = "";
+ const endMark = "";
+
+ const end = start + query.length;
+ text =
+ text.slice(0, start) +
+ startMark +
+ text.slice(start, end) +
+ endMark +
+ text.slice(end);
+ const startInfo = clipStart(text, start);
+ const endInfo = clipEnd(
+ text,
+ startInfo.position + startMark.length + endMark.length
+ );
+ text =
+ startInfo.prefix +
+ text.slice(startInfo.position, endInfo.position) +
+ endInfo.suffix;
+
+ return text;
+ } else {
+ return text;
+ }
+ } else {
+ return text;
+ }
+}
+
+function clipStart(text, pos) {
+ const clipStart = pos - 50;
+ if (clipStart < 0) {
+ // This will just return the start of the string
+ return {
+ position: 0,
+ prefix: "",
+ };
+ } else {
+ // We're clipping before the start of the string, walk backwards to the first space.
+ const spacePos = findSpace(text, pos, -1);
+ return {
+ position: spacePos.position,
+ prefix: "",
+ };
+ }
+}
+
+function clipEnd(text, pos) {
+ const clipEnd = pos + 200;
+ if (clipEnd > text.length) {
+ return {
+ position: text.length,
+ suffix: "",
+ };
+ } else {
+ const spacePos = findSpace(text, clipEnd, 1);
+ return {
+ position: spacePos.position,
+ suffix: spacePos.clipped ? "…" : "",
+ };
+ }
+}
+
+function findSpace(text, start, step) {
+ let stepPos = start;
+ while (stepPos > -1 && stepPos < text.length) {
+ const char = text[stepPos];
+ if (char === " " || char === "," || char === ":") {
+ return {
+ position: step === 1 ? stepPos : stepPos - step,
+ clipped: stepPos > 1 && stepPos < text.length,
+ };
+ }
+ stepPos = stepPos + step;
+ }
+
+ return {
+ position: stepPos - step,
+ clipped: false,
+ };
+}
+
+// removes highlighting as implemented by the mark tag
+function clearHighlight(searchterm, el) {
+ const childNodes = el.childNodes;
+ for (let i = childNodes.length - 1; i >= 0; i--) {
+ const node = childNodes[i];
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ if (
+ node.tagName === "MARK" &&
+ node.innerText.toLowerCase() === searchterm.toLowerCase()
+ ) {
+ el.replaceChild(document.createTextNode(node.innerText), node);
+ } else {
+ clearHighlight(searchterm, node);
+ }
+ }
+ }
+}
+
+function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
+}
+
+// highlight matches
+function highlight(term, el) {
+ const termRegex = new RegExp(term, "ig");
+ const childNodes = el.childNodes;
+
+ // walk back to front avoid mutating elements in front of us
+ for (let i = childNodes.length - 1; i >= 0; i--) {
+ const node = childNodes[i];
+
+ if (node.nodeType === Node.TEXT_NODE) {
+ // Search text nodes for text to highlight
+ const text = node.nodeValue;
+
+ let startIndex = 0;
+ let matchIndex = text.search(termRegex);
+ if (matchIndex > -1) {
+ const markFragment = document.createDocumentFragment();
+ while (matchIndex > -1) {
+ const prefix = text.slice(startIndex, matchIndex);
+ markFragment.appendChild(document.createTextNode(prefix));
+
+ const mark = document.createElement("mark");
+ mark.appendChild(
+ document.createTextNode(
+ text.slice(matchIndex, matchIndex + term.length)
+ )
+ );
+ markFragment.appendChild(mark);
+
+ startIndex = matchIndex + term.length;
+ matchIndex = text.slice(startIndex).search(new RegExp(term, "ig"));
+ if (matchIndex > -1) {
+ matchIndex = startIndex + matchIndex;
+ }
+ }
+ if (startIndex < text.length) {
+ markFragment.appendChild(
+ document.createTextNode(text.slice(startIndex, text.length))
+ );
+ }
+
+ el.replaceChild(markFragment, node);
+ }
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
+ // recurse through elements
+ highlight(term, node);
+ }
+ }
+}
+
+/* Link Handling */
+// get the offset from this page for a given site root relative url
+function offsetURL(url) {
+ var offset = getMeta("quarto:offset");
+ return offset ? offset + url : url;
+}
+
+// read a meta tag value
+function getMeta(metaName) {
+ var metas = window.document.getElementsByTagName("meta");
+ for (let i = 0; i < metas.length; i++) {
+ if (metas[i].getAttribute("name") === metaName) {
+ return metas[i].getAttribute("content");
+ }
+ }
+ return "";
+}
+
+function algoliaSearch(query, limit, algoliaOptions) {
+ const { getAlgoliaResults } = window["@algolia/autocomplete-preset-algolia"];
+
+ const applicationId = algoliaOptions["application-id"];
+ const searchOnlyApiKey = algoliaOptions["search-only-api-key"];
+ const indexName = algoliaOptions["index-name"];
+ const indexFields = algoliaOptions["index-fields"];
+ const searchClient = window.algoliasearch(applicationId, searchOnlyApiKey);
+ const searchParams = algoliaOptions["params"];
+ const searchAnalytics = !!algoliaOptions["analytics-events"];
+
+ return getAlgoliaResults({
+ searchClient,
+ queries: [
+ {
+ indexName: indexName,
+ query,
+ params: {
+ hitsPerPage: limit,
+ clickAnalytics: searchAnalytics,
+ ...searchParams,
+ },
+ },
+ ],
+ transformResponse: (response) => {
+ if (!indexFields) {
+ return response.hits.map((hit) => {
+ return hit.map((item) => {
+ return {
+ ...item,
+ text: highlightMatch(query, item.text),
+ };
+ });
+ });
+ } else {
+ const remappedHits = response.hits.map((hit) => {
+ return hit.map((item) => {
+ const newItem = { ...item };
+ ["href", "section", "title", "text"].forEach((keyName) => {
+ const mappedName = indexFields[keyName];
+ if (
+ mappedName &&
+ item[mappedName] !== undefined &&
+ mappedName !== keyName
+ ) {
+ newItem[keyName] = item[mappedName];
+ delete newItem[mappedName];
+ }
+ });
+ newItem.text = highlightMatch(query, newItem.text);
+ return newItem;
+ });
+ });
+ return remappedHits;
+ }
+ },
+ });
+}
+
+function fuseSearch(query, fuse, fuseOptions) {
+ return fuse.search(query, fuseOptions).map((result) => {
+ const addParam = (url, name, value) => {
+ const anchorParts = url.split("#");
+ const baseUrl = anchorParts[0];
+ const sep = baseUrl.search("\\?") > 0 ? "&" : "?";
+ anchorParts[0] = baseUrl + sep + name + "=" + value;
+ return anchorParts.join("#");
+ };
+
+ return {
+ title: result.item.title,
+ section: result.item.section,
+ href: addParam(result.item.href, kQueryArg, query),
+ text: highlightMatch(query, result.item.text),
+ };
+ });
+}
diff --git a/_proc/_docs/sitemap.xml b/_proc/_docs/sitemap.xml
index 7962ef429839b9d84125376b8dbf76d1c9652453..166b665a16e7fc585ce149eac002b7d13a7c98ba 100644
--- a/_proc/_docs/sitemap.xml
+++ b/_proc/_docs/sitemap.xml
@@ -1,3 +1,27 @@