Don't want to clone the repo? Here's a guide for setting up from scratch.
The readme below assumes you're running a bash terminal on MacOS or Linux, but the steps shouldn't differ too much if you're using shell commands on Windows or another operating system.
You can get straight up to speed by spinning up a new Svelte app using the create-svelte
CLI tool from your terminal.
Below are example terminal commands to get started:
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
Once you've created your new Svelte project, make sure to install the dependencies with npm install
(or pnpm install
or yarn
).
There's also one more dependency we need to install in our Sveltekit project: the Pieces OS client SDK.
npm install && npm install @pieces.app/pieces-os-client
Your Svelte app uses Vite in the compile phase. Anytime you make changes to your Svelte project files, save your changes and Vite will immediately update your development server to reflect those changes in your browser.
That means you can run your development server straight away and just get straight to coding. (Note: Svelte's default configuration will log A LOT of "unused css" callbacks at compile time, as your project gets bigger with more styling, so it can be handy to keep the --open
parameter in mind)
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
The general pattern of developing in Svelte starts in the src/routes
directory of your project. Your parent-level Svelte components that you can immediately see in your development server are Header.svelte
and page.svelte
in your routes
directory.
We'll be making the bulk of our changes to the page.svelte
, building the Pieces Copilot chat in this Svelte component. We may (optionally) need to bring in some UI components for that purpose:
You don't have to use ShadCN UI components if you're very familiar with Svelte but, in all other cases, using ShadCN (and Tailwind CSS) could save you time building the front end. If you choose to do so, follow these steps in order:
- First, add Tailwind CSS to your project
- Run another
npm install
command to install Tailwind's dependencies.
npx svelte-add@latest tailwindcss && npm install
- Modify your
svelte.config.js
configuration file in your project's root directory, adding the "alias" script to make your imports go smoother
const config = {
// ... other config
kit: { // ... other config
alias: { "@/*": "./path/to/lib/*",
},
},
};
- Install "ShadCN for Svelte" with the CLI command
npx shadcn-svelte@latest init
- The shadcn-svelte installer asks config questions during installation. We recommend these settings below:
Would you like to use TypeScript (recommended)? › Yes
Where is your global CSS file? › src/app.pcss
Where is your tailwind.config.[cjs|js|ts] located? › tailwind.config.js
Configure the import alias for components: › $lib/components
Configure the import alias for utils: › $lib/utils
You're now free to install and import any ShadCN UI components you'd like from the library. Using the same shadcn-svelte CLI tool to install them individually, you'll see each UI component appear in your project's src/lib/components/ui
directory.
Follow this step if you've chosen to install ShadCN. We'll install the Textarea and Button UI components, so we can use them later in our Svelte page
component for our copilot chat interface.
npx shadcn-svelte@latest add button
(thanks to Jordan for providing the original Typescript version of CopilotStreamController!)
We also need a singleton class to talk to the Pieces OS client and parse the messages for handling into our copilot chat.
Create a new Typescript file for your singleton class in the routes
directory named CopilotStreamController.ts
(or name your class how you choose, but make sure the file extension is a .ts
Typescript file.
You can copy/paste a working version of the CopilotStreamController adapted for Svelte here and save your CopilotStreamController file.
Then import your CopilotStreamController class as a module in your page.svelte
component. Your new CopilotStreamController is saved in the same routes
directory as your page.svelte
component, so the import statement below should work.
+page.svelte
import CopilotStreamController from './CopilotStreamController'
We'll make sure we're using the shared instance of CopilotStreamController thanks the getInstance
method that we already defined in the CopilotStreamController class. We can use that method to mount CopilotStreamController into the page.svelte
component, assigning it as a value to a controller
instance that we define within the <script></script>
tags of page.svelte
.
We'll also use Sveltekit's onMount
call to initiate our shared CopilotStreamController instance within the mounted lifecycle of our app:
<script>
import { onMount } from 'svelte';
let controller;
onMount(() => {
controller = CopilotStreamController.getInstance();
});
</script>
We want to break our stream into different UI blocks, so that we can read back on our chat history as a conversation between us as the user and Pieces OS client as the copilot.
(Note: the ideas behind the chat_history UI elements and typing indicator component are entirely credit to semicognitive's sveltekit-chat repo).
To do that, we declare:
- A
chat_history
array that also holds reactive binding roles "user" and "assistant" (this is the beauty of using Svelte; we can mix patterns and assign responsibilities from those design patterns all within one object) - A
sendChat
method for ourpage.svelte
component that updates thechat_history
array conditionally. If it's the last message parsed CopilotStreamController received from Pieces OS client,sendChat
will append that to the "assistant" role inchat_history
, otherwisesendChat
will move back in the index of CopilotStreamController's content and append the string content at that index into the "user" role ofchat_history
- We'll also use
isSending
boolean to bring in a "typing" UI effect on the front end, for whenever our chat stream is awaiting the copilot's response to our sent messages. - We'll use a
scrollToBottom
function to update our page view and chat component, whenever the our chat overflows past the original height of the conversation box.scrollToBottom
uses Sveltekit's built-inrequestAnimationFrame
method to keep track of the last rendered frame in our copilot conversation.
let chat_history: { role: "user" | "assistant"; content: string }[] = [];
let isSending = false;
let userInput = '';
async function sendChat() {
isSending = true;
await controller.sendMessage(userInput, (newMessage) => {
if (chat_history.length > 0 && chat_history[chat_history.length - 1].role === 'assistant') {
chat_history = [...chat_history.slice(0, -1), { role: 'assistant', content: newMessage }];
} else {
chat_history = [...chat_history, { role: 'assistant', content: newMessage }];
}
isSending = false;
scrollToBottom();
});
chat_history = [...chat_history, { role: 'user', content: userInput }];
userInput = '';
scrollToBottom();
}
function scrollToBottom() {
requestAnimationFrame(() => {
const chatSection = document.querySelector('.chat-section');
if (chatSection) {
chatSection.scrollTop = chatSection.scrollHeight;
}
});
}
We haven't declared or used our Textarea component yet, but we can save the user a ton of time by clearing the user input from Textarea whenever the Send button is clicked or the Enter key is pressed and a message is sent to the copilot.
We handle these two events with our handleSubmit
and handleKeyDown
methods for page.svelte
:
async function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
await sendChat();
}
}
async function handleSubmit(this: HTMLFormElement) {
if ($response.loading) return;
const formData: FormData = new FormData(this);
const message = formData.get("message") as string;
if (message == "") return;
userInput = message;
sendChat();
this.reset();
}
And that's it for the <script></script>
section of our page.svelte
component, when it comes to the bulk of it's event-driven logic and receiving data from CopilotStreamController. The rest of the work lies in building the front-end UI in the <body></body>
and <style></style>
sections of page.svelte
.
You can find a full working example conversation UI for page.svelte
at the Pieces x Svelte repo here. Copy and paste the body and style sections from that repo into your project's page component, if you're building from scratch and don't already have that version of page.svelte
in your project.
To create a production version of your app:
You can preview the production build with npm run preview
npm run preview
When you're ready to deploy your app to the web, use the run build
command
npm run build
Svelte and Vite may need you to install an adapter for your production server's environment. See the official Svelte documentation here for a guide on installing adapters to your project.