Key Concepts β
Smile introduces a new way to build behavioral experiments which promotes modularity and reusability. Smile leverages the capabilities of existing open source libraries including Vite and Vue, while providing an entirely new interface for specifying and debugging interactive experiments.
The first concept to introduce is the notion of a View. A View is a self-contained bit of code that represents a single "phase" or part of an experiment. For example, the part of your experiment that collects informed consent might be one View. Another View might be the debriefing form. Below we will describe how you define the sequence of Views in your experiment.

Each View is minimally a Vue component (a special type of web development file) which can be written in whatever way you please. However, Smile provides a custom API for building Views. The key idea behind this API is the concept of 'steps'. A step is a sequenced event that occurs within a view. For example, a View might have a step that presents a question to the participant, a step that collects a response, and a step that displays the results.

A useful way to think about it is the Views are like slides in a presentation software like Powerpoint/Keynote and steps are like "builds" or "animations steps" that occur within a slide.
A final key concept of Smile is the idea of persistence. Persistence refers to the fact that information about the application is preserved across page reloads. When you record data from your experiment in a database that is one kind of persistence. Another is when information about the current application survives across page reloads. It is fairly common for participants to reload their browser window, or to close a window and reopen it at another time. In most experiment frameworks, the task begins anew when a participant does this which can be problematic for certain research questions. Instead, Smile makes it easy to persist variables in a way that they will survive page reloads.
Creating a simple Experiment View β
To help make these concepts more concrete, we will walk through the process of creating a simple experiment view. We will start with a simple view that presents the user with sequence of words on the screen and collects a response. We will slowly build this up by adding complexity and features to help illustrate the key concepts in Smile.
Each View is a Vue component. You can create a new view by creating a new file in the src/user/components
folder. For example, if you want to create a new view called MyView.vue
, you would create a new file in the src/user/components
folder called MyView.vue
.
A default Vue component has three parts:
- A script (javscript or typescript)
- A template (HTML + Vue syntax)
- A style (CSS)
The script is the JavaScript that is used to control the behavior of the component. The template is the HTML that is displayed to the user. The style is the CSS that is used to style the component.
<script setup></script>
<template>
<div>
<h1>My Experiment</h1>
</div>
</template>
<style scoped></style>
This example View component does nothing. It just displays the text "My Experiment" in a large (h1) font.
To begin using Smile, we need to import the Smile API and use it to define the steps in the experiment.
<script setup>
// import and initialize smile API
import useViewAPI from '@/core/composables/useViewAPI'
const api = useViewAPI()
api.steps.append([
{ word: 'THIS' }, // step 1
{ word: 'IS' }, // step 2
{ word: 'A' }, // step 3
{ word: 'TEST' }, // step 4
])
</script>
<template>
<div>
<h1>My Experiment</h1>
</div>
</template>
<style scoped></style>
This defines four steps in the experiment. Each step has a word
property that is displayed to the user. This is incomplete though because it doesn't actually show the word to the user. We'd like to step through these steps each time the user presses the spacebar.
<script setup>
// import and initialize smile API
import useViewAPI from '@/core/composables/useViewAPI'
const api = useViewAPI()
api.steps.append([
{ word: 'THIS' },
{ word: 'IS' },
{ word: 'A' },
{ word: 'TEST' },
])
api.onKeyDown(' ', () => {
api.goNextStep()
})
</script>
<template>
<div>
<h1>My Experiment</h1>
</div>
</template>
<style scoped></style>
This uses the api.onKeyDown
method to listen for the spacebar key press and advance to the next step. You can also go to the previous step with api.goPrevStep()
or jump to a specific step with api.goToStep(pathname)
. We will talk about paths in a later section of the documentation.
Now we actually need to display the word to the user. We can do this by updating the template part of the component.
<script setup>
// import and initialize smile API
import useViewAPI from '@/core/composables/useViewAPI'
const api = useViewAPI()
api.steps.append([
{ word: 'THIS' },
{ word: 'IS' },
{ word: 'A' },
{ word: 'TEST' },
])
api.onKeyDown(' ', () => {
api.goNextStep()
})
</script>
<template>
<div>
<h1>{{ api.stepData.word }}</h1>
</div>
</template>
<style scoped></style>
This uses the api.stepData
object to access the data for the current step. The .word
property is defined because we added it to the step data in the api.steps.append
method.
When we advance to the next step using the api.goNextStep()
method, it automatically changes the api.stepData
to refer to the next step and reactively updates the template to display the new word.
This is already a working View! On first load it will show the word "THIS" since that is the first step. Then if the user presses the spacebar, it will advance to the next step and show the word "IS" and so on. When it get to the last step, subsequent presses of the spacebar will do nothing since there are no more steps to advance to.
This covers only part of the Smile API and development approach but we hope is illustrates how Smile makes it easy to build experiments.
Adding a timer to the experiment β
The examples so far could be easily done in raw Vue syntax using reactive properties e.g., ref()
. Let's go a little bit deeper to explore some unique functionality provided by Smile's API.
We usually want to record some information about the user. In this example, it might make sense to record the amount of time it took them to press each spacebar. We can use Smile's API to add a timer to the View to measure the reaction time of the user.
<script setup>
// import and initialize smile API
import useViewAPI from '@/core/composables/useViewAPI'
const api = useViewAPI()
api.steps.append([
{ word: 'THIS' },
{ word: 'IS' },
{ word: 'A' },
{ word: 'TEST' },
])
// start the timer if it is not already started
if (!api.isTimerStarted()) {
api.startTimer()
}
api.onKeyDown(' ', () => {
const reactionTime = api.elapsedTime()
api.stepData.reactionTime = reactionTime
api.recordStep()
api.goNextStep()
})
</script>
<template>
<div>
<h1>{{ api.stepData.word }}</h1>
</div>
</template>
<style scoped></style>
First we start the timer if it is not already started. You might wonder why we need to check if it is already started since the <script setup>
section only runs once. The reason is that Smile persists information across page reloads. This way if your participant reloads the page in their browser, Smile will detect the timer was already started and continue measuring time with respect to the first time it was started. Of course, if you don't want that more fancy behavior you can just call api.startTimer()
without checking if it was already started, which will restart it to measure "from the last page load."
This example shows another aspect of Smile's API. We use api.elapsedTime()
to measure the time it took the user to press the spacebar. Then we write the resulting data to a new property in the api.stepData
object called api.stepData.reactionTime
. We are free to make new properties on the current api.stepData
or to modify existing ones.
Then we called api.recordStep()
to record the step data. This persists the data so that it will be written to the database record for this participant. It's worth mentioning here that this doesn't mean the data will be immediately stored in our database -- for example Firebase (the recommended database for Smile) limits the frequency by which we can write to documents but rest assured api.recordStep()
will buffer your participants trial data so that on the next opportunity it is safely written to the database. In addition even if the subject reloads the browser at this point, the data for that trial will be restored for later syncing, limiting data loss.
What this section reveals it that Smile's API goes beyond basic Vue components to provide ways to define steps or trials in an experiment, save data to a database, persist data across page loads, and provides convenient ways to record data typically needed in behavioral experiments.
It gets much more fancy and powerful from there. For example here is a more complex example of a Stroop experiment which uses hierarchically nested steps, randomization, and more. We provide complete documentation of steps and the API.
Transitioning to the next View β
The last step is that we need to leave this View and go to the next one. To do this we should change it so that when there are no more steps we exit to the next View.
<script setup>
// import and initialize smile API
import useViewAPI from '@/core/composables/useViewAPI'
const api = useViewAPI()
api.steps.append([
{ word: 'THIS' },
{ word: 'IS' },
{ word: 'A' },
{ word: 'TEST' },
])
// start the timer if it is not already started
if (!api.isTimerStarted()) {
api.startTimer()
}
api.onKeyDown(' ', () => {
const reactionTime = api.elapsedTime()
api.stepData.reactionTime = reactionTime
api.recordStep()
if (api.isLastStep()) {
api.goNextView()
} else {
api.goNextStep()
}
})
</script>
<template>
<div>
<h1>{{ api.stepData.word }}</h1>
</div>
</template>
<style scoped></style>
Here the api.isLastStep()
method is used to check if the current step is the last step. There are several equivalent ways to do that as well for example !api.hasNextStep()
or api.stepIndex >= api.nSteps
, but part of Smile's API design principal is to give you very clear, commonly used function names which can help avoid typos or errors in logic. If it is the last step, we exit to the next View with api.goNextView()
. If it is not, we advance to the next step with api.goNextStep()
.
The use of api.goNextView()
means that even if we change the order of our Views in the overall flow of our experiment we don't need to update our code. This means it's easy to share your Views with others and to reuse them in different experiments.
Hopefully this gives you a sense of how Smile's API can be used to build experiments. Smile's API provides many more complex features which is introduced in the rest of the documentation. But before we get into these advanced features, we will now walk through the process of placing this View in the Timeline.
Placing your new Experiment View in the Timeline β
All the files that you regularly need to edit are in the src/user
folder. The design.js
file is the main entry point for your experiment. It is where you define the overall flow of your experiment.
We recommend you take a look at this file. It is fairly long and starts with some boilerplate configuration and importing. But the key section is where the timeline is defined. For example, look for a code section like this:
// demographic survey
timeline.pushSeqView({
name: 'demograph',
component: DemographicSurvey,
})
// windowsizer
timeline.pushSeqView({
name: 'windowsizer',
component: WindowSizer,
})
// instructions
timeline.pushSeqView({
name: 'instructions',
component: Instructions,
})
This shows the three Views that are currently defined in the experiment. The timeline.pushSeqView
method is used to add a new View to the timeline. The name
property is used to identify the View. The component
property is the Vue component that is used to display the View.
We can add our new View to the timeline by adding a new pushSeqView
call. Lets say we wanted to add it after the windowsizer View. We would add it like this:
// put this up at the top of the design.js file with the other imports
import MyView from '@/user/components/MyView.vue'
// demographic survey
timeline.pushSeqView({
name: 'demograph',
component: DemographicSurvey,
})
// windowsizer
timeline.pushSeqView({
name: 'windowsizer',
component: WindowSizer,
})
// myview inserted here
timeline.pushSeqView({
name: 'myview',
component: MyView,
})
// instructions
timeline.pushSeqView({
name: 'instructions',
component: Instructions,
})
Here we imported our new View (you should do that up at the top of the design.js
file with the other imports). Then we added a new pushSeqView
call to add it to the timeline. The name
property is used to identify the View. The component
property is the Vue component that is used to display the View.
You'll notice that there are many other Views in the default timeline including WindowSizer
, Instructions
, and Consent
. These are all built in Views that are provided by Smile which are commonly used in experiments. You can of course remove any of these, or edit them to your liking. Some are quite sophisticated and can save you a lot of time such as the InstructionsQuiz
View which can be used to quickly build comprehension check quizzes.
With this change there is a new sequence to the experiment
... -> demographic survey -> windowsizer -> myview -> instructions -> ...
If we had pasted the myview
View before the windowsizer
View, the sequence would have been:
... -> demographic survey -> myview -> windowsizer -> instructions -> ...
Smile's timeline API also allow randomized flows for showing different sequences depending on what condition a participant is assigned to.
Now that we have defined a simple View and placed it in the design/timeline, we are ready to test it out.
Developing and debugging your experiment β
A final key concept of Smile is the advanced tools which help you develop and debug your experiment. We think of it as enabling "brain surgery" on your experiment. You can use these tools to bounce around, inspect various elements of your interface, and check the format of your data. You can read more about development in the remainder of the documentation. However, assuming you've done everything to install the required software, setup and configured your repo, you can type
npm run dev
to start the development server. This will show you something like this:
> smile@0.0.0 dev
> vite
β Regenerating local environment file based on git info env/.env.git.local
VITE v6.3.3 ready in 474 ms
β Local: http://localhost:3010/nyuccl/smile/main/
β Network: use --host to expose
β Inspect: http://localhost:3010/nyuccl/smile/main/__inspect/
β press h + enter to show help
β stripping dev/present mode components from src/core/App.vue
β stripping dev/present mode components from src/core/SmileApp.vue
Then you simply open the URL labeled "Local" in your browser.
Critically, when running in development mode, Smile provides a GUI "overlay" which allows you to see and explore the current state and flow of the experiment. The dev mode tools are best explained in this video: