Skip to content

Commit 7c90490

Browse files
leotleebwalderman
andauthored
User/leolee/web model context (#1094)
* Add WebModelContext explainer * Updated example. --------- Co-authored-by: Brandon Walderman <[email protected]>
1 parent 5be5ab0 commit 7c90490

File tree

8 files changed

+653
-0
lines changed

8 files changed

+653
-0
lines changed
438 KB
Loading
21.6 KB
Loading
551 KB
Loading

WebModelContext/example/index.html

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Stamp Collecting Database</title>
7+
<link rel="stylesheet" href="./style.css">
8+
</head>
9+
<body>
10+
<div id="app">
11+
<h1>Historical Stamp Database</h1>
12+
13+
<div id="content">
14+
<!-- Stamp collection list -->
15+
<div id="stampSection">
16+
<h2>Browse Unique Stamps</h2>
17+
<div id="stampCollection">
18+
<!-- Stamps loaded here -->
19+
</div>
20+
</div>
21+
22+
<!-- Add stamp Section -->
23+
<div id="addStampSection">
24+
<h2>Add a Historical Stamp</h2>
25+
<form id="addStampForm">
26+
<input type="text" id="stampName" placeholder="Stamp Name" required>
27+
<textarea id="stampDescription" placeholder="Description" rows="3" required></textarea>
28+
<input type="number" id="stampYear" placeholder="Year (e.g., 1847)" required>
29+
<input type="text" id="stampImageUrl" placeholder="Image URL (optional)">
30+
<button type="submit">Add Stamp</button>
31+
</form>
32+
33+
<div id="confirmationMessage"></div>
34+
</div>
35+
</div>
36+
</div>
37+
<script src="./script.js"></script>
38+
</body>
39+
</html>

WebModelContext/example/script.js

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// Authors: Andrew Nolan, Brandon Walderman
2+
// Date: 7-16-2025
3+
// Sample code. NOT to be used in production settings.
4+
5+
// Predefined stamps for the initial collection
6+
let stamps = [
7+
{ name: 'Blue Penny', description: 'A famous stamp from Mauritius, issued in 1847.', year: 1847, imageUrl: 'images/blue-penny.jpg' },
8+
{ name: 'Inverted Jenny', description: 'A US stamp featuring an upside-down airplane, issued in 1918.', year: 1918, imageUrl: 'images/inverted-jenny.png' },
9+
{ name: 'Penny Black', description: 'The world’s first adhesive stamp, issued in 1840 in the UK.', year: 1840, imageUrl: 'images/penny-black.png' },
10+
];
11+
12+
// Function to render the stamp collection
13+
function renderStamps() {
14+
const stampCollectionDiv = document.getElementById('stampCollection');
15+
stampCollectionDiv.innerHTML = stamps.map(stamp => {
16+
// Dynamically build the stamp's HTML
17+
let stampHtml = `
18+
<div>
19+
<p><strong>${stamp.name}</strong> (${stamp.year}): ${stamp.description}</p>
20+
`;
21+
22+
// Only add the image if a valid URL is present
23+
if (stamp.imageUrl) {
24+
stampHtml += `<img src="${stamp.imageUrl}" alt="${stamp.name}" />`;
25+
}
26+
27+
stampHtml += `</div>`;
28+
return stampHtml;
29+
}).join('');
30+
}
31+
32+
// Function to handle adding a new stamp
33+
function addStamp(stampName, stampDescription, stampYear, stampImageUrl) {
34+
// Add the new stamp to the collection
35+
stamps.push({
36+
name: stampName,
37+
description: stampDescription,
38+
year: stampYear,
39+
imageUrl: stampImageUrl || null
40+
});
41+
42+
// Confirm addition and update the collection
43+
document.getElementById('confirmationMessage').textContent = `Stamp "${stampName}" added successfully!`;
44+
renderStamps();
45+
}
46+
47+
document.getElementById('addStampForm').addEventListener('submit', (event) => {
48+
event.preventDefault();
49+
50+
const stampName = document.getElementById('stampName').value;
51+
const stampDescription = document.getElementById('stampDescription').value;
52+
const stampYear = document.getElementById('stampYear').value;
53+
const stampImageUrl = document.getElementById('stampImageUrl').value;
54+
55+
addStamp(stampName, stampDescription, stampYear, stampImageUrl);
56+
});
57+
58+
if ('agent' in window) {
59+
window.agent.provideContext({
60+
tools: [
61+
{
62+
name: "add-stamp",
63+
description: "Add a new stamp to the collection",
64+
params: {
65+
type: "object",
66+
properties: {
67+
name: { type: "string", description: "The name of the stamp" },
68+
description: { type: "string", description: "A brief description of the stamp" },
69+
year: { type: "number", description: "The year the stamp was issued" },
70+
imageUrl: { type: "string", description: "An optional image URL for the stamp" }
71+
},
72+
required: ["name", "description", "year"]
73+
},
74+
},
75+
{
76+
name: "search-stamp",
77+
description: "Search for stamps by name or year",
78+
params: {
79+
type: "object",
80+
properties: {
81+
name: { type: "string", description: "The name of the stamp to search for" },
82+
year: { type: "number", description: "The year of the stamp to search for" },
83+
},
84+
},
85+
}
86+
]
87+
});
88+
89+
window.addEventListener('toolcall', async (e) => {
90+
if (e.name === 'add-stamp') {
91+
const { name, description, year, imageUrl } = e.input;
92+
addStamp(name, description, year, imageUrl);
93+
94+
return e.respondWith({
95+
content: [
96+
{
97+
type: "text",
98+
text: `Stamp "${name}" added successfully! The collection now contains ${stamps.length} stamps.`,
99+
},
100+
]
101+
});
102+
} else if (e.name === 'search-stamp') {
103+
const { name, year } = e.input;
104+
105+
// Filter stamps by name or year
106+
const results = stamps.filter((stamp) =>
107+
(name && stamp.name.toLowerCase().includes(name.toLowerCase())) ||
108+
(year && stamp.year === year)
109+
);
110+
111+
if (results.length === 0) {
112+
return {
113+
content: [
114+
{
115+
type: "text",
116+
text: "No stamps found matching the search criteria.",
117+
},
118+
],
119+
};
120+
}
121+
122+
// Format results for MCP
123+
const formattedResults = results.map((stamp) => ({
124+
name: stamp.name,
125+
year: stamp.year,
126+
description: stamp.description,
127+
imageUrl: stamp.imageUrl || "No image available",
128+
}));
129+
130+
const responseText = formattedResults
131+
.map(
132+
(stamp) =>
133+
`Name: ${stamp.name}\nYear: ${stamp.year}\nDescription: ${stamp.description}\nImage: ${stamp.imageUrl}\n---`
134+
)
135+
.join("\n");
136+
137+
// Prepare content array starting with text
138+
const content = [
139+
{
140+
type: "text",
141+
text: responseText,
142+
},
143+
];
144+
145+
// Add images for stamps that have imageUrl
146+
for (const stamp of results) {
147+
if (stamp.imageUrl && stamp.imageUrl !== "No image available") {
148+
const imageData = await imageUrlToBase64(stamp.imageUrl);
149+
if (imageData) {
150+
content.push({
151+
type: "image",
152+
data: imageData.base64Data,
153+
mimeType: imageData.mimeType,
154+
});
155+
}
156+
}
157+
}
158+
159+
return e.respondWith({ content });
160+
}
161+
})
162+
}
163+
164+
// Helper function to convert image URL to base64
165+
async function imageUrlToBase64(imageUrl) {
166+
try {
167+
const response = await fetch(imageUrl);
168+
if (!response.ok) {
169+
throw new Error(`Failed to fetch image: ${response.status}`);
170+
}
171+
172+
const blob = await response.blob();
173+
const mimeType = blob.type || 'image/jpeg';
174+
175+
return new Promise((resolve, reject) => {
176+
const reader = new FileReader();
177+
reader.onload = () => {
178+
const base64Data = reader.result.split(',')[1];
179+
resolve({ base64Data, mimeType });
180+
};
181+
reader.onerror = reject;
182+
reader.readAsDataURL(blob);
183+
});
184+
} catch (error) {
185+
console.error('Error converting image to base64:', error);
186+
return null;
187+
}
188+
}
189+
190+
// Initial render of stamps
191+
renderStamps();

WebModelContext/example/style.css

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
body {
2+
font-family: Arial, sans-serif;
3+
margin: 0;
4+
padding: 0;
5+
display: flex;
6+
justify-content: center;
7+
align-items: flex-start;
8+
background-color: #f3f4f7;
9+
}
10+
11+
#app {
12+
max-width: 1000px;
13+
text-align: center;
14+
background: white;
15+
padding: 20px;
16+
border-radius: 10px;
17+
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
18+
}
19+
20+
h1 {
21+
color: #333;
22+
}
23+
24+
#content {
25+
display: flex;
26+
gap: 20px; /* Add space between sections */
27+
justify-content: space-between;
28+
}
29+
30+
#stampSection {
31+
flex: 1;
32+
text-align: left;
33+
}
34+
35+
#addStampSection {
36+
flex: 0.6; /* Set smaller size for the form */
37+
text-align: left;
38+
background: #f9f9f9;
39+
padding: 15px;
40+
border-radius: 10px;
41+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
42+
}
43+
44+
#stampCollection, #confirmationMessage {
45+
margin-top: 20px;
46+
font-size: 1.1rem;
47+
color: #333;
48+
}
49+
50+
form input, form textarea, form button {
51+
margin: 10px 0;
52+
padding: 10px;
53+
width: 100%;
54+
}
55+
56+
button {
57+
background-color: #007bff;
58+
color: white;
59+
border: none;
60+
border-radius: 5px;
61+
cursor: pointer;
62+
}
63+
64+
button:hover {
65+
background-color: #0056b3;
66+
}
67+
68+
#stampCollection img {
69+
max-width: 100px;
70+
margin: 10px;
71+
border: 1px solid #ddd;
72+
border-radius: 5px;
73+
}

0 commit comments

Comments
 (0)