Skip to content

Commit

Permalink
Merge pull request #159 from midday-ai/feature/inbox-batch-upload
Browse files Browse the repository at this point in the history
Batch upload
  • Loading branch information
pontusab committed Jun 16, 2024
2 parents 5979eae + 67662dc commit d52ce3e
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 28 deletions.
1 change: 1 addition & 0 deletions apps/dashboard/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
NEXT_PUBLIC_SUPABASE_ID=
SUPABASE_SERVICE_KEY=
SUPABASE_API_KEY=
RESEND_API_KEY=
LOOPS_ENDPOINT=
LOOPS_API_KEY=
Expand Down
3 changes: 1 addition & 2 deletions apps/dashboard/README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
## Dashboard
##
## Dashboard
8 changes: 4 additions & 4 deletions apps/dashboard/src/components/inbox-empty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ type Props = {
export function InboxEmpty({ inboxId }: Props) {
return (
<div className="h-[calc(100vh-150px)] flex items-center justify-center">
<div className="flex flex-col items-center w-[330px]">
<div className="flex flex-col items-center max-w-[380px] w-full">
<Icons.InboxEmpty className="mb-4 w-[35px] h-[35px]" />
<div className="text-center mb-6 space-y-2">
<h2 className="font-medium text-lg">Magic Inbox</h2>
<p className="text-[#606060] text-sm">
Use this email to send invoices and receipts to Midday. We will
extract and reconcile them against transactions. Additionally, you
can search based on the information within them.
Use the email to send receipts to Midday. We will extract and
reconcile them against your transactions. Additionally, you can also
upload receipts by simply dragging and dropping them here.
<br />
</p>
</div>
Expand Down
5 changes: 4 additions & 1 deletion apps/dashboard/src/components/inbox-header.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { TabsList, TabsTrigger } from "@midday/ui/tabs";
import { parseAsString, useQueryStates } from "nuqs";
import { startTransition } from "react";
import { InboxOrdering } from "./inbox-ordering";
import { InboxSearch } from "./inbox-search";
import { InboxSettingsModal } from "./modals/inbox-settings-modal";
import { startTransition } from "react";
import { UploadButton } from "./tables/vault/upload-button";

type Props = {
forwardEmail: string;
Expand Down Expand Up @@ -56,6 +57,8 @@ export function InboxHeader({
inboxId={inboxId}
inboxForwarding={inboxForwarding}
/>

<UploadButton />
</div>
</div>
);
Expand Down
155 changes: 155 additions & 0 deletions apps/dashboard/src/components/inbox-upload-zone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"use client";

import { invalidateCacheAction } from "@/actions/invalidate-cache-action";
import { resumableUpload } from "@/utils/upload";
import { createClient } from "@midday/supabase/client";
import { cn } from "@midday/ui/cn";
import { useToast } from "@midday/ui/use-toast";
import { nanoid } from "nanoid";
import { type ReactNode, useEffect, useRef, useState } from "react";
import { useDropzone } from "react-dropzone";

type Props = {
teamId: string;
children: ReactNode;
};

export function UploadZone({ children, teamId }: Props) {
const supabase = createClient();
const [progress, setProgress] = useState(0);
const [showProgress, setShowProgress] = useState(false);
const [toastId, setToastId] = useState(null);
const uploadProgress = useRef([]);
const { toast, dismiss, update } = useToast();

useEffect(() => {
if (!toastId && showProgress) {
const { id } = toast({
title: `Uploading ${uploadProgress.current.length} files`,
progress,
variant: "progress",
description: "Please do not close browser until completed",
duration: Infinity,
});

setToastId(id);
} else {
update(toastId, {
progress,
title: `Uploading ${uploadProgress.current.length} files`,
});
}
}, [showProgress, progress, toastId]);

const onDrop = async (files) => {
// NOTE: If onDropRejected
if (!files.length) {
return;
}

// Set default progress
uploadProgress.current = files.map(() => 0);

setShowProgress(true);

// Add uploaded folder so we can filter background job on this
const filePath = [teamId, nanoid(), "inbox", "uploaded"];

try {
await Promise.all(
files.map(async (file, idx) =>
resumableUpload(supabase, {
bucket: "vault",
path: filePath,
file,
onProgress: (bytesUploaded, bytesTotal) => {
uploadProgress.current[idx] = (bytesUploaded / bytesTotal) * 100;

const _progress = uploadProgress.current.reduce(
(acc, currentValue) => {
return acc + currentValue;
},
0
);

setProgress(Math.round(_progress / files.length));
},
})
)
);

// Reset once done
uploadProgress.current = [];

setProgress(0);
toast({
title: "Upload successful.",
variant: "success",
duration: 2000,
});

setShowProgress(false);
setToastId(null);
dismiss(toastId);
invalidateCacheAction([`vault_${teamId}`]);
} catch {
toast({
duration: 2500,
variant: "error",
title: "Something went wrong please try again.",
});
}
};

const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
onDropRejected: ([reject]) => {
if (reject?.errors.find(({ code }) => code === "file-too-large")) {
toast({
duration: 2500,
variant: "error",
title: "File size to large.",
});
}

if (reject?.errors.find(({ code }) => code === "file-invalid-type")) {
toast({
duration: 2500,
variant: "error",
title: "File type not supported.",
});
}
},
maxSize: 3000000, // 3MB
maxFiles: 10,
accept: {
"image/png": [".png"],
"image/jpeg": [".jpg", ".jpeg"],
"application/pdf": [".pdf"],
},
});

return (
<div
{...getRootProps({ onClick: (evt) => evt.stopPropagation() })}
className="relative h-full"
>
<div className="absolute top-0 bottom-0 right-0 left-0 z-[51] pointer-events-none">
<div
className={cn(
"bg-background dark:bg-[#1A1A1A] h-full flex items-center justify-center text-center invisible",
isDragActive && "visible"
)}
>
<input {...getInputProps()} id="upload-files" />
<p className="text-xs">
Drop your receipts here. <br />
Maximum of 10 files at a time.
</p>
</div>
</div>

{children}
</div>
);
}
31 changes: 20 additions & 11 deletions apps/dashboard/src/components/inbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getUser } from "@midday/supabase/cached-queries";
import { getInboxQuery } from "@midday/supabase/queries";
import { createClient } from "@midday/supabase/server";
import { InboxEmpty } from "./inbox-empty";
import { UploadZone } from "./inbox-upload-zone";
import { InboxView } from "./inbox-view";

type Props = {
Expand All @@ -13,25 +14,33 @@ export async function Inbox({ ascending, query }: Props) {
const user = await getUser();
const supabase = createClient();

const teamId = user?.data?.team_id as string;

const inbox = await getInboxQuery(supabase, {
to: 10000,
teamId: user.data.team_id,
teamId,
ascending,
});

if (!inbox?.data?.length && !query) {
return <InboxEmpty inboxId={user?.data?.team?.inbox_id} />;
return (
<UploadZone teamId={teamId}>
<InboxEmpty inboxId={user?.data?.team?.inbox_id} />
</UploadZone>
);
}

return (
<InboxView
items={inbox?.data}
teamId={user?.data?.team?.id}
inboxId={user?.data?.team?.inbox_id}
forwardEmail={user?.data?.team?.inbox_email}
inboxForwarding={user?.data?.team?.inbox_forwarding}
ascending={ascending}
query={query}
/>
<UploadZone teamId={teamId}>
<InboxView
items={inbox?.data}
teamId={teamId}
inboxId={user?.data?.team?.inbox_id}
forwardEmail={user?.data?.team?.inbox_email}
inboxForwarding={user?.data?.team?.inbox_forwarding}
ascending={ascending}
query={query}
/>
</UploadZone>
);
}
6 changes: 5 additions & 1 deletion apps/dashboard/src/components/tables/vault/upload-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import { Button } from "@midday/ui/button";

export function UploadButton({ disableActions }) {
type Props = {
disableActions?: boolean;
};

export function UploadButton({ disableActions }: Props) {
return (
<Button
variant="outline"
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const env = createEnv({
OPENAI_API_KEY: z.string(),
GROQ_API_KEY: z.string(),
SUPABASE_SERVICE_KEY: z.string(),
SUPABASE_API_KEY: z.string(),
UPSTASH_REDIS_REST_TOKEN: z.string(),
UPSTASH_REDIS_REST_URL: z.string(),
LOOPS_ENDPOINT: z.string(),
Expand Down Expand Up @@ -73,6 +74,7 @@ export const env = createEnv({
process.env.NEXT_PUBLIC_NOVU_APPLICATION_IDENTIFIER,
NEXT_PUBLIC_TRIGGER_API_KEY: process.env.NEXT_PUBLIC_TRIGGER_API_KEY,
SUPABASE_SERVICE_KEY: process.env.SUPABASE_SERVICE_KEY,
SUPABASE_API_KEY: process.env.SUPABASE_API_KEY,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
GROQ_API_KEY: process.env.GROQ_API_KEY,
API_ROUTE_SECRET: process.env.API_ROUTE_SECRET,
Expand Down
5 changes: 0 additions & 5 deletions apps/website/src/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ import {
AccordionTrigger,
} from "@midday/ui/accordion";
import { cn } from "@midday/ui/cn";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@midday/ui/collapsible";
import {
ContextMenu,
ContextMenuContent,
Expand Down
Binary file modified bun.lockb
Binary file not shown.
4 changes: 2 additions & 2 deletions packages/jobs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@fast-csv/format": "5.0.0",
"@langchain/community": "^0.2.10",
"@langchain/core": "^0.2.6",
"@langchain/openai": "^0.1.3",
Expand All @@ -18,8 +19,7 @@
"@trigger.dev/react": "2.3.19",
"@trigger.dev/resend": "2.3.19",
"@trigger.dev/sdk": "2.3.19",
"@trigger.dev/supabase": "2.3.19",
"@fast-csv/format": "5.0.0",
"@trigger.dev/supabase": "^2.3.19",
"change-case": "^5.4.4",
"langchain": "^0.2.5",
"nanoid": "^5.0.7"
Expand Down
11 changes: 10 additions & 1 deletion packages/jobs/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Database } from "@midday/supabase/src/types";
import { Resend } from "@trigger.dev/resend";
import { TriggerClient } from "@trigger.dev/sdk";
import { Supabase } from "@trigger.dev/supabase";
import { Supabase, SupabaseManagement } from "@trigger.dev/supabase";

export const client = new TriggerClient({
id: "midday-G6Yq",
Expand All @@ -15,6 +15,15 @@ export const supabase = new Supabase<Database>({
supabaseKey: process.env.SUPABASE_SERVICE_KEY!,
});

const supabaseManagement = new SupabaseManagement({
id: "supabase-management",
apiKey: process.env.SUPABASE_API_KEY!,
});

export const db = supabaseManagement.db(
`https://${process.env.NEXT_PUBLIC_SUPABASE_ID}.supabase.co`
);

export const resend = new Resend({
id: "resend",
apiKey: process.env.RESEND_API_KEY!,
Expand Down
3 changes: 2 additions & 1 deletion packages/jobs/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const Jobs = {
TRANSACTIONS_SETUP: "transactions-setup",
INBOX_DOCUMENT: "inbox-document",
INBOX_MATCH: "inbox-match",
INBOX_UPLOAD: "inbox-upload",
ONBOARDING_EMAILS: "onboarding-emails",
TRANSACTIONS_INITIAL_SYNC: "transactions-initial-sync",
TRANSACTIONS_MANUAL_SYNC: "transactions-manual-sync",
Expand All @@ -18,7 +19,7 @@ export const Events = {
TRANSACTIONS_SETUP: "transactions.setup",
INBOX_DOCUMENT: "inbox.document",
INBOX_MATCH: "inbox.match",
ONBOARDING_EMAILS: "onboarding-emails",
ONBOARDING_EMAILS: "onboarding.emails",
TRANSACTIONS_INITIAL_SYNC: "transactions.initial.sync",
TRANSACTIONS_MANUAL_SYNC: "transactions.manual.sync",
TRANSACTIONS_IMPORT: "transactions.import",
Expand Down
1 change: 1 addition & 0 deletions packages/jobs/src/inbox/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./match";
export * from "./document";
export * from "./upload";
Loading

0 comments on commit d52ce3e

Please sign in to comment.