Skip to content

Commit

Permalink
feat: paste assistant data handling and feature iteration (#3720)
Browse files Browse the repository at this point in the history
* feat: paste assistant data handling and feature iteration

* feat: improved data fetching and thread switching

* feat: empty state

* chore: cannot use server side props

* chore: comments and tidy up

* chore: lints

* chore: missing argument
  • Loading branch information
SiTaggart authored Jan 30, 2024
1 parent 22a917a commit f2b7756
Show file tree
Hide file tree
Showing 36 changed files with 1,658 additions and 349 deletions.
16 changes: 14 additions & 2 deletions cypress/integration/api/og-image.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
context("GET /api/og-image", () => {
context("GET /api/component-og-image", () => {
it("gets an image", () => {
cy.request("GET", "/api/og-image?componentRequested=primitives/box").then((response) => {
cy.request("GET", "/api/component-og-image?componentRequested=primitives/box").then((response) => {
expect(response.status).to.eq(200);
expect(response.headers["content-type"]).to.eq("image/png");
console.log(response);
});
});
});

context("GET /api/simple-og-image", () => {
it("gets an image", () => {
cy.request("GET", "/api/simple-og-image?title=Hello%20World&description=This%20is%20a%20description").then(
(response) => {
expect(response.status).to.eq(200);
expect(response.headers["content-type"]).to.eq("image/png");
console.log(response);
},
);
});
});
2 changes: 1 addition & 1 deletion cypress/integration/api/paste-assistant-message.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ context("POST /api/paste-assistant-message", () => {

after(() => {
// delete the thread
cy.request("DELETE", "/api/paste-assistant-thread", { id: threadId });
cy.request("DELETE", `/api/paste-assistant-thread/${threadId}`);
});

it("creates an message on an ai thread", () => {
Expand Down
6 changes: 3 additions & 3 deletions cypress/integration/api/paste-assistant-thread.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
context("POST /api/paste-assistant-thread", () => {
context("/api/paste-assistant-thread", () => {
let threadId: string;
it("creates an ai thread", () => {
cy.request("POST", "/api/paste-assistant-thread", {}).then((response) => {
Expand All @@ -7,7 +7,7 @@ context("POST /api/paste-assistant-thread", () => {
});
});
it("updates an ai thread", () => {
cy.request("PUT", "/api/paste-assistant-thread", { id: threadId, metadata: { testKey: "testData" } }).then(
cy.request("PUT", `/api/paste-assistant-thread/${threadId}`, { metadata: { testKey: "testData" } }).then(
(response) => {
expect(response.status).to.eq(200);
expect(response.body.metadata.testKey).to.eq("testData");
Expand All @@ -21,7 +21,7 @@ context("POST /api/paste-assistant-thread", () => {
});
});
it("deletes an ai thread", () => {
cy.request("DELETE", "/api/paste-assistant-thread", { id: threadId }).then((response) => {
cy.request("DELETE", `/api/paste-assistant-thread/${threadId}`).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.deleted).to.eq(true);
});
Expand Down
1 change: 1 addition & 0 deletions cypress/integration/sitemap-vrt/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const SITEMAP = [
"/customization/",
"/",
"/assistant",
"/blog/",
"/blog/2020-11-26-growing-pains-and-how-we-scaled-our-design-system-support/",
"/blog/2021-04-29-insights-and-metrics-that-inform-the-paste-design-system/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ When sharing a link to a component page on the internet, we supply a dynamically

## The way it works

We use [`@vercel/og`](https://vercel.com/docs/functions/edge-functions/og-image-generation) to generate the image via an edge function, `/api/og-image`.
We use [`@vercel/og`](https://vercel.com/docs/functions/edge-functions/og-image-generation) to generate the image via an edge function, `/api/component-og-image`.

## Local development

Expand All @@ -18,7 +18,7 @@ To start, run:
yarn start:website
```

Once the website have started running, you can hit the function at `/api/og-image`.
Once the website have started running, you can hit the function at `/api/component-og-image`.

## Technology / Stack

Expand Down
7 changes: 6 additions & 1 deletion packages/paste-website/.eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{
"extends": ["../../.eslintrc.js", "plugin:@next/next/recommended", "plugin:@next/next/core-web-vitals"],
"extends": [
"../../.eslintrc.js",
"plugin:@next/next/recommended",
"plugin:@next/next/core-web-vitals",
"plugin:@tanstack/eslint-plugin-query/recommended"
],
"root": true,
"rules": {
"import/no-default-export": "off",
Expand Down
8 changes: 6 additions & 2 deletions packages/paste-website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"@octokit/plugin-paginate-graphql": "^4.0.0",
"@slack/web-api": "^7.0.1",
"@supabase/supabase-js": "^2.36.0",
"@tanstack/react-query": "^5.17.9",
"@tanstack/react-query-devtools": "^5.17.10",
"@twilio-paste/account-switcher": "^3.0.1",
"@twilio-paste/alert": "^14.1.0",
"@twilio-paste/alert-dialog": "^9.2.0",
Expand Down Expand Up @@ -155,7 +157,7 @@
"highcharts-react-official": "^3.1.0",
"lodash": "4.17.21",
"lottie-web": "^5.7.4",
"markdown-to-jsx": "^7.3.2",
"markdown-to-jsx": "^7.4.0",
"mdast-util-from-markdown": "^1.3.0",
"mdast-util-mdx": "^2.0.0",
"mdast-util-to-markdown": "^1.5.0",
Expand Down Expand Up @@ -185,11 +187,13 @@
"use-resize-observer": "^9.1.0",
"uuid": "^9.0.1",
"winston": "^3.11.0",
"zod": "^3.22.4"
"zod": "^3.22.4",
"zustand": "^4.4.7"
},
"devDependencies": {
"@next/eslint-plugin-next": "^14.0.0",
"@storybook/react": "7.6.4",
"@tanstack/eslint-plugin-query": "^5.17.7",
"@testing-library/react": "^13.4.0",
"tsx": "^4.0.0"
},
Expand Down
68 changes: 68 additions & 0 deletions packages/paste-website/src/api/assistantAPIs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { type UseMutationResult, useMutation } from "@tanstack/react-query";

export const useCreateThreadMutation = (): UseMutationResult => {
return useMutation({
mutationFn: async () => {
return fetch("/api/paste-assistant-thread", {
method: "POST",
});
},
mutationKey: ["create-thread"],
});
};

export const useDeleteThreadMutation = (): UseMutationResult => {
return useMutation({
mutationFn: async (id) => {
return fetch(`/api/paste-assistant-thread/${id}`, {
method: "DELETE",
});
},
mutationKey: ["delete-thread"],
});
};

export const useUpdateThreadMutation = (): UseMutationResult => {
return useMutation({
mutationFn: async ({ id, threadTitle }: any) => {
return fetch(`/api/paste-assistant-thread/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ metadata: { threadTitle } }),
});
},
mutationKey: ["update-thread"],
});
};

export const useCreateAssistantRunMutation = (): UseMutationResult => {
return useMutation({
mutationFn: async (messageDetails) => {
return fetch("/api/paste-assistant-message", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(messageDetails),
});
},
mutationKey: ["create-assistant-run"],
});
};

export const useSimpleCompletionMutation = (): UseMutationResult => {
return useMutation({
mutationFn: async (completionDetails) => {
return fetch("/api/paste-assistant-simple-completion/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(completionDetails),
});
},
mutationKey: ["simple-completion"],
});
};
143 changes: 143 additions & 0 deletions packages/paste-website/src/components/assistant/Assistant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/* eslint-disable camelcase */
import type { ThreadMessage } from "openai/resources/beta/threads/messages/messages";
import * as React from "react";

import {
useCreateAssistantRunMutation,
useCreateThreadMutation,
useSimpleCompletionMutation,
useUpdateThreadMutation,
} from "../../api/assistantAPIs";
import { useAssistantMessagesStore } from "../../stores/assistantMessagesStore";
import { useAssistantRunStore } from "../../stores/assistantRunStore";
import { useAssistantThreadsStore } from "../../stores/assistantThreadsStore";
import useStoreWithLocalStorage from "../../stores/useStore";
import { AssistantCanvas } from "./AssistantCanvas";
import { AssistantComposer } from "./AssistantComposer";
import { AssistantEmptyState } from "./AssistantEmptyState";
import { AsssistantLayout } from "./AssistantLayout";
import { AssistantThreads } from "./AssistantThreads";
import { AssistantHeader } from "./AsststantHeader";

const getMockMessage = ({ message }: { message: string }): ThreadMessage => {
const date = new Date();

return {
id: "",
object: "thread.message",
created_at: Math.floor(date.getTime() / 1000),
thread_id: "xxxx",
role: "user",
content: [
{
type: "text",
text: {
value: message,
annotations: [],
},
},
],
file_ids: [],
assistant_id: null,
run_id: null,
metadata: {},
};
};

export const Assistant: React.FC = () => {
const threadsStore = useStoreWithLocalStorage(useAssistantThreadsStore, (state) => state);
const createAssistantRun = useCreateAssistantRunMutation();
const createThreadMutation = useCreateThreadMutation();
const updateThreadMutation = useUpdateThreadMutation();
const simpleCompletionMutation = useSimpleCompletionMutation();
const setActiveRun = useAssistantRunStore((state) => state.setActiveRun);
const addMessage = useAssistantMessagesStore((state) => state.addMessage);
const messages = useAssistantMessagesStore((state) => state.messages);

if (threadsStore == null) return null;

const handleMessageCreation = (message: string, threadId: string): void => {
// add the new user message to the store to optimistically render it whilst we wait for openAI to do its thing
addMessage(getMockMessage({ message }));

// Create a new "assistant run" on the thread so that openAI processes the new message and updates the thread with a response
createAssistantRun.mutate(
{ threadId, message },
{
onSuccess: async (run) => {
// @ts-expect-error I don't know how to type this right now so it knows it's a response
const newRun = await run.json();
setActiveRun(newRun.run);
},
},
);

if (messages.length === 0) {
/**
* summarise the first question as the title of the
* thread, if this is the first message in that thread
* for it to appear in the thread list as an identifier
* of the thread
*/
simpleCompletionMutation.mutate(
{
prompt:
"If this is the start of a threaded conversation, summarise the subject of the converation in as few words as possible: ",
context: message,
},
{
onSuccess: async (completion) => {
// @ts-expect-error I don't know how to type this right now so it knows it's a response
const newCompletion = await completion.json();
// update the thread title in the store
threadsStore?.setThreadTitle(threadId, newCompletion.choices[0].message.content);
// update the thread title in openAI
updateThreadMutation.mutate({ id: threadId, threadTitle: newCompletion.choices[0].message.content });
},
},
);
}
};

/**
* From one of the canned new thread messages, create a new thread, select it, and then
* create a new message run on the newly created thread.
*
* @param {string} message
*/
const handleCannedThreadCreation = (message: string): void => {
createThreadMutation.mutate(
{},
{
onSuccess: async (data) => {
// @ts-expect-error I don't know how to type this right now so it knows it's a response
const newThread = await data.json();
if (threadsStore == null) return;
threadsStore.createAndSelectThread(newThread);
handleMessageCreation(message, newThread.id);
},
},
);
};

return (
<AsssistantLayout.Window>
<AsssistantLayout.Threads>
<AsssistantLayout.ThreadsHeader>
<AssistantHeader />
</AsssistantLayout.ThreadsHeader>
<AssistantThreads />
</AsssistantLayout.Threads>
<AsssistantLayout.Canvas>
{threadsStore.selectedThreadID == null && (
<AssistantEmptyState onCannedThreadCreation={handleCannedThreadCreation} />
)}
{threadsStore.selectedThreadID != null && <AssistantCanvas selectedThreadID={threadsStore.selectedThreadID} />}
<AsssistantLayout.Composer>
<AssistantComposer onMessageCreation={handleMessageCreation} />
</AsssistantLayout.Composer>
</AsssistantLayout.Canvas>
</AsssistantLayout.Window>
);
};
/* eslint-enable camelcase */
Loading

0 comments on commit f2b7756

Please sign in to comment.