From fc4537925d96d3a28898d4007ef063158a51b18b Mon Sep 17 00:00:00 2001 From: Aamir Azad <82281117+aamirazad@users.noreply.github.com> Date: Mon, 8 Jul 2024 21:56:11 -0400 Subject: [PATCH] feat: add whishper integration (#62) Fixes #63 ### TL;DR ### What changed? ### Why? --- src/app/actions.ts | 42 +++++- src/app/paperless/page.tsx | 6 +- src/app/settings/page.tsx | 69 ++++++++++ src/app/whishper/page.tsx | 203 +++++++++++++++++++++++++++++ src/components/document-viewer.tsx | 3 +- src/components/topnav.tsx | 17 +++ src/server/db/schema.ts | 3 +- src/types/index.ts | 30 +++++ 8 files changed, 367 insertions(+), 6 deletions(-) create mode 100644 src/app/whishper/page.tsx diff --git a/src/app/actions.ts b/src/app/actions.ts index 3908076..acac180 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -3,10 +3,18 @@ import { db } from "@/server/db"; import type { UsersTableType } from "@/server/db/schema"; import { users } from "@/server/db/schema"; -import type { PaperlessDocumentsType } from "@/types"; +import type { PaperlessDocumentsType, WhishperRecordingsType } from "@/types"; import { auth } from "@clerk/nextjs/server"; import type { AdviceAPIType } from "@/types"; +/* +Clerk helpers + ___| | ___ _ __| | __ + / __| |/ _ | '__| |/ / +| (__| | __| | | < + \___|_|\___|_| |_|\_\ +*/ + export async function setUserProperty( propertyName: K, value: UsersTableType[K], @@ -44,6 +52,15 @@ export async function getUserData() { return userData; } +/* +Paperless +| _ \ __ _ _ __ ___ _ __| | ___ ___ ___ +| |_) / _` | '_ \ / _ | '__| |/ _ / __/ __| +| __| (_| | |_) | __| | | | __\__ \__ \ +|_| \__,_| .__/ \___|_| |_|\___|___|___/ + |_| +*/ + export async function getPaperlessDocuments(query: string) { const userData = await getUserData(); @@ -64,3 +81,26 @@ export async function getPaperlessDocuments(query: string) { return data; } + +/* +Whishper + \ \ / (_)___| |__ _ __ ___ _ __ + \ \ /\ / /| / __| '_ \| '_ \ / _ | '__| + \ V V / | \__ | | | | |_) | __| | + \_/\_/ |_|___|_| |_| .__/ \___|_| + |_| +*/ + +export async function getWhishperRecordings(query: string) { + const userData = await getUserData(); + + if (!query || query == "null" || query.length < 3 || !userData) return null; + + const response = await fetch(`${userData.whishperURL}/api/transcriptions`); + + const data = (await response.json()) as WhishperRecordingsType; + + console.log(data) + + return data; +} diff --git a/src/app/paperless/page.tsx b/src/app/paperless/page.tsx index fa68ac5..bcdb724 100644 --- a/src/app/paperless/page.tsx +++ b/src/app/paperless/page.tsx @@ -131,7 +131,7 @@ function DocumentsPage() { const paperlessDocumentMap = PaperlessDocuments.data.results; - if (paperlessDocumentMap.length === 0) { + if (!paperlessDocumentMap ?? paperlessDocumentMap.length === 0) { return

No results!

; } @@ -165,7 +165,9 @@ export default function PaperlessPage() {
- +
+ +
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index c31b715..756bf29 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -171,6 +171,70 @@ function PaperlessToken({ setActiveTab, userData }: FormProps) { ); } +function WhishperURL({ setActiveTab, userData }: FormProps) { + const [isAutofilled, setIsAutofilled] = useState(false); + const formSchema = z.object({ + URL: z.string(), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + URL: "", + }, + }); + + if (userData.whishperURL && !isAutofilled) { + form.setValue("URL", userData.whishperURL); + setIsAutofilled(true); + } + + async function onSubmit(values: z.infer) { + if (values.URL == "") { + setActiveTab((prevTab) => prevTab + 2); // Skip api key form + } else { + setActiveTab((prevTab) => prevTab + 1); // Increment activeTab + } + try { + await setUserProperty("whishperURL", values.URL); + // Operation succeeded, show success toast + toast("Your whishper URL preferences was saved"); + // Optionally, move to a new tab or take another action to indicate success + } catch { + // Operation failed, show error toast + toast("Uh oh! Something went wrong.", { + description: "Your whishper URL preferences were not saved.", + action: { + label: "Go back", + onClick: () => setActiveTab((prevTab) => prevTab - 1), // Go back to try again + }, + }); + } + } + + return ( +
+ + ( + + Whishper URL + + + + Leave empty to disable + + + )} + /> + + + + ); +} + interface ProgressIndicatorProps { activeTab: number; totalTabs: number; @@ -227,6 +291,11 @@ export function Forms() { setActiveTab={setActiveTab} userData={userData} />, + , ]; return ( <> diff --git a/src/app/whishper/page.tsx b/src/app/whishper/page.tsx new file mode 100644 index 0000000..a757f36 --- /dev/null +++ b/src/app/whishper/page.tsx @@ -0,0 +1,203 @@ +"use client"; + +import { SignedIn, SignedOut } from "@clerk/nextjs"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { array, z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import Link from "next/link"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useCallback } from "react"; +import { + useQuery, + QueryClientProvider, + QueryClient, +} from "@tanstack/react-query"; +import { getUserData } from "../actions"; +import LoadingSpinner from "@/components/loading-spinner"; +import type { WhishperRecordingsType } from "@/types"; +import OpenInternalLink from "@/components/internal-link"; +import OpenExternalLInk from "@/components/external-link"; + +const queryClient = new QueryClient(); + +async function getWhishperRecordings(query: string) { + const userData = await getUserData(); + + if (!query || query == "null" || query.length < 3 || !userData) return null; + + const response = await fetch(`${userData.whishperURL}/api/transcriptions`); + + const data = (await response.json()) as WhishperRecordingsType; + const lowerCaseQuery = query.toLowerCase(); + return data.filter( + (item) => + item.fileName.toLowerCase().includes(lowerCaseQuery) || + item.result.text.toLowerCase().includes(lowerCaseQuery), + ); +} + +function SearchForm() { + const formSchema = z.object({ + query: z.string().min(3).max(256), + }); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const givenQuery = searchParams.get("query") ?? ""; + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + query: givenQuery, + }, + }); + + const createQueryString = useCallback( + (name: string, value: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.set(name, value); + + return params.toString(); + }, + [searchParams], + ); + + function onSubmit(values: z.infer) { + if (values.query) + router.replace(pathname + "?" + createQueryString("query", values.query)); + } + + return ( +
+ + ( + + Search for your voice recordings + + + + + + )} + /> + + + + ); +} + +function RecordingsList() { + const searchParams = useSearchParams(); + const query = searchParams.get("query"); + + const WhishperRecordings = useQuery({ + queryKey: ["key", query], + queryFn: async () => { + if (!query) { + return Promise.resolve(null); + } + const response = await getWhishperRecordings(query); + return response; + }, + // This ensures the query does not run if there's no query string + enabled: !!query, + }); + + const userData = useQuery({ + queryKey: ["userData"], + queryFn: async () => { + const data = await getUserData(); + return data; + }, + }); + + if (!query) { + return

Start Searching!

; + } + + if (!userData.data?.whishperURL) { + return ( +

+ You need to set your whishper url in{" "} + settings +

+ ); + } + + if (WhishperRecordings.isLoading || userData.isLoading) { + return Loading...; + } + + if (!WhishperRecordings.data || WhishperRecordings.error) { + return ( +

+ Connection failed! Check that the whishper url is set correctly in{" "} + settings +

+ ); + } + + const WhishperRecordingsMap = WhishperRecordings.data; + + if (!WhishperRecordingsMap ?? WhishperRecordingsMap.length === 0) { + return

No results!

; + } + + return ( + <> +

Search Results

+
    + {WhishperRecordingsMap.map((recording, index) => ( +
  • + + {recording.fileName.split("_WHSHPR_")[1]} + +
  • + ))} +
+ + ); +} + +export default function WhishperPage() { + return ( +
+
+ +
+ Please sign in +
+
+ +
+
+ +
+
+ + + +
+
+
+
+
+ ); +} diff --git a/src/components/document-viewer.tsx b/src/components/document-viewer.tsx index a86fbf5..d5cfeac 100644 --- a/src/components/document-viewer.tsx +++ b/src/components/document-viewer.tsx @@ -3,12 +3,11 @@ import { useState, useEffect, useRef } from "react"; import { Button } from "./ui/button"; import { useRouter } from "next/navigation"; -import { getAdvice, getUserData } from "@/app/actions"; +import { getUserData } from "@/app/actions"; import { useQuery, QueryClientProvider, QueryClient, - useQueryClient, } from "@tanstack/react-query"; import type { AdviceAPIType } from "@/types"; import OpenInternalLink from "./internal-link"; diff --git a/src/components/topnav.tsx b/src/components/topnav.tsx index 15e63f8..7fe365a 100644 --- a/src/components/topnav.tsx +++ b/src/components/topnav.tsx @@ -130,6 +130,13 @@ export function TopNav() { Paperless-ngx + + Whishper + +
@@ -160,6 +167,16 @@ export function TopNav() { Paperless-ngx + + + + Whishper + + + diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 5104c6a..022c5e2 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -18,6 +18,7 @@ export const users = createTable("users", { userId: varchar("userId", { length: 256 }).notNull().unique(), paperlessURL: varchar("paperlessURL", { length: 256 }), paperlessToken: varchar("paperlessToken", { length: 256 }), + whishperURL: varchar("whishperURL", { length: 256 }), }); -export type UsersTableType = typeof users.$inferSelect; \ No newline at end of file +export type UsersTableType = typeof users.$inferSelect; diff --git a/src/types/index.ts b/src/types/index.ts index ed86760..bd1bb75 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -68,6 +68,36 @@ export type PaperlessDocumentsType = { }[]; }; +export type WhishperRecordingsType = { + id: string; + status: number; + language: string; + modelSize: string; + task: string; + device: string; + fileName: string; + sourceUrl: string; + result: { + language: string; + duration: number; + segments: { + end: number; + id: string; + start: number; + score: number; + text: string; + words: { + end: number; + start: number; + word: string; + score: number; + }[]; + }[]; + text: string; + }; + translations: []; +}[]; + export type AdviceAPIType = { slip: { slip_id: number;