Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Onboarding #245

Merged
merged 28 commits into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1c65f23
rename onboarding to old
elie222 Oct 13, 2024
ead6124
Add basic resend style onboarding
elie222 Oct 14, 2024
ac06483
Build out more of bulk unsub and ai automation in onboarding flow
elie222 Oct 14, 2024
2f6a034
Merge branch 'main' into onboarding
elie222 Oct 14, 2024
448f883
Improve cold email blocker for onboarding
elie222 Oct 14, 2024
9b6fac7
cold email and onboarding adjustments
elie222 Oct 15, 2024
b88bf60
fetch real data for bulk unsub
elie222 Oct 15, 2024
54364ca
clean up timespans and copy
elie222 Oct 15, 2024
4eac9df
unsub in onboarding
elie222 Oct 16, 2024
ffc7f29
Merge branch 'main' into onboarding
elie222 Oct 18, 2024
928cf7d
Add AI to find example matches for a given rules prompt
elie222 Oct 18, 2024
2ca0d31
ai step for onboarding
elie222 Oct 18, 2024
934a144
show results of test in onboarding
elie222 Oct 20, 2024
90f8698
show ai assistant onboarding results. show video demos
elie222 Oct 20, 2024
741cb95
add onboarding to side nav
elie222 Oct 20, 2024
a006805
show real unsub status in onboarding
elie222 Oct 20, 2024
e906818
mark onboarding completed
elie222 Oct 20, 2024
e1e1387
only send to upgrade if not premium. and other onboarding fixes
elie222 Oct 20, 2024
2b6adfc
adjust pricing to remove free
elie222 Oct 20, 2024
97a8277
book a call link for copilot plan
elie222 Oct 20, 2024
a063bfe
adjust additional seats copilot
elie222 Oct 20, 2024
488ad77
adjust ordering on onboarding
elie222 Oct 20, 2024
5e60deb
clean up onboarding code
elie222 Oct 20, 2024
4df9f51
adjust onboarding copy
elie222 Oct 20, 2024
cdea9da
fix missing react dep
elie222 Oct 20, 2024
378f97b
make coderabbit pr fixes
elie222 Oct 20, 2024
4f9048b
fix up onboarding for long ai input
elie222 Oct 20, 2024
ff24e91
wrap rule actions in middleware
elie222 Oct 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ You are an expert in TypeScript, Node.js, Next.js App Router, React, Prisma, Pos
- Use React Hook Form with Zod for validation. The same validation should be done in the server action too. Example:

```tsx
import { Input } from "@/components/Input";
import { Button } from "@/components/ui/button";
elie222 marked this conversation as resolved.
Show resolved Hide resolved

export const ProcessHistory = () => {
const {
register,
Expand Down
101 changes: 101 additions & 0 deletions apps/web/__tests__/ai-example-matches.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, it, expect, vi } from "vitest";
import { gmail_v1 } from "@googleapis/gmail";
import { aiFindExampleMatches } from "@/utils/ai/example-matches/find-example-matches";
import { queryBatchMessages } from "@/utils/gmail/message";
import { ParsedMessage } from "@/utils/types";
import { findExampleMatchesSchema } from "@/utils/ai/example-matches/find-example-matches";

vi.mock("server-only", () => ({}));
vi.mock("@/utils/gmail/message", () => ({
queryBatchMessages: vi.fn(),
}));
elie222 marked this conversation as resolved.
Show resolved Hide resolved

describe("aiFindExampleMatches", () => {
it("should find example matches based on user prompt", async () => {
const user = {
email: "[email protected]",
aiProvider: null,
aiModel: null,
aiApiKey: null,
};

const gmail = {} as gmail_v1.Gmail;
const accessToken = "fake-access-token";
const rulesPrompt = `
* Label newsletters as "Newsletter" and archive them.
* Label emails that require a reply as "Reply Required".
* If a customer asks to set up a call, send them my calendar link: https://cal.com/example
`.trim();

const mockMessages = [
{
id: "msg1",
threadId: "thread1",
headers: {
from: "[email protected]",
subject: "Weekly Industry Digest",
},
snippet: "Top stories in our industry this week...",
},
{
id: "msg2",
threadId: "thread2",
headers: {
from: "[email protected]",
subject: "Urgent: Need your input",
},
snippet: "Could you please review this proposal and get back to me...",
},
{
id: "msg3",
threadId: "thread3",
headers: {
from: "[email protected]",
subject: "Interested in setting up a call",
},
snippet: "I'd like to discuss your services. Can we schedule a call?",
},
];

vi.mocked(queryBatchMessages).mockResolvedValue({
messages: mockMessages as ParsedMessage[],
nextPageToken: null,
});

const result = await aiFindExampleMatches(
user,
gmail,
accessToken,
rulesPrompt,
);

expect(result).toEqual(
expect.objectContaining({
matches: expect.arrayContaining([
expect.objectContaining({
emailId: expect.any(String),
rule: expect.stringContaining("Newsletter"),
}),
expect.objectContaining({
emailId: expect.any(String),
rule: expect.stringContaining("Reply Required"),
}),
expect.objectContaining({
emailId: expect.any(String),
rule: expect.stringContaining("calendar link"),
}),
]),
}),
);

expect(result.matches).toHaveLength(3);
expect(findExampleMatchesSchema.safeParse(result).success).toBe(true);
expect(queryBatchMessages).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
maxResults: expect.any(Number),
}),
);
}, 15000); // Increased timeout for AI call
});
9 changes: 4 additions & 5 deletions apps/web/app/(app)/automation/RulesPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const examplePrompts = [
'Label pitch decks as "Pitch Deck" and forward them to [email protected]',
"Reply to cold emails by telling them to check out Inbox Zero. Then mark them as spam",
'Label high priority emails as "High Priority"',
"If a founder asks to set up a call, send them my Cal link: https://cal.com/max",
"If a founder asks to set up a call, send them my Cal link: https://cal.com/example",
"If someone asks to cancel a plan, ask to set up a call by sending my Cal link",
'If a founder sends me an investor update, label it "Investor Update" and archive it',
'If someone pitches me their startup, label it as "Investing", archive it, and respond with a friendly reply that I no longer have time to look at the email but if they get a warm intro, that\'s their best bet to get funding from me',
Expand Down Expand Up @@ -151,7 +151,7 @@ function RulesPromptForm({
<div className="sm:col-span-2">
<CardHeader>
<CardTitle>
How your AI personal assistant should handle your emails
How your AI personal assistant should handle incoming emails
</CardTitle>
<CardDescription>
Write a prompt for your assistant to follow.
Expand All @@ -176,9 +176,8 @@ Feel free to add as many as you want:
* Archive all marketing emails.
* Label receipts as "Receipt" and forward them to [email protected].
* Label emails that require a reply as "Reply Required".
* If a customer asks to set up a call, send them my Cal link: https://cal.com/max
* Review any emails from [email protected] and see if any are about finance. If so, respond with a friendly draft a reply that answers the question.
`}
* If a customer asks to set up a call, send them my Cal link: https://cal.com/example
* Review any emails from [email protected] and see if any are about finance. If so, respond with a friendly draft a reply that answers the question.`}
/>

<div className="flex gap-2">
Expand Down
26 changes: 12 additions & 14 deletions apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeMobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,15 @@ export function BulkUnsubscribeRowMobile({
mutate,
posthog,
});
const { unsubscribeLoading, onUnsubscribe } = useUnsubscribe({
item,
hasUnsubscribeAccess,
mutate,
posthog,
refetchPremium,
});
const { unsubscribeLoading, onUnsubscribe, unsubscribeLink } = useUnsubscribe(
{
item,
hasUnsubscribeAccess,
mutate,
refetchPremium,
posthog,
},
);
const { archiveAllLoading, onArchiveAll } = useArchiveAll({
item,
posthog,
Expand Down Expand Up @@ -117,15 +119,11 @@ export function BulkUnsubscribeRowMobile({
variant={
item.status === NewsletterStatus.UNSUBSCRIBED ? "red" : "default"
}
asChild={!!item.lastUnsubscribeLink}
asChild
>
<Link
href={
hasUnsubscribeAccess && item.lastUnsubscribeLink
? cleanUnsubscribeLink(item.lastUnsubscribeLink) || "#"
: "#"
}
target="_blank"
href={unsubscribeLink}
target={unsubscribeLink !== "#" ? "_blank" : undefined}
onClick={onUnsubscribe}
rel="noreferrer"
>
Expand Down
28 changes: 12 additions & 16 deletions apps/web/app/(app)/bulk-unsubscribe/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,18 +157,18 @@ function UnsubscribeButton<T extends Row>({
item: T;
hasUnsubscribeAccess: boolean;
mutate: () => Promise<void>;
posthog: PostHog;
refetchPremium: () => Promise<any>;
posthog: PostHog;
}) {
const { unsubscribeLoading, onUnsubscribe } = useUnsubscribe({
item,
hasUnsubscribeAccess,
mutate,
posthog,
refetchPremium,
});

const isLink = hasUnsubscribeAccess && item.lastUnsubscribeLink;
const { unsubscribeLoading, onUnsubscribe, unsubscribeLink } = useUnsubscribe(
{
item,
hasUnsubscribeAccess,
mutate,
posthog,
refetchPremium,
},
);

return (
<Button
Expand All @@ -179,12 +179,8 @@ function UnsubscribeButton<T extends Row>({
asChild
>
<Link
href={
isLink
? cleanUnsubscribeLink(item.lastUnsubscribeLink || "") || ""
: ""
}
target={isLink ? "_blank" : undefined}
href={unsubscribeLink}
target={unsubscribeLink !== "#" ? "_blank" : undefined}
onClick={onUnsubscribe}
rel="noreferrer"
>
Expand Down
4 changes: 4 additions & 0 deletions apps/web/app/(app)/bulk-unsubscribe/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ export function useUnsubscribe<T extends Row>({
return {
unsubscribeLoading,
onUnsubscribe,
unsubscribeLink:
hasUnsubscribeAccess && item.lastUnsubscribeLink
? cleanUnsubscribeLink(item.lastUnsubscribeLink) || "#"
: "#",
elie222 marked this conversation as resolved.
Show resolved Hide resolved
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type SubmitHandler, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenIcon } from "lucide-react";
import { Modal, useModal } from "@/components/Modal";
import { Button } from "@/components/Button";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/Input";
import {
type UpdateColdEmailSettingsBody,
Expand All @@ -29,7 +29,7 @@ export function ColdEmailPromptModal(props: {

return (
<>
<Button onClick={openModal} type="button" color="white">
<Button onClick={openModal} type="button" variant="outline">
<PenIcon className="mr-2 h-4 w-4" />
Edit Prompt
</Button>
Expand Down Expand Up @@ -102,7 +102,7 @@ function ColdEmailPromptForm(props: {
/>

<div className="mt-2">
<Button type="submit" size="lg" loading={isSubmitting}>
<Button type="submit" loading={isSubmitting}>
Save
</Button>
</div>
Expand Down
Loading