Inside Appwrite's new Command Center
Tags: appwrite, svelte, ai, opensource
Published: 2023-08-30 15:13:15

The Appwrite Console is the main way developers like us manage their Appwrite instances. It has been carefully designed to squeeze out the best developer experience possible, and we hope you've been loving it just as much as we have. 

Engineers like us are hyper-optimisers. It’s a part of our identity. We celebrate our compulsion to overthink every decision we make, like when we argue about tabs vs. spaces, build custom Vim plugins, or add keyboard shortcuts that we forget the next day. If this sounds like you, get ready to unleash the power user inside you.

We know reaching for a mouse is a lot of effort, so we are adding a keyboard-oriented feature, the Command Center. Now you can navigate the Appwrite Console with shortcuts alone. Let’s go over what it can do, and how we built it.

![Appwrite's console with a command center on top](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q4ze76dfj3epqajjmhdv.png)

## What is the Command Center?

The Command Center is a menu that contains a large variety of shortcuts to the most used Console-related tasks. By using its search input, you can quickly filter through commands, and execute one.  

For example, if you're viewing a database, you can open the Command Center and search for the `Create Attribute` command. It'll open a panel in which you can choose the type of attribute you want to create, and open the form to create said attribute. The best part? You can operate the entire Command Center using just the keyboard.

You may notice that most commands will have keyboard combinations next to them. When the command center is closed, you may input these keyboard shortcuts to instantly activate the associated command, without needing to open the command center! Some commands are context-aware though, and will only appear on certain pages.

Some commands may even open up sub-panels, which brings us to another exciting new feature of the Command Center.

## AI assistant sub-panel

As part of this release, we also are shipping an AI assistant! You can ask any Appwrite-related question, and it will answer in detail, fetching information from our documentation, and giving code snippets where applicable. 


![Screenshot of the AI assistant panel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2f1m19lpcavyz409h7t7.png)



This is still an experimental feature, so the answers may not be 100% accurate. Expect to see more enhanced capabilities from the AI assistant in the future!

## How we implemented the Command Center

The Command Center has a few challenges going for it. We need to:
- Contextually show/hide commands based on the current route and dynamic values
- Support sub-panels
- Rank commands according to how useful they will be in any given moment
- Trigger commands based on keyboard shortcuts
	- Disable these shortcuts when using an input, a modal, or a wizard
- Be able to search for entities inside your Appwrite instance and provide related commands

### Contextual commands

To do this, we created a combination of custom [Svelte Stores](https://svelte.dev/docs/svelte-store). We have a helper, `registerCommands`, that adds a list of commands to that store, and should only be called inside Svelte components or routes:

```ts
$: $registerCommands([
	{
		label: 'Go to Appwrite website',
		callback() {
			goto('https://appwrite.io/')
		},
		keys: ['g', 'a', 'w']
	}
])
```


There's something interesting about the code-block above. It starts with a dollar sign accompanied by a colon ( `$:` ). In Svelte, that marks a statement as reactive, meaning that if any variable inside that block changes, it gets re-run. This allows us to pull off some nifty tricks:

```ts
$: $registerCommands([
	{
		label: 'Go to Databases',
		callback() {
			goto('/databases')
		},
		keys: ['d'],
		// Disable if we're already in a Database page
		disabled: $page.url.pathname.startsWith('/databases'),
		rank: 10
	},
	{
		// Dynamic labels
		label: `Set theme to ${$theme === 'dark' ? 'light' : 'dark'}`
		callback() {
			$theme = $theme === 'dark' ? 'light' : 'dark';
		},
		rank: 1
	}
])
```

In this example above, `page` and `theme` are Svelte stores. By prefixing them with a dollar sign, you get access to their values. Whenever these values get updated, `$registerCommands` is run again, and the commands get updated. 

Now, if it gets run again, how come the commands don't get duplicated? That's thanks to how `registerCommands` works internally. Whenever it gets run again, it'll delete the previous commands before adding the new ones.

P.S. You may notice that `registerCommands` is also pre-fixed with a dollar sign, meaning it's also a store! Why though? Go to our [console repository](https://github.com/appwrite/console) and try to figure out why! Let me know in the comments if you find out 🔍

Another important thing to note: Whenever a component gets destroyed, the commands are also deleted. This means that we can define page-exclusive commands, or even group them under a certain route! This is because of SvelteKit's file-based router. In this router, every page is defined by a separate Svelte component, meaning that to represent a page at `localhost/databases`,  you'd have a file  `routes/databases/+page.svelte`. It doesn't stop there though, if you want to define a layout that wraps around all sub-routes inside the `database` folder (e.g. `/databases/123`, `/databases/456/settings`), you can!

```html
<!-- routes/databases/+layout.svelte -->
<div class="container">
	<slot />
</div>

<!-- routes/databases/[database-id]/+page.svelte -->
<div>Hello database!</div>

<!-- Route 'localhost/databases/123' will render: -->
<div class="container">
	<slot />
</div>
```

With this in mind, if we want to define commands that only appear on databases sub-routes, we just need to run `registerCommands` inside `routes/databases/+layout.svelte`!

### Keyboard shortcuts

All the registered commands are stored into their own separate store. We then have an internal key-down handler that we bind to the page's window, to listen for any incoming commands.

```ts
<svelte:window on:keydown={$commandCenterKeyDownHandler} />
``` 

The handler does a lot of things under the hood, such as:
- Preventing keys coming from input or text-area elements from triggering a command
- Preventing keys from within wizards or modals from triggering a command
- Deal with command conflicts, e.g. if one command has a shortcut `g`, and another has a shortcut `g then e`, if you press `g`, it'll wait a while to see if you press `e`. If you do press it, it triggers the second command, otherwise, it'll trigger the first one

### Sub-panels

Commands may also open up sub-panels. To open up a sub-panel, you just need to execute `addSubPanel` inside one of the commands.

```ts
$: $registerCommands([
	{
		label: 'Create attribute',
		callback() {
			addSubPanel({
				name: 'Create attribute',
				component: CreateAttribute
			})
		},
		keys: ['c', 'a'],
	},
])
```

Where `CreateAttribute` is a component that expands upon our `Template` component.

```html
<!-- CreateAttribute.svelte -->
<script lang="ts">
    let search = '';
    let options = attributeOptions.map((option) => {
        return {
            label: option.name,
            icon: option.icon,
            callback() {
                initCreateAttribute(option.name);
            }
        };
    });

    $: filteredOptions = options.filter((option) => {
        return option.label.toLowerCase().includes(search.toLowerCase());
    });
</script>

<Template options={filteredOptions} bind:search>
    <div class="u-flex u-cross-center u-gap-8" slot="option" let:option>
        <i class="icon-{option.icon}" />
        <span>{option.label}</span>
    </div>
</Template>
```

### Searchers

Besides `registerCommands`, we also have `registerSearchers`. Searchers are basically functions that receive an input, and return a list of commands.

```ts
const people = ['Thomas', 'Torsten', 'Arman'];

const peopleSearcher = ((input) => {
	return people
		.filter((p) => p.toLowerCase().includes(input.toLowerCase()))
		.map((p) => ({
			label: `Select ${p}`,
			callback: () => {
				console.log('You selected', p);
			}
		}));
}) satisfies Searcher;

$registerSearchers(peopleSearcher);
```

Now if you input `thomas` into the Command Center, you'll see a new command called `Select Thomas`. Pretty straightforward! 


## AI Assistant

Our initial implementation of the Appwrite Assistant is loosely based on [this tutorial](https://dagster.io/blog/chatgpt-langchain). 

In summary, we offer a wrapper over OpenAI's ChatGPT API. We do this because we have to expand the API's functionality, due to 2 issues:
- ChatGPT's training data is not kept up-to-date, meaning it does not have access to our latest documentation
- We only want the Assistant to answer Appwrite-related questions

To solve these problems, we store all the relevant documentation data for the assistant to access. We then feed it to [LangChain](https://www.langchain.com/), which provides a helper to answer questions over docs. Finally, we chain that helper to the ChatGPT API, and provide some predefined parameters to ensure we restrict the accepted questions to Appwrite-related topics.

You can see the code for the Assistant on our [public GitHub repository](https://github.com/appwrite/assistant).

## Closing Notes

The Command Center is one of the most exciting additions to our Console, and I hope this article helped illustrate its capabilities, and how you may implement a similar feature into your own apps. If you have any feedback or questions, please let us know in the comments, or hop on over our [Discord server](https://appwrite.io/discord) to talk with us!