Skip to content

Commit 43e4c94

Browse files
committed
Add sprite style switching
1 parent a374418 commit 43e4c94

8 files changed

Lines changed: 139 additions & 7 deletions

File tree

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ The project starts from:
2323

2424
It keeps the map and AI chat as real ArcGIS web components, then wraps them in Tiny Assistant, with a tiny movable assistant character named Globby.
2525

26+
The character layer is designed around Codex-style pet spritesheets, so it can
27+
also work with compatible sprites from projects like
28+
[Petdex](https://petdex.crafter.run/) when they follow the same atlas layout.
29+
For example, this repo includes a local copy of the
30+
[Boba sprite](https://petdex.crafter.run/pets/boba).
31+
2632
## Try The Demo
2733

2834
Open the live demo:
@@ -87,6 +93,11 @@ body { overflow: hidden; }
8793
<tiny-arcgis-assistant
8894
reference-element="#map"
8995
sprite-src="https://ceddc.github.io/tiny-assistant/assets/globby-spritesheet.webp"
96+
sprites='{
97+
"globby": "https://ceddc.github.io/tiny-assistant/assets/globby-spritesheet.webp",
98+
"clippy": "https://ceddc.github.io/tiny-assistant/assets/clippy-spritesheet.webp",
99+
"boba": "https://ceddc.github.io/tiny-assistant/assets/boba-spritesheet.webp"
100+
}'
90101
heading="My map assistant"
91102
description="Ask questions about this map"
92103
suggested-prompts='["Summarize this map.", "Zoom to the most important feature."]'>
@@ -105,8 +116,10 @@ the local built module from `dist`.
105116
GitHub Pages hosts your built `tiny-assistant.js` and sprite assets;
106117
`https://js.arcgis.com/5.0/` remains Esri's ArcGIS CDN. The public custom element
107118
is `tiny-arcgis-assistant`. It accepts `reference-element`, `sprite-src`,
108-
`heading`, `description`, optional `start-hidden`, and optional
109-
`suggested-prompts`.
119+
`heading`, `description`, optional `start-hidden`, optional
120+
`suggested-prompts`, optional `sprite`, and optional `sprites`. When multiple
121+
sprites are configured, the right-click menu includes a Style section for
122+
switching between them.
110123

111124
## ArcGIS Account Requirement
112125

@@ -164,7 +177,7 @@ the built `tiny-assistant.js` file published by GitHub Pages.
164177
- `src/components/tiny-arcgis-assistant.js` defines the public
165178
`<tiny-arcgis-assistant>` element.
166179
- `src/components/tiny-assistant-character.js` renders and animates Globby from
167-
the spritesheet.
180+
Codex-style spritesheets.
168181
- `src/lib/tiny-assistant-controller.js` wires sign-in, chat open/close,
169182
positioning, ArcGIS assistant styling patches, and animation states.
170183
- `index.html` is the main demo page.

external-user.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<tiny-arcgis-assistant
2929
reference-element="#map"
3030
sprite-src="./assets/globby-spritesheet.webp"
31+
sprites='{"globby":"./assets/globby-spritesheet.webp","clippy":"./assets/clippy-spritesheet.webp","boba":"./assets/boba-spritesheet.webp"}'
3132
heading="Change in wheat production 2017-2022"
3233
description="Explore wheat production in the U.S. from 2017 to 2022."
3334
suggested-prompts='["Go to the county that produced the most wheat in 2022.", "How does that compare to the average county that produced wheat?", "How many counties produced less wheat in 2022 than in 2017?"]'>

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575

7676
<tiny-arcgis-assistant
7777
sprite-src="./assets/globby-spritesheet.webp"
78+
sprites='{"globby":"./assets/globby-spritesheet.webp","clippy":"./assets/clippy-spritesheet.webp","boba":"./assets/boba-spritesheet.webp"}'
7879
reference-element="#demo-map"
7980
suggested-prompts='["Go to the county that produced the most wheat in 2022.", "How does that compare to the average county that produced wheat?", "How many counties produced less wheat in 2022 than in 2017?"]'
8081
heading="Change in wheat production 2017-2022"
1.84 MB
Loading
1.32 MB
Loading

src/components/tiny-arcgis-assistant.js

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,53 @@ function parseSuggestedPrompts(value) {
7575
}
7676
}
7777

78+
function toSpriteLabel(name) {
79+
return name
80+
.replace(/[-_]+/g, " ")
81+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
82+
}
83+
84+
function normalizeSpriteEntries(entries, fallbackSrc) {
85+
const sprites = Object.entries(entries || {})
86+
.map(([name, src]) => [String(name).trim(), String(src).trim()])
87+
.filter(([name, src]) => name && src);
88+
89+
if (!sprites.some(([name]) => name === "globby") && fallbackSrc) {
90+
sprites.unshift(["globby", fallbackSrc]);
91+
}
92+
93+
return sprites;
94+
}
95+
96+
function parseSprites(value, fallbackSrc) {
97+
if (!value) {
98+
return normalizeSpriteEntries({ globby: fallbackSrc }, fallbackSrc);
99+
}
100+
101+
try {
102+
const parsed = JSON.parse(value);
103+
return normalizeSpriteEntries(parsed, fallbackSrc);
104+
} catch {
105+
const entries = {};
106+
value
107+
.split(/[\n,]/)
108+
.map((entry) => entry.trim())
109+
.filter(Boolean)
110+
.forEach((entry) => {
111+
const separatorIndex = entry.indexOf(":");
112+
if (separatorIndex <= 0) {
113+
return;
114+
}
115+
116+
const name = entry.slice(0, separatorIndex).trim();
117+
const src = entry.slice(separatorIndex + 1).trim();
118+
entries[name] = src;
119+
});
120+
121+
return normalizeSpriteEntries(entries, fallbackSrc);
122+
}
123+
}
124+
78125
class TinyArcgisAssistant extends HTMLElement {
79126
#controller = null;
80127

@@ -86,6 +133,12 @@ class TinyArcgisAssistant extends HTMLElement {
86133
const agents = Array.from(this.children);
87134
const startHidden = booleanAttribute(this, "start-hidden");
88135
const spriteSrc = this.getAttribute("sprite-src") || "";
136+
const spriteEntries = parseSprites(this.getAttribute("sprites"), spriteSrc);
137+
const selectedSpriteName =
138+
this.getAttribute("sprite") || spriteEntries[0]?.[0] || "globby";
139+
const selectedSprite =
140+
spriteEntries.find(([name]) => name === selectedSpriteName) ||
141+
spriteEntries[0] || ["globby", spriteSrc];
89142
const referenceElement =
90143
this.getAttribute("reference-element") || "arcgis-map";
91144
const heading = this.getAttribute("heading") || "ArcGIS AI Assistant";
@@ -103,8 +156,8 @@ class TinyArcgisAssistant extends HTMLElement {
103156

104157
const globby = createElement("tiny-assistant-character", {
105158
class: "tiny-assistant-character",
106-
title: "Globby",
107-
"sprite-src": spriteSrc,
159+
title: toSpriteLabel(selectedSprite[0]),
160+
"sprite-src": selectedSprite[1],
108161
hidden: startHidden,
109162
});
110163

@@ -154,7 +207,7 @@ class TinyArcgisAssistant extends HTMLElement {
154207
[card, toggle],
155208
);
156209

157-
const menu = createElement("menu", { class: "globby-menu", hidden: true }, [
210+
const menuChildren = [
158211
createMenuButton(
159212
{
160213
class: "follow-cursor-toggle",
@@ -168,7 +221,33 @@ class TinyArcgisAssistant extends HTMLElement {
168221
{ class: "hide-tiny-assistant-button" },
169222
"Hide Tiny Assistant",
170223
),
171-
]);
224+
];
225+
226+
if (spriteEntries.length > 1) {
227+
menuChildren.push(
228+
createElement("li", { class: "globby-menu-section" }, [
229+
document.createTextNode("Style"),
230+
]),
231+
...spriteEntries.map(([name, src]) =>
232+
createMenuButton(
233+
{
234+
class: "style-toggle",
235+
role: "menuitemradio",
236+
"aria-checked": String(name === selectedSprite[0]),
237+
"data-sprite-name": name,
238+
"data-sprite-src": src,
239+
},
240+
toSpriteLabel(name),
241+
),
242+
),
243+
);
244+
}
245+
246+
const menu = createElement(
247+
"menu",
248+
{ class: "globby-menu", hidden: true },
249+
menuChildren,
250+
);
172251

173252
const showButton = createElement(
174253
"button",

src/lib/tiny-assistant-controller.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export function initializeTinyAssistant(host) {
1313
const hideGlobbyButton = host.querySelector(".hide-tiny-assistant-button");
1414
const panelMenuToggle = host.querySelector(".panel-menu-toggle");
1515
const showGlobbyButton = host.querySelector(".show-tiny-assistant-button");
16+
const styleToggles = Array.from(host.querySelectorAll(".style-toggle"));
1617
const globby = host.querySelector("tiny-assistant-character");
1718
const globbyStatus = host.querySelector(".tiny-assistant-status");
1819
const mapElement = document.querySelector(
@@ -35,6 +36,11 @@ export function initializeTinyAssistant(host) {
3536
let assistantSignatureBeforeSubmit = "";
3637
let watchedAssistantRoots = new WeakSet();
3738
let expandedPanelSide = "left";
39+
let activeSpriteName =
40+
styleToggles.find((button) => button.getAttribute("aria-checked") === "true")
41+
?.dataset.spriteName ||
42+
host.getAttribute("sprite") ||
43+
"globby";
3844
const globbyStartupMode = pageParams.get("globby")?.toLowerCase();
3945
const startWithGlobbyHidden = [
4046
"0",
@@ -555,6 +561,13 @@ export function initializeTinyAssistant(host) {
555561
await openChatOrSignIn();
556562
});
557563

564+
styleToggles.forEach((button) => {
565+
button.addEventListener("click", () => {
566+
setAssistantStyle(button.dataset.spriteName, button.dataset.spriteSrc);
567+
hideGlobbyMenu();
568+
});
569+
});
570+
558571
hideGlobbyButton?.addEventListener("click", () => {
559572
hideGlobbyMenu();
560573
setGlobbyVisible(false);
@@ -718,12 +731,36 @@ export function initializeTinyAssistant(host) {
718731
"aria-checked",
719732
String(followCursorEnabled),
720733
);
734+
styleToggles.forEach((button) => {
735+
button.setAttribute(
736+
"aria-checked",
737+
String(button.dataset.spriteName === activeSpriteName),
738+
);
739+
});
721740
if (panelMenuToggle) {
722741
panelMenuToggle.textContent =
723742
assistantBubble?.dataset.mode === "full" ? "Close chat" : "Open chat";
724743
}
725744
}
726745

746+
function setAssistantStyle(name, src) {
747+
if (!globby || !name || !src) {
748+
return;
749+
}
750+
751+
activeSpriteName = name;
752+
host.setAttribute("sprite", name);
753+
globby.setAttribute("sprite-src", src);
754+
globby.setAttribute("title", toSpriteLabel(name));
755+
updateGlobbyMenuLabels();
756+
}
757+
758+
function toSpriteLabel(name) {
759+
return String(name)
760+
.replace(/[-_]+/g, " ")
761+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
762+
}
763+
727764
async function patchArcgisMapSize() {
728765
if (!mapElement) {
729766
return;

test.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<tiny-arcgis-assistant
3131
reference-element="#map"
3232
sprite-src="https://ceddc.github.io/tiny-assistant/assets/globby-spritesheet.webp"
33+
sprites='{"globby":"https://ceddc.github.io/tiny-assistant/assets/globby-spritesheet.webp","clippy":"https://ceddc.github.io/tiny-assistant/assets/clippy-spritesheet.webp","boba":"https://ceddc.github.io/tiny-assistant/assets/boba-spritesheet.webp"}'
3334
heading="Tiny Assistant"
3435
description="Ask questions about this ArcGIS web map">
3536
<arcgis-assistant-navigation-agent></arcgis-assistant-navigation-agent>

0 commit comments

Comments
 (0)