Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 6 additions & 6 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@
"check-types": "next typegen && tsc --noEmit"
},
"dependencies": {
"@pilot/core": "workspace:*",
"@pilot/db": "workspace:*",
"@pilot/instagram": "workspace:*",
"@pilot/types": "workspace:*",
"@pilot/ui": "workspace:*",
"@ai-sdk/google": "^2.0.54",
"@ai-sdk/react": "^2.0.139",
"@hookform/resolvers": "^5.2.2",
"@inngest/realtime": "^0.3.3",
"@neondatabase/serverless": "^1.0.2",
"@opentelemetry/winston-transport": "^0.16.2",
"@origin-space/image-cropper": "^0.1.9",
"@pilot/core": "workspace:*",
"@pilot/db": "workspace:*",
"@pilot/instagram": "workspace:*",
"@pilot/types": "workspace:*",
"@pilot/ui": "workspace:*",
"@polar-sh/better-auth": "^1.8.1",
"@polar-sh/nextjs": "^0.4.11",
"@polar-sh/sdk": "^0.34.17",
Expand All @@ -50,7 +50,7 @@
"dotenv": "^17.3.1",
"drizzle-orm": "^0.44.7",
"import-in-the-middle": "^2.0.6",
"inngest": "^3.52.2",
"inngest": "^4.4.0",
"lucide-react": "^0.542.0",
"motion": "^12.34.3",
"next": "16.1.6",
Expand Down
69 changes: 44 additions & 25 deletions apps/app/src/actions/sidekick/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { and, eq } from "drizzle-orm";
import { enqueueBusinessKnowledgeSync } from "@/lib/supermemory/events";
import {
getSidekickSetupStatusByUserId,
SIDEKICK_SETUP_STEPS,
} from "@pilot/core/sidekick/personalization";

export type SidekickOnboardingData = {
offerLinks?: {
Expand All @@ -39,7 +43,7 @@ export type SidekickOnboardingData = {
};

export async function updateSidekickOnboardingData(
data: SidekickOnboardingData
data: SidekickOnboardingData,
) {
const session = await auth.api.getSession({
headers: await headers(),
Expand Down Expand Up @@ -69,8 +73,8 @@ export async function updateSidekickOnboardingData(
and(
eq(userOfferLink.userId, session.user.id),
eq(userOfferLink.type, link.type),
eq(userOfferLink.url, link.url)
)
eq(userOfferLink.url, link.url),
),
);

if (existingLinks.length === 0) {
Expand All @@ -93,8 +97,8 @@ export async function updateSidekickOnboardingData(
and(
eq(userOffer.userId, session.user.id),
eq(userOffer.name, offer.name),
eq(userOffer.content, offer.content)
)
eq(userOffer.content, offer.content),
),
);

if (existingOffers.length === 0) {
Expand All @@ -117,8 +121,8 @@ export async function updateSidekickOnboardingData(
.where(
and(
eq(userFaq.userId, session.user.id),
eq(userFaq.question, faq.question)
)
eq(userFaq.question, faq.question),
),
);

if (existingFaqs.length === 0) {
Expand Down Expand Up @@ -195,7 +199,7 @@ export async function deleteOffer(offerId: string) {
await db
.delete(userOffer)
.where(
and(eq(userOffer.id, offerId), eq(userOffer.userId, session.user.id))
and(eq(userOffer.id, offerId), eq(userOffer.userId, session.user.id)),
);

await enqueueBusinessKnowledgeSync(session.user.id, "deleteOffer");
Expand Down Expand Up @@ -228,8 +232,8 @@ export async function saveSidekickOfferLink(linkData: {
and(
eq(userOfferLink.url, linkData.url),
eq(userOfferLink.userId, session.user.id),
eq(userOfferLink.type, linkData.type)
)
eq(userOfferLink.type, linkData.type),
),
);

if (existingLinks.length === 0) {
Expand Down Expand Up @@ -322,8 +326,8 @@ export async function saveSidekickOffer(offerData: {
and(
eq(userOffer.userId, session.user.id),
eq(userOffer.name, offerData.name),
eq(userOffer.content, offerData.content)
)
eq(userOffer.content, offerData.content),
),
);

if (existingOffers.length === 0) {
Expand All @@ -336,10 +340,7 @@ export async function saveSidekickOffer(offerData: {
});
}

await enqueueBusinessKnowledgeSync(
session.user.id,
"saveSidekickOffer",
);
await enqueueBusinessKnowledgeSync(session.user.id, "saveSidekickOffer");

return { success: true };
} catch (error) {
Expand Down Expand Up @@ -582,22 +583,40 @@ export async function checkSidekickOnboardingStatus() {

try {
const db = await getRLSDb();
const userData = await db
.select({
sidekick_onboarding_complete: user.sidekick_onboarding_complete,
})
.from(user)
.where(eq(user.id, session.user.id))
.then((res) => res[0]);
const result = await getSidekickSetupStatusByUserId(db, session.user.id);

if (result.success) {
return result.data;
}

return {
sidekick_onboarding_complete:
userData?.sidekick_onboarding_complete || false,
sidekick_onboarding_complete: false,
isReady: false,
resumeStep: 0,
resumeHref: "/sidekick-onboarding?step=0",
completedSteps: 0,
totalSteps: SIDEKICK_SETUP_STEPS.length,
missing: ["Sidekick setup data"],
steps: SIDEKICK_SETUP_STEPS.map((step) => ({
...step,
complete: false,
})),
error: result.error,
};
} catch (error) {
console.error("Error checking onboarding status:", error);
return {
sidekick_onboarding_complete: false,
isReady: false,
resumeStep: 0,
resumeHref: "/sidekick-onboarding?step=0",
completedSteps: 0,
totalSteps: SIDEKICK_SETUP_STEPS.length,
missing: ["Sidekick setup data"],
steps: SIDEKICK_SETUP_STEPS.map((step) => ({
...step,
complete: false,
})),
error: "Failed to check onboarding status",
};
}
Expand Down
102 changes: 66 additions & 36 deletions apps/app/src/app/(dashboard)/(workspace)/automations/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,50 @@
import { Suspense } from "react";
import { Button } from "@pilot/ui/components/button";
import { Plus } from "lucide-react";
import { LockKeyhole, Plus } from "lucide-react";
import Link from "next/link";
import AutomationsList from "@/components/automations/list";
import AutomationsLogs from "@/components/automations/logs";
import { Skeleton } from "@pilot/ui/components/skeleton";
import { SidekickLayout } from "@/components/sidekick/layout";
import { getUser } from "@/lib/auth-utils";
import { getBillingStatus } from "@/lib/billing/enforce";
import { getInstagramIntegration } from "@/actions/instagram";

function InstagramLockedAutomations() {
return (
<div className="rounded-lg border bg-muted/30 p-8 text-center">
<LockKeyhole
className="mx-auto mb-3 size-8 text-muted-foreground"
aria-hidden="true"
/>
<h2 className="text-balance text-lg font-semibold">
Automations are locked
</h2>
<p className="mx-auto mt-2 max-w-2xl text-pretty text-sm text-muted-foreground">
Once Instagram is connected, Pilot can safely load automation rules,
logs, and create flows that send replies from the right account.
</p>
</div>
);
}

export default async function AutomationsPage() {
const user = await getUser();
const billingStatus = user ? await getBillingStatus(user.id) : null;
const [user, instagram] = await Promise.all([
getUser(),
getInstagramIntegration(),
]);
const billingStatus =
user && instagram.connected ? await getBillingStatus(user.id) : null;
const isFrozen = billingStatus?.flags.isStructurallyFrozen ?? false;
const canCreateAutomation = billingStatus?.flags.canCreateAutomation ?? false;
const canCreateAutomation =
instagram.connected && (billingStatus?.flags.canCreateAutomation ?? false);

return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold font-heading">Automations</h1>
<p className="text-muted-foreground">
<p className="text-pretty text-muted-foreground">
Build automated replies for common DM and comment questions.
</p>
</div>
Expand All @@ -40,42 +64,48 @@ export default async function AutomationsPage() {
</div>

{isFrozen && (
<p className="text-sm text-muted-foreground">
Your workspace is frozen because it is above the current plan cap. Existing automations remain visible, but changes are disabled until usage is reduced or the plan is upgraded.
<p className="text-pretty text-sm text-muted-foreground">
Your workspace is frozen because it is above the current plan cap.
Existing automations remain visible, but changes are disabled until
usage is reduced or the plan is upgraded.
</p>
)}

<SidekickLayout>
<Suspense
fallback={
<div className="w-full max-w-xl">
<div className="space-y-3">
<Skeleton className="h-6 w-1/3" />
<Skeleton className="h-4 w-1/2" />
</div>
<div className="mt-4 space-y-3">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
{instagram.connected ? (
<SidekickLayout>
<Suspense
fallback={
<div className="w-full max-w-xl">
<div className="space-y-3">
<Skeleton className="h-6 w-1/3" />
<Skeleton className="h-4 w-1/2" />
</div>
<div className="mt-4 space-y-3">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
</div>
</div>
</div>
}
>
<AutomationsLogs />
</Suspense>
}
>
<AutomationsLogs />
</Suspense>

<Suspense
fallback={
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 w-full">
<Skeleton className="h-48 w-full" />
<Skeleton className="h-48 w-full" />
<Skeleton className="h-48 w-full" />
</div>
}
>
<AutomationsList />
</Suspense>
</SidekickLayout>
<Suspense
fallback={
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 w-full">
<Skeleton className="h-48 w-full" />
<Skeleton className="h-48 w-full" />
<Skeleton className="h-48 w-full" />
</div>
}
>
<AutomationsList />
</Suspense>
</SidekickLayout>
) : (
<InstagramLockedAutomations />
)}
</div>
);
}
51 changes: 42 additions & 9 deletions apps/app/src/app/(dashboard)/(workspace)/contacts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
import { getInstagramIntegration } from "@/actions/instagram";
import { fetchContacts } from "@/actions/contacts";
import ContactsTable from "@/components/contacts/contacts-table";
import { LockKeyhole } from "lucide-react";

export const dynamic = "force-dynamic";

function InstagramLockedContacts() {
return (
<div className="rounded-lg border bg-muted/30 p-8 text-center">
<LockKeyhole
className="mx-auto mb-3 size-8 text-muted-foreground"
aria-hidden="true"
/>
<h2 className="text-balance text-lg font-semibold">
Contacts are locked
</h2>
<p className="mx-auto mt-2 max-w-2xl text-pretty text-sm text-muted-foreground">
Once Instagram is connected, Pilot can safely fetch synced contacts,
notes, tags, and follow-up state.
</p>
</div>
);
}

export default async function ContactsPage() {
const instagram = await getInstagramIntegration();

let contacts = null;
let hasError = false;
try {
contacts = await fetchContacts();
} catch (error) {
console.error("Error in ContactsPage:", error);
hasError = true;

if (instagram.connected) {
try {
contacts = await fetchContacts();
} catch (error) {
console.error("Error in ContactsPage:", error);
hasError = true;
}
}

if (hasError) {
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold font-heading tracking-tight">Contacts</h1>
<h1 className="text-3xl font-bold font-heading tracking-tight">
Contacts
</h1>
<p className="text-destructive">
Failed to load contacts. Please try again later.
</p>
Expand All @@ -29,13 +56,19 @@ export default async function ContactsPage() {
return (
<div className="flex flex-col gap-6">
<div>
<h1 className="text-3xl font-bold font-heading tracking-tight">Contacts</h1>
<p className="text-muted-foreground">
<h1 className="text-3xl font-bold font-heading tracking-tight">
Contacts
</h1>
<p className="text-pretty text-muted-foreground">
Keep your leads, notes, tags, and follow-ups in one view.
</p>
</div>

<ContactsTable contacts={contacts!} />
{instagram.connected ? (
<ContactsTable contacts={contacts!} />
) : (
<InstagramLockedContacts />
)}
</div>
);
}
Loading
Loading