diff --git a/fetch-discord-users.js b/fetch-discord-users.js index 99734af..5548df5 100644 --- a/fetch-discord-users.js +++ b/fetch-discord-users.js @@ -20,6 +20,7 @@ const userIds = [ "329656222213079053", // eeveon "1296648831370268756", // gleamingk111 "317342307705683968", // elki + "137445249759838208", // Jack5 "86612976529838080", // jar of galaxies "138378536497971200", // mitru "353959847189938196", // rickerd102 diff --git a/src/app.css b/src/app.css index 68fadc7..0e83182 100644 --- a/src/app.css +++ b/src/app.css @@ -61,6 +61,17 @@ button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } +input[type="checkbox"] { + appearance: none; + border-radius: 0; + padding: 0; + height: 25px; + width: 25px; + &:checked { + background: #646cff; + } +} + @media (prefers-color-scheme: light) { :root { color: #213547; diff --git a/src/lib/components/Settings.svelte b/src/lib/components/Settings.svelte index 2f4f63b..4e74a2c 100644 --- a/src/lib/components/Settings.svelte +++ b/src/lib/components/Settings.svelte @@ -5,6 +5,8 @@ let { show, onClose } = $props(); let music_vol = $state(parseFloat(localStorage.getItem("settings.music_volume") ?? "1.0")); let sfx_vol = $state(parseFloat(localStorage.getItem("settings.sfx_volume") ?? "1.0")); + let ollama_enabled = $state(localStorage.getItem("settings.ollama_enabled") === "true"); + let ollama_model = $state(localStorage.getItem("settings.ollama_model") ?? "gemma4:e2b"); let users = await (await fetch("/discord-users.json")).json(); @@ -24,6 +26,10 @@ label: "Playtesting", users: ["1429874676590575907", "317342307705683968"], }, + { + label: "AI integration", + users: ["137445249759838208"] + }, { label: "Feedback & support", users: [ @@ -40,10 +46,12 @@ function onchange() { localStorage.setItem("settings.music_volume", music_vol.toString()); + localStorage.setItem("settings.ollama_model", ollama_model); } function onpointerup() { localStorage.setItem("settings.sfx_volume", sfx_vol.toString()); sfx.bubble(); + localStorage.setItem("settings.ollama_enabled", ollama_enabled.toString()); } @@ -68,6 +76,19 @@ />
+

AI Integration

+

If enabled, element names and icons are generated by a local Ollama model.
No multiworld data is processed by any third party.

+
+ Enabled + +
+ {#if ollama_enabled} +
+ Ollama model name + +
+ {/if} +

Credits

elementipelago

a game made for Archipelago

diff --git a/src/lib/consts.js b/src/lib/consts.js index e8ed461..2292b1b 100644 --- a/src/lib/consts.js +++ b/src/lib/consts.js @@ -11,3 +11,78 @@ export const APWORLD_VERSION_REGEX = new RegExp( ) + ")(-.*)?$", ); + +export const SUBSTITUTE_ICONS = [ + "apple", + "armor", + "ball", + "berry", + "boat", + "book", + "boots", + "car", + "chest", + "element", + "emerald", + "hat", + "heart", + "leaf", + "magic", + "marker", + "metal", + "music", + "potion", + "sand", + "wand", +]; +export const ELEMENT_ICONS = [ + "bow", + "cave", + "coin", + "desert", + "egg", + "fire", + "gun", + "hammer", + "hills", + "hourglass", + "house", + "ice", + "island", + "key", + "map", + "money", + "mountains", + "piece", + "planet", + "quest", + "ring", + "rock", + "shop", + "sign", + "skull", + "spear", + "sword", + "tree", + "upgrade", + "void", + "water", +]; +export const ALL_ICONS = [...SUBSTITUTE_ICONS, ...ELEMENT_ICONS]; +export const ICON_PALETTES = [ + "lightblue", + "turqoise", + "green", + "lime", + "yellow", + "orange", + "warmred", + "red", + "magenta", + "purple", + "indigo", + "blue", + "white", +]; + +export const ELEMENT_SYNONYMS = ["chemical", "element", "ingredient", "material", "substance"]; diff --git a/src/lib/state/apclient.svelte.js b/src/lib/state/apclient.svelte.js index a996dc1..f258dea 100644 --- a/src/lib/state/apclient.svelte.js +++ b/src/lib/state/apclient.svelte.js @@ -2,7 +2,16 @@ import { Client } from "archipelago.js"; import { createSubscriber } from "svelte/reactivity"; import { elementIdToName, elementNameToId } from "../utils"; import { iconForIntermediate, iconForItem, iconForLocation } from "../icons"; -import { INTERMEDIATE_AMOUNT, LOCATION_AMOUNT, NON_ELEMENT_ITEMS } from "../consts"; +import { + ALL_ICONS, + ELEMENT_ICONS, + ELEMENT_SYNONYMS, + ICON_PALETTES, + INTERMEDIATE_AMOUNT, + LOCATION_AMOUNT, + NON_ELEMENT_ITEMS, + SUBSTITUTE_ICONS, +} from "../consts"; import { get_name, init_naming } from "../names.js"; import { createGraph, ElementKind } from "../graph.js"; import { get, readable } from "svelte/store"; @@ -29,6 +38,7 @@ import { get, readable } from "svelte/store"; compound_amount: number, compounds_are_ingredients: number, graph_seed: number, + elements: Record version: string, }} SlotData @@ -142,8 +152,13 @@ class APStore { #eventEmitter = new EventEmitter(); + /** @type {Map} */ + #elementCache = new Map(); + /** @type {Set} */ + #elementQueue = new Set(); + #elementQueueProcessing = -1; // Not processing /** @type {Record} */ - #elementData = {}; + #elementData = $state({}); // Refresh Drawer on update /** @type {Set} */ #receivedElements = new Set(); @@ -224,6 +239,11 @@ class APStore { 4, this.#slotData.compounds_are_ingredients, ); + /** @type {SlotData["elements"] | undefined} */ + const savedElements = await this.#client.storage.fetch("elementipelago_elements", true); + if (savedElements) { + this.#slotData.elements = savedElements; + } const scoutedLocations = this.#client.scout( this.#client.room.allLocations.filter((loc) => loc <= LOCATION_AMOUNT), @@ -250,7 +270,7 @@ class APStore { } for (const item of await scoutedLocations) { - this.#addElementData(item, true); + this.queueElement(item, true); } this.#client.items.hints.forEach((hint) => this.#extendReceivedHints(hint)); @@ -335,37 +355,410 @@ class APStore { /** * @param {Item} item */ - #addElementData(item, isLocation = false) { - const name = isLocation ? item.locationName : item.name; + queueElement(item, isLocation = false) { + let name = isLocation ? item.locationName : item.name; if (this.#elementData[name] !== undefined) { + if (this.#elementData[name].location !== "Loading...") { + return; + } + } else { + const elem_id = elementNameToId(name); + + this.#elementCache.set(name, { item, isLocation }); + this.#elementData[name] = { + elem_id, + name: name, + icon: "/sprites/elements/void.png", + alt: "void", + location: "Loading...", + player: isLocation ? item.receiver.alias : item.sender.alias, + game: isLocation ? item.receiver.game : item.sender.game, + }; + } + + const newElement = this.#receivedElements.has(name) || this.#sentElements.has(name); + if (newElement && !this.#elementQueue.has(name)) { + this.#elementQueue.add(name); + this.#processQueue(); + } + } + + async #processQueue() { + if (this.#elementQueueProcessing >= 0) { return; } + this.#elementQueueProcessing = 0; + + do { + const elementArray = Array.from(this.#elementQueue); + elementArray.sort((a, b) => { + /** @param {string} name */ + const getKind = (name) => { + const id = elementNameToId(name); + return id.kind; + }; + + const kindOrder = [ElementKind.INPUT, ElementKind.INTERMEDIATE, ElementKind.OUTPUT]; + const kindA = kindOrder.indexOf(getKind(a)); + const kindB = kindOrder.indexOf(getKind(b)); + const orderA = kindA === -1 ? 99 : kindA; + const orderB = kindB === -1 ? 99 : kindB; + return orderA - orderB; + }); + + const name = elementArray[this.#elementQueueProcessing]; + if (name !== undefined) { + const entry = this.#elementCache.get(name); + if (entry !== undefined) { + try { + await this.#addElementData(entry.item, entry.isLocation); + this.#elementQueue.delete(name); + this.#elementQueueProcessing = -1; // Reset to 0 + } catch (e) { + console.error( + `Failed to process element "${name}", temporarily skipping`, + e, + ); + if (this.#elementQueueProcessing < elementArray.length) { + await new Promise((r) => setTimeout(r, 10)); + } else { + console.error("All elements in queue failing, stopping"); + this.#elementQueueProcessing = -2; + } + } + } + } else { + this.#elementQueueProcessing = -2; + } + this.#elementQueueProcessing++; // Increase error count or go to -1 on stop + } while (this.#elementQueueProcessing >= 0); + } + + /** + * @param {string} userPrompt + */ + async #queryOllama(userPrompt, newName = true, iconAndColor = true) { + // Show only some icons and colors to the AI to see more random pictures + let randomIcons = [...ALL_ICONS]; + randomIcons.sort((_a, _b) => Math.floor(Math.random() * 2) * 2 - 1); + randomIcons = randomIcons.slice(0, 15); + let randomColors = [...ICON_PALETTES]; + randomColors.sort((_a, _b) => Math.floor(Math.random() * 2) * 2 - 1); + randomColors = randomColors.slice(0, 5); + // System prompt must only contain general guidance, no specific direction or restrictions + let systemPrompt = `You are an experienced alchemist and natural philosopher cataloguing newly discovered alchemical components in modern times. Your expertise is in${newName ? " naming them based on their unusual origins and recipes (NOT icons/colors)" : ""}${newName && iconAndColor ? ", and" : ""}${iconAndColor ? " categorizing their appearances with a single icon and color each." : ""}\nFor every request,${newName ? " produce a name that is a creative and archaic re-invention of the provided info" : ""}${newName && iconAndColor ? ", and" : ""}${iconAndColor ? " choose one each of the available icons and colors" : ""}.${iconAndColor ? `\nIcons (choose appearance-accurate): ${randomIcons.join(",")}\nColors: ${randomColors.join(",")}` : ""}`; + + /** @type {{ type: string; properties: { name?: object; icon?: object; color?: object; }; required: string[]; }} */ + const schema = { + type: "object", + properties: { + name: { type: "string" }, + icon: { type: "string", enum: [...SUBSTITUTE_ICONS, ...ELEMENT_ICONS] }, + color: { type: "string", enum: ICON_PALETTES }, + }, + required: ["name", "icon", "color"], + }; + if (!newName) { + delete schema.properties.name; + schema.required.shift(); + } + if (!iconAndColor) { + delete schema.properties.icon; + delete schema.properties.color; + schema.required.pop(); + schema.required.pop(); + } + + try { + console.log(`$ ${userPrompt}`); + const response = await fetch("http://localhost:11434/api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: localStorage.getItem("settings.ollama_model") ?? "gemma4:e2b", + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: userPrompt }, + ], + format: schema, + think: false, + stream: false, + }), + }); + const json = JSON.parse((await response.json()).message.content); + console.log(`> ${JSON.stringify(json)}`); + return json; + } catch (e) { + console.error("Ollama query failed", e); + throw e; + } + } + + /** + * @param {number} num + */ + toRoman(num) { + const numerals = { + M: 1000, + CM: 900, + D: 500, + CD: 400, + C: 100, + XC: 90, + L: 50, + XL: 40, + X: 10, + IX: 9, + V: 5, + IV: 4, + I: 1, + }; + let roman = ""; + for (let i in numerals) { + // @ts-ignore + while (num >= numerals[i]) { + roman += i; + // @ts-ignore + num -= numerals[i]; + } + } + return roman; + } + + /** + * @param {string} loc + * @param {string} name + */ + nameLocationUniquely(loc, name) { + const existingLocs = Object.values(this.#elementData) + .filter((elem) => elem && elem.location !== "Loading..." && elem.name !== name) + .map((elem) => elem.location); + + const romanRegex = /\s*([IVXLCDM]+)$/; + let baseLoc = loc.replace(romanRegex, ""); + + let i = 2; + while (existingLocs.includes(loc)) { + loc = `${baseLoc} ${this.toRoman(i)}`.trim(); + i++; + } + return loc; + } + + /** + * @param {Item} item + * @param {boolean} isLocation + */ + async #addElementData(item, isLocation) { + let name = isLocation ? item.locationName : item.name; const displayName = isLocation ? item.name : item.locationName; const elem_id = elementNameToId(name); + const ollamaEnabled = localStorage.getItem("settings.ollama_enabled") === "true"; + let loc, icon_name; - if (elem_id.kind === ElementKind.INTERMEDIATE) { - loc = get_name(); - icon_name = iconForIntermediate(loc); + const elementSlotData = this.slotData.elements?.[name]; + if (elementSlotData) { + loc = elementSlotData.location; + icon_name = elementSlotData.alt; + } else if (!ollamaEnabled) { + if (elem_id.kind === ElementKind.INTERMEDIATE) { + loc = get_name(); + icon_name = iconForIntermediate(loc); + } else { + loc = + elem_id.kind === ElementKind.INTERMEDIATE || item.locationGame === "Archipelago" + ? get_name() + : displayName; + + icon_name = isLocation + ? iconForLocation(item.game, loc) + : iconForItem(item.game, loc); + } } else { - loc = - elem_id.kind === ElementKind.INTERMEDIATE || item.locationGame === "Archipelago" - ? get_name() - : displayName; + const startingElement = item.locationGame === "Archipelago"; + const fromOtherPlayer = + item.sender && item.sender.slot !== this.#client.players.self.slot; + const elementSynonym = + ELEMENT_SYNONYMS[Math.floor(Math.random() * ELEMENT_SYNONYMS.length)]; + + let response; + if (startingElement) { + // Elements available from the start get fixed seed names + loc = get_name(); + response = await this.#queryOllama( + `Select an icon and color for the element "${loc}"`, + false, + ); + } else if (fromOtherPlayer) { + // Elements sent by other players are named based on the sender location name (player and world information is not used as it leads to repetitive names, Progressive items do not trigger this) + response = await this.#queryOllama( + `A new element was discovered within the location/artifact "${item.locationName}".\nTransform the location's wording into an alchemical name, without omitting spaces nor adding common English terms. The final name MUST be 1-3 words long, and may use mixed case (e.g., "X of Y").\nAlso choose a relevant icon and an accurate color (or a fantasical color if the location is significantly unusual within its icon class).`, + ); + } else { + // All other elements are combinations with parents + /** @type {ElementData | undefined} */ + let parent1; + /** @type {ElementData | undefined} */ + let parent2; + let fromRecipe = false; + for (const [[i1, i2], ps] of this.#graph.recipes.entries()) { + for (const p of ps) { + if (elementIdToName(p) === name) { + const parent1Name = elementIdToName(i1); + const parent2Name = elementIdToName(i2); + if (parent1Name === undefined || parent2Name === undefined) { + throw new Error( + `Element "${name}" parent names unexpectedly undefined`, + ); + } + parent1 = this.#elementData[parent1Name]; + parent2 = this.#elementData[parent2Name]; + if ( + parent1 && + parent2 && + parent1.location !== "Loading..." && + parent2.location !== "Loading..." + ) { + fromRecipe = true; + break; + } + } + } + if (fromRecipe) break; + } - icon_name = isLocation ? iconForLocation(item.game, loc) : iconForItem(item.game, loc); + if (!fromRecipe) { + // Element is a compound, treat single parent element (once it has been named) as both parents + const compoundParent = this.#elementData[item.locationName]; + if (compoundParent && compoundParent.location !== "Loading...") { + parent1 = compoundParent; + parent2 = parent1; + } + } + + if ( + !parent1 || + parent1.location === "Loading..." || + !parent2 || + parent2.location === "Loading..." + ) { + throw new Error(`Element "${name}" parents not yet processed'`); + } + const parent1Appearance = parent1.alt.replace("-", " "); + const parent2Appearance = parent2.alt.replace("-", " "); + const sameParents = parent1.elem_id === parent2.elem_id; + + const forOtherPlayer = + item.receiver && item.receiver.slot !== this.#client.players.self.slot; + + if (!forOtherPlayer) { + // Combinations that create new elements or items are named based on their parents' names + if (item.name === "Progressive Item Limit") { + // Progressive Item Limit allows more elements on screen at once + response = await this.#queryOllama( + `A by-product is created in the reaction from ${!sameParents ? "combining" : "the transmutation of"} "${parent1.location}" (icon/color: ${parent1Appearance})${!sameParents ? ` and "${parent2.location}" (icon/color: ${parent2Appearance})` : ""}, and it has an unlimiting effect.\nTransform and expand the provided names and effect (NOT the icons/colors) to create the name of the by-product. The final name MUST be 1-3 words long and use a synonym of an important word. It may also use adjectives, mixed case (e.g., "X of Y") and gibberish Latin (for shortening).\nAlso choose a fitting icon that is not used by the ingredients, and one of the ingredients' colors.`, + ); + } else if (item.name === "Progressive Filter") { + // Progressive Filter adds indications of progress to the UI + response = await this.#queryOllama( + `A by-product is created in the reaction from ${!sameParents ? "combining" : "the transmutation of"} "${parent1.location}" (icon/color: ${parent1Appearance})${!sameParents ? ` and "${parent2.location}" (icon/color: ${parent2Appearance})` : ""}, and it has a filtering effect.\nTransform and expand the provided names and effect (NOT the icons/colors) to create the name of the by-product. The final name MUST be 1-3 words long and use a synonym of an important word. It may also use adjectives, mixed case (e.g., "X of Y") and gibberish Latin (for shortening).\nAlso choose a fitting icon that is not used by the ingredients, and one of the ingredients' colors.`, + ); + } else if (!fromRecipe) { + // Compounds are by-products of combinations, they get a random icon and color + icon_name = ALL_ICONS[Math.floor(Math.random() * ALL_ICONS.length)]; + if (SUBSTITUTE_ICONS.includes(icon_name)) { + icon_name = `${icon_name}-${ICON_PALETTES[Math.floor(Math.random() * ICON_PALETTES.length)]}`; + } + const compoundLook = icon_name.split("-"); + compoundLook.reverse(); + const compoundLookStr = compoundLook.join(" "); + const compoundLookVowel = /^[aeiou]/i.test(compoundLookStr); + response = await this.#queryOllama( + `A by-product is created in the chemical reaction that produces "${parent1.location}" (icon/color: ${parent1Appearance}), and it is different in composition, having the appearance of ${compoundLookVowel ? "an" : "a"} ${compoundLookStr}.\nCreate a believable alchemical name for the by-product that contains a tangential reference to the ingredient's name. The final name MUST be 1-3 words long and use a synonym of an important word. It may also use adjectives, mixed case (e.g., "X of Y") and gibberish Latin (for shortening).`, + true, + false, + ); + } else { + // All other items are elements + response = await this.#queryOllama( + `A new ${elementSynonym} was created in the reaction from ${!sameParents ? "combining" : "the transmutation of"} "${parent1.location}" (icon/color: ${parent1Appearance})${!sameParents ? ` and "${parent2.location}" (icon/color: ${parent2Appearance})` : ""}.\nTransform and expand the ingredient name${!sameParents ? "s" : ""} (NOT the icons/colors) into a new alchemical name without omitting spaces. The final name MUST be 1-3 words long and use a synonym of an important word. It may also use adjectives, mixed case (e.g., "X of Y") and gibberish Latin (for shortening).\n${parent1Appearance !== parent2Appearance ? "Also choose the icon of one ingredient and the color of the other." : `Also choose an icon and color, one or both of which MUST be different from yet still be relevant to the ingredient${!sameParents ? "s" : ""}.`}`, + ); + } + } else { + // Combinations that send items to other players are named differently depending on what game they are sending to and if they are ingredients or not + let ingredient = false; + for (const [i1, i2] of this.#graph.recipes.keys()) { + if (elementIdToName(i1) === name || elementIdToName(i2) === name) { + ingredient = true; + break; + } + } + + if (item.receiver.game !== "Elementipelago") { + if (!ingredient) { + // Those that send items to NON-Elementipelago players and are NOT ingredients are not given new names + loc = displayName; + response = await this.#queryOllama( + `Select an accurate icon and color for the item "${loc}"`, + false, + ); + } else { + // Those that send items to NON-Elementipelago players but ARE ingredients are named based on their metadata + response = await this.#queryOllama( + `A by-product was created in the reaction from ${!sameParents ? "combining" : "the transmutation of"} "${parent1.location}" (icon/color: ${parent1Appearance})${!sameParents ? ` and "${parent2.location}" (icon/color: ${parent2Appearance})` : ""}, which is a component of a greater whole.\nTransform and expand the ingredient names (NOT the icons/colors) into a new alchemical name without omitting spaces. The name MUST allude to its greater whole (named "${item.name}") and be 1-3 words long. It may also use mixed case (e.g., "X of Y") and gibberish Latin (for shortening).\nAlso choose a fitting icon and color that are not used by the ingredients.`, + ); + } + } else if (!ingredient) { + // Those that send items to Elementipelago players but are NOT ingredients are fully dedicated to the receiver + response = await this.#queryOllama( + `A new ${elementSynonym} is assumed to be created in the reaction from ${!sameParents ? "combining" : "the transmutation of"} "${parent1.location}" (icon/color: ${parent1Appearance})${!sameParents ? ` and "${parent2.location}" (icon/color: ${parent2Appearance})` : ""}. This undiscovered ${elementSynonym} has been dedicated to the alchemist "${item.receiver.alias}" (omit all forms of "Doodle God" and "Elementipelago").\nCreate its name based on the ingredient and dedication names (NOT the icons/colors), it should be vague/illusory enough so it can take any form once discovered. The final name MUST be 1-3 words long, and may also use adjectives and mixed case (e.g., "X of Y").\nAlso choose an icon and color, one or both of which MUST be different from the ingredient${!sameParents ? "s" : ""}.`, + ); + } else { + // Those that send items to Elementipelago players and ARE ingredients pay homage to the receiver + response = await this.#queryOllama( + `A new ${elementSynonym} was created in the reaction from ${!sameParents ? "combining" : "the transmutation of"} "${parent1.location}" (icon/color: ${parent1Appearance})${!sameParents ? ` and "${parent2.location}" (icon/color: ${parent2Appearance})` : ""}. Its discovery has led to various advancements by the alchemist "${item.receiver.alias}" (omit all forms of "Doodle God" and "Elementipelago").\nTransform and expand the ingredient name${!sameParents ? "s" : ""} (NOT the icons/colors) and homage into a new alchemical name without omitting spaces. The final name MUST be 1-3 words long and use a synonym of an important word. It may also use adjectives, mixed case (e.g., "X of Y") and gibberish Latin (for shortening).\n${parent1Appearance !== parent2Appearance ? "Also choose the icon of one ingredient and the color of the other." : `Also choose an icon and color, one or both of which MUST be different from yet still be relevant to the ingredient${!sameParents ? "s" : ""}.`}`, + ); + } + } + } + + if ("name" in response) { + loc = response.name; + } + if ("icon" in response && "color" in response) { + icon_name = response.icon; + if (SUBSTITUTE_ICONS.includes(response.icon)) { + icon_name = `${response.icon}-${response.color}`; + } + } } + if (loc === "Loading...") { + throw new Error(`Element "${name}" unexpectedly not named'`); + } + if (!elementSlotData) { + loc = this.nameLocationUniquely(loc, name); + } this.#elementData[name] = { - elem_id, - name: name, + ...this.#elementData[name], icon: "/sprites/elements/" + icon_name + ".png", alt: icon_name, location: loc, - player: isLocation ? item.receiver.alias : item.sender.alias, - game: isLocation ? item.receiver.game : item.sender.game, }; + + if (!elementSlotData && ollamaEnabled) { + this.slotData.elements ??= {}; + this.slotData.elements[name] = { + location: loc, + alt: icon_name, + }; + this.#client.storage + .prepare("elementipelago_elements", {}) + .update({ [name]: this.slotData.elements[name] }) + .commit(); + } } #updateElementSets() { @@ -417,7 +810,7 @@ class APStore { newLocalReceived.push(item.name); } this.#receivedElements.add(item.name); - this.#addElementData(item); + this.queueElement(item); } const oldUpgrades = { ...this.#localGameData.current.upgrades }; @@ -440,9 +833,13 @@ class APStore { */ #extendSentElements(locations) { for (const location of locations) { - this.#sentElements.add( - this.#client.package.lookupLocationName("Elementipelago", location), - ); + const name = this.#client.package.lookupLocationName("Elementipelago", location); + this.#sentElements.add(name); + + const entry = this.#elementCache.get(name); + if (entry !== undefined) { + this.queueElement(entry.item, entry.isLocation); + } } } diff --git a/src/lib/state/toast.svelte.js b/src/lib/state/toast.svelte.js index 1b34d57..f6a3cb5 100644 --- a/src/lib/state/toast.svelte.js +++ b/src/lib/state/toast.svelte.js @@ -13,7 +13,7 @@ import { apstore } from "./apclient.svelte"; export const toast_queue = $state([]); /** - * @param {Item[]} items + * @param {Item[]} elements */ apstore.on("elementsReceived", (elements) => { if (elements.length > 0) { @@ -39,9 +39,11 @@ apstore.on("upgradesReceived", (oldUpgrades, newUpgrades) => { @returns {{title: string, description: string, image: string}} */ function elementsReceivedMessage(elements) { + const ollamaEnabled = localStorage.getItem("settings.ollama_enabled") === "true"; + const first_item_data = apstore.elementData[elements[0].name]; let image = first_item_data.icon; - let first_item = first_item_data.location; + let first_item = !ollamaEnabled ? first_item_data.location : first_item_data.name; let others_suffix = elements.length > 1 ? ` + ${elements.length - 1} more` : ""; let description = `${first_item}${others_suffix}`;