LLM Santa is an AI-powered voice assistant that researches gifts with an LLM brain. Writes 5 researched and recommended gift ideas to a google spreadsheet.
Before starting, you'll need the following accounts and API keys:
Google Drive : (link)
To write the output spreadsheet
Make.com : (link)
To create a automation workflow
Free tier available
SERP API : (link)
To generate search results
Free tier available
SystemPrompt.io : (link)
To convert messages into structured data
Free 14 day trial available (must choose execution plan of $30/month)
The first step it to record a voice message. For this we will use the SpeechRecognitionAPI. Learn more .
File: SantaWrapper.tsx
"use client" ;
import Santa from "./Santa" ;
import { useState, useRef } from "react" ;
import "./santa.css" ;
export default function AiSantaClient () {
const [ isTalking , setIsTalking ] = useState ( false );
const [ isRecording , setIsRecording ] = useState ( false );
const [ isButtonDisabled , setIsButtonDisabled ] = useState ( false );
const recognitionRef = useRef < SpeechRecognition | null >( null );
const transcriptRef = useRef < string >( "" );
const webhookUrl = "https://hook.eu2.make.com/wdfuvmtnlo1ra8fjwlw63ealybueyln9" ;
const handleToggleTalking = () => {
if ( ! isTalking) {
// Check for browser support
const SpeechRecognition =
(window as any ).SpeechRecognition ||
(window as any ).webkitSpeechRecognition;
if ( ! SpeechRecognition) {
console. error (
"SpeechRecognition API is not supported in this browser."
);
return ;
}
recognitionRef.current = new SpeechRecognition ();
recognitionRef.current.continuous = false ;
recognitionRef.current.interimResults = false ;
recognitionRef.current.lang = "en-US" ;
recognitionRef.current. onresult = async ( event ) => {
const transcript = event.results[ 0 ][ 0 ].transcript;
transcriptRef.current = transcript;
try {
setIsButtonDisabled ( true );
const response = await fetch (webhookUrl, {
method: "POST" ,
headers: {
"Content-Type" : "application/json" ,
},
body: JSON . stringify ({ transcript }),
}
);
if ( ! response.ok) {
throw new Error ( "Failed to send transcript" );
}
console. log ( "Transcript sent successfully to the webhook." );
} catch (error) {
console. error ( "Error sending transcript:" , error);
} finally {
setIsButtonDisabled ( false );
}
};
recognitionRef.current. onerror = ( event ) => {
console. error ( "Speech recognition error:" , event.error);
};
recognitionRef.current. onend = () => {
setIsRecording ( false );
setIsTalking ( false );
};
recognitionRef.current. start ();
setIsRecording ( true );
setIsTalking ( true );
} else {
if (recognitionRef.current && isRecording) {
recognitionRef.current. stop ();
setIsRecording ( false );
}
setIsTalking ( false );
}
};
return (
< main
className = { `${ styles [ "santa-wrapper" ] } ${ isTalking ? "talking" : ""}` }
>
< Santa isTalking = {isTalking} />
< div className = {styles.actionBar} >
< p className = "instructions" >
Let Santa help you find the perfect present for Christmas
</ p >
< button
onClick = {handleToggleTalking}
className = {styles.toggleButton}
disabled = {isButtonDisabled}
>
{ isTalking ? "Stop Santa Talking" : "Toggle Santa Talking" }
</ button >
</ div >
</ main >
);
If you want to add a sparkling of Santa, you can use the following code to create an illustration of Santa. (credit)
The code will be fully functional without this step, as all functionality is in the SantaWrapper.tsx
file.
File: Santa.tsx
import styles from "./santa.css" ;
interface SantaProps {
isTalking ?: boolean ;
}
export default function Santa ({ isTalking = false } : SantaProps ) {
return (
< div
className = { `${ styles . cartoon } ${ styles . hb } ${ isTalking ? styles . talking : ""}` }
>
< div className = { `${ styles . leg } ${ styles . ha }` } > </ div >
< div className = { `${ styles . leg } ${ styles . ha }` } > </ div >
< div className = { `${ styles . hands } ${ styles . r }` } > </ div >
< div className = {styles.arm} > </ div >
< div className = {styles.body} >
< div className = {styles.belt} > </ div >
< div className = { `${ styles . buttons } ${ styles . r }` } > </ div >
</ div >
< div className = {styles.beard} > </ div >
< div className = { `${ styles . head } ${ styles . r }` } > </ div >
< div className = {styles.mustache} > </ div >
< div className = {styles.mustache} > </ div >
< div className = { `${ styles . cheeks } ${ styles . r }` } > </ div >
< div className = { `${ styles . eyes } ${ styles . r }` } > </ div >
< div className = { `${ styles . hat } ${ styles . ha } ${ styles . hb }` } > </ div >
</ div >
);
}
file: Santa.css
.cartoon {
position : absolute ;
top : 50 % ;
left : 50 % ;
transform : translate ( -50 % , -50 % );
width : 80 vmin ;
height : 80 vmin ;
}
.cartoon div {
position : absolute ;
box-sizing : border-box ;
}
.b {
border : 0.5 vmin solid ;
}
.r {
border-radius : 100 % ;
}
.hb::before ,
.ha::after {
content : "" ;
display : block ;
position : absolute ;
box-sizing : border-box ;
}
/****/
@keyframes snow {
0% {
background-position :
0 0 ,
0 0 ,
0 0 ,
0 0 ;
}
40% {
background-position :
10 px 14 vmin ,
-20 px 28 vmin ,
20 px 20 vmin ,
75 px 20 vmin ;
}
60% {
background-position :
-10 px 21 vmin ,
-30 px 42 vmin ,
30 px 30 vmin ,
50 px 30 vmin ;
}
100% {
background-position :
0 35 vmin ,
0 70 vmin ,
0 50 vmin ,
0 50 vmin ;
}
}
.santa-wrapper {
margin : 0 ;
padding : 0 ;
width : 100 vw ;
height : 100 vh ;
overflow : hidden ;
background : #1585 ;
background-image : radial-gradient (
circle at 50 % 50 % ,
white 2.5 % ,
transparent 0
),
radial-gradient ( circle at 30 % 90 % , white 1.5 % , transparent 0 ),
radial-gradient ( circle at 70 % 10 % , white 1 % , transparent 0 ),
radial-gradient ( circle at 10 % 40 % , white 1 % , transparent 0 );
background-size :
45 vmin 35 vmin ,
50 vmin 70 vmin ,
60 vmin 50 vmin ,
65 vmin 50 vmin ;
background-position :
0 0 ,
0 0 ,
0 0 ,
0 0 ;
}
.cartoon {
--skin : #fca ;
--beard : #eee ;
--eyes : #630a ;
--cheeks : #f001 ;
--belt : #111 ;
--belt-buckle : gold ;
--suit : #a00 ;
}
.head {
width : 25 % ;
height : 25 % ;
background : var ( --skin );
top : 10 % ;
left : 50 % ;
transform : translate ( -50 % , 0 );
}
.beard {
width : 30 % ;
height : 40 % ;
background : var ( --beard );
top : 10 % ;
left : 50 % ;
transform : translate ( -50 % , 0 );
border-radius : 100 % / 120 % 120 % 80 % 80 % ;
}
.mustache {
width : 10 % ;
height : 10 % ;
background : #fff ;
border-radius : 100 % 20 % 100 % 0 ;
top : 31 % ;
left : 51 % ;
transform-origin : top right ;
transform : translate ( -100 % , 0 ) rotate ( 25 deg );
}
.mustache + .mustache {
left : 49 % ;
border-radius : 20 % 100 % 0 100 % ;
transform-origin : top left ;
transform : rotate ( -25 deg );
}
.eyes {
width : 2 % ;
height : 2 % ;
background : var ( --eyes );
top : 23 % ;
left : 45 % ;
box-shadow : 6.66 vmin 0 var ( --eyes );
}
.cheeks {
width : 5 % ;
height : 3 % ;
background : var ( --cheeks );
top : 25.5 % ;
left : 43 % ;
box-shadow : 7.25 vmin 0 var ( --cheeks );
}
.body {
width : 50 % ;
height : 50 % ;
background : var ( --suit );
border-radius : 100 % / 150 % 150 % 25 % 25 % ;
top : 35 % ;
left : 50 % ;
transform : translate ( -50 % , 0 );
background-image : radial-gradient (
circle at 50 % -50 % ,
transparent 75 % ,
var ( --belt ) 0 83 % ,
transparent 0
),
linear-gradient ( to right , transparent 42 % , white 43 % 57 % , transparent 58 % );
}
.arm {
width : 65 % ;
height : 40 % ;
background : #a00 ;
border-radius : 100 % / 170 % 170 % 25 % 25 % ;
top : 37 % ;
left : 50 % ;
transform : translate ( -50 % , 0 );
box-shadow : inset 0 0 0 10 vmin #0002 ;
background-image : linear-gradient ( transparent 20 % , #0003 );
}
.belt {
width : 20 % ;
height : 15 % ;
border : 1 vmin solid var ( --belt-buckle );
border-radius : 1 vmin ;
top : 75 % ;
left : 50 % ;
transform : translate ( -50 % , -50 % );
background : var ( --belt-buckle );
box-shadow : inset 1 vmin 0 0 1.75 vmin var ( --belt );
}
.buttons {
width : 5 % ;
height : 5 % ;
background : var ( --belt );
color : var ( --belt );
top : 33 % ;
left : 50 % ;
transform : translate ( -50 % , 0 );
box-shadow :
0 5 vmin ,
0 10 vmin 0 0.1 vmin ,
0 22 vmin ;
opacity : 0.75 ;
}
.hat {
width : 23 % ;
height : 20 % ;
background : var ( --suit );
border-radius : 100 % 20 % 0 0 ;
top : -2 % ;
left : 50 % ;
transform : translate ( -50 % , 0 ) rotate ( 1 deg );
}
.hat::before {
width : 110 % ;
height : 40 % ;
border-radius : 100 % / 50 % ;
bottom : -17 % ;
left : -5 % ;
box-shadow : inset 0 4 vmin white ;
transform : rotate ( -2 deg );
}
.hat::after {
width : 8 vmin ;
height : 8 vmin ;
border-radius : 50 % ;
background : var ( --beard );
right : -5 vmin ;
top : -15 % ;
}
.hands {
width : 13 % ;
height : 13 % ;
background : var ( --belt );
top : 70 % ;
left : 18 % ;
box-shadow : 41 vmin 0 var ( --belt );
}
.leg {
width : 19 % ;
height : 25 % ;
background : var ( --suit );
transform : skew ( 2 deg );
top : 75 % ;
left : 29 % ;
background-image : linear-gradient ( #0002 , transparent 70 % , var ( --belt ) 0 );
}
.leg + .leg {
left : 52 % ;
}
.leg::after {
width : 110 % ;
height : 20 % ;
background : black ;
bottom : 0 ;
left : -6 % ;
border-radius : 10 vmin 10 vmin 0 0 ;
}
.leg + .leg::after {
left : -4 % ;
}
/* Add these new animations for talking */
@keyframes talking {
0% ,
100% {
transform : translate ( -50 % , 0 );
}
50% {
transform : translate ( -50 % , 2 % );
}
}
.talking .beard {
animation : talking 0.5 s infinite ease-in-out ;
}
.talking .mustache {
animation : talking 0.5 s infinite ease-in-out ;
}
/* Toggle Button Styles */
.toggleButton {
padding : 0.75 rem 1.5 rem ;
font-size : 1.2 rem ;
background-color : #ff4757 ;
color : white ;
border : none ;
border-radius : 50 px ;
cursor : pointer ;
transition :
background-color 0.3 s ease ,
box-shadow 0.3 s ease ,
transform 0.2 s ease ;
display : flex ;
align-items : center ;
justify-content : center ;
min-width : 200 px ;
height : 50 px ;
box-shadow : 0 4 px 6 px rgba ( 0 , 0 , 0 , 0.1 );
}
.toggleButton:hover {
background-color : #e84118 ;
box-shadow : 0 6 px 8 px rgba ( 0 , 0 , 0 , 0.15 );
transform : translateY ( -2 px );
}
.toggleButton:active {
background-color : #c23616 ;
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.1 );
transform : translateY ( 0 );
}
.toggleButton:disabled {
background-color : #a4b0be ;
cursor : not-allowed ;
box-shadow : none ;
}
/* Action Bar Styles */
.actionBar {
position : fixed ;
bottom : 0 ;
left : 0 ;
width : 100 % ;
background : rgba ( 255 , 255 , 255 , 0.95 );
padding : 15 px 30 px ;
box-shadow : 0 -2 px 10 px rgba ( 0 , 0 , 0 , 0.1 );
display : flex ;
flex-direction : row ;
align-items : center ;
justify-content : center ;
gap : 20 px ;
}
.speechBubble {
background : #ffffff ;
border-radius : 15 px ;
padding : 20 px ;
max-width : 60 % ;
box-shadow : 0 4 px 6 px rgba ( 0 , 0 , 0 , 0.1 );
font-family : "Comic Sans MS" , cursive, sans-serif ;
font-size : 1 rem ;
color : #333333 ;
position : relative ;
animation : fadeIn 0.5 s ease-in-out ;
}
.speechBubble::after {
content : "" ;
position : absolute ;
top : 50 % ;
left : -15 px ; /* Position the tail towards Santa */
transform : translateY ( -50 % );
width : 0 ;
height : 0 ;
border : 15 px solid transparent ;
border-right-color : #ffffff ;
}
.inputField {
display : block ;
margin : 10 px auto ;
padding : 10 px 15 px ;
width : 90 % ;
max-width : 250 px ;
border : 2 px solid #ff6f61 ;
border-radius : 25 px ;
font-size : 1 rem ;
transition : border-color 0.3 s ease ;
}
.inputField:focus {
border-color : #ff3b2e ;
outline : none ;
box-shadow : 0 0 5 px rgba ( 255 , 59 , 46 , 0.5 );
}
/* Fade-in Animation for Speech Bubble */
@keyframes fadeIn {
from {
opacity : 0 ;
transform : translateY ( 20 px );
}
to {
opacity : 1 ;
transform : translateY ( 0 );
}
}
/* Responsive Adjustments */
@media ( max-width : 768 px ) {
.actionBar {
flex-direction : column ;
padding : 10 px 20 px ;
gap : 15 px ;
}
.speechBubble {
max-width : 80 % ;
}
.toggleButton {
min-width : 150 px ;
height : 45 px ;
font-size : 1 rem ;
}
}
@media ( max-width : 480 px ) {
.speechBubble {
max-width : 100 % ;
}
.toggleButton {
width : 100 % ;
}
}
We now need to create a webhook URL that will be used to post the text translation of the voice message to. When we click the toggle button, we will post the text translation of the voice message to the webhook URL.
Go to your make.com account and create a new webhook URL in a new scenario called AI Santa. Copy the webhook URL and paste it into the WEBHOOK_URL
variable in the SantaWrapper.tsx
file.
Now we are ready to test the scenario. Click the toggle button and see if Santa can interpret your voice message.
You have now successfully created a voice-activated Santa that can interpret your voice message and write it to a google spreadsheet.
The button in the React component should listen to your microphone
The VoiceRecognitionAPI will record your message and convert it to text
The text is then posted to the webhook URL
You can see your text in the output bundle in the Make.com webhook scenario
See it in action here: LLM Santa
We need to create a prompt that can interpret the voice message and convert it to a structured query. We can do this via API using the command below from the terminal. We could also use the Systemprompt.io Wizard prompt wizard.
curl -X POST "https://api.systemprompt.io/v1/prompt" \
-H "api-key: <apikey>" \
-H "Content-Type: application/json" \
-d '{
"instruction": {
"static": "1. Assume the role of Santa who understands the needs and desires of individuals from the information provided.\n2. Receive detailed information about a user, which can include age, interests, hobbies, recent activities, preferences, and any known wishlist items.\n3. Use this information to craft a Google search query that would help find a Christmas present suitable for the user.\n4. The search query should be concise but include key terms likely to yield relevant and desirable gift options.\n5. Consider various types of presents such as toys, gadgets, experiences, fashion items, or books, based on the user' \' 's information.\n6. Maintain a warm and jolly tone in crafting the response, ensuring it embodies the spirit of Santa Claus.\n7. Provide a justification for the query choices if necessary, explaining how they align with the user' \' 's interests or personality traits.\n8. Be adaptable to unusual or sparse user information, utilizing general festive knowledge or common gift-giving trends when specific details are lacking.",
"state": "{{conversation.history}}",
"dynamic": "{{message}}"
},
"input": {
"name": "SantaGiftQuerySchema",
"description": "This schema outlines instructions for generating a search query as Santa Claus to find an ideal Christmas gift. It includes guidelines for understanding user information, utilizing it to craft meaningful queries, and maintaining a festive tone. The goal is to leverage user data like age, interests, and preferences to form effective search terms that identify suitable gift ideas online, embodied by the spirit of Santa.",
"type": ["message"],
"reference": []
},
"output": {
"name": "SantaGiftQuerySchema",
"type": "structured_data",
"schema": {
"type": "object",
"required": [
"queryString",
"language",
"region"
],
"properties": {
"region": {
"type": "string",
"description": "The regional setting for the search, specified as a country code."
},
"language": {
"type": "string",
"description": "The language in which the search results are desired."
},
"queryString": {
"type": "string",
"description": "The main text of the search query."
}
},
"additionalProperties": false
}
},
"metadata": {
"title": "Santa",
"description": "You are santa, given information about a user, create the perfect search query for finding a christmas present in google."
}
}'
After creating the prompt, we can edit it in the systemprompt console . Take note of the title, as we will need this to call the prompt via the API.
** Important: **
Key things to note here, the output type is structured_data
and the schema is defined in the output
section, this is important for mapping in the Make.com scenario. We've provided a sample schema, but you can edit this to be more specific.
The instructions will be posted to the LLM. The state
variable is optional, but can be used to include the conversation history. The dynamic
variable is required, and will be replaced by the text translation of the voice message.
We now need to add the systemprompt API call to the Make.com scenario.
Select execute prompt from the Systemprompt API integration. Select the prompt title we created earlier.
We need to select the output of the webhook "transcript" as the input for the prompt.
We can add the SERP API call to the Make.com scenario. Select the search query from the previous step as the input.
We can now parse the JSON response from the SERP API call. To do this, select the output bundle from the SERP API call, and copy and paste this into the data structure of the JSON parser. Make sure to map the output correctly.
Make sure you are testing the scenario as you progress, you should now be able to talk to Santa and get a list a products from the SERP API. The next step it to analyse and parse the results into a list of gift ideas.
We need to create a prompt that can interpret the structured query and convert it to a list of gift ideas. We can do this via API using the command below from the terminal. We could also use the Systemprompt.io Wizard prompt wizard.
curl -X POST "https://api.systemprompt.io/v1/prompt" \
-H "api-key: <apikey>" \
-H "Content-Type: application/json" \
-d '{
"instruction": {
"static": "As the SantaProductSelector, your task is to recommend five suitable Christmas gifts from a given list of products, tailored to a specific gift recipient. Use the following guidelines: \n\n1. Maintain a warm and festive tone\n2. Analyze the product list thoroughly\n3. Understand the gift recipient description\n4. Match products to recipient traits\n5. Balance practical and fun items\n6. Optionally rank or explain choices\n7. Focus on spreading festive joy",
"state": "{{conversation.history}}",
"dynamic": "{{message}}"
},
"input": {
"name": "SantaProductSelector",
"description": "The SantaProductSelector schema is designed for identifying potential Christmas gifts. It processes a list of products and recipient details to provide tailored gift recommendations. It emphasizes understanding the recipient's preferences and selecting a diverse set of gifts, balancing practicality and entertainment. The output includes a ranked list of five gifts with possible explanations to highlight reasoning. Input should be clearly categorized into 'Product List' and 'Recipient Description' for effective processing.",
"type": ["message"],
"reference": []
},
"output": {
"name": "SantaProductSelector",
"type": "structured_data",
"schema": {
"type": "object",
"properties": {
"products": {
"type": "array",
"items": {
"type": "object",
"required": [
"productLink",
"title",
"description",
"price",
"choiceMessage"
],
"properties": {
"price": {
"type": "number",
"description": "The price of the product."
},
"title": {
"type": "string",
"description": "The title of the product."
},
"description": {
"type": "string",
"description": "A brief description of the product."
},
"productLink": {
"type": "string",
"format": "uri",
"description": "A URL link to the product page."
},
"choiceMessage": {
"type": "string",
"description": "A message explaining why this product was chosen."
}
}
},
"maxItems": 5,
"minItems": 5
}
},
"required": ["products"]
}
},
"metadata": {
"title": "Santa product picker",
"description": "Given a list of products and a description of the person to be gifted to, pick 5 potential gifts for christmas"
}
}'
After creating the prompt, we can edit it in the systemprompt console . Take note of the title, as we will need this to call the prompt via the API.
** Important: **
Key things to note here, the output type is structured_data
and the schema is defined in the output
section, this is important for mapping in the Make.com scenario. We've provided a sample schema, but you can edit this to be more specific.
We now need to add the systemprompt API call to the Make.com scenario.
Select execute prompt from the Systemprompt API integration. Select the prompt title we created earlier.
We need to select the output of the JSON parser as the input for the prompt.
We are nearly finished now. We can now write the gift ideas to a google spreadsheet.
Select the output of the structured data prompt as the input for the iterator.
Add a google spreadsheet integration and select the iterator as the input.
Make sure you make the values of the structured data from systemprompt to the columns of the google spreadsheet.
Any problems following the demo? Reach out to us on Discord and we'll help you out.